rechartsのCustomActiveShapePieChartをTypeScriptで攻略する
記事作成時の recharts のバージョン: 2.12.7
はじめに
前回の記事recharts の円グラフ入門(TypeScript)では、recharts の円グラフの基本的な使い方を紹介しました。
今回のテーマとなるCustomActiveShapePieChart
は、rechats の公式サイトの samples の中でも発展的な内容になっており、また、TypeScript への変換も少しコツが必要です。
ということで、早速本題です。
CustomActiveShapePieChart
ドーナツ型の円グラフです。とてもおしゃれですね。
ドーナツの穴の部分にデータのラベルが、ドーナツの外側の部分にデータの値が表示されるようになっています。
下の公式サイトに飛んでみると分かりますが、円グラフをホバーすると表示されるラベルも変化するとてもインタラクティブなグラフです。
公式のサンプルコード:https://recharts.org/en-US/examples/CustomActiveShapePieChart
TypeScript に変換したコードを先に載せておきます。
import { useCallback, useState } from "react";
import { PieChart, Pie, Sector, ResponsiveContainer } from "recharts";
const data = [
{ name: "Group A", value: 400 },
{ name: "Group B", value: 300 },
{ name: "Group C", value: 300 },
{ name: "Group D", value: 200 },
];
type Data = (typeof data)[number];
type ActiveShapeProps = {
cx: number;
cy: number;
midAngle: number;
innerRadius: number;
outerRadius: number;
startAngle: number;
endAngle: number;
fill: string;
payload: Data;
percent: number;
value: number;
};
// ユーザー定期型ガードを追加
const isPropertyAccessible = (
value: unknown
): value is { [key: string]: unknown } => {
return value != null;
};
const isActiveShapeProps = (props: unknown): props is ActiveShapeProps => {
if (!isPropertyAccessible(props)) return false;
if (!isPropertyAccessible(props.payload)) return false;
return (
typeof props.cx === "number" &&
typeof props.cy === "number" &&
typeof props.midAngle === "number" &&
typeof props.innerRadius === "number" &&
typeof props.outerRadius === "number" &&
typeof props.startAngle === "number" &&
typeof props.endAngle === "number" &&
typeof props.fill === "string" &&
typeof props.payload.name === "string" &&
typeof props.payload.value === "number" &&
typeof props.percent === "number" &&
typeof props.value === "number"
);
};
const renderActiveShape = (props: unknown) => {
if (!isActiveShapeProps(props)) return <></>;
const RADIAN = Math.PI / 180;
const {
cx,
cy,
midAngle,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
payload,
percent,
value,
} = props;
const sin = Math.sin(-RADIAN * midAngle);
const cos = Math.cos(-RADIAN * midAngle);
const sx = cx + (outerRadius + 10) * cos;
const sy = cy + (outerRadius + 10) * sin;
const mx = cx + (outerRadius + 30) * cos;
const my = cy + (outerRadius + 30) * sin;
const ex = mx + (cos >= 0 ? 1 : -1) * 22;
const ey = my;
const textAnchor = cos >= 0 ? "start" : "end";
return (
<g>
<text
x={cx}
y={cy}
dy={8}
textAnchor="middle"
fill={fill}
fontSize="1.5em"
>
{payload.name}
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={outerRadius + 6}
outerRadius={outerRadius + 10}
fill={fill}
/>
<path
d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
stroke={fill}
fill="none"
/>
<circle cx={ex} cy={ey} r={2} fill={fill} stroke="none" />
<text
x={ex + (cos >= 0 ? 1 : -1) * 12}
y={ey}
textAnchor={textAnchor}
fill="#333"
fontSize="1.5em"
>{`PV ${value}`}</text>
<text
x={ex + (cos >= 0 ? 1 : -1) * 12}
y={ey}
dy={18}
textAnchor={textAnchor}
fill="#999"
fontSize="1.0em"
>
{`(Rate ${(percent * 100).toFixed(2)}%)`}
</text>
</g>
);
};
export const CustomActiveShapePieChart = () => {
const [activeIndex, setActiveIndex] = useState(0);
const onPieEnter = useCallback(
(_: string, index: number) => {
setActiveIndex(index);
},
[setActiveIndex]
);
return (
<ResponsiveContainer width="80%" height={400}>
<PieChart>
<Pie
activeIndex={activeIndex}
activeShape={renderActiveShape}
data={data}
innerRadius="60%"
fill="#8884d8"
dataKey="value"
onMouseEnter={onPieEnter}
/>
</PieChart>
</ResponsiveContainer>
);
};
基本的な事項の解説
<PieChart><Pie /></PieChart>
のように、
PieChart
コンポーネントの children としてPie
コンポーネントを渡すことで円グラフを描画しています。
円グラフにマウスをホバーさせたときのラベルのアニメーションは、mouse enter イベントをトリガーに実行されるコールバック関数をonMouseEnter
プロパティに設定することで設定しています。(React らしくuseState
とuseCallback
を使って実装されてますね)
この円グラフのコア部分になるラベルの表示部分については、label
プロパティに DOM 要素を返す関数renderActiveShape
を設定することで実装されています。
この関数が返している SVG 要素の細かい内容については(ただちゃんと読んでないだけですが)今回は割愛したいと思います。
公式からカスタマイズした点
recharts のサンプルコードはすべて JavaScript で書かれていますが、基本的にはちょっとした修正で TypeScript に変換できます。
ただし、今回の円グラフは実は簡単にはいかない代物になっています。
詰まりポイントとしては、renderActiveShape
関数の引数props
の型定義が難しい点にあります。
github のソースコードを見てみると、Pie
コンポーネントのactivShape
プロパティに渡せる値の型はActiveShape
型であり、
ActiveShape
型は具体的には以下であることがわかります。
export type ActiveShape<
PropsType = Record<string, any>,
ElementType = SVGElement
> =
| ReactElement<SVGProps<ElementType>>
| ((props: PropsType) => ReactElement<SVGProps<ElementType>>)
| ((props: unknown) => JSX.Element)
| SVGProps<ElementType>
| boolean;
また、僕の使っている VScode にrenderActiveShape
関数の返り値の型は何か聞いてみると、下記のようにJSX.Element
型であることが分かります。
ここで、いま得た2つの情報を見比べてみると、
ActiveShape
型の中で関数かつ返り値がJSX.Element
型であるのは(props: unknown) => JSX.Element
型のみであることがわかります。
ということで、renderActiveShape
関数の引数props
の型はunknown
型とするのが良さそうです。
(この辺りは深く理解できていない部分もあるので、もし間違っている点あったらぜひ教えてください)
ということで、ここからはrenderActiveShape
関数の引数の型をunknown
型にして、型の絞り込みを行って TypeScript のコンパイルエラーを解消していく部分の解説をします。
型の絞り込み
型の絞り込みによってunknown
型を以下のActiveShapeProps
型に絞り込みます。
const data = [
{ name: "Group A", value: 400 },
{ name: "Group B", value: 300 },
{ name: "Group C", value: 300 },
{ name: "Group D", value: 200 },
];
type Data = (typeof data)[number];
type ActiveShapeProps = {
cx: number;
cy: number;
midAngle: number;
innerRadius: number;
outerRadius: number;
startAngle: number;
endAngle: number;
fill: string;
payload: Data;
percent: number;
value: number;
};
型の絞り込みでは、まずunknown
型からプロパティアクセスできる型への絞り込みを行います。
具体的には、以下のようなユーザー定義型ガードの関数isPropertyAccessible
を作ります。
const isPropertyAccessible = (
value: unknown
): value is { [key: string]: unknown } => {
return value != null;
};
「プロパティアクセスできる型」は{ [key: string]: unknown }
と表せることがポイントです。
(この実装は 「プロを目指す人のための TypeScript 入門」のコラム 30 の内容を参考にしました。詳細が知りたい型はそちらを参照ください。)
続いて、プロパティアクセスできる型からActiveShapeProps
型に絞り込むためのユーザー定義型ガードを愚直に作ります。
const isActiveShapeProps = (props: unknown): props is ActiveShapeProps => {
if (!isPropertyAccessible(props)) return false;
if (!isPropertyAccessible(props.payload)) return false;
return (
typeof props.cx === "number" &&
typeof props.cy === "number" &&
typeof props.midAngle === "number" &&
typeof props.innerRadius === "number" &&
typeof props.outerRadius === "number" &&
typeof props.startAngle === "number" &&
typeof props.endAngle === "number" &&
typeof props.fill === "string" &&
typeof props.payload.name === "string" &&
typeof props.payload.value === "number" &&
typeof props.percent === "number" &&
typeof props.value === "number"
);
};
上記のユーザー定義型ガードisActiveShapeProps
をunknow
型の絞り込みに使えば、無事コンパイルエラーを解消することができます!めでたしめでたし!
まとめ
recharts の発展的な円グラフCustomActiveShapePieChart
の概要の説明と、TypeScript 化のポイントについて説明しました。
正直any
やas
を使えばここまでせずとも突破できるとは思いますが、TypeScript に負けた??気がするのであまりやりたくないですよね。
この情報が誰かの役に立つことがあれば嬉しいです。
Discussion