From bab7b850f143e29b601428ac6f929d3b17a71f4f Mon Sep 17 00:00:00 2001 From: Kanit Wongsuphasawat Date: Sat, 21 Apr 2018 15:35:05 -0700 Subject: [PATCH] Make tooltip support tooltip array for specifying multiple fields --- build/vega-lite-schema.json | 66 ++++++++++++++++++++++++++++++++ src/compile/common.ts | 2 +- src/compile/mark/mixins.ts | 26 +++++++++++-- src/encoding.ts | 4 +- test/compile/mark/mixins.test.ts | 18 ++++++++- 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index f1d4858c37..a06b54963a 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -2006,6 +2006,12 @@ }, { "$ref": "#/definitions/TextValueDefWithCondition" + }, + { + "items": { + "$ref": "#/definitions/TextFieldDef" + }, + "type": "array" } ], "description": "The tooltip text to show upon mouse hover." @@ -2211,6 +2217,12 @@ }, { "$ref": "#/definitions/TextValueDefWithCondition" + }, + { + "items": { + "$ref": "#/definitions/TextFieldDef" + }, + "type": "array" } ], "description": "The tooltip text to show upon mouse hover." @@ -6366,6 +6378,60 @@ }, "type": "object" }, + "TextFieldDef": { + "additionalProperties": false, + "properties": { + "aggregate": { + "$ref": "#/definitions/Aggregate", + "description": "Aggregation function for the field\n(e.g., `mean`, `sum`, `median`, `min`, `max`, `count`).\n\n__Default value:__ `undefined` (None)" + }, + "bin": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/BinParams" + } + ], + "description": "A flag for binning a `quantitative` field, or [an object defining binning parameters](https://vega.github.io/vega-lite/docs/bin.html#params).\nIf `true`, default [binning parameters](https://vega.github.io/vega-lite/docs/bin.html) will be applied.\n\n__Default value:__ `false`" + }, + "field": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/RepeatRef" + } + ], + "description": "__Required.__ A string defining the name of the field from which to pull a data value\nor an object defining iterated values from the [`repeat`](https://vega.github.io/vega-lite/docs/repeat.html) operator.\n\n__Note:__ Dots (`.`) and brackets (`[` and `]`) can be used to access nested objects (e.g., `\"field\": \"foo.bar\"` and `\"field\": \"foo['bar']\"`).\nIf field names contain dots or brackets but are not nested, you can use `\\\\` to escape dots and brackets (e.g., `\"a\\\\.b\"` and `\"a\\\\[0\\\\]\"`).\nSee more details about escaping in the [field documentation](https://vega.github.io/vega-lite/docs/field.html).\n\n__Note:__ `field` is not required if `aggregate` is `count`." + }, + "format": { + "description": "The [formatting pattern](https://vega.github.io/vega-lite/docs/format.html) for a text field. If not defined, this will be determined automatically.", + "type": "string" + }, + "timeUnit": { + "$ref": "#/definitions/TimeUnit", + "description": "Time unit (e.g., `year`, `yearmonth`, `month`, `hours`) for a temporal field.\nor [a temporal field that gets casted as ordinal](https://vega.github.io/vega-lite/docs/type.html#cast).\n\n__Default value:__ `undefined` (None)" + }, + "title": { + "description": "A title for the field. If `null`, the title will be removed.\n\n__Default value:__ derived from the field's name and transformation function (`aggregate`, `bin` and `timeUnit`). If the field has an aggregate function, the function is displayed as part of the title (e.g., `\"Sum of Profit\"`). If the field is binned or has a time unit applied, the applied function is shown in parentheses (e.g., `\"Profit (binned)\"`, `\"Transaction Date (year-month)\"`). Otherwise, the title is simply the field name.\n\n__Notes__:\n\n1) You can customize the default field title format by providing the [`fieldTitle` property in the [config](config.html) or [`fieldTitle` function via the `compile` function's options](compile.html#field-title).\n\n2) If both field definition's `title` and axis, header, or legend `title` are defined, axis/header/legend title will be used.", + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/Type", + "description": "The encoded field's type of measurement (`\"quantitative\"`, `\"temporal\"`, `\"ordinal\"`, or `\"nominal\"`).\nIt can also be a `\"geojson\"` type for encoding ['geoshape'](geoshape.html)." + } + }, + "required": [ + "type" + ], + "type": "object" + }, "TickConfig": { "additionalProperties": false, "properties": { diff --git a/src/compile/common.ts b/src/compile/common.ts index 680a4dd61c..fcd445048b 100644 --- a/src/compile/common.ts +++ b/src/compile/common.ts @@ -80,7 +80,7 @@ export function formatSignalRef(fieldDef: FieldDef, specifiedFormat: str }; } else if (fieldDef.type === 'quantitative') { return { - signal: `${formatExpr(vgField(fieldDef, {expr}), format)}` + signal: `${formatExpr(vgField(fieldDef, {expr, binSuffix: 'range'}), format)}` }; } else if (isTimeFieldDef(fieldDef)) { const isUTCScale = isScaleFieldDef(fieldDef) && fieldDef['scale'] && fieldDef['scale'].type === ScaleType.UTC; diff --git a/src/compile/mark/mixins.ts b/src/compile/mark/mixins.ts index fc51263ec3..16a407147e 100644 --- a/src/compile/mark/mixins.ts +++ b/src/compile/mark/mixins.ts @@ -1,6 +1,6 @@ import {isArray} from 'vega-util'; import {NONPOSITION_SCALE_CHANNELS} from '../../channel'; -import {ChannelDef, FieldDef, getFieldDef, isConditionalSelection, isValueDef} from '../../fielddef'; +import {ChannelDef, FieldDef, FieldDefWithCondition, getFieldDef, isConditionalSelection, isValueDef, TextFieldDef, ValueDefWithCondition, vgField} from '../../fielddef'; import * as log from '../../log'; import {MarkDef} from '../../mark'; import {expression} from '../../predicate'; @@ -96,7 +96,7 @@ export function baseEncodeEntry(model: UnitModel, ignore: Ignore) { ...markDefProperties(model.markDef, ignore), ...color(model), ...nonPosition('opacity', model), - ...text(model, 'tooltip'), + ...tooltip(model), ...text(model, 'href') }; } @@ -167,8 +167,28 @@ function wrapCondition( } } -export function text(model: UnitModel, channel: 'text' | 'tooltip' | 'href' = 'text') { +export function tooltip(model: UnitModel) { + const channel = 'tooltip'; const channelDef = model.encoding[channel]; + if (isArray(channelDef)) { + const keyValues = channelDef.map((fieldDef) => { + const key = fieldDef.title !== undefined ? fieldDef.title : vgField(fieldDef, {binSuffix: 'range'}); + const value = ref.text(fieldDef, model.config).signal; + return `"${key}": ${value}`; + }); + return {tooltip: {signal: `{${keyValues.join(', ')}}`}}; + } else { + // if not an array, behave just like text + return textCommon(model, channel, channelDef); + } +} + +export function text(model: UnitModel, channel: 'text' | 'href' = 'text') { + const channelDef = model.encoding[channel]; + return textCommon(model, channel, channelDef); +} + +function textCommon(model: UnitModel, channel: 'text' | 'href' | 'tooltip', channelDef: FieldDefWithCondition> | ValueDefWithCondition>) { return wrapCondition(model, channelDef, channel, (cDef) => ref.text(cDef, model.config)); } diff --git a/src/encoding.ts b/src/encoding.ts index 69d716b365..815644cf67 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -146,7 +146,7 @@ export interface Encoding { /** * The tooltip text to show upon mouse hover. */ - tooltip?: FieldDefWithCondition> | ValueDefWithCondition>; + tooltip?: FieldDefWithCondition> | ValueDefWithCondition> | TextFieldDef[]; /** * A URL to load upon mouse click. @@ -224,7 +224,7 @@ export function normalizeEncoding(encoding: Encoding, mark: Mark): Encod return normalizedEncoding; } - if (channel === 'detail' || channel === 'order') { + if (channel === 'detail' || channel === 'order' || (channel === 'tooltip' && isArray(encoding[channel]))) { const channelDef = encoding[channel]; if (channelDef) { // Array of fieldDefs for detail channel (or production rule) diff --git a/test/compile/mark/mixins.test.ts b/test/compile/mark/mixins.test.ts index 5c18216f85..d567b1d6f1 100644 --- a/test/compile/mark/mixins.test.ts +++ b/test/compile/mark/mixins.test.ts @@ -2,7 +2,7 @@ import {assert} from 'chai'; import {X, Y} from '../../../src/channel'; -import {color, pointPosition} from '../../../src/compile/mark/mixins'; +import {color, pointPosition, tooltip} from '../../../src/compile/mark/mixins'; import * as log from '../../../src/log'; import {parseUnitModelWithScaleAndLayoutSize} from '../../util'; @@ -201,6 +201,22 @@ describe('compile/mark/mixins', () => { }); }); + describe('tootlip()', () => { + it('generates tooltip object signal for an array of tooltip fields', function () { + const model = parseUnitModelWithScaleAndLayoutSize({ + "mark": "point", + "encoding": { + "tooltip": [ + {"field": "Horsepower", "type": "quantitative"}, + {"field": "Acceleration", "type": "quantitative"} + ] + } + }); + const props = tooltip(model); + assert.deepEqual(props.tooltip, {signal: '{"Horsepower": format(datum["Horsepower"], ""), "Acceleration": format(datum["Acceleration"], "")}'}); + }); + }); + describe('midPoint()', function () { it('should return correctly for lat/lng', function () { const model = parseUnitModelWithScaleAndLayoutSize({