Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[charts] Propagate the axis scale to the valueFormatter #16555

Merged
merged 5 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/data/charts/axis/FormatterD3.js
Original file line number Diff line number Diff line change
@@ -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 (
<LineChart
dataset={dataset}
xAxis={[
{
scaleType: 'log',
label: 'f (Hz)',
dataKey: 'frequency',
tickNumber: 20,
valueFormatter: (f, context) => {
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}
>
<ChartsReferenceLine x={f0} />
</LineChart>
);
}
76 changes: 76 additions & 0 deletions docs/data/charts/axis/FormatterD3.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<LineChart
dataset={dataset}
xAxis={[
{
scaleType: 'log',
label: 'f (Hz)',
dataKey: 'frequency',
tickNumber: 20,
valueFormatter: (f, context) => {
if (context.location === 'tick') {
const d3Text = (
context.scale as ScaleLogarithmic<number, number, never>
).tickFormat(
30,
'e',
)(f);
Comment on lines +38 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😭 I tried to fix this typing, but there are too many generics 😆

Technically it should be possible to use the correct type based on the scaleType: 'log'. But due to how our generics are setup and the usage/initialization/direct-override of the types everywhere, typescript can't automatically deduce the types


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<number, number, never>
).tickFormat(
30,
'f',
)(f);

return d3Text;
}
return f.toLocaleString();
},
},
]}
series={[{ dataKey: 'voltage' }]}
{...otherSetting}
>
<ChartsReferenceLine x={f0} />
</LineChart>
);
}
7 changes: 7 additions & 0 deletions docs/data/charts/axis/axis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion docs/pages/x/api/charts/axis-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"default": "'extremities'"
},
"valueFormatter": {
"type": { "description": "(value: V, context: AxisValueFormatterContext) =&gt; string" }
"type": {
"description": "&lt;TScaleName extends S&gt;(value: V, context: AxisValueFormatterContext&lt;TScaleName&gt;) =&gt; string"
}
},
"zoom": { "type": { "description": "boolean | ZoomOptions" }, "isProPlan": true }
}
Expand Down
8 changes: 5 additions & 3 deletions packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ function DefaultHeatmapTooltipContent(props: Pick<HeatmapTooltipProps, 'classes'
const [xIndex, yIndex] = value;

const formattedX =
xAxis.valueFormatter?.(xAxis.data![xIndex], { location: 'tooltip' }) ??
xAxis.data![xIndex].toLocaleString();
xAxis.valueFormatter?.(xAxis.data![xIndex], {
location: 'tooltip',
scale: xAxis.scale,
}) ?? xAxis.data![xIndex].toLocaleString();
const formattedY =
yAxis.valueFormatter?.(yAxis.data![yIndex], { location: 'tooltip' }) ??
yAxis.valueFormatter?.(yAxis.data![yIndex], { location: 'tooltip', scale: yAxis.scale }) ??
yAxis.data![yIndex].toLocaleString();
const formattedValue = series[seriesId].valueFormatter(value, {
dataIndex: identifier.dataIndex,
Expand Down
6 changes: 5 additions & 1 deletion packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
5 changes: 4 additions & 1 deletion packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 3 additions & 3 deletions packages/x-charts/src/hooks/useTicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand Down Expand Up @@ -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,
}));
Expand All @@ -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,
}));
Expand Down
37 changes: 27 additions & 10 deletions packages/x-charts/src/models/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<S extends ScaleName = ScaleName> =
| {
/**
* 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,
Expand Down Expand Up @@ -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?: <TScaleName extends S>(
value: V,
context: AxisValueFormatterContext<TScaleName>,
) => string;
/**
* If `true`, hide this value in the tooltip
*/
Expand Down