Статьи

Как создать Performant, основанные на шаблонах диаграммы с помощью Paths.js

В недавнем посте мы обсудили современное состояние визуализации данных в браузере, в частности, в библиотеках SVG. Там мы сосредоточились на Snap.svg , но мы представили Paths.js как жизнеспособную альтернативу — используемую вместе с механизмом шаблонов или библиотекой привязки данных. Чтобы быть справедливым, Paths способен на гораздо большее, как мы собираемся показать в следующих разделах, с реальными вариантами использования, которые помогут вам разобраться в его логике.

Вступление

Основная цель Paths — помочь внешним разработчикам создавать SVG-пути с лучшей производительностью через интуитивно понятный интерфейс. Как следует из названия, несмотря на наличие примитивов для таких форм, как rect или circle , все можно свести к контурам. Этот подход объединяет различные диаграммы, предоставляя согласованный интерфейс, где команды рисования всегда возвращают список путей, готовых для рисования. Заменив статические движки шаблонов (такие как Mustache или Handlebars) библиотеками привязки данных, такими как Ractive.js , Angular или React , вы даже можете получить анимированную графику бесплатно.

Вероятно, лучшая вещь в Paths.js — это то, что он предлагает три дополнительных API с возрастающим уровнем абстракции. Самый низкий уровень — это цепочечный API, который генерирует произвольный путь SVG. Кроме того, определены пути для простых геометрических фигур, таких как многоугольники или секторы круга. API самого высокого уровня позволяет создавать некоторые простые графики, которые могут быть снабжены набором данных. (Проверьте это демо, чтобы увидеть доступные примитивы.)

На самом деле, обратите внимание на то, что: Лучшая особенность Paths заключается в том, что вы можете использовать библиотеку на стороне сервера с Node.js, поскольку она напрямую не зависит от какой-либо библиотеки 1 . Таким образом, вы можете перенести создание структуры диаграммы и деталей на сервер. Помимо ускорения работы приложений, вы можете полностью исключить отправку необработанных данных клиенту, экономя время ожидания и уменьшая объем информации, которой вы делитесь с клиентами.

Почему Пути?

Наибольшим преимуществом использования Paths.js вместо, скажем, D3 или Snap.svg, является то, что последние являются обязательными, в то время как Paths по своей природе поддерживает декларативное программирование в сочетании с механизмами шаблонов или (даже лучше) средами привязки данных.

Использование Paths с такими фреймворками, как Ractive или React, вызывает, в свою очередь, еще одно преимущество. Эти структуры, на самом деле, используют определенные оптимизации, чтобы уменьшить количество повторений и перерисовок, необходимых каждый раз, когда необходимо изменить DOM; они сохраняют «теневые» копии DOM, для которых выполняют обновление в своего рода «пакетном режиме», и, наконец, обновляют реальный DOM с наименьшим количеством возможных изменений.

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

Наконец, Paths является легким и модульным: вы можете загружать только те компоненты, которые вам действительно нужны, фокусируясь на графиках или просто манипуляции SVG. Paths — одна из тех библиотек, которые фокусируются на нескольких вещах, пытаясь их оптимизировать. В общем, вы можете объединить несколько таких библиотек для выполнения сложных задач. В D3, с другой стороны, есть много дополнительных служебных методов — это прекрасно, если они вам нужны, поскольку у вас есть все, что вам нужно, в одном месте, но немного тяжело, если вы этого не делаете.

Использование путей

Как уже упоминалось, вы можете использовать пути с Node.js или в браузере. В последнем случае вы можете загрузить его как модули AMD или как отдельную библиотеку.

Пути на узле

Если вы хотите использовать его на сервере, сначала установите его, набрав эту команду на консоли (при условии, что вы правильно установили узел и он находится в глобальном пути):

npm install paths-js

После установки вы можете загрузить отдельные модули:

 var Pie = require('paths-js/pie'); 

Пути в браузере: модули AMD

Paths.js распространяется вместе с Bower , и вы можете установить его из командной строки:

bower install paths-js

Или, конечно, просто загрузите его вручную из репозитория на GitHub.

Paths структурирован в различные модули AMD и может быть загружен с помощью загрузчиков модулей AMD. Используя RequireJS (при условии, что вы установили пути с Bower), вы можете настроить его следующим образом:

 require.config({ 'paths': 'components/paths-js/dist/amd' }); 

Фактический путь будет зависеть от вашей конфигурации Bower или, для загрузки вручную, от структуры ваших папок. (Будьте осторожны, когда размещаете папку amd указанную выше.)

После правильной настройки вы можете легко потребовать отдельные модули:

 var Pie = require('paths/pie'); 

Пути в браузере: автономный скрипт

