📈

LLMと相性のいいReactのChartライブラリを考察してみた🦜

2024/03/25に公開

今回はLLMと相性のいいReactのChartライブラリを探すために検証を行いました。ReactのChartライブラリといえば数多く種類が存在し、どれを採用するのがいいか迷うところです。下記のサイトはReactのChartライブラリが一覧で整理されており、これだけでも数十個候補になるライブラリがあることがわかります。

https://awesome.cube.dev/?tools=charts&frameworks=react

はじめに

本記事では数多くあるReactのChartライブラリの中からLLMと相性がいいライブラリを様々な観点から考察することを目的とします。

前提

Goalイメージ

HighCharts GPTというプロダクトを例に挙げますが、以下のようにLLMから生成した結果をグラフで表示させることを目的とします。

また、HighChartsは非常に優れたChartプロダクトですが、商用利用する際にはライセンスが有料となっているため今回は対象外としています。

https://www.highcharts.com/chat/gpt/

結論

少し検証内容が長くなってしまうので、先に考察結果を紹介します。

ライブラリ名 シンプルさ 拡張性 LLMとの相性 データ構造のタイプ
Recharts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造
AG Charts ⭐️⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造
Ant Design Charts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造
nivo ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️ オブジェクトベースのデータ構造
Unovis ⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造
Chart.js ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造
apexcharts ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造

1st Step: 最終更新日が半年以内のライブラリに絞り込む

まずは数あるライブラリの中から最終更新日が半年以内のものに絞り込みました。どれだけ魅力的なライブラリでも適切にメンテナンスされていなければ現場で採用するのは難しくなります。その結果以下の7件が該当しました。

*満遍なく検討しましたが、これが全てではありません。

ライブラリ名 Star数 説明 URL
Chart.js 63K 最もStar数の多いChartライブラリで、HTML5を使用したレスポンシブでモダンなチャートを簡単に実装できます。 Chart.js
Recharts 22K Reactベースのライブラリで、宣言的にカスタマイズ性高くチャートを構築できます。 Recharts
apexcharts 14K インタラクティブでモダンなデザインのチャートを提供するライブラリです。多種多様なタイプをサポートしており、簡単にリッチなチャートを作成できます。 apexcharts
AG Charts 59 高性能なJavaScriptチャートライブラリで、AG Gridの開発者によって作られました。大量のデータを扱うアプリケーション向けに最適化されており、スムーズなアニメーションとインタラクティブなチャート作成が可能です。 AG Charts
nivo 12K Reactをベースにした豊富なコンポーネントを提供するライブラリです。SVGとCanvasのレンダリングをサポートしており、大量のデータや複雑なインタラクションを扱うチャートに適しています。 nivo
Ant Design Charts 1.8K Ant Designシステムに基づいたReactのチャートライブラリで、優れたデザインとUXを提供します。data visualizationを簡単かつ効果的に実装できるように設計されています。 Ant Design Charts
Unovis 1.7K 強力なdata visualizationとデータ分析のためのJavaScriptライブラリで、複雑なデータセットを扱う際に特化しています。カスタマイズ可能で、多彩なタイプとインタラクティブな機能を備えています。 Unovis

2nd Step: 各ライブラリの比較から共通点を見つける

候補に上がるライブラリを絞ったので、次に各ライブラリを網羅的に確認しました。
すると、グラフに渡すデータ構造に大きく2つのパターンがあることがわかりました。

実際にRecharts(オブジェクトベース)とChart.js(配列ベース)を例に挙げてそれぞれ解説をします。

オブジェクトベースのデータ構造(Rechartsなど)

オブジェクトベースのデータ構造は以下の例です。

const data = [
  { label: 'March', pv: 23, uv: 13 },
  { label: 'April', pv: 45, uv: 25 },
  { label: 'May', pv: 56, uv: 36 },
  { label: 'June', pv: 67, uv: 47 }
];

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

メリット

ラベルに対して紐づくデータが構造化されている

配列ベースに対してデータの不一致がおきずらい。

デメリット

データ構造の中にValue値以外の情報を含めずらい

配列ベースのデータ構造で解説しますが、オブジェクトベースのデータ構造ではあくまでLabelに関連するValueを定義することに適しています。

