Обработка кликов по легенде

Легенда в Chart.js представляет собой интерактивный элемент интерфейса, отображающий список наборов данных графика. По умолчанию легенда умеет:

  • включать и отключать отображение datasets;
  • отображать цветовые маркеры;
  • показывать названия серий;
  • реагировать на наведение и клики.

Стандартное поведение особенно полезно при работе с графиками, содержащими большое количество наборов данных.

Базовая конфигурация легенды располагается в разделе plugins.legend.

const config = {
    type: 'line',
    data,
    options: {
        plugins: {
            legend: {
                display: true
            }
        }
    }
};

Стандартная обработка кликов

По умолчанию Chart.js автоматически скрывает или показывает dataset при клике по элементу легенды.

const config = {
    type: 'bar',
    data: {
        labels: ['Январь', 'Февраль', 'Март'],
        datasets: [
            {
                label: 'Продажи',
                data: [10, 20, 30],
                backgroundColor: 'royalblue'
            },
            {
                label: 'Расходы',
                data: [5, 15, 25],
                backgroundColor: 'tomato'
            }
        ]
    }
};

При клике по пункту «Продажи» соответствующий dataset скрывается. Повторный клик возвращает его обратно.


Свойство onClick

Для переопределения стандартного поведения используется обработчик onClick.

options: {
    plugins: {
        legend: {
            onClick(event, legendItem, legend) {

            }
        }
    }
}

Аргументы обработчика

Аргумент Описание
event Объект события браузера
legendItem Информация о нажатом элементе
legend Экземпляр легенды

Получение индекса набора данных

Каждый элемент легенды связан с dataset через свойство datasetIndex.

onClick(event, legendItem, legend) {
    const index = legendItem.datasetIndex;

    console.log(index);
}

Значение индекса соответствует позиции dataset внутри массива datasets.

datasets: [
    { label: 'A' }, // index = 0
    { label: 'B' }, // index = 1
    { label: 'C' }  // index = 2
]

Получение экземпляра графика

Экземпляр графика доступен через legend.chart.

onClick(event, legendItem, legend) {
    const chart = legend.chart;

    console.log(chart);
}

Через объект графика можно:

  • изменять данные;
  • скрывать datasets;
  • обновлять отображение;
  • запускать анимации;
  • изменять конфигурацию.

Скрытие и отображение datasets вручную

Chart.js предоставляет методы hide() и show().

onClick(event, legendItem, legend) {
    const chart = legend.chart;
    const index = legendItem.datasetIndex;

    if (chart.isDatasetVisible(index)) {
        chart.hide(index);
    } else {
        chart.show(index);
    }
}

Проверка видимости

Метод:

chart.isDatasetVisible(index)

возвращает true или false.


Полное переопределение стандартного поведения

При указании собственного onClick встроенная логика больше не используется.

Стандартный механизм можно восстановить вручную.

const defaultLegendClickHandler =
    Chart.defaults.plugins.legend.onClick;

const config = {
    type: 'line',
    data,
    options: {
        plugins: {
            legend: {
                onClick(event, legendItem, legend) {

                    console.log('Клик по легенде');

                    defaultLegendClickHandler(
                        event,
                        legendItem,
                        legend
                    );
                }
            }
        }
    }
};

Подобная схема позволяет расширять стандартное поведение без его потери.


Изменение внешнего вида при клике

Обработчик может менять стили datasets.

Изменение прозрачности

onClick(event, legendItem, legend) {

    const chart = legend.chart;
    const dataset =
        chart.data.datasets[legendItem.datasetIndex];

    dataset.backgroundColor = 'rgba(65, 105, 225, 0.2)';

    chart.update();
}

Динамическое изменение цвета линии

onClick(event, legendItem, legend) {

    const chart = legend.chart;

    const dataset =
        chart.data.datasets[legendItem.datasetIndex];

    dataset.borderColor = 'red';

    chart.update();
}

Переключение толщины линии

onClick(event, legendItem, legend) {

    const chart = legend.chart;

    const dataset =
        chart.data.datasets[legendItem.datasetIndex];

    dataset.borderWidth =
        dataset.borderWidth === 2 ? 6 : 2;

    chart.update();
}

