In dc.js, why is elasticX not working properly for Time Series Chart - dc.js

Here is the jfiddle - http://jsfiddle.net/inasisi/6v639g9g/1/
As you can see the X axis is not scaled properly. I can calculate the min and max date and set the scale properly but don't want to do it after each filter. Would prefer if elasticX works properly.
Any ideas?
var chartGroup = "chartGroup";
data = [{
"run_date": "2013-01-20",
"current_grade": "Kindergarten",
"students": 1
}, {
"run_date": "2013-01-20",
"current_grade": "First",
"students": 2
}, {
"run_date": "2014-03-22",
"current_grade": "Kindergarten",
"students": 3
}, {
"run_date": "2014-03-22",
"current_grade": "First",
"students": 4
}, {
"run_date": "2015-10-06",
"current_grade": "Kindergarten",
"students": 5
}, {
"run_date": "2015-10-06",
"current_grade": "First",
"students": 21
}, {
"run_date": "2015-02-13",
"current_grade": "Kindergarten",
"students": 31
}, {
"run_date": "2015-02-13",
"current_grade": "First",
"students": 26
}, ];
var ndx = crossfilter(data);
var dateFormat = d3.time.format("%Y-%m-%d");
data.forEach(function (d) {
d.run_date = Date.parse(d.run_date);
});
var ndx = crossfilter(data);
filterDateDimension = ndx.dimension(function (d) {
return [d.run_date];
});
dateDimension = ndx.dimension(function (d) {
return [d.run_date];
});
var minDate = dateDimension.bottom(1)[0].run_date;
var maxDate = dateDimension.top(1)[0].run_date;
var runsStudentsGroup = dateDimension.group().reduceSum(function (fact) {
return fact.students;
});
var totalStudentsChart = dc.lineChart("#students_chart", chartGroup);
totalStudentsChart.renderArea(true)
.width(300)
.height(300)
.x(d3.time.scale())
.elasticY(true)
.elasticX(true)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
.dimension(dateDimension)
//.colors('red')
.group(runsStudentsGroup);
dc.renderAll(chartGroup);
$('.day_filter').on('click', function () {
console.log(dateDimension.top(Infinity));
console.log($(this).val());
dateDimension.filter(function (d) {
console.log(d > new Date(2015, 0, 1));
return d > new Date(2015, 0, 1);
});
console.log(dateDimension.top(Infinity));
dc.redrawAll();
});

