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