4

I've been working on a Infovis toolkit project and though all functionality is done I haven't been able to finish the visuals. The Infovis toolkit API documentation is good but my custom node types don't work. I'm using a hypertree and I want make two different custom node types. One that's from an image and the other as a drawn path. All help is greatly appreciated, thanks!

EDIT: [ The solution I was trying turned out to be not so handy. Instead I used onCreateLabel() from the JIT controllers to customize the nodes with HTML. Saw a clear improvement in performance and got much more flexibility in customizing the nodes. ]

This is what I've come up with so far:

$jit.Hypertree.Plot.NodeTypes.implement({  
    'customNode': {  
         'render': function(node, canvas) {
            var img = new Image();
            img.src = "../icon.png";
            var pos = node.pos.getc(true);
            var ctx = canvas.getCtx();

            ctx.drawImage(img, pos.x-15, pos.y-15);                 
            /*
            //...And an other one like this but drawn as a path
            ctx.beginPath();
            ctx.moveTo(pos.x-25, pos.y-15);
            ctx.lineTo(25, -15);
            ctx.lineTo(-35, 0);
            ctx.closePath();
            ctx.strokeStyle = "#fff";
            ctx.fillStyle = "#bf5fa4";
            ctx.fill();
            ctx.stroke();*/
        }
       }
});
neutron
  • 15
  • 3
neutron
  • 41
  • 1
  • 3

4 Answers4

7

Because you're setting the image src to a file URL, it takes time to load the file. So the code is hitting the drawImage call before the image has loaded.

You can get around this by modifying your code to run the drawImage call in the onload event handler for the image (which runs once the image has finished loading).

$jit.Hypertree.Plot.NodeTypes.implement({  
    'customNode': {  
        'render': function (node, canvas) {
            var img = new Image(),
                pos = node.pos.getc(true),
                ctx = canvas.getCtx();

            img.onload = function () {
                ctx.drawImage(img, pos.x - 15, pos.y - 15);
            };

            img.src = '../icon.png';
        }
    }
});
thefrontender
  • 1,844
  • 14
  • 22
3

Answer given above is correct. However there are 2 drawbacks of it.

1: It does not have contains method without which, the custom node will not fire events like onMouseEnter or onClick etc.

2: Images will flicker every time you move any node in graph or any time you pan the whole graph. This is because an image is loaded every time render function is called to re-plot the node. This also affects performance because of the same reason. A better option is to have images loaded once for all nodes having 'image' type and just re-plot them in the render function. This will help performance and stop images from flickering.

We can have a function 'loadImages' which loads images in each node with 'type' as 'image'. Each node which is of type 'image' should also have a property called 'url' which is a url of the image to load.

'Data' section of such node in json data would look something like below.

"data": {
      "$color": "#EBB056",
      "$type": "image",
      "$dim": 7,
      "$url":"magnify.png"  // url of the image
    }

The loadImages function should be called right after the loadJson function i.e. right after loading json data.

// load JSON data.
fd.loadJSON(json);

//load images in node
loadImages();

Below is the implementation of loadImages function.

function loadImages(){
    fd.graph.eachNode( function(node){
        if( node.getData('type') == 'image' ){
            var img = new Image();
            img.addEventListener('load', function(){
                node.setData('image',img); // store this image object in node
            }, false);
            img.src=node.getData('url');
        }
    });
}

Finally, your custom node implementation would look something like below.

$jit.ForceDirected.Plot.NodeTypes.implement({
'image': { 
         'render': function(node, canvas){
                var ctx = canvas.getCtx(); 
                var pos = node.pos.getc(true);
                if( node.getData('image') != 0 ){
                var img = node.getData('image');
                ctx.drawImage( img, pos.x-15, pos.y-15);
                }
            }, 
            'contains': function(node,pos){ 
                var npos = node.pos.getc(true); 
                dim = node.getData('dim'); 
                return this.nodeHelper.circle.contains(npos, pos, dim); 
            } 
}
});
Pratik Patel
  • 1,305
  • 1
  • 17
  • 44
