ReoGrid ReoGrid Web

クロスシート数式で多拠点の売上サマリーを作る

· unvell team
クロスシート数式で多拠点の売上サマリーを作る

「拠点別サマリー」は、業務アプリで最もよく登場するスプレッドシートの形のひとつです。拠点や部門ごとに1枚ずつシートがあり、それらの合計を1か所に集める集計シートがある ── という構成です。ReoGrid Web 1.3 より前は、この集計を JavaScript で手書きする必要がありました。今ではグリッドが複数シートを束ねる ワークブック になり、あるシートの数式が別シートのセルを参照できます。つまり、サマリーは 数式だけ で組めます。

このチュートリアルでは、3つの拠点シート(Tokyo・Osaka・Nagoya)と、すべての数値がクロスシート数式になっているサマリーシートを構築します。どの拠点の数値を編集してサマリーに戻っても、合計がシートをまたいで再計算されます ── 追加の配線は一切不要です。

完成形は マルチシートワークブックのデモ です。本記事はそのコードを簡略化したものです。

1. ワークブックを作る

各インスタンスは workbook コーディネーターを公開します。最初のシートは自動生成されるので、残りは addSheet で追加します。addSheet は新しいシートを返すので、その worksheet を取得できます。

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

const BRANCHES = ['Tokyo', 'Osaka', 'Nagoya'];

const grid = createReogrid({
  workspace: '#grid',
  licenseKey: 'YOUR_LICENSE_KEY', // Pro キーは https://unvell.com/portal で取得
  initialSheetName: BRANCHES[0],   // 自動生成された最初のシートをリネーム
});

// 最初のシートは既に 'Tokyo'。残りの拠点を追加する。
const sheets = [grid.worksheet];
for (let i = 1; i < BRANCHES.length; i++) {
  sheets.push(grid.workbook.addSheet(BRANCHES[i]).worksheet);
}

下部のシートタブバーは標準で付いてきます ── 追加・名前変更・ドラッグ並べ替え・非表示・タブ色に最初から対応しています。

2. 拠点シートを1枚作る

各拠点シートは小さな Q1 の表です。行に商品、列に月、行ごとの Q1 Total=SUM(...) で、最下部に Total 行を置きます。数値は setCellInput(入力したように編集可能なまま残る)で、ラベルは setValue で書き込みます。

const MONTHS = ['Jan', 'Feb', 'Mar'];
const PRODUCTS = ['Laptops', 'Tablets', 'Phones', 'Accessories'];

// 拠点ごとの月次サンプル値
const DATA: Record<string, number[][]> = {
  Tokyo:  [[52, 55, 60], [31, 30, 34], [40, 44, 47], [18, 17, 20]],
  Osaka:  [[44, 46, 49], [27, 29, 31], [36, 38, 41], [15, 16, 18]],
  Nagoya: [[39, 41, 45], [24, 26, 28], [33, 35, 37], [13, 14, 16]],
};

const header = { bold: true, backgroundColor: '#e2e8f0', textAlign: 'center' } as const;
const totalRow = { bold: true, backgroundColor: '#dbeafe', textAlign: 'right' } as const;

