Skip to content

Commit

Permalink
Add href as channel and cursor as a property for marks (#3229)
Browse files Browse the repository at this point in the history
  • Loading branch information
domoritz authored and kanitw committed Jan 15, 2018
1 parent cebdffb commit b4dfb96
Show file tree
Hide file tree
Showing 21 changed files with 558 additions and 32 deletions.
445 changes: 444 additions & 1 deletion build/vega-lite-schema.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions scripts/rename-schema.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ perl -pi -e s,'GenericHConcatSpec<CompositeUnitSpec>','HConcatSpec',g build/vega
perl -pi -e s,'GenericUnitSpec<EncodingWithFacet\,AnyMark>','FacetedCompositeUnitSpecAlias',g build/vega-lite-schema.json
perl -pi -e s,'GenericUnitSpec<Encoding\,AnyMark>','CompositeUnitSpecAlias',g build/vega-lite-schema.json

perl -pi -e s,'FieldDefWithCondition<FieldDef\>','FieldDefWithCondition',g build/vega-lite-schema.json
perl -pi -e s,'ValueDefWithCondition<FieldDef\>','ValueDefWithCondition',g build/vega-lite-schema.json

perl -pi -e s,'FieldDefWithCondition<TextFieldDef\>','TextFieldDefWithCondition',g build/vega-lite-schema.json
perl -pi -e s,'ValueDefWithCondition<TextFieldDef\>','TextValueDefWithCondition',g build/vega-lite-schema.json

Expand Down
29 changes: 28 additions & 1 deletion site/docs/encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ The `encoding` property of a single view specification represents the mapping be
"text": ...,
"tooltip": ...,

// Hyperlink Channel
"href": ...,

// Order Channel
"order": ...,

Expand All @@ -51,7 +54,8 @@ The keys in the `encoding` object are encoding channels. Vega-lite supports the

- [Position Channels](#position): `x`, `y`, `x2`, `y2`
- [Mark Property Channels](#mark-prop): `color`, `opacity`, `shape`, `size`
- [Text and Tooltip Channels](#text): `text`, `tooltip`
- [Text and Tooltip Channels](#text): `text`, `tooltip`
- [Hyperlink Channel](#href): `href`
- [Level of Detail Channel](#detail): `detail`
- [Order Channel](#order): `order`
- [Facet Channels](#facet): `row`, `column`
Expand Down Expand Up @@ -204,6 +208,29 @@ In addition to the constant `value`, [value definitions](#value-def) of `text` a

{% include table.html props="condition" source="TextValueDefWithCondition" %}

{:#href}
## Hyperlink Channel

By setting the `href` channel, a mark becomes a hyperlink. The specified URL is loaded upon a muse click. The [`cursor` mark property](mark.html#hyperlink) can be set to `pointer` to serve as affordance for hyperlinks.

{% include table.html props="href" source="Encoding" %}


{:#href-field-def}
### Hyperlink Field Definition

In addition to [`field`](field.html), [`type`](type.html), [`bin`](bin.html), [`timeUnit`](timeunit.html) and [`aggregate`](aggregate.html),
[field definitions](#field-def) for the `href` channel can include the `condition` property to specify conditional logic.

{% include table.html props="condition" source="FieldDefWithCondition" %}

{:#href-value-def}
### Hyperlink Value Definition

In addition to the constant `value`, [value definitions](#value-def) of the `href` channel can include the `condition` property to specify conditional logic.

{% include table.html props="condition" source="ValueDefWithCondition" %}


{:#detail}
## Level of Detail Channel
Expand Down
8 changes: 7 additions & 1 deletion site/docs/mark/mark.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,17 @@ The rest of this section describe groups of properties supported by the `mark` c

{% include table.html props="opacity,fillOpacity,strokeOpacity" source="MarkConfig" %}


### Stroke Style

{% include table.html props="strokeWidth,strokeDash,strokeDashOffset" source="MarkConfig" %}

{:#hyperlink}
### Hyperlink Properties

Marks can act as hyperlinks when the `href` property or [channel](encoding.html#href) is defined. A `cursor` property can also be provided to serve as affordance for the links.

{% include table.html props="href,cursor" source="MarkConfig" %}

<!-- one example for custom fill/stroke -->

{:#interpolate}
Expand Down
19 changes: 12 additions & 7 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export namespace Channel {
export const ORDER: 'order' = 'order';
export const DETAIL: 'detail' = 'detail';
export const TOOLTIP: 'tooltip' = 'tooltip';
export const HREF: 'href' = 'href';
}

export type Channel = keyof Encoding<any> | keyof FacetMapping<any>;
Expand All @@ -50,6 +51,7 @@ export const DETAIL = Channel.DETAIL;
export const ORDER = Channel.ORDER;
export const OPACITY = Channel.OPACITY;
export const TOOLTIP = Channel.TOOLTIP;
export const HREF = Channel.HREF;

const UNIT_CHANNEL_INDEX: Flag<keyof Encoding<any>> = {
x: 1,
Expand All @@ -63,7 +65,8 @@ const UNIT_CHANNEL_INDEX: Flag<keyof Encoding<any>> = {
opacity: 1,
text: 1,
detail: 1,
tooltip: 1
tooltip: 1,
href: 1,
};

const FACET_CHANNEL_INDEX: Flag<keyof FacetMapping<any>> = {
Expand Down Expand Up @@ -93,7 +96,7 @@ export const SINGLE_DEF_CHANNELS: SingleDefChannel[] = flagKeys(SINGLE_DEF_CHANN
// Using the following line leads to TypeError: Cannot read property 'elementTypes' of undefined
// when running the schema generator
// export type SingleDefChannel = typeof SINGLE_DEF_CHANNELS[0];
export type SingleDefChannel = 'x' | 'y' | 'x2' | 'y2' | 'row' | 'column' | 'size' | 'shape' | 'color' | 'opacity' | 'text' | 'tooltip';
export type SingleDefChannel = 'x' | 'y' | 'x2' | 'y2' | 'row' | 'column' | 'size' | 'shape' | 'color' | 'opacity' | 'text' | 'tooltip' | 'href';



Expand Down Expand Up @@ -124,9 +127,10 @@ export type PositionScaleChannel = typeof POSITION_SCALE_CHANNELS[0];

// NON_POSITION_SCALE_CHANNEL = SCALE_CHANNELS without X, Y
const {
// x2 and y2 share the same scale as x and y
// text and tooltip has format instead of scale
text: _t, tooltip: _tt,
// x2 and y2 share the same scale as x and y
// text and tooltip have format instead of scale,
// href has neither format, nor scale
text: _t, tooltip: _tt, href: _hr,
// detail and order have no scale
detail: _dd, order: _oo,
...NONPOSITION_SCALE_CHANNEL_INDEX
Expand Down Expand Up @@ -159,7 +163,6 @@ export interface SupportedMark {
line?: boolean;
area?: boolean;
text?: boolean;
tooltip?: boolean;
}

/**
Expand All @@ -184,6 +187,7 @@ export function getSupportedMark(channel: Channel): SupportedMark {
case COLOR:
case DETAIL:
case TOOLTIP:
case HREF:
case ORDER: // TODO: revise (order might not support rect, which is not stackable?)
case OPACITY:
case ROW:
Expand Down Expand Up @@ -223,9 +227,10 @@ export function rangeType(channel: Channel): RangeType {
case ROW:
case COLUMN:
case SHAPE:
// TEXT and TOOLTIP have no scale but have discrete output
// TEXT, TOOLTIP, and HREF have no scale but have discrete output
case TEXT:
case TOOLTIP:
case HREF:
return 'discrete';

// Color can be either continuous or discrete, depending on scale type.
Expand Down
1 change: 1 addition & 0 deletions src/compile/mark/area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const area: MarkCompiler = {

...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('opacity', model),
};
}
Expand Down
1 change: 1 addition & 0 deletions src/compile/mark/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const bar: MarkCompiler = {
...y(model, stack),
...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('opacity', model)
};
}
Expand Down
1 change: 1 addition & 0 deletions src/compile/mark/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const line: MarkCompiler = {
...mixins.pointPosition('y', model, ref.mid(height)),
...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('opacity', model),
...mixins.nonPosition('size', model, {
vgChannel: 'strokeWidth' // VL's line size is strokeWidth
Expand Down
3 changes: 2 additions & 1 deletion src/compile/mark/mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,10 @@ export function pathGroupingFields(encoding: Encoding<string>): string[] {
case 'y':
case 'order':
case 'tooltip':
case 'href':
case 'x2':
case 'y2':
// TODO: case 'href', 'cursor':
// TODO: case 'cursor':

// text, shape, shouldn't be a part of line/area
case 'text':
Expand Down
2 changes: 1 addition & 1 deletion src/compile/mark/mixins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function wrapCondition(
}
}

export function text(model: UnitModel, channel: 'text' | 'tooltip' = 'text') {
export function text(model: UnitModel, channel: 'text' | 'tooltip' | 'href' = 'text') {
const channelDef = model.encoding[channel];
return wrapCondition(model, channelDef, channel, (cDef) => ref.text(cDef, model.config));
}
Expand Down
1 change: 1 addition & 0 deletions src/compile/mark/point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function encodeEntry(model: UnitModel, fixedShape?: 'circle' | 'square') {

...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('size', model),
...shapeMixins(model, config, fixedShape),
...mixins.nonPosition('opacity', model),
Expand Down
1 change: 1 addition & 0 deletions src/compile/mark/rect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const rect: MarkCompiler = {
...y(model),
...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('opacity', model),
};
}
Expand Down
1 change: 1 addition & 0 deletions src/compile/mark/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const rule: MarkCompiler = {

...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('opacity', model),
...mixins.nonPosition('size', model, {
vgChannel: 'strokeWidth' // VL's rule size is strokeWidth
Expand Down
1 change: 1 addition & 0 deletions src/compile/mark/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const text: MarkCompiler = {
...mixins.text(model),
...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('opacity', model),
...mixins.nonPosition('size', model, {
vgChannel: 'fontSize' // VL's text size is fontSize
Expand Down
2 changes: 2 additions & 0 deletions src/compile/mark/tick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const tick: MarkCompiler = {
[vgThicknessChannel]: {value: config.tick.thickness},

...mixins.color(model),
...mixins.text(model, 'tooltip'),
...mixins.text(model, 'href'),
...mixins.nonPosition('opacity', model),
};
}
Expand Down
5 changes: 5 additions & 0 deletions src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export interface Encoding<F> {
*/
tooltip?: FieldDefWithCondition<TextFieldDef<F>> | ValueDefWithCondition<TextFieldDef<F>>;

/**
* A URL to load upon mouse click.
*/
href?: FieldDefWithCondition<FieldDef<F>> | ValueDefWithCondition<FieldDef<F>>;

/**
* Stack order for stacked marks or order of data points in line marks for connected scatter plots.
*
Expand Down
1 change: 1 addition & 0 deletions src/fielddef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export function channelCompatibility(fieldDef: FieldDef<Field>, channel: Channel
case 'text':
case 'detail':
case 'tooltip':
case 'href':
return COMPATIBLE;

case 'opacity':
Expand Down
21 changes: 17 additions & 4 deletions src/vega.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export interface VgSignal {
push?: string;
}

export type VgEncodeChannel = 'x'|'x2'|'xc'|'width'|'y'|'y2'|'yc'|'height'|'opacity'|'fill'|'fillOpacity'|'stroke'|'strokeWidth'|'strokeOpacity'|'strokeDash'|'strokeDashOffset'|'cursor'|'clip'|'size'|'shape'|'path'|'innerRadius'|'outerRadius'|'startAngle'|'endAngle'|'interpolate'|'tension'|'orient'|'url'|'align'|'baseline'|'text'|'dir'|'ellipsis'|'limit'|'dx'|'dy'|'radius'|'theta'|'angle'|'font'|'fontSize'|'fontWeight'|'fontStyle';
export type VgEncodeChannel = 'x'|'x2'|'xc'|'width'|'y'|'y2'|'yc'|'height'|'opacity'|'fill'|'fillOpacity'|'stroke'|'strokeWidth'|'strokeOpacity'|'strokeDash'|'strokeDashOffset'|'cursor'|'clip'|'size'|'shape'|'path'|'innerRadius'|'outerRadius'|'startAngle'|'endAngle'|'interpolate'|'tension'|'orient'|'url'|'align'|'baseline'|'text'|'dir'|'ellipsis'|'limit'|'dx'|'dy'|'radius'|'theta'|'angle'|'font'|'fontSize'|'fontWeight'|'fontStyle'|'tooltip'|'href'|'cursor';
export type VgEncodeEntry = {
[k in VgEncodeChannel]?: VgValueRef | (VgValueRef & {test?: string})[];
};
Expand Down Expand Up @@ -1056,6 +1056,18 @@ export interface VgMarkConfig {
* Placeholder text if the `text` channel is not specified
*/
text?: string;

/**
* A URL to load upon mouse click. If defined, the mark acts as a hyperlink.
*
* @format uri
*/
href?: string;

/**
* The mouse cursor used over the mark. Any valid [CSS cursor type](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#Values) can be used.
*/
cursor?: 'auto' | 'default' | 'none' | 'context-menu' | 'help' | 'pointer' | 'progress' | 'wait' | 'cell' | 'crosshair' | 'text' | 'vertical-text' | 'alias' | 'copy' | 'move' | 'no-drop' | 'not-allowed' | 'e-resize' | 'n-resize' | 'ne-resize' | 'nw-resize' | 's-resize' | 'se-resize' | 'sw-resize' | 'w-resize' | 'ew-resize' | 'ns-resize' | 'nesw-resize' | 'nwse-resize' | 'col-resize' | 'row-resize' | 'all-scroll' | 'zoom-in' | 'zoom-out' | 'grab' | 'grabbing';
}

const VG_MARK_CONFIG_INDEX: Flag<keyof VgMarkConfig> = {
Expand Down Expand Up @@ -1084,17 +1096,18 @@ const VG_MARK_CONFIG_INDEX: Flag<keyof VgMarkConfig> = {
font: 1,
fontSize: 1,
fontWeight: 1,
fontStyle: 1
fontStyle: 1,
cursor: 1,
href: 1,
// commented below are vg channel that do not have mark config.
// 'x'|'x2'|'xc'|'width'|'y'|'y2'|'yc'|'height'
// cursor: 1,
// clip: 1,
// dir: 1,
// ellipsis: 1,
// endAngle: 1,
// path: 1,
// innerRadius: 1,
// outerRadius: 1,
// path: 1,
// startAngle: 1,
// url: 1,
};
Expand Down
2 changes: 1 addition & 1 deletion test/channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('channel', () => {

describe('SCALE_CHANNELS', () => {
it('should be UNIT_CHANNELS without X2, Y2, ORDER, DETAIL, TEXT, LABEL, TOOLTIP', () => {
assert.deepEqual(SCALE_CHANNELS, without(UNIT_CHANNELS, ['x2', 'y2', 'order', 'detail', 'text', 'label', 'tooltip']));
assert.deepEqual(SCALE_CHANNELS, without(UNIT_CHANNELS, ['x2', 'y2', 'order', 'detail', 'text', 'label', 'tooltip', 'href']));
});
});

Expand Down
15 changes: 0 additions & 15 deletions test/compile/mark/mark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,21 +105,6 @@ describe('Mark', function() {
assert.equal(markGroup[0].from.data, 'main');
});
});

describe('Bar with tooltip', () => {
it('should pass tooltip value to encoding', () => {
const model = parseUnitModelWithScaleAndLayoutSize({
"mark": "bar",
"encoding": {
"x": {"type": "quantitative", "field": "Cost__Other", "aggregate": "sum"},
"y": {"bin": true, "type": "quantitative", "field": "Cost__Total_$"},
"tooltip": {"value": "foo"}
}
});
const markGroup = parseMarkGroup(model);
assert.equal(markGroup[0].encode.update.tooltip.value, 'foo');
});
});
});

describe('getPathSort', () => {
Expand Down
28 changes: 28 additions & 0 deletions test/compile/mark/point.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,34 @@ describe('Mark: Point', function() {
});

});

describe('with tooltip', () => {
const model = parseUnitModelWithScaleAndLayoutSize({
"mark": "point",
"encoding": {
"tooltip": {"value": "foo"}
}
});
const props = point.encodeEntry(model);

it('should pass tooltip value to encoding', () => {
assert.deepEqual(props.tooltip, {value: "foo"});
});
});

describe('with href', () => {
const model = parseUnitModelWithScaleAndLayoutSize({
"mark": "point",
"encoding": {
"href": {"value": "https://idl.cs.washington.edu/"}
}
});
const props = point.encodeEntry(model);

it('should pass href value to encoding', () => {
assert.deepEqual(props.href, {value: 'https://idl.cs.washington.edu/'});
});
});
});

describe('Mark: Square', function() {
Expand Down

0 comments on commit b4dfb96

Please sign in to comment.