GraphQLとPostgresを使用したリアルタイムチャートの構築

公開: 2022-03-10
簡単な要約↬チャートや図でデータを視覚化する以外に、データを理解するためのより良い方法はありません。 JSコミュニティには、データの視覚化を容易にする優れたオープンソースプロジェクトがいくつかありますが、これらのグラフをバックアップしてリアルタイムにすることができるリアルタイムバックエンドを構築するための頼りになるソリューションはありませんでした。 GraphQL(リアルタイムサブスクリプションの仕様が明確に定義されています)を使用すると、リアルタイムバックエンドを数秒で実行し、それを使用してリアルタイムチャートを強化できます。

チャートは、データを扱うあらゆる業界の不可欠な部分を形成します。 チャートは、投票およびポーリング業界で役立ちます。また、チャートは、使用するユーザーとクライアントのさまざまな動作と特性をよりよく理解するのに役立ちます。

なぜリアルタイムチャートがそれほど重要なのですか? そうですね、新しいデータが継続的に生成される場合に役立ちます。 たとえば、株価を視覚化するためにライブタイムシリーズを使用する場合は、リアルタイムチャートに最適です。 このチュートリアルでは、まさにこの特定のタスクに適したオープンソーステクノロジーを使用してリアルタイムチャートを作成する方法を説明します。

このチュートリアルには、ReactとGraphQLの基本的な知識が必要です。

スタック

  1. PostgreSQL
    チャートを使用する背後にあるまさにポイントは、「巨大な」ボリュームデータを視覚化することです。 したがって、大きなデータを効率的に処理し、それを再構築するための直感的なAPIを提供するデータベースが必要です。 SQLデータベースを使用すると、データを抽象化して集約するビューを作成できます。 実績のある非常に効率的なデータベースであるPostgresを使用します。 また、TimescaleやPostGISなどの優れたオープンソース拡張機能があり、ジオロケーションベースのチャートと時系列ベースのチャートをそれぞれ作成できます。 時系列チャートの作成にはタイムスケールを使用します。
  2. GraphQLエンジン
    この投稿はリアルタイムチャートの作成に関するものであり、GraphQLにはリアルタイムサブスクリプションの明確に定義された仕様が付属しています。 Hasura GraphQL Engineは、Postgres接続を取得し、リアルタイムGraphQLを介してPostgresデータをクエリできるオープンソースのGraphQLサーバーです。 また、カスタムアクセス制御ルールに基づいてデータを制限するのに役立つアクセス制御レイヤーが付属しています。
  3. ChartJS
    ChartJSは、JavaScriptを使用してチャートを作成するための、人気があり、手入れの行き届いたオープンソースライブラリです。 chart.jsとそのReactJS抽象化react-chartjs-2を使用します。 Reactの理由については、Reactが直感的なイベント駆動型APIを開発者に提供するためです。 また、Reactの一方向のデータフローは、データ駆動型のグラフを作成するのに理想的です。
ジャンプした後もっと! 以下を読み続けてください↓

要件

このチュートリアルでは、システムに次のものが必要です。

  1. Docker CE
    Dockerは、アプリケーションをコンテナー化できるソフトウェアです。 Dockerイメージは、ソフトウェアとその依存関係および最小限のオペレーティングシステムを含む独立したパケットです。 このようなDockerイメージは、Dockerがインストールされている任意のマシンで技術的に実行できます。 このチュートリアルにはdockerが必要です。
    • Dockerについてもっと読む
    • Dockerをインストールする
  2. npm:npmはJavaScriptのパッケージ管理です。

デモ

現時点から過去20分間のある場所の最高気温を5秒間隔で示す次のライブ時系列チャートを作成します。

リアルタイムチャートのGIFデモ
リアルタイムチャートのGIFデモ

バックエンドの設定

サービスの実行

バックエンドは、Postgresデータベース、そのタイムスケール拡張、およびHasuraGraphQLエンジンで構成されています。 それぞれのDockerイメージを実行して、データベースとGraphQLサーバーを実行してみましょう。 docker-compose.yamlというファイルを作成し、このコンテンツを貼り付けます。

