Фильтрация и сортировка элементов легенды

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

  • фильтрацию элементов;
  • сортировку;
  • изменение подписей;
  • скрытие отдельных записей;
  • динамическое обновление списка.

Настройка выполняется через объект:

options: {
    plugins: {
        legend: {
            // параметры
        }
    }
}

Фильтрация и сортировка особенно важны при работе:

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

Структура объекта legend.labels

Управление элементами легенды выполняется через объект:

options.plugins.legend.labels

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

Свойство Назначение
generateLabels Полная генерация элементов легенды
filter Фильтрация элементов
sort Сортировка элементов
usePointStyle Использование маркеров точек
pointStyle Тип маркера
color Цвет текста
font Настройки шрифта

Наиболее важными для управления отображением являются:

labels: {
    filter: function(item, data) {},
    sort: function(a, b, data) {}
}

Принцип работы фильтрации

Метод filter вызывается для каждого элемента легенды.

Если функция возвращает:

  • true — элемент отображается;
  • false — элемент скрывается.

Сигнатура:

filter(legendItem, chartData)

Параметры:

Параметр Описание
legendItem Объект текущего элемента легенды
chartData Данные графика

Простейшая фильтрация

Скрытие определённого набора данных:

const config = {
    type: 'bar',

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

    options: {
        plugins: {
            legend: {
                labels: {
                    filter: function(item) {
                        return item.text !== 'Расходы';
                    }
                }
            }
        }
    }
};

В легенде будет отображаться только элемент "Продажи".


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

Объект legendItem содержит информацию об элементе легенды.

Пример структуры:

{
    text: 'Продажи',
    fillStyle: 'blue',
    strokeStyle: 'black',
    hidden: false,
    datasetIndex: 0,
    lineCap: 'butt',
    lineDash: [],
    lineJoin: 'miter',
    lineWidth: 1
}

Наиболее полезные свойства:

Свойство Назначение
text Текст элемента
datasetIndex Индекс набора данных
hidden Состояние скрытия
fillStyle Цвет маркера
strokeStyle Цвет границы

Фильтрация по индексу набора данных

Иногда требуется скрыть технические или служебные datasets.

labels: {
    filter: function(item) {
        return item.datasetIndex !== 0;
    }
}

Первый набор данных не попадёт в легенду.


Фильтрация по пользовательским свойствам

Datasets могут содержать дополнительные поля.

datasets: [
    {
        label: 'Доход',
        data: [10, 20, 30],
        visibleInLegend: true
    },
    {
        label: 'Служебные данные',
        data: [5, 10, 15],
        visibleInLegend: false
    }
]

Фильтр:

labels: {
    filter: function(item, data) {

        const dataset =
            data.datasets[item.datasetIndex];

        return dataset.visibleInLegend;
    }
}

Такой подход особенно удобен в крупных приложениях.


Динамическая фильтрация

Фильтрация может зависеть от состояния интерфейса.

let showExpenses = false;

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

    data: {
        labels: ['Пн', 'Вт', 'Ср'],
        datasets: [
            {
                label: 'Доход',
                data: [100, 200, 150]
            },
            {
                label: 'Расход',
                data: [50, 70, 90]
            }
        ]
    },

    options: {
        plugins: {
            legend: {
                labels: {
                    filter: function(item) {

                        if (
                            item.text === 'Расход' &&
                            !showExpenses
                        ) {
                            return false;
                        }

                        return true;
                    }
                }
            }
        }
    }
});

После изменения переменной:

showExpenses = true;
chart.update();

легенда будет перестроена.


Полное исключение скрытых datasets

Chart.js позволяет скрывать наборы данных программно:

chart.hide(1);

Однако скрытый dataset всё равно остаётся в легенде. Для удаления из списка:

labels: {
    filter: function(item, data) {

        const dataset =
            data.datasets[item.datasetIndex];

        return !dataset.hidden;
    }
}

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

Свойство sort определяет порядок отображения элементов.

Сигнатура:

sort(a, b, data)

Возвращаемые значения:

Значение Результат
< 0 a раньше b
> 0 b раньше a
0 порядок не меняется

Простая сортировка по алфавиту

labels: {
    sort: function(a, b) {

        return a.text.localeCompare(b.text);
    }
}

Результат:

Доход
Продажи
Расходы

Обратная сортировка

labels: {
    sort: function(a, b) {

        return b.text.localeCompare(a.text);
    }
}

Сортировка по datasetIndex

labels: {
    sort: function(a, b) {

        return a.datasetIndex - b.datasetIndex;
    }
}

Такой подход позволяет явно контролировать порядок.


Пользовательский порядок отображения

Создание приоритетов:

const order = {
    'Доход': 1,
    'Расход': 2,
    'Налоги': 3
};

Сортировка:

labels: {
    sort: function(a, b) {

        return order[a.text] - order[b.text];
    }
}

Сортировка по значениям datasets

Можно сортировать легенду по данным графика.

Например, по сумме значений:

labels: {
    sort: function(a, b, data) {

        const datasetA =
            data.datasets[a.datasetIndex];

        const datasetB =
            data.datasets[b.datasetIndex];

        const sumA =
            datasetA.data.reduce(
                (acc, value) => acc + value,
                0
            );

        const sumB =
            datasetB.data.reduce(
                (acc, value) => acc + value,
                0
            );

        return sumB - sumA;
    }
}

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


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

labels: {
    sort: function(a, b, data) {

        function average(arr) {
            return arr.reduce(
                (acc, val) => acc + val,
                0
            ) / arr.length;
        }

        const avgA = average(
            data.datasets[a.datasetIndex].data
        );

        const avgB = average(
            data.datasets[b.datasetIndex].data
        );

        return avgB - avgA;
    }
}

Комбинирование filter и sort

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

labels: {

    filter: function(item) {
        return item.text !== 'Технические';
    },

    sort: function(a, b) {
        return a.text.localeCompare(b.text);
    }
}

Порядок работы:

  1. Генерация элементов;
  2. Фильтрация;
  3. Сортировка;
  4. Отрисовка.

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

Для полного контроля применяется метод:

generateLabels(chart)

Он позволяет:

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

Пример собственной генерации

labels: {

    generateLabels: function(chart) {

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

                return {
                    text:
                        dataset.label +
                        ' (' +
                        dataset.data.length +
                        ')',

                    fillStyle:
                        dataset.backgroundColor,

                    hidden: false,

                    datasetIndex: index
                };
            }
        );
    }
}

Результат:

Продажи (12)
Расходы (12)

Фильтрация внутри generateLabels

