📊

Rechartsを使用したグラフ実装例 棒グラフ応用編

2025/03/03に公開

株式会社 Rehab for JAPAN エンジニアのもじゃ(@moja_moja)です。

今回は Recharts を使用して棒グラフの実装例 応用編を紹介していきたいと思います。

基礎的なグラフの実装例や説明はこちらの記事に記載しております。

なお、紹介するサンプルコードは Recharts 公式のExamples で「Run」を実行するとライブラリをインストールしなくても確認することができます。

Y 軸に Bar を複数表示させる方法

下記の画像のような 月単位で異なる Key を持つ Bar を表示したいケースがあるとします。

そのようなケースは<YAxis />に対して<Bar />コンポーネントを追加することで実現できます。

import React, { PureComponent } from "react";
import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
} from "recharts";

const data = [
  {
    name: "1月",
    contract: 4000,
    cancellation: 2400,
  },
  {
    name: "2月",
    contract: 3000,
    cancellation: 1398,
  },
  {
    name: "3月",
    contract: 2000,
    cancellation: 9800,
  },
  {
    name: "4月",
    contract: 2780,
    cancellation: 3908,
  },
  {
    name: "5月",
    contract: 1890,
    cancellation: 4800,
  },
  {
    name: "6月",
    contract: 2390,
    cancellation: 3800,
  },
];

export default class Example extends PureComponent {
  render() {
    return (
      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 20,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <XAxis dataKey="name" />
          <YAxis />
          <Legend />
          <Bar dataKey="contract" fill="#82ca9d" />
+         <Bar dataKey="cancellation" fill="#8884d8" />
        </BarChart>
      </ResponsiveContainer>
    );
  }
}

これでcontractcancellationの Bar を各月の中で 2 つ表示することができましたが、課題も残っています。

一例だと、X 軸は「~月」と日本語ですが、凡例は dataKey に紐づくものを表示しているので英語表記になります。

また、contractcancellationの Bar の幅が同じですが、cancellationの Bar の幅をもう少し小さくしたいケースや、間隔を広げる・狭めたいケースなどもあると思います。

そこで次は凡例やBar の幅・間隔の変更方法について紹介したいと思います。

凡例の変更方法

<Legend />コンポーネントのpayloadを使用する

<Legend />には payload というプロパティがあります。

この中には名称を変更できるvalueや表示するアイコンを変更できるtypeなどがあります。

<Legend
  payload={[
    { value: "契約", type: "wye", color: "#8884d8" },
    { value: "解約", type: "star", color: "#82ca9d" },
  ]}
/>

type については以下のものが使用できるようですが、 plainline は実行してもエラーになるみたいです。

LegendType =
  "plainline" |
  "line" |
  "square" |
  "rect" |
  "circle" |
  "cross" |
  "diamond" |
  "star" |
  "triangle" |
  "wye" |
  "none";

Bar の幅・間隔を調整する方法

Bar の幅を変更するには<BarChart /><Bar />コンポーネントに対してbarSizeを使用することで調整ができます。

Bar の幅を統一したい時は<BarChart />に対してbarSizeを使用し、個別で設定したい場合は<Bar />に対してbarSizeを使用します。

  <BarChart
    width={500}
    height={300}
    data={data}
    margin={{
      top: 20,
      right: 30,
      left: 20,
      bottom: 5,
    }}
+   barSize={15} // Barの幅を統一させたい場合はこちらに記述
  >
    <XAxis dataKey="name" />
    <YAxis />
    <Legend
      payload={[
        { value: "契約", type: "rect", color: "#8884d8" },
        { value: "解約", type: "star", color: "#82ca9d" },
      ]}
    />
  {/* 個別に設定したい場合はBarに対してbarSizeを記述 */}
    <Bar
      dataKey="cancellation"
      fill="#8884d8"
+    barSize={20}
    />
    <Bar
      dataKey="contract"
      fill="#82ca9d"
+    barSize={10}
    />
  </BarChart>

Bar 同士の間隔の調整方法