docker-composeは、複数のdockerイメージを宣言的に実行するためのユーティリティです。

 version: '2' services: timescale: image: timescale/timescaledb:latest-pg10 restart: always environment: POSTGRES_PASSWORD: postgrespassword volumes: - db_data:/var/lib/postgresql/data graphql-engine: image: hasura/graphql-engine:v1.0.0-alpha38 ports: - "8080:8080" depends_on: - "timescale" restart: always environment: HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@timescale:5432/postgres HASURA_GRAPHQL_ACCESS_KEY: mylongsecretkey command: - graphql-engine - serve - --enable-console volumes: db_data:

このdocker-compose.yamlには、次の2つのサービスの仕様が含まれています。

  1. timescale
    これは、Timescale拡張機能がインストールされたPostgresデータベースです。 ポート5432で実行するように構成されています。
  2. graphql-engine
    これは、Hasura GraphQL Engineインスタンスです。つまり、データベースをポイントし、その上にGraphQLAPIを提供するGraphQLサーバーです。 ポート8080で実行するように構成されており、ポート8080は、このDockerコンテナーが実行されているマシンのポート8080にマップされています。 これは、マシンのlocalhost:8080からこのGraphQLサーバーにアクセスできることを意味します。

docker-compose.yamlを配置した場所で次のコマンドを実行して、これらのdockerコンテナーを実行してみましょう。

 docker-compose up -d

このコマンドは、クラウドからDockerイメージをプルし、指定された順序で実行します。 インターネットの速度によっては、数秒かかる場合があります。 完了すると、 https://localhost:8080/consoleでGraphQLEngineコンソールにアクセスできます。

HasuraGraphQLエンジンコンソール
Hasura GraphQLエンジンコンソール(大プレビュー)

データベースの設定

次に、さまざまな時間の温度の値を格納する温度というテーブルを作成しましょう。 コンソールの[データ]タブに移動し、[ SQL ]セクションに移動します。 次のSQLブロックを実行して、 temperatureテーブルを作成します。

 CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );

これにより、データベースに単純なPostgresテーブルが作成されます。 ただし、Timescale拡張機能の時間間隔分割を活用したいと考えています。 これを行うには、SQLコマンドを実行して、このテーブルをタイムスケールのハイパーテーブルに変換する必要があります。

 SELECT create_hypertable('temperature', 'recorded_at');

このコマンドは、 recorded_atフィールドに時間で分割されたハイパーテーブルを作成します。

これで、このテーブルが作成されたので、直接GraphQLクエリの作成を開始できます。 上部のGraphiQLタブをクリックして、それらを試すことができます。 最初に突然変異を作ってみてください:

 mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } }

上記のGraphQLミューテーションは、 temperatureテーブルに行を挿入します。 次に、GraphQLクエリを作成して、データが挿入されたかどうかを確認します。

次に、クエリを作成してみてください。

 query { temperature { recorded_at temperature location } }

それがうまくいったことを願っています:)

さて、私たちの手元のタスクは、現在の瞬間から過去20分間の5秒間隔で場所の最高気温を示すライブ時系列チャートを作成することです。 このデータを正確に提供するビューを作成しましょう。

 CREATE VIEW last_20_min_temp AS ( SELECT time_bucket('5 seconds', recorded_at) AS five_sec_interval, location, MAX(temperature) AS max_temp FROM temperature WHERE recorded_at > NOW() - interval '20 minutes' GROUP BY five_sec_interval, location ORDER BY five_sec_interval ASC );

このビューは、 temperatureテーブルのデータを5秒ウィンドウに最大温度( max_temp)でグループ化します。 二次グループ化は、 locationフィールドを使用して行われます。 このデータはすべて、現時点から過去20分間のものです。

それでおしまい。 バックエンドが設定されています。 さて、素敵なリアルタイムチャートを作成しましょう。

フロントエンド

こんにちはGraphQLサブスクリプション

