Mike Bostock uses the following snippet to generate uniformly-spaced bins for a histogram:
var data = d3.layout.histogram()
.bins(x.ticks(20))
(values);
source
Is there any way to adapt this to a project that uses dc.js and crossfilter.js?
Essentially, I want to dynamically generate the bins and then use the chart to filter on a particular attribute. Total newbie to this. Any guidance would be appreciated!
dc.js supports histograms via crossfilter. Use a group for your bar chart that looks something like this:
var binwidth = 0.2;
var dim = ndx.dimension(function(d) { return d.x; });
var group = dim.group(function(d) { return binwidth * Math.floor(d.x/binwidth); });
This tells crossfilter to use keys binwidth apart.
And initialize the bar chart with these units:
chart.xUnits(dc.units.fp.precision(binwidth));
Related
I'm trying to create a number of interactive pie charts using dc.js to represent a basketball team's stats. One pie chart would have each player's points, another their salary, etc., represented as a portion of the team's total.
I don't really understand how to use the crossfilter .dimension() and .group() functions. And does reduceSum() really need to be in there? All I'm getting is an empty pie chart. Other examples I've seen have differenty structured data and don't apply to this situation.
Here is a bit of the relevant code for a salary pie chart:
var data = [
{"player":"boomhauer", "rebound_avg":12.1, "salary":4000, "point_avg":15},
{"player":"bill", "rebound_avg":4.2, "salary":3000, "point_avg":20},
{"player":"hank", "rebound_avg":12.1, "salary":4000, "point_avg":15},
{"player":"dale", "rebound_avg":16.1, "salary":6000, "point_avg":4},
];
var ndx = crossfilter(data);
var playerDim = ndx.dimension(function(d) {return d.player;});
var salaryPerPlayer = playerDim.group().reduceSum(function(d) {return d.salary;});
var salaryGroupPerPlayer = salaryPerPlayer.group(function(d) {return d.player; });
var salaryRingChart = dc.pieChart("#chart-ring-salary");
salaryRingChart
.width(200)
.height(200)
.slicesCap(17)
.innerRadius(10)
.dimension(salaryPerPlayer)
.group(salaryGroupPerPlayer)
.renderLabel(true);
dc.renderAll();
All I'm getting is an empty pie chart.
Thanks for your help!
I think just lose the salaryGroupPerPlayer:
var data = [
{"player":"boomhauer", "rebound_avg":12.1, "salary":4000, "point_avg":15},
{"player":"bill", "rebound_avg":4.2, "salary":3000, "point_avg":20},
{"player":"hank", "rebound_avg":12.1, "salary":4000, "point_avg":15},
{"player":"dale", "rebound_avg":16.1, "salary":6000, "point_avg":4},
];
var ndx = crossfilter(data);
var playerDim = ndx.dimension(function(d) {return d.player;});
var salaryPerPlayer = playerDim.group().reduceSum(function(d) {return d.salary;});
var salaryRingChart = dc.pieChart("#chart-ring-salary");
salaryRingChart
.width(200)
.height(200)
.slicesCap(17)
.innerRadius(10)
.dimension(salaryPerPlayer)
.group(salaryPerPlayer)
.renderLabel(true);
dc.renderAll();
Here's a good Crossfilter tutorial: http://blog.rusty.io/2012/09/17/crossfilter-tutorial/
The reduceSum is necessary because the default aggregation on a group in Crossfilter is a count. This would just count the number of records for each player, which is 1. So, not very interesting!
I would question why you are using Crossfilter at all though. The data is all pre-aggregated and you are only going to have 1 dimension (player). Filtering on these charts won't really make sense. Crossfilter is designed more for scenarios in which you have dis-aggregated data (maybe 1 record for each player in each game, o even play-by-play data) and you want to aggregate the data and filter dynamically on different dimensions.
I have summed up the dataset using nest,rollup and d3.sum functions and I'm able to display the pie chart properly.But I'm unable display percentage for each slice based on the summed total.Can anyone please give suggestions on this issue...
Mycode:
======
d3.csv("pi.csv", function(error, csv_data) {
var data = d3.nest()
.key(function(d) { return d.ip;})
.rollup(function(d) {
return d3.sum(d, function(g) {return g.value; });
}).entries(csv_data);
data.forEach(function(d) {
d.ip= d.key;
d.value = d.values;
});
My dataset:
=========
ip,timestamp,value
92.239.29.77,1412132430000,3190
92.239.29.77,1412142430000,319011
92.239.29.78,1412128830000,545568
92.239.29.78,1412130600000,616409
92.239.29.78,1412132430000,319087
92.239.29.76,1412130600000,616409
92.239.29.76,1412132430000,319087
Thanks in advance
So it seems that you want to display the percentage of each pie slice on the chart. All you really need to do is to calculate the the total of the values and iterate across the data variable adding in the percentage.
The data.forEach function doesn't add anything to the data variable, so I would drop that. I should also point out that the d3.nest function rolls up and sums each individual ip entry, so you're not getting sum of all values. However, this is pretty easy to do with d3, you can just use d3.sum:
var tots = d3.sum(data, function(d) {
return d.values;
});
Once you've done that you iterate across the data variable like:
data.forEach(function(d) {
d.percentage = d.values / tots;
});
You can then access the percentage on each pie slice using something like d.data.percentage
And putting it all together.
Note you could also compute the percentage from the start and end angles for each slice: (d.endAngle - d.startAngle)/(2*Math.PI)*100.
For example if I have the following csv file:
category, number, total
A,1,3
A,2,5
A,3,1
B,1,4
B,2,6
B,3,1
C,1,5
C,2,2
C,3,4
I was able to follow the following example and separate out the data into different csv files and composing each one.
github link
However, I was wondering how would I recreate the same lineCharts if I were to only have a single csv file and separate each lineChart by each grouped category.
Thanks.
#minikomi's answer is the straight d3 way to do this.
The dc.js/crossfilter way to do this (if you want your charts to reduce values for each key and interact/filter with other dc charts) is to reduce multiple values in a single group like this:
var group = dimension.group().reduce(
function(p, v) { // add
p[v.type] = (p[v.type] || 0) + v.value;
return p;
},
function(p, v) { // remove
p[v.type] -= v.value;
return p;
},
function() { // initial
return {};
});
https://github.com/dc-js/dc.js/wiki/FAQ#rows-contain-a-single-value-but-a-different-value-per-row
Then you can specify each line chart by passing the group along with an accessor to the .group method like so:
lineChartA.group(group, 'A', function(a) { return x.A; })
lineChartB.group(group, 'B', function(a) { return x.B; })
If you want to combine the line charts in a single chart, you can compose them with the composite chart or series chart
You can reduce the data to give 3 different arrays, each which only contain data from each category:
var grouped = data.reduce(function(o,d) {
if(o[d.category]) {
o[d.category].push(d);
} else {
o[d.category] = [d];
}
return o;
}, {});
Usually in d3 we work with arrays of data, so I'd use d3.map to convert it to an array of pairs key / value
var lineData = d3.map(grouped).entries()
Now, you can use this to create your lines (leaving out creating scales x and y), svg element etc.:
var line = d3.svg.line()
.x(function(d){return x(d.number)})
.y(function(d){return y(d.total)})
var linesGroup = svg.append("g")
var lines = linesGroup.data(lineData).enter()
.append("line")
.attr("d", function(d){return line(d.value)})
You could also set the stroke color using the d.key for the d3.map entries (which will come from the key we used in the reduce step - the category). Don't forget to convert your csv data to numbers too using parseInt().
I'm trying to draw an area chart using dc.js, and the end date (i.e. far right) of the chart is based on the current date, not the last date in the dataset. In cases where there's a date gap between data points, I want the area to extend from one point to the next, not draw at 0.
Given this data:
var data = [
{domain: "foo.com", project: "pdp", repo: "myrepo", commit_date: "6/1/2014", lines_added: 100, lines_deleted: 50},
{domain: "foo.com", project: "pdp", repo: "myrepo", commit_date: "7/1/2014", lines_added: 100, lines_deleted: 50}
];
var ndx = crossfilter(data);
The chart's line/area currently ends at the "7/1/2014" data point, but I want it to stretch the entire length of the chart.
The relevant code for drawing the chart is:
var dateDim = ndx.dimension(function(d) {return d.commit_date;});
var minDate = dateDim.bottom(1)[0].commit_date;
var maxDate = new Date();
var domainGroup = dateDim.group().reduceSum(function(d) {return d.cumulative_lines;});
unshippedlineChart
.width(500).height(200)
.dimension(dateDim)
.group(domainGroup)
.renderArea(true)
.x(d3.time.scale().domain([minDate,maxDate]))
.brushOn(false)
.interpolate('step-after')
.yAxisLabel("Unshipped Value");
Full example is at http://jsfiddle.net/xayhkcvn/1/
You didn't actually ask a question :-), but I think you may be looking for ways to prefilter your data so that it gets extended to today, and to remove any zeros.
This stuff isn't built into dc.js, but there is some example code in the FAQ which may help. Specifically, there is a function remove_empty_bins which adapts a group to remove any zeros.
You could similarly define a function to add a final point (untested):
function duplicate_final_bin(source_group, key) {
return {
all:function () {
var ret = Array.prototype.slice.call(source_group.all()); // copy array
if(!ret.length) return ret;
ret.push({key: key, value: ret[ret.length-1].value});
return ret;
}
};
}
You can compose this with remove_empty_bins:
var super_group = duplicate_final_bin(remove_empty_bins(domainGroup), maxDate);
The idea is to create a wrapper object which dynamically adds or remove stuff from the (always changing) source_group.all() on demand. dc.js will call group.all() whenever it is redrawing, and these wrappers intercept that call and adapt the data the crossfilter group returns.
I am using this library: Dimensional Charting to build some relatively standard charts that need CrossFilter functionality.
I have been following the examples but they aren't working for me.
Here is my code:
var dashData = crossfilter(data.report),
dataByHour = dashData.dimension(function(d){ return d3.time.hour(new Date(d.timestamp))}),
totalByHour = dataByHour.group().reduceSum(function(d) { return d.amount }),
dc.barChart("#graphTimeOverview")
.width(990) // (optional) define chart width, :default = 200
.height(250) // (optional) define chart height, :default = 200
.transitionDuration(500) // (optional) define chart transition duration, :default = 500
.margins({top: 10, right: 50, bottom: 30, left: 40})
.dimension(dataByHour) // set dimension
.group(totalByHour) // set group
.elasticY(true)
.centerBar(true)
.gap(1)
.x(d3.time.scale().domain([new Date(data.report[0].timestamp), new Date(data.report[(data.report.length - 1)].timestamp)]))
.round(d3.time.hour.round)
.xUnits(d3.time.hours)
.renderHorizontalGridLines(true);
dc.renderAll();
I know the crossfilter data is working correctly, here is a sample of the group:
totalByHour:
[ {key:(new Date(1361746800000)), value:6170.17},
{key:(new Date(1361678400000)), value:3003},
{key:(new Date(1361581200000)), value:2350.42},
{key:(new Date(1361667600000)), value:1636.19},
etc...
]
Unfortunately all this gets me is an empty graph, it seems to compute the y-axis correctly, so it would seem to me that it can read the data, however I never see any bar values:
Maybe the data.report array is not sorted by timestamp (the sample provided is unsorted). In your code, you assume that those values are sorted. You can try using
// Compute the timestamp extent
var timeExtent = d3.extent(data.report, function(d) { return d.timestamp; });
dc.barChart("#graphTimeOverview")
// more settings here
.x(d3.time.scale().domain(timeExtent.map(function(d) { return new Date(d); })))
.round(d3.time.hour.round)
.xUnits(d3.time.hours)
.renderHorizontalGridLines(true);
It would be easier to tell what is the problem if you provide a jsFiddle.
I started using dc.js few days ago, so I don't know for sure. But, I think your code should go in
d3.csv("data.csv", function(data) { //your-code };
or
d3.json("data.json", function(data) {//your-code};
or
jQuery.getJson("data.json", function(data){//your-code});