下記の画像の赤矢印の間隔を調整したい場合はbarGap、青矢印の間隔を調整したい場合はbarCategoryGapを使用します。

注意点として、<BarChart /><Bar />に対してbarSizeが設定されていた場合、barCategoryGapを設定しても反映されないようです。

<BarChart
  width={500}
  height={300}
  data={data}
  margin={{
    top: 20,
    right: 30,
    left: 20,
    bottom: 5,
  }}
  barCategoryGap={20} // barSizeを記述していた場合、反映されない
  barSize={15}
>
  ~
  <Bar dataKey="cancellation" fill="#8884d8" />
  <Bar dataKey="contract" fill="#82ca9d" />
</BarChart>

barCategoryGapは設定した場合、設定した値に合わせて<Bar/>コンポーネントのbarSizeが自動で調整されるようになっています。

しかし、<BarChart/><Bar/>に対してbarSizeを記述するとそちらが優先的に反映されるため、barCategoryGapを記述していても反映されないようです。

また、barGapに関しても width の幅よりも barGap で表示する幅が大きくなってしまった場合、ライブラリ側で自動で調整され、間隔が 0 になってしまうようです。

発生する条件としては、widthが固定値で 1 ~ 12 月のような長い期間のデータを所持している + 各月の中で複数のグラフを表示させる必要がある場合、

barGapbarCategoryGapを設定しても記述した間隔にならないことがあったので反映がうまくいかない時はwidthbarGapbarCategoryGapを確認してみるとよいかもしれないです。

積み上げ棒グラフの実装例

次は積み上げ Bar の実装例について紹介していきたいと思います。

import React, { PureComponent } from "react";
import {
  BarChart,
  Bar,
  LabelList,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
} from "recharts";

const data = [
  {
    name: "新宿店",
    hamburger: 10000,
    potato: 10000,
    drink: 20000,
    sideMenu: 30000,
  },
  {
    name: "池袋店",
    hamburger: 15000,
    potato: 13000,
    drink: 14000,
    sideMenu: 18000,
  },
  {
    name: "渋谷店",
    hamburger: 25000,
    potato: 30000,
    drink: 20000,
    sideMenu: 40000,
  },
  {
    name: "原宿店",
    hamburger: 12000,
    potato: 28000,
    drink: 18000,
    sideMenu: 29000,
  },
];

export default class Example extends PureComponent {
  render() {
    return (
      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis domain={[0, "dataMax + 5000"]} />
          <Tooltip />
          <Bar
            stackId="stack"
            dataKey="hamburger"
            fill="#8884d8"
            isAnimationActive={false}
          >
            <LabelList dataKey="hamburger" fill="#000000" />
          </Bar>
          <Bar
            stackId="stack"
            dataKey="potato"
            fill="#82ca9d"
            isAnimationActive={false}
          >
            <LabelList dataKey="potato" fill="#000000" />
          </Bar>
          <Bar
            stackId="stack"
            dataKey="sideMenu"
            fill="#ffc658"
            isAnimationActive={false}
          >
            <LabelList dataKey="sideMenu" fill="#000000" />
          </Bar>
          <Bar
            stackId="stack"
            dataKey="drink"
            fill="#83a6ed"
            isAnimationActive={false}
          >
            <LabelList dataKey="drink" fill="#000000" />
          </Bar>
        </BarChart>
      </ResponsiveContainer>
    );
  }
}

積み上げ棒グラフにする場合、積み上げたい<Bar /> コンポーネントに対して、stackId="~”と定義します。

このstackId が一致している <Bar /> 同士が積み上がっていきます。

注意点として、<LabelList />を追加する場合は<LabelList />に対してdataKey="~"を追加で記述する必要があります。

もし、追加しなかった場合 Label自体 は表示されますが、積み上がっていく度に加算された値が表示されるようになります。

LabelList に dataKey を記述したパターン

LabelList に dataKey を記述しないパターン

dataKey を記述しない場合、potato の Bar で表示される値は「hamburger の数字 + potato の数字」となり、sideMenu の場合は「hamburger の数字 + potato の数字 + sideMenu の数字」が表示されるようになります。

