Chart.js: evenly distribute ticks when using maxTicksLimit
Chart.js uses an integral skipRatio
(to figure out how many labels to skip). With Chart.js v2.1.x, you can write your own plugin to use a fractional skipRatio
Preview
Script
Chart.pluginService.register({
afterUpdate: function (chart) {
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
// store the original maxTicksLimit
xScale.options.ticks._maxTicksLimit = xScale.options.ticks.maxTicksLimit;
// let chart.js draw the first and last label
xScale.options.ticks.maxTicksLimit = (xScale.ticks.length % xScale.options.ticks._maxTicksLimit === 0) ? 1 : 2;
var originalXScaleDraw = xScale.draw
xScale.draw = function () {
originalXScaleDraw.apply(this, arguments);
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
var helpers = Chart.helpers;
var tickFontColor = helpers.getValueOrDefault(xScale.options.ticks.fontColor, Chart.defaults.global.defaultFontColor);
var tickFontSize = helpers.getValueOrDefault(xScale.options.ticks.fontSize, Chart.defaults.global.defaultFontSize);
var tickFontStyle = helpers.getValueOrDefault(xScale.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle);
var tickFontFamily = helpers.getValueOrDefault(xScale.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily);
var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily);
var tl = xScale.options.gridLines.tickMarkLength;
var isRotated = xScale.labelRotation !== 0;
var yTickStart = xScale.top;
var yTickEnd = xScale.top + tl;
var chartArea = chart.chartArea;
// use the saved ticks
var maxTicks = xScale.options.ticks._maxTicksLimit - 1;
var ticksPerVisibleTick = xScale.ticks.length / maxTicks;
// chart.js uses an integral skipRatio - this causes all the fractional ticks to be accounted for between the last 2 labels
// we use a fractional skipRatio
var ticksCovered = 0;
helpers.each(xScale.ticks, function (label, index) {
if (index < ticksCovered)
return;
ticksCovered += ticksPerVisibleTick;
// chart.js has already drawn these 2
if (index === 0 || index === (xScale.ticks.length - 1))
return;
// copy of chart.js code
var xLineValue = this.getPixelForTick(index);
var xLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines);
if (this.options.gridLines.display) {
this.ctx.lineWidth = this.options.gridLines.lineWidth;
this.ctx.strokeStyle = this.options.gridLines.color;
xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
// Draw the label area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.moveTo(xLineValue, yTickStart);
this.ctx.lineTo(xLineValue, yTickEnd);
}
// Draw the chart area
if (this.options.gridLines.drawOnChartArea) {
this.ctx.moveTo(xLineValue, chartArea.top);
this.ctx.lineTo(xLineValue, chartArea.bottom);
}
// Need to stroke in the loop because we are potentially changing line widths & colours
this.ctx.stroke();
}
if (this.options.ticks.display) {
this.ctx.save();
this.ctx.translate(xLabelValue + this.options.ticks.labelOffset, (isRotated) ? this.top + 12 : this.options.position === "top" ? this.bottom - tl : this.top + tl);
this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1);
this.ctx.font = tickLabelFont;
this.ctx.textAlign = (isRotated) ? "right" : "center";
this.ctx.textBaseline = (isRotated) ? "middle" : this.options.position === "top" ? "bottom" : "top";
this.ctx.fillText(label, 0, 0);
this.ctx.restore();
}
}, xScale);
}
};
}
},
});
Fiddle - http://jsfiddle.net/bh63pe1v/
A simpler solution until this is permanently fixed by the Chart JS contributors is to include a decimal in maxTicksLimit
.
For example:
maxTicksLimit: 8,
produces a huge gap at the end.
maxTicksLimit: 8.1,
Does not produce a huge gap at the end.
Depending on what you want to set your maxTicksLimit to, you need to play around with different decimals to see which one produces the best result.