diff --git a/pages/common/formatters.ts b/pages/common/formatters.ts index 94523fa6..00dbb40f 100644 --- a/pages/common/formatters.ts +++ b/pages/common/formatters.ts @@ -17,36 +17,31 @@ export function dateFormatter(value: null | number) { .join("\n"); } -export function numberFormatter(value: null | number) { +export function numberFormatter(value: number | null, locale: string = "en-US"): string { if (value === null) { return ""; } - const format = (num: number) => parseFloat(num.toFixed(2)).toString(); // trims unnecessary decimals - const absValue = Math.abs(value); if (absValue === 0) { return "0"; } - if (absValue < 0.01) { - return value.toExponential(0); - } - - if (absValue >= 1e9) { - return format(value / 1e9) + "G"; - } - - if (absValue >= 1e6) { - return format(value / 1e6) + "M"; - } - - if (absValue >= 1e3) { - return format(value / 1e3) + "K"; + // Use scientific notation for very small numbers + if (absValue < 1e-9) { + return new Intl.NumberFormat(locale, { + notation: "scientific", + maximumFractionDigits: 9, + }).format(value); } - return format(value); + // Use compact notation for normal range + return new Intl.NumberFormat(locale, { + notation: "compact", + compactDisplay: "short", + maximumFractionDigits: 9, + }).format(value); } export function moneyFormatter(value: null | number) { diff --git a/src/core/__tests__/chart-core-axes.test.tsx b/src/core/__tests__/chart-core-axes.test.tsx index d9455e86..237b6076 100644 --- a/src/core/__tests__/chart-core-axes.test.tsx +++ b/src/core/__tests__/chart-core-axes.test.tsx @@ -126,11 +126,12 @@ describe("CoreChart: axes", () => { test("uses default numeric axes formatters for integer values", () => { renderChart({ highcharts, options: { series, xAxis: { title: { text: "X" } }, yAxis: { title: { text: "Y" } } } }); getAxisOptionsFormatters().forEach((formatter) => { + // See https://www.unicode.org/cldr/cldr-aux/charts/29/verify/numbers/en.html expect(formatter.call(mockAxisContext({ value: 0 }))).toBe("0"); expect(formatter.call(mockAxisContext({ value: 1 }))).toBe("1"); expect(formatter.call(mockAxisContext({ value: 1_000 }))).toBe("1K"); expect(formatter.call(mockAxisContext({ value: 1_000_000 }))).toBe("1M"); - expect(formatter.call(mockAxisContext({ value: 1_000_000_000 }))).toBe("1G"); + expect(formatter.call(mockAxisContext({ value: 1_000_000_000 }))).toBe("1B"); }); }); @@ -140,7 +141,8 @@ describe("CoreChart: axes", () => { expect(formatter.call(mockAxisContext({ value: 2.0 }))).toBe("2"); expect(formatter.call(mockAxisContext({ value: 2.03 }))).toBe("2.03"); expect(formatter.call(mockAxisContext({ value: 0.03 }))).toBe("0.03"); - expect(formatter.call(mockAxisContext({ value: 0.003 }))).toBe("3e-3"); + expect(formatter.call(mockAxisContext({ value: 0.003 }))).toBe("0.003"); + expect(formatter.call(mockAxisContext({ value: 0.0000000003 }))).toBe("3E-10"); }); }); diff --git a/src/core/formatters.tsx b/src/core/formatters.tsx index 91656d2e..7e8cc7d2 100644 --- a/src/core/formatters.tsx +++ b/src/core/formatters.tsx @@ -102,30 +102,25 @@ function secondFormatter(value: number) { }); } -function numberFormatter(value: number): string { - const format = (num: number) => parseFloat(num.toFixed(2)).toString(); // trims unnecessary decimals - +function numberFormatter(value: number, locale: string = "en-US"): string { const absValue = Math.abs(value); if (absValue === 0) { return "0"; } - if (absValue < 0.01) { - return value.toExponential(0); - } - - if (absValue >= 1e9) { - return format(value / 1e9) + "G"; - } - - if (absValue >= 1e6) { - return format(value / 1e6) + "M"; - } - - if (absValue >= 1e3) { - return format(value / 1e3) + "K"; + // Use scientific notation for very small numbers + if (absValue < 1e-9) { + return new Intl.NumberFormat(locale, { + notation: "scientific", + maximumFractionDigits: 9, + }).format(value); } - return format(value); + // Use compact notation for normal range + return new Intl.NumberFormat(locale, { + notation: "compact", + compactDisplay: "short", + maximumFractionDigits: 9, + }).format(value); } diff --git a/test/functional/cartesian-chart.test.ts b/test/functional/cartesian-chart.test.ts index 4cffc106..b3067e9a 100644 --- a/test/functional/cartesian-chart.test.ts +++ b/test/functional/cartesian-chart.test.ts @@ -14,8 +14,8 @@ test( "pins chart tooltip after hovering chart point and then chart point group", setupTest("#/01-cartesian-chart/column-chart-test", async (page) => { const chart = w.findCartesianHighcharts('[data-testid="grouped-column-chart"]'); - const point = chart.find('[aria-label="Jul 2019 6.32K, Prev costs"]'); - const expectedTooltipContent = ["Jul 2019\nCosts\n8.77K\nPrev costs\n6.32K"]; + const point = chart.find('[aria-label="Jul 2019 6.322K, Prev costs"]'); + const expectedTooltipContent = ["Jul 2019\nCosts\n8.768K\nPrev costs\n6.322K"]; const pointBox = await page.getBoundingBox(point.toSelector()); const pointCenter = [pointBox.left + pointBox.width / 2, pointBox.top + pointBox.height / 2]; diff --git a/tsconfig.json b/tsconfig.json index ca919918..c0c3c358 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "target": "ES2019", "jsx": "react-jsx", "types": [], - "lib": ["es2019", "dom", "dom.iterable"], + "lib": ["es2020", "dom", "dom.iterable"], "module": "ESNext", "moduleResolution": "Node", "esModuleInterop": true,