Tworzenie wykresów w czasie rzeczywistym za pomocą GraphQL i Postgres

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ Nie ma lepszego sposobu na zrozumienie danych niż wizualizacja ich za pomocą wykresów i diagramów. Społeczność JS ma kilka świetnych projektów open source, które ułatwiają wizualizację danych, jednak nie było rozwiązania do budowania zaplecza w czasie rzeczywistym, które może tworzyć kopie zapasowe tych wykresów i czynić je w czasie rzeczywistym. Dzięki GraphQL (który ma dobrze zdefiniowaną specyfikację dla subskrypcji w czasie rzeczywistym), możemy uzyskać działający w czasie rzeczywistym backend działający w ciągu kilku sekund i wykorzystać go do zasilania wykresów w czasie rzeczywistym.

Wykresy stanowią integralną część każdej branży zajmującej się danymi. Wykresy są przydatne w branży głosowania i ankietowania, a także świetnie pomagają nam lepiej zrozumieć różne zachowania i cechy użytkowników i klientów, z którymi współpracujemy.

Dlaczego wykresy w czasie rzeczywistym są tak ważne? Cóż, są przydatne w przypadkach, gdy nowe dane są tworzone w sposób ciągły; na przykład, gdy korzystanie z serii w czasie rzeczywistym do wizualizacji cen akcji jest świetnym zastosowaniem dla wykresów w czasie rzeczywistym. W tym samouczku wyjaśnię, jak tworzyć wykresy w czasie rzeczywistym za pomocą technologii open-source odpowiednich do tego konkretnego zadania.

Uwaga : Ten samouczek wymaga podstawowej znajomości React i GraphQL.

Stos

  1. PostgreSQL
    Istotą korzystania z wykresów jest wizualizacja „ogromnych” danych o wolumenach. Dlatego potrzebujemy bazy danych, która wydajnie obsługuje duże dane i zapewnia intuicyjne API do ich restrukturyzacji. Bazy danych SQL pozwalają nam tworzyć widoki, które są dla nas abstrakcyjne i agregujące dane. Będziemy korzystać z Postgresa, który jest sprawdzoną i wysoce wydajną bazą danych. Posiada również fantazyjne rozszerzenia typu open source, takie jak Timescale i PostGIS, które pozwalają nam odpowiednio budować wykresy oparte na geolokalizacji i szeregach czasowych. Do budowy naszego wykresu szeregów czasowych będziemy używać skali czasu.
  2. Silnik GraphQL
    Ten post dotyczy budowania wykresów w czasie rzeczywistym, a GraphQL zawiera dobrze zdefiniowaną specyfikację subskrypcji w czasie rzeczywistym. Hasura GraphQL Engine to serwer GraphQL o otwartym kodzie źródłowym, który pobiera połączenie z Postgres i umożliwia wysyłanie zapytań do danych Postgres przez GraphQL w czasie rzeczywistym. Jest również wyposażony w warstwę kontroli dostępu, która pomaga ograniczyć dane w oparciu o niestandardowe reguły kontroli dostępu.
  3. WykresJS
    ChartJS to popularna i dobrze utrzymana biblioteka open source do tworzenia wykresów za pomocą JavaScript. Użyjemy chart.js wraz z jego abstrakcją ReactJS react-chartjs-2 . O tym, dlaczego React, to dlatego, że React zapewnia programistom intuicyjny interfejs API oparty na zdarzeniach. Ponadto jednokierunkowy przepływ danych w React jest idealny do tworzenia wykresów opartych na danych.
Więcej po skoku! Kontynuuj czytanie poniżej ↓

Wymagania

W tym samouczku będziesz potrzebować następujących elementów w swoim systemie:

  1. Docker CE
    Docker to oprogramowanie, które umożliwia konteneryzację aplikacji. Obraz dokera to niezależny pakiet, który zawiera oprogramowanie wraz z jego zależnościami oraz minimalistyczny system operacyjny. Takie obrazy dokowane można technicznie uruchomić na dowolnym komputerze, na którym zainstalowano dok. Do tego samouczka będziesz potrzebować dokera.
    • Przeczytaj więcej o Dockerze
    • Zainstaluj Docker
  2. npm: npm to pakiet zarządzania dla JavaScript.

Próbny

Zbudujemy następujący wykres szeregów czasowych na żywo, który pokazuje maksymalną temperaturę lokalizacji w odstępach 5 sekund w ciągu ostatnich 20 minut od chwili obecnej.

Prezentacja GIF wykresu w czasie rzeczywistym
Prezentacja GIF wykresu w czasie rzeczywistym

Konfigurowanie zaplecza

Prowadzenie usług

Backend składa się z bazy danych Postgres, jej rozszerzenia skali czasu oraz Hasura GraphQL Engine. Uruchommy bazę danych i nasz serwer GraphQL, uruchamiając odpowiednie obrazy dockera. Utwórz plik o nazwie docker-compose.yaml i wklej do niego tę zawartość.

