SnowflakeとReactを使ったデータのリアルタイム可視化

# はじめに
はじめまして！

本記事では、Snowflakeに蓄積されたデータ（仮想IoT機器の温度）を、Express（Node.js）を使ったバックエンドで取得し、Reactフロントエンドを通じてリアルタイムで可視化するデモを紹介します。Snowflakeからのリアルタイムデータ取得から視覚化までの一連の流れが理解でき、IoTやデータ可視化などに応用できるはず...！

# 使用するライブラリとバージョン
このプロジェクトでは、以下のライブラリを使用します。

- Node.js & Express: サーバーサイドでのAPI構築（Express v4.19.2）
- Socket.io: クライアントとサーバー間でリアルタイム通信を実現（Socket.io v4.7.5）
- Snowflake SDK: Snowflakeデータベースへのアクセス（Snowflake SDK v1.12.0）
- React: フロントエンドのUI構築（React v18.3.1）
- Chart.js: グラフの描画に使用（Chart.js v4.4.4）
- その他の依存関係: 詳細は`package.json`を参照

# ディレクトリ構成
プロジェクト全体のディレクトリ構成は以下のようになります。

```sh
.
├── node_modules/            # プロジェクトの依存パッケージ（.gitignore対象）
├── server/
│   ├── .env                 # 環境変数の設定ファイル（Snowflakeとの接続に必要）
│   ├── server.js            # Expressのサーバー設定
│   └── snowflakeClient.js   # Snowflakeとの接続を行うクライアント
├── src/
│   ├── components/
│   │   └── TemperatureChart.jsx  # 温度データをグラフ化するReactコンポーネント
│   ├── App.css              # アプリ全体のスタイル
│   ├── App.jsx              # アプリのメインコンポーネント
│   ├── index.css            # グローバルスタイル
│   └── index.js             # Reactアプリのエントリーポイント
├── .gitignore               # Gitで無視するファイルを指定
├── package.json             # プロジェクトの依存関係とスクリプト
├── package-lock.json        # 依存パッケージの正確なバージョンを記録
└── snowflake.sql            # 仮想IoTデバイスデータの定義と挿入スクリプト
```

# プロジェクトの全体的な流れ
1. プロジェクトのセットアップ: 依存関係の管理と初期設定を行います。
2. データの準備: `snowflake.sql` を使ってSnowflakeに仮想IoT機器からの温度データを挿入します。
3. 環境設定ファイルの作成: バックエンドがSnowflakeに接続するための環境変数を設定します。
4. Snowflakeクライアントの設定: Snowflakeデータベースに接続するクライアントを設定します。
5. バックエンドサーバーの起動: ExpressとSocket.ioを使用してバックエンドサーバーを起動し、データを提供します。
6. フロントエンドの起動: Reactアプリケーションを起動して、リアルタイムデータを可視化します。

# 実行手順
## 1. プロジェクトのセットアップ
まずはプロジェクトの依存関係をインストールします。プロジェクトのルートディレクトリに移動し、以下のコマンドを実行します。

```sh
npm install
```

このコマンドを実行することで、`package.json` に定義された依存関係をもとに、`node_modules` ディレクトリと `package-lock.json` が生成されます。
- `node_modules` には、プロジェクトで使用するすべての依存パッケージが格納されます。
- `package-lock.json` は、依存関係のバージョンを固定して、インストールするパッケージが同じ環境で一致するように管理するファイルです。

以下が今回使用する `package.json` の内容です。

```package.json
{
  "name": "snowflake-test",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "axios": "^1.7.5",
    "chart.js": "^4.4.4",
    "chartjs-adapter-date-fns": "^3.0.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "easy-logging": "^1.1.1",
    "express": "^4.19.2",
    "react": "^18.3.1",
    "react-chartjs-2": "^5.2.0",
    "react-dom": "^18.3.1",
    "react-scripts": "5.0.1",
    "snowflake-sdk": "^1.12.0",
    "socket.io": "^4.7.5",
    "socket.io-client": "^4.7.5",
    "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"
    ]
  }
}
```

## 2. Snowflakeでデータの準備
Snowflakeにデータベースとテーブルを作成し、仮想の温度データを保存します。

Snowflakeのワークシートにアクセスし、以下の`snowflake.sql` の内容を実行してください。このSQLスクリプトはIoT機器の温度データを持つためのテーブルを作成し、いくつかのサンプルデータを挿入します。

