d3.js sunburst with dynamically built updatable data structure - d3.js

I am attempting to create a fully dynamic Sunburst graph using d3.js.
The examples and tutorials I have located tend to use existing/fully-populated data structures which may have the ability to modify the value of existing arcs but does not allow the ability to add child arcs as needed.
Likewise the tutorials I have located which allow new datasets simply replace the existing structure and begin drawing from scratch.
This is not the behavior I am trying to implement.
What I need is a dynamically built graph based on incoming data as it is provided.
I am able to append children to the end of the data set, transition and render the results without issue. The problem occurs any time I insert a child somewhere within the existing structure, d3’s selectAll() does not function as expected. It includes the new arc (which has yet to be drawn) resulting in any remaining arcs being rendered incorrectly. Then when transitioning the arcs it seems to get the arcs Dom ID and data it supposedly represents gets mixed up. The new arc is not visible and an empty space exists where new arc should be placed.
To be clear my intent is:
Add to the existing data structure allowing new children to be added when new information is provided
To transition existing arcs opening space for the new arcs before they are created and drawn
Broken down into four steps of the jsfiddle example:
Initialization of the graph (draws an invisible “root” arc)
{ name:"a_0", children: [] }
Adding First Child data and it’s children to root
{ name:"a_0", children:[
{ name:"a_1", children:[ { name:"a_2", children:[ { name:"a_3" } ] } ] }
] }
Adding Second Child and underlying children to root
{ name:"a_0", children:[
{ name:"a_1", children:[ { name:"a_2", children:[ { name:"a_3" } ] } ] },
{ name:"a_4", children:[ { name:"a_5", children:[ { name:"a_6" } ] } ] }
] }
Inserting another child within the existing arc a_2
{ name:"a_0", children:[
{ name:"a_1", children:[
{ name:"a_2", children:[
{ name:"a_3" },
{ name:"a_7" }
] }
] },
{ name:"a_4", children:[
{ name:"a_5", children:[
{ name:"a_6" }
] }
] }
] }
Step 1 works just fine
Step 2 draws the arcs properly
Step 3 transitions the existing arcs and adds the new arcs to the graph
Step 4 results some unexpected behavior.
During the transition of existing and entering of new arcs some of the arcs "jump around" losing the proper association with their respective data
The end result appears to be:
a_0 - is correct
a_1 & a_2 - look correct
a_3 - has shrunk to accommodate the new sibling a_7 - expected behavior
a_4 - disappears
a_5 - jumps down where a_4 should be
a_6 - (looks like) it is duplicated and exists once where it should be and where a_5 should be
a_7 - not displayed, location where it should be is empty space and appears to be associated with a_6 data
What the end result looks like and what is really going on are not the same.
In the attempt to update the graph the selectAll() for the existing arcs includes (a_0, a_1, a_2, a_3, a_4, a_5, a_7). Where the existing a_6 is not included in the selectAll() but a_7 (which has not been drawn) is.
The enter() function appears to operate on the existing a_6 which is then treated as a new arc
It looked like I was on the right track getting all the way to a_6, but I have not figured out the reason for the behavior when adding a_7.
The jsFidde executes the steps as described above including:
Unique colors for each arc
A table displaying the name of each arc,
If the arc is being handled by d3js' selectAll() (i.e. "existing") or enter() (i.e. "new"),
The d3 Index as it is currently being assigned when drawing existing or new arcs.
Expected target position where each arc should appear after any transitioning,
Arctween information as an Arc is being transitioned from its former location to the new location and
Questions:
What is going on that would cause this behavior in Step 4?
Is there a way to ensure the integrity between each arc and the data it represents?
Is there a way to insert children into the existing structure or update the graph in this dynamic manor?
Working example on jsfiddle https://jsfiddle.net/mfitzgerald/j2eowwya/
var dataObj = { name:"a_0", color: "none" };
var height = 300;
var width = 500;
var radius = Math.min(width, height) / 2;
var graph = d3.select("#graph")
.attr('height', height)
.attr('width', width)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d, i) { return 1; });
var arc = d3.svg.arc()
.startAngle(function(d) { if (isNaN(d.x)) { d.x = 0; } d.x0 = d.x; return d.x; })
.endAngle(function(d) { if (isNaN(d.dx)) { d.dx = 0; } d.dx0 = d.dx; return d.x + d.dx; })
.innerRadius(function(d) { if (isNaN(d.y)) { d.y = 0; } d.y0 = d.y; return Math.sqrt(d.y); })
.outerRadius(function(d) { if (isNaN(d.dy)) { d.dy = 0; } d.dy0 = d.dy; return Math.sqrt(d.y + d.dy); });
var arcTween = function(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0, y: a.y0, dy: a.dy0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
a.y0 = b.y;
a.dy0 = b.dy;
displayStats("arctween", b);
return arc(b);
};
}
// Root Arc
graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes)
.enter()
.append('path')
.attr('class', function(d) { return "arc " + d.name; })
.attr("d", arc)
.attr("id", function(d, i) { return "path_"+i; })
.attr("name", function(d) { return d.name; })
.style("fill", "none");
function updateGraph() {
console.log("Update Graph");
console.log(dataObj);
var update = graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes);
// Move existing Arcs
update.each(function(d, i) {
displayStats("target", d, i, "existing");
var domId = $(this).attr("id");
console.log("["+i+"] Exist Arc name:"+d.name+", dom_id:"+domId);
})
.transition()
.delay(function(d, i) { return i * 250; })
.duration(1500)
.attrTween("d", arcTween);
// Add New Arcs
update.enter().append('path')
.attr('class', function(d, i) { return "arc "+d.name; })
.attr("d", arc)
.attr("id", function(d, i) {
var domId = "path_"+i;
console.log("["+i+"] NEW Arc name:"+d.name+", dom_id:"+domId);
displayStats("target", d, i, "new");
return domId;
})
.style("stroke", "#fff")
.style("fill", function(d) { return d.color; })
.style("opacity", 0)
.transition()
.delay(function(d, i) { return i * 250; })
.duration(1500)
.style("opacity", .5)
.attrTween("d", arcTween);
}

