I'd like to create a stacked bar chart using DC.JS.
I've tried to utilize the documentation from DC.JS (graph,source code) to no avail - Below is my attempt (here is my attempt in jsfiddle) and my most recent attempt in CodePen.
I'd like the 'Name' as the X axis and 'Type' as the stacks.
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
<script src="https://rawgithub.com/NickQiZhu/dc.js/master/web/js/crossfilter.js"></script>
<script src="https://cdnjs.site44.com/dc2.js"></script>
<div id="chart"></div>
Javascript
var data = [ {"Name":"Abby","Type":"Apple"}, {"Name":"Abby","Type":"Banana"}, {"Name":"Bob","Type":"Apple"} ]
data.forEach(function(x) {
x.Speed = +x.Type;
});
var ndx = crossfilter(data)
var xdim = ndx.dimension(function (d) {return d.Name;});
function root_function(dim,stack_name) {
return dim.group().reduce(
function(p, v) {
p[v[stack_name]] = (p[v[stack_name]] || 0) + v.Speed;
return p;},
function(p, v) {
p[v[stack_name]] = (p[v[stack_name]] || 0) - v.Speed;
return p;},
function() {
return {};
});}
var ydim = root_function(xdim,'Type')
function sel_stack(i) {
return function(d) {
return d.value[i];
};}
var chart = dc.barChart("#chart");
chart
.x(d3.scale.ordinal().domain(xdim))
.dimension(xdim)
.group(ydim, "1", sel_stack('1'))
.xUnits(dc.units.ordinal);
for(var i = 2; i<6; ++i)
chart.stack(ydim, ''+i, sel_stack(i));
chart.render();
I've been fiddling with this for some time and I have some additional findings:
When I replace the data array with the following it works
data = [ {"Name":"Abby","Type":"1"}, {"Name":"Abby","Type":"2"}, {"Name":"Bob","Type":"1"} ]
But it only works when I swapped out dc 1.7.5 (https://cdnjs.cloudflare.com/ajax/libs/dc/1.7.5/dc.min.js) with dc 2.1.0-dev (https://github.com/dc-js/dc.js/blob/develop/web/js/dc.js)
However when I replace the data array with the following it doesn't work:
data = [ {"Name":"Abby","Type":"3"}, {"Name":"Abby","Type":"4"}, {"Name":"Bob","Type":"2"} ]
I believe the root issue lies in the root_function.
v.Speed is always NaN in your current example. Because +x.Type attempts to convert a string like "Apple" into a number and fails. If you just want to count, then add or subtract 1 in your reducer, rather than v.Speed. Then you need to update your sel_stack code and chart code to handle this change, of course.
Here's a working example for the 2 types in your data. You'll have to update it to handle arbitrary numbers of types, probably by building an array of all your types up front and then looping through it to add stacks to the chart: http://codepen.io/anon/pen/GjjyOv?editors=1010
var data = [ {"Name":"Abby","Type":"Apple"}, {"Name":"Abby","Type":"Banana"}, {"Name":"Bob","Type":"Apple"} ]
var ndx = crossfilter(data)
var xdim = ndx.dimension(function (d) {return d.Name;});
In the reducer, just add and subtract 1 to count:
var ydim = xdim.group().reduce(
function(p, v) {
p[v.Type] = (p[v.Type] || 0) + 1;
return p;},
function(p, v) {
p[v.Type] = (p[v.Type] || 0) - 1;
return p;},
function() {
return {};
});
sel_stack no longer takes a number, but a key:
function sel_stack(valueKey) {
return function(d) {
return d.value[valueKey];
};}
var chart = dc.barChart("#chart");
Here we hard-code the stack key, for the purpose of the example:
chart
.x(d3.scale.ordinal().domain(xdim))
.dimension(xdim)
.group(ydim, "Apple", sel_stack('Apple'))
.xUnits(dc.units.ordinal);
Again, the other hard-coded stack key. You'll need to recreate the loop after creating some sort of data structure that holds all of your stack values.
//for(var i = 2; i<6; ++i)
chart.stack(ydim, 'Banana', sel_stack('Banana'));
chart.render();
Related
I know there are a lot of questions on that specific subject on SO but none of the solutions seem to work in my case.
This is my data :
var theData = [{
"value": "190.79",
"age_days": "22",
"criteria": "FX"
}, {
"value": "18.43",
"age_days": "22",
"criteria": "FX"
}, {...}]
I put the data into buckets as such :
var getAge = (d) => {
if ((d.age_days) <= 7) {
return (["1w", "2w", "1m", "3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 14) {
return (["2w", "1m", "3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 30) {
return (["1m", "3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 90) {
return (["3m", "6m", "1y", "All"]);
} else if ((d.age_days) <= 180) {
return (["6m", "1y", "All"]);
} else if ((d.age_days) <= 360) {
return (["1y", "All"]);
} else {
return (["All"]);
}
};
var ndx = crossfilter(theData);
var dims = {};
var groups = {};
dims.age = ndx.dimension(getAge,true);
groups.age = {};
groups.age.valueSum = dims.age.group().reduceSum((d) => d.value);
I then try to order the group using the fake group approach :
var sort_group = (source_group, order) => {
return {
all: () => {
let g = source_group.all();
let map = {};
g.forEach(function (kv) {
map[kv.key] = kv.value;
});
return order
.filter((k) => map.hasOwnProperty(k))
.map((k) => {
return {key: k, value: map[k]}
});
}
};
};
var the_order = ["1w", "2w", "1m", "3m", "6m", "1y", "All"];
var the_sorted_age_group = sort_group(groups.age.valueSum, the_order);
then I create the barChart using
theAgeChart
.height(200)
.width(400)
.dimension(dims.age)
.group(the_sorted_age_group)
.valueAccessor((d) => d.value)
.x(d3.scaleBand())
.xUnits(dc.units.ordinal);
But it still comes out using the default sort :
I've created a jsFiddle here which contains everything.
How can I get my bars sorted as I want them to be sorted ?
When elasticX is true or the x scale domain is not set, the coordinate grid mixin will generate the X domain
if (_chart.elasticX() || _x.domain().length === 0) {
_x.domain(_chart._ordinalXDomain());
}
(source)
That totally makes sense, but it always sorts the domain when it generates it:
_chart._ordinalXDomain = function () {
var groups = _chart._computeOrderedGroups(_chart.data());
return groups.map(_chart.keyAccessor());
};
(source)
I guess we could consider not sorting the domain when ordering is null.
Anyway, one workaround is to set the domain yourself:
.x(d3.scaleBand().domain(the_order))
You don't need to sort the group for a bar chart. (For a line chart, the group order must agree with the scale domain, but it doesn't matter for the bar chart.)
With the domain set, this also works:
.group(groups.age.valueSum)
Fork of your fiddle.
I guess the moral of the story is that it's complicated to generate charts automatically. Most of the time one does want the X domain sorted, but what's the best way to allow the user to provide their own sort?
I would not say this is the best way, but there is a way to make it work.
I am using working with the code from How to create a row chart from multiple columns.
However I need to get the averages of the data. I normally do this via value accessor but am unsure of how to do this with this functionality.
function regroup(dim, cols) {
var _groupAll = dim.groupAll().reduce(
function(p, v) { // add
cols.forEach(function(c) {
p[c] += v[c];
});
return p;
},
function(p, v) { // remove
cols.forEach(function(c) {
p[c] -= v[c];
});
return p;
},
function() { // init
var p = {};
cols.forEach(function(c) {
p[c] = 0;
});
return p;
});
return {
all: function() {
// or _.pairs, anything to turn the object into an array
return d3.map(_groupAll.value()).entries();
}
};
}
I just need to be able to get the current sum of each column and divide it by the count of the rows based on the current filter state.
The code is similar to the one in this fiddle multi column fiddle
It seems that the easiest way to get the count of records is to create another crossfilter.groupAll() with the default reduction:
var _recordCount = dim.groupAll();
Then you can divide each sum by the current count when rotating the results:
all: function() {
var count = _recordCount.value();
// or _.pairs, anything to turn the object into an array
return d3.map(_groupAll.value())
.entries().map(function(kv) {
return {key: kv.key, value: kv.value / count};
});
}
Here's the updated fiddle: http://jsfiddle.net/37uET/32/
I'm new to dc.js so I am probably missing out on something, I have time series data that I am trying to display on 3 time dimensions: by month, by day and by hour - basically to display machine (manufacturing) efficiency across time.
So I made a barchart for the months and hours,and a linechart for the days. code below:
var cfdata = crossfilter(dataArray);
var avgadd = function(p,v) {
p.count++;
p.sum += v.efficiency;
p.avg = p.sum/p.count;
return p;
},
avgremove = function(p,v) {
p.count--;
p.sum -= v.efficiency;
p.avg = p.sum/p.count;
return p;
},
avginit = function(){
return {
count: 0,
sum: 0,
avg: 0
}
};
var parseDate = d3.time.format('%a %b %d %Y %H:%M:%S GMT%Z (UTC)').parse;
dataArray.forEach(function(d) {
d.date = parseDate(d.date);
d.hour = d3.time.hour(d.date).getHours();
d.day = d3.time.day(d.date);
d.month = d3.time.month(d.date);
// d.year = d3.time.year(d.date).getFullYear();
});
// dimensions: efficiency by hour
var hourDim = cfdata.dimension(function(d){return d.hour});
var hourlyEff = hourDim.group().reduce(avgadd, avgremove, avginit);
// dimensions: efficiency by day
var dayDim = cfdata.dimension(function(d){return d.day});
var minDay = dayDim.bottom(1)[0].date;
var maxDay = dayDim.top(1)[0].date;
var dailyEff = dayDim.group().reduce(avgadd, avgremove, avginit);
// dimensions: efficieny by month and year
var monthDim = cfdata.dimension(function(d){return d.month});
var minMonth = monthDim.bottom(1)[0].date;
var maxMonth = monthDim.top(1)[0].date;
var monthlyEff = monthDim.group().reduce(avgadd, avgremove, avginit);
dc.barChart(".month-eff-chart")
.height(180)
.width(900)
.dimension(monthDim)
.group(monthlyEff)
.valueAccessor(function(p){return p.value.avg})
.centerBar(true)
.x(d3.time.scale().domain([minMonth, maxMonth]))
.xUnits(d3.time.months)
.xAxis().ticks(d3.time.month, 1).tickFormat(d3.time.format("%b %y"));
dc.lineChart(".day-eff-chart")
.height(180)
.width(900)
.dimension(dayDim)
.group(dailyEff)
.valueAccessor(function(p){return p.value.avg})
.elasticX(true)
.x(d3.time.scale())
.xUnits(d3.time.days)
.xAxis().ticks(d3.time.week, 1).tickFormat(d3.time.format("%W/%y"));
dc.barChart(".hour-eff-chart")
.height(180)
.width(900)
.dimension(hourDim)
.group(hourlyEff)
.valueAccessor(function(p){return p.value.avg})
.x(d3.scale.linear().domain([0,23]));
dc.renderAll();
so when I open the page and filter across any of the barcharts, the other barchart will filter just fine, but the linechart will just become empty - the lines will completely disappear. when I change the linechart to a barchart, it works - filters just fine.
I must be missing something here.
also, suggestions on how to display time series data in a more meaningful way is also welcome. Thanks!
EDIT: JsFiddle: http://jsfiddle.net/shuzf2vm/2/
The reason that this doesn't work is that your averages will produce NaNs when the count is zero. You need to protect your divisions like this:
var avgadd = function(p,v) {
p.count++;
p.sum += v.efficiency;
p.avg = p.count ? p.sum/p.count : 0;
return p;
},
avgremove = function(p,v) {
p.count--;
p.sum -= v.efficiency;
p.avg = p.count ? p.sum/p.count : 0;
return p;
},
Working fork: http://jsfiddle.net/gordonwoodhull/hsqa43uk/3/
The reason it behaves differently for bar charts versus line charts is interesting. Both protect the output to the drawing functions and detect NaNs. But the bar chart protects each bar individually, while the line chart outputs a single multi-segment line and throws the whole line out if any point is bad.
(It might be more helpful to use d3.svg.line.defined here, if anyone cares to file a PR.)
I'm trying to get just one value from d3's max function but it's returning an entire array. Here is an example:
var data = {
"Jim" : [
{
"Value" : [10,11,12]
}
]
}
var myMax = d3.max(data.Jim, function(d){
var maxVal = d["Value"];
return maxVal;
})
console.log(myMax + "max")
It returns 10,11,12max. It should return 12.
you're trying to find the maximum of the array data.Jim which only has one element = {"Value" : [10,11,12]} which d3 promptly returns as the maximum using your given accessor function. Try changing your code to the following:
var myMax = d3.max(data.Jim, function(d){
var maxVal = d3.max(d["Value"]);
return maxVal;
})
I currently have a sample pie chart in the js fiddle that is able to do a static update between two datasets (with the second dataset having more values)
[1,2,3,4,5]
[1,2,3,4,5,6,7,8,9,10]
http://jsfiddle.net/qkHK6/115/
My aim is to get the data join animation depicted in
http://bl.ocks.org/mbostock/5682158
There obviously is quite a bit of complication when Mike is using the findPreceding and findNeighbouring functions and these are obviously used to form the animation with the new data.
function findNeighborArc(i, data0, data1, key) {
var d;
return (d = findPreceding(i, data0, data1, key)) ? {startAngle: d.endAngle, endAngle: d.endAngle}
: (d = findFollowing(i, data0, data1, key)) ? {startAngle: d.startAngle, endAngle: d.startAngle}
: null;
}
// Find the element in data0 that joins the highest preceding element in data1.
function findPreceding(i, data0, data1, key) {
var m = data0.length;
while (--i >= 0) {
var k = key(data1[i]);
for (var j = 0; j < m; ++j) {
if (key(data0[j]) === k) return data0[j];
}
}
}
// Find the element in data0 that joins the lowest following element in data1.
function findFollowing(i, data0, data1, key) {
var n = data1.length, m = data0.length;
while (++i < n) {
var k = key(data1[i]);
for (var j = 0; j < m; ++j) {
if (key(data0[j]) === k) return data0[j];
}
}
}
However I'm really struggling to apply it to my own js fiddle graph I'm unsure how to apply the information between two simple data arrays as obviously the key functions are not really needed.
If anyone could point me in the right direction or would be able to give some advice I'd appreciate it as I've attempted this several times and I'm still trying to get to grips d3 and its syntax.
To animate a transition for a pie chart where values are added, the same tween function as with the same number of elements can be used (see e.g. this example). The only difference is the initialisation for the new arcs -- instead from starting from where they actually are, they start from 0 to make it appears as if they're coming in.
The trick is therefore to take this into account when setting ._current:
.each(function(d) {
this._current = {data: d.data,
value: d.value,
startAngle: 0,
endAngle: 0};
});
Apart from that, the code is basically the same as for the example I've linked to above. Complete example here. I've simplified your code (e.g. removed the additional g elements) so that the core update code is easier to understand.