diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 67bc00abf55..a1bef00d5b3 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -548,9 +548,22 @@ function handleGeo(gd, ev) { if(attr === 'zoom') { var scale = geoLayout.projection.scale; + var minscale = geoLayout.projection.minscale; + var maxscale = geoLayout.projection.maxscale; + + if(maxscale === -1) maxscale = Infinity; var newScale = (val === 'in') ? 2 * scale : 0.5 * scale; - Registry.call('_guiRelayout', gd, id + '.projection.scale', newScale); + // make sure the scale is within the min/max bounds + if(newScale > maxscale) { + newScale = maxscale; + } else if(newScale < minscale) { + newScale = minscale; + } + + if(newScale !== scale) { + Registry.call('_guiRelayout', gd, id + '.projection.scale', newScale); + } } } diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index db7a2d83564..594cb073335 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -709,6 +709,14 @@ function getProjection(geoLayout) { projection.precision(constants.precision); + // https://github.com/d3/d3-zoom/blob/master/README.md#zoom_scaleExtent + projection.scaleExtent = function() { + var minscale = projLayout.minscale; + var maxscale = projLayout.maxscale; + if(maxscale === -1) maxscale = Infinity; + return [100 * minscale, 100 * maxscale]; + }; + if(geoLayout._isSatellite) { projection.tilt(projLayout.tilt).distance(projLayout.distance); } diff --git a/src/plots/geo/layout_attributes.js b/src/plots/geo/layout_attributes.js index 97ef64786e6..f320976dbe8 100644 --- a/src/plots/geo/layout_attributes.js +++ b/src/plots/geo/layout_attributes.js @@ -177,6 +177,26 @@ var attrs = module.exports = overrideAll({ 'that fits the map\'s lon and lat ranges. ' ].join(' ') }, + minscale: { + valType: 'number', + min: 0, + dflt: 0, + description: [ + 'Minimal zoom level of the map view.', + 'A minscale of *0.5* (50%) corresponds to a zoom level', + 'where the map has half the size of base zoom level.' + ].join(' ') + }, + maxscale: { + valType: 'number', + min: 0, + dflt: -1, + description: [ + 'Maximal zoom level of the map view.', + 'A maxscale of *2* (200%) corresponds to a zoom level', + 'where the map is twice as big as the base layer.' + ].join(' ') + }, }, center: { lon: { diff --git a/src/plots/geo/layout_defaults.js b/src/plots/geo/layout_defaults.js index 54b4d49bda4..411a42cfdb1 100644 --- a/src/plots/geo/layout_defaults.js +++ b/src/plots/geo/layout_defaults.js @@ -161,6 +161,8 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { } coerce('projection.scale'); + coerce('projection.minscale'); + coerce('projection.maxscale'); show = coerce('showland', !visible ? false : undefined); if(show) coerce('landcolor'); @@ -205,6 +207,8 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) { // clear attributes that will get auto-filled later if(fitBounds) { delete geoLayoutOut.projection.scale; + delete geoLayoutOut.projection.minscale; + delete geoLayoutOut.projection.maxscale; if(isScoped) { delete geoLayoutOut.center.lon; diff --git a/src/plots/geo/zoom.js b/src/plots/geo/zoom.js index 2d79d69f581..824ba39a0ef 100644 --- a/src/plots/geo/zoom.js +++ b/src/plots/geo/zoom.js @@ -32,6 +32,7 @@ module.exports = createGeoZoom; function initZoom(geo, projection) { return d3.behavior.zoom() .translate(projection.translate()) + .scaleExtent(projection.scaleExtent()) .scale(projection.scale()); } diff --git a/test/plot-schema.json b/test/plot-schema.json index fd2b1659e37..eb189f22bbb 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -2585,6 +2585,20 @@ "valType": "number" }, "editType": "plot", + "maxscale": { + "description": "Maximal zoom level of the map view. A maxscale of *2* (200%) corresponds to a zoom level where the map is twice as big as the base layer.", + "dflt": -1, + "editType": "plot", + "min": 0, + "valType": "number" + }, + "minscale": { + "description": "Minimal zoom level of the map view. A minscale of *0.5* (50%) corresponds to a zoom level where the map has half the size of base zoom level.", + "dflt": 0, + "editType": "plot", + "min": 0, + "valType": "number" + }, "parallels": { "description": "For conic projection types only. Sets the parallels (tangent, secant) where the cone intersects the sphere.", "editType": "plot",