Skip to content

Commit eee89a2

Browse files
committed
initial-translation
1 parent ff8b86e commit eee89a2

6 files changed

Lines changed: 147 additions & 95 deletions

File tree

packages/x-charts-premium/src/internals/plugins/useGeoProjection/useGeoProjection.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import type { GeoProjection } from '@mui/x-charts-vendor/d3-geo';
2222
import type { UseGeoProjectionSignature } from './useGeoProjection.types';
2323

24-
const PROJECTION_FACTORIES: Record<
24+
export const PROJECTION_FACTORIES: Record<
2525
useGeoProjectionTypes.D3NamedProjection,
2626
(() => GeoProjection) | undefined
2727
> = {
@@ -48,7 +48,7 @@ const PROJECTION_FACTORIES: Record<
4848
};
4949

5050
export const useGeoProjection: ChartPlugin<UseGeoProjectionSignature> = ({ params, store }) => {
51-
const { geoData, geoFeatureKey, projection } = params;
51+
const { geoData, geoFeatureKey, projection, parallels } = params;
5252

5353
const isFirstRender = React.useRef(true);
5454
React.useEffect(() => {
@@ -62,8 +62,9 @@ export const useGeoProjection: ChartPlugin<UseGeoProjectionSignature> = ({ param
6262
geoData: geoData ?? null,
6363
geoFeatureKey: geoFeatureKey ?? 'name',
6464
projection: projection ?? null,
65+
parallels: parallels ?? null,
6566
});
66-
}, [geoData, geoFeatureKey, projection, store]);
67+
}, [geoData, geoFeatureKey, projection, parallels, store]);
6768

6869
return {};
6970
};
@@ -72,6 +73,7 @@ useGeoProjection.params = {
7273
geoData: true,
7374
geoFeatureKey: true,
7475
projection: true,
76+
parallels: true,
7577
};
7678

7779
useGeoProjection.getDefaultizedParams = ({ params }) => ({ ...params });
@@ -81,6 +83,7 @@ useGeoProjection.getInitialState = (params) => ({
8183
geoData: params.geoData ?? null,
8284
geoFeatureKey: params.geoFeatureKey ?? 'name',
8385
projection: params.projection ?? null,
86+
parallels: params.parallels ?? null,
8487
factories: PROJECTION_FACTORIES,
8588
},
8689
});

