第5回 2020年版 ReactのRechartsで新型コロナウイルス感染症対策サイトのデータを可視化する


# 1. 概要
2020年3月4日に公開された[新型コロナウイルス感染症対策サイト](https://stopcovid19.metro.tokyo.lg.jp/)は、Vue＋Nuxt.jsで構築されており、プログラムはGithubで公開され、データも[オープンデータカタログ](https://catalog.data.metro.tokyo.lg.jp/dataset)として公開される予定となっています。

今回は、この新型コロナウイルス感染症対策サイトのデータをReactのRechartsを利用して、可視化を行いたいと思います。

- [新型コロナウイルス感染症対策サイト](https://stopcovid19.metro.tokyo.lg.jp/)
- [新型コロナウイルス感染症対策サイト Github](https://github.com/tokyo-metropolitan-gov/covid19)
- [Recharts](http://recharts.org/en-US/)

# 2. 前提条件
## 作業日時
- 2020/3/12

## 環境
- MacBook Pro
- macOS Catalina

## ソフトウェアのバージョン
| ソフトウェア | バージョン |
|:-:|:-:|
| React              | 16.13.0      |
| React-dom          | 16.13.0      |
| Typescript         | 3.7.2        |
| Recharts           | 2.0.0-beta.1 |
| Material UI        | v4.9.5       |
| @material-ui/core  | 4.9.5        |
| @material-ui/icons | 4.9.1        |

# 3. Reactアプリの作成
## 3.1. Rechartsのインストール
`create-react-app`でサンプル用のReactのアプリを作成します。
`Recharts`の他に`Material UI`をインストールします。

```console
$ npx create-react-app recharts-sample --template typescript
$ cd recharts-sample/
$ yarn add recharts @material-ui/core @material-ui/icons
$ yarn add -D @types/recharts
```

## 3.2. プログラムの作成

まず、`App.tsx`を修正します。
グラフを表示する`Charts`のコンポーネントを追加します。

```tsx:src/App.tsx
import React from 'react';
import './App.css';
import Charts from './Charts';

function App() {
  return (
    <div className="App">
      <Charts />
    </div>
  );
}

export default App;
```

次に、`Charts`のコンポーネントを作成します。

`fetch`で非同期に外部サイトからデータを取得し、取得完了後にグラフを表示しています。
＊[東京都のオープンデータカタログ](https://catalog.data.metro.tokyo.lg.jp/dataset)にデータがすべて揃っていないので、今回は新型コロナウイルス感染症対策サイトのデータを拝借させて貰っています。

取得したデータを`Recharts`で描画可能なデータ形式に変換し、 `<BarChart data={patients} ...` という形でデータを渡します。また、`<XAxis dataKey="date" ...` で対象のデータを指定します。

```tsx:src/Charts.tsx
//import * as React from 'react';
import React, { useState, useEffect } from "react";

import { Theme, createStyles, makeStyles } from '@material-ui/core';

import {
  Paper,
  Typography,
  Grid,
  CircularProgress
} from '@material-ui/core';

import {
  ResponsiveContainer,
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip as ChartTooltip,
} from 'recharts';

import { format, getUnixTime, parse } from 'date-fns'

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      margin: theme.spacing(1),
    },
    charts: {
      width: '100%',
      height: '300px',
    },
    card: {
      width: '100%',
      height: '300px',
    }
  }),
);

export type Contacts = {
  date: string,
  data: Array<any>
};
export type Querents = {
  date: string,
  data: Array<any>
};
export type Patients = {
  date: string,
  data: Array<any>
};
export type PatientsSummary = {
  date: string,
  data: Array<any>
};
export type DischargesSummary = {
  date: string,
  data: Array<any>
};
export type Inspections = {
  date: string,
  data: Array<any>
};
export type InspectionsSmmary = {
  date: string,
  data: {
    都内: Array<any>,
    その他: Array<any>
  },
  labels: Array<string>
};
export type MainSummary = {
  attr: string,
  value: number,
  children: Array<any>
};

export type ICovid19Data = {
  contacts: Contacts
  querents: Querents,
  patients: Patients,
  patients_summary: PatientsSummary,
  discharges_summary: DischargesSummary,
  inspections: Inspections,
  inspections_summary: InspectionsSmmary,
  lastUpdate: string,
  main_summary: MainSummary
};

type Props = {};

// FetchAPIでのデータ取得先
const url = "https://raw.githubusercontent.com/tokyo-metropolitan-gov/covid19/development/data/data.json"

const chart_margin = { top: 5, right: 50, left: 50, bottom: 25 };

/*
* Rechartsで可視化
*/
export const Charts = (props: Props) => {

  const [codiva_data, setData] = useState<ICovid19Data | undefined>();
  const [loading, setLoading] = useState(true);

  const classes = useStyles(props);

  // useEffect
  useEffect(() => {
    console.log('useEffect');

    let cleanedUp = false;

    const fetchData = async () => {
      const response = await fetch(url);
      const result: ICovid19Data = await response.json();

      if (!cleanedUp) { // unmount後にデータの読み込みが完了した場合はデータを設定しない
        // Objectをコピーする。 Object.assign でも良い。
        setData(prevState => {
          return { ...prevState, ...result };
        });

        setLoading(false);
      }
    };

    fetchData();

    return () => { cleanedUp = true; }; //　戻り値の関数がunmount時点で実行される

  }, [setData]);

  if (loading || codiva_data === undefined) {
    console.log("Loadingの表示");
    return (<div><CircularProgress>Loading...</CircularProgress></div>)

  } else {
    console.log("グラフの表示");

    const patients = codiva_data.patients_summary.data;
    for (var key in patients) {
      patients[key].date = getUnixTime(parse(patients[key].日付, "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", new Date())) * 1000;
    }

    const contacts = codiva_data.contacts.data;
    for (key in contacts) {
      contacts[key].date = getUnixTime(parse(contacts[key].date, 'yyyy-MM-dd', new Date())) * 1000;
    }

    const querents = codiva_data.querents.data;
    for (key in querents) {
      querents[key].date = getUnixTime(parse(querents[key].date, 'yyyy-MM-dd', new Date())) * 1000;
    }

    const inspections = Array();
    for (key in codiva_data.inspections_summary.labels) {
      inspections.push({
        date: getUnixTime(parse(codiva_data.inspections_summary.labels[key], 'M/d', new Date())) * 1000,
        tokyo: codiva_data.inspections_summary.data.都内[key],
        other: codiva_data.inspections_summary.data.その他[key],
      })
    }

    return (
      <Grid container className={classes.root} spacing={3} justify="center" >
        <Grid item xs={6} >
          <Paper>
            <Typography variant="h5">陽性患者数</Typography>
            <div className={classes.charts}>
              {/* // 親要素でwidth,heightの指定が必要。 */}
              <ResponsiveContainer width="95%">
                <BarChart
                  // ResponsiveContainerを利用しない場合はwidth,heightを指定する
                  // width={600}  //グラフ全体の幅を指定
                  // height={280}  //グラフ全体の高さを指定          
                  data={patients} // 表示するデータ  
                  margin={chart_margin}>
                  <XAxis // X軸
                    dataKey="date" // X軸の基準となるデータ項目名
                    tickFormatter={(x) => format(x, 'M/d')} // X軸を YYYY/MM/DD 形式で表示します
                  />
                  <YAxis
                    domain={[0, 10]}
                    ticks={[0, 2, 4, 6, 8, 10]} // Y軸に表示する温度
                    unit="人" // Y軸の単位
                  />
                  <CartesianGrid // ガイド線の表示
                    stroke="#ccc"
                    strokeDasharray="3 3"
                  />
                  <ChartTooltip // ツールチップの表示
                    labelFormatter={(x) => format(x as number, 'M/d')} // ラベルの表示フォーマット（日付）
                  />
                  <Bar
                    name="陽性患者数"
                    dataKey="小計" // charts_data のキー
                    stroke="salmon" // 線の色
                    fill="salmon"
                    unit="人" //単位
                  />
                </BarChart>
              </ResponsiveContainer>
            </div>
          </Paper>
        </Grid>
        <Grid item xs={6} >
          <Paper>
            <Typography variant="h5">検査実施数</Typography>
            <div className={classes.charts}>
              <ResponsiveContainer width="95%">
                <BarChart data={inspections} margin={chart_margin}>
                  <XAxis dataKey="date" tickFormatter={(x) => format(x, 'M/d')} />
                  <YAxis domain={[0, 250]} ticks={[0, 50, 100, 150, 200, 250]} unit="人" />
                  <CartesianGrid stroke="#ccc" strokeDasharray="3 3" />
                  <ChartTooltip labelFormatter={(x) => format(x as number, 'M/d')} />
                  <Bar name="都内発生" dataKey="tokyo" stackId="a" stroke="salmon" fill="salmon" unit="人" />
                  <Bar name="その他" dataKey="other" stackId="a" stroke="#8884d8" fill="#8884d8" unit="人" />
                </BarChart>
              </ResponsiveContainer>
            </div>
          </Paper>
        </Grid>
        <Grid item xs={6} >
          <Paper>
            <Typography variant="h5">新型コロナコールセンター相談件数</Typography>
            <div className={classes.charts}>
              <ResponsiveContainer width="95%">
                <BarChart data={contacts} margin={chart_margin}>
                  <XAxis dataKey="date" tickFormatter={(x) => format(x, 'M/d')} />
                  <YAxis domain={[0, 700]} ticks={[0, 100, 200, 300, 400, 500, 600, 700]} unit="人" />
                  <CartesianGrid stroke="#ccc" strokeDasharray="3 3" />
                  <ChartTooltip labelFormatter={(x) => format(x as number, 'M/d')} />
                  <Bar name="相談件数" dataKey="小計" stroke="salmon" fill="salmon" unit="人" />
                </BarChart>
              </ResponsiveContainer>
            </div>
          </Paper>
        </Grid>
        <Grid item xs={6} >
          <Paper>
            <Typography variant="h5">新型コロナ受診相談窓口相談件数</Typography>
            <div className={classes.charts}>
              <ResponsiveContainer width="95%">
                <BarChart data={querents} margin={chart_margin}>
                  <XAxis dataKey="date" tickFormatter={(x) => format(x, 'M/d')} />
                  <YAxis domain={[0, 3000]} ticks={[0, 500, 1000, 1500, 2000, 2500, 3000]} unit="人" />
                  <CartesianGrid stroke="#ccc" strokeDasharray="3 3" />
                  <ChartTooltip labelFormatter={(x) => format(x as number, 'M/d')} />
                  <Bar name="相談件数" dataKey="小計" stroke="salmon" fill="salmon" unit="人" />
                </BarChart>
              </ResponsiveContainer>
            </div>
          </Paper>
        </Grid>
      </Grid>
    );
  }

}

export default Charts;
```
# 4. 実行結果
`yarn start`でサーバーを起動し、画面を表示してみます。

![Rechartsサンプル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293373/fbbfb320-fb25-b0dc-a7b4-b37c85cb4aba.gif)

# 5. 最後に
今回はRechartsを利用して可視化を行う方法について説明しました。
データを設定するだけで綺麗なグラフを表示してくれます。レポート機能などはRechartsである程度対応できると思います。

次回は[FirebaseのCloud storageに画像をアップロードする方法](https://qiita.com/tetsurotayam/items/30a0609a0cca5ba5ff0a)について説明をします。

# 6. 過去の記事
過去のReactに関する記事です。

- [第1回 2020年版 Node.js+Reactのインストール](https://qiita.com/tetsurotayama/items/6dd91dc37941fab462a4)
- [第2回 2020年版 ReactのMaterial UI V4の使い方について](https://qiita.com/tetsurotayama/items/12d6c7b3463246306e87)
- [第3回 2020年版 React+Firebaseでアプリを作成する](https://qiita.com/tetsurotayama/items/179daca2cacb0caad0c5)
- [第4回 2020年版 既存のウェブサイトに React を追加する](https://qiita.com/tetsurotayama/items/a2e4edd5bc46bc592c32)
- [第6回 2020年版 React+Firebaseで画像のアップロード（その１）](https://qiita.com/tetsurotayama/items/30a0609a0cca5ba5ff0a)
- [第7回 2020年版 React+Firebaseで画像のアップロード（その２）](https://qiita.com/tetsurotayama/items/5129f0cfb21f9ec9b9a0)
- [第8回 2020年版 React+Firebaseで画像のアップロード（その３）](https://qiita.com/tetsurotayama/items/6fb3d3b6e01cac77c85f)
- [第9回 2020年版 ReactにStoryshotsを導入する](https://qiita.com/tetsurotayama/items/dd637e90f4ea008fd878)