GraphQLとPostgresを使用したリアルタイムチャートの構築
公開: 2022-03-10チャートは、データを扱うあらゆる業界の不可欠な部分を形成します。 チャートは、投票およびポーリング業界で役立ちます。また、チャートは、使用するユーザーとクライアントのさまざまな動作と特性をよりよく理解するのに役立ちます。
なぜリアルタイムチャートがそれほど重要なのですか? そうですね、新しいデータが継続的に生成される場合に役立ちます。 たとえば、株価を視覚化するためにライブタイムシリーズを使用する場合は、リアルタイムチャートに最適です。 このチュートリアルでは、まさにこの特定のタスクに適したオープンソーステクノロジーを使用してリアルタイムチャートを作成する方法を説明します。
注:このチュートリアルには、ReactとGraphQLの基本的な知識が必要です。
スタック
- PostgreSQL
チャートを使用する背後にあるまさにポイントは、「巨大な」ボリュームデータを視覚化することです。 したがって、大きなデータを効率的に処理し、それを再構築するための直感的なAPIを提供するデータベースが必要です。 SQLデータベースを使用すると、データを抽象化して集約するビューを作成できます。 実績のある非常に効率的なデータベースであるPostgresを使用します。 また、TimescaleやPostGISなどの優れたオープンソース拡張機能があり、ジオロケーションベースのチャートと時系列ベースのチャートをそれぞれ作成できます。 時系列チャートの作成にはタイムスケールを使用します。 - GraphQLエンジン
この投稿はリアルタイムチャートの作成に関するものであり、GraphQLにはリアルタイムサブスクリプションの明確に定義された仕様が付属しています。 Hasura GraphQL Engineは、Postgres接続を取得し、リアルタイムGraphQLを介してPostgresデータをクエリできるオープンソースのGraphQLサーバーです。 また、カスタムアクセス制御ルールに基づいてデータを制限するのに役立つアクセス制御レイヤーが付属しています。 - ChartJS
ChartJSは、JavaScriptを使用してチャートを作成するための、人気があり、手入れの行き届いたオープンソースライブラリです。chart.js
とそのReactJS抽象化react-chartjs-2
を使用します。 Reactの理由については、Reactが直感的なイベント駆動型APIを開発者に提供するためです。 また、Reactの一方向のデータフローは、データ駆動型のグラフを作成するのに理想的です。
要件
このチュートリアルでは、システムに次のものが必要です。
- Docker CE
Dockerは、アプリケーションをコンテナー化できるソフトウェアです。 Dockerイメージは、ソフトウェアとその依存関係および最小限のオペレーティングシステムを含む独立したパケットです。 このようなDockerイメージは、Dockerがインストールされている任意のマシンで技術的に実行できます。 このチュートリアルにはdockerが必要です。- Dockerについてもっと読む
- Dockerをインストールする
- npm:npmはJavaScriptのパッケージ管理です。
デモ
現時点から過去20分間のある場所の最高気温を5秒間隔で示す次のライブ時系列チャートを作成します。
バックエンドの設定
サービスの実行
バックエンドは、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つのサービスの仕様が含まれています。
-
timescale
これは、Timescale拡張機能がインストールされたPostgresデータベースです。 ポート5432で実行するように構成されています。 -
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コンソールにアクセスできます。
データベースの設定
次に、さまざまな時間の温度の値を格納する温度というテーブルを作成しましょう。 コンソールの[データ]タブに移動し、[ 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
で開きます。
クライアント側の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クライアントを使用できるように、 App
をApolloProvider
内にラップします。 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
に戻って、グラフが更新されるのを確認できます。
仕上げ
上記で説明したアイデアを使用して、ほとんどのリアルタイムチャートを作成できます。 アルゴリズムは次のとおりです。
- Postgresを使用してGraphQLエンジンをデプロイします。
- データを保存するテーブルを作成します。
- Reactアプリからこれらのテーブルをサブスクライブします。
- チャートをレンダリングします。
ソースコードはここにあります。