【Recharts】少し特殊なユースケース【React】


## はじめに

React アプリケーションにおいてグラフの UI を作成したいというのはよくあると思うが、3rd パーティ製ライブラリの選択肢が非常に多く、どれが良いのか迷ってしまいがちである。
React のグラフライブラリ紹介記事などでいくつかみてみたところ、**Recharts** がバランスが良さそうに感じたので使ってみることにした。

https://github.com/recharts/recharts

## やりたかったこと

### あるグラフの上に別のタイプのグラフを重ねる

例えば、折れ線グラフで示した平均気温のデータを、棒グラフで示した降水量のデータに重ねて表示するようなシチュエーションである。

### Area Chart の上下反転

面グラフ (**Area Chart**)[^1] は通常、グラフ縦軸の下から積み上がっていくが、これを「上から積み下げる」ような形で表示したい。

[^1]: 面グラフについてはこちら: https://ja.wikipedia.org/wiki/%E9%9D%A2%E3%82%B0%E3%83%A9%E3%83%95

## 環境・前提

```
- react: 18.3.1
- next: 14.2.3
- recharts: 2.13.0-alpha.1
```

## storybook で概要を確認

### ウェブ

下記 URL にて、ウェブ上で稼働している storybook があるため、いち早くグラフの見た目や使い方をサクッと確認するにはベスト。

https://recharts.org/en-US/storybook

### ローカル

既存の storybook に少し手を加えながら動作確認してみたい場合は、ローカルで storybook を起動し、コードをいじりながらやるのが良い。

```sh
$ git clone https://github.com/recharts/recharts.git
$ cd recharts
$ npm install
$ npm run storybook
```

## 作業

ここからは Next.js アプリへの Recharts の導入と使用方法について記述する。

### 導入

```sh
npm install recharts
```

### ユースケース 1: あるグラフの上に別のタイプのグラフを重ねる

```tsx
import {
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
  LineChart,
  Line,
  BarChart,
  Bar,
} from 'recharts';

// データ
const data = [
  {
    month: 'Jan',
    avgTemp: 3.1,
    sumPcpn: 50.6,
  },
  {
    month: 'Feb',
    avgTemp: 4.2,
    sumPcpn: 47.1,
  },
  {
    month: 'Mar',
    avgTemp: 7.7,
    sumPcpn: 95.5,
  },
  {
    month: 'Apr',
    avgTemp: 12.8,
    sumPcpn: 109.8,
  },
  {
    month: 'May',
    avgTemp: 17.4,
    sumPcpn: 129.8,
  },
  {
    month: 'Jun',
    avgTemp: 20.8,
    sumPcpn: 131.8,
  },
  {
    month: 'Jul',
    avgTemp: 24.6,
    sumPcpn: 134.6,
  },
  {
    month: 'Aug',
    avgTemp: 25.9,
    sumPcpn: 118.2,
  },
  {
    month: 'Sep',
    avgTemp: 22.3,
    sumPcpn: 187.6,
  },
  {
    month: 'Oct',
    avgTemp: 16.6,
    sumPcpn: 193.5,
  },
  {
    month: 'Nov',
    avgTemp: 10.5,
    sumPcpn: 79.1,
  },
  {
    month: 'Dec',
    avgTemp: 5.3,
    sumPcpn: 48.5,
  },
];

export const ErrorBudget = () => {
  const w = 600;
  const h = 400;

  return (
    <ResponsiveContainer width="100%" height="100%">
      <div style={{ position: 'relative' }}> // ①
        <BarChart
          data={data}
          width={w}
          height={h}
          style={{
            position: 'absolute', // ①
            left: '34px', // ②
            width: '100%',
          }}
          margin={{
            top: 0,
            right: 0,
            left: -48,
            bottom: 0,
          }}
          barSize={20}
        >
          <Bar dataKey="sumPcpn" fill="royalblue" />
          <XAxis tick={false} />
          <YAxis
            fontSize="0.5rem"
            domain={[0, 250]} // ③
            orientation="right" // ②
            tickFormatter={(value) => `${value}mm`}
          />
        </BarChart>
        <LineChart
          width={w}
          height={h}
          data={data}
          style={{
	      position: 'absolute', // ①
	      left: '-60px', // ②
	      width: '100%',
	     }}
          margin={{
            top: 0,
            right: 0,
            bottom: 0,
            left: 0,
          }}
        >
          <Tooltip />
          <XAxis dataKey="month" fontSize="0.5rem" interval={0} />
          <YAxis
            fontSize="0.5rem"
            domain={[0, 35]} // ③
            tickFormatter={(value) => `${value}℃`}
          />
          <Line
            type="monotone"
            dataKey="avgTemp"
            stroke="tomato"
            strokeWidth={2}
          />
        </LineChart>
      </div>
    </ResponsiveContainer>
  );
};
```

このコードでは、`data` で定義した 12 ヶ月分の{月平均気温 (`avgTemp`)、月間降水量 (`sumPcpn`)}の情報をグラフに表示している。`LineChart` が月平均気温を、`BarChart` が月間降水量に対応している。
詳細の説明は API ドキュメントや storybook に譲るが、実装上のポイントとなると思った点を以下に挙げる:

