nooshu - Matt Hobbs' Web Development Blog

Kneeling on the shoulders of giants

Visual thesaurus using arbor.js

Wow, I can’t believe we are at the end of January – one twelfth of the year gone already! I’ve been so busy this month I’ve barely had time to sit down and work on any personal projects. Thankfully I had a spare few hours over the weekend to work on a project I’ve been meaning to finish for a couple of weeks: a visual thesaurus using JavaScript.

Visual Thesaurus using JavaScript

Visual thesaurus representation of the word 'clean'.

You may (or may not) know of the Visual Thesaurus by Thinkmap; very useful but it’s a paid for service, I use it so infrequently I can’t justify paying for it (there is a free trial though). Recently a very interesting graph visualisation library popped up on delicious called arbor.js, so I decided to have a closer look and see if I could use it for the JavaScript version. For the word data I’ve used the Bighugelabs thesaurus API which I previously used to create my Post Thesaurus WordPress plug-in (shameless plug!).

Arbor.js took a little getting used to as it uses a mixture jQuery, canvas and its own methods; but there’s a fairly extensive set of documentation and a set of usage examples. Once you get your head around adding nodes and edges to the system, arbor takes care of the rest. One thing I did notice is the site examples and my VT experiment run much smoother in Firefox than they do in Chrome, which is quite surprising considering how quick the V8 engine is in Chrome. Maybe this will be improved in future versions.

So to the demo; I’ve tried to keep it very simple to use. Enter the word you wish to visualise and click search. Once the API returns a result it’s plotted by arbor. You can drill down into the result by clicking on the sub-particles (and remove them by clicking again). The individual words can be clicked and you’re given the option to search again using the selected word. I still haven’t got the perfect system setup as some results can be a little jittery; usually when an even set of words are returned. I’ll keep playing with system settings to see if I can improve this.

Any suggestions, comments or feature improvements please leave a comment below!

JP on February 9 11 / 39 Permalink

This is very interesting, but the demo doesn’t appear to be working…

Matt on February 9 11 / 39 Permalink

I’ve tested in Chrome, Safari and Firefox and it works fine. What browser are you using?

Dámaris on March 14 11 / 72 Permalink

Thanks for your contribution! I’m trying myself to figure out how to refresh the JSON files everytime there is an onClick(), but I’m still stuck trying to get nodes from JSON files instead of creating them in the same .js file.

To do so, I just put this in the “main.js” file provided in the example of arborjs:

1
var data = $.getJSON("prueba.json",function(data {sys.graft({nodes:data.nodes,edges:data.edges})})

I put this inside this function:
$(document).ready(function()

But I don’t see anything apart from the frame…my json file is in the same folder as the js one.

Could you give me any hints on this?

Thanks anyway lighting up my search a bit =)

Dámaris.

Matt on March 14 11 / 72 Permalink

Hi Dámaris,
Are you getting anything back if you get a console.log on the data variable? It looks like you are missing a closing bracket on the function from your example “function(data){….}”. Do you have an example online anywhere? It’s a bit hard to see what the issue could be without all the code :)

Dámaris on March 15 11 / 73 Permalink

Hi Matt,

I thought I had posted something, but it disappeared =( Sorry for the little info, I just got the example from arborjs/docs/sample-project and I changed this in main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 $(document).ready(function(){
    var sys = arbor.ParticleSystem(1000, 600, 0.5)
    sys.parameters({gravity:true})
    sys.renderer = Renderer("#viewport")
     var data = $.getJSON("prueba.json",function(data){sys.graft({nodes=data.nodes,edges=data.edges})})  
   
    // add some nodes to the graph and watch it go...
    /*sys.addEdge('a','b')
    sys.addEdge('a','c')
    sys.addEdge('a','d')
    sys.addEdge('a','e')
    sys.addNode('f', {alone:true, mass:.25})
    sys.addEdge('f','b')
   */
   
  })

})(this.jQuery)

