Tag Archive for 'rgraph'

Visualizing Linux package dependencies

I’ve been building a Linux package dependency visualizer with Python and the JavaScript Infovis Toolkit that gathers all dependencies for a linux package and displays them in an interactive tree visualization.

So, let’s say your query is wine and you want to see dependencies for that package. The visualization will display wine as the centered node, laying its dependencies on outer concentric circles like this:

rg1

By clicking on xbase-clients you’ll set this node as root:

rg2

Then, the visualization will query for xbase-clients dependencies, morphing its state into the new node’s perspective:

rg3

You can play with the example here.

I’ll explain how to build this in case you want your own at home.
I guess this is going to be also a nice tutorial on how to configure the RGraph visualization to run advanced examples, including the new morphing animations in version 1.0.7a.

Server Side

Server side we need to build a service that can transform the apt-rdepends output for package dependencies into a JSON tree structure.

The apt-rdepends is a linux tool (which you can install with apt-get install apt-rdepends) that displays a hierarchy of package dependencies for a given package. Here’s an example when querying for erlang:

cmd1

You can either use popen2 or commands.getoutput to fetch the output for a system call in Python, I’ll do the latter.
The main function that makes the system call and returns the answer could be something like this:

def get_dependency_tree(package=''):
    out = commands.getoutput("apt-rdepends " + package).split("\n")
    ans = []
    #if dependencies were found for this package.
    if len(out) > 3 and out[3].strip() == package:
        ans = out[3:]
    else:
        ans = [package]
    return make_tree(package=ans[0].strip(), source=ans, level=2)

The make_tree function will create the tree structure that will then be serialized into JSON to be processed client side.

We will first need a make_tree_node function that creates a tree node structure from a package’s name:

#returns a tree node
def make_tree_node(id, node_name):
    node_name = node_name.strip()
    return {
            'id': id,
            'name': node_name,
            'children': [],
            'data': []
    }

As you can see, this is the same tree node as the JSON tree structure defined for the JIT:

var json = {
	"id": "aUniqueIdentifier",
	"name": "usually a nodes name",
	"data": [
	    {key:"some key",       value: "some value"},
		{key:"some other key", value: "some other value"}
	],
	children: [/* other nodes or empty */]
};

Our make_tree function will receive as formal parameters the root package, the response from the apt-rdepends call, an integer that will specify the max depth for the tree (in case we want to prune it to some level) and an id prefix that will be set for each node:

def make_tree(package='', source=[], level=1, prefix=''):
    node = make_tree_node(package + '_' + prefix, package)
    if level > 0:
        deps = get_package_deps(package, source)
        [node['children'].append(make_tree(elem, source, level -1, package)) for elem in deps]
    return node

As you can see, make_tree recursively creates nodes and appends them to their parent children property.

Finally, I also made a get_package_deps function that retrieves all children for a given package, parsing source:

def get_package_deps(package_name='', source=[]):
    ans, found_package_name = [], False
    #test if is a dependency line
    dependency = lambda package: package.strip().startswith('Depends:')
    for line in source:
        #package name line
        if not found_package_name and package_name == line.strip():
            found_package_name = True
        #it's a package dependency, add its name to the answer
        elif found_package_name and dependency(line):
            ans.append(line.split("Depends: ")[1].split("(")[0].strip())
        #end of dependency lines
        elif found_package_name and not dependency(line):
            return ans
    return ans

If you used Django, then you could expose your service in the views.py file like this:

def apt_dependencies(request, mode, package):
    json = aptdependencies.get_dependency_tree(package)
    json_string = simplejson.dumps(json)
    return render_to_response('raw.html', { 'json' : json_string })

Client Side

All the JavaScript Infovis Toolkit visualizations are customizable via controller methods.
If this is the first time you use this library, perhaps it would be better to start with the RGraph quick tutorial first.

First we define a simple Log object, that will write the current state of the graph to a label (like loading… or stuff like that).

I’ll use Mootools, but you can use whatever you want.

var Log = {
	elem: false,
	getElem: function() {
		return this.elem? this.elem : this.elem = $('log');
	},

	write: function(text) {
		var elem = this.getElem();
		elem.set('html', text);
	}
};

Then we can define an init function, that instanciates the RGraph object and returns it.
We will pass a controller to this object, that implements the onBeforeCompute, onAfterCompute, onPlaceLabel and onCreateLabel methods.
I’ll also define some utility methods, like requestGraph and preprocessTree:

