Сейчас все активно обсуждают радости и гадости удалённой работы, и мы даже выпустили статью на эту тему. А ещё провели небольшой опрос и попросили наших пользователей рассказать, с какими трудностями они сталкиваются во время работы дома (спойлер — все ленятся). Статистику нужно как-то красиво оформить, так почему бы не сделать SVG-график? Заодно научимся чему-нибудь новому. Поехали!

Интерактивная диаграмма
Вот такую диаграмму получим в итоге

Давайте немного поговорим о том, что такое SVG. Это формат векторной графики, который позволяет растягивать и сжимать изображения без потери качества, очень удобно. Такая возможность реализована за счёт крутой особенности SVG — фактически это обычный код. Каждое изображение описано опорными точками, кривыми и простыми фигурами, которые представляют из себя строки кода, их можно изменять в обычном редакторе. И, конечно, SVG-элементы можно создавать с нуля самим.

Именно этим и займёмся — создадим SVG-диаграмму и легенду к ней, а затем стилизуем всё с помощью CSS и в самом конце добавим немного JavaScript-магии. Вам понадобятся базовые знания HTML, CSS и JavaScript. Если их пока нет — самое время пройти бесплатные интерактивные курсы, а после этого вернуться к туториалу.

Создаём диаграмму

1. Подготовка

Начнём с родительского блока. График и легенда будут лежать внутри элемента .canvas, который мы сделаем флекс-контейнером, чтобы выровнять дочерние элементы внутри него.

<div class="canvas"></div>
.canvas {
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: 800px;
}

2. Добавляем SVG

Есть разные методы вставки SVG-элементов, самый подходящий для наших целей — вставить код элемента непосредственно в HTML, то есть заинлайнить. Это способ позволяет изменять свойства SVG-элементов и фигур прямо из CSS. Добавим разметку диаграммы:

<div class="canvas">
  <svg class="chart" width="500" height="500" viewBox="0 0 50 50">
    <circle class="unit" r="15.9" cx="50%" cy="50%"></circle>
    <circle class="unit" r="15.9" cx="50%" cy="50%"></circle>
    <circle class="unit" r="15.9" cx="50%" cy="50%"></circle>
    <circle class="unit" r="15.9" cx="50%" cy="50%"></circle>
    <circle class="unit" r="15.9" cx="50%" cy="50%"></circle>
    <circle class="unit" r="15.9" cx="50%" cy="50%"></circle>
    <circle class="unit" r="15.9" cx="50%" cy="50%"></circle>
  </svg>
</div>

Мы использовали тег svg — специальный тег для создания SVG-элемента, задали элементу размеры с помощью width и height, а ещё добавили атрибут viewBox. Для чего он нужен? Вообще содержимое SVG-элемента отрисовывается на бесконечном холсте, и изменение размеров элемента не влияет на его содержимое. С помощью viewBox как раз можно задать область отрисовки, которая будет растягиваться и сжиматься в зависимости от размеров. В нашем случае первые два нуля в значении — это координаты верхнего левого угла области (начало отсчёта системы координат), а следующие два числа — ширина и высота соответственно.

Внутри SVG-элемента мы нарисовали несколько одинаковых окружностей, задав радиус и центр окружности с помощью тега circle и его атрибутов, где: r — радиус окружности, cx— координата центра окружности по горизонтальной оси, а cy — по вертикальной. По умолчанию центр окружности расположен в начале координат — левом верхнем углу, поэтому мы задаём значение 50%, чтобы окружность находилась по центру.

Количество окружностей равно количеству секций в диаграмме. У нас их будет семь.

3. Создаём секции

На прошлом шаге мы получили обычные окружности, но хотелось бы превратить их в диаграмму в виде пончика, то есть нужно как-то вырезать середину. Мы реализуем это с помощью добавления обводки окружности. Оставим только её и уберём заливку самой фигуры — fill.

.unit {
  fill: none;
  stroke-width: 10;
}

.unit:nth-child(1) {
  stroke: #86cfa3;
}

.unit:nth-child(2) {
  stroke: #a2c6e0;
}

.unit:nth-child(3) {
  stroke: #ffc7ec;
}

/* Далее ещё четыре секции по аналогии */

Свойство stroke-width задаёт толщину обводки — она у всех секций одинаковая. А цвет разный, поэтому свойство stroke, которое за него отвечает, мы прописали отдельно для каждой окружности. Кстати, мы не указываем единицы измерения у свойств обводки, потому что по умолчанию это пиксели, и нам нужны именно они.

