Портальный рендер подсказок

Проблема позиционирования подсказок в графиках

Визуализация данных в браузере сталкивается с ограничениями DOM-иерархии. Подсказки (tooltip), привязанные к элементам графика, часто оказываются внутри контейнера с ограничениями:

  • overflow: hidden обрезает всплывающие элементы
  • локальный z-index конфликтует с соседними слоями интерфейса
  • SVG-контекст ограничивает абсолютное позиционирование HTML-элементов
  • трансформации (transform) создают новые контексты наложения

В результате tooltip может отображаться частично, перекрываться соседними компонентами или вообще исчезать за пределами области графика.

Портальный рендер решает эту проблему переносом узла подсказки в другой DOM-контейнер.


Суть портального рендера

Портальный рендер (portal rendering) основан на механизме React Portal:

  • элемент создаётся внутри React-компонента
  • фактически монтируется в другой DOM-узел (например, document.body)
  • визуально сохраняет логическую связь с источником данных

Ключевая идея:

DOM-структура ≠ визуальная привязка

Tooltip остаётся частью графика логически, но физически располагается вне ограниченного контейнера.


Архитектура tooltip в Nivo

В Nivo подсказки реализуются через слой @nivo/tooltip.

Основные компоненты:

  • контейнер графика (SVG или HTML)
  • слой взаимодействия (events: hover, mousemove)
  • менеджер tooltip-состояния
  • рендерер всплывающего компонента
  • портал (опционально) для переноса в document.body

Схема потока данных:

hover event
   ↓
calculation of datum
   ↓
tooltip state update
   ↓
tooltip renderer
   ↓
portal (optional)
   ↓
DOM body overlay

Реализация через React Portal

Механизм базируется на ReactDOM.createPortal:

import { createPortal } from 'react-dom'

function TooltipPortal({ children }) {
    const container = document.body
    return createPortal(children, container)
}

Tooltip, отрисованный через портал, получает:

  • абсолютное позиционирование относительно viewport
  • независимость от родительских overflow
  • стабильный z-index

Проблема контекстного позиционирования

Перенос tooltip в document.body создаёт новую задачу: вычисление координат.

Исходная позиция курсора:

SVG coordinate system

Целевая система:

Viewport coordinate system

Необходима трансформация координат:

  • учёт getBoundingClientRect
  • компенсация скролла страницы
  • масштабирование графика

Вычисление позиции tooltip

Типовая формула позиционирования:

const rect = container.getBoundingClientRect()

const x = mouseX + rect.left
const y = mouseY + rect.top

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

  • scaleX, scaleY
  • padding контейнера
  • margin визуализации

Встроенный портал в Nivo Tooltip

Внутренняя реализация tooltip в Nivo использует абстракцию контейнера рендера.

Логика:

  • tooltip рендерится через отдельный React-слой
  • при включённом режиме portal переносится в root DOM
  • управление осуществляется через контекст tooltip manager

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

const TooltipLayer = ({ tooltip }) => {
    if (!tooltip) return null

    return createPortal(
        <div style={styles.tooltip}>
            {tooltip.content}
        </div>,
        document.body
    )
}

Конфликты z-index и stacking context

Без портала tooltip часто сталкивается с проблемами:

  • график внутри transform: translate3d
  • родитель с opacity < 1
  • изолированные stacking context

Портальный рендер устраняет эти ограничения, так как tooltip выходит на верхний уровень DOM.

Корректная модель слоёв:

layer 1: application layout
layer 2: chart SVG
layer 3: portal tooltips
layer 4: modals / overlays

Событийная модель tooltip

Tooltip в Nivo работает на основе событий:

  • mousemove
  • mouseenter
  • mouseleave

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

onMouseMove(event) {
    const datum = getDatum(event)
    setTooltip({
        x: event.clientX,
        y: event.clientY,
        data: datum
    })
}

Далее tooltip обновляется и перерисовывается уже в портале.


Интеграция с кастомными tooltip-компонентами

Портальный рендер не ограничивает структуру содержимого.

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

const CustomTooltip = ({ datum }) => {
    return (
        <div className="tooltip-box">
            <div>{datum.label}</div>
            <div>{datum.value}</div>
        </div>
    )
}

При использовании портала этот компонент получает независимое позиционирование:

const PositionedTooltip = ({ x, y, children }) => (
    <div style={{ position: 'absolute', left: x, top: y }}>
        {children}
    </div>
)

Поведение при скролле и ресайзе

Портальные tooltip требуют синхронизации с viewport:

  • scroll изменяет абсолютные координаты
  • resize меняет bounding rect контейнера
  • zoom влияет на scale расчётов

Типовая стратегия обновления:

window.addEventListener('scroll', updatePosition)
window.addEventListener('resize', updatePosition)

Оптимизация перерендеров

Портальный tooltip чувствителен к частым обновлениям мыши.

Применяются техники:

  • throttle движения мыши
  • мемоизация datum
  • условный рендер только при изменении позиции
  • разделение state tooltip и state chart

Использование альтернативы portal

В некоторых конфигурациях портальный рендер отключается:

  • строгие ограничения sandbox окружения
  • SSR-рендер без DOM
  • embedded dashboards

В этом случае tooltip остаётся внутри SVG/HTML контейнера, что возвращает ограничения overflow и stacking context.


Влияние портала на производительность

Портальный рендер изменяет нагрузку:

Положительные эффекты:

  • уменьшение layout thrashing внутри SVG
  • независимый слой рендера
  • снижение конфликтов repaint

Отрицательные эффекты:

  • дополнительные DOM-операции
  • глобальные события (scroll/resize)
  • потенциальные частые reflow при неправильной оптимизации

Типовые ошибки при работе с portal tooltip

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

  • неверный расчёт координат относительно viewport
  • отсутствие очистки event listeners
  • конфликт нескольких tooltip слоёв
  • рендер в null контейнер при SSR
  • потеря синхронизации при анимациях графика

Координация с анимациями графиков

При анимациях (например, переходы линий или баров) tooltip должен учитывать:

  • промежуточные состояния
  • easing-функции
  • задержки обновления позиции

Иначе возникает эффект «дрожания» tooltip относительно точки данных.


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

Портальный tooltip можно рассматривать как отдельный визуальный слой:

data layer → chart layer → interaction layer → portal layer

Такой подход позволяет:

  • изолировать UI-слои
  • унифицировать overlay-компоненты
  • масштабировать сложные dashboards

Связь с глобальными overlay системами

Портальный tooltip часто интегрируется с:

  • модальными окнами
  • dropdown-меню
  • системами уведомлений

Все они используют общий принцип:

render outside hierarchy, position relative to viewport

В экосистеме Nivo это обеспечивает согласованное поведение визуальных слоёв поверх графиков.