```snowflake.sql
-- Iotデバイスの温度を疑似的に作成するSQL
-- Snowflakeウェブインターフェース上で使用することを想定

CREATE DATABASE iot_data;
USE DATABASE iot_data;

CREATE TABLE IF NOT EXISTS temperature_readings (
    id INTEGER AUTOINCREMENT,
    device_id STRING,
    timestamp TIMESTAMP,
    temperature FLOAT,
    PRIMARY KEY (id)
);

INSERT INTO temperature_readings (device_id, timestamp, temperature) VALUES
    ('device_1', CURRENT_TIMESTAMP, 22.5),
    ('device_2', CURRENT_TIMESTAMP, 19.3),
    ('device_1', TIMESTAMPADD(hour, -1, CURRENT_TIMESTAMP), 21.8),
    ('device_2', TIMESTAMPADD(hour, -1, CURRENT_TIMESTAMP), 20.1),
    ('device_3', TIMESTAMPADD(hour, -1, CURRENT_TIMESTAMP), 28.1),
    ('device_1', TIMESTAMPADD(hour, -2, CURRENT_TIMESTAMP), 22.0),
    ('device_2', TIMESTAMPADD(hour, -2, CURRENT_TIMESTAMP), 19.9),
    ('device_1', TIMESTAMPADD(hour, -3, CURRENT_TIMESTAMP), 21.7),
    ('device_2', TIMESTAMPADD(hour, -3, CURRENT_TIMESTAMP), 20.2),
    ('device_1', TIMESTAMPADD(hour, -4, CURRENT_TIMESTAMP), 22.2),
    ('device_2', TIMESTAMPADD(hour, -4, CURRENT_TIMESTAMP), 19.5),
    ('device_1', TIMESTAMPADD(hour, -5, CURRENT_TIMESTAMP), 21.6),
    ('device_2', TIMESTAMPADD(hour, -5, CURRENT_TIMESTAMP), 20.4),
    ('device_3', TIMESTAMPADD(hour, -5, CURRENT_TIMESTAMP), 22.6),
    
    -- ...
    -- ここに適宜追加のデータを挿入
    -- ...

-- 以下は、データベースを削除する時に使う
-- Drop database iot_data;
```
> 注記: データの挿入部分（`INSERT INTO`）には、必要に応じて追加のデータを挿入してください。

![スクリーンショット 2024-10-06 16.50.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364787/1e0bea1f-dd16-7e73-d06e-fd32799f3f3b.png)


## 3. 環境設定ファイルの作成
バックエンドがSnowflakeに接続するために、環境変数を設定する必要があります。ご自身のSnowflakeアカウント情報を確認し、`/server` ディレクトリに `.env` ファイルを作成し、以下の内容を追加してください。

```.env
SNOWFLAKE_ACCOUNT=<組織-アカウント名>
SNOWFLAKE_USERNAME=<Snowflakeのユーザーネーム>
SNOWFLAKE_PASSWORD=<Snowflakeのパスワード>
SNOWFLAKE_WAREHOUSE=<ウェアハウスの名前>
SNOWFLAKE_DATABASE=<データベース名>
SNOWFLAKE_SCHEMA=<スキーマ名（デフォルトはpublic）>
```
> 注記：この設定をミスると、バックエンドでSnowflakeのデータベースに接続できないため注意してください。

ご自身の**アカウント識別子**（組織名-アカウント名）は以下の手順で確認することが可能です。

https://docs.snowflake.com/ja/user-guide/admin-account-identifier#finding-the-organization-and-account-name-for-an-account

## 4. Snowflakeとの接続設定
`server/snowflakeClient.js` で、Snowflakeデータベースに接続する設定を行います。以下がそのプログラムです。

```snowflakeClient.js
// 環境変数の読み込み
const path = require("path");
const dotenv = require("dotenv");
dotenv.config({ path: path.resolve(__dirname, "./.env") });

// snowflake接続のための設定
const snowflake = require("snowflake-sdk");
const connection = snowflake.createConnection({
  account: process.env.SNOWFLAKE_ACCOUNT,
  username: process.env.SNOWFLAKE_USERNAME,
  password: process.env.SNOWFLAKE_PASSWORD,
  warehouse: process.env.SNOWFLAKE_WAREHOUSE,
  database: process.env.SNOWFLAKE_DATABASE,
  schema: process.env.SNOWFLAKE_SCHEMA,
});
// snowflake.configure({ logLevel: "DEBUG" }); // 詳細なログを取得したい場合に使う

// snowflakeへ接続
connection.connect((err, conn) => {
  if (err) {
    console.error("snowflakeへの接続に失敗しました。: " + err.message);
  } else {
    console.log("snowflakeへの接続に成功しました。");
  }
});

module.exports = connection;
```
ポイント
- dotenv ライブラリを使用して環境変数を読み込み、Snowflakeの接続情報を設定しています。
- `snowflake.createConnection` を使って接続を作成し、接続の成功または失敗のログを出力しています。

