React.jsを用いたGridDBとWebAPIによるデータの可視化

はじめに

このブログでは、Webアプリのお供としてGridDBを使う方法を紹介します。具体的には、スキーマの作成、インジェスト、クエリのすべてのステップでGridDB WebAPIを利用する方法について説明します。

全てのプロセスはReact.jsのフロントエンドで処理されます。つまり、ユーザはGridDB WebAPIの認証情報を入力し、CSVファイルをアップロードし、コンテナがCOLLECTIONTIMESERIESかを選択し、GridDBに適切なスキーマを作成します。コンテナが作成されると、.csvファイル全体がデータベース上にアップロードされます。

データが揃ったら、クエリでサーバからデータを受け取り、最終的に recharts library を使って可視化することができます。

前提条件

まずは、GridDBサーバを立ち上げてください。また、GridDB WebAPIをインストールし、サーバ上で動作させます。

フロントエンドでは、react.js とチャートライブラリをインストールする必要があります。package.jsonファイルとは以下のようなものです。

{
  "name": "griddb-charts",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.8.2",
    "@emotion/styled": "^11.8.1",
    "@mui/material": "^5.5.0",
    "@testing-library/jest-dom": "^5.16.2",
    "@testing-library/react": "^12.1.4",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^0.26.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "5.0.0",
    "recharts": "^2.1.9",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

ファイルのトップには、インポートしたものもすべて取り込みます。

import React, { useState, useEffect } from 'react';
import './App.css';
import { usePapaParse } from 'react-papaparse';

import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  Tooltip,
  Legend
} from "recharts";
import Box from '@mui/material/Box';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import Container from '@mui/material/Container';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import FormHelperText from "@mui/material/FormHelperText";
import Grid from '@mui/material/Grid'

ETL

実装については、ユーザー認証、スキーマとコンテナの作成と送信、コンテナへのデータのプッシュ、データのクエリ、そしてデータをチャートに表示するところまでを順番に説明します。

証明書

まず始めに、ユーザと管理者の認証情報を入力するための簡単なテキスト・フィールドをいくつか作成します。このブログでは、GridDB のデフォルト値である admin/admin を、コンセプトの証明として使用することにします。

また、例をシンプルにするために、material ui のReact.jsフレームワークを使用します。

        <TextField
            id="user"
            label="Username"
            variant="standard"
            onChange={handleUser}
            required={true}
          />
          <TextField
            id="standard-basic"
            label="Password"
            variant="standard"
            onChange={handlePass}
            required={true}
          />

ここでは、テキストフィールドを別々の関数で処理していることがわかります。Reactの useState を使って変更を処理し、ユーザーの入力を保存しています。シンプルにするために、何らかのチェックは行わず、単純にすべての入力をstateに保存します。

  const [user, setUser] = useState('')
  const [pass, setPass] = useState('')

  const handleUser = (event) => {
    let val = event.target.value
    setUser(val)
  }

  const handlePass = (event) => {
    let val = event.target.value
    setPass(val)
  }

適切な javascript の fetch リクエストを作成するために、ユーザーの認証情報を base64 に変換してください。これは次のように行います。

  const [encodedUserPass, setEncodedUserPass] = useState('')

  useEffect(() => {
    let encodedUserPass = btoa(user + ":" + pass)
    setEncodedUserPass(encodedUserPass)
  }, [pass])

useEffectの関数コールバックの後の [pass] は、ステート変数 pass が更新されたときに、この特定の関数を更新することを意味しています。最もきれいな解決策ではありませんが、今回の目的には合っています。

CSVファイルをアップロードする

まず、ユーザが .csv ファイルをアップロードできるようにしましょう。これはHTMLを使って非常に簡単に実現できます。

<label >Choose a CSV File to Upload:</label>

        <input type="file"
          id="csvFile" name="file" onChange={fileHandler}
          accept=".csv"></input>
          

この fileHandler 関数は papa-parse ライブラリを利用してファイルを読み込み、内容をパースして後で使用するためにいくつかの React ステートを設定します。

