Разработка гибких и поддерживаемых круговых диаграмм с помощью CSS и SVG

Добавлено 27 января 2016 в 13:00

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

Хотя сейчас задача и не представляется такой невозможной, как это было раньше, до сих пор не существует простого однозначного решения. Тем не менее, сегодня существует много лучших (по сравнению с методами, приведенными выше), более поддерживаемых способов реализации круговых диаграмм.

Решение на основе transform

Это решение является лучшим в плане разметки: для него необходим только один элемент, а всё остальное будет выполнено с помощью псевдоэлементов, преобразований и CSS градиентов. Давайте начнем с простого элемента:

<div class="pie"></div>

Теперь давайте предположим, что нам нужна круговая диаграмма, которая отображает жестко заданные 20%. Над тем, чтобы она была гибкой, мы поработаем позже. Сначала стилизуем элемент как круг, который будет нашим фоном (рисунок 1):

 
Рисунок 1 – Наша отправная точка (круговая диаграмма, отображающая 0%)
.pie {
  width: 100px; height: 100px;
  border-radius: 50%;
  background: yellowgreen;
}

Наша круговая диаграмма будет зеленой (а конкретнее, yellowgreen) с коричневым сектором (#655), показывающим долю в процентах. У нас мог бы возникнуть соблазн использовать наклонные преобразования для процентной части, но как показывают небольшие эксперименты, это будет довольно неаккуратным решением. Вместо этого, мы раскрасим левую и правую части нашего круга в два цвета и используем вращение псевдоэлемента, чтобы раскрыть только ту долю круга, которая нам нужна.

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

background-image:
  linear-gradient(to right, transparent 50%, #655 0);
 
Рисунок 2 – Раскрашивание правой части нашего круга в коричневый цвет простым линейным градиентом

Как вы можете увидеть на рисунке 2, это всё, что нам требовалось. Теперь мы можем приступить к стилизации псевдоэлемента, который будет выступать в качестве маски:

.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
}
 
Рисунок 3 – Псевдоэлемент, действующий как маска, выделен здесь с помощью пунктирной границы

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

  • так как мы хотим скрыть коричневую часть нашего круга, то мы должны применить к псевдоэлементу зеленый фон, используя background-color: inherit, чтобы избежать дублирования при назначении ему такого же цвета фона, как у родительского элемента;
  • мы хотим, чтобы он вращался вокруг центра круга, который находится на середине левой стороны псевдоэлемента, поэтому мы должны задать transform-origin значение 0 50% или просто left;
  • мы не хотим, чтобы он был прямоугольником, так как при этом он выходит за края круговой диаграммы, поэтому мы должны либо применить overflow: hidden к .pie, либо задать ему border-radius, чтобы сделать его полукругом.

Сложив всё это вместе, мы получим следующий стиль для нашего псевдоэлемента;

.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
}
 
Рисунок 4 – Наш псевдоэлемент (показан с пунктирной границей) после окончания стилизации
Примечание: Не используйте background: inherit;, вместо backround-color: inherit;, так как в этом случае будет унаследован и градиент!

Теперь наша круговая диаграмма выглядит, как на рисунке 4. Здесь и начинается самое интересное! Мы можем начать вращать псевдоэлемент, применяя преобразование rotate(). Для 20%, которые мы пытаемся реализовать, мы можем использовать значение 72deg (0.2 × 360 = 72), или .2turn, что более читаемо. На рисунке 5 вы можете увидеть, как это выглядит и для нескольких других значений.

 
 
 
Рисунок 5 – Наша простая круговая диаграмма, показывающая разные процентные доли, слева направо: 10% (36deg или .1turn), 20% (72deg или .2turn), 40% (144deg или .4turn)

Можно подумать, что дело сделано, но, к сожалению, не всё так просто. Наша круговая диаграмма отлично подходит для отображения процентных долей от 0 до 50%, но если мы попытаемся отобразить 60-процентный поворот (применив .6turn), получится то, что изображено на рисунке 6. Но всё же не теряйте надежду, мы можем это исправить и сделаем это!

 
Рисунок 6 – Наша круговая диаграмма ломается для долей более 50% (здесь показано для 60%)

