Перейти к содержимому
Поиск
Разработка

Chart.js: графики для веб-интерфейсов — мастерская и 6 примеров

Мини-мастерская Chart.js и шесть живых интерфейсов: динамика заявок, сравнение услуг, бюджет, загрузка команды, план и факт, детализация. Данные, пояснения, готовый HTML и исходники.

Александр Некрасов
Chart.js — данные в интерфейсе: столбчатый, линейный и кольцевой графики

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

Здесь можно сразу собрать свой график в мини-мастерской, изменить шесть готовых интерфейсов и забрать исходники. Разберём не только new Chart(), но и обновление данных, процентные диаграммы, доступность, экспорт PNG, загрузку через fetch() и передачу данных из PHP. Примеры используют Chart.js 4.6.1.

Попробуйте за пять минут: задайте свои подписи и значения в мастерской → переключите столбцы на линию → скачайте HTML → откройте пример «Детализация» и нажмите на столбец. Так видно, чем обычная картинка отличается от рабочего элемента интерфейса.

Что даёт Chart.js и где он пригодится

Библиотека подходит для личных кабинетов, CRM, административных панелей, отчётов и небольших аналитических разделов. Ей не нужен React: достаточно HTML и JavaScript. При этом библиотеку можно подключить и к приложению со сборкой. Вы выбираете тип графика, передаёте подписи и массивы чисел, задаёте параметры отображения.

Главная единица данных — dataset, то есть один ряд: например, «Создано» или «Закрыто». Подписи labels задают категории по горизонтали. Значение с индексом 2 относится к подписи с тем же индексом. Если независимо отсортировать подписи и числа, график сохранит красивый вид, но начнёт показывать неверные связи.

const data = {
  labels: ['Пн', 'Вт', 'Ср'],
  datasets: [
    { label: 'Создано', data: [12, 18, 15], borderColor: '#007f86' },
    { label: 'Закрыто', data: [10, 16, 17], borderColor: '#b66b00' }
  ]
};

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

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

Документация: подключение Chart.js к странице и сборщику.

Мини-мастерская: соберите график под свою задачу

Начните с редактора: выберите столбцы, линию или кольцо, введите название, единицы измерения, подписи и значения. Меняйте палитру, легенду и анимацию; добавляйте и удаляйте строки. Результат пересчитывается рядом, поэтому сразу видно, как выбранная форма влияет на чтение данных.

Упражнение: создайте четыре канала обращений, задайте разные значения и сначала посмотрите на столбцы. Затем переключите тип на кольцо. Столбцы помогают сравнить близкие значения, кольцо — увидеть доли целого. Если категории длинные или их слишком много, сначала подумайте о читаемости, а уже потом о декоративных настройках.

Экспорт HTML сохраняет вашу рабочую мастерскую целиком: данные, настройки, Chart.js и стили уже находятся внутри файла. Его можно открыть без интернета; сброс вернёт состояние на момент экспорта. PNG удобен для документа или переписки, но теряет интерактивность. Исходные данные сохраняйте отдельно: картинка не заменяет таблицу.

Все введённые в эту мастерскую значения обрабатываются в браузере. Демо не отправляет их в CRM или на сервер и не подключено к рабочей аналитике. Обновление страницы возвращает начальный набор, если вы заранее не сохранили результат.

Шесть интерфейсов: попробуйте готовые сценарии

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

Сценарий Где использовать Что попробовать
Динамика обращений Главная страница службы поддержки Переключить 7, 14 и 28 дней
Сравнение услуг Отчёт по спросу на услуги Изменить числа и порядок столбцов
Структура бюджета Распределение расходов проекта Скрыть категорию и сравнить итоги
Загрузка команд Планирование ресурсов Сравнить часы и доли в процентах
План и факт Контроль выполнения показателя Изменить план и увидеть отклонение
От графика к заявкам Оперативная работа в CRM Выбрать статус и открыть соответствующие строки

Скачать все исходники ZIP. В архиве — мастерская, шесть сценариев, стили, JavaScript, локальные библиотеки и инструкция запуска. Общие страницы index.html и lab.html запускайте через локальный сервер по README: экспорт читает соседние файлы проекта. Готовые страницы в папке examples/ и HTML, сохранённый кнопкой экспорта, открываются самостоятельно без сети. Данные демонстрационные: фильтрация и изменения происходят внутри страницы.

