Not understanding d3 v4 new general update pattern in nested data - d3.js

I've used previous versions of D3 and coming back at v4 I've encountered I don't quite grasp the new update pattern. I have nested data and I wish to update my visualization with new child nodes, here is a minimum example:
function update(data) {
var rows = d3
.selectAll("div")
.data(data)
.enter()
.append("div")
.classed("row", true)
var cells = rows
.selectAll("div")
.data(function(d) { return d })
cells
.enter()
.append("div")
.classed("cell", true)
.text(function(d) {return d})
}
var arr = [
[1, 2, 3],
[1, 2, 3],
[1, 2, 3]
]
var button = d3.select("body")
.append("button")
.text("update")
.on("click", modifyData);
function modifyData(){
arr.forEach(row => {
row.push(4)
});
update(arr);
}
update(arr);
https://jsfiddle.net/59qnhb8d/
I would expect my viz to update with a column of 4's. Any hints appreciated.

You need to update and MERGE the new divs with the old divs:
function update(data) {
var rows = d3
.selectAll(".row")
.data(data)
rows.exit().remove();
var newRows = rows.enter()
.append("div")
.classed("row", true)
var cells = rows.merge(newRows) //here you tell it to merge new rows with existing rows
.selectAll(".cell")
.data(function(d) { console.log(d); return d })
cells.exit().remove();
var newCells = cells
.enter()
.append("div")
.classed("cell", true)
cells
.merge(newCells)
.text(function(d) {return d})
}
Fiddle here: https://jsfiddle.net/59qnhb8d/31/

A few things have likely changed here is Mike Bostocks General Update Pattern. Here is a fiddle with a working update Fiddle. There are 4 important steps in the update pattern:
1) Binding the new data
2) Removing nodes no longer needed
3) Merging non-removed nodes with the newly created ones
4) Updating nodes.
The change is in the update function, the updated version is below.
I went with the .each method for each row created to have better control over the nesting, and without having to do any modifications to the data object. The .each method uses the same pattern, just on its own nested nodes.
function update(data) {
var rows = d3.select("body").selectAll(".row").data(data); // bind data
rows.exit().remove(); // remove old nodes
rows.enter().append("div") // add new nodes
.classed("row", true)
.merge(rows) // merge new with existing and update all
.each(function(d){
let el = d3.select(this);
let cells = el.selectAll(".cell").data(d); // bind data
cells.exit().remove(); //remove old
cells.enter().append("div") //enter new
.classed("cell", true)
.merge(cells) //merge
.text(d=>d) // set text on all nodes.
});
}

Related

Updating only those SVG elements where the underlying bound data has been modified

