From 7d48d4ac20290fcb49ed7b3f21ac59bfcfc3700b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 8 Jun 2024 11:15:23 -0700 Subject: [PATCH] tip title format (#2074) * tip title format * channel, not title --- docs/marks/tip.md | 2 +- src/marks/tip.d.ts | 14 +++++++++- src/marks/tip.js | 10 +++---- src/scales.d.ts | 2 +- ...oreFormat.svg => tipFormatTitleFormat.svg} | 2 +- test/output/tipFormatTitleFormatFunction.svg | 28 +++++++++++++++++++ test/output/tipFormatTitleFormatShorthand.svg | 28 +++++++++++++++++++ test/plots/tip-format.ts | 12 ++++++-- 8 files changed, 87 insertions(+), 11 deletions(-) rename test/output/{tipFormatTitleIgnoreFormat.svg => tipFormatTitleFormat.svg} (96%) create mode 100644 test/output/tipFormatTitleFormatFunction.svg create mode 100644 test/output/tipFormatTitleFormatShorthand.svg diff --git a/docs/marks/tip.md b/docs/marks/tip.md index 388d10c7de..a943088326 100644 --- a/docs/marks/tip.md +++ b/docs/marks/tip.md @@ -131,7 +131,7 @@ Plot.rectY(olympians, Plot.binX({y: "sum"}, {x: "weight", y: (d) => d.sex === "m ``` ::: -The order and formatting of channels in the tip can be customized with the **format** option , which accepts a key-value object mapping channel names to formats. Each [format](../features/formats.md) can be a string (for number or time formats), a function that receives the value as input and returns a string, true to use the default format, and null or false to suppress. The order of channels in the tip follows their order in the format object followed by any additional channels. +The order and formatting of channels in the tip can be customized with the **format** option , which accepts a key-value object mapping channel names to formats. Each [format](../features/formats.md) can be a string (for number or time formats), a function that receives the value as input and returns a string, true to use the default format, and null or false to suppress. The order of channels in the tip follows their order in the format object followed by any additional channels. When using the **title** channel, the **format** option may be specified as a string or a function; the given format will then apply to the **title** channel. A channel’s label can be specified alongside its value as a {value, label} object; if a channel label is not specified, the associated scale’s label is used, if any; if there is no associated scale, or if the scale has no label, the channel name is used instead. diff --git a/src/marks/tip.d.ts b/src/marks/tip.d.ts index e115e9db32..906051820c 100644 --- a/src/marks/tip.d.ts +++ b/src/marks/tip.d.ts @@ -74,7 +74,7 @@ export interface TipOptions extends MarkOptions, TextStyles { * is interpreted as a (UTC) time format for temporal channels, and otherwise * a number format. */ - format?: {[name in ChannelName]?: boolean | string | ((d: any, i: number) => string)}; + format?: {[name in ChannelName]?: null | boolean | TipFormat} | TipFormat; /** The image filter for the tip’s box; defaults to a drop shadow. */ pathFilter?: string; @@ -86,6 +86,18 @@ export interface TipOptions extends MarkOptions, TextStyles { textPadding?: number; } +/** + * How to format channel values; one of: + * + * - a [d3-format][1] string for numeric scales + * - a [d3-time-format][2] string for temporal scales + * - a function passed a channel *value* and *index*, returning a string + * + * [1]: https://d3js.org/d3-time + * [2]: https://d3js.org/d3-time-format + */ +export type TipFormat = string | ((d: any, i: number) => string); + /** * Returns a new tip mark for the given *data* and *options*. * diff --git a/src/marks/tip.js b/src/marks/tip.js index 5516e3859a..d741b8e103 100644 --- a/src/marks/tip.js +++ b/src/marks/tip.js @@ -84,7 +84,7 @@ export class Tip extends Mark { for (const key in defaults) if (key in this.channels) this[key] = defaults[key]; // apply default even if channel this.splitLines = splitter(this); this.clipLine = clipper(this); - this.format = {...format}; // defensive copy before mutate; also promote nullish to empty + this.format = typeof format === "string" || typeof format === "function" ? {title: format} : {...format}; // defensive copy before mutate; also promote nullish to empty } render(index, scales, values, dimensions, context) { const mark = this; @@ -120,10 +120,10 @@ export class Tip extends Mark { // channels as name-value pairs. let sources, format; if ("title" in values) { - sources = values.channels; + sources = getSourceChannels.call(this, {title: values.channels.title}, scales); format = formatTitle; } else { - sources = getSourceChannels.call(this, values, scales); + sources = getSourceChannels.call(this, values.channels, scales); format = formatChannels; } @@ -319,7 +319,7 @@ function getPath(anchor, m, r, width, height) { } // Note: mutates this.format! -function getSourceChannels({channels}, scales) { +function getSourceChannels(channels, scales) { const sources = {}; // Promote x and y shorthand for paired channels (in order). @@ -384,7 +384,7 @@ function maybeExpandPairedFormat(format, channels, key) { } function formatTitle(i, index, {title}) { - return formatDefault(title.value[i], i); + return this.format.title(title.value[i], i); } function* formatChannels(i, index, channels, scales, values) { diff --git a/src/scales.d.ts b/src/scales.d.ts index 80ba1d3ed2..94112d6180 100644 --- a/src/scales.d.ts +++ b/src/scales.d.ts @@ -588,7 +588,7 @@ export interface ScaleOptions extends ScaleDefaults { * [1]: https://d3js.org/d3-time * [2]: https://d3js.org/d3-time-format */ - tickFormat?: string | ((t: any, i: number) => any) | null; + tickFormat?: string | ((d: any, i: number) => any) | null; /** * The rotation angle of axis tick labels in degrees clocksize; defaults to 0. diff --git a/test/output/tipFormatTitleIgnoreFormat.svg b/test/output/tipFormatTitleFormat.svg similarity index 96% rename from test/output/tipFormatTitleIgnoreFormat.svg rename to test/output/tipFormatTitleFormat.svg index f83f3730b5..bf3a364540 100644 --- a/test/output/tipFormatTitleIgnoreFormat.svg +++ b/test/output/tipFormatTitleFormat.svg @@ -22,7 +22,7 @@ - ​0 + ​0.01 \ No newline at end of file diff --git a/test/output/tipFormatTitleFormatFunction.svg b/test/output/tipFormatTitleFormatFunction.svg new file mode 100644 index 0000000000..e766740988 --- /dev/null +++ b/test/output/tipFormatTitleFormatFunction.svg @@ -0,0 +1,28 @@ + + + + + 0 + + + + + ​0.02 + + + \ No newline at end of file diff --git a/test/output/tipFormatTitleFormatShorthand.svg b/test/output/tipFormatTitleFormatShorthand.svg new file mode 100644 index 0000000000..24916ca8cf --- /dev/null +++ b/test/output/tipFormatTitleFormatShorthand.svg @@ -0,0 +1,28 @@ + + + + + 0 + + + + + ​0.03 + + + \ No newline at end of file diff --git a/test/plots/tip-format.ts b/test/plots/tip-format.ts index 51afdd1110..a268c1c56d 100644 --- a/test/plots/tip-format.ts +++ b/test/plots/tip-format.ts @@ -104,8 +104,16 @@ export async function tipFormatTitleExplicit() { return tip({length: 1}, {title: [new Date("2010-01-01")]}); } -export async function tipFormatTitleIgnoreFormat() { - return tip({length: 1}, {title: [0], format: {title: ".2f"}}); +export async function tipFormatTitleFormat() { + return tip({length: 1}, {title: [0.009], format: {title: ".2f"}}); +} + +export async function tipFormatTitleFormatFunction() { + return tip({length: 1}, {title: [0.019], format: (d) => d.toFixed(2)}); +} + +export async function tipFormatTitleFormatShorthand() { + return tip({length: 1}, {title: [0.029], format: ".2f"}); } export async function tipFormatTitlePrimitive() {