Rechartsでグラフをごりごりカスタマイズするぞ!(RadarChart編)
グラフライブラリのRechartsでグラフをカスタマイズする機会があったので、その知見についてまとめます。
主にRadarChartについて書いていきます。
単純に使うだけならexampleをそのまま使うだけで使えちゃいます。
<div
className={css({
w: "700px",
h: "640px",
})}
>
<ResponsiveContainer width="100%" height="100%">
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={data}>
<PolarGrid />
<PolarAngleAxis dataKey="subject" />
<PolarRadiusAxis angle={30} domain={[0, 150]} />
<Radar
name="Mike"
dataKey="A"
stroke="#8884d8"
fill="#8884d8"
fillOpacity={0.6}
/>
<Radar
name="Lily"
dataKey="B"
stroke="#82ca9d"
fill="#82ca9d"
fillOpacity={0.6}
/>
<Legend />
</RadarChart>
</ResponsiveContainer>
</div>
ただデフォルトのグラフでは物足りないので、カスタマイズすることになりました。
変えたい部分は
- データの差分にのみ色をつけて、差を分かりやすくしたい
- 項目のLabelが地味なので、ちょっとリッチにしたい
データの差分にのみ色をつけたい
差分に色をつけるってどういうことかというと、こういうことです↓
ということで差に色をつける設定を探しましたが、ありませんでした(多分)。。。
なので、自分で作ることになります。
いくつか考えついた方法を書いていきます。
方法1
今回はLily - Mikeの差分を取りたいので、MikeのRadar.fillをwhiteにする。
(引く方をwhiteに)
確かに差分にのみ色がついたが。。。。
色がwhiteなので、背景の目盛りまで消えてしまった!
ということでこの方法は使えませんでした。
方法2
svg elementのpolygonを使って、頑張って差分を表現する。
ただし複雑な座標計算を要するので、大変です。。。
描画する自作polygonはRadarのshapeに渡すことで描画してくれます。またpropsにはpointsなどの座標データが最低限返ってきます。
具体的には
のように項目と項目の間ごとに分割する。
分割された区間内のpolygonの形は3種類に分けることができる。
- 描画なし
- これは両方の項目の差分が負な時
- 四角形
- これは両方の項目の差分が正な時
- これは比較的実装が楽(比較的)
- 4点の座標をprops.pointsから頑張って算出しましょう
- 三角形
- これは片方の項目のみ差分が負な時
- これは実装が大変です
- 2線の交差点の座標を計算しないといけないので、四角形の時より実装が複雑になります
- 根気よく計算しましょう
ということで、計算が大変なのでこの方法は採用しませんでした。次が本命です。
方法3
cssのmaskを使います。コードは↓
const LilyRadar = ({
points,
// biome-ignore lint/suspicious/noExplicitAny: allow any type
}: { points: any }) => {
let pointsStr = "";
for (let i = 0; i < points.length; i++) {
pointsStr += `${points[i].x},${points[i].y} `;
}
return (
<>
<polygon
points={pointsStr}
fill="rgba(246, 231, 239, 0.75)"
mask="url(#mikeMask)"
/>
<polygon points={pointsStr} fill="transparent" stroke="#A11261" />
</>
);
};
const MikeRadar = ({
points,
// biome-ignore lint/suspicious/noExplicitAny: allow any type
}: { points: any }) => {
let pointsStr = "";
for (let i = 0; i < points.length; i++) {
pointsStr += `${points[i].x},${points[i].y} `;
}
return (
<>
<defs>
<mask id="mikeMask">
<rect x="0" y="0" width="1000" height="1000" fill="white" />
<polygon points={pointsStr} fill="black" />
</mask>
</defs>
<Polygon
points={points}
stroke="#F2B180"
fill="transparent"
className={css({
pointerEvents: "none",
})}
/>
</>
);
};
<ResponsiveContainer width="100%" height="100%">
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={data}>
<PolarGrid />
<PolarAngleAxis dataKey="subject" />
<PolarRadiusAxis angle={30} domain={[0, 150]} />
<Radar
name="Lily"
dataKey="B"
shape={({ points }) => <LilyRadar points={points} />}
/>
<Radar
name="Mike"
dataKey="A"
shape={({ points }) => <MikeRadar points={points} />}
/>
<Legend />
</RadarChart>
</ResponsiveContainer>
仕組みはMikeのRadarをカスタム描画するタイミングでpointsを元にmaskを作ります。
maskではwhiteなところを残しつつ、その中のblackなところを切り取ります(説明が下手すぎる)。↓
<defs>
<mask id="mikeMask">
<rect x="0" y="0" width="1000" height="1000" fill="white" />
<polygon points={pointsStr} fill="black" />
</mask>
</defs>
そしてLilyのRadarをカスタム描画するタイミングで、maskにidを指定します。
mask="url(#mikeMask)"
ということで差分に色をつけることができました
項目のLabelをリッチにしたい
次に項目のLabelをリッチにしようと思います。
項目のLabelとは「History」や「English」とある部分です。
色を変えるだけなら簡単に変えれますが、自分でカスタマイズしようとするとPolarAngleAxisのtickにコンポーネントを渡すことで実現できます。
ただ渡すコンポーネントはRadar同様にsvg elementなので、divとかは渡すことができません(これが何気に大変。。。)。
tick={(label) => {
return (
<CustomLabel
label={label}
data={data}
/>
);
}}
このlabel内で主に使うデータは
- 基準座標のx,y
- textAnchor
- start/middle/end
- payload
- payload.valueにはLabelのテキストが格納されている
CustomLabelの中で<rect/>などを配置してそのx,yにlabel.xなどをそのまま渡そうとすると、グラフにCustomLabelが重なってしまうので、色々と座標計算する必要があります。
- textAnchorを元にx軸方向を調整する
- textAnchorを組み込むことで、Labelを中央配置することができます
- ResizeObserverなどを使って可変要素のwidthをgetすることで、CustomLabel全体のwidthを保持しておく
- そのwidth(コード内ではgroupWidth)とtextAnchorを元に↓のような調整を行う
-
let transformX = 0; switch (label.textAnchor) { case "start": transformX = 0; break; case "middle": transformX = groupWidth / 2; break; case "end": transformX = groupWidth; break; default: break; } const x = label.x - transformX;
- Labelの大きさが大きくなりすぎると、textAnchorを組み込んでもグラフに重なってしまうので、中心から同じ距離離してあげる必要があります
-
//中心からどれくらいの割合で離れさせるか const gap = 10; const rad = (label.payload.coordinate * Math.PI) / 180; const x = label.x + gap * Math.cos(rad); const y = label.y - gap * Math.sin(rad);
- gapは離す重みを表しています。この数値を調整することでどれくらい離すかを決めることができます。
-
愚痴
- shapeとかから提供されるpropsの型がanyなのが使いにくかったです。
- やはり型は固ければ良い
- props.payloadの中にもっとデータを入れて欲しいなぁと思った
- 自分でデータ整形して、渡さないと自由にCustomLabelを作りにくい