function buildBranch(ws, branch: string) {
  ws.suspendRender();
  ws.setGridSize(2 + PRODUCTS.length, 5); // ヘッダー + 商品行 + 合計行、列 A..E
  ws.column(0).setWidth(140);

  // ヘッダー行: 拠点名 + 月 + 「Q1 Total」
  ws.cell('A1').setValue(branch).setStyle({ ...header, textAlign: 'left' });
  MONTHS.forEach((m, i) => ws.cell(0, 1 + i).setValue(m).setStyle(header));
  ws.cell('E1').setValue('Q1 Total').setStyle(header);

  // 商品ごとに1行。月の値 + =SUM の Q1 合計
  PRODUCTS.forEach((product, p) => {
    const row = 1 + p;               // 行インデックス 1.. → A1 の 2行目..
    ws.cell(row, 0).setValue(product);
    DATA[branch][p].forEach((v, m) => ws.setCellInput(row, 1 + m, String(v)));
    ws.setCellInput(row, 4, `=SUM(B${row + 1}:D${row + 1})`);
  });

  // 最下部の Total 行: 商品行を列ごとに合計
  const last = 1 + PRODUCTS.length;  // 合計行のインデックス
  ws.cell(last, 0).setValue('Total').setStyle({ ...totalRow, textAlign: 'left' });
  for (let c = 1; c <= 4; c++) {
    ws.setCellInput(last, c, `=SUM(${String.fromCharCode(66 + c - 1)}2:${String.fromCharCode(66 + c - 1)}${last})`);
    ws.cell(last, c).setStyle(totalRow);
  }

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

BRANCHES.forEach((branch, i) => buildBranch(sheets[i], branch));

小さいけれど重要なポイントが2つあります。suspendRender() / resumeRender() は書き込みをまとめて1回の再描画にバッチ化します。setFrozenRows(1) はスクロール中もヘッダーを固定表示します。このループの後、各拠点シートの E6 セルにはその拠点の Q1 総合計が入っています。

3. クロスシート数式で集計する

ここが 1.3 の新機能です。サマリーシートはデータをコピーしません。各セルが 'シート名'!セル の参照で 拠点シートを直接読み取り ます。シート名は引用符で囲んでおくと、どんな名前(非ASCIIを含む)でも安全に書けます。

function buildSummary(ws) {
  ws.suspendRender();
  ws.setGridSize(2 + BRANCHES.length, 5);
  ws.column(0).setWidth(140);

  ws.cell('A1').setValue('Branch').setStyle({ ...header, textAlign: 'left' });
  MONTHS.forEach((m, i) => ws.cell(0, 1 + i).setValue(m).setStyle(header));
  ws.cell('E1').setValue('Q1 Total').setStyle(header);

  // 拠点ごとに1行 ── 各セルがその拠点の Total 行をシートをまたいで参照する。
  BRANCHES.forEach((branch, i) => {
    const row = 1 + i;
    ws.cell(row, 0).setValue(branch);
    for (let c = 1; c <= 4; c++) {
      const colLetter = String.fromCharCode(66 + c - 1); // B..E
      const totalRowA1 = 2 + PRODUCTS.length;            // 拠点シートの Total 行(A1表記)
      ws.setCellInput(row, c, `='${branch}'!${colLetter}${totalRowA1}`);
    }
  });

  // 総合計 ── このシート上の拠点行に対する通常の SUM。
  const grand = 1 + BRANCHES.length;
  ws.cell(grand, 0).setValue('All Branches').setStyle({ ...totalRow, textAlign: 'left' });
  for (let c = 1; c <= 4; c++) {
    const colLetter = String.fromCharCode(66 + c - 1);
    ws.setCellInput(grand, c, `=SUM(${colLetter}2:${colLetter}${grand})`);
    ws.cell(grand, c).setStyle(totalRow);
  }

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

const summary = grid.workbook.addSheet('Summary').worksheet;
buildSummary(summary);

// 集計結果が最初に見えるよう、Summary シートをアクティブにする。
grid.workbook.setActiveSheet(BRANCHES.length);

サマリーの拠点別セルはクロスシートの 単一セル 参照(='Tokyo'!E6)を使い、総合計行はそれら拠点行に対する同一シート内の通常の =SUM(...) を使っています。=SUM(Tokyo!E2:E5) のようにクロスシートの 範囲 を直接集計しても構いません ── どちらの書き方でも同じように解決されます。

手書きの集計より優れている理由

サマリーがコピー値ではなく数式なので、ワークブックが変化しても正しさを保ちます。参照のライフサイクルは Excel 互換です。

  • 拠点の数値を編集 → その Total が再計算され、それを読むサマリーセルもシートをまたいでライブ更新されます。
  • シート名を変更(例: TokyoTokyo HQ)→ すべての ='Tokyo'!… 参照が自動的に書き換わります。
  • 行を挿入/削除 → サマリーからの参照が追従してシフトします。
  • シートを削除 → その参照は #REF! になり、クロスシートの循環は #CYCLE! として報告されます。

参照は 大文字小文字を区別せず 解決されるため、='tokyo'!e6='Tokyo'!E6 は同じです。

ワークブック全体をエクスポート

インスタンスレベルの I/O は 全シートを一括で 扱うため、1回の呼び出しで複数シートの .xlsx を1ファイルに書き出します。ビュー状態(アクティブシート・非表示シート・タブ色)も往復します。

grid.saveAsXlsx({ filename: 'q1-sales.xlsx' });   // 4シートを1ファイルに
await grid.loadFromUrl('/data/q1-sales.xlsx');     // 全シートを読み戻す
const doc = grid.toJson();                          // ワークブック全体の JSON

まとめ

マルチシートワークブックにより、ReoGrid Web は単一グリッドから、業務アプリが本当に必要とする「集計」のパターンへと進化します。そしてクロスシート数式によって、その集計は手作業で維持するものではなく宣言的なものになります。公開APIに破壊的変更はなく、マルチシートは Lite / Pro 両対応です。

ReoGrid Web を試してみる

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

関連記事

ニュースレター

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

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