Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve tick generation for linear scales #5938

Merged
merged 3 commits into from Jan 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 6 additions & 11 deletions src/scales/scale.linear.js
@@ -1,6 +1,5 @@
'use strict';

var defaults = require('../core/core.defaults');
var helpers = require('../helpers/index');
var scaleService = require('../core/core.scaleService');
var Ticks = require('../core/core.ticks');
Expand Down Expand Up @@ -133,20 +132,16 @@ module.exports = function(Chart) {
// Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero
this.handleTickRangeOptions();
},
getTickLimit: function() {
var maxTicks;
// Returns the maximum number of ticks based on the scale dimension
_computeTickLimit: function() {
var me = this;
var tickOpts = me.options.ticks;
var tickFont;

if (me.isHorizontal()) {
maxTicks = Math.min(tickOpts.maxTicksLimit ? tickOpts.maxTicksLimit : 11, Math.ceil(me.width / 50));
} else {
// The factor of 2 used to scale the font size has been experimentally determined.
var tickFontSize = helpers.valueOrDefault(tickOpts.fontSize, defaults.global.defaultFontSize);
maxTicks = Math.min(tickOpts.maxTicksLimit ? tickOpts.maxTicksLimit : 11, Math.ceil(me.height / (2 * tickFontSize)));
return Math.ceil(me.width / 40);
}

return maxTicks;
tickFont = helpers.options._parseFont(me.options.ticks);
return Math.ceil(me.height / tickFont.lineHeight);
},
// Called after the ticks are built. We need
handleDirectionalChanges: function() {
Expand Down
71 changes: 51 additions & 20 deletions src/scales/scale.linearbase.js
Expand Up @@ -16,35 +16,41 @@ function generateTicks(generationOptions, dataRange) {
// for details.

var stepSize = generationOptions.stepSize;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stepSize < 0 doesn't seems a valid value, so maybe we can simplify all these checks by clamping this value:

var stepSize = Math.max(0, generationOptions.stepSize);
var unit = stepSize || 1;
//...

var unit = stepSize || 1;
var maxNumSpaces = generationOptions.maxTicks - 1;
var min = generationOptions.min;
var max = generationOptions.max;
var spacing, precision, factor, niceRange, niceMin, niceMax, numSpaces;

if (stepSize && stepSize > 0) {
spacing = stepSize;
} else {
niceRange = helpers.niceNum(dataRange.max - dataRange.min, false);
spacing = helpers.niceNum(niceRange / (generationOptions.maxTicks - 1), true);

precision = generationOptions.precision;
if (!helpers.isNullOrUndef(precision)) {
// If the user specified a precision, round to that number of decimal places
factor = Math.pow(10, precision);
spacing = Math.ceil(spacing * factor) / factor;
}
var precision = generationOptions.precision;
var spacing, factor, niceMin, niceMax, numSpaces;

// spacing is set to a nice number of the dataRange divided by maxNumSpaces.
// stepSize is used as a minimum unit if it is specified.
spacing = helpers.niceNum((dataRange.max - dataRange.min) / maxNumSpaces / unit) * unit;
numSpaces = Math.ceil(dataRange.max / spacing) - Math.floor(dataRange.min / spacing);
if (numSpaces > maxNumSpaces) {
// If the calculated num of spaces exceeds maxNumSpaces, recalculate it
spacing = helpers.niceNum(numSpaces * spacing / maxNumSpaces / unit) * unit;
}
// If a precision is not specified, calculate factor based on spacing
if (!factor) {

if (stepSize || helpers.isNullOrUndef(precision)) {
// If a precision is not specified, calculate factor based on spacing
factor = Math.pow(10, helpers.decimalPlaces(spacing));
} else {
// If the user specified a precision, round to that number of decimal places
factor = Math.pow(10, precision);
spacing = Math.ceil(spacing * factor) / factor;
}

niceMin = Math.floor(dataRange.min / spacing) * spacing;
niceMax = Math.ceil(dataRange.max / spacing) * spacing;

// If min, max and stepSize is set and they make an evenly spaced scale use it.
if (!helpers.isNullOrUndef(min) && !helpers.isNullOrUndef(max) && stepSize) {
if (stepSize) {
// If very close to our whole number, use it.
if (helpers.almostWhole((max - min) / stepSize, spacing / 1000)) {
if (!helpers.isNullOrUndef(min) && helpers.almostWhole(min / spacing, spacing / 1000)) {
niceMin = min;
}
if (!helpers.isNullOrUndef(max) && helpers.almostWhole(max / spacing, spacing / 1000)) {
niceMax = max;
}
}
Expand Down Expand Up @@ -146,7 +152,32 @@ module.exports = function(Chart) {
}
}
},
getTickLimit: noop,

getTickLimit: function() {
var me = this;
var tickOpts = me.options.ticks;
var stepSize = tickOpts.stepSize;
var maxTicksLimit = tickOpts.maxTicksLimit;
var maxTicks;

if (stepSize) {
maxTicks = Math.ceil(me.max / stepSize) - Math.floor(me.min / stepSize) + 1;
} else {
maxTicks = me._computeTickLimit();
maxTicksLimit = maxTicksLimit || 11;
}

if (maxTicksLimit) {
maxTicks = Math.min(maxTicksLimit, maxTicks);
}

return maxTicks;
},

_computeTickLimit: function() {
return Number.POSITIVE_INFINITY;
},

handleDirectionalChanges: noop,

buildTicks: function() {
Expand All @@ -155,7 +186,7 @@ module.exports = function(Chart) {
var tickOpts = opts.ticks;

// Figure out what the max number of ticks we can support it is based on the size of
// the axis area. For now, we say that the minimum tick spacing in pixels must be 50
// the axis area. For now, we say that the minimum tick spacing in pixels must be 40
// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
// the graph. Make sure we always have at least 2 ticks
var maxTicks = me.getTickLimit();
Expand Down
4 changes: 0 additions & 4 deletions src/scales/scale.logarithmic.js
Expand Up @@ -15,10 +15,6 @@ function generateTicks(generationOptions, dataRange) {
var ticks = [];
var valueOrDefault = helpers.valueOrDefault;

// Figure out what the max number of ticks we can support it is based on the size of
// the axis area. For now, we say that the minimum tick spacing in pixels must be 50
// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
// the graph
var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min))));

var endExp = Math.floor(helpers.log10(dataRange.max));
Expand Down
8 changes: 3 additions & 5 deletions src/scales/scale.radialLinear.js
Expand Up @@ -357,11 +357,9 @@ module.exports = function(Chart) {
// Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero
me.handleTickRangeOptions();
},
getTickLimit: function() {
var opts = this.options;
var tickOpts = opts.ticks;
var tickBackdropHeight = getTickBackdropHeight(opts);
return Math.min(tickOpts.maxTicksLimit ? tickOpts.maxTicksLimit : 11, Math.ceil(this.drawingArea / tickBackdropHeight));
// Returns the maximum number of ticks based on the scale dimension
_computeTickLimit: function() {
return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options));
},
convertTicksToLabels: function() {
var me = this;
Expand Down
49 changes: 48 additions & 1 deletion test/specs/scale.linear.tests.js
Expand Up @@ -570,7 +570,7 @@ describe('Linear Scale', function() {
expect(chart.scales.yScale0).not.toEqual(undefined); // must construct
expect(chart.scales.yScale0.min).toBe(1);
expect(chart.scales.yScale0.max).toBe(11);
expect(chart.scales.yScale0.ticks).toEqual(['11', '9', '7', '5', '3', '1']);
expect(chart.scales.yScale0.ticks).toEqual(['11', '10', '8', '6', '4', '2', '1']);
});

it('Should create decimal steps if stepSize is a decimal number', function() {
Expand Down Expand Up @@ -749,6 +749,53 @@ describe('Linear Scale', function() {
expect(chart.scales.yScale0.ticks).toEqual(['0.06', '0.05', '0.04', '0.03', '0.02', '0.01', '0']);
});

it('Should correctly limit the maximum number of ticks', function() {
var chart = window.acquireChart({
type: 'bar',
data: {
labels: ['a', 'b'],
datasets: [{
data: [0.5, 2.5]
}]
},
options: {
scales: {
yAxes: [{
id: 'yScale'
}]
}
}
});

expect(chart.scales.yScale.ticks).toEqual(['2.5', '2.0', '1.5', '1.0', '0.5']);

chart.options.scales.yAxes[0].ticks.maxTicksLimit = 11;
chart.update();

expect(chart.scales.yScale.ticks).toEqual(['2.5', '2.0', '1.5', '1.0', '0.5']);

chart.options.scales.yAxes[0].ticks.maxTicksLimit = 21;
chart.update();

expect(chart.scales.yScale.ticks).toEqual([
'2.5', '2.4', '2.3', '2.2', '2.1', '2.0', '1.9', '1.8', '1.7', '1.6',
'1.5', '1.4', '1.3', '1.2', '1.1', '1.0', '0.9', '0.8', '0.7', '0.6',
'0.5'
]);

chart.options.scales.yAxes[0].ticks.maxTicksLimit = 11;
chart.options.scales.yAxes[0].ticks.stepSize = 0.01;
chart.update();

expect(chart.scales.yScale.ticks).toEqual(['2.5', '2.0', '1.5', '1.0', '0.5']);

chart.options.scales.yAxes[0].ticks.min = 0.3;
chart.options.scales.yAxes[0].ticks.max = 2.8;
chart.update();

expect(chart.scales.yScale.ticks).toEqual(['2.8', '2.5', '2.0', '1.5', '1.0', '0.5', '0.3']);
});

it('Should build labels using the user supplied callback', function() {
var chart = window.acquireChart({
type: 'bar',
Expand Down
37 changes: 37 additions & 0 deletions test/specs/scale.radialLinear.tests.js
Expand Up @@ -282,6 +282,43 @@ describe('Test the radial linear scale', function() {
expect(chart.scale.end).toBe(0);
});

it('Should correctly limit the maximum number of ticks', function() {
var chart = window.acquireChart({
type: 'radar',
data: {
labels: ['label1', 'label2', 'label3'],
datasets: [{
data: [0.5, 1.5, 2.5]
}]
},
options: {
scale: {
pointLabels: {
display: false
}
}
}
});

expect(chart.scale.ticks).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']);

chart.options.scale.ticks.maxTicksLimit = 11;
chart.update();

expect(chart.scale.ticks).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']);

chart.options.scale.ticks.stepSize = 0.01;
chart.update();

expect(chart.scale.ticks).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']);

chart.options.scale.ticks.min = 0.3;
chart.options.scale.ticks.max = 2.8;
chart.update();

expect(chart.scale.ticks).toEqual(['0.3', '0.5', '1.0', '1.5', '2.0', '2.5', '2.8']);
});

it('Should build labels using the user supplied callback', function() {
var chart = window.acquireChart({
type: 'radar',
Expand Down