Skip to content

Instantly share code, notes, and snippets.

@XavierGimenez
Last active July 13, 2023 08:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save XavierGimenez/9d4423c12dd2ed3804ac4e5736dccdf5 to your computer and use it in GitHub Desktop.
Save XavierGimenez/9d4423c12dd2ed3804ac4e5736dccdf5 to your computer and use it in GitHub Desktop.
Chord Diagram with vega.js
license: bsd-3-clause

Chord diagram with vega.js

WIP: Recreating the D3 Chord Diagram with vega.js

Actually, the grammar does not provide any vega transform (layout/hierarchy transforms) to create chord diagrams, so data has been generated from d3 using d3.chord() to compute the chord layout and d3.ribbon() to generate the ribbons:

chord = d3.chord().padAngle(0.05).sortSubgroups(d3.descending)
ribbon = d3.ribbon().radius(innerRadius)
var chords = chord(data);
console.log(JSON.stringify(chords.groups));
console.log(
    JSON.stringify(
        chords.map(function(chord) { return {'path' : ribbon(chord)} ; })
    )
);

feeding the data into the vega specifications, is relatively straighforward to generate the diagram.

Next steps:

Vega.js can be extended, so a new Data Transform could be implemented in order to do all the data transformation, so we can rely only on vega spec to create the diagram.

{
"$schema": "https://vega.github.io/schema/vega/v3.0.json",
"height": 700,
"width": 700,
"signals": [
{
"name": "originX",
"value": 0
},
{
"name": "originY",
"value": 0
},
{
"name": "inner_radius",
"value": 270
},
{
"name": "outer_radius",
"value": 290
}
],
"data": [
{
"name": "chords",
"values": [
{
"index": 0,
"startAngle": 0,
"endAngle": 1.8024478065173115,
"value": 29630
},
{
"index": 1,
"startAngle": 1.8524478065173116,
"endAngle": 3.0830761941597418,
"value": 20230
},
{
"index": 2,
"startAngle": 3.1330761941597416,
"endAngle": 5.583991554422396,
"value": 40290
},
{
"index": 3,
"startAngle": 5.633991554422396,
"endAngle": 6.233185307179585,
"value": 9850
}
],
"transform": [
{
"type": "formula",
"expr": "(((datum.startAngle + datum.endAngle) / 2) * 180 / PI) - 90",
"as": "angle_degrees"
},
{
"type": "formula",
"expr": "PI * datum.angle_degrees / 180",
"as": "radians"
},
{
"type": "formula",
"expr": "inrange(datum.angle_degrees, [90, 270])",
"as": "leftside"
},
{
"type": "formula",
"expr": "originX + outer_radius * cos(datum.radians)",
"as": "x"
},
{
"type": "formula",
"expr": "originY + outer_radius * sin(datum.radians)",
"as": "y"
},
{
"type": "identifier",
"as": "id"
}
]
},
{
"name": "ribbonsPaths",
"values": [
{
"path": "M1.6532731788489267e-14,-270A270,270,0,0,1,179.74503477833923,-201.47387540952704Q0,0,1.6532731788489267e-14,-270Z"
},
{
"path": "M257.9441963123624,-79.77964394987895A270,270,0,0,1,269.55864233110634,15.431731743737712Q0,0,47.593852285621686,265.7721302631531A270,270,0,0,1,15.79042891013979,269.5378681277156Q0,0,257.9441963123624,-79.77964394987895Z"
},
{
"path": "M179.74503477833923,-201.47387540952704A270,270,0,0,1,257.9441963123624,-79.77964394987895Q0,0,-250.3025045637076,-101.2356469290098A270,270,0,0,1,-173.7722230248407,-206.6475610913543Q0,0,179.74503477833923,-201.47387540952704Z"
},
{
"path": "M269.55864233110634,15.431731743737712A270,270,0,0,1,262.7879131272173,61.98800459961664Q0,0,-61.298411065465004,-262.94962407436384A270,270,0,0,1,-44.988658091159294,-266.225507123485Q0,0,269.55864233110634,15.431731743737712Z"
},
{
"path": "M259.3613876898222,75.04445732770499A270,270,0,0,1,169.33420199841783,210.29961491538455Q0,0,259.3613876898222,75.04445732770499Z"
},
{
"path": "M2.2994162497374937,269.99020849821653A270,270,0,0,1,-223.2692658242784,151.8250142048Q0,0,80.43841422887775,257.73952261215874A270,270,0,0,1,47.593852285621686,265.7721302631531Q0,0,2.2994162497374937,269.99020849821653Z"
},
{
"path": "M169.33420199841783,210.29961491538455A270,270,0,0,1,80.43841422887775,257.73952261215874Q0,0,-44.988658091159294,-266.225507123485A270,270,0,0,1,-28.883747501993874,-268.45060836258324Q0,0,169.33420199841783,210.29961491538455Z"
},
{
"path": "M-223.2692658242784,151.8250142048A270,270,0,0,1,-268.5114966715347,28.312120288180456Q0,0,-223.2692658242784,151.8250142048Z"
},
{
"path": "M-268.5114966715347,28.312120288180456A270,270,0,0,1,-250.3025045637076,-101.2356469290098Q0,0,-28.883747501993874,-268.45060836258324A270,270,0,0,1,-13.494375703083628,-269.6625703066409Q0,0,-268.5114966715347,28.312120288180456Z"
},
{
"path": "M-163.22697956031422,-215.0742967990754A270,270,0,0,1,-61.298411065465004,-262.94962407436384Q0,0,-163.22697956031422,-215.0742967990754Z"
}
]
}
],
"marks": [
{
"type": "arc",
"from": {
"data": "chords"
},
"encode": {
"enter": {
"fill": {
"value": "#aa0000"
},
"x": {
"signal": "0 * width/2"
},
"y": {
"signal": "0 * height/2"
}
},
"update": {
"startAngle": {
"field": "startAngle"
},
"endAngle": {
"field": "endAngle"
},
"padAngle": {
"value": 0
},
"innerRadius": {
"signal": "inner_radius"
},
"outerRadius": {
"signal": "outer_radius"
}
}
}
},
{
"type": "text",
"from": {
"data": "chords"
},
"encode": {
"enter": {
"text": {
"signal": "'Block ' + datum.id"
}
},
"update": {
"x": {
"field": "x"
},
"y": {
"field": "y"
},
"dx": {
"signal": "(datum.leftside ? -1 : 1) * 6"
},
"angle": {
"signal": "datum.leftside ? datum.angle_degrees - 180 : datum.angle_degrees"
},
"align": {
"signal": "datum.leftside ? 'right' : 'left'"
}
}
}
},
{
"type": "path",
"from": {
"data": "ribbonsPaths"
},
"encode": {
"enter": {
"fill": {
"value": "#c0c0c0"
},
"opacity": {
"value": 0.25
},
"stroke": {
"value": "black"
}
},
"update": {
"path": {
"field": "path"
}
}
}
}
]
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/vega@4"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@3"></script>
</head>
<body>
<div id="vis"></div>
<script>
const spec = "chord.vg.json";
vegaEmbed('#vis', spec, {defaultStyle: true});
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment