Monthly Archive for August, 2009

HTML, SVG or Canvas Labels?

As you might know, the JavaScript InfoVis Toolkit uses the HTML5 Canvas element for plotting and animating graphs. This is all very nice, Canvas performance compared to other techniques for plotting these things (SVG for example) is by far superior. But of course, there are drawbacks.

Canvas better performance is due to the fact that there are no tracked elements: the Canvas is simply an image and you’re drawing there just like you’d be drawing something in paint. One big problem is that there’s no native possibility to add events to what’s drawn in Canvas, like a plotted node, edge or label.

As opposed to Canvas, SVG has a DOM/XML like spec: you have all these tags (<g> <text> <rect>) and each of them is just like a DOM element: you can add click event handlers, individual styling with CSS, etc.
Having to keep track of all these elements and handling a DOM-tree makes the performance of SVG not suitable for visualizing (and animating) medium to large datasets on the web.

Using HTML labels

Just like SVG, HTML is a DOM/XML-like spec, where you can add event handlers to each element. Also, every web developer knows HTML so exposing HTML labels through user-defined controller methods in the library seemed to me like a good choice. For controller methods like onCreateLabel or onPlaceLabel an HTML element is passed and the user can style or add event-handlers to it.

For example, here’s a fragment of the code used in the RGraph demo. You can see the rest of the code here:

        //Add the name of the node in the correponding label
        //and a click handler to move the graph.
        //This method is called once, on label creation.
        onCreateLabel: function(domElement, node){
            domElement.innerHTML = node.name;
            domElement.onclick = function(){
                rgraph.onClick(node.id);
            };
        },
        //Change some label dom properties.
        //This method is called each time a label is plotted.
        onPlaceLabel: function(domElement, node){
            var style = domElement.style;
            style.display = '';
            style.cursor = 'pointer';

            if (node._depth <= 1) {
                style.fontSize = "0.8em";
                style.color = "#ccc";

            } else if(node._depth == 2){
                style.fontSize = "0.7em";
                style.color = "#494949";

            } else {
                style.display = 'none';
            }

            var left = parseInt(style.left);
            var w = domElement.offsetWidth;
            style.left = (left - w / 2) + 'px';
        }

In my opinion this is a good approach, good points are:

  1. I'm using well known HTML elements.
  2. Dealing with DOM elements let's you add event handlers, individual styling and things like that.

Weak points are:

  1. I'm using a DOM tree, which means that if labels are plotted at all times I'm exhaustively updating the DOM and this might lead to performance problems.
  2. HTML is good for structuring pages, but for example you might want to apply transformations to HTML elements (like rotating labels, etc), and these aren't supported by all browsers yet.

    So one of the problems that might arise is, for example, the fact that in radial layouts labels might be occluded:

    occluded labels

Using SVG labels

So I began exploring other possibilities to create labels. For this I abstracted the Label interface I had and split it into:

  • Graph.Label.Native (for native canvas labels)
  • Graph.Label.DOM(abstract class for dom elements)
  • Graph.Label.HTML(extends the DOM interface with HTML specific stuff)
  • Graph.Label.SVG(extends the DOM interface with SVG specific stuff)

I also modified the Canvas class so you can specify the type of labels you want to use, labels:'HTML', labels:'SVG' or labels:'Native'. Default's HTML.

The same RGraph example code now would look like this:

        //Add the name of the node in the correponding label
        //and a click handler to move the graph.
        //This method is called once, on label creation.
        onCreateLabel: function(domElement, node){
            domElement.firstChild
              .appendChild(document
                .createTextNode(node.name));
            domElement.onclick = function(){
                rgraph.onClick(node.id, {
                  hideLabels: false
                });
            };
        },
        //Change some label dom properties.
        //This method is called each time a label is plotted.
        onPlaceLabel: function(domElement, node){
            var bb = domElement.getBBox();
            if(bb) {
              //center the label
              var x = domElement.getAttribute('x');
              var y = domElement.getAttribute('y');
              //get polar coordinates
              var p = node.pos.getp(true);
              //get angle in degrees
              var pi = Math.PI;
              var cond = (p.theta > pi/2 && p.theta < 3* pi /2);
              if(cond) {
                domElement.setAttribute('x', x - bb.width );
                domElement.setAttribute('y', y - bb.height );
              } else if(node.id == rgraph.root) {
                domElement.setAttribute('x', x - bb.width/2);
              }

              var thetap =  cond? p.theta + pi : p.theta;
                domElement.setAttribute('transform', 'rotate('
                + thetap * 360 / (2 * pi) + ' ' + x + ' ' + y + ')');
            }

This code does a little bit more than just plotting the label, it rotates the labels so they're not occluded:

Good points of this approach are:

  • Just like with any other DOM element, you can add event handlers.
  • You can apply transformations to labels.

Weak points:

  • Performance, for the same reasons as HTML.
  • IE does not support SVG.

Bonus good point: Google is making work SVG in IE with some open source library that works apparently the same as the ExCanvas library. Here's the Open Source project that will be presented here.

That's like the main reason why I've been considering a different approach for labels ;)

