Creación de gráficos en tiempo real con GraphQL y Postgres

Publicado: 2022-03-10
Resumen rápido ↬ No hay mejor manera de comprender los datos que visualizarlos con gráficos y diagramas. La comunidad JS tiene algunos grandes proyectos de código abierto que facilitan la visualización de datos, sin embargo, no ha habido una solución para construir backends en tiempo real que puedan respaldar estos gráficos y hacerlos en tiempo real. Con GraphQL (que tiene una especificación bien definida para las suscripciones en tiempo real), podemos ejecutar un backend en tiempo real en segundos y usarlo para generar gráficos en tiempo real.

Los gráficos forman una parte integral de cualquier industria que trate con datos. Los gráficos son útiles en la industria de votaciones y encuestas, y también son excelentes para ayudarnos a comprender mejor los diferentes comportamientos y características de los usuarios y clientes con los que trabajamos.

¿Por qué son tan importantes los gráficos en tiempo real? Bueno, son útiles en los casos en que se producen nuevos datos continuamente; por ejemplo, cuando se usan series en tiempo real para visualizar los precios de las acciones, es un gran uso para los gráficos en tiempo real. En este tutorial, explicaré cómo crear gráficos en tiempo real con tecnologías de código abierto aptas exactamente para esta tarea en particular.

Nota : este tutorial requiere conocimientos básicos de React y GraphQL.

Apilar

  1. postgresql
    El punto detrás del uso de Gráficos es visualizar datos de volúmenes "enormes". Por lo tanto, necesitamos una base de datos que maneje de manera eficiente grandes cantidades de datos y proporcione una API intuitiva para reestructurarla. Las bases de datos SQL nos permiten crear vistas que resumen y agregan datos para nosotros. Usaremos Postgres, que es una base de datos probada y altamente eficiente. También tiene elegantes extensiones de código abierto como Timescale y PostGIS que nos permiten crear gráficos basados ​​en geolocalización y series temporales, respectivamente. Usaremos Timescale para construir nuestro gráfico de serie de tiempo.
  2. Motor GraphQL
    Esta publicación trata sobre la creación de gráficos en tiempo real, y GraphQL viene con una especificación bien definida para suscripciones en tiempo real. Hasura GraphQL Engine es un servidor GraphQL de código abierto que toma una conexión de Postgres y le permite consultar los datos de Postgres en GraphQL en tiempo real. También viene con una capa de control de acceso que lo ayuda a restringir sus datos según las reglas de control de acceso personalizadas.
  3. GráficoJS
    ChartJS es una biblioteca de código abierto popular y bien mantenida para crear gráficos con JavaScript. Usaremos chart.js junto con su abstracción ReactJS react-chartjs-2 . Acerca de por qué React, es porque React empodera a los desarrolladores con una API intuitiva basada en eventos. Además, el flujo de datos unidireccional de React es ideal para crear gráficos basados ​​en datos.
¡Más después del salto! Continúe leyendo a continuación ↓

Requisitos

Para este tutorial, necesitará lo siguiente en su sistema:

  1. estibador CE
    Docker es un software que le permite contener sus aplicaciones. Una imagen acoplable es un paquete independiente que contiene software junto con sus dependencias y un sistema operativo minimalista. Dichas imágenes de docker se pueden ejecutar técnicamente en cualquier máquina que tenga instalado docker. Necesitará Docker para este tutorial.
    • Leer más sobre Docker
    • Instalar ventana acoplable
  2. npm: npm es el administrador de paquetes para JavaScript.

Manifestación

Construiremos el siguiente gráfico de serie de tiempo en vivo que muestra la temperatura máxima de una ubicación en intervalos de 5 segundos durante los últimos 20 minutos desde el momento presente.

Demostración GIF del gráfico en tiempo real
Demostración GIF del gráfico en tiempo real

Configurando el back-end

Ejecución de los servicios

El backend se compone de una base de datos de Postgres, su extensión de escala de tiempo y Hasura GraphQL Engine. Hagamos que la base de datos y nuestro servidor GraphQL se ejecuten ejecutando las respectivas imágenes acoplables. Cree un archivo llamado docker-compose.yaml y pegue este contenido en él.

