|
16 | 16 | import re |
17 | 17 | from functools import partial |
18 | 18 | from numbers import Number |
| 19 | +from typing import Callable, Iterator, TypeVar |
19 | 20 |
|
20 | 21 | import cycler |
21 | 22 | import matplotlib.colors as mcolors |
|
68 | 69 | DEFAULT_CYCLE_SAMPLES = 10 |
69 | 70 | DEFAULT_CYCLE_LUMINANCE = 90 |
70 | 71 |
|
| 72 | +_RegistryValue = TypeVar("_RegistryValue") |
| 73 | + |
| 74 | + |
| 75 | +class _RefreshingRegistry(dict[str, _RegistryValue]): |
| 76 | + """ |
| 77 | + Dictionary-like registry that rebuilds itself before reads. |
| 78 | +
|
| 79 | + This keeps constructor registries aligned with modules that may be reloaded |
| 80 | + in-place during tests or interactive use. |
| 81 | + """ |
| 82 | + |
| 83 | + def __init__(self, factory: Callable[[], dict[str, _RegistryValue]]) -> None: |
| 84 | + self._factory = factory |
| 85 | + super().__init__(factory()) |
| 86 | + |
| 87 | + def _refresh(self) -> None: |
| 88 | + super().clear() |
| 89 | + super().update(self._factory()) |
| 90 | + |
| 91 | + def __contains__(self, key: object) -> bool: |
| 92 | + self._refresh() |
| 93 | + return super().__contains__(key) |
| 94 | + |
| 95 | + def __getitem__(self, key: str) -> _RegistryValue: |
| 96 | + self._refresh() |
| 97 | + return super().__getitem__(key) |
| 98 | + |
| 99 | + def __iter__(self) -> Iterator[str]: |
| 100 | + self._refresh() |
| 101 | + return super().__iter__() |
| 102 | + |
| 103 | + def __len__(self) -> int: |
| 104 | + self._refresh() |
| 105 | + return super().__len__() |
| 106 | + |
| 107 | + def get( |
| 108 | + self, key: str, default: _RegistryValue | None = None |
| 109 | + ) -> _RegistryValue | None: |
| 110 | + self._refresh() |
| 111 | + return super().get(key, default) |
| 112 | + |
| 113 | + def items(self): # type: ignore[override] |
| 114 | + self._refresh() |
| 115 | + return super().items() |
| 116 | + |
| 117 | + def keys(self): # type: ignore[override] |
| 118 | + self._refresh() |
| 119 | + return super().keys() |
| 120 | + |
| 121 | + def values(self): # type: ignore[override] |
| 122 | + self._refresh() |
| 123 | + return super().values() |
| 124 | + |
| 125 | + def copy(self) -> dict[str, _RegistryValue]: |
| 126 | + self._refresh() |
| 127 | + return dict(super().items()) |
| 128 | + |
| 129 | + |
| 130 | +def _build_norm_registry() -> dict[str, type[mcolors.Normalize]]: |
| 131 | + registry: dict[str, type[mcolors.Normalize]] = { |
| 132 | + "none": mcolors.NoNorm, |
| 133 | + "null": mcolors.NoNorm, |
| 134 | + "div": pcolors.DivergingNorm, |
| 135 | + "diverging": pcolors.DivergingNorm, |
| 136 | + "segmented": pcolors.SegmentedNorm, |
| 137 | + "segments": pcolors.SegmentedNorm, |
| 138 | + "log": mcolors.LogNorm, |
| 139 | + "linear": mcolors.Normalize, |
| 140 | + "power": mcolors.PowerNorm, |
| 141 | + "symlog": mcolors.SymLogNorm, |
| 142 | + } |
| 143 | + if hasattr(mcolors, "TwoSlopeNorm"): |
| 144 | + registry["twoslope"] = mcolors.TwoSlopeNorm |
| 145 | + return registry |
| 146 | + |
| 147 | + |
| 148 | +def _build_locator_registry() -> dict[str, object]: |
| 149 | + registry = { |
| 150 | + "none": mticker.NullLocator, |
| 151 | + "null": mticker.NullLocator, |
| 152 | + "auto": mticker.AutoLocator, |
| 153 | + "log": mticker.LogLocator, |
| 154 | + "maxn": mticker.MaxNLocator, |
| 155 | + "linear": mticker.LinearLocator, |
| 156 | + "multiple": mticker.MultipleLocator, |
| 157 | + "fixed": mticker.FixedLocator, |
| 158 | + "index": pticker.IndexLocator, |
| 159 | + "discrete": pticker.DiscreteLocator, |
| 160 | + "discreteminor": partial(pticker.DiscreteLocator, minor=True), |
| 161 | + "symlog": mticker.SymmetricalLogLocator, |
| 162 | + "logit": mticker.LogitLocator, |
| 163 | + "minor": mticker.AutoMinorLocator, |
| 164 | + "date": mdates.AutoDateLocator, |
| 165 | + "microsecond": mdates.MicrosecondLocator, |
| 166 | + "second": mdates.SecondLocator, |
| 167 | + "minute": mdates.MinuteLocator, |
| 168 | + "hour": mdates.HourLocator, |
| 169 | + "day": mdates.DayLocator, |
| 170 | + "weekday": mdates.WeekdayLocator, |
| 171 | + "month": mdates.MonthLocator, |
| 172 | + "year": mdates.YearLocator, |
| 173 | + "lon": partial(pticker.LongitudeLocator, dms=False), |
| 174 | + "lat": partial(pticker.LatitudeLocator, dms=False), |
| 175 | + "deglon": partial(pticker.LongitudeLocator, dms=False), |
| 176 | + "deglat": partial(pticker.LatitudeLocator, dms=False), |
| 177 | + } |
| 178 | + if hasattr(mpolar, "ThetaLocator"): |
| 179 | + registry["theta"] = mpolar.ThetaLocator |
| 180 | + if _version_cartopy >= "0.18": |
| 181 | + registry["dms"] = partial(pticker.DegreeLocator, dms=True) |
| 182 | + registry["dmslon"] = partial(pticker.LongitudeLocator, dms=True) |
| 183 | + registry["dmslat"] = partial(pticker.LatitudeLocator, dms=True) |
| 184 | + return registry |
| 185 | + |
| 186 | + |
| 187 | +def _build_formatter_registry() -> dict[str, object]: |
| 188 | + registry = { # note default LogFormatter uses ugly e+00 notation |
| 189 | + "none": mticker.NullFormatter, |
| 190 | + "null": mticker.NullFormatter, |
| 191 | + "auto": pticker.AutoFormatter, |
| 192 | + "date": mdates.AutoDateFormatter, |
| 193 | + "scalar": mticker.ScalarFormatter, |
| 194 | + "simple": pticker.SimpleFormatter, |
| 195 | + "fixed": mticker.FixedLocator, |
| 196 | + "index": pticker.IndexFormatter, |
| 197 | + "sci": pticker.SciFormatter, |
| 198 | + "sigfig": pticker.SigFigFormatter, |
| 199 | + "frac": pticker.FracFormatter, |
| 200 | + "func": mticker.FuncFormatter, |
| 201 | + "strmethod": mticker.StrMethodFormatter, |
| 202 | + "formatstr": mticker.FormatStrFormatter, |
| 203 | + "datestr": mdates.DateFormatter, |
| 204 | + "log": mticker.LogFormatterSciNotation, |
| 205 | + "logit": mticker.LogitFormatter, |
| 206 | + "eng": mticker.EngFormatter, |
| 207 | + "percent": mticker.PercentFormatter, |
| 208 | + "e": partial(pticker.FracFormatter, symbol=r"$e$", number=np.e), |
| 209 | + "pi": partial(pticker.FracFormatter, symbol=r"$\pi$", number=np.pi), |
| 210 | + "tau": partial(pticker.FracFormatter, symbol=r"$\tau$", number=2 * np.pi), |
| 211 | + "lat": partial(pticker.SimpleFormatter, negpos="SN"), |
| 212 | + "lon": partial(pticker.SimpleFormatter, negpos="WE", wraprange=(-180, 180)), |
| 213 | + "deg": partial(pticker.SimpleFormatter, suffix="\N{DEGREE SIGN}"), |
| 214 | + "deglat": partial( |
| 215 | + pticker.SimpleFormatter, suffix="\N{DEGREE SIGN}", negpos="SN" |
| 216 | + ), |
| 217 | + "deglon": partial( |
| 218 | + pticker.SimpleFormatter, |
| 219 | + suffix="\N{DEGREE SIGN}", |
| 220 | + negpos="WE", |
| 221 | + wraprange=(-180, 180), |
| 222 | + ), |
| 223 | + "math": mticker.LogFormatterMathtext, |
| 224 | + } |
| 225 | + if hasattr(mpolar, "ThetaFormatter"): |
| 226 | + registry["theta"] = mpolar.ThetaFormatter |
| 227 | + if hasattr(mdates, "ConciseDateFormatter"): |
| 228 | + registry["concise"] = mdates.ConciseDateFormatter |
| 229 | + if _version_cartopy >= "0.18": |
| 230 | + registry["dms"] = partial(pticker.DegreeFormatter, dms=True) |
| 231 | + registry["dmslon"] = partial(pticker.LongitudeFormatter, dms=True) |
| 232 | + registry["dmslat"] = partial(pticker.LatitudeFormatter, dms=True) |
| 233 | + return registry |
| 234 | + |
| 235 | + |
71 | 236 | # Normalizer registry |
72 | | -NORMS = { |
73 | | - "none": mcolors.NoNorm, |
74 | | - "null": mcolors.NoNorm, |
75 | | - "div": pcolors.DivergingNorm, |
76 | | - "diverging": pcolors.DivergingNorm, |
77 | | - "segmented": pcolors.SegmentedNorm, |
78 | | - "segments": pcolors.SegmentedNorm, |
79 | | - "log": mcolors.LogNorm, |
80 | | - "linear": mcolors.Normalize, |
81 | | - "power": mcolors.PowerNorm, |
82 | | - "symlog": mcolors.SymLogNorm, |
83 | | -} |
84 | | -if hasattr(mcolors, "TwoSlopeNorm"): |
85 | | - NORMS["twoslope"] = mcolors.TwoSlopeNorm |
| 237 | +NORMS = _RefreshingRegistry(_build_norm_registry) |
86 | 238 |
|
87 | 239 | # Locator registry |
88 | 240 | # NOTE: Will raise error when you try to use degree-minute-second |
89 | 241 | # locators with cartopy < 0.18. |
90 | | -LOCATORS = { |
91 | | - "none": mticker.NullLocator, |
92 | | - "null": mticker.NullLocator, |
93 | | - "auto": mticker.AutoLocator, |
94 | | - "log": mticker.LogLocator, |
95 | | - "maxn": mticker.MaxNLocator, |
96 | | - "linear": mticker.LinearLocator, |
97 | | - "multiple": mticker.MultipleLocator, |
98 | | - "fixed": mticker.FixedLocator, |
99 | | - "index": pticker.IndexLocator, |
100 | | - "discrete": pticker.DiscreteLocator, |
101 | | - "discreteminor": partial(pticker.DiscreteLocator, minor=True), |
102 | | - "symlog": mticker.SymmetricalLogLocator, |
103 | | - "logit": mticker.LogitLocator, |
104 | | - "minor": mticker.AutoMinorLocator, |
105 | | - "date": mdates.AutoDateLocator, |
106 | | - "microsecond": mdates.MicrosecondLocator, |
107 | | - "second": mdates.SecondLocator, |
108 | | - "minute": mdates.MinuteLocator, |
109 | | - "hour": mdates.HourLocator, |
110 | | - "day": mdates.DayLocator, |
111 | | - "weekday": mdates.WeekdayLocator, |
112 | | - "month": mdates.MonthLocator, |
113 | | - "year": mdates.YearLocator, |
114 | | - "lon": partial(pticker.LongitudeLocator, dms=False), |
115 | | - "lat": partial(pticker.LatitudeLocator, dms=False), |
116 | | - "deglon": partial(pticker.LongitudeLocator, dms=False), |
117 | | - "deglat": partial(pticker.LatitudeLocator, dms=False), |
118 | | -} |
119 | | -if hasattr(mpolar, "ThetaLocator"): |
120 | | - LOCATORS["theta"] = mpolar.ThetaLocator |
121 | | -if _version_cartopy >= "0.18": |
122 | | - LOCATORS["dms"] = partial(pticker.DegreeLocator, dms=True) |
123 | | - LOCATORS["dmslon"] = partial(pticker.LongitudeLocator, dms=True) |
124 | | - LOCATORS["dmslat"] = partial(pticker.LatitudeLocator, dms=True) |
| 242 | +LOCATORS = _RefreshingRegistry(_build_locator_registry) |
125 | 243 |
|
126 | 244 | # Formatter registry |
127 | 245 | # NOTE: Critical to use SimpleFormatter for cardinal formatters rather than |
|
130 | 248 | # is their distinguishing feature relative to ultraplot formatter. |
131 | 249 | # NOTE: Will raise error when you try to use degree-minute-second |
132 | 250 | # formatters with cartopy < 0.18. |
133 | | -FORMATTERS = { # note default LogFormatter uses ugly e+00 notation |
134 | | - "none": mticker.NullFormatter, |
135 | | - "null": mticker.NullFormatter, |
136 | | - "auto": pticker.AutoFormatter, |
137 | | - "date": mdates.AutoDateFormatter, |
138 | | - "scalar": mticker.ScalarFormatter, |
139 | | - "simple": pticker.SimpleFormatter, |
140 | | - "fixed": mticker.FixedLocator, |
141 | | - "index": pticker.IndexFormatter, |
142 | | - "sci": pticker.SciFormatter, |
143 | | - "sigfig": pticker.SigFigFormatter, |
144 | | - "frac": pticker.FracFormatter, |
145 | | - "func": mticker.FuncFormatter, |
146 | | - "strmethod": mticker.StrMethodFormatter, |
147 | | - "formatstr": mticker.FormatStrFormatter, |
148 | | - "datestr": mdates.DateFormatter, |
149 | | - "log": mticker.LogFormatterSciNotation, # NOTE: this is subclass of Mathtext class |
150 | | - "logit": mticker.LogitFormatter, |
151 | | - "eng": mticker.EngFormatter, |
152 | | - "percent": mticker.PercentFormatter, |
153 | | - "e": partial(pticker.FracFormatter, symbol=r"$e$", number=np.e), |
154 | | - "pi": partial(pticker.FracFormatter, symbol=r"$\pi$", number=np.pi), |
155 | | - "tau": partial(pticker.FracFormatter, symbol=r"$\tau$", number=2 * np.pi), |
156 | | - "lat": partial(pticker.SimpleFormatter, negpos="SN"), |
157 | | - "lon": partial(pticker.SimpleFormatter, negpos="WE", wraprange=(-180, 180)), |
158 | | - "deg": partial(pticker.SimpleFormatter, suffix="\N{DEGREE SIGN}"), |
159 | | - "deglat": partial(pticker.SimpleFormatter, suffix="\N{DEGREE SIGN}", negpos="SN"), |
160 | | - "deglon": partial( |
161 | | - pticker.SimpleFormatter, |
162 | | - suffix="\N{DEGREE SIGN}", |
163 | | - negpos="WE", |
164 | | - wraprange=(-180, 180), |
165 | | - ), # noqa: E501 |
166 | | - "math": mticker.LogFormatterMathtext, # deprecated (use SciNotation subclass) |
167 | | -} |
168 | | -if hasattr(mpolar, "ThetaFormatter"): |
169 | | - FORMATTERS["theta"] = mpolar.ThetaFormatter |
170 | | -if hasattr(mdates, "ConciseDateFormatter"): |
171 | | - FORMATTERS["concise"] = mdates.ConciseDateFormatter |
172 | | -if _version_cartopy >= "0.18": |
173 | | - FORMATTERS["dms"] = partial(pticker.DegreeFormatter, dms=True) |
174 | | - FORMATTERS["dmslon"] = partial(pticker.LongitudeFormatter, dms=True) |
175 | | - FORMATTERS["dmslat"] = partial(pticker.LatitudeFormatter, dms=True) |
| 251 | +FORMATTERS = _RefreshingRegistry(_build_formatter_registry) |
176 | 252 |
|
177 | 253 | # Scale registry and presets |
178 | 254 | SCALES = mscale._scale_mapping |
|
0 commit comments