GraphQLサブスクリプションは、本質的に「ライブ」GraphQLクエリです。 これらはWebSocket上で動作し、GraphQLクエリとまったく同じ応答構造を持っています。 https://localhost:8080/consoleに戻り、作成したビューへのGraphQLサブスクリプションを作成してみてください。

 subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } }

このサブスクリプションは、場所がLondonであるビューのデータをサブスクライブし、 five_second_intervalsの昇順で並べ替えられます。

当然、過去20分間にデータベースに何も挿入していないため、ビューからの応答は空の配列になります。 (20分以内にこのセクションに到達すると、いつか挿入したエントリが表示される場合があります。)

 { "data": { "last_20_min_temp": [] } }

このサブスクリプションをオンのままにして、別のタブを開き、以前に実行したのと同じミューテーションを使用して、 temperaturesテーブルに別の値を挿入してみてください。 挿入後、サブスクリプションがオンになっていたタブに戻ると、応答が自動的に更新されていることがわかります。 これが、GraphQLEngineが提供するリアルタイムの魔法です。 このサブスクリプションを使用して、リアルタイムチャートを強化しましょう。

Create-React-App入門

createreactアプリを使用してReactアプリスターターをすぐに始めましょう。 次のコマンドを実行します。

 npx create-react-app time-series-chart

これにより、空のスタータープロジェクトが作成されます。 それにcdして、GraphQLとチャートライブラリをインストールします。 また、タイムスタンプを人間が読める形式に変換するためのモーメントをインストールします。

 cd time-series-chart npm install --save apollo-boost apollo-link-ws subscriptions-transport-ws graphql react-apollo chart.js react-chartjs-2 moment

最後に、 npm startを使用してアプリを実行すると、基本的なReactアプリがhttps://localhost:3000で開きます。

生のcreate-react-app
生のcreat-react-app(大プレビュー)

クライアント側のGraphQL用のApolloクライアントのセットアップ

Apolloクライアントは現在、GraphQL準拠のサーバーで動作する最高のGraphQLクライアントです。 リレーモダンも優れていますが、サーバーはリレーモダンのすべての利点を活用するためにリレー仕様をサポートする必要があります。 このチュートリアルでは、クライアント側のGraphQLにApolloクライアントを使用します。 アプリにApolloクライアントを提供するためのセットアップを実行してみましょう。

次のコードスニペットはドキュメントから直接取得されているため、この設定の微妙な点については説明していません。 Reactアプリディレクトリのsrc/index.jsに移動し、Apolloクライアントをインスタンス化して、このコードスニペットをReactDOM.renderの上に追加します。

 import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache });

最後に、子コンポーネントでApolloクライアントを使用できるように、 AppApolloProvider内にラップします。 App.jsは最終的に次のようになります。

 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { WebSocketLink } from 'apollo-link-ws'; import { ApolloClient } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create a WebSocket link: const link = new WebSocketLink({ uri: `ws://localhost:8080/v1alpha1/graphql`, options: { reconnect: true, connectionParams: { headers: { "x-hasura-admin-secret: "mylongsecretkey" } } } }) const cache = new InMemoryCache(); const client = new ApolloClient({ link, cache }); ReactDOM.render( ( <ApolloProvider client={client}> <App /> </ApolloProvider> ), document.getElementById('root') );

Apolloクライアントがセットアップされました。 アプリからリアルタイムのGraphQLを簡単に使用できるようになりました。 src/App.jsに移動します。

チャートの作成

ChartJSは、チャートを作成するための非常に優れたAPIを提供します。 折れ線グラフを作成します。 したがって、折れ線グラフは次の形式のデータを想定しています。

 { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], }

上記のデータセットを折れ線グラフのレンダリングに使用すると、次のようになります。

サンプル折れ線グラフ
サンプル折れ線グラフ(大きなプレビュー)

最初にこのサンプルチャートを作成してみましょう。 react-chartjs-2からLineをインポートし、上記のオブジェクトをデータプロパティとして渡してレンダリングします。 renderメソッドは次のようになります。

 render() { const data = { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], } return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Line data={data} /> </div> ); }

次に、ビュー内のデータをサブスクライブして、折れ線グラフにフィードします。 しかし、クライアントでサブスクリプションを実行するにはどうすればよいでしょうか。

Apolloの<Subscription>コンポーネントは、コンポーネントの子がサブスクリプションデータのコンテキストでレンダリングされるレンダリングプロップパターンを使用して機能します。

 <Subscription subscription={gql`subscription { parent { child } }`} /> { ({data, error, loading}) => { if (error) return <Error error={error} />; if (loading) return <Loading />; return <RenderData data={data} />; } } </Subscription>

そのようなSubscriptionコンポーネントの1つを使用してビューをサブスクライブし、サブスクリプションデータをChartJSが期待する構造に変換してみましょう。 変換ロジックは次のようになります。

 let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); })