Если рассматривать отображение долей 50%–100%, как отдельную проблему, то можно заметить, что для них мы можем использовать перевернутую версию предыдущего решения: коричневый псевдоэлемент, вращающийся, соответственно, от 0 до .5turn. Таким образом, для доли 60% код псевдоэлемента будет выглядеть следующим образом:

.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background: #655;
  transform-origin: left;
  transform: rotate(.1turn);
}
 
Рисунок 7 – Наша, теперь правильная, круговая диаграмма со значением 60%

На рисунке 7 вы можете увидеть код в действии. Так как теперь мы разработали способ для отображения любой процентной доли, то можем и анимировать круговую диаграмму от 0% до 100% с помощью CSS анимации, создав своеобразный индикатор прогресса:

@keyframes spin {
  to { transform: rotate(.5turn); }
}

@keyframes bg {
  50% { background: #655; }
}

.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
  animation: spin 3s linear infinite,
             bg 6s step-end infinite;
}

See the Pen Animated pie chart CSS by RadioProg (@radioprog) on CodePen.

Всё хорошо, но как нам стилизовать несколько статических круговых диаграмм с различными процентными долями, что и является наиболее общим случаем использования диаграмм? В идеале, мы хотим, чтобы была возможность напечатать что-то вроде этого:

<div class="pie">20%</div>
<div class="pie">60%</div>

...и получить две круговые диаграммы, одна из которых показывает 20%, а другая – 60%. Во-первых, мы рассмотрим, как можно это сделать с помощью встроенных стилей, а затем мы всегда можем написать короткий скрипт для разбора текстового контента и добавить указанные встроенные стили для элегантности кода, инкапсуляции, поддерживаемости и, возможно, самое главное, доступности.

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

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

See the Pen Выбор цветов by RadioProg (@radioprog) on CodePen.

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

Так как наша анимация приостановлена, будет показан только первый ее кадр (определяется нашим отрицательным значением animation-delay). Процентная доля, показанная на круговой диаграмме, будет равна процентной доле, которую составляет наш animation-delay в общей продолжительности. Например, с текущей продолжительностью 6s, нам необходимо значение animation-delay, равное -1.2s, чтобы показать долю 20%. Для упрощения вычислений мы будем устанавливать продолжительность в 100s. Имейте в виду, что, так как анимация остановлена навсегда, значение ее продолжительности, которое мы устанавливаем, не играет никакой другой роли.

И последний вопрос: анимация применяется к псевдоэлементу, но мы хотим установить встроенный стиль на элемент .pie. Тем не менее, хотя на <div> нет анимации, мы можем установить для него animation-delay, как встроенный стиль, а затем использовать animation-delay: inherit; для псевдоэлемента. Сложив всё это вместе, наша разметка для 20% и 60% круговых диаграмм будет выглядеть следующим образом:

<div class="pie"
     style="animation-delay: -20s"></div>
<div class="pie"
     style="animation-delay: -60s"></div>

И CSS код для этой анимации станет следующим (правила для .pie не показаны, так как остались теми же):

@keyframes spin {
  to { transform: rotate(.5turn); }
}

