SpreadJS×Wijmo!スプレッドシートでドリルダウンチャートを実現する

JavaScriptスプレッドシートライブラリ「SpreadJS(スプレッドJS)」では「フローティングオブジェクト」機能を使用することでシート上に任意のHTML要素を挿入できますが、この機能を使うことでシート上に「Wijmo(ウィジモ)」のような外部のコントロールを表示することもできます。

本記事では、WijmoのFlexChartコントロールをフローティングオブジェクトとしてSpreadJSに組み込み、SpreadJS単体では実現が難しいドリルダウンチャートを実現する方法について解説します。

SpreadJS上にWijmoのFlexChartコントロールを表示

開発環境

事前準備

今回作成するファイルは次の4つです。

index.htmlページ本体。ページの要素としてSpreadJSコントロールを配置します
app.jsSpreadJSとFlexChartを作成するコードを記載します
data.jsSpreadJSとFlexChartに表示するデータを記載します
styles.css各種ページ要素のスタイル定義を記載します

SpreadJSやWijmoを使用するには、専用のモジュールを環境に配置する必要があります。CDNを参照(Wijmoのみ)する方法やnpm(SpreadJSWijmo)などから入手する方法もありますが、今回は環境に直接SpreadJSとWijmoのモジュールを配置していきます。あらかじめSpreadJSとWijmoの製品版かトライアル版をご用意ください。トライアル版は以下より無償で入手可能です。

製品版、またはトライアル版をダウンロードしたら、ZIPファイルを解凍し、以下のファイルを環境にコピーします。なお、SpreadJSのCSSファイルは以下に記載のもの含め7種類あるのでお好みのテーマのものを選択してください。

  • scripts/gc.spread.sheets.all.18.1.4.min.js
  • scripts/plugins/gc.spread.sheets.shapes.18.1.4.min.js
  • scripts/plugins/gc.spread.sheets.datacharts.18.1.4.min.js
  • scripts/resources/gc.spread.sheets.resources.ja.18.1.4.min.js
  • css/gc.spread.sheets.excel2013white.18.1.4.css
  • scripts/wijmo.min.js
  • scripts/wijmo.chart.min.js
  • scripts/wijmo.chart.animation.min.js
  • scripts/cultures/wijmo.culture.ja.min.js
  • css/wijmo.min.css

フローティングオブジェクトの追加

SpreadJSのフローティングオブジェクトは、Excelのシェイプのようにシート上に配置するオブジェクトで、位置やサイズを自由に変更することができます。また、内部に任意のHTML要素を配置できるのも大きな特長です。

この記事ではフローティングオブジェクト内にWijmoのFlexChartコントロールを配置して、SpreadJS単体では実現が難しいドリルダウンチャートを実現するサンプルを作成します。フローティングオブジェクトについてはヘルプデモで詳しく解説しているのでご一読ください。

まずはSpreadJS上にフローティングオブジェクトを追加した画面を作成します。前述の各モジュールと「app.js」、「data.js」、「styles.css」への参照設定をHTMLファイルに追加します。
※ WijmoのモジュールをCDNから参照する場合は、コメントアウトされている部分とライブラリの参照先を入れ替えてください。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <link href="css/gc.spread.sheets.excel2013white.18.1.4.css" rel="stylesheet"/>
    <script src="scripts/gc.spread.sheets.all.18.1.4.min.js"></script>
    <script src="scripts/plugins/gc.spread.sheets.shapes.18.1.4.min.js"></script>
    <script src="scripts/plugins/gc.spread.sheets.datacharts.18.1.4.min.js"></script>
    <script src="scripts/resources/gc.spread.sheets.resources.ja.18.1.4.min.js"></script>

    <!-- ローカルのWijmoのライブラリを参照する場合 -->
    <link href="css/wijmo.min.css" rel="stylesheet" />
    <script src="scripts/wijmo.min.js"></script>
    <script src="scripts/wijmo.chart.min.js"></script>
    <script src="scripts/wijmo.chart.animation.min.js"></script>
    <script src="scripts/cultures/wijmo.culture.ja.min.js"></script>

    <!-- CDNからWijmoのライブラリを参照する場合 -->
    <!-- <link href="https://cdn.mescius.com/wijmo/5.20251.40/styles/wijmo.min.css" rel="stylesheet"/>
    <script src="https://cdn.mescius.com/wijmo/5.20251.40/controls/wijmo.min.js"></script>
    <script src="https://cdn.mescius.com/wijmo/5.20251.40/controls/wijmo.chart.min.js"></script>
    <script src="https://cdn.mescius.com/wijmo/5.20251.40/controls/wijmo.chart.animation.min.js"></script>
    <script src="https://cdn.mescius.com/wijmo/5.20251.40/controls/cultures/wijmo.culture.ja.min.js"></script> -->

    <link href="css/styles.css" rel="stylesheet"/>
    <script src="scripts/data.js"></script>
    <script src="scripts/app.js"></script>
