From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 4 Feb 2020 00:22:09 +0000 (-0800) Subject: Generate ticks when source is data (#7044) X-Git-Tag: v3.0.0-alpha~78 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3b61a1f86e37e08c9c00be96a55598c41010dc56;p=thirdparty%2FChart.js.git Generate ticks when source is data (#7044) Generate ticks when source is data --- diff --git a/docs/getting-started/v3-migration.md b/docs/getting-started/v3-migration.md index 97728b1c2..07b544fc8 100644 --- a/docs/getting-started/v3-migration.md +++ b/docs/getting-started/v3-migration.md @@ -60,10 +60,11 @@ Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released * `scales.[x/y]Axes.ticks.reverse` was renamed to `scales[id].reverse` * `scales.[x/y]Axes.ticks.suggestedMax` was renamed to `scales[id].suggestedMax` * `scales.[x/y]Axes.ticks.suggestedMin` was renamed to `scales[id].suggestedMin` +* `scales.[x/y]Axes.ticks.unitStepSize` was removed. Use `scales[id].ticks.stepSize` * `scales.[x/y]Axes.time.format` was renamed to `scales[id].time.parser` * `scales.[x/y]Axes.time.max` was renamed to `scales[id].max` * `scales.[x/y]Axes.time.min` was renamed to `scales[id].min` -* The dataset option `tension` was renamed to `lineTension` +* The dataset option `tension` was removed. Use `lineTension` * To override the platform class used in a chart instance, pass `platform: PlatformClass` in the config object. Note that the class should be passed, not an instance of the class. ### Animations diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js index 8927636ec..ceda10d8c 100644 --- a/src/core/core.interaction.js +++ b/src/core/core.interaction.js @@ -2,7 +2,7 @@ import helpers from '../helpers/index'; import {_isPointInArea} from '../helpers/helpers.canvas'; -import {_lookup, _rlookup} from '../helpers/helpers.collection'; +import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection'; /** * Helper function to get relative position for an event @@ -53,7 +53,7 @@ function binarySearch(metaset, axis, value, intersect) { const {controller, data, _sorted} = metaset; const iScale = controller._cachedMeta.iScale; if (iScale && axis === iScale.axis && _sorted && data.length) { - const lookupMethod = iScale._reversePixels ? _rlookup : _lookup; + const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey; if (!intersect) { return lookupMethod(data, axis, value); } else if (controller._sharedOptions) { diff --git a/src/helpers/helpers.collection.js b/src/helpers/helpers.collection.js index 15535c25c..e8e7c48ff 100644 --- a/src/helpers/helpers.collection.js +++ b/src/helpers/helpers.collection.js @@ -1,5 +1,28 @@ 'use strict'; +/** + * Binary search + * @param {array} table - the table search. must be sorted! + * @param {number} value - value to find + * @private + */ +export function _lookup(table, value) { + let hi = table.length - 1; + let lo = 0; + let mid; + + while (hi - lo > 1) { + mid = (lo + hi) >> 1; + if (table[mid] < value) { + lo = mid; + } else { + hi = mid; + } + } + + return {lo, hi}; +} + /** * Binary search * @param {array} table - the table search. must be sorted! @@ -7,7 +30,7 @@ * @param {number} value - value to find * @private */ -export function _lookup(table, key, value) { +export function _lookupByKey(table, key, value) { let hi = table.length - 1; let lo = 0; let mid; @@ -31,7 +54,7 @@ export function _lookup(table, key, value) { * @param {number} value - value to find * @private */ -export function _rlookup(table, key, value) { +export function _rlookupByKey(table, key, value) { let hi = table.length - 1; let lo = 0; let mid; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index d93b190db..64fefc4e1 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -6,7 +6,7 @@ import {isFinite, isNullOrUndef, mergeIf, valueOrDefault} from '../helpers/helpe import {toRadians} from '../helpers/helpers.math'; import {resolve} from '../helpers/helpers.options'; import Scale from '../core/core.scale'; -import {_lookup} from '../helpers/helpers.collection'; +import {_lookup, _lookupByKey} from '../helpers/helpers.collection'; // Integer constants are from the ES6 spec. const MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; @@ -79,6 +79,102 @@ function arrayUnique(items) { return [...set]; } +function parse(scale, input) { + if (isNullOrUndef(input)) { + return null; + } + + const adapter = scale._adapter; + const options = scale.options.time; + const parser = options.parser; + let value = input; + + if (typeof parser === 'function') { + value = parser(value); + } + + // Only parse if its not a timestamp already + if (!isFinite(value)) { + value = typeof parser === 'string' + ? adapter.parse(value, parser) + : adapter.parse(value); + } + + if (value === null) { + return value; + } + + if (options.round) { + value = scale._adapter.startOf(value, options.round); + } + + return +value; +} + +function getDataTimestamps(scale) { + const isSeries = scale.options.distribution === 'series'; + let timestamps = scale._cache.data || []; + let i, ilen, metas; + + if (timestamps.length) { + return timestamps; + } + + metas = scale._getMatchingVisibleMetas(); + + if (isSeries && metas.length) { + return metas[0].controller._getAllParsedValues(scale); + } + + for (i = 0, ilen = metas.length; i < ilen; ++i) { + timestamps = timestamps.concat(metas[i].controller._getAllParsedValues(scale)); + } + + // We can not assume data is in order or unique - not even for single dataset + // It seems to be somewhat faster to do sorting first + return (scale._cache.data = arrayUnique(timestamps.sort(sorter))); +} + +function getLabelTimestamps(scale) { + const isSeries = scale.options.distribution === 'series'; + const timestamps = scale._cache.labels || []; + let i, ilen, labels; + + if (timestamps.length) { + return timestamps; + } + + labels = scale._getLabels(); + for (i = 0, ilen = labels.length; i < ilen; ++i) { + timestamps.push(parse(scale, labels[i])); + } + + // We could assume labels are in order and unique - but let's not + return (scale._cache.labels = isSeries ? timestamps : arrayUnique(timestamps.sort(sorter))); +} + +function getAllTimestamps(scale) { + let timestamps = scale._cache.all || []; + let label, data; + + if (timestamps.length) { + return timestamps; + } + + data = getDataTimestamps(scale); + label = getLabelTimestamps(scale); + 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 = scale._cache.all = timestamps; + + return timestamps; +} + /** * 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 @@ -136,7 +232,7 @@ function buildLookupTable(timestamps, min, max, distribution) { * index [0, 1] or [n - 1, n] are used for the interpolation. */ function interpolate(table, skey, sval, tkey) { - const {lo, hi} = _lookup(table, skey, sval); + const {lo, hi} = _lookupByKey(table, skey, sval); // Note: the lookup table ALWAYS contains at least 2 items (min and max) const prev = table[lo]; @@ -149,38 +245,6 @@ function interpolate(table, skey, sval, tkey) { return prev[tkey] + offset; } -function parse(scale, input) { - if (isNullOrUndef(input)) { - return null; - } - - const adapter = scale._adapter; - const options = scale.options.time; - const parser = options.parser; - let value = input; - - if (typeof parser === 'function') { - value = parser(value); - } - - // Only parse if its not a timestamp already - if (!isFinite(value)) { - value = typeof parser === 'string' - ? adapter.parse(value, parser) - : adapter.parse(value); - } - - if (value === null) { - return value; - } - - if (options.round) { - value = scale._adapter.startOf(value, options.round); - } - - return +value; -} - /** * Figures out what unit results in an appropriate number of auto-generated ticks */ @@ -224,6 +288,15 @@ function determineMajorUnit(unit) { } } +function addTick(timestamps, ticks, time) { + if (!timestamps.length) { + return; + } + const {lo, hi} = _lookup(timestamps, time); + const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; + ticks.add(timestamp); +} + /** * Generates a maximum of `capacity` timestamps between min and max, rounded to the * `minor` unit using the given scale time `options`. @@ -237,9 +310,9 @@ function generate(scale) { const options = scale.options; const timeOpts = options.time; const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, scale._getLabelCapacity(min)); - const stepSize = resolve([timeOpts.stepSize, timeOpts.unitStepSize, 1]); + const stepSize = valueOrDefault(timeOpts.stepSize, 1); const weekday = minor === 'week' ? timeOpts.isoWeekday : false; - const ticks = []; + const ticks = new Set(); let first = min; let time; @@ -256,15 +329,28 @@ function generate(scale) { throw min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor; } - for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) { - ticks.push(time); - } + if (scale.options.ticks.source === 'data') { + // need to make sure ticks are in data in this case + const timestamps = getDataTimestamps(scale); + + for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) { + addTick(timestamps, ticks, time); + } + + if (time === max || options.bounds === 'ticks') { + addTick(timestamps, ticks, time); + } + } else { + for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) { + ticks.add(time); + } - if (time === max || options.bounds === 'ticks') { - ticks.push(time); + if (time === max || options.bounds === 'ticks') { + ticks.add(time); + } } - return ticks; + return [...ticks]; } /** @@ -332,80 +418,11 @@ function ticksFromTimestamps(scale, values, majorUnit) { return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); } -function getDataTimestamps(scale) { - const isSeries = scale.options.distribution === 'series'; - let timestamps = scale._cache.data || []; - let i, ilen, metas; - - if (timestamps.length) { - return timestamps; - } - - metas = scale._getMatchingVisibleMetas(); - - if (isSeries && metas.length) { - return metas[0].controller._getAllParsedValues(scale); - } - - for (i = 0, ilen = metas.length; i < ilen; ++i) { - timestamps = timestamps.concat(metas[i].controller._getAllParsedValues(scale)); - } - - // We can not assume data is in order or unique - not even for single dataset - // It seems to be somewhat faster to do sorting first - return (scale._cache.data = arrayUnique(timestamps.sort(sorter))); -} - -function getLabelTimestamps(scale) { - const isSeries = scale.options.distribution === 'series'; - const timestamps = scale._cache.labels || []; - let i, ilen, labels; - - if (timestamps.length) { - return timestamps; - } - - labels = scale._getLabels(); - for (i = 0, ilen = labels.length; i < ilen; ++i) { - timestamps.push(parse(scale, labels[i])); - } - - // We could assume labels are in order and unique - but let's not - return (scale._cache.labels = isSeries ? timestamps : arrayUnique(timestamps.sort(sorter))); -} - -function getAllTimestamps(scale) { - let timestamps = scale._cache.all || []; - let label, data; - - if (timestamps.length) { - return timestamps; - } - - data = getDataTimestamps(scale); - label = getLabelTimestamps(scale); - 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 = scale._cache.all = timestamps; - - return timestamps; -} - - function getTimestampsForTicks(scale) { - const options = scale.options; - const source = options.ticks.source; - - if (source === 'data' || (source === 'auto' && options.distribution === 'series')) { - return getAllTimestamps(scale); - } else if (source === 'labels') { + if (scale.options.ticks.source === 'labels') { return getLabelTimestamps(scale); } + return generate(scale); } @@ -581,7 +598,6 @@ class TimeScale extends Scale { const timeOpts = options.time; const tickOpts = options.ticks; const distribution = options.distribution; - const timestamps = getTimestampsForTicks(me); if (options.bounds === 'ticks' && timestamps.length) {