Графики и диаграммы на веб-страницах. Часть 3

August 31, 2009Comments Off on Графики и диаграммы на веб-страницах. Часть 3

Согласно своему определению диаграмма (от греч. diagramma — изображение, рисунок, чертеж) - это графическое изображение, наглядно показывающее соотношение каких-либо величин. Прошлые две статьи серии были посвящены классическим видам диаграмм, т.е. линейным и столбчатым. Сегодня же я начну знакомство с парой javascript библиотек, служащих не для показа соотношения каких-либо величин, а для отображения их структуры, связей и взаимодействия.

Из приведенного выше описания первое, что приходит на ум, – это изображения всевозможных “сетей”, деревьев и графов. Не является слишком сложным придумать возможные сферы их применения: начиная от изображения генеалогических деревьев, производственных циклов или отношений в коллективе. Ох, как мне сразу вспомнились милые студенческие годы, когда мы на занятиях по психологии рисовали всевозможные схемы разбиения коллектива на подгруппы и взаимоотношения в виде стрелочек, как между этими подгруппами, так и внутри их. В общем, придумать еще несколько возможных сфер применения я оставляю вам, а сам же сосредоточусь на техническом описании того, какие javascript-библиотеки умеют рисовать “сети”, графы и деревья, а также то, как ими пользоваться. В плане рассказ об двух достаточно популярных библиотеках: JIT или JavaScript Information Visualization Toolkit, а также MooWheel.