1

The answer given by Pratik did not quite work for my hypertree - there were some minor modifications I needed to make to get the node images to appear and scale appropriately in a hypertree representation. Hopefully this answer will assist other users with similar implementation trouble. disclaimer I am a novice Javascript programmer and it is likely that I missed something essential in Pratik's answer that would have made it work. Further, I would never have gotten close without his answer, for which I am grateful.

No change to the data definition in json - worked as expected. A key note for a me was to be sure to set overridable: true e.g.

var ht = new $jit.Hypertree({ 
  Node: {
      overridable: true,
      dim: 9,
      color: "#ffffff",
      type: "square"
    },

I needed the image height/width to scale them appropriately later. I implemented it in the loadImages function provided by Pratik so it would only run once:

function loadImages(){
    ht.graph.eachNode( function(node){
        if( node.getData('type') == 'image' ){
            var img = new Image();
            img.addEventListener('load', function(){
                node.setData('image',img); // store this image object in node
                node.setData('imageheight',this.height); 
                node.setData('imagewidth',this.width);
            }, false);
            img.src=node.getData('url');
        }
    });
}

No change to the timing of the loadImages() function call just after the loadJSON.

My custom node looked like this:

$jit.Hypertree.Plot.NodeTypes.implement({
    'image': { 
             'render': function(node, canvas){
                    var ctx = canvas.getCtx(); 
                    var pos = node.pos.getc().$scale(node.scale); 
                    var nconfig = this.node; 
                    if( node.getData('image') != 0 ){ 
                        var img = node.getData('image');
                        var dim = node.getData('dim');
                        var w = node.getData('imagewidth');
                        var h = node.getData('imageheight');
                        var r = 50/h; // Scaling factor to scale images to 50px height
                        w = r * w;
                        h = r * h;
                        var dist = Math.sqrt( (pos.x*pos.x)+(pos.y*pos.y)); //dist to origin
                        var scale = 1 - (dist/node.scale);
                        // scale nodes based on distance to origin
                        var sw = nconfig.transform ? w * scale : w/dim/2;
                        var sh = nconfig.transform ? h * scale : h/dim/2;
                        if (node._depth < 2) { 
                            ctx.drawImage(img, 
                                pos.x - sh/2, //center images on nodes
                                pos.y - sw/2, 
                                sw, 
                                sh);
                        }  
                    }  
              },  
              'contains': function(node,pos){ 
                    var npos = node.pos.getc().$scale(node.scale);
                    var h = node.getData('imageheight');
                    var w = node.getData('imagewidth');
                    return this.nodeHelper.rectangle.contains(npos, pos, w, h); 
              }
          }  
    });

The only thing outstanding issue is that the images don't appear until after I have clicked the chart. I think this has to do with img.addEventListener('load', function(){ but haven't been able to figure out what causes the trouble. Any thoughts are appreciated.

Josh
  • 11
  • 3
0

I don't know what I'm doing different from everyone else, or if the library has changed since the dinosaur age. But for whatever reason, getPos() and node.pos.getc(true) gives me a math based coordinate system and not the actual pixels needed when using HyperTree.

When using images, especially when using ctx.drawImage, JavaScript expects pixel-based coordinates. I'm no math genius so it took a while to figure out what the problem was and how to work around it. Multiplying the value given by node.pos.getc(true) with the canvas.width was the only workaround I found, which led me to do something like:

$jit.Hypertree.Plot.NodeTypes.implement({  
    'customNode': {
        'render': function (node, canvas) {
            let img = new Image(),
                pos = node.pos.getc(true),
                ctx = canvas.getCtx();

            img.onload = function () {
                ctx.drawImage(img, pos.x*canvas.Width -(imageWidth/2), pos.y*canvas.Height -(imageHeight/2))
            };

            img.src = '/resources/images/customImage.png';
        }
    }
});

Which should center the image at the expected position.

Torxed
  • 22,866
  • 14
  • 82
  • 131