Skip to content

Instantly share code, notes, and snippets.

@denjn5
Last active August 11, 2017 11:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save denjn5/1a3f8e44cdcb3054121dfd991f59fbc2 to your computer and use it in GitHub Desktop.
Save denjn5/1a3f8e44cdcb3054121dfd991f59fbc2 to your computer and use it in GitHub Desktop.
Zoomable Dynamically Updating Sunburst (D3 version 4)
license: gpl-3.0
height: 410
border: no

Who doesn't love sunbursts? Visit my blog for more.

I updated Kerry Rodden’s Zoomable sunburst with updating data to D3 version 4. His work was based on Mike Bostock's Zoomable Sunburst and Sunburst Partition examples. He'd overcome some challenging tween issues (nicely done!). I added an opening tween in arcTweenData() that animates the sunburst creation, growing it from 0, and streamlines the tweening process (no longer need to stash() previous x0 and x1 values).

The hierarchical dataset is built the fly. It starts as a string quote in English, with the original language words in brackets. I dynamically analyze and transform it using regex, jQuery, and d3-hierarchy. The user can toggle between modes: "Linear" (words in order they appear in English) and "Grouped" (only show directly-translated words, and group them by first appearance). I don't change the dataset on the fly, instead change the source of the slice sizing from d.size to d.grpSize. Each node object has the form: {name: "g1063", size: 1, grpSize: 2, parent: 0, word: "g1063", eWord: "For"}.

The continuous coloration scheme leverages d3.scaleLinear() thanks to Jerome Cukier's excellent post d3: scales, and color. The palettes are from Mary Stribley's Website Color Schemes.

Click on any arc to zoom in, and click on the center circle to zoom out. Use the Linear/Grouped radio buttons to update the data.

