Skip to content

Instantly share code, notes, and snippets.

@XavierGimenez
Last active November 23, 2018 12:35
Show Gist options
  • Save XavierGimenez/0cc94bc1bde3a55abe1c2cbea1134da0 to your computer and use it in GitHub Desktop.
Save XavierGimenez/0cc94bc1bde3a55abe1c2cbea1134da0 to your computer and use it in GitHub Desktop.
Stretched Chord with vega.js

Stretched Chord with vega.js

Porting the Stretched Chord by Nadieh Bremer to vega.js

WIP from previous block

So far, Mixing D3 for the data generation and vega.js for rendering and minimal logic. Once the spec is parsed, just populate signals/datasets and render the vega view.

Pending to evaluate extending Vega with a new Data Transform so we can generate Stretched Chords only with vega.js.

{
"$schema": "https://vega.github.io/schema/vega/v4.3.json",
"height": 700,
"width": 700,
"signals": [
{
"name": "originX",
"value": 0
},
{
"name": "originY",
"value": 0
}
],
"data": [
{
"name": "chords",
"values": null,
"transform": [
{
"type": "formula",
"expr": "datum.startAngle + offset",
"as": "startAngle"
},
{
"type": "formula",
"expr": "datum.endAngle + offset",
"as": "endAngle"
},
{
"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": null
}
],
"marks": [
{
"type": "arc",
"from": {
"data": "chords"
},
"encode": {
"enter": {
"fill": {
"value": "#aa0000"
},
"x": {
"signal": "0 * width/2"
},
"y": {
"signal": "0 * height/2"
},
"opacity": {
"signal": "(indexof(emptyStrokeIndexes, datum.id) != -1)? 0:1"
}
},
"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"
},
"opacity": {
"signal": "(indexof(emptyStrokeIndexes, datum.id) != -1)? 0:1"
}
},
"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": {
"signal": "(indexof(emptyStrokeIndexes, datum.sourceId+1) != -1)? 0: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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
</head>
<body>
<div id="vis"></div>
<script>
var respondents = 95,
emptyPerc = 0.4,
//How many "units" would define this empty percentage
emptyStroke = Math.round(respondents * emptyPerc),
//Calculate how far the chord diagram needs to be rotated clockwise
//to make the dummy invisible chord center vertically
offset = Math.PI * (emptyStroke/(respondents + emptyStroke)) / 2,
startAngle = function(d) { return d.startAngle + offset; },
endAngle = function(d) { return d.endAngle + offset; };
var chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending);
var innerRadius = 270,
outerRadius = 290;
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(startAngle)
.endAngle(endAngle);
var ribbon = d3.ribbon()
.radius(innerRadius)
.startAngle(startAngle)
.endAngle(endAngle);
var data = [
[0,0,0,0,10,5,15,0], //X
[0,0,0,0,5,15,20,0], //Y
[0,0,0,0,15,5,5,0], //Z
[0,0,0,0,0,0,0,emptyStroke], //Dummy stroke
[10,5,15,0,0,0,0,0], //C
[5,15,5,0,0,0,0,0], //B
[15,20,5,0,0,0,0,0], //A
[0,0,0,emptyStroke,0,0,0,0] //Dummy stroke
];
// TODO:
// unharcode this
var emptyStrokeIndexes = [4,8];
var chords = chord(data);
var ribbonsPaths = chords.map(function(chord, index) {
return {
'path' : ribbon(chord),
'sourceId' : chord.source.index,
'targetId' : chord.target.index,
}
});
console.log('___ ribbonPaths ____');
console.log(JSON.stringify(ribbonsPaths));
console.log('___ chords.groups ____');
console.log(chords);
console.log(
JSON.stringify(chords.groups)
);
var view;
vega.loader()
.load('chords.vg.json')
.then(function(data) {
var spec = JSON.parse(data);
// add signals
spec.signals.push({
'name' : 'inner_radius',
'value' : innerRadius
});
spec.signals.push({
'name' : 'outer_radius',
'value' : outerRadius
});
spec.signals.push({
'name' : 'offset',
'value' : offset
});
spec.signals.push({
'name' : 'emptyStrokeIndexes',
'value' : emptyStrokeIndexes
});
// inject the data
_.find(spec.data, ['name', 'chords'])
.values = JSON.parse(JSON.stringify(chords.groups));
_.find(spec.data, ['name', 'ribbonsPaths'])
.values = JSON.parse(JSON.stringify(ribbonsPaths));
console.log('___ spec ____');
console.log(spec);
render(spec);
});
function render(spec) {
view = new vega.View(vega.parse(spec))
.renderer('canvas') // set renderer (canvas or svg)
.initialize('#vis') // initialize view within parent DOM container
.hover() // enable hover encode set processing
.run();
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment