├── .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 | ![matplotlib_map_utils logo](matplotlib_map_utils/docs/assets/mmu_logo_w_elements.png) 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 | ![Map with all common elements added](matplotlib_map_utils/docs/assets/readme_bigmap.png) 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 | ![Example north arrow](matplotlib_map_utils/docs/assets/readme_northarrow.png) 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 | ![Customized north arrow](matplotlib_map_utils/docs/assets/readme_northarrow_customization.png) 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 | ![Example north arrow rotation](matplotlib_map_utils/docs/assets/readme_northarrow_rotation.png) 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 | ![Example scale bar](matplotlib_map_utils/docs/assets/readme_scalebar.png) 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 | ![Customized scale bar](matplotlib_map_utils/docs/assets/readme_scalebar_customization.png) 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 | ![Example inset map](matplotlib_map_utils/docs/assets/readme_insetmap.png) 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 | ![Customized scale bar](matplotlib_map_utils/docs/assets/readme_indicators.png) 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 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | " \n", 352 | " \n", 353 | " \n", 354 | " \n", 355 | " \n", 356 | " \n", 357 | " \n", 358 | " \n", 359 | " \n", 360 | " \n", 361 | " \n", 362 | " \n", 363 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | "
STATEFPgeometry
035POLYGON ((-12139410.211 3695244.95, -12139373....
146POLYGON ((-11583670.355 5621144.876, -11582880...
206MULTIPOLYGON (((-13202983.627 3958997.68, -132...
321MULTIPOLYGON (((-9952591.899 4373541.269, -995...
401MULTIPOLYGON (((-9802056.717 3568885.452, -980...
\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 | " \n", 419 | " \n", 420 | " \n", 421 | " \n", 422 | " \n", 423 | " \n", 424 | " \n", 425 | " \n", 426 | " \n", 427 | " \n", 428 | " \n", 429 | " \n", 430 | " \n", 431 | " \n", 432 | " \n", 433 | " \n", 434 | " \n", 435 | " \n", 436 | " \n", 437 | " \n", 438 | " \n", 439 | " \n", 440 | " \n", 441 | " \n", 442 | " \n", 443 | " \n", 444 | " \n", 445 | " \n", 446 | " \n", 447 | " \n", 448 | " \n", 449 | " \n", 450 | " \n", 451 | " \n", 452 | " \n", 453 | " \n", 454 | " \n", 455 | "
STATEFPgeometryNAME
035POLYGON ((-12139410.211 3695244.95, -12139373....New Mexico
146POLYGON ((-11583670.355 5621144.876, -11582880...South Dakota
206MULTIPOLYGON (((-13202983.627 3958997.68, -132...California
321MULTIPOLYGON (((-9952591.899 4373541.269, -995...Kentucky
401MULTIPOLYGON (((-9802056.717 3568885.452, -980...Alabama
\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 | " \n", 506 | " \n", 507 | " \n", 508 | " \n", 509 | " \n", 510 | " \n", 511 | " \n", 512 | " \n", 513 | " \n", 514 | " \n", 515 | " \n", 516 | " \n", 517 | " \n", 518 | " \n", 519 | " \n", 520 | " \n", 521 | " \n", 522 | " \n", 523 | " \n", 524 | " \n", 525 | " \n", 526 | " \n", 527 | " \n", 528 | " \n", 529 | " \n", 530 | " \n", 531 | " \n", 532 | " \n", 533 | " \n", 534 | " \n", 535 | " \n", 536 | " \n", 537 | " \n", 538 | " \n", 539 | " \n", 540 | " \n", 541 | " \n", 542 | " \n", 543 | " \n", 544 | " \n", 545 | " \n", 546 | " \n", 547 | " \n", 548 | "
STATEFPgeometryNAMEBEA_REGION
035POLYGON ((-12139410.211 3695244.95, -12139373....New MexicoSouthwest
146POLYGON ((-11583670.355 5621144.876, -11582880...South DakotaPlains
206MULTIPOLYGON (((-13202983.627 3958997.68, -132...CaliforniaFar West
321MULTIPOLYGON (((-9952591.899 4373541.269, -995...KentuckySoutheast
401MULTIPOLYGON (((-9802056.717 3568885.452, -980...AlabamaSoutheast
\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 | ] --------------------------------------------------------------------------------