Skip to content

Commit 9fda5ec

Browse files
kurkleetimberg
authored andcommitted
Use binary search for interpolations (#6958)
1 parent b76dd46 commit 9fda5ec

File tree

6 files changed

+112
-75
lines changed

6 files changed

+112
-75
lines changed

src/core/core.datasetController.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,10 @@ helpers.extend(DatasetController.prototype, {
980980
* @private
981981
*/
982982
_getSharedOptions: function(mode, el, options) {
983+
if (!mode) {
984+
// store element option sharing status for usage in interactions
985+
this._sharedOptions = options && options.$shared;
986+
}
983987
if (mode !== 'reset' && options && options.$shared && el && el.options && el.options.$shared) {
984988
return {target: el.options, options};
985989
}

src/core/core.interaction.js

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use strict';
22

33
import helpers from '../helpers/index';
4-
import {isNumber} from '../helpers/helpers.math';
54
import {_isPointInArea} from '../helpers/helpers.canvas';
5+
import {_lookup, _rlookup} from '../helpers/helpers.collection';
66

77
/**
88
* Helper function to get relative position for an event
@@ -42,38 +42,58 @@ function evaluateAllVisibleItems(chart, handler) {
4242
}
4343

4444
/**
45-
* Helper function to check the items at the hovered index on the index scale
45+
* Helper function to do binary search when possible
46+
* @param {object} metaset - the dataset meta
47+
* @param {string} axis - the axis mide. x|y|xy
48+
* @param {number} value - the value to find
49+
* @param {boolean} intersect - should the element intersect
50+
* @returns {lo, hi} indices to search data array between
51+
*/
52+
function binarySearch(metaset, axis, value, intersect) {
53+
const {controller, data, _sorted} = metaset;
54+
const iScale = controller._cachedMeta.iScale;
55+
if (iScale && axis === iScale.axis && _sorted) {
56+
const lookupMethod = iScale._reversePixels ? _rlookup : _lookup;
57+
if (!intersect) {
58+
return lookupMethod(data, axis, value);
59+
} else if (controller._sharedOptions) {
60+
// _sharedOptions indicates that each element has equal options -> equal proportions
61+
// So we can do a ranged binary search based on the range of first element and
62+
// be confident to get the full range of indices that can intersect with the value.
63+
const el = data[0];
64+
const range = typeof el.getRange === 'function' && el.getRange(axis);
65+
if (range) {
66+
const start = lookupMethod(data, axis, value - range);
67+
const end = lookupMethod(data, axis, value + range);
68+
return {lo: start.lo, hi: end.hi};
69+
}
70+
}
71+
}
72+
// Default to all elements, when binary search can not be used.
73+
return {lo: 0, hi: data.length - 1};
74+
}
75+
76+
/**
77+
* Helper function to get items using binary search, when the data is sorted.
4678
* @param {Chart} chart - the chart
4779
* @param {string} axis - the axis mode. x|y|xy
4880
* @param {object} position - the point to be nearest to
4981
* @param {function} handler - the callback to execute for each visible item
50-
* @return whether all scales were of a suitable type
82+
* @param {boolean} intersect - consider intersecting items
5183
*/
52-
function evaluateItemsAtIndex(chart, axis, position, handler) {
84+
function optimizedEvaluateItems(chart, axis, position, handler, intersect) {
5385
const metasets = chart._getSortedVisibleDatasetMetas();
54-
const indices = [];
55-
for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
56-
const metaset = metasets[i];
57-
const iScale = metaset.controller._cachedMeta.iScale;
58-
if (!iScale || axis !== iScale.axis || !iScale.getIndexForPixel) {
59-
return false;
60-
}
61-
const index = iScale.getIndexForPixel(position[axis]);
62-
if (!isNumber(index)) {
63-
return false;
64-
}
65-
indices.push(index);
66-
}
67-
// do this only after checking whether all scales are of a suitable type
86+
const value = position[axis];
6887
for (let i = 0, ilen = metasets.length; i < ilen; ++i) {
69-
const metaset = metasets[i];
70-
const index = indices[i];
71-
const element = metaset.data[index];
72-
if (!element.skip) {
73-
handler(element, metaset.index, index);
88+
const {index, data} = metasets[i];
89+
let {lo, hi} = binarySearch(metasets[i], axis, value, intersect);
90+
for (let j = lo; j <= hi; ++j) {
91+
const element = data[j];
92+
if (!element.skip) {
93+
handler(element, index, j);
94+
}
7495
}
7596
}
76-
return true;
7797
}
7898

7999
/**
@@ -112,12 +132,7 @@ function getIntersectItems(chart, position, axis) {
112132
}
113133
};
114134

115-
const optimized = evaluateItemsAtIndex(chart, axis, position, evaluationFunc);
116-
if (optimized) {
117-
return items;
118-
}
119-
120-
evaluateAllVisibleItems(chart, evaluationFunc);
135+
optimizedEvaluateItems(chart, axis, position, evaluationFunc, true);
121136
return items;
122137
}
123138

@@ -154,12 +169,7 @@ function getNearestItems(chart, position, axis, intersect) {
154169
}
155170
};
156171

157-
const optimized = evaluateItemsAtIndex(chart, axis, position, evaluationFunc);
158-
if (optimized) {
159-
return items;
160-
}
161-
162-
evaluateAllVisibleItems(chart, evaluationFunc);
172+
optimizedEvaluateItems(chart, axis, position, evaluationFunc);
163173
return items;
164174
}
165175

src/elements/element.point.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ class Point extends Element {
7777
helpers.canvas.drawPoint(ctx, options, me.x, me.y);
7878
}
7979
}
80+
81+
getRange() {
82+
const options = this.options || {};
83+
return options.radius + options.hitRadius;
84+
}
8085
}
8186

8287
Point.prototype._type = 'point';

src/elements/element.rectangle.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ class Rectangle extends Element {
181181
y: this.y
182182
};
183183
}
184+
185+
getRange(axis) {
186+
return axis === 'x' ? this.width / 2 : this.height / 2;
187+
}
184188
}
185189

186190
Rectangle.prototype._type = 'rectangle';

src/helpers/helpers.collection.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
/**
4+
* Binary search
5+
* @param {array} table - the table search. must be sorted!
6+
* @param {string} key - property name for the value in each entry
7+
* @param {number} value - value to find
8+
* @private
9+
*/
10+
export function _lookup(table, key, value) {
11+
let hi = table.length - 1;
12+
let lo = 0;
13+
let mid;
14+
15+
while (hi - lo > 1) {
16+
mid = (lo + hi) >> 1;
17+
if (table[mid][key] < value) {
18+
lo = mid;
19+
} else {
20+
hi = mid;
21+
}
22+
}
23+
24+
return {lo, hi};
25+
}
26+
27+
/**
28+
* Reverse binary search
29+
* @param {array} table - the table search. must be sorted!
30+
* @param {string} key - property name for the value in each entry
31+
* @param {number} value - value to find
32+
* @private
33+
*/
34+
export function _rlookup(table, key, value) {
35+
let hi = table.length - 1;
36+
let lo = 0;
37+
let mid;
38+
39+
while (hi - lo > 1) {
40+
mid = (lo + hi) >> 1;
41+
if (table[mid][key] < value) {
42+
hi = mid;
43+
} else {
44+
lo = mid;
45+
}
46+
}
47+
48+
return {lo, hi};
49+
}

src/scales/scale.time.js

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import defaults from '../core/core.defaults';
55
import helpers from '../helpers/index';
66
import {toRadians} from '../helpers/helpers.math';
77
import Scale from '../core/core.scale';
8+
import {_lookup} from '../helpers/helpers.collection';
89

910
const resolve = helpers.options.resolve;
1011
const valueOrDefault = helpers.valueOrDefault;
@@ -130,45 +131,18 @@ function buildLookupTable(timestamps, min, max, distribution) {
130131
return table;
131132
}
132133

133-
// @see adapted from https://www.anujgakhar.com/2014/03/01/binary-search-in-javascript/
134-
function lookup(table, key, value) {
135-
let lo = 0;
136-
let hi = table.length - 1;
137-
let mid, i0, i1;
138-
139-
while (lo >= 0 && lo <= hi) {
140-
mid = (lo + hi) >> 1;
141-
i0 = mid > 0 && table[mid - 1] || null;
142-
i1 = table[mid];
143-
144-
if (!i0) {
145-
// given value is outside table (before first item)
146-
return {lo: null, hi: i1};
147-
} else if (i1[key] < value) {
148-
lo = mid + 1;
149-
} else if (i0[key] > value) {
150-
hi = mid - 1;
151-
} else {
152-
return {lo: i0, hi: i1};
153-
}
154-
}
155-
156-
// given value is outside table (after last item)
157-
return {lo: i1, hi: null};
158-
}
159-
160134
/**
161135
* Linearly interpolates the given source `value` using the table items `skey` values and
162136
* returns the associated `tkey` value. For example, interpolate(table, 'time', 42, 'pos')
163137
* returns the position for a timestamp equal to 42. If value is out of bounds, values at
164138
* index [0, 1] or [n - 1, n] are used for the interpolation.
165139
*/
166140
function interpolate(table, skey, sval, tkey) {
167-
const range = lookup(table, skey, sval);
141+
const {lo, hi} = _lookup(table, skey, sval);
168142

169143
// Note: the lookup table ALWAYS contains at least 2 items (min and max)
170-
const prev = !range.lo ? table[0] : !range.hi ? table[table.length - 2] : range.lo;
171-
const next = !range.lo ? table[1] : !range.hi ? table[table.length - 1] : range.hi;
144+
const prev = table[lo];
145+
const next = table[hi];
172146

173147
const span = next[skey] - prev[skey];
174148
const ratio = span ? (sval - prev[skey]) / span : 0;
@@ -716,15 +690,6 @@ class TimeScale extends Scale {
716690
return interpolate(me._table, 'pos', pos, 'time');
717691
}
718692

719-
getIndexForPixel(pixel) {
720-
const me = this;
721-
if (me.options.distribution !== 'series') {
722-
return null; // not implemented
723-
}
724-
const index = Math.round(me._numIndices * me.getDecimalForPixel(pixel));
725-
return index < 0 || index >= me.numIndices ? null : index;
726-
}
727-
728693
/**
729694
* @private
730695
*/

0 commit comments

Comments
 (0)