matplotlib-map-utils 1.0.3__py3-none-any.whl → 2.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 +5 -1
- matplotlib_map_utils/core/__init__.py +4 -0
- matplotlib_map_utils/{north_arrow.py → core/north_arrow.py} +56 -55
- matplotlib_map_utils/core/scale_bar.py +1200 -0
- matplotlib_map_utils/defaults/__init__.py +4 -0
- matplotlib_map_utils/{defaults.py → defaults/north_arrow.py} +4 -4
- matplotlib_map_utils/defaults/scale_bar.py +377 -0
- matplotlib_map_utils/validation/__init__.py +2 -0
- matplotlib_map_utils/validation/functions.py +231 -0
- matplotlib_map_utils/validation/north_arrow.py +175 -0
- matplotlib_map_utils/validation/scale_bar.py +274 -0
- matplotlib_map_utils-2.0.0.dist-info/METADATA +281 -0
- matplotlib_map_utils-2.0.0.dist-info/RECORD +18 -0
- {matplotlib_map_utils-1.0.3.dist-info → matplotlib_map_utils-2.0.0.dist-info}/WHEEL +1 -1
- matplotlib_map_utils/validation.py +0 -332
- matplotlib_map_utils-1.0.3.dist-info/METADATA +0 -131
- matplotlib_map_utils-1.0.3.dist-info/RECORD +0 -11
- {matplotlib_map_utils-1.0.3.dist-info → matplotlib_map_utils-2.0.0.dist-info}/LICENSE +0 -0
- {matplotlib_map_utils-1.0.3.dist-info → matplotlib_map_utils-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,281 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: matplotlib-map-utils
|
3
|
+
Version: 2.0.0
|
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
|
+
Requires-Python: >=3.9
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
License-File: LICENSE
|
14
|
+
Requires-Dist: matplotlib>=3.9.0
|
15
|
+
Requires-Dist: cartopy>=0.23.0
|
16
|
+
Requires-Dist: great-circle-calculator>=1.3.1
|
17
|
+
|
18
|
+
# matplotlib-map-utils
|
19
|
+
|
20
|
+
---
|
21
|
+
|
22
|
+
**Documentation**: See `docs` folder
|
23
|
+
|
24
|
+
**Source Code**: [Available on GitHub](https://github.com/moss-xyz/matplotlib-map-utils)
|
25
|
+
|
26
|
+
---
|
27
|
+
|
28
|
+
### Introduction
|
29
|
+
|
30
|
+
`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/).
|
31
|
+
|
32
|
+
As of `v2.x` (the current version), this includes two tools:
|
33
|
+
|
34
|
+
* `north_arrow.py`, which generates a high quality, context-aware north arrow for a given plot.
|
35
|
+
|
36
|
+
* `scale_bar.py`, which generates a high quality, context-aware scale bar to a given plot.
|
37
|
+
|
38
|
+
Future releases (if the project is continued) might provide a similar tool inset maps, or other functions that I have created myself that give more control in the formatting of maps.
|
39
|
+
|
40
|
+
---
|
41
|
+
|
42
|
+
### Installation
|
43
|
+
|
44
|
+
This package is available on PyPi, and can be installed like so:
|
45
|
+
|
46
|
+
```bash
|
47
|
+
pip install matplotlib-map-utils
|
48
|
+
```
|
49
|
+
|
50
|
+
The requirements for this package are:
|
51
|
+
|
52
|
+
* `python >= 3.9.0` (due to the dictionary-concatenation method utilized)
|
53
|
+
|
54
|
+
* `matplotlib >= 3.9.0` (might work with lower versions but not guaranteed)
|
55
|
+
|
56
|
+
* `cartopy >= 0.23.0` (due to earlier bug with calling `copy()` on `CRS` objects)
|
57
|
+
|
58
|
+
---
|
59
|
+
|
60
|
+
### Package Structure
|
61
|
+
|
62
|
+
The package is arrayed in the following way:
|
63
|
+
|
64
|
+
```bash
|
65
|
+
package_name/
|
66
|
+
├── __init__.py
|
67
|
+
│
|
68
|
+
├── core/
|
69
|
+
│ ├── __init__.py
|
70
|
+
│ ├── north_arrow.py
|
71
|
+
│ ├── scale_bar.py
|
72
|
+
├── validation/
|
73
|
+
│ ├── __init__.py
|
74
|
+
│ ├── functions.py
|
75
|
+
│ ├── north_arrow.py
|
76
|
+
│ └── scale_bar.py
|
77
|
+
├── defaults/
|
78
|
+
│ ├── __init__.py
|
79
|
+
│ ├── north_arrow.py
|
80
|
+
│ └── scale_bar.py
|
81
|
+
```
|
82
|
+
|
83
|
+
Where:
|
84
|
+
|
85
|
+
* `core` contains the main functions and classes for each object
|
86
|
+
|
87
|
+
* `validation` contains type hints for each variable and functions to validate inputs
|
88
|
+
|
89
|
+
* `defaults` contains default settings for each object at different paper sizes
|
90
|
+
|
91
|
+
---
|
92
|
+
|
93
|
+
### North Arrow
|
94
|
+
|
95
|
+
<details>
|
96
|
+
<summary><i>Expand instructions</i></summary>
|
97
|
+
|
98
|
+
#### Quick Start
|
99
|
+
|
100
|
+
Importing the North Arrow functions and classes can be done like so:
|
101
|
+
|
102
|
+
```py
|
103
|
+
from matplotlib_map_utils.core.north_arrow import NorthArrow, north_arrow
|
104
|
+
```
|
105
|
+
|
106
|
+
The quickest way to add a single north arrow to a single plot is to use the `north_arrow` function:
|
107
|
+
|
108
|
+
```python
|
109
|
+
# Setting up a plot
|
110
|
+
fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
|
111
|
+
# Adding a north arrow to the upper-right corner of the axis, without any rotation (see Rotation under Formatting Components for details)
|
112
|
+
north_arrow.north_arrow(ax=ax, location="upper right", rotation={"degrees":0})
|
113
|
+
```
|
114
|
+
|
115
|
+
An object-oriented approach is also supported:
|
116
|
+
|
117
|
+
```python
|
118
|
+
# Setting up a plot
|
119
|
+
fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
|
120
|
+
# Creating a north arrow for the upper-right corner of the axis, without any rotation (see Rotation under Formatting Components for details)
|
121
|
+
na = north_arrow.NorthArrow(location="upper right", rotation={"degrees":0})
|
122
|
+
# Adding the artist to the plot
|
123
|
+
ax.add_artist(na)
|
124
|
+
```
|
125
|
+
|
126
|
+
Both of these will create an output like the following:
|
127
|
+
|
128
|
+

|
129
|
+
|
130
|
+
#### Customization
|
131
|
+
|
132
|
+
Both the object-oriented and functional approaches can be customized to allow for fine-grained control over formatting:
|
133
|
+
|
134
|
+
```python
|
135
|
+
north_arrow(
|
136
|
+
ax,
|
137
|
+
location = "upper right", # accepts a valid string from the list of locations
|
138
|
+
scale = 0.5, # accepts a valid positive float or integer
|
139
|
+
# each of the follow accepts arguments from a customized style dictionary
|
140
|
+
base = {"facecolor":"green"},
|
141
|
+
fancy = False,
|
142
|
+
label = {"text":"North"},
|
143
|
+
shadow = {"alpha":0.8},
|
144
|
+
pack = {"sep":6},
|
145
|
+
aob = {"pad":2},
|
146
|
+
rotation = {"degrees": 35}
|
147
|
+
)
|
148
|
+
```
|
149
|
+
|
150
|
+
This will create an output like the following:
|
151
|
+
|
152
|
+

|
153
|
+
|
154
|
+
Refer to `docs\howto_north_arrow` for details on how to customize each facet of the north arrow.
|
155
|
+
|
156
|
+
#### Rotation
|
157
|
+
|
158
|
+
The north arrow object is also capable of pointing towards "true north", given a CRS and reference point:
|
159
|
+
|
160
|
+

|
161
|
+
|
162
|
+
Instructions for how to do so can be found in `docs\howto_north_arrow`.
|
163
|
+
</details>
|
164
|
+
|
165
|
+
---
|
166
|
+
|
167
|
+
### Scale Bar
|
168
|
+
|
169
|
+
<details>
|
170
|
+
<summary><i>Expand instructions</i></summary>
|
171
|
+
|
172
|
+
#### Quick Start
|
173
|
+
|
174
|
+
Importing the Scale Bar functions and classes can be done like so:
|
175
|
+
|
176
|
+
```py
|
177
|
+
from matplotlib_map_utils.core.scale_bar import ScaleBar, scale_bar
|
178
|
+
```
|
179
|
+
|
180
|
+
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:
|
181
|
+
|
182
|
+
```python
|
183
|
+
# Setting up a plot
|
184
|
+
fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
|
185
|
+
# Adding a scale bar to the upper-right corner of the axis, in the same projection as whatever geodata you plotted
|
186
|
+
# Here, this scale bar will have the "boxes" style
|
187
|
+
scale_bar(ax=ax, location="upper right", style="boxes", bar={"projection":3857})
|
188
|
+
```
|
189
|
+
|
190
|
+
An object-oriented approach is also supported:
|
191
|
+
|
192
|
+
```python
|
193
|
+
# Setting up a plot
|
194
|
+
fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
|
195
|
+
# Adding a scale bar to the upper-right corner of the axis, in the same projection as whatever geodata you plotted
|
196
|
+
# Here, we change the boxes to "ticks"
|
197
|
+
sb = ScaleBar(location="upper right", style="boxes", bar={"projection":3857})
|
198
|
+
# Adding the artist to the plot
|
199
|
+
ax.add_artist(sb)
|
200
|
+
```
|
201
|
+
|
202
|
+
Both of these will create an output like the following (function is left, class is right):
|
203
|
+
|
204
|
+

|
205
|
+
|
206
|
+
#### Customization
|
207
|
+
|
208
|
+
Both the object-oriented and functional approaches can be customized to allow for fine-grained control over formatting:
|
209
|
+
|
210
|
+
```python
|
211
|
+
scale_bar(
|
212
|
+
ax,
|
213
|
+
location = "upper right", # accepts a valid string from the list of locations
|
214
|
+
style = "boxes", # accepts a valid positive float or integer
|
215
|
+
# each of the follow accepts arguments from a customized style dictionary
|
216
|
+
bar = {"unit":"mi", "length":2}, # converting the units to miles, and changing the length of the bar (in inches)
|
217
|
+
labels = {"style":"major", "loc":"below"}, # placing a label on each major division, and moving them below the bar
|
218
|
+
units = {"loc":"text"}, # changing the location of the units text to the major division labels
|
219
|
+
text = {"fontfamily":"monospace"}, # changing the font family of all the text to monospace
|
220
|
+
)
|
221
|
+
```
|
222
|
+
|
223
|
+
This will create an output like the following:
|
224
|
+
|
225
|
+

|
226
|
+
|
227
|
+
Refer to `docs\howto_scale_bar` for details on how to customize each facet of the scale bar.
|
228
|
+
|
229
|
+
</details>
|
230
|
+
|
231
|
+
---
|
232
|
+
|
233
|
+
### Development Notes
|
234
|
+
|
235
|
+
#### Inspiration and Thanks
|
236
|
+
|
237
|
+
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.
|
238
|
+
|
239
|
+
Two more projects assisted with the creation of this script:
|
240
|
+
|
241
|
+
* [`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.
|
242
|
+
|
243
|
+
* [`Cartopy`](https://github.com/SciTools/cartopy/issues/2361) fixed an issue inherent to calling `.copy()` on `CRS` objects.
|
244
|
+
|
245
|
+
#### Future Roadmap
|
246
|
+
|
247
|
+
With the release of `v2.x`, and the addition of **Scale Bar** tools, this project has achieved the two main objectives that I set out to.
|
248
|
+
|
249
|
+
If I continue development of this project, I will be looking to add or fix the following features:
|
250
|
+
|
251
|
+
* **North Arrow:**
|
252
|
+
|
253
|
+
* Copy the image-rendering functionality of the Scale Bar to allow for rotation of the entire object, label and arrow together
|
254
|
+
|
255
|
+
* Create more styles for the arrow, potentiallly including a compass rose and a line-only arrow
|
256
|
+
|
257
|
+
* **Scale Bar:**
|
258
|
+
|
259
|
+
* 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)
|
260
|
+
|
261
|
+
* Fix/improve the `dual_bars()` function, which currently doesn't work great with rotations
|
262
|
+
|
263
|
+
* Clean up the variable naming scheme (consistency on `loc` vs `position`, `style` vs `type`, etc.)
|
264
|
+
|
265
|
+
* Create more styles for the bar, potentiallly including dual boxes and a sawtooth bar
|
266
|
+
|
267
|
+
If that goes well, `v3` can then either create a tool for generating inset maps (which `matplotlib` has *some* support for), or the various functions that I have created in the past that assist with formatting a map "properly", such as centering on a given object and coverting FIPS codes.
|
268
|
+
|
269
|
+
I am also open to ideas for other extensions to create!
|
270
|
+
|
271
|
+
#### Support and Contributions
|
272
|
+
|
273
|
+
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!
|
274
|
+
|
275
|
+
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.
|
276
|
+
|
277
|
+
---
|
278
|
+
|
279
|
+
### License
|
280
|
+
|
281
|
+
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.
|
@@ -0,0 +1,18 @@
|
|
1
|
+
matplotlib_map_utils/__init__.py,sha256=X6lN1yfP4ckb6l-ej20YqFvIHUNLZQxRWnvvyWBlJEY,305
|
2
|
+
matplotlib_map_utils/core/__init__.py,sha256=G4fxPpfE77EhZr7yGZCjppP7zvwRthl8yHM0b2KgrFs,184
|
3
|
+
matplotlib_map_utils/core/north_arrow.py,sha256=vikwYtSP2-sPRF_SQBALezB3uEY_PHA9dglm503hkvU,22531
|
4
|
+
matplotlib_map_utils/core/scale_bar.py,sha256=PQuq4mV3kSVQeQECLosAhr4lGl8iJ7ln0evacwo25TU,60679
|
5
|
+
matplotlib_map_utils/defaults/__init__.py,sha256=_pegE5kv_sb0ansSF4XpWBRwboaP4zUjWY1KIGbK-TE,119
|
6
|
+
matplotlib_map_utils/defaults/north_arrow.py,sha256=uZb1RsUWxFTHywm8HATj_9iPF_GjCs_Z2HOn0JchjTY,8571
|
7
|
+
matplotlib_map_utils/defaults/scale_bar.py,sha256=GpXiWUHcOsv43G1HOfpqw-dzDPQQzQB7RNdtIf0e7Bc,8225
|
8
|
+
matplotlib_map_utils/scratch/map_utils.py,sha256=j8dOX9uuotl9rRCAXapFLHycUwVE4nzIrqWYOGG2Lgg,19653
|
9
|
+
matplotlib_map_utils/scratch/north_arrow_old_classes.py,sha256=1xKQ6yUghX4BWzIv8GsGBHDDPJ8B0Na7ixdw2jgtTqw,50993
|
10
|
+
matplotlib_map_utils/validation/__init__.py,sha256=0fL3N63jxjRwTU44b7-6ZYZJfOT_0ac7dx7M6Gpu_5M,52
|
11
|
+
matplotlib_map_utils/validation/functions.py,sha256=QpOHs-GQ1NUMXO0HxAtEZvAcrXwsIE2ekqUhYm-IKGg,11783
|
12
|
+
matplotlib_map_utils/validation/north_arrow.py,sha256=dlWbcKit7dq93PJVrv1efE_865irT6zwBuqD6NYLYPg,10349
|
13
|
+
matplotlib_map_utils/validation/scale_bar.py,sha256=7rYs7ei0rQ5iJfapcBWkn7s4P-CnSh9B441GpsGpFO4,17628
|
14
|
+
matplotlib_map_utils-2.0.0.dist-info/LICENSE,sha256=aFLFZg6LEJFpTlNQ8su3__jw4GfV-xWBmC1cePkKZVw,35802
|
15
|
+
matplotlib_map_utils-2.0.0.dist-info/METADATA,sha256=zXSPUe0aS1rGQ75zjbS7tFASAcBeodQgwnttkMCPFu8,10323
|
16
|
+
matplotlib_map_utils-2.0.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
17
|
+
matplotlib_map_utils-2.0.0.dist-info/top_level.txt,sha256=6UyDpxsnMhSOd9a-abQe0lLJveybJyYtUHMdX7zXgKA,21
|
18
|
+
matplotlib_map_utils-2.0.0.dist-info/RECORD,,
|
@@ -1,332 +0,0 @@
|
|
1
|
-
############################################################
|
2
|
-
# validation.py contains all the main objects and functions
|
3
|
-
# for checking inputs passed to class definitions
|
4
|
-
############################################################
|
5
|
-
|
6
|
-
### IMPORTING PACKAGES ###
|
7
|
-
|
8
|
-
# Default packages
|
9
|
-
import warnings
|
10
|
-
# Math packages
|
11
|
-
import numpy
|
12
|
-
# Geo packages
|
13
|
-
import pyproj
|
14
|
-
# Graphical packages
|
15
|
-
import matplotlib
|
16
|
-
# matplotlib's useful validation functions
|
17
|
-
import matplotlib.rcsetup
|
18
|
-
# The types we use in this script
|
19
|
-
from typing import Tuple, TypedDict, Literal, get_args
|
20
|
-
|
21
|
-
### TYPE HINTS ###
|
22
|
-
# This section of the code is for defining structured dictionaries and lists
|
23
|
-
# for the inputs necessary for object creation we've created (such as the style dictionaries)
|
24
|
-
# so that intellisense can help with autocompletion
|
25
|
-
|
26
|
-
class _TYPE_BASE(TypedDict, total=False):
|
27
|
-
coords: numpy.array # must be 2D numpy array
|
28
|
-
facecolor: str # any color value for matplotlib
|
29
|
-
edgecolor: str # any color value for matplotlib
|
30
|
-
linewidth: float | int # between 0 and inf
|
31
|
-
zorder: int # any integer
|
32
|
-
|
33
|
-
class _TYPE_FANCY(TypedDict, total=False):
|
34
|
-
coords: numpy.array # must be 2D numpy array
|
35
|
-
facecolor: str # any color value for matplotlib
|
36
|
-
zorder: int # any integer
|
37
|
-
|
38
|
-
class _TYPE_LABEL(TypedDict, total=False):
|
39
|
-
text: str # any string that you want to display ("N" or "North" being the most common)
|
40
|
-
position: Literal["top", "bottom", "left", "right"] # from matplotlib documentation
|
41
|
-
ha: Literal["left", "center", "right"] # from matplotlib documentation
|
42
|
-
va: Literal["baseline", "bottom", "center", "center_baseline", "top"] # from matplotlib documentation
|
43
|
-
fontsize: str | float | int # any fontsize value for matplotlib
|
44
|
-
fontfamily: Literal["serif", "sans-serif", "cursive", "fantasy", "monospace"] # from matplotlib documentation
|
45
|
-
fontstyle: Literal["normal", "italic", "oblique"] # from matplotlib documentation
|
46
|
-
color: str # any color value for matplotlib
|
47
|
-
fontweight: Literal["normal", "bold", "heavy", "light", "ultrabold", "ultralight"] # from matplotlib documentation
|
48
|
-
stroke_width: float | int # between 0 and infinity
|
49
|
-
stroke_color: str # any color value for matplotlib
|
50
|
-
rotation: float | int # between -360 and 360
|
51
|
-
zorder: int # any integer
|
52
|
-
|
53
|
-
class _TYPE_SHADOW(TypedDict, total=False):
|
54
|
-
offset: Tuple[float | int, float | int] # two-length tuple or list of x,y values in points
|
55
|
-
alpha: float | int # between 0 and 1
|
56
|
-
shadow_rgbFace: str # any color vlaue for matplotlib
|
57
|
-
|
58
|
-
class _TYPE_PACK(TypedDict, total=False):
|
59
|
-
sep: float | int # between 0 and inf
|
60
|
-
align: Literal["top", "bottom", "left", "right", "center", "baseline"] # from matplotlib documentation
|
61
|
-
pad: float | int # between 0 and inf
|
62
|
-
width: float | int # between 0 and inf
|
63
|
-
height: float | int # between 0 and inf
|
64
|
-
mode: Literal["fixed", "expand", "equal"] # from matplotlib documentation
|
65
|
-
|
66
|
-
class _TYPE_AOB(TypedDict, total=False):
|
67
|
-
facecolor: str # NON-STANDARD: used to set the facecolor of the offset box (i.e. to white), any color vlaue for matplotlib
|
68
|
-
edgecolor: str # NON-STANDARD: used to set the edge of the offset box (i.e. to black), any color vlaue for matplotlib
|
69
|
-
alpha: float | int # NON-STANDARD: used to set the transparency of the face color of the offset box^, between 0 and 1
|
70
|
-
pad: float | int # between 0 and inf
|
71
|
-
borderpad: float | int # between 0 and inf
|
72
|
-
prop: str | float | int # any fontsize value for matplotlib
|
73
|
-
frameon: bool # any bool
|
74
|
-
# bbox_to_anchor: None # NOTE: currently unvalidated, use at your own risk!
|
75
|
-
# bbox_transform: None
|
76
|
-
|
77
|
-
class _TYPE_ROTATION(TypedDict, total=False):
|
78
|
-
degrees: float | int # anything between -360 and 360, or None for "auto"
|
79
|
-
crs: str | int | pyproj.CRS # only required if degrees is None: should be a valid cartopy or pyproj crs, or a string that can be converted to that
|
80
|
-
reference: Literal["axis", "data", "center"] # only required if degrees is None: should be either "axis" or "data" or "center"
|
81
|
-
coords: Tuple[float | int, float | int] # only required if degrees is None: should be a tuple of coordinates in the relevant reference window
|
82
|
-
|
83
|
-
### VALIDITY CHECKS ###
|
84
|
-
# Functions and variables used for validating inputs for classes
|
85
|
-
# All have a similar form, taking in the name of the property (prop), the value (val)
|
86
|
-
# some parameters to check against (min/max, list, type, etc.),
|
87
|
-
# and whether or not None is acceptable value
|
88
|
-
|
89
|
-
def _validate_list(prop, val, list, none_ok=False):
|
90
|
-
if none_ok==False and val is None:
|
91
|
-
raise ValueError(f"None is not a valid value for {prop}, please provide a value in this list: {list}")
|
92
|
-
elif none_ok==True and val is None:
|
93
|
-
return val
|
94
|
-
elif not val in list:
|
95
|
-
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value in this list: {list}")
|
96
|
-
return val
|
97
|
-
|
98
|
-
def _validate_range(prop, val, min, max, none_ok=False):
|
99
|
-
if none_ok==False and val is None:
|
100
|
-
raise ValueError(f"None is not a valid value for {prop}, please provide a value between {min} and {max}")
|
101
|
-
elif none_ok==True and val is None:
|
102
|
-
return val
|
103
|
-
elif type(val) != int and type(val) != float:
|
104
|
-
raise ValueError(f"The supplied type is not valid for {prop}, please provide a float or integer between {min} and {max}")
|
105
|
-
elif max is not None:
|
106
|
-
if not val >= min and not val <= max:
|
107
|
-
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value between {min} and {max}")
|
108
|
-
elif max is None:
|
109
|
-
if not val >= min:
|
110
|
-
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value greater than {min}")
|
111
|
-
return val
|
112
|
-
|
113
|
-
def _validate_type(prop, val, match, none_ok=False):
|
114
|
-
if none_ok==False and val is None:
|
115
|
-
raise ValueError(f"None is not a valid value for {prop}, please provide an object of type {match}")
|
116
|
-
elif none_ok==True and val is None:
|
117
|
-
return val
|
118
|
-
elif not type(val)==match:
|
119
|
-
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide an object of type {match}")
|
120
|
-
return val
|
121
|
-
|
122
|
-
def _validate_coords(prop, val, numpy_type, dims, none_ok=False):
|
123
|
-
if none_ok==False and val is None:
|
124
|
-
raise ValueError(f"None is not a valid value for {prop}, please provide an object of type {numpy_type}")
|
125
|
-
elif none_ok==True and val is None:
|
126
|
-
return val
|
127
|
-
elif not type(val)==numpy_type:
|
128
|
-
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide an object of type {numpy_type}")
|
129
|
-
elif not val.ndim==dims:
|
130
|
-
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a numpy array with {dims} dimensions")
|
131
|
-
return val
|
132
|
-
|
133
|
-
def _validate_tuple(prop, val, length, types, none_ok=False):
|
134
|
-
if none_ok==False and val is None:
|
135
|
-
raise ValueError(f"None is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
136
|
-
elif none_ok==True and val is None:
|
137
|
-
return val
|
138
|
-
elif type(val)!=tuple:
|
139
|
-
raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
140
|
-
elif len(val)!=length:
|
141
|
-
raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
142
|
-
else:
|
143
|
-
for item in val:
|
144
|
-
if type(item) not in types:
|
145
|
-
raise ValueError(f"{type(item)} is not a valid value for the items in {prop}, please provide a value of one of the following types: {types}")
|
146
|
-
return val
|
147
|
-
|
148
|
-
def _validate_color_or_none(prop, val, none_ok=False):
|
149
|
-
if none_ok==False and val is None:
|
150
|
-
raise ValueError(f"None is not a valid value for {prop}, please provide a color string acceptable to matplotlib instead")
|
151
|
-
elif none_ok==True and val is None:
|
152
|
-
return val
|
153
|
-
else:
|
154
|
-
matplotlib.rcsetup.validate_color(val)
|
155
|
-
return val
|
156
|
-
|
157
|
-
# NOTE: This one is a bit messy, particularly with the rotation module, but I can't think of a better way to do it...
|
158
|
-
def _validate_crs(prop, val, rotation_dict, none_ok=False):
|
159
|
-
degrees = rotation_dict.get("degrees",None)
|
160
|
-
crs = rotation_dict.get("crs",None)
|
161
|
-
reference = rotation_dict.get("reference",None)
|
162
|
-
coords = rotation_dict.get("coords",None)
|
163
|
-
|
164
|
-
if degrees is None:
|
165
|
-
if reference == "center":
|
166
|
-
if crs is None:
|
167
|
-
raise ValueError(f"If degrees is set to None, and reference is 'center', then a valid crs must be supplied")
|
168
|
-
else:
|
169
|
-
if crs is None or reference is None or coords is None:
|
170
|
-
raise ValueError(f"If degrees is set to None, then crs, reference, and coords cannot be None: please provide a valid input for each of these variables instead")
|
171
|
-
elif (type(degrees)==int or type(degrees)==float) and (crs is not None or reference is not None or coords is not None):
|
172
|
-
warnings.warn(f"A value for degrees was supplied; values for crs, reference, and coords will be ignored")
|
173
|
-
return val
|
174
|
-
else:
|
175
|
-
if none_ok==False and val is None:
|
176
|
-
raise ValueError(f"If degrees is set to None, then {prop} cannot be None: please provide a valid CRS input for PyProj instead")
|
177
|
-
elif none_ok==True and val is None:
|
178
|
-
return val
|
179
|
-
# This happens if (a) a value for CRS is supplied and (b) a value for degrees is NOT supplied
|
180
|
-
if type(val)==pyproj.CRS:
|
181
|
-
pass
|
182
|
-
else:
|
183
|
-
try:
|
184
|
-
val = pyproj.CRS.from_user_input(val)
|
185
|
-
except:
|
186
|
-
raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input for PyProj instead")
|
187
|
-
return val
|
188
|
-
|
189
|
-
# This final one is used for keys that are not validated
|
190
|
-
def _skip_validation(val, none_ok=False):
|
191
|
-
return val
|
192
|
-
|
193
|
-
### VALIDITY DICTS ###
|
194
|
-
# These compile the functions above^, as well as matplotlib's built-in validity functions
|
195
|
-
# into dictionaries that can be used to validate all the inputs to a dictionary at once
|
196
|
-
|
197
|
-
_VALIDATE_PRIMARY = {
|
198
|
-
"location":{"func":_validate_list, "kwargs":{"list":["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]}},
|
199
|
-
"scale":{"func":_validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
|
200
|
-
}
|
201
|
-
|
202
|
-
_VALIDATE_BASE = {
|
203
|
-
"coords":{"func":_validate_coords, "kwargs":{"numpy_type":numpy.ndarray, "dims":2}}, # must be 2D numpy array
|
204
|
-
"facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
205
|
-
"edgecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
206
|
-
"linewidth":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
207
|
-
"zorder":{"func":_validate_type, "kwargs":{"match":int}} # any integer
|
208
|
-
}
|
209
|
-
|
210
|
-
_VALIDATE_FANCY = {
|
211
|
-
"coords":{"func":_validate_coords, "kwargs":{"numpy_type":numpy.ndarray, "dims":2}}, # must be 2D numpy array
|
212
|
-
"facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
213
|
-
"zorder":{"func":_validate_type, "kwargs":{"match":int}} # any integer
|
214
|
-
}
|
215
|
-
|
216
|
-
_VALID_LABEL_POSITION = get_args(_TYPE_LABEL.__annotations__["position"])
|
217
|
-
_VALID_LABEL_HA = get_args(_TYPE_LABEL.__annotations__["ha"])
|
218
|
-
_VALID_LABEL_VA = get_args(_TYPE_LABEL.__annotations__["va"])
|
219
|
-
_VALID_LABEL_FONTFAMILY = get_args(_TYPE_LABEL.__annotations__["fontfamily"])
|
220
|
-
_VALID_LABEL_FONTSTYLE = get_args(_TYPE_LABEL.__annotations__["fontstyle"])
|
221
|
-
_VALID_LABEL_FONTWEIGHT = get_args(_TYPE_LABEL.__annotations__["fontweight"])
|
222
|
-
|
223
|
-
_VALIDATE_LABEL = {
|
224
|
-
"text":{"func":_validate_type, "kwargs":{"match":str}}, # any string
|
225
|
-
"position":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_POSITION}},
|
226
|
-
"ha":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_HA}},
|
227
|
-
"va":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_VA}},
|
228
|
-
"fontsize":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize value for matplotlib
|
229
|
-
"fontfamily":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_FONTFAMILY}},
|
230
|
-
"fontstyle":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_FONTSTYLE}},
|
231
|
-
"color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
232
|
-
"fontweight":{"func":matplotlib.rcsetup.validate_fontweight}, # any fontweight value for matplotlib
|
233
|
-
"stroke_width":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
234
|
-
"stroke_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
235
|
-
"rotation":{"func":_validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # anything between -360 and 360, or None for "auto"
|
236
|
-
"zorder":{"func":_validate_type, "kwargs":{"match":int}} # any integer
|
237
|
-
}
|
238
|
-
|
239
|
-
_VALIDATE_SHADOW = {
|
240
|
-
"offset":{"func":_validate_tuple, "kwargs":{"length":2, "types":[float, int]}},
|
241
|
-
"alpha":{"func":_validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # any value between 0 and 1
|
242
|
-
"shadow_rgbFace":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
243
|
-
}
|
244
|
-
|
245
|
-
_VALID_PACK_ALIGN = get_args(_TYPE_PACK.__annotations__["align"])
|
246
|
-
_VALID_PACK_MODE = get_args(_TYPE_PACK.__annotations__["mode"])
|
247
|
-
|
248
|
-
_VALIDATE_PACK = {
|
249
|
-
"sep":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
250
|
-
"align":{"func":_validate_list, "kwargs":{"list":_VALID_PACK_ALIGN}},
|
251
|
-
"pad":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
252
|
-
"width":{"func":_validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
|
253
|
-
"height":{"func":_validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
|
254
|
-
"mode":{"func":_validate_list, "kwargs":{"list":_VALID_PACK_MODE}}
|
255
|
-
}
|
256
|
-
|
257
|
-
_VALIDATE_AOB = {
|
258
|
-
"facecolor":{"func":_validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
|
259
|
-
"edgecolor":{"func":_validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
|
260
|
-
"alpha":{"func":_validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # any value between 0 and 1
|
261
|
-
"pad":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
262
|
-
"borderpad":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
263
|
-
"prop":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize value for matplotlib
|
264
|
-
"frameon":{"func":_validate_type, "kwargs":{"match":bool}}, # any bool
|
265
|
-
"bbox_to_anchor":{"func":_skip_validation}, # TODO: Currently unvalidated! Make sure to remove from _validate_dict once updated!
|
266
|
-
"bbox_transform":{"func":_skip_validation} # TODO: Currently unvalidated! Make sure to remove from _validate_dict once updated!
|
267
|
-
}
|
268
|
-
|
269
|
-
_VALID_ROTATION_REFERENCE = get_args(_TYPE_ROTATION.__annotations__["reference"])
|
270
|
-
|
271
|
-
_VALIDATE_ROTATION = {
|
272
|
-
"degrees":{"func":_validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # anything between -360 and 360, or None for "auto"
|
273
|
-
"crs":{"func":_validate_crs, "kwargs":{"none_ok":True}}, # see _validate_crs for details on what is accepted
|
274
|
-
"reference":{"func":_validate_list, "kwargs":{"list":_VALID_ROTATION_REFERENCE, "none_ok":True}}, # see _VALID_ROTATION_REFERENCE for accepted values
|
275
|
-
"coords":{"func":_validate_tuple, "kwargs":{"length":2, "types":[float, int], "none_ok":True}} # only required if degrees is None: should be a tuple of coordinates in the relevant reference window
|
276
|
-
}
|
277
|
-
|
278
|
-
### MORE VALIDITY FUNCTIONS ###
|
279
|
-
# These are more customized, and so are separated from the _validate_* functions above
|
280
|
-
# Mainly, they can process the input dictionaries wholesale, as well as the individual functions in it
|
281
|
-
|
282
|
-
def _validate_dict(input_dict, default_dict, functions, to_validate: list=None, return_clean=False, parse_false=True):
|
283
|
-
if input_dict == False:
|
284
|
-
if parse_false == True:
|
285
|
-
return None
|
286
|
-
else:
|
287
|
-
return False
|
288
|
-
elif input_dict is None or input_dict == True:
|
289
|
-
return default_dict
|
290
|
-
elif type(input_dict) != dict:
|
291
|
-
raise ValueError(f"A dictionary (NoneType) must be provided, please double-check your inputs")
|
292
|
-
else:
|
293
|
-
values = default_dict | input_dict
|
294
|
-
# Pre-checking that no invalid keys are passed
|
295
|
-
invalid = [key for key in values.keys() if key not in functions.keys() and key not in ["bbox_to_anchor", "bbox_transform"]]
|
296
|
-
if len(invalid) > 0:
|
297
|
-
print(f"Warning: Invalid keys detected ({invalid}). These will be ignored.")
|
298
|
-
# First, trimming our values to only those we need to validate
|
299
|
-
if to_validate is not None:
|
300
|
-
values = {key: val for key, val in values.items() if key in to_validate}
|
301
|
-
functions = {key: val for key, val in functions.items() if key in values.keys()}
|
302
|
-
else:
|
303
|
-
values = {key: val for key, val in values.items() if key in functions.keys()}
|
304
|
-
functions = {key: val for key, val in functions.items() if key in values.keys()}
|
305
|
-
# Now, running the function with the necessary kwargs
|
306
|
-
for key,val in values.items():
|
307
|
-
fd = functions[key]
|
308
|
-
func = fd["func"]
|
309
|
-
# NOTE: This is messy but the only way to get the rotation value to the crs function
|
310
|
-
if key=="crs":
|
311
|
-
_ = func(prop=key, val=val, rotation_dict=values, **fd["kwargs"])
|
312
|
-
# Our custom functions always have this dictionary key in them, so we know what form they take
|
313
|
-
elif "kwargs" in fd:
|
314
|
-
_ = func(prop=key, val=val, **fd["kwargs"])
|
315
|
-
# The matplotlib built-in functions DON'T have that, and only ever take the one value
|
316
|
-
else:
|
317
|
-
_ = func(val)
|
318
|
-
if return_clean==True:
|
319
|
-
return values
|
320
|
-
|
321
|
-
# This function can process the _VALIDATE dictionaries we established above, but for single variables at a time
|
322
|
-
def _validate(validate_dict, prop, val, return_val=True, kwargs={}):
|
323
|
-
fd = validate_dict[prop]
|
324
|
-
func = fd["func"]
|
325
|
-
# Our custom functions always have this dictionary key in them, so we know what form they take
|
326
|
-
if "kwargs" in fd:
|
327
|
-
val = func(prop=prop, val=val, **(fd["kwargs"] | kwargs))
|
328
|
-
# The matplotlib built-in functions DON'T have that, and only ever take the one value
|
329
|
-
else:
|
330
|
-
val = func(val)
|
331
|
-
if return_val==True:
|
332
|
-
return val
|