Легко и просто: графики и диаграммы на веб-страницах. Часть 4

Эта статья завершит рассказ о методиках внедрения в html-страницы графиков, диаграмм, а также рассказ о javascript-библиотеках, умеющих “рисовать” красивые и интерактивные изображения таких структур данных, как графы и деревья. В прошлой статье я начал рассказ об одной из самых лучших библиотек подобного плана – jit.

Первый пример, который мы сделали с помощью jit, демонстрировал методику построения Hypertree. То есть дерева, размер и расположение узлов которого упорядочены специально таким образом, чтобы акцентировать внимание пользователя на центральном узле дерева или на том узле, который был выделен пользователем. Несмотря на свое название, HyperTree умеет отображать еще и “настоящие” графы, а не только деревья. Сегодня я расскажу об оставшихся в “арсенале” JIT компонентах: RGraph, SpaceTree и TreeMap. Но прежде чем мы перейдем к рассмотрению “красивостей”, я хотел бы уделить пару слов классу TreeUtil. Из названия ясно, что класс носит вспомогательный характер и используется для выполнения неких “вычислительных работ” над данными, образующими дерево. И действительно, вот, к примеру, набор методов, которые входят в состав класса TreeUtil: prune, getParent, getSubtree, getLeaves, eachLevel, each, loadSubtrees. Чтобы понять, какую работу выполняет первый метод “prune”, нужно вспомнить определение уровня, на котором находится каждый из узлов дерева. Предположим, что у нас есть следующая иерархия: в качестве корня дерева будет узел “food”, из которого отходят две ветви “fruits” и “vegetables”, из которых, в свою очередь, произрастают уже конкретные наименования фруктов и овощей, например, “apples”, “oranges”, “potato”. Так вот, корневой узел имеет номер уровня, равный нулю, затем узлы “oranges” и “vegetables” имеют уровень, равный единице. Уровень номер два соответствует узлам дерева с названиями овощей и фруктов. Метод prune принимает на вход два параметра: массив с данными для дерева и число. Число – это номер уровня, после которого все узлы дерева будут удалены. В результате выполнения следующего кода из набора данных будут удалены все узлы, расположенные на уровне 2 и ниже (т.е. конкретные названия фруктов):
var data = {id: 'food', name: 'Food’, children : [
{id: ‘fruits’, name: 'Fruits', children: [
{id: "apples", name: "Apples", children: []},
{id: "oranges", name: "Oranges", children: []}
]}
] };
TreeUtil.prune (data, 1);

Еще из функций в арсенале TreeUtils можно назвать getParent, getSubtree, getLeaves, eachLevel и each. Все эти функции как первый параметр получают ссылку на дерево, а остальные параметры специфичны для каждой конкретной ситуации. Так, первая функция getParent получает в качестве второго параметра идентификатор узла дерева, а возвращает ссылку на его родительский узел. Функция getSubtree также получает идентификатор любого из узлов дерева, а как результат возвращается все поддерево, “выросшее” из указанного узла. Функция getLeaves возвращает массив всех узлов-“листьев” (т.е. узлов дерева, у которых нет дочерних узлов), расположенных выше maxLevel – второго параметра функции. Функции eachLevel и each схожи: обе позволяют выполнить обход всех узлов дерева и вызвать для каждого из них пользовательскую функцию. Например, в следующем коде я хочу найти все узлы дерева в отрезке от нулевого уровня до второго и вывести на экран их идентификаторы и номер уровня, на котором узел расположен:
TreeUtil.eachLevel(tree, 0, 2, function(node, level) {
alert(‘id = ’ + node.id + ' level = ' + level);
});
// и второй пример
TreeUtil.each(tree, function(node) {
alert(‘id = ’ + node.id);
});

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