import { usePapaParse } from 'react-papaparse';
  const { readString } = usePapaParse();

  const [fullCsvData, setFullCsvData] = useState(null);
  const [fileName, setFileName] = useState('');
  const [selectedFile, setSelectedFile] = useState(null);

  const fileHandler = (event) => {
    const file = event.target.files[0]
    let name = file.name
    let x = name.substring(0, name.indexOf('.')); //remove the .csv file extension
    const reader = new FileReader();
    reader.addEventListener('load', (event) => {
      var data = event.target.result

      readString(data, {
        worker: true,
        complete: (results) => {
          setFileName(x)
          console.log("selected file: ", results.data[0])
          setFullCsvData(results.data)
          setSelectedFile(results.data[0]);
          setOpen(true)
        },
      });
    });

    reader.readAsText(file);

  };

ユーザが .csv ファイルをアップロードすると、 fileHandler 関数が起動し、 event としてファイルを受け取ります。そして、ファイルの内容を読み込んで、すべてをパースします。パースされると、様々な React ステートが設定されます。ファイル名、csv コンテンツ全体、そしてカラム名に対応するデータの最初の配列が設定されます。

カラムタイプを設定する

ユーザーのファイルを解析する際、行のデータ型を確実に解析することが困難な場合があります。この問題を回避するために、ファイルがアップロードされるとモーダルを開き、ユーザーが各列のデータ型を設定できるようにします。このデータを使用して、Web API でスキーマを作成するために使用されるオブジェクトを形成します。

WebAPIが求める構造は以下の通りです。

'{
    "columns": [
        {
            "name": "timestamp",
            "type": "TIMESTAMP"
        },
        {
            "name": "name",
            "type": "STRING"
        },
        {
            "name": "value",
            "type": "FLOAT"
        }
    ],
    "container_name": "test",
    "container_type": "TIME_SERIES",
    "rowkey": true
}'

ユーザーがカラム情報を入力し、「Create Schema」ボタンをクリックすると、HTTPリクエストで送信するための適切なデータが準備されます。

<Button onClick={handleSchema} variant="contained">Create Schema</Button>

The handleSchema function simply calls our putSchema function

GridDB WebAPI HTTP Request (Put Schema/Container)

  const handleSchema = () => {
    if (Object.keys(chartColumns).length !== 0) { //chartColumns is an array of the column names from the csv
      putSchema(chartColumns)
    }
  }

  const putSchema = (obj) => {

    let data = new Object();
    data.columns = [];
    let n = Object.keys(obj).length
    let i = 0
    for (const property in obj) {
      if (i < n) {
        data.columns[i] = { "name": property, "type": (obj[property]).toUpperCase() }
        i++
      }
    }
    data["container_name"] = fileName // grabbed from the react State 
    data["container_type"] = "COLLECTION" // hardcoded for now
    data["rowkey"] = "true"

    let raw = JSON.stringify(data);

    let myHeaders = new Headers();
    myHeaders.append("Content-type", "application/json")
    myHeaders.append("Authorization", "Basic " + encodedUserPass);

    let requestOptions = {
      method: 'POST',
      headers: myHeaders,
      body: raw,
      redirect: 'follow'
    };

    fetch(`http://${ADDRESS}/griddb/v2/defaultCluster/dbs/public/containers`, requestOptions)
      .then(response => response.text())
      .then(result => {
        setChartColumns("Successful. Now Push Data") // displays where the object was being formed
        console.log(result)
      })
      .catch(error => {
        setChartColumns(error)
        console.log("Error: ", error)
      });
  }

ここでは、最初の Web API HTTP リクエストを作成します。その前に、収集したすべてのデータを取得し、API が期待するデータ構造を形成します。今のところ、コンテナ タイプを COLLECTION にハードコードしていますが、これはモーダルにスイッチを追加することで簡単に修正することができます。