- 二つのグラフを位置的にざっくり重ね合わせる (①)
	- `position='relative'` のプロパティを持った div の下に二つのグラフ (`BarChart`, `LineChart`) を配置し、それらを `position='absolute'` 指定することで、親要素からの相対距離で位置を指定する。
- 一つのグラフの Y 軸は右側に持ってくる (②)
	- デフォルトでは Y 軸はグラフ左側に表示される。メモリが重なるとみづらいため、降水量データの Y 軸を右側に移動させる。`<BarChart>` > `<YAxis>` > `orientation="right"` を指定することで実現できる。なおこの際、グラフのデータ部分が左側に寄ってしまうため、BarChart, LineChart それぞれの `style.left` を調整することで表示崩れを整えている。
- Y 軸範囲を設定する (③)
	- `<BarChart>` \> `<YAxis>` \> `domain` および `<LineChart>` \> `<YAxis>` \> `domain` に上下限値を指定する。

これによって作成されたグラフの一例が以下である

![スクリーンショット 2024-07-14 23.52.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/4180df4f-a83f-9265-30d0-da3927e384b0.png)


### ユースケース 2: Area Chart の上下反転

```tsx
import { ResponsiveContainer, XAxis, YAxis, AreaChart, Area } from 'recharts';

// データ
const data = [
  {
    month: 'Jan',
    sumPcpn: 50.6,
    sumSnow: 60,
    dummy: 0,
  },
  {
    month: 'Feb',
    sumPcpn: 47.1,
    sumSnow: 50,
    dummy: 0,
  },
  {
    month: 'Mar',
    sumPcpn: 95.5,
    sumSnow: 10,
    dummy: 0,
  },
  {
    month: 'Apr',
    sumPcpn: 109.8,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'May',
    sumPcpn: 129.8,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'Jun',
    sumPcpn: 131.8,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'Jul',
    sumPcpn: 134.6,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'Aug',
    sumPcpn: 118.2,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'Sep',
    sumPcpn: 187.6,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'Oct',
    sumPcpn: 193.5,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'Nov',
    sumPcpn: 79.1,
    sumSnow: 0,
    dummy: 0,
  },
  {
    month: 'Dec',
    sumPcpn: 48.5,
    sumSnow: 10,
    dummy: 0,
  },
];

export const ErrorBudget = () => {
  const w = 600;
  const h = 400;

  // ①
  const maxSum = data.reduce(
    (acc, cur) => Math.max(acc, cur.sumPcpn + cur.sumSnow),
    0,
  );
  // ①
  const data2 = data.map((x) =>
    Object.assign(x, { dummy: maxSum - (x.sumPcpn + x.sumSnow) }),
  );

  return (
    <ResponsiveContainer>
      <div>
        <AreaChart data={data2} width={w} height={h}>
          <XAxis dataKey="month" fontSize="0.5rem" interval={0} />
          <YAxis
            fontSize="0.5rem"
            domain={[0, maxSum]} // ①
            tickFormatter={(value) => `${value}mm`}
          />

    	　  // ②
          <Area
            type="monotone"
            dataKey="dummy"
            stackId="1"
            stroke="seashell"
            fill="seashell"
          />
          <Area
            type="monotone"
            dataKey="sumSnow"
            stackId="1"
            stroke="powderblue"
            fill="powderblue"
          />
          <Area
            type="monotone"
            dataKey="sumPcpn"
            stackId="1"
            stroke="royalblue"
            fill="royalblue"
          />
        </AreaChart>
      </div>
    </ResponsiveContainer>
  );
};
```

このコードでは、`data` で定義した 12 ヶ月分の{月間降水量 (`sumPcpn`)、月間降雪量 (`sumSnow`)}の情報を二つの AreaChart グラフに表示している。
実装上のポイントは以下:

- AreaChart を上下反転（上から積み下げていく）ためには、一番下の層となる部分も何らかのデータが必要となるため、それを `dummy` として定義する。dummy の値は、「各月の降水量と降雪量の値の合計値に足すと全てが等しくなるような最小の数」ということになる。つまり、**降水量と降雪量の最大値から、各月に対する降水量と降雪量の合計を引いたものを、各月に足せば良い** ので、その辺りの計算を行っている (①)。
- `<AreaChart>` 内の `<Area>` は、記述順に下から上に積み重なっていく。そのため、前述の dummy 層を最初に記述し、次は降雪量、最後が降水量という順番にすることで、その逆順に上から下に向けて積み下がっていくような表現になる (②)。

これによって作成されたグラフの一例が以下である

![スクリーンショット 2024-07-15 0.50.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/05661720-6b56-019e-deed-33947b01abbd.png)


## 終わりに

Recharts を使えば、基本的なグラフ描画には困らないと感じた。また、本ライブラリは React18 にも対応しているが、他のいくつかのグラフライブラリでは対応していないものもあるようで、重要なファクターである。さらに、最近のライブラリでは storybook の整備が充実していることが多く、開発体験をより加速させてくれて大変ありがたい。