Costruire grafici in tempo reale con GraphQL e Postgres
Pubblicato: 2022-03-10I grafici costituiscono una parte integrante di qualsiasi settore che si occupa di dati. I grafici sono utili nel settore del voto e dei sondaggi e sono anche ottimi per aiutarci a comprendere meglio i diversi comportamenti e le caratteristiche degli utenti e dei clienti con cui lavoriamo.
Perché i grafici in tempo reale sono così importanti? Bene, sono utili nei casi in cui nuovi dati vengono prodotti continuamente; ad esempio, quando si utilizzano serie in tempo reale per visualizzare i prezzi delle azioni è un ottimo utilizzo per i grafici in tempo reale. In questo tutorial, spiegherò come creare grafici in tempo reale con tecnologie open source adatte esattamente a questo particolare compito.
Nota : questo tutorial richiede una conoscenza di base di React e GraphQL.
Pila
- PostgreSQL
Il vero punto dietro l'utilizzo di Grafici è visualizzare dati di volumi "enormi". Pertanto, abbiamo bisogno di un database che gestisca in modo efficiente dati di grandi dimensioni e fornisca un'API intuitiva per ristrutturarli. I database SQL ci consentono di creare viste che astraggono e aggregano i dati per noi. Utilizzeremo Postgres, un database collaudato e altamente efficiente. Ha anche estensioni open source fantasiose come Timescale e PostGIS che ci consentono di costruire rispettivamente grafici basati sulla geolocalizzazione e basati su serie temporali. Useremo Timescale per costruire il nostro grafico delle serie temporali. - Motore GraphQL
Questo post riguarda la creazione di grafici in tempo reale e GraphQL viene fornito con specifiche ben definite per gli abbonamenti in tempo reale. Hasura GraphQL Engine è un server GraphQL open source che accetta una connessione Postgres e consente di interrogare i dati Postgres su GraphQL in tempo reale. Viene inoltre fornito con un livello di controllo degli accessi che ti aiuta a limitare i tuoi dati in base a regole di controllo degli accessi personalizzate. - GraficoJS
ChartJS è una libreria open source popolare e ben mantenuta per la creazione di grafici con JavaScript. Useremochart.js
insieme alla sua astrazione ReactJSreact-chartjs-2
. Sul motivo per cui React, è perché React offre agli sviluppatori un'API intuitiva basata su eventi. Inoltre, il flusso di dati unidirezionale di React è ideale per la creazione di grafici basati sui dati.
Requisiti
Per questo tutorial, avrai bisogno di quanto segue sul tuo sistema:
- Docker CE
Docker è un software che ti consente di containerizzare le tue applicazioni. Un'immagine docker è un pacchetto indipendente che contiene software insieme alle sue dipendenze e un sistema operativo minimalista. Tali immagini Docker possono essere eseguite tecnicamente in qualsiasi macchina su cui è installato Docker. Avrai bisogno della finestra mobile per questo tutorial.- Ulteriori informazioni su Docker
- Installa Docker
- npm: npm è il pacchetto manage per JavaScript.
Demo
Costruiremo il seguente grafico delle serie temporali in tempo reale che mostra la temperatura massima di un luogo a intervalli di 5 secondi negli ultimi 20 minuti dal momento presente.
Configurazione del backend
Esecuzione dei servizi
Il backend comprende un database Postgres, la sua estensione per la scala temporale e Hasura GraphQL Engine. Facciamo funzionare il database e il nostro server GraphQL eseguendo le rispettive immagini docker. Crea un file chiamato docker-compose.yaml
e incollaci questo contenuto.
Nota : docker-compose
è un'utilità per eseguire più immagini docker in modo dichiarativo.
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:
Questo docker-compose.yaml
contiene le specifiche per due servizi:
-
timescale
Questo è il nostro database Postgres con l'estensione Timescale installata. È configurato per funzionare sulla porta 5432. -
graphql-engine
Questa è la nostra istanza di Hasura GraphQL Engine, ovvero il server GraphQL che punta al database e fornisce API GraphQL su di esso. È configurato per essere eseguito sulla porta 8080 e la porta 8080 è mappata sulla porta 8080 della macchina su cui è in esecuzione questo contenitore Docker. Ciò significa che puoi accedere a questo server GraphQL tramitelocalhost:8080
della macchina.
Eseguiamo questi contenitori docker eseguendo il comando seguente ovunque tu abbia posizionato il tuo docker-compose.yaml
.
docker-compose up -d
Questo comando estrae le immagini della finestra mobile dal cloud e le esegue nell'ordine indicato. Potrebbero essere necessari alcuni secondi in base alla velocità di Internet. Una volta completato, puoi accedere alla tua console GraphQL Engine all'indirizzo https://localhost:8080/console
.
Configurazione del database
Quindi, creiamo una tabella chiamata temperatura che memorizza i valori delle temperature in momenti diversi. Vai alla scheda Dati nella console e vai alla sezione SQL
. Crea la nostra tabella delle temperature
eseguendo questo blocco SQL:
CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );
Questo crea una semplice tabella Postgres nel database. Ma desideriamo sfruttare il partizionamento dell'intervallo di tempo dell'estensione Timescale. Per fare ciò, dobbiamo convertire questa tabella nell'hypertable di timescale eseguendo il comando SQL:
SELECT create_hypertable('temperature', 'recorded_at');
Questo comando crea un'ipertabella che viene partizionata per tempo nel campo recorded_at
.
Ora, dal momento che questa tabella è stata creata, possiamo iniziare direttamente a fare query GraphQL su di essa. Puoi provarli facendo clic sulla scheda GraphiQL
in alto. Prova prima a fare una mutazione:
mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } }
La mutazione GraphQL sopra inserisce una riga nella tabella delle temperature
. Ora prova a fare una query GraphQL per verificare se i dati sono stati inseriti.
Quindi prova a fare una query:
query { temperature { recorded_at temperature location } }
Spero che abbia funzionato :)
Ora, il nostro compito è creare un grafico di serie temporali in tempo reale che mostri la temperatura massima di un luogo a intervalli di 5 secondi negli ultimi 20 minuti dal momento presente. Creiamo una vista che ci fornisca esattamente questi dati.
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 );
Questa visualizzazione raggruppa i dati dalla tabella delle temperature
in finestre di 5 secondi con la loro temperatura massima ( max_temp)
. Il raggruppamento secondario viene eseguito utilizzando il campo location
. Tutti questi dati provengono solo dagli ultimi venti minuti dal momento presente.
Questo è tutto. Il nostro backend è impostato. Costruiamo ora un bel grafico in tempo reale.
Fine frontale
Ciao abbonamenti GraphQL
Gli abbonamenti GraphQL sono essenzialmente query GraphQL "attive". Operano su WebSocket e hanno esattamente la stessa struttura di risposta delle query GraphQL. Torna a https://localhost:8080/console
e prova a fare un abbonamento GraphQL alla vista che abbiamo creato.
subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } }
Questo abbonamento sottoscrive i dati nella vista in cui la posizione è London
ed è ordinato in ordine crescente di five_second_intervals
.
Naturalmente, la risposta dalla vista sarebbe un array vuoto perché non abbiamo inserito nulla nel database negli ultimi venti minuti. (Potresti vedere la voce che abbiamo inserito qualche tempo fa se hai raggiunto questa sezione entro venti minuti.)
{ "data": { "last_20_min_temp": [] } }
Mantenendo attivo questo abbonamento, apri un'altra scheda e prova a inserire un altro valore nella tabella delle temperatures
utilizzando la stessa mutazione che abbiamo eseguito in precedenza. Dopo l'inserimento, se torni alla scheda in cui era attivo l'abbonamento, vedresti che la risposta si aggiorna automaticamente. Questa è la magia in tempo reale fornita da GraphQL Engine. Usiamo questo abbonamento per alimentare il nostro grafico in tempo reale.
Iniziare con Create-React-App
Iniziamo rapidamente con un avviatore di app React utilizzando l'app create react. Esegui il comando:
npx create-react-app time-series-chart
Questo creerà un progetto iniziale vuoto. cd
in esso e installa GraphQL e le librerie dei grafici. Inoltre, installa Moment per convertire i timestamp in un formato leggibile dall'uomo.
cd time-series-chart npm install --save apollo-boost apollo-link-ws subscriptions-transport-ws graphql react-apollo chart.js react-chartjs-2 moment
Infine, esegui l'app con npm start
e un'app React di base si aprirà su https://localhost:3000
.
Configurazione del client Apollo per GraphQL lato client
Il client Apollo è attualmente il miglior client GraphQL che funziona con qualsiasi server compatibile con GraphQL. Anche Relay Modern è buono, ma il server deve supportare le specifiche di Relay per sfruttare tutti i vantaggi di Relay Modern. Useremo il client Apollo per GraphQL lato client per questo tutorial. Eseguiamo la configurazione per fornire il client Apollo all'app.
Non sto entrando nelle sottigliezze di questa configurazione perché i seguenti frammenti di codice sono presi direttamente dai documenti. Vai a src/index.js
nella directory dell'app React e crea un'istanza del client Apollo e aggiungi questo frammento di codice sopra 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 });
Infine, avvolgi l' App
all'interno di ApolloProvider
in modo da poter utilizzare il client Apollo nei componenti figli. Il tuo App.js
dovrebbe finalmente assomigliare a:
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') );
Il client Apollo è stato impostato. Ora possiamo facilmente utilizzare GraphQL in tempo reale dalla nostra app. Vai a src/App.js
.
Costruire il grafico
ChartJS fornisce un'API piuttosto ordinata per la creazione di grafici. Costruiremo un grafico a linee; quindi un grafico a linee si aspetta dati del modulo:
{ "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], }
Se il set di dati sopra viene utilizzato per il rendering di un grafico a linee, sarebbe simile a questo:
Proviamo prima a costruire questo grafico di esempio. Importa Line
da react-chartjs-2
e rendila passando l'oggetto sopra come supporto dati. Il metodo di rendering sarebbe simile a:
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> ); }
Successivamente, sottoscriveremo i dati nella nostra vista e li alimenteremo al grafico a linee. Ma come eseguiamo gli abbonamenti sul client?
I componenti <Subscription>
di Apollo funzionano utilizzando il pattern prop di rendering in cui i figli di un componente vengono renderizzati con il contesto dei dati di sottoscrizione.
<Subscription subscription={gql`subscription { parent { child } }`} /> { ({data, error, loading}) => { if (error) return <Error error={error} />; if (loading) return <Loading />; return <RenderData data={data} />; } } </Subscription>
Usiamo uno di questi componenti di Subscription
per iscriverci alla nostra vista e quindi trasformare i dati di abbonamento nella struttura che ChartJS si aspetta. La logica di trasformazione si presenta così:
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'); })
Nota : puoi anche utilizzare la libreria open source graphq2chartjs per trasformare i dati dalla risposta di GraphQL a un modulo previsto da ChartJS.
Dopo averlo utilizzato all'interno del componente Abbonamento, il nostro App.js
si presenta come:
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;
Avrai un grafico in tempo reale completamente funzionante pronto su https://localhost:3000
. Tuttavia, sarebbe vuoto, quindi popolamo alcuni dati di esempio in modo da poter effettivamente vedere accadere qualcosa di magico.
Nota : ho aggiunto alcune altre opzioni al grafico a linee perché non mi piacciono quelle animazioni fantasiose in ChartJS. Una serie temporale sembra dolce quando è semplice, tuttavia, puoi rimuovere le opzioni di sostegno se lo desideri.
Inserimento di dati campione
Scriviamo uno script che popola il nostro database con dati fittizi. Crea una directory separata (al di fuori di questa app) e crea un file chiamato script.js
con il seguente contenuto,
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 );
Ora esegui questi due comandi:
npm install --save node-fetch node script.js
Puoi tornare a https://localhost:3000
e vedere l'aggiornamento del grafico.
Terminando
Puoi costruire la maggior parte dei grafici in tempo reale usando le idee che abbiamo discusso sopra. L'algoritmo è:
- Distribuisci il motore GraphQL con Postgres;
- Crea tabelle in cui desideri memorizzare i dati;
- Iscriviti a quelle tabelle dalla tua app React;
- Rendi il grafico.
Puoi trovare il codice sorgente qui.