#Gordon has answered the question. The issue was resolved by adding a key function when joining with .data() in the updateGraph code.
Forked example on jsfiddle
var update = graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes, function(d) { return d.name; } );
I believe the answers to the questions are:
The .data() function uses an indexed array which only uniquely identifies each arc given any new arcs are appended to the end of the array. Once one is inserted this would cause the data, graphed arcs and associated DOM ids to be misaligned.
Using the key function, as suggested by Gordon, allows unique identification of specific nodes keeping the Data and Graph in sync as expected.
Update
An additional modification would need to be made as the DOM id was set by the array index of the data element there would still be an invalid association with the DOM and the underlying graph/data.
This would result in 2 a_4 DOM id's. Instead of using the array index using the Node Name as the DOM id should keep this association correct.

Related

d3: how to drag line elements independently of background

I have developed an applet that shows a d3 diagonal tree. The graph is navigatable by dragging the background.
It is based on the code found at the following link:
https://bl.ocks.org/adamfeuer/042bfa0dde0059e2b288
I am trying to have vertical lines across the page to further annotate the tree/ graph (based on the following link: https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19).
See below:
See here: https://jsfiddle.net/chrisclarkson100/opfq6ve8/28/
I append the lines to the graph as follows:
var data_line = [
{
'x1': 300,
'y1': 700,
'x2': 300,
'y2': 700
},
////....
];
// Generating the svg lines attributes
var lineAttributes = {
....
'x1': function(d) {
return d.x1;
},
'y1': function(d) {
return screen.availHeight;
},
'x2': function(d) {
return d.x2;
},
'y2': function(d) {
return 0;
}
};
var drag_line = d3.behavior.drag()
.origin(function(d) { return d; })
.on('drag', dragged_line);
// Pointer to the d3 lines
var svg = d3.select('body').select('svg');
var lines = svg
.selectAll('line')
.data(data_line)
.enter().append('g')
.attr('class', 'link');
links_lines=lines.append('line')
.attr(lineAttributes)
.call(drag_line);
lines.append('text')
.attr('class','link_text')
.attr("x", d => d.x1)
.attr("y", d => 350)
.style('fill', 'darkOrange')
.style("font-size", "30px")
function dragged_line() {
var x = d3.event.dx;
var y = d3.event.dy;
var line = d3.select(this);
// Update the line properties
var attributes = {
x1: parseInt(line.attr('x1')) + x,
y1: parseInt(line.attr('y1')) + y,
x2: parseInt(line.attr('x2')) + x,
y2: parseInt(line.attr('y2')) + y,
};
line.attr(attributes);
}
The lines display as I wanted and are draggable. However, when I drag them, the background/ tree network moves with them... I want the different draggable elements to be independent of eachother.
Can anybody spot how I'm failing to make the dragging interactivity of each element independent of eachother?
Here's a JSFiddle that seems to do what you want– you can move the vertical lines without moving the tree; and move the tree without moving the vertical lines:
https://jsfiddle.net/adamfeuer/gd4ouvez/125/
(That JSFiddle uses a much smaller dataset of tree nodes; the one you linked to was too big to easily iterate and debug.)
The issue with the code you posted is that the zoom (pan) function for the tree is active at the same time the zoom() for the lines is active, so the tree and the active line drag at the same time.
I added a simple mechanism to separate the two – a boolean called lineDragActive. The code then checks for that in the tree zoom(), sets it true when a line drag starts, and false when the line drag ends:
// Define the zoom function for the zoomable tree
// flag indicates if line dragging is active...
// if so, we don't want to drag the tree
var lineDragActive = false;
function zoom() {
if (lineDragActive == false) {
// not line dragging, so we can zaoom (drag) the tree
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
[...]
function drag_linestarted() {
// tell others not to zoom while we are zooming (dragging)
lineDragActive = true;
d3.select(this).classed(activeClassName, true);
}
function drag_lineended() {
// tell others that zooming (dragging) is allowed
lineDragActive = false;
d3.select(this).classed(activeClassName, false);
label = baseSvg.selectAll('.link_text').attr("transform", "translate(" + String(Number(this.x1.baseVal.value) - 400) + "," + 0 + ")");
}

Circles keep accumulating and are not merging correctly

I am trying to merge these circles but I keep getting a graph of accumulating circles as opposed to circles moving across the graph?
What am I missing?
I have attached the code below. This function is called updatechart. It corresponds to a slider. So whenever I move the slider across the screen. I corresponding year it lands on is where the updated circle data should move.
var filteredyears = d3.nest()
.key(function(d) {
if(year === d.year){
return d;
}
}).entries(globaldataset);
var circled = svg.selectAll('.countries')
.data(filteredyears[1].values);
var circledEnter = circled.enter()
circled.merge(circledEnter);
circledEnter.append("circle").attr("cx", function(d) {
return xScale(d.gdpPercap);
})
.attr("cy", function(d) {
return yScale(d.lifeExp);
})
.attr('transform', "translate("+[40,30]+")")
.attr( 'r', function(d) {
return rScale(d.population) / 100})
.style("fill", function(d) {
if(d.continent == 'Asia'){
return '#fc5a74';
} else if (d.continent == 'Europe') {
return '#fee633';
} else if (d.continent == 'Africa') {
return '#24d5e8';
} else if (d.continent == 'Americas') {
return '#82e92d';
} else if (d.continent == 'Oceania') {
return '#fc5a74';
}
})
.style("stroke", "black");
circled.exit().remove();
You have a couple of issues using the merge() method, which is indeed quite hard to understand initially.
First, you have to reassign your selection:
circled = circled.merge(circledEnter);
Now, from this point on, apply the changes to circled, not to circledEnter:
circled.attr("//etc...
Besides that, your exit selection won't work, since you're calling it on the merged selection. Put it before the merge.
Finally, append goes to the circledEnter selection, before merging, as well as all attributes that don't change.
Here is a very basic demo showing it:
var svg = d3.select("svg"),
color = d3.scaleOrdinal(d3.schemeCategory10);
render();
function render() {
var data = d3.range(~~(1 + Math.random() * 9));
var circles = svg.selectAll("circle")
.data(data);
circles.exit().remove();
var circlesEnter = circles.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) {
return color(d);
});
circles = circlesEnter.merge(circles);
circles.attr("cx", function() {
return 5 + Math.random() * 290
})
.attr("cy", function() {
return 5 + Math.random() * 140
});
}
d3.interval(render, 1000);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

d3v4 update creates duplicate element

I have rewritten most of my d3 code to v4, but the new update pattern is throwing me off. The example below is for a force diagram. A duplicate circle is created within the first container upon every update. The data in my example does not actually change, but it's irrelevant. If I use new data, the same issue (a duplicate circle) occurs.
var w = 800,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var dataset = {};
function setData() {
dataset.nodes = [{
value: 200
}, {
value: 100
}, {
value: 50
}];
}
setData();
var rScale = d3.scaleSqrt()
.range([0, 100])
.domain([0, d3.max(dataset.nodes.map(function(d) {
return d.value;
}))]);
var node = svg.append("g")
.attr("class", "nodes")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
.selectAll(".node");
var simulation = d3.forceSimulation(dataset.nodes)
.force("charge", d3.forceManyBody().strength(-1600))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaDecay(.05)
.on("tick", ticked);
function ticked() {
node.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
function restart() {
// Apply the general update pattern to the nodes.
node = node.data(dataset.nodes, function(d) {
return d.id;
});
node.exit().remove();
node = node.enter().append("g")
.attr("class", "node")
.merge(node);
node.append("circle")
.attr("r", function(d) {
return rScale(d.value);
});
// Update and restart the simulation.
simulation.nodes(dataset.nodes);
simulation.alpha(1).restart();
}
restart();
function update() {
setData();
restart();
}
d3.select("#update").on("click", update);
If you click the Update button in this codepen (https://codepen.io/cplindem/pen/wpQbQe), you will see all three circles animate as the simulation restarts, but behind the largest circle, there is another, identical circle that does not animate. You can also see the new circle appear in the html if you inspect it.
What am I doing wrong?
Your first problem seems to be that you are keying the data on an 'id' field, but your data doesn't have any ids, so that needs changed or you just keep adding new groups:
function setData() {
dataset.nodes = [{
value: 200,
id: "A"
}, {
value: 100,
id: "B"
}, {
value: 50,
id: "C"
}];
console.log("dataset", dataset);
}
The second problem is you merge the new and updated selection and then append new circles to all of them, even the existing ones (so you have multiple circles per group on pressing update). I got it to work by doing this: make the new nodes, merge with existing selection, add circles to just the new nodes, update the circles in all the nodes:
node.exit().remove();
var newNodes = node.enter().append("g");
node = newNodes
.attr("class", "node")
.merge(node);
newNodes.append("circle");
node.select("circle")
.attr("r", function(d) {
return rScale(d.value);
});
Whether that 2nd bit is optimal I don't know, I'm still more anchored in v3 myself...
https://codepen.io/anon/pen/WdLexR

d3 - sunburst partition. Different sizes for each node

I'm a newbie to the d3 library and javascript in general.
I'm trying to achieve something like
this, where I have a sunburst partition but each node has a different height with respect to the radial center - but the padding to its parent/child stays the same.
I've tried looking around and couldn't come up with any solutions.
(trying to change the innerRadius/outerRadius parameters didn't seem to work :( ).
Here is my code:
var vis = d3.select("#chart").append("svg")
.style("margin", "auto")
.style("position", "relative")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.sort(function (a, b) { return d3.ascending(a.time, b.time); })
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.n_leaves+1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
//read data from json file and visualize it
d3.text("5rrasx_out.json", function(text) {
var data = JSON.parse(text);
var json = buildHierarchy(data,'5rrasx');
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json);
var dataSummary = [{label: 'pos', count: totalPos}, {label: 'neg', count: totalNeg}];
//set title
$("#title").text(json.title.replace(/\[.*\]/g,""));
//set chart
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("path")
.attr("class", "sunburst_node")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return (d.sentiment > 0) ? colors["pos"] : colors["neg"]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.on("click", click);
};
Any help would be much appreciated.
Thanks!
I know this is not a proper answer to the question above, but in case someone needs a sunburst with different dimensions for each node, here I post how to do it in R using the ggsunburst package.
# install ggsunburst
if (!require("ggplot2")) install.packages("ggplot2")
if (!require("rPython")) install.packages("rPython")
install.packages("http://genome.crg.es/~didac/ggsunburst/ggsunburst_0.0.10.tar.gz", repos=NULL, type="source")
library(ggsunburst)
# one possible input for ggsunburst is newick format
# consider the following newick "(((A,B),C),D,E);"
# you can define the distance in node A with "A:0.5"
# you can define size in node E with "E[&&NHX:size=5]"
# adding both attributes to the newick
nw <- '(((A:0.5,B),C:3),D[&&NHX:size=5],E[&&NHX:size=5]);'
sb <- sunburst_data(nw)
sunburst(sb, rects.fill.aes = "name") + scale_fill_discrete(guide=F)
as you can see in the code, these attributes can be defined independently, and as you can see in the plot they affect the dimennsions of the correponding nodes:
node "A" is 0.5 times shorter than "B", which is defined by the attribute "distance"
E has an angle 5 times wider than C, which is defined by the attribute "size".
and here an attempt to resemble the example posted in the question with a newick tree
nw <- "(((.:0[&&NHX:support=1.0:dist=0.0:name=.:size=3],a3:1[&&NHX:color=2:support=1.0:dist=1.0:name=a3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=a2])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=a1],b1:1.8[&&NHX:color=1:support=1.0:dist=1.8:name=b1:size=5],(((a4:1[&&NHX:color=1:support=1.0:dist=1.0:name=a4:size=1],b4:1.8[&&NHX:color=-1:support=1.0:dist=1.8:name=b4:size=1],c4:1.5[&&NHX:color=2:support=1.0:dist=1.5:name=c4:size=1],d4:0.8[&&NHX:color=-2:support=1.0:dist=0.8:name=d4:size=1])1:1[&&NHX:color=1:support=1.0:dist=1.0:name=b3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=b2:size=1],(c3:1[&&NHX:color=1:support=1.0:dist=1.0:name=c3:size=1],(e4:1[&&NHX:color=-2:support=1.0:dist=1.0:name=e4:size=1])1:0.5[&&NHX:color=-1:support=1.0:dist=0.5:name=d3:size=1])1:0.5[&&NHX:color=1:support=1.0:dist=0.5:name=c2:size=1])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=c1:size=1],d1:0.8[&&NHX:color=3:support=1.0:dist=0.8:name=d1:size=20]);"
sb <- sunburst_data(nw, node_attributes = "color")
sunburst(sb, leaf_labels.size = 4, node_labels.size = 4, node_labels = T, node_labels.min = 1, rects.fill.aes = "color") +
scale_fill_gradient2(guide=F) + ylim(-8,NA)

D3.js - layouts - value accessor

I'm just starting out with D3.js. I've created a simple enough donut chart using this example. My problem is, if I have an array of objects as my data source - data points for ex. would be a1.foo or a1.bar - and I want to switch between them, how would i go about doing this? My current solution looks ugly and it can't be the proper way of doing it - code below.
//Call on window change event
//Based on some parameter, change the data for the document
//vary d.foo to d.bar and so on
var donut = d3.layout.pie().value(function(d){ return d.foo})
arcs = arcs.data(donut(data)); // update the data
Is there a way I can set the value accessor at run time other than defining a new pie function?
Generally to switch the data that is being displayed you would create a redraw() function that would then update the data for the chart. In the redraw you'll need to make sure to handle the three cases - what should be done when data elements are modified, what should be done when new data elements are added, and what should be done when data elements are removed.
It usually looks something like this (this example changes the data set through panning, but it doesn't really matter). See the full code at http://bl.ocks.org/1962173.
function redraw () {
var rects, labels
, minExtent = d3.time.day(brush.extent()[0])
, maxExtent = d3.time.day(brush.extent()[1])
, visItems = items.filter(function (d) { return d.start < maxExtent && d.end > minExtent});
...
// upate the item rects
rects = itemRects.selectAll('rect')
.data(visItems, function (d) { return d.id; }) // update the data
.attr('x', function(d) { return x1(d.start); })
.attr('width', function(d) { return x1(d.end) - x1(d.start); });
rects.enter().append('rect') // draw the new elements
.attr('x', function(d) { return x1(d.start); })
.attr('y', function(d) { return y1(d.lane) + .1 * y1(1) + 0.5; })
.attr('width', function(d) { return x1(d.end) - x1(d.start); })
.attr('height', function(d) { return .8 * y1(1); })
.attr('class', function(d) { return 'mainItem ' + d.class; });
rects.exit().remove(); // remove the old elements
}

Resources