Кастомная отрисовка легенды через legendItems

В библиотеке Chart.js легенда графика формируется автоматически на основе наборов данных (datasets). Каждый элемент легенды представляет отдельную серию данных и содержит:

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

Массив объектов легенды называется legendItems. Он используется внутренним механизмом Chart.js для генерации визуальных элементов легенды и может быть полностью переопределён.

Стандартная генерация легенды выполняется через:

options: {
    plugins: {
        legend: {
            labels: {
                generateLabels(chart) {
                    // Возврат массива legendItems
                }
            }
        }
    }
}

Метод generateLabels() позволяет вручную создавать элементы легенды, управляя их содержимым и поведением.


Структура объекта legendItem

Каждый элемент массива представляет объект со специальными свойствами.

Пример:

{
    text: 'Продажи',
    fillStyle: '#36A2EB',
    strokeStyle: '#1E88E5',
    lineWidth: 2,
    hidden: false,
    datasetIndex: 0
}

Основные свойства:

Свойство Назначение
text Текст легенды
fillStyle Цвет заливки маркера
strokeStyle Цвет рамки
lineWidth Толщина линии
hidden Скрыт ли dataset
datasetIndex Индекс набора данных
lineCap Стиль окончания линии
lineDash Пунктир
lineJoin Соединение линий
borderRadius Скругление
fontColor Цвет текста
pointStyle Тип маркера

Полная кастомизация generateLabels

Базовый пример

const chart = new Chart(ctx, {
    type: 'bar',

    data: {
        labels: ['Январь', 'Февраль', 'Март'],
        datasets: [
            {
                label: 'Доход',
                data: [10, 20, 30],
                backgroundColor: '#4CAF50'
            },
            {
                label: 'Расход',
                data: [15, 12, 18],
                backgroundColor: '#F44336'
            }
        ]
    },

    options: {
        plugins: {
            legend: {
                labels: {
                    generateLabels(chart) {

                        const datasets = chart.data.datasets;

                        return datasets.map((dataset, index) => ({
                            text: dataset.label,
                            fillStyle: dataset.backgroundColor,
                            hidden: !chart.isDatasetVisible(index),
                            datasetIndex: index
                        }));

                    }
                }
            }
        }
    }
});

В данном примере:

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

Получение стандартных legendItems

Во многих случаях требуется не полная замена легенды, а модификация стандартной логики.

Для этого используется встроенный генератор:

Chart.defaults.plugins.legend.labels.generateLabels(chart)

Пример:

generateLabels(chart) {

    const original =
        Chart.defaults.plugins.legend.labels.generateLabels(chart);

    return original.map(item => {

        item.text = 'Серия: ' + item.text;

        return item;
    });
}

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

  • сохранить встроенную механику;
  • изменить только нужные свойства;
  • избежать конфликтов с внутренними алгоритмами Chart.js.

Изменение текста легенды

Добавление вычисляемых значений

Часто легенда должна содержать дополнительную информацию.

Например:

  • сумму значений;
  • среднее;
  • проценты;
  • динамические показатели.

Пример:

generateLabels(chart) {

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

        const total = dataset.data.reduce((a, b) => a + b, 0);

        return {
            text: `${dataset.label} (${total})`,
            fillStyle: dataset.backgroundColor,
            hidden: !chart.isDatasetVisible(index),
            datasetIndex: index
        };

    });
}

Результат:

Доход (60)
Расход (45)

Добавление процентов

generateLabels(chart) {

    const grandTotal = chart.data.datasets
        .flatMap(ds => ds.data)
        .reduce((a, b) => a + b, 0);

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

        const total = dataset.data.reduce((a, b) => a + b, 0);

        const percent =
            ((total / grandTotal) * 100).toFixed(1);

        return {
            text: `${dataset.label} — ${percent}%`,
            fillStyle: dataset.backgroundColor,
            datasetIndex: index
        };

    });
}

Управление цветами legendItems