Native Canvas labels

Native Canvas labels make use of the HTML5 Canvas text API to plot labels.
Since the labels are just painted in the Canvas there's no DOM tree to update, and performance is good.
The Canvas text API has fillText, strokeText and measureText as methods. You can read more about the Canvas Text API here.

This is the code I added to the Graph.Label.Native class:

Graph.Label.Native = new Class({

    plotLabel: function(canvas, node, controller) {
        var ctx = canvas.getCtx();
        var coord = node.pos.getc(true);
        ctx.fillText(node.name, coord.x, coord.y);
    },

    hideLabel: $empty
});

A very good point about this approach is performance. Also, the code is simpler. You don't have to keep a labelContainer and update DOM labels each time you're making an animation.

Weak points are:

  • Opera does not support this feature.
  • You can't natively add event handlers to labels. I think I've seen someone do something similar for text in processing, but I'm not sure there's a good way of doing this without keeping track of the position of each label and perform a check each time a click is triggered in the canvas element.
  • I should change the way I define controller methods, in order to be able to pass a custom label object with x, y, theta, rho, width, height properties that could be modified on the fly, and then translate these changes into translate and rotate native canvas calls to be able to plot the text the way the user wants it. This seems just to damn complicated.... But I'll consider it.

Anyway, these are the methods I've found to plot labels into graphs.

Which one do you think is the best?
Do you know about any other approaches I could take to solve this problem?

Version 1.1.3

I just tagged the JavaScript InfoVis Toolkit with version 1.1.3. It’s been some time since the last release, and I wanted to use this post to make a summary of the changes and to describe some of the new features that have been added to the library.

I’ll start with the new features:

SpaceTree: SwitchAlignment

I added some new global configuration properties to the SpaceTree: align and indent.

Align sets the alignment of the tree to center, left and right:



The indent parameter sets an offset between a parent and its children when the alignment is left or right. You can also use the switchAlignment method for changing the alignment of the tree with an animation.

SpaceTree: Multiple nodes in path

I added two new methods to the SpaceTree: addNodeInPath and clearNodesInPath.
These two methods allow you to add a node to the “selected-nodes” path. When a node belongs to the “selected-nodes” path it remains always visible (as in always expanded).

I made this small video to show the feature:

SpaceTree: MultiTree

I added a SpaceTree configuration property called multitree.
If multitree=true, the visualization will search for the $orn data property in each node and display the subtrees according to their orientation.

In this example I set multitree=true and set $orn=’left’ for some nodes and $orn=’right’ for others. This way I create a partition of the tree:

I also use the setRoot method to set the clicked node as root for the visualization. This way the clicked node is centered and a centrifugal view from that node is drawn.

Bug Fixes

I’ve been fixing a couple of bugs also, most of them have to do with Treemaps:

I also want to thank Guido Schmidt for his interest in the library and work on the GWT JIT component.

There’s also been some JIT development for Grails done by Bertrand Goetzmann.

Anyway, things are looking good! :)

Back to Basics

Most of us JavaScript developers can’t work without using a JavaScript Library/Framework today. JavaScript is a very nice language, but it requires for you to write a lot of code before you can implement some interesting animation, drag and drop feature, or Ajax request/polling.
This is in part due to the fact that every line of code we write must be browser compatible, and that adds lots of “ifs” to our code.
So I guess it’s understandable to think that choosing a framework that can abstract these kind of things for us is good. By using a framework we can concentrate on other kind of problems, more like high-level-usability-pattern problems.

Some frameworks stick with JavaScript as their main language (like MooTools or JQuery). Other frameworks simply let you type another kind of language that then gets compiled into JavaScript. I guess that having a language with built-in classical inheritance syntax and a nice IDE to support it are good reasons to develop these kind of libraries that translate some code into another.

Frameworks are undeniably good, but most of the things I learn about the JavaScript programming language were learn while hacking some pure JavaScript code.
When hacking pure JavaScript code you find yourself hacking common language idioms that are usually abstracted by frameworks. And it’s nice to understand how these things work. When you get how a specific JavaScript pattern/idiom works you get to understand lots of things about the language itself.