I had to fix a few things to get the chart to display and to get the filter to work at all. I'll just quote those without explaining, since those aren't what the question is about:
d.run_date = new Date(d.run_date);
//...
return d.run_date; // twice
//...
filterDateDimension.filter(function (d) {
//...
dc.redrawAll(chartGroup);
To answer your main question, which is frequently asked, crossfilter does not automatically remove empty bins. You can use a "fake group" to filter them out.
Adding:
function remove_empty_bins(source_group) {
return {
all:function () {
return source_group.all().filter(function(d) {
return d.value != 0;
});
}
};
}
//...
.group(remove_empty_bins(runsStudentsGroup));
Working fork of your fiddle here: http://jsfiddle.net/gordonwoodhull/8an2n1eL/5/
(The transition in this example is particularly screwy, and will be fixed in 2.1.)

Related

How to render timeseries/categorical stacked bar chart in dc.js, filtering stack and bar

Most examples I have found are using data that has time and number
var data = [
{
"Time": "19-Jan-2018 11:24:49.000 UTC",
"Speed": 1.885
},
{
"Time": "19-Jan-2018 11:24:59.000 UTC",
"Speed": 1.875
},
{
"Time": "19-Jan-2018 11:25:00.000 UTC",
"Speed": 1.878
},
{
"Time": "19-Jan-2018 11:25:01.000 UTC",
"Speed": 1.876
}
]
I am looking to stack type
var data = [
{
"Time": "19-Jan-2018 11:24:49.000 UTC",
"type": "CAT"
},
{
"Time": "19-Jan-2018 11:24:59.000 UTC",
"type": "DOG"
},
{
"Time": "19-Jan-2018 11:25:00.000 UTC",
"type": "CAT"
},
{
"Time": "19-Jan-2018 11:25:01.000 UTC",
"Type": "BAT"
}
]
How can I stack categorical data, while allowing the user to select time/category pairs, as in the following Example?
I adapted the example to time/category data in this fiddle.
Those dates would only parse in Chrome, so
const parseDate = d3.utcParse("%d-%b-%Y %H:%M:%S.%L UTC");
data.forEach(d => {
d.Time = parseDate(d.Time);
})
I changed the key functions to use ,
function multikey(x,y) {
return x + ',' + y;
}
function splitkey(k) {
return k.split(',');
}
I also changed fake group stack_second to convert string-dates from the multikeys back into Dates, and to initialize categories to 0 (since every stack has to be present for every X).
function stack_second(group, categories) {
return {
all: function() {
var all = group.all(),
m = {};
// build matrix from multikey/value pairs
all.forEach(function(kv) {
var ks = splitkey(kv.key);
m[ks[0]] = m[ks[0]] || Object.fromEntries(categories.map(c=>[c,0]));
m[ks[0]][ks[1]] = kv.value;
});
// then produce multivalue key/value pairs
return Object.keys(m).map(function(k) {
return {key: new Date(k), value: m[k]};
});
}
};
}
Get the array of categories from the source data:
const categories = Array.from(new Set(data.map(d => d.Type)).values());
When dealing with date/time, you have to choose a d3 time interval appropriate for your data. Here minutes looked right. Using UTC d3-time methods everywhere because your source data is UTC.
const interval = d3.utcMinute;
Calculate xscale domain and apply:
let extent = d3.extent(data, d=>d.Time);
extent[1] = interval.offset(extent[1], 1)
chart
.x(d3.scaleTime().domain(extent))
.xUnits(interval.range)
Right number of ticks, also formatted in UTC:
chart.xAxis().ticks(d3.utcMinute).tickFormat(d3.utcFormat('%H:%M'))
Match colors between stacks and wedges with
.colors(d3.scaleOrdinal().domain(categories).range(d3.schemeCategory10))
Crossfilter initialization, using the interval and categories:
const interval = d3.utcMinute; // choose appropriate to your data
var cf = crossfilter(data),
timeTypeDim = cf.dimension(function(d) { return multikey(interval(d.Time), d.Type); }),
timeTypeGroup = timeTypeDim.group(), // reduceCount by default
stackedGroup = stack_second(timeTypeGroup, categories);
And here's the chart code for completeness, although we've already discussed the relevant parts:
function sel_stack(i) {
return function(d) {
return d.value[i];
};
}
chart
.width(600)
.height(400)
.colors(d3.scaleOrdinal().domain(categories).range(d3.schemeCategory10))
.controlsUseVisibility(true)
.x(d3.scaleTime().domain(extent))
.xUnits(interval.range)
.margins({left: 80, top: 20, right: 10, bottom: 20})
.brushOn(false)
.clipPadding(10)
.title(function(d) {
return d.key + '[' + this.layer + ']: ' + d.value[this.layer];
})
.legend(dc.legend().x(540).y(50))
.dimension(timeTypeDim)
.group(stackedGroup, categories[0], sel_stack(categories[0]))
.renderLabel(true);

Display multiple bar on barChart from a custom reducer

I have a group with custom reducer calculating various total and average values. The goal is to show them all on the same barChart. But I can only get the first bar to show. Here is the JSFiddler
https://jsfiddle.net/71k0guxe/15/
Is it possible to show all the value on the barChart?
Thanks in advance!
Data
ID,SurveySent,ResponseReceived
1,Yes,No
2,No,No
3,Yes,Yes
4,No,No
5,Yes,Yes
6,No,No
7,Yes,No
8,No,No
9,Yes,No
10,No,No
Code
var chart = dc.barChart("#test");
//d3.csv("morley.csv", function(error, experiments) {
var experiments = d3.csvParse(d3.select('pre#data').text());
var ndx = crossfilter(experiments),
dimStat = ndx.dimension(function(d) {return "Statistics";}),
groupStat = dimStat.group().reduce(reduceAdd, reduceRemove, reduceInitial);
function reduceAdd(p, v) {
++p.count;
if (v.SurveySent === "Yes") p.sent++;
if (v.ResponseReceived === "Yes") p.received++;
return p;
}
function reduceRemove(p, v) {
--p.count;
if (v.SurveySent === "Yes") p.sent--;
if (v.ResponseReceived === "Yes") p.received--;
return p;
}
function reduceInitial() {
return {count: 0, sent: 0, received: 0};
}
chart
.width(400)
.height(400)
.xUnits(dc.units.ordinal)
.label(function(d) { return d.data.value })
.elasticY(true)
.x(d3.scaleOrdinal().domain(["Total", "Sent", "Received"]))
.brushOn(false)
.yAxisLabel("This is the Y Axis!")
.dimension(dimStat)
.group(groupStat)
.valueAccessor(function (d) {
//Is it possible to return count sent and received all from here?
return d.value.count;
})
.on('renderlet', function(chart) {
chart.selectAll('rect').on("click", function(d) {
console.log("click!", d);
});
});
chart.render();
Just got some idea from the FAQ section of dc.js/wiki/FAQ
Fake Groups
"dc.js uses a very limited part of the crossfilter API - in fact, it really only uses dimension.filter() and group.all()."
I don't care about filtering, so i just need to mark up my own group.all. Basically transpose it from one row to multiple row. Works my purpose.
/* solution */
var groupStatTranposed = group_transpose(groupStat);
function group_transpose(source_group, f) {
return {
all:function () {
return [
{key: "Total", value: source_group.all()[0].value.count},
{key: "Sent", value: source_group.all()[0].value.sent},
{key: "Received", value: source_group.all()[0].value.received}
];
}
};
}
//use groupStatTranposed in the chart.
/** solution */

dc.js setting the colour of sub bar chart

I'm trying to set the color of a barchart in a composite chart on click but the renderlet is having problems.
dimensions={
{"Data1": ["line", AppStyles.color.warning],
"Data2": ["line", AppStyles.color.danger],
"Data3": ["bar", AppStyles.color.blue]
}
}
formatData = (data) => {
let formattedData = [];
for(let key in data) {
formattedData.push({
...data[key],
x: this.parseDate.parse(data[key].x)
})
}
return formattedData;
}
componentDidMount(){
let data = this.formatData(this.props.data);
this.ndx = crossfilter.crossfilter(data);
this.chart = dc.compositeChart(this.multiLineChartContainer);
this.dimension = this.ndx.dimension((d) => {
return d.x;
});
let minDate = this.dimension.bottom(1)[0].x;
let maxDate = this.dimension.top(1)[0].x;
let composeGroup = [];
Object.keys(this.props.dimensions).map((dim,i) => {
let grp = this.dimension.group().reduceSum((d) => {
return d[dim];
});
if(this.props.dimensions[dim][0] === "bar"){
composeGroup.push(dc.barChart(this.multiLineChartContainer)
.group(grp, dim)
.colors("blue")
.centerBar(true)
.renderlet((chart) => {
chart.selectAll('rect.bar').on('click', (event)=>{
console.log("clicked rect")
chart.colorAccessor(function(d){
console.log(d.key);
return d.key
}).colors(d3.scale.ordinal().domain(Object.keys(this.props.dimensions))
.range(['blue', '#5C5ED7', 'red', AppStyles.color.warning]))
})
})
)
} else {
composeGroup.push(dc.lineChart(this.multiLineChartContainer)
.group(grp, dim)
.colors(this.props.dimensions[dim][1])
.useRightYAxis(true)
);
}
});
this.chart.width(this.props.width)
.height(this.props.height)
.renderHorizontalGridLines(true)
.x(d3.time.scale().domain([minDate, maxDate]))
.elasticY(true)
.elasticX(true)
.xAxisLabel("Cohort")
.brushOn(false)
.yAxisLabel("Left")
.rightYAxisLabel("Right")
.xUnits(()=>{
return 30;
})
.legend(dc.legend().x(this.chart.width()- 130))
.compose(composeGroup)
this.chart.renderlet((chart) => {
chart.selectAll('circle, rect.bar').on("click", (event) => {
this.props.dataSelect(event);
});
});
this.chart.xAxis().ticks(5)
this.chart.render();
}
I've tried adding a renderlet to the subchart but it never gets invoked. I'm about to rerender the entire chart now and set transitionDuration to 0 just to reassign the colours. Is this really the best way to do this?

Get all the data from a clicked node in C3JS

How do I get all the data from a node when clicked? Right now I get only the x and value.
How do I get all the values from the JSON data object used for plotting the graph?
var chart_scatterplot = c3.generate({tooltip: {
contents: function(d, defaultTitleFormat, defaultValueFormat, color) {
var company = jsonfile[d[0].index].company;
var mailCount = jsonfile[d[0].index].mailCount;
var lastInteractedInDays = jsonfile[d[0].index].lastInteractedInDays;
var companyData = "<table class='data-c3-table'><tr><td>" + company + "</td></tr><tr><td>" + mailCount + "</td></tr><tr><td>" + lastInteractedInDays + "</td></tr></table>"
return companyData;
//return (company+mailCount+lastInteractedInDays) // formatted html as youmailCount want
}},
point: {r: 7},
data: {
json: jsonfile,
x: 'mailCount',
keys: {
value: ['mailCount', 'lastInteractedInDays'],
},
color: function(color, d) {
if (d.value > average) {
return "#F86A52"
} else {
return "#49B5A6"
};
},
type: 'scatter',
onclick: function(d) {
abc(d);
}
},axis: {
x: {
label: 'Interactions',
tick: {
fit: false
}
},
y: {
label: 'Days'
}},legend: {
show: false}});
var abc = function(v) {console.log("hello" + JSON.stringify(v));}
Attached is a fiddle - https://jsfiddle.net/npmarkunda/eqfyeeh1/
This will give you the full data associated with the node on click.
onclick: function(d) {
console.log(jsonfile[d.index]);//will console. the clicked node's data.
}
Working code here

Update multiple tags rowchart in dc.js

I am looking for how to create a rowchart in dc.js to show and filter items with multiple tags. I've summed up a few answers given on stack overflow, and now have a working code.
var data = [
{id:1, tags: [1,2,3]},
{id:2, tags: [3]},
{id:3, tags: [1]},
{id:4, tags: [2,3]},
{id:5, tags: [3]},
{id:6, tags: [1,2,3]},
{id:7, tags: [1,2]}];
var content=crossfilter(data);
var idDimension = content.dimension(function (d) { return d.id; });
var grid = dc.dataTable("#idgrid");
grid
.dimension(idDimension)
.group(function(d){ return "ITEMS" })
.columns([
function(d){return d.id+" : "; },
function(d){return d.tags;},
])
function reduceAdd(p, v) {
v.tags.forEach (function(val, idx) {
p[val] = (p[val] || 0) + 1; //increment counts
});
return p;
}
function reduceRemove(p, v) {
v.tags.forEach (function(val, idx) {
p[val] = (p[val] || 0) - 1; //decrement counts
});
return p;
}
function reduceInitial() {
return {};
}
var tags = content.dimension(function (d) { return d.tags });
var groupall = tags.groupAll();
var tagsGroup = groupall.reduce(reduceAdd, reduceRemove, reduceInitial).value();
tagsGroup.all = function() {
var newObject = [];
for (var key in this) {
if (this.hasOwnProperty(key) && key != "") {
newObject.push({
key: key,
value: this[key]
});
}
}
return newObject;
}
var tagsChart = dc.rowChart("#idtags")
tagsChart
.width(400)
.height(200)
.renderLabel(true)
.labelOffsetY(10)
.gap(2)
.group(tagsGroup)
.dimension(tags)
.elasticX(true)
.transitionDuration(1000)
.colors(d3.scale.category10())
.label(function (d) { return d.key })
.filterHandler (function (dimension, filters) {
var fm = filters.map(Number)
dimension.filter(null);
if (fm.length === 0)
dimension.filter(null);
else
dimension.filterFunction(function (d) {
for (var i=0; i < fm.length; i++) {
if (d.indexOf(fm[i]) <0) return false;
}
return true;
});
return filters;
}
)
.xAxis().ticks(5);
It can be seen on http://jsfiddle.net/ewm76uru/24/
Nevertheless, the rowchart is not updated when I filter by one tag. For example, on jsfiddle, if you select tag '1', it filters items 1,3,6 and 7. Fine. But the rowchart is not updated... I Should have tag '3' count lowered to 2 for example.
Is there a way to have the rowchart tags counts updated each time I filter by tags ?
Thanks.
After a long struggle, I think I have finally gathered a working solution.
As said on crossfilter documentation : "a grouping intersects the crossfilter's current filters, except for the associated dimension's filter"
So, the tags dimension is not filtered when tag selection is modified, and there is no flag or function to force this reset. Nevertheless, there is a workaround (given here : https://github.com/square/crossfilter/issues/146).
The idea is to duplicate the 'tags' dimension, and to use it as the filtered dimension :
var tags = content.dimension(function (d) { return d.tags });
// duplicate the dimension
var tags2 = content.dimension(function (d) { return d.tags });
var groupall = tags.groupAll();
...
tagsChart
.group(tagsGroup)
.dimension(tags2) // and use this duplicated dimension
as it can been seen here :
http://jsfiddle.net/ewm76uru/30/
I hope this will help.

Resources