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/base.py ADDED
@@ -0,0 +1,3240 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ The first-level axes subclass used for all ultraplot figures.
4
+ Implements basic shared functionality.
5
+ """
6
+ import copy
7
+ import inspect
8
+ import re
9
+ from numbers import Integral
10
+
11
+ import matplotlib.axes as maxes
12
+ import matplotlib.axis as maxis
13
+ import matplotlib.cm as mcm
14
+ import matplotlib.colors as mcolors
15
+ import matplotlib.container as mcontainer
16
+ import matplotlib.contour as mcontour
17
+ import matplotlib.legend as mlegend
18
+ import matplotlib.offsetbox as moffsetbox
19
+ import matplotlib.patches as mpatches
20
+ import matplotlib.projections as mproj
21
+ import matplotlib.text as mtext
22
+ import matplotlib.ticker as mticker
23
+ import matplotlib.transforms as mtransforms
24
+ import numpy as np
25
+ from matplotlib import cbook
26
+
27
+ from .. import colors as pcolors
28
+ from .. import constructor
29
+ from .. import ticker as pticker
30
+ from ..config import rc
31
+ from ..internals import ic # noqa: F401
32
+ from ..internals import (
33
+ _kwargs_to_args,
34
+ _not_none,
35
+ _pop_kwargs,
36
+ _pop_params,
37
+ _pop_props,
38
+ _pop_rc,
39
+ _translate_loc,
40
+ _version_mpl,
41
+ docstring,
42
+ guides,
43
+ labels,
44
+ rcsetup,
45
+ warnings,
46
+ )
47
+ from ..utils import _fontsize_to_pt, edges, units
48
+
49
+ try:
50
+ from cartopy.crs import CRS, PlateCarree
51
+ except Exception:
52
+ CRS = PlateCarree = object
53
+
54
+ __all__ = ["Axes"]
55
+
56
+
57
+ # A-b-c label string
58
+ ABC_STRING = "abcdefghijklmnopqrstuvwxyz"
59
+
60
+ # Legend align options
61
+ ALIGN_OPTS = {
62
+ None: {
63
+ "center": "center",
64
+ "left": "center left",
65
+ "right": "center right",
66
+ "top": "upper center",
67
+ "bottom": "lower center",
68
+ },
69
+ "left": {
70
+ "top": "upper right",
71
+ "center": "center right",
72
+ "bottom": "lower right",
73
+ },
74
+ "right": {
75
+ "top": "upper left",
76
+ "center": "center left",
77
+ "bottom": "lower left",
78
+ },
79
+ "top": {"left": "lower left", "center": "lower center", "right": "lower right"},
80
+ "bottom": {"left": "upper left", "center": "upper center", "right": "upper right"},
81
+ }
82
+
83
+
84
+ # Projection docstring
85
+ _proj_docstring = """
86
+ proj, projection : \
87
+ str, `cartopy.crs.Projection`, or `~mpl_toolkits.basemap.Basemap`, optional
88
+ The map projection specification(s). If ``'cart'`` or ``'cartesian'``
89
+ (the default), a `~ultraplot.axes.CartesianAxes` is created. If ``'polar'``,
90
+ a `~ultraplot.axes.PolarAxes` is created. Otherwise, the argument is
91
+ interpreted by `~ultraplot.constructor.Proj`, and the result is used
92
+ to make a `~ultraplot.axes.GeoAxes` (in this case the argument can be
93
+ a `cartopy.crs.Projection` instance, a `~mpl_toolkits.basemap.Basemap`
94
+ instance, or a projection name listed in :ref:`this table <proj_table>`).
95
+ """
96
+ _proj_kw_docstring = """
97
+ proj_kw, projection_kw : dict-like, optional
98
+ Keyword arguments passed to `~mpl_toolkits.basemap.Basemap` or
99
+ cartopy `~cartopy.crs.Projection` classes on instantiation.
100
+ """
101
+ _backend_docstring = """
102
+ backend : {'cartopy', 'basemap'}, default: :rc:`geo.backend`
103
+ Whether to use `~mpl_toolkits.basemap.Basemap` or
104
+ `~cartopy.crs.Projection` for map projections.
105
+ """
106
+ docstring._snippet_manager["axes.proj"] = _proj_docstring
107
+ docstring._snippet_manager["axes.proj_kw"] = _proj_kw_docstring
108
+ docstring._snippet_manager["axes.backend"] = _backend_docstring
109
+
110
+
111
+ # Colorbar and legend space
112
+ _space_docstring = """
113
+ queue : bool, optional
114
+ If ``True`` and `loc` is the same as an existing {name}, the input
115
+ arguments are added to a queue and this function returns ``None``.
116
+ This is used to "update" the same {name} with successive ``ax.{name}(...)``
117
+ calls. If ``False`` (the default) and `loc` is the same as an existing
118
+ *inset* {name}, the old {name} is removed. If ``False`` and `loc` is an
119
+ *outer* {name}, the {name}s are "stacked".
120
+ space : unit-spec, default: None
121
+ For outer {name}s only. The fixed space between the {name} and the subplot
122
+ edge. %(units.em)s
123
+ When the :ref:`tight layout algorithm <ug_tight>` is active for the figure,
124
+ `space` is computed automatically (see `pad`). Otherwise, `space` is set to
125
+ a suitable default.
126
+ pad : unit-spec, default: :rc:`subplots.panelpad` or :rc:`{default}`
127
+ For outer {name}s, this is the :ref:`tight layout padding <ug_tight>`
128
+ between the {name} and the subplot (default is :rcraw:`subplots.panelpad`).
129
+ For inset {name}s, this is the fixed space between the axes
130
+ edge and the {name} (default is :rcraw:`{default}`).
131
+ %(units.em)s
132
+ align : {{'center', 'top', 'bottom', 'left', 'right', 't', 'b', 'l', 'r'}}, optional
133
+ For outer {name}s only. How to align the {name} against the subplot edge.
134
+ The values ``'top'`` and ``'bottom'`` are valid for left and right {name}s
135
+ and ``'left'`` and ``'right'`` are valid for top and bottom {name}s.
136
+ The default is always ``'center'``.
137
+ """
138
+ docstring._snippet_manager["axes.legend_space"] = _space_docstring.format(
139
+ name="legend", default="legend.borderaxespad"
140
+ )
141
+ docstring._snippet_manager["axes.colorbar_space"] = _space_docstring.format(
142
+ name="colorbar", default="colorbar.insetpad"
143
+ )
144
+
145
+
146
+ # Transform docstring
147
+ # Used for text and add_axes
148
+ _transform_docstring = """
149
+ transform : {'data', 'axes', 'figure', 'subfigure'} \
150
+ or `~matplotlib.transforms.Transform`, optional
151
+ The transform used to interpret the bounds. Can be a
152
+ `~matplotlib.transforms.Transform` instance or a string representing
153
+ the `~matplotlib.axes.Axes.transData`, `~matplotlib.axes.Axes.transAxes`,
154
+ `~matplotlib.figure.Figure.transFigure`, or
155
+ `~matplotlib.figure.Figure.transSubfigure`, transforms.
156
+ """
157
+ docstring._snippet_manager["axes.transform"] = _transform_docstring
158
+
159
+
160
+ # Inset docstring
161
+ # NOTE: Used by SubplotGrid.inset_axes
162
+ _inset_docstring = """
163
+ Add an inset axes.
164
+ This is similar to `matplotlib.axes.Axes.inset_axes`.
165
+
166
+ Parameters
167
+ ----------
168
+ bounds : 4-tuple of float
169
+ The (left, bottom, width, height) coordinates for the axes.
170
+ %(axes.transform)s
171
+ Default is to use the same projection as the current axes.
172
+ %(axes.proj)s
173
+ %(axes.proj_kw)s
174
+ %(axes.backend)s
175
+ zorder : float, default: 4
176
+ The `zorder <https://matplotlib.org/stable/gallery/misc/zorder_demo.html>`__
177
+ of the axes. Should be greater than the zorder of elements in the parent axes.
178
+ zoom : bool, default: True or False
179
+ Whether to draw lines indicating the inset zoom using `~Axes.indicate_inset_zoom`.
180
+ The line positions will automatically adjust when the parent or inset axes limits
181
+ change. Default is ``True`` only if both axes are `~ultraplot.axes.CartesianAxes`.
182
+ zoom_kw : dict, optional
183
+ Passed to `~Axes.indicate_inset_zoom`.
184
+
185
+ Other parameters
186
+ ----------------
187
+ **kwargs
188
+ Passed to `ultraplot.axes.Axes`.
189
+
190
+ Returns
191
+ -------
192
+ ultraplot.axes.Axes
193
+ The inset axes.
194
+
195
+ See also
196
+ --------
197
+ Axes.indicate_inset_zoom
198
+ matplotlib.axes.Axes.inset_axes
199
+ matplotlib.axes.Axes.indicate_inset
200
+ matplotlib.axes.Axes.indicate_inset_zoom
201
+ """
202
+ _indicate_inset_docstring = """
203
+ Add indicators denoting the zoom range of the inset axes.
204
+ This will replace previously drawn zoom indicators.
205
+
206
+ Parameters
207
+ ----------
208
+ %(artist.patch)s
209
+ zorder : float, default: 3.5
210
+ The `zorder <https://matplotlib.org/stable/gallery/misc/zorder_demo.html>`__ of
211
+ the indicators. Should be greater than the zorder of elements in the parent axes.
212
+
213
+ Other parameters
214
+ ----------------
215
+ **kwargs
216
+ Passed to `~matplotlib.patches.Patch`.
217
+
218
+ Note
219
+ ----
220
+ This command must be called from the inset axes rather than the parent axes.
221
+ It is called automatically when ``zoom=True`` is passed to `~Axes.inset_axes`
222
+ and whenever the axes are drawn (so the line positions always track the axis
223
+ limits even if they are later changed).
224
+
225
+ See also
226
+ --------
227
+ matplotlib.axes.Axes.indicate_inset
228
+ matplotlib.axes.Axes.indicate_inset_zoom
229
+ """
230
+ docstring._snippet_manager["axes.inset"] = _inset_docstring
231
+ docstring._snippet_manager["axes.indicate_inset"] = _indicate_inset_docstring
232
+
233
+
234
+ # Panel docstring
235
+ # NOTE: Used by SubplotGrid.panel_axes
236
+ _panel_loc_docstring = """
237
+ ========== =====================
238
+ Location Valid keys
239
+ ========== =====================
240
+ left ``'left'``, ``'l'``
241
+ right ``'right'``, ``'r'``
242
+ bottom ``'bottom'``, ``'b'``
243
+ top ``'top'``, ``'t'``
244
+ ========== =====================
245
+ """
246
+ _panel_docstring = """
247
+ Add a panel axes.
248
+
249
+ Parameters
250
+ ----------
251
+ side : str, optional
252
+ The panel location. Valid location keys are as follows.
253
+
254
+ %(axes.panel_loc)s
255
+
256
+ width : unit-spec, default: :rc:`subplots.panelwidth`
257
+ The panel width.
258
+ %(units.in)s
259
+ space : unit-spec, default: None
260
+ The fixed space between the panel and the subplot edge.
261
+ %(units.em)s
262
+ When the :ref:`tight layout algorithm <ug_tight>` is active for the figure,
263
+ `space` is computed automatically (see `pad`). Otherwise, `space` is set to
264
+ a suitable default.
265
+ pad : unit-spec, default: :rc:`subplots.panelpad`
266
+ The :ref:`tight layout padding <ug_tight>` between the panel and the subplot.
267
+ %(units.em)s
268
+ share : bool, default: True
269
+ Whether to enable axis sharing between the *x* and *y* axes of the
270
+ main subplot and the panel long axes for each panel in the "stack".
271
+ Sharing between the panel short axis and other panel short axes
272
+ is determined by figure-wide `sharex` and `sharey` settings.
273
+
274
+ Other parameters
275
+ ----------------
276
+ **kwargs
277
+ Passed to `ultraplot.axes.CartesianAxes`. Supports all valid
278
+ `~ultraplot.axes.CartesianAxes.format` keywords.
279
+
280
+ Returns
281
+ -------
282
+ ultraplot.axes.CartesianAxes
283
+ The panel axes.
284
+ """
285
+ docstring._snippet_manager["axes.panel_loc"] = _panel_loc_docstring
286
+ docstring._snippet_manager["axes.panel"] = _panel_docstring
287
+
288
+
289
+ # Format docstrings
290
+ _axes_format_docstring = """
291
+ title : str or sequence, optional
292
+ The axes title. Can optionally be a sequence strings, in which case
293
+ the title will be selected from the sequence according to `~Axes.number`.
294
+ abc : bool or str or sequence, default: :rc:`abc`
295
+ The "a-b-c" subplot label style. Must contain the character ``a`` or ``A``,
296
+ for example ``'a.'``, or ``'A'``. If ``True`` then the default style of
297
+ ``'a'`` is used. The ``a`` or ``A`` is replaced with the alphabetic character
298
+ matching the `~Axes.number`. If `~Axes.number` is greater than 26, the
299
+ characters loop around to a, ..., z, aa, ..., zz, aaa, ..., zzz, etc.
300
+ Can also be a sequence of strings, in which case the "a-b-c" label
301
+ will simply be selected from the sequence according to `~Axes.number`.
302
+ abcloc, titleloc : str, default: :rc:`abc.loc`, :rc:`title.loc`
303
+ Strings indicating the location for the a-b-c label and main title.
304
+ The following locations are valid:
305
+
306
+ .. _title_table:
307
+
308
+ ======================== ============================
309
+ Location Valid keys
310
+ ======================== ============================
311
+ center above axes ``'center'``, ``'c'``
312
+ left above axes ``'left'``, ``'l'``
313
+ right above axes ``'right'``, ``'r'``
314
+ lower center inside axes ``'lower center'``, ``'lc'``
315
+ upper center inside axes ``'upper center'``, ``'uc'``
316
+ upper right inside axes ``'upper right'``, ``'ur'``
317
+ upper left inside axes ``'upper left'``, ``'ul'``
318
+ lower left inside axes ``'lower left'``, ``'ll'``
319
+ lower right inside axes ``'lower right'``, ``'lr'``
320
+ ======================== ============================
321
+
322
+ abcborder, titleborder : bool, default: :rc:`abc.border` and :rc:`title.border`
323
+ Whether to draw a white border around titles and a-b-c labels positioned
324
+ inside the axes. This can help them stand out on top of artists
325
+ plotted inside the axes.
326
+ abcbbox, titlebbox : bool, default: :rc:`abc.bbox` and :rc:`title.bbox`
327
+ Whether to draw a white bbox around titles and a-b-c labels positioned
328
+ inside the axes. This can help them stand out on top of artists plotted
329
+ inside the axes.
330
+ abc_kw, title_kw : dict-like, optional
331
+ Additional settings used to update the a-b-c label and title
332
+ with ``text.update()``.
333
+ titlepad : float, default: :rc:`title.pad`
334
+ The padding for the inner and outer titles and a-b-c labels.
335
+ %(units.pt)s
336
+ titleabove : bool, default: :rc:`title.above`
337
+ Whether to try to put outer titles and a-b-c labels above panels,
338
+ colorbars, or legends that are above the axes.
339
+ abctitlepad : float, default: :rc:`abc.titlepad`
340
+ The horizontal padding between a-b-c labels and titles in the same location.
341
+ %(units.pt)s
342
+ ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle \
343
+ : str or sequence, optional
344
+ Shorthands for the below keywords.
345
+ lefttitle, centertitle, righttitle, upperlefttitle, uppercentertitle, upperrighttitle, \
346
+ lowerlefttitle, lowercentertitle, lowerrighttitle : str or sequence, optional
347
+ Additional titles in specific positions (see `title` for details). This works as
348
+ an alternative to the ``ax.format(title='Title', titleloc=loc)`` workflow and
349
+ permits adding more than one title-like label for a single axes.
350
+ a, alpha, fc, facecolor, ec, edgecolor, lw, linewidth, ls, linestyle : default: \
351
+ :rc:`axes.alpha`, :rc:`axes.facecolor`, :rc:`axes.edgecolor`, :rc:`axes.linewidth`, '-'
352
+ Additional settings applied to the background patch, and their
353
+ shorthands. Their defaults values are the ``'axes'`` properties.
354
+ """
355
+ _figure_format_docstring = """
356
+ rowlabels, collabels, llabels, tlabels, rlabels, blabels
357
+ Aliases for `leftlabels` and `toplabels`, and for `leftlabels`,
358
+ `toplabels`, `rightlabels`, and `bottomlabels`, respectively.
359
+ leftlabels, toplabels, rightlabels, bottomlabels : sequence of str, optional
360
+ Labels for the subplots lying along the left, top, right, and
361
+ bottom edges of the figure. The length of each list must match
362
+ the number of subplots along the corresponding edge.
363
+ leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float or unit-spec, default\
364
+ : :rc:`leftlabel.pad`, :rc:`toplabel.pad`, :rc:`rightlabel.pad`, :rc:`bottomlabel.pad`
365
+ The padding between the labels and the axes content.
366
+ %(units.pt)s
367
+ leftlabels_kw, toplabels_kw, rightlabels_kw, bottomlabels_kw : dict-like, optional
368
+ Additional settings used to update the labels with ``text.update()``.
369
+ figtitle
370
+ Alias for `suptitle`.
371
+ suptitle : str, optional
372
+ The figure "super" title, centered between the left edge of the leftmost
373
+ subplot and the right edge of the rightmost subplot.
374
+ suptitlepad : float, default: :rc:`suptitle.pad`
375
+ The padding between the super title and the axes content.
376
+ %(units.pt)s
377
+ suptitle_kw : optional
378
+ Additional settings used to update the super title with ``text.update()``.
379
+ includepanels : bool, default: False
380
+ Whether to include panels when aligning figure "super titles" along the top
381
+ of the subplot grid and when aligning the `spanx` *x* axis labels and
382
+ `spany` *y* axis labels along the sides of the subplot grid.
383
+ """
384
+ _rc_init_docstring = """
385
+ """
386
+ _rc_format_docstring = """
387
+ rc_mode : int, optional
388
+ The context mode passed to `~ultraplot.config.Configurator.context`.
389
+ rc_kw : dict-like, optional
390
+ An alternative to passing extra keyword arguments. See below.
391
+ **kwargs
392
+ {}Keyword arguments that match the name of an `~ultraplot.config.rc` setting are
393
+ passed to `ultraplot.config.Configurator.context` and used to update the axes.
394
+ If the setting name has "dots" you can simply omit the dots. For example,
395
+ ``abc='A.'`` modifies the :rcraw:`abc` setting, ``titleloc='left'`` modifies the
396
+ :rcraw:`title.loc` setting, ``gridminor=True`` modifies the :rcraw:`gridminor`
397
+ setting, and ``gridbelow=True`` modifies the :rcraw:`grid.below` setting. Many
398
+ of the keyword arguments documented above are internally applied by retrieving
399
+ settings passed to `~ultraplot.config.Configurator.context`.
400
+ """
401
+ docstring._snippet_manager["rc.init"] = _rc_format_docstring.format(
402
+ "Remaining keyword arguments are passed to `matplotlib.axes.Axes`.\n "
403
+ )
404
+ docstring._snippet_manager["rc.format"] = _rc_format_docstring.format("")
405
+ docstring._snippet_manager["axes.format"] = _axes_format_docstring
406
+ docstring._snippet_manager["figure.format"] = _figure_format_docstring
407
+
408
+
409
+ # Colorbar docstrings
410
+ _colorbar_args_docstring = """
411
+ mappable : mappable, colormap-spec, sequence of color-spec, \
412
+ or sequence of `~matplotlib.artist.Artist`
413
+ There are four options here:
414
+
415
+ 1. A `~matplotlib.cm.ScalarMappable` (e.g., an object returned by
416
+ `~ultraplot.axes.PlotAxes.contourf` or `~ultraplot.axes.PlotAxes.pcolormesh`).
417
+ 2. A `~matplotlib.colors.Colormap` or registered colormap name used to build a
418
+ `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar range and ticks depend
419
+ on the arguments `values`, `vmin`, `vmax`, and `norm`. The default for a
420
+ `~ultraplot.colors.ContinuousColormap` is ``vmin=0`` and ``vmax=1`` (note that
421
+ passing `values` will "discretize" the colormap). The default for a
422
+ `~ultraplot.colors.DiscreteColormap` is ``values=np.arange(0, cmap.N)``.
423
+ 3. A sequence of hex strings, color names, or RGB[A] tuples. A
424
+ `~ultraplot.colors.DiscreteColormap` will be generated from these colors and
425
+ used to build a `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar
426
+ range and ticks depend on the arguments `values`, `norm`, and
427
+ `norm_kw`. The default is ``values=np.arange(0, len(mappable))``.
428
+ 4. A sequence of `matplotlib.artist.Artist` instances (e.g., a list of
429
+ `~matplotlib.lines.Line2D` instances returned by `~ultraplot.axes.PlotAxes.plot`).
430
+ A colormap will be generated from the colors of these objects (where the
431
+ color is determined by ``get_color``, if available, or ``get_facecolor``).
432
+ The colorbar range and ticks depend on the arguments `values`, `norm`, and
433
+ `norm_kw`. The default is to infer colorbar ticks and tick labels
434
+ by calling `~matplotlib.artist.Artist.get_label` on each artist.
435
+
436
+ values : sequence of float or str, optional
437
+ Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This maps the colormap
438
+ colors to numeric values using `~ultraplot.colors.DiscreteNorm`. If the colormap is
439
+ a `~ultraplot.colors.ContinuousColormap` then its colors will be "discretized".
440
+ These These can also be strings, in which case the list indices are used for
441
+ tick locations and the strings are applied as tick labels.
442
+ """
443
+ _colorbar_kwargs_docstring = """
444
+ orientation : {None, 'horizontal', 'vertical'}, optional
445
+ The colorbar orientation. By default this depends on the "side" of the subplot
446
+ or figure where the colorbar is drawn. Inset colorbars are always horizontal.
447
+ norm : norm-spec, optional
448
+ Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This is the continuous
449
+ normalizer used to scale the `~ultraplot.colors.ContinuousColormap` (or passed
450
+ to `~ultraplot.colors.DiscreteNorm` if `values` was passed). Passed to the
451
+ `~ultraplot.constructor.Norm` constructor function.
452
+ norm_kw : dict-like, optional
453
+ Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. These are the
454
+ normalizer keyword arguments. Passed to `~ultraplot.constructor.Norm`.
455
+ vmin, vmax : float, optional
456
+ Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. These are the minimum
457
+ and maximum colorbar values. Passed to `~ultraplot.constructor.Norm`.
458
+ label, title : str, optional
459
+ The colorbar label. The `title` keyword is also accepted for
460
+ consistency with `~matplotlib.axes.Axes.legend`.
461
+ reverse : bool, optional
462
+ Whether to reverse the direction of the colorbar. This is done automatically
463
+ when descending levels are used with `~ultraplot.colors.DiscreteNorm`.
464
+ rotation : float, default: 0
465
+ The tick label rotation.
466
+ grid, edges, drawedges : bool, default: :rc:`colorbar.grid`
467
+ Whether to draw "grid" dividers between each distinct color.
468
+ extend : {'neither', 'both', 'min', 'max'}, optional
469
+ Direction for drawing colorbar "extensions" (i.e. color keys for out-of-bounds
470
+ data on the end of the colorbar). Default behavior is to use the value of `extend`
471
+ passed to the plotting command or use ``'neither'`` if the value is unknown.
472
+ extendfrac : float, optional
473
+ The length of the colorbar "extensions" relative to the length of the colorbar.
474
+ This is a native matplotlib `~matplotlib.figure.Figure.colorbar` keyword.
475
+ extendsize : unit-spec, default: :rc:`colorbar.extend` or :rc:`colorbar.insetextend`
476
+ The length of the colorbar "extensions" in physical units. Default is
477
+ :rcraw:`colorbar.extend` for outer colorbars and :rcraw:`colorbar.insetextend`
478
+ for inset colorbars. %(units.em)s
479
+ extendrect : bool, default: False
480
+ Whether to draw colorbar "extensions" as rectangles. If ``False`` then
481
+ the extensions are drawn as triangles.
482
+ locator, ticks : locator-spec, optional
483
+ Used to determine the colorbar tick positions. Passed to the
484
+ `~ultraplot.constructor.Locator` constructor function. By default
485
+ `~matplotlib.ticker.AutoLocator` is used for continuous color levels
486
+ and `~ultraplot.ticker.DiscreteLocator` is used for discrete color levels.
487
+ locator_kw : dict-like, optional
488
+ Keyword arguments passed to `matplotlib.ticker.Locator` class.
489
+ minorlocator, minorticks
490
+ As with `locator`, `ticks` but for the minor ticks. By default
491
+ `~matplotlib.ticker.AutoMinorLocator` is used for continuous color levels
492
+ and `~ultraplot.ticker.DiscreteLocator` is used for discrete color levels.
493
+ minorlocator_kw
494
+ As with `locator_kw`, but for the minor ticks.
495
+ format, formatter, ticklabels : formatter-spec, optional
496
+ The tick label format. Passed to the `~ultraplot.constructor.Formatter`
497
+ constructor function.
498
+ formatter_kw : dict-like, optional
499
+ Keyword arguments passed to `matplotlib.ticker.Formatter` class.
500
+ frame, frameon : bool, default: :rc:`colorbar.frameon`
501
+ For inset colorbars only. Indicates whether to draw a "frame",
502
+ just like `~matplotlib.axes.Axes.legend`.
503
+ tickminor : bool, optional
504
+ Whether to add minor ticks using `~matplotlib.colorbar.ColorbarBase.minorticks_on`.
505
+ tickloc, ticklocation : {'bottom', 'top', 'left', 'right'}, optional
506
+ Where to draw tick marks on the colorbar. Default is toward the outside
507
+ of the subplot for outer colorbars and ``'bottom'`` for inset colorbars.
508
+ tickdir, tickdirection : {'out', 'in', 'inout'}, default: :rc:`tick.dir`
509
+ Direction of major and minor colorbar ticks.
510
+ ticklen : unit-spec, default: :rc:`tick.len`
511
+ Major tick lengths for the colorbar ticks.
512
+ ticklenratio : float, default: :rc:`tick.lenratio`
513
+ Relative scaling of `ticklen` used to determine minor tick lengths.
514
+ tickwidth : unit-spec, default: `linewidth`
515
+ Major tick widths for the colorbar ticks.
516
+ or :rc:`tick.width` if `linewidth` was not passed.
517
+ tickwidthratio : float, default: :rc:`tick.widthratio`
518
+ Relative scaling of `tickwidth` used to determine minor tick widths.
519
+ ticklabelcolor, ticklabelsize, ticklabelweight \
520
+ : default: :rc:`tick.labelcolor`, :rc:`tick.labelsize`, :rc:`tick.labelweight`.
521
+ The font color, size, and weight for colorbar tick labels
522
+ labelloc, labellocation : {'bottom', 'top', 'left', 'right'}
523
+ The colorbar label location. Inherits from `tickloc` by default. Default is toward
524
+ the outside of the subplot for outer colorbars and ``'bottom'`` for inset colorbars.
525
+ labelcolor, labelsize, labelweight \
526
+ : default: :rc:`label.color`, :rc:`label.size`, and :rc:`label.weight`.
527
+ The font color, size, and weight for the colorbar label.
528
+ a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth : default\
529
+ : :rc:`colorbar.framealpha`, :rc:`colorbar.framecolor`
530
+ For inset colorbars only. Controls the transparency and color of
531
+ the background frame.
532
+ lw, linewidth, c, color : optional
533
+ Controls the line width and edge color for both the colorbar
534
+ outline and the level dividers.
535
+ %(axes.edgefix)s
536
+ rasterize : bool, default: :rc:`colorbar.rasterize`
537
+ Whether to rasterize the colorbar solids. The matplotlib default was ``True``
538
+ but ultraplot changes this to ``False`` since rasterization can cause misalignment
539
+ between the color patches and the colorbar outline.
540
+ **kwargs
541
+ Passed to `~matplotlib.figure.Figure.colorbar`.
542
+ """
543
+ _edgefix_docstring = """
544
+ edgefix : bool or float, default: :rc:`edgefix`
545
+ Whether to fix the common issue where white lines appear between adjacent
546
+ patches in saved vector graphics (this can slow down figure rendering).
547
+ See this `github repo <https://github.com/jklymak/contourfIssues>`__ for a
548
+ demonstration of the problem. If ``True``, a small default linewidth of
549
+ ``0.3`` is used to cover up the white lines. If float (e.g. ``edgefix=0.5``),
550
+ this specific linewidth is used to cover up the white lines. This feature is
551
+ automatically disabled when the patches have transparency.
552
+ """
553
+ docstring._snippet_manager["axes.edgefix"] = _edgefix_docstring
554
+ docstring._snippet_manager["axes.colorbar_args"] = _colorbar_args_docstring
555
+ docstring._snippet_manager["axes.colorbar_kwargs"] = _colorbar_kwargs_docstring
556
+
557
+
558
+ # Legend docstrings
559
+ _legend_args_docstring = """
560
+ handles : list of artist, optional
561
+ List of matplotlib artists, or a list of lists of artist instances (see the `center`
562
+ keyword). If not passed, artists with valid labels (applied by passing `label` or
563
+ `labels` to a plotting command or calling `~matplotlib.artist.Artist.set_label`)
564
+ are retrieved automatically. If the object is a `~matplotlib.contour.ContourSet`,
565
+ `~matplotlib.contour.ContourSet.legend_elements` is used to select the central
566
+ artist in the list (generally useful for single-color contour plots). Note that
567
+ ultraplot's `~ultraplot.axes.PlotAxes.contour` and `~ultraplot.axes.PlotAxes.contourf`
568
+ accept a legend `label` keyword argument.
569
+ labels : list of str, optional
570
+ A matching list of string labels or ``None`` placeholders, or a matching list of
571
+ lists (see the `center` keyword). Wherever ``None`` appears in the list (or
572
+ if no labels were passed at all), labels are retrieved by calling
573
+ `~matplotlib.artist.Artist.get_label` on each `~matplotlib.artist.Artist` in the
574
+ handle list. If a handle consists of a tuple group of artists, labels are inferred
575
+ from the artists in the tuple (if there are multiple unique labels in the tuple
576
+ group of artists, the tuple group is expanded into unique legend entries --
577
+ otherwise, the tuple group elements are drawn on top of eachother). For details
578
+ on matplotlib legend handlers and tuple groups, see the matplotlib `legend guide \
579
+ <https://matplotlib.org/stable/tutorials/intermediate/legend_guide.html>`__.
580
+ """
581
+ _legend_kwargs_docstring = """
582
+ frame, frameon : bool, optional
583
+ Toggles the legend frame. For centered-row legends, a frame
584
+ independent from matplotlib's built-in legend frame is created.
585
+ ncol, ncols : int, optional
586
+ The number of columns. `ncols` is an alias, added
587
+ for consistency with `~matplotlib.pyplot.subplots`.
588
+ order : {'C', 'F'}, optional
589
+ Whether legend handles are drawn in row-major (``'C'``) or column-major
590
+ (``'F'``) order. Analagous to `numpy.array` ordering. The matplotlib
591
+ default was ``'F'`` but ultraplot changes this to ``'C'``.
592
+ center : bool, optional
593
+ Whether to center each legend row individually. If ``True``, we draw
594
+ successive single-row legends "stacked" on top of each other. If ``None``,
595
+ we infer this setting from `handles`. By default, `center` is set to ``True``
596
+ if `handles` is a list of lists (each sublist is used as a row in the legend).
597
+ alphabetize : bool, default: False
598
+ Whether to alphabetize the legend entries according to
599
+ the legend labels.
600
+ title, label : str, optional
601
+ The legend title. The `label` keyword is also accepted, for consistency
602
+ with `~matplotlib.figure.Figure.colorbar`.
603
+ fontsize, fontweight, fontcolor : optional
604
+ The font size, weight, and color for the legend text. Font size is interpreted
605
+ by `~ultraplot.utils.units`. The default font size is :rcraw:`legend.fontsize`.
606
+ titlefontsize, titlefontweight, titlefontcolor : optional
607
+ The font size, weight, and color for the legend title. Font size is interpreted
608
+ by `~ultraplot.utils.units`. The default size is `fontsize`.
609
+ borderpad, borderaxespad, handlelength, handleheight, handletextpad, \
610
+ labelspacing, columnspacing : unit-spec, optional
611
+ Various matplotlib `~matplotlib.axes.Axes.legend` spacing arguments.
612
+ %(units.em)s
613
+ a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth \
614
+ : default: :rc:`legend.framealpha`, :rc:`legend.facecolor`, :rc:`legend.edgecolor`, \
615
+ :rc:`axes.linewidth`
616
+ The opacity, face color, edge color, and edge width for the legend frame.
617
+ c, color, lw, linewidth, m, marker, ls, linestyle, dashes, ms, markersize : optional
618
+ Properties used to override the legend handles. For example, for a
619
+ legend describing variations in line style ignoring variations
620
+ in color, you might want to use ``color='black'``.
621
+ handle_kw : dict-like, optional
622
+ Additional properties used to override legend handles, e.g.
623
+ ``handle_kw={'edgecolor': 'black'}``. Only line properties
624
+ can be passed as keyword arguments.
625
+ handler_map : dict-like, optional
626
+ A dictionary mapping instances or types to a legend handler.
627
+ This `handler_map` updates the default handler map found at
628
+ `matplotlib.legend.Legend.get_legend_handler_map`.
629
+ **kwargs
630
+ Passed to `~matplotlib.axes.Axes.legend`.
631
+ """
632
+ docstring._snippet_manager["axes.legend_args"] = _legend_args_docstring
633
+ docstring._snippet_manager["axes.legend_kwargs"] = _legend_kwargs_docstring
634
+
635
+
636
+ def _align_bbox(align, length):
637
+ """
638
+ Return a simple alignment bounding box for intersection calculations.
639
+ """
640
+ if align in ("left", "bottom"):
641
+ bounds = [[0, 0], [length, 0]]
642
+ elif align in ("top", "right"):
643
+ bounds = [[1 - length, 0], [1, 0]]
644
+ elif align == "center":
645
+ bounds = [[0.5 * (1 - length), 0], [0.5 * (1 + length), 0]]
646
+ else:
647
+ raise ValueError(f"Invalid align {align!r}.")
648
+ return mtransforms.Bbox(bounds)
649
+
650
+
651
+ class _TransformedBoundsLocator:
652
+ """
653
+ Axes locator for `~Axes.inset_axes` and other axes.
654
+ """
655
+
656
+ def __init__(self, bounds, transform):
657
+ self._bounds = bounds
658
+ self._transform = transform
659
+
660
+ def __call__(self, ax, renderer): # noqa: U100
661
+ transfig = getattr(ax.figure, "transSubfigure", ax.figure.transFigure)
662
+ bbox = mtransforms.Bbox.from_bounds(*self._bounds)
663
+ bbox = mtransforms.TransformedBbox(bbox, self._transform)
664
+ bbox = mtransforms.TransformedBbox(bbox, transfig.inverted())
665
+ return bbox
666
+
667
+
668
+ class Axes(maxes.Axes):
669
+ """
670
+ The lowest-level `~matplotlib.axes.Axes` subclass used by ultraplot.
671
+ Implements basic universal features.
672
+ """
673
+
674
+ _name = None # derived must override
675
+ _name_aliases = ()
676
+ _make_inset_locator = _TransformedBoundsLocator
677
+
678
+ def __repr__(self):
679
+ # Show the position in the geometry excluding panels. Panels are
680
+ # indicated by showing their parent geometry plus a 'side' argument.
681
+ # WARNING: This will not be used in matplotlib 3.3.0 (and probably next
682
+ # minor releases) because native __repr__ is defined in SubplotBase.
683
+ ax = self._get_topmost_axes()
684
+ name = type(self).__name__
685
+ prefix = "" if ax is self else "parent_"
686
+ params = {}
687
+ if self._name in ("cartopy", "basemap"):
688
+ name = name.replace("_" + self._name.title(), "Geo")
689
+ params["backend"] = self._name
690
+ if self._inset_parent:
691
+ name = re.sub("Axes(Subplot)?", "AxesInset", name)
692
+ params["bounds"] = tuple(np.round(self._inset_bounds, 2))
693
+ if self._altx_parent or self._alty_parent:
694
+ name = re.sub("Axes(Subplot)?", "AxesTwin", name)
695
+ params["axis"] = "x" if self._altx_parent else "y"
696
+ if self._colorbar_fill:
697
+ name = re.sub("Axes(Subplot)?", "AxesFill", name)
698
+ params["side"] = self._axes._panel_side
699
+ if self._panel_side:
700
+ name = re.sub("Axes(Subplot)?", "AxesPanel", name)
701
+ params["side"] = self._panel_side
702
+ try:
703
+ nrows, ncols, num1, num2 = (
704
+ ax.get_subplotspec().get_topmost_subplotspec()._get_geometry()
705
+ ) # noqa: E501
706
+ params[prefix + "index"] = (num1, num2)
707
+ except (IndexError, ValueError, AttributeError): # e.g. a loose axes
708
+ left, bottom, width, height = np.round(self._position.bounds, 2)
709
+ params["left"], params["bottom"], params["size"] = (
710
+ left,
711
+ bottom,
712
+ (width, bottom),
713
+ ) # noqa: E501
714
+ if ax.number:
715
+ params[prefix + "number"] = ax.number
716
+ params = ", ".join(f"{key}={value!r}" for key, value in params.items())
717
+ return f"{name}({params})"
718
+
719
+ def __str__(self):
720
+ return self.__repr__()
721
+
722
+ @docstring._snippet_manager
723
+ def __init__(self, *args, **kwargs):
724
+ """
725
+ Parameters
726
+ ----------
727
+ *args
728
+ Passed to `matplotlib.axes.Axes`.
729
+ %(axes.format)s
730
+
731
+ Other parameters
732
+ ----------------
733
+ %(rc.init)s
734
+
735
+ See also
736
+ --------
737
+ Axes.format
738
+ matplotlib.axes.Axes
739
+ ultraplot.axes.PlotAxes
740
+ ultraplot.axes.CartesianAxes
741
+ ultraplot.axes.PolarAxes
742
+ ultraplot.axes.GeoAxes
743
+ ultraplot.figure.Figure.subplot
744
+ ultraplot.figure.Figure.add_subplot
745
+ """
746
+ # Remove subplot-related args
747
+ # NOTE: These are documented on add_subplot()
748
+ ss = kwargs.pop("_subplot_spec", None) # see below
749
+ number = kwargs.pop("number", None)
750
+ autoshare = kwargs.pop("autoshare", None)
751
+ autoshare = _not_none(autoshare, True)
752
+
753
+ # Remove format-related args and initialize
754
+ rc_kw, rc_mode = _pop_rc(kwargs)
755
+ kw_format = _pop_props(kwargs, "patch") # background properties
756
+ if "zorder" in kw_format: # special case: refers to the entire axes
757
+ kwargs["zorder"] = kw_format.pop("zorder")
758
+ for cls, sig in self._format_signatures.items():
759
+ if isinstance(self, cls):
760
+ kw_format.update(_pop_params(kwargs, sig))
761
+ super().__init__(*args, **kwargs)
762
+
763
+ # Varous scalar properties
764
+ self._active_cycle = rc["axes.prop_cycle"]
765
+ self._auto_format = None # manipulated by wrapper functions
766
+ self._abc_border_kwargs = {}
767
+ self._abc_loc = None
768
+ self._abc_title_pad = rc["abc.titlepad"]
769
+ self._title_above = rc["title.above"]
770
+ self._title_border_kwargs = {} # title border properties
771
+ self._title_loc = None
772
+ self._title_pad = rc["title.pad"]
773
+ self._title_pad_current = None
774
+ self._altx_parent = None # for cartesian axes only
775
+ self._alty_parent = None
776
+ self._colorbar_fill = None
777
+ self._inset_parent = None
778
+ self._inset_bounds = None # for introspection ony
779
+ self._inset_zoom = False
780
+ self._inset_zoom_artists = None
781
+ self._panel_hidden = False # True when "filled" with cbar/legend
782
+ self._panel_align = {} # store 'align' and 'length' for "filled" cbar/legend
783
+ self._panel_parent = None
784
+ self._panel_share = False
785
+ self._panel_sharex_group = False # see _apply_auto_share
786
+ self._panel_sharey_group = False # see _apply_auto_share
787
+ self._panel_side = None
788
+ self._tight_bbox = None # bounding boxes are saved
789
+ self.xaxis.isDefault_minloc = True # ensure enabled at start (needed for dual)
790
+ self.yaxis.isDefault_minloc = True
791
+
792
+ # Various dictionary properties
793
+ # NOTE: Critical to use self.text() so they are patched with _update_label
794
+ self._legend_dict = {}
795
+ self._colorbar_dict = {}
796
+ d = self._panel_dict = {}
797
+ d["left"] = [] # NOTE: panels will be sorted inside-to-outside
798
+ d["right"] = []
799
+ d["bottom"] = []
800
+ d["top"] = []
801
+ d = self._title_dict = {}
802
+ kw = {"zorder": 3.5, "transform": self.transAxes}
803
+ d["abc"] = self.text(0, 0, "", **kw)
804
+ d["left"] = self._left_title # WARNING: track in case mpl changes this
805
+ d["center"] = self.title
806
+ d["right"] = self._right_title
807
+ d["upper left"] = self.text(0, 0, "", va="top", ha="left", **kw)
808
+ d["upper center"] = self.text(0, 0.5, "", va="top", ha="center", **kw)
809
+ d["upper right"] = self.text(0, 1, "", va="top", ha="right", **kw)
810
+ d["lower left"] = self.text(0, 0, "", va="bottom", ha="left", **kw)
811
+ d["lower center"] = self.text(0, 0.5, "", va="bottom", ha="center", **kw)
812
+ d["lower right"] = self.text(0, 1, "", va="bottom", ha="right", **kw)
813
+
814
+ # Subplot-specific settings
815
+ # NOTE: Default number for any axes is None (i.e., no a-b-c labels allowed)
816
+ # and for subplots added with add_subplot is incremented automatically
817
+ # WARNING: For mpl>=3.4.0 subplotspec assigned *after* initialization using
818
+ # set_subplotspec. Tried to defer to setter but really messes up both format()
819
+ # and _apply_auto_share(). Instead use workaround: Have Figure.add_subplot pass
820
+ # subplotspec as a hidden keyword arg. Non-subplots don't need this arg.
821
+ # See: https://github.com/matplotlib/matplotlib/pull/18564
822
+ self._number = None
823
+ if number: # not None or False
824
+ self.number = number
825
+ if ss is not None: # always passed from add_subplot
826
+ self.set_subplotspec(ss)
827
+ if autoshare:
828
+ self._apply_auto_share()
829
+
830
+ # Default formatting
831
+ # NOTE: This ignores user-input rc_mode. Mode '1' applies ultraplot
832
+ # features which is necessary on first run. Default otherwise is mode '2'
833
+ self.format(rc_kw=rc_kw, rc_mode=1, skip_figure=True, **kw_format)
834
+
835
+ def _add_inset_axes(
836
+ self,
837
+ bounds,
838
+ transform=None,
839
+ *,
840
+ proj=None,
841
+ projection=None,
842
+ zoom=None,
843
+ zoom_kw=None,
844
+ zorder=None,
845
+ **kwargs,
846
+ ):
847
+ """
848
+ Add an inset axes using arbitrary projection.
849
+ """
850
+ # Converting transform to figure-relative coordinates
851
+ transform = self._get_transform(transform, "axes")
852
+ locator = self._make_inset_locator(bounds, transform)
853
+ bounds = locator(self, None).bounds
854
+ label = kwargs.pop("label", "inset_axes")
855
+ zorder = _not_none(zorder, 4)
856
+
857
+ # Parse projection and inherit from the current axes by default
858
+ # NOTE: The _parse_proj method also accepts axes classes.
859
+ proj = _not_none(proj=proj, projection=projection)
860
+ if proj is None:
861
+ if self._name in ("cartopy", "basemap"):
862
+ proj = copy.copy(self.projection)
863
+ else:
864
+ proj = self._name
865
+ kwargs = self.figure._parse_proj(proj, **kwargs)
866
+
867
+ # Create axes and apply locator. The locator lets the axes adjust
868
+ # automatically if we used data coords. Called by ax.apply_aspect()
869
+ cls = mproj.get_projection_class(kwargs.pop("projection"))
870
+ ax = cls(self.figure, bounds, zorder=zorder, label=label, **kwargs)
871
+ ax.set_axes_locator(locator)
872
+ ax._inset_parent = self
873
+ ax._inset_bounds = bounds
874
+ self.add_child_axes(ax)
875
+
876
+ # Add zoom indicator (NOTE: requires matplotlib >= 3.0)
877
+ zoom_default = self._name == "cartesian" and ax._name == "cartesian"
878
+ zoom = ax._inset_zoom = _not_none(zoom, zoom_default)
879
+ if zoom:
880
+ zoom_kw = zoom_kw or {}
881
+ ax.indicate_inset_zoom(**zoom_kw)
882
+ return ax
883
+
884
+ def _add_queued_guides(self):
885
+ """
886
+ Draw the queued-up legends and colorbars. Wrapper funcs and legend func let
887
+ user add handles to location lists with successive calls.
888
+ """
889
+ # Draw queued colorbars
890
+ for (loc, align), colorbar in tuple(self._colorbar_dict.items()):
891
+ if not isinstance(colorbar, tuple):
892
+ continue
893
+ handles, labels, kwargs = colorbar
894
+ cb = self._add_colorbar(handles, labels, loc=loc, align=align, **kwargs)
895
+ self._colorbar_dict[(loc, align)] = cb
896
+
897
+ # Draw queued legends
898
+ # WARNING: Passing empty list labels=[] to legend causes matplotlib
899
+ # _parse_legend_args to search for everything. Ensure None if empty.
900
+ for (loc, align), legend in tuple(self._legend_dict.items()):
901
+ if not isinstance(legend, tuple) or any(
902
+ isinstance(_, mlegend.Legend) for _ in legend
903
+ ): # noqa: E501
904
+ continue
905
+ handles, labels, kwargs = legend
906
+ leg = self._add_legend(handles, labels, loc=loc, align=align, **kwargs)
907
+ self._legend_dict[(loc, align)] = leg
908
+
909
+ def _add_guide_frame(
910
+ self, xmin, ymin, width, height, *, fontsize, fancybox=None, **kwargs
911
+ ):
912
+ """
913
+ Add a colorbar or multilegend frame.
914
+ """
915
+ # TODO: Shadow patch does not seem to work. Unsure why.
916
+ # TODO: Add basic 'colorbar' and 'legend' artists with
917
+ # shared control over background frame.
918
+ shadow = kwargs.pop("shadow", None) # noqa: F841
919
+ renderer = self.figure._get_renderer()
920
+ fontsize = _fontsize_to_pt(fontsize)
921
+ fontsize = (fontsize / 72) / self._get_size_inches()[0] # axes relative units
922
+ fontsize = renderer.points_to_pixels(fontsize)
923
+ patch = mpatches.FancyBboxPatch(
924
+ (xmin, ymin),
925
+ width,
926
+ height,
927
+ snap=True,
928
+ zorder=4.5,
929
+ mutation_scale=fontsize,
930
+ transform=self.transAxes,
931
+ )
932
+ patch.set_clip_on(False)
933
+ if fancybox:
934
+ patch.set_boxstyle("round", pad=0, rounding_size=0.2)
935
+ else:
936
+ patch.set_boxstyle("square", pad=0)
937
+ patch.update(kwargs)
938
+ self.add_artist(patch)
939
+ return patch
940
+
941
+ def _add_guide_panel(self, loc="fill", align="center", length=0, **kwargs):
942
+ """
943
+ Add a panel to be filled by an "outer" colorbar or legend.
944
+ """
945
+ # NOTE: For colorbars we include 'length' when determining whether to allocate
946
+ # new panel but for legend just test whether that 'align' position was filled.
947
+ # WARNING: Hide content but 1) do not use ax.set_visible(False) so that
948
+ # tight layout will include legend and colorbar and 2) do not use
949
+ # ax.clear() so that top panel title and a-b-c label can remain.
950
+ bbox = _align_bbox(align, length)
951
+ if loc == "fill":
952
+ ax = self
953
+ elif loc in ("left", "right", "top", "bottom"):
954
+ ax = None
955
+ for pax in self._panel_dict[loc]:
956
+ if not pax._panel_hidden or align in pax._panel_align:
957
+ continue
958
+ if not any(bbox.overlaps(b) for b in pax._panel_align.values()):
959
+ ax = pax
960
+ break
961
+ if ax is None:
962
+ ax = self.panel_axes(loc, filled=True, **kwargs)
963
+ else:
964
+ raise ValueError(f"Invalid filled panel location {loc!r}.")
965
+ for s in ax.spines.values():
966
+ s.set_visible(False)
967
+ ax.xaxis.set_visible(False)
968
+ ax.yaxis.set_visible(False)
969
+ ax.patch.set_facecolor("none")
970
+ ax._panel_hidden = True
971
+ ax._panel_align[align] = bbox
972
+ return ax
973
+
974
+ @warnings._rename_kwargs("0.10", rasterize="rasterized")
975
+ def _add_colorbar(
976
+ self,
977
+ mappable,
978
+ values=None,
979
+ *,
980
+ loc=None,
981
+ align=None,
982
+ space=None,
983
+ pad=None,
984
+ width=None,
985
+ length=None,
986
+ shrink=None,
987
+ label=None,
988
+ title=None,
989
+ reverse=False,
990
+ rotation=None,
991
+ grid=None,
992
+ edges=None,
993
+ drawedges=None,
994
+ extend=None,
995
+ extendsize=None,
996
+ extendfrac=None,
997
+ ticks=None,
998
+ locator=None,
999
+ locator_kw=None,
1000
+ format=None,
1001
+ formatter=None,
1002
+ ticklabels=None,
1003
+ formatter_kw=None,
1004
+ minorticks=None,
1005
+ minorlocator=None,
1006
+ minorlocator_kw=None,
1007
+ tickminor=None,
1008
+ ticklen=None,
1009
+ ticklenratio=None,
1010
+ tickdir=None,
1011
+ tickdirection=None,
1012
+ tickwidth=None,
1013
+ tickwidthratio=None,
1014
+ ticklabelsize=None,
1015
+ ticklabelweight=None,
1016
+ ticklabelcolor=None,
1017
+ labelloc=None,
1018
+ labellocation=None,
1019
+ labelsize=None,
1020
+ labelweight=None,
1021
+ labelcolor=None,
1022
+ c=None,
1023
+ color=None,
1024
+ lw=None,
1025
+ linewidth=None,
1026
+ edgefix=None,
1027
+ rasterized=None,
1028
+ **kwargs,
1029
+ ):
1030
+ """
1031
+ The driver function for adding axes colorbars.
1032
+ """
1033
+ # Parse input arguments and apply defaults
1034
+ # TODO: Get the 'best' inset colorbar location using the legend algorithm
1035
+ # and implement inset colorbars the same as inset legends.
1036
+ grid = _not_none(
1037
+ grid=grid, edges=edges, drawedges=drawedges, default=rc["colorbar.grid"]
1038
+ ) # noqa: E501
1039
+ length = _not_none(length=length, shrink=shrink)
1040
+ label = _not_none(title=title, label=label)
1041
+ labelloc = _not_none(labelloc=labelloc, labellocation=labellocation)
1042
+ locator = _not_none(ticks=ticks, locator=locator)
1043
+ formatter = _not_none(ticklabels=ticklabels, formatter=formatter, format=format)
1044
+ minorlocator = _not_none(minorticks=minorticks, minorlocator=minorlocator)
1045
+ color = _not_none(c=c, color=color, default=rc["axes.edgecolor"])
1046
+ linewidth = _not_none(lw=lw, linewidth=linewidth)
1047
+ ticklen = units(_not_none(ticklen, rc["tick.len"]), "pt")
1048
+ tickdir = _not_none(tickdir=tickdir, tickdirection=tickdirection)
1049
+ tickwidth = units(_not_none(tickwidth, linewidth, rc["tick.width"]), "pt")
1050
+ linewidth = units(_not_none(linewidth, default=rc["axes.linewidth"]), "pt")
1051
+ ticklenratio = _not_none(ticklenratio, rc["tick.lenratio"])
1052
+ tickwidthratio = _not_none(tickwidthratio, rc["tick.widthratio"])
1053
+ rasterized = _not_none(rasterized, rc["colorbar.rasterized"])
1054
+
1055
+ # Build label and locator keyword argument dicts
1056
+ # NOTE: This carefully handles the 'maxn' and 'maxn_minor' deprecations
1057
+ kw_label = {}
1058
+ locator_kw = locator_kw or {}
1059
+ formatter_kw = formatter_kw or {}
1060
+ minorlocator_kw = minorlocator_kw or {}
1061
+ for key, value in (
1062
+ ("size", labelsize),
1063
+ ("weight", labelweight),
1064
+ ("color", labelcolor),
1065
+ ):
1066
+ if value is not None:
1067
+ kw_label[key] = value
1068
+ kw_ticklabels = {}
1069
+ for key, value in (
1070
+ ("size", ticklabelsize),
1071
+ ("weight", ticklabelweight),
1072
+ ("color", ticklabelcolor),
1073
+ ("rotation", rotation),
1074
+ ):
1075
+ if value is not None:
1076
+ kw_ticklabels[key] = value
1077
+ for b, kw in enumerate((locator_kw, minorlocator_kw)):
1078
+ key = "maxn_minor" if b else "maxn"
1079
+ name = "minorlocator" if b else "locator"
1080
+ nbins = kwargs.pop("maxn_minor" if b else "maxn", None)
1081
+ if nbins is not None:
1082
+ kw["nbins"] = nbins
1083
+ warnings._warn_ultraplot(
1084
+ f"The colorbar() keyword {key!r} was deprecated in v0.10. To "
1085
+ "achieve the same effect, you can pass 'nbins' to the new default "
1086
+ f"locator DiscreteLocator using {name}_kw={{'nbins': {nbins}}}."
1087
+ )
1088
+
1089
+ # Generate and prepare the colorbar axes
1090
+ # NOTE: The inset axes function needs 'label' to know how to pad the box
1091
+ # TODO: Use seperate keywords for frame properties vs. colorbar edge properties?
1092
+ if loc in ("fill", "left", "right", "top", "bottom"):
1093
+ length = _not_none(length, rc["colorbar.length"]) # for _add_guide_panel
1094
+ kwargs.update({"align": align, "length": length})
1095
+ extendsize = _not_none(extendsize, rc["colorbar.extend"])
1096
+ ax = self._add_guide_panel(
1097
+ loc, align, length=length, width=width, space=space, pad=pad
1098
+ ) # noqa: E501
1099
+ cax, kwargs = ax._parse_colorbar_filled(**kwargs)
1100
+ else:
1101
+ kwargs.update({"label": label, "length": length, "width": width})
1102
+ extendsize = _not_none(extendsize, rc["colorbar.insetextend"])
1103
+ cax, kwargs = self._parse_colorbar_inset(
1104
+ loc=loc, pad=pad, **kwargs
1105
+ ) # noqa: E501
1106
+
1107
+ # Parse the colorbar mappable
1108
+ # NOTE: Account for special case where auto colorbar is generated from 1D
1109
+ # methods that construct an 'artist list' (i.e. colormap scatter object)
1110
+ if (
1111
+ np.iterable(mappable)
1112
+ and len(mappable) == 1
1113
+ and isinstance(mappable[0], mcm.ScalarMappable)
1114
+ ): # noqa: E501
1115
+ mappable = mappable[0]
1116
+ if not isinstance(mappable, mcm.ScalarMappable):
1117
+ mappable, kwargs = cax._parse_colorbar_arg(mappable, values, **kwargs)
1118
+ else:
1119
+ pop = _pop_params(kwargs, cax._parse_colorbar_arg, ignore_internal=True)
1120
+ if pop:
1121
+ warnings._warn_ultraplot(
1122
+ f"Input is already a ScalarMappable. "
1123
+ f"Ignoring unused keyword arg(s): {pop}"
1124
+ )
1125
+
1126
+ # Parse 'extendsize' and 'extendfrac' keywords
1127
+ # TODO: Make this auto-adjust to the subplot size
1128
+ vert = kwargs["orientation"] == "vertical"
1129
+ if extendsize is not None and extendfrac is not None:
1130
+ warnings._warn_ultraplot(
1131
+ f"You cannot specify both an absolute extendsize={extendsize!r} "
1132
+ f"and a relative extendfrac={extendfrac!r}. Ignoring 'extendfrac'."
1133
+ )
1134
+ extendfrac = None
1135
+ if extendfrac is None:
1136
+ width, height = cax._get_size_inches()
1137
+ scale = height if vert else width
1138
+ extendsize = units(extendsize, "em", "in")
1139
+ extendfrac = extendsize / max(scale - 2 * extendsize, units(1, "em", "in"))
1140
+
1141
+ # Parse the tick locators and formatters
1142
+ # NOTE: In presence of BoundaryNorm or similar handle ticks with special
1143
+ # DiscreteLocator or else get issues (see mpl #22233).
1144
+ norm = mappable.norm
1145
+ formatter = _not_none(formatter, getattr(norm, "_labels", None), "auto")
1146
+ formatter = constructor.Formatter(formatter, **formatter_kw)
1147
+ categorical = isinstance(formatter, mticker.FixedFormatter)
1148
+ if locator is not None:
1149
+ locator = constructor.Locator(locator, **locator_kw)
1150
+ if minorlocator is not None: # overrides tickminor
1151
+ minorlocator = constructor.Locator(minorlocator, **minorlocator_kw)
1152
+ elif tickminor is None:
1153
+ tickminor = False if categorical else rc["xy"[vert] + "tick.minor.visible"]
1154
+ if isinstance(norm, mcolors.BoundaryNorm): # DiscreteNorm or BoundaryNorm
1155
+ ticks = getattr(norm, "_ticks", norm.boundaries)
1156
+ segmented = isinstance(getattr(norm, "_norm", None), pcolors.SegmentedNorm)
1157
+ if locator is None:
1158
+ if categorical or segmented:
1159
+ locator = mticker.FixedLocator(ticks)
1160
+ else:
1161
+ locator = pticker.DiscreteLocator(ticks)
1162
+ if tickminor and minorlocator is None:
1163
+ minorlocator = pticker.DiscreteLocator(ticks, minor=True)
1164
+
1165
+ # Special handling for colorbar keyword arguments
1166
+ # WARNING: Critical to not pass empty major locators in matplotlib < 3.5
1167
+ # See this issue: https://github.com/ultraplot-dev/ultraplot/issues/301
1168
+ # WARNING: ultraplot 'supports' passing one extend to a mappable function
1169
+ # then overwriting by passing another 'extend' to colobar. But contour
1170
+ # colorbars break when you try to change its 'extend'. Matplotlib gets
1171
+ # around this by just silently ignoring 'extend' passed to colorbar() but
1172
+ # we issue warning. Also note ContourSet.extend existed in matplotlib 3.0.
1173
+ # WARNING: Confusingly the only default way to have auto-adjusting
1174
+ # colorbar ticks is to specify no locator. Then _get_ticker_locator_formatter
1175
+ # uses the default ScalarFormatter on the axis that already has a set axis.
1176
+ # Otherwise it sets a default axis with locator.create_dummy_axis() in
1177
+ # update_ticks() which does not track axis size. Workaround is to manually
1178
+ # set the locator and formatter axis... however this messes up colorbar lengths
1179
+ # in matplotlib < 3.2. So we only apply this conditionally and in earlier
1180
+ # verisons recognize that DiscreteLocator will behave like FixedLocator.
1181
+ axis = cax.yaxis if vert else cax.xaxis
1182
+ if not isinstance(mappable, mcontour.ContourSet):
1183
+ extend = _not_none(extend, "neither")
1184
+ kwargs["extend"] = extend
1185
+ elif extend is not None and extend != mappable.extend:
1186
+ warnings._warn_ultraplot(
1187
+ "Ignoring extend={extend!r}. ContourSet extend cannot be changed."
1188
+ )
1189
+ if (
1190
+ isinstance(locator, mticker.NullLocator)
1191
+ or hasattr(locator, "locs")
1192
+ and len(locator.locs) == 0
1193
+ ):
1194
+ minorlocator, tickminor = None, False # attempted fix
1195
+ for ticker in (locator, formatter, minorlocator):
1196
+ if _version_mpl < "3.2":
1197
+ pass # see notes above
1198
+ elif isinstance(ticker, mticker.TickHelper):
1199
+ ticker.set_axis(axis)
1200
+
1201
+ # Create colorbar and update ticks and axis direction
1202
+ # NOTE: This also adds the guides._update_ticks() monkey patch that triggers
1203
+ # updates to DiscreteLocator when parent axes is drawn.
1204
+ obj = cax._colorbar_fill = cax.figure.colorbar(
1205
+ mappable,
1206
+ cax=cax,
1207
+ ticks=locator,
1208
+ format=formatter,
1209
+ drawedges=grid,
1210
+ extendfrac=extendfrac,
1211
+ **kwargs,
1212
+ )
1213
+ # obj.minorlocator = minorlocator # backwards compatibility
1214
+ obj.update_ticks = guides._update_ticks.__get__(obj) # backwards compatible
1215
+ if minorlocator is not None:
1216
+ obj.update_ticks()
1217
+ elif tickminor:
1218
+ obj.minorticks_on()
1219
+ else:
1220
+ obj.minorticks_off()
1221
+ if getattr(norm, "descending", None):
1222
+ axis.set_inverted(True)
1223
+ if reverse: # potentially double reverse, although that would be weird...
1224
+ axis.set_inverted(True)
1225
+
1226
+ # Update other colorbar settings
1227
+ # WARNING: Must use the colorbar set_label to set text. Calling set_label
1228
+ # on the actual axis will do nothing!
1229
+ axis.set_tick_params(which="both", color=color, direction=tickdir)
1230
+ axis.set_tick_params(which="major", length=ticklen, width=tickwidth)
1231
+ axis.set_tick_params(
1232
+ which="minor",
1233
+ length=ticklen * ticklenratio,
1234
+ width=tickwidth * tickwidthratio,
1235
+ ) # noqa: E501
1236
+ if label is not None:
1237
+ obj.set_label(label)
1238
+ if labelloc is not None:
1239
+ axis.set_label_position(labelloc)
1240
+ axis.label.update(kw_label)
1241
+ for label in axis.get_ticklabels():
1242
+ label.update(kw_ticklabels)
1243
+ kw_outline = {"edgecolor": color, "linewidth": linewidth}
1244
+ if obj.outline is not None:
1245
+ obj.outline.update(kw_outline)
1246
+ if obj.dividers is not None:
1247
+ obj.dividers.update(kw_outline)
1248
+ if obj.solids:
1249
+ from . import PlotAxes
1250
+
1251
+ obj.solids.set_rasterized(rasterized)
1252
+ PlotAxes._fix_patch_edges(obj.solids, edgefix=edgefix)
1253
+
1254
+ # Register location and return
1255
+ self._register_guide("colorbar", obj, (loc, align)) # possibly replace another
1256
+ return obj
1257
+
1258
+ def _add_legend(
1259
+ self,
1260
+ handles=None,
1261
+ labels=None,
1262
+ *,
1263
+ loc=None,
1264
+ align=None,
1265
+ width=None,
1266
+ pad=None,
1267
+ space=None,
1268
+ frame=None,
1269
+ frameon=None,
1270
+ ncol=None,
1271
+ ncols=None,
1272
+ alphabetize=False,
1273
+ center=None,
1274
+ order=None,
1275
+ label=None,
1276
+ title=None,
1277
+ fontsize=None,
1278
+ fontweight=None,
1279
+ fontcolor=None,
1280
+ titlefontsize=None,
1281
+ titlefontweight=None,
1282
+ titlefontcolor=None,
1283
+ handle_kw=None,
1284
+ handler_map=None,
1285
+ **kwargs,
1286
+ ):
1287
+ """
1288
+ The driver function for adding axes legends.
1289
+ """
1290
+ # Parse input argument units
1291
+ ncol = _not_none(ncols=ncols, ncol=ncol)
1292
+ order = _not_none(order, "C")
1293
+ frameon = _not_none(frame=frame, frameon=frameon, default=rc["legend.frameon"])
1294
+ fontsize = _not_none(fontsize, rc["legend.fontsize"])
1295
+ titlefontsize = _not_none(
1296
+ title_fontsize=kwargs.pop("title_fontsize", None),
1297
+ titlefontsize=titlefontsize,
1298
+ default=rc["legend.title_fontsize"],
1299
+ )
1300
+ fontsize = _fontsize_to_pt(fontsize)
1301
+ titlefontsize = _fontsize_to_pt(titlefontsize)
1302
+ if order not in ("F", "C"):
1303
+ raise ValueError(
1304
+ f"Invalid order {order!r}. Please choose from "
1305
+ "'C' (row-major, default) or 'F' (column-major)."
1306
+ )
1307
+
1308
+ # Convert relevant keys to em-widths
1309
+ for setting in rcsetup.EM_KEYS: # em-width keys
1310
+ pair = setting.split("legend.", 1)
1311
+ if len(pair) == 1:
1312
+ continue
1313
+ _, key = pair
1314
+ value = kwargs.pop(key, None)
1315
+ if isinstance(value, str):
1316
+ value = units(kwargs[key], "em", fontsize=fontsize)
1317
+ if value is not None:
1318
+ kwargs[key] = value
1319
+
1320
+ # Generate and prepare the legend axes
1321
+ if loc in ("fill", "left", "right", "top", "bottom"):
1322
+ lax = self._add_guide_panel(loc, align, width=width, space=space, pad=pad)
1323
+ kwargs.setdefault("borderaxespad", 0)
1324
+ if not frameon:
1325
+ kwargs.setdefault("borderpad", 0)
1326
+ try:
1327
+ kwargs["loc"] = ALIGN_OPTS[lax._panel_side][align]
1328
+ except KeyError:
1329
+ raise ValueError(f"Invalid align={align!r} for legend loc={loc!r}.")
1330
+ else:
1331
+ lax = self
1332
+ pad = kwargs.pop("borderaxespad", pad)
1333
+ kwargs["loc"] = loc # simply pass to legend
1334
+ kwargs["borderaxespad"] = units(pad, "em", fontsize=fontsize)
1335
+
1336
+ # Handle and text properties that are applied after-the-fact
1337
+ # NOTE: Set solid_capstyle to 'butt' so line does not extend past error bounds
1338
+ # shading in legend entry. This change is not noticable in other situations.
1339
+ kw_frame, kwargs = lax._parse_frame("legend", **kwargs)
1340
+ kw_text = {}
1341
+ if fontcolor is not None:
1342
+ kw_text["color"] = fontcolor
1343
+ if fontweight is not None:
1344
+ kw_text["weight"] = fontweight
1345
+ kw_title = {}
1346
+ if titlefontcolor is not None:
1347
+ kw_title["color"] = titlefontcolor
1348
+ if titlefontweight is not None:
1349
+ kw_title["weight"] = titlefontweight
1350
+ kw_handle = _pop_props(kwargs, "line")
1351
+ kw_handle.setdefault("solid_capstyle", "butt")
1352
+ kw_handle.update(handle_kw or {})
1353
+
1354
+ # Parse the legend arguments using axes for auto-handle detection
1355
+ # TODO: Update this when we no longer use "filled panels" for outer legends
1356
+ pairs, multi = lax._parse_legend_handles(
1357
+ handles,
1358
+ labels,
1359
+ ncol=ncol,
1360
+ order=order,
1361
+ center=center,
1362
+ alphabetize=alphabetize,
1363
+ handler_map=handler_map,
1364
+ )
1365
+ title = _not_none(label=label, title=title)
1366
+ kwargs.update(
1367
+ {
1368
+ "title": title,
1369
+ "frameon": frameon,
1370
+ "fontsize": fontsize,
1371
+ "handler_map": handler_map,
1372
+ "title_fontsize": titlefontsize,
1373
+ }
1374
+ )
1375
+
1376
+ # Add the legend and update patch properties
1377
+ # TODO: Add capacity for categorical labels in a single legend like seaborn
1378
+ # rather than manual handle overrides with multiple legends.
1379
+ if multi:
1380
+ objs = lax._parse_legend_centered(pairs, kw_frame=kw_frame, **kwargs)
1381
+ else:
1382
+ kwargs.update({key: kw_frame.pop(key) for key in ("shadow", "fancybox")})
1383
+ objs = [lax._parse_legend_aligned(pairs, ncol=ncol, order=order, **kwargs)]
1384
+ objs[0].legendPatch.update(kw_frame)
1385
+ for obj in objs:
1386
+ if hasattr(lax, "legend_") and lax.legend_ is None:
1387
+ lax.legend_ = obj # make first legend accessible with get_legend()
1388
+ else:
1389
+ lax.add_artist(obj)
1390
+
1391
+ # Update legend patch and elements
1392
+ # WARNING: legendHandles only contains the *first* artist per legend because
1393
+ # HandlerBase.legend_artist() called in Legend._init_legend_box() only
1394
+ # returns the first artist. Instead we try to iterate through offset boxes.
1395
+ for obj in objs:
1396
+ obj.set_clip_on(False) # needed for tight bounding box calculations
1397
+ box = getattr(obj, "_legend_handle_box", None)
1398
+ for obj in guides._iter_children(box):
1399
+ if isinstance(obj, mtext.Text):
1400
+ kw = kw_text
1401
+ else:
1402
+ kw = {
1403
+ key: val
1404
+ for key, val in kw_handle.items()
1405
+ if hasattr(obj, "set_" + key)
1406
+ } # noqa: E501
1407
+ if hasattr(obj, "set_sizes") and "markersize" in kw_handle:
1408
+ kw["sizes"] = np.atleast_1d(kw_handle["markersize"])
1409
+ obj.update(kw)
1410
+
1411
+ # Register location and return
1412
+ if isinstance(objs[0], mpatches.FancyBboxPatch):
1413
+ objs = objs[1:]
1414
+ obj = objs[0] if len(objs) == 1 else tuple(objs)
1415
+ self._register_guide("legend", obj, (loc, align)) # possibly replace another
1416
+
1417
+ return obj
1418
+
1419
+ def _apply_title_above(self):
1420
+ """
1421
+ Change assignment of outer titles between main subplot and upper panels.
1422
+ This is called when a panel is created or `_update_title` is called.
1423
+ """
1424
+ # NOTE: Similar to how _apply_axis_sharing() is called in _align_axis_labels()
1425
+ # this is called in _align_super_labels() so we get the correct offset.
1426
+ paxs = self._panel_dict["top"]
1427
+ if not paxs:
1428
+ return
1429
+ pax = paxs[-1]
1430
+ names = ("left", "center", "right")
1431
+ if self._abc_loc in names:
1432
+ names += ("abc",)
1433
+ if not self._title_above:
1434
+ return
1435
+ if pax._panel_hidden and self._title_above == "panels":
1436
+ return
1437
+ pax._title_pad = self._title_pad
1438
+ pax._abc_title_pad = self._abc_title_pad
1439
+ for name in names:
1440
+ labels._transfer_label(self._title_dict[name], pax._title_dict[name])
1441
+
1442
+ def _apply_auto_share(self):
1443
+ """
1444
+ Automatically configure axis sharing based on the horizontal and
1445
+ vertical extent of subplots in the figure gridspec.
1446
+ """
1447
+
1448
+ # Panel axes sharing, between main subplot and its panels
1449
+ # NOTE: _panel_share means "include this panel in the axis sharing group" while
1450
+ # _panel_sharex_group indicates the group itself and may include main axes
1451
+ def shared(paxs):
1452
+ return [pax for pax in paxs if not pax._panel_hidden and pax._panel_share]
1453
+
1454
+ # Internal axis sharing, share stacks of panels and main axes with each other
1455
+ # NOTE: This is called on the main axes whenver a panel is created.
1456
+ # NOTE: This block is why, even though we have figure-wide share[xy], we
1457
+ # still need the axes-specific _share[xy]_override attribute.
1458
+ if not self._panel_side: # this is a main axes
1459
+ # Top and bottom
1460
+ bottom = self
1461
+ paxs = shared(self._panel_dict["bottom"])
1462
+ if paxs:
1463
+ bottom = paxs[-1]
1464
+ bottom._panel_sharex_group = False
1465
+ for iax in (self, *paxs[:-1]):
1466
+ iax._panel_sharex_group = True
1467
+ iax._sharex_setup(bottom) # parent is bottom-most
1468
+ paxs = shared(self._panel_dict["top"])
1469
+ for iax in paxs:
1470
+ iax._panel_sharex_group = True
1471
+ iax._sharex_setup(bottom)
1472
+ # Left and right
1473
+ # NOTE: Order of panel lists is always inside-to-outside
1474
+ left = self
1475
+ paxs = shared(self._panel_dict["left"])
1476
+ if paxs:
1477
+ left = paxs[-1]
1478
+ left._panel_sharey_group = False
1479
+ for iax in (self, *paxs[:-1]):
1480
+ iax._panel_sharey_group = True
1481
+ iax._sharey_setup(left) # parent is left-most
1482
+ paxs = shared(self._panel_dict["right"])
1483
+ for iax in paxs:
1484
+ iax._panel_sharey_group = True
1485
+ iax._sharey_setup(left)
1486
+
1487
+ # External axes sharing, sometimes overrides panel axes sharing
1488
+ # Share x axes
1489
+ parent, *children = self._get_share_axes("x")
1490
+ for child in children:
1491
+ child._sharex_setup(parent)
1492
+ # Share y axes
1493
+ parent, *children = self._get_share_axes("y")
1494
+ for child in children:
1495
+ child._sharey_setup(parent)
1496
+ # Global sharing, use the reference subplot because why not
1497
+ ref = self.figure._subplot_dict.get(self.figure._refnum, None)
1498
+ if self is not ref:
1499
+ if self.figure._sharex > 3:
1500
+ self._sharex_setup(ref, labels=False)
1501
+ if self.figure._sharey > 3:
1502
+ self._sharey_setup(ref, labels=False)
1503
+
1504
+ def _artist_fully_clipped(self, artist):
1505
+ """
1506
+ Return a boolean flag, ``True`` if the artist is clipped to the axes
1507
+ and can thus be skipped in layout calculations.
1508
+ """
1509
+ clip_box = artist.get_clip_box()
1510
+ clip_path = artist.get_clip_path()
1511
+ types_noclip = (
1512
+ maxes.Axes,
1513
+ maxis.Axis,
1514
+ moffsetbox.AnnotationBbox,
1515
+ moffsetbox.OffsetBox,
1516
+ )
1517
+ return not isinstance(artist, types_noclip) and (
1518
+ artist.get_clip_on()
1519
+ and (clip_box is not None or clip_path is not None)
1520
+ and (clip_box is None or np.all(clip_box.extents == self.bbox.extents))
1521
+ and (
1522
+ clip_path is None
1523
+ or isinstance(clip_path, mtransforms.TransformedPatchPath)
1524
+ and clip_path._patch is self.patch
1525
+ )
1526
+ )
1527
+
1528
+ def _get_legend_handles(self, handler_map=None):
1529
+ """
1530
+ Internal implementation of matplotlib's ``get_legend_handles_labels``.
1531
+ """
1532
+ if not self._panel_hidden: # this is a normal axes
1533
+ axs = [self]
1534
+ elif self._panel_parent: # this is an axes-wide legend
1535
+ axs = list(self._panel_parent._iter_axes(hidden=False, children=True))
1536
+ else: # this is a figure-wide legend
1537
+ axs = list(self.figure._iter_axes(hidden=False, children=True))
1538
+ handles = []
1539
+ handler_map_full = mlegend.Legend.get_default_handler_map()
1540
+ handler_map_full = handler_map_full.copy()
1541
+ handler_map_full.update(handler_map or {})
1542
+ for ax in axs:
1543
+ for attr in ("lines", "patches", "collections", "containers"):
1544
+ for handle in getattr(ax, attr, []): # guard against API changes
1545
+ label = handle.get_label()
1546
+ handler = mlegend.Legend.get_legend_handler(
1547
+ handler_map_full, handle
1548
+ ) # noqa: E501
1549
+ if handler and label and label[0] != "_":
1550
+ handles.append(handle)
1551
+ return handles
1552
+
1553
+ def _get_share_axes(self, sx, panels=False):
1554
+ """
1555
+ Return the axes whose horizontal or vertical extent in the main gridspec
1556
+ matches the horizontal or vertical extent of this axes.
1557
+ """
1558
+ # NOTE: The lefmost or bottommost axes are at the start of the list.
1559
+ if not isinstance(self, maxes.SubplotBase):
1560
+ return [self]
1561
+ i = 0 if sx == "x" else 1
1562
+ sy = "y" if sx == "x" else "x"
1563
+ argfunc = np.argmax if sx == "x" else np.argmin
1564
+ irange = self._range_subplotspec(sx)
1565
+ axs = self.figure._iter_axes(hidden=False, children=False, panels=panels)
1566
+ axs = [ax for ax in axs if ax._range_subplotspec(sx) == irange]
1567
+ axs = list({self, *axs}) # self may be missing during initialization
1568
+ pax = axs.pop(argfunc([ax._range_subplotspec(sy)[i] for ax in axs]))
1569
+ return [pax, *axs] # return with leftmost or bottommost first
1570
+
1571
+ def _get_span_axes(self, side, panels=False):
1572
+ """
1573
+ Return the axes whose left, right, top, or bottom sides abutt against
1574
+ the same row or column as this axes. Deflect to shared panels.
1575
+ """
1576
+ if side not in ("left", "right", "bottom", "top"):
1577
+ raise ValueError(f"Invalid side {side!r}.")
1578
+ if not isinstance(self, maxes.SubplotBase):
1579
+ return [self]
1580
+ x, y = "xy" if side in ("left", "right") else "yx"
1581
+ idx = 0 if side in ("left", "top") else 1 # which side to test
1582
+ coord = self._range_subplotspec(x)[idx] # side for a particular axes
1583
+ axs = self.figure._iter_axes(hidden=False, children=False, panels=panels)
1584
+ axs = [ax for ax in axs if ax._range_subplotspec(x)[idx] == coord] or [self]
1585
+ out = []
1586
+ for ax in axs:
1587
+ other = getattr(ax, "_share" + y)
1588
+ if other and other._panel_parent: # this is a shared panel
1589
+ ax = other
1590
+ out.append(ax)
1591
+ return out
1592
+
1593
+ def _get_size_inches(self):
1594
+ """
1595
+ Return the width and height of the axes in inches.
1596
+ """
1597
+ width, height = self.figure.get_size_inches()
1598
+ bbox = self.get_position()
1599
+ width = width * abs(bbox.width)
1600
+ height = height * abs(bbox.height)
1601
+ return np.array([width, height])
1602
+
1603
+ def _get_topmost_axes(self):
1604
+ """
1605
+ Return the topmost axes including panels and parents.
1606
+ """
1607
+ for _ in range(5):
1608
+ self = self._axes or self
1609
+ self = self._panel_parent or self
1610
+ return self
1611
+
1612
+ def _get_transform(self, transform, default="data"):
1613
+ """
1614
+ Translates user input transform. Also used in an axes method.
1615
+ """
1616
+ # TODO: Can this support cartopy transforms? Seems not when this
1617
+ # is used for inset axes bounds but maybe in other places?
1618
+ transform = _not_none(transform, default)
1619
+ if isinstance(transform, mtransforms.Transform):
1620
+ return transform
1621
+ elif CRS is not object and isinstance(transform, CRS):
1622
+ return transform
1623
+ elif PlateCarree is not object and transform == "map":
1624
+ return PlateCarree()
1625
+ elif transform == "data":
1626
+ return self.transData
1627
+ elif transform == "axes":
1628
+ return self.transAxes
1629
+ elif transform == "figure":
1630
+ return self.figure.transFigure
1631
+ elif transform == "subfigure":
1632
+ return self.figure.transSubfigure
1633
+ else:
1634
+ raise ValueError(f"Unknown transform {transform!r}.")
1635
+
1636
+ def _register_guide(self, guide, obj, key, **kwargs):
1637
+ """
1638
+ Queue up or replace objects for legends and list-of-artist style colorbars.
1639
+ """
1640
+ # Initial stuff
1641
+ if guide not in ("legend", "colorbar"):
1642
+ raise TypeError(f"Invalid type {guide!r}.")
1643
+ dict_ = self._legend_dict if guide == "legend" else self._colorbar_dict
1644
+
1645
+ # Remove previous instances
1646
+ # NOTE: No good way to remove inset colorbars right now until the bounding
1647
+ # box and axes are merged into some kind of subclass. Just fine for now.
1648
+ if key in dict_ and not isinstance(dict_[key], tuple):
1649
+ prev = dict_.pop(key) # possibly pop a queued object
1650
+ if guide == "colorbar":
1651
+ pass
1652
+ elif hasattr(self, "legend_") and prev.axes.legend_ is prev:
1653
+ self.legend_ = None # was never added as artist
1654
+ else:
1655
+ prev.remove() # remove legends and inner colorbars
1656
+
1657
+ # Replace with instance or update the queue
1658
+ # NOTE: This is valid for both mappable-values pairs and handles-labels pairs
1659
+ if not isinstance(obj, tuple) or any(
1660
+ isinstance(_, mlegend.Legend) for _ in obj
1661
+ ): # noqa: E501
1662
+ dict_[key] = obj
1663
+ else:
1664
+ handles, labels = obj
1665
+ if not np.iterable(handles) or type(handles) is tuple:
1666
+ handles = [handles]
1667
+ if not np.iterable(labels) or isinstance(labels, str):
1668
+ labels = [labels] * len(handles)
1669
+ length = min(len(handles), len(labels)) # mimics 'zip' behavior
1670
+ handles_full, labels_full, kwargs_full = dict_.setdefault(key, ([], [], {}))
1671
+ handles_full.extend(handles[:length])
1672
+ labels_full.extend(labels[:length])
1673
+ kwargs_full.update(kwargs)
1674
+
1675
+ def _update_guide(
1676
+ self,
1677
+ objs,
1678
+ legend=None,
1679
+ legend_kw=None,
1680
+ queue_legend=True,
1681
+ colorbar=None,
1682
+ colorbar_kw=None,
1683
+ queue_colorbar=True,
1684
+ ):
1685
+ """
1686
+ Update queues for on-the-fly legends and colorbars or track keyword arguments.
1687
+ """
1688
+ # WARNING: Important to always cache the keyword arguments so e.g.
1689
+ # duplicate subsequent calls still enforce user and default behavior.
1690
+ # WARNING: This should generally be last in the pipeline before calling
1691
+ # the plot function or looping over data columns. The colormap parser
1692
+ # and standardize functions both modify colorbar_kw and legend_kw.
1693
+ legend_kw = legend_kw or {}
1694
+ colorbar_kw = colorbar_kw or {}
1695
+ guides._cache_guide_kw(objs, "legend", legend_kw)
1696
+ guides._cache_guide_kw(objs, "colorbar", colorbar_kw)
1697
+ if legend:
1698
+ align = legend_kw.pop("align", None)
1699
+ queue = legend_kw.pop("queue", queue_legend)
1700
+ self.legend(objs, loc=legend, align=align, queue=queue, **legend_kw)
1701
+ if colorbar:
1702
+ align = colorbar_kw.pop("align", None)
1703
+ queue = colorbar_kw.pop("queue", queue_colorbar)
1704
+ self.colorbar(objs, loc=colorbar, align=align, queue=queue, **colorbar_kw)
1705
+
1706
+ @staticmethod
1707
+ def _parse_frame(guide, fancybox=None, shadow=None, **kwargs):
1708
+ """
1709
+ Parse frame arguments.
1710
+ """
1711
+ # NOTE: Here we permit only 'edgewidth' to avoid conflict with
1712
+ # 'linewidth' used for legend handles and colorbar edge.
1713
+ kw_frame = _pop_kwargs(
1714
+ kwargs,
1715
+ alpha=("a", "framealpha", "facealpha"),
1716
+ facecolor=("fc", "framecolor", "facecolor"),
1717
+ edgecolor=("ec",),
1718
+ edgewidth=("ew",),
1719
+ )
1720
+ _kw_frame_default = {
1721
+ "alpha": f"{guide}.framealpha",
1722
+ "facecolor": f"{guide}.facecolor",
1723
+ "edgecolor": f"{guide}.edgecolor",
1724
+ "edgewidth": "axes.linewidth",
1725
+ }
1726
+ for key, name in _kw_frame_default.items():
1727
+ kw_frame.setdefault(key, rc[name])
1728
+ for key in ("facecolor", "edgecolor"):
1729
+ if kw_frame[key] == "inherit":
1730
+ kw_frame[key] = rc["axes." + key]
1731
+ kw_frame["linewidth"] = kw_frame.pop("edgewidth")
1732
+ kw_frame["fancybox"] = _not_none(fancybox, rc[f"{guide}.fancybox"])
1733
+ kw_frame["shadow"] = _not_none(shadow, rc[f"{guide}.shadow"])
1734
+ return kw_frame, kwargs
1735
+
1736
+ @staticmethod
1737
+ def _parse_colorbar_arg(
1738
+ mappable, values=None, norm=None, norm_kw=None, vmin=None, vmax=None, **kwargs
1739
+ ):
1740
+ """
1741
+ Generate a mappable from flexible non-mappable input. Useful in bridging
1742
+ the gap between legends and colorbars (e.g., creating colorbars from line
1743
+ objects whose data values span a natural colormap range).
1744
+ """
1745
+ # For container objects, we just assume color is the same for every item.
1746
+ # Works for ErrorbarContainer, StemContainer, BarContainer.
1747
+ if (
1748
+ np.iterable(mappable)
1749
+ and len(mappable) > 0
1750
+ and all(isinstance(obj, mcontainer.Container) for obj in mappable)
1751
+ ):
1752
+ mappable = [obj[0] for obj in mappable]
1753
+
1754
+ # Colormap instance
1755
+ if isinstance(mappable, mcolors.Colormap) or isinstance(mappable, str):
1756
+ cmap = constructor.Colormap(mappable)
1757
+ if values is None and isinstance(cmap, pcolors.DiscreteColormap):
1758
+ values = [None] * cmap.N # sometimes use discrete norm
1759
+
1760
+ # List of colors
1761
+ elif np.iterable(mappable) and all(map(mcolors.is_color_like, mappable)):
1762
+ cmap = pcolors.DiscreteColormap(list(mappable), "_no_name")
1763
+ if values is None:
1764
+ values = [None] * len(mappable) # always use discrete norm
1765
+
1766
+ # List of artists
1767
+ # NOTE: Do not check for isinstance(Artist) in case it is an mpl collection
1768
+ elif np.iterable(mappable) and all(
1769
+ hasattr(obj, "get_color") or hasattr(obj, "get_facecolor")
1770
+ for obj in mappable # noqa: E501
1771
+ ):
1772
+ # Generate colormap from colors and infer tick labels
1773
+ colors = []
1774
+ for obj in mappable:
1775
+ if hasattr(obj, "update_scalarmappable"): # for e.g. pcolor
1776
+ obj.update_scalarmappable()
1777
+ color = (
1778
+ obj.get_color()
1779
+ if hasattr(obj, "get_color")
1780
+ else obj.get_facecolor()
1781
+ ) # noqa: E501
1782
+ if isinstance(color, np.ndarray):
1783
+ color = color.squeeze() # e.g. single color scatter plot
1784
+ if not mcolors.is_color_like(color):
1785
+ raise ValueError(
1786
+ "Cannot make colorbar from artists with more than one color."
1787
+ ) # noqa: E501
1788
+ colors.append(color)
1789
+ # Try to infer tick values and tick labels from Artist labels
1790
+ cmap = pcolors.DiscreteColormap(colors, "_no_name")
1791
+ if values is None:
1792
+ values = [None] * len(mappable)
1793
+ else:
1794
+ values = list(values)
1795
+ for i, (obj, val) in enumerate(zip(mappable, values)):
1796
+ if val is not None:
1797
+ continue
1798
+ val = obj.get_label()
1799
+ if val and val[0] == "_":
1800
+ continue
1801
+ values[i] = val
1802
+
1803
+ else:
1804
+ raise ValueError(
1805
+ "Input colorbar() argument must be a scalar mappable, colormap name "
1806
+ f"or object, list of colors, or list of artists. Got {mappable!r}."
1807
+ )
1808
+
1809
+ # Generate continuous normalizer, and possibly discrete normalizer. Update
1810
+ # the outgoing locator and formatter if user does not override.
1811
+ norm_kw = norm_kw or {}
1812
+ norm = norm or "linear"
1813
+ vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop("vmin", None), default=0)
1814
+ vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop("vmax", None), default=1)
1815
+ norm = constructor.Norm(norm, vmin=vmin, vmax=vmax, **norm_kw)
1816
+ if values is not None:
1817
+ ticks = []
1818
+ labels = None
1819
+ for i, val in enumerate(values):
1820
+ try:
1821
+ val = float(val)
1822
+ except (TypeError, ValueError):
1823
+ pass
1824
+ if val is None:
1825
+ val = i
1826
+ ticks.append(val)
1827
+ if any(isinstance(_, str) for _ in ticks):
1828
+ labels = list(map(str, ticks))
1829
+ ticks = np.arange(len(ticks))
1830
+ if len(ticks) == 1:
1831
+ levels = [ticks[0] - 1, ticks[0] + 1]
1832
+ else:
1833
+ levels = edges(ticks)
1834
+ from . import PlotAxes
1835
+
1836
+ norm, cmap, _ = PlotAxes._parse_level_norm(
1837
+ levels, norm, cmap, discrete_ticks=ticks, discrete_labels=labels
1838
+ )
1839
+
1840
+ # Return ad hoc ScalarMappable and update locator and formatter
1841
+ # NOTE: If value list doesn't match this may cycle over colors.
1842
+ mappable = mcm.ScalarMappable(norm, cmap)
1843
+ return mappable, kwargs
1844
+
1845
+ def _parse_colorbar_filled(
1846
+ self,
1847
+ length=None,
1848
+ align=None,
1849
+ tickloc=None,
1850
+ ticklocation=None,
1851
+ orientation=None,
1852
+ **kwargs,
1853
+ ):
1854
+ """
1855
+ Return the axes and adjusted keyword args for a panel-filling colorbar.
1856
+ """
1857
+ # Parse input arguments
1858
+ side = self._panel_side
1859
+ side = _not_none(side, "left" if orientation == "vertical" else "bottom")
1860
+ align = _not_none(align, "center")
1861
+ length = _not_none(length=length, default=rc["colorbar.length"])
1862
+ ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation)
1863
+
1864
+ # Calculate inset bounds for the colorbar
1865
+ delta = 0.5 * (1 - length)
1866
+ if side in ("bottom", "top"):
1867
+ if align == "left":
1868
+ bounds = (0, 0, length, 1)
1869
+ elif align == "center":
1870
+ bounds = (delta, 0, length, 1)
1871
+ elif align == "right":
1872
+ bounds = (2 * delta, 0, length, 1)
1873
+ else:
1874
+ raise ValueError(f"Invalid align={align!r} for colorbar loc={side!r}.")
1875
+ else:
1876
+ if align == "bottom":
1877
+ bounds = (0, 0, 1, length)
1878
+ elif align == "center":
1879
+ bounds = (0, delta, 1, length)
1880
+ elif align == "top":
1881
+ bounds = (0, 2 * delta, 1, length)
1882
+ else:
1883
+ raise ValueError(f"Invalid align={align!r} for colorbar loc={side!r}.")
1884
+
1885
+ # Add the axes as a child of the original axes
1886
+ cls = mproj.get_projection_class("ultraplot_cartesian")
1887
+ locator = self._make_inset_locator(bounds, self.transAxes)
1888
+ ax = cls(self.figure, locator(self, None).bounds, zorder=5)
1889
+ ax.set_axes_locator(locator)
1890
+ self.add_child_axes(ax)
1891
+ ax.patch.set_facecolor("none") # ignore axes.alpha application
1892
+
1893
+ # Handle default keyword args
1894
+ if orientation is None:
1895
+ orientation = "horizontal" if side in ("bottom", "top") else "vertical"
1896
+ if orientation == "horizontal":
1897
+ outside, inside = "bottom", "top"
1898
+ if side == "top":
1899
+ outside, inside = inside, outside
1900
+ ticklocation = _not_none(ticklocation, outside)
1901
+ else:
1902
+ outside, inside = "left", "right"
1903
+ if side == "right":
1904
+ outside, inside = inside, outside
1905
+ ticklocation = _not_none(ticklocation, outside)
1906
+ kwargs.update({"orientation": orientation, "ticklocation": ticklocation})
1907
+ return ax, kwargs
1908
+
1909
+ def _parse_colorbar_inset(
1910
+ self,
1911
+ loc=None,
1912
+ width=None,
1913
+ length=None,
1914
+ shrink=None,
1915
+ frame=None,
1916
+ frameon=None,
1917
+ label=None,
1918
+ pad=None,
1919
+ tickloc=None,
1920
+ ticklocation=None,
1921
+ orientation=None,
1922
+ **kwargs,
1923
+ ):
1924
+ """
1925
+ Return the axes and adjusted keyword args for an inset colorbar.
1926
+ """
1927
+ # Basic colorbar properties
1928
+ frame = _not_none(frame=frame, frameon=frameon, default=rc["colorbar.frameon"])
1929
+ length = _not_none(
1930
+ length=length, shrink=shrink, default=rc["colorbar.insetlength"]
1931
+ ) # noqa: E501
1932
+ width = _not_none(width, rc["colorbar.insetwidth"])
1933
+ pad = _not_none(pad, rc["colorbar.insetpad"])
1934
+ length = units(length, "em", "ax", axes=self, width=True) # x direction
1935
+ width = units(width, "em", "ax", axes=self, width=False) # y direction
1936
+ xpad = units(pad, "em", "ax", axes=self, width=True)
1937
+ ypad = units(pad, "em", "ax", axes=self, width=False)
1938
+
1939
+ # Extra space accounting for colorbar label and tick labels
1940
+ labspace = rc["xtick.major.size"] / 72
1941
+ fontsize = rc["xtick.labelsize"]
1942
+ fontsize = _fontsize_to_pt(fontsize)
1943
+ if label is not None:
1944
+ labspace += 2.4 * fontsize / 72
1945
+ else:
1946
+ labspace += 1.2 * fontsize / 72
1947
+ labspace /= self._get_size_inches()[1] # space for labels
1948
+
1949
+ # Location in axes-relative coordinates
1950
+ # Bounds are x0, y0, width, height in axes-relative coordinates
1951
+ if loc == "upper right":
1952
+ bounds_inset = [1 - xpad - length, 1 - ypad - width]
1953
+ bounds_frame = [1 - 2 * xpad - length, 1 - 2 * ypad - width - labspace]
1954
+ elif loc == "upper left":
1955
+ bounds_inset = [xpad, 1 - ypad - width]
1956
+ bounds_frame = [0, 1 - 2 * ypad - width - labspace]
1957
+ elif loc == "lower left":
1958
+ bounds_inset = [xpad, ypad + labspace]
1959
+ bounds_frame = [0, 0]
1960
+ else:
1961
+ bounds_inset = [1 - xpad - length, ypad + labspace]
1962
+ bounds_frame = [1 - 2 * xpad - length, 0]
1963
+ bounds_inset.extend((length, width)) # inset axes
1964
+ bounds_frame.extend((2 * xpad + length, 2 * ypad + width + labspace))
1965
+
1966
+ # Make axes and frame with zorder matching default legend zorder
1967
+ cls = mproj.get_projection_class("ultraplot_cartesian")
1968
+ locator = self._make_inset_locator(bounds_inset, self.transAxes)
1969
+ ax = cls(self.figure, locator(self, None).bounds, zorder=5)
1970
+ ax.patch.set_facecolor("none")
1971
+ ax.set_axes_locator(locator)
1972
+ self.add_child_axes(ax)
1973
+ kw_frame, kwargs = self._parse_frame("colorbar", **kwargs)
1974
+ if frame:
1975
+ frame = self._add_guide_frame(*bounds_frame, fontsize=fontsize, **kw_frame)
1976
+
1977
+ # Handle default keyword args
1978
+ if orientation is not None and orientation != "horizontal":
1979
+ warnings._warn_ultraplot(
1980
+ f"Orientation for inset colorbars must be horizontal. "
1981
+ f"Ignoring orientation={orientation!r}."
1982
+ )
1983
+ ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation)
1984
+ if ticklocation is not None and ticklocation != "bottom":
1985
+ warnings._warn_ultraplot("Inset colorbars can only have ticks on the bottom.")
1986
+ kwargs.update({"orientation": "horizontal", "ticklocation": "bottom"})
1987
+ return ax, kwargs
1988
+
1989
+ def _parse_legend_aligned(self, pairs, ncol=None, order=None, **kwargs):
1990
+ """
1991
+ Draw an individual legend with aligned columns. Includes support
1992
+ for switching legend-entries between column-major and row-major.
1993
+ """
1994
+ # Potentially change the order of handles to column-major
1995
+ npairs = len(pairs)
1996
+ ncol = _not_none(ncol, 3)
1997
+ nrow = npairs // ncol + 1
1998
+ array = np.empty((nrow, ncol), dtype=object)
1999
+ for i, pair in enumerate(pairs):
2000
+ array.flat[i] = pair # must be assigned individually
2001
+ if order == "C":
2002
+ array = array.T
2003
+
2004
+ # Return a legend
2005
+ # NOTE: Permit drawing empty legend to catch edge cases
2006
+ pairs = [pair for pair in array.flat if isinstance(pair, tuple)]
2007
+ args = tuple(zip(*pairs)) or ([], [])
2008
+ return mlegend.Legend(self, *args, ncol=ncol, **kwargs)
2009
+
2010
+ def _parse_legend_centered(
2011
+ self,
2012
+ pairs,
2013
+ *,
2014
+ fontsize,
2015
+ loc=None,
2016
+ title=None,
2017
+ frameon=None,
2018
+ kw_frame=None,
2019
+ **kwargs,
2020
+ ):
2021
+ """
2022
+ Draw "legend" with centered rows by creating separate legends for
2023
+ each row. The label spacing/border spacing will be exactly replicated.
2024
+ """
2025
+ # Parse input args
2026
+ # NOTE: Main legend() function applies default 'legend.loc' of 'best' when
2027
+ # users pass legend=True or call legend without 'loc'. Cannot issue warning.
2028
+ kw_frame = kw_frame or {}
2029
+ kw_frame["fontsize"] = fontsize
2030
+ if loc is None or loc == "best": # white lie
2031
+ loc = "upper center"
2032
+ if not isinstance(loc, str):
2033
+ raise ValueError(
2034
+ f"Invalid loc={loc!r} for centered-row legend. Must be string."
2035
+ )
2036
+ keys = ("bbox_transform", "bbox_to_anchor")
2037
+ kw_ignore = {key: kwargs.pop(key) for key in keys if key in kwargs}
2038
+ if kw_ignore:
2039
+ warnings._warn_ultraplot(
2040
+ f"Ignoring invalid centered-row legend keyword args: {kw_ignore!r}"
2041
+ )
2042
+
2043
+ # Iterate and draw
2044
+ # NOTE: Empirical testing shows spacing fudge factor necessary to
2045
+ # exactly replicate the spacing of standard aligned legends.
2046
+ # NOTE: We confine possible bounding box in *y*-direction, but do not
2047
+ # confine it in *x*-direction. Matplotlib will automatically move
2048
+ # left-to-right if you request this.
2049
+ legs = []
2050
+ kwargs.update({"loc": loc, "frameon": False})
2051
+ space = kwargs.get("labelspacing", None) or rc["legend.labelspacing"]
2052
+ height = (((1 + space * 0.85) * fontsize) / 72) / self._get_size_inches()[1]
2053
+ for i, ipairs in enumerate(pairs):
2054
+ extra = int(i > 0 and title is not None)
2055
+ if "upper" in loc:
2056
+ base, offset = 1, -extra
2057
+ elif "lower" in loc:
2058
+ base, offset = 0, len(pairs)
2059
+ else: # center
2060
+ base, offset = 0.5, 0.5 * (len(pairs) - extra)
2061
+ y0, y1 = base + (offset - np.array([i + 1, i])) * height
2062
+ bb = mtransforms.Bbox([[0, y0], [1, y1]])
2063
+ leg = mlegend.Legend(
2064
+ self,
2065
+ *zip(*ipairs),
2066
+ bbox_to_anchor=bb,
2067
+ bbox_transform=self.transAxes,
2068
+ ncol=len(ipairs),
2069
+ title=title if i == 0 else None,
2070
+ **kwargs,
2071
+ )
2072
+ legs.append(leg)
2073
+
2074
+ # Draw manual fancy bounding box for un-aligned legend
2075
+ # WARNING: legendPatch uses the default transform, i.e. universal coordinates
2076
+ # in points. Means we have to transform mutation scale into transAxes sizes.
2077
+ # WARNING: Tempting to use legendPatch for everything but for some reason
2078
+ # coordinates are messed up. In some tests all coordinates were just result
2079
+ # of get window extent multiplied by 2 (???). Anyway actual box is found in
2080
+ # _legend_box attribute, which is accessed by get_window_extent.
2081
+ objs = tuple(legs)
2082
+ if frameon and legs:
2083
+ rend = self.figure._get_renderer() # arbitrary renderer
2084
+ trans = self.transAxes.inverted()
2085
+ bboxes = [leg.get_window_extent(rend).transformed(trans) for leg in legs]
2086
+ bb = mtransforms.Bbox.union(bboxes)
2087
+ bounds = (bb.xmin, bb.ymin, bb.xmax - bb.xmin, bb.ymax - bb.ymin)
2088
+ self._add_guide_frame(*bounds, **kw_frame)
2089
+ return objs
2090
+
2091
+ @staticmethod
2092
+ def _parse_legend_group(handles, labels=None):
2093
+ """
2094
+ Parse possibly tuple-grouped input handles.
2095
+ """
2096
+
2097
+ # Helper function. Retrieve labels from a tuple group or from objects
2098
+ # in a container. Multiple labels lead to multiple legend entries.
2099
+ def _legend_label(*objs): # noqa: E301
2100
+ labs = []
2101
+ for obj in objs:
2102
+ if hasattr(obj, "get_label"): # e.g. silent list
2103
+ lab = obj.get_label()
2104
+ if lab is not None and str(lab)[:1] != "_":
2105
+ labs.append(lab)
2106
+ return tuple(labs)
2107
+
2108
+ # Helper function. Translate handles in the input tuple group. Extracts
2109
+ # legend handles from contour sets and extracts labeled elements from
2110
+ # matplotlib containers (important for histogram plots).
2111
+ ignore = (mcontainer.ErrorbarContainer,)
2112
+ containers = (cbook.silent_list, mcontainer.Container)
2113
+
2114
+ def _legend_tuple(*objs): # noqa: E306
2115
+ handles = []
2116
+ for obj in objs:
2117
+ if isinstance(obj, ignore) and not _legend_label(obj):
2118
+ continue
2119
+ if hasattr(obj, "update_scalarmappable"): # for e.g. pcolor
2120
+ obj.update_scalarmappable()
2121
+ if isinstance(obj, mcontour.ContourSet): # extract single element
2122
+ hs, _ = obj.legend_elements()
2123
+ label = getattr(obj, "_legend_label", "_no_label")
2124
+ if hs: # non-empty
2125
+ obj = hs[len(hs) // 2]
2126
+ obj.set_label(label)
2127
+ if isinstance(obj, containers): # extract labeled elements
2128
+ hs = (obj, *guides._iter_iterables(obj))
2129
+ hs = tuple(filter(_legend_label, hs))
2130
+ if hs:
2131
+ handles.extend(hs)
2132
+ elif obj: # fallback to first element
2133
+ handles.append(obj[0])
2134
+ else:
2135
+ handles.append(obj)
2136
+ elif hasattr(obj, "get_label"):
2137
+ handles.append(obj)
2138
+ else:
2139
+ warnings._warn_ultraplot(f"Ignoring invalid legend handle {obj!r}.")
2140
+ return tuple(handles)
2141
+
2142
+ # Sanitize labels. Ignore e.g. extra hist() or hist2d() return values,
2143
+ # auto-detect labels in tuple group, auto-expand tuples with diff labels
2144
+ # NOTE: Allow handles and labels of different length like
2145
+ # native matplotlib. Just truncate extra values with zip().
2146
+ if labels is None:
2147
+ labels = [None] * len(handles)
2148
+ ihandles, ilabels = [], []
2149
+ for hs, label in zip(handles, labels):
2150
+ # Filter objects
2151
+ if type(hs) is not tuple: # ignore Containers (tuple subclasses)
2152
+ hs = (hs,)
2153
+ hs = _legend_tuple(*hs)
2154
+ labs = _legend_label(*hs)
2155
+ if not hs:
2156
+ continue
2157
+ # Unfurl tuple of handles
2158
+ if label is None and len(labs) > 1:
2159
+ hs = tuple(filter(_legend_label, hs))
2160
+ ihandles.extend(hs)
2161
+ ilabels.extend(_.get_label() for _ in hs)
2162
+ # Append this handle with some name
2163
+ else:
2164
+ hs = hs[0] if len(hs) == 1 else hs # unfurl for better error messages
2165
+ label = label if label is not None else labs[0] if labs else "_no_label"
2166
+ ihandles.append(hs)
2167
+ ilabels.append(label)
2168
+ return ihandles, ilabels
2169
+
2170
+ def _parse_legend_handles(
2171
+ self,
2172
+ handles,
2173
+ labels,
2174
+ ncol=None,
2175
+ order=None,
2176
+ center=None,
2177
+ alphabetize=None,
2178
+ handler_map=None,
2179
+ ):
2180
+ """
2181
+ Parse input handles and labels.
2182
+ """
2183
+ # Handle lists of lists
2184
+ # TODO: Often desirable to label a "mappable" with one data value. Maybe add a
2185
+ # legend option for the *number of samples* or *sample points* when drawing
2186
+ # legends for mappables. Look into "legend handlers", might just want to add
2187
+ # handlers by passing handler_map to legend() and get_legend_handles_labels().
2188
+ is_list = lambda obj: ( # noqa: E731
2189
+ np.iterable(obj) and not isinstance(obj, (str, tuple))
2190
+ )
2191
+ to_list = lambda obj: ( # noqa: E731
2192
+ obj.tolist()
2193
+ if isinstance(obj, np.ndarray)
2194
+ else obj if obj is None or is_list(obj) else [obj]
2195
+ )
2196
+ handles, labels = to_list(handles), to_list(labels)
2197
+ if handles and not labels and all(isinstance(h, str) for h in handles):
2198
+ handles, labels = labels, handles
2199
+ multi = any(is_list(h) and len(h) > 1 for h in (handles or ()))
2200
+ if multi and order == "F":
2201
+ warnings._warn_ultraplot(
2202
+ "Column-major ordering of legend handles is not supported "
2203
+ "for horizontally-centered legends."
2204
+ )
2205
+ if multi and ncol is not None:
2206
+ warnings._warn_ultraplot(
2207
+ "Detected list of *lists* of legend handles. Ignoring "
2208
+ 'the user input property "ncol".'
2209
+ )
2210
+ if labels and not handles:
2211
+ warnings._warn_ultraplot(
2212
+ "Passing labels without handles is unsupported in ultraplot. "
2213
+ "Please explicitly pass the handles to legend() or pass labels "
2214
+ "to plotting commands with e.g. plot(data_1d, label='label') or "
2215
+ "plot(data_2d, labels=['label1', 'label2', ...]). After passing "
2216
+ "labels to plotting commands you can call legend() without any "
2217
+ "arguments or with the handles as a sole positional argument."
2218
+ )
2219
+ ncol = _not_none(ncol, 3)
2220
+ center = _not_none(center, multi)
2221
+
2222
+ # Iterate over each sublist and parse independently
2223
+ pairs = []
2224
+ if not multi: # temporary
2225
+ handles, labels = [handles], [labels]
2226
+ elif labels is None:
2227
+ labels = [labels] * len(handles)
2228
+ for ihandles, ilabels in zip(handles, labels):
2229
+ ihandles, ilabels = to_list(ihandles), to_list(ilabels)
2230
+ if ihandles is None:
2231
+ ihandles = self._get_legend_handles(handler_map)
2232
+ ihandles, ilabels = self._parse_legend_group(ihandles, ilabels)
2233
+ ipairs = list(zip(ihandles, ilabels))
2234
+ if alphabetize:
2235
+ ipairs = sorted(ipairs, key=lambda pair: pair[1])
2236
+ pairs.append(ipairs)
2237
+
2238
+ # Manage (handle, label) pairs in context of the 'center' option
2239
+ if not multi:
2240
+ pairs = pairs[0]
2241
+ if center:
2242
+ multi = True
2243
+ pairs = [pairs[i * ncol : (i + 1) * ncol] for i in range(len(pairs))]
2244
+ else:
2245
+ if not center: # standardize format based on input
2246
+ multi = False # no longer is list of lists
2247
+ pairs = [pair for ipairs in pairs for pair in ipairs]
2248
+
2249
+ if multi:
2250
+ pairs = [ipairs for ipairs in pairs if ipairs]
2251
+ return pairs, multi
2252
+
2253
+ def _range_subplotspec(self, s):
2254
+ """
2255
+ Return the column or row range for the subplotspec.
2256
+ """
2257
+ if not isinstance(self, maxes.SubplotBase):
2258
+ raise RuntimeError("Axes must be a subplot.")
2259
+ ss = self.get_subplotspec().get_topmost_subplotspec()
2260
+ row1, row2, col1, col2 = ss._get_rows_columns()
2261
+ if s == "x":
2262
+ return (col1, col2)
2263
+ else:
2264
+ return (row1, row2)
2265
+
2266
+ def _range_tightbbox(self, s):
2267
+ """
2268
+ Return the tight bounding box span from the cached bounding box.
2269
+ """
2270
+ # TODO: Better testing for axes visibility
2271
+ bbox = self._tight_bbox
2272
+ if bbox is None:
2273
+ return np.nan, np.nan
2274
+ if s == "x":
2275
+ return bbox.xmin, bbox.xmax
2276
+ else:
2277
+ return bbox.ymin, bbox.ymax
2278
+
2279
+ def _sharex_setup(self, sharex, **kwargs):
2280
+ """
2281
+ Configure x-axis sharing for panels. See also `~CartesianAxes._sharex_setup`.
2282
+ """
2283
+ self._share_short_axis(sharex, "left", **kwargs) # x axis of left panels
2284
+ self._share_short_axis(sharex, "right", **kwargs)
2285
+ self._share_long_axis(sharex, "bottom", **kwargs) # x axis of bottom panels
2286
+ self._share_long_axis(sharex, "top", **kwargs)
2287
+
2288
+ def _sharey_setup(self, sharey, **kwargs):
2289
+ """
2290
+ Configure y-axis sharing for panels. See also `~CartesianAxes._sharey_setup`.
2291
+ """
2292
+ self._share_short_axis(sharey, "bottom", **kwargs) # y axis of bottom panels
2293
+ self._share_short_axis(sharey, "top", **kwargs)
2294
+ self._share_long_axis(sharey, "left", **kwargs) # y axis of left panels
2295
+ self._share_long_axis(sharey, "right", **kwargs)
2296
+
2297
+ def _share_short_axis(self, share, side, **kwargs):
2298
+ """
2299
+ Share the "short" axes of panels in this subplot with other panels.
2300
+ """
2301
+ if share is None or self._panel_side:
2302
+ return # if this is a panel
2303
+ s = "x" if side in ("left", "right") else "y"
2304
+ caxs = self._panel_dict[side]
2305
+ paxs = share._panel_dict[side]
2306
+ caxs = [pax for pax in caxs if not pax._panel_hidden]
2307
+ paxs = [pax for pax in paxs if not pax._panel_hidden]
2308
+ for cax, pax in zip(caxs, paxs): # may be uneven
2309
+ getattr(cax, f"_share{s}_setup")(pax, **kwargs)
2310
+
2311
+ def _share_long_axis(self, share, side, **kwargs):
2312
+ """
2313
+ Share the "long" axes of panels in this subplot with other panels.
2314
+ """
2315
+ # NOTE: We do not check _panel_share because that only controls
2316
+ # sharing with main subplot, not other subplots
2317
+ if share is None or self._panel_side:
2318
+ return # if this is a panel
2319
+ s = "x" if side in ("top", "bottom") else "y"
2320
+ paxs = self._panel_dict[side]
2321
+ paxs = [pax for pax in paxs if not pax._panel_hidden]
2322
+ for pax in paxs:
2323
+ getattr(pax, f"_share{s}_setup")(share, **kwargs)
2324
+
2325
+ def _reposition_subplot(self):
2326
+ """
2327
+ Reposition the subplot axes.
2328
+ """
2329
+ # WARNING: In later versions self.numRows, self.numCols, and self.figbox
2330
+ # are @property definitions that never go stale but in mpl < 3.4 they are
2331
+ # attributes that must be updated explicitly with update_params().
2332
+ # WARNING: In early versions matplotlib only removes '_layoutbox' and
2333
+ # '_poslayoutbox' when calling public set_position but in later versions it
2334
+ # calls set_in_layout(False) which removes children from get_tightbbox().
2335
+ # Therefore try to use _set_position() even though it is private
2336
+ if not isinstance(self, maxes.SubplotBase):
2337
+ raise RuntimeError("Axes must be a subplot.")
2338
+ setter = getattr(self, "_set_position", self.set_position)
2339
+ if _version_mpl >= "3.4":
2340
+ setter(self.get_subplotspec().get_position(self.figure))
2341
+ else:
2342
+ self.update_params()
2343
+ setter(self.figbox) # equivalent to above
2344
+
2345
+ def _update_abc(self, **kwargs):
2346
+ """
2347
+ Update the a-b-c label.
2348
+ """
2349
+ # Properties
2350
+ # NOTE: Border props only apply for "inner" title locations so we need to
2351
+ # store on the axes whenever they are modified in case the current location
2352
+ # is an 'outer' location then re-apply in case 'loc' is subsequently changed
2353
+ kw = rc.fill(
2354
+ {
2355
+ "size": "abc.size",
2356
+ "weight": "abc.weight",
2357
+ "color": "abc.color",
2358
+ "family": "font.family",
2359
+ },
2360
+ context=True,
2361
+ )
2362
+ kwb = rc.fill(
2363
+ {
2364
+ "border": "abc.border",
2365
+ "borderwidth": "abc.borderwidth",
2366
+ "bbox": "abc.bbox",
2367
+ "bboxpad": "abc.bboxpad",
2368
+ "bboxcolor": "abc.bboxcolor",
2369
+ "bboxstyle": "abc.bboxstyle",
2370
+ "bboxalpha": "abc.bboxalpha",
2371
+ },
2372
+ context=True,
2373
+ )
2374
+ self._abc_border_kwargs.update(kwb)
2375
+
2376
+ # A-b-c labels. Build as a...z...aa...zz...aaa...zzz
2377
+ # NOTE: The abc string should already be validated here
2378
+ abc = rc.find("abc", context=True) # 1st run, or changed
2379
+ if abc is True:
2380
+ abc = "a"
2381
+ if abc is False:
2382
+ abc = ""
2383
+ if abc is None or self.number is None:
2384
+ pass
2385
+ elif isinstance(abc, str):
2386
+ nabc, iabc = divmod(self.number - 1, 26)
2387
+ if abc: # should have been validated to contain 'a' or 'A'
2388
+ old = re.search("[aA]", abc).group() # return first occurrence
2389
+ new = (nabc + 1) * ABC_STRING[iabc]
2390
+ new = new.upper() if old == "A" else new
2391
+ abc = abc.replace(old, new, 1) # replace first occurrence
2392
+ kw["text"] = abc
2393
+ else:
2394
+ if self.number > len(abc):
2395
+ raise ValueError(
2396
+ f"Invalid abc list length {len(abc)} "
2397
+ f"for axes with number {self.number}."
2398
+ )
2399
+ else:
2400
+ kw["text"] = abc[self._number - 1]
2401
+
2402
+ # Update a-b-c label
2403
+ loc = rc.find("abc.loc", context=True)
2404
+ loc = self._abc_loc = _translate_loc(loc or self._abc_loc, "text")
2405
+ if loc not in ("left", "right", "center"):
2406
+ kw.update(self._abc_border_kwargs)
2407
+ kw.update(kwargs)
2408
+ self._title_dict["abc"].update(kw)
2409
+
2410
+ def _update_title(self, loc, title=None, **kwargs):
2411
+ """
2412
+ Update the title at the specified location.
2413
+ """
2414
+ # Titles, with two workflows here:
2415
+ # 1. title='name' and titleloc='position'
2416
+ # 2. ltitle='name', rtitle='name', etc., arbitrarily many titles
2417
+ # NOTE: This always updates the *current* title and deflection to panels
2418
+ # is handled later so that titles set with set_title() are deflected too.
2419
+ # See notes in _update_super_labels() and _apply_title_above().
2420
+ # NOTE: Matplotlib added axes.titlecolor in version 3.2 but we still use
2421
+ # custom title.size, title.weight, title.color properties for retroactive
2422
+ # support in older matplotlib versions. First get params and update kwargs.
2423
+ kw = rc.fill(
2424
+ {
2425
+ "size": "title.size",
2426
+ "weight": "title.weight",
2427
+ "color": "title.color",
2428
+ "family": "font.family",
2429
+ },
2430
+ context=True,
2431
+ )
2432
+ if "color" in kw and kw["color"] == "auto":
2433
+ del kw["color"] # WARNING: matplotlib permits invalid color here
2434
+ kwb = rc.fill(
2435
+ {
2436
+ "border": "title.border",
2437
+ "borderwidth": "title.borderwidth",
2438
+ "bbox": "title.bbox",
2439
+ "bboxpad": "title.bboxpad",
2440
+ "bboxcolor": "title.bboxcolor",
2441
+ "bboxstyle": "title.bboxstyle",
2442
+ "bboxalpha": "title.bboxalpha",
2443
+ },
2444
+ context=True,
2445
+ )
2446
+ self._title_border_kwargs.update(kwb)
2447
+
2448
+ # Update the padding settings read at drawtime. Make sure to
2449
+ # update them on the panel axes if 'title.above' is active.
2450
+ pad = rc.find("abc.titlepad", context=True)
2451
+ if pad is not None:
2452
+ self._abc_title_pad = pad
2453
+ pad = rc.find("title.pad", context=True) # title
2454
+ if pad is not None:
2455
+ self._title_pad = pad
2456
+ self._set_title_offset_trans(pad)
2457
+
2458
+ # Get the title location. If 'titleloc' was used then transfer text
2459
+ # from the old location to the new location.
2460
+ if loc is not None:
2461
+ loc = _translate_loc(loc, "text")
2462
+ else:
2463
+ old = self._title_loc
2464
+ loc = rc.find("title.loc", context=True)
2465
+ loc = self._title_loc = _translate_loc(loc or self._title_loc, "text")
2466
+ if loc != old and old is not None:
2467
+ labels._transfer_label(self._title_dict[old], self._title_dict[loc])
2468
+
2469
+ # Update the title text. For outer panels, add text to the panel if
2470
+ # necesssary. For inner panels, use the border and bbox settings.
2471
+ if loc not in ("left", "right", "center"):
2472
+ kw.update(self._title_border_kwargs)
2473
+ if title is None:
2474
+ pass
2475
+ elif isinstance(title, str):
2476
+ kw["text"] = title
2477
+ elif np.iterable(title) and all(isinstance(_, str) for _ in title):
2478
+ if self.number is None:
2479
+ pass
2480
+ elif self.number > len(title):
2481
+ raise ValueError(
2482
+ f"Invalid title list length {len(title)} "
2483
+ f"for axes with number {self.number}."
2484
+ )
2485
+ else:
2486
+ kw["text"] = title[self.number - 1]
2487
+ else:
2488
+ raise ValueError(f"Invalid title {title!r}. Must be string(s).")
2489
+ kw.update(kwargs)
2490
+ self._title_dict[loc].update(kw)
2491
+
2492
+ def _update_title_position(self, renderer):
2493
+ """
2494
+ Update the position of inset titles and outer titles. This is called
2495
+ by matplotlib at drawtime.
2496
+ """
2497
+ # Update title positions
2498
+ # NOTE: Critical to do this every time in case padding changes or
2499
+ # we added or removed an a-b-c label in the same position as a title
2500
+ width, height = self._get_size_inches()
2501
+ x_pad = self._title_pad / (72 * width)
2502
+ y_pad = self._title_pad / (72 * height)
2503
+ for loc, obj in self._title_dict.items():
2504
+ x, y = (0, 1)
2505
+ if loc == "abc": # redirect
2506
+ loc = self._abc_loc
2507
+ if loc == "left":
2508
+ x = 0
2509
+ elif loc == "center":
2510
+ x = 0.5
2511
+ elif loc == "right":
2512
+ x = 1
2513
+ if loc in ("upper center", "lower center"):
2514
+ x = 0.5
2515
+ elif loc in ("upper left", "lower left"):
2516
+ x = x_pad
2517
+ elif loc in ("upper right", "lower right"):
2518
+ x = 1 - x_pad
2519
+ if loc in ("upper left", "upper right", "upper center"):
2520
+ y = 1 - y_pad
2521
+ elif loc in ("lower left", "lower right", "lower center"):
2522
+ y = y_pad
2523
+ obj.set_position((x, y))
2524
+
2525
+ # Get title padding. Push title above tick marks since matplotlib ignores them.
2526
+ # This is known matplotlib problem but especially annoying with top panels.
2527
+ # NOTE: See axis.get_ticks_position for inspiration
2528
+ pad = self._title_pad
2529
+ abcpad = self._abc_title_pad
2530
+ if self.xaxis.get_visible() and any(
2531
+ tick.tick2line.get_visible() and not tick.label2.get_visible()
2532
+ for tick in self.xaxis.majorTicks
2533
+ ):
2534
+ pad += self.xaxis.get_tick_padding()
2535
+
2536
+ # Avoid applying padding on every draw in case it is expensive to change
2537
+ # the title Text transforms every time.
2538
+ pad_current = self._title_pad_current
2539
+ if pad_current is None or not np.isclose(pad, pad_current):
2540
+ self._title_pad_current = pad
2541
+ self._set_title_offset_trans(pad)
2542
+
2543
+ # Adjust the above-axes positions with builtin algorithm
2544
+ # WARNING: Make sure the name of this private function doesn't change
2545
+ super()._update_title_position(renderer)
2546
+
2547
+ # Sync the title position with the a-b-c label position
2548
+ aobj = self._title_dict["abc"]
2549
+ tobj = self._title_dict[self._abc_loc]
2550
+ aobj.set_transform(tobj.get_transform())
2551
+ aobj.set_position(tobj.get_position())
2552
+ aobj.set_ha(tobj.get_ha())
2553
+ aobj.set_va(tobj.get_va())
2554
+
2555
+ # Offset title away from a-b-c label
2556
+ # NOTE: Title texts all use axes transform in x-direction
2557
+ if not tobj.get_text() or not aobj.get_text():
2558
+ return
2559
+ awidth, twidth = (
2560
+ obj.get_window_extent(renderer).transformed(self.transAxes.inverted()).width
2561
+ for obj in (aobj, tobj)
2562
+ )
2563
+ ha = aobj.get_ha()
2564
+ pad = (abcpad / 72) / self._get_size_inches()[0]
2565
+ aoffset = toffset = 0
2566
+ if ha == "left":
2567
+ toffset = awidth + pad
2568
+ elif ha == "right":
2569
+ aoffset = -(twidth + pad)
2570
+ else: # guaranteed center, there are others
2571
+ toffset = 0.5 * (awidth + pad)
2572
+ aoffset = -0.5 * (twidth + pad)
2573
+ aobj.set_x(aobj.get_position()[0] + aoffset)
2574
+ tobj.set_x(tobj.get_position()[0] + toffset)
2575
+
2576
+ def _update_super_title(self, suptitle=None, **kwargs):
2577
+ """
2578
+ Update the figure super title.
2579
+ """
2580
+ # NOTE: This is actually *figure-wide* setting, but that line gets blurred
2581
+ # where we have shared axes, spanning labels, etc. May cause redundant
2582
+ # assignments if using SubplotGrid.format() but this is fast so nbd.
2583
+ if self.number is None:
2584
+ # NOTE: Kludge prevents changed *figure-wide* settings from getting
2585
+ # overwritten when user makes a new panels or insets. Funky limitation but
2586
+ # kind of makes sense to make these inaccessible from panels.
2587
+ return
2588
+ kw = rc.fill(
2589
+ {
2590
+ "size": "suptitle.size",
2591
+ "weight": "suptitle.weight",
2592
+ "color": "suptitle.color",
2593
+ "family": "font.family",
2594
+ },
2595
+ context=True,
2596
+ )
2597
+ kw.update(kwargs)
2598
+ if suptitle or kw:
2599
+ self.figure._update_super_title(suptitle, **kw)
2600
+
2601
+ def _update_super_labels(self, side, labels=None, **kwargs):
2602
+ """
2603
+ Update the figure super labels.
2604
+ """
2605
+ fig = self.figure
2606
+ if self.number is None:
2607
+ return # NOTE: see above
2608
+ kw = rc.fill(
2609
+ {
2610
+ "color": side + "label.color",
2611
+ "rotation": side + "label.rotation",
2612
+ "size": side + "label.size",
2613
+ "weight": side + "label.weight",
2614
+ "family": "font.family",
2615
+ },
2616
+ context=True,
2617
+ )
2618
+ kw.update(kwargs)
2619
+ if labels or kw:
2620
+ fig._update_super_labels(side, labels, **kw)
2621
+
2622
+
2623
+ @staticmethod
2624
+ def get_center_of_axes(axes = None):
2625
+ positions = [ax.get_position() for ax in axes]
2626
+ # get the outermost coordinates
2627
+ box = mtransforms.Bbox.from_extents(
2628
+ min(p.bounds[0] for p in positions),
2629
+ min(p.bounds[1] for p in positions),
2630
+ max(p.bounds[0] + p.bounds[2] for p in positions),
2631
+ max(p.bounds[1] + p.bounds[3] for p in positions)
2632
+ )
2633
+ return box
2634
+
2635
+
2636
+
2637
+ def _update_share_labels(self, axes=None, target="x"):
2638
+ """Update shared axis labels for a group of axes.
2639
+
2640
+ Parameters
2641
+ ----------
2642
+ axes : list of int or list of Axes, optional
2643
+ The axes indices or Axes objects to share labels between
2644
+ target : {'x', 'y'}, optional
2645
+ Which axis labels to share ('x' for x-axis, 'y' for y-axis)
2646
+ """
2647
+ if not axes:
2648
+ return
2649
+
2650
+ # Convert indices to actual axes objects
2651
+ if isinstance(axes[0], int):
2652
+ axes = [self.figure.axes[i] for i in axes]
2653
+
2654
+ # Get the center position of the axes group
2655
+ if box := self.get_center_of_axes(axes):
2656
+ # Reuse existing label if possible
2657
+ if target == 'x':
2658
+ label = axes[0].xaxis.label
2659
+ # Update position and properties
2660
+ label.set_position((
2661
+ (box.bounds[0] + box.bounds[2])/2,
2662
+ box.bounds[1]
2663
+ ))
2664
+ else: # y-axis
2665
+ label = axes[0].yaxis.label
2666
+ # Update position and properties
2667
+ label.set_position((
2668
+ box.bounds[0],
2669
+ (box.bounds[1] + box.bounds[3])/2
2670
+ ))
2671
+
2672
+ label.set_ha('center')
2673
+ label.set_va('center')
2674
+
2675
+ # Share the same label object across all axes
2676
+ # Skip first axes since we used its label
2677
+ for ax in axes[1:]:
2678
+ if target == 'x':
2679
+ ax.xaxis.label = label
2680
+ else:
2681
+ ax.yaxis.label = label
2682
+
2683
+
2684
+ @docstring._snippet_manager
2685
+ def format(
2686
+ self,
2687
+ *,
2688
+ title=None,
2689
+ title_kw=None,
2690
+ abc_kw=None,
2691
+ ltitle=None,
2692
+ lefttitle=None,
2693
+ ctitle=None,
2694
+ centertitle=None,
2695
+ rtitle=None,
2696
+ righttitle=None,
2697
+ ultitle=None,
2698
+ upperlefttitle=None,
2699
+ uctitle=None,
2700
+ uppercentertitle=None,
2701
+ urtitle=None,
2702
+ upperrighttitle=None,
2703
+ lltitle=None,
2704
+ lowerlefttitle=None,
2705
+ lctitle=None,
2706
+ lowercentertitle=None,
2707
+ lrtitle=None,
2708
+ lowerrighttitle=None,
2709
+ share_xlabels = None,
2710
+ share_ylabels = None,
2711
+ **kwargs,
2712
+ ):
2713
+ """
2714
+ Modify the a-b-c label, axes title(s), and background patch,
2715
+ and call `ultraplot.figure.Figure.format` on the axes figure.
2716
+
2717
+ Parameters
2718
+ ----------
2719
+ %(axes.format)s
2720
+
2721
+ Important
2722
+ ---------
2723
+ `abc`, `abcloc`, `titleloc`, `titleabove`, `titlepad`, and
2724
+ `abctitlepad` are actually :ref:`configuration settings <ug_config>`.
2725
+ We explicitly document these arguments here because it is common to
2726
+ change them for specific axes. But many :ref:`other configuration
2727
+ settings <ug_format>` can be passed to ``format`` too.
2728
+
2729
+ Other parameters
2730
+ ----------------
2731
+ %(figure.format)s
2732
+ %(rc.format)s
2733
+
2734
+ See also
2735
+ --------
2736
+ ultraplot.axes.CartesianAxes.format
2737
+ ultraplot.axes.PolarAxes.format
2738
+ ultraplot.axes.GeoAxes.format
2739
+ ultraplot.figure.Figure.format
2740
+ ultraplot.gridspec.SubplotGrid.format
2741
+ ultraplot.config.Configurator.context
2742
+ """
2743
+ skip_figure = kwargs.pop("skip_figure", False) # internal keyword arg
2744
+ params = _pop_params(kwargs, self.figure._format_signature)
2745
+
2746
+ # Initiate context block
2747
+ rc_kw, rc_mode = _pop_rc(kwargs)
2748
+ with rc.context(rc_kw, mode=rc_mode):
2749
+ # Behavior of titles in presence of panels
2750
+ above = rc.find("title.above", context=True)
2751
+ if above is not None:
2752
+ self._title_above = above # used for future titles
2753
+
2754
+
2755
+ # Update a-b-c label and titles
2756
+ abc_kw = abc_kw or {}
2757
+ title_kw = title_kw or {}
2758
+ self._update_share_labels(share_xlabels, target = 'x')
2759
+ self._update_share_labels(share_ylabels, target = 'y')
2760
+ self._update_abc(**abc_kw)
2761
+ self._update_title(None, title, **title_kw)
2762
+ self._update_title(
2763
+ "left",
2764
+ _not_none(ltitle=ltitle, lefttitle=lefttitle),
2765
+ **title_kw,
2766
+ )
2767
+ self._update_title(
2768
+ "center",
2769
+ _not_none(ctitle=ctitle, centertitle=centertitle),
2770
+ **title_kw,
2771
+ )
2772
+ self._update_title(
2773
+ "right",
2774
+ _not_none(rtitle=rtitle, righttitle=righttitle),
2775
+ **title_kw,
2776
+ )
2777
+ self._update_title(
2778
+ "upper left",
2779
+ _not_none(ultitle=ultitle, upperlefttitle=upperlefttitle),
2780
+ **title_kw,
2781
+ )
2782
+ self._update_title(
2783
+ "upper center",
2784
+ _not_none(uctitle=uctitle, uppercentertitle=uppercentertitle),
2785
+ **title_kw,
2786
+ )
2787
+ self._update_title(
2788
+ "upper right",
2789
+ _not_none(urtitle=urtitle, upperrighttitle=upperrighttitle),
2790
+ **title_kw,
2791
+ )
2792
+ self._update_title(
2793
+ "lower left",
2794
+ _not_none(lltitle=lltitle, lowerlefttitle=lowerlefttitle),
2795
+ **title_kw,
2796
+ )
2797
+ self._update_title(
2798
+ "lower center",
2799
+ _not_none(lctitle=lctitle, lowercentertitle=lowercentertitle),
2800
+ **title_kw,
2801
+ )
2802
+ self._update_title(
2803
+ "lower right",
2804
+ _not_none(lrtitle=lrtitle, lowerrighttitle=lowerrighttitle),
2805
+ **title_kw,
2806
+ )
2807
+
2808
+ # Update the axes style
2809
+ # NOTE: This will also raise an error if unknown args are encountered
2810
+ cycle = rc.find("axes.prop_cycle", context=True)
2811
+ if cycle is not None:
2812
+ self.set_prop_cycle(cycle)
2813
+ self._update_background(**kwargs)
2814
+
2815
+ # Update super labels and super title
2816
+ # NOTE: To avoid resetting figure-wide settings when new axes are created
2817
+ # we only proceed if using the default context mode. Simliar to geo.py
2818
+ if skip_figure: # avoid recursion
2819
+ return
2820
+ if rc_mode == 1: # avoid resetting
2821
+ return
2822
+ self.figure.format(rc_kw=rc_kw, rc_mode=rc_mode, skip_axes=True, **params)
2823
+
2824
+ def draw(self, renderer=None, *args, **kwargs):
2825
+ # Perform extra post-processing steps
2826
+ # NOTE: In *principle* these steps go here but should already be complete
2827
+ # because auto_layout() (called by figure pre-processor) has to run them
2828
+ # before aligning labels. So these are harmless no-ops.
2829
+ self._add_queued_guides()
2830
+ self._apply_title_above()
2831
+ if self._colorbar_fill:
2832
+ self._colorbar_fill.update_ticks(manual_only=True) # only if needed
2833
+ if self._inset_parent is not None and self._inset_zoom:
2834
+ self.indicate_inset_zoom()
2835
+ super().draw(renderer, *args, **kwargs)
2836
+
2837
+ def get_tightbbox(self, renderer, *args, **kwargs):
2838
+ # Perform extra post-processing steps
2839
+ # NOTE: This should be updated alongside draw(). We also cache the resulting
2840
+ # bounding box to speed up tight layout calculations (see _range_tightbbox).
2841
+ self._add_queued_guides()
2842
+ self._apply_title_above()
2843
+ if self._colorbar_fill:
2844
+ self._colorbar_fill.update_ticks(manual_only=True) # only if needed
2845
+ if self._inset_parent is not None and self._inset_zoom:
2846
+ self.indicate_inset_zoom()
2847
+ self._tight_bbox = super().get_tightbbox(renderer, *args, **kwargs)
2848
+ return self._tight_bbox
2849
+
2850
+ def get_default_bbox_extra_artists(self):
2851
+ # Further restrict artists to those with disabled clipping
2852
+ # or use the axes bounding box or patch path for clipping.
2853
+ # NOTE: Critical to ignore x and y axis, spines, and all child axes.
2854
+ # For some reason these have clipping 'enabled' but it is not respected.
2855
+ # NOTE: Matplotlib already tries to do this inside get_tightbbox() but
2856
+ # their approach fails for cartopy axes clipped by paths and not boxes.
2857
+ return [
2858
+ artist
2859
+ for artist in super().get_default_bbox_extra_artists()
2860
+ if not self._artist_fully_clipped(artist)
2861
+ ]
2862
+
2863
+ def set_prop_cycle(self, *args, **kwargs):
2864
+ # Silent override. This is a strict superset of matplotlib functionality.
2865
+ # Includes both ultraplot syntax with positional arguments interpreted as
2866
+ # color arguments and oldschool matplotlib cycler(key, value) syntax.
2867
+ if len(args) == 2 and isinstance(args[0], str) and np.iterable(args[1]):
2868
+ if _pop_props({args[0]: object()}, "line"): # if a valid line property
2869
+ kwargs = {args[0]: args[1]} # pass as keyword argument
2870
+ args = ()
2871
+ cycle = self._active_cycle = constructor.Cycle(*args, **kwargs)
2872
+ return super().set_prop_cycle(cycle) # set the property cycler after validation
2873
+
2874
+ @docstring._snippet_manager
2875
+ def inset(self, *args, **kwargs):
2876
+ """
2877
+ %(axes.inset)s
2878
+ """
2879
+ return self._add_inset_axes(*args, **kwargs)
2880
+
2881
+ @docstring._snippet_manager
2882
+ def inset_axes(self, *args, **kwargs):
2883
+ """
2884
+ %(axes.inset)s
2885
+ """
2886
+ return self._add_inset_axes(*args, **kwargs)
2887
+
2888
+ @docstring._snippet_manager
2889
+ def indicate_inset_zoom(self, **kwargs):
2890
+ """
2891
+ %(axes.indicate_inset)s
2892
+ """
2893
+ # Add the inset indicators
2894
+ parent = self._inset_parent
2895
+ if not parent:
2896
+ raise ValueError("This command can only be called from an inset axes.")
2897
+ kwargs.update(_pop_props(kwargs, "patch")) # impose alternative defaults
2898
+ if not self._inset_zoom_artists:
2899
+ kwargs.setdefault("zorder", 3.5)
2900
+ kwargs.setdefault("linewidth", rc["axes.linewidth"])
2901
+ kwargs.setdefault("edgecolor", rc["axes.edgecolor"])
2902
+ xlim, ylim = self.get_xlim(), self.get_ylim()
2903
+ rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0])
2904
+ rectpatch, connects = parent.indicate_inset(rect, self)
2905
+
2906
+ # Update indicator properties
2907
+ # NOTE: Unlike matplotlib we sync zoom box properties with connection lines.
2908
+ if self._inset_zoom_artists:
2909
+ rectpatch_prev, connects_prev = self._inset_zoom_artists
2910
+ rectpatch.update_from(rectpatch_prev)
2911
+ rectpatch.set_zorder(rectpatch_prev.get_zorder())
2912
+ rectpatch_prev.remove()
2913
+ for line, line_prev in zip(connects, connects_prev):
2914
+ line.update_from(line_prev)
2915
+ line.set_zorder(line_prev.get_zorder()) # not included in update_from
2916
+ line_prev.remove()
2917
+ rectpatch.update(kwargs)
2918
+ for line in connects:
2919
+ line.update(kwargs)
2920
+ self._inset_zoom_artists = (rectpatch, connects)
2921
+ return rectpatch, connects
2922
+
2923
+ @docstring._snippet_manager
2924
+ def panel(self, side=None, **kwargs):
2925
+ """
2926
+ %(axes.panel)s
2927
+ """
2928
+ return self.figure._add_axes_panel(self, side, **kwargs)
2929
+
2930
+ @docstring._snippet_manager
2931
+ def panel_axes(self, side=None, **kwargs):
2932
+ """
2933
+ %(axes.panel)s
2934
+ """
2935
+ return self.figure._add_axes_panel(self, side, **kwargs)
2936
+
2937
+ @docstring._obfuscate_params
2938
+ @docstring._snippet_manager
2939
+ def colorbar(self, mappable, values=None, loc=None, location=None, **kwargs):
2940
+ """
2941
+ Add an inset colorbar or an outer colorbar along the edge of the axes.
2942
+
2943
+ Parameters
2944
+ ----------
2945
+ %(axes.colorbar_args)s
2946
+ loc, location : int or str, default: :rc:`colorbar.loc`
2947
+ The colorbar location. Valid location keys are shown in the below table.
2948
+
2949
+ .. _colorbar_table:
2950
+
2951
+ ================== =======================================
2952
+ Location Valid keys
2953
+ ================== =======================================
2954
+ outer left ``'left'``, ``'l'``
2955
+ outer right ``'right'``, ``'r'``
2956
+ outer bottom ``'bottom'``, ``'b'``
2957
+ outer top ``'top'``, ``'t'``
2958
+ default inset ``'best'``, ``'inset'``, ``'i'``, ``0``
2959
+ upper right inset ``'upper right'``, ``'ur'``, ``1``
2960
+ upper left inset ``'upper left'``, ``'ul'``, ``2``
2961
+ lower left inset ``'lower left'``, ``'ll'``, ``3``
2962
+ lower right inset ``'lower right'``, ``'lr'``, ``4``
2963
+ "filled" ``'fill'``
2964
+ ================== =======================================
2965
+
2966
+ shrink
2967
+ Alias for `length`. This is included for consistency with
2968
+ `matplotlib.figure.Figure.colorbar`.
2969
+ length \
2970
+ : float or unit-spec, default: :rc:`colorbar.length` or :rc:`colorbar.insetlength`
2971
+ The colorbar length. For outer colorbars, units are relative to the axes
2972
+ width or height (default is :rcraw:`colorbar.length`). For inset
2973
+ colorbars, floats interpreted as em-widths and strings interpreted
2974
+ by `~ultraplot.utils.units` (default is :rcraw:`colorbar.insetlength`).
2975
+ width : unit-spec, default: :rc:`colorbar.width` or :rc:`colorbar.insetwidth
2976
+ The colorbar width. For outer colorbars, floats are interpreted as inches
2977
+ (default is :rcraw:`colorbar.width`). For inset colorbars, floats are
2978
+ interpreted as em-widths (default is :rcraw:`colorbar.insetwidth`).
2979
+ Strings are interpreted by `~ultraplot.utils.units`.
2980
+ %(axes.colorbar_space)s
2981
+ Has no visible effect if `length` is ``1``.
2982
+
2983
+ Other parameters
2984
+ ----------------
2985
+ %(axes.colorbar_kwargs)s
2986
+
2987
+ See also
2988
+ --------
2989
+ ultraplot.figure.Figure.colorbar
2990
+ matplotlib.figure.Figure.colorbar
2991
+ """
2992
+ # Translate location and possibly infer from orientation. Also optionally
2993
+ # infer align setting from keywords stored on object.
2994
+ orientation = kwargs.get("orientation", None)
2995
+ kwargs = guides._flush_guide_kw(mappable, "colorbar", kwargs)
2996
+ loc = _not_none(loc=loc, location=location)
2997
+ if orientation is not None: # possibly infer loc from orientation
2998
+ if orientation not in ("vertical", "horizontal"):
2999
+ raise ValueError(
3000
+ f"Invalid colorbar orientation {orientation!r}. Must be 'vertical' or 'horizontal'."
3001
+ ) # noqa: E501
3002
+ if loc is None:
3003
+ loc = {"vertical": "right", "horizontal": "bottom"}[orientation]
3004
+ loc = _translate_loc(loc, "colorbar", default=rc["colorbar.loc"])
3005
+ align = kwargs.pop("align", None)
3006
+ align = _translate_loc(align, "align", default="center")
3007
+
3008
+ # Either draw right now or queue up for later. The queue option lets us
3009
+ # successively append objects (e.g. lines) to a colorbar artist list.
3010
+ queue = kwargs.pop("queue", False)
3011
+ if queue:
3012
+ self._register_guide("colorbar", (mappable, values), (loc, align), **kwargs)
3013
+ else:
3014
+ return self._add_colorbar(mappable, values, loc=loc, align=align, **kwargs)
3015
+
3016
+ @docstring._concatenate_inherited # also obfuscates params
3017
+ @docstring._snippet_manager
3018
+ def legend(self, handles=None, labels=None, loc=None, location=None, **kwargs):
3019
+ """
3020
+ Add an inset legend or outer legend along the edge of the axes.
3021
+
3022
+ Parameters
3023
+ ----------
3024
+ %(axes.legend_args)s
3025
+ loc, location : int or str, default: :rc:`legend.loc`
3026
+ The legend location. Valid location keys are shown in the below table.
3027
+
3028
+ .. _legend_table:
3029
+
3030
+ ================== =======================================
3031
+ Location Valid keys
3032
+ ================== =======================================
3033
+ outer left ``'left'``, ``'l'``
3034
+ outer right ``'right'``, ``'r'``
3035
+ outer bottom ``'bottom'``, ``'b'``
3036
+ outer top ``'top'``, ``'t'``
3037
+ "best" inset ``'best'``, ``'inset'``, ``'i'``, ``0``
3038
+ upper right inset ``'upper right'``, ``'ur'``, ``1``
3039
+ upper left inset ``'upper left'``, ``'ul'``, ``2``
3040
+ lower left inset ``'lower left'``, ``'ll'``, ``3``
3041
+ lower right inset ``'lower right'``, ``'lr'``, ``4``
3042
+ center left inset ``'center left'``, ``'cl'``, ``5``
3043
+ center right inset ``'center right'``, ``'cr'``, ``6``
3044
+ lower center inset ``'lower center'``, ``'lc'``, ``7``
3045
+ upper center inset ``'upper center'``, ``'uc'``, ``8``
3046
+ center inset ``'center'``, ``'c'``, ``9``
3047
+ "filled" ``'fill'``
3048
+ ================== =======================================
3049
+
3050
+ width : unit-spec, optional
3051
+ For outer legends only. The space allocated for the legend
3052
+ box. This does nothing if the :ref:`tight layout algorithm
3053
+ <ug_tight>` is active for the figure.
3054
+ %(units.in)s
3055
+ %(axes.legend_space)s
3056
+
3057
+ Other parameters
3058
+ ----------------
3059
+ %(axes.legend_kwargs)s
3060
+
3061
+ See also
3062
+ --------
3063
+ ultraplot.figure.Figure.legend
3064
+ matplotlib.axes.Axes.legend
3065
+ """
3066
+ # Translate location and possibly infer from orientation. Also optionally
3067
+ # infer align setting from keywords stored on object.
3068
+ kwargs = guides._flush_guide_kw(handles, "legend", kwargs)
3069
+ loc = _not_none(loc=loc, location=location)
3070
+ loc = _translate_loc(loc, "legend", default=rc["legend.loc"])
3071
+ align = kwargs.pop("align", None)
3072
+ align = _translate_loc(align, "align", default="center")
3073
+
3074
+ # Either draw right now or queue up for later. Handles can be successively
3075
+ # added to a single location this way. Used for on-the-fly legends.
3076
+ queue = kwargs.pop("queue", False)
3077
+ if queue:
3078
+ self._register_guide("legend", (handles, labels), (loc, align), **kwargs)
3079
+ else:
3080
+ return self._add_legend(handles, labels, loc=loc, align=align, **kwargs)
3081
+
3082
+ @docstring._concatenate_inherited
3083
+ @docstring._snippet_manager
3084
+ def text(
3085
+ self,
3086
+ *args,
3087
+ border=False,
3088
+ bbox=False,
3089
+ bordercolor="w",
3090
+ borderwidth=2,
3091
+ borderinvert=False,
3092
+ borderstyle="miter",
3093
+ bboxcolor="w",
3094
+ bboxstyle="round",
3095
+ bboxalpha=0.5,
3096
+ bboxpad=None,
3097
+ **kwargs,
3098
+ ):
3099
+ """
3100
+ Add text to the axes.
3101
+
3102
+ Parameters
3103
+ ----------
3104
+ x, y, [z] : float
3105
+ The coordinates for the text. `~ultraplot.axes.ThreeAxes` accept an
3106
+ optional third coordinate. If only two are provided this automatically
3107
+ redirects to the `~mpl_toolkits.mplot3d.Axes3D.text2D` method.
3108
+ s, text : str
3109
+ The string for the text.
3110
+ %(axes.transform)s
3111
+
3112
+ Other parameters
3113
+ ----------------
3114
+ border : bool, default: False
3115
+ Whether to draw border around text.
3116
+ borderwidth : float, default: 2
3117
+ The width of the text border.
3118
+ bordercolor : color-spec, default: 'w'
3119
+ The color of the text border.
3120
+ borderinvert : bool, optional
3121
+ If ``True``, the text and border colors are swapped.
3122
+ borderstyle : {'miter', 'round', 'bevel'}, optional
3123
+ The `line join style \
3124
+ <https://matplotlib.org/stable/gallery/lines_bars_and_markers/joinstyle.html>`__
3125
+ used for the border.
3126
+ bbox : bool, default: False
3127
+ Whether to draw a bounding box around text.
3128
+ bboxcolor : color-spec, default: 'w'
3129
+ The color of the text bounding box.
3130
+ bboxstyle : boxstyle, default: 'round'
3131
+ The style of the bounding box.
3132
+ bboxalpha : float, default: 0.5
3133
+ The alpha for the bounding box.
3134
+ bboxpad : float, default: :rc:`title.bboxpad`
3135
+ The padding for the bounding box.
3136
+ %(artist.text)s
3137
+
3138
+ **kwargs
3139
+ Passed to `matplotlib.axes.Axes.text`.
3140
+
3141
+ See also
3142
+ --------
3143
+ matplotlib.axes.Axes.text
3144
+ """
3145
+ # Translate positional args
3146
+ # Audo-redirect to text2D for 3D axes if not enough arguments passed
3147
+ # NOTE: The transform must be passed positionally for 3D axes with 2D coords
3148
+ keys = "xy"
3149
+ func = super().text
3150
+ if self._name == "three":
3151
+ if len(args) >= 4 or "z" in kwargs:
3152
+ keys += "z"
3153
+ else:
3154
+ func = self.text2D
3155
+ keys = (*keys, ("s", "text"), "transform")
3156
+ args, kwargs = _kwargs_to_args(keys, *args, **kwargs)
3157
+ *args, transform = args
3158
+ if any(arg is None for arg in args):
3159
+ raise TypeError("Missing required positional argument.")
3160
+ if transform is None:
3161
+ transform = self.transData
3162
+ else:
3163
+ transform = self._get_transform(transform)
3164
+ with warnings.catch_warnings(): # ignore duplicates (internal issues?)
3165
+ warnings.simplefilter("ignore", warnings.UltraplotWarning)
3166
+ kwargs.update(_pop_props(kwargs, "text"))
3167
+
3168
+ # Update the text object using a monkey patch
3169
+ obj = func(*args, transform=transform, **kwargs)
3170
+ obj.update = labels._update_label.__get__(obj)
3171
+ obj.update(
3172
+ {
3173
+ "border": border,
3174
+ "bordercolor": bordercolor,
3175
+ "borderinvert": borderinvert,
3176
+ "borderwidth": borderwidth,
3177
+ "borderstyle": borderstyle,
3178
+ "bbox": bbox,
3179
+ "bboxcolor": bboxcolor,
3180
+ "bboxstyle": bboxstyle,
3181
+ "bboxalpha": bboxalpha,
3182
+ "bboxpad": bboxpad,
3183
+ }
3184
+ )
3185
+ return obj
3186
+
3187
+ def _iter_axes(self, hidden=False, children=False, panels=True):
3188
+ """
3189
+ Return a list of visible axes, panel axes, and child axes of both.
3190
+
3191
+ Parameters
3192
+ ----------
3193
+ hidden : bool, optional
3194
+ Whether to include "hidden" panels.
3195
+ children : bool, optional
3196
+ Whether to include children. Note this now includes "twin" axes.
3197
+ panels : bool or str or sequence of str, optional
3198
+ Whether to include panels or the panels to include.
3199
+ """
3200
+ # Parse panels
3201
+ if panels is False:
3202
+ panels = ()
3203
+ elif panels is True or panels is None:
3204
+ panels = ("left", "right", "bottom", "top")
3205
+ elif isinstance(panels, str):
3206
+ panels = (panels,)
3207
+ if not set(panels) <= {"left", "right", "bottom", "top"}:
3208
+ raise ValueError(f"Invalid sides {panels!r}.")
3209
+ # Iterate
3210
+ axs = (self, *(ax for side in panels for ax in self._panel_dict[side]))
3211
+ for iax in axs:
3212
+ if not hidden and iax._panel_hidden:
3213
+ continue # ignore hidden panel and its colorbar/legend child
3214
+ iaxs = (iax, *(iax.child_axes if children else ()))
3215
+ for jax in iaxs:
3216
+ if not jax.get_visible():
3217
+ continue # safety first
3218
+ yield jax
3219
+
3220
+ @property
3221
+ def number(self):
3222
+ """
3223
+ The axes number. This controls the order of a-b-c labels and the
3224
+ order of appearance in the `~ultraplot.gridspec.SubplotGrid` returned
3225
+ by `~ultraplot.figure.Figure.subplots`.
3226
+ """
3227
+ return self._number
3228
+
3229
+ @number.setter
3230
+ def number(self, num):
3231
+ if num is None or isinstance(num, Integral) and num > 0:
3232
+ self._number = num
3233
+ else:
3234
+ raise ValueError(f"Invalid number {num!r}. Must be integer >=1.")
3235
+
3236
+
3237
+ # Apply signature obfuscation after storing previous signature
3238
+ # NOTE: This is needed for __init__
3239
+ Axes._format_signatures = {Axes: inspect.signature(Axes.format)}
3240
+ Axes.format = docstring._obfuscate_kwargs(Axes.format)