import {BinTransform as VgBinTransform, Transforms as VgTransform} from 'vega'; import {isString} from 'vega-util'; import {BinParams, binToString, isBinning, isParameterExtent} from '../../bin.js'; import {Channel} from '../../channel.js'; import {binRequiresRange, FieldName, isTypedFieldDef, normalizeBin, TypedFieldDef, vgField} from '../../channeldef.js'; import {Config} from '../../config.js'; import {BinTransform} from '../../transform.js'; import {Dict, duplicate, hash, isEmpty, keys, replacePathInField, unique, vals} from '../../util.js'; import {binFormatExpression} from '../format.js'; import {isUnitModel, Model, ModelWithField} from '../model.js'; import {parseSelectionExtent} from '../selection/parse.js'; import {NonPositionScaleChannel, PositionChannel} from './../../channel.js'; import {DataFlowNode} from './dataflow.js'; function rangeFormula(model: ModelWithField, fieldDef: TypedFieldDef, channel: Channel, config: Config) { if (binRequiresRange(fieldDef, channel)) { // read format from axis or legend, if there is no format then use config.numberFormat const guide = isUnitModel(model) ? (model.axis(channel as PositionChannel) ?? model.legend(channel as NonPositionScaleChannel) ?? {}) : {}; const startField = vgField(fieldDef, {expr: 'datum'}); const endField = vgField(fieldDef, {expr: 'datum', binSuffix: 'end'}); return { formulaAs: vgField(fieldDef, {binSuffix: 'range', forAs: true}), formula: binFormatExpression(startField, endField, guide.format, guide.formatType, config), }; } return {}; } function binKey(bin: BinParams, field: string) { return `${binToString(bin)}_${field}`; } function getSignalsFromModel(model: Model, key: string) { return { signal: model.getName(`${key}_bins`), extentSignal: model.getName(`${key}_extent`), }; } export function getBinSignalName(model: Model, field: string, bin: boolean | BinParams) { const normalizedBin = normalizeBin(bin, undefined) ?? {}; const key = binKey(normalizedBin, field); return model.getName(`${key}_bins`); } function isBinTransform(t: TypedFieldDef | BinTransform): t is BinTransform { return 'as' in t; } function createBinComponent(t: TypedFieldDef | BinTransform, bin: boolean | BinParams, model: Model) { let as: [string, string]; let span: string; if (isBinTransform(t)) { as = isString(t.as) ? [t.as, `${t.as}_end`] : [t.as[0], t.as[1]]; } else { as = [vgField(t, {forAs: true}), vgField(t, {binSuffix: 'end', forAs: true})]; } const normalizedBin = {...normalizeBin(bin, undefined)}; const key = binKey(normalizedBin, t.field); const {signal, extentSignal} = getSignalsFromModel(model, key); if (isParameterExtent(normalizedBin.extent)) { const ext = normalizedBin.extent; span = parseSelectionExtent(model, ext.param, ext); delete normalizedBin.extent; // Vega-Lite selection extent map to Vega's span property. } const binComponent: BinComponent = { bin: normalizedBin, field: t.field, as: [as], ...(signal ? {signal} : {}), ...(extentSignal ? {extentSignal} : {}), ...(span ? {span} : {}), }; return {key, binComponent}; } export interface BinComponent { bin: BinParams; field: FieldName; extentSignal?: string; signal?: string; span?: string; /** Pairs of strings of the names of start and end signals */ as: [string, string][]; // Range Formula formula?: string; formulaAs?: string; } export class BinNode extends DataFlowNode { public clone() { return new BinNode(null, duplicate(this.bins)); } constructor( parent: DataFlowNode, private bins: Dict, ) { super(parent); } public static makeFromEncoding(parent: DataFlowNode, model: ModelWithField) { const bins = model.reduceFieldDef((binComponentIndex: Dict, fieldDef, channel) => { if (isTypedFieldDef(fieldDef) && isBinning(fieldDef.bin)) { const {key, binComponent} = createBinComponent(fieldDef, fieldDef.bin, model); binComponentIndex[key] = { ...binComponent, ...binComponentIndex[key], ...rangeFormula(model, fieldDef, channel, model.config), }; } return binComponentIndex; }, {} as Dict); if (isEmpty(bins)) { return null; } return new BinNode(parent, bins); } /** * Creates a bin node from BinTransform. * The optional parameter should provide */ public static makeFromTransform(parent: DataFlowNode, t: BinTransform, model: Model) { const {key, binComponent} = createBinComponent(t, t.bin, model); return new BinNode(parent, { [key]: binComponent, }); } /** * Merge bin nodes. This method either integrates the bin config from the other node * or if this node already has a bin config, renames the corresponding signal in the model. */ public merge(other: BinNode, renameSignal: (s1: string, s2: string) => void) { for (const key of keys(other.bins)) { if (key in this.bins) { renameSignal(other.bins[key].signal, this.bins[key].signal); // Ensure that we don't have duplicate names for signal pairs this.bins[key].as = unique([...this.bins[key].as, ...other.bins[key].as], hash); } else { this.bins[key] = other.bins[key]; } } for (const child of other.children) { other.removeChild(child); child.parent = this; } other.remove(); } public producedFields() { return new Set( vals(this.bins) .map((c) => c.as) .flat(2), ); } public dependentFields() { return new Set(vals(this.bins).map((c) => c.field)); } public hash() { return `Bin ${hash(this.bins)}`; } public assemble(): VgTransform[] { return vals(this.bins).flatMap((bin) => { const transform: VgTransform[] = []; const [binAs, ...remainingAs] = bin.as; const {extent, ...params} = bin.bin; const binTrans: VgBinTransform = { type: 'bin', field: replacePathInField(bin.field), as: binAs, signal: bin.signal, ...(!isParameterExtent(extent) ? {extent} : {extent: null}), ...(bin.span ? {span: {signal: `span(${bin.span})`}} : {}), ...params, }; if (!extent && bin.extentSignal) { transform.push({ type: 'extent', field: replacePathInField(bin.field), signal: bin.extentSignal, }); binTrans.extent = {signal: bin.extentSignal}; } transform.push(binTrans); for (const as of remainingAs) { for (let i = 0; i < 2; i++) { transform.push({ type: 'formula', expr: vgField({field: binAs[i]}, {expr: 'datum'}), as: as[i], }); } } if (bin.formula) { transform.push({ type: 'formula', expr: bin.formula, as: bin.formulaAs, }); } return transform; }); } }