Как создать столбчатую диаграмму на чистом Javascript и SVG

web development, javascript, svg
Как создать столбчатую диаграмму на чистом Javascript и SVG

В прошлой статье мы пошагово разобрали, как с нуля построить пончиковую диаграмму на чистом JavaScript и SVG. Мы познакомились с элементом <circle>, параметрами stroke-dasharray и stroke-offset, и даже освежили школьную тригонометрию. В этой статье мы разберём другой тип визуализации – столбчатую диаграмму (bar chart). Она идеально подходит, когда нужно сравнить значения между категориями: по месяцам, регионам, продуктам и т.д.

Как и в прошлый раз, мы будем делать всё своими руками: без сторонних библиотек, с полным контролем над разметкой, и в удобном виде – через один класс BarChart.

В этой статье вы узнаете:

  • как рисовать прямоугольники с нужными размерами в SVG;
  • как рассчитывать координаты и отступы;
  • как добавить подписи.

Что у нас получится в итоге

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

  • диаграмма с настраиваемой высотой, шириной и цветом столбцов;
  • столбцы с закруглёнными углами и подписями значений и меток;
  • подписи меток с поддержкой многострочности;
  • работа только с неотрицательными данными.
12 январь 2024 19 февраль 2024 20 март 2024 24 апрель 2024 27 май 2024 27 июнь 2024 34 июль 2024 38 август 2024 49 сентябрь 2024 34 октябрь 2024 42 ноябрь 2024 35 декабрь 2024

Вот пример, как будет выглядеть использование нашего класса:

Пример использования класса BarChart
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:

Файл 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 и опишем каркас класса:

Файл 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")
    }
}

В результате получится вот такая диаграмма:

12 январь 2024 19 февраль 2024 20 март 2024 24 апрель 2024 27 май 2024 27 июнь 2024 34 июль 2024 38 август 2024 49 сентябрь 2024 34 октябрь 2024 42 ноябрь 2024 35 декабрь 2024

Итого

Вот так, шаг за шагом, мы создали настоящую столбчатую диаграмму на чистом JavaScript и SVG – без сторонних библиотек и зависимостей. У нас получилось:

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

всё это – в виде небольшого (всего 75 строк!), понятного класса BarChart, с которым легко работать.

Теперь вы можете использовать такую диаграмму где угодно: в отчётах, дашбордах или просто для визуализации данных в своих проектах. Хотите добавить анимации? Легенду? Разные цвета для разных значений? Всё в ваших руках!

Чистый JavaScript – мощный, но при этом доступный инструмент. И теперь, надеюсь, вы увидели, насколько просто и приятно с ним работать. Если раньше он казался «сложным» или «громоздким», возможно, сейчас вы измените своё мнение.

А главное – вся логика у вас под контролем. Вы сами управляете тем, как и что отрисовывается. И, как и в случае с пончиковой диаграммой, это не просто визуализация – это результат ваших собственных усилий!

Итоговый файл 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>
        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>
Итоговый файл 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) {
        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
        }
    }
}