Если вы предпочитаете избегать модулей AMD, вы можете смело включать Paths как отдельный скрипт: вам нужен файл dist/global/paths.js . Как только он будет включен в вашу страницу, объект paths будет доступен в глобальной области видимости, так что отдельные модули будут доступны как paths.Pie , paths.Polygon и так далее. Помимо многословия, вы теряете возможность импортировать только те модули, которые вам нужны, но если вам нужно много из них, это окажет незначительное влияние.

API низкого уровня

Как упоминалось, целью API самого низкого уровня является создание путей. Создание целей так же просто, как вызов одного конструктора: Path() . Весь API является цепным, так что вы можете создать сложный путь, вызывая методы в результате предыдущих вызовов. Объекты Path предлагают методы для постепенного расширения текущего пути; сохраняется ссылка на последнюю точку пути, и из этой точки можно добавлять линии или кривые, имитируя синтаксис SVG для путей. Основные методы, которые вам понадобятся:

  1. moveto(x, y) : перемещает курсор к пройденным координатам.
  2. lineto(x, y) : рисует линию от конца пути к этим координатам.
  3. curveto(x1, y1, x2, y2, x, y) : рисует кубическую кривую Безье из текущей точки в (x, y), используя (x1, y1) в качестве контрольной точки в начале кривой и (x2, у2) в качестве контрольной точки в конце кривой.
  4. smoothcurveto(x2, y2, x, y) : рисует кубическую кривую Безье из текущей точки в (x, y), неявно вычисляя первую контрольную точку на основе второй и предыдущей команды (если есть).
  5. arc('rx', 'ry', 'xrot', 'large_arc_flag', 'sweep_flag', 'x', 'y') : рисует эллиптическую дугу из текущей точки в (x, y), управляя радиусами эллипса и вращение через другие параметры.
  6. closepath() : закрывает путь, превращая его в многоугольник.

Все доступные методы также поддерживают «подробный» API, так что именованные параметры (в форме объекта конфигурации) можно без проблем передавать каждому из них. Например, moveto выше метод moveto может называться Paths().moveto({x: 10, y: 3}) или Paths().moveto(10, 3) . Названия параметров соответствуют спецификации SVG.

Доступно больше методов, и в общем случае есть однозначное соответствие командам SVG Paths . Например, qcurveto(x1, y1, x, y) и smoothqcurveto(x, y) являются аналогом smoothcurveto для smoothcurveto для квадратичных кривых.

В целом, это работает лучше с движком шаблонов, но это не является строго обязательным. Вы можете использовать Path с императивным стилем, как показано в следующих примерах. Однако это не лучшая практика:

 <title>PathsJs test</title> <style type="text/css"> .ocean { fill: blue; } </style> <svg width="640px" height="480px"><path id="testpath" class="ocean"></path></svg> <script type="text/javascript" src="lib/paths.js"></script> 
 var Path = require('paths/path'); var path = Path() .moveto(10, 20) .lineto(30, 50) .lineto(25, 28) .qcurveto(27, 30, 32, 27) .closepath(); document.getElementById("testpath").setAttribute("d", path.print()); 

Метод print() объектов Path преобразует путь, составленный в соответствующую строку данных SVG, как это будет указано в атрибуте путей d (data). Как только мы получим это значение, мы можем вручную установить соответствующий атрибут на любом пути, просто используя селекторы CSS и методы getElementById / getElementsBy* .

Конечно, использование шаблонизатора сильно повлияет на объем стандартного кода, который нам нужно написать:

 <svg width="640px" height="480px"><path d="{{ path.print() }}" fill="blue"></path></svg> 

Это устраняет необходимость вручную устанавливать атрибут d для #testpath и даже назначать идентификатор элементу пути. Это лучший практический стиль для создания рисунков SVG с помощью Paths.

API среднего уровня

Как бы он ни был силен, на практике вам редко понадобится низкоуровневый API. Причина в том, что Paths предлагает построенный на его основе API с более высоким уровнем абстракции, который позволяет напрямую создавать полигоны и фигуры с интуитивно понятным синтаксисом.

Прежде чем мы рассмотрим пример, важно понять, как работает этот API. Мы уже заявили, что все, что создается этой библиотекой, является путем. Действительно, каждый из методов API среднего уровня возвращает объект с двумя полями:

 { path: <path object=""> centroid: [<x>, <y>] } 

Поле path содержит объект Path , совместимый с низкоуровневым API, и поэтому является расширяемым: строку, описывающую атрибут данных пути, можно, как обычно, получить с помощью метода print() .

Вместо этого поле centroid формально не связано с полученным путем, но, тем не менее, оно может быть очень полезным: оно эмпирически вычисляется как точка, как-то центральная по отношению к фигуре, которую можно использовать, например, для позиционирования этикетка для формы.

