Построение диаграмм в реальном времени с помощью GraphQL и Postgres

Опубликовано: 2022-03-10
Краткое резюме ↬ Нет лучшего способа понять данные, чем визуализировать их с помощью диаграмм и диаграмм. В сообществе JS есть несколько замечательных проектов с открытым исходным кодом, которые упрощают визуализацию данных, однако не существует готового решения для создания серверных частей в реальном времени, которые могли бы поддерживать эти диаграммы и делать их в реальном времени. С GraphQL (у которого есть четко определенная спецификация для подписок в реальном времени) мы можем запустить серверную часть в реальном времени за считанные секунды и использовать ее для построения графиков в реальном времени.

Диаграммы являются неотъемлемой частью любой отрасли, которая имеет дело с данными. Диаграммы полезны в индустрии голосования и опросов, а также отлично помогают нам лучше понять различное поведение и характеристики пользователей и клиентов, с которыми мы работаем.

Почему графики в реальном времени так важны? Что ж, они полезны в тех случаях, когда новые данные создаются непрерывно; например, при использовании динамических рядов для визуализации цен на акции отлично подходят графики в реальном времени. В этом уроке я объясню, как создавать графики в реальном времени с помощью технологий с открытым исходным кодом, подходящих именно для этой конкретной задачи.

Примечание . Для этого руководства требуются базовые знания React и GraphQL.

Куча

  1. PostgreSQL
    Сам смысл использования диаграмм заключается в визуализации «огромных» объемов данных. Поэтому нам нужна база данных, которая эффективно обрабатывает большие данные и предоставляет интуитивно понятный API для их реструктуризации. Базы данных SQL позволяют нам создавать представления, которые абстрагируют и объединяют данные для нас. Мы будем использовать Postgres, проверенную временем и высокоэффективную базу данных. Он также имеет причудливые расширения с открытым исходным кодом, такие как Timescale и PostGIS, которые позволяют нам строить диаграммы на основе геолокации и временных рядов соответственно. Мы будем использовать шкалу времени для построения диаграммы временных рядов.
  2. Графический движок QL
    Этот пост посвящен построению диаграмм в реальном времени, а GraphQL поставляется с четко определенной спецификацией для подписки в реальном времени. Hasura GraphQL Engine — это сервер GraphQL с открытым исходным кодом, который принимает соединение Postgres и позволяет запрашивать данные Postgres через GraphQL в реальном времени. Он также поставляется с уровнем контроля доступа, который помогает вам ограничивать ваши данные на основе настраиваемых правил контроля доступа.
  3. ЧартJS
    ChartJS — популярная и хорошо поддерживаемая библиотека с открытым исходным кодом для построения диаграмм с помощью JavaScript. Мы будем использовать chart.js вместе с его абстракцией react-chartjs-2 . О том, почему React, потому что React предоставляет разработчикам интуитивно понятный API, управляемый событиями. Кроме того, однонаправленный поток данных React идеально подходит для построения диаграмм, управляемых данными.
Еще после прыжка! Продолжить чтение ниже ↓

Требования

Для этого руководства вам потребуется следующее в вашей системе:

  1. Докер СЕ
    Docker — это программное обеспечение, которое позволяет вам контейнеризовать ваши приложения. Docker-образ — это независимый пакет, содержащий программное обеспечение вместе с его зависимостями и минималистичной операционной системой. Такие образы докеров технически можно запускать на любой машине, на которой установлен докер. Для этого урока вам понадобится докер.
    • Подробнее о Докере
    • Установить Докер
  2. npm: npm — это управление пакетами для JavaScript.

Демо

Мы построим следующую диаграмму временных рядов в реальном времени, которая показывает максимальную температуру местоположения с интервалом в 5 секунд за последние 20 минут с настоящего момента.

GIF-демонстрация диаграммы в реальном времени
GIF-демонстрация диаграммы в реальном времени

Настройка бэкенда

Запуск служб

Серверная часть состоит из базы данных Postgres, расширения временной шкалы и Hasura GraphQL Engine. Давайте запустим базу данных и наш сервер GraphQL, запустив соответствующие образы докеров. Создайте файл с именем docker-compose.yaml и вставьте в него это содержимое.

