react-chartjs-2 を使ってトレーニング履歴を複数の折れ線グラフで表示してみる
React の学習を兼ねてトレーニングの履歴管理を行うWebアプリを作ってみました。
ジムに行ってマシンを使いながら筋トレをしていますが前回どの重量で何回やったかを管理して徐々に負荷を増やしていく必要があります。
前回までの履歴をグラフ化すればどの負荷を増やすべきか一目瞭然だと思い以下のような見た目で履歴表示の部分を実装しました。横軸が日付けで左縦軸が重量、右縦軸が回数です。
利用した技術
フロントエンドは React で構築しバックエンドには Firebase を利用しました。状態管理は Context 、 UI Component には Chakra UI を使っています。
グラフの描画については Chart.js を使うために React 用のラッパーライブラリである react-chartjs-2 を利用しました。
グラフの描画方法
react-chartjs-2 の使い方
基本的には以下の手順で簡単にグラフを描画できます。
- react-chartjs-2 をインストール
- 使いたいグラフのコンポーネントを import
- import したコンポーネントに options, data を渡す
折れ線グラフだと以下のようなコードになります。(ドキュメントのサンプル)
import React from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import faker from 'faker';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
export const options = {
responsive: true,
plugins: {
legend: {
position: 'top' as const,
},
title: {
display: true,
text: 'Chart.js Line Chart',
},
},
};
const labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
export const data = {
labels,
datasets: [
{
label: 'Dataset 1',
data: labels.map(() => faker.datatype.number({ min: -1000, max: 1000 })),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.5)',
},
{
label: 'Dataset 2',
data: labels.map(() => faker.datatype.number({ min: -1000, max: 1000 })),
borderColor: 'rgb(53, 162, 235)',
backgroundColor: 'rgba(53, 162, 235, 0.5)',
},
],
};
export function App() {
return <Line options={options} data={data} />;
}
options にはグラフの装飾をするための設定、data.labels には横軸のラベル、data.datasets に各折線グラフの縦軸の値や線の色のデータを配列で指定することでグラフが描画できます。
メニューごとに複数のグラフを描画する
メニューごとにグラフを作る必要があるので LineChart というオリジナルコンポーネントを作成し map を使ってメニューの数だけループするようにしました。見た目の調整のため Chakra UI のコンポーネント(Flex, Box)を使っています。
<Flex flexWrap="wrap" display={{ base: "block", md: "flex" }}>
{menus.map((menu, index) => (
<Box mx={{ base: 1, md: 7 }} my={5} maxW="400px" key={menu.id}>
<LineChart
labels={labels}
menuName={menu.name}
weightData={weightDataList[index]}
countData={countDataList[index]}
setData={setDataList[index]}
/>
</Box>
))}
</Flex>
labels はグラフの横軸の値の配列なので日付の数値を履歴(hisotory)から取り出して配列に入れています。ちなみに history は date, menus が含まれるデータ構造です。
export type History = {
id: string;
date: string;
day: number;
menus: Menu[];
};
export type Menu = {
id: string;
name: string;
memo: string;
weight: number | null;
weightType: WeightType;
count: number | null;
set: number | null;
};
weightDataList, countDataList, setDataList には重さ、回数、セット数の値を history から抜き出して配列に入れています。この時 menu ごとに分けたいので二次元配列で格納しました。
const [labels, setLabels] = useState<string[]>([]);
const [menus, setMenus] = useState<HistoryMenu[]>([]);
const [weightDataList, setWeightDataList] = useState<number[][]>([]);
const [countDataList, setCountDataList] = useState<number[][]>([]);
const [setDataList, setSetDataList] = useState<number[][]>([]);
const setLineData = useCallback(() => {
let weights: number[][] = [];
let counts: number[][] = [];
let sets: number[][] = [];
menus.forEach((targetMenu, index) => {
weights[index] = [];
counts[index] = [];
sets[index] = [];
histories.forEach((history) => {
history.menus.forEach((menu) => {
if (menu.id === targetMenu.id) {
weights[index].push(menu.weight as number);
counts[index].push(menu.count as number);
sets[index].push(menu.set as number);
}
});
if (!history.menus.map((menu) => menu.id).includes(targetMenu.id)) {
counts[index].push(0);
sets[index].push(0);
}
});
});
setWeightDataList(weights);
setCountDataList(counts);
setSetDataList(sets);
}, [histories, menus]);
useEffect(() => {
if (menus.length > 0) setLineData();
}, [menus.length, setLineData]);
LineCart コンポーネントでは props で受け取ったデータを Line コンポーネントに渡せばオッケーです。
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
type Props = {
labels: string[];
menuName: string;
weightData: number[];
countData: number[];
setData: number[];
};
export const LineChart = (props: Props) => {
const { labels, menuName, weightData, countData, setData } = props;
const options = {
responsive: true,
plugins: {
legend: {
position: "top" as const,
},
title: {
display: true,
text: menuName,
},
},
interaction: {
mode: "index" as const,
intersect: false,
},
stacked: false,
scales: {
y: {
type: "linear" as const,
display: true,
position: "left" as const,
},
y1: {
type: "linear" as const,
display: true,
position: "right" as const,
grid: {
drawOnChartArea: false,
},
},
y2: {
type: "linear" as const,
display: false,
},
},
};
const data = {
labels,
datasets: [
{
label: "重さ",
data: weightData,
borderColor: "#f75c3d",
backgroundColor: "#fff",
yAxisID: "y",
},
{
label: "回数",
data: countData,
borderColor: "#76C6DE",
backgroundColor: "#fff",
yAxisID: "y1",
},
{
label: "セット数",
data: setData,
borderColor: "lightgrey",
backgroundColor: "#fff",
yAxisID: "y2",
},
],
};
return <Line options={options} data={data} />;
};
menus が4つの場合は以下のように4つのグラフが並びます。
まとめ
React, TypeScript, react-chartjs-2, Chakra UI などを使ってトレーニング履歴を複数の折れ線グラフで表示する方法について解説しました。
グラフ表示の実装の際にお役に立てば幸いです。
react-chartjs-2 を使うと棒グラフ、円グラフ、バブルチャートなどを簡単に描画できるのでとても便利ですね。
作成したWebアプリの最終的なソースコードを GitHub にて公開しています。ご興味あればどうぞ。
Discussion