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 '';
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;
}
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();
}
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;
}
--- /dev/null
+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
+ }
+};
--- /dev/null
+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
+ }
+};
--- /dev/null
+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
+ }
+};
--- /dev/null
+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
+ }
+};
--- /dev/null
+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
+ }
+};
min: 0.1,
max: 1,
ticks: {
+ autoSkip: false,
callback: function(value) {
return value.toString();
}
min: 0.1,
max: 1,
ticks: {
+ autoSkip: false,
callback: function(value) {
return value.toString();
}
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() {
});
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
});
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
}
});
- 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() {
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() {
y: {
type: 'logarithmic',
ticks: {
+ autoSkip: false,
callback: function(value) {
return value;
}
});
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);
});
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);
});
type: 'logarithmic',
reverse: true,
ticks: {
+ autoSkip: false,
callback: function(value) {
return value;
}
});
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);
});
});
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);
});
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() {
});
// 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() {