@@ -22,24 +22,26 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
2222
2323function handleGeoDefaults ( geoLayoutIn , geoLayoutOut , coerce , opts ) {
2424 var subplotData = getSubplotData ( opts . fullData , 'geo' , opts . id ) ;
25- var traceIndices = subplotData . map ( function ( t ) { return t . index ; } ) ;
25+ var traceIndices = subplotData . map ( function ( t ) {
26+ return t . index ;
27+ } ) ;
2628
2729 var resolution = coerce ( 'resolution' ) ;
2830 var scope = coerce ( 'scope' ) ;
2931 var scopeParams = constants . scopeDefaults [ scope ] ;
3032
3133 var projType = coerce ( 'projection.type' , scopeParams . projType ) ;
32- var isAlbersUsa = geoLayoutOut . _isAlbersUsa = projType === 'albers usa' ;
34+ var isAlbersUsa = ( geoLayoutOut . _isAlbersUsa = projType === 'albers usa' ) ;
3335
3436 // no other scopes are allowed for 'albers usa' projection
35- if ( isAlbersUsa ) scope = geoLayoutOut . scope = 'usa' ;
37+ if ( isAlbersUsa ) scope = geoLayoutOut . scope = 'usa' ;
3638
37- var isScoped = geoLayoutOut . _isScoped = ( scope !== 'world' ) ;
38- var isSatellite = geoLayoutOut . _isSatellite = projType === 'satellite' ;
39- var isConic = geoLayoutOut . _isConic = projType . indexOf ( 'conic' ) !== - 1 || projType === 'albers' ;
40- var isClipped = geoLayoutOut . _isClipped = ! ! constants . lonaxisSpan [ projType ] ;
39+ var isScoped = ( geoLayoutOut . _isScoped = scope !== 'world' ) ;
40+ var isSatellite = ( geoLayoutOut . _isSatellite = projType === 'satellite' ) ;
41+ var isConic = ( geoLayoutOut . _isConic = projType . indexOf ( 'conic' ) !== - 1 || projType === 'albers' ) ;
42+ var isClipped = ( geoLayoutOut . _isClipped = ! ! constants . lonaxisSpan [ projType ] ) ;
4143
42- if ( geoLayoutIn . visible === false ) {
44+ if ( geoLayoutIn . visible === false ) {
4345 // should override template.layout.geo.show* - see issue 4482
4446
4547 // make a copy
@@ -54,29 +56,26 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
5456 newTemplate . showocean = false ;
5557 newTemplate . showrivers = false ;
5658 newTemplate . showsubunits = false ;
57- if ( newTemplate . lonaxis ) newTemplate . lonaxis . showgrid = false ;
58- if ( newTemplate . lataxis ) newTemplate . lataxis . showgrid = false ;
59+ if ( newTemplate . lonaxis ) newTemplate . lonaxis . showgrid = false ;
60+ if ( newTemplate . lataxis ) newTemplate . lataxis . showgrid = false ;
5961
6062 // set ref to copy
6163 geoLayoutOut . _template = newTemplate ;
6264 }
6365 var visible = coerce ( 'visible' ) ;
6466
6567 var show ;
66- for ( var i = 0 ; i < axesNames . length ; i ++ ) {
68+ for ( var i = 0 ; i < axesNames . length ; i ++ ) {
6769 var axisName = axesNames [ i ] ;
6870 var dtickDflt = [ 30 , 10 ] [ i ] ;
6971 var rangeDflt ;
7072
71- if ( isScoped ) {
73+ if ( isScoped ) {
7274 rangeDflt = scopeParams [ axisName + 'Range' ] ;
7375 } else {
7476 var dfltSpans = constants [ axisName + 'Span' ] ;
7577 var hSpan = ( dfltSpans [ projType ] || dfltSpans [ '*' ] ) / 2 ;
76- var rot = coerce (
77- 'projection.rotation.' + axisName . slice ( 0 , 3 ) ,
78- scopeParams . projRotate [ i ]
79- ) ;
78+ var rot = coerce ( 'projection.rotation.' + axisName . slice ( 0 , 3 ) , scopeParams . projRotate [ i ] ) ;
8079 rangeDflt = [ rot - hSpan , rot + hSpan ] ;
8180 }
8281
@@ -85,7 +84,7 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
8584 coerce ( axisName + '.dtick' , dtickDflt ) ;
8685
8786 show = coerce ( axisName + '.showgrid' , ! visible ? false : undefined ) ;
88- if ( show ) {
87+ if ( show ) {
8988 coerce ( axisName + '.gridcolor' ) ;
9089 coerce ( axisName + '.gridwidth' ) ;
9190 coerce ( axisName + '.griddash' ) ;
@@ -114,27 +113,27 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
114113 var centerLon = ( lon0 + lon1 ) / 2 ;
115114 var projLon ;
116115
117- if ( ! isAlbersUsa ) {
116+ if ( ! isAlbersUsa ) {
118117 var dfltProjRotate = isScoped ? scopeParams . projRotate : [ centerLon , 0 , 0 ] ;
119118
120119 projLon = coerce ( 'projection.rotation.lon' , dfltProjRotate [ 0 ] ) ;
121120 coerce ( 'projection.rotation.lat' , dfltProjRotate [ 1 ] ) ;
122121 coerce ( 'projection.rotation.roll' , dfltProjRotate [ 2 ] ) ;
123122
124123 show = coerce ( 'showcoastlines' , ! isScoped && visible ) ;
125- if ( show ) {
124+ if ( show ) {
126125 coerce ( 'coastlinecolor' ) ;
127126 coerce ( 'coastlinewidth' ) ;
128127 }
129128
130129 show = coerce ( 'showocean' , ! visible ? false : undefined ) ;
131- if ( show ) coerce ( 'oceancolor' ) ;
130+ if ( show ) coerce ( 'oceancolor' ) ;
132131 }
133132
134133 var centerLonDflt ;
135134 var centerLatDflt ;
136135
137- if ( isAlbersUsa ) {
136+ if ( isAlbersUsa ) {
138137 // 'albers usa' does not have a 'center',
139138 // these values were found using via:
140139 // projection.invert([geoLayout.center.lon, geoLayoutIn.center.lat])
@@ -148,12 +147,12 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
148147 coerce ( 'center.lon' , centerLonDflt ) ;
149148 coerce ( 'center.lat' , centerLatDflt ) ;
150149
151- if ( isSatellite ) {
150+ if ( isSatellite ) {
152151 coerce ( 'projection.tilt' ) ;
153152 coerce ( 'projection.distance' ) ;
154153 }
155154
156- if ( isConic ) {
155+ if ( isConic ) {
157156 var dfltProjParallels = scopeParams . projParallels || [ 0 , 60 ] ;
158157 coerce ( 'projection.parallels' , dfltProjParallels ) ;
159158 }
@@ -163,24 +162,24 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
163162 coerce ( 'projection.maxscale' ) ;
164163
165164 show = coerce ( 'showland' , ! visible ? false : undefined ) ;
166- if ( show ) coerce ( 'landcolor' ) ;
165+ if ( show ) coerce ( 'landcolor' ) ;
167166
168167 show = coerce ( 'showlakes' , ! visible ? false : undefined ) ;
169- if ( show ) coerce ( 'lakecolor' ) ;
168+ if ( show ) coerce ( 'lakecolor' ) ;
170169
171170 show = coerce ( 'showrivers' , ! visible ? false : undefined ) ;
172- if ( show ) {
171+ if ( show ) {
173172 coerce ( 'rivercolor' ) ;
174173 coerce ( 'riverwidth' ) ;
175174 }
176175
177176 show = coerce ( 'showcountries' , isScoped && scope !== 'usa' && visible ) ;
178- if ( show ) {
177+ if ( show ) {
179178 coerce ( 'countrycolor' ) ;
180179 coerce ( 'countrywidth' ) ;
181180 }
182181
183- if ( scope === 'usa' || ( scope === 'north america' && resolution === 50 ) ) {
182+ if ( scope === 'usa' || ( scope === 'north america' && resolution === 50 ) ) {
184183 // Only works for:
185184 // USA states at 110m
186185 // USA states + Canada provinces at 50m
@@ -189,37 +188,61 @@ function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce, opts) {
189188 coerce ( 'subunitwidth' ) ;
190189 }
191190
192- if ( ! isScoped ) {
191+ if ( ! isScoped ) {
193192 // Does not work in non-world scopes
194193 show = coerce ( 'showframe' , visible ) ;
195- if ( show ) {
194+ if ( show ) {
196195 coerce ( 'framecolor' ) ;
197196 coerce ( 'framewidth' ) ;
198197 }
199198 }
200199
201200 coerce ( 'bgcolor' ) ;
202201
203- var fitBounds = coerce ( 'fitbounds' ) ;
204-
205- // clear attributes that will get auto-filled later
206- if ( fitBounds ) {
207- delete geoLayoutOut . projection . scale ;
208-
209- if ( isScoped ) {
210- delete geoLayoutOut . center . lon ;
211- delete geoLayoutOut . center . lat ;
212- } else if ( isClipped ) {
213- delete geoLayoutOut . center . lon ;
214- delete geoLayoutOut . center . lat ;
215- delete geoLayoutOut . projection . rotation . lon ;
216- delete geoLayoutOut . projection . rotation . lat ;
217- delete geoLayoutOut . lonaxis . range ;
218- delete geoLayoutOut . lataxis . range ;
202+ // `fitbounds` updates a selection of view attributes, specific to the projection type.
203+ // Check to see if a user has set any of these. If they have, skip fitbounds. Otherwise
204+ // null out the proper attributes and run the fitting logic.
205+ coerce ( 'fitbounds' ) ;
206+ if ( geoLayoutOut . fitbounds ) {
207+ const centerIn = geoLayoutIn . center || { } ;
208+ const projectionIn = geoLayoutIn . projection || { } ;
209+ const rotationIn = projectionIn . rotation || { } ;
210+ const lonaxisIn = geoLayoutIn . lonaxis || { } ;
211+ const lataxisIn = geoLayoutIn . lataxis || { } ;
212+ // All projection types will set these attributes
213+ const viewAttributes = [
214+ { dst : geoLayoutOut . center , key : 'lon' , src : centerIn } ,
215+ { dst : geoLayoutOut . center , key : 'lat' , src : centerIn } ,
216+ { dst : geoLayoutOut . projection , key : 'scale' , src : projectionIn }
217+ ] ;
218+ // Branch order is important because scoped projections can also be clipped,
219+ // but these should be treated as scoped below
220+ if ( isScoped ) {
221+ // Scoped only sets center, so move on
222+ } else if ( isClipped ) {
223+ viewAttributes . push (
224+ { dst : geoLayoutOut . projection . rotation , key : 'lon' , src : rotationIn } ,
225+ { dst : geoLayoutOut . projection . rotation , key : 'lat' , src : rotationIn } ,
226+ { dst : geoLayoutOut . lonaxis , key : 'range' , src : lonaxisIn } ,
227+ { dst : geoLayoutOut . lataxis , key : 'range' , src : lataxisIn }
228+ ) ;
229+ } else {
230+ viewAttributes . push ( { dst : geoLayoutOut . projection . rotation , key : 'lon' , src : rotationIn } ) ;
231+ }
232+
233+ // Add entries for axis ranges with no `dst` key for non-clipped projections.
234+ // These ranges signal user view-config intent, but non-clipped projections should
235+ // skip the null step because it would break the fit calc.
236+ if ( ! isClipped ) {
237+ viewAttributes . push ( { key : 'range' , src : lonaxisIn } , { key : 'range' , src : lataxisIn } ) ;
238+ }
239+ const hasUserView = viewAttributes . some ( ( { src, key } ) => src [ key ] != null ) ; // Use loose comparison so null/undefined count as unset
240+ if ( hasUserView || constants . fitboundsIncompatible . has ( projType ) ) {
241+ geoLayoutOut . fitbounds = false ;
219242 } else {
220- delete geoLayoutOut . center . lon ;
221- delete geoLayoutOut . center . lat ;
222- delete geoLayoutOut . projection . rotation . lon ;
243+ // Set auto-filled view attributes to null so updateProjection can
244+ // compute the fit from scratch and fullLayout matches user input
245+ viewAttributes . forEach ( ( { dst , key } ) => dst && ( dst [ key ] = null ) ) ;
223246 }
224247 }
225248}
0 commit comments