Criando gráficos em tempo real com GraphQL e Postgres
Publicados: 2022-03-10Os gráficos são parte integrante de qualquer setor que lida com dados. Os gráficos são úteis no setor de votação e pesquisas, e também são ótimos para nos ajudar a entender melhor os diferentes comportamentos e características dos usuários e clientes com quem trabalhamos.
Por que os gráficos em tempo real são tão importantes? Bem, eles são úteis nos casos em que novos dados são produzidos continuamente; por exemplo, ao usar séries em tempo real para visualizar os preços das ações, é um ótimo uso para gráficos em tempo real. Neste tutorial, explicarei como construir gráficos em tempo real com tecnologias de código aberto aptas exatamente para essa tarefa específica.
Nota : Este tutorial requer conhecimento básico de React e GraphQL.
Pilha
- PostgreSQL
O ponto por trás do uso de gráficos é visualizar dados de volumes “enormes”. Precisamos, portanto, de um banco de dados que lide com dados grandes com eficiência e forneça uma API intuitiva para reestruturá-los. Bancos de dados SQL nos permitem fazer visualizações que abstraem e agregam dados para nós. Usaremos o Postgres, que é um banco de dados altamente eficiente e testado pelo tempo. Ele também possui extensões sofisticadas de código aberto, como Timescale e PostGIS, que nos permitem construir gráficos baseados em geolocalização e baseados em séries temporais, respectivamente. Usaremos a escala de tempo para construir nosso gráfico de séries temporais. - Mecanismo GraphQL
Este post é sobre a construção de gráficos em tempo real, e o GraphQL vem com uma especificação bem definida para assinaturas em tempo real. Hasura GraphQL Engine é um servidor GraphQL de código aberto que usa uma conexão Postgres e permite consultar os dados do Postgres em GraphQL em tempo real. Ele também vem com uma camada de controle de acesso que ajuda a restringir seus dados com base em regras de controle de acesso personalizadas. - ChartJS
ChartJS é uma biblioteca de código aberto popular e bem mantida para construir gráficos com JavaScript. Usaremoschart.js
junto com sua abstração ReactJSreact-chartjs-2
. Sobre o porquê do React, é porque o React capacita os desenvolvedores com uma API intuitiva orientada a eventos. Além disso, o fluxo de dados unidirecional do React é ideal para construir gráficos orientados a dados.
Requisitos
Para este tutorial, você precisará do seguinte em seu sistema:
- Docker CE
Docker é um software que permite que você conteinerize seus aplicativos. Uma imagem docker é um pacote independente que contém software junto com suas dependências e um sistema operacional minimalista. Essas imagens do docker podem ser executadas tecnicamente em qualquer máquina que tenha o docker instalado. Você precisará do docker para este tutorial.- Leia mais sobre Docker
- Instalar o Docker
- npm: npm é o gerenciador de pacotes para JavaScript.
Demonstração
Construiremos o seguinte gráfico de séries temporais ao vivo que mostra a temperatura máxima de um local em intervalos de 5 segundos nos últimos 20 minutos a partir do momento atual.

Configurando o back-end
Executando os serviços
O backend é composto por um banco de dados Postgres, sua extensão de escala de tempo e Hasura GraphQL Engine. Vamos executar o banco de dados e nosso servidor GraphQL executando as respectivas imagens do docker. Crie um arquivo chamado docker-compose.yaml
e cole esse conteúdo nele.
Nota : docker-compose
é um utilitário para executar várias imagens docker de forma declarativa.
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:
Este docker-compose.yaml
contém a especificação de dois serviços:
-
timescale
Este é o nosso banco de dados Postgres com a extensão Timescale instalada. Ele está configurado para ser executado na porta 5432. -
graphql-engine
Esta é a nossa instância Hasura GraphQL Engine, ou seja, o servidor GraphQL que aponta para o banco de dados e fornece APIs GraphQL sobre ele. Ele está configurado para ser executado na porta 8080 e a porta 8080 é mapeada para a porta 8080 da máquina na qual esse contêiner docker é executado. Isso significa que você pode acessar este servidor GraphQL pelolocalhost:8080
da máquina.
Vamos executar esses contêineres docker executando o comando a seguir onde quer que você tenha colocado seu docker-compose.yaml
.
docker-compose up -d
Esse comando extrai as imagens do docker da nuvem e as executa na ordem especificada. Pode levar alguns segundos com base na velocidade da sua internet. Depois de concluído, você pode acessar o console do GraphQL Engine em https://localhost:8080/console
.

