How can I have a custom reduce function in my crossfilter groups? - dc.js

I have a numeric field in my dataset. I am doing and dashboard to filter on this field through year, age, and so on. The fact is that this field is a ratio over a sample of the population, therefore when I am creating a var ratesPerAge = ageDim.group().reduceSum(function(d) {return +d.suicidesPercentage}); the rate of each record is summed up since I am using .reduceSum().
My goal is to have something like the average and having each time the average rate on the filtered rows.
I have tried to implement the solution I want using this function I have found in another thread.
var col1DimTotal = col1Dim.group().reduce(reduceAdd, reduceRemove,
reduceInitial);
function reduceAdd(p, v) {
++p.count;
p.total += v.value;
return p;
}
function reduceRemove(p, v) {
--p.count;
p.total -= v.value;
return p;
}
function reduceInitial() {
return {count: 0, total: 0};
}
However, this did not achieve what I wanted.
This is my rate dimension and the grouping with another dimension:
var ratesDim = ndx.dimension(function(d) {return
d.suicidesPercentage;});
var ageDim = ndx.dimension(function(d) {return d.age});
var ratesPerAge = ageDim.group().reduceSum(function(d) {return
+d.suicidesPercentage});
I would like to filter trough the average rate of the rows I am selecting.

I think I solved this and I managed to achieve what I wanted:
var yearDim = ndx.dimension(function(d) { return new Date(d.year); });
function reduceAddAvg(p,v) {
++p.count
p.sum += v.suicidesPercentage;
p.avg = p.sum/p.count;
return p;
}
function reduceRemoveAvg(p,v) {
--p.count
p.sum -= v.suicidesPercentage;
p.avg = p.count ? p.sum/p.count : 0;
return p;
}
function reduceInitAvg() {
return {count:0, sum:0, avg:0};
}
var ratesPerYear = yearDim.group().reduce(reduceAddAvg, reduceRemoveAvg, reduceInitAvg);
yearChart // line chart
.width(660)
.height(400)
.dimension(yearDim)
.group(ratesPerYear).valueAccessor(function (d) {
return d.value.avg;
}) // this is the reference of the AVG
.x(d3.scaleTime().domain([new Date("January 1, 1987 00:00"), new
Date("January 4, 2015 00:00:00")]))
.elasticY(true)
.controlsUseVisibility(true)
.yAxisLabel("Rates over 100k people")
.xAxisLabel("Year")
.centerBar(true);

Related

Dynamic filtering with D3

I'm quite new to D3 and coding in general. I'm trying to set up a bar chart which includes/excludes data depending on a checkbox. I have a set of product groups and countries which I want to toggle in/out of the total represented by the bar. The output should be one bar per product.
My full data set has many more products, product groups and countries so it is not viable to create a key-value pair for each potential combination of checkboxes. Instead I would like to create a function that re-evaluates the checkboxes and re-filters the data and updates the rollup when a checkbox is changed.
I'm not sure where this function should sit in my code or what it should look like... This is what I'm working with at the moment:
var data = data.filter(function(d) {
if (document.getElementById("nz_button").checked) {
return d.country == 'NZ'
}
if (document.getElementById("au_button").checked) {
return d.country == 'AU'
}
if (document.getElementById("us_button").checked) {
return d.country == 'US'
}
})
// to see how many distinct groups there are and sum volume
var products = d3.nest()
.key(function(d) {
return d.product
})
.rollup(function(leaves) {
var sum = 0;
leaves.forEach(function(d) {
sum += d.volume;
})
return sum
})
.entries(data);
Full code: http://plnkr.co/edit/qezdwMLt48RPc8KH17hS?p=preview
Maybe I should be working with selections and re-running the nest/rollup when required?
Any help appreciated. Thanks :)
You can move the full code which makes the graph in a new function like this:
function makeDataGraph(data) {//function to make the graph.
//
// FILTER
//
var data = data.filter(function(d) {
if (document.getElementById("au_button").checked) {
return d.country == 'AU'
}
if (document.getElementById("us_button").checked) {
return d.country == 'US'
}
if (document.getElementById("nz_button").checked) {
return d.country == 'NZ'
}
})
// to see how many distinct groups there are and sum volume
var products = d3.nest()
.key(function(d) {
return d.product
})
.rollup(function(leaves) {
var sum = 0;
leaves.forEach(function(d) {
sum += d.volume;
})
return sum
})
.entries(data);
// sorting on descending total
console.log(products);
products.sort(function(a, b) {
return b.values - a.values
})
var max = d3.max(products, function(d) {
return d.values;
});
var xscale = d3.scale.linear()
.domain([0, max])
.range([0, 600])
var svg = d3.select("svg");
//
// Still needs to be cleaned up \/ \/
//
var rects = svg.selectAll("rect.product")
.data(products)
rects.exit().remove();
rects.enter().append("rect").classed("product", true)
rects.attr({
x: 200,
y: function(d, i) {
return 100 + i * 50
},
width: function(d, i) {
return xscale(d.values)
},
height: 50
}).on("click", function(d, i) {
console.log(i, d);
})
var labels = svg.selectAll("text.label")
.data(products)
labels.exit().remove();
labels.enter().append("text").classed("label", true)
labels.attr({
x: 195,
y: function(d, i) {
return 128 + i * 50
},
"text-anchor": "end",
"alignment-baseline": "middle"
}).text(function(d) {
return d.key || "N/A"
})
var volume = svg.selectAll("text.volume")
.data(products);
volume.exit().remove();
volume.enter().append("text").classed("volume", true)
volume.attr({
x: function(d, i) {
return 205 + xscale(d.values)
},
y: function(d, i) {
return 128 + i * 50
},
"text-anchor": "start",
"alignment-baseline": "middle"
}).text(function(d) {
return d.values || "N/A"
})
}
Remember to do rects.exit().remove(); so that when the data is changed on click of the checkbox, rectangles related to old data is removed.
Now you can call this function from the click event and also afterloading the tsv like this:
d3.tsv("data.tsv", function(err, udata) {
var udata = udata.map(process);
console.log("udata", udata);
var data = udata // making new var to preserve unfiltered data
makeDataGraph(data);//call the function to make graph
function handleClick() { // event handler...
makeDataGraph(data)
}
//add listener to all check boxes.
d3.selectAll(".filter_button").on("click", handleClick);
});
working code here

show percentage in d3 pie chart

