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

Интеграция декларативных визуализационных спецификаций Vega и Vega-Lite во фронтенд-экосистему Vue строится вокруг принципа отделения описания графика от жизненного цикла компонента. Vue отвечает за реактивность и DOM-обновления, Vega — за рендеринг сцены, Vega-Lite — за компиляцию высокоуровневых спецификаций в Vega runtime.

Ключевой архитектурный слой состоит из трёх элементов:

  • реактивные данные Vue (state)
  • спецификация Vega / Vega-Lite (declarative spec)
  • экземпляр View Vega (runtime renderer)

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


Базовый паттерн: обёртка над Vega View

Vega предоставляет объект View, который управляет отрисовкой, масштабированием и обработкой событий. В Vue он обычно инкапсулируется в компонент.

import { parse } from 'vega';
import { View } from 'vega';
import { onMounted, onBeforeUnmount, watch, ref } from 'vue';

export default {
  props: {
    spec: Object,
    data: Array
  },
  setup(props) {
    const container = ref(null);
    let view = null;

    const init = () => {
      const runtime = parse(props.spec);
      view = new View(runtime)
        .renderer('canvas')
        .initialize(container.value)
        .hover();

      view.runAsync();
    };

    onMounted(init);

    onBeforeUnmount(() => {
      if (view) view.finalize();
    });

    watch(
      () => [props.spec, props.data],
      () => {
        if (!view) return;
        view.change('table', vega.changeset().remove(() => true).insert(props.data));
        view.runAsync();
      },
      { deep: true }
    );

    return { container };
  }
};

Основной принцип: Vue не управляет отрисовкой, он только триггерит обновление данных.


Интеграция через Vega-Lite: компиляционный слой

Vega-Lite добавляет промежуточный этап компиляции спецификации в Vega runtime.

import vegaEmbed from 'vega-embed';
import { onMounted, ref, watch } from 'vue';

export default {
  props: {
    spec: Object
  },
  setup(props) {
    const container = ref(null);
    let viewInstance = null;

    const render = async () => {
      const result = await vegaEmbed(container.value, props.spec, {
        actions: false
      });

      viewInstance = result.view;
    };

    onMounted(render);

    watch(
      () => props.spec,
      async () => {
        if (viewInstance) {
          await render();
        }
      },
      { deep: true }
    );

    return { container };
  }
};

Ключевое отличие: Vega-Lite управляет трансформацией данных и генерацией Vega runtime автоматически, что снижает контроль, но увеличивает скорость разработки.


Реактивная модель данных Vue и синхронизация с Vega dataset

Vega использует именованные наборы данных (data sources), которые можно обновлять без полной перерисовки сцены.

watch(
  () => props.data,
  (newData) => {
    if (!view) return;

    view.change(
      'table',
      vega.changeset().remove(() => true).insert(newData)
    );

    view.runAsync();
  },
  { deep: true }
);

Важно, что идентификатор 'table' должен совпадать с именем dataset в спецификации:

{
  "data": [
    {
      "name": "table",
      "values": []
    }
  ],
  "mark": "bar",
  "encoding": {
    "x": { "field": "category", "type": "ordinal" },
    "y": { "field": "value", "type": "quantitative" }
  }
}

Инкапсуляция через Composition API

Современный подход во Vue предполагает вынос логики Vega в composable.

import { ref, onMounted, onBeforeUnmount } from 'vue';
import { parse } from 'vega';
import { View } from 'vega';

export function useVega(specRef) {
  const container = ref(null);
  let view = null;

  const render = () => {
    const runtime = parse(specRef.value);

    view = new View(runtime)
      .renderer('canvas')
      .initialize(container.value);

    view.runAsync();
  };

  onMounted(render);

  onBeforeUnmount(() => {
    if (view) view.finalize();
  });

  return { container };
}

Такой подход отделяет инфраструктуру визуализации от UI-компонентов.


Работа с событиями Vega внутри Vue

Vega поддерживает систему событий, которая может быть связана с реактивными состояниями Vue.

view.addSignalListener('hovered', (name, value) => {
  hoveredPoint.value = value;
});