正負の値がある積み上げ棒グラフの実装例

正負の値が混在している積み上げ Bar も実装することができます。

ポイントは 2 つあります。

  1. 値をマイナスにする
  2. <BarChart />コンポーネントにstackOffset="sign"を追加する

値をマイナスにしてstackOffsetを記述しない場合、正・負で Bar がうまく積み上がらない見た目になるので注意が必要です。

const data = [
  {
    name: "新宿店",
    hamburger: 10000,
    potato: 10000,
    drink: 20000,
+   sideMenu: -30000,
  },
  {
    name: "池袋店",
+   hamburger: -15000,
    potato: 13000,
    drink: 14000,
    sideMenu: 18000,
  },
  {
    name: "渋谷店",
    hamburger: 25000,
    potato: 30000,
+   drink: -20000,
    sideMenu: 40000,
  },
  {
    name: "原宿店",
    hamburger: 12000,
+   potato: -28000,
    drink: 18000,
    sideMenu: 29000,
  },
];


    <ResponsiveContainer width="100%" height="100%">
      <BarChart
        width={500}
        height={300}
        data={data}
        margin={{
          top: 5,
          right: 30,
          left: 20,
          bottom: 5,
        }}
+         stackOffset="sign"
      >
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="name" />
        <YAxis domain={["auto", "dataMax"]} />
        <Tooltip />
        <Bar
          stackId="stack"
          dataKey="hamburger"
          fill="#8884d8"
          isAnimationActive={false}
        >
          <LabelList dataKey="hamburger" fill="#000000" />
        </Bar>
        <Bar
          stackId="stack"
          dataKey="potato"
          fill="#82ca9d"
          isAnimationActive={false}
        >
          <LabelList dataKey="potato" fill="#000000" />
        </Bar>
        <Bar
          stackId="stack"
          dataKey="drink"
          fill="#ffc658"
          isAnimationActive={false}
        >
          <LabelList dataKey="drink" fill="#000000" />
        </Bar>
        <Bar
          stackId="stack"
          dataKey="sideMenu"
          fill="#83a6ed"
          isAnimationActive={false}
        >
          <LabelList dataKey="sideMenu" fill="#000000" />
        </Bar>
      </BarChart>
    </ResponsiveContainer>

詰まったポイント

プロダクトの仕様で下記のような、要件がありました。

  • 積み上げ Bar でYAixsの domain の最大値を超える値がデータとしてある場合はオーバーフローする
  • domain は固定値が入る
  • 各 Bar の中に数字を中央寄せで表示させたい

オーバーフローと Bar の中に数字を表示する方法はYAixsコンポーネントに allowDataOverflowを記述と<Bar/>コンポーネントに対して<LabelList position="center"/>を記述することで解決します。

const data = [
  {
    name: "新宿店",
    hamburger: 10000,
    potato: 10000,
    drink: 20000,
    sideMenu: 30000,
  },
  {
    name: "池袋店",
    hamburger: 15000,
    potato: 13000,
    drink: 14000,
    sideMenu: 18000,
  },
  {
    name: "渋谷店",
    hamburger: 25000,
    potato: 80000,
    drink: 70000,
    sideMenu: 50000,
  },
  {
    name: "原宿店",
    hamburger: 12000,
    potato: 28000,
    drink: 18000,
    sideMenu: 29000,
  },
];

  <ResponsiveContainer width="100%" height="100%">
    <BarChart
      width={500}
      height={300}
      data={data}
      margin={{
        top: 5,
        right: 30,
        left: 20,
        bottom: 5,
      }}
      stackOffset="sign"
    >
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis dataKey="name" />
      <YAxis
-        domain={[0, "dataMax"]}
+        domain={[0, 100000]}
+        allowDataOverflow
        />
      <Tooltip />
      <Legend
        payload={[
          { value: "ハンバーガー", type: "rect", color: "#8884d8" },
          { value: "ポテト", type: "rect", color: "#82ca9d" },
          { value: "ドリンク", type: "rect", color: "#ffc658" },
          { value: "サイドメニュー", type: "rect", color: "#83a6ed" },
        ]}
      />

      <Bar stackId="stack" dataKey="hamburger" fill="#8884d8">
