Трансформация identifier

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

В экосистеме Vega-Lite эта трансформация особенно важна при:

  • анимации и обновлении данных;
  • отслеживании объектов между рендерами;
  • интерактивных состояниях;
  • join-операциях;
  • работе с потоковыми данными;
  • создании сложных визуализаций с переходами.

Трансформация добавляет новое поле в каждую запись:

{
  "type": "identifier",
  "as": "id"
}

После обработки каждая строка получает уникальное значение:

[
  { "category": "A", "value": 10, "id": 1 },
  { "category": "B", "value": 20, "id": 2 },
  { "category": "C", "value": 30, "id": 3 }
]

Основной принцип работы

identifier не анализирует содержимое данных и не вычисляет хеши. Трансформация просто создаёт уникальный числовой идентификатор для каждой строки.

Особенности:

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

Синтаксис

Vega

{
  "type": "identifier",
  "as": "id"
}

Параметры

Параметр Тип Описание
type string Тип трансформации
as string Имя создаваемого поля

Простейший пример

Исходные данные:

[
  { "name": "Alice", "score": 15 },
  { "name": "Bob", "score": 22 },
  { "name": "Carol", "score": 17 }
]

Трансформация:

{
  "transform": [
    {
      "type": "identifier",
      "as": "id"
    }
  ]
}

Результат:

[
  { "name": "Alice", "score": 15, "id": 1 },
  { "name": "Bob", "score": 22, "id": 2 },
  { "name": "Carol", "score": 17, "id": 3 }
]

Использование в Vega-Lite

В Vega-Lite identifier обычно применяется через массив transform.

Пример:

{
  "data": {
    "values": [
      { "x": 1, "y": 5 },
      { "x": 2, "y": 9 },
      { "x": 3, "y": 7 }
    ]
  },

  "transform": [
    {
      "identifier": "uid"
    }
  ],

  "mark": "point",

  "encoding": {
    "x": { "field": "x", "type": "quantitative" },
    "y": { "field": "y", "type": "quantitative" }
  }
}

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


Отличие от calculate

Часто возникает путаница между identifier и calculate.

calculate

{
  "calculate": "datum.x + '-' + datum.y",
  "as": "id"
}

identifier

{
  "type": "identifier",
  "as": "id"
}

Ключевое различие:

identifier calculate
Создаёт уникальный ID Вычисляет выражение
Не зависит от данных Зависит от значений
Гарантирует уникальность Не гарантирует уникальность
Использует внутренний счётчик Использует формулу

Использование при анимации

Одна из важнейших областей применения — корректная работа анимаций и transitions.

При обновлении данных визуализация должна понимать:

  • какой объект является новым;
  • какой изменился;
  • какой был удалён.

Без стабильного идентификатора система не может сопоставить элементы между состояниями.

Проблема без identifier

Исходный набор:

[
  { "x": 1, "y": 10 },
  { "x": 2, "y": 20 }
]

Новый набор:

[
  { "x": 1, "y": 15 },
  { "x": 2, "y": 25 }
]

Без идентификаторов движок может интерпретировать изменения как:

  • удаление старых точек;
  • создание новых.

Это приводит к неправильной анимации.


Стабильность объектов

identifier позволяет сохранить связь между элементами.

{
  "type": "identifier",
  "as": "id"
}

Теперь объект:

{ "x": 1, "y": 10, "id": 1 }

после обновления остаётся тем же логическим объектом:

{ "x": 1, "y": 15, "id": 1 }

Работа с потоковыми данными

При потоковой визуализации данные постоянно добавляются.

Пример:

[
  { "time": 1, "value": 10 },
  { "time": 2, "value": 14 }
]

Через некоторое время:

[
  { "time": 3, "value": 18 }
]

Трансформация:

{
  "type": "identifier",
  "as": "eventId"
}

создаёт уникальный идентификатор для каждой новой записи.

Это особенно важно в:

  • real-time dashboards;
  • monitoring systems;
  • telemetry;
  • IoT-визуализациях;
  • финансовых графиках.

Использование вместе с lookup

