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/figure.py ADDED
@@ -0,0 +1,2102 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ The figure class used for all ultraplot figures.
4
+ """
5
+ import functools
6
+ import inspect
7
+ import os
8
+ from numbers import Integral
9
+
10
+ import matplotlib.axes as maxes
11
+ import matplotlib.figure as mfigure
12
+ import matplotlib.gridspec as mgridspec
13
+ import matplotlib.projections as mproj
14
+ import matplotlib.text as mtext
15
+ import matplotlib.transforms as mtransforms
16
+ import numpy as np
17
+
18
+ from . import axes as paxes
19
+ from . import constructor
20
+ from . import gridspec as pgridspec
21
+ from .config import rc, rc_matplotlib
22
+ from .internals import ic # noqa: F401
23
+ from .internals import (
24
+ _not_none,
25
+ _pop_params,
26
+ _pop_rc,
27
+ _translate_loc,
28
+ context,
29
+ docstring,
30
+ labels,
31
+ warnings,
32
+ )
33
+ from .utils import units
34
+
35
+ __all__ = [
36
+ "Figure",
37
+ ]
38
+
39
+
40
+ # Preset figure widths or sizes based on academic journal recommendations
41
+ # NOTE: Please feel free to add to this!
42
+ JOURNAL_SIZES = {
43
+ "aaas1": "5.5cm",
44
+ "aaas2": "12cm",
45
+ "agu1": ("95mm", "115mm"),
46
+ "agu2": ("190mm", "115mm"),
47
+ "agu3": ("95mm", "230mm"),
48
+ "agu4": ("190mm", "230mm"),
49
+ "ams1": 3.2,
50
+ "ams2": 4.5,
51
+ "ams3": 5.5,
52
+ "ams4": 6.5,
53
+ "nat1": "89mm",
54
+ "nat2": "183mm",
55
+ "pnas1": "8.7cm",
56
+ "pnas2": "11.4cm",
57
+ "pnas3": "17.8cm",
58
+ }
59
+
60
+
61
+ # Figure docstring
62
+ _figure_docstring = """
63
+ refnum : int, optional
64
+ The reference subplot number. The `refwidth`, `refheight`, and `refaspect`
65
+ keyword args are applied to this subplot, and the aspect ratio is conserved
66
+ for this subplot in the `~Figure.auto_layout`. The default is the first
67
+ subplot created in the figure.
68
+ refaspect : float or 2-tuple of float, optional
69
+ The reference subplot aspect ratio. If scalar, this indicates the width
70
+ divided by height. If 2-tuple, this indicates the (width, height). Ignored
71
+ if both `figwidth` *and* `figheight` or both `refwidth` *and* `refheight` were
72
+ passed. The default value is ``1`` or the "data aspect ratio" if the latter
73
+ is explicitly fixed (as with `~ultraplot.axes.PlotAxes.imshow` plots and
74
+ `~ultraplot.axes.Axes.GeoAxes` projections; see `~matplotlib.axes.Axes.set_aspect`).
75
+ refwidth, refheight : unit-spec, default: :rc:`subplots.refwidth`
76
+ The width, height of the reference subplot.
77
+ %(units.in)s
78
+ Ignored if `figwidth`, `figheight`, or `figsize` was passed. If you
79
+ specify just one, `refaspect` will be respected.
80
+ ref, aspect, axwidth, axheight
81
+ Aliases for `refnum`, `refaspect`, `refwidth`, `refheight`.
82
+ *These may be deprecated in a future release.*
83
+ figwidth, figheight : unit-spec, optional
84
+ The figure width and height. Default behavior is to use `refwidth`.
85
+ %(units.in)s
86
+ If you specify just one, `refaspect` will be respected.
87
+ width, height
88
+ Aliases for `figwidth`, `figheight`.
89
+ figsize : 2-tuple, optional
90
+ Tuple specifying the figure ``(width, height)``.
91
+ sharex, sharey, share \
92
+ : {0, False, 1, 'labels', 'labs', 2, 'limits', 'lims', 3, True, 4, 'all'}, \
93
+ default: :rc:`subplots.share`
94
+ The axis sharing "level" for the *x* axis, *y* axis, or both
95
+ axes. Options are as follows:
96
+
97
+ * ``0`` or ``False``: No axis sharing. This also sets the default `spanx`
98
+ and `spany` values to ``False``.
99
+ * ``1`` or ``'labels'`` or ``'labs'``: Only draw axis labels on the bottommost
100
+ row or leftmost column of subplots. Tick labels still appear on every subplot.
101
+ * ``2`` or ``'limits'`` or ``'lims'``: As above but force the axis limits, scales,
102
+ and tick locations to be identical. Tick labels still appear on every subplot.
103
+ * ``3`` or ``True``: As above but only show the tick labels on the bottommost
104
+ row and leftmost column of subplots.
105
+ * ``4`` or ``'all'``: As above but also share the axis limits, scales, and
106
+ tick locations between subplots not in the same row or column.
107
+
108
+ spanx, spany, span : bool or {0, 1}, default: :rc:`subplots.span`
109
+ Whether to use "spanning" axis labels for the *x* axis, *y* axis, or both
110
+ axes. Default is ``False`` if `sharex`, `sharey`, or `share` are ``0`` or
111
+ ``False``. When ``True``, a single, centered axis label is used for all axes
112
+ with bottom and left edges in the same row or column. This can considerably
113
+ redundancy in your figure. "Spanning" labels integrate with "shared" axes. For
114
+ example, for a 3-row, 3-column figure, with ``sharey > 1`` and ``spany == True``,
115
+ your figure will have 1 y axis label instead of 9 y axis labels.
116
+ alignx, aligny, align : bool or {0, 1}, default: :rc:`subplots.align`
117
+ Whether to `"align" axis labels \
118
+ <https://matplotlib.org/stable/gallery/subplots_axes_and_figures/align_labels_demo.html>`__
119
+ for the *x* axis, *y* axis, or both axes. Aligned labels always appear in the same
120
+ row or column. This is ignored if `spanx`, `spany`, or `span` are ``True``.
121
+ %(gridspec.shared)s
122
+ %(gridspec.scalar)s
123
+ tight : bool, default: :rc`subplots.tight`
124
+ Whether automatic calls to `~Figure.auto_layout` should include
125
+ :ref:`tight layout adjustments <ug_tight>`. If you manually specified a spacing
126
+ in the call to `~ultraplot.ui.subplots`, it will be used to override the tight
127
+ layout spacing. For example, with ``left=1``, the left margin is set to 1
128
+ em-width, while the remaining margin widths are calculated automatically.
129
+ %(gridspec.tight)s
130
+ journal : str, optional
131
+ String corresponding to an academic journal standard used to control the figure
132
+ width `figwidth` and, if specified, the figure height `figheight`. See the below
133
+ table. Feel free to add to this table by submitting a pull request.
134
+
135
+ .. _journal_table:
136
+
137
+ =========== ==================== \
138
+ ===============================================================================
139
+ Key Size description Organization
140
+ =========== ==================== \
141
+ ===============================================================================
142
+ ``'aaas1'`` 1-column \
143
+ `American Association for the Advancement of Science <aaas_>`_ (e.g. *Science*)
144
+ ``'aaas2'`` 2-column ”
145
+ ``'agu1'`` 1-column `American Geophysical Union <agu_>`_
146
+ ``'agu2'`` 2-column ”
147
+ ``'agu3'`` full height 1-column ”
148
+ ``'agu4'`` full height 2-column ”
149
+ ``'ams1'`` 1-column `American Meteorological Society <ams_>`_
150
+ ``'ams2'`` small 2-column ”
151
+ ``'ams3'`` medium 2-column ”
152
+ ``'ams4'`` full 2-column ”
153
+ ``'nat1'`` 1-column `Nature Research <nat_>`_
154
+ ``'nat2'`` 2-column ”
155
+ ``'pnas1'`` 1-column \
156
+ `Proceedings of the National Academy of Sciences <pnas_>`_
157
+ ``'pnas2'`` 2-column ”
158
+ ``'pnas3'`` landscape page ”
159
+ =========== ==================== \
160
+ ===============================================================================
161
+
162
+ .. _aaas: \
163
+ https://www.sciencemag.org/authors/instructions-preparing-initial-manuscript
164
+ .. _agu: \
165
+ https://www.agu.org/Publish-with-AGU/Publish/Author-Resources/Graphic-Requirements
166
+ .. _ams: \
167
+ https://www.ametsoc.org/ams/index.cfm/publications/authors/journal-and-bams-authors/figure-information-for-authors/
168
+ .. _nat: \
169
+ https://www.nature.com/nature/for-authors/formatting-guide
170
+ .. _pnas: \
171
+ https://www.pnas.org/page/authors/format
172
+ """
173
+ docstring._snippet_manager["figure.figure"] = _figure_docstring
174
+
175
+
176
+ # Multiple subplots
177
+ _subplots_params_docstring = """
178
+ array : `ultraplot.gridspec.GridSpec` or array-like of int, optional
179
+ The subplot grid specifier. If a `~ultraplot.gridspec.GridSpec`, one subplot is
180
+ drawn for each unique `~ultraplot.gridspec.GridSpec` slot. If a 2D array of integers,
181
+ one subplot is drawn for each unique integer in the array. Think of this array as
182
+ a "picture" of the subplot grid -- for example, the array ``[[1, 1], [2, 3]]``
183
+ creates one long subplot in the top row, two smaller subplots in the bottom row.
184
+ Integers must range from 1 to the number of plots, and ``0`` indicates an
185
+ empty space -- for example, ``[[1, 1, 1], [2, 0, 3]]`` creates one long subplot
186
+ in the top row with two subplots in the bottom row separated by a space.
187
+ nrows, ncols : int, default: 1
188
+ The number of rows and columns in the subplot grid. Ignored
189
+ if `array` was passed. Use these arguments for simple subplot grids.
190
+ order : {'C', 'F'}, default: 'C'
191
+ Whether subplots are numbered in column-major (``'C'``) or row-major (``'F'``)
192
+ order. Analogous to `numpy.array` ordering. This controls the order that
193
+ subplots appear in the `SubplotGrid` returned by this function, and the order
194
+ of subplot a-b-c labels (see `~ultraplot.axes.Axes.format`).
195
+ %(axes.proj)s
196
+
197
+ To use different projections for different subplots, you have
198
+ two options:
199
+
200
+ * Pass a *list* of projection specifications, one for each subplot.
201
+ For example, ``pplt.subplots(ncols=2, proj=('cart', 'robin'))``.
202
+ * Pass a *dictionary* of projection specifications, where the
203
+ keys are integers or tuples of integers that indicate the projection
204
+ to use for the corresponding subplot number(s). If a key is not
205
+ provided, the default projection ``'cartesian'`` is used. For example,
206
+ ``pplt.subplots(ncols=4, proj={2: 'cyl', (3, 4): 'stere'})`` creates
207
+ a figure with a default Cartesian axes for the first subplot, a Mercator
208
+ projection for the second subplot, and a Stereographic projection
209
+ for the third and fourth subplots.
210
+
211
+ %(axes.proj_kw)s
212
+ If dictionary of properties, applies globally. If list or dictionary of
213
+ dictionaries, applies to specific subplots, as with `proj`. For example,
214
+ ``pplt.subplots(ncols=2, proj='cyl', proj_kw=({'lon_0': 0}, {'lon_0': 180})``
215
+ centers the projection in the left subplot on the prime meridian and in the
216
+ right subplot on the international dateline.
217
+ %(axes.backend)s
218
+ If string, applies to all subplots. If list or dict, applies to specific
219
+ subplots, as with `proj`.
220
+ %(gridspec.shared)s
221
+ %(gridspec.vector)s
222
+ %(gridspec.tight)s
223
+ """
224
+ docstring._snippet_manager["figure.subplots_params"] = _subplots_params_docstring
225
+
226
+
227
+ # Extra args docstring
228
+ _axes_params_docstring = """
229
+ **kwargs
230
+ Passed to the ultraplot class `ultraplot.axes.CartesianAxes`, `ultraplot.axes.PolarAxes`,
231
+ `ultraplot.axes.GeoAxes`, or `ultraplot.axes.ThreeAxes`. This can include keyword
232
+ arguments for projection-specific ``format`` commands.
233
+ """
234
+ docstring._snippet_manager["figure.axes_params"] = _axes_params_docstring
235
+
236
+
237
+ # Multiple subplots docstring
238
+ _subplots_docstring = """
239
+ Add an arbitrary grid of subplots to the figure.
240
+
241
+ Parameters
242
+ ----------
243
+ %(figure.subplots_params)s
244
+
245
+ Other parameters
246
+ ----------------
247
+ %(figure.figure)s
248
+ %(figure.axes_params)s
249
+
250
+ Returns
251
+ -------
252
+ axs : SubplotGrid
253
+ The axes instances stored in a `SubplotGrid`.
254
+
255
+ See also
256
+ --------
257
+ ultraplot.ui.figure
258
+ ultraplot.ui.subplots
259
+ ultraplot.figure.Figure.subplot
260
+ ultraplot.figure.Figure.add_subplot
261
+ ultraplot.gridspec.SubplotGrid
262
+ ultraplot.axes.Axes
263
+ """
264
+ docstring._snippet_manager["figure.subplots"] = _subplots_docstring
265
+
266
+
267
+ # Single subplot docstring
268
+ _subplot_docstring = """
269
+ Add a subplot axes to the figure.
270
+
271
+ Parameters
272
+ ----------
273
+ *args : int, tuple, or `~matplotlib.gridspec.SubplotSpec`, optional
274
+ The subplot location specifier. Your options are:
275
+
276
+ * A single 3-digit integer argument specifying the number of rows,
277
+ number of columns, and gridspec number (using row-major indexing).
278
+ * Three positional arguments specifying the number of rows, number of
279
+ columns, and gridspec number (int) or number range (2-tuple of int).
280
+ * A `~matplotlib.gridspec.SubplotSpec` instance generated by indexing
281
+ a ultraplot `~ultraplot.gridspec.GridSpec`.
282
+
283
+ For integer input, the implied geometry must be compatible with the implied
284
+ geometry from previous calls -- for example, ``fig.add_subplot(331)`` followed
285
+ by ``fig.add_subplot(132)`` is valid because the 1 row of the second input can
286
+ be tiled into the 3 rows of the the first input, but ``fig.add_subplot(232)``
287
+ will raise an error because 2 rows cannot be tiled into 3 rows. For
288
+ `~matplotlib.gridspec.SubplotSpec` input, the `~matplotlig.gridspec.SubplotSpec`
289
+ must be derived from the `~ultraplot.gridspec.GridSpec` used in previous calls.
290
+
291
+ These restrictions arise because we allocate a single,
292
+ unique `~Figure.gridspec` for each figure.
293
+ number : int, optional
294
+ The axes number used for a-b-c labeling. See `~ultraplot.axes.Axes.format` for
295
+ details. By default this is incremented automatically based on the other subplots
296
+ in the figure. Use e.g. ``number=None`` or ``number=False`` to ensure the subplot
297
+ has no a-b-c label. Note the number corresponding to ``a`` is ``1``, not ``0``.
298
+ autoshare : bool, default: True
299
+ Whether to automatically share the *x* and *y* axes with subplots spanning the
300
+ same rows and columns based on the figure-wide `sharex` and `sharey` settings.
301
+ This has no effect if :rcraw:`subplots.share` is ``False`` or if ``sharex=False``
302
+ or ``sharey=False`` were passed to the figure.
303
+ %(axes.proj)s
304
+ %(axes.proj_kw)s
305
+ %(axes.backend)s
306
+
307
+ Other parameters
308
+ ----------------
309
+ %(figure.axes_params)s
310
+
311
+ See also
312
+ --------
313
+ ultraplot.figure.Figure.add_axes
314
+ ultraplot.figure.Figure.subplots
315
+ ultraplot.figure.Figure.add_subplots
316
+ """
317
+ docstring._snippet_manager["figure.subplot"] = _subplot_docstring
318
+
319
+
320
+ # Single axes
321
+ _axes_docstring = """
322
+ Add a non-subplot axes to the figure.
323
+
324
+ Parameters
325
+ ----------
326
+ rect : 4-tuple of float
327
+ The (left, bottom, width, height) dimensions of the axes in
328
+ figure-relative coordinates.
329
+ %(axes.proj)s
330
+ %(axes.proj_kw)s
331
+ %(axes.backend)s
332
+
333
+ Other parameters
334
+ ----------------
335
+ %(figure.axes_params)s
336
+
337
+ See also
338
+ --------
339
+ ultraplot.figure.Figure.subplot
340
+ ultraplot.figure.Figure.add_subplot
341
+ ultraplot.figure.Figure.subplots
342
+ ultraplot.figure.Figure.add_subplots
343
+ """
344
+ docstring._snippet_manager["figure.axes"] = _axes_docstring
345
+
346
+
347
+ # Colorbar or legend panel docstring
348
+ _space_docstring = """
349
+ loc : str, optional
350
+ The {name} location. Valid location keys are as follows.
351
+
352
+ %(axes.panel_loc)s
353
+
354
+ space : float or str, default: None
355
+ The fixed space between the {name} and the subplot grid edge.
356
+ %(units.em)s
357
+ When the :ref:`tight layout algorithm <ug_tight>` is active for the figure,
358
+ `space` is computed automatically (see `pad`). Otherwise, `space` is set to
359
+ a suitable default.
360
+ pad : float or str, default: :rc:`subplots.innerpad` or :rc:`subplots.panelpad`
361
+ The :ref:`tight layout padding <ug_tight>` between the {name} and the
362
+ subplot grid. Default is :rcraw:`subplots.innerpad` for the first {name}
363
+ and :rcraw:`subplots.panelpad` for subsequently "stacked" {name}s.
364
+ %(units.em)s
365
+ row, rows
366
+ Aliases for `span` for {name}s on the left or right side.
367
+ col, cols
368
+ Aliases for `span` for {name}s on the top or bottom side.
369
+ span : int or 2-tuple of int, default: None
370
+ Integer(s) indicating the span of the {name} across rows and columns of
371
+ subplots. For example, ``fig.{name}(loc='b', col=1)`` draws a {name} beneath
372
+ the leftmost column of subplots, and ``fig.{name}(loc='b', cols=(1, 2))``
373
+ draws a {name} beneath the left two columns of subplots. By default
374
+ the {name} will span every subplot row and column.
375
+ align : {{'center', 'top', 't', 'bottom', 'b', 'left', 'l', 'right', 'r'}}, optional
376
+ For outer {name}s only. How to align the {name} against the
377
+ subplot edge. The values ``'top'`` and ``'bottom'`` are valid for left and
378
+ right {name}s and ``'left'`` and ``'right'`` are valid for top and bottom
379
+ {name}s. The default is always ``'center'``.
380
+ """
381
+ docstring._snippet_manager["figure.legend_space"] = _space_docstring.format(
382
+ name="legend"
383
+ ) # noqa: E501
384
+ docstring._snippet_manager["figure.colorbar_space"] = _space_docstring.format(
385
+ name="colorbar"
386
+ ) # noqa: E501
387
+
388
+
389
+ # Save docstring
390
+ _save_docstring = """
391
+ Save the figure.
392
+
393
+ Parameters
394
+ ----------
395
+ path : path-like, optional
396
+ The file path. User paths are expanded with `os.path.expanduser`.
397
+ **kwargs
398
+ Passed to `~matplotlib.figure.Figure.savefig`
399
+
400
+ See also
401
+ --------
402
+ Figure.save
403
+ Figure.savefig
404
+ matplotlib.figure.Figure.savefig
405
+ """
406
+ docstring._snippet_manager["figure.save"] = _save_docstring
407
+
408
+
409
+ def _get_journal_size(preset):
410
+ """
411
+ Return the width and height corresponding to the given preset.
412
+ """
413
+ value = JOURNAL_SIZES.get(preset, None)
414
+ if value is None:
415
+ raise ValueError(
416
+ f"Unknown preset figure size specifier {preset!r}. "
417
+ "Current options are: " + ", ".join(map(repr, JOURNAL_SIZES.keys()))
418
+ )
419
+ figwidth = figheight = None
420
+ try:
421
+ figwidth, figheight = value
422
+ except (TypeError, ValueError):
423
+ figwidth = value
424
+ return figwidth, figheight
425
+
426
+
427
+ def _add_canvas_preprocessor(canvas, method, cache=False):
428
+ """
429
+ Return a pre-processer that can be used to override instance-level
430
+ canvas draw() and print_figure() methods. This applies tight layout
431
+ and aspect ratio-conserving adjustments and aligns labels. Required
432
+ so canvas methods instantiate renderers with the correct dimensions.
433
+ """
434
+
435
+ # NOTE: Renderer must be (1) initialized with the correct figure size or
436
+ # (2) changed inplace during draw, but vector graphic renderers *cannot*
437
+ # be changed inplace. So options include (1) monkey patch
438
+ # canvas.get_width_height, overriding figure.get_size_inches, and exploit
439
+ # the FigureCanvasAgg.get_renderer() implementation (because FigureCanvasAgg
440
+ # queries the bbox directly rather than using get_width_height() so requires
441
+ # workaround), (2) override bbox and bbox_inches as *properties* (but these
442
+ # are really complicated, dangerous, and result in unnecessary extra draws),
443
+ # or (3) simply override canvas draw methods. Our choice is #3.
444
+ def _canvas_preprocess(self, *args, **kwargs):
445
+ fig = self.figure # update even if not stale! needed after saves
446
+ func = getattr(type(self), method) # the original method
447
+
448
+ # Bail out if we are already adjusting layout
449
+ # NOTE: The _is_adjusting check necessary when inserting new
450
+ # gridspec rows or columns with the qt backend.
451
+ # NOTE: Return value for macosx _draw is the renderer, for qt draw is
452
+ # nothing, and for print_figure is some figure object, but this block
453
+ # has never been invoked when calling print_figure.
454
+ if fig._is_adjusting:
455
+ if method == "_draw": # macosx backend
456
+ return fig._get_renderer()
457
+ else:
458
+ return
459
+
460
+ # Adjust layout
461
+ # NOTE: The authorized_context is needed because some backends disable
462
+ # constrained layout or tight layout before printing the figure.
463
+ ctx1 = fig._context_adjusting(cache=cache)
464
+ ctx2 = fig._context_authorized() # skip backend set_constrained_layout()
465
+ ctx3 = rc.context(fig._render_context) # draw with figure-specific setting
466
+ with ctx1, ctx2, ctx3:
467
+ fig.auto_layout()
468
+ return func(self, *args, **kwargs)
469
+
470
+ # Add preprocessor
471
+ setattr(canvas, method, _canvas_preprocess.__get__(canvas))
472
+ return canvas
473
+
474
+
475
+ class Figure(mfigure.Figure):
476
+ """
477
+ The `~matplotlib.figure.Figure` subclass used by ultraplot.
478
+ """
479
+
480
+ # Shared error and warning messages
481
+ _share_message = (
482
+ "Axis sharing level can be 0 or False (share nothing), "
483
+ "1 or 'labels' or 'labs' (share axis labels), "
484
+ "2 or 'limits' or 'lims' (share axis limits and axis labels), "
485
+ "3 or True (share axis limits, axis labels, and tick labels), "
486
+ "or 4 or 'all' (share axis labels and tick labels in the same gridspec "
487
+ "rows and columns and share axis limits across all subplots)."
488
+ )
489
+ _space_message = (
490
+ "To set the left, right, bottom, top, wspace, or hspace gridspec values, "
491
+ "pass them as keyword arguments to pplt.figure() or pplt.subplots(). Please "
492
+ "note they are now specified in physical units, with strings interpreted by "
493
+ "pplt.units() and floats interpreted as font size-widths."
494
+ )
495
+ _tight_message = (
496
+ "ultraplot uses its own tight layout algorithm that is activated by default. "
497
+ "To disable it, set pplt.rc['subplots.tight'] to False or pass tight=False "
498
+ "to pplt.subplots(). For details, see fig.auto_layout()."
499
+ )
500
+ _warn_interactive = True # disabled after first warning
501
+
502
+ def __repr__(self):
503
+ opts = {}
504
+ for attr in ("refaspect", "refwidth", "refheight", "figwidth", "figheight"):
505
+ value = getattr(self, "_" + attr)
506
+ if value is not None:
507
+ opts[attr] = np.round(value, 2)
508
+ geom = ""
509
+ if self.gridspec:
510
+ nrows, ncols = self.gridspec.get_geometry()
511
+ geom = f"nrows={nrows}, ncols={ncols}, "
512
+ opts = ", ".join(f"{key}={value!r}" for key, value in opts.items())
513
+ return f"Figure({geom}{opts})"
514
+
515
+ # NOTE: If _rename_kwargs argument is an invalid identifier, it is
516
+ # simply used in the warning message.
517
+ @docstring._obfuscate_kwargs
518
+ @docstring._snippet_manager
519
+ @warnings._rename_kwargs(
520
+ "0.7.0", axpad="innerpad", autoformat="pplt.rc.autoformat = {}"
521
+ )
522
+ def __init__(
523
+ self,
524
+ *,
525
+ refnum=None,
526
+ ref=None,
527
+ refaspect=None,
528
+ aspect=None,
529
+ refwidth=None,
530
+ refheight=None,
531
+ axwidth=None,
532
+ axheight=None,
533
+ figwidth=None,
534
+ figheight=None,
535
+ width=None,
536
+ height=None,
537
+ journal=None,
538
+ sharex=None,
539
+ sharey=None,
540
+ share=None, # used for default spaces
541
+ spanx=None,
542
+ spany=None,
543
+ span=None,
544
+ alignx=None,
545
+ aligny=None,
546
+ align=None,
547
+ left=None,
548
+ right=None,
549
+ top=None,
550
+ bottom=None,
551
+ wspace=None,
552
+ hspace=None,
553
+ space=None,
554
+ tight=None,
555
+ outerpad=None,
556
+ innerpad=None,
557
+ panelpad=None,
558
+ wpad=None,
559
+ hpad=None,
560
+ pad=None,
561
+ wequal=None,
562
+ hequal=None,
563
+ equal=None,
564
+ wgroup=None,
565
+ hgroup=None,
566
+ group=None,
567
+ **kwargs,
568
+ ):
569
+ """
570
+ Parameters
571
+ ----------
572
+ %(figure.figure)s
573
+
574
+ Other parameters
575
+ ----------------
576
+ %(figure.format)s
577
+ **kwargs
578
+ Passed to `matplotlib.figure.Figure`.
579
+
580
+ See also
581
+ --------
582
+ Figure.format
583
+ ultraplot.ui.figure
584
+ ultraplot.ui.subplots
585
+ matplotlib.figure.Figure
586
+ """
587
+ # Add figure sizing settings
588
+ # NOTE: We cannot catpure user-input 'figsize' here because it gets
589
+ # automatically filled by the figure manager. See ui.figure().
590
+ # NOTE: The figure size is adjusted according to these arguments by the
591
+ # canvas preprocessor. Although in special case where both 'figwidth' and
592
+ # 'figheight' were passes we update 'figsize' to limit side effects.
593
+ refnum = _not_none(refnum=refnum, ref=ref, default=1) # never None
594
+ refaspect = _not_none(refaspect=refaspect, aspect=aspect)
595
+ refwidth = _not_none(refwidth=refwidth, axwidth=axwidth)
596
+ refheight = _not_none(refheight=refheight, axheight=axheight)
597
+ figwidth = _not_none(figwidth=figwidth, width=width)
598
+ figheight = _not_none(figheight=figheight, height=height)
599
+ messages = []
600
+ if journal is not None:
601
+ jwidth, jheight = _get_journal_size(journal)
602
+ if jwidth is not None and figwidth is not None:
603
+ messages.append(("journal", journal, "figwidth", figwidth))
604
+ if jheight is not None and figheight is not None:
605
+ messages.append(("journal", journal, "figheight", figheight))
606
+ figwidth = _not_none(jwidth, figwidth)
607
+ figheight = _not_none(jheight, figheight)
608
+ if figwidth is not None and refwidth is not None:
609
+ messages.append(("figwidth", figwidth, "refwidth", refwidth))
610
+ refwidth = None
611
+ if figheight is not None and refheight is not None:
612
+ messages.append(("figheight", figheight, "refheight", refheight))
613
+ refheight = None
614
+ if (
615
+ figwidth is None
616
+ and figheight is None
617
+ and refwidth is None
618
+ and refheight is None
619
+ ): # noqa: E501
620
+ refwidth = rc["subplots.refwidth"] # always inches
621
+ if np.iterable(refaspect):
622
+ refaspect = refaspect[0] / refaspect[1]
623
+ for key1, val1, key2, val2 in messages:
624
+ warnings._warn_ultraplot(
625
+ f"Got conflicting figure size arguments {key1}={val1!r} and "
626
+ f"{key2}={val2!r}. Ignoring {key2!r}."
627
+ )
628
+ self._refnum = refnum
629
+ self._refaspect = refaspect
630
+ self._refaspect_default = 1 # updated for imshow and geographic plots
631
+ self._refwidth = units(refwidth, "in")
632
+ self._refheight = units(refheight, "in")
633
+ self._figwidth = figwidth = units(figwidth, "in")
634
+ self._figheight = figheight = units(figheight, "in")
635
+
636
+ # Add special consideration for interactive backends
637
+ backend = _not_none(rc.backend, "")
638
+ backend = backend.lower()
639
+ interactive = "nbagg" in backend or "ipympl" in backend
640
+ if not interactive:
641
+ pass
642
+ elif figwidth is None or figheight is None:
643
+ figsize = rc["figure.figsize"] # modified by ultraplot
644
+ self._figwidth = figwidth = _not_none(figwidth, figsize[0])
645
+ self._figheight = figheight = _not_none(figheight, figsize[1])
646
+ self._refwidth = self._refheight = None # critical!
647
+ if self._warn_interactive:
648
+ Figure._warn_interactive = False # set class attribute
649
+ warnings._warn_ultraplot(
650
+ "Auto-sized ultraplot figures are not compatible with interactive "
651
+ "backends like '%matplotlib widget' and '%matplotlib notebook'. "
652
+ f"Reverting to the figure size ({figwidth}, {figheight}). To make "
653
+ "auto-sized figures, please consider using the non-interactive "
654
+ "(default) backend. This warning message is shown the first time "
655
+ "you create a figure without explicitly specifying the size."
656
+ )
657
+
658
+ # Add space settings
659
+ # NOTE: This is analogous to 'subplotpars' but we don't worry about
660
+ # user mutability. Think it's perfectly fine to ask users to simply
661
+ # pass these to pplt.figure() or pplt.subplots(). Also overriding
662
+ # 'subplots_adjust' would be confusing since we switch to absolute
663
+ # units and that function is heavily used outside of ultraplot.
664
+ params = {
665
+ "left": left,
666
+ "right": right,
667
+ "top": top,
668
+ "bottom": bottom,
669
+ "wspace": wspace,
670
+ "hspace": hspace,
671
+ "space": space,
672
+ "wequal": wequal,
673
+ "hequal": hequal,
674
+ "equal": equal,
675
+ "wgroup": wgroup,
676
+ "hgroup": hgroup,
677
+ "group": group,
678
+ "wpad": wpad,
679
+ "hpad": hpad,
680
+ "pad": pad,
681
+ "outerpad": outerpad,
682
+ "innerpad": innerpad,
683
+ "panelpad": panelpad,
684
+ }
685
+ self._gridspec_params = params # used to initialize the gridspec
686
+ for key, value in tuple(params.items()):
687
+ if not isinstance(value, str) and np.iterable(value) and len(value) > 1:
688
+ raise ValueError(
689
+ f"Invalid gridspec parameter {key}={value!r}. Space parameters "
690
+ "passed to Figure() must be scalar. For vector spaces use "
691
+ "GridSpec() or pass space parameters to subplots()."
692
+ )
693
+
694
+ # Add tight layout setting and ignore native settings
695
+ pars = kwargs.pop("subplotpars", None)
696
+ if pars is not None:
697
+ warnings._warn_ultraplot(
698
+ f"Ignoring subplotpars={pars!r}. " + self._space_message
699
+ )
700
+ if kwargs.pop("tight_layout", None):
701
+ warnings._warn_ultraplot("Ignoring tight_layout=True. " + self._tight_message)
702
+ if kwargs.pop("constrained_layout", None):
703
+ warnings._warn_ultraplot(
704
+ "Ignoring constrained_layout=True. " + self._tight_message
705
+ )
706
+ if rc_matplotlib.get("figure.autolayout", False):
707
+ warnings._warn_ultraplot(
708
+ "Setting rc['figure.autolayout'] to False. " + self._tight_message
709
+ )
710
+ if rc_matplotlib.get("figure.constrained_layout.use", False):
711
+ warnings._warn_ultraplot(
712
+ "Setting rc['figure.constrained_layout.use'] to False. "
713
+ + self._tight_message # noqa: E501
714
+ )
715
+ try:
716
+ rc_matplotlib["figure.autolayout"] = False # this is rcParams
717
+ except KeyError:
718
+ pass
719
+ try:
720
+ rc_matplotlib["figure.constrained_layout.use"] = False # this is rcParams
721
+ except KeyError:
722
+ pass
723
+ self._tight_active = _not_none(tight, rc["subplots.tight"])
724
+
725
+ # Translate share settings
726
+ translate = {"labels": 1, "labs": 1, "limits": 2, "lims": 2, "all": 4}
727
+ sharex = _not_none(sharex, share, rc["subplots.share"])
728
+ sharey = _not_none(sharey, share, rc["subplots.share"])
729
+ sharex = 3 if sharex is True else translate.get(sharex, sharex)
730
+ sharey = 3 if sharey is True else translate.get(sharey, sharey)
731
+ if sharex not in range(5):
732
+ raise ValueError(f"Invalid sharex={sharex!r}. " + self._share_message)
733
+ if sharey not in range(5):
734
+ raise ValueError(f"Invalid sharey={sharey!r}. " + self._share_message)
735
+ self._sharex = int(sharex)
736
+ self._sharey = int(sharey)
737
+
738
+ # Translate span and align settings
739
+ spanx = _not_none(
740
+ spanx, span, False if not sharex else None, rc["subplots.span"]
741
+ ) # noqa: E501
742
+ spany = _not_none(
743
+ spany, span, False if not sharey else None, rc["subplots.span"]
744
+ ) # noqa: E501
745
+ if spanx and (alignx or align): # only warn when explicitly requested
746
+ warnings._warn_ultraplot('"alignx" has no effect when spanx=True.')
747
+ if spany and (aligny or align):
748
+ warnings._warn_ultraplot('"aligny" has no effect when spany=True.')
749
+ self._spanx = bool(spanx)
750
+ self._spany = bool(spany)
751
+ alignx = _not_none(alignx, align, rc["subplots.align"])
752
+ aligny = _not_none(aligny, align, rc["subplots.align"])
753
+ self._alignx = bool(alignx)
754
+ self._aligny = bool(aligny)
755
+
756
+ # Initialize the figure
757
+ # NOTE: Super labels are stored inside {axes: text} dictionaries
758
+ self._gridspec = None
759
+ self._panel_dict = {"left": [], "right": [], "bottom": [], "top": []}
760
+ self._subplot_dict = {} # subplots indexed by number
761
+ self._subplot_counter = 0 # avoid add_subplot() returning an existing subplot
762
+ self._is_adjusting = False
763
+ self._is_authorized = False
764
+ self._includepanels = None
765
+ self._render_context = {}
766
+ rc_kw, rc_mode = _pop_rc(kwargs)
767
+ kw_format = _pop_params(kwargs, self._format_signature)
768
+ if figwidth is not None and figheight is not None:
769
+ kwargs["figsize"] = (figwidth, figheight)
770
+ with self._context_authorized():
771
+ super().__init__(**kwargs)
772
+
773
+ # Super labels. We don't rely on private matplotlib _suptitle attribute and
774
+ # _align_axis_labels supports arbitrary spanning labels for subplot groups.
775
+ # NOTE: Don't use 'anchor' rotation mode otherwise switching to horizontal
776
+ # left and right super labels causes overlap. Current method is fine.
777
+ self._suptitle = self.text(0.5, 0.95, "", ha="center", va="bottom")
778
+ self._supxlabel_dict = {} # an axes: label mapping
779
+ self._supylabel_dict = {} # an axes: label mapping
780
+ self._suplabel_dict = {"left": {}, "right": {}, "bottom": {}, "top": {}}
781
+ self._suptitle_pad = rc["suptitle.pad"]
782
+ d = self._suplabel_props = {} # store the super label props
783
+ d["left"] = {"va": "center", "ha": "right"}
784
+ d["right"] = {"va": "center", "ha": "left"}
785
+ d["bottom"] = {"va": "top", "ha": "center"}
786
+ d["top"] = {"va": "bottom", "ha": "center"}
787
+ d = self._suplabel_pad = {} # store the super label padding
788
+ d["left"] = rc["leftlabel.pad"]
789
+ d["right"] = rc["rightlabel.pad"]
790
+ d["bottom"] = rc["bottomlabel.pad"]
791
+ d["top"] = rc["toplabel.pad"]
792
+
793
+ # Format figure
794
+ # NOTE: This ignores user-input rc_mode.
795
+ self.format(rc_kw=rc_kw, rc_mode=1, skip_axes=True, **kw_format)
796
+
797
+ def _context_adjusting(self, cache=True):
798
+ """
799
+ Prevent re-running auto layout steps due to draws triggered by figure
800
+ resizes. Otherwise can get infinite loops.
801
+ """
802
+ kw = {"_is_adjusting": True}
803
+ if not cache:
804
+ kw["_cachedRenderer"] = None # temporarily ignore it
805
+ return context._state_context(self, **kw)
806
+
807
+ def _context_authorized(self):
808
+ """
809
+ Prevent warning message when internally calling no-op methods. Otherwise
810
+ emit warnings to help new users.
811
+ """
812
+ return context._state_context(self, _is_authorized=True)
813
+
814
+ @staticmethod
815
+ def _parse_backend(backend=None, basemap=None):
816
+ """
817
+ Handle deprication of basemap and cartopy package.
818
+ """
819
+ if basemap is not None:
820
+ backend = ("cartopy", "basemap")[bool(basemap)]
821
+ warnings._warn_ultraplot(
822
+ f"The 'basemap' keyword was deprecated in version 0.10.0 and will be "
823
+ f"removed in a future release. Please use backend={backend!r} instead."
824
+ )
825
+ return backend
826
+
827
+ def _parse_proj(
828
+ self,
829
+ proj=None,
830
+ projection=None,
831
+ proj_kw=None,
832
+ projection_kw=None,
833
+ backend=None,
834
+ basemap=None,
835
+ **kwargs,
836
+ ):
837
+ """
838
+ Translate the user-input projection into a registered matplotlib
839
+ axes class. Input projection can be a string, `matplotlib.axes.Axes`,
840
+ `cartopy.crs.Projection`, or `mpl_toolkits.basemap.Basemap`.
841
+ """
842
+ # Parse arguments
843
+ proj = _not_none(proj=proj, projection=projection, default="cartesian")
844
+ proj_kw = _not_none(proj_kw=proj_kw, projection_kw=projection_kw, default={})
845
+ backend = self._parse_backend(backend, basemap)
846
+ if isinstance(proj, str):
847
+ proj = proj.lower()
848
+ if isinstance(self, paxes.Axes):
849
+ proj = self._name
850
+ elif isinstance(self, maxes.Axes):
851
+ raise ValueError("Matplotlib axes cannot be added to ultraplot figures.")
852
+
853
+ # Search axes projections
854
+ name = None
855
+ if isinstance(proj, str):
856
+ try:
857
+ mproj.get_projection_class("ultraplot_" + proj)
858
+ except (KeyError, ValueError):
859
+ pass
860
+ else:
861
+ name = proj
862
+ # Helpful error message
863
+ if (
864
+ name is None
865
+ and backend is None
866
+ and isinstance(proj, str)
867
+ and constructor.Projection is object
868
+ and constructor.Basemap is object
869
+ ):
870
+ raise ValueError(
871
+ f"Invalid projection name {proj!r}. If you are trying to generate a "
872
+ "GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap "
873
+ "then cartopy or basemap must be installed. Otherwise the known axes "
874
+ f"subclasses are:\n{paxes._cls_table}"
875
+ )
876
+ # Search geographic projections
877
+ # NOTE: Also raises errors due to unexpected projection type
878
+ if name is None:
879
+ proj = constructor.Proj(proj, backend=backend, include_axes=True, **proj_kw)
880
+ name = proj._proj_backend
881
+ kwargs["map_projection"] = proj
882
+
883
+ kwargs["projection"] = "ultraplot_" + name
884
+ return kwargs
885
+
886
+ def _get_align_axes(self, side):
887
+ """
888
+ Return the main axes along the edge of the figure.
889
+ """
890
+ x, y = "xy" if side in ("left", "right") else "yx"
891
+ axs = self._subplot_dict.values()
892
+ if not axs:
893
+ return []
894
+ ranges = np.array([ax._range_subplotspec(x) for ax in axs])
895
+ edge = ranges[:, 0].min() if side in ("left", "top") else ranges[:, 1].max()
896
+ idx = 0 if side in ("left", "top") else 1
897
+ axs = [ax for ax in axs if ax._range_subplotspec(x)[idx] == edge]
898
+ axs = [ax for ax in sorted(axs, key=lambda ax: ax._range_subplotspec(y)[0])]
899
+ axs = [ax for ax in axs if ax.get_visible()]
900
+ return axs
901
+
902
+ def _get_align_coord(self, side, axs, includepanels=False):
903
+ """
904
+ Return the figure coordinate for centering spanning axis labels or super titles.
905
+ """
906
+ # Get position in figure relative coordinates
907
+ if not all(isinstance(ax, paxes.Axes) for ax in axs):
908
+ raise RuntimeError("Axes must be ultraplot axes.")
909
+ if not all(isinstance(ax, maxes.SubplotBase) for ax in axs):
910
+ raise RuntimeError("Axes must be subplots.")
911
+ s = "y" if side in ("left", "right") else "x"
912
+ axs = [ax._panel_parent or ax for ax in axs] # deflect to main axes
913
+ if includepanels: # include panel short axes?
914
+ axs = [_ for ax in axs for _ in ax._iter_axes(panels=True, children=False)]
915
+ ranges = np.array([ax._range_subplotspec(s) for ax in axs])
916
+ min_, max_ = ranges[:, 0].min(), ranges[:, 1].max()
917
+ ax_lo = axs[np.where(ranges[:, 0] == min_)[0][0]]
918
+ ax_hi = axs[np.where(ranges[:, 1] == max_)[0][0]]
919
+ box_lo = ax_lo.get_subplotspec().get_position(self)
920
+ box_hi = ax_hi.get_subplotspec().get_position(self)
921
+ if s == "x":
922
+ pos = 0.5 * (box_lo.x0 + box_hi.x1)
923
+ else:
924
+ pos = 0.5 * (box_lo.y1 + box_hi.y0) # 'lo' is actually on top of figure
925
+ ax = axs[(np.argmin(ranges[:, 0]) + np.argmax(ranges[:, 1])) // 2]
926
+ ax = ax._panel_parent or ax # always use main subplot for spanning labels
927
+ return pos, ax
928
+
929
+ def _get_offset_coord(self, side, axs, renderer, *, pad=None, extra=None):
930
+ """
931
+ Return the figure coordinate for offsetting super labels and super titles.
932
+ """
933
+ s = "x" if side in ("left", "right") else "y"
934
+ cs = []
935
+ objs = tuple(
936
+ _
937
+ for ax in axs
938
+ for _ in ax._iter_axes(panels=True, children=True, hidden=True)
939
+ ) # noqa: E501
940
+ objs = objs + (extra or ()) # e.g. top super labels
941
+ for obj in objs:
942
+ bbox = obj.get_tightbbox(renderer) # cannot use cached bbox
943
+ attr = s + "max" if side in ("top", "right") else s + "min"
944
+ c = getattr(bbox, attr)
945
+ c = (c, 0) if side in ("left", "right") else (0, c)
946
+ c = self.transFigure.inverted().transform(c)
947
+ c = c[0] if side in ("left", "right") else c[1]
948
+ cs.append(c)
949
+ width, height = self.get_size_inches()
950
+ if pad is None:
951
+ pad = self._suplabel_pad[side] / 72
952
+ pad = pad / width if side in ("left", "right") else pad / height
953
+ return min(cs) - pad if side in ("left", "bottom") else max(cs) + pad
954
+
955
+ def _get_renderer(self):
956
+ """
957
+ Get a renderer at all costs. See matplotlib's tight_layout.py.
958
+ """
959
+ if hasattr(self, "_cached_render"):
960
+ renderer = self._cachedRenderer
961
+ else:
962
+ canvas = self.canvas
963
+ if canvas and hasattr(canvas, "get_renderer"):
964
+ renderer = canvas.get_renderer()
965
+ else:
966
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
967
+
968
+ canvas = FigureCanvasAgg(self)
969
+ renderer = canvas.get_renderer()
970
+ return renderer
971
+
972
+ def _add_axes_panel(self, ax, side=None, **kwargs):
973
+ """
974
+ Add an axes panel.
975
+ """
976
+ # Interpret args
977
+ # NOTE: Axis sharing not implemented for figure panels, 99% of the
978
+ # time this is just used as construct for adding global colorbars and
979
+ # legends, really not worth implementing axis sharing
980
+ ax = ax._altx_parent or ax
981
+ ax = ax._alty_parent or ax
982
+ if not isinstance(ax, paxes.Axes):
983
+ raise RuntimeError("Cannot add panels to non-ultraplot axes.")
984
+ if not isinstance(ax, maxes.SubplotBase):
985
+ raise RuntimeError("Cannot add panels to non-subplot axes.")
986
+ orig = ax._panel_side
987
+ if orig is None:
988
+ pass
989
+ elif side is None or side == orig:
990
+ ax, side = ax._panel_parent, orig
991
+ else:
992
+ raise RuntimeError(f"Cannot add {side!r} panel to existing {orig!r} panel.")
993
+ side = _translate_loc(side, "panel", default=_not_none(orig, "right"))
994
+
995
+ # Add and setup the panel accounting for index changes
996
+ # NOTE: Always put tick labels on the 'outside' and permit arbitrary
997
+ # keyword arguments passed from the user.
998
+ gs = self.gridspec
999
+ if not gs:
1000
+ raise RuntimeError("The gridspec must be active.")
1001
+ kw = _pop_params(kwargs, gs._insert_panel_slot)
1002
+ ss, share = gs._insert_panel_slot(side, ax, **kw)
1003
+ kwargs["autoshare"] = False
1004
+ kwargs.setdefault("number", False) # power users might number panels
1005
+ pax = self.add_subplot(ss, **kwargs)
1006
+ pax._panel_side = side
1007
+ pax._panel_share = share
1008
+ pax._panel_parent = ax
1009
+ ax._panel_dict[side].append(pax)
1010
+ ax._apply_auto_share()
1011
+ axis = pax.yaxis if side in ("left", "right") else pax.xaxis
1012
+ getattr(axis, "tick_" + side)() # set tick and tick label position
1013
+ axis.set_label_position(side) # set label position
1014
+ return pax
1015
+
1016
+ def _add_figure_panel(
1017
+ self, side=None, span=None, row=None, col=None, rows=None, cols=None, **kwargs
1018
+ ):
1019
+ """
1020
+ Add a figure panel.
1021
+ """
1022
+ # Interpret args and enforce sensible keyword args
1023
+ side = _translate_loc(side, "panel", default="right")
1024
+ if side in ("left", "right"):
1025
+ for key, value in (("col", col), ("cols", cols)):
1026
+ if value is not None:
1027
+ raise ValueError(f"Invalid keyword {key!r} for {side!r} panel.")
1028
+ span = _not_none(span=span, row=row, rows=rows)
1029
+ else:
1030
+ for key, value in (("row", row), ("rows", rows)):
1031
+ if value is not None:
1032
+ raise ValueError(f"Invalid keyword {key!r} for {side!r} panel.")
1033
+ span = _not_none(span=span, col=col, cols=cols)
1034
+
1035
+ # Add and setup panel
1036
+ # NOTE: This is only called internally by colorbar and legend so
1037
+ # do not need to pass aribtrary axes keyword arguments.
1038
+ gs = self.gridspec
1039
+ if not gs:
1040
+ raise RuntimeError("The gridspec must be active.")
1041
+ ss, _ = gs._insert_panel_slot(side, span, filled=True, **kwargs)
1042
+ pax = self.add_subplot(ss, autoshare=False, number=False)
1043
+ plist = self._panel_dict[side]
1044
+ plist.append(pax)
1045
+ pax._panel_side = side
1046
+ pax._panel_share = False
1047
+ pax._panel_parent = None
1048
+ return pax
1049
+
1050
+ def _add_subplot(self, *args, **kwargs):
1051
+ """
1052
+ The driver function for adding single subplots.
1053
+ """
1054
+ # Parse arguments
1055
+ kwargs = self._parse_proj(**kwargs)
1056
+ args = args or (1, 1, 1)
1057
+ gs = self.gridspec
1058
+
1059
+ # Integer arg
1060
+ if len(args) == 1 and isinstance(args[0], Integral):
1061
+ if not 111 <= args[0] <= 999:
1062
+ raise ValueError(f"Input {args[0]} must fall between 111 and 999.")
1063
+ args = tuple(map(int, str(args[0])))
1064
+
1065
+ # Subplot spec
1066
+ if len(args) == 1 and isinstance(
1067
+ args[0], (maxes.SubplotBase, mgridspec.SubplotSpec)
1068
+ ):
1069
+ ss = args[0]
1070
+ if isinstance(ss, maxes.SubplotBase):
1071
+ ss = ss.get_subplotspec()
1072
+ if gs is None:
1073
+ gs = ss.get_topmost_subplotspec().get_gridspec()
1074
+ if not isinstance(gs, pgridspec.GridSpec):
1075
+ raise ValueError(
1076
+ "Input subplotspec must be derived from a ultraplot.GridSpec."
1077
+ )
1078
+ if ss.get_topmost_subplotspec().get_gridspec() is not gs:
1079
+ raise ValueError(
1080
+ "Input subplotspec must be derived from the active figure gridspec."
1081
+ )
1082
+
1083
+ # Row and column spec
1084
+ # TODO: How to pass spacing parameters to gridspec? Consider overriding
1085
+ # subplots adjust? Or require using gridspec manually?
1086
+ elif (
1087
+ len(args) == 3
1088
+ and all(isinstance(arg, Integral) for arg in args[:2])
1089
+ and all(isinstance(arg, Integral) for arg in np.atleast_1d(args[2]))
1090
+ ):
1091
+ nrows, ncols, num = args
1092
+ i, j = np.resize(num, 2)
1093
+ if gs is None:
1094
+ gs = pgridspec.GridSpec(nrows, ncols)
1095
+ orows, ocols = gs.get_geometry()
1096
+ if orows % nrows:
1097
+ raise ValueError(
1098
+ f"The input number of rows {nrows} does not divide the "
1099
+ f"figure gridspec number of rows {orows}."
1100
+ )
1101
+ if ocols % ncols:
1102
+ raise ValueError(
1103
+ f"The input number of columns {ncols} does not divide the "
1104
+ f"figure gridspec number of columns {ocols}."
1105
+ )
1106
+ if any(_ < 1 or _ > nrows * ncols for _ in (i, j)):
1107
+ raise ValueError(
1108
+ "The input subplot indices must fall between "
1109
+ f"1 and {nrows * ncols}. Instead got {i} and {j}."
1110
+ )
1111
+ rowfact, colfact = orows // nrows, ocols // ncols
1112
+ irow, icol = divmod(i - 1, ncols) # convert to zero-based
1113
+ jrow, jcol = divmod(j - 1, ncols)
1114
+ irow, icol = irow * rowfact, icol * colfact
1115
+ jrow, jcol = (jrow + 1) * rowfact - 1, (jcol + 1) * colfact - 1
1116
+ ss = gs[irow : jrow + 1, icol : jcol + 1]
1117
+
1118
+ # Otherwise
1119
+ else:
1120
+ raise ValueError(f"Invalid add_subplot positional arguments {args!r}.")
1121
+
1122
+ # Add the subplot
1123
+ # NOTE: Pass subplotspec as keyword arg for mpl >= 3.4 workaround
1124
+ # NOTE: Must assign unique label to each subplot or else subsequent calls
1125
+ # to add_subplot() in mpl < 3.4 may return an already-drawn subplot in the
1126
+ # wrong location due to gridspec override. Is against OO package design.
1127
+ self.gridspec = gs # trigger layout adjustment
1128
+ self._subplot_counter += 1 # unique label for each subplot
1129
+ kwargs.setdefault("label", f"subplot_{self._subplot_counter}")
1130
+ kwargs.setdefault("number", 1 + max(self._subplot_dict, default=0))
1131
+ kwargs.pop("refwidth", None) # TODO: remove this
1132
+ ax = super().add_subplot(ss, _subplot_spec=ss, **kwargs)
1133
+ if ax.number:
1134
+ self._subplot_dict[ax.number] = ax
1135
+ return ax
1136
+
1137
+ def _add_subplots(
1138
+ self,
1139
+ array=None,
1140
+ nrows=1,
1141
+ ncols=1,
1142
+ order="C",
1143
+ proj=None,
1144
+ projection=None,
1145
+ proj_kw=None,
1146
+ projection_kw=None,
1147
+ backend=None,
1148
+ basemap=None,
1149
+ **kwargs,
1150
+ ):
1151
+ """
1152
+ The driver function for adding multiple subplots.
1153
+ """
1154
+
1155
+ # Clunky helper function
1156
+ # TODO: Consider deprecating and asking users to use add_subplot()
1157
+ def _axes_dict(naxs, input, kw=False, default=None):
1158
+ # First build up dictionary
1159
+ if not kw: # 'string' or {1: 'string1', (2, 3): 'string2'}
1160
+ if np.iterable(input) and not isinstance(input, (str, dict)):
1161
+ input = {num + 1: item for num, item in enumerate(input)}
1162
+ elif not isinstance(input, dict):
1163
+ input = {range(1, naxs + 1): input}
1164
+ else: # {key: value} or {1: {key: value1}, (2, 3): {key: value2}}
1165
+ nested = [isinstance(_, dict) for _ in input.values()]
1166
+ if not any(nested): # any([]) == False
1167
+ input = {range(1, naxs + 1): input.copy()}
1168
+ elif not all(nested):
1169
+ raise ValueError(f"Invalid input {input!r}.")
1170
+ # Unfurl keys that contain multiple axes numbers
1171
+ output = {}
1172
+ for nums, item in input.items():
1173
+ nums = np.atleast_1d(nums)
1174
+ for num in nums.flat:
1175
+ output[num] = item.copy() if kw else item
1176
+ # Fill with default values
1177
+ for num in range(1, naxs + 1):
1178
+ if num not in output:
1179
+ output[num] = {} if kw else default
1180
+ if output.keys() != set(range(1, naxs + 1)):
1181
+ raise ValueError(
1182
+ f"Have {naxs} axes, but {input!r} includes props for the axes: "
1183
+ + ", ".join(map(repr, sorted(output)))
1184
+ + "."
1185
+ )
1186
+ return output
1187
+
1188
+ # Build the subplot array
1189
+ # NOTE: Currently this may ignore user-input nrows/ncols without warning
1190
+ if order not in ("C", "F"): # better error message
1191
+ raise ValueError(f"Invalid order={order!r}. Options are 'C' or 'F'.")
1192
+ gs = None
1193
+ if array is None or isinstance(array, mgridspec.GridSpec):
1194
+ if array is not None:
1195
+ gs, nrows, ncols = array, array.nrows, array.ncols
1196
+ array = np.arange(1, nrows * ncols + 1)[..., None]
1197
+ array = array.reshape((nrows, ncols), order=order)
1198
+ else:
1199
+ array = np.atleast_1d(array)
1200
+ array[array == None] = 0 # None or 0 both valid placeholders # noqa: E711
1201
+ array = array.astype(int)
1202
+ if array.ndim == 1: # interpret as single row or column
1203
+ array = array[None, :] if order == "C" else array[:, None]
1204
+ elif array.ndim != 2:
1205
+ raise ValueError(f"Expected 1D or 2D array of integers. Got {array}.")
1206
+
1207
+ # Parse input format, gridspec, and projection arguments
1208
+ # NOTE: Permit figure format keywords for e.g. 'collabels' (more intuitive)
1209
+ nums = np.unique(array[array != 0])
1210
+ naxs = len(nums)
1211
+ if any(num < 0 or not isinstance(num, Integral) for num in nums.flat):
1212
+ raise ValueError(f"Expected array of positive integers. Got {array}.")
1213
+ proj = _not_none(projection=projection, proj=proj)
1214
+ proj = _axes_dict(naxs, proj, kw=False, default="cartesian")
1215
+ proj_kw = _not_none(projection_kw=projection_kw, proj_kw=proj_kw) or {}
1216
+ proj_kw = _axes_dict(naxs, proj_kw, kw=True)
1217
+ backend = self._parse_backend(backend, basemap)
1218
+ backend = _axes_dict(naxs, backend, kw=False)
1219
+ axes_kw = {
1220
+ num: {"proj": proj[num], "proj_kw": proj_kw[num], "backend": backend[num]}
1221
+ for num in proj
1222
+ }
1223
+ for key in ("gridspec_kw", "subplot_kw"):
1224
+ kw = kwargs.pop(key, None)
1225
+ if not kw:
1226
+ continue
1227
+ warnings._warn_ultraplot(
1228
+ f"{key!r} is not necessary in ultraplot. Pass the "
1229
+ "parameters as keyword arguments instead."
1230
+ )
1231
+ kwargs.update(kw or {})
1232
+ figure_kw = _pop_params(kwargs, self._format_signature)
1233
+ gridspec_kw = _pop_params(kwargs, pgridspec.GridSpec._update_params)
1234
+
1235
+ # Create or update the gridspec and add subplots with subplotspecs
1236
+ # NOTE: The gridspec is added to the figure when we pass the subplotspec
1237
+ if gs is None:
1238
+ gs = pgridspec.GridSpec(*array.shape, **gridspec_kw)
1239
+ else:
1240
+ gs.update(**gridspec_kw)
1241
+ axs = naxs * [None] # list of axes
1242
+ axids = [np.where(array == i) for i in np.sort(np.unique(array)) if i > 0]
1243
+ axcols = np.array([[x.min(), x.max()] for _, x in axids])
1244
+ axrows = np.array([[y.min(), y.max()] for y, _ in axids])
1245
+ for idx in range(naxs):
1246
+ num = idx + 1
1247
+ x0, x1 = axcols[idx, 0], axcols[idx, 1]
1248
+ y0, y1 = axrows[idx, 0], axrows[idx, 1]
1249
+ ss = gs[y0 : y1 + 1, x0 : x1 + 1]
1250
+ kw = {**kwargs, **axes_kw[num], "number": num}
1251
+ axs[idx] = self.add_subplot(ss, **kw)
1252
+
1253
+ self.format(skip_axes=True, **figure_kw)
1254
+ return pgridspec.SubplotGrid(axs)
1255
+
1256
+ def _align_axis_label(self, x):
1257
+ """
1258
+ Align *x* and *y* axis labels in the perpendicular and parallel directions.
1259
+ """
1260
+ # NOTE: Always use 'align' if 'span' is True to get correct offset
1261
+ # NOTE: Must trigger axis sharing here so that super label alignment
1262
+ # with tight=False is valid. Kind of kludgey but oh well.
1263
+ seen = set()
1264
+ span = getattr(self, "_span" + x)
1265
+ align = getattr(self, "_align" + x)
1266
+ for ax in self._subplot_dict.values():
1267
+ if isinstance(ax, paxes.CartesianAxes):
1268
+ ax._apply_axis_sharing() # always!
1269
+ else:
1270
+ continue
1271
+ pos = getattr(ax, x + "axis").get_label_position()
1272
+ if ax in seen or pos not in ("bottom", "left"):
1273
+ continue # already aligned or cannot align
1274
+ axs = ax._get_span_axes(pos, panels=False) # returns panel or main axes
1275
+ if any(getattr(ax, "_share" + x) for ax in axs):
1276
+ continue # nothing to align or axes have parents
1277
+ seen.update(axs)
1278
+ if span or align:
1279
+ if hasattr(self, "_align_label_groups"):
1280
+ group = self._align_label_groups[x]
1281
+ else:
1282
+ group = getattr(self, "_align_" + x + "label_grp", None)
1283
+ if group is not None: # fail silently to avoid fragile API changes
1284
+ for ax in axs[1:]:
1285
+ group.join(axs[0], ax) # add to grouper
1286
+ if span:
1287
+ self._update_axis_label(pos, axs)
1288
+
1289
+ def _align_super_labels(self, side, renderer):
1290
+ """
1291
+ Adjust the position of super labels.
1292
+ """
1293
+ # NOTE: Ensure title is offset only here.
1294
+ for ax in self._subplot_dict.values():
1295
+ ax._apply_title_above()
1296
+ if side not in ("left", "right", "bottom", "top"):
1297
+ raise ValueError(f"Invalid side {side!r}.")
1298
+ labs = self._suplabel_dict[side]
1299
+ axs = tuple(ax for ax, lab in labs.items() if lab.get_text())
1300
+ if not axs:
1301
+ return
1302
+ c = self._get_offset_coord(side, axs, renderer)
1303
+ for lab in labs.values():
1304
+ s = "x" if side in ("left", "right") else "y"
1305
+ lab.update({s: c})
1306
+
1307
+ def _align_super_title(self, renderer):
1308
+ """
1309
+ Adjust the position of the super title.
1310
+ """
1311
+ if not self._suptitle.get_text():
1312
+ return
1313
+ axs = self._get_align_axes("top") # returns outermost panels
1314
+ if not axs:
1315
+ return
1316
+ labs = tuple(t for t in self._suplabel_dict["top"].values() if t.get_text())
1317
+ pad = (self._suptitle_pad / 72) / self.get_size_inches()[1]
1318
+ x, _ = self._get_align_coord("top", axs, includepanels=self._includepanels)
1319
+ y = self._get_offset_coord("top", axs, renderer, pad=pad, extra=labs)
1320
+ self._suptitle.set_ha("center")
1321
+ self._suptitle.set_va("bottom")
1322
+ self._suptitle.set_position((x, y))
1323
+
1324
+ def _update_axis_label(self, side, axs):
1325
+ """
1326
+ Update the aligned axis label for the input axes.
1327
+ """
1328
+ # Get the central axis and the spanning label (initialize if it does not exist)
1329
+ # NOTE: Previously we secretly used matplotlib axis labels for spanning labels,
1330
+ # offsetting them between two subplots if necessary. Now we track designated
1331
+ # 'super' labels and replace the actual labels with spaces so they still impact
1332
+ # the tight bounding box and thus allocate space for the spanning label.
1333
+ x, y = "xy" if side in ("bottom", "top") else "yx"
1334
+ c, ax = self._get_align_coord(side, axs, includepanels=self._includepanels)
1335
+ axlab = getattr(ax, x + "axis").label # the central label
1336
+ suplabs = getattr(self, "_sup" + x + "label_dict") # dict of spanning labels
1337
+ suplab = suplabs.get(ax, None)
1338
+ if suplab is None and not axlab.get_text().strip():
1339
+ return # nothing to transfer from the normal label
1340
+ if suplab is not None and not suplab.get_text().strip():
1341
+ return # nothing to update on the super label
1342
+ if suplab is None:
1343
+ props = ("ha", "va", "rotation", "rotation_mode")
1344
+ suplab = suplabs[ax] = self.text(0, 0, "")
1345
+ suplab.update({prop: getattr(axlab, "get_" + prop)() for prop in props})
1346
+
1347
+ # Copy text from the central label to the spanning label
1348
+ # NOTE: Must use spaces rather than newlines, otherwise tight layout
1349
+ # won't make room. Reason is Text implementation (see Text._get_layout())
1350
+ labels._transfer_label(axlab, suplab) # text, color, and font properties
1351
+ count = 1 + suplab.get_text().count("\n")
1352
+ space = "\n".join(" " * count)
1353
+ for ax in axs: # includes original 'axis'
1354
+ axis = getattr(ax, x + "axis")
1355
+ axis.label.set_text(space)
1356
+
1357
+ # Update spanning label position then add simple monkey patch
1358
+ # NOTE: Simply using axis._update_label_position() when this is
1359
+ # called is not sufficient. Fails with e.g. inline backend.
1360
+ t = mtransforms.IdentityTransform() # set in pixels
1361
+ cx, cy = axlab.get_position()
1362
+ if x == "x":
1363
+ trans = mtransforms.blended_transform_factory(self.transFigure, t)
1364
+ coord = (c, cy)
1365
+ else:
1366
+ trans = mtransforms.blended_transform_factory(t, self.transFigure)
1367
+ coord = (cx, c)
1368
+ suplab.set_transform(trans)
1369
+ suplab.set_position(coord)
1370
+ setpos = getattr(mtext.Text, "set_" + y)
1371
+
1372
+ def _set_coord(self, *args, **kwargs): # noqa: E306
1373
+ setpos(self, *args, **kwargs)
1374
+ setpos(suplab, *args, **kwargs)
1375
+
1376
+ setattr(axlab, "set_" + y, _set_coord.__get__(axlab))
1377
+
1378
+ def _update_super_labels(self, side, labels, **kwargs):
1379
+ """
1380
+ Assign the figure super labels and update settings.
1381
+ """
1382
+ # Update the label parameters
1383
+ if side not in ("left", "right", "bottom", "top"):
1384
+ raise ValueError(f"Invalid side {side!r}.")
1385
+ kw = rc.fill(
1386
+ {
1387
+ "color": side + "label.color",
1388
+ "rotation": side + "label.rotation",
1389
+ "size": side + "label.size",
1390
+ "weight": side + "label.weight",
1391
+ "family": "font.family",
1392
+ },
1393
+ context=True,
1394
+ )
1395
+ kw.update(kwargs) # used when updating *existing* labels
1396
+ props = self._suplabel_props[side]
1397
+ props.update(kw) # used when creating *new* labels
1398
+
1399
+ # Get the label axes
1400
+ # WARNING: In case users added labels then changed the subplot geometry we
1401
+ # have to remove labels whose axes don't match the current 'align' axes.
1402
+ axs = self._get_align_axes(side)
1403
+ if not axs:
1404
+ return # occurs if called while adding axes
1405
+ if not labels:
1406
+ labels = [None for _ in axs] # indicates that text should not be updated
1407
+ if not kw and all(_ is None for _ in labels):
1408
+ return # nothing to update
1409
+ if len(labels) != len(axs):
1410
+ raise ValueError(
1411
+ f"Got {len(labels)} {side} labels but found {len(axs)} axes "
1412
+ f"along the {side} side of the figure."
1413
+ )
1414
+ src = self._suplabel_dict[side]
1415
+ extra = src.keys() - set(axs)
1416
+ for ax in extra: # e.g. while adding axes
1417
+ text = src[ax].get_text()
1418
+ if text:
1419
+ warnings._warn_ultraplot(
1420
+ f"Removing {side} label with text {text!r} from axes {ax.number}."
1421
+ )
1422
+ src[ax].remove() # remove from the figure
1423
+
1424
+ # Update the label text
1425
+ tf = self.transFigure
1426
+ for ax, label in zip(axs, labels):
1427
+ if ax in src:
1428
+ obj = src[ax]
1429
+ elif side in ("left", "right"):
1430
+ trans = mtransforms.blended_transform_factory(tf, ax.transAxes)
1431
+ obj = src[ax] = self.text(0, 0.5, "", transform=trans)
1432
+ obj.update(props)
1433
+ else:
1434
+ trans = mtransforms.blended_transform_factory(ax.transAxes, tf)
1435
+ obj = src[ax] = self.text(0.5, 0, "", transform=trans)
1436
+ obj.update(props)
1437
+ if kw:
1438
+ obj.update(kw)
1439
+ if label is not None:
1440
+ obj.set_text(label)
1441
+
1442
+ def _update_super_title(self, title, **kwargs):
1443
+ """
1444
+ Assign the figure super title and update settings.
1445
+ """
1446
+ kw = rc.fill(
1447
+ {
1448
+ "size": "suptitle.size",
1449
+ "weight": "suptitle.weight",
1450
+ "color": "suptitle.color",
1451
+ "family": "font.family",
1452
+ },
1453
+ context=True,
1454
+ )
1455
+ kw.update(kwargs)
1456
+ if kw:
1457
+ self._suptitle.update(kw)
1458
+ if title is not None:
1459
+ self._suptitle.set_text(title)
1460
+
1461
+ @docstring._concatenate_inherited
1462
+ @docstring._snippet_manager
1463
+ def add_axes(self, rect, **kwargs):
1464
+ """
1465
+ %(figure.axes)s
1466
+ """
1467
+ kwargs = self._parse_proj(**kwargs)
1468
+ return super().add_axes(rect, **kwargs)
1469
+
1470
+ @docstring._concatenate_inherited
1471
+ @docstring._snippet_manager
1472
+ def add_subplot(self, *args, **kwargs):
1473
+ """
1474
+ %(figure.subplot)s
1475
+ """
1476
+ return self._add_subplot(*args, **kwargs)
1477
+
1478
+ @docstring._snippet_manager
1479
+ def subplot(self, *args, **kwargs): # shorthand
1480
+ """
1481
+ %(figure.subplot)s
1482
+ """
1483
+ return self._add_subplot(*args, **kwargs)
1484
+
1485
+ @docstring._snippet_manager
1486
+ def add_subplots(self, *args, **kwargs):
1487
+ """
1488
+ %(figure.subplots)s
1489
+ """
1490
+ return self._add_subplots(*args, **kwargs)
1491
+
1492
+ @docstring._snippet_manager
1493
+ def subplots(self, *args, **kwargs):
1494
+ """
1495
+ %(figure.subplots)s
1496
+ """
1497
+ return self._add_subplots(*args, **kwargs)
1498
+
1499
+ def auto_layout(self, renderer=None, aspect=None, tight=None, resize=None):
1500
+ """
1501
+ Automatically adjust the figure size and subplot positions. This is
1502
+ triggered automatically whenever the figure is drawn.
1503
+
1504
+ Parameters
1505
+ ----------
1506
+ renderer : `~matplotlib.backend_bases.RendererBase`, optional
1507
+ The renderer. If ``None`` a default renderer will be produced.
1508
+ aspect : bool, optional
1509
+ Whether to update the figure size based on the reference subplot aspect
1510
+ ratio. By default, this is ``True``. This only has an effect if the
1511
+ aspect ratio is fixed (e.g., due to an image plot or geographic projection).
1512
+ tight : bool, optional
1513
+ Whether to update the figuer size and subplot positions according to
1514
+ a "tight layout". By default, this takes on the value of `tight` passed
1515
+ to `Figure`. If nothing was passed, it is :rc:`subplots.tight`.
1516
+ resize : bool, optional
1517
+ If ``False``, the current figure dimensions are fixed and automatic
1518
+ figure resizing is disabled. By default, the figure size may change
1519
+ unless both `figwidth` and `figheight` or `figsize` were passed
1520
+ to `~Figure.subplots`, `~Figure.set_size_inches` was called manually,
1521
+ or the figure was resized manually with an interactive backend.
1522
+ """
1523
+ # *Impossible* to get notebook backend to work with auto resizing so we
1524
+ # just do the tight layout adjustments and skip resizing.
1525
+ gs = self.gridspec
1526
+ renderer = self._get_renderer()
1527
+ if aspect is None:
1528
+ aspect = True
1529
+ if tight is None:
1530
+ tight = self._tight_active
1531
+ if resize is False: # fix the size
1532
+ self._figwidth, self._figheight = self.get_size_inches()
1533
+ self._refwidth = self._refheight = None # critical!
1534
+
1535
+ # Helper functions
1536
+ # NOTE: Have to draw legends and colorbars early (before reaching axes
1537
+ # draw methods) because we have to take them into account for alignment.
1538
+ # Also requires another figure resize (which triggers a gridspec update).
1539
+ def _draw_content():
1540
+ for ax in self._iter_axes(hidden=False, children=True):
1541
+ ax._add_queued_guides() # may trigger resizes if panels are added
1542
+
1543
+ def _align_content(): # noqa: E306
1544
+ for axis in "xy":
1545
+ self._align_axis_label(axis)
1546
+ for side in ("left", "right", "top", "bottom"):
1547
+ self._align_super_labels(side, renderer)
1548
+ self._align_super_title(renderer)
1549
+
1550
+ # Update the layout
1551
+ # WARNING: Tried to avoid two figure resizes but made
1552
+ # subsequent tight layout really weird. Have to resize twice.
1553
+ _draw_content()
1554
+ if not gs:
1555
+ return
1556
+ if aspect:
1557
+ gs._auto_layout_aspect()
1558
+ _align_content()
1559
+ if tight:
1560
+ gs._auto_layout_tight(renderer)
1561
+ _align_content()
1562
+
1563
+ @warnings._rename_kwargs(
1564
+ "0.10.0", mathtext_fallback="pplt.rc.mathtext_fallback = {}"
1565
+ )
1566
+ @docstring._snippet_manager
1567
+ def format(
1568
+ self,
1569
+ axs=None,
1570
+ *,
1571
+ figtitle=None,
1572
+ suptitle=None,
1573
+ suptitle_kw=None,
1574
+ llabels=None,
1575
+ leftlabels=None,
1576
+ leftlabels_kw=None,
1577
+ rlabels=None,
1578
+ rightlabels=None,
1579
+ rightlabels_kw=None,
1580
+ blabels=None,
1581
+ bottomlabels=None,
1582
+ bottomlabels_kw=None,
1583
+ tlabels=None,
1584
+ toplabels=None,
1585
+ toplabels_kw=None,
1586
+ rowlabels=None,
1587
+ collabels=None, # aliases
1588
+ includepanels=None,
1589
+ **kwargs,
1590
+ ):
1591
+ """
1592
+ Modify figure-wide labels and call ``format`` for the
1593
+ input axes. By default the numbered subplots are used.
1594
+
1595
+ Parameters
1596
+ ----------
1597
+ axs : sequence of `~ultraplot.axes.Axes`, optional
1598
+ The axes to format. Default is the numbered subplots.
1599
+ %(figure.format)s
1600
+
1601
+ Important
1602
+ ---------
1603
+ `leftlabelpad`, `toplabelpad`, `rightlabelpad`, and `bottomlabelpad`
1604
+ keywords are actually :ref:`configuration settings <ug_config>`.
1605
+ We explicitly document these arguments here because it is common to
1606
+ change them for specific figures. But many :ref:`other configuration
1607
+ settings <ug_format>` can be passed to ``format`` too.
1608
+
1609
+ Other parameters
1610
+ ----------------
1611
+ %(axes.format)s
1612
+ %(cartesian.format)s
1613
+ %(polar.format)s
1614
+ %(geo.format)s
1615
+ %(rc.format)s
1616
+
1617
+ See also
1618
+ --------
1619
+ ultraplot.axes.Axes.format
1620
+ ultraplot.axes.CartesianAxes.format
1621
+ ultraplot.axes.PolarAxes.format
1622
+ ultraplot.axes.GeoAxes.format
1623
+ ultraplot.gridspec.SubplotGrid.format
1624
+ ultraplot.config.Configurator.context
1625
+ """
1626
+ # Initiate context block
1627
+ axs = axs or self._subplot_dict.values()
1628
+ skip_axes = kwargs.pop("skip_axes", False) # internal keyword arg
1629
+ rc_kw, rc_mode = _pop_rc(kwargs)
1630
+ with rc.context(rc_kw, mode=rc_mode):
1631
+ # Update background patch
1632
+ kw = rc.fill({"facecolor": "figure.facecolor"}, context=True)
1633
+ self.patch.update(kw)
1634
+
1635
+ # Update super title and label spacing
1636
+ pad = rc.find("suptitle.pad", context=True) # super title
1637
+ if pad is not None:
1638
+ self._suptitle_pad = pad
1639
+ for side in tuple(self._suplabel_pad): # super labels
1640
+ pad = rc.find(side + "label.pad", context=True)
1641
+ if pad is not None:
1642
+ self._suplabel_pad[side] = pad
1643
+ if includepanels is not None:
1644
+ self._includepanels = includepanels
1645
+
1646
+ # Update super title and labels text and settings
1647
+ suptitle_kw = suptitle_kw or {}
1648
+ leftlabels_kw = leftlabels_kw or {}
1649
+ rightlabels_kw = rightlabels_kw or {}
1650
+ bottomlabels_kw = bottomlabels_kw or {}
1651
+ toplabels_kw = toplabels_kw or {}
1652
+ self._update_super_title(
1653
+ _not_none(figtitle=figtitle, suptitle=suptitle),
1654
+ **suptitle_kw,
1655
+ )
1656
+ self._update_super_labels(
1657
+ "left",
1658
+ _not_none(rowlabels=rowlabels, leftlabels=leftlabels, llabels=llabels),
1659
+ **leftlabels_kw,
1660
+ )
1661
+ self._update_super_labels(
1662
+ "right",
1663
+ _not_none(rightlabels=rightlabels, rlabels=rlabels),
1664
+ **rightlabels_kw,
1665
+ )
1666
+ self._update_super_labels(
1667
+ "bottom",
1668
+ _not_none(bottomlabels=bottomlabels, blabels=blabels),
1669
+ **bottomlabels_kw,
1670
+ )
1671
+ self._update_super_labels(
1672
+ "top",
1673
+ _not_none(collabels=collabels, toplabels=toplabels, tlabels=tlabels),
1674
+ **toplabels_kw,
1675
+ )
1676
+
1677
+ # Update the main axes
1678
+ if skip_axes: # avoid recursion
1679
+ return
1680
+ kws = {
1681
+ cls: _pop_params(kwargs, sig)
1682
+ for cls, sig in paxes.Axes._format_signatures.items()
1683
+ }
1684
+ classes = set() # track used dictionaries
1685
+ for ax in axs:
1686
+ kw = {
1687
+ key: value
1688
+ for cls, kw in kws.items()
1689
+ for key, value in kw.items()
1690
+ if isinstance(ax, cls) and not classes.add(cls)
1691
+ }
1692
+ ax.format(rc_kw=rc_kw, rc_mode=rc_mode, skip_figure=True, **kw, **kwargs)
1693
+
1694
+ # Warn unused keyword argument(s)
1695
+ kw = {
1696
+ key: value
1697
+ for name in kws.keys() - classes
1698
+ for key, value in kws[name].items()
1699
+ }
1700
+ if kw:
1701
+ warnings._warn_ultraplot(
1702
+ f"Ignoring unused projection-specific format() keyword argument(s): {kw}" # noqa: E501
1703
+ )
1704
+
1705
+ @docstring._concatenate_inherited
1706
+ @docstring._snippet_manager
1707
+ def colorbar(
1708
+ self,
1709
+ mappable,
1710
+ values=None,
1711
+ loc=None,
1712
+ location=None,
1713
+ row=None,
1714
+ col=None,
1715
+ rows=None,
1716
+ cols=None,
1717
+ span=None,
1718
+ space=None,
1719
+ pad=None,
1720
+ width=None,
1721
+ **kwargs,
1722
+ ):
1723
+ """
1724
+ Add a colorbar along the side of the figure.
1725
+
1726
+ Parameters
1727
+ ----------
1728
+ %(axes.colorbar_args)s
1729
+ length : float, default: :rc:`colorbar.length`
1730
+ The colorbar length. Units are relative to the span of the rows and
1731
+ columns of subplots.
1732
+ shrink : float, optional
1733
+ Alias for `length`. This is included for consistency with
1734
+ `matplotlib.figure.Figure.colorbar`.
1735
+ width : unit-spec, default: :rc:`colorbar.width`
1736
+ The colorbar width.
1737
+ %(units.in)s
1738
+ %(figure.colorbar_space)s
1739
+ Has no visible effect if `length` is ``1``.
1740
+
1741
+ Other parameters
1742
+ ----------------
1743
+ %(axes.colorbar_kwargs)s
1744
+
1745
+ See also
1746
+ --------
1747
+ ultraplot.axes.Axes.colorbar
1748
+ matplotlib.figure.Figure.colorbar
1749
+ """
1750
+ # Backwards compatibility
1751
+ ax = kwargs.pop("ax", None)
1752
+ cax = kwargs.pop("cax", None)
1753
+ if isinstance(values, maxes.Axes):
1754
+ cax = _not_none(cax_positional=values, cax=cax)
1755
+ values = None
1756
+ if isinstance(loc, maxes.Axes):
1757
+ ax = _not_none(ax_positional=loc, ax=ax)
1758
+ loc = None
1759
+ # Helpful warning
1760
+ if kwargs.pop("use_gridspec", None) is not None:
1761
+ warnings._warn_ultraplot(
1762
+ "Ignoring the 'use_gridspec' keyword. ultraplot always allocates "
1763
+ "additional space for colorbars using the figure gridspec "
1764
+ "rather than 'stealing space' from the parent subplot."
1765
+ )
1766
+ # Fill this axes
1767
+ if cax is not None:
1768
+ with context._state_context(cax, _internal_call=True): # do not wrap pcolor
1769
+ cb = super().colorbar(mappable, cax=cax, **kwargs)
1770
+ # Axes panel colorbar
1771
+ elif ax is not None:
1772
+ cb = ax.colorbar(
1773
+ mappable, values, space=space, pad=pad, width=width, **kwargs
1774
+ )
1775
+ # Figure panel colorbar
1776
+ else:
1777
+ loc = _not_none(loc=loc, location=location, default="r")
1778
+ ax = self._add_figure_panel(
1779
+ loc,
1780
+ row=row,
1781
+ col=col,
1782
+ rows=rows,
1783
+ cols=cols,
1784
+ span=span,
1785
+ width=width,
1786
+ space=space,
1787
+ pad=pad,
1788
+ )
1789
+ cb = ax.colorbar(mappable, values, loc="fill", **kwargs)
1790
+ return cb
1791
+
1792
+ @docstring._concatenate_inherited
1793
+ @docstring._snippet_manager
1794
+ def legend(
1795
+ self,
1796
+ handles=None,
1797
+ labels=None,
1798
+ loc=None,
1799
+ location=None,
1800
+ row=None,
1801
+ col=None,
1802
+ rows=None,
1803
+ cols=None,
1804
+ span=None,
1805
+ space=None,
1806
+ pad=None,
1807
+ width=None,
1808
+ **kwargs,
1809
+ ):
1810
+ """
1811
+ Add a legend along the side of the figure.
1812
+
1813
+ Parameters
1814
+ ----------
1815
+ %(axes.legend_args)s
1816
+ %(figure.legend_space)s
1817
+ width : unit-spec, optional
1818
+ The space allocated for the legend box. This does nothing if
1819
+ the :ref:`tight layout algorithm <ug_tight>` is active for the figure.
1820
+ %(units.in)s
1821
+
1822
+ Other parameters
1823
+ ----------------
1824
+ %(axes.legend_kwargs)s
1825
+
1826
+ See also
1827
+ --------
1828
+ ultraplot.axes.Axes.legend
1829
+ matplotlib.axes.Axes.legend
1830
+ """
1831
+ ax = kwargs.pop("ax", None)
1832
+ # Axes panel legend
1833
+ if ax is not None:
1834
+ leg = ax.legend(
1835
+ handles, labels, space=space, pad=pad, width=width, **kwargs
1836
+ )
1837
+ # Figure panel legend
1838
+ else:
1839
+ loc = _not_none(loc=loc, location=location, default="r")
1840
+ ax = self._add_figure_panel(
1841
+ loc,
1842
+ row=row,
1843
+ col=col,
1844
+ rows=rows,
1845
+ cols=cols,
1846
+ span=span,
1847
+ width=width,
1848
+ space=space,
1849
+ pad=pad,
1850
+ )
1851
+ leg = ax.legend(handles, labels, loc="fill", **kwargs)
1852
+ return leg
1853
+
1854
+ @docstring._snippet_manager
1855
+ def save(self, filename, **kwargs):
1856
+ """
1857
+ %(figure.save)s
1858
+ """
1859
+ return self.savefig(filename, **kwargs)
1860
+
1861
+ @docstring._concatenate_inherited
1862
+ @docstring._snippet_manager
1863
+ def savefig(self, filename, **kwargs):
1864
+ """
1865
+ %(figure.save)s
1866
+ """
1867
+ # Automatically expand the user name. Undocumented because we
1868
+ # do not want to overwrite the matplotlib docstring.
1869
+ if isinstance(filename, str):
1870
+ filename = os.path.expanduser(filename)
1871
+ super().savefig(filename, **kwargs)
1872
+
1873
+ @docstring._concatenate_inherited
1874
+ def set_canvas(self, canvas):
1875
+ """
1876
+ Set the figure canvas. Add monkey patches for the instance-level
1877
+ `~matplotlib.backend_bases.FigureCanvasBase.draw` and
1878
+ `~matplotlib.backend_bases.FigureCanvasBase.print_figure` methods.
1879
+
1880
+ Parameters
1881
+ ----------
1882
+ canvas : `~matplotlib.backend_bases.FigureCanvasBase`
1883
+ The figure canvas.
1884
+
1885
+ See also
1886
+ --------
1887
+ matplotlib.figure.Figure.set_canvas
1888
+ """
1889
+ # NOTE: Use the _draw method if it exists, e.g. for osx backends. Critical
1890
+ # or else wrong renderer size is used.
1891
+ # NOTE: See _add_canvas_preprocessor for details. Critical to not add cache
1892
+ # print_figure renderer when the print method (print_pdf, print_png, etc.)
1893
+ # calls Figure.draw(). Otherwise have issues where (1) figure size and/or
1894
+ # bounds are incorrect after saving figure *then* displaying it in qt or inline
1895
+ # notebook backends, and (2) figure fails to update correctly after successively
1896
+ # modifying and displaying within inline notebook backend (previously worked
1897
+ # around this by forcing additional draw() call in this function before
1898
+ # proceeding with print_figure). Set the canvas and add monkey patches
1899
+ # to the instance-level draw and print_figure methods.
1900
+ method = "draw"
1901
+ # if getattr(canvas, "_draw", None):
1902
+ # method = "_draw"
1903
+ # method = '_draw' if callable(getattr(canvas, '_draw', None)) else 'draw'
1904
+ _add_canvas_preprocessor(canvas, "print_figure", cache=False) # saves, inlines
1905
+ _add_canvas_preprocessor(canvas, method, cache=True) # renderer displays
1906
+ super().set_canvas(canvas)
1907
+
1908
+ def _is_same_size(self, figsize, eps=None):
1909
+ """
1910
+ Test if the figure size is unchanged up to some tolerance in inches.
1911
+ """
1912
+ eps = _not_none(eps, 0.01)
1913
+ figsize_active = self.get_size_inches()
1914
+ if figsize is None: # e.g. GridSpec._calc_figsize() returned None
1915
+ return True
1916
+ else:
1917
+ return np.all(np.isclose(figsize, figsize_active, rtol=0, atol=eps))
1918
+
1919
+ @docstring._concatenate_inherited
1920
+ def set_size_inches(self, w, h=None, *, forward=True, internal=False, eps=None):
1921
+ """
1922
+ Set the figure size. If this is being called manually or from an interactive
1923
+ backend, update the default layout with this fixed size. If the figure size is
1924
+ unchanged or this is an internal call, do not update the default layout.
1925
+
1926
+ Parameters
1927
+ ----------
1928
+ *args : float
1929
+ The width and height passed as positional arguments or a 2-tuple.
1930
+ forward : bool, optional
1931
+ Whether to update the canvas.
1932
+ internal : bool, optional
1933
+ Whether this is an internal resize.
1934
+ eps : float, optional
1935
+ The deviation from the current size in inches required to treat this
1936
+ as a user-triggered figure resize that fixes the layout.
1937
+
1938
+ See also
1939
+ --------
1940
+ matplotlib.figure.Figure.set_size_inches
1941
+ """
1942
+ # Parse input args
1943
+ figsize = w if h is None else (w, h)
1944
+ if not np.all(np.isfinite(figsize)):
1945
+ raise ValueError(f"Figure size must be finite, not {figsize}.")
1946
+
1947
+ # Fix the figure size if this is a user action from an interactive backend
1948
+ # NOTE: If we fail to detect 'user' resize from the user, not only will
1949
+ # result be incorrect, but qt backend will crash because it detects a
1950
+ # recursive size change, since preprocessor size will differ.
1951
+ # NOTE: Bitmap renderers calculate the figure size in inches from
1952
+ # int(Figure.bbox.[width|height]) which rounds to whole pixels. When
1953
+ # renderer calls set_size_inches, size may be effectively the same, but
1954
+ # slightly changed due to roundoff error! Therefore only compare approx size.
1955
+ attrs = ("_is_idle_drawing", "_is_drawing", "_draw_pending")
1956
+ backend = any(getattr(self.canvas, attr, None) for attr in attrs)
1957
+ internal = internal or self._is_adjusting
1958
+ samesize = self._is_same_size(figsize, eps)
1959
+ ctx = context._empty_context() # context not necessary most of the time
1960
+ if not backend and not internal and not samesize:
1961
+ ctx = self._context_adjusting() # do not trigger layout solver
1962
+ self._figwidth, self._figheight = figsize
1963
+ self._refwidth = self._refheight = None # critical!
1964
+
1965
+ # Apply the figure size
1966
+ # NOTE: If size changes we always update the gridspec to enforce fixed spaces
1967
+ # and panel widths (necessary since axes use figure relative coords)
1968
+ with ctx: # avoid recursion
1969
+ super().set_size_inches(figsize, forward=forward)
1970
+ if not samesize: # gridspec positions will resolve differently
1971
+ self.gridspec.update()
1972
+
1973
+ def _iter_axes(self, hidden=False, children=False, panels=True):
1974
+ """
1975
+ Iterate over all axes and panels in the figure belonging to the
1976
+ `~ultraplot.axes.Axes` class. Exclude inset and twin axes.
1977
+
1978
+ Parameters
1979
+ ----------
1980
+ hidden : bool, optional
1981
+ Whether to include "hidden" panels.
1982
+ children : bool, optional
1983
+ Whether to include child axes. Note this now includes "twin" axes.
1984
+ panels : bool or str or sequence of str, optional
1985
+ Whether to include panels or the panels to include.
1986
+ """
1987
+ # Parse panels
1988
+ if panels is False:
1989
+ panels = ()
1990
+ elif panels is True or panels is None:
1991
+ panels = ("left", "right", "bottom", "top")
1992
+ elif isinstance(panels, str):
1993
+ panels = (panels,)
1994
+ if not set(panels) <= {"left", "right", "bottom", "top"}:
1995
+ raise ValueError(f"Invalid sides {panels!r}.")
1996
+ # Iterate
1997
+ axs = (
1998
+ *self._subplot_dict.values(),
1999
+ *(ax for side in panels for ax in self._panel_dict[side]),
2000
+ )
2001
+ for ax in axs:
2002
+ if not hidden and ax._panel_hidden:
2003
+ continue # ignore hidden panel and its colorbar/legend child
2004
+ yield from ax._iter_axes(hidden=hidden, children=children, panels=panels)
2005
+
2006
+ @property
2007
+ def gridspec(self):
2008
+ """
2009
+ The single `~ultraplot.gridspec.GridSpec` instance used for all
2010
+ subplots in the figure.
2011
+
2012
+ See also
2013
+ --------
2014
+ ultraplot.figure.Figure.subplotgrid
2015
+ ultraplot.gridspec.GridSpec.figure
2016
+ ultraplot.gridspec.SubplotGrid.gridspec
2017
+ """
2018
+ return self._gridspec
2019
+
2020
+ @gridspec.setter
2021
+ def gridspec(self, gs):
2022
+ if not isinstance(gs, pgridspec.GridSpec):
2023
+ raise ValueError("Gridspec must be a ultraplot.GridSpec instance.")
2024
+ self._gridspec = gs
2025
+ gs.figure = self # trigger copying settings from the figure
2026
+
2027
+ @property
2028
+ def subplotgrid(self):
2029
+ """
2030
+ A `~ultraplot.gridspec.SubplotGrid` containing the numbered subplots in the
2031
+ figure. The subplots are ordered by increasing `~ultraplot.axes.Axes.number`.
2032
+
2033
+ See also
2034
+ --------
2035
+ ultraplot.figure.Figure.gridspec
2036
+ ultraplot.gridspec.SubplotGrid.figure
2037
+ """
2038
+ return pgridspec.SubplotGrid([s for _, s in sorted(self._subplot_dict.items())])
2039
+
2040
+ @property
2041
+ def tight(self):
2042
+ """
2043
+ Whether the :ref:`tight layout algorithm <ug_tight>` is active for the
2044
+ figure. This value is passed to `~ultraplot.figure.Figure.auto_layout`
2045
+ every time the figure is drawn. Can be changed e.g. ``fig.tight = False``.
2046
+
2047
+ See also
2048
+ --------
2049
+ ultraplot.figure.Figure.auto_layout
2050
+ """
2051
+ return self._tight_active
2052
+
2053
+ @tight.setter
2054
+ def tight(self, b):
2055
+ self._tight_active = bool(b)
2056
+
2057
+ # Apply signature obfuscation after getting keys
2058
+ # NOTE: This is needed for axes and figure instantiation.
2059
+ _format_signature = inspect.signature(format)
2060
+ format = docstring._obfuscate_kwargs(format)
2061
+
2062
+
2063
+ # Add deprecated properties. There are *lots* of properties we pass to Figure
2064
+ # and do not like idea of publicly tracking every single one of them. If we
2065
+ # want to improve user introspection consider modifying Figure.__repr__.
2066
+ for _attr in ("alignx", "aligny", "sharex", "sharey", "spanx", "spany", "tight", "ref"):
2067
+
2068
+ def _get_deprecated(self, attr=_attr):
2069
+ warnings._warn_ultraplot(
2070
+ f"The property {attr!r} is no longer public as of v0.8. It will be "
2071
+ "removed in a future release."
2072
+ )
2073
+ return getattr(self, "_" + attr)
2074
+
2075
+ _getter = property(_get_deprecated)
2076
+ setattr(Figure, _attr, property(_get_deprecated))
2077
+
2078
+
2079
+ # Disable native matplotlib layout and spacing functions when called
2080
+ # manually and emit warning message to help new users.
2081
+ for _attr, _msg in (
2082
+ ("set_tight_layout", Figure._tight_message),
2083
+ ("set_constrained_layout", Figure._tight_message),
2084
+ ("tight_layout", Figure._tight_message),
2085
+ ("init_layoutbox", Figure._tight_message),
2086
+ ("execute_constrained_layout", Figure._tight_message),
2087
+ ("subplots_adjust", Figure._space_message),
2088
+ ):
2089
+ _func = getattr(Figure, _attr, None)
2090
+ if _func is None:
2091
+ continue
2092
+
2093
+ @functools.wraps(_func) # noqa: E301
2094
+ def _disable_method(self, *args, func=_func, message=_msg, **kwargs):
2095
+ message = f"fig.{func.__name__}() has no effect on ultraplot figures. " + message
2096
+ if self._is_authorized:
2097
+ return func(self, *args, **kwargs)
2098
+ else:
2099
+ warnings._warn_ultraplot(message) # noqa: E501, U100
2100
+
2101
+ _disable_method.__doc__ = None # remove docs
2102
+ setattr(Figure, _attr, _disable_method)