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/gridspec.py ADDED
@@ -0,0 +1,1698 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ The gridspec and subplot grid classes used throughout ultraplot.
4
+ """
5
+ import inspect
6
+ import itertools
7
+ import re
8
+ from collections.abc import MutableSequence
9
+ from numbers import Integral
10
+
11
+ import matplotlib.axes as maxes
12
+ import matplotlib.gridspec as mgridspec
13
+ import matplotlib.transforms as mtransforms
14
+ import numpy as np
15
+
16
+ from . import axes as paxes
17
+ from .config import rc
18
+ from .internals import ic # noqa: F401
19
+ from .internals import _not_none, docstring, warnings
20
+ from .utils import _fontsize_to_pt, units
21
+
22
+ __all__ = ["GridSpec", "SubplotGrid", "SubplotsContainer"] # deprecated
23
+
24
+
25
+ # Gridspec vector arguments
26
+ # Valid for figure() and GridSpec()
27
+ _shared_docstring = """
28
+ left, right, top, bottom : unit-spec, default: None
29
+ The fixed space between the subplots and the figure edge.
30
+ %(units.em)s
31
+ If ``None``, the space is determined automatically based on the tick and
32
+ label settings. If :rcraw:`subplots.tight` is ``True`` or ``tight=True`` was
33
+ passed to the figure, the space is determined by the tight layout algorithm.
34
+ """
35
+ _scalar_docstring = """
36
+ wspace, hspace, space : unit-spec, default: None
37
+ The fixed space between grid columns, rows, or both.
38
+ %(units.em)s
39
+ If ``None``, the space is determined automatically based on the font size and axis
40
+ sharing settings. If :rcraw:`subplots.tight` is ``True`` or ``tight=True`` was
41
+ passed to the figure, the space is determined by the tight layout algorithm.
42
+ """
43
+ _vector_docstring = """
44
+ wspace, hspace, space : unit-spec or sequence, default: None
45
+ The fixed space between grid columns, rows, and both, respectively. If
46
+ float, string, or ``None``, this value is expanded into lists of length
47
+ ``ncols - 1`` (for `wspace`) or length ``nrows - 1`` (for `hspace`). If
48
+ a sequence, its length must match these lengths.
49
+ %(units.em)s
50
+
51
+ For elements equal to ``None``, the space is determined automatically based
52
+ on the tick and label settings. If :rcraw:`subplots.tight` is ``True`` or
53
+ ``tight=True`` was passed to the figure, the space is determined by the tight
54
+ layout algorithm. For example, ``subplots(ncols=3, tight=True, wspace=(2, None))``
55
+ fixes the space between columns 1 and 2 but lets the tight layout algorithm
56
+ determine the space between columns 2 and 3.
57
+ wratios, hratios : float or sequence, optional
58
+ Passed to `~ultraplot.gridspec.GridSpec`, denotes the width and height
59
+ ratios for the subplot grid. Length of `wratios` must match the number
60
+ of columns, and length of `hratios` must match the number of rows.
61
+ width_ratios, height_ratios
62
+ Aliases for `wratios`, `hratios`. Included for
63
+ consistency with `matplotlib.gridspec.GridSpec`.
64
+ wpad, hpad, pad : unit-spec or sequence, optional
65
+ The tight layout padding between columns, rows, and both, respectively.
66
+ Unlike ``space``, these control the padding between subplot content
67
+ (including text, ticks, etc.) rather than subplot edges. As with
68
+ ``space``, these can be scalars or arrays optionally containing ``None``.
69
+ For elements equal to ``None``, the default is `innerpad`.
70
+ %(units.em)s
71
+ """
72
+ _tight_docstring = """
73
+ wequal, hequal, equal : bool, default: :rc:`subplots.equalspace`
74
+ Whether to make the tight layout algorithm apply equal spacing
75
+ between columns, rows, or both.
76
+ wgroup, hgroup, group : bool, default: :rc:`subplots.groupspace`
77
+ Whether to make the tight layout algorithm just consider spaces between
78
+ adjacent subplots instead of entire columns and rows of subplots.
79
+ outerpad : unit-spec, default: :rc:`subplots.outerpad`
80
+ The scalar tight layout padding around the left, right, top, bottom figure edges.
81
+ %(units.em)s
82
+ innerpad : unit-spec, default: :rc:`subplots.innerpad`
83
+ The scalar tight layout padding between columns and rows. Synonymous with `pad`.
84
+ %(units.em)s
85
+ panelpad : unit-spec, default: :rc:`subplots.panelpad`
86
+ The scalar tight layout padding between subplots and their panels,
87
+ colorbars, and legends and between "stacks" of these objects.
88
+ %(units.em)s
89
+ """
90
+ docstring._snippet_manager["gridspec.shared"] = _shared_docstring
91
+ docstring._snippet_manager["gridspec.scalar"] = _scalar_docstring
92
+ docstring._snippet_manager["gridspec.vector"] = _vector_docstring
93
+ docstring._snippet_manager["gridspec.tight"] = _tight_docstring
94
+
95
+
96
+ def _disable_method(attr):
97
+ """
98
+ Disable the inherited method.
99
+ """
100
+
101
+ def _dummy_method(*args):
102
+ raise RuntimeError(f"Method {attr}() is disabled on ultraplot gridspecs.")
103
+
104
+ _dummy_method.__name__ = attr
105
+ return _dummy_method
106
+
107
+
108
+ class _SubplotSpec(mgridspec.SubplotSpec):
109
+ """
110
+ A thin `~matplotlib.gridspec.SubplotSpec` subclass with a nice string
111
+ representation and a few helper methods.
112
+ """
113
+
114
+ def __repr__(self):
115
+ # NOTE: Also include panel obfuscation here to avoid confusion. If this
116
+ # is a panel slot generated internally then show zero info.
117
+ try:
118
+ nrows, ncols, num1, num2 = self._get_geometry()
119
+ except (IndexError, ValueError, AttributeError):
120
+ return "SubplotSpec(unknown)"
121
+ else:
122
+ return f"SubplotSpec(nrows={nrows}, ncols={ncols}, index=({num1}, {num2}))"
123
+
124
+ def _get_geometry(self):
125
+ """
126
+ Return the geometry and scalar indices relative to the "unhidden" non-panel
127
+ geometry. May trigger error if this is in a "hidden" panel slot.
128
+ """
129
+ gs = self.get_gridspec()
130
+ num1, num2 = self.num1, self.num2
131
+ if isinstance(gs, GridSpec):
132
+ nrows, ncols = gs.get_geometry()
133
+ num1, num2 = gs._decode_indices(num1, num2) # may trigger error
134
+ return nrows, ncols, num1, num2
135
+
136
+ def _get_rows_columns(self, ncols=None):
137
+ """
138
+ Return the row and column indices. The resulting indices include
139
+ "hidden" panel rows and columns. See `GridSpec.get_grid_positions`.
140
+ """
141
+ # NOTE: Sort of confusing that this doesn't have 'total' in name but that
142
+ # is by analogy with get_grid_positions(). This is used for grid positioning.
143
+ gs = self.get_gridspec()
144
+ if isinstance(gs, GridSpec):
145
+ ncols = _not_none(ncols, gs.ncols_total)
146
+ else:
147
+ ncols = _not_none(ncols, gs.ncols)
148
+ row1, col1 = divmod(self.num1, ncols)
149
+ row2, col2 = divmod(self.num2, ncols)
150
+ return row1, row2, col1, col2
151
+
152
+ def get_position(self, figure, return_all=False):
153
+ # Silent override. Older matplotlib versions can create subplots
154
+ # with negative heights and widths that crash on instantiation.
155
+ # Instead better to dynamically adjust the bounding box and hope
156
+ # that subsequent adjustments will correct the subplot position.
157
+ gs = self.get_gridspec()
158
+ if isinstance(gs, GridSpec):
159
+ nrows, ncols = gs.get_total_geometry()
160
+ else:
161
+ nrows, ncols = gs.get_geometry()
162
+ rows, cols = np.unravel_index([self.num1, self.num2], (nrows, ncols))
163
+ bottoms, tops, lefts, rights = gs.get_grid_positions(figure)
164
+ bottom = bottoms[rows].min()
165
+ top = max(bottom, tops[rows].max())
166
+ left = lefts[cols].min()
167
+ right = max(left, rights[cols].max())
168
+ bbox = mtransforms.Bbox.from_extents(left, bottom, right, top)
169
+ if return_all:
170
+ return bbox, rows[0], cols[0], nrows, ncols
171
+ else:
172
+ return bbox
173
+
174
+
175
+ class GridSpec(mgridspec.GridSpec):
176
+ """
177
+ A `~matplotlib.gridspec.GridSpec` subclass that permits variable spacing
178
+ between successive rows and columns and hides "panel slots" from indexing.
179
+ """
180
+
181
+ def __repr__(self):
182
+ nrows, ncols = self.get_geometry()
183
+ prows, pcols = self.get_panel_geometry()
184
+ params = {"nrows": nrows, "ncols": ncols}
185
+ if prows:
186
+ params["nrows_panel"] = prows
187
+ if pcols:
188
+ params["ncols_panel"] = pcols
189
+ params = ", ".join(f"{key}={value!r}" for key, value in params.items())
190
+ return f"GridSpec({params})"
191
+
192
+ def __getattr__(self, attr):
193
+ # Redirect to private 'layout' attributes that are fragile w.r.t.
194
+ # matplotlib version. Cannot set these by calling super().__init__()
195
+ # because we make spacing arguments non-settable properties.
196
+ if "layout" in attr:
197
+ return None
198
+ super().__getattribute__(attr) # native error message
199
+
200
+ @docstring._snippet_manager
201
+ def __init__(self, nrows=1, ncols=1, **kwargs):
202
+ """
203
+ Parameters
204
+ ----------
205
+ nrows : int, optional
206
+ The number of rows in the subplot grid.
207
+ ncols : int, optional
208
+ The number of columns in the subplot grid.
209
+
210
+ Other parameters
211
+ ----------------
212
+ %(gridspec.shared)s
213
+ %(gridspec.vector)s
214
+ %(gridspec.tight)s
215
+
216
+ See also
217
+ --------
218
+ ultraplot.ui.figure
219
+ ultraplot.figure.Figure
220
+ ultraplot.ui.subplots
221
+ ultraplot.figure.Figure.subplots
222
+ ultraplot.figure.Figure.add_subplots
223
+ matplotlib.gridspec.GridSpec
224
+
225
+ Important
226
+ ---------
227
+ Adding axes panels, axes or figure colorbars, and axes or figure legends
228
+ quietly augments the gridspec geometry by inserting "panel slots". However,
229
+ subsequently indexing the gridspec with ``gs[num]`` or ``gs[row, col]`` will
230
+ ignore the "panel slots". This permits adding new subplots by passing
231
+ ``gs[num]`` or ``gs[row, col]`` to `~ultraplot.figure.Figure.add_subplot`
232
+ even in the presence of panels (see `~GridSpec.__getitem__` for details).
233
+ This also means that each `GridSpec` is `~ultraplot.figure.Figure`-specific,
234
+ i.e. it can only be used once (if you are working with `GridSpec` instances
235
+ manually and want the same geometry for multiple figures, you must create
236
+ a copy with `GridSpec.copy` before working on the subsequent figure).
237
+ """
238
+ # Fundamental GridSpec properties
239
+ self._nrows_total = nrows
240
+ self._ncols_total = ncols
241
+ self._left = None
242
+ self._right = None
243
+ self._bottom = None
244
+ self._top = None
245
+ self._hspace_total = [None] * (nrows - 1)
246
+ self._wspace_total = [None] * (ncols - 1)
247
+ self._hratios_total = [1] * nrows
248
+ self._wratios_total = [1] * ncols
249
+ self._left_default = None
250
+ self._right_default = None
251
+ self._bottom_default = None
252
+ self._top_default = None
253
+ self._hspace_total_default = [None] * (nrows - 1)
254
+ self._wspace_total_default = [None] * (ncols - 1)
255
+ self._figure = None # initial state
256
+
257
+ # Capture rc settings used for default spacing
258
+ # NOTE: This is consistent with conversion of 'em' units to inches on gridspec
259
+ # instantiation. In general it seems strange for future changes to rc settings
260
+ # to magically update an existing gridspec layout. This also may improve draw
261
+ # time as manual or auto figure resizes repeatedly call get_grid_positions().
262
+ scales = {"in": 0, "inout": 0.5, "out": 1, None: 1}
263
+ self._xtickspace = scales[rc["xtick.direction"]] * rc["xtick.major.size"]
264
+ self._ytickspace = scales[rc["ytick.direction"]] * rc["ytick.major.size"]
265
+ self._xticklabelspace = (
266
+ _fontsize_to_pt(rc["xtick.labelsize"]) + rc["xtick.major.pad"]
267
+ ) # noqa: E501
268
+ self._yticklabelspace = (
269
+ 2 * _fontsize_to_pt(rc["ytick.labelsize"]) + rc["ytick.major.pad"]
270
+ ) # noqa: E501
271
+ self._labelspace = _fontsize_to_pt(rc["axes.labelsize"]) + rc["axes.labelpad"]
272
+ self._titlespace = _fontsize_to_pt(rc["axes.titlesize"]) + rc["axes.titlepad"]
273
+
274
+ # Tight layout and panel-related properties
275
+ # NOTE: The wpanels and hpanels contain empty strings '' (indicating main axes),
276
+ # or one of 'l', 'r', 'b', 't' (indicating axes panels) or 'f' (figure panels)
277
+ outerpad = _not_none(kwargs.pop("outerpad", None), rc["subplots.outerpad"])
278
+ innerpad = _not_none(kwargs.pop("innerpad", None), rc["subplots.innerpad"])
279
+ panelpad = _not_none(kwargs.pop("panelpad", None), rc["subplots.panelpad"])
280
+ pad = _not_none(kwargs.pop("pad", None), innerpad) # alias of innerpad
281
+ self._outerpad = units(outerpad, "em", "in")
282
+ self._innerpad = units(innerpad, "em", "in")
283
+ self._panelpad = units(panelpad, "em", "in")
284
+ self._hpad_total = [units(pad, "em", "in")] * (nrows - 1)
285
+ self._wpad_total = [units(pad, "em", "in")] * (ncols - 1)
286
+ self._hequal = rc["subplots.equalspace"]
287
+ self._wequal = rc["subplots.equalspace"]
288
+ self._hgroup = rc["subplots.groupspace"]
289
+ self._wgroup = rc["subplots.groupspace"]
290
+ self._hpanels = [""] * nrows # axes and figure panel identification
291
+ self._wpanels = [""] * ncols
292
+ self._fpanels = { # array representation of figure panel spans
293
+ "left": np.empty((0, nrows), dtype=bool),
294
+ "right": np.empty((0, nrows), dtype=bool),
295
+ "bottom": np.empty((0, ncols), dtype=bool),
296
+ "top": np.empty((0, ncols), dtype=bool),
297
+ }
298
+ self._update_params(pad=pad, **kwargs)
299
+
300
+ def __getitem__(self, key):
301
+ """
302
+ Get a `~matplotlib.gridspec.SubplotSpec`. "Hidden" slots allocated for axes
303
+ panels, colorbars, and legends are ignored. For example, given a gridspec with
304
+ 2 subplot rows, 3 subplot columns, and a "panel" row between the subplot rows,
305
+ calling ``gs[1, 1]`` returns a `~matplotlib.gridspec.SubplotSpec` corresponding
306
+ to the central subplot on the second row rather than a "panel" slot.
307
+ """
308
+ return self._make_subplot_spec(key, includepanels=False)
309
+
310
+ def _make_subplot_spec(self, key, includepanels=False):
311
+ """
312
+ Generate a subplotspec either ignoring panels or including panels.
313
+ """
314
+
315
+ # Convert the indices into endpoint-inclusive (start, stop)
316
+ def _normalize_index(key, size, axis=None): # noqa: E306
317
+ if isinstance(key, slice):
318
+ start, stop, _ = key.indices(size)
319
+ if stop > start:
320
+ return start, stop - 1
321
+ else:
322
+ if key < 0:
323
+ key += size
324
+ if 0 <= key < size:
325
+ return key, key # endpoing inclusive
326
+ extra = "for gridspec" if axis is None else f"along axis {axis}"
327
+ raise IndexError(f"Invalid index {key} {extra} with size {size}.")
328
+
329
+ # Normalize the indices
330
+ if includepanels:
331
+ nrows, ncols = self.get_total_geometry()
332
+ else:
333
+ nrows, ncols = self.get_geometry()
334
+ if not isinstance(key, tuple): # usage gridspec[1,2]
335
+ num1, num2 = _normalize_index(key, nrows * ncols)
336
+ elif len(key) == 2:
337
+ k1, k2 = key
338
+ num1 = _normalize_index(k1, nrows, axis=0)
339
+ num2 = _normalize_index(k2, ncols, axis=1)
340
+ num1, num2 = np.ravel_multi_index((num1, num2), (nrows, ncols))
341
+ else:
342
+ raise ValueError(f"Invalid index {key!r}.")
343
+
344
+ # Return the subplotspec
345
+ if not includepanels:
346
+ num1, num2 = self._encode_indices(num1, num2)
347
+ return _SubplotSpec(self, num1, num2)
348
+
349
+ def _encode_indices(self, *args, which=None):
350
+ """
351
+ Convert indices from the "unhidden" gridspec geometry into indices for the
352
+ total geometry. If `which` is not passed these should be flattened indices.
353
+ """
354
+ nums = []
355
+ idxs = self._get_indices(which)
356
+ for arg in args:
357
+ try:
358
+ nums.append(idxs[arg])
359
+ except (IndexError, TypeError):
360
+ raise ValueError(f"Invalid gridspec index {arg}.")
361
+ return nums[0] if len(nums) == 1 else nums
362
+
363
+ def _decode_indices(self, *args, which=None):
364
+ """
365
+ Convert indices from the total geometry into the "unhidden" gridspec
366
+ geometry. If `which` is not passed these should be flattened indices.
367
+ """
368
+ nums = []
369
+ idxs = self._get_indices(which)
370
+ for arg in args:
371
+ try:
372
+ nums.append(idxs.index(arg))
373
+ except ValueError:
374
+ raise ValueError(f"Invalid gridspec index {arg}.")
375
+ return nums[0] if len(nums) == 1 else nums
376
+
377
+ def _filter_indices(self, key, panel=False):
378
+ """
379
+ Filter the vector attribute for "unhidden" or "hidden" slots.
380
+ """
381
+ # NOTE: Currently this is just used for unused internal properties,
382
+ # defined for consistency with the properties ending in "total".
383
+ # These may be made public in a future version.
384
+ which = key[0]
385
+ space = "space" in key or "pad" in key
386
+ idxs = self._get_indices(which=which, space=space, panel=panel)
387
+ vector = getattr(self, key + "_total")
388
+ return [vector[i] for i in idxs]
389
+
390
+ def _get_indices(self, which=None, space=False, panel=False):
391
+ """
392
+ Get the indices associated with "unhidden" or "hidden" slots.
393
+ """
394
+ if which:
395
+ panels = getattr(self, f"_{which}panels")
396
+ else:
397
+ panels = [h + w for h, w in itertools.product(self._hpanels, self._wpanels)]
398
+ if not space:
399
+ idxs = [i for i, p in enumerate(panels) if p]
400
+ else:
401
+ idxs = [
402
+ i
403
+ for i, (p1, p2) in enumerate(zip(panels[:-1], panels[1:]))
404
+ if p1 == p2 == "f"
405
+ or p1 in ("l", "t")
406
+ and p2 in ("l", "t", "")
407
+ or p1 in ("r", "b", "")
408
+ and p2 in ("r", "b")
409
+ ]
410
+ if not panel:
411
+ length = len(panels) - 1 if space else len(panels)
412
+ idxs = [i for i in range(length) if i not in idxs]
413
+ return idxs
414
+
415
+ def _modify_subplot_geometry(self, newrow=None, newcol=None):
416
+ """
417
+ Update the axes subplot specs by inserting rows and columns as specified.
418
+ """
419
+ fig = self.figure
420
+ ncols = self._ncols_total - int(newcol is not None) # previous columns
421
+ inserts = (newrow, newrow, newcol, newcol)
422
+ for ax in fig._iter_axes(hidden=True, children=True):
423
+ # Get old index
424
+ # NOTE: Endpoints are inclusive, not exclusive!
425
+ if not isinstance(ax, maxes.SubplotBase):
426
+ continue
427
+ gs = ax.get_subplotspec().get_gridspec()
428
+ ss = ax.get_subplotspec().get_topmost_subplotspec()
429
+ # Get a new subplotspec
430
+ coords = list(ss._get_rows_columns(ncols=ncols))
431
+ for i in range(4):
432
+ if inserts[i] is not None and coords[i] >= inserts[i]:
433
+ coords[i] += 1
434
+ row1, row2, col1, col2 = coords
435
+ key1 = slice(row1, row2 + 1)
436
+ key2 = slice(col1, col2 + 1)
437
+ ss_new = self._make_subplot_spec((key1, key2), includepanels=True)
438
+ # Apply new subplotspec
439
+ # NOTE: We should only have one possible level of GridSpecFromSubplotSpec
440
+ # nesting -- from making side colorbars with length less than 1.
441
+ if ss is ax.get_subplotspec():
442
+ ax.set_subplotspec(ss_new)
443
+ elif ss is getattr(gs, "_subplot_spec", None):
444
+ gs._subplot_spec = ss_new
445
+ else:
446
+ raise RuntimeError("Unexpected GridSpecFromSubplotSpec nesting.")
447
+ ax._reposition_subplot()
448
+
449
+ def _parse_panel_arg(self, side, arg):
450
+ """
451
+ Return the indices associated with a new figure panel on the specified side.
452
+ Try to find room in the current mosaic of figure panels.
453
+ """
454
+ # Add a subplot panel. Index depends on the side
455
+ # NOTE: This always "stacks" new panels on old panels
456
+ if isinstance(arg, maxes.SubplotBase) and isinstance(arg, paxes.Axes):
457
+ slot = side[0]
458
+ ss = arg.get_subplotspec().get_topmost_subplotspec()
459
+ offset = len(arg._panel_dict[side]) + 1
460
+ row1, row2, col1, col2 = ss._get_rows_columns()
461
+ if side in ("left", "right"):
462
+ iratio = col1 - offset if side == "left" else col2 + offset
463
+ start, stop = row1, row2
464
+ else:
465
+ iratio = row1 - offset if side == "top" else row2 + offset
466
+ start, stop = col1, col2
467
+
468
+ # Add a figure panel. Index depends on the side and the input 'span'
469
+ # NOTE: Here the 'span' indices start at '1' by analogy with add_subplot()
470
+ # integers and with main subplot numbers. Also *ignores panel slots*.
471
+ # NOTE: This only "stacks" panels if requested slots are filled. Slots are
472
+ # tracked with figure panel array (a boolean mask where each row corresponds
473
+ # to a panel, moving toward the outside, and True indicates a slot is filled).
474
+ elif (
475
+ arg is None
476
+ or isinstance(arg, Integral)
477
+ or np.iterable(arg)
478
+ and all(isinstance(_, Integral) for _ in arg)
479
+ ):
480
+ slot = "f"
481
+ array = self._fpanels[side]
482
+ nacross = (
483
+ self._ncols_total if side in ("left", "right") else self._nrows_total
484
+ ) # noqa: E501
485
+ npanels, nalong = array.shape
486
+ arg = np.atleast_1d(_not_none(arg, (1, nalong)))
487
+ if arg.size not in (1, 2):
488
+ raise ValueError(
489
+ f"Invalid span={arg!r}. Must be scalar or 2-tuple of coordinates."
490
+ ) # noqa: E501
491
+ if any(s < 1 or s > nalong for s in arg):
492
+ raise ValueError(
493
+ f"Invalid span={arg!r}. Coordinates must satisfy 1 <= c <= {nalong}."
494
+ ) # noqa: E501
495
+ start, stop = arg[0] - 1, arg[-1] # non-inclusive starting at zero
496
+ iratio = -1 if side in ("left", "top") else nacross # default values
497
+ for i in range(npanels): # possibly use existing panel slot
498
+ if not any(array[i, start:stop]):
499
+ array[i, start:stop] = True
500
+ if side in ("left", "top"): # descending moves us closer to 0
501
+ iratio = npanels - 1 - i # index in ratios array
502
+ else: # descending array moves us closer to nacross - 1
503
+ iratio = nacross - (npanels - i) # index in ratios array
504
+ break
505
+ if iratio == -1 or iratio == nacross: # no slots so we must add to array
506
+ iarray = np.zeros((1, nalong), dtype=bool)
507
+ iarray[0, start:stop] = True
508
+ array = np.concatenate((array, iarray), axis=0)
509
+ self._fpanels[side] = array # replace array
510
+ which = "h" if side in ("left", "right") else "w"
511
+ start, stop = self._encode_indices(start, stop - 1, which=which)
512
+
513
+ else:
514
+ raise ValueError(f"Invalid panel argument {arg!r}.")
515
+
516
+ # Return subplotspec indices
517
+ # NOTE: Convert using the lengthwise indices
518
+ return slot, iratio, slice(start, stop + 1)
519
+
520
+ def _insert_panel_slot(
521
+ self,
522
+ side,
523
+ arg,
524
+ *,
525
+ share=None,
526
+ width=None,
527
+ space=None,
528
+ pad=None,
529
+ filled=False,
530
+ ):
531
+ """
532
+ Insert a panel slot into the existing gridspec. The `side` is the panel side
533
+ and the `arg` is either an axes instance or the figure row-column span.
534
+ """
535
+ # Parse input args and get user-input properties, default properties
536
+ fig = self.figure
537
+ if fig is None:
538
+ raise RuntimeError("Figure must be assigned to gridspec.")
539
+ if side not in ("left", "right", "bottom", "top"):
540
+ raise ValueError(f"Invalid side {side}.")
541
+ slot, idx, span = self._parse_panel_arg(side, arg)
542
+ pad = units(pad, "em", "in")
543
+ space = units(space, "em", "in")
544
+ width = units(width, "in")
545
+ share = False if filled else share if share is not None else True
546
+ which = "w" if side in ("left", "right") else "h"
547
+ panels = getattr(self, f"_{which}panels")
548
+ pads = getattr(self, f"_{which}pad_total") # no copies!
549
+ ratios = getattr(self, f"_{which}ratios_total")
550
+ spaces = getattr(self, f"_{which}space_total")
551
+ spaces_default = getattr(self, f"_{which}space_total_default")
552
+ new_outer_slot = idx in (-1, len(panels))
553
+ new_inner_slot = not new_outer_slot and panels[idx] != slot
554
+
555
+ # Retrieve default spaces
556
+ # NOTE: Cannot use 'wspace' and 'hspace' for top and right colorbars because
557
+ # that adds an unnecessary tick space. So bypass _get_default_space totally.
558
+ pad_default = (
559
+ self._panelpad
560
+ if slot != "f"
561
+ or side in ("left", "top")
562
+ and panels[0] == "f"
563
+ or side in ("right", "bottom")
564
+ and panels[-1] == "f"
565
+ else self._innerpad
566
+ )
567
+ inner_space_default = (
568
+ _not_none(pad, pad_default)
569
+ if side in ("top", "right")
570
+ else self._get_default_space(
571
+ "hspace_total" if side == "bottom" else "wspace_total",
572
+ title=False, # no title between subplot and panel
573
+ share=3 if share else 0, # space for main subplot labels
574
+ pad=_not_none(pad, pad_default),
575
+ )
576
+ )
577
+ outer_space_default = self._get_default_space(
578
+ (
579
+ "bottom"
580
+ if not share and side == "top"
581
+ else "left" if not share and side == "right" else side
582
+ ),
583
+ title=True, # room for titles deflected above panels
584
+ pad=self._outerpad if new_outer_slot else self._innerpad,
585
+ )
586
+ if new_inner_slot:
587
+ outer_space_default += self._get_default_space(
588
+ "hspace_total" if side in ("bottom", "top") else "wspace_total",
589
+ share=None, # use external share setting
590
+ pad=0, # use no additional padding
591
+ )
592
+ width_default = units(
593
+ rc["colorbar.width" if filled else "subplots.panelwidth"], "in"
594
+ )
595
+
596
+ # Adjust space, ratio, and panel indicator arrays
597
+ # If slot exists, overwrite width, pad, space if they were provided by the user
598
+ # If slot does not exist, modify gemoetry and add insert new spaces
599
+ attr = "ncols" if side in ("left", "right") else "nrows"
600
+ idx_offset = int(side in ("top", "left"))
601
+ idx_inner_space = idx - int(side in ("bottom", "right")) # inner colorbar space
602
+ idx_outer_space = idx - int(side in ("top", "left")) # outer colorbar space
603
+ if new_outer_slot or new_inner_slot:
604
+ idx += idx_offset
605
+ idx_inner_space += idx_offset
606
+ idx_outer_space += idx_offset
607
+ newcol, newrow = (idx, None) if attr == "ncols" else (None, idx)
608
+ setattr(self, f"_{attr}_total", 1 + getattr(self, f"_{attr}_total"))
609
+ panels.insert(idx, slot)
610
+ ratios.insert(idx, _not_none(width, width_default))
611
+ pads.insert(idx_inner_space, _not_none(pad, pad_default))
612
+ spaces.insert(idx_inner_space, space)
613
+ spaces_default.insert(idx_inner_space, inner_space_default)
614
+ if new_inner_slot:
615
+ spaces_default.insert(idx_outer_space, outer_space_default)
616
+ else:
617
+ setattr(self, f"_{side}_default", outer_space_default)
618
+ else:
619
+ newrow = newcol = None
620
+ spaces_default[idx_inner_space] = inner_space_default
621
+ if width is not None:
622
+ ratios[idx] = width
623
+ if pad is not None:
624
+ pads[idx_inner_space] = pad
625
+ if space is not None:
626
+ spaces[idx_inner_space] = space
627
+
628
+ # Update the figure and axes and return a SubplotSpec
629
+ # NOTE: For figure panels indices are determined by user-input spans.
630
+ self._modify_subplot_geometry(newrow, newcol)
631
+ figsize = self._update_figsize()
632
+ if figsize is not None:
633
+ fig.set_size_inches(figsize, internal=True, forward=False)
634
+ else:
635
+ self.update()
636
+ key = (span, idx) if side in ("left", "right") else (idx, span)
637
+ ss = self._make_subplot_spec(key, includepanels=True) # bypass obfuscation
638
+ return ss, share
639
+
640
+ def _get_space(self, key):
641
+ """
642
+ Return the currently active vector inner space or scalar outer space
643
+ accounting for both default values and explicit user overrides.
644
+ """
645
+ # NOTE: Default panel spaces should have been filled by _insert_panel_slot.
646
+ # They use 'panelpad' and the panel-local 'share' setting. This function
647
+ # instead fills spaces between subplots depending on sharing setting.
648
+ fig = self.figure
649
+ if not fig:
650
+ raise ValueError("Figure must be assigned to get grid positions.")
651
+ attr = f"_{key}" # user-specified
652
+ attr_default = f"_{key}_default" # default values
653
+ value = getattr(self, attr)
654
+ value_default = getattr(self, attr_default)
655
+ if key in ("left", "right", "bottom", "top"):
656
+ if value_default is None:
657
+ value_default = self._get_default_space(key)
658
+ setattr(self, attr_default, value_default)
659
+ return _not_none(value, value_default)
660
+ elif key in ("wspace_total", "hspace_total"):
661
+ result = []
662
+ for i, (val, val_default) in enumerate(zip(value, value_default)):
663
+ if val_default is None:
664
+ val_default = self._get_default_space(key)
665
+ value_default[i] = val_default
666
+ result.append(_not_none(val, val_default))
667
+ return result
668
+ else:
669
+ raise ValueError(f"Unknown space parameter {key!r}.")
670
+
671
+ def _get_default_space(self, key, pad=None, share=None, title=True):
672
+ """
673
+ Return suitable default scalar inner or outer space given a shared axes
674
+ setting. This is only relevant when "tight layout" is disabled.
675
+ """
676
+ # NOTE: Internal spacing args are stored in inches to simplify the
677
+ # get_grid_positions() calculations.
678
+ fig = self.figure
679
+ if fig is None:
680
+ raise RuntimeError("Figure must be assigned.")
681
+ if key == "right":
682
+ pad = _not_none(pad, self._outerpad)
683
+ space = 0
684
+ elif key == "top":
685
+ pad = _not_none(pad, self._outerpad)
686
+ space = self._titlespace if title else 0
687
+ elif key == "left":
688
+ pad = _not_none(pad, self._outerpad)
689
+ space = self._labelspace + self._yticklabelspace + self._ytickspace
690
+ elif key == "bottom":
691
+ pad = _not_none(pad, self._outerpad)
692
+ space = self._labelspace + self._xticklabelspace + self._xtickspace
693
+ elif key == "wspace_total":
694
+ pad = _not_none(pad, self._innerpad)
695
+ share = _not_none(share, fig._sharey, 0)
696
+ space = self._ytickspace
697
+ if share < 3:
698
+ space += self._yticklabelspace
699
+ if share < 1:
700
+ space += self._labelspace
701
+ elif key == "hspace_total":
702
+ pad = _not_none(pad, self._innerpad)
703
+ share = _not_none(share, fig._sharex, 0)
704
+ space = self._xtickspace
705
+ if title:
706
+ space += self._titlespace
707
+ if share < 3:
708
+ space += self._xticklabelspace
709
+ if share < 1:
710
+ space += self._labelspace
711
+ else:
712
+ raise ValueError(f"Invalid space key {key!r}.")
713
+ return pad + space / 72
714
+
715
+ def _get_tight_space(self, w):
716
+ """
717
+ Get tight layout spaces between the input subplot rows or columns.
718
+ """
719
+ # Get constants
720
+ fig = self.figure
721
+ if not fig:
722
+ return
723
+ if w == "w":
724
+ x, y = "xy"
725
+ group = self._wgroup
726
+ nacross = self.nrows_total
727
+ space = self.wspace_total
728
+ pad = self.wpad_total
729
+ else:
730
+ x, y = "yx"
731
+ group = self._hgroup
732
+ nacross = self.ncols_total
733
+ space = self.hspace_total
734
+ pad = self.hpad_total
735
+
736
+ # Iterate along each row or column space
737
+ axs = tuple(fig._iter_axes(hidden=True, children=False))
738
+ space = list(space) # a copy
739
+ ralong = np.array([ax._range_subplotspec(x) for ax in axs])
740
+ racross = np.array([ax._range_subplotspec(y) for ax in axs])
741
+ for i, (s, p) in enumerate(zip(space, pad)):
742
+ # Find axes that abutt aginst this row or column space
743
+ groups = []
744
+ for j in range(nacross): # e.g. each row
745
+ # Get the indices for axes that meet this row or column edge.
746
+ # NOTE: Rigorously account for empty and overlapping slots here
747
+ filt = (racross[:, 0] <= j) & (j <= racross[:, 1])
748
+ if sum(filt) < 2:
749
+ continue # no interface
750
+ ii = i
751
+ idx1 = idx2 = np.array(())
752
+ while ii >= 0 and idx1.size == 0:
753
+ filt1 = ralong[:, 1] == ii # i.e. r / b edge abutts against this
754
+ (idx1,) = np.where(filt & filt1)
755
+ ii -= 1
756
+ ii = i + 1
757
+ while ii <= len(space) and idx2.size == 0:
758
+ filt2 = ralong[:, 0] == ii # i.e. l / t edge abutts against this
759
+ (idx2,) = np.where(filt & filt2)
760
+ ii += 1
761
+ # Put axes into unique groups and store as (l, r) or (b, t) pairs.
762
+ axs1, axs2 = [axs[_] for _ in idx1], [axs[_] for _ in idx2]
763
+ if x != "x": # order bottom-to-top
764
+ axs1, axs2 = axs2, axs1
765
+ for group1, group2 in groups:
766
+ if any(_ in group1 for _ in axs1) or any(_ in group2 for _ in axs2):
767
+ group1.update(axs1)
768
+ group2.update(axs2)
769
+ break
770
+ else:
771
+ if axs1 and axs2:
772
+ groups.append((set(axs1), set(axs2))) # form new group
773
+ # Determing the spaces using cached tight bounding boxes
774
+ # NOTE: Set gridspec space to zero if there are no adjacent edges
775
+ if not group:
776
+ groups = [
777
+ (
778
+ set(ax for (group1, _) in groups for ax in group1),
779
+ set(ax for (_, group2) in groups for ax in group2),
780
+ )
781
+ ]
782
+ margins = []
783
+ for group1, group2 in groups:
784
+ x1 = max(ax._range_tightbbox(x)[1] for ax in group1)
785
+ x2 = min(ax._range_tightbbox(x)[0] for ax in group2)
786
+ margins.append((x2 - x1) / self.figure.dpi)
787
+ s = 0 if not margins else max(0, s - min(margins) + p)
788
+ space[i] = s
789
+
790
+ return space
791
+
792
+ def _auto_layout_aspect(self):
793
+ """
794
+ Update the underlying default aspect ratio.
795
+ """
796
+ # Get the axes
797
+ fig = self.figure
798
+ if not fig:
799
+ return
800
+ ax = fig._subplot_dict.get(fig._refnum, None)
801
+ if ax is None:
802
+ return
803
+
804
+ # Get aspect ratio
805
+ ratio = ax.get_aspect() # the aspect ratio in *data units*
806
+ if ratio == "auto":
807
+ return
808
+ elif ratio == "equal":
809
+ ratio = 1
810
+ elif isinstance(ratio, str):
811
+ raise RuntimeError(f"Unknown aspect ratio mode {ratio!r}.")
812
+ else:
813
+ ratio = 1 / ratio
814
+
815
+ # Compare to current aspect after scaling by data ratio
816
+ # Noat matplotlib 3.2.0 expanded get_data_ratio to work for all axis scales:
817
+ # https://github.com/matplotlib/matplotlib/commit/87c742b99dc6b9a190f8c89bc6256ced72f5ab80 # noqa: E501
818
+ aspect = ratio / ax.get_data_ratio()
819
+ if fig._refaspect is not None:
820
+ return # fixed by user
821
+ if np.isclose(aspect, fig._refaspect_default):
822
+ return # close enough to the default aspect
823
+ fig._refaspect_default = aspect
824
+
825
+ # Update the layout
826
+ figsize = self._update_figsize()
827
+ if not fig._is_same_size(figsize):
828
+ fig.set_size_inches(figsize, internal=True)
829
+
830
+ def _auto_layout_tight(self, renderer):
831
+ """
832
+ Update the underlying spaces with tight layout values. If `resize` is
833
+ ``True`` and the auto figure size has changed then update the figure
834
+ size. Either way always update the subplot positions.
835
+ """
836
+ # Initial stuff
837
+ fig = self.figure
838
+ if not fig:
839
+ return
840
+ if not any(fig._iter_axes(hidden=True, children=False)):
841
+ return # skip tight layout if there are no subplots in the figure
842
+
843
+ # Get the tight bounding box around the whole figure.
844
+ # NOTE: This triggers ultraplot.axes.Axes.get_tightbbox which *caches* the
845
+ # computed bounding boxes used by _range_tightbbox below.
846
+ pad = self._outerpad
847
+ obox = fig.bbox_inches # original bbox
848
+ bbox = fig.get_tightbbox(renderer)
849
+
850
+ # Calculate new figure margins
851
+ # NOTE: Negative spaces are common where entire rows/columns of gridspec
852
+ # are empty but it seems to result in wrong figure size + grid positions. Not
853
+ # worth correcting so instead enforce positive margin sizes. Will leave big
854
+ # empty slot but that is probably what should happen under this scenario.
855
+ left = self.left
856
+ bottom = self.bottom
857
+ right = self.right
858
+ top = self.top
859
+ self._left_default = max(0, left - (bbox.xmin - 0) + pad)
860
+ self._bottom_default = max(0, bottom - (bbox.ymin - 0) + pad)
861
+ self._right_default = max(0, right - (obox.xmax - bbox.xmax) + pad)
862
+ self._top_default = max(0, top - (obox.ymax - bbox.ymax) + pad)
863
+
864
+ # Calculate new subplot row and column spaces. Enforce equal
865
+ # default spaces between main subplot edges if requested.
866
+ hspace = self._get_tight_space("h")
867
+ wspace = self._get_tight_space("w")
868
+ if self._hequal:
869
+ idxs = self._get_indices("h", space=True)
870
+ space = max(hspace[i] for i in idxs)
871
+ for i in idxs:
872
+ hspace[i] = space
873
+ if self._wequal:
874
+ idxs = self._get_indices("w", space=True)
875
+ space = max(wspace[i] for i in idxs)
876
+ for i in idxs:
877
+ wspace[i] = space
878
+ self._hspace_total_default = hspace
879
+ self._wspace_total_default = wspace
880
+
881
+ # Update the layout
882
+ # NOTE: fig.set_size_inches() always updates the gridspec to enforce fixed
883
+ # spaces (necessary since native position coordinates are figure-relative)
884
+ # and to enforce fixed panel ratios. So only self.update() if we skip resize.
885
+ figsize = self._update_figsize()
886
+ if not fig._is_same_size(figsize):
887
+ fig.set_size_inches(figsize, internal=True)
888
+ else:
889
+ self.update()
890
+
891
+ def _update_figsize(self):
892
+ """
893
+ Return an updated auto layout figure size accounting for the
894
+ gridspec and figure parameters. May or may not need to be applied.
895
+ """
896
+ fig = self.figure
897
+ if fig is None: # drawing before subplots are added?
898
+ return
899
+ ax = fig._subplot_dict.get(fig._refnum, None)
900
+ if ax is None: # drawing before subplots are added?
901
+ return
902
+ ss = ax.get_subplotspec().get_topmost_subplotspec()
903
+ y1, y2, x1, x2 = ss._get_rows_columns()
904
+ refhspace = sum(self.hspace_total[y1:y2])
905
+ refwspace = sum(self.wspace_total[x1:x2])
906
+ refhpanel = sum(
907
+ self.hratios_total[i] for i in range(y1, y2 + 1) if self._hpanels[i]
908
+ ) # noqa: E501
909
+ refwpanel = sum(
910
+ self.wratios_total[i] for i in range(x1, x2 + 1) if self._wpanels[i]
911
+ ) # noqa: E501
912
+ refhsubplot = sum(
913
+ self.hratios_total[i] for i in range(y1, y2 + 1) if not self._hpanels[i]
914
+ ) # noqa: E501
915
+ refwsubplot = sum(
916
+ self.wratios_total[i] for i in range(x1, x2 + 1) if not self._wpanels[i]
917
+ ) # noqa: E501
918
+
919
+ # Get the reference sizes
920
+ # NOTE: The sizing arguments should have been normalized already
921
+ figwidth, figheight = fig._figwidth, fig._figheight
922
+ refwidth, refheight = fig._refwidth, fig._refheight
923
+ refaspect = _not_none(fig._refaspect, fig._refaspect_default)
924
+ if refheight is None and figheight is None:
925
+ if figwidth is not None:
926
+ gridwidth = figwidth - self.spacewidth - self.panelwidth
927
+ refwidth = gridwidth * refwsubplot / self.gridwidth
928
+ if refwidth is not None: # WARNING: do not change to elif!
929
+ refheight = refwidth / refaspect
930
+ else:
931
+ raise RuntimeError("Figure size arguments are all missing.")
932
+ if refwidth is None and figwidth is None:
933
+ if figheight is not None:
934
+ gridheight = figheight - self.spaceheight - self.panelheight
935
+ refheight = gridheight * refhsubplot / self.gridheight
936
+ if refheight is not None:
937
+ refwidth = refheight * refaspect
938
+ else:
939
+ raise RuntimeError("Figure size arguments are all missing.")
940
+
941
+ # Get the auto figure size. Might trigger 'not enough room' error later
942
+ # NOTE: For e.g. [[1, 1, 2, 2], [0, 3, 3, 0]] we make sure to still scale the
943
+ # reference axes like a square even though takes two columns of gridspec.
944
+ if refheight is not None:
945
+ refheight -= refhspace + refhpanel
946
+ gridheight = refheight * self.gridheight / refhsubplot
947
+ figheight = gridheight + self.spaceheight + self.panelheight
948
+ if refwidth is not None:
949
+ refwidth -= refwspace + refwpanel
950
+ gridwidth = refwidth * self.gridwidth / refwsubplot
951
+ figwidth = gridwidth + self.spacewidth + self.panelwidth
952
+
953
+ # Return the figure size
954
+ figsize = (figwidth, figheight)
955
+ if all(np.isfinite(figsize)):
956
+ return figsize
957
+ else:
958
+ warnings._warn_ultraplot(f"Auto resize failed. Invalid figsize {figsize}.")
959
+
960
+ def _update_params(
961
+ self,
962
+ *,
963
+ left=None,
964
+ bottom=None,
965
+ right=None,
966
+ top=None,
967
+ wspace=None,
968
+ hspace=None,
969
+ space=None,
970
+ wpad=None,
971
+ hpad=None,
972
+ pad=None,
973
+ wequal=None,
974
+ hequal=None,
975
+ equal=None,
976
+ wgroup=None,
977
+ hgroup=None,
978
+ group=None,
979
+ outerpad=None,
980
+ innerpad=None,
981
+ panelpad=None,
982
+ hratios=None,
983
+ wratios=None,
984
+ width_ratios=None,
985
+ height_ratios=None,
986
+ ):
987
+ """
988
+ Update the user-specified properties.
989
+ """
990
+
991
+ # Assign scalar args
992
+ # WARNING: The key signature here is critical! Used in ui.py to
993
+ # separate out figure keywords and gridspec keywords.
994
+ def _assign_scalar(key, value, convert=True):
995
+ if value is None:
996
+ return
997
+ if not np.isscalar(value):
998
+ raise ValueError(f"Unexpected {key}={value!r}. Must be scalar.")
999
+ if convert:
1000
+ value = units(value, "em", "in")
1001
+ setattr(self, f"_{key}", value)
1002
+
1003
+ hequal = _not_none(hequal, equal)
1004
+ wequal = _not_none(wequal, equal)
1005
+ hgroup = _not_none(hgroup, group)
1006
+ wgroup = _not_none(wgroup, group)
1007
+ _assign_scalar("left", left)
1008
+ _assign_scalar("right", right)
1009
+ _assign_scalar("bottom", bottom)
1010
+ _assign_scalar("top", top)
1011
+ _assign_scalar("panelpad", panelpad)
1012
+ _assign_scalar("outerpad", outerpad)
1013
+ _assign_scalar("innerpad", innerpad)
1014
+ _assign_scalar("hequal", hequal, convert=False)
1015
+ _assign_scalar("wequal", wequal, convert=False)
1016
+ _assign_scalar("hgroup", hgroup, convert=False)
1017
+ _assign_scalar("wgroup", wgroup, convert=False)
1018
+
1019
+ # Assign vector args
1020
+ # NOTE: Here we employ obfuscation that skips 'panel' indices. So users could
1021
+ # still call self.update(wspace=[1, 2]) even if there is a right-axes panel
1022
+ # between each subplot. To control panel spaces users should instead pass
1023
+ # 'pad' or 'space' to panel_axes(), colorbar(), or legend() on creation.
1024
+ def _assign_vector(key, values, space):
1025
+ if values is None:
1026
+ return
1027
+ idxs = self._get_indices(key[0], space=space)
1028
+ nidxs = len(idxs)
1029
+ values = np.atleast_1d(values)
1030
+ if values.size == 1:
1031
+ values = np.repeat(values, nidxs)
1032
+ if values.size != nidxs:
1033
+ raise ValueError(f"Expected len({key}) == {nidxs}. Got {values.size}.")
1034
+ list_ = getattr(self, f"_{key}_total")
1035
+ for i, value in enumerate(values):
1036
+ if value is None:
1037
+ continue
1038
+ list_[idxs[i]] = value
1039
+
1040
+ if pad is not None and not np.isscalar(pad):
1041
+ raise ValueError(f"Parameter pad={pad!r} must be scalar.")
1042
+ if space is not None and not np.isscalar(space):
1043
+ raise ValueError(f"Parameter space={space!r} must be scalar.")
1044
+ hpad = _not_none(hpad, pad)
1045
+ wpad = _not_none(wpad, pad)
1046
+ hpad = units(hpad, "em", "in")
1047
+ wpad = units(wpad, "em", "in")
1048
+ hspace = _not_none(hspace, space)
1049
+ wspace = _not_none(wspace, space)
1050
+ hspace = units(hspace, "em", "in")
1051
+ wspace = units(wspace, "em", "in")
1052
+ hratios = _not_none(hratios=hratios, height_ratios=height_ratios)
1053
+ wratios = _not_none(wratios=wratios, width_ratios=width_ratios)
1054
+ _assign_vector("hpad", hpad, space=True)
1055
+ _assign_vector("wpad", wpad, space=True)
1056
+ _assign_vector("hspace", hspace, space=True)
1057
+ _assign_vector("wspace", wspace, space=True)
1058
+ _assign_vector("hratios", hratios, space=False)
1059
+ _assign_vector("wratios", wratios, space=False)
1060
+
1061
+ @docstring._snippet_manager
1062
+ def copy(self, **kwargs):
1063
+ """
1064
+ Return a copy of the `GridSpec` with the `~ultraplot.figure.Figure`-specific
1065
+ "panel slots" removed. This can be useful if you want to draw multiple
1066
+ figures with the same geometry. Properties are inherited from this
1067
+ `GridSpec` by default but can be changed by passing keyword arguments.
1068
+
1069
+ Parameters
1070
+ ----------
1071
+ %(gridspec.shared)s
1072
+ %(gridspec.vector)s
1073
+ %(gridspec.tight)s
1074
+
1075
+ See also
1076
+ --------
1077
+ GridSpec.update
1078
+ """
1079
+ # WARNING: For some reason copy.copy() fails. Updating e.g. wpanels
1080
+ # and hpanels on the copy also updates this object. No idea why.
1081
+ nrows, ncols = self.get_geometry()
1082
+ gs = GridSpec(nrows, ncols)
1083
+ hidxs = self._get_indices("h")
1084
+ widxs = self._get_indices("w")
1085
+ gs._hratios_total = [self._hratios_total[i] for i in hidxs]
1086
+ gs._wratios_total = [self._wratios_total[i] for i in widxs]
1087
+ hidxs = self._get_indices("h", space=True)
1088
+ widxs = self._get_indices("w", space=True)
1089
+ gs._hpad_total = [self._hpad_total[i] for i in hidxs]
1090
+ gs._wpad_total = [self._wpad_total[i] for i in widxs]
1091
+ gs._hspace_total = [self._hspace_total[i] for i in hidxs]
1092
+ gs._wspace_total = [self._wspace_total[i] for i in widxs]
1093
+ gs._hspace_total_default = [self._hspace_total_default[i] for i in hidxs]
1094
+ gs._wspace_total_default = [self._wspace_total_default[i] for i in widxs]
1095
+ for key in (
1096
+ "left",
1097
+ "right",
1098
+ "bottom",
1099
+ "top",
1100
+ "labelspace",
1101
+ "titlespace",
1102
+ "xtickspace",
1103
+ "ytickspace",
1104
+ "xticklabelspace",
1105
+ "yticklabelspace",
1106
+ "outerpad",
1107
+ "innerpad",
1108
+ "panelpad",
1109
+ "hequal",
1110
+ "wequal",
1111
+ ):
1112
+ value = getattr(self, "_" + key)
1113
+ setattr(gs, "_" + key, value)
1114
+ gs.update(**kwargs)
1115
+ return gs
1116
+
1117
+ def get_geometry(self):
1118
+ """
1119
+ Return the number of "unhidden" non-panel rows and columns in the grid
1120
+ (see `GridSpec` for details).
1121
+
1122
+ See also
1123
+ --------
1124
+ GridSpec.get_panel_geometry
1125
+ GridSpec.get_total_geometry
1126
+ """
1127
+ nrows, ncols = self.get_total_geometry()
1128
+ nrows_panels, ncols_panels = self.get_panel_geometry()
1129
+ return nrows - nrows_panels, ncols - ncols_panels
1130
+
1131
+ def get_panel_geometry(self):
1132
+ """
1133
+ Return the number of "hidden" panel rows and columns in the grid
1134
+ (see `GridSpec` for details).
1135
+
1136
+ See also
1137
+ --------
1138
+ GridSpec.get_geometry
1139
+ GridSpec.get_total_geometry
1140
+ """
1141
+ nrows = sum(map(bool, self._hpanels))
1142
+ ncols = sum(map(bool, self._wpanels))
1143
+ return nrows, ncols
1144
+
1145
+ def get_total_geometry(self):
1146
+ """
1147
+ Return the total number of "unhidden" and "hidden" rows and columns
1148
+ in the grid (see `GridSpec` for details).
1149
+
1150
+ See also
1151
+ --------
1152
+ GridSpec.get_geometry
1153
+ GridSpec.get_panel_geometry
1154
+ GridSpec.get_grid_positions
1155
+ """
1156
+ return self._nrows_total, self._ncols_total
1157
+
1158
+ def get_grid_positions(self, figure=None):
1159
+ """
1160
+ Return the subplot grid positions allowing for variable inter-subplot
1161
+ spacing and using physical units for the spacing terms. The resulting
1162
+ positions include "hidden" panel rows and columns.
1163
+
1164
+ Note
1165
+ ----
1166
+ The physical units for positioning grid cells are converted from em-widths to
1167
+ inches when the `GridSpec` is instantiated. This means that subsequent changes
1168
+ to :rcraw:`font.size` will have no effect on the spaces. This is consistent
1169
+ with :rcraw:`font.size` having no effect on already-instantiated figures.
1170
+
1171
+ See also
1172
+ --------
1173
+ GridSpec.get_total_geometry
1174
+ """
1175
+ # Grab the figure size
1176
+ if not self.figure:
1177
+ self._figure = figure
1178
+ if not self.figure:
1179
+ raise RuntimeError("Figure must be assigned to gridspec.")
1180
+ if figure is not self.figure:
1181
+ raise RuntimeError(
1182
+ f"Input figure {figure} does not match gridspec figure {self.figure}."
1183
+ ) # noqa: E501
1184
+ fig = _not_none(figure, self.figure)
1185
+ figwidth, figheight = fig.get_size_inches()
1186
+ spacewidth, spaceheight = self.spacewidth, self.spaceheight
1187
+ panelwidth, panelheight = self.panelwidth, self.panelheight
1188
+ hratios, wratios = self.hratios_total, self.wratios_total
1189
+ hidxs, widxs = self._get_indices("h"), self._get_indices("w")
1190
+
1191
+ # Scale the subplot slot ratios and keep the panel slots fixed
1192
+ hsubplot = np.array([hratios[i] for i in hidxs])
1193
+ wsubplot = np.array([wratios[i] for i in widxs])
1194
+ hsubplot = (figheight - panelheight - spaceheight) * hsubplot / np.sum(hsubplot)
1195
+ wsubplot = (figwidth - panelwidth - spacewidth) * wsubplot / np.sum(wsubplot)
1196
+ for idx, ratio in zip(hidxs, hsubplot):
1197
+ hratios[idx] = ratio # modify the main subplot ratios
1198
+ for idx, ratio in zip(widxs, wsubplot):
1199
+ wratios[idx] = ratio
1200
+
1201
+ # Calculate accumulated heights of columns
1202
+ norm = (figheight - spaceheight) / (figheight * sum(hratios))
1203
+ if norm < 0:
1204
+ raise RuntimeError(
1205
+ "Not enough room for axes. Try increasing the figure height or "
1206
+ "decreasing the 'top', 'bottom', or 'hspace' gridspec spaces."
1207
+ )
1208
+ cell_heights = [r * norm for r in hratios]
1209
+ sep_heights = [0] + [s / figheight for s in self.hspace_total]
1210
+ heights = np.cumsum(np.column_stack([sep_heights, cell_heights]).flat)
1211
+
1212
+ # Calculate accumulated widths of rows
1213
+ norm = (figwidth - spacewidth) / (figwidth * sum(wratios))
1214
+ if norm < 0:
1215
+ raise RuntimeError(
1216
+ "Not enough room for axes. Try increasing the figure width or "
1217
+ "decreasing the 'left', 'right', or 'wspace' gridspec spaces."
1218
+ )
1219
+ cell_widths = [r * norm for r in wratios]
1220
+ sep_widths = [0] + [s / figwidth for s in self.wspace_total]
1221
+ widths = np.cumsum(np.column_stack([sep_widths, cell_widths]).flat)
1222
+
1223
+ # Return the figure coordinates
1224
+ tops, bottoms = (1 - self.top / figheight - heights).reshape((-1, 2)).T
1225
+ lefts, rights = (self.left / figwidth + widths).reshape((-1, 2)).T
1226
+ return bottoms, tops, lefts, rights
1227
+
1228
+ @docstring._snippet_manager
1229
+ def update(self, **kwargs):
1230
+ """
1231
+ Update the gridspec with arbitrary initialization keyword arguments
1232
+ and update the subplot positions.
1233
+
1234
+ Parameters
1235
+ ----------
1236
+ %(gridspec.shared)s
1237
+ %(gridspec.vector)s
1238
+ %(gridspec.tight)s
1239
+
1240
+ See also
1241
+ --------
1242
+ GridSpec.copy
1243
+ """
1244
+ # Apply positions to all axes
1245
+ # NOTE: This uses the current figure size to fix panel widths
1246
+ # and determine physical grid spacing.
1247
+ self._update_params(**kwargs)
1248
+ fig = self.figure
1249
+ if fig is None:
1250
+ return
1251
+ for ax in fig.axes:
1252
+ if not isinstance(ax, maxes.SubplotBase):
1253
+ continue
1254
+ ss = ax.get_subplotspec().get_topmost_subplotspec()
1255
+ if ss.get_gridspec() is not self: # should be impossible
1256
+ continue
1257
+ ax._reposition_subplot()
1258
+ fig.stale = True
1259
+
1260
+ @property
1261
+ def figure(self):
1262
+ """
1263
+ The `ultraplot.figure.Figure` uniquely associated with this `GridSpec`.
1264
+ On assignment the gridspec parameters and figure size are updated.
1265
+
1266
+ See also
1267
+ --------
1268
+ ultraplot.gridspec.SubplotGrid.figure
1269
+ ultraplot.figure.Figure.gridspec
1270
+ """
1271
+ return self._figure
1272
+
1273
+ @figure.setter
1274
+ def figure(self, fig):
1275
+ from .figure import Figure
1276
+
1277
+ if not isinstance(fig, Figure):
1278
+ raise ValueError("Figure must be a ultraplot figure.")
1279
+ if self._figure and self._figure is not fig:
1280
+ raise ValueError(
1281
+ "Cannot use the same gridspec for multiple figures. "
1282
+ "Please use gridspec.copy() to make a copy."
1283
+ )
1284
+ self._figure = fig
1285
+ self._update_params(**fig._gridspec_params)
1286
+ fig._gridspec_params.clear()
1287
+ figsize = self._update_figsize()
1288
+ if figsize is not None:
1289
+ fig.set_size_inches(figsize, internal=True, forward=False)
1290
+ else:
1291
+ self.update()
1292
+
1293
+ # Delete attributes. Don't like having special setters and getters for some
1294
+ # settings and not others. Width and height ratios can be updated with update().
1295
+ # Also delete obsolete 'subplotpars' and built-in tight layout function.
1296
+ tight_layout = _disable_method("tight_layout") # instead use custom tight layout
1297
+ subgridspec = _disable_method("subgridspec") # instead use variable spaces
1298
+ get_width_ratios = _disable_method("get_width_ratios")
1299
+ get_height_ratios = _disable_method("get_height_ratios")
1300
+ set_width_ratios = _disable_method("set_width_ratios")
1301
+ set_height_ratios = _disable_method("set_height_ratios")
1302
+ get_subplot_params = _disable_method("get_subplot_params")
1303
+ locally_modified_subplot_params = _disable_method("locally_modified_subplot_params")
1304
+
1305
+ # Immutable helper properties used to calculate figure size and subplot positions
1306
+ # NOTE: The spaces are auto-filled with defaults wherever user left them unset
1307
+ gridheight = property(lambda self: sum(self.hratios))
1308
+ gridwidth = property(lambda self: sum(self.wratios))
1309
+ panelheight = property(lambda self: sum(self.hratios_panel))
1310
+ panelwidth = property(lambda self: sum(self.wratios_panel))
1311
+ spaceheight = property(lambda self: self.bottom + self.top + sum(self.hspace_total))
1312
+ spacewidth = property(lambda self: self.left + self.right + sum(self.wspace_total))
1313
+
1314
+ # Geometry properties. These are included for consistency with get_geometry
1315
+ # functions (would be really confusing if self.nrows, self.ncols disagree).
1316
+ nrows = property(
1317
+ lambda self: self._nrows_total - sum(map(bool, self._hpanels)), doc=""
1318
+ ) # noqa: E501
1319
+ ncols = property(
1320
+ lambda self: self._ncols_total - sum(map(bool, self._wpanels)), doc=""
1321
+ ) # noqa: E501
1322
+ nrows_panel = property(lambda self: sum(map(bool, self._hpanels)))
1323
+ ncols_panel = property(lambda self: sum(map(bool, self._wpanels)))
1324
+ nrows_total = property(lambda self: self._nrows_total)
1325
+ ncols_total = property(lambda self: self._ncols_total)
1326
+
1327
+ # Make formerly public instance-level attributes immutable and redirect space
1328
+ # properties so they try to retrieve user settings then fallback to defaults.
1329
+ # NOTE: These are undocumented for the time being. Generally properties should
1330
+ # be changed with update() and introspection not really necessary.
1331
+ left = property(lambda self: self._get_space("left"))
1332
+ bottom = property(lambda self: self._get_space("bottom"))
1333
+ right = property(lambda self: self._get_space("right"))
1334
+ top = property(lambda self: self._get_space("top"))
1335
+ hratios = property(lambda self: self._filter_indices("hratios", panel=False))
1336
+ wratios = property(lambda self: self._filter_indices("wratios", panel=False))
1337
+ hratios_panel = property(lambda self: self._filter_indices("hratios", panel=True))
1338
+ wratios_panel = property(lambda self: self._filter_indices("wratios", panel=True))
1339
+ hratios_total = property(lambda self: list(self._hratios_total))
1340
+ wratios_total = property(lambda self: list(self._wratios_total))
1341
+ hspace = property(lambda self: self._filter_indices("hspace", panel=False))
1342
+ wspace = property(lambda self: self._filter_indices("wspace", panel=False))
1343
+ hspace_panel = property(lambda self: self._filter_indices("hspace", panel=True))
1344
+ wspace_panel = property(lambda self: self._filter_indices("wspace", panel=True))
1345
+ hspace_total = property(lambda self: self._get_space("hspace_total"))
1346
+ wspace_total = property(lambda self: self._get_space("wspace_total"))
1347
+ hpad = property(lambda self: self._filter_indices("hpad", panel=False))
1348
+ wpad = property(lambda self: self._filter_indices("wpad", panel=False))
1349
+ hpad_panel = property(lambda self: self._filter_indices("hpad", panel=True))
1350
+ wpad_panel = property(lambda self: self._filter_indices("wpad", panel=True))
1351
+ hpad_total = property(lambda self: list(self._hpad_total))
1352
+ wpad_total = property(lambda self: list(self._wpad_total))
1353
+
1354
+
1355
+ class SubplotGrid(MutableSequence, list):
1356
+ """
1357
+ List-like, array-like object used to store subplots returned by
1358
+ `~ultraplot.figure.Figure.subplots`. 1D indexing uses the underlying list of
1359
+ `~ultraplot.axes.Axes` while 2D indexing uses the `~SubplotGrid.gridspec`.
1360
+ See `~SubplotGrid.__getitem__` for details.
1361
+ """
1362
+
1363
+ def __repr__(self):
1364
+ if not self:
1365
+ return "SubplotGrid(length=0)"
1366
+ length = len(self)
1367
+ nrows, ncols = self.gridspec.get_geometry()
1368
+ return f"SubplotGrid(nrows={nrows}, ncols={ncols}, length={length})"
1369
+
1370
+ def __str__(self):
1371
+ return self.__repr__()
1372
+
1373
+ def __len__(self):
1374
+ return list.__len__(self)
1375
+
1376
+ def insert(self, key, value): # required for MutableSequence
1377
+ value = self._validate_item(value, scalar=True)
1378
+ list.insert(self, key, value)
1379
+
1380
+ def __init__(self, sequence=None, **kwargs):
1381
+ """
1382
+ Parameters
1383
+ ----------
1384
+ sequence : sequence
1385
+ A sequence of `ultraplot.axes.Axes` subplots or their children.
1386
+
1387
+ See also
1388
+ --------
1389
+ ultraplot.ui.subplots
1390
+ ultraplot.figure.Figure.subplots
1391
+ ultraplot.figure.Figure.add_subplots
1392
+ """
1393
+ n = kwargs.pop("n", None)
1394
+ order = kwargs.pop("order", None)
1395
+ if n is not None or order is not None:
1396
+ warnings._warn_ultraplot(
1397
+ f"Ignoring n={n!r} and order={order!r}. As of v0.8 SubplotGrid "
1398
+ "handles 2D indexing by leveraging the subplotspec extents rather than "
1399
+ "directly emulating 2D array indexing. These arguments are no longer "
1400
+ "needed and will be removed in a future release."
1401
+ )
1402
+ sequence = _not_none(sequence, [])
1403
+ sequence = self._validate_item(sequence, scalar=False)
1404
+ super().__init__(sequence, **kwargs)
1405
+
1406
+ def __getattr__(self, attr):
1407
+ """
1408
+ Get a missing attribute. Simply redirects to the axes if the `SubplotGrid`
1409
+ is singleton and raises an error otherwise. This can be convenient for
1410
+ single-axes figures generated with `~ultraplot.figure.Figure.subplots`.
1411
+ """
1412
+ # Redirect to the axes
1413
+ if not self or attr[:1] == "_":
1414
+ return super().__getattribute__(attr) # trigger default error
1415
+ if len(self) == 1:
1416
+ return getattr(self[0], attr)
1417
+
1418
+ # Obscure deprecated behavior
1419
+ # WARNING: This is now deprecated! Instead we dynamically define a few
1420
+ # dedicated relevant commands that can be called from the grid (see below).
1421
+ import functools
1422
+
1423
+ warnings._warn_ultraplot(
1424
+ "Calling arbitrary axes methods from SubplotGrid was deprecated in v0.8 "
1425
+ "and will be removed in a future release. Please index the grid or loop "
1426
+ "over the grid instead."
1427
+ )
1428
+ if not self:
1429
+ return None
1430
+ objs = tuple(getattr(ax, attr) for ax in self) # may raise error
1431
+ if not any(map(callable, objs)):
1432
+ return objs[0] if len(self) == 1 else objs
1433
+ elif all(map(callable, objs)):
1434
+
1435
+ @functools.wraps(objs[0])
1436
+ def _iterate_subplots(*args, **kwargs):
1437
+ result = []
1438
+ for func in objs:
1439
+ result.append(func(*args, **kwargs))
1440
+ if len(self) == 1:
1441
+ return result[0]
1442
+ elif all(res is None for res in result):
1443
+ return None
1444
+ elif all(isinstance(res, paxes.Axes) for res in result):
1445
+ return SubplotGrid(result, n=self._n, order=self._order)
1446
+ else:
1447
+ return tuple(result)
1448
+
1449
+ _iterate_subplots.__doc__ = inspect.getdoc(objs[0])
1450
+ return _iterate_subplots
1451
+ else:
1452
+ raise AttributeError(f"Found mixed types for attribute {attr!r}.")
1453
+
1454
+ def __getitem__(self, key):
1455
+ """
1456
+ Get an axes.
1457
+
1458
+ Parameters
1459
+ ----------
1460
+ key : int, slice, or 2-tuple
1461
+ The index. If 1D then the axes in the corresponding
1462
+ sublist are returned. If 2D then the axes that intersect
1463
+ the corresponding `~SubplotGrid.gridspec` slots are returned.
1464
+
1465
+ Returns
1466
+ -------
1467
+ axs : ultraplot.axes.Axes or SubplotGrid
1468
+ The axes. If the index included slices then
1469
+ another `SubplotGrid` is returned.
1470
+
1471
+ Example
1472
+ -------
1473
+ >>> import ultraplot as pplt
1474
+ >>> fig, axs = pplt.subplots(nrows=3, ncols=3)
1475
+ >>> axs[5] # the subplot in the second row, third column
1476
+ >>> axs[1, 2] # the subplot in the second row, third column
1477
+ >>> axs[:, 0] # a SubplotGrid containing the subplots in the first column
1478
+ """
1479
+ if isinstance(key, tuple) and len(key) == 1:
1480
+ key = key[0]
1481
+ # List-style indexing
1482
+ if isinstance(key, (Integral, slice)):
1483
+ slices = isinstance(key, slice)
1484
+ objs = list.__getitem__(self, key)
1485
+ # Gridspec-style indexing
1486
+ elif (
1487
+ isinstance(key, tuple)
1488
+ and len(key) == 2
1489
+ and all(isinstance(ikey, (Integral, slice)) for ikey in key)
1490
+ ):
1491
+ # WARNING: Permit no-op slicing of empty grids here
1492
+ slices = any(isinstance(ikey, slice) for ikey in key)
1493
+ objs = []
1494
+ if self:
1495
+ gs = self.gridspec
1496
+ ss_key = gs._make_subplot_spec(key) # obfuscates panels
1497
+ row1_key, col1_key = divmod(ss_key.num1, gs.ncols)
1498
+ row2_key, col2_key = divmod(ss_key.num2, gs.ncols)
1499
+ for ax in self:
1500
+ ss = ax._get_topmost_axes().get_subplotspec().get_topmost_subplotspec()
1501
+ row1, col1 = divmod(ss.num1, gs.ncols)
1502
+ row2, col2 = divmod(ss.num2, gs.ncols)
1503
+ inrow = row1_key <= row1 <= row2_key or row1_key <= row2 <= row2_key
1504
+ incol = col1_key <= col1 <= col2_key or col1_key <= col2 <= col2_key
1505
+ if inrow and incol:
1506
+ objs.append(ax)
1507
+ if not slices and len(objs) == 1: # accounts for overlapping subplots
1508
+ objs = objs[0]
1509
+ else:
1510
+ raise IndexError(f"Invalid index {key!r}.")
1511
+ if isinstance(objs, list):
1512
+ return SubplotGrid(objs)
1513
+ else:
1514
+ return objs
1515
+
1516
+ def __setitem__(self, key, value):
1517
+ """
1518
+ Add an axes.
1519
+
1520
+ Parameters
1521
+ ----------
1522
+ key : int or slice
1523
+ The 1D index.
1524
+ value : `ultraplot.axes.Axes`
1525
+ The ultraplot subplot or its child or panel axes,
1526
+ or a sequence thereof if the index was a slice.
1527
+ """
1528
+ if isinstance(key, Integral):
1529
+ value = self._validate_item(value, scalar=True)
1530
+ elif isinstance(key, slice):
1531
+ value = self._validate_item(value, scalar=False)
1532
+ else:
1533
+ raise IndexError("Multi dimensional item assignment is not supported.")
1534
+ return super().__setitem__(key, value) # could be list[:] = [1, 2, 3]
1535
+
1536
+ @classmethod
1537
+ def _add_command(cls, src, name):
1538
+ """
1539
+ Add a `SubplotGrid` method that iterates through axes methods.
1540
+ """
1541
+
1542
+ # Create the method
1543
+ def _grid_command(self, *args, **kwargs):
1544
+ objs = []
1545
+ for ax in self:
1546
+ obj = getattr(ax, name)(*args, **kwargs)
1547
+ objs.append(obj)
1548
+ return SubplotGrid(objs)
1549
+
1550
+ # Clean the docstring
1551
+ cmd = getattr(src, name)
1552
+ doc = inspect.cleandoc(cmd.__doc__) # dedents
1553
+ dot = doc.find(".")
1554
+ if dot != -1:
1555
+ doc = doc[:dot] + " for every axes in the grid" + doc[dot:]
1556
+ doc = re.sub(
1557
+ r"^(Returns\n-------\n)(.+)(\n\s+)(.+)",
1558
+ r"\1SubplotGrid\2A grid of the resulting axes.",
1559
+ doc,
1560
+ )
1561
+
1562
+ # Apply the method
1563
+ _grid_command.__qualname__ = f"SubplotGrid.{name}"
1564
+ _grid_command.__name__ = name
1565
+ _grid_command.__doc__ = doc
1566
+ setattr(cls, name, _grid_command)
1567
+
1568
+ def _validate_item(self, items, scalar=False):
1569
+ """
1570
+ Validate assignments. Accept diverse iterable inputs.
1571
+ """
1572
+ gridspec = None
1573
+ message = (
1574
+ "SubplotGrid can only be filled with ultraplot subplots "
1575
+ "belonging to the same GridSpec. Instead got {}."
1576
+ )
1577
+ items = np.atleast_1d(items)
1578
+ if self:
1579
+ gridspec = self.gridspec # compare against existing gridspec
1580
+ for item in items.flat:
1581
+ if not isinstance(item, paxes.Axes):
1582
+ raise ValueError(message.format(f"the object {item!r}"))
1583
+ item = item._get_topmost_axes()
1584
+ if not isinstance(item, maxes.SubplotBase):
1585
+ raise ValueError(message.format(f"the axes {item!r}"))
1586
+ gs = item.get_subplotspec().get_topmost_subplotspec().get_gridspec()
1587
+ if not isinstance(gs, GridSpec):
1588
+ raise ValueError(message.format(f"the GridSpec {gs!r}"))
1589
+ if gridspec and gs is not gridspec:
1590
+ raise ValueError(message.format("at least two different GridSpecs"))
1591
+ gridspec = gs
1592
+ if not scalar:
1593
+ items = tuple(items.flat)
1594
+ elif items.size == 1:
1595
+ items = items.flat[0]
1596
+ else:
1597
+ raise ValueError("Input must be a single ultraplot axes.")
1598
+ return items
1599
+
1600
+ @docstring._snippet_manager
1601
+ def format(self, **kwargs):
1602
+ """
1603
+ Call the ``format`` command for the `~SubplotGrid.figure`
1604
+ and every axes in the grid.
1605
+
1606
+ Parameters
1607
+ ----------
1608
+ %(axes.format)s
1609
+ **kwargs
1610
+ Passed to the projection-specific ``format`` command for each axes.
1611
+ Valid only if every axes in the grid belongs to the same class.
1612
+
1613
+ Other parameters
1614
+ ----------------
1615
+ %(figure.format)s
1616
+ %(cartesian.format)s
1617
+ %(polar.format)s
1618
+ %(geo.format)s
1619
+ %(rc.format)s
1620
+
1621
+ See also
1622
+ --------
1623
+ ultraplot.axes.Axes.format
1624
+ ultraplot.axes.CartesianAxes.format
1625
+ ultraplot.axes.PolarAxes.format
1626
+ ultraplot.axes.GeoAxes.format
1627
+ ultraplot.figure.Figure.format
1628
+ ultraplot.config.Configurator.context
1629
+ """
1630
+ self.figure.format(axs=self, **kwargs)
1631
+
1632
+ @property
1633
+ def figure(self):
1634
+ """
1635
+ The `ultraplot.figure.Figure` uniquely associated with this `SubplotGrid`.
1636
+ This is used with the `SubplotGrid.format` command.
1637
+
1638
+ See also
1639
+ --------
1640
+ ultraplot.gridspec.GridSpec.figure
1641
+ ultraplot.gridspec.SubplotGrid.gridspec
1642
+ ultraplot.figure.Figure.subplotgrid
1643
+ """
1644
+ return self.gridspec.figure
1645
+
1646
+ @property
1647
+ def gridspec(self):
1648
+ """
1649
+ The `~ultraplot.gridspec.GridSpec` uniquely associated with this `SubplotGrid`.
1650
+ This is used to resolve 2D indexing. See `~SubplotGrid.__getitem__` for details.
1651
+
1652
+ See also
1653
+ --------
1654
+ ultraplot.figure.Figure.gridspec
1655
+ ultraplot.gridspec.SubplotGrid.figure
1656
+ ultraplot.gridspec.SubplotGrid.shape
1657
+ """
1658
+ # Return the gridspec associatd with the grid
1659
+ if not self:
1660
+ raise ValueError("Unknown gridspec for empty SubplotGrid.")
1661
+ ax = self[0]
1662
+ ax = ax._get_topmost_axes()
1663
+ return ax.get_subplotspec().get_topmost_subplotspec().get_gridspec()
1664
+
1665
+ @property
1666
+ def shape(self):
1667
+ """
1668
+ The shape of the `~ultraplot.gridspec.GridSpec` associated with the grid.
1669
+ See `~SubplotGrid.__getitem__` for details.
1670
+
1671
+ See also
1672
+ --------
1673
+ ultraplot.gridspec.SubplotGrid.gridspec
1674
+ """
1675
+ # NOTE: Considered deprecating this but on second thought since this is
1676
+ # a 2D array-like object it should definitely have a shape attribute.
1677
+ return self.gridspec.get_geometry()
1678
+
1679
+
1680
+ # Dynamically add commands to generate twin or inset axes
1681
+ # TODO: Add commands that plot the input data for every
1682
+ # axes in the grid along a third dimension.
1683
+ for _src, _name in (
1684
+ (paxes.Axes, "panel"),
1685
+ (paxes.Axes, "panel_axes"),
1686
+ (paxes.Axes, "inset"),
1687
+ (paxes.Axes, "inset_axes"),
1688
+ (paxes.CartesianAxes, "altx"),
1689
+ (paxes.CartesianAxes, "alty"),
1690
+ (paxes.CartesianAxes, "dualx"),
1691
+ (paxes.CartesianAxes, "dualy"),
1692
+ (paxes.CartesianAxes, "twinx"),
1693
+ (paxes.CartesianAxes, "twiny"),
1694
+ ):
1695
+ SubplotGrid._add_command(_src, _name)
1696
+
1697
+ # Deprecated
1698
+ SubplotsContainer = warnings._rename_objs("0.8.0", SubplotsContainer=SubplotGrid)