Поле update: динамическое обновление

Сигналы как основа реактивности

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

Сигнал в Vega имеет структуру:

{
  "name": "width",
  "value": 400,
  "update": "containerSize()[0]"
}

Поле update определяет выражение, которое пересчитывает значение сигнала при изменении зависимостей. В отличие от value, которое задаёт начальное состояние, update делает сигнал динамическим.

Ключевой принцип:

  • value — фиксированная инициализация
  • update — реактивное пересчитываемое выражение

Семантика поля update

Поле update представляет собой выражение на языке выражений Vega (expression language), которое может включать:

  • арифметические операции
  • обращения к другим сигналам
  • функции среды исполнения Vega
  • данные события (events)

Пример зависимости от другого сигнала:

{
  "name": "height",
  "value": 300,
  "update": "width * 0.75"
}

Здесь height автоматически пересчитывается при изменении width.


Обновление через события

Одна из ключевых возможностей update — реакция на пользовательские события.

{
  "name": "mouseX",
  "value": 0,
  "on": [
    {
      "events": "mousemove",
      "update": "eventX()"
    }
  ]
}

Хотя здесь используется поле on, логика та же: каждое событие приводит к вычислению выражения update, которое перезаписывает значение сигнала.

Важная особенность: update выполняется в момент наступления события и может использовать объект event, доступный в контексте выражения.


Различие между value, bind и update

Сигналы в Vega часто комбинируются с привязками интерфейса:

{
  "name": "threshold",
  "value": 10,
  "bind": {"input": "range", "min": 0, "max": 100},
  "update": "clamp(threshold, 0, 100)"
}

Здесь происходит наложение нескольких механизмов:

  • bind связывает сигнал с UI-элементом
  • update корректирует значение при изменении других сигналов
  • value задаёт начальное состояние

Иерархия обновления:

  1. пользовательское событие или binding
  2. пересчёт выражения update
  3. реактивное обновление зависимых компонентов

Вычисляемые сигналы и реактивные графы

Каждый сигнал с update становится узлом реактивного графа зависимостей. Vega строит граф, в котором:

  • узлы — сигналы
  • ребра — зависимости между ними

Пример цепочки:

{
  "name": "a",
  "value": 10
},
{
  "name": "b",
  "update": "a * 2"
},
{
  "name": "c",
  "update": "b + 5"
}

При изменении a происходит каскадное обновление: a → b → c


update в контексте данных

Хотя поле update чаще ассоциируется с сигналами, динамическое обновление данных реализуется через API Vega View, где используется похожая модель реактивности.

view.change('table', 
  vega.changeset()
    .insert([{x: 1, y: 2}])
).run();

Здесь реактивность уже не декларативная, но концептуально связана с update: любое изменение данных запускает пересчёт визуализации.


Динамическое обновление через expression language

В выражениях update доступны функции Vega:

{
  "name": "opacity",
  "value": 1,
  "update": "clamp(scale('opacityScale', datum.value), 0, 1)"
}

Особенности выражений:

  • доступ к шкалам через scale()
  • доступ к текущему элементу через datum
  • доступ к событиям через event
  • доступ к сигналам по имени

update и взаимодействие с мышью

Типичный пример интерактивного управления:

{
  "name": "dragX",
  "value": 0,
  "on": [
    {
      "events": "mousedown",
      "update": "x()"
    },
    {
      "events": "drag",
      "update": "dragX + eventX() - x()"
    }
  ]
}

Логика здесь основана на инкрементальном обновлении:

  • фиксируется начальная позиция
  • при движении мыши вычисляется разница
  • update сохраняет новое состояние

Роль update в Vega-Lite

Vega-Lite абстрагирует сигналы, но при компиляции генерирует Vega-конструкции с update.

Например, интерактивное выделение:

{
  "params": [
    {
      "name": "brush",
      "select": "interval"
    }
  ]
}

После компиляции Vega-Lite создаёт сигналы, где update управляет:

  • границами выделения
  • синхронизацией координат
  • пересчётом фильтров

Таким образом, update остаётся внутренним механизмом реактивности, скрытым за декларативным API.


Динамическое форматирование данных

Сигналы с update часто используются для управления визуальными параметрами:

{
  "name": "colorDomain",
  "update": "domain('colorScale')"
}

Или для вычисляемых параметров:

{
  "name": "radius",
  "update": "sqrt(datum.value) * 5"
}

Это позволяет отделить данные от логики отображения, сохраняя реактивную природу.


Каскадные обновления и производительность

Система update оптимизирует пересчёты через граф зависимостей. При изменении сигнала пересчитываются только зависимые узлы.

Пример:

{
  "name": "base",
  "value": 1
},
{
  "name": "mid",
  "update": "base + 10"
},
{
  "name": "top",
  "update": "mid * 2"
}

Изменение base приводит к пересчёту только затронутой цепочки, без полного рендеринга всех элементов.


Условные выражения в update

Поле поддерживает условную логику:

{
  "name": "state",
  "value": "idle",
  "update": "hover ? 'active' : 'idle'"
}

Или более сложные конструкции:

{
  "name": "size",
  "update": "selected ? 20 : (hover ? 12 : 8)"
}

Такие выражения позволяют реализовывать поведение интерфейса без imperative-кода.


Интеграция с API View

Хотя update работает внутри декларативного описания, внешний JavaScript API может изменять сигналы напрямую:

view.signal('threshold', 42).run();

После этого система автоматически:

  • пересчитывает update-выражения
  • обновляет графики
  • применяет фильтры и трансформации

Сигналы как состояние приложения

Использование update превращает Vega в реактивную модель состояния:

  • сигналы заменяют глобальные переменные
  • update заменяет функции пересчёта
  • граф зависимостей заменяет ручное управление состоянием

Пример состояния интерфейса:

{
  "name": "zoom",
  "value": 1,
  "on": [
    {
      "events": "wheel!",
      "update": "clamp(zoom * pow(1.001, event.deltaY * -1), 0.5, 10)"
    }
  ]
}

Ограничения выражений update

Несмотря на гибкость, выражения имеют ограничения:

  • отсутствует полноценный JavaScript
  • нет асинхронности
  • ограниченный набор функций
  • отсутствие побочных эффектов

Это обеспечивает предсказуемость реактивной модели и упрощает оптимизацию.


Типовые паттерны использования

1. Интерактивное масштабирование

{
  "name": "scale",
  "value": 1,
  "on": [
    {
      "events": "wheel",
      "update": "scale * 1.1"
    }
  ]
}

2. Переключение режимов отображения

{
  "name": "mode",
  "value": "line",
  "on": [
    {
      "events": "@toggle:click",
      "update": "mode === 'line' ? 'bar' : 'line'"
    }
  ]
}

3. Следование за курсором

{
  "name": "cursorY",
  "on": [
    {
      "events": "mousemove",
      "update": "eventY()"
    }
  ]
}

Роль update в архитектуре реактивных визуализаций

Поле update является ключевым механизмом, связывающим:

  • пользовательские события
  • состояние сигналов
  • вычисление визуальных параметров
  • реакцию графиков

В совокупности это формирует декларативную модель, где логика изменения состояния описывается не императивно, а через выражения зависимости и пересчёта.