ReoGrid ReoGrid Web

JavaScriptスプレッドシートでピボットテーブル ― 数行のAPIで、ライブ更新されるクロス集計を

· unvell team
JavaScriptスプレッドシートでピボットテーブル ― 数行のAPIで、ライブ更新されるクロス集計を

データ検証は不正なレコードをセルの手前で止めました。並べ替えとフィルターは、入ってきたデータの中をユーザーが探し回れるようにしました。次に来るのは、スプレッドシートが最後に必ず聞かれる質問です ― で、合計はいくら? 地域別・商品別の売上。部署別・拠点別の人数。四半期ごとの受注件数。Excel ならピボットテーブルの出番 ― そして ReoGrid Web では(v1.4 以降)、それが API ワンコールで、しかも元データの変更にライブで追随します。

ピボットテーブルは Pro 機能です(料金)。作成・編集には Pro が必要ですが、Pro で作成したドキュメントは無料の Lite でもそのまま表示されます。


ワンコールでピボット

A〜E 列に売上レコードが200行あるとします ― 地域商品四半期チャネル売上、ヘッダーは0行目。これをクロス集計するのはワンコールです。

import { createReogrid } from '@reogrid/pro';

const { worksheet: ws } = createReogrid('#grid');

const pivot = ws.createPivot({
  source: { row: 0, col: 0, rows: 201, columns: 5 }, // A1:E201、先頭行がヘッダー
  anchor: { row: 0, column: 6 },                      // 出力は G1 から
  rows:    [{ field: '地域' }],
  columns: [{ field: '商品' }],
  values:  [{ field: '売上', agg: 'sum', numberFormat: '#,##0' }],
});

G1 を起点に、グリッドがブロックを書き出します ― 地域ごとに1行、商品ごとに1列、各セルはその地域×商品の売上合計。右端に総計列、最下段に総計行が付きます。出力セルはロック済み ― 計算結果の上にユーザーが上書きすることはできません。そしてこのブロックは貼り付けではなくライブなオブジェクトです。ソースの 売上 セルをどれか書き換えれば、影響する合計がその場で再計算されます。

フィールドの参照は列インデックスではなくヘッダー名です。ピボットは source.row からフィールド一覧を読み取るので、{ field: '地域' } は「ヘッダーセルに 地域 と書いてある列」の意味 ― ソースの列を並べ替えても定義はそのまま生きています。


PivotDefinition の解剖

createPivot が受け取るものの全部を一箇所に。

フィールド既定値役割
sourceRangePositionソース範囲。先頭行がフィールドヘッダー
anchor{ row, column }出力ブロックの左上セル
rows{ field }[][]行軸のフィールド(ヘッダー名で指定)
columns{ field }[][]列軸のフィールド
values{ field, agg, caption?, numberFormat? }[]メジャー ― 何を集計するか
filters{ field, include? }[]field の値が include にあるレコードだけを対象に
showRowGrandTotalsbooleantrue右端の総計列
showColumnGrandTotalsbooleantrue最下段の総計行
layout'tabular''tabular'出力レイアウト(v1 は tabular)

各値フィールドの agg は7種類の集計から選びます:'sum''count''countNumbers''average''max''min''product'。紛らわしいのは2つ ― count は空でないレコードすべてを数え、countNumbers は数値のものだけを数えます。Excel の COUNTA と COUNT の関係と同じです。

値フィールドは自分の表示形式を持てますし、同じソース列を2回集計することもできます。

values: [
  { field: '売上', agg: 'sum',     caption: '合計',   numberFormat: '#,##0' },
  { field: '売上', agg: 'average', caption: '平均単価', numberFormat: '#,##0.0' },
],

caption は既定の「Sum of 売上」風の見出しを置き換え、numberFormat はそのメジャーの全値セルに数値書式を適用します ― 合計が 12847 ではなく 12,847 で出てくるわけです。


勝手に追随する ― ほぼ全部

ここが、ピボットオブジェクトと一回きりの「この範囲を集計」ヘルパーの分かれ目です。ワークシートはセルが変わるたびにピボットエンジンへ通知します。変わったセルがどれかのピボットのソース範囲内なら、そのピボットは再計算され、出力ブロックが書き直されます。売上 セルに新しい数字を打ち込めば、その地域の小計・その商品の列合計・総計が、フォーカスを外す前に更新されています。

ピボットが自力で気づけない編集がひとつだけあります ― 構造の変更です。定義は sourceanchor を固定の範囲として保持していて、行や列の挿入・削除ではそれらがずれません。構造を変えたあとは、ソースを読み直すように伝えてください。