配列ベースのデータ構造(Chart.jsなど)

配列ベースのデータ構造は以下の例です。

const data = {
  labels: ["March", "April", "May", "June"],
  datasets: [
    {
      label: 'pv',
      data: [23, 45, 56, 67],
    },
    {
      label: 'uv',
      data: [13, 25, 36, 47],
    },
  ]
};

https://www.chartjs.org/docs/latest/samples/line/line.html

メリット

各Valueに対して追加情報を付与できる。

const data = {
  labels: ["March", "April", "May", "June"],
  datasets: [
    {
      label: 'pv',
      data: [23000, 45000, 56000, 67000],
      // ↓ のような情報
      borderColor: "red",
      backgroundColor: "white",
      fill: false
    },
  ]
}

オブジェクトベースのデータ構造だった場合、それぞれのValueに対して追加情報を付与したい場合はデータ構造の外で定義する必要が生まれます。

デメリット

データ量が増すほど複雑になる

以下の例のようにデータ量が増えた際にはオブジェクトベースの方がシンプルな構造を保ったまま拡張することができます。

また、配列ベースだとIndexがズレた場合Labelに対してValueが不一致が起きる可能性があります。

オブジェクトベースのデータ構造が複雑化した場合
const data = [
  { label: 'March', pv: 23000, uv: 13000, revenue: 2000, newVisitors: 7000, sessions: 15000, bounceRate: 45 },
  { label: 'April', pv: 45000, uv: 25000, revenue: 4500, newVisitors: 15000, sessions: 30000, bounceRate: 50 },
  { label: 'May', pv: 56000, uv: 36000, revenue: 6000, newVisitors: 20000, sessions: 40000, bounceRate: 55 },
  { label: 'June', pv: 67000, uv: 47000, revenue: 8000, newVisitors: 22000, sessions: 50000, bounceRate: 60 }
];
配列ベースのデータ構造が複雑化した場合
const data = {
  labels: ["March", "April", "May", "June"],
  datasets: [
    {
      label: 'pv',
      data: [23000, 45000, 56000, 67000],
    },
    {
      label: 'uv',
      data: [13000, 25000, 36000, 47000],
    },
    {
      label: 'revenue',
      data: [2000, 4000, 6000, 8000],
    },
    {
      label: 'newVisitors',
      data: [7000, 15000, 20000, 22000],
    },
    {
      label: 'sessions',
      data: [15000, 30000, 40000, 50000],
    },
    {
      label: 'bounceRate',
      data: [45, 50, 55, 60],
    }
  ]
};

考察

これまでの内容から以下のことが考えられます。

  • オブジェクトベースの場合、シンプルさと確実性に長けている
  • 配列ベースは拡張性が高い一方で、データ量によっては複雑性が増し、データの不一致を起こす可能性が高まる

各ライブラリをパターンで仕分ける

1st Stepで絞り込んだライブラリをパターンで分けたところ、以下の表のようになりました。

ライブラリ データ構造のタイプ
Recharts オブジェクトベースのデータ構造
AG Charts オブジェクトベースのデータ構造
Ant Design Charts オブジェクトベースのデータ構造
Unovis オブジェクトベースのデータ構造
nivo オブジェクトベースのデータ構造
Chart.js 配列ベースのデータ構造
apexcharts 配列ベースのデータ構造

3rd Step: 実際にLLMからグラフを生成する

では実際に全てのライブラリでLLMからjsonデータを生成し、グラフで表示してみます。

前提

全てのライブラリに対して共通の内容でグラフを表示させてみます。

補足

  • system roleの内容を各Chartライブラリに合わせて設定します。
  • 入力内容がuser roleの内容として送信されます。
  • 複雑なプロンプトではなく最小限で構築します。

入力する内容

第1四半期(Q1)の売上は5,000,000ドルです。
第2四半期(Q2)の売上は5,500,000ドルです。
第3四半期(Q3)の売上は6,000,000ドルです。
第4四半期(Q4)では、売上が6,500,000ドルに達しました。

期待する出力結果

  • 四半期のQ1〜Q4をX軸のLabelに表示
  • 各期に対応するValueを表示

3つの項目で評価