I just commented the part where nodes and edges are created and put “var data…” (it works fine when i remove the commentary)

In my prueba.json file, which is in the same folder as main.js, I have this:

1
{"nodes":{"Pau":{"lenght":20},"NBA":{"lenght":20},"Lakers":{"lenght":20},"España":{"lenght":20}},"edges":{"Pau":{"NBA":{"length":2.5,"weight":2},"Lakers":{"length":2.5,"weight":2},"España":{"length":2.5,"weight":2}}}

I cannot log my variable, suppose is something like console.log(data)?

Thanks

Dámaris.

Dámaris on March 16 11 / 74 Permalink

Solved, I had a = instead of : in sys.graft, and somehow my json had something wrong, but I changed it and it works…

Thanks for your demo, it’s pretty much what I was looking for =)

Dámaris.

Matt on March 16 11 / 74 Permalink

Apologies Dámaris, didn’t get round to looking at your code. I’m glad you worked it out in the end. JSON is a real pain if there are small errors, very temperamental!

Thanks for the feedback on the demo, If you post your demo on the web somewhere in the future, be sure to leave a comment, I’d love to see what other developers are using arbor for :)

Dámaris on March 18 11 / 76 Permalink

Hello again, thanks again for your help, your code is helping me a lot. I’m currently working on my way to represent ontologies, and semantic relations using arborjs, and I’m a bit confused with the way to retrieve information from the json object.

I have a JSON like this:

1
{"nodes":{"Staples Center":{"color":"blue","level":0},"Los Angeles Clippers":{"color":"red","level":1},"Los Angeles Sparks":{"color":"blue","level":1},"Los Angeles Kings":{"color":"blue","level":1},"Los Angeles D-fenders":{"color":"green","level":1},"Lakers":{"color":"purple","level":1},"WNBA":{"color":"purple","level":1},"NHL":{"color":"purple","level":1}},"edges":{"Staples Center":{"Los Angeles Clippers":{"role":"gathers","length":2.5},"Los Angeles Sparks":{"role":"gathers","length":2.5},"Los Angeles Kings":{"role":"gathers","length":2.5},"Los Angeles D-fenders":{"role":"gathers","length":2.5},"Lakers":{"role":"gathers","length":2.5}},"Los Angeles Sparks":{"WNBA":{"role":"belongs to","length":2.5}},"Los Angeles Kings":{"NHL":{"role":"belongs to","length":2.5}}}}

Which is well retrieved and represented when I do this:

1
var data = $.getJSON("prueba.json",function(data){sys.graft({nodes:data.nodes,edges:data.edges})})

The thing is: I’m trying to access to specific nodes using their key, but I cannot.

Something like this:

1
data.nodes.Lakers

or

1
nodes.Lakers

won’t access to the data related to “Lakers” node. Or if I want to create a node with the name of one of my nodes, and try:

1
var aux = nodes.Lakers.name

I get nothing. I’ve tried to see what I get with alert, and looking in forums, but I don’t see where I’m doing wrong.

Is it that the only way to access to a specific noun is by using eachNode and comparing all way long?? I hope it’s not!

Do you have any ideas on this?

Thanks a lot,

Dámaris.

Matt on March 19 11 / 77 Permalink

Hi Dámaris,

I’ve added an example of how to access your JSON data. Take a look at the source. I’ve added a few console.log’s that will show if you have Firebug installed (or use Webkits web inspector).

Hope that helps!

Dámaris on March 22 11 / 80 Permalink

Thanks for your help, I have a clearer idea of how it works now. The problem I think is when I try to use “data” out of the context where the json is retrieved, so I have to either declare data as a global variable or call getJSON everytime I need it.

Right now I’m puzzled with the silliest of the problems I’ve come across so far =P How can I reduce the length of my edges? It seems silly but I have tried everything and nothing works, a small graph, if it’s alone on the screen, stretches up until it reaches the border of the frame, don’t know why…

I have this json:

