Skip to content

Commit

Permalink
feat: support locale formatting using d3 locale (#6119)
Browse files Browse the repository at this point in the history
* Better validation

* review
  • Loading branch information
djbarnwal authored Nov 22, 2024
1 parent 6bb0f5b commit 34c0952
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 11 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@testing-library/svelte": "^4.0.0",
"@types/chroma-js": "^2.4.3",
"@types/codemirror": "^5.60.15",
"@types/d3-format": "^3.0.4",
"@types/d3-time-format": "^4.0.3",
"@types/dompurify": "^3.0.5",
"@types/luxon": "^3.4.2",
Expand Down
29 changes: 29 additions & 0 deletions web-common/src/features/metrics-views/editor/metrics-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,35 @@
"type": "string",
"description": "Controls the formatting of this measure using a d3-format string."
},
"format_d3_locale": {
"type": "object",
"description": "Defines a custom locale for d3-format.",
"properties": {
"decimal": { "type": "string", "description": "The decimal point (e.g., \".\")." },
"thousands": { "type": "string", "description": "The group separator (e.g., \",\")." },
"grouping": {
"type": "array",
"items": { "type": "integer" },
"description": "The array of group sizes (e.g., [3]), cycled as needed."
},
"currency": {
"type": "array",
"items": { "type": "string" },
"minItems": 2,
"maxItems": 2,
"description": "The currency prefix and suffix (e.g., [\"$\", \"\"])."
},
"numerals": {
"type": "array",
"items": { "type": "string" },
"description": "Optional array of ten strings to replace the numerals 0-9."
},
"percent": { "type": "string", "description": "Optional symbol to replace the percent suffix." },
"minus": { "type": "string", "description": "Optional minus sign (defaults to \"\")." },
"nan": { "type": "string", "description": "Optional value for not-a-number (defaults to \"NaN\")." }
},
"required": ["currency"]
},
"ignore": {
"type": "boolean",
"description": "If true, hides the measure from the dashboard."
Expand Down
46 changes: 35 additions & 11 deletions web-common/src/lib/number-formatting/format-measure-value.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import {
currencyHumanizer,
getLocaleFromConfig,
includesCurrencySymbol,
isValidD3Locale,
} from "@rilldata/web-common/lib/number-formatting/utils/d3-format-utils";
import type { MetricsViewSpecMeasureV2 } from "@rilldata/web-common/runtime-client";
import { format as d3format } from "d3-format";
import {
format as d3format,
formatLocale as d3FormatLocale,
type FormatLocaleDefinition,
} from "d3-format";
import {
FormatPreset,
NumberKind,
Expand Down Expand Up @@ -108,14 +118,6 @@ function humanizeDataType(
}
}

/**
* Parse the currency symbol from a d3 format string.
* For d3 the currency symbol is always "$" in the format string
*/
export function includesCurrencySymbol(formatString: string): boolean {
return formatString.includes("$");
}

/**
* This function is intended to provide a lossless
* humanized string representation of a number in cases
Expand Down Expand Up @@ -176,7 +178,18 @@ export function createMeasureValueFormatter<T extends null | undefined = never>(
// otherwise, use the humanize formatter.
if (measureSpec.formatD3 !== undefined && measureSpec.formatD3 !== "") {
try {
const d3formatter = d3format(measureSpec.formatD3);
let d3formatter: (n: number | { valueOf(): number }) => string;

const isValidLocale = isValidD3Locale(measureSpec.formatD3Locale);
if (isValidLocale) {
const locale = getLocaleFromConfig(
measureSpec.formatD3Locale as FormatLocaleDefinition,
);
d3formatter = d3FormatLocale(locale).format(measureSpec.formatD3);
} else {
d3formatter = d3format(measureSpec.formatD3);
}

const hasCurrencySymbol = includesCurrencySymbol(measureSpec.formatD3);
const hasPercentSymbol = measureSpec.formatD3.includes("%");
return (value: number | string | T) => {
Expand All @@ -185,6 +198,16 @@ export function createMeasureValueFormatter<T extends null | undefined = never>(
// For the Big Number and Tooltips, override the d3formatter
if (isBigNumber || isTooltip) {
if (hasCurrencySymbol) {
if (isValidLocale && measureSpec?.formatD3Locale?.currency) {
const currency = measureSpec.formatD3Locale.currency as [
string,
string,
];
return currencyHumanizer(
currency,
humanizer(value, FormatPreset.HUMANIZE),
);
}
return humanizer(value, FormatPreset.CURRENCY_USD);
} else if (hasPercentSymbol) {
return humanizer(value, FormatPreset.PERCENTAGE);
Expand All @@ -194,7 +217,8 @@ export function createMeasureValueFormatter<T extends null | undefined = never>(
}
return d3formatter(value);
};
} catch {
} catch (error) {
console.warn("Invalid d3 format:", error);
return (value: number | string | T) =>
typeof value === "number"
? humanizer(value, FormatPreset.HUMANIZE)
Expand Down
45 changes: 45 additions & 0 deletions web-common/src/lib/number-formatting/utils/d3-format-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { MetricsViewSpecMeasureV2FormatD3Locale } from "@rilldata/web-common/runtime-client";
import type { FormatLocaleDefinition } from "d3-format";

export function isValidD3Locale(
config: MetricsViewSpecMeasureV2FormatD3Locale | undefined,
): boolean {
if (!config) return false;
if (config.currency) {
// currency is an array of 2 strings
if (!Array.isArray(config.currency) || config.currency.length !== 2)
return false;
return true;
}
return false;
}

export function getLocaleFromConfig(
config: FormatLocaleDefinition,
): FormatLocaleDefinition {
const base: FormatLocaleDefinition = {
currency: ["$", ""],
thousands: ",",
grouping: [3],
decimal: ".",
};

return { ...base, ...config };
}

export function currencyHumanizer(
currency: [string, string],
humanizedValue: string,
): string {
const [prefix, suffix] = currency;
// Replace the "$" symbol with the appropriate currency prefix/suffix
return `${prefix}${humanizedValue.replace(/\$/g, "")}${suffix}`;
}

/**
* Parse the currency symbol from a d3 format string.
* For d3 the currency symbol is always "$" in the format string
*/
export function includesCurrencySymbol(formatString: string): boolean {
return formatString.includes("$");
}

1 comment on commit 34c0952

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.