d3.js: How to add a data key? - javascript

I'm learning D3.js and trying to get my head around data keys used with streamgraphs. I would like to adapt the official streamgraph example:
...so that each path has an explicit data key, and so that the mouseover logs the data key.
The official example adds paths as follows:
var area = d3.svg.area()
.x(function(d) { console.log('x', d.data); return d.x * w / mx; })
.y0(function(d) { return h - d.y0 * h / my; })
.y1(function(d) { return h - (d.y + d.y0) * h / my; });
vis.selectAll("path")
.data(data0)
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area);
I tried adapting the code as follows, but I'm not sure how to change the structure of data0 (currently an array of arrays) to achieve what I want:
vis.selectAll("path")
.data(data0, function(d) { return d.name }) // Add key function
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area)
.on("mouseover", function (d,i) {
console.log("mouseover", d.name); // Log name property on mouseover
});
As it stands, without my having made any changes to the structure of data0, it unsurprisingly does not work. How can I add a name property to data0 without also messing up the area and .data() functions?
UPDATE: To be a bit clearer: the D3 docs say that the area function is expecting a two-dimensional array. So if I change data0 from a two-dimensional array, to an array of objects, each with a name key and a data key, how can I also change what I pass to area?

The data in the example doesn't have a "name" property, so you would need to add that to the data to use it. The data keys you refer to are used when merging/updating data, i.e. you have drawn some paths already and then update (some of them). The .data() function will try to figure out what data is updated and what data is new. If that doesn't work for you, you can use the data key to help it, i.e. in your case tell it that things with the same name are the same data.

If what you mean by data keys are "data legends", then you might want to take a look at the following examples where I've completely separated the placement of magnitudes, legend bullets and legend text in different areas of the charts.
Multiple D3 Pie Charts Mixed In With HTML Layout Constructs
Multiple D3 Horizontal Bar Charts Mixed In With HTML Layout Constructs
In each of the examples, you'll clearly see how the data is labeled, structured, passed in, and used.
I also tied them together through mouseover and mouseout events so that mousing over or out of any element causes all elements in a set to change color simultaneously.
I hope this helps.
Frank

Related

Getting color as a JSON input to a d3 bullet chart