Трансформация lookup часто требует уникальных ключей.

Пример

Таблица пользователей:

[
  { "name": "Alice" },
  { "name": "Bob" }
]

После identifier:

[
  { "name": "Alice", "id": 1 },
  { "name": "Bob", "id": 2 }
]

Теперь можно выполнять join:

{
  "type": "lookup",
  "from": "orders",
  "key": "userId",
  "fields": ["id"],
  "values": ["amount"]
}

Работа с selections

Во многих интерактивных сценариях необходимы уникальные ключи.

Например:

  • выделение точек;
  • hover-состояния;
  • drag-and-drop;
  • фильтрация;
  • cross-filtering.

Пример

{
  "transform": [
    {
      "identifier": "id"
    }
  ]
}

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


Поведение при фильтрации

Важно понимать: identifier выполняется в определённом порядке конвейера.

Пример

{
  "transform": [
    {
      "type": "identifier",
      "as": "id"
    },
    {
      "type": "filter",
      "expr": "datum.value > 10"
    }
  ]
}

Сначала создаются ID, затем выполняется фильтрация.

Если поменять порядок:

{
  "transform": [
    {
      "type": "filter",
      "expr": "datum.value > 10"
    },
    {
      "type": "identifier",
      "as": "id"
    }
  ]
}

идентификаторы будут назначены только оставшимся строкам.


Поведение при агрегировании

После агрегации строки меняются.

Исходные данные:

[
  { "category": "A", "value": 10 },
  { "category": "A", "value": 20 }
]

После aggregate:

[
  { "category": "A", "sum": 30 }
]

Если identifier расположен до агрегации:

[
  { "category": "A", "value": 10, "id": 1 },
  { "category": "A", "value": 20, "id": 2 }
]

после aggregate старые ID теряются.

Поэтому обычно identifier размещают после операций, изменяющих структуру набора данных.


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

Трансформация window создаёт порядковые значения, но они не являются эквивалентом identifier.

window

{
  "type": "window",
  "ops": ["row_number"],
  "as": ["row"]
}

identifier

{
  "type": "identifier",
  "as": "id"
}

Различия:

window row_number identifier
Может пересчитываться Обычно стабилен
Зависит от сортировки Не зависит
Может изменяться Уникален
Используется для ранжирования Используется для идентификации

Внутренний механизм

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

При обработке строки:

  1. счётчик увеличивается;
  2. значение присваивается объекту;
  3. запись передаётся дальше по pipeline.

Упрощённая модель:

let counter = 0;

data.forEach(item => {
  item.id = ++counter;
});

Идентификаторы и порядок данных

identifier зависит от порядка обработки.

Пример:

[
  { "name": "A" },
  { "name": "B" }
]

Результат:

[
  { "name": "A", "id": 1 },
  { "name": "B", "id": 2 }
]

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

[
  { "name": "B" },
  { "name": "A" }
]

результат станет:

[
  { "name": "B", "id": 1 },
  { "name": "A", "id": 2 }
]

Когда identifier использовать не следует

identifier не подходит для:

  • постоянных database ID;
  • бизнес-ключей;
  • UUID;
  • сохранения данных между сессиями;
  • внешних API;
  • серверной синхронизации.

Причина: значения генерируются локально и не являются глобально стабильными.


Частая ошибка: ожидание постоянства ID

Неверный подход:

{
  "type": "identifier",
  "as": "userId"
}

и последующее сохранение userId в базе.

После перезагрузки визуализации идентификаторы могут измениться.


Использование вместе с force-directed graph

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

Пример:

{
  "transform": [
    {
      "type": "identifier",
      "as": "nodeId"
    }
  ]
}

Это позволяет:

  • отслеживать вершины;
  • сохранять состояние симуляции;
  • правильно обновлять связи.

Identifier и duplicate rows

Даже полностью одинаковые строки получают разные значения.

Исходные данные:

[
  { "value": 10 },
  { "value": 10 },
  { "value": 10 }
]

Результат:

[
  { "value": 10, "id": 1 },
  { "value": 10, "id": 2 },
  { "value": 10, "id": 3 }
]

