Skip to content

Commit 6e87fa5

Browse files
committed
default interaction
1 parent 2c61e1d commit 6e87fa5

8 files changed

Lines changed: 117 additions & 12 deletions

File tree

docs/data/charts/map/ProjectionMapShape.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default function ProjectionMapShape() {
7878
<ChartsGeoDataProviderPremium
7979
geoData={isConicProjection(projection) ? USAStates : countries}
8080
projection={projection}
81-
zoom={{ rotationAllowed: 'long', translationAllowed: 'both' }}
81+
zoom
8282
height={360}
8383
apiRef={apiRef}
8484
>

docs/data/charts/map/ProjectionMapShape.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export default function ProjectionMapShape() {
9090
<ChartsGeoDataProviderPremium
9191
geoData={isConicProjection(projection) ? USAStates : countries}
9292
projection={projection}
93-
zoom={{ rotationAllowed: 'long', translationAllowed: 'both' }}
93+
zoom
9494
height={360}
9595
apiRef={apiRef}
9696
>

docs/data/charts/map/map.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ The zoom object has two properties to limit this behavior:
217217
- `rotationAllowed`: `'both' | 'long' | 'lat' | 'none'` Limit how the center can be modified
218218
- `translationAllowed`: `'both' | 'x' | 'y' | 'none'` Limit how the translation can be modified
219219

220-
In most cases, the configuration `{ translationAllowed: 'y', rotationAllowed: 'long' }` is the more appropriate.
220+
By default, both are derived from the chosen projection so each map behaves as expected without extra configuration:
221+
222+
- Globe-like (azimuthal) and conic projections rotate the sphere (`{ rotationAllowed: 'both', translationAllowed: 'none' }`).
223+
- Flat (cylindrical) projections pan and wrap eastwest (`{ rotationAllowed: 'long', translationAllowed: 'both' }`).
221224

222225
You can also modify the zoom result with:
223226

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export const selectorChartGeoProjectionState =
66
useGeoProjectionSelectors.selectorChartGeoProjectionState;
77
export const selectorChartGeoData = useGeoProjectionSelectors.selectorChartGeoData;
88
export const selectorChartProjection = useGeoProjectionSelectors.selectorChartProjection;
9+
export const selectorChartRawProjection = useGeoProjectionSelectors.selectorChartRawProjection;
910
export const selectorChartGeoPath = useGeoProjectionSelectors.selectorChartGeoPath;

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { describe, it, expect } from 'vitest';
22
import type { GeoProjection } from '@mui/x-charts-vendor/d3-geo';
3-
import { geoMercator, geoOrthographic } from '@mui/x-charts-vendor/d3-geo';
4-
import { clampTranslationAxis, getRotation } from './mapZoom.utils';
3+
import {
4+
geoConicConformal,
5+
geoAlbersUsa,
6+
geoMercator,
7+
geoOrthographic,
8+
geoStereographic,
9+
geoAlbers,
10+
} from '@mui/x-charts-vendor/d3-geo';
11+
import { clampTranslationAxis, getProjectionFamily, getRotation } from './mapZoom.utils';
512

613
const EXTENT: [[number, number], [number, number]] = [
714
[0, 0],
@@ -125,4 +132,26 @@ describe('mapZoomUtils', () => {
125132
);
126133
});
127134
});
135+
136+
describe('getProjectionFamily', () => {
137+
it('conic detection', () => {
138+
expect(getProjectionFamily(geoConicConformal())).to.deep.equal('conic');
139+
});
140+
141+
it('conic detection for Albers', () => {
142+
expect(getProjectionFamily(geoAlbers())).to.deep.equal('conic');
143+
});
144+
145+
it('conic detection for Albers USA', () => {
146+
expect(getProjectionFamily(geoAlbersUsa())).to.deep.equal('conic');
147+
});
148+
149+
it('cylindrical detection for Mercator', () => {
150+
expect(getProjectionFamily(geoMercator())).to.deep.equal('cylindrical');
151+
});
152+
153+
it('azimuthal detection for Stereographic', () => {
154+
expect(getProjectionFamily(geoStereographic())).to.deep.equal('azimuthal');
155+
});
156+
});
128157
});

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

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,82 @@
11
import type { GeoProjection } from '@mui/x-charts-vendor/d3-geo';
22
import { geoPath } from '@mui/x-charts-vendor/d3-geo';
33
import { selectorChartDrawingArea } from '@mui/x-charts/internals';
4-
import type { ChartUsedStore } from '@mui/x-charts/internals';
4+
import type { ChartUsedStore, useGeoProjectionTypes } from '@mui/x-charts/internals';
55
import { selectorChartGeoData } from '../useGeoProjection/useGeoProjection.selectors';
66
import type { MapRotationAxis, MapTranslationAxis } from './useGeoProjectionZoom.types';
77

88
const DEG = Math.PI / 180;
99
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
1010