Динамическая смена цвета

generateLabels(chart) {

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

        text: dataset.label,

        fillStyle:
            dataset.data[0] > 20
                ? '#4CAF50'
                : '#F44336',

        datasetIndex: index
    }));
}

Градиентные легенды

generateLabels(chart) {

    const ctx = chart.ctx;

    const gradient = ctx.createLinearGradient(0, 0, 100, 0);

    gradient.addColorStop(0, '#2196F3');
    gradient.addColorStop(1, '#9C27B0');

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

        text: dataset.label,
        fillStyle: gradient,
        datasetIndex: index

    }));
}

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

Chart.js поддерживает различные формы маркеров.

Пример:

labels: {
    usePointStyle: true,

    generateLabels(chart) {

        return chart.data.datasets.map((dataset, index) => ({
            text: dataset.label,
            fillStyle: dataset.backgroundColor,
            pointStyle: 'triangle',
            datasetIndex: index
        }));
    }
}

Доступные значения:

Значение Форма
circle Круг
triangle Треугольник
rect Прямоугольник
rectRounded Скруглённый
rectRot Повернутый квадрат
cross Крест
crossRot Поворотный крест
star Звезда
line Линия
dash Пунктир

Скрытие отдельных элементов легенды

Иногда часть datasets не должна отображаться.

Пример:

generateLabels(chart) {

    return chart.data.datasets
        .filter(dataset => !dataset.hiddenInLegend)
        .map((dataset, index) => ({
            text: dataset.label,
            fillStyle: dataset.backgroundColor,
            datasetIndex: index
        }));
}

Dataset:

{
    label: 'Служебные данные',
    hiddenInLegend: true
}

Кастомная сортировка legendItems

Сортировка по сумме значений

generateLabels(chart) {

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

            const total =
                dataset.data.reduce((a, b) => a + b, 0);

            return {
                text: dataset.label,
                fillStyle: dataset.backgroundColor,
                datasetIndex: index,
                total
            };

        })
        .sort((a, b) => b.total - a.total);
}

Алфавитная сортировка

.sort((a, b) => a.text.localeCompare(b.text))

Добавление пользовательских свойств

Объекты legendItems могут содержать собственные поля.

generateLabels(chart) {

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

        text: dataset.label,
        fillStyle: dataset.backgroundColor,
        datasetIndex: index,

        customData: {
            total: dataset.data.reduce((a, b) => a + b, 0),
            category: dataset.category
        }

    }));
}

Такие свойства удобно использовать в:

  • кастомных HTML-легендах;
  • обработчиках событий;
  • плагинах.

Полное отключение стандартной легенды

Для собственной HTML-легенды встроенную необходимо отключить.

plugins: {
    legend: {
        display: false
    }
}

После этого legendItems можно использовать вручную.


Создание HTML-легенды через legendItems

Получение элементов

const items =
    chart.options.plugins.legend.labels.generateLabels(chart);

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

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

items.forEach(item => {

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

    div.innerHTML = `
        <span style="
            display:inline-block;
            width:12px;
            height:12px;
            background:${item.fillStyle};
            margin-right:8px;
        "></span>

        ${item.text}
    `;

    legendContainer.appendChild(div);

});

Интерактивная HTML-легенда

Переключение datasets

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

    chart.toggleDataVisibility(item.datasetIndex);

    chart.update();

});

Полный пример

const chart = new Chart(ctx, {

    type: 'line',

    data: {
        labels: ['A', 'B', 'C'],
        datasets: [
            {
                label: 'Продажи',
                data: [10, 20, 30],
                borderColor: '#2196F3'
            },
            {
                label: 'Прибыль',
                data: [5, 15, 25],
                borderColor: '#F44336'
            }
        ]
    },

    options: {
        plugins: {
            legend: {
                display: false
            }
        }
    }
});

const legend =
    document.getElementById('legend');

