import * as CHANNEL from 'vega-lite/build/src/channel' ; import { ExtendedChannel } from 'vega-lite/build/src/channel' ; import { channelCompatibility , TypedFieldDef } from 'vega-lite/build/src/channeldef' ; import { channelScalePropertyIncompatability , hasDiscreteDomain , Scale , ScaleType , scaleTypeSupportProperty , } from 'vega-lite/build/src/scale' ; import { normalizeTimeUnit , isLocalSingleTimeUnit , isUTCTimeUnit } from 'vega-lite/build/src/timeunit' ; import * as TYPE from 'vega-lite/build/src/type' ; import { QueryConfig } from '../config' ; import { getEncodingNestedProp , Property , SCALE_PROPS } from '../property' ; import { PropIndex } from '../propindex' ; import { AutoCountQuery , FieldQuery , isFieldQuery , ScaleQuery , scaleType , toFieldDef } from '../query/encoding' ; import { ExpandedType , isDiscrete } from '../query/expandedtype' ; import { PrimitiveType , Schema } from '../schema' ; import { contains } from '../util' ; import { isWildcard , Wildcard } from '../wildcard' ; import { EncodingConstraint , EncodingConstraintModel } from './base' ; export const FIELD_CONSTRAINTS = [ { name : 'aggregateOpSupportedByType' , description : 'Aggregate function should be supported by data type.' , properties : [ Property . TYPE , Property . AGGREGATE ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . aggregate ) { return ! isDiscrete ( fieldQ . type ) ; } return true ; } , } , { name : 'asteriskFieldWithCountOnly' , description : 'Field="*" should be disallowed except aggregate="count"' , properties : [ Property . FIELD , Property . AGGREGATE ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { return ( fieldQ . field === '*' ) === ( fieldQ . aggregate === 'count' ) ; } , } , { name : 'minCardinalityForBin' , description : 'binned quantitative field should not have too low cardinality' , properties : [ Property . BIN , Property . FIELD , Property . TYPE ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , schema , _ , opt ) => { if ( fieldQ . bin && fieldQ . type === TYPE . QUANTITATIVE ) { const fieldQwithoutBin = { channel : fieldQ . channel , field : fieldQ . field , type : fieldQ . type , } ; return schema . cardinality ( fieldQwithoutBin ) >= opt . minCardinalityForBin ; } return true ; } , } , { name : 'binAppliedForQuantitative' , description : 'bin should be applied to quantitative field only.' , properties : [ Property . TYPE , Property . BIN ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . bin ) { return fieldQ . type === TYPE . QUANTITATIVE ; } return true ; } , } , { name : 'channelFieldCompatible' , description : `encoding channel's range type be compatible with channel type.` , properties : [ Property . CHANNEL , Property . TYPE , Property . BIN , Property . TIMEUNIT ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , schema , encWildcardIndex , opt ) => { const fieldDef = { field : 'f' , ... toFieldDef ( fieldQ , { schema , props : [ 'bin' , 'timeUnit' , 'type' ] } ) , } ; const { compatible } = channelCompatibility ( fieldDef , fieldQ . channel as ExtendedChannel ) ; if ( compatible ) { return true ; } else { const isFacet = fieldQ . channel === 'row' || fieldQ . channel === 'column' || fieldQ . channel === 'facet' ; const unit = fieldDef . timeUnit && normalizeTimeUnit ( fieldDef . timeUnit ) ?. unit ; if ( isFacet && unit && ( isLocalSingleTimeUnit ( unit ) || isUTCTimeUnit ( unit ) ) ) { return true ; } return false ; } } , } , { name : 'hasFn' , description : 'A field with as hasFn flag should have one of aggregate, timeUnit, or bin.' , properties : [ Property . AGGREGATE , Property . BIN , Property . TIMEUNIT ] , allowWildcardForProperties : true , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . hasFn ) { return ! ! fieldQ . aggregate || ! ! fieldQ . bin || ! ! fieldQ . timeUnit ; } return true ; } , } , { name : 'omitScaleZeroWithBinnedField' , description : 'Do not use scale zero with binned field' , properties : [ Property . SCALE , getEncodingNestedProp ( 'scale' , 'zero' ) , Property . BIN ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . bin && fieldQ . scale ) { if ( ( fieldQ . scale as ScaleQuery ) . zero === true ) { return false ; } } return true ; } , } , { name : 'onlyOneTypeOfFunction' , description : 'Only of of aggregate, autoCount, timeUnit, or bin should be applied at the same time.' , properties : [ Property . AGGREGATE , Property . AUTOCOUNT , Property . TIMEUNIT , Property . BIN ] , allowWildcardForProperties : true , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( isFieldQuery ( fieldQ ) ) { const numFn = ( ! isWildcard ( fieldQ . aggregate ) && ! ! fieldQ . aggregate ? 1 : 0 ) + ( ! isWildcard ( fieldQ . bin ) && ! ! fieldQ . bin ? 1 : 0 ) + ( ! isWildcard ( fieldQ . timeUnit ) && ! ! fieldQ . timeUnit ? 1 : 0 ) ; return numFn <= 1 ; } return true ; } , } , { name : 'timeUnitAppliedForTemporal' , description : 'Time unit should be applied to temporal field only.' , properties : [ Property . TYPE , Property . TIMEUNIT ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . timeUnit && fieldQ . type !== TYPE . TEMPORAL ) { return false ; } return true ; } , } , { name : 'timeUnitShouldHaveVariation' , description : 'A particular time unit should be applied only if they produce unique values.' , properties : [ Property . TIMEUNIT , Property . TYPE ] , allowWildcardForProperties : false , strict : false , satisfy : ( fieldQ , schema , encWildcardIndex , opt ) => { if ( fieldQ . timeUnit && fieldQ . type === TYPE . TEMPORAL ) { if ( ! encWildcardIndex . has ( 'timeUnit' ) && ! opt . constraintManuallySpecifiedValue ) { return true ; } return schema . timeUnitHasVariation ( fieldQ ) ; } return true ; } , } , { name : 'scalePropertiesSupportedByScaleType' , description : 'Scale properties must be supported by correct scale type' , properties : [ ] . concat ( SCALE_PROPS , [ Property . SCALE , Property . TYPE ] ) , allowWildcardForProperties : true , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . scale ) { const scale = fieldQ . scale as ScaleQuery ; const sType = scaleType ( fieldQ ) ; if ( sType === undefined || sType === null ) { return true ; } for ( const scaleProp in scale ) { if ( scaleProp === 'type' || scaleProp === 'name' || scaleProp === 'enum' ) { continue ; } const sProp = scaleProp as keyof Scale ; if ( sType === 'point' ) { if ( ! scaleTypeSupportProperty ( 'point' , sProp ) && ! scaleTypeSupportProperty ( 'band' , sProp ) ) { return false ; } } else if ( ! scaleTypeSupportProperty ( sType , sProp ) ) { return false ; } } } return true ; } , } , { name : 'scalePropertiesSupportedByChannel' , description : 'Not all scale properties are supported by all encoding channels' , properties : [ ] . concat ( SCALE_PROPS , [ Property . SCALE , Property . CHANNEL ] ) , allowWildcardForProperties : true , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ ) { const channel = fieldQ . channel as ExtendedChannel ; const scale = fieldQ . scale as ScaleQuery ; if ( channel && ! isWildcard ( channel ) && scale ) { if ( channel === 'row' || channel === 'column' || channel === 'facet' ) { return false ; } for ( const scaleProp in scale ) { if ( ! scale . hasOwnProperty ( scaleProp ) ) continue ; if ( scaleProp === 'type' || scaleProp === 'name' || scaleProp === 'enum' ) { continue ; } const isSupported = channelScalePropertyIncompatability ( channel , scaleProp as keyof Scale ) === undefined ; if ( ! isSupported ) { return false ; } } } } return true ; } , } , { name : 'typeMatchesPrimitiveType' , description : "Data type should be supported by field's primitive type." , properties : [ Property . FIELD , Property . TYPE ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , schema , encWildcardIndex , opt ) => { if ( fieldQ . field === '*' ) { return true ; } const primitiveType = schema . primitiveType ( fieldQ . field as string ) ; const type = fieldQ . type ; if ( ! encWildcardIndex . has ( 'field' ) && ! encWildcardIndex . has ( 'type' ) && ! opt . constraintManuallySpecifiedValue ) { return true ; } switch ( primitiveType ) { case PrimitiveType . BOOLEAN : case PrimitiveType . STRING : return type !== TYPE . QUANTITATIVE && type !== TYPE . TEMPORAL ; case PrimitiveType . NUMBER : case PrimitiveType . INTEGER : return type !== TYPE . TEMPORAL ; case PrimitiveType . DATETIME : return type === TYPE . TEMPORAL ; case null : return false ; } throw new Error ( 'Not implemented' ) ; } , } , { name : 'typeMatchesSchemaType' , description : "Enumerated data type of a field should match the field's type in the schema." , properties : [ Property . FIELD , Property . TYPE ] , allowWildcardForProperties : false , strict : false , satisfy : ( fieldQ , schema , encWildcardIndex , opt ) => { if ( ! encWildcardIndex . has ( 'field' ) && ! encWildcardIndex . has ( 'type' ) && ! opt . constraintManuallySpecifiedValue ) { return true ; } if ( fieldQ . field === '*' ) { return fieldQ . type === TYPE . QUANTITATIVE ; } return schema . vlType ( fieldQ . field as string ) === fieldQ . type ; } , } , { name : 'maxCardinalityForCategoricalColor' , description : 'Categorical channel should not have too high cardinality' , properties : [ Property . CHANNEL , Property . FIELD ] , allowWildcardForProperties : false , strict : false , satisfy : ( fieldQ , schema , _ , opt ) => { if ( fieldQ . channel === CHANNEL . COLOR && ( fieldQ . type === TYPE . NOMINAL || fieldQ . type === ExpandedType . KEY ) ) { return schema . cardinality ( fieldQ ) <= opt . maxCardinalityForCategoricalColor ; } return true ; } , } , { name : 'maxCardinalityForFacet' , description : 'Row/column channel should not have too high cardinality' , properties : [ Property . CHANNEL , Property . FIELD , Property . BIN , Property . TIMEUNIT ] , allowWildcardForProperties : false , strict : false , satisfy : ( fieldQ , schema , _ , opt ) => { if ( fieldQ . channel === CHANNEL . ROW || fieldQ . channel === CHANNEL . COLUMN ) { return schema . cardinality ( fieldQ ) <= opt . maxCardinalityForFacet ; } return true ; } , } , { name : 'maxCardinalityForShape' , description : 'Shape channel should not have too high cardinality' , properties : [ Property . CHANNEL , Property . FIELD , Property . BIN , Property . TIMEUNIT ] , allowWildcardForProperties : false , strict : false , satisfy : ( fieldQ , schema , _ , opt ) => { if ( fieldQ . channel === CHANNEL . SHAPE ) { return schema . cardinality ( fieldQ ) <= opt . maxCardinalityForShape ; } return true ; } , } , { name : 'dataTypeAndFunctionMatchScaleType' , description : 'Scale type must match data type' , properties : [ Property . TYPE , Property . SCALE , getEncodingNestedProp ( 'scale' , 'type' ) , Property . TIMEUNIT , Property . BIN , ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . scale ) { const type = fieldQ . type ; const sType = scaleType ( fieldQ ) ; if ( isDiscrete ( type ) ) { return sType === undefined || hasDiscreteDomain ( sType ) ; } else if ( type === TYPE . TEMPORAL ) { if ( ! fieldQ . timeUnit ) { return contains ( [ ScaleType . TIME , ScaleType . UTC , undefined ] , sType ) ; } else { return contains ( [ ScaleType . TIME , ScaleType . UTC , undefined ] , sType ) || hasDiscreteDomain ( sType ) ; } } else if ( type === TYPE . QUANTITATIVE ) { if ( fieldQ . bin ) { return contains ( [ ScaleType . LINEAR , undefined ] , sType ) ; } else { return contains ( [ ScaleType . LOG , ScaleType . POW , ScaleType . SQRT , ScaleType . QUANTILE , ScaleType . QUANTIZE , ScaleType . LINEAR , undefined , ] , sType ) ; } } } return true ; } , } , { name : 'stackIsOnlyUsedWithXY' , description : 'stack should only be allowed for x and y channels' , properties : [ Property . STACK , Property . CHANNEL ] , allowWildcardForProperties : false , strict : true , satisfy : ( fieldQ , _ , __ , ___ ) => { if ( fieldQ . stack ) { return fieldQ . channel === CHANNEL . X || fieldQ . channel === CHANNEL . Y ; } return true ; } , } , ] . map ( ( ec ) => new EncodingConstraintModel < FieldQuery > ( ec ) ) ; export const FIELD_CONSTRAINT_INDEX = FIELD_CONSTRAINTS . reduce ( ( m , ec ) => { m [ ec . name ( ) ] = ec ; return m ; } , { } ) ; export const FIELD_CONSTRAINTS_BY_PROPERTY = FIELD_CONSTRAINTS . reduce ( ( index , c ) => { for ( const prop of c . properties ( ) ) { index . set ( prop , index . get ( prop ) || [ ] ) ; index . get ( prop ) . push ( c ) ; } return index ; } , new PropIndex < EncodingConstraintModel < FieldQuery > [ ] > ( ) ) ;