Пример круговой диаграммы в SVG средствами Raphael и PHP

в 10:23, , рубрики: javascript, php, Raphael, svg, векторная графика, диаграммы, метки: , , , ,

В ходе разработки одного из наших проектов мы столкнулись с необходимостью отдавать в клиентском html большое количество графики. С точки зрения минимализации нагрузки на сервер, строить объёмную графику на стороне клиента — это единственное правильное решение. При поиске готовых и подходящих нам JS решений из этой области мы основательно подсели на JS библиотеку Raphael, которая позволяет легко прорисовывать векторную графику во всех актуальных на сегодня браузерах. Разобравшись в функционале и отладив несколько функций по созданию основных типов диаграмм, мы решили поделиться здесь своими наработками.

Пример круговой диаграммы в SVG средствами Raphael и PHP

Итак. Описанный ниже пример создаёт средствами 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js