Приветствую!


Совсем недавно для одного проекта мне понадобилось отображать проценты в круглых графиках(?). И как обычно я принялся искать готовое решение в интернете, однако ничего путного найти не удалось (возможно из-за того что я точно не знаю как этот элемент правильно называется). Более-менее то что мне нужно я нашел в библиотеке Knob, но его функционал оказался излишен, т.к изменять значения в графике нет необходимости, помимо этого в библиотеке затесался баг. В итоге пришлось сочинять очередной велосипед.

image

Сперва я смотрел в сторону css, ну сами посудите — круг делаем через border-radius, а border-color покажет сколько процентов… Разумеется это применимо к 0%, 25%, 50%, 75% и 100%:

html:
<div class="dial procent0"><p>0%</p></div>
<div class="dial procent25"><p>25%</p></div>
<div class="dial procent50"><p>50%</p></div>
<div class="dial procent75"><p>75%</p></div>
<div class="dial procent100"><p>100%</p></div>

scss:
.dial {
	width: 80px;
	height: 80px;
	font-size: 20px;
	text-align: center;
	line-height: 80px;
	border: 6px solid #262832;
	border-radius: 100%;
	margin: 20px;
	display: inline-block;
	transform: rotate(-45deg);
	p {
		margin: 0;
		transform: rotate(45deg);
	}
	&.procent25 {
		border-right-color: #689f38;
	}
	&.procent50 {
		border-right-color: #689f38;
		border-bottom-color: #689f38;
	}
	&.procent75 {
		border-right-color: #689f38;
		border-bottom-color: #689f38;
		border-left-color: #689f38;
	}
	&.procent100 {
		border-color: #689f38;
	}
}

> jsfiddle

Если рассуждать в этом ключе дальше то можно задействовать псевдоэлементы, и рисовать один круг над другим. т.е если нам нужно показать 33% то мы рисуем 2 круга по 25%, просто второй круг поворачиваем так что бы при наложении закрашенным оказалось 33% border, для этого нужно просто рассчитать на сколько градусов повернуть псевдоэлемент:

$procent*3,6-90$


html:
<div class="dial"><p>33%</p></div>

scss:
.dial {
	width: 80px;
	height: 80px;
	font-size: 20px;
	text-align: center;
	line-height: 80px;
	border: 6px solid #262832;
	border-radius: 100%;
	margin: 20px;
	display: inline-block;
	transform: rotate(-45deg);
	border-right-color: #1390d4;
	p {
		margin: 0;
		transform: rotate(45deg);
	}
	&::before {
		content: '';
		display: block;
		position: absolute;
		width: 80px;
		height: 80px;
		border-radius: 100%;
		border: 6px solid transparent;
		border-right-color:#1390d4;
		margin: -6px -6px;
		transform: rotate(28.8deg);
	}
}

> jsfiddle

Разумеется если нужно будет показать график 51% и больше то нужно будет закрасить border-bottom. Загвоздка остаётся в том что проценты в моём графике не статичны, а рисовать для каждого графика свой стиль — мягко говоря не правильно, а ведь возможны дробные значения… Тут то нам и понадобится JavaScript, правда доступа к псевдоэлементам в джаве нет ибо находятся они вне DOM-дерева и к ним нельзя обратиться как к простым HTML-элементам. Конечно можно ::before заменить на span и крутить его… Но если пришлось использовать JavaScript то тогда можно рисовать график на canvas, тем более что в canvas есть специальная функция arc — которая рисует окружности.

Всё что я писал выше, лишь для того что бы показать ход моих мыслей

Рисуем круг:

var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.arc(100,75,50,0,2*Math.PI);
ctx.stroke();

image
context.arc(x,y,r,sAngle,eAngle,counterclockwise);

На счёт x,y — всё понятно, r(радиус) тоже не вызывает вопросов, необязательная опция counterclockwise — говорит направление в котором рисовать окружность, по умолчанию false = по часовой, true = против часовой.

sAngle и eAngle это начальная и конечная точка на окружности в радианах, более понятно что такое радианы объяснит гифка:
image

Чтобы перевести проценты в радианы нужно использовать формулу:

$radian=2*?*procent/100$



Собственно это и всё что нужно для того что бы нарисовать график.

html:
<div class="dial blue" data-width="180" data-lineWidth="41">66.233467</div>

