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

Архитектура интеграции Vega и Angular строится вокруг изолированного визуального слоя, который получает декларативную спецификацию графика и обновляется через жизненный цикл компонентов. Основной принцип — отделение описания визуализации от механики рендера, что полностью соответствует реактивной модели Angular.


Установка и базовая подготовка окружения

Интеграция начинается с подключения библиотек визуализации:

npm install vega vega-lite vega-embed
  • vega — ядро визуализации и runtime для исполнения спецификаций
  • vega-lite — декларативный уровень описания графиков
  • vega-embed — слой интеграции с DOM и удобный API для встраивания

Angular-проект при этом остаётся стандартным приложением с компонентной структурой и системой DI.


Базовый компонент визуализации

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

import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import embed, { EmbedOptions } from 'vega-embed';
import { VisualizationSpec } from 'vega-typings';

@Component({
  selector: 'app-vega-chart',
  template: `<div #container></div>`,
  changeDetection: 0
})
export class VegaChartComponent implements AfterViewInit, OnChanges, OnDestroy {
  @ViewChild('container', { static: true }) container!: ElementRef;

  @Input() spec!: VisualizationSpec;
  @Input() options?: EmbedOptions;

  private view: any;

  ngAfterViewInit(): void {
    this.render();
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['spec'] && !changes['spec'].firstChange) {
      this.render();
    }
  }

  private async render(): Promise<void> {
    if (!this.container) return;

    const result = await embed(this.container.nativeElement, this.spec, {
      actions: false,
      ...this.options
    });

    this.view = result.view;
  }

  ngOnDestroy(): void {
    if (this.view) {
      this.view.finalize();
    }
  }
}

Режимы обновления и управление жизненным циклом

Vega использует объект View, который представляет активный рендеринг графика. При повторной передаче спецификации возможны два подхода:

  1. Полный пересоздание визуализации
  2. Частичное обновление данных через API View

В Angular-практике часто используется комбинированный подход:

  • изменение spec → пересоздание
  • изменение data → инкрементальное обновление

Реактивная модель через RxJS

Интеграция с потоками данных позволяет синхронизировать визуализацию с состоянием приложения.

import { Component, Input } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { VisualizationSpec } from 'vega-typings';

@Component({
  selector: 'app-stream-chart',
  template: `<app-vega-chart [spec]="spec$ | async"></app-vega-chart>`
})
export class StreamChartComponent {
  private specSubject = new BehaviorSubject<VisualizationSpec | null>(null);

  spec$ = this.specSubject.asObservable();

  @Input() set data(value: any[]) {
    this.specSubject.next(this.buildSpec(value));
  }

  private buildSpec(data: any[]): VisualizationSpec {
    return {
      $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
      data: { values: data },
      mark: 'line',
      encoding: {
        x: { field: 'x', type: 'quantitative' },
        y: { field: 'y', type: 'quantitative' }
      }
    };
  }
}

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


Интеграция с Angular Change Detection

При использовании Vega важно учитывать стратегию обнаружения изменений.

