【React】レーダーチャート(radar chart)をライブラリ無しで実装する
やりたいこと
このようなレーダー(スパイダー)チャートをReactでSVGとして実装したい。
ReactのChartライブラリだとRechartsなどが候補として挙がるが、今回は外部のライブラリ無しでコンポーネントとして実装してみる。
実装方法
1. コンポーネント実装
import styles from "../styles/radarChart.module.scss";
// 対象のチャート形状(頂点の数)const ALLOW_VERTEX_COUNTS = [3, 5, 6, 7];const MIN_SCORE_VALUE = 0;const MAX_SCORE_VALUE = 10;const BASE_SIZE = 200;
type Score = { value: number; title: string;};
export default function RadarChart({ scores, baseColor, ariaLabel,}: { scores: Score[]; baseColor: string; ariaLabel: string;}) { let scoreValues: number[] = []; try { if (!ALLOW_VERTEX_COUNTS.includes(scores.length)) throw new Error(); scoreValues = scores.map(({ value }) => { if (value < MIN_SCORE_VALUE || value > MAX_SCORE_VALUE) throw new Error(); return value; }); } catch (e) { return <></>; }
return ( <div className={styles.block}> <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${BASE_SIZE} ${BASE_SIZE}`} aria-label={ariaLabel} > <title>{ariaLabel}</title> {createPaths(ariaLabel, baseColor, scoreValues)} </svg> <dl className={styles.scores}> {scores.map((score) => ( <div className={styles.score}> <dt>{score.title}</dt> <dd className={styles.value}>{score.value.toFixed(1)}</dd> </div> ))} </dl> </div> );}
/** * チャートのパスを生成 */function createPaths( ariaLabel: string, baseColor: string, scoreValues: number[]) { const vertexCount = scoreValues.length;
const sizes: number[] = []; for (let i = vertexCount; i > 0; i--) { sizes.push((BASE_SIZE * i) / vertexCount); }
const allPentagonOffsets: Offset[][] = []; for (const size of sizes) { const pentagonOffsets: Offset[] = []; for (let i = 0; i < vertexCount; i++) { pentagonOffsets.push(getOffset(vertexCount, size, i)); } allPentagonOffsets.push(pentagonOffsets); }
// 外側に広がる線 const outwardLinePaths: JSX.Element[] = []; allPentagonOffsets[0].forEach((offset, index) => { outwardLinePaths.push( <path d={`M ${BASE_SIZE / 2} ${BASE_SIZE / 2} L ${offset.x} ${offset.y}`} stroke="#dce5eb" key={index} /> ); });
// 各拡大度のn角形 const pentagonLinePaths: JSX.Element[] = []; allPentagonOffsets.forEach((allPentagonOffset, index) => { let pentagonPath = "M "; for (let i = 0; i <= allPentagonOffset.length; i++) { if (i !== 0) pentagonPath += " L "; const offset = i === allPentagonOffset.length ? allPentagonOffset[0] : allPentagonOffset[i]; pentagonPath += `${offset.x} ${offset.y}`; } pentagonLinePaths.push( <path d={pentagonPath} fill="none" stroke="#dce5eb" key={index} /> ); });
// スコアを表したn角形 let pentagonD = "M "; // スコアを表したn角形の頂点 const vertexPaths: JSX.Element[] = [];
for (let i = 0; i <= vertexCount; i++) { if (i !== 0) pentagonD += " L ";
let offset = getOffset( vertexCount, (scoreValues[i] / MAX_SCORE_VALUE) * BASE_SIZE, i ); if (i !== vertexCount) { vertexPaths.push( <circle cx={offset.x} cy={offset.y} r="3" fill={baseColor} key={i} /> ); } else { offset = getOffset( vertexCount, (scoreValues[0] / MAX_SCORE_VALUE) * BASE_SIZE, 0 ); } pentagonD += `${offset.x} ${offset.y}`; } const pentagonPaths = ( <path d={pentagonD} fill={`${baseColor}30`} stroke={baseColor} /> );
return ( <> {outwardLinePaths} {pentagonLinePaths} {pentagonPaths} {vertexPaths} </> );}
type Offset = { x: string; y: string;};
function getOffset(vertexCount: number, size: number, i: number): Offset { const x = ( BASE_SIZE / 2 + (size / 2) * Math.sin(((2 * Math.PI) / vertexCount) * i) ).toFixed(1); const y = ( BASE_SIZE / 2 - (size / 2) * Math.cos(((2 * Math.PI) / vertexCount) * i) ).toFixed(1);
return { x: x, y: y };}
radarChart.tsx
を上記のように実装する。
2. スタイルをあてる
.block { position: relative; display: flex; align-items: center; justify-content: center; width: 230px; height: 230px; padding: 35px;}
.radar-chart svg { padding: 5px;}
.scores { position: absolute; width: 100%; height: 100%;
> .score:nth-child(1) { top: -1%; left: 50%; transform: translateX(-50%); }
> .score:nth-child(2) { top: 24%; right: 7%; transform: translateX(50%); }
> .score:nth-child(3) { right: 7%; bottom: 24%; transform: translateX(50%); }
> .score:nth-child(4) { bottom: -1%; left: 50%; transform: translateX(-50%); }
> .score:nth-child(5) { bottom: 24%; left: 7%; transform: translateX(-50%); }
> .score:nth-child(6) { top: 24%; left: 7%; transform: translateX(-50%); }}
.score { position: absolute; font-size: 0.8rem; text-align: center;}
.value { margin: 0;}
チャート自体は描画できたがサイズや点数の配置が未設定なので、6角形のレーダーチャートの場合を想定してradarChart.module.scss
を実装。
3. コンポーネントを呼び出す
import RadarChart from "./radarChart";
export default function SomeComponent() { return ( <RadarChart scores={[ { title: "項目1", value: 5 }, { title: "項目2", value: 6 }, { title: "項目3", value: 7 }, { title: "項目4", value: 8 }, { title: "項目5", value: 9 }, { title: "項目6", value: 10 }, ]} baseColor="#181b29" ariaLabel="レーダーチャートのサンプル" /> );}
あとは適当なpropsを指定し呼び出せばOK。各propsの詳細は以下の通り。
props | 詳細 |
---|---|
scores | 項目の説明とスコア |
baseColor | チャート色付き部分の色 |
ariaLabel | レーダーチャートの説明 |
これでレーダーチャートを使い回せるようになった 🎉