Примечание . docker-compose — это утилита для декларативного запуска нескольких образов Docker.

 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:

Этот docker-compose.yaml содержит спецификацию для двух сервисов:

  1. timescale
    Это наша база данных Postgres с установленным расширением Timescale. Он настроен для работы на порту 5432.
  2. graphql-engine
    Это наш экземпляр Hasura GraphQL Engine, то есть сервер GraphQL, который указывает на базу данных и предоставляет API-интерфейсы GraphQL поверх нее. Он настроен для работы на порту 8080, а порт 8080 сопоставлен с портом 8080 машины, на которой работает этот док-контейнер. Это означает, что вы можете получить доступ к этому серверу GraphQL через локальный хост localhost:8080 машины.

Давайте запустим эти контейнеры Docker, выполнив следующую команду везде, где вы разместили свой docker-compose.yaml .

 docker-compose up -d

Эта команда извлекает образы докеров из облака и запускает их в заданном порядке. Это может занять несколько секунд в зависимости от скорости вашего интернета. После завершения вы можете получить доступ к консоли GraphQL Engine по адресу https://localhost:8080/console .

Консоль Hasura GraphQL Engine
Консоль Hasura GraphQL Engine (большой предварительный просмотр)

Настройка базы данных

Далее давайте создадим таблицу с именем температура, в которой хранятся значения температуры в разное время. Перейдите на вкладку Data в консоли и перейдите в раздел SQL . Создайте нашу таблицу temperature , запустив этот блок SQL:

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

Это создает простую таблицу Postgres в базе данных. Но мы хотим использовать разделение временных интервалов расширения Timescale. Для этого мы должны преобразовать эту таблицу в гипертаблицу шкалы времени, выполнив команду SQL:

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

Эта команда создает гипертаблицу, разделенную по времени в поле recorded_at .

Теперь, когда эта таблица создана, мы можем напрямую приступить к выполнению запросов GraphQL к ней. Вы можете попробовать их, щелкнув вкладку GraphiQL вверху. Сначала попробуйте сделать мутацию:

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

Приведенная выше мутация GraphQL вставляет строку в таблицу temperature . Теперь попробуйте сделать запрос GraphQL, чтобы проверить, были ли вставлены данные.

Затем попробуйте сделать запрос:

 query { temperature { recorded_at temperature location } }

Надеюсь, это сработало :)

Теперь наша задача состоит в том, чтобы создать диаграмму временных рядов в реальном времени, которая показывает максимальную температуру местоположения с интервалом в 5 секунд за последние 20 минут с настоящего момента. Давайте создадим представление, которое дает нам именно эти данные.

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

Это представление группирует данные из таблицы temperature в 5-секундных окнах с их максимальной температурой ( max_temp) . Вторичная группировка выполняется с использованием поля location . Все эти данные только за последние двадцать минут с настоящего момента.

Вот и все. Наш бэкэнд настроен. Давайте теперь построим хороший график в реальном времени.

Внешний интерфейс

Привет подписки на GraphQL

Подписки GraphQL — это, по сути, «живые» запросы GraphQL. Они работают через WebSockets и имеют точно такую ​​же структуру ответов, что и запросы GraphQL. Вернитесь на https://localhost:8080/console и попробуйте сделать подписку GraphQL на созданное нами представление.

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

Эта подписка подписывается на данные в представлении, где находится London , и они упорядочены в порядке возрастания five_second_intervals .

Естественно, ответом представления будет пустой массив, поскольку за последние двадцать минут мы ничего не вставляли в базу данных. (Вы могли бы увидеть запись, которую мы вставили когда-то назад, если бы вы добрались до этого раздела в течение двадцати минут.)

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

Сохраняя эту подписку, откройте другую вкладку и попробуйте вставить другое значение в таблицу temperatures , используя ту же мутацию, которую мы выполнили ранее. После вставки, если вы вернетесь на вкладку, на которой была включена подписка, вы увидите, что ответ обновился автоматически. Это магия реального времени, которую обеспечивает GraphQL Engine. Давайте воспользуемся этой подпиской, чтобы включить нашу диаграмму в реальном времени.

Начало работы с приложением Create-React