Most of the people don’t do this today. You can see posts about call and apply, closures and private members patterns very often in Reddit and Ajaxian, and each time that post appears lots of other people upmod it. So that means that most of the people today probably use the bind Function method without really knowing what it does.

The worst part is that JavaScript is a very beginner-friendly language, a good start for people not having a computer sicence related background, but at the same time there are lots of things about the language itself that are advanced features (object mutability, booleans as defaults, functions as first class citizen, prototypal inheritance) and most of the users are never aware of these features, most of the time due to the abstraction of the frameworks they’re using.

An Example

This is a very simple example (and interesting interview question also).
If you’re dealing with the dom when hacking JavaScript then you might often use the hasClass and removeClass methods from JQuery, MooTools, or whatever.

So, how would you write them?

function hasClass(domElement, className) {
 //code here...
}

function removeClass(domElement, className) {
//code here...
}

Please, if you’re reading this, take five minutes of your time and write these functions out before reading the answers. Believe me, it’s worth the effort. I mean, how much time can it take?

The Answers

click here to show the answers

Conclusion

These methods could have probably been written differently if disk space and performance weren’t such an important issue in JavaScript, but with those constraints even the most trivial methods like hasClass and removeClass can be interesting to read.
So this is my recommendation: try to understand how things you normally use are implemented. There are lots of interesting JavaScript libraries that make excellent code that’s performant and save disk space (:P) so check them out, you might learn about lots of things, even the most simple functions can have interesting concepts.

MultiTrees (Part 1)

I found this CHI 94′ visualization paper on MultiTrees in the internet and I’ve been trying to see what type of applications and ideas I could “borrow” for the JavaScript InfoVis Toolkit.

What are MultiTrees?

A MultiTree is a type of directed acyclic graph (DAG) where each node has a tree both as parent structure and as descendants.

For example, a MultiTree can be created by overlapping different tree structures on a set of nodes, as shown in the picture below:

As you can see MultiTrees can have cycles if they’re not directed.

Laying and Navigating MultiTrees

What’s interesting about this structure is that if we take two nodes x and y such that x <= y and the path connecting them, then we can form a topological tree by adding all descendants and ancestors of each node belonging to that path in the new graph.

Implementation

I recently pushed support for a small subset of MultiTrees: those where x = y.
This is just a centered node and its tree of descendants and ancestors.

The tree navigation is the same as the SpaceTree:

I also implemented a way of changing the current focused node, so that when the clicked node is centered a centrifugal view from that node is adopted.
This way the current selected node is set as root of the visualization.
Hopefully you’ll understand the difference between this navigation and the previous one.

Anyway, as soon as I get this feature fully functional I’ll make another post explaining how to use this stuff in the JavaScript InfoVis Toolkit.

Outside In

While taking a look at some of the talks by Tamara Munzner I found this video/documentary about turning a sphere inside out.
I’ve found this to be very impressive (and pedagogic). The style of the video reminds me the Moebius Transformation video I posted last year.

For those who don’t know Tamara Munzner she’s like a big reference in all InfoVis-related stuff. One of the most inspiring InfoVis talks I’ve found was done by her and can be seen here. This is the talk that started my interest in InfoVis and the making of the JavaScript InfoVis Toolkit.

Drawing Trees

While trying to fix a SpaceTree layout issue for version 1.1.2 of the JavaScript InfoVis Toolkit I found a Microsoft Research paper that describes a functional programming approach for rendering trees in an aesthetically pleasing way.

But what is aesthetically pleasing?

Andrew J. Kennedy takes Radack and Walker rules:

  1. Two nodes at the same level should be placed at least a given distance apart.
  2. A parent should be centred over its offspring.
  3. Tree drawings should be symmetrical with respect to reflection—a tree and
    its mirror image should produce drawings that are reflections of each other. In
    particular, this means that symmetric trees will be rendered symmetrically.
  4. Identical subtrees should be rendered identically—their position in the larger
    tree should not affect their appearance.
  5. Trees should be as narrow as possible without violating these rules

In order to calculate nodes’ positions Kennedy takes a “bottom-up” approach:

Starting from the root node, draw for each node all its subtrees without breaking any rules. Fit these subtrees together without changing their shape, and also without breaking rules 1 and 3 (i.e do not break symmetry and avoid cluttering/overlapping of nodes). Finally, center their parent above them like specified in rule 2.

The “fitting” of the subtrees is calculated by operating on subtrees extents. A subtree extent is a data structure containing the relative coordinates of the boundary of a subtree. One frequent operation between extents is merging:

Other operations involve setting the distance between two extents, translating extents, etc.

