import {Color, InitSignal, NewSignal, RangeConfig, RangeScheme, SignalRef} from 'vega';import {isObject, mergeConfig} from 'vega-util';import {Axis, AxisConfig, AxisConfigMixins, AXIS_CONFIGS, isConditionalAxisValue} from './axis';import {signalOrValueRefWithCondition, signalRefOrValue} from './compile/common';import {CompositeMarkConfigMixins, getAllCompositeMarks} from './compositemark';import {ExprOrSignalRef, ExprRef, replaceExprRefInIndex} from './expr';import {VL_ONLY_LEGEND_CONFIG} from './guide';import {HeaderConfigMixins, HEADER_CONFIGS} from './header';import {defaultLegendConfig, LegendConfig} from './legend';import * as mark from './mark';import { AnyMarkConfig, Mark, MarkConfig, MarkConfigMixins, MARK_CONFIGS, PRIMITIVE_MARKS, VL_ONLY_MARK_CONFIG_PROPERTIES, VL_ONLY_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX} from './mark';import {assembleParameterSignals} from './parameter';import {ProjectionConfig} from './projection';import {defaultScaleConfig, ScaleConfig} from './scale';import {defaultConfig as defaultSelectionConfig, SelectionConfig} from './selection';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';
export interface ViewConfig<ES extends ExprRef | SignalRef> extends BaseViewBackground<ES> { width?: number;
height?: number;
continuousWidth?: number;
discreteWidth?: number | {step: number}; continuousHeight?: number;
discreteHeight?: number | {step: number};
step?: number;
clip?: boolean;}
export function getViewConfigContinuousSize<ES extends ExprRef | SignalRef>( viewConfig: ViewConfig<ES>, channel: 'width' | 'height') { return viewConfig[channel] ?? viewConfig[channel === 'width' ? 'continuousWidth' : 'continuousHeight']; }
export function getViewConfigDiscreteStep<ES extends ExprRef | SignalRef>( viewConfig: ViewConfig<ES>, channel: 'width' | 'height') { const size = getViewConfigDiscreteSize(viewConfig, channel); return isStep(size) ? size.step : DEFAULT_STEP;}
export function getViewConfigDiscreteSize<ES extends ExprRef | SignalRef>( viewConfig: ViewConfig<ES>, channel: 'width' | 'height') { const size = viewConfig[channel] ?? viewConfig[channel === 'width' ? 'discreteWidth' : 'discreteHeight']; return getFirstDefined(size, {step: viewConfig.step});}
export const DEFAULT_STEP = 20;
export const defaultViewConfig: ViewConfig<SignalRef> = { continuousWidth: 200, continuousHeight: 200, step: DEFAULT_STEP};
export function isVgScheme(rangeScheme: string[] | RangeScheme): rangeScheme is RangeScheme { return rangeScheme && !!rangeScheme['scheme'];}
export type ColorConfig = Record<string, Color>;
export type FontSizeConfig = Record<string, number>;
export interface VLOnlyConfig<ES extends ExprRef | SignalRef> { font?: string;
color?: boolean | ColorConfig;
fontSize?: boolean | FontSizeConfig;
countTitle?: string;
fieldTitle?: 'verbal' | 'functional' | 'plain';
numberFormat?: string;
timeFormat?: string;
customFormatTypes?: boolean;
view?: ViewConfig<ES>;
scale?: ScaleConfig<ES>;
selection?: SelectionConfig;}
export type StyleConfigIndex<ES extends ExprRef | SignalRef> = Partial<Record<string, AnyMarkConfig<ES> | Axis<ES>>> & MarkConfigMixins<ES> & { 'guide-title'?: MarkConfig<ES>;
'guide-label'?: MarkConfig<ES>;
'group-title'?: MarkConfig<ES>;
'group-subtitle'?: MarkConfig<ES>; };
export interface Config<ES extends ExprRef | SignalRef = ExprRef | SignalRef> extends TopLevelProperties<ES>, VLOnlyConfig<ES>, MarkConfigMixins<ES>, CompositeMarkConfigMixins, AxisConfigMixins<ES>, HeaderConfigMixins<ES>, CompositionConfigMixins { range?: RangeConfig;
legend?: LegendConfig<ES>;
title?: TitleConfig<ES>;
projection?: ProjectionConfig;
style?: StyleConfigIndex<ES>;
lineBreak?: string | ES;
aria?: boolean;
signals?: (InitSignal | NewSignal)[];}
export const defaultConfig: Config<SignalRef> = { background: 'white',
padding: 5, timeFormat: '%b %d, %Y', countTitle: 'Count of Records',
view: defaultViewConfig,
mark: mark.defaultMarkConfig,
arc: {}, area: {}, bar: mark.defaultBarConfig, circle: {}, geoshape: {}, image: {}, line: {}, point: {}, rect: mark.defaultRectConfig, rule: {color: 'black'}, square: {}, text: {color: 'black'}, tick: mark.defaultTickConfig, trail: {},
boxplot: { size: 14, extent: 1.5, box: {}, median: {color: 'white'}, outliers: {}, rule: {}, ticks: null },
errorbar: { center: 'mean', rule: true, ticks: false },
errorband: { band: { opacity: 0.3 }, borders: false },
scale: defaultScaleConfig,
projection: {},
legend: defaultLegendConfig, header: {titlePadding: 10, labelPadding: 10}, headerColumn: {}, headerRow: {}, headerFacet: {},
selection: defaultSelectionConfig, style: {},
title: {},
facet: {spacing: DEFAULT_SPACING}, concat: {spacing: DEFAULT_SPACING}};
const tab10 = [ '#4c78a8', '#f58518', '#e45756', '#72b7b2', '#54a24b', '#eeca3b', '#b279a2', '#ff9da6', '#9d755d', '#bab0ac'];
export const DEFAULT_FONT_SIZE = { text: 11, guideLabel: 10, guideTitle: 11, groupTitle: 13, groupSubtitle: 12};
export const DEFAULT_COLOR = { blue: tab10[0], orange: tab10[1], red: tab10[2], teal: tab10[3], green: tab10[4], yellow: tab10[5], purple: tab10[6], pink: tab10[7], brown: tab10[8], gray0: '#000', gray1: '#111', gray2: '#222', gray3: '#333', gray4: '#444', gray5: '#555', gray6: '#666', gray7: '#777', gray8: '#888', gray9: '#999', gray10: '#aaa', gray11: '#bbb', gray12: '#ccc', gray13: '#ddd', gray14: '#eee', gray15: '#fff'};
export function colorSignalConfig(color: boolean | ColorConfig = {}): Config { return { signals: [ { name: 'color', value: isObject(color) ? {...DEFAULT_COLOR, ...color} : DEFAULT_COLOR } ], mark: {color: {signal: 'color.blue'}}, rule: {color: {signal: 'color.gray0'}}, text: { color: {signal: 'color.gray0'} }, style: { 'guide-label': { fill: {signal: 'color.gray0'} }, 'guide-title': { fill: {signal: 'color.gray0'} }, 'group-title': { fill: {signal: 'color.gray0'} }, 'group-subtitle': { fill: {signal: 'color.gray0'} }, cell: { stroke: {signal: 'color.gray8'} } }, axis: { domainColor: {signal: 'color.gray13'}, gridColor: {signal: 'color.gray8'}, tickColor: {signal: 'color.gray13'} }, range: { category: [ {signal: 'color.blue'}, {signal: 'color.orange'}, {signal: 'color.red'}, {signal: 'color.teal'}, {signal: 'color.green'}, {signal: 'color.yellow'}, {signal: 'color.purple'}, {signal: 'color.pink'}, {signal: 'color.brown'}, {signal: 'color.grey8'} ] } };}
export function fontSizeSignalConfig(fontSize: boolean | FontSizeConfig): Config { return { signals: [ { name: 'fontSize', value: isObject(fontSize) ? {...DEFAULT_FONT_SIZE, ...fontSize} : DEFAULT_FONT_SIZE } ], text: { fontSize: {signal: 'fontSize.text'} }, style: { 'guide-label': { fontSize: {signal: 'fontSize.guideLabel'} }, 'guide-title': { fontSize: {signal: 'fontSize.guideTitle'} }, 'group-title': { fontSize: {signal: 'fontSize.groupTitle'} }, 'group-subtitle': { fontSize: {signal: 'fontSize.groupSubtitle'} } } };}
export function fontConfig(font: string): Config { return { text: {font}, style: { 'guide-label': {font}, 'guide-title': {font}, 'group-title': {font}, 'group-subtitle': {font} } };}
function getAxisConfigInternal(axisConfig: AxisConfig<ExprOrSignalRef>) { const props = keys(axisConfig || {}); const axisConfigInternal: AxisConfig<SignalRef> = {}; for (const prop of props) { const val = axisConfig[prop]; axisConfigInternal[prop as any] = isConditionalAxisValue<any, ExprOrSignalRef>(val) ? signalOrValueRefWithCondition<any>(val) : signalRefOrValue(val); } return axisConfigInternal;}
function getStyleConfigInternal(styleConfig: StyleConfigIndex<ExprOrSignalRef>) { const props = keys(styleConfig);
const styleConfigInternal: StyleConfigIndex<SignalRef> = {}; for (const prop of props) { 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;
export function initConfig(specifiedConfig: Config = {}): Config<SignalRef> { const {color, font, fontSize, ...restConfig} = specifiedConfig; const mergedConfig = mergeConfig( {}, defaultConfig, font ? fontConfig(font) : {}, color ? colorSignalConfig(color) : {}, fontSize ? fontSizeSignalConfig(fontSize) : {}, restConfig || {} ); const outputConfig: Config<SignalRef> = 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]) { outputConfig[markConfigType] = replaceExprRefInIndex(mergedConfig[markConfigType]); } }
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] = replaceExprRefInIndex(mergedConfig[headerConfigType]); } }
if (mergedConfig.legend) { outputConfig.legend = replaceExprRefInIndex(mergedConfig.legend); }
if (mergedConfig.scale) { outputConfig.scale = replaceExprRefInIndex(mergedConfig.scale); }
if (mergedConfig.style) { outputConfig.style = getStyleConfigInternal(mergedConfig.style); }
if (mergedConfig.title) { outputConfig.title = replaceExprRefInIndex(mergedConfig.title); }
if (mergedConfig.view) { outputConfig.view = replaceExprRefInIndex(mergedConfig.view); }
return outputConfig;}
const MARK_STYLES = ['view', ...PRIMITIVE_MARKS] as ('view' | Mark)[];
const VL_ONLY_CONFIG_PROPERTIES: (keyof Config)[] = [ 'color', 'fontSize', 'background', 'padding', 'facet', 'concat', 'numberFormat', 'timeFormat', 'countTitle', 'header',
'axisQuantitative', 'axisTemporal', 'axisDiscrete', 'axisPoint',
'axisXBand', 'axisXPoint', 'axisXDiscrete', 'axisXQuantitative', 'axisXTemporal',
'axisYBand', 'axisYPoint', 'axisYDiscrete', 'axisYQuantitative', 'axisYTemporal',
'scale', 'selection', 'overlay' as keyof Config ];
const VL_ONLY_ALL_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX = { view: ['continuousWidth', 'continuousHeight', 'discreteWidth', 'discreteHeight', 'step'], ...VL_ONLY_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX};
export function stripAndRedirectConfig(config: Config<SignalRef>) { config = duplicate(config);
for (const prop of VL_ONLY_CONFIG_PROPERTIES) { delete config[prop]; }
if (config.axis) { for (const prop in config.axis) { if (isConditionalAxisValue(config.axis[prop])) { delete config.axis[prop]; } } }
if (config.legend) { for (const prop of VL_ONLY_LEGEND_CONFIG) { delete config.legend[prop]; } }
if (config.mark) { for (const prop of VL_ONLY_MARK_CONFIG_PROPERTIES) { delete config.mark[prop]; }
if (config.mark.tooltip && isObject(config.mark.tooltip)) { delete config.mark.tooltip; } }
if (config.params) { config.signals = (config.signals || []).concat(assembleParameterSignals(config.params)); delete config.params; }
for (const markType of MARK_STYLES) { for (const prop of VL_ONLY_MARK_CONFIG_PROPERTIES) { delete config[markType][prop]; }
const vlOnlyMarkSpecificConfigs = VL_ONLY_ALL_MARK_SPECIFIC_CONFIG_PROPERTY_INDEX[markType]; if (vlOnlyMarkSpecificConfigs) { for (const prop of vlOnlyMarkSpecificConfigs) { delete config[markType][prop]; } }
redirectConfigToStyleConfig(config, markType); }
for (const m of getAllCompositeMarks()) { delete config[m]; }
redirectTitleConfig(config);
for (const prop in config) { if (isObject(config[prop]) && isEmpty(config[prop])) { delete config[prop]; } }
return isEmpty(config) ? undefined : config;}
function redirectTitleConfig(config: Config<SignalRef>) { const {titleMarkConfig, subtitleMarkConfig, subtitle} = extractTitleConfig(config.title);
if (!isEmpty(titleMarkConfig)) { config.style['group-title'] = { ...config.style['group-title'], ...titleMarkConfig }; } if (!isEmpty(subtitleMarkConfig)) { config.style['group-subtitle'] = { ...config.style['group-subtitle'], ...subtitleMarkConfig }; }
if (!isEmpty(subtitle)) { config.title = subtitle; } else { delete config.title; }}
function redirectConfigToStyleConfig( config: Config<SignalRef>, prop: Mark | 'view' | string, toProp?: string, compositeMarkPart?: string) { const propConfig: MarkConfig<SignalRef> = compositeMarkPart ? config[prop][compositeMarkPart] : config[prop];
if (prop === 'view') { toProp = 'cell'; }
const style: MarkConfig<SignalRef> = { ...propConfig, ...(config.style[toProp ?? prop] as MarkConfig<SignalRef>) };
if (!isEmpty(style)) { config.style[toProp ?? prop] = style; }
if (!compositeMarkPart) { delete config[prop]; }}