I have a stackblitz here - https://stackblitz.com/edit/d3-one-y-axis?embed=1&file=src/app/bar-chart.ts&hideNavigation=1
I have a stacked bar chart with line chart on top.
The bar chart and line chart have two different data sets and I'm using a seond y-axis to plot the line chart data.
The line chart points are the totals for the two stacked charts in each months column.
Instead of having separate data and y-axis for the line chart is it possible to add up the data from the each months stacked bar and plot that on the graph using one y axis
It can be achieved in several ways. You can either redefine line.x, .y and .defined accessors using all three d, i, data arguments or you can map the data like this:
.data(
linedata.reduce(function(acc, current, index) {
let isFirstPair = index % 2 === 0;
let currentDate = that.y1(current.date)
let currentValue = that.y1(current.value)
if (isFirstPair) {
acc.push({ date: currentDate, value: currentValue });
} else {
acc[acc.length - 1].value += currentValue;
}
return acc;
}, [])
)
It will create a new object for every consequent pair in the source array. You may need to tweak the date or .x accessor.
Related
I want to display percentages on the y-axis of a dc.js barchart that can dynamically change when filtering the chart itself or some other charts.
Here is my case:
var ndx = crossfilter(dataCsvInitial);
var all = ndx.groupAll();
var accCredLimDim = ndx.dimension(function(d) { return d.acct_curr_crlimit;});
Then, I group by bins:
var value_range_credlim = maxCredLim - minCredLim; // defined earlier...
var nb_of_bins_credlim = 50,
bin_width_credlim = value_range_credlim/nb_of_bins_credlim;
var accCredLimGrp = accCredLimDim.group(function(d) {return Math.floor(d/bin_width_credlim)*bin_width_credlim;});
And draw my bar chart:
var creditBar = dc.barChart("#creditDistrib");
creditBar
.width(600)
.height(250)
.margins({top: 10, right: 50, bottom: 30, left: 50})
.dimension(accCredLimDim)
.group(accCredLimGrp)
.transitionDuration(500)
.x(d3.scaleLinear().domain([minCredLim, maxCredLim]))
.xUnits(function(){return nb_of_bins_credlim;})
.elasticY(true)
.brushOn(true)
.xAxisLabel("Credit Limit")
I succeeded to do what I want initially just by customizing the yAxis().tickFormat() attribute of the bar chart, by dividing the tick value by the total number of rows being filtered at the moment:
creditBarChart.yAxis().tickFormat(function (d) {
return 100*d/all.value() + '%';
});
And I recompute these ticks every time a transition is being made, because my y-axis is elastic:
creditBar
.on("pretransition", function(){
creditBar.yAxis().tickFormat(function (d) {
if (!creditBar.hasFilter()){
return Math.trunc(100*d/all.value()) + '%';
}
});
});
As you can see, I don't update the ticks when the bar chart is being filtered. Indeed, when it is filtered I want the ticks to remain unchanged, as the y-axis should not change. However, because I am dividing the tick value by all.value() this rule cannot work when filtering the bar chart itself. The displayed percentages are obviously wrong.
This question is quite close to solving my problem: link but it is applicable only for categorical bar chart...
How can I display percentages on the y-axis ticks, that can change of values when filtering other charts and also when filtering the chart itself?
Is there a sort of all.value() that would be computed excluding the effect of filtering a specified chart?
Thanks!
Since you want the groupAll not to observe the filter on this chart, you should use the chart dimension's groupAll not the one on the crossfilter object. From the docs:
Note: a grouping intersects the crossfilter's current filters, except
for the associated dimension's filter. Thus, group methods consider
only records that satisfy every filter except this dimension's filter.
So, if the crossfilter of payments is filtered by type and total, then
groupAll by total only observes the filter by type.
That's kind of a mouthful, but I hope the intention is clear.
var accCredLimDim = ndx.dimension(function(d) { return d.acct_curr_crlimit;});
var all = accCredLimDim.groupAll();
Once you do that, you don't have to put an if statement in your tickFormat definition:
creditBar
.on("pretransition", function(){
creditBar.yAxis().tickFormat(function (d) {
return Math.trunc(100*d/all.value()) + '%';
});
});
The if statement was incorrect for a couple of reasons. First, there could be a filter on this chart and also filters on the other charts. Second, any accessor you call, like tickFormat, needs to return a value every time it is called. But this would return undefined if there was any filter on this chart, because that is the default return value in JS.
Good Evening Everyone,
I'm trying to take the data from a database full of hour reports (name, timestamp, hours worked, etc.) and create a plot using dc.js to visualize the data. I would like the timestamp to be on the x-axis, the sum of hours for the particular timestamp on the y-axis, and a new bar graph for each unique name all on the same chart.
It appears based on my objectives that using crossfilter.js the timestamp should be my 'dimension' and then the sum of hours should be my 'group'.
Question 1, how would I then use the dimension and group to further split the data based on the person's name and then create a bar graph to add to my composite graph? I would like for the crossfilter.js functionality to remain intact so that if I add a date range tool or some other user controllable filter, everything updates accordingly.
Question 2, my timestamps are in MySQL datetime format: YYYY-mm-dd HH:MM:SS so how would I go about dropping precision? For instance, if I want to combine all entries from the same day into one entry (day precision) or combine all entries in one month into a single entry (month precision).
Thanks in advance!
---- Added on 2017/01/28 16:06
To further clarify, I'm referencing the Crossfilter & DC APIs alongside the DC NASDAQ and Composite examples. The Composite example has shown me how to place multiple line/bar charts on a single graph. On the composite chart I've created, each of the bar charts I've added a dimension based off of the timestamps in the data-set. Now I'm trying to figure out how to define the groups for each. I want each bar chart to represent the total time worked per timestamp.
For example, I have five people in my database, so I want there to be five bar charts within the single composite chart. Today all five submitted reports saying they worked 8 hours, so now all five bar charts should show a mark at 01/28/2017 on the x-axis and 8 hours on the y-axis.
var parseDate = d3.time.format('%Y-%m-%d %H:%M:%S').parse;
data.forEach(function(d) {
d.timestamp = parseDate(d.timestamp);
});
var ndx = crossfilter(data);
var writtenDimension = ndx.dimension(function(d) {
return d.timestamp;
});
var hoursSumGroup = writtenDimension.group().reduceSum(function(d) {
return d.time_total;
});
var minDate = parseDate('2017-01-01 00:00:00');
var maxDate = parseDate('2017-01-31 23:59:59');
var mybarChart = dc.compositeChart("#my_chart");
mybarChart
.width(window.innerWidth)
.height(480)
.x(d3.time.scale().domain([minDate,maxDate]))
.brushOn(false)
.clipPadding(10)
.yAxisLabel("This is the Y Axis!")
.compose([
dc.barChart(mybarChart)
.dimension(writtenDimension)
.colors('red')
.group(hoursSumGroup, "Top Line")
]);
So based on what I have right now and the example I've provided, in the compose section I should have 5 charts because there are 5 people (obviously this needs to be dynamic in the end) and each of those charts should only show the timestamp: total_time data for that person.
At this point I don't know how to further breakup the group hoursSumGroup based on each person and this is where my Question #1 comes in and I need help figuring out.
Question #2 above is that I want to make sure that the code is both dynamic (more people can be handled without code change), when minDate and maxDate are later tied to user input fields, the charts update automatically (I assume through adjusting the dimension variable in some way), and if I add a names filter that if I unselect names that the chart will update by removing the data for that person.
A Question #3 that I'm now realizing I'll want to figure out is how to get the person's name to show up in the pointer tooltip (the title) along with timestamp and total_time values.
There are a number of ways to go about this, but I think the easiest thing to do is to create a custom reduction which reduces each person into a sub-bin.
First off, addressing question #2, you'll want to set up your dimension based on the time interval you're interested in. For instance, if you're looking at days:
var writtenDimension = ndx.dimension(function(d) {
return d3.time.hour(d.timestamp);
});
chart.xUnits(d3.time.hours);
This will cause each timestamp to be rounded down to the nearest hour, and tell the chart to calculate the bar width accordingly.
Next, here's a custom reduction (from the FAQ) which will create an object for each reduced value, with values for each person's name:
var hoursSumGroup = writtenDimension.group().reduce(
function(p, v) { // add
p[v.name] = (p[v.name] || 0) + d.time_total;
return p;
},
function(p, v) { // remove
p[v.name] -= d.time_total;
return p;
},
function() { // init
return {};
});
I did not go with the series example I mentioned in the comments, because I think composite keys can be difficult to deal with. That's another option, and I'll expand my answer if that's necessary.
Next, we can feed the composite line charts with value accessors that can fetch the value by name.
Assume we have an array names.
compositeChart.shareTitle(false);
compositeChart.compose(
names.map(function(name) {
return dc.lineChart(compositeChart)
.dimension(writtenDimension)
.colors('red')
.group(hoursSumGroup)
.valueAccessor(function(kv) {
return kv.value[name];
})
.title(function(kv) {
return name + ' ' + kv.key + ': ' + kv.value;
});
}));
Again, it wouldn't make sense to use bar charts here, because they would obscure each other.
If you filter a name elsewhere, it will cause the line for the name to drop to zero. Having the line disappear entirely would probably not be so simple.
The above shareTitle(false) ensures that the child charts will draw their own titles; the title functions just add the current name to those titles (which would usually just be key:value).
Using different dimensions of the same dataset, there are three dc.js Line Charts on screen.
When user clicks a datapoint on any lineChart, I wish to locate and return the data values for that corresponding point from all other charts, including the one clicked on.
I am also attempting (on mouseover) to change the circle fill color to red for the datapoint being hovered, as well as for the corresponding datapoint (same "x" value) for all other charts.
I am using the .filter() method but haven't been successful getting the desired data. The error message is: "Uncaught TypeError: myCSV[i].filter is not a function"
Full jsFiddle demo/example
lc1.on('renderlet', function(lc1) {
var allDots1 = lc1.selectAll('circle.dot');
var allDots2 = lc2.selectAll('circle.dot');
var allDots3 = lc3.selectAll('circle.dot');
allDots1.on('click', function(d) {
var d2find = d.x;
var d2find2 = d3.select(this).datum();
console.log(myCSV);
alert('Captured:'+"\nX-axis (Date): "+d2find2.x +"\nY-axis (Value): "+ d2find2.y +"\nDesired: display corresponding values from all three charts for this (date/time) datapoint");
allDots2.filter(d=>d.x == d2find2).attr('fill','red');
findAllPoints(d2find2);
});//END allDots1.on(click);
function findAllPoints(datum) {
var objOut = {};
var arrLines=['car','bike','moto'];
for (var i = 0; i < 3; i++) {
thisSrx = arrLines[i];
console.log('thisSrx: '+thisSrx);
console.log(myCSV[i].date)
console.log(datum.x);
//loop thru myCSV obj, compare myCSV[i].date to clicked "x" val
//build objOut with data from each graph at same "X" (d/t) as clicked
objOut[i] = myCSV[i].filter(e => e.date === datum.x)[0][thisSrx];
}
$('#msg').html( JSON.stringify(objOut) );
console.log( JSON.stringify(objOut) );
}//END fn findAllPoints()
});//END lc1.on(renderlet)
myCSV contains all three data points, so I don't see the need to loop through the three charts independently - findAllPoints is going to find the same array entry for all three data series anyway.
The main problem you have here is that date objects don't compare equal if they have the same value. This is because == (and ===) evaluate object identity if the operands are objects:
> var d1 = new Date(), d2 = new Date(d1)
undefined
> d1
Mon Feb 13 2017 09:03:53 GMT-0500 (EST)
> d2
Mon Feb 13 2017 09:03:53 GMT-0500 (EST)
> d1==d2
false
> d1.getTime()===d2.getTime()
true
There are two ways to deal with this.
Approach 1: use second event argument
If the items in all the charts match up item by item, you can just use the index.
All d3 callbacks pass both the datum and the index. So you can modify your callback like this:
allDots1.on('click', function(d,i) {
// ...
allDots2.filter((d,j)=> j===i).attr('fill','red').style('fill-opacity', 1);
alert(JSON.stringify(myCSV[i]));
});
http://jsfiddle.net/gordonwoodhull/drbtmL77/7/
Approach 2: compare by date
If the different charts might have different data indices, you probably want to compare by date, but use Date.getTime() to get an integer you can compare with ===:
allDots1.on('click', function(d) {
var d2find = d.x;
// ...
allDots2.filter(d=> d.x.getTime()===d2find.getTime()).attr('fill','red').style('fill-opacity', 1);
var row = myCSV.filter(r=>r.date.getTime()===d2find.getTime())
alert(JSON.stringify(row));
});
http://jsfiddle.net/gordonwoodhull/drbtmL77/10/
Note that in either case, you're going to need to also change the opacity of the dot in the other charts, because otherwise they don't show until they are hovered.
Not sure when you want to reset this - I guess it might make more sense to show the corresponding dots on mouseover and hide them on mouseout. Hopefully this is enough to get you started!
All nvd3 examples (which I've found) look like this:
return [
{
values: sin, //values - represents the array of {x,y} data points
key: 'Sine Wave', //key - the name of the series.
color: '#ff7f0e' //color - optional: choose your own line color.
}, ...]
I want to use a function which would use different keys based on the size of the chart / drawing area.
So if I have a large drawing area I have space for the whole name Sine Wave and in small areas I'd just display sin.
Yes, I could go through the series and update the key property, but it would be easier to put all the necessary data into the object and choose on render time, which key should be used.
You can use chart.legend.key()
chart.legend.key(function(d){
// Return the key you want for series d here based on screen realestate
// FYI: The default would return d.key
});
So it could look something like this:
function setLegendKeys(d){
var width = document.body.clientWidth;
var abbreviations = {
'Sine Wave': 'sin',
'Cosine Wave': 'cos'
};
if (width < 500){
return abbreviations[d.key];
}
return d.key;
}
chart.legend.key(setLegendKeys)
See this Plunk for a live example:
http://plnkr.co/edit/lisKuZ5ivj25QhyjlQUW?p=preview
jsFiddle:
http://jsfiddle.net/PYeFP/
I have a bar chart set up that graphs a users number of trips by day
tripVolume = dc.barChart("#trip-volume")
.width(980) // (optional) define chart width, :default = 200
.height(75) // (optional) define chart height, :default = 200
.transitionDuration(0) // (optional) define chart transition duration, :default = 500
.margins({ top: 10, right: 50, bottom: 30, left: 40 })
.dimension(tripsByDateDimension) // set dimension
.group(tripsByDateGroup) // set group
// (optional) whether chart should rescale y axis to fit data, :default = false
.elasticY(false)
// (optional) whether chart should rescale x axis to fit data, :default = false
.elasticX(false)
// define x scale
.x(d3.time.scale().domain([tripsByDateDimension.bottom(1)[0].startDate, tripsByDateDimension.top(1)[0].startDate ]))
// (optional) set filter brush rounding
.round(d3.time.day.round)
// define x axis units
.xUnits(d3.time.days)
// (optional) whether bar should be center to its x value, :default=false
.centerBar(true)
// (optional) render horizontal grid lines, :default=false
.renderHorizontalGridLines(true)
// (optional) render vertical grid lines, :default=false
.renderVerticalGridLines(true)
.brushOn(false);
The graph displays fine but I would like to filter it using some jQuery controls.
When the user selects the date I am trying to add a filter to the chart, the filter gets added but the chart does not change, even if I redraw() or render().
This is how the crossfilter is setup:
tripsCx = crossfilter(data.rows);
var allTripsGroup = tripsCx.groupAll();
var tripsByDateDimension = tripsCx.dimension(function (d) { return d.startDate; });
var tripsByDateGroup = tripsByDateDimension.group(d3.time.day);
The following are some of the methods I have used to try to apply a filter:
This should use the filterRange:
d.filter(d.dimension().top(20)[19], d.dimension().top(20)[0]);
FilterFunction:
d.filter(function (d) {
return d.getTime() > start.valueOf() && d.getTime() < end.valueOf();
});
FilterExact:
d.filter(d.dimension().top(20)[0]);
I also tried bypassing the chart and applying the filter directly on the dimension:
d.dimension().filterFunction(function (d) {
return d.getTime() > start.valueOf() && d.getTime() < end.valueOf()
});
Nothing I have done causes the chart to change.
I am beginning to think that I have the wrong expectation of what the filter function should do?
How can I manually filter the data in the dimension to have the chart updated?
I don't want to use a brush.
I will be filtering the data based on different criteria, I'm just trying to get the simple case working first.
I've spent a couple of days on this now and I'm at a loss as to what to try next.
Any help would be greatly appreciated.
Have you tried to reset your x property of the graph after setting the crossfilter filter
I have a somewhat similar case and what I do after each action that changes the filtered values is something along the lines of
.x(..).dimension(...).group(...)
after creating/setting the filters
Tried to do something like that
$('#filter').on('click', function(){
var minDate = tripsByDateDimension.top(5)[4].startDate;
var maxDate = tripsByDateDimension.top(5)[0].startDate;
console.log(tripVolume.filters());
tripVolume.filter([minDate, maxDate]);
tripVolume.x(d3.time.scale().domain([minDate,maxDate]));
console.log(tripVolume.filters());
dc.redrawAll()
});
http://jsfiddle.net/PYeFP/5/
Better answer per the discussion in the comment is to add the filter to the dimension, not the chart
Finally, one needs to realize what is mentioned in https://github.com/square/crossfilter/wiki/API-Reference#group-map-reduce
Note: a grouping intersects the crossfilter's current filters, except for the associated dimension's filter. Thus, group methods consider only records that satisfy every filter except this dimension's filter. So, if the crossfilter of payments is filtered by type and total, then group by total only observes the filter by type.
(also see https://groups.google.com/d/msg/dc-js-user-group/UFxvUND7hmY/btbAjqIIzl8J)