Skip to content

Commit 97e2373

Browse files
authored
Change ticks.mode to scale.distribution (#4582)
Fix `ticks.mode` behavior when `ticks.source` is `auto`: the lookup table is now built from the data and not from the ticks, so data (and ticks) are correctly distributed along the scale. Rename the option to `distribution` (more explicit than `mode`) and since this option applies from now on the data, it seems better to have it under `scale` instead `scale.ticks`.
1 parent 53b7a63 commit 97e2373

File tree

2 files changed

+107
-93
lines changed

2 files changed

+107
-93
lines changed

src/scales/scale.time.js

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ function sorter(a, b) {
6464
return a - b;
6565
}
6666

67+
function arrayUnique(items) {
68+
var hash = {};
69+
var out = [];
70+
var i, ilen, item;
71+
72+
for (i = 0, ilen = items.length; i < ilen; ++i) {
73+
item = items[i];
74+
if (!hash[item]) {
75+
hash[item] = true;
76+
out.push(item);
77+
}
78+
}
79+
80+
return out;
81+
}
82+
6783
/**
6884
* Returns an array of {time, pos} objects used to interpolate a specific `time` or position
6985
* (`pos`) on the scale, by searching entries before and after the requested value. `pos` is
@@ -73,31 +89,33 @@ function sorter(a, b) {
7389
* to create the lookup table. The table ALWAYS contains at least two items: min and max.
7490
*
7591
* @param {Number[]} timestamps - timestamps sorted from lowest to highest.
76-
* @param {Boolean} linear - If true, timestamps will be spread linearly along the min/max
77-
* range, so basically, the table will contains only two items: {min, 0} and {max, 1}. If
78-
* false, timestamps will be positioned at the same distance from each other. In this case,
79-
* only timestamps that break the time linearity are registered, meaning that in the best
80-
* case, all timestamps are linear, the table contains only min and max.
92+
* @param {String} distribution - If 'linear', timestamps will be spread linearly along the min
93+
* and max range, so basically, the table will contains only two items: {min, 0} and {max, 1}.
94+
* If 'series', timestamps will be positioned at the same distance from each other. In this
95+
* case, only timestamps that break the time linearity are registered, meaning that in the
96+
* best case, all timestamps are linear, the table contains only min and max.
8197
*/
82-
function buildLookupTable(timestamps, min, max, linear) {
83-
if (linear || !timestamps.length) {
98+
function buildLookupTable(timestamps, min, max, distribution) {
99+
if (distribution === 'linear' || !timestamps.length) {
84100
return [
85101
{time: min, pos: 0},
86102
{time: max, pos: 1}
87103
];
88104
}
89105

90106
var table = [];
91-
var items = timestamps.slice(0);
107+
var items = [min];
92108
var i, ilen, prev, curr, next;
93109

94-
if (min < timestamps[0]) {
95-
items.unshift(min);
96-
}
97-
if (max > timestamps[timestamps.length - 1]) {
98-
items.push(max);
110+
for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
111+
curr = timestamps[i];
112+
if (curr > min && curr < max) {
113+
items.push(curr);
114+
}
99115
}
100116

117+
items.push(max);
118+
101119
for (i = 0, ilen = items.length; i < ilen; ++i) {
102120
next = items[i + 1];
103121
prev = items[i - 1];
@@ -334,6 +352,15 @@ module.exports = function(Chart) {
334352
var defaultConfig = {
335353
position: 'bottom',
336354

355+
/**
356+
* Data distribution along the scale:
357+
* - 'linear': data are spread according to their time (distances can vary),
358+
* - 'series': data are spread at the same distance from each other.
359+
* @see https://github.com/chartjs/Chart.js/pull/4507
360+
* @since 2.7.0
361+
*/
362+
distribution: 'linear',
363+
337364
time: {
338365
parser: false, // false == a pattern string from http://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment
339366
format: false, // DEPRECATED false == date objects, moment object, callback or a pattern string from http://momentjs.com/docs/#/parsing/string-format/
@@ -359,15 +386,6 @@ module.exports = function(Chart) {
359386
ticks: {
360387
autoSkip: false,
361388

362-
/**
363-
* Ticks distribution along the scale:
364-
* - 'linear': ticks and data are spread according to their time (distances can vary),
365-
* - 'series': ticks and data are spread at the same distance from each other.
366-
* @see https://github.com/chartjs/Chart.js/pull/4507
367-
* @since 2.7.0
368-
*/
369-
mode: 'linear',
370-
371389
/**
372390
* Ticks generation input values:
373391
* - 'auto': generates "optimal" ticks based on scale size and time options.
@@ -430,44 +448,54 @@ module.exports = function(Chart) {
430448
var me = this;
431449
var chart = me.chart;
432450
var options = me.options;
433-
var datasets = chart.data.datasets || [];
434451
var min = parse(options.time.min, me) || MAX_INTEGER;
435452
var max = parse(options.time.max, me) || MIN_INTEGER;
436453
var timestamps = [];
454+
var datasets = [];
437455
var labels = [];
438456
var i, j, ilen, jlen, data, timestamp;
439457

440458
// Convert labels to timestamps
441459
for (i = 0, ilen = chart.data.labels.length; i < ilen; ++i) {
442-
timestamp = parse(chart.data.labels[i], me);
443-
min = Math.min(min, timestamp);
444-
max = Math.max(max, timestamp);
445-
labels.push(timestamp);
460+
labels.push(parse(chart.data.labels[i], me));
446461
}
447462

448463
// Convert data to timestamps
449-
for (i = 0, ilen = datasets.length; i < ilen; ++i) {
464+
for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
450465
if (chart.isDatasetVisible(i)) {
451-
data = datasets[i].data;
466+
data = chart.data.datasets[i].data;
452467

453468
// Let's consider that all data have the same format.
454469
if (helpers.isObject(data[0])) {
455-
timestamps[i] = [];
470+
datasets[i] = [];
456471

457472
for (j = 0, jlen = data.length; j < jlen; ++j) {
458473
timestamp = parse(data[j], me);
459-
min = Math.min(min, timestamp);
460-
max = Math.max(max, timestamp);
461-
timestamps[i][j] = timestamp;
474+
timestamps.push(timestamp);
475+
datasets[i][j] = timestamp;
462476
}
463477
} else {
464-
timestamps[i] = labels.slice(0);
478+
timestamps.push.apply(timestamps, labels);
479+
datasets[i] = labels.slice(0);
465480
}
466481
} else {
467-
timestamps[i] = [];
482+
datasets[i] = [];
468483
}
469484
}
470485

486+
if (labels.length) {
487+
// Sort labels **after** data have been converted
488+
labels = arrayUnique(labels).sort(sorter);
489+
min = Math.min(min, labels[0]);
490+
max = Math.max(max, labels[labels.length - 1]);
491+
}
492+
493+
if (timestamps.length) {
494+
timestamps = arrayUnique(timestamps).sort(sorter);
495+
min = Math.min(min, timestamps[0]);
496+
max = Math.max(max, timestamps[timestamps.length - 1]);
497+
}
498+
471499
// In case there is no valid min/max, let's use today limits
472500
min = min === MAX_INTEGER ? +moment().startOf('day') : min;
473501
max = max === MIN_INTEGER ? +moment().endOf('day') + 1 : max;
@@ -477,36 +505,36 @@ module.exports = function(Chart) {
477505
me.max = Math.max(min + 1, max);
478506

479507
// PRIVATE
480-
me._datasets = timestamps;
481508
me._horizontal = me.isHorizontal();
482-
me._labels = labels.sort(sorter); // Sort labels **after** data have been converted
483509
me._table = [];
510+
me._timestamps = {
511+
data: timestamps,
512+
datasets: datasets,
513+
labels: labels
514+
};
484515
},
485516

486517
buildTicks: function() {
487518
var me = this;
488519
var min = me.min;
489520
var max = me.max;
490-
var timeOpts = me.options.time;
491-
var ticksOpts = me.options.ticks;
521+
var options = me.options;
522+
var timeOpts = options.time;
523+
var ticksOpts = options.ticks;
492524
var formats = timeOpts.displayFormats;
493525
var capacity = me.getLabelCapacity(min);
494526
var unit = timeOpts.unit || determineUnit(timeOpts.minUnit, min, max, capacity);
495527
var majorUnit = determineMajorUnit(unit);
496528
var timestamps = [];
497529
var ticks = [];
498-
var hash = {};
499530
var i, ilen, timestamp;
500531

501532
switch (ticksOpts.source) {
502533
case 'data':
503-
for (i = 0, ilen = me._datasets.length; i < ilen; ++i) {
504-
timestamps.push.apply(timestamps, me._datasets[i]);
505-
}
506-
timestamps.sort(sorter);
534+
timestamps = me._timestamps.data;
507535
break;
508536
case 'labels':
509-
timestamps = me._labels;
537+
timestamps = me._timestamps.labels;
510538
break;
511539
case 'auto':
512540
default:
@@ -522,12 +550,10 @@ module.exports = function(Chart) {
522550
min = parse(timeOpts.min, me) || min;
523551
max = parse(timeOpts.max, me) || max;
524552

525-
// Remove ticks outside the min/max range and duplicated entries
553+
// Remove ticks outside the min/max range
526554
for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
527555
timestamp = timestamps[i];
528-
if (timestamp >= min && timestamp <= max && !hash[timestamp]) {
529-
// hash is used to efficiently detect timestamp duplicates
530-
hash[timestamp] = true;
556+
if (timestamp >= min && timestamp <= max) {
531557
ticks.push(timestamp);
532558
}
533559
}
@@ -540,7 +566,7 @@ module.exports = function(Chart) {
540566
me._majorUnit = majorUnit;
541567
me._minorFormat = formats[unit];
542568
me._majorFormat = formats[majorUnit];
543-
me._table = buildLookupTable(ticks, min, max, ticksOpts.mode === 'linear');
569+
me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution);
544570

545571
return ticksFromTimestamps(ticks, majorUnit);
546572
},
@@ -609,7 +635,7 @@ module.exports = function(Chart) {
609635
var time = null;
610636

611637
if (index !== undefined && datasetIndex !== undefined) {
612-
time = me._datasets[datasetIndex][index];
638+
time = me._timestamps.datasets[datasetIndex][index];
613639
}
614640

615641
if (time === null) {

0 commit comments

Comments
 (0)