diff --git a/change/@fluentui-react-charts-function-tickformat.json b/change/@fluentui-react-charts-function-tickformat.json new file mode 100644 index 00000000000000..017a71395ee78d --- /dev/null +++ b/change/@fluentui-react-charts-function-tickformat.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: allow a function for the cartesian tickFormat so the numeric value axis can render a literal % or route through an app/i18n formatter (string behavior unchanged)", + "packageName": "@fluentui/react-charts", + "email": "michael@xerilium.com", + "dependentChangeType": "patch" +} diff --git a/packages/charts/react-charts/library/etc/react-charts.api.md b/packages/charts/react-charts/library/etc/react-charts.api.md index b841ad9d64148c..b0ac946dc7bfc0 100644 --- a/packages/charts/react-charts/library/etc/react-charts.api.md +++ b/packages/charts/react-charts/library/etc/react-charts.api.md @@ -237,7 +237,7 @@ export interface CartesianChartProps { strokeWidth?: number; styles?: CartesianChartStyles; svgProps?: React_2.SVGProps; - tickFormat?: string; + tickFormat?: string | ((value: number | Date) => string); tickPadding?: number; tickValues?: number[] | Date[] | string[] | undefined; timeFormatLocale?: TimeLocaleDefinition; @@ -1496,7 +1496,7 @@ export interface ModifiedCartesianChartProps extends CartesianChartProps { stringDatasetForYAxisDomain?: string[]; tickParams?: { tickValues?: number[] | Date[] | string[]; - tickFormat?: string; + tickFormat?: string | ((value: number | Date) => string); }; xAxisInnerPadding?: number; xAxisOuterPadding?: number; diff --git a/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.types.ts b/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.types.ts index 41188b05f0cd0d..f1ac49625f3a40 100644 --- a/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.types.ts +++ b/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.types.ts @@ -239,11 +239,16 @@ export interface CartesianChartProps { tickValues?: number[] | Date[] | string[] | undefined; /** - * the format for the data on x-axis. For date object this can be specified to your requirement. Eg: '%m/%d', '%d' - * Please look at https://github.com/d3/d3-time-format for all the formats supported for date axis - * Only applicable for date axis. For y-axis format use yAxisTickFormat prop. + * Format for x-axis tick labels. Accepts either a d3-format string or a `(value) => string` function. + * + * String: a d3-time-format specifier for date axes (e.g. `'%m/%d'`) or a d3-format specifier for + * numeric axes. See https://github.com/d3/d3-time-format and https://github.com/d3/d3-format. + * + * Function: formats the tick value directly — useful when a d3-format string is insufficient, + * e.g. appending a literal `%` to an already-0–100 axis (d3's `%` type multiplies by 100), + * or routing ticks through an app/i18n formatter. For y-axis format use `yAxisTickFormat`. */ - tickFormat?: string; + tickFormat?: string | ((value: number | Date) => string); /** * Width of line stroke @@ -644,7 +649,7 @@ export interface ModifiedCartesianChartProps extends CartesianChartProps { */ tickParams?: { tickValues?: number[] | Date[] | string[]; - tickFormat?: string; + tickFormat?: string | ((value: number | Date) => string); }; /** diff --git a/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts b/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts index 13a136b274bc90..a7d12096e46f64 100644 --- a/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts +++ b/packages/charts/react-charts/library/src/utilities/UtilityUnitTests.test.ts @@ -212,6 +212,16 @@ describe('createNumericXAxis', () => { utils.createNumericXAxis(xAxisParams, {}, utils.ChartTypes.HorizontalBarChartWithAxis); expect(xAxisParams.xAxisElement).toMatchSnapshot(); }); + + it('should format value-axis ticks with a function tickFormat (e.g. a literal %)', () => { + const xAxisParams = createXAxisParams(); + const result = utils.createNumericXAxis( + xAxisParams, + { tickFormat: (value: number | Date) => `${value as number}%` }, + utils.ChartTypes.HorizontalBarChartWithAxis, + ); + expect(result.tickLabels.every((label: string) => label.endsWith('%'))).toBe(true); + }); }); conditionalDescribe(isTimezoneSet(Timezone.UTC) && env === 'TEST')('createDateXAxis', () => { @@ -274,6 +284,14 @@ conditionalDescribe(isTimezoneSet(Timezone.UTC) && env === 'TEST')('createDateXA }); expect(xAxisParams.xAxisElement).toMatchSnapshot(); }); + + it('should format date-axis ticks with a function tickFormat', () => { + const xAxisParams = createXAxisParams({ domainNRangeValues }); + const result = utils.createDateXAxis(xAxisParams, { + tickFormat: (value: number | Date) => `Y${(value as Date).getFullYear()}`, + }); + expect(result.tickLabels.every((label: string) => label.startsWith('Y'))).toBe(true); + }); }); describe('createStringXAxis', () => { diff --git a/packages/charts/react-charts/library/src/utilities/utilities.ts b/packages/charts/react-charts/library/src/utilities/utilities.ts index 936d9b2398fcdc..10de56c4d732c2 100644 --- a/packages/charts/react-charts/library/src/utilities/utilities.ts +++ b/packages/charts/react-charts/library/src/utilities/utilities.ts @@ -188,7 +188,7 @@ export interface IXAxisParams extends AxisProps { } export interface ITickParams { tickValues?: Date[] | number[] | string[]; - tickFormat?: string; + tickFormat?: string | ((value: number | Date) => string); } export interface IYAxisParams extends AxisProps { @@ -288,7 +288,9 @@ export function createNumericXAxis( return tickText[_index]; } if (tickParams.tickFormat) { - return d3Format(tickParams.tickFormat)(domainValue); + return typeof tickParams.tickFormat === 'function' + ? tickParams.tickFormat(typeof domainValue === 'number' ? domainValue : domainValue.valueOf()) + : d3Format(tickParams.tickFormat)(domainValue); } const xAxisValue = typeof domainValue === 'number' ? domainValue : domainValue.valueOf(); return defaultFormat?.(xAxisValue) === '' ? '' : (formatToLocaleString(xAxisValue, culture) as string); @@ -498,6 +500,9 @@ export function createDateXAxis( if (tickParams.tickValues && tickText && typeof tickText[_index] !== 'undefined') { return tickText[_index]; } + if (typeof tickParams.tickFormat === 'function') { + return tickParams.tickFormat(domainValue); + } if (customDateTimeFormatter) { return customDateTimeFormatter(domainValue); } diff --git a/packages/charts/react-charts/stories/src/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisFunctionTickFormat.stories.tsx b/packages/charts/react-charts/stories/src/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisFunctionTickFormat.stories.tsx new file mode 100644 index 00000000000000..62ee554db2ff6a --- /dev/null +++ b/packages/charts/react-charts/stories/src/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisFunctionTickFormat.stories.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import type { JSXElement } from '@fluentui/react-components'; +import type { HorizontalBarChartWithAxisDataPoint } from '@fluentui/react-charts'; +import { HorizontalBarChartWithAxis, getColorFromToken, DataVizPalette } from '@fluentui/react-charts'; + +export const HorizontalBarWithAxisFunctionTickFormat = (): JSXElement => { + const points: HorizontalBarChartWithAxisDataPoint[] = [ + { x: 10, y: 'Q1', legend: 'Completion', color: getColorFromToken(DataVizPalette.color1) }, + { x: 45, y: 'Q2', legend: 'Completion', color: getColorFromToken(DataVizPalette.color2) }, + { x: 72, y: 'Q3', legend: 'Completion', color: getColorFromToken(DataVizPalette.color3) }, + { x: 95, y: 'Q4', legend: 'Completion', color: getColorFromToken(DataVizPalette.color4) }, + ]; + + return ( +
+ `${value as number}%`} + /> +
+ ); +}; + +HorizontalBarWithAxisFunctionTickFormat.parameters = { + docs: { + description: { + story: + 'Demonstrates passing a function to `tickFormat`. A d3-format `%` string would multiply the value by 100, ' + + 'double-scaling an already-0–100 axis. A function formats the tick directly, appending a literal `%`.', + }, + }, +}; diff --git a/packages/charts/react-charts/stories/src/HorizontalBarChartWithAxis/index.stories.tsx b/packages/charts/react-charts/stories/src/HorizontalBarChartWithAxis/index.stories.tsx index e353123c5fe970..adc0945642406a 100644 --- a/packages/charts/react-charts/stories/src/HorizontalBarChartWithAxis/index.stories.tsx +++ b/packages/charts/react-charts/stories/src/HorizontalBarChartWithAxis/index.stories.tsx @@ -8,6 +8,7 @@ export { HorizontalBarWithAxisStringAxisTooltip } from './HorizontalBarChartWith export { HorizontalBarWithAxisDynamic } from './HorizontalBarChartWithAxisDynamic.stories'; export { HorizontalBarWithAxisNegative } from './HorizontalBarChartWithAxisNegative.stories'; export { HorizontalBarWithAxisCategoryOrder } from './HorizontalBarChartWithAxisCategoryOrder.stories'; +export { HorizontalBarWithAxisFunctionTickFormat } from './HorizontalBarChartWithAxisFunctionTickFormat.stories'; export default { title: 'Charts/HorizontalBarChartWithAxis',