diff --git a/src/scales/scale.timeseries.js b/src/scales/scale.timeseries.js index f8228fe3431..e42d87817a5 100644 --- a/src/scales/scale.timeseries.js +++ b/src/scales/scale.timeseries.js @@ -1,30 +1,30 @@ import TimeScale from './scale.time'; -import {_lookup} from '../helpers/helpers.collection'; -import {isNullOrUndef} from '../helpers/helpers.core'; +import {_lookupByKey} from '../helpers/helpers.collection'; /** * Linearly interpolates the given source `val` using the table. If value is out of bounds, values - * at index [0, 1] or [n - 1, n] are used for the interpolation. + * at edges are used for the interpolation. * @param {object} table * @param {number} val * @param {boolean} [reverse] lookup time based on position instead of vice versa * @return {object} */ function interpolate(table, val, reverse) { + let lo = 0; + let hi = table.length - 1; let prevSource, nextSource, prevTarget, nextTarget; - - // Note: the lookup table ALWAYS contains at least 2 items (min and max) if (reverse) { - prevSource = Math.floor(val); - nextSource = Math.ceil(val); - prevTarget = table[prevSource]; - nextTarget = table[nextSource]; + if (val >= table[lo].pos && val <= table[hi].pos) { + ({lo, hi} = _lookupByKey(table, 'pos', val)); + } + ({pos: prevSource, time: prevTarget} = table[lo]); + ({pos: nextSource, time: nextTarget} = table[hi]); } else { - const result = _lookup(table, val); - prevTarget = result.lo; - nextTarget = result.hi; - prevSource = table[prevTarget]; - nextSource = table[nextTarget]; + if (val >= table[lo].time && val <= table[hi].time) { + ({lo, hi} = _lookupByKey(table, 'time', val)); + } + ({time: prevSource, pos: prevTarget} = table[lo]); + ({time: nextSource, pos: nextTarget} = table[hi]); } const span = nextSource - prevSource; @@ -42,7 +42,9 @@ class TimeSeriesScale extends TimeScale { /** @type {object[]} */ this._table = []; /** @type {number} */ - this._maxIndex = undefined; + this._minPos = undefined; + /** @type {number} */ + this._tableRange = undefined; } /** @@ -51,8 +53,9 @@ class TimeSeriesScale extends TimeScale { initOffsets() { const me = this; const timestamps = me._getTimestampsForTable(); - me._table = me.buildLookupTable(timestamps); - me._maxIndex = me._table.length - 1; + const table = me._table = me.buildLookupTable(timestamps); + me._minPos = interpolate(table, me.min); + me._tableRange = interpolate(table, me.max) - me._minPos; super.initOffsets(timestamps); } @@ -68,28 +71,37 @@ class TimeSeriesScale extends TimeScale { * @protected */ buildLookupTable(timestamps) { - const me = this; - const {min, max} = me; - if (!timestamps.length) { + const {min, max} = this; + const items = []; + const table = []; + let i, ilen, prev, curr, next; + + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = timestamps[i]; + if (curr >= min && curr <= max) { + items.push(curr); + } + } + + if (items.length < 2) { + // In case there is less that 2 timestamps between min and max, the scale is defined by min and max return [ {time: min, pos: 0}, {time: max, pos: 1} ]; } - const items = [min]; - let i, ilen, curr; + for (i = 0, ilen = items.length; i < ilen; ++i) { + next = items[i + 1]; + prev = items[i - 1]; + curr = items[i]; - for (i = 0, ilen = timestamps.length; i < ilen; ++i) { - curr = timestamps[i]; - if (curr > min && curr < max) { - items.push(curr); + // only add points that breaks the scale linearity + if (Math.round((next + prev) / 2) !== curr) { + table.push({time: curr, pos: i / (ilen - 1)}); } } - - items.push(max); - - return items; + return table; } /** @@ -119,25 +131,12 @@ class TimeSeriesScale extends TimeScale { return timestamps; } - /** - * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) - * @param {number} [index] - * @return {number} - */ - getPixelForValue(value, index) { - const me = this; - const offsets = me._offsets; - const pos = me._normalized && me._maxIndex > 0 && !isNullOrUndef(index) - ? index / me._maxIndex : me.getDecimalForValue(value); - return me.getPixelForDecimal((offsets.start + pos) * offsets.factor); - } - /** * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) * @return {number} */ getDecimalForValue(value) { - return interpolate(this._table, value) / this._maxIndex; + return (interpolate(this._table, value) - this._minPos) / this._tableRange; } /** @@ -148,7 +147,7 @@ class TimeSeriesScale extends TimeScale { const me = this; const offsets = me._offsets; const decimal = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end; - return interpolate(me._table, decimal * this._maxIndex, true); + return interpolate(me._table, decimal * me._tableRange + me._minPos, true); } } diff --git a/test/fixtures/scale.timeseries/source-data-offset-min-max.png b/test/fixtures/scale.timeseries/source-data-offset-min-max.png index a2374719329..824bf36bd51 100644 Binary files a/test/fixtures/scale.timeseries/source-data-offset-min-max.png and b/test/fixtures/scale.timeseries/source-data-offset-min-max.png differ diff --git a/test/fixtures/scale.timeseries/source-labels-offset-min-max.png b/test/fixtures/scale.timeseries/source-labels-offset-min-max.png index a2374719329..824bf36bd51 100644 Binary files a/test/fixtures/scale.timeseries/source-labels-offset-min-max.png and b/test/fixtures/scale.timeseries/source-labels-offset-min-max.png differ diff --git a/test/fixtures/scale.timeseries/ticks-reverse-max.png b/test/fixtures/scale.timeseries/ticks-reverse-max.png index 1189549dbf5..9887ed88db3 100644 Binary files a/test/fixtures/scale.timeseries/ticks-reverse-max.png and b/test/fixtures/scale.timeseries/ticks-reverse-max.png differ diff --git a/test/fixtures/scale.timeseries/ticks-reverse-min-max.png b/test/fixtures/scale.timeseries/ticks-reverse-min-max.png index f6c66ec676f..dc7e79882f8 100644 Binary files a/test/fixtures/scale.timeseries/ticks-reverse-min-max.png and b/test/fixtures/scale.timeseries/ticks-reverse-min-max.png differ diff --git a/test/fixtures/scale.timeseries/ticks-reverse-min.png b/test/fixtures/scale.timeseries/ticks-reverse-min.png index 97315b374c5..5a30d3f0260 100644 Binary files a/test/fixtures/scale.timeseries/ticks-reverse-min.png and b/test/fixtures/scale.timeseries/ticks-reverse-min.png differ diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js index e9fccdf8faf..4113d8a497e 100644 --- a/test/specs/scale.time.tests.js +++ b/test/specs/scale.time.tests.js @@ -764,7 +764,7 @@ describe('Time scale tests', function() { var start = scale.left; var slice = scale.width / 5; - expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(start + slice); + expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(86); expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(start + slice * 5); }); it ('should add a step after if scale.max is after the last data', function() { @@ -776,10 +776,9 @@ describe('Time scale tests', function() { chart.update(); var start = scale.left; - var slice = scale.width / 5; expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start); - expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * 4); + expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(388); }); it ('should add steps before and after if scale.min/max are outside the data range', function() { var chart = this.chart; @@ -790,11 +789,8 @@ describe('Time scale tests', function() { options.max = '2050'; chart.update(); - var start = scale.left; - var slice = scale.width / 6; - - expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(start + slice); - expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(start + slice * 5); + expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(71); + expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(401); }); }); describe('is "time"', function() {