matplotlib-map-utils 3.1.1__tar.gz → 3.1.2__tar.gz

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.
Files changed (31) hide show
  1. matplotlib_map_utils-3.1.2/PKG-INFO +485 -0
  2. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/README.md +2 -0
  3. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/core/scale_bar.py +62 -10
  4. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/defaults/scale_bar.py +31 -6
  5. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/validation/scale_bar.py +11 -1
  6. matplotlib_map_utils-3.1.2/matplotlib_map_utils.egg-info/PKG-INFO +485 -0
  7. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/pyproject.toml +45 -46
  8. matplotlib_map_utils-3.1.1/PKG-INFO +0 -1158
  9. matplotlib_map_utils-3.1.1/matplotlib_map_utils.egg-info/PKG-INFO +0 -1158
  10. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/LICENSE +0 -0
  11. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/__init__.py +0 -0
  12. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/core/__init__.py +0 -0
  13. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/core/inset_map.py +0 -0
  14. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/core/north_arrow.py +0 -0
  15. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/defaults/__init__.py +0 -0
  16. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/defaults/inset_map.py +0 -0
  17. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/defaults/north_arrow.py +0 -0
  18. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/scratch/map_utils.py +0 -0
  19. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/scratch/north_arrow_old_classes.py +0 -0
  20. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/utils/__init__.py +0 -0
  21. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/utils/usa.json +0 -0
  22. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/utils/usa.py +0 -0
  23. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/validation/__init__.py +0 -0
  24. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/validation/functions.py +0 -0
  25. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/validation/inset_map.py +0 -0
  26. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils/validation/north_arrow.py +0 -0
  27. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils.egg-info/SOURCES.txt +0 -0
  28. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils.egg-info/dependency_links.txt +0 -0
  29. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils.egg-info/requires.txt +0 -0
  30. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/matplotlib_map_utils.egg-info/top_level.txt +0 -0
  31. {matplotlib_map_utils-3.1.1 → matplotlib_map_utils-3.1.2}/setup.cfg +0 -0