Found this on github? See it working at http://bl.ocks.org/denjn5.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
@import url('https://fonts.googleapis.com/css?family=Raleway');
body {
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
#gameboard {
height: 400px;
width: 400px;
}
path {
stroke: #fff;
}
text {
pointer-events: none;
}
</style>
<body>
<svg id="gameboard"></svg>
<label><input class="mode" type="radio" name="mode" value="linear" checked> Linear</label>
<label><input class="mode" type="radio" name="mode" value="grouped"> Grouped</label>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script>
$(function () {
// helpful: https://bl.ocks.org/kerryrodden/477c1bfb081b783f80ad
// palettes: https://designschool.canva.com/blog/website-color-schemes/
// Global Variables
var gWidth = $('#gameboard').width(), // Width of the svg palette
gHeight = $('#gameboard').height(), // Height of the svg palette
radius = (Math.min(gWidth, gHeight) / 2) - 10,
quote = ["John 3:16-17", "For[g1063] God[g2316] so[g3779] loved[g25] the world[g2889], that[g5620] he gave[g1325] his[g846] only begotten[g3439] Son[g5207], that[g2443] whosoever[g3956] believeth[g4100] in[g1519] him[g846] should[g622] not[g3361] perish[g622], but[g235] have[g2192] everlasting[g166] life[g2222]. For[g1063] God[g2316] sent[g649] not[g3756] his[g846] Son[g5207] into[g1519] the world[g2889] to[g2443] condemn[g2919] the world[g2889]; but[g235] that[g2443] the world[g2889] through[g1223] him[g846] might be saved[g4982]."],
mode = $('.mode:checked').val(), // Linear or grouped, based on radio buttons
svg = d3.select("svg").append("g").attr("transform", "translate(" + gWidth / 2 + "," + (gHeight / 2) + ")"),
color_palettes = [['#4abdac', '#fc4a1a', '#f7b733'], ['#f03b20', '#feb24c', '#ffeda0'], ['#007849', '#0375b4', '#ffce00'], ['#373737', '#dcd0c0', '#c0b283'], ['#e37222', '#07889b', '#eeaa7b'], ['#062f4f', '#813772', '#b82601'], ['#565656', '#76323f', '#c09f80']];
// D3 Global Variables
var root = textToHierarchy(quote[0], quote[1]),
node = root, // Save root for tweening
x = d3.scaleLinear().range([0, 2 * Math.PI]),
y = d3.scaleSqrt().range([0, radius]),
color = d3.scaleLinear().domain([0, 0.5, 1]).range(color_palettes[~~(Math.random() * 6)]),
partition = d3.partition();
// Calculate the d path for each slice.
var arc = d3.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x0))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x1))); })
.innerRadius(function(d) { return Math.max(0, y(d.y0)); })
.outerRadius(function(d) { return Math.max(0, y(d.y1)); });
// Build the sunburst.
var first_build = true;
function update() {
// Determine how to size the slices.
if (mode == "linear") {
root.sum(function (d) { return d.size; });
} else {
root.sum(function (d) { return d.grpSize; });
}
if (first_build) {
// Add a <path d="[shape]" style="fill: [color];"><title>[popup text]</title></path>
// to each <g> element; add click handler; save slice widths for tweening
svg.selectAll("path").data(partition(root).descendants()).enter().append("path")
.style("fill", function (d) { return d.parent ? color(d.x0) : "white"; }) // Return white for root.
.on("click", click);
svg.selectAll("path").append("title").text(function (d) { return d.data.word; })
first_build = false;
} else {
svg.selectAll("path").data(partition(root).descendants());
}
svg.selectAll("path").transition().duration(1000).attrTween("d", arcTweenData);
}
update(); // GO!
// Respond to radio click.
$('.mode').on("change", function change() {
mode = $('.mode:checked').val();
update();
});
// Respond to slice click.
function click(d) {
node = d;
svg.selectAll("path").transition().duration(1000).attrTween("d", arcTweenZoom(d));
}
// When switching data: interpolate the arcs in data space.
function arcTweenData(a, i) {
// (a.x0s ? a.x0s : 0) -- grab the prev saved x0 or set to 0 (for 1st time through)
// avoids the stash() and allows the sunburst to grow into being
var oi = d3.interpolate({ x0: (a.x0s ? a.x0s : 0), x1: (a.x1s ? a.x1s : 0) }, a);
function tween(t) {
var b = oi(t);
a.x0s = b.x0;
a.x1s = b.x1;
return arc(b);
}
if (i == 0) {
// If we are on the first arc, adjust the x domain to match the root node
// at the current zoom level. (We only need to do this once.)
var xd = d3.interpolate(x.domain(), [node.x0, node.x1]);
return function (t) {
x.domain(xd(t));
return tween(t);
};
} else {
return tween;
}
}
// When zooming: interpolate the scales.
function arcTweenZoom(d) {
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]), // [d.y0, 1]
yr = d3.interpolate(y.range(), [d.y0 ? 40 : 0, radius]);
return function (d, i) {
return i
? function (t) { return arc(d); }
: function (t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
};
}
// Take text from Strong's and format as hierarchy with root.
function textToHierarchy(rootNode, quote) {
var vsWords = quote.replace(/[^A-Za-z0-9 /[]/g, "").replace(/[/[]/g, "|").split(" ");
var sbWords = [{ "name": rootNode, "parent": "" }];
for (i = 0; i < vsWords.length ; i++) {
//sbWords.filter(function (value) { return value == vsWords[i]; }).length;
word = vsWords[i].split("|");
if (word.length == 1) {
sbWords.push({ "name": i, "word": word[0], "parent": rootNode, "size": 1, "grpSize": 0 });
} else {
// If this combo of English:Greek word exists, then add a
filtered = sbWords.filter(function (value) { return value.word == word[1] && value.eWord == word[0]; });
var newGrpSize = 1;
if (filtered.length > 0) {
filtered[0].grpSize += 1;
newGrpSize = 0;
}
sbWords.push({ "name": word[1], "parent": i, "word": word[1], "size": 1, "grpSize": newGrpSize, "eWord": word[0] });
sbWords.push({ "name": i, "word": word[0], "parent": rootNode, "size": 0, "grpSize": 0 });
}
}
var root = d3.stratify().id(function (d) { return d.name; })
.parentId(function (d) { return d.parent; })(sbWords);
return root;
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment