vega-lite
Version:
Vega-Lite is a concise high-level language for interactive visualization.
398 lines (346 loc) • 13.4 kB
text/typescript
import type {SignalRef} from 'vega';
import {isArray} from 'vega-util';
import {COLUMN, FACET, ROW} from '../channel.js';
import {Field, FieldName, hasConditionalFieldOrDatumDef, isFieldOrDatumDef, isValueDef} from '../channeldef.js';
import {SharedCompositeEncoding} from '../compositemark/index.js';
import {boxPlotNormalizer} from '../compositemark/boxplot.js';
import {errorBandNormalizer} from '../compositemark/errorband.js';
import {errorBarNormalizer} from '../compositemark/errorbar.js';
import {channelHasField, Encoding} from '../encoding.js';
import {ExprRef} from '../expr.js';
import * as log from '../log/index.js';
import {Projection} from '../projection.js';
import {FacetedUnitSpec, GenericSpec, LayerSpec, UnitSpec} from '../spec/index.js';
import {GenericCompositionLayoutWithColumns} from '../spec/base.js';
import {GenericConcatSpec} from '../spec/concat.js';
import {
FacetEncodingFieldDef,
FacetFieldDef,
FacetMapping,
GenericFacetSpec,
isFacetMapping,
NormalizedFacetSpec,
} from '../spec/facet.js';
import {NormalizedSpec} from '../spec/index.js';
import {NormalizedLayerSpec} from '../spec/layer.js';
import {SpecMapper} from '../spec/map.js';
import {isLayerRepeatSpec, LayerRepeatSpec, NonLayerRepeatSpec, RepeatSpec} from '../spec/repeat.js';
import {isUnitSpec, NormalizedUnitSpec} from '../spec/unit.js';
import {isEmpty, keys, omit, varName} from '../util.js';
import {isSignalRef} from '../vega.schema.js';
import {NonFacetUnitNormalizer, NormalizerParams} from './base.js';
import {PathOverlayNormalizer} from './pathoverlay.js';
import {replaceRepeaterInEncoding, replaceRepeaterInFacet} from './repeater.js';
import {RuleForRangedLineNormalizer} from './ruleforrangedline.js';
export class CoreNormalizer extends SpecMapper<NormalizerParams, FacetedUnitSpec<Field>, LayerSpec<Field>> {
private nonFacetUnitNormalizers: NonFacetUnitNormalizer<any>[] = [
boxPlotNormalizer,
errorBarNormalizer,
errorBandNormalizer,
new PathOverlayNormalizer(),
new RuleForRangedLineNormalizer(),
];
public map(spec: GenericSpec<FacetedUnitSpec<Field>, LayerSpec<Field>, RepeatSpec, Field>, params: NormalizerParams) {
// Special handling for a faceted unit spec as it can return a facet spec, not just a layer or unit spec like a normal unit spec.
if (isUnitSpec(spec)) {
const hasRow = channelHasField(spec.encoding, ROW);
const hasColumn = channelHasField(spec.encoding, COLUMN);
const hasFacet = channelHasField(spec.encoding, FACET);
if (hasRow || hasColumn || hasFacet) {
return this.mapFacetedUnit(spec, params);
}
}
return super.map(spec, params);
}
// This is for normalizing non-facet unit
public mapUnit(spec: UnitSpec<Field>, params: NormalizerParams): NormalizedUnitSpec | NormalizedLayerSpec {
const {parentEncoding, parentProjection} = params;
const encoding = replaceRepeaterInEncoding(spec.encoding, params.repeater);
const specWithReplacedEncoding = {
...spec,
...(spec.name ? {name: [params.repeaterPrefix, spec.name].filter((n) => n).join('_')} : {}),
...(encoding ? {encoding} : {}),
};
if (parentEncoding || parentProjection) {
return this.mapUnitWithParentEncodingOrProjection(specWithReplacedEncoding, params);
}
const normalizeLayerOrUnit = this.mapLayerOrUnit.bind(this);
for (const unitNormalizer of this.nonFacetUnitNormalizers) {
if (unitNormalizer.hasMatchingType(specWithReplacedEncoding, params.config)) {
return unitNormalizer.run(specWithReplacedEncoding, params, normalizeLayerOrUnit);
}
}
return specWithReplacedEncoding as NormalizedUnitSpec;
}
protected mapRepeat(
spec: RepeatSpec,
params: NormalizerParams,
): GenericConcatSpec<NormalizedSpec> | NormalizedLayerSpec {
if (isLayerRepeatSpec(spec)) {
return this.mapLayerRepeat(spec, params);
} else {
return this.mapNonLayerRepeat(spec, params);
}
}
private mapLayerRepeat(
spec: LayerRepeatSpec,
params: NormalizerParams,
): GenericConcatSpec<NormalizedSpec> | NormalizedLayerSpec {
const {repeat, spec: childSpec, ...rest} = spec;
const {row, column, layer} = repeat;
const {repeater = {}, repeaterPrefix = ''} = params;
if (row || column) {
return this.mapRepeat(
{
...spec,
repeat: {
...(row ? {row} : {}),
...(column ? {column} : {}),
},
spec: {
repeat: {layer},
spec: childSpec,
},
},
params,
);
} else {
return {
...rest,
layer: layer.map((layerValue) => {
const childRepeater = {
...repeater,
layer: layerValue,
};
const childName = `${(childSpec.name ? `${childSpec.name}_` : '') + repeaterPrefix}child__layer_${varName(
layerValue,
)}`;
const child = this.mapLayerOrUnit(childSpec, {...params, repeater: childRepeater, repeaterPrefix: childName});
child.name = childName;
return child;
}),
};
}
}
private mapNonLayerRepeat(spec: NonLayerRepeatSpec, params: NormalizerParams): GenericConcatSpec<NormalizedSpec> {
const {repeat, spec: childSpec, data, ...remainingProperties} = spec;
if (!isArray(repeat) && spec.columns) {
// is repeat with row/column
spec = omit(spec, ['columns']);
log.warn(log.message.columnsNotSupportByRowCol('repeat'));
}
const concat: NormalizedSpec[] = [];
const {repeater = {}, repeaterPrefix = ''} = params;
const row = (!isArray(repeat) && repeat.row) || [repeater ? repeater.row : null];
const column = (!isArray(repeat) && repeat.column) || [repeater ? repeater.column : null];
const repeatValues = (isArray(repeat) && repeat) || [repeater ? repeater.repeat : null];
// cross product
for (const repeatValue of repeatValues) {
for (const rowValue of row) {
for (const columnValue of column) {
const childRepeater = {
repeat: repeatValue,
row: rowValue,
column: columnValue,
layer: repeater.layer,
};
const childName = `${(childSpec.name ? `${childSpec.name}_` : '') + repeaterPrefix}child__${
isArray(repeat)
? `${varName(repeatValue)}`
: (repeat.row ? `row_${varName(rowValue)}` : '') + (repeat.column ? `column_${varName(columnValue)}` : '')
}`;
const child = this.map(childSpec, {...params, repeater: childRepeater, repeaterPrefix: childName});
child.name = childName;
// we move data up
concat.push(omit(child, ['data']) as NormalizedSpec);
}
}
}
const columns = isArray(repeat) ? spec.columns : repeat.column ? repeat.column.length : 1;
return {
data: childSpec.data ?? data, // data from child spec should have precedence
align: 'all',
...remainingProperties,
columns,
concat,
};
}
protected mapFacet(
spec: GenericFacetSpec<UnitSpec<Field>, LayerSpec<Field>, Field>,
params: NormalizerParams,
): GenericFacetSpec<NormalizedUnitSpec, NormalizedLayerSpec, FieldName> {
const {facet} = spec;
if (isFacetMapping(facet) && spec.columns) {
// is facet with row/column
spec = omit(spec, ['columns']);
log.warn(log.message.columnsNotSupportByRowCol('facet'));
}
return super.mapFacet(spec, params);
}
private mapUnitWithParentEncodingOrProjection(
spec: FacetedUnitSpec<Field>,
params: NormalizerParams,
): NormalizedUnitSpec | NormalizedLayerSpec {
const {encoding, projection} = spec;
const {parentEncoding, parentProjection, config} = params;
const mergedProjection = mergeProjection({parentProjection, projection});
const mergedEncoding = mergeEncoding({
parentEncoding,
encoding: replaceRepeaterInEncoding(encoding, params.repeater),
});
return this.mapUnit(
{
...spec,
...(mergedProjection ? {projection: mergedProjection} : {}),
...(mergedEncoding ? {encoding: mergedEncoding} : {}),
},
{config},
);
}
private mapFacetedUnit(spec: FacetedUnitSpec<Field>, normParams: NormalizerParams): NormalizedFacetSpec {
// New encoding in the inside spec should not contain row / column
// as row/column should be moved to facet
const {row, column, facet, ...encoding} = spec.encoding;
// Mark and encoding should be moved into the inner spec
const {mark, width, projection, height, view, params, encoding: _, ...outerSpec} = spec;
const {facetMapping, layout} = this.getFacetMappingAndLayout({row, column, facet}, normParams);
const newEncoding = replaceRepeaterInEncoding(encoding, normParams.repeater);
return this.mapFacet(
{
...outerSpec,
...layout,
// row / column has higher precedence than facet
facet: facetMapping,
spec: {
...(width ? {width} : {}),
...(height ? {height} : {}),
...(view ? {view} : {}),
...(projection ? {projection} : {}),
mark,
encoding: newEncoding,
...(params ? {params} : {}),
},
},
normParams,
);
}
private getFacetMappingAndLayout(
facets: {
row: FacetEncodingFieldDef<Field>;
column: FacetEncodingFieldDef<Field>;
facet: FacetEncodingFieldDef<Field>;
},
params: NormalizerParams,
): {facetMapping: FacetMapping<FieldName> | FacetFieldDef<FieldName>; layout: GenericCompositionLayoutWithColumns} {
const {row, column, facet} = facets;
if (row || column) {
if (facet) {
log.warn(log.message.facetChannelDropped([...(row ? [ROW] : []), ...(column ? [COLUMN] : [])]));
}
const facetMapping: any = {};
const layout: any = {};
for (const channel of [ROW, COLUMN]) {
const def = facets[channel];
if (def) {
const {align, center, spacing, columns, ...defWithoutLayout} = def;
facetMapping[channel] = defWithoutLayout;
for (const prop of ['align', 'center', 'spacing'] as const) {
if (def[prop] !== undefined) {
layout[prop] ??= {};
layout[prop][channel] = def[prop];
}
}
}
}
return {facetMapping, layout};
} else {
const {align, center, spacing, columns, ...facetMapping} = facet;
return {
facetMapping: replaceRepeaterInFacet(facetMapping, params.repeater),
layout: {
...(align ? {align} : {}),
...(center ? {center} : {}),
...(spacing ? {spacing} : {}),
...(columns ? {columns} : {}),
},
};
}
}
public mapLayer(
spec: LayerSpec<Field>,
{parentEncoding, parentProjection, ...otherParams}: NormalizerParams,
): NormalizedLayerSpec {
// Special handling for extended layer spec
const {encoding, projection, ...rest} = spec;
const params: NormalizerParams = {
...otherParams,
parentEncoding: mergeEncoding({parentEncoding, encoding, layer: true}),
parentProjection: mergeProjection({parentProjection, projection}),
};
return super.mapLayer(
{
...rest,
...(spec.name ? {name: [params.repeaterPrefix, spec.name].filter((n) => n).join('_')} : {}),
},
params,
);
}
}
function mergeEncoding({
parentEncoding,
encoding = {},
layer,
}: {
parentEncoding: SharedCompositeEncoding<any>;
encoding: SharedCompositeEncoding<any> | Encoding<any>;
layer?: boolean;
}): Encoding<any> {
let merged: any = {};
if (parentEncoding) {
const channels = new Set([...keys(parentEncoding), ...keys(encoding)]);
for (const channel of channels) {
const channelDef = (encoding as any)[channel];
const parentChannelDef = parentEncoding[channel];
if (isFieldOrDatumDef(channelDef)) {
// Field/Datum Def can inherit properties from its parent
// Note that parentChannelDef doesn't have to be a field/datum def if the channelDef is already one.
const mergedChannelDef = {
...parentChannelDef,
...channelDef,
};
merged[channel] = mergedChannelDef;
} else if (hasConditionalFieldOrDatumDef(channelDef)) {
merged[channel] = {
...channelDef,
condition: {
...parentChannelDef,
...channelDef.condition,
},
};
} else if (channelDef || channelDef === null) {
merged[channel] = channelDef;
} else if (
layer ||
isValueDef(parentChannelDef) ||
isSignalRef(parentChannelDef) ||
isFieldOrDatumDef(parentChannelDef) ||
isArray(parentChannelDef)
) {
merged[channel] = parentChannelDef;
}
}
} else {
merged = encoding;
}
return !merged || isEmpty(merged) ? undefined : merged;
}
function mergeProjection<ES extends ExprRef | SignalRef>(opt: {
parentProjection: Projection<ES>;
projection: Projection<ES>;
}) {
const {parentProjection, projection} = opt;
if (parentProjection && projection) {
log.warn(log.message.projectionOverridden({parentProjection, projection}));
}
return projection ?? parentProjection;
}