Дискретные масштабы: d3.scaleOrdinal()

Дискретные (ordinal) масштабы в D3.js предназначены для отображения значений из конечного набора категорий в соответствующие значения выходного диапазона. В отличие от линейных или логарифмических масштабов, работающих с непрерывными числовыми данными, ordinal-скейлы ориентированы на категориальные данные, где важен сам факт принадлежности к классу, а не числовое расстояние между значениями.

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


Основы d3.scaleOrdinal()

Функция d3.scaleOrdinal() создаёт масштаб, который сопоставляет дискретные значения из домена (domain) с дискретными значениями диапазона (range).

const scale = d3.scaleOrdinal()
    .domain(["A", "B", "C"])
    .range([10, 20, 30]);

В этом примере:

  • “A” → 10
  • “B” → 20
  • “C” → 30

Если передать значение, отсутствующее в домене, поведение зависит от конфигурации масштаба.


Принцип работы ordinal-отображения

Ordinal scale работает как ассоциативное отображение:

  1. Первое появившееся значение связывается с первым элементом range
  2. Второе — со вторым и так далее
  3. Если значений больше, чем элементов range, происходит циклическое использование или расширение (в зависимости от реализации и настройки)

Если domain явно не задан, D3 строит его автоматически на основе входных данных при первом вызове масштаба.


Автоматическое формирование домена

Одной из ключевых особенностей является автоматическое накопление домена:

const scale = d3.scaleOrdinal()
    .range(["red", "green", "blue"]);

scale("apple");   // "red"
scale("banana");  // "green"
scale("cherry");  // "blue"
scale("apple");   // "red" (уже закреплённое соответствие)

Здесь domain формируется динамически:

["apple", "banana", "cherry"]

Порядок появления данных определяет соответствие.


Поведение при неизвестных значениях

Если входное значение не встречалось ранее:

  • оно добавляется в domain
  • ему назначается следующий свободный элемент range

Пример:

const scale = d3.scaleOrdinal()
    .range(["#1f77b4", "#ff7f0e"]);

scale("A"); // "#1f77b4"
scale("B"); // "#ff7f0e"
scale("C"); // "#1f77b4" (повтор диапазона)

Если range короче domain, начинается циклическое повторение значений.


Явное задание domain и range

Явная конфигурация позволяет полностью контролировать соответствия:

const scale = d3.scaleOrdinal()
    .domain(["low", "medium", "high"])
    .range(["#2ca02c", "#ff7f0e", "#d62728"]);

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


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

Ordinal scales часто применяются для цветового кодирования категорий:

const color = d3.scaleOrdinal()
    .domain(["Chrome", "Firefox", "Safari", "Edge"])
    .range(d3.schemeCategory10);

Здесь используется встроенная палитра D3:

  • d3.schemeCategory10 — 10 контрастных цветов
  • подходит для визуализации категорий без числового смысла

Пример применения:

svg.selectAll("circle")
    .data(browsersData)
    .enter()
    .append("circle")
    .attr("cx", d => x(d.value))
    .attr("cy", d => y(d.count))
    .attr("r", 5)
    .attr("fill", d => color(d.browser));

Разница между ordinal и other scales

Ordinal scale отличается от других типов масштабов:

Линейные масштабы

d3.scaleLinear()
  • работают с числами
  • сохраняют пропорции
  • подходят для измерений

Логарифмические

d3.scaleLog()
  • нелинейные числовые зависимости
  • используются для больших диапазонов

Ordinal

d3.scaleOrdinal()
  • работают с категориями
  • не предполагают числовой интерпретации
  • важен только факт соответствия

Цикличность range и её последствия

Если domain становится больше range, начинается повторение значений:

const scale = d3.scaleOrdinal()
    .range(["A", "B"]);

scale("x1"); // "A"
scale("x2"); // "B"
scale("x3"); // "A"
scale("x4"); // "B"

Это поведение может быть полезным для:

  • генерации повторяющихся шаблонов
  • простых цветовых схем

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


Кэширование соответствий

Ordinal scale сохраняет соответствия между вызовами:

const scale = d3.scaleOrdinal()
    .range(["red", "blue"]);

