From d48c212760ba945cb230a03fda808e2985e958c3 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 21 Nov 2025 17:37:59 +0100 Subject: [PATCH 1/2] [charts] Support tooltip anchor position for radar --- .../ChartsTooltip/ChartsTooltipContainer.tsx | 2 +- .../src/RadarChart/seriesConfig/index.ts | 2 + .../seriesConfig/tooltipPosition.ts | 65 +++++++++++ .../useChartTooltip.selectors.ts | 110 ++++++++++++------ .../useChartPolarAxis.selectors.ts | 21 ++-- .../tooltipItemPositionGetter.types.ts | 6 +- 6 files changed, 160 insertions(+), 46 deletions(-) create mode 100644 packages/x-charts/src/RadarChart/seriesConfig/tooltipPosition.ts diff --git a/packages/x-charts/src/ChartsTooltip/ChartsTooltipContainer.tsx b/packages/x-charts/src/ChartsTooltip/ChartsTooltipContainer.tsx index ea22c37ad1cce..25feebf46924e 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsTooltipContainer.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsTooltipContainer.tsx @@ -131,7 +131,7 @@ function ChartsTooltipContainer(inProps: ChartsTooltipContainerProps) { trigger === 'item' && computedAnchor === 'node' ? selectorChartsTooltipItemPosition : () => null, - [position], + position, ); React.useEffect(() => { diff --git a/packages/x-charts/src/RadarChart/seriesConfig/index.ts b/packages/x-charts/src/RadarChart/seriesConfig/index.ts index d96cd29f5abe4..808b9b848e831 100644 --- a/packages/x-charts/src/RadarChart/seriesConfig/index.ts +++ b/packages/x-charts/src/RadarChart/seriesConfig/index.ts @@ -5,12 +5,14 @@ import { ChartSeriesTypeConfig } from '../../internals/plugins/models/seriesConf import legendGetter from './legend'; import tooltipGetter, { axisTooltipGetter } from './tooltip'; import getSeriesWithDefaultValues from './getSeriesWithDefaultValues'; +import tooltipItemPositionGetter from './tooltipPosition'; export const radarSeriesConfig: ChartSeriesTypeConfig<'radar'> = { colorProcessor: getColor, seriesProcessor: formatter, legendGetter, tooltipGetter, + tooltipItemPositionGetter, axisTooltipGetter, getSeriesWithDefaultValues, radiusExtremumGetter, diff --git a/packages/x-charts/src/RadarChart/seriesConfig/tooltipPosition.ts b/packages/x-charts/src/RadarChart/seriesConfig/tooltipPosition.ts new file mode 100644 index 0000000000000..a6f914c671179 --- /dev/null +++ b/packages/x-charts/src/RadarChart/seriesConfig/tooltipPosition.ts @@ -0,0 +1,65 @@ +import { D3OrdinalScale } from '../../models/axis'; +import { generatePolar2svg } from '../../internals/plugins/featurePlugins/useChartPolarAxis/coordinateTransformation'; +import { getDrawingAreaCenter } from '../../internals/plugins/featurePlugins/useChartPolarAxis'; +import type { TooltipItemPositionGetter } from '../../internals/plugins/models/seriesConfig/tooltipItemPositionGetter.types'; + +const tooltipItemPositionGetter: TooltipItemPositionGetter<'radar'> = (params) => { + const { series, identifier, axesConfig, drawingArea, placement } = params; + + if (!identifier) { + return null; + } + const itemSeries = series.radar?.series[identifier.seriesId]; + + if (itemSeries == null) { + return null; + } + + const { radiusAxes, rotationAxes } = axesConfig; + + if (radiusAxes === undefined || rotationAxes === undefined) { + return null; + } + + // Only one rotation axis is supported for radar charts + const rotationAxis = rotationAxes.axis[rotationAxes.axisIds[0]]; + + const metrics = (rotationAxis.scale.domain() as (string | number)[]) ?? []; + const angles = metrics.map((key) => (rotationAxis.scale as D3OrdinalScale)(key)!); + + const { cx, cy } = getDrawingAreaCenter(drawingArea); + const polar2svg = generatePolar2svg({ cx, cy }); + + const points = itemSeries.data.map((value, dataIndex) => { + const rId = radiusAxes.axisIds[dataIndex]; + const r = radiusAxes.axis[rId].scale(value)!; + + const angle = angles[dataIndex]; + return polar2svg(r, angle); + }); + + if (points.length === 0) { + return null; + } + + const [top, right, bottom, left] = points.reduce( + (acc, [x, y]) => { + return [Math.min(y, acc[0]), Math.max(x, acc[1]), Math.max(y, acc[2]), Math.min(x, acc[3])]; + }, + [Infinity, -Infinity, -Infinity, Infinity], + ); + + switch (placement) { + case 'right': + return { x: right, y: (top + bottom) / 2 }; + case 'bottom': + return { x: (left + right) / 2, y: bottom }; + case 'left': + return { x: left, y: (top + bottom) / 2 }; + case 'top': + default: + return { x: (left + right) / 2, y: top }; + } +}; + +export default tooltipItemPositionGetter; diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts index 6cc24eeb6cfab..10150c64fd381 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts @@ -24,11 +24,22 @@ import { selectorChartsLastInteraction, } from './useChartInteraction.selectors'; import { ChartSeriesConfig } from '../../models/seriesConfig/seriesConfig.types'; -import { AxisId, ChartsXAxisProps, ChartsYAxisProps } from '../../../../models/axis'; +import { + AxisId, + ChartsRadiusAxisProps, + ChartsRotationAxisProps, + ChartsXAxisProps, + ChartsYAxisProps, +} from '../../../../models/axis'; import { ComputeResult } from '../useChartCartesianAxis/computeAxisValue'; import { selectorChartDrawingArea } from '../../corePlugins/useChartDimensions/useChartDimensions.selectors'; import { ChartDrawingArea } from '../../../../hooks/useDrawingArea'; import { isCartesianSeries } from '../../../isCartesian'; +import { + selectorChartRadiusAxis, + selectorChartRotationAxis, +} from '../useChartPolarAxis/useChartPolarAxis.selectors'; +import { ComputeResult as ComputePolarResult } from '../useChartPolarAxis/computeAxisValue'; export const selectorChartsTooltipItem = createSelector( selectorChartsLastInteraction, @@ -47,60 +58,89 @@ export const selectorChartsTooltipItemIsDefined = createSelector( lastInteraction === 'keyboard' ? keyboardItemIsDefined : interactionItemIsDefined, ); -export const selectorChartsTooltipItemPosition = createSelector( +const selectorChartsTooltipAxisConfig = createSelector( selectorChartsTooltipItem, - selectorChartDrawingArea, - selectorChartSeriesConfig, selectorChartXAxis, selectorChartYAxis, + selectorChartRotationAxis, + selectorChartRadiusAxis, selectorChartSeriesProcessed, - (_, placement?: 'top' | 'bottom' | 'left' | 'right') => placement, - - function selectorChartsTooltipItemPosition( + function selectorChartsTooltipAxisConfig( identifier: ChartItemIdentifierWithData | null, - drawingArea: ChartDrawingArea, - seriesConfig: ChartSeriesConfig, { axis: xAxis, axisIds: xAxisIds }: ComputeResult, { axis: yAxis, axisIds: yAxisIds }: ComputeResult, + rotationAxes: ComputePolarResult, + radiusAxes: ComputePolarResult, series: ProcessedSeries, - placement: 'top' | 'bottom' | 'left' | 'right' = 'top', ) { if (!identifier) { - return null; + return {}; } const itemSeries = series[identifier.type as T]?.series[identifier.seriesId] as | ChartSeriesDefaultized | undefined; - if (itemSeries) { - const axesConfig: TooltipPositionGetterAxesConfig = {}; + if (!itemSeries) { + return {}; + } + const axesConfig: TooltipPositionGetterAxesConfig = { + rotationAxes, + radiusAxes, + }; - const xAxisId: AxisId | undefined = isCartesianSeries(itemSeries) - ? (itemSeries.xAxisId ?? xAxisIds[0]) - : undefined; - const yAxisId: AxisId | undefined = isCartesianSeries(itemSeries) - ? (itemSeries.yAxisId ?? yAxisIds[0]) - : undefined; + const xAxisId: AxisId | undefined = isCartesianSeries(itemSeries) + ? (itemSeries.xAxisId ?? xAxisIds[0]) + : undefined; + const yAxisId: AxisId | undefined = isCartesianSeries(itemSeries) + ? (itemSeries.yAxisId ?? yAxisIds[0]) + : undefined; - if (xAxisId !== undefined) { - axesConfig.x = xAxis[xAxisId]; - } - if (yAxisId !== undefined) { - axesConfig.y = yAxis[yAxisId]; - } + if (xAxisId !== undefined) { + axesConfig.x = xAxis[xAxisId]; + } + if (yAxisId !== undefined) { + axesConfig.y = yAxis[yAxisId]; + } - return ( - seriesConfig[itemSeries.type as T].tooltipItemPositionGetter?.({ - series, - drawingArea, - axesConfig, - identifier, - placement, - }) ?? null - ); + return axesConfig; + }, +); + +export const selectorChartsTooltipItemPosition = createSelector( + selectorChartsTooltipItem, + selectorChartDrawingArea, + selectorChartSeriesConfig, + selectorChartSeriesProcessed, + selectorChartsTooltipAxisConfig, + + function selectorChartsTooltipItemPosition( + identifier: ChartItemIdentifierWithData | null, + drawingArea: ChartDrawingArea, + seriesConfig: ChartSeriesConfig, + series: ProcessedSeries, + axesConfig: TooltipPositionGetterAxesConfig, + placement: 'top' | 'bottom' | 'left' | 'right' = 'top', + ) { + if (!identifier) { + return {}; } - return null; + const itemSeries = series[identifier.type as T]?.series[identifier.seriesId] as + | ChartSeriesDefaultized + | undefined; + + if (!itemSeries) { + return null; + } + return ( + seriesConfig[itemSeries.type as T].tooltipItemPositionGetter?.({ + series, + drawingArea, + axesConfig, + identifier, + placement, + }) ?? null + ); }, ); diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.selectors.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.selectors.ts index a36fe396680cb..76b73b0b5f3c1 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.selectors.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.selectors.ts @@ -1,4 +1,4 @@ -import { createSelector } from '@mui/x-internals/store'; +import { createSelector, createSelectorMemoized } from '@mui/x-internals/store'; import { selectorChartDrawingArea } from '../../corePlugins/useChartDimensions'; import { selectorChartSeriesConfig, @@ -7,6 +7,7 @@ import { import { UseChartPolarAxisSignature } from './useChartPolarAxis.types'; import { ChartState } from '../../models/chart'; import { computeAxisValue } from './computeAxisValue'; +import type { ChartDrawingArea } from '../../../../hooks/useDrawingArea'; export const selectorChartPolarAxisState = (state: ChartState<[], [UseChartPolarAxisSignature]>) => state.polarAxis; @@ -25,7 +26,7 @@ export const selectorChartRawRadiusAxis = createSelector( * The only interesting selectors that merge axis data and zoom if provided. */ -export const selectorChartRotationAxis = createSelector( +export const selectorChartRotationAxis = createSelectorMemoized( selectorChartRawRotationAxis, selectorChartDrawingArea, selectorChartSeriesProcessed, @@ -40,7 +41,7 @@ export const selectorChartRotationAxis = createSelector( }), ); -export const selectorChartRadiusAxis = createSelector( +export const selectorChartRadiusAxis = createSelectorMemoized( selectorChartRawRadiusAxis, selectorChartDrawingArea, selectorChartSeriesProcessed, @@ -55,7 +56,13 @@ export const selectorChartRadiusAxis = createSelector( }), ); -export const selectorChartPolarCenter = createSelector(selectorChartDrawingArea, (drawingArea) => ({ - cx: drawingArea.left + drawingArea.width / 2, - cy: drawingArea.top + drawingArea.height / 2, -})); +export function getDrawingAreaCenter(drawingArea: ChartDrawingArea) { + return { + cx: drawingArea.left + drawingArea.width / 2, + cy: drawingArea.top + drawingArea.height / 2, + }; +} +export const selectorChartPolarCenter = createSelector( + selectorChartDrawingArea, + getDrawingAreaCenter, +); diff --git a/packages/x-charts/src/internals/plugins/models/seriesConfig/tooltipItemPositionGetter.types.ts b/packages/x-charts/src/internals/plugins/models/seriesConfig/tooltipItemPositionGetter.types.ts index 2330fc9be8a8c..cbf399a1cacac 100644 --- a/packages/x-charts/src/internals/plugins/models/seriesConfig/tooltipItemPositionGetter.types.ts +++ b/packages/x-charts/src/internals/plugins/models/seriesConfig/tooltipItemPositionGetter.types.ts @@ -5,18 +5,18 @@ import type { import { ChartsRotationAxisProps, ChartsRadiusAxisProps, - PolarAxisDefaultized, ComputedXAxis, ComputedYAxis, } from '../../../../models/axis'; import { ChartDrawingArea } from '../../../../hooks/useDrawingArea'; import { ProcessedSeries } from '../../corePlugins/useChartSeries'; +import { ComputeResult } from '../../featurePlugins/useChartPolarAxis/computeAxisValue'; export interface TooltipPositionGetterAxesConfig { x?: ComputedXAxis; y?: ComputedYAxis; - rotation?: PolarAxisDefaultized; - radius?: PolarAxisDefaultized; + rotationAxes?: ComputeResult; + radiusAxes?: ComputeResult; } export type TooltipItemPositionGetter = (params: { From 3eab18812321cf887634f398c1f300344058108e Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 21 Nov 2025 17:51:05 +0100 Subject: [PATCH 2/2] fix --- .../useChartInteraction/useChartTooltip.selectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts index 10150c64fd381..dbcd3c3e6e53b 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts @@ -123,7 +123,7 @@ export const selectorChartsTooltipItemPosition = createSelector( placement: 'top' | 'bottom' | 'left' | 'right' = 'top', ) { if (!identifier) { - return {}; + return null; } const itemSeries = series[identifier.type as T]?.series[identifier.seriesId] as