score:1

Accepted answer

I have a first "working" solution... and it's uglier than I tought it would be:

const chart = dc.seriesChart('#myChart')
    // ...
    .x(d3.scaleTime())
    .elasticY(true)
    .elasticX(true)
    .on('renderlet', updateTicks);
    .xAxis()
        .tickFormat(d3.timeFormat('%Y-%m-%d'))
        .ticks(10); // We set 10 ticks by default
chart.render();

// Needed to prevent infinite loops...
let redrawing = false;

function updateTicks(chart) {
    if (redrawing) {
        redrawing = false;
    } else {
        const days = (chart.xAxisMax() - chart.xAxisMin()) / 86400000;
        if (days < 10) {
            chart.xAxis().ticks(d3.timeDay);
        } else {
            chart.xAxis().ticks(10);
        }
        redrawing = true;
        chart.redraw();
    }
}

xAxisMin() and xAxisMax() values aren't calculated until the chart has already been rendered or redrawn, so I have to use the renderlet event instead of preRedraw. And since the chart is already drawn, any change I make to the axis won't be effective until the next redraw... so I have to force it myself (and prevent an infinite loop, because my redraw will trigger a new renderlet event).

It's not only ugly code, the axis transition is actually visible on the chart. I could avoid it by using the pretransition event instead, as pointed out by Gordon. Or by calculating the min and max values on the preRender/preRedraw event, directly from the Crossfilter group, but it's starting to feel like a huge overkill.


Here is a working-but-still-ugly solution, calculating the min and max myself:

const chart = dc.seriesChart('#myChart')
    // ...
    .x(d3.scaleTime())
    .elasticY(true)
    .elasticX(true)
    .on('preRender', updateTicks),
    .on('preRedraw', updateTicks);
    .xAxis().tickFormat(d3.timeFormat('%Y-%m-%d'));
chart.render();

function updateTicks(chart) {
    const range = chart.group().all().reduce((accum, d) => minMax(d.key.date, accum), {});
    const days = 1 + ((new Date(range.max) - new Date(range.min)) / 86400000);
    if (days <= 7) {
        chart.xAxis().ticks(d3.timeDay);
    } else if (days <= 30) {
        chart.xAxis().ticks(d3.timeMonday);
    } else {
        chart.xAxis().ticks(d3.timeMonth);
    }
}

function minMax(val, obj) {
    if ((obj.min === undefined) || (val < obj.min)) {
        obj.min = val;
    }
    if ((obj.max === undefined) || (val > obj.max)) {
        obj.max = val;
    }
    return obj;
}

Not ugly code per se, it's just annoying - traversing the whole data array each time I redraw the chart, only to find the min and max values, which will be calculated again by dc/crossfilter/d3 anyway.


Related Query