├── .gitignore
├── .gitattributes
├── matplotlib_map_utils
├── utils
│ ├── __init__.py
│ ├── usa.py
│ └── usa.json
├── validation
│ ├── __init__.py
│ ├── inset_map.py
│ ├── north_arrow.py
│ ├── functions.py
│ └── scale_bar.py
├── docs
│ ├── assets
│ │ ├── mmu_logo.png
│ │ ├── mmu_logo.xcf
│ │ ├── readme_bigmap.png
│ │ ├── readme_indicators.png
│ │ ├── readme_insetmap.png
│ │ ├── readme_northarrow.png
│ │ ├── readme_scalebar.png
│ │ ├── mmu_logo_w_elements.png
│ │ ├── readme_northarrow_rotation.png
│ │ ├── readme_scalebar_customization.png
│ │ └── readme_northarrow_customization.png
│ └── howto_utils.ipynb
├── defaults
│ ├── __init__.py
│ ├── inset_map.py
│ ├── scale_bar.py
│ └── north_arrow.py
├── core
│ ├── __init__.py
│ └── north_arrow.py
└── __init__.py
├── pyproject.toml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | scratch/
3 | dist/
4 | *.egg-info/
5 | .venv/
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/matplotlib_map_utils/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .usa import USA
2 |
3 | __all__ = ["USA"]
--------------------------------------------------------------------------------
/matplotlib_map_utils/validation/__init__.py:
--------------------------------------------------------------------------------
1 | from .north_arrow import *
2 | from .scale_bar import *
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/mmu_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/mmu_logo.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/mmu_logo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/mmu_logo.xcf
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_bigmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_bigmap.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/defaults/__init__.py:
--------------------------------------------------------------------------------
1 | from .north_arrow import _DEFAULTS_NA
2 | from .scale_bar import _DEFAULTS_SB
3 |
4 | __all__ = ["_DEFAULTS_NA","_DEFAULTS_SB"]
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_indicators.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_indicators.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_insetmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_insetmap.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_northarrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_northarrow.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_scalebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_scalebar.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/mmu_logo_w_elements.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/mmu_logo_w_elements.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_northarrow_rotation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_northarrow_rotation.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_scalebar_customization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_scalebar_customization.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/assets/readme_northarrow_customization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moss-xyz/matplotlib-map-utils/HEAD/matplotlib_map_utils/docs/assets/readme_northarrow_customization.png
--------------------------------------------------------------------------------
/matplotlib_map_utils/core/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 | from .north_arrow import NorthArrow, north_arrow
3 | from .scale_bar import ScaleBar, scale_bar, dual_bars
4 | from .inset_map import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail, inset_usa
5 |
6 | __all__ = ["NorthArrow", "north_arrow",
7 | "ScaleBar", "scale_bar", "dual_bars",
8 | "InsetMap","inset_map", "ExtentIndicator","indicate_extent", "DetailIndicator","indicate_detail", "inset_usa"]
--------------------------------------------------------------------------------
/matplotlib_map_utils/__init__.py:
--------------------------------------------------------------------------------
1 | # This handles importing of all the functions and classes
2 | from .core.north_arrow import NorthArrow, north_arrow
3 | from .core.scale_bar import ScaleBar, scale_bar, dual_bars
4 | from .core.inset_map import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail, inset_usa
5 | from typing import Literal
6 |
7 | # This defines what wildcard imports should import
8 | __all__ = ["NorthArrow", "north_arrow",
9 | "ScaleBar", "scale_bar", "dual_bars",
10 | "InsetMap","inset_map", "ExtentIndicator","indicate_extent", "DetailIndicator","indicate_detail", "inset_usa",
11 | "set_size"]
12 |
13 | def set_size(size: Literal["xs","xsmall","x-small",
14 | "sm","small",
15 | "md","medium",
16 | "lg","large",
17 | "xl","xlarge","x-large"]):
18 |
19 | NorthArrow.set_size(size)
20 | ScaleBar.set_size(size)
21 | InsetMap.set_size(size)
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "matplotlib-map-utils"
7 | version = "3.1.1"
8 | authors = [
9 | { name="David Moss", email="davidmoss1221@gmail.com" },
10 | ]
11 | description = "A suite of tools for creating maps in matplotlib"
12 | readme = "README.md"
13 | requires-python = ">=3.10"
14 | classifiers = [
15 | "Programming Language :: Python :: 3",
16 | "License :: OSI Approved :: GNU General Public License (GPL)",
17 | "Operating System :: OS Independent",
18 | "Framework :: Matplotlib",
19 | ]
20 | dependencies = [
21 | "matplotlib>=3.9.0",
22 | "cartopy>=0.23.0",
23 | "great-circle-calculator>=1.3.1"
24 | ]
25 |
26 | [tool.setuptools.packages.find]
27 | exclude = ["matplotlib_map_utils.scratch*"]
28 |
29 | [tool.setuptools.package-data]
30 | "matplotlib_map_utils.utils" = ["*.json"]
31 |
32 | [dependency-groups]
33 | dev = [
34 | "contextily>=1.6.2",
35 | "geopandas>=1.1.1",
36 | "ipykernel>=6.30.1",
37 | "jupyter>=1.1.1",
38 | "notebook>=7.4.7",
39 | "pip>=25.2",
40 | "pygris>=0.2.0",
41 | ]
42 |
43 | [project.urls]
44 | "Homepage" = "https://github.com/moss-xyz/matplotlib-map-utils/"
45 | "Bug Tracker" = "https://github.com/moss-xyz/matplotlib-map-utils/issues"
46 |
--------------------------------------------------------------------------------
/matplotlib_map_utils/defaults/inset_map.py:
--------------------------------------------------------------------------------
1 | #################################################################
2 | # defaults/inset_map.py contains default values for the InsetMaps
3 | # at difference plot sizes (xsmall to xlarge)
4 | # see their corresponding sizes under each default heading
5 | #################################################################
6 |
7 | # The main variables that update are the following:
8 | # inset map: size, pad
9 | # labels: sep, style
10 | # units: sep
11 | # text: fontsize, stroke_width (also changes labels and units)
12 | # aob: pad, borderpad
13 |
14 | ## X-SMALL DEFAULTS
15 | # Should work well for ~A8ish paper (2 to 3 inches, or 5 to 8 cm)
16 |
17 | # Map
18 | _INSET_MAP_XS = {
19 | "size":0.5,
20 | "pad":0.05,
21 | }
22 |
23 | ## SMALL DEFAULTS
24 | # Should work well for ~A6 paper (4 to 6 inches, or 11 to 15 cm)
25 |
26 | # Map
27 | _INSET_MAP_SM = {
28 | "size":1,
29 | "pad":0.1,
30 | }
31 |
32 | ## MEDIUM DEFAULTS
33 | # Should work well for ~A4/Letter paper (8 to 12 inches, or 21 to 30 cm)
34 |
35 | # Map
36 | _INSET_MAP_MD = {
37 | "size":2,
38 | "pad":0.25,
39 | }
40 |
41 | ## LARGE DEFAULTS
42 | # Should work well for ~A2 paper (16 to 24 inches, or 42 to 60 cm)
43 |
44 | # Map
45 | _INSET_MAP_LG = {
46 | "size":4,
47 | "pad":0.5,
48 | }
49 |
50 | ## X-LARGE DEFAULTS
51 | # Should work well for ~A0/Poster paper (33 to 47 inches, or 85 to 120 cm)
52 |
53 | # Map
54 | _INSET_MAP_XL = {
55 | "size":8,
56 | "pad":1,
57 | }
58 |
59 | ## CONTAINER
60 | # This makes an easy-to-call dictionary of all the defaults we've set, for easy unpacking by the set_size function
61 | _DEFAULTS_IM = {
62 | "xs":[_INSET_MAP_XS],
63 | "sm":[_INSET_MAP_SM],
64 | "md":[_INSET_MAP_MD],
65 | "lg":[_INSET_MAP_LG],
66 | "xl":[_INSET_MAP_XL],
67 | }
--------------------------------------------------------------------------------
/matplotlib_map_utils/validation/inset_map.py:
--------------------------------------------------------------------------------
1 | ############################################################
2 | # validation/inset_map.py contains all the main objects
3 | # for checking inputs passed to class definitions
4 | ############################################################
5 |
6 | ### IMPORTING PACKAGES ###
7 |
8 | # Geo packages
9 | import matplotlib.axes
10 | import pyproj
11 | # Graphical packages
12 | import matplotlib
13 | # matplotlib's useful validation functions
14 | import matplotlib.rcsetup
15 | # The types we use in this script
16 | from typing import TypedDict, Literal
17 | # Finally, the validation functions
18 | from . import functions as vf
19 |
20 | ### ALL ###
21 | # This code tells other packages what to import if not explicitly stated
22 | __all__ = [
23 | "_TYPE_INSET", "_VALIDATE_INSET",
24 | "_TYPE_EXTENT", "_VALIDATE_EXTENT",
25 | "_TYPE_DETAIL", "_VALIDATE_DETAIL",
26 | ]
27 |
28 | ### TYPE HINTS ###
29 | # This section of the code is for defining structured dictionaries and lists
30 | # for the inputs necessary for object creation we've created (such as the style dictionaries)
31 | # so that intellisense can help with autocompletion
32 |
33 | class _TYPE_INSET(TypedDict, total=False):
34 | size: int | float | tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between 0 and inf
35 | pad: int | float | tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between 0 and inf
36 | coords: tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between -inf and inf
37 |
38 | class _TYPE_EXTENT(TypedDict, total=False):
39 | pax: matplotlib.axes.Axes # any Matplotlib Axes
40 | bax: matplotlib.axes.Axes # any Matplotlib Axes
41 | pcrs: str | int | pyproj.CRS # should be a valid cartopy or pyproj crs, or a string or int that can be converted to that
42 | bcrs: str | int | pyproj.CRS # should be a valid cartopy or pyproj crs, or a string or int that can be converted to that
43 | straighten: bool # either true or false
44 | pad: float | int # any positive float or integer
45 | plot: bool # either true or false
46 | to_return: Literal["shape","patch","fig","ax"] | None # any item in the list, or None if nothing should be returned
47 | facecolor: str # a color to use for the face of the box
48 | linecolor: str # a color to use for the edge of the box
49 | alpha: float | int # any positive float or integer
50 | linewidth: float | int # any positive float or integer
51 |
52 | class _TYPE_DETAIL(TypedDict, total=False):
53 | to_return: Literal["connectors", "lines"] | None # any item in the list, or None if nothing should be returned
54 | connector_color: str # a color to use for the face of the box
55 | connector_width: float | int # any positive float or integer
56 |
57 | ### VALIDITY DICTS ###
58 | # These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
59 | # into dictionaries that can be used to validate all the inputs to a dictionary at once
60 |
61 | _VALIDATE_INSET = {
62 | "location":{"func":vf._validate_list, "kwargs":{"list":["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]}},
63 | "size":{"func":vf._validate_or, "kwargs":{"funcs":[vf._validate_range, vf._validate_and], "kwargs":[{"min":0, "none_ok":True}, {"funcs":[vf._validate_tuple, vf._validate_iterable], "kwargs":[{"length":2, "types":[float, int]}, {"func":vf._validate_range, "kwargs":{"min":0}}]}]}}, # between 0 and inf, or a two-tuple of (x,y) size, each between 0 and inf
64 | "pad":{"func":vf._validate_or, "kwargs":{"funcs":[vf._validate_range, vf._validate_and], "kwargs":[{"min":0, "none_ok":True}, {"funcs":[vf._validate_tuple, vf._validate_iterable], "kwargs":[{"length":2, "types":[float, int]}, {"func":vf._validate_range, "kwargs":{"min":0}}]}]}}, # between 0 and inf, or a two-tuple of (x,y) size, each between 0 and inf
65 | "coords":{"func":vf._validate_tuple, "kwargs":{"length":2, "types":[float, int], "none_ok":True}}, # a two-tuple of coordinates where you want to place the inset map
66 | "to_plot":{"func":vf._validate_iterable, "kwargs":{"func":vf._validate_keys, "kwargs":{"keys":["data","kwargs"], "none_ok":True}}}, # a list of dictionaries, where each contains "data" and "kwargs" keys
67 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}}, # any int
68 | }
69 |
70 | _VALIDATE_EXTENT = {
71 | "pax":{"func":vf._validate_type, "kwargs":{"match":matplotlib.axes.Axes}}, # any Matplotlib Axes
72 | "bax":{"func":vf._validate_type, "kwargs":{"match":matplotlib.axes.Axes}}, # any Matplotlib Axes
73 | "pcrs":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # any valid projection input for PyProj
74 | "bcrs":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # any valid projection input for PyProj
75 | "straighten":{"func":vf._validate_type, "kwargs":{"match":bool}}, # true or false
76 | "pad":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
77 | "plot":{"func":vf._validate_type, "kwargs":{"match":bool}}, # true or false
78 | "facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
79 | "linecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
80 | "alpha":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
81 | "linewidth":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
82 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}}, # any int
83 | "to_return":{"func":vf._validate_list, "kwargs":{"list":["shape", "patch", "fig", "ax"], "none_ok":True}}, # any value in this list
84 | }
85 |
86 | _VALIDATE_DETAIL = {
87 | "to_return":{"func":vf._validate_list, "kwargs":{"list":["connectors", "lines"], "none_ok":True}}, # any value in this list
88 | "connector_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
89 | "connector_width":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
90 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}}, # any int
91 | }
--------------------------------------------------------------------------------
/matplotlib_map_utils/defaults/scale_bar.py:
--------------------------------------------------------------------------------
1 | #################################################################
2 | # defaults/scale_bar.py contains default values for the ScaleBars
3 | # at difference plot sizes (xsmall to xlarge)
4 | # see their corresponding sizes under each default heading
5 | #################################################################
6 |
7 | # The main variables that update are the following:
8 | # bar: height, edgewidth, tickwidth
9 | # labels: sep, style
10 | # units: sep
11 | # text: fontsize, stroke_width (also changes labels and units)
12 | # aob: pad, borderpad
13 |
14 | ## X-SMALL DEFAULTS
15 | # Should work well for ~A8ish paper (2 to 3 inches, or 5 to 8 cm)
16 | # The arrow will appear to be ~1/10 of an inch in height
17 |
18 | # Bar
19 | _BAR_XS = {
20 | "projection":None,
21 | "unit":None,
22 | "rotation":0,
23 | "max":None,
24 | "length":None,
25 | "height":0.05, # changed
26 | "reverse":False,
27 | "major_div":None,
28 | "minor_div":None,
29 | "minor_frac":0.66,
30 | "minor_type":"none",
31 | "major_mult":None,
32 | "facecolors":["black","white"],
33 | "edgecolors":"black",
34 | "edgewidth":0.5, # changed
35 | "tick_loc":"above",
36 | "basecolors":["black"],
37 | "tickcolors":["black"],
38 | "tickwidth":0.5 # changed
39 | }
40 |
41 | # Labels
42 | _LABELS_XS = {
43 | "labels":None,
44 | "format":".2f",
45 | "format_int":True,
46 | "style":"first_last", # changed
47 | "loc":"above",
48 | "pad":0,
49 | "sep":1.5, # changed
50 | }
51 |
52 | # Units
53 | _UNITS_XS = {
54 | "label":None,
55 | "loc":"bar",
56 | "pad":0,
57 | "sep":1.5, # changed
58 | }
59 |
60 | # Text
61 | _TEXT_XS = {
62 | "fontsize":4, # changed
63 | "textcolor":"black",
64 | "fontfamily":"sans-serif",
65 | "fontstyle":"normal",
66 | "fontweight":"regular",
67 | "stroke_width":0.5, # changed
68 | "stroke_color":"white",
69 | "rotation":None,
70 | "rotation_mode":"anchor",
71 | }
72 |
73 | # AOB
74 | _AOB_XS = {
75 | "facecolor":None,
76 | "edgecolor":None,
77 | "alpha":None,
78 | "pad":0.1, # changed
79 | "borderpad":0.1, # changed
80 | "prop":"medium",
81 | "frameon":False,
82 | "bbox_to_anchor":None,
83 | "bbox_transform":None
84 | }
85 |
86 | ## SMALL DEFAULTS
87 | # Should work well for ~A6 paper (4 to 6 inches, or 11 to 15 cm)
88 | # The arrow will appear to be ~1/4 of an inch in height
89 |
90 | # Bar
91 | _BAR_SM = {
92 | "projection":None,
93 | "unit":None,
94 | "rotation":0,
95 | "max":None,
96 | "length":None,
97 | "height":0.075, # changed
98 | "reverse":False,
99 | "major_div":None,
100 | "minor_div":None,
101 | "minor_frac":0.66,
102 | "minor_type":"none",
103 | "major_mult":None,
104 | "facecolors":["black","white"],
105 | "edgecolors":"black",
106 | "edgewidth":0.75, # changed
107 | "tick_loc":"above",
108 | "basecolors":["black"],
109 | "tickcolors":["black"],
110 | "tickwidth":0.75 # changed
111 | }
112 |
113 | # Labels
114 | _LABELS_SM = {
115 | "labels":None,
116 | "format":".2f",
117 | "format_int":True,
118 | "style":"first_last", # changed
119 | "loc":"above",
120 | "pad":0,
121 | "sep":3, # changed
122 | }
123 |
124 | # Units
125 | _UNITS_SM = {
126 | "label":None,
127 | "loc":"bar",
128 | "pad":0,
129 | "sep":3, # changed
130 | }
131 |
132 | # Text
133 | _TEXT_SM = {
134 | "fontsize":6, # changed
135 | "textcolor":"black",
136 | "fontfamily":"sans-serif",
137 | "fontstyle":"normal",
138 | "fontweight":"regular",
139 | "stroke_width":0.5, # changed
140 | "stroke_color":"white",
141 | "rotation":None,
142 | "rotation_mode":"anchor",
143 | }
144 |
145 | # AOB
146 | _AOB_SM = {
147 | "facecolor":None,
148 | "edgecolor":None,
149 | "alpha":None,
150 | "pad":0.33, # changed
151 | "borderpad":0.33, # changed
152 | "prop":"medium",
153 | "frameon":False,
154 | "bbox_to_anchor":None,
155 | "bbox_transform":None
156 | }
157 |
158 | ## MEDIUM DEFAULTS
159 | # Should work well for ~A4/Letter paper (8 to 12 inches, or 21 to 30 cm)
160 | # The arrow will appear to be ~ 1/2 an inch or ~1 cm in height
161 |
162 | # Bar
163 | _BAR_MD = {
164 | "projection":None,
165 | "unit":None,
166 | "rotation":0,
167 | "max":None,
168 | "length":None,
169 | "height":0.1, # changed
170 | "reverse":False,
171 | "major_div":None,
172 | "minor_div":None,
173 | "minor_frac":0.66,
174 | "minor_type":"first",
175 | "major_mult":None,
176 | "facecolors":["black","white"],
177 | "edgecolors":"black",
178 | "edgewidth":1, # changed
179 | "tick_loc":"above",
180 | "basecolors":["black"],
181 | "tickcolors":["black"],
182 | "tickwidth":1.5 # changed
183 | }
184 |
185 | # Labels
186 | _LABELS_MD = {
187 | "labels":None,
188 | "format":".2f",
189 | "format_int":True,
190 | "style":"major",
191 | "loc":"above",
192 | "pad":0,
193 | "sep":5, # changed
194 | }
195 |
196 | # Units
197 | _UNITS_MD = {
198 | "label":None,
199 | "loc":"bar",
200 | "pad":0,
201 | "sep":5, # changed
202 | }
203 |
204 | # Text
205 | _TEXT_MD = {
206 | "fontsize":12, # changed
207 | "textcolor":"black",
208 | "fontfamily":"sans-serif",
209 | "fontstyle":"normal",
210 | "fontweight":"regular",
211 | "stroke_width":1, # changed
212 | "stroke_color":"white",
213 | "rotation":None,
214 | "rotation_mode":"anchor",
215 | }
216 |
217 | # AOB
218 | _AOB_MD = {
219 | "facecolor":None,
220 | "edgecolor":None,
221 | "alpha":None,
222 | "pad":0.5, # changed
223 | "borderpad":0.5, # changed
224 | "prop":"medium",
225 | "frameon":False,
226 | "bbox_to_anchor":None,
227 | "bbox_transform":None
228 | }
229 |
230 | ## LARGE DEFAULTS
231 | # Should work well for ~A2 paper (16 to 24 inches, or 42 to 60 cm)
232 | # The arrow will appear to be ~an inch in height
233 |
234 | # Bar
235 | _BAR_LG = {
236 | "projection":None,
237 | "unit":None,
238 | "rotation":0,
239 | "max":None,
240 | "length":None,
241 | "height":0.2, # changed
242 | "reverse":False,
243 | "major_div":None,
244 | "minor_div":None,
245 | "minor_frac":0.66,
246 | "minor_type":"first",
247 | "major_mult":None,
248 | "facecolors":["black","white"],
249 | "edgecolors":"black",
250 | "edgewidth":2, # changed
251 | "tick_loc":"above",
252 | "basecolors":["black"],
253 | "tickcolors":["black"],
254 | "tickwidth":3 # changed
255 | }
256 |
257 | # Labels
258 | _LABELS_LG = {
259 | "labels":None,
260 | "format":".2f",
261 | "format_int":True,
262 | "style":"major",
263 | "loc":"above",
264 | "pad":0,
265 | "sep":8, # changed
266 | }
267 |
268 | # Units
269 | _UNITS_LG = {
270 | "label":None,
271 | "loc":"bar",
272 | "pad":0,
273 | "sep":8, # changed
274 | }
275 |
276 | # Text
277 | _TEXT_LG = {
278 | "fontsize":24, # changed
279 | "textcolor":"black",
280 | "fontfamily":"sans-serif",
281 | "fontstyle":"normal",
282 | "fontweight":"regular",
283 | "stroke_width":2, # changed
284 | "stroke_color":"white",
285 | "rotation":None,
286 | "rotation_mode":"anchor",
287 | }
288 |
289 | # AOB
290 | _AOB_LG = {
291 | "facecolor":None,
292 | "edgecolor":None,
293 | "alpha":None,
294 | "pad":1, # changed
295 | "borderpad":1, # changed
296 | "prop":"medium",
297 | "frameon":False,
298 | "bbox_to_anchor":None,
299 | "bbox_transform":None
300 | }
301 |
302 | ## X-LARGE DEFAULTS
303 | # Should work well for ~A0/Poster paper (33 to 47 inches, or 85 to 120 cm)
304 | # The arrow will appear to be ~2 inches in height
305 |
306 | # Bar
307 | _BAR_XL = {
308 | "projection":None,
309 | "unit":None,
310 | "rotation":0,
311 | "max":None,
312 | "length":None,
313 | "height":0.4, # changed
314 | "reverse":False,
315 | "major_div":None,
316 | "minor_div":None,
317 | "minor_frac":0.66,
318 | "minor_type":"first",
319 | "major_mult":None,
320 | "facecolors":["black","white"],
321 | "edgecolors":"black",
322 | "edgewidth":4, # changed
323 | "tick_loc":"above",
324 | "basecolors":["black"],
325 | "tickcolors":["black"],
326 | "tickwidth":5 # changed
327 | }
328 |
329 | # Labels
330 | _LABELS_XL = {
331 | "labels":None,
332 | "format":".2f",
333 | "format_int":True,
334 | "style":"major",
335 | "loc":"above",
336 | "pad":0,
337 | "sep":12, # changed
338 | }
339 |
340 | # Units
341 | _UNITS_XL = {
342 | "label":None,
343 | "loc":"bar",
344 | "pad":0,
345 | "sep":12, # changed
346 | }
347 |
348 | # Text
349 | _TEXT_XL = {
350 | "fontsize":48, # changed
351 | "textcolor":"black",
352 | "fontfamily":"sans-serif",
353 | "fontstyle":"normal",
354 | "fontweight":"regular",
355 | "stroke_width":4, # changed
356 | "stroke_color":"white",
357 | "rotation":None,
358 | "rotation_mode":"anchor",
359 | }
360 |
361 | # AOB
362 | _AOB_XL = {
363 | "facecolor":None,
364 | "edgecolor":None,
365 | "alpha":None,
366 | "pad":2, # changed
367 | "borderpad":2, # changed
368 | "prop":"medium",
369 | "frameon":False,
370 | "bbox_to_anchor":None,
371 | "bbox_transform":None
372 | }
373 |
374 | ## CONTAINER
375 | # This makes an easy-to-call dictionary of all the defaults we've set, for easy unpacking by the set_size function
376 | _DEFAULTS_SB = {
377 | "xs":[_BAR_XS, _LABELS_XS, _UNITS_XS, _TEXT_XS, _AOB_XS],
378 | "sm":[_BAR_SM, _LABELS_SM, _UNITS_SM, _TEXT_SM, _AOB_SM],
379 | "md":[_BAR_MD, _LABELS_MD, _UNITS_MD, _TEXT_MD, _AOB_MD],
380 | "lg":[_BAR_LG, _LABELS_LG, _UNITS_LG, _TEXT_LG, _AOB_LG],
381 | "xl":[_BAR_XL, _LABELS_XL, _UNITS_XL, _TEXT_XL, _AOB_XL],
382 | }
--------------------------------------------------------------------------------
/matplotlib_map_utils/defaults/north_arrow.py:
--------------------------------------------------------------------------------
1 | #####################################################################
2 | # defaults/north_arrow.py contains default values for the NorthArrows
3 | # at difference plot sizes (xsmall to xlarge)
4 | # see their corresponding sizes under each default heading
5 | #####################################################################
6 |
7 | # The main variables that update are the following:
8 | # base: scale, linewidth
9 | # fancy: coords (xsmall only)
10 | # label: fontsize, stroke_width
11 | # shadow: offset
12 | # pack: sep
13 | # aob: pad, borderpad
14 |
15 | ### IMPORTING PACKAGES ###
16 |
17 | # Math packages
18 | import numpy
19 |
20 | ### INDEPENDENT DEFAULT VALUES ###
21 |
22 | # Defaults for rotating the arrow to point towards True North (see _rotate_arrow for how it is used)
23 | # This default is the only one that is static: the rest can and should change depending on the size of your figure
24 | _ROTATION_ALL = {
25 | "degrees":None,
26 | "crs":None,
27 | "reference":None,
28 | "coords":None
29 | }
30 |
31 | # We also use the same coordinates for the arrow's base, regardless of size
32 | # This is because we can scale the arrow larger/smaller using the scale parameter instead
33 | _COORDS_BASE = numpy.array([
34 | (0.50, 1.00),
35 | (0.10, 0.00),
36 | (0.50, 0.10),
37 | (0.90, 0.00),
38 | (0.50, 1.00)
39 | ])
40 |
41 | # Similarly, we use the same coordinates for the arrows "fancy" part
42 | # EXCEPT when it gets too small (x-small), as rasterization makes it difficult to see the white edge
43 | _COORDS_FANCY = numpy.array([
44 | (0.50, 0.85),
45 | (0.50, 0.20),
46 | (0.80, 0.10),
47 | (0.50, 0.85)
48 | ])
49 |
50 | _COORDS_FANCY_XS = numpy.array([
51 | (0.50, 1.00),
52 | (0.50, 0.10),
53 | (0.90, 0.00),
54 | (0.50, 1.00)
55 | ])
56 |
57 | ## X-SMALL DEFAULTS
58 | # Should work well for ~A8ish paper (2 to 3 inches, or 5 to 8 cm)
59 | # The arrow will appear to be ~1/10 of an inch in height
60 | # Here is also the only place that we use the _COORDS_FANCY_XS array!
61 |
62 | # Scale
63 | _SCALE_XS = 0.12
64 |
65 | # Base
66 | _BASE_XS = {
67 | "coords":_COORDS_BASE,
68 | "facecolor":"white",
69 | "edgecolor":"black",
70 | "linewidth":0.7,
71 | "zorder":98
72 | }
73 |
74 | # Fancy
75 | _FANCY_XS = {
76 | "coords":_COORDS_FANCY_XS,
77 | "facecolor":"black",
78 | "zorder":99
79 | }
80 |
81 | # Label
82 | _LABEL_XS = {
83 | "text":"N",
84 | "position":"bottom",
85 | "ha":"center",
86 | "va":"baseline",
87 | "fontsize":6,
88 | "fontfamily":"sans-serif",
89 | "fontstyle":"normal",
90 | "color":"black",
91 | "fontweight":"regular",
92 | "stroke_width":0.5,
93 | "stroke_color":"white",
94 | "rotation":0,
95 | "zorder":99
96 | }
97 |
98 | # Shadow
99 | _SHADOW_XS = {
100 | "offset":(1,-1),
101 | "alpha":0.5,
102 | "shadow_rgbFace":"black",
103 | }
104 |
105 | # VPacker/HPacker
106 | _PACK_XS = {
107 | "sep":1.5,
108 | "align":"center",
109 | "pad":0,
110 | "width":None,
111 | "height":None,
112 | "mode":"fixed"
113 | }
114 |
115 | # AnchoredOffsetBox (AOB)
116 | _AOB_XS = {
117 | "facecolor":None,
118 | "edgecolor":None,
119 | "alpha":None,
120 | "pad":0.2,
121 | "borderpad":0.2,
122 | "prop":"medium",
123 | "frameon":False,
124 | "bbox_to_anchor":None,
125 | "bbox_transform":None
126 | }
127 |
128 | ## SMALL DEFAULTS
129 | # Should work well for ~A6 paper (4 to 6 inches, or 11 to 15 cm)
130 | # The arrow will appear to be ~1/4 of an inch in height
131 |
132 | # Scale
133 | _SCALE_SM = 0.25
134 |
135 | # Base
136 | _BASE_SM = {
137 | "coords":_COORDS_BASE,
138 | "facecolor":"white",
139 | "edgecolor":"black",
140 | "linewidth":0.5,
141 | "zorder":98
142 | }
143 |
144 | # Fancy
145 | _FANCY_SM = {
146 | "coords":_COORDS_FANCY,
147 | "facecolor":"black",
148 | "zorder":99
149 | }
150 |
151 | # Label
152 | _LABEL_SM = {
153 | "text":"N",
154 | "position":"bottom",
155 | "ha":"center",
156 | "va":"baseline",
157 | "fontsize":8,
158 | "fontfamily":"sans-serif",
159 | "fontstyle":"normal",
160 | "color":"black",
161 | "fontweight":"regular",
162 | "stroke_width":0.5,
163 | "stroke_color":"white",
164 | "rotation":0,
165 | "zorder":99
166 | }
167 |
168 | # Shadow
169 | _SHADOW_SM = {
170 | "offset":(2,-2),
171 | "alpha":0.5,
172 | "shadow_rgbFace":"black",
173 | }
174 |
175 | # VPacker/HPacker
176 | _PACK_SM = {
177 | "sep":3,
178 | "align":"center",
179 | "pad":0,
180 | "width":None,
181 | "height":None,
182 | "mode":"fixed"
183 | }
184 |
185 | # AnchoredOffsetBox (AOB)
186 | _AOB_SM = {
187 | "facecolor":None,
188 | "edgecolor":None,
189 | "alpha":None,
190 | "pad":0.33,
191 | "borderpad":0.33,
192 | "prop":"medium",
193 | "frameon":False,
194 | "bbox_to_anchor":None,
195 | "bbox_transform":None
196 | }
197 |
198 | ## MEDIUM DEFAULTS
199 | # Should work well for ~A4/Letter paper (8 to 12 inches, or 21 to 30 cm)
200 | # The arrow will appear to be ~ 1/2 an inch or ~1 cm in height
201 |
202 | # Scale
203 | _SCALE_MD = 0.50
204 |
205 | # Base
206 | _BASE_MD = {
207 | "coords":_COORDS_BASE,
208 | "facecolor":"white",
209 | "edgecolor":"black",
210 | "linewidth":1,
211 | "zorder":98
212 | }
213 |
214 | # Fancy
215 | _FANCY_MD = {
216 | "coords":_COORDS_FANCY,
217 | "facecolor":"black",
218 | "zorder":99
219 | }
220 |
221 | # Label
222 | _LABEL_MD = {
223 | "text":"N",
224 | "position":"bottom",
225 | "ha":"center",
226 | "va":"baseline",
227 | "fontsize":16,
228 | "fontfamily":"sans-serif",
229 | "fontstyle":"normal",
230 | "color":"black",
231 | "fontweight":"regular",
232 | "stroke_width":1,
233 | "stroke_color":"white",
234 | "rotation":0,
235 | "zorder":99
236 | }
237 |
238 | # Shadow
239 | _SHADOW_MD = {
240 | "offset":(4,-4),
241 | "alpha":0.5,
242 | "shadow_rgbFace":"black",
243 | }
244 |
245 | # VPacker/HPacker
246 | _PACK_MD = {
247 | "sep":5,
248 | "align":"center",
249 | "pad":0,
250 | "width":None,
251 | "height":None,
252 | "mode":"fixed"
253 | }
254 |
255 | # AnchoredOffsetBox (AOB)
256 | _AOB_MD = {
257 | "facecolor":None,
258 | "edgecolor":None,
259 | "alpha":None,
260 | "pad":0.5,
261 | "borderpad":0.5,
262 | "prop":"medium",
263 | "frameon":False,
264 | "bbox_to_anchor":None,
265 | "bbox_transform":None
266 | }
267 |
268 | ## LARGE DEFAULTS
269 | # Should work well for ~A2 paper (16 to 24 inches, or 42 to 60 cm)
270 | # The arrow will appear to be ~an inch in height
271 |
272 | # Scale
273 | _SCALE_LG = 1
274 |
275 | # Base
276 | _BASE_LG = {
277 | "coords":_COORDS_BASE,
278 | "facecolor":"white",
279 | "edgecolor":"black",
280 | "linewidth":2,
281 | "zorder":98
282 | }
283 |
284 | # Fancy
285 | _FANCY_LG = {
286 | "coords":_COORDS_FANCY,
287 | "facecolor":"black",
288 | "zorder":99
289 | }
290 |
291 | # Label
292 | _LABEL_LG = {
293 | "text":"N",
294 | "position":"bottom",
295 | "ha":"center",
296 | "va":"baseline",
297 | "fontsize":32,
298 | "fontfamily":"sans-serif",
299 | "fontstyle":"normal",
300 | "color":"black",
301 | "fontweight":"regular",
302 | "stroke_width":2,
303 | "stroke_color":"white",
304 | "rotation":0,
305 | "zorder":99
306 | }
307 |
308 | # Shadow
309 | _SHADOW_LG = {
310 | "offset":(8,-8),
311 | "alpha":0.5,
312 | "shadow_rgbFace":"black",
313 | }
314 |
315 | # VPacker/HPacker
316 | _PACK_LG = {
317 | "sep":8,
318 | "align":"center",
319 | "pad":0,
320 | "width":None,
321 | "height":None,
322 | "mode":"fixed"
323 | }
324 |
325 | # AnchoredOffsetBox (AOB)
326 | _AOB_LG = {
327 | "facecolor":None,
328 | "edgecolor":None,
329 | "alpha":None,
330 | "pad":1,
331 | "borderpad":1,
332 | "prop":"medium",
333 | "frameon":False,
334 | "bbox_to_anchor":None,
335 | "bbox_transform":None
336 | }
337 |
338 | ## X-LARGE DEFAULTS
339 | # Should work well for ~A0/Poster paper (33 to 47 inches, or 85 to 120 cm)
340 | # The arrow will appear to be ~2 inches in height
341 |
342 | # Scale
343 | _SCALE_XL = 2
344 |
345 | # Base
346 | _BASE_XL = {
347 | "coords":_COORDS_BASE,
348 | "facecolor":"white",
349 | "edgecolor":"black",
350 | "linewidth":4,
351 | "zorder":98
352 | }
353 |
354 | # Fancy
355 | _FANCY_XL = {
356 | "coords":_COORDS_FANCY,
357 | "facecolor":"black",
358 | "zorder":99
359 | }
360 |
361 | # Label
362 | _LABEL_XL = {
363 | "text":"N",
364 | "position":"bottom",
365 | "ha":"center",
366 | "va":"baseline",
367 | "fontsize":64,
368 | "fontfamily":"sans-serif",
369 | "fontstyle":"normal",
370 | "color":"black",
371 | "fontweight":"regular",
372 | "stroke_width":4,
373 | "stroke_color":"white",
374 | "rotation":0,
375 | "zorder":99
376 | }
377 |
378 | # Shadow
379 | _SHADOW_XL = {
380 | "offset":(16,-16),
381 | "alpha":0.5,
382 | "shadow_rgbFace":"black",
383 | }
384 |
385 | # VPacker/HPacker
386 | _PACK_XL = {
387 | "sep":12,
388 | "align":"center",
389 | "pad":0,
390 | "width":None,
391 | "height":None,
392 | "mode":"fixed"
393 | }
394 |
395 | # AnchoredOffsetBox (AOB)
396 | _AOB_XL = {
397 | "facecolor":None,
398 | "edgecolor":None,
399 | "alpha":None,
400 | "pad":2,
401 | "borderpad":2,
402 | "prop":"medium",
403 | "frameon":False,
404 | "bbox_to_anchor":None,
405 | "bbox_transform":None
406 | }
407 |
408 | ## CONTAINER
409 | # This makes an easy-to-call dictionary of all the defaults we've set, for easy unpacking by the set_size function
410 | _DEFAULTS_NA = {
411 | "xs":[_SCALE_XS, _BASE_XS, _FANCY_XS, _LABEL_XS, _SHADOW_XS, _PACK_XS, _AOB_XS],
412 | "sm":[_SCALE_SM, _BASE_SM, _FANCY_SM, _LABEL_SM, _SHADOW_SM, _PACK_SM, _AOB_SM],
413 | "md":[_SCALE_MD, _BASE_MD, _FANCY_MD, _LABEL_MD, _SHADOW_MD, _PACK_MD, _AOB_MD],
414 | "lg":[_SCALE_LG, _BASE_LG, _FANCY_LG, _LABEL_LG, _SHADOW_LG, _PACK_LG, _AOB_LG],
415 | "xl":[_SCALE_XL, _BASE_XL, _FANCY_XL, _LABEL_XL, _SHADOW_XL, _PACK_XL, _AOB_XL],
416 | }
--------------------------------------------------------------------------------
/matplotlib_map_utils/validation/north_arrow.py:
--------------------------------------------------------------------------------
1 | ############################################################
2 | # validation/north_arrow.py contains all the main objects
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 | # Finally, the validation functions
21 | from . import functions as vf
22 |
23 | ### ALL ###
24 | # This code tells other packages what to import if not explicitly stated
25 | __all__ = [
26 | "_TYPE_BASE", "_TYPE_FANCY", "_TYPE_LABEL", "_TYPE_SHADOW",
27 | "_TYPE_PACK", "_TYPE_AOB", "_TYPE_ROTATION"
28 | ]
29 |
30 | ### TYPE HINTS ###
31 | # This section of the code is for defining structured dictionaries and lists
32 | # for the inputs necessary for object creation we've created (such as the style dictionaries)
33 | # so that intellisense can help with autocompletion
34 |
35 | class _TYPE_BASE(TypedDict, total=False):
36 | coords: numpy.array # must be 2D numpy array
37 | facecolor: str # any color value for matplotlib
38 | edgecolor: str # any color value for matplotlib
39 | linewidth: float | int # between 0 and inf
40 | zorder: int # any integer
41 |
42 | class _TYPE_FANCY(TypedDict, total=False):
43 | coords: numpy.array # must be 2D numpy array
44 | facecolor: str # any color value for matplotlib
45 | zorder: int # any integer
46 |
47 | class _TYPE_LABEL(TypedDict, total=False):
48 | text: str # any string that you want to display ("N" or "North" being the most common)
49 | position: Literal["top", "bottom", "left", "right"] # from matplotlib documentation
50 | ha: Literal["left", "center", "right"] # from matplotlib documentation
51 | va: Literal["baseline", "bottom", "center", "center_baseline", "top"] # from matplotlib documentation
52 | fontsize: str | float | int # any fontsize value for matplotlib
53 | fontfamily: Literal["serif", "sans-serif", "cursive", "fantasy", "monospace"] # from matplotlib documentation
54 | fontstyle: Literal["normal", "italic", "oblique"] # from matplotlib documentation
55 | color: str # any color value for matplotlib
56 | fontweight: Literal["normal", "bold", "heavy", "light", "ultrabold", "ultralight"] # from matplotlib documentation
57 | stroke_width: float | int # between 0 and infinity
58 | stroke_color: str # any color value for matplotlib
59 | rotation: float | int # between -360 and 360
60 | zorder: int # any integer
61 |
62 | class _TYPE_SHADOW(TypedDict, total=False):
63 | offset: Tuple[float | int, float | int] # two-length tuple or list of x,y values in points
64 | alpha: float | int # between 0 and 1
65 | shadow_rgbFace: str # any color vlaue for matplotlib
66 |
67 | class _TYPE_PACK(TypedDict, total=False):
68 | sep: float | int # between 0 and inf
69 | align: Literal["top", "bottom", "left", "right", "center", "baseline"] # from matplotlib documentation
70 | pad: float | int # between 0 and inf
71 | width: float | int # between 0 and inf
72 | height: float | int # between 0 and inf
73 | mode: Literal["fixed", "expand", "equal"] # from matplotlib documentation
74 |
75 | class _TYPE_AOB(TypedDict, total=False):
76 | facecolor: str # NON-STANDARD: used to set the facecolor of the offset box (i.e. to white), any color vlaue for matplotlib
77 | edgecolor: str # NON-STANDARD: used to set the edge of the offset box (i.e. to black), any color vlaue for matplotlib
78 | alpha: float | int # NON-STANDARD: used to set the transparency of the face color of the offset box^, between 0 and 1
79 | pad: float | int # between 0 and inf
80 | borderpad: float | int # between 0 and inf
81 | prop: str | float | int # any fontsize value for matplotlib
82 | frameon: bool # any bool
83 | # bbox_to_anchor: None # NOTE: currently unvalidated, use at your own risk!
84 | # bbox_transform: None # NOTE: currently unvalidated, use at your own risk!
85 |
86 | class _TYPE_ROTATION(TypedDict, total=False):
87 | degrees: float | int # anything between -360 and 360, or None for "auto"
88 | 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
89 | reference: Literal["axis", "data", "center"] # only required if degrees is None: should be either "axis" or "data" or "center"
90 | coords: Tuple[float | int, float | int] # only required if degrees is None: should be a tuple of coordinates in the relevant reference window
91 |
92 | ### VALIDITY DICTS ###
93 | # These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
94 | # into dictionaries that can be used to validate all the inputs to a dictionary at once
95 |
96 | _VALIDATE_PRIMARY = {
97 | "location":{"func":vf._validate_list, "kwargs":{"list":["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]}},
98 | "scale":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
99 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}}, # any int
100 | }
101 |
102 | _VALIDATE_BASE = {
103 | "coords":{"func":vf._validate_coords, "kwargs":{"numpy_type":numpy.ndarray, "dims":2}}, # must be 2D numpy array
104 | "facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
105 | "edgecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
106 | "linewidth":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
107 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}} # any integer
108 | }
109 |
110 | _VALIDATE_FANCY = {
111 | "coords":{"func":vf._validate_coords, "kwargs":{"numpy_type":numpy.ndarray, "dims":2}}, # must be 2D numpy array
112 | "facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
113 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}} # any integer
114 | }
115 |
116 | _VALID_LABEL_POSITION = get_args(_TYPE_LABEL.__annotations__["position"])
117 | _VALID_LABEL_HA = get_args(_TYPE_LABEL.__annotations__["ha"])
118 | _VALID_LABEL_VA = get_args(_TYPE_LABEL.__annotations__["va"])
119 | _VALID_LABEL_FONTFAMILY = get_args(_TYPE_LABEL.__annotations__["fontfamily"])
120 | _VALID_LABEL_FONTSTYLE = get_args(_TYPE_LABEL.__annotations__["fontstyle"])
121 | _VALID_LABEL_FONTWEIGHT = get_args(_TYPE_LABEL.__annotations__["fontweight"])
122 |
123 | _VALIDATE_LABEL = {
124 | "text":{"func":vf._validate_type, "kwargs":{"match":str}}, # any string
125 | "position":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABEL_POSITION}},
126 | "ha":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABEL_HA}},
127 | "va":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABEL_VA}},
128 | "fontsize":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize value for matplotlib
129 | "fontfamily":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABEL_FONTFAMILY}},
130 | "fontstyle":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABEL_FONTSTYLE}},
131 | "color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
132 | "fontweight":{"func":matplotlib.rcsetup.validate_fontweight}, # any fontweight value for matplotlib
133 | "stroke_width":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
134 | "stroke_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
135 | "rotation":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # anything between -360 and 360, or None for "auto"
136 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}} # any integer
137 | }
138 |
139 | _VALIDATE_SHADOW = {
140 | "offset":{"func":vf._validate_tuple, "kwargs":{"length":2, "types":[float, int]}},
141 | "alpha":{"func":vf._validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # any value between 0 and 1
142 | "shadow_rgbFace":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
143 | }
144 |
145 | _VALID_PACK_ALIGN = get_args(_TYPE_PACK.__annotations__["align"])
146 | _VALID_PACK_MODE = get_args(_TYPE_PACK.__annotations__["mode"])
147 |
148 | _VALIDATE_PACK = {
149 | "sep":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
150 | "align":{"func":vf._validate_list, "kwargs":{"list":_VALID_PACK_ALIGN}},
151 | "pad":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
152 | "width":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
153 | "height":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
154 | "mode":{"func":vf._validate_list, "kwargs":{"list":_VALID_PACK_MODE}}
155 | }
156 |
157 | _VALIDATE_AOB = {
158 | "facecolor":{"func":vf._validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
159 | "edgecolor":{"func":vf._validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
160 | "alpha":{"func":vf._validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # any value between 0 and 1
161 | "pad":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
162 | "borderpad":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
163 | "prop":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize value for matplotlib
164 | "frameon":{"func":vf._validate_type, "kwargs":{"match":bool}}, # any bool
165 | "bbox_to_anchor":{"func":vf._skip_validation}, # NOTE: currently unvalidated, use at your own risk!
166 | "bbox_transform":{"func":vf._skip_validation} # NOTE: currently unvalidated, use at your own risk!
167 | }
168 |
169 | _VALID_ROTATION_REFERENCE = get_args(_TYPE_ROTATION.__annotations__["reference"])
170 |
171 | _VALIDATE_ROTATION = {
172 | "degrees":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # anything between -360 and 360, or None for "auto"
173 | "crs":{"func":vf._validate_crs, "kwargs":{"none_ok":True}}, # see _validate_crs for details on what is accepted
174 | "reference":{"func":vf._validate_list, "kwargs":{"list":_VALID_ROTATION_REFERENCE, "none_ok":True}}, # see _VALID_ROTATION_REFERENCE for accepted values
175 | "coords":{"func":vf._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
176 | }
--------------------------------------------------------------------------------
/matplotlib_map_utils/validation/functions.py:
--------------------------------------------------------------------------------
1 | # Default packages
2 | import warnings
3 | # Geo packages
4 | import pyproj
5 | # matplotlib's useful validation functions
6 | import matplotlib.rcsetup
7 |
8 | ### VALIDITY CHECKS ###
9 | # Functions and variables used for validating inputs for classes
10 | # All have a similar form, taking in the name of the property (prop), the value (val)
11 | # some parameters to check against (min/max, list, type, etc.),
12 | # and whether or not None is acceptable value
13 |
14 | def _validate_list(prop, val, list, none_ok=False):
15 | if none_ok==False and val is None:
16 | raise ValueError(f"None is not a valid value for {prop}, please provide a value in this list: {list}")
17 | elif none_ok==True and val is None:
18 | return val
19 | elif not val in list:
20 | raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value in this list: {list}")
21 | return val
22 |
23 | def _validate_range(prop, val, min, max=None, none_ok=False):
24 | if none_ok==False and val is None:
25 | raise ValueError(f"None is not a valid value for {prop}, please provide a value between {min} and {max}")
26 | elif none_ok==True and val is None:
27 | return val
28 | elif type(val) != int and type(val) != float:
29 | raise ValueError(f"The supplied type is not valid for {prop}, please provide a float or integer between {min} and {max}")
30 | elif max is not None:
31 | if not val >= min and not val <= max:
32 | raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value between {min} and {max}")
33 | elif max is None:
34 | if not val >= min:
35 | raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value greater than {min}")
36 | return val
37 |
38 | def _validate_type(prop, val, match, none_ok=False):
39 | if none_ok==False and val is None:
40 | raise ValueError(f"None is not a valid value for {prop}, please provide an object of type {match}")
41 | elif none_ok==True and val is None:
42 | return val
43 | elif not type(val)==match:
44 | raise ValueError(f"'{val}' is not a valid value for {prop}, please provide an object of type {match}")
45 | return val
46 |
47 | def _validate_types(prop, val, matches, none_ok=False):
48 | if none_ok==False and val is None:
49 | raise ValueError(f"None is not a valid value for {prop}, please provide an object of type {matches}")
50 | elif none_ok==True and val is None:
51 | return val
52 | elif not type(val) in matches:
53 | raise ValueError(f"'{val}' is not a valid value for {prop}, please provide an object of type {matches}")
54 | return val
55 |
56 | def _validate_coords(prop, val, numpy_type, dims, none_ok=False):
57 | if none_ok==False and val is None:
58 | raise ValueError(f"None is not a valid value for {prop}, please provide an object of type {numpy_type}")
59 | elif none_ok==True and val is None:
60 | return val
61 | elif not type(val)==numpy_type:
62 | raise ValueError(f"'{val}' is not a valid value for {prop}, please provide an object of type {numpy_type}")
63 | elif not val.ndim==dims:
64 | raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a numpy array with {dims} dimensions")
65 | return val
66 |
67 | def _validate_tuple(prop, val, length, types, none_ok=False):
68 | if none_ok==False and val is None:
69 | raise ValueError(f"None is not a valid value for {prop}, please provide a tuple of length {length} instead")
70 | elif none_ok==True and val is None:
71 | return val
72 | elif not isinstance(val, (tuple, list)):
73 | raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
74 | elif len(val)!=length:
75 | raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
76 | else:
77 | for item in val:
78 | if type(item) not in types:
79 | 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}")
80 | return val
81 |
82 | def _validate_color_or_none(prop, val, none_ok=False):
83 | if none_ok==False and val is None:
84 | raise ValueError(f"None is not a valid value for {prop}, please provide a color string acceptable to matplotlib instead")
85 | elif none_ok==True and val is None:
86 | return val
87 | else:
88 | matplotlib.rcsetup.validate_color(val)
89 | return val
90 |
91 | # NOTE: This one is a bit messy, particularly with the rotation module, but I can't think of a better way to do it...
92 | def _validate_crs(prop, val, rotation_dict, none_ok=False):
93 | degrees = rotation_dict.get("degrees",None)
94 | crs = rotation_dict.get("crs",None)
95 | reference = rotation_dict.get("reference",None)
96 | coords = rotation_dict.get("coords",None)
97 |
98 | if degrees is None:
99 | if reference == "center":
100 | if crs is None:
101 | raise ValueError(f"If degrees is set to None, and reference is 'center', then a valid crs must be supplied")
102 | else:
103 | if crs is None or reference is None or coords is None:
104 | 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")
105 | elif (type(degrees)==int or type(degrees)==float) and (crs is not None or reference is not None or coords is not None):
106 | warnings.warn(f"A value for degrees was supplied; values for crs, reference, and coords will be ignored")
107 | return val
108 | else:
109 | if none_ok==False and val is None:
110 | raise ValueError(f"If degrees is set to None, then {prop} cannot be None: please provide a valid CRS input for PyProj instead")
111 | elif none_ok==True and val is None:
112 | return val
113 | # This happens if (a) a value for CRS is supplied and (b) a value for degrees is NOT supplied
114 | if type(val)==pyproj.CRS:
115 | pass
116 | else:
117 | try:
118 | val = pyproj.CRS.from_user_input(val)
119 | except:
120 | raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input that PyProj can use instead")
121 | return val
122 |
123 | # A simpler validation function for CRSs
124 | def _validate_projection(prop, val, none_ok=False):
125 | if type(val)==pyproj.CRS:
126 | pass
127 | else:
128 | try:
129 | val = pyproj.CRS.from_user_input(val)
130 | except:
131 | raise Exception(f"Invalid CRS supplied ({val}) for {prop}, please provide a valid CRS input that PyProj can use instead")
132 | return val
133 |
134 | # This is specifically to apply another validation function to the items in a list
135 | # Ex. if we want to validate a LIST of colors instead of a single color
136 | def _validate_iterable(prop, val, func, kwargs=None):
137 | # Making sure we wrap everything in a list
138 | if not isinstance(val, (tuple, list)):
139 | val = [val]
140 | # Then, we apply our validation func with optional kwargs to each item in said list, relying on it to return an error value
141 | if kwargs is not None:
142 | for v in val:
143 | v = func(prop=prop, val=v, **kwargs)
144 | return val
145 | # The matplotlib built-in functions DON'T have that, and only ever take the one value
146 | else:
147 | for v in val:
148 | v = func(v)
149 | return val
150 |
151 | # This is to check for the structure of a dictionary-like object
152 | def _validate_keys(prop, val, keys, none_ok=False):
153 | if none_ok==False and val is None:
154 | raise ValueError(f"None is not a valid value for {prop}, please provide a dictionary with keys {keys} instead")
155 | elif none_ok==True and val is None:
156 | return val
157 | elif not isinstance(val, (dict)):
158 | raise ValueError(f"{val} is not a valid value for {prop}, please provide a dictionary with keys {keys} instead")
159 | else:
160 | for k in val.keys():
161 | if k not in keys:
162 | raise ValueError(f"{k} is not a valid key for the items in {prop}, please provide a dictionary with keys {keys} instead")
163 | return val
164 |
165 | # This is to apply multiple validation functions to a value, if needed - only one needs to pass
166 | # Ex. If an item can be a string OR a list of strings, we can use this to validate it
167 | def _validate_or(prop, val, funcs, kwargs):
168 | success = False
169 | # Simply iterate through each func and kwarg
170 | for f,k in zip(funcs,kwargs):
171 | # We wrap the attempts in a try block to suppress the errors
172 | try:
173 | val = f(prop=prop, val=val, **k)
174 | # If we pass, we can stop here and return the value
175 | success = True
176 | break
177 | except Exception as e:
178 | continue
179 | if success == False:
180 | # If we didn't return a value and exit the loop yet, then the passed value is incorrect, as we raise an error
181 | raise ValueError(f"{val} is not a valid value for {prop}, please check the documentation")
182 | else:
183 | return val
184 |
185 | # This is the same, but ALL need to pass
186 | def _validate_and(prop, val, funcs, kwargs):
187 | success = True
188 | # Simply iterate through each func and kwarg
189 | for f,k in zip(funcs,kwargs):
190 | # We wrap the attempts in a try block to suppress the errors
191 | try:
192 | val = f(prop=prop, val=val, **k)
193 | except:
194 | # If we fail, we can stop here and return the value
195 | success = False
196 | break
197 | if success == False:
198 | # If we didn't return a value and exit the loop yet, then the passed value is incorrect, as we raise an error
199 | raise ValueError(f"{val} is not a valid value for {prop}, please check the documentation")
200 | else:
201 | return val
202 |
203 | # This final one is used for keys that are not validated
204 | def _skip_validation(val, none_ok=False):
205 | return val
206 |
207 |
208 | ### MORE VALIDITY FUNCTIONS ###
209 | # These are more customized, and so are separated from the _validate_* functions above
210 | # Mainly, they can process the input dictionaries wholesale, as well as the individual functions in it
211 | def _validate_dict(input_dict, default_dict, functions, to_validate=None, return_clean=False, parse_false=True):
212 | if input_dict == False:
213 | if parse_false == True:
214 | return None
215 | else:
216 | return False
217 | elif input_dict is None or input_dict == True:
218 | return default_dict
219 | elif type(input_dict) != dict:
220 | raise ValueError(f"A dictionary (NoneType) must be provided, please double-check your inputs")
221 | else:
222 | values = default_dict | input_dict
223 | # Pre-checking that no invalid keys are passed
224 | invalid = [key for key in values.keys() if key not in functions.keys() and key not in ["bbox_to_anchor", "bbox_transform"]]
225 | if len(invalid) > 0:
226 | warnings.warn(f"Warning: Invalid keys detected ({invalid}). These will be ignored.")
227 | # First, trimming our values to only those we need to validate
228 | if to_validate == "input":
229 | values = {key: val for key, val in values.items() if (key in input_dict.keys() and key in functions.keys())} # have to check against both here
230 | functions = {key: val for key, val in functions.items() if key in values.keys()}
231 | elif to_validate is not None:
232 | values = {key: val for key, val in values.items() if key in to_validate}
233 | functions = {key: val for key, val in functions.items() if key in values.keys()}
234 | else:
235 | values = {key: val for key, val in values.items() if key in functions.keys()}
236 | functions = {key: val for key, val in functions.items() if key in values.keys()}
237 | # Now, running the function with the necessary kwargs
238 | for key,val in values.items():
239 | fd = functions[key]
240 | func = fd["func"]
241 | # NOTE: This is messy but the only way to get the rotation value to the crs function
242 | if key=="crs":
243 | _ = func(prop=key, val=val, rotation_dict=values, **fd["kwargs"])
244 | # Our custom functions always have this dictionary key in them, so we know what form they take
245 | elif "kwargs" in fd:
246 | _ = func(prop=key, val=val, **fd["kwargs"])
247 | # The matplotlib built-in functions DON'T have that, and only ever take the one value
248 | else:
249 | _ = func(val)
250 | if return_clean==True:
251 | return values
252 |
253 | # This function can process the _VALIDATE dictionaries we established above, but for single variables at a time
254 | def _validate(validate_dict, prop, val, return_val=True, kwargs={}):
255 | fd = validate_dict[prop]
256 | func = fd["func"]
257 | # Most of our custom functions always have this dictionary key in them, so we know what form they take
258 | if "kwargs" in fd:
259 | val = func(prop=prop, val=val, **(fd["kwargs"] | kwargs))
260 | # The matplotlib built-in functions DON'T have that, and only ever take the one value
261 | else:
262 | val = func(val)
263 | if return_val==True:
264 | return val
--------------------------------------------------------------------------------
/matplotlib_map_utils/utils/usa.py:
--------------------------------------------------------------------------------
1 | import re
2 | import json
3 | import warnings
4 | from importlib import resources
5 | from typing import List, Literal, Union
6 |
7 | # Literal lists, for intellisense
8 | regions = Literal["Midwest", "Northeast", "South", "West",
9 | "Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
10 |
11 | divisions = Literal["East North Central", "East South Central", "Mid-Atlantic", "Mountain",
12 | "New England", "Pacific", "South Atlantic", "West North Central", "West South Central",
13 | "Commonwealth", "Compact of Free Association", "Incorporated and Unorganized",
14 | "Unincorporated and Unorganized", "Unincorporated and Organized"]
15 |
16 | ombs = Literal["Region I", "Region II", "Region III", "Region IV", "Region IX", "Region V",
17 | "Region VI", "Region VII", "Region VIII", "Region X",
18 | "Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
19 |
20 | beas = Literal["Far West", "Great Lakes", "Mideast", "New England", "Plains",
21 | "Rocky Mountain", "Southeast", "Southwest",
22 | "Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
23 |
24 | returns = Literal["fips","name","abbr","object","dict"]
25 |
26 | class USA:
27 | # No arguments need to pass on initialization really
28 | def __init__(self):
29 | self._jurisdictions = self._load_json()
30 |
31 | # This is just for loading the JSON
32 | def _load_json(self):
33 | with resources.files("matplotlib_map_utils.utils").joinpath("usa.json").open("r") as f:
34 | usa_json = json.load(f)
35 | return usa_json
36 |
37 | # Getter for all jurisdictions, VALID OR NOT
38 | @property
39 | def _all(self):
40 | return self._jurisdictions
41 |
42 | # Getter for all valid jurisdictions
43 | @property
44 | def jurisdictions(self):
45 | return self.filter_valid(True, self._all, "object")
46 |
47 | # Getter for all valid states
48 | @property
49 | def states(self):
50 | return self.filter_state(True, self.jurisdictions, "object")
51 |
52 | # Getter for all valid territories
53 | @property
54 | def territories(self):
55 | return self.filter_territory(True, self.jurisdictions, "object")
56 |
57 | # Getters to generate distinct values for Region, Division, OMB, and BEA
58 | # which are useful if you can't recall which options are valid
59 | # First, the function that will get the distinct values
60 | def _distinct_options(self, key):
61 | # First getting all the available options from the list
62 | options = [j[key] for j in self.jurisdictions if j[key] is not None]
63 | # Creating the distinct set
64 | options_set = set(options)
65 | # Returning the set (but as a list)
66 | # this will also be alphabetically sorted
67 | options = list(options_set)
68 | options.sort()
69 | return options
70 |
71 | # The getters are now just calls to the properties
72 | @property
73 | def regions(self):
74 | return self._distinct_options("region")
75 |
76 | @property
77 | def divisions(self):
78 | return self._distinct_options("division")
79 |
80 | @property
81 | def omb(self):
82 | return self._distinct_options("omb")
83 |
84 | @property
85 | def bea(self):
86 | return self._distinct_options("bea")
87 |
88 | # Main filter function
89 | # Each filter step will follow the same process
90 | ## Check that there is a non-None filter
91 | ## Normalize the input to be in a list (if not already)
92 | ## Perform the filter step
93 | # Each step is also available as its own independent function, as needed
94 | def filter(self, valid: bool | None=True,
95 | fips: str | int | None=None,
96 | name: str | None=None,
97 | abbr: str | None=None,
98 | state: bool | None=None,
99 | contiguous: bool | None=None,
100 | territory: bool | None=None,
101 | region: Union[regions, List[regions]]=None,
102 | division: Union[divisions, List[divisions]]=None,
103 | omb: Union[ombs, List[ombs]]=None,
104 | bea: Union[beas, List[beas]]=None,
105 | to_return: Union[returns, List[returns]]="fips"):
106 |
107 | # Getting a copy of our jurisdictions, which will be filtered each time
108 | filter_juris = self.jurisdictions.copy()
109 |
110 | # Starting with an initial valid filtering
111 | # Which will drop invalid FIPS codes 03, 07, 14, 43, and 52
112 | if (valid is not None) and (len(filter_juris) > 0):
113 | filter_juris = self.filter_valid(valid, filter_juris, to_return="_ignore")
114 |
115 | # Going through each step
116 | if (fips is not None) and (len(filter_juris) > 0):
117 | filter_juris = self.filter_fips(fips, filter_juris, to_return="_ignore")
118 |
119 | if (name is not None) and (len(filter_juris) > 0):
120 | filter_juris = self.filter_name(name, filter_juris, to_return="_ignore")
121 |
122 | if (abbr is not None) and (len(filter_juris) > 0):
123 | filter_juris = self.filter_abbr(abbr, filter_juris, to_return="_ignore")
124 |
125 | if (state is not None) and (len(filter_juris) > 0):
126 | filter_juris = self.filter_state(state, filter_juris, to_return="_ignore")
127 |
128 | if (contiguous is not None) and (len(filter_juris) > 0):
129 | filter_juris = self.filter_contiguous(contiguous, filter_juris, to_return="_ignore")
130 |
131 | if (territory is not None) and (len(filter_juris) > 0):
132 | filter_juris = self.filter_territory(territory, filter_juris, to_return="_ignore")
133 |
134 | if (region is not None) and (len(filter_juris) > 0):
135 | filter_juris = self.filter_region(region, filter_juris, to_return="_ignore")
136 |
137 | if (division is not None) and (len(filter_juris) > 0):
138 | filter_juris = self.filter_division(division, filter_juris, to_return="_ignore")
139 |
140 | if (omb is not None) and (len(filter_juris) > 0):
141 | filter_juris = self.filter_omb(omb, filter_juris, to_return="_ignore")
142 |
143 | if (bea is not None) and (len(filter_juris) > 0):
144 | filter_juris = self.filter_bea(bea, filter_juris, to_return="_ignore")
145 |
146 | # Final step is to process the input based on to_return
147 | # and then return it!
148 | return self._process_return(filter_juris, to_return)
149 |
150 | # Filtering bool values (valid, state, contiguous, territory)
151 | # Will accept either true or false
152 | def _filter_bool(self, value, key, to_filter=None, to_return="_ignore"):
153 | # If nothing is passed to to_filter, getting the jurisdictions list
154 | to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
155 | if not isinstance(value, bool):
156 | warnings.warn(f"Invalid {key} filter: {value}. Only boolean values (True/False) are considered valid, see documentation for details.")
157 | else:
158 | # Performing the filter
159 | filtered = [j for j in to_filter if j[key] == value]
160 | # And returning the values
161 | return self._process_return(filtered, to_return)
162 |
163 | # Shortcuts for filtering based on valid, state, contiguous, and territory
164 | def filter_valid(self, valid: bool, to_filter=None, to_return="fips"):
165 | return self._filter_bool(valid, "valid", to_filter, to_return)
166 |
167 | def filter_state(self, state: bool, to_filter=None, to_return="fips"):
168 | return self._filter_bool(state, "state", to_filter, to_return)
169 |
170 | def filter_contiguous(self, contiguous: bool, to_filter=None, to_return="fips"):
171 | return self._filter_bool(contiguous, "contiguous", to_filter, to_return)
172 |
173 | def filter_territory(self, territory: bool, to_filter=None, to_return="fips"):
174 | return self._filter_bool(territory, "territory", to_filter, to_return)
175 |
176 | # Filtering FIPS
177 | # Will accept an integer or a two-digit string as an input
178 | # If a longer string is inserted, will truncate to only the first two characters
179 | def filter_fips(self, fips: str | List[str], to_filter=None, to_return="abbr"):
180 | # If nothing is passed to to_filter, getting the jurisdictions list
181 | to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
182 | # Normalizing the fips value being passed
183 | fips = self._normalize_input(fips)
184 | # This will store the cleaned-up fips codes
185 | fips_clean = []
186 | for f in fips:
187 | # If the input is an integer, convert it to a two-digit string
188 | if isinstance(f, int):
189 | fips_clean.append(str(f).zfill(2)[:2])
190 | # If the input is already a string, get the first two characters
191 | elif isinstance(f, str):
192 | fips_clean.append(f.zfill(2)[:2])
193 | # Otherwise, throw a *warning*
194 | else:
195 | warnings.warn(f"Invalid FIPS filter: {f}. Only integers and strings are considered valid, see documentation for details.")
196 | # Now can use the clean fips to actually filter
197 | filtered = [j for j in to_filter if j["fips"] in fips_clean]
198 | # And returning the values
199 | return self._process_return(filtered, to_return)
200 |
201 | # Filtering name
202 | # Will accept strings
203 | # Will normalize the string first (trim, case, special characters), before checking
204 | # Some states also have an alias available for checking against (Washington, D.C. and District of Columbia are equivalent)
205 | def filter_name(self, name: str | List[str], to_filter=None, to_return="fips"):
206 | # If nothing is passed to to_filter, getting the jurisdictions list
207 | to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
208 | # Normalizing the name input being passed
209 | name = self._normalize_input(name)
210 | # This will store the cleaned-up name input
211 | name_clean = []
212 | for n in name:
213 | # If the input is a string, clean it
214 | if isinstance(n, str):
215 | name_clean.append(self._normalize_string(n, case="lower"))
216 | else:
217 | warnings.warn(f"Invalid name filter: {n}. Only strings are considered valid, see documentation for details.")
218 | # Now we can use the clean name to filter
219 | # Note that we also normalize the names and aliases in our to_filter list!
220 | filtered = [j for j in to_filter if ((self._normalize_string(j["name"], case="lower") in name_clean) or
221 | (j["alias"] is not None and self._normalize_string(j["alias"], case="lower") in name_clean))]
222 | # And returning the values
223 | return self._process_return(filtered, to_return)
224 |
225 | # Filtering abbr
226 | # Will accept strings
227 | # Will normalize the string first (trim, case, special characters), before checking
228 | # If a string longer than two characters is passed, will only look at the first two characters!
229 | def filter_abbr(self, abbr: str | List[str], to_filter=None, to_return="fips"):
230 | # If nothing is passed to to_filter, getting the jurisdictions list
231 | to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
232 | # Normalizing the input being passed
233 | abbr = self._normalize_input(abbr)
234 | # This will store the cleaned-up input
235 | abbr_clean = []
236 | for a in abbr:
237 | # If the input is a string, clean it
238 | if isinstance(a, str):
239 | abbr_clean.append(self._normalize_string(a, case="lower"))
240 | else:
241 | warnings.warn(f"Invalid abbr filter: {a}. Only strings are considered valid, see documentation for details.")
242 | # Now we can use the clean input to filter
243 | filtered = [j for j in to_filter if (j["abbr"] is not None and self._normalize_string(j["abbr"], case="lower")[:2] in abbr_clean)]
244 | # And returning the values
245 | return self._process_return(filtered, to_return)
246 |
247 | # Filtering for categorical values (region/division/omb/bea)
248 | # Will get the list of acceptable values and compare inputs to it
249 | # while also warning if an invalid filter is requested
250 | def _filter_categorical(self, input, key, to_filter=None, to_return="_ignore"):
251 | # If nothing is passed to to_filter, getting the jurisdictions list
252 | to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
253 | # Normalizing the input being passed
254 | input = self._normalize_input(input)
255 | # This has the acceptable inputs we want to compare against
256 | accepted_inputs = self._distinct_options(key)
257 | # This will store the cleaned-up input
258 | input_clean = []
259 | for i in input:
260 | # If the input is not a string, warn
261 | if not isinstance(i, str):
262 | warnings.warn(f"Invalid {key} filter: {i}. Only strings are considered valid, see documentation for details.")
263 | # If the input is not in our list, warn the user
264 | elif i not in accepted_inputs:
265 | warnings.warn(f"Invalid {key} filter: {i}. Only the following inputs are considered valid: {accepted_inputs}.")
266 | # Otherwise, add it to our list
267 | else:
268 | input_clean.append(i)
269 | # Now we can use the clean input to filter
270 | filtered = [j for j in to_filter if j[key] in input_clean]
271 | # And returning the values
272 | return self._process_return(filtered, to_return)
273 |
274 | # Iterations for each categorical filter based on their respective inputs
275 | def filter_region(self, region: Union[regions, List[regions]], to_filter=None, to_return="fips"):
276 | return self._filter_categorical(region, "region", to_filter, to_return)
277 |
278 | def filter_division(self, division: Union[divisions, List[divisions]], to_filter=None, to_return="fips"):
279 | return self._filter_categorical(division, "division", to_filter, to_return)
280 |
281 | def filter_omb(self, omb: Union[ombs, List[ombs]], to_filter=None, to_return="fips"):
282 | return self._filter_categorical(omb, "omb", to_filter, to_return)
283 |
284 | def filter_bea(self, bea: Union[beas, List[beas]], to_filter=None, to_return="fips"):
285 | return self._filter_categorical(bea, "bea", to_filter, to_return)
286 |
287 | # Function that processes the returning of a filtered jurisdiction
288 | def _process_return(self, filter_juris, to_return):
289 | # If the length is zero, warn!
290 | if filter_juris is None or len(filter_juris) == 0:
291 | warnings.warn(f"No matching entities found. Please refer to the documentation and double-check your filters.")
292 | return None
293 | if to_return is None:
294 | to_return == "_ignore"
295 | # Available options for to_return include fips, name, and abbr
296 | elif to_return.lower() == "fips":
297 | juris_return = [j["fips"] for j in filter_juris]
298 | elif to_return.lower() == "name":
299 | juris_return = [j["name"] for j in filter_juris]
300 | elif to_return.lower() == "abbr":
301 | juris_return = [j["abbr"] for j in filter_juris]
302 | # Can also request that the entire object be returned, in which case nothing is done
303 | # This will also happen if an invalid return object is passed
304 | elif to_return.lower() not in ["object","dict","_ignore"]:
305 | warnings.warn(f"Invalid to_return request: {to_return}. The entire object will be returned.")
306 | juris_return = filter_juris.copy()
307 | else:
308 | juris_return = filter_juris.copy()
309 |
310 | # Now, also processing the return request based on the length of the returned list
311 | # If the length is zero, warn!
312 | if len(juris_return) == 0:
313 | warnings.warn(f"No matching entities found. Please refer to the documentation and double-check your filters.")
314 | return None
315 | # If only one element is returned, return the element itself, not a list
316 | elif len(juris_return) == 1 and to_return != "_ignore":
317 | return juris_return[0]
318 | # Otherwise return the whole thing
319 | else:
320 | return juris_return
321 |
322 | # Utility function to normalize a string that is passed to it
323 | def _normalize_string(self, string, case="keep", nan="", spaces="_"):
324 | string = string.strip()
325 | if case == "lower":
326 | string = string.lower()
327 | string = re.sub(r"\W\S",nan,string)
328 | string = re.sub(r"\s",spaces,string)
329 | return string
330 |
331 | # Utility function to convert a relevant non-list input to a list
332 | def _normalize_input(self, input):
333 | if not isinstance(input, (list, tuple)):
334 | return [input]
335 | else:
336 | return input
--------------------------------------------------------------------------------
/matplotlib_map_utils/validation/scale_bar.py:
--------------------------------------------------------------------------------
1 | ############################################################
2 | # validation/scale_bar.py contains all the main objects
3 | # for checking inputs passed to class definitions
4 | ############################################################
5 |
6 | ### IMPORTING PACKAGES ###
7 |
8 | # Geo packages
9 | import pyproj
10 | # matplotlib's useful validation functions
11 | import matplotlib.rcsetup
12 | # The types we use in this script
13 | from typing import TypedDict, Literal, get_args
14 | # Finally, the validation functions
15 | from . import functions as vf
16 |
17 | ### ALL ###
18 | # This code tells other packages what to import if not explicitly stated
19 | __all__ = [
20 | "preferred_divs", "convert_dict", "units_standard",
21 | "_TYPE_BAR", "_TYPE_LABELS", "_TYPE_UNITS", "_TYPE_TEXT", "_TYPE_AOB"
22 | ]
23 |
24 | ### CONSTANTS ###
25 | # These are constants that we use elsewhere in the script
26 | # when setting up a scale bar
27 |
28 | # A list of preferred "highest" numbers
29 | # And the corresponding major and min divs
30 | preferred_divs = {
31 | 2:[4,2],
32 | 2.5:[5,1],
33 | 3:[3,3],
34 | 4:[4,2],
35 | 5:[5,1],
36 | 6:[3,2],
37 | 7:[2,1],
38 | 8:[4,2],
39 | 9:[3,3],
40 | 10:[2,1],
41 | }
42 |
43 | # For converting between units
44 | # Everything is relative to the meter
45 | convert_dict = {
46 | "m":1,
47 | "ft":0.3048,
48 | "yd":0.9144,
49 | "mi":1609.34,
50 | "nmi":1852,
51 | "km":1000,
52 | }
53 |
54 | # Standardizing the text of the units
55 | units_standard = {
56 | # degrees
57 | # note these are not valid units to convert INTO
58 | # "deg":"deg", "deg":"degree",
59 | # feet
60 | # the last one at the end is how many projections report it
61 | "ft":"ft", "ftUS":"ft", "foot":"ft", "feet":"ft", "US survey foot":"ft",
62 | # yards
63 | "yd":"yd", "yard":"yd", "yards":"yd",
64 | # miles
65 | "mi":"mi", "mile":"mi", "miles":"mi",
66 | # nautical miles
67 | # note that nm is NOT accepted - that is nanometers!
68 | "nmi":"nmi", "nautical":"nmi", "nautical mile":"nmi", "nautical miles":"nmi",
69 | # meters
70 | "m":"m", "meter":"m", "metre":"m", "meters":"m", "metres":"m",
71 | # kilometers
72 | "km":"km", "kilometer":"km", "kilometers":"km", "kilometre":"km", "kilometres":"km",
73 | }
74 |
75 | ### TYPE HINTS ###
76 | # This section of the code is for defining structured dictionaries and lists
77 | # for the inputs necessary for object creation we've created (such as the style dictionaries)
78 | # so that intellisense can help with autocompletion
79 |
80 | class _TYPE_BAR(TypedDict, total=False):
81 | projection: str | int | pyproj.CRS # should be a valid cartopy or pyproj crs, or a string or int that can be converted to that
82 | unit: Literal["m","km","ft","yd","mi","nmi"] # the units you want to convert the bar to, if different than the projection units
83 | rotation: float | int # between -360 and 360
84 | max: float | int # the max bar value, in desired units (as specified by units dict)
85 | length: float | int # the length of the bar in inches (if > 1) or as a % of the axis (if between 0 and 1)
86 | height: float | int # the height of the bar in inches
87 | reverse: bool # flag if the order of the elements should be reversed
88 | major_div: int # the number of major divisions on the bar
89 | minor_div: int # the number of minor divisions on the bar
90 | minor_frac: float # the fraction of the major division that the minor division should be (e.g. 0.5 = half the size of the major division)
91 | minor_type: Literal["all","first","none"] # whether the minor divisions should be drawn on all major divisions or just the first one
92 | major_mult: float | int # used in conjunction with major_div to define the length of the bar, if desired
93 | # Boxes only
94 | facecolors: list | tuple | str # a color or list of colors to use for the faces of the boxes
95 | edgecolors: list | tuple | str # a color or list of colors to use for the edges of the boxes
96 | edgewidth: float | int # the line thickness of the edges of the boxes
97 | # Ticks only
98 | tick_loc: Literal["above","below","middle"] # the location of the ticks relative to the bar
99 | basecolors: list | tuple | str # a color or list of colors to use for the bottom bar
100 | tickcolors: list | tuple | str # a color or list of colors to use for the ticks
101 | tickwidth: float | int # the line thickness of the bottom bar and ticks
102 |
103 |
104 | class _TYPE_LABELS(TypedDict, total=False):
105 | labels: list | tuple # a list of text labels to replace the default labels of the major elements
106 | format: str # a format string to apply to the default labels of the major elements
107 | format_int: bool # if True, float divisions that end in zero wil be converted to ints (e.g. 1.0 -> 1)
108 | style: Literal["major","first_last","last_only","minor_all","minor_first"] # each selection in the list creates a different set of labels
109 | loc: Literal["above","below"] # whether the major text elements should appear above or below the bar
110 | fontsize: str | float | int # any fontsize value for matplotlib
111 | textcolors: list | str # a color or list of colors to use for the major text elements
112 | fontfamily: Literal["serif", "sans-serif", "cursive", "fantasy", "monospace"] # from matplotlib documentation
113 | fontstyle: Literal["normal", "italic", "oblique"] # from matplotlib documentation
114 | fontweight: Literal["normal", "bold", "heavy", "light", "ultrabold", "ultralight"] # from matplotlib documentation
115 | stroke_width: float | int # between 0 and infinity
116 | stroke_color: str # optional: any color value for matplotlib
117 | rotation: float | int # a value between -360 and 360 to rotate the text elements by
118 | rotation_mode: Literal["anchor","default"] # from matplotlib documentation
119 | sep: float | int # between 0 and inf, used to add separation between the labels and the bar
120 | pad: float | int # between 0 and inf, used to add separation between the labels and the bar
121 |
122 |
123 | class _TYPE_UNITS(TypedDict, total=False):
124 | label: str # an override for the units label
125 | loc: Literal["bar","text","opposite"] # where the units text should appear (in line with the bar, or the major div text, or opposite the major div text)
126 | fontsize: str | float | int # any fontsize value for matplotlib
127 | textcolor: str # any color value for matplotlib
128 | fontfamily: Literal["serif", "sans-serif", "cursive", "fantasy", "monospace"] # from matplotlib documentation
129 | fontstyle: Literal["normal", "italic", "oblique"] # from matplotlib documentation
130 | fontweight: Literal["normal", "bold", "heavy", "light", "ultrabold", "ultralight"] # from matplotlib documentation
131 | stroke_width: float | int # between 0 and infinity
132 | stroke_color: str # any color value for matplotlib
133 | rotation: float | int # between -360 and 360
134 | rotation_mode: Literal["anchor","default"] # from matplotlib documentation
135 | sep: float | int # between 0 and inf, used to add separation between the units text and the bar ("opposite" only)
136 | pad: float | int # between 0 and inf, used to add separation between the units text and the bar ("opposite" only)
137 |
138 |
139 | class _TYPE_TEXT(TypedDict, total=False):
140 | fontsize: str | float | int # any fontsize value for matplotlib
141 | textcolor: list | str # a color or list of colors to use for all the text elements
142 | fontfamily: Literal["serif", "sans-serif", "cursive", "fantasy", "monospace"] # from matplotlib documentation
143 | fontstyle: Literal["normal", "italic", "oblique"] # from matplotlib documentation
144 | fontweight: Literal["normal", "bold", "heavy", "light", "ultrabold", "ultralight"] # from matplotlib documentation
145 | stroke_width: float | int # between 0 and infinity
146 | stroke_color: str # optional: any color value for matplotlib
147 | rotation: float | int # a value between -360 and 360 to rotate the text elements by
148 | rotation_mode: Literal["anchor","default"] # from matplotlib documentation
149 |
150 |
151 | class _TYPE_AOB(TypedDict, total=False):
152 | facecolor: str # NON-STANDARD: used to set the facecolor of the offset box (i.e. to white), any color vlaue for matplotlib
153 | edgecolor: str # NON-STANDARD: used to set the edge of the offset box (i.e. to black), any color vlaue for matplotlib
154 | alpha: float | int # NON-STANDARD: used to set the transparency of the face color of the offset box^, between 0 and 1
155 | pad: float | int # between 0 and inf
156 | borderpad: float | int # between 0 and inf
157 | prop: str | float | int # any fontsize value for matplotlib
158 | frameon: bool # any bool
159 | # bbox_to_anchor: None # NOTE: currently unvalidated, use at your own risk!
160 | # bbox_transform: None # NOTE: currently unvalidated, use at your own risk!
161 |
162 | ### VALIDITY DICTS ###
163 | # These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
164 | # into dictionaries that can be used to validate all the inputs to a dictionary at once
165 |
166 | _VALIDATE_PRIMARY = {
167 | "style":{"func":vf._validate_list, "kwargs":{"list":["ticks","boxes"]}},
168 | "location":{"func":vf._validate_list, "kwargs":{"list":["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]}},
169 | "zorder":{"func":vf._validate_type, "kwargs":{"match":int}}, # only check that it is an int
170 | }
171 |
172 | _VALID_BAR_TICK_LOC = get_args(_TYPE_BAR.__annotations__["tick_loc"])
173 | _VALID_BAR_MINOR_TYPE = get_args(_TYPE_BAR.__annotations__["minor_type"])
174 |
175 | _VALIDATE_BAR = {
176 | "projection":{"func":vf._validate_or, "kwargs":{"funcs":[vf._validate_projection, vf._validate_list], "kwargs":[{"none_ok":False}, {"list":["px","pixel","pixels","pt","point","points","dx","custom","axis"], "none_ok":False}]}}, # between 0 and inf, or a two-tuple of (x,y) size, each between 0 and inf
177 | "unit":{"func":vf._validate_list, "kwargs":{"list":list(units_standard.keys()), "none_ok":True}}, # any of the listed unit values are accepted
178 | "rotation":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # between -360 and 360 degrees
179 | "max":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
180 | "length":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
181 | "height":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
182 | "reverse":{"func":vf._validate_type, "kwargs":{"match":bool}}, # any bool
183 |
184 | "major_div":{"func":vf._validate_range, "kwargs":{"min":1, "max":None, "none_ok":True}}, # between 1 and inf
185 | "minor_div":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
186 | "minor_frac":{"func":vf._validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # ticks only: between 0 and 1
187 | "minor_type":{"func":vf._validate_list, "kwargs":{"list":_VALID_BAR_MINOR_TYPE, "none_ok":True}}, # any item in the list, or None (for no minor)
188 | "major_mult":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
189 |
190 | "facecolors":{"func":vf._validate_iterable, "kwargs":{"func":matplotlib.rcsetup.validate_color}}, # boxes only: any color value for matplotlib
191 | "edgecolors":{"func":vf._validate_iterable, "kwargs":{"func":matplotlib.rcsetup.validate_color}}, # boxes only: any color value for matplotlib
192 | "edgewidth":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # boxes only: between 0 and inf
193 |
194 | "tick_loc":{"func":vf._validate_list, "kwargs":{"list":_VALID_BAR_TICK_LOC}}, # ticks only: any item in the list
195 | "basecolors":{"func":vf._validate_iterable, "kwargs":{"func":matplotlib.rcsetup.validate_color}}, # ticks only: any color value for matplotlib
196 | "tickcolors":{"func":vf._validate_iterable, "kwargs":{"func":matplotlib.rcsetup.validate_color}}, # ticks only: any color value for matplotlib
197 | "tickwidth":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # ticks only: between 0 and inf
198 | }
199 |
200 | _VALID_LABELS_STYLE = get_args(_TYPE_LABELS.__annotations__["style"])
201 | _VALID_LABELS_LOC = get_args(_TYPE_LABELS.__annotations__["loc"])
202 | _VALID_LABELS_FONTFAMILY = get_args(_TYPE_LABELS.__annotations__["fontfamily"])
203 | _VALID_LABELS_FONTSTYLE = get_args(_TYPE_LABELS.__annotations__["fontstyle"])
204 | _VALID_LABELS_FONTWEIGHT = get_args(_TYPE_LABELS.__annotations__["fontweight"])
205 | _VALID_LABELS_ROTATION_MODE = get_args(_TYPE_LABELS.__annotations__["rotation_mode"])
206 |
207 | _VALIDATE_LABELS = {
208 | "labels":{"func":vf._validate_iterable, "kwargs":{"func":vf._validate_types,"kwargs":{"matches":[str,bool,int,float], "none_ok":True}}}, # any list of strings
209 | "format":{"func":vf._validate_type, "kwargs":{"match":str}}, # only check that it is a string, not that it is a valid format string
210 | "format_int":{"func":vf._validate_type, "kwargs":{"match":bool}}, # any bool
211 | "style":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABELS_STYLE}}, # any item in the list
212 | "loc":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABELS_LOC, "none_ok":True}}, # any string in the list we allow
213 | "fontsize":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize for matplotlib
214 | "textcolors":{"func":vf._validate_iterable, "kwargs":{"func":matplotlib.rcsetup.validate_color}}, # any color value for matplotlib
215 | "fontfamily":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABELS_FONTFAMILY}},
216 | "fontstyle":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABELS_FONTSTYLE}},
217 | "fontweight":{"func":matplotlib.rcsetup.validate_fontweight}, # any fontweight value for matplotlib
218 | "stroke_width":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
219 | "stroke_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
220 | "rotation":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # between -360 and 360 degrees
221 | "rotation_mode":{"func":vf._validate_list, "kwargs":{"list":_VALID_LABELS_ROTATION_MODE, "none_ok":True}}, # any string in the list we allow
222 | "sep":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
223 | "pad":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
224 | }
225 |
226 | _VALID_UNITS_LOC = get_args(_TYPE_UNITS.__annotations__["loc"])
227 | _VALID_UNITS_FONTFAMILY = get_args(_TYPE_UNITS.__annotations__["fontfamily"])
228 | _VALID_UNITS_FONTSTYLE = get_args(_TYPE_UNITS.__annotations__["fontstyle"])
229 | _VALID_UNITS_FONTWEIGHT = get_args(_TYPE_UNITS.__annotations__["fontweight"])
230 | _VALID_UNITS_ROTATION_MODE = get_args(_TYPE_UNITS.__annotations__["rotation_mode"])
231 |
232 | _VALIDATE_UNITS = {
233 | "label":{"func":vf._validate_type, "kwargs":{"match":str, "none_ok":True}}, # any string
234 | "loc":{"func":vf._validate_list, "kwargs":{"list":_VALID_UNITS_LOC, "none_ok":True}}, # any string in the list we allow
235 | "fontsize":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize for matplotlib
236 | "textcolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
237 | "fontfamily":{"func":vf._validate_list, "kwargs":{"list":_VALID_UNITS_FONTFAMILY}},
238 | "fontstyle":{"func":vf._validate_list, "kwargs":{"list":_VALID_UNITS_FONTSTYLE}},
239 | "fontweight":{"func":matplotlib.rcsetup.validate_fontweight}, # any fontweight value for matplotlib
240 | "stroke_width":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
241 | "stroke_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
242 | "rotation":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # between -360 and 360 degrees
243 | "rotation_mode":{"func":vf._validate_list, "kwargs":{"list":_VALID_UNITS_ROTATION_MODE, "none_ok":True}}, # any string in the list we allow
244 | "sep":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
245 | "pad":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
246 | }
247 |
248 | _VALID_TEXT_FONTFAMILY = get_args(_TYPE_TEXT.__annotations__["fontfamily"])
249 | _VALID_TEXT_FONTSTYLE = get_args(_TYPE_TEXT.__annotations__["fontstyle"])
250 | _VALID_TEXT_FONTWEIGHT = get_args(_TYPE_TEXT.__annotations__["fontweight"])
251 | _VALID_TEXT_ROTATION_MODE = get_args(_TYPE_TEXT.__annotations__["rotation_mode"])
252 |
253 | _VALIDATE_TEXT = {
254 | "fontsize":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize for matplotlib
255 | "textcolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
256 | "fontfamily":{"func":vf._validate_list, "kwargs":{"list":_VALID_TEXT_FONTFAMILY}},
257 | "fontstyle":{"func":vf._validate_list, "kwargs":{"list":_VALID_TEXT_FONTSTYLE}},
258 | "fontweight":{"func":matplotlib.rcsetup.validate_fontweight}, # any fontweight value for matplotlib
259 | "stroke_width":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
260 | "stroke_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
261 | "rotation":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # between -360 and 360 degrees
262 | "rotation_mode":{"func":vf._validate_list, "kwargs":{"list":_VALID_TEXT_ROTATION_MODE, "none_ok":True}}, # any string in the list we allow
263 | }
264 |
265 | _VALIDATE_AOB = {
266 | "facecolor":{"func":vf._validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
267 | "edgecolor":{"func":vf._validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
268 | "alpha":{"func":vf._validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # any value between 0 and 1
269 | "pad":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
270 | "borderpad":{"func":vf._validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
271 | "prop":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize value for matplotlib
272 | "frameon":{"func":vf._validate_type, "kwargs":{"match":bool}}, # any bool
273 | "bbox_to_anchor":{"func":vf._skip_validation}, # NOTE: currently unvalidated, use at your own risk!
274 | "bbox_transform":{"func":vf._skip_validation}, # NOTE: currently unvalidated, use at your own risk!
275 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ---
4 |
5 | **Documentation:** See `docs` folder
6 |
7 | **Source Code:** [Available on GitHub](https://github.com/moss-xyz/matplotlib-map-utils)
8 |
9 | **Feedback:** I welcome any and all feedback! See the *Development Notes* below for more details.
10 |
11 | ---
12 |
13 | ### 👋 Introduction
14 |
15 | `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/).
16 |
17 | As of `v3.x` (the current version), this includes three-ish elements:
18 |
19 | * `north_arrow.py`, for adding a north arrow to a given plot.
20 |
21 | * `scale_bar.py`, for adding a scale bar to a given plot.
22 |
23 | * `inset_map.py`, for adding inset maps and detail/extent indicators to a given plot.
24 |
25 | The three elements listed above are all intended to be high-resolution, easily modifiable, and context-aware, relative to your specific plot.
26 |
27 | This package also contains a single utility object:
28 |
29 | * `usa.py`, which contains a class that helps filter for states and territories within the USA based on given characteristics.
30 |
31 | Together, these allow for the easy creation of a map such as the following:
32 |
33 | 
34 |
35 | ---
36 |
37 | ### 💾 Installation
38 |
39 | This package is available on PyPi, and can be installed like so:
40 |
41 | ```bash
42 | pip install matplotlib-map-utils
43 | ```
44 |
45 | The requirements for this package are:
46 |
47 | * `python >= 3.10` (due to the use of the pipe operator to concatenate dictionaries and types)
48 |
49 | * `matplotlib >= 3.9` (might work with lower versions but not guaranteed)
50 |
51 | * `cartopy >= 0.23` (due to earlier bug with calling `copy()` on `CRS` objects)
52 |
53 | ---
54 |
55 | ### 📦 Package Structure
56 |
57 |
58 | The package is arrayed in the following way:
59 |
60 | ```bash
61 | package_name/
62 | ├── __init__.py
63 | │
64 | ├── core/
65 | │ ├── __init__.py
66 | │ ├── inset_map.py
67 | │ ├── north_arrow.py
68 | │ ├── scale_bar.py
69 | ├── validation/
70 | │ ├── __init__.py
71 | │ ├── functions.py
72 | │ └── inset_map.py
73 | │ ├── north_arrow.py
74 | │ └── scale_bar.py
75 | ├── defaults/
76 | │ ├── __init__.py
77 | │ ├── north_arrow.py
78 | │ └── scale_bar.py
79 | │ └── inset_map.py
80 | ├── utils/
81 | │ ├── __init__.py
82 | │ ├── usa.py
83 | │ └── usa.json
84 | ```
85 |
86 | Where:
87 |
88 | * `core` contains the main functions and classes for each object
89 |
90 | * `validation` contains type hints for each variable and functions to validate inputs
91 |
92 | * `defaults` contains default settings for each object at different paper sizes
93 |
94 | * `utils` contains utility functions and objects
95 |
96 |
97 |
98 | ---
99 |
100 | ### 🧭 North Arrow
101 |
102 |
103 | Expand instructions
104 |
105 | #### Quick Start
106 |
107 | Importing the North Arrow functions and classes can be done like so:
108 |
109 | ```py
110 | from matplotlib_map_utils.core.north_arrow import NorthArrow, north_arrow
111 | from matplotlib_map_utils.core import NorthArrow, north_arrow # also valid
112 | from matplotlib_map_utils import NorthArrow, north_arrow # also valid
113 | ```
114 |
115 | The quickest way to add a single north arrow to a single plot is to use the `north_arrow` function:
116 |
117 | ```python
118 | # Setting up a plot
119 | fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
120 | # Adding a north arrow to the upper-right corner of the axis, without any rotation (see Rotation under Formatting Components for details)
121 | north_arrow.north_arrow(ax=ax, location="upper right", rotation={"degrees":0})
122 | ```
123 |
124 | An object-oriented approach is also supported:
125 |
126 | ```python
127 | # Setting up a plot
128 | fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
129 | # Creating a north arrow for the upper-right corner of the axis, without any rotation (see Rotation under Formatting Components for details)
130 | na = north_arrow.NorthArrow(location="upper right", rotation={"degrees":0})
131 | # Adding the artist to the plot
132 | ax.add_artist(na)
133 | ```
134 |
135 | Both of these will create an output like the following:
136 |
137 | 
138 |
139 | #### Customization
140 |
141 | Both the object-oriented and functional approaches can be customized to allow for fine-grained control over formatting:
142 |
143 | ```python
144 | north_arrow(
145 | ax,
146 | location = "upper right", # accepts a valid string from the list of locations
147 | scale = 0.5, # accepts a valid positive float or integer
148 | # each of the follow accepts arguments from a customized style dictionary
149 | base = {"facecolor":"green"},
150 | fancy = False,
151 | label = {"text":"North"},
152 | shadow = {"alpha":0.8},
153 | pack = {"sep":6},
154 | aob = {"pad":2},
155 | rotation = {"degrees": 35}
156 | )
157 | ```
158 |
159 | This will create an output like the following:
160 |
161 | 
162 |
163 | Refer to `docs\howto_north_arrow` for details on how to customize each facet of the north arrow.
164 |
165 | _Note: only add a north arrow **after** adding all of your geodata and changing your axis limits!_
166 |
167 | #### Rotation
168 |
169 | The north arrow object is also capable of pointing towards "true north", given a CRS and reference point:
170 |
171 | 
172 |
173 | Instructions for how to do so can be found in `docs\howto_north_arrow`.
174 |
175 |
176 | ---
177 |
178 | ### 📏 Scale Bar
179 |
180 |
181 | Expand instructions
182 |
183 | #### Quick Start
184 |
185 | Importing the Scale Bar functions and classes can be done like so:
186 |
187 | ```py
188 | from matplotlib_map_utils.core.scale_bar import ScaleBar, scale_bar
189 | from matplotlib_map_utils.core import ScaleBar, scale_bar # also valid
190 | from matplotlib_map_utils import ScaleBar, scale_bar # also valid
191 | ```
192 |
193 | 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:
194 |
195 | ```python
196 | # Setting up a plot
197 | # NOTE: you MUST set the desired DPI here, when the subplots are created
198 | # so that the scale_bar's DPI matches!
199 | fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
200 | # Adding a scale bar to the upper-right corner of the axis, in the same projection as whatever geodata you plotted
201 | # Here, this scale bar will have the "boxes" style
202 | scale_bar(ax=ax, location="upper right", style="boxes", bar={"projection":3857})
203 | ```
204 |
205 | An object-oriented approach is also supported:
206 |
207 | ```python
208 | # Setting up a plot
209 | # NOTE: you MUST set the desired DPI here, when the subplots are created
210 | # so that the scale_bar's DPI matches!
211 | fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
212 | # Adding a scale bar to the upper-right corner of the axis, in the same projection as whatever geodata you plotted
213 | # Here, we change the boxes to "ticks"
214 | sb = ScaleBar(location="upper right", style="ticks", bar={"projection":3857})
215 | # Adding the artist to the plot
216 | ax.add_artist(sb)
217 | ```
218 |
219 | Both of these will create an output like the following (function is left, class is right):
220 |
221 | 
222 |
223 | #### Customization
224 |
225 | Both the object-oriented and functional approaches can be customized to allow for fine-grained control over formatting:
226 |
227 | ```python
228 | scale_bar(
229 | ax,
230 | location = "upper right", # accepts a valid string from the list of locations
231 | style = "boxes", # accepts a valid positive float or integer
232 | # each of the follow accepts arguments from a customized style dictionary
233 | bar = {"unit":"mi", "length":2}, # converting the units to miles, and changing the length of the bar (in inches)
234 | labels = {"style":"major", "loc":"below"}, # placing a label on each major division, and moving them below the bar
235 | units = {"loc":"text"}, # changing the location of the units text to the major division labels
236 | text = {"fontfamily":"monospace"}, # changing the font family of all the text to monospace
237 | )
238 | ```
239 |
240 | This will create an output like the following:
241 |
242 | 
243 |
244 | Refer to `docs\howto_scale_bar` for details on how to customize each facet of the scale bar.
245 |
246 | _Note: only add a scale bar **after** adding all of your geodata and changing your axis limits!_
247 |
248 | #### Specifying Length
249 |
250 | There are three main ways of specifying the length of a scale bar:
251 |
252 | - `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).
253 | - The default value of the scale bar utilizes this method, with a `length` value of `0.25` (meaning 25% of the axis).
254 | - It will automatically orient itself against the horizontal or vertical axis when calculating its fraction, based on the value supplied for `rotation`.
255 | - 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.
256 | - Values `major_div` and `minor_div` are ignored, while a value for `max` will _override_ `length`.
257 |
258 | - `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`.
259 | - 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.
260 | - 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.
261 | - Will _override_ any value provided for `length`, and give a warning that it is doing so!
262 | - 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).
263 |
264 | - `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.
265 | - 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.
266 | - This is the _only_ use case for `major_mult` - using it anywhere else will result in warnings and/or errors!
267 | - Specifying either `max` or `length` will override this method!
268 | - `minor_div` can still be _optionally_ provided.
269 |
270 | 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:
271 |
272 | - 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)
273 |
274 | - 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))
275 |
276 | - 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))
277 |
278 | 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).
279 |
280 |
281 | ---
282 |
283 | ### 🗺️ Inset Map
284 |
285 |
286 | Expand instructions
287 |
288 | #### Quick Start
289 |
290 | Importing the Inset Map functions and classes can be done like so:
291 |
292 | ```py
293 | from matplotlib_map_utils.core.inset_map import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail
294 | from matplotlib_map_utils.core import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail # also valid
295 | from matplotlib_map_utils import InsetMap, inset_map, ExtentIndicator, indicate_extent, DetailIndicator, indicate_detail # also valid
296 | ```
297 |
298 | The quickest way to add a single inset map to an existing plot is the `inset_map` function:
299 |
300 | ```python
301 | # Setting up a plot
302 | fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
303 | # Adding an inset map to the upper-right corner of the axis
304 | iax = inset_map(ax=ax, location="upper right", size=0.75, pad=0, xticks=[], yticks=[])
305 | # You can now plot additional data to iax as desired
306 | ```
307 |
308 | An object-oriented approach is also supported:
309 |
310 | ```python
311 | # Setting up a plot
312 | fig, ax = matplotlib.pyplot.subplots(1,1, figsize=(5,5), dpi=150)
313 | # Creating an object for the inset map
314 | im = InsetMap(location="upper right", size=0.75, pad=0, xticks=[], yticks=[])
315 | # Adding the inset map template to the plot
316 | iax = im.create(ax=ax)
317 | # You can now plot additional data to iax as desired
318 | ```
319 |
320 | Both of these will create an output like the following:
321 |
322 | 
323 |
324 | #### Extent and Detail Indicators
325 |
326 | Inset maps can be paired with either an extent or detail indicator, to provide additional geographic context to the inset map
327 |
328 | ```python
329 | indicate_extent(inset_axis, parent_axis, inset_crs, parent_crs, ...)
330 | indicate_detail(parent_axis, inset_axis, parent_crs, inset_crs, ...)
331 | ```
332 |
333 | This will create an output like the following (extent indicator on the left, detail indicator on the right):
334 |
335 | 
336 |
337 | Refer to `docs\howto_inset_map` for details on how to customize the inset map and indicators to your liking.
338 |
339 |
340 | ---
341 |
342 | ### 🛠️ Utilities
343 |
344 |
345 | Expand instructions
346 |
347 | #### Quick Start
348 |
349 | Importing the bundled utility functions and classes can be done like so:
350 |
351 | ```py
352 | from matplotlib_map_utils.utils import USA
353 | ```
354 |
355 | 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.
356 |
357 | An example:
358 |
359 | ```python
360 | # Loading the object
361 | usa = USA()
362 | # Getting a list FIPS codes for US States
363 | usa.filter(states=True, to_return="fips")
364 | # Getting a list of State Names for states in the South and Midwest regions
365 | usa.filter(region=["South","Midwest"], to_return="name")
366 | ```
367 |
368 | Refer to `docs\howto_utils` for details on how to use this class, including with `pandas.apply()`.
369 |
370 |
371 | ---
372 |
373 | ### 📝 Development Notes
374 |
375 | #### Inspiration and Thanks
376 |
377 | 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.
378 |
379 | Two more projects assisted with the creation of this script:
380 |
381 | * [`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.
382 |
383 | * [`Cartopy`](https://github.com/SciTools/cartopy/issues/2361) fixed an issue inherent to calling `.copy()` on `CRS` objects.
384 |
385 | #### Releases
386 |
387 |
388 | See prior release notes
389 |
390 | - `v1.0.x`: Initial releases featuring the North Arrow element, along with some minor bug fixes.
391 |
392 | - `v2.0.0`: Initial release of the Scale Bar element.
393 |
394 | - `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).
395 |
396 | - `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.
397 |
398 | - `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.
399 |
400 |
401 | - `v3.0.0`: Release of inset map and extent and detail indicator classes and functions.
402 |
403 | - `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).
404 |
405 | - `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.
406 |
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 |
409 | #### Future Roadmap
410 |
411 | With the release of `v3.x`, this project has achieved full coverage of the "main" map elements I think are necessary.
412 |
413 |
414 | If I continue development of this project, I will be looking to add or fix the following features:
415 |
416 | * For all: switch to a system based on Pydantic for easier type validation
417 |
418 | * **North Arrow:**
419 |
420 | * Copy the image-rendering functionality of the Scale Bar to allow for rotation of the entire object, label and arrow together
421 |
422 | * Create more styles for the arrow, potentially including a compass rose and a line-only arrow
423 |
424 | * **Scale Bar:**
425 |
426 | * 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)
427 |
428 | * Fix/improve the `dual_bars()` function, which currently doesn't work great with rotations
429 |
430 | * Clean up the variable naming scheme (consistency on `loc` vs `position`, `style` vs `type`, etc.)
431 |
432 | * Create more styles for the bar, potentially including dual boxes and a sawtooth bar
433 |
434 | * **Inset Map:**
435 |
436 | * Clean up the way that connectors are drawn for detail indicators
437 |
438 | * New functionality for placing multiple inset maps at once (with context-aware positioning to prevent overlap with each other)
439 |
440 | * **Utils:**
441 |
442 | * (USA): Stronger fuzzy search mechanics, so that it will accept flexible inputs for FIPS/abbr/name
443 |
444 | * (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.)
445 |
446 | * (USA): Stronger typing options, so you don't have to recall which `region` or `division` types are available, etc.
447 |
448 | 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!
449 |
450 |
451 |
452 | #### Support and Contributions
453 |
454 | 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!
455 |
456 | 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.
457 |
458 | I am open to contributions, especially to help tackle the roadmap above!
459 |
460 | ---
461 |
462 | ### ⚖️ License
463 |
464 | 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.
--------------------------------------------------------------------------------
/matplotlib_map_utils/core/north_arrow.py:
--------------------------------------------------------------------------------
1 | ############################################################
2 | # north_arrow.py contains all the main objects and functions
3 | # for creating the north arrow artist rendered to plots
4 | ############################################################
5 |
6 | ### IMPORTING PACKAGES ###
7 |
8 | # Default packages
9 | import warnings
10 | import math
11 | import copy
12 | # Math packages
13 | import numpy
14 | # Geo packages
15 | import cartopy
16 | import pyproj
17 | # Graphical packages
18 | import matplotlib.artist
19 | import matplotlib.patches
20 | import matplotlib.patheffects
21 | import matplotlib.offsetbox
22 | # The types we use in this script
23 | from typing import Literal
24 | # The information contained in our helper scripts (validation and defaults)
25 | from ..defaults import north_arrow as nad
26 | from ..validation import north_arrow as nat
27 | from ..validation import functions as naf
28 |
29 | ### INITIALIZATION ###
30 |
31 | # Setting the defaults to the "medium" size, which is roughly optimized for A4/Letter paper
32 | # Making these as globals is important for the set_size() function to work later
33 | _DEFAULT_SCALE, _DEFAULT_BASE, _DEFAULT_FANCY, _DEFAULT_LABEL, _DEFAULT_SHADOW, _DEFAULT_PACK, _DEFAULT_AOB = nad._DEFAULTS_NA["md"]
34 | _DEFAULT_ROTATION = nad._ROTATION_ALL
35 |
36 | ### CLASSES ###
37 |
38 | # The main object model of the north arrow
39 | # Note that, except for location, all the components for the artist are dictionaries
40 | # These can be accessed and updated with dot notation (like NorthArrow.base)
41 | class NorthArrow(matplotlib.artist.Artist):
42 |
43 | ## INITIALIZATION ##
44 | def __init__(self, location: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]="upper right",
45 | scale: None | float | int=None,
46 | base: None | bool | nat._TYPE_BASE = None, fancy: None | bool | nat._TYPE_FANCY = None,
47 | label: None | bool | nat._TYPE_LABEL = None, shadow: None | bool | nat._TYPE_SHADOW = None,
48 | pack: None | nat._TYPE_PACK = None, aob: None | nat._TYPE_AOB = None, rotation: None | nat._TYPE_ROTATION = None,
49 | zorder: int=99,):
50 | # Starting up the object with the base properties of a matplotlib Artist
51 | matplotlib.artist.Artist.__init__(self)
52 |
53 | # If a dictionary is passed to any of the elements, validate that it is "correct", and then store the information
54 | # Note that we also merge the provided dict with the default style dict, so no keys are missing
55 | # If a specific component is not desired, it should be set to False during initialization
56 |
57 | # Location is stored as just a string
58 | location = naf._validate(nat._VALIDATE_PRIMARY, "location", location)
59 | self._location = location
60 |
61 | zorder = naf._validate(nat._VALIDATE_PRIMARY, "zorder", zorder)
62 | self._zorder = zorder
63 |
64 | # Scale will set to the default size if no value is passed
65 | scale = naf._validate(nat._VALIDATE_PRIMARY, "scale", scale)
66 | if scale is None:
67 | self._scale = _DEFAULT_SCALE
68 | else:
69 | self._scale = scale
70 |
71 | # Main elements
72 | base = naf._validate_dict(base, _DEFAULT_BASE, nat._VALIDATE_BASE, return_clean=True, parse_false=False)
73 | self._base = base
74 |
75 | fancy = naf._validate_dict(fancy, _DEFAULT_FANCY, nat._VALIDATE_FANCY, return_clean=True, parse_false=False)
76 | self._fancy = fancy
77 |
78 | label = naf._validate_dict(label, _DEFAULT_LABEL, nat._VALIDATE_LABEL, return_clean=True, parse_false=False)
79 | self._label = label
80 |
81 | shadow = naf._validate_dict(shadow, _DEFAULT_SHADOW, nat._VALIDATE_SHADOW, return_clean=True, parse_false=False)
82 | self._shadow = shadow
83 |
84 | # Other properties
85 | pack = naf._validate_dict(pack, _DEFAULT_PACK, nat._VALIDATE_PACK, return_clean=True, parse_false=False)
86 | self._pack = pack
87 | aob = naf._validate_dict(aob, _DEFAULT_AOB, nat._VALIDATE_AOB, return_clean=True, parse_false=False)
88 | self._aob = aob
89 | rotation = naf._validate_dict(rotation, _DEFAULT_ROTATION | rotation, nat._VALIDATE_ROTATION, return_clean=True, parse_false=False)
90 | self._rotation = rotation
91 |
92 | ## INTERNAL PROPERTIES ##
93 | # This allows for easy-updating of properties
94 | # Each property will have the same pair of functions
95 | # 1) calling the property itself returns its dictionary (NorthArrow.base will output {...})
96 | # 2) passing a dictionary will update key values (NorthArrow.base = {...} will update present keys)
97 |
98 | # location/loc
99 | @property
100 | def location(self):
101 | return self._location
102 |
103 | @location.setter
104 | def location(self, val: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]):
105 | val = naf._validate(nat._VALIDATE_PRIMARY, "location", val)
106 | self._location = val
107 |
108 | @property
109 | def loc(self):
110 | return self._location
111 |
112 | @loc.setter
113 | def loc(self, val: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]):
114 | val = naf._validate(nat._VALIDATE_PRIMARY, "location", val)
115 | self._location = val
116 |
117 | # scale
118 | @property
119 | def scale(self):
120 | return self._scale
121 |
122 | @scale.setter
123 | def scale(self, val: None | float | int):
124 | val = naf._validate(nat._VALIDATE_PRIMARY, "scale", val)
125 | if val is None:
126 | self._scale = _DEFAULT_SCALE
127 | else:
128 | self._scale = val
129 |
130 | # base
131 | @property
132 | def base(self):
133 | return self._base
134 |
135 | @base.setter
136 | def base(self, val: dict):
137 | val = naf._validate_type("base", val, dict)
138 | val = naf._validate_dict(val, self._base, nat._VALIDATE_BASE, return_clean=True, parse_false=False)
139 | self._base = val
140 |
141 | # fancy
142 | @property
143 | def fancy(self):
144 | return self._fancy
145 |
146 | @fancy.setter
147 | def fancy(self, val: dict):
148 | val = naf._validate_type("fancy", val, dict)
149 | val = naf._validate_dict(val, self._fancy, nat._VALIDATE_FANCY, return_clean=True, parse_false=False)
150 | self._fancy = val
151 |
152 | # label
153 | @property
154 | def label(self):
155 | return self._label
156 |
157 | @label.setter
158 | def label(self, val: dict):
159 | val = naf._validate_type("label", val, dict)
160 | val = naf._validate_dict(val, self._label, nat._VALIDATE_LABEL, return_clean=True, parse_false=False)
161 | self._label = val
162 |
163 | # shadow
164 | @property
165 | def shadow(self):
166 | return self._shadow
167 |
168 | @shadow.setter
169 | def shadow(self, val: dict):
170 | val = naf._validate_type("shadow", val, dict)
171 | val = naf._validate_dict(val, self._shadow, nat._VALIDATE_SHADOW, return_clean=True, parse_false=False)
172 | self._shadow = val
173 |
174 | # pack
175 | @property
176 | def pack(self):
177 | return self._pack
178 |
179 | @pack.setter
180 | def pack(self, val: dict):
181 | val = naf._validate_type("pack", val, dict)
182 | val = naf._validate_dict(val, self._pack, nat._VALIDATE_PACK, return_clean=True, parse_false=False)
183 | self._pack = val
184 |
185 | # aob
186 | @property
187 | def aob(self):
188 | return self._aob
189 |
190 | @aob.setter
191 | def aob(self, val: dict):
192 | val = naf._validate_type("aob", val, dict)
193 | val = naf._validate_dict(val, self._aob, nat._VALIDATE_AOB, return_clean=True, parse_false=False)
194 | self._aob = val
195 |
196 | # rotation
197 | @property
198 | def rotation(self):
199 | return self._rotation
200 |
201 | @rotation.setter
202 | def rotation(self, val: dict):
203 | val = naf._validate_type("rotation", val, dict)
204 | val = naf._validate_dict(val, self._rotation, nat._VALIDATE_ROTATION, return_clean=True, parse_false=False)
205 | self._rotation = val
206 |
207 | # zorder
208 | @property
209 | def zorder(self):
210 | return self._zorder
211 |
212 | @zorder.setter
213 | def zorder(self, val: int):
214 | val = naf._validate(nat._VALIDATE_PRIMARY, "zorder", val)
215 | self._zorder = val
216 |
217 | ## COPY FUNCTION ##
218 | # This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
219 | # Instead, you can use add_artist() like normal, but with add_artist(na.copy())
220 | # Thank you to the cartopy team for helping fix a bug with this!
221 | def copy(self):
222 | return copy.deepcopy(self)
223 |
224 | ## DRAW FUNCTION ##
225 | # Calling ax.add_artist() on this object triggers the following draw() function
226 | # THANK YOU to matplotlib-scalebar for figuring this out
227 | # Note that we never specify the renderer - the axis takes care of it!
228 | def draw(self, renderer, *args, **kwargs):
229 | # Can re-use the drawing function we already established, but return the object instead
230 | na_artist = north_arrow(ax=self.axes, location=self._location, scale=self._scale, draw=False,
231 | base=self._base, fancy=self._fancy,
232 | label=self._label, shadow=self._shadow,
233 | pack=self._pack, aob=self._aob, rotation=self._rotation,
234 | zorder=self._zorder)
235 | # This handles the actual drawing
236 | na_artist.axes = self.axes
237 | na_artist.set_figure(self.axes.get_figure())
238 | na_artist.set_zorder(self._zorder)
239 | na_artist.draw(renderer)
240 |
241 | ## SIZE FUNCTION ##
242 | # This function will update the default dictionaries used based on the size of map being created
243 | # See defaults.py for more information on the dictionaries used here
244 | def set_size(size: Literal["xs","xsmall","x-small",
245 | "sm","small",
246 | "md","medium",
247 | "lg","large",
248 | "xl","xlarge","x-large"]):
249 | # Bringing in our global default values to update them
250 | global _DEFAULT_SCALE, _DEFAULT_BASE, _DEFAULT_FANCY, _DEFAULT_LABEL, _DEFAULT_SHADOW, _DEFAULT_PACK, _DEFAULT_AOB
251 | # Changing the global default values as required
252 | if size.lower() in ["xs","xsmall","x-small"]:
253 | _DEFAULT_SCALE, _DEFAULT_BASE, _DEFAULT_FANCY, _DEFAULT_LABEL, _DEFAULT_SHADOW, _DEFAULT_PACK, _DEFAULT_AOB = nad._DEFAULTS_NA["xs"]
254 | elif size.lower() in ["sm","small"]:
255 | _DEFAULT_SCALE, _DEFAULT_BASE, _DEFAULT_FANCY, _DEFAULT_LABEL, _DEFAULT_SHADOW, _DEFAULT_PACK, _DEFAULT_AOB = nad._DEFAULTS_NA["sm"]
256 | elif size.lower() in ["md","medium"]:
257 | _DEFAULT_SCALE, _DEFAULT_BASE, _DEFAULT_FANCY, _DEFAULT_LABEL, _DEFAULT_SHADOW, _DEFAULT_PACK, _DEFAULT_AOB = nad._DEFAULTS_NA["md"]
258 | elif size.lower() in ["lg","large"]:
259 | _DEFAULT_SCALE, _DEFAULT_BASE, _DEFAULT_FANCY, _DEFAULT_LABEL, _DEFAULT_SHADOW, _DEFAULT_PACK, _DEFAULT_AOB = nad._DEFAULTS_NA["lg"]
260 | elif size.lower() in ["xl","xlarge","x-large"]:
261 | _DEFAULT_SCALE, _DEFAULT_BASE, _DEFAULT_FANCY, _DEFAULT_LABEL, _DEFAULT_SHADOW, _DEFAULT_PACK, _DEFAULT_AOB = nad._DEFAULTS_NA["xl"]
262 | else:
263 | raise ValueError("Invalid value supplied, try one of ['xsmall', 'small', 'medium', 'large', 'xlarge'] instead")
264 |
265 | ### DRAWING FUNCTIONS ###
266 |
267 | # This function presents a way to draw the north arrow independent of the NorthArrow object model
268 | # and is actually used by the object model when draw() is called anyways
269 | def north_arrow(ax, draw=True,
270 | location: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]="upper right",
271 | scale: None | float | int=None,
272 | base: None | bool | nat._TYPE_BASE=None,
273 | fancy: None | bool | nat._TYPE_FANCY=None,
274 | label: None | bool | nat._TYPE_LABEL=None,
275 | shadow: None | bool | nat._TYPE_SHADOW=None,
276 | pack: None | nat._TYPE_PACK=None,
277 | aob: None | nat._TYPE_AOB=None,
278 | rotation: None | nat._TYPE_ROTATION=None,
279 | zorder: int=99,):
280 |
281 | # First, validating the three primary inputs
282 | _location = naf._validate(nat._VALIDATE_PRIMARY, "location", location)
283 | _zorder = naf._validate(nat._VALIDATE_PRIMARY, "zorder", zorder)
284 |
285 | if scale is None:
286 | _scale = _DEFAULT_SCALE
287 | else:
288 | _scale = naf._validate(nat._VALIDATE_PRIMARY, "scale", scale)
289 |
290 | # This works the same as it does with the NorthArrow object
291 | # If a dictionary is passed to any of the elements, first validate that it is "correct"
292 | # Note that we also merge the provided dict with the default style dict, so no keys are missing
293 | # If a specific component is not desired, it should be set to False in the function call
294 | _base = naf._validate_dict(base, _DEFAULT_BASE, nat._VALIDATE_BASE, return_clean=True)
295 | _fancy = naf._validate_dict(fancy, _DEFAULT_FANCY, nat._VALIDATE_FANCY, return_clean=True)
296 | _label = naf._validate_dict(label, _DEFAULT_LABEL, nat._VALIDATE_LABEL, return_clean=True)
297 | _shadow = naf._validate_dict(shadow, _DEFAULT_SHADOW, nat._VALIDATE_SHADOW, return_clean=True)
298 | _pack = naf._validate_dict(pack, _DEFAULT_PACK, nat._VALIDATE_PACK, return_clean=True)
299 | _aob = naf._validate_dict(aob, _DEFAULT_AOB, nat._VALIDATE_AOB, return_clean=True)
300 | _rotation = naf._validate_dict(rotation, _DEFAULT_ROTATION, nat._VALIDATE_ROTATION, return_clean=True)
301 |
302 | # First, getting the figure for our axes
303 | fig = ax.get_figure()
304 |
305 | # We will place the arrow components in an AuxTransformBox so they are scaled in inches
306 | # Props to matplotlib-scalebar
307 | scale_box = matplotlib.offsetbox.AuxTransformBox(fig.dpi_scale_trans)
308 |
309 | ## BASE ARROW ##
310 | # Because everything is dependent on this component, it ALWAYS exists
311 | # However, if we don't want it (base=False), then we'll hide it
312 | if base == False:
313 | base_artist = matplotlib.patches.Polygon(_DEFAULT_BASE["coords"] * _scale, closed=True, visible=False, **_del_keys(_DEFAULT_BASE, ["coords","scale"]))
314 | else:
315 | base_artist = matplotlib.patches.Polygon(_base["coords"] * _scale, closed=True, visible=True, **_del_keys(_base, ["coords","scale"]))
316 |
317 | ## ARROW SHADOW ##
318 | # This is not its own artist, but instead just something we modify about the base artist using a path effect
319 | if _shadow:
320 | base_artist.set_path_effects([matplotlib.patheffects.withSimplePatchShadow(**_shadow)])
321 |
322 | # With our base arrow "done", we can add it to scale_box
323 | # which transforms our coordinates, multiplied by the scale factor, to inches
324 | # so a line from (0,0) to (0,1) would be 1 inch long, and from (0,0) to (0,0.5) half an inch, etc.
325 | scale_box.add_artist(base_artist)
326 |
327 | ## FANCY ARROW ##
328 | # If we want the fancy extra patch, we need another artist
329 | if _fancy:
330 | # Note that here, unfortunately, we are reliant on the scale attribute from the base arrow
331 | fancy_artist = matplotlib.patches.Polygon(_fancy["coords"] * _scale, closed=True, visible=bool(_fancy), **_del_keys(_fancy, ["coords"]))
332 | # It is also added to the scale_box so it is scaled in-line
333 | scale_box.add_artist(fancy_artist)
334 |
335 | ## LABEL ##
336 | # The final artist is for the label
337 | if _label:
338 | # Correctly constructing the textprops dict for the label
339 | text_props = _del_keys(_label, ["text","position","stroke_width","stroke_color"])
340 | # If we have stroke settings, create a path effect for them
341 | if _label["stroke_width"] > 0:
342 | label_stroke = [matplotlib.patheffects.withStroke(linewidth=_label["stroke_width"], foreground=_label["stroke_color"])]
343 | text_props["path_effects"] = label_stroke
344 | # The label is not added to scale_box, it lives in its own TextArea artist instead
345 | # Also, the dictionary does not need to be unpacked, textprops does that for us
346 | label_box = matplotlib.offsetbox.TextArea(_label["text"], textprops=text_props)
347 |
348 | ## STACKING THE ARTISTS ##
349 | # If we have multiple artists, we need to stack them using a V or H packer
350 | if _label and (_base or _fancy):
351 | if _label["position"]=="top":
352 | pack_box = matplotlib.offsetbox.VPacker(children=[label_box, scale_box], **_pack)
353 | elif _label["position"]=="bottom":
354 | pack_box = matplotlib.offsetbox.VPacker(children=[scale_box, label_box], **_pack)
355 | elif _label["position"]=="left":
356 | pack_box = matplotlib.offsetbox.HPacker(children=[label_box, scale_box], **_pack)
357 | elif _label["position"]=="right":
358 | pack_box = matplotlib.offsetbox.HPacker(children=[scale_box, label_box], **_pack)
359 | else:
360 | raise Exception("Invalid position applied, try one of 'top', 'bottom', 'left', 'right'")
361 | # If we only have the base, then that's the only thing we'll add to the box
362 | # I keep this in a VPacker just so that the rest of the code is functional, and doesn't depend on a million if statements
363 | else:
364 | pack_box = matplotlib.offsetbox.VPacker(children=[scale_box], **_pack)
365 |
366 | ## CREATING THE OFFSET BOX ##
367 | # The AnchoredOffsetBox allows us to place the pack_box relative to our axes
368 | # Note that the position string (upper left, lower right, center, etc.) comes from the location variable
369 | aob_box = matplotlib.offsetbox.AnchoredOffsetbox(loc=_location, child=pack_box, **_del_keys(_aob, ["facecolor","edgecolor","alpha"]))
370 | # Also setting the facecolor and transparency of the box
371 | if _aob["facecolor"] is not None:
372 | aob_box.patch.set_facecolor(_aob["facecolor"])
373 | aob_box.patch.set_visible(True)
374 | if _aob["edgecolor"] is not None:
375 | aob_box.patch.set_edgecolor(_aob["edgecolor"])
376 | aob_box.patch.set_visible(True)
377 | if _aob["alpha"]:
378 | aob_box.patch.set_alpha(_aob["alpha"])
379 | aob_box.patch.set_visible(True)
380 |
381 | ## ROTATING THE ARROW ##
382 | # If no rotation amount is passed, (attempt to) calculate it
383 | if _rotation["degrees"] is None:
384 | rotate_deg = _rotate_arrow(ax, _rotation)
385 | else:
386 | rotate_deg = _rotation["degrees"]
387 | # Then, apply the rotation to the aob box
388 | _iterative_rotate(aob_box, rotate_deg)
389 |
390 | aob_box.set_zorder(_zorder)
391 |
392 | ## DRAWING ##
393 | # If this option is set to true, we'll draw the final artists as desired
394 | if draw==True:
395 | _ = ax.add_artist(aob_box)
396 | # If not, we'll return the aob_box as an artist object (the NorthArrow draw() function uses this)
397 | else:
398 | return aob_box
399 |
400 | ### HELPING FUNTIONS ###
401 | # These are quick functions we use to help in other parts of this process
402 |
403 | # This function calculates the desired rotation of the arrow
404 | # It uses 3 pieces of information: the CRS, the reference frame, and the coordinates of the reference point
405 | # This code is 100% inspired by EOMaps, who also answered my questions about the inner workings of their equiavlent functions
406 | def _rotate_arrow(ax, rotate_dict) -> float | int:
407 | crs = rotate_dict["crs"]
408 | ref = rotate_dict["reference"]
409 | crd = rotate_dict["coords"] # should be (x,y) for axis ref, (lat,lng) for data ref
410 |
411 | ## CONVERTING FROM AXIS TO DATA COORDAINTES ##
412 | # If reference is set to axis, need to convert the axis coordinates (ranging from 0 to 1) to data coordinates (in native crs)
413 | if ref=="axis":
414 | # the transLimits transformation is for converting between data and axes units
415 | # so this code gets us the data (geographic) units of the chosen axis coordinates
416 | reference_point = ax.transLimits.inverted().transform((crd[0], crd[1]))
417 | # If reference is set to center, then do the same thing, but use the midpoint of the axis by default
418 | elif ref=="center":
419 | reference_point = ax.transLimits.inverted().transform((0.5,0.5))
420 | # Finally if the reference is set to data, we assume the provided coordinates are already in data units, no transformation needed!
421 | elif ref=="data":
422 | reference_point = crd
423 |
424 | ## CONVERTING TO GEODETIC COORDINATES ##
425 | # Initializing a CRS, so we can transform between coordinate systems appropriately
426 | if type(crs) == pyproj.CRS:
427 | og_proj = cartopy.crs.CRS(crs)
428 | else:
429 | try:
430 | og_proj = cartopy.crs.CRS(pyproj.CRS(crs))
431 | except:
432 | raise Exception("Invalid CRS Supplied")
433 | # Converting to the geodetic version of the CRS supplied
434 | gd_proj = og_proj.as_geodetic()
435 | # Converting reference point to the geodetic system
436 | reference_point_gd = gd_proj.transform_point(reference_point[0], reference_point[1], og_proj)
437 | # Converting the coordinates BACK to the original system
438 | reference_point = og_proj.transform_point(reference_point_gd[0], reference_point_gd[1], gd_proj)
439 | # And adding an offset to find "north", relative to that
440 | north_point = og_proj.transform_point(reference_point_gd[0], reference_point_gd[1] + 0.01, gd_proj)
441 |
442 | ## CALCULATING THE ANGLE ##
443 | # numpy.arctan2 wants coordinates in (y,x) because it flips them when doing the calculation
444 | # i.e. the angle found is between the line segment ((0,0), (1,0)) and ((0,0), (b,a)) when calling numpy.arctan2(a,b)
445 | try:
446 | rad = -1 * numpy.arctan2(north_point[0] - reference_point[0], north_point[1] - reference_point[1])
447 | except:
448 | warnings.warn("Unable to calculate rotation of arrow, setting to 0 degrees")
449 | rad = 0
450 | # Converting radians to degrees
451 | deg = math.degrees(rad)
452 |
453 | # Returning the degree number
454 | return deg
455 |
456 | # Unfortunately, matplotlib doesn't allow AnchoredOffsetBoxes or V/HPackers to have a rotation transformation (why? No idea)
457 | # So, we have to set it on the individual child objects (namely the base arrow and fancy arrow patches)
458 | def _iterative_rotate(artist, deg):
459 | # Building the affine rotation transformation
460 | transform_rotation = matplotlib.transforms.Affine2D().rotate_deg(deg)
461 | artist.set_transform(transform_rotation + artist.get_transform())
462 | # Repeating the process if there is a child component
463 | if artist.get_children():
464 | for child in artist.get_children():
465 | _iterative_rotate(child, deg)
466 |
467 | # This function will remove any keys we specify from a dictionary
468 | # This is useful if we need to unpack on certain values from a dictionary, and is used in north_arrow()
469 | def _del_keys(dict, to_remove):
470 | return {key: val for key, val in dict.items() if key not in to_remove}
--------------------------------------------------------------------------------
/matplotlib_map_utils/docs/howto_utils.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "## **Map Utilities**\n",
8 | "This notebook will provide a tutorial for using the utility classes and functions in this package to aid with mapping"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "metadata": {},
14 | "source": [
15 | "### **Set-Up**"
16 | ]
17 | },
18 | {
19 | "cell_type": "code",
20 | "execution_count": 1,
21 | "metadata": {},
22 | "outputs": [],
23 | "source": [
24 | "# Packages used by this tutorial\n",
25 | "import geopandas # manipulating geographic data\n",
26 | "import numpy # creating arrays\n",
27 | "import pygris # easily acquiring shapefiles from the US Census\n",
28 | "import matplotlib.pyplot # visualization"
29 | ]
30 | },
31 | {
32 | "cell_type": "code",
33 | "execution_count": null,
34 | "metadata": {},
35 | "outputs": [],
36 | "source": [
37 | "# Downloading the state-level dataset from pygris\n",
38 | "states = pygris.states(cb=True, year=2022, cache=False).to_crs(3857)"
39 | ]
40 | },
41 | {
42 | "cell_type": "markdown",
43 | "metadata": {},
44 | "source": [
45 | "### **USA**\n",
46 | "\n",
47 | "The `USA` class within `utils` is intended to help users (a) quickly isolate subsets of states they want to include in their maps, and (b) enrich their data with additional characteristics (such as state abbreviations, regional/divisional groupings, and the like)"
48 | ]
49 | },
50 | {
51 | "cell_type": "code",
52 | "execution_count": 3,
53 | "metadata": {},
54 | "outputs": [],
55 | "source": [
56 | "# Importing the main package\n",
57 | "from matplotlib_map_utils.utils import USA"
58 | ]
59 | },
60 | {
61 | "cell_type": "code",
62 | "execution_count": 4,
63 | "metadata": {},
64 | "outputs": [],
65 | "source": [
66 | "# Creating a usa object\n",
67 | "usa = USA() # this will load the data from ./utils/usa.json"
68 | ]
69 | },
70 | {
71 | "cell_type": "markdown",
72 | "metadata": {},
73 | "source": [
74 | "The `USA` class contains a list of all [states](https://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States#States) and [territories](https://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States#Territories) for the USA in a list of dictionary objects.\n",
75 | "\n",
76 | "The included states and territories are based upon [this Wikipedia page](https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code) listing all the available FIPS codes.\n",
77 | "\n",
78 | "Each state and territory has the following attributes available for it:\n",
79 | "\n",
80 | "* `fips`: A two-character `string` representing [the FIPS code](https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code#FIPS_state_codes). *Note that both FIPS 5-1 and FIPS 5-2 codes are included, but 5-1 Territory codes are marked as \"invalid\" (ex. FIPS code 66 is preferred over code 14 for Guam).*\n",
81 | "\n",
82 | "* `name`: A `string` representing the name of the state or territory, with proper captialization and punctuation. Generally follows the name provided in the FIPS code table (above), with some minor modifications (`Washington, D.C.` is used instead of `District of Columbia`).\n",
83 | "\n",
84 | "* `abbr`: A two-character `string` representing the [proper abbreviation](https://en.wikipedia.org/wiki/List_of_U.S._state_and_territory_abbreviations) (or \"alpha code\") for the state or territory. *Note that all states have abbreviations, but not all territories do*.\n",
85 | "\n",
86 | "* `valid`: A `boolean` variable representing if the given entry is *valid* according to **FIPS 5-2**. \"Invalid\" entries (for territories such as Guam, American Samoa, and the like) are retained for backwards compatibility with older datasets, but should be superseded by \"valid\" entires for these territories (usually, with a higher FIPS value).\n",
87 | "\n",
88 | "* `state`: A `boolean` variable representing if the given entry is a *state*, per [this list](https://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States#States). Note that `Washington, D.C.` is *not* a state.\n",
89 | "\n",
90 | "* `contiguous`: A `boolean` variable representing if the given entry is part of the *contiguous United States*, also referred to as the \"lower 48\" or *CONUS*, per [this list](https://en.wikipedia.org/wiki/Contiguous_United_States). Note that `Washington, D.C.` *is* included in this list.\n",
91 | "\n",
92 | "* `territory`: A `boolean` variable representing if the given entry is a *territory*, per [this list](https://en.wikipedia.org/wiki/Territories_of_the_United_States). Note that `Washington, D.C.` is *not* included in this list.\n",
93 | "\n",
94 | "* `region`: For states and Washington, D.C., this will be their [Census designated *region*](https://en.wikipedia.org/wiki/List_of_regions_of_the_United_States#Census_Bureau%E2%80%93designated_regions_and_divisions) (`Northeast`, `Midwest`, `South`, or `West`). For territories, this will be either `Inhabited Territory`, `Uninhabited Territory`, or `Sovereign State` (for Palau, Micronesia, and Marshall Islands).\n",
95 | "\n",
96 | "* `division`: For states and Washington, D.C., this will be their [Census designated *division*](https://en.wikipedia.org/wiki/List_of_regions_of_the_United_States#Census_Bureau%E2%80%93designated_regions_and_divisions) (such as `New England`, `West North Central`, `Mountain`, or `Pacific`). For territories, this will be either `Commonwealth`, `Compact of Free Association`, `Incorporated and Unorganized`, `Unincorporated and Unorganized`, `Unincorporated and Organized`, per [this list](https://en.wikipedia.org/wiki/Territories_of_the_United_States).\n",
97 | "\n",
98 | "* `omb`: For states and Washington, D.C., this will be their [OMB administrative region](https://en.wikipedia.org/wiki/List_of_regions_of_the_United_States#Agency_administrative_regions) (such as `Region I` or `Region IX`). For territories, this will have the same value has `region`.\n",
99 | "\n",
100 | "* `bea`: For states and Washington, D.C., this will be their [Bureau of Economic Analysis region](https://en.wikipedia.org/wiki/List_of_regions_of_the_United_States#Bureau_of_Economic_Analysis_regions) (such as `Great Lakes` or `Far West`). For territories, this will have the same value has `region`.\n",
101 | "\n",
102 | "* `alias`: This field is only filled in if an entry has a common second name, such as `District of Columbia` instead of `Washington, D.C.`, and `Virgin Islands of the U.S.` instead of `U.S. Virgin Islands`. For most, it is left blank."
103 | ]
104 | },
105 | {
106 | "cell_type": "code",
107 | "execution_count": 5,
108 | "metadata": {},
109 | "outputs": [
110 | {
111 | "data": {
112 | "text/plain": [
113 | "{'fips': '01',\n",
114 | " 'name': 'Alabama',\n",
115 | " 'abbr': 'AL',\n",
116 | " 'valid': True,\n",
117 | " 'state': True,\n",
118 | " 'contiguous': True,\n",
119 | " 'territory': False,\n",
120 | " 'region': 'South',\n",
121 | " 'division': 'East South Central',\n",
122 | " 'omb': 'Region IV',\n",
123 | " 'bea': 'Southeast',\n",
124 | " 'alias': None}"
125 | ]
126 | },
127 | "execution_count": 5,
128 | "metadata": {},
129 | "output_type": "execute_result"
130 | }
131 | ],
132 | "source": [
133 | "# Looking at a single example\n",
134 | "usa.jurisdictions[0]"
135 | ]
136 | },
137 | {
138 | "cell_type": "markdown",
139 | "metadata": {},
140 | "source": [
141 | "#### **Filtering**\n",
142 | "\n",
143 | "All entries are available through entry points such as `usa.jurisdictions` (all *valid* entries), `usa.states` (all states), and `usa.territories` (all territories), for users to iterate over the list-of-dicts as desired. However, a convenience `filter()` function is also provided for the `USA` class, which allows users to easily apply layered filters.\n",
144 | "\n",
145 | "The arguments for `filter()` mirror the properties available for each state/territory, except for *alias* (see below), and they can accept either single values or lists-of-values.\n",
146 | "\n",
147 | "The final argument of `filter()` is `to_return`, which tells the function what value you want to return:\n",
148 | "\n",
149 | "* `fips` (default), `name`, or `abbr` will return *just that field* for each returned entry.\n",
150 | "\n",
151 | "* `object` or `dict` will return the full list-of-dicts that passes the filter\n",
152 | "\n",
153 | "Some notes:\n",
154 | "\n",
155 | "* Filters are applied \"top-to-bottom\" in the order they are shown-above, and act as \"and\" filters\n",
156 | "\n",
157 | "* If only a single entry is going to be returned, it will be removed from the list and returned as a single value\n",
158 | "\n",
159 | "* *Note that the* name *filter compares against both the* name *and* alias *fields*"
160 | ]
161 | },
162 | {
163 | "cell_type": "code",
164 | "execution_count": 6,
165 | "metadata": {},
166 | "outputs": [
167 | {
168 | "data": {
169 | "text/plain": [
170 | "['Alabama', 'Alaska', 'Delaware', 'Washington, D.C.']"
171 | ]
172 | },
173 | "execution_count": 6,
174 | "metadata": {},
175 | "output_type": "execute_result"
176 | }
177 | ],
178 | "source": [
179 | "# Filtering based on a list of FIPS codes\n",
180 | "usa.filter(fips=[\"01\",\"02\",\"10\",\"11\"], to_return=\"name\")"
181 | ]
182 | },
183 | {
184 | "cell_type": "code",
185 | "execution_count": 7,
186 | "metadata": {},
187 | "outputs": [
188 | {
189 | "data": {
190 | "text/plain": [
191 | "['CA', 'OR', 'WA']"
192 | ]
193 | },
194 | "execution_count": 7,
195 | "metadata": {},
196 | "output_type": "execute_result"
197 | }
198 | ],
199 | "source": [
200 | "# Filtering for Pacific contiguous states\n",
201 | "usa.filter(division=\"Pacific\", contiguous=True, to_return=\"abbr\")"
202 | ]
203 | },
204 | {
205 | "cell_type": "code",
206 | "execution_count": 8,
207 | "metadata": {},
208 | "outputs": [
209 | {
210 | "data": {
211 | "text/plain": [
212 | "'California'"
213 | ]
214 | },
215 | "execution_count": 8,
216 | "metadata": {},
217 | "output_type": "execute_result"
218 | }
219 | ],
220 | "source": [
221 | "# If only a single value is going to be returned, it will not be returned as a list\n",
222 | "usa.filter(abbr=\"CA\", to_return=\"name\")"
223 | ]
224 | },
225 | {
226 | "cell_type": "code",
227 | "execution_count": 9,
228 | "metadata": {},
229 | "outputs": [
230 | {
231 | "name": "stderr",
232 | "output_type": "stream",
233 | "text": [
234 | "C:\\Users\\david\\OneDrive\\Programming\\matplotlib-map-utils\\matplotlib_map_utils\\utils\\usa.py:291: UserWarning: No matching entities found. Please refer to the documentation and double-check your filters.\n",
235 | " warnings.warn(f\"No matching entities found. Please refer to the documentation and double-check your filters.\")\n"
236 | ]
237 | }
238 | ],
239 | "source": [
240 | "# If no entries are returned, a warning will show\n",
241 | "usa.filter(territory=True, state=True)"
242 | ]
243 | },
244 | {
245 | "cell_type": "markdown",
246 | "metadata": {},
247 | "source": [
248 | "`filter()` is intended to be easy-to-use whether filtering based on a single dimension, or multiple. However, each property also has its own filter available as a standalone function, following the form `filter_EXAMPLE()`: `filter_valid()`, `filter_fips()`, `filter_region()`, and so on.\n",
249 | "\n",
250 | "Each of these standalone functions accepts the same three arguments:\n",
251 | "\n",
252 | "* The `value` you want to filter by\n",
253 | "\n",
254 | "* (Optional) The list-of-dicts you want to filter (if left blank, will filter all valid states/territories)\n",
255 | "\n",
256 | "* `to_return`, which accepts the same arguments that `filter()` does\n",
257 | "\n",
258 | "Using this, you can build your own processing pipeline to filter the jurisdictions as you prefer."
259 | ]
260 | },
261 | {
262 | "cell_type": "code",
263 | "execution_count": 10,
264 | "metadata": {},
265 | "outputs": [
266 | {
267 | "data": {
268 | "text/plain": [
269 | "['Alabama',\n",
270 | " 'Arkansas',\n",
271 | " 'Delaware',\n",
272 | " 'Washington, D.C.',\n",
273 | " 'Florida',\n",
274 | " 'Georgia',\n",
275 | " 'Kentucky',\n",
276 | " 'Louisiana',\n",
277 | " 'Maryland',\n",
278 | " 'Mississippi',\n",
279 | " 'North Carolina',\n",
280 | " 'Oklahoma',\n",
281 | " 'South Carolina',\n",
282 | " 'Tennessee',\n",
283 | " 'Texas',\n",
284 | " 'Virginia',\n",
285 | " 'West Virginia']"
286 | ]
287 | },
288 | "execution_count": 10,
289 | "metadata": {},
290 | "output_type": "execute_result"
291 | }
292 | ],
293 | "source": [
294 | "# Getting all valid states\n",
295 | "valid = usa.filter_valid(True, to_return=\"object\")\n",
296 | "# Filtering that for all contiguous states\n",
297 | "contiguous = usa.filter_contiguous(True, valid, \"object\")\n",
298 | "# Filtering that for all Southern states\n",
299 | "south = usa.filter_region(\"South\", contiguous, \"name\")\n",
300 | "south"
301 | ]
302 | },
303 | {
304 | "cell_type": "markdown",
305 | "metadata": {},
306 | "source": [
307 | "#### **Pandas**\n",
308 | "\n",
309 | "The original impetus for this utility class was to help filter/enrich DataFrames and GeoDataFrames with additional data for each state/territory, which can be quite useful for plotting. See below for an example as to how this works."
310 | ]
311 | },
312 | {
313 | "cell_type": "code",
314 | "execution_count": 11,
315 | "metadata": {},
316 | "outputs": [
317 | {
318 | "data": {
319 | "text/html": [
320 | "
\n",
321 | "\n",
334 | "
\n",
335 | " \n",
336 | " \n",
337 | " | \n",
338 | " STATEFP | \n",
339 | " geometry | \n",
340 | "
\n",
341 | " \n",
342 | " \n",
343 | " \n",
344 | " | 0 | \n",
345 | " 35 | \n",
346 | " POLYGON ((-12139410.211 3695244.95, -12139373.... | \n",
347 | "
\n",
348 | " \n",
349 | " | 1 | \n",
350 | " 46 | \n",
351 | " POLYGON ((-11583670.355 5621144.876, -11582880... | \n",
352 | "
\n",
353 | " \n",
354 | " | 2 | \n",
355 | " 06 | \n",
356 | " MULTIPOLYGON (((-13202983.627 3958997.68, -132... | \n",
357 | "
\n",
358 | " \n",
359 | " | 3 | \n",
360 | " 21 | \n",
361 | " MULTIPOLYGON (((-9952591.899 4373541.269, -995... | \n",
362 | "
\n",
363 | " \n",
364 | " | 4 | \n",
365 | " 01 | \n",
366 | " MULTIPOLYGON (((-9802056.717 3568885.452, -980... | \n",
367 | "
\n",
368 | " \n",
369 | "
\n",
370 | "
"
371 | ],
372 | "text/plain": [
373 | " STATEFP geometry\n",
374 | "0 35 POLYGON ((-12139410.211 3695244.95, -12139373....\n",
375 | "1 46 POLYGON ((-11583670.355 5621144.876, -11582880...\n",
376 | "2 06 MULTIPOLYGON (((-13202983.627 3958997.68, -132...\n",
377 | "3 21 MULTIPOLYGON (((-9952591.899 4373541.269, -995...\n",
378 | "4 01 MULTIPOLYGON (((-9802056.717 3568885.452, -980..."
379 | ]
380 | },
381 | "execution_count": 11,
382 | "metadata": {},
383 | "output_type": "execute_result"
384 | }
385 | ],
386 | "source": [
387 | "# Let's say you had an incomplete GeoDataFrame, that just contained the FIPS Code (STATEFP)\n",
388 | "gdf = states[[\"STATEFP\",\"geometry\"]].copy()\n",
389 | "gdf.head()"
390 | ]
391 | },
392 | {
393 | "cell_type": "code",
394 | "execution_count": 12,
395 | "metadata": {},
396 | "outputs": [
397 | {
398 | "data": {
399 | "text/html": [
400 | "\n",
401 | "\n",
414 | "
\n",
415 | " \n",
416 | " \n",
417 | " | \n",
418 | " STATEFP | \n",
419 | " geometry | \n",
420 | " NAME | \n",
421 | "
\n",
422 | " \n",
423 | " \n",
424 | " \n",
425 | " | 0 | \n",
426 | " 35 | \n",
427 | " POLYGON ((-12139410.211 3695244.95, -12139373.... | \n",
428 | " New Mexico | \n",
429 | "
\n",
430 | " \n",
431 | " | 1 | \n",
432 | " 46 | \n",
433 | " POLYGON ((-11583670.355 5621144.876, -11582880... | \n",
434 | " South Dakota | \n",
435 | "
\n",
436 | " \n",
437 | " | 2 | \n",
438 | " 06 | \n",
439 | " MULTIPOLYGON (((-13202983.627 3958997.68, -132... | \n",
440 | " California | \n",
441 | "
\n",
442 | " \n",
443 | " | 3 | \n",
444 | " 21 | \n",
445 | " MULTIPOLYGON (((-9952591.899 4373541.269, -995... | \n",
446 | " Kentucky | \n",
447 | "
\n",
448 | " \n",
449 | " | 4 | \n",
450 | " 01 | \n",
451 | " MULTIPOLYGON (((-9802056.717 3568885.452, -980... | \n",
452 | " Alabama | \n",
453 | "
\n",
454 | " \n",
455 | "
\n",
456 | "
"
457 | ],
458 | "text/plain": [
459 | " STATEFP geometry NAME\n",
460 | "0 35 POLYGON ((-12139410.211 3695244.95, -12139373.... New Mexico\n",
461 | "1 46 POLYGON ((-11583670.355 5621144.876, -11582880... South Dakota\n",
462 | "2 06 MULTIPOLYGON (((-13202983.627 3958997.68, -132... California\n",
463 | "3 21 MULTIPOLYGON (((-9952591.899 4373541.269, -995... Kentucky\n",
464 | "4 01 MULTIPOLYGON (((-9802056.717 3568885.452, -980... Alabama"
465 | ]
466 | },
467 | "execution_count": 12,
468 | "metadata": {},
469 | "output_type": "execute_result"
470 | }
471 | ],
472 | "source": [
473 | "# Now we want to add the name of each state\n",
474 | "# When using .apply() on a single column, it can be quite straightforward\n",
475 | "gdf[\"NAME\"] = gdf[\"STATEFP\"].apply(lambda x: usa.filter_fips(x, to_return=\"name\"))\n",
476 | "gdf.head()"
477 | ]
478 | },
479 | {
480 | "cell_type": "code",
481 | "execution_count": 13,
482 | "metadata": {},
483 | "outputs": [
484 | {
485 | "data": {
486 | "text/html": [
487 | "\n",
488 | "\n",
501 | "
\n",
502 | " \n",
503 | " \n",
504 | " | \n",
505 | " STATEFP | \n",
506 | " geometry | \n",
507 | " NAME | \n",
508 | " BEA_REGION | \n",
509 | "
\n",
510 | " \n",
511 | " \n",
512 | " \n",
513 | " | 0 | \n",
514 | " 35 | \n",
515 | " POLYGON ((-12139410.211 3695244.95, -12139373.... | \n",
516 | " New Mexico | \n",
517 | " Southwest | \n",
518 | "
\n",
519 | " \n",
520 | " | 1 | \n",
521 | " 46 | \n",
522 | " POLYGON ((-11583670.355 5621144.876, -11582880... | \n",
523 | " South Dakota | \n",
524 | " Plains | \n",
525 | "
\n",
526 | " \n",
527 | " | 2 | \n",
528 | " 06 | \n",
529 | " MULTIPOLYGON (((-13202983.627 3958997.68, -132... | \n",
530 | " California | \n",
531 | " Far West | \n",
532 | "
\n",
533 | " \n",
534 | " | 3 | \n",
535 | " 21 | \n",
536 | " MULTIPOLYGON (((-9952591.899 4373541.269, -995... | \n",
537 | " Kentucky | \n",
538 | " Southeast | \n",
539 | "
\n",
540 | " \n",
541 | " | 4 | \n",
542 | " 01 | \n",
543 | " MULTIPOLYGON (((-9802056.717 3568885.452, -980... | \n",
544 | " Alabama | \n",
545 | " Southeast | \n",
546 | "
\n",
547 | " \n",
548 | "
\n",
549 | "
"
550 | ],
551 | "text/plain": [
552 | " STATEFP geometry NAME \\\n",
553 | "0 35 POLYGON ((-12139410.211 3695244.95, -12139373.... New Mexico \n",
554 | "1 46 POLYGON ((-11583670.355 5621144.876, -11582880... South Dakota \n",
555 | "2 06 MULTIPOLYGON (((-13202983.627 3958997.68, -132... California \n",
556 | "3 21 MULTIPOLYGON (((-9952591.899 4373541.269, -995... Kentucky \n",
557 | "4 01 MULTIPOLYGON (((-9802056.717 3568885.452, -980... Alabama \n",
558 | "\n",
559 | " BEA_REGION \n",
560 | "0 Southwest \n",
561 | "1 Plains \n",
562 | "2 Far West \n",
563 | "3 Southeast \n",
564 | "4 Southeast "
565 | ]
566 | },
567 | "execution_count": 13,
568 | "metadata": {},
569 | "output_type": "execute_result"
570 | }
571 | ],
572 | "source": [
573 | "# Now we want to add their BEA region\n",
574 | "# When using .apply() on an entire DF, need to state the axis of transformation (0 for rows, 1 for columns)\n",
575 | "gdf[\"BEA_REGION\"] = gdf.apply(lambda x: usa.filter_fips(x[\"STATEFP\"], to_return=\"object\")[\"bea\"], axis=1)\n",
576 | "gdf.head()"
577 | ]
578 | }
579 | ],
580 | "metadata": {
581 | "kernelspec": {
582 | "display_name": "personal",
583 | "language": "python",
584 | "name": "python3"
585 | },
586 | "language_info": {
587 | "codemirror_mode": {
588 | "name": "ipython",
589 | "version": 3
590 | },
591 | "file_extension": ".py",
592 | "mimetype": "text/x-python",
593 | "name": "python",
594 | "nbconvert_exporter": "python",
595 | "pygments_lexer": "ipython3",
596 | "version": "3.13.5"
597 | }
598 | },
599 | "nbformat": 4,
600 | "nbformat_minor": 2
601 | }
602 |
--------------------------------------------------------------------------------
/matplotlib_map_utils/utils/usa.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "fips": "01",
4 | "name": "Alabama",
5 | "abbr": "AL",
6 | "valid": true,
7 | "state": true,
8 | "contiguous": true,
9 | "territory": false,
10 | "region": "South",
11 | "division": "East South Central",
12 | "omb": "Region IV",
13 | "bea": "Southeast",
14 | "alias": null
15 | },
16 | {
17 | "fips": "02",
18 | "name": "Alaska",
19 | "abbr": "AK",
20 | "valid": true,
21 | "state": true,
22 | "contiguous": false,
23 | "territory": false,
24 | "region": "West",
25 | "division": "Pacific",
26 | "omb": "Region X",
27 | "bea": "Far West",
28 | "alias": null
29 | },
30 | {
31 | "fips": "03",
32 | "name": "American Samoa",
33 | "abbr": null,
34 | "valid": false,
35 | "state": null,
36 | "contiguous": null,
37 | "territory": true,
38 | "region": null,
39 | "division": null,
40 | "omb": null,
41 | "bea": null,
42 | "alias": null
43 | },
44 | {
45 | "fips": "04",
46 | "name": "Arizona",
47 | "abbr": "AZ",
48 | "valid": true,
49 | "state": true,
50 | "contiguous": true,
51 | "territory": false,
52 | "region": "West",
53 | "division": "Mountain",
54 | "omb": "Region IX",
55 | "bea": "Southwest",
56 | "alias": null
57 | },
58 | {
59 | "fips": "05",
60 | "name": "Arkansas",
61 | "abbr": "AR",
62 | "valid": true,
63 | "state": true,
64 | "contiguous": true,
65 | "territory": false,
66 | "region": "South",
67 | "division": "West South Central",
68 | "omb": "Region VI",
69 | "bea": "Southeast",
70 | "alias": null
71 | },
72 | {
73 | "fips": "06",
74 | "name": "California",
75 | "abbr": "CA",
76 | "valid": true,
77 | "state": true,
78 | "contiguous": true,
79 | "territory": false,
80 | "region": "West",
81 | "division": "Pacific",
82 | "omb": "Region IX",
83 | "bea": "Far West",
84 | "alias": null
85 | },
86 | {
87 | "fips": "07",
88 | "name": "Canal Zone",
89 | "abbr": null,
90 | "valid": false,
91 | "state": null,
92 | "contiguous": null,
93 | "territory": true,
94 | "region": null,
95 | "division": null,
96 | "omb": null,
97 | "bea": null,
98 | "alias": null
99 | },
100 | {
101 | "fips": "08",
102 | "name": "Colorado",
103 | "abbr": "CO",
104 | "valid": true,
105 | "state": true,
106 | "contiguous": true,
107 | "territory": false,
108 | "region": "West",
109 | "division": "Mountain",
110 | "omb": "Region VIII",
111 | "bea": "Rocky Mountain",
112 | "alias": null
113 | },
114 | {
115 | "fips": "09",
116 | "name": "Connecticut",
117 | "abbr": "CT",
118 | "valid": true,
119 | "state": true,
120 | "contiguous": true,
121 | "territory": false,
122 | "region": "Northeast",
123 | "division": "New England",
124 | "omb": "Region I",
125 | "bea": "New England",
126 | "alias": null
127 | },
128 | {
129 | "fips": "10",
130 | "name": "Delaware",
131 | "abbr": "DE",
132 | "valid": true,
133 | "state": true,
134 | "contiguous": true,
135 | "territory": false,
136 | "region": "South",
137 | "division": "South Atlantic",
138 | "omb": "Region III",
139 | "bea": "Mideast",
140 | "alias": null
141 | },
142 | {
143 | "fips": "11",
144 | "name": "Washington, D.C.",
145 | "abbr": "DC",
146 | "valid": true,
147 | "state": false,
148 | "contiguous": true,
149 | "territory": false,
150 | "region": "South",
151 | "division": "South Atlantic",
152 | "omb": "Region III",
153 | "bea": "Mideast",
154 | "alias": "District of Columbia"
155 | },
156 | {
157 | "fips": "12",
158 | "name": "Florida",
159 | "abbr": "FL",
160 | "valid": true,
161 | "state": true,
162 | "contiguous": true,
163 | "territory": false,
164 | "region": "South",
165 | "division": "South Atlantic",
166 | "omb": "Region IV",
167 | "bea": "Southeast",
168 | "alias": null
169 | },
170 | {
171 | "fips": "13",
172 | "name": "Georgia",
173 | "abbr": "GA",
174 | "valid": true,
175 | "state": true,
176 | "contiguous": true,
177 | "territory": false,
178 | "region": "South",
179 | "division": "South Atlantic",
180 | "omb": "Region IV",
181 | "bea": "Southeast",
182 | "alias": null
183 | },
184 | {
185 | "fips": "14",
186 | "name": "Guam",
187 | "abbr": null,
188 | "valid": false,
189 | "state": null,
190 | "contiguous": null,
191 | "territory": true,
192 | "region": null,
193 | "division": null,
194 | "omb": null,
195 | "bea": null,
196 | "alias": null
197 | },
198 | {
199 | "fips": "15",
200 | "name": "Hawaii",
201 | "abbr": "HI",
202 | "valid": true,
203 | "state": true,
204 | "contiguous": false,
205 | "territory": false,
206 | "region": "West",
207 | "division": "Pacific",
208 | "omb": "Region IX",
209 | "bea": "Far West",
210 | "alias": null
211 | },
212 | {
213 | "fips": "16",
214 | "name": "Idaho",
215 | "abbr": "ID",
216 | "valid": true,
217 | "state": true,
218 | "contiguous": true,
219 | "territory": false,
220 | "region": "West",
221 | "division": "Mountain",
222 | "omb": "Region X",
223 | "bea": "Rocky Mountain",
224 | "alias": null
225 | },
226 | {
227 | "fips": "17",
228 | "name": "Illinois",
229 | "abbr": "IL",
230 | "valid": true,
231 | "state": true,
232 | "contiguous": true,
233 | "territory": false,
234 | "region": "Midwest",
235 | "division": "East North Central",
236 | "omb": "Region V",
237 | "bea": "Great Lakes",
238 | "alias": null
239 | },
240 | {
241 | "fips": "18",
242 | "name": "Indiana",
243 | "abbr": "IN",
244 | "valid": true,
245 | "state": true,
246 | "contiguous": true,
247 | "territory": false,
248 | "region": "Midwest",
249 | "division": "East North Central",
250 | "omb": "Region V",
251 | "bea": "Great Lakes",
252 | "alias": null
253 | },
254 | {
255 | "fips": "19",
256 | "name": "Iowa",
257 | "abbr": "IA",
258 | "valid": true,
259 | "state": true,
260 | "contiguous": true,
261 | "territory": false,
262 | "region": "Midwest",
263 | "division": "West North Central",
264 | "omb": "Region VII",
265 | "bea": "Plains",
266 | "alias": null
267 | },
268 | {
269 | "fips": "20",
270 | "name": "Kansas",
271 | "abbr": "KS",
272 | "valid": true,
273 | "state": true,
274 | "contiguous": true,
275 | "territory": false,
276 | "region": "Midwest",
277 | "division": "West North Central",
278 | "omb": "Region VI",
279 | "bea": "Plains",
280 | "alias": null
281 | },
282 | {
283 | "fips": "21",
284 | "name": "Kentucky",
285 | "abbr": "KY",
286 | "valid": true,
287 | "state": true,
288 | "contiguous": true,
289 | "territory": false,
290 | "region": "South",
291 | "division": "East South Central",
292 | "omb": "Region IV",
293 | "bea": "Southeast",
294 | "alias": null
295 | },
296 | {
297 | "fips": "22",
298 | "name": "Louisiana",
299 | "abbr": "LA",
300 | "valid": true,
301 | "state": true,
302 | "contiguous": true,
303 | "territory": false,
304 | "region": "South",
305 | "division": "West South Central",
306 | "omb": "Region VI",
307 | "bea": "Southeast",
308 | "alias": null
309 | },
310 | {
311 | "fips": "23",
312 | "name": "Maine",
313 | "abbr": "ME",
314 | "valid": true,
315 | "state": true,
316 | "contiguous": true,
317 | "territory": false,
318 | "region": "Northeast",
319 | "division": "New England",
320 | "omb": "Region I",
321 | "bea": "New England",
322 | "alias": null
323 | },
324 | {
325 | "fips": "24",
326 | "name": "Maryland",
327 | "abbr": "MD",
328 | "valid": true,
329 | "state": true,
330 | "contiguous": true,
331 | "territory": false,
332 | "region": "South",
333 | "division": "South Atlantic",
334 | "omb": "Region III",
335 | "bea": "Mideast",
336 | "alias": null
337 | },
338 | {
339 | "fips": "25",
340 | "name": "Massachusetts",
341 | "abbr": "MA",
342 | "valid": true,
343 | "state": true,
344 | "contiguous": true,
345 | "territory": false,
346 | "region": "Northeast",
347 | "division": "New England",
348 | "omb": "Region I",
349 | "bea": "New England",
350 | "alias": null
351 | },
352 | {
353 | "fips": "26",
354 | "name": "Michigan",
355 | "abbr": "MI",
356 | "valid": true,
357 | "state": true,
358 | "contiguous": true,
359 | "territory": false,
360 | "region": "Midwest",
361 | "division": "East North Central",
362 | "omb": "Region V",
363 | "bea": "Great Lakes",
364 | "alias": null
365 | },
366 | {
367 | "fips": "27",
368 | "name": "Minnesota",
369 | "abbr": "MN",
370 | "valid": true,
371 | "state": true,
372 | "contiguous": true,
373 | "territory": false,
374 | "region": "Midwest",
375 | "division": "West North Central",
376 | "omb": "Region V",
377 | "bea": "Plains",
378 | "alias": null
379 | },
380 | {
381 | "fips": "28",
382 | "name": "Mississippi",
383 | "abbr": "MS",
384 | "valid": true,
385 | "state": true,
386 | "contiguous": true,
387 | "territory": false,
388 | "region": "South",
389 | "division": "East South Central",
390 | "omb": "Region IV",
391 | "bea": "Southeast",
392 | "alias": null
393 | },
394 | {
395 | "fips": "29",
396 | "name": "Missouri",
397 | "abbr": "MO",
398 | "valid": true,
399 | "state": true,
400 | "contiguous": true,
401 | "territory": false,
402 | "region": "Midwest",
403 | "division": "West North Central",
404 | "omb": "Region VII",
405 | "bea": "Plains",
406 | "alias": null
407 | },
408 | {
409 | "fips": "30",
410 | "name": "Montana",
411 | "abbr": "MT",
412 | "valid": true,
413 | "state": true,
414 | "contiguous": true,
415 | "territory": false,
416 | "region": "West",
417 | "division": "Mountain",
418 | "omb": "Region VIII",
419 | "bea": "Rocky Mountain",
420 | "alias": null
421 | },
422 | {
423 | "fips": "31",
424 | "name": "Nebraska",
425 | "abbr": "NE",
426 | "valid": true,
427 | "state": true,
428 | "contiguous": true,
429 | "territory": false,
430 | "region": "Midwest",
431 | "division": "West North Central",
432 | "omb": "Region VII",
433 | "bea": "Plains",
434 | "alias": null
435 | },
436 | {
437 | "fips": "32",
438 | "name": "Nevada",
439 | "abbr": "NV",
440 | "valid": true,
441 | "state": true,
442 | "contiguous": true,
443 | "territory": false,
444 | "region": "West",
445 | "division": "Mountain",
446 | "omb": "Region IX",
447 | "bea": "Far West",
448 | "alias": null
449 | },
450 | {
451 | "fips": "33",
452 | "name": "New Hampshire",
453 | "abbr": "NH",
454 | "valid": true,
455 | "state": true,
456 | "contiguous": true,
457 | "territory": false,
458 | "region": "Northeast",
459 | "division": "New England",
460 | "omb": "Region I",
461 | "bea": "New England",
462 | "alias": null
463 | },
464 | {
465 | "fips": "34",
466 | "name": "New Jersey",
467 | "abbr": "NJ",
468 | "valid": true,
469 | "state": true,
470 | "contiguous": true,
471 | "territory": false,
472 | "region": "Northeast",
473 | "division": "Mid-Atlantic",
474 | "omb": "Region II",
475 | "bea": "Mideast",
476 | "alias": null
477 | },
478 | {
479 | "fips": "35",
480 | "name": "New Mexico",
481 | "abbr": "NM",
482 | "valid": true,
483 | "state": true,
484 | "contiguous": true,
485 | "territory": false,
486 | "region": "West",
487 | "division": "Mountain",
488 | "omb": "Region VI",
489 | "bea": "Southwest",
490 | "alias": null
491 | },
492 | {
493 | "fips": "36",
494 | "name": "New York",
495 | "abbr": "NY",
496 | "valid": true,
497 | "state": true,
498 | "contiguous": true,
499 | "territory": false,
500 | "region": "Northeast",
501 | "division": "Mid-Atlantic",
502 | "omb": "Region II",
503 | "bea": "Mideast",
504 | "alias": null
505 | },
506 | {
507 | "fips": "37",
508 | "name": "North Carolina",
509 | "abbr": "NC",
510 | "valid": true,
511 | "state": true,
512 | "contiguous": true,
513 | "territory": false,
514 | "region": "South",
515 | "division": "South Atlantic",
516 | "omb": "Region IV",
517 | "bea": "Southeast",
518 | "alias": "N Carolina"
519 | },
520 | {
521 | "fips": "38",
522 | "name": "North Dakota",
523 | "abbr": "ND",
524 | "valid": true,
525 | "state": true,
526 | "contiguous": true,
527 | "territory": false,
528 | "region": "Midwest",
529 | "division": "West North Central",
530 | "omb": "Region VIII",
531 | "bea": "Plains",
532 | "alias": "N Dakota"
533 | },
534 | {
535 | "fips": "39",
536 | "name": "Ohio",
537 | "abbr": "OH",
538 | "valid": true,
539 | "state": true,
540 | "contiguous": true,
541 | "territory": false,
542 | "region": "Midwest",
543 | "division": "East North Central",
544 | "omb": "Region V",
545 | "bea": "Great Lakes",
546 | "alias": null
547 | },
548 | {
549 | "fips": "40",
550 | "name": "Oklahoma",
551 | "abbr": "OK",
552 | "valid": true,
553 | "state": true,
554 | "contiguous": true,
555 | "territory": false,
556 | "region": "South",
557 | "division": "West South Central",
558 | "omb": "Region VI",
559 | "bea": "Southwest",
560 | "alias": null
561 | },
562 | {
563 | "fips": "41",
564 | "name": "Oregon",
565 | "abbr": "OR",
566 | "valid": true,
567 | "state": true,
568 | "contiguous": true,
569 | "territory": false,
570 | "region": "West",
571 | "division": "Pacific",
572 | "omb": "Region X",
573 | "bea": "Far West",
574 | "alias": null
575 | },
576 | {
577 | "fips": "42",
578 | "name": "Pennsylvania",
579 | "abbr": "PA",
580 | "valid": true,
581 | "state": true,
582 | "contiguous": true,
583 | "territory": false,
584 | "region": "Northeast",
585 | "division": "Mid-Atlantic",
586 | "omb": "Region III",
587 | "bea": "Mideast",
588 | "alias": null
589 | },
590 | {
591 | "fips": "43",
592 | "name": "Puerto Rico",
593 | "abbr": null,
594 | "valid": false,
595 | "state": null,
596 | "contiguous": null,
597 | "territory": true,
598 | "region": null,
599 | "division": null,
600 | "omb": null,
601 | "bea": null,
602 | "alias": null
603 | },
604 | {
605 | "fips": "44",
606 | "name": "Rhode Island",
607 | "abbr": "RI",
608 | "valid": true,
609 | "state": true,
610 | "contiguous": true,
611 | "territory": false,
612 | "region": "Northeast",
613 | "division": "New England",
614 | "omb": "Region I",
615 | "bea": "New England",
616 | "alias": null
617 | },
618 | {
619 | "fips": "45",
620 | "name": "South Carolina",
621 | "abbr": "SC",
622 | "valid": true,
623 | "state": true,
624 | "contiguous": true,
625 | "territory": false,
626 | "region": "South",
627 | "division": "South Atlantic",
628 | "omb": "Region IV",
629 | "bea": "Southeast",
630 | "alias": "S Carolina"
631 | },
632 | {
633 | "fips": "46",
634 | "name": "South Dakota",
635 | "abbr": "SD",
636 | "valid": true,
637 | "state": true,
638 | "contiguous": true,
639 | "territory": false,
640 | "region": "Midwest",
641 | "division": "West North Central",
642 | "omb": "Region VIII",
643 | "bea": "Plains",
644 | "alias": "S Dakota"
645 | },
646 | {
647 | "fips": "47",
648 | "name": "Tennessee",
649 | "abbr": "TN",
650 | "valid": true,
651 | "state": true,
652 | "contiguous": true,
653 | "territory": false,
654 | "region": "South",
655 | "division": "East South Central",
656 | "omb": "Region IV",
657 | "bea": "Southeast",
658 | "alias": null
659 | },
660 | {
661 | "fips": "48",
662 | "name": "Texas",
663 | "abbr": "TX",
664 | "valid": true,
665 | "state": true,
666 | "contiguous": true,
667 | "territory": false,
668 | "region": "South",
669 | "division": "West South Central",
670 | "omb": "Region VI",
671 | "bea": "Southwest",
672 | "alias": null
673 | },
674 | {
675 | "fips": "49",
676 | "name": "Utah",
677 | "abbr": "UT",
678 | "valid": true,
679 | "state": true,
680 | "contiguous": true,
681 | "territory": false,
682 | "region": "West",
683 | "division": "Mountain",
684 | "omb": "Region VIII",
685 | "bea": "Rocky Mountain",
686 | "alias": null
687 | },
688 | {
689 | "fips": "50",
690 | "name": "Vermont",
691 | "abbr": "VT",
692 | "valid": true,
693 | "state": true,
694 | "contiguous": true,
695 | "territory": false,
696 | "region": "Northeast",
697 | "division": "New England",
698 | "omb": "Region I",
699 | "bea": "New England",
700 | "alias": null
701 | },
702 | {
703 | "fips": "51",
704 | "name": "Virginia",
705 | "abbr": "VA",
706 | "valid": true,
707 | "state": true,
708 | "contiguous": true,
709 | "territory": false,
710 | "region": "South",
711 | "division": "South Atlantic",
712 | "omb": "Region III",
713 | "bea": "Southeast",
714 | "alias": null
715 | },
716 | {
717 | "fips": "52",
718 | "name": "U.S. Virgin Islands",
719 | "abbr": null,
720 | "valid": false,
721 | "state": null,
722 | "contiguous": null,
723 | "territory": true,
724 | "region": null,
725 | "division": null,
726 | "omb": null,
727 | "bea": null,
728 | "alias": "Virgin Islands of the U.S."
729 | },
730 | {
731 | "fips": "53",
732 | "name": "Washington",
733 | "abbr": "WA",
734 | "valid": true,
735 | "state": true,
736 | "contiguous": true,
737 | "territory": false,
738 | "region": "West",
739 | "division": "Pacific",
740 | "omb": "Region III",
741 | "bea": "Mideast",
742 | "alias": null
743 | },
744 | {
745 | "fips": "54",
746 | "name": "West Virginia",
747 | "abbr": "WV",
748 | "valid": true,
749 | "state": true,
750 | "contiguous": true,
751 | "territory": false,
752 | "region": "South",
753 | "division": "South Atlantic",
754 | "omb": "Region III",
755 | "bea": "Southeast",
756 | "alias": "W Virginia"
757 | },
758 | {
759 | "fips": "55",
760 | "name": "Wisconsin",
761 | "abbr": "WI",
762 | "valid": true,
763 | "state": true,
764 | "contiguous": true,
765 | "territory": false,
766 | "region": "Midwest",
767 | "division": "East North Central",
768 | "omb": "Region V",
769 | "bea": "Great Lakes",
770 | "alias": null
771 | },
772 | {
773 | "fips": "56",
774 | "name": "Wyoming",
775 | "abbr": "WY",
776 | "valid": true,
777 | "state": true,
778 | "contiguous": true,
779 | "territory": false,
780 | "region": "West",
781 | "division": "Mountain",
782 | "omb": "Region VIII",
783 | "bea": "Rocky Mountain",
784 | "alias": null
785 | },
786 | {
787 | "fips": "60",
788 | "name": "American Samoa",
789 | "abbr": "AS",
790 | "valid": true,
791 | "state": false,
792 | "contiguous": false,
793 | "territory": true,
794 | "region": "Inhabited Territory",
795 | "division": "Unincorporated and Unorganized",
796 | "omb": "Inhabited Territory",
797 | "bea": "Inhabited Territory",
798 | "alias": null
799 | },
800 | {
801 | "fips": "64",
802 | "name": "Federated States of Micronesia",
803 | "abbr": "FM",
804 | "valid": true,
805 | "state": false,
806 | "contiguous": false,
807 | "territory": true,
808 | "region": "Sovereign State",
809 | "division": "Compact of Free Association",
810 | "omb": "Sovereign State",
811 | "bea": "Sovereign State",
812 | "alias": "Micronesia"
813 | },
814 | {
815 | "fips": "66",
816 | "name": "Guam",
817 | "abbr": "GU",
818 | "valid": true,
819 | "state": false,
820 | "contiguous": false,
821 | "territory": true,
822 | "region": "Inhabited Territory",
823 | "division": "Unincorporated and Organized",
824 | "omb": "Inhabited Territory",
825 | "bea": "Inhabited Territory",
826 | "alias": null
827 | },
828 | {
829 | "fips": "67",
830 | "name": "Johnston Atoll",
831 | "abbr": null,
832 | "valid": true,
833 | "state": false,
834 | "contiguous": false,
835 | "territory": true,
836 | "region": "Uninhabited Territory",
837 | "division": "Unincorporated and Unorganized",
838 | "omb": "Uninhabited Territory",
839 | "bea": "Uninhabited Territory",
840 | "alias": null
841 | },
842 | {
843 | "fips": "68",
844 | "name": "Marshall Islands",
845 | "abbr": "MH",
846 | "valid": true,
847 | "state": false,
848 | "contiguous": false,
849 | "territory": true,
850 | "region": "Sovereign State",
851 | "division": "Compact of Free Association",
852 | "omb": "Sovereign State",
853 | "bea": "Sovereign State",
854 | "alias": "Republic of the Marshall Islands"
855 | },
856 | {
857 | "fips": "69",
858 | "name": "Northern Mariana Islands",
859 | "abbr": "MP",
860 | "valid": true,
861 | "state": false,
862 | "contiguous": false,
863 | "territory": true,
864 | "region": "Inhabited Territory",
865 | "division": "Commonwealth",
866 | "omb": "Inhabited Territory",
867 | "bea": "Inhabited Territory",
868 | "alias": null
869 | },
870 | {
871 | "fips": "70",
872 | "name": "Palau",
873 | "abbr": "PW",
874 | "valid": true,
875 | "state": false,
876 | "contiguous": false,
877 | "territory": true,
878 | "region": "Sovereign State",
879 | "division": "Compact of Free Association",
880 | "omb": "Sovereign State",
881 | "bea": "Sovereign State",
882 | "alias": null
883 | },
884 | {
885 | "fips": "71",
886 | "name": "Midway Islands",
887 | "abbr": null,
888 | "valid": true,
889 | "state": false,
890 | "contiguous": false,
891 | "territory": true,
892 | "region": "Uninhabited Territory",
893 | "division": "Unincorporated and Unorganized",
894 | "omb": "Uninhabited Territory",
895 | "bea": "Uninhabited Territory",
896 | "alias": "Midway Atoll"
897 | },
898 | {
899 | "fips": "72",
900 | "name": "Puerto Rico",
901 | "abbr": "PR",
902 | "valid": true,
903 | "state": false,
904 | "contiguous": false,
905 | "territory": true,
906 | "region": "Inhabited Territory",
907 | "division": "Commonwealth",
908 | "omb": "Inhabited Territory",
909 | "bea": "Inhabited Territory",
910 | "alias": null
911 | },
912 | {
913 | "fips": "74",
914 | "name": "U.S. Minor Outlying Islands",
915 | "abbr": "UM",
916 | "valid": true,
917 | "state": false,
918 | "contiguous": false,
919 | "territory": true,
920 | "region": "Uninhabited Territory",
921 | "division": "Unincorporated and Unorganized",
922 | "omb": "Uninhabited Territory",
923 | "bea": "Uninhabited Territory",
924 | "alias": null
925 | },
926 | {
927 | "fips": "76",
928 | "name": "Navassa Island",
929 | "abbr": null,
930 | "valid": true,
931 | "state": false,
932 | "contiguous": false,
933 | "territory": true,
934 | "region": "Uninhabited Territory",
935 | "division": "Unincorporated and Unorganized",
936 | "omb": "Uninhabited Territory",
937 | "bea": "Uninhabited Territory",
938 | "alias": null
939 | },
940 | {
941 | "fips": "78",
942 | "name": "U.S. Virgin Islands",
943 | "abbr": "VI",
944 | "valid": true,
945 | "state": false,
946 | "contiguous": false,
947 | "territory": true,
948 | "region": "Inhabited Territory",
949 | "division": "Unincorporated and Organized",
950 | "omb": "Inhabited Territory",
951 | "bea": "Inhabited Territory",
952 | "alias": "Virgin Islands of the U.S."
953 | },
954 | {
955 | "fips": "79",
956 | "name": "Wake Island",
957 | "abbr": null,
958 | "valid": true,
959 | "state": false,
960 | "contiguous": false,
961 | "territory": true,
962 | "region": "Uninhabited Territory",
963 | "division": "Unincorporated and Unorganized",
964 | "omb": "Uninhabited Territory",
965 | "bea": "Uninhabited Territory",
966 | "alias": null
967 | },
968 | {
969 | "fips": "81",
970 | "name": "Baker Island",
971 | "abbr": null,
972 | "valid": true,
973 | "state": false,
974 | "contiguous": false,
975 | "territory": true,
976 | "region": "Uninhabited Territory",
977 | "division": "Unincorporated and Unorganized",
978 | "omb": "Uninhabited Territory",
979 | "bea": "Uninhabited Territory",
980 | "alias": null
981 | },
982 | {
983 | "fips": "84",
984 | "name": "Howland Island",
985 | "abbr": null,
986 | "valid": true,
987 | "state": false,
988 | "contiguous": false,
989 | "territory": true,
990 | "region": "Uninhabited Territory",
991 | "division": "Unincorporated and Unorganized",
992 | "omb": "Uninhabited Territory",
993 | "bea": "Uninhabited Territory",
994 | "alias": null
995 | },
996 | {
997 | "fips": "86",
998 | "name": "Jarvis Island",
999 | "abbr": null,
1000 | "valid": true,
1001 | "state": false,
1002 | "contiguous": false,
1003 | "territory": true,
1004 | "region": "Uninhabited Territory",
1005 | "division": "Unincorporated and Unorganized",
1006 | "omb": "Uninhabited Territory",
1007 | "bea": "Uninhabited Territory",
1008 | "alias": null
1009 | },
1010 | {
1011 | "fips": "89",
1012 | "name": "Kingman Reef",
1013 | "abbr": null,
1014 | "valid": true,
1015 | "state": false,
1016 | "contiguous": false,
1017 | "territory": true,
1018 | "region": "Uninhabited Territory",
1019 | "division": "Unincorporated and Unorganized",
1020 | "omb": "Uninhabited Territory",
1021 | "bea": "Uninhabited Territory",
1022 | "alias": null
1023 | },
1024 | {
1025 | "fips": "95",
1026 | "name": "Palmyra Atoll",
1027 | "abbr": null,
1028 | "valid": true,
1029 | "state": false,
1030 | "contiguous": false,
1031 | "territory": true,
1032 | "region": "Uninhabited Territory",
1033 | "division": "Incorporated and Unorganized",
1034 | "omb": "Uninhabited Territory",
1035 | "bea": "Uninhabited Territory",
1036 | "alias": null
1037 | }
1038 | ]
--------------------------------------------------------------------------------