Remove lookup table from TimeScale (#7532)

* Add getDecimalForValue
* Move interpolate to timeseries scale
* Remove getTimestampsForTable from time scale
* Remove parameters from buildLookupTable
* Restore getValueForPixel
* Remove table from time scale
This commit is contained in:
Ben McCann
2020-06-20 13:39:39 -07:00
committed by GitHub
parent 09a6aea396
commit 1c2a03225e
2 changed files with 133 additions and 122 deletions

View File

@@ -2,7 +2,7 @@ import adapters from '../core/core.adapters';
import {isFinite, isNullOrUndef, mergeIf, valueOrDefault} from '../helpers/helpers.core';
import {toRadians} from '../helpers/helpers.math';
import Scale from '../core/core.scale';
import {_arrayUnique, _filterBetween, _lookup, _lookupByKey} from '../helpers/helpers.collection';
import {_arrayUnique, _filterBetween, _lookup} from '../helpers/helpers.collection';
/**
* @typedef { import("../core/core.adapters").Unit } Unit
@@ -79,31 +79,6 @@ function parse(scale, input) {
return +value;
}
/**
* Linearly interpolates the given source `value` using the table items `skey` values and
* returns the associated `tkey` value. For example, interpolate(table, 'time', 42, 'pos')
* returns the position for a timestamp equal to 42. If value is out of bounds, values at
* index [0, 1] or [n - 1, n] are used for the interpolation.
* @param {object} table
* @param {string} skey
* @param {number} sval
* @param {string} tkey
* @return {object}
*/
function interpolate(table, skey, sval, tkey) {
const {lo, hi} = _lookupByKey(table, skey, sval);
// Note: the lookup table ALWAYS contains at least 2 items (min and max)
const prev = table[lo];
const next = table[hi];
const span = next[skey] - prev[skey];
const ratio = span ? (sval - prev[skey]) / span : 0;
const offset = (next[tkey] - prev[tkey]) * ratio;
return prev[tkey] + offset;
}
/**
* Figures out what unit results in an appropriate number of auto-generated ticks
* @param {Unit} minUnit
@@ -173,41 +148,6 @@ function addTick(timestamps, ticks, time) {
ticks[timestamp] = true;
}
/**
* Returns the start and end offsets from edges in the form of {start, end}
* where each value is a relative width to the scale and ranges between 0 and 1.
* They add extra margins on the both sides by scaling down the original scale.
* Offsets are added when the `offset` option is true.
* @param {object} table
* @param {number[]} timestamps
* @param {number} min
* @param {number} max
* @param {object} options
* @return {object}
*/
function computeOffsets(table, timestamps, min, max, options) {
let start = 0;
let end = 0;
let first, last;
if (options.offset && timestamps.length) {
first = interpolate(table, 'time', timestamps[0], 'pos');
if (timestamps.length === 1) {
start = 1 - first;
} else {
start = (interpolate(table, 'time', timestamps[1], 'pos') - first) / 2;
}
last = interpolate(table, 'time', timestamps[timestamps.length - 1], 'pos');
if (timestamps.length === 1) {
end = last;
} else {
end = (last - interpolate(table, 'time', timestamps[timestamps.length - 2], 'pos')) / 2;
}
}
return {start, end, factor: 1 / (start + 1 + end)};
}
/**
* @param {TimeScale} scale
* @param {object[]} ticks
@@ -318,8 +258,6 @@ class TimeScale extends Scale {
this._majorUnit = undefined;
/** @type {object} */
this._offsets = {};
/** @type {object[]} */
this._table = [];
}
init(options) {
@@ -355,13 +293,6 @@ class TimeScale extends Scale {
};
}
/**
* @protected
*/
getTimestampsForTable() {
return [this.min, this.max];
}
determineDataLimits() {
const me = this;
const options = me.options;
@@ -397,7 +328,7 @@ class TimeScale extends Scale {
min = isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit);
max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1;
// Make sure that max is strictly higher than min (required by the lookup table)
// Make sure that max is strictly higher than min (required by the timeseries lookup table)
me.min = Math.min(min, max);
me.max = Math.max(min + 1, max);
}
@@ -445,8 +376,7 @@ class TimeScale extends Scale {
: determineUnitForFormatting(me, ticks.length, timeOpts.minUnit, me.min, me.max));
me._majorUnit = !tickOpts.major.enabled || me._unit === 'year' ? undefined
: determineMajorUnit(me._unit);
me._table = me.buildLookupTable(me.getTimestampsForTable(), min, max);
me._offsets = computeOffsets(me._table, timestamps, min, max, options);
me.initOffsets(timestamps);
if (options.reverse) {
ticks.reverse();
@@ -455,6 +385,39 @@ class TimeScale extends Scale {
return ticksFromTimestamps(me, ticks, me._majorUnit);
}
/**
* Returns the start and end offsets from edges in the form of {start, end}
* where each value is a relative width to the scale and ranges between 0 and 1.
* They add extra margins on the both sides by scaling down the original scale.
* Offsets are added when the `offset` option is true.
* @param {number[]} timestamps
* @return {object}
* @protected
*/
initOffsets(timestamps) {
const me = this;
let start = 0;
let end = 0;
let first, last;
if (me.options.offset && timestamps.length) {
first = me.getDecimalForValue(timestamps[0]);
if (timestamps.length === 1) {
start = 1 - first;
} else {
start = (me.getDecimalForValue(timestamps[1]) - first) / 2;
}
last = me.getDecimalForValue(timestamps[timestamps.length - 1]);
if (timestamps.length === 1) {
end = last;
} else {
end = (last - me.getDecimalForValue(timestamps[timestamps.length - 2])) / 2;
}
}
me._offsets = {start, end, factor: 1 / (start + 1 + end)};
}
/**
* Generates a maximum of `capacity` timestamps between min and max, rounded to the
* `minor` unit using the given scale time `options`.
@@ -514,27 +477,6 @@ class TimeScale extends Scale {
return Object.keys(ticks).map(x => +x);
}
/**
* Returns an array of {time, pos} objects used to interpolate a specific `time` or position
* (`pos`) on the scale, by searching entries before and after the requested value. `pos` is
* a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other
* extremity (left + width or top + height). Note that it would be more optimized to directly
* store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need
* to create the lookup table. The table ALWAYS contains at least two items: min and max.
*
* @param {number[]} timestamps - timestamps sorted from lowest to highest.
* @param {number} min
* @param {number} max
* @return {object[]}
* @protected
*/
buildLookupTable(timestamps, min, max) {
return [
{time: min, pos: 0},
{time: max, pos: 1}
];
}
/**
* @param {number} value
* @return {string}
@@ -586,6 +528,15 @@ class TimeScale extends Scale {
}
}
/**
* @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)
* @return {number}
*/
getDecimalForValue(value) {
const me = this;
return (value - me.min) / (me.max - me.min);
}
/**
* @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)
* @return {number}
@@ -593,7 +544,7 @@ class TimeScale extends Scale {
getPixelForValue(value) {
const me = this;
const offsets = me._offsets;
const pos = interpolate(me._table, 'time', value, 'pos');
const pos = me.getDecimalForValue(value);
return me.getPixelForDecimal((offsets.start + pos) * offsets.factor);
}
@@ -605,7 +556,7 @@ class TimeScale extends Scale {
const me = this;
const offsets = me._offsets;
const pos = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
return interpolate(me._table, 'pos', pos, 'time');
return me.min + pos * (me.max - me.min);
}
/**

View File

@@ -1,5 +1,30 @@
import TimeScale from './scale.time';
import {_arrayUnique} from '../helpers/helpers.collection';
import {_arrayUnique, _lookupByKey} from '../helpers/helpers.collection';
/**
* Linearly interpolates the given source `value` using the table items `skey` values and
* returns the associated `tkey` value. For example, interpolate(table, 'time', 42, 'pos')
* returns the position for a timestamp equal to 42. If value is out of bounds, values at
* index [0, 1] or [n - 1, n] are used for the interpolation.
* @param {object} table
* @param {string} skey
* @param {number} sval
* @param {string} tkey
* @return {object}
*/
function interpolate(table, skey, sval, tkey) {
const {lo, hi} = _lookupByKey(table, skey, sval);
// Note: the lookup table ALWAYS contains at least 2 items (min and max)
const prev = table[lo];
const next = table[hi];
const span = next[skey] - prev[skey];
const ratio = span ? (sval - prev[skey]) / span : 0;
const offset = (next[tkey] - prev[tkey]) * ratio;
return prev[tkey] + offset;
}
/**
* @param {number} a
@@ -12,29 +37,19 @@ function sorter(a, b) {
class TimeSeriesScale extends TimeScale {
/**
* Returns all timestamps
* @protected
* @param {object} props
*/
getTimestampsForTable() {
constructor(props) {
super(props);
/** @type {object[]} */
this._table = [];
}
initOffsets(timestamps) {
const me = this;
let timestamps = me._cache.all || [];
if (timestamps.length) {
return timestamps;
}
const data = me.getDataTimestamps();
const label = me.getLabelTimestamps();
if (data.length && label.length) {
// If combining labels and data (data might not contain all labels),
// we need to recheck uniqueness and sort
timestamps = _arrayUnique(data.concat(label).sort(sorter));
} else {
timestamps = data.length ? data : label;
}
timestamps = me._cache.all = timestamps;
return timestamps;
me._table = me.buildLookupTable();
super.initOffsets(timestamps);
}
/**
@@ -45,13 +60,13 @@ class TimeSeriesScale extends TimeScale {
* store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need
* to create the lookup table. The table ALWAYS contains at least two items: min and max.
*
* @param {number[]} timestamps - timestamps sorted from lowest to highest.
* @param {number} min
* @param {number} max
* @return {object[]}
* @protected
*/
buildLookupTable(timestamps, min, max) {
buildLookupTable() {
const me = this;
const {min, max} = me;
const timestamps = me._getTimestampsForTable();
if (!timestamps.length) {
return [
{time: min, pos: 0},
@@ -86,6 +101,40 @@ class TimeSeriesScale extends TimeScale {
return table;
}
/**
* Returns all timestamps
* @private
*/
_getTimestampsForTable() {
const me = this;
let timestamps = me._cache.all || [];
if (timestamps.length) {
return timestamps;
}
const data = me.getDataTimestamps();
const label = me.getLabelTimestamps();
if (data.length && label.length) {
// If combining labels and data (data might not contain all labels),
// we need to recheck uniqueness and sort
timestamps = _arrayUnique(data.concat(label).sort(sorter));
} else {
timestamps = data.length ? data : label;
}
timestamps = me._cache.all = timestamps;
return timestamps;
}
/**
* @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)
* @return {number}
*/
getDecimalForValue(value) {
return interpolate(this._table, 'time', value, 'pos');
}
/**
* @protected
*/
@@ -121,6 +170,17 @@ class TimeSeriesScale extends TimeScale {
// We could assume labels are in order and unique - but let's not
return (me._cache.labels = timestamps);
}
/**
* @param {number} pixel
* @return {number}
*/
getValueForPixel(pixel) {
const me = this;
const offsets = me._offsets;
const pos = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
return interpolate(me._table, 'pos', pos, 'time');
}
}
TimeSeriesScale.id = 'timeseries';