Визуальная синхронизация нескольких графиков в Nivo опирается на единый принцип: согласование масштабов, доменов данных и логики отрисовки осей. В отличие от библиотек, где предусмотрена встроенная связка графиков, здесь синхронизация реализуется на уровне архитектуры React-приложения и повторного использования конфигурации графиков.
Любая ось в Nivo строится вокруг диапазона значений (domain). Если несколько графиков должны визуально совпадать по шкале, ключевым становится жёсткое выравнивание domain по X и Y.
Типичная ошибка — позволить каждому графику вычислять domain автоматически:
min = 10, max = 200min = 0, max = 500В результате оси теряют смысловую сопоставимость.
Правильный подход — вынести вычисление доменов на уровень контейнера:
const sharedXDomain = [0, 100];
const sharedYDomain = [0, 1000];
И применять их ко всем графикам:
<ResponsiveLine
data={dataA}
xScale={{ type: 'linear', min: sharedXDomain[0], max: sharedXDomain[1] }}
yScale={{ type: 'linear', min: sharedYDomain[0], max: sharedYDomain[1] }}
/>
<ResponsiveLine
data={dataB}
xScale={{ type: 'linear', min: sharedXDomain[0], max: sharedXDomain[1] }}
yScale={{ type: 'linear', min: sharedYDomain[0], max: sharedYDomain[1] }}
/>
Ключевой принцип: синхронизация осей начинается с синхронизации масштабов.
В реальных приложениях данные динамические, поэтому домен нельзя задавать вручную. Его необходимо вычислять:
function computeGlobalDomain(datasets) {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
datasets.forEach(data => {
data.forEach(series => {
series.data.forEach(point => {
if (point.x < minX) minX = point.x;
if (point.x > maxX) maxX = point.x;
if (point.y < minY) minY = point.y;
if (point.y > maxY) maxY = point.y;
});
});
});
return {
x: [minX, maxX],
y: [minY, maxY]
};
}
Затем результат передаётся всем графикам через props или контекст.
При работе с временными рядами проблема усложняется из-за формата
данных Date.
Для Nivo важно, чтобы:
timexScale={{
type: 'time',
format: 'native',
precision: 'day',
min: new Date('2024-01-01'),
max: new Date('2024-12-31')
}}
Если один график получает неполный диапазон дат, а второй — полный, визуальная синхронизация нарушается. Поэтому домен времени также должен быть глобальным.
Практически всегда используется паттерн «общая конфигурация»:
const commonAxis = {
tickSize: 5,
tickPadding: 8,
tickRotation: 0,
legendOffset: 40
};
const commonXScale = {
type: 'linear',
min: 0,
max: 100
};
Использование:
<ResponsiveLine
data={dataA}
axisBottom={{
...commonAxis,
legend: 'Время'
}}
xScale={commonXScale}
/>
<ResponsiveLine
data={dataB}
axisBottom={{
...commonAxis,
legend: 'Время'
}}
xScale={commonXScale}
/>
Это устраняет визуальные расхождения между осями: одинаковые ticks, одинаковые отступы и идентичная геометрия сетки.
Даже при одинаковом domain графики могут визуально «разъезжаться», если различаются:
Для строгой синхронизации ticks задаются явно:
const tickValues = [0, 20, 40, 60, 80, 100];
И применяются ко всем графикам:
axisBottom={{
tickValues,
tickSize: 5
}}
В более сложных случаях ticks генерируются централизованно:
function generateTicks(min, max, step) {
const ticks = [];
for (let i = min; i <= max; i += step) {
ticks.push(i);
}
return ticks;
}
Синхронизация осей часто сопровождается синхронизацией hover-состояний.
Nivo поддерживает slices внутри одного графика, но между несколькими графиками это реализуется вручную.
const [hoverX, setHoverX] = useState(null);
Передача обработчиков:
<ResponsiveLine
data={dataA}
onMouseM ove={point => setHoverX(point.x)}
onMouseLe ave={() => setHoverX(null)}
/>
<ResponsiveLine
data={dataB}
onMouseM ove={point => setHoverX(point.x)}
/>
Далее используется кастомный слой (custom layer) для отображения вертикальной линии:
const Crosshair = ({ xScale, innerHeight, hoverX }) => {
if (hoverX === null) return null;
const x = xScale(hoverX);
return (
<line
x1={x}
x2={x}
y1={0}
y2={innerHeight}
stroke="black"
/>
);
};
И подключение слоя:
layers={[
'grid',
'markers',
'areas',
'lines',
'points',
'slices',
props => <Crosshair {...props} hoverX={hoverX} />
]}
Если графики поддерживают zoom/pan (через кастомную реализацию или внешние библиотеки), необходимо синхронизировать:
Пример централизованного состояния:
const [viewDomain, setViewDomain] = useState({
x: [0, 100],
y: [0, 1000]
});
И применение:
xScale={{
type: 'linear',
min: viewDomain.x[0],
max: viewDomain.x[1]
}}
Любое изменение масштаба одного графика должно обновлять состояние, которое переиспользуется всеми остальными.
Даже при одинаковых значениях оси могут выглядеть по-разному из-за formatter функций.
axisBottom={{
format: value => `${value}%`
}}
Если один график использует проценты, а другой — абсолютные значения, визуальная связь теряется.
Поэтому форматирование также выносится в общий слой:
const formatPercent = v => `${v}%`;
И используется везде одинаково.
В Nivo графики чувствительны к:
Разные отступы приводят к тому, что оси визуально «сдвигаются».
const commonMargin = {
top: 20,
right: 20,
bottom: 50,
left: 60
};
<ResponsiveLine
margin={commonMargin}
/>
Наиболее устойчивый подход — выделение отдельного слоя синхронизации:
Пример структуры:
<ChartsSyncProvider>
<ChartA />
<ChartB />
<ChartC />
</ChartsSyncProvider>
Внутри провайдера:
linear vs time)Каждая из этих проблем приводит к визуальному разрыву, даже если данные идентичны.
Если графики загружаются по API с разной скоростью, синхронизация должна учитывать состояние загрузки:
if (!isLoadedA || !isLoadedB) return null;
или использование промежуточного состояния «pending domain».
Согласованная система осей в Nivo строится как комбинация:
Такая модель превращает набор независимых графиков в визуально связанную систему координат, где каждый компонент является частью единой аналитической плоскости.