Skip to content

Commit

Permalink
Optimize selection functions for ID-only stores.
Browse files Browse the repository at this point in the history
  • Loading branch information
arvind authored and jheer committed Mar 8, 2022
1 parent 0c54c76 commit 440f48e
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 35 deletions.
5 changes: 5 additions & 0 deletions packages/vega-selections/src/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import {field} from 'vega-util';

export const Intersect = 'intersect';
export const Union = 'union';
export const VlMulti = 'vlMulti';
export const VlPoint = 'vlPoint';
export const Or = 'or';
export const And = 'and';

export const SelectionId = '_vgsid_';
export const $selectionId = field(SelectionId);
67 changes: 44 additions & 23 deletions packages/vega-selections/src/selectionResolve.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {And, Or, Union, VlMulti, VlPoint} from './constants';
import {intersection, union} from 'd3-array';
import {array, toNumber} from 'vega-util';
import {$selectionId, And, Or, SelectionId, Union, VlMulti, VlPoint} from './constants';

/**
* Resolves selection for use as a scale domain or reads via the API.
Expand All @@ -18,7 +19,7 @@ export function selectionResolve(name, op, isMulti, vl5) {
var data = this.context.data[name],
entries = data ? data.values.value : [],
resolved = {}, multiRes = {}, types = {},
entry, fields, values, unit, field, res, resUnit, type, union,
entry, fields, values, unit, field, value, res, resUnit, type, union,
n = entries.length, i = 0, j, m;

// First union all entries within the same unit.
Expand All @@ -28,34 +29,51 @@ export function selectionResolve(name, op, isMulti, vl5) {
fields = entry.fields;
values = entry.values;

for (j = 0, m = fields.length; j < m; ++j) {
field = fields[j];
res = resolved[field.field] || (resolved[field.field] = {});
if (fields && values) { // Intentional selection stores
for (j = 0, m = fields.length; j < m; ++j) {
field = fields[j];
res = resolved[field.field] || (resolved[field.field] = {});
resUnit = res[unit] || (res[unit] = []);
types[field.field] = type = field.type.charAt(0);
union = ops[`${type}_union`];
res[unit] = union(resUnit, array(values[j]));
}

// If the same multi-selection is repeated over views and projected over
// an encoding, it may operate over different fields making it especially
// tricky to reliably resolve it. At best, we can de-dupe identical entries
// but doing so may be more computationally expensive than it is worth.
// Instead, for now, we simply transform our store representation into
// a more human-friendly one.
if (isMulti) {
resUnit = multiRes[unit] || (multiRes[unit] = []);
resUnit.push(array(values).reduce((obj, curr, j) => (obj[fields[j].field] = curr, obj), {}));
}
} else { // Short circuit extensional selectionId stores which hold sorted IDs unique to each unit.
field = SelectionId;
value = $selectionId(entry);
res = resolved[field] || (resolved[field] = {});
resUnit = res[unit] || (res[unit] = []);
types[field.field] = type = field.type.charAt(0);
union = ops[type + '_union'];
res[unit] = union(resUnit, array(values[j]));
}
resUnit.push(value);

// If the same multi-selection is repeated over views and projected over
// an encoding, it may operate over different fields making it especially
// tricky to reliably resolve it. At best, we can de-dupe identical entries
// but doing so may be more computationally expensive than it is worth.
// Instead, for now, we simply transform our store representation into
// a more human-friendly one.
if (isMulti) {
resUnit = multiRes[unit] || (multiRes[unit] = []);
resUnit.push(array(values).reduce((obj, curr, j) => (obj[fields[j].field] = curr, obj), {}));
if (isMulti) {
resUnit = multiRes[unit] || (multiRes[unit] = []);
resUnit.push({[SelectionId]: value});
}
}
}

// Then resolve fields across units as per the op.
op = op || Union;
Object.keys(resolved).forEach(field => {
resolved[field] = Object.keys(resolved[field])
.map(unit => resolved[field][unit])
.reduce((acc, curr) => acc === undefined ? curr : ops[types[field] + '_' + op](acc, curr));
});
if (resolved[SelectionId]) {
resolved[SelectionId] = ops[`${SelectionId}_${op}`](...Object.values(resolved[SelectionId]));
} else {
Object.keys(resolved).forEach(field => {
resolved[field] = Object.keys(resolved[field])
.map(unit => resolved[field][unit])
.reduce((acc, curr) => acc === undefined ? curr : ops[`${types[field]}_${op}`](acc, curr));
});
}

entries = Object.keys(multiRes);
if (isMulti && entries.length) {
Expand All @@ -69,6 +87,9 @@ export function selectionResolve(name, op, isMulti, vl5) {
}

var ops = {
[`${SelectionId}_union`]: union,
[`${SelectionId}_intersect`]: intersection,

E_union: function(base, value) {
if (!base.length) return value;

Expand Down
12 changes: 5 additions & 7 deletions packages/vega-selections/src/selectionTest.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {bisector} from 'd3-array';
import {Intersect} from './constants';
import {$selectionId, Intersect} from './constants';
import {field, inrange, isArray, isDate, toNumber} from 'vega-util';

const SELECTION_ID = '_vgsid_',
TYPE_ENUM = 'E',
const TYPE_ENUM = 'E',
TYPE_RANGE_INC = 'R',
TYPE_RANGE_EXC = 'R-E',
TYPE_RANGE_LE = 'R-LE',
Expand Down Expand Up @@ -105,8 +104,7 @@ export function selectionTest(name, datum, op) {
return n && intersect;
}

const selectionId = field(SELECTION_ID),
bisect = bisector(selectionId),
const bisect = bisector($selectionId),
bisectLeft = bisect.left,
bisectRight = bisect.right;

Expand All @@ -115,11 +113,11 @@ export function selectionIdTest(name, datum, op) {
entries = data ? data.values.value : [],
unitIdx = data ? data[UNIT_INDEX] && data[UNIT_INDEX].value : undefined,
intersect = op === Intersect,
value = selectionId(datum),
value = $selectionId(datum),
index = bisectLeft(entries, value);

if (index === entries.length) return false;
if (selectionId(entries[index]) !== value) return false;
if ($selectionId(entries[index]) !== value) return false;

if (unitIdx && intersect) {
if (unitIdx.size === 1) return true;
Expand Down
12 changes: 8 additions & 4 deletions packages/vega-selections/src/selectionTuples.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {extend, field} from 'vega-util';
import {$selectionId, SelectionId} from './constants';

/**
* Maps an array of scene graph items to an array of selection tuples.
* @param {string} name - The name of the dataset representing the selection.
* @param {string} unit - The name of the unit view.
* @param {string} base - The base object that generated tuples extend.
*
* @returns {array} An array of selection entries for the given unit.
*/
export function selectionTuples(array, base) {
return array.map(x => extend({
values: base.fields.map(f => (f.getter || (f.getter = field(f.field)))(x.datum))
}, base));
return array.map(x => extend(
base.fields ? {
values: base.fields.map(f => (f.getter || (f.getter = field(f.field)))(x.datum))
} : {
[SelectionId]: $selectionId(x.datum)
}, base));
}
2 changes: 1 addition & 1 deletion packages/vega-selections/src/selectionVisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ export function selectionVisitor(name, args, scope, params) {
if (!hasOwnProperty(params, dataName)) {
params[dataName] = scope.getData(data).tuplesRef();
}
}
}

0 comments on commit 440f48e

Please sign in to comment.