## 5. バックエンドサーバーの起動
次に、バックエンドサーバーを起動してSnowflakeと連携し、データをクライアントに提供します。

`/server` ディレクトリに移動し、以下のコマンドを実行します。
```sh
node server.js
```
`server/server.js` では、Expressを使用してHTTPサーバーを立ち上げ、Socket.ioでリアルタイム通信を行います。`snowflakeClient`を通じて温度データを取得し、30秒ごとに最新のデータをクライアントに送信します。

```server.js
const express = require("express");
const cors = require("cors");
const http = require("http");
const { Server } = require("socket.io");
const snowflakeClient = require("./snowflakeClient");

const app = express();
app.use(cors());
const server = http.createServer(app);

const io = new Server(server, {
  cors: {
    origin: "http://localhost:3000", // フロントエンドのオリジンを指定
    methods: ["GET", "POST"], // 許可するHTTPメソッド
  },
});

app.get("/", (_req, res) => {
  res.send("バックエンドサーバーヘルスチェック");
});


const query = `
  SELECT * FROM temperature_readings
  ORDER BY timestamp DESC
  //   LIMIT 10
`;

io.on("connection", (socket) => {
  console.log("クライエントに接続しました。");

  const fetchData = () => {
    snowflakeClient.execute({
      sqlText: query,
      complete: (err, _stmt, rows) => {
        // rows: クエリの実行結果として返された行データの配列
        if (err) {
          console.error("クエリの実行に失敗しました。: " + err.message);
        } else {
          socket.emit("FromAPI", rows);
        }
      },
    });
  };
  fetchData();
  setInterval(fetchData, 30000); // 30秒ごとにデータを更新

  socket.on("disconnect", () => {
    console.log("クライアントとの接続が切れました。");
  });
});

server.listen(4001, () => {
  console.log("バックエンドサーバーが4001ポートで立ち上がりました。");
});
```

## 6. フロントエンドの起動
最後に、フロントエンドのReactアプリケーションを起動してデータを可視化します。
プロジェクトのルートディレクトリで以下のコマンドを実行します。
```sh
npm run start
```
`TemperatureChart.jsx` では、`Chart.js` を使って温度データをグラフとして描画しています。データは`Socket.io`を通じてリアルタイムでバックエンドから取得され、Reactの`useState`と`useEffect`フックを使って画面に表示されます。