1. Динамика обращений: период и два ряда данных

Первый интерфейс отвечает на вопрос: успевает ли команда закрывать новые обращения? Два линейных ряда показывают созданные и закрытые заявки по дням. Переключатель периода позволяет посмотреть последние 7, 14 или 28 дней из одного фиксированного набора. Значения не генерируются заново при каждом нажатии, поэтому сравнение воспроизводимо.

Попробуйте: выберите 7 дней — в исходном наборе поступило 94 обращения, закрыто 86. Затем переключите длинный период и сравните линии с итогами и таблицей. Локальный всплеск на коротком интервале может выглядеть иначе в контексте месяца. При этом сумма закрытых за период не обязана совпадать с числом созданных: команда может закрывать ранее поступившие заявки.

Логика приложения выбирает строки и считает показатели. Chart.js получает уже подготовленные массивы и перерисовывает линию. Следующий фрагмент предполагает, что days содержит записи в хронологическом порядке, а chart — созданный график с двумя рядами.

function showPeriod(days, count, chart) {
  const visible = days.slice(-count);
  chart.data.labels = visible.map(day => day.label);
  chart.data.datasets[0].data = visible.map(day => day.created);
  chart.data.datasets[1].data = visible.map(day => day.resolved);
  chart.update('none');
  return visible;
}

Даты здесь выступают строковыми категориями. Они не превращают график в временную шкалу с автоматическим разбором дат. Для настоящей оси time понадобится совместимый адаптер дат; порядок и границы периода всё равно должны быть определены в приложении.

Попробовать динамику обращений ↑

2. Сравнение услуг: редактирование и сортировка

Горизонтальные столбцы удобны, когда названия категорий длинные: «Аудит интерфейса», «Формы и заявки», «Каталог услуг». Такой блок полезен в отчёте по спросу на услуги. Введите собственные количества и переключите сортировку, чтобы быстро найти наиболее востребованное направление.

Попробуйте: замените у «Поддержки» 12 на 40 и включите порядок по убыванию. Общая сумма станет 135, а «Поддержка» окажется лидером. Категория должна переместиться вместе со своим числом. Затем задайте ноль: это допустимое значение. Пустое поле означает незавершённый ввод, а не нулевой результат.

Сортировку выполняет JavaScript приложения. Сам Chart.js использует indexAxis: 'y', чтобы расположить категории по вертикали. Сортируйте копию массива объектов, а затем из одного результата получайте и подписи, и значения.

const ordered = [...services].sort((a, b) => b.count - a.count);
chart.options.indexAxis = 'y';
chart.data.labels = ordered.map(item => item.name);
chart.data.datasets[0].data = ordered.map(item => item.count);
chart.update();

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

Документация: горизонтальные столбцы и настройки осей.

Попробовать сравнение услуг ↑

3. Структура бюджета: что меняется при скрытии категории

Кольцевая диаграмма показывает распределение небольшой суммы по нескольким статьям. Она подходит для расходов проекта, структуры подписки или распределения ресурсов. Рядом обязательно нужны значения и единицы: по площади сегмента нельзя надёжно восстановить точное число.

Попробуйте: выключите «Разработку» кнопкой легенды или отметкой «Показывать» в таблице. Общий бюджет останется 240 000 ₽, а на диаграмме будет показано 120 000 ₽. Оба способа управления синхронизированы. Скрытие части графика не удаляет строку из исходных данных и не уменьшает настоящий бюджет.

Кнопки HTML-легенды и переключатели таблицы доступны с клавиатуры. У скрытой категории есть явная подпись, а состояние кнопки передаётся через aria-pressed. Приложение выбирает отмеченные строки, считает общий и видимый итоги и передаёт Chart.js новый массив. Доли вычисляются от видимой суммы: после скрытия категории знаменатель меняется.

function budgetModel(rows) {
  // У каждой категории есть id, name, value и visible.
  const visible = rows.filter(row => row.visible);
  return {
    all: rows.reduce((sum, row) => sum + row.value, 0),
    shown: visible.reduce((sum, row) => sum + row.value, 0),
    labels: visible.map(row => row.name),
    values: visible.map(row => row.value)
  };
}

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

Попробовать структуру бюджета ↑

4. Загрузка команд: часы и нормализация до 100%

