From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:39:39 +0000 (-0700) Subject: Remove lookup table from TimeScale (#7532) X-Git-Tag: v3.0.0-beta.2~74 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1c2a03225ed761e75709a9184c184a697e2ce3cf;p=thirdparty%2FChart.js.git 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 --- diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 2f82656de..e5c63913f 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -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); } /** diff --git a/src/scales/scale.timeseries.js b/src/scales/scale.timeseries.js index 009d8dfb1..053e8d099 100644 --- a/src/scales/scale.timeseries.js +++ b/src/scales/scale.timeseries.js @@ -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() { - const me = this; - let timestamps = me._cache.all || []; - - if (timestamps.length) { - return timestamps; - } + constructor(props) { + super(props); - 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; + /** @type {object[]} */ + this._table = []; + } - return timestamps; + initOffsets(timestamps) { + const me = this; + 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';