Criando gráficos em tempo real com GraphQL e Postgres

Publicados: 2022-03-10
Resumo rápido ↬ Não há melhor maneira de entender os dados do que visualizando-os com gráficos e diagramas. A comunidade JS tem alguns ótimos projetos de código aberto que facilitam a visualização de dados, no entanto, não há uma solução para criar back-ends em tempo real que possam apoiar esses gráficos e torná-los em tempo real. Com o GraphQL (que possui uma especificação bem definida para assinaturas em tempo real), podemos obter um back-end em tempo real em execução em segundos e usá-lo para alimentar gráficos em tempo real.

Os 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

  1. 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.
  2. 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.
  3. ChartJS
    ChartJS é uma biblioteca de código aberto popular e bem mantida para construir gráficos com JavaScript. Usaremos chart.js junto com sua abstração ReactJS react-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.
Mais depois do salto! Continue lendo abaixo ↓

Requisitos

Para este tutorial, você precisará do seguinte em seu sistema:

  1. 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
  2. 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.

Demonstração GIF do gráfico em tempo real
Demonstração GIF do gráfico em tempo real

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:

  1. timescale
    Este é o nosso banco de dados Postgres com a extensão Timescale instalada. Ele está configurado para ser executado na porta 5432.
  2. 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 pelo localhost: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 .

Console do mecanismo Hasura GraphQL
Console do Hasura GraphQL Engine (visualização grande)

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 .

Raw create-react-app
Raw creat-react-app (visualização grande)

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:

Exemplo de gráfico de linhas
Exemplo de gráfico de linhas (visualização grande)

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 é:

  1. Implante o GraphQL Engine com Postgres;
  2. Crie tabelas onde deseja armazenar dados;
  3. Assine essas tabelas do seu aplicativo React;
  4. Renderize o gráfico.

Você pode encontrar o código-fonte aqui.