From dba4d5ea68b066728e8e042972a67859268765e4 Mon Sep 17 00:00:00 2001 From: Lukas Hermann Date: Tue, 2 Apr 2024 16:01:30 -0700 Subject: [PATCH] chore: recursively substitute signals for expressions --- src/compile/buildmodel.ts | 7 +- src/compile/compile.ts | 6 +- src/compile/concat.ts | 15 +++-- src/compile/facet.ts | 15 +++-- src/compile/layer.ts | 8 +-- src/compile/mark/init.ts | 6 +- src/compile/model.ts | 12 ++-- src/compile/projection/parse.ts | 5 +- src/compile/unit.ts | 29 ++------ src/config.ts | 114 +++++--------------------------- src/encoding.ts | 12 ++-- src/expr.ts | 31 ++++++++- src/legend.ts | 2 +- src/mark.ts | 8 +-- src/projection.ts | 2 +- src/scale.ts | 2 +- src/spec/base.ts | 4 +- src/spec/unit.ts | 6 +- src/vega.schema.ts | 18 ++++- test/expr.test.ts | 16 +++++ 20 files changed, 148 insertions(+), 170 deletions(-) create mode 100644 test/expr.test.ts diff --git a/src/compile/buildmodel.ts b/src/compile/buildmodel.ts index 7bbc17726b..d4ba74dc71 100644 --- a/src/compile/buildmodel.ts +++ b/src/compile/buildmodel.ts @@ -1,4 +1,4 @@ -import type {SignalRef} from 'vega'; +import type {ExprRef, SignalRef} from 'vega'; import {Config} from '../config'; import * as log from '../log'; import {isAnyConcatSpec, isFacetSpec, isLayerSpec, isUnitSpec, LayoutSizeMixins, NormalizedSpec} from '../spec'; @@ -7,9 +7,10 @@ import {FacetModel} from './facet'; import {LayerModel} from './layer'; import {Model} from './model'; import {UnitModel} from './unit'; +import {SubstituteType} from '../vega.schema'; export function buildModel( - spec: NormalizedSpec, + spec: SubstituteType, parent: Model, parentGivenName: string, unitSize: LayoutSizeMixins, @@ -19,7 +20,7 @@ export function buildModel( return new FacetModel(spec, parent, parentGivenName, config); } else if (isLayerSpec(spec)) { return new LayerModel(spec, parent, parentGivenName, unitSize, config); - } else if (isUnitSpec(spec)) { + } else if (isUnitSpec(spec)) { return new UnitModel(spec, parent, parentGivenName, unitSize, config); } else if (isAnyConcatSpec(spec)) { return new ConcatModel(spec, parent, parentGivenName, config); diff --git a/src/compile/compile.ts b/src/compile/compile.ts index 13e76dcaa8..5c54f19539 100644 --- a/src/compile/compile.ts +++ b/src/compile/compile.ts @@ -20,6 +20,7 @@ import {buildModel} from './buildmodel'; import {assembleRootData} from './data/assemble'; import {optimizeDataflow} from './data/optimize'; import {Model} from './model'; +import {deepReplaceExprRef} from '../expr'; export interface CompileOptions { /** @@ -89,7 +90,8 @@ export function compile(inputSpec: TopLevelSpec, opt: CompileOptions = {}) { // - Decompose all extended unit specs into composition of unit spec. For example, a box plot get expanded into multiple layers of bars, ticks, and rules. The shorthand row/column channel is also expanded to a facet spec. // - Normalize autosize and width or height spec - const spec = normalize(inputSpec, config); + const normalized = normalize(inputSpec, config); + const spec = deepReplaceExprRef(normalized) as any; // 3. Build Model: normalized spec -> Model (a tree structure) @@ -128,7 +130,7 @@ export function compile(inputSpec: TopLevelSpec, opt: CompileOptions = {}) { return { spec: vgSpec, - normalized: spec + normalized }; } finally { // Reset the singleton logger if a logger is provided diff --git a/src/compile/concat.ts b/src/compile/concat.ts index b81632e78f..76a146d6e6 100644 --- a/src/compile/concat.ts +++ b/src/compile/concat.ts @@ -1,9 +1,9 @@ -import {NewSignal, SignalRef} from 'vega'; +import {ExprRef, NewSignal, SignalRef} from 'vega'; import {Config} from '../config'; import * as log from '../log'; import {isHConcatSpec, isVConcatSpec, NormalizedConcatSpec, NormalizedSpec} from '../spec'; import {keys} from '../util'; -import {VgData, VgLayout} from '../vega.schema'; +import {SubstituteType, VgData, VgLayout} from '../vega.schema'; import {buildModel} from './buildmodel'; import {parseData} from './data/parse'; import {assembleLayoutSignals} from './layoutsize/assemble'; @@ -13,7 +13,12 @@ import {Model} from './model'; export class ConcatModel extends Model { public readonly children: Model[]; - constructor(spec: NormalizedConcatSpec, parent: Model, parentGivenName: string, config: Config) { + constructor( + spec: SubstituteType, + parent: Model, + parentGivenName: string, + config: Config + ) { super(spec, 'concat', parent, parentGivenName, config, spec.resolve); if (spec.resolve?.axis?.x === 'shared' || spec.resolve?.axis?.y === 'shared') { @@ -59,7 +64,9 @@ export class ConcatModel extends Model { // TODO(#2415): support shared axes } - private getChildren(spec: NormalizedConcatSpec): NormalizedSpec[] { + private getChildren( + spec: SubstituteType + ): SubstituteType[] { if (isVConcatSpec(spec)) { return spec.vconcat; } else if (isHConcatSpec(spec)) { diff --git a/src/compile/facet.ts b/src/compile/facet.ts index e7f073d72e..556df7d632 100644 --- a/src/compile/facet.ts +++ b/src/compile/facet.ts @@ -4,14 +4,14 @@ import {isBinning} from '../bin'; import {COLUMN, ExtendedChannel, FacetChannel, FACET_CHANNELS, POSITION_SCALE_CHANNELS, ROW} from '../channel'; import {FieldName, FieldRefOption, initFieldDef, TypedFieldDef, vgField} from '../channeldef'; import {Config} from '../config'; -import {ExprRef, replaceExprRef} from '../expr'; +import {ExprRef} from '../expr'; import * as log from '../log'; import {hasDiscreteDomain} from '../scale'; import {DEFAULT_SORT_OP, EncodingSortField, isSortField, SortOrder} from '../sort'; import {NormalizedFacetSpec} from '../spec'; import {EncodingFacetMapping, FacetFieldDef, FacetMapping, isFacetMapping} from '../spec/facet'; import {keys} from '../util'; -import {isVgRangeStep, VgData, VgLayout, VgMarkGroup} from '../vega.schema'; +import {isVgRangeStep, SubstituteType, VgData, VgLayout, VgMarkGroup} from '../vega.schema'; import {buildModel} from './buildmodel'; import {assembleFacetData} from './data/assemble'; import {sortArrayIndexField} from './data/calculate'; @@ -40,7 +40,12 @@ export class FacetModel extends ModelWithField { public readonly children: Model[]; - constructor(spec: NormalizedFacetSpec, parent: Model, parentGivenName: string, config: Config) { + constructor( + spec: SubstituteType, + parent: Model, + parentGivenName: string, + config: Config + ) { super(spec, 'facet', parent, parentGivenName, config, spec.resolve); this.child = buildModel(spec.spec, this, this.getName('child'), undefined, config); @@ -82,9 +87,7 @@ export class FacetModel extends ModelWithField { // Cast because we call initFieldDef, which assumes general FieldDef. // However, FacetFieldDef is a bit more constrained than the general FieldDef const facetFieldDef = initFieldDef(fieldDef, channel) as FacetFieldDef; - if (facetFieldDef.header) { - facetFieldDef.header = replaceExprRef(facetFieldDef.header); - } else if (facetFieldDef.header === null) { + if (facetFieldDef.header === null) { facetFieldDef.header = null; } return facetFieldDef; diff --git a/src/compile/layer.ts b/src/compile/layer.ts index 933871c077..fb18d6c316 100644 --- a/src/compile/layer.ts +++ b/src/compile/layer.ts @@ -1,10 +1,10 @@ -import {Legend as VgLegend, NewSignal, SignalRef, Title as VgTitle} from 'vega'; +import {Legend as VgLegend, NewSignal, SignalRef, Title as VgTitle, ExprRef} from 'vega'; import {array} from 'vega-util'; import {Config} from '../config'; import * as log from '../log'; import {isLayerSpec, isUnitSpec, LayoutSizeMixins, NormalizedLayerSpec} from '../spec'; import {keys} from '../util'; -import {VgData, VgLayout} from '../vega.schema'; +import {SubstituteType, VgData, VgLayout} from '../vega.schema'; import {assembleAxisSignals} from './axis/assemble'; import {parseLayerAxes} from './axis/parse'; import {parseData} from './data/parse'; @@ -21,7 +21,7 @@ export class LayerModel extends Model { public readonly children: Model[]; constructor( - spec: NormalizedLayerSpec, + spec: SubstituteType, parent: Model, parentGivenName: string, parentGivenSize: LayoutSizeMixins, @@ -39,7 +39,7 @@ export class LayerModel extends Model { if (isLayerSpec(layer)) { return new LayerModel(layer, this, this.getName(`layer_${i}`), layoutSize, config); } else if (isUnitSpec(layer)) { - return new UnitModel(layer, this, this.getName(`layer_${i}`), layoutSize, config); + return new UnitModel(layer, this, this.getName(`layer_${i}`), layoutSize, config) as UnitModel; } throw new Error(log.message.invalidSpec(layer)); diff --git a/src/compile/mark/init.ts b/src/compile/mark/init.ts index 3c730e5d39..513a9cd709 100644 --- a/src/compile/mark/init.ts +++ b/src/compile/mark/init.ts @@ -3,7 +3,6 @@ import {isBinned, isBinning} from '../../bin'; import {isFieldDef, isNumericDataDef, isUnbinnedQuantitativeFieldOrDatumDef, isTypedFieldDef} from '../../channeldef'; import {Config} from '../../config'; import {Encoding, isAggregate} from '../../encoding'; -import {replaceExprRef} from '../../expr'; import * as log from '../../log'; import { AREA, @@ -25,10 +24,7 @@ import {QUANTITATIVE, TEMPORAL} from '../../type'; import {contains, getFirstDefined} from '../../util'; import {getMarkConfig, getMarkPropOrConfig} from '../common'; -export function initMarkdef(originalMarkDef: MarkDef, encoding: Encoding, config: Config) { - // FIXME: markDef expects that exprRefs are replaced recursively but replaceExprRef only replaces the top level - const markDef: MarkDef = replaceExprRef(originalMarkDef) as any; - +export function initMarkdef(markDef: MarkDef, encoding: Encoding, config: Config) { // set orient, which can be overridden by rules as sometimes the specified orient is invalid. const specifiedOrient = getMarkPropOrConfig('orient', markDef, config); markDef.orient = orient(markDef.type, encoding, specifiedOrient); diff --git a/src/compile/model.ts b/src/compile/model.ts index 4ecdf6f3fe..c9da0679c8 100644 --- a/src/compile/model.ts +++ b/src/compile/model.ts @@ -22,7 +22,7 @@ import {ChannelDef, FieldDef, FieldRefOption, getFieldDef, vgField} from '../cha import {Config} from '../config'; import {Data, DataSourceType} from '../data'; import {forEach, reduce} from '../encoding'; -import {ExprRef, replaceExprRef} from '../expr'; +import {ExprRef} from '../expr'; import * as log from '../log'; import {Resolve} from '../resolve'; import {hasDiscreteDomain} from '../scale'; @@ -38,7 +38,7 @@ import {NormalizedSpec} from '../spec/index'; import {extractTitleConfig, isText, TitleParams} from '../title'; import {normalizeTransform, Transform} from '../transform'; import {contains, Dict, duplicate, isEmpty, keys, varName} from '../util'; -import {isVgRangeStep, VgData, VgEncodeEntry, VgLayout, VgMarkGroup} from '../vega.schema'; +import {isVgRangeStep, SubstituteType, VgData, VgEncodeEntry, VgLayout, VgMarkGroup} from '../vega.schema'; import {assembleAxes} from './axis/assemble'; import {AxisComponentIndex} from './axis/component'; import {signalOrValueRef} from './common'; @@ -187,21 +187,21 @@ export abstract class Model { public abstract readonly children: Model[]; constructor( - spec: NormalizedSpec, + spec: SubstituteType, public readonly type: SpecType, public readonly parent: Model, parentGivenName: string, public readonly config: Config, resolve: Resolve, - view?: ViewBackground + view?: ViewBackground ) { this.parent = parent; this.config = config; - this.view = replaceExprRef(view); + this.view = view; // If name is not provided, always use parent's givenName to avoid name conflicts. this.name = spec.name ?? parentGivenName; - this.title = isText(spec.title) ? {text: spec.title} : spec.title ? replaceExprRef(spec.title) : undefined; + this.title = isText(spec.title) ? {text: spec.title} : spec.title; // Shared name maps this.scaleNameMap = parent ? parent.scaleNameMap : new NameMap(); diff --git a/src/compile/projection/parse.ts b/src/compile/projection/parse.ts index 3eb10005ac..a53433eb45 100644 --- a/src/compile/projection/parse.ts +++ b/src/compile/projection/parse.ts @@ -3,7 +3,6 @@ import {hasOwnProperty} from 'vega-util'; import {LATITUDE, LATITUDE2, LONGITUDE, LONGITUDE2, SHAPE} from '../../channel'; import {getFieldOrDatumDef} from '../../channeldef'; import {DataSourceType} from '../../data'; -import {replaceExprRef} from '../../expr'; import {PROJECTION_PROPERTIES} from '../../projection'; import {GEOJSON} from '../../type'; import {deepEqual, duplicate, every} from '../../util'; @@ -17,7 +16,7 @@ export function parseProjection(model: Model) { function parseUnitProjection(model: UnitModel): ProjectionComponent { if (model.hasProjection) { - const proj = replaceExprRef(model.specifiedProjection); + const proj = model.specifiedProjection; const fit = !(proj && (proj.scale != null || proj.translate != null)); const size = fit ? [model.getSizeSignalRef('width'), model.getSizeSignalRef('height')] : undefined; const data = fit ? gatherFitData(model) : undefined; @@ -25,7 +24,7 @@ function parseUnitProjection(model: UnitModel): ProjectionComponent { const projComp = new ProjectionComponent( model.projectionName(true), { - ...replaceExprRef(model.config.projection), + ...(model.config.projection ?? {}), ...proj }, size, diff --git a/src/compile/unit.ts b/src/compile/unit.ts index c56a8d4501..d1e97998d0 100644 --- a/src/compile/unit.ts +++ b/src/compile/unit.ts @@ -1,5 +1,4 @@ import {NewSignal, SignalRef} from 'vega'; -import {isArray} from 'vega-util'; import {Axis, AxisInternal, isConditionalAxisValue} from '../axis'; import { Channel, @@ -27,7 +26,7 @@ import {Config} from '../config'; import {isGraticuleGenerator} from '../data'; import * as vlEncoding from '../encoding'; import {Encoding, initEncoding} from '../encoding'; -import {ExprRef, replaceExprRef} from '../expr'; +import {ExprRef} from '../expr'; import {LegendInternal} from '../legend'; import {GEOSHAPE, isMarkDef, Mark, MarkDef} from '../mark'; import {Projection} from '../projection'; @@ -37,7 +36,7 @@ import {LayoutSizeMixins, NormalizedUnitSpec} from '../spec'; import {isFrameMixins} from '../spec/base'; import {stack, StackProperties} from '../stack'; import {keys} from '../util'; -import {VgData, VgLayout} from '../vega.schema'; +import {SubstituteType, VgData, VgLayout} from '../vega.schema'; import {assembleAxisSignals} from './axis/assemble'; import {AxisInternalIndex} from './axis/component'; import {parseUnitAxes} from './axis/parse'; @@ -74,13 +73,13 @@ export class UnitModel extends ModelWithField { protected specifiedLegends: LegendInternalIndex = {}; - public specifiedProjection: Projection = {}; + public specifiedProjection: Projection = {}; public readonly selection: SelectionParameter[] = []; public children: Model[] = []; constructor( - spec: NormalizedUnitSpec, + spec: SubstituteType, parent: Model, parentGivenName: string, parentGivenSize: LayoutSizeMixins = {}, @@ -154,25 +153,13 @@ export class UnitModel extends ModelWithField { | PositionFieldDef | MarkPropFieldOrDatumDef; if (fieldOrDatumDef) { - scales[channel] = this.initScale(fieldOrDatumDef.scale ?? {}); + // TODO: remove as Scale when we have better types for scale + scales[channel] = fieldOrDatumDef.scale as Scale; } return scales; }, {} as ScaleIndex); } - private initScale(scale: Scale): Scale { - const {domain, range} = scale; - // TODO: we could simplify this function if we had a recursive replace function - const scaleInternal = replaceExprRef(scale); - if (isArray(domain)) { - scaleInternal.domain = domain.map(signalRefOrValue); - } - if (isArray(range)) { - scaleInternal.range = range.map(signalRefOrValue); - } - return scaleInternal as Scale; - } - private initAxes(encoding: Encoding): AxisInternalIndex { return POSITION_SCALE_CHANNELS.reduce((_axis, channel) => { // Position Axis @@ -212,9 +199,7 @@ export class UnitModel extends ModelWithField { if (fieldOrDatumDef && supportLegend(channel)) { const legend = fieldOrDatumDef.legend; - _legend[channel] = legend - ? replaceExprRef(legend) // convert truthy value to object - : legend; + _legend[channel] = legend; } return _legend; diff --git a/src/config.ts b/src/config.ts index e18ae6e766..8afe460f7a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,10 @@ import {Color, InitSignal, Locale, NewSignal, RangeConfig, RangeScheme, SignalRef, writeConfig} from 'vega'; import {isObject, mergeConfig} from 'vega-util'; -import {Axis, AxisConfig, AxisConfigMixins, AXIS_CONFIGS, isConditionalAxisValue} from './axis'; -import {signalOrValueRefWithCondition, signalRefOrValue} from './compile/common'; +import {Axis, AxisConfigMixins, isConditionalAxisValue} from './axis'; import {CompositeMarkConfigMixins, getAllCompositeMarks} from './compositemark'; -import {ExprRef, replaceExprRef} from './expr'; +import {ExprRef, deepReplaceExprRef} from './expr'; import {VL_ONLY_LEGEND_CONFIG} from './guide'; -import {HeaderConfigMixins, HEADER_CONFIGS} from './header'; +import {HeaderConfigMixins} from './header'; import {defaultLegendConfig, LegendConfig} from './legend'; import * as mark from './mark'; import { @@ -13,7 +12,6 @@ import { Mark, MarkConfig, MarkConfigMixins, - MARK_CONFIGS, PRIMITIVE_MARKS, VL_ONLY_MARK_CONFIG_PROPERTIES, VL_ONLY_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX @@ -25,7 +23,7 @@ import {defaultConfig as defaultSelectionConfig, SelectionConfig} from './select import {BaseViewBackground, CompositionConfigMixins, DEFAULT_SPACING, isStep} from './spec/base'; import {TopLevelProperties} from './spec/toplevel'; import {extractTitleConfig, TitleConfig} from './title'; -import {duplicate, getFirstDefined, isEmpty, keys, omit} from './util'; +import {duplicate, getFirstDefined, isEmpty} from './util'; export interface ViewConfig extends BaseViewBackground { /** @@ -93,7 +91,7 @@ export function getViewConfigDiscreteSize( export const DEFAULT_STEP = 20; -export const defaultViewConfig: ViewConfig = { +export const defaultViewConfig: ViewConfig = { continuousWidth: 200, continuousHeight: 200, step: DEFAULT_STEP @@ -272,7 +270,7 @@ export interface Config /** * Projection configuration, which determines default properties for all [projections](https://vega.github.io/vega-lite/docs/projection.html). For a full list of projection configuration options, please see the [corresponding section of the projection documentation](https://vega.github.io/vega-lite/docs/projection.html#config). */ - projection?: ProjectionConfig; + projection?: ProjectionConfig; /** An object hash that defines key-value mappings to determine default properties for marks with a given [style](https://vega.github.io/vega-lite/docs/mark.html#mark-def). The keys represent styles names; the values have to be valid [mark configuration objects](https://vega.github.io/vega-lite/docs/mark.html#config). */ style?: StyleConfigIndex; @@ -300,7 +298,7 @@ export interface Config signals?: (InitSignal | NewSignal)[]; } -export const defaultConfig: Config = { +export const defaultConfig: Config = { background: 'white', padding: 5, @@ -420,7 +418,7 @@ export const DEFAULT_COLOR = { gray15: '#fff' }; -export function colorSignalConfig(color: boolean | ColorConfig = {}): Config { +export function colorSignalConfig(color: boolean | ColorConfig = {}): Config { return { signals: [ { @@ -472,7 +470,7 @@ export function colorSignalConfig(color: boolean | ColorConfig = {}): Config { }; } -export function fontSizeSignalConfig(fontSize: boolean | FontSizeConfig): Config { +export function fontSizeSignalConfig(fontSize: boolean | FontSizeConfig): Config { return { signals: [ { @@ -500,7 +498,7 @@ export function fontSizeSignalConfig(fontSize: boolean | FontSizeConfig): Config }; } -export function fontConfig(font: string): Config { +export function fontConfig(font: string): Config { return { text: {font}, style: { @@ -512,49 +510,14 @@ export function fontConfig(font: string): Config { }; } -function getAxisConfigInternal(axisConfig: AxisConfig) { - const props = keys(axisConfig || {}); - const axisConfigInternal: AxisConfig = {}; - for (const prop of props) { - const val = axisConfig[prop]; - axisConfigInternal[prop as any] = isConditionalAxisValue(val) - ? signalOrValueRefWithCondition(val) - : signalRefOrValue(val); - } - return axisConfigInternal; -} - -function getStyleConfigInternal(styleConfig: StyleConfigIndex) { - const props = keys(styleConfig); - - const styleConfigInternal: StyleConfigIndex = {}; - for (const prop of props) { - // We need to cast to cheat a bit here since styleConfig can be either mark config or axis config - styleConfigInternal[prop as any] = getAxisConfigInternal(styleConfig[prop] as any); - } - return styleConfigInternal; -} - -const configPropsWithExpr = [ - ...MARK_CONFIGS, - ...AXIS_CONFIGS, - ...HEADER_CONFIGS, - 'background', - 'padding', - 'legend', - 'lineBreak', - 'scale', - 'style', - 'title', - 'view' -] as const; - /** * Merge specified config with default config and config for the `color` flag, * then replace all expressions with signals */ -export function initConfig(specifiedConfig: Config = {}): Config { - const {color, font, fontSize, selection, ...restConfig} = specifiedConfig; +export function initConfig(specifiedConfig: Config = {}): Config { + // TODO: we are using initConfig in the normalizer and in the compiler and therefore run deepReplaceExprRef multiple times on the config + const {color, font, fontSize, selection, ...restConfig} = deepReplaceExprRef(specifiedConfig) as Config; + const mergedConfig = mergeConfig( {}, duplicate(defaultConfig), @@ -569,54 +532,7 @@ export function initConfig(specifiedConfig: Config = {}): Config { writeConfig(mergedConfig, 'selection', selection, true); } - const outputConfig: Config = omit(mergedConfig, configPropsWithExpr); - - for (const prop of ['background', 'lineBreak', 'padding']) { - if (mergedConfig[prop]) { - outputConfig[prop] = signalRefOrValue(mergedConfig[prop]); - } - } - - for (const markConfigType of mark.MARK_CONFIGS) { - if (mergedConfig[markConfigType]) { - // FIXME: outputConfig[markConfigType] expects that types are replaced recursively but replaceExprRef only replaces one level deep - outputConfig[markConfigType] = replaceExprRef(mergedConfig[markConfigType]) as any; - } - } - - for (const axisConfigType of AXIS_CONFIGS) { - if (mergedConfig[axisConfigType]) { - outputConfig[axisConfigType] = getAxisConfigInternal(mergedConfig[axisConfigType]); - } - } - - for (const headerConfigType of HEADER_CONFIGS) { - if (mergedConfig[headerConfigType]) { - outputConfig[headerConfigType] = replaceExprRef(mergedConfig[headerConfigType]); - } - } - - if (mergedConfig.legend) { - outputConfig.legend = replaceExprRef(mergedConfig.legend); - } - - if (mergedConfig.scale) { - outputConfig.scale = replaceExprRef(mergedConfig.scale); - } - - if (mergedConfig.style) { - outputConfig.style = getStyleConfigInternal(mergedConfig.style); - } - - if (mergedConfig.title) { - outputConfig.title = replaceExprRef(mergedConfig.title); - } - - if (mergedConfig.view) { - outputConfig.view = replaceExprRef(mergedConfig.view); - } - - return outputConfig; + return mergedConfig; } const MARK_STYLES = new Set(['view', ...PRIMITIVE_MARKS]) as ReadonlySet<'view' | Mark>; diff --git a/src/encoding.ts b/src/encoding.ts index 0dafbc975e..691cd4796d 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -1,4 +1,4 @@ -import {AggregateOp} from 'vega'; +import {AggregateOp, ExprRef, SignalRef} from 'vega'; import {array, isArray} from 'vega-util'; import {isArgmaxDef, isArgminDef} from './aggregate'; import {isBinned, isBinning} from './bin'; @@ -94,7 +94,7 @@ import {EncodingFacetMapping} from './spec/facet'; import {AggregatedFieldDef, BinTransform, TimeUnitTransform} from './transform'; import {isContinuous, isDiscrete, QUANTITATIVE, TEMPORAL} from './type'; import {keys, some} from './util'; -import {isSignalRef} from './vega.schema'; +import {SubstituteType, isSignalRef} from './vega.schema'; import {isBinnedTimeUnit} from './timeunit'; export interface Encoding { @@ -507,7 +507,11 @@ export function extractTransformsFromEncoding(oldEncoding: Encoding, config }; } -export function markChannelCompatible(encoding: Encoding, channel: Channel, mark: Mark) { +export function markChannelCompatible( + encoding: SubstituteType, ExprRef, SignalRef>, + channel: Channel, + mark: Mark +) { const markSupported = supportMark(channel, mark); if (!markSupported) { return false; @@ -526,7 +530,7 @@ export function markChannelCompatible(encoding: Encoding, channel: Chann } export function initEncoding( - encoding: Encoding, + encoding: SubstituteType, ExprRef, SignalRef>, mark: Mark, filled: boolean, config: Config diff --git a/src/expr.ts b/src/expr.ts index 06bb922386..ac0242c727 100644 --- a/src/expr.ts +++ b/src/expr.ts @@ -1,6 +1,7 @@ +import {SignalRef, isArray, isObject} from 'vega'; import {signalRefOrValue} from './compile/common'; import {Dict, keys} from './util'; -import {MappedExclude} from './vega.schema'; +import {MappedExclude, SubstituteType} from './vega.schema'; export interface ExprRef { /** @@ -21,3 +22,31 @@ export function replaceExprRef>(index: T) { } return newIndex as MappedExclude; } + +export function deepReplaceExprRef(o: T): SubstituteType { + if (isExprRef(o)) { + const {expr, ...rest} = o; + return { + signal: expr, + ...deepReplaceExprRef(rest) + } as any; + } + if (isArray(o)) { + return o.map(deepReplaceExprRef) as any; + } + + if (isObject(o)) { + const r: Dict = {}; + for (const [k, v] of Object.entries(o)) { + if (k === 'values') { + // ignore iterating into data + r[k] = v; + } else { + r[k] = deepReplaceExprRef(v); + } + } + return r as any; + } + + return o as any; +} diff --git a/src/legend.ts b/src/legend.ts index 421ef5ad38..9c2a30ffc7 100644 --- a/src/legend.ts +++ b/src/legend.ts @@ -190,7 +190,7 @@ export interface LegendEncoding { gradient?: GuideEncodingEntry; } -export const defaultLegendConfig: LegendConfig = { +export const defaultLegendConfig: LegendConfig = { gradientHorizontalMaxLength: 200, gradientHorizontalMinLength: 100, gradientVerticalMaxLength: 200, diff --git a/src/mark.ts b/src/mark.ts index a779ff0421..d8573bcc25 100644 --- a/src/mark.ts +++ b/src/mark.ts @@ -343,7 +343,7 @@ export const VL_ONLY_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX: { tick: ['bandSize', 'thickness'] }; -export const defaultMarkConfig: MarkConfig = { +export const defaultMarkConfig: MarkConfig = { color: '#4c78a8', invalid: 'filter', timeUnitBandSize: 1 @@ -653,14 +653,14 @@ export interface MarkDef = { +export const defaultBarConfig: RectConfig = { binSpacing: 1, continuousBandSize: DEFAULT_RECT_BAND_SIZE, minBandSize: 0.25, timeUnitBandPosition: 0.5 }; -export const defaultRectConfig: RectConfig = { +export const defaultRectConfig: RectConfig = { binSpacing: 0, continuousBandSize: DEFAULT_RECT_BAND_SIZE, minBandSize: 0.25, @@ -677,7 +677,7 @@ export interface TickConfig extends MarkConfig = { +export const defaultTickConfig: TickConfig = { thickness: 1 }; diff --git a/src/projection.ts b/src/projection.ts index 1eca2fed43..cbf86cf36c 100644 --- a/src/projection.ts +++ b/src/projection.ts @@ -25,7 +25,7 @@ export interface Projection /** * Any property of Projection can be in config */ -export type ProjectionConfig = Projection; +export type ProjectionConfig = Projection; export const PROJECTION_PROPERTIES: (keyof Projection)[] = [ 'type', diff --git a/src/scale.ts b/src/scale.ts index e0a0c06e35..06b34cdacd 100644 --- a/src/scale.ts +++ b/src/scale.ts @@ -426,7 +426,7 @@ export interface ScaleConfig { zero?: boolean; } -export const defaultScaleConfig: ScaleConfig = { +export const defaultScaleConfig: ScaleConfig = { pointPadding: 0.5, barBandPaddingInner: 0.1, diff --git a/src/spec/base.ts b/src/spec/base.ts index 578542719a..0a2fe0bce1 100644 --- a/src/spec/base.ts +++ b/src/spec/base.ts @@ -8,7 +8,7 @@ import {Resolve} from '../resolve'; import {TitleParams} from '../title'; import {Transform} from '../transform'; import {Flag, keys} from '../util'; -import {LayoutAlign, RowCol} from '../vega.schema'; +import {LayoutAlign, RowCol, SubstituteType} from '../vega.schema'; import {isConcatSpec, isVConcatSpec} from './concat'; import {isFacetMapping, isFacetSpec} from './facet'; @@ -285,7 +285,7 @@ const COMPOSITION_LAYOUT_PROPERTIES = keys(COMPOSITION_LAYOUT_INDEX); export type SpecType = 'unit' | 'facet' | 'layer' | 'concat'; export function extractCompositionLayout( - spec: NormalizedSpec, + spec: SubstituteType, specType: keyof CompositionConfigMixins, config: CompositionConfigMixins ): GenericCompositionLayoutWithColumns { diff --git a/src/spec/unit.ts b/src/spec/unit.ts index da69818436..41e77da55a 100644 --- a/src/spec/unit.ts +++ b/src/spec/unit.ts @@ -1,3 +1,4 @@ +import {SignalRef} from 'vega'; import {FieldName} from '../channeldef'; import {CompositeEncoding, FacetedCompositeEncoding} from '../compositemark'; import {Encoding} from '../encoding'; @@ -8,6 +9,7 @@ import {SelectionParameter} from '../selection'; import {Field} from './../channeldef'; import {BaseSpec, DataMixins, FrameMixins, GenericCompositionLayout, ResolveMixins} from './base'; import {TopLevel, TopLevelParameter} from './toplevel'; +import {SubstituteType} from '../vega.schema'; /** * Base interface for a unit (single-view) specification. */ @@ -61,6 +63,8 @@ export type FacetedUnitSpec = GenericUn export type TopLevelUnitSpec = TopLevel> & DataMixins; -export function isUnitSpec(spec: BaseSpec): spec is FacetedUnitSpec | NormalizedUnitSpec { +export function isUnitSpec( + spec: BaseSpec +): spec is SubstituteType, ExprRef, ES> | SubstituteType { return 'mark' in spec; } diff --git a/src/vega.schema.ts b/src/vega.schema.ts index 01dd369167..724cb02be4 100644 --- a/src/vega.schema.ts +++ b/src/vega.schema.ts @@ -54,12 +54,28 @@ export type MappedExclude = { [P in keyof T]: Exclude; }; +export type DeepExclude = T extends U + ? never + : // eslint-disable-next-line @typescript-eslint/ban-types + T extends object + ? { + [K in keyof T]: DeepExclude; + } + : T; + +export type SubstituteType = T extends A + ? B + : // eslint-disable-next-line @typescript-eslint/ban-types + T extends object + ? {[K in keyof T]: SubstituteType} + : T; + export type MapExcludeAndKeepSignalAs = { [P in keyof T]: SignalRef extends T[P] ? Exclude | S : Exclude; }; // Remove ValueRefs from mapped types -export type MappedExcludeValueRef = MappedExclude | NumericValueRef | ColorValueRef>; +export type MappedExcludeValueRef = DeepExclude | NumericValueRef | ColorValueRef>; export type MapExcludeValueRefAndReplaceSignalWith = MapExcludeAndKeepSignalAs< T, diff --git a/test/expr.test.ts b/test/expr.test.ts new file mode 100644 index 0000000000..6866be28bd --- /dev/null +++ b/test/expr.test.ts @@ -0,0 +1,16 @@ +import {deepReplaceExprRef} from '../src/expr'; + +describe('expr', () => { + describe('deepReplaceExprRef', () => { + it('should replace expression refs', () => { + expect(deepReplaceExprRef({expr: 'foo'})).toEqual({signal: 42}); + expect(deepReplaceExprRef({foo: {bar: {expr: 'foo'}}})).toEqual({foo: {bar: {signal: 42}}}); + expect(deepReplaceExprRef({foo: {expr: 'foo'}, bar: 12})).toEqual({foo: {signal: 42}, bar: 12}); + expect(deepReplaceExprRef([{expr: 'foo'}])).toEqual([{signal: 42}]); + }); + + it('should ignore values to avoid expensive recursion', () => { + expect(deepReplaceExprRef({values: {expr: 'foo'}})).toEqual({values: {expr: 'foo'}}); + }); + }); +});