UNPKG

compassql

Version:

CompassQL visualization query language

330 lines (280 loc) 11 kB
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); }