Configurando o banco de dados
Em seguida, vamos criar uma tabela chamada temperatura que armazena os valores das temperaturas em diferentes momentos. Vá para a guia Dados no console e vá para a seção SQL
. Crie nossa tabela de temperature
executando este bloco SQL:
CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );
Isso cria uma tabela Postgres simples no banco de dados. Mas desejamos aproveitar o particionamento de intervalo de tempo da extensão Timescale. Para isso, devemos converter esta tabela em hipertabela de escala de tempo executando o comando SQL:
SELECT create_hypertable('temperature', 'recorded_at');
Este comando cria uma hipertabela que é particionada por tempo no campo recorded_at
.
Agora, uma vez que esta tabela foi criada, podemos começar diretamente a fazer consultas GraphQL sobre ela. Você pode experimentá-los clicando na guia GraphiQL
na parte superior. Tente fazer uma mutação primeiro:
mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } }
A mutação GraphQL acima insere uma linha na tabela de temperature
. Agora tente fazer uma consulta GraphQL para verificar se os dados foram inseridos.
Então tente fazer uma consulta:
query { temperature { recorded_at temperature location } }
Espero que tenha funcionado :)
Agora, nossa tarefa é criar um gráfico de série temporal ao vivo que mostre a temperatura máxima de um local em intervalos de 5 segundos nos últimos 20 minutos a partir do momento atual. Vamos criar uma visão que nos dê exatamente esses dados.
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 );
Esta visualização agrupa os dados da tabela de temperature
em janelas de 5 segundos com sua temperatura máxima ( max_temp)
. O agrupamento secundário é feito usando o campo de location
. Todos esses dados são apenas dos últimos vinte minutos do momento presente.
É isso. Nosso back-end está configurado. Vamos agora construir um bom gráfico em tempo real.
A parte dianteira
Olá assinaturas do GraphQL
As assinaturas do GraphQL são essencialmente consultas GraphQL “ao vivo”. Eles operam em WebSockets e têm exatamente a mesma estrutura de resposta que as consultas do GraphQL. Volte para https://localhost:8080/console
e tente fazer uma assinatura do GraphQL para a visualização que criamos.
subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } }
Essa assinatura assina os dados na exibição em que o local é London
e é ordenado em ordem crescente de five_second_intervals
.
Naturalmente, a resposta da visão seria um array vazio porque não inserimos nada no banco de dados nos últimos vinte minutos. (Você pode ver a entrada que inserimos algum tempo atrás se você chegou a esta seção em vinte minutos.)

{ "data": { "last_20_min_temp": [] } }
Mantendo essa assinatura ativa, abra outra aba e tente inserir outro valor na tabela de temperatures
utilizando a mesma mutação que realizamos anteriormente. Após a inserção, se você voltar para a guia em que a assinatura estava, você verá a resposta sendo atualizada automaticamente. Essa é a mágica em tempo real que o GraphQL Engine oferece. Vamos usar essa assinatura para alimentar nosso gráfico em tempo real.
Começando com Create-React-App
Vamos começar rapidamente com um iniciador de aplicativo React usando create react app. Execute o comando:
npx create-react-app time-series-chart
Isso criará um projeto inicial vazio. cd
nele e instale o GraphQL e as bibliotecas de gráficos. Além disso, instale o momento para converter carimbos de data/hora em um formato legível por humanos.
cd time-series-chart npm install --save apollo-boost apollo-link-ws subscriptions-transport-ws graphql react-apollo chart.js react-chartjs-2 moment
Por fim, execute o aplicativo com npm start
e um aplicativo React básico será aberto em https://localhost:3000
.