Составные столбцы показывают, из каких видов работы складывается загрузка каждой команды. В режиме часов виден абсолютный объём. В режиме процентов — структура времени внутри команды. Это разные вопросы: одинаковые доли не означают одинаковую нагрузку.

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

Встроенное накопление stacked складывает ряды. Преобразование в 100% делает наш код: каждое значение делится на сумму своей команды. Обе оси включают накопление, а процентная ось ограничивается диапазоном от 0 до 100.

function asPercent(rows) {
  // rows: массив команд, values: часы по видам работ.
  return rows.map(row => {
    const total = row.values.reduce((sum, value) => sum + value, 0);
    return {
      ...row,
      values: row.values.map(value => total === 0 ? 0 : value / total * 100)
    };
  });
}

const scales = {
  x: { stacked: true },
  y: { stacked: true, beginAtZero: true, max: 100 }
};

Не округляйте исходные доли до построения графика: сумма округлённых чисел может стать 99% или 101%. Округление оставляйте для подписей. Команда с нулевыми часами получает нулевые значения и понятное сообщение об отсутствии нагрузки, без деления на ноль.

Документация: столбчатая диаграмма с накоплением.

Попробовать загрузку команд ↑

5. План и факт: смешанная диаграмма

Столбцы показывают фактическое значение, линия — план на тот же период. Такой блок применим к выручке, обработанным обращениям или количеству выполненных работ. В примере оба ряда измеряются в одинаковых единицах и используют общую ось: сравнение получается прямым.

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

Chart.js умеет объединять типы на уровне отдельных рядов. Порядок order помогает оставить линию перед столбцами. Расчёт разницы, процента выполнения и текстового вывода относится к приложению.

const config = {
  type: 'bar',
  data: {
    labels: ['Январь', 'Февраль', 'Март'],
    datasets: [
      { type: 'bar', label: 'Факт, шт.', data: [28, 35, 32],
        backgroundColor: '#007f86', order: 2 },
      { type: 'line', label: 'План, шт.', data: [30, 30, 36],
        borderColor: '#b66b00', borderWidth: 3, order: 1 }
    ]
  },
  options: { scales: { y: { beginAtZero: true } } }
};

Если план равен нулю, процент выполнения не определён привычной формулой. Покажите абсолютную разницу и подпись «план не задан» или другое согласованное правило. Не подставляйте бесконечность. Разные единицы, например рубли и проценты, потребуют отдельного решения по осям и пояснения пользователю.

Документация: смешанные диаграммы.

Попробовать план и факт ↑

6. От графика к заявкам: диаграмма как фильтр

Здесь график становится входом в рабочий список. Нажмите на столбец статуса — под ним останутся соответствующие заявки. Такой подход полезен в CRM: пользователь сначала видит распределение, а затем сразу переходит к записям, из которых получился показатель.

Попробуйте: выберите статус мышью, затем повторите действие через текстовые кнопки. Сбросьте фильтр и сравните количество строк. У выбранного состояния должно быть явное обозначение: одного изменения цвета на canvas недостаточно.

Chart.js сообщает, какой элемент получил нажатие. Саму фильтрацию массива, обновление таблицы и текст результата выполняет приложение. Обработчик ниже — фрагмент настройки уже создаваемого графика; функция selectStatus() должна обновлять интерфейс вашего списка.

const statusKeys = ['new', 'progress', 'done'];
const options = {
  onClick(event, elements) {
    if (elements.length === 0) return;
    selectStatus(statusKeys[elements[0].index]);
  }
};

document.querySelectorAll('[data-status]').forEach(button => {
  button.addEventListener('click', () => selectStatus(button.dataset.status));
});

Пустое место между столбцами не должно сбрасывать фильтр случайным образом. Кнопка «Все» делает сброс предсказуемым. Для клавиатуры оставляйте обычные кнопки с видимым фокусом, а число найденных заявок сообщайте текстом в области aria-live="polite".

Документация: события и взаимодействие с графиком.

Попробовать переход к заявкам ↑

Подключение: готовый HTML, npm и регистрация компонентов

Для отдельной страницы удобна UMD-сборка: она создаёт глобальный объект Chart. Ниже — минимальный полный HTML. Сохраните его в файл и откройте в браузере. В этом варианте библиотека загружается из CDN, поэтому понадобится интернет. В архиве статьи используется локальная копия: замените адрес скрипта на путь к chart.umd.min.js.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Обращения по каналам</title>
  <style>
    body { max-width: 760px; margin: 24px auto; padding: 0 16px;
      font: 16px/1.5 system-ui, sans-serif; }
    .chart-box { position: relative; height: 320px; min-width: 0; }
    td, th { padding: 8px 16px; text-align: left; }
  </style>