I have a force simulation graph using d3 v4. Each node is bound to some data, which I use for example to determine the radius of each node.
The underlying bound data is updated periodically, and for some nodes it changes, and for others it stays the same.
I want to be able to select just those DOM elements for which the bound data changes, so that I can highlight these elements on my graph.
For example, suppose that initially my data (which is bound to the forceSimulation nodes) is:
data = [{id: 1, type: 0}, {id: 2, type: 1}]
and it is then updated to:
data = [{id: 1, type: 1}, {id: 2, type: 1}]
I'd like to be able to select just the DOM element that corresponds to id=1 so that I can for example make the colour change temporarily.
The update selection contains both id=1 and id=2 - I could maintain an internal mapping of previous data values and compare, but this seems inefficient.
Thanks,
Adam
If a single datum attribute can be checked to see if the bound data has changed, one method would be to track that attribute as a property using selection.property and a custom property such as type. When appending the data you could define the property fairly easily:
.append("circle")
.property("type",function(d) { return d.type; });
Then, when updating, you could filter based on which data are matching or not matching the property:
circles.data(newdata)
.filter(function(d) {
return d.type != d3.select(this).property("type")
})
This filter will return those elements that have changed their type. Now we can re-assign the property to reflect the new type and transition those filtered elements.
The snippet below should demonstrate this, the datum is just a number one or two (represented by blue and orange), and is used to set the property type. Click on the svg to update the data, only those circles which change their datum will temporarily change their radius, while also changing their color to reflect their new datum.
var svg = d3.select("body")
.append("svg")
.attr("width",400)
.attr("height",400);
var circles = svg.selectAll("circle")
.data(data())
.enter("circle")
.append("circle")
.attr("cy",function(d,i) {
return Math.floor(i/5) * 40 + 20;
})
.attr("cx", function(d,i) {
return i%5 * 40 + 20
})
.attr("r", 8)
.attr("fill",function(d) { return (d) ? "steelblue" : "orange"})
.property("type",function(d) { return d; });
// update on click:
svg.on("click", function() {
circles.data(data())
.filter(function(d) {
return d != d3.select(this).property("type") // filter out unchanged data
})
.property("type",function(d) { return d; }) // update to reflect new data
.transition()
.attr("r", 20)
.attr("fill","crimson")
.duration(500)
.transition()
.attr("fill",function(d) { return (d) ? "steelblue" : "orange" })
.attr("r",8)
.duration(500);
})
function data() {
var output = [];
d3.range(20).map(function(d) {
output.push(Math.round(Math.random()));
})
return output;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

How to pass subselection of data to child node?

If I have nested array like this
var ar = [[[1,0],[2,0],[3,0]], [[1,0],[2,0],[3,0]]]
I want to create two svg elements, this is easy
var svg = d3.select('div.main`)
.selectAll('svg')
.data(ar)
.enter()
.append('svg')
And now I want to bind subarrays to svg selection, something like this
var g = svg.selectAll('g')
.data(function(d,i) {return d[i];})
.enter()
.append('g')
after that the data attached to g should be
[[1,0],[2,0],[3,0]]
I know what this line is not correct .data(function(d,i) {return d[i];}) Just do not know how to explain it different way.
If I understand the question correctly,
Your are right, the issues arise from the identified line. You don't need to return d[i] as the data for the new selection, d represents each individual datum associated with each svg, d[i] represents only a one part of each datum.
If you want each datum, in its entirety, just append a g as normal:
var g = svg.append("g");
Try console.log on g.data() and you will see that your data is there still as you want, it is bound to each g.
You can then use each of these datums, bound to each g and carried over from each svg, as data to create new features. Passing the datum looks like: .data(function(d) { return d; }). The snippet below should help put it all together:
var data = [[[10,10],[30,30],[50,50]], [[10,20],[80,30],[50,60]] ];
var svg = d3.select('body')
.selectAll('svg')
.data(data)
.enter()
.append('svg')
.attr("height",100)
.attr("width",200);
var g = svg.append("g");
console.log("Data Bound To First G in First SVG:")
console.log(g.data()[0]);
console.log("Data Bound To Second G in Second SVG:")
console.log(g.data()[1]);
// Data is now available to make features:
g.selectAll("circle")
.data(function(d) { return d; })
.enter()
.append("circle")
.attr("r",10)
.attr("cx",function(d) { return d[0] })
.attr("cy",function(d) { return d[1] });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>

D3 Multiple Pie Chart Updates

I am quite new to D3 but have been working through some mbostocks examples but hitting an issue when trying to update multiple pie charts. I can generate these fine from my data array but when I want to update them I run into an issue.
The issue is quite simple but I am a little stuck on how to fix this. I have run up my code in js fiddle that can be found here. You will see that in my example I build three pies, then wait 3 seconds and update these to new data. The issue I have is that all pies always seem to get updated with the same data.
I believe this is due to the way I am making the path selection in order to update the pie. it looks like I am updating each all the paths each time with each data array so they all end up being updated with the last dataset in my array.
If anyone knows how I can update this in order to correctly build the pies I would be very grateful of any help, pointers or comments.
var data = [
[3, 4, 5, 9],
[1, 7, 3, 4],
[4, 3, 2, 1],
];
function getData() {
// Generate some random data to update the pie with
tdata = []
for(i in data) {
rdata = []
for(c in data[i]) {
rdata.push(Math.floor((Math.random() * 10) + 1) )
}
tdata.push(rdata)
}
return tdata
}
// ------------
var m = 10,
r = 100
var mycolors = ["red","#FF7F00","#F5CC11","#D61687","#1E93C1","#64B72D","#999999"]
var arc = d3.svg.arc()
.innerRadius(r / 2)
.outerRadius(r)
var pie = d3.layout.pie()
.value(function(d) { return d; })
.sort(null);
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter()
.append("svg")
.attr("width", (r + m) * 2)
.attr("height", (r + m) * 2)
.attr("id", function(d,i) {return 'pie'+i;})
.append("svg:g")
.attr("transform", "translate(" + (r + m) + "," + (r + m) + ")");
var path = svg.selectAll("path")
.data(pie)
.enter()
.append("svg:path")
.attr("d", arc)
.style("fill", function(d, i) { return mycolors[i]; })
.each(function(d) { this._current = d; }); // store the initial angles
var titles = svg.append("svg:text")
.attr("class", "title")
.text(function(d,i) {return i;})
.attr("dy", "5px")
.attr("text-anchor", "middle")
// -- Do the updates
//------------------------
setInterval(function() {
change()
}, 3000);
function change() {
// Update the Pie charts with random data
piedata = getData()
svg.each(function(d,i) {
path = path.data(pie(piedata[i]))
path.transition().duration(1000).attrTween("d", arcTween);
})
// temp, print new array to screen
tdata = ""
for(x in piedata) {
tdata += "<strong>"+x+":</strong> "+piedata[x]+"<br>"
}
$('#pieData').html(tdata)
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Right, I finally got this working and am posting the working solution incase others are trying to do the same thing.
I expect this might not be the best nor most efficient way of doing it but this is going to be fine for what I need (at this point). But if anyone still has any better solutions it would be good to hear from you.
I ended up selecting the paths based on a unique id that I gave the individual SVG elements which I created, then just updated these paths only. Sounds simple now when I say it like this but did have me stumped for a while.
function change() {
// Update the Pie charts with random data
var newdata = getData()
for(x in newdata) {
var npath = d3.select("#pie"+x).selectAll("path").data(pie(newdata[x]))
npath.transition().duration(1000).attrTween("d", arcTween); // redraw the arcs
}
}
Full working copy can be found at http://jsfiddle.net/THT75/nskwwbnf/

How do I dynamically update displayed elements in D3

I have a function that loads an initial array of points onto a map using D3:
var fires = []; //huge array of objects
function update(selection) {
feature = g.selectAll("path")
.data(selection);
feature.attr("class", "update");
feature.enter().append("path")
.attr("class", "enter")
.style("fill", function(d) {return colorScale(d.area)});
feature.exit().remove();
}
I call it initially with a large array:
update(fires);
I also have a histogram that allows me to narrow down the selection based on years. Using a D3 brush, on 'brushend' I call a function called brushed:
function brushed() {
if (!d3.event.sourceEvent) return; // only transition after input
var extent0 = brush.extent();
startYear = Math.floor(extent0[0]);
endYear = Math.floor(extent0[1]);
var selectedFires = fires.filter(function(fire) {
if(fire.year >= startYear && fire.year <= endYear) return fire;
});
console.log(selectedFires.length); //does reflect the proper amount of elements
update(selectedFires);
}
When I narrow the selection, the points on the map disappear as expected. When I widen it, the points/elements do not return. The array is getting the correct elements in it, they're just not being appended to the SVG.
I'm missing something fundamental as the examples I've seen: http://bl.ocks.org/mbostock/3808218 appear to append elements just fine.
Without seeing the rest of the code (which really helps), and focusing on your selection piece alone, try this:
function update(selection) {
// binding the data
feature = g.selectAll(".path")
.data(selection);
// exit selection
feature
.exit()
.remove();
// enter selection
feature
.enter()
.append("path")
.attr("class","path");
// update selection
feature
.style("fill", function(d) {return colorScale(d.area)});
// update selection
feature
.style("fill", function(d) {return colorScale(d.area)})
.attr("d",path); // this was the missing piece!
}
NOTE: you also want to comment out where you hardcoded the extent of the brush:
//brush
var brush = d3.svg.brush()
.x(areaYearScale)
//.extent([1984, 2013]) // comment this out
.on("brushend", brushed);

How not to skip the first element with .data()

The data array contains exactly 5 objects.
The following code displays only the last 4, and skip the first :
d3.csv(csv_data, function(error, data){
var table = d3.select("#div");
var tr = table.selectAll("tr")
.data(data)
.enter()
.append("tr");
var td = tr.selectAll("td")
.data(function(d){return [d.x,d.y];})
.enter()
.append("td")
.html(function(d){return d;});
});

Resources