currently my pie chart in d3 shows the sum of numbers,i want percentage instead .
for eg: currently i have a pie chart for how many people submitted application who visited our site. our current pie chart shows like this : people submitted 17,000 and people didn't submitted-10,000
but i need this in percentage also. how can i get that.
please find the pie code below and let me know what changes do i need to make this work. I am new to JavaScript and D3.
ko.bindingHandlers.pieChart = {
init: function (element, valueAccessor) {
var _options = valueAccessor();
var _data = _options.transformer(_options.data);
$(element).css("marginLeft", "auto");
$(element).css("marginRight", "auto");
if (typeof _options.maxWidth != "undefined") {
var _max = _options.maxWidth * 1;
$(element).width(Math.min($(element).parent().width(), _max));
}
if ($(element).find("svg").length > 0 && _data.length == 0) {
$(element).find("svg").empty();
}
if (_data.length > 0 && isNaN(_data[0].value)) {
_data = [];
$(element).find("svg").empty();
}
if ($(element).is(":visible")) {
nv.addGraph(function () {
var _chart = nv.models.growingPieChart()
.x(function (d) {
return d.label
})
.y(function (d) {
return d.value
})
.height($(element).width())
.showLabels(true).showLegend(false)
.tooltipContent(function (key, y, e, graph) {
return '<h3>' + hueUtils.htmlEncode(key) + '</h3><p>' + y + '</p>'
});
var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
_d3.datum(_data)
.transition().duration(150)
.each("end", _options.onComplete != null ? _options.onComplete : void(0))
.call(_chart);
if (_options.fqs) {
$.each(_options.fqs(), function (cnt, item) {
if (item.id() == _options.data.widget_id && item.field() == _options.field()) {
_chart.selectSlices($.map(item.filter(), function (it) {
return it.value();
}));
}
});
}
$(element).data("chart", _chart);
var _resizeTimeout = -1;
nv.utils.windowResize(function () {
window.clearTimeout(_resizeTimeout);
_resizeTimeout = window.setTimeout(function () {
_chart.update();
}, 200);
});
$(element).on("forceUpdate", function () {
_chart.update();
});
$(element).height($(element).width());
var _parentSelector = typeof _options.parentSelector != "undefined" ? _options.parentSelector : ".card-widget";
$(element).parents(_parentSelector).on("resize", function () {
if (typeof _options.maxWidth != "undefined") {
var _max = _options.maxWidth * 1;
$(element).width(Math.min($(element).parent().width(), _max));
}
$(element).height($(element).width());
_chart.update();
});
return _chart;
}, function () {
var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
_d3.selectAll(".nv-slice").on("click",
function (d, i) {
if (typeof _options.onClick != "undefined") {
chartsUpdatingState();
_options.onClick(d);
}
});
});
}
},
update: function (element, valueAccessor) {
var _options = valueAccessor();
var _data = _options.transformer(_options.data);
var _chart = $(element).data("chart");
if (_chart) {
var _d3 = d3.select($(element).find("svg")[0]);
_d3.datum(_data)
.transition().duration(150)
.each("end", _options.onComplete != null ? _options.onComplete : void(0))
.call(_chart);
if (_options.fqs) {
$.each(_options.fqs(), function (cnt, item) {
if (item.id() == _options.data.widget_id && item.field() == _options.field()) {
_chart.selectSlices($.map(item.filter(), function (it) {
return it.value();
}));
}
});
}
chartsNormalState();
}
else if (_data.length > 0) {
ko.bindingHandlers.pieChart.init(element, valueAccessor);
}
}
};
A fiddle would be useful to test this against (hint hint), but I'm pretty sure you want to change this line:
.y(function (d) {
return d.value
})
to this
.y(function (d) {
return d.value/total
})
You may have to define total. Like I said, without a jsfiddle or at least some indication of the format of your data, it's hard to determine if this is actually what's wrong or how to fix it.
Note: a pie chart of relative percentages will look exactly the same as a pie chart of the original numbers. You might be able to change the label and only the label, as follows:
return '<h3>' + hueUtils.htmlEncode(key) + '</h3><p>' + y + '</p>'
to this
return '<h3>' + hueUtils.htmlEncode(key) + '</h3><p>' + (y/total) + '</p>'
Hopefully both of those should work. You will have to define total, if it isn't already defined elsewhere. If not:
var total = 0;
_data.forEach(function(d){total += d.value});
Good luck!
It would be even more helpful to include information such as the library you are using and a fully reproducible example using a gist, codepen, jsfiddle, etc. I am guessing you are using hue and more specifically growingPieChart. If my guesses are correct, then you can modify your tooltipContent function similar to #Vyross answer that posted while I was typing this.
.tooltipContent(function (key, y, e, graph) {
return '<h3>' + hueUtils.htmlEncode(key) + '</h3><p>' +
(+y) / d3.sum(
d3.select(graph.container).select('g').datum(),
function(d){return d.value}
) +
'</p>'
});

Is there a way to make a key or legend for a heat map with dc.js?

