matplotlib-map-utils 2.0.2__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matplotlib_map_utils/__init__.py +16 -1
- matplotlib_map_utils/core/__init__.py +5 -1
- matplotlib_map_utils/core/inset_map.py +952 -0
- matplotlib_map_utils/core/north_arrow.py +0 -4
- matplotlib_map_utils/core/scale_bar.py +0 -6
- matplotlib_map_utils/defaults/inset_map.py +67 -0
- matplotlib_map_utils/utils/__init__.py +3 -0
- matplotlib_map_utils/utils/usa.json +1038 -0
- matplotlib_map_utils/utils/usa.py +336 -0
- matplotlib_map_utils/validation/functions.py +51 -15
- matplotlib_map_utils/validation/inset_map.py +88 -0
- matplotlib_map_utils/validation/north_arrow.py +1 -1
- matplotlib_map_utils/validation/scale_bar.py +3 -5
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info}/METADATA +163 -19
- matplotlib_map_utils-3.0.0.dist-info/RECORD +24 -0
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info}/WHEEL +1 -1
- matplotlib_map_utils-2.0.2.dist-info/RECORD +0 -18
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info/licenses}/LICENSE +0 -0
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,952 @@
|
|
1
|
+
############################################################
|
2
|
+
# inset_map.py contains all the main objects and functions
|
3
|
+
# for creating the inset map axis and its indicators
|
4
|
+
############################################################
|
5
|
+
|
6
|
+
### IMPORTING PACKAGES ###
|
7
|
+
|
8
|
+
# Default packages
|
9
|
+
import warnings
|
10
|
+
import copy
|
11
|
+
# Math packages
|
12
|
+
import numpy
|
13
|
+
# Geo packages
|
14
|
+
import pyproj
|
15
|
+
import shapely
|
16
|
+
# Graphical packages
|
17
|
+
import matplotlib
|
18
|
+
import matplotlib.artist
|
19
|
+
import matplotlib.patches
|
20
|
+
import matplotlib.colors
|
21
|
+
# The types we use in this script
|
22
|
+
from typing import Literal
|
23
|
+
# The information contained in our helper scripts (validation and defaults)
|
24
|
+
from ..defaults import inset_map as imd
|
25
|
+
from ..validation import inset_map as imt
|
26
|
+
from ..validation import functions as imf
|
27
|
+
|
28
|
+
### INITIALIZATION ###
|
29
|
+
|
30
|
+
# Setting the defaults to the "medium" size, which is roughly optimized for A4/Letter paper
|
31
|
+
# Making these as globals is important for the set_size() function to work later
|
32
|
+
_DEFAULT_INSET_MAP = imd._DEFAULTS_IM["md"][0]
|
33
|
+
|
34
|
+
### CLASSES ###
|
35
|
+
# Note these are really just to be convenient when storing the
|
36
|
+
# configuration options that are used by the drawing functions instead
|
37
|
+
|
38
|
+
# The main object model of the inset map
|
39
|
+
class InsetMap(matplotlib.artist.Artist):
|
40
|
+
|
41
|
+
## INITIALIZATION ##
|
42
|
+
def __init__(self,
|
43
|
+
location: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]="lower left",
|
44
|
+
size: imt._TYPE_INSET["size"]=None,
|
45
|
+
pad: imt._TYPE_INSET["pad"]=None,
|
46
|
+
coords: imt._TYPE_INSET["coords"]=None,
|
47
|
+
transform=None,
|
48
|
+
to_plot=None,
|
49
|
+
**kwargs):
|
50
|
+
# Starting up the object with the base properties of a matplotlib Artist
|
51
|
+
matplotlib.artist.Artist.__init__(self)
|
52
|
+
|
53
|
+
# Validating each of the passed parameters
|
54
|
+
self._location = imf._validate(imt._VALIDATE_INSET, "location", location)
|
55
|
+
self._size = imf._validate(imt._VALIDATE_INSET, "size", size)
|
56
|
+
self._pad = imf._validate(imt._VALIDATE_INSET, "pad", pad)
|
57
|
+
self._coords = imf._validate(imt._VALIDATE_INSET, "coords", coords)
|
58
|
+
self._to_plot = imf._validate(imt._VALIDATE_INSET, "to_plot", to_plot)
|
59
|
+
|
60
|
+
# Checking if we need to override values for size and pad
|
61
|
+
if self._size is None:
|
62
|
+
self._size = _DEFAULT_INSET_MAP["size"]
|
63
|
+
if self._pad is None:
|
64
|
+
self._pad = _DEFAULT_INSET_MAP["pad"]
|
65
|
+
|
66
|
+
self._transform = transform # not validated!
|
67
|
+
self._kwargs = kwargs # not validated!
|
68
|
+
|
69
|
+
# We do set the zorder for our objects individually,
|
70
|
+
# but we ALSO set it for the entire artist, here
|
71
|
+
# Thank you to matplotlib-scalebar for this tip
|
72
|
+
zorder = 99
|
73
|
+
|
74
|
+
## INTERNAL PROPERTIES ##
|
75
|
+
# This allows for easy-updating of properties
|
76
|
+
# Each property will have the same pair of functions
|
77
|
+
# 1) calling the property itself returns its value (InsetMap.size will output (width,height))
|
78
|
+
# 2) passing a value will update it (InsetMap.size = (width,height) will update it)
|
79
|
+
|
80
|
+
# location/loc
|
81
|
+
@property
|
82
|
+
def location(self):
|
83
|
+
return self._location
|
84
|
+
|
85
|
+
@location.setter
|
86
|
+
def location(self, val: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]):
|
87
|
+
val = imf._validate(imt._VALIDATE_INSET, "location", val)
|
88
|
+
self._location = val
|
89
|
+
|
90
|
+
@property
|
91
|
+
def loc(self):
|
92
|
+
return self._location
|
93
|
+
|
94
|
+
@loc.setter
|
95
|
+
def loc(self, val: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]):
|
96
|
+
val = imf._validate(imt._VALIDATE_INSET, "location", val)
|
97
|
+
self._location = val
|
98
|
+
|
99
|
+
# size
|
100
|
+
@property
|
101
|
+
def size(self):
|
102
|
+
return self._size
|
103
|
+
|
104
|
+
@size.setter
|
105
|
+
def size(self, val):
|
106
|
+
val = imf._validate(imt._VALIDATE_INSET, "size", val)
|
107
|
+
if val is not None:
|
108
|
+
self._size = val
|
109
|
+
else:
|
110
|
+
self._size = _DEFAULT_INSET_MAP["size"]
|
111
|
+
|
112
|
+
# pad
|
113
|
+
@property
|
114
|
+
def pad(self):
|
115
|
+
return self._pad
|
116
|
+
|
117
|
+
@pad.setter
|
118
|
+
def pad(self, val):
|
119
|
+
val = imf._validate(imt._VALIDATE_INSET, "pad", val)
|
120
|
+
if val is not None:
|
121
|
+
self._pad = val
|
122
|
+
else:
|
123
|
+
self._pad = _DEFAULT_INSET_MAP["pad"]
|
124
|
+
|
125
|
+
# coords
|
126
|
+
@property
|
127
|
+
def coords(self):
|
128
|
+
return self._coords
|
129
|
+
|
130
|
+
@coords.setter
|
131
|
+
def coords(self, val):
|
132
|
+
val = imf._validate(imt._VALIDATE_INSET, "coords", val)
|
133
|
+
self._coords = val
|
134
|
+
|
135
|
+
# transform
|
136
|
+
@property
|
137
|
+
def transform(self):
|
138
|
+
return self._transform
|
139
|
+
|
140
|
+
@transform.setter
|
141
|
+
def transform(self, val):
|
142
|
+
self._transform = val
|
143
|
+
|
144
|
+
# kwargs
|
145
|
+
@property
|
146
|
+
def kwargs(self):
|
147
|
+
return self._kwargs
|
148
|
+
|
149
|
+
@kwargs.setter
|
150
|
+
def kwargs(self, val):
|
151
|
+
if isinstance(val, dict):
|
152
|
+
self._kwargs = self._kwargs | val
|
153
|
+
else:
|
154
|
+
raise ValueError("kwargs expects a dictionary, please try again")
|
155
|
+
|
156
|
+
# to_plot
|
157
|
+
@property
|
158
|
+
def to_plot(self):
|
159
|
+
return self._to_plot
|
160
|
+
|
161
|
+
@to_plot.setter
|
162
|
+
def to_plot(self, val):
|
163
|
+
val = imf._validate(imt._VALIDATE_INSET, "to_plot", val)
|
164
|
+
self._to_plot = val
|
165
|
+
|
166
|
+
## COPY FUNCTION ##
|
167
|
+
# This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
|
168
|
+
# Instead, you can use add_artist() like normal, but with add_artist(na.copy())
|
169
|
+
# Thank you to the cartopy team for helping fix a bug with this!
|
170
|
+
def copy(self):
|
171
|
+
return copy.deepcopy(self)
|
172
|
+
|
173
|
+
## CREATE FUNCTION ##
|
174
|
+
# Calling InsetMap.create(ax) will create an inset map with the specified parameters on the given axis
|
175
|
+
# Note that this is different than the way NorthArrows and ScaleBars are rendered (via draw/add_artist())!
|
176
|
+
def create(self, pax, **kwargs):
|
177
|
+
# Can re-use the drawing function we already established, but return the object instead
|
178
|
+
iax = inset_map(ax=pax, location=self._location, size=self._size,
|
179
|
+
pad=self._pad, coords=self._coords, transform=self._transform,
|
180
|
+
**self._kwargs, **kwargs)
|
181
|
+
|
182
|
+
# If data is passed to to_plot, then we plot that on the newly created axis as well
|
183
|
+
for d in self._to_plot:
|
184
|
+
if d is not None:
|
185
|
+
if "kwargs" in d.keys():
|
186
|
+
d["data"].plot(ax=iax, **d["kwargs"])
|
187
|
+
else:
|
188
|
+
d["data"].plot(ax=iax)
|
189
|
+
# Instead of "drawing", we have to return the axis, for further manipulation
|
190
|
+
return iax
|
191
|
+
|
192
|
+
## SIZE FUNCTION ##
|
193
|
+
# This function will update the default dictionaries used based on the size of map being created
|
194
|
+
# See defaults.py for more information on the dictionaries used here
|
195
|
+
def set_size(size: Literal["xs","xsmall","x-small",
|
196
|
+
"sm","small",
|
197
|
+
"md","medium",
|
198
|
+
"lg","large",
|
199
|
+
"xl","xlarge","x-large"]):
|
200
|
+
# Bringing in our global default values to update them
|
201
|
+
global _DEFAULT_INSET_MAP
|
202
|
+
# Changing the global default values as required
|
203
|
+
if size.lower() in ["xs","xsmall","x-small"]:
|
204
|
+
_DEFAULT_INSET_MAP = imd._DEFAULTS_IM["xs"][0]
|
205
|
+
elif size.lower() in ["sm","small"]:
|
206
|
+
_DEFAULT_INSET_MAP = imd._DEFAULTS_IM["sm"][0]
|
207
|
+
elif size.lower() in ["md","medium"]:
|
208
|
+
_DEFAULT_INSET_MAP = imd._DEFAULTS_IM["md"][0]
|
209
|
+
elif size.lower() in ["lg","large"]:
|
210
|
+
_DEFAULT_INSET_MAP = imd._DEFAULTS_IM["lg"][0]
|
211
|
+
elif size.lower() in ["xl","xlarge","x-large"]:
|
212
|
+
_DEFAULT_INSET_MAP = imd._DEFAULTS_IM["xl"][0]
|
213
|
+
else:
|
214
|
+
raise ValueError("Invalid value supplied, try one of ['xsmall', 'small', 'medium', 'large', 'xlarge'] instead")
|
215
|
+
|
216
|
+
# The main object model of the extent indicator
|
217
|
+
class ExtentIndicator(matplotlib.artist.Artist):
|
218
|
+
|
219
|
+
## INITIALIZATION ##
|
220
|
+
def __init__(self,
|
221
|
+
to_return: imt._TYPE_EXTENT["to_return"]=None,
|
222
|
+
straighten: imt._TYPE_EXTENT["straighten"]=True,
|
223
|
+
pad: imt._TYPE_EXTENT["pad"]=0.05,
|
224
|
+
plot: imt._TYPE_EXTENT["plot"]=True,
|
225
|
+
facecolor: imt._TYPE_EXTENT["facecolor"]="red",
|
226
|
+
linecolor: imt._TYPE_EXTENT["linecolor"]="red",
|
227
|
+
alpha: imt._TYPE_EXTENT["alpha"]=0.5,
|
228
|
+
linewidth: imt._TYPE_EXTENT["linewidth"]=1,
|
229
|
+
**kwargs):
|
230
|
+
# Starting up the object with the base properties of a matplotlib Artist
|
231
|
+
matplotlib.artist.Artist.__init__(self)
|
232
|
+
|
233
|
+
# Validating each of the passed parameters
|
234
|
+
self._to_return = imf._validate(imt._VALIDATE_EXTENT, "to_return", to_return)
|
235
|
+
self._straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
|
236
|
+
self._pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
|
237
|
+
self._plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
|
238
|
+
self._facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
|
239
|
+
self._linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
|
240
|
+
self._alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
|
241
|
+
self._linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
|
242
|
+
|
243
|
+
self._kwargs = kwargs # not validated!
|
244
|
+
|
245
|
+
# We do set the zorder for our objects individually,
|
246
|
+
# but we ALSO set it for the entire artist, here
|
247
|
+
# Thank you to matplotlib-scalebar for this tip
|
248
|
+
zorder = 99
|
249
|
+
|
250
|
+
## INTERNAL PROPERTIES ##
|
251
|
+
# This allows for easy-updating of properties
|
252
|
+
# Each property will have the same pair of functions
|
253
|
+
# 1) calling the property itself returns its value (ExtentIndicator.facecolor will output color)
|
254
|
+
# 2) passing a value will update it (ExtentIndicator.facecolor = color will update it)
|
255
|
+
|
256
|
+
# to_return
|
257
|
+
@property
|
258
|
+
def to_return(self):
|
259
|
+
return self._to_return
|
260
|
+
|
261
|
+
@to_return.setter
|
262
|
+
def to_return(self, val):
|
263
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "to_return", val)
|
264
|
+
self._to_return = val
|
265
|
+
|
266
|
+
# straighten
|
267
|
+
@property
|
268
|
+
def straighten(self):
|
269
|
+
return self._straighten
|
270
|
+
|
271
|
+
@straighten.setter
|
272
|
+
def straighten(self, val):
|
273
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "straighten", val)
|
274
|
+
self._straighten = val
|
275
|
+
|
276
|
+
# pad
|
277
|
+
@property
|
278
|
+
def pad(self):
|
279
|
+
return self._pad
|
280
|
+
|
281
|
+
@pad.setter
|
282
|
+
def pad(self, val):
|
283
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "pad", val)
|
284
|
+
self._pad = val
|
285
|
+
|
286
|
+
# plot
|
287
|
+
@property
|
288
|
+
def plot(self):
|
289
|
+
return self._plot
|
290
|
+
|
291
|
+
@plot.setter
|
292
|
+
def plot(self, val):
|
293
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "plot", val)
|
294
|
+
self._plot = val
|
295
|
+
|
296
|
+
# facecolor
|
297
|
+
@property
|
298
|
+
def facecolor(self):
|
299
|
+
return self._facecolor
|
300
|
+
|
301
|
+
@facecolor.setter
|
302
|
+
def facecolor(self, val):
|
303
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "facecolor", val)
|
304
|
+
self._facecolor = val
|
305
|
+
|
306
|
+
# linecolor
|
307
|
+
@property
|
308
|
+
def linecolor(self):
|
309
|
+
return self._linecolor
|
310
|
+
|
311
|
+
@linecolor.setter
|
312
|
+
def linecolor(self, val):
|
313
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "linecolor", val)
|
314
|
+
self._linecolor = val
|
315
|
+
|
316
|
+
# alpha
|
317
|
+
@property
|
318
|
+
def alpha(self):
|
319
|
+
return self._alpha
|
320
|
+
|
321
|
+
@alpha.setter
|
322
|
+
def alpha(self, val):
|
323
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "alpha", val)
|
324
|
+
self._alpha = val
|
325
|
+
|
326
|
+
# linewidth
|
327
|
+
@property
|
328
|
+
def linewidth(self):
|
329
|
+
return self._linewidth
|
330
|
+
|
331
|
+
@linewidth.setter
|
332
|
+
def linewidth(self, val):
|
333
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "linewidth", val)
|
334
|
+
self._linewidth = val
|
335
|
+
|
336
|
+
# kwargs
|
337
|
+
@property
|
338
|
+
def kwargs(self):
|
339
|
+
return self._kwargs
|
340
|
+
|
341
|
+
@kwargs.setter
|
342
|
+
def kwargs(self, val):
|
343
|
+
if isinstance(val, dict):
|
344
|
+
self._kwargs = self._kwargs | val
|
345
|
+
else:
|
346
|
+
raise ValueError("kwargs expects a dictionary, please try again")
|
347
|
+
|
348
|
+
## COPY FUNCTION ##
|
349
|
+
# This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
|
350
|
+
# Instead, you can use add_artist() like normal, but with add_artist(na.copy())
|
351
|
+
# Thank you to the cartopy team for helping fix a bug with this!
|
352
|
+
def copy(self):
|
353
|
+
return copy.deepcopy(self)
|
354
|
+
|
355
|
+
## CREATE FUNCTION ##
|
356
|
+
# Calling ExtentIndicator.create(ax) will create an inset map with the specified parameters on the given axis
|
357
|
+
# Note that this is different than the way NorthArrows and ScaleBars are rendered (via draw/add_artist())!
|
358
|
+
def create(self,
|
359
|
+
pax: imt._TYPE_EXTENT["pax"],
|
360
|
+
bax: imt._TYPE_EXTENT["bax"],
|
361
|
+
pcrs: imt._TYPE_EXTENT["pcrs"],
|
362
|
+
bcrs: imt._TYPE_EXTENT["bcrs"], **kwargs):
|
363
|
+
|
364
|
+
# Can re-use the drawing function we already established, but return the object instead
|
365
|
+
exi = indicate_extent(pax=pax, bax=bax, pcrs=pcrs, bcrs=bcrs,
|
366
|
+
to_return=self._to_return, straighten=self._straighten,
|
367
|
+
pad=self._pad, plot=self._plot,
|
368
|
+
facecolor=self._facecolor, linecolor=self._linecolor,
|
369
|
+
alpha=self._alpha, linewidth=self._linewidth,
|
370
|
+
**self._kwargs, **kwargs)
|
371
|
+
|
372
|
+
# The indicator will be drawn automatically if plot is True
|
373
|
+
# If we have anything to return from to_return, we will do so here
|
374
|
+
if exi is not None:
|
375
|
+
return exi
|
376
|
+
|
377
|
+
# The main object model of the detail indicator
|
378
|
+
class DetailIndicator(matplotlib.artist.Artist):
|
379
|
+
|
380
|
+
## INITIALIZATION ##
|
381
|
+
def __init__(self,
|
382
|
+
to_return: imt._TYPE_DETAIL["to_return"]=None,
|
383
|
+
straighten: imt._TYPE_EXTENT["straighten"]=True,
|
384
|
+
pad: imt._TYPE_EXTENT["pad"]=0.05,
|
385
|
+
plot: imt._TYPE_EXTENT["plot"]=True,
|
386
|
+
facecolor: imt._TYPE_EXTENT["facecolor"]="none",
|
387
|
+
linecolor: imt._TYPE_EXTENT["linecolor"]="black",
|
388
|
+
alpha: imt._TYPE_EXTENT["alpha"]=1,
|
389
|
+
linewidth: imt._TYPE_EXTENT["linewidth"]=1,
|
390
|
+
connector_color: imt._TYPE_DETAIL["connector_color"]="black",
|
391
|
+
connector_width: imt._TYPE_DETAIL["connector_width"]=1,
|
392
|
+
**kwargs):
|
393
|
+
# Starting up the object with the base properties of a matplotlib Artist
|
394
|
+
matplotlib.artist.Artist.__init__(self)
|
395
|
+
|
396
|
+
# Validating each of the passed parameters
|
397
|
+
self._straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
|
398
|
+
self._pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
|
399
|
+
self._plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
|
400
|
+
self._facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
|
401
|
+
self._linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
|
402
|
+
self._alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
|
403
|
+
self._linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
|
404
|
+
self._to_return = imf._validate(imt._VALIDATE_DETAIL, "to_return", to_return)
|
405
|
+
self._connector_color = imf._validate(imt._VALIDATE_DETAIL, "connector_color", connector_color)
|
406
|
+
self._connector_width = imf._validate(imt._VALIDATE_DETAIL, "connector_width", connector_width)
|
407
|
+
|
408
|
+
self._kwargs = kwargs # not validated!
|
409
|
+
|
410
|
+
# We do set the zorder for our objects individually,
|
411
|
+
# but we ALSO set it for the entire artist, here
|
412
|
+
# Thank you to matplotlib-scalebar for this tip
|
413
|
+
zorder = 99
|
414
|
+
|
415
|
+
## INTERNAL PROPERTIES ##
|
416
|
+
# This allows for easy-updating of properties
|
417
|
+
# Each property will have the same pair of functions
|
418
|
+
# 1) calling the property itself returns its value (DetailIndicator.facecolor will output color)
|
419
|
+
# 2) passing a value will update it (DetailIndicator.facecolor = color will update it)
|
420
|
+
|
421
|
+
# to_return
|
422
|
+
@property
|
423
|
+
def to_return(self):
|
424
|
+
return self._to_return
|
425
|
+
|
426
|
+
@to_return.setter
|
427
|
+
def to_return(self, val):
|
428
|
+
val = imf._validate(imt._VALIDATE_DETAIL, "to_return", val)
|
429
|
+
self._to_return = val
|
430
|
+
|
431
|
+
# straighten
|
432
|
+
@property
|
433
|
+
def straighten(self):
|
434
|
+
return self._straighten
|
435
|
+
|
436
|
+
@straighten.setter
|
437
|
+
def straighten(self, val):
|
438
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "straighten", val)
|
439
|
+
self._straighten = val
|
440
|
+
|
441
|
+
# pad
|
442
|
+
@property
|
443
|
+
def pad(self):
|
444
|
+
return self._pad
|
445
|
+
|
446
|
+
@pad.setter
|
447
|
+
def pad(self, val):
|
448
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "pad", val)
|
449
|
+
self._pad = val
|
450
|
+
|
451
|
+
# plot
|
452
|
+
@property
|
453
|
+
def plot(self):
|
454
|
+
return self._plot
|
455
|
+
|
456
|
+
@plot.setter
|
457
|
+
def plot(self, val):
|
458
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "plot", val)
|
459
|
+
self._plot = val
|
460
|
+
|
461
|
+
# facecolor
|
462
|
+
@property
|
463
|
+
def facecolor(self):
|
464
|
+
return self._facecolor
|
465
|
+
|
466
|
+
@facecolor.setter
|
467
|
+
def facecolor(self, val):
|
468
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "facecolor", val)
|
469
|
+
self._facecolor = val
|
470
|
+
|
471
|
+
# linecolor
|
472
|
+
@property
|
473
|
+
def linecolor(self):
|
474
|
+
return self._linecolor
|
475
|
+
|
476
|
+
@linecolor.setter
|
477
|
+
def linecolor(self, val):
|
478
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "linecolor", val)
|
479
|
+
self._linecolor = val
|
480
|
+
|
481
|
+
# alpha
|
482
|
+
@property
|
483
|
+
def alpha(self):
|
484
|
+
return self._alpha
|
485
|
+
|
486
|
+
@alpha.setter
|
487
|
+
def alpha(self, val):
|
488
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "alpha", val)
|
489
|
+
self._alpha = val
|
490
|
+
|
491
|
+
# linewidth
|
492
|
+
@property
|
493
|
+
def linewidth(self):
|
494
|
+
return self._linewidth
|
495
|
+
|
496
|
+
@linewidth.setter
|
497
|
+
def linewidth(self, val):
|
498
|
+
val = imf._validate(imt._VALIDATE_EXTENT, "linewidth", val)
|
499
|
+
self._linewidth = val
|
500
|
+
|
501
|
+
# connector_color
|
502
|
+
@property
|
503
|
+
def connector_color(self):
|
504
|
+
return self._connector_color
|
505
|
+
|
506
|
+
@connector_color.setter
|
507
|
+
def connector_color(self, val):
|
508
|
+
val = imf._validate(imt._VALIDATE_DETAIL, "connector_color", val)
|
509
|
+
self._connector_color = val
|
510
|
+
|
511
|
+
# connector_width
|
512
|
+
@property
|
513
|
+
def connector_width(self):
|
514
|
+
return self._connector_width
|
515
|
+
|
516
|
+
@connector_width.setter
|
517
|
+
def connector_width(self, val):
|
518
|
+
val = imf._validate(imt._VALIDATE_DETAIL, "connector_width", val)
|
519
|
+
self._connector_width = val
|
520
|
+
|
521
|
+
# kwargs
|
522
|
+
@property
|
523
|
+
def kwargs(self):
|
524
|
+
return self._kwargs
|
525
|
+
|
526
|
+
@kwargs.setter
|
527
|
+
def kwargs(self, val):
|
528
|
+
if isinstance(val, dict):
|
529
|
+
self._kwargs = self._kwargs | val
|
530
|
+
else:
|
531
|
+
raise ValueError("kwargs expects a dictionary, please try again")
|
532
|
+
|
533
|
+
## COPY FUNCTION ##
|
534
|
+
# This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
|
535
|
+
# Instead, you can use add_artist() like normal, but with add_artist(na.copy())
|
536
|
+
# Thank you to the cartopy team for helping fix a bug with this!
|
537
|
+
def copy(self):
|
538
|
+
return copy.deepcopy(self)
|
539
|
+
|
540
|
+
## CREATE FUNCTION ##
|
541
|
+
# Calling DetailIndicator.create(ax) will create an inset map with the specified parameters on the given axis
|
542
|
+
# Note that this is different than the way NorthArrows and ScaleBars are rendered (via draw/add_artist())!
|
543
|
+
def create(self,
|
544
|
+
pax: imt._TYPE_EXTENT["pax"],
|
545
|
+
iax: imt._TYPE_EXTENT["bax"],
|
546
|
+
pcrs: imt._TYPE_EXTENT["pcrs"],
|
547
|
+
icrs: imt._TYPE_EXTENT["bcrs"], **kwargs):
|
548
|
+
|
549
|
+
# Can re-use the drawing function we already established, but return the object instead
|
550
|
+
dti = indicate_detail(pax=pax, iax=iax, pcrs=pcrs, icrs=icrs,
|
551
|
+
to_return=self._to_return, straighten=self._straighten,
|
552
|
+
pad=self._pad, plot=self._plot,
|
553
|
+
facecolor=self._facecolor, linecolor=self._linecolor,
|
554
|
+
alpha=self._alpha, linewidth=self._linewidth,
|
555
|
+
connector_color=self._connector_color,
|
556
|
+
connector_width=self._connector_width,
|
557
|
+
**self._kwargs, **kwargs)
|
558
|
+
|
559
|
+
# The indicator will be drawn automatically if plot is True
|
560
|
+
# If we have anything to return from to_return, we will do so here
|
561
|
+
if dti is not None:
|
562
|
+
return dti
|
563
|
+
|
564
|
+
### DRAWING FUNCTIONS ###
|
565
|
+
# See here for doc: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.inset_axes.html
|
566
|
+
# See here for kwargs: https://matplotlib.org/stable/api/_as_gen/mpl_toolkits.axes_grid1.inset_locator.inset_axes.html#mpl_toolkits.axes_grid1.inset_locator.inset_axes
|
567
|
+
|
568
|
+
# Function for creating an inset map, independent of the InsetMap object model
|
569
|
+
# It is intended to be an easier-to-use API than the default inset_axes
|
570
|
+
def inset_map(ax,
|
571
|
+
location: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]="upper right",
|
572
|
+
size: imt._TYPE_INSET["size"]=None,
|
573
|
+
pad: imt._TYPE_INSET["pad"]=None,
|
574
|
+
coords: imt._TYPE_INSET["coords"]=None,
|
575
|
+
transform=None,
|
576
|
+
**kwargs):
|
577
|
+
|
578
|
+
## VALIDATION ##
|
579
|
+
location = imf._validate(imt._VALIDATE_INSET, "location", location)
|
580
|
+
size = imf._validate(imt._VALIDATE_INSET, "size", size)
|
581
|
+
pad = imf._validate(imt._VALIDATE_INSET, "pad", pad)
|
582
|
+
coords = imf._validate(imt._VALIDATE_INSET, "coords", coords)
|
583
|
+
|
584
|
+
if size is None:
|
585
|
+
size = _DEFAULT_INSET_MAP["size"]
|
586
|
+
if pad is None:
|
587
|
+
pad = _DEFAULT_INSET_MAP["pad"]
|
588
|
+
|
589
|
+
## SET-UP ##
|
590
|
+
# Getting the figure
|
591
|
+
fig = ax.get_figure()
|
592
|
+
|
593
|
+
## SIZE ##
|
594
|
+
# Setting the desired dimensions of the inset map
|
595
|
+
# The default inset_axis() function does this as a fraction of the parent axis
|
596
|
+
# But the size variable expects dimensions in inches
|
597
|
+
|
598
|
+
# Casting size to width and height
|
599
|
+
if isinstance(size, (tuple, list)):
|
600
|
+
inset_width, inset_height = size
|
601
|
+
else:
|
602
|
+
inset_width = size
|
603
|
+
inset_height = size
|
604
|
+
|
605
|
+
## PADDING ##
|
606
|
+
# Padding is expressed in inches here, unlike traditional matplotlib
|
607
|
+
# which expresses it as a fraction of the font size
|
608
|
+
if isinstance(pad, (tuple, list)):
|
609
|
+
pad_x, pad_y = pad
|
610
|
+
else:
|
611
|
+
pad_x = pad
|
612
|
+
pad_y = pad
|
613
|
+
|
614
|
+
## RESIZING ##
|
615
|
+
# Getting the current dimensions of the parent axis in inches (ignoring ticks and labels - just the axis itself)
|
616
|
+
parent_axis_bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
617
|
+
|
618
|
+
# Expressing the desired height and width of the inset map as a fraction of the parent
|
619
|
+
# We do this because, later on, we're assuming everything is in ax.transAxes
|
620
|
+
# which ranges from 0 to 1, as a fraction of the parent axis
|
621
|
+
inset_width = inset_width / parent_axis_bbox.width
|
622
|
+
inset_height = inset_height / parent_axis_bbox.height
|
623
|
+
# and doing the same for the padding
|
624
|
+
pad_x = pad_x / parent_axis_bbox.width
|
625
|
+
pad_y = pad_y / parent_axis_bbox.height
|
626
|
+
|
627
|
+
## PLACEMENT ##
|
628
|
+
# Calculating the start coordinate (which is always the bottom-left corner) of the inset map
|
629
|
+
# based on the desired location, padding (inches) from the side, and the height and width of the inset map
|
630
|
+
if coords is None:
|
631
|
+
# First, the x coordinate
|
632
|
+
if location in ["upper left", "center left", "lower left"]:
|
633
|
+
x = pad_x
|
634
|
+
elif location in ["upper center", "center", "lower center"]:
|
635
|
+
x = (1 - (inset_width + pad_x)) / 2
|
636
|
+
elif location in ["upper right", "center right", "lower right"]:
|
637
|
+
x = 1 - (inset_width + pad_x)
|
638
|
+
# Then the y coordinate
|
639
|
+
if location in ["upper left", "upper center", "upper right"]:
|
640
|
+
y = 1 - (inset_height + pad_y)
|
641
|
+
elif location in ["center left", "center", "center right"]:
|
642
|
+
y = (1 - (inset_height + pad_y)) / 2
|
643
|
+
elif location in ["lower left", "lower center", "lower right"]:
|
644
|
+
y = pad_y
|
645
|
+
# If coordinates are passed, calculate references with respect to that
|
646
|
+
# NOTE: in this case, padding is ignored!
|
647
|
+
else:
|
648
|
+
# Transforming the passed coordinates to transAxes coordinates
|
649
|
+
if transform is not None and transform != ax.transAxes:
|
650
|
+
# Coords needs to be x,y
|
651
|
+
coords = transform.transform(coords) # Transforming the coordinates to display units
|
652
|
+
coords = ax.transAxes.inverted().transform(coords) # Converting back to ax.transAxes
|
653
|
+
# Now coords can be treated as basically an offset value
|
654
|
+
# Here, we use inset_width and inset_height, because we are expressing everything in relative axis units
|
655
|
+
# That's what the transformations were for!
|
656
|
+
# First, the x coordinate
|
657
|
+
if location in ["upper left", "center left", "lower left"]:
|
658
|
+
x = coords[0]
|
659
|
+
elif location in ["upper center", "center", "lower center"]:
|
660
|
+
x = coords[0] - (inset_width / 2)
|
661
|
+
elif location in ["upper right", "center right", "lower right"]:
|
662
|
+
x = coords[0] - inset_width
|
663
|
+
# Then the y coordinate
|
664
|
+
if location in ["upper left", "upper center", "upper right"]:
|
665
|
+
y = coords[1] - inset_height
|
666
|
+
elif location in ["center left", "center", "center right"]:
|
667
|
+
y = coords[1] - (inset_height / 2)
|
668
|
+
elif location in ["lower left", "lower center", "lower right"]:
|
669
|
+
y = coords[1]
|
670
|
+
|
671
|
+
## DRAWING ##
|
672
|
+
# Creating the new inset map with the specified height, width, and location
|
673
|
+
# by default, inset_axes requires everything to be in ax.transAxes coordinates
|
674
|
+
iax = ax.inset_axes([x, y, inset_width, inset_height], **kwargs)
|
675
|
+
|
676
|
+
# We also set the anchor here, such that it stays fixed when any resizing takes place
|
677
|
+
loc_anchors = {
|
678
|
+
"upper left": "NW", "upper center": "N", "upper right": "NE",
|
679
|
+
"center left": "W", "center": "C", "center right": "E",
|
680
|
+
"lower left": "SW", "lower center": "S", "lower right": "SE"
|
681
|
+
}
|
682
|
+
iax.set_anchor(anchor=loc_anchors[location])
|
683
|
+
|
684
|
+
# The new inset axis is returned
|
685
|
+
return iax
|
686
|
+
|
687
|
+
# This function will display the extent/bounds of one axis on the other
|
688
|
+
# This is can be used on an inset map to show where the bounds of the parent axis lay
|
689
|
+
# or, it is called by detail_indicator to show where the bounds of an inset axis lay on the parent
|
690
|
+
# here, PAX means "plotting axis" (where the indicator is plotted)
|
691
|
+
# and BAX means "bounds axis" (where the extent is derived from)
|
692
|
+
def indicate_extent(pax: imt._TYPE_EXTENT["pax"],
|
693
|
+
bax: imt._TYPE_EXTENT["bax"],
|
694
|
+
pcrs: imt._TYPE_EXTENT["pcrs"],
|
695
|
+
bcrs: imt._TYPE_EXTENT["bcrs"],
|
696
|
+
to_return: imt._TYPE_EXTENT["to_return"]=None,
|
697
|
+
straighten: imt._TYPE_EXTENT["straighten"]=True,
|
698
|
+
pad: imt._TYPE_EXTENT["pad"]=0.05,
|
699
|
+
plot: imt._TYPE_EXTENT["plot"]=True,
|
700
|
+
facecolor: imt._TYPE_EXTENT["facecolor"]="red",
|
701
|
+
linecolor: imt._TYPE_EXTENT["linecolor"]="red",
|
702
|
+
alpha: imt._TYPE_EXTENT["alpha"]=0.5,
|
703
|
+
linewidth: imt._TYPE_EXTENT["linewidth"]=1,
|
704
|
+
**kwargs):
|
705
|
+
|
706
|
+
## VALIDATION ##
|
707
|
+
pax = imf._validate(imt._VALIDATE_EXTENT, "pax", pax)
|
708
|
+
bax = imf._validate(imt._VALIDATE_EXTENT, "bax", bax)
|
709
|
+
pcrs = imf._validate(imt._VALIDATE_EXTENT, "pcrs", pcrs)
|
710
|
+
bcrs = imf._validate(imt._VALIDATE_EXTENT, "bcrs", bcrs)
|
711
|
+
to_return = imf._validate(imt._VALIDATE_EXTENT, "to_return", to_return)
|
712
|
+
straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
|
713
|
+
pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
|
714
|
+
plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
|
715
|
+
facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
|
716
|
+
linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
|
717
|
+
alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
|
718
|
+
linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
|
719
|
+
|
720
|
+
# Make sure the figure layout is calculated
|
721
|
+
fig = pax.get_figure()
|
722
|
+
fig.draw_without_rendering()
|
723
|
+
|
724
|
+
# Get the limits of the bounds axis (which will be in its own crs)
|
725
|
+
ymin, ymax = bax.get_ylim()
|
726
|
+
yrange = abs(ymax-ymin)
|
727
|
+
xmin, xmax = bax.get_xlim()
|
728
|
+
xrange = abs(xmax-xmin)
|
729
|
+
|
730
|
+
# Buffering the points, if desired
|
731
|
+
# Note that this is treated as a percentage increase/decrease!
|
732
|
+
if pad is not None and isinstance(pad, (float, int)):
|
733
|
+
pad_data = min(yrange, xrange)*pad
|
734
|
+
else:
|
735
|
+
pad_data = 0
|
736
|
+
|
737
|
+
# Converting it into a tuple of coordinates
|
738
|
+
# in the order of lower-left, upper-left, upper-right, lower-right
|
739
|
+
extent_corners = [(xmin-pad_data, ymin-pad_data), (xmin-pad_data, ymax+pad_data),
|
740
|
+
(xmax+pad_data, ymax+pad_data), (xmax+pad_data, ymin-pad_data)]
|
741
|
+
|
742
|
+
# Converting the points
|
743
|
+
# This is now ready to be plotted on the parent axis, if desired
|
744
|
+
transform_crs = pyproj.Transformer.from_crs(bcrs, pcrs, always_xy=True)
|
745
|
+
extent_points = numpy.array([transform_crs.transform(p[0],p[1]) for p in extent_corners])
|
746
|
+
extent_shape = shapely.Polygon(extent_points)
|
747
|
+
|
748
|
+
# Straightening the points if desired
|
749
|
+
if straighten == True:
|
750
|
+
extent_shape = shapely.envelope(extent_shape)
|
751
|
+
|
752
|
+
# return extent_shape
|
753
|
+
|
754
|
+
# Plotting, if desired
|
755
|
+
if plot == True:
|
756
|
+
# Note that the alpha ONLY applies to the facecolor!
|
757
|
+
extent_patch = matplotlib.patches.Polygon(list(extent_shape.exterior.coords), transform=pax.transData,
|
758
|
+
facecolor=matplotlib.colors.to_rgba(facecolor, alpha=alpha),
|
759
|
+
edgecolor=linecolor, linewidth=linewidth, **kwargs)
|
760
|
+
pax.add_artist(extent_patch)
|
761
|
+
|
762
|
+
# Deciding what we need to return
|
763
|
+
if to_return is None:
|
764
|
+
pass
|
765
|
+
elif to_return == "shape":
|
766
|
+
return extent_shape
|
767
|
+
elif to_return == "patch":
|
768
|
+
return extent_patch
|
769
|
+
elif to_return == "fig":
|
770
|
+
return [fig.transFigure.inverted().transform(pax.transData.transform(p)) for p in list(extent_shape.exterior.coords)[::-1]]
|
771
|
+
elif to_return == "ax":
|
772
|
+
return [pax.transAxes.inverted().transform(pax.transData.transform(p)) for p in list(extent_shape.exterior.coords)[::-1]]
|
773
|
+
else:
|
774
|
+
pass
|
775
|
+
|
776
|
+
# Detail indicators are for when the inset map shows a zoomed-in section of the parent map
|
777
|
+
# This will also, call extent_indicator too, as the two are linked
|
778
|
+
# here, PAX means "parent axis" (where the indicator is plotted)
|
779
|
+
# and IAX means "inset axis" (which contains the detail/zoomed-in section)
|
780
|
+
def indicate_detail(pax: imt._TYPE_EXTENT["pax"],
|
781
|
+
iax: imt._TYPE_EXTENT["bax"],
|
782
|
+
pcrs: imt._TYPE_EXTENT["pcrs"],
|
783
|
+
icrs: imt._TYPE_EXTENT["bcrs"],
|
784
|
+
to_return: imt._TYPE_DETAIL["to_return"]=None,
|
785
|
+
straighten: imt._TYPE_EXTENT["straighten"]=True,
|
786
|
+
pad: imt._TYPE_EXTENT["pad"]=0.05,
|
787
|
+
plot: imt._TYPE_EXTENT["plot"]=True,
|
788
|
+
facecolor: imt._TYPE_EXTENT["facecolor"]="none",
|
789
|
+
alpha: imt._TYPE_EXTENT["alpha"]=1,
|
790
|
+
linecolor: imt._TYPE_EXTENT["linecolor"]="black",
|
791
|
+
linewidth: imt._TYPE_EXTENT["linewidth"]=1,
|
792
|
+
# connector_color: imt._TYPE_DETAIL["connector_color"]="black",
|
793
|
+
# connector_width: imt._TYPE_DETAIL["connector_width"]=1,
|
794
|
+
**kwargs):
|
795
|
+
|
796
|
+
fig = pax.get_figure()
|
797
|
+
fig.draw_without_rendering()
|
798
|
+
|
799
|
+
## VALIDATION ##
|
800
|
+
pax = imf._validate(imt._VALIDATE_EXTENT, "pax", pax)
|
801
|
+
iax = imf._validate(imt._VALIDATE_EXTENT, "bax", iax)
|
802
|
+
pcrs = imf._validate(imt._VALIDATE_EXTENT, "pcrs", pcrs)
|
803
|
+
icrs = imf._validate(imt._VALIDATE_EXTENT, "bcrs", icrs)
|
804
|
+
to_return = imf._validate(imt._VALIDATE_DETAIL, "to_return", to_return)
|
805
|
+
straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
|
806
|
+
pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
|
807
|
+
plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
|
808
|
+
facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
|
809
|
+
alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
|
810
|
+
linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
|
811
|
+
linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
|
812
|
+
# connector_color = imf._validate(imt._VALIDATE_DETAIL, "connector_color", connector_color)
|
813
|
+
# connector_width = imf._validate(imt._VALIDATE_DETAIL, "connector_width", connector_width)
|
814
|
+
|
815
|
+
# Drawing the extent indicator on the main map
|
816
|
+
# Setting to_return="ax" gets us the corners of the patch in pax.transAxes coordinates
|
817
|
+
# We only need the first 4 points - the fifth is a repeated point to enforce "closure"
|
818
|
+
corners_extent = indicate_extent(pax=pax, bax=iax, pcrs=pcrs, bcrs=icrs,
|
819
|
+
straighten=straighten, pad=pad, plot=plot,
|
820
|
+
facecolor=facecolor, linecolor=linecolor,
|
821
|
+
alpha=alpha, linewidth=linewidth*1.25,
|
822
|
+
to_return="ax", **kwargs)[:4]
|
823
|
+
|
824
|
+
# Getting the inset axis points and transforming them to the parent axis CRS
|
825
|
+
corners_inset = _inset_corners_to_parent(pax, iax)
|
826
|
+
|
827
|
+
# Getting the center of both the extent and the inset, which we will then use to decide WHICH lines to draw
|
828
|
+
center_extent_x = sum([p[0] for p in corners_extent]) / len(corners_extent)
|
829
|
+
center_extent_y = sum([p[1] for p in corners_extent]) / len(corners_extent)
|
830
|
+
center_inset_x = sum([p[0] for p in corners_inset]) / len(corners_inset)
|
831
|
+
center_inset_y = sum([p[1] for p in corners_inset]) / len(corners_inset)
|
832
|
+
|
833
|
+
## CONNECTION ##
|
834
|
+
# This part is quite tricky, and involves connecting the inset map to its extent indicator
|
835
|
+
# To do so, we make an educated guess about which corners we need to connect,
|
836
|
+
# based on the relative position of each object
|
837
|
+
|
838
|
+
# If our extent is horizontally centered with our inset, connect just the left or right edges
|
839
|
+
if (abs(center_extent_y - center_inset_y) / abs(center_inset_y)) <= 0.30:
|
840
|
+
if center_extent_x > center_inset_x:
|
841
|
+
# extent lefts + inset rights
|
842
|
+
connections = [[corners_extent[0], corners_inset[3]], [corners_extent[1], corners_inset[2]]]
|
843
|
+
else:
|
844
|
+
# extent rights + inset lefts
|
845
|
+
connections = [[corners_extent[3], corners_inset[0]], [corners_extent[2], corners_inset[1]]]
|
846
|
+
|
847
|
+
# If instead our extent is vertically centered, connect just the top or bottom edges
|
848
|
+
elif (abs(center_extent_x - center_inset_x) / abs(center_inset_x)) <= 0.30:
|
849
|
+
if center_extent_y > center_inset_y:
|
850
|
+
# extent bottoms + inset tops
|
851
|
+
connections = [[corners_extent[0], corners_inset[1]], [corners_extent[3], corners_inset[2]]]
|
852
|
+
else:
|
853
|
+
# extent tops + inset bottoms
|
854
|
+
connections = [[corners_extent[1], corners_inset[0]], [corners_extent[2], corners_inset[3]]]
|
855
|
+
|
856
|
+
# The most common cases will be when the inset is in a corner...
|
857
|
+
elif center_extent_x > center_inset_x and center_extent_y > center_inset_y:
|
858
|
+
# top-left and bottom-right corners for each
|
859
|
+
connections = [[corners_extent[1], corners_inset[1]], [corners_extent[3], corners_inset[3]]]
|
860
|
+
elif center_extent_x <= center_inset_x and center_extent_y > center_inset_y:
|
861
|
+
# top-right and bottom-left corners for each
|
862
|
+
connections = [[corners_extent[2], corners_inset[2]], [corners_extent[0], corners_inset[0]]]
|
863
|
+
elif center_extent_x > center_inset_x and center_extent_y <= center_inset_y:
|
864
|
+
# bottom-left and top-right corners for each
|
865
|
+
connections = [[corners_extent[0], corners_inset[0]], [corners_extent[2], corners_inset[2]]]
|
866
|
+
elif center_extent_x <= center_inset_x and center_extent_y <= center_inset_y:
|
867
|
+
# top-right and bottom-left corners for each
|
868
|
+
connections = [[corners_extent[2], corners_inset[2]], [corners_extent[0], corners_inset[0]]]
|
869
|
+
|
870
|
+
## PLOTTING ##
|
871
|
+
if plot == True:
|
872
|
+
# A manual plot call, to connect the corners to each other
|
873
|
+
for c in connections:
|
874
|
+
# This is listed as [extent_x, inset_x], [extent_y, inset_y]
|
875
|
+
pax.plot([c[0][0], c[1][0]], [c[0][1], c[1][1]],
|
876
|
+
color=linecolor, linewidth=linewidth, transform=pax.transAxes)
|
877
|
+
|
878
|
+
# Also updating the linewidth and color of the inset map itsef, to match
|
879
|
+
for a in ["top","bottom","left","right"]:
|
880
|
+
iax.spines[a].set_linewidth(linewidth*1.2) # making this slightly thicker
|
881
|
+
iax.spines[a].set_edgecolor(linecolor)
|
882
|
+
|
883
|
+
# Returning as requested
|
884
|
+
if to_return is None:
|
885
|
+
pass
|
886
|
+
elif to_return == "connectors" or to_return == "lines":
|
887
|
+
return connections
|
888
|
+
else:
|
889
|
+
pass
|
890
|
+
|
891
|
+
### HELPING FUNCTIONS ###
|
892
|
+
|
893
|
+
# This is a top-level helping function
|
894
|
+
# that will return an axis with inset maps drawn for Alaska, Hawaii, DC, and/or Puerto Rico
|
895
|
+
# NOTE that as of this initial release, it assumes your map is in CRS 3857 for positioning
|
896
|
+
def inset_usa(ax, alaska=True, hawaii=True, dc=True, puerto_rico=True, size=None, pad=None, **kwargs):
|
897
|
+
# This will return all of the axes we create
|
898
|
+
to_return = []
|
899
|
+
|
900
|
+
# Alaska and Hawaii are positioned relative to each other
|
901
|
+
if alaska == True and hawaii == True:
|
902
|
+
aax = inset_map(ax, "lower left", size, pad, **kwargs)
|
903
|
+
to_return.append(aax)
|
904
|
+
# Need to shift over the hawaii axis by the size of the alaska axis
|
905
|
+
# Note that we add xmax and xmin together here, to account for the padding (xmin is the amount of padding)
|
906
|
+
shift_right = float(aax.get_window_extent().transformed(ax.transAxes.inverted()).xmax) + float(aax.get_window_extent().transformed(ax.transAxes.inverted()).xmin)
|
907
|
+
# also need to shift it up, by the amount of the padding (which we can crib from ymin)
|
908
|
+
shift_up = float(aax.get_window_extent().transformed(ax.transAxes.inverted()).ymin)
|
909
|
+
hax = inset_map(ax, "lower left", size, pad, coords=(shift_right, shift_up), **kwargs)
|
910
|
+
to_return.append(hax)
|
911
|
+
else:
|
912
|
+
if alaska == True:
|
913
|
+
aax = inset_map(ax, "lower_left", size, pad, **kwargs)
|
914
|
+
to_return.append(aax)
|
915
|
+
if hawaii == True:
|
916
|
+
hax = inset_map(ax, "lower left", size, pad, **kwargs)
|
917
|
+
to_return.append(hax)
|
918
|
+
|
919
|
+
# Puerto Rico is positioned off the coast of Florida
|
920
|
+
if puerto_rico == True:
|
921
|
+
pax = inset_map(ax, "lower right", size, pad, **kwargs)
|
922
|
+
to_return.append(pax)
|
923
|
+
|
924
|
+
# DC is off the coast of DC
|
925
|
+
if dc == True:
|
926
|
+
dax = inset_map(ax, "center right", size, pad, **kwargs)
|
927
|
+
to_return.append(dax)
|
928
|
+
|
929
|
+
# Finally, returning everything
|
930
|
+
return to_return
|
931
|
+
|
932
|
+
|
933
|
+
# This retrieves the position of the inset axes (iax)
|
934
|
+
# in the coordinates of its parent axis
|
935
|
+
def _inset_corners_to_parent(pax, iax):
|
936
|
+
# Make sure the figure layout is calculated
|
937
|
+
fig = pax.get_figure()
|
938
|
+
fig.draw_without_rendering()
|
939
|
+
|
940
|
+
# Get positions as Bbox objects in figure coordinates (0-1)
|
941
|
+
iax_pos = iax.get_position()
|
942
|
+
|
943
|
+
# Extract corners in figure coordinates
|
944
|
+
figure_corners = numpy.array([(iax_pos.x0, iax_pos.y0), (iax_pos.x0, iax_pos.y1),
|
945
|
+
(iax_pos.x1, iax_pos.y1), (iax_pos.x1, iax_pos.y0)])
|
946
|
+
|
947
|
+
# Convert to parent axes coordinates (0-1, as a fraction of each axis)
|
948
|
+
parent_corners = pax.transAxes.inverted().transform(
|
949
|
+
fig.transFigure.transform(figure_corners)
|
950
|
+
)
|
951
|
+
|
952
|
+
return parent_corners
|