import {AggregateOp} from 'vega';import {array, isArray} from 'vega-util';import {isArgmaxDef, isArgminDef} from './aggregate';import {isBinned, isBinning} from './bin';import { ANGLE, CHANNELS, COLOR, DESCRIPTION, DETAIL, FILL, FILLOPACITY, HREF, isChannel, isNonPositionScaleChannel, isSecondaryRangeChannel, isXorY, KEY, LATITUDE, LATITUDE2, LONGITUDE, LONGITUDE2, OPACITY, ORDER, RADIUS, RADIUS2, SHAPE, SIZE, STROKE, STROKEDASH, STROKEOPACITY, STROKEWIDTH, supportMark, TEXT, THETA, THETA2, TOOLTIP, URL, X, X2, Y, Y2, Channel} from './channel';import { binRequiresRange, ChannelDef, ColorDef, Field, FieldDef, FieldDefWithoutScale, getFieldDef, getGuide, hasConditionalFieldDef, initChannelDef, initFieldDef, isConditionalDef, isDatumDef, isFieldDef, isTypedFieldDef, isValueDef, LatLongDef, NumericArrayMarkPropDef, NumericMarkPropDef, OrderFieldDef, OrderValueDef, PolarDef, Position2Def, PositionDef, SecondaryFieldDef, ShapeDef, StringFieldDef, StringFieldDefWithCondition, StringValueDefWithCondition, TextDef, title, TypedFieldDef, vgField} from './channeldef';import {Config} from './config';import * as log from './log';import {Mark} from './mark';import {EncodingFacetMapping} from './spec/facet';import {AggregatedFieldDef, BinTransform, TimeUnitTransform} from './transform';import {QUANTITATIVE, TEMPORAL} from './type';import {keys, some} from './util';import {isSignalRef} from './vega.schema';
export interface Encoding<F extends Field> { x?: PositionDef<F>;
y?: PositionDef<F>;
x2?: Position2Def<F>;
y2?: Position2Def<F>;
longitude?: LatLongDef<F>;
latitude?: LatLongDef<F>;
longitude2?: Position2Def<F>;
latitude2?: Position2Def<F>;
theta?: PolarDef<F>;
theta2?: Position2Def<F>;
radius?: PolarDef<F>;
radius2?: Position2Def<F>;
color?: ColorDef<F>;
fill?: ColorDef<F>;
stroke?: ColorDef<F>;
opacity?: NumericMarkPropDef<F>;
fillOpacity?: NumericMarkPropDef<F>;
strokeOpacity?: NumericMarkPropDef<F>;
strokeWidth?: NumericMarkPropDef<F>;
strokeDash?: NumericArrayMarkPropDef<F>;
size?: NumericMarkPropDef<F>;
angle?: NumericMarkPropDef<F>;
shape?: ShapeDef<F>; detail?: FieldDefWithoutScale<F> | FieldDefWithoutScale<F>[];
key?: FieldDefWithoutScale<F>;
text?: TextDef<F>;
tooltip?: StringFieldDefWithCondition<F> | StringValueDefWithCondition<F> | StringFieldDef<F>[] | null;
href?: StringFieldDefWithCondition<F> | StringValueDefWithCondition<F>;
url?: StringFieldDefWithCondition<F> | StringValueDefWithCondition<F>;
description?: StringFieldDefWithCondition<F> | StringValueDefWithCondition<F>;
order?: OrderFieldDef<F> | OrderFieldDef<F>[] | OrderValueDef;}
export interface EncodingWithFacet<F extends Field> extends Encoding<F>, EncodingFacetMapping<F> {}
export function channelHasField<F extends Field>( encoding: EncodingWithFacet<F>, channel: keyof EncodingWithFacet<F>): boolean { const channelDef = encoding && encoding[channel]; if (channelDef) { if (isArray(channelDef)) { return some(channelDef, fieldDef => !!fieldDef.field); } else { return isFieldDef(channelDef) || hasConditionalFieldDef<Field>(channelDef); } } return false;}
export function isAggregate(encoding: EncodingWithFacet<any>) { return some(CHANNELS, channel => { if (channelHasField(encoding, channel)) { const channelDef = encoding[channel]; if (isArray(channelDef)) { return some(channelDef, fieldDef => !!fieldDef.aggregate); } else { const fieldDef = getFieldDef(channelDef); return fieldDef && !!fieldDef.aggregate; } } return false; });}
export function extractTransformsFromEncoding(oldEncoding: Encoding<any>, config: Config) { const groupby: string[] = []; const bins: BinTransform[] = []; const timeUnits: TimeUnitTransform[] = []; const aggregate: AggregatedFieldDef[] = []; const encoding: Encoding<string> = {};
forEach(oldEncoding, (channelDef, channel) => { if (isFieldDef(channelDef)) { const {field, aggregate: aggOp, bin, timeUnit, ...remaining} = channelDef; if (aggOp || timeUnit || bin) { const guide = getGuide(channelDef); const isTitleDefined = guide && guide.title; let newField = vgField(channelDef, {forAs: true}); const newFieldDef: FieldDef<string> = { ...(isTitleDefined ? [] : {title: title(channelDef, config, {allowDisabling: true})}), ...remaining, field: newField };
if (aggOp) { let op: AggregateOp;
if (isArgmaxDef(aggOp)) { op = 'argmax'; newField = vgField({op: 'argmax', field: aggOp.argmax}, {forAs: true}); newFieldDef.field = `${newField}.${field}`; } else if (isArgminDef(aggOp)) { op = 'argmin'; newField = vgField({op: 'argmin', field: aggOp.argmin}, {forAs: true}); newFieldDef.field = `${newField}.${field}`; } else if (aggOp !== 'boxplot' && aggOp !== 'errorbar' && aggOp !== 'errorband') { op = aggOp; }
if (op) { const aggregateEntry: AggregatedFieldDef = { op, as: newField }; if (field) { aggregateEntry.field = field; } aggregate.push(aggregateEntry); } } else { groupby.push(newField); if (isTypedFieldDef(channelDef) && isBinning(bin)) { bins.push({bin, field, as: newField}); groupby.push(vgField(channelDef, {binSuffix: 'end'})); if (binRequiresRange(channelDef, channel)) { groupby.push(vgField(channelDef, {binSuffix: 'range'})); } if (isXorY(channel)) { const secondaryChannel: SecondaryFieldDef<string> = { field: newField + '_end' }; encoding[channel + '2'] = secondaryChannel; } newFieldDef.bin = 'binned'; if (!isSecondaryRangeChannel(channel)) { newFieldDef['type'] = QUANTITATIVE; } } else if (timeUnit) { timeUnits.push({ timeUnit, field, as: newField });
const formatType = isTypedFieldDef(channelDef) && channelDef.type !== TEMPORAL && 'time'; if (formatType) { if (channel === TEXT || channel === TOOLTIP) { newFieldDef['formatType'] = formatType; } else if (isNonPositionScaleChannel(channel)) { newFieldDef['legend'] = { formatType, ...newFieldDef['legend'] }; } else if (isXorY(channel)) { newFieldDef['axis'] = { formatType, ...newFieldDef['axis'] }; } } } }
encoding[channel as any] = newFieldDef; } else { groupby.push(field); encoding[channel as any] = oldEncoding[channel]; } } else { encoding[channel as any] = oldEncoding[channel]; } });
return { bins, timeUnits, aggregate, groupby, encoding };}
export function markChannelCompatible(encoding: Encoding<string>, channel: Channel, mark: Mark) { const markSupported = supportMark(channel, mark); if (!markSupported) { return false; } else if (markSupported === 'binned') { const primaryFieldDef = encoding[channel === X2 ? X : Y];
if (isFieldDef(primaryFieldDef) && isFieldDef(encoding[channel]) && isBinned(primaryFieldDef.bin)) { return true; } else { return false; } } return true;}
export function initEncoding( encoding: Encoding<string>, mark: Mark, filled: boolean, config: Config): Encoding<string> { return keys(encoding).reduce((normalizedEncoding: Encoding<string>, channel: Channel) => { if (!isChannel(channel)) { log.warn(log.message.invalidEncodingChannel(channel)); return normalizedEncoding; }
const channelDef = encoding[channel]; if (channel === 'angle' && mark === 'arc' && !encoding.theta) { log.warn(log.message.REPLACE_ANGLE_WITH_THETA); channel = THETA; }
if (!markChannelCompatible(encoding, channel, mark)) { log.warn(log.message.incompatibleChannel(channel, mark)); return normalizedEncoding; }
if (channel === SIZE && mark === 'line') { const fieldDef = getFieldDef(encoding[channel]); if (fieldDef?.aggregate) { log.warn(log.message.LINE_WITH_VARYING_SIZE); return normalizedEncoding; } }
if (channel === COLOR && (filled ? 'fill' in encoding : 'stroke' in encoding)) { log.warn(log.message.droppingColor('encoding', {fill: 'fill' in encoding, stroke: 'stroke' in encoding})); return normalizedEncoding; }
if ( channel === DETAIL || (channel === ORDER && !isArray(channelDef) && !isValueDef(channelDef)) || (channel === TOOLTIP && isArray(channelDef)) ) { if (channelDef) { (normalizedEncoding[channel] as any) = array(channelDef).reduce( (defs: FieldDef<string>[], fieldDef: FieldDef<string>) => { if (!isFieldDef(fieldDef)) { log.warn(log.message.emptyFieldDef(fieldDef, channel)); } else { defs.push(initFieldDef(fieldDef, channel)); } return defs; }, [] ); } } else { if (channel === TOOLTIP && channelDef === null) { normalizedEncoding[channel] = null; } else if ( !isFieldDef(channelDef) && !isDatumDef(channelDef) && !isValueDef(channelDef) && !isConditionalDef(channelDef) && !isSignalRef(channelDef) ) { log.warn(log.message.emptyFieldDef(channelDef, channel)); return normalizedEncoding; }
normalizedEncoding[channel as any] = initChannelDef(channelDef as ChannelDef, channel, config); } return normalizedEncoding; }, {});}
export function normalizeEncoding(encoding: Encoding<string>, config: Config): Encoding<string> { const normalizedEncoding: Encoding<string> = {};
for (const channel of keys(encoding)) { const newChannelDef = initChannelDef(encoding[channel], channel, config, {compositeMark: true}); normalizedEncoding[channel as any] = newChannelDef; }
return normalizedEncoding;}
export function fieldDefs<F extends Field>(encoding: EncodingWithFacet<F>): FieldDef<F>[] { const arr: FieldDef<F>[] = []; for (const channel of keys(encoding)) { if (channelHasField(encoding, channel)) { const channelDef = encoding[channel]; const channelDefArray = array(channelDef); for (const def of channelDefArray) { if (isFieldDef(def)) { arr.push(def); } else if (hasConditionalFieldDef<F>(def)) { arr.push(def.condition); } } } } return arr;}
export function forEach<U extends Record<any, any>>( mapping: U, f: (cd: ChannelDef, c: keyof U) => void, thisArg?: any) { if (!mapping) { return; }
for (const channel of keys(mapping)) { const el = mapping[channel]; if (isArray(el)) { for (const channelDef of el as unknown[]) { f.call(thisArg, channelDef, channel); } } else { f.call(thisArg, el, channel); } }}
export function reduce<T, U extends Record<any, any>>( mapping: U, f: (acc: any, fd: TypedFieldDef<string>, c: keyof U) => U, init: T, thisArg?: any) { if (!mapping) { return init; }
return keys(mapping).reduce((r, channel) => { const map = mapping[channel]; if (isArray(map)) { return map.reduce((r1: T, channelDef: ChannelDef) => { return f.call(thisArg, r1, channelDef, channel); }, r); } else { return f.call(thisArg, r, map, channel); } }, init);}
export function pathGroupingFields(mark: Mark, encoding: Encoding<string>): string[] { return keys(encoding).reduce((details, channel) => { switch (channel) { case X: case Y: case HREF: case DESCRIPTION: case URL: case X2: case Y2: case THETA: case THETA2: case RADIUS: case RADIUS2:
case LATITUDE: case LONGITUDE: case LATITUDE2: case LONGITUDE2:
case TEXT: case SHAPE: case ANGLE:
case TOOLTIP: return details;
case ORDER: if (mark === 'line' || mark === 'trail') { return details; }
case DETAIL: case KEY: { const channelDef = encoding[channel]; if (isArray(channelDef) || isFieldDef(channelDef)) { for (const fieldDef of array(channelDef)) { if (!fieldDef.aggregate) { details.push(vgField(fieldDef, {})); } } } return details; }
case SIZE: if (mark === 'trail') { return details; }
case COLOR: case FILL: case STROKE: case OPACITY: case FILLOPACITY: case STROKEOPACITY: case STROKEDASH: case STROKEWIDTH: {
const fieldDef = getFieldDef<string>(encoding[channel]); if (fieldDef && !fieldDef.aggregate) { details.push(vgField(fieldDef, {})); } return details; } } }, []);}