Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: split field's band into field's bandPosition & mark's width/height: {band: ...} #7190

Merged
merged 5 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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