次に、各カラムの名前とタイプを含むカラムの配列を内部に持つオブジェクトを作成します。データ構造が設定されたら、それを JSON.stringify して、requestOptions と共にリクエストのボディに送信します。一度リクエストが行われると、エラーがある場合はモーダルに表示されることに注意してください。成功した場合は、シンプルなメッセージが表示され、データをプッシュするようユーザーに促します。

GridDB WebAPI コンテナへのCSVデータプッシュ機能

次に、GridDBサーバがHTTPリクエストでコンテナとスキーマの作成を受け入れたら、HTTPリクエストで .csv データをサーバに送信します。

ユーザーは PUSH DATA ボタンを押して、putData 機能を起動します。

  const handlePushingData = () => {
    if (fullCsvData !== null) {
      putData(fullCsvData)
    }
  }

const putData = (data) => {

    data.shift();
    let removeEmpties = data.filter(ele => ele.length > 1)
    console.log("data: ", removeEmpties)

    let raw = JSON.stringify(removeEmpties)
    console.log(raw)

    let myHeaders = new Headers();
    myHeaders.append("Content-type", "application/json")
    myHeaders.append("Authorization", "Basic " + encodedUserPass);

    let requestOptions = {
      method: 'PUT',
      headers: myHeaders,
      body: raw,
      redirect: 'follow'
    };

    fetch(`http://${ADDRESS}/griddb/v2/defaultCluster/dbs/public/containers/${fileName}/rows`, requestOptions)
      .then(response => response.text())
      .then(result => setChartColumns("Successful: ", result))
      .catch(error => {
        setChartColumns(error)
        console.log("Error: ", error)
      });
  }

putSchema関数と同様に、React のステートからデータの全内容を取得し、送信する適切なデータを取得するためにいくつかの基本的な処理を行います。まず、配列の最初の要素を取り除き、これは単にカラム名です。次に、空の要素がある場合はそれを削除します。この HTTP リクエストメソッドがPUT(vs.POST) であることに注意してください。

成功した場合、カラムの量がモーダルに表示されます。エラーの場合は、その旨もモーダルに表示されます。ここまでで、.csvデータをサーバーに取り込むことができました。次は、クエリと表示です。

クエリ

デモのために、GridDBサーバに問い合わせ、データを再取得して可視化します。WebAPIによる問い合わせは非常に簡単です。

curl -X POST --basic -u admin:admin -H "Content-type:application/json" http://127.0.0.1:8080/griddb/v2/defaultCluster/dbs/public/containers/test/rows -d  '{"limit":1000}'  

このアプリケーションでは、ユーザが認証情報を入力し、QUERY ボタンを押して handleSubmitCreds 関数を実行した後にロードするようにします。サーバーはクエリの完全なデータで応答します。そのデータを取得し、recharts が表示できるように変換します。

  const handleSubmitCreds = () => {
    let raw = JSON.stringify({
      "limit": 100
    });

    let myHeaders = new Headers();
    myHeaders.append("Content-type", "application/json")
    myHeaders.append("Authorization", "Basic " + encodedUserPass);

    let requestOptions = {
      method: 'POST',
      headers: myHeaders,
      body: raw,
      redirect: 'follow'
    };

    fetch(`http://${ADDRESS}/griddb/v2/defaultCluster/dbs/public/containers/CEREAL/rows`, requestOptions)
      .then(response => response.text())
      .then(result => {
        let resp = JSON.parse(result)
        let rows = resp.rows

        let c = resp.columns
        let columns = [];
        c.forEach(val => columns.push(val.name))

        let map = new Map();
        let fullChartData = [];
        // transform data into more usable obj
        for (let i = 0; i < 72; i++) { //hard coding the length of rows (72)
          for (let j = 0; j < 16; j++) { // hard coding length of columns (16)
            map.set(columns[j], rows[i][j])
          }
          const obj = Object.fromEntries(map);
          fullChartData.push(obj)
        }
        setData(fullChartData)

      })
      .catch(error => console.log('error', error));
  }

useEffectの中の空の配列は、Reactにこの関数をページロード時のみ実行するよう指示するだけで、再レンダリングのたびに実行するわけではありません。