I am following the following D3 bullet chart example, trying to modify it a bit so that the different colors of the ranges are also included in the JSON: http://www.d3noob.org/2013/07/introduction-to-bullet-charts-in-d3js.html. The reason for the change is that I need the colors to be dynamic and depend on various things.
This exists one other place in the forum, but old and unsolved. I should add that I am a total newbie to d3, and don't have a lot of JavaScript experience in general.
Here is the JSON I use. "rangecolor" will in the future be an array of different colors, as there are several ranges, but for simplicity I attempt only with one color to begin with.
{
"title":"Memory Used",
"subtitle":"MBytes",
"ranges":[256,512,1024],
"rangecolor": "red",
"measures":[768],
"markers":[900]
}
Now, getting an idea of how to use it, I looked at the working example for title:
var title = svg.append("g")
.style("text-anchor", "end")
.attr("transform", "translate(-6," + height / 2 + ")");
title.append("text")
.attr("class", "title")
.text(function(d) { return d.title; });
The problem is that I cannot get the following to work:
d3.selectAll(".bullet .range.s0")
.style("fill", function(d) { return d.rangecolor; });
The following does work:
d3.selectAll(".bullet .range.s0")
.style("fill", function(d) { return "red"; });
And I can also extract the rangecolor value to the title:
title.append("text")
.attr("class", "title")
.text(function(d) { return d.rangecolor; }); //works - title is now "red"
My approach might be misguided, so any help on how to best include color ranges to the JSON and using it would be much appreciated.
The problem is that when you select all bullets, there is no data bound to them so d is undefined here:
d3.selectAll(".bullet .range.s0")
.style("fill", function(d) { return d.rangecolor; });
Why? You did not perform a data join like this:
d3.selectAll('.something').data(somethingData)
.style('fill', function (d) { // d is defined });
You should wonder why it works for the title on the contrary. This is because when you do this:
var title = svg.append("g");
title inherits data from the svg selection. See Mike Bostock explanation. In fact I was myself not aware of this behaviour, I prefer performing data joins explicitly.
I don't know the overall structure of your code, but you might apply you rangecolor properties with data inheritance (as for title) or refactor to use explicit data joins.
I have talked with some experienced developers that state that the tutorial I am following is not a good one. It is a bit messy, and I am trying to find something cleaner. Troubleshooting has been difficult in this regard, and
The exact issue presented here was solved by using d3.selectAll(".bullet .range.s0").data(data).

d3js stacked bar chart not updating

I am experimenting with a stacked bar chart in d3js and ran into enter exit selection difficulties. I used the d3.stack to get two arrays organized by keys, then I tried to follow the general update pattern. The problem I'm having now is the data is not getting updated when I click a different state in the dropdown menu. Here's the problem code and a link to the full project: http://plnkr.co/edit/8N8b2yUYRF9zqRkjkIiO?p=preview
var series = g.append("g")
var seriesready =
series.selectAll("g")
.data(stack(data))
.enter().append("g")
.attr("fill",function(d){console.log(d); return z(d.key)}) //not logging
when I update the bar chart
var rectangles =
seriesready.selectAll("rect")
.data(function(d){return d})
rectangles.exit().remove()
rectangles.enter().append("rect")
.attr("width", x.bandwidth())
.transition()
.duration(1500)
.attr("transform", function(d) {console.log(d); return "translate(" + x(d.data.Date) + ",0)"; })
.attr("height", function(d) {
return height - y(d[1]-d[0]);
})
.attr("y", function(d) {
return y(d[1]-d[0]);
});
I also think I'm getting confused as to what selections should be removed or added. Would really appreciate any pointers. Data viz is fun to work with, but I still haven't fully grasped data binding yet.
I have not made the switch to version 4 yet, but the data binding methodology is the same i think.
You need to define a key function as the second parameter to the .data() function.
A key function may be specified to control which datum is assigned to
which element, replacing the default join-by-index.
https://github.com/d3/d3-selection/blob/master/README.md#selection_data
Your updated code
http://plnkr.co/edit/wwdjJEflZtyACr6w9LiS?p=preview
The changed code:
var seriesUpdate = series.selectAll("g")
.data(stack(data),d=>d)
var seriesready = seriesUpdate.enter().append("g")
.attr("fill",function(d){return z(d.key)})
seriesUpdate.exit().remove()
When binding data to elements, D3 calculates what data is new/existing/removed in relation to the selection. By default it does this by data index - the size of the input array. Since the computed stack data for michigan and ohio both return 2 sets of data (injured and killed), D3 views this as "same" data, thus it's an update.
If you define a key function, D3 recognizes the computed stack data for michigan and ohio as being "different" data, thus it's an enter.
With a key function, when you select Ohio first, the enter selection is size 2 with Ohio. If you then select Michigan, the enter selection is size 2 with Michigan, and the exit selection is size 2 with Ohio.

How to add an array of annotations to a d3 barchart?

I've managed to draw a barchart (it's inverted I know :)) for each year of data by reading in the CSV data and then using d3.nest() to group the data for each date per year, see block here or blockbuilder here.
However I'm am now trying to append notes from my annotations array to each chart and I'm stuck.
One approach I'm trying is to selectAll(".charts") and to append my nested annotations array i.e. annotationsPerYear. But I'm finding it difficult to join my annotationsPerYear key with my charts and then to iterate and append the notes for each year onto the correct chart. Any ideas?
You can rely on nested selections for this. You're already using nested selection with this bit:
svg.selectAll(".bar")
.data(function(d) {return d.values;})
The above binds different data to each of the 3 SVGs created earlier. It does this by calling the function(d) 3 times, and returns a different d.values each time.
You can do a similar thing to bind and create a different set of annotations in each SVG. You need a function (passed to data()) that returns the applicable annotations per chart, but this time you don't have something pre-computed like d.values. Instead, you have to extract the applicable annotations out of annotations array, using filter():
svg.selectAll(".annotation")
.data(function(d) {
// return annotations whose key matches d's key
return annotations.filter(function(a) { return a.key == d.key; });
})
Then you can use enter() and append() as you've done for the bars to create the text and position it. I'm not sure how you intend to lay it out, but altogether you want something like this:
svg.selectAll(".annotation")
.data(function(d) {
return annotations.filter(function(a) { return a.key == d.key; });
})
.enter()
.append("text")
.attr("class", "annotation")
.attr("x", function(d) { return x(d.xPos); })
.attr("y", function(d) { return d.yPos; })
.text(function(d) { return d.note; })
See:
Updated block
Updated blockbuilder

