📈

【React】Chart.jsでモチベーショングラフを作る

2022/03/19に公開約7,100字

概要

個人アプリ開発の中で、モチベーショングラフ作りたいな〜となったので、作りました💪

モチベーショングラフとは

モチベーショングラフとは、こういうものです。

過去の自分自身の出来事・体験を振り返り、時系列でのモチベーションの揺れ動きをグラフで表したもの

https://nikken-career.jp/special/3758/#:~:text=モチベーショングラフとは、過去,さとして作成します。

就活でたまに使われたりしますね。

環境

react: 17.0.2
next: 12.0.9
chart.js: 3.7.1
react-chartjs-2: 4.0.1
@mui/material: 5.3.1

作るもの

  1. 線グラフ
  2. スライドバー(グラフの値を調節する)

線グラフ

まずは線グラフの表示からいきましょう。表示に必要なのは、以下の3つです。

  • 表示に際して必要なchart.jsモジュールの設定
  • 表示するデータ・オプションの設定
  • 線グラフコンポーネントのrender

chart.jsモジュールの設定

最初、これをせずにただ線グラフコンポーネントをimportして、renderしようとしたら、Error: "category" is not a registered scale.と怒られました。

なんでだろ?と調べていると、stackoverflowで「モジュールの設定をすればいけるよー」みたいなことが書いてありました。

https://stackoverflow.com/questions/67727603/error-category-is-not-a-registered-scale

どういうことだ??と思い公式のドキュメントを見ると、こういうことが書かれていました。

Chart.js 3 is tree-shakeable, so it is necessary to import and register the controllers, elements, scales and plugins you are going to use.

https://www.chartjs.org/docs/latest/getting-started/integration.html

どうやら、chart.jsのグラフは表示の際、グラフを構成するモジュールのうち、必要でないものは取り除き、本当に必要なコードだけを残す仕様(tree-shaking)となっているようでした。

なので、具体的な内容は後で書きますが、線グラフのデータやオプションをセットする中にそのミニマムな状態では取り扱っていない内容があり、そのために上記のエラーが出ている、ということでした。

したがって、フロントからモジュールの設定を別途入れてあげたところ、このエラーは直りました。
コードは以下です。

Chart.js
import { Chart, registerables } from "chart.js"

Chart.register(...registerables)

registerablesに、contollerやelementといった、chart.jsのグラフに関する全てのモジュールが入っていて、それをこちらから登録してあげています。
これで表示に際して、全てのモジュールが適用されるというわけです。

しかしやや不可解なのが、後述するデータやオプションの設定ではcategoryという名前のものには触れていませんでした。なので、なんでError: "category" is not a registered scale.という怒られ方をされたんだろう?ということです。

まあおそらく別のデータを設定したら、categoryも依存的に動く仕組みになっていたのでしょう(超適当)。

では次!

データ・オプションの設定

ここでは横軸のラベルやグラフの値をセットしていきます。
そんなに特筆することもないので、早速コードを載せます!

graph.tsx
const labels = [
  "小学生",
  "中学生",
  "高校生",
  "大学生",
  "20代前半",
  "20代後半",
]
const data = {
  labels: labels,
  datasets: [
    {
      label: "人生",
      data: [40, 60, 70, 40, 50, 80],
      borderColor: "rgb(75, 192, 192)",
    },
  ],
}
const options: {} = {
  maintainAspectRatio: false,
  responsive: false,
}

labelsが横軸のラベル、datasetsのlabelが凡例の名前、dataがグラフの数値、borderColorが線の色です。
また、optionsのmaintainAspectRatioが縦横比固定か否かです。

responsiveをfalseにしているのは、ここはデフォルトでtrueなのですが、このままにしていると最初それがグラフだと判別できないほど大きく表示されてしまい、調べた結果ここをfalseにすると指定したサイズ通りに表示されるとのことだったので、そうしています。

では次!

線グラフコンポーネントのrender

あとはrenderするだけです。
線グラフはLineという名前で、まるごとreact-chartjs-2からimportできます。
heightやwidthを指定して、表示してみましょう〜

graph.tsx
import { Chart, registerables } from "chart.js"
import { Line } from "react-chartjs-2"

Chart.register(...registerables)

const Graph = () => {
  const labels = [
    "小学生",
    "中学生",
    "高校生",
    "大学生",
    "20代前半",
    "20代後半",
  ]
  const data = {
    labels: labels,
    datasets: [
      {
        label: "人生",
        data: [40, 60, 70, 40, 50, 80],
        borderColor: "rgb(75, 192, 192)",
      },
    ],
  }

  const options: {} = {
    maintainAspectRatio: false,
    responsive: false,
  }

  return (
    <Line height={300} width={400} data={data} options={options} />
  )
}

