From fd68addd99ea158ebf3e115c4e26b973aada1727 Mon Sep 17 00:00:00 2001 From: Branden Palmer Date: Mon, 19 Jan 2026 16:41:18 -0600 Subject: [PATCH 1/4] Add mocks for dynamic default testing --- test/image/mocks/map_dynamic_defaults.json | 15 ++++++++++++ .../map_dynamic_defaults_anti_meridian.json | 15 ++++++++++++ .../map_dynamic_defaults_anti_meridian_2.json | 15 ++++++++++++ .../map_dynamic_defaults_center_set.json | 23 +++++++++++++++++++ .../mocks/map_dynamic_defaults_zoom_set.json | 20 ++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 test/image/mocks/map_dynamic_defaults.json create mode 100644 test/image/mocks/map_dynamic_defaults_anti_meridian.json create mode 100644 test/image/mocks/map_dynamic_defaults_anti_meridian_2.json create mode 100644 test/image/mocks/map_dynamic_defaults_center_set.json create mode 100644 test/image/mocks/map_dynamic_defaults_zoom_set.json diff --git a/test/image/mocks/map_dynamic_defaults.json b/test/image/mocks/map_dynamic_defaults.json new file mode 100644 index 00000000000..269967cd28a --- /dev/null +++ b/test/image/mocks/map_dynamic_defaults.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "hovertext": ["San Marino", "Cairo", "Istanbul", "Trondheim"], + "lat": [43.9360958, 30.06263, 41.01384, 63.43049], + "legendgroup": "", + "lon": [12.4417702, 31.24967, 28.94966, 10.39506], + "marker": { + "color": "#636efa" + }, + "mode": "markers", + "type": "scattermap" + } + ] +} diff --git a/test/image/mocks/map_dynamic_defaults_anti_meridian.json b/test/image/mocks/map_dynamic_defaults_anti_meridian.json new file mode 100644 index 00000000000..39f97bd9022 --- /dev/null +++ b/test/image/mocks/map_dynamic_defaults_anti_meridian.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "hovertext": ["P1", "P2", "P3", "P4"], + "lat": [43.9360958, 30.06263, 41.01384, 63.43049], + "legendgroup": "", + "lon": [170, 180, -180, -170], + "marker": { + "color": "#636efa" + }, + "mode": "markers", + "type": "scattermap" + } + ] +} diff --git a/test/image/mocks/map_dynamic_defaults_anti_meridian_2.json b/test/image/mocks/map_dynamic_defaults_anti_meridian_2.json new file mode 100644 index 00000000000..177b51ce3ff --- /dev/null +++ b/test/image/mocks/map_dynamic_defaults_anti_meridian_2.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "hovertext": ["P1", "P2", "P3", "P4"], + "lat": [43.9360958, 30.06263, 41.01384, 43.43049], + "legendgroup": "", + "lon": [100, 180, -180, -170], + "marker": { + "color": "#636efa" + }, + "mode": "markers", + "type": "scattermap" + } + ] +} diff --git a/test/image/mocks/map_dynamic_defaults_center_set.json b/test/image/mocks/map_dynamic_defaults_center_set.json new file mode 100644 index 00000000000..0e939ed37b3 --- /dev/null +++ b/test/image/mocks/map_dynamic_defaults_center_set.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "hovertext": ["San Marino", "Cairo", "Istanbul", "Trondheim"], + "lat": [43.9360958, 30.06263, 41.01384, 63.43049], + "legendgroup": "", + "lon": [12.4417702, 31.24967, 28.94966, 10.39506], + "marker": { + "color": "#636efa" + }, + "mode": "markers", + "type": "scattermap" + } + ], + "layout": { + "map": { + "center": { + "lat": 90, + "lon": 90 + } + } + } +} diff --git a/test/image/mocks/map_dynamic_defaults_zoom_set.json b/test/image/mocks/map_dynamic_defaults_zoom_set.json new file mode 100644 index 00000000000..af2ade74067 --- /dev/null +++ b/test/image/mocks/map_dynamic_defaults_zoom_set.json @@ -0,0 +1,20 @@ +{ + "data": [ + { + "hovertext": ["San Marino", "Cairo", "Istanbul", "Trondheim"], + "lat": [43.9360958, 30.06263, 41.01384, 63.43049], + "legendgroup": "", + "lon": [12.4417702, 31.24967, 28.94966, 10.39506], + "marker": { + "color": "#636efa" + }, + "mode": "markers", + "type": "scattermap" + } + ], + "layout": { + "map": { + "zoom": 2 + } + } +} From 6896aaaf06def4410fe39da1045663619437f68f Mon Sep 17 00:00:00 2001 From: Branden Palmer Date: Mon, 19 Jan 2026 16:45:25 -0600 Subject: [PATCH 2/4] Update map handleDefaults sig --- src/plots/map/layout_defaults.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plots/map/layout_defaults.js b/src/plots/map/layout_defaults.js index 5cb2531fa1c..e3b08556277 100644 --- a/src/plots/map/layout_defaults.js +++ b/src/plots/map/layout_defaults.js @@ -12,11 +12,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { type: 'map', attributes: layoutAttributes, handleDefaults: handleDefaults, - partition: 'y' + partition: 'y', + fullData }); }; -function handleDefaults(containerIn, containerOut, coerce) { +function handleDefaults(containerIn, containerOut, coerce, opts) { coerce('style'); coerce('center.lon'); coerce('center.lat'); From ca1bfe0b4570286e33c1d29139d2920dc369290f Mon Sep 17 00:00:00 2001 From: Branden Palmer Date: Mon, 19 Jan 2026 16:47:18 -0600 Subject: [PATCH 3/4] Add custom bounding box helpers These work with anti-meridian unlike turf.js. --- src/plots/map/layout_defaults.js | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/plots/map/layout_defaults.js b/src/plots/map/layout_defaults.js index e3b08556277..069efdbcb87 100644 --- a/src/plots/map/layout_defaults.js +++ b/src/plots/map/layout_defaults.js @@ -47,6 +47,51 @@ function handleDefaults(containerIn, containerOut, coerce, opts) { containerOut._input = containerIn; } +function getMinBoundLon(lon) { + if (!lon.length) return { minLon: 0, maxLon: 0 }; + + // normalize to [0, 360) + const norm = lon.map(to360).sort((a, b) => a - b); + + let maxGap = -1; + let gapIndex = 0; + + // find largest gap + for (let i = 0; i < norm.length; i++) { + const curr = norm[i]; + const next = norm[(i + 1) % norm.length]; + const gap = (next - curr + 360) % 360; + + if (gap > maxGap) { + maxGap = gap; + gapIndex = i; + } + } + + // take complement of largest gap + let minLon = norm[(gapIndex + 1) % norm.length]; + let maxLon = norm[gapIndex]; + minLon = to180(minLon) + maxLon = to180(maxLon) + + return { minLon, maxLon }; + + // https://gis.stackexchange.com/questions/201789/verifying-formula-that-will-convert-longitude-0-360-to-180-to-180 + function to180(deg) { + return ((deg + 180) % 360) - 180 + } + function to360(deg) { + return ((deg % 360) + 360) % 360; + } +} + +function getMinBoundLat(lat) { + return { + minLat: Math.min(...lat), + maxLat: Math.max(...lat) + }; +} + function handleLayerDefaults(layerIn, layerOut) { function coerce(attr, dflt) { return Lib.coerce(layerIn, layerOut, layoutAttributes.layers, attr, dflt); From 5dc6b40e34814be06b74d047950751bf09e3ca93 Mon Sep 17 00:00:00 2001 From: Branden Palmer Date: Mon, 19 Jan 2026 16:51:35 -0600 Subject: [PATCH 4/4] Call fitBounds in ctor and on update --- src/plots/map/layout_defaults.js | 16 ++++++++++++++++ src/plots/map/map.js | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/plots/map/layout_defaults.js b/src/plots/map/layout_defaults.js index 069efdbcb87..823a07b972e 100644 --- a/src/plots/map/layout_defaults.js +++ b/src/plots/map/layout_defaults.js @@ -25,6 +25,22 @@ function handleDefaults(containerIn, containerOut, coerce, opts) { coerce('bearing'); coerce('pitch'); + // dynamically set center/zoom if neither param provided + if (!containerIn?.center && !containerIn?.zoom) { + var [{ lon, lat }] = opts.fullData; + var { minLon, maxLon } = getMinBoundLon(lon); + var { minLat, maxLat } = getMinBoundLat(lat); + // this param is called bounds in mapLibre ctor + // not to be confused with maxBounds aliased below + containerOut.fitBounds = { + west: minLon, + east: maxLon, + south: minLat, + north: maxLat, + }; + } + + // bounds is really for setting maxBounds in mapLibre ctor var west = coerce('bounds.west'); var east = coerce('bounds.east'); var south = coerce('bounds.south'); diff --git a/src/plots/map/map.js b/src/plots/map/map.js index 9a397743482..e1cb87f76db 100644 --- a/src/plots/map/map.js +++ b/src/plots/map/map.js @@ -80,6 +80,10 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) { var bounds = opts.bounds; var maxBounds = bounds ? [[bounds.west, bounds.south], [bounds.east, bounds.north]] : null; + var fitBounds = opts.fitBounds ? [ + [opts.fitBounds.west, opts.fitBounds.south], + [opts.fitBounds.east, opts.fitBounds.north], + ] : null; // create the map! var map = self.map = new maplibregl.Map({ @@ -90,6 +94,10 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) { zoom: opts.zoom, bearing: opts.bearing, pitch: opts.pitch, + bounds: fitBounds, + fitBoundsOptions: { + padding: 20, + }, maxBounds: maxBounds, interactive: !self.isStatic, @@ -334,6 +342,10 @@ proto.updateLayout = function(fullLayout) { if(!this.dragging && !this.wheeling) { map.setCenter(convertCenter(opts.center)); map.setZoom(opts.zoom); + if (opts.fitBounds) { + var { west, south, east, north } = opts.fitBounds + map.fitBounds([[west, south], [east, north]], { padding: 20 }) + } map.setBearing(opts.bearing); map.setPitch(opts.pitch); }