]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Generate ticks when source is data (#7044)
authorBen McCann <322311+benmccann@users.noreply.github.com>
Tue, 4 Feb 2020 00:22:09 +0000 (16:22 -0800)
committerGitHub <noreply@github.com>
Tue, 4 Feb 2020 00:22:09 +0000 (19:22 -0500)
Generate ticks when source is data

docs/getting-started/v3-migration.md
src/core/core.interaction.js
src/helpers/helpers.collection.js
src/scales/scale.time.js

index 97728b1c24eb811f9138b586c9bc2662867a1714..07b544fc855d9a5f61b7f98f456de0e4b3631ddd 100644 (file)
@@ -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
index 8927636ec3b49888f82202452d7cbf25ff17496e..ceda10d8c58ba9727b53f32ee47d01573357cf63 100644 (file)
@@ -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) {
index 15535c25c3f0b9137d506bdb0644cbd11c80946e..e8e7c48ff674cfba22f6f8e9f8b61504238b92da 100644 (file)
@@ -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;
index d93b190db99e3737fde26bf529f2fab7449b898b..64fefc4e19efd56910cdb11e8d305bbf33ffe438 100644 (file)
@@ -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) {