[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
🫑

Reactグラフライブラリの検証の備忘録(MUI X Charts, Recharts, Victory)

2024/09/19に公開

こんにちは!最近会社で開発している機能について、「グラフ化したらめちゃくちゃユーザーさん見やすくなりそうだな」と思いまして、React のグラフライブラリについて調べてみました。

この記事では、React グラフライブラリの代表格である RechartVictoryMUI X Charts の 3 つを比較検証したその特徴をご紹介しつつ、全体的に気づいたことも揃えてつらつらと書いてみました。

同じようにグラフライブラリを比較検討されている方や、これから利用し始める方にとって、少しでも役に立つ記事になれば嬉しいです。

検証準備

検証に入る前に、いくつか基本的なところについて残しておきます。

グラフライブラリでよく出てくる用語

実際に比較検証を行う前に、グラフについて学んだ中で、よく出る目撃した単語をまとめました。これらの単語はすべてのライブラリで共通して利用されているため、グラフ周辺のユビキタスとして知っておくと良さそうです。

用語 意味
data, dataset グラフに反映させるデータ
series データの項目(折れ線グラフと棒グラフを同時に 2 種類表示、さまざまな棒グラフを並列に表示、など複数のシリーズで成り立つグラフがある。降水量と気温、商品 A,B,C などが個別のシリーズにあたる)
axis 軸(X 軸,Y 軸)
tooltip グラフをホバーした時に表示されるデータ詳細
legand 凡例

基本情報での比較

ライブラリ名 作成日 最終更新日 週間ダウンロード数 バンドルサイズ
MUI X Charts 約 4 年前 2024/9/17 197,274 245.51kb ※
Rechart 約 9 年前 2024/9/17 2,062,123 598.2kb
Victory 約 9 年前 2024/9/12 233,805 490.9kb

※ バンドルサイズはBUNDLEPHOBIAを利用して出しましたが、MUI X Charts は見つからなかったため自分でサイズをバンドルしてきて算出しました。そのため、他との比較としては曖昧なところがあります。また、MUI X Charts は本家 MUI と依存関係があるため、Rechart や Victory よりも総合的にバンドルサイズが大きくなる可能性もあると思っています。

Rechart や Victory が先駆者で、MUI X Charts が後発ですね。ただ、どのライブラリも開発は活発に行われており、その中では Rechart がデファクトスタンダードになっていそうです。

また、チャートライブラリということもあってか、サイズはどれも大きそうというイメージでした。

比較検証

ここからは、3 つのライブラリについて詳しく見ていきました。折れ線グラフや棒グラフなどのシンプルなグラフで検証を進めていきますが、特に以下が気になっていましたので、実現できるかを見ていきます。

  • 棒グラフと折れ線グラフのシリーズを足すグラフ、積み上げ棒グラフなどは対応できるか
  • デザインカスタマイズの柔軟性はどの程度あるか
  • パフォーマンスには問題がないか

すべての検証コードは以下となっております(バーっと検証した感じで結構汚くてゴメンナサイ)。
https://stackblitz.com/edit/vitejs-vite-mfgmbk

MUI X Charts

MUI は UI ライブラリとしてよくお世話になっていたため、まず最初にこちらを実際に書いてみました。

MUI Sample1

MUI Sample1 シンプルな棒線グラフ
import { BarChart } from "@mui/x-charts";
import { FC, useMemo, useState } from "react";
import { axisClasses } from "@mui/x-charts/ChartsAxis";
import {
  RadioGroup,
  FormControl,
  FormLabel,
  FormControlLabel,
  Radio,
  Stack,
  Typography,
  Box,
} from "@mui/material";
import { DataSelect, items, type Item } from "../datas/item";

const createDataset = (currentSelect: DataSelect) => {
  const customDataset: Item[] = [];
  switch (currentSelect) {
    case "ringo":
      customDataset.push(
        ...items.map((data) => ({
          itemA: data.itemA,
          month: data.month,
        }))
      );
      break;
    case "mikan":
      customDataset.push(
        ...items.map((data) => ({
          itemB: data.itemB,
          itemC: data.itemC,
          month: data.month,
        }))
      );
      break;
    case "gokei":
      customDataset.push(
        ...items.map((data) => ({
          itemB: data.itemB,
          itemA: data.itemA,
          month: data.month,
        }))
      );
      break;
  }
  return customDataset;
};

export const Sample1: FC = () => {
  const [currentDataSelect, setDataCurrentSelect] =
    useState<DataSelect>("gokei");

  const series = useMemo(() => {
    const s = [];
    const valueFormatter = (value: number | null) => `${value}`;

    if (currentDataSelect === "ringo" || currentDataSelect === "gokei") {
      s.push({
        dataKey: "itemA",
        stack: "A",
        label: "りんごがとれた数",
        valueFormatter,
        color: "#62b4f7",
      });
    }

    if (currentDataSelect === "mikan" || currentDataSelect === "gokei") {
      s.push({
        dataKey: "itemB",
        label: "みかんがとれた数",
        stack: "A",
        valueFormatter,
        color: "#7fea79",
      });
    }

    if (currentDataSelect === "mikan") {
      s.push({
        dataKey: "itemC",
        label: "有田みかんの数",
        valueFormatter,
        color: "#f8ed69",
      });
    }

    return s;
  }, [currentDataSelect]);

  return (
    <Box>
      <Typography variant="h6">
        MUI X Charts Sample 1 bar only : りんごとみかんがとれた数
      </Typography>

      <FormControl sx={{ py: 2 }}>
        <FormLabel id="data-select">表示データ</FormLabel>
        <RadioGroup
          aria-labelledby="data-select"
          name="controlled-radio-buttons-group"
          value={currentDataSelect}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
            setDataCurrentSelect(
              (event.target as HTMLInputElement).value as DataSelect
            );
          }}>
          <Stack direction="row">
            <FormControlLabel
              value="mikan"
              control={<Radio />}
              label="みかん"
            />
            <FormControlLabel
              value="ringo"
              control={<Radio />}
              label="りんご"
            />
            <FormControlLabel value="gokei" control={<Radio />} label="合計" />
          </Stack>
        </RadioGroup>
      </FormControl>

      <div className="bar-chart">
        <BarChart
          dataset={createDataset(currentDataSelect)}
          axisHighlight={{
            x: "band",
          }}
          // barLabel={(item: BarItem, _: BarLabelContext) => String(item.value)}
          borderRadius={2}
          xAxis={[
            {
              scaleType: "band",
              dataKey: "month",
              tickPlacement: "start",
              tickLabelPlacement: "middle",
              position: "top",
              // typeエラーになるが設定はできる
              // categoryGapRatio: 0.1,
              // barGapRatio: 0.1,
            },
          ]}
          yAxis={[
            {
              // label: 'rainfall',
              labelStyle: {
                fontSize: 12,
              },
            },
          ]}
          series={series}
          grid={{ horizontal: true }}
          // ここで全体のバーの大きさは調整
          width={1000}
          height={300}
          margin={{
            left: 30,
            bottom: 40,
          }}
          sx={{
            [`.${axisClasses.left} .${axisClasses.label}`]: {
              transform: "translateX(-10px)",
            },
          }}
          slotProps={{
            legend: {
              direction: "row",
              position: { vertical: "top", horizontal: "left" },
              padding: 0,
              labelStyle: {
                fontSize: 14,
              },
            },
          }}
          tooltip={{
            trigger: "axis",
          }}
        />
      </div>
    </Box>
  );
};