На данном этапе каждая секция занимает всю окружность, нам нужно изменить это поведение. Для этого используем свойство stroke-dasharray. Оно предназначено для создания пунктирных обводок и в качестве значения принимает длину отрезков и пробелов между ними. В нашем случае мы используем свойство не совсем традиционным способом: отрезок будет только один — сама секция, а вся остальная окружность — это пробел.

Ещё учтём, что по умолчанию все секции будут начинаться из одной точки, а нам нужно расположить их одну за другой по кругу. Чтобы добиться нужного поведения, используем свойство stroke-dashoffset — оно позволит сдвинуть обводку на нужное нам расстояние.

.unit:nth-child(1) {
  stroke: #86cfa3;
  stroke-dasharray: 8 100;
}

.unit:nth-child(2) {
  stroke: #a2c6e0;
  stroke-dasharray: 11 100;
  stroke-dashoffset: -8;
}

.unit:nth-child(3) {
  stroke: #ffc7ec;
  stroke-dasharray: 11 100;
  stroke-dashoffset: -19;
}

/* Далее ещё четыре секции по аналогии */

Давайте подробнее остановимся на значении свойства stroke-dashoffset. У первой секции этого свойства нет, потому что её не нужно никуда сдвигать, а у остальных сдвиг равен длине всех предшествующих секций. То есть у второго элемента сдвиг равен длине первой секции, у третьего — длине первой и второй секции и так далее. Значение отрицательное, потому что сдвигаем по часовой стрелке, а по умолчанию сдвиг идёт против.

4. Немного математики

Можно пропустить, если вы не любите математику.

Возможно, вы заметили, что на втором шаге мы задали окружностям довольно странный радиус — 15.9, и теперь пришло время рассказать, почему значение именно такое.

В графиках используются данные в процентах, то есть каждая секция диаграммы иллюстрирует какой-то процент от общего количества. В нашем случае длина секции, которая была прописана первым значением у свойства stroke-dasharray, должна отражать этот процент, то есть 100% соответствует длине всей окружности, а n% — длине секции. В таком случае, чтобы найти длину окружности, нам нужно вспомнить школьную формулу C = 2πr. Вот здесь мы схитрим и изначально договоримся, что длина окружности будет равна 100px, и тогда путём нехитрых вычислений выведем из формулы радиус, равный примерно 15.9px. Вот откуда появилось значение.

Для чего это было нужно? Допустим, нас попросили построить диаграмму по некоторым статистическим данным: 1% котов рыжие (и любят командовать), а 99% котов — других цветов. Благодаря тому, что мы приравняли длину всей окружности к 100, длина секций будет равна проценту из статистики — 1 и 99 соответственно. Это очень удобно. Если бы мы пошли альтернативным путём и не стали отталкиваться от длины окружности, то для каждого процента приходилось бы вычислять длину секции из пропорции, и она была бы дробным числом с кучей знаков после запятой.

Здесь стоит отметить, что нам помогает провернуть этот хак атрибут viewBox, который был задан в самом начале SVG-элементу. Масштабируя область видимости, мы может управлять размером диаграммы. При этом радиус окружности изменять не будем — мы зафиксировали его значение по причинам, изложенным выше.

Вот откуда взялась длина секции в свойстве stroke-dasharray — это процент, который занимает каждая секция диаграммы согласно данным статистики.

5. Легенда

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

  1. Добавляем разметку легенды внутрь элемента .canvas:

    <div class="legend">
      <p class="title">Что мешает во время работы дома?</p>    
      <ul class="caption-list">
        <li class="caption-item">еда</li>
        <li class="caption-item">соседи</li>
        <li class="caption-item">отсутствие рабочего места</li>
        <li class="caption-item">мало двигаюсь</li>
        <li class="caption-item">отсутствие чёткого графика работы</li>
        <li class="caption-item">неудобная коммуникация с коллегами</li>
        <li class="caption-item">много ленюсь</li>
      </ul>
    </div>
  2. Убираем лишние отступы и маркеры у списка, задаём отступы нужным элементам, указываем максимальную ширину всего блока с легендой, стилизуем текст и добавляем эффект при наведении на название секции:

    .legend {
      max-width: 250px;
      margin-left: 30px;
    }
    
    .title {  
      font-family: "Verdana", sans-serif;
      font-size: 18px;
      line-height: 21px;
      color: #591d48;
    }
    
    .caption-list {
      margin: 0;
      padding: 0;
      list-style: none;
    }
    
    .caption-item {
      position: relative;
    
      margin: 20px 0;
      padding-left: 30px;
      
      font-family: "Verdana", sans-serif;
      font-size: 16px;
      line-height: 18px;
      color: #591d48;  
      cursor: pointer;
    }
    
    .caption-item:hover {
      opacity: 0.8;
    }
  3. На прошлом этапе мы убрали встроенные маркеры у списка и добавили паддинг слева у его элементов, подготовив место для кастомных маркеров. Теперь создадим их с помощью псевдоэлемента beforeи добавим такие же цвета, как у секций диаграммы:

    .caption-item::before {
      content: "";
      
      position: absolute;
      top: 0;
      left: 0;
      
      width: 20px;
      height: 20px;
      
      border-radius: 8px;
    }
    
    .caption-item:nth-child(1)::before {
      background-color: #86cfa3;
    }
    
    .caption-item:nth-child(2)::before {
      background-color: #a2c6e0;
    }
    
    .caption-item:nth-child(3)::before {
      background-color: #ffc7ec;
    }
    
    /* Далее ещё четыре секции по аналогии */

    Готово! Теперь у нас есть диаграмма и легенда к ней.

