diff --git a/draftlogs/7371_add.md b/draftlogs/7371_add.md new file mode 100644 index 00000000000..f134f571ff9 --- /dev/null +++ b/draftlogs/7371_add.md @@ -0,0 +1 @@ + - Add `minscale`, `maxscale` geo plot attributes [[#7371](https://github.com/plotly/plotly.js/pull/7371)] diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 82c5dff9a48..8ebe360ebc0 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -543,9 +543,22 @@ function handleGeo(gd, ev) { if(attr === 'zoom') { var scale = geoLayout.projection.scale; + var minscale = geoLayout.projection.minscale; + var maxscale = geoLayout.projection.maxscale === -1 ? Infinity : geoLayout.projection.maxscale; + var max = Math.max(minscale, maxscale); + var min = Math.min(minscale, maxscale); 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 > max) { + newScale = max; + } else if (newScale < min) { + newScale = min; + } + + 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 797ab8373b6..d3b7ee4ec13 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -484,7 +484,11 @@ proto.updateFx = function(fullLayout, geoLayout) { if(dragMode === 'pan') { bgRect.node().onmousedown = null; - bgRect.call(createGeoZoom(_this, geoLayout)); + var zoom = createGeoZoom(_this, geoLayout) + bgRect.call(zoom); + // TODO: Figure out how to restrict when this transition occurs. Or is it a no-op if nothing has changed? + // Trigger transition to handle if minscale attribute isn't 0 + zoom.event(bgRect) bgRect.on('dblclick.zoom', zoomReset); if(!gd._context._scrollZoom.geo) { bgRect.on('wheel.zoom', null); @@ -709,6 +713,15 @@ function getProjection(geoLayout) { projection.precision(constants.precision); + // https://d3js.org/d3-zoom#zoom_scaleExtent + projection.scaleExtent = () => { + var minscale = projLayout.minscale; + var maxscale = projLayout.maxscale === -1 ? Infinity : projLayout.maxscale; + var max = Math.max(minscale, maxscale); + var min = Math.min(minscale, maxscale); + return [100 * min, 100 * max]; + }; + 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 b94cca3bec5..aa9e6e59acc 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 a5d64e2e9f8..8848f5b7f05 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..6cfe1e8f0df 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()); } @@ -132,7 +133,10 @@ function zoomNonClipped(geo, projection) { function handleZoomstart() { d3.select(this).style(zoomstartStyle); - mouse0 = d3.mouse(this); + var rect = this.getBBox() + mouse0 = d3.event.sourceEvent + ? d3.mouse(this) + : [rect.x + rect.width / 2, rect.y + rect.height / 2]; rotate0 = projection.rotate(); translate0 = projection.translate(); lastRotate = rotate0; @@ -140,8 +144,10 @@ function zoomNonClipped(geo, projection) { } function handleZoom() { - mouse1 = d3.mouse(this); - + var rect = this.getBBox() + mouse1 = d3.event.sourceEvent + ? d3.mouse(this) + : [rect.x + rect.width / 2, rect.y + rect.height / 2]; if(outside(mouse0)) { zoom.scale(projection.scale()); zoom.translate(projection.translate()); @@ -210,7 +216,10 @@ function zoomClipped(geo, projection) { zoom.on('zoomstart', function() { d3.select(this).style(zoomstartStyle); - var mouse0 = d3.mouse(this); + var rect = this.getBBox() + var mouse0 = d3.event.sourceEvent + ? d3.mouse(this) + : [rect.x + rect.width / 2, rect.y + rect.height / 2]; var rotate0 = projection.rotate(); var lastRotate = rotate0; var translate0 = projection.translate(); @@ -219,7 +228,10 @@ function zoomClipped(geo, projection) { zoomPoint = position(projection, mouse0); zoomOn.call(zoom, 'zoom', function() { - var mouse1 = d3.mouse(this); + var rect = this.getBBox() + var mouse1 = d3.event.sourceEvent + ? d3.mouse(this) + : [rect.x + rect.width / 2, rect.y + rect.height / 2]; projection.scale(view.k = d3.event.scale); diff --git a/test/plot-schema.json b/test/plot-schema.json index 6aa77cf3338..3eca5b100d7 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -2373,6 +2373,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",