MUI Sample2

MUI Sample2 複数シリーズ,積み上げ棒グラフ
import { FC } from "react";
import {
  ResponsiveChartContainer,
  LinePlot,
  BarPlot,
  ChartsXAxis,
  ChartsYAxis,
  ChartsAxisHighlight,
  ChartsLegend,
  ChartsTooltip,
  ChartsAxisTooltipContent,
} from "@mui/x-charts";
import { Box, Typography } from "@mui/material";

export const Sample2: FC = () => {
  return (
    <Box>
      <Typography variant="h6">
        MUI X Charts Sample2 bar and line :
        りんごとみかんの総数ととても良いりんご率
      </Typography>
      <Box sx={{ width: "100%", maxWidth: 600 }}>
        <ResponsiveChartContainer
          dataset={[
            {
              ringo: 10,
              mikan: 20,
              bestRingo: 4,
              bestMikan: 3,
              bestRingoPercentage: 10,
              bestMikanPercentage: 30,
              month: "2023/04",
            },
            {
              ringo: 15,
              mikan: 26,
              bestRingo: 5,
              bestMikan: 7,
              bestRingoPercentage: 2,
              bestMikanPercentage: 10,
              month: "2023/05",
            },
            {
              ringo: 40,
              mikan: 23,
              bestRingo: 8,
              bestMikan: 2,
              bestRingoPercentage: 50,
              bestMikanPercentage: 5,
              month: "2023/06",
            },
          ]}
          xAxis={[
            {
              scaleType: "band",
              dataKey: "month",
              id: "x-axis-month",
            },
          ]}
          yAxis={[
            {
              labelStyle: {
                fontSize: 12,
              },
              id: "y-axis",
            },
          ]}
          series={[
            {
              dataKey: "ringo",
              type: "bar",
              stack: "A",
              label: "りんごの総数",
              color: "#62b4f7",
            },
            {
              dataKey: "mikan",
              type: "bar",
              stack: "A",
              label: "みかんの総数",
              color: "#7fea79",
            },
            {
              type: "bar",
              dataKey: "bestRingo",
              stack: "B",
              label: "よいりんご",
              color: "#62b4f7",
            },
            {
              dataKey: "bestMikan",
              type: "bar",
              stack: "B",
              label: "よいみかん",
              color: "#7fea79",
            },
            {
              type: "line",
              dataKey: "bestRingoPercentage",
              label: "よいりんご率",
              color: "#0846a3",
            },
            {
              type: "line",
              dataKey: "bestMikanPercentage",
              label: "よいみかん率",
              color: "#0846a3",
            },
          ]}
          height={400}
          margin={{ left: 30 }}>
          <BarPlot />
          <LinePlot />
          <ChartsXAxis axisId="x-axis-month" />
          <ChartsYAxis axisId="y-axis" />
          <ChartsTooltip
            trigger="axis"
            slots={{
              axisContent: ({ axisData, classes, series }) => {
                // ここで比率や前週比を出す
                // ChartsAxisTooltipContentを書き換える必要あり
                return (
                  <>
                    <ChartsAxisTooltipContent
                      axisData={axisData}
                      classes={classes}
                      contentProps={{
                        series: series,
                      }}
                    />
                  </>
                );
              },
            }}
          />
          <ChartsAxisHighlight />
          <ChartsLegend
            position={{ vertical: "top", horizontal: "left" }}
            slotProps={{ legend: { labelStyle: { fontSize: 14 } } }}
          />
        </ResponsiveChartContainer>
      </Box>
    </Box>
  );
};