function init() {
  //Set node radius to 3 pixels.
  Config.nodeRadius = 3;

  //Create a canvas object.
  var canvas= new Canvas('infovis', '#ccddee', '#772277');

  //Instanciate the RGraph
  var rgraph= new RGraph(canvas,  {
	//Here will be stored the
	//clicked node name and id
  	nodeId: "",
  	nodeName: "",

  	//Refresh the clicked node name
	//and id values before computing
	//an animation.
	onBeforeCompute: function(node) {
  		Log.write("centering " + node.name + "...");
		this.nodeId = node.id;
  		this.nodeName = node.name;
  	},

  //Add a controller to assign the node's name
  //and some extra events to the created label.
  	onCreateLabel: function(domElement, node) {
  		var d = $(domElement);
  		d.setOpacity(0.6).set('html', node.name).addEvents({
  			'mouseenter': function() {
  				d.setOpacity(1);
  			},
  			'mouseleave': function() {
  				d.setOpacity(0.6);
  			},
  			'click': function() {
				if(Log.elem.innerHTML == "done") rgraph.onClick(d.id);
  			}
  		});
  	},

	//Once the label is placed we slightly
	//change the positioning values in order
	//to center or hide the label
  	onPlaceLabel: function(domElement, node) {
		var d = $(domElement);
		d.setStyle('display', 'none');
		 if(node._depth <= 1) {
			d.set('html', node.name).setStyles({
				'width': '',
				'height': '',
				'display':''
			}).setStyle('left', (d.getStyle('left').toInt()
				- domElement.offsetWidth / 2) + 'px');
		}
	},

	//Once the node is centered we
	//can request for the new dependency
	//graph.
	onAfterCompute: function() {
		Log.write("done");
		this.requestGraph();
	},

	//We make our call to the service in order
	//to fetch the new dependency tree for
	//this package.
   	requestGraph: function() {
  		var that = this, id = this.nodeId, name = this.nodeName;
  		Log.write("requesting info...");
  		var jsonRequest = new Request.JSON({
  			'url': '/service/apt-dependencies/tree/'
					+ encodeURIComponent(name) + '/',

  			onSuccess: function(json) {
  				Log.write("morphing...");
				//Once me received the data
				//we preprocess the ids of the nodes
				//received to match existing nodes
				//in the graph and perform a morphing
				//operation.
  				that.preprocessTree(json);
				GraphOp.morph(rgraph, json, {
  					'id': id,
  					'type': 'fade',
  					'duration':2000,
  					hideLabels:true,
  					onComplete: function() {
						Log.write('done');
					},
  					onAfterCompute: $empty,
  					onBeforeCompute: $empty
  				});
  			},

  			onFailure: function() {
  				Log.write("sorry, the request failed");
  			}
  		}).get();
  	},

	//This method searches for nodes that already
	//existed in the visualization and sets the new node's
	//id to the previous one. That way, all existing nodes
	//that exist also in the new data won't be deleted.
 	preprocessTree: function(json) {
  		var ch = json.children;
  		var getNode = function(nodeName) {
  			for(var i=0; i<ch.length; i++) {
  				if(ch[i].name == nodeName) return ch[i];
  			}
  			return false;
  		};
  		json.id = rgraph.root;
		var root = rgraph.graph.getNode(rgraph.root);
  		GraphUtil.eachAdjacency(root, function(elem) {
  			var nodeTo = elem.nodeTo, jsonNode = getNode(nodeTo.name);
  			if(jsonNode) jsonNode.id = nodeTo.id;
  		});
  	}

  });

  return rgraph;
}

I did say advanced example.
You can always go to a simpler example to begin here.

Finally we have to initialize the visualization when the page loads, so we’ll attach an initialization function like this:

