Визуализация данных в браузере сталкивается с ограничениями DOM-иерархии. Подсказки (tooltip), привязанные к элементам графика, часто оказываются внутри контейнера с ограничениями:
overflow: hidden обрезает всплывающие элементыz-index конфликтует с соседними слоями
интерфейсаtransform) создают новые контексты
наложенияВ результате tooltip может отображаться частично, перекрываться соседними компонентами или вообще исчезать за пределами области графика.
Портальный рендер решает эту проблему переносом узла подсказки в другой DOM-контейнер.
Портальный рендер (portal rendering) основан на механизме React Portal:
document.body)Ключевая идея:
DOM-структура ≠ визуальная привязка
Tooltip остаётся частью графика логически, но физически располагается вне ограниченного контейнера.
В Nivo подсказки реализуются через слой
@nivo/tooltip.
Основные компоненты:
document.bodyСхема потока данных:
hover event
↓
calculation of datum
↓
tooltip state update
↓
tooltip renderer
↓
portal (optional)
↓
DOM body overlay
Механизм базируется на ReactDOM.createPortal:
import { createPortal } from 'react-dom'
function TooltipPortal({ children }) {
const container = document.body
return createPortal(children, container)
}
Tooltip, отрисованный через портал, получает:
overflowz-indexПеренос tooltip в document.body создаёт новую задачу:
вычисление координат.
Исходная позиция курсора:
SVG coordinate system
Целевая система:
Viewport coordinate system
Необходима трансформация координат:
getBoundingClientRectТиповая формула позиционирования:
const rect = container.getBoundingClientRect()
const x = mouseX + rect.left
const y = mouseY + rect.top
При использовании масштабируемых графиков добавляются поправки:
scaleX, scaleYВнутренняя реализация tooltip в Nivo использует абстракцию контейнера рендера.
Логика:
Упрощённая модель:
const TooltipLayer = ({ tooltip }) => {
if (!tooltip) return null
return createPortal(
<div style={styles.tooltip}>
{tooltip.content}
</div>,
document.body
)
}
Без портала tooltip часто сталкивается с проблемами:
transform: translate3dopacity < 1Портальный рендер устраняет эти ограничения, так как tooltip выходит на верхний уровень DOM.
Корректная модель слоёв:
layer 1: application layout
layer 2: chart SVG
layer 3: portal tooltips
layer 4: modals / overlays
Tooltip в Nivo работает на основе событий:
mousemovemouseentermouseleaveПоток обработки:
onMouseMove(event) {
const datum = getDatum(event)
setTooltip({
x: event.clientX,
y: event.clientY,
data: datum
})
}
Далее 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:
Типовая стратегия обновления:
window.addEventListener('scroll', updatePosition)
window.addEventListener('resize', updatePosition)
Портальный tooltip чувствителен к частым обновлениям мыши.
Применяются техники:
В некоторых конфигурациях портальный рендер отключается:
В этом случае tooltip остаётся внутри SVG/HTML контейнера, что возвращает ограничения overflow и stacking context.
Портальный рендер изменяет нагрузку:
Положительные эффекты:
Отрицательные эффекты:
Распространённые проблемы:
null контейнер при SSRПри анимациях (например, переходы линий или баров) tooltip должен учитывать:
Иначе возникает эффект «дрожания» tooltip относительно точки данных.
Портальный tooltip можно рассматривать как отдельный визуальный слой:
data layer → chart layer → interaction layer → portal layer
Такой подход позволяет:
Портальный tooltip часто интегрируется с:
Все они используют общий принцип:
render outside hierarchy, position relative to viewport
В экосистеме Nivo это обеспечивает согласованное поведение визуальных слоёв поверх графиков.