Rechartsを使用したグラフ実装例 棒グラフ応用編
株式会社 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>
);
}
}
これでcontract
・cancellation
の Bar を各月の中で 2 つ表示することができましたが、課題も残っています。
一例だと、X 軸は「~月」と日本語ですが、凡例は dataKey に紐づくものを表示しているので英語表記になります。
また、contract
とcancellation
の 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 月のような長い期間のデータを所持している + 各月の中で複数のグラフを表示させる必要がある場合、
barGap
やbarCategoryGap
を設定しても記述した間隔にならないことがあったので反映がうまくいかない時はwidth
やbarGap
やbarCategoryGap
を確認してみるとよいかもしれないです。
積み上げ棒グラフの実装例
次は積み上げ 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 つあります。
- 値をマイナスにする
-
<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?.height
かheight
の値を取得するareaHeight
を定義します。
オーバーフローした Bar の場合、areaHeight
には 0 が入ってくるため、0 の場合は null を返し、それ以外は<text />
を表示させるように条件を定義してあげることでオーバーフローした Bar の文字が上部に残る問題が解消しました。
詰まったポイント 2
次に 積み上げ Bar の上部に数字を表示したいとの要望がありました。
Bar の上部に数字を出す場合は<LabelList />
にposition="top"
と記述することで実現はできます。
しかし、今回表示する Bar が積み上げ Bar だったので、最上部の Bar は数字が表示されるが、他の Bar の数字は表示されないことが実装してわかりました。
正確には下記の画像の赤枠の辺りにハンバーガー・ポテト・ドリンクの数字は表示されています。
これはBar
コンポーネント は底部から描画されるため、ハンバーガーのBar
とLabelList
が描画された後にポテトの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>
の x
やy
に対して、左右のどちらかにずらすことはできましたが、中央に表示することができない点と、CSS の z-index で重なり順を変更できないかと検討しましたが、SVG の <text>
要素では調べる限り設定ができなかったのでこの方法は断念しました。
<LabelList/>
の position をinsideTop
に変更する
position をtop
にした場合、各 Bar の外側に表示されますが、insideTop
にすると、各 Bar の内側の上部に表示されるようになります。
しかし、期待している UI は数字が Bar の上部に表示だったのでこの方法も断念しました。
解決策
BarChart
にreverseStackOrder
を追加する
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 の導入を検討している人や導入して同じような問題で悩んでいる人の参考になれたら幸いです。
Discussion