Работа с несколькими datasets одновременно

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

Скрытие всех datasets кроме выбранного

onClick(event, legendItem, legend) {

    const chart = legend.chart;
    const selectedIndex = legendItem.datasetIndex;

    chart.data.datasets.forEach((dataset, index) => {

        if (index === selectedIndex) {
            chart.show(index);
        } else {
            chart.hide(index);
        }
    });
}

Подобный режим напоминает фильтрацию данных.


Режим одиночного выбора

Иногда требуется поведение, аналогичное radio-button.

onClick(event, legendItem, legend) {

    const chart = legend.chart;
    const clickedIndex = legendItem.datasetIndex;

    const visibleCount =
        chart.data.datasets.filter((_, i) =>
            chart.isDatasetVisible(i)
        ).length;

    chart.data.datasets.forEach((_, i) => {

        if (i === clickedIndex) {

            if (visibleCount === 1 &&
                chart.isDatasetVisible(i)) {

                chart.show(i);

            } else {
                chart.show(i);
            }

        } else {
            chart.hide(i);
        }
    });
}

Переключение групп datasets

При сложных визуализациях datasets могут объединяться в логические группы.

const groups = {
    sales: [0, 1],
    expenses: [2, 3]
};

onClick(event, legendItem, legend) {

    const chart = legend.chart;

    const groupName =
        legendItem.text.toLowerCase();

    const indexes = groups[groupName];

    indexes.forEach(index => {

        if (chart.isDatasetVisible(index)) {
            chart.hide(index);
        } else {
            chart.show(index);
        }
    });
}

Изменение данных графика по клику

Обработчик способен полностью менять содержимое datasets.

onClick(event, legendItem, legend) {

    const chart = legend.chart;

    chart.data.datasets[0].data = [
        100,
        200,
        300
    ];

    chart.update();
}

Добавление новых datasets

onClick(event, legendItem, legend) {

    const chart = legend.chart;

    chart.data.datasets.push({
        label: 'Новый dataset',
        data: [5, 15, 25],
        backgroundColor: 'green'
    });

    chart.update();
}

Удаление datasets

onClick(event, legendItem, legend) {

    const chart = legend.chart;

    chart.data.datasets.splice(
        legendItem.datasetIndex,
        1
    );

    chart.update();
}

Использование пользовательских состояний

Часто требуется хранить дополнительные флаги.

const state = {
    selectedDataset: null
};

const config = {
    options: {
        plugins: {
            legend: {
                onClick(event, legendItem, legend) {

                    state.selectedDataset =
                        legendItem.datasetIndex;

                    console.log(state);
                }
            }
        }
    }
};

Работа с анимацией

После изменения графика обычно вызывается:

chart.update();

Метод запускает повторную отрисовку с анимацией.

Отключение анимации

chart.update('none');

Использование события onHover

Легенда поддерживает реакцию на наведение.

legend: {

    onHover(event, legendItem, legend) {

        event.native.target.style.cursor =
            'pointer';
    }
}

Использование события onLeave

legend: {

    onLeave(event, legendItem, legend) {

        event.native.target.style.cursor =
            'default';
    }
}

Изменение прозрачности остальных линий

Популярный интерфейсный приём — выделение выбранной серии.

onClick(event, legendItem, legend) {

    const chart = legend.chart;
    const selected = legendItem.datasetIndex;

    chart.data.datasets.forEach((dataset, index) => {

        if (index === selected) {

            dataset.borderWidth = 5;

        } else {

            dataset.borderWidth = 1;
        }
    });

    chart.update();
}

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

Chart.js позволяет полностью изменить содержимое легенды.

legend: {

    labels: {

        generateLabels(chart) {

            return chart.data.datasets.map(
                (dataset, index) => ({

                    text: dataset.label,
                    fillStyle: dataset.backgroundColor,
                    hidden: !chart.isDatasetVisible(index),
                    datasetIndex: index
                })
            );
        }
    }
}

После этого onClick продолжает работать с пользовательскими элементами.


Создание собственной HTML-легенды