window.addEvent('domready', function() {
	var rgraph = init();
	new Request.JSON({
	  	'url':'/service/apt-dependencies/tree/wine/',
	  	onSuccess: function(json) {
			  //load wine dependency tree.
			 rgraph.loadTreeFromJSON(json);
			  //compute positions
			  rgraph.compute();
			  //make first plot
			  rgraph.plot();
			  Log.write("done");
			  rgraph.controller.nodeName = name;
	  	},

	  	onFailure: function() {
	  		Log.write("failed!");
	  	}
	}).get();

HTML and CSS

These are the HTML and CSS files I used to make this example/tutorial.
The HTML:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>

Linux package dependency visualizer

</title>
<link type="text/css" href="/static/css/style.css" rel="stylesheet" />
<script type="text/javascript" src="/static/js/mootools-1.2.js"></script>

<!--[if IE]>
<script language="javascript" type="text/javascript" src="/static/js/excanvas.js"></script>
<![endif]-->
<script language="javascript" type="text/javascript" src="/static/js/core/RGraph.js"></script>
<script language="javascript" type="text/javascript" src="/static/js/example/example-rgraph.js"></script>

</head>

<body onload="">

<canvas id="infovis" width="900" height="500"></canvas>
<div id="label_container"></div>

</body>
</html>
<div id="log"></div>

Note: You’ll probably have to change the path to the CSS and JavaScript files.

and the CSS file:

html,body {
	width:100%;
	height:100%;
	margin:0;padding:0;
	background-color:#333;
	text-align:center;
	font-size:0.94em;
	font-family:"Trebuchet MS",Verdana,sans-serif;
}

#infovis {
	width:900px;
	height:500px;
	background-color:#222;

}

.node {
	color: #fff;
	background-color:#222;
	font-weight:bold;
	padding:1px;
	cursor:pointer;
	font-size:0.8em;
}

.hidden {
	display:none;
}

Remarks

Although still in alpha, the JavaScript Infovis Toolkit can be used to perform advanced animations, customizing your visualization via a controller and not messing with the code.
This example also shows that it can be used to do more advanced things that only plotting static animations, interacting with services and handling pretty well visualizations where the dataset changes over time.
You can download the library here, latest version is 1.0.7a.
You can also go to the main project page to know more.

Hope it was useful.
Feel free to post any comment or questions.
Bye!

Weighted nodes, weighted edges

I’ve been doing some work on the JIT lately, fixing some bugs and adding some new features.
Some important changes to mention are:

  • A complete refactoring of the Spacetree. That code was not clear. I also binded the ST to the Animation object used by the Hypertree and RGraph. This allows it to have easeIn and easeOut transitions. I also updated the documentation for this class.
    Main functionality is now packaged under the Tree Class, making a good distinction between generic code (say Tree.Util for example) and more specific code (like the Tree.Geometry object, or Tree.Label).
    Here’s the old example of an “infinite” spacetree, done with the “new code”.
  • Hypertree and RGraphs can now handle weighted nodes and edges in trees and graphs. The same goes for the Spacetree, although I have not tested that yet.

    Weighted nodes:
    This goes only for the RGraph and the Hypertree, since I don’t see a clear representation of weighted nodes in the Spacetree, and the Treemap already represents weight either with size or color.
    Weighted nodes are enabled when setting Config.allowVariableNodeDiameters = true.
    Remember what a dataset is? A dataset is a reference to the property data of a JSON node representation. There you can store data by appending { key:’someKey’, value:’someValue’} objects to the data array property.
    The value of the first object of the data property will be taken in account to calculate the nodes diameters. You will also want to specify two properties of the config object, the nodeRangeDiameters property:

    		//Property: nodeRangeDiameters
    		//Diameters range. For variable node weights.
    		nodeRangeDiameters: {
    			min: 10,
    			max: 35
    		},
    

    Which specifies the specific range of your nodes diameters. Nodes will range from 10px to 35px as default. The other property is the nodeRangeValues:

    		//Property: nodeRangeValues
    		// The interval of the values of the first object of your dataSet.
    		// A superset of the values can also be specified.
    		nodeRangeValues: {
    			min: 1,
    			max: 35
    		},
    

    You’ll need to specify range values for your first dataset object value. This is all the information we need to know in order to plot a RGraph or Hypertree with weighted nodes. Here’s an example of a K6 Hypergraph with variable node diameters (and weighted edges).

    A note on usability
    There’s an extra property for the Hypertree Config object called Config.transformNodes. When applying a moebius transformation of the tree (that is, when the tree moves), tree nodes and edges change their proportion. This is not a good feature if you’re planning to add weighted nodes in your Hypertree, since they will be deformed and thus the user won’t be able to tell which node is bigger than which. You can set this property to false when using weighted nodes on your visualization.

    Weighted edges
    Two new methods have been included in the controller object, these are onBeforePlotLine(adj); and onAfterPlotLine(adj);. Both receive an adjacency object, which has the following structure:

    var adj = {
    	nodeFrom: ""/* A node connected with this adjacence */,
    	nodeTo: ""/* Other node connected with this adjacence */,
    	data: { //An object storing your custom information.
    		weight: w
    	}
    };

    Use the two controller methods to change the canvas lineWidth property or the stroke color (more information on that here). For example,

    /* some code... */
    
     var rgraph= new RGraph(canvas,  {
      	//Use onBeforePlotLine and onAfterPlotLine controller
      	//methods to adjust your canvas lineWidth
      	//parameter in order to plot weighted edges on
      	//your graph. You can also change the color of the lines.
      	onBeforePlotLine: function(adj) {
      			lineW = canvas.getContext().lineWidth;
      			canvas.getContext().lineWidth = adj.data.weight;
      	},
    
      	onAfterPlotLine: function(adj) {
      			canvas.getContext().lineWidth = lineW;
      	},
    
    /* some other code*/
    

    Ok, but how do I store edge weights?
    The JSON Graph structure has been extended to the following form (notice that the old Graph structure is still accepted).

    var json = [
    {
    	"id": "aUniqueIdentifier",
    	"name": "usually a nodes name",
    	"data": [
    	    {key:"some key",       value: "some value"},
    		{key:"some other key", value: "some other value"}
    	],
    	"adjacencies": [
    	{
    		nodeTo:"aNodeId",
    		data: {} //put whatever you want here
    	}
    	//more objects like these...
    	]
    } /* ... more nodes here ... */ ];
    

    JSON Tree structures
    For JSON Trees is simpler. If you have a Node A and B is a child of A, then store in Bs dataset a property concerning the weight of the edge A-B. These nodes will be stored in the adj object onBeforePlotLine and onAfterPlotLine. You can access them by doing adj.nodeFrom and adj.nodeTo.

    Here’s an example of a K6 RGraph with weighted nodes and edges.

  • The last example also shows a new feature for the RGraph, polar interpolation. You will notice that node transitions are different from previous examples. You can change the interpolation by setting Config.interpolation to ‘polar’ or ‘linear’. I’ll make a more detailed explanation for polar interpolation in some other post. If you want to know more about the cool features of the paper inspiring the RGraph, you can see this post.
  • API Changes
    These features introduced an api change that has already been updated in all tutorials, although I have not checked for errors yet (will do today and/or tomorrow). You should not set the controller property from the ST, RGraph, Treemaps and Hypertree instances. That is, you can’t do:

    var st = new ST(canvas);
    st.controller = ; //the controller object
    

    Instead, you should do:

    var st = new ST(canvas, controller);
    
  • I updated all examples packaged with the library, also adding the two K6 examples showed above. Code that depends on the Mootools library (that is, the example files and the Treemap visualization) has been updated to the final 1.2 version of the Mootools library. This library is shipped as an extra with the JIT.

Special thanks to Rene Becker for pointing bugs and Daniel Herrero for suggesting cool performance improvements.
Remember that you can post any question, suggestion or comment on the JIT google group.

Get the library already!
Bye!

Feeding JSON graph structures to the JIT

Version 1.0.3a of the JIT allows you to load graph structures to the RGraph and Hypertree objects. I chose a different JSON structure for graphs, since JSON tree structures don’t seem conceptually suitable for this task.
Hypertree and RGraph objects have a new method called loadGraphFromJSON(json [,i]) that takes a graph structure (described below) and optionally an index to set a particular node as root for the visualization. Please refer to the documentation for more information.

The graph structure

The JSON graph structure is an array of nodes, each having as properties:

  • id a unique identifier for the node.
  • name a node’s name.
  • data The data property contains a dataset. That is, an array of key-value objects defined by the user. Roughly speaking, this is where you can put some extra information about your node. You’ll be able to access this information at different stages of the computation of the JIT visualizations by using a controller.
  • adjacencies An array of strings each representing a nodes id.

For example,

var json = [
{
	"id": "aUniqueIdentifier",
	"name": "usually a nodes name",
	"data": [
	    {key:"some key",       value: "some value"},
		{key:"some other key", value: "some other value"}
	],
	"adjacencies": ["anotherUniqueIdentifier", "yetAnotherUniqueIdentifier" /* ... */]
} /* ... more nodes here ... */ ];

I did a small example of a K6 rendered with a RGraph. The JSON graph structure used for this example is:

var json= [
    {"id":"node0",
     "name":"node0 name",
     "data":[
        {"key":"some key",
         "value":"some value"},
        {"key":"some other key",
         "value":"some other value"}],
     "adjacencies":["node1","node2","node3","node4","node5"]},
    {"id":"node1",
     "name":"node1 name",
     "data":[
        {"key":"some key",
         "value":"some value"},
        {"key":"some other key",
         "value":"some other value"}],
     "adjacencies":["node0","node2","node3","node4","node5"]},
    {"id":"node2",
     "name":"node2 name",
     "data":[
        {"key":"some key",
         "value":"some value"},
        {"key":"some other key",
         "value":"some other value"}],
     "adjacencies":["node0","node1","node3","node4","node5"]},
    {"id":"node3",
     "name":"node3 name",
     "data":[
        {"key":"some key",
         "value":"some value"},
        {"key":"some other key",
         "value":"some other value"}],
     "adjacencies":["node0","node1","node2","node4","node5"]},
    {"id":"node4",
     "name":"node4 name",
     "data":[
        {"key":"some key",
         "value":"some value"},
        {"key":"some other key",
         "value":"some other value"}],
     "adjacencies":["node0","node1","node2","node3","node5"]},
    {"id":"node5",
     "name":"node5 name",
     "data":[
        {"key":"some key",
         "value":"some value"},
        {"key":"some other key",
         "value":"some other value"}],
     "adjacencies":["node0","node1","node2","node3","node4"]}];

You can post any question at the google group for this project.
Enjoy!