そして、それぞれ私の体感になりますが以下3つの項目でライブラリを比較していきます。
各項目に対して最大⭐️5つで評価します。

  • シンプルさ
    • 注入するdataの構造がシンプルかどうか
  • 拡張性
    • グラフ自体のカスタマイズ性
  • LLMとの相性
    • LLMと組み合わせた時の相性

Rechartsの場合

system prompt
{
  role: "system",
  content: "You are a helpful assistant designed to output JSON. Return Data Format is `data: { label: {label}, sales: {value} }`.",
},
generated data
{
  "data": [
    {
      "label": "Q1",
      "sales": 5000000
    },
    {
      "label": "Q2",
      "sales": 5500000
    },
    {
      "label": "Q3",
      "sales": 6000000
    },
    {
      "label": "Q4",
      "sales": 6500000
    }
  ]
}
component.tsx
<LineChart data={data}>
  <CartesianGrid strokeDasharray="3 3" />
  <XAxis dataKey="label" />
  <YAxis />
  <Tooltip />
  <Legend />
  <Line type="monotone" dataKey="sales" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>

評価

シンプルさ:⭐️⭐️⭐️⭐️⭐️
拡張性:⭐️⭐️⭐️⭐️
LLMとの相性:⭐️⭐️⭐️

コメント

データ構造が非常にシンプルで、データ量の増加にも耐えうる構造だと感じました。

拡張性は高く柔軟にカスタマイズ可能ですが、内容によってはその分コード量が増えることが予想されます。

LLMとの相性の観点で気になったこととして、コンポーネント側でX軸、Y軸の変数名(ここではlabelとsales)を指定する必要があります。そのため、LLMから生成される値を事前に知っておく、もしくは一致させる必要があるのは注意点かと思いました。

AG Chartsの場合

system prompt
{
  role: "system",
  content: `You are a helpful assistant designed to output JSON. Return Data Format is 
    "data: { 
      title: {
        text: {title},
      },
      data: [{
        quarter: {quarter},
        sales: {value}
      }],
      series: [{ type: 'line', xKey: 'quarter', yKey: 'sales' }],
    }".`,
},
generated data
{
  "data": {
    "title": {
      "text": "Quarterly Sales Data"
    },
    "data": [
      {
        "quarter": "Q1",
        "sales": 5000000
      },
      {
        "quarter": "Q2",
        "sales": 5500000
      },
      {
        "quarter": "Q3",
        "sales": 6000000
      },
      {
        "quarter": "Q4",
        "sales": 6500000
      }
    ],
    "series": [
      {
        "type": "line",
        "xKey": "quarter",
        "yKey": "sales"
      }
    ]
  }
}
component.tsx
<AgChartsReact options={data} />

評価

シンプルさ:⭐️⭐️⭐️⭐️
拡張性:⭐️⭐️
LLMとの相性:⭐️⭐️⭐️⭐️⭐️

コメント

AG Chartsを使用して一番メリットに感じた点は、LLMから生成されたデータをそのままコンポーネントに注入できることです。

そのため、LLM側と実装側でデータの調整をすることがなくなります。

その一方、実装側でカスタマイズするには少し不便に感じたことからシンプルにLLMと組み合わせる場合に相性がいいように感じました。

また、AG Chartsの無料枠が充実している利点はありますが、プロダクトとして展開されていることから、よりリッチなコンポーネントを使用する場合に料金が発生してしまいます。

Ant Design Chartsの場合

system prompt
{
  role: "system",
  content: "You are a helpful assistant designed to output JSON. Return Data Format is `data: { label: {label}, sales: {value} }`.",
},
generated data
{
  "data": [
    {
      "label": "Q1",
      "sales": 5000000
    },
    {
      "label": "Q2",
      "sales": 5500000
    },
    {
      "label": "Q3",
      "sales": 6000000
    },
    {
      "label": "Q4",
      "sales": 6500000
    }
  ]
}
component.tsx
const config = {
  xField: 'label',
  yField: 'sales',
  point: {
    shapeField: 'square',
    sizeField: 4,
  },
  interaction: {
    tooltip: {
      marker: false,
    },
  },
};

<Line data={data} {...config} />

評価