1
{"nodes":{"Staples Center":{"label":"Staples Center","length":2.5,"color":"blue","use":"root"},"Los Angeles Clippers":{"label":"Los Angeles Clippers","length":2.5,"color":"red","use":"child"},"Los Angeles Sparks":{"label":"Los Angeles Sparks","length":2.5,"color":"blue","use":"child"},"Los Angeles Kings":{"label":"Los Angeles Kings","length":2.5,"color":"blue","use":"child"},"Los Angeles D-fenders":{"label":"Los Angeles D-fenders","color":"green","use":"child"},"Lakers":{"label":"Lakers","length":2.5,"color":"purple","use":"child"}},"edges":{"Staples Center":{"Los Angeles Clippers":{"role":"gathers","length":1},"Los Angeles Sparks":{"role":"gathers","length":1},"Los Angeles Kings":{"role":"gathers","length":1},"Los Angeles D-fenders":{"role":"gathers","length":1},"Lakers":{"role":"gathers","length":1}}}}

Then, I add edges to my sys doing this (inside the appropriate loop)

1
sys.addEdge(source,target,{role:edge.data.node,length:0.2});

And inside redraw() I have something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
 particleSystem.eachEdge(function(edge, pt1, pt2){
          // edge: {source:Node, target:Node, length:#, data:{}}
          // pt1:  {x:#, y:#}  source position in screen coords
          // pt2:  {x:#, y:#}  target position in screen coords

          // draw a line from pt1 to pt2
          ctx.strokeStyle = "rgba(0,0,0, .333)";
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(pt1.x, pt1.y);
          ctx.lineTo(pt2.x, pt2.y);
          ctx.stroke();
        })

It has to do with the position where nodes are, is it? Because it draws a line from the root node to its children, but how can I change that in a way that they get closer?

Dámaris.

Matt on March 22 11 / 80 Permalink

Hi Dámaris,
That’s a good question, I think in my demo I let the nodes do whatever they like without any length restrictions.

I’ll see if I can put a small demo together.

Dámaris on March 23 11 / 81 Permalink

I found a little problem in your demo, if there are too many nodes together, the node in the center tries to escape and everything runs a bit crazy….I tried typing something like “run” which has lots of synonyms, I’ll try to see why that happens, it might have to do with the node repulsion, or the implementation of the moved handler.

Or maybe it’s just that we cannot put so many nodes together. I’ll let you know if I find a way to solve it.

Matt on March 23 11 / 81 Permalink

Hi Dámaris,
Yes I noticed that myself a couple of times. The system seems to settle into an equilibrium then maybe small rounding errors make it go crazy. I tried playing with the system variables but none of them seemed to help. The only solution I could see was to stop the system after a set number of seconds. Not ideal at all. I emailed the arbor.js author to see if he had a solution but had no reply unfortunately.

It may be something you run into too, so yes if you do find a solution send it over, it’d be much appreciated :)

Dámaris on March 25 11 / 83 Permalink

About the bug in the library, I don’t know how to solve it yet, another way to avoid that would be to only allow a maximum number of nodes surrounding the central node, or show the rest in any other way, like a node with (…)…not satisfactory solution at all though…

About the length of the edges, I’m still puzzled over it, it seems that the graph stretches out to reach the size of the canvas, regardless the length I give to every node. It’s something dynamic, isn’t it? Apart from resizing my canvas, I don’t see any other way, do you?

I’m trying to represent an ontology, and that makes things more difficult, because any node can be related to any node, there is no fixed structure like in your example. I wouldn’t have been able to get here without your help though, thanks!

Matt on March 27 11 / 85 Permalink

Hey Dámaris,

I think I solved the juddering on my example. I changed a couple of system settings; enabled gravity and increased the stiffness to 900. It settles down after a couple of seconds. I think enabling the gravity acts as a dampener.