Оптимальная конфигурация:

  • ChangeDetectionStrategy.OnPush
  • обновление входных данных через immutable структуры
  • минимизация пересоздания спецификаций
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  selector: 'app-optimized-vega',
  template: `<app-vega-chart [spec]="spec"></app-vega-chart>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedVegaComponent {
  spec = {};
}

Работа с динамическими данными

Vega поддерживает обновление данных без полного пересоздания визуализации через API:

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

В Angular-интеграции это обычно инкапсулируется в сервис:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class VegaViewService {
  private view: any;

  register(view: any): void {
    this.view = view;
  }

  updateData(name: string, data: any[]): void {
    if (!this.view) return;

    this.view.change(
      name,
      (window as any).vega.changeset().remove(() => true).insert(data)
    );

    this.view.run();
  }
}

Инкапсуляция Vega в Angular Service Layer

Разделение логики визуализации и представления повышает переиспользуемость.

import { Injectable } from '@angular/core';
import { VisualizationSpec } from 'vega-typings';

@Injectable({ providedIn: 'root' })
export class VegaSpecFactory {
  createBarChart(data: any[]): VisualizationSpec {
    return {
      $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
      data: { values: data },
      mark: 'bar',
      encoding: {
        x: { field: 'category', type: 'nominal' },
        y: { field: 'value', type: 'quantitative' }
      }
    };
  }
}

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

Vega-спецификации часто зависят от пользовательских параметров, получаемых через Reactive Forms.

form.valueChanges.subscribe(params => {
  this.spec = {
    data: { values: this.data },
    mark: params.type,
    encoding: {
      x: { field: params.xField, type: 'quantitative' },
      y: { field: params.yField, type: 'quantitative' }
    }
  };
});

Это создаёт динамическую генерацию визуализаций на основе состояния формы.


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

Основные узкие места интеграции:

  • пересоздание View при каждом изменении
  • избыточные Angular change detection циклы
  • большие JSON-spec объекты

Методы оптимизации:

  • мемоизация спецификаций
  • использование trackBy при списках графиков
  • вынесение Vega View из Angular зоны
import { NgZone } from '@angular/core';

constructor(private zone: NgZone) {}

private render(): void {
  this.zone.runOutsideAngular(async () => {
    const result = await embed(this.container.nativeElement, this.spec);
    this.view = result.view;
  });
}

Поддержка масштабирования и ресайза

Vega не всегда автоматически адаптируется к изменениям контейнера, поэтому требуется явный пересчёт размеров.

window.addEventListener('resize', () => {
  if (this.view) {
    this.view.resize().run();
  }
});

В Angular-компонентах это обычно синхронизируется с ResizeObserver.


Использование Vega-Lite трансляции

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

import { compile } from 'vega-lite';

const vegaSpec = compile(vlSpec).spec;

Это позволяет использовать более высокоуровневое описание и сохранять совместимость с Vega runtime.


Интеграция с модульной архитектурой Angular

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

/visualization
  /components
  /services
  /factories
  /models

Модуль экспортирует:

  • универсальный компонент отображения
  • фабрики спецификаций
  • сервис управления состоянием View

Типизация спецификаций

TypeScript играет ключевую роль в предотвращении ошибок в Vega-конфигурациях.

import { TopLevelSpec } from 'vega-lite';

const spec: TopLevelSpec = {
  data: { values: [] },
  mark: 'line',
  encoding: {
    x: { field: 'x', type: 'quantitative' },
    y: { field: 'y', type: 'quantitative' }
  }
};

Интеграция с серверным рендерингом (SSR)

При использовании Angular Universal возникают ограничения:

  • DOM недоступен
  • Vega View не может быть создан

Решение — условная инициализация:

import { isPlatformBrowser } from '@angular/common';

constructor(@Inject(PLATFORM_ID) private platformId: Object) {}

ngAfterViewInit(): void {
  if (isPlatformBrowser(this.platformId)) {
    this.render();
  }
}

Обработка ошибок и устойчивость

Vega может выбрасывать ошибки при некорректных данных или спецификациях.

try {
  await embed(el, spec);
} catch (e) {
  console.error('Vega render error', e);
}

В промышленной интеграции ошибки часто маршрутизируются в глобальный error handler Angular.


Архитектурные паттерны интеграции

Используются следующие подходы:

  • Wrapper Component Pattern — изоляция Vega в компоненте
  • Factory Pattern — генерация спецификаций
  • Facade Pattern — управление View через сервис
  • Reactive Pattern — потоковые данные через RxJS
  • Immutable State Pattern — предотвращение лишних ререндеров

Взаимодействие с внешними библиотеками данных

Vega легко комбинируется с:

  • D3 (кастомные трансформации данных до spec)
  • NgRx (глобальное состояние графиков)
  • Apollo GraphQL (стриминг данных)

Пример интеграции с NgRx:

this.store.select(selectChartData).subscribe(data => {
  this.spec = this.specFactory.createBarChart(data);
});

Механика встроенных действий Vega

Vega поддерживает интерактивность через signals:

{
  "signals": [
    { "name": "hover", "on": [{ "events": "rect:mouseover", "update": "datum" }] }
  ]
}

Angular при этом не вмешивается в runtime, ограничиваясь передачей спецификации, что сохраняет разделение ответственности между UI и визуализацией.