Использование сигналов в transform и encode

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

Каждый сигнал — это именованное значение, которое может:

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

Ключевая особенность заключается в том, что сигналы не принадлежат строго ни к данным, ни к визуальному слою. Они существуют как промежуточный реактивный механизм, обеспечивающий связь между этапами dataflow-пайплайна Vega.

Базовая форма сигнала:

{
  "signals": [
    {
      "name": "threshold",
      "value": 10
    }
  ]
}

Сигнал может быть вычисляемым:

{
  "name": "dynamicThreshold",
  "update": "width / 2"
}

Или зависеть от событий:

{
  "name": "mouseX",
  "on": [
    {
      "events": "mousemove",
      "update": "x()"
    }
  ]
}

Эта модель делает сигналы универсальным механизмом управления поведением всей визуализации.


Использование сигналов в transform

Сигналы в transform используются для динамического изменения данных на этапе обработки. Transform в Vega — это последовательность операций над данными (filter, calculate, lookup, aggregate и другие), и сигналы могут участвовать в этих операциях через выражения.

Наиболее типичный случай — фильтрация данных по сигналу:

{
  "type": "filter",
  "expr": "datum.value > threshold"
}

Здесь threshold — сигнал, определённый на верхнем уровне спецификации. При изменении сигнала пересчитывается результат transform, и данные автоматически обновляются.

Вычисляемые поля через signals

Transform formula (calculate) позволяет создавать новые поля, зависящие от сигналов:

{
  "type": "formula",
  "as": "scaledValue",
  "expr": "datum.value * scaleFactor"
}

Где scaleFactor — сигнал.

Такая конструкция делает трансформации параметризуемыми без необходимости пересобирать спецификацию.

Условные преобразования

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

{
  "type": "formula",
  "as": "category",
  "expr": "useLogScale ? log(datum.value) : datum.value"
}

Здесь один сигнал управляет всей стратегией обработки данных.

Агрегации с параметризацией

Сигналы могут влиять на группировки и агрегирование через выражения, подаваемые в calculate перед aggregate:

{
  "type": "formula",
  "as": "bucket",
  "expr": "floor(datum.value / binSize)"
}

Где binSize управляется сигналом, позволяя динамически менять дискретизацию без изменения структуры transform-пайплайна.


Сигналы в encode

Encode — это слой, отвечающий за визуальные свойства mark-ов: позицию, цвет, размер, форму и другие атрибуты. Именно здесь сигналы проявляют себя наиболее явно, поскольку напрямую управляют тем, как визуализация выглядит в каждый момент времени.

Базовая структура использования сигналов в encode

В Vega encode любое поле может быть задано через сигнал:

{
  "type": "rect",
  "encode": {
    "enter": {
      "width": { "signal": "barWidth" },
      "fill": { "value": "steelblue" }
    }
  }
}

Сигнал barWidth может динамически меняться, влияя на отрисовку всех элементов.

Позиционирование через сигналы

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

{
  "encode": {
    "update": {
      "x": { "signal": "xScale(datum.category)" },
      "y": { "signal": "yScale(datum.value)" }
    }
  }
}

Здесь сигнал используется не только как число, но и как выражение, включающее функции шкал.

Интерактивное управление стилями

Encode позволяет привязывать визуальные стили к сигналам состояния интерфейса:

{
  "fill": {
    "signal": "datum.id === selectedId ? 'orange' : 'gray'"
  }
}

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

Сигналы и события в encode

Encode тесно связан с событиями, и сигналы могут изменяться прямо из пользовательских действий:

{
  "hover": {
    "fillOpacity": { "value": 1 }
  },
  "update": {
    "fillOpacity": { "signal": "hovered ? 1 : 0.6" }
  }
}

Сигнал hovered может управляться через event stream, а encode автоматически реагирует на его изменение.


Связка transform → encode через сигналы

Наиболее важная концепция заключается в том, что сигналы формируют непрерывную связь между transform и encode, создавая реактивный поток данных:

  1. transform обрабатывает данные с учётом сигналов;
  2. результат transform поступает в dataflow;
  3. encode использует сигналы для отображения результата;
  4. изменение сигнала инициирует пересчёт transform и перерисовку encode.

Пример комплексной цепочки:

{
  "signals": [
    { "name": "binSize", "value": 10 }
  ],
  "data": [
    {
      "name": "table",
      "transform": [
        {
          "type": "formula",
          "as": "binned",
          "expr": "floor(datum.value / binSize) * binSize"
        }
      ]
    }
  ],
  "marks": [
    {
      "type": "rect",
      "encode": {
        "update": {
          "x": { "signal": "xScale(datum.binned)" },
          "width": { "signal": "xScale.bandwidth()" }
        }
      }
    }
  ]
}

Изменение binSize приводит к полной перестройке данных и мгновенному обновлению визуального представления.


Сигналы в calculate и выражениях transform

Особое значение имеют выражения Vega Expression Language, где сигналы доступны как глобальные переменные. Это позволяет строить вычисления, не ограниченные структурой transform.

{
  "type": "filter",
  "expr": "datum.value > minValue && datum.value < maxValue"
}

Оба сигнала управляют диапазоном фильтрации.

В более сложных случаях сигналы формируют параметры моделей:

{
  "type": "formula",
  "as": "normalized",
  "expr": "(datum.value - meanValue) / stdDev"
}

Такая конструкция делает transform зависимым от статистических параметров, вычисляемых или задаваемых динамически.


Сигналы как средство синхронизации состояния

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

Особенно важно, что:

  • сигналы обновляются асинхронно относительно dataflow;
  • любое изменение сигнала может инициировать каскадные пересчёты;
  • encode не хранит состояние, а только отражает сигналы;
  • transform может как читать сигналы, так и зависеть от них.

Динамическая параметризация пайплайнов

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

{
  "type": "filter",
  "expr": "enableFilter ? datum.value > threshold : true"
}

Или переключать режим агрегации:

{
  "type": "aggregate",
  "ops": { "signal": "aggType" }
}

Хотя не все поля transform напрямую поддерживают сигналы, выражения позволяют обойти это ограничение через вычисления.


Encode как конечная точка реактивного графа

Encode является конечным этапом реактивного графа Vega, где сигналы определяют итоговое визуальное состояние. Любое изменение сигнала проходит следующий путь:

  • пересчёт transform (если сигнал используется там);
  • обновление данных;
  • повторное вычисление encode-выражений;
  • перерисовка mark-ов.

В результате encode становится не статической декларацией, а реактивным отображением состояния сигналов.


Композиция сигналов между transform и encode

Сложные визуализации часто используют сигналы как композиционные элементы:

{
  "signals": [
    { "name": "scaleFactor", "value": 1 },
    { "name": "offset", "value": 0 }
  ]
}

В transform:

{
  "type": "formula",
  "as": "computed",
  "expr": "datum.value * scaleFactor + offset"
}

В encode:

{
  "y": { "signal": "yScale(datum.computed)" }
}

Такой подход обеспечивает единое пространство параметров для всей визуализации, где transform и encode работают как части одного реактивного контура.