所感としては以下の通りです。

  • 複数のシリーズ表示,積み上げ棒グラフなどの機能はすべて揃っている。
  • BarChart, LineChart というコンポーネントに対して、多くの設定を Props で渡していくパターンと、子要素に欲しいパーツ(凡例, X/Y 軸, ツールチップ等)のコンポーネントを渡していくパタン(コンポジションパターン)の 2 通りの書き方がある。
    • 1 つのコンポーネントに Props をつける場合だと、チャートなのでどうしても Props が多くなりがちで、該当の Props がどのパーツ用なのかの見通しが悪いと感じた。そのため、コンポジションパターンでの書き方の方が直感的で嬉しかった。
  • スタイリングやクラスの付け替えなどを利用して、細かいところまでスタイリングが可能。余白や全体バランスを見て気になるところは地道な修正で手が届きそう。
    • ただしサンプルコードの通りコードが複雑になる印象だった(axisClassestransformなど)。

overviewに書いてある通り、中身の実装には D3.js というライブラリと SVG が利用されています。アニメーションには react-spring が採用されていました。

Recharts

続いて圧倒的によくダウンロードされているらしい Recharts を見てみました。

Recharts Sample

Recharts Sample 複数シリーズ,折れ線グラフ
import { FC, useState } from "react";
import {
  Bar,
  BarChart,
  Legend,
  Tooltip,
  Rectangle,
  ResponsiveContainer,
  XAxis,
  YAxis,
} from "recharts";
import { DataSelect, items } from "../datas/item";
import {
  Box,
  FormControl,
  FormControlLabel,
  FormLabel,
  Radio,
  RadioGroup,
  Stack,
  Typography,
} from "@mui/material";

