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
}
}


]
}