我有第一个“可行”的解决方案......它比我想象的要丑:
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() 和xAxisMax() 值在图表已经被渲染或重绘之前不会被计算,所以我必须使用renderlet 事件而不是preRedraw。而且由于图表已经绘制,我对轴所做的任何更改都将在下一次重绘之前生效......所以我必须自己强制它(并防止无限循环,因为我的重绘将触发一个新的渲染组件事件)。
这不仅是丑陋的代码,轴的转换实际上在图表上是可见的。正如 Gordon 所指出的,我可以通过使用 pretransition 事件来避免它。或者通过直接从 Crossfilter 组计算 preRender/preRedraw 事件的最小值和最大值,但它开始感觉像是一个巨大的矫枉过正。
这是一个可行但仍然丑陋的解决方案,我自己计算最小值和最大值:
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;
}
代码本身并不难看,只是很烦人 - 每次我重绘图表时都要遍历整个数据数组,只是为了找到最小值和最大值,无论如何这将由 dc/crossfilter/d3 再次计算。