I made a heat map with dc.js and I was wondering if there is a key or a legend function for heat maps in dc.js. I have searched the internet and can't seem to find a built in way of doing it, so has anyone else tackled this problem?
I faced this same task and what I did is I made a second heat map that is one row long from the min value to the max value.
var range = maxValue - minValue;
var heatArr = [];
for (var i = 0; i < 24; i++) {
heatArr.push({
val: minValue + i / 23 * rangeValue,
index: i
});
}
var ndx = crossfilter(heatArr);
var keyHeatmap = ndx.dimension(function(d) {
return [d.index, 1];
});
var keyHeatmapGroup = keyHeatmap.group().reduceSum(function(d) {
return d.val;
});
var heatmapChart = dc.heatMap("#heatmapKey");
var heatColorMapping = function(d) {
return d3.scale.linear().domain([minValue, maxValue]).range(["blue", "red"])(d);
};
heatColorMapping.domain = function() {
return [minValue, maxValue];
};
heatmapChart.width(400)
.height(80)
.dimension(keyHeatmap)
.group(keyHeatmapGroup)
.colorAccessor(function(d) {
return d.value;
})
.keyAccessor(function(d) { return d.key[0]; })
.valueAccessor(function(d) { return d.key[1]; })
.colsLabel(function(d){
return heatArr[d].val.toFixed(0);
})
.rowsLabel(function(d) {
return "Key";
})
.transitionDuration(0)
.colors(heatColorMapping1)
.calculateColorDomain();
heatmapChart.xBorderRadius(0);
heatmapChart.yBorderRadius(0);
minValue and maxValue were found in my original heat map. My final result looked like this:

dc.js time series line chart

I'm plotting a time series line chart which takes time on x-axis, other resp values on y-axis. I need help doing that. The graph is not plotted with the following code. It shows only the lines(axes) and labels but no graph nor the values on axes.
data = [{"date": "31-03-2015 11:05:36", "snr": 3.2}, {"date": "31-03-2015 11:05:36", "snr": 4.9},...{"date": "31-03-2015 11:05:36", snr: 5.0}];
var parseDate = d3.time.format("%d-%m-%Y %H:%M:%S").parse;
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var dimByTime = cf.dimension(function(d) { return d.date;});
var groupForSNR = dimByTime.groupAll();
groupForSNR.all = function() { return 1; };
var snrAP = dc.lineChart("#snrAP");
snrAP
.dimension(dimByTime)
.group(groupForSNR).valueAccessor(function(d) { return d.value.snr; })
.x(d3.time.scale())
.xUnits(d3.time.day);
dc.renderAll();
I want to show snr on y-axis and the corresponding time on x-axis.
Well, I've fixed it myself. May not be the optimum solution, but it still serves my purpose. Here is the code with modification...
var dimByTime = cf.dimension(function(d) { return d3.time.minute(d.date);});
var groupForSNR = dimByTime.group().reduce(
function reduceAdd(p, v) { return v.snr; },
function reduceRemove(p, v) { return v.snr; },
function reduceInitial() { return 0; });
groupForSNR.dispose();
var snrAP = dc.lineChart("#snrAP");
snrAP
.dimension(dimByTime)
.group(groupForSNR)
.x(d3.time.scale().domain([new Date(2015, 02, 21), new Date()]))
.xUnits(d3.time.minute)
.title(function(d){ return "Time: " + d.data.key.toDateString() + "\nS/N Ratio: " + numFormat(d.data.value); })
.xAxis().ticks(5);

Heatmap for multivalue variables