+        <LabelList position="center" dataKey="hamburger" fill="#000000" />
      </Bar>
      <Bar stackId="stack" dataKey="potato" fill="#82ca9d">
+        <LabelList position="center" dataKey="potato" fill="#000000" />
      </Bar>
      <Bar stackId="stack" dataKey="drink" fill="#ffc658">
+        <LabelList position="center" dataKey="drink" fill="#000000" />
      </Bar>
      <Bar stackId="stack" dataKey="sideMenu" fill="#83a6ed">
+        <LabelList position="center" dataKey="sideMenu" fill="#000000" />
      </Bar>
    </BarChart>
  </ResponsiveContainer>

ただ、この実装では任意の Bar がオーバーフローしたときに、<LabelList />の文字が画像のように上部に残ってしまうことがわかりました。

解決策

LabelList の content でカスタム Label を作成して対応

<LabelList />には content というプロパティがあり、関数を定義することができます。

const CustomStackBarLabel = (props) => {
  const { x, y, width, height, value, viewBox } = props;

  const areaHeight = viewBox?.height || height;

  if (areaHeight === 0) {
    return null;
  }
  return (
    <text
      x={x + width / 2}
      y={y + height / 2}
      textAnchor="middle"
      dominantBaseline="central"
      fontSize={14}
    >
      {value}
    </text>
  );
};

このコードではまずviewBox?.heightheightの値を取得するareaHeightを定義します。

オーバーフローした Bar の場合、areaHeightには 0 が入ってくるため、0 の場合は null を返し、それ以外は<text />を表示させるように条件を定義してあげることでオーバーフローした Bar の文字が上部に残る問題が解消しました。

詰まったポイント 2

次に 積み上げ Bar の上部に数字を表示したいとの要望がありました。

Bar の上部に数字を出す場合は<LabelList />position="top"と記述することで実現はできます。

しかし、今回表示する Bar が積み上げ Bar だったので、最上部の Bar は数字が表示されるが、他の Bar の数字は表示されないことが実装してわかりました。

正確には下記の画像の赤枠の辺りにハンバーガー・ポテト・ドリンクの数字は表示されています。

これはBarコンポーネント は底部から描画されるため、ハンバーガーのBarLabelList が描画された後にポテトのBarが描画するため、ハンバーガーのLabelListが見えなくなることがわかりました。

LabelList が Bar に隠れてしまうサンプルコード

import React, { PureComponent } from "react";
import {
  BarChart,
  Bar,
  LabelList,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
  ResponsiveContainer,
} from "recharts";

const data = [
  {
    name: "新宿店",
    hamburger: 10000,
    potato: 10000,
    drink: 20000,
    sideMenu: 30000,
  },
  {
    name: "池袋店",
    hamburger: 15000,
    potato: 13000,
    drink: 14000,
    sideMenu: 18000,
  },
  {
    name: "渋谷店",
    hamburger: 25000,
    potato: 30000,
    drink: 20000,
    sideMenu: 40000,
  },
  {
    name: "原宿店",
    hamburger: 12000,
    potato: 28000,
    drink: 18000,
    sideMenu: 29000,
  },
];

export default class Example extends PureComponent {
  render() {
    return (
      <ResponsiveContainer width="100%" height="100%">
        <BarChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
          stack
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis domain={[0, "dataMax + 5000"]} />
          <Tooltip />
          <Legend
            payload={[
              { value: "ハンバーガー", type: "rect", color: "#8884d8" },
              { value: "ポテト", type: "rect", color: "#82ca9d" },
              { value: "ドリンク", type: "rect", color: "#ffc658" },
              { value: "サイドメニュー", type: "rect", color: "#83a6ed" },
            ]}
          />
          <Bar stackId="stack" dataKey="hamburger" fill="#8884d8">
            <LabelList position="top" dataKey="hamburger" fill="#000000" />
          </Bar>
          <Bar stackId="stack" dataKey="potato" fill="#82ca9d">
            <LabelList position="top" dataKey="potato" fill="#000000" />
          </Bar>
          <Bar stackId="stack" dataKey="sideMenu" fill="#ffc658">
            <LabelList position="top" dataKey="sideMenu" fill="#000000" />
          </Bar>
          <Bar stackId="stack" dataKey="drink" fill="#83a6ed">
            <LabelList position="top" dataKey="drink" fill="#000000" />
          </Bar>
        </BarChart>
      </ResponsiveContainer>
    );
  }
}

