В ходе разработки одного из наших проектов мы столкнулись с необходимостью отдавать в клиентском html большое количество графики. С точки зрения минимализации нагрузки на сервер, строить объёмную графику на стороне клиента — это единственное правильное решение. При поиске готовых и подходящих нам JS решений из этой области мы основательно подсели на JS библиотеку Raphael, которая позволяет легко прорисовывать векторную графику во всех актуальных на сегодня браузерах. Разобравшись в функционале и отладив несколько функций по созданию основных типов диаграмм, мы решили поделиться здесь своими наработками.
Итак. Описанный ниже пример создаёт средствами Raphael и PHP круговую диаграмму в формате SVG, представленную на изображении. Мы постарались максимально подробно описать исходный код, чтобы помочь всем тем, кто самостоятельно разобраться в этом не нашёл сил или времени.
Файл circle.php:
<?php
// для начала необходимо создать объект с определённым идентификатором,
// заданной ширины и высоты для дальнейшей работы
function start_paper($id, $width, $height) {
return 'var r = Raphael("' .$id .'", ' .$width .', ' .$height .');';
}
// создание круговой диаграммы
function paper_circle_chart($params) {
// расчёт радиуса "излома" как половины толщины сектора
// для создания 3D эффекта выпуклости диаграммы
$shadow_width = floor(($params['radius']-$params['inradius'])*0.47);
$pradius = $params['radius'] - $shadow_width;
// радиус выноса линий выноски
$outradius = $params['radius'] + $params['text_radius'];
// сумма данных
$total = array_sum($params['data']);
$i = 0;
$prev = 0;
$fangel = 0;
$code = '';
$pcode = '';
$pline = '';
$ptext = '';
$pstext = '';
$center = '';
$begin = true;
// рисуем секторы диаграммы в цикле
foreach ($params['data'] as $k => $v) {
// определяем цвет текущего сектора
$color = $params['colors'][$i];
// если нулевой сектор проходим мимо
if ($v == 0) {
$i++;
continue;
}
// если диаграмма состоит из одного сектора - рисуем сплошной круг
elseif ($v == $total) {
$pend = deg2rad(45);
$code = '
r.circle(' .$params['centerx'] .', ' .$params['centery'] .', ' .$params['radius'] .').attr({"fill": "' .$color .'"})';
if (count($params['texts'])) {
$dxc = round($params['centerx'] + $pradius * sin($pend), 2);
$dyc = round($params['centery'] - $pradius * cos($pend), 2);
$dxc1 = round($params['centerx'] + $outradius * sin($pend), 2);
$dyc1 = round($params['centery'] - $outradius * cos($pend), 2);
$dxc2 = $dxc1 + $params['text_width'];
$dxc3 = $dxc1 + round($params['text_width']/2, 2);
$dyc2 = $dyc1 - $params['text_minus'];
$dyc3 = $dyc1 + $params['text_plus'];
$pcode .= '
r.circle(' .$dxc .', ' .$dyc .', ' .$params['point_radius'] .')';
$pline .= '
r.path("M'. $dxc .','. $dyc .' L'. $dxc1 .','. $dyc1 .' L'. $dxc2 .','. $dyc1 .'")';
$ptext .= '
r.text(' .$dxc3 .', ' .$dyc2 .', "100 %")';
$pstext .= '
r.text(' .$dxc3 .', ' .$dyc3 .', "' .$params['texts'][$i] .'")';
}
}
// иначе - рисуем текущий сектор
else {
$percent = $v / $total;
$angel = 360 * $percent;
$rad = deg2rad($angel);
$end = $prev + $rad;
$pend = $prev + $rad/2;
$dx = round($params['centerx'] + $params['radius'] * sin($prev), 2);
$dy = round($params['centery'] - $params['radius'] * cos($prev), 2);
$dxp = round($params['centerx'] + $params['radius'] * sin($end), 2);
$dyp = round($params['centery'] - $params['radius'] * cos($end), 2);
if ($percent > 0.5) $sec = 1;
else $sec = 0;
if (!$begin) $code .= ',';
$code .= '
r.path("M' .$params['centerx'] .',' .$params['centery'] .' L' .$dx .',' .$dy .' A' .$params['radius'] .',' .$params['radius'] .' 0 ' .$sec .',1 ' .$dxp .',' .$dyp .' z").attr({"fill": "' .$color .'"})';
if (count($params['texts'])) {
$dxc = round($params['centerx'] + $pradius * sin($pend), 2);
$dyc = round($params['centery'] - $pradius * cos($pend), 2);
$dxc1 = round($params['centerx'] + $outradius * sin($pend), 2);
$dyc1 = round($params['centery'] - $outradius * cos($pend), 2);
if (($fangel + $angel/2) > 180) {
$dxc2 = $dxc1 - $params['text_width'];
$dxc3 = $dxc1 - round($params['text_width']/2, 2);
}
else {
$dxc2 = $dxc1 + $params['text_width'];
$dxc3 = $dxc1 + round($params['text_width']/2, 2);
}
$dyc2 = $dyc1 - $params['text_minus'];
$dyc3 = $dyc1 + $params['text_plus'];
if (!$begin) {
$pcode .= ',';
$ptext .= ',';
$pstext .= ',';
$pline .= ',';
}
$pcode .= '
r.circle(' .$dxc .', ' .$dyc .', ' .$params['point_radius'] .')';
$pline .= '
r.path("M'. $dxc .','. $dyc .' L'. $dxc1 .','. $dyc1 .' L'. $dxc2 .','. $dyc1 .'")';
$ptext .= '
r.text(' .$dxc3 .', ' .$dyc2 .', "' .round($percent * 100) .' %")';
$pstext .= '
r.text(' .$dxc3 .', ' .$dyc3 .', "' .$params['texts'][$i] .'")';
}
$i++;
$begin = false;
$prev = $end;
$fangel += $angel;
}
}
// устанавливаем атрибут "контур" для всех секторов диагрммы
if ($code) $code = '
var st = r.set();
st.push(' .$code .'
);
st.attr({"stroke": "none"});';
// устанавливаем атрибут "цвет" для всех линий выноски
if ($pline) $pline = '
var st = r.set();
st.push(' .$pline .'
);
st.attr({"stroke": "' .$params['line_color'] .'"});';
// устанавливаем атрибуты "контур" и "цвет заливки" для плашек всех линий выноски
if ($pcode) $pcode = '
var st = r.set();
st.push(' .$pcode .'
);
st.attr({"fill": "' .$params['text_color'] .'", "stroke": "' .$params['stroke_color'] .'"});';
// устанавливаем атрибуты "шрифт", "размер" и "цвет" для подписей над линией выноски
if ($ptext) $ptext = '
var st = r.set();
st.push(' .$ptext .'
);
st.attr({"font-family": "' .$params['font'] .'", "font-size": "' .$params['text_name'] .'", "fill": "' .$params['text_color'] .'", "cursor": "default"});';
// устанавливаем атрибуты "шрифт", "размер" и "цвет" для подписей под линией выноски
if ($pstext) $pstext = '
var st = r.set();
st.push(' .$pstext .'
);
st.attr({"font-family": "' .$params['font'] .'", "font-size": "' .$params['text_small'] .'", "fill": "' .$params['text_color'] .'", "cursor": "default"});';
// создаём центр диаграммы и текст внутри него
$inradius = '';
if ($params['inradius'] > 0) {
$inradius = '
r.circle(' .$params['centerx'] .', ' .$params['centery'] .', ' .$params['inradius'] .').attr({"fill": "' .$params['center_text_back'] .'", "stroke": "none"});';
if ($params['shadow']) $inradius .= '
r.circle(' .$params['centerx'] .', ' .$params['centery'] .', ' .($params['radius']-$shadow_width+floor($shadow_width/2)) .').attr({"fill": "none", "stroke": "#FFFFFF", "stroke-width": "' .$shadow_width .'", "stroke-opacity": "0.15"});';
if ($params['center_text']) {
$center = '
r.text(' .$params['centerx'] .', ' .$params['centery'] .', "' .$params['center_text'] .'").attr({"font-family": "' .$params['font'] .'", "font-size": "' .$params['center_text_size'] .'", "fill": "' .$params['center_text_color'] .'", "cursor": "default"});';
}
}
return $code .$inradius .$pline .$pcode .$ptext .$pstext .$center;
}
?>
Файл test.php:
<?php
include("circle.php");
$params = array ( // параметры диаграммы
'font' => 'PT Sans, Tahoma', // шрифт
'text_color' => '#212121', // цвет текста
'line_color' => '#494949', // цвет линий выноски
'text_width' => 40, // ширина горизотальной части линий выноски
'text_radius' => 14, // расстояние отступа горизотальной части линий выноски от диаграммы
'point_radius' => 3, // радиус плашки линии выноски ( круг в месте соединения линии выноски с сектором данных )
'stroke_color' => '#EFEFEF', // цвет контура плашки
'text_name' => 13, // размер текста над линией выноски
'text_small' => 11, // размер текста под линией выноски
'text_minus' => 10, // отступ текста вверх над линией выноски
'text_plus' => 8, // отступ текста вниз под линией выноски
'centerx' => 100, // центр диаграммы по оси x
'centery' => 100, // центр диаграммы по оси y
'radius' => 42, // внешний радиус диаграммы
'inradius' => 19, // радиус внутреннего круга
'center_text_back' => '90-#e7e7e7-#ffffff:60', // цвет внутреннего круга
'center_text' => 416, // надпись в центре внутреннего круга
'center_text_size' => 14, // размер текста в центре внутреннего круга
'center_text_color' => '#212121', // цвет текста в центре внутреннего круга
'data' => array( // исходные данные
139,
112,
89,
76),
'texts' => array ( // подписи к секторам, отображаются под линией выноски
'пас',
'навес',
'в разрез',
'прострел'
),
'colors' => array ( // цвета секторов
'0-#08b2ff-#0e56d4',
'0-#fffa17-#ffba17',
'0-#e0070e-#f15722',
'0-#BCE408-#5FBB00'
),
'shadow' => 1 // делать или нет 3D эффект выпуклости диаграммы
);
// создаём диаграмму, в <head></head> размещаем js код, в <body></body> - <div></div>
// c идентификатором идентичным переданному в конструктор диаграммы start_paper()
$head = start_paper('diagram', 200, 200) .paper_circle_chart($params);
$body = '<div id="diagram"></div>';
echo '
<!DOCTYPE html>
<html>
<head>
<title>Пример круговой SVG диаграммы средствами Raphael и PHP</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="/raphael-min.js"></script>
<script>
window.onload = function () {'
.$head .'
};
</script>
</head>
<body>'
.$body .'
</body>
</html>';
?>
Приведенный выше исходный код полностью рабочий. Каждый желающий поковыряться и разобраться в изложенном материале самостоятельно может скачать библиотеку и собрать три файла (circle.php, test.php и raphael-min.js) в единое целое.
Итоговый html, который получает клиент:
<!DOCTYPE html>
<html>
<head>
<title>Пример круговой SVG диаграммы средствами Raphael и PHP</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="/raphael-min.js"></script>
<script>
window.onload = function () {var r = Raphael("diagram", 200, 200);
var st = r.set();
st.push(
r.path("M100,100 L100,58 A42,42 0 0,1 136.27,121.18 z").attr({"fill": "0-#08b2ff-#0e56d4"}),
r.path("M100,100 L136.27,121.18 A42,42 0 0,1 74.6,133.45 z").attr({"fill": "0-#fffa17-#ffba17"}),
r.path("M100,100 L74.6,133.45 A42,42 0 0,1 61.7,82.76 z").attr({"fill": "0-#e0070e-#f15722"}),
r.path("M100,100 L61.7,82.76 A42,42 0 0,1 100,58 z").attr({"fill": "0-#BCE408-#5FBB00"})
);
st.attr({"stroke": "none"});
r.circle(100, 100, 19).attr({"fill": "90-#e7e7e7-#ffffff:60", "stroke": "none"});
r.circle(100, 100, 37).attr({"fill": "none", "stroke": "#FFFFFF", "stroke-width": "10", "stroke-opacity": "0.15"});
var st = r.set();
st.push(
r.path("M127.75,84.07 L148.57,72.12 L188.57,72.12"),
r.path("M106.24,131.39 L110.93,154.92 L150.93,154.92"),
r.path("M68.99,107.89 L45.73,113.81 L5.73,113.81"),
r.path("M82.63,73.13 L69.59,52.97 L29.59,52.97")
);
st.attr({"stroke": "#494949"});
var st = r.set();
st.push(
r.circle(127.75, 84.07, 3),
r.circle(106.24, 131.39, 3),
r.circle(68.99, 107.89, 3),
r.circle(82.63, 73.13, 3)
);
st.attr({"fill": "#212121", "stroke": "#EFEFEF"});
var st = r.set();
st.push(
r.text(168.57, 62.12, "33 %"),
r.text(130.93, 144.92, "27 %"),
r.text(25.73, 103.81, "21 %"),
r.text(49.59, 42.97, "18 %")
);
st.attr({"font-family": "PT Sans, Tahoma", "font-size": "13", "fill": "#212121", "cursor": "default"});
var st = r.set();
st.push(
r.text(168.57, 80.12, "пас"),
r.text(130.93, 162.92, "навес"),
r.text(25.73, 121.81, "в разрез"),
r.text(49.59, 60.97, "прострел")
);
st.attr({"font-family": "PT Sans, Tahoma", "font-size": "11", "fill": "#212121", "cursor": "default"});
r.text(100, 100, "416").attr({"font-family": "PT Sans, Tahoma", "font-size": "14", "fill": "#212121", "cursor": "default"});
};
</script>
</head>
<body><div id="diagram"></div>
</body>
</html>
О том, как работает градиентная заливка (0-#BCE408-#5FBB00) с точки зрения синтаксиса Raphael и другие моменты касательно функций этой библиотеки и их параметров достаточно подробно изложены в документации. К слову подробная документация, широкий функционал и кроссбраузерность этого решения — являются, с нашей точки зрения, неоспоримым преимуществом данной библиотеки над аналогичными средствами.
Надеемся, что наш опыт будет полезен всем тем, кто впервые столкнулся с задачей построения векторной графики в html средствами JS. Постараемся ответить на все вопросы и комментарии по данной теме.
Автор: Andrii13