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

Support href and cursor properties for marks #3229

Merged
merged 4 commits into from
Jan 15, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
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 can be set to `pointer` to serve as affordance for hyperlinks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a link for "The cursor mark property"?


{% 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
7 changes: 6 additions & 1 deletion site/docs/mark/mark.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,16 @@ 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" %}

### Link Properties

Marks can act as hyperlinks when the `href` property is defined. A `cursor` property can also be provided to serve as affordance for the links.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

href property or channel

(add link for "channel" part)


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

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

{:#interpolate}
Expand Down
18 changes: 11 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,9 @@ 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, tooltip, and href have format instead of scale
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

href does not have format

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 +162,6 @@ export interface SupportedMark {
line?: boolean;
area?: boolean;
text?: boolean;
tooltip?: boolean;
}

/**
Expand All @@ -184,6 +186,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 +226,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 @@ -23,6 +23,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'|'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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why previously list cursor as "vg channel that do not have mark config". Can you test if cursor works below for sure?

Btw, does it really make sense to have href as a mark config too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
  "$schema": "https://vega.github.io/schema/vega-lite/v2.json",
  "description": "A simple bar chart with embedded data.",
  "data": {
    "values": [
      {"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
      {"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
      {"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
    ]
  },
  "mark": "bar",
  "encoding": {
    "x": {"field": "a", "type": "ordinal"},
    "y": {"field": "b", "type": "quantitative"}
  },
  "config": {
    "bar": {
      "cursor": "pointer"
    }
  }
}

works

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: 15 additions & 0 deletions test/compile/mark/mark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ describe('Mark', function() {
assert.equal(markGroup[0].encode.update.tooltip.value, 'foo');
});
});

describe('Bar with href', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, I have created test for these types of basic test in point.

Also, going forward, let's try to use describe for function and namespace rather than cases (which it is more appropriate).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I'm moving the tooltip and href tests.

it('should pass href 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_$"},
"href": {"value": "https://idl.cs.washington.edu/"}
}
});
const markGroup = parseMarkGroup(model);
assert.equal(markGroup[0].encode.update.href.value, 'https://idl.cs.washington.edu/');
});
});
});

describe('getPathSort', () => {
Expand Down