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/config.py ADDED
@@ -0,0 +1,1809 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tools for setting up ultraplot and configuring global settings.
4
+ See the :ref:`configuration guide <ug_config>` for details.
5
+ """
6
+ # NOTE: The matplotlib analogue to this file is actually __init__.py
7
+ # but it makes more sense to have all the setup actions in a separate file
8
+ # so the namespace of the top-level module is unpolluted.
9
+ # NOTE: Why also load colormaps and cycles in this file and not colors.py?
10
+ # Because I think it makes sense to have all the code that "runs" (i.e. not
11
+ # just definitions) in the same place, and I was having issues with circular
12
+ # dependencies and where import order of __init__.py was affecting behavior.
13
+ import logging
14
+ import os
15
+ import re
16
+ import sys
17
+ from collections import namedtuple
18
+ from collections.abc import MutableMapping
19
+ from numbers import Real
20
+
21
+ import cycler
22
+ import matplotlib as mpl
23
+ import matplotlib.colors as mcolors
24
+ import matplotlib.font_manager as mfonts
25
+ import matplotlib.mathtext # noqa: F401
26
+ import matplotlib.style.core as mstyle
27
+ import numpy as np
28
+ from matplotlib import RcParams
29
+
30
+ from .internals import ic # noqa: F401
31
+ from .internals import (
32
+ _not_none,
33
+ _pop_kwargs,
34
+ _pop_props,
35
+ _translate_grid,
36
+ _version_mpl,
37
+ docstring,
38
+ rcsetup,
39
+ warnings,
40
+ )
41
+
42
+ try:
43
+ from IPython import get_ipython
44
+ except ImportError:
45
+
46
+ def get_ipython():
47
+ return
48
+
49
+
50
+ # Suppress warnings emitted by mathtext.py (_mathtext.py in recent versions)
51
+ # when when substituting dummy unavailable glyph due to fallback disabled.
52
+ logging.getLogger("matplotlib.mathtext").setLevel(logging.ERROR)
53
+
54
+ __all__ = [
55
+ "Configurator",
56
+ "rc",
57
+ "rc_ultraplot",
58
+ "rc_matplotlib",
59
+ "use_style",
60
+ "config_inline_backend",
61
+ "register_cmaps",
62
+ "register_cycles",
63
+ "register_colors",
64
+ "register_fonts",
65
+ "RcConfigurator", # deprecated
66
+ "inline_backend_fmt", # deprecated
67
+ ]
68
+
69
+ # Constants
70
+ COLORS_KEEP = ("red", "green", "blue", "cyan", "yellow", "magenta", "white", "black")
71
+
72
+ # Configurator docstrings
73
+ _rc_docstring = """
74
+ local : bool, default: True
75
+ Whether to load settings from the `~Configurator.local_files` file.
76
+ user : bool, default: True
77
+ Whether to load settings from the `~Configurator.user_file` file.
78
+ default : bool, default: True
79
+ Whether to reload built-in default ultraplot settings.
80
+ """
81
+ docstring._snippet_manager["rc.params"] = _rc_docstring
82
+
83
+ # Registration docstrings
84
+ _shared_docstring = """
85
+ *args : path-spec or `~ultraplot.colors.{type}Colormap`, optional
86
+ The {objects} to register. These can be file paths containing
87
+ RGB data or `~ultraplot.colors.{type}Colormap` instances. By default,
88
+ if positional arguments are passed, then `user` is set to ``False``.
89
+
90
+ Valid file extensions are listed in the below table. Note that {objects}
91
+ are registered according to their filenames -- for example, ``name.xyz``
92
+ will be registered as ``'name'``.
93
+ """ # noqa: E501
94
+ _cmap_exts_docstring = """
95
+ =================== ==========================================
96
+ Extension Description
97
+ =================== ==========================================
98
+ ``.json`` JSON database of the channel segment data.
99
+ ``.hex`` Comma-delimited list of HEX strings.
100
+ ``.rgb``, ``.txt`` 3-4 column table of channel values.
101
+ =================== ==========================================
102
+ """
103
+ _cycle_exts_docstring = """
104
+ ================== ==========================================
105
+ Extension Description
106
+ ================== ==========================================
107
+ ``.hex`` Comma-delimited list of HEX strings.
108
+ ``.rgb``, ``.txt`` 3-4 column table of channel values.
109
+ ================== ==========================================
110
+ """
111
+ _color_docstring = """
112
+ *args : path-like or dict, optional
113
+ The colors to register. These can be file paths containing RGB data or
114
+ dictionary mappings of names to RGB values. By default, if positional
115
+ arguments are passed, then `user` is set to ``False``. Files must have
116
+ the extension ``.txt`` and should contain one line per color in the
117
+ format ``name : hex``. Whitespace is ignored.
118
+ """
119
+ _font_docstring = """
120
+ *args : path-like, optional
121
+ The font files to add. By default, if positional arguments are passed, then
122
+ `user` is set to ``False``. Files must have the extensions ``.ttf`` or ``.otf``.
123
+ See `this link \
124
+ <https://gree2.github.io/python/2015/04/27/python-change-matplotlib-font-on-mac>`__
125
+ for a guide on converting other font files to ``.ttf`` and ``.otf``.
126
+ """
127
+ _register_docstring = """
128
+ user : bool, optional
129
+ Whether to reload {objects} from `~Configurator.user_folder`. Default is
130
+ ``False`` if positional arguments were passed and ``True`` otherwise.
131
+ local : bool, optional
132
+ Whether to reload {objects} from `~Configurator.local_folders`. Default is
133
+ ``False`` if positional arguments were passed and ``True`` otherwise.
134
+ default : bool, default: False
135
+ Whether to reload the default {objects} packaged with ultraplot.
136
+ Default is always ``False``.
137
+ """
138
+ docstring._snippet_manager["rc.cmap_params"] = _register_docstring.format(
139
+ objects="colormaps"
140
+ ) # noqa: E501
141
+ docstring._snippet_manager["rc.cycle_params"] = _register_docstring.format(
142
+ objects="color cycles"
143
+ ) # noqa: E501
144
+ docstring._snippet_manager["rc.color_params"] = _register_docstring.format(
145
+ objects="colors"
146
+ ) # noqa: E501
147
+ docstring._snippet_manager["rc.font_params"] = _register_docstring.format(
148
+ objects="fonts"
149
+ ) # noqa: E501
150
+ docstring._snippet_manager["rc.cmap_args"] = _shared_docstring.format(
151
+ objects="colormaps", type="Continuous"
152
+ ) # noqa: E501
153
+ docstring._snippet_manager["rc.cycle_args"] = _shared_docstring.format(
154
+ objects="color cycles", type="Discrete"
155
+ ) # noqa: E501
156
+ docstring._snippet_manager["rc.color_args"] = _color_docstring
157
+ docstring._snippet_manager["rc.font_args"] = _font_docstring
158
+ docstring._snippet_manager["rc.cmap_exts"] = _cmap_exts_docstring
159
+ docstring._snippet_manager["rc.cycle_exts"] = _cycle_exts_docstring
160
+
161
+
162
+ def _init_user_file():
163
+ """
164
+ Initialize .ultraplotrc file.
165
+ """
166
+ file = Configurator.user_file()
167
+ if not os.path.exists(file):
168
+ Configurator._save_yaml(file, comment=True)
169
+
170
+
171
+ def _init_user_folders():
172
+ """
173
+ Initialize .ultraplot folder.
174
+ """
175
+ for subfolder in ("", "cmaps", "cycles", "colors", "fonts"):
176
+ folder = Configurator.user_folder(subfolder)
177
+ if not os.path.isdir(folder):
178
+ os.mkdir(folder)
179
+
180
+
181
+ def _get_data_folders(folder, user=True, local=True, default=True, reverse=False):
182
+ """
183
+ Return data folder paths in reverse order of precedence.
184
+ """
185
+ # When loading colormaps, cycles, and colors, files in the latter
186
+ # directories overwrite files in the former directories. When loading
187
+ # fonts, the resulting paths need to be *reversed*.
188
+ paths = []
189
+ if default:
190
+ paths.append(os.path.join(os.path.dirname(__file__), folder))
191
+ if user:
192
+ paths.append(Configurator.user_folder(folder))
193
+ if local:
194
+ paths.extend(Configurator.local_folders(folder))
195
+ if reverse:
196
+ paths = paths[::-1]
197
+ return paths
198
+
199
+
200
+ def _iter_data_objects(folder, *args, **kwargs):
201
+ """
202
+ Iterate over input objects and files in the data folders that should be
203
+ registered. Also yield an index indicating whether these are user files.
204
+ """
205
+ i = 0 # default files
206
+ for i, path in enumerate(_get_data_folders(folder, **kwargs)):
207
+ for dirname, dirnames, filenames in os.walk(path):
208
+ for filename in filenames:
209
+ if filename[0] == ".": # UNIX-style hidden files
210
+ continue
211
+ path = os.path.join(dirname, filename)
212
+ yield i, path
213
+ i += 1 # user files
214
+ for path in args:
215
+ path = os.path.expanduser(path)
216
+ if os.path.isfile(path):
217
+ yield i, path
218
+ else:
219
+ raise FileNotFoundError(f"Invalid file path {path!r}.")
220
+
221
+
222
+ def _filter_style_dict(rcdict, warn=True):
223
+ """
224
+ Filter out blacklisted style parameters.
225
+ """
226
+ # NOTE: This implements bugfix: https://github.com/matplotlib/matplotlib/pull/17252
227
+ # This fix is *critical* for ultraplot because we always run style.use()
228
+ # when the configurator is made. Without fix backend is reset every time
229
+ # you import ultraplot in jupyter notebooks. So apply retroactively.
230
+ rcdict_filtered = {}
231
+ for key in rcdict:
232
+ if key in mstyle.STYLE_BLACKLIST:
233
+ if warn:
234
+ warnings._warn_ultraplot(
235
+ f"Dictionary includes a parameter, {key!r}, that is not related "
236
+ "to style. Ignoring."
237
+ )
238
+ else:
239
+ rcdict_filtered[key] = rcdict[key]
240
+ return rcdict_filtered
241
+
242
+
243
+ def _get_default_style_dict():
244
+ """
245
+ Get the default rc parameters dictionary with deprecated parameters filtered.
246
+ """
247
+ # NOTE: Use RcParams update to filter and translate deprecated settings
248
+ # before actually applying them to rcParams down pipeline. This way we can
249
+ # suppress warnings for deprecated default params but still issue warnings
250
+ # when user-supplied stylesheets have deprecated params.
251
+ # WARNING: Some deprecated rc params remain in dictionary as None so we
252
+ # filter them out. Beware if hidden attribute changes.
253
+ # WARNING: The examples.directory deprecation was handled specially inside
254
+ # RcParams in early versions. Manually pop it out here.
255
+ rcdict = _filter_style_dict(mpl.rcParamsDefault, warn=False)
256
+ with warnings.catch_warnings():
257
+ warnings.simplefilter("ignore", mpl.MatplotlibDeprecationWarning)
258
+ rcdict = dict(RcParams(rcdict))
259
+ for attr in ("_deprecated_set", "_deprecated_remain_as_none"):
260
+ deprecated = getattr(mpl, attr, ())
261
+ for key in deprecated: # _deprecated_set is in matplotlib < 3.4
262
+ rcdict.pop(key, None)
263
+ rcdict.pop("examples.directory", None) # special case for matplotlib < 3.2
264
+ return rcdict
265
+
266
+
267
+ def _get_style_dict(style, filter=True):
268
+ """
269
+ Return a dictionary of settings belonging to the requested style(s). If `filter`
270
+ is ``True``, invalid style parameters like `backend` are filtered out.
271
+ """
272
+ # NOTE: This is adapted from matplotlib source for the following changes:
273
+ # 1. Add an 'original' pseudo style. Like rcParamsOrig except we also reload
274
+ # from the user matplotlibrc file.
275
+ # 2. When the style is changed we reset to the default state ignoring matplotlibrc.
276
+ # By contrast matplotlib applies styles on top of current state (including
277
+ # matplotlibrc changes and runtime rcParams changes) but the word 'style'
278
+ # implies a rigid static format. This makes more sense.
279
+ # 3. Add a separate function that returns lists of style dictionaries so that
280
+ # we can modify the active style in a context block. ultraplot context is more
281
+ # conservative than matplotlib's rc_context because it gets called a lot
282
+ # (e.g. every time you make an axes and every format() call). Instead of
283
+ # copying the entire rcParams dict we just track the keys that were changed.
284
+ style_aliases = {
285
+ "538": "fivethirtyeight",
286
+ "mpl20": "default",
287
+ "mpl15": "classic",
288
+ "original": mpl.matplotlib_fname(),
289
+ "seaborn": "seaborn-v0_8",
290
+ }
291
+
292
+ # Always apply the default style *first* so styles are rigid
293
+ kw_matplotlib = _get_default_style_dict()
294
+ if style == "default" or style is mpl.rcParamsDefault:
295
+ return kw_matplotlib
296
+
297
+ # Apply limited deviations from the matplotlib style that we want to propagate to
298
+ # other styles. Want users selecting stylesheets to have few surprises, so
299
+ # currently just enforce the new aesthetically pleasing fonts.
300
+ kw_matplotlib["font.family"] = "sans-serif"
301
+ for fmly in ("serif", "sans-serif", "monospace", "cursive", "fantasy"):
302
+ kw_matplotlib["font." + fmly] = rcsetup._rc_matplotlib_default["font." + fmly]
303
+
304
+ # Apply user input style(s) one by one
305
+ if isinstance(style, str) or isinstance(style, dict):
306
+ styles = [style]
307
+ else:
308
+ styles = style
309
+ for style in styles:
310
+ if isinstance(style, dict):
311
+ kw = style
312
+ elif isinstance(style, str):
313
+ style = style_aliases.get(style, style)
314
+ if style in mstyle.library:
315
+ kw = mstyle.library[style]
316
+ else:
317
+ try:
318
+ kw = mpl.rc_params_from_file(style, use_default_template=False)
319
+ except IOError:
320
+ raise IOError(
321
+ f"Style {style!r} not found in the style library and input "
322
+ "is not a valid URL or file path. Available styles are: "
323
+ + ", ".join(map(repr, mstyle.available))
324
+ + "."
325
+ )
326
+ else:
327
+ raise ValueError(f"Invalid style {style!r}. Must be string or dictionary.")
328
+ if filter:
329
+ kw = _filter_style_dict(kw, warn=True)
330
+ kw_matplotlib.update(kw)
331
+
332
+ return kw_matplotlib
333
+
334
+
335
+ def _infer_ultraplot_dict(kw_params):
336
+ """
337
+ Infer values for ultraplot's "added" parameters from stylesheet parameters.
338
+ """
339
+ kw_ultraplot = {}
340
+ mpl_to_ultraplot = {
341
+ "xtick.labelsize": (
342
+ "tick.labelsize",
343
+ "grid.labelsize",
344
+ ),
345
+ "ytick.labelsize": (
346
+ "tick.labelsize",
347
+ "grid.labelsize",
348
+ ),
349
+ "axes.titlesize": (
350
+ "abc.size",
351
+ "suptitle.size",
352
+ "title.size",
353
+ "leftlabel.size",
354
+ "rightlabel.size",
355
+ "toplabel.size",
356
+ "bottomlabel.size",
357
+ ),
358
+ "text.color": (
359
+ "abc.color",
360
+ "suptitle.color",
361
+ "title.color",
362
+ "tick.labelcolor",
363
+ "grid.labelcolor",
364
+ "leftlabel.color",
365
+ "rightlabel.color",
366
+ "toplabel.color",
367
+ "bottomlabel.color",
368
+ ),
369
+ }
370
+ for key, params in mpl_to_ultraplot.items():
371
+ if key in kw_params:
372
+ value = kw_params[key]
373
+ for param in params:
374
+ kw_ultraplot[param] = value
375
+ return kw_ultraplot
376
+
377
+
378
+ def config_inline_backend(fmt=None):
379
+ """
380
+ Set up the ipython `inline backend display format \
381
+ <https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-matplotlib>`__
382
+ and ensure that inline figures always look the same as saved figures.
383
+ This runs the following ipython magic commands:
384
+
385
+ .. code-block:: ipython
386
+
387
+ %config InlineBackend.figure_formats = rc['inlineformat']
388
+ %config InlineBackend.rc = {} # never override rc settings
389
+ %config InlineBackend.close_figures = True # cells start with no active figures
390
+ %config InlineBackend.print_figure_kwargs = {'bbox_inches': None}
391
+
392
+ When the inline backend is inactive or unavailable, this has no effect.
393
+ This function is called when you modify the :rcraw:`inlineformat` property.
394
+
395
+ Parameters
396
+ ----------
397
+ fmt : str or sequence, default: :rc:`inlineformat`
398
+ The inline backend file format or a list thereof. Valid formats
399
+ include ``'jpg'``, ``'png'``, ``'svg'``, ``'pdf'``, and ``'retina'``.
400
+
401
+ See also
402
+ --------
403
+ Configurator
404
+ """
405
+ # Note if inline backend is unavailable this will fail silently
406
+ ipython = get_ipython()
407
+ if ipython is None:
408
+ return
409
+ fmt = _not_none(fmt, rc_ultraplot["inlineformat"])
410
+ if isinstance(fmt, str):
411
+ fmt = [fmt]
412
+ elif np.iterable(fmt):
413
+ fmt = list(fmt)
414
+ else:
415
+ raise ValueError(f"Invalid inline backend format {fmt!r}. Must be string.")
416
+ ipython.magic("config InlineBackend.figure_formats = " + repr(fmt))
417
+ ipython.magic("config InlineBackend.rc = {}")
418
+ ipython.magic("config InlineBackend.close_figures = True")
419
+ ipython.magic("config InlineBackend.print_figure_kwargs = {'bbox_inches': None}")
420
+
421
+
422
+ def use_style(style):
423
+ """
424
+ Apply the `matplotlib style(s) \
425
+ <https://matplotlib.org/stable/tutorials/introductory/customizing.html>`__
426
+ with `matplotlib.style.use`. This function is
427
+ called when you modify the :rcraw:`style` property.
428
+
429
+ Parameters
430
+ ----------
431
+ style : str or sequence or dict-like
432
+ The matplotlib style name(s) or stylesheet filename(s), or dictionary(s)
433
+ of settings. Use ``'default'`` to apply matplotlib default settings and
434
+ ``'original'`` to include settings from your user ``matplotlibrc`` file.
435
+
436
+ See also
437
+ --------
438
+ Configurator
439
+ matplotlib.style.use
440
+ """
441
+ # NOTE: This function is not really necessary but makes ultraplot's
442
+ # stylesheet-supporting features obvious. Plus changing the style does
443
+ # so much *more* than changing rc params or quick settings, so it is
444
+ # nice to have dedicated function instead of just another rc_param name.
445
+ kw_matplotlib = _get_style_dict(style)
446
+ rc_matplotlib.update(kw_matplotlib)
447
+ rc_ultraplot.update(_infer_ultraplot_dict(kw_matplotlib))
448
+
449
+
450
+ @docstring._snippet_manager
451
+ def register_cmaps(*args, user=None, local=None, default=False):
452
+ """
453
+ Register named colormaps. This is called on import.
454
+
455
+ Parameters
456
+ ----------
457
+ %(rc.cmap_args)s
458
+
459
+ %(rc.cmap_exts)s
460
+
461
+ %(rc.cmap_params)s
462
+
463
+ See also
464
+ --------
465
+ register_cycles
466
+ register_colors
467
+ register_fonts
468
+ ultraplot.demos.show_cmaps
469
+ """
470
+ # Register input colormaps
471
+ from . import colors as pcolors
472
+
473
+ user = _not_none(user, not bool(args)) # skip user folder if input args passed
474
+ local = _not_none(local, not bool(args))
475
+ paths = []
476
+ for arg in args:
477
+ if isinstance(arg, mcolors.Colormap):
478
+ pcolors._cmap_database.register(arg, name=arg.name)
479
+ else:
480
+ paths.append(arg)
481
+
482
+ # Register data files
483
+ for i, path in _iter_data_objects(
484
+ "cmaps", *paths, user=user, local=local, default=default
485
+ ):
486
+ cmap = pcolors.ContinuousColormap.from_file(path, warn_on_failure=True)
487
+ if not cmap:
488
+ continue
489
+ if i == 0 and cmap.name.lower() in pcolors.CMAPS_CYCLIC:
490
+ cmap.set_cyclic(True)
491
+ pcolors._cmap_database.register(cmap, name=cmap.name)
492
+
493
+
494
+ @docstring._snippet_manager
495
+ def register_cycles(*args, user=None, local=None, default=False):
496
+ """
497
+ Register named color cycles. This is called on import.
498
+
499
+ Parameters
500
+ ----------
501
+ %(rc.cycle_args)s
502
+
503
+ %(rc.cycle_exts)s
504
+
505
+ %(rc.cycle_params)s
506
+
507
+ See also
508
+ --------
509
+ register_cmaps
510
+ register_colors
511
+ register_fonts
512
+ ultraplot.demos.show_cycles
513
+ """
514
+ # Register input color cycles
515
+ from . import colors as pcolors
516
+
517
+ user = _not_none(user, not bool(args)) # skip user folder if input args passed
518
+ local = _not_none(local, not bool(args))
519
+ paths = []
520
+ for arg in args:
521
+ if isinstance(arg, mcolors.Colormap):
522
+ pcolors._cmap_database.register(arg, name=arg.name)
523
+ else:
524
+ paths.append(arg)
525
+
526
+ # Register data files
527
+ for _, path in _iter_data_objects(
528
+ "cycles", *paths, user=user, local=local, default=default
529
+ ):
530
+ cmap = pcolors.DiscreteColormap.from_file(path, warn_on_failure=True)
531
+ if not cmap:
532
+ continue
533
+ pcolors._cmap_database.register(cmap, name=cmap.name)
534
+
535
+
536
+ @docstring._snippet_manager
537
+ def register_colors(
538
+ *args, user=None, local=None, default=False, space=None, margin=None, **kwargs
539
+ ):
540
+ """
541
+ Register named colors. This is called on import.
542
+
543
+ Parameters
544
+ ----------
545
+ %(rc.color_args)s
546
+ %(rc.color_params)s
547
+ space : {'hcl', 'hsl', 'hpl'}, optional
548
+ The colorspace used to pick "perceptually distinct" colors from
549
+ the `XKCD color survey <https://xkcd.com/color/rgb/>`__.
550
+ If passed then `default` is set to ``True``.
551
+ margin : float, default: 0.1
552
+ The margin used to pick "perceptually distinct" colors from the
553
+ `XKCD color survey <https://xkcd.com/color/rgb/>`__. The normalized hue,
554
+ saturation, and luminance of each color must differ from the channel
555
+ values of the prededing colors by `margin` in order to be registered.
556
+ Must fall between ``0`` and ``1`` (``0`` will register all colors).
557
+ If passed then `default` is set to ``True``.
558
+ **kwargs
559
+ Additional color name specifications passed as keyword arguments rather
560
+ than positional argument dictionaries.
561
+
562
+ See also
563
+ --------
564
+ register_cmaps
565
+ register_cycles
566
+ register_fonts
567
+ ultraplot.demos.show_colors
568
+ """
569
+ from . import colors as pcolors
570
+
571
+ default = default or space is not None or margin is not None
572
+ margin = _not_none(margin, 0.1)
573
+ space = _not_none(space, "hcl")
574
+
575
+ # Remove previously registered colors
576
+ # NOTE: Try not to touch matplotlib colors for compatibility
577
+ srcs = {"xkcd": pcolors.COLORS_XKCD, "opencolor": pcolors.COLORS_OPEN}
578
+ if default: # possibly slow but not these dicts are empty on startup
579
+ for src in srcs.values():
580
+ for key in src:
581
+ if key not in COLORS_KEEP:
582
+ pcolors._color_database.pop(key, None) # this also clears cache
583
+ src.clear()
584
+
585
+ # Register input colors
586
+ user = _not_none(user, not bool(args) and not bool(kwargs)) # skip if args passed
587
+ local = _not_none(local, not bool(args) and not bool(kwargs))
588
+ paths = []
589
+ for arg in args:
590
+ if isinstance(arg, dict):
591
+ kwargs.update(arg)
592
+ else:
593
+ paths.append(arg)
594
+ for key, color in kwargs.items():
595
+ if mcolors.is_color_like(color):
596
+ pcolors._color_database[key] = mcolors.to_rgba(color)
597
+ else:
598
+ raise ValueError(f"Invalid color {key}={color!r}.")
599
+
600
+ # Load colors from file and get their HCL values
601
+ # NOTE: Colors that come *later* overwrite colors that come earlier.
602
+ for i, path in _iter_data_objects(
603
+ "colors", *paths, user=user, local=local, default=default
604
+ ):
605
+ loaded = pcolors._load_colors(path, warn_on_failure=True)
606
+ if i == 0:
607
+ cat, _ = os.path.splitext(os.path.basename(path))
608
+ if cat not in srcs:
609
+ raise RuntimeError(f"Unknown ultraplot color database {path!r}.")
610
+ src = srcs[cat]
611
+ if cat == "xkcd":
612
+ for key in COLORS_KEEP:
613
+ loaded[key] = pcolors._color_database[key] # keep the same
614
+ loaded = pcolors._standardize_colors(loaded, space, margin)
615
+ src.clear()
616
+ src.update(loaded) # needed for demos.show_colors()
617
+ pcolors._color_database.update(loaded)
618
+
619
+
620
+ @docstring._snippet_manager
621
+ def register_fonts(*args, user=True, local=True, default=False):
622
+ """
623
+ Register font families. This is called on import.
624
+
625
+ Parameters
626
+ ----------
627
+ %(rc.font_args)s
628
+ %(rc.font_params)s
629
+
630
+ See also
631
+ --------
632
+ register_cmaps
633
+ register_cycles
634
+ register_colors
635
+ ultraplot.demos.show_fonts
636
+ """
637
+ # Find ultraplot fonts
638
+ # WARNING: Must search data files in reverse because font manager will
639
+ # not overwrite existing fonts with user-input fonts.
640
+ # WARNING: If you include a font file with an unrecognized style,
641
+ # matplotlib may use that font instead of the 'normal' one! Valid styles:
642
+ # 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman',
643
+ # 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black'
644
+ # https://matplotlib.org/api/font_manager_api.html
645
+ # For macOS the only fonts with 'Thin' in one of the .ttf file names
646
+ # are Helvetica Neue and .SF NS Display Condensed. Never try to use these!
647
+ paths_ultraplot = _get_data_folders(
648
+ "fonts", user=user, local=local, default=default, reverse=True
649
+ )
650
+ fnames_ultraplot = set(mfonts.findSystemFonts(paths_ultraplot))
651
+ for path in args:
652
+ path = os.path.expanduser(path)
653
+ if os.path.isfile(path):
654
+ fnames_ultraplot.add(path)
655
+ else:
656
+ raise FileNotFoundError(f"Invalid font file path {path!r}.")
657
+
658
+ # Detect user-input ttc fonts and issue warning
659
+ fnames_ultraplot_ttc = {
660
+ file for file in fnames_ultraplot if os.path.splitext(file)[1] == ".ttc"
661
+ }
662
+ if fnames_ultraplot_ttc:
663
+ warnings._warn_ultraplot(
664
+ "Ignoring the following .ttc fonts because they cannot be "
665
+ "saved into PDF or EPS files (see matplotlib issue #3135): "
666
+ + ", ".join(map(repr, sorted(fnames_ultraplot_ttc)))
667
+ + ". Please consider expanding them into separate .ttf files."
668
+ )
669
+
670
+ # Rebuild font cache only if necessary! Can be >50% of total import time!
671
+ fnames_all = {font.fname for font in mfonts.fontManager.ttflist}
672
+ fnames_ultraplot -= fnames_ultraplot_ttc
673
+ if not fnames_all >= fnames_ultraplot:
674
+ warnings._warn_ultraplot(
675
+ "Rebuilding font cache. This usually happens "
676
+ "after installing or updating ultraplot."
677
+ )
678
+ if hasattr(mfonts.fontManager, "addfont"):
679
+ # Newer API lets us add font files manually and deprecates TTFPATH. However
680
+ # to cache fonts added this way, we must call json_dump explicitly.
681
+ # NOTE: Previously, cache filename was specified as _fmcache variable, but
682
+ # recently became inaccessible. Must reproduce mpl code instead.
683
+ # NOTE: Older mpl versions used fontList.json as the cache, but these
684
+ # versions also did not have 'addfont', so makes no difference.
685
+ for fname in fnames_ultraplot:
686
+ mfonts.fontManager.addfont(fname)
687
+ cache = os.path.join(
688
+ mpl.get_cachedir(), f"fontlist-v{mfonts.FontManager.__version__}.json"
689
+ )
690
+ mfonts.json_dump(mfonts.fontManager, cache)
691
+ else:
692
+ # Older API requires us to modify TTFPATH
693
+ # NOTE: Previously we tried to modify TTFPATH before importing
694
+ # font manager with hope that it would load ultraplot fonts on
695
+ # initialization. But 99% of the time font manager just imports
696
+ # the FontManager from cache, so we would have to rebuild anyway.
697
+ paths = ":".join(paths_ultraplot)
698
+ if "TTFPATH" not in os.environ:
699
+ os.environ["TTFPATH"] = paths
700
+ elif paths not in os.environ["TTFPATH"]:
701
+ os.environ["TTFPATH"] += ":" + paths
702
+ mfonts._rebuild()
703
+
704
+ # Remove ttc files and 'Thin' fonts *after* rebuild
705
+ # NOTE: 'Thin' filter is ugly kludge but without this matplotlib picks up on
706
+ # Roboto thin ttf files installed on the RTD server when compiling docs.
707
+ mfonts.fontManager.ttflist = [
708
+ font
709
+ for font in mfonts.fontManager.ttflist
710
+ if os.path.splitext(font.fname)[1] != ".ttc"
711
+ and (_version_mpl >= "3.3" or "Thin" not in os.path.basename(font.fname))
712
+ ]
713
+
714
+
715
+ class Configurator(MutableMapping, dict):
716
+ """
717
+ A dictionary-like class for managing `matplotlib settings
718
+ <https://matplotlib.org/stable/tutorials/introductory/customizing.html>`__
719
+ stored in `rc_matplotlib` and :ref:`ultraplot settings <ug_rcultraplot>`
720
+ stored in `rc_ultraplot`. This class is instantiated as the `rc` object
721
+ on import. See the :ref:`user guide <ug_config>` for details.
722
+ """
723
+
724
+ def __repr__(self):
725
+ cls = type("rc", (dict,), {}) # temporary class with short name
726
+ src = cls({key: val for key, val in rc_ultraplot.items() if "." not in key})
727
+ return type(rc_matplotlib).__repr__(src).strip()[:-1] + ",\n ...\n })"
728
+
729
+ def __str__(self):
730
+ cls = type("rc", (dict,), {}) # temporary class with short name
731
+ src = cls({key: val for key, val in rc_ultraplot.items() if "." not in key})
732
+ return type(rc_matplotlib).__str__(src) + "\n..."
733
+
734
+ def __iter__(self):
735
+ yield from rc_ultraplot # sorted ultraplot settings, ignoring deprecations
736
+ yield from rc_matplotlib # sorted matplotlib settings, ignoring deprecations
737
+
738
+ def __len__(self):
739
+ return len(tuple(iter(self)))
740
+
741
+ def __delitem__(self, key): # noqa: U100
742
+ raise RuntimeError("rc settings cannot be deleted.")
743
+
744
+ def __delattr__(self, attr): # noqa: U100
745
+ raise RuntimeError("rc settings cannot be deleted.")
746
+
747
+ @docstring._snippet_manager
748
+ def __init__(self, local=True, user=True, default=True, **kwargs):
749
+ """
750
+ Parameters
751
+ ----------
752
+ %(rc.params)s
753
+ """
754
+ self._context = []
755
+ self._init(local=local, user=user, default=default, **kwargs)
756
+
757
+ def __getitem__(self, key):
758
+ """
759
+ Return an `rc_matplotlib` or `rc_ultraplot` setting using dictionary notation
760
+ (e.g., ``value = pplt.rc[name]``).
761
+ """
762
+ key, _ = self._validate_key(key) # might issue ultraplot removed/renamed error
763
+ try:
764
+ return rc_ultraplot[key]
765
+ except KeyError:
766
+ pass
767
+ return rc_matplotlib[key] # might issue matplotlib removed/renamed error
768
+
769
+ def __setitem__(self, key, value):
770
+ """
771
+ Modify an `rc_matplotlib` or `rc_ultraplot` setting using dictionary notation
772
+ (e.g., ``pplt.rc[name] = value``).
773
+ """
774
+ kw_ultraplot, kw_matplotlib = self._get_item_dicts(key, value)
775
+ rc_ultraplot.update(kw_ultraplot)
776
+ rc_matplotlib.update(kw_matplotlib)
777
+
778
+ def __getattr__(self, attr):
779
+ """
780
+ Return an `rc_matplotlib` or `rc_ultraplot` setting using "dot" notation
781
+ (e.g., ``value = pplt.rc.name``).
782
+ """
783
+ if attr[:1] == "_":
784
+ return super().__getattribute__(attr) # raise built-in error
785
+ else:
786
+ return self.__getitem__(attr)
787
+
788
+ def __setattr__(self, attr, value):
789
+ """
790
+ Modify an `rc_matplotlib` or `rc_ultraplot` setting using "dot" notation
791
+ (e.g., ``pplt.rc.name = value``).
792
+ """
793
+ if attr[:1] == "_":
794
+ super().__setattr__(attr, value)
795
+ else:
796
+ self.__setitem__(attr, value)
797
+
798
+ def __enter__(self):
799
+ """
800
+ Apply settings from the most recent context block.
801
+ """
802
+ if not self._context:
803
+ raise RuntimeError(
804
+ "rc object must be initialized for context block using rc.context()."
805
+ )
806
+ context = self._context[-1]
807
+ kwargs = context.kwargs
808
+ rc_new = context.rc_new # used for context-based _get_item_context
809
+ rc_old = context.rc_old # used to re-apply settings without copying whole dict
810
+ for key, value in kwargs.items():
811
+ kw_ultraplot, kw_matplotlib = self._get_item_dicts(key, value)
812
+ for rc_dict, kw_new in zip(
813
+ (rc_ultraplot, rc_matplotlib),
814
+ (kw_ultraplot, kw_matplotlib),
815
+ ):
816
+ for key, value in kw_new.items():
817
+ rc_old[key] = rc_dict[key]
818
+ rc_new[key] = rc_dict[key] = value
819
+
820
+ def __exit__(self, *args): # noqa: U100
821
+ """
822
+ Restore settings from the most recent context block.
823
+ """
824
+ if not self._context:
825
+ raise RuntimeError(
826
+ "rc object must be initialized for context block using rc.context()."
827
+ )
828
+ context = self._context[-1]
829
+ for key, value in context.rc_old.items():
830
+ kw_ultraplot, kw_matplotlib = self._get_item_dicts(key, value)
831
+ rc_ultraplot.update(kw_ultraplot)
832
+ rc_matplotlib.update(kw_matplotlib)
833
+ del self._context[-1]
834
+
835
+ def _init(self, *, local, user, default, skip_cycle=False):
836
+ """
837
+ Initialize the configurator.
838
+ """
839
+ # Always remove context objects
840
+ self._context.clear()
841
+
842
+ # Update from default settings
843
+ # NOTE: see _remove_blacklisted_style_params bugfix
844
+ if default:
845
+ rc_matplotlib.update(_get_style_dict("original", filter=False))
846
+ rc_matplotlib.update(rcsetup._rc_matplotlib_default)
847
+ rc_ultraplot.update(rcsetup._rc_ultraplot_default)
848
+ for key, value in rc_ultraplot.items():
849
+ kw_ultraplot, kw_matplotlib = self._get_item_dicts(
850
+ key, value, skip_cycle=skip_cycle
851
+ )
852
+ rc_matplotlib.update(kw_matplotlib)
853
+ rc_ultraplot.update(kw_ultraplot)
854
+
855
+ # Update from user home
856
+ user_path = None
857
+ if user:
858
+ user_path = self.user_file()
859
+ if os.path.isfile(user_path):
860
+ self.load(user_path)
861
+
862
+ # Update from local paths
863
+ if local:
864
+ local_paths = self.local_files()
865
+ for path in local_paths:
866
+ if path == user_path: # local files always have precedence
867
+ continue
868
+ self.load(path)
869
+
870
+ @staticmethod
871
+ def _validate_key(key, value=None):
872
+ """
873
+ Validate setting names and handle `rc_ultraplot` deprecations.
874
+ """
875
+ # NOTE: Not necessary to check matplotlib key here because... not sure why.
876
+ # Think deprecated matplotlib keys are not involved in any synced settings.
877
+ # Also note _check_key includes special handling for some renamed keys.
878
+ if not isinstance(key, str):
879
+ raise KeyError(f"Invalid key {key!r}. Must be string.")
880
+ key = key.lower()
881
+ if "." not in key:
882
+ key = rcsetup._rc_nodots.get(key, key)
883
+ key, value = rc_ultraplot._check_key(key, value) # may issue deprecation warning
884
+ return key, value
885
+
886
+ @staticmethod
887
+ def _validate_value(key, value):
888
+ """
889
+ Validate setting values and convert numpy ndarray to list if possible.
890
+ """
891
+ # NOTE: Ideally would implicitly validate on subsequent assignment to rc
892
+ # dictionaries, but must explicitly do it here, so _get_item_dicts can
893
+ # work with e.g. 'tick.lenratio', so _get_item_dicts does not have to include
894
+ # deprecated name handling in its if statements, and so _load_file can
895
+ # catch errors and emit warnings with line number indications as files
896
+ # are being read rather than after the end of the file reading.
897
+ if isinstance(value, np.ndarray):
898
+ value = value.item() if value.size == 1 else value.tolist()
899
+ validate_matplotlib = getattr(rc_matplotlib, "validate", None)
900
+ validate_ultraplot = rc_ultraplot._validate
901
+ if validate_matplotlib is not None and key in validate_matplotlib:
902
+ value = validate_matplotlib[key](value)
903
+ elif key in validate_ultraplot:
904
+ value = validate_ultraplot[key](value)
905
+ return value
906
+
907
+ def _get_item_context(self, key, mode=None):
908
+ """
909
+ As with `~Configurator.__getitem__` but the search is limited based
910
+ on the context mode and ``None`` is returned if the key is not found.
911
+ """
912
+ key, _ = self._validate_key(key)
913
+ if mode is None:
914
+ mode = self._context_mode
915
+ cache = tuple(context.rc_new for context in self._context)
916
+ if mode == 0:
917
+ rcdicts = (*cache, rc_ultraplot, rc_matplotlib)
918
+ elif mode == 1:
919
+ rcdicts = (*cache, rc_ultraplot) # added settings only!
920
+ elif mode == 2:
921
+ rcdicts = (*cache,)
922
+ else:
923
+ raise ValueError(f"Invalid caching mode {mode!r}.")
924
+ for rcdict in rcdicts:
925
+ if not rcdict:
926
+ continue
927
+ try:
928
+ return rcdict[key]
929
+ except KeyError:
930
+ continue
931
+ if mode == 0: # otherwise return None
932
+ raise KeyError(f"Invalid rc setting {key!r}.")
933
+
934
+ def _get_item_dicts(self, key, value, skip_cycle=False):
935
+ """
936
+ Return dictionaries for updating the `rc_ultraplot` and `rc_matplotlib`
937
+ properties associated with this key. Used when setting items, entering
938
+ context blocks, or loading files.
939
+ """
940
+ # Get validated key, value, and child keys
941
+ key, value = self._validate_key(key, value)
942
+ value = self._validate_value(key, value)
943
+ keys = (key,) + rcsetup._rc_children.get(key, ()) # settings to change
944
+ contains = lambda *args: any(arg in keys for arg in args) # noqa: E731
945
+
946
+ # Fill dictionaries of matplotlib and ultraplot settings
947
+ # NOTE: Raise key error right away so it can be caught by _load_file().
948
+ # Also ignore deprecation warnings so we only get them *once* on assignment
949
+ kw_ultraplot = {} # custom properties
950
+ kw_matplotlib = {} # builtin properties
951
+ with warnings.catch_warnings():
952
+ warnings.simplefilter("ignore", mpl.MatplotlibDeprecationWarning)
953
+ warnings.simplefilter("ignore", warnings.UltraplotWarning)
954
+ for key in keys:
955
+ if key in rc_matplotlib:
956
+ kw_matplotlib[key] = value
957
+ elif key in rc_ultraplot:
958
+ kw_ultraplot[key] = value
959
+ else:
960
+ raise KeyError(f"Invalid rc setting {key!r}.")
961
+
962
+ # Special key: configure inline backend
963
+ if contains("inlineformat"):
964
+ config_inline_backend(value)
965
+
966
+ # Special key: apply stylesheet
967
+ elif contains("style"):
968
+ if value is not None:
969
+ ikw_matplotlib = _get_style_dict(value)
970
+ kw_matplotlib.update(ikw_matplotlib)
971
+ kw_ultraplot.update(_infer_ultraplot_dict(ikw_matplotlib))
972
+
973
+ # Cycler
974
+ # NOTE: Have to skip this step during initial ultraplot import
975
+ elif contains("cycle") and not skip_cycle:
976
+ from .colors import _get_cmap_subtype
977
+
978
+ cmap = _get_cmap_subtype(value, "discrete")
979
+ kw_matplotlib["axes.prop_cycle"] = cycler.cycler("color", cmap.colors)
980
+ kw_matplotlib["patch.facecolor"] = "C0"
981
+
982
+ # Turning bounding box on should turn border off and vice versa
983
+ elif contains("abc.bbox", "title.bbox", "abc.border", "title.border"):
984
+ if value:
985
+ name, this = key.split(".")
986
+ other = "border" if this == "bbox" else "bbox"
987
+ kw_ultraplot[name + "." + other] = False
988
+
989
+ # Fontsize
990
+ # NOTE: Re-application of e.g. size='small' uses the updated 'font.size'
991
+ elif contains("font.size"):
992
+ kw_ultraplot.update(
993
+ {
994
+ key: value
995
+ for key, value in rc_ultraplot.items()
996
+ if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings
997
+ }
998
+ )
999
+ kw_matplotlib.update(
1000
+ {
1001
+ key: value
1002
+ for key, value in rc_matplotlib.items()
1003
+ if key in rcsetup.FONT_KEYS and value in mfonts.font_scalings
1004
+ }
1005
+ )
1006
+
1007
+ # Tick length/major-minor tick length ratio
1008
+ elif contains("tick.len", "tick.lenratio"):
1009
+ if contains("tick.len"):
1010
+ ticklen = value
1011
+ ratio = rc_ultraplot["tick.lenratio"]
1012
+ else:
1013
+ ticklen = rc_ultraplot["tick.len"]
1014
+ ratio = value
1015
+ kw_matplotlib["xtick.minor.size"] = ticklen * ratio
1016
+ kw_matplotlib["ytick.minor.size"] = ticklen * ratio
1017
+
1018
+ # Spine width/major-minor tick width ratio
1019
+ elif contains("tick.width", "tick.widthratio"):
1020
+ if contains("tick.width"):
1021
+ tickwidth = value
1022
+ ratio = rc_ultraplot["tick.widthratio"]
1023
+ else:
1024
+ tickwidth = rc_ultraplot["tick.width"]
1025
+ ratio = value
1026
+ kw_matplotlib["xtick.minor.width"] = tickwidth * ratio
1027
+ kw_matplotlib["ytick.minor.width"] = tickwidth * ratio
1028
+
1029
+ # Gridline width
1030
+ elif contains("grid.width", "grid.widthratio"):
1031
+ if contains("grid.width"):
1032
+ gridwidth = value
1033
+ ratio = rc_ultraplot["grid.widthratio"]
1034
+ else:
1035
+ gridwidth = rc_ultraplot["grid.width"]
1036
+ ratio = value
1037
+ kw_ultraplot["gridminor.linewidth"] = gridwidth * ratio
1038
+ kw_ultraplot["gridminor.width"] = gridwidth * ratio
1039
+
1040
+ # Gridline toggling
1041
+ elif contains("grid", "gridminor"):
1042
+ b, which = _translate_grid(
1043
+ value, "gridminor" if contains("gridminor") else "grid"
1044
+ )
1045
+ kw_matplotlib["axes.grid"] = b
1046
+ kw_matplotlib["axes.grid.which"] = which
1047
+
1048
+ return kw_ultraplot, kw_matplotlib
1049
+
1050
+ @staticmethod
1051
+ def _get_axisbelow_zorder(axisbelow):
1052
+ """
1053
+ Convert the `axisbelow` string to its corresponding `zorder`.
1054
+ """
1055
+ if axisbelow is True:
1056
+ zorder = 0.5
1057
+ elif axisbelow is False:
1058
+ zorder = 2.5
1059
+ elif axisbelow in ("line", "lines"):
1060
+ zorder = 1.5
1061
+ else:
1062
+ raise ValueError(f"Unexpected axisbelow value {axisbelow!r}.")
1063
+ return zorder
1064
+
1065
+ def _get_background_props(self, patch_kw=None, native=True, **kwargs):
1066
+ """
1067
+ Return background properties, optionally filtering the output dictionary
1068
+ based on the context.
1069
+ """
1070
+ # Deprecated behavior
1071
+ context = native or self._context_mode == 2
1072
+ if patch_kw:
1073
+ warnings._warn_ultraplot(
1074
+ "'patch_kw' is no longer necessary as of ultraplot v0.8. "
1075
+ "Pass the parameters as keyword arguments instead."
1076
+ )
1077
+ kwargs.update(patch_kw)
1078
+
1079
+ # Get user-input properties and changed rc settings
1080
+ # NOTE: Here we use 'color' as an alias for just 'edgecolor' rather than
1081
+ # both 'edgecolor' and 'facecolor' to match 'xcolor' and 'ycolor' arguments.
1082
+ props = _pop_props(kwargs, "patch")
1083
+ if "color" in props:
1084
+ props.setdefault("edgecolor", props.pop("color"))
1085
+ for key in ("alpha", "facecolor", "linewidth", "edgecolor"):
1086
+ value = self.find("axes." + key, context=context)
1087
+ if value is not None:
1088
+ props.setdefault(key, value)
1089
+
1090
+ # Partition properties into face and edge
1091
+ kw_face = _pop_kwargs(props, "alpha", "facecolor")
1092
+ kw_edge = _pop_kwargs(props, "edgecolor", "linewidth", "linestyle")
1093
+ kw_edge["capstyle"] = "projecting" # NOTE: needed to fix cartopy bounds
1094
+ if "color" in props:
1095
+ kw_edge.setdefault("edgecolor", props.pop("color"))
1096
+ if kwargs:
1097
+ raise TypeError(f"Unexpected keyword argument(s): {kwargs!r}")
1098
+
1099
+ return kw_face, kw_edge
1100
+
1101
+ def _get_gridline_bool(self, grid=None, axis=None, which="major", native=True):
1102
+ """
1103
+ Return major and minor gridline toggles from ``axes.grid``, ``axes.grid.which``,
1104
+ and ``axes.grid.axis``, optionally returning `None` based on the context.
1105
+ """
1106
+ # NOTE: If you pass 'grid' or 'gridminor' the native args are updated
1107
+ # NOTE: Very careful to return not None only if setting was changed.
1108
+ # Avoid unnecessarily triggering grid redraws (esp. bad for geo.py)
1109
+ context = native or self._context_mode == 2
1110
+ grid_on = self.find("axes.grid", context=context)
1111
+ which_on = self.find("axes.grid.which", context=context)
1112
+ if grid_on is not None or which_on is not None: # if *one* was changed
1113
+ axis_on = self["axes.grid.axis"] # always need this property
1114
+ grid_on = _not_none(grid_on, self["axes.grid"])
1115
+ which_on = _not_none(which_on, self["axes.grid.which"])
1116
+ axis = _not_none(axis, "x")
1117
+ axis_on = axis is None or axis_on in (axis, "both")
1118
+ which_on = which_on in (which, "both")
1119
+ grid = _not_none(grid, grid_on and axis_on and which_on)
1120
+ return grid
1121
+
1122
+ def _get_gridline_props(self, which="major", native=True, rebuild=False):
1123
+ """
1124
+ Return gridline properties, optionally filtering the output dictionary
1125
+ based on the context.
1126
+ """
1127
+ # Line properties
1128
+ # NOTE: Gridline zorder is controlled automatically by matplotlib but
1129
+ # must be controlled manually for geographic projections
1130
+ key = "grid" if which == "major" else "gridminor"
1131
+ prefix = "grid_" if native else "" # for native gridlines use this prefix
1132
+ context = not rebuild and (native or self._context_mode == 2)
1133
+ kwlines = self.fill(
1134
+ {
1135
+ f"{prefix}alpha": f"{key}.alpha",
1136
+ f"{prefix}color": f"{key}.color",
1137
+ f"{prefix}linewidth": f"{key}.linewidth",
1138
+ f"{prefix}linestyle": f"{key}.linestyle",
1139
+ },
1140
+ context=context,
1141
+ )
1142
+ axisbelow = self.find("axes.axisbelow", context=context)
1143
+ if axisbelow is not None:
1144
+ if native: # this is a native plot so use set_axisbelow() down the line
1145
+ kwlines["axisbelow"] = axisbelow
1146
+ else: # this is a geographic plot so apply with zorder
1147
+ kwlines["zorder"] = self._get_axisbelow_zorder(axisbelow)
1148
+ return kwlines
1149
+
1150
+ def _get_label_props(self, native=True, **kwargs):
1151
+ """
1152
+ Return the axis label properties, optionally filtering the output dictionary
1153
+ based on the context.
1154
+ """
1155
+ # Get the label settings
1156
+ # NOTE: This permits passing arbitrary additional args to set_[xy]label()
1157
+ context = native or self._context_mode == 2
1158
+ kw = self.fill(
1159
+ {
1160
+ "color": "axes.labelcolor",
1161
+ "weight": "axes.labelweight",
1162
+ "size": "axes.labelsize",
1163
+ "family": "font.family",
1164
+ "labelpad": "axes.labelpad", # read by set_xlabel/set_ylabel
1165
+ },
1166
+ context=context,
1167
+ )
1168
+ for key, value in kwargs.items():
1169
+ if value is not None: # allow e.g. color=None
1170
+ kw[key] = value
1171
+ return kw
1172
+
1173
+ def _get_loc_string(self, string, axis=None, native=True):
1174
+ """
1175
+ Return `tickloc` and `spineloc` location strings from the `rc` boolean toggles,
1176
+ optionally returning `None` based on the context.
1177
+ """
1178
+ context = native or self._context_mode == 2
1179
+ axis = _not_none(axis, "x")
1180
+ opt1, opt2 = ("top", "bottom") if axis == "x" else ("left", "right")
1181
+ b1 = self.find(f"{string}.{opt1}", context=context)
1182
+ b2 = self.find(f"{string}.{opt2}", context=context)
1183
+ if b1 is None and b2 is None:
1184
+ return None
1185
+ elif b1 and b2:
1186
+ return "both"
1187
+ elif b1:
1188
+ return opt1
1189
+ elif b2:
1190
+ return opt2
1191
+ else:
1192
+ return "neither"
1193
+
1194
+ def _get_tickline_props(self, axis=None, which="major", native=True, rebuild=False):
1195
+ """
1196
+ Return the tick line properties, optionally filtering the output dictionary
1197
+ based on the context.
1198
+ """
1199
+ # Tick properties obtained with rc.category
1200
+ # NOTE: This loads 'size', 'width', 'pad', 'bottom', and 'top'
1201
+ axis = _not_none(axis, "x")
1202
+ context = not rebuild and (native or self._context_mode == 2)
1203
+ kwticks = self.category(f"{axis}tick.{which}", context=context)
1204
+ kwticks.pop("visible", None)
1205
+ for key in ("color", "direction"):
1206
+ value = self.find(f"{axis}tick.{key}", context=context)
1207
+ if value is not None:
1208
+ kwticks[key] = value
1209
+ return kwticks
1210
+
1211
+ def _get_ticklabel_props(self, axis=None, native=True, rebuild=False):
1212
+ """
1213
+ Return the tick label properties, optionally filtering the output dictionary
1214
+ based on the context.
1215
+ """
1216
+ # NOTE: 'tick.label' properties are now synonyms of 'grid.label' properties
1217
+ sprefix = axis or ""
1218
+ cprefix = sprefix if _version_mpl >= "3.4" else "" # new settings
1219
+ context = not rebuild and (native or self._context_mode == 2)
1220
+ kwtext = self.fill(
1221
+ {
1222
+ "color": f"{cprefix}tick.labelcolor", # native setting sometimes avail
1223
+ "size": f"{sprefix}tick.labelsize", # native setting always avail
1224
+ "weight": "tick.labelweight", # native setting never avail
1225
+ "family": "font.family", # apply manually
1226
+ },
1227
+ context=context,
1228
+ )
1229
+ if kwtext.get("color", None) == "inherit":
1230
+ # Inheritence is not automatic for geographic
1231
+ # gridline labels so we apply inheritence here.
1232
+ kwtext["color"] = self[f"{sprefix}tick.color"]
1233
+ return kwtext
1234
+
1235
+ @staticmethod
1236
+ def local_files():
1237
+ """
1238
+ Return locations of files named ``ultraplotrc`` in this directory and in parent
1239
+ directories. "Hidden" files with a leading dot are also recognized. These are
1240
+ automatically loaded when ultraplot is imported.
1241
+
1242
+ See also
1243
+ --------
1244
+ Configurator.user_file
1245
+ Configurator.local_folders
1246
+ """
1247
+ cdir = os.getcwd()
1248
+ paths = []
1249
+ while cdir: # i.e. not root
1250
+ for name in ("ultraplotrc", ".ultraplotrc"):
1251
+ path = os.path.join(cdir, name)
1252
+ if os.path.isfile(path):
1253
+ paths.append(path)
1254
+ ndir = os.path.dirname(cdir)
1255
+ if ndir == cdir: # root
1256
+ break
1257
+ cdir = ndir
1258
+ return paths[::-1] # sort from decreasing to increasing importantce
1259
+
1260
+ @staticmethod
1261
+ def local_folders(subfolder=None):
1262
+ """
1263
+ Return locations of folders named ``ultraplot_cmaps``, ``ultraplot_cycles``,
1264
+ ``ultraplot_colors``, and ``ultraplot_fonts`` in this directory and in parent
1265
+ directories. "Hidden" folders with a leading dot are also recognized. Files
1266
+ in these directories are automatically loaded when ultraplot is imported.
1267
+
1268
+ See also
1269
+ --------
1270
+ Configurator.user_folder
1271
+ Configurator.local_files
1272
+ """
1273
+ cdir = os.getcwd()
1274
+ paths = []
1275
+ if subfolder is None:
1276
+ subfolder = ("cmaps", "cycles", "colors", "fonts")
1277
+ if isinstance(subfolder, str):
1278
+ subfolder = (subfolder,)
1279
+ while cdir: # i.e. not root
1280
+ for prefix in ("ultraplot", ".ultraplot"):
1281
+ for suffix in subfolder:
1282
+ path = os.path.join(cdir, "_".join((prefix, suffix)))
1283
+ if os.path.isdir(path):
1284
+ paths.append(path)
1285
+ ndir = os.path.dirname(cdir)
1286
+ if ndir == cdir: # root
1287
+ break
1288
+ cdir = ndir
1289
+ return paths[::-1]
1290
+
1291
+ @staticmethod
1292
+ def _config_folder():
1293
+ """
1294
+ Get the XDG ultraplot folder.
1295
+ """
1296
+ home = os.path.expanduser("~")
1297
+ base = os.environ.get("XDG_CONFIG_HOME")
1298
+ if not base:
1299
+ base = os.path.join(home, ".config")
1300
+ if sys.platform.startswith(("linux", "freebsd")) and os.path.isdir(base):
1301
+ configdir = os.path.join(base, "ultraplot")
1302
+ else:
1303
+ configdir = os.path.join(home, ".ultraplot")
1304
+ return configdir
1305
+
1306
+ @staticmethod
1307
+ def user_file():
1308
+ """
1309
+ Return location of the default ultraplotrc file. On Linux, this is either
1310
+ ``$XDG_CONFIG_HOME/ultraplot/ultraplotrc`` or ``~/.config/ultraplot/ultraplotrc``
1311
+ if the `XDG directory <https://wiki.archlinux.org/title/XDG_Base_Directory>`__
1312
+ is unset. On other operating systems, this is ``~/.ultraplot/ultraplotrc``. The
1313
+ location ``~/.ultraplotrc`` or ``~/.ultraplot/ultraplotrc`` is always returned if the
1314
+ file exists, regardless of the operating system. If multiple valid locations
1315
+ are found, a warning is raised.
1316
+
1317
+ See also
1318
+ --------
1319
+ Configurator.user_folder
1320
+ Configurator.local_files
1321
+ """
1322
+ # Support both loose files and files inside .ultraplot
1323
+ file = os.path.join(Configurator.user_folder(), "ultraplotrc")
1324
+ universal = os.path.join(os.path.expanduser("~"), ".ultraplotrc")
1325
+ if os.path.isfile(universal):
1326
+ if file != universal and os.path.isfile(file):
1327
+ warnings._warn_ultraplot(
1328
+ "Found conflicting default user ultraplotrc files at "
1329
+ f"{universal!r} and {file!r}. Ignoring the second one."
1330
+ )
1331
+ file = universal
1332
+ return file
1333
+
1334
+ @staticmethod
1335
+ def user_folder(subfolder=None):
1336
+ """
1337
+ Return location of the default ultraplot folder. On Linux, this
1338
+ is either ``$XDG_CONFIG_HOME/ultraplot`` or ``~/.config/ultraplot``
1339
+ if the `XDG directory <https://wiki.archlinux.org/title/XDG_Base_Directory>`__
1340
+ is unset. On other operating systems, this is ``~/.ultraplot``. The location
1341
+ ``~/.ultraplot`` is always returned if the folder exists, regardless of the
1342
+ operating system. If multiple valid locations are found, a warning is raised.
1343
+
1344
+ See also
1345
+ --------
1346
+ Configurator.user_file
1347
+ Configurator.local_folders
1348
+ """
1349
+ # Try the XDG standard location
1350
+ # NOTE: This is borrowed from matplotlib.get_configdir
1351
+ home = os.path.expanduser("~")
1352
+ universal = folder = os.path.join(home, ".ultraplot")
1353
+ if sys.platform.startswith(("linux", "freebsd")):
1354
+ xdg = os.environ.get("XDG_CONFIG_HOME")
1355
+ xdg = xdg or os.path.join(home, ".config")
1356
+ folder = os.path.join(xdg, "ultraplot")
1357
+ # Fallback to the loose ~/.ultraplot if it is present
1358
+ # NOTE: This is critical or we might ignore previously stored settings!
1359
+ if os.path.isdir(universal):
1360
+ if folder != universal and os.path.isdir(folder):
1361
+ warnings._warn_ultraplot(
1362
+ "Found conflicting default user ultraplot folders at "
1363
+ f"{universal!r} and {folder!r}. Ignoring the second one."
1364
+ )
1365
+ folder = universal
1366
+ # Return the folder
1367
+ if subfolder:
1368
+ folder = os.path.join(folder, subfolder)
1369
+ return folder
1370
+
1371
+ def context(self, *args, mode=0, file=None, **kwargs):
1372
+ """
1373
+ Temporarily modify the rc settings in a "with as" block.
1374
+
1375
+ Parameters
1376
+ ----------
1377
+ *args
1378
+ Dictionaries of `rc` keys and values.
1379
+ file : path-like, optional
1380
+ Filename from which settings should be loaded.
1381
+ **kwargs
1382
+ `rc` names and values passed as keyword arguments.
1383
+ If the name has dots, simply omit them.
1384
+
1385
+ Other parameters
1386
+ ----------------
1387
+ mode : {0, 1, 2}, optional
1388
+ The context mode. Dictates the behavior of `~Configurator.find`,
1389
+ `~Configurator.fill`, and `~Configurator.category` within a
1390
+ "with as" block when called with ``context=True``.
1391
+
1392
+ The options are as follows:
1393
+
1394
+ * ``mode=0``: Matplotlib's `rc_matplotlib` settings
1395
+ and ultraplot's `rc_ultraplot` settings are all returned,
1396
+ whether or not they are local to the "with as" block.
1397
+ * ``mode=1``: Matplotlib's `rc_matplotlib` settings are only
1398
+ returned if they are local to the "with as" block. For example,
1399
+ if :rcraw:`axes.titlesize` was passed to `~Configurator.context`,
1400
+ then ``pplt.rc.find('axes.titlesize', context=True)`` will return
1401
+ this value, but ``pplt.rc.find('axes.titleweight', context=True)`` will
1402
+ return ``None``. This is used internally when instantiating axes.
1403
+ * ``mode=2``: Matplotlib's `rc_matplotlib` settings and ultraplot's
1404
+ `rc_ultraplot` settings are only returned if they are local to the
1405
+ "with as" block. This is used internally when formatting axes.
1406
+
1407
+ Note
1408
+ ----
1409
+ Context "modes" are primarily used internally but may also be useful for power
1410
+ users. Mode ``1`` is used when `~ultraplot.axes.Axes.format` is called during
1411
+ axes instantiation, and mode ``2`` is used when `~ultraplot.axes.Axes.format`
1412
+ is manually called by users. The latter prevents successive calls to
1413
+ `~ultraplot.axes.Axes.format` from constantly looking up and re-applying
1414
+ unchanged settings and significantly increasing the runtime.
1415
+
1416
+ Example
1417
+ -------
1418
+ The below applies settings to axes in a specific figure using
1419
+ `~Configurator.context`.
1420
+
1421
+ >>> import ultraplot as pplt
1422
+ >>> with pplt.rc.context(ticklen=5, metalinewidth=2):
1423
+ >>> fig, ax = pplt.subplots()
1424
+ >>> ax.plot(data)
1425
+
1426
+ The below applies settings to a specific axes using
1427
+ `~ultraplot.axes.Axes.format`, which uses `~Configurator.context`
1428
+ internally.
1429
+
1430
+ >>> import ultraplot as pplt
1431
+ >>> fig, ax = pplt.subplots()
1432
+ >>> ax.format(ticklen=5, metalinewidth=2)
1433
+ """
1434
+ # Add input dictionaries
1435
+ for arg in args:
1436
+ if not isinstance(arg, dict):
1437
+ raise ValueError(f"Non-dictionary argument {arg!r}.")
1438
+ kwargs.update(arg)
1439
+
1440
+ # Add settings from file
1441
+ if file is not None:
1442
+ kw = self._load_file(file)
1443
+ kw = {key: value for key, value in kw.items() if key not in kwargs}
1444
+ kwargs.update(kw)
1445
+
1446
+ # Activate context object
1447
+ if mode not in range(3):
1448
+ raise ValueError(f"Invalid mode {mode!r}.")
1449
+ cls = namedtuple("RcContext", ("mode", "kwargs", "rc_new", "rc_old"))
1450
+ context = cls(mode=mode, kwargs=kwargs, rc_new={}, rc_old={})
1451
+ self._context.append(context)
1452
+ return self
1453
+
1454
+ def category(self, cat, *, trimcat=True, context=False):
1455
+ """
1456
+ Return a dictionary of settings beginning with the substring ``cat + '.'``.
1457
+ Optionally limit the search to the context level.
1458
+
1459
+ Parameters
1460
+ ----------
1461
+ cat : str, optional
1462
+ The `rc` setting category.
1463
+ trimcat : bool, default: True
1464
+ Whether to trim ``cat`` from the key names in the output dictionary.
1465
+ context : bool, default: False
1466
+ If ``True``, then settings not found in the context dictionaries
1467
+ are omitted from the output dictionary. See `~Configurator.context`.
1468
+
1469
+ See also
1470
+ --------
1471
+ Configurator.find
1472
+ Configurator.fill
1473
+ """
1474
+ kw = {}
1475
+ if cat not in rcsetup._rc_categories:
1476
+ raise ValueError(
1477
+ f"Invalid rc category {cat!r}. Valid categories are: "
1478
+ + ", ".join(map(repr, rcsetup._rc_categories))
1479
+ + "."
1480
+ )
1481
+ for key in self:
1482
+ if not re.match(rf"\A{cat}\.[^.]+\Z", key):
1483
+ continue
1484
+ value = self._get_item_context(key, None if context else 0)
1485
+ if value is None:
1486
+ continue
1487
+ if trimcat:
1488
+ key = re.sub(rf"\A{cat}\.", "", key)
1489
+ kw[key] = value
1490
+ return kw
1491
+
1492
+ def fill(self, props, *, context=False):
1493
+ """
1494
+ Return a dictionary filled with settings whose names match the string values
1495
+ in the input dictionary. Optionally limit the search to the context level.
1496
+
1497
+ Parameters
1498
+ ----------
1499
+ props : dict-like
1500
+ Dictionary whose values are setting names -- for example
1501
+ ``rc.fill({'edgecolor': 'axes.edgecolor', 'facecolor': 'axes.facecolor'})``.
1502
+ context : bool, default: False
1503
+ If ``True``, then settings not found in the context dictionaries
1504
+ are omitted from the output dictionary. See `~Configurator.context`.
1505
+
1506
+ See also
1507
+ --------
1508
+ Configurator.category
1509
+ Configurator.find
1510
+ """
1511
+ kw = {}
1512
+ for key, value in props.items():
1513
+ item = self._get_item_context(value, None if context else 0)
1514
+ if item is not None:
1515
+ kw[key] = item
1516
+ return kw
1517
+
1518
+ def find(self, key, *, context=False):
1519
+ """
1520
+ Return a single setting. Optionally limit the search to the context level.
1521
+
1522
+ Parameters
1523
+ ----------
1524
+ key : str
1525
+ The single setting name.
1526
+ context : bool, default: False
1527
+ If ``True``, then ``None`` is returned if the setting is not found
1528
+ in the context dictionaries. See `~Configurator.context`.
1529
+
1530
+ See also
1531
+ --------
1532
+ Configurator.category
1533
+ Configurator.fill
1534
+ """
1535
+ return self._get_item_context(key, None if context else 0)
1536
+
1537
+ def update(self, *args, **kwargs):
1538
+ """
1539
+ Update several settings at once.
1540
+
1541
+ Parameters
1542
+ ----------
1543
+ *args : str or dict-like, optional
1544
+ A dictionary containing `rc` keys and values. You can also pass
1545
+ a "category" name as the first argument, in which case all
1546
+ settings are prepended with ``'category.'``. For example,
1547
+ ``rc.update('axes', labelsize=20, titlesize=20)`` changes the
1548
+ :rcraw:`axes.labelsize` and :rcraw:`axes.titlesize` settings.
1549
+ **kwargs
1550
+ `rc` keys and values passed as keyword arguments.
1551
+ If the name has dots, simply omit them.
1552
+
1553
+ See also
1554
+ --------
1555
+ Configurator.category
1556
+ Configurator.fill
1557
+ """
1558
+ prefix, kw = "", {}
1559
+ if not args:
1560
+ pass
1561
+ elif len(args) == 1 and isinstance(args[0], str):
1562
+ prefix = args[0]
1563
+ elif len(args) == 1 and isinstance(args[0], dict):
1564
+ kw = args[0]
1565
+ elif len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], dict):
1566
+ prefix, kw = args
1567
+ else:
1568
+ raise ValueError(
1569
+ f"Invalid arguments {args!r}. Usage is either "
1570
+ "rc.update(dict), rc.update(kwy=value, ...), "
1571
+ "rc.update(category, dict), or rc.update(category, key=value, ...)."
1572
+ )
1573
+ prefix = prefix and prefix + "."
1574
+ kw.update(kwargs)
1575
+ for key, value in kw.items():
1576
+ self.__setitem__(prefix + key, value)
1577
+
1578
+ @docstring._snippet_manager
1579
+ def reset(self, local=True, user=True, default=True, **kwargs):
1580
+ """
1581
+ Reset the configurator to its initial state.
1582
+
1583
+ Parameters
1584
+ ----------
1585
+ %(rc.params)s
1586
+ """
1587
+ self._init(local=local, user=user, default=default, **kwargs)
1588
+
1589
+ def _load_file(self, path):
1590
+ """
1591
+ Return dictionaries of ultraplot and matplotlib settings loaded from the file.
1592
+ """
1593
+ # WARNING: Critical to not yet apply _get_item_dicts() syncing or else we
1594
+ # can overwrite input settings (e.g. label.size followed by font.size).
1595
+ path = os.path.expanduser(path)
1596
+ added = set()
1597
+ rcdict = {}
1598
+ with open(path, "r") as fh:
1599
+ for idx, line in enumerate(fh):
1600
+ # Strip comments
1601
+ message = f"line #{idx + 1} in file {path!r}"
1602
+ stripped = line.split("#", 1)[0].strip()
1603
+ if not stripped:
1604
+ pass # no warning
1605
+ continue
1606
+ # Parse the pair
1607
+ pair = stripped.split(":", 1)
1608
+ if len(pair) != 2:
1609
+ warnings._warn_ultraplot(f'Illegal {message}:\n{line}"')
1610
+ continue
1611
+ # Detect duplicates
1612
+ key, value = map(str.strip, pair)
1613
+ if key in added:
1614
+ warnings._warn_ultraplot(f"Duplicate rc key {key!r} on {message}.")
1615
+ added.add(key)
1616
+ # Get child dictionaries. Careful to have informative messages
1617
+ with warnings.catch_warnings():
1618
+ warnings.simplefilter("error", warnings.UltraplotWarning)
1619
+ try:
1620
+ key, value = self._validate_key(key, value)
1621
+ value = self._validate_value(key, value)
1622
+ except KeyError:
1623
+ warnings.simplefilter("default", warnings.UltraplotWarning)
1624
+ warnings._warn_ultraplot(f"Invalid rc key {key!r} on {message}.")
1625
+ continue
1626
+ except ValueError as err:
1627
+ warnings.simplefilter("default", warnings.UltraplotWarning)
1628
+ warnings._warn_ultraplot(
1629
+ f"Invalid rc value {value!r} for key {key!r} on {message}: {err}"
1630
+ ) # noqa: E501
1631
+ continue
1632
+ except warnings.UltraplotWarning as err:
1633
+ warnings.simplefilter("default", warnings.UltraplotWarning)
1634
+ warnings._warn_ultraplot(
1635
+ f"Outdated rc key {key!r} on {message}: {err}"
1636
+ ) # noqa: E501
1637
+ warnings.simplefilter("ignore", warnings.UltraplotWarning)
1638
+ key, value = self._validate_key(key, value)
1639
+ value = self._validate_value(key, value)
1640
+ # Update the settings
1641
+ rcdict[key] = value
1642
+
1643
+ return rcdict
1644
+
1645
+ def load(self, path):
1646
+ """
1647
+ Load settings from the specified file.
1648
+
1649
+ Parameters
1650
+ ----------
1651
+ path : path-like
1652
+ The file path.
1653
+
1654
+ See also
1655
+ --------
1656
+ Configurator.save
1657
+ """
1658
+ rcdict = self._load_file(path)
1659
+ for key, value in rcdict.items():
1660
+ self.__setitem__(key, value)
1661
+
1662
+ @staticmethod
1663
+ def _save_rst(path):
1664
+ """
1665
+ Create an RST table file. Used for online docs.
1666
+ """
1667
+ string = rcsetup._rst_table()
1668
+ with open(path, "w") as fh:
1669
+ fh.write(string)
1670
+
1671
+ @staticmethod
1672
+ def _save_yaml(path, user_dict=None, *, comment=False, description=False):
1673
+ """
1674
+ Create a YAML file. Used for online docs and default and user-generated
1675
+ ultraplotrc files. Extra settings can be passed with the input dictionary.
1676
+ """
1677
+ user_table = ()
1678
+ if user_dict: # add always-uncommented user settings
1679
+ user_table = rcsetup._yaml_table(user_dict, comment=False)
1680
+ user_table = ("# Changed settings", user_table, "")
1681
+ ultraplot_dict = (
1682
+ rcsetup._rc_ultraplot_table if description else rcsetup._rc_ultraplot_default
1683
+ ) # noqa: E501
1684
+ ultraplot_table = rcsetup._yaml_table(
1685
+ ultraplot_dict, comment=comment, description=description
1686
+ ) # noqa: E501
1687
+ ultraplot_table = ("# ultraplot settings", ultraplot_table, "")
1688
+ matplotlib_dict = rcsetup._rc_matplotlib_default
1689
+ matplotlib_table = rcsetup._yaml_table(matplotlib_dict, comment=comment)
1690
+ matplotlib_table = ("# Matplotlib settings", matplotlib_table)
1691
+ parts = (
1692
+ "#--------------------------------------------------------------------",
1693
+ "# Use this file to change the default ultraplot and matplotlib settings.",
1694
+ "# The syntax is identical to matplotlibrc syntax. For details see:",
1695
+ "# https://ultraplot.readthedocs.io/en/latest/configuration.html",
1696
+ "# https://matplotlib.org/stable/tutorials/introductory/customizing.html",
1697
+ "#--------------------------------------------------------------------",
1698
+ *user_table, # empty if nothing passed
1699
+ *ultraplot_table,
1700
+ *matplotlib_table,
1701
+ )
1702
+ with open(path, "w") as fh:
1703
+ fh.write("\n".join(parts))
1704
+
1705
+ def save(self, path=None, user=True, comment=None, backup=True, description=False):
1706
+ """
1707
+ Save the current settings to a ``ultraplotrc`` file. This writes
1708
+ the default values commented out plus the values that *differ*
1709
+ from the defaults at the top of the file.
1710
+
1711
+ Parameters
1712
+ ----------
1713
+ path : path-like, default: 'ultraplotrc'
1714
+ The file name and/or directory. The default file name is ``ultraplotrc``
1715
+ and the default directory is the current directory.
1716
+ user : bool, default: True
1717
+ If ``True`` then settings that have been `~Configurator.changed` from
1718
+ the ultraplot defaults are shown uncommented at the top of the file.
1719
+ backup : bool, default: True
1720
+ Whether to "backup" an existing file by renaming with the suffix ``.bak``
1721
+ or overwrite an existing file.
1722
+ comment : bool, optional
1723
+ Whether to comment out the default settings. If not passed
1724
+ this takes the same value as `user`.
1725
+ description : bool, default: False
1726
+ Whether to include descriptions of each setting (as seen in the
1727
+ :ref:`user guide table <ug_rctable>`) as comments.
1728
+
1729
+ See also
1730
+ --------
1731
+ Configurator.load
1732
+ Configurator.changed
1733
+ """
1734
+ path = os.path.expanduser(path or ".")
1735
+ if os.path.isdir(path): # includes ''
1736
+ path = os.path.join(path, "ultraplotrc")
1737
+ if os.path.isfile(path) and backup:
1738
+ backup = path + ".bak"
1739
+ os.rename(path, backup)
1740
+ warnings._warn_ultraplot(f"Existing file {path!r} was moved to {backup!r}.")
1741
+ comment = _not_none(comment, user)
1742
+ user_dict = self.changed if user else None
1743
+ self._save_yaml(path, user_dict, comment=comment, description=description)
1744
+
1745
+ @property
1746
+ def _context_mode(self):
1747
+ """
1748
+ Return the highest (least permissive) context mode.
1749
+ """
1750
+ return max((context.mode for context in self._context), default=0)
1751
+
1752
+ @property
1753
+ def changed(self):
1754
+ """
1755
+ A dictionary of settings that have changed from the ultraplot defaults.
1756
+
1757
+ See also
1758
+ --------
1759
+ Configurator.save
1760
+ """
1761
+ # Carefully detect changed settings
1762
+ rcdict = {}
1763
+ for key, value in self.items():
1764
+ default = rcsetup._get_default_param(key)
1765
+ if (
1766
+ isinstance(value, Real)
1767
+ and isinstance(default, Real)
1768
+ and np.isclose(value, default)
1769
+ ): # noqa: E501
1770
+ pass
1771
+ elif value == default:
1772
+ pass
1773
+ else:
1774
+ rcdict[key] = value
1775
+ # Ignore non-style-related settings. See mstyle.STYLE_BLACKLIST
1776
+ # TODO: For now not sure how to detect if prop cycle changed since
1777
+ # we cannot load it from _cmap_database in rcsetup.
1778
+ rcdict.pop("interactive", None) # changed by backend
1779
+ rcdict.pop("axes.prop_cycle", None)
1780
+ return _filter_style_dict(rcdict, warn=False)
1781
+
1782
+ # Renamed methods
1783
+ load_file = warnings._rename_objs("0.8.0", load_file=load)
1784
+
1785
+
1786
+ # Initialize locations
1787
+ _init_user_folders()
1788
+ _init_user_file()
1789
+
1790
+ #: A dictionary-like container of matplotlib settings. Assignments are
1791
+ #: validated and restricted to recognized setting names.
1792
+ rc_matplotlib = mpl.rcParams # PEP8 4 lyfe
1793
+
1794
+ #: A dictionary-like container of ultraplot settings. Assignments are
1795
+ #: validated and restricted to recognized setting names.
1796
+ rc_ultraplot = rcsetup._rc_ultraplot_default.copy() # a validated rcParams-style dict
1797
+
1798
+ #: Instance of `Configurator`. This controls both `rc_matplotlib` and `rc_ultraplot`
1799
+ #: settings. See the :ref:`configuration guide <ug_config>` for details.
1800
+ rc = Configurator(skip_cycle=True)
1801
+
1802
+ # Deprecated
1803
+ RcConfigurator = warnings._rename_objs(
1804
+ "0.8.0",
1805
+ RcConfigurator=Configurator,
1806
+ )
1807
+ inline_backend_fmt = warnings._rename_objs(
1808
+ "0.6.0", inline_backend_fmt=config_inline_backend
1809
+ )