Создание собственных расширений

Модель расширяемости в Kepler.gl построена вокруг принципов Redux-архитектуры и интеграции с визуальным стеком deck.gl. Расширения не являются отдельным изолированным API — они встраиваются в поток данных приложения через переопределение редьюсеров, добавление middleware, кастомных слоёв и модификацию состояния визуализации (visState, mapState, uiState).

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


Модель расширяемости и точки встраивания

Kepler.gl предоставляет несколько уровней, на которых возможно вмешательство:

1. Redux-слой

  • редьюсеры (reducers)
  • экшены (actions)
  • middleware

2. Слой визуализации

  • deck.gl layers
  • кастомные шейдеры (в ограниченном виде)
  • модификация Layer Manager

3. Слой обработки данных

  • processors
  • pre-aggregation hooks
  • кастомные преобразования данных при загрузке

4. UI слой

  • панели управления
  • кастомные компоненты интерфейса
  • расширение control panels

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


Расширение через Redux: reducers и enhancers

Основной механизм внедрения логики — композиция store через enhancers.

Подключение кастомного редьюсера

import keplerGlReducer from '@kepler.gl/reducers';

const customReducer = (state, action) => {
  switch (action.type) {
    case 'CUSTOM_SET_FLAG':
      return {
        ...state,
        customFlag: action.payload
      };
    default:
      return state;
  }
};

export const combinedReducer = keplerGlReducer.initialState({
  uiState: {
    activeSidePanel: null
  }
});

Дальнейшее расширение происходит через combineReducers:

import { combineReducers } from 'redux';

const rootReducer = combineReducers({
  keplerGl: combinedReducer,
  custom: customReducer
});

Важный момент

Kepler.gl ожидает строгую структуру состояния. Любое расширение должно:

  • не ломать visState
  • не мутировать datasets
  • соблюдать immutability контракт Redux

Кастомные экшены и интеграция с Kepler.gl pipeline

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

export const setCustomFilter = (value) => ({
  type: 'CUSTOM_SET_FILTER',
  value
});

Интеграция с Kepler.gl actions:

import KeplerGl from '@kepler.gl/components';
import { addDataToMap } from '@kepler.gl/actions';

dispatch(addDataToMap({
  datasets: {
    info: {
      label: 'Custom Dataset',
      id: 'custom_data'
    },
    data
  }
}));

В расширениях часто создаются “обёртки” над стандартными экшенами для автоматической предобработки данных.


Расширение визуализации через deck.gl layers

Наиболее мощный механизм расширений — добавление кастомных слоёв через deck.gl.

Создание кастомного слоя

import { CompositeLayer } from '@deck.gl/core';

class HeatmapClusterLayer extends CompositeLayer {
  renderLayers() {
    return [
      new ScatterplotLayer({
        id: `${this.id}-scatter`,
        data: this.props.data,
        getPosition: d => d.coordinates,
        getRadius: 100,
        getFillColor: [255, 100, 0, 180]
      })
    ];
  }
}

HeatmapClusterLayer.layerName = 'HeatmapClusterLayer';

Интеграция в Kepler.gl

import KeplerGl from '@kepler.gl/components';

const customLayers = {
  heatmap_cluster: HeatmapClusterLayer
};

<KeplerGl
  id="map"
  width={1200}
  height={800}
  mapboxApiAccessToken={TOKEN}
  customLayers={customLayers}
/>

Принцип работы

Kepler.gl Layer Manager:

  • регистрирует слой по layerName
  • связывает его с visState.layers
  • прокидывает props из состояния
  • обновляет слой при изменении данных

Расширение visState: пользовательские визуальные конфигурации

visState — центральная структура, управляющая слоями, фильтрами и интеракциями.

Добавление кастомных параметров

const customVisStateReducer = (state, action) => {
  switch (action.type) {
    case 'SET_POINT_SIZE_MULTIPLIER':
      return {
        ...state,
        pointSizeMultiplier: action.value
      };
    default:
      return state;
  }
};

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