@@ -0,0 +1,485 @@
1
+ Metadata-Version: 2.4
2
+ Name: matplotlib-map-utils
3
+ Version: 3.1.2
4
+ Summary: A suite of tools for creating maps in matplotlib
5
+ Author-email: David Moss <davidmoss1221@gmail.com>
6
+ Project-URL: Homepage, https://github.com/moss-xyz/matplotlib-map-utils/
7
+ Project-URL: Bug Tracker, https://github.com/moss-xyz/matplotlib-map-utils/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Framework :: Matplotlib
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: matplotlib>=3.9.0
16
+ Requires-Dist: cartopy>=0.23.0
17
+ Requires-Dist: great-circle-calculator>=1.3.1
18
+ Dynamic: license-file
19
+
20
+ ![matplotlib_map_utils logo](matplotlib_map_utils/docs/assets/mmu_logo_w_elements.png)
21
+
22
+ ---
23
+
24
+ **Documentation:** See `docs` folder
25
+
26
+ **Source Code:** [Available on GitHub](https://github.com/moss-xyz/matplotlib-map-utils)
27
+
28
+ **Feedback:** I welcome any and all feedback! See the *Development Notes* below for more details.
29
+
30
+ ---
31
+
32
+ ### 👋 Introduction
33
+
34
+ `matplotlib_map_utils` is intended to be a package that provides various functions and objects that assist with the the creation of maps using [`matplotlib`](https://matplotlib.org/stable/).
35
+
36
+ As of `v3.x` (the current version), this includes three-ish elements:
37
+
38
+ * `north_arrow.py`, for adding a north arrow to a given plot.
39
+
40
+ * `scale_bar.py`, for adding a scale bar to a given plot.
41
+
42
+ * `inset_map.py`, for adding inset maps and detail/extent indicators to a given plot.
43
+
44
+ The three elements listed above are all intended to be high-resolution, easily modifiable, and context-aware, relative to your specific plot.
45
+
46
+ This package also contains a single utility object:
47
+
48
+ * `usa.py`, which contains a class that helps filter for states and territories within the USA based on given characteristics.
49
+
50
+ Together, these allow for the easy creation of a map such as the following:
51
+
52
+ ![Map with all common elements added](matplotlib_map_utils/docs/assets/readme_bigmap.png)
53
+
54
+ ---
55
+
56
+ ### 💾 Installation
57
+
58
+ This package is available on PyPi, and can be installed like so:
59
+
60
+ ```bash
61
+ pip install matplotlib-map-utils
62
+ ```
63
+
64
+ The requirements for this package are:
65
+
66
+ * `python >= 3.10` (due to the use of the pipe operator to concatenate dictionaries and types)
67
+
68
+ * `matplotlib >= 3.9` (might work with lower versions but not guaranteed)
69
+
70
+ * `cartopy >= 0.23` (due to earlier bug with calling `copy()` on `CRS` objects)
71
+
72
+ ---
73
+
74
+ ### 📦 Package Structure
75
+
76
+ <details>
77
+ <summary><i>The package is arrayed in the following way:</i></summary>
78
+
79
+ ```bash
80
+ package_name/
81
+ ├── __init__.py
82
+
83
+ ├── core/
84
+ │ ├── __init__.py
85
+ │ ├── inset_map.py
86
+ │ ├── north_arrow.py
87
+ │ ├── scale_bar.py
88
+ ├── validation/
89
+ │ ├── __init__.py
90
+ │ ├── functions.py
91
+ │ └── inset_map.py
92
+ │ ├── north_arrow.py
93
+ │ └── scale_bar.py
94
+ ├── defaults/
95
+ │ ├── __init__.py
96
+ │ ├── north_arrow.py
97
+ │ └── scale_bar.py
98
+ │ └── inset_map.py
99
+ ├── utils/
100
+ │ ├── __init__.py
101
+ │ ├── usa.py
102
+ │ └── usa.json
103
+ ```
104
+
105
+ Where:
106
+
107
+ * `core` contains the main functions and classes for each object
108
+
109
+ * `validation` contains type hints for each variable and functions to validate inputs
110
+
111
+ * `defaults` contains default settings for each object at different paper sizes
112
+
113
+ * `utils` contains utility functions and objects
114
+
115
+ </details>
116
+
117
+ ---
118
+
119
+ ### 🧭 North Arrow
120
+
121
+ <details>
122
+ <summary><i>Expand instructions</i></summary>
123
+
124
+ #### Quick Start
125
+
126
+ Importing the North Arrow functions and classes can be done like so:
127
+
128
+ ```py
129
+ from matplotlib_map_utils.core.north_arrow import NorthArrow, north_arrow
130
+ from matplotlib_map_utils.core import NorthArrow, north_arrow # also valid
131
+ from matplotlib_map_utils import NorthArrow, north_arrow # also valid
132
+ ```
133
+
134
+ The quickest way to add a single north arrow to a single plot is to use the `north_arrow` function:
135
+
136
+ ```python
137
+ # Setting up a plot
138
+ fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
139
+ # Adding a north arrow to the upper-right corner of the axis, without any rotation (see Rotation under Formatting Components for details)
140
+ north_arrow.north_arrow(ax=ax, location="upper right", rotation={"degrees":0})
141
+ ```
142
+
143
+ An object-oriented approach is also supported:
144
+
145
+ ```python
146
+ # Setting up a plot
147
+ fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
148
+ # Creating a north arrow for the upper-right corner of the axis, without any rotation (see Rotation under Formatting Components for details)
149
+ na = north_arrow.NorthArrow(location="upper right", rotation={"degrees":0})
150
+ # Adding the artist to the plot
151
+ ax.add_artist(na)
152
+ ```
153
+
154
+ Both of these will create an output like the following:
155
+
156
+ ![Example north arrow](matplotlib_map_utils/docs/assets/readme_northarrow.png)
157
+
158
+ #### Customization
159
+
160
+ Both the object-oriented and functional approaches can be customized to allow for fine-grained control over formatting:
161
+
162
+ ```python
163
+ north_arrow(
164
+ ax,
165
+ location = "upper right", # accepts a valid string from the list of locations
166
+ scale = 0.5, # accepts a valid positive float or integer
167
+ # each of the follow accepts arguments from a customized style dictionary
168
+ base = {"facecolor":"green"},
169
+ fancy = False,
170
+ label = {"text":"North"},
171
+ shadow = {"alpha":0.8},
172
+ pack = {"sep":6},
173
+ aob = {"pad":2},
174
+ rotation = {"degrees": 35}
175
+ )
176
+ ```
177
+
178
+ This will create an output like the following:
179
+
180
+ ![Customized north arrow](matplotlib_map_utils/docs/assets/readme_northarrow_customization.png)
181
+
182
+ Refer to `docs\howto_north_arrow` for details on how to customize each facet of the north arrow.
183
+
184
+ _Note: only add a north arrow **after** adding all of your geodata and changing your axis limits!_
185
+
186
+ #### Rotation
187
+
188
+ The north arrow object is also capable of pointing towards "true north", given a CRS and reference point:
189
+
190
+ ![Example north arrow rotation](matplotlib_map_utils/docs/assets/readme_northarrow_rotation.png)
191
+
192
+ Instructions for how to do so can be found in `docs\howto_north_arrow`.
193
+ </details>
194
+
195
+ ---
196
+
197
+ ### 📏 Scale Bar
198
+
199
+ <details>
200
+ <summary><i>Expand instructions</i></summary>
201
+
202
+ #### Quick Start
203
+
204
+ Importing the Scale Bar functions and classes can be done like so:
205
+
206
+ ```py
207
+ from matplotlib_map_utils.core.scale_bar import ScaleBar, scale_bar
208
+ from matplotlib_map_utils.core import ScaleBar, scale_bar # also valid
209
+ from matplotlib_map_utils import ScaleBar, scale_bar # also valid
210
+ ```
211
+
212
+ There are two available styles for the scale bars: `boxes` and `ticks`. The quickest way to add one to a single plot is to use the `scale_bar` function:
213
+
214
+ ```python
215
+ # Setting up a plot
216
+ # NOTE: you MUST set the desired DPI here, when the subplots are created
217
+ # so that the scale_bar's DPI matches!
218
+ fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
219
+ # Adding a scale bar to the upper-right corner of the axis, in the same projection as whatever geodata you plotted
220
+ # Here, this scale bar will have the "boxes" style
221
+ scale_bar(ax=ax, location="upper right", style="boxes", bar={"projection":3857})
222
+ ```
223
+
224
+ An object-oriented approach is also supported:
225
+
226
+ ```python
227
+ # Setting up a plot
228
+ # NOTE: you MUST set the desired DPI here, when the subplots are created
229
+ # so that the scale_bar's DPI matches!
230
+ fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
231
+ # Adding a scale bar to the upper-right corner of the axis, in the same projection as whatever geodata you plotted
232
+ # Here, we change the boxes to "ticks"
233
+ sb = ScaleBar(location="upper right", style="ticks", bar={"projection":3857})
234
+ # Adding the artist to the plot
235
+ ax.add_artist(sb)
236
+ ```
237
+
238
+ Both of these will create an output like the following (function is left, class is right):
239
+
240
+ ![Example scale bar](matplotlib_map_utils/docs/assets/readme_scalebar.png)
241
+
242
+ #### Customization
243
+
244
+ Both the object-oriented and functional approaches can be customized to allow for fine-grained control over formatting:
245
+
246
+ ```python
247
+ scale_bar(
248
+ ax,
249
+ location = "upper right", # accepts a valid string from the list of locations
250
+ style = "boxes", # accepts a valid positive float or integer
251
+ # each of the follow accepts arguments from a customized style dictionary
252
+ bar = {"unit":"mi", "length":2}, # converting the units to miles, and changing the length of the bar (in inches)
253
+ labels = {"style":"major", "loc":"below"}, # placing a label on each major division, and moving them below the bar
254
+ units = {"loc":"text"}, # changing the location of the units text to the major division labels
255
+ text = {"fontfamily":"monospace"}, # changing the font family of all the text to monospace
256
+ )
257
+ ```
258
+
259
+ This will create an output like the following:
260
+
261
+ ![Customized scale bar](matplotlib_map_utils/docs/assets/readme_scalebar_customization.png)
262
+
263
+ Refer to `docs\howto_scale_bar` for details on how to customize each facet of the scale bar.
264
+
265
+ _Note: only add a scale bar **after** adding all of your geodata and changing your axis limits!_
266
+
267
+ #### Specifying Length
268
+
269
+ There are three main ways of specifying the length of a scale bar:
270
+
271
+ - `length` is used to set the total length of the bar, either in _inches_ (for values >= 1) or as a _fraction of the axis_ (for values < 1).
272
+ - The default value of the scale bar utilizes this method, with a `length` value of `0.25` (meaning 25% of the axis).
273
+ - It will automatically orient itself against the horizontal or vertical axis when calculating its fraction, based on the value supplied for `rotation`.
274
+ - Note that any values here will be rounded to a "nice" whole integer, so the length will *always be approximate*; ex., if two inches is 9,128 units, your scale bar will end up being 9,000 units, and therefore a little less than two inches.
275
+ - Values `major_div` and `minor_div` are ignored, while a value for `max` will _override_ `length`.
276
+
277
+ - `max` is used to define the total length of the bar, _in the same units as your map_, as determined by the value of `projection` and `unit`.
278
+ - Ex: If you are using a projection in feet, and give a `max` of `1000`, your scale bar will be representative of 1,000 feet.
279
+ - Ex: If you are using a projection in feet, but provide a value of `meter` to `unit`, and give a `max` of `1000`, your scale bar will be representative of 1,000 meters.
280
+ - Will _override_ any value provided for `length`, and give a warning that it is doing so!
281
+ - Values can be optionally be provided for `major_div` and `minor_div`, to subdivide the bar into major or minor segments as you desire; if left blank, values for these will be calculated automatically (see `preferred_divs` in `validation/scale_bar.py` for the values used).
282
+
283
+ - `major_mult` can be used alongside `major_div` to _derive_ the total length: `major_mult` is the _length of a **single** major division_, in the _same units as your map_ (as determined by the value of `projection` and `unit`), which is then multiplied out by `major_div` to arrive at the desired length of the bar.
284
+ - Ex: If you set `major_mult` to 1,000, and `major_div` to 3, your bar will be 3,000 units long, divided into three 1,000 segments.
285
+ - This is the _only_ use case for `major_mult` - using it anywhere else will result in warnings and/or errors!
286
+ - Specifying either `max` or `length` will override this method!
287
+ - `minor_div` can still be _optionally_ provided.
288
+
289
+ All of the above cases expect a valid CRS to be supplied to the `projection` parameter, to correctly calculate the relative size of the bar with respect to the map's underlying units. However, three _additional_ values may be passed to `projection`, to override this behavior entirely:
290
+
291
+ - If `projection` is set to `px`, `pixel`, or `pixels`, then values for `max` and `major_mult` are interpreted as being in _pixels_ (so a `max` of 1,000 will result in a bar 1,000 pixels long)
292
+
293
+ - If `projection` is set to `pt`, `point`, or `points`, then values for `max` and `major_mult` are interpreted as being in _points_ (so a `max` of 1,000 will result in a bar 1,000 points long (a point is 1/72 of an inch))
294
+
295
+ - If `projection` is set to `dx`, `custom`, or `axis`, then values for `max` and `major_mult` are interpreted as being in _the units of the x or y axis_ (so a `max` of 1,000 will result in a bar equal to 1,000 units of the x-axis (if orientated horizontally))
296
+
297
+ The intent of these additional methods is to provide an alternative interface for defining the bar, in the case of non-standard projections, or for non-cartographic use cases (in particular, this is inspired by the `dx` implementation of `matplotlib-scalebar`). However, this puts the onus on the user to know how big their bar should be - you also cannot pass a value to `unit` to convert! Note you can provide custom label text to the bar via the `labels` and `units` arguments (ex. if you need to label "inches" or something).
298
+ </details>
299
+
300
+ ---
301
+
302
+ ### 🗺️ Inset Map
303
+
304
+ <details>
305
+ <summary><i>Expand instructions</i></summary>
306
+
307
+ #### Quick Start
308
+
309
+ Importing the Inset Map functions and classes can be done like so:
310
+
311
+ ```py
312
+ from matplotlib_map_utils.core.inset_map import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail
313
+ from matplotlib_map_utils.core import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail # also valid
314
+ from matplotlib_map_utils import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail # also valid
315
+ ```
316
+
317
+ The quickest way to add a single inset map to an existing plot is the `inset_map` function:
318
+
319
+ ```python
320
+ # Setting up a plot
321
+ fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
322
+ # Adding an inset map to the upper-right corner of the axis
323
+ iax = inset_map(ax=ax, location="upper right", size=0.75, pad=0, xticks=[], yticks=[])
324
+ # You can now plot additional data to iax as desired
325
+ ```
326
+
327
+ An object-oriented approach is also supported:
328
+
329
+ ```python
330
+ # Setting up a plot
331
+ fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
332
+ # Creating an object for the inset map
333
+ im = InsetMap(location="upper right", size=0.75, pad=0, xticks=[], yticks=[])
334
+ # Adding the inset map template to the plot
335
+ iax = im.create(ax=ax)
336
+ # You can now plot additional data to iax as desired
337
+ ```
338
+
339
+ Both of these will create an output like the following:
340
+
341
+ ![Example inset map](matplotlib_map_utils/docs/assets/readme_insetmap.png)
342
+
343
+ #### Extent and Detail Indicators
344
+
345
+ Inset maps can be paired with either an extent or detail indicator, to provide additional geographic context to the inset map
346
+
347
+ ```python
348
+ indicate_extent(inset_axis, parent_axis, inset_crs, parent_crs, ...)
349
+ indicate_detail(parent_axis, inset_axis, parent_crs, inset_crs, ...)
350
+ ```
351
+
352
+ This will create an output like the following (extent indicator on the left, detail indicator on the right):
353
+
354
+ ![Customized scale bar](matplotlib_map_utils/docs/assets/readme_indicators.png)
355
+
356
+ Refer to `docs\howto_inset_map` for details on how to customize the inset map and indicators to your liking.
357
+ </details>
358
+
359
+ ---
360
+
361
+ ### 🛠️ Utilities
362
+
363
+ <details>
364
+ <summary><i>Expand instructions</i></summary>
365
+
366
+ #### Quick Start
367
+
368
+ Importing the bundled utility functions and classes can be done like so:
369
+
370
+ ```py
371
+ from matplotlib_map_utils.utils import USA
372
+ ```
373
+
374
+ As of `v2.1.0`, there is only one utility class available: `USA`, an object to help quickly filter for subsets of US states and territories. This utility class is still in beta, and might change.
375
+
376
+ An example:
377
+
378
+ ```python
379
+ # Loading the object
380
+ usa = USA()
381
+ # Getting a list FIPS codes for US States
382
+ usa.filter(states=True, to_return="fips")
383
+ # Getting a list of State Names for states in the South and Midwest regions
384
+ usa.filter(region=["South","Midwest"], to_return="name")
385
+ ```
386
+
387
+ Refer to `docs\howto_utils` for details on how to use this class, including with `pandas.apply()`.
388
+ </details>
389
+
390
+ ---
391
+
392
+ ### 📝 Development Notes
393
+
394
+ #### Inspiration and Thanks
395
+
396
+ This project was heavily inspired by [`matplotlib-scalebar`](https://github.com/ppinard/matplotlib-scalebar/), and much of the code is either directly copied or a derivative of that project, since it uses the same "artist"-based approach.
397
+
398
+ Two more projects assisted with the creation of this script:
399
+
400
+ * [`EOmaps`](https://github.com/raphaelquast/EOmaps/discussions/231) provided code for calculating the rotation required to point to "true north" for an arbitrary point and CRS for the north arrow.
401
+
402
+ * [`Cartopy`](https://github.com/SciTools/cartopy/issues/2361) fixed an issue inherent to calling `.copy()` on `CRS` objects.
403
+
404
+ #### Releases
405
+
406
+ <details>
407
+ <summary><i>See prior release notes</i></summary>
408
+
409
+ - `v1.0.x`: Initial releases featuring the North Arrow element, along with some minor bug fixes.
410
+
411
+ - `v2.0.0`: Initial release of the Scale Bar element.
412
+
413
+ - `v2.0.1`: Fixed a bug in the `dual_bars()` function that prevented empty dictionaries to be passed. Also added a warning when auto-calculated bar widths appear to be exceeding the dimension of the axis (usually occurs when the axis is <2 kilometers or miles long, depending on the units selected).
414
+
415
+ - `v2.0.2`: Changed f-string formatting to alternate double and single quotes, so as to maintain compatibility with versions of Python before 3.12 (see [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/3)). However, this did reveal that another aspect of the code, namely concatenating `type` in function arguments, requires 3.10, and so the minimum python version was incremented.
416
+
417
+ - `v2.1.0`: Added a utility class, `USA`, for filtering subsets of US states and territories based on FIPS code, name, abbreviation, region, division, and more. This is considered a beta release, and might be subject to change later on.
418
+ </details>
419
+
420
+ - `v3.0.0`: Release of inset map and extent and detail indicator classes and functions.
421
+
422
+ - `v3.0.1`: Fixed a bug that led to an incorrect Scale Bar being rendered when using the function method (`scale_bar()`) on a plot containing raster data (see [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/10) for details).
423
+
424
+ - `v3.1.0`: Overhauled the functionality for specifying the the length of a scale bar, including support for custom units/projections (similar to `matplotlib-scalebar`'s `dx` argument) and to specify the length of a major division instead of the entire scale bar, as requested [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/10). Added ability to set artist-level `zorder` variables for all elements, with both the function and class method approaches, as requested [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/9) and [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/10). Also fixed a bug related to custom division labels on the scale bar.
425
+
426
+ - `v3.1.1`: Fixed a bug that led to errors when creating a `scale_bar` at resolutions below 5km or 1 mile, due to a bug in the backend configuration functions (namely, `_config_bar_dim()`), which was fixed by correctly instantiating the necessary variable `ax_units` in other cases via an `else` statement (see [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/14) for details).
427
+
428
+ - `v3.1.2`: Fixed a compatibility issue with [Ultraplot](https://github.com/Ultraplot/UltraPlot), primarily affecting the `ScaleBar` element, where text would rasterize at a low resolution (see [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/16) and [here](https://github.com/moss-xyz/matplotlib-map-utils/pull/17) for details). A big thank you to cvanelteren on the Ultraplot team for identifying and implementing the necessary fixes, as well as making adjustments to the Ultraplot package to improve compatibility!
429
+
430
+ #### Future Roadmap
431
+
432
+ With the release of `v3.x`, this project has achieved full coverage of the "main" map elements I think are necessary.
433
+
434
+ <details>
435
+ <summary><i>If I continue development of this project, I will be looking to add or fix the following features:</i></summary>
436
+
437
+ * For all: switch to a system based on Pydantic for easier type validation
438
+
439
+ * **North Arrow:**
440
+
441
+ * Copy the image-rendering functionality of the Scale Bar to allow for rotation of the entire object, label and arrow together
442
+
443
+ * Create more styles for the arrow, potentially including a compass rose and a line-only arrow
444
+
445
+ * **Scale Bar:**
446
+
447
+ * Allow for custom unit definitions (instead of just metres/feet/miles/kilometres/etc.), so that the scale bar can be used on arbitrary plots (such as inches/cm/mm, mathmatical plots, and the like)
448
+
449
+ * Fix/improve the `dual_bars()` function, which currently doesn't work great with rotations
450
+
451
+ * Clean up the variable naming scheme (consistency on `loc` vs `position`, `style` vs `type`, etc.)
452
+
453
+ * Create more styles for the bar, potentially including dual boxes and a sawtooth bar
454
+
455
+ * **Inset Map:**
456
+
457
+ * Clean up the way that connectors are drawn for detail indicators
458
+
459
+ * New functionality for placing multiple inset maps at once (with context-aware positioning to prevent overlap with each other)
460
+
461
+ * **Utils:**
462
+
463
+ * (USA): Stronger fuzzy search mechanics, so that it will accept flexible inputs for FIPS/abbr/name
464
+
465
+ * (USA): More integrated class types to allow for a more fully-formed object model (USA being a `Country`, with subclasses related to `State` and `Territory` that have their own classes of attributes, etc.)
466
+
467
+ * (USA): Stronger typing options, so you don't have to recall which `region` or `division` types are available, etc.
468
+
469
+ Future releases (if the project is continued) will probably focus on other functions that I have created myself that give more control in the formatting of maps. I am also open to ideas for other extensions to create!
470
+
471
+ </details>
472
+
473
+ #### Support and Contributions
474
+
475
+ If you notice something is not working as intended or if you'd like to add a feature yourself, I welcome PRs - just be sure to be descriptive as to what you are changing and why, including code examples!
476
+
477
+ If you are having issues using this script, feel free to leave a post explaining your issue, and I will try and assist, though I have no guaranteed SLAs as this is just a hobby project.
478
+
479
+ I am open to contributions, especially to help tackle the roadmap above!
480
+
481
+ ---
482
+
483
+ ### ⚖️ License
484
+
485
+ I know nothing about licensing, so I went with the GPL license. If that is incompatible with any of the dependencies, please let me know.
@@ -406,6 +406,8 @@ Two more projects assisted with the creation of this script:
406
406
 
407
407
  - `v3.1.1`: Fixed a bug that led to errors when creating a `scale_bar` at resolutions below 5km or 1 mile, due to a bug in the backend configuration functions (namely, `_config_bar_dim()`), which was fixed by correctly instantiating the necessary variable `ax_units` in other cases via an `else` statement (see [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/14) for details).
408
408
 
409
+ - `v3.1.2`: Fixed a compatibility issue with [Ultraplot](https://github.com/Ultraplot/UltraPlot), primarily affecting the `ScaleBar` element, where text would rasterize at a low resolution (see [here](https://github.com/moss-xyz/matplotlib-map-utils/issues/16) and [here](https://github.com/moss-xyz/matplotlib-map-utils/pull/17) for details). A big thank you to cvanelteren on the Ultraplot team for identifying and implementing the necessary fixes, as well as making adjustments to the Ultraplot package to improve compatibility!
410
+
409
411
  #### Future Roadmap
410
412
 
411
413
  With the release of `v3.x`, this project has achieved full coverage of the "main" map elements I think are necessary.
@@ -35,6 +35,7 @@ from ..validation import functions as sbf
35
35
 
36
36
  _DEFAULT_BAR, _DEFAULT_LABELS, _DEFAULT_UNITS, _DEFAULT_TEXT, _DEFAULT_AOB = sbd._DEFAULTS_SB["md"]
37
37
 
38
+
38
39
  ### CLASSES ###
39
40
 
40
41
  class ScaleBar(matplotlib.artist.Artist):
@@ -205,9 +206,17 @@ class ScaleBar(matplotlib.artist.Artist):
205
206
  # THANK YOU to matplotlib-scalebar for figuring this out
206
207
  # Note that we never specify the renderer - the axis takes care of it!
207
208
  def draw(self, renderer, *args, **kwargs):
209
+ # Prefer renderer dpi for class-based artists so exports stay sharp
210
+ # when savefig(dpi=...) differs from the figure construction dpi.
211
+ _bar = copy.deepcopy(self._bar)
212
+ if _bar.get("raster_dpi", None) is None:
213
+ _raster_dpi, _ = _resolve_raster_dpi(
214
+ _bar, self.axes.get_figure(), renderer=renderer
215
+ )
216
+ _bar["raster_dpi"] = _raster_dpi
208
217
  # Can re-use the drawing function we already established, but return the object instead
209
218
  sb_artist = scale_bar(ax=self.axes, style=self._style, location=self._location, draw=False,
210
- bar=self._bar, units=self._units,
219
+ bar=_bar, units=self._units,
211
220
  labels=self._labels, text=self._text, aob=self._aob,
212
221
  zorder=self._zorder)
213
222
  # This handles the actual drawing
@@ -251,6 +260,22 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
251
260
  aob: None | bool | sbt._TYPE_AOB=None,
252
261
  zorder: int=99,
253
262
  return_aob: bool=True,):
263
+ # For the default function mode, dispatch to the Artist class so final
264
+ # rasterization happens at draw-time with the active renderer dpi.
265
+ if draw == True and return_aob == True:
266
+ _ = ax.add_artist(
267
+ ScaleBar(
268
+ style=style,
269
+ location=location,
270
+ bar=bar,
271
+ units=units,
272
+ labels=labels,
273
+ text=text,
274
+ aob=aob,
275
+ zorder=zorder,
276
+ )
277
+ )
278
+ return
254
279
 
255
280
  ##### VALIDATION #####
256
281
  _style = sbf._validate(sbt._VALIDATE_PRIMARY, "style", style)
@@ -272,6 +297,12 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
272
297
  _text = sbf._validate_dict(text, copy.deepcopy(_DEFAULT_TEXT), sbt._VALIDATE_TEXT, return_clean=True) # this one has to be a deepcopy due to dictionary immutability
273
298
  _aob = sbf._validate_dict(aob, _DEFAULT_AOB, sbt._VALIDATE_AOB, return_clean=True)
274
299
 
300
+ # Raster controls for the temporary rendered image.
301
+ # These are kept explicit so output quality is not coupled to external rc state.
302
+ _fig = ax.get_figure()
303
+ _raster_dpi, _raster_dpi_scale = _resolve_raster_dpi(_bar, _fig)
304
+ _raster_dpi = _raster_dpi * _raster_dpi_scale
305
+
275
306
  ##### CONFIGURING TEXT #####
276
307
  # First need to convert each string font size (if any) to a point size
277
308
  for d in [_text, _labels, _units]:
@@ -290,7 +321,7 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
290
321
  # First, ensuring matplotlib knows the correct dimensions for everything
291
322
  # as we need it to be accurate to calculate out the plots!
292
323
  if draw:
293
- ax.get_figure().draw_without_rendering()
324
+ _fig.draw_without_rendering()
294
325
 
295
326
  # Getting the config for the bar (length, text, divs, etc.)
296
327
  bar_max, bar_length, units_label, major_div, minor_div = _config_bar(ax, _bar)
@@ -308,7 +339,7 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
308
339
  units_label = _units["label"]
309
340
 
310
341
  # Creating a temporary figure and axis for rendering later
311
- fig_temp, ax_temp = _temp_figure(ax)
342
+ fig_temp, ax_temp = _temp_figure(ax, dpi=_raster_dpi)
312
343
 
313
344
  ##### BAR CONSTRUCTION #####
314
345
 
@@ -472,7 +503,14 @@ def scale_bar(ax, draw=True, style: Literal["ticks","boxes"]="boxes",
472
503
 
473
504
  # Placing the image in an OffsetBox, while rotating if desired
474
505
  # We have to set the zoom level to be relative to the DPI as well (image is in pixels)
475
- offset_img = matplotlib.offsetbox.OffsetImage(img_scale_bar, origin="upper", zoom=72/fig_temp.dpi)
506
+ offset_img = matplotlib.offsetbox.OffsetImage(
507
+ img_scale_bar,
508
+ origin="upper",
509
+ zoom=72/fig_temp.dpi,
510
+ interpolation=_bar.get("interpolation", "none"),
511
+ dpi_cor=_bar.get("dpi_cor", True),
512
+ resample=_bar.get("resample", False),
513
+ )
476
514
  # If desired, we can just return the rendered image in the final OffsetImage
477
515
  # This will override any aob or draw selections! Only the OffsetImage is returned!
478
516
  if return_aob==False:
@@ -1087,15 +1125,18 @@ def _format_numeric(val, fmt, integer_override=True):
1087
1125
  return f"{val:{fmt}}"
1088
1126
 
1089
1127
  # A small function for creating a temporary figure based on a provided axis
1090
- def _temp_figure(ax, axis=False, visible=False):
1128
+ def _temp_figure(ax, axis=False, visible=False, dpi=None):
1091
1129
  # Getting the figure of the provided axis
1092
1130
  fig = ax.get_figure()
1131
+ # If no dpi is passed, fall back to the figure dpi
1132
+ if dpi is None:
1133
+ dpi = fig.dpi
1093
1134
  # Getting the dimensions of the axis
1094
1135
  ax_bbox = ax.patch.get_window_extent()
1095
1136
  # Converting to inches and rounding up
1096
1137
  ax_dim = math.ceil(max(ax_bbox.height, ax_bbox.width) / fig.dpi)
1097
1138
  # Creating a new temporary figure
1098
- fig_temp, ax_temp = matplotlib.pyplot.subplots(1,1, figsize=(ax_dim*1.5, ax_dim*1.5), dpi=fig.dpi)
1139
+ fig_temp, ax_temp = matplotlib.pyplot.subplots(1,1, figsize=(ax_dim*1.5, ax_dim*1.5), dpi=dpi)
1099
1140
  # Turning off the x and y labels if desired
1100
1141
  if axis == False:
1101
1142
  ax_temp.axis("off")
@@ -1270,9 +1311,9 @@ def _render_as_image(fig, ax, artist, rotation, add=True, remove=True, close=Tru
1270
1311
  # If needed, adding the artist to the axis
1271
1312
  if add == True:
1272
1313
  ax.add_artist(artist)
1273
- # Draw the figure, but without showing it, to place all the elements
1274
- fig.draw_without_rendering()
1275
- # Sets the canvas for the figure to AGG (Anti-Grain Geometry)
1314
+ # Render directly with Agg; a prior draw_without_rendering() can override
1315
+ # temporary figure DPI in some wrappers (e.g., UltraPlot), which makes
1316
+ # raster_dpi ineffective.
1276
1317
  canvas = FigureCanvasAgg(fig)
1277
1318
  # Draws the figure onto the canvas
1278
1319
  canvas.draw()
@@ -1291,4 +1332,15 @@ def _render_as_image(fig, ax, artist, rotation, add=True, remove=True, close=Tru
1291
1332
  if close == True:
1292
1333
  matplotlib.pyplot.close(fig)
1293
1334
  # Returning the image
1294
- return img
1335
+ return img
1336
+
1337
+
1338
+ def _resolve_raster_dpi(bar, fig, renderer=None):
1339
+ """
1340
+ Resolve base raster DPI and raster scale for temporary rendering.
1341
+ """
1342
+ raster_dpi = bar.get("raster_dpi", None)
1343
+ if raster_dpi is None:
1344
+ raster_dpi = renderer.dpi if renderer is not None else fig.dpi
1345
+ raster_dpi_scale = bar.get("raster_dpi_scale", 1)
1346
+ return raster_dpi, raster_dpi_scale