Давайте быстро начнем работу со стартером приложения React, используя приложение create response. Запустите команду:

 npx create-react-app time-series-chart

Это создаст пустой стартовый проект. cd в него и установите библиотеки GraphQL и диаграмм. Также установите момент для преобразования временных меток в удобочитаемый формат.

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

Наконец, запустите приложение с помощью npm start , и базовое приложение React откроется по адресу https://localhost:3000 .

Необработанное приложение «создать-реагировать»
Raw creat-react-app (большой предварительный просмотр)

Настройка клиента Apollo для GraphQL на стороне клиента

Клиент Apollo в настоящее время является лучшим клиентом GraphQL, который работает с любым сервером, совместимым с GraphQL. Relay Modern тоже хорош, но сервер должен поддерживать спецификацию Relay, чтобы использовать все преимущества Relay modern. В этом руководстве мы будем использовать клиент Apollo для GraphQL на стороне клиента. Давайте выполним настройку, чтобы предоставить приложению клиент Apollo.

Я не буду вдаваться в тонкости этой настройки, потому что следующие фрагменты кода взяты непосредственно из документации. Перейдите к src/index.js в каталоге приложения React, создайте экземпляр клиента Apollo и добавьте этот фрагмент кода выше 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 });

Наконец, оберните App внутри ApolloProvider , чтобы мы могли использовать клиент Apollo в дочерних компонентах. Ваш App.js должен наконец выглядеть так:

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

Клиент Apollo настроен. Теперь мы можем легко использовать GraphQL в реальном времени из нашего приложения. Перейдите в src/App.js .

Построение диаграммы

ChartJS предоставляет довольно удобный API для построения диаграмм. Мы будем строить линейный график; поэтому линейная диаграмма ожидает данные в форме:

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

Если приведенный выше набор данных используется для рендеринга линейной диаграммы, он будет выглядеть примерно так:

Образец линейной диаграммы
Образец линейного графика (большой предварительный просмотр)

Давайте сначала попробуем построить этот образец диаграммы. Импортируйте строку из Line react-chartjs-2 и визуализируйте ее, передавая указанный выше объект в качестве реквизита данных. Метод рендеринга будет выглядеть примерно так:

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

Далее мы подпишемся на данные в нашем представлении и передадим их на линейный график. Но как мы выполняем подписки на клиенте?

Компоненты Apollo <Subscription> работают с использованием шаблона поддержки рендеринга, в котором дочерние элементы компонента визуализируются в контексте данных подписки.

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

Давайте воспользуемся одним таким компонентом Subscription , чтобы подписаться на наше представление, а затем преобразуем данные подписки в структуру, которую ожидает ChartJS. Логика трансформации выглядит так:

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

Примечание . Вы также можете использовать библиотеку с открытым исходным кодом graphq2chartjs для преобразования данных из ответа GraphQL в форму, ожидаемую ChartJS.

После использования этого внутри компонента Subscription наш App.js выглядит так:

 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;

У вас будет готовая полностью рабочая диаграмма в реальном времени по адресу https://localhost:3000 . Однако он будет пустым, поэтому давайте заполним некоторые образцы данных, чтобы мы действительно могли увидеть, как происходит какое-то волшебство.

Примечание . Я добавил еще несколько параметров в линейный график, потому что мне не нравятся эти причудливые анимации в ChartJS. Временной ряд выглядит мило, когда он прост, однако вы можете удалить поддержку параметров, если хотите.

Вставка демонстрационных данных

Давайте напишем скрипт, который заполняет нашу базу данных фиктивными данными. Создайте отдельный каталог (вне этого приложения) и создайте файл с именем script.js со следующим содержимым:

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

Теперь запустите эти две команды:

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

Вы можете вернуться на https://localhost:3000 и посмотреть, как обновляется диаграмма.

Заканчивать

Вы можете построить большинство графиков в реальном времени, используя идеи, которые мы обсуждали выше. Алгоритм:

  1. Развернуть GraphQL Engine с Postgres;
  2. Создайте таблицы, в которых вы хотите хранить данные;
  3. Подпишитесь на эти таблицы из своего приложения React;
  4. Отобразите диаграмму.

Вы можете найти исходный код здесь.