Rechartsで表示するデータをMaterial-UIのSelect Componentで切り替えできるようにしてみた

2021.08.04

こんにちは、CX事業本部 IoT事業部の若槻です。

前回のエントリでは、UIライブラリMaterial-UIのSelect Componentを使用して、コンボボックスを実装しました。

今回は、ReactのグラフライブラリRechartsで表示するデータを前回実装したコンボボックスを使用して切り替えできるようにしてみました。

環境

  • React:17.0.1
  • Recharts:2.0.10

やってみた

デモ

ソースコードは上記のCodeSandboxのサンドボックスをご確認ください。

コード概要

円グラフ本体のモジュールです。RechartsのPieChartを使用しています。実装はExampleを参考にしました。

src/components/atoms/PieChart.tsx

import React from "react";
import { PieChart, Pie, Cell, Legend } from "recharts";

interface answerCountGroupedByChoice {
  name: string;
  count: number;
}

export const PieChartWithCustomizedLabel: React.FC<{
  data: answerCountGroupedByChoice[];
}> = ({ data }) => {
  const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
  const RADIAN = Math.PI / 180;
  const renderCustomizedLabel = ({
    cx,
    cy,
    midAngle,
    innerRadius,
    outerRadius,
    percent,
  }: any) => {
    const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
    const x = cx + radius * Math.cos(-midAngle * RADIAN);
    const y = cy + radius * Math.sin(-midAngle * RADIAN);

    return (
      <text
        x={x}
        y={y}
        fill="white"
        textAnchor={x > cx ? "start" : "end"}
        dominantBaseline="central"
      >
        {`${(percent * 100).toFixed(0)}%`}
      </text>
    );
  };

  return (
    <PieChart width={600} height={250}>
      <Pie
        data={data}
        cy={100}
        labelLine={false}
        label={renderCustomizedLabel}
        outerRadius={80}
        fill="#8884d8"
        dataKey="count"
        startAngle={-270}
      >
        {data.map((_: answerCountGroupedByChoice, index: number) => (
          <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
        ))}
      </Pie>
      <Legend />
    </PieChart>
  );
};

export default PieChartWithCustomizedLabel;

コンボボックスや円グラフを表示するページのモジュールです。

src/App.tsx

import "./styles.css";
import React, { useState, useRef } from "react";

import ComboBox, { ComboBoxItem } from "./components/atoms/ComboBox";
import { PieChartWithCustomizedLabel } from "./components/atoms/PieChart";

interface AnswerCountGroupedByChoice {
  name: string;
  count: number;
}

interface EnqueteResult {
  enqueteId: string;
  enqueteTitle: string;
  questions: {
    questionId: string;
    questionText: string;
    answerCountGroupedByChoice: AnswerCountGroupedByChoice[];
  }[];
}

const enqueteResultList: EnqueteResult[] = [
  {
    enqueteId: "1",
    enqueteTitle: "会社説明会(7月)",
    questions: [
      {
        questionId: "1",
        questionText: "得意なプログラム言語は?",
        answerCountGroupedByChoice: [
          { name: "1. Python", count: 40 },
          { name: "2. Ruby", count: 30 },
          { name: "3. JavaScript", count: 30 },
          { name: "4. Go", count: 20 },
        ],
      },
      {
        questionId: "2",
        questionText: "興味のある職種は?",
        answerCountGroupedByChoice: [
          { name: "1. エンジニア", count: 20 },
          { name: "2. システムアーキテクト", count: 15 },
          { name: "3. 営業", count: 10 },
          { name: "4. スクラムマスター", count: 15 },
        ],
      },
    ],
  },
  {
    enqueteId: "2",
    enqueteTitle: "会社説明会(8月)",
    questions: [
      {
        questionId: "1",
        questionText: "興味のある職種は?",
        answerCountGroupedByChoice: [
          { name: "1. エンジニア", count: 30 },
          { name: "2. システムアーキテクト", count: 20 },
          { name: "3. 営業", count: 15 },
          { name: "4. スクラムマスター", count: 25 },
        ],
      },
      {
        questionId: "2",
        questionText: "弊社の求人をどこで知りましたか?",
        answerCountGroupedByChoice: [
          { name: "1. DecvelopersIO", count: 15 },
          { name: "2. Web広告", count: 20 },
          { name: "3. SNS", count: 20 },
          { name: "4. 人材会社からの紹介", count: 30 },
        ],
      },
    ],
  },
];

