Интеграция с React

Интеграция Vega и Vega-Lite в React-приложения строится вокруг идеи декларативного описания визуализации через JSON-спецификацию и императивного рендеринга через движок Vega. В экосистеме React это приводит к нескольким устойчивым моделям:

  • использование обёрток над Vega (react-vega, vega-embed)
  • прямое управление View через жизненный цикл компонентов
  • генерация спецификаций через состояние React
  • изоляция рендеринга графиков от повторных ререндеров React

Vega-Lite выступает высокоуровневым DSL, который компилируется в Vega-spec, после чего уже отрисовывается движком Vega.


Установка и базовая конфигурация

Основные зависимости:

npm install react-vega vega vega-lite vega-embed

Типовая связка:

  • vega — ядро рендеринга
  • vega-lite — декларативный слой описания графиков
  • vega-embed — утилита для вставки графиков в DOM
  • react-vega — React-обёртка

Базовая интеграция через react-vega

react-vega предоставляет компонент <Vega />, который принимает спецификацию и автоматически управляет жизненным циклом визуализации.

import { Vega } from 'react-vega';

const spec = {
  width: 400,
  height: 200,
  data: {
    values: [
      { x: 1, y: 2 },
      { x: 2, y: 5 },
      { x: 3, y: 3 }
    ]
  },
  mark: 'line',
  encoding: {
    x: { field: 'x', type: 'quantitative' },
    y: { field: 'y', type: 'quantitative' }
  }
};

export default function Chart() {
  return <Vega spec={spec} />;
}

Особенность этого подхода заключается в том, что React не управляет графиком напрямую — он лишь передаёт спецификацию, а Vega самостоятельно обновляет визуализацию при изменении входных данных.


Управление Vega-Lite спецификациями

Vega-Lite позволяет писать более компактные описания графиков:

const vlSpec = {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  data: { values: [] },
  mark: 'bar',
  encoding: {
    x: { field: 'category', type: 'nominal' },
    y: { field: 'value', type: 'quantitative' }
  }
};

В React важно учитывать, что любые изменения объекта спецификации приводят к потенциальному пересозданию визуализации. Поэтому используется мемоизация:

import { useMemo } from 'react';
import { VegaLite } from 'react-vega';

function BarChart({ data }) {
  const spec = useMemo(() => ({
    $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
    data: { values: data },
    mark: 'bar',
    encoding: {
      x: { field: 'name', type: 'nominal' },
      y: { field: 'value', type: 'quantitative' }
    }
  }), [data]);

  return <VegaLite spec={spec} />;
}

Использование vega-embed в React

vega-embed предоставляет более низкоуровневый контроль и используется, когда требуется доступ к View или расширенная настройка.

import { useEffect, useRef } from 'react';
import embed from 'vega-embed';

function VegaContainer({ spec }) {
  const containerRef = useRef(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const result = embed(containerRef.current, spec, {
      actions: false
    });

    return () => result.finalize();
  }, [spec]);

  return <div ref={containerRef} />;
}

Ключевой момент — обязательное освобождение ресурсов через finalize(), иначе при частых обновлениях возникает утечка памяти из-за накопления внутренних View экземпляров.


Управление жизненным циклом View

При прямой работе с Vega API создаётся объект View, который необходимо явно контролировать:

import { useEffect, useRef } from 'react';
import * as vega from 'vega';

function RawVega({ spec }) {
  const ref = useRef(null);
  const viewRef = useRef(null);

  useEffect(() => {
    const runtime = vega.parse(spec);

    const view = new vega.View(runtime)
      .renderer('canvas')
      .initialize(ref.current);

    view.run();

    viewRef.current = view;

    return () => view.finalize();
  }, [spec]);

  return <div ref={ref} />;
}

Этот подход используется при необходимости:

  • кастомного рендеринга
  • интеграции с внешними canvas/WebGL слоями
  • контроля событий низкого уровня

Обновление данных без пересоздания графика

Частая проблема — полное пересоздание View при каждом изменении данных. Vega позволяет обновлять dataset напрямую:

useEffect(() => {
  if (!viewRef.current) return;

  viewRef.current
    .change('table', vega.changeset().remove(() => true).insert(data))
    .run();
}, [data]);

Такой подход значительно повышает производительность при частых обновлениях (стриминг, дашборды, realtime аналитика).


Работа с событиями

Vega поддерживает систему событий, которая легко интегрируется с React:

