vega-lite
Version:
Vega-Lite is a concise high-level language for interactive visualization.
104 lines (86 loc) • 3.82 kB
text/typescript
import {stringValue} from 'vega-util';
import {VL_SELECTION_RESOLVE} from './index.js';
import {isScaleChannel, ScaleChannel} from '../../channel.js';
import * as log from '../../log/index.js';
import {hasContinuousDomain} from '../../scale.js';
import {isLayerModel, Model} from '../model.js';
import {UnitModel} from '../unit.js';
import {SelectionProjection} from './project.js';
import {SelectionCompiler} from './index.js';
import {replacePathInField} from '../../util.js';
import {NewSignal} from 'vega';
const scaleBindings: SelectionCompiler<'interval'> = {
defined: (selCmpt) => {
return selCmpt.type === 'interval' && selCmpt.resolve === 'global' && selCmpt.bind && selCmpt.bind === 'scales';
},
parse: (model, selCmpt) => {
const bound: SelectionProjection[] = (selCmpt.scales = []);
for (const proj of selCmpt.project.items) {
const channel = proj.channel;
if (!isScaleChannel(channel)) {
continue;
}
const scale = model.getScaleComponent(channel);
const scaleType = scale ? scale.get('type') : undefined;
if (scaleType == 'sequential') {
log.warn(log.message.SEQUENTIAL_SCALE_DEPRECATED);
}
if (!scale || !hasContinuousDomain(scaleType)) {
log.warn(log.message.SCALE_BINDINGS_CONTINUOUS);
continue;
}
scale.set('selectionExtent', {param: selCmpt.name, field: proj.field}, true);
bound.push(proj);
}
},
topLevelSignals: (model, selCmpt, signals) => {
const bound = selCmpt.scales.filter((proj) => signals.filter((s) => s.name === proj.signals.data).length === 0);
// Top-level signals are only needed for multiview displays and if this
// view's top-level signals haven't already been generated.
if (!model.parent || isTopLevelLayer(model) || bound.length === 0) {
return signals;
}
// vlSelectionResolve does not account for the behavior of bound scales in
// multiview displays. Each unit view adds a tuple to the store, but the
// state of the selection is the unit selection most recently updated. This
// state is captured by the top-level signals that we insert and "push
// outer" to from within the units. We need to reassemble this state into
// the top-level named signal, except no single selCmpt has a global view.
const namedSg: NewSignal = signals.find((s) => s.name === selCmpt.name);
let update = namedSg.update;
if (update.includes(VL_SELECTION_RESOLVE)) {
namedSg.update = `{${bound
.map((proj) => `${stringValue(replacePathInField(proj.field))}: ${proj.signals.data}`)
.join(', ')}}`;
} else {
for (const proj of bound) {
const mapping = `${stringValue(replacePathInField(proj.field))}: ${proj.signals.data}`;
if (!update.includes(mapping)) {
update = `${update.substring(0, update.length - 1)}, ${mapping}}`;
}
}
namedSg.update = update;
}
return signals.concat(bound.map((proj) => ({name: proj.signals.data})));
},
signals: (model, selCmpt, signals) => {
// Nested signals need only push to top-level signals with multiview displays.
if (model.parent && !isTopLevelLayer(model)) {
for (const proj of selCmpt.scales) {
const signal: any = signals.find((s) => s.name === proj.signals.data);
signal.push = 'outer';
delete signal.value;
delete signal.update;
}
}
return signals;
},
};
export default scaleBindings;
export function domain(model: UnitModel, channel: ScaleChannel) {
const scale = stringValue(model.scaleName(channel));
return `domain(${scale})`;
}
function isTopLevelLayer(model: Model): boolean {
return model.parent && isLayerModel(model.parent) && (!model.parent.parent || isTopLevelLayer(model.parent.parent));
}