diff --git a/docs/data/charts/axis/FormatterD3.js b/docs/data/charts/axis/FormatterD3.js new file mode 100644 index 0000000000000..a4d827d4b59dd --- /dev/null +++ b/docs/data/charts/axis/FormatterD3.js @@ -0,0 +1,66 @@ +import * as React from 'react'; + +import { axisClasses } from '@mui/x-charts/ChartsAxis'; +import { LineChart } from '@mui/x-charts/LineChart'; +import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine'; + +const otherSetting = { + height: 300, + grid: { horizontal: true, vertical: true }, + sx: { + [`& .${axisClasses.left} .${axisClasses.label}`]: { + transform: 'translateX(-10px)', + }, + }, +}; + +// https://en.wikipedia.org/wiki/Low-pass_filter +const f0 = 440; +const frequencyResponse = (f) => 5 / Math.sqrt(1 + (f / f0) ** 2); + +const dataset = [ + 0.1, 0.5, 0.8, 1, 5, 8, 10, 50, 80, 100, 500, 800, 1_000, 5_000, 8_000, 10_000, + 50_000, 80_000, 100_000, 500_000, 800_000, 1_000_000, +].map((f) => ({ frequency: f, voltage: frequencyResponse(f) })); + +export default function FormatterD3() { + return ( + { + if (context.location === 'tick') { + const d3Text = context.scale.tickFormat(30, 'e')(f); + + return d3Text; + } + return `${f.toLocaleString()}Hz`; + }, + }, + ]} + yAxis={[ + { + scaleType: 'log', + label: 'Vo/Vi', + valueFormatter: (f, context) => { + if (context.location === 'tick') { + const d3Text = context.scale.tickFormat(30, 'f')(f); + + return d3Text; + } + return f.toLocaleString(); + }, + }, + ]} + series={[{ dataKey: 'voltage' }]} + {...otherSetting} + > + + + ); +} diff --git a/docs/data/charts/axis/FormatterD3.tsx b/docs/data/charts/axis/FormatterD3.tsx new file mode 100644 index 0000000000000..86bbb7f228da4 --- /dev/null +++ b/docs/data/charts/axis/FormatterD3.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { ScaleLogarithmic } from '@mui/x-charts-vendor/d3-scale'; +import { axisClasses } from '@mui/x-charts/ChartsAxis'; +import { LineChart } from '@mui/x-charts/LineChart'; +import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine'; + +const otherSetting = { + height: 300, + grid: { horizontal: true, vertical: true }, + sx: { + [`& .${axisClasses.left} .${axisClasses.label}`]: { + transform: 'translateX(-10px)', + }, + }, +}; + +// https://en.wikipedia.org/wiki/Low-pass_filter +const f0 = 440; +const frequencyResponse = (f: number) => 5 / Math.sqrt(1 + (f / f0) ** 2); + +const dataset = [ + 0.1, 0.5, 0.8, 1, 5, 8, 10, 50, 80, 100, 500, 800, 1_000, 5_000, 8_000, 10_000, + 50_000, 80_000, 100_000, 500_000, 800_000, 1_000_000, +].map((f) => ({ frequency: f, voltage: frequencyResponse(f) })); + +export default function FormatterD3() { + return ( + { + if (context.location === 'tick') { + const d3Text = ( + context.scale as ScaleLogarithmic + ).tickFormat( + 30, + 'e', + )(f); + + return d3Text; + } + return `${f.toLocaleString()}Hz`; + }, + }, + ]} + yAxis={[ + { + scaleType: 'log', + label: 'Vo/Vi', + valueFormatter: (f, context) => { + if (context.location === 'tick') { + const d3Text = ( + context.scale as ScaleLogarithmic + ).tickFormat( + 30, + 'f', + )(f); + + return d3Text; + } + return f.toLocaleString(); + }, + }, + ]} + series={[{ dataKey: 'voltage' }]} + {...otherSetting} + > + + + ); +} diff --git a/docs/data/charts/axis/axis.md b/docs/data/charts/axis/axis.md index ba9734f2c6d00..88193f0bf105f 100644 --- a/docs/data/charts/axis/axis.md +++ b/docs/data/charts/axis/axis.md @@ -70,6 +70,13 @@ To distinguish tick and tooltip, it uses the `context.location`. {{"demo": "FormatterDemoNoSnap.js"}} +#### Using the D3 formatter + +The context gives you access to the axis scale. +The D3 [tickFormat(tickNumber, scpecifier)](https://d3js.org/d3-scale/linear#tickFormat) method can be interesting to adapt ticks format based on the scale properties. + +{{"demo": "FormatterD3.js"}} + ### Axis sub domain By default, the axis domain is computed such that all your data is visible. diff --git a/docs/pages/x/api/charts/axis-config.json b/docs/pages/x/api/charts/axis-config.json index d5d5f7117ac4e..3a17227050988 100644 --- a/docs/pages/x/api/charts/axis-config.json +++ b/docs/pages/x/api/charts/axis-config.json @@ -34,7 +34,9 @@ "default": "'extremities'" }, "valueFormatter": { - "type": { "description": "(value: V, context: AxisValueFormatterContext) => string" } + "type": { + "description": "<TScaleName extends S>(value: V, context: AxisValueFormatterContext<TScaleName>) => string" + } }, "zoom": { "type": { "description": "boolean | ZoomOptions" }, "isProPlan": true } } diff --git a/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx b/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx index 14c4acce3fbc7..a4833edfd6f2e 100644 --- a/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx +++ b/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx @@ -60,10 +60,12 @@ function DefaultHeatmapTooltipContent(props: Pick string; @@ -179,6 +180,8 @@ const getText = ( } return label?.({ value, formattedValue }) ?? formattedValue; }; +const isZAxis = (axis: AxisDefaultized | ZAxisDefaultized): axis is ZAxisDefaultized => + (axis as AxisDefaultized).scale === undefined; const ContinuousColorLegend = consumeThemeProps( 'MuiContinuousColorLegend', @@ -223,7 +226,8 @@ const ContinuousColorLegend = consumeThemeProps( // Get texts to display - const valueFormatter = (axisItem as AxisDefaultized)?.valueFormatter; + const valueFormatter = isZAxis(axisItem) ? undefined : axisItem.valueFormatter; + const formattedMin = valueFormatter ? valueFormatter(minValue, { location: 'legend' }) : minValue.toLocaleString(); diff --git a/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx index c094ddf44173c..3418e943e9886 100644 --- a/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx @@ -183,8 +183,9 @@ const PiecewiseColorLegend = consumeThemeProps( return null; } const valueFormatter = (v: number | Date) => - (axisItem as AxisDefaultized).valueFormatter?.(v, { location: 'legend' }) ?? - v.toLocaleString(); + (axisItem as AxisDefaultized).valueFormatter?.(v, { + location: 'legend', + }) ?? v.toLocaleString(); const formattedLabels = colorMap.thresholds.map(valueFormatter); diff --git a/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx b/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx index 2a239a72afa4d..905bfb356987f 100644 --- a/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx +++ b/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx @@ -123,7 +123,10 @@ export function useAxisTooltip(): UseAxisTooltipReturnValue | null { ((v: string | number | Date) => usedAxis.scaleType === 'utc' ? utcFormatter(v) : v.toLocaleString()); - const axisFormattedValue = axisFormatter(axisValue, { location: 'tooltip' }); + const axisFormattedValue = axisFormatter(axisValue, { + location: 'tooltip', + scale: usedAxis.scale, + }); return { axisDirection: xAxisHasData ? 'x' : 'y', diff --git a/packages/x-charts/src/hooks/useTicks.ts b/packages/x-charts/src/hooks/useTicks.ts index 6e9466317cb6c..a175c78e35dfb 100644 --- a/packages/x-charts/src/hooks/useTicks.ts +++ b/packages/x-charts/src/hooks/useTicks.ts @@ -110,7 +110,7 @@ export function useTicks( return [ ...filteredDomain.map((value) => ({ value, - formattedValue: valueFormatter?.(value, { location: 'tick' }) ?? `${value}`, + formattedValue: valueFormatter?.(value, { location: 'tick', scale }) ?? `${value}`, offset: scale(value)! - (scale.step() - scale.bandwidth()) / 2 + @@ -141,7 +141,7 @@ export function useTicks( return filteredDomain.map((value) => ({ value, - formattedValue: valueFormatter?.(value, { location: 'tick' }) ?? `${value}`, + formattedValue: valueFormatter?.(value, { location: 'tick', scale }) ?? `${value}`, offset: scale(value)!, labelOffset: 0, })); @@ -158,7 +158,7 @@ export function useTicks( return ticks.map((value: any) => ({ value, formattedValue: - valueFormatter?.(value, { location: 'tick' }) ?? scale.tickFormat(tickNumber)(value), + valueFormatter?.(value, { location: 'tick', scale }) ?? scale.tickFormat(tickNumber)(value), offset: scale(value), labelOffset: 0, })); diff --git a/packages/x-charts/src/models/axis.ts b/packages/x-charts/src/models/axis.ts index 821c69f6e781f..d48275d04cd73 100644 --- a/packages/x-charts/src/models/axis.ts +++ b/packages/x-charts/src/models/axis.ts @@ -269,15 +269,29 @@ export interface AxisScaleComputedConfig { }; } -export type AxisValueFormatterContext = { - /** - * Location indicates where the value will be displayed. - * - `'tick'` The value is displayed on the axis ticks. - * - `'tooltip'` The value is displayed in the tooltip when hovering the chart. - * - `'legend'` The value is displayed in the legend when using color legend. - */ - location: 'tick' | 'tooltip' | 'legend'; -}; +export type AxisValueFormatterContext = + | { + /** + * Location indicates where the value will be displayed. + * - `'tick'` The value is displayed on the axis ticks. + * - `'tooltip'` The value is displayed in the tooltip when hovering the chart. + * - `'legend'` The value is displayed in the legend when using color legend. + */ + location: 'legend'; + } + | { + /** + * Location indicates where the value will be displayed. + * - `'tick'` The value is displayed on the axis ticks. + * - `'tooltip'` The value is displayed in the tooltip when hovering the chart. + * - `'legend'` The value is displayed in the legend when using color legend. + */ + location: 'tick' | 'tooltip'; + /** + * The d3-scale instance associated to the axis. + */ + scale: AxisScaleConfig[S]['scale']; + }; export type AxisConfig< S extends ScaleName = ScaleName, @@ -312,7 +326,10 @@ export type AxisConfig< * @param {AxisValueFormatterContext} context The rendering context of the value. * @returns {string} The string to display. */ - valueFormatter?: (value: V, context: AxisValueFormatterContext) => string; + valueFormatter?: ( + value: V, + context: AxisValueFormatterContext, + ) => string; /** * If `true`, hide this value in the tooltip */