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
@@ -0,0 +1,862 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Utilities for processing input data passed to plotting commands.
4
+ """
5
+ import functools
6
+ import sys
7
+
8
+ import numpy as np
9
+ import numpy.ma as ma
10
+
11
+ from . import ic # noqa: F401
12
+ from . import _not_none, warnings
13
+
14
+ try:
15
+ from cartopy.crs import PlateCarree
16
+ except ModuleNotFoundError:
17
+ PlateCarree = object
18
+
19
+
20
+ # Constants
21
+ BASEMAP_FUNCS = ( # default latlon=True
22
+ "barbs",
23
+ "contour",
24
+ "contourf",
25
+ "hexbin",
26
+ "imshow",
27
+ "pcolor",
28
+ "pcolormesh",
29
+ "plot",
30
+ "quiver",
31
+ "scatter",
32
+ "streamplot",
33
+ "step",
34
+ )
35
+ CARTOPY_FUNCS = ( # default transform=PlateCarree()
36
+ "barbs",
37
+ "contour",
38
+ "contourf",
39
+ "fill",
40
+ "fill_between",
41
+ "fill_betweenx", # NOTE: not sure if these work
42
+ "imshow",
43
+ "pcolor",
44
+ "pcolormesh",
45
+ "plot",
46
+ "quiver",
47
+ "scatter",
48
+ "streamplot",
49
+ "step",
50
+ "tricontour",
51
+ "tricontourf",
52
+ "tripcolor", # NOTE: not sure why these work
53
+ )
54
+
55
+
56
+ def _load_objects():
57
+ """
58
+ Load array-like objects.
59
+ """
60
+ # NOTE: We just want to detect if *input arrays* belong to these types -- and if
61
+ # this is the case, it means the module has already been imported! So, we only
62
+ # try loading these classes within autoformat calls. This saves >500ms of import
63
+ # time. We use ndarray as the default value for unimported types and in loops we
64
+ # are careful to check membership to np.ndarray before anything else.
65
+ global ndarray, DataArray, DataFrame, Series, Index, Quantity
66
+ ndarray = np.ndarray
67
+ DataArray = getattr(sys.modules.get("xarray", None), "DataArray", ndarray)
68
+ DataFrame = getattr(sys.modules.get("pandas", None), "DataFrame", ndarray)
69
+ Series = getattr(sys.modules.get("pandas", None), "Series", ndarray)
70
+ Index = getattr(sys.modules.get("pandas", None), "Index", ndarray)
71
+ Quantity = getattr(sys.modules.get("pint", None), "Quantity", ndarray)
72
+
73
+
74
+ _load_objects()
75
+
76
+
77
+ # Type utilities
78
+ def _is_numeric(data):
79
+ """
80
+ Test whether input is numeric array rather than datetime or strings.
81
+ """
82
+ array = _to_numpy_array(data)
83
+ return len(data) and (
84
+ np.issubdtype(array.dtype, np.number)
85
+ or np.issubdtype(array.dtype, object)
86
+ and all(isinstance(_, np.number) for _ in array.flat)
87
+ )
88
+
89
+
90
+ def _is_categorical(data):
91
+ """
92
+ Test whether input is array of strings.
93
+ """
94
+ array = _to_numpy_array(data)
95
+ return len(data) and (
96
+ np.issubdtype(array.dtype, str)
97
+ or np.issubdtype(array.dtype, object)
98
+ and any(isinstance(_, str) for _ in array.flat)
99
+ )
100
+
101
+
102
+ def _is_descending(data):
103
+ """
104
+ Test whether the input data is descending. This is used for auto axis reversal.
105
+ """
106
+ # NOTE: Want this to work with e.g. datetime object arrays and numpy datetime
107
+ # arrays so use try except clause.
108
+ data = _to_numpy_array(data)
109
+ if data.ndim > 1 or data.size < 2:
110
+ return False
111
+ try:
112
+ return all(x != abs(x) for x in np.diff(data))
113
+ except TypeError:
114
+ return False
115
+
116
+
117
+ def _to_duck_array(data, strip_units=False):
118
+ """
119
+ Convert arbitrary input to duck array. Preserve array containers with metadata.
120
+ """
121
+ _load_objects()
122
+ if data is None:
123
+ raise ValueError("Invalid data None.")
124
+ if not isinstance(
125
+ data, (ndarray, DataArray, DataFrame, Series, Index, Quantity)
126
+ ) or not np.iterable(data):
127
+ # WARNING: this strips e.g. scalar DataArray metadata
128
+ data = _to_numpy_array(data)
129
+ if strip_units: # used for z coordinates that cannot have units
130
+ if isinstance(data, (ndarray, Quantity)):
131
+ if Quantity is not ndarray and isinstance(data, Quantity):
132
+ data = data.magnitude
133
+ elif isinstance(data, DataArray):
134
+ if Quantity is not ndarray and isinstance(data.data, Quantity):
135
+ data = data.copy(deep=False)
136
+ data.data = data.data.magnitude
137
+ return data
138
+
139
+
140
+ def _to_numpy_array(data, strip_units=False):
141
+ """
142
+ Convert arbitrary input to numpy array. Preserve masked arrays and unit arrays.
143
+ """
144
+ _load_objects()
145
+ if data is None:
146
+ raise ValueError("Invalid data None.")
147
+ if isinstance(data, ndarray):
148
+ pass
149
+ elif isinstance(data, DataArray):
150
+ data = data.data # support pint quantities that get unit-stripped later
151
+ elif isinstance(data, (DataFrame, Series, Index)):
152
+ data = data.values
153
+ if Quantity is not ndarray and isinstance(data, Quantity):
154
+ if strip_units:
155
+ return np.atleast_1d(data.magnitude)
156
+ else:
157
+ return np.atleast_1d(data.magnitude) * data.units
158
+ else:
159
+ return np.atleast_1d(data) # natively preserves masked arrays
160
+
161
+
162
+ def _to_masked_array(data, *, copy=False):
163
+ """
164
+ Convert numpy array to masked array with consideration for datetimes and quantities.
165
+ """
166
+ units = None
167
+ if ndarray is not Quantity and isinstance(data, Quantity):
168
+ data, units = data.magnitude, data.units
169
+ else:
170
+ data = _to_numpy_array(data)
171
+ if data.dtype == "O":
172
+ data = ma.array(data, mask=False)
173
+ else:
174
+ data = ma.masked_invalid(data, copy=copy)
175
+ if np.issubdtype(data.dtype, np.integer):
176
+ data = data.astype(np.float64)
177
+ if np.issubdtype(data.dtype, np.number):
178
+ data.fill_value *= np.nan # default float fill_value is 1e+20 or 1e+20 + 0j
179
+ else:
180
+ pass # leave with default fill_value (e.g. NaT for datetime data)
181
+ return data, units
182
+
183
+
184
+ # Input data transformations
185
+ def _to_edges(x, y, z):
186
+ """
187
+ Enforce that coordinates are edges. Convert from centers if possible.
188
+ """
189
+ from ..utils import edges, edges2d
190
+
191
+ xlen, ylen = x.shape[-1], y.shape[0]
192
+ if z.ndim == 2 and z.shape[1] == xlen and z.shape[0] == ylen:
193
+ # Get edges given centers
194
+ if all(z.ndim == 1 and z.size > 1 and _is_numeric(z) for z in (x, y)):
195
+ x = edges(x)
196
+ y = edges(y)
197
+ else:
198
+ if x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 and _is_numeric(x):
199
+ x = edges2d(x)
200
+ if y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 and _is_numeric(y):
201
+ y = edges2d(y)
202
+ elif z.shape[-1] != xlen - 1 or z.shape[0] != ylen - 1:
203
+ # Helpful error message
204
+ raise ValueError(
205
+ f"Input shapes x {x.shape} and y {y.shape} must match "
206
+ f"array centers {z.shape} or "
207
+ f"array borders {tuple(i + 1 for i in z.shape)}."
208
+ )
209
+ return x, y
210
+
211
+
212
+ def _to_centers(x, y, z):
213
+ """
214
+ Enforce that coordinates are centers. Convert from edges if possible.
215
+ """
216
+ xlen, ylen = x.shape[-1], y.shape[0]
217
+ if z.ndim == 2 and z.shape[1] == xlen - 1 and z.shape[0] == ylen - 1:
218
+ # Get centers given edges
219
+ if all(z.ndim == 1 and z.size > 1 and _is_numeric(z) for z in (x, y)):
220
+ x = 0.5 * (x[1:] + x[:-1])
221
+ y = 0.5 * (y[1:] + y[:-1])
222
+ else:
223
+ if x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 and _is_numeric(x):
224
+ x = 0.25 * (x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:])
225
+ if y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 and _is_numeric(y):
226
+ y = 0.25 * (y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:])
227
+ elif z.shape[-1] != xlen or z.shape[0] != ylen:
228
+ # Helpful error message
229
+ raise ValueError(
230
+ f"Input shapes x {x.shape} and y {y.shape} "
231
+ f"must match z centers {z.shape} "
232
+ f"or z borders {tuple(i+1 for i in z.shape)}."
233
+ )
234
+ return x, y
235
+
236
+
237
+ # Input argument processing
238
+ def _from_data(data, *args):
239
+ """
240
+ Try to convert positional `key` arguments to `data[key]`. If argument is string
241
+ it could be a valid positional argument like `fmt` so do not raise error.
242
+ """
243
+ if data is None:
244
+ return
245
+ args = list(args)
246
+ for i, arg in enumerate(args):
247
+ if isinstance(arg, str):
248
+ try:
249
+ array = data[arg]
250
+ except KeyError:
251
+ pass
252
+ else:
253
+ args[i] = array
254
+ return args
255
+
256
+
257
+ def _preprocess_or_redirect(*keys, keywords=None, allow_extra=True):
258
+ """
259
+ Redirect internal plotting calls to native matplotlib methods. Also convert
260
+ keyword args to positional and pass arguments through 'data' dictionary.
261
+ """
262
+ # Keyword arguments processed through 'data'
263
+ # Positional arguments are always processed through data
264
+ keywords = keywords or ()
265
+ if isinstance(keywords, str):
266
+ keywords = (keywords,)
267
+
268
+ def _decorator(func):
269
+ name = func.__name__
270
+ from . import _kwargs_to_args
271
+
272
+ @functools.wraps(func)
273
+ def _preprocess_or_redirect(self, *args, **kwargs):
274
+ if getattr(self, "_internal_call", None):
275
+ # Redirect internal matplotlib call to native function
276
+ from ..axes import PlotAxes
277
+
278
+ func_native = getattr(super(PlotAxes, self), name)
279
+ return func_native(*args, **kwargs)
280
+ else:
281
+ # Impose default coordinate system
282
+ from ..constructor import Proj
283
+
284
+ if self._name == "basemap" and name in BASEMAP_FUNCS:
285
+ if kwargs.get("latlon", None) is None:
286
+ kwargs["latlon"] = True
287
+ if self._name == "cartopy" and name in CARTOPY_FUNCS:
288
+ if kwargs.get("transform", None) is None:
289
+ kwargs["transform"] = PlateCarree()
290
+ else:
291
+ kwargs["transform"] = Proj(kwargs["transform"])
292
+
293
+ # Process data args
294
+ # NOTE: Raises error if there are more args than keys
295
+ args, kwargs = _kwargs_to_args(
296
+ keys, *args, allow_extra=allow_extra, **kwargs
297
+ )
298
+ data = kwargs.pop("data", None)
299
+ if data is not None:
300
+ args = _from_data(data, *args)
301
+ for key in set(keywords) & set(kwargs):
302
+ kwargs[key] = _from_data(data, kwargs[key])
303
+
304
+ # Auto-setup matplotlib with the input unit registry
305
+ _load_objects()
306
+ for arg in args:
307
+ if ndarray is not DataArray and isinstance(arg, DataArray):
308
+ arg = arg.data
309
+ if ndarray is not Quantity and isinstance(arg, Quantity):
310
+ ureg = getattr(arg, "_REGISTRY", None)
311
+ if hasattr(ureg, "setup_matplotlib"):
312
+ ureg.setup_matplotlib(True)
313
+
314
+ # Sanitize colors
315
+ # Lookup can be a 2-tuple with colormap, and integer denoting the color from that map
316
+ # it also sanitizes if the second color is a float. Note that for float, the color
317
+ # will be picked proportional to idx/255
318
+ from .. import colors as pcolor
319
+
320
+ color = kwargs.pop("color", None)
321
+ if isinstance(color, tuple) and len(color) == 2:
322
+ cmap, color = color
323
+ color = pcolor._cmap_database.get_cmap(cmap)(color)
324
+ if color is not None:
325
+ kwargs["color"] = color
326
+ # Call main function
327
+ return func(self, *args, **kwargs) # call unbound method
328
+
329
+ return _preprocess_or_redirect
330
+
331
+ return _decorator
332
+
333
+
334
+ # Stats utiltiies
335
+ def _dist_clean(distribution):
336
+ """
337
+ Clean the distribution data for processing by `boxplot` or `violinplot`.
338
+ Handles np.ndarrays where the ndarray is a list of lists of variable sizes.
339
+ """
340
+ if isinstance(distribution, np.ndarray):
341
+ if distribution.dtype == object:
342
+ # Handle list of lists with variable sizes
343
+ return tuple(
344
+ np.array(sublist, dtype=float)
345
+ for sublist in distribution
346
+ if len(sublist) > 0
347
+ )
348
+ else:
349
+ # Handle regular numpy arrays
350
+ if distribution.ndim == 1:
351
+ distribution = distribution[:, None]
352
+ distribution, units = _to_masked_array(distribution) # no copy needed
353
+ distribution = tuple(
354
+ distribution[..., i].compressed() for i in range(distribution.shape[-1])
355
+ )
356
+ if units is not None:
357
+ distribution = tuple(dist * units for dist in distribution)
358
+ return distribution
359
+ elif isinstance(distribution, list):
360
+ # Handle list of lists directly
361
+ return tuple(
362
+ np.array(sublist, dtype=float)
363
+ for sublist in distribution
364
+ if len(sublist) > 0
365
+ )
366
+ else:
367
+ raise ValueError("Input must be a numpy array or a list of lists")
368
+
369
+
370
+ def _dist_reduce(data, *, mean=None, means=None, median=None, medians=None, **kwargs):
371
+ """
372
+ Reduce statistical distributions to means and medians. Tack on a
373
+ distribution keyword argument for processing down the line.
374
+ """
375
+ # TODO: Permit 3D array with error dimension coming first
376
+ means = _not_none(mean=mean, means=means)
377
+ medians = _not_none(median=median, medians=medians)
378
+ if means and medians:
379
+ warnings._warn_ultraplot(
380
+ "Cannot have both means=True and medians=True. Using former."
381
+ )
382
+ medians = None
383
+ if means or medians:
384
+ distribution, units = _to_masked_array(data)
385
+ distribution = distribution.filled()
386
+ if distribution.ndim != 2:
387
+ raise ValueError(
388
+ f"Expected 2D array for means=True. Got {distribution.ndim}D."
389
+ )
390
+ if units is not None:
391
+ distribution = distribution * units
392
+ if means:
393
+ data = np.nanmean(distribution, axis=0)
394
+ else:
395
+ data = np.nanmedian(distribution, axis=0)
396
+ kwargs["distribution"] = distribution
397
+
398
+ # Save argument passed to _error_bars
399
+ return (data, kwargs)
400
+
401
+
402
+ def _dist_range(
403
+ data,
404
+ distribution,
405
+ *,
406
+ errdata=None,
407
+ absolute=False,
408
+ label=False,
409
+ stds=None,
410
+ pctiles=None,
411
+ stds_default=None,
412
+ pctiles_default=None,
413
+ ):
414
+ """
415
+ Return a plottable characteristic range for the statistical distribution
416
+ relative to the input coordinate (generally a mean or median).
417
+ """
418
+ # Parse stds arguments
419
+ # NOTE: Have to guard against "truth value of an array is ambiguous" errors
420
+ if stds is True:
421
+ stds = stds_default
422
+ elif stds is False or stds is None:
423
+ stds = None
424
+ else:
425
+ stds = np.atleast_1d(stds)
426
+ if stds.size == 1:
427
+ stds = sorted((-stds.item(), stds.item()))
428
+ elif stds.size != 2:
429
+ raise ValueError("Expected scalar or length-2 stdev specification.")
430
+
431
+ # Parse pctiles arguments
432
+ if pctiles is True:
433
+ pctiles = pctiles_default
434
+ elif pctiles is False or pctiles is None:
435
+ pctiles = None
436
+ else:
437
+ pctiles = np.atleast_1d(pctiles)
438
+ if pctiles.size == 1:
439
+ delta = (100 - pctiles.item()) / 2.0
440
+ pctiles = sorted((delta, 100 - delta))
441
+ elif pctiles.size != 2:
442
+ raise ValueError("Expected scalar or length-2 pctiles specification.")
443
+
444
+ # Incompatible settings
445
+ if distribution is None and any(_ is not None for _ in (stds, pctiles)):
446
+ raise ValueError(
447
+ "To automatically compute standard deviations or percentiles on "
448
+ "columns of data you must pass means=True or medians=True."
449
+ )
450
+ if stds is not None and pctiles is not None:
451
+ warnings._warn_ultraplot(
452
+ "Got both a standard deviation range and a percentile range for "
453
+ "auto error indicators. Using the standard deviation range."
454
+ )
455
+ pctiles = None
456
+ if distribution is not None and errdata is not None:
457
+ stds = pctiles = None
458
+ warnings._warn_ultraplot(
459
+ "You explicitly provided the error bounds but also requested "
460
+ "automatically calculating means or medians on data columns. "
461
+ 'It may make more sense to use the "stds" or "pctiles" keyword args '
462
+ "and have *ultraplot* calculate the error bounds."
463
+ )
464
+
465
+ # Compute error data in format that can be passed to maxes.Axes.errorbar()
466
+ # NOTE: Include option to pass symmetric deviation from central points
467
+ if errdata is not None:
468
+ # Manual error data
469
+ if data.ndim != 1:
470
+ raise ValueError(
471
+ "Passing both 2D data coordinates and 'errdata' is not yet supported."
472
+ )
473
+ label_default = "uncertainty"
474
+ err = _to_numpy_array(errdata)
475
+ if (
476
+ err.ndim not in (1, 2)
477
+ or err.shape[-1] != data.size
478
+ or err.ndim == 2
479
+ and err.shape[0] != 2
480
+ ):
481
+ raise ValueError(
482
+ f"Input 'errdata' has shape {err.shape}. Expected (2, {data.size})."
483
+ )
484
+ if err.ndim == 1:
485
+ abserr = err
486
+ err = np.empty((2, err.size))
487
+ err[0, :] = data - abserr # translated back to absolute deviations below
488
+ err[1, :] = data + abserr
489
+ elif stds is not None:
490
+ # Standard deviations
491
+ # NOTE: Invalid values were handled by _dist_reduce
492
+ label_default = rf"{abs(stds[1])}$\sigma$ range"
493
+ stds = _to_numpy_array(stds)[:, None]
494
+ err = data + stds * np.nanstd(distribution, axis=0)
495
+ elif pctiles is not None:
496
+ # Percentiles
497
+ # NOTE: Invalid values were handled by _dist_reduce
498
+ label_default = f"{pctiles[1] - pctiles[0]}% range"
499
+ err = np.nanpercentile(distribution, pctiles, axis=0)
500
+ else:
501
+ warnings._warn_ultraplot(
502
+ "Error indications are missing from the dataset reduced by a "
503
+ "mean or median operation. Consider passing e.g. bars=True."
504
+ )
505
+ err = None
506
+
507
+ # Adjust error bounds
508
+ if err is not None and not absolute: # for errorbar() ingestion
509
+ err = err - data
510
+ err[0, :] *= -1 # absolute deviations from central points
511
+
512
+ # Apply legend entry
513
+ if isinstance(label, str):
514
+ pass
515
+ elif label: # e.g. label=True says to use a default label
516
+ label = label_default
517
+ else:
518
+ label = None
519
+
520
+ return err, label
521
+
522
+
523
+ def _safe_mask(mask, *args):
524
+ """
525
+ Safely apply the mask to the input arrays, accounting for existing masked
526
+ or invalid values. Values matching ``False`` are set to `np.nan`.
527
+ """
528
+ # NOTE: Could also convert unmasked data to masked. But other way around is
529
+ # easier becase np.ma gives us correct fill values for data subtypes.
530
+ _load_objects()
531
+ invalid = ~mask # True if invalid
532
+ args_masked = []
533
+ for data in args:
534
+ data, units = _to_masked_array(data, copy=True)
535
+ nan = data.fill_value
536
+ data = data.filled()
537
+ if data.size > 1 and data.shape != invalid.shape:
538
+ raise ValueError(
539
+ f"Mask shape {mask.shape} incompatible with array shape {data.shape}."
540
+ )
541
+ if data.size == 1 or invalid.size == 1: # NOTE: happens with _restrict_inbounds
542
+ pass
543
+ elif invalid.size == 1:
544
+ data = nan if invalid.item() else data
545
+ elif data.size > 1:
546
+ data[invalid] = nan
547
+ if units is not None:
548
+ data = data * units
549
+ args_masked.append(data)
550
+ return args_masked[0] if len(args_masked) == 1 else args_masked
551
+
552
+
553
+ def _safe_range(data, lo=0, hi=100):
554
+ """
555
+ Safely return the minimum and maximum (default) or percentile range accounting
556
+ for masked values. Use min and max functions when possible for speed. Return
557
+ ``None`` if we fail to get a valid range.
558
+ """
559
+ _load_objects()
560
+ data, units = _to_masked_array(data)
561
+ data = data.compressed() # remove all invalid values
562
+ min_ = max_ = None
563
+ if data.size:
564
+ min_ = np.min(data) if lo <= 0 else np.percentile(data, lo)
565
+ if hasattr(min_, "dtype") and np.issubdtype(min_.dtype, np.integer):
566
+ min_ = np.float64(min_)
567
+ try:
568
+ is_finite = np.isfinite(min_)
569
+ except TypeError:
570
+ is_finite = True
571
+ if not is_finite:
572
+ min_ = None
573
+ elif units is not None:
574
+ min_ *= units
575
+ if data.size:
576
+ max_ = np.max(data) if hi >= 100 else np.percentile(data, hi)
577
+ if hasattr(max_, "dtype") and np.issubdtype(max_.dtype, np.integer):
578
+ max_ = np.float64(max_)
579
+ try:
580
+ is_finite = np.isfinite(min_)
581
+ except TypeError:
582
+ is_finite = True
583
+ if not is_finite:
584
+ max_ = None
585
+ elif units is not None:
586
+ max_ *= units
587
+ return min_, max_
588
+
589
+
590
+ # Metadata utilities
591
+ def _meta_coords(*args, which="x", **kwargs):
592
+ """
593
+ Return the index arrays associated with string coordinates and
594
+ keyword arguments updated with index locators and formatters.
595
+ """
596
+ # NOTE: Why FixedLocator and not IndexLocator? The ticks chosen by the latter
597
+ # depend on other plotted content.
598
+ # NOTE: Why IndexFormatter and not FixedFormatter? The former ensures labels
599
+ # correspond to indices while the latter can mysteriously truncate labels.
600
+ from ..constructor import Formatter, Locator
601
+
602
+ res = []
603
+ for data in args:
604
+ data = _to_duck_array(data)
605
+ if not _is_categorical(data):
606
+ res.append(data)
607
+ continue
608
+ if data.ndim > 1:
609
+ raise ValueError("Non-1D string coordinate input is unsupported.")
610
+ ticks = np.arange(len(data))
611
+ labels = list(map(str, data))
612
+ kwargs.setdefault(which + "locator", Locator(ticks))
613
+ kwargs.setdefault(which + "formatter", Formatter(labels, index=True))
614
+ kwargs.setdefault(which + "minorlocator", Locator("null"))
615
+ res.append(ticks) # use these as data coordinates
616
+ return (*res, kwargs)
617
+
618
+
619
+ def _meta_labels(data, axis=0, always=True):
620
+ """
621
+ Return the array-like "labels" along axis `axis`. If `always` is ``False``
622
+ we return ``None`` for simple ndarray input.
623
+ """
624
+ # NOTE: Previously inferred 'axis 1' metadata of 1D variable using the
625
+ # data values metadata but that is incorrect. The paradigm for 1D plots
626
+ # is we have row coordinates representing x, data values representing y,
627
+ # and column coordinates representing individual series.
628
+ _load_objects()
629
+ labels = None
630
+ if axis not in (0, 1, 2):
631
+ raise ValueError(f"Invalid axis {axis}.")
632
+ if isinstance(data, (ndarray, Quantity)):
633
+ if not always:
634
+ pass
635
+ elif axis < data.ndim:
636
+ labels = np.arange(data.shape[axis])
637
+ else: # requesting 'axis 1' on a 1D array
638
+ labels = np.array([0])
639
+ # Xarray object
640
+ # NOTE: Even if coords not present .coords[dim] auto-generates indices
641
+ elif isinstance(data, DataArray):
642
+ if axis < data.ndim:
643
+ labels = data.coords[data.dims[axis]]
644
+ elif not always:
645
+ pass
646
+ else:
647
+ labels = np.array([0])
648
+ # Pandas object
649
+ elif isinstance(data, (DataFrame, Series, Index)):
650
+ if axis == 0 and isinstance(data, (DataFrame, Series)):
651
+ labels = data.index
652
+ elif axis == 1 and isinstance(data, (DataFrame,)):
653
+ labels = data.columns
654
+ elif not always:
655
+ pass
656
+ else: # beyond dimensionality
657
+ labels = np.array([0])
658
+ # Everything else
659
+ # NOTE: Ensure data is at least 1D in _to_duck_array so this covers everything
660
+ else:
661
+ raise ValueError(f"Unrecognized array type {type(data)}.")
662
+ return labels
663
+
664
+
665
+ def _meta_title(data, include_units=True):
666
+ """
667
+ Return the "title" of an array-like object with metadata.
668
+ Include units in the title if `include_units` is ``True``.
669
+ """
670
+ _load_objects()
671
+ title = units = None
672
+ if isinstance(data, ndarray):
673
+ pass
674
+ # Xarray object with possible long_name, standard_name, and units attributes.
675
+ # Output depends on if units is True. Prefer long_name (come last in loop).
676
+ elif isinstance(data, DataArray):
677
+ title = getattr(data, "name", None)
678
+ for key in ("standard_name", "long_name"):
679
+ title = data.attrs.get(key, title)
680
+ if include_units:
681
+ units = _meta_units(data)
682
+ # Pandas object. DataFrame has no native name attribute but user can add one
683
+ # See: https://github.com/pandas-dev/pandas/issues/447
684
+ elif isinstance(data, (DataFrame, Series, Index)):
685
+ title = getattr(data, "name", None) or None
686
+ # Pint Quantity
687
+ elif isinstance(data, Quantity):
688
+ if include_units:
689
+ units = _meta_units(data)
690
+ # Add units or return units alone
691
+ if title and units:
692
+ title = f"{title} ({units})"
693
+ else:
694
+ title = title or units
695
+ if title is not None:
696
+ title = str(title).strip()
697
+ return title
698
+
699
+
700
+ def _meta_units(data):
701
+ """
702
+ Get the unit string from the `xarray.DataArray` attributes or the
703
+ `pint.Quantity`. Format the latter with :rcraw:`unitformat`.
704
+ """
705
+ _load_objects()
706
+ # Get units from the attributes
707
+ if ndarray is not DataArray and isinstance(data, DataArray):
708
+ units = data.attrs.get("units", None)
709
+ data = data.data
710
+ if units is not None:
711
+ return units
712
+ # Get units from the quantity
713
+ if ndarray is not Quantity and isinstance(data, Quantity):
714
+ from ..config import rc
715
+
716
+ fmt = rc.unitformat
717
+ try:
718
+ units = format(data.units, fmt)
719
+ except (TypeError, ValueError):
720
+ warnings._warn_ultraplot(
721
+ f"Failed to format pint quantity with format string {fmt!r}."
722
+ )
723
+ else:
724
+ if "L" in fmt: # auto-apply LaTeX math indicator
725
+ units = "$" + units + "$"
726
+ return units
727
+
728
+
729
+ # Geographic utiltiies
730
+ def _geo_basemap_1d(x, *ys, xmin=-180, xmax=180):
731
+ """
732
+ Fix basemap geographic 1D data arrays.
733
+ """
734
+ ys = _geo_clip(*ys)
735
+ x_orig, ys_orig, ys = x, ys, []
736
+ for y_orig in ys_orig:
737
+ x, y = _geo_inbounds(x_orig, y_orig, xmin=xmin, xmax=xmax)
738
+ ys.append(y)
739
+ return (x, *ys)
740
+
741
+
742
+ def _geo_basemap_2d(x, y, *zs, xmin=-180, xmax=180, globe=False):
743
+ """
744
+ Fix basemap geographic 2D data arrays.
745
+ """
746
+ y = _geo_clip(y)
747
+ x_orig, y_orig, zs_orig, zs = x, y, zs, []
748
+ for z_orig in zs_orig:
749
+ x, y, z = x_orig, y_orig, z_orig
750
+ x, z = _geo_inbounds(x, z, xmin=xmin, xmax=xmax)
751
+ if globe and z is not None and x.ndim == 1 and y.ndim == 1:
752
+ x, y, z = _geo_globe(x, y, z, xmin=xmin, modulo=False)
753
+ zs.append(z)
754
+ return (x, y, *zs)
755
+
756
+
757
+ def _geo_cartopy_1d(x, *ys):
758
+ """
759
+ Fix cartopy geographic 1D data arrays.
760
+ """
761
+ ys = _geo_clip(ys)
762
+ return (x, *ys)
763
+
764
+
765
+ def _geo_cartopy_2d(x, y, *zs, globe=False):
766
+ """
767
+ Fix cartopy geographic 2D data arrays.
768
+ """
769
+ y = _geo_clip(y)
770
+ x_orig, y_orig, zs_orig = x, y, zs
771
+ zs = []
772
+ for z_orig in zs_orig:
773
+ x, y, z = x_orig, y_orig, z_orig
774
+ if globe and z is not None and x.ndim == 1 and y.ndim == 1:
775
+ x, y, z = _geo_globe(x, y, z, modulo=True)
776
+ zs.append(z)
777
+ return (x, y, *zs)
778
+
779
+
780
+ def _geo_clip(*ys):
781
+ """
782
+ Ensure latitudes fall within ``-90`` to ``90``. Important if we
783
+ add graticule edges with `edges`.
784
+ """
785
+ ys = tuple(np.clip(y, -90, 90) for y in ys)
786
+ return ys[0] if len(ys) == 1 else ys
787
+
788
+
789
+ def _geo_inbounds(x, y, xmin=-180, xmax=180):
790
+ """
791
+ Fix conflicts with map coordinates by rolling the data to fall between the
792
+ minimum and maximum longitudes and masking out-of-bounds data points.
793
+ """
794
+ # Roll in same direction if some points on right-edge extend
795
+ # more than 360 above min longitude; *they* should be on left side
796
+ if x.ndim != 1:
797
+ return x, y
798
+ lonroll = np.where(x > xmin + 360)[0] # tuple of ids
799
+ if lonroll.size: # non-empty
800
+ roll = x.size - lonroll.min()
801
+ x = np.roll(x, roll)
802
+ y = np.roll(y, roll, axis=-1)
803
+ x[:roll] -= 360 # make monotonic
804
+ # Set NaN where data not in range xmin, xmax. Must be done for regional smaller
805
+ # projections or get weird side-effects from valid data outside boundaries
806
+ y, units = _to_masked_array(y)
807
+ nan = y.fill_value
808
+ y = y.filled()
809
+ if not y.shape:
810
+ pass
811
+ elif x.size - 1 == y.shape[-1]: # test western/eastern grid cell edges
812
+ mask = (x[1:] < xmin) | (x[:-1] > xmax)
813
+ y[..., mask] = nan
814
+ elif x.size == y.shape[-1]: # test the centers and pad by one for safety
815
+ (where,) = np.where((x < xmin) | (x > xmax))
816
+ y[..., where[1:-1]] = nan
817
+ return x, y
818
+
819
+
820
+ def _geo_globe(x, y, z, xmin=-180, modulo=False):
821
+ """
822
+ Ensure global coverage by fixing gaps over poles and across
823
+ longitude seams. Increases the size of the arrays.
824
+ """
825
+ # Cover gaps over poles by appending polar data
826
+ with np.errstate(all="ignore"):
827
+ p1 = np.mean(z[0, :]) # do not ignore NaN if present
828
+ p2 = np.mean(z[-1, :])
829
+ ps = (-90, 90) if (y[0] < y[-1]) else (90, -90)
830
+ z1 = np.repeat(p1, z.shape[1])
831
+ z2 = np.repeat(p2, z.shape[1])
832
+ y = ma.concatenate((ps[:1], y, ps[1:]))
833
+ z = ma.concatenate((z1[None, :], z, z2[None, :]), axis=0)
834
+ # Cover gaps over cartopy longitude seam
835
+ # Ensure coordinates span 360 after modulus
836
+ if modulo:
837
+ if x[0] % 360 != (x[-1] + 360) % 360:
838
+ x = ma.concatenate((x, (x[0] + 360,)))
839
+ z = ma.concatenate((z, z[:, :1]), axis=1)
840
+ # Cover gaps over basemap longitude seam
841
+ # Ensure coordinates span exactly 360
842
+ else:
843
+ # Interpolate coordinate centers to seam. Size possibly augmented by 2
844
+ if x.size == z.shape[1]:
845
+ if x[0] + 360 != x[-1]:
846
+ xi = np.array([x[-1], x[0] + 360]) # input coordinates
847
+ xq = xmin + 360 # query coordinate
848
+ zq = ma.concatenate((z[:, -1:], z[:, :1]), axis=1)
849
+ zq = (zq[:, :1] * (xi[1] - xq) + zq[:, 1:] * (xq - xi[0])) / (
850
+ xi[1] - xi[0]
851
+ ) # noqa: E501
852
+ x = ma.concatenate(((xmin,), x, (xmin + 360,)))
853
+ z = ma.concatenate((zq, z, zq), axis=1)
854
+ # Extend coordinate edges to seam. Size possibly augmented by 1.
855
+ elif x.size - 1 == z.shape[1]:
856
+ if x[0] != xmin:
857
+ x = ma.append(xmin, x)
858
+ x[-1] = xmin + 360
859
+ z = ma.concatenate((z[:, -1:], z), axis=1)
860
+ else:
861
+ raise ValueError("Unexpected shapes of coordinates or data arrays.")
862
+ return x, y, z