Adding a filter in dc.js / Crossfilter not updating the chart - d3.js

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)

Related

DC.JS/Crossfilter: Percentage on y-axis of a bar chart with linear x-axis?

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.

Dynamic colors in DC charts

I'm building a data dashboard using DC.js and was wondering if it was possible to change the color of the slices in a pie chart dynamically based on the value in the field it is referring to.
Basically I've built a pie chart aggregating the costume colors of different superheroes and I'd love to be able to color each slice with the color it is referring to - so the slice for 'Black' is colored black, the slice for 'Green' is colored green and so forth.
I'm fairly new to DC.js so accept that it may not be possible, but wanted to throw it out there and see if it could be done!
I tried including an array within .ordinalColors but couldn't figure out if there was a way to pull in the data from the field dynamically. I'm assuming that I'd have to change the data in the .csv file to a string that could be recognised as a color reference, but not sure how to go about doing that.
function show_costume_color(ndx) {
var costume_color_dim = ndx.dimension(dc.pluck('Costume Colour'));
var costume_color = costume_color_dim.group();
dc.pieChart('#costume-color')
.width(500)
.height(500)
.radius(500)
.innerRadius(100)
.slicesCap([7])
.transitionDuration(1500)
.dimension(costume_color_dim)
.group(costume_color);
}
CSV data comes in the below format
ID,name,Gender,Eye color,Race,Hair color,Publisher,Alignment,Superpower,Superpower Strength Level,Costume
Colour
0,A-Bomb,Male,Yellow,Human,No Hair,Marvel Comics,Good,Superhuman
Strength,10,None
1,Abin Sur,Male,Blue,Ungaran,No Hair,DC Comics,Good,Cosmic Power,40,Green
Yes, of course. Everything is specified dynamically in dc.js.
Assuming you are using dc.js v3 (and d3 v4+) the way I would suggest doing this is by creating another CSV file with the color assignments you want, something like
Name, RGB
Red, #ff1122
Blue, #1133ff
...
Then you can load the second file in parallel with your data using Promise.all(),
Promise.all([d3.csv('data.csv'), d3.csv('colors.csv')])
.then(function(data, colors) {
// rest of code will go here
});
ordinalColors is a nice convenience method, but if you want complete control, and to understand exactly what's going on, it's better to supply your own color scale. In this case, we want an ordinal scale, which maps specific discrete values to specific colors.
Under the covers, dc.js always deals with colors by using the colorAccessor to fetch a value for the the item, and then mapping this value using a color scale. You can think of the value that the accessor returns as a "color name", which is pretty convenient because it's exactly what you want here.
So you can populate a d3.scaleOrdinal with the domain of color names and the range of RGB colors:
var colorScale = d3.scaleOrdinal()
.domain(colors.map(row => row.Name))
.range(colors.map(row => row.RGB));
Now supply it to your chart using .colors():
chart.colors(colorScale);
What's really handy about this approach is that you can supply the same color scale for multiple charts, in order to make sure they are consistent. This is something that you don't get automatically in dc.js, because charts don't know very much about each other.
So, I managed to figure it out through an extensive period of trial and error and now I'm off and away with my dashboard. Thanks for your help, Gordon - it really made the difference! It needs a bit of tidying up but my working test code is below.
// Bring in data from both csv files
Promise.all([d3.csv("../data/heroes_information.csv"),
d3.csv("../data/costume_colors.csv")])
.then(function(data) {
// Tidy up data before use
data.forEach(function(d) {
d.Height = +d.Height;
d.Weight = +d.Weight;
d.Strength = +d.Strength;
});
// Bring in colorScale to dynamically color pie chart slices
var ndxcol = crossfilter(data[1]);
var colorScale = d3.scaleOrdinal()
.domain(data[1].map(row => row.Name))
.range(data[1].map(row => row.RGB));
// Bring in superhero data
var ndx = crossfilter(data[0]);
// Define chart types
var publisherSelector = dc.selectMenu('#publisher-selector')
var genderChart = dc.rowChart('#gender-balance');
// Define chart dimensions
var publisherChoice = ndx.dimension(dc.pluck('Publisher'));
var genderBalance = ndx.dimension(dc.pluck('Gender'));
// Define chart groups
var genderNumber = genderBalance.group();
var publisherNumber = publisherChoice.group();
// Draw charts
publisherSelector
.dimension(publisherChoice)
.group(publisherNumber);
genderChart
.width(500)
.height(200)
.margins({ top: 30, right: 30, bottom: 30, left: 30 })
.dimension(genderBalance)
.group(genderNumber)
.gap(6)
.colors(colorScale)
.transitionDuration(500)
.x(d3.scaleOrdinal())
.elasticX(true);
dc.renderAll();
});