const items =
    Chart.defaults.plugins.legend.labels.generateLabels(chart);

items.forEach(item => {

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

    button.textContent = item.text;

    button.style.border =
        `2px solid ${item.strokeStyle}`;

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

        chart.setDatasetVisibility(
            item.datasetIndex,
            !chart.isDatasetVisible(item.datasetIndex)
        );

        chart.update();

    });

    legend.appendChild(button);

});

Работа с Doughnut и Pie диаграммами

У круговых диаграмм legendItems работают иначе.

Вместо datasets легенда обычно создаётся по отдельным сегментам.

Пример:

generateLabels(chart) {

    const data = chart.data;

    return data.labels.map((label, index) => ({

        text: label,

        fillStyle:
            data.datasets[0].backgroundColor[index],

        hidden: !chart.getDataVisibility(index),

        index

    }));
}

Управление сегментами Pie Chart

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

    chart.toggleDataVisibility(item.index);

    chart.update();

});

Использование legendItems внутри плагинов

legendItems особенно полезны при создании пользовательских плагинов.

Пример:

const customLegendPlugin = {

    id: 'customLegendPlugin',

    afterUpdate(chart) {

        const items =
            chart.options.plugins.legend.labels
                .generateLabels(chart);

        console.log(items);

    }
};

Регистрация:

Chart.register(customLegendPlugin);

Добавление иконок в legendItems

generateLabels(chart) {

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

        text: '⬤ ' + dataset.label,
        fillStyle: dataset.backgroundColor,
        datasetIndex: index

    }));
}

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

generateLabels(chart) {

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

        const visible =
            chart.isDatasetVisible(index);

        return {

            text: dataset.label,

            fillStyle:
                visible
                    ? dataset.backgroundColor
                    : '#BDBDBD',

            fontColor:
                visible
                    ? '#000'
                    : '#999',

            hidden: !visible,

            datasetIndex: index
        };

    });
}

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

generateLabels(chart) {

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

        text: dataset.label,
        strokeStyle: dataset.borderColor,
        lineDash: [5, 5],
        lineWidth: 3,
        fillStyle: 'transparent',
        datasetIndex: index

    }));
}

Кастомная геометрия маркеров

Скругление

borderRadius: 10

Толщина рамки

lineWidth: 4

Прозрачность

fillStyle: 'rgba(33,150,243,0.3)'

Производительность при большом количестве legendItems

При работе с десятками и сотнями элементов рекомендуется:

  • не выполнять тяжёлые вычисления внутри generateLabels;
  • кэшировать результаты;
  • избегать постоянного создания DOM-элементов;
  • использовать виртуализацию HTML-легенд;
  • обновлять только изменённые элементы.

Плохой подход:

generateLabels(chart) {

    return expensiveCalculation();
}

Лучший вариант:

const cache = [];

generateLabels(chart) {

    if (cache.length) {
        return cache;
    }

    cache.push(
        ...chart.data.datasets.map((dataset, index) => ({
            text: dataset.label,
            datasetIndex: index
        }))
    );

    return cache;
}

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

Отсутствие datasetIndex

Без datasetIndex переключение видимости перестаёт работать.

Неправильно:

{
    text: dataset.label
}

Правильно:

{
    text: dataset.label,
    datasetIndex: index
}

Возврат не массива

Неправильно:

return {};

Правильно:

return [];

Использование HTML внутри text

Chart.js не рендерит HTML внутри canvas-легенды.

Неправильно:

text: '<b>Продажи</b>'

Архитектура кастомных легенд

Наиболее масштабируемая структура:

datasets
    ↓
generateLabels()
    ↓
legendItems
    ↓
HTML renderer
    ↓
обработчики событий
    ↓
chart.update()

Такой подход обеспечивает:

  • полную независимость от встроенной легенды;
  • интеграцию с любым UI;
  • поддержку React/Vue/Angular;
  • динамическое обновление интерфейса;
  • сложную бизнес-логику отображения.