labels: {

    generateLabels: function(chart) {

        return chart.data.datasets
            .filter(dataset => {
                return dataset.showLegend;
            })
            .map((dataset, index) => {

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

Группировка элементов легенды

Иногда несколько datasets должны отображаться как один элемент.

generateLabels: function(chart) {

    return [
        {
            text: 'Финансовые показатели',
            fillStyle: 'green',
            datasetIndex: 0
        }
    ];
}

Сортировка внутри generateLabels

generateLabels: function(chart) {

    return chart.data.datasets
        .sort((a, b) => {

            return b.priority - a.priority;
        })
        .map((dataset, index) => {

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

Работа с круговыми диаграммами

В pie и doughnut легенда создаётся иначе: элементы соответствуют не datasets, а сегментам.

Пример:

data: {
    labels: ['Chrome', 'Firefox', 'Safari'],
    datasets: [{
        data: [65, 20, 15]
    }]
}

Каждый label становится отдельным элементом легенды.


Фильтрация сегментов pie chart

labels: {
    filter: function(item) {

        return item.text !== 'Safari';
    }
}

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

labels: {
    sort: function(a, b) {

        return a.text.localeCompare(b.text);
    }
}

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

const config = {

    type: 'line',

    data: {

        labels: [
            'Янв',
            'Фев',
            'Мар'
        ],

        datasets: [
            {
                label: 'Доход',
                data: [100, 200, 300],
                priority: 1
            },

            {
                label: 'Расход',
                data: [50, 70, 90],
                priority: 2
            },

            {
                label: 'Служебные',
                data: [1, 2, 3],
                hiddenInLegend: true,
                priority: 999
            }
        ]
    },

    options: {

        plugins: {

            legend: {

                labels: {

                    filter: function(item, data) {

                        const dataset =
                            data.datasets[
                                item.datasetIndex
                            ];

                        return !dataset.hiddenInLegend;
                    },

                    sort: function(a, b, data) {

                        const datasetA =
                            data.datasets[
                                a.datasetIndex
                            ];

                        const datasetB =
                            data.datasets[
                                b.datasetIndex
                            ];

                        return (
                            datasetA.priority -
                            datasetB.priority
                        );
                    }
                }
            }
        }
    }
};

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

При работе с десятками и сотнями datasets необходимо учитывать:

  • filter вызывается для каждого элемента;
  • sort может выполняться многократно;
  • тяжёлые вычисления замедляют рендеринг.

Неэффективный код:

sort: function(a, b, data) {

    // сложные вычисления
}

Предпочтительно заранее вычислять значения:

dataset.total = calculateTotal(dataset);

Кэширование вычислений

Оптимизированный вариант:

datasets.forEach(dataset => {

    dataset.cachedTotal =
        dataset.data.reduce(
            (a, b) => a + b,
            0
        );
});

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

sort: function(a, b, data) {

    return (
        data.datasets[b.datasetIndex]
            .cachedTotal -

        data.datasets[a.datasetIndex]
            .cachedTotal
    );
}

Частые ошибки

Использование item.label вместо item.text

Неверно:

filter: function(item) {
    return item.label !== 'Расход';
}

Правильно:

filter: function(item) {
    return item.text !== 'Расход';
}

Отсутствие return

Неверно:

filter: function(item) {

    item.text !== 'Расход';
}

Правильно:

filter: function(item) {

    return item.text !== 'Расход';
}

Изменение datasets внутри sort

Нежелательно:

sort: function(a, b, data) {

    data.datasets.reverse();

    return 0;
}

Функция сортировки должна быть чистой.


Рекомендации по архитектуре

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

Удобный подход:

{
    label: 'Доход',
    data: [1, 2, 3],

    meta: {
        visibleInLegend: true,
        priority: 1,
        category: 'finance'
    }
}

Централизация логики

Полезно выносить обработчики:

function legendFilter(item, data) {

    const dataset =
        data.datasets[item.datasetIndex];

    return dataset.meta.visibleInLegend;
}

Предсказуемый порядок

Даже если datasets уже расположены правильно, лучше явно задавать сортировку:

sort: function(a, b) {

    return a.datasetIndex - b.datasetIndex;
}

Это предотвращает ошибки при динамическом обновлении данных.


Интеграция с пользовательским интерфейсом

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

  • с чекбоксами;
  • с выпадающими списками;
  • с поиском;
  • с категориями;
  • с правами доступа.

Пример:

let currentCategory = 'finance';
filter: function(item, data) {

    const dataset =
        data.datasets[item.datasetIndex];

    return (
        dataset.category === currentCategory
    );
}

После смены категории:

chart.update();

легенда обновится автоматически.


Полностью управляемая легенда

В сложных приложениях легенда может формироваться полностью вручную:

generateLabels: function(chart) {

    return customLegendItems;
}

Такой подход используется:

  • в BI-системах;
  • в CRM;
  • в финансовых панелях;
  • в системах мониторинга;
  • в дашбордах реального времени.

Он позволяет превращать легенду из вспомогательного элемента в полноценный интерфейс управления визуализацией.