Чтобы оценить разницу между двумя уровнями абстракции, мы создадим многоугольник, почти идентичный предыдущему разделу, на этот раз с использованием объекта Polygon :

 <svg width="640px" height="480px"><path id="testpath" class="ocean"></path><path id="testpath2" class="ocean" transform="translate(100)"></path></svg> 
 var Polygon = require('paths/polygon'); var polygon2 = Polygon({ points: [[10, 20], [30, 50], [25, 28], [32, 27]], closed: true }); document.getElementById("testpath2").setAttribute("d", polygon2.path.print()); 

Если вы протестируете этот код, вы увидите, что эти две фигуры на первый взгляд выглядят очень похоже. Разница в том, что первая, построенная с помощью низкоуровневого API, имеет одну сторону, построенную с квадратичной кривой вместо сегмента. Действительно, низкоуровневый API имеет то преимущество, что позволяет смешивать разные типы линий в одной форме.

В API среднего уровня нет такого метода, который позволял бы вам удобно их смешивать. Но не бойтесь, ничего не потеряно: как мы уже говорили, вы всегда можете отредактировать Path возвращаемый Polygon(...) :

 <svg width="640px" height="480px"><path id="testpath" class="ocean"></path><path id="testpath2" class="ocean" transform="translate(100)"></path><path id="testpath3" class="ocean" transform="translate(50)"></path></svg> 
 var polygon3 = Polygon({ points: [[10, 20], [30, 50], [25, 28]], closed: false }); console.log(polygon3.path.print()) var polygon3Path = polygon3.path .qcurveto(27, 30, 32, 27) .closepath(); document.getElementById("testpath3").setAttribute("d", polygon3Path.print()); 

Полный список объектов, доступных для интерфейса второго уровня:

  • Полигон : закрытые и открытые полигоны.
  • Полурегулярный многоугольник : особый случай многоугольника: позволяет создавать треугольники, квадраты, пятиугольники и т. Д. (Правильные многоугольники и их неправильные варианты). Он определяется относительно центра, и углы между сегментами от центра до точек все одинаковы, в то время как расстояния этих точек могут быть постоянными (правильные многоугольники) или могут варьироваться (нерегулярные).
  • Прямоугольник : еще один особый тип многоугольника, хотя на этот раз более интуитивно понятный.
  • Безье : рисует гладкую кривую Безье, проходящую через список вершин. Возвращенный путь всегда открыт.
  • Сектор : круговой сектор.
  • Соединитель : определяется как S-образный путь между двумя заданными точками. (Лучший способ понять это — попробовать.)

Несколько примеров, чтобы лучше показать полурегулярные полигоны:

Полурегулярные полигоны

Треугольник (равносторонний)

 var SemiRegularPolygon = require('paths/semi-regular-polygon'); var triangle = SemiRegularPolygon({ center: [50, 50], radii: [20, 20, 20] }); document.getElementById("triangle").setAttribute("d", triangle.path.print()); 

Треугольник (равнобедренный)

 var triangleIrregular = SemiRegularPolygon({ center: [50, 50], radii: [20, 30, 30] }); 

Площадь

 var square = SemiRegularPolygon({ center: [50, 50], radii: [20, 20, 20, 20] }); 

пятиугольник

 var pentagon = SemiRegularPolygon({ center: [50, 50], radii: [20, 20, 20, 20, 20] }); 

Пентагон (Нерегулярный)

 var pentagonIrregular = SemiRegularPolygon({ center: [50, 50], radii: [25, 20, 40, 30, 20] }); 

API высокого уровня

Это API высшего уровня, предоставляемый Paths. Цель его методов — позволить создавать полные диаграммы, начиная с набора данных, для визуализации. Как всегда, все переводится на путь! В частности, все эти методы возвращают объект, содержащий поле curves , массив с формами, созданными для каждой точки данных. Фигуры на curves — это объекты с несколькими соответствующими полями:

  • item : ссылка на соответствующий элемент данных.
  • index : индекс соответствующего элемента данных в массиве данных.
  • Одно или несколько полей, содержащих объекты фигур (например, sector для круговых диаграмм, line и area для линейных диаграмм).

Возвращенные объекты могут иметь дополнительные поля помимо curves , в зависимости от каждой диаграммы. Но каждый метод диаграммы принимает аргумент compute на входе. Этот параметр позволяет пользователям передавать любое количество функций для вычисления дополнительных полей на основе входных данных. (Вычисление цветов было бы типичным примером.)

При использовании этих высокоуровневых методов механизмы шаблонов практически необходимы, поскольку они избавляют разработчиков от большого количества стандартного кода. Они также упрощают создание диаграммы, автоматизируя итерацию по набору форм сложной диаграммы.

