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