Implementation

Kennedy implements this algorithm in Standard ML. I made a JavaScript adaptation. I like the Standard ML version a lot more; in this case rich typing makes very elegant code.

Here’s my code implementation, in case you want to compare it to Kennedy’s.
My version takes into account different tree layouts (left, right, bottom, top), siblings and subtrees offsets and different node sizes as opposed to Kennedy’s version.

 function movetree(node, prop, val, orn) {
   var p = (orn == "left" || orn == "right")? "y" : "x";
   node[prop][p] += val;
 };

 function moveextent(extent, val) {
     var ans = [];
     $each(extent, function(elem) {
         elem = slice.call(elem);
         elem[0] += val;
         elem[1] += val;
         ans.push(elem);
     });
     return ans;
 };

 function merge(ps, qs) {
   if(ps.length == 0) return qs;
   if(qs.length == 0) return ps;
   var p = ps.shift(), q = qs.shift();
   return [[p[0], q[1]]].concat(merge(ps, qs));
 };

 function mergelist(ls, def) {
     def = def || [];
     if(ls.length == 0) return def;
     var ps = ls.pop();
     return mergelist(ls, merge(ps, def));
 };

 function fit(ext1, ext2, subtreeOffset, siblingOffset, i) {
     i = i || 0;
     if(ext1.length <= i ||
        ext2.length <= i) return 0;

     var p = ext1[i][1], q = ext2[i][0];
     return Math.max(fit(ext1, ext2, subtreeOffset, siblingOffset, ++i) + subtreeOffset,
                 p - q + siblingOffset);
 };

 function fitlistl(es, subtreeOffset, siblingOffset) {
   function $fitlistl(acc, es, i) {
       i = i || 0;
       if(es.length <= i) return [];
       var e = es[i], ans = fit(acc, e, subtreeOffset, siblingOffset);
       return [ans].concat($fitlistl(merge(acc, moveextent(e, ans)), es, ++i));
   };
   return $fitlistl([], es);
 };

 function fitlistr(es, subtreeOffset, siblingOffset) {
   function $fitlistr(acc, es, i) {
       i = i || 0;
       if(es.length <= i) return [];
       var e = es[i], ans = -fit(e, acc, subtreeOffset, siblingOffset);
       return [ans].concat($fitlistr(merge(moveextent(e, ans), acc), es, ++i));
   };
   es = slice.call(es);
   var ans = $fitlistr([], es.reverse());
   return ans.reverse();
 };

 function fitlist(es, subtreeOffset, siblingOffset) {
    var esl = fitlistl(es, subtreeOffset, siblingOffset),
        esr = fitlistr(es, subtreeOffset, siblingOffset);
    for(var i = 0, ans = []; i < esl.length; i++) {
        ans[i] = (esl[i] + esr[i]) / 2;
    }
    return ans;
 };

 function design(graph, node, prop, config) {
     var orn = config.orientation;
     var auxp = ['x', 'y'], auxs = ['width', 'height'];
     var ind = +(orn == "left" || orn == "right");
     var p = auxp[ind], notp = auxp[1 - ind];

     var cnode = config.Node;
     var s = auxs[ind], nots = auxs[1 - ind];

     var siblingOffset = config.siblingOffset;
     var subtreeOffset = config.subtreeOffset;

     var GUtil = Graph.Util;
     function $design(node, maxsize, acum) {
         var sval = (cnode.overridable && node.data["$" + s]) || cnode[s];
         var notsval = maxsize || ((cnode.overridable && node.data["$" + nots]) || cnode[nots]);

         var trees = [], extents = [], chmaxsize = false;
         var chacum = notsval + config.levelDistance;
         GUtil.eachSubnode(node, function(n) {
             if(n.exist) {
                 if(!chmaxsize)
                    chmaxsize = getBoundaries(graph, config, n._depth);

                 var s = $design(n, chmaxsize[nots], acum + chacum);
                 trees.push(s.tree);
                 extents.push(s.extent);
             }
         });
         var positions = fitlist(extents, subtreeOffset, siblingOffset);
         for(var i=0, ptrees = [], pextents = []; i < trees.length; i++) {
             movetree(trees[i], prop, positions[i], orn);
             pextents.push(moveextent(extents[i], positions[i]));
         }
         var resultextent = [[-sval/2, sval/2]].concat(mergelist(pextents));
         node[prop][p] = 0;

         if (orn == "top" || orn == "left") {
            node[prop][notp] = acum;
         } else {
            node[prop][notp] = -acum;
         }
         return {
           tree: node,
           extent: resultextent
         };
     };
     $design(node, false, 0);
 };