diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index 446bb1cd95..c99b9e876e 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -28308,6 +28308,11 @@ ], "description": "For text marks, the vertical text baseline. One of `\"alphabetic\"` (default), `\"top\"`, `\"middle\"`, `\"bottom\"`, `\"line-top\"`, `\"line-bottom\"`, or an expression reference that provides one of the valid values. The `\"line-top\"` and `\"line-bottom\"` values operate similarly to `\"top\"` and `\"bottom\"`, but are calculated relative to the `lineHeight` rather than `fontSize` alone.\n\nFor range marks, the vertical alignment of the marks. One of `\"top\"`, `\"middle\"`, `\"bottom\"`.\n\n__Note:__ Expression reference is *not* supported for range marks." }, + "binSpacing": { + "description": "Offset between bars for binned field. The ideal value for this is either 0 (preferred by statisticians) or 1 (Vega-Lite default, D3 example style).\n\n__Default value:__ `1`", + "minimum": 0, + "type": "number" + }, "blend": { "anyOf": [ { @@ -28333,6 +28338,11 @@ ], "description": "Default color.\n\n__Default value:__ `\"#4682b4\"`\n\n__Note:__\n- This property cannot be used in a [style config](https://vega.github.io/vega-lite/docs/mark.html#style-config).\n- The `fill` and `stroke` properties have higher precedence than `color` and will override `color`." }, + "continuousBandSize": { + "description": "The default size of the bars on continuous scales.\n\n__Default value:__ `5`", + "minimum": 0, + "type": "number" + }, "cornerRadius": { "anyOf": [ { @@ -28421,6 +28431,18 @@ } ] }, + "discreteBandSize": { + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/RelativeBandSize" + } + ], + "description": "The default size of the bars with discrete dimensions. If unspecified, the default size is `step-2`, which provides 2 pixel offset between bars.", + "minimum": 0 + }, "dx": { "anyOf": [ { @@ -28633,6 +28655,17 @@ } ] }, + "minBandSize": { + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/definitions/ExprRef" + } + ], + "description": "The minimum band size for bar and rectangle marks. __Default value:__ `0.25`" + }, "opacity": { "anyOf": [ { diff --git a/src/compile/mark/encode/position-rect.ts b/src/compile/mark/encode/position-rect.ts index 05bd00bdc2..4223b149f4 100644 --- a/src/compile/mark/encode/position-rect.ts +++ b/src/compile/mark/encode/position-rect.ts @@ -52,7 +52,9 @@ export function rectPosition(model: UnitModel, channel: 'x' | 'y' | 'theta' | 'r const offsetScaleChannel = getOffsetChannel(channel); - const isBarBand = mark === 'bar' && (channel === 'x' ? orient === 'vertical' : orient === 'horizontal'); + const isBarOrTickBand = + (mark === 'bar' && (channel === 'x' ? orient === 'vertical' : orient === 'horizontal')) || + (mark === 'tick' && (channel === 'y' ? orient === 'vertical' : orient === 'horizontal')); // x, x2, and width -- we must specify two of these in all conditions if ( @@ -68,7 +70,7 @@ export function rectPosition(model: UnitModel, channel: 'x' | 'y' | 'theta' | 'r channel, model }); - } else if (((isFieldOrDatumDef(channelDef) && hasDiscreteDomain(scaleType)) || isBarBand) && !channelDef2) { + } else if (((isFieldOrDatumDef(channelDef) && hasDiscreteDomain(scaleType)) || isBarOrTickBand) && !channelDef2) { return positionAndSize(channelDef, channel, model); } else { return rangePosition(channel, model, {defaultPos: 'zeroOrMax', defaultPos2: 'zeroOrMin'}); @@ -118,8 +120,11 @@ function defaultSizeRef( } } if (!hasFieldDef) { - const {bandPaddingInner, barBandPaddingInner, rectBandPaddingInner} = config.scale; - const padding = getFirstDefined(bandPaddingInner, mark === 'bar' ? barBandPaddingInner : rectBandPaddingInner); // this part is like paddingInner in scale.ts + const {bandPaddingInner, barBandPaddingInner, rectBandPaddingInner, tickBandPaddingInner} = config.scale; + const padding = getFirstDefined( + bandPaddingInner, + mark === 'tick' ? tickBandPaddingInner : mark === 'bar' ? barBandPaddingInner : rectBandPaddingInner + ); // this part is like paddingInner in scale.ts if (isSignalRef(padding)) { return {signal: `(1 - (${padding.signal})) * ${sizeChannel}`}; } else if (isNumber(padding)) { @@ -150,8 +155,12 @@ function positionAndSize( const offsetScaleName = model.scaleName(offsetScaleChannel); const offsetScale = model.getScaleComponent(getOffsetScaleChannel(channel)); - // use "size" channel for bars, if there is orient and the channel matches the right orientation - const useVlSizeChannel = (orient === 'horizontal' && channel === 'y') || (orient === 'vertical' && channel === 'x'); + const useVlSizeChannel = + // Always uses size channel for ticks, because tick only calls rectPosition() for the size channel + markDef.type === 'tick' || + // use "size" channel for bars, if there is orient and the channel matches the right orientation + (orient === 'horizontal' && channel === 'y') || + (orient === 'vertical' && channel === 'x'); // Use size encoding / mark property / config if it exists let sizeMixins; @@ -315,7 +324,7 @@ function rectBinPosition({ const axis = (model.component.axes as any)[channel]?.[0]; const axisTranslate = axis?.get('translate') ?? 0.5; // vega default is 0.5 - const spacing = isXorY(channel) ? (getMarkPropOrConfig('binSpacing', markDef, config) ?? 0) : 0; + const spacing = isXorY(channel) ? getMarkPropOrConfig('binSpacing', markDef, config) ?? 0 : 0; const channel2 = getSecondaryRangeChannel(channel); const vgChannel = getVgPositionChannel(channel); diff --git a/src/compile/mark/tick.ts b/src/compile/mark/tick.ts index cf0654f657..2a2bae8238 100644 --- a/src/compile/mark/tick.ts +++ b/src/compile/mark/tick.ts @@ -1,10 +1,7 @@ -import {isNumber} from 'vega-util'; -import {isVgRangeStep, VgValueRef} from '../../vega.schema'; -import {exprFromSignalRefOrValue, getMarkPropOrConfig, signalOrValueRef} from '../common'; +import {getMarkPropOrConfig, signalOrValueRef} from '../common'; import {UnitModel} from '../unit'; import {MarkCompiler} from './base'; import * as encode from './encode'; -import {getOffsetScaleChannel} from '../../channel'; export const tick: MarkCompiler = { vgMark: 'rect', @@ -13,7 +10,8 @@ export const tick: MarkCompiler = { const {config, markDef} = model; const orient = markDef.orient; - const vgSizeChannel = orient === 'horizontal' ? 'width' : 'height'; + const vgSizeAxisChannel = orient === 'horizontal' ? 'x' : 'y'; + const vgThicknessAxisChannel = orient === 'horizontal' ? 'y' : 'x'; const vgThicknessChannel = orient === 'horizontal' ? 'height' : 'width'; return { @@ -26,49 +24,12 @@ export const tick: MarkCompiler = { theta: 'ignore' }), - ...encode.pointPosition('x', model, {defaultPos: 'mid', vgChannel: 'xc'}), - ...encode.pointPosition('y', model, {defaultPos: 'mid', vgChannel: 'yc'}), - - // size / thickness => width / height - ...encode.nonPosition('size', model, { - defaultRef: defaultSize(model), - vgChannel: vgSizeChannel + ...encode.rectPosition(model, vgSizeAxisChannel), + ...encode.pointPosition(vgThicknessAxisChannel, model, { + defaultPos: 'mid', + vgChannel: vgThicknessAxisChannel === 'y' ? 'yc' : 'xc' }), [vgThicknessChannel]: signalOrValueRef(getMarkPropOrConfig('thickness', markDef, config)) }; } }; - -function defaultSize(model: UnitModel): VgValueRef { - const {config, markDef} = model; - const {orient} = markDef; - - const vgSizeChannel = orient === 'horizontal' ? 'width' : 'height'; - const positionChannel = orient === 'horizontal' ? 'x' : 'y'; - - const offsetScaleChannel = getOffsetScaleChannel(positionChannel); - - // Use offset scale if exists - const scale = model.getScaleComponent(offsetScaleChannel) || model.getScaleComponent(positionChannel); - - const markPropOrConfig = - getMarkPropOrConfig('size', markDef, config, {vgChannel: vgSizeChannel}) ?? config.tick.bandSize; - - if (markPropOrConfig !== undefined) { - return signalOrValueRef(markPropOrConfig); - } else if (scale?.get('type') === 'band') { - const scaleName = model.scaleName(offsetScaleChannel) || model.scaleName(positionChannel); - return {scale: scaleName, band: 1}; - } - - const scaleRange = scale?.get('range'); - const {tickBandPaddingInner} = config.scale; - - const step = scaleRange && isVgRangeStep(scaleRange) ? scaleRange.step : model[vgSizeChannel]; - - if (isNumber(step) && isNumber(tickBandPaddingInner)) { - return {value: step * (1 - tickBandPaddingInner)}; - } else { - return {signal: `(1 - ${exprFromSignalRefOrValue(tickBandPaddingInner)}) * ${exprFromSignalRefOrValue(step)}`}; - } -} diff --git a/src/mark.ts b/src/mark.ts index 46a4cc23e5..ca6819ae1f 100644 --- a/src/mark.ts +++ b/src/mark.ts @@ -55,8 +55,8 @@ export function isPathMark(m: Mark | CompositeMark): m is PathMark { return ['line', 'area', 'trail'].includes(m); } -export function isRectBasedMark(m: Mark | CompositeMark): m is 'rect' | 'bar' | 'image' | 'arc' { - return ['rect', 'bar', 'image', 'arc' /* arc is rect/interval in polar coordinate */].includes(m); +export function isRectBasedMark(m: Mark | CompositeMark): m is 'rect' | 'bar' | 'image' | 'arc' | 'tick' { + return ['rect', 'bar', 'image', 'arc', 'tick' /* arc is rect/interval in polar coordinate */].includes(m); } export const PRIMITIVE_MARKS = new Set(keys(Mark)); @@ -647,13 +647,6 @@ export interface MarkDef = { - binSpacing: 1, - continuousBandSize: DEFAULT_RECT_BAND_SIZE, - minBandSize: 0.25, - timeUnitBandPosition: 0.5 -}; - export const defaultRectConfig: RectConfig = { binSpacing: 0, continuousBandSize: DEFAULT_RECT_BAND_SIZE, @@ -661,7 +654,15 @@ export const defaultRectConfig: RectConfig = { timeUnitBandPosition: 0.5 }; -export interface TickConfig extends MarkConfig, TickThicknessMixins { +export const defaultBarConfig: RectConfig = { + ...defaultRectConfig, + binSpacing: 1 +}; + +export interface TickConfig + extends MarkConfig, + TickThicknessMixins, + RectConfig { /** * The width of the ticks. * @@ -672,6 +673,7 @@ export interface TickConfig extends MarkConfig = { + ...defaultRectConfig, thickness: 1 }; diff --git a/test/compile/mark/tick.test.ts b/test/compile/mark/tick.test.ts index bbc4e57761..7af4eefb33 100644 --- a/test/compile/mark/tick.test.ts +++ b/test/compile/mark/tick.test.ts @@ -111,7 +111,7 @@ describe('Mark: Tick', () => { }); it('should scale on y', () => { - expect(props.yc).toEqual({scale: Y, field: 'Cylinders', band: 0.5}); + expect(props.y).toEqual({scale: Y, field: 'Cylinders'}); }); it('width should be tick thickness with default orient vertical', () => { @@ -119,7 +119,7 @@ describe('Mark: Tick', () => { }); it('height should be matched to field with default orient vertical', () => { - expect(props.height).toEqual({scale: 'y', band: 1}); + expect(props.height).toEqual({signal: "max(0.25, bandwidth('y'))"}); }); }); describe('with quantitative x and ordinal y with yOffset', () => { @@ -139,11 +139,10 @@ describe('Mark: Tick', () => { }); it('should scale on y', () => { - expect(props.yc).toEqual({ + expect(props.y).toEqual({ scale: Y, field: 'Cylinders', offset: { - band: 0.5, field: 'Acceleration', scale: 'yOffset' } @@ -155,7 +154,7 @@ describe('Mark: Tick', () => { }); it('height should be matched to field with default orient vertical', () => { - expect(props.height).toEqual({scale: 'yOffset', band: 1}); + expect(props.height).toEqual({signal: "max(0.25, bandwidth('yOffset'))"}); }); }); @@ -217,7 +216,7 @@ describe('Mark: Tick', () => { }); const props = tick.encodeEntry(model); it('sets mark height to (1-tickBandPaddingInner) * plot_height', () => { - expect(props.height).toEqual({signal: '(1 - 0.25) * height'}); + expect(props.height).toEqual({signal: '0.75 * height'}); }); }); @@ -231,7 +230,7 @@ describe('Mark: Tick', () => { }); const props = tick.encodeEntry(model); it('sets mark width to (1-tickBandPaddingInner) * plot_width', () => { - expect(props.width).toEqual({signal: '(1 - 0.25) * width'}); + expect(props.width).toEqual({signal: '0.75 * width'}); }); }); });