D3: incorporate two datasets in bin - histogram layout - javascript

In D3:
I have created histograms with a multi column array, "age" and "gender", using the "age" column. My histogram is made with just the "age" data. But I want to be able to also have the corresponding "gender" data stored with or in the bins for the histogram. That way I could calculate something about dealing with "gender" data for a bin when I mouseover that bin - and, again, that bin (and the whole histogram) is built on the "age" data.
Sample data:
{
"age":[22,21,30,26,28,26,23,32,30,26,34,23,33,23,34,28,34,35,31,15,26,34,32,21,35,28,27,26,24,19,21,32,23,23,23,23,28,32,22,26,32,21,25,30,23,30,21,20,28,28,26,26,31,39,25,30,36,23,38,30,30,31,23,22,28,26,23,32,26,34,28,30,24,27,26,38,24,30,34,25,28,35,22,27,26,25,29,32,26,30,33,33,31,31,37,28,27,28,29,16],
"gender":["1","0","0","0","1","1","1","1","1","1","0","1","0","0","1","0","0","0","1","1","1","1","0","0","1","0","1","0","0","1","0","0","1","0","0","1","1","0","1","1","0","1","0","0","1","1","0","0","1","0","1","1","1","0","1","1","1","0","1","1","0","1","0","1","1","1","1","1","0","1","1","0","1","0","1","1","1","1","0","0","1","0","1","1","0","0","1","1","0","1","1","1","0","1","1","0","0","1","1","0"]
}
Here is some of the code. I think it is all that corresponds directly to this issue but let me know if more is needed:
var histogram = d3.layout.histogram()
.frequency(false)
.bins(x.ticks(10));
var canvas = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("100sample.json", function(error, data) {
//var allData = histogram(allAge.age);
var allAge = histogram(data.age);
var allBars = canvas.selectAll(".allBar")
.data(allAge)
.enter().insert("rect", ".axis")
.attr("class", "allBar")
.attr("x", function(d) { return x(d.x) + 1; })
.attr("y", function(d) { return y(d.y); })
.attr("width", x(allAge[0].dx + allAge[0].x) - x(allAge[0].x) - 1)
.attr("height", function(d) { return height - y(d.y); })
.on('mouseover', allTip.show)
.on('mouseout', allTip.hide);
I have everything working with "mouseover" and building the histogram, etc. Every age value has a corresponding gender value, but the gender value does not change the bins / histogram. I was thinking there is something at play with adding the gender values to
.data()
but somehow not calling them on the histogram layout?
How do I incorporate this extra data that corresponds to the data being used for the histogram?
--- Update ----
I understand it is necessary to bind the entire dataset to the svg / canvas allBars variable so that I can access allAge for the histogram and gender for the metadata I am trying to calculate on mouseover.
I have:
var total = [allAge, data.gender];
after creating the allAge histogram from data.age. This may not be the best way to bring in all data at once...
Now, instead of:
.data(allAge)
I have:
.data(total)
So I need to index total[0] on the histogram construction and total[1] on the metadata that will be associated with mouseover. How do I go about indexing within the allbars var - specifically within the .attr to build the histogram?

You're really close!
What you want is to call .data(data) to bind all your data, not just the ages. This will make the data available to the callbacks bound with .on at the end. So why are you passing in allAge? Well, because that's what you're getting back from the histogram function. The docs state that
The return value is an array of arrays: each element in the outer array represents a bin, and each bin contains the associated elements from the input values.
Good - so you don't lose the data by running it through the histogram function. But won't it get confused by having two arrays in a object? That's where the accessor function comes in. Chain it to where you define histogram. You might get away with function(d){return d.age} for the callback, or you may need to use an array length 2 and go by index. You can also try zipping your arrays.
I don't think you'll need to modify your .attr calls, since they operate on the bins, not the data inside them. I also recommend taking another look at how you set the width - again, you'll have to fiddle with it, but perhaps function(d){return x(d.dx) -1} would work better. It's a bit of a red flag to access the data directly in these, even if dx should be the same.
Hope that's helpful. Let me know if I'm totally off base (since I didn't write your code for you).

Related

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.

D3 pie chart transition

I'm studying transitions in D3js, trying to get them working with a simple pie chart that is derived from a Mike Bostock example.
I want to transition the data in the pie from data to data2, but the chart does not update.
This is the transition statement:
path.selectAll("path").data(pie(data2)).transition().duration(2000);
What am I missing?
EDIT
I've got it working with the below. Now I'd like to understand why this is working. I understand the .attr("d",arc) part, but why do I need to selectAll("path")?
path.selectAll("path").data(pie(data2)).transition().duration(2000).attr("d",arc);
END EDIT
The complete code (JSFiddle here):
var width = 200,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var data = [2, 19, 18, 99, 100];
var data2 = [100, 1200, 20, 88, 12];
var pie, arc, svg, path, data;
var chartCanvas = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height);
var path = chartCanvas
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function setupPieChart() {
pie = d3.layout.pie()
.value(function (d) {
return d;
})
.sort(null);
arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20)
path
.selectAll("path")
.data(pie(data))
.enter()
.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function (d) {
this._current = d;
}); // store the initial angles
console.log(path);
}
setupPieChart();
console.log(path);
path.selectAll("path").data(pie(data2)).transition().duration(2000);
.transition starts a transition for the attributes change(s) declared after it. You don't set / do anything after .transition, so there is nothing to interpolate over. In the original example from Mike, you'll see he sets the d attribute after starting the transition, specifying a custom interpolator (arcTween)
I immediately see you are missing some important part of the update process. You copied the original code, but you forgot the update part :-). I can tell, because I see in your code you store the initial angles.
Look again at the code here and try to understand the function arcTween. More information can be found here. You need an arc tween function to calculate the new angles based on the initial angles (which is why you stored the initial angles in the first place :-).
I won't do the fiddle at the moment, cause in my experience, you learn more if you try to understand the arc tween function (as I did here . This is a link to a personal project of mine, but feel free to copy code as you see fit).
You need to .selectAll("path") as those are the actual elements that will update. When doing d3, try to think of the chart elements as following: Elements that are not visible yet (which is enter collection), elements that are visible now (which can be seen as the update collection) and elements that can be removed (the exit collection).
You need to see those elements based on the data you want to visualize. D3 is data driven documents, so everything is in relation to the data you want to show on the screen: if you have data but no elements yet, you do an "enter" of elements. So you do a selection of elements that are not in the DOM yet, but will soon be, because you will bind them to the data you have.
If you already have elements on the screen, and the number of elements matches the data you need to show (for example: var data = [20 ,30 , 40], you got 3 pieces of data here for 3 div's on the screen, you got a matching amount), then d3 will update your selection (hence the update collection or update selection) so the elements properties match the data.
If you have more elements on the screen then there is data to show, then you can do an exit of elements (again, hence the exit selection or collection). I hope that made it a bit more clear and that it made sense as well :-)

