diff --git a/src/compile/layoutsize/init.ts b/src/compile/layoutsize/init.ts index 759fb9ef54..a44a42147c 100644 --- a/src/compile/layoutsize/init.ts +++ b/src/compile/layoutsize/init.ts @@ -17,12 +17,14 @@ export function initLayoutSize({ const sizeType = getSizeType(channel); const fieldDef = getFieldDef(encoding[channel]) as PositionFieldDef; if (isStep(size[sizeType])) { - if (isContinuous(fieldDef)) { - delete size[sizeType]; - log.warn(log.message.cannotUseStepWithContinuous(sizeType)); - } else if (isDiscrete(fieldDef) && fit) { - delete size[sizeType]; - log.warn(log.message.cannotUseStepWithFit(sizeType)); + if (fieldDef) { + if (isContinuous(fieldDef)) { + delete size[sizeType]; + log.warn(log.message.stepDropped(sizeType, 'continuous')); + } else if (isDiscrete(fieldDef) && fit) { + delete size[sizeType]; + log.warn(log.message.stepDropped(sizeType, 'fit')); + } } } } diff --git a/src/compile/scale/parse.ts b/src/compile/scale/parse.ts index 78d76b861a..fd25689a4c 100644 --- a/src/compile/scale/parse.ts +++ b/src/compile/scale/parse.ts @@ -20,14 +20,16 @@ import {parseScaleDomain} from './domain'; import {parseScaleProperty, parseScaleRange} from './properties'; import {scaleType} from './type'; -export function parseScales(model: Model) { +export function parseScales(model: Model, {ignoreRange}: {ignoreRange?: boolean} = {}) { parseScaleCore(model); parseScaleDomain(model); for (const prop of NON_TYPE_DOMAIN_RANGE_VEGA_SCALE_PROPERTIES) { parseScaleProperty(model, prop); } - // range depends on zero - parseScaleRange(model); + if (!ignoreRange) { + // range depends on zero + parseScaleRange(model); + } } export function parseScaleCore(model: Model) { diff --git a/src/compile/scale/range.ts b/src/compile/scale/range.ts index 772fe1ad5c..96200dcc4d 100644 --- a/src/compile/scale/range.ts +++ b/src/compile/scale/range.ts @@ -28,16 +28,14 @@ import { isContinuousToDiscrete, isExtendedScheme, Scale, - ScaleType, scaleTypeSupportProperty, Scheme } from '../../scale'; import {isStep, LayoutSizeMixins} from '../../spec/base'; -import {Type} from '../../type'; import * as util from '../../util'; import {isSignalRef, SchemeConfig, VgRange} from '../../vega.schema'; import {getBinSignalName} from '../data/bin'; -import {Rename, SignalRefWrapper} from '../signal'; +import {SignalRefWrapper} from '../signal'; import {Explicit, makeExplicit, makeImplicit} from '../split'; import {UnitModel} from '../unit'; import {ScaleComponentIndex} from './component'; @@ -57,31 +55,8 @@ export function parseUnitScaleRange(model: UnitModel) { if (!localScaleCmpt) { return; } - const mergedScaleCmpt = model.getScaleComponent(channel); - const specifiedScale = model.specifiedScales[channel]; - const fieldDef = model.fieldDef(channel); - - const scaleType = mergedScaleCmpt.get('type'); - const sizeType = getSizeType(channel); - - const rangeWithExplicit = parseRangeForChannel( - channel, - model.getSignalName.bind(model), - scaleType, - fieldDef.type, - specifiedScale, - model.config, - localScaleCmpt.get('zero'), - model.mark, - model.getName(sizeType), - model.size, - model.fit, - { - x: getBinStepSignal(model, 'x'), - y: getBinStepSignal(model, 'y') - } - ); + const rangeWithExplicit = parseRangeForChannel(channel, model); localScaleCmpt.setWithExplicit('range', rangeWithExplicit); }); @@ -108,20 +83,13 @@ function getBinStepSignal(model: UnitModel, channel: 'x' | 'y'): SignalRefWrappe /** * Return mixins that includes one of the Vega range types (explicit range, range.step, range.scheme). */ -export function parseRangeForChannel( - channel: Channel, - getSignalName: Rename, - scaleType: ScaleType, - type: Type, - specifiedScale: Scale, - config: Config, - zero: boolean, - mark: Mark, - sizeSignal: string, - size: LayoutSizeMixins, - fit: boolean = false, - xyStepSignals: {x?: SignalRefWrapper; y?: SignalRefWrapper} = {} -): Explicit { +export function parseRangeForChannel(channel: ScaleChannel, model: UnitModel): Explicit { + const specifiedScale = model.specifiedScales[channel]; + const {size, fit} = model; + + const mergedScaleCmpt = model.getScaleComponent(channel); + const scaleType = mergedScaleCmpt.get('type'); + // Check if any of the range properties is specified. // If so, check if it is compatible and make sure that we only output one of the properties for (const property of RANGE_PROPERTIES) { @@ -161,22 +129,7 @@ export function parseRangeForChannel( } } - return makeImplicit( - defaultRange( - channel, - getSignalName, - scaleType, - type, - config, - zero, - mark, - sizeSignal, - size, - xyStepSignals, - specifiedScale.domain, - fit - ) - ); + return makeImplicit(defaultRange(channel, model)); } function parseScheme(scheme: Scheme): SchemeConfig { @@ -189,23 +142,21 @@ function parseScheme(scheme: Scheme): SchemeConfig { return {scheme: scheme}; } -function defaultRange( - channel: Channel, - getSignalName: Rename, - scaleType: ScaleType, - type: Type, - config: Config, - zero: boolean, - mark: Mark, - sizeSignal: string, - size: LayoutSizeMixins, - xyStepSignals: {x?: SignalRefWrapper; y?: SignalRefWrapper}, - domain: Domain, - fit: boolean -): VgRange { +function defaultRange(channel: ScaleChannel, model: UnitModel): VgRange { + const {size, config, fit, mark} = model; + + const getSignalName = model.getSignalName.bind(model); + + const {type} = model.fieldDef(channel); + + const mergedScaleCmpt = model.getScaleComponent(channel); + const scaleType = mergedScaleCmpt.get('type'); + + const {domain} = model.specifiedScales[channel]; + switch (channel) { case X: - case Y: + case Y: { // If there is no explicit width/height for discrete x/y scales if (util.contains(['point', 'band'], scaleType)) { if (channel === X && !size.width) { @@ -228,16 +179,21 @@ function defaultRange( // We will later replace these temporary names with // the final name in assembleScaleRange() + const sizeType = getSizeType(channel); + const sizeSignal = model.getName(sizeType); + if (channel === Y && hasContinuousDomain(scaleType)) { // For y continuous scale, we have to start from the height as the bottom part has the max value. return [SignalRefWrapper.fromName(getSignalName, sizeSignal), 0]; } else { return [0, SignalRefWrapper.fromName(getSignalName, sizeSignal)]; } + } case SIZE: { // TODO: support custom rangeMin, rangeMax + const zero = model.component.scales[channel].get('zero'); const rangeMin = sizeRangeMin(mark, zero, config); - const rangeMax = sizeRangeMax(mark, size, xyStepSignals, config); + const rangeMax = sizeRangeMax(mark, size, model, config); if (isContinuousToDiscrete(scaleType)) { return interpolateRange( rangeMin, @@ -341,12 +297,12 @@ function sizeRangeMin(mark: Mark, zero: boolean, config: Config) { export const MAX_SIZE_RANGE_STEP_RATIO = 0.95; -function sizeRangeMax( - mark: Mark, - size: LayoutSizeMixins, - xyStepSignals: {x?: SignalRefWrapper; y?: SignalRefWrapper}, - config: Config -): number | SignalRef { +function sizeRangeMax(mark: Mark, size: LayoutSizeMixins, model: UnitModel, config: Config): number | SignalRef { + const xyStepSignals = { + x: getBinStepSignal(model, 'x'), + y: getBinStepSignal(model, 'y') + }; + switch (mark) { case 'bar': case 'tick': { diff --git a/src/log/message.ts b/src/log/message.ts index 17b550f4df..edd9f4b7f3 100644 --- a/src/log/message.ts +++ b/src/log/message.ts @@ -21,14 +21,6 @@ export const INVALID_SPEC = 'Invalid spec'; // FIT export const FIT_NON_SINGLE = 'Autosize "fit" only works for single views and layered views.'; -export function cannotUseStepWithFit(sizeType: 'width' | 'height') { - return `Cannot use a "step" ${sizeType} when "autosize" is "fit".`; -} - -export function cannotUseStepWithContinuous(sizeType: 'width' | 'height') { - return `Cannot use a "step" ${sizeType} when the ${sizeType === 'width' ? 'x' : 'y'}-field is continuous.`; -} - // SELECTION export function cannotProjectOnChannelWithoutField(channel: Channel) { return `Cannot project a selection on encoding channel "${channel}", which has no field.`; diff --git a/src/scale.ts b/src/scale.ts index 7e320601b6..c28a40e6eb 100644 --- a/src/scale.ts +++ b/src/scale.ts @@ -124,6 +124,8 @@ export function scaleTypePrecedence(scaleType: ScaleType): number { export const CONTINUOUS_TO_CONTINUOUS_SCALES: ScaleType[] = ['linear', 'log', 'pow', 'sqrt', 'symlog', 'time', 'utc']; const CONTINUOUS_TO_CONTINUOUS_INDEX = toSet(CONTINUOUS_TO_CONTINUOUS_SCALES); +export const QUANTITATIVE_SCALES: ScaleType[] = ['linear', 'log', 'pow', 'sqrt', 'symlog']; + export const CONTINUOUS_TO_DISCRETE_SCALES: ScaleType[] = ['quantile', 'quantize', 'threshold']; const CONTINUOUS_TO_DISCRETE_INDEX = toSet(CONTINUOUS_TO_DISCRETE_SCALES); diff --git a/test/compile/compile.test.ts b/test/compile/compile.test.ts index 9fd9896db5..8957d2c6d2 100644 --- a/test/compile/compile.test.ts +++ b/test/compile/compile.test.ts @@ -135,7 +135,7 @@ describe('compile/compile', () => { x: {field: 'b', type: 'quantitative'} } }).spec; - expect(localLogger.warns[0]).toEqual(log.message.cannotUseStepWithFit('height')); + expect(localLogger.warns[0]).toEqual(log.message.stepDropped('height', 'fit')); expect(spec.width).toEqual(200); expect(spec.height).toEqual(200); }) diff --git a/test/compile/layoutsize/init.test.ts b/test/compile/layoutsize/init.test.ts index 9ac70364e9..3ca294c076 100644 --- a/test/compile/layoutsize/init.test.ts +++ b/test/compile/layoutsize/init.test.ts @@ -14,7 +14,7 @@ describe('compile/layout', () => { } }); - expect(localLogger.warns[0]).toEqual(log.message.cannotUseStepWithContinuous('width')); + expect(localLogger.warns[0]).toEqual(log.message.stepDropped('width', 'continuous')); expect(model.component.layoutSize.get('width')).toBe(200); }) diff --git a/test/compile/scale/range.test.ts b/test/compile/scale/range.test.ts index 4b9ea29bb9..0b9e843ed2 100644 --- a/test/compile/scale/range.test.ts +++ b/test/compile/scale/range.test.ts @@ -8,208 +8,125 @@ import {makeExplicit, makeImplicit} from '../../../src/compile/split'; import {Config, defaultConfig, DEFAULT_STEP} from '../../../src/config'; import * as log from '../../../src/log'; import {Mark} from '../../../src/mark'; -import {CONTINUOUS_TO_CONTINUOUS_SCALES, DISCRETE_DOMAIN_SCALES, ScaleType} from '../../../src/scale'; -import {NOMINAL, ORDINAL, QUANTITATIVE} from '../../../src/type'; - -const identity = (x: string) => x; +import {QUANTITATIVE_SCALES, ScaleType} from '../../../src/scale'; +import {parseUnitModelWithScaleExceptRange} from '../../util'; describe('compile/scale', () => { describe('parseRange()', () => { describe('position', () => { - it('should return [0, plot_width] for x-continuous scales by default.', () => { - for (const scaleType of CONTINUOUS_TO_CONTINUOUS_SCALES) { - expect( - parseRangeForChannel( - 'x', - identity, - scaleType, - QUANTITATIVE, - {}, - defaultConfig, - true, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([0, {signal: 'plot_width'}])); - } - }); + it('should return [0, width] / [height, 0] for x/y-continuous scales by default.', () => { + for (const scaleType of QUANTITATIVE_SCALES) { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + x: {field: 'x', type: 'quantitative', scale: {type: scaleType}}, + y: {field: 'y', type: 'quantitative', scale: {type: scaleType}} + } + }); + + expect(parseRangeForChannel('x', model)).toEqual(makeImplicit([0, {signal: 'width'}])); - it('should return [plot_height,0] for y-continuous scales by default.', () => { - for (const scaleType of CONTINUOUS_TO_CONTINUOUS_SCALES) { - expect( - parseRangeForChannel( - 'y', - identity, - scaleType, - QUANTITATIVE, - {}, - defaultConfig, - true, - 'point', - 'plot_height', - {} - ) - ).toEqual(makeImplicit([{signal: 'plot_height'}, 0])); + expect(parseRangeForChannel('y', model)).toEqual(makeImplicit([{signal: 'height'}, 0])); } }); - it('should return [0, plot_height] for y-discrete scales with height by default.', () => { - for (const scaleType of DISCRETE_DOMAIN_SCALES) { - expect( - parseRangeForChannel( - 'y', - identity, - scaleType, - QUANTITATIVE, - {}, - defaultConfig, - true, - 'point', - 'plot_height', - {height: 200} - ) - ).toEqual(makeImplicit([0, {signal: 'plot_height'}])); + it('should return [0, width] / [height, 0] for x/y-discrete scales with width/height by default.', () => { + for (const scaleType of [ScaleType.BAND, ScaleType.POINT]) { + const model = parseUnitModelWithScaleExceptRange({ + width: 200, + height: 200, + mark: 'point', + encoding: { + x: {field: 'x', type: 'nominal', scale: {type: scaleType}}, + y: {field: 'y', type: 'nominal', scale: {type: scaleType}} + } + }); + + expect(parseRangeForChannel('x', model)).toEqual(makeImplicit([0, {signal: 'width'}])); + + expect(parseRangeForChannel('y', model)).toEqual(makeImplicit([0, {signal: 'height'}])); } }); - it( - 'should support custom range.', - log.wrap(localLogger => { - expect( - parseRangeForChannel( - 'x', - identity, - 'linear', - QUANTITATIVE, - {range: [0, 100]}, - defaultConfig, - true, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeExplicit([0, 100])); - expect(localLogger.warns.length).toEqual(0); - }) - ); - - it('should return config.view.discreteWidth for x- band/point scales by default.', () => { - for (const scaleType of ['point', 'band'] as ScaleType[]) { - expect( - parseRangeForChannel( - 'x', - identity, - scaleType, - NOMINAL, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(expect.objectContaining(makeImplicit({step: 20}))); + it('should return [0, width] / [height, 0] for x/y-discrete scales with numberic config.view.discreteWidth/Height', () => { + for (const scaleType of [ScaleType.BAND, ScaleType.POINT]) { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + x: {field: 'x', type: 'nominal', scale: {type: scaleType}}, + y: {field: 'y', type: 'nominal', scale: {type: scaleType}} + }, + config: { + view: {discreteWidth: 200, discreteHeight: 200} + } + }); + + expect(parseRangeForChannel('x', model)).toEqual(makeImplicit([0, {signal: 'width'}])); + + expect(parseRangeForChannel('y', model)).toEqual(makeImplicit([0, {signal: 'height'}])); } }); - it('should return config.view.discreteWidth for y- band/point scales by default.', () => { - for (const scaleType of ['point', 'band'] as ScaleType[]) { - expect( - parseRangeForChannel( - 'y', - identity, - scaleType, - NOMINAL, - {}, - defaultConfig, - undefined, - 'point', - 'plot_height', - {} - ) - ).toEqual(expect.objectContaining(makeImplicit({step: 20}))); + it('should return config.view.discreteWidth for x/y-band/point scales by default.', () => { + for (const scaleType of [ScaleType.BAND, ScaleType.POINT]) { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + x: {field: 'x', type: 'nominal', scale: {type: scaleType}}, + y: {field: 'y', type: 'nominal', scale: {type: scaleType}} + } + }); + + expect(parseRangeForChannel('x', model)).toEqual(makeImplicit({step: 20})); + + expect(parseRangeForChannel('y', model)).toEqual(makeImplicit({step: 20})); } }); - it( - 'should drop rangeStep if model is fit', - log.wrap(localLogger => { - for (const scaleType of ['point', 'band'] as ScaleType[]) { - expect( - parseRangeForChannel( - 'x', - identity, - scaleType, - NOMINAL, - {}, - defaultConfig, - undefined, - 'text', - 'plot_width', - {width: {step: 23}}, - true - ) - ).toEqual(makeImplicit([0, {signal: 'plot_width'}])); + it('should drop rangeStep if model is fit', () => { + const model = parseUnitModelWithScaleExceptRange({ + autosize: 'fit', + mark: 'point', + encoding: { + x: {field: 'x', type: 'nominal'}, + y: {field: 'y', type: 'nominal'} } - expect(localLogger.warns[0]).toEqual(log.message.stepDropped('width', 'fit')); - }) - ); - it('should return specified step if topLevelSize is undefined for band/point scales', () => { - for (const scaleType of ['point', 'band'] as ScaleType[]) { - expect( - parseRangeForChannel( - 'x', - identity, - scaleType, - NOMINAL, - {}, - defaultConfig, - undefined, - 'text', - 'plot_width', - {width: {step: 23}} - ) - ).toEqual(makeExplicit({step: 23})); - } + }); + + expect(parseRangeForChannel('x', model)).toEqual(makeImplicit([0, {signal: 'width'}])); + expect(parseRangeForChannel('y', model)).toEqual(makeImplicit([0, {signal: 'height'}])); }); - it('should return default topLevelSize if config.view.discreteWidth is a number', () => { - for (const scaleType of ['point', 'band'] as ScaleType[]) { - expect( - parseRangeForChannel( - 'x', - identity, - scaleType, - NOMINAL, - {}, - {view: {discreteWidth: 200}}, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([0, {signal: 'plot_width'}])); + it('should return specified step for band/point scales', () => { + for (const scaleType of [ScaleType.BAND, ScaleType.POINT]) { + const model = parseUnitModelWithScaleExceptRange({ + width: {step: 23}, + height: {step: 24}, + mark: 'point', + encoding: { + x: {field: 'x', type: 'nominal', scale: {type: scaleType}}, + y: {field: 'y', type: 'nominal', scale: {type: scaleType}} + } + }); + + expect(parseRangeForChannel('x', model)).toEqual(makeExplicit({step: 23})); + + expect(parseRangeForChannel('y', model)).toEqual(makeExplicit({step: 24})); } }); it('should drop rangeStep for continuous scales', () => { - for (const scaleType of CONTINUOUS_TO_CONTINUOUS_SCALES) { + for (const scaleType of QUANTITATIVE_SCALES) { log.wrap(localLogger => { - expect( - parseRangeForChannel( - 'x', - identity, - scaleType, - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'text', - 'plot_width', - {width: {step: 23}} - ) - ).toEqual(makeImplicit([0, {signal: 'plot_width'}])); + const model = parseUnitModelWithScaleExceptRange({ + width: {step: 23}, + mark: 'point', + encoding: { + x: {field: 'x', type: 'quantitative', scale: {type: scaleType}} + } + }); + + expect(parseRangeForChannel('x', model)).toEqual(makeImplicit([0, {signal: 'width'}])); expect(localLogger.warns[0]).toEqual(log.message.stepDropped('width', 'continuous')); })(); } @@ -217,453 +134,368 @@ describe('compile/scale', () => { }); describe('color', () => { - it('should use the specified scheme for a nominal color field.', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'ordinal', - NOMINAL, - {scheme: 'warm'}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeExplicit({scheme: 'warm'})); + it('should support custom scheme.', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'quantitative', scale: {scheme: 'viridis'}} + } + }); + + expect(parseRangeForChannel('color', model)).toEqual(makeExplicit({scheme: 'viridis'})); }); it('should use the specified scheme with extent for a nominal color field.', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'ordinal', - NOMINAL, - {scheme: {name: 'warm', extent: [0.2, 1]}}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeExplicit({scheme: 'warm', extent: [0.2, 1]})); + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'quantitative', scale: {scheme: {name: 'warm', extent: [0.2, 1]}}} + } + }); + + expect(parseRangeForChannel('color', model)).toEqual(makeExplicit({scheme: 'warm', extent: [0.2, 1]})); }); - it('should use the specified range for a nominal color field.', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'ordinal', - NOMINAL, - {range: ['red', 'green', 'blue']}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeExplicit(['red', 'green', 'blue'])); + it('should support custom range.', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'nominal', scale: {range: ['red', 'blue']}} + } + }); + expect(parseRangeForChannel('color', model)).toEqual(makeExplicit(['red', 'blue'])); }); it('should use default category range in Vega for a nominal color field.', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'ordinal', - NOMINAL, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit('category')); + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'nominal'} + } + }); + + expect(parseRangeForChannel('color', model)).toEqual(makeImplicit('category')); }); it('should use default ordinal range in Vega for an ordinal color field.', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'ordinal', - ORDINAL, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit('ordinal')); + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'ordinal'} + } + }); + + expect(parseRangeForChannel('color', model)).toEqual(makeImplicit('ordinal')); }); it('should use default ramp range in Vega for a temporal/quantitative color field.', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit('ramp')); - }); + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'quantitative'} + } + }); - it('should use the specified scheme with count for a quantitative color field.', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'ordinal', - QUANTITATIVE, - {scheme: {name: 'viridis', count: 3}}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeExplicit({scheme: 'viridis', count: 3})); + expect(parseRangeForChannel('color', model)).toEqual(makeImplicit('ramp')); }); - it('should use default ramp range for quantile/quantize scales', () => { - const scales: ScaleType[] = ['quantile', 'quantize']; - scales.forEach(discretizingScale => { - expect( - parseRangeForChannel( - 'color', - identity, - discretizingScale, - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit('ramp')); + it('should use the specified scheme with count for a quantitative color field.', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'quantitative', scale: {scheme: {name: 'viridis', count: 3}}} + } }); - }); - it('should use default ramp range for threshold scale', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'threshold', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit('ramp')); + expect(parseRangeForChannel('color', model)).toEqual(makeExplicit({scheme: 'viridis', count: 3})); }); - it('should use default color range for log scale', () => { - expect( - parseRangeForChannel( - 'color', - identity, - 'log', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit('ramp')); + it('should use default ramp range for quantile/quantize/threshold scales', () => { + const scales: ScaleType[] = ['quantile', 'quantize', 'threshold']; + scales.forEach(discretizingScale => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + color: {field: 'x', type: 'quantitative', scale: {type: discretizingScale}} + } + }); + + expect(parseRangeForChannel('color', model)).toEqual(makeImplicit('ramp')); + }); }); }); describe('opacity', () => { it("should use default opacityRange as opacity's scale range.", () => { - expect( - parseRangeForChannel( - 'opacity', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([defaultConfig.scale.minOpacity, defaultConfig.scale.maxOpacity])); + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + opacity: {field: 'x', type: 'quantitative'} + } + }); + + expect(parseRangeForChannel('opacity', model)).toEqual( + makeImplicit([defaultConfig.scale.minOpacity, defaultConfig.scale.maxOpacity]) + ); }); }); describe('size', () => { describe('bar', () => { - it('should return [minBandSize, maxBandSize] if both are specified', () => { - const config = { - scale: {minBandSize: 2, maxBandSize: 9} - }; - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - config, - undefined, - 'bar', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([2, 9])); + it('should return [minBandSize, maxBandSize] from config.bar when zero is excluded if both are specified', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'bar', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + }, + config: { + scale: {minBandSize: 2, maxBandSize: 9} + } + }); + + expect(parseRangeForChannel('size', model)).toEqual(makeImplicit([2, 9])); }); - it('should return [continuousBandSize, xRangeStep-1] by default since min/maxSize config are not specified', () => { - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'bar', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([2, DEFAULT_STEP - 1])); + it('should return [continuousBandSize, xRangeStep-1] when zero is excluded by default since min/maxSize config are not specified', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'bar', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + } + }); + + expect(parseRangeForChannel('size', model)).toEqual(makeImplicit([2, DEFAULT_STEP - 1])); }); }); describe('tick', () => { - it('should return [minBandSize, maxBandSize] if both are specified', () => { - const config = { - scale: {minBandSize: 4, maxBandSize: 9} - }; - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - config, - undefined, - 'tick', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([4, 9])); + it('should return [minBandSize, maxBandSize] when zero is excluded if both are specified', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'tick', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + }, + config: { + scale: {minBandSize: 2, maxBandSize: 9} + } + }); + + expect(parseRangeForChannel('size', model)).toEqual(makeImplicit([2, 9])); }); - it('should return [(default)minBandSize, step-1] by default since maxSize config is not specified', () => { - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'tick', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([defaultConfig.scale.minBandSize, DEFAULT_STEP - 1])); + it('should return [(default)minBandSize, step-1] when zero is excluded by default since maxSize config is not specified', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'tick', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + } + }); + + expect(parseRangeForChannel('size', model)).toEqual(makeImplicit([2, DEFAULT_STEP - 1])); }); }); describe('text', () => { - it('should return [minFontSize, maxFontSize]', () => { - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'text', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([defaultConfig.scale.minFontSize, defaultConfig.scale.maxFontSize])); + it('should return [minFontSize, maxFontSize] when zero is excluded', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'text', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + } + }); + + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit([defaultConfig.scale.minFontSize, defaultConfig.scale.maxFontSize]) + ); }); }); describe('rule', () => { - it('should return [minStrokeWidth, maxStrokeWidth]', () => { - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'rule', - 'plot_width', - {} - ) - ).toEqual(makeImplicit([defaultConfig.scale.minStrokeWidth, defaultConfig.scale.maxStrokeWidth])); + it('should return [minStrokeWidth, maxStrokeWidth] when zero is excluded', () => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'rule', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + } + }); + + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit([defaultConfig.scale.minStrokeWidth, defaultConfig.scale.maxStrokeWidth]) + ); }); }); describe('point, square, circle', () => { - it('should return [minSize, maxSize]', () => { - for (const m of ['point', 'square', 'circle'] as Mark[]) { - const config = { - scale: { - minSize: 5, - maxSize: 25 + it('should return [minSize, maxSize] when zero is excluded', () => { + for (const mark of ['point', 'square', 'circle'] as Mark[]) { + const model = parseUnitModelWithScaleExceptRange({ + mark, + encoding: { + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + }, + config: { + scale: { + minSize: 5, + maxSize: 25 + } + } + }); + + expect(parseRangeForChannel('size', model)).toEqual(makeImplicit([5, 25])); + } + }); + + it('should return [0, maxSize] when zero is included', () => { + for (const mark of ['point', 'square', 'circle'] as Mark[]) { + const model = parseUnitModelWithScaleExceptRange({ + mark, + encoding: { + size: {field: 'x', type: 'quantitative'} + }, + config: { + scale: { + minSize: 5, + maxSize: 25 + } } - }; + }); - expect( - parseRangeForChannel('size', identity, 'linear', QUANTITATIVE, {}, config, undefined, m, 'plot_width', {}) - ).toEqual(makeImplicit([5, 25])); + expect(parseRangeForChannel('size', model)).toEqual(makeImplicit([0, 25])); } }); it('should return [0, (minBandSize-2)^2] if both x and y are discrete and size is quantitative (thus use zero=true, by default)', () => { - for (const m of ['point', 'square', 'circle'] as Mark[]) { - expect( - parseRangeForChannel('size', identity, 'linear', QUANTITATIVE, {}, defaultConfig, true, m, 'plot_width', { - width: {step: 11}, - height: {step: 13} - }) - ).toEqual(makeImplicit([0, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11])); + for (const mark of ['point', 'square', 'circle'] as Mark[]) { + const model = parseUnitModelWithScaleExceptRange({ + width: {step: 11}, + height: {step: 13}, + mark, + encoding: { + x: {field: 'x', type: 'nominal'}, + y: {field: 'y', type: 'nominal'}, + size: {field: 'x', type: 'quantitative'} + } + }); + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit([0, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11]) + ); } }); it('should return [9, (minBandSize-2)^2] if both x and y are discrete and size is not quantitative (thus use zero=false, by default)', () => { - for (const m of ['point', 'square', 'circle'] as Mark[]) { - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - false, - m, - 'plot_width', - { - width: {step: 11}, - height: {step: 13} - } - ) - ).toEqual(makeImplicit([9, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11])); + for (const mark of ['point', 'square', 'circle'] as Mark[]) { + const model = parseUnitModelWithScaleExceptRange({ + width: {step: 11}, + height: {step: 13}, + mark, + encoding: { + x: {field: 'x', type: 'nominal'}, + y: {field: 'y', type: 'nominal'}, + size: {field: 'x', type: 'nominal'} + } + }); + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit([9, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11]) + ); } }); it('should return [9, (minBandSize-2)^2] if both x and y are discrete and size is quantitative but use zero=false', () => { - for (const m of ['point', 'square', 'circle'] as Mark[]) { - expect( - parseRangeForChannel( - 'size', - identity, - 'linear', - QUANTITATIVE, - {}, - defaultConfig, - false, - m, - 'plot_width', - { - width: {step: 11}, - height: {step: 13} - } - ) - ).toEqual(makeImplicit([9, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11])); + for (const mark of ['point', 'square', 'circle'] as Mark[]) { + const model = parseUnitModelWithScaleExceptRange({ + width: {step: 11}, + height: {step: 13}, + mark, + encoding: { + x: {field: 'x', type: 'nominal'}, + y: {field: 'y', type: 'nominal'}, + size: {field: 'x', type: 'quantitative', scale: {zero: false}} + } + }); + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit([9, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11]) + ); } }); it('should return [0, (xRangeStep-2)^2] if x is discrete and y is continuous and size is quantitative (thus use zero=true, by default)', () => { - for (const m of ['point', 'square', 'circle'] as Mark[]) { - expect( - parseRangeForChannel('size', identity, 'linear', QUANTITATIVE, {}, defaultConfig, true, m, 'plot_width', { - width: {step: 11}, - height: {step: 13} - }) - ).toEqual(makeImplicit([0, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11])); + for (const mark of ['point', 'square', 'circle'] as Mark[]) { + const model = parseUnitModelWithScaleExceptRange({ + width: {step: 11}, + mark, + encoding: { + x: {field: 'x', type: 'nominal'}, + y: {field: 'y', type: 'quantitative'}, + size: {field: 'x', type: 'quantitative'} + } + }); + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit([0, MAX_SIZE_RANGE_STEP_RATIO * 11 * MAX_SIZE_RANGE_STEP_RATIO * 11]) + ); + } + }); + + it('should return signal to calculate appropriate size if x is discrete and y is binned continuous and size is quantitative ', () => { + for (const mark of ['point', 'square', 'circle'] as Mark[]) { + const model = parseUnitModelWithScaleExceptRange({ + width: {step: 11}, + mark, + encoding: { + x: {field: 'x', type: 'nominal'}, + y: {bin: true, field: 'y', type: 'quantitative'}, + size: {field: 'x', type: 'quantitative'} + } + }); + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit([ + 0, + { + signal: + 'pow(0.95 * min(11, height / ((bin_maxbins_10_y_bins.stop - bin_maxbins_10_y_bins.start) / bin_maxbins_10_y_bins.step)), 2)' + } + ]) + ); } }); it('should return range interpolation of length 4 for quantile/quantize scales', () => { const scales: ScaleType[] = ['quantile', 'quantize']; - scales.forEach(discretizingScale => { - expect( - parseRangeForChannel( - 'size', - identity, - discretizingScale, - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit({signal: 'sequence(9, 361 + (361 - 9) / (4 - 1), (361 - 9) / (4 - 1))'})); + scales.forEach(type => { + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {type}} + } + }); + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit({signal: 'sequence(9, 361 + (361 - 9) / (4 - 1), (361 - 9) / (4 - 1))'}) + ); }); }); it('should return range interpolation of length 4 for threshold scale', () => { - expect( - parseRangeForChannel( - 'size', - identity, - 'threshold', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit({signal: 'sequence(9, 361 + (361 - 9) / (3 - 1), (361 - 9) / (3 - 1))'})); + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + size: {field: 'x', type: 'quantitative', scale: {type: 'threshold'}} + } + }); + expect(parseRangeForChannel('size', model)).toEqual( + makeImplicit({signal: 'sequence(9, 361 + (361 - 9) / (3 - 1), (361 - 9) / (3 - 1))'}) + ); }); }); }); describe('shape', () => { it("should use default symbol range in Vega as shape's scale range.", () => { - expect( - parseRangeForChannel( - 'shape', - identity, - 'ordinal', - QUANTITATIVE, - {}, - defaultConfig, - undefined, - 'point', - 'plot_width', - {} - ) - ).toEqual(makeImplicit('symbol')); + const model = parseUnitModelWithScaleExceptRange({ + mark: 'point', + encoding: { + shape: {field: 'x', type: 'nominal'} + } + }); + expect(parseRangeForChannel('shape', model)).toEqual(makeImplicit('symbol')); }); }); }); diff --git a/test/util.ts b/test/util.ts index 5dfb37581c..74400dbcca 100644 --- a/test/util.ts +++ b/test/util.ts @@ -4,6 +4,7 @@ import {FacetModel} from '../src/compile/facet'; import {LayerModel} from '../src/compile/layer'; import {Model} from '../src/compile/model'; import {RepeatModel} from '../src/compile/repeat'; +import {parseScales} from '../src/compile/scale/parse'; import {UnitModel} from '../src/compile/unit'; import {initConfig} from '../src/config'; import {normalize} from '../src/normalize/index'; @@ -51,6 +52,12 @@ export function parseUnitModelWithScale(spec: TopLevel) { return model; } +export function parseUnitModelWithScaleExceptRange(spec: TopLevel) { + const model = parseUnitModel(spec); + parseScales(model, {ignoreRange: true}); + return model; +} + export function parseUnitModelWithScaleAndLayoutSize(spec: TopLevel) { const model = parseUnitModelWithScale(spec); model.parseLayoutSize();