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

Optimize selection functions for ID-only stores #3397

Merged
merged 1 commit into from
Mar 8, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
}
}
}