D3 update circle-pack data new nodes overlap existing nodes

I'm following the General Update Pattern but having an issue with regards to layering.
Using a circle-pack layout, I pack the new data, update, enter and exit the circle elements. However, when new elements enter, they overlap the updated circles.
Data key function is based on element name:
.data(nodes, function(d, i) { return d.name; });
So my circle pack has a spot for the updated circle (of the correct location and size) but it's hidden behind its newly entered parent circle.
Is there a way to send these updated nodes to the front or redraw them over the entered circles?
--UPDATE--
As suggested by the person who closed this issue, I've tried implementing the linked to solution using moveToFront.
I added the following code in my update section (which didn't change anything) and then tried adding it after the enter and exit code, which also didn't make any difference.
.each("end", function(d){ d3.select(this).moveToFront(); });
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
For clarity, this is what the selection and update looks like:
// Load data into svg, join new data with old elements, if any.
var nodes = pack.nodes(postData);
node = root = postData;
groupNodes = svg.selectAll("g")
.data(nodes, function(d, i) { return d.name; });
// Update and transition existing elements
groupNodes.select("circle")
.transition()
.duration(duration)
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.attr('r', function(d) { return d.r; })
.each("end", function(d){ d3.select(this).moveToFront(); });
This moveToFront code does not make a difference to my output, and the updated circles remain behind the entered selection circles.
To summarize: the issue seems to be caused by a hierarchy layout (circle-packing) which expects the circles to be drawn in the order of the data's hierarchy. The d3 update pattern (using enter, update and exit selections) causes selected update elements to remain in the svg when the hierarchy is re-drawn, and the new layers are drawn over it. The parents of those nodes are already correctly set, so parentNode.appendChild doesn't do anything in this case, because it's not the cause of the issue.
Here is a fiddle to demonstrate my issue. I've tried putting the moveToFront code in various places, with no visible difference.
When you hit the "Change Data" button, it'll redraw the circles, but any circles whose names overlap between the two data sets are not nested properly in the circle-pack. Children of "Group A" are hidden behind one of the parent circles. You can verify the nodes are there via Inspect Element.
Another pic from the updated fiddle:
D3 provides a way to reorder elements based on the data bound to them with the .sort() function. In your case, the condition to check is the .depth attribute of the elements -- "deeper" elements should appear in front:
svg.selectAll("g")
.sort(function (a, b) {
if (a.depth < b.depth) return -1;
else return 1;
});
Complete demo here.

d3 - sunburst - transition given updated data -- trying to animate, not snap

I am working on a sunburst viz based off of Mike Bostock's Zoomable Sunburst example.
I want to be able to change the underlying data using a whole new JSON (which has the same structure but different 'size' values), and have the sunburst animate a transition to reflect the updated data.
If I change the data of the path elements using .data(), and then attempt to update in the following fashion:
path.data(partition.nodes(transformed_json))
.transition()
.duration(750)
.attrTween("d", arcTween(transformed_json));
(..which is pretty much the exact same code as the click fn)
function click(d) {
path.transition()
.duration(750)
.attrTween("d", arcTween(d));
}
..I find that the sunburst does correctly change to reflect the new data, but it snaps into place rather than smoothly transitioning, like it does when you zoom in.
http://jsfiddle.net/jTV2y/ <-- Here is a jsfiddle with the issue isolated (the transition happens one second after you click 'Run')
I'm guessing that I need to create a different arcTween() fn, but my d3 understanding is not there yet. Many thanks!
Your example is quite similar to the sunburst partition example, which also updates data with a transition. The difference is that in this example it's the same underlying data with different value accessors. This means that you can't save the previous value in the data (as that will be different), but need to put it somewhere else (e.g. the DOM element).
The updated tween function looks like this:
function arcTweenUpdate(a) {
var i = d3.interpolate({x: this.x0, dx: this.dx0}, a);
return function(t) {
var b = i(t);
this.x0 = b.x;
this.dx0 = b.dx;
return arc(b);
};
}
This requires, as in the original example, to save the original x and dx values:
.enter().append("path")
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
});
Complete example here. This one has a kind of weird transition which is cause by the different order of the data in the layout. You can disable that by calling .sort(null), see here.

Categories