How to Create Performant, Template-based Charts with Paths.Js

Originally published at: http://www.sitepoint.com/create-performant-template-based-charts-paths-js/

In a recent post we discussed the state of the art of data visualization in the browser – in particular, SVG libraries. There, we focused on Snap.svg, but we introduced Paths.js as a viable alternative – used together with a template engine or a data binding library. To be fair, Paths is capable of much more, as we are going to show in the following sections, with real use cases to walk you through its logic.

Introduction

Paths’ main purpose is to help front-end developers generate SVG paths with better performance via an intuitive interface. As the name suggests, despite having primitives for shapes like rect or circle, everything can be reduced to paths. This approach unifies different charts, providing a consistent interface where drawing commands always return a list of paths ready to be drawn. By replacing static template engines (such as Mustache or Handlebars) with data binding libraries such as Ractive.js, Angular or React, you can even get animated graphics for free.

Probably the best thing about Paths.js is that it offers three incremental APIs, with increasing levels of abstraction. The lowest level is a chainable API that generates an arbitrary SVG path. On top of this, paths for simple geometric shapes such as polygons or circle sectors are defined. The highest level API allows the generation of some simple graphs that can be fed with a collection of data. (Check out this demo to see the available primitives.)

Actually, strike that: Paths’ best feature is that you can use the library as is on the server-side with Node.js, since it doesn’t directly rely on any library1. This way, you can move the generation of chart structure and details onto the server. Besides speeding up apps, you can thus avoid sending raw data to the client altogether, saving latency and reducing the amount of information you share with clients.

Why Paths?

The greatest advantage of using Paths.js instead of — say — D3 or Snap.svg, is that the latter are imperative, while Paths inherently supports declarative programming when paired with template engines or (even better) data binding frameworks.

The use of Paths with frameworks like Ractive or React causes, in turn, another advantage. These frameworks, in fact, use specific optimizations to reduce the number of reflows and repaints needed every time the DOM has to be modified; they keep “shadow” copies of the DOM, against which they perform the update in a sort of “batch mode”, and finally update the real DOM with the fewest number of changes possible.

Another area where these frameworks make a difference is in event handling. By default, they use event delegation, improving performance in those situations where the same event is attached to a series of elements. The solution is simply to attach these event handlers to some common container of the elements, but it is far too easy to overlook such a pattern when an imperative approach is used (with dire consequences – such as unresponsive pages).

Finally, Paths is lightweight and modular: you can load just the components you actually need, focusing on charts, or just SVG manipulation. Paths is one of those libraries that focus on a few things, trying to optimize them. In general, you can combine a bunch of these kinds of libraries together to perform complex tasks. D3, on the other hand, has many extra utility methods – which is great if you need them, since you have everything you need in one place, but a bit heavy if you don’t.

Using Paths

As mentioned, you can use Paths with Node.js or on the browser. In the latter scenario, you can either load it as AMD modules or as a standalone library.

Paths on Node

If you want to use it on the server, first install it by typing this command on your console (assuming you have correctly installed node and it is in the global path):

npm install paths-js

Once installed, you can load the individual modules:

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

Paths on the Browser: AMD Modules

Paths.js is distributed with Bower, and you can install it from the command line:

bower install paths-js

Or, of course, just download it manually from its repository on GitHub.

Paths is structured into various AMD modules, and can be loaded with AMD module loaders. Using RequireJS (assuming you have installed Paths with Bower), this how you can configure it:

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

The actual path will depend on your Bower configuration or, for manual downloads, on the structure of your folders. (Be careful where you place the amd folder linked above.)

After configuring it correctly, you can easily require individual modules:

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

Paths on the Browser: Standalone Script

If you prefer to avoid AMD modules, you can safely include Paths as a standalone script: the file you need is dist/global/paths.js. Once it is included in your page, the paths object will be available in the global scope, so that the individual modules will be available as paths.Pie, paths.Polygon and so on. Besides verbosity, you lose the ability to import only the modules you need – but if you need many of them, this will have a negligible impact.

Low-Level API

The lowest level API target, as mentioned, is creating paths. Targets creation is as easy as calling one constructor: Path(). The whole API is chainable, so that you can create a complex path by invoking methods on the result of previous calls. Path objects offer methods to extend the current path incrementally; a reference to the last point in the path is kept, and lines or curves can be added from that point, mimicking the SVG syntax for paths. The main methods you will need are:

  1. moveto(x, y): moves the cursor to the coordinates passed.
  2. lineto(x, y): draws a line from the end of the path to those coordinates.
  3. curveto(x1, y1, x2, y2, x, y): draws a cubic Bézier curve from the current point to (x,y) using (x1,y1) as the control point at the beginning of the curve and (x2,y2) as the control point at the end of the curve.
  4. smoothcurveto(x2, y2, x, y): draws a cubic Bézier curve from the current point to (x,y), implicitly computing the first control point based on the second one and the previous command (if any).
  5. arc('rx', 'ry', 'xrot', 'large_arc_flag', 'sweep_flag', 'x', 'y'): draws an elliptical arc from the current point to (x, y), controlling ellipse radii and rotation through the other parameters.
  6. closepath(): closes the path, turning it into a polygon.

All the methods available support a “verbose” API as well, so that named parameters (in the form of a configuration object) can be passed to each of them seamlessly. For example, the moveto method above can be called as Paths().moveto({x: 10, y: 3}) or Paths().moveto(10, 3). The names of the parameters follow the SVG specification.

More methods are available, and in general there is a one-to-one matching with SVG Paths commands. For example, qcurveto(x1, y1, x, y) and smoothqcurveto(x, y) are the analogous to curveto smoothcurveto for quadratic curves.

In general, this works better with a template engine, but it isn’t strictly necessary. You can use Paths with an imperative style, as the next examples demonstrate. However, this is not the best practice:

    <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());

The print() method of Path objects translates the path constructed into the corresponding SVG data string, as it would appear in the d (data) attribute of paths. Once we obtain that value, we can manually set the proper attribute on any path just using CSS selectors and the getElementById / getElementsBy* methods.

Of course, using a template engine would greatly impact the amount of boilerplate code we need to write:

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

This removes the need to manually set a d attribute for #testpath, and even to assign an id to the path element. This is the best practice style for creating SVG drawings with Paths.

Continue reading this article on SitePoint

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.