export const Sample: FC = () => {
  const [currentDataSelect, setDataCurrentSelect] =
    useState<DataSelect>("gokei");

  return (
    <Box sx={{ width: 1000 }}>
      <Typography variant="h6">
        Recharts Sample 1 bar only : りんごとみかんがとれた数
      </Typography>

      <FormControl sx={{ py: 2 }}>
        <FormLabel id="data-select">表示データ</FormLabel>
        <RadioGroup
          aria-labelledby="data-select"
          name="controlled-radio-buttons-group"
          value={currentDataSelect}
          onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
            setDataCurrentSelect(
              (event.target as HTMLInputElement).value as DataSelect
            );
          }}>
          <Stack direction="row">
            <FormControlLabel
              value="mikan"
              control={<Radio />}
              label="みかん"
            />
            <FormControlLabel
              value="ringo"
              control={<Radio />}
              label="りんご"
            />
            <FormControlLabel value="gokei" control={<Radio />} label="合計" />
          </Stack>
        </RadioGroup>
      </FormControl>
      <Box height={400}>
        <ResponsiveContainer width="100%" height="100%">
          <BarChart
            width={500}
            height={1000}
            data={items}
            margin={{
              top: 0,
              right: 0,
              left: 0,
              bottom: 5,
            }}>
            <XAxis dataKey="month" />
            <YAxis max={500} />
            <Legend
              align="left"
              verticalAlign="top"
              height={40}
              margin={{ bottom: 100, top: 100 }}
            />
            <Tooltip />
            {(currentDataSelect === "ringo" ||
              currentDataSelect === "gokei") && (
              <Bar
                dataKey="itemA"
                barSize={40}
                fill="#8884d8"
                stackId="a"
                name="りんごがとれた数"
                activeBar={<Rectangle fill="#808080" stroke="#808080" />}
              />
            )}
            {(currentDataSelect === "mikan" ||
              currentDataSelect === "gokei") && (
              <Bar
                dataKey="itemB"
                barSize={30}
                stackId="a"
                fill="#82ca9d"
                name="みかんがとれた数"
                activeBar={<Rectangle fill="#808080" stroke="#808080" />}
              />
            )}
            {currentDataSelect === "mikan" && (
              <Bar
                dataKey="itemC"
                stackId="b"
                barSize={20}
                name="有田みかんの数"
                fill="pink"
                activeBar={<Rectangle fill="#808080" stroke="#808080" />}
              />
            )}
          </BarChart>
        </ResponsiveContainer>
      </Box>
    </Box>
  );
};

所感としては以下の通りです。

  • 複数のシリーズ表示,積み上げ棒グラフなどの機能はすべて揃っている。
  • MUI X Charts とは異なり、コンポジションパターンでしか設計ができないようになっている。
  • グラフメモリの大きさや種類などは変えられるが、細かいところをちょこちょこカスタマイズするというのは難しそう。そのため全体として見通しの良いシンプルな使い方になる。しかし、Tooltip の contentPropsのようにどっさりと入れ替えるような手段も残されている。