ws.insertRows(5, 3);       // 構造の編集 ― 保存された範囲は動かない
pivot.refresh();           // ソース範囲を読み直して再描画

ソース内の値の編集:自動。挿入・削除:refresh() 一発。


コードから操る

createPivotPivotHandle を返します ― ワークシートに委譲するだけの、薄くてステートレスなハンドルです。

pivot.update({ columns: [{ field: '四半期' }] }); // 変更をマージして再計算
pivot.refresh();                                  // ソースから強制再計算
pivot.definition;                                 // 現在の解決済み定義
pivot.bounds;                                     // 出力が占める矩形、なければ null
pivot.remove();                                   // ブロックを消去し、セルのロックを解除

update部分的な定義を受け取ってマージします ― 変えたい部分だけを渡せばいい。これがフィールド選択 UI を安く作れる理由です。<select> を3つ置けば、どの変更も update ワンコールで済みます。

rowSelect.onchange = () =>
  pivot.update({ rows: [{ field: rowSelect.value }] });

aggSelect.onchange = () =>
  pivot.update({ values: [{ field: '売上', agg: aggSelect.value, numberFormat: '#,##0' }] });

bounds は「出力がどこまで広がったか」に答えます ― ブロックを画面内にスクロールさせたり、周囲をスタイリングしたりするのに便利です。そしてハンドルにあるものはすべて、id をキーにワークシート側にも存在します。ハンドルを持っていない場面用に:ws.getPivot(id)ws.getPivots()ws.getPivotBounds(id)ws.updatePivot(id, partial)ws.refreshPivot(id)ws.removePivot(id)


フィルターと総計

レポートフィルターは、どのソースレコードをピボットに入れるかをそもそもの入口で制限します ― Excel の「レポートフィルター」ボックスに相当。include に許可する値を並べます。

const pivot = ws.createPivot({
  source: { row: 0, col: 0, rows: 201, columns: 5 },
  anchor: { row: 0, column: 6 },
  rows:    [{ field: '地域' }],
  columns: [{ field: '四半期' }],
  values:  [{ field: '売上', agg: 'sum', numberFormat: '#,##0' }],
  filters: [{ field: 'チャネル', include: ['オンライン'] }],  // オンライン売上のみ
});

// update ひとつでレポート全体を店頭販売に切り替え
pivot.update({ filters: [{ field: 'チャネル', include: ['店頭'] }] });

ソース表のオートフィルターとは自然に併用できますが、両者は独立です。オートフィルターはユーザーの視界から行を隠すもの、ピボットのフィルターはどのレコードを集計に入れるかを決めるもの。行を隠してもピボットからは消えません。

総計はどちらも既定でオンです。ブロックを詰めたいとき ― たとえばピボットをグラフに食わせるので総計にスケールを歪められたくないとき ― はオフにできます。

pivot.update({ showRowGrandTotals: false, showColumnGrandTotals: false });

実例:売上ダッシュボードの一角

完全なコードです ― A〜E 列に200件の売上レコードを生成し、G 列にピボット、そしてそれをライブで組み替える2つのドロップダウン。

import { createReogrid } from '@reogrid/pro';

const { worksheet: ws } = createReogrid('#grid');

ws.suspendRender(); // 初期構築はまとめて

// ── ソース表:地域 / 商品 / 四半期 / チャネル / 売上 ──
const HEADERS = ['地域', '商品', '四半期', 'チャネル', '売上'];
HEADERS.forEach((h, c) => {
  ws.cell(0, c).setValue(h).setStyle({
    bold: true, backgroundColor: '#1e3a5f', color: '#e2e8f0', textAlign: 'center',
  });
});

const REGIONS  = ['東', '西', '北', '南'];
const PRODUCTS = ['りんご', 'バナナ', 'さくらんぼ'];
const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'];
const CHANNELS = ['オンライン', '店頭'];
const pick = (arr: readonly string[]) => arr[Math.floor(Math.random() * arr.length)];

for (let r = 1; r <= 200; r++) {
  ws.setCellInput(r, 0, pick(REGIONS));
  ws.setCellInput(r, 1, pick(PRODUCTS));
  ws.setCellInput(r, 2, pick(QUARTERS));
  ws.setCellInput(r, 3, pick(CHANNELS));
  ws.setCellInput(r, 4, String(Math.round(10 + Math.random() * 90)));
}

ws.setFrozenRows(1);
ws.resumeRender();

// ── ピボット:地域 × 商品、売上の合計 ──
const pivot = ws.createPivot({
  source: { row: 0, col: 0, rows: 201, columns: 5 },
  anchor: { row: 0, column: 6 },
  rows:    [{ field: '地域' }],
  columns: [{ field: '商品' }],
  values:  [{ field: '売上', agg: 'sum', numberFormat: '#,##0' }],
});

