From 0e6271c951262e92dd95097b4bcca058ceb13d92 Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Thu, 11 Jan 2018 20:52:38 +0500 Subject: [PATCH] Href channel. Ref #1855 --- build/vega-lite-schema.json | 25 ++++++++++++++++++++++++- site/docs/encoding.md | 14 +++++++------- src/channel.ts | 18 +++++++++++------- src/compile/mark/area.ts | 1 + src/compile/mark/bar.ts | 1 + src/compile/mark/line.ts | 1 + src/compile/mark/mixins.ts | 2 +- src/compile/mark/point.ts | 1 + src/compile/mark/rect.ts | 1 + src/compile/mark/rule.ts | 1 + src/compile/mark/text.ts | 1 + src/compile/mark/tick.ts | 2 ++ src/encoding.ts | 5 +++++ src/fielddef.ts | 1 + test/channel.test.ts | 2 +- test/compile/mark/mark.test.ts | 15 +++++++++++++++ 16 files changed, 74 insertions(+), 17 deletions(-) diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index 5817097154a..663d7c2ba87 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -1299,6 +1299,17 @@ ], "description": "Additional levels of detail for grouping data in aggregate views and\nin line and area marks without mapping data to a specific visual channel." }, + "href": { + "anyOf": [ + { + "$ref": "#/definitions/TextFieldDefWithCondition" + }, + { + "$ref": "#/definitions/TextValueDefWithCondition" + } + ], + "description": "A URL to load upon mouse click." + }, "opacity": { "anyOf": [ { @@ -1447,6 +1458,17 @@ ], "description": "Additional levels of detail for grouping data in aggregate views and\nin line and area marks without mapping data to a specific visual channel." }, + "href": { + "anyOf": [ + { + "$ref": "#/definitions/TextFieldDefWithCondition" + }, + { + "$ref": "#/definitions/TextValueDefWithCondition" + } + ], + "description": "A URL to load upon mouse click." + }, "opacity": { "anyOf": [ { @@ -4508,7 +4530,8 @@ "color", "opacity", "text", - "tooltip" + "tooltip", + "href" ], "type": "string" }, diff --git a/site/docs/encoding.md b/site/docs/encoding.md index 11363b1ea3a..0f83116138e 100644 --- a/site/docs/encoding.md +++ b/site/docs/encoding.md @@ -180,27 +180,27 @@ In addition to the constant `value`, [value definitions](#value-def) of mark pro See [the `condition`](condition.html) page for examples how to specify condition logic. {:#text} -## Text and Tooltip Channels +## Text, Tooltip, and HREF Channels -Text and tooltip channels directly encode text values of the data fields. +Text, tooltip, and HREF channels directly encode text values of the data fields. By default, Vega-Lite automatically determines appropriate format for quantitative and temporal values. Users can set `format` property to customize text and time format. Similar to mark property channels, definitions of text and tooltip channels can include the `condition` property to specify conditional logic. -{% include table.html props="text,tooltip" source="Encoding" %} +{% include table.html props="text,tooltip,href" source="Encoding" %} {:#text-field-def} -### Text and Tooltip Field Definition +### Text, Tooltip, and HREF 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 `text` and `tooltip` channels may also include these properties: +[field definitions](#field-def) for `text`, `tooltip`, and `href` channels may also include these properties: {% include table.html props="format,condition" source="TextFieldDefWithCondition" %} {:#text-value-def} -### Text and Tooltip Value Definition +### Text, Tooltip, and HREF Value Definition -In addition to the constant `value`, [value definitions](#value-def) of `text` and `tooltip` channels can include the `condition` property to specify conditional logic. +In addition to the constant `value`, [value definitions](#value-def) of `text`, `tooltip`, and `href` channels can include the `condition` property to specify conditional logic. {% include table.html props="condition" source="TextValueDefWithCondition" %} diff --git a/src/channel.ts b/src/channel.ts index 46e25808d2e..b6a0aefcc92 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -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 | keyof FacetMapping; @@ -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> = { x: 1, @@ -63,7 +65,8 @@ const UNIT_CHANNEL_INDEX: Flag> = { opacity: 1, text: 1, detail: 1, - tooltip: 1 + tooltip: 1, + href: 1, }; const FACET_CHANNEL_INDEX: Flag> = { @@ -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'; @@ -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 has format instead of scale + text: _t, tooltip: _tt, href: _hr, // detail and order have no scale detail: _dd, order: _oo, ...NONPOSITION_SCALE_CHANNEL_INDEX @@ -159,7 +162,6 @@ export interface SupportedMark { line?: boolean; area?: boolean; text?: boolean; - tooltip?: boolean; } /** @@ -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: @@ -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. diff --git a/src/compile/mark/area.ts b/src/compile/mark/area.ts index 92035b10190..b0a6d60a89f 100644 --- a/src/compile/mark/area.ts +++ b/src/compile/mark/area.ts @@ -14,6 +14,7 @@ export const area: MarkCompiler = { ...mixins.color(model), ...mixins.text(model, 'tooltip'), + ...mixins.text(model, 'href'), ...mixins.nonPosition('opacity', model), }; } diff --git a/src/compile/mark/bar.ts b/src/compile/mark/bar.ts index 3bc755c39e5..c2d0532fc62 100644 --- a/src/compile/mark/bar.ts +++ b/src/compile/mark/bar.ts @@ -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) }; } diff --git a/src/compile/mark/line.ts b/src/compile/mark/line.ts index 05eb69f058f..56c62cc8e35 100644 --- a/src/compile/mark/line.ts +++ b/src/compile/mark/line.ts @@ -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 diff --git a/src/compile/mark/mixins.ts b/src/compile/mark/mixins.ts index 197f1ecda39..614970dd56c 100644 --- a/src/compile/mark/mixins.ts +++ b/src/compile/mark/mixins.ts @@ -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)); } diff --git a/src/compile/mark/point.ts b/src/compile/mark/point.ts index cba61122f24..abf9f562322 100644 --- a/src/compile/mark/point.ts +++ b/src/compile/mark/point.ts @@ -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), diff --git a/src/compile/mark/rect.ts b/src/compile/mark/rect.ts index b69f4c20996..77d36bfc808 100644 --- a/src/compile/mark/rect.ts +++ b/src/compile/mark/rect.ts @@ -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), }; } diff --git a/src/compile/mark/rule.ts b/src/compile/mark/rule.ts index d8e88d4c869..2aff4237e34 100644 --- a/src/compile/mark/rule.ts +++ b/src/compile/mark/rule.ts @@ -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 diff --git a/src/compile/mark/text.ts b/src/compile/mark/text.ts index e875f0a27b7..18662a27b39 100644 --- a/src/compile/mark/text.ts +++ b/src/compile/mark/text.ts @@ -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 diff --git a/src/compile/mark/tick.ts b/src/compile/mark/tick.ts index 9f4b63c4272..b6dd450d731 100644 --- a/src/compile/mark/tick.ts +++ b/src/compile/mark/tick.ts @@ -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), }; } diff --git a/src/encoding.ts b/src/encoding.ts index 750a30420ca..d586b7d5004 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -98,6 +98,11 @@ export interface Encoding { */ tooltip?: FieldDefWithCondition> | ValueDefWithCondition>; + /** + * A URL to load upon mouse click. + */ + href?: FieldDefWithCondition> | ValueDefWithCondition>; + /** * Stack order for stacked marks or order of data points in line marks for connected scatter plots. * diff --git a/src/fielddef.ts b/src/fielddef.ts index 286a00b6ce7..1a9efad07c4 100644 --- a/src/fielddef.ts +++ b/src/fielddef.ts @@ -541,6 +541,7 @@ export function channelCompatibility(fieldDef: FieldDef, channel: Channel case 'text': case 'detail': case 'tooltip': + case 'href': return COMPATIBLE; case 'opacity': diff --git a/test/channel.test.ts b/test/channel.test.ts index b1b787daaab..f22ca07c485 100644 --- a/test/channel.test.ts +++ b/test/channel.test.ts @@ -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'])); }); }); diff --git a/test/compile/mark/mark.test.ts b/test/compile/mark/mark.test.ts index 6f32699a809..bc5a4e71dca 100644 --- a/test/compile/mark/mark.test.ts +++ b/test/compile/mark/mark.test.ts @@ -120,6 +120,21 @@ describe('Mark', function() { assert.equal(markGroup[0].encode.update.tooltip.value, 'foo'); }); }); + + describe('Bar with href', () => { + 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', () => {