Замена стандартных компонентов

Архитектура Kepler.gl построена вокруг связки React и Redux, где визуальный интерфейс представляется набором изолированных компонентов, управляемых единым состоянием. Основные слои системы разделены на три ключевые области: visState (данные и визуализация), mapState (параметры камеры и карты) и uiState (интерфейсные элементы). Замена стандартных компонентов выполняется именно через слой uiState и механизм внедрения пользовательских React-компонентов поверх базовой реализации.

UI Kepler.gl не является монолитным. Каждый крупный элемент интерфейса выделен в самостоятельный компонент:

  • панель слоёв (Layer Manager)
  • панель фильтров (Filter Panel)
  • панель легенды (Legend)
  • тулбары управления картой
  • модальные окна и диалоги
  • контейнеры оверлеев

Каждый из этих элементов подписан на Redux-состояние и получает данные через селекторы. Это позволяет заменять отдельные части интерфейса без изменения логики визуализации.

Ключевая особенность — отсутствие жёсткой привязки UI к внутренним структурам Kepler.gl. Вместо этого используется регистрация компонентов через механизм инъекции.

Механизм injectComponents

Основной способ замены стандартных элементов интерфейса — функция injectComponents. Она позволяет подменять или расширять внутренние React-компоненты библиотеки.

import {injectComponents} from 'kepler.gl/components';

Инъекция выполняется до рендера основного приложения:

injectComponents([
  [KeplerGlLayerManager, CustomLayerManager],
  [KeplerGlPanelHeader, CustomPanelHeader]
]);

Каждая пара представляет собой:

  • оригинальный компонент Kepler.gl
  • пользовательскую реализацию

После регистрации Kepler.gl автоматически использует новые компоненты вместо встроенных.

Механизм работает на уровне dependency injection, а не через наследование, что позволяет полностью заменить поведение UI без форка библиотеки.

Замена панели слоёв

Панель слоёв — один из наиболее часто кастомизируемых элементов, так как она тесно связана с доменной логикой приложения.

Стандартный LayerManager получает список слоёв из visState.layers и управляет их конфигурацией. При замене компонента важно сохранить контракт данных:

function CustomLayerManager(props) {
  const {layers, onLayerChange, dataset} = props;

  return (
    <div className="custom-layer-manager">
      {layers.map(layer => (
        <div key={layer.id}>
          <span>{layer.config.label}</span>
          <button onCl ick={() => onLayerChange(layer, {visible: false})}>
            скрыть
          </button>
        </div>
      ))}
    </div>
  );
}

Ключевым моментом является сохранение callback-интерфейса:

  • onLayerChange
  • dispatch
  • updateLayerConfig

Нарушение этих контрактов приводит к потере синхронизации с Redux-состоянием.

Кастомизация MapControls

Контролы карты (zoom, rotation, pitch) реализованы как отдельный слой UI, взаимодействующий с mapState.

Переопределение позволяет полностью изменить поведение навигации:

function CustomMapControls({mapState, setMapControl}) {
  const zoomIn = () => setMapControl('zoom', mapState.zoom + 1);
  const zoomOut = () => setMapControl('zoom', mapState.zoom - 1);

  return (
    <div className="controls">
      <button onCl ick={zoomIn}>+</button>
      <button onCl ick={zoomOut}>-</button>
    </div>
  );
}

Важно учитывать, что mapState является единственным источником истины. Любые локальные состояния компонентов должны синхронизироваться через actions Kepler.gl, иначе карта и UI расходятся.

Замена контейнеров оверлеев

Оверлеи используются для модальных окон, тултипов и всплывающих панелей. Их замена требует работы с порталами React.

Kepler.gl использует отдельный DOM-узел для оверлеев, обычно привязанный к document.body.

Пример кастомного контейнера:

function CustomModalContainer({children}) {
  return (
    <div className="custom-modal-root">
      {children}
    </div>
  );
}

При инъекции важно сохранить механизм рендеринга через ReactDOM.createPortal, иначе модальные окна теряют позиционирование относительно карты.

Работа с uiState при замене компонентов

uiState управляет видимостью панелей и состоянием интерфейса. При замене компонентов необходимо учитывать следующие поля:

  • uiState.currentModal
  • uiState.activeSidePanel
  • uiState.readOnly

При создании кастомного интерфейса часто возникает необходимость вручную читать эти значения через mapStateToProps.

Пример интеграции:

const mapStateToProps = state => ({
  isReadOnly: state.keplerGl.uiState.readOnly,
  activePanel: state.keplerGl.uiState.activeSidePanel
});

Игнорирование этих флагов приводит к расхождению поведения между стандартными и кастомными компонентами.

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

FilterPanel тесно связан с visState.filters. Каждый фильтр описывается как объект с диапазоном значений, типом и состоянием активности.

Кастомная реализация может полностью изменить UI представление фильтра, сохранив только логику:

function CustomFilterPanel({filters, updateFilter}) {
  return (
    <div>
      {filters.map(f => (
        <div key={f.id}>
          <label>{f.name}</label>
          <input
            type="range"
            min={f.domain[0]}
            max={f.domain[1]}
            value={f.value}
            onCha nge={e =>
              updateFilter(f.id, {value: Number(e.target.value)})
            }
          />
        </div>
      ))}
    </div>
  );
}

Важный аспект — фильтры могут иметь разные типы (timeRange, range, multiSelect), и кастомный компонент должен учитывать их структуру.

Интеграция собственных компонентов с Redux Kepler.gl

Kepler.gl использует namespace редьюсеров, обычно подключаемых как:

import keplerGlReducer from 'kepler.gl/reducers';

При замене UI-компонентов важно не создавать отдельное состояние, дублирующее Redux, а использовать существующие action creators:

  • addLayer
  • removeLayer
  • updateLayer
  • setFilter
  • toggleSidePanel

Пример подключения:

import {updateMap} from 'kepler.gl/actions';

dispatch(updateMap({latitude: 50, longitude: 30, zoom: 10}));

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

Переопределение header-компонентов панелей

Header каждого блока (слои, фильтры, датасеты) обычно содержит базовые действия: сворачивание, закрытие, меню.

Замена header-компонента позволяет встроить бизнес-логику:

function CustomPanelHeader({title, onClose}) {
  return (
    <div className="panel-header">
      <h3>{title}</h3>
      <button onCl ick={onClose}>закрыть</button>
    </div>
  );
}

При этом важно учитывать, что заголовки часто управляют состоянием uiState, и прямое изменение DOM без dispatch приводит к визуальным артефактам.

Ограничения и конфликтные сценарии замены компонентов

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

  • жесткая зависимость от структуры visState
  • невозможность полной замены логики без форка reducers
  • чувствительность к версии Kepler.gl
  • необходимость сохранения контрактов props

Особенно критично соблюдение формы объектов слоёв и фильтров. Любое расхождение в структуре приводит к ошибкам сериализации схемы (KeplerGlSchema).

Также следует учитывать, что некоторые компоненты зависят от внутренних HOC Kepler.gl. Их замена без повторного подключения контекста приводит к потере доступа к dispatch и селекторам.

Паттерн частичной замены интерфейса

На практике редко выполняется полная замена UI. Чаще применяется частичный подход:

  • сохранение LayerManager
  • кастомизация FilterPanel
  • замена MapControls
  • добавление собственного overlay

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