import {parseSelector} from 'vega-event-selector';
import {array, isObject, isString, stringValue} from 'vega-util';
import {isTimerSelection, selectionCompilers, SelectionComponent, STORE} from './index.js';
import {warn} from '../../log/index.js';
import {BaseSelectionConfig, SelectionParameter, ParameterExtent} from '../../selection.js';
import {Dict, duplicate, entries, replacePathInField, varName} from '../../util.js';
import {DataFlowNode, OutputNode} from '../data/dataflow.js';
import {FilterNode} from '../data/filter.js';
import {Model} from '../model.js';
import {UnitModel} from '../unit.js';
import {DataSourceType} from '../../data.js';
import {ParameterPredicate} from '../../predicate.js';
import {
MULTIPLE_TIMER_ANIMATION_SELECTION,
selectionAsScaleDomainWithoutField,
selectionAsScaleDomainWrongEncodings,
} from '../../log/message.js';
export function parseUnitSelection(model: UnitModel, selDefs: SelectionParameter[]) {
const selCmpts: Dict> = {};
const selectionConfig = model.config.selection;
if (!selDefs || !selDefs.length) return selCmpts;
let nTimerSelections = 0;
for (const def of selDefs) {
const name = varName(def.name);
const selDef = def.select;
const type = isString(selDef) ? selDef : selDef.type;
const defaults: BaseSelectionConfig = isObject(selDef) ? duplicate(selDef) : {type};
// Set default values from config if a property hasn't been specified,
// or if it is true. E.g., "translate": true should use the default
// event handlers for translate. However, true may be a valid value for
// a property (e.g., "nearest": true).
const cfg = selectionConfig[type];
for (const key in cfg) {
// Project transform applies its defaults.
if (key === 'fields' || key === 'encodings') {
continue;
}
if (key === 'mark') {
(defaults as any).mark = {...(cfg as any).mark, ...(defaults as any).mark};
}
if ((defaults as any)[key] === undefined || (defaults as any)[key] === true) {
(defaults as any)[key] = duplicate((cfg as any)[key] ?? (defaults as any)[key]);
}
}
const selCmpt: SelectionComponent = (selCmpts[name] = {
...defaults,
name,
type,
init: def.value,
bind: def.bind,
events: isString(defaults.on) ? parseSelector(defaults.on, 'scope') : array(duplicate(defaults.on)),
} as any);
if (isTimerSelection(selCmpt)) {
nTimerSelections++;
// check for multiple timer selections and ignore all but the first one
if (nTimerSelections > 1) {
delete selCmpts[name];
continue;
}
}
const def_ = duplicate(def); // defensive copy to prevent compilers from causing side effects
for (const c of selectionCompilers) {
if (c.defined(selCmpt) && c.parse) {
c.parse(model, selCmpt, def_);
}
}
}
if (nTimerSelections > 1) {
// if multiple timer selections were found, issue a warning
warn(MULTIPLE_TIMER_ANIMATION_SELECTION);
}
return selCmpts;
}
export function parseSelectionPredicate(
model: Model,
pred: ParameterPredicate,
dfnode?: DataFlowNode,
datum = 'datum',
): string {
const name = isString(pred) ? pred : pred.param;
const vname = varName(name);
const store = stringValue(vname + STORE);
let selCmpt;
try {
selCmpt = model.getSelectionComponent(vname, name);
} catch {
// If a selection isn't found, treat as a variable parameter and coerce to boolean.
return `!!${vname}`;
}
if (selCmpt.project.timeUnit) {
const child = dfnode ?? model.component.data.raw;
const tunode = selCmpt.project.timeUnit.clone();
if (child.parent) {
tunode.insertAsParentOf(child);
} else {
child.parent = tunode;
}
}
const fn = selCmpt.project.hasSelectionId ? 'vlSelectionIdTest(' : 'vlSelectionTest(';
const resolve = selCmpt.resolve === 'global' ? ')' : `, ${stringValue(selCmpt.resolve)})`;
const test = `${fn}${store}, ${datum}${resolve}`;
const length = `length(data(${store}))`;
return pred.empty === false ? `${length} && ${test}` : `!${length} || ${test}`;
}
export function parseSelectionExtent(model: Model, name: string, extent: ParameterExtent) {
const vname = varName(name);
const encoding = (extent as any).encoding;
let field = (extent as any).field;
let selCmpt;
try {
selCmpt = model.getSelectionComponent(vname, name);
} catch {
// If a selection isn't found, treat it as a variable parameter.
return vname;
}
if (!encoding && !field) {
field = selCmpt.project.items[0].field;
if (selCmpt.project.items.length > 1) {
warn(selectionAsScaleDomainWithoutField(field));
}
} else if (encoding && !field) {
const encodings = selCmpt.project.items.filter((p) => p.channel === encoding);
if (!encodings.length || encodings.length > 1) {
field = selCmpt.project.items[0].field;
warn(selectionAsScaleDomainWrongEncodings(encodings, encoding, extent, field));
} else {
field = encodings[0].field;
}
}
return `${selCmpt.name}[${stringValue(replacePathInField(field))}]`;
}
export function materializeSelections(model: UnitModel, main: OutputNode) {
for (const [selection, selCmpt] of entries(model.component.selection ?? {})) {
const lookupName = model.getName(`lookup_${selection}`);
model.component.data.outputNodes[lookupName] = selCmpt.materialized = new OutputNode(
new FilterNode(main, model, {param: selection}),
lookupName,
DataSourceType.Lookup,
model.component.data.outputNodeRefCounts,
);
}
}