試したこと

<LabelList/> の content でカスタム Label を作成して対応する

LabelListには content でカスタム Label を作成して、解決できるか試してみました。

<text>xyに対して、左右のどちらかにずらすことはできましたが、中央に表示することができない点と、CSS の z-index で重なり順を変更できないかと検討しましたが、SVG の  <text>  要素では調べる限り設定ができなかったのでこの方法は断念しました。

<LabelList/> の position をinsideTopに変更する

position をtopにした場合、各 Bar の外側に表示されますが、insideTopにすると、各 Bar の内側の上部に表示されるようになります。

しかし、期待している UI は数字が Bar の上部に表示だったのでこの方法も断念しました。

解決策

BarChartreverseStackOrderを追加する

Recharts で積み重ねられた要素を描画するときは通常、左から右にレンダリングされます。

しかし、reverseStackOrder={true} にすると右から左にレンダリングされるようになります。

このレンダリングは SVG レイヤーに影響し、Bar の中で定義している LabelList は<text> 要素の SVG のため影響対象になります。

このreverseStackOrder を記述することでレンダリングの順序が逆になるのであれば、この問題を解決できるのではないかと思い、下記のような形にコードを修正しました。

return (
  <ResponsiveContainer width="100%" height="100%">
  <BarChart
    width={500}
    height={300}
    data={data}
    margin={{
      top: 5,
      right: 30,
      left: 20,
      bottom: 5,
    }}
+   reverseStackOrder
  >
    <CartesianGrid strokeDasharray="3 3" />
    <XAxis dataKey="name" />
    <YAxis domain={[0, "dataMax + 5000"]} />
    <Tooltip />
    <Legend
      payload={[
        { value: "ハンバーガー", type: "rect", color: "#8884d8" },
        { value: "ポテト", type: "rect", color: "#82ca9d" },
        { value: "ドリンク", type: "rect", color: "#ffc658" },
        { value: "サイドメニュー", type: "rect", color: "#83a6ed" },
      ]}
    />
+   <Bar stackId="stack" dataKey="sideMenu" fill="#83a6ed">
+     <LabelList position="top" dataKey="sideMenu" fill="#000000"/>
+   </Bar>
+   <Bar stackId="stack" dataKey="drink" fill="#ffc658">
+     <LabelList position="top" dataKey="drink" fill="#000000"/>
+   </Bar>
+   <Bar stackId="stack" dataKey="potato" fill="#82ca9d">
+     <LabelList position="top"dataKey="potato" fill="#000000"/>
+   </Bar>
+   <Bar stackId="stack" dataKey="hamburger" fill="#8884d8">
+     <LabelList position="top" dataKey="hamburger" fill="#000000"/>
+   </Bar>
  </BarChart>
  </ResponsiveContainer>
);

<BarChart/>コンポーネントにreverseStackOrderを記述すると Bar の描画順が逆転するため、各 Bar を記述順を変更することで各 Bar の上部に数字を表示することができました。

終わりに

3 記事に渡って Recharts を使用した実装例や小技・詰まった点・解決策については紹介してきましたが、今回の記事が最後となります。

Recharts は様々なグラフを描画できる反面、詰まったポイントで紹介したものは導入や検討段階で気づくことは難しく

実装している中で予想外の問題にぶつかるケースが多く解決まで時間がかかったものもあります。

今後、Recharts の導入を検討している人や導入して同じような問題で悩んでいる人の参考になれたら幸いです。

Rehab Tech Blog

Discussion