Skip to content

Commit f3a2fe6

Browse files
committed
[charts] Add tooltip position
1 parent 6173633 commit f3a2fe6

11 files changed

Lines changed: 414 additions & 315 deletions

File tree

packages/x-charts-premium/src/Map/seriesConfig/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import tooltipGetter from './tooltip';
1212
import getSeriesWithDefaultValues from './getSeriesWithDefaultValues';
1313
import descriptionGetter from './descriptionGetter';
1414
import keyboardFocusHandler from './keyboardFocusHandler';
15+
import tooltipItemPositionGetter from './tooltipPosition';
1516

1617
export const mapShapeSeriesConfig: ChartSeriesTypeConfig<'mapShape'> = {
1718
seriesProcessor,
1819
colorProcessor: getColor,
1920
legendGetter,
2021
tooltipGetter,
22+
tooltipItemPositionGetter,
2123
getSeriesWithDefaultValues,
2224
keyboardFocusHandler,
2325
identifierSerializer: identifierSerializerSeriesIdDataIndex,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { geoPath } from '@mui/x-charts-vendor/d3-geo';
2+
import type { TooltipItemPositionGetter } from '@mui/x-charts/internals';
3+
4+
const tooltipItemPositionGetter: TooltipItemPositionGetter<'mapShape'> = (params) => {
5+
const { series, identifier, axesConfig, placement } = params;
6+
7+
if (!identifier || identifier.dataIndex === undefined) {
8+
return null;
9+
}
10+
const itemSeries = series.mapShape?.series[identifier.seriesId];
11+
12+
if (itemSeries == null) {
13+
return null;
14+
}
15+
16+
if (axesConfig.geo === undefined) {
17+
return null;
18+
}
19+
20+
const { projection, geoData, featureIndexesByName } = axesConfig.geo;
21+
22+
if (projection == null || geoData == null) {
23+
return null;
24+
}
25+
26+
const featureIndex = featureIndexesByName.get(itemSeries.data[identifier.dataIndex].name)?.[0];
27+
28+
if (featureIndex === undefined) {
29+
return null;
30+
}
31+
32+
const feature = geoData.features[featureIndex];
33+
34+
const path = geoPath(projection);
35+
36+
const [[x0, y0], [x1, y1]] = path.bounds(feature);
37+
38+
39+
switch (placement) {
40+
case 'right':
41+
return { x: x1, y: (y0 + y1) / 2 };
42+
case 'bottom':
43+
return { x: (x0 + x1) / 2, y: y1 };
44+
case 'left':
45+
return { x: x0, y: (y0 + y1) / 2 };
46+
case 'top':
47+
default:
48+
return { x: (x0 + x1) / 2, y: y0 };
49+
}
50+
};
51+
52+
export default tooltipItemPositionGetter;

packages/x-charts-premium/src/hooks/useGeoData.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { type ExtendedFeatureCollection } from '@mui/x-charts-vendor/d3-geo';
33
import { useStore } from '@mui/x-charts/internals';
44
import {
5-
selectorChartRawGeoData,
5+
selectorChartGeoData,
66
type UseGeoProjectionSignature,
77
} from '../internals/plugins/useGeoProjection';
88

@@ -12,5 +12,5 @@ import {
1212
*/
1313
export function useGeoData(): ExtendedFeatureCollection | null {
1414
const store = useStore<[UseGeoProjectionSignature]>();
15-
return store.use(selectorChartRawGeoData);
15+
return store.use(selectorChartGeoData);
1616
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export * from './useGeoProjection';
22
export * from './useGeoProjection.types';
3-
export * from './useGeoProjection.selectors';
3+
export * from './useGeoProjection.selectors';
Lines changed: 9 additions & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -1,237 +1,9 @@
1-
import { createSelector, createSelectorMemoized } from '@mui/x-internals/store';
2-
import { type ChartState, selectorChartDrawingArea } from '@mui/x-charts/internals';
3-
import {
4-
geoAlbers,
5-
geoAlbersUsa,
6-
geoAzimuthalEqualArea,
7-
geoAzimuthalEquidistant,
8-
geoConicConformal,
9-
geoConicEqualArea,
10-
geoConicEquidistant,
11-
geoEqualEarth,
12-
geoEquirectangular,
13-
geoGnomonic,
14-
geoMercator,
15-
geoNaturalEarth1,
16-
geoOrthographic,
17-
geoStereographic,
18-
geoTransverseMercator,
19-
geoPath,
20-
type ExtendedFeatureCollection,
21-
type GeoProjection,
22-
type GeoPath,
23-
type GeoConicProjection,
24-
} from '@mui/x-charts-vendor/d3-geo';
25-
import type {
26-
D3NamedProjection,
27-
GeoProjectionInput,
28-
UseGeoProjectionSignature,
29-
UseGeoProjectionState,
30-
} from './useGeoProjection.types';
31-
32-
const PROJECTION_FACTORIES: Record<D3NamedProjection, (() => GeoProjection) | undefined> = {
33-
// Azimuthal projections (https://d3js.org/d3-geo/azimuthal)
34-
azimuthalEqualArea: geoAzimuthalEqualArea,
35-
azimuthalEquidistant: geoAzimuthalEquidistant,
36-
gnomonic: geoGnomonic,
37-
orthographic: geoOrthographic,
38-
stereographic: geoStereographic,
39-
40-
// Conic projections (https://d3js.org/d3-geo/conic)
41-
conicConformal: geoConicConformal,
42-
conicEqualArea: geoConicEqualArea,
43-
conicEquidistant: geoConicEquidistant,
44-
albers: geoAlbers,
45-
albersUsa: geoAlbersUsa, // Special composition for the USA with an edge case for Alaska and Hawaii.
46-
47-
// Cylindrical projections (https://d3js.org/d3-geo/cylindrical)
48-
equirectangular: geoEquirectangular,
49-
mercator: geoMercator,
50-
transverseMercator: geoTransverseMercator,
51-
equalEarth: geoEqualEarth,
52-
naturalEarth1: geoNaturalEarth1,
53-
};
54-
55-
const isConicProjection = (projection: GeoProjection): projection is GeoConicProjection => {
56-
return 'parallels' in projection && typeof projection.parallels === 'function';
57-
};
58-
export const selectorChartGeoProjectionState = (
59-
state: ChartState<[], [UseGeoProjectionSignature]>,
60-
): UseGeoProjectionState['geoProjection'] | undefined => state.geoProjection;
61-
62-
export const selectorChartRawGeoData: (
63-
state: ChartState<[], [UseGeoProjectionSignature]>,
64-
) => ExtendedFeatureCollection | null = createSelector(
65-
selectorChartGeoProjectionState,
66-
(geoProjection) => geoProjection?.geoData ?? null,
67-
);
68-
69-
export const selectorChartRawProjection = createSelector(
70-
selectorChartGeoProjectionState,
71-
(geoProjection): GeoProjectionInput | null => geoProjection?.projection ?? null,
72-
);
73-
74-
export const selectorChartRawScale = createSelector(
75-
selectorChartGeoProjectionState,
76-
(geoProjection): number | null => geoProjection?.scale ?? null,
77-
);
78-
79-
const selectorChartRotate = createSelectorMemoized(
80-
selectorChartGeoProjectionState,
81-
(geoProjection): [number, number] | null => geoProjection?.rotate ?? null,
82-
);
83-
84-
const selectorChartTranslate = createSelectorMemoized(
85-
selectorChartGeoProjectionState,
86-
(geoProjection): [number, number] | null => geoProjection?.translate ?? null,
87-
);
88-
89-
const selectorChartParallels = createSelectorMemoized(
90-
selectorChartGeoProjectionState,
91-
selectorChartRotate,
92-
(geoProjection, rotate): [number, number] =>
93-
geoProjection?.parallels ?? (rotate ? [rotate[1] - 15, rotate[1] + 15] : [30, 30]),
94-
);
95-
/**
96-
* Map a feature's `properties.name` to its index in `geoData.features`,
97-
* for fast lookup by name when joining series rows to features.
98-
*
99-
* Features without a string `properties.name` are skipped; on duplicates,
100-
* the first occurrence wins.
101-
*/
102-
export const selectorChartGeoFeatureIndexesByName = createSelectorMemoized(
103-
selectorChartRawGeoData,
104-
(geoData): ReadonlyMap<string, number[]> => {
105-
const map = new Map<string, number[]>();
106-
if (!geoData) {
107-
return map;
108-
}
109-
geoData.features.forEach((feature, index) => {
110-
const name = feature.properties?.name;
111-
if (typeof name !== 'string') {
112-
return;
113-
}
114-
if (map.has(name)) {
115-
map.get(name)!.push(index);
116-
return;
117-
}
118-
map.set(name, [index]);
119-
});
120-
return map;
121-
},
122-
);
123-
124-
/**
125-
* Resolves the raw `projection` input into a ready-to-use `GeoProjection` instance
126-
* fitted to the chart's drawing area.
127-
*
128-
* - String inputs (e.g. `'mercator'`) are mapped to the matching d3-geo factory.
129-
* - `GeoProjection` instances are used as-is, then fitted.
130-
* - Returns `null` when no projection is registered or the name is unknown.
131-
*/
132-
export const selectorChartProjection = createSelectorMemoized(
133-
selectorChartRawProjection,
134-
selectorChartRawGeoData,
135-
selectorChartParallels,
136-
selectorChartRotate,
137-
selectorChartTranslate,
138-
selectorChartRawScale,
139-
selectorChartDrawingArea,
140-
(
141-
projectionInput,
142-
geoData,
143-
parallels,
144-
rotate,
145-
translate,
146-
scale,
147-
drawingArea,
148-
): GeoProjection | null => {
149-
if (!projectionInput) {
150-
return null;
151-
}
152-
let projection: GeoProjection;
153-
if (typeof projectionInput === 'string') {
154-
const factory = PROJECTION_FACTORIES[projectionInput];
155-
if (!factory) {
156-
if (process.env.NODE_ENV !== 'production') {
157-
console.error(
158-
`MUI X Charts: Unknown projection name '${projectionInput}'. ` +
159-
`Expected one of: ${Object.keys(PROJECTION_FACTORIES).join(', ')}.`,
160-
);
161-
}
162-
return null;
163-
}
164-
projection = factory();
165-
if (isConicProjection(projection)) {
166-
projection.parallels(parallels);
167-
}
168-
} else {
169-
projection = projectionInput;
170-
}
171-
if (geoData) {
172-
if (isConicProjection(projection)) {
173-
if (rotate) {
174-
projection.rotate?.(rotate);
175-
}
176-
177-
if (!scale) {
178-
const [[x0, y0], [x1, y1]] = geoPath(projection).bounds(geoData);
179-
180-
const currentScale = projection.scale();
181-
182-
const fitScale = Math.min(
183-
currentScale * (drawingArea.width / (x1 - x0)),
184-
currentScale * (drawingArea.height / (y1 - y0)),
185-
);
186-
projection.scale(fitScale);
187-
} else {
188-
projection.scale(scale);
189-
}
190-
191-
return projection;
192-
}
193-
194-
if (rotate) {
195-
projection.rotate?.(rotate);
196-
}
197-
198-
if (scale) {
199-
projection.scale(scale);
200-
projection.clipExtent?.([
201-
[drawingArea.left, drawingArea.top],
202-
[drawingArea.left + drawingArea.width, drawingArea.top + drawingArea.height],
203-
]);
204-
} else {
205-
projection.fitExtent?.(
206-
[
207-
[drawingArea.left, drawingArea.top],
208-
[drawingArea.left + drawingArea.width, drawingArea.top + drawingArea.height],
209-
],
210-
geoData,
211-
);
212-
}
213-
214-
projection.translate(
215-
translate ?? [
216-
drawingArea.left + drawingArea.width / 2,
217-
drawingArea.top + drawingArea.height / 2,
218-
],
219-
);
220-
}
221-
return projection;
222-
},
223-
);
224-
225-
/**
226-
* Resolves the raw `projection` input into a ready-to-use `GeoPath` instance
227-
* fitted to the chart's drawing area.
228-
*/
229-
export const selectorChartGeoPath = createSelectorMemoized(
230-
selectorChartProjection,
231-
(projection): GeoPath | null => {
232-
if (!projection) {
233-
return null;
234-
}
235-
return geoPath(projection);
236-
},
237-
);
1+
import { useGeoProjectionSelectors } from '@mui/x-charts/internals';
2+
3+
export const selectorChartGeoFeatureIndexesByName = useGeoProjectionSelectors.selectorChartGeoFeatureIndexesByName;
4+
export const selectorChartGeoProjectionState = useGeoProjectionSelectors.selectorChartGeoProjectionState;
5+
export const selectorChartGeoData = useGeoProjectionSelectors.selectorChartGeoData;
6+
export const selectorChartRawProjection = useGeoProjectionSelectors.selectorChartRawProjection;
7+
export const selectorChartRawScale = useGeoProjectionSelectors.selectorChartRawScale;
8+
export const selectorChartProjection = useGeoProjectionSelectors.selectorChartProjection;
9+
export const selectorChartGeoPath = useGeoProjectionSelectors.selectorChartGeoPath;

0 commit comments

Comments
 (0)