</head>
<body>
    <div id="ss"></div>
</body>
</html>

続いて「app.js」を以下のように作成します。
SpreadJSのシートにテーブルとフローティングオブジェクトを追加し、テーブルには「data.js」のdataを連結しています。なお、下記のSetChart()関数と「・・・(中略)・・・」については後の項目で解説します。
※ ライセンスキーを設定しない場合、トライアル版を示すメッセージが表示されます。ライセンスキーの設定方法についてはこちら(SpreadJSWijmo)をご覧ください。

// SpreadJSのライセンスキーとカルチャの設定
// GC.Spread.Sheets.LicenseKey = 'ここにSpreadJSのライセンスキーを設定します';
GC.Spread.Common.CultureManager.culture('ja-jp');

// Wijmoのライセンスキーの設定
// wijmo.setLicenseKey('ここにWijmoのライセンスキーを設定します');

const spreadNS = GC.Spread.Sheets;
var view;
var sheet;
var chart;
var currentItem;

window.onload = async () => {
  // SpreadJSの生成と設定
  const spread = new spreadNS.Workbook("ss");

  // フローティングオブジェクトにUIライブラリを組み込む場合、
  // シート切り替え時に再描画が必要となるため、
  // 本サンプルではシートタブを非表示にしてシングルシートのアプリとしています
  spread.options.tabStripVisible = false;

  // デフォルトのEnterキーの挙動(セル編集確定後のカーソル移動)により、
  // シートとチャートでハイライト表示の対象が次のセル/データ点に移動してしまうのを防止し、
  // 編集を確定したセル/データ点をハイライトしたままにするため、
  // Enterキー押下時のカーソル移動を抑制し、編集だけを終了させるカスタム処理を登録
  spread.commandManager().register(
    "endEditing",
    {
      canUndo: false,
      execute: () => {
        // Enterキー押下による編集の終了
        if (sheet && sheet.isEditing()) {
          sheet.endEdit();
          return true;
        }
        return false;
      },
    },
    13,
    false,
    false,
    false,
    false
  );

  // シートの設定
  sheet = spread.getActiveSheet();
  sheet.options.gridline.showHorizontalGridline = false;
  sheet.options.gridline.showVerticalGridline = false;
  sheet.options.selectionBorderColor = 'red';
  sheet.options.selectionBackColor = 'transparent';

  // テーブルの設定
  const table = sheet.tables.add('table1', 0, 0, 35, 3);
  const col1 = new spreadNS.Tables.TableColumn(1, "year", "年");
  const col2 = new spreadNS.Tables.TableColumn(2, "month", "月");
  const col3 = new spreadNS.Tables.TableColumn(3, "temp", "気温");
  col1.dataStyle({ hAlign: spreadNS.HorizontalAlign.right });
  table.autoGenerateColumns(false);
  table.bind([col1, col2, col3], '', data);

  // フローティングオブジェクトの追加
  const fo = new spreadNS.FloatingObjects.FloatingObject(
    "flexChart", 210, 20, 600, 400
  );
  const div = document.createElement('div');
  div.innerHTML = '<div id="chart"></div>';
  fo.content(div);
  fo.fixedPosition(true);
  sheet.floatingObjects.add(fo);

  // フローティングオブジェクトのHTML要素を使用したチャートの表示
  SetChart();
  
  // チャートの更新
    ・・・(中略)・・・
}

フローティングオブジェクトのHTML要素は、FloatingObjectのcontentメソッドを使って追加します。ここでは「<div id="chart"></div>」を追加しています。そしてSetChart()関数を使ってこのdiv要素にFlexChartを設定します。