Recharts も D3.js と SVG がメインで、ここは MUI X Charts と似ているように感じました。Recharts は 2016 年ごろから存在する昔からのライブラリで、現在でも内部ではクラスコンポーネントが利用されていました。また、アニメーションには react-smooth というライブラリが利用されていましたが、これは Recharts が作ったものになっています。react-spring が出てきたのは 2018 年ごろだったため、当時は手頃なアニメーションライブラリがなくて作った、ということなのでしょうか?

Victory

Victory のサンプルも色々と動かして作ってみようとしたのですが、結果的にドキュメントを読むにとどめました。理由は後述しますが、その前に Victory についての書簡は以下の通りです。

  • 複数のシリーズ表示,積み上げ棒グラフなどの機能はすべて揃っている。
  • MUI X Charts や Recharts と比べて、カスタマイズ性に富んでいて多機能。
  • React Native でも利用できるのが推し。

こちらも Recharts と同じくらい前から存在しているライブラリであり、内部ではクラスコンポーネントが利用されていました。

今回 Victory を実際に書いて検証しなかった理由は、これまで見てきた MUI X Charts と Recharts と内部的な構造やライブラリの利用方法が非常に似ていたため、ほぼ同じだろうと思ったためです。この時点で検証の必要性が半減しましたが、さらに Victory のような多機能性を今回は求めていない(つまり MUI X Charts や Recharts で十分)と判断したため、検証を取りやめた次第です。

グラフライブラリの特徴

ここまで検証できればある程度どんなグラフツールがあるのかを想像することができました。ここからは、ここまで見ているうちに気づいた、React のグラフライブラリの特徴について、考えたことをまとめてみます。

依存パッケージ

また、MUI X Charts をはじめとした MUI X シリーズは、複雑な UI を実現するためのパッケージとして小分けにされています。よく見てみると x-charts とは別に@mui/x-charts-vendor が存在しており、D3.js の実装はこのライブラリに閉じるようにしているみたいですね。

D3.js については後述しますが、シンプルなグラフだけではなくさまざまなデータビジュアライゼーションを実現するための低レベルなライブラリ。MUI X Charts の作りとしては以下 2 つに分かれていまして、@mui/x-charts-vendor は D3.js をラップしたようなパッケージになっていました。

  • @mui/x-charts
  • @mui/x-charts-vendor

@mui/x-charts-vendor について確認したところ、こちらの役割としては D3.js を ES Modules でも CommonJS でも利用できるようにベンダリングしたものでした。D3.js は ES Modules のみをサポートしますが、MUI は CommonJS でも動くようにしたかったということです。

https://www.npmjs.com/package/@mui/x-charts-vendor

Recharts や Victory の構造についても確認しましたが、これらのライブラリは以下の victory-vendor に依存して D3.js の機能にアクセスしていました。深くは見れていませんが、こちらも@mui/x-charts-vendor と全く同じような機構になっています。

https://www.npmjs.com/package/victory-vendor

両者はほぼ同じ意図を持ったベンダリングの機構ですが、victory-vendor の方が今は古くなった D3.js のパッケージを利用しているくらいの違いでした(victory-vendor が依存している 3-voronoi については、2019 年にリポジトリがアーカイブされています)。

MUI X Charts は後発なので victory-vendor を利用する流れもあったのかなと思いましたが、MUI の中に依存するパッケージを閉じるようにすることを選んだということ。ライブラリ開発の哲学を感じますね。

D3.js

D3.js + SVG がデファクトとなっている React グラフライブラリ界隈ですが、呼び出し側で D3.js を意識することはほぼなかったです。D3.js を生で利用するとどのような感じになるのか気になったため、実際に簡単な表を実装してみました。

D3.js Sample

D3.js Sample 複数シリーズ
import { Box, Stack, Typography } from "@mui/material";
import * as d3 from "d3";
import { format } from "date-fns";
import { FC, useMemo, useRef, useState } from "react";
import { useMount } from "react-use";