Configurando o cliente Apollo para o GraphQL do lado do cliente
O cliente Apollo é atualmente o melhor cliente GraphQL que funciona com qualquer servidor compatível com GraphQL. O Relay moderno também é bom, mas o servidor deve oferecer suporte à especificação de retransmissão para aproveitar todos os benefícios do Relay moderno. Usaremos o cliente Apollo para o GraphQL do lado do cliente para este tutorial. Vamos realizar a configuração para fornecer o cliente Apollo ao aplicativo.
Não estou entrando nas sutilezas dessa configuração porque os trechos de código a seguir são retirados diretamente dos documentos. Vá para src/index.js
no diretório do aplicativo React e instancie o cliente Apollo e adicione este trecho de código acima 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 });
Por fim, envolva o App
dentro do ApolloProvider
para que possamos usar o cliente Apollo nos componentes filhos. Seu App.js
deve finalmente se parecer com:
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') );
O cliente Apollo foi configurado. Agora podemos usar facilmente o GraphQL em tempo real do nosso aplicativo. Vá para src/App.js
.
Construindo o gráfico
ChartJS fornece uma API bem legal para construir gráficos. Estaremos construindo um gráfico de linhas; então um gráfico de linhas espera dados da forma:
{ "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], }
Se o conjunto de dados acima for usado para renderizar um gráfico de linhas, ele ficaria assim:

Vamos tentar construir este gráfico de exemplo primeiro. Importe a Line
do react-chartjs-2
e renderize-a passando o objeto acima como um suporte de dados. O método render seria algo como:
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> ); }
Em seguida, assinaremos os dados em nossa visualização e os alimentaremos no gráfico de linhas. Mas como realizamos assinaturas no cliente?
Os componentes <Subscription>
do Apollo funcionam usando o padrão render prop onde os filhos de um componente são renderizados com o contexto dos dados da assinatura.
<Subscription subscription={gql`subscription { parent { child } }`} /> { ({data, error, loading}) => { if (error) return <Error error={error} />; if (loading) return <Loading />; return <RenderData data={data} />; } } </Subscription>
Vamos usar um desses componentes de Subscription
para assinar nossa exibição e, em seguida, transformar os dados de assinatura na estrutura que o ChartJS espera. A lógica de transformação fica assim:
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'); })
Observação : você também pode usar a biblioteca de código aberto graphq2chartjs para transformar os dados da resposta do GraphQL em um formulário que o ChartJS espera.
Depois de usar isso dentro do componente Subscription, nosso App.js
se parece com:
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;
Você terá um gráfico em tempo real totalmente funcional pronto em https://localhost:3000
. No entanto, estaria vazio, então vamos preencher alguns dados de amostra para que possamos realmente ver alguma mágica acontecer.
Nota : adicionei mais algumas opções ao gráfico de linhas porque não gosto dessas animações sofisticadas no ChartJS. Uma série temporal parece legal quando é simples, no entanto, você pode remover o suporte de opções, se quiser.
Inserindo Dados de Amostra
Vamos escrever um script que preencha nosso banco de dados com dados fictícios. Crie um diretório separado (fora deste aplicativo) e crie um arquivo chamado script.js
com o seguinte conteúdo,
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 );
Agora execute estes dois comandos:
npm install --save node-fetch node script.js
Você pode voltar para https://localhost:3000
e ver a atualização do gráfico.
Terminando
Você pode construir a maioria dos gráficos em tempo real usando as ideias que discutimos acima. O algoritmo é:
- Implante o GraphQL Engine com Postgres;
- Crie tabelas onde deseja armazenar dados;
- Assine essas tabelas do seu aplicativo React;
- Renderize o gráfico.
Você pode encontrar o código-fonte aqui.