Теперь мы вернемся к рассмотрению графических компонентов JIT и поговорим о SpaceTree. Как выглядит дерево, отображаемое с помощью SpaceTree, показано на рис. 1 и рис. 2. Давайте начнем с небольшого примера построения SpaceTree и разберем его по шагам. Первый шаг – это, конечно, подготовка данных. Данные для SpaceTree ничем не отличаются от данных, которые были использованы для показанного в прошлой статье примера с HyperTree:
var data =
{id: 'organization', name: 'Organization', children : [
{id: 'managers', name: 'Managers', children: [
{id: "jim", name: "Jim Tapkin", children: []},
]},
{id: 'security', name: 'Security', children: [
{id: "ivan", name: "Ivan Dolvich", children: []},
{id: "igor", name: "Igor Dolvich", children: []},
]},
{id: 'planning', name: 'Planning', children: [
{id: "tim", name: "Tim Timur", children: []},
{id: "chen", name: "Li Chen", children: []},
]} ]};

Теперь нужно создать объект “холст” или canvas, на котором я буду рисовать диаграмму. Естественно, что в тексте самой html-странички должен присутствовать div-блок с идентификатором “placeholder”, так чтобы jit знал, внутрь какого из html-элементов страницы нужно поместить будущий рисунок:
var canvas = new Canvas('placeholder', {
'injectInto': 'placeholder',
'width': 1024,
'height': 768 });

Теперь дело осталось за, собственно, созданием компонента SpaceTree. Параметры его конструктора очевидны: ссылка на “холст” для рисования и объект с конфигурационными переменными, управляющим внешним видом рисунка. Что касается настроек внешнего вида рисунка, то их названия говорят за себя. Первые две опции — “duration” и “translation” — задают анимацию, срабатывающую при клике пользователя по любому из узлов дерева, а также время, в течение которого будет “проиграна” анимация. Параметр “orientation” управляет ориентацией дерева и принимает значения left, top, bottom, right. То, как будет выглядеть в этих случаях SpaceTree, можно увидеть на рис. 2. Для того чтобы понять, на что влияет параметр levelDistance, внимательно рассмотрите рис. 1. Если на рисунке отображается сразу много узлов, то нужен тонкий контроль за расстоянием между узлами дерева, так чтобы они не наползали друг на друга. Для настройки расстояния по горизонтали между узлом и его непосредственными дочерними узлами используется levelDistance. Для управления расстоянием между узлами деревам по вертикали используется параметр siblingOffset. Еще из полезных, хоть и не показанных в примере настроек, есть levelsToShow. Согласитесь, что если дерево большое, то показать сразу все узлы невозможно. Итак, levelsToShow (по умолчанию равное двум) управляет тем, сколько уровней дерева будет показано на рисунке одновременно. В том случае, если вы не хотите отобразить на рисунке подписи к узлам дерева, то можно указать параметр “withLabels”, равным false. Остальные настройки управляют тем, как будут выглядеть узлы дерева (свойства, объединенные под названием Node) и соединяющие их дуги (Edge). Внешний вид узла дерева (форма) задается параметром type, который принимает значения circle, rectangle, ellipse, square. Также можно отключить отображение узла, если задать для type значение, равное none. Размеры узла определяются совокупностью значений width, height и dim (радиус круга). Фоновый цвет и толщина линий, которыми “рисуется” узел дерева, зависят от переменных lineWidth и color. Последний параметр overridable, будучи включенным, позволяет задавать для каждого из узлов дерева индивидуальный внешний вид. Как это сделать, можно легко найти в примерах, размещенных на официальном сайте jit ( сайт ). Что касается настроек внешнего вида линий, соединяющих узлы дерева, то я сделал их красного цвета в пять пикселей толщиной. Параметр же type служит для управления типом линии и принимает значения "none", "line", "quadratic:begin", "quadratic:end", "bezier" и "arrow". Затем для каждого из узлов дерева я захотел расположить рядышком текстовую надпись. Настройка внешнего вида надписи делегирует функции обрабатывающему событие “onCreateLabel”. Здесь моя функция должна назначить для подписи стилевое оформление (css-класс ‘labelClass’), а также сам текст подписи. Для того чтобы рисунок SpaceTree был интерактивным, я назначил каждой из надписей функцию, “ловящую” click пользователя по надписи и выполняющую центрирование дерева вокруг выделенного пользователем узла дерева. После клика на любом из узлов SpaceTree пересчитает новое расположение узлов дерева так, чтобы выбранный пользователем узел находился в центре экрана и были видны только соседние с ним узлы.
var st = new ST(canvas, {
duration: 800,
transition: Trans.Quart.easeInOut,
orientation: 'top',
levelDistance: 150,
Node: {
width: 100, height: 100, dim: 20,
type: 'circle',
color: '#00ff00',
overridable: false
},
Edge: {
type: 'bezier',
overridable: false,
lineWidth: 5,
color: '#ff0000'
},
onCreateLabel: function(label, node){
label.id = node.id;
label.innerHTML = node.name;
label.onclick = function(){
st.onClick(node.id);
};
var style = label.className="labelClass";
}, });