MooWheel (домашний сайт проекта http://www.unwieldy.net/projects/moowheel/) служит для рисования на html-страницах “круга отношений”. Визуально это выглядит как множество расположенных вдоль линии круга объектов (надписей), соединенных между собой линиями. Для лучшего восприятия при наведении пользователем мыши на какой-либо из элементов, выполняется подсветка как самого элемента, так и всех исходящих из него линий-связей. Как это выглядит можно увидеть на рис. 1.



MooWheel – это javascript библиотека, построенная поверху другой популярной javascript-библиотеки mootools. Mootools – представляет собой универсальный набор функций, служащих для манипуляций над деревом DOM html-странички, поиска html-элементов по специальным условиям-фильтрам, применения к ним визуальных стилей, анимации и т.д. К слову, рассмотренная в прошлой статье библиотека flot также являлась надстройкой над другой популярной javascript библиотекой– jquery. И jquery и mootols являются библиотеками общего назначения и содержат сходный набор функционала. Я не хочу углубляться в рассказ о том, что такое mootools т.к. это не помещает и не поможет работать с MooWheel. А если все же Mootools вас заинтересует, то прошу обращаться к домашнему сайту mootools - http://mootools.net/. Технически, MooWheel рисует изображение с помощью canvas. А в том случае если у посетителя сайта браузер не поддерживает эту функциональность (microsoft IE), то используется имитация canvas с помощью vml, т.е. все устроено также как и в flot. После того как вы скачали и распаковали архив библиотеки с сайта http://www.unwieldy.net/projects/moowheel/, то в вашем распоряжении кроме самой библиотеки moowheel.js (размером 16 кб) и файла excanvas.js, имитирующего canvas через vml (23 кб), будут еще и файлы библиотеки mootools (100 кб). Все описанные выше скрипты представлены не в сжатом виде, так что суммарный размер файлов подключаемых к html-странице можно еще значительно уменьшить. Естественно, если вы на своем сайте уже используете библиотеку mootools с версией отличной от идущей в поставке с moowheel (1.2), то никаких проблем это не вызывает. Итак, первым шагом мы подключаем к html-страничке все упомянутые выше библиотеки, затем создаем внутри страницы специальный блок “пустышку”, содержимое которого будет вскоре заменено на круговую диаграмму (в строгом понимании этого слова, MooWheels рисует не диаграмму, но я буду придерживаться этого обозначения для простоты):
  1. <html> 
  2.   <head>
  3.      <script type="text/javascript" src="mootools-1.2-core-nc.js"></script>
  4.      <script type="text/javascript" src="mootools-1.2-more.js.js"></script>
  5.      <script type="text/javascript" src="excanvas.js"></script>
  6.      <script type="text/javascript" src="canvastext.js"></script>
  7.      <script type="text/javascript" src="moowheel.js"></script>
  8.      <script type="text/javascript">
  9.            window.addEvent('domready', doMooWheel);
  10.           // делаем какую-то работу
  11.           function doMooWheel() {
  12.           }
  13.      </script> 
  14.   </head>
  15.  <body>
  16.   <div id="placeholder"> заготовка </div>
  17.  </body> 
  18. </html>
Как видите, сразу после того как страница, точнее ее дерево dom, будет загружена, то сработает вызов функции “doMooWheel”. Задача которой - подготовить массив с данными, а затем подать и данные, и специальный конфигурационный объект с настройками внешнего вида диаграммы внутрь класса MooWheel. Что касается данных – то они представляют собой массив объектов, каждый из которых описывает отдельный узел круговой диаграммы и соединяющие их линии:
  1. function doMooWheel() {
  2.  var config = {};
  3.  var data = [
  4.    {id:'cloud', text: "Cloud",  'connections': ['ball'] },
  5.    {id:'ball', text: "Ball", 'connections': ['tower'] },
  6.    {id:'tower', text: "Tower",  'connections': ['cloud', 'spanner'] },
  7.    {id:'spaner', text: "Spanner", 'connections': ['ball'] },
  8.    {id:'flatirion', text: "Flatiron",   'connections': ['spanner'] }
  9.   ]; 
  10.   // и строим диаграмму
  11.   var w = new MooWheel(data, $('placeholder'), config);
  12. };
Как видите устройство каждого из элементов диаграммы тривиально: у каждого элемента есть уникальный идентификатор (свойство “id”) затем, собственно, текст надписи (свойство “text”) и массив с идентификаторами тех узлов, с которыми мы хотим соединить текущий (“connections”). То, что у меня получилось, показано на рис. 1. Первое улучшение рисунка в том, чтобы добавить к каждой надписи небольшую картинку-иконку. Для этого нужно создать объект Image (стандартный javascript объект, представляющий собой картинку), затем установить значение свойства “src”, т.е. указать местоположение файла с изображением. И, финальный аккорд, передать ссылку на изображение как свойство “image” внутрь массива с данными для круговой диаграммы. То, что у меня получилось, показано на рис. 2.
  1. var img_1 = new Image();
  2. // создали изображение и указали путь к нему
  3. img_1.src = “http://study-and-dev.com/jdiagrams/moo/pics/pic_1.png”;
  4. // и теперь сошлемся на это изображение
  5. var data = [
  6.    {id:'cloud', text: "Cloud",  'connections': ['ball'] , image: img_1},
  7.  ... и все как обычно …
В том случае, если данные для построения рисунка данные не представлены явно внутри javascript/html, а загружаются динамически с сервера с помощью ajax, то следует использовать не класс “MooWheels”, а “MooWheels.Remote”.
  1. new MooWheel.Remote(false, $('placeholder'), 
  2.   { url: 'http://my-site.com/script.php }
  3. });


Массив с данными, которые формирует php-скрипт, должен иметь точно такую структуру, как и массив “data” в первых двух примерах. Очевидно, что потребуется замена свойству “image”. Так для представления пути к картинке теперь используют “imageUrl” – и больше никаких отличий. Что касается управления внешним видом рисунка, то здесь MooWheel не на высоте: во-первых вы можете менять цвет и ширину линий соединяющих точки на рисунке, можно включать и выключать подсветку линий соединений при наведении мыши на какой-либо из узлов рисунка и … и все. Из того, что мне в практике потребовалось, но отсутствовало в MooWheel – это возможность управлять размером и гарнитурой шрифта для вывода подписей, возможности масштабировать изображение или отдельные его элементы (все эти функции отлично реализуются с помощью прямого доступа к тегу canvas). В любом случае, рассматривать MooWheel как “готовое к использованию” решение трудно, поскольку приходится дорабатывать исходный код библиотеки руками (к счастью, написан код удобочитаемо и понятно). На этом про MooWheel все, а мы переходим к другой, более сложной и полезной библиотеке – jit (JavaScript Information Visualization Toolkit).

JIT специализируется на профессиональном отображении таких структур данных как графы и деревья. JIT представляет не только широкий набор конкретных стратегий визуализации: RGraph, HyperTree, SpaceTree, TreeMap, но и возможность придать рисунку интерактивность. Также вы можете настроить внешний вид и узлов и соединяющих их линий практически до “последнего пикселя”. Домашний сайт проекта размещен по адресу http://thejit.org, там же можете скачать исходный код библиотеки (70 кб) и файлы с примерами использования и справочную документацию (это обязательно, т.к. разобраться во всем множество функционала JIT без тщательного штудирования справки по api очень тяжело). Начало работы с jit практически не отличается от того как мы работали с moowheel: все также в самом начале html-страницы нужно подключить библиотеки jit и создать html-блок “пустышку” в котором вскоре разместится изображение настоящего дерева:
  1. <html>
  2.   <head>
  3.       <!--[if IE]><script language="javascript" type="text/javascript" src="Extras/excanvas.js"></script><![endif]-->
  4.       <script language="javascript" type="text/javascript" src="jit.js"></script>
  5.   </head>
  6.   <body>
  7.      <div id="placeholder"></div>
  8.   <body>
  9. </html>
Теперь нужно написать код, который после загрузки страницы (события “domReady”) будет конструировать массив с данными (очевидно, что данные зависят от того какой вид графа или дерева мы будем строить). В следующем примере я решил нарисовать HyperTree. HyperTree – это дерево, т.е. набор узлов, связанных между собой иерархическими отношениями в одном направлении и без циклов. Например, информация о структуре некоторой организации, составляющих ее отделах, подотделах и сотрудниках (вообще-то, в практике реальна ситуация, когда один и тот же сотрудник может относится к нескольким отделам) может быть отлично представлена в виде HyperTree:
  1. var data = {
  2.     id: 'organization', name: 'Organization', 
  3.     data : {url: 'http://site.com', foundation: "1.4.1990"}, 
  4.     children : [
  5.        {
  6.          id: 'managers', name: 'Managers',
  7.          children: [
  8.           {id: "jim", name: "Jim Tapkin", children: []},
  9.           {id: "pet", name: "Pet Maslov", children: []},
  10.          ]
  11.        },
  12.        {
  13.          id: 'security', name: 'Security', 
  14.          children: [
  15.           {id: "ivan", name: "Ivan Dolvich", children: []},
  16.           {id: "igor", name: "Igor Dolvich", children: []},
  17.          ]
  18.        }
  19.     ]
  20. };
Как видите, данные для HyperTree имеют древовидную структуру: на верхнем уровне (корнем дерева) находится элемент “organization”, в которую входят два отдела “managers” и “security”. Отделы, в свою очередь, заполнены конкретными сотрудниками. Каждый элемент дерева обязан иметь как минимум идентификатор “id” и название “name”. Также если элемент не является конечным (не является “листом”), то у него может быть произвольное количество дочерних элементов; и все они находятся внутри массива “children”. В примере, корневой элемент “organization” также содержит свойство “data” с какими-то не понятными характеристиками “url” и “foundation” (дата основания). Дело в том, что jit построен по принципам MVC (Model View Controller) т.е. отделяет информацию, привязанную к узлу “data”, от ее внешнего вида и поведения. Фактически, мы можем, использую информацию, хранящуюся в “data”, реализовать свой уникальный внешний вид диаграммы: настроить то, как выглядит и ведет себя каждый узел по отдельности. Следующий этап – это вызов конструктора HyperTree, перед которым нужно подготовить объект Canvas т.е. “холст”, на котором мы вскоре будем рисовать.
  1. var canvas = new Canvas('htree', { injectInto: 'placeholder', width: 640, height: 480});
Здесь все очевидно: свойство “injectTo” указывает внутрь какого html-элемента будет помещен “холст” для рисования и его линейные размеры.
  1. window.ht = new Hypertree(canvas, {
  2.   Node: { type: "circle", dim: 9, color: "#ff0000" },
  3.   Edge: { lineWidth: 2, color: "#00ff00" },
  4.   duration: 1500,
  5.   transition: Trans.Quart.easeInOut,
  6.   onCreateLabel: funOnCreateLabel ,
  7.   onPlaceLabel: funOnPlaceLabel
  8. });
  9.  
  10. // теперь выполняем загрузку данных и отрисовку дерева
  11. ht.loadJSON(data);
  12. ht.refresh();
Первый параметр конструктора класса HyperTree сложностей не вызывает, а вот второй, отвечающий за внешний вид рисунка, содержит много нового и неочевидного. Прежде всего, мы настраиваем внешний вид узлов дерева “Node”. В примере я решил, что узлы дерева будут отображены в форме кружка “circle” (также есть варианты “none”, “square”, “rectangle”, “circle”, “triangle”, “star”). Параметр “dim” задает размер узла дерева, а “color”, очевидно, его цвет. Для настройки внешнего вида ребер, соединяющих узлы дерева, используем свойство “Edge”. Так я решил сделать толщину линий равной 2 пикселям и зеленого цвета. Параметры “duration” и “transition” управляют анимацией HyperTree. Дело в том, что способности человека к восприятию больших объемов информации ограничены и желательно, чтобы одновременно в поле внимания попали только те узлы дерева, которые сгруппированы вокруг центрального (вначале это корень дерева). А остальные узлы дерева будут спрятаны где-то “вдали”. Но если пользователь выполняет клик по какому-то узлу дерева расположенному “на периферии”, то этот узел становится центральным. Т.е. он перемещается в центр рисунка и рядом с ним будут расположены и видны только те узлы, с которыми он непосредственно соединен. Итак, параметр “transition” управляет тем, какая анимация будет проиграна при смене движении узлов с периферии в центр, а то, сколько это займет времени, задается опцией “ duration”. Параметры “onCreateLabel” и “funOnPlaceLabel” ссылаются на функции, которые должен определить пользователь и внутри которых нужно детально указать как должны выглядеть узлы дерева. К примеру, моя функция funOnCreateLabel, определяет узел как обычный фрагмент текста, помещенный внутрь тега “i” (а ведь можно было назначить каждому узлу и индивидуальную картинку). Что касается привязанного к узлу дерева обработчику события “onclick”, то он вызывает тот самый, описанный выше метод центрирования изображения дерева на элементе, по которому был выполнен клик.
  1. function funOnCreateLabel(domElement, node) {
  2.   domElement.innerHTML = '<i>' + node.name + '</i>';
  3.   domElement.onclick = function () {  
  4.      window.ht.onClick(node.id); 
  5.   };
  6. }
Очевидно, что в ходе перемещения узлов необходимо изменять не только их координаты (забота JIT), но и параметры внешнего вида: те же размеры шрифта и цвет. Так функция funOnPlaceLabel в зависимости от того на какой “глубине” находится узел, задает различным его размер шрифта. Если узел является центральным, т.е. по нему был выполнен клик, то его глубина равна нулю, а надпись для узла будет выведена жирным шрифтом размером в 16px. Узлы с “глубиной” 1 – это те узлы, с которыми центральный узел соединен напрямую, и для них размер шрифта будет поменьше – 14px. Узлы же с глубиной 2 и более нужно вообще спрятать:
  1. function funOnPlaceLabel(domElement, node) {
  2.   var style = domElement.style;
  3.   style.display = '';
  4.   style.cursor = 'pointer';
  5.   if (node._depth == 0) {
  6.      style.fontSize = "16px";
  7.      style.fontWeight = 'bold';
  8.      style.color = "#ff0000";
  9.   }
  10.   else if (node._depth == 1) {
  11.     style.fontSize = "14px";
  12.     style.fontWeight = 'bold';
  13.     style.color = "#aa0000";
  14.   } else {
  15.     style.display = 'none';
  16.   }
  17.   var left = parseInt(style.left);
  18.   var w = domElement.offsetWidth;
  19.   style.left = (left - w / 2) + 'px';
  20. }
То, что у меня получилось, показано на рис. 3.



Однако статическая картинка не дает в полной мере возможность оценить результат и “поиграть” с построенным деревом, так что я рекомендую обратить ваше внимание на примеры HyperTree, идущие в поставке с JIT.