Skip to content

Commit 91fbd71

Browse files
authored
Merge pull request #7884 from plotly/cam/7674/compute-map-default-bounds
fix: Dynamically compute default zoom/center for map traces
2 parents dc46bce + b2c9dd4 commit 91fbd71

20 files changed

Lines changed: 913 additions & 460 deletions

CONTRIBUTING.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,10 @@ All traces modules set:
405405
This object is used to generate the plot-schema JSON.
406406
- `_module.supplyDefaults`: Takes in input trace settings and coerces them into "full" settings
407407
under `gd._fullData`. This one is called during the figure-wide `Plots.supplyDefaults` routine.
408-
Note that the `supplyDefaults` method performance should scale with the number of attributes (**not** the
409-
number of data points - so it should not loop over any data arrays).
408+
As a rule, `supplyDefaults` performance should scale with the number of attributes rather than
409+
the number of data points, so avoid looping over data arrays in this function. That being said,
410+
there are a few exceptions to this rule due to technical requirements (`fitbounds` on map subplots).
411+
The same guidance applies to `_module.supplyLayoutDefaults`.
410412
- `_module.calc`: Converts inputs data into "calculated" (or sanitized) data. This one is called during
411413
the figure-wide `Plots.doCalcdata` routine. The `calc` method is allowed to
412414
scale with the number of data points and is in general more costly than `supplyDefaults`.

draftlogs/7884_fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix `scattermap`, `densitymap` traces not showing all points by dynamically computing `center`, `zoom` values [[#7884](https://github.com/plotly/plotly.js/pull/7884)], with thanks to @palmerusaf and @DhruvGarg111 for the contributions!

src/plots/map/constants.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,15 @@ var styleValuesMap = sortObjectKeys(stylesMap);
7070

7171
module.exports = {
7272
styleValueDflt: 'basic',
73-
stylesMap: stylesMap,
74-
styleValuesMap: styleValuesMap,
75-
73+
stylesMap,
74+
styleValuesMap,
7675
traceLayerPrefix: 'plotly-trace-layer-',
7776
layoutLayerPrefix: 'plotly-layout-layer-',
78-
7977
missingStyleErrorMsg: [
8078
'No valid maplibre style found, please set `map.style` to one of:',
8179
styleValuesMap.join(', '),
8280
'or use a tile service.'
8381
].join('\n'),
84-
85-
mapOnErrorMsg: 'Map error.'
82+
mapOnErrorMsg: 'Map error.',
83+
fitBoundsPadding: 20
8684
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { computeBbox } from '../../lib/geo_location_utils';
2+
import type { MapLayout, ScattermapData } from '../../types/generated/schema';
3+
4+
// Same shape as the user-facing `map.bounds` attribute, but with all fields required
5+
type LonLatBox = Required<NonNullable<MapLayout['bounds']>>;
6+
7+
// Minimal shape of the fullData entries this helper reads
8+
interface FitBoundsTrace extends Pick<ScattermapData, 'subplot' | 'visible'> {
9+
// Tighten lat/lon to be more specific than default
10+
lat?: ArrayLike<number>;
11+
lon?: ArrayLike<number>;
12+
// Broaden type since this could run against multiple trace types
13+
type?: string;
14+
}
15+
16+
/**
17+
* Compute a lon/lat bounding box from lonlat-bearing traces (`scattermap`,
18+
* `densitymap`) on the given map subplot.
19+
*
20+
* Returns null when:
21+
* - no fittable data exists on the subplot;
22+
* - a location-based trace (`choroplethmap`) is present — those carry
23+
* `locations`/`geojson`, not raw lon/lat, and need geojson bbox handling
24+
* that isn't implemented here.
25+
*
26+
* @param fullData - The full data array (post supply-defaults)
27+
* @param subplotId - e.g. `'map'`, `'map2'`
28+
*/
29+
export function getMapFitBounds(fullData: FitBoundsTrace[], subplotId: string): LonLatBox | null {
30+
const coordinates: [number, number][] = [];
31+
32+
for (const trace of fullData) {
33+
if (trace.subplot !== subplotId || trace.visible !== true) continue;
34+
35+
// choroplethmap traces carry locations/geojson, not raw lon/lat; bail
36+
// out rather than frame around a subset of the subplot's data.
37+
if (trace.type === 'choroplethmap') return null;
38+
39+
const { lat, lon } = trace;
40+
if (!lon || !lat) continue;
41+
42+
const len = Math.min(lon.length, lat.length);
43+
for (let j = 0; j < len; j++) {
44+
const lo = lon[j];
45+
const la = lat[j];
46+
if (Number.isFinite(lo) && Number.isFinite(la)) coordinates.push([lo, la]);
47+
}
48+
}
49+
50+
const bbox = computeBbox({ type: 'MultiPoint', coordinates });
51+
if (!bbox) return null;
52+
53+
const [west, south, east, north] = bbox;
54+
return { west, east, south, north };
55+
}

src/plots/map/index.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,6 @@ exports.plot = function plot(gd) {
5555
fullLayout[id]._subplot = map;
5656
}
5757

58-
if(!map.viewInitial) {
59-
map.viewInitial = {
60-
center: Lib.extendFlat({}, opts.center),
61-
zoom: opts.zoom,
62-
bearing: opts.bearing,
63-
pitch: opts.pitch
64-
};
65-
}
66-
6758
map.plot(subplotCalcData, fullLayout, gd._promises);
6859
}
6960
};

0 commit comments

Comments
 (0)