D3 - Add data points using data in chart

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.

dc.js Incorporate regression chart into existing scatterplot with crossfilter

I am using dc.js and crossfilter.js to create a d3 dashboard, and am wondering how to implement a regression line into a scatterplot chart that responds to filtering.
I have been playing with a few examples re adding a regression line, but I have been unsuccessful extracting and incorporating the code.
I don't have a problem with the math, but rather with how to access the filtered data from the dimension, and then how to add the regression line to to the filtered scatterplot chart (so that the regression line also responds to future filtering).
jsFiddle Demo
var data = [
{"record":"record","date":"date","cars":"cars","bikes":"bikes"},
{"record":"1","date":"01/05/2012","cars":"1488.1","bikes":"49.73"},
{"record":"2","date":"02/05/2012","cars":"1374.29","bikes":"52.44"},
{"record":"3","date":"03/05/2012","cars":"1353.01","bikes":"47.92"},
{"record":"4","date":"04/05/2012","cars":"1420.33","bikes":"50.69"},
{"record":"5","date":"05/05/2012","cars":"1544.11","bikes":"47.47"},
{"record":"6","date":"06/05/2012","cars":"1292.84","bikes":"47.75"},
{"record":"7","date":"07/05/2012","cars":"1318.9","bikes":"48.64"},
{"record":"8","date":"08/05/2012","cars":"1686.3","bikes":"50.9"},
{"record":"9","date":"09/05/2012","cars":"1603.99","bikes":"53.44"},
{"record":"10","date":"10/05/2012","cars":"1420.1","bikes":"53.29"},
{"record":"11","date":"11/05/2012","cars":"1410.8","bikes":"54.06"},
{"record":"12","date":"12/05/2012","cars":"1374.62","bikes":"51.24"},
{"record":"13","date":"13/05/2012","cars":"1279.53","bikes":"53.96"},
{"record":"14","date":"14/05/2012","cars":"1330.47","bikes":"49.5"},
{"record":"15","date":"15/05/2012","cars":"1377.61","bikes":"52.32"},
{"record":"16","date":"16/05/2012","cars":"1302.12","bikes":"51.96"},
{"record":"17","date":"17/05/2012","cars":"1326.9","bikes":"49.86"},
{"record":"18","date":"18/05/2012","cars":"1181.55","bikes":"50.25"},
{"record":"19","date":"19/05/2012","cars":"1493.75","bikes":"51.24"},
{"record":"20","date":"20/05/2012","cars":"1463.9","bikes":"50.88"},
{"record":"21","date":"21/05/2012","cars":"1370.16","bikes":"51.09"},
{"record":"22","date":"22/05/2012","cars":"1403.3","bikes":"51.67"},
{"record":"23","date":"23/05/2012","cars":"1277.65","bikes":"49.3"},
{"record":"24","date":"24/05/2012","cars":"1361.94","bikes":"50.47"},
{"record":"25","date":"25/05/2012","cars":"1400.8","bikes":"51.55"},
{"record":"26","date":"26/05/2012","cars":"1289.09","bikes":"47.17"},
{"record":"27","date":"27/05/2012","cars":"1258.39","bikes":"52.12"},
{"record":"28","date":"28/05/2012","cars":"1288.71","bikes":"49.28"},
{"record":"29","date":"29/05/2012","cars":"1511.86","bikes":"50.73"},
{"record":"30","date":"30/05/2012","cars":"1300.38","bikes":"52.39"},
{"record":"31","date":"31/05/2012","cars":"1455.19","bikes":"49.53"},
{"record":"32","date":"01/06/2012","cars":"1311.89","bikes":"50.37"},
{"record":"33","date":"02/06/2012","cars":"1368.64","bikes":"50.87"},
{"record":"34","date":"03/06/2012","cars":"1360.05","bikes":"50.51"},
{"record":"35","date":"04/06/2012","cars":"1382.56","bikes":"49.67"},
{"record":"36","date":"05/06/2012","cars":"1304.15","bikes":"47.6"},
{"record":"37","date":"06/06/2012","cars":"1271.57","bikes":"50.22"},
{"record":"38","date":"07/06/2012","cars":"1442.38","bikes":"50.8"},
{"record":"39","date":"08/06/2012","cars":"1406.38","bikes":"53.14"},
{"record":"40","date":"09/06/2012","cars":"1724.16","bikes":"49.66"},
{"record":"41","date":"10/06/2012","cars":"1931.05","bikes":"53"},
{"record":"42","date":"11/06/2012","cars":"1669.47","bikes":"53.71"},
{"record":"43","date":"12/06/2012","cars":"1794.06","bikes":"51.78"},
{"record":"44","date":"13/06/2012","cars":"1625.98","bikes":"51.58"},
{"record":"45","date":"14/06/2012","cars":"1371.51","bikes":"52.36"},
{"record":"46","date":"15/06/2012","cars":"1418.05","bikes":"47.64"},
{"record":"47","date":"16/06/2012","cars":"1431","bikes":"53.14"},
{"record":"48","date":"17/06/2012","cars":"1527.21","bikes":"48.63"},
{"record":"49","date":"18/06/2012","cars":"1320.95","bikes":"51.7"},
{"record":"50","date":"19/06/2012","cars":"1396.93","bikes":"52.92"}
];
tSel1 = "cars";
tSel2 = "bikes";
data.forEach(function (d) {
d[tSel1] = +d[tSel1];
d[tSel2] = +d[tSel2];
});
var facts = crossfilter(data);
var allDimension = facts.groupAll();
var scatterDimension = facts.dimension(function(d) {return [+d[tSel1], +d[tSel2]];});
var scatterGroup = scatterDimension.group().reduceSum(function(d) { return d[tSel1]; });
var maxY1 = d3.max(data, function(d) {return d[tSel1]});
var maxY2 = d3.max(data, function(d) {return d[tSel2]});
var maxY1Plus = maxY1 + (maxY1 * 0.1);
var maxY2Plus = maxY2 + (maxY2 * 0.1);
var minY1 = d3.min(data, function(d) {return d[tSel1]});
var minY1Minus = minY1 * 0.9;
var minY2 = d3.min(data, function(d) {return d[tSel2]});
var minY2Minus = minY2 * 0.9;
xyScatterChart = dc.scatterPlot("#scatterPlot");
xyScatterChart
.width(600)
.height(400)
.margins({top: 20, right: 20, bottom: 20, left: 60})
.dimension(scatterDimension)
.group(scatterGroup)
.symbolSize(6)
.highlightedSize(15)
.brushOn(false)
.excludedOpacity(0.5)
.excludedSize(5)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
.x(d3.scale.linear().domain([minY1Minus,maxY1Plus]))
.y(d3.scale.linear().domain([minY2Minus,maxY2Plus]));
dc.renderAll();
dc.redrawAll();
<link href="http://dc-js.github.io/dc.js/css/dc.css" rel="stylesheet"/>
<script src="http://dc-js.github.io/dc.js/js/d3.js"></script>
<script src="http://dc-js.github.io/dc.js/js/crossfilter.js"></script>
<script src="http://dc-js.github.io/dc.js/js/dc.js"></script>
<div id="scatterPlot"></div>
References:
https://groups.google.com/forum/#!topic/dc-js-user-group/HaQMegKa_U0
https://bl.ocks.org/ctufts/298bfe4b11989960eeeecc9394e9f118
It would be awesome to include an example in dc.js, since this is something lots of people can use.
Maybe we can work together on that? I don't know the math but here's a simple way to use a composite chart to display a line on data calculated from an aggregated group.
First off, here's the composite chart with the old scatter plot embedded in it:
var composite = dc.compositeChart("#composite");
composite
.width(600)
.height(400)
.margins({top: 20, right: 20, bottom: 20, left: 60})
.dimension(scatterDimension)
.group(scatterGroup)
.compose([
dc.scatterPlot(composite)
.symbolSize(6)
.highlightedSize(15)
.brushOn(false)
.excludedOpacity(0.5)
.excludedSize(5)
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true),
dc.lineChart(composite)
.group(regressionGroup(scatterGroup))
])
.x(d3.scale.linear().domain([minY1Minus,maxY1Plus]))
.y(d3.scale.linear().domain([minY2Minus,maxY2Plus]));
Note that we're supplying the scatter group to both the composite and the scatter plot. That's just because the composite chart requires a group even though it doesn't actually use it.
We've moved the parameters that have to do with coordinates to the main (composite) chart, but everything that is specific to the scatter plot stays on it. We've also added a line chart to the composite, which uses a "fake group" based on the scatter group.
This fake group is particularly fake, but it should be enough to get you started. Since I don't have time to learn the math today, I'll just pretend that the first and last points are the regression:
function regressionGroup(group) {
return {
all: function() {
var _all = group.all();
var first, last;
for(var i=0; i < _all.length; ++i) {
var key = _all[i].key;
if(!isNaN(key[0]) && !isNaN(key[1])) {
var kv = {key: key[0], value: key[1]};
if(!first)
first = kv;
last = kv;
}
}
return [first, last];
}
};
}
As with all fake groups, the idea is to calculate some group-like data when the chart asks for it (and no sooner), based on another group. Here the calculation is not very interesting, because you know how to calculate a regression and I don't. You'll want to replace first and last and the for loop with a real calculation; all this is doing is checking for valid points and keeping the first and last ones that it finds.
Interestingly, the scatter plot takes data where the key contains both x and y coordinates, but the line chart takes data where the key is x and the value is y. That's why we have the transformation kv = {key: key[0], value: key[1]}
Postscript
Note that you'll run into a dc.js bug if you put the regression guide points outside of the domain - the stack mixin is too aggressive about clipping points to the domain. There is an easy, ugly workaround that seems to work in this case: tell the line chart it has an ordinal x scale even though it doesn't:
var composite = dc.compositeChart("#composite"),
lineChart;
composite
.width(600)
// ...
.compose([
// ...
lineChart = dc.lineChart(composite)
.group(regressionGroup(scatterGroup))
])
lineChart.isOrdinal = d3.functor(true);
Yuck! But it works! This hack probably only works inside a composite!
https://jsfiddle.net/gordonwoodhull/5tpcxov1/12/
I have a fully functional example of regression. I was precisely doing it when I came here for help and I found your question. It requires regression.js (here).
This follows Gordon's excellent suggestion of a "fake group", which should really be called an inline group, or immediate group, or even group on-the-fly. Here is mine:
function myRegressionGroup(group, min, max, filter = false) {
return {
all: function() {
var _all = group.all();
var first, last;
if(filter) reg = regression.linear(_all.filter(function(k,v) {if(k.key[0]) return k.key}).map((k,v) => k.key));
else reg = regression.linear(_all.map((k,v) => k.key));
first = reg.predict(min);
last = reg.predict(max)
return [{key:first[0], value: first[1]}, {key: last[0], value: last[1]}]
}
};
}
Please notice that this function requires a crossfilter group and also the min and max from the x-scale. Since you typically have these values calculated for your xScale, all it takes is reusing them here. This is because the function uses the extremes with the predict method to calculate the two points of the regression line.
The optional filter data wrangler is for you to decide whether to remove empty values on x or not.
#Gordon, how should I do in order to include my regression example in the Examples of using dc.js?

