Функции преобразования данных и пользовательская логика в Vega и Vega-Lite строятся вокруг расширяемой системы выражений, трансформаций и сигналов. Архитектура ориентирована на декларативное описание визуализаций, однако допускает внедрение императивных вычислений в строго определённых точках расширения, что позволяет адаптировать систему под нестандартные сценарии анализа и рендеринга данных.
В основе вычислений Vega лежит язык выражений, применяемый в сигналах, шкалах, вычисляемых полях и параметрах визуальных компонентов. Этот язык компилируется и исполняется через модуль выражений, обеспечивающий безопасное выполнение ограниченного набора операций.
Расширение набора функций происходит через регистрацию пользовательских выражений. В рантайме Vega можно добавлять новые функции в глобальное пространство выражений:
import { expressionFunction } from "vega-expression";
expressionFunction("clamp01", (x) => {
return Math.max(0, Math.min(1, x));
});
После регистрации функция становится доступной в любом выражении спецификации:
{
"type": "formula",
"expr": "clamp01(datum.value / 100)"
}
Ключевая особенность механизма заключается в том, что пользовательские функции интегрируются в тот же интерпретатор, что и встроенные операции. Это означает отсутствие различий в синтаксисе между стандартными и расширенными функциями, но при этом сохраняется контроль над безопасностью исполнения.
Система выражений Vega работает в изолированной среде. Пользовательские функции не имеют доступа к DOM, глобальному объекту браузера или произвольным модулям JavaScript. Передача данных осуществляется только через параметры выражения.
Это накладывает архитектурное ограничение: функции должны быть чистыми, детерминированными и не иметь побочных эффектов. Любая попытка работы с внешним состоянием должна быть вынесена на уровень приложения, а не выражений Vega.
Типичная ошибка при проектировании расширений — попытка реализовать асинхронную логику внутри выражений. Vega не поддерживает Promise внутри expression API, поэтому все вычисления должны быть синхронными.
Помимо выражений, основной механизм расширения логики обработки данных — пользовательские трансформации. Vega использует систему трансформов как цепочку операций над потоками данных, где каждый шаг принимает массив входных значений и возвращает модифицированный массив.
Базовый интерфейс кастомного трансформа:
import { Transform } from "vega-dataflow";
class MovingAverage extends Transform {
constructor(params) {
super(params);
}
transform(_, pulse) {
const window = this.param("window") || 5;
const values = pulse.source;
for (let i = 0; i < values.length; i++) {
let sum = 0;
let count = 0;
for (let j = Math.max(0, i - window + 1); j <= i; j++) {
sum += values[j].value;
count++;
}
values[i].avg = sum / count;
}
return pulse.reflow();
}
}
После реализации трансформация регистрируется в системе Vega:
import { register } from "vega-dataflow";
register("movingAverage", MovingAverage);
Использование в спецификации Vega:
{
"type": "movingAverage",
"window": 10
}
Такая модель позволяет внедрять сложные алгоритмы обработки данных: фильтрацию временных рядов, кластеризацию, агрегации, пользовательские статистические методы.
Vega-Lite как декларативный слой не поддерживает прямое описание классов трансформаций, но транслирует высокоуровневые конструкции в Vega-spec. Пользовательские трансформации подключаются на уровне конфигурации компиляции или через расширение рантайма Vega.
Типовой сценарий интеграции:
import vegaEmbed from "vega-embed";
import { register } from "vega-dataflow";
register("movingAverage", MovingAverage);
const spec = {
data: { values: dataset },
transform: [
{
type: "movingAverage",
window: 7
}
],
mark: "line",
encoding: {
x: { field: "date", type: "temporal" },
y: { field: "avg", type: "quantitative" }
}
};
vegaEmbed("#vis", spec);
Таким образом, Vega-Lite остаётся декларативным интерфейсом, а расширения реализуются на уровне движка исполнения.
Сигналы в Vega представляют реактивную систему переменных, которые могут зависеть от данных, взаимодействия пользователя и других сигналов. Через сигналы часто реализуется пользовательская логика, связанная с интерактивностью.
Пользовательские функции могут использоваться внутри выражений сигналов:
{
"signals": [
{
"name": "normalizedValue",
"update": "clamp01(mouseX / width)"
}
]
}
Сигналы пересчитываются при изменении зависимостей, формируя реактивный граф вычислений. Это позволяет строить интерактивные визуализации без явного управления состоянием.
Помимо программной регистрации функций, Vega поддерживает добавление функций через конфигурацию окружения исполнения. В некоторых сборках Vega Expression Engine возможно определить набор пользовательских функций при инициализации:
import { compile } from "vega-expression";
const expr = compile("scaleValue(x)", {
functions: {
scaleValue: (x) => x * 10
}
});
Подобный подход используется при изоляции окружений или внедрении ограниченного набора вычислений в sandbox-среде.
Vega и Vega-Lite используют JSON как основной формат описания визуализаций. Это накладывает ограничение: пользовательские функции не сериализуются вместе со спецификацией.
Следствие этого ограничения:
Такое разделение делает невозможным “самодостаточный” JSON с произвольным JavaScript-кодом, что является осознанным архитектурным решением.
Одним из наиболее частых сценариев является использование
пользовательских функций в вычисляемых полях (formula
transform):
{
"type": "formula",
"as": "score",
"expr": "normalize(datum.raw)"
}
Здесь normalize — заранее зарегистрированная функция.
Подобный подход используется для:
Пользовательские функции часто комбинируются с агрегирующими трансформациями. В этом случае важно учитывать порядок выполнения операций в dataflow-графе Vega.
Например, сначала выполняется агрегация:
{
"type": "aggregate",
"groupby": ["category"],
"fields": ["value"],
"ops": ["sum"],
"as": ["total"]
}
После этого применяется пользовательская функция:
{
"type": "formula",
"expr": "scaleTotal(datum.total)"
}
Такая последовательность обеспечивает корректную работу функций, зависящих от агрегированных значений.
Так как Vega выполняет вычисления в режиме dataflow-графа, пользовательские функции становятся частью критического пути исполнения. Их производительность напрямую влияет на:
Поэтому вычисления должны быть минимальными по сложности, избегать вложенных циклов при больших объёмах данных и по возможности переносить тяжёлую обработку в этап подготовки данных вне Vega.
Расширения в Vega обычно группируются в несколько устойчивых паттернов:
Каждый из этих подходов решает разные задачи, но все они объединены единым dataflow-движком, в котором пользовательская логика становится частью графа вычислений, а не внешним управляющим кодом.