「data.js」には次のようなデータとそれを操作する関数を記述します。

データは、1964年、1994年、および2024年の仙台市の月平均気温です。なお、ここに記載している2つの関数は、WijmoのCollectionViewクラスのグループ化機能を利用するためのものです。

const data = [
  { year: '1964年', month: '1月', temp: 1.8 },
  { year: '1964年', month: '2月', temp: 0.3 },
  { year: '1964年', month: '3月', temp: 4.0 },
  { year: '1964年', month: '4月', temp: 10.8 },
  { year: '1964年', month: '5月', temp: 15.2 },
  { year: '1964年', month: '6月', temp: 18.5 },
  { year: '1964年', month: '7月', temp: 22.5 },
  { year: '1964年', month: '8月', temp: 24.9 },
  { year: '1964年', month: '9月', temp: 18.9 },
  { year: '1964年', month: '10月', temp: 13.3 },
  { year: '1964年', month: '11月', temp: 8.5 },
  { year: '1964年', month: '12月', temp: 3.8 },
  { year: '1994年', month: '1月', temp: 1.8 },
  { year: '1994年', month: '2月', temp: 2.8 },
  { year: '1994年', month: '3月', temp: 4.0 },
  { year: '1994年', month: '4月', temp: 11.0 },
  { year: '1994年', month: '5月', temp: 15.5 },
  { year: '1994年', month: '6月', temp: 18.6 },
  { year: '1994年', month: '7月', temp: 24.2 },
  { year: '1994年', month: '8月', temp: 26.6 },
  { year: '1994年', month: '9月', temp: 22.2 },
  { year: '1994年', month: '10月', temp: 16.6 },
  { year: '1994年', month: '11月', temp: 9.6 },
  { year: '1994年', month: '12月', temp: 4.4 },
  { year: '2024年', month: '1月', temp: 4.2 },
  { year: '2024年', month: '2月', temp: 4.7 },
  { year: '2024年', month: '3月', temp: 6.0 },
  { year: '2024年', month: '4月', temp: 14.8 },
  { year: '2024年', month: '5月', temp: 17.8 },
  { year: '2024年', month: '6月', temp: 21.5 },
  { year: '2024年', month: '7月', temp: 26.1 },
  { year: '2024年', month: '8月', temp: 27.4 },
  { year: '2024年', month: '9月', temp: 23.5 },
  { year: '2024年', month: '10月', temp: 18.0 },
  { year: '2024年', month: '11月', temp: 10.9 },
  { year: '2024年', month: '12月', temp: 4.3 }
];

const getData = () => {
  return new wijmo.collections.CollectionView(data, {
    groupDescriptions: ['year', 'month']
  });
};

const getGroupData = (group) => {
  const arr = [];
  group.groups.forEach(g => {
    arr.push({
      name: g.name,
      temp: g.getAggregate(wijmo.Aggregate.Avg, 'temp'),
      group: g
    });
  });
  return arr;
};

最後にSpreadJSとFlexChartに関連するスタイルを「styles.css」に記述します。

/* SpreadJSのスタイル */
#ss {
  inline-size: 900px;
  block-size: 490px;
  border: 1px solid silver;
}

/* FlexChart関連のスタイル */
#chart {
  inline-size: 100%;
  block-size: 100%;
}

.wj-flexchart .wj-state-selected {
  stroke: red;
  stroke-width: 3;
  stroke-dasharray: 1;
}

a:link {
  color: blue;
}

a:visited {
  color: blue;
}

a:active {
  color: red;
}

FlexChartコントロールの配置

先ほど作成したフローティングオブジェクトにWijmoのFlexChartを表示するSetChart()関数を次のように記述します。

・・・(中略)・・・
window.onload = async () => {
・・・(中略)・・・
}