11+
type ProjectionFamily = 'azimuthal' | 'conic' | 'cylindrical';
12+
13+
const PROJECTION_FAMILY: Record<useGeoProjectionTypes.D3NamedProjection, ProjectionFamily> = {
14+
azimuthalEqualArea: 'azimuthal',
15+
azimuthalEquidistant: 'azimuthal',
16+
gnomonic: 'azimuthal',
17+
orthographic: 'azimuthal',
18+
stereographic: 'azimuthal',
19+
conicConformal: 'conic',
20+
conicEqualArea: 'conic',
21+
conicEquidistant: 'conic',
22+
albers: 'conic',
23+
albersUsa: 'conic',
24+
equirectangular: 'cylindrical',
25+
mercator: 'cylindrical',
26+
transverseMercator: 'cylindrical',
27+
equalEarth: 'cylindrical',
28+
naturalEarth1: 'cylindrical',
29+
};
30+
31+
/**
32+
* The interaction that feels natural for each projection family, used as the default for
33+
* `rotationAllowed`/`translationAllowed` when the consumer does not set them explicitly.
34+
*/
35+
const FAMILY_INTERACTION: Record<
36+
ProjectionFamily,
37+
{ rotationAllowed: MapRotationAxis; translationAllowed: MapTranslationAxis }
38+
> = {
39+
azimuthal: { rotationAllowed: 'both', translationAllowed: 'none' },
40+
conic: { rotationAllowed: 'both', translationAllowed: 'none' },
41+
cylindrical: { rotationAllowed: 'long', translationAllowed: 'both' },
42+
};
43+
44+
export function getProjectionFamily(
45+
projection: useGeoProjectionTypes.GeoProjectionInput | null,
46+
): ProjectionFamily {
47+
if (projection == null) {
48+
return 'cylindrical'; // fallback to avoid useless edge cases
49+
}
50+
if (typeof projection === 'string') {
51+
return PROJECTION_FAMILY[projection] ?? 'cylindrical';
52+
}
53+
54+
// Try to guess if users provided custom projection
55+
56+
// Conic projections expose the `parallels` accessor (e.g. conicConformal, albers).
57+
if ('parallels' in projection) {
58+
return 'conic';
59+
}
60+
// Composite projections such as `albersUsa` stitch several conics together and so drop `rotate`
61+
// (there is no single sphere to spin). Treat them like the conic family they are built from.
62+
if (typeof projection.rotate !== 'function') {
63+
return 'conic';
64+
}
65+
// Azimuthal projections clip the sphere to a disc, so their clip angle sits strictly between 0 and
66+
// 180° (e.g. 90° for orthographic). Flat projections leave it at 0° (no clipping).
67+
const clipAngle = projection.clipAngle?.();
68+
if (typeof clipAngle === 'number' && clipAngle > 0 && clipAngle < 180) {
69+
return 'azimuthal';
70+
}
71+
return 'cylindrical';
72+
}
73+
74+
export function getDefaultMapInteraction(
75+
projection: useGeoProjectionTypes.GeoProjectionInput | null,
76+
): { rotationAllowed: MapRotationAxis; translationAllowed: MapTranslationAxis } {
77+
return FAMILY_INTERACTION[getProjectionFamily(projection)];
78+
}
79+
1180
/**
1281
* The geographic `center` (`[longitude, latitude]`) such that, after scaling the projection by
1382
* `zoomFactor` and applying `rotate([-center[0], -center[1]])`, the geographic coordinate

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import {
99
useRegisterZoomGestures,
1010
} from '@mui/x-charts-pro/internals';
1111
import type { GeoProjection } from '@mui/x-charts-vendor/d3-geo';
12-
import { selectorChartProjection } from '../useGeoProjection/useGeoProjection.selectors';
12+
import {
13+
selectorChartProjection,
14+
selectorChartRawProjection,
15+
} from '../useGeoProjection/useGeoProjection.selectors';
1316
import type { MapZoomView, UseGeoProjectionZoomSignature } from './useGeoProjectionZoom.types';
14-
import { getRotation, getTranslation } from './mapZoom.utils';
17+
import { getDefaultMapInteraction, getRotation, getTranslation } from './mapZoom.utils';
1518

1619
/** Multiplicative zoom step applied per wheel tick. */
1720
const WHEEL_ZOOM_STEP = 1.1;
@@ -25,12 +28,14 @@ export const useGeoProjectionZoom: ChartPlugin<UseGeoProjectionZoomSignature> =
2528
}) => {
2629
const { zoom, onZoomChange, view } = params;
2730

31+
const interactionDefaults = getDefaultMapInteraction(selectorChartRawProjection(store.state));
32+
2833
const {
2934
minZoomLevel = 1,
3035
maxZoomLevel = 8,
3136
maxGap = 0,
32-
rotationAllowed = 'both',
33-
translationAllowed = 'both',
37+
rotationAllowed = interactionDefaults.rotationAllowed,
38+
translationAllowed = interactionDefaults.translationAllowed,
3439
} = typeof zoom === 'object' ? zoom : {};
3540

3641
// `zoom` is either a boolean or a config object; an object always enables the interaction.

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,11 @@ export interface MapZoomOptions {
4141
/**
4242
* Which axes the map can be rotated along while panning or zooming.
4343
* For example, `'long'` lets the map rotate east–west but locks the north–south tilt.
44-
* @default 'both'
4544
*/
4645
rotationAllowed?: MapRotationAxis;
4746
/**
4847
* Which axes the map can be translated along while panning or zooming.
4948
* For example, `'y'` lets the map translate vertically but locks the horizontal movement.
50-
* @default 'both'
5149
*/
5250
translationAllowed?: MapTranslationAxis;
5351
/**

0 commit comments

Comments
 (0)