const App = () => {
  //ComboBoxのアイテムとするアンケート一覧をStateで管理
  const [enqueteOptions] = useState<ComboBoxItem[]>(
    enqueteResultList.map((d) => {
      return {
        id: d.enqueteId,
        value: d.enqueteTitle,
      };
    })
  );
  //アンケートComboBoxで選択中のアンケートIDをStateで管理
  const [selectedEnqueteId, setSelectedEnqueteId] = useState<string>(
    enqueteResultList[0].enqueteId
  );
  //選択中のアンケートの質問一覧をRefで管理
  const questionOptionsRef = useRef(
    enqueteResultList
      .filter((d) => d.enqueteId === selectedEnqueteId)[0]
      .questions.map((d) => {
        return {
          id: d.questionId,
          value: d.questionText,
        };
      })
  );
  //質問ComboBoxで選択中の質問IDをStateで管理
  const [selectedQuestionId, setSelectedQuestionId] = useState(
    enqueteResultList[0].questions[0].questionId
  );

  const getAnswerCountGroupedByChoiceOfSpecifiedQuestion = (
    enqueteResultGroupedByChoice: EnqueteResult[],
    enqueteId: string,
    questionNumber: string
  ): AnswerCountGroupedByChoice[] => {
    return enqueteResultGroupedByChoice
      .filter((d) => d.enqueteId === enqueteId)[0]
      .questions.filter((d) => String(d.questionId) === questionNumber)[0]
      .answerCountGroupedByChoice;
  };

  const onEnqueteComboBoxChangeHandler = (enqueteId: string) => {
    //選択したアンケートIDをStateに指定
    setSelectedEnqueteId(enqueteId);

    //選択したアンケートの質問一覧
    const selectedEnqueteQuestions = enqueteResultList.filter(
      (d) => d.enqueteId === enqueteId
    )[0].questions;

    //選択したアンケートの先頭の質問をStateに指定
    setSelectedQuestionId(selectedEnqueteQuestions[0].questionId);

    //選択したアンケートの質問をRefに指定
    questionOptionsRef.current = selectedEnqueteQuestions.map((d) => {
      return {
        id: d.questionId,
        value: d.questionText,
      };
    });
  };

  return (
    <>
      <ComboBox
        inputLabel="アンケート"
        items={enqueteOptions}
        value={selectedEnqueteId}
        defaultValue={enqueteOptions[0].id}
        onChange={(selected) => onEnqueteComboBoxChangeHandler(selected)}
      />
      <ComboBox
        inputLabel="質問"
        items={questionOptionsRef.current}
        value={selectedQuestionId}
        defaultValue={"1"}
        onChange={(selected) => setSelectedQuestionId(selected)}
      />
      <PieChartWithCustomizedLabel
        data={getAnswerCountGroupedByChoiceOfSpecifiedQuestion(
          enqueteResultList,
          selectedEnqueteId,
          selectedQuestionId
        )}
      />
    </>
  );
};

export default App;

円グラフ周り以外の実装は基本的に前回のエントリを踏襲しています。

App.tsxからPieChart.tsxへは、コンボボックスで選択した質問の下記形式のデータを渡すことにより円グラフの表示を制御しています。

[
  { name: "1. Python", count: 40 },
  { name: "2. Ruby", count: 30 },
  { name: "3. JavaScript", count: 30 },
  { name: "4. Go", count: 20 }
]

おわりに

Rechartsで表示するデータをMaterial-UIのSelect Componentで切り替えできるようにしてみました。自分の操作で画面上の物体がグラフィカルに動かせるようになるのはやはり楽しいですね。

実装としては、前回のエントリでSelect Componentの切り替えによるState管理の実装をクリアしており、また、PieChart.tsxに直接渡せる集計済みのアンケートデータをあらかじめ作成していたので、今回はそこまで重くはなかったです。一方で実際の実装ではデータベースなどから取得したデータを今回のような形式に変換/集計する部分の実装が重い部分になってきます。またRechartsではグラフ毎にまったく異なる形式のデータを要するので、実装するグラフ種類が増えるほどデータ変換処理も増えていきます。データを可視化するためのダッシュボードなどの実装では付き物ではありますがやはり大変です。

以上