Skip to content

Commit 3b76488

Browse files
authored
Refine logarithmic scaling / tick generation (#9166)
* Refine logarithmic scaling / tick generation * Disable autoSkip on reverese test * Reduce ticks, fix min
1 parent 2031cdf commit 3b76488

14 files changed

+243
-50
lines changed

src/core/core.ticks.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ const formatters = {
6666
if (tickValue === 0) {
6767
return '0';
6868
}
69-
const remain = tickValue / (Math.pow(10, Math.floor(log10(tickValue))));
70-
if (remain === 1 || remain === 2 || remain === 5) {
69+
const remain = ticks[index].significand || (tickValue / (Math.pow(10, Math.floor(log10(tickValue)))));
70+
if ([1, 2, 3, 5, 10, 15].includes(remain) || index > 0.8 * ticks.length) {
7171
return formatters.numeric.call(this, tickValue, index, ticks);
7272
}
7373
return '';

src/scales/scale.logarithmic.js

+60-31
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,68 @@ import Scale from '../core/core.scale';
55
import LinearScaleBase from './scale.linearbase';
66
import Ticks from '../core/core.ticks';
77

8+
const log10Floor = v => Math.floor(log10(v));
9+
const changeExponent = (v, m) => Math.pow(10, log10Floor(v) + m);
10+
811
function isMajor(tickVal) {
9-
const remain = tickVal / (Math.pow(10, Math.floor(log10(tickVal))));
12+
const remain = tickVal / (Math.pow(10, log10Floor(tickVal)));
1013
return remain === 1;
1114
}
1215

16+
function steps(min, max, rangeExp) {
17+
const rangeStep = Math.pow(10, rangeExp);
18+
const start = Math.floor(min / rangeStep);
19+
const end = Math.ceil(max / rangeStep);
20+
return end - start;
21+
}
22+
23+
function startExp(min, max) {
24+
const range = max - min;
25+
let rangeExp = log10Floor(range);
26+
while (steps(min, max, rangeExp) > 10) {
27+
rangeExp++;
28+
}
29+
while (steps(min, max, rangeExp) < 10) {
30+
rangeExp--;
31+
}
32+
return Math.min(rangeExp, log10Floor(min));
33+
}
34+
35+
1336
/**
1437
* Generate a set of logarithmic ticks
1538
* @param generationOptions the options used to generate the ticks
1639
* @param dataRange the range of the data
1740
* @returns {object[]} array of tick objects
1841
*/
19-
function generateTicks(generationOptions, dataRange) {
20-
const endExp = Math.floor(log10(dataRange.max));
21-
const endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp));
42+
function generateTicks(generationOptions, {min, max}) {
43+
min = finiteOrDefault(generationOptions.min, min);
2244
const ticks = [];
23-
let tickVal = finiteOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min))));
24-
let exp = Math.floor(log10(tickVal));
25-
let significand = Math.floor(tickVal / Math.pow(10, exp));
45+
const minExp = log10Floor(min);
46+
let exp = startExp(min, max);
2647
let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1;
27-
28-
do {
29-
ticks.push({value: tickVal, major: isMajor(tickVal)});
30-
31-
++significand;
32-
if (significand === 10) {
33-
significand = 1;
34-
++exp;
48+
const stepSize = Math.pow(10, exp);
49+
const base = minExp > exp ? Math.pow(10, minExp) : 0;
50+
const start = Math.round((min - base) * precision) / precision;
51+
const offset = Math.floor((min - base) / stepSize / 10) * stepSize * 10;
52+
let significand = Math.floor((start - offset) / Math.pow(10, exp));
53+
let value = finiteOrDefault(generationOptions.min, Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision);
54+
while (value < max) {
55+
ticks.push({value, major: isMajor(value), significand});
56+
if (significand >= 10) {
57+
significand = significand < 15 ? 15 : 20;
58+
} else {
59+
significand++;
60+
}
61+
if (significand >= 20) {
62+
exp++;
63+
significand = 2;
3564
precision = exp >= 0 ? 1 : precision;
3665
}
37-
38-
tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision;
39-
} while (exp < endExp || (exp === endExp && significand < endSignificand));
40-
41-
const lastTick = finiteOrDefault(generationOptions.max, tickVal);
42-
ticks.push({value: lastTick, major: isMajor(tickVal)});
66+
value = Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision;
67+
}
68+
const lastTick = finiteOrDefault(generationOptions.max, value);
69+
ticks.push({value: lastTick, major: isMajor(lastTick), significand});
4370

4471
return ticks;
4572
}
@@ -92,6 +119,12 @@ export default class LogarithmicScale extends Scale {
92119
this._zero = true;
93120
}
94121

122+
// if data has `0` in it or `beginAtZero` is true, min (non zero) value is at bottom
123+
// of scale, and it does not equal suggestedMin, lower the min bound by one exp.
124+
if (this._zero && this.min !== this._suggestedMin && !isFinite(this._userMin)) {
125+
this.min = min === changeExponent(this.min, 0) ? changeExponent(this.min, -1) : changeExponent(this.min, 0);
126+
}
127+
95128
this.handleTickRangeOptions();
96129
}
97130

