Skip to content

Commit 3089cd4

Browse files
committed
[charts] Support tooltip anchor position for radar
1 parent 5e126f7 commit 3089cd4

File tree

5 files changed

+159
-45
lines changed

5 files changed

+159
-45
lines changed

packages/x-charts/src/RadarChart/seriesConfig/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { ChartSeriesTypeConfig } from '../../internals/plugins/models/seriesConf
55
import legendGetter from './legend';
66
import tooltipGetter, { axisTooltipGetter } from './tooltip';
77
import getSeriesWithDefaultValues from './getSeriesWithDefaultValues';
8+
import tooltipItemPositionGetter from './tooltipPosition';
89

910
export const radarSeriesConfig: ChartSeriesTypeConfig<'radar'> = {
1011
colorProcessor: getColor,
1112
seriesProcessor: formatter,
1213
legendGetter,
1314
tooltipGetter,
15+
tooltipItemPositionGetter,
1416
axisTooltipGetter,
1517
getSeriesWithDefaultValues,
1618
radiusExtremumGetter,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { D3OrdinalScale } from '../../models/axis';
2+
import { generatePolar2svg } from '../../internals/plugins/featurePlugins/useChartPolarAxis/coordinateTransformation';
3+
import { getDrawingAreaCenter } from '../../internals/plugins/featurePlugins/useChartPolarAxis';
4+
import type { TooltipItemPositionGetter } from '../../internals/plugins/models/seriesConfig/tooltipItemPositionGetter.types';
5+
6+
const tooltipItemPositionGetter: TooltipItemPositionGetter<'radar'> = (params) => {
7+
const { series, identifier, axesConfig, drawingArea, placement } = params;
8+
9+
if (!identifier) {
10+
return null;
11+
}
12+
const itemSeries = series.radar?.series[identifier.seriesId];
13+
14+
if (itemSeries == null) {
15+
return null;
16+
}
17+
18+
const { radiusAxes, rotationAxes } = axesConfig;
19+
20+
if (radiusAxes === undefined || rotationAxes === undefined) {
21+
return null;
22+
}
23+
24+
// Only one rotation axis is supported for radar charts
25+
const rotationAxis = rotationAxes.axis[rotationAxes.axisIds[0]];
26+
27+
const metrics = (rotationAxis.scale.domain() as (string | number)[]) ?? [];
28+
const angles = metrics.map((key) => (rotationAxis.scale as D3OrdinalScale)(key)!);
29+
30+
const { cx, cy } = getDrawingAreaCenter(drawingArea);
31+
const polar2svg = generatePolar2svg({ cx, cy });
32+
33+
const points = itemSeries.data.map((value, dataIndex) => {
34+
const rId = radiusAxes.axisIds[dataIndex];
35+
const r = radiusAxes.axis[rId].scale(value)!;
36+
37+
const angle = angles[dataIndex];
38+
return polar2svg(r, angle);
39+
});
40+
41+
if (points.length === 0) {
42+
return null;
43+
}
44+
45+
const [top, right, bottom, left] = points.reduce(
46+
(acc, [x, y]) => {
47+
return [Math.min(y, acc[0]), Math.max(x, acc[1]), Math.max(y, acc[2]), Math.min(x, acc[3])];
48+
},
49+
[Infinity, -Infinity, -Infinity, Infinity],
50+
);
51+
52+
switch (placement) {
53+
case 'right':
54+
return { x: right, y: (top + bottom) / 2 };
55+
case 'bottom':
56+
return { x: (left + right) / 2, y: bottom };
57+
case 'left':
58+
return { x: left, y: (top + bottom) / 2 };
59+
case 'top':
60+
default:
61+
return { x: (left + right) / 2, y: top };
62+
}
63+
};
64+
65+
export default tooltipItemPositionGetter;

packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartTooltip.selectors.ts

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,22 @@ import {
2424
selectorChartsLastInteraction,
2525
} from './useChartInteraction.selectors';
2626
import { ChartSeriesConfig } from '../../models/seriesConfig/seriesConfig.types';
27-
import { AxisId, ChartsXAxisProps, ChartsYAxisProps } from '../../../../models/axis';
27+
import {
28+
AxisId,
29+
ChartsRadiusAxisProps,
30+
ChartsRotationAxisProps,
31+
ChartsXAxisProps,
32+
ChartsYAxisProps,
33+
} from '../../../../models/axis';
2834
import { ComputeResult } from '../useChartCartesianAxis/computeAxisValue';
2935
import { selectorChartDrawingArea } from '../../corePlugins/useChartDimensions/useChartDimensions.selectors';
3036
import { ChartDrawingArea } from '../../../../hooks/useDrawingArea';
3137
import { isCartesianSeries } from '../../../isCartesian';
38+
import {
39+
selectorChartRadiusAxis,
40+
selectorChartRotationAxis,
41+
} from '../useChartPolarAxis/useChartPolarAxis.selectors';
42+
import { ComputeResult as ComputePolarResult } from '../useChartPolarAxis/computeAxisValue';
3243

3344
export const selectorChartsTooltipItem = createSelector(
3445
selectorChartsLastInteraction,
@@ -47,60 +58,89 @@ export const selectorChartsTooltipItemIsDefined = createSelector(
4758
lastInteraction === 'keyboard' ? keyboardItemIsDefined : interactionItemIsDefined,
4859
);
4960

50-
export const selectorChartsTooltipItemPosition = createSelector(
61+
const selectorChartsTooltipAxisConfig = createSelector(
5162
selectorChartsTooltipItem,
52-
selectorChartDrawingArea,
53-
selectorChartSeriesConfig,
5463
selectorChartXAxis,
5564
selectorChartYAxis,
65+
selectorChartRotationAxis,
66+
selectorChartRadiusAxis,
5667
selectorChartSeriesProcessed,
57-
(_, placement?: 'top' | 'bottom' | 'left' | 'right') => placement,
58-
59-
function selectorChartsTooltipItemPosition<T extends ChartSeriesType>(
68+
function selectorChartsTooltipAxisConfig<T extends ChartSeriesType>(
6069
identifier: ChartItemIdentifierWithData<T> | null,
61-
drawingArea: ChartDrawingArea,
62-
seriesConfig: ChartSeriesConfig<T>,
6370
{ axis: xAxis, axisIds: xAxisIds }: ComputeResult<ChartsXAxisProps>,
6471
{ axis: yAxis, axisIds: yAxisIds }: ComputeResult<ChartsYAxisProps>,
72+
rotationAxes: ComputePolarResult<ChartsRotationAxisProps>,
73+
radiusAxes: ComputePolarResult<ChartsRadiusAxisProps>,
6574
series: ProcessedSeries<T>,
66-
placement: 'top' | 'bottom' | 'left' | 'right' = 'top',
6775
) {
6876
if (!identifier) {
69-
return null;
77+
return {};
7078
}
7179

7280
const itemSeries = series[identifier.type as T]?.series[identifier.seriesId] as
7381
| ChartSeriesDefaultized<T>
7482
| undefined;
7583

76-
if (itemSeries) {
77-
const axesConfig: TooltipPositionGetterAxesConfig = {};
84+
if (!itemSeries) {
85+
return {};
86+
}
87+
const axesConfig: TooltipPositionGetterAxesConfig = {
88+
rotationAxes,
89+
radiusAxes,
90+
};
7891

79-
const xAxisId: AxisId | undefined = isCartesianSeries(itemSeries)
80-
? (itemSeries.xAxisId ?? xAxisIds[0])
81-
: undefined;
82-
const yAxisId: AxisId | undefined = isCartesianSeries(itemSeries)
83-
? (itemSeries.yAxisId ?? yAxisIds[0])
84-
: undefined;
92+
const xAxisId: AxisId | undefined = isCartesianSeries(itemSeries)
93+
? (itemSeries.xAxisId ?? xAxisIds[0])
94+
: undefined;
95+
const yAxisId: AxisId | undefined = isCartesianSeries(itemSeries)
96+
? (itemSeries.yAxisId ?? yAxisIds[0])
97+
: undefined;
8598

86-
if (xAxisId !== undefined) {
87-
axesConfig.x = xAxis[xAxisId];
88-
}
89-
if (yAxisId !== undefined) {
90-
axesConfig.y = yAxis[yAxisId];
91-
}
99+
if (xAxisId !== undefined) {
100+
axesConfig.x = xAxis[xAxisId];
101+
}
102+
if (yAxisId !== undefined) {
103+
axesConfig.y = yAxis[yAxisId];
104+
}
92105

93-
return (
94-
seriesConfig[itemSeries.type as T].tooltipItemPositionGetter?.({
95-
series,
96-
drawingArea,
97-
axesConfig,
98-
identifier,
99-
placement,
100-
}) ?? null
101-
);
106+
return axesConfig;
107+
},
108+
);
109+
110+
export const selectorChartsTooltipItemPosition = createSelector(
111+
selectorChartsTooltipItem,
112+
selectorChartDrawingArea,
113+
selectorChartSeriesConfig,
114+
selectorChartSeriesProcessed,
115+
selectorChartsTooltipAxisConfig,
116+
117+
function selectorChartsTooltipItemPosition<T extends ChartSeriesType>(
118+
identifier: ChartItemIdentifierWithData<T> | null,
119+
drawingArea: ChartDrawingArea,
120+
seriesConfig: ChartSeriesConfig<T>,
121+
series: ProcessedSeries<T>,
122+
axesConfig: TooltipPositionGetterAxesConfig,
123+
placement: 'top' | 'bottom' | 'left' | 'right' = 'top',
124+
) {
125+
if (!identifier) {
126+
return {};
102127
}
103128

104-
return null;
129+
const itemSeries = series[identifier.type as T]?.series[identifier.seriesId] as
130+
| ChartSeriesDefaultized<T>
131+
| undefined;
132+
133+
if (!itemSeries) {
134+
return null;
135+
}
136+
return (
137+
seriesConfig[itemSeries.type as T].tooltipItemPositionGetter?.({
138+
series,
139+
drawingArea,
140+
axesConfig,
141+
identifier,
142+
placement,
143+
}) ?? null
144+
);
105145
},
106146
);

packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.selectors.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSelector } from '@mui/x-internals/store';
1+
import { createSelector, createSelectorMemoized } from '@mui/x-internals/store';
22
import { selectorChartDrawingArea } from '../../corePlugins/useChartDimensions';
33
import {
44
selectorChartSeriesConfig,
@@ -7,6 +7,7 @@ import {
77
import { UseChartPolarAxisSignature } from './useChartPolarAxis.types';
88
import { ChartState } from '../../models/chart';
99
import { computeAxisValue } from './computeAxisValue';
10+
import type { ChartDrawingArea } from '../../../../hooks/useDrawingArea';
1011

1112
export const selectorChartPolarAxisState = (state: ChartState<[], [UseChartPolarAxisSignature]>) =>
1213
state.polarAxis;
@@ -25,7 +26,7 @@ export const selectorChartRawRadiusAxis = createSelector(
2526
* The only interesting selectors that merge axis data and zoom if provided.
2627
*/
2728

28-
export const selectorChartRotationAxis = createSelector(
29+
export const selectorChartRotationAxis = createSelectorMemoized(
2930
selectorChartRawRotationAxis,
3031
selectorChartDrawingArea,
3132
selectorChartSeriesProcessed,
@@ -40,7 +41,7 @@ export const selectorChartRotationAxis = createSelector(
4041
}),
4142
);
4243

43-
export const selectorChartRadiusAxis = createSelector(
44+
export const selectorChartRadiusAxis = createSelectorMemoized(
4445
selectorChartRawRadiusAxis,
4546
selectorChartDrawingArea,
4647
selectorChartSeriesProcessed,
@@ -55,7 +56,13 @@ export const selectorChartRadiusAxis = createSelector(
5556
}),
5657
);
5758

58-
export const selectorChartPolarCenter = createSelector(selectorChartDrawingArea, (drawingArea) => ({
59-
cx: drawingArea.left + drawingArea.width / 2,
60-
cy: drawingArea.top + drawingArea.height / 2,
61-
}));
59+
export function getDrawingAreaCenter(drawingArea: ChartDrawingArea) {
60+
return {
61+
cx: drawingArea.left + drawingArea.width / 2,
62+
cy: drawingArea.top + drawingArea.height / 2,
63+
};
64+
}
65+
export const selectorChartPolarCenter = createSelector(
66+
selectorChartDrawingArea,
67+
getDrawingAreaCenter,
68+
);

packages/x-charts/src/internals/plugins/models/seriesConfig/tooltipItemPositionGetter.types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ import type {
55
import {
66
ChartsRotationAxisProps,
77
ChartsRadiusAxisProps,
8-
PolarAxisDefaultized,
98
ComputedXAxis,
109
ComputedYAxis,
1110
} from '../../../../models/axis';
1211
import { ChartDrawingArea } from '../../../../hooks/useDrawingArea';
1312
import { ProcessedSeries } from '../../corePlugins/useChartSeries';
13+
import { ComputeResult } from '../../featurePlugins/useChartPolarAxis/computeAxisValue';
1414

1515
export interface TooltipPositionGetterAxesConfig {
1616
x?: ComputedXAxis;
1717
y?: ComputedYAxis;
18-
rotation?: PolarAxisDefaultized<any, any, ChartsRotationAxisProps>;
19-
radius?: PolarAxisDefaultized<any, any, ChartsRadiusAxisProps>;
18+
rotationAxes?: ComputeResult<ChartsRotationAxisProps>;
19+
radiusAxes?: ComputeResult<ChartsRadiusAxisProps>;
2020
}
2121

2222
export type TooltipItemPositionGetter<TSeriesType extends ChartSeriesType> = (params: {

0 commit comments

Comments
 (0)