シンプルさ:⭐️⭐️⭐️⭐️⭐️
拡張性:⭐️⭐️⭐️
LLMとの相性:⭐️⭐️⭐️⭐️

コメント

Ant Design ChartsもAG Chartsに似て、LLMから生成されるデータを素直に注入することができます。(上記のconfigデータ自体もLLMに生成させることも可能です。)

また、データ構造も非常にシンプルなためLLMと相性が良く実装することができそうです。

一つ懸念があるとすると、ドキュメントの大半が中国語で書かれていることで実装に詰まった場合で苦戦しそうな予感がしました。

nivoの場合

system prompt
{
  role: "system",
  content: `You are a helpful assistant designed to output JSON. Return Data Format is 
    "data: [
      {
        "id": "sales",
        "color": rgb(147 197 253),
        "data": [{ x: quarter, y: sales }]
      }
    ]
  ".`,
},
generated data
{
  "data": [
    {
      "id": "sales",
      "color": "rgb(147, 197, 253)",
      "data": [
        {
          "x": "Q1",
          "y": 5000000
        },
        {
          "x": "Q2",
          "y": 5500000
        },
        {
          "x": "Q3",
          "y": 6000000
        },
        {
          "x": "Q4",
          "y": 6500000
        }
      ]
    }
  ]
}
component.tsx
<ResponsiveLine
  data={data}
  margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
  xScale={{ type: 'point' }}
  yScale={{
    type: 'linear',
    min: 'auto',
    max: 'auto',
    stacked: true,
    reverse: false
  }}
  yFormat=" >-.2f"
  axisTop={null}
  axisRight={null}
  axisLeft={{
    tickSize: 5,
    tickPadding: 5,
    tickRotation: 0,
    legend: 'sales',
    legendOffset: -40,
    legendPosition: 'middle',
    truncateTickAt: 0
  }}
/>

評価

シンプルさ:⭐️⭐️
拡張性:⭐️⭐️⭐️⭐️⭐️
LLMとの相性:⭐️⭐️

コメント

nivoは特に拡張性に富んだライブラリに感じました。その一方で、シンプルな実装をしたい場合は他に比べてコードが肥大化してしまう欠点もあります。

特にコンポーネントのコードを見ていただくとわかるとおり、他のライブラリに比べてもコード量が多いことがわかります。

Unovisの場合

system prompt
{
  role: "system",
  content: `You are a helpful assistant designed to output JSON. Return Data Format is 
    type Data = { quarter: number; sales: number }
    "data: { quarter: {quarter}, sales: {value} }". 
  `,
},
generated data
{
  "data": [
    {
      "quarter": 1,
      "sales": 5000000
    },
    {
      "quarter": 2,
      "sales": 5500000
    },
    {
      "quarter": 3,
      "sales": 6000000
    },
    {
      "quarter": 4,
      "sales": 6500000
    }
  ]
}
component.tsx
<VisXYContainer>
  <VisLine
    data={data}
    x={useCallback(d => d.quarter, [])}
    y={useCallback(d => d.sales, [])}
  />
  <VisAxis type="x" />
  <VisAxis type="y" />
</VisXYContainer>

評価

シンプルさ:⭐️⭐️⭐️
拡張性:⭐️⭐️⭐️⭐️
LLMとの相性:⭐️⭐️⭐️

コメント

Univosはシンプルなデータ構造でグラフを構築できますが、Recharts同様にコンポーネント側がLLMから生成される変数を知っておく必要がある点がデメリットに感じました。

また使用するデータがnumber型である必要があったため、プロンプトで型を指定する必要がありました。

Chart.jsの場合

system prompt
{
  role: "system",
  content: `You are a helpful assistant designed to output JSON. Return Data Format is
    type quarter = string[]
    type sales = number[]
    {
      'data': {
      'labels': {quarter},
      'datasets': [
        {
          'label': 'sales',
          'data': {sales},
          'borderColor': 'rgb(147, 197, 253)', 
          'backgroundColor': 'rgba(147, 197, 253, 0.5)'
        }
      ]
    }
  }`,
},
generated data
{
  "data": {
    "labels": ["Q1", "Q2", "Q3", "Q4"],
    "datasets": [
      {
        "label": "sales",
        "data": [5000000, 5500000, 6000000, 6500000],
        "borderColor": "rgb(147, 197, 253)", 
        "backgroundColor": "rgba(147, 197, 253, 0.5)"
      }
    ]
  }
}
component.tsx
<Line data={data} />

