Fix overlapping ticks on log scale (#7242)

This commit is contained in:
Ben McCann
2020-04-03 15:12:34 -07:00
committed by Evert Timberg
parent beb43eeba8
commit 338c75afd9
4 changed files with 78 additions and 59 deletions

View File

@@ -155,7 +155,7 @@ Animation system was completely rewritten in Chart.js v3. Each property can now
#### Ticks
* `options.ticks.major` and `options.ticks.minor` were replaced with scriptable options for tick fonts.
* `Chart.Ticks.formatters.linear` and `Chart.Ticks.formatters.logarithmic` were replaced with `Chart.Ticks.formatters.numeric`.
* `Chart.Ticks.formatters.linear` was renamed to `Chart.Ticks.formatters.numeric`.
#### Tooltip

View File

@@ -1,63 +1,82 @@
import {isArray} from '../helpers/helpers.core';
import {log10} from '../helpers/helpers.math';
/**
* Namespace to hold formatters for different types of ticks
* @namespace Chart.Ticks.formatters
*/
const formatters = {
/**
* Formatter for value labels
* @method Chart.Ticks.formatters.values
* @param value the value to display
* @return {string|string[]} the label to display
*/
values(value) {
return isArray(value) ? value : '' + value;
},
/**
* Formatter for numeric ticks
* @method Chart.Ticks.formatters.numeric
* @param tickValue {number} the value to be formatted
* @param index {number} the position of the tickValue parameter in the ticks array
* @param ticks {object[]} the list of ticks being converted
* @return {string} string representation of the tickValue parameter
*/
numeric(tickValue, index, ticks) {
if (tickValue === 0) {
return '0'; // never show decimal places for 0
}
// If we have lots of ticks, don't use the ones
let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value;
// If we have a number like 2.5 as the delta, figure out how many decimal places we need
if (Math.abs(delta) > 1 && tickValue !== Math.floor(tickValue)) {
// not an integer
delta = tickValue - Math.floor(tickValue);
}
const logDelta = log10(Math.abs(delta));
const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value));
const minTick = Math.min(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value));
const locale = this.chart.options.locale;
if (maxTick < 1e-4 || minTick > 1e+7) { // all ticks are small or big numbers; use scientific notation
const logTick = log10(Math.abs(tickValue));
let numExponential = Math.floor(logTick) - Math.floor(logDelta);
numExponential = Math.max(Math.min(numExponential, 20), 0);
return tickValue.toExponential(numExponential);
}
let numDecimal = -1 * Math.floor(logDelta);
numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places
return new Intl.NumberFormat(locale, {minimumFractionDigits: numDecimal, maximumFractionDigits: numDecimal}).format(tickValue);
}
};
/**
* Formatter for logarithmic ticks
* @method Chart.Ticks.formatters.logarithmic
* @param tickValue {number} the value to be formatted
* @param index {number} the position of the tickValue parameter in the ticks array
* @param ticks {object[]} the list of ticks being converted
* @return {string} string representation of the tickValue parameter
*/
formatters.logarithmic = function(tickValue, index, ticks) {
if (tickValue === 0) {
return '0';
}
const remain = tickValue / (Math.pow(10, Math.floor(log10(tickValue))));
if (remain === 1 || remain === 2 || remain === 5) {
return formatters.numeric.call(this, tickValue, index, ticks);
}
return '';
};
/**
* Namespace to hold static tick generation functions
* @namespace Chart.Ticks
*/
export default {
/**
* Namespace to hold formatters for different types of ticks
* @namespace Chart.Ticks.formatters
*/
formatters: {
/**
* Formatter for value labels
* @method Chart.Ticks.formatters.values
* @param value the value to display
* @return {string|string[]} the label to display
*/
values(value) {
return isArray(value) ? value : '' + value;
},
/**
* Formatter for numeric ticks
* @method Chart.Ticks.formatters.numeric
* @param tickValue {number} the value to be formatted
* @param index {number} the position of the tickValue parameter in the ticks array
* @param ticks {object[]} the list of ticks being converted
* @return {string} string representation of the tickValue parameter
*/
numeric(tickValue, index, ticks) {
if (tickValue === 0) {
return '0'; // never show decimal places for 0
}
// If we have lots of ticks, don't use the ones
let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value;
// If we have a number like 2.5 as the delta, figure out how many decimal places we need
if (Math.abs(delta) > 1 && tickValue !== Math.floor(tickValue)) {
// not an integer
delta = tickValue - Math.floor(tickValue);
}
const logDelta = log10(Math.abs(delta));
const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value));
const minTick = Math.min(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value));
const locale = this.chart.options.locale;
if (maxTick < 1e-4 || minTick > 1e+7) { // all ticks are small or big numbers; use scientific notation
const logTick = log10(Math.abs(tickValue));
let numExponential = Math.floor(logTick) - Math.floor(logDelta);
numExponential = Math.max(Math.min(numExponential, 20), 0);
return tickValue.toExponential(numExponential);
}
let numDecimal = -1 * Math.floor(logDelta);
numDecimal = Math.max(Math.min(numDecimal, 20), 0); // toFixed has a max of 20 decimal places
return new Intl.NumberFormat(locale, {minimumFractionDigits: numDecimal, maximumFractionDigits: numDecimal}).format(tickValue);
}
}
};
export default {formatters};

View File

@@ -50,7 +50,7 @@ function generateTicks(generationOptions, dataRange) {
const defaultConfig = {
// label settings
ticks: {
callback: Ticks.formatters.numeric,
callback: Ticks.formatters.logarithmic,
major: {
enabled: true
}

View File

@@ -679,7 +679,7 @@ describe('Logarithmic Scale tests', function() {
}
});
expect(getLabels(chart.scales.y)).toEqual(['80', '70', '60', '50', '40', '30', '20', '10', '9', '8', '7', '6', '5', '4', '3', '2', '1']);
expect(getLabels(chart.scales.y)).toEqual(['', '', '', '50', '', '', '20', '10', '', '', '', '', '5', '', '', '2', '1']);
});
it('should build labels using the user supplied callback', function() {