@keyframes bg {
  50% { background: #655; }
}

.pie::before {
  /* [остальные стили не изменились] */
  animation: spin 50s linear infinite,
             bg 100s step-end infinite;
  animation-play-state: paused;
  animation-delay: inherit;
}

На данный момент мы можем преобразовать разметку для использования процентов в качестве контента, как мы изначально и собирались сделать, и добавить встроенные стили animation-delay через простой скрипт:

$$('.pie').forEach(function(pie) {
  var p = parseFloat(pie.textContent);
  pie.style.animationDelay = '-' + p + 's';
});

Обратите внимание, что мы оставили нетронутым текст, так как он необходим нам для доступности и удобства использования. Сейчас наши круговые диаграммы выглядят, как на рисунке 8. Мы должны скрыть текст, который можно сделать доступным через color: transparent так, чтобы он оставался выбираемым и печатаемым. Для дополнительного глянца мы можем поместить значения процентов в центр круговой диаграммы, чтобы они не находились в случайном месте, когда пользователь попытается выделить их. Чтобы сделать это, нам необходимо:

20%
60%
Рисунок 8 – Наш текст перед тем, как мы спрячем его
  • преобразовать значение height диаграммы в line-height (или добавить значение line-height, равное height, но это будет бессмысленным дублированием кода, так как line-height будет установлено в вычисленное значение height, что хорошо);
  • задать размер и положение псевдоэлемента с помощью абсолютного позиционирования, чтобы он не толкал текст вниз;
  • добавить text-align: center;, чтобы отцентрировать текст по горизонтали.

Окончательный код выглядит так:

.pie {
  position: relative;
  width: 100px;
  line-height: 100px;
  border-radius: 50%;
  background: yellowgreen;
  background-image:
    linear-gradient(to right, transparent 50%, #655 0);
  color: transparent;
  text-align: center;
}

@keyframes spin {
  to { transform: rotate(.5turn); }
}
@keyframes bg {
  50% { background: #655; }
}

.pie::before {
  content: '';
  position: absolute;
  top: 0; left: 50%;
  width: 50%; height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
  animation: spin 50s linear infinite,
             bg 100s step-end infinite;
  animation-play-state: paused;
  animation-delay: inherit;
}

See the Pen JGLOyr by RadioProg (@radioprog) on CodePen.

Решение на основе SVG

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

Начнем с круга:

<svg width="100" height="100">
<circle r="30" cx="50" cy="50" />
</svg>

Теперь применим к нему несколько базовых стилей:

circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 30;
}

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

Рисунок 9 – Наша начальная точка: зеленый SVG круг с толстой #655 обводкой

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

stroke-dasharray: 20 10;
Рисунок 10 – Простая пунктирная обводка, созданная с помощью stroke-dasharray

Это означает, что мы хотим получить тире длиной 20 с промежутками длиной 10, как те, что изображены на рисунке 10. В этот момент вы можете быть удивлены, что этот SVG пример имеет что-то общее с круговыми диаграммами. Но всё становится яснее, когда мы применим обводку с длиной тире 0 и промежутками длиной больше или равной длине окружности нашего круга (C = 2πr, или в нашем случае C = 2π × 30 ≈ 189):

stroke-dasharray: 0 189;
Рисунок 11 – Несколько значений stroke-dasharray и их результат; слева направо: 0 189; 40 189; 95 189; 150 189

Как вы можете видеть, в первом круге на рисунке 11 это полностью удаляет обводку, и мы остаемся только с зеленым кругом. Однако веселье начинается, когда мы начинаем увеличивать первое значение (рисунок 11): из-за такого длинного разрыва мы получим уже не пунктирную обводку, а обводку, которая покрывает такую процентную долю окружности круга, какую мы укажем.

Возможно, вы уже поняли, в какую сторону мы двигаемся: если уменьшить радиус нашего круга достаточно, чтобы он полностью закрывался своей обводкой, мы, в конечном итоге, получим что-то, очень напоминающее круговую диаграмму. Например, на рисунке 12 вы можете увидеть, как это будет выглядеть при применении к кругу с радиусом 25 и шириной обводки (stroke-width) 50:

Рисунок 12 – Наше SVG изображение начинает напоминать круговую диаграмму

Помните: SVG обводки всегда наполовину внутри и наполовину снаружи элемента, к которому они применяются. В будущем нам будет доступно управление этим поведением.

<svg width="100" height="100">
  <circle r="25" cx="50" cy="50" />
</svg>
circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 50;
  stroke-dasharray: 60 158; /* 2π × 25 ≈ 158 */
}

Теперь превратить это изображение в круговую диаграмму, подобную тем, что мы создали в предыдущем решении, довольно легко: нужно просто добавить больший зеленый круг под обводку и повернуть его на 90° против часовой стрелки так, чтобы он начинался вверху в середине. Поскольку <svg> элемент также является и HTML элементом, мы можем просто применить к нему стиль:

svg {
  transform: rotate(-90deg);
  background: yellowgreen;
  border-radius: 50%;
}
Рисунок 13 – Конечная круговая диаграмма с SVG

Вы можете увидеть окончательный результат на рисунке 13. Этот метод делает еще проще анимацию круговой диаграммы от 0% до 100%. Нам просто нужно создать CSS анимацию, которая изменяет stroke-dasharray от 0 158 до 158 158:

@keyframes fillup {
  to { stroke-dasharray: 158 158; }
}

circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 50;
  stroke-dasharray: 0 158;
  animation: fillup 5s linear infinite;
}