Nota : docker-compose es una utilidad para ejecutar varias imágenes 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 contiene la especificación para dos servicios:

  1. timescale
    Esta es nuestra base de datos Postgres con la extensión Timescale instalada. Está configurado para ejecutarse en el puerto 5432.
  2. graphql-engine
    Esta es nuestra instancia del motor Hasura GraphQL, es decir, el servidor GraphQL que apunta a la base de datos y proporciona las API de GraphQL sobre ella. Está configurado para ejecutarse en el puerto 8080, y el puerto 8080 está asignado al puerto 8080 de la máquina en la que se ejecuta este contenedor acoplable. Esto significa que puede acceder a este servidor GraphQL a través de localhost:8080 de la máquina.

Ejecutemos estos contenedores docker ejecutando el siguiente comando donde hayas colocado tu docker-compose.yaml .

 docker-compose up -d

Este comando extrae las imágenes de la ventana acoplable de la nube y las ejecuta en el orden indicado. Puede tardar unos segundos según la velocidad de Internet. Una vez que esté completo, puede acceder a su consola GraphQL Engine en https://localhost:8080/console .

Consola del motor Hasura GraphQL
Consola Hasura GraphQL Engine (vista previa grande)

Configuración de la base de datos

A continuación, creemos una tabla llamada temperatura que almacene los valores de las temperaturas en diferentes momentos. Vaya a la pestaña Datos en la consola y vaya a la sección SQL . Cree nuestra tabla de temperature ejecutando este bloque SQL:

 CREATE TABLE temperature ( temperature numeric not null, location text not null, recorded_at timestamptz not null default now() );

Esto crea una tabla de Postgres simple en la base de datos. Pero deseamos aprovechar la partición de intervalos de tiempo de la extensión Timescale. Para hacer esto, debemos convertir esta tabla en hipertabla de escala de tiempo ejecutando el comando SQL:

 SELECT create_hypertable('temperature', 'recorded_at');

Este comando crea una hipertabla que está dividida por tiempo en el campo recorded_at .

Ahora, desde que se creó esta tabla, podemos comenzar directamente a realizar consultas GraphQL sobre ella. Puede probarlos haciendo clic en la pestaña GraphiQL en la parte superior. Intenta hacer una mutación primero:

 mutation { insert_temperature ( objects: [{ temperature: 13.4 location: "London" }] ) { returning { recorded_at temperature } } }

La mutación de GraphQL anterior inserta una fila en la tabla de temperature . Ahora intente hacer una consulta GraphQL para verificar si se insertaron los datos.

A continuación, intente hacer una consulta:

 query { temperature { recorded_at temperature location } }

Espero que haya funcionado :)

Ahora, la tarea que tenemos entre manos es crear un gráfico de serie de tiempo en vivo que muestre la temperatura máxima de una ubicación en intervalos de 5 segundos durante los últimos 20 minutos desde el momento presente. Vamos a crear una vista que nos proporcione exactamente estos datos.

 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 vista agrupa los datos de la tabla de temperature en ventanas de 5 segundos con su temperatura máxima ( max_temp) . La agrupación secundaria se realiza utilizando el campo de location . Todos estos datos son solo de los últimos veinte minutos desde el momento presente.

Eso es todo. Nuestro backend está configurado. Ahora construyamos un buen gráfico en tiempo real.

Interfaz

Hola GraphQL Suscripciones

Las suscripciones de GraphQL son esencialmente consultas de GraphQL "en vivo". Operan sobre WebSockets y tienen exactamente la misma estructura de respuesta que las consultas de GraphQL. Regrese a https://localhost:8080/console e intente suscribirse a GraphQL para la vista que creamos.

 subscription { last_20_min_temp( order_by: { five_sec_interval: asc } where: { location: { _eq: "London" } } ) { five_sec_interval location max_temp } }

Esta suscripción se suscribe a los datos en la vista donde la ubicación es London y se ordena en orden ascendente de los five_second_intervals .

Naturalmente, la respuesta de la vista sería una matriz vacía porque no hemos insertado nada en la base de datos en los últimos veinte minutos. (Es posible que vea la entrada que insertamos hace un tiempo si llegó a esta sección dentro de los veinte minutos).

 { "data": { "last_20_min_temp": [] } }

Manteniendo esta suscripción activada, abra otra pestaña e intente insertar otro valor en la tabla de temperatures usando la misma mutación que realizamos anteriormente. Después de insertar, si regresa a la pestaña donde estaba la suscripción, verá que la respuesta se actualizó automáticamente. Esa es la magia en tiempo real que proporciona GraphQL Engine. Usemos esta suscripción para potenciar nuestro gráfico en tiempo real.

Primeros pasos con Create-React-App

Comencemos rápidamente con un iniciador de aplicación React usando crear aplicación de reacción. Ejecute el comando:

 npx create-react-app time-series-chart