const SetChart = () => {
  // CollectionViewの生成
  view = getData();

  // FlexChartの作成
  chart = new wijmo.chart.FlexChart('#chart', {
    chartType: wijmo.chart.ChartType.Column,
    bindingX: 'name',
    series: [{
      binding: 'temp',
      name: 'temp'
    }],
    axisY: {
      title: '気温[℃]',
      min: 0,
      max: 30
    },
    header: '仙台の気温 (年平均)',
    legend: {
      position: wijmo.chart.Position.None
    },
    tooltip: { content: '' },
    selectionMode: wijmo.chart.SelectionMode.Point,
    selectionChanged: (s) => {
      if (s.selection) {
        const point = s.selection.collectionView.currentItem;
        if (point && point.group && !point.group.isBottomLevel) {
          // currentItemの保存
          // currentItem = point;
          // 階層の切り替え(子階層の表示)
          // showGroup(point.group);
        } else if (s._selectionMode == wijmo.chart.SelectionMode.Point) {
          // 選択されたポイントに対応するデータを選択
          // showCurrentData();
        }
      }
    },
    itemsSource: getGroupData(view),
    options: { htmlText: true } // ヘッダ内のHTML有効化
  });

  // アニメーション動作
  const ani = new wijmo.chart.animation.ChartAnimation(chart);
};

ドリルダウン動作とテーブルとチャートの連携についてはまだ実装していないので、それらに関連する部分はコメントアウトしています。

以上を記述した後にアプリケーションを実行すると、次のようになります。

SpreadJS上にWijmoのFlexChartコントロールを表示

ドリルダウン&ドリルアップ機能の実装

FlexChartのドリルダウン動作は、次の機能を使って実現します。

Wijmoのドリルダウンチャートのデモでは、FlexChart用のdivタグの上にドリルダウンに使用するヘッダ用のdivを作成していますが、このサンプルではFlexChartのhtmlTextオプションをtrueに設定してheaderプロパティに直接HTML要素を記述しています。

FlexChartでのドリルダウン処理の流れを下の概要図に示します。

ドリルダウン処理の概要図

ドリルダウン処理

  1. FlexChartに年平均気温が表示されているときに任意の年の棒をクリックする
  2. FlexChartのselectionChangedイベントが発生する
  3. CollectionViewの機能を使って子のgroupを取得する
  4. showGroup()関数を呼び出す
  5. 取得したgroupに対応したデータをCollectionViewで生成する
  6. 生成されたデータをFlexChartのitemsSourceプロパティに設定する

ドリルアップ処理

  1. FlexChartに月平均気温が表示されているときにheaderのリンク(年)をクリックする
  2. headerに設定されたHTML要素のonclickイベントが発生する
  3. switchGroup()関数を呼び出す
  4. CollectionViewの機能を使って親のgroupを取得する
  5. switchGroup()関数内でshowGroup()関数を呼び出す
  6. 取得したgroupに対応したデータをCollectionViewで生成する
  7. 生成されたデータをFlexChartのitemsSourceプロパティに設定する

それでは、先ほどコメントアウトした「currentItem = point;」と「showGroup(point.group);」のコメントを解除し、各関数を記述しましょう。

・・・(中略)・・・
// FlexChartの作成
  chart = new wijmo.chart.FlexChart('#chart', {
    ・・・(中略)・・・
    selectionChanged: (s) => {
      if (s.selection) {
        const point = s.selection.collectionView.currentItem;
        if (point && point.group && !point.group.isBottomLevel) {
          // currentItemの保存
          currentItem = point;
          // 階層の切り替え(子階層の表示)
          showGroup(point.group);
        } else if (s._selectionMode == wijmo.chart.SelectionMode.Point) {
          // 選択されたポイントに対応するデータを選択
          // showCurrentData();
        }
      }
    },
    ・・・(中略)・・・
  });
・・・(中略)・・・

新たに追加する関数はshowGroup()updateChartHeader()switchGroup()の3つです。

・・・(中略)・・・
// 現在の階層に応じたチャートとシートの設定
const showGroup = (group) => {
  // ヘッダの更新
  updateChartHeader(group);

  // チャートの更新
  const level = 'level' in group ? group.level + 1 : 0;
  const palette = wijmo.chart.Palettes.standard;
  const index = view.groups.indexOf(group);
  chart.chartType = level > 0
    ? wijmo.chart.ChartType.LineSymbols
    : wijmo.chart.ChartType.Column;
  chart.series[0].style = {
    fill: palette[(level + index) % palette.length],
    stroke: palette[(level + index) % palette.length]
  };
  chart.itemsSource = getGroupData(group);
  chart.selection = null;

  // テーブルの更新
  // showCurrentData();
};

showGroup()関数は、指定されたgroupに対応したチャートを表示します。具体的には、チャートに表示するデータ、チャートの種類、およびチャートの系列の色を切り替えます。

