В экосистеме Vega сигналы представляют собой реактивные переменные, которые управляют параметрами визуализации и обновляются при изменении входных данных, событий мыши, размеров контейнера и других источников состояния. Их ключевая особенность — возможность взаимного использования значений через выражения, формируя граф зависимостей внутри спецификации.
Сигналы в Vega-Lite существуют опосредованно: они компилируются в низкоуровневые сигналы Vega, однако логика взаимодействия сохраняет ту же модель — вычисление значений на основе других реактивных источников.
Чтение одного сигнала из другого реализуется через выражения сигнального языка. В Vega сигналы доступны по имени в контексте выражения или через функцию доступа:
{
"signals": [
{ "name": "a", "value": 10 },
{ "name": "b", "update": "a * 2" }
]
}
Здесь сигнал b зависит от a. При изменении
a автоматически пересчитывается b.
Другой вариант явного обращения:
{
"name": "c",
"update": "signal('a') + 5"
}
Функция signal('a') делает зависимость явной на уровне
выражения, что полезно при динамическом доступе по имени.
Сигналы читаются внутри выражений, которые интерпретируются движком Vega Expression Grammar. Внутри таких выражений доступно прямое обращение по имени:
{
"signals": [
{ "name": "widthFactor", "value": 2 },
{ "name": "height", "update": "widthFactor * 100" }
]
}
При этом система автоматически строит граф зависимостей:
height зависит от widthFactorwidthFactor инициируют пересчёт
heightПодобная модель исключает необходимость ручного управления порядком вычислений.
Существует два способа чтения сигналов:
"update": "a + b"
Зависимости выводятся анализом выражения. Такой подход компактный, но требует корректного статического анализа строки выражения.
"update": "signal('a') + signal('b')"
Этот способ снижает неоднозначность при сложных сценариях, особенно когда имя сигнала вычисляется динамически.
Сигналы могут образовывать цепочки произвольной глубины:
{
"signals": [
{ "name": "a", "value": 1 },
{ "name": "b", "update": "a + 1" },
{ "name": "c", "update": "b + 1" },
{ "name": "d", "update": "c + 1" }
]
}
Здесь формируется направленный ациклический граф:
a → b → c → d
Каждое изменение источника приводит к каскадному пересчёту зависимых значений.
Сигналы часто используются внутри интерактивных обработчиков событий:
{
"signals": [
{
"name": "mouseX",
"value": 0,
"on": [
{
"events": "mousemove",
"update": "x()"
}
]
},
{
"name": "normalized",
"update": "mouseX / width"
}
]
}
Здесь normalized читает значение mouseX,
которое обновляется событиями мыши. Это создаёт реактивный поток данных,
в котором события преобразуются в сигналы, а сигналы — в вычисляемые
значения.
Сигналы активно используются внутри шкал и трансформаций:
{
"signals": [
{ "name": "padding", "value": 20 }
],
"scales": [
{
"name": "x",
"padding": { "signal": "padding" }
}
]
}
Здесь сигнал padding используется как реактивный
параметр масштаба. Изменение сигнала приводит к перерасчёту шкалы и всей
визуализации.
В Vega-Lite прямое управление сигналами ограничено, однако через
параметры (params) создаются аналогичные зависимости:
{
"params": [
{
"name": "threshold",
"value": 50
}
],
"mark": "bar",
"encoding": {
"opacity": {
"condition": {
"test": "datum.value > threshold",
"value": 1
},
"value": 0.3
}
}
}
После компиляции threshold превращается в сигнал Vega,
который может быть использован в выражениях и взаимодействиях.
Одной из наиболее мощных возможностей является доступ к сигналам через динамические ключи:
{
"signals": [
{ "name": "x0", "value": 10 },
{ "name": "x1", "value": 20 },
{ "name": "index", "value": 1 },
{ "name": "selected", "update": "signal('x' + index)" }
]
}
Здесь selected вычисляется на основе конкатенации строки
и имени сигнала. Такая конструкция позволяет реализовывать:
При построении графа зависимостей система учитывает порядок:
Если сигнал зависит от другого сигнала, пересчёт всегда происходит снизу вверх по графу зависимостей.
Циклические зависимости запрещены:
{
"name": "a",
"update": "b + 1"
},
{
"name": "b",
"update": "a + 1"
}
Такая конструкция приводит к ошибке вычисления графа.
В transform-слое сигналы часто используются для фильтрации и вычислений:
{
"transform": [
{
"type": "filter",
"expr": "datum.value > threshold"
}
],
"signals": [
{ "name": "threshold", "value": 100 }
]
}
Выражение expr получает доступ к сигналам как к
глобальным переменным контекста.
Сигналы часто выступают как промежуточный слой абстракции:
{
"signals": [
{ "name": "base", "value": 10 },
{ "name": "scale", "value": 2 },
{ "name": "result", "update": "base * scale" },
{ "name": "adjusted", "update": "result + base" }
]
}
Такая композиция позволяет:
Сигналы активно применяются для управления геометрией визуализации:
{
"signals": [
{ "name": "margin", "value": 30 },
{ "name": "innerWidth", "update": "width - margin * 2" },
{ "name": "innerHeight", "update": "height - margin * 2" }
]
}
Любое изменение размеров контейнера автоматически приводит к пересчёту всех производных сигналов.
При проектировании систем на основе сигналов учитываются следующие ограничения:
Эти ограничения обеспечивают детерминированность реактивной системы и предсказуемость визуализаций.