getRadius: d => d.size * state.visState.pointSizeMultiplier

Таким образом расширение может влиять на рендеринг без изменения core logic Kepler.gl.


Расширение UI компонентов

UI Kepler.gl основан на наборе переиспользуемых React-компонентов.

Переопределение панелей

import KeplerGl from '@kepler.gl/components';

const customPanels = {
  sidePanel: CustomSidePanel
};

<KeplerGl
  id="map"
  width={800}
  height={600}
  mapboxApiAccessToken={TOKEN}
  panels={customPanels}
/>

Добавление control components

Расширения часто внедряют:

  • кастомные фильтры
  • дополнительные sliders
  • внешние переключатели слоёв

Пример:

const CustomControl = ({ dispatch }) => (
  <button onCl ick={() => dispatch({ type: 'CUSTOM_TOGGLE' })}>
    Toggle mode
  </button>
);

Расширение через middleware

Middleware используется для перехвата потоков экшенов:

const keplerMiddleware = store => next => action => {
  if (action.type === 'LAYER_VISUAL_CHANNEL_CHANGE') {
    console.log('Layer updated:', action);
  }
  return next(action);
};

Применение middleware позволяет:

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

Обработка данных перед загрузкой (processors)

Kepler.gl использует data processors для нормализации входных данных.

Кастомный processor

function customProcessor(data) {
  return data.map(row => ({
    ...row,
    timestamp: new Date(row.time).getTime()
  }));
}

Интеграция:

dispatch(addDataToMap({
  datasets: {
    info: { id: 'processed' },
    data: customProcessor(rawData)
  }
}));

Паттерн плагинной архитектуры

Расширения в Kepler.gl часто оформляются как плагины:

Структура плагина

/plugins
  /custom-layer
    index.js
    layer.js
    reducer.js
    ui.js

Реестр плагинов

const plugins = [
  {
    name: 'custom-layer-plugin',
    reducer: customReducer,
    layer: HeatmapClusterLayer,
    ui: CustomPanel
  }
];

Такая структура позволяет масштабировать систему без изменения core пакетов.


Интеграция расширений в жизненный цикл карты

Расширения взаимодействуют с несколькими фазами:

1. Инициализация

  • загрузка datasets
  • регистрация layers
  • установка initialState

2. Обновление состояния

  • фильтры
  • взаимодействия пользователя
  • внешние события

3. Рендеринг

  • пересборка deck.gl layers
  • пересчёт позиций и стилей

4. Синхронизация

  • экспорт состояния
  • взаимодействие с внешними API

Композиция расширений и конфликтность

При одновременном использовании нескольких расширений возникают типовые проблемы:

  • пересечение layerId
  • конфликт редьюсеров
  • несогласованность visState
  • дублирование processors

Решение заключается в:

  • строгой namespace-изоляции
  • префиксах для action types
  • централизованной регистрации расширений
  • мемоизации вычислений в слоях

Расширение возможностей рендеринга через комбинированные слои

Комбинирование нескольких deck.gl слоёв внутри одного расширения позволяет строить сложные визуальные эффекты:

renderLayers() {
  return [
    new ScatterplotLayer(this.props),
    new TextLayer({
      ...this.props,
      getText: d => d.label
    })
  ];
}

Такой подход часто используется для:

  • кластеризации
  • heatmap + markers overlay
  • аналитических визуализаций

Управление производительностью расширений

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

  • батчинг обновлений Redux
  • минимизацию пересоздания layers
  • использование pure components в UI
  • мемоизацию вычислений координат
  • ограничение количества одновременно активных слоёв

Особенно затратны:

  • кастомные CompositeLayer
  • частые перерасчёты visState
  • сложные фильтры на больших датасетах

Расширение событийной модели

Kepler.gl поддерживает реакцию на события карты:

onStateCha nge = (action, state) => {
  if (action.type === 'UPDATE_MAP') {
    syncExternalSystem(state.visState);
  }
};

Это позволяет строить:

  • синхронизацию с backend
  • real-time dashboards
  • связку с внешними аналитическими системами