В прошлой статье мы пошагово разобрали, как с нуля построить пончиковую диаграмму на чистом 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
}
}
}