useEffect(() => {
  if (!viewRef.current) return;

  viewRef.current.addSignalListener('hover', (name, value) => {
    console.log(name, value);
  });
}, []);

Также можно подписываться на клики по маркерам:

viewRef.current.addEventListener('click', (event, item) => {
  console.log(item.datum);
});

При этом важно учитывать, что события живут вне React-цикла, поэтому синхронизация с состоянием требует аккуратного использования useCallback и useRef.


Интеграция с состоянием React

Типовой паттерн — связывание состояния с Vega spec:

function Dashboard() {
  const [filter, setFilter] = useState('A');

  const spec = useMemo(() => ({
    data: { values: getData(filter) },
    mark: 'line',
    encoding: {
      x: { field: 'x', type: 'quantitative' },
      y: { field: 'y', type: 'quantitative' }
    }
  }), [filter]);

  return (
    <>
      <button onCl ick={() => setFilter('A')}>A</button>
      <button onCl ick={() => setFilter('B')}>B</button>
      <Vega spec={spec} />
    </>
  );
}

Основной принцип — спецификация должна быть функцией состояния, а не мутируемым объектом.


Оптимизация ререндеров

При работе с Vega в React критично избегать:

  • создания новой spec на каждый render
  • передачи нестабильных ссылок
  • глубоких пересборок объектов данных

Используются техники:

useMemo для spec

const spec = useMemo(() => buildSpec(data), [data]);

стабилизация данных

const stableData = useMemo(() => data, [JSON.stringify(data)]);

разделение слоёв

Вместо одного большого spec используется композиция:

  • отдельные datasets
  • переиспользуемые mark-слои
  • внешние сигналы

Resize и адаптивность

Vega требует явного пересчёта размеров:

useEffect(() => {
  if (!viewRef.current) return;

  viewRef.current.resize().run();
}, [width, height]);

В React часто используется ResizeObserver:

useEffect(() => {
  const ro = new ResizeObserver(([entry]) => {
    setSize({
      width: entry.contentRect.width,
      height: entry.contentRect.height
    });
  });

  ro.observe(ref.current);

  return () => ro.disconnect();
}, []);

Темизация и стилизация

Vega поддерживает темы через конфигурацию:

const spec = {
  config: {
    axis: {
      labelColor: '#666',
      titleColor: '#333'
    },
    style: {
      cell: {
        stroke: '#ddd'
      }
    }
  }
};

В React удобно хранить тему отдельно:

const theme = useContext(ThemeContext);

const spec = useMemo(() => ({
  config: theme.vegaConfig,
  data: { values: data },
  mark: 'bar'
}), [theme, data]);

SSR и проблемы гидратации

Vega напрямую зависит от DOM и canvas, поэтому серверный рендеринг требует изоляции:

const isClient = typeof window !== 'undefined';

return isClient ? <Vega spec={spec} /> : null;

При использовании Next.js или аналогов графики обычно рендерятся только на клиенте.


Архитектура сложных дашбордов

При масштабировании приложений используется следующая структура:

  • слой данных (API / WebSocket)
  • слой трансформации (selectors / memoized builders)
  • слой спецификаций Vega
  • слой визуализации React

Пример разделения:

const useChartSpec = (data) =>
  useMemo(() => ({
    data: { values: transform(data) },
    mark: 'area',
    encoding: {
      x: { field: 'time', type: 'temporal' },
      y: { field: 'value', type: 'quantitative' }
    }
  }), [data]);

Типизация Vega-Lite в TypeScript

import { VisualizationSpec } from 'vega-embed';

const spec: VisualizationSpec = {
  mark: 'line',
  data: { values: [] }
};

Типизация помогает избегать ошибок в encoding и структуре данных, особенно при динамическом построении графиков.


Потоковые данные и realtime обновления

Vega хорошо подходит для потоковых систем:

useEffect(() => {
  const interval = setInterval(() => {
    setData(prev => [...prev.slice(-50), generatePoint()]);
  }, 1000);

  return () => clearInterval(interval);
}, []);

И затем обновление через changeset без пересоздания view обеспечивает стабильную производительность даже при высокой частоте обновлений.


Ошибки и отладка

Типичные проблемы:

  • несоответствие полей данных encoding
  • пересоздание spec без мемоизации
  • утечки View при отсутствии finalize
  • конфликт canvas и CSS размеров

Для диагностики полезно включать:

vega.debug.enable(true);

и анализировать runtime через встроенные инструменты Vega Inspector.