type Props = {
  data?: { date: Date; lineValue: number; barValue: number }[];
  width?: number;
  height?: number;
  marginTop?: number;
  marginRight?: number;
  marginBottom?: number;
  marginLeft?: number;
};
export const Sample1: FC<Props> = ({
  data = [
    { date: new Date("2021/01"), lineValue: 15, barValue: 34 },
    { date: new Date("2021/02"), lineValue: 22, barValue: 52 },
    { date: new Date("2021/03"), lineValue: 36, barValue: 100 },
    { date: new Date("2021/04"), lineValue: 41, barValue: 32 },
    { date: new Date("2021/05"), lineValue: 59, barValue: 5 },
    { date: new Date("2021/06"), lineValue: 89, barValue: 150 },
    { date: new Date("2021/07"), lineValue: 17, barValue: 44 },
  ],
  width = 640,
  height = 400,
  marginTop = 40,
  marginRight = 40,
  marginBottom = 20,
  marginLeft = 40,
}) => {
  const svgRef = useRef<SVGSVGElement | null>(null);
  const [tooltip, setTooltip] = useState<{
    x: number;
    y: number;
    date: Date;
    value: number;
    show: boolean;
  } | null>(null);

  const baseColor = useMemo(() => {
    const color = d3.color("steelblue");
    if (color === null) {
      throw new Error("color is null");
    }
    return color;
  }, []);

  const dateExtent = d3.extent(data, (d) => d.date) as [Date, Date];
  // 日付の範囲に余裕を加える
  const startDate = new Date(dateExtent[0].getTime());
  startDate.setDate(startDate.getDate() - 15);
  const endDate = new Date(dateExtent[1].getTime());
  endDate.setDate(endDate.getDate() + 15);

  const dateX = d3
    .scaleTime()
    .domain([startDate, endDate])
    .range([marginLeft, width - marginRight]);

  const combinedExtent = [
    Math.min(...data.map((d) => d.lineValue), ...data.map((d) => d.barValue)),
    Math.max(...data.map((d) => d.lineValue), ...data.map((d) => d.barValue)),
  ];
  const commonY = d3
    .scaleLinear()
    .domain(combinedExtent)
    .nice() // スケールの端数を整える
    .range([height - marginBottom, marginTop]);

  const line = d3
    .line<{ date: Date; lineValue: number }>()
    .x((d) => dateX(d.date))
    .y((d) => commonY(d.lineValue))
    .curve(d3.curveCatmullRom.alpha(0.5));
  const d = line(data);
  if (d === null) {
    throw new Error("line is null");
  }

  const backgroundBarX = d3
    .scaleBand()
    .range([marginLeft, width - marginRight])
    .domain(
      data.map(function (d) {
        return format(d.date, "yyyy/MM");
      })
    )
    .padding(0);
  const xAxis = d3
    .axisBottom(dateX)
    .ticks(10)
    .tickFormat((d) => d3.timeFormat("%Y/%m")(d as Date));

  const yAxis = d3
    .axisLeft(commonY)
    .ticks((commonY.domain()[1] - commonY.domain()[0]) / 10)
    .tickSize(5)
    .tickSizeOuter(4)
    .tickFormat((d) => `${d} !`);

  const xAxisRef = useRef<SVGGElement | null>(null);
  const yAxisRef = useRef<SVGGElement | null>(null);

  useMount(() => {
    if (xAxisRef.current) {
      d3.select(xAxisRef.current).call(xAxis);
    }
    if (yAxisRef.current) {
      d3.select(yAxisRef.current).call(yAxis);
    }

    const svg = d3.select(svgRef.current);

    // 棒グラフ
    svg
      .selectAll(".bar") // 要素がない
      .data(data)
      .join("rect") // 要素を追加
      .attr("x", function (d) {
        const formatted = format(d.date, "yyyy/MM");
        const barX = backgroundBarX(formatted);
        if (barX === undefined) {
          throw new Error("barX is undefined");
        }
        return barX + backgroundBarX.bandwidth() * 0.1;
      })
      .attr("y", function (d) {
        return commonY(d.barValue);
      })
      .attr("width", function () {
        return backgroundBarX.bandwidth() * 0.8; // 例として80%の幅を設定
      })
      .attr("height", function (d) {
        return height - commonY(d.barValue) - marginBottom - 1;
      })
      .attr("fill", baseColor.brighter(0.5).formatHex());

    // Lineグラフ
    svg
      .append("path")
      .datum(data)
      .attr("fill", "none")
      .attr("stroke", baseColor.brighter(1).formatHex())
      .attr("stroke-width", 1.5)
      .attr("d", d);

    const circlesGroup = svg.append("g").attr("class", "circles-group");
    circlesGroup
      .selectAll("circle")
      .data(data)
      .join("circle")
      .attr("fill", "white")
      .attr("stroke", baseColor.brighter(1).formatHex())
      .attr("stroke-width", 1.5)
      .attr("cx", (d) => dateX(d.date))
      .attr("cy", (d) => commonY(d.lineValue))
      .attr("r", 5);

    // 背景(アクティブ検知)
    svg
      .selectAll(".backgroundBar")
      .data(data)
      .join("rect") // 要素を追加
      .attr("x", function (d) {
        const barX = backgroundBarX(format(d.date, "yyyy/MM"));
        if (barX === undefined) {
          throw new Error("barX is undefined");
        }
        return barX;
      })
      .attr("width", backgroundBarX.bandwidth())
      .attr("y", marginTop)
      .attr("height", function () {
        return height - marginTop - marginBottom;
      })
      .style("fill", "none")
      .style("pointer-events", "all")
      .on("mouseover", function (event, d) {
        d3.select(this).style("fill", "rgba(0, 0, 0, 0.1)");
        setTooltip({
          x: event.offsetX,
          y: event.offsetY,
          date: d.date,
          value: d.lineValue,
          show: true,
        });
      })
      .on("mouseout", function () {
        d3.select(this).style("fill", "none");
        setTooltip(null);
      });
  });

  return (
    <Stack spacing={4}>
      <Typography variant="h6">
        d3js Sample 1 bar only : かんたんな表
      </Typography>
      <Box sx={{ position: "relative" }}>
        <svg width={width} height={height} ref={svgRef}>
          {/** XAxis */}
          <g
            ref={xAxisRef}
            transform={`translate(0,${height - marginBottom})`}
          />
          {/** YAxis */}
          <g ref={yAxisRef} transform={`translate(${marginLeft},0)`} />
        </svg>
        {tooltip && tooltip.show ? "ツールチップあるよ" : "ツールチップないよ"}
      </Box>
    </Stack>
  );
};

