Accessor функции

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

В основе большинства слоёв deck.gl лежит идея: каждый визуальный примитив (точка, линия, полигон, инстанс) получает набор атрибутов, которые затем передаются в WebGL-шейдеры. Accessor-функции определяют, как именно из входного объекта данных получить значение этих атрибутов.


Роль accessor-функций в пайплайне рендеринга

При построении слоя deck.gl проходит несколько этапов:

  1. Загрузка и хранение данных (data)
  2. Преобразование данных в атрибуты (attribute extraction)
  3. Передача атрибутов в GPU
  4. Рендеринг через шейдеры

Accessor-функции участвуют на втором этапе, формируя промежуточное представление данных.

Каждый слой может принимать параметры вида:

getPosition: d => d.coordinates,
getColor: d => [255, 0, 0],
getWidth: d => d.size

Эти функции вызываются для каждого элемента массива data.


Базовая структура accessor-параметров

Accessor в deck.gl — это функция вида:

(d, {index, data, target}) => any

Где:

  • d — текущий объект данных
  • index — индекс элемента
  • data — весь массив данных
  • target — предварительно выделенный массив (оптимизация памяти)

Пример использования:

getPosition: (d, {index}) => {
  return [d.lon, d.lat];
}

Типы accessor-значений

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

Векторные значения

Используются для координат и цветов:

getPosition: d => [d.x, d.y, d.z],
getColor: d => [255, d.intensity, 0, 255]

Важно, что размер массива должен соответствовать ожиданиям слоя (например, 2D или 3D координаты).


Скалярные значения

Используются для ширины, высоты, радиуса:

getRadius: d => d.value * 10,
getWidth: d => Math.sqrt(d.weight)

Условные вычисления

Accessor может включать произвольную логику:

getColor: d => {
  if (d.type === 'A') return [255, 0, 0];
  if (d.type === 'B') return [0, 255, 0];
  return [0, 0, 255];
}

Оптимизация через target-параметр

Для больших датасетов важно избегать лишних аллокаций массивов. deck.gl предоставляет target:

getPosition: (d, {target}) => {
  target[0] = d.x;
  target[1] = d.y;
  target[2] = d.z;
  return target;
}

Это снижает нагрузку на GC и ускоряет рендеринг.


Встроенные (shorthand) accessors

Если accessor не указан как функция, можно использовать строку:

getPosition: 'coordinates'

В этом случае deck.gl автоматически извлекает значение из объекта:

d['coordinates']

Это упрощённая форма, которая особенно полезна при работе с плоскими структурами данных.


Доступ к индексам и контексту

Accessor-функции могут использовать индекс и внешний контекст:

getColor: (d, {index, data}) => {
  return index % 2 === 0 ? [255, 0, 0] : [0, 0, 255];
}

Это позволяет создавать визуальные паттерны, зависящие не только от данных, но и от их положения в массиве.


Связь accessor-функций и attributeManager

Внутри deck.gl accessor-функции компилируются в атрибуты через AttributeManager. Процесс выглядит следующим образом:

  • слой объявляет accessor (getPosition)
  • deck.gl вызывает функцию для каждого элемента
  • результат записывается в WebGL buffer
  • buffer привязывается к шейдеру как attribute

Таким образом accessor — это мост между JavaScript-данными и GPU-атрибутами.


Кэширование результатов accessor

Для повышения производительности deck.gl может кэшировать результаты accessor-функций, если:

  • данные не изменились
  • accessor-функция не была заменена
  • слой не пересоздавался

Это особенно важно для сложных вычислений:

getElevation: d => Math.sin(d.value) * 1000

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


Асинхронные ограничения

Accessor-функции в deck.gl должны быть синхронными. Недопустимы конструкции вида:

getColor: async d => {
  const res = await fetch(...);
  return res.color;
}

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


Сложные структуры данных

Accessor может работать с вложенными объектами:

getPosition: d => d.geometry.coordinates,
getColor: d => d.properties.style.color

При этом важно учитывать стоимость доступа к глубоким структурам при больших объёмах данных.


Комбинирование accessor с transform-функциями

Accessor часто используется вместе с преобразованием данных перед передачей в слой:

const data = rawData.map(d => ({
  position: [d.x, d.y],
  color: d.value > 10 ? [255, 0, 0] : [0, 0, 255]
}));

new ScatterplotLayer({
  data,
  getPosition: d => d.position,
  getColor: d => d.color
});

Такой подход снижает вычислительную нагрузку внутри accessor.


Переиспользование accessor-функций

Accessor-функции могут выноситься в отдельные модули:

export const getDefaultColor = d =>
  d.active ? [0, 200, 0] : [200, 0, 0];

И использоваться в разных слоях:

getColor: getDefaultColor

Это повышает консистентность визуализации между слоями.


Производительность и частые ошибки

Наиболее распространённые проблемы:

  • создание новых массивов внутри accessor без необходимости
  • сложные вычисления без предварительной агрегации
  • использование тяжёлых операций (например, JSON.parse)
  • глубокие цепочки доступа к данным

Оптимизация обычно сводится к минимизации работы внутри accessor и переносу вычислений на этап подготовки данных.


Поведение при обновлении данных

При изменении data accessor-функции пересчитываются. При этом deck.gl старается минимизировать перерасчёт:

  • если изменился только accessor — пересчёт атрибутов
  • если изменились данные — полный пересчёт
  • если изменились оба — полный rebuild буферов

Accessor и декларативность слоёв

Accessor-функции усиливают декларативный характер deck.gl. Вместо императивного описания рендеринга задаётся правило трансформации данных:

  • что взять из объекта
  • как преобразовать
  • как передать в GPU

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