Это важно при визуализации плотных наборов данных.


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

Трансформация identifier относится к очень дешёвым операциям.

Причины:

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

Сложность:

O(n)

где n — количество строк.


Практический пример в Vega

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",

  "data": [
    {
      "name": "table",

      "values": [
        { "category": "A", "value": 28 },
        { "category": "B", "value": 55 },
        { "category": "C", "value": 43 }
      ],

      "transform": [
        {
          "type": "identifier",
          "as": "id"
        }
      ]
    }
  ],

  "marks": [
    {
      "type": "rect",

      "from": {
        "data": "table"
      },

      "encode": {
        "enter": {
          "x": { "field": "id" },
          "y": { "field": "value" }
        }
      }
    }
  ]
}

Практический пример в Vega-Lite

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "data": {
    "values": [
      { "year": 2020, "sales": 100 },
      { "year": 2021, "sales": 120 },
      { "year": 2022, "sales": 140 }
    ]
  },

  "transform": [
    {
      "identifier": "id"
    }
  ],

  "mark": "line",

  "encoding": {
    "x": {
      "field": "year",
      "type": "ordinal"
    },

    "y": {
      "field": "sales",
      "type": "quantitative"
    },

    "detail": {
      "field": "id"
    }
  }
}

Комбинирование с calculate

Часто используется совместно с вычисляемыми полями.

{
  "transform": [
    {
      "type": "identifier",
      "as": "id"
    },
    {
      "type": "formula",
      "expr": "datum.value * 100",
      "as": "scaled"
    }
  ]
}

Результат:

[
  {
    "value": 10,
    "id": 1,
    "scaled": 1000
  }
]

Комбинирование с collect

collect сортирует данные.

{
  "transform": [
    {
      "type": "collect",
      "sort": {
        "field": "value"
      }
    },
    {
      "type": "identifier",
      "as": "id"
    }
  ]
}

В этом случае идентификаторы будут соответствовать уже отсортированному набору.


Влияние порядка transform-операций

Порядок трансформаций критически важен.

Вариант 1

[
  identifier,
  filter,
  aggregate
]

Вариант 2

[
  filter,
  aggregate,
  identifier
]

Во втором случае ID создаются только для итоговых агрегированных строк.


Типичные сценарии применения

Интерактивные scatter plot

Каждая точка должна иметь уникальный ключ.

Streaming visualizations

Новые записи постоянно появляются в потоке.

Animated transitions

Объекты должны корректно сопоставляться между кадрами.

Complex joins

Необходимы внутренние ключи.

Incremental updates

Нужно понимать, какие строки изменились.


Ограничения трансформации

identifier не предоставляет:

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

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


Сравнение с UUID

UUID

550e8400-e29b-41d4-a716-446655440000

identifier

1
2
3
4

UUID подходит для:

  • серверов;
  • баз данных;
  • распределённых систем.

identifier подходит для:

  • визуализации;
  • client-side state;
  • анимации;
  • внутреннего tracking.

Практические рекомендации

Создавать identifier после structural transforms

Рекомендуемый порядок:

filter → aggregate → lookup → identifier

Не использовать как бизнес-ключ

identifier — исключительно технический идентификатор.

Не рассчитывать на постоянство значений

При изменении порядка данных ID изменятся.

Использовать короткие имена полей

{
  "type": "identifier",
  "as": "_id"
}

уменьшает объём памяти при больших наборах данных.


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

Генерация ID до aggregate

identifier → aggregate

В большинстве случаев бессмысленно.

Использование в качестве внешнего ключа

"id": 15

не должен использоваться вне визуализации.

Ожидание детерминированности

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

Дублирование существующих ID

Если данные уже содержат уникальный ключ, identifier может быть избыточен.


Когда identifier особенно полезен

Наибольшую ценность трансформация представляет в динамических визуализациях:

  • dashboards;
  • monitoring systems;
  • live analytics;
  • animated charts;
  • network graphs;
  • интерактивных исследовательских интерфейсах;
  • real-time telemetry;
  • event-driven visualizations.