I’ve put together a small example showing the length of edges. The system does seem to act strangely when it comes to length. It expands to fit the canvas when it can and only really observes the edge length when the the nodes are forced together. Hopefully the example helps you out a little.

Dámaris on March 28 11 / 86 Permalink

Hi Matt!

Thanks for the examples, I’m afraid that it keeps running wild when I for instance type the word “run” which has lots of synonyms.

About the length issue, it does act strangely when all nodes are pretty short, it ignores them to expand and fit the canvas, but if any of them is long enough, it works fine. I don’t want to set an extra-long edge to keep the rest ok, so I think the best thing may be to resize the canvas…

Matt on March 28 11 / 86 Permalink

Oh damn, yeah I see what you mean with “run”, it does go a little crazy. Hmmm okay back to the drawing board :)

I can’t see any way of turning off the auto expand in the API. You can add padding to the canvas, maybe that could be a slight solution? Or maybe as a hack add 1 long node and edge which is the same colour as the background? Not nice at all but it may work.

Dámaris on March 30 11 / 88 Permalink

Have you noticed that nodes in your demo can be dragged when the mouse is actually not on the nodes, but within a very wide ratio?

Do you know if that has by any chance to do with the call

dragged = particleSystem.nearest(_mouseP);

that assumes that “dragged” has to have some value, regardless how far you are from the nodes?

I will try to fix it, if you have any ideas, let me know,

thanks

Dámaris.

Matt on March 30 11 / 88 Permalink

Hi Dámaris, I can’t say I’d noticed actually. I will look see what it’s doing. I think I grabbed that code straight from one of the demos by the author so it may just be a limitation of arbor.js. I’ll do a little investigating.

Dámaris on March 31 11 / 89 Permalink

I found a way, but it’s a bit dodgy actually, I went to the “nearest” function definition in arbor.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
nearest:function(x)
{

    if(u!==null)
    {
        x=g.fromScreen(x)
    }

    var w={node:null,point:null,distance:null};
    var v=g;
    $.each(c.nodes,function(B,y)

    {
   
    var z=y._p;

    if(z.x===null||z.y===null)
    {
        return
    }

    var A=z.subtract(x).magnitude();
   
        if(w.distance===null||A<w.distance)
    {
   
        w={node:y,point:z,distance:A};

        if(u!==null)
        {
            w.screenPoint=g.toScreen(z)
        }
    }

    }

        );


    if(w.node)
    {

        if(u!==null){

            w.distance=g.toScreen(w.node.p).subtract(g.toScreen(x)).magnitude()

                }


                            return w

        }else{

                 return null



              }
       },

where it calculates the minimum distance from our mouse to every node, and I just put something like:

1
if(A < 100){return w}

when it’s going to return the node result.
That way I make sure no node will be selected if my mouse is too far from any of them.
2 problems:

1. That value (100) suits my demo right now, but it should be related to the size of the nodes of every canvas, don’t know how to do it though.
2.If I drag my node too fast, as the mouse gets further from the node which is being dragged, that “if(A<100)” is no longer true and I drop my node.If I found a way to, once I've started to drag a node, there is no more calculation of the nearest node…I’ll keep working on it.

Dámaris on March 31 11 / 89 Permalink

Sorry, I put if(A< 100), that's incorrect, it'd be if(w.distance < 100).

Dámaris on March 31 11 / 89 Permalink

Ok, I think I solved it. I just commented most of the code in “moved” function in our handler, and left this:

1
2
3
4
5
6
7
 moved:function(e){
        var pos = $(canvas).offset();
                _mouseP = arbor.Point(e.pageX-pos.left, e.pageY-pos.top);
                nearest = particleSystem.nearest(_mouseP);
       
        return false;
      },

So there are no more problems related to how far the node and the mouse are while I’m dragging, and I just adjusted the minimum distance from 100 to 50, so that it suits my example better.

This again is not the best of the options, since it will always depend on how big your nodes are, but at least there is no undesirable dragging =)

Matt on March 31 11 / 89 Permalink