評価

シンプルさ:⭐️⭐️⭐️
拡張性:⭐️⭐️⭐️
LLMとの相性:⭐️⭐️⭐️⭐️

コメント

Chart.jsは配列ベースの構造のため、データ量が少ない場合はシンプルな構造になりますが、データ量が増えるとデータ構造も複雑になっていきます。

しかし、提供されているコンポーネント自体は非常にわかりやすくシンプルなため、複雑なグラフを描画する目的でなければスムーズにLLMと組み合わせ可能に感じました。

生成されるデータを直接注入可能な点も非常に相性が良く感じます。

apexchartsの場合

system prompt
{
  role: "system",
  content: `You are a helpful assistant designed to output JSON. Return Data Format is 
    "data: { 
      options: {
        chart: {
          id: {id}
        },
        xaxis: {
          quarter: {quarter}
        }
      }, 
      series: [
        {
          name: "sales",
          data: {sales}
        }
      ] 
    }". 
  `,
},
generated data
{
  "data": {
    "options": {
      "chart": {
        "id": "sales"
      },
      "xaxis": {
        "quarter": ["Q1", "Q2", "Q3", "Q4"]
      }
    }, 
    "series": [
      {
        "name": "sales",
        "data": [5000000, 5500000, 6000000, 6500000]
      }
    ] 
  }
}
component.tsx
<Chart
  options={data.options}
  series={data.series}
  // 🐱 このtpyeを自由に切り替えることで同一データでも見せ方を可変にできる
  type="line"
/>

評価

シンプルさ:⭐️⭐️⭐️
拡張性:⭐️⭐️⭐️
LLMとの相性:⭐️⭐️⭐️⭐️

コメント

apexchartsは独特なデータ構造を持っていることから、プロンプトで正確に生成結果を制御する必要があります。

しかし、コンポーネントに対して生成されたデータを素直に注入できることと、同一のデータから複数のtypeに切り替え可能なことは非常に強みに感じました。

考察結果

ライブラリ名 シンプルさ 拡張性 LLMとの相性 データ構造のタイプ
Recharts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造
AG Charts ⭐️⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造
Ant Design Charts ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ オブジェクトベースのデータ構造
nivo ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️ オブジェクトベースのデータ構造
Unovis ⭐️⭐️⭐️ ⭐️⭐️ ⭐️⭐️⭐️ オブジェクトベースのデータ構造
Chart.js ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造
apexcharts ⭐️⭐️⭐️ ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️ 配列ベースのデータ構造
  • AG ChartsとAnt Design Charts、Chart.jsは生成されるデータを直接注入可能なことからLLMと相性がいいと判断しました。

  • RechartsとAnt Design Chartsはデータ構造が非常にシンプルなため、トークン数も抑えながら確実性高くデータを生成できると考えます。

  • apexchartsは同一の生成されたデータから、複数typeのグラフを描画できることは非常にメリットに感じました。

まとめ

今回は数あるReactのChartライブラリから個人の視点でLLMと相性がいいと考えられるものを考察しました。

初めに各ライブラリを比較する中でデータ構造が「オブジェクトベースのデータ構造」と「配列ベースのデータ構造」の二種類に分かれることに着目しました。そして、データ量が複雑化した場合のケースを考慮すると、使用するトークン数と確実性の観点から「オブジェクトベースのデータ構造」がLLMと相性がいいと考えました。

その上で各ライブラリを上記二つに分類し、それぞれLLMからグラフを生成してみました。その結果、ライブラリごとに強みやLLMとの相性が異なり、大きく特徴が分かれる結果になりました。

今回はどのライブラリが一番LLMと相性がいいかを断定することはしませんが、これまでに整理したChartライブラリの特性とLLMとの相性、グラフのUIなどから総合的に判断していただけるといいかと思います。

最後に

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)

【面談フォームはこちら】

https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

AI Shift Tech Blog

Discussion