export default Graph

現時点でのUIは以下です。

いいですね。それではスライドバーの実装に移りましょう。

スライドバー

とはいっても、スライドバーは表示とグラフの値の管理しかやることがないので、一瞬です。

値の保存・管理にはuseStateを使用し、スライドバーはMaterial UIから持ってきます。
またスライドバーのhandlerとしてsetStateを入れています。

特筆することはこれくらいなので、先ほどのグラフと併せたコードをいっきに載せてしまいます。

graph.tsx
graph.tsx
import { Slider as MuiSlider, InputLabel, Box } from "@mui/material"
import { Chart, registerables } from "chart.js"
import { useState } from "react"
import { Line } from "react-chartjs-2"
import { css } from "@emotion/react"

Chart.register(...registerables)

type Props = {
  stage: string
  value: number
  onChange: (event: Event, value: number | number[]) => void
}

const Slider = (props: Props) => {
  return (
    <div
      css={css`
        display: flex;
        margin-top: 5px;
      `}
    >
      <label>{props.stage}</label>
      <Box width={400}>
        <MuiSlider
          defaultValue={50}
          aria-label="slider"
          valueLabelDisplay="auto"
          value={props.value}
          onChange={props.onChange}
          css={css`
            margin-left: 20px;
          `}
        />
      </Box>
    </div>
  )
}

const Graph = () => {
  const [elementary, setElementary] = useState(50)
  const [middle, setMiddle] = useState(50)
  const [senior, setSenior] = useState(50)
  const [university, setUniversity] = useState(50)
  const [early20s, setEarly20s] = useState(50)
  const [late20s, setLate20s] = useState(50)
  const handleElementary = (event: Event, newValue: number | number[]) => {
    setElementary(newValue as number)
  }
  const handleMiddle = (event: Event, newValue: number | number[]) => {
    setMiddle(newValue as number)
  }
  const handleSenior = (event: Event, newValue: number | number[]) => {
    setSenior(newValue as number)
  }
  const handleUniversity = (event: Event, newValue: number | number[]) => {
    setUniversity(newValue as number)
  }
  const handleEarly20s = (event: Event, newValue: number | number[]) => {
    setEarly20s(newValue as number)
  }
  const handleLate20s = (event: Event, newValue: number | number[]) => {
    setLate20s(newValue as number)
  }
  const labels = [
    "小学生",
    "中学生",
    "高校生",
    "大学生",
    "20代前半",
    "20代後半",
  ]
  const data = {
    labels: labels,
    datasets: [
      {
        label: "人生",
        data: [40, 60, 70, 40, 50, 80],
        borderColor: "rgb(75, 192, 192)",
      },
    ],
  }

  const options: {} = {
    maintainAspectRatio: false,
    responsive: false,
  }

  return (
    <div
      css={css`
        display: flex;
        justify-content: space-between;
        align-items: center;
        flex-flow: column;
      `}
    >
      <Line height={300} width={400} data={data} options={options} />
      <div
        css={css`
          display: flex;
          flex-flow: column;
          margin-top: 40px;
        `}
      >
        <Slider stage="小学生" value={elementary} onChange={handleElementary} />
        <Slider stage="中学生" value={middle} onChange={handleMiddle} />
        <Slider stage="高校生" value={senior} onChange={handleSenior} />
        <Slider stage="大学生" value={university} onChange={handleUniversity} />
        <Slider stage="20代前半" value={early20s} onChange={handleEarly20s} />
        <Slider stage="20代後半" value={late20s} onChange={handleLate20s} />
      </div>
    </div>
  )
}

export default Graph

cssはよしなにつけました。

5つのhandlerのところが、同じロジックが複数あってもちゃもちゃしてますが、とりあえずはこのままでいいでしょう。
handlerの引数にはeventを設けないと、使用時に型で怒られるので注意です。

完成

最終形のUIはこちらです。
グラフの見た目が変わる様子を動画でお見せしたかったですが、埋め込めなさそうなので、やむなし。

所感

モチベーショングラフ、案外かんたんに作れてよかったです😁

参考記事

https://dev.classmethod.jp/articles/react-chartjs-2/
https://react-chartjs-2.netlify.app/docs/working-with-events

Discussion

ログインするとコメントできます