オープンソースライブラリgraphq2chartjsを使用して、GraphQL応答からChartJSが期待する形式にデータを変換することもできます。

サブスクリプションコンポーネント内でこれを使用した後、 App.jsは次のようになります。

 import React, { Component } from 'react'; import { Line } from 'react-chartjs-2'; import { Subscription } from 'react-apollo'; import gql from 'graphql-tag'; import moment from 'moment'; const TWENTY_MIN_TEMP_SUBSCRIPTION= gql' subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } } ' class App extends Component { render() { return ( <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '20px'}} > <Subscription subscription={TWENTY_MIN_TEMP_SUBSCRIPTION}> { ({data, error, loading}) => { if (error) { console.error(error); return "Error"; } if (loading) { return "Loading"; } let chartJSData = { labels: [], datasets: [{ label: "Max temperature every five seconds", data: [], pointBackgroundColor: [], borderColor: 'brown', fill: false }] }; data.last_20_min_temp.forEach((item) => { const humanReadableTime = moment(item.five_sec_interval).format('LTS'); chartJSData.labels.push(humanReadableTime); chartJSData.datasets[0].data.push(item.max_temp); chartJSData.datasets[0].pointBackgroundColor.push('brown'); }) return ( <Line data={chartJSData} options={{ animation: {duration: 0}, scales: { yAxes: [{ticks: { min: 5, max: 20 }}]} }} /> ); } } </Subscription> </div> ); } } export default App;

https://localhost:3000で、完全に機能するリアルタイムチャートを用意できます。 ただし、空になるので、実際に魔法が発生するのを確認できるように、サンプルデータを入力してみましょう。

ChartJSの派手なアニメーションが気に入らないため、折れ線グラフにいくつかのオプションを追加しました。 時系列は単純な場合は見栄えがしますが、必要に応じてオプションの小道具を削除できます。

サンプルデータの挿入

データベースにダミーデータを入力するスクリプトを作成しましょう。 別のディレクトリ(このアプリの外部)を作成し、次の内容のscript.jsというファイルを作成します。

 const fetch = require('node-fetch'); setInterval( () => { const randomTemp = (Math.random() * 5) + 10; fetch( `https://localhost:8080/v1alpha1/graphql`, { method: 'POST', body: JSON.stringify({ query: ` mutation ($temp: numeric) { insert_temperature ( objects: [{ temperature: $temp location: "London" }] ) { returning { recorded_at temperature } } } `, variables: { temp: randomTemp } }) } ).then((resp) => resp.json().then((respObj) => console.log(JSON.stringify(respObj, null, 2)))); }, 2000 );

次に、次の2つのコマンドを実行します。

 npm install --save node-fetch node script.js

https://localhost:3000に戻って、グラフが更新されるのを確認できます。

仕上げ

上記で説明したアイデアを使用して、ほとんどのリアルタイムチャートを作成できます。 アルゴリズムは次のとおりです。

  1. Postgresを使用してGraphQLエンジンをデプロイします。
  2. データを保存するテーブルを作成します。
  3. Reactアプリからこれらのテーブルをサブスクライブします。
  4. チャートをレンダリングします。

ソースコードはここにあります。