compassql
Version:
CompassQL visualization query language
436 lines (382 loc) • 11.5 kB
text/typescript
import {Axis, AXIS_PROPERTIES} from 'vega-lite/build/src/axis';
import {BinParams} from 'vega-lite/build/src/bin';
import {Channel, COLOR, COLUMN, ROW, SIZE, X, Y} from 'vega-lite/build/src/channel';
import {TypedFieldDef} from 'vega-lite/build/src/channeldef';
import {Legend, LEGEND_PROPERTIES} from 'vega-lite/build/src/legend';
import * as MARK from 'vega-lite/build/src/mark';
import {Mark} from 'vega-lite/build/src/mark';
import {Scale, ScaleType, SCALE_PROPERTIES} from 'vega-lite/build/src/scale';
import {EncodingSortField, SortOrder} from 'vega-lite/build/src/sort';
import {StackOffset} from 'vega-lite/build/src/stack';
import {TimeUnit} from 'vega-lite/build/src/timeunit';
import * as TYPE from 'vega-lite/build/src/type';
import {QueryConfig} from './config';
import {isEncodingNestedProp, Property} from './property';
import {Schema} from './schema';
import {extend, isArray} from './util';
export const SHORT_WILDCARD: SHORT_WILDCARD = '?';
export type SHORT_WILDCARD = '?';
export interface Wildcard<T> {
name?: string;
/**
* List of values to enumerate
*/
enum?: T[];
}
export type WildcardProperty<T> = T | Wildcard<T> | SHORT_WILDCARD;
export interface ExtendedWildcard<T> extends Wildcard<T> {
[prop: string]: any;
}
export function isWildcard(prop: any): prop is Wildcard<any> | SHORT_WILDCARD {
return isShortWildcard(prop) || isWildcardDef(prop);
}
export function isShortWildcard(prop: any): prop is SHORT_WILDCARD {
return prop === SHORT_WILDCARD;
}
export function isWildcardDef(prop: any): prop is Wildcard<any> {
return prop !== undefined && prop != null && (!!prop.enum || !!prop.name) && !isArray(prop);
}
export function initWildcard(
prop: SHORT_WILDCARD | ExtendedWildcard<any>,
defaultName: string,
defaultEnumValues: any[]
): ExtendedWildcard<any> {
return extend(
{},
{
name: defaultName,
enum: defaultEnumValues
},
prop === SHORT_WILDCARD ? {} : prop
);
}
/**
* Initial short names from list of full camelCaseNames.
* For each camelCaseNames, return unique short names based on initial (e.g., `ccn`)
*/
function initNestedPropName(fullNames: string[]) {
let index = {};
let has = {};
for (const fullName of fullNames) {
const initialIndices = [0];
for (let i = 0; i < fullName.length; i++) {
if (fullName.charAt(i).toUpperCase() === fullName.charAt(i)) {
initialIndices.push(i);
}
}
let shortName = initialIndices
.map(i => fullName.charAt(i))
.join('')
.toLowerCase();
if (!has[shortName]) {
index[fullName] = shortName;
has[shortName] = true;
continue;
}
// If duplicate, add last character and try again!
if (initialIndices[initialIndices.length - 1] !== fullName.length - 1) {
shortName = initialIndices
.concat([fullName.length - 1])
.map(i => fullName.charAt(i))
.join('')
.toLowerCase();
if (!has[shortName]) {
index[fullName] = shortName;
has[shortName] = true;
continue;
}
}
for (let i = 1; !index[fullName]; i++) {
let shortNameWithNo = shortName + '_' + i;
if (!has[shortNameWithNo]) {
index[fullName] = shortNameWithNo;
has[shortNameWithNo] = true;
break;
}
}
}
return index;
}
export const DEFAULT_NAME = {
mark: 'm',
channel: 'c',
aggregate: 'a',
autoCount: '#',
hasFn: 'h',
bin: 'b',
sort: 'so',
stack: 'st',
scale: 's',
format: 'f',
axis: 'ax',
legend: 'l',
value: 'v',
timeUnit: 'tu',
field: 'f',
type: 't',
binProps: {
maxbins: 'mb',
min: 'mi',
max: 'ma',
base: 'b',
step: 's',
steps: 'ss',
minstep: 'ms',
divide: 'd'
},
sortProps: {
field: 'f',
op: 'o',
order: 'or'
},
scaleProps: initNestedPropName(SCALE_PROPERTIES),
axisProps: initNestedPropName(AXIS_PROPERTIES),
legendProps: initNestedPropName(LEGEND_PROPERTIES)
};
export function getDefaultName(prop: Property) {
if (isEncodingNestedProp(prop)) {
return DEFAULT_NAME[prop.parent] + '-' + DEFAULT_NAME[prop.parent + 'Props'][prop.child];
}
if (DEFAULT_NAME[prop]) {
return DEFAULT_NAME[prop];
}
/* istanbul ignore next */
throw new Error('Default name undefined for ' + prop);
}
/**
* Generic index for default enum (values to enumerate) of a particular definition type.
*/
export type DefEnumIndex<T> = {[P in keyof T]-?: T[P][]};
const DEFAULT_BOOLEAN_ENUM = [false, true];
export type EnumIndex = {
mark: Mark[];
channel: Channel[];
autoCount: boolean[];
hasFn: boolean[];
} & DefEnumIndex<TypedFieldDef<string>> & {
sort: (EncodingSortField<string> | SortOrder)[];
stack: StackOffset[];
format: string[];
scale: boolean[];
axis: boolean[];
legend: boolean[];
value: any[];
binProps: Partial<DefEnumIndex<BinParams>>;
sortProps: Partial<DefEnumIndex<EncodingSortField<string>>>;
scaleProps: Partial<DefEnumIndex<Scale>>;
axisProps: Partial<DefEnumIndex<Axis>>;
legendProps: Partial<DefEnumIndex<Legend>>;
};
const DEFAULT_BIN_PROPS_ENUM: DefEnumIndex<BinParams> = {
maxbins: [5, 10, 20],
extent: [undefined],
base: [10],
step: [undefined],
steps: [undefined],
minstep: [undefined],
divide: [[5, 2]],
binned: [false],
anchor: [undefined],
nice: [true]
};
const DEFAULT_SORT_PROPS: DefEnumIndex<EncodingSortField<string>> = {
field: [undefined], // This should be never call and instead read from the schema
op: ['min', 'mean'],
order: ['ascending', 'descending']
};
const DEFAULT_SCALE_PROPS_ENUM: DefEnumIndex<Scale> = {
type: [undefined, ScaleType.LOG],
domain: [undefined],
base: [undefined],
exponent: [1, 2],
constant: [undefined],
bins: [undefined],
clamp: DEFAULT_BOOLEAN_ENUM,
nice: DEFAULT_BOOLEAN_ENUM,
reverse: DEFAULT_BOOLEAN_ENUM,
round: DEFAULT_BOOLEAN_ENUM,
zero: DEFAULT_BOOLEAN_ENUM,
padding: [undefined],
paddingInner: [undefined],
paddingOuter: [undefined],
interpolate: [undefined],
range: [undefined],
rangeStep: [17, 21],
scheme: [undefined]
};
const DEFAULT_AXIS_PROPS_ENUM: DefEnumIndex<Axis> = {
zindex: [1, 0],
offset: [undefined],
orient: [undefined],
values: [undefined],
bandPosition: [undefined],
encoding: [undefined],
domain: DEFAULT_BOOLEAN_ENUM,
domainColor: [undefined],
domainDash: [undefined],
domainDashOffset: [undefined],
domainOpacity: [undefined],
domainWidth: [undefined],
formatType: [undefined],
grid: DEFAULT_BOOLEAN_ENUM,
gridColor: [undefined],
gridDash: [undefined],
gridDashOffset: [undefined],
gridOpacity: [undefined],
gridWidth: [undefined],
format: [undefined],
labels: DEFAULT_BOOLEAN_ENUM,
labelAlign: [undefined],
labelAngle: [undefined],
labelBaseline: [undefined],
labelColor: [undefined],
labelFlushOffset: [undefined],
labelFont: [undefined],
labelFontSize: [undefined],
labelFontStyle: [undefined],
labelFontWeight: [undefined],
labelLimit: [undefined],
labelOpacity: [undefined],
labelSeparation: [undefined],
labelOverlap: [undefined],
labelPadding: [undefined],
labelBound: [undefined],
labelFlush: [undefined],
maxExtent: [undefined],
minExtent: [undefined],
position: [undefined],
ticks: DEFAULT_BOOLEAN_ENUM,
tickColor: [undefined],
tickCount: [undefined],
tickDash: [undefined],
tickExtra: [undefined],
tickDashOffset: [undefined],
tickMinStep: [undefined],
tickOffset: [undefined],
tickOpacity: [undefined],
tickRound: [undefined],
tickSize: [undefined],
tickWidth: [undefined],
title: [undefined],
titleAlign: [undefined],
titleAnchor: [undefined],
titleAngle: [undefined],
titleBaseline: [undefined],
titleColor: [undefined],
titleFont: [undefined],
titleFontSize: [undefined],
titleFontStyle: [undefined],
titleFontWeight: [undefined],
titleLimit: [undefined],
titleOpacity: [undefined],
titlePadding: [undefined],
titleX: [undefined],
titleY: [undefined]
};
const DEFAULT_LEGEND_PROPS_ENUM: DefEnumIndex<Legend> = {
orient: ['left', 'right'],
format: [undefined],
type: [undefined],
values: [undefined],
zindex: [undefined],
clipHeight: [undefined],
columnPadding: [undefined],
columns: [undefined],
cornerRadius: [undefined],
direction: [undefined],
encoding: [undefined],
fillColor: [undefined],
formatType: [undefined],
gridAlign: [undefined],
offset: [undefined],
padding: [undefined],
rowPadding: [undefined],
strokeColor: [undefined],
labelAlign: [undefined],
labelBaseline: [undefined],
labelColor: [undefined],
labelFont: [undefined],
labelFontSize: [undefined],
labelFontStyle: [undefined],
labelFontWeight: [undefined],
labelLimit: [undefined],
labelOffset: [undefined],
labelOpacity: [undefined],
labelOverlap: [undefined],
labelPadding: [undefined],
labelSeparation: [undefined],
legendX: [undefined],
legendY: [undefined],
gradientLength: [undefined],
gradientOpacity: [undefined],
gradientStrokeColor: [undefined],
gradientStrokeWidth: [undefined],
gradientThickness: [undefined],
symbolDash: [undefined],
symbolDashOffset: [undefined],
symbolFillColor: [undefined],
symbolOffset: [undefined],
symbolOpacity: [undefined],
symbolSize: [undefined],
symbolStrokeColor: [undefined],
symbolStrokeWidth: [undefined],
symbolType: [undefined],
tickCount: [undefined],
tickMinStep: [undefined],
title: [undefined],
titleAnchor: [undefined],
titleAlign: [undefined],
titleBaseline: [undefined],
titleColor: [undefined],
titleFont: [undefined],
titleFontSize: [undefined],
titleFontStyle: [undefined],
titleFontWeight: [undefined],
titleLimit: [undefined],
titleOpacity: [undefined],
titleOrient: [undefined],
titlePadding: [undefined]
};
// Use FullEnumIndex to make sure we have all properties specified here!
export const DEFAULT_ENUM_INDEX: EnumIndex = {
mark: [MARK.POINT, MARK.BAR, MARK.LINE, MARK.AREA, MARK.RECT, MARK.TICK, MARK.TEXT],
channel: [X, Y, ROW, COLUMN, SIZE, COLOR], // TODO: TEXT
aggregate: [undefined, 'mean'],
autoCount: DEFAULT_BOOLEAN_ENUM,
bin: DEFAULT_BOOLEAN_ENUM,
hasFn: DEFAULT_BOOLEAN_ENUM,
timeUnit: [undefined, TimeUnit.YEAR, TimeUnit.MONTH, TimeUnit.MINUTES, TimeUnit.SECONDS],
field: [undefined], // This is not used as field should be read from schema
type: [TYPE.NOMINAL, TYPE.ORDINAL, TYPE.QUANTITATIVE, TYPE.TEMPORAL],
sort: ['ascending', 'descending'],
stack: ['zero', 'normalize', 'center', null],
value: [undefined],
format: [undefined],
title: [undefined],
scale: [true],
axis: DEFAULT_BOOLEAN_ENUM,
legend: DEFAULT_BOOLEAN_ENUM,
binProps: DEFAULT_BIN_PROPS_ENUM,
sortProps: DEFAULT_SORT_PROPS,
scaleProps: DEFAULT_SCALE_PROPS_ENUM,
axisProps: DEFAULT_AXIS_PROPS_ENUM,
legendProps: DEFAULT_LEGEND_PROPS_ENUM
};
// TODO: rename this to getDefaultEnum
export function getDefaultEnumValues(prop: Property, schema: Schema, opt: QueryConfig): any[] {
if (prop === 'field' || (isEncodingNestedProp(prop) && prop.parent === 'sort' && prop.child === 'field')) {
// For field, by default enumerate all fields
return schema.fieldNames();
}
let val;
if (isEncodingNestedProp(prop)) {
val = opt.enum[prop.parent + 'Props'][prop.child];
} else {
val = opt.enum[prop];
}
if (val !== undefined) {
return val;
}
/* istanbul ignore next */
throw new Error('No default enumValues for ' + JSON.stringify(prop));
}