diff --git a/docs/docs/developers/build/metrics-view/measures/measures-formatting.md b/docs/docs/developers/build/metrics-view/measures/measures-formatting.md index cd5b4bbd2fe0..655ff026d9eb 100644 --- a/docs/docs/developers/build/metrics-view/measures/measures-formatting.md +++ b/docs/docs/developers/build/metrics-view/measures/measures-formatting.md @@ -5,7 +5,7 @@ sidebar_label: "Measure Formatting" sidebar_position: 15 --- -When creating your measures in Rill, you have the option to pick from a preset of formats that we provide to you or use the [d3-format](https://d3js.org/d3-format) parameter to format your data in any way you like. While the big number in the explore dashboard won't apply all the decimals changes (it will add currency or percentage if that is the type), you will be able to see the changes in the dimension leaderboard and pivot tables. +When creating your measures in Rill, you have the option to pick from a preset of formats that we provide to you or use the [d3-format](https://d3js.org/d3-format) parameter to format your data in any way you like. An explicit `format_d3` is applied everywhere the measure is displayed — the big number, tooltips, the dimension leaderboard, TDD, and pivot tables — except chart axis labels, which stay abbreviated to keep them compact. ![Metrics Editor](/img/build/metrics-view/metrics-editor.png) @@ -35,7 +35,7 @@ For further customization of your measures, you can switch to the YAML view and, ## Examples -As explained in the introduction, you'll notice that in each of the screenshots the Big Number doesn't always follow the exact formatting, but will change based on percentage/currency formatting. This is as designed, as there is a fixed width that the number has to be displayed in. Instead, you'll see these values in the dimension leaderboard, TDD, and pivot tables. +Measures with an explicit `format_d3` follow that format exactly in the big number, tooltips, the dimension leaderboard, TDD, and pivot tables. Chart axis labels are the one exception: they remain abbreviated so they fit in the available space. Measures using a `format_preset` continue to show an abbreviated (humanized) big number, since presets do not specify an exact precision. If you have any questions, please review our [reference documentation.](/reference/project-files/metrics-views) diff --git a/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte b/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte index 4a882f6ef9d4..c0ea6eee2a59 100644 --- a/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte +++ b/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte @@ -212,7 +212,10 @@ $: cursorStyle = scrubController?.getCursorStyle(hoverState.screenX, xScale); // Formatters - $: measureFormatter = createMeasureValueFormatter(measure); + // Hover readouts use the tooltip context: it honors an explicit d3 format + // and shows more precision than the table default, so small-but-meaningful + // values (e.g. sub-cent costs) don't round away to ~$0.00. + $: measureFormatter = createMeasureValueFormatter(measure, "tooltip"); $: valueFormatter = (value: number | null): string => { if (value === null) return "no data"; return measureFormatter(value); diff --git a/web-common/src/lib/number-formatting/format-measure-value-locale.spec.ts b/web-common/src/lib/number-formatting/format-measure-value-locale.spec.ts index e716de819b5b..fb4c5f938904 100644 --- a/web-common/src/lib/number-formatting/format-measure-value-locale.spec.ts +++ b/web-common/src/lib/number-formatting/format-measure-value-locale.spec.ts @@ -21,9 +21,8 @@ describe("format-measure-value with d3_locale", () => { // Test with a number that should have thousand separators const result = formatter(123456); - // The big number formatter will abbreviate to "123k" but should preserve locale settings - // For big numbers, we humanize so we get "123k" with space separators - expect(result).toContain("123"); + // Big numbers honor the explicit d3 format, including its locale settings + expect(result).toBe("123 456"); }); it("should apply custom currency symbols from d3_locale for big numbers", () => { @@ -43,10 +42,8 @@ describe("format-measure-value with d3_locale", () => { const result = formatter(1234567); - // Should have the rupee symbol - expect(result).toContain("₹"); - // Should be humanized to something like "₹1.23M" or "₹1M" - expect(result).toMatch(/₹\d/); + // Should use the rupee symbol and the Indian grouping from the locale + expect(result).toBe("₹12,34,567"); }); it("should apply custom decimal separators in tooltips", () => { @@ -88,9 +85,8 @@ describe("format-measure-value with d3_locale", () => { const result = formatter(1234567); - // For big numbers with plain d3 format, it should be humanized and use custom separators - // Since there's no currency or percent, it should abbreviate the number - expect(result).toBe("1.23M"); + // Big numbers honor the plain d3 format with the custom separators + expect(result).toBe("1'234'567"); }); it("should handle different grouping patterns", () => { @@ -111,9 +107,8 @@ describe("format-measure-value with d3_locale", () => { // Test with 10 million const result = formatter(10000000); - // Should be humanized to something like "10M" - expect(result).toContain("10"); - expect(result).toMatch(/M/); + // Should use the Indian grouping pattern from the locale + expect(result).toBe("1,00,00,000"); }); it("should work with currency suffix instead of prefix", () => { @@ -133,9 +128,48 @@ describe("format-measure-value with d3_locale", () => { const result = formatter(5000); - // Should have Euro as suffix - expect(result).toContain("€"); - expect(result).toMatch(/\d+k?€/); + // Should have Euro as suffix and the space thousand separator + expect(result).toBe("5 000€"); + }); +}); + +describe("format-measure-value with an explicit formatD3", () => { + // Sub-cent measure, e.g. cost per click: an explicit d3 format must be + // honored in tooltips and big numbers, otherwise values like $0.0002 get + // humanized/rounded into invisibility (see the 2-decimal currency default). + const subCentMeasure: MetricsViewSpecMeasure = { + name: "cost_per_click", + expression: "SUM(cost) / SUM(clicks)", + formatD3: "$,.4f", + }; + + it("honors formatD3 precision in the tooltip context", () => { + const formatter = createMeasureValueFormatter(subCentMeasure, "tooltip"); + expect(formatter(0.0002)).toBe("$0.0002"); + expect(formatter(0.00023456)).toBe("$0.0002"); + expect(formatter(1234.5678)).toBe("$1,234.5678"); + }); + + it("honors formatD3 precision in the big-number context", () => { + const formatter = createMeasureValueFormatter(subCentMeasure, "big-number"); + expect(formatter(0.0002)).toBe("$0.0002"); + expect(formatter(1234567.8901)).toBe("$1,234,567.8901"); + }); + + it("keeps humanized values in the axis context so tick labels stay compact", () => { + const formatter = createMeasureValueFormatter(subCentMeasure, "axis"); + expect(formatter(0.0002)).toBe("$2e-4"); + expect(formatter(1234567.8901)).toBe("$1M"); + }); + + it("still humanizes preset-formatted measures in the big-number context", () => { + const presetMeasure: MetricsViewSpecMeasure = { + name: "total_cost", + expression: "SUM(cost)", + formatPreset: "currency_usd", + }; + const formatter = createMeasureValueFormatter(presetMeasure, "big-number"); + expect(formatter(1234567.8901)).toBe("$1.23M"); }); }); diff --git a/web-common/src/lib/number-formatting/format-measure-value.ts b/web-common/src/lib/number-formatting/format-measure-value.ts index 74c947240002..4e70b93ac981 100644 --- a/web-common/src/lib/number-formatting/format-measure-value.ts +++ b/web-common/src/lib/number-formatting/format-measure-value.ts @@ -201,6 +201,9 @@ const memoizedHumanizeDataTypeUnabridged = memoize(humanizeDataTypeUnabridged, { * This higher-order function takes a measure spec and returns * a function appropriate for formatting values from that measure. * + * When the measure has an explicit valid `formatD3`, it is honored in every + * context except "axis", where values are humanized so tick labels stay compact. + * * You may optionally add type paramaters to allow non-numeric null * undefined values to be passed through unmodified. * - `createMeasureValueFormatter(measureSpec)` will pass through null and undefined values unchanged @@ -218,7 +221,6 @@ export function createMeasureValueFormatter( const useUnabridged = type === "unabridged"; const isBigNumber = type === "big-number"; const isAxis = type === "axis"; - const isTooltip = type === "tooltip"; // Extract locale configuration from d3_locale const localeConfig: LocaleConfig | undefined = @@ -276,9 +278,12 @@ export function createMeasureValueFormatter( const coerced = coerceToNumber(value); if (typeof coerced !== "number") return value as T; - // For the Big Number, Axis and Tooltips, override the d3formatter - // with humanized values that respect the locale configuration - if (isBigNumber || isTooltip || isAxis) { + // For the Axis, override the d3formatter with humanized values + // that respect the locale configuration: + // tick labels must stay compact regardless of the measure's format. + // All other contexts (including tooltips and the Big Number) honor + // the explicit d3 format, so the precision it specifies is never lost. + if (isAxis) { if (hasCurrencySymbol) { if (isValidLocale && measureSpec?.formatD3Locale?.currency) { const currency = measureSpec.formatD3Locale.currency as [ diff --git a/web-local/tests/explores/number-formatting.spec.ts b/web-local/tests/explores/number-formatting.spec.ts index a422c8e58ecd..5a4a0a7caf16 100644 --- a/web-local/tests/explores/number-formatting.spec.ts +++ b/web-local/tests/explores/number-formatting.spec.ts @@ -83,7 +83,7 @@ dimensions: ["No Format", "301k", "300,576.84"], ["percentage", "30.1M%", "30.1M%"], ["interval_ms", "5 m", "5m 576ms"], - ["d3 fixed", "301k", "300,576.84"], + ["d3 fixed", "300576.840", "300576.840"], ]) { // check bignum with correct format exists/is visible await expect( diff --git a/web-local/tests/explores/timeseries.spec.ts b/web-local/tests/explores/timeseries.spec.ts index db7d3249e5a2..17883c1a6497 100644 --- a/web-local/tests/explores/timeseries.spec.ts +++ b/web-local/tests/explores/timeseries.spec.ts @@ -89,9 +89,11 @@ async function verifyChartTooltipData( const expectedValue = point.records[measureName]; if (expectedValue !== null && expectedValue !== undefined) { - const formatter = createMeasureValueFormatter({ - formatPreset: "humanize", - }); + // The chart formats hover readouts with the tooltip context + const formatter = createMeasureValueFormatter( + { formatPreset: "humanize" }, + "tooltip", + ); expect(valueText!.trim()).toBe(formatter(expectedValue)); }