雑魚なりにこういうの作りました。ざっくりとレポートしたいと思います。
新型コロナの状況
主な環境・プラグインなど
環境 | 名称 |
---|---|
FW | Next.js |
静的型付け | TypeScript |
CSS | Styled-Components |
整形 | ESLint、Prettier |
チャート | react-vis, react-google-charts |
表 | react-data-table-component |
fetch | axios |
他 | useState,useContext,useMemo |
この Web ページでできること
- 増加や減少、平均値といった各数値を視覚的に表現
- 累積・当日の切り替えトグルの設置
- ツリーマップによる直感的な数値構成の表示
- 各都道県別のコロナ関連のデータ表示
きっかけ
- なにかモダンな言語を勉強してみたい
- インプットだけではなくてアウトプットしたい
- 比較的簡単そうで身近なもの(業務後の空き時間につくれるもの)
といった条件をもとに、身近で参考サイトもあるので勉強の題材としてはよさそうということで、新型コロナウイルスの感染状況の Web ページ作ることに決めました。
環境の全体像
新型コロナウイルスに関するデータは厚生労働省のオープンデータから取得しています。東洋経済オンラインの新型コロナウイルス感染状況というページを参考に作成しました。
なぜ react-vis なのか
- 見た目がシンプル・かっこいい
- データ形式がわかりやすい、機能も必要充分
- 公式のサンプルも充実している
- Github スター数が TOP 3に入る
- Rechart より容量が軽い
処理の全体的な流れ
- データフェッチ
- CSV データを基底オブジェクト形式に変換(変換の基底につかう)
- 基底クラスに 2.で作ったオブジェクトをぶちこむ
- react-vis コンポーネントの Props に対応するデータを 3.で作った関数によって出力
- すべての処理が終わったあとページを表示
データフェッチ
データがないと何も始まらないので、まずは引っ張ってきます。
データのソースが公的機関でかつ定期的に更新されているものが良かったので、厚生労働省と東洋経済オンライン様からデータを拝借しています。
当初は Github に公開リポジトリがありましたが、更新が停止していました。
以下に実装の一部分を晒します。
実装において axios 関数を使用しており、PCR 検査人数、陽性者数、重症者数などのデータを、非同期通信によってそれぞれ別の URL から取得しています。
const [
_positive,
_pcrTested,
_hospitalized,
_recovery,
_death,
_cases,
_severe,
] = await Promise.all([
axios.get(URL_COVID.POSITIVE),
axios.get(URL_COVID.PCR_TESTED),
axios.get(URL_COVID.HOSPITALIZED),
axios.get(URL_COVID.RECOVERY),
axios.get(URL_COVID.DEATH),
axios.get(URL_COVID.CASE),
axios.get(URL_COVID.SEVERE),
]);
CSV データを基底オブジェクト(基底連想配列)に変換
取得した CSV が string 型の長い文字列だったので以下の関数を実装し、二次元配列に変換しました。これをデータフェッチした回数ぶん実行します。
/**
* CSVフォーマットで記載されたStringを二次元配列に変換する
* @param csvData CSVフォーマットで記載された文字列
* @returns 二次元配列のcsvのデータ
*/
export const separate = (csvData: string) => {
return csvData.split(/\r?\n/).map((line) => line.split(","));
};
csvData.split(/\r?\n/)
こちらの部分で改行コードで文字列を分割し、配列を生成します。
map((line) => line.split(",")
行ごとに分割された配列に対して、カンマの文字でさらに分割し、二次元配列ができます。
二次元配列に変換することでデータをピックアップできるようになりました。
日付をキーとして、検査人数、陽性者数、重症者数などのデータをまとめ、基底のオブジェクトの型に近づけていきます。
基底オブジェクト(新型コロナ都道府県別情報)
今回もっとも苦労した点でもあり要となる項目です。
基底オブジェクト(新型コロナ都道府県別情報)Prefecture 型を定義します。
// 都道府県ごとに集計結果をまとめる
/**
* 都道府県別 新型コロナウイルスまとめ
* @property code 都道府県コード
* @property name 都道府県名称
* @property nameE 都道府県ローマ字
* @property dailyCovid 日付ごとの新型コロナウイルスの感染データ
*/
export type Prefecture = {
code: string;
name: string;
nameE: string;
dailyCovid: Covid[];
};
その中身となる DailyCovid 型は以下のとおりです。
/**
* 日付ごとに 新型コロナウイルス情報をまとめる
* @property ymd 年月日 YYYY/MM/DD
* @property pcrTested 検査実施人数
* @property positive 陽性者数
* @property hospitalized 入院治療等を要する者の数
* @property recovered 退院又は療養解除となった者の数
* @property severe 重症者
* @property deaths 死亡者数
*/
export type Covid = {
ymd: string;
pcrTested: number;
positive: number;
hospitalized: number;
recovered: number;
severe: number;
deaths: number;
effectiveReproductionNumber: number;
[key: string]: number | string;
};
Prefecture 型は都道府県ごとに日々のコロナ情報をまとめたものです。
このように基底となる連想配列を定義し、データのピックアップや並び替えのロジックを簡単に構築できるようになりました。
実際に格納されるデータは以下の画像のようになりました。
日付をキーとする連想配列を実装するか迷いましたが、あえてこのような形をとったのはインデックスの参照によって簡単に日付範囲を抽出できるためです。
クラスに基底オブジェクトをぶちこむ
クラス図ですがざっくり以下のようになっています。
抽象クラス BaseGenerator をあえて実装した理由として、その子にあたる Generator クラスの開発補助者に対し、抽象クラスによる宣言の束縛を理解してもらいたかったというのがあります。
const gene: Generator = new Generator(prefectureData);
クラスに関数を実装する
react-vis の Data format Referenceによるとグラフ表示には
const myData = [
{ x: "A", y: 10 },
{ x: "B", y: 5 },
{ x: "C", y: 15 },
];
といった連想配列をもつ配列をコンポーネントに渡す必要があることがわかります。Generator クラスに上記のデータを返却できるようにロジックを作りました。
/**
* 棒グラフデータ作成
* デフォルトで直近日から30日前までの棒グラフ用のオブジェクトの配列を取得する
* @param property 抽出したいキー名
* @param begin 抽出開始インデックス Default: 0
* @param end 抽出終了インデックス Default: 30
* @returns 棒グラフ用のオブジェクトの配列
*/
barChart(property: string, begin = Generator.DEFAULT_BEGIN, end = Generator.DEFAULT_END): VerticalBar[] {
let result: VerticalBar[] = new Array();
const dates = extractData(this.dailyCovid, "ymd");
const extracted = extractData(this.dailyCovid, property);
for (let i = begin; i < end; i++) {
const ymd = dates[i];
const value = extracted[i];
result.unshift({ x: ymd, y: value });
}
return result;
}
barChart 関数にある extractData 関数の定義は以下のようになってます。
引数 begin があるかどうかで処理が分岐しています。今回呼び出されたケースでは「ここ」の処理にいきます
/**
* 抽出元データのオブジェクトから任意のプロパティ値のみを抽出する
* @param dataObj 抽出対象データ [{},{},{},{}]
* @param key 抽出するプロパティ名称、またはインデックス
* @param begin 抽出開始インデックス
* @param end 抽出終了インデックス
* @returns keyの名称にマッチしたデータ
*/
export const extractData = <T>(
dataObj: T[],
key: Key,
begin?: number,
end?: number
) => {
let closedData: any[] = [];
if (begin && end){
return closedData = between(dataObj, key, begin, end);
}
if(begin) {
//中略
return closedData;
}
// ここの処理にいく
dataObj.forEach((obj: any) => {
closedData.push(obj[key]);
});
return closedData;
};
VerticalBar の定義は以下のとおりです。
/**
* @property x 日付
* @property y 値
*/
export type VerticalBar = { x: string; y: number };
最後にクライアントサイドで以下のように利用してあげれば、react-vis のグラフ表示に必要な Props ができます。
const gene: Generator = new Generator(prefectureData);
//return()内
<VerticalBarSeries
data={gene.barchart("positive")}
barWidth={0.4}
>
<XAxis />
<YAxis />
</VerticalBarSeries>
Interactive Map
この厨二病のような名前は適切な日本語訳がないのでそのまま英語を使用しています。Interactive Map は以下の特徴があります。
- 日本地図のどこかをクリックするとコンボボックスの値が連動して変化
- 逆にコンボボックスの値を変更すると、日本地図の選択状態も切替わる
- 選択された都道府県によってグラフのデータも変わる
方法は SVG フォーマットの画像を改造して実装しました。
const JapanSVG:React.FC<TypeJapanSVG> = (_props) => {
// ハンドラー 省略
return (
<Wrapper>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 500 400"
aria-label="Map of Japan"
id="svg-japan-map"
name="svg-japan-map"
>
<Path
onMouseOver={(e) => mouseOverHandler(e)}
onMouseOut={(e) => mouseOutHandler(e)}
onClick={(e: React.MouseEvent) => clickHandler(e)}
className="13"
id="Tokyo"
name="Tokyo"
d="m 310.35993,279.40889 -0.68,-0.39 -0.14,-0.41 ・・・長いので省略"
/>
{/*以下 Pathタグが46こ並ぶ*/}
この実装、力技すぎるの反省しております。
そもそも SVG とは
W3C で仕様が定義され、矩形や円、直線、文字列などの図形オブジェクトを XML 形式で記述し、Web ページでの図形描画にも使うことができる画像フォーマットはどれか。
平成29年度 秋期 応用情報技術者試験より引用
とこちらの問題にある通り、XML 形式で書かれた画像の拡張子です。
XML 形式なので DOM であり、HTML でもあり JSX でもあるとも言えます。
Next.js(React.js)の return()関数内に SVG をすべてぶちこむことで、コンポーネントを定義しました。
またイベントハンドラの定義は以下のように、マウスオーバー・アウト時に透明度を変化させてカーソルの位置の視認性を気持ちあげています。
const mouseOverHandler = (e: any) => {
e.target.setAttribute("fill-opacity", "0.7");
};
const mouseOutHandler = (e: any) => {
e.target.removeAttribute("fill-opacity");
};
Tree Map
理由:
- コロナに Tree Map を使用している日本のサイトはなかった
- 直感的に全体の状況がわかる
- なんかかっこいい
です。海外の株価のビジュアライズに使用されていたりします。スタイリッシュで欧米的な感じがするオリジナリティを醸し出したかったので採用しました。
Google App Engine にデプロイ
Next.js のデプロイ先の候補として真っ先挙げられるのは公式の vercel かと思います。ただ、採用に至らなかった理由としては
413: FUNCTION_PAYLOAD_TOO_LARGEのエラー問題です。
Serverless Functions have a payload limit of 5mb for request and response bodies.
How do I bypass the 5MB body size limit of Vercel Serverless Functions?
公式の引用ですが、リクエストボディに含める容量は5MBが限度です。CSR に切り替えることを検討しましたが、ソースコードの修正を行うのも今更でバグを生みたくもなかったので vercel は諦めました。vercel は簡単にデプロイできる素晴らしいサーバーレスプラットフォームなので、今後ともうまく付き合っていきたいです。
純粋な好奇心もあって GAE を選び、無料枠で Next.js をデプロイしました。
Next.js を GAE で動かす(CloudBuild から自動デプロイ)
まとめ
- Next.js のディレクトリ構成、CSR、SSR、SG の概念を学べた
- データビジュアライゼーションの勉強ができた
- react-vis のもつ機能についておおよそ理解できた
- GAE の設定も理解が深まった
- メンバーにタスク割り振りながら作業するレベルが上がった