Графики, диаграммы, графы … и все это в веб? Часть 1

October 15, 2007

Хотя в своей повседневной работе я часто сталкиваюсь с необходимостью программировать всевозможные отчеты, диаграммы, графики, но до не давних пор сфера моих интересов была смещена в сторону desktop standalone приложений. В редких случаях, когда возникала потребность в разработке программок работающих в браузере, я обходился встроенным в состав flex компонентом “графики”, исходные данные задавались в формате xml, визуализация была приятная и стильная – одним словом, меня все устраивало. И вот в очередном проекте мне пришлось углубится в детальное изучение различных подходов к созданию внедренных в веб-страницы графиков, диаграмм, оценить их плюсы, минусы. Так что, пока воспоминания свежи, спешу поделиться с вами.

Как строить графики в веб? Вариант 1: в общем случае графики, диаграммы разных форм и видов - это все картинки, которые могут быть отрисованы с помощью: php, java или любого серверного языка программирования - благо универсальные библиотеки рисующие 2d изображения появились давным-давно. Вариант 2: отрисовка выполняется с помощью activex-компонентов или java-апплетов. Вариант 3: использование возможностей svg графики или тега canvas и, наконец, flash/flex. Очевидно, что самый плохой вариант - 2. Несмотря на потенциальную возможность реализовать сколь угодно сложное поведение внедренного в страницу компонента, добавить анимацию, интерактивность - все это теряется на фоне слов “не универсально”, “activex-только для windows”, “вирусы и activex – дружная семья”, “java плагин огромен по размеру и есть не у всех”. Первый подход (с генерацией статической картинки серверным скриптом) - самый универсальный и независящий от возможностей клиента. Вы не зависите от версии браузера, наличия в нем flash-плагина, настроек корпоративного proxy-сервера, который может резать часть трафика – те же flash-ролики. В минусы идет потеря интерактивности: так по нажатию на какой либо столбец в диаграмме достаточно сложно реализовать некоторый скрипт, который меняет изображение, переходит к новой странице, масштабирует картинку и т.д. Есть проблемы и с поддержкой анимации: необходимо использовать посторонние библиотеки. Например для php встроенный модуль gd2 не умеет работать с анимированными gif, но вы можете использовать gifsicle (http://www.lcdf.org/gifsicle/) или http://www.phpclasses.org/browse/package/3163.html. В минусы можно записать еще и неэкономное расходование сетевого трафика в некоторых случаях. В своей статье ([[Про тег html canvas - 1]]) я приводил пример, когда при отрисовке графика функции z=f(x,y), большая часть получившегося изображения будет залита одним цветом фона и лишь небольшая часть картинки будет отображать линии графика функции, оси, подписи. Для эксперимента я создавал картинку размером 1024*768 с нарисованной линией синусоиды. Ее размеры в разных форматах были таковы: gif - 8 кб, jpeg - 33 кб, png - зашкалил за 210 кб. В тот раз я обосновывал необходимость переноса логики построения графики на сторону клиента, а ajax поможет подгружать в страницу нужные данные. Если вы еще не сталкивались с тегом canvas - то это особый тег позволяющий выполнять рисование внутри веб-страницы: линии, дуги, кривые безье, работа с другими изображениями, операции масштабирования или вращения и т.д. на javascript. Формат изображений svg - основанный на xml и внедряется в дерево dom веб-страницы, поддерживается различные фигуры, заливки, текст, анимация - и все это доступно для управления через javascript. Увы, но в internet explorer для поддержки этих двух стандартов нужны сторонние плагины, о которых массовый пользователь ничего не знает, и устанавливать не собирается - в отличие от flash-плагина, распространенного, известного и легко обновляемого. Так значит flash - самое то? Увы, не совсем и не всегда. Что хочет сделать клиент после того как посмотрел страницу? Сохранить или распечатать. Увы, но самый замечательный браузер в мире - internet explorer что 6, что 7 не умеет сохранять flash-ролики. А это значит, что как бы хорошо не вела себя в плане сохранения flash-контента opera или firefox - решение увы не универсально. К тому же если flash-ролик не содержит внутри себя набор данных необходимых для построения графика, а загружает их динамически с сервера, то сохранить такую страницу в принципе не возможно (тут я немного лукавлю, решение есть - но технически очень сложное). Та же проблема с сохранением страницы на диске верна и для ajax-основанных сайтов. Таким образом идеального решения нет. И как вывод: если вам нужно решение работающее железно всегда и везде, страницы сохраняются и печатаются - на сервере генерируется статическая картинка. Важна возможность создания интерактивных графиков, диаграмм - flash.

Разумеется, что за я не первый кто озаботился проблемой построения красивых и функциональных графиков, на рынке есть и платные и бесплатные библиотеки позволяющие автоматизировать процесс разработки диаграмм. Сегодня я расскажу об трех интересных продуктах sparkline, libchart, graphpite - это основанные на php решения для динамической генерации изображения. Разумеется что подобных библиотек достаточно много, например по следующему адресу находится целый каталог подобных скриптов: http://www.hotscripts.com/PHP/Scripts_and_Programs/Graphs_and_Charts/. В следующий раз я закрою тему, рассказав об библиотеках для flash. Если будет достаточное количество откликов, то я также расскажу и о задаче визуализации в вебе графов. Если вы никогда не сталкивались с тем как работать в php с графикой и библиотекой gd2, то милости прошу познакомиться с http://www.php5.ru/articles/image. По правде говоря, есть еще один подход к построению диаграмм - имитация с помощью чистого html (например, столбчатая диаграмма будет представлена в виде таблицы - смотрится не так ужасно, как звучит). Если вас это заинтересует, то http://www.webguys.com/pdavis/Programs/html_graphs/, а мы переходим к sparkline.

Sparkline - написанная на php библиотека для построения маленьких, “wordlike” графиков внедряемых в текст страницы. Домашний сайт проекта http://sparkline.org/. В качестве технических требований к хостингу - только наличие модуля gd2 - есть везде и давно, даже бесплатных хостингах. В настоящее время есть несколько портов данной библиотеки на java, perl, ruby, javascript-версия (как основа используется canvas). Например, вот пример кода для использования javascript-основанной версии:
  1. <html>
  2.   <style type="text/css">  
  3.       .sparkline { display: none; }   
  4.   </style>
  5.   <script language="javascript" src="jspark.js"></script>
  6.   <body> 
  7.      Всю следующую неделю будет только холодать, только в конце недели выглянет солнце: 
  8.      <span class="sparkline" style="width: 50px;">-2,-10,-8,-7,5,7</span>
  9.   </body>
  10. </html>
Все что мне нужно было сделать - это загрузить с сайта http://ejohn.org/projects/jspark/ файл библиотеки jspark.js, подключить ее к файлу html. В коде самой страницы я могу создавать произвольные теги с атрибутом class равным “sparkline ”. При загрузке страницы, содержимое этих тегов (если оно есть) будет автоматически заменено на динамически сгенерированный тег canvas (вы догадались, что это не работает в ie?), график же строится на основании чисел перечисленных внутри тега через запятую. Пример работы кода показан на рис. 1.



Смотрится, откровенно, не очень: для восприятия не хватает осей и опорных подписей значений – но все же, как удобно применять sparkline, например, в основанных на mediawiki сайтах, подготовке документации и только из-за этого можно простить все.

Версия sparkline для php получше - прежде всего есть две версии sparkline: Sparkline_Bar и Sparkline_Line - диаграммы в виде набора столбиков (bar chart) и в виде линии соответственно. Я не привожу кода с примерами работы данной библиотеки поскольку ее php-версия достаточно сыра (я нашел пару багов практически не копая в глубину), и в терпеть ее недостатки по сравнению с libchart или graphpite просто не за что – данные уже не встраиваются внутрь hml-кода, а хранятся на сервере и вносятся внутрь объекта диаграммы с помощью функций setData (x, y) – схожий подход применяют и две следующие библиотеки, но качество кода там гораздо выше.

Теперь рассмотрим вторую библиотеку libchart (http://naku.dohcrew.com/libchart/pages/introduction/). Поддерживаются четыре вида диаграмм: VerticalBarChart, HorizontalBarChart, LineChart, PieChart. Методика работы традиционна: загружаете файлы библиотеки и подключаете их к своему проекту, затем создаете объект служащий для создания одной из тех самых четырех диаграмм. В качестве параметра конструктора следует указать размер будущего изображения и остается только наполнить диаграмму информацией. Для этого нам нужен еще один объект XYDataSet, метод addPoint которого как раз и служит для добавления очередной точки. XYDataSet привязывается к объекту диаграммы с помощью метода $chart->setDataSet($dataSet); и наконец мы задаем название диаграммы и говорим Render – создать изображение, как параметр задается имя файла, в который будет сохранено сформированное изображение, если же файл не задан, но результат в формате png будет отправлен сразу клиенту. Разве что вам придется явно указать тип отправляемого документа с помощью вызова header(что_мы_возвращаем). Интерес представляет также и возможность разместить на одной диаграмме несколько серий с данными (это не умеет делать sparkline). К сожалению совместить на одной картинке столбчатую диаграмму, линейную или круговую не возможно – столбчатые со столбчатыми, линейные с линейными, а круговые – вообще ни с чем. В примере ниже создаются диаграммы различных видов на основании случайных данных.
  1. header ('Content-Type: image/png');
  2. // задаем тип возвращемого документа
  3.  
  4. include "libchart/classes/libchart.php";
  5. // подключаем библиотеку libchart
  6.  
  7. // создаем какую либо диаграмму
  8. //$chart = new HorizontalBarChart(450, 250);
  9. //$chart = new VerticalBarChart(450, 250);
  10. //$chart = new VerticalBarChart(450, 250);
  11. $chart = new LineChart(450, 250);
  12.  
  13. //создаем источник данных для диаграммы в виде набора серий
  14. $dataSet = new XYSeriesDataSet();
  15. // привязываем серии к диаграмме
  16. $chart->setDataSet($dataSet);
  17.  
  18. // заполняем случайными данным диаграмму
  19. for ($i = 0; $i < 3; $i++){
  20.   $serie1 = new XYDataSet();
  21.   for ($j = 0; $j < 5; $j++)
  22.      $serie1->addPoint(new Point("User_" . $j, $i * 20 + rand(1, 10)));
  23.   $dataSet->addSerie("Line # " . $i, $serie1);
  24. }
  25. // указываем название диаграммы
  26. $chart->setTitle("Multi Series Demo");
  27. // рисуем
  28. $chart->render();
Я не зря в названии диаграммы и подписях к осям использовал латиницу – русские буквы превращаются в кракозябры – придется исправлять. Пример работы скрипта показан на рис. 2.



В следующем примере я создаю круговую диаграмму и заодно подключаю собственные шрифты:
  1. header ('Content-Type: image/png');
  2.  
  3. include "libchart/classes/libchart.php";
  4.  
  5. $chart = new PieChart(500, 300);
  6. $dataSet = new XYDataSet();
  7. $dataSet->addPoint(new Point(iconv('windows-1251', 'utf-8', 'Дельфины'), 40));
  8. $dataSet->addPoint(new Point(iconv('windows-1251', 'utf-8', 'Касатки'), 15));
  9. $dataSet->addPoint(new Point(iconv('windows-1251', 'utf-8', 'Акулы'), 65));
  10. $chart->setDataSet($dataSet);
  11. $chart->setTitle(iconv('windows-1251', 'utf-8', 'Обитатели моря'));
  12. $chart->render();
Результат показа на рис. 3.



Код почти аналогичен прошлому примеру, разве что перед выводом текста я выполнил его преобразование из исходной кодировки windows-1251 в кодировку utf-8 с помощью функции iconv. Для отрисовки текста используются два файла шрифта размещенные в подкаталоге libchart/fonts – естественно, вы можете заменить их на другие.

Самой же интересной и сложной является библиотека graPHPite, ее домашний сайт (http://graphpite.sourceforge.net/). Кроме отрисовки уже знакомых нам столбчатых, круговых и линейных диаграмм возможно создавать комбинированные диаграммы, например: векторную диаграмму, лепестковую, логарифмическую, step, impulse, диаграммы с областями, столбчатые диаграммы с накоплением и много другое. На диаграмму можно выводить фоновое изображение, выполнять заливки градиентом, создавать линии со сглаживанием. Интересен и подход с специализированными генераторами данных привязываемых к диаграмме. Например, есть объект генератор случайных чисел по заданным параметрам, есть генератор-функция, генератор функция-вектор, генератор на основании содержимого некоторого массива. Придумана и функция фильтров позволяющих сгенерированные с помощью некоторого генератора данные перед использованием немного “подрихотовать”, например, если генератор создает значения 0,10,20… то фильтр может превратить эти значения перед выводом в 0%, 10%, 20%,… Далее я приведу пример кода создающего совмещенную диаграмму: области + столбчатую. Результат работы показан на рис. 4.


  1. include("Image/Graph.php");
  2.  
  3.  // создаем объект диаграммы с заданными размерами
  4.  $Graph =& new Image_Graph(400, 300);
  5.  
  6.  // создается область рисования внутри диаграммы
  7.  $PlotArea =& $Graph->add(new Image_Graph_PlotArea());
  8.  
  9.  // создается источник данных - случайный набор чисел в отрезке от 2 до 15 и числом 10
  10.  $DataSet =& new Image_Graph_Dataset_Random(10, 2, 15, true);
  11.  
  12.  // И создаем собственно график на основании области рисования и набора данных
  13.  $Plot =& $PlotArea->addPlot(new Image_Graph_Plot_Area($DataSet));
  14.  
  15.  // задаем цвет линии
  16.  $Plot->setLineColor(IMAGE_GRAPH_GRAY);
  17.  
  18.  // выполняем заливку синим цветом области 
  19.  $BLUE =& $Graph->newColor(IMAGE_GRAPH_BLUE, 100);
  20.  $Plot->setFillStyle($BLUE);
  21.  
  22.  // Создаем источник данных для второй диаграммы - тоже случайные данные
  23.  $DataSet2 =& new Image_Graph_Dataset_Random(8, 30, 80, false);
  24.  
  25.  // Создаем, собственно, диаграмму - столбчатую
  26.  $Plot2 =& $PlotArea->addPlot(new Image_Graph_Plot_Bar($DataSet2));
  27.  
  28.  // Задаем параметры заливки цветом
  29.  $ORANGE =& $Graph->newColor(IMAGE_GRAPH_ORANGE, 100);
  30.  $Plot2->setFillStyle($ORANGE);
  31.  
  32.  // Указываем - отображать подписи к осям
  33.  $AxisX =& $PlotArea->getAxis(IMAGE_GRAPH_AXIS_X); 
  34.  $AxisY =& $PlotArea->getAxis(IMAGE_GRAPH_AXIS_Y); 
  35.  
  36.  // и рисовать стрелочки
  37.  $AxisX->showArrow();
  38.  $AxisY->showArrow();
  39.  
  40.  // Теперь можно указать маркеры для значений графика
  41.  $Marker =& $Plot->add(new Image_Graph_Marker_Value(IMAGE_GRAPH_PCT_Y_MAX));
  42.  
  43.  // фоновый цвет будет белым
  44.  $Marker->setFillColor(IMAGE_GRAPH_RED);
  45.  
  46.  // черная граница
  47.  $Marker->setBorderColor(IMAGE_GRAPH_BLACK);
  48.  
  49.  // задаем форму маркера
  50.  $PointingMarker =& $Plot->add(new Image_Graph_Marker_Pointing_Angular(20, $Marker));
  51.  
  52.  // и привязываем маркер к графику # 1
  53.  $Plot->setMarker($PointingMarker); 
  54.  
  55.  // теперь форматируем значения текста для маркера
  56.  $Marker->setDataPreProcessor(new Image_Graph_DataPreprocessor_Formatted("%0.1f%%"));
  57.  
  58.  // возможно указать легенду диаграммы с помощью пользовательских шрифтов 
  59.  $Arial =& $Graph->addFont(new Image_Graph_Font_TTF("arial.ttf"));
  60.  
  61.  // задаются размеры шрифта
  62.  $Arial->setSize(11);
  63.  
  64.  // И выводим заданную надпись заданным шрифтом, незабываем также выполнить преобразование в формат utf-8
  65.  $Graph->add(new Image_Graph_Title(iconv("windows-1251", "utf-8", "Пример диаграммы с областями и столбиками"), $Arial));
  66.  
  67.  // строим график и отправляем его клиенту
  68.  $Graph->done();
Возможности библиотеки безграничны. Перспективы – неясны. Последняя версия 1.2.1 была выпущена еще пару лет назад и какого-то целенаправленного развития я не заметил, с другой стороны я сделал на основе этой библиотеки большой проект, багов не обнаружил и крайне доволен.