@@ -102,28 +135,24 @@ export default class LogarithmicScale extends Scale {
102135

103136
const setMin = v => (min = minDefined ? min : v);
104137
const setMax = v => (max = maxDefined ? max : v);
105-
const exp = (v, m) => Math.pow(10, Math.floor(log10(v)) + m);
106138

107139
if (min === max) {
108140
if (min <= 0) { // includes null
109141
setMin(1);
110142
setMax(10);
111143
} else {
112-
setMin(exp(min, -1));
113-
setMax(exp(max, +1));
144+
setMin(changeExponent(min, -1));
145+
setMax(changeExponent(max, +1));
114146
}
115147
}
116148
if (min <= 0) {
117-
setMin(exp(max, -1));
149+
setMin(changeExponent(max, -1));
118150
}
119151
if (max <= 0) {
120-
setMax(exp(min, +1));
121-
}
122-
// if data has `0` in it or `beginAtZero` is true, min (non zero) value is at bottom
123-
// of scale, and it does not equal suggestedMin, lower the min bound by one exp.
124-
if (this._zero && this.min !== this._suggestedMin && min === exp(this.min, 0)) {
125-
setMin(exp(min, -1));
152+
153+
setMax(changeExponent(min, +1));
126154
}
155+
127156
this.min = min;
128157
this.max = max;
129158
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module.exports = {
2+
config: {
3+
type: 'line',
4+
data: {
5+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
6+
datasets: [{
7+
backgroundColor: 'red',
8+
borderColor: 'red',
9+
fill: false,
10+
data: [23, 21, 34, 52, 115, 3333, 5116]
11+
}]
12+
},
13+
options: {
14+
responsive: true,
15+
scales: {
16+
x: {
17+
display: false,
18+
},
19+
y: {
20+
type: 'logarithmic',
21+
ticks: {
22+
autoSkip: false
23+
}
24+
}
25+
}
26+
}
27+
},
28+
options: {
29+
spriteText: true
30+
}
31+
};
21.4 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module.exports = {
2+
config: {
3+
type: 'line',
4+
data: {
5+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
6+
datasets: [{
7+
backgroundColor: 'red',
8+
borderColor: 'red',
9+
fill: false,
10+
data: [5000.002, 5000.012, 5000.01, 5000.03, 5000.04, 5000.004, 5000.032]
11+
}]
12+
},
13+
options: {
14+
responsive: true,
15+
scales: {
16+
x: {
17+
display: false,
18+
},
19+
y: {
20+
type: 'logarithmic',
21+
ticks: {
22+
autoSkip: false
23+
}
24+
}
25+
}
26+
}
27+
},
28+
options: {
29+
spriteText: true
30+
}
31+
};
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module.exports = {
2+
config: {
3+
type: 'line',
4+
data: {
5+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
6+
datasets: [{
7+
backgroundColor: 'red',
8+
borderColor: 'red',
9+
fill: false,
10+
data: [25, 24, 27, 32, 45, 30, 28]
11+
}]
12+
},
13+
options: {
14+
responsive: true,
15+
scales: {
16+
x: {
17+
display: false,
18+
},
19+
y: {
20+
type: 'logarithmic',
21+
ticks: {
22+
autoSkip: false
23+
}
24+
}
25+
}
26+
}
27+
},
28+
options: {
29+
spriteText: true
30+
}
31+
};
23.1 KB
Loading
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module.exports = {
2+
config: {
3+
type: 'line',
4+
data: {
5+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
6+
datasets: [{
7+
backgroundColor: 'red',
8+
borderColor: 'red',
9+
fill: false,
10+
data: [250, 240, 270, 320, 450, 300, 280]
11+
}]
12+
},
13+
options: {
14+
responsive: true,
15+
scales: {
16+
x: {
17+
display: false,
18+
},
19+
y: {
20+
type: 'logarithmic',
21+
min: 233,
22+
max: 471,
23+
ticks: {
24+
autoSkip: false
25+
}
26+
}
27+
}
28+
}
29+
},
30+
options: {
31+
spriteText: true
32+
}
33+
};
21.6 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module.exports = {
2+
config: {
3+
type: 'line',
4+
data: {
5+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
6+
datasets: [{
7+
backgroundColor: 'red',
8+
borderColor: 'red',
9+
fill: false,
10+
data: [3, 1, 4, 2, 5, 3, 16]
11+
}]
12+
},
13+
options: {
14+
responsive: true,
15+
scales: {
16+
x: {
17+
display: false,
18+
},
19+
y: {
20+
type: 'logarithmic',
21+
ticks: {
22+
autoSkip: false
23+
}
24+
}
25+
}
26+
}
27+
},
28+
options: {
29+
spriteText: true
30+
}
31+
};
22.1 KB
Loading

test/specs/core.ticks.tests.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ describe('Test tick generators', function() {
7171
min: 0.1,
7272
max: 1,
7373
ticks: {
74+
autoSkip: false,
7475
callback: function(value) {
7576
return value.toString();
7677
}
@@ -81,6 +82,7 @@ describe('Test tick generators', function() {
8182
min: 0.1,
8283
max: 1,
8384
ticks: {
85+
autoSkip: false,
8486
callback: function(value) {
8587
return value.toString();
8688
}
@@ -93,8 +95,8 @@ describe('Test tick generators', function() {
9395
var xLabels = getLabels(chart.scales.x);
9496
var yLabels = getLabels(chart.scales.y);
9597

96-
expect(xLabels).toEqual(['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']);
97-
expect(yLabels).toEqual(['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']);
98+
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']);
99+
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']);
98100
});
99101

100102
describe('formatters.numeric', function() {

0 commit comments

Comments
 (0)