I am creating a heatmap using dc.js master API. My example is based on this. But the major difference is that the rows and columns have multivalues.
Below is the dataset in CSV format:
id,city,visitor,animals,food
1,NYC,854,"Lion,Tiger,Rabbit,Ape,Zebra,Monkey,Elephant,Horse","Apple,Banana,Chicken,Egg,Fish,Grape,Ham,Ice,Juice"
2,LAX,123,"Cat,Tiger,Rabbit,Ape,Whale,Bear,Zebra,Donkey,Goat,Turtle","Apple,Banana,Cake,Fish,Sugar,Bamboo,Leaf,Ham,Ice,Water"
3,LON,584,"Lion,Tiger,Ape,Shark,Panda,Zebra,Deer,Turtle,Bear,","Apple,Coke,Cake,Fish,Bamboo,Water,Grape,Orange"
4,TOR,704,"Cat,Rabbit,Ape,Whale,Bear,Panda,Donkey,Turtle,Cheetah","Banana,Cake,Orange,Kiwi,Sugar,Bamboo,Leaf,Goat,Ice,Juice"
5,SFO,855,"Lion,Tiger,Ape,Zebra,Monkey,Elephant,Donkey,Goat,Turtle,Cheetah","Apple,Cake,Grape,Ham,Juice,Hay"
6,DAL,654,"Salmon,Penguin,Rabbit,Ape,Whale,Goat,Tortoise","Apple,Banana,Cake,Ice,Water,Earthworm"
Animals and food each has multiple values. The rows are based on food and the columns are based on animals. I am able to create the heatmap but there is no interaction with other charts. For example, if I can click on a box, there is no action in the bar chart. The error message found at console is "Uncaught TypeError: Cannot read property 'all' of undefined." Here is the code.
parsecsv = function (string) {
var rows = d3.csv.parse(string);
var records = [];
rows.forEach(function (d, i) {
d.animals= d.animals.split(/,/);
d.food = d.food.split(/,/);
records.push(d)
});
return records
};
var chartGroup = "chartGroup";
var heatmapChart = dc.heatMap("#heatmap", chartGroup);
var pieChart1 = dc.pieChart("#piechart1", chartGroup);
var pieChart2 = dc.pieChart("#piechart2", chartGroup);
var barChart = dc.barChart("#barchart", chartGroup);
var ndx = crossfilter(parsecsv(csvtext));
console.log(ndx);
var animals_food_dim = ndx.dimension(function(d) { console.log (d.animals);return [d.animals, d.food]; });
var animals_food_group = animals_food_dim.groupAll().reduce(reduceAdd, reduceRemove, reduceInitial).value();
function reduceAdd(p, v) {
// skip empty values
if (v.animals[0] === "" || v.food[0]==="") return p;
v.animals.forEach (function(val1, idx1) {
v.food.forEach (function(val2, idx2) {
var temp_array=[val1,val2];
p[temp_array] = (p[temp_array] || 0) + 1; //increment counts
//console.log(val1+":"+val2, p[temp_array],temp_array);
});
});
return p;
}
function reduceRemove(p, v) {
if (v.animals[0] === "") return p; // skip empty values
v.animals.forEach (function(val1, idx1) {
v.food.forEach (function(val2, idx2) {
var temp_array=[val1,val2];
p[temp_array] = (p[temp_array] || 0) - 1; //increment counts
//console.log(val1+":"+val2,p[temp_array]);
});
});
return p;
}
function reduceInitial() {
return {};
}
animals_food_group.all = function() {
var newObject = [];
for (var key in this) {
if (this.hasOwnProperty(key) && key != "all" && key != "top") {
var temp_array=[key.substring(0,key.indexOf(",")),key.substring(key.indexOf(",")+1)];
//console.log(temp_array,this[temp_array]);
newObject.push({
key: temp_array,
value: this[temp_array]
});
}
}
return newObject;
};
animals_food_group.top = function(count) {
var newObject = this.all();
newObject.sort(function(a, b){return b.value - a.value});
return newObject.slice(0, count);
};
heatmapChart
.width(12 * 80 + 80)
.height(27 * 10 + 40)
.dimension(animals_food_dim)
.group(animals_food_group)
.keyAccessor(function(d) {return d.key[0];})
.valueAccessor(function(d) {return d.key[1];})
.colorAccessor(function(d) {return +d.value;})
.linearColors(["#FFEAEA", "#FF0000"])
.title(function(d) {
return "Animals: " + d.key[0] + "\n" +
"Food: " + d.key[1] + "\n" +
"Count: " + ( d.value) + " ";})
.calculateColorDomain();
heatmapChart.render();
var city_dim = ndx.dimension(function(d) {return d.city; });
var city_group = city_dim.group().reduceSum(function(d) {return +d.visitor;});
barChart
.dimension(city_dim)
.group(city_group)
.width(12 * 80 + 80)
.height(480)
.elasticY(true)
.x(d3.scale.ordinal().domain(["NYC","LAX","LON","TOR","SFO","DAL"]))
.xUnits(dc.units.ordinal)
.elasticY(true)
.centerBar(false)
.xAxisPadding(50);
barChart.render();
I bet I must did something wrong in reduceAdd, reduceRemove and/or animals_food_group.all. Would someone help me how to fix the interaction problem?
The JSFiddle is http://jsfiddle.net/chitester11/u33yb8k5/.

Resources