Skip to main content

Force-directed Node-Link Diagram

This is the Vega Force Directed Layout Example

Vega example



NetPanorama example

Vega specification

Layout algorithm is limited to the force-directed layout provided by d3-force, though the forces can be customized.

The layout is computed by a transform, which sets a signal; the specification for rendering the links includes "require": {"signal": "force"},, which implicitly constructs a network such that sourceX can be set to datum.source.x. In contrast, in NetPanorama networks are explicitly constructed, rather than being implicitly constructed as a side-effect of computing a layout, which allows a great deal of control.

The clusters are pre-computed in the dataset: if they were not, there would be no way to do the clustering using Vega.


{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "A node-link diagram with force-directed layout, depicting character co-occurrence in the novel Les Misérables.",
"width": 700,
"height": 500,
"padding": 0,
"autosize": "none",

Define signals, and bid to UI controls that can change them


"signals": [
{ "name": "cx", "update": "width / 2" },
{ "name": "cy", "update": "height / 2" },
{ "name": "nodeRadius", "value": 8,
"bind": {"input": "range", "min": 1, "max": 50, "step": 1} },
{ "name": "nodeCharge", "value": -30,
"bind": {"input": "range", "min":-100, "max": 10, "step": 1} },
{ "name": "linkDistance", "value": 30,
"bind": {"input": "range", "min": 5, "max": 100, "step": 1} },
{ "name": "static", "value": true,
"bind": {"input": "checkbox"} },
{
"description": "State variable for active node fix status.",
"name": "fix", "value": false,
"on": [
{
"events": "symbol:mouseout[!event.buttons], window:mouseup",
"update": "false"
},
{
"events": "symbol:mouseover",
"update": "fix || true"
},
{
"events": "[symbol:mousedown, window:mouseup] > window:mousemove!",
"update": "xy()",
"force": true
}
]
},
{
"description": "Graph node most recently interacted with.",
"name": "node", "value": null,
"on": [
{
"events": "symbol:mouseover",
"update": "fix === true ? item() : node"
}
]
},
{
"description": "Flag to restart Force simulation upon data changes.",
"name": "restart", "value": false,
"on": [
{"events": {"signal": "fix"}, "update": "fix && fix.length"}
]
}
],

Load the node and edge data



"data": [
{
"name": "node-data",
"url": "/data/miserables.json",
"format": {"type": "json", "property": "nodes"}
},
{
"name": "link-data",
"url": "/data/miserables.json",
"format": {"type": "json", "property": "links"}
}
],

Define color scale for the nodes


"scales": [
{
"name": "color",
"type": "ordinal",
"domain": {"data": "node-data", "field": "group"},
"range": {"scheme": "category20c"}
}
],


"marks": [

Render nodes


{
"name": "nodes",
"type": "symbol",
"zindex": 1,

"from": {"data": "node-data"},
"on": [
{
"trigger": "fix",
"modify": "node",
"values": "fix === true ? {fx: node.x, fy: node.y} : {fx: fix[0], fy: fix[1]}"
},
{
"trigger": "!fix",
"modify": "node", "values": "{fx: null, fy: null}"
}
],

"encode": {
"enter": {
"fill": {"scale": "color", "field": "group"},
"stroke": {"value": "white"}
},
"update": {
"size": {"signal": "2 * nodeRadius * nodeRadius"},
"cursor": {"value": "pointer"}
}
},

Apply a "force" transform to compute a force-directed layout.


"transform": [
{
"type": "force",
"iterations": 300,
"restart": {"signal": "restart"},
"static": {"signal": "static"},
"signal": "force",
"forces": [
{"force": "center", "x": {"signal": "cx"}, "y": {"signal": "cy"}},
{"force": "collide", "radius": {"signal": "nodeRadius"}},
{"force": "nbody", "strength": {"signal": "nodeCharge"}},
{"force": "link", "links": "link-data", "distance": {"signal": "linkDistance"}}
]
}
]


},

Render links


{
"type": "path",
"from": {"data": "link-data"},
"interactive": false,
"encode": {
"update": {
"stroke": {"value": "#ccc"},
"strokeWidth": {"value": 0.5}
}
},

"transform": [
{
"type": "linkpath",
"require": {"signal": "force"},
"shape": "line",
"sourceX": "datum.source.x", "sourceY": "datum.source.y",
"targetX": "datum.target.x", "targetY": "datum.target.y"
}
]



}
]
}

NetPanorama specification

The specification for the rendering of nodes and links is simplified by lifting the construction of the network and the computing of the layout to separate sections of the specification.


{

define parameters


"parameters": [
{
"name": "nodeRadius",
"bind": { "input": "range", "label": "Size", "min": "1", "max": "50", "step": "1" },
"value": 8
},
{
"name": "nodeCharge",
"bind": { "input": "range", "label": "Node charge", "min": "-600", "max": "0", "step": "1" },
"value": -400
},
{
"name": "linkDistance",
"bind": { "input": "range", "label": "linkDistance", "min": "5", "max": "200", "step": "1" },
"value": 30
},
],

Load the node and edge data...



"data": [
{
"name": "nodes",
"url": "/data/miserables.json",
"format": {"type": "json", "property": "nodes"},
"transform": [
{ "type": "calculate", "calculate": "index", "as": "id" }
]
},
{
"name": "links",
"url": "/data/miserables.json",
"format": {"type": "json", "property": "links"}
}
],

...and assemble into a network.


"networks": [
{
"name": "le-mis-network",
"nodes": "nodes",
"links": "links",
"directed": true,
"source_node": [ "index", "source" ],
"target_node": [ "index", "target" ]
}
],

Compute a force directed layout


"layouts": [
{
"name": "le-mis-layout",
"network": "le-mis-network",
"type": "d3-force",

"forces": [
{ "force": "center", "x": {"expression": "bounds.width / 2"}, "y": {"expression": "bounds.height/2"} },
{ "force": "collide", "radius": {"parameter": "nodeRadius"} },
{ "force": "nbody", "strength": {"parameter": "nodeCharge"} },
{ "force": "link", "distance": {"parameter": "linkDistance"} }
]
}
],

Define a color scale for the node.


"scales": [
{
"name": "color",
"type": "ordinal",
/* "domain": {"data": "node-data", "field": "group"}, */
"domain": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
/* "range": {"scheme": "category20c"}, */
"scheme": "category20c"
}
],


"vis": [

Render the links


{
"entries": "le-mis-network.links",
"layout": "le-mis-layout",
"mark": {
"type": "linkpath",
"start": "source",
"end": "target"
}
},

Render the nodes


{
"entries": "le-mis-network.nodes",
"layout": "le-mis-layout",
"mark": {
"type": "circle",
"area": { "expression": "2 * params.nodeRadius * params.nodeRadius" },
"fill": {"scale": "color", "field": "group"}
},
"actions": [
{ "interaction": "drag" }
]
}


]
}