Excellent stuff Dámaris, you’ve been busy :) I’ll have a look over that this weekend and add it back into my example too.

Dámaris on April 7 11 / 96 Permalink

Hello Matt, yes, I’ve been busy, I added a function called “is_over_node” to arbor.js, and it works much better now, it’s adapted to my shapes, but works fine for me =) It’s something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
is_over_node:function(x,n)
{
//this function receives x as the mouse position and n as an object with the structure: //{"x_center","y_center","lenght","width"}

                    var centro_x =  n.x - n.width/2;
                                        var centro_y =  n.y;                           
                    var xComponent = (Math.pow(x.x - centro_x, 2) / Math.pow(n.height, 2));
                            var yComponent = (Math.pow(x.y - centro_y, 2) / Math.pow(n.height, 2));
                    var value_1 = xComponent + yComponent;
                                       
                                });
                                       
                            centro_x = n.x + n.width/2;                            
                        centro_y = n.y;
                    xComponent = (Math.pow(x.x - centro_x, 2) / Math.pow(n.height, 2));                
                    yComponent = (Math.pow(x.y - centro_y, 2) / Math.pow(n.height, 2));
                                        var value_2 = xComponent + yComponent;
                   

                    if(Math.abs(n.x - x.x) &lt; (n.width/2) &amp;&amp; Math.abs(n.y - x.y) &lt; (n.height/2) || (value_1 &lt; 0.24) || (value_2 &lt; 0.24))
                                        {
                       
                        return true
                    }
                                else
                                {
                         return false
                    }
                       
               
},

//The comparison with 0.24 is completely empirical, it found that that&#039;s the number good for ellipses (or the ellipses I built), whereas 1 is what you might find for the classic circumference.

I have a question now, don't know how to separate a single and double click event, because the single click is always called…I got this pseudo-solution but is still insufficient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var handler =  {

         
         