scss:
.dial {
	border-color: #22262f;
	color: #689F38;
	display: inline-block;
	text-align: left;
	p {
		text-align:center;
		font-weight: bold;
		color: #fff;
		white-space: nowrap;
		position: relative;
		overflow: hidden;
		z-index: 1;
		margin: 0;
	}
	canvas {
		position: absolute;
	}
}

JavaScript + jQuery:
$(function(){
	// Ищем все элементы с class="dial"
	var dials = $(".dial");
	// Перебираем все .dial и пихуем туда canvas с графиком.
	for (i=0; i < dials.length; i++){
		var width = (typeof $(dials[i]).attr("data-width") != 'undefined') ? Math.round($(dials[i]).attr("data-width")) : 80;
		var procent = (Number($(dials[i]).html()) > 0 && Number($(dials[i]).html()) < 100) ? Math.round(Number($(dials[i]).html()) * 10)/10 : 0;
		var lineWidth = (typeof $(dials[i]).attr("data-lineWidth") != 'undefined') ? Number($(dials[i]).attr("data-lineWidth")) : width / 10;
                if(lineWidth >= width) lineWidth = width+1;
		var size = width+lineWidth;
		var lineRound = (typeof $(dials[i]).attr("data-lineRound") != 'undefined') ? true : false;
		var borderColor = $(dials[i]).css("border-color");
		var color = $(dials[i]).css("color");
		// Меняем размер .dial в зависимости от data-width="80"
		// Устанавливаем размер шрифта так что бы он вмещался в круг не задевая border
		$(dials[i]).css({"width": size + 'px', "height": size + 'px', "font-size": Math.floor((width-lineWidth) / 4) + 'px'});
		// Вставляем canvas такого же размера что и родитель.
		$(dials[i]).html('<canvas id="dial' + i + '" width="' + size + '" height="' + size + '"></canvas><p>' + procent + '%</p>');
		// Выравниваем текст по вертикали
		$("p", dials[i]).css({"line-height": size + 'px'});
		var canvas = document.getElementById("dial" + i);
    var context = canvas.getContext("2d");
		// считаем по формуле радианы
		var radian = 2*Math.PI*procent/100;
		// рисуем круг для фона
		context.arc(width/2+lineWidth/2, width/2+lineWidth/2, width/2, 0, 2*Math.PI, false);
		context.lineWidth = lineWidth;
		context.strokeStyle = borderColor;
		context.stroke();
		context.beginPath();
		// рисуем круг с процентами
		context.arc(width/2+lineWidth/2, width/2+lineWidth/2, width/2, 1.5 * Math.PI, radian+1.5 * Math.PI, false);
		context.strokeStyle = color;
		// Можно скруглить концы отрезка если передан параметр data-lineRound
		if (lineRound == true && lineWidth < width) context.lineCap = "round";
    context.stroke();
	}
});

Чтобы добавить круглый график(?) нужно добавить на страницу:

<div class="dial">проценты (float)</div>

В качестве атрибутов можно добавить:

data-width — диаметр (по умолчанию 80)
data-lineWidth — ширина линии (по умолчанию 1/10 от диаметра)
* размер графика равен data-width + data-lineWidth
data-lineRound — округлять края отрезка.

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

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

