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.
@@ -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
+ ![Example north arrow](matplotlib_map_utils/docs/assets/readme_northarrow.png)
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
+ ![Customized north arrow](matplotlib_map_utils/docs/assets/readme_northarrow_customization.png)
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
+ ![Example north arrow rotation](matplotlib_map_utils/docs/assets/readme_northarrow_rotation.png)
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
+ ![Example scale bar](matplotlib_map_utils/docs/assets/readme_scalebar.png)
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
+ ![Customized scale bar](matplotlib_map_utils/docs/assets/readme_scalebar_customization.png)
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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