```TemperatureChart.jsx
import React from "react";
import { Line } from "react-chartjs-2";
import "chart.js/auto";

const TemperatureChart = ({ data }) => {
  if (!data || data.length === 0) {
    return <div>データがありません</div>;
  }

  // タイムスタンプをISO 8601形式に変換
  const parsedData = data.map((d) => ({
    ...d,
    TIMESTAMP: new Date(d.TIMESTAMP.replace(" ", "T")), // " "を"T"に置き換えてISO形式に変換
  }));

  // データを昇順にソート（最も古いタイムスタンプから新しいタイムスタンプへ）
  const sortedData = parsedData.sort((a, b) => new Date(a.TIMESTAMP) - new Date(b.TIMESTAMP));

  /*
  DEVICE_IDごとにデータをグループ化
  ・acc（アキュムレータ）: これまでの累積された結果を保持するオブジェクト。
  ・current（現在の要素）: sortedData配列の現在処理中の要素。
  */
  const groupedData = sortedData.reduce((acc, current) => {
    const { DEVICE_ID } = current; // currentオブジェクトからDEVICE_IDを抽出
    if (!acc[DEVICE_ID]) { // accオブジェクトにDEVICE_IDのキーが存在しない場合
      acc[DEVICE_ID] = []; // 新しい配列を作成して、そのキーに割り当てる
    }
    acc[DEVICE_ID].push(current); // DEVICE_IDのキーの配列にcurrentを追加
    return acc; // 更新されたaccを返す
  }, {});

  // デバイスごとの色を定義
  const deviceColors = {
    device_1: "rgba(75, 192, 192, 1)",
    device_2: "rgba(255, 99, 132, 1)",
    device_3: "rgba(54, 162, 235, 1)",
    device_4: "rgba(255, 206, 86, 1)",
    device_5: "rgba(153, 102, 255, 1)",
    device_6: "rgba(255, 159, 64, 1)",
    device_7: "rgba(199, 199, 199, 1)",
    device_8: "rgba(83, 102, 255, 1)",
    device_9: "rgba(99, 255, 132, 1)",
    device_10: "rgba(235, 64, 52, 1)",
    // 追加のデバイスがある場合はさらに下に追加
  };

  // デバイスごとにデータセットを作成
  const datasets = Object.keys(groupedData).map((deviceId) => {
    const color = deviceColors[deviceId] || "rgba(0, 0, 0, 1)"; // デバイスごとの色を使用、ない場合は黒をデフォルトに
    return {
      label: deviceId,
      data: groupedData[deviceId].map((d) => ({ x: d.TIMESTAMP, y: d.TEMPERATURE })), // オブジェクト形式で{x, y}を渡す
      // fill: true,
      backgroundColor: color.replace(", 1)", ", 0.2)"), // 透明度を追加
      borderColor: color,
    };
  });

  const chartData = {
    datasets: datasets,
  };

  // 最も古いタイムスタンプと最新のタイムスタンプを取得
  const minTimestamp = new Date(sortedData[0].TIMESTAMP);
  const maxTimestamp = new Date(sortedData[sortedData.length - 1].TIMESTAMP);

  /// 温度の最小値と最大値を取得してy軸の範囲を設定
  const minTemperature = Math.min(...sortedData.map((d) => d.TEMPERATURE));
  const maxTemperature = Math.max(...sortedData.map((d) => d.TEMPERATURE));

  const options = {
    responsive: true,
    maintainAspectRatio: false, // グラフのアスペクト比を固定しない
    scales: {
      x: {
        type: "time",
        time: {
          unit: "hour", // データポイントを時間単位で表示
          tooltipFormat: "MM/dd/yyyy, HH:mm", // ツールチップに表示するフォーマット
          displayFormats: {
            hour: "MM/dd HH:mm", // 横軸に表示するフォーマット
          },
        },
        title: {
          display: true,
          text: "日時", // y軸のタイトル
        },
        min: minTimestamp, // 横軸の最小値
        max: maxTimestamp, // 横軸の最大値
      },
      y: {
        min: minTemperature - 1, // 温度の最小値を少し下げて余裕を持たせる
        max: maxTemperature + 1, // 温度の最大値を少し上げて余裕を持たせる
        title: {
          display: true,
          text: "温度 (°C)", // y軸のタイトル
        },
      },
    },
  };

  return (
    <div style={{ overflowX: "auto", width: "100%" }}>
      <div style={{ minWidth: "600px", height: "500px" }}>
        {/* 固定幅を設定して、スクロール可能に */}
        <Line data={chartData} options={options} />
      </div>
    </div>
  );
};

export default TemperatureChart;
```

`App.jsx` では、Socket.ioを使用してバックエンドからリアルタイムでデータを受信し、そのデータを`TemperatureChart`コンポーネントに渡してグラフを描画します。
```App.jsx
import "./App.css";
import React, { useEffect, useState } from "react";
import { io } from "socket.io-client";
import TemperatureChart from "./components/TemperatureChart";

const ENDPOINT = "http://localhost:4001";

function App() {
  const [data, setData] = useState([]);
  useEffect(() => {
    const socket = io(ENDPOINT);
    socket.on("FromAPI", (data) => {
      setData(data);
    });
    return () => socket.disconnect();
  }, []);

  return (
    <div className='App'>
      <h1>IoT機器の温度一覧</h1>
      <TemperatureChart data={data} />
    </div>
  );
}

export default App;
```

# 可視化
こんな感じで、Snowflake上に格納されている時系列データをフロントで可視化することができました。

![361614879-e7315a3d-e728-4346-8f47-03224c853609.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3364787/54e63108-ba98-dd23-d88b-13b7314519e3.png)


# まとめ
本記事では、Snowflake上の仮想IoTデバイスの温度データを、リアルタイムでReactフロントエンドで可視化するまでの一連の流れを簡単な例を用いて紹介しました。Snowflakeとの連携、バックエンドサーバー構築、リアルタイム通信、そしてフロントエンドでのデータ可視化と、どのようなフローで実行できるのか興味のある方はぜひコードを動かしてみてください。

私のGitHubには、より詳細なプログラムを公開しておりますので、よければこちらもぜひご参照ください！:sunny:

https://github.com/shwg8986/Snowflake-react/tree/main