Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeseries: support panning #9345

Merged
merged 4 commits into from Jul 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 44 additions & 45 deletions 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;
Expand All @@ -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;
}

/**
Expand All @@ -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);
}

Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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);
}
}

Expand Down
Binary file modified test/fixtures/scale.timeseries/source-data-offset-min-max.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/scale.timeseries/source-labels-offset-min-max.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/scale.timeseries/ticks-reverse-max.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/scale.timeseries/ticks-reverse-min-max.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/fixtures/scale.timeseries/ticks-reverse-min.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 4 additions & 8 deletions test/specs/scale.time.tests.js
Expand Up @@ -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() {
Expand All @@ -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;
Expand All @@ -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() {
Expand Down