Добавляем немного интерактивности

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

1. Анимация

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

Давайте добавим анимацию во время отрисовки секций диаграммы. Для этого будем постепенно изменять свойство stroke-dasharray, начиная с нуля и заканчивая теми значениями, которые мы указали ранее — для каждой секции оно своё. То есть длина секции будет постепенно возрастать от нуля до нужного значения.

Для создания анимации используем ключевое слово @keyframes, зададим название анимации и укажем состояние в начальной точке — 0%:

@keyframes render {
  0% {
    stroke-dasharray: 0 100;
  }
}

Мы намеренно не указали состояние в конечной точки анимации — 100%, так как у каждой секции своя длина. В данном случае после выполнения анимации свойство stroke-dasharray примет то значение, которое мы уже прописали ранее для каждой секции. Так работает анимация — по окончании элемент возвращается в исходное состояние.

Теперь добавим к уже имеющимся свойствам элемента .unit новые — animation-name и animation-duration. Они позволят задать название и продолжительность анимации и применить её к секциям диаграммы.

.unit {
  /* Здесь должны быть свойства, заданные ранее */

  animation-name: render;
  animation-duration: 1.5s; 
}

Всё работает — секции постепенно появляются в течение заданного времени.

2. Эффект при наведении

Добавим плавное увеличение секции при наведении на неё. Используем псевдокласс hover, чтобы увеличить толщину обводки наших окружностей-секций и добавить им полупрозрачность.

.unit:hover {
  opacity: 0.8;
  stroke-width: 12;
}

Сейчас переход между состояниями происходит резко, но это поведение можно изменить с помощью плавных переходов. Укажем свойства, которые нужно изменять плавно (в нашем случае все), и длительность перехода — за это отвечают transition-property и transition-duration.

.unit {
  /* Здесь должны быть свойства, заданные ранее */

  transition-property: all; 
  transition-duration: 1.5s;
}

Теперь переход между состояниями происходит плавно.

3. Немного JavaScript

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

Мы напишем небольшой скрипт, который позволит выделять нужную секцию при наведении на её название в легенде. Эффекты будем использовать точно такие же, как при наведении на саму секцию. Мы уже прописали их для .unit:hover, поэтому теперь просто продублируем для специального класса hovered, который далее будем использовать в JavaScript-коде.

.hovered {
  opacity: 0.8;
  stroke-width: 12;
}

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

  1. Найти все названия секций в легенде.
  2. Найти все секции диаграммы.
  3. Добавить каждому названию секции отслеживание событий наведения и снятия курсора.
  4. Внутри обработчиков этих событий добавлять или удалять класс hovered у секций диаграммы в зависимости от положения курсора.
let captionsList = document.querySelectorAll('.caption-item');
let unitsList = document.querySelectorAll('.unit');

captionsList.forEach(function (item, index) {
  item.addEventListener('mouseover', function () {
     unitsList[index].classList.add('hovered');
  });
  
  item.addEventListener('mouseout', function () {
     unitsList[index].classList.remove('hovered');
  });
});

Здесь мы отслеживаем движение мыши с помощью событий mouseover (курсор над элементом) и mouseout (курсор уходит с элемента). А для поиска нужной секции используем совпадение порядковых номеров названий в легенде и секций диаграммы — index названия = index секции. Нужно учесть, что их всегда должно быть равное количество.

Вот теперь диаграмма готова! Весь код, который мы написали в туториале, доступен на CodePen.

See the Pen svg pie chart by sasha_htmlacademy (@sasha-sm) on CodePen.

Полезности

SVG — для настоящих джедаев вёрстки

Почувствуйте силу на интерактивных курсах. 11 глав бесплатно, и −30% на подписку в первую неделю.

Регистрация

Нажатие на кнопку — согласие на обработку персональных данных