Синхронизация нескольких графиков

Синхронизация графиков в Chart.js предполагает согласованное поведение нескольких экземпляров диаграмм, при котором взаимодействие с одним графиком приводит к изменениям состояния других. Подобный подход используется для анализа временных рядов, сравнительных визуализаций и дашбордов, где требуется единый контекст отображения данных.

Библиотека Chart.js изначально не предоставляет встроенной глобальной синхронизации между графиками, однако её архитектура на основе событий, плагинов и программного управления состоянием позволяет реализовать синхронное поведение через пользовательские механизмы.


Архитектурная основа взаимодействия графиков

Каждый график Chart.js представляет собой независимый экземпляр класса Chart, содержащий:

  • данные (data)
  • конфигурацию (options)
  • состояние взаимодействия (hover, tooltip, active elements)
  • масштабирование и трансформации (zoom, pan при использовании плагинов)

Синхронизация достигается за счёт:

  • перехвата событий взаимодействия (mousemove, mouseout, click)
  • использования API методов setActiveElements, update, resetZoom
  • передачи контекста между экземплярами графиков
  • общих обработчиков событий или event bus

Синхронизация всплывающих подсказок (tooltips)

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

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

Базовая модель синхронизации tooltip

function syncTooltips(sourceChart, charts, event) {
    const points = sourceChart.getElementsAtEventForMode(
        event,
        'index',
        { intersect: false },
        false
    );

    if (!points.length) return;

    const index = points[0].index;

    charts.forEach(chart => {
        const activeElements = chart.data.datasets.map((_, datasetIndex) => ({
            datasetIndex,
            index
        }));

        chart.setActiveElements(activeElements);
        chart.tooltip.setActiveElements(activeElements, {
            x: event.x,
            y: event.y
        });

        chart.update('none');
    });
}

Ключевые особенности подхода

  • используется единый индекс категории
  • активные элементы синхронизируются по datasetIndex и index
  • обновление выполняется без анимации ('none') для минимизации задержек

Синхронизация масштабирования и панорамирования

При использовании chartjs-plugin-zoom появляется возможность синхронизировать масштаб и прокрутку.

Основной принцип

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

  • chart.scales
  • chart.getZoomedScaleBounds
  • события zoom и pan

Пример синхронизации масштаба по оси X

function syncZoom(sourceChart, charts) {
    const xScale = sourceChart.scales.x;

    const min = xScale.min;
    const max = xScale.max;

    charts.forEach(chart => {
        if (chart === sourceChart) return;

        chart.options.scales.x.min = min;
        chart.options.scales.x.max = max;

        chart.update('none');
    });
}

Особенности реализации

  • синхронизация выполняется только по нужным осям (обычно x)
  • важно предотвращать рекурсивные обновления
  • рекомендуется использовать флаг блокировки событий

Предотвращение циклических обновлений

При синхронизации нескольких графиков возникает риск бесконечного цикла обновлений. Решение основано на временном отключении обработчиков.

let isSyncing = false;

function safeSync(callback) {
    if (isSyncing) return;
    isSyncing = true;

    callback();

    isSyncing = false;
}

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

chart.canvas.addEventListener('mousemove', (event) => {
    safeSync(() => syncTooltips(chart, charts, event));
});

Синхронизация курсора (crosshair effect)

Crosshair позволяет отображать вертикальную или горизонтальную линию на всех графиках одновременно.

Реализация обычно выполняется через пользовательский плагин.

Базовый плагин crosshair

const crosshairPlugin = {
    id: 'crosshairSync',

    afterEvent(chart, args) {
        const event = args.event;

        if (!event || event.type !== 'mousemove') return;

        syncCrosshair(chart, event);
    }
};

Логика отрисовки

function syncCrosshair(sourceChart, event) {
    const x = event.x;

    window.syncedCharts.forEach(chart => {
        chart.crosshairX = x;
        chart.draw();
    });
}

Единый event bus для управления графиками

При большом количестве графиков локальная синхронизация становится сложной. Используется централизованный event bus.

Реализация через простую шину событий

const bus = {
    events: {},

    on(event, handler) {
        if (!this.events[event]) this.events[event] = [];
        this.events[event].push(handler);
    },

    emit(event, payload) {
        (this.events[event] || []).forEach(h => h(payload));
    }
};

Подключение графиков к шине

bus.on('tooltip-sync', ({ chart, event }) => {
    syncTooltips(chart, window.charts, event);
});

bus.on('zoom-sync', ({ chart }) => {
    syncZoom(chart, window.charts);
});

Генерация событий

chart.canvas.addEventListener('mousemove', (event) => {
    bus.emit('tooltip-sync', { chart, event });
});

Синхронизация выделения элементов (hover/active state)

Chart.js хранит активные элементы в массиве activeElements. Их синхронизация позволяет выделять одинаковые точки на нескольких графиках.

function syncActiveElements(sourceChart, charts, event) {
    const elements = sourceChart.getElementsAtEventForMode(
        event,
        'nearest',
        { intersect: true },
        false
    );

    charts.forEach(chart => {
        chart.setActiveElements(elements);
        chart.update('none');
    });
}

Особенность заключается в том, что активные элементы должны соответствовать одинаковой структуре datasets.


Синхронизация временных шкал

При работе с временными рядами часто используется тип оси time. Синхронизация требует унификации:

  • формата времени
  • диапазона отображения
  • шагов тиков

Пример выравнивания диапазона

function syncTimeScale(sourceChart, charts) {
    const scale = sourceChart.scales.x;

    charts.forEach(chart => {
        chart.options.scales.x.min = scale.min;
        chart.options.scales.x.max = scale.max;
        chart.update('none');
    });
}

Производительность при синхронизации

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

  • частоту событий mousemove
  • стоимость перерисовки (update)
  • использование requestAnimationFrame
  • отключение анимации

Оптимизация через throttling

function throttle(fn, delay) {
    let lastCall = 0;

    return (...args) => {
        const now = Date.now();
        if (now - lastCall < delay) return;
        lastCall = now;
        fn(...args);
    };
}

Групповое управление экземплярами Chart

Для упрощения синхронизации используется контейнер:

const chartGroup = {
    charts: [],

    add(chart) {
        this.charts.push(chart);
    },

    syncTooltip(source, event) {
        syncTooltips(source, this.charts, event);
    },

    syncZoom(source) {
        syncZoom(source, this.charts);
    }
};

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


Поведение при асинхронных обновлениях данных

Если графики получают данные из разных источников, синхронизация требует учета:

  • задержек загрузки
  • частичных обновлений datasets
  • пересчета масштаба после update

Стратегия согласования

  • блокировка синхронизации до завершения обновления всех графиков
  • единый trigger dataReady
  • повторная инициализация шкал
Promise.all(dataPromises).then(() => {
    charts.forEach(chart => chart.update());
    syncZoom(referenceChart, charts);
});

Использование пользовательских плагинов для синхронизации

Chart.js plugin API позволяет встроить синхронизацию на уровне жизненного цикла:

  • beforeInit
  • afterEvent
  • afterUpdate
  • afterRender

Пример объединённого плагина

const syncPlugin = {
    id: 'syncPlugin',

    afterEvent(chart, args) {
        const event = args.event;
        if (!event) return;

        syncTooltips(chart, window.charts, event);
    },

    afterUpdate(chart) {
        syncZoom(chart, window.charts);
    }
};

Масштабируемая модель синхронизации

При увеличении количества графиков применяется многослойная модель:

  • слой взаимодействия (events)
  • слой логики синхронизации (bus, controllers)
  • слой визуализации (Chart instances)

Такое разделение позволяет:

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