Amcharts v4 × Reactでガントチャート作成入門

## はじめに
フロントエンドの開発でAmchartsを使ったチャートの実装を結構やってきたので、その知見を残しておきたいので投稿した。

## 目的
* ガントチャートの実装を通してAmchartsを使ったチャートを実装する手法を知る

## 前提
* React × TypeScriptで開発
* ガントチャートを作る

## Amchartsとは
* [Amcharts](https://www.amcharts.com/)はチャート系のJavaScriptのライブラリでTypeScriptにも対応している
* canvas要素ではなくsvg要素が使われる
* [チャートの種類](https://www.amcharts.com/demos/)も多い
* コードサンプルも豊富で導入ハードルも高くはない


## Reactとの組み合わせ
* プロジェクトへのインストール方法は[Using TypeScript or ES6 – amCharts 4 Documentation](https://www.amcharts.com/docs/v4/getting-started/using-typescript-or-es6/)
* AmchartsはReact前提のライブラリではない（一応Reactを使った実装サンプルは公式で載せてはいる）
* ノードを直接いじる
* そのためReactで使うときは、Refを通してデザインや機能を入れていく
* チャートについてのコードを書いていく前に、以下のようにRefとRefを適用するdivやRefを通してチャートの定義をしていくためのhooks（useLayoutEffect）を用意

```typescript
import React, { useLayoutEffect, useRef } from "react";
import * as am4charts from "@amcharts/amcharts4/charts";
import * as am4core from "@amcharts/amcharts4/core";

const GanttChart: React.FC = () => {
	const divRef = useRef<HTMLDivElement | null>(null);
	const chartRef = useRef<am4charts.XYChart | null>(null) // chart本体のnodeとなる、chartの更新は主にこのRefを通してやる

	useLayoutEffect(() => {
	  if (divRef.current) {
		const chart = am4core.create(divRef.current, am4charts.XYChart); // divRefのnodeがchartのコンテナとして適用される

	    // 以降でchart周りの実装内容を記述

		chartRef.current = chart; // 最後にchartのRefを保持しておくと、別のhooks内でRefを通して色々いじることができる
		return () => chart.dispose(); // unmount時にdisposeする必要がある
	  }
	}, []);

	return (
	  <div>
       {/* chartのコンテナとなるRefを渡す */}
	    <div ref={divRef} style={{ height: 200, width: "100%" }} />
	  </div>
	)
};
```

* ここまではガントチャート以外でもほとんど同じになる
* 以降ではこのコードに徐々に追加していく形でコードを載せていく

## チャート実装のフロー
* チャート部分はどうしても記述が長くなりがちなので、ある程度の処理のかたまりで同じところで書いたり順序を決めておくといい
* 自分がx軸とy軸のあるチャート（AmchartsでいうXYcharts）を実装するときにやるフローは以下の通り（大体なので例外はある）
	1. データの用意 
	2. チャート全体・チャートの軸以外に関わるプロパティの設定
	3. x軸の定義・x軸に関わるプロパティの設定
	4. y軸の定義・y軸に関わるプロパティの設定
	5. Series（データと対応させてプロットしている部分）の定義・Seriesに関わるプロパティの設定
	6. メインのチャート以外の細かな機能（スクロールバー、バレット、カーソル等）の追加

* 大体5までやればチャートとして成立はする
* 以降ではこのフローに沿ってコードを追加していく

## データの用意
* チャート自体を実装する前に表示するデータを適当に用意する

```typescript
import React, { useLayoutEffect, useRef } from "react";
import * as am4charts from "@amcharts/amcharts4/charts";
import * as am4core from "@amcharts/amcharts4/core";

const GanttChart: React.FC = () => {
	const divRef = useRef<HTMLDivElement | null>(null);
	const chartRef = useRef<am4charts.XYChart | null>(null) 

	// 追加
	const chartData = [
	  {
	    category: "A",
	    start: 1609462800000, // 2021-01-01 10:00:00
	    end: 1609466400000 // 2021-01-01 11:00:00
	  },
	  {
	    category: "B",
	    start: 1609473600000, // 2021-01-01 13:00:00
	    end: 1609477200000 // 2021-01-01 14:00:00
	  }
  ];

	useLayoutEffect(() => {
	  if (divRef.current) {
		const chart = am4core.create(divRef.current, am4charts.XYChart); 
	    
	  }
	}, []);

	return (
	  <div>
	    <div ref={divRef}　style={{ height: 200, width: "100%" }} /> 
	  </div>
	)
};
```

* どういった形式のJSONにするのかはチャートによって変わる
* ガントチャートはy軸に対応する値（例でいう`category`）とx軸に対応する時刻の開始、終了があれば表示するだけであれば十分
* 補足として`start`と`end`はunixミリ秒

## チャート全体・チャートの軸以外に関わるプロパティの設定
* ここから先は`useLayoutEffect`内で実装されていく
* チャート全体の定義は特に厳密には決めてないが、そこは実装者が雰囲気で決めている
* 基本的にこのセクションでは以下のchartの定義をしただけで最低限は終了

```typescript
const chart = am4core.create(divRef.current, am4charts.XYChart); 
```

* これ以外は表示の細かい位置調整や色、サイズといった細かいプロパティを設定する
* 例としては以下のように高さを設定したり、paddingを入れる
* 注意点としてここでのheightはあくまでチャートの高さでチャートを描画するコンポーネント自体のheightではない
* そのためchartの描画する（今回は具体的にはdivRefを適用させる）`div`に対しても対応したheightをstyleで当てる必要がある

```typescript
chart.height = 200;
chart.paddingTop = 0;
chart.paddingBottom = 0;
```

* 今回は他にチャートに適応するデータの`start`や`end`の形式を指定する
* 形式を指定するとtooltip等を表示する場合に任意の形式で表示されるようになる
* chartに適用するデータもここで設定する

```typescript
chart.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss";
chart.data = chartData; // データの適用
```

##  x軸の定義・x軸に関わるプロパティの設定
* 最低限のコードは以下だけで、他は追加の設定となる

```typescript
const dateAxis = chart.xAxes.push(new am4charts.DateAxis());
```

* イメージとしてはchartのxAxesにDateAxisを追加する感じ
* これはchartのx軸に対して複数の軸を追加でき、複数のグラフを描画することが可能であることを意味する
* ガントチャートのみを表示したいのであればDateAxisのみの追加で問題ない
* 他にやれることは多いが今回は以下の2つを追加したい
	* 表示される幅の範囲を少し広げるようにする
	* グリッド線の真下に時間のラベルを表示するようにしたい（言葉ではイメージできないと思うので実際に描画してみて違いを確認すると理解できる）
* 2つ目に関しては[Positioning Axis Elements – amCharts 4 Documentation](https://www.amcharts.com/docs/v4/concepts/axes/positioning-axis-elements/#_location_and_DateAxis)に目を通すといい

```typescript
// 0 ~ 1 で指定
// 値が大きいほど範囲が広がる
dateAxis.extraMin = 0.1;
dateAxis.extraMax = 0.1;
dateAxis.renderer.labels.template.location = 0.0001; // この1行を消したときの違いを確認すると何の設定なのか圧倒的にイメージしやすい
```


## y軸の定義・y軸に関わるプロパティの設定
* 最低限コードは以下だけ

```typescript
const categoryAxis = chart.yAxes.push(new am4charts.CategoryAxis());
// 適用したデータに対してy軸として設定したフィールドを指定
// 今回はcategory
categoryAxis.dataFields.category = "category";
// locationはchartのgridの位置関係を0 ~ 1で指定する
// 試しに色々変えてみるとどこが変わるのかわかりやすい
categoryAxis.renderer.grid.template.location = 0;
```

* 基本はx軸のときと流れは同じで、以降は細かい見せ方に関するプロパティの指定
* category axisの場合、チャートは適用するデータの配列の0番目から順にx軸に近いところから（要するに下から）描画される
* ガンチャートは上から見るのが基本なので、今回は適用するデータも`start`が時間的に早い順になっていることもあり、逆転させたい
* その場合は以下のように設定

```typescript
categoryAxis.renderer.inversed = true;
```

## Seriesの定義・Seriesに関わるプロパティの設定
* x、y軸のときと同じようにまずは任意のseriesを追加する
* その際に適用するデータの配列の各々の要素のどのプロパティがx軸の値に対応し、y軸が値がどのプロパティに対応するかを指定する
* 余談だが適用するデータを参照するように設定するのは、基本的に`dataFields`というプロパティ以降のプロパティから対応するデータのプロパティ名を文字列で指定する
* seriesの種類は様々存在するが、ガントチャートの場合は`ColumnSeries`を使うことになる

```typescript
const series = chart.series.push(new　am4charts.ColumnSeries());
// ガントチャートのx軸は幅を指定するので2つを指定する
series.dataFields.openDateX = "start";
series.dataFields.dateX = "end";
series.dataFields.categoryY = "category";
```

* 上記のコードだけで問題なく描画され、あとは見せ方の設定になる
* 今回はseriesにカーソルを当てたときにtooltipを出すようにしてみる

```typescript
// {}内でプロパティ名を書くと対応した値が利用される
// 改行を入れたいときは改行文字を入れる
series.columns.template.tooltipText = "category: {category} \n 開始: {openDateX} \n 終了: {dateX}";
// カーソルを当てたときにtooltipがカーソルに付いて動くようにする
// fixedかpointerの二択でどう違うかは試してみるといい
series.columns.template.tooltipPosition = "pointer";
```

* ここまでできればひとまずガントチャート自体は描画される

## メインのチャート以外の細かな機能の追加
* ここからは追加機能を設定していく

### スクロールバー
* まずはズーム機能とそのスクロールバーを追加する
* スクロールバーのイメージがわからない場合はコードを真似して描画してみると何のことかはわかる
* スクロールバーの種類は豊富にあるが、今回は`XYChartScrollbar`を使用する
* さらにスクロールバー上にもseriesが見えるようにしていく

```typescript
const scrollbar = new am4charts.XYChartScrollbar();
chart.scrollbarX = scrollbar;
// 前のセクションで定義したseriesをスクロールバーのseriesにpush
scrollbar.series.push(series);
```

### カーソル
* 次にチャート上でマウスを使って範囲を指定したらズームされる機能を追加する
* その場合amChartsでは`am4charts.XYCursor`というクラスを使うことになる
* 以下のコードだけで自動的にズーム機能が実装される

```typescript
chart.cursor = new am4charts.XYCursor();
```

### イベント
* 最後に特定のアクションにフックして任意の処理を実行させるイベントを使ってみる
* イベントが使いこなせると出来ることが一気に増えので必ず使いこなせるようにしたい
* 今回はチャート上をダブルクリックした際にズーム範囲をリセットする機能を追加したい
* まずユーザがチャート上をダブルクリックした際に、チャートのどの部分が、そして何のイベントが発火されるのかを把握していく必要がある
* 結論から言うと`plotContainer`という部分の`doublehit`というイベントが発火される
* イベントの見つけ方は
	1. 公式のクラス毎のドキュメントにイベントの一覧がある（大体どのクラスも同じイベントがある）ので、該当しそうなアクション時に発火するイベントに目星をつける（今回は`doublehit`という比較的わかりやすいのがある）[参考](https://www.amcharts.com/docs/v4/reference/container/#Events)
	2. チャートのどの部分のイベントが発火するのかは手探りでやってみるしかなく、ここは一度実装して意図した挙動ができない場合は、他の部分のイベントで試すなどをする必要がある（とはいえある程度は目星つくので大変でもない）
* 以下、実装

```typescript
// 引数に与える関数が実行される
chart.plotContainer.events.on("doublehit", () =>
	dateAxis.zoom({ start: 0, end: 1 })
);
```

* 補足として、第2引数のコールバック関数は引数を使うことができ、使う場合も多いにある

## 完成
* 以下が完全なコードとなる（今までのコードをくっつけただけ）

```typescript
import React, { useLayoutEffect, useRef } from "react";
import * as am4charts from "@amcharts/amcharts4/charts";
import * as am4core from "@amcharts/amcharts4/core";

const GanttChart: React.FC = () => {
  const divRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<am4charts.XYChart | null>(null);

  const chartData = [
    {
      category: "A",
      start: 1609462800000, // 2021-01-01 10:00:00
      end: 1609466400000 // 2021-01-01 11:00:00
    },
    {
      category: "B",
      start: 1609473600000, // 2021-01-01 13:00:00
      end: 1609477200000 // 2021-01-01 14:00:00
    }
  ];

  useLayoutEffect(() => {
    if (divRef.current) {
      const chart = am4core.create(divRef.current, am4charts.XYChart);
      chart.height = 200;
      chart.paddingTop = 0;
      chart.paddingBottom = 0;
      chart.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss";
      chart.data = chartData;

      const dateAxis = chart.xAxes.push(new am4charts.DateAxis());
      dateAxis.extraMin = 0.1;
      dateAxis.extraMax = 0.1;
		dateAxis.renderer.labels.template.location = 0.0001;

      const categoryAxis = chart.yAxes.push(new am4charts.CategoryAxis());
      categoryAxis.dataFields.category = "category";
      categoryAxis.renderer.grid.template.location = 0;
      categoryAxis.renderer.inversed = true;

      const series = chart.series.push(new am4charts.ColumnSeries());
      series.dataFields.openDateX = "start";
      series.dataFields.dateX = "end";
      series.dataFields.categoryY = "category";
      series.columns.template.tooltipText =　"category: {category} \n 開始: {openDateX} \n 終了: {dateX}";
      series.columns.template.tooltipPosition = "pointer";

      const scrollbar = new am4charts.XYChartScrollbar();
      chart.scrollbarX = scrollbar;
      scrollbar.series.push(series);

      chart.cursor = new am4charts.XYCursor();

      chart.plotContainer.events.on("doublehit", () =>
        dateAxis.zoom({ start: 0, end: 1 })
      );

      chartRef.current = chart;

      return () => chart.dispose();
    }
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ height: 200, width: "100%" }} />
    </div>
  )
};
```

### 完成イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535721/3aaafb83-8d70-9238-fc43-02b07c7987eb.png)

* 補足として、今回は`chartRef.current = chart;`の1行でしか`chartRef`は使っていないが、最もよくあることとして適用するデータを更新する際に以下のように別の`useLayoutEffect`内で使用される

```typescript
useLayoutEffect(() => {
  if (chartRef.current) {
    chartRef.current.data = chartData;
  }
}, [chartData]);
```

## おわりに
今回はAmchartsを使ってガントチャートを例として実装した。しかし全体的な実装の方法や流れは他のチャートであっても大きく変わりはなく、イベントといった強力な機能の使い方も見せたので、ガントチャートでなくても応用してほしい。
また例ではあくまでベタ書きのようなコードとなっているので、必要であれば当然どんどんリファクタをしていく必要はある。
Amchartsは情報の多くは公式の説明かgithubのissueで集めるしかなく、複雑な要件を満たすチャートを作るには、基本的に類似したチャートを説明している公式ページを読んで最終的には自分で手探りで実装していくしかないので大変なことが多いが、意外とできないことはないので頑張ってほしい。