]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Use binary search for interpolations (#6958)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 21 Jan 2020 23:31:17 +0000 (01:31 +0200)
committerEvert Timberg <evert.timberg+github@gmail.com>
Tue, 21 Jan 2020 23:31:17 +0000 (18:31 -0500)
src/core/core.datasetController.js
src/core/core.interaction.js
src/elements/element.point.js
src/elements/element.rectangle.js
src/helpers/helpers.collection.js [new file with mode: 0644]
src/scales/scale.time.js

index b0a63f02f483cddc7fceee202232c469d306e591..2e1b31fb43400d6a7f003c6f5a28b27c419f60e3 100644 (file)
@@ -980,6 +980,10 @@ helpers.extend(DatasetController.prototype, {
         * @private
         */
        _getSharedOptions: function(mode, el, options) {
+               if (!mode) {
+                       // store element option sharing status for usage in interactions
+                       this._sharedOptions = options && options.$shared;
+               }
                if (mode !== 'reset' && options && options.$shared && el && el.options && el.options.$shared) {
                        return {target: el.options, options};
                }
index 3e130e02fd6c5c354c29ccc8c995ac6c48154770..77780e0d8d2c1e026ed99ddffca0d8abfcebb568 100644 (file)
@@ -1,8 +1,8 @@
 'use strict';
 
 import helpers from '../helpers/index';
-import {isNumber} from '../helpers/helpers.math';
 import {_isPointInArea} from '../helpers/helpers.canvas';
+import {_lookup, _rlookup} from '../helpers/helpers.collection';
 
 /**
  * Helper function to get relative position for an event
@@ -42,38 +42,58 @@ function evaluateAllVisibleItems(chart, handler) {
 }
 
 /**
- * Helper function to check the items at the hovered index on the index scale
+ * Helper function to do binary search when possible
+ * @param {object} metaset - the dataset meta
+ * @param {string} axis - the axis mide. x|y|xy
+ * @param {number} value - the value to find
+ * @param {boolean} intersect - should the element intersect
+ * @returns {lo, hi} indices to search data array between
+ */
+function binarySearch(metaset, axis, value, intersect) {
+       const {controller, data, _sorted} = metaset;
+       const iScale = controller._cachedMeta.iScale;
+       if (iScale && axis === iScale.axis && _sorted) {
+               const lookupMethod = iScale._reversePixels ? _rlookup : _lookup;
+               if (!intersect) {
+                       return lookupMethod(data, axis, value);
+               } else if (controller._sharedOptions) {
+                       // _sharedOptions indicates that each element has equal options -> equal proportions
+                       // So we can do a ranged binary search based on the range of first element and
+                       // be confident to get the full range of indices that can intersect with the value.
+                       const el = data[0];
+                       const range = typeof el.getRange === 'function' && el.getRange(axis);
+                       if (range) {
+                               const start = lookupMethod(data, axis, value - range);
+                               const end = lookupMethod(data, axis, value + range);
+                               return {lo: start.lo, hi: end.hi};
+                       }
+               }
+       }
+       // Default to all elements, when binary search can not be used.
+       return {lo: 0, hi: data.length - 1};
+}
+
+/**
+ * Helper function to get items using binary search, when the data is sorted.
  * @param {Chart} chart - the chart
  * @param {string} axis - the axis mode. x|y|xy
  * @param {object} position - the point to be nearest to
  * @param {function} handler - the callback to execute for each visible item
- * @return whether all scales were of a suitable type
+ * @param {boolean} intersect - consider intersecting items
  */
-function evaluateItemsAtIndex(chart, axis, position, handler) {
+function optimizedEvaluateItems(chart, axis, position, handler, intersect) {
        const metasets = chart._getSortedVisibleDatasetMetas();
-       const indices = [];
-       for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
-               const metaset = metasets[i];
-               const iScale = metaset.controller._cachedMeta.iScale;
-               if (!iScale || axis !== iScale.axis || !iScale.getIndexForPixel) {
-                       return false;
-               }
-               const index = iScale.getIndexForPixel(position[axis]);
-               if (!isNumber(index)) {
-                       return false;
-               }
-               indices.push(index);
-       }
-       // do this only after checking whether all scales are of a suitable type
+       const value = position[axis];
        for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
-               const metaset = metasets[i];
-               const index = indices[i];
-               const element = metaset.data[index];
-               if (!element.skip) {
-                       handler(element, metaset.index, index);
+               const {index, data} = metasets[i];
+               let {lo, hi} = binarySearch(metasets[i], axis, value, intersect);
+               for (let j = lo; j <= hi; ++j) {
+                       const element = data[j];
+                       if (!element.skip) {
+                               handler(element, index, j);
+                       }
                }
        }
-       return true;
 }
 
 /**
@@ -112,12 +132,7 @@ function getIntersectItems(chart, position, axis) {
                }
        };
 
-       const optimized = evaluateItemsAtIndex(chart, axis, position, evaluationFunc);
-       if (optimized) {
-               return items;
-       }
-
-       evaluateAllVisibleItems(chart, evaluationFunc);
+       optimizedEvaluateItems(chart, axis, position, evaluationFunc, true);
        return items;
 }
 
@@ -154,12 +169,7 @@ function getNearestItems(chart, position, axis, intersect) {
                }
        };
 
-       const optimized = evaluateItemsAtIndex(chart, axis, position, evaluationFunc);
-       if (optimized) {
-               return items;
-       }
-
-       evaluateAllVisibleItems(chart, evaluationFunc);
+       optimizedEvaluateItems(chart, axis, position, evaluationFunc);
        return items;
 }
 
index 25b5e2d1981f13d7d2a9e001f3a1dd26ba7fb518..2e9275f41f401f16f5fe5a4ed9bd5679340e16b9 100644 (file)
@@ -77,6 +77,11 @@ class Point extends Element {
                        helpers.canvas.drawPoint(ctx, options, me.x, me.y);
                }
        }
+
+       getRange() {
+               const options = this.options || {};
+               return options.radius + options.hitRadius;
+       }
 }
 
 Point.prototype._type = 'point';
index 85d957a5220bdfa8fb771558cf742c0fd4b0146c..239dd9ebb0e40338155a3a7520f9a32aa4ecb5fb 100644 (file)
@@ -181,6 +181,10 @@ class Rectangle extends Element {
                        y: this.y
                };
        }
+
+       getRange(axis) {
+               return axis === 'x' ? this.width / 2 : this.height / 2;
+       }
 }
 
 Rectangle.prototype._type = 'rectangle';
diff --git a/src/helpers/helpers.collection.js b/src/helpers/helpers.collection.js
new file mode 100644 (file)
index 0000000..15535c2
--- /dev/null
@@ -0,0 +1,49 @@
+'use strict';
+
+/**
+ * Binary search
+ * @param {array} table - the table search. must be sorted!
+ * @param {string} key - property name for the value in each entry
+ * @param {number} value - value to find
+ * @private
+ */
+export function _lookup(table, key, value) {
+       let hi = table.length - 1;
+       let lo = 0;
+       let mid;
+
+       while (hi - lo > 1) {
+               mid = (lo + hi) >> 1;
+               if (table[mid][key] < value) {
+                       lo = mid;
+               } else {
+                       hi = mid;
+               }
+       }
+
+       return {lo, hi};
+}
+
+/**
+ * Reverse binary search
+ * @param {array} table - the table search. must be sorted!
+ * @param {string} key - property name for the value in each entry
+ * @param {number} value - value to find
+ * @private
+ */
+export function _rlookup(table, key, value) {
+       let hi = table.length - 1;
+       let lo = 0;
+       let mid;
+
+       while (hi - lo > 1) {
+               mid = (lo + hi) >> 1;
+               if (table[mid][key] < value) {
+                       hi = mid;
+               } else {
+                       lo = mid;
+               }
+       }
+
+       return {lo, hi};
+}
index 6d28e496e5fcddb196e3185f6a4e228bacff1f75..2bd00c6a12bf3e2b746c3cd556970a8950983414 100644 (file)
@@ -5,6 +5,7 @@ import defaults from '../core/core.defaults';
 import helpers from '../helpers/index';
 import {toRadians} from '../helpers/helpers.math';
 import Scale from '../core/core.scale';
+import {_lookup} from '../helpers/helpers.collection';
 
 const resolve = helpers.options.resolve;
 const valueOrDefault = helpers.valueOrDefault;
@@ -130,33 +131,6 @@ function buildLookupTable(timestamps, min, max, distribution) {
        return table;
 }
 
-// @see adapted from https://www.anujgakhar.com/2014/03/01/binary-search-in-javascript/
-function lookup(table, key, value) {
-       let lo = 0;
-       let hi = table.length - 1;
-       let mid, i0, i1;
-
-       while (lo >= 0 && lo <= hi) {
-               mid = (lo + hi) >> 1;
-               i0 = mid > 0 && table[mid - 1] || null;
-               i1 = table[mid];
-
-               if (!i0) {
-                       // given value is outside table (before first item)
-                       return {lo: null, hi: i1};
-               } else if (i1[key] < value) {
-                       lo = mid + 1;
-               } else if (i0[key] > value) {
-                       hi = mid - 1;
-               } else {
-                       return {lo: i0, hi: i1};
-               }
-       }
-
-       // given value is outside table (after last item)
-       return {lo: i1, hi: null};
-}
-
 /**
  * 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')
@@ -164,11 +138,11 @@ function lookup(table, key, value) {
  * index [0, 1] or [n - 1, n] are used for the interpolation.
  */
 function interpolate(table, skey, sval, tkey) {
-       const range = lookup(table, skey, sval);
+       const {lo, hi} = _lookup(table, skey, sval);
 
        // Note: the lookup table ALWAYS contains at least 2 items (min and max)
-       const prev = !range.lo ? table[0] : !range.hi ? table[table.length - 2] : range.lo;
-       const next = !range.lo ? table[1] : !range.hi ? table[table.length - 1] : range.hi;
+       const prev = table[lo];
+       const next = table[hi];
 
        const span = next[skey] - prev[skey];
        const ratio = span ? (sval - prev[skey]) / span : 0;
@@ -716,15 +690,6 @@ class TimeScale extends Scale {
                return interpolate(me._table, 'pos', pos, 'time');
        }
 
-       getIndexForPixel(pixel) {
-               const me = this;
-               if (me.options.distribution !== 'series') {
-                       return null; // not implemented
-               }
-               const index = Math.round(me._numIndices * me.getDecimalForPixel(pixel));
-               return index < 0 || index >= me.numIndices ? null : index;
-       }
-
        /**
         * @private
         */