Uwaga : docker-compose to narzędzie do deklaratywnego uruchamiania wielu obrazów dockera.

 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:

Ten docker-compose.yaml zawiera specyfikację dwóch usług:

  1. timescale
    To jest nasza baza danych Postgres z zainstalowanym rozszerzeniem Timescale. Jest skonfigurowany do pracy na porcie 5432.
  2. graphql-engine
    To jest nasza instancja Hasura GraphQL Engine, czyli serwer GraphQL, który wskazuje na bazę danych i udostępnia nad nią GraphQL API. Jest skonfigurowany do działania w porcie 8080, a port 8080 jest mapowany na port 8080 komputera, na którym działa ten kontener Docker. Oznacza to, że możesz uzyskać dostęp do tego serwera GraphQL poprzez localhost:8080 maszyny.

Uruchommy te kontenery platformy Docker, uruchamiając następujące polecenie w dowolnym miejscu, w którym umieściłeś docker-compose.yaml .

 docker-compose up -d

To polecenie pobiera obrazy dokowane z chmury i uruchamia je w podanej kolejności. Może to potrwać kilka sekund w zależności od szybkości Twojego internetu. Po zakończeniu możesz uzyskać dostęp do konsoli GraphQL Engine pod adresem https://localhost:8080/console .

Konsola silnika Hasura GraphQL
Konsola Hasura GraphQL Engine (duży podgląd)

Konfigurowanie bazy danych

Następnie stwórzmy tabelę o nazwie temperatura, która przechowuje wartości temperatur w różnych momentach. Przejdź do zakładki Dane w konsoli i przejdź do sekcji SQL . Utwórz naszą tabelę temperature , uruchamiając ten blok SQL:

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

Tworzy to prostą tabelę Postgres w bazie danych. Ale chcemy wykorzystać partycjonowanie przedziałów czasowych rozszerzenia Timescale. Aby to zrobić, musimy przekonwertować tę tabelę na hipertabelę skali czasu, uruchamiając polecenie SQL:

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

To polecenie tworzy hipertablicę podzieloną według czasu w polu recorded_at .

Teraz, odkąd ta tabela została utworzona, możemy od razu zacząć tworzyć zapytania GraphQL nad nią. Możesz je wypróbować, klikając na zakładkę GraphiQL na górze. Najpierw spróbuj zrobić mutację:

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

Mutacja GraphQL powyżej wstawia wiersz w tabeli temperature . Teraz spróbuj wykonać zapytanie GraphQL, aby sprawdzić, czy dane zostały wstawione.

Następnie spróbuj wykonać zapytanie:

 query { temperature { recorded_at temperature location } }

Mam nadzieję, że zadziałało :)

Teraz naszym zadaniem jest stworzenie wykresu szeregów czasowych na żywo, który pokazuje maksymalną temperaturę lokalizacji w odstępach co 5 sekund w ciągu ostatnich 20 minut od chwili obecnej. Stwórzmy widok, który daje nam dokładnie te dane.

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

Ten widok grupuje dane z tabeli temperature w 5-sekundowych oknach z ich maksymalną temperaturą ( max_temp) . Grupowanie drugorzędne odbywa się za pomocą pola location . Wszystkie te dane pochodzą tylko z ostatnich dwudziestu minut od chwili obecnej.

Otóż ​​to. Nasz backend jest skonfigurowany. Zbudujmy teraz ładny wykres w czasie rzeczywistym.

Frontend

Witam Subskrypcje GraphQL

Subskrypcje GraphQL są zasadniczo „na żywo” zapytaniami GraphQL. Działają na WebSockets i mają dokładnie taką samą strukturę odpowiedzi, jak zapytania GraphQL. Wróć do https://localhost:8080/console i spróbuj zrobić subskrypcję GraphQL do utworzonego przez nas widoku.

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

Ta subskrypcja subskrybuje dane w widoku, w którym lokalizacja to London , i jest uporządkowana w kolejności rosnącej od five_second_intervals .

Oczywiście odpowiedź z widoku byłaby pustą tablicą, ponieważ w ciągu ostatnich dwudziestu minut nie wstawialiśmy niczego do bazy danych. (Możesz zobaczyć wpis, który wstawiliśmy kiedyś, jeśli dotarłeś do tej sekcji w ciągu dwudziestu minut).

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

Utrzymując tę ​​subskrypcję, otwórz kolejną zakładkę i spróbuj wstawić inną wartość w tabeli temperatures , używając tej samej mutacji, którą wykonaliśmy wcześniej. Po wstawieniu, jeśli wrócisz do zakładki, na której była subskrypcja, zobaczysz odpowiedź zaktualizowaną automatycznie. To magia czasu rzeczywistego, którą zapewnia GraphQL Engine. Wykorzystajmy tę subskrypcję do zasilania naszego wykresu w czasie rzeczywistym.