observeSingleAndDoubleClick: function (canvas, clicked, double_click, timeout) {

            ++clicks;
                if (clicks === 1) {
                    var timeoutCallback = function (e) {
                        if (clicks === 1) {
                            $(canvas).bind(&#039;mousemove&#039;, handler.clicked);
                        } else {
                            $(canvas).bind(&#039;mousemove&#039;, handler.double_click);
                        }
                        clicks = 0;
                };
                    timeoutCallback.bind(this, e).delay(timeout / 1000);
                            }
              return false;
          },

//after that, the rest of the functions, included clicked and double_click

And in the part where we're listening, i have:

1
2
3
4
5
canvas.mousemove(handler.over_node);
canvas.mousemove(handler.over_edge);
canvas.mousedown(handler.observeSingleAndDoubleClick);
//canvas.mousedown(handler.clicked);
//canvas.dblclick(handler.double_click);

When I comment the two last lines, it doesn't work, but if I comment the function observeSingleAndDoubleClick, and uncomment the latest, it works, but still I cannot distinguish between single and double click.

Any ideas about what I might be doing wrong? The functions clicked and double_click work fine separatedly.

Thanks

Dámaris.

Matt on April 7 11 / 96 Permalink

Hi Dámaris,

Thanks for the code again.

The problem you are having, is it a custom set of events you are applying to the canvas element or are you using jQuery to add the events? If you haven’t tried already it may be worth trying to use the jQuery dblclick(). I must admit I’ve never tried applying a single and double click event to the same element… very good question. I’ll have a play and see what I come up with.

Dámaris on April 12 11 / 101 Permalink

Hi Matt,

I’m looking at forums and just realising I’m a long way off getting jQuery round my head…I’m actually starting to learn it (your demo was my first lesson =P ) and still getting there…

This is what I’m doing to distinguish single and double-click:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
initMouseHandling:function(){
                        var dragged = null,
                        _mouseP,
                        selected,
                        nearest = null,
                        show = true,
                        num_console = 0,
                        timeout,
                        clicks,
                        delay = 500;


    var handler =  {


     clicked:function(e){
    ...

   timeout = setTimeout(function() {
    // This inner function is called after the delay
    // to handle the 'click-only' event.
            alert('Click');
            timeout = null;
        }, delay)

    },

    double_click:function(e){
    ...

     if (timeout) {
            // Clear the timeout since this is a double-click and we don't want
            // the 'click-only' code to run.
            clearTimeout(timeout);
            timeout = null;
        }
        // Double-click handling code goes here.
        alert('Double-click');

    }}

    canvas.mousemove(handler.over_node);
    canvas.mousemove(handler.over_edge);
    canvas.mousedown(handler.clicked);
    canvas.dblclick(handler.double_click);
    }}

But still get both the single and double message when double-clicking. Any ideas of what I might be doing wrong? I don’t know if I should make callbacks as I’ve seen in other solutions, but this one seems easier,…

I have asked about this in a forum, going two different ways, hope it helps:

http://stackoverflow.com/questions/4993035/distinguish-between-single-and-doubleclick/5634422#5634422

http://stackoverflow.com/questions/1472433/how-can-jquery-be-used-to-handle-timer-in-click-dblclick-separation/5634722#5634722

Thanks

Dámaris.

Shehbaz on May 24 11 / 143 Permalink

Matt,

I am trying to build visual thesaurus. I am able to make it work on all the browsers but IE (7,8). I have checked and arbor.js have many syntax errors like trailing comma’s, missing semicolons etc. Have you tested your example on IE (7,8) ?
I am trying to fix all the js problems but it seems a long job so in the meantime I would like to know if you are able to find a solution. Thanks.

Matt on May 24 11 / 143 Permalink

Hi Shehbaz,
I must admit I never tested the example in IE, I actually assumed it just wouldn’t work. As it was really a throw-away project I never took the time to look into it.

Even the arbor.js webpage doesn’t run any of the demos so it looks like Christian Swinehart the author of arbor.js never tested for IE either. I see no mention of browsers supported by the library so I very much doubt IE is on there. Unfortunately it could be quite a major job getting it to work in IE.

Shehbaz on May 27 11 / 146 Permalink

Hi Matt,

It appears that the swingy.js (from where arbor is mostly inspired/taken) have the same problem, Doesnt work on any IE :) I have pin pointed the problem but I am unable to solve it. Just for your knowledge the issue is with __getter__ and __setter__ IE doesnt like it. And if you change it to createProperty (which IE accepts) then it starts failing on Node.prototype. They are in the atoms.js or line 35 or arbor.js.
If I am able to fix,which btw have little hope , I will share it.

Matt on March 2 12 / 61 Permalink

Hi Gitesh,
The demo doesn’t actually use springy.js for the graphing, but thanks for letting me know as I hadn’t heard about springy. I’ll have a look and see how it compares.

Todd on March 20 12 / 79 Permalink

I setup labels for all my edges and enclosed those labels in a rectangle. But I don’t see how to collect the edge.name when I click in the area with the rectangle and label. I can see how nodes can be evaluated, just not edges.

Thanks!

Matt on March 23 12 / 82 Permalink

Hi Todd,
As an alternative to arbour.js you may want to give d3.js a try. It looks to have a much simpler API. Arbour.js is great but the support isn’t there if you run into an issue. If I had more time I would use d3 in my demo rather than arbour.js.

Daphne on January 21 13 / 20 Permalink

Hello, Matt.
Do you know how arbor system assigns the initial position values for those nodes? It seems to be random since every time I reload the page, nodes’ positions are different. I have the requirement that every node should stay in the positions before the window is closed. My current solution is to store position data in cookie, and when browser loads again, read the cookie data and assign initial values for node.p. I try to put the node position assignment logic in a button click event, but browser reports increasing errors like a endless loop. But assignment for only one node, no problem. Can you please give me some suggestion how to solve my problems?

Leave a Comment

Your email will not be published. Required fields are marked *