</head>
<body>
  <h1>Обращения по каналам</h1>
  <p>Количество обращений за неделю. Всего: 59.</p>
  <div class="chart-box">
    <canvas id="chart" role="img" aria-label="Сравнение каналов обращений.
      Точные значения приведены в таблице ниже."></canvas>
  </div>
  <table>
    <caption>Данные графика, обращения</caption>
    <thead><tr><th scope="col">Канал</th><th scope="col">Количество</th></tr></thead>
    <tbody>
      <tr><th scope="row">Сайт</th><td>28</td></tr>
      <tr><th scope="row">Телефон</th><td>19</td></tr>
      <tr><th scope="row">Почта</th><td>12</td></tr>
    </tbody>
  </table>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.6.1/dist/chart.umd.min.js"></script>
  <script>
    new Chart(document.getElementById('chart'), {
      type: 'bar',
      data: {
        labels: ['Сайт', 'Телефон', 'Почта'],
        datasets: [{ label: 'Обращения, шт.', data: [28, 19, 12],
          backgroundColor: '#007f86' }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        animation: matchMedia('(prefers-reduced-motion: reduce)').matches
          ? false : { duration: 350 },
        scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }
      }
    });
  </script>
</body>
</html>

Скрипт библиотеки стоит перед кодом создания графика. Если поставить defer только внешней библиотеке, а следующий встроенный скрипт оставить обычным, он может выполниться раньше загрузки Chart. В проекте со сборщиком установите зафиксированную версию:

npm install chart.js@4.6.1
import Chart from 'chart.js/auto';
// После появления canvas в DOM:
const chart = new Chart(document.getElementById('chart'), config);

Импорт auto регистрирует доступные компоненты. Если важен размер сборки, можно импортировать только необходимое. Для простых столбцов набор будет таким; для линии, кольца и других типов он отличается.

import {
  Chart, BarController, BarElement, CategoryScale,
  LinearScale, Tooltip, Legend
} from 'chart.js';

Chart.register(BarController, BarElement, CategoryScale,
  LinearScale, Tooltip, Legend);

Не смешивайте UMD-глобальный объект и модульный импорт без необходимости. Фиксируйте версии и проверяйте обновления на своих сценариях. CDN — способ доставки файла, а не обязательная часть работы библиотеки.

Документация: UMD, npm и tree-shaking.

Данные: проверка, обновление, сброс и загрузка

Форма возвращает строки. Number('') даёт ноль, поэтому сначала проверьте пустой ввод, затем преобразуйте число. Для денег можно разрешить дробную часть, для количества заявок обычно нужны целые значения. null иногда означает отсутствующее наблюдение; не заменяйте его нулём без осознанного правила.

function readCount(raw) {
  const text = String(raw ?? '').trim();
  if (text === '') throw new Error('Введите количество');
  const value = Number(text);
  if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
    throw new Error('Нужно целое число от нуля');
  }
  return value;
}

Ошибку показывайте рядом с полем. Если расчёт сейчас недействителен, явно обозначьте это и заблокируйте экспорт результата. Старый график без предупреждения может выглядеть как ответ на новые, ещё не проверенные данные.

Обычное изменение данных не требует создавать новый экземпляр. Измените массивы и вызовите update(); режим 'none' обновляет без анимации. Сброс формы восстанавливает исходную модель данных. Метод reset() у Chart.js связан с состоянием анимации и не возвращает ваши первоначальные числа.

chart.data.labels = ['Апрель', 'Май'];
chart.data.datasets[0].data = [18, 24];
chart.update('none');

// Перед полным пересозданием на том же canvas:
chart.destroy();
// Теперь можно вызвать new Chart(canvas, nextConfig).

Для API добавьте состояния загрузки, пустого ответа и ошибки. Быстрое переключение фильтров создаёт гонку: старый медленный ответ способен заменить новый. Ниже — фрагмент загрузчика для существующего графика с одним рядом. Сервер возвращает {labels: [...], values: [...]}; элемент status показывает состояние текстом.

