Поле signals: реактивные переменные

Signals в Vega и Vega-Lite: реактивная модель состояния

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

В Vega-Lite signals скрыты от пользователя на уровне спецификации, но активно генерируются компилятором при трансляции Vega-Lite в Vega, обеспечивая интерактивность, привязки и вычисляемые свойства визуализации.


Каждый signal в Vega представляет собой узел реактивного графа вычислений. Значение сигнала может зависеть от:

  • других signals
  • входных данных (data)
  • пользовательских событий (event streams)
  • выражений (expressions)
  • системных параметров (width, height, cursor position)

Изменение любого источника автоматически инициирует пересчёт всех зависимых сигналов.

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


Объявление signals в спецификации

Signals задаются в верхнем уровне Vega specification:

{
  "signals": [
    {
      "name": "width",
      "value": 400
    },
    {
      "name": "height",
      "value": 300
    }
  ]
}

Signal всегда имеет имя и начальное значение. Дополнительно могут использоваться:

  • update — выражение пересчёта
  • bind — привязка к UI элементам
  • on — реакция на события

Вычисляемые signals (reactive expressions)

Signals могут вычисляться динамически через выражения:

{
  "signals": [
    {
      "name": "radius",
      "value": 10,
      "update": "width / 20"
    }
  ]
}

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

Выражения используют встроенный expression language Vega, поддерживающий:

  • арифметику
  • условные операторы
  • функции Math
  • доступ к данным и событиям

Событийная модель signals

Signals тесно связаны с event streams. Любое взаимодействие пользователя может быть источником обновления.

{
  "signals": [
    {
      "name": "mouseX",
      "value": 0,
      "on": [
        {
          "events": "mousemove",
          "update": "event.x"
        }
      ]
    }
  ]
}

Здесь event.x — текущее положение курсора.

Возможны сложные фильтры событий:

"events": "@plot:wheel"

или комбинации:

"events": [
  {"events": "mousedown", "update": "x()"},
  {"events": "mouseup", "update": "0"}
]

Signals и привязка к интерфейсу (bind)

Signals могут быть связаны с HTML-элементами управления.

{
  "signals": [
    {
      "name": "threshold",
      "value": 50,
      "bind": {
        "input": "range",
        "min": 0,
        "max": 100
      }
    }
  ]
}

Такой signal становится двунаправленным:

  • изменение UI обновляет signal
  • изменение signal обновляет UI

Использование signals в визуальных параметрах

Signals применяются в любом месте спецификации Vega:

Масштабирование элементов

{
  "marks": [
    {
      "type": "circle",
      "encode": {
        "enter": {
          "x": {"signal": "width / 2"},
          "y": {"signal": "height / 2"},
          "size": {"signal": "radius * radius"}
        }
      }
    }
  ]
}

Управление стилем

"fill": {"signal": "mouseX > 200 ? 'red' : 'blue'"}

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


Взаимодействие signals с data flow

Signals могут зависеть от данных:

{
  "signals": [
    {
      "name": "maxValue",
      "update": "max(data('table'), d => d.value)"
    }
  ]
}

Таким образом, сигналы становятся мостом между data pipeline и визуальным представлением.


Сигналы и трансформации

Signals часто управляют трансформациями данных:

{
  "transform": [
    {
      "filter": "datum.value > threshold"
    }
  ]
}

Здесь threshold — signal, изменяемый пользователем.

Это создаёт динамическую фильтрацию без пересборки графа.


Иерархия сигналов и область видимости

Signals имеют область видимости:

  • глобальные signals (уровень spec)
  • локальные signals (внутри групп)
  • производные signals (computed)

Вложенные группы могут переопределять значения:

{
  "signals": [
    {"name": "scale", "value": 1}
  ],
  "marks": [
    {
      "type": "group",
      "signals": [
        {"name": "scale", "value": 2}
      ]
    }
  ]
}

Внутри группы используется локальная версия signal.


Порядок вычисления и зависимости

Signal dependency graph строится автоматически:

  • если A зависит от B
  • и B изменился
  • A пересчитывается

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

mouseX → normalizedX → color → fill

Изменение одного входа распространяется каскадно.


Производительность и оптимизация signals

Реактивная модель оптимизируется через:

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

Signals не пересчитываются глобально — только локально затронутые узлы.


Signals в Vega-Lite компиляции

В Vega-Lite signals не пишутся вручную, но генерируются автоматически:

  • масштабирование (width, height)
  • selection (brush, point, interval)
  • tooltips
  • legends interactivity

Например, selection brush в Vega-Lite компилируется в:

  • signals для границ выделения
  • signals для состояния selection
  • transforms, зависящие от selection signal

Пример: интерактивный brush selection

Vega-Lite:

{
  "mark": "point",
  "selection": {
    "brush": {"type": "interval"}
  }
}

Компиляция в Vega включает signals:

  • brush_x
  • brush_y
  • brush_end

и их event bindings:

"on": [
  {"events": "mousedown", "update": "..."},
  {"events": "mousemove", "update": "..."},
  {"events": "mouseup", "update": "..."}
]

Signals и JavaScript интеграция

Vega runtime позволяет подключаться к signals через API:

view.signal("threshold", 30).run();

Изменение signal программно:

view.signal("mouseX", 120).run();

Получение значения:

view.signal("mouseX");

Signals становятся точками интеграции между декларативной визуализацией и внешним приложением.


Сложные выражения и композиция signals

Signals поддерживают композицию:

{
  "name": "opacity",
  "update": "clamp(mouseX / width, 0, 1)"
}

или логические цепочки:

"update": "selected && datum.value > threshold"

Это позволяет строить компактные реактивные правила без императивной логики.


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

  • управление масштабом визуализации (zoom)
  • интерактивная фильтрация данных
  • динамическая легенда
  • tooltip позиционирование
  • адаптивная геометрия графиков
  • синхронизация нескольких визуализаций

Signals становятся универсальным механизмом управления состоянием визуализации, заменяя отдельные subsystems интерактивности единой реактивной моделью.