👏

Cloud Firestore×Graph

2021/09/05に公開約9,500字

この記事について

Next.js+TypeScript+firebaseの構成で、ポモドーロタイマーアプリを作っています。

この記事では、Cloud Firestoreからデータフェッチとレンダリングを行いApexchartsでグラフ化する方法を解説していきます。

何か間違っている点などありましたらご指摘いただけると幸いです。🙏

解説すること

  • ApexChartsをNext.jsで使う方法
  • データをべた書きしてグラフ化
  • データをFirestoreから取得してグラフ化

参考にした記事

ApexChartsとは

ApexChartsとは、ユーザがデータをインタラクティブ(対話的)に扱えるように設計されたグラフ作成ライブラリの一つです。MITライセンスで作成された無料のOSSのため、手軽に利用できる商用アプリケーションです。

Next.jsでどう扱うのか

ApexChartsをNext.jsで扱うために、グラフが実際に画面表示されるメインページのsrc/pages/index.tsxとグラフデータを設定するsrc/views/ApexCharts/index.tsxを編集します。

データをべた書きしてグラフ化

完成したグラフ

メインページの設定

まずは、npmからReactアプリケーションにReact-ApexChartsコンポーネントをインストールします。

npm install --save react-apexcharts apexcharts

ApexChartsコンポーネントをNext.jsにimportし、return内でレンダリングします。

src/pages/index.tsx
const DynamicGraphComponentWithNoSSR = dynamic(() => import('../views/ApexCharts/index'), { ssr: false });

const Home = ({ currentUser }) => {

  return (
        <div style={{ marginBottom: 40 }}>
          <p className="title">作業時間</p>
          <React.StrictMode>
            <DynamicGraphComponentWithNoSSR />
          </React.StrictMode>
        </div>
  );
};

export default Home;

グラフデータの設定

今回は、ApexCharts/Area Chartのデモコードをほぼそのまま使います。

series/nameにはカテゴリ名、series/dataには勉強時間をべた書きします。

optionsには、グラフ全体のデザインを設定できます。
options/chart/heightにはグラフのスケール、options/chart/typeにはグラフの形状、
xaxis/typeにはX軸に格納するデータ型、xaxis/categoriesには日付を1000ミリ秒単位でべた書きします。

最後に、<ReactApexChart />をreturnする際の属性(props):options={this.state.options} , series={this.state.series} を渡します。

src/views/ApexCharts/index.tsx
import React, { Component } from 'react';
import ReactApexChart from 'react-apexcharts';

class Graph extends Component<{}, { series: any[]; options: any }> {
  constructor(props) {
    super(props);

    this.state = {
      series: [
        {
          name: 'React勉強',
          data: [30, 90, 60, 100, 60],
        },
        {
          name: 'Vue勉強',
          data: [20, 120, 40, 150, 40],
        },
      ],
      options: {
        chart: {
          height: 350,
          type: 'area',
        },
        dataLabels: {
          enabled: false,
        },
        stroke: {
          curve: 'smooth',
        },
        xaxis: {
          type: 'datetime',
          categories: [
            '2021-06-10T00:00:00.000Z',
            '2021-06-11T01:30:00.000Z',
            '2021-06-12T02:30:00.000Z',
            '2021-06-13T03:30:00.000Z',
            '2021-06-14T04:30:00.000Z',
          ],
          labels: {
            datetimeFormatter: {
              year: 'yyyy',
              month: "MMM'yy",
              day: 'MM/dd',
              hour: 'HH:mm',
            },
          },
        },
        tooltip: {
          x: {
            format: 'dd/MM/yy HH:mm',
          },
        },
      },
    };
  }

  render() {
    return (
      <div id="chart">
        <ReactApexChart options={this.state.options} series={this.state.series} type="area" height={350} />
      </div>
    );
  }
}

export default Graph;

ここまでは、グラフのベースコードに値をべた書きすればグラフ化されるという初歩的な内容でした。

そして、データをDBから引っ張る内容はネットで少ししか得られなかったので、以下の内容を共有します!!

Cloud Firestoreからデータフェッチしてグラフ化

完成したグラフ

※コードをべた書きしていた場合とデータの入れ方を変更しております。

グラフデータの設定

ここで、React Hooksを使ってCloud Firestoreからデータフェッチとレンダリングを行いたいので、ApexChartsをクラス型コンポーネントから関数型コンポーネントに書き換えます。

変更前のクラス型コンポーネント