В качестве дополнительного усовершенствования мы можем задать определенный радиус круга так, чтобы длина его окружности составляла (бесконечно близко к) 100, и поэтому мы сможем указывать длины stroke-dasharray, как проценты, без каких-либо расчетов. Поскольку длина окружности равна 2πr, нам необходим радиус 100 ÷ 2π ≈ 15.915494309, который для наших нужд может быть округлен до 16. Также мы зададим размеры SVG в атрибуте viewBox, вместо атрибутов width и height, чтобы сделать его подстраиваемым под размеры его контейнера.

После этих модификаций разметка круговой диаграммы, изображенной на рисунке 13, станет следующей:

<svg viewBox="0 0 32 32">
  <circle r="16" cx="16" cy="16" />
</svg>

А CSS станет таким:

svg {
  width: 100px; height: 100px;
  transform: rotate(-90deg);
  background: yellowgreen;
  border-radius: 50%;
}

circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 32;
  stroke-dasharray: 38 100; /* для 38% */
}

Обратите внимание, как легко теперь изменить процентную долю. Хотя даже с таким упрощением мы не хотим повторять всю эту SVG разметку для каждой круговой диаграммы. Пришло время JavaScript, чтобы помочь нам небольшой автоматизацией. Мы напишем небольшой скрипт, чтобы взять простую HTML разметку, подобную этой...

<div class="pie">20%</div>
<div class="pie">60%</div>

...и добавить встроенный SVG внутрь каждого элемента .pie со всеми необходимыми элементами и атрибутами. Он также добавит элемент <title> для доступности так, чтобы пользователи экранных дикторов также могли узнать, какие проценты отображаются. Окончательный скрипт будет выглядеть следующим образом:

$$('.pie').forEach(function(pie) {
  var p = parseFloat(pie.textContent);
  var NS = "http://www.w3.org/2000/svg";
  var svg = document.createElementNS(NS, "svg");
  var circle = document.createElementNS(NS, "circle");
  var title = document.createElementNS(NS, "title");
  circle.setAttribute("r", 16);
  circle.setAttribute("cx", 16);
  circle.setAttribute("cy", 16);
  circle.setAttribute("stroke-dasharray", p + " 100");
  svg.setAttribute("viewBox", "0 0 32 32");
  title.textContent = pie.textContent;
  pie.textContent = '';
  svg.appendChild(title);
  svg.appendChild(circle);
  pie.appendChild(svg);
});

Вот оно! Вы можете подумать, что CSS метод лучше, так как его код проще и более знаком. Однако SVG метод имеет определенные преимущества перед решением на чистом CSS:

  • проще добавить третий цвет: просто добавьте еще один обведенный круг и передвиньте его обводку с помощью stroke-dashoffset. Или добавьте длину его обводки к длине обводки предыдущего круга перед (под) ним. Как именно вы добавите третий цвет к круговой диаграмме, созданной первым способом?
  • мы не должны прикладывать каких-либо дополнительных усилий для печати, так как SVG элементы рассматриваются, как контент и печатаются так же, как элементы <img>. Первое решение зависит от фона и, таким образом, будет не напечатано;
  • мы можем изменять цвета с помощью встроенных стилей, что означает, что мы можем легко изменять их через скрипты (т.е. в зависимости от ввода пользователя). Первое решение опирается на псевдоэлементы, которые не могут принимать встроенные стили, кроме как через наследование, что не всегда удобно.

See the Pen SVG pie charts by RadioProg (@radioprog) on CodePen.

Спецификации по теме

Будущее круговых диаграмм

Конические градиенты также будет здесь очень полезны. Всё, что потребуется для круговой диаграммы, это круглый элемент с коническим градиентом с двумя цветовыми остановками. Например, 40% круговая диаграмма с рисунка 5 будет такой простой:

.pie {
  width: 100px; height: 100px;
  border-radius: 50%;
  background: conic-gradient(#655 40%, yellowgreen 0);
}

Кроме того, как только обновленная функция attr(), определенная в CSS Values Level 3, начнет широко применяться, вы сможете управлять процентной долей с помощью простого HTML атрибута:

background: conic-gradient(#655 attr(data-value %), yellowgreen 0);

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

background: conic-gradient(deeppink 20%, #fb3 0, #fb3 30%, yellowgreen 0);

Вот и всё! Оставляйте комментарии!


На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.


Сообщить об ошибке