scale("cat"); // red
scale("dog"); // blue
scale("cat"); // red (то же значение)

Система работает как внутренний словарь:

domain → range mapping

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


Доступ к domain и range

Масштаб можно настраивать и читать:

scale.domain(); // получить текущий domain
scale.range();  // получить текущий range

Изменение:

scale.domain(["A", "B", "C"]);
scale.range([1, 2, 3]);

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


Работа с неизвестным значением (fallback)

D3 позволяет задать значение по умолчанию:

const scale = d3.scaleOrdinal()
    .domain(["A", "B"])
    .range(["red", "blue"])
    .unknown("gray");

Если передать неизвестный элемент:

scale("Z"); // "gray"

Это важно при работе с неполными или грязными данными.


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

Ordinal scale применяется в задачах, где данные представляют категории:

  • типы событий
  • группы пользователей
  • категории товаров
  • названия стран или регионов
  • уровни приоритетов

Пример группировки:

const groupColor = d3.scaleOrdinal()
    .domain(["admin", "user", "guest"])
    .range(["#e74c3c", "#3498db", "#2ecc71"]);

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

Ordinal scale часто применяется не только к цветам, но и к другим атрибутам:

Штриховка или стиль

const strokeScale = d3.scaleOrdinal()
    .domain(["A", "B", "C"])
    .range(["solid", "dashed", "dotted"]);

Размеры

const sizeScale = d3.scaleOrdinal()
    .domain(["small", "medium", "large"])
    .range([3, 6, 10]);

Сравнение с scaleBand()

Хотя обе конструкции работают с категориями, они решают разные задачи:

  • d3.scaleOrdinal() → произвольное отображение значений
  • d3.scaleBand() → распределение по оси с позиционированием и шириной

Ordinal scale не отвечает за геометрию размещения, только за соответствие значений.


Внутренняя логика сопоставления

Поведение можно представить как структуру:

Map<domain_value, range_value>

При первом вызове:

if value not in map:
    assign next range item
    store mapping
return mapped value

Эта логика делает ordinal scale детерминированным после первичного заполнения.


Детали поведения при изменении данных

При динамических данных важно учитывать:

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

Пример:

const scale = d3.scaleOrdinal()
    .range(["red", "green"]);

scale("A");
scale("B");

scale.range(["blue", "yellow"]);

scale("A"); // "red" или "blue" зависит от внутреннего состояния

Из-за этого изменение range в динамических системах требует осторожности.


Оптимизация использования в визуализациях

Для стабильных графиков обычно применяются следующие подходы:

  • фиксированный domain
  • фиксированный range
  • предопределённые категории до отрисовки
const categories = ["A", "B", "C", "D"];

const scale = d3.scaleOrdinal()
    .domain(categories)
    .range(d3.schemeTableau10);

Это исключает непредсказуемое поведение при обновлении данных.


Особенности интеграции с данными JSON

При работе с API или JSON-структурами ordinal scale часто используется для:

  • группировки объектов
  • цветового кодирования
  • построения легенд
data.map(d => ({
    ...d,
    color: colorScale(d.category)
}));

Поведение при пустом domain

Если domain не задан и не накоплен:

const scale = d3.scaleOrdinal()
    .range(["red", "blue"]);

scale("A"); // "red"

domain автоматически формируется:

["A"]

Поведение при пустом range

Если range не задан:

  • возвращается undefined для всех значений
  • масштаб не имеет практического смысла
const scale = d3.scaleOrdinal();

scale("A"); // undefined

Закономерности распределения значений

Ordinal scale не гарантирует:

  • равномерности
  • пропорциональности
  • сохранения порядка

Его задача — исключительно отображение категорий в значения без математической интерпретации между ними.


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

Ordinal scale часто становится основой для построения легенд:

const legendItems = scale.domain().map(d => ({
    label: d,
    color: scale(d)
}));

Каждый элемент domain соответствует одному визуальному элементу интерфейса.


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

Ordinal scale обладает высокой производительностью:

  • операции lookup O(1)
  • отсутствие вычислительных трансформаций
  • минимальная память при фиксированном domain

Это делает его подходящим для больших категориальных наборов данных.