Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use binary search for interpolations #6958

Merged
merged 3 commits into from
Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/core/core.datasetController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}
Expand Down
82 changes: 46 additions & 36 deletions src/core/core.interaction.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
benmccann marked this conversation as resolved.
Show resolved Hide resolved
// _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]);
benmccann marked this conversation as resolved.
Show resolved Hide resolved
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;
}

/**
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
5 changes: 5 additions & 0 deletions src/elements/element.point.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 4 additions & 0 deletions src/elements/element.rectangle.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ class Rectangle extends Element {
y: this.y
};
}

getRange(axis) {
return axis === 'x' ? this.width / 2 : this.height / 2;
}
}

Rectangle.prototype._type = 'rectangle';
Expand Down
49 changes: 49 additions & 0 deletions src/helpers/helpers.collection.js
Original file line number Diff line number Diff line change
@@ -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};
}
43 changes: 4 additions & 39 deletions src/scales/scale.time.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -130,45 +131,18 @@ 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')
* 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.
*/
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;
Expand Down Expand Up @@ -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
*/
Expand Down