Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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");
});
});

Expand Down
13 changes: 9 additions & 4 deletions web-common/src/lib/number-formatting/format-measure-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<null | undefined>(measureSpec)` will pass through null and undefined values unchanged
Expand All @@ -218,7 +221,6 @@ export function createMeasureValueFormatter<T extends null | undefined = never>(
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 =
Expand Down Expand Up @@ -276,9 +278,12 @@ export function createMeasureValueFormatter<T extends null | undefined = never>(
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 [
Expand Down
2 changes: 1 addition & 1 deletion web-local/tests/explores/number-formatting.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 5 additions & 3 deletions web-local/tests/explores/timeseries.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
Loading