DC.js bar chart with date axis

I would like to create a bar chart based on dates in x-axis. Labels should be displayed as month (i.e. Jan, Jan'17 - preferred). Within my data I have always first date of following months, i.e. 01Jan, 01Feb, 01Mar. I have created a chart but I am not able to make it aligned.
var chart = dc.barChart("#" + el.id);
var chCategory = ndx.dimension(function(d) {return d[chCategoryName];});
chValues = chCategory.group().reduceSum(
return parseFloat(d[chValueName]);});
//set range for x-axis
var minDate = chCategory.bottom(1)[0][chCategoryName];
var maxDate = chCategory.top(1)[0][chCategoryName];
chart
.width(800)
.height(200)
.x(d3.time.scale().domain([minDate,maxDate]))
.xUnits(d3.time.months)
.dimension(chCategory)
.group(chValues)
.renderHorizontalGridLines(true)
// .centerBar(true) //does not look better
.controlsUseVisibility(true)
.ordinalColors(arrColors)
.transitionDuration(1000)
.margins({top: 10, left: 80, right: 5, bottom: 20})
I have already read post: dc.js x-axis will not display ticks as months, shows decimals instead
but I am not able to implement it in a way that will keep correct sorting for different years.
dc.js takes the domain pretty literally - the x axis stretches exactly from the beginning to the end, disregarding the width of the bars or their placement. It's a design bug.
Here are two workarounds.
keep bars centered and add padding
If you're using elasticX you can manually correct it like this:
chart.centerBar(true)
.xAxisPadding(15).xAxisPaddingUnit('day')
If you're just setting the domain manually, that's
minDate = d3.time.day.offset(minDate, -15);
maxDate = d3.time.day.offset(maxDate, 15);
align the ticks to the left of bars and correct the right side of the domain
You don't say what problem you run into when you don't center the bars. But I know the right bar can get clipped.
If you want the elasticX effect, you can implement it manually like this, offsetting the right side by a month (example):
function calc_domain(chart) {
var min = d3.min(chart.group().all(), function(kv) { return kv.key; }),
max = d3.max(chart.group().all(), function(kv) { return kv.key; });
max = d3.time.month.offset(max, 1);
chart.x().domain([min, max]);
}
chart.on('preRender', calc_domain);
chart.on('preRedraw', calc_domain);
Or without elasticX that's just:
maxDate = d3.time.month.offset(maxDate, 1);

Resources