mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-02-20 01:31:20 +01:00
Refine logarithmic scaling / tick generation (#9166)
* Refine logarithmic scaling / tick generation * Disable autoSkip on reverese test * Reduce ticks, fix min
This commit is contained in:
@@ -66,8 +66,8 @@ const formatters = {
|
||||
if (tickValue === 0) {
|
||||
return '0';
|
||||
}
|
||||
const remain = tickValue / (Math.pow(10, Math.floor(log10(tickValue))));
|
||||
if (remain === 1 || remain === 2 || remain === 5) {
|
||||
const remain = ticks[index].significand || (tickValue / (Math.pow(10, Math.floor(log10(tickValue)))));
|
||||
if ([1, 2, 3, 5, 10, 15].includes(remain) || index > 0.8 * ticks.length) {
|
||||
return formatters.numeric.call(this, tickValue, index, ticks);
|
||||
}
|
||||
return '';
|
||||
|
||||
@@ -5,41 +5,68 @@ import Scale from '../core/core.scale';
|
||||
import LinearScaleBase from './scale.linearbase';
|
||||
import Ticks from '../core/core.ticks';
|
||||
|
||||
const log10Floor = v => Math.floor(log10(v));
|
||||
const changeExponent = (v, m) => Math.pow(10, log10Floor(v) + m);
|
||||
|
||||
function isMajor(tickVal) {
|
||||
const remain = tickVal / (Math.pow(10, Math.floor(log10(tickVal))));
|
||||
const remain = tickVal / (Math.pow(10, log10Floor(tickVal)));
|
||||
return remain === 1;
|
||||
}
|
||||
|
||||
function steps(min, max, rangeExp) {
|
||||
const rangeStep = Math.pow(10, rangeExp);
|
||||
const start = Math.floor(min / rangeStep);
|
||||
const end = Math.ceil(max / rangeStep);
|
||||
return end - start;
|
||||
}
|
||||
|
||||
function startExp(min, max) {
|
||||
const range = max - min;
|
||||
let rangeExp = log10Floor(range);
|
||||
while (steps(min, max, rangeExp) > 10) {
|
||||
rangeExp++;
|
||||
}
|
||||
while (steps(min, max, rangeExp) < 10) {
|
||||
rangeExp--;
|
||||
}
|
||||
return Math.min(rangeExp, log10Floor(min));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a set of logarithmic ticks
|
||||
* @param generationOptions the options used to generate the ticks
|
||||
* @param dataRange the range of the data
|
||||
* @returns {object[]} array of tick objects
|
||||
*/
|
||||
function generateTicks(generationOptions, dataRange) {
|
||||
const endExp = Math.floor(log10(dataRange.max));
|
||||
const endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp));
|
||||
function generateTicks(generationOptions, {min, max}) {
|
||||
min = finiteOrDefault(generationOptions.min, min);
|
||||
const ticks = [];
|
||||
let tickVal = finiteOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min))));
|
||||
let exp = Math.floor(log10(tickVal));
|
||||
let significand = Math.floor(tickVal / Math.pow(10, exp));
|
||||
const minExp = log10Floor(min);
|
||||
let exp = startExp(min, max);
|
||||
let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1;
|
||||
|
||||
do {
|
||||
ticks.push({value: tickVal, major: isMajor(tickVal)});
|
||||
|
||||
++significand;
|
||||
if (significand === 10) {
|
||||
significand = 1;
|
||||
++exp;
|
||||
const stepSize = Math.pow(10, exp);
|
||||
const base = minExp > exp ? Math.pow(10, minExp) : 0;
|
||||
const start = Math.round((min - base) * precision) / precision;
|
||||
const offset = Math.floor((min - base) / stepSize / 10) * stepSize * 10;
|
||||
let significand = Math.floor((start - offset) / Math.pow(10, exp));
|
||||
let value = finiteOrDefault(generationOptions.min, Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision);
|
||||
while (value < max) {
|
||||
ticks.push({value, major: isMajor(value), significand});
|
||||
if (significand >= 10) {
|
||||
significand = significand < 15 ? 15 : 20;
|
||||
} else {
|
||||
significand++;
|
||||
}
|
||||
if (significand >= 20) {
|
||||
exp++;
|
||||
significand = 2;
|
||||
precision = exp >= 0 ? 1 : precision;
|
||||
}
|
||||
|
||||
tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision;
|
||||
} while (exp < endExp || (exp === endExp && significand < endSignificand));
|
||||
|
||||
const lastTick = finiteOrDefault(generationOptions.max, tickVal);
|
||||
ticks.push({value: lastTick, major: isMajor(tickVal)});
|
||||
value = Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision;
|
||||
}
|
||||
const lastTick = finiteOrDefault(generationOptions.max, value);
|
||||
ticks.push({value: lastTick, major: isMajor(lastTick), significand});
|
||||
|
||||
return ticks;
|
||||
}
|
||||
@@ -92,6 +119,12 @@ export default class LogarithmicScale extends Scale {
|
||||
this._zero = true;
|
||||
}
|
||||
|
||||
// if data has `0` in it or `beginAtZero` is true, min (non zero) value is at bottom
|
||||
// of scale, and it does not equal suggestedMin, lower the min bound by one exp.
|
||||
if (this._zero && this.min !== this._suggestedMin && !isFinite(this._userMin)) {
|
||||
this.min = min === changeExponent(this.min, 0) ? changeExponent(this.min, -1) : changeExponent(this.min, 0);
|
||||
}
|
||||
|
||||
this.handleTickRangeOptions();
|
||||
}
|
||||
|
||||
@@ -102,28 +135,24 @@ export default class LogarithmicScale extends Scale {
|
||||
|
||||
const setMin = v => (min = minDefined ? min : v);
|
||||
const setMax = v => (max = maxDefined ? max : v);
|
||||
const exp = (v, m) => Math.pow(10, Math.floor(log10(v)) + m);
|
||||
|
||||
if (min === max) {
|
||||
if (min <= 0) { // includes null
|
||||
setMin(1);
|
||||
setMax(10);
|
||||
} else {
|
||||
setMin(exp(min, -1));
|
||||
setMax(exp(max, +1));
|
||||
setMin(changeExponent(min, -1));
|
||||
setMax(changeExponent(max, +1));
|
||||
}
|
||||
}
|
||||
if (min <= 0) {
|
||||
setMin(exp(max, -1));
|
||||
setMin(changeExponent(max, -1));
|
||||
}
|
||||
if (max <= 0) {
|
||||
setMax(exp(min, +1));
|
||||
}
|
||||
// if data has `0` in it or `beginAtZero` is true, min (non zero) value is at bottom
|
||||
// of scale, and it does not equal suggestedMin, lower the min bound by one exp.
|
||||
if (this._zero && this.min !== this._suggestedMin && min === exp(this.min, 0)) {
|
||||
setMin(exp(min, -1));
|
||||
|
||||
setMax(changeExponent(min, +1));
|
||||
}
|
||||
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
31
test/fixtures/scale.logarithmic/large-range.js
vendored
Normal file
31
test/fixtures/scale.logarithmic/large-range.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
config: {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||
datasets: [{
|
||||
backgroundColor: 'red',
|
||||
borderColor: 'red',
|
||||
fill: false,
|
||||
data: [23, 21, 34, 52, 115, 3333, 5116]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'logarithmic',
|
||||
ticks: {
|
||||
autoSkip: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
spriteText: true
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/scale.logarithmic/large-range.png
vendored
Normal file
BIN
test/fixtures/scale.logarithmic/large-range.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
31
test/fixtures/scale.logarithmic/large-values-small-range.js
vendored
Normal file
31
test/fixtures/scale.logarithmic/large-values-small-range.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
config: {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||
datasets: [{
|
||||
backgroundColor: 'red',
|
||||
borderColor: 'red',
|
||||
fill: false,
|
||||
data: [5000.002, 5000.012, 5000.01, 5000.03, 5000.04, 5000.004, 5000.032]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'logarithmic',
|
||||
ticks: {
|
||||
autoSkip: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
spriteText: true
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/scale.logarithmic/large-values-small-range.png
vendored
Normal file
BIN
test/fixtures/scale.logarithmic/large-values-small-range.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
31
test/fixtures/scale.logarithmic/med-range.js
vendored
Normal file
31
test/fixtures/scale.logarithmic/med-range.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
config: {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||
datasets: [{
|
||||
backgroundColor: 'red',
|
||||
borderColor: 'red',
|
||||
fill: false,
|
||||
data: [25, 24, 27, 32, 45, 30, 28]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'logarithmic',
|
||||
ticks: {
|
||||
autoSkip: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
spriteText: true
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/scale.logarithmic/med-range.png
vendored
Normal file
BIN
test/fixtures/scale.logarithmic/med-range.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
33
test/fixtures/scale.logarithmic/min-max.js
vendored
Normal file
33
test/fixtures/scale.logarithmic/min-max.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
config: {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||
datasets: [{
|
||||
backgroundColor: 'red',
|
||||
borderColor: 'red',
|
||||
fill: false,
|
||||
data: [250, 240, 270, 320, 450, 300, 280]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'logarithmic',
|
||||
min: 233,
|
||||
max: 471,
|
||||
ticks: {
|
||||
autoSkip: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
spriteText: true
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/scale.logarithmic/min-max.png
vendored
Normal file
BIN
test/fixtures/scale.logarithmic/min-max.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
31
test/fixtures/scale.logarithmic/small-range.js
vendored
Normal file
31
test/fixtures/scale.logarithmic/small-range.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
config: {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
||||
datasets: [{
|
||||
backgroundColor: 'red',
|
||||
borderColor: 'red',
|
||||
fill: false,
|
||||
data: [3, 1, 4, 2, 5, 3, 16]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'logarithmic',
|
||||
ticks: {
|
||||
autoSkip: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
spriteText: true
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/scale.logarithmic/small-range.png
vendored
Normal file
BIN
test/fixtures/scale.logarithmic/small-range.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -71,6 +71,7 @@ describe('Test tick generators', function() {
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
callback: function(value) {
|
||||
return value.toString();
|
||||
}
|
||||
@@ -81,6 +82,7 @@ describe('Test tick generators', function() {
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
callback: function(value) {
|
||||
return value.toString();
|
||||
}
|
||||
@@ -93,8 +95,8 @@ describe('Test tick generators', function() {
|
||||
var xLabels = getLabels(chart.scales.x);
|
||||
var yLabels = getLabels(chart.scales.y);
|
||||
|
||||
expect(xLabels).toEqual(['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']);
|
||||
expect(yLabels).toEqual(['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']);
|
||||
expect(xLabels).toEqual(['0.1', '0.11', '0.12', '0.13', '0.14', '0.15', '0.16', '0.17', '0.18', '0.19', '0.2', '0.25', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']);
|
||||
expect(yLabels).toEqual(['0.1', '0.11', '0.12', '0.13', '0.14', '0.15', '0.16', '0.17', '0.18', '0.19', '0.2', '0.25', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']);
|
||||
});
|
||||
|
||||
describe('formatters.numeric', function() {
|
||||
|
||||
@@ -131,11 +131,11 @@ describe('Logarithmic Scale tests', function() {
|
||||
});
|
||||
|
||||
expect(chart.scales.y).not.toEqual(undefined); // must construct
|
||||
expect(chart.scales.y.min).toBe(10);
|
||||
expect(chart.scales.y.min).toBe(40);
|
||||
expect(chart.scales.y.max).toBe(1000);
|
||||
|
||||
expect(chart.scales.y1).not.toEqual(undefined); // must construct
|
||||
expect(chart.scales.y1.min).toBe(1);
|
||||
expect(chart.scales.y1.min).toBe(5);
|
||||
expect(chart.scales.y1.max).toBe(5000);
|
||||
|
||||
expect(chart.scales.y2).not.toEqual(undefined); // must construct
|
||||
@@ -189,7 +189,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
});
|
||||
|
||||
expect(chart.scales.y1).not.toEqual(undefined); // must construct
|
||||
expect(chart.scales.y1.min).toBe(1);
|
||||
expect(chart.scales.y1.min).toBe(5);
|
||||
expect(chart.scales.y1.max).toBe(5000);
|
||||
|
||||
expect(chart.scales.y2).not.toEqual(undefined); // must construct
|
||||
@@ -271,11 +271,11 @@ describe('Logarithmic Scale tests', function() {
|
||||
}
|
||||
});
|
||||
|
||||
expect(chart.scales.x.min).toBe(1);
|
||||
expect(chart.scales.x.min).toBe(2);
|
||||
expect(chart.scales.x.max).toBe(100);
|
||||
|
||||
expect(chart.scales.y.min).toBe(1);
|
||||
expect(chart.scales.y.max).toBe(200);
|
||||
expect(chart.scales.y.min).toBe(6);
|
||||
expect(chart.scales.y.max).toBe(150);
|
||||
});
|
||||
|
||||
it('should correctly determine the max & min for scatter data when 0 values are present', function() {
|
||||
@@ -417,8 +417,8 @@ describe('Logarithmic Scale tests', function() {
|
||||
chart.data.datasets[0].data = [0.15, 0.15];
|
||||
chart.update();
|
||||
|
||||
expect(chart.scales.y.min).toBe(0.01);
|
||||
expect(chart.scales.y.max).toBe(1);
|
||||
expect(chart.scales.y.min).toBe(0.1);
|
||||
expect(chart.scales.y.max).toBe(0.15);
|
||||
});
|
||||
|
||||
it('should use the min and max options', function() {
|
||||
@@ -528,6 +528,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
y: {
|
||||
type: 'logarithmic',
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
callback: function(value) {
|
||||
return value;
|
||||
}
|
||||
@@ -538,7 +539,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
});
|
||||
|
||||
var scale = chart.scales.y;
|
||||
expect(getLabels(scale)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80]);
|
||||
expect(getLabels(scale)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50, 60, 70, 80]);
|
||||
expect(scale.start).toEqual(1);
|
||||
expect(scale.end).toEqual(80);
|
||||
});
|
||||
@@ -568,7 +569,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
|
||||
var scale = chart.scales.y;
|
||||
// Counts down because the lines are drawn top to bottom
|
||||
expect(getLabels(scale)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30]);
|
||||
expect(getLabels(scale)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30]);
|
||||
expect(scale.start).toEqual(0.1);
|
||||
expect(scale.end).toEqual(30);
|
||||
});
|
||||
@@ -589,6 +590,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
type: 'logarithmic',
|
||||
reverse: true,
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
callback: function(value) {
|
||||
return value;
|
||||
}
|
||||
@@ -599,7 +601,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
});
|
||||
|
||||
var scale = chart.scales.y;
|
||||
expect(getLabels(scale)).toEqual([80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
||||
expect(getLabels(scale)).toEqual([80, 70, 60, 50, 40, 30, 20, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
||||
expect(scale.start).toEqual(80);
|
||||
expect(scale.end).toEqual(1);
|
||||
});
|
||||
@@ -629,7 +631,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
});
|
||||
|
||||
var scale = chart.scales.y;
|
||||
expect(getLabels(scale)).toEqual([30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
||||
expect(getLabels(scale)).toEqual([30, 20, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
||||
expect(scale.start).toEqual(30);
|
||||
expect(scale.end).toEqual(1);
|
||||
});
|
||||
@@ -646,13 +648,16 @@ describe('Logarithmic Scale tests', function() {
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
type: 'logarithmic'
|
||||
type: 'logarithmic',
|
||||
ticks: {
|
||||
autoSkip: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(getLabels(chart.scales.y)).toEqual(['1', '2', '', '', '5', '', '', '', '', '10', '20', '', '', '50', '', '', '']);
|
||||
expect(getLabels(chart.scales.y)).toEqual(['1', '2', '3', '', '5', '', '', '', '', '10', '15', '20', '30', '', '50', '60', '70', '80']);
|
||||
});
|
||||
|
||||
it('should build labels using the user supplied callback', function() {
|
||||
@@ -679,7 +684,7 @@ describe('Logarithmic Scale tests', function() {
|
||||
});
|
||||
|
||||
// Just the index
|
||||
expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16']);
|
||||
expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17']);
|
||||
});
|
||||
|
||||
it('should correctly get the correct label for a data item', function() {
|
||||
|
||||
Reference in New Issue
Block a user