D3.js not updating data

I am updating a D3 stacked bar graph in combination with AngularJS. In this, all data is initially visible, and updates filter out undesired data. In the past it has worked without issue using this model:
data = [{
name: John Doe,
splits: [{distance: 1, time: 1234},]
},...]
Now I am attempting to to add one more bar to each stack using this model:
data = [{
name: John Doe
time: 12345,
splits: [{distance: 1, time: 1234},]
},...]
My issue is updating the data. My calculated values are recalculated correctly, such as the domain for scaling. A line for time update still only recognizes the data values from before the update (code snippet heavily truncated for brevity):
// Set ranges of values in axes
x.domain(data.map(function(d) { return d.name}));
y.domain([ min , max]);
// Y Axis is updated to the correct time range
chart.append('g').attr('class', 'y axis').call(yAxis).selectAll('text').style('font-family','Open Sans').style('font-size', '.9rem');
// data join / Select Athlete
var athlete = chart.selectAll(".athlete").data(data),
athdata = athlete.enter(),
console.log(data) // data is correct here
// add container for each athlete
athplace = athdata.append("g")
.attr("class", "athlete")
.attr("transform", function(d) { return "translate(" + x(d.name) + ",0)"; })
.text(function(d) { return d.name}),
// ENTER time
athplace.append('rect')
.attr('class', "time")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.time.time); })
.attr("height", 0).transition().duration(300)
.attr("height", function(d) { return height-y(d.time); });
... enter splits & labels
// exit splits & athlete
splitlabels.exit().transition().duration(300).attr('opacity', 0).remove();
splits.exit().transition().duration(300).attr('height', 0).attr('opacity', 0).remove();
athlete.exit().transition().duration(300).attr('width',0).attr('opacity', 0).remove();
console.log(athlete) // data is still correct
// UPDATE time, this chain has the problem with data not being updated. "d" is equal to previous values
athlete.selectAll('rect.time')
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.time.time); })
.attr("height", function(d) { return height-y(d.time); });
Due to these errors, the updated columns represent the wrong data and produce the wrong visualization. I have been staring/testing at this all day trying to isolate the issue to what I have now. Could someone more experienced with D3 give me some insight?
Note: for those interested, this is all in an Angular directive where I $watch for changes to the data values, although I am 100% sure that that is not the issue.
Edit
Here is a JSFiddle that illustrates the same error as in my script. All of the code was extracted directly from the script exhibiting the issue. The interval update at the bottom imitates the swapping of data that would normally happen.
I played with your example for a little bit and made a few changes that may be of use.
First, I re-factored your global var's up top to inside your update function. By doing this, we are not "double-appending" where our x and y axis left off. This was seemingly drawing our chart on top of our old chart.
However, fixing this then gave me the new chart overlaying on top of our old. To fix this, I call d3.selectAll("svg > *").remove(); within update to remove all attached SVG elements and groupings, giving us a "clean slate" to render our new chart on. This solves the overlaying issue.
You mention this is shortened for brevity, so I hope this example can help with your actual application
Updated Fiddle