packages/x-charts-premium/src/internals/plugins/useGeoProjectionZoom/useGeoProjectionZoom.ts

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
'use client';
22
import * as React from 'react';
33
import { useEffectAfterFirstRender } from '@mui/x-internals/useEffectAfterFirstRender';
4-
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
5-
import { selectorChartDrawingArea } from '@mui/x-charts/internals';
4+
import { getDefaultTranslation } from '@mui/x-charts/internals';
65
import type { ChartPlugin } from '@mui/x-charts/internals';
76
import {
87
useDragGesture,
@@ -14,10 +13,10 @@ import type { GeoProjection } from '@mui/x-charts-vendor/d3-geo';
1413
import {
1514
selectorChartProjection,
1615
selectorChartRawProjection,
17-
selectorFitScale,
1816
} from '../useGeoProjection/useGeoProjection.selectors';
1917
import type { MapZoomView, UseGeoProjectionZoomSignature } from './useGeoProjectionZoom.types';
2018
import { getDefaultMapInteraction, getRotation, getTranslation } from './mapZoom.utils';
19+
import { PROJECTION_FACTORIES } from '../useGeoProjection';
2120

2221
/** Multiplicative zoom step applied per wheel tick. */
2322
const WHEEL_ZOOM_STEP = 1.1;
@@ -31,6 +30,11 @@ export const useGeoProjectionZoom: ChartPlugin<UseGeoProjectionZoomSignature> =
3130
}) => {
3231
const { zoom, onViewChange, view } = params;
3332

33+
const initialViewRef = React.useRef<MapZoomView>({
34+
zoomLevel: store.state.geoProjectionZoom.zoomLevel ?? 1,
35+
center: store.state.geoProjectionZoom.center ?? [0, 0],
36+
translation: store.state.geoProjectionZoom.translation ?? [0, 0],
37+
});
3438
const interactionDefaults = getDefaultMapInteraction(selectorChartRawProjection(store.state));
3539

3640
const {
@@ -45,41 +49,6 @@ export const useGeoProjectionZoom: ChartPlugin<UseGeoProjectionZoomSignature> =
4549
const enabled = zoom !== false;
4650
const isControlled = view !== undefined;
4751

48-
useEnhancedEffect(() => {
49-
// Set the default translation such that the entire map is visible.
50-
if (store.state.geoProjectionZoom.translation !== null) {
51-
return;
52-
}
53-
54-
const fitScale = selectorFitScale(store.state);
55-
const projection = selectorChartProjection(store.state);
56-
const drawingArea = selectorChartDrawingArea(store.state);
57-
58-
if (!projection || !fitScale) {
59-
return;
60-
}
61-
62-
const scale = projection?.scale();
63-
const center = projection?.center();
64-
projection?.scale(fitScale);
65-
66-
const defaultTranslation = getTranslation(
67-
store,
68-
projection,
69-
center,
70-
[drawingArea.width / 2, drawingArea.height / 2],
71-
'both',
72-
0,
73-
);
74-
if (defaultTranslation) {
75-
store.set('geoProjectionZoom', {
76-
...store.state.geoProjectionZoom,
77-
translation: defaultTranslation,
78-
});
79-
}
80-
projection?.scale(scale);
81-
}, [store]);
82-
8352
const getProjection = React.useCallback(
8453
(): GeoProjection | null => selectorChartProjection(store.state),
8554
[store],
@@ -314,7 +283,11 @@ export const useGeoProjectionZoom: ChartPlugin<UseGeoProjectionZoomSignature> =
314283
const zoomOut = React.useCallback(() => zoomBy(1 / BUTTON_ZOOM_STEP), [zoomBy]);
315284

316285
const resetZoom = React.useCallback(() => {
317-
applyView({ zoomLevel: 1, center: [0, 0], translation: [0, 0] });
286+
const view = initialViewRef.current;
287+
if (!view) {
288+
return;
289+
}
290+
applyView(view);
318291
}, [applyView]);
319292

320293
const publicAPI = { zoomIn, zoomOut, resetZoom };
@@ -337,10 +310,22 @@ useGeoProjectionZoom.getDefaultizedParams = ({ params }) => ({
337310
zoom: params.zoom ?? false,
338311
});
339312

340-
useGeoProjectionZoom.getInitialState = (params) => ({
341-
geoProjectionZoom: {
342-
zoomLevel: params.view?.zoomLevel ?? params.initialView?.zoomLevel ?? 1,
343-
center: params.view?.center ?? params.initialView?.center ?? [0, 0],
344-
translation: params.view?.translation ?? params.initialView?.translation ?? null,
345-
},
346-
});
313+
useGeoProjectionZoom.getInitialState = (params) => {
314+
const center = params.view?.center ?? params.initialView?.center ?? [0, 0];
315+
return {
316+
geoProjectionZoom: {
317+
zoomLevel: params.view?.zoomLevel ?? params.initialView?.zoomLevel ?? 1,
318+
center,
319+
translation:
320+
params.view?.translation ??
321+
params.initialView?.translation ??
322+
getDefaultTranslation(
323+
params.projection,
324+
PROJECTION_FACTORIES,
325+
params.geoData,
326+
params.parallels,
327+
center,
328+
),
329+
},
330+
};
331+
};

packages/x-charts/src/internals/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export * from './plugins/featurePlugins/useChartKeyboardNavigation';
4848
export * from './plugins/featurePlugins/useChartClosestPoint';
4949
export * from './plugins/featurePlugins/useChartBrush';
5050
export * as useGeoProjectionSelectors from './plugins/featurePlugins/useGeoProjection/useGeoProjection.selectors';
51+
export { getDefaultTranslation } from './plugins/featurePlugins/useGeoProjection/projection.utils';
5152
export * as useGeoProjectionTypes from './plugins/featurePlugins/useGeoProjection/useGeoProjection.types';
5253
export * as useGeoProjectionZoomTypes from './plugins/featurePlugins/useGeoProjectionZoom/useGeoProjectionZoom.types';
5354
export * from './plugins/featurePlugins/useChartItemClick';
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { geoPath } from '@mui/x-charts-vendor/d3-geo';
2+
import type {
3+
ExtendedFeatureCollection,
4+
GeoProjection,
5+
GeoConicProjection,
6+
} from '@mui/x-charts-vendor/d3-geo';
7+
import type { D3NamedProjection, GeoProjectionInput } from './useGeoProjection.types';
8+
9+
export const isConicProjection = (projection: GeoProjection): projection is GeoConicProjection => {
10+
return 'parallels' in projection && typeof projection.parallels === 'function';
11+
};
12+
13+
const DEFAULT_PARALLELS: [number, number] = [30, 30];
14+
export function getParallels(
15+
parallels: [number, number] | null | undefined,
16+
center: [number, number] | null,
17+
): [number, number] {
18+
if (parallels) {
19+
return parallels;
20+
}
21+
if (center) {
22+
return [-center[1] - 15, -center[1] + 15];
23+
}
24+
return DEFAULT_PARALLELS;
25+
}
26+
27+
/**
28+
* Builds a *fresh* `GeoProjection` instance from the raw projection input.
29+
*
30+
* This is intentionally not a memoized selector: callers mutate the returned projection (to fit,
31+
* scale, translate, rotate it). Returning a shared instance would mean mutations leak between
32+
* consumers and, worse, that a recomputed view keeps the same object reference — so store
33+
* subscribers comparing with `Object.is` would not detect the change and the map would render one
34+
* update behind.
35+
*/
36+
export function resolveProjectionInstance(
37+
projectionInput: GeoProjectionInput | null,
38+
projectionFactory: Record<D3NamedProjection, (() => GeoProjection) | undefined> | null,
39+
parallels: [number, number],
40+
): GeoProjection | null {
41+
if (projectionInput === null) {
42+
return null;
43+
}
44+
if (typeof projectionInput !== 'string') {
45+
return projectionInput;
46+
}
47+
const factory = projectionFactory?.[projectionInput];
48+
if (!factory) {
49+
if (process.env.NODE_ENV !== 'production') {
50+
console.error(
51+
`MUI X Charts: Unknown projection name '${projectionInput}'. ` +
52+
`Expected one of: ${Object.keys(projectionFactory ?? {}).join(', ')}.`,
53+
);
54+
}
55+
return null;
56+
}
57+
const projection = factory();
58+
if (isConicProjection(projection)) {
59+
projection.parallels(parallels);
60+
}
61+
return projection;
62+
}
63+
64+
/**
65+
* Helper function to compute the translation needed to fit the map in the drawing area at zoomLevel=1
66+
*/
67+
export function getDefaultTranslation(
68+
projectionInput: GeoProjectionInput | null | undefined,
69+
projectionFactory: Record<D3NamedProjection, (() => GeoProjection) | undefined> | null,
70+
geoData: ExtendedFeatureCollection | null | undefined,
71+
parallels: [number, number] | null | undefined,
72+
center: [number, number],
73+
): [number, number] | null {
74+
if (!projectionInput || !geoData) {
75+
return null;
76+
}
77+
78+
const projection = resolveProjectionInstance(
79+
projectionInput,
80+
projectionFactory,
81+
getParallels(parallels, center),
82+
);
83+
if (!projection) {
84+
return null;
85+
}
86+
87+
projection.rotate([-center[0], -center[1]]);
88+
89+
const [[ux0, uy0], [ux1, uy1]] = geoPath(projection).bounds(geoData);
90+
91+
const centerPoint = projection(center);
92+
93+
if (!centerPoint) {
94+
return null;
95+
}
96+
97+
return [
98+
(centerPoint[0] - (ux0 + ux1) / 2) / (ux1 - ux0),
99+
(centerPoint[1] - (uy0 + uy1) / 2) / (uy1 - uy0),
100+
];
101+
}

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

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type {
44
ExtendedFeatureCollection,
55
GeoProjection,
66
GeoPath,
7-
GeoConicProjection,
87
} from '@mui/x-charts-vendor/d3-geo';
98
import type {
109
D3NamedProjection,
@@ -16,11 +15,9 @@ import { selectorChartDrawingArea } from '../../corePlugins/useChartDimensions/u
1615
import type { UseGeoProjectionZoomSignature } from '../useGeoProjectionZoom/useGeoProjectionZoom.types';
1716
import type { GeoTooltipPosition } from '../../corePlugins/useChartSeriesConfig';
1817
import type { ChartState } from '../../models/chart';
18+
import { getParallels, isConicProjection, resolveProjectionInstance } from './projection.utils';
1919

2020
const ZERO_COORDINATES: [number, number] = [0, 0];
21-
const isConicProjection = (projection: GeoProjection): projection is GeoConicProjection => {
22-
return 'parallels' in projection && typeof projection.parallels === 'function';
23-
};
2421

2522
export const selectorChartGeoProjectionState = (
2623
state: ChartState<[], [UseGeoProjectionSignature]>,
@@ -75,18 +72,15 @@ const selectorChartCenter = createSelectorMemoized(
7572
const selectorChartTranslation = createSelectorMemoized(
7673
selectorChartGeoProjectionZoomState,
7774
function selectorChartTranslation(geoProjectionZoom): [number, number] | null {
78-
return geoProjectionZoom?.translation ?? ZERO_COORDINATES;
75+
return geoProjectionZoom?.translation ?? null;
7976
},
8077
);
8178

82-
const DEFAULT_PARALLELS: [number, number] = [30, 30];
8379
const selectorChartParallels = createSelectorMemoized(
8480
selectorChartGeoProjectionState,
8581
selectorChartCenter,
8682
function selectorChartParallels(geoProjection, center): [number, number] {
87-
return (
88-
geoProjection?.parallels ?? (center ? [-center[1] - 15, -center[1] + 15] : DEFAULT_PARALLELS)
89-
);
83+
return getParallels(geoProjection?.parallels, center);
9084
},
9185
);
9286
/**
@@ -127,43 +121,6 @@ export const selectorChartGeoFeatureIndexesByName = createSelectorMemoized(
127121
},
128122
);
129123

130-
/**
131-
* Builds a *fresh* `GeoProjection` instance from the raw projection input.
132-
*
133-
* This is intentionally not a memoized selector: callers mutate the returned projection (to fit,
134-
* scale, translate, rotate it). Returning a shared instance would mean mutations leak between
135-
* consumers and, worse, that a recomputed view keeps the same object reference — so store
136-
* subscribers comparing with `Object.is` would not detect the change and the map would render one
137-
* update behind.
138-
*/
139-
function resolveProjectionInstance(
140-
projectionInput: GeoProjectionInput | null,
141-
projectionFactory: Record<D3NamedProjection, (() => GeoProjection) | undefined> | null,
142-
parallels: [number, number],
143-
): GeoProjection | null {
144-
if (projectionInput === null) {
145-
return null;
146-
}
147-
if (typeof projectionInput !== 'string') {
148-
return projectionInput;
149-
}
150-
const factory = projectionFactory?.[projectionInput];
151-
if (!factory) {
152-
if (process.env.NODE_ENV !== 'production') {
153-
console.error(
154-
`MUI X Charts: Unknown projection name '${projectionInput}'. ` +
155-
`Expected one of: ${Object.keys(projectionFactory ?? {}).join(', ')}.`,
156-
);
157-
}
158-
return null;
159-
}
160-
const projection = factory();
161-
if (isConicProjection(projection)) {
162-
projection.parallels(parallels);
163-
}
164-
return projection;
165-
}
166-
167124
export const selectorFitScale = createSelector(
168125
selectorChartRawProjection,
169126
selectorChartProjectionFactory,

packages/x-charts/src/internals/plugins/featurePlugins/useGeoProjection/useGeoProjection.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export interface UseGeoProjectionParameters {
4747
* or a custom `GeoProjection` instance.
4848
*/
4949
projection?: GeoProjectionInput;
50+
/**
51+
* The two standard parallels used by conic projections, if applicable.
52+
* Used for projection 'conicConformal', 'conicEqualArea', 'conicEquidistant'.
53+
*/
54+
parallels?: [number, number] | null;
5055
}
5156

5257
export type UseGeoProjectionDefaultizedParameters = UseGeoProjectionParameters;

0 commit comments

Comments
 (0)