Завершающий штрих – это с помощью метода loadJSON загрузить внутрь SpaceTree подготовленные на первом шаге данные. Затем я выполняю “расчет” (compute) месторасположения узлов дерева. А последний вызов метода “onClick” привел к имитации выделения пользователем корня дерева. То есть на экране мы увидим лишь корень дерева и те узлы, что непосредственно связаны с ним.
st.loadJSON(data);
st.compute();
st.onClick(st.root);

То, что у меня получилось, показано на рис. 1. Конечно, показать на статической картинке всю красоту анимации изменения рисунка дерева при “кликах” по его узлам невозможно, так что советую обратиться распложенным на сайте jit ( сайт примерам и “поиграть” с ними. Там же можно найти хороший пример работы с SpaceTree, где показывается то, как можно динамически добавлять новые и удалять узлы дерева, например, по клику по ним.

На этом про компонент SpaceTree все, а мы переходим к рассмотрению следующего средства визуализации – Rgraph. Сразу скажу: то, что у нас должно получиться, показано на рис. 3. Как видите, эта структура данных отлично подходит для радиальных деревьев, т.е. деревьев, узлы которых расположены вдоль концентрических окружностей с центром, совпадающим с расположением корневого элемента. Для следующего примера я решил воспользоваться набором данных из предыдущего примера, а вот конструирование canvas, или “холста” для рисования уже имеет небольшие особенности.

var canvas = new Canvas('placeholder ', {
injectInto: 'placeholder',
width:800,
height: 800,
backgroundCanvas: {
styles: {strokeStyle: 'red', lineWidth: 2 },
impl: {
init: function(){},
plot: function(canvas, ctx){
var times = 3, d = 100;
var pi2 = Math.PI * 2;
for (var i = 1; i <<<= times; i++) {
ctx.beginPath();
ctx.arc(0, 0, i * d, 0, pi2, true);
ctx.stroke();
ctx.closePath();
}
}
} } } );

Дело в том, что компонент RGraph не берет на себя заботу автоматически нарисовать набор концентрических окружностей – все это мы должны сделать сами внутри объекта canvas. Так, для рисования линий я выбрал красный цвет (strokeStyle), затем толщину линий в два пикселя (lineWidth). Само же рисование реализовано внутри функции plot: там я организовал цикл на три повторения, каждый из которых рисует окружность радиусом 100, 200, 300 пикселей. Обратите внимание на наличие функции init, хоть она сама ничего не делает, но ее присутствие обязательно. Следующий шаг – это создание объекта RGraph. Точно так же, как это уже делалось раньше, я вызываю конструктор RGraph, передав ему ссылку на объект “холст” и специальный объект с настройками внешнего вида рисунка. К счастью, 90% всех переменных, управляющих внешним видом диаграмм, совпадают и для HyperTree, и для RGraph, и для SpaceTree. То есть вы можете смело использовать для управления внешним видом и узлов и дуг Rgraph те опции, что я использовал для примера с SpaceTree. Что касается “чудесного” совпадения нарисованных окружностей на фоне диаграммы (с радиусом 100px) с расположением узлов, то здесь все дело в том, что для управления расстоянием, с которым будут располагаться узлы R-дерева, используется все та же знакомая нам опция levelDistance, значение которой по умолчанию как раз и равно ста. Хотя я не настроил в примере ни одной из опций, связанных с анимацией преобразования дерева, но Rgraph по умолчанию поддерживает функцию, так чтобы по “клику” на любом из узлов дерева он перемещался бы в центр. И снова: для того чтобы этот механизм “включить”, мне пришлось переопределить функцию onCreateLabel, внутри которой формируется html-код текстовой надписи для каждого из узлов дерева.
var rgraph = new RGraph(canvas, {
Node: {
color: 'black',
width: 15, height: 15, type: 'rectangle'},
Edge: {
color: 'black' },
onCreateLabel: function(elt, node){
elt.innerHTML = node.name;
elt.onclick = function(){
rgraph.onClick(node.id);
};
},
onPlaceLabel: function(domElement, node){
var style = domElement.style;
style.display = '';
style.cursor = 'pointer';
if (node._depth <<<= 1) {
style.fontSize = "1.4em";
style.color = "black";
} else if(node._depth == 2){
style.fontSize = "1.7em";
style.color = "black";
} else
style.display = 'none';
var left = parseInt(style.left);
var w = domElement.offsetWidth;
style.left = (left - w / 2) + 'px';
} });

Единственное, что представляет хоть какую-то сложность в примере выше – это код функции onPlaceLabel. Вызывается эта функция всякий раз, когда расположение узлов R-дерева меняется. Я не хочу показывать все узлы одновременно, а значит, должен проверить для каждого из узлов, каково его значение “глубины” относительно текущего активного узла (или корня дерева). Затем я спрячу все узлы, удаленные от центра более чем на два уровня. Что касается подписей узлов, расположенных на уровне ноль и один, то я задаю им размер шрифта немного больший, чем размер шрифта для узлов, расположенных дальше от центра. Завершающий штрих – загрузить внутрь Rgraph данные и инициировать процесс рисования диаграммы: rgraph.loadJSON(data);
rgraph.refresh();

Последний из компонентов jit – это Treemap, показанный на рис. 4. Эта структура данных идеально подходит для того, чтобы в ограниченном пространстве одновременно отобразить сведения о большом количестве элементов, связанных отношениями подчинения. Так, если какой-то узел дерева является дочерним по отношению к другому узлу, то и выражается это в том, что в прямоугольник родительского узла будут помещены все дочерние узлы. Надо сказать, что TreeMap стоит особняком от рассмотренных ранее HyperTree, SpaceTree и RGraph, т.к. для того, чтобы нарисовать диаграмму, TreeMap не использует canvas, а формирует множество тегов div, многократно вложенных друг в друга. Честно говоря, я в своей практике ни разу не сталкивался с необходимостью использования TreeMap, т.к. за счет “игры” с настройками других компонентов jit можно добиться отображения одновременно желаемого количества компонентов на экране. A если вам хочется показать информацию именно в таком стиле, как это делает TreeMap, то это проще сделать самому “руками”, сформировав набор вложенных друг в друга блоков div. Плюс в том, что вы получаете полный контроль за расположением элементов, наличием между ними отступов. Так, при использовании TreeMap тяжело определить действительную последовательность вложенных узлов-элементов, если количество уровней больше трех.

Естественно, что доступных библиотек, позволяющих внедрять в html-страницы графики, диаграммы, изображения деревьев и графов, очень много. И зачастую они содержат набор функций более совершенный, чем описанные мною flot, moowheel, jit. Как оправдание я могу сказать, что рассмотренный мною набор библиотек покрывает 90% потребностей начинающего javascript-разработчика и при всем этом остается простым в использовании.

black-zorro@tut.by black-zorro.com


Компьютерная газета. Статья была опубликована в номере 32 за 2009 год в рубрике программирование

©1997-2022 Компьютерная газета