function makeLoader(chart, status) {
  let controller;
  let requestId = 0;
  return async function load(url) {
    controller?.abort();
    controller = new AbortController();
    const current = ++requestId;
    status.textContent = 'Загрузка…';
    chart.data.labels = [];
    chart.data.datasets[0].data = [];
    chart.update('none');
    try {
      const response = await fetch(url, { signal: controller.signal });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const data = await response.json();
      if (current !== requestId) return;
      if (!Array.isArray(data.labels) || !Array.isArray(data.values) ||
          data.labels.length !== data.values.length ||
          !data.labels.every(value => typeof value === 'string') ||
          !data.values.every(value => Number.isFinite(value) && value >= 0)) {
        throw new Error('Некорректный формат данных');
      }
      chart.data.labels = data.labels;
      chart.data.datasets[0].data = data.values;
      chart.update('none');
      status.textContent = data.labels.length ? 'Данные обновлены' : 'Нет данных';
    } catch (error) {
      if (current !== requestId || error.name === 'AbortError') return;
      status.textContent = 'Не удалось загрузить данные. Повторите попытку.';
    }
  };
}

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

Документация: update(), destroy(), reset() и другие методы.

Мобильная версия и доступность canvas

Chart.js определяет размер через родительский контейнер. Дайте ему отдельную обёртку с относительным позиционированием и высотой, затем отключите сохранение пропорций. Не задавайте width="100%" или height="40vh" в HTML-атрибутах canvas: размеры макета принадлежат CSS-контейнеру.

<div class="chart-box">
  <canvas id="report" role="img" aria-label="Загрузка команд в часах.
    Значения доступны в таблице под графиком."></canvas>
</div>
<style>
  .chart-box { position: relative; height: 360px; min-width: 0; }
  @media (max-width: 600px) { .chart-box { height: 300px; } }
</style>
<!-- В options графика: responsive: true, maintainAspectRatio: false -->

На узком экране важны не только размеры canvas. Кнопки должны переноситься, длинные подписи — оставаться понятными, поля — иметь видимые названия. Таблицу можно превратить в карточки строк, сохранив смысл каждой колонки. Проверьте портретную ориентацию, ширину около 360 пикселей и работу без наведения мыши.

Canvas не создаёт доступную таблицу автоматически. role="img" и aria-label дают название изображению, но не раскрывают все значения. Оставьте рядом текстовый вывод и таблицу данных. Интерактивные действия повторите обычными HTML-кнопками; легенда и подсказки внутри рисунка не должны быть единственным способом получить информацию.

Учитывайте prefers-reduced-motion в собственных настройках анимации. Библиотека не принимает это продуктовое решение вместо вас. При открытии графика в ранее скрытой панели иногда полезно вызвать resize() после её появления.

Документация: адаптивный контейнер.

Документация: доступность графиков.

Экспорт PNG и исходников без потери смысла

Метод toBase64Image() экспортирует текущее содержимое canvas. Если вызвать его во время анимации, можно получить промежуточный кадр. Для кнопки сохранения удобно остановить анимацию и выполнить обновление без неё. Другой вариант — дождаться animation.onComplete.

Белый CSS-фон обёртки не становится пикселями PNG. Для непрозрачного изображения нужен фон, нарисованный на самом canvas. Следующий небольшой плагин подключается в массив plugins при создании графика.

const whiteCanvas = {
  id: 'whiteCanvas',
  beforeDraw(chart) {
    const { ctx, width, height } = chart;
    ctx.save();
    ctx.globalCompositeOperation = 'destination-over';
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, width, height);
    ctx.restore();
  }
};

// В конфигурации new Chart: plugins: [whiteCanvas]
function downloadPng(chart) {
  chart.stop();
  chart.update('none');
  const link = document.createElement('a');
  link.download = 'chart.png';
  link.href = chart.toBase64Image('image/png');
  link.click();
}

В экспортируемый отчёт добавляйте период, единицы и источник данных: отдельная картинка легко теряет контекст. Для дальнейшего редактирования сохраняйте HTML и исходные числа. Обычный JSON.stringify() не переносит функции обратного вызова и код плагинов, поэтому JSON конфигурации не всегда равнозначен полноценной странице.

Скачанный архив статьи содержит локальную библиотеку. Страницы в папке examples/ уже автономны; общую мастерскую запускайте через локальный сервер по инструкции в архиве. Сохранённый из неё HTML также работает без интернета. Если позднее добавите реальные запросы к API, способ запуска и разрешения сервера потребуется настроить отдельно.