Однако, как всегда, они не являются строго необходимыми. Давайте рассмотрим пример того, как можно обойтись без них:

 <svg id="chart-test" width="200px" height="200px"></svg> 
 var somePalette = ['blue', 'green', 'red', 'yellow', 'orange'], Pie = require('paths/pie'), pie = Pie({ data: [ { name: 'Italy', population: 59859996 }, { name: 'Mexico', population: 118395054 }, { name: 'France', population: 65806000 }, { name: 'Argentina', population: 40117096 }, { name: 'Japan', population: 127290000 } ], accessor: function(x) { return x.population; }, compute: { color: function(i) { return somePalette[i]; } }, center: [50, 50], r: 30, R: 50 }), chartSvg = document.getElementById("chart-test"), chartFragment = document.createDocumentFragment(), dx = parseInt(chartSvg.getAttribute('width'), 10) / 2, dy = parseInt(chartSvg.getAttribute('height'), 10) / 2; pie.curves.forEach(function (d, i){ var path = document.createElementNS('http://www.w3.org/2000/svg',"path"); path.setAttributeNS(null, 'd', d.sector.path.print()); path.setAttributeNS(null, 'style', 'fill:' + d.color); var label = document.createElementNS('http://www.w3.org/2000/svg',"text"); label.textContent = d.item.name; label.setAttributeNS(null, 'x', d.sector.centroid[0]); label.setAttributeNS(null, 'y', d.sector.centroid[1]); chartFragment.appendChild(path); chartFragment.appendChild(label); }); chartSvg.appendChild(chartFragment); 

В приведенном выше коде мы используем фрагмент кода, чтобы собрать все сектора перед тем, как фактически добавить их на страницу — и, таким образом, инициируем перекомпоновку только один раз, а не дважды для каждого сектора (один раз для пути и один раз для метки). Элементы фрагмента документа вставляются за один раз, в то время как, если бы мы использовали элемент svg:g для их группировки, каждый узел был бы вставлен отдельно. (Более того, в результирующем SVG будет возможность создания избыточной группы.) Еще большее преимущество фрагментов документа состоит в том, что если нам нужно клонировать всю диаграмму и добавить ее несколько раз на страницу, каждая операция клонирования требует постоянной количество вставок узлов вместо их линейного числа.

Теперь давайте сравним предыдущий код с созданием того же графика, что и выше, используя Ractive:

 <div id="pie-chart"></div><script id="myChartTemplate" type="text/ractive"> <svg width=375 height=400> {{# pie }} {{# curves:num }} <path on-click="expand" d="{{ sector.path.print() }}" fill="{{ color }}" ></path> <text text-anchor="middle" x="d.sector.centroid[0]" y="d.sector.centroid[1]">{{ item.name }}</text> </g> {{/ curves }} {{/ end of pie}} </svg> </script> 
 var Pie = require('paths/pie'); var ractive = new Ractive({ el: 'pie-chart', template: '#myChartTemplate', data: { pie: Pie({ data: [ { name: 'Italy', population: 59859996 }, { name: 'Mexico', population: 118395054 }, { name: 'France', population: 65806000 }, { name: 'Argentina', population: 40117096 }, { name: 'Japan', population: 127290000 } ], accessor: function(x) { return x.population; }, compute: { color: function(i) { return somePalette[i]; } }, center: [50, 50], r: 30, R: 50 }) } }); 

Результат выглядит лучше, чище, и структура графика сразу становится очевидной, если посмотреть на разметку.

В настоящее время доступно 9 различных типов диаграмм:

Вы можете взглянуть на пути [витрина] (http://andreaferretti.github.io/paths-js-demo/), чтобы увидеть, как эти диаграммы выглядят. Все представленные там примеры используют Ractive для создания великолепных анимаций.

Вывод

В этот момент вы можете спросить, действительно ли Paths правильный выбор для вас. Нет, конечно, простого ответа на это. По-разному. Если вам нужны готовые виджеты и графики, вероятно, нет — возможно, вам лучше использовать Highcharts , Flotcharts или Dimple .

Однако мы не можем не подчеркнуть преимущества использования декларативного стиля программирования в визуализации данных. Если вы обязательно используете Paths, результат, вероятно, не будет стоить усилий по изучению новой библиотеки.

Пути полезны в тех случаях, когда вам нужно создавать собственные диаграммы с персонализированным стилем или анимацией или пользовательским поведением в ответ на взаимодействие с пользователем. Но Paths — реальное изменение игры, где вам нужно генерировать графику на сервере. Paths чрезвычайно упрощает создание разметки в логике и отправку ее клиенту в виде JSON или строк.

Наконец, вот несколько ссылок для дальнейшего чтения:


  1. Paths.js зависит только от основных методов EcmaScript 5 , что является проблемой для старых браузеров. Этот polyfill добавляет необходимую поддержку. ↩