Skip to content

feature: Add projection.minscale and projection.maxscale to geo subplots #7482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/plots/geo/geo.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,21 @@ proto.updateProjection = function(geoCalcData, fullLayout) {

var projection = this.projection = getProjection(geoLayout);

projection.getScale = function() {
return projection.scale();
};

projection.setScale = function(scale) {
this._initalScale = this._initalScale || projection.scale();

var minscale = projLayout.minscale;
var maxscale = projLayout.maxscale;
if(minscale !== undefined) scale = Math.max(minscale, scale / this._initalScale) * this._initalScale;
if(maxscale !== undefined) scale = Math.min(maxscale, scale / this._initalScale) * this._initalScale;

return projection.scale(scale);
};

// setup subplot extent [[x0,y0], [x1,y1]]
var extent = [[
gs.l + gs.w * domain.x[0],
Expand Down Expand Up @@ -265,7 +280,7 @@ proto.updateProjection = function(geoCalcData, fullLayout) {
projection.fitExtent(extent, rangeBox);

var b = this.bounds = projection.getBounds(rangeBox);
var s = this.fitScale = projection.scale();
var s = this.fitScale = projection.getScale();
var t = projection.translate();

if(geoLayout.fitbounds) {
Expand All @@ -276,13 +291,13 @@ proto.updateProjection = function(geoCalcData, fullLayout) {
);

if(isFinite(k2)) {
projection.scale(k2 * s);
projection.setScale(k2 * s);
} else {
Lib.warn('Something went wrong during' + this.id + 'fitbounds computations.');
}
} else {
// adjust projection to user setting
projection.scale(projLayout.scale * s);
projection.setScale(projLayout.scale * s);
}

// px coordinates of view mid-point,
Expand Down
18 changes: 18 additions & 0 deletions src/plots/geo/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ var attrs = module.exports = overrideAll({
'that fits the map\'s lon and lat ranges. '
].join(' ')
},
minscale: {
valType: 'number',
min: 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,
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: {
Expand Down
11 changes: 10 additions & 1 deletion src/plots/geo/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,16 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
coerce('projection.parallels', dfltProjParallels);
}

coerce('projection.scale');
var minscale = coerce('projection.minscale');
var maxscale = coerce('projection.maxscale');
if(minscale !== undefined && maxscale !== undefined && minscale > maxscale) {
geoLayoutOut.projection.minscale = minscale = undefined;
geoLayoutOut.projection.maxscale = maxscale = undefined;
}

var scale = coerce('projection.scale');
if(minscale !== undefined) geoLayoutOut.projection.scale = scale = Math.max(minscale, scale);
if(maxscale !== undefined) geoLayoutOut.projection.scale = scale = Math.min(maxscale, scale);

show = coerce('showland', !visible ? false : undefined);
if(show) coerce('landcolor');
Expand Down
21 changes: 11 additions & 10 deletions src/plots/geo/zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module.exports = createGeoZoom;
function initZoom(geo, projection) {
return d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale());
.scale(projection.getScale());
}

// sync zoom updates with user & full layout
Expand Down Expand Up @@ -60,7 +60,7 @@ function sync(geo, projection, cb) {
}

cb(set);
set('projection.scale', projection.scale() / geo.fitScale);
set('projection.scale', projection.getScale() / geo.fitScale);
set('fitbounds', false);
gd.emit('plotly_relayout', eventData);
}
Expand All @@ -75,13 +75,13 @@ function zoomScoped(geo, projection) {

function handleZoom() {
projection
.scale(d3.event.scale)
.setScale(d3.event.scale)
.translate(d3.event.translate);
geo.render(true);

var center = projection.invert(geo.midPt);
geo.graphDiv.emit('plotly_relayouting', {
'geo.projection.scale': projection.scale() / geo.fitScale,
'geo.projection.scale': projection.getScale() / geo.fitScale,
'geo.center.lon': center[0],
'geo.center.lat': center[1]
});
Expand Down Expand Up @@ -143,12 +143,12 @@ function zoomNonClipped(geo, projection) {
mouse1 = d3.mouse(this);

if(outside(mouse0)) {
zoom.scale(projection.scale());
zoom.scale(projection.getScale());
zoom.translate(projection.translate());
return;
}

projection.scale(d3.event.scale);
projection.setScale(d3.event.scale);
projection.translate([translate0[0], d3.event.translate[1]]);

if(!zoomPoint) {
Expand All @@ -167,7 +167,7 @@ function zoomNonClipped(geo, projection) {
var rotate = projection.rotate();
var center = projection.invert(geo.midPt);
geo.graphDiv.emit('plotly_relayouting', {
'geo.projection.scale': projection.scale() / geo.fitScale,
'geo.projection.scale': projection.getScale() / geo.fitScale,
'geo.center.lon': center[0],
'geo.center.lat': center[1],
'geo.projection.rotation.lon': -rotate[0]
Expand Down Expand Up @@ -199,7 +199,7 @@ function zoomNonClipped(geo, projection) {
// zoom for clipped projections
// inspired by https://www.jasondavies.com/maps/d3.geo.zoom.js
function zoomClipped(geo, projection) {
var view = {r: projection.rotate(), k: projection.scale()};
var view = {r: projection.rotate(), k: projection.getScale()};
var zoom = initZoom(geo, projection);
var event = d3eventDispatch(zoom, 'zoomstart', 'zoom', 'zoomend');
var zooming = 0;
Expand All @@ -221,7 +221,8 @@ function zoomClipped(geo, projection) {
zoomOn.call(zoom, 'zoom', function() {
var mouse1 = d3.mouse(this);

projection.scale(view.k = d3.event.scale);
projection.setScale(d3.event.scale);
view.k = projection.getScale();

if(!zoomPoint) {
// if no zoomPoint, the mouse wasn't over the actual geography yet
Expand Down Expand Up @@ -272,7 +273,7 @@ function zoomClipped(geo, projection) {

var _rotate = projection.rotate();
geo.graphDiv.emit('plotly_relayouting', {
'geo.projection.scale': projection.scale() / geo.fitScale,
'geo.projection.scale': projection.getScale() / geo.fitScale,
'geo.projection.rotation.lon': -_rotate[0],
'geo.projection.rotation.lat': -_rotate[1]
});
Expand Down
78 changes: 78 additions & 0 deletions test/jasmine/tests/geo_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2824,3 +2824,81 @@ describe('plotly_relayouting', function() {
});
});
});


describe('minscale and maxscale', function() {
function scroll(pos, delta) {
return new Promise(function(resolve) {
mouseEvent('mousemove', pos[0], pos[1]);
mouseEvent('scroll', pos[0], pos[1], {deltaX: delta[0], deltaY: delta[1]});
setTimeout(resolve, 100);
});
}

var gd;

beforeEach(function() { gd = createGraphDiv(); });

afterEach(destroyGraphDiv);

var allTests = [
{
name: 'non-clipped',
mock: require('../../image/mocks/geo_winkel-tripel')
},
{
name: 'clipped',
mock: require('../../image/mocks/geo_orthographic')
},
{
name: 'scoped',
mock: require('../../image/mocks/geo_europe-bubbles')
}
];

allTests.forEach(function(test) {
it(test.name + ' maxscale', function(done) {
var fig = Lib.extendDeep({}, test.mock);
fig.layout.width = 700;
fig.layout.height = 500;
fig.layout.dragmode = 'pan';
if(!fig.layout.geo.projection) fig.layout.geo.projection = {};
fig.layout.geo.projection.maxscale = 1.2;

var initialScale;

Plotly.newPlot(gd, fig)
.then(function() {
initialScale = gd._fullLayout.geo._subplot.projection.scale();

return scroll([200, 250], [-200, -200]);
})
.then(function() {
expect(gd._fullLayout.geo._subplot.projection.scale()).toEqual(1.2 * initialScale);
})
.then(done, done.fail);
});

it(test.name + ' minscale', function(done) {
var fig = Lib.extendDeep({}, test.mock);
fig.layout.width = 700;
fig.layout.height = 500;
fig.layout.dragmode = 'pan';
if(!fig.layout.geo.projection) fig.layout.geo.projection = {};
fig.layout.geo.projection.minscale = 0.8;

var initialScale;

Plotly.newPlot(gd, fig)
.then(function() {
initialScale = gd._fullLayout.geo._subplot.projection.scale();

return scroll([200, 250], [200, 200]);
})
.then(function() {
expect(gd._fullLayout.geo._subplot.projection.scale()).toEqual(0.8 * initialScale);
})
.then(done, done.fail);
});
});
});
12 changes: 12 additions & 0 deletions test/plot-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,18 @@
"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.",
"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.",
"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",
Expand Down