From e7b45b8ca0dd67915a8d8d962747f56de41a4cab Mon Sep 17 00:00:00 2001 From: Lukas Hermann Date: Fri, 5 May 2023 18:48:49 -0400 Subject: [PATCH] feat: allow custom tooltip formatting (#8883) Co-authored-by: GitHub Actions Bot Co-authored-by: Kanit Wongsuphasawat --- build/vega-lite-schema.json | 34 +++++ .../config_numberFormatType_tooltip.png | Bin 0 -> 8965 bytes .../config_numberFormatType_tooltip.svg | 1 + .../config_numberFormatType_tooltip.vg.json | 126 ++++++++++++++++++ .../config_numberFormatType_tooltip.vl.json | 19 +++ site/docs/config.md | 14 +- src/compile/mark/encode/tooltip.ts | 9 +- src/config.ts | 81 ++++++----- test/compile/mark/encode/tooltip.test.ts | 55 ++++++++ 9 files changed, 294 insertions(+), 45 deletions(-) create mode 100644 examples/compiled/config_numberFormatType_tooltip.png create mode 100644 examples/compiled/config_numberFormatType_tooltip.svg create mode 100644 examples/compiled/config_numberFormatType_tooltip.vg.json create mode 100644 examples/specs/config_numberFormatType_tooltip.vl.json diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json index a6c9d50f57..152f30f779 100644 --- a/build/vega-lite-schema.json +++ b/build/vega-lite-schema.json @@ -7765,6 +7765,10 @@ "$ref": "#/definitions/TitleConfig", "description": "Title configuration, which determines default properties for all [titles](https://vega.github.io/vega-lite/docs/title.html). For a full list of title configuration options, please see the [corresponding section of the title documentation](https://vega.github.io/vega-lite/docs/title.html#config)." }, + "tooltipFormat": { + "$ref": "#/definitions/FormatConfig", + "description": "Define [custom format configuration](https://vega.github.io/vega-lite/docs/config.html#format) for tooltips. If unspecified, default format config will be applied." + }, "trail": { "$ref": "#/definitions/LineConfig", "description": "Trail-Specific Config" @@ -10851,6 +10855,36 @@ "number" ] }, + "FormatConfig": { + "additionalProperties": false, + "properties": { + "normalizedNumberFormat": { + "description": "If normalizedNumberFormatType is not specified, D3 number format for axis labels, text marks, and tooltips of normalized stacked fields (fields with `stack: \"normalize\"`). For example `\"s\"` for SI units. Use [D3's number format pattern](https://github.com/d3/d3-format#locale_format).\n\nIf `config.normalizedNumberFormatType` is specified and `config.customFormatTypes` is `true`, this value will be passed as `format` alongside `datum.value` to the `config.numberFormatType` function. __Default value:__ `%`", + "type": "string" + }, + "normalizedNumberFormatType": { + "description": "[Custom format type](https://vega.github.io/vega-lite/docs/config.html#custom-format-type) for `config.normalizedNumberFormat`.\n\n__Default value:__ `undefined` -- This is equilvalent to call D3-format, which is exposed as [`format` in Vega-Expression](https://vega.github.io/vega/docs/expressions/#format). __Note:__ You must also set `customFormatTypes` to `true` to use this feature.", + "type": "string" + }, + "numberFormat": { + "description": "If numberFormatType is not specified, D3 number format for guide labels, text marks, and tooltips of non-normalized fields (fields *without* `stack: \"normalize\"`). For example `\"s\"` for SI units. Use [D3's number format pattern](https://github.com/d3/d3-format#locale_format).\n\nIf `config.numberFormatType` is specified and `config.customFormatTypes` is `true`, this value will be passed as `format` alongside `datum.value` to the `config.numberFormatType` function.", + "type": "string" + }, + "numberFormatType": { + "description": "[Custom format type](https://vega.github.io/vega-lite/docs/config.html#custom-format-type) for `config.numberFormat`.\n\n__Default value:__ `undefined` -- This is equilvalent to call D3-format, which is exposed as [`format` in Vega-Expression](https://vega.github.io/vega/docs/expressions/#format). __Note:__ You must also set `customFormatTypes` to `true` to use this feature.", + "type": "string" + }, + "timeFormat": { + "description": "Default time format for raw time values (without time units) in text marks, legend labels and header labels.\n\n__Default value:__ `\"%b %d, %Y\"` __Note:__ Axes automatically determine the format for each label automatically so this config does not affect axes.", + "type": "string" + }, + "timeFormatType": { + "description": "[Custom format type](https://vega.github.io/vega-lite/docs/config.html#custom-format-type) for `config.timeFormat`.\n\n__Default value:__ `undefined` -- This is equilvalent to call D3-time-format, which is exposed as [`timeFormat` in Vega-Expression](https://vega.github.io/vega/docs/expressions/#timeFormat). __Note:__ You must also set `customFormatTypes` to `true` and there must *not* be a `timeUnit` defined to use this feature.", + "type": "string" + } + }, + "type": "object" + }, "Generator": { "anyOf": [ { diff --git a/examples/compiled/config_numberFormatType_tooltip.png b/examples/compiled/config_numberFormatType_tooltip.png new file mode 100644 index 0000000000000000000000000000000000000000..ae559c96246127015cd3b8a3d40ea6fc62bf9474 GIT binary patch literal 8965 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 \ No newline at end of file diff --git a/examples/compiled/config_numberFormatType_tooltip.vg.json b/examples/compiled/config_numberFormatType_tooltip.vg.json new file mode 100644 index 0000000000..b5c555ffc5 --- /dev/null +++ b/examples/compiled/config_numberFormatType_tooltip.vg.json @@ -0,0 +1,126 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "Testing global number formatting config", + "background": "white", + "padding": 5, + "width": 150, + "height": 150, + "style": "cell", + "data": [ + { + "name": "source_0", + "url": "data/cars.json", + "format": {"type": "json", "parse": {"Year": "date"}}, + "transform": [ + { + "type": "aggregate", + "groupby": ["Year"], + "ops": ["average"], + "fields": ["Miles_per_Gallon"], + "as": ["average_Miles_per_Gallon"] + }, + { + "type": "filter", + "expr": "(isDate(datum[\"Year\"]) || (isValid(datum[\"Year\"]) && isFinite(+datum[\"Year\"]))) && isValid(datum[\"average_Miles_per_Gallon\"]) && isFinite(+datum[\"average_Miles_per_Gallon\"])" + } + ] + } + ], + "marks": [ + { + "name": "marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "source_0"}, + "encode": { + "update": { + "tooltip": { + "signal": "{\"Year\": timeFormat(datum[\"Year\"], '%b %d, %Y'), \"Average of Miles_per_Gallon\": format(datum[\"average_Miles_per_Gallon\"], \".8f\")}" + }, + "fill": {"value": "#4c78a8"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"Year: \" + (timeFormat(datum[\"Year\"], '%b %d, %Y')) + \"; Average of Miles_per_Gallon: \" + (format(datum[\"average_Miles_per_Gallon\"], \".8f\"))" + }, + "xc": {"scale": "x", "field": "Year"}, + "width": {"value": 5}, + "y": {"scale": "y", "field": "average_Miles_per_Gallon"}, + "y2": {"scale": "y", "value": 0} + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "time", + "domain": {"data": "source_0", "field": "Year"}, + "range": [0, {"signal": "width"}], + "padding": 5 + }, + { + "name": "y", + "type": "linear", + "domain": {"data": "source_0", "field": "average_Miles_per_Gallon"}, + "range": [{"signal": "height"}, 0], + "nice": true, + "zero": true + } + ], + "axes": [ + { + "scale": "x", + "orient": "bottom", + "gridScale": "y", + "grid": true, + "tickCount": {"signal": "ceil(width/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(height/40)"}, + "tickMinStep": 1, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + }, + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "Year", + "labelFlush": true, + "labelOverlap": true, + "tickCount": {"signal": "ceil(width/40)"}, + "zindex": 0 + }, + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Average of Miles_per_Gallon", + "format": "d", + "labelOverlap": true, + "tickCount": {"signal": "ceil(height/40)"}, + "tickMinStep": 1, + "zindex": 0 + } + ], + "config": { + "tooltipFormat": {"numberFormat": ".8f"}, + "customFormatTypes": true + } +} diff --git a/examples/specs/config_numberFormatType_tooltip.vl.json b/examples/specs/config_numberFormatType_tooltip.vl.json new file mode 100644 index 0000000000..487f4bfae7 --- /dev/null +++ b/examples/specs/config_numberFormatType_tooltip.vl.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "Testing global number formatting config", + "width": 150, + "height": 150, + "data": {"url": "data/cars.json"}, + "mark": {"type": "bar", "tooltip": true}, + "encoding": { + "x": {"field": "Year", "type": "temporal"}, + "y": {"field": "Miles_per_Gallon", "type": "quantitative", "aggregate": "average"} + }, + "config": { + "tooltipFormat": { + "numberFormat": ".8f" + }, + "numberFormat": "d", + "customFormatTypes": true + } +} diff --git a/site/docs/config.md b/site/docs/config.md index 809f275d98..0b990ec856 100644 --- a/site/docs/config.md +++ b/site/docs/config.md @@ -44,13 +44,13 @@ The rest of this page outlines different types of config properties: A Vega-Lite `config` object can have the following top-level properties: -{% include table.html props="autosize,background,countTitle,fieldTitle,font,lineBreak,padding" source="Config" %} +{% include table.html props="autosize,background,countTitle,fieldTitle,font,lineBreak,padding,tooltipFormat" source="Config" %} {:#format} ## Format Configuration -These config properties define the default number and time formats for text marks as well as axes, headers, and legends: +These config properties define the default number and time formats for text marks as well as axes, headers, tooltip, and legends: {% include table.html props="numberFormat,numberFormatType,normalizedNumberFormat,normalizedNumberFormatType,timeFormat,timeFormatType,customFormatTypes" source="Config" %} @@ -69,7 +69,7 @@ vega.expressionFunction('customFormatA', function(datum, params) { }); ``` -2. Setting the `customFormatTypes` config to `true`. +(2) Setting the `customFormatTypes` config to `true`. ```js { @@ -78,7 +78,7 @@ vega.expressionFunction('customFormatA', function(datum, params) { } ``` -3. You can then use this custom format function with `format` and `formatType` properties in text encodings and guides (axis/legend/header). +(3) You can then use this custom format function with `format` and `formatType` properties in text encodings and guides (axis/legend/header). ```js { @@ -87,6 +87,12 @@ vega.expressionFunction('customFormatA', function(datum, params) { } ``` +### Customize Formatter for Tooltips only + +Since tooltips have more screen estate and less chance of collisions, sometimes it is desirable to have a truncated format in a visualization, with a longer format in the tooltip. For example, in the visualization below, we want the y-axis to have the format `d` so it does not have a decimal point, so as not to have incredibly long labels, but on the tooltip it has the longer `.8f`. To achieve this specificity, one can add a `tooltipFormat` prop to their config that conforms to the [FormatConfig](#format) type. + + + {:#axis-config} ## Guide Configurations diff --git a/src/compile/mark/encode/tooltip.ts b/src/compile/mark/encode/tooltip.ts index 9b20c73f99..bf31cdd695 100644 --- a/src/compile/mark/encode/tooltip.ts +++ b/src/compile/mark/encode/tooltip.ts @@ -72,6 +72,7 @@ export function tooltipData( config: Config, {reactiveGeom}: {reactiveGeom?: boolean} = {} ) { + const formatConfig = {...config, ...config.tooltipFormat}; const toSkip = {}; const expr = reactiveGeom ? 'datum.datum' : 'datum'; const tuples: {channel: Channel; key: string; value: string}[] = []; @@ -86,7 +87,7 @@ export function tooltipData( type: (encoding[mainChannel] as TypedFieldDef).type // for secondary field def, copy type from main channel }; - const title = fieldDef.title || defaultTitle(fieldDef, config); + const title = fieldDef.title || defaultTitle(fieldDef, formatConfig); const key = array(title).join(', '); let value: string; @@ -99,7 +100,7 @@ export function tooltipData( const startField = vgField(fieldDef, {expr}); const endField = vgField(fieldDef2, {expr}); const {format, formatType} = getFormatMixins(fieldDef); - value = binFormatExpression(startField, endField, format, formatType, config); + value = binFormatExpression(startField, endField, format, formatType, formatConfig); toSkip[channel2] = true; } } @@ -116,12 +117,12 @@ export function tooltipData( format, formatType, expr, - config, + config: formatConfig, normalizeStack: true }).signal; } - value ??= textRef(fieldDef, config, expr).signal; + value ??= textRef(fieldDef, formatConfig, expr).signal; tuples.push({channel, key, value}); } diff --git a/src/config.ts b/src/config.ts index 4c1a905e3c..e18ae6e766 100644 --- a/src/config.ts +++ b/src/config.ts @@ -107,43 +107,7 @@ export type ColorConfig = Record; export type FontSizeConfig = Record; -export interface VLOnlyConfig { - /** - * Default font for all text marks, titles, and labels. - */ - font?: string; - - /** - * Default color signals. - * - * @hidden - */ - color?: boolean | ColorConfig; - - /** - * Default font size signals. - * - * @hidden - */ - fontSize?: boolean | FontSizeConfig; - - /** - * Default axis and legend title for count fields. - * - * __Default value:__ `'Count of Records`. - * - * @type {string} - */ - countTitle?: string; - - /** - * Defines how Vega-Lite generates title for fields. There are three possible styles: - * - `"verbal"` (Default) - displays function in a verbal style (e.g., "Sum of field", "Year-month of date", "field (binned)"). - * - `"function"` - displays function using parentheses and capitalized texts (e.g., "SUM(field)", "YEARMONTH(date)", "BIN(field)"). - * - `"plain"` - displays only the field name without functions (e.g., "field", "date", "field"). - */ - fieldTitle?: 'verbal' | 'functional' | 'plain'; - +export interface FormatConfig { /** * If numberFormatType is not specified, * D3 number format for guide labels, text marks, and tooltips of non-normalized fields (fields *without* `stack: "normalize"`). For example `"s"` for SI units. @@ -197,12 +161,55 @@ export interface VLOnlyConfig { * __Note:__ You must also set `customFormatTypes` to `true` and there must *not* be a `timeUnit` defined to use this feature. */ timeFormatType?: string; +} + +export interface VLOnlyConfig extends FormatConfig { + /** + * Default font for all text marks, titles, and labels. + */ + font?: string; + + /** + * Default color signals. + * + * @hidden + */ + color?: boolean | ColorConfig; + + /** + * Default font size signals. + * + * @hidden + */ + fontSize?: boolean | FontSizeConfig; + + /** + * Default axis and legend title for count fields. + * + * __Default value:__ `'Count of Records`. + * + * @type {string} + */ + countTitle?: string; + + /** + * Defines how Vega-Lite generates title for fields. There are three possible styles: + * - `"verbal"` (Default) - displays function in a verbal style (e.g., "Sum of field", "Year-month of date", "field (binned)"). + * - `"function"` - displays function using parentheses and capitalized texts (e.g., "SUM(field)", "YEARMONTH(date)", "BIN(field)"). + * - `"plain"` - displays only the field name without functions (e.g., "field", "date", "field"). + */ + fieldTitle?: 'verbal' | 'functional' | 'plain'; /** * Allow the `formatType` property for text marks and guides to accept a custom formatter function [registered as a Vega expression](https://vega.github.io/vega-lite/usage/compile.html#format-type). */ customFormatTypes?: boolean; + /** + * Define [custom format configuration](https://vega.github.io/vega-lite/docs/config.html#format) for tooltips. If unspecified, default format config will be applied. + */ + tooltipFormat?: FormatConfig; + /** Default properties for [single view plots](https://vega.github.io/vega-lite/docs/spec.html#single). */ view?: ViewConfig; diff --git a/test/compile/mark/encode/tooltip.test.ts b/test/compile/mark/encode/tooltip.test.ts index 2a71bacbfe..238e7bf007 100644 --- a/test/compile/mark/encode/tooltip.test.ts +++ b/test/compile/mark/encode/tooltip.test.ts @@ -248,6 +248,61 @@ describe('compile/mark/encode/tooltip', () => { }); }); + it('returns correct tooltip signal for formatted normalized stacked field using tooltipFormat', () => { + expect( + tooltipRefForEncoding( + { + x: { + aggregate: 'sum', + field: 'IMDB_Rating', + type: 'quantitative' + } + }, + { + fieldChannel: 'x', + groupbyChannels: [], + groupbyFields: new Set(), + offset: 'normalize', + impute: false, + stackBy: [] + }, + {...defaultConfig, tooltipFormat: {normalizedNumberFormat: '.4%'}, customFormatTypes: true} + ) + ).toEqual({ + signal: `{"Sum of IMDB_Rating": format(datum["sum_IMDB_Rating_end"]-datum["sum_IMDB_Rating_start"], ".4%")}` + }); + }); + + it('returns correct tooltip signal for formatted normalized stacked field preferring tooltipFormat', () => { + expect( + tooltipRefForEncoding( + { + x: { + aggregate: 'sum', + field: 'IMDB_Rating', + type: 'quantitative' + } + }, + { + fieldChannel: 'x', + groupbyChannels: [], + groupbyFields: new Set(), + offset: 'normalize', + impute: false, + stackBy: [] + }, + { + ...defaultConfig, + tooltipFormat: {normalizedNumberFormat: '.4%'}, + normalizedNumberFormat: '.2%', + customFormatTypes: true + } + ) + ).toEqual({ + signal: `{"Sum of IMDB_Rating": format(datum["sum_IMDB_Rating_end"]-datum["sum_IMDB_Rating_start"], ".4%")}` + }); + }); + it('returns correct tooltip signal for binned field with custom title', () => { expect( tooltipRefForEncoding(