Документация: фон canvas для экспорта.

Документация: завершение анимации.

Данные из PHP: передайте JSON, а не строку JavaScript

В PHP-приложении сервер может подготовить агрегированные данные, а Chart.js отрисует их в браузере. Не собирайте JavaScript склеиванием строк из базы. Используйте json_encode() и отдельный блок application/json. Флаги JSON_HEX_* защищают границу HTML, в частности от последовательности закрытия тега внутри подписи.

Это фрагмент PHP-шаблона. Canvas с идентификатором chart и библиотека Chart.js должны быть подключены выше. В рабочем проекте запрос к базе выполняется с проверкой прав, а ошибки кодирования обрабатываются серверным кодом.

<?php
$chartData = [
    'labels' => ['Сайт', 'Телефон', 'Почта'],
    'values' => [28, 19, 12],
];
$json = json_encode(
    $chartData,
    JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP |
    JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR
);
?>
<script type="application/json" id="chart-data"><?= $json ?></script>
<script>
  const initial = JSON.parse(document.getElementById('chart-data').textContent);
  new Chart(document.getElementById('chart'), {
    type: 'bar',
    data: {
      labels: initial.labels,
      datasets: [{ label: 'Обращения, шт.', data: initial.values }]
    },
    options: { scales: { y: { beginAtZero: true } } }
  });
</script>

Не прогоняйте этот JSON через htmlspecialchars(): внутри script-блока HTML-сущности не превращаются обратно в кавычки JSON. Все переданные браузеру данные доступны посетителю в исходном коде и инструментах разработчика. Пароли, токены и закрытые поля в график не передают.

Для больших отчётов лучше отдавать готовые агрегаты через отдельный API, а не выгружать в страницу все записи базы. Формат ответа удобно сохранить тем же: подписи, числовые ряды и метаданные периода. Тогда форма отображения не зависит от способа получения данных.

Документация PHP: json_encode().

Частые ошибки и границы библиотеки

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

Симптом Причина Что сделать
Chart is not defined Код выполняется до загрузки UMD или смешаны способы импорта Проверить загрузку файла и порядок выполнения скриптов
Компонент не зарегистрирован При выборочном импорте отсутствует контроллер, шкала или элемент Добавить нужное в Chart.register() либо начать с chart.js/auto
Canvas уже используется Новый экземпляр создан поверх старого Обновить существующий график или вызвать destroy() перед заменой
График растёт или обрезается Неверные размеры родителя, ограничение grid/flex Выделить контейнер, задать высоту и min-width: 0
Пустое поле стало нулём Число преобразовано до проверки строки Различать пустое значение, null и настоящий ноль
На PNG нет фона или части столбцов Экспорт взят во время анимации; фон был только в CSS Завершить кадр и нарисовать фон на canvas
100% не сходятся Деление не по той группе или раннее округление Проверить знаменатель, нулевую сумму и сохранять точные доли
Графиком нельзя управлять с клавиатуры Действия существуют только внутри canvas Добавить HTML-кнопки, таблицу и видимое состояние выбора

Масштабирование колесом и жестами не появляется автоматически: для него обычно подключают отдельный chartjs-plugin-zoom. Временная шкала требует адаптера дат. Сложная география, биржевые терминалы и очень большие потоки точек требуют отдельной оценки инструментов и производительности.

Для проверки бизнес-смысла полезны три простых набора: все значения нулевые, одно значение сильно больше остальных, несколько одинаковых значений. Затем попробуйте ошибочный ввод и быстрые переключения. Хороший интерфейс объясняет каждый из этих случаев без необходимости читать исходники.

Исходники, документация и что изучить дальше

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

Скачать мастерскую и шесть интерфейсов. Начните с одного сценария, замените демонстрационные массивы своими данными, проверьте нули и ошибки, затем подключайте API. Перед публикацией повторите проверку на узком экране и убедитесь, что таблица соответствует графику.

Для продолжения пригодятся другие материалы сайта: Alpine.js — интерактивность прямо в HTML, GridStack.js — конструктор дашбордов и noUiSlider — диапазоны и настройки интерфейса. Alpine.js помогает связать состояние фильтров, GridStack размещает карточки, а noUiSlider задаёт диапазоны. Chart.js добавляет к этим интерфейсам наглядное представление данных.