Esto creará un proyecto inicial vacío. cd en él e instale GraphQL y las bibliotecas de gráficos. Además, instale el momento para convertir las marcas de tiempo a un formato legible 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

Finalmente, ejecute la aplicación con npm start y se abrirá una aplicación React básica en https://localhost:3000 .

Raw crear-reaccionar-aplicación
Raw creat-react-app (Vista previa grande)

Configuración del cliente Apollo para GraphQL del lado del cliente

El cliente Apollo es actualmente el mejor cliente GraphQL que funciona con cualquier servidor compatible con GraphQL. La retransmisión moderna también es buena, pero el servidor debe admitir la especificación de retransmisión para aprovechar todos los beneficios de la retransmisión moderna. Usaremos el cliente Apollo para GraphQL del lado del cliente para este tutorial. Realicemos la configuración para proporcionar el cliente Apollo a la aplicación.

No voy a entrar en las sutilezas de esta configuración porque los siguientes fragmentos de código se toman directamente de los documentos. Dirígete a src/index.js en el directorio de la aplicación React e instancia el cliente Apollo y agrega este fragmento de código arriba de 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 });

Finalmente, envuelva la App dentro ApolloProvider para que podamos usar el cliente Apollo en los componentes secundarios. Su App.js finalmente debería verse así:

 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') );

Se ha configurado el cliente Apollo. Ahora podemos usar fácilmente GraphQL en tiempo real desde nuestra aplicación. Dirígete a src/App.js .

Construyendo el gráfico

ChartJS proporciona una API bastante ordenada para crear gráficos. Construiremos un gráfico de líneas; por lo que un gráfico de líneas espera datos de la forma:

 { "labels": ["label1", "label2", "label3", "label4"], "datasets": [{ "label": "Sample dataset", "data": [45, 23, 56, 55], "pointBackgroundColor": ["red", "brown", "green", "yellow"], "borderColor": "brown", "fill": false }], }

Si el conjunto de datos anterior se usa para representar un gráfico de líneas, se vería así:

Ejemplo de gráfico de líneas
Ejemplo de gráfico de líneas (vista previa grande)

Intentemos construir este gráfico de muestra primero. Importe Line desde react-chartjs-2 y reprodúzcalo pasando el objeto anterior como apoyo de datos. El método de render sería 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> ); }

A continuación, nos suscribiremos a los datos de nuestra vista y los introduciremos en el gráfico de líneas. Pero, ¿cómo realizamos las suscripciones en el cliente?

Los componentes <Subscription> de Apollo funcionan con el patrón de render prop donde los elementos secundarios de un componente se representan con el contexto de los datos de suscripción.

 <Subscription subscription={gql`subscription { parent { child } }`} /> { ({data, error, loading}) => { if (error) return <Error error={error} />; if (loading) return <Loading />; return <RenderData data={data} />; } } </Subscription>

Usemos uno de esos componentes de Subscription para suscribirnos a nuestra vista y luego transformemos los datos de suscripción a la estructura que ChartJS espera. La lógica de transformación se ve así:

 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 : también puede usar la biblioteca de código abierto graphq2chartjs para transformar los datos de la respuesta de GraphQL a un formulario que espera ChartJS.

Después de usar esto dentro del componente de suscripción, nuestro App.js se ve así:

 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;

Tendrá un gráfico en tiempo real completamente funcional listo en https://localhost:3000 . Sin embargo, estaría vacío, así que completemos algunos datos de muestra para que podamos ver cómo sucede algo de magia.

Nota : he agregado algunas opciones más al gráfico de líneas porque no me gustan esas animaciones sofisticadas en ChartJS. Una serie de tiempo se ve bien cuando es simple, sin embargo, puede eliminar las opciones si lo desea.

Insertar datos de muestra

Escribamos un script que llene nuestra base de datos con datos ficticios. Cree un directorio separado (fuera de esta aplicación) y cree un archivo llamado script.js con el siguiente contenido,

 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 );

Ahora ejecuta estos dos comandos:

 npm install --save node-fetch node script.js

Puede volver a https://localhost:3000 y ver la actualización del gráfico.

Terminando

Puede crear la mayoría de los gráficos en tiempo real utilizando las ideas que discutimos anteriormente. El algoritmo es:

  1. Implemente GraphQL Engine con Postgres;
  2. Cree tablas donde desee almacenar datos;
  3. Suscríbase a esas tablas desde su aplicación React;
  4. Renderice el gráfico.

Puedes encontrar el código fuente aquí.