データを可視化する

上記の関数により、単にデータを照会するだけでなく、データをより使いやすい形に変換することができました。

recharts が期待するデータを得るために、Javascript の map object を使ってキーと値のデータペアを設定し、次に Object.fromEntries を使ってそのマップからオブジェクトを作成します。

今回のデモでは、kaggle cereals datasetを使用します。上の関数で読み込んだデータで、すべてのデータを折れ線グラフにすることができますが、個々のシリアルのマクロおよびミクロ栄養素を棒グラフで表示する方が、より興味深いユースケースとなるでしょう。そのために、すべてのシリアルがリストアップされたドロップダウンリストを作成します。ユーザーがシリアルを選択すると、アプリは対応するデータを見つけ、それをグラフに表示します。

    <FormControl fullWidth>
      <InputLabel id="demo-simple-select-label">Cereal</InputLabel>
      <Select labelId="demo-simple-select-label" id="demo-simple-select" value={choice} label="Cereal" onChange={handleChange}>
        <MenuItem value={"100% Bran"}>100% Bran</MenuItem>
        <MenuItem value={"100% Natural Bran"}>100% Natural Bran</MenuItem>
        <MenuItem value={"All-Bran"}>All Bran</MenuItem>
        <MenuItem value={"All-Bran with Extra Fiber"}> All-Bran with Extra Fiber</MenuItem>
        <MenuItem value={"Almond Delight"}>Almond Delight</MenuItem>

ユーザーがシリアルを選択すると、handleChange関数が実行されます。

  const handleChange = (event) => {
    let val = event.target.value
    console.log("val: ", val)
    setChoice(val);
  };

これは、シリアル名で選択するためのReactステートを設定します。アプリがその変化を検出すると、次のような関数が起動します。

  useEffect(() => {
    if (data !== null) {
      let userChoice = data.find(val => val.NAME == choice)
      setDisplayedData(userChoice);
    } else console.log("data still null")
  }, [choice])

この関数は、javascript の配列の find メソッドを使用して、React のステートに data として保存されている完全なデータセットから適切なデータを探します。このデータが見つかると、棒グラフは適切なデータ (displayedData) を表示します。

<BarChart
          width={1500}
          height={500}
          data={[displayedData]}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
          <XAxis dataKey="NAME" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar type="monotone" stackId="a" dataKey="MANUFACTURER" fill="#FFF" />
          <Bar type="monotone" stackId="a" dataKey="TYPE" fill="#FFF" />
          <Bar type="monotone" stackId="b" dataKey="CALORIES" fill="red" />
          <Bar type="monotone" stackId="c" dataKey="PROTEIN" fill="pink" />
          <Bar type="monotone" stackId="c" dataKey="FAT" fill="orange" />
          <Bar type="monotone" stackId="d" dataKey="SODIUM" fill="#82ca9d" />
          <Bar type="monotone" stackId="d" dataKey="FIBER" fill="#82ca9d" />
          <Bar type="monotone" stackId="c" dataKey="CARBO" fill="purple" />
          <Bar type="monotone" stackId="e" dataKey="SUGARS" fill="#82ca9d" />
          <Bar type="monotone" stackId="e" dataKey="POTASS" fill="#82ca9d" />
          <Bar type="monotone" stackId="f" dataKey="VITAMINS" fill="#82ca9d" />
          <Bar type="monotone" stackId="f" dataKey="SHELF" fill="#82ca9d" />
          <Bar type="monotone" stackId="f" dataKey="WEIGHT" fill="#82ca9d" />
        </BarChart>        

このグラフでは、ミクロ、マクロの栄養素を好きなように色分けして積み重ねることができます。今回のデモでは、カロリーをバーで表示する以外には何も設定していません。以下は、シナモントーストクランチのチャートです。

まとめ

このようにして簡単にcsvデータを取り込み、react.jsの recharts に読み込むことができるようになりました。

ソースコードの全文は、Githubページでご覧いただけます。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *