В основе интерактивности в Vega лежит модель сигналов — реактивных переменных, которые управляют состоянием визуализации. Сигналы реагируют на события DOM, действия пользователя, вычисления выражений и изменения данных. Каждый сигнал представляет собой узел реактивного графа, где изменение одного значения может каскадно обновлять элементы сцены.
Сигналы в Vega описываются декларативно и могут быть связаны с
событиями через поле on, которое задаёт поток событий и
способ обновления значения.
Пример базового сигнала:
{
"signals": [
{
"name": "xPos",
"value": 0,
"on": [
{
"events": "mousemove",
"update": "x()"
}
]
}
]
}
В данном случае каждое движение мыши инициирует пересчёт значения сигнала. Это простая модель, но она становится источником проблем при высокой частоте событий.
События вроде mousemove, touchmove,
scroll или wheel генерируются с высокой
частотой — десятки или сотни раз в секунду. Если каждое событие напрямую
вызывает пересчёт сигналов и перерисовку сцены, возникает несколько
проблем:
Особенно критично это проявляется в Vega-Lite, где декларативные конструкции компилируются в Vega-спецификацию, а взаимодействия автоматически создают дополнительные сигналы и трансформации.
Для управления частотой обновлений используется механизм debounce — задержка обработки событий до момента, когда поток событий стабилизируется.
В Vega debounce задаётся прямо внутри определения события сигнала:
{
"signals": [
{
"name": "hoverX",
"value": 0,
"on": [
{
"events": "mousemove",
"update": "x()",
"debounce": 50
}
]
}
]
}
Значение debounce: 50 означает, что обновление сигнала
произойдёт только после того, как события перестанут поступать в течение
50 миллисекунд. Это существенно снижает количество вычислений при
непрерывных движениях курсора.
Механизм работает как буферизация потока событий: пока новые события продолжают приходить, обновление откладывается.
Хотя debounce и throttle часто рассматриваются вместе, в Vega применяется именно debounce-модель:
Vega не предоставляет прямого throttle как отдельного параметра сигналов, но аналогичное поведение можно имитировать через выражения времени и сравнение timestamp.
При построении интерактивных графиков debounce применяется не только к координатам курсора, но и к более сложным сценариям:
Пример debounce для выделения области:
{
"signals": [
{
"name": "brush",
"value": null,
"on": [
{
"events": "mousedown, mousemove, mouseup",
"update": "invert('xscale', [x(), x()])",
"debounce": 30
}
]
}
]
}
В этом случае debounce предотвращает чрезмерное обновление диапазона выделения при каждом движении мыши, сохраняя плавность и снижая нагрузку на вычисление фильтров.
Сигналы в Vega формируют направленный граф зависимостей. При отсутствии debounce каждое событие может инициировать пересчёт всей цепочки зависимых сигналов.
Типичный каскад:
Debounce разрывает этот поток на уровне входного сигнала, уменьшая частоту активации всей цепочки.
Vega-Lite генерирует сигналы автоматически при использовании интерактивных примитивов:
selection (single, multi, interval);bind (инпуты HTML);tooltip;zoom и pan.При компиляции в Vega эти конструкции превращаются в набор сигналов с событиями. Без оптимизации они могут вызывать избыточные обновления даже при минимальных взаимодействиях.
Добавление debounce в сгенерированные сигналы позволяет:
Debounce часто используется вместе с выражениями Vega Expression Language. Например, вычисление нормализованных координат:
{
"signals": [
{
"name": "normX",
"on": [
{
"events": "mousemove",
"update": "clamp(x()/width, 0, 1)",
"debounce": 40
}
]
}
]
}
Здесь debounce снижает частоту пересчёта нормализованного значения, которое может использоваться в нескольких слоях визуализации.
Выбор значения debounce зависит от характера взаимодействия:
В сложных визуализациях часто применяется неоднородная стратегия: быстрые сигналы (позиции курсора) обновляются с малым debounce, а тяжёлые сигналы (фильтрация, агрегация) — с увеличенным.
При работе напрямую через API View (инстанс визуализации
Vega) debounce не добавляется автоматически, но поведение сигналов
остаётся тем же:
view.signal("hoverX", value).runAsync();
Если сигнал определён с debounce, Vega сама управляет частотой
обновлений, а runAsync лишь инициирует пересчёт реактивного
графа.
В практических реализациях часто применяются следующие подходы:
group)
для изоляции пересчётов.Такая структура позволяет избежать глобальных пересборок сцены при каждом движении мыши и сохраняет предсказуемость реактивного поведения.