При этом сигналы Vega становятся мостом между декларативной визуализацией и реактивным состоянием приложения.

Типичный сценарий:

  • Vega генерирует сигнал (signal)
  • Vue хранит реактивное значение (ref)
  • UI реагирует на изменение состояния

Динамическое обновление спецификации Vega-Lite

В случае Vega-Lite частой задачей является полная пересборка спецификации при изменении фильтров или параметров.

watch(
  () => [props.filter, props.metric],
  () => {
    const newSpec = {
      ...props.spec,
      transform: [
        {
          filter: `datum.category == '${props.filter}'`
        }
      ],
      encoding: {
        ...props.spec.encoding,
        y: { field: props.metric, type: 'quantitative' }
      }
    };

    vegaEmbed(container.value, newSpec, { actions: false });
  }
);

Такая модель проще, но менее производительна по сравнению с низкоуровневым Vega API.


Оптимизация перерисовки и управление производительностью

При интеграции с Vega критично избегать полного пересоздания View.

Основные техники оптимизации:

1. Обновление dataset вместо rerender

  • используется view.change()

2. Debounce реактивных изменений

import debounce from 'lodash/debounce';

const update = debounce((data) => {
  view.change('table', vega.changeset().insert(data));
  view.runAsync();
}, 100);

3. Изоляция больших спецификаций

  • разделение визуализаций на несколько независимых компонентов Vue

Интеграция событий мыши и DOM overlay

Vega может работать в canvas-режиме, но Vue часто требует HTML-оверлеи.

view.addEventListener('click', (event, item) => {
  tooltip.value = {
    x: event.clientX,
    y: event.clientY,
    data: item.datum
  };
});

HTML-слой Vue накладывается поверх canvas, обеспечивая UI-уровень взаимодействия.


Гибридная архитектура: Vega + Vue state management

В сложных приложениях используется связка:

  • Vue (Pinia / reactive store)
  • Vega runtime
  • Vega-Lite spec generator

Поток данных:

  1. Store обновляет состояние
  2. Computed строит Vega-Lite spec
  3. Vega компилирует runtime
  4. View рендерит результат
const spec = computed(() => ({
  mark: 'line',
  data: { values: store.data },
  encoding: {
    x: { field: 'time', type: 'temporal' },
    y: { field: 'value', type: 'quantitative' }
  }
}));

Разделение ответственности между слоями

Во взаимодействии Vue и Vega/Vega-Lite формируется строгая градация:

  • Vue: состояние, UI, события
  • Vega-Lite: декларация визуализации
  • Vega: рендеринг и runtime execution

Такое разделение устраняет необходимость прямого DOM-манипулирования и снижает связность компонентов.


Типовые ошибки интеграции

На практике часто возникают архитектурные проблемы:

1. Двойной контроль DOM

  • попытка Vue управлять canvas Vega напрямую

2. Пересоздание View при каждом обновлении

  • приводит к утечкам памяти

3. Несогласованность dataset names

  • Vega не находит обновляемый источник данных

4. Неправильная реактивность spec

  • глубокие watch без оптимизации приводят к деградации производительности

Событийная модель и двусторонняя синхронизация

Vega сигналы могут быть использованы как источник обратной связи:

view.signal('xScale', (name, value) => {
  store.setScale(value);
});

Таким образом формируется замкнутый цикл:

Vue → Vega spec → Vega View → signals → Vue state


Использование модульной архитектуры визуализаций

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

  • barChartSpec()
  • lineChartSpec()
  • scatterPlotSpec()

Каждый модуль возвращает чистую Vega-Lite спецификацию, не зависящую от Vue.

export function barChartSpec(data) {
  return {
    mark: 'bar',
    data: { values: data },
    encoding: {
      x: { field: 'x', type: 'ordinal' },
      y: { field: 'y', type: 'quantitative' }
    }
  };
}

Итоговая модель взаимодействия слоёв

  • Vue управляет реактивностью и жизненным циклом компонентов
  • Vega-Lite определяет структуру визуализации
  • Vega выполняет рендеринг и обработку интерактивности
  • связка работает через dataset updates, signals и пересборку spec

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