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 0000000000..ae559c9624 Binary files /dev/null and b/examples/compiled/config_numberFormatType_tooltip.png differ diff --git a/examples/compiled/config_numberFormatType_tooltip.svg b/examples/compiled/config_numberFormatType_tooltip.svg new file mode 100644 index 0000000000..069d22f072 --- /dev/null +++ b/examples/compiled/config_numberFormatType_tooltip.svg @@ -0,0 +1 @@ +197019751980Year0102030Average of Miles_per_Gallon \ 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(