From 76666d2634176191eafda89637c76c63352f677e Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Mon, 3 Nov 2025 12:23:19 +0000 Subject: [PATCH 1/4] chore: move to Intl.NumberFormat for formatting numbers --- pages/common/formatters.ts | 31 +++++++++------------ src/core/__tests__/chart-core-axes.test.tsx | 6 ++-- src/core/formatters.tsx | 31 +++++++++------------ tsconfig.json | 2 +- 4 files changed, 31 insertions(+), 39 deletions(-) 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/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, From f31ef11e5a3831299f3a05c773a2ce08b9bbfc60 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Wed, 5 Nov 2025 13:11:59 +0000 Subject: [PATCH 2/4] test: update number formatting precision in cartesian chart test --- test/functional/cartesian-chart.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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]; From c80f8115ce598eb0d384fa9454a381b89af3ffb0 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Mon, 24 Nov 2025 13:58:30 +0000 Subject: [PATCH 3/4] feat: adds a `locale` prop --- pages/common/formatters.ts | 7 +++- src/core/__tests__/chart-core-axes.test.tsx | 6 +-- src/core/chart-api/chart-extra-context.tsx | 2 + src/core/chart-api/index.tsx | 20 +++++++-- src/core/chart-core.tsx | 25 ++++++++---- src/core/components/core-tooltip.tsx | 10 ++++- src/core/formatters.tsx | 45 ++++++++++++--------- src/core/interfaces.ts | 7 ++++ src/core/utils.ts | 10 ++--- 9 files changed, 88 insertions(+), 44 deletions(-) diff --git a/pages/common/formatters.ts b/pages/common/formatters.ts index 00dbb40f..178bcbeb 100644 --- a/pages/common/formatters.ts +++ b/pages/common/formatters.ts @@ -17,6 +17,9 @@ export function dateFormatter(value: null | number) { .join("\n"); } +/** + * @see https://www.unicode.org/cldr/cldr-aux/charts/29/verify/numbers/en.html + */ export function numberFormatter(value: number | null, locale: string = "en-US"): string { if (value === null) { return ""; @@ -32,7 +35,7 @@ export function numberFormatter(value: number | null, locale: string = "en-US"): if (absValue < 1e-9) { return new Intl.NumberFormat(locale, { notation: "scientific", - maximumFractionDigits: 9, + maximumFractionDigits: 2, }).format(value); } @@ -40,7 +43,7 @@ export function numberFormatter(value: number | null, locale: string = "en-US"): return new Intl.NumberFormat(locale, { notation: "compact", compactDisplay: "short", - maximumFractionDigits: 9, + maximumFractionDigits: 2, }).format(value); } diff --git a/src/core/__tests__/chart-core-axes.test.tsx b/src/core/__tests__/chart-core-axes.test.tsx index 237b6076..d9455e86 100644 --- a/src/core/__tests__/chart-core-axes.test.tsx +++ b/src/core/__tests__/chart-core-axes.test.tsx @@ -126,12 +126,11 @@ 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("1B"); + expect(formatter.call(mockAxisContext({ value: 1_000_000_000 }))).toBe("1G"); }); }); @@ -141,8 +140,7 @@ 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("0.003"); - expect(formatter.call(mockAxisContext({ value: 0.0000000003 }))).toBe("3E-10"); + expect(formatter.call(mockAxisContext({ value: 0.003 }))).toBe("3e-3"); }); }); diff --git a/src/core/chart-api/chart-extra-context.tsx b/src/core/chart-api/chart-extra-context.tsx index f1fd257d..f24c3639 100644 --- a/src/core/chart-api/chart-extra-context.tsx +++ b/src/core/chart-api/chart-extra-context.tsx @@ -32,6 +32,7 @@ export namespace ChartExtraContext { tooltipEnabled: boolean; keyboardNavigationEnabled: boolean; labels: ChartLabels; + locale: string; } export interface Handlers { @@ -63,6 +64,7 @@ export function createChartContext(): ChartExtraContext { tooltipEnabled: false, keyboardNavigationEnabled: false, labels: {}, + locale: "en-US", }, handlers: {}, state: {}, diff --git a/src/core/chart-api/index.tsx b/src/core/chart-api/index.tsx index f0056750..116eae80 100644 --- a/src/core/chart-api/index.tsx +++ b/src/core/chart-api/index.tsx @@ -253,19 +253,28 @@ export class ChartAPI { }, onFocusGroup: (group: Highcharts.Point[]) => { this.highlightActions(group, { isApiCall: false, overrideTooltipLock: true }); - this.chartExtraNavigation.announceElement(getGroupAccessibleDescription(group), false); + this.chartExtraNavigation.announceElement( + getGroupAccessibleDescription(group, this.context.settings.locale), + false, + ); }, onFocusPoint: (point: Highcharts.Point) => { const labels = this.context.settings.labels; this.highlightActions(point, { isApiCall: false, overrideTooltipLock: true }); - this.chartExtraNavigation.announceElement(getPointAccessibleDescription(point, labels), false); + this.chartExtraNavigation.announceElement( + getPointAccessibleDescription(point, labels, this.context.settings.locale), + false, + ); }, onBlur: () => this.clearChartHighlight({ isApiCall: false }), onActivateGroup: () => { const current = this.chartExtraTooltip.get(); if (current.group.length > 0) { this.chartExtraTooltip.pinTooltip(); - this.chartExtraNavigation.announceElement(getGroupAccessibleDescription(current.group), true); + this.chartExtraNavigation.announceElement( + getGroupAccessibleDescription(current.group, this.context.settings.locale), + true, + ); } }, onActivatePoint: () => { @@ -273,7 +282,10 @@ export class ChartAPI { if (current.point) { const labels = this.context.settings.labels; this.chartExtraTooltip.pinTooltip(); - this.chartExtraNavigation.announceElement(getPointAccessibleDescription(current.point, labels), true); + this.chartExtraNavigation.announceElement( + getPointAccessibleDescription(current.point, labels, this.context.settings.locale), + true, + ); } }, }; diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index ad4116b4..88e6dd19 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -6,8 +6,8 @@ import clsx from "clsx"; import type Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; -import { getIsRtl, useMergeRefs, useUniqueId } from "@cloudscape-design/component-toolkit/internal"; -import { isDevelopment } from "@cloudscape-design/component-toolkit/internal"; +import { getIsRtl, isDevelopment, useMergeRefs, useUniqueId } from "@cloudscape-design/component-toolkit/internal"; +import { normalizeLocale } from "@cloudscape-design/components/internal/utils/locale"; import Spinner from "@cloudscape-design/components/spinner"; import { getDataAttributes } from "../internal/base-component/get-data-attributes"; @@ -15,6 +15,7 @@ import { InternalBaseComponentProps } from "../internal/base-component/use-base- import * as Styles from "../internal/chart-styles"; import { castArray } from "../internal/utils/utils"; import { useChartAPI } from "./chart-api"; +import { ChartExtraContext } from "./chart-api/chart-extra-context"; import { ChartContainer } from "./chart-container"; import { ChartApplication } from "./components/core-application"; import { ChartFilters } from "./components/core-filters"; @@ -66,6 +67,9 @@ export function InternalCoreChart({ ...rest }: CoreChartProps & InternalBaseComponentProps) { const highcharts = rest.highcharts as null | typeof Highcharts; + + const locale = normalizeLocale("InternalCoreChart", rest.locale ?? null); + const labels = useChartI18n({ ariaLabel, ariaDescription, i18nStrings }); const context = { chartId: useUniqueId(), @@ -74,7 +78,8 @@ export function InternalCoreChart({ tooltipEnabled: tooltipOptions?.enabled !== false, keyboardNavigationEnabled: keyboardNavigation, labels, - }; + locale, + } satisfies ChartExtraContext.Settings; const handlers = { onHighlight, onClearHighlight, onVisibleItemsChange }; const state = { visibleItems }; const api = useChartAPI(context, handlers, state); @@ -203,7 +208,7 @@ export function InternalCoreChart({ keyboardNavigation: options.accessibility?.keyboardNavigation ?? { enabled: !keyboardNavigation }, point: { // Point description formatter is overridden to respect custom axes value formatters. - descriptionFormatter: (point) => getPointAccessibleDescription(point, labels), + descriptionFormatter: (point) => getPointAccessibleDescription(point, labels, locale), ...options.accessibility?.point, }, }, @@ -266,7 +271,7 @@ export function InternalCoreChart({ opposite: inverted && isRtl() ? !xAxisOptions.opposite : xAxisOptions.opposite, className: xAxisClassName(inverted, xAxisOptions.className), title: axisTitle(xAxisOptions.title ?? {}, !inverted || verticalAxisTitlePlacement === "side"), - labels: axisLabels(xAxisOptions.labels ?? {}), + labels: axisLabels(xAxisOptions.labels ?? {}, locale), })), yAxis: castArray(options.yAxis)?.map((yAxisOptions) => ({ ...Styles.yAxisOptions, @@ -276,7 +281,7 @@ export function InternalCoreChart({ opposite: !inverted && isRtl() ? !yAxisOptions.opposite : yAxisOptions.opposite, className: yAxisClassName(inverted, yAxisOptions.className), title: axisTitle(yAxisOptions.title ?? {}, inverted || verticalAxisTitlePlacement === "side"), - labels: axisLabels(yAxisOptions.labels ?? {}), + labels: axisLabels(yAxisOptions.labels ?? {}, locale), plotLines: yAxisPlotLines(yAxisOptions.plotLines, emphasizeBaseline), // We use reversed stack by default so that the order of points in the tooltip and series in the legend // correspond the order of stacks. @@ -333,6 +338,7 @@ export function InternalCoreChart({ {context.tooltipEnabled && ( (options: O): O { +function axisLabels( + options: O, + locale: string, +): O { return { style: Styles.axisLabelsCss, formatter: function () { - const formattedValue = getFormatter(this.axis)(this.value); + const formattedValue = getFormatter(locale, this.axis)(this.value); return formattedValue.replace(/\n/g, "
"); }, ...options, diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx index 25ce55eb..0263d9bc 100644 --- a/src/core/components/core-tooltip.tsx +++ b/src/core/components/core-tooltip.tsx @@ -44,10 +44,12 @@ export function ChartTooltip({ getTooltipContent: getTooltipContentOverrides, api, i18nStrings, + locale, }: CoreChartProps.TooltipOptions & { i18nStrings?: BaseI18nStrings; getTooltipContent?: CoreChartProps.GetTooltipContent; api: ChartAPI; + locale: string; }) { const [expandedSeries, setExpandedSeries] = useState({}); const tooltip = useSelector(api.tooltipStore, (s) => s); @@ -70,6 +72,7 @@ export function ChartTooltip({ group: tooltip.group, expandedSeries, setExpandedSeries, + locale, }); if (!content) { return null; @@ -117,6 +120,7 @@ function getTooltipContent( api: ChartAPI, props: CoreChartProps.GetTooltipContentProps & { renderers?: CoreChartProps.TooltipContentRenderer; + locale: string; } & ExpandedSeriesStateProps, ): null | RenderedTooltipContent { if (props.point && props.point.series.type === "pie") { @@ -136,8 +140,10 @@ function getTooltipContentCartesian( expandedSeries, renderers = {}, setExpandedSeries, + locale, }: CoreChartProps.GetTooltipContentProps & { renderers?: CoreChartProps.TooltipContentRenderer; + locale: string; } & ExpandedSeriesStateProps, ): RenderedTooltipContent { // The cartesian tooltip might or might not have a selected point, but it always has a non-empty group. @@ -148,7 +154,7 @@ function getTooltipContentCartesian( api.renderMarker(getSeriesMarkerType(series), getSeriesColor(series), true); const matchedItems = findTooltipSeriesItems(getChartSeries(chart.series), group); const detailItems: ChartSeriesDetailItem[] = matchedItems.map((item) => { - const valueFormatter = getFormatter(item.point.series.yAxis); + const valueFormatter = getFormatter(locale, item.point.series.yAxis); const itemY = isXThreshold(item.point.series) ? null : (item.point.y ?? null); const customContent = renderers.point ? renderers.point({ item }) : undefined; return { @@ -176,7 +182,7 @@ function getTooltipContentCartesian( }; }); // We only support cartesian charts with a single x axis. - const titleFormatter = getFormatter(chart.xAxis[0]); + const titleFormatter = getFormatter(locale, chart.xAxis[0]); const slotRenderProps: CoreChartProps.TooltipSlotProps = { x, items: matchedItems }; return { header: renderers.header?.(slotRenderProps) ?? titleFormatter(x), diff --git a/src/core/formatters.tsx b/src/core/formatters.tsx index 7e8cc7d2..6cd12b11 100644 --- a/src/core/formatters.tsx +++ b/src/core/formatters.tsx @@ -7,7 +7,7 @@ import { CoreChartProps } from "./interfaces"; // Takes value formatter from the axis options (InternalXAxisOptions.valueFormatter or InternalYAxisOptions.valueFormatter), // or provides a default formatter for numeric and datetime values. -export function getFormatter(axis?: Highcharts.Axis) { +export function getFormatter(locale: string, axis?: Highcharts.Axis) { return (value: unknown): string => { if (typeof value === "string") { return value; @@ -28,13 +28,13 @@ export function getFormatter(axis?: Highcharts.Axis) { if (axis.options.type === "datetime") { const extremes = axis.getExtremes(); const formatter = getDefaultDatetimeFormatter([extremes.dataMin, extremes.dataMax]); - return formatter(value); + return formatter(value, locale); } - return numberFormatter(value); + return numberFormatter(value, locale); }; } -function getDefaultDatetimeFormatter(extremes: [number, number]): (value: number) => string { +function getDefaultDatetimeFormatter(extremes: [number, number]): (value: number, locale: string) => string { const second = 1000; const minute = 60 * second; const hour = 60 * minute; @@ -59,50 +59,57 @@ function getDefaultDatetimeFormatter(extremes: [number, number]): (value: number return secondFormatter; } -function yearFormatter(value: number) { - return new Date(value).toLocaleDateString("en-US", { +function yearFormatter(value: number, locale: string) { + return new Date(value).toLocaleDateString(locale, { year: "numeric", }); } -function monthFormatter(value: number) { - return new Date(value).toLocaleDateString("en-US", { +function monthFormatter(value: number, locale: string) { + return new Date(value).toLocaleDateString(locale, { year: "numeric", month: "short", }); } -function dayFormatter(value: number) { - return new Date(value).toLocaleDateString("en-US", { +function dayFormatter(value: number, locale: string) { + return new Date(value).toLocaleDateString(locale, { month: "short", day: "numeric", }); } -function hourFormatter(value: number) { - return new Date(value).toLocaleDateString("en-US", { +function hourFormatter(value: number, locale: string) { + return new Date(value).toLocaleDateString(locale, { month: "short", day: "numeric", hour: "numeric", }); } -function minuteFormatter(value: number) { - return new Date(value).toLocaleDateString("en-US", { +function minuteFormatter(value: number, locale: string) { + return new Date(value).toLocaleDateString(locale, { hour: "numeric", minute: "numeric", }); } -function secondFormatter(value: number) { - return new Date(value).toLocaleDateString("en-US", { +function secondFormatter(value: number, locale: string) { + return new Date(value).toLocaleDateString(locale, { hour: "numeric", minute: "numeric", second: "numeric", }); } -function numberFormatter(value: number, locale: string = "en-US"): string { +/** + * @see https://www.unicode.org/cldr/cldr-aux/charts/29/verify/numbers/en.html + */ +export function numberFormatter(value: number | null, locale: string): string { + if (value === null) { + return ""; + } + const absValue = Math.abs(value); if (absValue === 0) { @@ -113,7 +120,7 @@ function numberFormatter(value: number, locale: string = "en-US"): string { if (absValue < 1e-9) { return new Intl.NumberFormat(locale, { notation: "scientific", - maximumFractionDigits: 9, + maximumFractionDigits: 2, }).format(value); } @@ -121,6 +128,6 @@ function numberFormatter(value: number, locale: string = "en-US"): string { return new Intl.NumberFormat(locale, { notation: "compact", compactDisplay: "short", - maximumFractionDigits: 9, + maximumFractionDigits: 2, }).format(value); } diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index e299768e..5b7c8bed 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -91,6 +91,12 @@ export interface BaseChartOptions { * * `additionalFilters` (optional, slot) - A slot for custom chart filters at the top of the chart. */ filter?: BaseFilterOptions; + + /** + * The locale to be used for formatting numbers, dates, etc. Defaults to the browser locale. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation + */ + locale?: string; } export interface BaseLegendOptions { @@ -308,6 +314,7 @@ export interface CoreChartProps | "ariaDescription" | "filter" | "noData" + | "locale" >, CoreCartesianOptions { /** diff --git a/src/core/utils.ts b/src/core/utils.ts index 87b2243b..77abfbd8 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -237,10 +237,10 @@ export function getChartAccessibleDescription(chart: Highcharts.Chart) { return chart.options.lang?.accessibility?.chartContainerLabel ?? ""; } -export function getPointAccessibleDescription(point: Highcharts.Point, labels: ChartLabels) { +export function getPointAccessibleDescription(point: Highcharts.Point, labels: ChartLabels, locale: string) { if (point.series.xAxis && point.series.yAxis) { - const formattedX = getFormatter(point.series.xAxis)(point.x); - const formattedY = getFormatter(point.series.yAxis)(point.y); + const formattedX = getFormatter(locale, point.series.xAxis)(point.x); + const formattedY = getFormatter(locale, point.series.yAxis)(point.y); return `${formattedX} ${formattedY}, ${point.series.name}`; } else if (point.series.type === "pie") { const segmentLabel = labels.chartSegmentLabel ? `${labels.chartSegmentLabel} ` : ""; @@ -250,9 +250,9 @@ export function getPointAccessibleDescription(point: Highcharts.Point, labels: C } } -export function getGroupAccessibleDescription(group: readonly Highcharts.Point[]) { +export function getGroupAccessibleDescription(group: readonly Highcharts.Point[], locale: string) { const firstPoint = group[0]; - return getFormatter(firstPoint.series.xAxis)(firstPoint.x); + return getFormatter(locale, firstPoint.series.xAxis)(firstPoint.x); } // The area-, line-, or scatter series markers are rendered as single graphic elements, From 498af9a4559c407d9bb214f475f5491129f143d9 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Wed, 7 Jan 2026 09:25:25 +0000 Subject: [PATCH 4/4] chore: use locale utility + tweak formatter --- .../__snapshots__/documenter.test.ts.snap | 25 +++++++++++++++++++ src/core/__tests__/chart-core-axes.test.tsx | 4 +-- src/core/chart-core.tsx | 2 +- src/core/formatters.tsx | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 65578760..6163cccb 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -232,6 +232,12 @@ This property corresponds to [chart.inverted](https://api.highcharts.com/highcha "optional": true, "type": "BaseLegendOptions", }, + { + "description": "The locale to be used for formatting numbers, dates, etc. Defaults to the browser locale.", + "name": "locale", + "optional": true, + "type": "string", + }, { "description": "Defines options to represent empty, no-match, loading, and error state of the chart, including: * \`statusType\` (optional, "finished" | "loading" | "error") - Specifies the current status of loading data. @@ -879,6 +885,12 @@ Supported Highcharts versions: 12.", "optional": true, "type": "BaseLegendOptions", }, + { + "description": "The locale to be used for formatting numbers, dates, etc. Defaults to the browser locale.", + "name": "locale", + "optional": true, + "type": "string", + }, { "description": "Defines options to represent empty, no-match, loading, and error state of the chart, including: * \`statusType\` (optional, "finished" | "loading" | "error") - Specifies the current status of loading data. @@ -1647,6 +1659,19 @@ Supported Highcharts versions: 12.", "type": "CoreChartProps.LegendOptions", "visualRefreshTag": undefined, }, + { + "analyticsTag": undefined, + "defaultValue": undefined, + "deprecatedTag": undefined, + "description": "The locale to be used for formatting numbers, dates, etc. Defaults to the browser locale.", + "i18nTag": undefined, + "inlineType": undefined, + "name": "locale", + "optional": true, + "systemTags": undefined, + "type": "string", + "visualRefreshTag": undefined, + }, { "analyticsTag": undefined, "defaultValue": undefined, diff --git a/src/core/__tests__/chart-core-axes.test.tsx b/src/core/__tests__/chart-core-axes.test.tsx index d9455e86..a643384d 100644 --- a/src/core/__tests__/chart-core-axes.test.tsx +++ b/src/core/__tests__/chart-core-axes.test.tsx @@ -130,7 +130,7 @@ describe("CoreChart: axes", () => { 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 +140,7 @@ 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("3E-3"); }); }); diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index 58b2cec2..7b63c771 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -7,7 +7,7 @@ import type Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; import { getIsRtl, isDevelopment, useMergeRefs, useUniqueId } from "@cloudscape-design/component-toolkit/internal"; -import { normalizeLocale } from "@cloudscape-design/components/internal/utils/locale"; +import { normalizeLocale } from "@cloudscape-design/component-toolkit/internal/locale"; import Spinner from "@cloudscape-design/components/spinner"; import { getDataAttributes } from "../internal/base-component/get-data-attributes"; diff --git a/src/core/formatters.tsx b/src/core/formatters.tsx index 6cd12b11..ca422ce0 100644 --- a/src/core/formatters.tsx +++ b/src/core/formatters.tsx @@ -117,7 +117,7 @@ export function numberFormatter(value: number | null, locale: string): string { } // Use scientific notation for very small numbers - if (absValue < 1e-9) { + if (absValue < 10e-3) { return new Intl.NumberFormat(locale, { notation: "scientific", maximumFractionDigits: 2,