Архитектура интеграции Vega и Angular строится вокруг изолированного визуального слоя, который получает декларативную спецификацию графика и обновляется через жизненный цикл компонентов. Основной принцип — отделение описания визуализации от механики рендера, что полностью соответствует реактивной модели Angular.
Интеграция начинается с подключения библиотек визуализации:
npm install vega vega-lite vega-embed
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, который представляет
активный рендеринг графика. При повторной передаче спецификации возможны
два подхода:
В Angular-практике часто используется комбинированный подход:
spec → пересозданиеdata → инкрементальное обновлениеИнтеграция с потоками данных позволяет синхронизировать визуализацию с состоянием приложения.
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 и синхронизацией состояния.
При использовании Vega важно учитывать стратегию обнаружения изменений.
Оптимальная конфигурация:
ChangeDetectionStrategy.OnPushimport { 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();
}
}
Разделение логики визуализации и представления повышает переиспользуемость.
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' }
}
};
}
}
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 при каждом измененииМетоды оптимизации:
trackBy при списках графиков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 spec:
import { compile } from 'vega-lite';
const vegaSpec = compile(vlSpec).spec;
Это позволяет использовать более высокоуровневое описание и сохранять совместимость с Vega runtime.
В крупных приложениях визуализация выделяется в отдельный модуль:
/visualization
/components
/services
/factories
/models
Модуль экспортирует:
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' }
}
};
При использовании Angular Universal возникают ограничения:
Решение — условная инициализация:
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.
Используются следующие подходы:
Vega легко комбинируется с:
Пример интеграции с NgRx:
this.store.select(selectChartData).subscribe(data => {
this.spec = this.specFactory.createBarChart(data);
});
Vega поддерживает интерактивность через signals:
{
"signals": [
{ "name": "hover", "on": [{ "events": "rect:mouseover", "update": "datum" }] }
]
}
Angular при этом не вмешивается в runtime, ограничиваясь передачей спецификации, что сохраняет разделение ответственности между UI и визуализацией.