Improving D3 Sequence Sunburst Example

This D3 example served as my starting point:
http://bl.ocks.org/kerryrodden/7090426
I wanted to change data that feeds the diagram, and I made following new example:
http://jsfiddle.net/ZGVK3/
One can notice at least two problems:
Legend is wrong. This is because it still contains 'hardcoded' names from original example.
All nodes are colored black. This is because the color scheme is also 'hardcoded' only for node names from original example.
How to improve the original example (or my jsfiddle, it doesn't matter) so that legend and coloring are self-adjusted to the data that feeds the diagram?
You can use an ordinal scale to map colors to the different node names. Implementing it would only require a few minor changes to your existing code.
Step 1. Create an ordinal scale for the colors
Instead of having colors be simply a list of color names, hard-coded to specific names, use d3.scale.ordinal(), and set the .range() to be an array of the colors you want to use. For example:
var colors = d3.scale.ordinal()
.range(["#5687d1","#7b615c","#de783b","#6ab975","#a173d1","#bbbbbb"]);
This would create an ordinal scale that uses the same colors as the original visualization. Since your data would require more colors, you would want to add a few more to your range, otherwise colors will be repeated.
As a shortcut, you can use d3.scale.category20() to let d3 choose a range 20 categorical colors for you.
Now when setting the fill colors for your path element arcs and also your breadcrumbs, you would simply use colors(d.name) instead of colors[d.name].
Step 2. Use your data to construct the domain of the scale
The .domain() of this scale will be set once we have the data, since it will depend on a list of the unique names contained in the data. To do this, we can loop through the data, and create an array of the unique names. There are probably several ways to do this, but here's one that works well:
var uniqueNames = (function(a) {
var output = [];
a.forEach(function(d) {
if (output.indexOf(d.name) === -1) {
output.push(d.name);
}
});
return output;
})(nodes);
This creates an empty array, then loops through each element of the nodes array and if the node's name doesn't already exist in the new array, it is added.
Then you can simply set the new array to be the domain of the color scale:
colors.domain(uniqueNames);
Step 3. Use the scale's domain to build the legend
Since the legend is going to depend on the domain, make sure the drawLegend() function is called after the domain is set.
You can find the number of elements in the domain (for setting the height of the legend) by calling colors.domain().length. Then for the legend's .data(), you can use the domain itself. Finally, to set the fill color for the legend boxes, you call the color scale on d since each element in the domain is a name. Here's what those three changes to the legend look like in practice:
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", colors.domain().length * (li.h + li.s));
var g = legend.selectAll("g")
.data(colors.domain())
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return colors(d); });
And that's about it. Hope that helps.
Here's the updated JSFiddle.

d3.js: How to add a data key?

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

Categories