なお、コメントアウトしている「showCurrentData();」については次の項目で記述します。

・・・(中略)・・・
// 現在の階層に応じたヘッダの設定
const updateChartHeader = (group) => {
  let path = '';
  const item = group.items[0];
  const headers = [];
  for (let i = 0; i <= group.level; i++) {
    const prop = view.groupDescriptions[i].propertyName;
    const hdr = `<a href="#${path}" id="header" onclick="switchGroup()">
    ${{ year: '年' }[prop]}</a>: ${item[prop]}`
    headers.push(hdr);
    path += '/' + item[prop];
  }
  chart.header = headers.length > 0
    ? '仙台の気温 (月平均) - ' + headers.join('、')
    : '仙台の気温 (年平均)';
};

updateChartHeader()関数は、FlexChartのheaderプロパティの内容を切り替えます。子階層の折れ線グラフ(月平均気温)が表示されているときには、表題に加えてドリルアップ用のリンクを作成するaタグを追加します。このaタグがクリックされたときに発生するonclickイベントで下記のswitchGroup()関数が呼び出されます。

・・・(中略)・・・
// 階層の切り替え(親階層の表示)
const switchGroup = () => {
  const header = document.getElementById("header");
  let path = header.href;
  path = path.substr(path.lastIndexOf('#') + 1);
  const paths = path.split('/');
  let src = view;
  for (let i = 1; i < paths.length; i++) {
    for (let j = 0; j < src.groups.length; j++) {
      const group = src.groups[j];
      if (group.name == paths[i]) {
        src = group;
        break;
      }
    }
  }
  showGroup(src);

switchGroup()関数は、親のgroupを取得してshowGroup()関数を呼び出すことで親階層の棒グラフ(年平均気温)を表示します。

以上の変更と追加を行ってからアプリケーションを実行すると、チャートのドリルダウンとドリルアップが可能になります。

テーブルとチャートの連携

「data.js」のdataはSpreadJSのtableとWijmoのCollectionViewにデータ連結されていますが、tableとFlexChartの間は連結されていません。そのためtable上でアクティブセルを移動しても、対応するFlexChartのデータ点(棒や点)がハイライト表示されることはありません。同様に、FlexChart上で選択されたデータ点に対応するtable上のセルがアクティブになることもありません。

ここでは、この状況を改善してtable上のアクティブセルとFlexChart上のデータ点を同期させるほか、table上でセルを編集した結果が自動的にFlexChartに反映されるようにします。

以下は、この機能を実現するための仕組みを表した概要図です。

SpreadJSのデータとチャートの同期

前述の概要図に次の関数を追加しています。

  • FlexChartのselectionChangedイベント内のshowCurrentData()関数
  • SpreadJSのSelectionChangedイベント内のupdateChart()関数
  • SpreadJSのValueChangedイベント内のupdateChart()関数

それでは、これらの機能を実装していきましょう。

まずwindow.onloadイベントで省略していた「チャートの更新」を次のように記述します。

・・・(中略)・・・
window.onload = async () => {
  ・・・(中略)・・・
  // チャートの更新
  sheet.bind(spreadNS.Events.SelectionChanged, (e, info) => {
    updateChart();
  });
  sheet.bind(spreadNS.Events.ValueChanged, (e, info) => {
    updateChart();
  });
}
・・・(中略)・・・

次に「FlexChartの作成」の中の「選択されたポイントに対応するデータを選択」でコメントアウトされていた「showCurrentData();」のコメントを解除します。

・・・(中略)・・・
// FlexChartの作成
  chart = new wijmo.chart.FlexChart('#chart', {
    ・・・(中略)・・・
    selectionChanged: (s) => {
      if (s.selection) {
        const point = s.selection.collectionView.currentItem;
        if (point && point.group && !point.group.isBottomLevel) {
          // currentItemの保存
          currentItem = point;
          // 階層の切り替え(子階層の表示)
          showGroup(point.group);
        } else if (s._selectionMode == wijmo.chart.SelectionMode.Point) {
          // 選択されたポイントに対応するデータを選択
          showCurrentData();
        }
      }
    },
    ・・・(中略)・・・
  });
・・・(中略)・・・

さらにshowGroup()関数の中の「テーブルの更新」でコメントアウトされていた「showCurrentData();」のコメントも解除します。

・・・(中略)・・・
// 現在の階層に応じたチャートとシートの設定
const showGroup = (group) => {
  …(中略)・・・
  // テーブルの更新
  showCurrentData();
};
・・・(中略)・・・

続いてshowCurrentData()関数を次のように記述します。

・・・(中略)・・・
// 選択したデータ点に対応したテーブル上のセルの明示
const showCurrentData = () => {
  const point = chart.collectionView.currentItem;
  const table = sheet.tables.findByName("table1");

  // フィルタリングの実行
  const level = point.group.level;
  const rowFilter = table.rowFilter();
  if (level > 0) {
    const condition = new spreadNS.ConditionalFormatting.Condition(
      spreadNS.ConditionalFormatting.ConditionType.textCondition,
      {
        compareType: spreadNS.ConditionalFormatting.TextCompareType.equalsTo,
        expected: point.group.items[0].year,
      }
    );
    rowFilter.addFilterItem(0, condition);
    rowFilter.filter();
  } else {
    rowFilter.reset();
  }

  // 該当項目の明示とスクロール
  const range = table.dataRange();
  const dataIndex = data.indexOf(point.group.items[0]);
  sheet.setSelection(
    range.row + dataIndex,
    range.col,
    1,
    range.colCount
  );
  sheet.showCell(
    range.row + dataIndex,
    range.col,
    spreadNS.VerticalPosition.nearest,
    spreadNS.HorizontalPosition.left
  );
}

showCurrentData()関数は、CollectionViewを使って取得した現在のgroupのlevelに応じてtableのフィルタを設定/解除するほか、FlexChart上で選択されたデータ点に対応するシートのセル(年、月、気温)を選択状態にします。

updateChart()関数には次のようなコードを記述します。

・・・(中略)・・・
// シート編集後のチャートの更新
const updateChart = () => {
  const table = sheet.tables.findByName("table1");
  const filter = table.rowFilter();
  const row = sheet.getActiveRowIndex() + table.headerIndex();
  const year = sheet.getValue(row, 0);
  const month = sheet.getValue(row, 1);
  const temp = sheet.getValue(row, 2);
  if (filter.isFiltered()) {
    // 子階層が表示されている場合
    chart.itemsSource = getGroupData(currentItem.group);
    chart.selection = chart.series[0];
    const items = currentItem.group.items;
    for (let i = 0; i < items.length; i++) {
      if (items[i].year == year && items[i].month == month && items[i].temp == temp) {
        chart.series[0].collectionView.moveCurrentToPosition(i);
        break;
      }
    }
  } else {
    // 親階層が表示されている場合
    chart.itemsSource = getGroupData(view);
    chart.selection = chart.series[0];
    const items = chart.collectionView.items;
    for (let i = 0; i < items.length; i++) {
      if (items[i].name == year) {
        chart.series[0].collectionView.moveCurrentToPosition(i);
        break;
      }
    }
  }
}

updateChart()関数は、その時点で表示されている階層に応じてFlexChartのitemsSourceプロパティに設定するデータを切り替えるほか、編集されたセルの行番号に対応したデータ点を選択状態に設定します。

以上の変更を行ってアプリケーションを実行すると、アクティブセルを変更したときにFlexChartの対応するデータ点が選択され、逆にFlexChartでデータ点を選択すると対応するセルが選択状態になります。

下の動画は、table上で月平均気温を編集したときに、FlexChartの対応するデータ点が更新されるようすを示しています。

今回ご紹介した内容については以下のデモアプリケーションで確認できます(“Run Project”をクリックするとデモが起動します)。

さいごに

今回の記事では、「SpreadJS」のフローティングオブジェクトを使って「Wijmo」のFlexChartコントロールを活用する方法について解説しました。フローティングオブジェクトを使用して外部のUIコントロールを取り込むことにより、SpreadJS単体では難しい機能も実現することができます。

製品サイトでは、SpreadJSとWijmoの機能を手軽に体験できるデモアプリケーションやトライアル版も公開しておりますので、こちらもご確認ください。

また、ご導入前の製品に関するご相談、ご導入後の各種サービスに関するご質問など、お気軽にお問合せください。

\  この記事をシェアする  /