Skip to main content

Arc Diagram

Vega Arc Diagram Example

Vega example​


NetPanorama example​

Vega specification​

This example feels awkward, due to the absence of semantic concepts that are present in NetPanorama:

With no concept of a network and network transform, there is no transform to calculate the node degree directly. Instead, a series of tabular transforms are used: 2 construct separate tables recording node in/out-degrees, and then a series of 4 transforms to combine these into an undirected degree to be saved as an attribute of the nodes. This approach is both awkward and brittle: it would be impossible to replace the degree with an alternative metric such as betweeness centrality.

With no concept of a layout, the example instead creates symbol marks that are made invisible by setting their opacity to 0. This approach only works for layouts where the x and y coordinates of entries can be easily obtained by directly applying an expression or scale.

The specification of arcs is also complicated: with no concept of a network, the specification must apply a pair of transforms: 1 to find the source and target nodes of each edge, and 1 to obtain their coordinates.


{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "An arc diagram depicting character co-occurrence in the novel Les Misérables.",
"width": 770,
"padding": 5,

import edges data, as would be done in NetPanorama



"data": [
{
"name": "edges",
"url": "/data/miserables.json",
"format": {"type": "json", "property": "links"}
},

Construct separate tables recording node in/out-degrees, by aggregating rows of the link table using a COUNT operation.


{
"name": "sourceDegree",
"source": "edges",
"transform": [
{"type": "aggregate", "groupby": ["source"]}
]
},
{
"name": "targetDegree",
"source": "edges",
"transform": [
{"type": "aggregate", "groupby": ["target"]}
]
},

Load the node data...



{
"name": "nodes",
"url": "/data/miserables.json",
"format": {"type": "json", "property": "nodes"},

...and save the degree as a field by applying a sequence of 4 transforms.


"transform": [
{ "type": "window", "ops": ["rank"], "as": ["order"] },
{
"type": "lookup", "from": "sourceDegree", "key": "source",
"fields": ["index"], "as": ["sourceDegree"],
"default": {"count": 0}
},
{
"type": "lookup", "from": "targetDegree", "key": "target",
"fields": ["index"], "as": ["targetDegree"],
"default": {"count": 0}
},
{
"type": "formula", "as": "degree",
"expr": "datum.sourceDegree.count + datum.targetDegree.count"
}
]
}
],


"scales": [

Define a band scale that positions nodes.



{
"name": "position",
"type": "band",
"domain": {"data": "nodes", "field": "order", "sort": true},
"range": "width"
},

Define a scale for node color.


{
"name": "color",
"type": "ordinal",
"range": "category",
"domain": {"data": "nodes", "field": "group"}
}
],



"marks": [

Fake a "layout" by drawing symbols with opacity 0, positioned using the position scale.


{
"type": "symbol",
"name": "layout",
"interactive": false,
"from": {"data": "nodes"},
"encode": {
"enter": {
"opacity": {"value": 0}
},
"update": {
"x": {"scale": "position", "field": "order"},
"y": {"value": 0},
"size": {"field": "degree", "mult": 5, "offset": 10},
"fill": {"scale": "color", "field": "group"}
}
}
},

Draw the arcs.


{
"type": "path",
"from": {"data": "edges"},
"encode": {
"update": {
"stroke": {"value": "#000"},
"strokeOpacity": {"value": 0.2},
"strokeWidth": {"field": "value"}
}
},
"transform": [
{
"type": "lookup", "from": "layout", "key": "datum.index",
"fields": ["datum.source", "datum.target"],
"as": ["sourceNode", "targetNode"]
},
{
"type": "linkpath",
"sourceX": {"expr": "min(datum.sourceNode.x, datum.targetNode.x)"},
"targetX": {"expr": "max(datum.sourceNode.x, datum.targetNode.x)"},
"sourceY": {"expr": "0"},
"targetY": {"expr": "0"},
"shape": "arc"
}
]
},

Draw a circular mark for each label.


{
"type": "symbol",
"from": {"data": "layout"},
"encode": {
"update": {
"x": {"field": "x"},
"y": {"field": "y"},
"fill": {"field": "fill"},
"size": {"field": "size"}
}
}
},

Draw label for each node.


{
"type": "text",
"from": {"data": "nodes"},
"encode": {
"update": {
"x": {"scale": "position", "field": "order"},
"y": {"value": 7},
"fontSize": {"value": 9},
"align": {"value": "right"},
"baseline": {"value": "middle"},
"angle": {"value": -90},
"text": {"field": "name"}
}
}
}
]
}

NetPanorama specification​

The equivalent NetPanorama code is more explicit.

We begin by loading tabular data in the same way, but perform some additional steps:

  • assembling the data into a network: this allows the source and target nodes of an edge to be accessed.
  • defining an ordering for the nodes
  • constructing a layout

This may seem to introduce complexity, but it provides two main benefitS:

  • it simplifies subsequent stages (such as calculating node metrics and rendering edges);
  • it provides additional flexibility (e.g., to calculate a different node metric, or change the ordering)

{

"x": 30,
"y": 400,

Load the 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"}
}
],

Join the node and link tables to construct a network.


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

Define scales to set node sizes and colors.


"scales": [
{
"name": "radius",
"type": "linear",
"range": [1, 200],
"domain": [0, 40]
},
{
"name": "color",
"type": "ordinal",
"domain": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
"scheme": "tableau10"
}
],

Define an ordering for the nodes...


"orderings": [
{
"name": "group_order",
"data": "le-mis-network.nodes",
"orderBy": "index"
}
],

...and use this to construct and layout.


"layouts": [
{
"name": "le-mis-layout",
"data": "le-mis-network.nodes",
"pattern": "linear",
"order": "group_order",
"orientation": "horizontal"
}
],


"vis": [

Draw the arcs.


{
"entries": "le-mis-network.links",
"layout": "le-mis-layout",
"mark": {
"type": "linkpath",
"start": "source",
"end": "target",
"shape": "arc",
"directionForShape": { "ordering": "group_order", "reverse": true },
"strokeOpacity": 0.2,
"strokeWidth": { "field": "value" }
}
},

Draw a circle for each node.


{
"entries": "le-mis-network.nodes",
"layout": "le-mis-layout",

"mark": {
"type": "circle",
"area": { "field": "degree", "scale": "radius" },
"fill": {"scale": "color", "field": "group"}
"tooltip": {"field": "name"}
}
},

Write the name for each node.


{
"entries": "le-mis-network.nodes",
"layout": "le-mis-layout",

"mark": {
"type": "text",
"text": { "field": "name" },
"align":"right",
"angle": -90,
"dy": 0,
"dx": -10
}
}


]
}