From e50014252b8759d3884b4997bec4db61043248b8 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Feb 2025 15:21:31 +0100 Subject: [PATCH 1/5] Clean TS --- .../x-charts/src/ChartsLegend/ContinuousColorLegend.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx b/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx index 1bb3155185a57..d36832ef13237 100644 --- a/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx @@ -17,6 +17,7 @@ import { useUtilityClasses, } from './continuousColorLegendClasses'; import { useChartGradientIdObjectBoundBuilder } from '../hooks/useChartGradientId'; +import { ZAxisDefaultized } from '../models/z-axis'; type LabelFormatter = (params: { value: number | Date; formattedValue: string }) => 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(); From bd842c78d55bd08cea91e93c45b7b63f3176e32d Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Feb 2025 15:21:37 +0100 Subject: [PATCH 2/5] propagate scale --- .../src/Heatmap/HeatmapTooltip.tsx | 8 +++-- .../src/ChartsLegend/PiecewiseColorLegend.tsx | 5 +-- .../src/ChartsTooltip/useAxisTooltip.tsx | 5 ++- packages/x-charts/src/hooks/useTicks.ts | 6 ++-- packages/x-charts/src/models/axis.ts | 32 +++++++++++++------ 5 files changed, 38 insertions(+), 18 deletions(-) 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 - (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..ade4d8cdb7d8c 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'] | AxisScaleComputedConfig[S]['colorScale']; + }; export type AxisConfig< S extends ScaleName = ScaleName, From 1ebc706344f5b0a6b0c53300e27adeb0dc43ea15 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Feb 2025 16:20:15 +0100 Subject: [PATCH 3/5] remove some types --- packages/x-charts/src/models/axis.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/x-charts/src/models/axis.ts b/packages/x-charts/src/models/axis.ts index ade4d8cdb7d8c..d48275d04cd73 100644 --- a/packages/x-charts/src/models/axis.ts +++ b/packages/x-charts/src/models/axis.ts @@ -290,7 +290,7 @@ export type AxisValueFormatterContext = /** * The d3-scale instance associated to the axis. */ - scale: AxisScaleConfig[S]['scale'] | AxisScaleComputedConfig[S]['colorScale']; + scale: AxisScaleConfig[S]['scale']; }; export type AxisConfig< @@ -326,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 */ From ca14473db85814b242c8aa27d95a8dc35d67034a Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 12 Feb 2025 16:20:24 +0100 Subject: [PATCH 4/5] Add demo --- docs/data/charts/axis/FormatterD3.js | 66 +++++++++++++++++++++++ docs/data/charts/axis/FormatterD3.tsx | 76 +++++++++++++++++++++++++++ docs/data/charts/axis/axis.md | 7 +++ 3 files changed, 149 insertions(+) create mode 100644 docs/data/charts/axis/FormatterD3.js create mode 100644 docs/data/charts/axis/FormatterD3.tsx 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. From de8c1d81f90ae4ff6400fcfee1ccc2834c10d0d5 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 13 Feb 2025 09:35:23 +0100 Subject: [PATCH 5/5] docs:api --- docs/pages/x/api/charts/axis-config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 } }