Skip to content

Commit

Permalink
feat!: split field's band into field's bandPosition & mark's `wid…
Browse files Browse the repository at this point in the history
…th/height: {band: ...}` (#7190)

Co-authored-by: GitHub Actions Bot <vega-actions-bot@users.noreply.github.com>
  • Loading branch information
kanitw and GitHub Actions Bot committed Feb 17, 2021
1 parent 26fbaf9 commit af68557
Show file tree
Hide file tree
Showing 31 changed files with 609 additions and 393 deletions.
392 changes: 216 additions & 176 deletions build/vega-lite-schema.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions examples/compiled/scatter_image.vg.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@
"from": {"data": "data_0"},
"encode": {
"update": {
"width": {"value": 50},
"height": {"value": 50},
"description": {
"signal": "\"x: \" + (format(datum[\"x\"], \"\")) + \"; y: \" + (format(datum[\"y\"], \"\")) + \"; img: \" + (isValid(datum[\"img\"]) ? datum[\"img\"] : \"\"+datum[\"img\"])"
},
"xc": {"scale": "x", "field": "x"},
"width": {"value": 50},
"yc": {"scale": "y", "field": "y"},
"height": {"value": 50},
"url": {
"signal": "isValid(datum[\"img\"]) ? datum[\"img\"] : \"\"+datum[\"img\"]"
}
Expand Down
3 changes: 1 addition & 2 deletions examples/specs/bar_axis_space_saving.vl.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
"description": "Bar Chart with a spacing-saving y-axis",
"data": {"url": "data/cars.json"},
"height": {"step": 50},
"mark": {"type": "bar", "yOffset": 5, "cornerRadiusEnd": 2},
"mark": {"type": "bar", "yOffset": 5, "cornerRadiusEnd": 2, "height": {"band": 0.5}},
"encoding": {
"y": {
"field": "Origin",
"scale": {"padding": 0},
"band": 0.5,
"axis": {
"bandPosition": 0,
"grid": true,
Expand Down
5 changes: 2 additions & 3 deletions examples/specs/bar_month_band.vl.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": {"url": "data/seattle-weather.csv"},
"mark": "bar",
"mark": {"type": "bar", "width": {"band": 0.7}},
"encoding": {
"x": {
"timeUnit": "month",
"field": "date",
"band": 0.7
"field": "date"
},
"y": {
"aggregate": "mean",
Expand Down
2 changes: 1 addition & 1 deletion examples/specs/bar_month_band_config.vl.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"config": {
"mark": {
"timeUnitBand": 0.7
"timeUnitBandSize": 0.7
}
}
}
2 changes: 1 addition & 1 deletion examples/specs/circle_wilkinson_dotplot_stacked.vl.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"mark": "circle",
"encoding": {
"x": {"field": "data", "type": "ordinal"},
"y": {"aggregate": "count", "stack": true, "band": 0.5},
"y": {"aggregate": "count", "stack": true, "bandPosition": 0.5},
"detail": {"field": "id"}
}
}
2 changes: 1 addition & 1 deletion examples/specs/line_month_center_band.vl.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"interpolate": "monotone"
},
"encoding": {
"x": {"timeUnit": "month", "field": "date", "band": 0.5},
"x": {"timeUnit": "month", "field": "date", "bandPosition": 0.5},
"y": {"aggregate": "mean", "field": "temp_max"}
}
}
14 changes: 13 additions & 1 deletion site/docs/mark/bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Bar marks are useful in many visualizations, including bar charts, [stacked bar

A bar mark definition can contain any [standard mark properties](mark.html#mark-def) and the following special properties:

{% include table.html props="orient,align,baseline,binSpacing,cornerRadius,cornerRadiusEnd,cornerRadiusTopLeft,cornerRadiusTopRight,cornerRadiusBottomRight,cornerRadiusBottomLeft" source="MarkDef" %}
{% include table.html props="width,height,orient,align,baseline,binSpacing,cornerRadius,cornerRadiusEnd,cornerRadiusTopLeft,cornerRadiusTopRight,cornerRadiusBottomRight,cornerRadiusBottomLeft" source="MarkDef" %}

## Examples

Expand All @@ -63,10 +63,22 @@ If we map a different discrete field to the `y` channel, we can produce a horizo

<span class="vl-example" data-name="bar_aggregate"></span>

### Bar Chart with a Temporal Axis

While the `bar` mark typically uses the x and y channels to encode a pair of discrete and continuous fields, it can also be used with continuous fields on both channels. For example, given a bar chart with a temporal field on x, we can see that the x-scale is a continuous scale. By default, the size of bars on continuous scales will be set based on the [`continuousBandSize` config](#config).

<span class="vl-example" data-name="bar_month_temporal"></span>

{.#bar-width}

### Relative Bar Width

To adjust the bar to be smaller than the time unit step, you can adjust the bar's width to be a proportion of band. For example, the following chart sets the width to be 70% of the x band width.

<span class="vl-example" data-name="bar_month_band"></span>

### Bar Chart with a Discrete Temporal Axis

If you want to use a discrete scale instead, you can cast the field to have an `"ordinal"` type. This casting strategy can be useful for time units with low cardinality such as `"month"`.

<span class="vl-example" data-name="bar_month"></span>
Expand Down
2 changes: 1 addition & 1 deletion site/docs/mark/rect.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The `rect` mark represents an arbitrary rectangle.

A rect mark definition can contain any [standard mark properties](mark.html#mark-def) and the following special properties:

{% include table.html props="align,baseline,cornerRadius" source="MarkConfig" %}
{% include table.html props="width,height,align,baseline,cornerRadius" source="MarkConfig" %}

## Examples

Expand Down
102 changes: 70 additions & 32 deletions src/channeldef.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Gradient, SignalRef, Text} from 'vega';
import {Gradient, ScaleType, SignalRef, Text} from 'vega';
import {isArray, isBoolean, isNumber, isString} from 'vega-util';
import {Aggregate, isAggregateOp, isArgmaxDef, isArgminDef, isCountingAggregateOp} from './aggregate';
import {Axis} from './axis';
Expand All @@ -14,6 +14,7 @@ import {
FACET,
FILL,
FILLOPACITY,
getSizeChannel,
HREF,
isScaleChannel,
isSecondaryRangeChannel,
Expand All @@ -25,6 +26,8 @@ import {
LONGITUDE2,
OPACITY,
ORDER,
PolarPositionScaleChannel,
PositionScaleChannel,
RADIUS,
RADIUS2,
ROW,
Expand All @@ -44,7 +47,7 @@ import {
Y,
Y2
} from './channel';
import {getMarkConfig} from './compile/common';
import {getMarkConfig, getMarkPropOrConfig} from './compile/common';
import {isCustomFormatType} from './compile/format';
import {CompositeAggregate} from './compositemark';
import {Config} from './config';
Expand All @@ -56,12 +59,12 @@ import {ImputeParams} from './impute';
import {Legend} from './legend';
import * as log from './log';
import {LogicalComposition} from './logical';
import {isRectBasedMark, Mark, MarkDef} from './mark';
import {Predicate, ParameterPredicate} from './predicate';
import {isContinuousToDiscrete, Scale, SCALE_CATEGORY_INDEX} from './scale';
import {isRectBasedMark, Mark, MarkDef, RelativeBandSize} from './mark';
import {ParameterPredicate, Predicate} from './predicate';
import {hasDiscreteDomain, isContinuousToDiscrete, Scale, SCALE_CATEGORY_INDEX} from './scale';
import {isSortByChannel, Sort, SortOrder} from './sort';
import {isFacetFieldDef} from './spec/facet';
import {StackOffset, StackProperties} from './stack';
import {StackOffset} from './stack';
import {
getTimeUnitParts,
isLocalSingleTimeUnit,
Expand Down Expand Up @@ -469,14 +472,12 @@ export interface PositionBaseMixins {

export interface BandMixins {
/**
* For rect-based marks (`rect`, `bar`, and `image`), mark size relative to bandwidth of [band scales](https://vega.github.io/vega-lite/docs/scale.html#band), bins or time units. If set to `1`, the mark size is set to the bandwidth, the bin interval, or the time unit interval. If set to `0.5`, the mark size is half of the bandwidth or the time unit interval.
*
* For other marks, relative position on a band of a stacked, binned, time unit or band scale. If set to `0`, the marks will be positioned at the beginning of the band. If set to `0.5`, the marks will be positioned in the middle of the band.
* Relative position on a band of a stacked, binned, time unit, or band scale. For example, the marks will be positioned at the beginning of the band if set to `0`, and at the middle of the band if set to `0.5`.
*
* @minimum 0
* @maximum 1
*/
band?: number;
bandPosition?: number;
}

export type PositionFieldDef<F extends Field> = PositionFieldDefBase<F> & PositionMixins;
Expand Down Expand Up @@ -508,55 +509,92 @@ export interface PositionMixins {

export type PolarDef<F extends Field> = PositionFieldDefBase<F> | PositionDatumDefBase<F> | PositionValueDef;

export function getBand({
channel,
export function getBandPosition({
fieldDef,
fieldDef2,
markDef: mark,
stack,
config,
isMidPoint
config
}: {
isMidPoint?: boolean;
channel: Channel;
fieldDef: FieldDef<string> | DatumDef;
fieldDef2?: SecondaryChannelDef<string>;
stack: StackProperties;
markDef: MarkDef<Mark, SignalRef>;
config: Config<SignalRef>;
}): number {
if (isFieldOrDatumDef(fieldDef) && fieldDef.band !== undefined) {
return fieldDef.band;
if (isFieldOrDatumDef(fieldDef) && fieldDef.bandPosition !== undefined) {
return fieldDef.bandPosition;
}
if (isFieldDef(fieldDef)) {
const {timeUnit, bin} = fieldDef;
if (timeUnit && !fieldDef2) {
return isRectBasedMark(mark.type) ? 0 : getMarkConfig('timeUnitBandPosition', mark, config);
} else if (isBinning(bin)) {
return 0.5;
}
}

return undefined;
}

export function getBandSize({
channel,
fieldDef,
fieldDef2,
markDef: mark,
config,
scaleType,
useVlSizeChannel
}: {
channel: PositionScaleChannel | PolarPositionScaleChannel;
fieldDef: ChannelDef<string>;
fieldDef2?: SecondaryChannelDef<string>;
markDef: MarkDef<Mark, SignalRef>;
config: Config<SignalRef>;
scaleType: ScaleType;
useVlSizeChannel?: boolean;
}): number | RelativeBandSize | SignalRef {
const sizeChannel = getSizeChannel(channel);
const size = getMarkPropOrConfig(useVlSizeChannel ? 'size' : sizeChannel, mark, config, {
vgChannel: sizeChannel
});

if (size !== undefined) {
return size;
}

if (isFieldDef(fieldDef)) {
const {timeUnit, bin} = fieldDef;

if (timeUnit && !fieldDef2) {
if (isMidPoint) {
return getMarkConfig('timeUnitBandPosition', mark, config);
return {band: getMarkConfig('timeUnitBandSize', mark, config)};
} else if (isBinning(bin) && !hasDiscreteDomain(scaleType)) {
return {band: 1};
}
}

if (isRectBasedMark(mark.type)) {
if (scaleType) {
if (hasDiscreteDomain(scaleType)) {
return config[mark.type]?.discreteBandSize || {band: 1};
} else {
return isRectBasedMark(mark.type) ? getMarkConfig('timeUnitBand', mark, config) : 0;
return config[mark.type]?.continuousBandSize;
}
} else if (isBinning(bin)) {
return isRectBasedMark(mark.type) && !isMidPoint ? 1 : 0.5;
}
return config[mark.type]?.discreteBandSize;
}
if (stack?.fieldChannel === channel && isMidPoint) {
return 0.5;
}

return undefined;
}

export function hasBand(
channel: Channel,
export function hasBandEnd(
fieldDef: FieldDef<string>,
fieldDef2: SecondaryChannelDef<string>,
stack: StackProperties,
markDef: MarkDef<Mark, SignalRef>,
config: Config<SignalRef>
): boolean {
if (isBinning(fieldDef.bin) || (fieldDef.timeUnit && isTypedFieldDef(fieldDef) && fieldDef.type === 'temporal')) {
return !!getBand({channel, fieldDef, fieldDef2, stack, markDef, config});
// Need to check bandPosition because non-rect marks (e.g., point) with timeUnit
// doesn't have to use bandEnd if there is no bandPosition.
return getBandPosition({fieldDef, fieldDef2, markDef, config}) !== undefined;
}
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/compile/data/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
isGeoPositionChannel,
isScaleChannel
} from '../../channel';
import {binRequiresRange, FieldDef, hasBand, isTypedFieldDef, vgField} from '../../channeldef';
import {binRequiresRange, FieldDef, hasBandEnd, isTypedFieldDef, vgField} from '../../channeldef';
import * as log from '../../log';
import {AggregateTransform} from '../../transform';
import {Dict, duplicate, hash, keys, replacePathInField, setEqual} from '../../util';
Expand All @@ -23,7 +23,7 @@ function addDimension(dims: Set<string>, channel: Channel, fieldDef: FieldDef<st
if (
isTypedFieldDef(fieldDef) &&
isUnitModel(model) &&
hasBand(channel, fieldDef, channelDef2, model.stack, model.markDef, model.config)
hasBandEnd(fieldDef, channelDef2, model.markDef, model.config)
) {
dims.add(vgField(fieldDef, {}));
dims.add(vgField(fieldDef, {suffix: 'end'}));
Expand Down
6 changes: 3 additions & 3 deletions src/compile/data/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,16 @@ export class StackNode extends DataFlowNode {

// Impute
if (impute && dimensionFieldDef) {
const {band = 0.5, bin} = dimensionFieldDef;
const {bandPosition = 0.5, bin} = dimensionFieldDef;
if (bin) {
// As we can only impute one field at a time, we need to calculate
// mid point for a binned field
transform.push({
type: 'formula',
expr:
`${band}*` +
`${bandPosition}*` +
vgField(dimensionFieldDef, {expr: 'datum'}) +
`+${1 - band}*` +
`+${1 - bandPosition}*` +
vgField(dimensionFieldDef, {expr: 'datum', binSuffix: 'end'}),
as: vgField(dimensionFieldDef, {binSuffix: 'mid', forAs: true})
});
Expand Down
28 changes: 9 additions & 19 deletions src/compile/data/timeunit.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import {TimeUnitTransform as VgTimeUnitTransform} from 'vega';
import {getSecondaryRangeChannel} from '../../channel';
import {hasBand, vgField} from '../../channeldef';
import {vgField} from '../../channeldef';
import {getTimeUnitParts, normalizeTimeUnit} from '../../timeunit';
import {TimeUnitTransform} from '../../transform';
import {Dict, duplicate, hash, isEmpty, replacePathInField, vals, entries} from '../../util';
import {isUnitModel, ModelWithField} from '../model';
import {Dict, duplicate, entries, hash, isEmpty, replacePathInField, vals} from '../../util';
import {ModelWithField} from '../model';
import {DataFlowNode} from './dataflow';

export type TimeUnitComponent = TimeUnitTransform & {
/** whether to output time unit as a band (generate two formula including start and end) */
band?: boolean;
};
export type TimeUnitComponent = TimeUnitTransform;

export class TimeUnitNode extends DataFlowNode {
public clone() {
Expand All @@ -22,14 +18,9 @@ export class TimeUnitNode extends DataFlowNode {
}

public static makeFromEncoding(parent: DataFlowNode, model: ModelWithField) {
const formula = model.reduceFieldDef((timeUnitComponent: TimeUnitComponent, fieldDef, channel) => {
const formula = model.reduceFieldDef((timeUnitComponent: TimeUnitComponent, fieldDef) => {
const {field, timeUnit} = fieldDef;

const channelDef2 = isUnitModel(model) ? model.encoding[getSecondaryRangeChannel(channel)] : undefined;

const band =
isUnitModel(model) && hasBand(channel, fieldDef, channelDef2, model.stack, model.markDef, model.config);

if (timeUnit) {
const as = vgField(fieldDef, {forAs: true});
timeUnitComponent[
Expand All @@ -41,8 +32,7 @@ export class TimeUnitNode extends DataFlowNode {
] = {
as,
field,
timeUnit,
...(band ? {band: true} : {})
timeUnit
};
}
return timeUnitComponent;
Expand Down Expand Up @@ -77,10 +67,10 @@ export class TimeUnitNode extends DataFlowNode {
public merge(other: TimeUnitNode) {
this.formula = {...this.formula};

// if the same hash happen twice, merge "band"
// if the same hash happen twice, merge
for (const key in other.formula) {
if (!this.formula[key] || other.formula[key].band) {
// copy if it's not a duplicate or if we need to copy band over
if (!this.formula[key]) {
// copy if it's not a duplicate
this.formula[key] = other.formula[key];
}
}
Expand Down
Loading

0 comments on commit af68557

Please sign in to comment.