import { isString } from 'datalib/src/util' ; import { isAggregateOp } from 'vega-lite/build/src/aggregate' ; import { Channel , isChannel } from 'vega-lite/build/src/channel' ; import { Mark } from 'vega-lite/build/src/mark' ; import { FacetedUnitSpec } from 'vega-lite/build/src/spec' ; import { StackProperties } from 'vega-lite/build/src/stack' ; import { isUTCTimeUnit , isLocalSingleTimeUnit } from 'vega-lite/build/src/timeunit' ; import * as TYPE from 'vega-lite/build/src/type' ; import { getFullName } from 'vega-lite/build/src/type' ; import { DEFAULT_PROP_PRECEDENCE , EncodingNestedChildProp , getEncodingNestedProp , isEncodingNestedParent , Property , SORT_PROPS , VIEW_PROPS , } from '../property' ; import { PropIndex } from '../propindex' ; import { Dict , isArray , isBoolean , keys } from '../util' ; import { isShortWildcard , isWildcard , SHORT_WILDCARD } from '../wildcard' ; import { EncodingQuery , FieldQuery , FieldQueryBase , isAutoCountQuery , isDisabledAutoCountQuery , isEnabledAutoCountQuery , isFieldQuery , isValueQuery , } from './encoding' ; import { fromSpec , getVlStack , SpecQuery } from './spec' ; export type Replacer = ( s : string ) => string ; export function getReplacerIndex ( replaceIndex ) { return replaceIndex . map ( ( r ) => getReplacer ( r ) ) ; } export function getReplacer ( replace ) { return ( s ) => { if ( replace [ s ] !== undefined ) { return replace [ s ] ; } return s ; } ; } export function value ( v , replacer ) { if ( isWildcard ( v ) ) { if ( ! isShortWildcard ( v ) && v . enum ) { return SHORT_WILDCARD + JSON . stringify ( v . enum ) ; } else { return SHORT_WILDCARD ; } } if ( replacer ) { return replacer ( v ) ; } return v ; } export function replace ( v , replacer ) { if ( replacer ) { return replacer ( v ) ; } return v ; } export const REPLACE_NONE = new PropIndex < Replacer > ( ) ; export const INCLUDE_ALL = [ ] . concat ( DEFAULT_PROP_PRECEDENCE , SORT_PROPS , [ Property . TRANSFORM , Property . STACK ] , VIEW_PROPS ) . reduce ( ( pi , prop ) => pi . set ( prop , true ) , new PropIndex < boolean > ( ) ) ; export function vlSpec ( vlspec , include = INCLUDE_ALL , replace = REPLACE_NONE ) { const specQ = fromSpec ( vlspec ) ; return spec ( specQ , include , replace ) ; } export const PROPERTY_SUPPORTED_CHANNELS = { axis : { x : true , y : true , row : true , column : true } , legend : { color : true , opacity : true , size : true , shape : true } , scale : { x : true , y : true , color : true , opacity : true , row : true , column : true , size : true , shape : true } , sort : { x : true , y : true , path : true , order : true } , stack : { x : true , y : true } , } ; param specQ param include param replace export function spec ( specQ , include = INCLUDE_ALL , replace = REPLACE_NONE ) { const parts = [ ] ; if ( include . get ( Property . MARK ) ) { parts . push ( value ( specQ . mark , replace . get ( Property . MARK ) ) ) ; } if ( specQ . transform && specQ . transform . length > 0 ) { parts . push ( `transform:${ JSON . stringify ( specQ . transform ) }` ) ; } let stack ; if ( include . get ( Property . STACK ) ) { stack = getVlStack ( specQ ) ; } if ( specQ . encodings ) { const encodings = specQ . encodings . reduce ( ( encQs , encQ ) => { if ( ! isDisabledAutoCountQuery ( encQ ) ) { let str ; if ( ! ! stack && encQ . channel === stack . fieldChannel ) { str = encoding ( { ... encQ , stack : stack . offset } , include , replace ) ; } else { str = encoding ( encQ , include , replace ) ; } if ( str ) { encQs . push ( str ) ; } } return encQs ; } , [ ] ) . sort ( ) . join ( '|' ) ; if ( encodings ) { parts . push ( encodings ) ; } } for ( const viewProp of VIEW_PROPS ) { const propString = viewProp . toString ( ) ; if ( include . get ( viewProp ) && ! ! specQ [ propString ] ) { const value = specQ [ propString ] ; parts . push ( `${ propString }=${ JSON . stringify ( value ) }` ) ; } } return parts . join ( '|' ) ; } param encQ param include param replace export function encoding ( encQ , include = INCLUDE_ALL , replace = REPLACE_NONE ) { const parts = [ ] ; if ( include . get ( Property . CHANNEL ) ) { parts . push ( value ( encQ . channel , replace . get ( Property . CHANNEL ) ) ) ; } if ( isFieldQuery ( encQ ) ) { const fieldDefStr = fieldDef ( encQ , include , replace ) ; if ( fieldDefStr ) { parts . push ( fieldDefStr ) ; } } else if ( isValueQuery ( encQ ) ) { parts . push ( encQ . value ) ; } else if ( isAutoCountQuery ( encQ ) ) { parts . push ( 'autocount()' ) ; } return parts . join ( ':' ) ; } param encQ param include param replace export function fieldDef ( encQ , include = INCLUDE_ALL , replacer = REPLACE_NONE ) { if ( include . get ( Property . AGGREGATE ) && isDisabledAutoCountQuery ( encQ ) ) { return '-' ; } const fn = func ( encQ , include , replacer ) ; const props = fieldDefProps ( encQ , include , replacer ) ; let fieldAndParams ; if ( isFieldQuery ( encQ ) ) { fieldAndParams = include . get ( 'field' ) ? value ( encQ . field , replacer . get ( 'field' ) ) : '...' ; if ( include . get ( Property . TYPE ) ) { if ( isWildcard ( encQ . type ) ) { fieldAndParams += `,${ value ( encQ . type , replacer . get ( Property . TYPE ) ) }` ; } else { const typeShort = `${ encQ . type || TYPE . QUANTITATIVE }` . substr ( 0 , 1 ) ; fieldAndParams += `,${ value ( typeShort , replacer . get ( Property . TYPE ) ) }` ; } } fieldAndParams += props . map ( ( p ) => { const val = p . value instanceof Array ? `[${ p . value }]` : p . value ; return `,${ p . key }=${ val }` ; } ) . join ( '' ) ; } else if ( isAutoCountQuery ( encQ ) ) { fieldAndParams = '*,q' ; } if ( ! fieldAndParams ) { return null ; } if ( fn ) { const fnPrefix = isString ( fn ) ? fn : SHORT_WILDCARD + ( keys ( fn ) . length > 0 ? JSON . stringify ( fn ) : '' ) ; return `${ fnPrefix }(${ fieldAndParams })` ; } return fieldAndParams ; } /**
 * Return function part of
 */ function func ( fieldQ , include , replacer ) { if ( include . get ( Property . AGGREGATE ) && fieldQ . aggregate && ! isWildcard ( fieldQ . aggregate ) ) { return replace ( fieldQ . aggregate , replacer . get ( Property . AGGREGATE ) ) ; } else if ( include . get ( Property . AGGREGATE ) && isEnabledAutoCountQuery ( fieldQ ) ) { return replace ( 'count' , replacer . get ( Property . AGGREGATE ) ) ; } else if ( include . get ( Property . TIMEUNIT ) && fieldQ . timeUnit && ! isWildcard ( fieldQ . timeUnit ) ) { return replace ( fieldQ . timeUnit , replacer . get ( Property . TIMEUNIT ) ) ; } else if ( include . get ( Property . BIN ) && fieldQ . bin && ! isWildcard ( fieldQ . bin ) ) { return 'bin' ; } else { let fn = null ; for ( const prop of [ Property . AGGREGATE , Property . AUTOCOUNT , Property . TIMEUNIT , Property . BIN ] ) { const val = fieldQ [ prop ] ; if ( include . get ( prop ) && fieldQ [ prop ] && isWildcard ( val ) ) { fn = fn || { } ; fn [ prop ] = isShortWildcard ( val ) ? val : val . enum ; } } if ( fn && fieldQ . hasFn ) { fn . hasFn = true ; } return fn ; } } /**
 * Return key-value of parameters of field defs
 */ function fieldDefProps ( fieldQ , include , replacer ) { /** Encoding properties e.g., Scale, Axis, Legend */ const props = [ ] ; if ( ! isBoolean ( fieldQ . bin ) && ! isShortWildcard ( fieldQ . bin ) ) { const bin = fieldQ . bin ; for ( const child in bin ) { const prop = getEncodingNestedProp ( 'bin' , child as EncodingNestedChildProp ) ; if ( prop && include . get ( prop ) && bin [ child ] !== undefined ) { props . push ( { key : child , value : value ( bin [ child ] , replacer . get ( prop ) ) , } ) ; } } props . sort ( ( a , b ) => a . key . localeCompare ( b . key ) ) ; } for ( const parent of [ Property . SCALE , Property . SORT , Property . STACK , Property . AXIS , Property . LEGEND ] ) { if ( ! isWildcard ( fieldQ . channel ) && ! PROPERTY_SUPPORTED_CHANNELS [ parent ] [ fieldQ . channel as Channel ] ) { continue ; } if ( include . get ( parent ) && fieldQ [ parent ] !== undefined ) { const parentValue = fieldQ [ parent ] ; if ( isBoolean ( parentValue ) || parentValue === null ) { props . push ( { key : `${ parent }` , value : parentValue || false , } ) ; } else if ( isString ( parentValue ) ) { props . push ( { key : `${ parent }` , value : replace ( JSON . stringify ( parentValue ) , replacer . get ( parent ) ) , } ) ; } else { const nestedPropChildren = [ ] ; for ( const child in parentValue ) { const nestedProp = getEncodingNestedProp ( parent , child as EncodingNestedChildProp ) ; if ( nestedProp && include . get ( nestedProp ) && parentValue [ child ] !== undefined ) { nestedPropChildren . push ( { key : child , value : value ( parentValue [ child ] , replacer . get ( nestedProp ) ) , } ) ; } } if ( nestedPropChildren . length > 0 ) { const nestedPropObject = nestedPropChildren . sort ( ( a , b ) => a . key . localeCompare ( b . key ) ) . reduce ( ( o , item ) => { o [ item . key ] = item . value ; return o ; } , { } ) ; props . push ( { key : `${ parent }` , value : JSON . stringify ( nestedPropObject ) , } ) ; } } } } return props ; } export function parse ( shorthand ) { const splitShorthand = shorthand . split ( '|' ) ; const specQ = { mark : splitShorthand [ 0 ] as Mark , encodings : [ ] as EncodingQuery [ ] , } ; for ( let i = 1 ; i < splitShorthand . length ; i ++ ) { const part = splitShorthand [ i ] ; const splitPart = splitWithTail ( part , ':' , 1 ) ; const splitPartKey = splitPart [ 0 ] ; const splitPartValue = splitPart [ 1 ] ; if ( isChannel ( splitPartKey ) || splitPartKey === '?' ) { const encQ = shorthandParser . encoding ( splitPartKey , splitPartValue ) ; specQ . encodings . push ( encQ ) ; continue ; } if ( splitPartKey === 'transform' ) { specQ . transform = JSON . parse ( splitPartValue ) ; continue ; } } return specQ ; } param str param delim param number export function splitWithTail ( str , delim , count ) { const result = [ ] ; let lastIndex = 0 ; for ( let i = 0 ; i < count ; i ++ ) { const indexOfDelim = str . indexOf ( delim , lastIndex ) ; if ( indexOfDelim !== - 1 ) { result . push ( str . substring ( lastIndex , indexOfDelim ) ) ; lastIndex = indexOfDelim + 1 ; } else { break ; } } result . push ( str . substr ( lastIndex ) ) ; if ( result . length !== count + 1 ) { while ( result . length !== count + 1 ) { result . push ( '' ) ; } } return result ; } export namespace shorthandParser { export function encoding ( channel , fieldDefShorthand ) { const encQMixins = fieldDefShorthand . indexOf ( '(' ) !== - 1 ? fn ( fieldDefShorthand ) : rawFieldDef ( splitWithTail ( fieldDefShorthand , ',' , 2 ) ) ; return { channel , ... encQMixins , } ; } export function rawFieldDef ( fieldDefPart ) { const fieldQ = { } ; fieldQ . field = fieldDefPart [ 0 ] ; fieldQ . type = getFullName ( fieldDefPart [ 1 ] . toUpperCase ( ) ) || '?' ; const partParams = fieldDefPart [ 2 ] ; let closingBraceIndex = 0 ; let i = 0 ; while ( i < partParams . length ) { const propEqualSignIndex = partParams . indexOf ( '=' , i ) ; let parsedValue ; if ( propEqualSignIndex !== - 1 ) { const prop = partParams . substring ( i , propEqualSignIndex ) ; if ( partParams [ i + prop . length + 1 ] === '{' ) { const openingBraceIndex = i + prop . length + 1 ; closingBraceIndex = getClosingIndex ( openingBraceIndex , partParams , '}' ) ; const value = partParams . substring ( openingBraceIndex , closingBraceIndex + 1 ) ; parsedValue = JSON . parse ( value ) ; i = closingBraceIndex + 2 ; } else if ( partParams [ i + prop . length + 1 ] === '[' ) { const openingBracketIndex = i + prop . length + 1 ; const closingBracketIndex = getClosingIndex ( openingBracketIndex , partParams , ']' ) ; const value = partParams . substring ( openingBracketIndex , closingBracketIndex + 1 ) ; parsedValue = JSON . parse ( value ) ; i = closingBracketIndex + 2 ; } else { const propIndex = i ; let nextCommaIndex = partParams . indexOf ( ',' , i + prop . length ) ; if ( nextCommaIndex === - 1 ) { nextCommaIndex = partParams . length ; } i = nextCommaIndex + 1 ; parsedValue = JSON . parse ( partParams . substring ( propIndex + prop . length + 1 , nextCommaIndex ) ) ; } if ( isEncodingNestedParent ( prop ) ) { fieldQ [ prop ] = parsedValue ; } else { fieldQ . bin = fieldQ . bin || { } ; fieldQ . bin [ prop ] = parsedValue ; } } else { break ; } } return fieldQ ; } export function getClosingIndex ( openingBraceIndex , str , closingChar ) { for ( let i = openingBraceIndex ; i < str . length ; i ++ ) { if ( str [ i ] === closingChar ) { return i ; } } } export function fn ( fieldDefShorthand ) { const fieldQ = { } ; if ( fieldDefShorthand [ 0 ] === '?' ) { const closingBraceIndex = getClosingIndex ( 1 , fieldDefShorthand , '}' ) ; const fnEnumIndex = JSON . parse ( fieldDefShorthand . substring ( 1 , closingBraceIndex + 1 ) ) ; for ( const encodingProperty in fnEnumIndex ) { if ( isArray ( fnEnumIndex [ encodingProperty ] ) ) { fieldQ [ encodingProperty ] = { enum : fnEnumIndex [ encodingProperty ] } ; } else { fieldQ [ encodingProperty ] = fnEnumIndex [ encodingProperty ] ; } } return { ... fieldQ , ... rawFieldDef ( splitWithTail ( fieldDefShorthand . substring ( closingBraceIndex + 2 , fieldDefShorthand . length - 1 ) , ',' , 2 ) ) , } ; } else { const func = fieldDefShorthand . substring ( 0 , fieldDefShorthand . indexOf ( '(' ) ) ; const insideFn = fieldDefShorthand . substring ( func . length + 1 , fieldDefShorthand . length - 1 ) ; const insideFnParts = splitWithTail ( insideFn , ',' , 2 ) ; if ( isAggregateOp ( func ) ) { return { aggregate : func , ... rawFieldDef ( insideFnParts ) , } ; } else if ( isUTCTimeUnit ( func ) || isLocalSingleTimeUnit ( func ) ) { return { timeUnit : func , ... rawFieldDef ( insideFnParts ) , } ; } else if ( func === 'bin' ) { return { bin : { } , ... rawFieldDef ( insideFnParts ) , } ; } } } }