calculate: вычисляемые поля и выражения

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


Синтаксис transform calculate

В Vega-Lite calculate задаётся внутри массива transform:

{
  "transform": [
    {
      "calculate": "datum.a + datum.b",
      "as": "sum"
    }
  ]
}
  • calculate — выражение в языке Vega Expression Language
  • as — имя нового поля

В Vega (низкоуровневый формат) используется аналогичная логика:

{
  "type": "formula",
  "expr": "datum.a + datum.b",
  "as": "sum"
}

Несмотря на различие в названии (calculate vs formula), концепция идентична: вычисление нового поля на основе текущей записи (datum).


Контекст выполнения выражений

Выражения выполняются для каждой строки данных отдельно. Основной объект контекста:

  • datum — текущая запись данных
  • доступ к полям через datum.fieldName
  • доступ к глобальным сигналам (в Vega) через signal

Пример:

{
  "calculate": "datum.price * datum.quantity",
  "as": "total"
}

Каждая строка получает собственное значение total.


Основы языка выражений Vega

Vega Expression Language — это ограниченный, но мощный DSL, похожий на JavaScript, но без доступа к полноценной среде исполнения.

Поддерживаются:

  • арифметика: + - * / %
  • логика: && || !
  • сравнения: == != < > <= >=
  • условные выражения: condition ? a : b
  • функции: round(), floor(), ceil(), pow(), log(), sqrt()
  • строковые операции: substring(), indexof(), toUpperCase(), toLowerCase()
  • преобразования типов: number(), string(), boolean()

Арифметические вычисления

Частый сценарий — создание производных метрик:

{
  "transform": [
    {
      "calculate": "datum.revenue - datum.cost",
      "as": "profit"
    }
  ]
}

Или вычисление коэффициентов:

{
  "calculate": "datum.success / datum.total",
  "as": "ratio"
}

Важно учитывать отсутствие автоматической защиты от деления на ноль. Без явной проверки выражение может вернуть Infinity или NaN.

Безопасный вариант:

{
  "calculate": "datum.total == 0 ? 0 : datum.success / datum.total",
  "as": "ratio"
}

Условная логика

calculate активно используется для категоризации данных:

{
  "calculate": "datum.value > 100 ? 'high' : 'low'",
  "as": "category"
}

Многоуровневые условия:

{
  "calculate": "datum.value > 100 ? 'A' : datum.value > 50 ? 'B' : 'C'",
  "as": "grade"
}

Такой подход заменяет необходимость предварительной агрегации или классификации.


Работа со строками

В Vega Expression Language доступны базовые строковые операции:

{
  "calculate": "datum.firstName + ' ' + datum.lastName",
  "as": "fullName"
}

Извлечение подстроки:

{
  "calculate": "substring(datum.code, 0, 3)",
  "as": "prefix"
}

Поиск внутри строки:

{
  "calculate": "indexof(datum.email, '@') > -1",
  "as": "isEmailValid"
}

Преобразование регистра:

{
  "calculate": "toUpperCase(datum.country)",
  "as": "countryUpper"
}

Преобразование типов

Vega не всегда автоматически приводит типы, поэтому calculate часто используется для явного преобразования:

{
  "calculate": "number(datum.value)",
  "as": "valueNum"
}

Или:

{
  "calculate": "string(datum.id)",
  "as": "idStr"
}

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


Доступ к математическим функциям

Поддерживаются стандартные математические операции:

{
  "calculate": "round(datum.value * 100) / 100",
  "as": "rounded"
}
{
  "calculate": "pow(datum.x, 2) + pow(datum.y, 2)",
  "as": "distanceSquared"
}
{
  "calculate": "sqrt(datum.value)",
  "as": "root"
}

Композиция вычислений

Несколько calculate могут применяться последовательно. Каждый следующий шаг может использовать результат предыдущего:

{
  "transform": [
    {
      "calculate": "datum.price * datum.qty",
      "as": "total"
    },
    {
      "calculate": "datum.total * 0.2",
      "as": "tax"
    },
    {
      "calculate": "datum.total + datum.tax",
      "as": "final"
    }
  ]
}

Важно: порядок трансформаций имеет значение, так как поля создаются последовательно.


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

При работе с датами calculate может применяться для преобразований времени:

{
  "calculate": "toDate(datum.timestamp)",
  "as": "date"
}

Или извлечение компонентов (в зависимости от окружения Vega):

{
  "calculate": "hours(datum.date)",
  "as": "hour"
}

Часто временные операции комбинируются с агрегациями и шкалированием.


Vega-Lite и ограничения calculate

Vega-Lite специально ограничивает выражения, чтобы сохранить декларативность. Нельзя:

  • выполнять произвольный JavaScript
  • обращаться к внешним API
  • использовать циклы
  • создавать сложные структуры данных

Поддерживаются только выражения Vega Expression Language.

В Vega (низкий уровень) возможностей больше, но принцип вычислений тот же.


Типичные ошибки

1. Обращение к несуществующему полю

"calculate": "datum.price * datum.amount"

Если amount отсутствует, результат будет null.


2. Смешивание типов

"calculate": "datum.value + 10"

Если value — строка, результат может стать конкатенацией.

Решение:

"calculate": "number(datum.value) + 10"

3. Деление на ноль

"calculate": "datum.a / datum.b"

Без проверки b возможны некорректные значения.


Использование в визуализациях

calculate часто применяется перед:

  • построением шкал (scale)
  • фильтрацией (filter)
  • агрегацией (aggregate)
  • построением геометрии (mark)

Пример подготовки данных для диаграммы:

{
  "transform": [
    {
      "calculate": "datum.sales / datum.target",
      "as": "performance"
    }
  ],
  "mark": "bar",
  "encoding": {
    "x": {"field": "performance", "type": "quantitative"},
    "y": {"field": "name", "type": "nominal"}
  }
}

Связь с filter и other transforms

calculate часто используется вместе с filter:

{
  "transform": [
    {
      "calculate": "datum.value > 0",
      "as": "isPositive"
    },
    {
      "filter": "datum.isPositive"
    }
  ]
}

Такой подход позволяет сначала вычислить признак, затем использовать его для отбора данных.


Производительность

Хотя calculate оптимизирован, есть ограничения:

  • выражения выполняются для каждой строки
  • сложные цепочки могут замедлять рендеринг
  • повторяющиеся вычисления лучше выносить в отдельные поля

Практика:

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

Расширенные паттерны

Нормализация значений

{
  "calculate": "(datum.value - datum.min) / (datum.max - datum.min)",
  "as": "normalized"
}

Бинаризация

{
  "calculate": "datum.score >= 0.5 ? 1 : 0",
  "as": "binary"
}

Генерация категорий для визуализации

{
  "calculate": "datum.value < 10 ? 'small' : datum.value < 50 ? 'medium' : 'large'",
  "as": "bucket"
}

Роль calculate в архитектуре Vega

calculate выполняет функцию промежуточного слоя между сырыми данными и визуальными кодировками. Он:

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

Эта трансформация является фундаментальным инструментом подготовки данных внутри pipeline Vega/Vega-Lite, особенно в сценариях динамической визуальной аналитики.