/* разные цветовые схемы */
.dial {
	&.error {
		color: #d46d71;
		p {
			color: #d46d71;
		}
	}
	&.success {
		color: #689F38;
		border-color: rgba(#d46d71, 0.4);
		p{
			color: #689F38;
		}
	}
	&.blue {
		color: #1390d4;
	}
}

— на мой взгляд так удобнее, но всегда остаётся возможность указать цвет в style=«border-color: ***; color: ***»

> Демо

Всем спасибо за внимание.
Поделиться с друзьями
-->

Комментарии (20)


  1. vintage
    02.04.2017 08:56
    +20

    Поздравляю, вы изобрели SVG :-)


  1. Akuma
    02.04.2017 10:27

    Я пользуюсь вот этой библиотекой: https://github.com/kottenator/jquery-circle-progress
    Позволяет использовать картинку как «заливку», что довольно удобно.

    Не рекламы ради, но можете посмотреть результат например тут:
    https://sp-yuga.ru/s1313/


  1. kana-desu
    02.04.2017 11:14

    http://codepen.io/e-andrew/pen/PpVKmJ



    1. GeMir
      02.04.2017 12:38

      Snap.svg — ещё одна замечательная библиотека для работы с SVG.
      Пример анимированного индикатора (не мой).


  1. OtshelnikFm
    02.04.2017 11:46

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

    Называется pie chart


    1. Mingun
      02.04.2017 12:19

      Даже Pie / Donut chart (круговая / кольцевая диаграмма соответственно, если по-русски). Примеры (на d3.js): простая кольцевая диаграмма, множественная кольцевая диаграмма, библиотечка d3pie (для автора её функционал, наверное, излишен).


    1. mayorovp
      02.04.2017 12:22

      Нет, pie chart — это немного другое. То, что сделал автор, называется doughnut chart


  1. GeMir
    02.04.2017 12:46
    +3

    Использовать звёздочку в качестве знака умножения в LaTeX — дурной тон.

    $$\text{percentage}\cdot 3,6 - 90$$
    $$\text{radian} = 2\cdot \pi \cdot \frac{\text{percentage}}{100}$$
    

    image
    Для защиты текста, от превращения в последовательность переменных используйте

     \text{}
    


  1. djasonx
    02.04.2017 21:12

    Хорошая статья! Спасибо!


  1. al5dy
    02.04.2017 21:13
    -1

    Автор молодец конечно, но серьезно… в 2к17 писать с нуля диаграмму?
    1. тыц
    2. тыц
    и я всего навсего ввел в гугель — круговая диаграмма jquery (на англ. еще больше вариантов выдает.)


  1. helg1978
    02.04.2017 21:46

    Также в google charts можно получить донат из pie chart, используя свойство pieHole


  1. greshnick_007
    03.04.2017 01:29

    Спасибо большое за статью.
    Совсем недавно тоже решал вопрос с построением диаграмм. Только мне нужно было ее еще и сделать анимационной. Искал множество решений и через CSS и через библиотеки, но ни один вариант не понравился. В итоге создал свою диаграмму с возможностью настройки отображения диаграммы. Исходный код класса диаграммы и ссылка на демо. Буду признателен, если хабровчане прокомментируют мое решение. Спасибо большое!)


    1. funnybanana
      03.04.2017 01:33
      +1

      выглядит не эстетично, нужно бы добиться плавных линий…
      image


      1. Vladal
        03.04.2017 11:32

        Наше руководство любит «спидометры». Пожалуй, переведу пару отчетов на Вашу разработку, может, понравится им новый вид.


      1. subzey
        03.04.2017 18:15
        +1

        Достаточно канвас очищать на каждом кадре


  1. Odrin
    03.04.2017 10:37
    +1

    Делать 11 раз подряд вызов функции с одним и тем же аргументом — не самая лучшая практика.

    $(dials[i])
    


    1. Zenitchik
      03.04.2017 12:27
      -3

      Очевидно, JavaScript для автора — не родной.


  1. s_berez
    03.04.2017 14:59
    +1

    Да, это называется donut charts.
    В крайней своей задаче я использовал гугло графики. Есть еще аналоги например highcharts.
    https://developers.google.com/chart/interactive/docs/gallery/piechart

    Вот реальный скрин с продакшена:
    image


  1. subzey
    03.04.2017 15:16
    +1

    На SVG и stroke-dasharray можно очень дёшево сделать подобный бублик: http://jsfiddle.net/subzey/68s4myzk/embedded/result%2Chtml%2Cjs/


  1. Tamplier91
    04.04.2017 01:53
    +1

    В свое время столкнулся с подобной задачей, только вместо диаграмм были маркеры на карте для Leaflet с дополнительным функционалов вроде окрашивания в различные цвета в зависимости от состояния, индикации различных состояний на внешнем контуре в виде сегментов. Ввиду необходимости серьезной кастомизации под собственные цели отказался от использования готовых библиотек. Писал сам, встраивание, управление стилями, свойствами — обычным jQuery.

    В первой итерации попробовал канвас — получился примерно такой же код, как приведен автором — те же смещения, углы поворота, постоянная отрисовка. На 4K+ объектов в итоге начали ощущаться серьезные лаги, явное замедление скорости работы. Особенно при изменении масштаба карты.

    В итоге переделал с помощью SVG. Как результат:
    — ощутимый прирост скорости рендеринга,
    — втрое меньше кода,
    — из чего вытекает отсутствие даже самой необходимости использования сторонних решений (весь код управления маркером + логика поведения без шаблонов умещается на одном экране),
    — и самый весомый плюс — масштабируемость без потери качества. Более того, даже сам шаблон не потребовал создания путей и прочих элементов.
    Для круглого маркера: подложка+слои по количеству отрисовываемых сегментов.
    Так что с целью «поиграться» может и подойдет canvas, но гораздо проще и красивее задачу можно решить с помощью SVG графики.