Wednesday, September 23, 2015

Drawing graphs with vis.js

First of all I want to give a shout out to the guys who made amazing visjs library - it is a great thing. It has a ton of different features, I used only network, but there are much more out there.

We had a task recently: our customer works with a chain of suppliers and factories often related to each other, and all this data is stored in the database. We had to find a way to visualize data and display it as user friendly graph. And we found vis.js :) So we took our data, mapped it properly and got this with no effort at all:

Let's take a look on how it’s done.

In vis.js network there are nodes and edges and all you need to create your first network is basically this:


        var data = {
        nodes:
            [
                {id:1, label: "Entity 1"},
                {id:2, label: "Entity 2"},
                {id:3, label: "Entity 3"},
            ],
        edges:
            [
                {"from":1,"to":2,"arrows":"to"},
                {"from":2,"to":3,"arrows":"to"},
                {"from":3,"to":1,"arrows":"to"},
            ]
        };
        
        new vis.Network($("#container")[0], data, {});

That’s all, network is ready:

In my case I made a little wrapper in order to refresh graph and fetch data easily (some parts of code are cut. There is full version available to download at the end of the post):

'use strict';

window.Graph = window.Graph || {};

window.Graph.GraphManager = new function () {
    var self = this;

    var selectors = {
        graphContainerSelector: "#sp-graph-container",
        btnExportImageSelector: "#btnExportImage"
    };

    var $cache = {};

    var options = {};

    // vis.js network object
    var network = null;
    
    this.init = function (settings) {

        $.extend(true, options, settings);

        $cache.graphContainer = $(selectors.graphContainerSelector);
       /* ...cut… */
    };

    this.getGraphData = function () {
    
    /* ... cut ...*/

        var result = Graph.DataManager.getData(true);
               
        var data = mapData(result);

        var dsNodes = new vis.DataSet(data.nodes);

        var dsEdges = new vis.DataSet(data.edges);


        var nData = {
            nodes: dsNodes,
            edges: dsEdges
        };
        
        return nData;
    };

    // draw image
    this.drawGraph = function(data) {

        // if no network - init it and draw,
        // otherwise update network data
        if (network == null) {
            initNetwork(data);
            network.physics.options.enabled = false;
        } else {
            network.physics.options.enabled = true;
            network.setData({ nodes: data.nodes, edges: data.edges });
            network.physics.options.enabled = false;
        }
    };

    function initNetwork(data) {

        network = new vis.Network($cache.graphContainer[0],
        data, options.graphSettings);

        network.on("click", function (params) {

            if (params.nodes.length > 0) {
                alert("node id: " + params.nodes[0]);
            }

        });
    };
    
 /* … cut … */

    // create proper edges and nodes out of received data
    function mapData(jsonData) {

    /*... cut....*/

        var result = {
            nodes: jsonData.Entities,
            edges: jsonData.Relations
        };

        return result;
    }
};

What happens in here: getGraphData function is responsible for data retrieval, drawGraph renders (or re-renders) network. Notice that network is created only once, and if data was changed you can simply replace it in network in order to re-render it. mapData function is responsible for converting raw data into appropriate network data arrays. And graph data for this example stored in data.js file, in real app we retrieved it using synchronous ajax request.

We already able to render the network but if we do so it will look like this:

Nodes are huddled and the network looks messy. This is where visjs settings come in handy. If you take a look at the drawGraph function once again you will notice that I manually set network.physics.options.enabled option there. The trick here is that you can use network physics options as well to control space between nodes and to reduce the amount of the overlapped nodes. But once the graph is rendered I don’t want it to have its physics enabled anymore, so I disable it.

Along with the physics settings there are settings for edges and groups. Now let’s use GraphManager with proper options and draw graph again:

$(function () {        
        var options = {
            // url to get graph data from
            // (empty, since I use local data from data.js)
            graphDataUrl: '',
            graphSettings:
            {
                edges: {
                    "smooth": {
                        "type": "continuous",
                        "forceDirection": "none",
                        "roundness": 0
                    }
                },
                physics: {
                    enabled:true,
                    "barnesHut": {
                        "gravitationalConstant": -10000,
                        "centralGravity": 0,
                        "damping": 1,
                        "avoidOverlap": 1,
                        "springLength": 150,
                    },
                    "maxVelocity": 0.5,
                    "minVelocity": 0,
                    "timestep": 1
                },
                groups: {
                    Factory: {color: { background: 'Green' }},
                    Supplier: { color: { background: 'LightBlue' } },
                    SubContractor: { color: { background: 'Orange' } },
                    Agent:{color:{background:'Red'}},
                    SellingAgent:{color:{ background: 'Red'}},
                    Archived: { color: { background: 'Gray' } },
                    RequireApprove:{color:{background:'Pink'}}
                }
            }
        };
    
        Graph.GraphManager.init(options);
        var data = Graph.GraphManager.getGraphData();

        Graph.GraphManager.drawGraph(data);
        
    });

And we got this:

Still messy :) but the nodes are spread and since they are draggable it is much easier to adjust them as the user wants before he downloads it as image.

Once again, thanks visjs team for this amazing library.

Download the code for this article here.

1 comment :

  1. Hi Dennis, great article. Don't suppose you have a copy of the code with the get data remotely part working?

    ReplyDelete