D3 data binding [D3js in Action] - d3.js

I'm new to d3.js, and am working my way through the book "D3.js in action". So far I have been able to figure out all the questions I had, but this one I can't completely answer on my own, it seems.
I post the source code from the book here, since it is available on the books website and the authors homepage. This is the bl.ocks: http://bl.ocks.org/emeeks/raw/186d62271bb3069446b5/
The basis idea of the code is to create a spreadsheet-like layout out of div elements filled with fictious twitter data. Also implemented is a sort function to sort the data by timestamp and reorder the sheet. As well as a function to reestablish the original order.
Here is the code (I left out the part where the table structure is created, except the part where the data is bound):
<html>
<...>
<body>
<div id="traditional">
</div>
</body>
<footer>
<script>
d3.json("tweets.json",function(error,data) { createSpreadsheet(data.tweets)});
function createSpreadsheet(incData) {
var keyValues = d3.keys(incData[0])
d3.select("div.table")
.selectAll("div.datarow")
.data(incData, function(d) {return d.content})
.enter()
.append("div")
.attr("class", "datarow")
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
d3.selectAll("div.datarow")
.selectAll("div.data")
.data(function(d) {return d3.entries(d)})
.enter()
.append("div")
.attr("class", "data")
.html(function (d) {return d.value})
.style("left", function(d,i,j) {return (i * 100) + "px"});
d3.select("#traditional").insert("button", ".table")
.on("click", sortSheet).html("sort")
d3.select("#traditional").insert("button", ".table")
.on("click", restoreSheet).html("restore")
function sortSheet() {
var dataset = d3.selectAll("div.datarow").data();
dataset.sort(function(a,b) {
var a = new Date(a.timestamp);
var b = new Date(b.timestamp);
return a>=b ? 1 : (a<b ? -1 : 0);
})
d3.selectAll("div.datarow")
.data(dataset, function(d) {return d.content})
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
function restoreSheet() {
d3.selectAll("div.datarow")
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
}
</script>
</footer>
</html>
What I don't fully understand is how sortSheet and restoreSheet work.
This part of sortSheet looks like it rebinds data, but after console logging I think it doesn't actually rebind data to the DOM. Instead it just seems to redraw the div.tablerow elements based on the array index of the sorted array.
But then what purpose does the key-function have?
And why is the transition working? How does it know which old element to put in which new position?
EDIT:
---After some more reading I now know that selectAll().data() does indeed return the update selection. Apparenty the already bound data identified by the key function is re-sorted to match the order of the keys in the new dataset? Is that correct?
So the update selection contains the existing div.datarow s, but in a new ordering. The transition() function works on the new order, drawing the newly ordered div.datarow s beginning with index 0 for the first element to determine its position on the page, to index n for the last element. The graphical transition then somehow (how? by way of the update selection?) knows where the redrawn div.datarow was before and creates the transition-effect.
Is that correct so far?---
d3.selectAll("div.datarow")
.data(dataset, function(d) {return d.content}) //why the key function?
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
And what happens when the original order is restored? Apparently during both operations there is no actual rebinding of data, and the order of the div.datarows in the DOM does not change. So the restore function also redraws the layout based on the array index.
But what kind of selection does the .transition() work on? Is it an update? It is an update.
And why does the drawing using the index result in the old layout? Shouldn't the index of the DOM elements always be 0,1,...,n? I think it is. Apparently the old page layout is redrawn, with the DOM never having changed. But how can the transition() function create the appropriate graphical effect?
function restoreSheet() {
d3.selectAll("div.datarow")
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
I have been thinking for hours about this, but I can't find the correct answer I think.
Thanks for your help!

It all becomes clear when you understand where all these functions were called: inside the json function, where the data was originally bound. When a button calls the sortSheet function, a new array of objects is made and bound to the rows. The transition simply starts with the original order and move the rows according to the new order of the objects inside the array.
And what happens when the original order is restored?
Now comes the interesting part: restoreSheet is called inside the json function and has no access to the dataset variable. So, the data restoreSheet uses is the original data. Then, a transition simply moves the rows according to the order of the objects inside the original array.
I just made a fiddle replicating this: https://jsfiddle.net/k9012vro/2/
Check the code: I have an array with the original data. Then, a button called "sort" creates a new array.
When I click "original" the rectangles move back to the original position. But there is nothing special in that function, no new data being bound:
d3.select("#button1").on("click", function(){
rects.transition()
.duration(500).attr("x", function(d, i){ return i * 30})
});
It moves all the rectangles to the original positions because this function uses the same original data.

Related

D3.js binding nested data

I'm really new to coding, and also to asking questions about coding. So let me know if my explanation is overly complex, or if you need more context on anything, etc.
I am creating an interactive map of migration flows on the Mediterranean Sea. The flows show origin and destination regions of the migrant flows, as well as the total number of migrants, for Italy and Greece. Flows should be displayed in a Sankey diagram like manner. Because I am displaying the flows on a map and not in a diagram fashion, I am not using D3’s Sankey plugin, but creating my own paths.
My flow map, as of now (curved flows are on top of each other, should line up next to each other)
For generating my flows I have four points:
2 points for the straight middle part of the flow (country total)
1 point each for the curved outer parts (origin and destination region), using the two points of the straight middle part as starting points
The straight middle and both curved outer parts are each generated independently from their own data source. Flow lines are updated by changing the data source and calling the function again. The flow lines are generated using the SVG path mini-language. In order for the curved outer parts of the flows to show correctly, I need them to be lined up next to each other. To line them up correctly, I need to shift their starting points. The distance of the shift for each path element is determined by the width of the path elements before it. So, grouping by country, each path element i needs to know the sum of the width of the elements 0-i in the same group.
After grouping my data with d3.nest(), which would allow me to iterate over each group, I am not able to bind the data correctly to the path elements
I also can't figure out a loop function that adds up values for all elements 0-i. Any help here? (Sorry if this is kind of unrelated to the issue of binding nested data)
Here is a working function for the curved paths, working for unnested data:
function lineFlow(data, flowSubGroup, flowDir) {
var flowSelect = svg.select(".flowGroup").select(flowSubGroup).selectAll("path");
var flow = flowSelect.data(data);
var flowDirection = flowDir;
flow.enter()
.append("path").append("title");
flow
.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
.transition()
.duration(transitionDur)
.ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 + Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
flowSelect
.select("title")
.text(function(d) {
return d.region + "\n"
+ "Number of migrants: " + addSpaces(d.totalmig)});
};
I tried adapting the code to work with data grouped by country:
function lineFlowNested(data, flowSubGroup, flowDir) {
var g=svg.select(".flowGroup").select(flowSubGroup).append("g").data(data).enter();
var gflowSelect=g.selectAll("path");
var gflow=gflowSelect.data (function(d) {return d.values});
gflow.enter()
.append("path");
gflow.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
// .transition()
// .duration(transitionDur)
// .ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 - Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
};
which isn't working. What am I doing wrong? Thanks for any hints!

Reload nested data in D3.js

I do not manage to update a bar-chart with nested data in D3.js with new data.
I have nested data of the form:
data = [[1,2,3,4,5,6],[6,5,4,3,2,1]];
I managed to visualize the data by first appending a group for every subarray.
In the groups I then add the arrays as data (simplified):
function createGraph(l, svg){
var g = svg.selectAll("g")
.data(l)
.enter().append("g");
var rect = g.selectAll("rect)
.data(function(d){return d;})
.enter().append("rect")
. ...
}
However, when call the function again with different data, nothing happens.
It seems like in the second row, the rects do not get updated.
I have created a full example over at jsBin: http://jsbin.com/UfeCaGe/1/edit?js,output
A little more explanation of Lars' bug-catch, since I'd already started playing around...
The key was in this section of the code:
var group = svg.selectAll("g")
.data(l)
.enter().append("g");
The variable group is assigned the enter selection, not the raw selection. Then in the next line:
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
You end up defining bar as only the rectangles that are children of just-entered groups. So even though you were handling update correctly for the rectangles, that whole section of code wasn't even running. You need to save the group selection before branching the chain to deal with entering groups:
var group = chart.selectAll("g")
.data(dt);
group.enter().append("g");
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
Also, you're missing a j in your function declaration in your update. And you can reduce code duplication by putting your rectangle update code after your rectangle enter code, and then any attributes that get set in the update don't have to be specified for enter. (Some older examples don't use this pattern, because the original versions of d3 didn't automatically transfer newly-entered elements to the main selection.)
// enter
bar.enter().append("rect")
.attr("fill", function(d,i,j){
return colors(j);})
.attr("height", 0);
// update
bar.attr("transform", function(d, i, j) {
x = "translate("+(i*2.2*w+j*w)+",0)";
return x; })
.transition()
.duration(750)
.attr("width", w)
.attr("height", function(d){return d*10;});

Confused about data joins, select and selectAll

I'm confused about data joins.
I have an entering group element, called gEnter, to which I append
gEnter.append("g").attr("class", "dataLabels");
dataLabels is the container element for each data label I will make.
g is the update selection for the original group element. I bind my data like this:
var dataLabels = g.select(".dataLabels")
.selectAll(".dataLabel")
.data(function(d) {return d;});
where d is coming from the parent g element. For each new data point I append a .dataLabel, and give it a starting position 30 pixels up from the axis:
var dataLabelsEnter = dataLabels.enter()
.append("g")
.attr("class", "dataLabel")
.attr("transform", function(d, i) { return "translate("+ (xScale(d.category) + (xScale.rangeBand() / 2)) +","+(yScale(0) - 30)+")"; });
Each .dataLabel is itself a container for two text elements, so I append them for each new data point:
dataLabelsEnter.append("text")
.attr("class", "category")
.attr("text-anchor", "middle")
.style("font-weight", function(d, i) {
return (d.category == 'Total')
? 'bold'
: 'normal';
})
.text(function(d) {return d.category;});
dataLabelsEnter.append("text")
.attr("class", "value")
.attr("text-anchor", "middle")
.attr("transform", "translate(0,20)")
.style("font-weight", "bold")
.style("fill", function(d, i) {
return (d.count >= 0)
? '#1f77b4'
: '#BB1A03';
})
.text(function(d) {
var accounting = d3.format(",");
return (d.count >= 0)
? '+$' + accounting(d.count)
: '-$' + accounting(-d.count);
});
I then move to my update code, where things get interesting. First, I update the position of the container .dataLabel element. This works well:
dataLabels
.transition()
.duration(duration)
.attr("transform", function(d, i) {return "translate("+ (xScale(d.category) + (xScale.rangeBand() / 2)) +","+( yScale(d3.max([d.count,0])) - 30)+")"; });
Now I want to update the values of my labels. I try this:
dataLabels
.selectAll(".value")
.text(function(d, i) {
var accounting = d3.format(",");
// return d.count;
return (d.count >= 0)
? '+$' + accounting(d.count)
: '-$' + accounting(-d.count);
});
but it doesn't work. I try rebinding the data, using a .data(function(d){return d;}), but to no avail. No matter what I do, even if the data updates, here it's still the same as the initial draw. However, if I switch to
dataLabels
.select(".value")
.text(function(d, i) {
var accounting = d3.format(",");
// return d.count;
return (d.count >= 0)
? '+$' + accounting(d.count)
: '-$' + accounting(-d.count);
});
it works.
Can anyone explain why the latter selection gets the updated the data, but the former selection doesn't? I've read Mike Bostock's recent article on selections, but am still a little confused. I believe it has something to do with this sentence from the article:
Only selectAll has special behavior regarding grouping; select preserves the existing grouping.
Perhaps selectAll is creating new groups from each .dataLabel element, but the data is not being bound to them? I'm just not sure.
The difference is that selection.select propagates data from parent to child, whereas selection.selectAll does not. Read the paragraph you quoted again, in Non-Grouping Operations section:
Only selectAll has special behavior regarding grouping; select preserves the existing grouping. The select method differs because there is exactly one element in the new selection for each element in the old selection. Thus, select also propagates data from parent to child, whereas selectAll does not (hence the need for a data-join)!
So, when you did the data join on dataLabels, you’ve updated the data on the parent elements. But when you call dataLabels.selectAll(".value"), it doesn’t propagate data, so you were getting the old child data. If you switch to dataLabels.select(".value"), it propagates data to the selected children, so you get the new data again.
You could have propagated the data using selection.data, too, but since each label has one value element here, using selection.select is easier.
(Also, you might want to specify a key function.)

Accessing elements associated with data using D3 after they have been generated

I have a dataset, each item has been linked to svg rects using D3.
var bars = svg_content.selectAll("rect")
.data(dataset);
.enter()
.append("rect")
Assume the generation is complete (i.e. the .enter() process is complete and the rects have been generated).
How would I access the rect associated with a specific index of that dataset (for instance, the rect linked to the third piece of data)?
You can use selection.filter or the function form of the commonly used selection.select depending on your needs:
var third = selection.filter(function(d, i) { return i == 2; });
// Equivalently
var third = selection.select(function(d, i) { return i == 2; });
There are a few ways to do this. Generally, in d3, you tend to access the data from within a selection. So you would see something like:
var bars = svg_content.selectAll("rect")
.data(dataset);
.enter()
.append("rect")
.attr('class', function(d) { return d.myName; });
Here d is the data item from dataset that is associated with a particular rect. That code would class each rect with the "myName" property of each data item.
Let's say some you want to place one of these rects specially. One with myName='aName'. We will select that rectangle and set the 'tranform' attribute based on the associated data.
svg.content.selectAll('rect.aName')
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + 20 ')'; })
Note that in both cases you can also access the item's index and if it's relevant also the parent index (use function(d,i,j) {...})
Finally, though I don't encourage it in general, I have for unit tests directly accessed the data associated with an element with __data__. For example with jQuery:
$.find("svg rect.aName")[0].__data__;
You can play with a quick fiddle here

How to properly add and use D3 Events?

I'm having trouble understanding using D3 events and dispatch functions. I have a chart example that I've been working on called: "Vertical Bar Charts With Legends."
Drawing the charts and the legends was easy enough but I'd like to add the ability to highlight each bar as I mouseover its correlating text legend, located to the right of the chart.
I've read through all of the event documentation and even looked at a number of examples, most of which are pretty complicated, but I seem to be missing something. Would anyone know how to best accomplish the text legend mouseover functionality that dispatches events to automatically change colors of the corresponding vertical bars?
This question is similar to the one you posted in the d3-js Google Group. Without duplicating what I wrote there, I would reiterate that you probably don't want d3.dispatch; that is intended for custom event abstractions (such as brushes and behaviors). It'll be simpler to use native events.
If you want your legend to change the color of the corresponding bar on mouseover, then breakdown the problem into steps:
Detect mouseover on the legend.
Select the corresponding bar.
Change the bar's fill color.
First, use selection.on to listen for "mouseover" events on the legend elements. Your listener function will be called when the mouse goes over a legend element, and will be called with two arguments: the data (d) and the index (i). You can use this information to select the corresponding bar via d3.select. Lastly, use selection.style to change the "fill" style with the new color.
If you're not sure how to select the corresponding bar on legend mouseover, there are typically several options. The most straightforward is to select by index, assuming that the number of legend elements and number of rect elements are the same, and they are in the same order. In that case, if a local variable rect contains the rect elements, you could say:
function mouseover(d, i) {
d3.select(rect[0][i]).style("fill", "red");
}
If you don't want to rely on index, another option is to scan for the matching bar based on identical data. This uses selection.filter:
function mouseover(d, i) {
rect.filter(function(p) { return d === p; }).style("fill", "red");
}
Yet another option is to give each rect a unique ID, and then select by id. For example, on initialization, you could say:
rect.attr("id", function(d, i) { return "rect-" + i; });
Then, you could select the rect by id on mouseover:
function mouseover(d, i) {
d3.select("#rect-" + i).style("fill", "red");
}
The above example is contrived since I used the index to generate the id attribute (in which case, it's simpler and faster to use the first technique of selecting by index). A more realistic example would be if your data had a name property; you could then use d.name to generate the id attribute, and likewise select by id. You could also select by other attributes or class, if you don't want to generate a unique id.
Mike's answer is great.
I used it come up with this for selecting a cell in a grid I was drawing:
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y) #displays the cell x y location
d3.select(this).style("fill", "red");
So when I am entering the data in I added the event listener and using d3.select(this).
See the code in context below:
vis.selectAll("rect")
.data(singleArray)
.enter().append("svg:rect")
.attr("stroke", "none")
.attr("fill", (d) ->
if d.lifeForm
return "green"
else
return "white")
.attr("x", (d) -> xs(d.x))
.attr("y", (d) -> ys(d.y))
.attr("width", cellWidth)
.attr("height", cellHeight)
.on('click', (d, i) ->
console.log("X:" + d.x, "Y:" + d.y)
d3.select(this).style("fill", "red");
return
)

Resources