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?
Related
I have two charts, the first, a line chart, in which the user can brush.
Based on the selection, a bar chart (only one bar) updates its value thanks to a specific function. I would like to apply this specific function in an efficient manner to the new array.
I started by following the complex reduce example. There is something wrong with my logic because the function std gets
the all dataset instead of an array. It seems that to put the function within the valueAccessor is not the right thing to do.
This is my code:
/**********************************
* Step0: javascript functions *
**********************************/
// instead of calculating the desired metric on every change, which is slow, we'll just maintain
// these arrays of rows and calculate the metrics when needed in the accessor
function groupArrayAdd(keyfn) {
var bisect = d3.bisector(keyfn);
return function(elements, item) {
var pos = bisect.right(elements, keyfn(item));
elements.splice(pos, 0, item);
return elements;
};
}
function groupArrayRemove(keyfn) {
var bisect = d3.bisector(keyfn);
return function(elements, item) {
var pos = bisect.left(elements, keyfn(item));
if(keyfn(elements[pos])===keyfn(item))
elements.splice(pos, 1);
return elements;
};
}
function groupArrayInit() {
return [];
}
/**********************************
* Step1: Load data from json file *
**********************************/
d3.json("{% url "block__time_series" portfolio value_date %}").then(function(data){
const dateFormatSpecifier = "%Y-%m-%d";
const dateFormat = d3.timeFormat(dateFormatSpecifier);
const dateFormatParser = d3.timeParse(dateFormatSpecifier);
const numberFormat = d3.format('.2f');
data.forEach(function(d) {
d.dd = dateFormatParser(d.date);
d.month = d3.timeMonth(d.dd); // pre-calculate month for better performance
d.returns = +d.returns;
d.value = +d.value;
});
/******************************************************
* Step2: Create the dc.js chart objects & ling to div *
******************************************************/
const myBarChart = new dc.BarChart('#my_bar_chart');
const myLineChart = dc.compositeChart('#my_line_chart');
const palette_color_block4 = ["#6c5373", "#8badd9", "#b6d6f2", "#45788c", "#6E87F2", "#996A4E",
"#BF7761", "#735360", "#D994B0", "#6C5373", "#7F805E", "#A6A27A", "#48BDCC", "#FFC956", "#f2f2f2"]
/************************************************
* Step3: Run the data through crossfilter *
************************************************/
var facts = crossfilter(data), // Gets our 'facts' into crossfilter
returns = function (d) {return +d.returns}
/*Here my function that I want to use */
function std(kv) {
return d3.deviation(kv.value, returns);
}
/******************************************************
* Step4: Create the Dimensions *
******************************************************/
const dateDimension = facts.dimension(d => d.dd);
var returnsDimension = facts.dimension(returns);
var volGroup = dateDimension.group().reduce(groupArrayAdd(returns), groupArrayRemove(returns),
groupArrayInit);
var valueGroup = dateDimension.group().reduceSum(function (d) {return d.value; });
const moveMonths = facts.dimension(d => d.month);
const monthlyMoveGroup = moveMonths.group().reduceSum(d => d.value);
/***************************************
* Step5: Create the Visualisations *
***************************************/
myBarChart /* dc.BarChart('#my_bar_chart', 'chartGroup')*/
.width(400)
.height(200)
.x(d3.scaleBand())
.xUnits(dc.units.ordinal)
.colorAccessor(d => d.key)
.ordinalColors(palette_color_block4)
.margins({left: 80, top: 30, right: 10, bottom: 20})
.elasticY(false)
.brushOn(false)
.controlsUseVisibility(false)
.valueAccessor(std)
.dimension(returnsDimension)
.group(volGroup);
mylineChart /*dc.compositeChart('#my_line_chart', 'chartGroup')*/
.width(800)
.height(200)
.transitionDuration(1000)
.margins({top: 20, right: 10, bottom: 10, left: 10})
.dimension(moveMonths)
.mouseZoomable(true)
.round(d3.timeMonth.round)
.xUnits(d3.timeMonths)
.renderHorizontalGridLines(true)
.legend(new dc.Legend().x(800).y(10).itemHeight(13).gap(5))
.brushOn(true)
.title( function(d) { return dateFormat(d.key) + ': ' + numberFormat(d.value);
})
.valueAccessor(function (d) { return d.value})
.compose([
dc.lineChart(mylineChart).group(valueGroup , data[0].name)
]);
/****************************
* Step6: Render the Charts *
****************************/
dc.renderAll();
});
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 */
I've been happily using DC.js for some time. Today is the first time I've created two charts, from two dimensions, both running off the same crossfiltered variable, facts - and they don't filter each other. The bubble chart filters the bar chart but not vice versa. Am I making some obvious error? Very grateful for any pointers.
var facts = crossfilter(data);
var all = facts.groupAll();
print_filter(facts);
var appDimension = facts.dimension(function(d){ return d.ShortName; });
var appGroup = appDimension.group().reduce(
function(p,v){ p.count++; v.NumUsers==0?p.numUsers=1:p.numUsers=v.NumUsers; p.numClients=v.NumClients; p.lc=v.lc; p.LifeCycle=v.LifeCycle; p.fv=v.fv; p.FutureValue=v.FutureValue; return p; },
function(p,v){ p.count--; v.NumUsers==0?p.numUsers=1:p.numUsers=v.NumUsers; p.numClients=v.NumClients; p.lc=v.lc; p.LifeCycle=v.LifeCycle; p.fv=v.fv; p.FutureValue=v.FutureValue; return p; },
function(){ return { count:0, numUsers: 0, numClients: 0, lc: 0, LifeCycle: '', fv: 0, FutureValue: '' }; }
);
var lifeCycleDimension = facts.dimension(function(d){ return d.LifeCycle; });
var tempName='';
var lifeCycleGroup = lifeCycleDimension.group().reduce(
function(p,v){
if (tempName!=v.ShortName) {
p++;
tempName=v.ShortName;
}
return p;
},
function(p,v){
if (tempName!=v.ShortName) {
p--;
tempName=v.ShortName;
}
return p;
},
function(){ return 0; }
);
var yearlyBubbleChart = dc.bubbleChart('#col1')
.width(360)
.height(600)
.margins({top: 10, right: 100, bottom: 30, left: 40})
.dimension(appDimension)
.group(appGroup)
.clipPadding(200)
.yAxisLabel('Number of users')
.xAxisLabel('Number of clients')
// .ordinalColors(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628'])
.colorAccessor(function(p){
return p.value.lc;
})
.colors(colorbrewer.RdYlGn[9])
.colorDomain([1,6])
.keyAccessor(function (p) {
return p.value.numClients;
})
.valueAccessor(function (p) {
return p.value.numUsers;
})
.radiusValueAccessor(function (p) {
return p.value.fv;
})
.title(function(d){ return 'App: '+d.key + '\nNum users: '+d.value.numUsers+'\nNum clients: '+d.value.numClients+'\nLife cycle: '+d.value.LifeCycle+'\nFuture value: '+d.value.FutureValue; })
.maxBubbleRelativeSize(0.15)
.x(d3.scale.linear().domain([0, 608]))
.y(d3.scale.log().base(Math.E).domain([1, 120000]))
.r(d3.scale.linear().domain([0.5, 3]))
// .elasticY(true)
// .elasticX(true)
.yAxisPadding(100)
.xAxisPadding(500)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true);
yearlyBubbleChart.yAxis().tickFormat(function(d){ return Math.round(d); });
yearlyBubbleChart.xAxis().ticks(5);
var barAmountChart = dc.barChart('#barCount')
.width(530)
.height(200)
.margins({top: 10, right: 50, bottom: 30, left: 45})
.dimension(lifeCycleDimension)
.gap(1)
.group(lifeCycleGroup)
.title(function(d){ return d.value+' apps in '+d.key+' phase '; })
.x(d3.scale.ordinal().domain(['Idea','Plan 2017','New','Re-Newed','SunSet','Legacy']))
.xUnits(dc.units.ordinal);
barAmountChart.yAxis().ticks(4);
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.)
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.