diff --git a/src/compile/mark/mark.ts b/src/compile/mark/mark.ts index ff78106dfa..614c224a26 100644 --- a/src/compile/mark/mark.ts +++ b/src/compile/mark/mark.ts @@ -18,8 +18,7 @@ import {UnitModel} from '../unit'; import {isArray} from 'vega-util'; import {X, Y} from '../../channel'; -import {getFieldDef} from '../../encoding'; -import {field} from '../../fielddef'; +import {field, getFieldDef} from '../../fielddef'; import {isSelectionDomain} from '../../scale'; const markCompiler: {[type: string]: MarkCompiler} = { @@ -131,7 +130,7 @@ function detailFields(model: UnitModel): string[] { }); } } else { - const fieldDef = getFieldDef(encoding, channel); + const fieldDef = getFieldDef(encoding[channel]); if (fieldDef && !fieldDef.aggregate) { details.push(field(fieldDef, {binSuffix: 'start'})); } diff --git a/src/compile/mark/mixins.ts b/src/compile/mark/mixins.ts index 401c9f423f..a93e93f1c0 100644 --- a/src/compile/mark/mixins.ts +++ b/src/compile/mark/mixins.ts @@ -8,7 +8,7 @@ import {UnitModel} from '../unit'; import * as ref from './valueref'; import {NONSPATIAL_SCALE_CHANNELS} from '../../channel'; -import {Condition, FieldDef, isFieldDef, isValueDef} from '../../fielddef'; +import {Condition, FieldDef, getFieldDef, isValueDef} from '../../fielddef'; import {predicate} from '../selection/selection'; export function color(model: UnitModel) { @@ -63,6 +63,7 @@ export function nonPosition(channel: typeof NONSPATIAL_SCALE_CHANNELS[0], model: * Return a mixin that include a Vega production rule for a Vega-Lite conditional channel definition. * or a simple mixin if channel def has no condition. */ +// FIXME support ConditionFieldDef function wrapCondition(model: UnitModel, condition: Condition, vgChannel: string, valueRef: VgValueRef): VgEncodeEntry { if (condition) { const {selection, value} = condition; @@ -77,6 +78,7 @@ function wrapCondition(model: UnitModel, condition: Condition, vgChannel: s } } +// FIXME support ConditionFieldDef export function text(model: UnitModel, vgChannel: 'text' | 'tooltip' = 'text') { const channelDef = model.encoding[vgChannel]; const valueRef = (vgChannel === 'tooltip' && !channelDef) ? undefined : ref.text(channelDef, model.config); @@ -96,7 +98,7 @@ export function bandPosition(fieldDef: FieldDef, channel: 'x'|'y', model [channel+'c']: ref.fieldRef(fieldDef, scaleName, {}, {band: 0.5}) }; - if (isFieldDef(model.encoding.size)) { + if (getFieldDef(model.encoding.size)) { log.warn(log.message.cannotUseSizeFieldWithBandSize(channel)); // TODO: apply size to band and set scale range to some values between 0-1. // return { diff --git a/src/compile/mark/valueref.ts b/src/compile/mark/valueref.ts index 9b27f313c6..8392f7b010 100644 --- a/src/compile/mark/valueref.ts +++ b/src/compile/mark/valueref.ts @@ -93,6 +93,7 @@ export function midPoint(channel: Channel, channelDef: ChannelDef, scale if (channelDef) { /* istanbul ignore else */ + if (isFieldDef(channelDef)) { if (isBinScale(scale.type)) { // Use middle only for x an y to place marks in the center between start and end of the bin range. diff --git a/src/compile/model.ts b/src/compile/model.ts index 05a126747c..57667f2884 100644 --- a/src/compile/model.ts +++ b/src/compile/model.ts @@ -3,7 +3,7 @@ import {Channel, COLUMN, isChannel, NonspatialScaleChannel, ScaleChannel, X} fro import {CellConfig, Config} from '../config'; import {Data, DataSourceType, MAIN, RAW} from '../data'; import {forEach, reduce} from '../encoding'; -import {ChannelDef, field, FieldDef, FieldRefOption, isFieldDef, isRepeatRef} from '../fielddef'; +import {ChannelDef, field, FieldDef, FieldRefOption, getFieldDef, isFieldDef, isRepeatRef} from '../fielddef'; import {Legend} from '../legend'; import {hasDiscreteDomain, Scale} from '../scale'; import {SortField, SortOrder} from '../sort'; @@ -400,14 +400,19 @@ export abstract class ModelWithField extends Model { public reduceFieldDef(f: (acc: U, fd: FieldDef, c: Channel) => U, init: T, t?: any) { return reduce(this.getMapping(), (acc:U , cd: ChannelDef, c: Channel) => { - return isFieldDef(cd) ? f(acc, cd, c) : acc; + const fieldDef = getFieldDef(cd); + if (fieldDef) { + return f(acc, fieldDef, c); + } + return acc; }, init, t); } public forEachFieldDef(f: (fd: FieldDef, c: Channel) => void, t?: any) { forEach(this.getMapping(), (cd: ChannelDef, c: Channel) => { - if (isFieldDef(cd)) { - f(cd, c); + const fieldDef = getFieldDef(cd); + if (fieldDef) { + f(fieldDef, c); } }, t); } diff --git a/src/compile/unit.ts b/src/compile/unit.ts index bb160c216b..a0ecc8c80c 100644 --- a/src/compile/unit.ts +++ b/src/compile/unit.ts @@ -3,7 +3,7 @@ import {Channel, NONSPATIAL_SCALE_CHANNELS, UNIT_CHANNELS, UNIT_SCALE_CHANNELS, import {CellConfig, Config} from '../config'; import {Encoding, normalizeEncoding} from '../encoding'; import * as vlEncoding from '../encoding'; // TODO: remove -import {field, FieldDef, FieldRefOption, isFieldDef} from '../fielddef'; +import {field, FieldDef, FieldRefOption, getFieldDef, isConditionalDef, isFieldDef} from '../fielddef'; import {Legend} from '../legend'; import {FILL_STROKE_CONFIG, isMarkDef, Mark, MarkDef, TEXT as TEXT_MARK} from '../mark'; import {defaultScaleConfig, Domain, hasDiscreteDomain, Scale} from '../scale'; @@ -156,10 +156,13 @@ export class UnitModel extends ModelWithField { if (isFieldDef(channelDef)) { fieldDef = channelDef; specifiedScale = channelDef.scale; + } else if (isConditionalDef(channelDef) && isFieldDef(channelDef.condition)) { + fieldDef = channelDef.condition; + specifiedScale = channelDef.condition.scale; } else if (channel === 'x') { - fieldDef = vlEncoding.getFieldDef(encoding, 'x2'); + fieldDef = getFieldDef(encoding.x2); } else if (channel === 'y') { - fieldDef = vlEncoding.getFieldDef(encoding, 'y2'); + fieldDef = getFieldDef(encoding.y2); } if (fieldDef) { @@ -249,12 +252,15 @@ export class UnitModel extends ModelWithField { private initLegend(encoding: Encoding): Dict { return NONSPATIAL_SCALE_CHANNELS.reduce(function(_legend, channel) { const channelDef = encoding[channel]; - if (isFieldDef(channelDef)) { - const legendSpec = channelDef.legend; - if (legendSpec !== null && legendSpec !== false) { - _legend[channel] = {...legendSpec}; + if (channelDef) { + const legend = isFieldDef(channelDef) ? channelDef.legend : + (channelDef.condition && isFieldDef(channelDef.condition)) ? channelDef.condition.legend : null; + + if (legend !== null && legend !== false) { + _legend[channel] = {...legend}; } } + return _legend; }, {}); } diff --git a/src/encoding.ts b/src/encoding.ts index 62fe7c769c..107446e982 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -5,19 +5,21 @@ import {CompositeAggregate} from './compositemark'; import {Facet} from './facet'; import { ChannelDef, + Condition, + ConditionalLegendDef, + ConditionalTextDef, ConditionalValueDef, Field, FieldDef, isFieldDef, isValueDef, - LegendFieldDef, normalize, + normalizeFieldDef, OrderFieldDef, - PositionFieldDef, - TextFieldDef, + PositionDef, ValueDef } from './fielddef'; -import {Conditional, ConditionalLegendDef, ConditionalTextDef, normalizeFieldDef, PositionDef} from './fielddef'; +import {getFieldDef, hasConditionFieldDef} from './fielddef'; import * as log from './log'; import {Mark} from './mark'; import {isArray, some} from './util'; @@ -110,23 +112,12 @@ export function channelHasField(encoding: EncodingWithFacet, channel: Cha if (isArray(channelDef)) { return some(channelDef, (fieldDef) => !!fieldDef.field); } else { - return isFieldDef(channelDef); + return isFieldDef(channelDef) || hasConditionFieldDef(channelDef); } } return false; } -export function getFieldDef(encoding: EncodingWithFacet, channel: Channel): FieldDef { - const channelDef = encoding[channel]; - if (isArray(channelDef)) { - throw new Error('getFieldDef should be never used with detail or order when they have multiple fields'); - } else if (isFieldDef(channelDef)) { - return channelDef; - } - // TODO: if hasConditionFieldDef - - return undefined; -} export function isAggregate(encoding: EncodingWithFacet) { return some(CHANNELS, (channel) => { @@ -135,7 +126,8 @@ export function isAggregate(encoding: EncodingWithFacet) { if (isArray(channelDef)) { return some(channelDef, (fieldDef) => !!fieldDef.aggregate); } else { - return isFieldDef(channelDef) && !!channelDef.aggregate; + const fieldDef = getFieldDef(channelDef); + return fieldDef && !!fieldDef.aggregate; } } return false; @@ -153,8 +145,8 @@ export function normalizeEncoding(encoding: Encoding, mark: Mark): Encod // Drop line's size if the field is aggregated. if (channel === 'size' && mark === 'line') { - const channelDef = encoding[channel]; - if (isFieldDef(channelDef) && channelDef.aggregate) { + const fieldDef = getFieldDef(encoding[channel]); + if (fieldDef && fieldDef.aggregate) { log.warn(log.message.incompatibleChannel(channel, mark, 'when the field is aggregated.')); return normalizedEncoding; } @@ -200,6 +192,8 @@ export function fieldDefs(encoding: EncodingWithFacet): FieldDef[] (isArray(channelDef) ? channelDef : [channelDef]).forEach((def) => { if (isFieldDef(def)) { arr.push(def); + } else if (hasConditionFieldDef(def)) { + arr.push(def.condition); } }); } diff --git a/src/fielddef.ts b/src/fielddef.ts index 8a80c6963c..79649ab2d6 100644 --- a/src/fielddef.ts +++ b/src/fielddef.ts @@ -46,7 +46,7 @@ export type Condition = { * ... * } */ -export type ConditionalFieldDef = F & {condition?: Condition}; +export type ConditionalFieldDef, V extends ValueDef> = F & {condition?: Condition}; /** * A ValueDef with Condition @@ -55,7 +55,7 @@ export type ConditionalFieldDef = F & {condition?: Condition}; * value: ..., * } */ -export type ConditionalValueDef = V & {condition?: Condition}; +export type ConditionalValueDef, V extends ValueDef> = V & {condition?: Condition}; /** * Reference to a repeated value. @@ -173,7 +173,18 @@ export interface TextFieldDef extends FieldDef { export type ConditionalTextDef = Conditional, ValueDef>; -export type ChannelDef = FieldDef | ValueDef; +export type ChannelDef = Conditional, ValueDef>; + +export function isConditionalDef(channelDef: ChannelDef): channelDef is Conditional, ValueDef> { + return !!channelDef && !!channelDef.condition; +} + +/** + * Return if a channelDef is a ConditionalValueDef with ConditionFieldDef + */ +export function hasConditionFieldDef(channelDef: ChannelDef): channelDef is (ValueDef & {condition: FieldDef}) { + return !!channelDef && !!channelDef.condition && isFieldDef(channelDef.condition); +} export function isFieldDef(channelDef: ChannelDef): channelDef is FieldDef | PositionFieldDef | LegendFieldDef | OrderFieldDef | TextFieldDef { return !!channelDef && (!!channelDef['field'] || channelDef['aggregate'] === 'count'); @@ -296,13 +307,32 @@ export function defaultType(fieldDef: FieldDef, channel: Channel): Type { } } +/** + * Returns the fieldDef -- either from the outer channelDef or from the condition of channelDef. + * @param channelDef + */ +export function getFieldDef(channelDef: ChannelDef): FieldDef { + if (isFieldDef(channelDef)) { + return channelDef; + } else if (hasConditionFieldDef(channelDef)) { + return channelDef.condition; + } + return undefined; +} + /** * Convert type to full, lowercase type, or augment the fieldDef with a default type if missing. */ -export function normalize(channelDef: ChannelDef, channel: Channel) { +export function normalize(channelDef: ChannelDef, channel: Channel): ChannelDef { // If a fieldDef contains a field, we need type. if (isFieldDef(channelDef)) { return normalizeFieldDef(channelDef, channel); + } else if (hasConditionFieldDef(channelDef)) { + return { + ...channelDef, + // Need to cast as normalizeFieldDef normally return FieldDef, but here we know that it is definitely Condition + condition: normalizeFieldDef(channelDef.condition, channel) as Condition> + }; } return channelDef; } @@ -347,7 +377,6 @@ export function normalizeFieldDef(fieldDef: FieldDef, channel: Channel) log.warn(warning); } return fieldDef; - } export function normalizeBin(bin: Bin|boolean, channel: Channel) { diff --git a/src/stack.ts b/src/stack.ts index 85a4bd2796..f84cd21e96 100644 --- a/src/stack.ts +++ b/src/stack.ts @@ -3,7 +3,7 @@ import * as log from './log'; import {SUM_OPS} from './aggregate'; import {Channel, STACK_GROUP_CHANNELS, X, X2, Y, Y2} from './channel'; import {channelHasField, Encoding, isAggregate} from './encoding'; -import {Field, FieldDef, isFieldDef, PositionFieldDef} from './fielddef'; +import {Field, FieldDef, getFieldDef, isFieldDef, PositionFieldDef} from './fielddef'; import {AREA, BAR, CIRCLE, isMarkDef, LINE, Mark, MarkDef, POINT, RULE, SQUARE, TEXT, TICK} from './mark'; import {ScaleType} from './scale'; import {contains, isArray} from './util'; @@ -57,8 +57,9 @@ export function stack(m: Mark | MarkDef, encoding: Encoding, stackConfig: const stackBy = STACK_GROUP_CHANNELS.reduce((sc, channel) => { if (channelHasField(encoding, channel)) { const channelDef = encoding[channel]; - (isArray(channelDef) ? channelDef : [channelDef]).forEach((fieldDef) => { - if (isFieldDef(fieldDef) && !fieldDef.aggregate) { + (isArray(channelDef) ? channelDef : [channelDef]).forEach((cDef) => { + const fieldDef = getFieldDef(cDef); + if (!fieldDef.aggregate) { sc.push({ channel: channel, fieldDef: fieldDef