this is my first question in stackoverflow and I hope that you can help me with this problem for which I haven't found a solution yet. I want to use D3 v4 and the sunburst visualization in a web application to display and navigate on some data given by a json file. I managed to display the data with the sunburst (with tooltips) which also allows zooming in to the children using a transition when the user clicks on the corresponding arc.
For my application, I want to use an initial small json file that shows only a part of the complete (much bigger) data. This small data should be extended on demand and visualized dynamically in the sunburst when the user clicks on an arc (for which additional information can be retrieved). Furthermore, when the user goes "back" to the parent arc, the recently newly added data should be removed such that only the initial data is displayed again.
So the workflow: there is a sunburst at start showing (data1
); the user clicks on a child arc; this arc should be the new focus/center of the sunburst while concurrently the initial data is extended to (data2
) updating the sunburst visualization; the user sees the new focus and the newly loaded children of the selected arc; when the user navigates back, (data1
) should be displayed again. The last part is still missing and I will take care of that later but I am facing a problem before that. I managed to implement the switch between the two datasets but the result of the new focus and the sunburst behavior are not as expected. (data1
) and (data2
) are partial data examples from the flare.json dataset and when clicking on the arc "analytics" for which new data should be loaded and visualized, after the transition only two ("cluster" and "graph") of its three children (missing "optimization") are displayed. But additional clicking on the arc of "analytics" reveals more and more the missing third children. The more I click, the more the "correct" result is somehow approximated.
I created a working example in JSFiddle that demonstrates my concern and the appearing issue. Additionally, here is the code:
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2 - 10;
var color = d3.scaleOrdinal(d3.schemeCategory20);
var x = d3.scaleLinear()
.range([0, 2 * Math.PI]);
var y = d3.scaleSqrt()
.range([0, radius]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
var arc = d3.arc()
.padAngle(.01)
.padRadius(radius/3)
.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)); });
var partition = d3.partition();
var root = d3.hierarchy( data1 ).count();
var g = svg.selectAll("g")
.data( partition(root).descendants(), function(d){return d.data.name;})
.enter().append("g");
var tt = d3.select("body").append("tt")
.attr("class", "tooltip")
.style("opacity", 0);
var path = g.append("path")
.attr("d", arc)
.style("stroke", "white")
.style('stroke-width', 0.5)
.attr( "id", function(d){ return 'path' + d.data.name; })
.style("fill", function(d) { return color((d.children ? d : d.parent).data.name); })
.style("opacity", 0)
.on("click", click)
.on("mouseover", function(d) {
tt.transition()
.duration(200)
.style("opacity", 1.0);
tt.html( showTooltipInfo(d) )
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tt.transition()
.duration(500)
.style("opacity", 0);
})
.each(function(d,i){
this.xx0 = d.x0;
this.xx1 = d.x1;
this.yy0 = d.y0;
this.yy1 = d.y1;
})
.transition()
.duration(1000)
.style("opacity", 1);
function showTooltipInfo(d){
return d.data.name;
}
function click(d) {
svg.selectAll("g").select("path")
.transition()
.duration(750)
.attrTween("d", arcTweenZoom(d))
.each(function(d,i){
this.xx0 = d.x0;
this.xx1 = d.x1;
this.yy0 = d.y0;
this.yy1 = d.y1;
});
var selectedD = d;
setTimeout(function () {
var newRoot;
if (selectedD.data.name === "analytics")
newRoot = d3.hierarchy( data2 ).count();
else
return;
var groups = svg.selectAll("g")
.data( partition(newRoot).descendants(), function(d){return d.data.name;} );
groups.exit()
.transition()
.duration(10)
.style("opacity", 0)
.remove();
groups.select("path")
.transition()
.duration(750)
.attrTween("d", arcTweenData)
.each(function(d,i){
this.xx0 = d.x0;
this.xx1 = d.x1;
this.yy0 = d.y0;
this.yy1 = d.y1;
});
groups.enter().append("g").append("path")
.attr("d", arc)
.style("stroke", "white")
.style('stroke-width', 0.5)
.attr( "id", function(d){ return 'path' + d.data.name; })
.style("fill", function(d) { return color((d.children ? d : d.parent).data.name); })
.style("opacity", 0)
.on("click", click)
.on("mouseover", function(d) {
tt.transition()
.duration(200)
.style("opacity", 1.0);
tt.html( showTooltipInfo(d) )
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tt.transition()
.duration(500)
.style("opacity", 0);
})
.each(function(d,i){
this.xx0 = d.x0;
this.xx1 = d.x1;
this.yy0 = d.y0;
this.yy1 = d.y1;
})
.transition()
.delay(250)
.duration(750)
.style("opacity", 1);
}, 500);
}
function arcTweenData(a){
if ( this.xx0 !== undefined ){
var oi = d3.interpolate({x0: this.xx0, x1: this.xx1, y0: this.yy0, y1: this.yy1}, a);
var that = this;
return function(t) {
var b = oi(t);
that.xx0 = b.x0;
that.xx1 = b.x1;
that.yy0 = b.y0;
that.yy1 = b.y1;
return arc(b);
};
}
}
function arcTweenZoom(d){
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]),
yr = d3.interpolate(y.range(), [d.y0 ? 20 : 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);}
}
}
And the datasets data1
and data2
:
var data1 = {
"name": "flare",
"children": [
{
"name": "analytics",
"children": []
},
{
"name": "animate",
"children": [
{"name": "Easing", "size": 17010},
{"name": "FunctionSequence", "size": 5842},
{
"name": "interpolate",
"children": [
{"name": "ArrayInterpolator", "size": 1983},
{"name": "ColorInterpolator", "size": 2047},
{"name": "DateInterpolator", "size": 1375},
{"name": "Interpolator", "size": 8746},
{"name": "MatrixInterpolator", "size": 2202},
{"name": "NumberInterpolator", "size": 1382},
{"name": "ObjectInterpolator", "size": 1629},
{"name": "PointInterpolator", "size": 1675},
{"name": "RectangleInterpolator", "size": 2042}
]
},
{"name": "ISchedulable", "size": 1041},
{"name": "Parallel", "size": 5176},
{"name": "Pause", "size": 449},
{"name": "Scheduler", "size": 5593},
{"name": "Sequence", "size": 5534},
{"name": "Transition", "size": 9201},
{"name": "Transitioner", "size": 19975},
{"name": "TransitionEvent", "size": 1116},
{"name": "Tween", "size": 6006}
]
}]
};
var data2 = {
"name": "flare",
"children": [
{
"name": "analytics",
"children": [
{
"name": "cluster",
"children": [
{"name": "AgglomerativeCluster", "size": 3938},
{"name": "CommunityStructure", "size": 3812},
{"name": "HierarchicalCluster", "size": 6714},
{"name": "MergeEdge", "size": 743}
]
},
{
"name": "graph",
"children": [
{"name": "BetweennessCentrality", "size": 3534},
{"name": "LinkDistance", "size": 5731},
{"name": "MaxFlowMinCut", "size": 7840},
{"name": "ShortestPaths", "size": 5914},
{"name": "SpanningTree", "size": 3416}
]
},
{
"name": "optimization",
"children": [
{"name": "AspectRatioBanker", "size": 7074}
]
}
]
},
{
"name": "animate",
"children": [
{"name": "Easing", "size": 17010},
{"name": "FunctionSequence", "size": 5842},
{
"name": "interpolate",
"children": [
{"name": "ArrayInterpolator", "size": 1983},
{"name": "ColorInterpolator", "size": 2047},
{"name": "DateInterpolator", "size": 1375},
{"name": "Interpolator", "size": 8746},
{"name": "MatrixInterpolator", "size": 2202},
{"name": "NumberInterpolator", "size": 1382},
{"name": "ObjectInterpolator", "size": 1629},
{"name": "PointInterpolator", "size": 1675},
{"name": "RectangleInterpolator", "size": 2042}
]
},
{"name": "ISchedulable", "size": 1041},
{"name": "Parallel", "size": 5176},
{"name": "Pause", "size": 449},
{"name": "Scheduler", "size": 5593},
{"name": "Sequence", "size": 5534},
{"name": "Transition", "size": 9201},
{"name": "Transitioner", "size": 19975},
{"name": "TransitionEvent", "size": 1116},
{"name": "Tween", "size": 6006}
]
}]
};
The reason why I am using the setTimeout
call in the click method is that when I immediately apply the switch to the new data, the first transition/zoom event at the beginning of the click method is not applied since the switching occurs too fast. Maybe my approach is totally wrong in this case.
Do you have an idea what I am doing wrong or an advice how I can better solve this issue?
Any help is appreciated! Thank you a lot in advance!