ultraplot 0.99.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. ultraplot/__init__.py +115 -0
  2. ultraplot/__init__.py.rej +58 -0
  3. ultraplot/axes/__init__.py +42 -0
  4. ultraplot/axes/base.py +3240 -0
  5. ultraplot/axes/cartesian.py +1425 -0
  6. ultraplot/axes/geo.py +1675 -0
  7. ultraplot/axes/plot.py +4569 -0
  8. ultraplot/axes/polar.py +381 -0
  9. ultraplot/axes/shared.py +186 -0
  10. ultraplot/axes/three.py +34 -0
  11. ultraplot/cmaps/Algae.rgb +256 -0
  12. ultraplot/cmaps/Amp.rgb +256 -0
  13. ultraplot/cmaps/BR.rgb +256 -0
  14. ultraplot/cmaps/Balance.rgb +256 -0
  15. ultraplot/cmaps/Blues1_r.xml +17 -0
  16. ultraplot/cmaps/Blues2.xml +16 -0
  17. ultraplot/cmaps/Blues3.xml +25 -0
  18. ultraplot/cmaps/Blues4_r.xml +17 -0
  19. ultraplot/cmaps/Blues5.xml +16 -0
  20. ultraplot/cmaps/Blues6.xml +25 -0
  21. ultraplot/cmaps/Blues7.xml +16 -0
  22. ultraplot/cmaps/Blues8.xml +17 -0
  23. ultraplot/cmaps/Blues9.xml +1 -0
  24. ultraplot/cmaps/Boreal.json +53 -0
  25. ultraplot/cmaps/Browns1.xml +16 -0
  26. ultraplot/cmaps/Browns2.xml +26 -0
  27. ultraplot/cmaps/Browns3.xml +17 -0
  28. ultraplot/cmaps/Browns4.xml +17 -0
  29. ultraplot/cmaps/Browns5.xml +26 -0
  30. ultraplot/cmaps/Browns6.xml +17 -0
  31. ultraplot/cmaps/Browns7.xml +19 -0
  32. ultraplot/cmaps/Browns8.xml +11 -0
  33. ultraplot/cmaps/Browns9.xml +1 -0
  34. ultraplot/cmaps/ColdHot.rgb +229 -0
  35. ultraplot/cmaps/Crest.rgb +256 -0
  36. ultraplot/cmaps/Curl.rgb +512 -0
  37. ultraplot/cmaps/Deep.rgb +256 -0
  38. ultraplot/cmaps/Delta.rgb +512 -0
  39. ultraplot/cmaps/Dense.rgb +256 -0
  40. ultraplot/cmaps/Div.json +71 -0
  41. ultraplot/cmaps/DryWet.json +73 -0
  42. ultraplot/cmaps/Dusk.json +53 -0
  43. ultraplot/cmaps/Fire.json +53 -0
  44. ultraplot/cmaps/Flare.rgb +256 -0
  45. ultraplot/cmaps/Glacial.json +53 -0
  46. ultraplot/cmaps/Greens1_r.xml +26 -0
  47. ultraplot/cmaps/Greens2.xml +28 -0
  48. ultraplot/cmaps/Greens3_r.xml +28 -0
  49. ultraplot/cmaps/Greens4.xml +17 -0
  50. ultraplot/cmaps/Greens5.xml +16 -0
  51. ultraplot/cmaps/Greens6_r.xml +16 -0
  52. ultraplot/cmaps/Greens7.xml +16 -0
  53. ultraplot/cmaps/Greens8.xml +26 -0
  54. ultraplot/cmaps/Haline.rgb +256 -0
  55. ultraplot/cmaps/Ice.rgb +256 -0
  56. ultraplot/cmaps/IceFire.rgb +256 -0
  57. ultraplot/cmaps/Mako.rgb +256 -0
  58. ultraplot/cmaps/Marine.json +53 -0
  59. ultraplot/cmaps/Matter.rgb +256 -0
  60. ultraplot/cmaps/Mono.txt +256 -0
  61. ultraplot/cmaps/MonoCycle.txt +256 -0
  62. ultraplot/cmaps/NegPos.json +71 -0
  63. ultraplot/cmaps/Oranges1.xml +27 -0
  64. ultraplot/cmaps/Oranges2.xml +26 -0
  65. ultraplot/cmaps/Oranges3.xml +15 -0
  66. ultraplot/cmaps/Oranges4.xml +23 -0
  67. ultraplot/cmaps/Oxy.rgb +256 -0
  68. ultraplot/cmaps/Phase.rgb +256 -0
  69. ultraplot/cmaps/Purples1_r.xml +16 -0
  70. ultraplot/cmaps/Purples2.xml +17 -0
  71. ultraplot/cmaps/Purples3.xml +18 -0
  72. ultraplot/cmaps/Reds1.xml +26 -0
  73. ultraplot/cmaps/Reds2.xml +22 -0
  74. ultraplot/cmaps/Reds3.xml +23 -0
  75. ultraplot/cmaps/Reds4.xml +26 -0
  76. ultraplot/cmaps/Reds5.xml +17 -0
  77. ultraplot/cmaps/Rocket.rgb +256 -0
  78. ultraplot/cmaps/Solar.rgb +256 -0
  79. ultraplot/cmaps/Speed.rgb +256 -0
  80. ultraplot/cmaps/Stellar.json +53 -0
  81. ultraplot/cmaps/Sunrise.json +53 -0
  82. ultraplot/cmaps/Sunset.json +53 -0
  83. ultraplot/cmaps/Tempo.rgb +256 -0
  84. ultraplot/cmaps/Thermal.rgb +256 -0
  85. ultraplot/cmaps/Turbid.rgb +256 -0
  86. ultraplot/cmaps/Vivid.xml +11 -0
  87. ultraplot/cmaps/Vlag.rgb +256 -0
  88. ultraplot/cmaps/Yellows1.xml +17 -0
  89. ultraplot/cmaps/Yellows2.xml +17 -0
  90. ultraplot/cmaps/Yellows3.xml +17 -0
  91. ultraplot/cmaps/Yellows4.xml +17 -0
  92. ultraplot/cmaps/acton.txt +256 -0
  93. ultraplot/cmaps/bam.txt +256 -0
  94. ultraplot/cmaps/bamO.txt +256 -0
  95. ultraplot/cmaps/bamako.txt +256 -0
  96. ultraplot/cmaps/batlow.txt +256 -0
  97. ultraplot/cmaps/batlowK.txt +256 -0
  98. ultraplot/cmaps/batlowW.txt +256 -0
  99. ultraplot/cmaps/berlin.txt +256 -0
  100. ultraplot/cmaps/bilbao.txt +256 -0
  101. ultraplot/cmaps/broc.txt +256 -0
  102. ultraplot/cmaps/brocO.txt +256 -0
  103. ultraplot/cmaps/buda.txt +256 -0
  104. ultraplot/cmaps/bukavu.txt +256 -0
  105. ultraplot/cmaps/cork.txt +256 -0
  106. ultraplot/cmaps/corkO.txt +256 -0
  107. ultraplot/cmaps/davos.txt +256 -0
  108. ultraplot/cmaps/devon.txt +256 -0
  109. ultraplot/cmaps/fes.txt +256 -0
  110. ultraplot/cmaps/hawaii.txt +256 -0
  111. ultraplot/cmaps/imola.txt +256 -0
  112. ultraplot/cmaps/lajolla.txt +256 -0
  113. ultraplot/cmaps/lapaz.txt +256 -0
  114. ultraplot/cmaps/lisbon.txt +256 -0
  115. ultraplot/cmaps/nuuk.txt +256 -0
  116. ultraplot/cmaps/oleron.txt +256 -0
  117. ultraplot/cmaps/oslo.txt +256 -0
  118. ultraplot/cmaps/roma.txt +256 -0
  119. ultraplot/cmaps/romaO.txt +256 -0
  120. ultraplot/cmaps/tofino.txt +256 -0
  121. ultraplot/cmaps/tokyo.txt +256 -0
  122. ultraplot/cmaps/turku.txt +256 -0
  123. ultraplot/cmaps/vanimo.txt +256 -0
  124. ultraplot/cmaps/vik.txt +256 -0
  125. ultraplot/cmaps/vikO.txt +256 -0
  126. ultraplot/colors/opencolor.txt +132 -0
  127. ultraplot/colors/xkcd.txt +951 -0
  128. ultraplot/colors.py +3241 -0
  129. ultraplot/colors.py.rej +243 -0
  130. ultraplot/config.py +1809 -0
  131. ultraplot/constructor.py +1633 -0
  132. ultraplot/cycles/538.hex +2 -0
  133. ultraplot/cycles/FlatUI.hex +1 -0
  134. ultraplot/cycles/Qual1.rgb +7 -0
  135. ultraplot/cycles/Qual2.rgb +13 -0
  136. ultraplot/cycles/bmh.hex +2 -0
  137. ultraplot/cycles/classic.hex +2 -0
  138. ultraplot/cycles/colorblind.hex +2 -0
  139. ultraplot/cycles/colorblind10.hex +2 -0
  140. ultraplot/cycles/default.hex +2 -0
  141. ultraplot/cycles/ggplot.hex +1 -0
  142. ultraplot/cycles/seaborn.hex +2 -0
  143. ultraplot/cycles/tableau.hex +2 -0
  144. ultraplot/demos.py +1201 -0
  145. ultraplot/externals/__init__.py +5 -0
  146. ultraplot/externals/hsluv.py +330 -0
  147. ultraplot/figure.py +2102 -0
  148. ultraplot/fonts/FiraMath-Bold.ttf +0 -0
  149. ultraplot/fonts/FiraMath-ExtraLight.ttf +0 -0
  150. ultraplot/fonts/FiraMath-Heavy.ttf +0 -0
  151. ultraplot/fonts/FiraMath-Light.ttf +0 -0
  152. ultraplot/fonts/FiraMath-Medium.ttf +0 -0
  153. ultraplot/fonts/FiraMath-Regular.ttf +0 -0
  154. ultraplot/fonts/FiraMath-SemiBold.ttf +0 -0
  155. ultraplot/fonts/FiraMath-UltraLight.ttf +0 -0
  156. ultraplot/fonts/FiraSans-Black.ttf +0 -0
  157. ultraplot/fonts/FiraSans-BlackItalic.ttf +0 -0
  158. ultraplot/fonts/FiraSans-Bold.ttf +0 -0
  159. ultraplot/fonts/FiraSans-BoldItalic.ttf +0 -0
  160. ultraplot/fonts/FiraSans-ExtraBold.ttf +0 -0
  161. ultraplot/fonts/FiraSans-ExtraBoldItalic.ttf +0 -0
  162. ultraplot/fonts/FiraSans-ExtraLight.ttf +0 -0
  163. ultraplot/fonts/FiraSans-ExtraLightItalic.ttf +0 -0
  164. ultraplot/fonts/FiraSans-Italic.ttf +0 -0
  165. ultraplot/fonts/FiraSans-Light.ttf +0 -0
  166. ultraplot/fonts/FiraSans-LightItalic.ttf +0 -0
  167. ultraplot/fonts/FiraSans-Medium.ttf +0 -0
  168. ultraplot/fonts/FiraSans-MediumItalic.ttf +0 -0
  169. ultraplot/fonts/FiraSans-Regular.ttf +0 -0
  170. ultraplot/fonts/FiraSans-SemiBold.ttf +0 -0
  171. ultraplot/fonts/FiraSans-SemiBoldItalic.ttf +0 -0
  172. ultraplot/fonts/LICENSE_FIRAMATH.txt +92 -0
  173. ultraplot/fonts/LICENSE_FIRASANS.txt +97 -0
  174. ultraplot/fonts/LICENSE_NOTOSANS.txt +202 -0
  175. ultraplot/fonts/LICENSE_NOTOSERIF.txt +93 -0
  176. ultraplot/fonts/LICENSE_OPENSANS.txt +202 -0
  177. ultraplot/fonts/LICENSE_ROBOTO.txt +202 -0
  178. ultraplot/fonts/LICENSE_SOURCESANS.txt +93 -0
  179. ultraplot/fonts/LICENSE_SOURCESERIF.txt +93 -0
  180. ultraplot/fonts/LICENSE_TEXGYRE.txt +29 -0
  181. ultraplot/fonts/LICENSE_UBUNTU.txt +96 -0
  182. ultraplot/fonts/NotoSans-Bold.ttf +0 -0
  183. ultraplot/fonts/NotoSans-BoldItalic.ttf +0 -0
  184. ultraplot/fonts/NotoSans-Italic.ttf +0 -0
  185. ultraplot/fonts/NotoSans-Regular.ttf +0 -0
  186. ultraplot/fonts/NotoSerif-Bold.ttf +0 -0
  187. ultraplot/fonts/NotoSerif-BoldItalic.ttf +0 -0
  188. ultraplot/fonts/NotoSerif-Italic.ttf +0 -0
  189. ultraplot/fonts/NotoSerif-Regular.ttf +0 -0
  190. ultraplot/fonts/OpenSans-Bold.ttf +0 -0
  191. ultraplot/fonts/OpenSans-BoldItalic.ttf +0 -0
  192. ultraplot/fonts/OpenSans-Italic.ttf +0 -0
  193. ultraplot/fonts/OpenSans-Regular.ttf +0 -0
  194. ultraplot/fonts/OpenSans-Semibold.ttf +0 -0
  195. ultraplot/fonts/OpenSans-SemiboldItalic.ttf +0 -0
  196. ultraplot/fonts/Roboto-Black.ttf +0 -0
  197. ultraplot/fonts/Roboto-BlackItalic.ttf +0 -0
  198. ultraplot/fonts/Roboto-Bold.ttf +0 -0
  199. ultraplot/fonts/Roboto-BoldItalic.ttf +0 -0
  200. ultraplot/fonts/Roboto-Italic.ttf +0 -0
  201. ultraplot/fonts/Roboto-Light.ttf +0 -0
  202. ultraplot/fonts/Roboto-LightItalic.ttf +0 -0
  203. ultraplot/fonts/Roboto-Medium.ttf +0 -0
  204. ultraplot/fonts/Roboto-MediumItalic.ttf +0 -0
  205. ultraplot/fonts/Roboto-Regular.ttf +0 -0
  206. ultraplot/fonts/SourceSansPro-Black.ttf +0 -0
  207. ultraplot/fonts/SourceSansPro-BlackItalic.ttf +0 -0
  208. ultraplot/fonts/SourceSansPro-Bold.ttf +0 -0
  209. ultraplot/fonts/SourceSansPro-BoldItalic.ttf +0 -0
  210. ultraplot/fonts/SourceSansPro-ExtraLight.ttf +0 -0
  211. ultraplot/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
  212. ultraplot/fonts/SourceSansPro-Italic.ttf +0 -0
  213. ultraplot/fonts/SourceSansPro-Light.ttf +0 -0
  214. ultraplot/fonts/SourceSansPro-LightItalic.ttf +0 -0
  215. ultraplot/fonts/SourceSansPro-Regular.ttf +0 -0
  216. ultraplot/fonts/SourceSansPro-SemiBold.ttf +0 -0
  217. ultraplot/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
  218. ultraplot/fonts/SourceSerifPro-Black.ttf +0 -0
  219. ultraplot/fonts/SourceSerifPro-BlackItalic.ttf +0 -0
  220. ultraplot/fonts/SourceSerifPro-Bold.ttf +0 -0
  221. ultraplot/fonts/SourceSerifPro-BoldItalic.ttf +0 -0
  222. ultraplot/fonts/SourceSerifPro-ExtraLight.ttf +0 -0
  223. ultraplot/fonts/SourceSerifPro-ExtraLightItalic.ttf +0 -0
  224. ultraplot/fonts/SourceSerifPro-Italic.ttf +0 -0
  225. ultraplot/fonts/SourceSerifPro-Light.ttf +0 -0
  226. ultraplot/fonts/SourceSerifPro-LightItalic.ttf +0 -0
  227. ultraplot/fonts/SourceSerifPro-Regular.ttf +0 -0
  228. ultraplot/fonts/SourceSerifPro-SemiBold.ttf +0 -0
  229. ultraplot/fonts/SourceSerifPro-SemiBoldItalic.ttf +0 -0
  230. ultraplot/fonts/Ubuntu-Bold.ttf +0 -0
  231. ultraplot/fonts/Ubuntu-BoldItalic.ttf +0 -0
  232. ultraplot/fonts/Ubuntu-Italic.ttf +0 -0
  233. ultraplot/fonts/Ubuntu-Light.ttf +0 -0
  234. ultraplot/fonts/Ubuntu-LightItalic.ttf +0 -0
  235. ultraplot/fonts/Ubuntu-Medium.ttf +0 -0
  236. ultraplot/fonts/Ubuntu-MediumItalic.ttf +0 -0
  237. ultraplot/fonts/Ubuntu-Regular.ttf +0 -0
  238. ultraplot/fonts/texgyreadventor-bold.ttf +0 -0
  239. ultraplot/fonts/texgyreadventor-bolditalic.ttf +0 -0
  240. ultraplot/fonts/texgyreadventor-italic.ttf +0 -0
  241. ultraplot/fonts/texgyreadventor-regular.ttf +0 -0
  242. ultraplot/fonts/texgyrebonum-bold.ttf +0 -0
  243. ultraplot/fonts/texgyrebonum-bolditalic.ttf +0 -0
  244. ultraplot/fonts/texgyrebonum-italic.ttf +0 -0
  245. ultraplot/fonts/texgyrebonum-regular.ttf +0 -0
  246. ultraplot/fonts/texgyrechorus-mediumitalic.ttf +0 -0
  247. ultraplot/fonts/texgyrecursor-bold.ttf +0 -0
  248. ultraplot/fonts/texgyrecursor-bolditalic.ttf +0 -0
  249. ultraplot/fonts/texgyrecursor-italic.ttf +0 -0
  250. ultraplot/fonts/texgyrecursor-regular.ttf +0 -0
  251. ultraplot/fonts/texgyreheros-bold.ttf +0 -0
  252. ultraplot/fonts/texgyreheros-bolditalic.ttf +0 -0
  253. ultraplot/fonts/texgyreheros-italic.ttf +0 -0
  254. ultraplot/fonts/texgyreheros-regular.ttf +0 -0
  255. ultraplot/fonts/texgyrepagella-bold.ttf +0 -0
  256. ultraplot/fonts/texgyrepagella-bolditalic.ttf +0 -0
  257. ultraplot/fonts/texgyrepagella-italic.ttf +0 -0
  258. ultraplot/fonts/texgyrepagella-regular.ttf +0 -0
  259. ultraplot/fonts/texgyreschola-bold.ttf +0 -0
  260. ultraplot/fonts/texgyreschola-bolditalic.ttf +0 -0
  261. ultraplot/fonts/texgyreschola-italic.ttf +0 -0
  262. ultraplot/fonts/texgyreschola-regular.ttf +0 -0
  263. ultraplot/fonts/texgyretermes-bold.ttf +0 -0
  264. ultraplot/fonts/texgyretermes-bolditalic.ttf +0 -0
  265. ultraplot/fonts/texgyretermes-italic.ttf +0 -0
  266. ultraplot/fonts/texgyretermes-regular.ttf +0 -0
  267. ultraplot/gridspec.py +1698 -0
  268. ultraplot/internals/__init__.py +529 -0
  269. ultraplot/internals/benchmarks.py +26 -0
  270. ultraplot/internals/context.py +44 -0
  271. ultraplot/internals/docstring.py +139 -0
  272. ultraplot/internals/fonts.py +75 -0
  273. ultraplot/internals/guides.py +167 -0
  274. ultraplot/internals/inputs.py +862 -0
  275. ultraplot/internals/labels.py +85 -0
  276. ultraplot/internals/rcsetup.py +1933 -0
  277. ultraplot/internals/versions.py +61 -0
  278. ultraplot/internals/warnings.py +122 -0
  279. ultraplot/proj.py +325 -0
  280. ultraplot/scale.py +966 -0
  281. ultraplot/tests/__init__.py +28 -0
  282. ultraplot/tests/baseline/test_align_labels.png +0 -0
  283. ultraplot/tests/baseline/test_aligned_outer_guides.png +0 -0
  284. ultraplot/tests/baseline/test_aspect_ratios.png +0 -0
  285. ultraplot/tests/baseline/test_auto_diverging1.png +0 -0
  286. ultraplot/tests/baseline/test_auto_legend.png +0 -0
  287. ultraplot/tests/baseline/test_auto_reverse.png +0 -0
  288. ultraplot/tests/baseline/test_autodiverging3.png +0 -0
  289. ultraplot/tests/baseline/test_autodiverging4.png +0 -0
  290. ultraplot/tests/baseline/test_autodiverging5.png +0 -0
  291. ultraplot/tests/baseline/test_axes_colors.png +0 -0
  292. ultraplot/tests/baseline/test_bar_vectors.png +0 -0
  293. ultraplot/tests/baseline/test_bar_width.png +0 -0
  294. ultraplot/tests/baseline/test_both_ticklabels.png +0 -0
  295. ultraplot/tests/baseline/test_bounds_ticks.png +0 -0
  296. ultraplot/tests/baseline/test_boxplot_colors.png +0 -0
  297. ultraplot/tests/baseline/test_boxplot_vectors.png +0 -0
  298. ultraplot/tests/baseline/test_cartopy_contours.png +0 -0
  299. ultraplot/tests/baseline/test_cartopy_labels.png +0 -0
  300. ultraplot/tests/baseline/test_cartopy_manual.png +0 -0
  301. ultraplot/tests/baseline/test_centered_legends.png +0 -0
  302. ultraplot/tests/baseline/test_cmap_cycles.png +0 -0
  303. ultraplot/tests/baseline/test_colorbar.png +0 -0
  304. ultraplot/tests/baseline/test_colorbar_ticks.png +0 -0
  305. ultraplot/tests/baseline/test_colormap_mode.png +0 -0
  306. ultraplot/tests/baseline/test_column_iteration.png +0 -0
  307. ultraplot/tests/baseline/test_complex_ticks.png +0 -0
  308. ultraplot/tests/baseline/test_contour_labels.png +0 -0
  309. ultraplot/tests/baseline/test_contour_legend_with_label.png +0 -0
  310. ultraplot/tests/baseline/test_contour_legend_without_label.png +0 -0
  311. ultraplot/tests/baseline/test_contour_negative.png +0 -0
  312. ultraplot/tests/baseline/test_contour_single.png +0 -0
  313. ultraplot/tests/baseline/test_cutoff_ticks.png +0 -0
  314. ultraplot/tests/baseline/test_data_keyword.png +0 -0
  315. ultraplot/tests/baseline/test_discrete_ticks.png +0 -0
  316. ultraplot/tests/baseline/test_discrete_vs_fixed.png +0 -0
  317. ultraplot/tests/baseline/test_drawing_in_projection_with_globe.png +0 -0
  318. ultraplot/tests/baseline/test_drawing_in_projection_without_globe.png +0 -0
  319. ultraplot/tests/baseline/test_edge_fix.png +0 -0
  320. ultraplot/tests/baseline/test_flow_functions.png +0 -0
  321. ultraplot/tests/baseline/test_font_adjustments.png +0 -0
  322. ultraplot/tests/baseline/test_geographic_multiple_projections.png +0 -0
  323. ultraplot/tests/baseline/test_geographic_single_projection.png +0 -0
  324. ultraplot/tests/baseline/test_gray_adjustment.png +0 -0
  325. ultraplot/tests/baseline/test_histogram_legend.png +0 -0
  326. ultraplot/tests/baseline/test_histogram_types.png +0 -0
  327. ultraplot/tests/baseline/test_ignore_message.png +0 -0
  328. ultraplot/tests/baseline/test_inbounds_data.png +0 -0
  329. ultraplot/tests/baseline/test_init_format.png +0 -0
  330. ultraplot/tests/baseline/test_inner_title_zorder.png +0 -0
  331. ultraplot/tests/baseline/test_inset_basic.png +0 -0
  332. ultraplot/tests/baseline/test_inset_colorbars.png +0 -0
  333. ultraplot/tests/baseline/test_inset_colors_1.png +0 -0
  334. ultraplot/tests/baseline/test_inset_colors_2.png +0 -0
  335. ultraplot/tests/baseline/test_inset_zoom_update.png +0 -0
  336. ultraplot/tests/baseline/test_invalid_dist.png +0 -0
  337. ultraplot/tests/baseline/test_invalid_plot.png +0 -0
  338. ultraplot/tests/baseline/test_keep_guide_labels.png +0 -0
  339. ultraplot/tests/baseline/test_label_settings.png +0 -0
  340. ultraplot/tests/baseline/test_level_restriction.png +0 -0
  341. ultraplot/tests/baseline/test_levels_with_vmin_vmax.png +0 -0
  342. ultraplot/tests/baseline/test_locale_formatting.png +0 -0
  343. ultraplot/tests/baseline/test_locale_formatting_en_US.UTF-8.png +0 -0
  344. ultraplot/tests/baseline/test_manual_labels.png +0 -0
  345. ultraplot/tests/baseline/test_multi_formatting.png +0 -0
  346. ultraplot/tests/baseline/test_multiple_calls.png +0 -0
  347. ultraplot/tests/baseline/test_on_the_fly_mappable.png +0 -0
  348. ultraplot/tests/baseline/test_outer_align.png +0 -0
  349. ultraplot/tests/baseline/test_panel_dist.png +0 -0
  350. ultraplot/tests/baseline/test_panels_suplabels_three_hor_panels.png +0 -0
  351. ultraplot/tests/baseline/test_panels_with_sharing.png +0 -0
  352. ultraplot/tests/baseline/test_panels_without_sharing_1.png +0 -0
  353. ultraplot/tests/baseline/test_panels_without_sharing_2.png +0 -0
  354. ultraplot/tests/baseline/test_parametric_colors.png +0 -0
  355. ultraplot/tests/baseline/test_parametric_labels.png +0 -0
  356. ultraplot/tests/baseline/test_patch_format.png +0 -0
  357. ultraplot/tests/baseline/test_pie_charts.png +0 -0
  358. ultraplot/tests/baseline/test_pint_quantities.png +0 -0
  359. ultraplot/tests/baseline/test_polar_projections.png +0 -0
  360. ultraplot/tests/baseline/test_projection_dicts.png +0 -0
  361. ultraplot/tests/baseline/test_qualitative_colormaps_1.png +0 -0
  362. ultraplot/tests/baseline/test_qualitative_colormaps_2.png +0 -0
  363. ultraplot/tests/baseline/test_reversed_levels.png +0 -0
  364. ultraplot/tests/baseline/test_scatter_alpha.png +0 -0
  365. ultraplot/tests/baseline/test_scatter_args.png +0 -0
  366. ultraplot/tests/baseline/test_scatter_cycle.png +0 -0
  367. ultraplot/tests/baseline/test_scatter_inbounds.png +0 -0
  368. ultraplot/tests/baseline/test_scatter_sizes.png +0 -0
  369. ultraplot/tests/baseline/test_seaborn_heatmap.png +0 -0
  370. ultraplot/tests/baseline/test_seaborn_hist.png +0 -0
  371. ultraplot/tests/baseline/test_seaborn_relational.png +0 -0
  372. ultraplot/tests/baseline/test_seaborn_swarmplot.png +0 -0
  373. ultraplot/tests/baseline/test_segmented_norm.png +0 -0
  374. ultraplot/tests/baseline/test_segmented_norm_ticks.png +0 -0
  375. ultraplot/tests/baseline/test_share_all_basic.png +0 -0
  376. ultraplot/tests/baseline/test_singleton_legend.png +0 -0
  377. ultraplot/tests/baseline/test_span_labels.png +0 -0
  378. ultraplot/tests/baseline/test_spine_offset.png +0 -0
  379. ultraplot/tests/baseline/test_spine_side.png +0 -0
  380. ultraplot/tests/baseline/test_standardized_input.png +0 -0
  381. ultraplot/tests/baseline/test_statistical_boxplot.png +0 -0
  382. ultraplot/tests/baseline/test_three_axes.png +0 -0
  383. ultraplot/tests/baseline/test_tick_direction.png +0 -0
  384. ultraplot/tests/baseline/test_tick_labels.png +0 -0
  385. ultraplot/tests/baseline/test_tick_length.png +0 -0
  386. ultraplot/tests/baseline/test_tick_width.png +0 -0
  387. ultraplot/tests/baseline/test_title_deflection.png +0 -0
  388. ultraplot/tests/baseline/test_triangular_functions.png +0 -0
  389. ultraplot/tests/baseline/test_tuple_handles.png +0 -0
  390. ultraplot/tests/baseline/test_twin_axes_1.png +0 -0
  391. ultraplot/tests/baseline/test_twin_axes_2.png +0 -0
  392. ultraplot/tests/baseline/test_twin_axes_3.png +0 -0
  393. ultraplot/tests/baseline/test_uneven_levels.png +0 -0
  394. ultraplot/tests/test_1dplots.py +373 -0
  395. ultraplot/tests/test_2dplots.py +354 -0
  396. ultraplot/tests/test_axes.py +179 -0
  397. ultraplot/tests/test_colorbar.py +253 -0
  398. ultraplot/tests/test_docs.py +78 -0
  399. ultraplot/tests/test_format.py +340 -0
  400. ultraplot/tests/test_geographic.py +116 -0
  401. ultraplot/tests/test_imshow.py +110 -0
  402. ultraplot/tests/test_inset.py +28 -0
  403. ultraplot/tests/test_integration.py +149 -0
  404. ultraplot/tests/test_legend.py +181 -0
  405. ultraplot/tests/test_projections.py +138 -0
  406. ultraplot/tests/test_statistical_plotting.py +77 -0
  407. ultraplot/tests/test_subplots.py +174 -0
  408. ultraplot/ticker.py +879 -0
  409. ultraplot/ui.py +233 -0
  410. ultraplot/utils.py +912 -0
  411. ultraplot-0.99.3.dist-info/LICENSE.txt +427 -0
  412. ultraplot-0.99.3.dist-info/METADATA +88 -0
  413. ultraplot-0.99.3.dist-info/RECORD +416 -0
  414. ultraplot-0.99.3.dist-info/WHEEL +5 -0
  415. ultraplot-0.99.3.dist-info/entry_points.txt +2 -0
  416. ultraplot-0.99.3.dist-info/top_level.txt +1 -0
ultraplot/axes/geo.py ADDED
@@ -0,0 +1,1675 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Axes filled with cartographic projections.
4
+ """
5
+ import copy
6
+ import inspect
7
+
8
+ import matplotlib.axis as maxis
9
+ import matplotlib.path as mpath
10
+ import matplotlib.text as mtext
11
+ import matplotlib.ticker as mticker
12
+ import numpy as np
13
+
14
+ from .. import constructor
15
+ from .. import proj as pproj
16
+ from ..config import rc
17
+ from ..internals import ic # noqa: F401
18
+ from ..internals import _not_none, _pop_rc, _version_cartopy, docstring, warnings
19
+ from . import plot
20
+
21
+ try:
22
+ import cartopy.crs as ccrs
23
+ import cartopy.feature as cfeature
24
+ import cartopy.mpl.gridliner as cgridliner
25
+ from cartopy.crs import Projection
26
+ from cartopy.mpl.geoaxes import GeoAxes as _GeoAxes
27
+ except ModuleNotFoundError:
28
+ ccrs = cfeature = cgridliner = None
29
+ _GeoAxes = Projection = object
30
+
31
+ try:
32
+ from mpl_toolkits.basemap import Basemap
33
+ except ModuleNotFoundError:
34
+ Basemap = object
35
+
36
+ __all__ = ["GeoAxes"]
37
+
38
+
39
+ # Format docstring
40
+ _format_docstring = """
41
+ round : bool, default: :rc:`geo.round`
42
+ *For polar cartopy axes only*.
43
+ Whether to bound polar projections with circles rather than squares. Note that outer
44
+ gridline labels cannot be added to circle-bounded polar projections. When basemap
45
+ is the backend this argument must be passed to `~ultraplot.constructor.Proj` instead.
46
+ extent : {'globe', 'auto'}, default: :rc:`geo.extent`
47
+ *For cartopy axes only*.
48
+ Whether to auto adjust the map bounds based on plotted content. If ``'globe'`` then
49
+ non-polar projections are fixed with `~cartopy.mpl.geoaxes.GeoAxes.set_global`,
50
+ non-Gnomonic polar projections are bounded at the equator, and Gnomonic polar
51
+ projections are bounded at 30 degrees latitude. If ``'auto'`` nothing is done.
52
+ lonlim, latlim : 2-tuple of float, optional
53
+ *For cartopy axes only.*
54
+ The approximate longitude and latitude boundaries of the map, applied
55
+ with `~cartopy.mpl.geoaxes.GeoAxes.set_extent`. When basemap is the backend
56
+ this argument must be passed to `~ultraplot.constructor.Proj` instead.
57
+ boundinglat : float, optional
58
+ *For cartopy axes only.*
59
+ The edge latitude for the circle bounding North Pole and South Pole-centered
60
+ projections. When basemap is the backend this argument must be passed to
61
+ `~ultraplot.constructor.Proj` instead.
62
+ longrid, latgrid, grid : bool, default: :rc:`grid`
63
+ Whether to draw longitude and latitude gridlines.
64
+ Use the keyword `grid` to toggle both at once.
65
+ longridminor, latgridminor, gridminor : bool, default: :rc:`gridminor`
66
+ Whether to draw "minor" longitude and latitude lines.
67
+ Use the keyword `gridminor` to toggle both at once.
68
+ latmax : float, default: 80
69
+ The maximum absolute latitude for gridlines. Longitude gridlines are cut off
70
+ poleward of this value (note this feature does not work in cartopy 0.18).
71
+ nsteps : int, default: :rc:`grid.nsteps`
72
+ *For cartopy axes only.*
73
+ The number of interpolation steps used to draw gridlines.
74
+ lonlines, latlines : optional
75
+ Aliases for `lonlocator`, `latlocator`.
76
+ lonlocator, latlocator : locator-spec, optional
77
+ Used to determine the longitude and latitude gridline locations.
78
+ Passed to the `~ultraplot.constructor.Locator` constructor. Can be
79
+ string, float, list of float, or `matplotlib.ticker.Locator` instance.
80
+
81
+ For basemap or cartopy < 0.18, the defaults are ``'deglon'`` and
82
+ ``'deglat'``, which correspond to the `~ultraplot.ticker.LongitudeLocator`
83
+ and `~ultraplot.ticker.LatitudeLocator` locators (adapted from cartopy).
84
+ For cartopy >= 0.18, the defaults are ``'dmslon'`` and ``'dmslat'``,
85
+ which uses the same locators with ``dms=True``. This selects gridlines
86
+ at nice degree-minute-second intervals when the map extent is very small.
87
+ lonlines_kw, latlines_kw : optional
88
+ Aliases for `lonlocator_kw`, `latlocator_kw`.
89
+ lonlocator_kw, latlocator_kw : dict-like, optional
90
+ Keyword arguments passed to the `matplotlib.ticker.Locator` class.
91
+ lonminorlocator, latminorlocator, lonminorlines, latminorlines : optional
92
+ As with `lonlocator` and `latlocator` but for the "minor" gridlines.
93
+ lonminorlines_kw, latminorlines_kw : optional
94
+ Aliases for `lonminorlocator_kw`, `latminorlocator_kw`.
95
+ lonminorlocator_kw, latminorlocator_kw : optional
96
+ As with `lonlocator_kw`, and `latlocator_kw` but for the "minor" gridlines.
97
+ lonlabels, latlabels, labels : str, bool, or sequence, :rc:`grid.labels`
98
+ Whether to add non-inline longitude and latitude gridline labels, and on
99
+ which sides of the map. Use the keyword `labels` to set both at once. The
100
+ argument must conform to one of the following options:
101
+
102
+ * A boolean. ``True`` indicates the bottom side for longitudes and
103
+ the left side for latitudes, and ``False`` disables all labels.
104
+ * A string or sequence of strings indicating the side names, e.g.
105
+ ``'top'`` for longitudes or ``('left', 'right')`` for latitudes.
106
+ * A string indicating the side names with single characters, e.g.
107
+ ``'bt'`` for longitudes or ``'lr'`` for latitudes.
108
+ * A string matching ``'neither'`` (no labels), ``'both'`` (equivalent
109
+ to ``'bt'`` for longitudes and ``'lr'`` for latitudes), or ``'all'``
110
+ (equivalent to ``'lrbt'``, i.e. all sides).
111
+ * A boolean 2-tuple indicating whether to draw labels
112
+ on the ``(bottom, top)`` sides for longitudes,
113
+ and the ``(left, right)`` sides for latitudes.
114
+ * A boolean 4-tuple indicating whether to draw labels on the
115
+ ``(left, right, bottom, top)`` sides, as with the basemap
116
+ `~mpl_toolkits.basemap.Basemap.drawmeridians` and
117
+ `~mpl_toolkits.basemap.Basemap.drawparallels` `labels` keyword.
118
+
119
+ loninline, latinline, inlinelabels : bool, default: :rc:`grid.inlinelabels`
120
+ *For cartopy axes only.*
121
+ Whether to add inline longitude and latitude gridline labels. Use
122
+ the keyword `inlinelabels` to set both at once.
123
+ rotatelabels : bool, default: :rc:`grid.rotatelabels`
124
+ *For cartopy axes only.*
125
+ Whether to rotate non-inline gridline labels so that they automatically
126
+ follow the map boundary curvature.
127
+ labelpad : unit-spec, default: :rc:`grid.labelpad`
128
+ *For cartopy axes only.*
129
+ The padding between non-inline gridline labels and the map boundary.
130
+ %(units.pt)s
131
+ dms : bool, default: :rc:`grid.dmslabels`
132
+ *For cartopy axes only.*
133
+ Whether the default locators and formatters should use "minutes" and "seconds"
134
+ for gridline labels on small scales rather than decimal degrees. Setting this to
135
+ ``False`` is equivalent to ``ax.format(lonlocator='deglon', latlocator='deglat')``
136
+ and ``ax.format(lonformatter='deglon', latformatter='deglat')``.
137
+ lonformatter, latformatter : formatter-spec, optional
138
+ Formatter used to style longitude and latitude gridline labels.
139
+ Passed to the `~ultraplot.constructor.Formatter` constructor. Can be
140
+ string, list of string, or `matplotlib.ticker.Formatter` instance.
141
+
142
+ For basemap or cartopy < 0.18, the defaults are ``'deglon'`` and
143
+ ``'deglat'``, which correspond to `~ultraplot.ticker.SimpleFormatter`
144
+ presets with degree symbols and cardinal direction suffixes.
145
+ For cartopy >= 0.18, the defaults are ``'dmslon'`` and ``'dmslat'``,
146
+ which uses cartopy's `~cartopy.mpl.ticker.LongitudeFormatter` and
147
+ `~cartopy.mpl.ticker.LatitudeFormatter` formatters with ``dms=True``.
148
+ This formats gridlines that do not fall on whole degrees as "minutes" and
149
+ "seconds" rather than decimal degrees. Use ``dms=False`` to disable this.
150
+ lonformatter_kw, latformatter_kw : dict-like, optional
151
+ Keyword arguments passed to the `matplotlib.ticker.Formatter` class.
152
+ land, ocean, coast, rivers, lakes, borders, innerborders : bool, optional
153
+ Toggles various geographic features. These are actually the
154
+ :rcraw:`land`, :rcraw:`ocean`, :rcraw:`coast`, :rcraw:`rivers`,
155
+ :rcraw:`lakes`, :rcraw:`borders`, and :rcraw:`innerborders`
156
+ settings passed to `~ultraplot.config.Configurator.context`.
157
+ The style can be modified using additional `rc` settings.
158
+
159
+ For example, to change :rcraw:`land.color`, use
160
+ ``ax.format(landcolor='green')``, and to change
161
+ :rcraw:`land.zorder`, use ``ax.format(landzorder=4)``.
162
+ reso : {'lo', 'med', 'hi', 'x-hi', 'xx-hi'}, optional
163
+ *For cartopy axes only.*
164
+ The resolution of geographic features. When basemap is the backend this
165
+ must be passed to `~ultraplot.constructor.Proj` instead.
166
+ color : color-spec, default: :rc:`meta.color`
167
+ The color for the axes edge. Propagates to `labelcolor` unless specified
168
+ otherwise (similar to `ultraplot.axes.CartesianAxes.format`).
169
+ gridcolor : color-spec, default: :rc:`grid.color`
170
+ The color for the gridline labels.
171
+ labelcolor : color-spec, default: `color` or :rc:`grid.labelcolor`
172
+ The color for the gridline labels (`gridlabelcolor` is also allowed).
173
+ labelsize : unit-spec or str, default: :rc:`grid.labelsize`
174
+ The font size for the gridline labels (`gridlabelsize` is also allowed).
175
+ %(units.pt)s
176
+ labelweight : str, default: :rc:`grid.labelweight`
177
+ The font weight for the gridline labels (`gridlabelweight` is also allowed).
178
+ """
179
+ docstring._snippet_manager["geo.format"] = _format_docstring
180
+
181
+
182
+ class _GeoLabel(object):
183
+ """
184
+ Optionally omit overlapping check if an rc setting is disabled.
185
+ """
186
+
187
+ def check_overlapping(self, *args, **kwargs):
188
+ if rc["grid.checkoverlap"]:
189
+ return super().check_overlapping(*args, **kwargs)
190
+ else:
191
+ return False
192
+
193
+
194
+ # Add monkey patch to gridliner module
195
+ if cgridliner is not None and hasattr(cgridliner, "Label"): # only recent versions
196
+ _cls = type("Label", (_GeoLabel, cgridliner.Label), {})
197
+ cgridliner.Label = _cls
198
+
199
+
200
+ class _GeoAxis(object):
201
+ """
202
+ Dummy axis used by longitude and latitude locators and for storing view limits on
203
+ longitude and latitude coordinates. Modeled after how `matplotlib.ticker._DummyAxis`
204
+ and `matplotlib.ticker.TickHelper` are used to control tick locations and labels.
205
+ """
206
+
207
+ # NOTE: Due to cartopy bug (https://github.com/SciTools/cartopy/issues/1564)
208
+ # we store presistent longitude and latitude locators on axes, then *call*
209
+ # them whenever set_extent is called and apply *fixed* locators.
210
+ def __init__(self, axes):
211
+ self.axes = axes
212
+ self.major = maxis.Ticker()
213
+ self.minor = maxis.Ticker()
214
+ self.isDefault_majfmt = True
215
+ self.isDefault_majloc = True
216
+ self.isDefault_minloc = True
217
+ self._interval = None
218
+ self._use_dms = (
219
+ ccrs is not None
220
+ and isinstance(
221
+ axes.projection, (ccrs._RectangularProjection, ccrs.Mercator)
222
+ ) # noqa: E501
223
+ and _version_cartopy >= "0.18"
224
+ )
225
+
226
+ def _get_extent(self):
227
+ # Try to get extent but bail out for projections where this is
228
+ # impossible. So far just transverse Mercator
229
+ try:
230
+ return self.axes.get_extent()
231
+ except Exception:
232
+ lon0 = self.axes._get_lon0()
233
+ return (-180 + lon0, 180 + lon0, -90, 90)
234
+
235
+ @staticmethod
236
+ def _pad_ticks(ticks, vmin, vmax):
237
+ # Wrap up to the longitude/latitude range to avoid
238
+ # giant lists of 10,000 gridline locations.
239
+ if len(ticks) == 0:
240
+ return ticks
241
+ range_ = np.max(ticks) - np.min(ticks)
242
+ vmin = max(vmin, ticks[0] - range_)
243
+ vmax = min(vmax, ticks[-1] + range_)
244
+
245
+ # Pad the reported tick range up to specified range
246
+ step = ticks[1] - ticks[0] # MaxNLocator/AutoMinorLocator steps are equal
247
+ ticks_lo = np.arange(ticks[0], vmin, -step)[1:][::-1]
248
+ ticks_hi = np.arange(ticks[-1], vmax, step)[1:]
249
+ ticks = np.concatenate((ticks_lo, ticks, ticks_hi))
250
+ return ticks
251
+
252
+ def get_scale(self):
253
+ return "linear"
254
+
255
+ def get_tick_space(self):
256
+ return 9 # longstanding default of nbins=9
257
+
258
+ def get_major_formatter(self):
259
+ return self.major.formatter
260
+
261
+ def get_major_locator(self):
262
+ return self.major.locator
263
+
264
+ def get_minor_locator(self):
265
+ return self.minor.locator
266
+
267
+ def get_majorticklocs(self):
268
+ return self._get_ticklocs(self.major.locator)
269
+
270
+ def get_minorticklocs(self):
271
+ return self._get_ticklocs(self.minor.locator)
272
+
273
+ def set_major_formatter(self, formatter, default=False):
274
+ # NOTE: Cartopy formatters check Formatter.axis.axes.projection
275
+ # in order to implement special projection-dependent behavior.
276
+ self.major.formatter = formatter
277
+ formatter.set_axis(self)
278
+ self.isDefault_majfmt = default
279
+
280
+ def set_major_locator(self, locator, default=False):
281
+ self.major.locator = locator
282
+ if self.major.formatter:
283
+ self.major.formatter._set_locator(locator)
284
+ locator.set_axis(self)
285
+ self.isDefault_majloc = default
286
+
287
+ def set_minor_locator(self, locator, default=False):
288
+ self.minor.locator = locator
289
+ locator.set_axis(self)
290
+ self.isDefault_majfmt = default
291
+
292
+ def set_view_interval(self, vmin, vmax):
293
+ self._interval = (vmin, vmax)
294
+
295
+
296
+ class _LonAxis(_GeoAxis):
297
+ """
298
+ Axis with default longitude locator.
299
+ """
300
+
301
+ axis_name = "lon"
302
+
303
+ # NOTE: Basemap accepts tick formatters with drawmeridians(fmt=Formatter())
304
+ # Try to use cartopy formatter if cartopy installed. Otherwise use
305
+ # default builtin basemap formatting.
306
+ def __init__(self, axes):
307
+ super().__init__(axes)
308
+ if self._use_dms:
309
+ locator = formatter = "dmslon"
310
+ else:
311
+ locator = formatter = "deglon"
312
+ self.set_major_formatter(constructor.Formatter(formatter), default=True)
313
+ self.set_major_locator(constructor.Locator(locator), default=True)
314
+ self.set_minor_locator(mticker.AutoMinorLocator(), default=True)
315
+
316
+ def _get_ticklocs(self, locator):
317
+ # Prevent ticks from looping around
318
+ # NOTE: Cartopy 0.17 formats numbers offset by eps with the cardinal indicator
319
+ # (e.g. 0 degrees for map centered on 180 degrees). So skip in that case.
320
+ # NOTE: Common strange issue is e.g. MultipleLocator(60) starts out at
321
+ # -60 degrees for a map from 0 to 360 degrees. If always trimmed circular
322
+ # locations from right then would cut off rightmost gridline. Workaround is
323
+ # to trim on the side closest to central longitude (in this case the left).
324
+ eps = 1e-10
325
+ lon0 = self.axes._get_lon0()
326
+ ticks = np.sort(locator())
327
+ while ticks.size:
328
+ if np.isclose(ticks[0] + 360, ticks[-1]):
329
+ if _version_cartopy >= "0.18" or not np.isclose(ticks[0] % 360, 0):
330
+ ticks[-1] -= eps # ensure label appears on *right* not left
331
+ break
332
+ elif ticks[0] + 360 < ticks[-1]:
333
+ idx = (1, None) if lon0 - ticks[0] > ticks[-1] - lon0 else (None, -1)
334
+ ticks = ticks[slice(*idx)] # cut off ticks looped over globe
335
+ else:
336
+ break
337
+
338
+ # Append extra ticks in case longitude/latitude limits do not encompass
339
+ # the entire view range of map, e.g. for Lambert Conformal sectors.
340
+ # NOTE: Try to avoid making 10,000 element lists. Just wrap extra ticks
341
+ # up to the width of *reported* longitude range.
342
+ if isinstance(locator, (mticker.MaxNLocator, mticker.AutoMinorLocator)):
343
+ ticks = self._pad_ticks(ticks, lon0 - 180 + eps, lon0 + 180 - eps)
344
+
345
+ return ticks
346
+
347
+ def get_view_interval(self):
348
+ # NOTE: ultraplot tries to set its *own* view intervals to avoid dateline
349
+ # weirdness, but if rc['geo.extent'] is 'auto' the interval will be unset.
350
+ # In this case we use _get_extent() as a backup.
351
+ interval = self._interval
352
+ if interval is None:
353
+ extent = self._get_extent()
354
+ interval = extent[:2] # longitude extents
355
+ return interval
356
+
357
+
358
+ class _LatAxis(_GeoAxis):
359
+ """
360
+ Axis with default latitude locator.
361
+ """
362
+
363
+ axis_name = "lat"
364
+
365
+ def __init__(self, axes, latmax=90):
366
+ # NOTE: Need to pass projection because lataxis/lonaxis are
367
+ # initialized before geoaxes is initialized, because format() needs
368
+ # the axes and format() is called by ultraplot.axes.Axes.__init__()
369
+ self._latmax = latmax
370
+ super().__init__(axes)
371
+ if self._use_dms:
372
+ locator = formatter = "dmslat"
373
+ else:
374
+ locator = formatter = "deglat"
375
+ self.set_major_formatter(constructor.Formatter(formatter), default=True)
376
+ self.set_major_locator(constructor.Locator(locator), default=True)
377
+ self.set_minor_locator(mticker.AutoMinorLocator(), default=True)
378
+
379
+ def _get_ticklocs(self, locator):
380
+ # Adjust latitude ticks to fix bug in some projections. Harmless for basemap.
381
+ # NOTE: Maybe this was fixed by cartopy 0.18?
382
+ eps = 1e-10
383
+ ticks = np.sort(locator())
384
+ if ticks.size:
385
+ if ticks[0] == -90:
386
+ ticks[0] += eps
387
+ if ticks[-1] == 90:
388
+ ticks[-1] -= eps
389
+
390
+ # Append extra ticks in case longitude/latitude limits do not encompass
391
+ # the entire view range of map, e.g. for Lambert Conformal sectors.
392
+ if isinstance(locator, (mticker.MaxNLocator, mticker.AutoMinorLocator)):
393
+ ticks = self._pad_ticks(ticks, -90 + eps, 90 - eps)
394
+
395
+ # Filter ticks to latmax range
396
+ latmax = self.get_latmax()
397
+ ticks = ticks[(ticks >= -latmax) & (ticks <= latmax)]
398
+
399
+ return ticks
400
+
401
+ def get_latmax(self):
402
+ return self._latmax
403
+
404
+ def get_view_interval(self):
405
+ interval = self._interval
406
+ if interval is None:
407
+ extent = self._get_extent()
408
+ interval = extent[2:] # latitudes
409
+ return interval
410
+
411
+ def set_latmax(self, latmax):
412
+ self._latmax = latmax
413
+
414
+
415
+ class GeoAxes(plot.PlotAxes):
416
+ """
417
+ Axes subclass for plotting in geographic projections. Uses either cartopy
418
+ or basemap as a "backend".
419
+
420
+ Note
421
+ ----
422
+ This subclass uses longitude and latitude as the default coordinate system for all
423
+ plotting commands by internally passing ``transform=cartopy.crs.PlateCarree()`` to
424
+ cartopy commands and ``latlon=True`` to basemap commands. Also, when using basemap
425
+ as the "backend", plotting is still done "cartopy-style" by calling methods from
426
+ the axes instance rather than the `~mpl_toolkits.basemap.Basemap` instance.
427
+
428
+ Important
429
+ ---------
430
+ This axes subclass can be used by passing ``proj='proj_name'``
431
+ to axes-creation commands like `~ultraplot.figure.Figure.add_axes`,
432
+ `~ultraplot.figure.Figure.add_subplot`, and `~ultraplot.figure.Figure.subplots`,
433
+ where ``proj_name`` is a registered :ref:`PROJ projection name <proj_table>`.
434
+ You can also pass a `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap`
435
+ instance instead of a projection name. Alternatively, you can pass any of the
436
+ matplotlib-recognized axes subclass names ``proj='cartopy'``, ``proj='geo'``, or
437
+ ``proj='geographic'`` with a `~cartopy.crs.Projection` `map_projection` keyword
438
+ argument, or pass ``proj='basemap'`` with a `~mpl_toolkits.basemap.Basemap`
439
+ `map_projection` keyword argument.
440
+ """
441
+
442
+ @docstring._snippet_manager
443
+ def __init__(self, *args, **kwargs):
444
+ """
445
+ Parameters
446
+ ----------
447
+ *args
448
+ Passed to `matplotlib.axes.Axes`.
449
+ map_projection : `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap`
450
+ The cartopy or basemap projection instance. This is
451
+ passed automatically when calling axes-creation
452
+ commands like `~ultraplot.figure.Figure.add_subplot`.
453
+ %(geo.format)s
454
+
455
+ Other parameters
456
+ ----------------
457
+ %(axes.format)s
458
+ %(rc.init)s
459
+
460
+ See also
461
+ --------
462
+ GeoAxes.format
463
+ ultraplot.constructor.Proj
464
+ ultraplot.axes.Axes
465
+ ultraplot.axes.PlotAxes
466
+ ultraplot.figure.Figure.subplot
467
+ ultraplot.figure.Figure.add_subplot
468
+ """
469
+ super().__init__(*args, **kwargs)
470
+
471
+ def _get_lonticklocs(self, which="major"):
472
+ """
473
+ Retrieve longitude tick locations.
474
+ """
475
+ # Get tick locations from dummy axes
476
+ # NOTE: This is workaround for: https://github.com/SciTools/cartopy/issues/1564
477
+ # Since _axes_domain is wrong we determine tick locations ourselves with
478
+ # more accurate extent tracked by _LatAxis and _LonAxis.
479
+ axis = self._lonaxis
480
+ if which == "major":
481
+ lines = axis.get_majorticklocs()
482
+ else:
483
+ lines = axis.get_minorticklocs()
484
+ return lines
485
+
486
+ def _get_latticklocs(self, which="major"):
487
+ """
488
+ Retrieve latitude tick locations.
489
+ """
490
+ axis = self._lataxis
491
+ if which == "major":
492
+ lines = axis.get_majorticklocs()
493
+ else:
494
+ lines = axis.get_minorticklocs()
495
+ return lines
496
+
497
+ def _set_view_intervals(self, extent):
498
+ """
499
+ Update view intervals for lon and lat axis.
500
+ """
501
+ self._lonaxis.set_view_interval(*extent[:2])
502
+ self._lataxis.set_view_interval(*extent[2:])
503
+
504
+ @staticmethod
505
+ def _to_label_array(arg, lon=True):
506
+ """
507
+ Convert labels argument to length-5 boolean array.
508
+ """
509
+ array = arg
510
+ which = "lon" if lon else "lat"
511
+ array = np.atleast_1d(array).tolist()
512
+ if len(array) == 1 and array[0] is None:
513
+ array = [None] * 5
514
+ elif all(isinstance(_, str) for _ in array):
515
+ strings = array # iterate over list of strings
516
+ array = [False] * 5
517
+ opts = ("left", "right", "bottom", "top", "geo")
518
+ for string in strings:
519
+ if string == "all":
520
+ string = "lrbt"
521
+ elif string == "both":
522
+ string = "bt" if lon else "lr"
523
+ elif string == "neither":
524
+ string = ""
525
+ elif string in opts:
526
+ string = string[0]
527
+ if set(string) - set("lrbtg"):
528
+ raise ValueError(
529
+ f"Invalid {which}label string {string!r}. Must be one of "
530
+ + ", ".join(map(repr, (*opts, "neither", "both", "all")))
531
+ + " or a string of single-letter characters like 'lr'."
532
+ )
533
+ for char in string:
534
+ array["lrbtg".index(char)] = True
535
+ if rc["grid.geolabels"] and any(array):
536
+ array[4] = True # possibly toggle geo spine labels
537
+ elif not any(isinstance(_, str) for _ in array):
538
+ if len(array) == 1:
539
+ array.append(False) # default is to label bottom or left
540
+ if len(array) == 2:
541
+ array = [False, False, *array] if lon else [*array, False, False]
542
+ if len(array) == 4:
543
+ b = any(array) if rc["grid.geolabels"] else False
544
+ array.append(b) # possibly toggle geo spine labels
545
+ if len(array) != 5:
546
+ raise ValueError(f"Invald boolean label array length {len(array)}.")
547
+ array = list(map(bool, array))
548
+ else:
549
+ raise ValueError(f"Invalid {which}label spec: {arg}.")
550
+ return array
551
+
552
+ @docstring._snippet_manager
553
+ def format(
554
+ self,
555
+ *,
556
+ extent=None,
557
+ round=None,
558
+ lonlim=None,
559
+ latlim=None,
560
+ boundinglat=None,
561
+ longrid=None,
562
+ latgrid=None,
563
+ longridminor=None,
564
+ latgridminor=None,
565
+ latmax=None,
566
+ nsteps=None,
567
+ lonlocator=None,
568
+ lonlines=None,
569
+ latlocator=None,
570
+ latlines=None,
571
+ lonminorlocator=None,
572
+ lonminorlines=None,
573
+ latminorlocator=None,
574
+ latminorlines=None,
575
+ lonlocator_kw=None,
576
+ lonlines_kw=None,
577
+ latlocator_kw=None,
578
+ latlines_kw=None,
579
+ lonminorlocator_kw=None,
580
+ lonminorlines_kw=None,
581
+ latminorlocator_kw=None,
582
+ latminorlines_kw=None,
583
+ lonformatter=None,
584
+ latformatter=None,
585
+ lonformatter_kw=None,
586
+ latformatter_kw=None,
587
+ labels=None,
588
+ latlabels=None,
589
+ lonlabels=None,
590
+ rotatelabels=None,
591
+ loninline=None,
592
+ latinline=None,
593
+ inlinelabels=None,
594
+ dms=None,
595
+ labelpad=None,
596
+ labelcolor=None,
597
+ labelsize=None,
598
+ labelweight=None,
599
+ **kwargs,
600
+ ):
601
+ """
602
+ Modify map limits, longitude and latitude
603
+ gridlines, geographic features, and more.
604
+
605
+ Parameters
606
+ ----------
607
+ %(geo.format)s
608
+
609
+ Other parameters
610
+ ----------------
611
+ %(axes.format)s
612
+ %(figure.format)s
613
+ %(rc.format)s
614
+
615
+ See also
616
+ --------
617
+ ultraplot.axes.Axes.format
618
+ ultraplot.config.Configurator.context
619
+ """
620
+ # Initialize map boundary
621
+ # WARNING: Normal workflow is Axes.format() does 'universal' tasks including
622
+ # updating the map boundary (in the future may also handle gridlines). However
623
+ # drawing gridlines before basemap map boundary will call set_axes_limits()
624
+ # which initializes a boundary hidden from external access. So we must call
625
+ # it here. Must do this between mpl.Axes.__init__() and base.Axes.format().
626
+ if self._name == "basemap" and self._map_boundary is None:
627
+ if self.projection.projection in self._proj_non_rectangular:
628
+ patch = self.projection.drawmapboundary(ax=self)
629
+ self._map_boundary = patch
630
+ else:
631
+ self.projection.set_axes_limits(self) # initialize aspect ratio
632
+ self._map_boundary = object() # sentinel
633
+
634
+ # Initiate context block
635
+ rc_kw, rc_mode = _pop_rc(kwargs)
636
+ lonlabels = _not_none(lonlabels, labels)
637
+ latlabels = _not_none(latlabels, labels)
638
+ if "0.18" <= _version_cartopy < "0.20":
639
+ lonlabels = _not_none(lonlabels, loninline, inlinelabels)
640
+ latlabels = _not_none(latlabels, latinline, inlinelabels)
641
+ labelcolor = _not_none(labelcolor, kwargs.get("color", None))
642
+ if labelcolor is not None:
643
+ rc_kw["grid.labelcolor"] = labelcolor
644
+ if labelsize is not None:
645
+ rc_kw["grid.labelsize"] = labelsize
646
+ if labelweight is not None:
647
+ rc_kw["grid.labelweight"] = labelweight
648
+ with rc.context(rc_kw, mode=rc_mode):
649
+ # Apply extent mode first
650
+ # NOTE: We deprecate autoextent on _CartopyAxes with _rename_kwargs which
651
+ # does not translate boolean flag. So here apply translation.
652
+ if extent is not None and not isinstance(extent, str):
653
+ extent = ("globe", "auto")[int(bool(extent))]
654
+ self._update_boundary(round)
655
+ self._update_extent_mode(extent, boundinglat)
656
+
657
+ # Retrieve label toggles
658
+ # NOTE: Cartopy 0.18 and 0.19 inline labels require any of
659
+ # top, bottom, left, or right to be toggled then ignores them.
660
+ # Later versions of cartopy permit both or neither labels.
661
+ labels = _not_none(labels, rc.find("grid.labels", context=True))
662
+ lonlabels = _not_none(lonlabels, labels)
663
+ latlabels = _not_none(latlabels, labels)
664
+ lonarray = self._to_label_array(lonlabels, lon=True)
665
+ latarray = self._to_label_array(latlabels, lon=False)
666
+
667
+ # Update max latitude
668
+ latmax = _not_none(latmax, rc.find("grid.latmax", context=True))
669
+ if latmax is not None:
670
+ self._lataxis.set_latmax(latmax)
671
+
672
+ # Update major locators
673
+ lonlocator = _not_none(lonlocator=lonlocator, lonlines=lonlines)
674
+ latlocator = _not_none(latlocator=latlocator, latlines=latlines)
675
+ if lonlocator is not None:
676
+ lonlocator_kw = _not_none(
677
+ lonlocator_kw=lonlocator_kw,
678
+ lonlines_kw=lonlines_kw,
679
+ default={},
680
+ )
681
+ locator = constructor.Locator(lonlocator, **lonlocator_kw)
682
+ self._lonaxis.set_major_locator(locator)
683
+ if latlocator is not None:
684
+ latlocator_kw = _not_none(
685
+ latlocator_kw=latlocator_kw,
686
+ latlines_kw=latlines_kw,
687
+ default={},
688
+ )
689
+ locator = constructor.Locator(latlocator, **latlocator_kw)
690
+ self._lataxis.set_major_locator(locator)
691
+
692
+ # Update minor locators
693
+ lonminorlocator = _not_none(
694
+ lonminorlocator=lonminorlocator, lonminorlines=lonminorlines
695
+ )
696
+ latminorlocator = _not_none(
697
+ latminorlocator=latminorlocator, latminorlines=latminorlines
698
+ )
699
+ if lonminorlocator is not None:
700
+ lonminorlocator_kw = _not_none(
701
+ lonminorlocator_kw=lonminorlocator_kw,
702
+ lonminorlines_kw=lonminorlines_kw,
703
+ default={},
704
+ )
705
+ locator = constructor.Locator(lonminorlocator, **lonminorlocator_kw)
706
+ self._lonaxis.set_minor_locator(locator)
707
+ if latminorlocator is not None:
708
+ latminorlocator_kw = _not_none(
709
+ latminorlocator_kw=latminorlocator_kw,
710
+ latminorlines_kw=latminorlines_kw,
711
+ default={},
712
+ )
713
+ locator = constructor.Locator(latminorlocator, **latminorlocator_kw)
714
+ self._lataxis.set_minor_locator(locator)
715
+
716
+ # Update formatters
717
+ loninline = _not_none(
718
+ loninline, inlinelabels, rc.find("grid.inlinelabels", context=True)
719
+ ) # noqa: E501
720
+ latinline = _not_none(
721
+ latinline, inlinelabels, rc.find("grid.inlinelabels", context=True)
722
+ ) # noqa: E501
723
+ rotatelabels = _not_none(
724
+ rotatelabels, rc.find("grid.rotatelabels", context=True)
725
+ ) # noqa: E501
726
+ labelpad = _not_none(labelpad, rc.find("grid.labelpad", context=True))
727
+ dms = _not_none(dms, rc.find("grid.dmslabels", context=True))
728
+ nsteps = _not_none(nsteps, rc.find("grid.nsteps", context=True))
729
+ if lonformatter is not None:
730
+ lonformatter_kw = lonformatter_kw or {}
731
+ formatter = constructor.Formatter(lonformatter, **lonformatter_kw)
732
+ self._lonaxis.set_major_formatter(formatter)
733
+ if latformatter is not None:
734
+ latformatter_kw = latformatter_kw or {}
735
+ formatter = constructor.Formatter(latformatter, **latformatter_kw)
736
+ self._lataxis.set_major_formatter(formatter)
737
+ if dms is not None: # harmless if these are not GeoLocators
738
+ self._lonaxis.get_major_formatter()._dms = dms
739
+ self._lataxis.get_major_formatter()._dms = dms
740
+ self._lonaxis.get_major_locator()._dms = dms
741
+ self._lataxis.get_major_locator()._dms = dms
742
+
743
+ # Apply worker extent, feature, and gridline functions
744
+ lonlim = _not_none(lonlim, default=(None, None))
745
+ latlim = _not_none(latlim, default=(None, None))
746
+ self._update_extent(lonlim=lonlim, latlim=latlim, boundinglat=boundinglat)
747
+ self._update_features()
748
+ self._update_major_gridlines(
749
+ longrid=longrid,
750
+ latgrid=latgrid, # gridline toggles
751
+ lonarray=lonarray,
752
+ latarray=latarray, # label toggles
753
+ loninline=loninline,
754
+ latinline=latinline,
755
+ rotatelabels=rotatelabels,
756
+ labelpad=labelpad,
757
+ nsteps=nsteps,
758
+ )
759
+ self._update_minor_gridlines(
760
+ longrid=longridminor,
761
+ latgrid=latgridminor,
762
+ nsteps=nsteps,
763
+ )
764
+
765
+ # Parent format method
766
+ super().format(rc_kw=rc_kw, rc_mode=rc_mode, **kwargs)
767
+
768
+ @property
769
+ def gridlines_major(self):
770
+ """
771
+ The cartopy `~cartopy.mpl.gridliner.Gridliner`
772
+ used for major gridlines or a 2-tuple containing the
773
+ (longitude, latitude) major gridlines returned by
774
+ basemap's `~mpl_toolkits.basemap.Basemap.drawmeridians`
775
+ and `~mpl_toolkits.basemap.Basemap.drawparallels`.
776
+ This can be used for customization and debugging.
777
+ """
778
+ if self._name == "basemap":
779
+ return (self._lonlines_major, self._latlines_major)
780
+ else:
781
+ return self._gridlines_major
782
+
783
+ @property
784
+ def gridlines_minor(self):
785
+ """
786
+ The cartopy `~cartopy.mpl.gridliner.Gridliner`
787
+ used for minor gridlines or a 2-tuple containing the
788
+ (longitude, latitude) minor gridlines returned by
789
+ basemap's `~mpl_toolkits.basemap.Basemap.drawmeridians`
790
+ and `~mpl_toolkits.basemap.Basemap.drawparallels`.
791
+ This can be used for customization and debugging.
792
+ """
793
+ if self._name == "basemap":
794
+ return (self._lonlines_minor, self._latlines_minor)
795
+ else:
796
+ return self._gridlines_minor
797
+
798
+ @property
799
+ def projection(self):
800
+ """
801
+ The cartopy `~cartopy.crs.Projection` or basemap `~mpl_toolkits.basemap.Basemap`
802
+ instance associated with this axes.
803
+ """
804
+ return self._map_projection
805
+
806
+ @projection.setter
807
+ def projection(self, map_projection):
808
+ cls = self._proj_class
809
+ if not isinstance(map_projection, cls):
810
+ raise ValueError(f"Projection must be a {cls} instance.")
811
+ self._map_projection = map_projection
812
+
813
+
814
+ class _CartopyAxes(GeoAxes, _GeoAxes):
815
+ """
816
+ Axes subclass for plotting cartopy projections.
817
+ """
818
+
819
+ _name = "cartopy"
820
+ _name_aliases = ("geo", "geographic") # default 'geographic' axes
821
+ _proj_class = Projection
822
+ _proj_north = (
823
+ pproj.NorthPolarStereo,
824
+ pproj.NorthPolarGnomonic,
825
+ pproj.NorthPolarAzimuthalEquidistant,
826
+ pproj.NorthPolarLambertAzimuthalEqualArea,
827
+ )
828
+ _proj_south = (
829
+ pproj.SouthPolarStereo,
830
+ pproj.SouthPolarGnomonic,
831
+ pproj.SouthPolarAzimuthalEquidistant,
832
+ pproj.SouthPolarLambertAzimuthalEqualArea,
833
+ )
834
+ _proj_polar = _proj_north + _proj_south
835
+
836
+ # NOTE: The rename argument wrapper belongs here instead of format() because
837
+ # these arguments were previously only accepted during initialization.
838
+ @warnings._rename_kwargs("0.10", circular="round", autoextent="extent")
839
+ def __init__(self, *args, map_projection=None, **kwargs):
840
+ """
841
+ Parameters
842
+ ----------
843
+ map_projection : ~cartopy.crs.Projection
844
+ The map projection.
845
+ *args, **kwargs
846
+ Passed to `GeoAxes`.
847
+ """
848
+ # Initialize axes. Note that critical attributes like outline_patch
849
+ # needed by _format_apply are added before it is called.
850
+ import cartopy # noqa: F401 verify package is available
851
+
852
+ self.projection = map_projection # verify
853
+ polar = isinstance(self.projection, self._proj_polar)
854
+ latmax = 80 if polar else 90 # default latmax
855
+ self._is_round = False
856
+ self._boundinglat = None # NOTE: must start at None so _update_extent acts
857
+ self._gridlines_major = None
858
+ self._gridlines_minor = None
859
+ self._lonaxis = _LonAxis(self)
860
+ self._lataxis = _LatAxis(self, latmax=latmax)
861
+ # 'map_projection' argument is deprecated since cartopy 0.21 and
862
+ # replaced by 'projection'.
863
+ if _version_cartopy >= "0.21":
864
+ super().__init__(*args, projection=self.projection, **kwargs)
865
+ else:
866
+ super().__init__(*args, map_projection=self.projection, **kwargs)
867
+ for axis in (self.xaxis, self.yaxis):
868
+ axis.set_tick_params(which="both", size=0) # prevent extra label offset
869
+
870
+ def _apply_axis_sharing(self): # noqa: U100
871
+ """
872
+ No-op for now. In future will hide labels on certain subplots.
873
+ """
874
+ pass
875
+
876
+ @staticmethod
877
+ def _get_circle_path(N=100):
878
+ """
879
+ Return a circle `~matplotlib.path.Path` used as the outline for polar
880
+ stereographic, azimuthal equidistant, Lambert conformal, and gnomonic
881
+ projections. This was developed from `this cartopy example \
882
+ <https://scitools.org.uk/cartopy/docs/v0.15/examples/always_circular_stereo.html>`__.
883
+ """
884
+ theta = np.linspace(0, 2 * np.pi, N)
885
+ center, radius = [0.5, 0.5], 0.5
886
+ verts = np.vstack([np.sin(theta), np.cos(theta)]).T
887
+ return mpath.Path(verts * radius + center)
888
+
889
+ def _get_global_extent(self):
890
+ """
891
+ Return the global extent with meridian properly shifted.
892
+ """
893
+ lon0 = self._get_lon0()
894
+ return [-180 + lon0, 180 + lon0, -90, 90]
895
+
896
+ def _get_lon0(self):
897
+ """
898
+ Get the central longitude. Default is ``0``.
899
+ """
900
+ return self.projection.proj4_params.get("lon_0", 0)
901
+
902
+ def _init_gridlines(self):
903
+ """
904
+ Create monkey patched "major" and "minor" gridliners managed by ultraplot.
905
+ """
906
+
907
+ # Cartopy < 0.18 monkey patch. Helps filter valid coordates to lon_0 +/- 180
908
+ def _axes_domain(self, *args, **kwargs):
909
+ x_range, y_range = type(self)._axes_domain(self, *args, **kwargs)
910
+ if _version_cartopy < "0.18":
911
+ lon_0 = self.axes.projection.proj4_params.get("lon_0", 0)
912
+ x_range = np.asarray(x_range) + lon_0
913
+ return x_range, y_range
914
+
915
+ # Cartopy >= 0.18 monkey patch. Fixes issue where cartopy draws an overlapping
916
+ # dateline gridline (e.g. polar maps). See the nx -= 1 line in _draw_gridliner
917
+ def _draw_gridliner(self, *args, **kwargs): # noqa: E306
918
+ result = type(self)._draw_gridliner(self, *args, **kwargs)
919
+ if _version_cartopy >= "0.18":
920
+ lon_lim, _ = self._axes_domain()
921
+ if abs(np.diff(lon_lim)) == abs(np.diff(self.crs.x_limits)):
922
+ for collection in self.xline_artists:
923
+ if not getattr(collection, "_cartopy_fix", False):
924
+ collection.get_paths().pop(-1)
925
+ collection._cartopy_fix = True
926
+ return result
927
+
928
+ # Return the gridliner with monkey patch
929
+ gl = self.gridlines(crs=ccrs.PlateCarree())
930
+ gl._axes_domain = _axes_domain.__get__(gl)
931
+ gl._draw_gridliner = _draw_gridliner.__get__(gl)
932
+ gl.xlines = gl.ylines = False
933
+ self._toggle_gridliner_labels(gl, False, False, False, False, False)
934
+ return gl
935
+
936
+ @staticmethod
937
+ def _toggle_gridliner_labels(
938
+ gl, left=None, right=None, bottom=None, top=None, geo=None
939
+ ):
940
+ """
941
+ Toggle gridliner labels across different cartopy versions.
942
+ """
943
+ if _version_cartopy >= "0.18":
944
+ left_labels = "left_labels"
945
+ right_labels = "right_labels"
946
+ bottom_labels = "bottom_labels"
947
+ top_labels = "top_labels"
948
+ else: # cartopy < 0.18
949
+ left_labels = "ylabels_left"
950
+ right_labels = "ylabels_right"
951
+ bottom_labels = "xlabels_bottom"
952
+ top_labels = "xlabels_top"
953
+ if left is not None:
954
+ setattr(gl, left_labels, left)
955
+ if right is not None:
956
+ setattr(gl, right_labels, right)
957
+ if bottom is not None:
958
+ setattr(gl, bottom_labels, bottom)
959
+ if top is not None:
960
+ setattr(gl, top_labels, top)
961
+ if geo is not None: # only cartopy 0.20 supported but harmless
962
+ setattr(gl, "geo_labels", geo)
963
+
964
+ def _update_background(self, **kwargs):
965
+ """
966
+ Update the map background patches. This is called in `Axes.format`.
967
+ """
968
+ # TODO: Understand issue where setting global linewidth puts map boundary on
969
+ # top of land patches, but setting linewidth with format() (even with separate
970
+ # format() calls) puts map boundary underneath. Zorder seems to be totally
971
+ # ignored and using spines vs. patch makes no difference.
972
+ # NOTE: outline_patch is redundant, use background_patch instead
973
+ kw_face, kw_edge = rc._get_background_props(native=False, **kwargs)
974
+ kw_face["linewidth"] = 0
975
+ kw_edge["facecolor"] = "none"
976
+ if _version_cartopy >= "0.18":
977
+ self.patch.update(kw_face)
978
+ self.spines["geo"].update(kw_edge)
979
+ else:
980
+ self.background_patch.update(kw_face)
981
+ self.outline_patch.update(kw_edge)
982
+
983
+ def _update_boundary(self, round=None):
984
+ """
985
+ Update the map boundary path.
986
+ """
987
+ round = _not_none(round, rc.find("geo.round", context=True))
988
+ if round is None or not isinstance(self.projection, self._proj_polar):
989
+ pass
990
+ elif round:
991
+ self._is_round = True
992
+ self.set_boundary(self._get_circle_path(), transform=self.transAxes)
993
+ elif not round and self._is_round:
994
+ if hasattr(self, "_boundary"):
995
+ self._boundary()
996
+ else:
997
+ warnings._warn_ultraplot("Failed to reset round map boundary.")
998
+
999
+ def _update_extent_mode(self, extent=None, boundinglat=None):
1000
+ """
1001
+ Update the extent mode.
1002
+ """
1003
+ # NOTE: Use set_global rather than set_extent() or _update_extent() for
1004
+ # simplicity. Uses projection.[xy]_limits which may not be strictly global.
1005
+ # NOTE: For some reason initial call to _set_view_intervals may change the
1006
+ # default boundary with extent='auto'. Try this in a robinson projection:
1007
+ # ax.contour(np.linspace(-90, 180, N), np.linspace(0, 90, N), np.zeros(N, N))
1008
+ extent = _not_none(extent, rc.find("geo.extent", context=True))
1009
+ if extent is None:
1010
+ return
1011
+ if extent not in ("globe", "auto"):
1012
+ raise ValueError(
1013
+ f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'."
1014
+ )
1015
+ polar = isinstance(self.projection, self._proj_polar)
1016
+ if not polar:
1017
+ self.set_global()
1018
+ else:
1019
+ if isinstance(self.projection, pproj.NorthPolarGnomonic):
1020
+ default_boundinglat = 30
1021
+ elif isinstance(self.projection, pproj.SouthPolarGnomonic):
1022
+ default_boundinglat = -30
1023
+ else:
1024
+ default_boundinglat = 0
1025
+ boundinglat = _not_none(boundinglat, default_boundinglat)
1026
+ self._update_extent(boundinglat=boundinglat)
1027
+ if extent == "auto":
1028
+ # NOTE: This will work even if applied after plotting stuff
1029
+ # and fixing the limits. Very easy to toggle on and off.
1030
+ self.set_autoscalex_on(True)
1031
+ self.set_autoscaley_on(True)
1032
+
1033
+ def _update_extent(self, lonlim=None, latlim=None, boundinglat=None):
1034
+ """
1035
+ Set the projection extent.
1036
+ """
1037
+ # Projection extent
1038
+ # NOTE: Lon axis and lat axis extents are updated by set_extent.
1039
+ # WARNING: The set_extent method tries to set a *rectangle* between the *4*
1040
+ # (x, y) coordinate pairs (each corner), so something like (-180, 180, -90, 90)
1041
+ # will result in *line*, causing error! We correct this here.
1042
+ eps = 1e-10 # bug with full -180, 180 range when lon_0 != 0
1043
+ lon0 = self._get_lon0()
1044
+ proj = type(self.projection).__name__
1045
+ north = isinstance(self.projection, self._proj_north)
1046
+ south = isinstance(self.projection, self._proj_south)
1047
+ lonlim = _not_none(lonlim, (None, None))
1048
+ latlim = _not_none(latlim, (None, None))
1049
+ if north or south:
1050
+ if any(_ is not None for _ in (*lonlim, *latlim)):
1051
+ warnings._warn_ultraplot(
1052
+ f'{proj!r} extent is controlled by "boundinglat", '
1053
+ f"ignoring lonlim={lonlim!r} and latlim={latlim!r}."
1054
+ )
1055
+ if boundinglat is not None and boundinglat != self._boundinglat:
1056
+ lat0 = 90 if north else -90
1057
+ lon0 = self._get_lon0()
1058
+ extent = [lon0 - 180 + eps, lon0 + 180 - eps, boundinglat, lat0]
1059
+ self.set_extent(extent, crs=ccrs.PlateCarree())
1060
+ self._boundinglat = boundinglat
1061
+
1062
+ # Rectangular extent
1063
+ else:
1064
+ if boundinglat is not None:
1065
+ warnings._warn_ultraplot(
1066
+ f'{proj!r} extent is controlled by "lonlim" and "latlim", '
1067
+ f"ignoring boundinglat={boundinglat!r}."
1068
+ )
1069
+ if any(_ is not None for _ in (*lonlim, *latlim)):
1070
+ lonlim = list(lonlim)
1071
+ if lonlim[0] is None:
1072
+ lonlim[0] = lon0 - 180
1073
+ if lonlim[1] is None:
1074
+ lonlim[1] = lon0 + 180
1075
+ lonlim[0] += eps
1076
+ latlim = list(latlim)
1077
+ if latlim[0] is None:
1078
+ latlim[0] = -90
1079
+ if latlim[1] is None:
1080
+ latlim[1] = 90
1081
+ extent = lonlim + latlim
1082
+ self.set_extent(extent, crs=ccrs.PlateCarree())
1083
+
1084
+ def _update_features(self):
1085
+ """
1086
+ Update geographic features.
1087
+ """
1088
+ # NOTE: The e.g. cfeature.COASTLINE features are just for convenience,
1089
+ # lo res versions. Use NaturalEarthFeature instead.
1090
+ # WARNING: Seems cartopy features cannot be updated! Updating _kwargs
1091
+ # attribute does *nothing*.
1092
+ reso = rc["reso"] # resolution cannot be changed after feature created
1093
+ try:
1094
+ reso = constructor.RESOS_CARTOPY[reso]
1095
+ except KeyError:
1096
+ raise ValueError(
1097
+ f"Invalid resolution {reso!r}. Options are: "
1098
+ + ", ".join(map(repr, constructor.RESOS_CARTOPY))
1099
+ + "."
1100
+ )
1101
+ for name, args in constructor.FEATURES_CARTOPY.items():
1102
+ # Draw feature or toggle feature off
1103
+ b = rc.find(name, context=True)
1104
+ attr = f"_{name}_feature"
1105
+ feat = getattr(self, attr, None)
1106
+ drawn = feat is not None # if exists, apply *updated* settings
1107
+ if b is not None:
1108
+ if not b:
1109
+ if drawn: # toggle existing feature off
1110
+ feat.set_visible(False)
1111
+ else:
1112
+ if not drawn:
1113
+ feat = cfeature.NaturalEarthFeature(*args, reso)
1114
+ feat = self.add_feature(feat) # convert to FeatureArtist
1115
+ setattr(self, attr, feat)
1116
+
1117
+ # Update artist attributes (FeatureArtist._kwargs used back to v0.5).
1118
+ # For 'lines', need to specify edgecolor and facecolor
1119
+ # See: https://github.com/SciTools/cartopy/issues/803
1120
+ if feat is not None:
1121
+ kw = rc.category(name, context=drawn)
1122
+ if name in ("coast", "rivers", "borders", "innerborders"):
1123
+ if "color" in kw:
1124
+ kw.update({"edgecolor": kw.pop("color"), "facecolor": "none"})
1125
+ else:
1126
+ kw.update({"linewidth": 0})
1127
+ if "zorder" in kw:
1128
+ # NOTE: Necessary to update zorder directly because _kwargs
1129
+ # attributes are not applied until draw()... at which point
1130
+ # matplotlib is drawing in the order based on the *old* zorder.
1131
+ feat.set_zorder(kw["zorder"])
1132
+ if hasattr(feat, "_kwargs"):
1133
+ feat._kwargs.update(kw)
1134
+ if _version_cartopy >= "0.23":
1135
+ feat.set(**feat._kwargs)
1136
+
1137
+ def _update_gridlines(
1138
+ self,
1139
+ gl,
1140
+ which="major",
1141
+ longrid=None,
1142
+ latgrid=None,
1143
+ nsteps=None,
1144
+ ):
1145
+ """
1146
+ Update gridliner object with axis locators, and toggle gridlines on and off.
1147
+ """
1148
+ # Update gridliner collection properties
1149
+ # WARNING: Here we use native matplotlib 'grid' rc param for geographic
1150
+ # gridlines. If rc mode is 1 (first format call) use context=False
1151
+ kwlines = rc._get_gridline_props(which=which, native=False)
1152
+ kwtext = rc._get_ticklabel_props(native=False)
1153
+ gl.collection_kwargs.update(kwlines)
1154
+ gl.xlabel_style.update(kwtext)
1155
+ gl.ylabel_style.update(kwtext)
1156
+
1157
+ # Apply tick locations from dummy _LonAxis and _LatAxis axes
1158
+ # NOTE: This will re-apply existing gridline locations if unchanged.
1159
+ if nsteps is not None:
1160
+ gl.n_steps = nsteps
1161
+ latmax = self._lataxis.get_latmax()
1162
+ if _version_cartopy >= "0.19":
1163
+ gl.ylim = (-latmax, latmax)
1164
+ longrid = rc._get_gridline_bool(longrid, axis="x", which=which, native=False)
1165
+ if longrid is not None:
1166
+ gl.xlines = longrid
1167
+ latgrid = rc._get_gridline_bool(latgrid, axis="y", which=which, native=False)
1168
+ if latgrid is not None:
1169
+ gl.ylines = latgrid
1170
+ lonlines = self._get_lonticklocs(which=which)
1171
+ latlines = self._get_latticklocs(which=which)
1172
+ if _version_cartopy >= "0.18": # see lukelbd/ultraplot#208
1173
+ lonlines = (np.asarray(lonlines) + 180) % 360 - 180 # only for cartopy
1174
+ gl.xlocator = mticker.FixedLocator(lonlines)
1175
+ gl.ylocator = mticker.FixedLocator(latlines)
1176
+
1177
+ def _update_major_gridlines(
1178
+ self,
1179
+ longrid=None,
1180
+ latgrid=None,
1181
+ lonarray=None,
1182
+ latarray=None,
1183
+ loninline=None,
1184
+ latinline=None,
1185
+ labelpad=None,
1186
+ rotatelabels=None,
1187
+ nsteps=None,
1188
+ ):
1189
+ """
1190
+ Update major gridlines.
1191
+ """
1192
+ # Update gridline locations and style
1193
+ gl = self._gridlines_major
1194
+ if gl is None:
1195
+ gl = self._gridlines_major = self._init_gridlines()
1196
+ self._update_gridlines(
1197
+ gl,
1198
+ which="major",
1199
+ longrid=longrid,
1200
+ latgrid=latgrid,
1201
+ nsteps=nsteps,
1202
+ )
1203
+ gl.xformatter = self._lonaxis.get_major_formatter()
1204
+ gl.yformatter = self._lataxis.get_major_formatter()
1205
+
1206
+ # Update gridline label parameters
1207
+ # NOTE: Cartopy 0.18 and 0.19 can not draw both edge and inline labels. Instead
1208
+ # requires both a set 'side' and 'x_inline' is True (applied in GeoAxes.format).
1209
+ # NOTE: The 'xpadding' and 'ypadding' props were introduced in v0.16
1210
+ # with default 5 points, then set to default None in v0.18.
1211
+ # TODO: Cartopy has had two formatters for a while but we use the newer one.
1212
+ # See https://github.com/SciTools/cartopy/pull/1066
1213
+ if labelpad is not None:
1214
+ gl.xpadding = gl.ypadding = labelpad
1215
+ if loninline is not None:
1216
+ gl.x_inline = bool(loninline)
1217
+ if latinline is not None:
1218
+ gl.y_inline = bool(latinline)
1219
+ if rotatelabels is not None:
1220
+ gl.rotate_labels = bool(rotatelabels) # ignored in cartopy < 0.18
1221
+ if latinline is not None or loninline is not None:
1222
+ lon, lat = loninline, latinline
1223
+ b = True if lon and lat else "x" if lon else "y" if lat else None
1224
+ gl.inline_labels = b # ignored in cartopy < 0.20
1225
+
1226
+ # Gridline label toggling
1227
+ # Issue warning instead of error!
1228
+ if _version_cartopy < "0.18" and not isinstance(
1229
+ self.projection, (ccrs.Mercator, ccrs.PlateCarree)
1230
+ ):
1231
+ if any(latarray):
1232
+ warnings._warn_ultraplot(
1233
+ "Cannot add gridline labels to cartopy "
1234
+ f"{type(self.projection).__name__} projection."
1235
+ )
1236
+ latarray = [False] * 5
1237
+ if any(lonarray):
1238
+ warnings._warn_ultraplot(
1239
+ "Cannot add gridline labels to cartopy "
1240
+ f"{type(self.projection).__name__} projection."
1241
+ )
1242
+ lonarray = [False] * 5
1243
+ array = [
1244
+ (
1245
+ True
1246
+ if lon and lat
1247
+ else (
1248
+ "x"
1249
+ if lon
1250
+ else (
1251
+ "y"
1252
+ if lat
1253
+ else False if lon is not None or lon is not None else None
1254
+ )
1255
+ )
1256
+ )
1257
+ for lon, lat in zip(lonarray, latarray)
1258
+ ]
1259
+ self._toggle_gridliner_labels(gl, *array[:2], *array[2:4], array[4])
1260
+
1261
+ def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None):
1262
+ """
1263
+ Update minor gridlines.
1264
+ """
1265
+ gl = self._gridlines_minor
1266
+ if gl is None:
1267
+ gl = self._gridlines_minor = self._init_gridlines()
1268
+ self._update_gridlines(
1269
+ gl,
1270
+ which="minor",
1271
+ longrid=longrid,
1272
+ latgrid=latgrid,
1273
+ nsteps=nsteps,
1274
+ )
1275
+
1276
+ def get_extent(self, crs=None):
1277
+ # Get extent and try to repair longitude bounds.
1278
+ if crs is None:
1279
+ crs = ccrs.PlateCarree()
1280
+ extent = super().get_extent(crs=crs)
1281
+ if isinstance(crs, ccrs.PlateCarree):
1282
+ if np.isclose(extent[0], -180) and np.isclose(extent[-1], 180):
1283
+ # Repair longitude bounds to reflect dateline position
1284
+ # NOTE: This is critical so we can prevent duplicate gridlines
1285
+ # on dateline. See _update_gridlines.
1286
+ lon0 = self._get_lon0()
1287
+ extent[:2] = [lon0 - 180, lon0 + 180]
1288
+ return extent
1289
+
1290
+ def get_tightbbox(self, renderer, *args, **kwargs):
1291
+ # Perform extra post-processing steps
1292
+ # For now this just draws the gridliners
1293
+ self._apply_axis_sharing()
1294
+ if self.get_autoscale_on() and self.ignore_existing_data_limits:
1295
+ self.autoscale_view()
1296
+
1297
+ # Adjust location
1298
+ if _version_cartopy >= "0.18":
1299
+ self.patch._adjust_location() # this does the below steps
1300
+ elif getattr(self.background_patch, "reclip", None) and hasattr(
1301
+ self.background_patch, "orig_path"
1302
+ ):
1303
+ clipped_path = self.background_patch.orig_path.clip_to_bbox(self.viewLim)
1304
+ self.outline_patch._path = clipped_path
1305
+ self.background_patch._path = clipped_path
1306
+
1307
+ # Apply aspect
1308
+ self.apply_aspect()
1309
+ if _version_cartopy >= "0.23":
1310
+ gridliners = [
1311
+ a for a in self.artists if isinstance(a, cgridliner.Gridliner)
1312
+ ]
1313
+ else:
1314
+ gridliners = self._gridliners
1315
+
1316
+ for gl in gridliners:
1317
+ if _version_cartopy >= "0.18":
1318
+ gl._draw_gridliner(renderer=renderer)
1319
+ else:
1320
+ gl._draw_gridliner(background_patch=self.background_patch)
1321
+
1322
+ # Remove gridliners
1323
+ if _version_cartopy < "0.18":
1324
+ self._gridliners = []
1325
+
1326
+ return super().get_tightbbox(renderer, *args, **kwargs)
1327
+
1328
+ def set_extent(self, extent, crs=None):
1329
+ # Fix paths, so axes tight bounding box gets correct box! From this issue:
1330
+ # https://github.com/SciTools/cartopy/issues/1207#issuecomment-439975083
1331
+ # Also record the requested longitude latitude extent so we can use these
1332
+ # values for LongitudeLocator and LatitudeLocator. Otherwise if longitude
1333
+ # extent is across dateline LongitudeLocator fails because get_extent()
1334
+ # reports -180 to 180: https://github.com/SciTools/cartopy/issues/1564
1335
+ # NOTE: This is *also* not perfect because if set_extent() was called
1336
+ # and extent crosses map boundary of rectangular projection, the *actual*
1337
+ # resulting extent is the opposite. But that means user has messed up anyway
1338
+ # so probably doesn't matter if gridlines are also wrong.
1339
+ if crs is None:
1340
+ crs = ccrs.PlateCarree()
1341
+ if isinstance(crs, ccrs.PlateCarree):
1342
+ self._set_view_intervals(extent)
1343
+ with rc.context(mode=2): # do not reset gridline properties!
1344
+ if self._gridlines_major is not None:
1345
+ self._update_gridlines(self._gridlines_major, which="major")
1346
+ if self._gridlines_minor is not None:
1347
+ self._update_gridlines(self._gridlines_minor, which="minor")
1348
+ if _version_cartopy < "0.18":
1349
+ clipped_path = self.outline_patch.orig_path.clip_to_bbox(self.viewLim)
1350
+ self.outline_patch._path = clipped_path
1351
+ self.background_patch._path = clipped_path
1352
+ return super().set_extent(extent, crs=crs)
1353
+
1354
+ def set_global(self):
1355
+ # Set up "global" extent and update _LatAxis and _LonAxis view intervals
1356
+ result = super().set_global()
1357
+ self._set_view_intervals(self._get_global_extent())
1358
+ return result
1359
+
1360
+
1361
+ class _BasemapAxes(GeoAxes):
1362
+ """
1363
+ Axes subclass for plotting basemap projections.
1364
+ """
1365
+
1366
+ _name = "basemap"
1367
+ _proj_class = Basemap
1368
+ _proj_north = ("npaeqd", "nplaea", "npstere")
1369
+ _proj_south = ("spaeqd", "splaea", "spstere")
1370
+ _proj_polar = _proj_north + _proj_south
1371
+ _proj_non_rectangular = _proj_polar + ( # do not use axes spines as boundaries
1372
+ "ortho",
1373
+ "geos",
1374
+ "nsper",
1375
+ "moll",
1376
+ "hammer",
1377
+ "robin",
1378
+ "eck4",
1379
+ "kav7",
1380
+ "mbtfpq",
1381
+ "sinu",
1382
+ "vandg",
1383
+ )
1384
+
1385
+ def __init__(self, *args, map_projection=None, **kwargs):
1386
+ """
1387
+ Parameters
1388
+ ----------
1389
+ map_projection : ~mpl_toolkits.basemap.Basemap
1390
+ The map projection.
1391
+ *args, **kwargs
1392
+ Passed to `GeoAxes`.
1393
+ """
1394
+ # First assign projection and set axis bounds for locators
1395
+ # WARNING: Unlike cartopy projections basemaps cannot normally be reused.
1396
+ # To make syntax similar we make a copy.
1397
+ # WARNING: Investigated whether Basemap.__init__() could be called
1398
+ # twice with updated proj kwargs to modify map bounds after creation
1399
+ # and python immmediately crashes. Do not try again.
1400
+ import mpl_toolkits.basemap # noqa: F401 verify package is available
1401
+
1402
+ self.projection = copy.copy(map_projection) # verify
1403
+ lon0 = self._get_lon0()
1404
+ if self.projection.projection in self._proj_polar:
1405
+ latmax = 80 # default latmax for gridlines
1406
+ extent = [-180 + lon0, 180 + lon0]
1407
+ bound = getattr(self.projection, "boundinglat", 0)
1408
+ north = self.projection.projection in self._proj_north
1409
+ extent.extend([bound, 90] if north else [-90, bound])
1410
+ else:
1411
+ latmax = 90
1412
+ attrs = ("lonmin", "lonmax", "latmin", "latmax")
1413
+ extent = [getattr(self.projection, attr, None) for attr in attrs]
1414
+ if any(_ is None for _ in extent):
1415
+ extent = [180 - lon0, 180 + lon0, -90, 90] # fallback
1416
+
1417
+ # Initialize axes
1418
+ self._map_boundary = None # see format()
1419
+ self._has_recurred = False # use this to override plotting methods
1420
+ self._lonlines_major = None # store gridliner objects this way
1421
+ self._lonlines_minor = None
1422
+ self._latlines_major = None
1423
+ self._latlines_minor = None
1424
+ self._lonarray = 4 * [False] # cached label toggles
1425
+ self._latarray = 4 * [False] # cached label toggles
1426
+ self._lonaxis = _LonAxis(self)
1427
+ self._lataxis = _LatAxis(self, latmax=latmax)
1428
+ self._set_view_intervals(extent)
1429
+ super().__init__(*args, **kwargs)
1430
+
1431
+ def _get_lon0(self):
1432
+ """
1433
+ Get the central longitude.
1434
+ """
1435
+ return getattr(self.projection, "projparams", {}).get("lon_0", 0)
1436
+
1437
+ @staticmethod
1438
+ def _iter_gridlines(dict_):
1439
+ """
1440
+ Iterate over longitude latitude lines.
1441
+ """
1442
+ dict_ = dict_ or {}
1443
+ for pi in dict_.values():
1444
+ for pj in pi:
1445
+ for obj in pj:
1446
+ yield obj
1447
+
1448
+ def _update_background(self, **kwargs):
1449
+ """
1450
+ Update the map boundary patches. This is called in `Axes.format`.
1451
+ """
1452
+ # Non-rectangular projections
1453
+ # WARNING: Map boundary must be drawn before all other tasks. See __init__.
1454
+ # WARNING: With clipping on boundary lines are clipped by the axes bbox.
1455
+ if self.projection.projection in self._proj_non_rectangular:
1456
+ self.patch.set_facecolor("none") # make sure main patch is hidden
1457
+ kw_face, kw_edge = rc._get_background_props(native=False, **kwargs)
1458
+ kw = {**kw_face, **kw_edge, "rasterized": False, "clip_on": False}
1459
+ self._map_boundary.update(kw)
1460
+ # Rectangular projections
1461
+ else:
1462
+ kw_face, kw_edge = rc._get_background_props(native=False, **kwargs)
1463
+ self.patch.update({**kw_face, "edgecolor": "none"})
1464
+ for spine in self.spines.values():
1465
+ spine.update(kw_edge)
1466
+
1467
+ def _update_boundary(self, round=None):
1468
+ """
1469
+ No-op. Boundary mode cannot be changed in basemap.
1470
+ """
1471
+ # NOTE: Unlike the cartopy method we do not look up the rc setting here.
1472
+ if round is None:
1473
+ return
1474
+ else:
1475
+ warnings._warn_ultraplot(
1476
+ f"Got round={round!r}, but you cannot change the bounds of a polar "
1477
+ "basemap projection after creating it. Please pass 'round' to pplt.Proj " # noqa: E501
1478
+ "instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw')."
1479
+ )
1480
+
1481
+ def _update_extent_mode(self, extent=None, boundinglat=None): # noqa: U100
1482
+ """
1483
+ No-op. Extent mode cannot be changed in basemap.
1484
+ """
1485
+ # NOTE: Unlike the cartopy method we do not look up the rc setting here.
1486
+ if extent is None:
1487
+ return
1488
+ if extent not in ("globe", "auto"):
1489
+ raise ValueError(
1490
+ f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'."
1491
+ )
1492
+ if extent == "auto":
1493
+ warnings._warn_ultraplot(
1494
+ f"Got extent={extent!r}, but you cannot use auto extent mode "
1495
+ "in basemap projections. Please consider switching to cartopy."
1496
+ )
1497
+
1498
+ def _update_extent(self, lonlim=None, latlim=None, boundinglat=None):
1499
+ """
1500
+ No-op. Map bounds cannot be changed in basemap.
1501
+ """
1502
+ lonlim = _not_none(lonlim, (None, None))
1503
+ latlim = _not_none(latlim, (None, None))
1504
+ if boundinglat is not None or any(_ is not None for _ in (*lonlim, *latlim)):
1505
+ warnings._warn_ultraplot(
1506
+ f"Got lonlim={lonlim!r}, latlim={latlim!r}, boundinglat={boundinglat!r}"
1507
+ ', but you cannot "zoom into" a basemap projection after creating it. '
1508
+ "Please pass any of the following keyword arguments to pplt.Proj "
1509
+ "instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw'):"
1510
+ "'boundinglat', 'lonlim', 'latlim', 'llcrnrlon', 'llcrnrlat', "
1511
+ "'urcrnrlon', 'urcrnrlat', 'llcrnrx', 'llcrnry', 'urcrnrx', 'urcrnry', "
1512
+ "'width', or 'height'."
1513
+ )
1514
+
1515
+ def _update_features(self):
1516
+ """
1517
+ Update geographic features.
1518
+ """
1519
+ # NOTE: Also notable are drawcounties, blumarble, drawlsmask,
1520
+ # shadedrelief, and etopo methods.
1521
+ for name, method in constructor.FEATURES_BASEMAP.items():
1522
+ # Draw feature or toggle on and off
1523
+ b = rc.find(name, context=True)
1524
+ attr = f"_{name}_feature"
1525
+ feat = getattr(self, attr, None)
1526
+ drawn = feat is not None # if exists, apply *updated* settings
1527
+ if b is not None:
1528
+ if not b:
1529
+ if drawn: # toggle existing feature off
1530
+ for obj in feat:
1531
+ feat.set_visible(False)
1532
+ else:
1533
+ if not drawn:
1534
+ feat = getattr(self.projection, method)(ax=self)
1535
+ if not isinstance(feat, (list, tuple)): # list of artists?
1536
+ feat = (feat,)
1537
+ setattr(self, attr, feat)
1538
+
1539
+ # Update settings
1540
+ if feat is not None:
1541
+ kw = rc.category(name, context=drawn)
1542
+ for obj in feat:
1543
+ obj.update(kw)
1544
+
1545
+ def _update_gridlines(
1546
+ self,
1547
+ which="major",
1548
+ longrid=None,
1549
+ latgrid=None,
1550
+ lonarray=None,
1551
+ latarray=None,
1552
+ ):
1553
+ """
1554
+ Apply changes to the basemap axes.
1555
+ """
1556
+ latmax = self._lataxis.get_latmax()
1557
+ for axis, name, grid, array, method in zip(
1558
+ ("x", "y"),
1559
+ ("lon", "lat"),
1560
+ (longrid, latgrid),
1561
+ (lonarray, latarray),
1562
+ ("drawmeridians", "drawparallels"),
1563
+ ):
1564
+ # Correct lonarray and latarray label toggles by changing from lrbt to lrtb.
1565
+ # Then update the cahced toggle array. This lets us change gridline locs
1566
+ # while preserving the label toggle setting from a previous format() call.
1567
+ grid = rc._get_gridline_bool(grid, axis=axis, which=which, native=False)
1568
+ axis = getattr(self, f"_{name}axis")
1569
+ if len(array) == 5: # should be always
1570
+ array = array[:4]
1571
+ bools = 4 * [False] if which == "major" else getattr(self, f"_{name}array")
1572
+ array = [*array[:2], *array[2:4][::-1]] # flip lrbt to lrtb and skip geo
1573
+ for i, b in enumerate(array):
1574
+ if b is not None:
1575
+ bools[i] = b # update toggles
1576
+
1577
+ # Get gridlines
1578
+ # NOTE: This may re-apply existing gridlines.
1579
+ lines = list(getattr(self, f"_get_{name}ticklocs")(which=which))
1580
+ if name == "lon" and np.isclose(lines[0] + 360, lines[-1]):
1581
+ lines = lines[:-1] # prevent double labels
1582
+
1583
+ # Figure out whether we have to redraw meridians/parallels
1584
+ # NOTE: Always update minor gridlines if major locator also changed
1585
+ attr = f"_{name}lines_{which}"
1586
+ objs = getattr(self, attr) # dictionary of previous objects
1587
+ attrs = ["isDefault_majloc"] # always check this one
1588
+ attrs.append("isDefault_majfmt" if which == "major" else "isDefault_minloc")
1589
+ rebuild = lines and (
1590
+ not objs
1591
+ or any(_ is not None for _ in array) # user-input or initial toggles
1592
+ or any(not getattr(axis, attr) for attr in attrs) # none tracked yet
1593
+ )
1594
+ if rebuild and objs and grid is None: # get *previous* toggle state
1595
+ grid = all(obj.get_visible() for obj in self._iter_gridlines(objs))
1596
+
1597
+ # Draw or redraw meridian or parallel lines
1598
+ # Also mark formatters and locators as 'default'
1599
+ if rebuild:
1600
+ kwdraw = {}
1601
+ formatter = axis.get_major_formatter()
1602
+ if formatter is not None: # use functional formatter
1603
+ kwdraw["fmt"] = formatter
1604
+ for obj in self._iter_gridlines(objs):
1605
+ obj.set_visible(False)
1606
+ objs = getattr(self.projection, method)(
1607
+ lines, ax=self, latmax=latmax, labels=bools, **kwdraw
1608
+ )
1609
+ setattr(self, attr, objs)
1610
+
1611
+ # Update gridline settings
1612
+ # We use native matplotlib 'grid' rc param for geographic gridlines
1613
+ kwlines = rc._get_gridline_props(which=which, native=False, rebuild=rebuild)
1614
+ kwtext = rc._get_ticklabel_props(native=False, rebuild=rebuild)
1615
+ for obj in self._iter_gridlines(objs):
1616
+ if isinstance(obj, mtext.Text):
1617
+ obj.update(kwtext)
1618
+ else:
1619
+ obj.update(kwlines)
1620
+
1621
+ # Toggle existing gridlines on and off
1622
+ if grid is not None:
1623
+ for obj in self._iter_gridlines(objs):
1624
+ obj.set_visible(grid)
1625
+
1626
+ def _update_major_gridlines(
1627
+ self,
1628
+ longrid=None,
1629
+ latgrid=None,
1630
+ lonarray=None,
1631
+ latarray=None,
1632
+ loninline=None,
1633
+ latinline=None,
1634
+ rotatelabels=None,
1635
+ labelpad=None,
1636
+ nsteps=None,
1637
+ ):
1638
+ """
1639
+ Update major gridlines.
1640
+ """
1641
+ loninline, latinline, labelpad, rotatelabels, nsteps # avoid U100 error
1642
+ self._update_gridlines(
1643
+ which="major",
1644
+ longrid=longrid,
1645
+ latgrid=latgrid,
1646
+ lonarray=lonarray,
1647
+ latarray=latarray,
1648
+ )
1649
+
1650
+ def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None):
1651
+ """
1652
+ Update minor gridlines.
1653
+ """
1654
+ # Update gridline locations
1655
+ nsteps # avoid U100 error
1656
+ array = [None] * 4 # NOTE: must be None not False (see _update_gridlines)
1657
+ self._update_gridlines(
1658
+ which="minor",
1659
+ longrid=longrid,
1660
+ latgrid=latgrid,
1661
+ lonarray=array,
1662
+ latarray=array,
1663
+ )
1664
+ # Set isDefault_majloc, etc. to True for both axes
1665
+ # NOTE: This cannot be done inside _update_gridlines or minor gridlines
1666
+ # will not update to reflect new major gridline locations.
1667
+ for axis in (self._lonaxis, self._lataxis):
1668
+ axis.isDefault_majfmt = True
1669
+ axis.isDefault_majloc = True
1670
+ axis.isDefault_minloc = True
1671
+
1672
+
1673
+ # Apply signature obfuscation after storing previous signature
1674
+ GeoAxes._format_signatures[GeoAxes] = inspect.signature(GeoAxes.format)
1675
+ GeoAxes.format = docstring._obfuscate_kwargs(GeoAxes.format)