こうしてみると Recharts や MUI X Charts とほぼ UI の形が似通っていたため、これらも D3.js の恩恵を多大に受けていると感じました。ただなかなか難しい形にはなったので、やはり Recharts や MUI X Charts に頼らせていただくのが良さそうですね。

ライブラリの検証にあたりパフォーマンス付近も鑑みる予定でしたが、大元の内部構造がほぼ同じとあればパフォーマンスに差分が出ることもあまり考えにくいと感じました(SVG はデータ量が大きくなったり複雑な UI にならない限り、パフォーマンスフレンドリーなイメージです)。

終わりに

色々と触って見て、個人的にはグラフ描画とコンポジションパターンの相性がとても良いように感じました。グラフには多くの情報が必要なので、情報の渡し方としてのわかりやすさ(XAxis というパーツコンポーネントには軸に必要な情報を渡す、など)や必要なパーツを子要素に入れるという直感性が素敵だなと思います。

中身も少し読みましたがなかなか難解でした。また、各子要素のコンポーネントがどのパーツであるかは、コードレベルでは非常に泥臭く検証している様子も見受けられ(OSS 開発者の努力ですね)、それにつけてもコンポジションパターンの設計は難しそうとも思いましたが、機会があればチャレンジしてみたいです。

ただ結論として、グラフライブラリは正味なんでも良さそう、と思いました。作りが非常に似ており、それぞれベンチマークとして各ライブラリを見ていることもあると思いますので、どれをとっても大きい問題に直面しにくいと思いますし、何かあった時のリプレイスも比較的簡単そうという印象です。

ここまで読んでいただき、ありがとうございました。

Discussion