// ── ドロップダウン2つで全体を操作 ──
const colField = document.getElementById('col-field') as HTMLSelectElement;
colField.onchange = () =>
  pivot.update({ columns: colField.value ? [{ field: colField.value }] : [] });

const agg = document.getElementById('agg') as HTMLSelectElement;
agg.onchange = () =>
  pivot.update({
    values: [{ field: '売上', agg: agg.value as any, numberFormat: '#,##0' }],
  });

列のドロップダウンを 商品 から 四半期 に変えると、ブロックは地域×四半期のマトリクスとして描き直されます。集計を average に切り替えれば、全セル・小計・総計が合計から平均へ一斉に切り替わります。ソースの 売上 セルを編集して、合計が動くのを眺めてみてください。ピボットテーブルのデモでは、まさにこのセットアップがライブで動いています。

実務上のメモをひとつ:ピボットは数値をセルの数値値から、テキストを入力文字列から読み取るので、ソースへの投入は setCellInput(上の例)でも setValue でも構いません。メジャーが数値でない行は count には数えられますが、sumaveragecountNumbers には入りません。


Lite と Pro の境界線

ピボットテーブルの線引きはデータ検証と同じ発想です ― 作るのは Pro、表示はどこでも

操作Lite(無料)Pro
ピボットを含むドキュメントの読み込み ― ライブに描画される
定義の読み取り(getPivotsgetPivotgetPivotBounds
作成・更新・再計算・削除(createPivot など)

つまり Pro ライセンスの作成ツールでピボット付きのワークブックを作り、受け取り側の Lite ベースのビューアーでそれが正しく表示される、という構成が組めます ― ライセンスを背負うのは書き込み側だけです。

v1 のピボットは意図的に API ファーストです。フィールドを宣言するのはあなたで、計算と描画はグリッドの仕事。エンドユーザー向けのドラッグ&ドロップのフィールドパネルはロードマップにありますが、上の例のとおり、update につないだドロップダウン2つで今日でもかなりの範囲をカバーできます。


まとめ

ピボットテーブルは「200行のレコード」から「会議で本当に必要なあの数字」への最短距離です ― そしてここでは宣言ひとつ:ソースとアンカーと行・列・値フィールドを渡す createPivot。7種の集計、メジャーごとの見出しと数値書式、レポートフィルター、総計、そして update で実行時に組み替えられるハンドル。値の編集は自動で再計算、構造の編集は refresh() 一発です。

ピボットテーブルのデモで試してみてください ― ライブなデータに対してフィールドと集計を切り替えられます。API 全体はピボットテーブルのドキュメントへ。集計する前のソースデータをきれいに保つ話はデータ検証の記事、サマリーの横で生データを探索させる話は並べ替えとフィルターの記事へ。

ReoGrid Web を試してみる

React/Vue 向けの Canvas ベース Excel 互換スプレッドシートコンポーネント。 Lite は無料 — npm install 一発で始められます。

関連記事

JavaScriptスプレッドシートに「並べ替えとフィルター」を ― Excel 風オートフィルターを、クリックでもコードでも

ブラウザのデータ表に、Excel 風の列の並べ替えとヘッダードロップダウンのフィルターを追加。createAutoFilter 一発でユーザーにドロップダウンを、getColumnValues / setColumnFilter で同じフィルターをコードから操作。表示行数のライブ表示付きの実例と、並べ替え対フィルター・Lite 対 Pro の勘どころまで。

JavaScriptスプレッドシートに「入力規則」を ― 不正なデータをセルの手前で止める

ドロップダウンリスト、数値・日付の範囲チェック、数式によるカスタム検証 ― フォームバリデーションのコードを1行も書かずに、不正な入力をタイプした瞬間に弾く。ReoGrid Web の Excel 互換データ検証を、受注入力シートの実コードで解説。入力時メッセージ・エラーアラート・xlsx ラウンドトリップまで。

JavaScriptスプレッドシートで作る「信号機ダッシュボード」 ― 条件付き書式を実例で

在庫が少なければ赤、売上が目標超なら緑、キャンセル行は自動でフラグ ― セルを値に応じて色分けする処理を、スタイルのループを一切書かずに。ReoGrid Web の3種類の条件付き書式ルールを、値の変化に連動して再評価される在庫ダッシュボードの実コードで解説します。

ニュースレター

開発の最新情報をお届けします

新しいリリース・機能追加・お知らせをいち早く受け取るには、
メーリングリストにご登録ください。