使用 GraphQL 和 Postgres 构建实时图表
已发表: 2022-03-10图表是任何处理数据的行业不可或缺的一部分。 图表在投票和民意调查行业中很有用,它们还可以帮助我们更好地了解与我们合作的用户和客户的不同行为和特征。
为什么实时图表如此重要? 好吧,它们在不断产生新数据的情况下很有用; 例如,当使用实时序列来可视化股票价格时,实时图表非常有用。 在本教程中,我将解释如何使用适合此特定任务的开源技术构建实时图表。
注意:本教程需要 React 和 GraphQL 的基本知识。
堆
- PostgreSQL
使用图表背后的关键是可视化“大量”数据。 因此,我们需要一个能够有效处理大数据并提供直观 API 来重构数据的数据库。 SQL 数据库允许我们为我们创建抽象和聚合数据的视图。 我们将使用 Postgres,这是一个经过时间考验的高效数据库。 它还具有花哨的开源扩展,例如 Timescale 和 PostGIS,它们允许我们分别构建基于地理位置和基于时间序列的图表。 我们将使用 Timescale 来构建我们的时间序列图表。 - GraphQL 引擎
这篇文章是关于构建实时图表的,GraphQL 带有一个定义明确的实时订阅规范。 Hasura GraphQL Engine 是一个开源 GraphQL 服务器,它采用 Postgres 连接并允许您通过实时 GraphQL 查询 Postgres 数据。 它还带有访问控制层,可帮助您根据自定义访问控制规则限制数据。 - ChartJS
ChartJS 是一个流行且维护良好的开源库,用于使用 JavaScript 构建图表。 我们将使用chart.js
及其 ReactJS 抽象react-chartjs-2
。 关于为什么使用 React,是因为 React 为开发人员提供了直观的事件驱动 API。 此外,React 的单向数据流非常适合构建数据驱动的图表。
要求
对于本教程,您的系统上将需要以下内容:
- 码头工人 CE
Docker 是一种软件,可让您将应用程序容器化。 docker 镜像是一个独立的数据包,其中包含软件及其依赖项和简约的操作系统。 这样的 docker 镜像在技术上可以在任何安装了 docker 的机器上运行。 本教程将需要 docker。- 阅读有关 Docker 的更多信息
- 安装 Docker
- npm:npm 是 JavaScript 的包管理器。
演示
我们将构建以下实时时间序列图表,显示从现在开始的过去 20 分钟内以 5 秒为间隔的位置的最高温度。
设置后端
运行服务
后端由 Postgres 数据库、其时间尺度扩展和 Hasura GraphQL 引擎组成。 让我们通过运行各自的 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
包含两个服务的规范:
-
timescale
这是我们安装了 Timescale 扩展的 Postgres 数据库。 它被配置为在端口 5432 上运行。 -
graphql-engine
这是我们的 Hasura GraphQL Engine 实例,即指向数据库并在其上提供 GraphQL API 的 GraphQL 服务器。 它被配置为运行在 8080 端口,并且 8080 端口映射到运行这个 docker 容器的机器的 8080 端口。 这意味着你可以通过机器的localhost:8080
访问这个 GraphQL 服务器。
让我们通过在放置docker-compose.yaml
。
docker-compose up -d
此命令从云中提取 docker 映像并按给定顺序运行它们。 根据您的网速,可能需要几秒钟。 完成后,您可以通过https://localhost:8080/console
访问您的 GraphQL 引擎控制台。
设置数据库
接下来,让我们创建一个名为 temperature 的表,用于存储不同时间的温度值。 转到控制台中的数据选项卡,然后转到SQL
部分。 通过运行这个 SQL 块来创建我们的temperature
:
CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );
这将在数据库中创建一个简单的 Postgres 表。 但我们希望利用 Timescale 扩展的时间间隔分区。 为此,我们必须通过运行 SQL 命令将此表转换为 timescale 的超表:
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
( max_temp)
分组在 5 秒窗口中。 二级分组是使用location
字段完成的。 所有这些数据都来自现在的过去二十分钟。
而已。 我们的后台设置好了。 现在让我们构建一个漂亮的实时图表。
前端
你好 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
中插入另一个值。 插入后,如果您返回订阅所在的选项卡,您会看到响应已自动更新。 这就是 GraphQL 引擎提供的实时魔法。 让我们使用此订阅来支持我们的实时图表。
Create-React-App 入门
让我们使用 create react app 快速开始使用 React app starter。 运行命令:
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 兼容的服务器一起使用。 Relay Modern 也很好,但服务器必须支持 Relay 规范才能利用 Relay Modern 的所有优势。 在本教程中,我们将使用 Apollo 客户端作为客户端 GraphQL。 让我们执行设置以向应用程序提供 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 });
最后,将App
包装在ApolloProvider
中,以便我们可以在子组件中使用 Apollo 客户端。 您的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
并将其作为数据 prop 传递给上述对象。 渲染方法看起来像:
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>
组件使用 render prop 模式工作,其中组件的子级使用订阅数据的上下文进行渲染。
<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
组件来订阅我们的视图,然后将订阅数据转换为 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 期望的形式。
在 Subscription 组件中使用它之后,我们的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 中那些花哨的动画。 简单的时间序列看起来很漂亮,但是,如果您愿意,可以删除 options 属性。
插入样本数据
让我们编写一个脚本,用虚拟数据填充我们的数据库。 创建一个单独的目录(在这个应用程序之外)并创建一个名为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 );
现在运行这两个命令:
npm install --save node-fetch node script.js
您可以返回https://localhost:3000
并查看图表更新。
整理起来
您可以使用我们上面讨论的想法构建大多数实时图表。 算法是:
- 使用 Postgres 部署 GraphQL 引擎;
- 创建要存储数据的表;
- 从你的 React 应用订阅这些表;
- 渲染图表。
你可以在这里找到源代码。