Pierwsze kroki z aplikacją Create-React

Pozwól nam szybko zacząć od startera aplikacji React za pomocą aplikacji Create React. Uruchom polecenie:

 npx create-react-app time-series-chart

Spowoduje to utworzenie pustego projektu startowego. cd do niego i zainstaluj biblioteki GraphQL i wykresów. Zainstaluj także moment do konwersji znaczników czasu do formatu czytelnego dla człowieka.

 cd time-series-chart npm install --save apollo-boost apollo-link-ws subscriptions-transport-ws graphql react-apollo chart.js react-chartjs-2 moment

Na koniec uruchom aplikację z npm start , a podstawowa aplikacja React otworzy się pod adresem https://localhost:3000 .

Surowa aplikacja do tworzenia-reagowania
Surowa aplikacja creat-react (duży podgląd)

Konfigurowanie klienta Apollo dla GraphQL po stronie klienta

Klient Apollo jest obecnie najlepszym klientem GraphQL, który współpracuje z dowolnym serwerem zgodnym z GraphQL. Relay modern też jest dobry, ale serwer musi obsługiwać specyfikację relay, aby wykorzystać wszystkie zalety tej funkcji. W tym samouczku użyjemy klienta Apollo dla GraphQL po stronie klienta. Pozwól nam przeprowadzić konfigurację, aby dostarczyć klienta Apollo do aplikacji.

Nie zagłębiam się w subtelności tej konfiguracji, ponieważ poniższe fragmenty kodu są pobierane bezpośrednio z dokumentacji. Przejdź do src/index.js w katalogu aplikacji React i stwórz instancję klienta Apollo i dodaj ten fragment kodu nad 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 });

Na koniec owiń App w ApolloProvider , abyśmy mogli używać klienta Apollo w komponentach potomnych. Twój App.js powinien wreszcie wyglądać tak:

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

Klient Apollo został skonfigurowany. Teraz możemy z łatwością korzystać z GraphQL w czasie rzeczywistym z naszej aplikacji. Przejdź do src/App.js .

Budowanie wykresu

ChartJS zapewnia całkiem zgrabne API do budowania wykresów. Będziemy budować wykres liniowy; więc wykres liniowy oczekuje danych postaci:

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

Jeśli powyższy zestaw danych jest używany do renderowania wykresu liniowego, wyglądałoby to mniej więcej tak:

Przykładowy wykres liniowy
Przykładowy wykres liniowy (duży podgląd)

Spróbujmy najpierw zbudować ten przykładowy wykres. Importuj Line z react-chartjs-2 i renderuj ją przekazując powyższy obiekt jako właściwość danych. Metoda render wyglądałaby mniej więcej tak:

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

Następnie zasubskrybujemy dane w naszym widoku i wprowadzimy je do wykresu liniowego. Ale jak wykonujemy subskrypcje na kliencie?

Komponenty <Subscription> Apollo działają przy użyciu wzorca właściwości renderowania, w którym elementy potomne komponentu są renderowane z kontekstem danych subskrypcji.

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

Użyjmy jednego takiego składnika Subscription , aby zasubskrybować nasz widok, a następnie przekształcić dane subskrypcji do struktury, której oczekuje ChartJS. Logika transformacji wygląda tak:

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

Uwaga : Możesz również użyć biblioteki graphq2chartjs o otwartym kodzie źródłowym do przekształcenia danych z odpowiedzi GraphQL do postaci, której oczekuje ChartJS.

Po użyciu tego w komponencie Subskrypcja, nasz App.js wygląda tak:

 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;

Będziesz mieć gotowy w pełni działający wykres w czasie rzeczywistym pod adresem https://localhost:3000 . Byłby jednak pusty, więc wypełnijmy kilka przykładowych danych, abyśmy mogli zobaczyć, jak dzieje się coś magicznego.

Uwaga : dodałem więcej opcji do wykresu liniowego, ponieważ nie podobają mi się te fantazyjne animacje w ChartJS. Szeregi czasowe wyglądają ładnie, gdy są proste, jednak możesz usunąć rekwizyt opcji, jeśli chcesz.

Wstawianie przykładowych danych

Napiszmy skrypt, który zapełni naszą bazę fikcyjnymi danymi. Utwórz osobny katalog (poza tą aplikacją) i utwórz plik o nazwie script.js z następującą zawartością,

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

Teraz uruchom te dwie komendy:

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

Możesz wrócić do https://localhost:3000 i zobaczyć aktualizację wykresu.

Kończąc

Możesz zbudować większość wykresów w czasie rzeczywistym, korzystając z pomysłów, które omówiliśmy powyżej. Algorytm to:

  1. Wdróż silnik GraphQL z Postgresem;
  2. Twórz tabele, w których chcesz przechowywać dane;
  3. Zasubskrybuj te tabele z aplikacji React;
  4. Wyrenderuj wykres.

Kod źródłowy znajdziesz tutaj.