src/views/ApexCharts/index.tsx
class Graph extends Component<{}, { series: any[]; options: any }> {
  constructor(props) {
    super(props);

    this.state = {
      //series内のデータ(name, data)はべた書きしていました。
      series: [
        {
          name: 'React勉強',
          data: [30, 90, 60, 100, 60],
        },
        {
          name: 'Vue勉強',
          data: [20, 120, 40, 150, 40],
        },
      ],
      options: {
      //X軸のパラメータもべた書きしていました。
        xaxis: {
          type: 'datetime',
          categories: [
            '2021-06-10T00:00:00.000Z',
            '2021-06-11T01:30:00.000Z',
            '2021-06-12T02:30:00.000Z',
            '2021-06-13T03:30:00.000Z',
            '2021-06-14T04:30:00.000Z',
          ],

変更後の関数型コンポーネント

まずは、seriesに入れたいデータ(name, data)に対してTypeScriptの型宣言を行います。

src/views/ApexCharts/index.tsx

type GraphData = {
  x: string;
  y: number;
};

type Series = {
  name: string;
  data: GraphData[];
}[];

type GraphProps = {
  series: Series;
};

const Graph: React.FC<GraphProps> = ({ series }) => {
  const state = {
    series: series,

次に、ログインユーザ(userID)に対応するCategoryIDとデータをCloud Firestoreに格納します。

さらに、getSeries.getRecordsでCloud Firestoreから取得したx, y軸のデータをdata[]に格納した後、Cloud Firestoreから取得したデータ(name)とdata[]をseries[]に格納します。

src/util/firebase.js
  getSeries = async (userId) => {
    const getRecords = async (userId, categoryId) => {
      let data = [];
      const ref = db.collection('users').doc(userId).collection('record').where('categoryId', '==', categoryId);
      try {
        const querySnapshot = await ref.get();
        await Promise.all(
          querySnapshot.docs.map((doc) => {
            if (doc.exists) {
              data.push({ x: doc.data().date, y: doc.data().time });
            }
          }),
        );
        return data;
      } catch (e) {
        throw e;
      }
    };
    
    let series = [];
    const ref = this.categories.where('userId', '==', userId);
    try {
      await ref.get().then(async function (querySnapshot) {
        await Promise.all(
          querySnapshot.docs.map(async (doc) => {
            if (doc.exists) {
              const records = await getRecords(userId, doc.id);
              series.push({ name: doc.data().name, data: records });
            }
          }),
        );
      });
      console.log('return');
      return series;
    } catch (e) {
      throw e;
    }
  };
}

Cloud Firestoreの設定

格納データに関してですが、firebase.jsを見ればおおよその階層構造がわかると思います🙏。
今回のグラフ化に用いたデータのパラメータと型の対応は以下の通りです。

パラメータ
categories/doc/name string
categories/doc/userId string
users/doc/record/doc/categoryId string
users/doc/record/doc/date number
users/doc/record/doc/time number

dateの取得方法

以下のコードをデバッグすると、1970年1月1日(UTC)を基準とした1000ミリ秒単位の経過時間の整数値をブラウザで確認できます。この値は直感的に分かりにくいですね。🙈

let date = new Date('2021.09.05') .getDate();
console.log(data);

メインページの設定

コンポーネント内にprops:series={series}を渡してレンダリングします。

src/pages/index.tsx
return (
        <div style={{ marginBottom: 40 }}>
          <p className="title">作業時間</p>
          <DynamicGraphComponentWithNoSSR series={series} />
        </div>
  );

しかし、ここでsrc/views/ApexCharts/idnex.tsx内でTypeScriptエラーが発生しました。
デバッグしてアプリの起動はできますが、赤い波線がずっと残るのは辛いので対応します。

src/views/ApexCharts/idnex.tsx
  return (
    <div id="chart">
      <ReactApexChart options={state.options} series={state.series} type="area" height={350} />
    </div>
  );

optionsに発生したエラー

この呼び出しに一致するオーバーロードはありません。
  2 中 1 のオーバーロード, '(props: Props | Readonly<Props>): ReactApexChart' により、次のエラーが発生しました。
    型 '{ chart: { height: number; type: string; }; dataLabels: { enabled: boolean; }; stroke: { curve: string; }; xaxis: { type: string; labels: { datetimeFormatter: { year: string; month: string; day: string; hour: string; }; }; }; tooltip: { ...; }; }' を型 'ApexOptions' に割り当てることはできません。
      'chart.type' の型は、これらの型同士で互換性がありません。
        型 'string' を型 '"area" | "line" | "bar" | "histogram" | "pie" | "donut" | "radialBar" | "scatter" | "bubble" | "heatmap" | "treemap" | "boxPlot" | "candlestick" | "radar" | "polarArea" | "rangeBar"' に割り当てることはできません。
  2 中 2 のオーバーロード, '(props: Props, context: any): ReactApexChart' により、次のエラーが発生しました。
    型 '{ chart: { height: number; type: string; }; dataLabels: { enabled: boolean; }; stroke: { curve: string; }; xaxis: { type: string; labels: { datetimeFormatter: { year: string; month: string; day: string; hour: string; }; }; }; tooltip: { ...; }; }' を型 'ApexOptions' に割り当てることはできません。ts(2769)

ApexOptions のchart.typeで許される型は以下のユニオン型の以下の型群です。

 '"area" | "line" | "bar" | "histogram" | "pie" | "donut" | "radialBar" | "scatter" | "bubble" | "heatmap" | "treemap" | "boxPlot" | "candlestick" | "radar" | "polarArea" | "rangeBar"' 

今渡そうとしているオブジェクトのchart.type は以下のようにString型で定義しているので、上記のユニオン型群に一致しないというエラー内容ですね。

src/views/ApexCharts/idnex.tsx
    options: {
      chart: {
        height: 350,
        type: 'area',

TypeScriptの型一覧TypeScriptのGenericsを参考に、ユニオン型に変更してもエラーは消えてくれないです(´;ω;`)ウッ…
アプリは動くのでいったん保留にします。

    options: {
      chart: {
        height: 350,
        type: "area" || "line" || "bar" || "histogram" || "pie" || "donut" || "radialBar" || "scatter" || "bubble" || "heatmap" || "treemap" || "boxPlot" || "candlestick" || "radar" || "polarArea" || "rangeBar",
      },

積み残し

現段階では、Cloud Firestoreに扱いたいデータを直接打ち込んでいるので、カテゴリのCRUDと連携させてグラフ化できる仕様に変更したいですね🙈。

所感

初めはChart.jsが扱いやすそうでしたが、デザインと実装への興味からApexChartsを用いたグラフ化を行いました。

TSの型宣言、propsの受け渡し、関数の呼び出しには個人的に苦戦しましたが、コラボレータがCloud Firestoreとの連携を無事完了してくれました、さすがっす🙈。

最後までお読みいただきありがとうございました😊

Discussion

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