Canvas-легенда имеет ограничения. Для полной свободы часто создаётся HTML-версия.

Контейнер

<div id="legend-container"></div>
<canvas id="myChart"></canvas>

Генерация элементов

const legendContainer =
    document.getElementById('legend-container');

chart.data.datasets.forEach((dataset, index) => {

    const button =
        document.createElement('button');

    button.textContent = dataset.label;

    button.addEventListener('click', () => {

        if (chart.isDatasetVisible(index)) {
            chart.hide(index);
        } else {
            chart.show(index);
        }
    });

    legendContainer.appendChild(button);
});

Преимущества HTML-легенды

HTML-подход позволяет:

  • использовать любые CSS-стили;
  • добавлять иконки;
  • внедрять анимации;
  • создавать выпадающие списки;
  • использовать flex/grid;
  • интегрировать элементы интерфейса;
  • подключать framework-компоненты.

Обработка двойного клика

Chart.js не предоставляет встроенный dblclick, однако обработку можно реализовать вручную.

let lastClick = 0;

onClick(event, legendItem, legend) {

    const currentTime = Date.now();

    if (currentTime - lastClick < 300) {

        console.log('Двойной клик');
    }

    lastClick = currentTime;
}

Использование внешнего состояния приложения

При интеграции с React, Vue или Angular обработчик легенды часто обновляет состояние приложения.

onClick(event, legendItem, legend) {

    appState.activeDataset =
        legendItem.datasetIndex;

    renderUI();
}

Особенности производительности

Частые вызовы chart.update() могут снижать производительность при больших объёмах данных.

Оптимизации:

  • группировка изменений перед update();
  • отключение анимации;
  • минимизация количества datasets;
  • использование debounce/throttle;
  • обновление только необходимых параметров.

Типичные ошибки

Отсутствие chart.update()

Изменения datasets не отображаются без обновления графика.

dataset.borderColor = 'red';

// Нет update()

Неверный datasetIndex

const dataset =
    chart.data.datasets[legendItem.datasetIndex];

При удалении datasets индексы могут изменяться.


Конфликт со стандартным поведением

Если вызывается собственная логика и стандартный обработчик одновременно, возможно двойное переключение видимости.

defaultLegendClickHandler(...);

// Дополнительный hide/show

Архитектура сложной логики

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

function handleLegendClick(
    event,
    legendItem,
    legend
) {

    const chart = legend.chart;

    toggleDataset(
        chart,
        legendItem.datasetIndex
    );
}

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

class LegendController {

    constructor(chart) {
        this.chart = chart;
    }

    toggle(index) {

        if (this.chart.isDatasetVisible(index)) {
            this.chart.hide(index);
        } else {
            this.chart.show(index);
        }
    }
}

Интеграция с плагинами

Плагины Chart.js способны перехватывать события легенды.

const plugin = {

    id: 'legendPlugin',

    beforeEvent(chart, args) {

        console.log(args.event);
    }
};

Подключение:

new Chart(ctx, {
    type: 'line',
    data,
    plugins: [plugin]
});

Практический пример комплексной обработки

const config = {

    type: 'line',

    data: {

        labels: ['A', 'B', 'C'],

        datasets: [
            {
                label: 'Продажи',
                data: [10, 20, 30],
                borderColor: 'blue'
            },
            {
                label: 'Расходы',
                data: [5, 15, 25],
                borderColor: 'red'
            }
        ]
    },

    options: {

        plugins: {

            legend: {

                onClick(event, legendItem, legend) {

                    const chart = legend.chart;

                    const index =
                        legendItem.datasetIndex;

                    chart.data.datasets.forEach(
                        (dataset, i) => {

                            dataset.borderWidth =
                                i === index ? 5 : 1;
                        }
                    );

                    if (
                        chart.isDatasetVisible(index)
                    ) {
                        chart.hide(index);
                    } else {
                        chart.show(index);
                    }

                    chart.update();
                },

                onHover(event) {

                    event.native.target.style.cursor =
                        'pointer';
                },

                onLeave(event) {

                    event.native.target.style.cursor =
                        'default';
                }
            }
        }
    }
};