compassql
Version:
CompassQL visualization query language
330 lines (280 loc) • 11 kB
text/typescript
import {isObject} from 'datalib/src/util';
import {AggregateOp} from 'vega';
import {Axis} from 'vega-lite/build/src/axis';
import {BinParams} from 'vega-lite/build/src/bin';
import {Channel} from 'vega-lite/build/src/channel';
import * as vlChannelDef from 'vega-lite/build/src/channeldef';
import {ValueDef} from 'vega-lite/build/src/channeldef';
import {scaleType as compileScaleType} from 'vega-lite/build/src/compile/scale/type';
import {Encoding} from 'vega-lite/build/src/encoding';
import {Legend} from 'vega-lite/build/src/legend';
import {Mark} from 'vega-lite/build/src/mark';
import {Scale} 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 {Type as VLType} from 'vega-lite/build/src/type';
import {FlatProp, isEncodingNestedParent, Property} from '../property';
import {Schema} from '../schema';
import {isWildcard, SHORT_WILDCARD, Wildcard, WildcardProperty} from '../wildcard';
import {ExpandedType} from './expandedtype';
import {PROPERTY_SUPPORTED_CHANNELS} from './shorthand';
export type EncodingQuery = FieldQuery | ValueQuery | AutoCountQuery;
export interface EncodingQueryBase {
channel: WildcardProperty<Channel>;
description?: string;
}
export interface ValueQuery extends EncodingQueryBase {
value: WildcardProperty<boolean | number | string>;
}
export function isValueQuery(encQ: EncodingQuery): encQ is ValueQuery {
return encQ !== null && encQ !== undefined && encQ['value'] !== undefined;
}
export function isFieldQuery(encQ: EncodingQuery): encQ is FieldQuery {
return encQ !== null && encQ !== undefined && (encQ['field'] || encQ['aggregate'] === 'count');
}
export function isAutoCountQuery(encQ: EncodingQuery): encQ is AutoCountQuery {
return encQ !== null && encQ !== undefined && 'autoCount' in encQ;
}
export function isDisabledAutoCountQuery(encQ: EncodingQuery) {
return isAutoCountQuery(encQ) && encQ.autoCount === false;
}
export function isEnabledAutoCountQuery(encQ: EncodingQuery) {
return isAutoCountQuery(encQ) && encQ.autoCount === true;
}
/**
* A special encoding query that gets added internally if the `config.autoCount` flag is on. See SpecQueryModel.build for its generation.
*
* __Note:__ this type of query should not be specified by users.
*/
export interface AutoCountQuery extends EncodingQueryBase {
/**
* A count function that gets added internally if the config.autoCount flag in on.
* This allows us to add one extra encoding mapping if needed when the query produces
* plot that only have discrete fields.
* In such cases, adding count make the output plots way more meaningful.
*/
autoCount: WildcardProperty<boolean>;
type: 'quantitative';
}
export interface FieldQueryBase {
// FieldDef
aggregate?: WildcardProperty<AggregateOp>;
timeUnit?: WildcardProperty<TimeUnit>;
/**
* Special flag for enforcing that the field should have a fuction (one of timeUnit, bin, or aggregate).
*
* For example, if you enumerate both bin and aggregate then you need `undefined` for both.
*
* ```
* {aggregate: {enum: [undefined, 'mean', 'sum']}, bin: {enum: [false, true]}}
* ```
*
* This would enumerate a fieldDef with "mean", "sum", bin:true, and no function at all.
* If you want only "mean", "sum", bin:true, then use `hasFn: true`
*
* ```
* {aggregate: {enum: [undefined, 'mean', 'sum']}, bin: {enum: [false, true]}, hasFn: true}
* ```
*/
hasFn?: boolean;
bin?: boolean | BinQuery | SHORT_WILDCARD;
scale?: boolean | ScaleQuery | SHORT_WILDCARD;
sort?: SortOrder | EncodingSortField<string>;
stack?: StackOffset | SHORT_WILDCARD;
field?: WildcardProperty<string>;
type?: WildcardProperty<ExpandedType>;
axis?: boolean | AxisQuery | SHORT_WILDCARD;
legend?: boolean | LegendQuery | SHORT_WILDCARD;
format?: string;
}
export type FieldQuery = EncodingQueryBase & FieldQueryBase;
// Using Mapped Type from TS2.1 to declare query for an object without nested property
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#mapped-types
export type FlatQuery<T> = {[P in keyof T]: WildcardProperty<T[P]>};
export type FlatQueryWithEnableFlag<T> = (Wildcard<boolean> | {}) & FlatQuery<T>;
export type BinQuery = FlatQueryWithEnableFlag<BinParams>;
export type ScaleQuery = FlatQueryWithEnableFlag<Scale>;
export type AxisQuery = FlatQueryWithEnableFlag<Axis>;
export type LegendQuery = FlatQueryWithEnableFlag<Legend>;
const DEFAULT_PROPS = [
Property.AGGREGATE,
Property.BIN,
Property.TIMEUNIT,
Property.FIELD,
Property.TYPE,
Property.SCALE,
Property.SORT,
Property.AXIS,
Property.LEGEND,
Property.STACK,
Property.FORMAT
];
export interface ConversionParams {
schema?: Schema;
props?: FlatProp[];
wildcardMode?: 'skip' | 'null';
}
export function toEncoding(encQs: EncodingQuery[], params: ConversionParams): Encoding<string> {
const {wildcardMode = 'skip'} = params;
let encoding: Encoding<string> = {};
for (const encQ of encQs) {
if (isDisabledAutoCountQuery(encQ)) {
continue; // Do not include this in the output.
}
const {channel} = encQ;
// if channel is a wildcard, return null
if (isWildcard(channel)) {
throw new Error('Cannot convert wildcard channel to a fixed channel');
}
const channelDef = isValueQuery(encQ) ? toValueDef(encQ) : toFieldDef(encQ, params);
if (channelDef === null) {
if (params.wildcardMode === 'null') {
// contains invalid property (e.g., wildcard, thus cannot return a proper spec.)
return null;
}
continue;
}
// Otherwise, we can set the channelDef
encoding[channel] = channelDef;
}
return encoding;
}
export function toValueDef(valueQ: ValueQuery): ValueDef {
const {value} = valueQ;
if (isWildcard(value)) {
return null;
}
return {value};
}
export function toFieldDef(
encQ: FieldQuery | AutoCountQuery,
params: ConversionParams = {}
): vlChannelDef.TypedFieldDef<string> {
const {props = DEFAULT_PROPS, schema, wildcardMode = 'skip'} = params;
if (isFieldQuery(encQ)) {
const fieldDef = {} as vlChannelDef.TypedFieldDef<string>;
for (const prop of props) {
let encodingProperty = encQ[prop];
if (isWildcard(encodingProperty)) {
if (wildcardMode === 'skip') continue;
return null;
}
if (encodingProperty !== undefined) {
// if the channel supports this prop
const isSupportedByChannel =
!PROPERTY_SUPPORTED_CHANNELS[prop] || PROPERTY_SUPPORTED_CHANNELS[prop][encQ.channel as Channel];
if (!isSupportedByChannel) {
continue;
}
if (isEncodingNestedParent(prop) && isObject(encodingProperty)) {
encodingProperty = {...encodingProperty}; // Make a shallow copy first
for (const childProp in encodingProperty) {
// ensure nested properties are not wildcard before assigning to field def
if (isWildcard(encodingProperty[childProp])) {
if (wildcardMode === 'null') {
return null;
}
delete encodingProperty[childProp]; // skip
}
}
}
if (prop === 'bin' && encodingProperty === false) {
continue;
} else if (prop === 'type' && encodingProperty === 'key') {
fieldDef.type = 'nominal';
} else {
fieldDef[prop] = encodingProperty;
}
}
if (prop === Property.SCALE && schema && encQ.type === TYPE.ORDINAL) {
const scale = encQ.scale;
const {ordinalDomain} = schema.fieldSchema(encQ.field as string);
if (scale !== null && ordinalDomain) {
fieldDef[Property.SCALE] = {
domain: ordinalDomain,
// explicitly specfied domain property should override ordinalDomain
...(isObject(scale) ? scale : {})
};
}
}
}
return fieldDef;
} else {
if (encQ.autoCount === false) {
throw new Error(`Cannot convert {autoCount: false} into a field def`);
} else {
return {
aggregate: 'count',
field: '*',
type: 'quantitative'
};
}
}
}
/**
* Is a field query continuous field?
* This method is applicable only for fieldQuery without wildcard
*/
export function isContinuous(encQ: EncodingQuery) {
if (isFieldQuery(encQ)) {
return vlChannelDef.isContinuous(toFieldDef(encQ, {props: ['bin', 'timeUnit', 'field', 'type']}));
}
return isAutoCountQuery(encQ);
}
export function isMeasure(encQ: EncodingQuery) {
if (isFieldQuery(encQ)) {
return !isDimension(encQ) && encQ.type !== 'temporal';
}
return isAutoCountQuery(encQ);
}
/**
* Is a field query discrete field?
* This method is applicable only for fieldQuery without wildcard
*/
export function isDimension(encQ: EncodingQuery) {
if (isFieldQuery(encQ)) {
const fieldDef = toFieldDef(encQ, {props: ['bin', 'timeUnit', 'type']});
return vlChannelDef.isDiscrete(fieldDef) || !!fieldDef.timeUnit;
}
return false;
}
/**
* Returns the true scale type of an encoding.
* @returns {ScaleType} If the scale type was not specified, it is inferred from the encoding's TYPE.
* @returns {undefined} If the scale type was not specified and Type (or TimeUnit if applicable) is a Wildcard, there is no clear scale type
*/
export function scaleType(fieldQ: FieldQuery) {
const scale: ScaleQuery = fieldQ.scale === true || fieldQ.scale === SHORT_WILDCARD ? {} : fieldQ.scale || {};
const {type, channel, timeUnit, bin} = fieldQ;
// HACK: All of markType, and scaleConfig only affect
// sub-type of ordinal to quantitative scales (point or band)
// Currently, most of scaleType usage in CompassQL doesn't care about this subtle difference.
// Thus, instead of making this method requiring the global mark,
// we will just call it with mark = undefined .
// Thus, currently, we will always get a point scale unless a CompassQuery specifies band.
const markType: Mark = undefined;
if (isWildcard(scale.type) || isWildcard(type) || isWildcard(channel) || isWildcard(bin)) {
return undefined;
}
// If scale type is specified, then use scale.type
if (scale.type) {
return scale.type;
}
// if type is fixed and it's not temporal, we can ignore time unit.
if (type === 'temporal' && isWildcard(timeUnit)) {
return undefined;
}
// if type is fixed and it's not quantitative, we can ignore bin
if (type === 'quantitative' && isWildcard(bin)) {
return undefined;
}
let vegaLiteType: VLType = type === ExpandedType.KEY ? 'nominal' : type;
const fieldDef = {
type: vegaLiteType,
timeUnit: timeUnit as TimeUnit,
bin: bin as BinParams
};
return compileScaleType({type: scale.type}, channel, fieldDef, markType);
}