В прошлой статье мы пошагово разобрали, как с нуля построить пончиковую диаграмму на чистом JavaScript и SVG. Мы познакомились с элементом <circle>
, параметрами stroke-dasharray
и stroke-offset
, и даже освежили школьную тригонометрию. В этой статье мы разберём другой тип визуализации – столбчатую диаграмму (bar chart). Она идеально подходит, когда нужно сравнить значения между категориями: по месяцам, регионам, продуктам и т.д.
Как и в прошлый раз, мы будем делать всё своими руками: без сторонних библиотек, с полным контролем над разметкой, и в удобном виде – через один класс BarChart
.
В этой статье вы узнаете:
- как рисовать прямоугольники с нужными размерами в SVG;
- как рассчитывать координаты и отступы;
- как добавить подписи.
Что у нас получится в итоге
Прежде чем углубляться в реализацию, давай определимся, что мы хотим получить:
- диаграмма с настраиваемой высотой, шириной и цветом столбцов;
- столбцы с закруглёнными углами и подписями значений и меток;
- подписи меток с поддержкой многострочности;
- работа только с неотрицательными данными.
Вот пример, как будет выглядеть использование нашего класса:
const config = { svg: document.getElementById("bar-chart"), padding: {bottom: 25, left: 5, right: 5, top: 15}, bar: { // параметры столбцов radius: 5, // радиус загругления color: "#ffc154", // цвет width: 50, // ширина height: 300, // высота gap: 5 // отступ между столбцами }, value: {size: 12, color: "#ffc154"}, // параметры значений label: {size: 10, color: "#888"} // параметры меток } const data = [ {value: 12, label: "январь\n2024"}, {value: 19, label: "февраль\n2024"}, {value: 20, label: "март\n2024"}, {value: 24, label: "апрель\n2024"}, {value: 27, label: "май\n2024"}, {value: 27, label: "июнь\n2024"}, {value: 34, label: "июль\n2024"}, {value: 38, label: "август\n2024"}, {value: 49, label: "сентябрь\n2024"}, {value: 34, label: "октябрь\n2024"}, {value: 42, label: "ноябрь\n2024"}, {value: 35, label: "декабрь\n2024"} ] const barChart = new BarChart(config) barChart.plot(data)
Теперь давайте создадим всё это по шагам – от заготовки HTML и JS до красивой и понятной диаграммы.
Шаг 0. Подготовка
Начнём с создания двух файлов: bar_chart.html
и bar_chart.js
. В первом будет наша HTML-разметка и SVG-элемент, во втором – сам класс BarChart
, который мы будем постепенно наполнять.
Вот базовая заготовка bar_chart.html
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Столбчатая диаграмма</title> </head> <body> <svg id="bar-chart"></svg> <script src="bar_chart.js"></script> <script> // создание и управление диаграммой </script> </body> </html>
Теперь создадим файл bar_chart.js
и опишем каркас класса:
class BarChart { constructor(config) { this.svg = config.svg this.padding = config.padding ?? {left: 5, right: 5, top: 15, bottom: 25} this.bar = { radius: config.bar.radius ?? 0, color: config.bar.color ?? "#00bcd4", width: config.bar.width ?? 40, height: config.bar.height ?? 300, gap: config.bar.gap ?? 5 } this.value = { size: config.value.size ?? 12, color: config.value.color ?? "#333" } this.label = { size: config.label.size ?? 12, color: config.label.color ?? "#333" } } plot(data) { } }
Пока всё просто – мы подготовили класс и сохранили в нём нужные параметры из конфига. В следующем шаге займёмся самой интересной частью – рисованием прямоугольников!
Шаг 1. Отрисовка столбцов
В SVG столбцы можно нарисовать с помощью элемента <rect>
. У него есть координаты x
, y
, а также width
и height
. Если ширина столбцов нам дана в конфиге, то вот остальные параметрв нужно будет рассчитать для каждого столбца на основе:
- общего количества данных;
- отступов (gap);
- максимального значения (чтобы нормировать остальные высоты).
Также нужно будет не забыть обновить размеры svg. Высота svg складывается из высоты столбцов и вертикальных отступов (padding.top
и padding.bottom
). Эти отступы нужны, чтобы было, куда поместить подписи значений и меток. Для расчёта ширины svg нужно умножить количество столбцов на ширину столбца с отступом и вычесть один отступ, а также не забыть прибавить горизонтальные отступы (padding.left
и padding.right
).
Вот как мы можем это сделать в методе plot:
plot(data) { let height = this.padding.top + this.bar.height + this.padding.bottom let width = this.padding.left + data.length * (this.bar.width + this.bar.gap) - this.bar.gap + this.padding.right let maxValue = Math.max(...data.map(item => item.value)) this.svg.style.width = `${width}px` this.svg.style.height = `${height}px` }
Добавим метод для добавления прямоугольника в точке (x
, y
) с заданной высотой:
addBar(x, y, height) { let bar = document.createElementNS("http://www.w3.org/2000/svg", "rect") bar.setAttribute("x", x) bar.setAttribute("y", y) bar.setAttribute("width", this.bar.width) bar.setAttribute("height", height) bar.setAttribute("rx", this.bar.radius) bar.setAttribute("fill", this.bar.color) this.svg.appendChild(bar) }
Важно не забыть, что в диаграмме прямоугольники растут снизу вверх, поэтому необходимо учесть это при его размещении:
plot(data) { ... for (let i = 0; i < data.length; i++) { let barHeight = data[i].value / maxValue * this.bar.height let x = this.padding.left + i * (this.bar.width + this.bar.gap) let y = height - this.padding.bottom - barHeight if (data[i].value > 0) this.addBar(x, y, barHeight) } }
В результате получится следующая картинка:
Шаг 2. Добавляем подписи
Добавление подписей (как значениям, так и меткам) будем осуществлять с помощью svg элемента <text>
. Он должен быть уже знаком вам по прошлой статье. Чтобы не дублировать код, реализуем один метод добавления текстового содержимого. Также не забудем и о том, что текст может состоять более чем из одной строки:
addLabel(x, y, text, format, baseline) { for (let line of text.split("\n")) { let label = document.createElementNS('http://www.w3.org/2000/svg', "text") label.textContent = line label.setAttribute("x", x) label.setAttribute("y", y) label.setAttribute("dominant-baseline", baseline) label.setAttribute("text-anchor", "middle") label.setAttribute("fill", format.color) label.setAttribute("font-size", format.size) this.svg.appendChild(label) y += label.getBBox().height } }
Теперь в том же цикле, где мы добавляли прямоугольники, добавим подписи значений наверху столбцов (при этом столбцы с нулевой высотой мы подписывать не будем), а под столбцом добавим подписи меток:
plot(data) { ... for (let i = 0; i < data.length; i++) { ... let xc = x + this.bar.width / 2 if (data[i].value > 0) { this.addBar(x, y, barHeight) this.addLabel(xc, y, `${data[i].value}`, this.value, "text-after-edge") } this.addLabel(xc, height - this.padding.bottom, data[i].label, this.label, "text-before-edge") } }
В результате получится вот такая диаграмма:
Итого
Вот так, шаг за шагом, мы создали настоящую столбчатую диаграмму на чистом JavaScript и SVG – без сторонних библиотек и зависимостей. У нас получилось:
- настроить размеры и отступы между столбцами;
- масштабировать высоту по значению;
- добавить подписи меток и значений столбцов;
всё это – в виде небольшого (всего 75 строк!), понятного класса BarChart
, с которым легко работать.
Теперь вы можете использовать такую диаграмму где угодно: в отчётах, дашбордах или просто для визуализации данных в своих проектах. Хотите добавить анимации? Легенду? Разные цвета для разных значений? Всё в ваших руках!
Чистый JavaScript – мощный, но при этом доступный инструмент. И теперь, надеюсь, вы увидели, насколько просто и приятно с ним работать. Если раньше он казался «сложным» или «громоздким», возможно, сейчас вы измените своё мнение.
А главное – вся логика у вас под контролем. Вы сами управляете тем, как и что отрисовывается. И, как и в случае с пончиковой диаграммой, это не просто визуализация – это результат ваших собственных усилий!
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Столбчатая диаграмма</title> </head> <body> <svg id="bar-chart"></svg> <script src="bar_chart.js"></script> <script> const config = { svg: document.getElementById("bar-chart"), padding: {bottom: 25, left: 5, right: 5, top: 15}, bar: { // параметры столбцов radius: 5, // радиус загругления color: "#ffc154", // цвет width: 50, // ширина height: 300, // высота gap: 5 // отступ между столбцами }, value: {size: 12, color: "#ffc154"}, // параметры значений label: {size: 10, color: "#888"} // параметры меток } const data = [ {value: 12, label: "январь\n2024"}, {value: 19, label: "февраль\n2024"}, {value: 20, label: "март\n2024"}, {value: 24, label: "апрель\n2024"}, {value: 27, label: "май\n2024"}, {value: 27, label: "июнь\n2024"}, {value: 34, label: "июль\n2024"}, {value: 38, label: "август\n2024"}, {value: 49, label: "сентябрь\n2024"}, {value: 34, label: "октябрь\n2024"}, {value: 42, label: "ноябрь\n2024"}, {value: 35, label: "декабрь\n2024"} ] const barChart = new BarChart(config) barChart.plot(data) </script> </body> </html>
class BarChart { constructor(config) { this.svg = config.svg this.padding = config.padding ?? {left: 5, right: 5, top: 15, bottom: 25} this.bar = { radius: config.bar.radius ?? 0, color: config.bar.color ?? "#00bcd4", width: config.bar.width ?? 40, height: config.bar.height ?? 300, gap: config.bar.gap ?? 5 } this.value = { size: config.value.size ?? 12, color: config.value.color ?? "#333" } this.label = { size: config.label.size ?? 12, color: config.label.color ?? "#333" } } plot(data) { let height = this.padding.top + this.bar.height + this.padding.bottom let width = this.padding.left + data.length * (this.bar.width + this.bar.gap) - this.bar.gap + this.padding.right let maxValue = Math.max(...data.map(item => item.value)) this.svg.style.width = `${width}px` this.svg.style.height = `${height}px` for (let i = 0; i < data.length; i++) { let barHeight = data[i].value / maxValue * this.bar.height let x = this.padding.left + i * (this.bar.width + this.bar.gap) let y = height - this.padding.bottom - barHeight let xc = x + this.bar.width / 2 if (data[i].value > 0) { this.addBar(x, y, barHeight) this.addLabel(xc, y, `${data[i].value}`, this.value, "text-after-edge") } this.addLabel(xc, height - this.padding.bottom, data[i].label, this.label, "text-before-edge") } } addBar(x, y, height) { let bar = document.createElementNS("http://www.w3.org/2000/svg", "rect") bar.setAttribute("x", x) bar.setAttribute("y", y) bar.setAttribute("width", this.bar.width) bar.setAttribute("height", height) bar.setAttribute("rx", this.bar.radius) bar.setAttribute("fill", this.bar.color) this.svg.appendChild(bar) } addLabel(x, y, text, format, baseline) { for (let line of text.split("\n")) { let label = document.createElementNS('http://www.w3.org/2000/svg', "text") label.textContent = line label.setAttribute("x", x) label.setAttribute("y", y) label.setAttribute("dominant-baseline", baseline) label.setAttribute("text-anchor", "middle") label.setAttribute("fill", format.color) label.setAttribute("font-size", format.size) this.svg.appendChild(label) y += label.getBBox().height } } }