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/axes/base.py
ADDED
|
@@ -0,0 +1,3240 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
The first-level axes subclass used for all ultraplot figures.
|
|
4
|
+
Implements basic shared functionality.
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
import inspect
|
|
8
|
+
import re
|
|
9
|
+
from numbers import Integral
|
|
10
|
+
|
|
11
|
+
import matplotlib.axes as maxes
|
|
12
|
+
import matplotlib.axis as maxis
|
|
13
|
+
import matplotlib.cm as mcm
|
|
14
|
+
import matplotlib.colors as mcolors
|
|
15
|
+
import matplotlib.container as mcontainer
|
|
16
|
+
import matplotlib.contour as mcontour
|
|
17
|
+
import matplotlib.legend as mlegend
|
|
18
|
+
import matplotlib.offsetbox as moffsetbox
|
|
19
|
+
import matplotlib.patches as mpatches
|
|
20
|
+
import matplotlib.projections as mproj
|
|
21
|
+
import matplotlib.text as mtext
|
|
22
|
+
import matplotlib.ticker as mticker
|
|
23
|
+
import matplotlib.transforms as mtransforms
|
|
24
|
+
import numpy as np
|
|
25
|
+
from matplotlib import cbook
|
|
26
|
+
|
|
27
|
+
from .. import colors as pcolors
|
|
28
|
+
from .. import constructor
|
|
29
|
+
from .. import ticker as pticker
|
|
30
|
+
from ..config import rc
|
|
31
|
+
from ..internals import ic # noqa: F401
|
|
32
|
+
from ..internals import (
|
|
33
|
+
_kwargs_to_args,
|
|
34
|
+
_not_none,
|
|
35
|
+
_pop_kwargs,
|
|
36
|
+
_pop_params,
|
|
37
|
+
_pop_props,
|
|
38
|
+
_pop_rc,
|
|
39
|
+
_translate_loc,
|
|
40
|
+
_version_mpl,
|
|
41
|
+
docstring,
|
|
42
|
+
guides,
|
|
43
|
+
labels,
|
|
44
|
+
rcsetup,
|
|
45
|
+
warnings,
|
|
46
|
+
)
|
|
47
|
+
from ..utils import _fontsize_to_pt, edges, units
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
from cartopy.crs import CRS, PlateCarree
|
|
51
|
+
except Exception:
|
|
52
|
+
CRS = PlateCarree = object
|
|
53
|
+
|
|
54
|
+
__all__ = ["Axes"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# A-b-c label string
|
|
58
|
+
ABC_STRING = "abcdefghijklmnopqrstuvwxyz"
|
|
59
|
+
|
|
60
|
+
# Legend align options
|
|
61
|
+
ALIGN_OPTS = {
|
|
62
|
+
None: {
|
|
63
|
+
"center": "center",
|
|
64
|
+
"left": "center left",
|
|
65
|
+
"right": "center right",
|
|
66
|
+
"top": "upper center",
|
|
67
|
+
"bottom": "lower center",
|
|
68
|
+
},
|
|
69
|
+
"left": {
|
|
70
|
+
"top": "upper right",
|
|
71
|
+
"center": "center right",
|
|
72
|
+
"bottom": "lower right",
|
|
73
|
+
},
|
|
74
|
+
"right": {
|
|
75
|
+
"top": "upper left",
|
|
76
|
+
"center": "center left",
|
|
77
|
+
"bottom": "lower left",
|
|
78
|
+
},
|
|
79
|
+
"top": {"left": "lower left", "center": "lower center", "right": "lower right"},
|
|
80
|
+
"bottom": {"left": "upper left", "center": "upper center", "right": "upper right"},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Projection docstring
|
|
85
|
+
_proj_docstring = """
|
|
86
|
+
proj, projection : \
|
|
87
|
+
str, `cartopy.crs.Projection`, or `~mpl_toolkits.basemap.Basemap`, optional
|
|
88
|
+
The map projection specification(s). If ``'cart'`` or ``'cartesian'``
|
|
89
|
+
(the default), a `~ultraplot.axes.CartesianAxes` is created. If ``'polar'``,
|
|
90
|
+
a `~ultraplot.axes.PolarAxes` is created. Otherwise, the argument is
|
|
91
|
+
interpreted by `~ultraplot.constructor.Proj`, and the result is used
|
|
92
|
+
to make a `~ultraplot.axes.GeoAxes` (in this case the argument can be
|
|
93
|
+
a `cartopy.crs.Projection` instance, a `~mpl_toolkits.basemap.Basemap`
|
|
94
|
+
instance, or a projection name listed in :ref:`this table <proj_table>`).
|
|
95
|
+
"""
|
|
96
|
+
_proj_kw_docstring = """
|
|
97
|
+
proj_kw, projection_kw : dict-like, optional
|
|
98
|
+
Keyword arguments passed to `~mpl_toolkits.basemap.Basemap` or
|
|
99
|
+
cartopy `~cartopy.crs.Projection` classes on instantiation.
|
|
100
|
+
"""
|
|
101
|
+
_backend_docstring = """
|
|
102
|
+
backend : {'cartopy', 'basemap'}, default: :rc:`geo.backend`
|
|
103
|
+
Whether to use `~mpl_toolkits.basemap.Basemap` or
|
|
104
|
+
`~cartopy.crs.Projection` for map projections.
|
|
105
|
+
"""
|
|
106
|
+
docstring._snippet_manager["axes.proj"] = _proj_docstring
|
|
107
|
+
docstring._snippet_manager["axes.proj_kw"] = _proj_kw_docstring
|
|
108
|
+
docstring._snippet_manager["axes.backend"] = _backend_docstring
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Colorbar and legend space
|
|
112
|
+
_space_docstring = """
|
|
113
|
+
queue : bool, optional
|
|
114
|
+
If ``True`` and `loc` is the same as an existing {name}, the input
|
|
115
|
+
arguments are added to a queue and this function returns ``None``.
|
|
116
|
+
This is used to "update" the same {name} with successive ``ax.{name}(...)``
|
|
117
|
+
calls. If ``False`` (the default) and `loc` is the same as an existing
|
|
118
|
+
*inset* {name}, the old {name} is removed. If ``False`` and `loc` is an
|
|
119
|
+
*outer* {name}, the {name}s are "stacked".
|
|
120
|
+
space : unit-spec, default: None
|
|
121
|
+
For outer {name}s only. The fixed space between the {name} and the subplot
|
|
122
|
+
edge. %(units.em)s
|
|
123
|
+
When the :ref:`tight layout algorithm <ug_tight>` is active for the figure,
|
|
124
|
+
`space` is computed automatically (see `pad`). Otherwise, `space` is set to
|
|
125
|
+
a suitable default.
|
|
126
|
+
pad : unit-spec, default: :rc:`subplots.panelpad` or :rc:`{default}`
|
|
127
|
+
For outer {name}s, this is the :ref:`tight layout padding <ug_tight>`
|
|
128
|
+
between the {name} and the subplot (default is :rcraw:`subplots.panelpad`).
|
|
129
|
+
For inset {name}s, this is the fixed space between the axes
|
|
130
|
+
edge and the {name} (default is :rcraw:`{default}`).
|
|
131
|
+
%(units.em)s
|
|
132
|
+
align : {{'center', 'top', 'bottom', 'left', 'right', 't', 'b', 'l', 'r'}}, optional
|
|
133
|
+
For outer {name}s only. How to align the {name} against the subplot edge.
|
|
134
|
+
The values ``'top'`` and ``'bottom'`` are valid for left and right {name}s
|
|
135
|
+
and ``'left'`` and ``'right'`` are valid for top and bottom {name}s.
|
|
136
|
+
The default is always ``'center'``.
|
|
137
|
+
"""
|
|
138
|
+
docstring._snippet_manager["axes.legend_space"] = _space_docstring.format(
|
|
139
|
+
name="legend", default="legend.borderaxespad"
|
|
140
|
+
)
|
|
141
|
+
docstring._snippet_manager["axes.colorbar_space"] = _space_docstring.format(
|
|
142
|
+
name="colorbar", default="colorbar.insetpad"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Transform docstring
|
|
147
|
+
# Used for text and add_axes
|
|
148
|
+
_transform_docstring = """
|
|
149
|
+
transform : {'data', 'axes', 'figure', 'subfigure'} \
|
|
150
|
+
or `~matplotlib.transforms.Transform`, optional
|
|
151
|
+
The transform used to interpret the bounds. Can be a
|
|
152
|
+
`~matplotlib.transforms.Transform` instance or a string representing
|
|
153
|
+
the `~matplotlib.axes.Axes.transData`, `~matplotlib.axes.Axes.transAxes`,
|
|
154
|
+
`~matplotlib.figure.Figure.transFigure`, or
|
|
155
|
+
`~matplotlib.figure.Figure.transSubfigure`, transforms.
|
|
156
|
+
"""
|
|
157
|
+
docstring._snippet_manager["axes.transform"] = _transform_docstring
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Inset docstring
|
|
161
|
+
# NOTE: Used by SubplotGrid.inset_axes
|
|
162
|
+
_inset_docstring = """
|
|
163
|
+
Add an inset axes.
|
|
164
|
+
This is similar to `matplotlib.axes.Axes.inset_axes`.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
bounds : 4-tuple of float
|
|
169
|
+
The (left, bottom, width, height) coordinates for the axes.
|
|
170
|
+
%(axes.transform)s
|
|
171
|
+
Default is to use the same projection as the current axes.
|
|
172
|
+
%(axes.proj)s
|
|
173
|
+
%(axes.proj_kw)s
|
|
174
|
+
%(axes.backend)s
|
|
175
|
+
zorder : float, default: 4
|
|
176
|
+
The `zorder <https://matplotlib.org/stable/gallery/misc/zorder_demo.html>`__
|
|
177
|
+
of the axes. Should be greater than the zorder of elements in the parent axes.
|
|
178
|
+
zoom : bool, default: True or False
|
|
179
|
+
Whether to draw lines indicating the inset zoom using `~Axes.indicate_inset_zoom`.
|
|
180
|
+
The line positions will automatically adjust when the parent or inset axes limits
|
|
181
|
+
change. Default is ``True`` only if both axes are `~ultraplot.axes.CartesianAxes`.
|
|
182
|
+
zoom_kw : dict, optional
|
|
183
|
+
Passed to `~Axes.indicate_inset_zoom`.
|
|
184
|
+
|
|
185
|
+
Other parameters
|
|
186
|
+
----------------
|
|
187
|
+
**kwargs
|
|
188
|
+
Passed to `ultraplot.axes.Axes`.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
ultraplot.axes.Axes
|
|
193
|
+
The inset axes.
|
|
194
|
+
|
|
195
|
+
See also
|
|
196
|
+
--------
|
|
197
|
+
Axes.indicate_inset_zoom
|
|
198
|
+
matplotlib.axes.Axes.inset_axes
|
|
199
|
+
matplotlib.axes.Axes.indicate_inset
|
|
200
|
+
matplotlib.axes.Axes.indicate_inset_zoom
|
|
201
|
+
"""
|
|
202
|
+
_indicate_inset_docstring = """
|
|
203
|
+
Add indicators denoting the zoom range of the inset axes.
|
|
204
|
+
This will replace previously drawn zoom indicators.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
%(artist.patch)s
|
|
209
|
+
zorder : float, default: 3.5
|
|
210
|
+
The `zorder <https://matplotlib.org/stable/gallery/misc/zorder_demo.html>`__ of
|
|
211
|
+
the indicators. Should be greater than the zorder of elements in the parent axes.
|
|
212
|
+
|
|
213
|
+
Other parameters
|
|
214
|
+
----------------
|
|
215
|
+
**kwargs
|
|
216
|
+
Passed to `~matplotlib.patches.Patch`.
|
|
217
|
+
|
|
218
|
+
Note
|
|
219
|
+
----
|
|
220
|
+
This command must be called from the inset axes rather than the parent axes.
|
|
221
|
+
It is called automatically when ``zoom=True`` is passed to `~Axes.inset_axes`
|
|
222
|
+
and whenever the axes are drawn (so the line positions always track the axis
|
|
223
|
+
limits even if they are later changed).
|
|
224
|
+
|
|
225
|
+
See also
|
|
226
|
+
--------
|
|
227
|
+
matplotlib.axes.Axes.indicate_inset
|
|
228
|
+
matplotlib.axes.Axes.indicate_inset_zoom
|
|
229
|
+
"""
|
|
230
|
+
docstring._snippet_manager["axes.inset"] = _inset_docstring
|
|
231
|
+
docstring._snippet_manager["axes.indicate_inset"] = _indicate_inset_docstring
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Panel docstring
|
|
235
|
+
# NOTE: Used by SubplotGrid.panel_axes
|
|
236
|
+
_panel_loc_docstring = """
|
|
237
|
+
========== =====================
|
|
238
|
+
Location Valid keys
|
|
239
|
+
========== =====================
|
|
240
|
+
left ``'left'``, ``'l'``
|
|
241
|
+
right ``'right'``, ``'r'``
|
|
242
|
+
bottom ``'bottom'``, ``'b'``
|
|
243
|
+
top ``'top'``, ``'t'``
|
|
244
|
+
========== =====================
|
|
245
|
+
"""
|
|
246
|
+
_panel_docstring = """
|
|
247
|
+
Add a panel axes.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
side : str, optional
|
|
252
|
+
The panel location. Valid location keys are as follows.
|
|
253
|
+
|
|
254
|
+
%(axes.panel_loc)s
|
|
255
|
+
|
|
256
|
+
width : unit-spec, default: :rc:`subplots.panelwidth`
|
|
257
|
+
The panel width.
|
|
258
|
+
%(units.in)s
|
|
259
|
+
space : unit-spec, default: None
|
|
260
|
+
The fixed space between the panel and the subplot edge.
|
|
261
|
+
%(units.em)s
|
|
262
|
+
When the :ref:`tight layout algorithm <ug_tight>` is active for the figure,
|
|
263
|
+
`space` is computed automatically (see `pad`). Otherwise, `space` is set to
|
|
264
|
+
a suitable default.
|
|
265
|
+
pad : unit-spec, default: :rc:`subplots.panelpad`
|
|
266
|
+
The :ref:`tight layout padding <ug_tight>` between the panel and the subplot.
|
|
267
|
+
%(units.em)s
|
|
268
|
+
share : bool, default: True
|
|
269
|
+
Whether to enable axis sharing between the *x* and *y* axes of the
|
|
270
|
+
main subplot and the panel long axes for each panel in the "stack".
|
|
271
|
+
Sharing between the panel short axis and other panel short axes
|
|
272
|
+
is determined by figure-wide `sharex` and `sharey` settings.
|
|
273
|
+
|
|
274
|
+
Other parameters
|
|
275
|
+
----------------
|
|
276
|
+
**kwargs
|
|
277
|
+
Passed to `ultraplot.axes.CartesianAxes`. Supports all valid
|
|
278
|
+
`~ultraplot.axes.CartesianAxes.format` keywords.
|
|
279
|
+
|
|
280
|
+
Returns
|
|
281
|
+
-------
|
|
282
|
+
ultraplot.axes.CartesianAxes
|
|
283
|
+
The panel axes.
|
|
284
|
+
"""
|
|
285
|
+
docstring._snippet_manager["axes.panel_loc"] = _panel_loc_docstring
|
|
286
|
+
docstring._snippet_manager["axes.panel"] = _panel_docstring
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# Format docstrings
|
|
290
|
+
_axes_format_docstring = """
|
|
291
|
+
title : str or sequence, optional
|
|
292
|
+
The axes title. Can optionally be a sequence strings, in which case
|
|
293
|
+
the title will be selected from the sequence according to `~Axes.number`.
|
|
294
|
+
abc : bool or str or sequence, default: :rc:`abc`
|
|
295
|
+
The "a-b-c" subplot label style. Must contain the character ``a`` or ``A``,
|
|
296
|
+
for example ``'a.'``, or ``'A'``. If ``True`` then the default style of
|
|
297
|
+
``'a'`` is used. The ``a`` or ``A`` is replaced with the alphabetic character
|
|
298
|
+
matching the `~Axes.number`. If `~Axes.number` is greater than 26, the
|
|
299
|
+
characters loop around to a, ..., z, aa, ..., zz, aaa, ..., zzz, etc.
|
|
300
|
+
Can also be a sequence of strings, in which case the "a-b-c" label
|
|
301
|
+
will simply be selected from the sequence according to `~Axes.number`.
|
|
302
|
+
abcloc, titleloc : str, default: :rc:`abc.loc`, :rc:`title.loc`
|
|
303
|
+
Strings indicating the location for the a-b-c label and main title.
|
|
304
|
+
The following locations are valid:
|
|
305
|
+
|
|
306
|
+
.. _title_table:
|
|
307
|
+
|
|
308
|
+
======================== ============================
|
|
309
|
+
Location Valid keys
|
|
310
|
+
======================== ============================
|
|
311
|
+
center above axes ``'center'``, ``'c'``
|
|
312
|
+
left above axes ``'left'``, ``'l'``
|
|
313
|
+
right above axes ``'right'``, ``'r'``
|
|
314
|
+
lower center inside axes ``'lower center'``, ``'lc'``
|
|
315
|
+
upper center inside axes ``'upper center'``, ``'uc'``
|
|
316
|
+
upper right inside axes ``'upper right'``, ``'ur'``
|
|
317
|
+
upper left inside axes ``'upper left'``, ``'ul'``
|
|
318
|
+
lower left inside axes ``'lower left'``, ``'ll'``
|
|
319
|
+
lower right inside axes ``'lower right'``, ``'lr'``
|
|
320
|
+
======================== ============================
|
|
321
|
+
|
|
322
|
+
abcborder, titleborder : bool, default: :rc:`abc.border` and :rc:`title.border`
|
|
323
|
+
Whether to draw a white border around titles and a-b-c labels positioned
|
|
324
|
+
inside the axes. This can help them stand out on top of artists
|
|
325
|
+
plotted inside the axes.
|
|
326
|
+
abcbbox, titlebbox : bool, default: :rc:`abc.bbox` and :rc:`title.bbox`
|
|
327
|
+
Whether to draw a white bbox around titles and a-b-c labels positioned
|
|
328
|
+
inside the axes. This can help them stand out on top of artists plotted
|
|
329
|
+
inside the axes.
|
|
330
|
+
abc_kw, title_kw : dict-like, optional
|
|
331
|
+
Additional settings used to update the a-b-c label and title
|
|
332
|
+
with ``text.update()``.
|
|
333
|
+
titlepad : float, default: :rc:`title.pad`
|
|
334
|
+
The padding for the inner and outer titles and a-b-c labels.
|
|
335
|
+
%(units.pt)s
|
|
336
|
+
titleabove : bool, default: :rc:`title.above`
|
|
337
|
+
Whether to try to put outer titles and a-b-c labels above panels,
|
|
338
|
+
colorbars, or legends that are above the axes.
|
|
339
|
+
abctitlepad : float, default: :rc:`abc.titlepad`
|
|
340
|
+
The horizontal padding between a-b-c labels and titles in the same location.
|
|
341
|
+
%(units.pt)s
|
|
342
|
+
ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle \
|
|
343
|
+
: str or sequence, optional
|
|
344
|
+
Shorthands for the below keywords.
|
|
345
|
+
lefttitle, centertitle, righttitle, upperlefttitle, uppercentertitle, upperrighttitle, \
|
|
346
|
+
lowerlefttitle, lowercentertitle, lowerrighttitle : str or sequence, optional
|
|
347
|
+
Additional titles in specific positions (see `title` for details). This works as
|
|
348
|
+
an alternative to the ``ax.format(title='Title', titleloc=loc)`` workflow and
|
|
349
|
+
permits adding more than one title-like label for a single axes.
|
|
350
|
+
a, alpha, fc, facecolor, ec, edgecolor, lw, linewidth, ls, linestyle : default: \
|
|
351
|
+
:rc:`axes.alpha`, :rc:`axes.facecolor`, :rc:`axes.edgecolor`, :rc:`axes.linewidth`, '-'
|
|
352
|
+
Additional settings applied to the background patch, and their
|
|
353
|
+
shorthands. Their defaults values are the ``'axes'`` properties.
|
|
354
|
+
"""
|
|
355
|
+
_figure_format_docstring = """
|
|
356
|
+
rowlabels, collabels, llabels, tlabels, rlabels, blabels
|
|
357
|
+
Aliases for `leftlabels` and `toplabels`, and for `leftlabels`,
|
|
358
|
+
`toplabels`, `rightlabels`, and `bottomlabels`, respectively.
|
|
359
|
+
leftlabels, toplabels, rightlabels, bottomlabels : sequence of str, optional
|
|
360
|
+
Labels for the subplots lying along the left, top, right, and
|
|
361
|
+
bottom edges of the figure. The length of each list must match
|
|
362
|
+
the number of subplots along the corresponding edge.
|
|
363
|
+
leftlabelpad, toplabelpad, rightlabelpad, bottomlabelpad : float or unit-spec, default\
|
|
364
|
+
: :rc:`leftlabel.pad`, :rc:`toplabel.pad`, :rc:`rightlabel.pad`, :rc:`bottomlabel.pad`
|
|
365
|
+
The padding between the labels and the axes content.
|
|
366
|
+
%(units.pt)s
|
|
367
|
+
leftlabels_kw, toplabels_kw, rightlabels_kw, bottomlabels_kw : dict-like, optional
|
|
368
|
+
Additional settings used to update the labels with ``text.update()``.
|
|
369
|
+
figtitle
|
|
370
|
+
Alias for `suptitle`.
|
|
371
|
+
suptitle : str, optional
|
|
372
|
+
The figure "super" title, centered between the left edge of the leftmost
|
|
373
|
+
subplot and the right edge of the rightmost subplot.
|
|
374
|
+
suptitlepad : float, default: :rc:`suptitle.pad`
|
|
375
|
+
The padding between the super title and the axes content.
|
|
376
|
+
%(units.pt)s
|
|
377
|
+
suptitle_kw : optional
|
|
378
|
+
Additional settings used to update the super title with ``text.update()``.
|
|
379
|
+
includepanels : bool, default: False
|
|
380
|
+
Whether to include panels when aligning figure "super titles" along the top
|
|
381
|
+
of the subplot grid and when aligning the `spanx` *x* axis labels and
|
|
382
|
+
`spany` *y* axis labels along the sides of the subplot grid.
|
|
383
|
+
"""
|
|
384
|
+
_rc_init_docstring = """
|
|
385
|
+
"""
|
|
386
|
+
_rc_format_docstring = """
|
|
387
|
+
rc_mode : int, optional
|
|
388
|
+
The context mode passed to `~ultraplot.config.Configurator.context`.
|
|
389
|
+
rc_kw : dict-like, optional
|
|
390
|
+
An alternative to passing extra keyword arguments. See below.
|
|
391
|
+
**kwargs
|
|
392
|
+
{}Keyword arguments that match the name of an `~ultraplot.config.rc` setting are
|
|
393
|
+
passed to `ultraplot.config.Configurator.context` and used to update the axes.
|
|
394
|
+
If the setting name has "dots" you can simply omit the dots. For example,
|
|
395
|
+
``abc='A.'`` modifies the :rcraw:`abc` setting, ``titleloc='left'`` modifies the
|
|
396
|
+
:rcraw:`title.loc` setting, ``gridminor=True`` modifies the :rcraw:`gridminor`
|
|
397
|
+
setting, and ``gridbelow=True`` modifies the :rcraw:`grid.below` setting. Many
|
|
398
|
+
of the keyword arguments documented above are internally applied by retrieving
|
|
399
|
+
settings passed to `~ultraplot.config.Configurator.context`.
|
|
400
|
+
"""
|
|
401
|
+
docstring._snippet_manager["rc.init"] = _rc_format_docstring.format(
|
|
402
|
+
"Remaining keyword arguments are passed to `matplotlib.axes.Axes`.\n "
|
|
403
|
+
)
|
|
404
|
+
docstring._snippet_manager["rc.format"] = _rc_format_docstring.format("")
|
|
405
|
+
docstring._snippet_manager["axes.format"] = _axes_format_docstring
|
|
406
|
+
docstring._snippet_manager["figure.format"] = _figure_format_docstring
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# Colorbar docstrings
|
|
410
|
+
_colorbar_args_docstring = """
|
|
411
|
+
mappable : mappable, colormap-spec, sequence of color-spec, \
|
|
412
|
+
or sequence of `~matplotlib.artist.Artist`
|
|
413
|
+
There are four options here:
|
|
414
|
+
|
|
415
|
+
1. A `~matplotlib.cm.ScalarMappable` (e.g., an object returned by
|
|
416
|
+
`~ultraplot.axes.PlotAxes.contourf` or `~ultraplot.axes.PlotAxes.pcolormesh`).
|
|
417
|
+
2. A `~matplotlib.colors.Colormap` or registered colormap name used to build a
|
|
418
|
+
`~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar range and ticks depend
|
|
419
|
+
on the arguments `values`, `vmin`, `vmax`, and `norm`. The default for a
|
|
420
|
+
`~ultraplot.colors.ContinuousColormap` is ``vmin=0`` and ``vmax=1`` (note that
|
|
421
|
+
passing `values` will "discretize" the colormap). The default for a
|
|
422
|
+
`~ultraplot.colors.DiscreteColormap` is ``values=np.arange(0, cmap.N)``.
|
|
423
|
+
3. A sequence of hex strings, color names, or RGB[A] tuples. A
|
|
424
|
+
`~ultraplot.colors.DiscreteColormap` will be generated from these colors and
|
|
425
|
+
used to build a `~matplotlib.cm.ScalarMappable` on-the-fly. The colorbar
|
|
426
|
+
range and ticks depend on the arguments `values`, `norm`, and
|
|
427
|
+
`norm_kw`. The default is ``values=np.arange(0, len(mappable))``.
|
|
428
|
+
4. A sequence of `matplotlib.artist.Artist` instances (e.g., a list of
|
|
429
|
+
`~matplotlib.lines.Line2D` instances returned by `~ultraplot.axes.PlotAxes.plot`).
|
|
430
|
+
A colormap will be generated from the colors of these objects (where the
|
|
431
|
+
color is determined by ``get_color``, if available, or ``get_facecolor``).
|
|
432
|
+
The colorbar range and ticks depend on the arguments `values`, `norm`, and
|
|
433
|
+
`norm_kw`. The default is to infer colorbar ticks and tick labels
|
|
434
|
+
by calling `~matplotlib.artist.Artist.get_label` on each artist.
|
|
435
|
+
|
|
436
|
+
values : sequence of float or str, optional
|
|
437
|
+
Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This maps the colormap
|
|
438
|
+
colors to numeric values using `~ultraplot.colors.DiscreteNorm`. If the colormap is
|
|
439
|
+
a `~ultraplot.colors.ContinuousColormap` then its colors will be "discretized".
|
|
440
|
+
These These can also be strings, in which case the list indices are used for
|
|
441
|
+
tick locations and the strings are applied as tick labels.
|
|
442
|
+
"""
|
|
443
|
+
_colorbar_kwargs_docstring = """
|
|
444
|
+
orientation : {None, 'horizontal', 'vertical'}, optional
|
|
445
|
+
The colorbar orientation. By default this depends on the "side" of the subplot
|
|
446
|
+
or figure where the colorbar is drawn. Inset colorbars are always horizontal.
|
|
447
|
+
norm : norm-spec, optional
|
|
448
|
+
Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. This is the continuous
|
|
449
|
+
normalizer used to scale the `~ultraplot.colors.ContinuousColormap` (or passed
|
|
450
|
+
to `~ultraplot.colors.DiscreteNorm` if `values` was passed). Passed to the
|
|
451
|
+
`~ultraplot.constructor.Norm` constructor function.
|
|
452
|
+
norm_kw : dict-like, optional
|
|
453
|
+
Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. These are the
|
|
454
|
+
normalizer keyword arguments. Passed to `~ultraplot.constructor.Norm`.
|
|
455
|
+
vmin, vmax : float, optional
|
|
456
|
+
Ignored if `mappable` is a `~matplotlib.cm.ScalarMappable`. These are the minimum
|
|
457
|
+
and maximum colorbar values. Passed to `~ultraplot.constructor.Norm`.
|
|
458
|
+
label, title : str, optional
|
|
459
|
+
The colorbar label. The `title` keyword is also accepted for
|
|
460
|
+
consistency with `~matplotlib.axes.Axes.legend`.
|
|
461
|
+
reverse : bool, optional
|
|
462
|
+
Whether to reverse the direction of the colorbar. This is done automatically
|
|
463
|
+
when descending levels are used with `~ultraplot.colors.DiscreteNorm`.
|
|
464
|
+
rotation : float, default: 0
|
|
465
|
+
The tick label rotation.
|
|
466
|
+
grid, edges, drawedges : bool, default: :rc:`colorbar.grid`
|
|
467
|
+
Whether to draw "grid" dividers between each distinct color.
|
|
468
|
+
extend : {'neither', 'both', 'min', 'max'}, optional
|
|
469
|
+
Direction for drawing colorbar "extensions" (i.e. color keys for out-of-bounds
|
|
470
|
+
data on the end of the colorbar). Default behavior is to use the value of `extend`
|
|
471
|
+
passed to the plotting command or use ``'neither'`` if the value is unknown.
|
|
472
|
+
extendfrac : float, optional
|
|
473
|
+
The length of the colorbar "extensions" relative to the length of the colorbar.
|
|
474
|
+
This is a native matplotlib `~matplotlib.figure.Figure.colorbar` keyword.
|
|
475
|
+
extendsize : unit-spec, default: :rc:`colorbar.extend` or :rc:`colorbar.insetextend`
|
|
476
|
+
The length of the colorbar "extensions" in physical units. Default is
|
|
477
|
+
:rcraw:`colorbar.extend` for outer colorbars and :rcraw:`colorbar.insetextend`
|
|
478
|
+
for inset colorbars. %(units.em)s
|
|
479
|
+
extendrect : bool, default: False
|
|
480
|
+
Whether to draw colorbar "extensions" as rectangles. If ``False`` then
|
|
481
|
+
the extensions are drawn as triangles.
|
|
482
|
+
locator, ticks : locator-spec, optional
|
|
483
|
+
Used to determine the colorbar tick positions. Passed to the
|
|
484
|
+
`~ultraplot.constructor.Locator` constructor function. By default
|
|
485
|
+
`~matplotlib.ticker.AutoLocator` is used for continuous color levels
|
|
486
|
+
and `~ultraplot.ticker.DiscreteLocator` is used for discrete color levels.
|
|
487
|
+
locator_kw : dict-like, optional
|
|
488
|
+
Keyword arguments passed to `matplotlib.ticker.Locator` class.
|
|
489
|
+
minorlocator, minorticks
|
|
490
|
+
As with `locator`, `ticks` but for the minor ticks. By default
|
|
491
|
+
`~matplotlib.ticker.AutoMinorLocator` is used for continuous color levels
|
|
492
|
+
and `~ultraplot.ticker.DiscreteLocator` is used for discrete color levels.
|
|
493
|
+
minorlocator_kw
|
|
494
|
+
As with `locator_kw`, but for the minor ticks.
|
|
495
|
+
format, formatter, ticklabels : formatter-spec, optional
|
|
496
|
+
The tick label format. Passed to the `~ultraplot.constructor.Formatter`
|
|
497
|
+
constructor function.
|
|
498
|
+
formatter_kw : dict-like, optional
|
|
499
|
+
Keyword arguments passed to `matplotlib.ticker.Formatter` class.
|
|
500
|
+
frame, frameon : bool, default: :rc:`colorbar.frameon`
|
|
501
|
+
For inset colorbars only. Indicates whether to draw a "frame",
|
|
502
|
+
just like `~matplotlib.axes.Axes.legend`.
|
|
503
|
+
tickminor : bool, optional
|
|
504
|
+
Whether to add minor ticks using `~matplotlib.colorbar.ColorbarBase.minorticks_on`.
|
|
505
|
+
tickloc, ticklocation : {'bottom', 'top', 'left', 'right'}, optional
|
|
506
|
+
Where to draw tick marks on the colorbar. Default is toward the outside
|
|
507
|
+
of the subplot for outer colorbars and ``'bottom'`` for inset colorbars.
|
|
508
|
+
tickdir, tickdirection : {'out', 'in', 'inout'}, default: :rc:`tick.dir`
|
|
509
|
+
Direction of major and minor colorbar ticks.
|
|
510
|
+
ticklen : unit-spec, default: :rc:`tick.len`
|
|
511
|
+
Major tick lengths for the colorbar ticks.
|
|
512
|
+
ticklenratio : float, default: :rc:`tick.lenratio`
|
|
513
|
+
Relative scaling of `ticklen` used to determine minor tick lengths.
|
|
514
|
+
tickwidth : unit-spec, default: `linewidth`
|
|
515
|
+
Major tick widths for the colorbar ticks.
|
|
516
|
+
or :rc:`tick.width` if `linewidth` was not passed.
|
|
517
|
+
tickwidthratio : float, default: :rc:`tick.widthratio`
|
|
518
|
+
Relative scaling of `tickwidth` used to determine minor tick widths.
|
|
519
|
+
ticklabelcolor, ticklabelsize, ticklabelweight \
|
|
520
|
+
: default: :rc:`tick.labelcolor`, :rc:`tick.labelsize`, :rc:`tick.labelweight`.
|
|
521
|
+
The font color, size, and weight for colorbar tick labels
|
|
522
|
+
labelloc, labellocation : {'bottom', 'top', 'left', 'right'}
|
|
523
|
+
The colorbar label location. Inherits from `tickloc` by default. Default is toward
|
|
524
|
+
the outside of the subplot for outer colorbars and ``'bottom'`` for inset colorbars.
|
|
525
|
+
labelcolor, labelsize, labelweight \
|
|
526
|
+
: default: :rc:`label.color`, :rc:`label.size`, and :rc:`label.weight`.
|
|
527
|
+
The font color, size, and weight for the colorbar label.
|
|
528
|
+
a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth : default\
|
|
529
|
+
: :rc:`colorbar.framealpha`, :rc:`colorbar.framecolor`
|
|
530
|
+
For inset colorbars only. Controls the transparency and color of
|
|
531
|
+
the background frame.
|
|
532
|
+
lw, linewidth, c, color : optional
|
|
533
|
+
Controls the line width and edge color for both the colorbar
|
|
534
|
+
outline and the level dividers.
|
|
535
|
+
%(axes.edgefix)s
|
|
536
|
+
rasterize : bool, default: :rc:`colorbar.rasterize`
|
|
537
|
+
Whether to rasterize the colorbar solids. The matplotlib default was ``True``
|
|
538
|
+
but ultraplot changes this to ``False`` since rasterization can cause misalignment
|
|
539
|
+
between the color patches and the colorbar outline.
|
|
540
|
+
**kwargs
|
|
541
|
+
Passed to `~matplotlib.figure.Figure.colorbar`.
|
|
542
|
+
"""
|
|
543
|
+
_edgefix_docstring = """
|
|
544
|
+
edgefix : bool or float, default: :rc:`edgefix`
|
|
545
|
+
Whether to fix the common issue where white lines appear between adjacent
|
|
546
|
+
patches in saved vector graphics (this can slow down figure rendering).
|
|
547
|
+
See this `github repo <https://github.com/jklymak/contourfIssues>`__ for a
|
|
548
|
+
demonstration of the problem. If ``True``, a small default linewidth of
|
|
549
|
+
``0.3`` is used to cover up the white lines. If float (e.g. ``edgefix=0.5``),
|
|
550
|
+
this specific linewidth is used to cover up the white lines. This feature is
|
|
551
|
+
automatically disabled when the patches have transparency.
|
|
552
|
+
"""
|
|
553
|
+
docstring._snippet_manager["axes.edgefix"] = _edgefix_docstring
|
|
554
|
+
docstring._snippet_manager["axes.colorbar_args"] = _colorbar_args_docstring
|
|
555
|
+
docstring._snippet_manager["axes.colorbar_kwargs"] = _colorbar_kwargs_docstring
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# Legend docstrings
|
|
559
|
+
_legend_args_docstring = """
|
|
560
|
+
handles : list of artist, optional
|
|
561
|
+
List of matplotlib artists, or a list of lists of artist instances (see the `center`
|
|
562
|
+
keyword). If not passed, artists with valid labels (applied by passing `label` or
|
|
563
|
+
`labels` to a plotting command or calling `~matplotlib.artist.Artist.set_label`)
|
|
564
|
+
are retrieved automatically. If the object is a `~matplotlib.contour.ContourSet`,
|
|
565
|
+
`~matplotlib.contour.ContourSet.legend_elements` is used to select the central
|
|
566
|
+
artist in the list (generally useful for single-color contour plots). Note that
|
|
567
|
+
ultraplot's `~ultraplot.axes.PlotAxes.contour` and `~ultraplot.axes.PlotAxes.contourf`
|
|
568
|
+
accept a legend `label` keyword argument.
|
|
569
|
+
labels : list of str, optional
|
|
570
|
+
A matching list of string labels or ``None`` placeholders, or a matching list of
|
|
571
|
+
lists (see the `center` keyword). Wherever ``None`` appears in the list (or
|
|
572
|
+
if no labels were passed at all), labels are retrieved by calling
|
|
573
|
+
`~matplotlib.artist.Artist.get_label` on each `~matplotlib.artist.Artist` in the
|
|
574
|
+
handle list. If a handle consists of a tuple group of artists, labels are inferred
|
|
575
|
+
from the artists in the tuple (if there are multiple unique labels in the tuple
|
|
576
|
+
group of artists, the tuple group is expanded into unique legend entries --
|
|
577
|
+
otherwise, the tuple group elements are drawn on top of eachother). For details
|
|
578
|
+
on matplotlib legend handlers and tuple groups, see the matplotlib `legend guide \
|
|
579
|
+
<https://matplotlib.org/stable/tutorials/intermediate/legend_guide.html>`__.
|
|
580
|
+
"""
|
|
581
|
+
_legend_kwargs_docstring = """
|
|
582
|
+
frame, frameon : bool, optional
|
|
583
|
+
Toggles the legend frame. For centered-row legends, a frame
|
|
584
|
+
independent from matplotlib's built-in legend frame is created.
|
|
585
|
+
ncol, ncols : int, optional
|
|
586
|
+
The number of columns. `ncols` is an alias, added
|
|
587
|
+
for consistency with `~matplotlib.pyplot.subplots`.
|
|
588
|
+
order : {'C', 'F'}, optional
|
|
589
|
+
Whether legend handles are drawn in row-major (``'C'``) or column-major
|
|
590
|
+
(``'F'``) order. Analagous to `numpy.array` ordering. The matplotlib
|
|
591
|
+
default was ``'F'`` but ultraplot changes this to ``'C'``.
|
|
592
|
+
center : bool, optional
|
|
593
|
+
Whether to center each legend row individually. If ``True``, we draw
|
|
594
|
+
successive single-row legends "stacked" on top of each other. If ``None``,
|
|
595
|
+
we infer this setting from `handles`. By default, `center` is set to ``True``
|
|
596
|
+
if `handles` is a list of lists (each sublist is used as a row in the legend).
|
|
597
|
+
alphabetize : bool, default: False
|
|
598
|
+
Whether to alphabetize the legend entries according to
|
|
599
|
+
the legend labels.
|
|
600
|
+
title, label : str, optional
|
|
601
|
+
The legend title. The `label` keyword is also accepted, for consistency
|
|
602
|
+
with `~matplotlib.figure.Figure.colorbar`.
|
|
603
|
+
fontsize, fontweight, fontcolor : optional
|
|
604
|
+
The font size, weight, and color for the legend text. Font size is interpreted
|
|
605
|
+
by `~ultraplot.utils.units`. The default font size is :rcraw:`legend.fontsize`.
|
|
606
|
+
titlefontsize, titlefontweight, titlefontcolor : optional
|
|
607
|
+
The font size, weight, and color for the legend title. Font size is interpreted
|
|
608
|
+
by `~ultraplot.utils.units`. The default size is `fontsize`.
|
|
609
|
+
borderpad, borderaxespad, handlelength, handleheight, handletextpad, \
|
|
610
|
+
labelspacing, columnspacing : unit-spec, optional
|
|
611
|
+
Various matplotlib `~matplotlib.axes.Axes.legend` spacing arguments.
|
|
612
|
+
%(units.em)s
|
|
613
|
+
a, alpha, framealpha, fc, facecolor, framecolor, ec, edgecolor, ew, edgewidth \
|
|
614
|
+
: default: :rc:`legend.framealpha`, :rc:`legend.facecolor`, :rc:`legend.edgecolor`, \
|
|
615
|
+
:rc:`axes.linewidth`
|
|
616
|
+
The opacity, face color, edge color, and edge width for the legend frame.
|
|
617
|
+
c, color, lw, linewidth, m, marker, ls, linestyle, dashes, ms, markersize : optional
|
|
618
|
+
Properties used to override the legend handles. For example, for a
|
|
619
|
+
legend describing variations in line style ignoring variations
|
|
620
|
+
in color, you might want to use ``color='black'``.
|
|
621
|
+
handle_kw : dict-like, optional
|
|
622
|
+
Additional properties used to override legend handles, e.g.
|
|
623
|
+
``handle_kw={'edgecolor': 'black'}``. Only line properties
|
|
624
|
+
can be passed as keyword arguments.
|
|
625
|
+
handler_map : dict-like, optional
|
|
626
|
+
A dictionary mapping instances or types to a legend handler.
|
|
627
|
+
This `handler_map` updates the default handler map found at
|
|
628
|
+
`matplotlib.legend.Legend.get_legend_handler_map`.
|
|
629
|
+
**kwargs
|
|
630
|
+
Passed to `~matplotlib.axes.Axes.legend`.
|
|
631
|
+
"""
|
|
632
|
+
docstring._snippet_manager["axes.legend_args"] = _legend_args_docstring
|
|
633
|
+
docstring._snippet_manager["axes.legend_kwargs"] = _legend_kwargs_docstring
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _align_bbox(align, length):
|
|
637
|
+
"""
|
|
638
|
+
Return a simple alignment bounding box for intersection calculations.
|
|
639
|
+
"""
|
|
640
|
+
if align in ("left", "bottom"):
|
|
641
|
+
bounds = [[0, 0], [length, 0]]
|
|
642
|
+
elif align in ("top", "right"):
|
|
643
|
+
bounds = [[1 - length, 0], [1, 0]]
|
|
644
|
+
elif align == "center":
|
|
645
|
+
bounds = [[0.5 * (1 - length), 0], [0.5 * (1 + length), 0]]
|
|
646
|
+
else:
|
|
647
|
+
raise ValueError(f"Invalid align {align!r}.")
|
|
648
|
+
return mtransforms.Bbox(bounds)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class _TransformedBoundsLocator:
|
|
652
|
+
"""
|
|
653
|
+
Axes locator for `~Axes.inset_axes` and other axes.
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
def __init__(self, bounds, transform):
|
|
657
|
+
self._bounds = bounds
|
|
658
|
+
self._transform = transform
|
|
659
|
+
|
|
660
|
+
def __call__(self, ax, renderer): # noqa: U100
|
|
661
|
+
transfig = getattr(ax.figure, "transSubfigure", ax.figure.transFigure)
|
|
662
|
+
bbox = mtransforms.Bbox.from_bounds(*self._bounds)
|
|
663
|
+
bbox = mtransforms.TransformedBbox(bbox, self._transform)
|
|
664
|
+
bbox = mtransforms.TransformedBbox(bbox, transfig.inverted())
|
|
665
|
+
return bbox
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class Axes(maxes.Axes):
|
|
669
|
+
"""
|
|
670
|
+
The lowest-level `~matplotlib.axes.Axes` subclass used by ultraplot.
|
|
671
|
+
Implements basic universal features.
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
_name = None # derived must override
|
|
675
|
+
_name_aliases = ()
|
|
676
|
+
_make_inset_locator = _TransformedBoundsLocator
|
|
677
|
+
|
|
678
|
+
def __repr__(self):
|
|
679
|
+
# Show the position in the geometry excluding panels. Panels are
|
|
680
|
+
# indicated by showing their parent geometry plus a 'side' argument.
|
|
681
|
+
# WARNING: This will not be used in matplotlib 3.3.0 (and probably next
|
|
682
|
+
# minor releases) because native __repr__ is defined in SubplotBase.
|
|
683
|
+
ax = self._get_topmost_axes()
|
|
684
|
+
name = type(self).__name__
|
|
685
|
+
prefix = "" if ax is self else "parent_"
|
|
686
|
+
params = {}
|
|
687
|
+
if self._name in ("cartopy", "basemap"):
|
|
688
|
+
name = name.replace("_" + self._name.title(), "Geo")
|
|
689
|
+
params["backend"] = self._name
|
|
690
|
+
if self._inset_parent:
|
|
691
|
+
name = re.sub("Axes(Subplot)?", "AxesInset", name)
|
|
692
|
+
params["bounds"] = tuple(np.round(self._inset_bounds, 2))
|
|
693
|
+
if self._altx_parent or self._alty_parent:
|
|
694
|
+
name = re.sub("Axes(Subplot)?", "AxesTwin", name)
|
|
695
|
+
params["axis"] = "x" if self._altx_parent else "y"
|
|
696
|
+
if self._colorbar_fill:
|
|
697
|
+
name = re.sub("Axes(Subplot)?", "AxesFill", name)
|
|
698
|
+
params["side"] = self._axes._panel_side
|
|
699
|
+
if self._panel_side:
|
|
700
|
+
name = re.sub("Axes(Subplot)?", "AxesPanel", name)
|
|
701
|
+
params["side"] = self._panel_side
|
|
702
|
+
try:
|
|
703
|
+
nrows, ncols, num1, num2 = (
|
|
704
|
+
ax.get_subplotspec().get_topmost_subplotspec()._get_geometry()
|
|
705
|
+
) # noqa: E501
|
|
706
|
+
params[prefix + "index"] = (num1, num2)
|
|
707
|
+
except (IndexError, ValueError, AttributeError): # e.g. a loose axes
|
|
708
|
+
left, bottom, width, height = np.round(self._position.bounds, 2)
|
|
709
|
+
params["left"], params["bottom"], params["size"] = (
|
|
710
|
+
left,
|
|
711
|
+
bottom,
|
|
712
|
+
(width, bottom),
|
|
713
|
+
) # noqa: E501
|
|
714
|
+
if ax.number:
|
|
715
|
+
params[prefix + "number"] = ax.number
|
|
716
|
+
params = ", ".join(f"{key}={value!r}" for key, value in params.items())
|
|
717
|
+
return f"{name}({params})"
|
|
718
|
+
|
|
719
|
+
def __str__(self):
|
|
720
|
+
return self.__repr__()
|
|
721
|
+
|
|
722
|
+
@docstring._snippet_manager
|
|
723
|
+
def __init__(self, *args, **kwargs):
|
|
724
|
+
"""
|
|
725
|
+
Parameters
|
|
726
|
+
----------
|
|
727
|
+
*args
|
|
728
|
+
Passed to `matplotlib.axes.Axes`.
|
|
729
|
+
%(axes.format)s
|
|
730
|
+
|
|
731
|
+
Other parameters
|
|
732
|
+
----------------
|
|
733
|
+
%(rc.init)s
|
|
734
|
+
|
|
735
|
+
See also
|
|
736
|
+
--------
|
|
737
|
+
Axes.format
|
|
738
|
+
matplotlib.axes.Axes
|
|
739
|
+
ultraplot.axes.PlotAxes
|
|
740
|
+
ultraplot.axes.CartesianAxes
|
|
741
|
+
ultraplot.axes.PolarAxes
|
|
742
|
+
ultraplot.axes.GeoAxes
|
|
743
|
+
ultraplot.figure.Figure.subplot
|
|
744
|
+
ultraplot.figure.Figure.add_subplot
|
|
745
|
+
"""
|
|
746
|
+
# Remove subplot-related args
|
|
747
|
+
# NOTE: These are documented on add_subplot()
|
|
748
|
+
ss = kwargs.pop("_subplot_spec", None) # see below
|
|
749
|
+
number = kwargs.pop("number", None)
|
|
750
|
+
autoshare = kwargs.pop("autoshare", None)
|
|
751
|
+
autoshare = _not_none(autoshare, True)
|
|
752
|
+
|
|
753
|
+
# Remove format-related args and initialize
|
|
754
|
+
rc_kw, rc_mode = _pop_rc(kwargs)
|
|
755
|
+
kw_format = _pop_props(kwargs, "patch") # background properties
|
|
756
|
+
if "zorder" in kw_format: # special case: refers to the entire axes
|
|
757
|
+
kwargs["zorder"] = kw_format.pop("zorder")
|
|
758
|
+
for cls, sig in self._format_signatures.items():
|
|
759
|
+
if isinstance(self, cls):
|
|
760
|
+
kw_format.update(_pop_params(kwargs, sig))
|
|
761
|
+
super().__init__(*args, **kwargs)
|
|
762
|
+
|
|
763
|
+
# Varous scalar properties
|
|
764
|
+
self._active_cycle = rc["axes.prop_cycle"]
|
|
765
|
+
self._auto_format = None # manipulated by wrapper functions
|
|
766
|
+
self._abc_border_kwargs = {}
|
|
767
|
+
self._abc_loc = None
|
|
768
|
+
self._abc_title_pad = rc["abc.titlepad"]
|
|
769
|
+
self._title_above = rc["title.above"]
|
|
770
|
+
self._title_border_kwargs = {} # title border properties
|
|
771
|
+
self._title_loc = None
|
|
772
|
+
self._title_pad = rc["title.pad"]
|
|
773
|
+
self._title_pad_current = None
|
|
774
|
+
self._altx_parent = None # for cartesian axes only
|
|
775
|
+
self._alty_parent = None
|
|
776
|
+
self._colorbar_fill = None
|
|
777
|
+
self._inset_parent = None
|
|
778
|
+
self._inset_bounds = None # for introspection ony
|
|
779
|
+
self._inset_zoom = False
|
|
780
|
+
self._inset_zoom_artists = None
|
|
781
|
+
self._panel_hidden = False # True when "filled" with cbar/legend
|
|
782
|
+
self._panel_align = {} # store 'align' and 'length' for "filled" cbar/legend
|
|
783
|
+
self._panel_parent = None
|
|
784
|
+
self._panel_share = False
|
|
785
|
+
self._panel_sharex_group = False # see _apply_auto_share
|
|
786
|
+
self._panel_sharey_group = False # see _apply_auto_share
|
|
787
|
+
self._panel_side = None
|
|
788
|
+
self._tight_bbox = None # bounding boxes are saved
|
|
789
|
+
self.xaxis.isDefault_minloc = True # ensure enabled at start (needed for dual)
|
|
790
|
+
self.yaxis.isDefault_minloc = True
|
|
791
|
+
|
|
792
|
+
# Various dictionary properties
|
|
793
|
+
# NOTE: Critical to use self.text() so they are patched with _update_label
|
|
794
|
+
self._legend_dict = {}
|
|
795
|
+
self._colorbar_dict = {}
|
|
796
|
+
d = self._panel_dict = {}
|
|
797
|
+
d["left"] = [] # NOTE: panels will be sorted inside-to-outside
|
|
798
|
+
d["right"] = []
|
|
799
|
+
d["bottom"] = []
|
|
800
|
+
d["top"] = []
|
|
801
|
+
d = self._title_dict = {}
|
|
802
|
+
kw = {"zorder": 3.5, "transform": self.transAxes}
|
|
803
|
+
d["abc"] = self.text(0, 0, "", **kw)
|
|
804
|
+
d["left"] = self._left_title # WARNING: track in case mpl changes this
|
|
805
|
+
d["center"] = self.title
|
|
806
|
+
d["right"] = self._right_title
|
|
807
|
+
d["upper left"] = self.text(0, 0, "", va="top", ha="left", **kw)
|
|
808
|
+
d["upper center"] = self.text(0, 0.5, "", va="top", ha="center", **kw)
|
|
809
|
+
d["upper right"] = self.text(0, 1, "", va="top", ha="right", **kw)
|
|
810
|
+
d["lower left"] = self.text(0, 0, "", va="bottom", ha="left", **kw)
|
|
811
|
+
d["lower center"] = self.text(0, 0.5, "", va="bottom", ha="center", **kw)
|
|
812
|
+
d["lower right"] = self.text(0, 1, "", va="bottom", ha="right", **kw)
|
|
813
|
+
|
|
814
|
+
# Subplot-specific settings
|
|
815
|
+
# NOTE: Default number for any axes is None (i.e., no a-b-c labels allowed)
|
|
816
|
+
# and for subplots added with add_subplot is incremented automatically
|
|
817
|
+
# WARNING: For mpl>=3.4.0 subplotspec assigned *after* initialization using
|
|
818
|
+
# set_subplotspec. Tried to defer to setter but really messes up both format()
|
|
819
|
+
# and _apply_auto_share(). Instead use workaround: Have Figure.add_subplot pass
|
|
820
|
+
# subplotspec as a hidden keyword arg. Non-subplots don't need this arg.
|
|
821
|
+
# See: https://github.com/matplotlib/matplotlib/pull/18564
|
|
822
|
+
self._number = None
|
|
823
|
+
if number: # not None or False
|
|
824
|
+
self.number = number
|
|
825
|
+
if ss is not None: # always passed from add_subplot
|
|
826
|
+
self.set_subplotspec(ss)
|
|
827
|
+
if autoshare:
|
|
828
|
+
self._apply_auto_share()
|
|
829
|
+
|
|
830
|
+
# Default formatting
|
|
831
|
+
# NOTE: This ignores user-input rc_mode. Mode '1' applies ultraplot
|
|
832
|
+
# features which is necessary on first run. Default otherwise is mode '2'
|
|
833
|
+
self.format(rc_kw=rc_kw, rc_mode=1, skip_figure=True, **kw_format)
|
|
834
|
+
|
|
835
|
+
def _add_inset_axes(
|
|
836
|
+
self,
|
|
837
|
+
bounds,
|
|
838
|
+
transform=None,
|
|
839
|
+
*,
|
|
840
|
+
proj=None,
|
|
841
|
+
projection=None,
|
|
842
|
+
zoom=None,
|
|
843
|
+
zoom_kw=None,
|
|
844
|
+
zorder=None,
|
|
845
|
+
**kwargs,
|
|
846
|
+
):
|
|
847
|
+
"""
|
|
848
|
+
Add an inset axes using arbitrary projection.
|
|
849
|
+
"""
|
|
850
|
+
# Converting transform to figure-relative coordinates
|
|
851
|
+
transform = self._get_transform(transform, "axes")
|
|
852
|
+
locator = self._make_inset_locator(bounds, transform)
|
|
853
|
+
bounds = locator(self, None).bounds
|
|
854
|
+
label = kwargs.pop("label", "inset_axes")
|
|
855
|
+
zorder = _not_none(zorder, 4)
|
|
856
|
+
|
|
857
|
+
# Parse projection and inherit from the current axes by default
|
|
858
|
+
# NOTE: The _parse_proj method also accepts axes classes.
|
|
859
|
+
proj = _not_none(proj=proj, projection=projection)
|
|
860
|
+
if proj is None:
|
|
861
|
+
if self._name in ("cartopy", "basemap"):
|
|
862
|
+
proj = copy.copy(self.projection)
|
|
863
|
+
else:
|
|
864
|
+
proj = self._name
|
|
865
|
+
kwargs = self.figure._parse_proj(proj, **kwargs)
|
|
866
|
+
|
|
867
|
+
# Create axes and apply locator. The locator lets the axes adjust
|
|
868
|
+
# automatically if we used data coords. Called by ax.apply_aspect()
|
|
869
|
+
cls = mproj.get_projection_class(kwargs.pop("projection"))
|
|
870
|
+
ax = cls(self.figure, bounds, zorder=zorder, label=label, **kwargs)
|
|
871
|
+
ax.set_axes_locator(locator)
|
|
872
|
+
ax._inset_parent = self
|
|
873
|
+
ax._inset_bounds = bounds
|
|
874
|
+
self.add_child_axes(ax)
|
|
875
|
+
|
|
876
|
+
# Add zoom indicator (NOTE: requires matplotlib >= 3.0)
|
|
877
|
+
zoom_default = self._name == "cartesian" and ax._name == "cartesian"
|
|
878
|
+
zoom = ax._inset_zoom = _not_none(zoom, zoom_default)
|
|
879
|
+
if zoom:
|
|
880
|
+
zoom_kw = zoom_kw or {}
|
|
881
|
+
ax.indicate_inset_zoom(**zoom_kw)
|
|
882
|
+
return ax
|
|
883
|
+
|
|
884
|
+
def _add_queued_guides(self):
|
|
885
|
+
"""
|
|
886
|
+
Draw the queued-up legends and colorbars. Wrapper funcs and legend func let
|
|
887
|
+
user add handles to location lists with successive calls.
|
|
888
|
+
"""
|
|
889
|
+
# Draw queued colorbars
|
|
890
|
+
for (loc, align), colorbar in tuple(self._colorbar_dict.items()):
|
|
891
|
+
if not isinstance(colorbar, tuple):
|
|
892
|
+
continue
|
|
893
|
+
handles, labels, kwargs = colorbar
|
|
894
|
+
cb = self._add_colorbar(handles, labels, loc=loc, align=align, **kwargs)
|
|
895
|
+
self._colorbar_dict[(loc, align)] = cb
|
|
896
|
+
|
|
897
|
+
# Draw queued legends
|
|
898
|
+
# WARNING: Passing empty list labels=[] to legend causes matplotlib
|
|
899
|
+
# _parse_legend_args to search for everything. Ensure None if empty.
|
|
900
|
+
for (loc, align), legend in tuple(self._legend_dict.items()):
|
|
901
|
+
if not isinstance(legend, tuple) or any(
|
|
902
|
+
isinstance(_, mlegend.Legend) for _ in legend
|
|
903
|
+
): # noqa: E501
|
|
904
|
+
continue
|
|
905
|
+
handles, labels, kwargs = legend
|
|
906
|
+
leg = self._add_legend(handles, labels, loc=loc, align=align, **kwargs)
|
|
907
|
+
self._legend_dict[(loc, align)] = leg
|
|
908
|
+
|
|
909
|
+
def _add_guide_frame(
|
|
910
|
+
self, xmin, ymin, width, height, *, fontsize, fancybox=None, **kwargs
|
|
911
|
+
):
|
|
912
|
+
"""
|
|
913
|
+
Add a colorbar or multilegend frame.
|
|
914
|
+
"""
|
|
915
|
+
# TODO: Shadow patch does not seem to work. Unsure why.
|
|
916
|
+
# TODO: Add basic 'colorbar' and 'legend' artists with
|
|
917
|
+
# shared control over background frame.
|
|
918
|
+
shadow = kwargs.pop("shadow", None) # noqa: F841
|
|
919
|
+
renderer = self.figure._get_renderer()
|
|
920
|
+
fontsize = _fontsize_to_pt(fontsize)
|
|
921
|
+
fontsize = (fontsize / 72) / self._get_size_inches()[0] # axes relative units
|
|
922
|
+
fontsize = renderer.points_to_pixels(fontsize)
|
|
923
|
+
patch = mpatches.FancyBboxPatch(
|
|
924
|
+
(xmin, ymin),
|
|
925
|
+
width,
|
|
926
|
+
height,
|
|
927
|
+
snap=True,
|
|
928
|
+
zorder=4.5,
|
|
929
|
+
mutation_scale=fontsize,
|
|
930
|
+
transform=self.transAxes,
|
|
931
|
+
)
|
|
932
|
+
patch.set_clip_on(False)
|
|
933
|
+
if fancybox:
|
|
934
|
+
patch.set_boxstyle("round", pad=0, rounding_size=0.2)
|
|
935
|
+
else:
|
|
936
|
+
patch.set_boxstyle("square", pad=0)
|
|
937
|
+
patch.update(kwargs)
|
|
938
|
+
self.add_artist(patch)
|
|
939
|
+
return patch
|
|
940
|
+
|
|
941
|
+
def _add_guide_panel(self, loc="fill", align="center", length=0, **kwargs):
|
|
942
|
+
"""
|
|
943
|
+
Add a panel to be filled by an "outer" colorbar or legend.
|
|
944
|
+
"""
|
|
945
|
+
# NOTE: For colorbars we include 'length' when determining whether to allocate
|
|
946
|
+
# new panel but for legend just test whether that 'align' position was filled.
|
|
947
|
+
# WARNING: Hide content but 1) do not use ax.set_visible(False) so that
|
|
948
|
+
# tight layout will include legend and colorbar and 2) do not use
|
|
949
|
+
# ax.clear() so that top panel title and a-b-c label can remain.
|
|
950
|
+
bbox = _align_bbox(align, length)
|
|
951
|
+
if loc == "fill":
|
|
952
|
+
ax = self
|
|
953
|
+
elif loc in ("left", "right", "top", "bottom"):
|
|
954
|
+
ax = None
|
|
955
|
+
for pax in self._panel_dict[loc]:
|
|
956
|
+
if not pax._panel_hidden or align in pax._panel_align:
|
|
957
|
+
continue
|
|
958
|
+
if not any(bbox.overlaps(b) for b in pax._panel_align.values()):
|
|
959
|
+
ax = pax
|
|
960
|
+
break
|
|
961
|
+
if ax is None:
|
|
962
|
+
ax = self.panel_axes(loc, filled=True, **kwargs)
|
|
963
|
+
else:
|
|
964
|
+
raise ValueError(f"Invalid filled panel location {loc!r}.")
|
|
965
|
+
for s in ax.spines.values():
|
|
966
|
+
s.set_visible(False)
|
|
967
|
+
ax.xaxis.set_visible(False)
|
|
968
|
+
ax.yaxis.set_visible(False)
|
|
969
|
+
ax.patch.set_facecolor("none")
|
|
970
|
+
ax._panel_hidden = True
|
|
971
|
+
ax._panel_align[align] = bbox
|
|
972
|
+
return ax
|
|
973
|
+
|
|
974
|
+
@warnings._rename_kwargs("0.10", rasterize="rasterized")
|
|
975
|
+
def _add_colorbar(
|
|
976
|
+
self,
|
|
977
|
+
mappable,
|
|
978
|
+
values=None,
|
|
979
|
+
*,
|
|
980
|
+
loc=None,
|
|
981
|
+
align=None,
|
|
982
|
+
space=None,
|
|
983
|
+
pad=None,
|
|
984
|
+
width=None,
|
|
985
|
+
length=None,
|
|
986
|
+
shrink=None,
|
|
987
|
+
label=None,
|
|
988
|
+
title=None,
|
|
989
|
+
reverse=False,
|
|
990
|
+
rotation=None,
|
|
991
|
+
grid=None,
|
|
992
|
+
edges=None,
|
|
993
|
+
drawedges=None,
|
|
994
|
+
extend=None,
|
|
995
|
+
extendsize=None,
|
|
996
|
+
extendfrac=None,
|
|
997
|
+
ticks=None,
|
|
998
|
+
locator=None,
|
|
999
|
+
locator_kw=None,
|
|
1000
|
+
format=None,
|
|
1001
|
+
formatter=None,
|
|
1002
|
+
ticklabels=None,
|
|
1003
|
+
formatter_kw=None,
|
|
1004
|
+
minorticks=None,
|
|
1005
|
+
minorlocator=None,
|
|
1006
|
+
minorlocator_kw=None,
|
|
1007
|
+
tickminor=None,
|
|
1008
|
+
ticklen=None,
|
|
1009
|
+
ticklenratio=None,
|
|
1010
|
+
tickdir=None,
|
|
1011
|
+
tickdirection=None,
|
|
1012
|
+
tickwidth=None,
|
|
1013
|
+
tickwidthratio=None,
|
|
1014
|
+
ticklabelsize=None,
|
|
1015
|
+
ticklabelweight=None,
|
|
1016
|
+
ticklabelcolor=None,
|
|
1017
|
+
labelloc=None,
|
|
1018
|
+
labellocation=None,
|
|
1019
|
+
labelsize=None,
|
|
1020
|
+
labelweight=None,
|
|
1021
|
+
labelcolor=None,
|
|
1022
|
+
c=None,
|
|
1023
|
+
color=None,
|
|
1024
|
+
lw=None,
|
|
1025
|
+
linewidth=None,
|
|
1026
|
+
edgefix=None,
|
|
1027
|
+
rasterized=None,
|
|
1028
|
+
**kwargs,
|
|
1029
|
+
):
|
|
1030
|
+
"""
|
|
1031
|
+
The driver function for adding axes colorbars.
|
|
1032
|
+
"""
|
|
1033
|
+
# Parse input arguments and apply defaults
|
|
1034
|
+
# TODO: Get the 'best' inset colorbar location using the legend algorithm
|
|
1035
|
+
# and implement inset colorbars the same as inset legends.
|
|
1036
|
+
grid = _not_none(
|
|
1037
|
+
grid=grid, edges=edges, drawedges=drawedges, default=rc["colorbar.grid"]
|
|
1038
|
+
) # noqa: E501
|
|
1039
|
+
length = _not_none(length=length, shrink=shrink)
|
|
1040
|
+
label = _not_none(title=title, label=label)
|
|
1041
|
+
labelloc = _not_none(labelloc=labelloc, labellocation=labellocation)
|
|
1042
|
+
locator = _not_none(ticks=ticks, locator=locator)
|
|
1043
|
+
formatter = _not_none(ticklabels=ticklabels, formatter=formatter, format=format)
|
|
1044
|
+
minorlocator = _not_none(minorticks=minorticks, minorlocator=minorlocator)
|
|
1045
|
+
color = _not_none(c=c, color=color, default=rc["axes.edgecolor"])
|
|
1046
|
+
linewidth = _not_none(lw=lw, linewidth=linewidth)
|
|
1047
|
+
ticklen = units(_not_none(ticklen, rc["tick.len"]), "pt")
|
|
1048
|
+
tickdir = _not_none(tickdir=tickdir, tickdirection=tickdirection)
|
|
1049
|
+
tickwidth = units(_not_none(tickwidth, linewidth, rc["tick.width"]), "pt")
|
|
1050
|
+
linewidth = units(_not_none(linewidth, default=rc["axes.linewidth"]), "pt")
|
|
1051
|
+
ticklenratio = _not_none(ticklenratio, rc["tick.lenratio"])
|
|
1052
|
+
tickwidthratio = _not_none(tickwidthratio, rc["tick.widthratio"])
|
|
1053
|
+
rasterized = _not_none(rasterized, rc["colorbar.rasterized"])
|
|
1054
|
+
|
|
1055
|
+
# Build label and locator keyword argument dicts
|
|
1056
|
+
# NOTE: This carefully handles the 'maxn' and 'maxn_minor' deprecations
|
|
1057
|
+
kw_label = {}
|
|
1058
|
+
locator_kw = locator_kw or {}
|
|
1059
|
+
formatter_kw = formatter_kw or {}
|
|
1060
|
+
minorlocator_kw = minorlocator_kw or {}
|
|
1061
|
+
for key, value in (
|
|
1062
|
+
("size", labelsize),
|
|
1063
|
+
("weight", labelweight),
|
|
1064
|
+
("color", labelcolor),
|
|
1065
|
+
):
|
|
1066
|
+
if value is not None:
|
|
1067
|
+
kw_label[key] = value
|
|
1068
|
+
kw_ticklabels = {}
|
|
1069
|
+
for key, value in (
|
|
1070
|
+
("size", ticklabelsize),
|
|
1071
|
+
("weight", ticklabelweight),
|
|
1072
|
+
("color", ticklabelcolor),
|
|
1073
|
+
("rotation", rotation),
|
|
1074
|
+
):
|
|
1075
|
+
if value is not None:
|
|
1076
|
+
kw_ticklabels[key] = value
|
|
1077
|
+
for b, kw in enumerate((locator_kw, minorlocator_kw)):
|
|
1078
|
+
key = "maxn_minor" if b else "maxn"
|
|
1079
|
+
name = "minorlocator" if b else "locator"
|
|
1080
|
+
nbins = kwargs.pop("maxn_minor" if b else "maxn", None)
|
|
1081
|
+
if nbins is not None:
|
|
1082
|
+
kw["nbins"] = nbins
|
|
1083
|
+
warnings._warn_ultraplot(
|
|
1084
|
+
f"The colorbar() keyword {key!r} was deprecated in v0.10. To "
|
|
1085
|
+
"achieve the same effect, you can pass 'nbins' to the new default "
|
|
1086
|
+
f"locator DiscreteLocator using {name}_kw={{'nbins': {nbins}}}."
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
# Generate and prepare the colorbar axes
|
|
1090
|
+
# NOTE: The inset axes function needs 'label' to know how to pad the box
|
|
1091
|
+
# TODO: Use seperate keywords for frame properties vs. colorbar edge properties?
|
|
1092
|
+
if loc in ("fill", "left", "right", "top", "bottom"):
|
|
1093
|
+
length = _not_none(length, rc["colorbar.length"]) # for _add_guide_panel
|
|
1094
|
+
kwargs.update({"align": align, "length": length})
|
|
1095
|
+
extendsize = _not_none(extendsize, rc["colorbar.extend"])
|
|
1096
|
+
ax = self._add_guide_panel(
|
|
1097
|
+
loc, align, length=length, width=width, space=space, pad=pad
|
|
1098
|
+
) # noqa: E501
|
|
1099
|
+
cax, kwargs = ax._parse_colorbar_filled(**kwargs)
|
|
1100
|
+
else:
|
|
1101
|
+
kwargs.update({"label": label, "length": length, "width": width})
|
|
1102
|
+
extendsize = _not_none(extendsize, rc["colorbar.insetextend"])
|
|
1103
|
+
cax, kwargs = self._parse_colorbar_inset(
|
|
1104
|
+
loc=loc, pad=pad, **kwargs
|
|
1105
|
+
) # noqa: E501
|
|
1106
|
+
|
|
1107
|
+
# Parse the colorbar mappable
|
|
1108
|
+
# NOTE: Account for special case where auto colorbar is generated from 1D
|
|
1109
|
+
# methods that construct an 'artist list' (i.e. colormap scatter object)
|
|
1110
|
+
if (
|
|
1111
|
+
np.iterable(mappable)
|
|
1112
|
+
and len(mappable) == 1
|
|
1113
|
+
and isinstance(mappable[0], mcm.ScalarMappable)
|
|
1114
|
+
): # noqa: E501
|
|
1115
|
+
mappable = mappable[0]
|
|
1116
|
+
if not isinstance(mappable, mcm.ScalarMappable):
|
|
1117
|
+
mappable, kwargs = cax._parse_colorbar_arg(mappable, values, **kwargs)
|
|
1118
|
+
else:
|
|
1119
|
+
pop = _pop_params(kwargs, cax._parse_colorbar_arg, ignore_internal=True)
|
|
1120
|
+
if pop:
|
|
1121
|
+
warnings._warn_ultraplot(
|
|
1122
|
+
f"Input is already a ScalarMappable. "
|
|
1123
|
+
f"Ignoring unused keyword arg(s): {pop}"
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
# Parse 'extendsize' and 'extendfrac' keywords
|
|
1127
|
+
# TODO: Make this auto-adjust to the subplot size
|
|
1128
|
+
vert = kwargs["orientation"] == "vertical"
|
|
1129
|
+
if extendsize is not None and extendfrac is not None:
|
|
1130
|
+
warnings._warn_ultraplot(
|
|
1131
|
+
f"You cannot specify both an absolute extendsize={extendsize!r} "
|
|
1132
|
+
f"and a relative extendfrac={extendfrac!r}. Ignoring 'extendfrac'."
|
|
1133
|
+
)
|
|
1134
|
+
extendfrac = None
|
|
1135
|
+
if extendfrac is None:
|
|
1136
|
+
width, height = cax._get_size_inches()
|
|
1137
|
+
scale = height if vert else width
|
|
1138
|
+
extendsize = units(extendsize, "em", "in")
|
|
1139
|
+
extendfrac = extendsize / max(scale - 2 * extendsize, units(1, "em", "in"))
|
|
1140
|
+
|
|
1141
|
+
# Parse the tick locators and formatters
|
|
1142
|
+
# NOTE: In presence of BoundaryNorm or similar handle ticks with special
|
|
1143
|
+
# DiscreteLocator or else get issues (see mpl #22233).
|
|
1144
|
+
norm = mappable.norm
|
|
1145
|
+
formatter = _not_none(formatter, getattr(norm, "_labels", None), "auto")
|
|
1146
|
+
formatter = constructor.Formatter(formatter, **formatter_kw)
|
|
1147
|
+
categorical = isinstance(formatter, mticker.FixedFormatter)
|
|
1148
|
+
if locator is not None:
|
|
1149
|
+
locator = constructor.Locator(locator, **locator_kw)
|
|
1150
|
+
if minorlocator is not None: # overrides tickminor
|
|
1151
|
+
minorlocator = constructor.Locator(minorlocator, **minorlocator_kw)
|
|
1152
|
+
elif tickminor is None:
|
|
1153
|
+
tickminor = False if categorical else rc["xy"[vert] + "tick.minor.visible"]
|
|
1154
|
+
if isinstance(norm, mcolors.BoundaryNorm): # DiscreteNorm or BoundaryNorm
|
|
1155
|
+
ticks = getattr(norm, "_ticks", norm.boundaries)
|
|
1156
|
+
segmented = isinstance(getattr(norm, "_norm", None), pcolors.SegmentedNorm)
|
|
1157
|
+
if locator is None:
|
|
1158
|
+
if categorical or segmented:
|
|
1159
|
+
locator = mticker.FixedLocator(ticks)
|
|
1160
|
+
else:
|
|
1161
|
+
locator = pticker.DiscreteLocator(ticks)
|
|
1162
|
+
if tickminor and minorlocator is None:
|
|
1163
|
+
minorlocator = pticker.DiscreteLocator(ticks, minor=True)
|
|
1164
|
+
|
|
1165
|
+
# Special handling for colorbar keyword arguments
|
|
1166
|
+
# WARNING: Critical to not pass empty major locators in matplotlib < 3.5
|
|
1167
|
+
# See this issue: https://github.com/ultraplot-dev/ultraplot/issues/301
|
|
1168
|
+
# WARNING: ultraplot 'supports' passing one extend to a mappable function
|
|
1169
|
+
# then overwriting by passing another 'extend' to colobar. But contour
|
|
1170
|
+
# colorbars break when you try to change its 'extend'. Matplotlib gets
|
|
1171
|
+
# around this by just silently ignoring 'extend' passed to colorbar() but
|
|
1172
|
+
# we issue warning. Also note ContourSet.extend existed in matplotlib 3.0.
|
|
1173
|
+
# WARNING: Confusingly the only default way to have auto-adjusting
|
|
1174
|
+
# colorbar ticks is to specify no locator. Then _get_ticker_locator_formatter
|
|
1175
|
+
# uses the default ScalarFormatter on the axis that already has a set axis.
|
|
1176
|
+
# Otherwise it sets a default axis with locator.create_dummy_axis() in
|
|
1177
|
+
# update_ticks() which does not track axis size. Workaround is to manually
|
|
1178
|
+
# set the locator and formatter axis... however this messes up colorbar lengths
|
|
1179
|
+
# in matplotlib < 3.2. So we only apply this conditionally and in earlier
|
|
1180
|
+
# verisons recognize that DiscreteLocator will behave like FixedLocator.
|
|
1181
|
+
axis = cax.yaxis if vert else cax.xaxis
|
|
1182
|
+
if not isinstance(mappable, mcontour.ContourSet):
|
|
1183
|
+
extend = _not_none(extend, "neither")
|
|
1184
|
+
kwargs["extend"] = extend
|
|
1185
|
+
elif extend is not None and extend != mappable.extend:
|
|
1186
|
+
warnings._warn_ultraplot(
|
|
1187
|
+
"Ignoring extend={extend!r}. ContourSet extend cannot be changed."
|
|
1188
|
+
)
|
|
1189
|
+
if (
|
|
1190
|
+
isinstance(locator, mticker.NullLocator)
|
|
1191
|
+
or hasattr(locator, "locs")
|
|
1192
|
+
and len(locator.locs) == 0
|
|
1193
|
+
):
|
|
1194
|
+
minorlocator, tickminor = None, False # attempted fix
|
|
1195
|
+
for ticker in (locator, formatter, minorlocator):
|
|
1196
|
+
if _version_mpl < "3.2":
|
|
1197
|
+
pass # see notes above
|
|
1198
|
+
elif isinstance(ticker, mticker.TickHelper):
|
|
1199
|
+
ticker.set_axis(axis)
|
|
1200
|
+
|
|
1201
|
+
# Create colorbar and update ticks and axis direction
|
|
1202
|
+
# NOTE: This also adds the guides._update_ticks() monkey patch that triggers
|
|
1203
|
+
# updates to DiscreteLocator when parent axes is drawn.
|
|
1204
|
+
obj = cax._colorbar_fill = cax.figure.colorbar(
|
|
1205
|
+
mappable,
|
|
1206
|
+
cax=cax,
|
|
1207
|
+
ticks=locator,
|
|
1208
|
+
format=formatter,
|
|
1209
|
+
drawedges=grid,
|
|
1210
|
+
extendfrac=extendfrac,
|
|
1211
|
+
**kwargs,
|
|
1212
|
+
)
|
|
1213
|
+
# obj.minorlocator = minorlocator # backwards compatibility
|
|
1214
|
+
obj.update_ticks = guides._update_ticks.__get__(obj) # backwards compatible
|
|
1215
|
+
if minorlocator is not None:
|
|
1216
|
+
obj.update_ticks()
|
|
1217
|
+
elif tickminor:
|
|
1218
|
+
obj.minorticks_on()
|
|
1219
|
+
else:
|
|
1220
|
+
obj.minorticks_off()
|
|
1221
|
+
if getattr(norm, "descending", None):
|
|
1222
|
+
axis.set_inverted(True)
|
|
1223
|
+
if reverse: # potentially double reverse, although that would be weird...
|
|
1224
|
+
axis.set_inverted(True)
|
|
1225
|
+
|
|
1226
|
+
# Update other colorbar settings
|
|
1227
|
+
# WARNING: Must use the colorbar set_label to set text. Calling set_label
|
|
1228
|
+
# on the actual axis will do nothing!
|
|
1229
|
+
axis.set_tick_params(which="both", color=color, direction=tickdir)
|
|
1230
|
+
axis.set_tick_params(which="major", length=ticklen, width=tickwidth)
|
|
1231
|
+
axis.set_tick_params(
|
|
1232
|
+
which="minor",
|
|
1233
|
+
length=ticklen * ticklenratio,
|
|
1234
|
+
width=tickwidth * tickwidthratio,
|
|
1235
|
+
) # noqa: E501
|
|
1236
|
+
if label is not None:
|
|
1237
|
+
obj.set_label(label)
|
|
1238
|
+
if labelloc is not None:
|
|
1239
|
+
axis.set_label_position(labelloc)
|
|
1240
|
+
axis.label.update(kw_label)
|
|
1241
|
+
for label in axis.get_ticklabels():
|
|
1242
|
+
label.update(kw_ticklabels)
|
|
1243
|
+
kw_outline = {"edgecolor": color, "linewidth": linewidth}
|
|
1244
|
+
if obj.outline is not None:
|
|
1245
|
+
obj.outline.update(kw_outline)
|
|
1246
|
+
if obj.dividers is not None:
|
|
1247
|
+
obj.dividers.update(kw_outline)
|
|
1248
|
+
if obj.solids:
|
|
1249
|
+
from . import PlotAxes
|
|
1250
|
+
|
|
1251
|
+
obj.solids.set_rasterized(rasterized)
|
|
1252
|
+
PlotAxes._fix_patch_edges(obj.solids, edgefix=edgefix)
|
|
1253
|
+
|
|
1254
|
+
# Register location and return
|
|
1255
|
+
self._register_guide("colorbar", obj, (loc, align)) # possibly replace another
|
|
1256
|
+
return obj
|
|
1257
|
+
|
|
1258
|
+
def _add_legend(
|
|
1259
|
+
self,
|
|
1260
|
+
handles=None,
|
|
1261
|
+
labels=None,
|
|
1262
|
+
*,
|
|
1263
|
+
loc=None,
|
|
1264
|
+
align=None,
|
|
1265
|
+
width=None,
|
|
1266
|
+
pad=None,
|
|
1267
|
+
space=None,
|
|
1268
|
+
frame=None,
|
|
1269
|
+
frameon=None,
|
|
1270
|
+
ncol=None,
|
|
1271
|
+
ncols=None,
|
|
1272
|
+
alphabetize=False,
|
|
1273
|
+
center=None,
|
|
1274
|
+
order=None,
|
|
1275
|
+
label=None,
|
|
1276
|
+
title=None,
|
|
1277
|
+
fontsize=None,
|
|
1278
|
+
fontweight=None,
|
|
1279
|
+
fontcolor=None,
|
|
1280
|
+
titlefontsize=None,
|
|
1281
|
+
titlefontweight=None,
|
|
1282
|
+
titlefontcolor=None,
|
|
1283
|
+
handle_kw=None,
|
|
1284
|
+
handler_map=None,
|
|
1285
|
+
**kwargs,
|
|
1286
|
+
):
|
|
1287
|
+
"""
|
|
1288
|
+
The driver function for adding axes legends.
|
|
1289
|
+
"""
|
|
1290
|
+
# Parse input argument units
|
|
1291
|
+
ncol = _not_none(ncols=ncols, ncol=ncol)
|
|
1292
|
+
order = _not_none(order, "C")
|
|
1293
|
+
frameon = _not_none(frame=frame, frameon=frameon, default=rc["legend.frameon"])
|
|
1294
|
+
fontsize = _not_none(fontsize, rc["legend.fontsize"])
|
|
1295
|
+
titlefontsize = _not_none(
|
|
1296
|
+
title_fontsize=kwargs.pop("title_fontsize", None),
|
|
1297
|
+
titlefontsize=titlefontsize,
|
|
1298
|
+
default=rc["legend.title_fontsize"],
|
|
1299
|
+
)
|
|
1300
|
+
fontsize = _fontsize_to_pt(fontsize)
|
|
1301
|
+
titlefontsize = _fontsize_to_pt(titlefontsize)
|
|
1302
|
+
if order not in ("F", "C"):
|
|
1303
|
+
raise ValueError(
|
|
1304
|
+
f"Invalid order {order!r}. Please choose from "
|
|
1305
|
+
"'C' (row-major, default) or 'F' (column-major)."
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
# Convert relevant keys to em-widths
|
|
1309
|
+
for setting in rcsetup.EM_KEYS: # em-width keys
|
|
1310
|
+
pair = setting.split("legend.", 1)
|
|
1311
|
+
if len(pair) == 1:
|
|
1312
|
+
continue
|
|
1313
|
+
_, key = pair
|
|
1314
|
+
value = kwargs.pop(key, None)
|
|
1315
|
+
if isinstance(value, str):
|
|
1316
|
+
value = units(kwargs[key], "em", fontsize=fontsize)
|
|
1317
|
+
if value is not None:
|
|
1318
|
+
kwargs[key] = value
|
|
1319
|
+
|
|
1320
|
+
# Generate and prepare the legend axes
|
|
1321
|
+
if loc in ("fill", "left", "right", "top", "bottom"):
|
|
1322
|
+
lax = self._add_guide_panel(loc, align, width=width, space=space, pad=pad)
|
|
1323
|
+
kwargs.setdefault("borderaxespad", 0)
|
|
1324
|
+
if not frameon:
|
|
1325
|
+
kwargs.setdefault("borderpad", 0)
|
|
1326
|
+
try:
|
|
1327
|
+
kwargs["loc"] = ALIGN_OPTS[lax._panel_side][align]
|
|
1328
|
+
except KeyError:
|
|
1329
|
+
raise ValueError(f"Invalid align={align!r} for legend loc={loc!r}.")
|
|
1330
|
+
else:
|
|
1331
|
+
lax = self
|
|
1332
|
+
pad = kwargs.pop("borderaxespad", pad)
|
|
1333
|
+
kwargs["loc"] = loc # simply pass to legend
|
|
1334
|
+
kwargs["borderaxespad"] = units(pad, "em", fontsize=fontsize)
|
|
1335
|
+
|
|
1336
|
+
# Handle and text properties that are applied after-the-fact
|
|
1337
|
+
# NOTE: Set solid_capstyle to 'butt' so line does not extend past error bounds
|
|
1338
|
+
# shading in legend entry. This change is not noticable in other situations.
|
|
1339
|
+
kw_frame, kwargs = lax._parse_frame("legend", **kwargs)
|
|
1340
|
+
kw_text = {}
|
|
1341
|
+
if fontcolor is not None:
|
|
1342
|
+
kw_text["color"] = fontcolor
|
|
1343
|
+
if fontweight is not None:
|
|
1344
|
+
kw_text["weight"] = fontweight
|
|
1345
|
+
kw_title = {}
|
|
1346
|
+
if titlefontcolor is not None:
|
|
1347
|
+
kw_title["color"] = titlefontcolor
|
|
1348
|
+
if titlefontweight is not None:
|
|
1349
|
+
kw_title["weight"] = titlefontweight
|
|
1350
|
+
kw_handle = _pop_props(kwargs, "line")
|
|
1351
|
+
kw_handle.setdefault("solid_capstyle", "butt")
|
|
1352
|
+
kw_handle.update(handle_kw or {})
|
|
1353
|
+
|
|
1354
|
+
# Parse the legend arguments using axes for auto-handle detection
|
|
1355
|
+
# TODO: Update this when we no longer use "filled panels" for outer legends
|
|
1356
|
+
pairs, multi = lax._parse_legend_handles(
|
|
1357
|
+
handles,
|
|
1358
|
+
labels,
|
|
1359
|
+
ncol=ncol,
|
|
1360
|
+
order=order,
|
|
1361
|
+
center=center,
|
|
1362
|
+
alphabetize=alphabetize,
|
|
1363
|
+
handler_map=handler_map,
|
|
1364
|
+
)
|
|
1365
|
+
title = _not_none(label=label, title=title)
|
|
1366
|
+
kwargs.update(
|
|
1367
|
+
{
|
|
1368
|
+
"title": title,
|
|
1369
|
+
"frameon": frameon,
|
|
1370
|
+
"fontsize": fontsize,
|
|
1371
|
+
"handler_map": handler_map,
|
|
1372
|
+
"title_fontsize": titlefontsize,
|
|
1373
|
+
}
|
|
1374
|
+
)
|
|
1375
|
+
|
|
1376
|
+
# Add the legend and update patch properties
|
|
1377
|
+
# TODO: Add capacity for categorical labels in a single legend like seaborn
|
|
1378
|
+
# rather than manual handle overrides with multiple legends.
|
|
1379
|
+
if multi:
|
|
1380
|
+
objs = lax._parse_legend_centered(pairs, kw_frame=kw_frame, **kwargs)
|
|
1381
|
+
else:
|
|
1382
|
+
kwargs.update({key: kw_frame.pop(key) for key in ("shadow", "fancybox")})
|
|
1383
|
+
objs = [lax._parse_legend_aligned(pairs, ncol=ncol, order=order, **kwargs)]
|
|
1384
|
+
objs[0].legendPatch.update(kw_frame)
|
|
1385
|
+
for obj in objs:
|
|
1386
|
+
if hasattr(lax, "legend_") and lax.legend_ is None:
|
|
1387
|
+
lax.legend_ = obj # make first legend accessible with get_legend()
|
|
1388
|
+
else:
|
|
1389
|
+
lax.add_artist(obj)
|
|
1390
|
+
|
|
1391
|
+
# Update legend patch and elements
|
|
1392
|
+
# WARNING: legendHandles only contains the *first* artist per legend because
|
|
1393
|
+
# HandlerBase.legend_artist() called in Legend._init_legend_box() only
|
|
1394
|
+
# returns the first artist. Instead we try to iterate through offset boxes.
|
|
1395
|
+
for obj in objs:
|
|
1396
|
+
obj.set_clip_on(False) # needed for tight bounding box calculations
|
|
1397
|
+
box = getattr(obj, "_legend_handle_box", None)
|
|
1398
|
+
for obj in guides._iter_children(box):
|
|
1399
|
+
if isinstance(obj, mtext.Text):
|
|
1400
|
+
kw = kw_text
|
|
1401
|
+
else:
|
|
1402
|
+
kw = {
|
|
1403
|
+
key: val
|
|
1404
|
+
for key, val in kw_handle.items()
|
|
1405
|
+
if hasattr(obj, "set_" + key)
|
|
1406
|
+
} # noqa: E501
|
|
1407
|
+
if hasattr(obj, "set_sizes") and "markersize" in kw_handle:
|
|
1408
|
+
kw["sizes"] = np.atleast_1d(kw_handle["markersize"])
|
|
1409
|
+
obj.update(kw)
|
|
1410
|
+
|
|
1411
|
+
# Register location and return
|
|
1412
|
+
if isinstance(objs[0], mpatches.FancyBboxPatch):
|
|
1413
|
+
objs = objs[1:]
|
|
1414
|
+
obj = objs[0] if len(objs) == 1 else tuple(objs)
|
|
1415
|
+
self._register_guide("legend", obj, (loc, align)) # possibly replace another
|
|
1416
|
+
|
|
1417
|
+
return obj
|
|
1418
|
+
|
|
1419
|
+
def _apply_title_above(self):
|
|
1420
|
+
"""
|
|
1421
|
+
Change assignment of outer titles between main subplot and upper panels.
|
|
1422
|
+
This is called when a panel is created or `_update_title` is called.
|
|
1423
|
+
"""
|
|
1424
|
+
# NOTE: Similar to how _apply_axis_sharing() is called in _align_axis_labels()
|
|
1425
|
+
# this is called in _align_super_labels() so we get the correct offset.
|
|
1426
|
+
paxs = self._panel_dict["top"]
|
|
1427
|
+
if not paxs:
|
|
1428
|
+
return
|
|
1429
|
+
pax = paxs[-1]
|
|
1430
|
+
names = ("left", "center", "right")
|
|
1431
|
+
if self._abc_loc in names:
|
|
1432
|
+
names += ("abc",)
|
|
1433
|
+
if not self._title_above:
|
|
1434
|
+
return
|
|
1435
|
+
if pax._panel_hidden and self._title_above == "panels":
|
|
1436
|
+
return
|
|
1437
|
+
pax._title_pad = self._title_pad
|
|
1438
|
+
pax._abc_title_pad = self._abc_title_pad
|
|
1439
|
+
for name in names:
|
|
1440
|
+
labels._transfer_label(self._title_dict[name], pax._title_dict[name])
|
|
1441
|
+
|
|
1442
|
+
def _apply_auto_share(self):
|
|
1443
|
+
"""
|
|
1444
|
+
Automatically configure axis sharing based on the horizontal and
|
|
1445
|
+
vertical extent of subplots in the figure gridspec.
|
|
1446
|
+
"""
|
|
1447
|
+
|
|
1448
|
+
# Panel axes sharing, between main subplot and its panels
|
|
1449
|
+
# NOTE: _panel_share means "include this panel in the axis sharing group" while
|
|
1450
|
+
# _panel_sharex_group indicates the group itself and may include main axes
|
|
1451
|
+
def shared(paxs):
|
|
1452
|
+
return [pax for pax in paxs if not pax._panel_hidden and pax._panel_share]
|
|
1453
|
+
|
|
1454
|
+
# Internal axis sharing, share stacks of panels and main axes with each other
|
|
1455
|
+
# NOTE: This is called on the main axes whenver a panel is created.
|
|
1456
|
+
# NOTE: This block is why, even though we have figure-wide share[xy], we
|
|
1457
|
+
# still need the axes-specific _share[xy]_override attribute.
|
|
1458
|
+
if not self._panel_side: # this is a main axes
|
|
1459
|
+
# Top and bottom
|
|
1460
|
+
bottom = self
|
|
1461
|
+
paxs = shared(self._panel_dict["bottom"])
|
|
1462
|
+
if paxs:
|
|
1463
|
+
bottom = paxs[-1]
|
|
1464
|
+
bottom._panel_sharex_group = False
|
|
1465
|
+
for iax in (self, *paxs[:-1]):
|
|
1466
|
+
iax._panel_sharex_group = True
|
|
1467
|
+
iax._sharex_setup(bottom) # parent is bottom-most
|
|
1468
|
+
paxs = shared(self._panel_dict["top"])
|
|
1469
|
+
for iax in paxs:
|
|
1470
|
+
iax._panel_sharex_group = True
|
|
1471
|
+
iax._sharex_setup(bottom)
|
|
1472
|
+
# Left and right
|
|
1473
|
+
# NOTE: Order of panel lists is always inside-to-outside
|
|
1474
|
+
left = self
|
|
1475
|
+
paxs = shared(self._panel_dict["left"])
|
|
1476
|
+
if paxs:
|
|
1477
|
+
left = paxs[-1]
|
|
1478
|
+
left._panel_sharey_group = False
|
|
1479
|
+
for iax in (self, *paxs[:-1]):
|
|
1480
|
+
iax._panel_sharey_group = True
|
|
1481
|
+
iax._sharey_setup(left) # parent is left-most
|
|
1482
|
+
paxs = shared(self._panel_dict["right"])
|
|
1483
|
+
for iax in paxs:
|
|
1484
|
+
iax._panel_sharey_group = True
|
|
1485
|
+
iax._sharey_setup(left)
|
|
1486
|
+
|
|
1487
|
+
# External axes sharing, sometimes overrides panel axes sharing
|
|
1488
|
+
# Share x axes
|
|
1489
|
+
parent, *children = self._get_share_axes("x")
|
|
1490
|
+
for child in children:
|
|
1491
|
+
child._sharex_setup(parent)
|
|
1492
|
+
# Share y axes
|
|
1493
|
+
parent, *children = self._get_share_axes("y")
|
|
1494
|
+
for child in children:
|
|
1495
|
+
child._sharey_setup(parent)
|
|
1496
|
+
# Global sharing, use the reference subplot because why not
|
|
1497
|
+
ref = self.figure._subplot_dict.get(self.figure._refnum, None)
|
|
1498
|
+
if self is not ref:
|
|
1499
|
+
if self.figure._sharex > 3:
|
|
1500
|
+
self._sharex_setup(ref, labels=False)
|
|
1501
|
+
if self.figure._sharey > 3:
|
|
1502
|
+
self._sharey_setup(ref, labels=False)
|
|
1503
|
+
|
|
1504
|
+
def _artist_fully_clipped(self, artist):
|
|
1505
|
+
"""
|
|
1506
|
+
Return a boolean flag, ``True`` if the artist is clipped to the axes
|
|
1507
|
+
and can thus be skipped in layout calculations.
|
|
1508
|
+
"""
|
|
1509
|
+
clip_box = artist.get_clip_box()
|
|
1510
|
+
clip_path = artist.get_clip_path()
|
|
1511
|
+
types_noclip = (
|
|
1512
|
+
maxes.Axes,
|
|
1513
|
+
maxis.Axis,
|
|
1514
|
+
moffsetbox.AnnotationBbox,
|
|
1515
|
+
moffsetbox.OffsetBox,
|
|
1516
|
+
)
|
|
1517
|
+
return not isinstance(artist, types_noclip) and (
|
|
1518
|
+
artist.get_clip_on()
|
|
1519
|
+
and (clip_box is not None or clip_path is not None)
|
|
1520
|
+
and (clip_box is None or np.all(clip_box.extents == self.bbox.extents))
|
|
1521
|
+
and (
|
|
1522
|
+
clip_path is None
|
|
1523
|
+
or isinstance(clip_path, mtransforms.TransformedPatchPath)
|
|
1524
|
+
and clip_path._patch is self.patch
|
|
1525
|
+
)
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
def _get_legend_handles(self, handler_map=None):
|
|
1529
|
+
"""
|
|
1530
|
+
Internal implementation of matplotlib's ``get_legend_handles_labels``.
|
|
1531
|
+
"""
|
|
1532
|
+
if not self._panel_hidden: # this is a normal axes
|
|
1533
|
+
axs = [self]
|
|
1534
|
+
elif self._panel_parent: # this is an axes-wide legend
|
|
1535
|
+
axs = list(self._panel_parent._iter_axes(hidden=False, children=True))
|
|
1536
|
+
else: # this is a figure-wide legend
|
|
1537
|
+
axs = list(self.figure._iter_axes(hidden=False, children=True))
|
|
1538
|
+
handles = []
|
|
1539
|
+
handler_map_full = mlegend.Legend.get_default_handler_map()
|
|
1540
|
+
handler_map_full = handler_map_full.copy()
|
|
1541
|
+
handler_map_full.update(handler_map or {})
|
|
1542
|
+
for ax in axs:
|
|
1543
|
+
for attr in ("lines", "patches", "collections", "containers"):
|
|
1544
|
+
for handle in getattr(ax, attr, []): # guard against API changes
|
|
1545
|
+
label = handle.get_label()
|
|
1546
|
+
handler = mlegend.Legend.get_legend_handler(
|
|
1547
|
+
handler_map_full, handle
|
|
1548
|
+
) # noqa: E501
|
|
1549
|
+
if handler and label and label[0] != "_":
|
|
1550
|
+
handles.append(handle)
|
|
1551
|
+
return handles
|
|
1552
|
+
|
|
1553
|
+
def _get_share_axes(self, sx, panels=False):
|
|
1554
|
+
"""
|
|
1555
|
+
Return the axes whose horizontal or vertical extent in the main gridspec
|
|
1556
|
+
matches the horizontal or vertical extent of this axes.
|
|
1557
|
+
"""
|
|
1558
|
+
# NOTE: The lefmost or bottommost axes are at the start of the list.
|
|
1559
|
+
if not isinstance(self, maxes.SubplotBase):
|
|
1560
|
+
return [self]
|
|
1561
|
+
i = 0 if sx == "x" else 1
|
|
1562
|
+
sy = "y" if sx == "x" else "x"
|
|
1563
|
+
argfunc = np.argmax if sx == "x" else np.argmin
|
|
1564
|
+
irange = self._range_subplotspec(sx)
|
|
1565
|
+
axs = self.figure._iter_axes(hidden=False, children=False, panels=panels)
|
|
1566
|
+
axs = [ax for ax in axs if ax._range_subplotspec(sx) == irange]
|
|
1567
|
+
axs = list({self, *axs}) # self may be missing during initialization
|
|
1568
|
+
pax = axs.pop(argfunc([ax._range_subplotspec(sy)[i] for ax in axs]))
|
|
1569
|
+
return [pax, *axs] # return with leftmost or bottommost first
|
|
1570
|
+
|
|
1571
|
+
def _get_span_axes(self, side, panels=False):
|
|
1572
|
+
"""
|
|
1573
|
+
Return the axes whose left, right, top, or bottom sides abutt against
|
|
1574
|
+
the same row or column as this axes. Deflect to shared panels.
|
|
1575
|
+
"""
|
|
1576
|
+
if side not in ("left", "right", "bottom", "top"):
|
|
1577
|
+
raise ValueError(f"Invalid side {side!r}.")
|
|
1578
|
+
if not isinstance(self, maxes.SubplotBase):
|
|
1579
|
+
return [self]
|
|
1580
|
+
x, y = "xy" if side in ("left", "right") else "yx"
|
|
1581
|
+
idx = 0 if side in ("left", "top") else 1 # which side to test
|
|
1582
|
+
coord = self._range_subplotspec(x)[idx] # side for a particular axes
|
|
1583
|
+
axs = self.figure._iter_axes(hidden=False, children=False, panels=panels)
|
|
1584
|
+
axs = [ax for ax in axs if ax._range_subplotspec(x)[idx] == coord] or [self]
|
|
1585
|
+
out = []
|
|
1586
|
+
for ax in axs:
|
|
1587
|
+
other = getattr(ax, "_share" + y)
|
|
1588
|
+
if other and other._panel_parent: # this is a shared panel
|
|
1589
|
+
ax = other
|
|
1590
|
+
out.append(ax)
|
|
1591
|
+
return out
|
|
1592
|
+
|
|
1593
|
+
def _get_size_inches(self):
|
|
1594
|
+
"""
|
|
1595
|
+
Return the width and height of the axes in inches.
|
|
1596
|
+
"""
|
|
1597
|
+
width, height = self.figure.get_size_inches()
|
|
1598
|
+
bbox = self.get_position()
|
|
1599
|
+
width = width * abs(bbox.width)
|
|
1600
|
+
height = height * abs(bbox.height)
|
|
1601
|
+
return np.array([width, height])
|
|
1602
|
+
|
|
1603
|
+
def _get_topmost_axes(self):
|
|
1604
|
+
"""
|
|
1605
|
+
Return the topmost axes including panels and parents.
|
|
1606
|
+
"""
|
|
1607
|
+
for _ in range(5):
|
|
1608
|
+
self = self._axes or self
|
|
1609
|
+
self = self._panel_parent or self
|
|
1610
|
+
return self
|
|
1611
|
+
|
|
1612
|
+
def _get_transform(self, transform, default="data"):
|
|
1613
|
+
"""
|
|
1614
|
+
Translates user input transform. Also used in an axes method.
|
|
1615
|
+
"""
|
|
1616
|
+
# TODO: Can this support cartopy transforms? Seems not when this
|
|
1617
|
+
# is used for inset axes bounds but maybe in other places?
|
|
1618
|
+
transform = _not_none(transform, default)
|
|
1619
|
+
if isinstance(transform, mtransforms.Transform):
|
|
1620
|
+
return transform
|
|
1621
|
+
elif CRS is not object and isinstance(transform, CRS):
|
|
1622
|
+
return transform
|
|
1623
|
+
elif PlateCarree is not object and transform == "map":
|
|
1624
|
+
return PlateCarree()
|
|
1625
|
+
elif transform == "data":
|
|
1626
|
+
return self.transData
|
|
1627
|
+
elif transform == "axes":
|
|
1628
|
+
return self.transAxes
|
|
1629
|
+
elif transform == "figure":
|
|
1630
|
+
return self.figure.transFigure
|
|
1631
|
+
elif transform == "subfigure":
|
|
1632
|
+
return self.figure.transSubfigure
|
|
1633
|
+
else:
|
|
1634
|
+
raise ValueError(f"Unknown transform {transform!r}.")
|
|
1635
|
+
|
|
1636
|
+
def _register_guide(self, guide, obj, key, **kwargs):
|
|
1637
|
+
"""
|
|
1638
|
+
Queue up or replace objects for legends and list-of-artist style colorbars.
|
|
1639
|
+
"""
|
|
1640
|
+
# Initial stuff
|
|
1641
|
+
if guide not in ("legend", "colorbar"):
|
|
1642
|
+
raise TypeError(f"Invalid type {guide!r}.")
|
|
1643
|
+
dict_ = self._legend_dict if guide == "legend" else self._colorbar_dict
|
|
1644
|
+
|
|
1645
|
+
# Remove previous instances
|
|
1646
|
+
# NOTE: No good way to remove inset colorbars right now until the bounding
|
|
1647
|
+
# box and axes are merged into some kind of subclass. Just fine for now.
|
|
1648
|
+
if key in dict_ and not isinstance(dict_[key], tuple):
|
|
1649
|
+
prev = dict_.pop(key) # possibly pop a queued object
|
|
1650
|
+
if guide == "colorbar":
|
|
1651
|
+
pass
|
|
1652
|
+
elif hasattr(self, "legend_") and prev.axes.legend_ is prev:
|
|
1653
|
+
self.legend_ = None # was never added as artist
|
|
1654
|
+
else:
|
|
1655
|
+
prev.remove() # remove legends and inner colorbars
|
|
1656
|
+
|
|
1657
|
+
# Replace with instance or update the queue
|
|
1658
|
+
# NOTE: This is valid for both mappable-values pairs and handles-labels pairs
|
|
1659
|
+
if not isinstance(obj, tuple) or any(
|
|
1660
|
+
isinstance(_, mlegend.Legend) for _ in obj
|
|
1661
|
+
): # noqa: E501
|
|
1662
|
+
dict_[key] = obj
|
|
1663
|
+
else:
|
|
1664
|
+
handles, labels = obj
|
|
1665
|
+
if not np.iterable(handles) or type(handles) is tuple:
|
|
1666
|
+
handles = [handles]
|
|
1667
|
+
if not np.iterable(labels) or isinstance(labels, str):
|
|
1668
|
+
labels = [labels] * len(handles)
|
|
1669
|
+
length = min(len(handles), len(labels)) # mimics 'zip' behavior
|
|
1670
|
+
handles_full, labels_full, kwargs_full = dict_.setdefault(key, ([], [], {}))
|
|
1671
|
+
handles_full.extend(handles[:length])
|
|
1672
|
+
labels_full.extend(labels[:length])
|
|
1673
|
+
kwargs_full.update(kwargs)
|
|
1674
|
+
|
|
1675
|
+
def _update_guide(
|
|
1676
|
+
self,
|
|
1677
|
+
objs,
|
|
1678
|
+
legend=None,
|
|
1679
|
+
legend_kw=None,
|
|
1680
|
+
queue_legend=True,
|
|
1681
|
+
colorbar=None,
|
|
1682
|
+
colorbar_kw=None,
|
|
1683
|
+
queue_colorbar=True,
|
|
1684
|
+
):
|
|
1685
|
+
"""
|
|
1686
|
+
Update queues for on-the-fly legends and colorbars or track keyword arguments.
|
|
1687
|
+
"""
|
|
1688
|
+
# WARNING: Important to always cache the keyword arguments so e.g.
|
|
1689
|
+
# duplicate subsequent calls still enforce user and default behavior.
|
|
1690
|
+
# WARNING: This should generally be last in the pipeline before calling
|
|
1691
|
+
# the plot function or looping over data columns. The colormap parser
|
|
1692
|
+
# and standardize functions both modify colorbar_kw and legend_kw.
|
|
1693
|
+
legend_kw = legend_kw or {}
|
|
1694
|
+
colorbar_kw = colorbar_kw or {}
|
|
1695
|
+
guides._cache_guide_kw(objs, "legend", legend_kw)
|
|
1696
|
+
guides._cache_guide_kw(objs, "colorbar", colorbar_kw)
|
|
1697
|
+
if legend:
|
|
1698
|
+
align = legend_kw.pop("align", None)
|
|
1699
|
+
queue = legend_kw.pop("queue", queue_legend)
|
|
1700
|
+
self.legend(objs, loc=legend, align=align, queue=queue, **legend_kw)
|
|
1701
|
+
if colorbar:
|
|
1702
|
+
align = colorbar_kw.pop("align", None)
|
|
1703
|
+
queue = colorbar_kw.pop("queue", queue_colorbar)
|
|
1704
|
+
self.colorbar(objs, loc=colorbar, align=align, queue=queue, **colorbar_kw)
|
|
1705
|
+
|
|
1706
|
+
@staticmethod
|
|
1707
|
+
def _parse_frame(guide, fancybox=None, shadow=None, **kwargs):
|
|
1708
|
+
"""
|
|
1709
|
+
Parse frame arguments.
|
|
1710
|
+
"""
|
|
1711
|
+
# NOTE: Here we permit only 'edgewidth' to avoid conflict with
|
|
1712
|
+
# 'linewidth' used for legend handles and colorbar edge.
|
|
1713
|
+
kw_frame = _pop_kwargs(
|
|
1714
|
+
kwargs,
|
|
1715
|
+
alpha=("a", "framealpha", "facealpha"),
|
|
1716
|
+
facecolor=("fc", "framecolor", "facecolor"),
|
|
1717
|
+
edgecolor=("ec",),
|
|
1718
|
+
edgewidth=("ew",),
|
|
1719
|
+
)
|
|
1720
|
+
_kw_frame_default = {
|
|
1721
|
+
"alpha": f"{guide}.framealpha",
|
|
1722
|
+
"facecolor": f"{guide}.facecolor",
|
|
1723
|
+
"edgecolor": f"{guide}.edgecolor",
|
|
1724
|
+
"edgewidth": "axes.linewidth",
|
|
1725
|
+
}
|
|
1726
|
+
for key, name in _kw_frame_default.items():
|
|
1727
|
+
kw_frame.setdefault(key, rc[name])
|
|
1728
|
+
for key in ("facecolor", "edgecolor"):
|
|
1729
|
+
if kw_frame[key] == "inherit":
|
|
1730
|
+
kw_frame[key] = rc["axes." + key]
|
|
1731
|
+
kw_frame["linewidth"] = kw_frame.pop("edgewidth")
|
|
1732
|
+
kw_frame["fancybox"] = _not_none(fancybox, rc[f"{guide}.fancybox"])
|
|
1733
|
+
kw_frame["shadow"] = _not_none(shadow, rc[f"{guide}.shadow"])
|
|
1734
|
+
return kw_frame, kwargs
|
|
1735
|
+
|
|
1736
|
+
@staticmethod
|
|
1737
|
+
def _parse_colorbar_arg(
|
|
1738
|
+
mappable, values=None, norm=None, norm_kw=None, vmin=None, vmax=None, **kwargs
|
|
1739
|
+
):
|
|
1740
|
+
"""
|
|
1741
|
+
Generate a mappable from flexible non-mappable input. Useful in bridging
|
|
1742
|
+
the gap between legends and colorbars (e.g., creating colorbars from line
|
|
1743
|
+
objects whose data values span a natural colormap range).
|
|
1744
|
+
"""
|
|
1745
|
+
# For container objects, we just assume color is the same for every item.
|
|
1746
|
+
# Works for ErrorbarContainer, StemContainer, BarContainer.
|
|
1747
|
+
if (
|
|
1748
|
+
np.iterable(mappable)
|
|
1749
|
+
and len(mappable) > 0
|
|
1750
|
+
and all(isinstance(obj, mcontainer.Container) for obj in mappable)
|
|
1751
|
+
):
|
|
1752
|
+
mappable = [obj[0] for obj in mappable]
|
|
1753
|
+
|
|
1754
|
+
# Colormap instance
|
|
1755
|
+
if isinstance(mappable, mcolors.Colormap) or isinstance(mappable, str):
|
|
1756
|
+
cmap = constructor.Colormap(mappable)
|
|
1757
|
+
if values is None and isinstance(cmap, pcolors.DiscreteColormap):
|
|
1758
|
+
values = [None] * cmap.N # sometimes use discrete norm
|
|
1759
|
+
|
|
1760
|
+
# List of colors
|
|
1761
|
+
elif np.iterable(mappable) and all(map(mcolors.is_color_like, mappable)):
|
|
1762
|
+
cmap = pcolors.DiscreteColormap(list(mappable), "_no_name")
|
|
1763
|
+
if values is None:
|
|
1764
|
+
values = [None] * len(mappable) # always use discrete norm
|
|
1765
|
+
|
|
1766
|
+
# List of artists
|
|
1767
|
+
# NOTE: Do not check for isinstance(Artist) in case it is an mpl collection
|
|
1768
|
+
elif np.iterable(mappable) and all(
|
|
1769
|
+
hasattr(obj, "get_color") or hasattr(obj, "get_facecolor")
|
|
1770
|
+
for obj in mappable # noqa: E501
|
|
1771
|
+
):
|
|
1772
|
+
# Generate colormap from colors and infer tick labels
|
|
1773
|
+
colors = []
|
|
1774
|
+
for obj in mappable:
|
|
1775
|
+
if hasattr(obj, "update_scalarmappable"): # for e.g. pcolor
|
|
1776
|
+
obj.update_scalarmappable()
|
|
1777
|
+
color = (
|
|
1778
|
+
obj.get_color()
|
|
1779
|
+
if hasattr(obj, "get_color")
|
|
1780
|
+
else obj.get_facecolor()
|
|
1781
|
+
) # noqa: E501
|
|
1782
|
+
if isinstance(color, np.ndarray):
|
|
1783
|
+
color = color.squeeze() # e.g. single color scatter plot
|
|
1784
|
+
if not mcolors.is_color_like(color):
|
|
1785
|
+
raise ValueError(
|
|
1786
|
+
"Cannot make colorbar from artists with more than one color."
|
|
1787
|
+
) # noqa: E501
|
|
1788
|
+
colors.append(color)
|
|
1789
|
+
# Try to infer tick values and tick labels from Artist labels
|
|
1790
|
+
cmap = pcolors.DiscreteColormap(colors, "_no_name")
|
|
1791
|
+
if values is None:
|
|
1792
|
+
values = [None] * len(mappable)
|
|
1793
|
+
else:
|
|
1794
|
+
values = list(values)
|
|
1795
|
+
for i, (obj, val) in enumerate(zip(mappable, values)):
|
|
1796
|
+
if val is not None:
|
|
1797
|
+
continue
|
|
1798
|
+
val = obj.get_label()
|
|
1799
|
+
if val and val[0] == "_":
|
|
1800
|
+
continue
|
|
1801
|
+
values[i] = val
|
|
1802
|
+
|
|
1803
|
+
else:
|
|
1804
|
+
raise ValueError(
|
|
1805
|
+
"Input colorbar() argument must be a scalar mappable, colormap name "
|
|
1806
|
+
f"or object, list of colors, or list of artists. Got {mappable!r}."
|
|
1807
|
+
)
|
|
1808
|
+
|
|
1809
|
+
# Generate continuous normalizer, and possibly discrete normalizer. Update
|
|
1810
|
+
# the outgoing locator and formatter if user does not override.
|
|
1811
|
+
norm_kw = norm_kw or {}
|
|
1812
|
+
norm = norm or "linear"
|
|
1813
|
+
vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop("vmin", None), default=0)
|
|
1814
|
+
vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop("vmax", None), default=1)
|
|
1815
|
+
norm = constructor.Norm(norm, vmin=vmin, vmax=vmax, **norm_kw)
|
|
1816
|
+
if values is not None:
|
|
1817
|
+
ticks = []
|
|
1818
|
+
labels = None
|
|
1819
|
+
for i, val in enumerate(values):
|
|
1820
|
+
try:
|
|
1821
|
+
val = float(val)
|
|
1822
|
+
except (TypeError, ValueError):
|
|
1823
|
+
pass
|
|
1824
|
+
if val is None:
|
|
1825
|
+
val = i
|
|
1826
|
+
ticks.append(val)
|
|
1827
|
+
if any(isinstance(_, str) for _ in ticks):
|
|
1828
|
+
labels = list(map(str, ticks))
|
|
1829
|
+
ticks = np.arange(len(ticks))
|
|
1830
|
+
if len(ticks) == 1:
|
|
1831
|
+
levels = [ticks[0] - 1, ticks[0] + 1]
|
|
1832
|
+
else:
|
|
1833
|
+
levels = edges(ticks)
|
|
1834
|
+
from . import PlotAxes
|
|
1835
|
+
|
|
1836
|
+
norm, cmap, _ = PlotAxes._parse_level_norm(
|
|
1837
|
+
levels, norm, cmap, discrete_ticks=ticks, discrete_labels=labels
|
|
1838
|
+
)
|
|
1839
|
+
|
|
1840
|
+
# Return ad hoc ScalarMappable and update locator and formatter
|
|
1841
|
+
# NOTE: If value list doesn't match this may cycle over colors.
|
|
1842
|
+
mappable = mcm.ScalarMappable(norm, cmap)
|
|
1843
|
+
return mappable, kwargs
|
|
1844
|
+
|
|
1845
|
+
def _parse_colorbar_filled(
|
|
1846
|
+
self,
|
|
1847
|
+
length=None,
|
|
1848
|
+
align=None,
|
|
1849
|
+
tickloc=None,
|
|
1850
|
+
ticklocation=None,
|
|
1851
|
+
orientation=None,
|
|
1852
|
+
**kwargs,
|
|
1853
|
+
):
|
|
1854
|
+
"""
|
|
1855
|
+
Return the axes and adjusted keyword args for a panel-filling colorbar.
|
|
1856
|
+
"""
|
|
1857
|
+
# Parse input arguments
|
|
1858
|
+
side = self._panel_side
|
|
1859
|
+
side = _not_none(side, "left" if orientation == "vertical" else "bottom")
|
|
1860
|
+
align = _not_none(align, "center")
|
|
1861
|
+
length = _not_none(length=length, default=rc["colorbar.length"])
|
|
1862
|
+
ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation)
|
|
1863
|
+
|
|
1864
|
+
# Calculate inset bounds for the colorbar
|
|
1865
|
+
delta = 0.5 * (1 - length)
|
|
1866
|
+
if side in ("bottom", "top"):
|
|
1867
|
+
if align == "left":
|
|
1868
|
+
bounds = (0, 0, length, 1)
|
|
1869
|
+
elif align == "center":
|
|
1870
|
+
bounds = (delta, 0, length, 1)
|
|
1871
|
+
elif align == "right":
|
|
1872
|
+
bounds = (2 * delta, 0, length, 1)
|
|
1873
|
+
else:
|
|
1874
|
+
raise ValueError(f"Invalid align={align!r} for colorbar loc={side!r}.")
|
|
1875
|
+
else:
|
|
1876
|
+
if align == "bottom":
|
|
1877
|
+
bounds = (0, 0, 1, length)
|
|
1878
|
+
elif align == "center":
|
|
1879
|
+
bounds = (0, delta, 1, length)
|
|
1880
|
+
elif align == "top":
|
|
1881
|
+
bounds = (0, 2 * delta, 1, length)
|
|
1882
|
+
else:
|
|
1883
|
+
raise ValueError(f"Invalid align={align!r} for colorbar loc={side!r}.")
|
|
1884
|
+
|
|
1885
|
+
# Add the axes as a child of the original axes
|
|
1886
|
+
cls = mproj.get_projection_class("ultraplot_cartesian")
|
|
1887
|
+
locator = self._make_inset_locator(bounds, self.transAxes)
|
|
1888
|
+
ax = cls(self.figure, locator(self, None).bounds, zorder=5)
|
|
1889
|
+
ax.set_axes_locator(locator)
|
|
1890
|
+
self.add_child_axes(ax)
|
|
1891
|
+
ax.patch.set_facecolor("none") # ignore axes.alpha application
|
|
1892
|
+
|
|
1893
|
+
# Handle default keyword args
|
|
1894
|
+
if orientation is None:
|
|
1895
|
+
orientation = "horizontal" if side in ("bottom", "top") else "vertical"
|
|
1896
|
+
if orientation == "horizontal":
|
|
1897
|
+
outside, inside = "bottom", "top"
|
|
1898
|
+
if side == "top":
|
|
1899
|
+
outside, inside = inside, outside
|
|
1900
|
+
ticklocation = _not_none(ticklocation, outside)
|
|
1901
|
+
else:
|
|
1902
|
+
outside, inside = "left", "right"
|
|
1903
|
+
if side == "right":
|
|
1904
|
+
outside, inside = inside, outside
|
|
1905
|
+
ticklocation = _not_none(ticklocation, outside)
|
|
1906
|
+
kwargs.update({"orientation": orientation, "ticklocation": ticklocation})
|
|
1907
|
+
return ax, kwargs
|
|
1908
|
+
|
|
1909
|
+
def _parse_colorbar_inset(
|
|
1910
|
+
self,
|
|
1911
|
+
loc=None,
|
|
1912
|
+
width=None,
|
|
1913
|
+
length=None,
|
|
1914
|
+
shrink=None,
|
|
1915
|
+
frame=None,
|
|
1916
|
+
frameon=None,
|
|
1917
|
+
label=None,
|
|
1918
|
+
pad=None,
|
|
1919
|
+
tickloc=None,
|
|
1920
|
+
ticklocation=None,
|
|
1921
|
+
orientation=None,
|
|
1922
|
+
**kwargs,
|
|
1923
|
+
):
|
|
1924
|
+
"""
|
|
1925
|
+
Return the axes and adjusted keyword args for an inset colorbar.
|
|
1926
|
+
"""
|
|
1927
|
+
# Basic colorbar properties
|
|
1928
|
+
frame = _not_none(frame=frame, frameon=frameon, default=rc["colorbar.frameon"])
|
|
1929
|
+
length = _not_none(
|
|
1930
|
+
length=length, shrink=shrink, default=rc["colorbar.insetlength"]
|
|
1931
|
+
) # noqa: E501
|
|
1932
|
+
width = _not_none(width, rc["colorbar.insetwidth"])
|
|
1933
|
+
pad = _not_none(pad, rc["colorbar.insetpad"])
|
|
1934
|
+
length = units(length, "em", "ax", axes=self, width=True) # x direction
|
|
1935
|
+
width = units(width, "em", "ax", axes=self, width=False) # y direction
|
|
1936
|
+
xpad = units(pad, "em", "ax", axes=self, width=True)
|
|
1937
|
+
ypad = units(pad, "em", "ax", axes=self, width=False)
|
|
1938
|
+
|
|
1939
|
+
# Extra space accounting for colorbar label and tick labels
|
|
1940
|
+
labspace = rc["xtick.major.size"] / 72
|
|
1941
|
+
fontsize = rc["xtick.labelsize"]
|
|
1942
|
+
fontsize = _fontsize_to_pt(fontsize)
|
|
1943
|
+
if label is not None:
|
|
1944
|
+
labspace += 2.4 * fontsize / 72
|
|
1945
|
+
else:
|
|
1946
|
+
labspace += 1.2 * fontsize / 72
|
|
1947
|
+
labspace /= self._get_size_inches()[1] # space for labels
|
|
1948
|
+
|
|
1949
|
+
# Location in axes-relative coordinates
|
|
1950
|
+
# Bounds are x0, y0, width, height in axes-relative coordinates
|
|
1951
|
+
if loc == "upper right":
|
|
1952
|
+
bounds_inset = [1 - xpad - length, 1 - ypad - width]
|
|
1953
|
+
bounds_frame = [1 - 2 * xpad - length, 1 - 2 * ypad - width - labspace]
|
|
1954
|
+
elif loc == "upper left":
|
|
1955
|
+
bounds_inset = [xpad, 1 - ypad - width]
|
|
1956
|
+
bounds_frame = [0, 1 - 2 * ypad - width - labspace]
|
|
1957
|
+
elif loc == "lower left":
|
|
1958
|
+
bounds_inset = [xpad, ypad + labspace]
|
|
1959
|
+
bounds_frame = [0, 0]
|
|
1960
|
+
else:
|
|
1961
|
+
bounds_inset = [1 - xpad - length, ypad + labspace]
|
|
1962
|
+
bounds_frame = [1 - 2 * xpad - length, 0]
|
|
1963
|
+
bounds_inset.extend((length, width)) # inset axes
|
|
1964
|
+
bounds_frame.extend((2 * xpad + length, 2 * ypad + width + labspace))
|
|
1965
|
+
|
|
1966
|
+
# Make axes and frame with zorder matching default legend zorder
|
|
1967
|
+
cls = mproj.get_projection_class("ultraplot_cartesian")
|
|
1968
|
+
locator = self._make_inset_locator(bounds_inset, self.transAxes)
|
|
1969
|
+
ax = cls(self.figure, locator(self, None).bounds, zorder=5)
|
|
1970
|
+
ax.patch.set_facecolor("none")
|
|
1971
|
+
ax.set_axes_locator(locator)
|
|
1972
|
+
self.add_child_axes(ax)
|
|
1973
|
+
kw_frame, kwargs = self._parse_frame("colorbar", **kwargs)
|
|
1974
|
+
if frame:
|
|
1975
|
+
frame = self._add_guide_frame(*bounds_frame, fontsize=fontsize, **kw_frame)
|
|
1976
|
+
|
|
1977
|
+
# Handle default keyword args
|
|
1978
|
+
if orientation is not None and orientation != "horizontal":
|
|
1979
|
+
warnings._warn_ultraplot(
|
|
1980
|
+
f"Orientation for inset colorbars must be horizontal. "
|
|
1981
|
+
f"Ignoring orientation={orientation!r}."
|
|
1982
|
+
)
|
|
1983
|
+
ticklocation = _not_none(tickloc=tickloc, ticklocation=ticklocation)
|
|
1984
|
+
if ticklocation is not None and ticklocation != "bottom":
|
|
1985
|
+
warnings._warn_ultraplot("Inset colorbars can only have ticks on the bottom.")
|
|
1986
|
+
kwargs.update({"orientation": "horizontal", "ticklocation": "bottom"})
|
|
1987
|
+
return ax, kwargs
|
|
1988
|
+
|
|
1989
|
+
def _parse_legend_aligned(self, pairs, ncol=None, order=None, **kwargs):
|
|
1990
|
+
"""
|
|
1991
|
+
Draw an individual legend with aligned columns. Includes support
|
|
1992
|
+
for switching legend-entries between column-major and row-major.
|
|
1993
|
+
"""
|
|
1994
|
+
# Potentially change the order of handles to column-major
|
|
1995
|
+
npairs = len(pairs)
|
|
1996
|
+
ncol = _not_none(ncol, 3)
|
|
1997
|
+
nrow = npairs // ncol + 1
|
|
1998
|
+
array = np.empty((nrow, ncol), dtype=object)
|
|
1999
|
+
for i, pair in enumerate(pairs):
|
|
2000
|
+
array.flat[i] = pair # must be assigned individually
|
|
2001
|
+
if order == "C":
|
|
2002
|
+
array = array.T
|
|
2003
|
+
|
|
2004
|
+
# Return a legend
|
|
2005
|
+
# NOTE: Permit drawing empty legend to catch edge cases
|
|
2006
|
+
pairs = [pair for pair in array.flat if isinstance(pair, tuple)]
|
|
2007
|
+
args = tuple(zip(*pairs)) or ([], [])
|
|
2008
|
+
return mlegend.Legend(self, *args, ncol=ncol, **kwargs)
|
|
2009
|
+
|
|
2010
|
+
def _parse_legend_centered(
|
|
2011
|
+
self,
|
|
2012
|
+
pairs,
|
|
2013
|
+
*,
|
|
2014
|
+
fontsize,
|
|
2015
|
+
loc=None,
|
|
2016
|
+
title=None,
|
|
2017
|
+
frameon=None,
|
|
2018
|
+
kw_frame=None,
|
|
2019
|
+
**kwargs,
|
|
2020
|
+
):
|
|
2021
|
+
"""
|
|
2022
|
+
Draw "legend" with centered rows by creating separate legends for
|
|
2023
|
+
each row. The label spacing/border spacing will be exactly replicated.
|
|
2024
|
+
"""
|
|
2025
|
+
# Parse input args
|
|
2026
|
+
# NOTE: Main legend() function applies default 'legend.loc' of 'best' when
|
|
2027
|
+
# users pass legend=True or call legend without 'loc'. Cannot issue warning.
|
|
2028
|
+
kw_frame = kw_frame or {}
|
|
2029
|
+
kw_frame["fontsize"] = fontsize
|
|
2030
|
+
if loc is None or loc == "best": # white lie
|
|
2031
|
+
loc = "upper center"
|
|
2032
|
+
if not isinstance(loc, str):
|
|
2033
|
+
raise ValueError(
|
|
2034
|
+
f"Invalid loc={loc!r} for centered-row legend. Must be string."
|
|
2035
|
+
)
|
|
2036
|
+
keys = ("bbox_transform", "bbox_to_anchor")
|
|
2037
|
+
kw_ignore = {key: kwargs.pop(key) for key in keys if key in kwargs}
|
|
2038
|
+
if kw_ignore:
|
|
2039
|
+
warnings._warn_ultraplot(
|
|
2040
|
+
f"Ignoring invalid centered-row legend keyword args: {kw_ignore!r}"
|
|
2041
|
+
)
|
|
2042
|
+
|
|
2043
|
+
# Iterate and draw
|
|
2044
|
+
# NOTE: Empirical testing shows spacing fudge factor necessary to
|
|
2045
|
+
# exactly replicate the spacing of standard aligned legends.
|
|
2046
|
+
# NOTE: We confine possible bounding box in *y*-direction, but do not
|
|
2047
|
+
# confine it in *x*-direction. Matplotlib will automatically move
|
|
2048
|
+
# left-to-right if you request this.
|
|
2049
|
+
legs = []
|
|
2050
|
+
kwargs.update({"loc": loc, "frameon": False})
|
|
2051
|
+
space = kwargs.get("labelspacing", None) or rc["legend.labelspacing"]
|
|
2052
|
+
height = (((1 + space * 0.85) * fontsize) / 72) / self._get_size_inches()[1]
|
|
2053
|
+
for i, ipairs in enumerate(pairs):
|
|
2054
|
+
extra = int(i > 0 and title is not None)
|
|
2055
|
+
if "upper" in loc:
|
|
2056
|
+
base, offset = 1, -extra
|
|
2057
|
+
elif "lower" in loc:
|
|
2058
|
+
base, offset = 0, len(pairs)
|
|
2059
|
+
else: # center
|
|
2060
|
+
base, offset = 0.5, 0.5 * (len(pairs) - extra)
|
|
2061
|
+
y0, y1 = base + (offset - np.array([i + 1, i])) * height
|
|
2062
|
+
bb = mtransforms.Bbox([[0, y0], [1, y1]])
|
|
2063
|
+
leg = mlegend.Legend(
|
|
2064
|
+
self,
|
|
2065
|
+
*zip(*ipairs),
|
|
2066
|
+
bbox_to_anchor=bb,
|
|
2067
|
+
bbox_transform=self.transAxes,
|
|
2068
|
+
ncol=len(ipairs),
|
|
2069
|
+
title=title if i == 0 else None,
|
|
2070
|
+
**kwargs,
|
|
2071
|
+
)
|
|
2072
|
+
legs.append(leg)
|
|
2073
|
+
|
|
2074
|
+
# Draw manual fancy bounding box for un-aligned legend
|
|
2075
|
+
# WARNING: legendPatch uses the default transform, i.e. universal coordinates
|
|
2076
|
+
# in points. Means we have to transform mutation scale into transAxes sizes.
|
|
2077
|
+
# WARNING: Tempting to use legendPatch for everything but for some reason
|
|
2078
|
+
# coordinates are messed up. In some tests all coordinates were just result
|
|
2079
|
+
# of get window extent multiplied by 2 (???). Anyway actual box is found in
|
|
2080
|
+
# _legend_box attribute, which is accessed by get_window_extent.
|
|
2081
|
+
objs = tuple(legs)
|
|
2082
|
+
if frameon and legs:
|
|
2083
|
+
rend = self.figure._get_renderer() # arbitrary renderer
|
|
2084
|
+
trans = self.transAxes.inverted()
|
|
2085
|
+
bboxes = [leg.get_window_extent(rend).transformed(trans) for leg in legs]
|
|
2086
|
+
bb = mtransforms.Bbox.union(bboxes)
|
|
2087
|
+
bounds = (bb.xmin, bb.ymin, bb.xmax - bb.xmin, bb.ymax - bb.ymin)
|
|
2088
|
+
self._add_guide_frame(*bounds, **kw_frame)
|
|
2089
|
+
return objs
|
|
2090
|
+
|
|
2091
|
+
@staticmethod
|
|
2092
|
+
def _parse_legend_group(handles, labels=None):
|
|
2093
|
+
"""
|
|
2094
|
+
Parse possibly tuple-grouped input handles.
|
|
2095
|
+
"""
|
|
2096
|
+
|
|
2097
|
+
# Helper function. Retrieve labels from a tuple group or from objects
|
|
2098
|
+
# in a container. Multiple labels lead to multiple legend entries.
|
|
2099
|
+
def _legend_label(*objs): # noqa: E301
|
|
2100
|
+
labs = []
|
|
2101
|
+
for obj in objs:
|
|
2102
|
+
if hasattr(obj, "get_label"): # e.g. silent list
|
|
2103
|
+
lab = obj.get_label()
|
|
2104
|
+
if lab is not None and str(lab)[:1] != "_":
|
|
2105
|
+
labs.append(lab)
|
|
2106
|
+
return tuple(labs)
|
|
2107
|
+
|
|
2108
|
+
# Helper function. Translate handles in the input tuple group. Extracts
|
|
2109
|
+
# legend handles from contour sets and extracts labeled elements from
|
|
2110
|
+
# matplotlib containers (important for histogram plots).
|
|
2111
|
+
ignore = (mcontainer.ErrorbarContainer,)
|
|
2112
|
+
containers = (cbook.silent_list, mcontainer.Container)
|
|
2113
|
+
|
|
2114
|
+
def _legend_tuple(*objs): # noqa: E306
|
|
2115
|
+
handles = []
|
|
2116
|
+
for obj in objs:
|
|
2117
|
+
if isinstance(obj, ignore) and not _legend_label(obj):
|
|
2118
|
+
continue
|
|
2119
|
+
if hasattr(obj, "update_scalarmappable"): # for e.g. pcolor
|
|
2120
|
+
obj.update_scalarmappable()
|
|
2121
|
+
if isinstance(obj, mcontour.ContourSet): # extract single element
|
|
2122
|
+
hs, _ = obj.legend_elements()
|
|
2123
|
+
label = getattr(obj, "_legend_label", "_no_label")
|
|
2124
|
+
if hs: # non-empty
|
|
2125
|
+
obj = hs[len(hs) // 2]
|
|
2126
|
+
obj.set_label(label)
|
|
2127
|
+
if isinstance(obj, containers): # extract labeled elements
|
|
2128
|
+
hs = (obj, *guides._iter_iterables(obj))
|
|
2129
|
+
hs = tuple(filter(_legend_label, hs))
|
|
2130
|
+
if hs:
|
|
2131
|
+
handles.extend(hs)
|
|
2132
|
+
elif obj: # fallback to first element
|
|
2133
|
+
handles.append(obj[0])
|
|
2134
|
+
else:
|
|
2135
|
+
handles.append(obj)
|
|
2136
|
+
elif hasattr(obj, "get_label"):
|
|
2137
|
+
handles.append(obj)
|
|
2138
|
+
else:
|
|
2139
|
+
warnings._warn_ultraplot(f"Ignoring invalid legend handle {obj!r}.")
|
|
2140
|
+
return tuple(handles)
|
|
2141
|
+
|
|
2142
|
+
# Sanitize labels. Ignore e.g. extra hist() or hist2d() return values,
|
|
2143
|
+
# auto-detect labels in tuple group, auto-expand tuples with diff labels
|
|
2144
|
+
# NOTE: Allow handles and labels of different length like
|
|
2145
|
+
# native matplotlib. Just truncate extra values with zip().
|
|
2146
|
+
if labels is None:
|
|
2147
|
+
labels = [None] * len(handles)
|
|
2148
|
+
ihandles, ilabels = [], []
|
|
2149
|
+
for hs, label in zip(handles, labels):
|
|
2150
|
+
# Filter objects
|
|
2151
|
+
if type(hs) is not tuple: # ignore Containers (tuple subclasses)
|
|
2152
|
+
hs = (hs,)
|
|
2153
|
+
hs = _legend_tuple(*hs)
|
|
2154
|
+
labs = _legend_label(*hs)
|
|
2155
|
+
if not hs:
|
|
2156
|
+
continue
|
|
2157
|
+
# Unfurl tuple of handles
|
|
2158
|
+
if label is None and len(labs) > 1:
|
|
2159
|
+
hs = tuple(filter(_legend_label, hs))
|
|
2160
|
+
ihandles.extend(hs)
|
|
2161
|
+
ilabels.extend(_.get_label() for _ in hs)
|
|
2162
|
+
# Append this handle with some name
|
|
2163
|
+
else:
|
|
2164
|
+
hs = hs[0] if len(hs) == 1 else hs # unfurl for better error messages
|
|
2165
|
+
label = label if label is not None else labs[0] if labs else "_no_label"
|
|
2166
|
+
ihandles.append(hs)
|
|
2167
|
+
ilabels.append(label)
|
|
2168
|
+
return ihandles, ilabels
|
|
2169
|
+
|
|
2170
|
+
def _parse_legend_handles(
|
|
2171
|
+
self,
|
|
2172
|
+
handles,
|
|
2173
|
+
labels,
|
|
2174
|
+
ncol=None,
|
|
2175
|
+
order=None,
|
|
2176
|
+
center=None,
|
|
2177
|
+
alphabetize=None,
|
|
2178
|
+
handler_map=None,
|
|
2179
|
+
):
|
|
2180
|
+
"""
|
|
2181
|
+
Parse input handles and labels.
|
|
2182
|
+
"""
|
|
2183
|
+
# Handle lists of lists
|
|
2184
|
+
# TODO: Often desirable to label a "mappable" with one data value. Maybe add a
|
|
2185
|
+
# legend option for the *number of samples* or *sample points* when drawing
|
|
2186
|
+
# legends for mappables. Look into "legend handlers", might just want to add
|
|
2187
|
+
# handlers by passing handler_map to legend() and get_legend_handles_labels().
|
|
2188
|
+
is_list = lambda obj: ( # noqa: E731
|
|
2189
|
+
np.iterable(obj) and not isinstance(obj, (str, tuple))
|
|
2190
|
+
)
|
|
2191
|
+
to_list = lambda obj: ( # noqa: E731
|
|
2192
|
+
obj.tolist()
|
|
2193
|
+
if isinstance(obj, np.ndarray)
|
|
2194
|
+
else obj if obj is None or is_list(obj) else [obj]
|
|
2195
|
+
)
|
|
2196
|
+
handles, labels = to_list(handles), to_list(labels)
|
|
2197
|
+
if handles and not labels and all(isinstance(h, str) for h in handles):
|
|
2198
|
+
handles, labels = labels, handles
|
|
2199
|
+
multi = any(is_list(h) and len(h) > 1 for h in (handles or ()))
|
|
2200
|
+
if multi and order == "F":
|
|
2201
|
+
warnings._warn_ultraplot(
|
|
2202
|
+
"Column-major ordering of legend handles is not supported "
|
|
2203
|
+
"for horizontally-centered legends."
|
|
2204
|
+
)
|
|
2205
|
+
if multi and ncol is not None:
|
|
2206
|
+
warnings._warn_ultraplot(
|
|
2207
|
+
"Detected list of *lists* of legend handles. Ignoring "
|
|
2208
|
+
'the user input property "ncol".'
|
|
2209
|
+
)
|
|
2210
|
+
if labels and not handles:
|
|
2211
|
+
warnings._warn_ultraplot(
|
|
2212
|
+
"Passing labels without handles is unsupported in ultraplot. "
|
|
2213
|
+
"Please explicitly pass the handles to legend() or pass labels "
|
|
2214
|
+
"to plotting commands with e.g. plot(data_1d, label='label') or "
|
|
2215
|
+
"plot(data_2d, labels=['label1', 'label2', ...]). After passing "
|
|
2216
|
+
"labels to plotting commands you can call legend() without any "
|
|
2217
|
+
"arguments or with the handles as a sole positional argument."
|
|
2218
|
+
)
|
|
2219
|
+
ncol = _not_none(ncol, 3)
|
|
2220
|
+
center = _not_none(center, multi)
|
|
2221
|
+
|
|
2222
|
+
# Iterate over each sublist and parse independently
|
|
2223
|
+
pairs = []
|
|
2224
|
+
if not multi: # temporary
|
|
2225
|
+
handles, labels = [handles], [labels]
|
|
2226
|
+
elif labels is None:
|
|
2227
|
+
labels = [labels] * len(handles)
|
|
2228
|
+
for ihandles, ilabels in zip(handles, labels):
|
|
2229
|
+
ihandles, ilabels = to_list(ihandles), to_list(ilabels)
|
|
2230
|
+
if ihandles is None:
|
|
2231
|
+
ihandles = self._get_legend_handles(handler_map)
|
|
2232
|
+
ihandles, ilabels = self._parse_legend_group(ihandles, ilabels)
|
|
2233
|
+
ipairs = list(zip(ihandles, ilabels))
|
|
2234
|
+
if alphabetize:
|
|
2235
|
+
ipairs = sorted(ipairs, key=lambda pair: pair[1])
|
|
2236
|
+
pairs.append(ipairs)
|
|
2237
|
+
|
|
2238
|
+
# Manage (handle, label) pairs in context of the 'center' option
|
|
2239
|
+
if not multi:
|
|
2240
|
+
pairs = pairs[0]
|
|
2241
|
+
if center:
|
|
2242
|
+
multi = True
|
|
2243
|
+
pairs = [pairs[i * ncol : (i + 1) * ncol] for i in range(len(pairs))]
|
|
2244
|
+
else:
|
|
2245
|
+
if not center: # standardize format based on input
|
|
2246
|
+
multi = False # no longer is list of lists
|
|
2247
|
+
pairs = [pair for ipairs in pairs for pair in ipairs]
|
|
2248
|
+
|
|
2249
|
+
if multi:
|
|
2250
|
+
pairs = [ipairs for ipairs in pairs if ipairs]
|
|
2251
|
+
return pairs, multi
|
|
2252
|
+
|
|
2253
|
+
def _range_subplotspec(self, s):
|
|
2254
|
+
"""
|
|
2255
|
+
Return the column or row range for the subplotspec.
|
|
2256
|
+
"""
|
|
2257
|
+
if not isinstance(self, maxes.SubplotBase):
|
|
2258
|
+
raise RuntimeError("Axes must be a subplot.")
|
|
2259
|
+
ss = self.get_subplotspec().get_topmost_subplotspec()
|
|
2260
|
+
row1, row2, col1, col2 = ss._get_rows_columns()
|
|
2261
|
+
if s == "x":
|
|
2262
|
+
return (col1, col2)
|
|
2263
|
+
else:
|
|
2264
|
+
return (row1, row2)
|
|
2265
|
+
|
|
2266
|
+
def _range_tightbbox(self, s):
|
|
2267
|
+
"""
|
|
2268
|
+
Return the tight bounding box span from the cached bounding box.
|
|
2269
|
+
"""
|
|
2270
|
+
# TODO: Better testing for axes visibility
|
|
2271
|
+
bbox = self._tight_bbox
|
|
2272
|
+
if bbox is None:
|
|
2273
|
+
return np.nan, np.nan
|
|
2274
|
+
if s == "x":
|
|
2275
|
+
return bbox.xmin, bbox.xmax
|
|
2276
|
+
else:
|
|
2277
|
+
return bbox.ymin, bbox.ymax
|
|
2278
|
+
|
|
2279
|
+
def _sharex_setup(self, sharex, **kwargs):
|
|
2280
|
+
"""
|
|
2281
|
+
Configure x-axis sharing for panels. See also `~CartesianAxes._sharex_setup`.
|
|
2282
|
+
"""
|
|
2283
|
+
self._share_short_axis(sharex, "left", **kwargs) # x axis of left panels
|
|
2284
|
+
self._share_short_axis(sharex, "right", **kwargs)
|
|
2285
|
+
self._share_long_axis(sharex, "bottom", **kwargs) # x axis of bottom panels
|
|
2286
|
+
self._share_long_axis(sharex, "top", **kwargs)
|
|
2287
|
+
|
|
2288
|
+
def _sharey_setup(self, sharey, **kwargs):
|
|
2289
|
+
"""
|
|
2290
|
+
Configure y-axis sharing for panels. See also `~CartesianAxes._sharey_setup`.
|
|
2291
|
+
"""
|
|
2292
|
+
self._share_short_axis(sharey, "bottom", **kwargs) # y axis of bottom panels
|
|
2293
|
+
self._share_short_axis(sharey, "top", **kwargs)
|
|
2294
|
+
self._share_long_axis(sharey, "left", **kwargs) # y axis of left panels
|
|
2295
|
+
self._share_long_axis(sharey, "right", **kwargs)
|
|
2296
|
+
|
|
2297
|
+
def _share_short_axis(self, share, side, **kwargs):
|
|
2298
|
+
"""
|
|
2299
|
+
Share the "short" axes of panels in this subplot with other panels.
|
|
2300
|
+
"""
|
|
2301
|
+
if share is None or self._panel_side:
|
|
2302
|
+
return # if this is a panel
|
|
2303
|
+
s = "x" if side in ("left", "right") else "y"
|
|
2304
|
+
caxs = self._panel_dict[side]
|
|
2305
|
+
paxs = share._panel_dict[side]
|
|
2306
|
+
caxs = [pax for pax in caxs if not pax._panel_hidden]
|
|
2307
|
+
paxs = [pax for pax in paxs if not pax._panel_hidden]
|
|
2308
|
+
for cax, pax in zip(caxs, paxs): # may be uneven
|
|
2309
|
+
getattr(cax, f"_share{s}_setup")(pax, **kwargs)
|
|
2310
|
+
|
|
2311
|
+
def _share_long_axis(self, share, side, **kwargs):
|
|
2312
|
+
"""
|
|
2313
|
+
Share the "long" axes of panels in this subplot with other panels.
|
|
2314
|
+
"""
|
|
2315
|
+
# NOTE: We do not check _panel_share because that only controls
|
|
2316
|
+
# sharing with main subplot, not other subplots
|
|
2317
|
+
if share is None or self._panel_side:
|
|
2318
|
+
return # if this is a panel
|
|
2319
|
+
s = "x" if side in ("top", "bottom") else "y"
|
|
2320
|
+
paxs = self._panel_dict[side]
|
|
2321
|
+
paxs = [pax for pax in paxs if not pax._panel_hidden]
|
|
2322
|
+
for pax in paxs:
|
|
2323
|
+
getattr(pax, f"_share{s}_setup")(share, **kwargs)
|
|
2324
|
+
|
|
2325
|
+
def _reposition_subplot(self):
|
|
2326
|
+
"""
|
|
2327
|
+
Reposition the subplot axes.
|
|
2328
|
+
"""
|
|
2329
|
+
# WARNING: In later versions self.numRows, self.numCols, and self.figbox
|
|
2330
|
+
# are @property definitions that never go stale but in mpl < 3.4 they are
|
|
2331
|
+
# attributes that must be updated explicitly with update_params().
|
|
2332
|
+
# WARNING: In early versions matplotlib only removes '_layoutbox' and
|
|
2333
|
+
# '_poslayoutbox' when calling public set_position but in later versions it
|
|
2334
|
+
# calls set_in_layout(False) which removes children from get_tightbbox().
|
|
2335
|
+
# Therefore try to use _set_position() even though it is private
|
|
2336
|
+
if not isinstance(self, maxes.SubplotBase):
|
|
2337
|
+
raise RuntimeError("Axes must be a subplot.")
|
|
2338
|
+
setter = getattr(self, "_set_position", self.set_position)
|
|
2339
|
+
if _version_mpl >= "3.4":
|
|
2340
|
+
setter(self.get_subplotspec().get_position(self.figure))
|
|
2341
|
+
else:
|
|
2342
|
+
self.update_params()
|
|
2343
|
+
setter(self.figbox) # equivalent to above
|
|
2344
|
+
|
|
2345
|
+
def _update_abc(self, **kwargs):
|
|
2346
|
+
"""
|
|
2347
|
+
Update the a-b-c label.
|
|
2348
|
+
"""
|
|
2349
|
+
# Properties
|
|
2350
|
+
# NOTE: Border props only apply for "inner" title locations so we need to
|
|
2351
|
+
# store on the axes whenever they are modified in case the current location
|
|
2352
|
+
# is an 'outer' location then re-apply in case 'loc' is subsequently changed
|
|
2353
|
+
kw = rc.fill(
|
|
2354
|
+
{
|
|
2355
|
+
"size": "abc.size",
|
|
2356
|
+
"weight": "abc.weight",
|
|
2357
|
+
"color": "abc.color",
|
|
2358
|
+
"family": "font.family",
|
|
2359
|
+
},
|
|
2360
|
+
context=True,
|
|
2361
|
+
)
|
|
2362
|
+
kwb = rc.fill(
|
|
2363
|
+
{
|
|
2364
|
+
"border": "abc.border",
|
|
2365
|
+
"borderwidth": "abc.borderwidth",
|
|
2366
|
+
"bbox": "abc.bbox",
|
|
2367
|
+
"bboxpad": "abc.bboxpad",
|
|
2368
|
+
"bboxcolor": "abc.bboxcolor",
|
|
2369
|
+
"bboxstyle": "abc.bboxstyle",
|
|
2370
|
+
"bboxalpha": "abc.bboxalpha",
|
|
2371
|
+
},
|
|
2372
|
+
context=True,
|
|
2373
|
+
)
|
|
2374
|
+
self._abc_border_kwargs.update(kwb)
|
|
2375
|
+
|
|
2376
|
+
# A-b-c labels. Build as a...z...aa...zz...aaa...zzz
|
|
2377
|
+
# NOTE: The abc string should already be validated here
|
|
2378
|
+
abc = rc.find("abc", context=True) # 1st run, or changed
|
|
2379
|
+
if abc is True:
|
|
2380
|
+
abc = "a"
|
|
2381
|
+
if abc is False:
|
|
2382
|
+
abc = ""
|
|
2383
|
+
if abc is None or self.number is None:
|
|
2384
|
+
pass
|
|
2385
|
+
elif isinstance(abc, str):
|
|
2386
|
+
nabc, iabc = divmod(self.number - 1, 26)
|
|
2387
|
+
if abc: # should have been validated to contain 'a' or 'A'
|
|
2388
|
+
old = re.search("[aA]", abc).group() # return first occurrence
|
|
2389
|
+
new = (nabc + 1) * ABC_STRING[iabc]
|
|
2390
|
+
new = new.upper() if old == "A" else new
|
|
2391
|
+
abc = abc.replace(old, new, 1) # replace first occurrence
|
|
2392
|
+
kw["text"] = abc
|
|
2393
|
+
else:
|
|
2394
|
+
if self.number > len(abc):
|
|
2395
|
+
raise ValueError(
|
|
2396
|
+
f"Invalid abc list length {len(abc)} "
|
|
2397
|
+
f"for axes with number {self.number}."
|
|
2398
|
+
)
|
|
2399
|
+
else:
|
|
2400
|
+
kw["text"] = abc[self._number - 1]
|
|
2401
|
+
|
|
2402
|
+
# Update a-b-c label
|
|
2403
|
+
loc = rc.find("abc.loc", context=True)
|
|
2404
|
+
loc = self._abc_loc = _translate_loc(loc or self._abc_loc, "text")
|
|
2405
|
+
if loc not in ("left", "right", "center"):
|
|
2406
|
+
kw.update(self._abc_border_kwargs)
|
|
2407
|
+
kw.update(kwargs)
|
|
2408
|
+
self._title_dict["abc"].update(kw)
|
|
2409
|
+
|
|
2410
|
+
def _update_title(self, loc, title=None, **kwargs):
|
|
2411
|
+
"""
|
|
2412
|
+
Update the title at the specified location.
|
|
2413
|
+
"""
|
|
2414
|
+
# Titles, with two workflows here:
|
|
2415
|
+
# 1. title='name' and titleloc='position'
|
|
2416
|
+
# 2. ltitle='name', rtitle='name', etc., arbitrarily many titles
|
|
2417
|
+
# NOTE: This always updates the *current* title and deflection to panels
|
|
2418
|
+
# is handled later so that titles set with set_title() are deflected too.
|
|
2419
|
+
# See notes in _update_super_labels() and _apply_title_above().
|
|
2420
|
+
# NOTE: Matplotlib added axes.titlecolor in version 3.2 but we still use
|
|
2421
|
+
# custom title.size, title.weight, title.color properties for retroactive
|
|
2422
|
+
# support in older matplotlib versions. First get params and update kwargs.
|
|
2423
|
+
kw = rc.fill(
|
|
2424
|
+
{
|
|
2425
|
+
"size": "title.size",
|
|
2426
|
+
"weight": "title.weight",
|
|
2427
|
+
"color": "title.color",
|
|
2428
|
+
"family": "font.family",
|
|
2429
|
+
},
|
|
2430
|
+
context=True,
|
|
2431
|
+
)
|
|
2432
|
+
if "color" in kw and kw["color"] == "auto":
|
|
2433
|
+
del kw["color"] # WARNING: matplotlib permits invalid color here
|
|
2434
|
+
kwb = rc.fill(
|
|
2435
|
+
{
|
|
2436
|
+
"border": "title.border",
|
|
2437
|
+
"borderwidth": "title.borderwidth",
|
|
2438
|
+
"bbox": "title.bbox",
|
|
2439
|
+
"bboxpad": "title.bboxpad",
|
|
2440
|
+
"bboxcolor": "title.bboxcolor",
|
|
2441
|
+
"bboxstyle": "title.bboxstyle",
|
|
2442
|
+
"bboxalpha": "title.bboxalpha",
|
|
2443
|
+
},
|
|
2444
|
+
context=True,
|
|
2445
|
+
)
|
|
2446
|
+
self._title_border_kwargs.update(kwb)
|
|
2447
|
+
|
|
2448
|
+
# Update the padding settings read at drawtime. Make sure to
|
|
2449
|
+
# update them on the panel axes if 'title.above' is active.
|
|
2450
|
+
pad = rc.find("abc.titlepad", context=True)
|
|
2451
|
+
if pad is not None:
|
|
2452
|
+
self._abc_title_pad = pad
|
|
2453
|
+
pad = rc.find("title.pad", context=True) # title
|
|
2454
|
+
if pad is not None:
|
|
2455
|
+
self._title_pad = pad
|
|
2456
|
+
self._set_title_offset_trans(pad)
|
|
2457
|
+
|
|
2458
|
+
# Get the title location. If 'titleloc' was used then transfer text
|
|
2459
|
+
# from the old location to the new location.
|
|
2460
|
+
if loc is not None:
|
|
2461
|
+
loc = _translate_loc(loc, "text")
|
|
2462
|
+
else:
|
|
2463
|
+
old = self._title_loc
|
|
2464
|
+
loc = rc.find("title.loc", context=True)
|
|
2465
|
+
loc = self._title_loc = _translate_loc(loc or self._title_loc, "text")
|
|
2466
|
+
if loc != old and old is not None:
|
|
2467
|
+
labels._transfer_label(self._title_dict[old], self._title_dict[loc])
|
|
2468
|
+
|
|
2469
|
+
# Update the title text. For outer panels, add text to the panel if
|
|
2470
|
+
# necesssary. For inner panels, use the border and bbox settings.
|
|
2471
|
+
if loc not in ("left", "right", "center"):
|
|
2472
|
+
kw.update(self._title_border_kwargs)
|
|
2473
|
+
if title is None:
|
|
2474
|
+
pass
|
|
2475
|
+
elif isinstance(title, str):
|
|
2476
|
+
kw["text"] = title
|
|
2477
|
+
elif np.iterable(title) and all(isinstance(_, str) for _ in title):
|
|
2478
|
+
if self.number is None:
|
|
2479
|
+
pass
|
|
2480
|
+
elif self.number > len(title):
|
|
2481
|
+
raise ValueError(
|
|
2482
|
+
f"Invalid title list length {len(title)} "
|
|
2483
|
+
f"for axes with number {self.number}."
|
|
2484
|
+
)
|
|
2485
|
+
else:
|
|
2486
|
+
kw["text"] = title[self.number - 1]
|
|
2487
|
+
else:
|
|
2488
|
+
raise ValueError(f"Invalid title {title!r}. Must be string(s).")
|
|
2489
|
+
kw.update(kwargs)
|
|
2490
|
+
self._title_dict[loc].update(kw)
|
|
2491
|
+
|
|
2492
|
+
def _update_title_position(self, renderer):
|
|
2493
|
+
"""
|
|
2494
|
+
Update the position of inset titles and outer titles. This is called
|
|
2495
|
+
by matplotlib at drawtime.
|
|
2496
|
+
"""
|
|
2497
|
+
# Update title positions
|
|
2498
|
+
# NOTE: Critical to do this every time in case padding changes or
|
|
2499
|
+
# we added or removed an a-b-c label in the same position as a title
|
|
2500
|
+
width, height = self._get_size_inches()
|
|
2501
|
+
x_pad = self._title_pad / (72 * width)
|
|
2502
|
+
y_pad = self._title_pad / (72 * height)
|
|
2503
|
+
for loc, obj in self._title_dict.items():
|
|
2504
|
+
x, y = (0, 1)
|
|
2505
|
+
if loc == "abc": # redirect
|
|
2506
|
+
loc = self._abc_loc
|
|
2507
|
+
if loc == "left":
|
|
2508
|
+
x = 0
|
|
2509
|
+
elif loc == "center":
|
|
2510
|
+
x = 0.5
|
|
2511
|
+
elif loc == "right":
|
|
2512
|
+
x = 1
|
|
2513
|
+
if loc in ("upper center", "lower center"):
|
|
2514
|
+
x = 0.5
|
|
2515
|
+
elif loc in ("upper left", "lower left"):
|
|
2516
|
+
x = x_pad
|
|
2517
|
+
elif loc in ("upper right", "lower right"):
|
|
2518
|
+
x = 1 - x_pad
|
|
2519
|
+
if loc in ("upper left", "upper right", "upper center"):
|
|
2520
|
+
y = 1 - y_pad
|
|
2521
|
+
elif loc in ("lower left", "lower right", "lower center"):
|
|
2522
|
+
y = y_pad
|
|
2523
|
+
obj.set_position((x, y))
|
|
2524
|
+
|
|
2525
|
+
# Get title padding. Push title above tick marks since matplotlib ignores them.
|
|
2526
|
+
# This is known matplotlib problem but especially annoying with top panels.
|
|
2527
|
+
# NOTE: See axis.get_ticks_position for inspiration
|
|
2528
|
+
pad = self._title_pad
|
|
2529
|
+
abcpad = self._abc_title_pad
|
|
2530
|
+
if self.xaxis.get_visible() and any(
|
|
2531
|
+
tick.tick2line.get_visible() and not tick.label2.get_visible()
|
|
2532
|
+
for tick in self.xaxis.majorTicks
|
|
2533
|
+
):
|
|
2534
|
+
pad += self.xaxis.get_tick_padding()
|
|
2535
|
+
|
|
2536
|
+
# Avoid applying padding on every draw in case it is expensive to change
|
|
2537
|
+
# the title Text transforms every time.
|
|
2538
|
+
pad_current = self._title_pad_current
|
|
2539
|
+
if pad_current is None or not np.isclose(pad, pad_current):
|
|
2540
|
+
self._title_pad_current = pad
|
|
2541
|
+
self._set_title_offset_trans(pad)
|
|
2542
|
+
|
|
2543
|
+
# Adjust the above-axes positions with builtin algorithm
|
|
2544
|
+
# WARNING: Make sure the name of this private function doesn't change
|
|
2545
|
+
super()._update_title_position(renderer)
|
|
2546
|
+
|
|
2547
|
+
# Sync the title position with the a-b-c label position
|
|
2548
|
+
aobj = self._title_dict["abc"]
|
|
2549
|
+
tobj = self._title_dict[self._abc_loc]
|
|
2550
|
+
aobj.set_transform(tobj.get_transform())
|
|
2551
|
+
aobj.set_position(tobj.get_position())
|
|
2552
|
+
aobj.set_ha(tobj.get_ha())
|
|
2553
|
+
aobj.set_va(tobj.get_va())
|
|
2554
|
+
|
|
2555
|
+
# Offset title away from a-b-c label
|
|
2556
|
+
# NOTE: Title texts all use axes transform in x-direction
|
|
2557
|
+
if not tobj.get_text() or not aobj.get_text():
|
|
2558
|
+
return
|
|
2559
|
+
awidth, twidth = (
|
|
2560
|
+
obj.get_window_extent(renderer).transformed(self.transAxes.inverted()).width
|
|
2561
|
+
for obj in (aobj, tobj)
|
|
2562
|
+
)
|
|
2563
|
+
ha = aobj.get_ha()
|
|
2564
|
+
pad = (abcpad / 72) / self._get_size_inches()[0]
|
|
2565
|
+
aoffset = toffset = 0
|
|
2566
|
+
if ha == "left":
|
|
2567
|
+
toffset = awidth + pad
|
|
2568
|
+
elif ha == "right":
|
|
2569
|
+
aoffset = -(twidth + pad)
|
|
2570
|
+
else: # guaranteed center, there are others
|
|
2571
|
+
toffset = 0.5 * (awidth + pad)
|
|
2572
|
+
aoffset = -0.5 * (twidth + pad)
|
|
2573
|
+
aobj.set_x(aobj.get_position()[0] + aoffset)
|
|
2574
|
+
tobj.set_x(tobj.get_position()[0] + toffset)
|
|
2575
|
+
|
|
2576
|
+
def _update_super_title(self, suptitle=None, **kwargs):
|
|
2577
|
+
"""
|
|
2578
|
+
Update the figure super title.
|
|
2579
|
+
"""
|
|
2580
|
+
# NOTE: This is actually *figure-wide* setting, but that line gets blurred
|
|
2581
|
+
# where we have shared axes, spanning labels, etc. May cause redundant
|
|
2582
|
+
# assignments if using SubplotGrid.format() but this is fast so nbd.
|
|
2583
|
+
if self.number is None:
|
|
2584
|
+
# NOTE: Kludge prevents changed *figure-wide* settings from getting
|
|
2585
|
+
# overwritten when user makes a new panels or insets. Funky limitation but
|
|
2586
|
+
# kind of makes sense to make these inaccessible from panels.
|
|
2587
|
+
return
|
|
2588
|
+
kw = rc.fill(
|
|
2589
|
+
{
|
|
2590
|
+
"size": "suptitle.size",
|
|
2591
|
+
"weight": "suptitle.weight",
|
|
2592
|
+
"color": "suptitle.color",
|
|
2593
|
+
"family": "font.family",
|
|
2594
|
+
},
|
|
2595
|
+
context=True,
|
|
2596
|
+
)
|
|
2597
|
+
kw.update(kwargs)
|
|
2598
|
+
if suptitle or kw:
|
|
2599
|
+
self.figure._update_super_title(suptitle, **kw)
|
|
2600
|
+
|
|
2601
|
+
def _update_super_labels(self, side, labels=None, **kwargs):
|
|
2602
|
+
"""
|
|
2603
|
+
Update the figure super labels.
|
|
2604
|
+
"""
|
|
2605
|
+
fig = self.figure
|
|
2606
|
+
if self.number is None:
|
|
2607
|
+
return # NOTE: see above
|
|
2608
|
+
kw = rc.fill(
|
|
2609
|
+
{
|
|
2610
|
+
"color": side + "label.color",
|
|
2611
|
+
"rotation": side + "label.rotation",
|
|
2612
|
+
"size": side + "label.size",
|
|
2613
|
+
"weight": side + "label.weight",
|
|
2614
|
+
"family": "font.family",
|
|
2615
|
+
},
|
|
2616
|
+
context=True,
|
|
2617
|
+
)
|
|
2618
|
+
kw.update(kwargs)
|
|
2619
|
+
if labels or kw:
|
|
2620
|
+
fig._update_super_labels(side, labels, **kw)
|
|
2621
|
+
|
|
2622
|
+
|
|
2623
|
+
@staticmethod
|
|
2624
|
+
def get_center_of_axes(axes = None):
|
|
2625
|
+
positions = [ax.get_position() for ax in axes]
|
|
2626
|
+
# get the outermost coordinates
|
|
2627
|
+
box = mtransforms.Bbox.from_extents(
|
|
2628
|
+
min(p.bounds[0] for p in positions),
|
|
2629
|
+
min(p.bounds[1] for p in positions),
|
|
2630
|
+
max(p.bounds[0] + p.bounds[2] for p in positions),
|
|
2631
|
+
max(p.bounds[1] + p.bounds[3] for p in positions)
|
|
2632
|
+
)
|
|
2633
|
+
return box
|
|
2634
|
+
|
|
2635
|
+
|
|
2636
|
+
|
|
2637
|
+
def _update_share_labels(self, axes=None, target="x"):
|
|
2638
|
+
"""Update shared axis labels for a group of axes.
|
|
2639
|
+
|
|
2640
|
+
Parameters
|
|
2641
|
+
----------
|
|
2642
|
+
axes : list of int or list of Axes, optional
|
|
2643
|
+
The axes indices or Axes objects to share labels between
|
|
2644
|
+
target : {'x', 'y'}, optional
|
|
2645
|
+
Which axis labels to share ('x' for x-axis, 'y' for y-axis)
|
|
2646
|
+
"""
|
|
2647
|
+
if not axes:
|
|
2648
|
+
return
|
|
2649
|
+
|
|
2650
|
+
# Convert indices to actual axes objects
|
|
2651
|
+
if isinstance(axes[0], int):
|
|
2652
|
+
axes = [self.figure.axes[i] for i in axes]
|
|
2653
|
+
|
|
2654
|
+
# Get the center position of the axes group
|
|
2655
|
+
if box := self.get_center_of_axes(axes):
|
|
2656
|
+
# Reuse existing label if possible
|
|
2657
|
+
if target == 'x':
|
|
2658
|
+
label = axes[0].xaxis.label
|
|
2659
|
+
# Update position and properties
|
|
2660
|
+
label.set_position((
|
|
2661
|
+
(box.bounds[0] + box.bounds[2])/2,
|
|
2662
|
+
box.bounds[1]
|
|
2663
|
+
))
|
|
2664
|
+
else: # y-axis
|
|
2665
|
+
label = axes[0].yaxis.label
|
|
2666
|
+
# Update position and properties
|
|
2667
|
+
label.set_position((
|
|
2668
|
+
box.bounds[0],
|
|
2669
|
+
(box.bounds[1] + box.bounds[3])/2
|
|
2670
|
+
))
|
|
2671
|
+
|
|
2672
|
+
label.set_ha('center')
|
|
2673
|
+
label.set_va('center')
|
|
2674
|
+
|
|
2675
|
+
# Share the same label object across all axes
|
|
2676
|
+
# Skip first axes since we used its label
|
|
2677
|
+
for ax in axes[1:]:
|
|
2678
|
+
if target == 'x':
|
|
2679
|
+
ax.xaxis.label = label
|
|
2680
|
+
else:
|
|
2681
|
+
ax.yaxis.label = label
|
|
2682
|
+
|
|
2683
|
+
|
|
2684
|
+
@docstring._snippet_manager
|
|
2685
|
+
def format(
|
|
2686
|
+
self,
|
|
2687
|
+
*,
|
|
2688
|
+
title=None,
|
|
2689
|
+
title_kw=None,
|
|
2690
|
+
abc_kw=None,
|
|
2691
|
+
ltitle=None,
|
|
2692
|
+
lefttitle=None,
|
|
2693
|
+
ctitle=None,
|
|
2694
|
+
centertitle=None,
|
|
2695
|
+
rtitle=None,
|
|
2696
|
+
righttitle=None,
|
|
2697
|
+
ultitle=None,
|
|
2698
|
+
upperlefttitle=None,
|
|
2699
|
+
uctitle=None,
|
|
2700
|
+
uppercentertitle=None,
|
|
2701
|
+
urtitle=None,
|
|
2702
|
+
upperrighttitle=None,
|
|
2703
|
+
lltitle=None,
|
|
2704
|
+
lowerlefttitle=None,
|
|
2705
|
+
lctitle=None,
|
|
2706
|
+
lowercentertitle=None,
|
|
2707
|
+
lrtitle=None,
|
|
2708
|
+
lowerrighttitle=None,
|
|
2709
|
+
share_xlabels = None,
|
|
2710
|
+
share_ylabels = None,
|
|
2711
|
+
**kwargs,
|
|
2712
|
+
):
|
|
2713
|
+
"""
|
|
2714
|
+
Modify the a-b-c label, axes title(s), and background patch,
|
|
2715
|
+
and call `ultraplot.figure.Figure.format` on the axes figure.
|
|
2716
|
+
|
|
2717
|
+
Parameters
|
|
2718
|
+
----------
|
|
2719
|
+
%(axes.format)s
|
|
2720
|
+
|
|
2721
|
+
Important
|
|
2722
|
+
---------
|
|
2723
|
+
`abc`, `abcloc`, `titleloc`, `titleabove`, `titlepad`, and
|
|
2724
|
+
`abctitlepad` are actually :ref:`configuration settings <ug_config>`.
|
|
2725
|
+
We explicitly document these arguments here because it is common to
|
|
2726
|
+
change them for specific axes. But many :ref:`other configuration
|
|
2727
|
+
settings <ug_format>` can be passed to ``format`` too.
|
|
2728
|
+
|
|
2729
|
+
Other parameters
|
|
2730
|
+
----------------
|
|
2731
|
+
%(figure.format)s
|
|
2732
|
+
%(rc.format)s
|
|
2733
|
+
|
|
2734
|
+
See also
|
|
2735
|
+
--------
|
|
2736
|
+
ultraplot.axes.CartesianAxes.format
|
|
2737
|
+
ultraplot.axes.PolarAxes.format
|
|
2738
|
+
ultraplot.axes.GeoAxes.format
|
|
2739
|
+
ultraplot.figure.Figure.format
|
|
2740
|
+
ultraplot.gridspec.SubplotGrid.format
|
|
2741
|
+
ultraplot.config.Configurator.context
|
|
2742
|
+
"""
|
|
2743
|
+
skip_figure = kwargs.pop("skip_figure", False) # internal keyword arg
|
|
2744
|
+
params = _pop_params(kwargs, self.figure._format_signature)
|
|
2745
|
+
|
|
2746
|
+
# Initiate context block
|
|
2747
|
+
rc_kw, rc_mode = _pop_rc(kwargs)
|
|
2748
|
+
with rc.context(rc_kw, mode=rc_mode):
|
|
2749
|
+
# Behavior of titles in presence of panels
|
|
2750
|
+
above = rc.find("title.above", context=True)
|
|
2751
|
+
if above is not None:
|
|
2752
|
+
self._title_above = above # used for future titles
|
|
2753
|
+
|
|
2754
|
+
|
|
2755
|
+
# Update a-b-c label and titles
|
|
2756
|
+
abc_kw = abc_kw or {}
|
|
2757
|
+
title_kw = title_kw or {}
|
|
2758
|
+
self._update_share_labels(share_xlabels, target = 'x')
|
|
2759
|
+
self._update_share_labels(share_ylabels, target = 'y')
|
|
2760
|
+
self._update_abc(**abc_kw)
|
|
2761
|
+
self._update_title(None, title, **title_kw)
|
|
2762
|
+
self._update_title(
|
|
2763
|
+
"left",
|
|
2764
|
+
_not_none(ltitle=ltitle, lefttitle=lefttitle),
|
|
2765
|
+
**title_kw,
|
|
2766
|
+
)
|
|
2767
|
+
self._update_title(
|
|
2768
|
+
"center",
|
|
2769
|
+
_not_none(ctitle=ctitle, centertitle=centertitle),
|
|
2770
|
+
**title_kw,
|
|
2771
|
+
)
|
|
2772
|
+
self._update_title(
|
|
2773
|
+
"right",
|
|
2774
|
+
_not_none(rtitle=rtitle, righttitle=righttitle),
|
|
2775
|
+
**title_kw,
|
|
2776
|
+
)
|
|
2777
|
+
self._update_title(
|
|
2778
|
+
"upper left",
|
|
2779
|
+
_not_none(ultitle=ultitle, upperlefttitle=upperlefttitle),
|
|
2780
|
+
**title_kw,
|
|
2781
|
+
)
|
|
2782
|
+
self._update_title(
|
|
2783
|
+
"upper center",
|
|
2784
|
+
_not_none(uctitle=uctitle, uppercentertitle=uppercentertitle),
|
|
2785
|
+
**title_kw,
|
|
2786
|
+
)
|
|
2787
|
+
self._update_title(
|
|
2788
|
+
"upper right",
|
|
2789
|
+
_not_none(urtitle=urtitle, upperrighttitle=upperrighttitle),
|
|
2790
|
+
**title_kw,
|
|
2791
|
+
)
|
|
2792
|
+
self._update_title(
|
|
2793
|
+
"lower left",
|
|
2794
|
+
_not_none(lltitle=lltitle, lowerlefttitle=lowerlefttitle),
|
|
2795
|
+
**title_kw,
|
|
2796
|
+
)
|
|
2797
|
+
self._update_title(
|
|
2798
|
+
"lower center",
|
|
2799
|
+
_not_none(lctitle=lctitle, lowercentertitle=lowercentertitle),
|
|
2800
|
+
**title_kw,
|
|
2801
|
+
)
|
|
2802
|
+
self._update_title(
|
|
2803
|
+
"lower right",
|
|
2804
|
+
_not_none(lrtitle=lrtitle, lowerrighttitle=lowerrighttitle),
|
|
2805
|
+
**title_kw,
|
|
2806
|
+
)
|
|
2807
|
+
|
|
2808
|
+
# Update the axes style
|
|
2809
|
+
# NOTE: This will also raise an error if unknown args are encountered
|
|
2810
|
+
cycle = rc.find("axes.prop_cycle", context=True)
|
|
2811
|
+
if cycle is not None:
|
|
2812
|
+
self.set_prop_cycle(cycle)
|
|
2813
|
+
self._update_background(**kwargs)
|
|
2814
|
+
|
|
2815
|
+
# Update super labels and super title
|
|
2816
|
+
# NOTE: To avoid resetting figure-wide settings when new axes are created
|
|
2817
|
+
# we only proceed if using the default context mode. Simliar to geo.py
|
|
2818
|
+
if skip_figure: # avoid recursion
|
|
2819
|
+
return
|
|
2820
|
+
if rc_mode == 1: # avoid resetting
|
|
2821
|
+
return
|
|
2822
|
+
self.figure.format(rc_kw=rc_kw, rc_mode=rc_mode, skip_axes=True, **params)
|
|
2823
|
+
|
|
2824
|
+
def draw(self, renderer=None, *args, **kwargs):
|
|
2825
|
+
# Perform extra post-processing steps
|
|
2826
|
+
# NOTE: In *principle* these steps go here but should already be complete
|
|
2827
|
+
# because auto_layout() (called by figure pre-processor) has to run them
|
|
2828
|
+
# before aligning labels. So these are harmless no-ops.
|
|
2829
|
+
self._add_queued_guides()
|
|
2830
|
+
self._apply_title_above()
|
|
2831
|
+
if self._colorbar_fill:
|
|
2832
|
+
self._colorbar_fill.update_ticks(manual_only=True) # only if needed
|
|
2833
|
+
if self._inset_parent is not None and self._inset_zoom:
|
|
2834
|
+
self.indicate_inset_zoom()
|
|
2835
|
+
super().draw(renderer, *args, **kwargs)
|
|
2836
|
+
|
|
2837
|
+
def get_tightbbox(self, renderer, *args, **kwargs):
|
|
2838
|
+
# Perform extra post-processing steps
|
|
2839
|
+
# NOTE: This should be updated alongside draw(). We also cache the resulting
|
|
2840
|
+
# bounding box to speed up tight layout calculations (see _range_tightbbox).
|
|
2841
|
+
self._add_queued_guides()
|
|
2842
|
+
self._apply_title_above()
|
|
2843
|
+
if self._colorbar_fill:
|
|
2844
|
+
self._colorbar_fill.update_ticks(manual_only=True) # only if needed
|
|
2845
|
+
if self._inset_parent is not None and self._inset_zoom:
|
|
2846
|
+
self.indicate_inset_zoom()
|
|
2847
|
+
self._tight_bbox = super().get_tightbbox(renderer, *args, **kwargs)
|
|
2848
|
+
return self._tight_bbox
|
|
2849
|
+
|
|
2850
|
+
def get_default_bbox_extra_artists(self):
|
|
2851
|
+
# Further restrict artists to those with disabled clipping
|
|
2852
|
+
# or use the axes bounding box or patch path for clipping.
|
|
2853
|
+
# NOTE: Critical to ignore x and y axis, spines, and all child axes.
|
|
2854
|
+
# For some reason these have clipping 'enabled' but it is not respected.
|
|
2855
|
+
# NOTE: Matplotlib already tries to do this inside get_tightbbox() but
|
|
2856
|
+
# their approach fails for cartopy axes clipped by paths and not boxes.
|
|
2857
|
+
return [
|
|
2858
|
+
artist
|
|
2859
|
+
for artist in super().get_default_bbox_extra_artists()
|
|
2860
|
+
if not self._artist_fully_clipped(artist)
|
|
2861
|
+
]
|
|
2862
|
+
|
|
2863
|
+
def set_prop_cycle(self, *args, **kwargs):
|
|
2864
|
+
# Silent override. This is a strict superset of matplotlib functionality.
|
|
2865
|
+
# Includes both ultraplot syntax with positional arguments interpreted as
|
|
2866
|
+
# color arguments and oldschool matplotlib cycler(key, value) syntax.
|
|
2867
|
+
if len(args) == 2 and isinstance(args[0], str) and np.iterable(args[1]):
|
|
2868
|
+
if _pop_props({args[0]: object()}, "line"): # if a valid line property
|
|
2869
|
+
kwargs = {args[0]: args[1]} # pass as keyword argument
|
|
2870
|
+
args = ()
|
|
2871
|
+
cycle = self._active_cycle = constructor.Cycle(*args, **kwargs)
|
|
2872
|
+
return super().set_prop_cycle(cycle) # set the property cycler after validation
|
|
2873
|
+
|
|
2874
|
+
@docstring._snippet_manager
|
|
2875
|
+
def inset(self, *args, **kwargs):
|
|
2876
|
+
"""
|
|
2877
|
+
%(axes.inset)s
|
|
2878
|
+
"""
|
|
2879
|
+
return self._add_inset_axes(*args, **kwargs)
|
|
2880
|
+
|
|
2881
|
+
@docstring._snippet_manager
|
|
2882
|
+
def inset_axes(self, *args, **kwargs):
|
|
2883
|
+
"""
|
|
2884
|
+
%(axes.inset)s
|
|
2885
|
+
"""
|
|
2886
|
+
return self._add_inset_axes(*args, **kwargs)
|
|
2887
|
+
|
|
2888
|
+
@docstring._snippet_manager
|
|
2889
|
+
def indicate_inset_zoom(self, **kwargs):
|
|
2890
|
+
"""
|
|
2891
|
+
%(axes.indicate_inset)s
|
|
2892
|
+
"""
|
|
2893
|
+
# Add the inset indicators
|
|
2894
|
+
parent = self._inset_parent
|
|
2895
|
+
if not parent:
|
|
2896
|
+
raise ValueError("This command can only be called from an inset axes.")
|
|
2897
|
+
kwargs.update(_pop_props(kwargs, "patch")) # impose alternative defaults
|
|
2898
|
+
if not self._inset_zoom_artists:
|
|
2899
|
+
kwargs.setdefault("zorder", 3.5)
|
|
2900
|
+
kwargs.setdefault("linewidth", rc["axes.linewidth"])
|
|
2901
|
+
kwargs.setdefault("edgecolor", rc["axes.edgecolor"])
|
|
2902
|
+
xlim, ylim = self.get_xlim(), self.get_ylim()
|
|
2903
|
+
rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0])
|
|
2904
|
+
rectpatch, connects = parent.indicate_inset(rect, self)
|
|
2905
|
+
|
|
2906
|
+
# Update indicator properties
|
|
2907
|
+
# NOTE: Unlike matplotlib we sync zoom box properties with connection lines.
|
|
2908
|
+
if self._inset_zoom_artists:
|
|
2909
|
+
rectpatch_prev, connects_prev = self._inset_zoom_artists
|
|
2910
|
+
rectpatch.update_from(rectpatch_prev)
|
|
2911
|
+
rectpatch.set_zorder(rectpatch_prev.get_zorder())
|
|
2912
|
+
rectpatch_prev.remove()
|
|
2913
|
+
for line, line_prev in zip(connects, connects_prev):
|
|
2914
|
+
line.update_from(line_prev)
|
|
2915
|
+
line.set_zorder(line_prev.get_zorder()) # not included in update_from
|
|
2916
|
+
line_prev.remove()
|
|
2917
|
+
rectpatch.update(kwargs)
|
|
2918
|
+
for line in connects:
|
|
2919
|
+
line.update(kwargs)
|
|
2920
|
+
self._inset_zoom_artists = (rectpatch, connects)
|
|
2921
|
+
return rectpatch, connects
|
|
2922
|
+
|
|
2923
|
+
@docstring._snippet_manager
|
|
2924
|
+
def panel(self, side=None, **kwargs):
|
|
2925
|
+
"""
|
|
2926
|
+
%(axes.panel)s
|
|
2927
|
+
"""
|
|
2928
|
+
return self.figure._add_axes_panel(self, side, **kwargs)
|
|
2929
|
+
|
|
2930
|
+
@docstring._snippet_manager
|
|
2931
|
+
def panel_axes(self, side=None, **kwargs):
|
|
2932
|
+
"""
|
|
2933
|
+
%(axes.panel)s
|
|
2934
|
+
"""
|
|
2935
|
+
return self.figure._add_axes_panel(self, side, **kwargs)
|
|
2936
|
+
|
|
2937
|
+
@docstring._obfuscate_params
|
|
2938
|
+
@docstring._snippet_manager
|
|
2939
|
+
def colorbar(self, mappable, values=None, loc=None, location=None, **kwargs):
|
|
2940
|
+
"""
|
|
2941
|
+
Add an inset colorbar or an outer colorbar along the edge of the axes.
|
|
2942
|
+
|
|
2943
|
+
Parameters
|
|
2944
|
+
----------
|
|
2945
|
+
%(axes.colorbar_args)s
|
|
2946
|
+
loc, location : int or str, default: :rc:`colorbar.loc`
|
|
2947
|
+
The colorbar location. Valid location keys are shown in the below table.
|
|
2948
|
+
|
|
2949
|
+
.. _colorbar_table:
|
|
2950
|
+
|
|
2951
|
+
================== =======================================
|
|
2952
|
+
Location Valid keys
|
|
2953
|
+
================== =======================================
|
|
2954
|
+
outer left ``'left'``, ``'l'``
|
|
2955
|
+
outer right ``'right'``, ``'r'``
|
|
2956
|
+
outer bottom ``'bottom'``, ``'b'``
|
|
2957
|
+
outer top ``'top'``, ``'t'``
|
|
2958
|
+
default inset ``'best'``, ``'inset'``, ``'i'``, ``0``
|
|
2959
|
+
upper right inset ``'upper right'``, ``'ur'``, ``1``
|
|
2960
|
+
upper left inset ``'upper left'``, ``'ul'``, ``2``
|
|
2961
|
+
lower left inset ``'lower left'``, ``'ll'``, ``3``
|
|
2962
|
+
lower right inset ``'lower right'``, ``'lr'``, ``4``
|
|
2963
|
+
"filled" ``'fill'``
|
|
2964
|
+
================== =======================================
|
|
2965
|
+
|
|
2966
|
+
shrink
|
|
2967
|
+
Alias for `length`. This is included for consistency with
|
|
2968
|
+
`matplotlib.figure.Figure.colorbar`.
|
|
2969
|
+
length \
|
|
2970
|
+
: float or unit-spec, default: :rc:`colorbar.length` or :rc:`colorbar.insetlength`
|
|
2971
|
+
The colorbar length. For outer colorbars, units are relative to the axes
|
|
2972
|
+
width or height (default is :rcraw:`colorbar.length`). For inset
|
|
2973
|
+
colorbars, floats interpreted as em-widths and strings interpreted
|
|
2974
|
+
by `~ultraplot.utils.units` (default is :rcraw:`colorbar.insetlength`).
|
|
2975
|
+
width : unit-spec, default: :rc:`colorbar.width` or :rc:`colorbar.insetwidth
|
|
2976
|
+
The colorbar width. For outer colorbars, floats are interpreted as inches
|
|
2977
|
+
(default is :rcraw:`colorbar.width`). For inset colorbars, floats are
|
|
2978
|
+
interpreted as em-widths (default is :rcraw:`colorbar.insetwidth`).
|
|
2979
|
+
Strings are interpreted by `~ultraplot.utils.units`.
|
|
2980
|
+
%(axes.colorbar_space)s
|
|
2981
|
+
Has no visible effect if `length` is ``1``.
|
|
2982
|
+
|
|
2983
|
+
Other parameters
|
|
2984
|
+
----------------
|
|
2985
|
+
%(axes.colorbar_kwargs)s
|
|
2986
|
+
|
|
2987
|
+
See also
|
|
2988
|
+
--------
|
|
2989
|
+
ultraplot.figure.Figure.colorbar
|
|
2990
|
+
matplotlib.figure.Figure.colorbar
|
|
2991
|
+
"""
|
|
2992
|
+
# Translate location and possibly infer from orientation. Also optionally
|
|
2993
|
+
# infer align setting from keywords stored on object.
|
|
2994
|
+
orientation = kwargs.get("orientation", None)
|
|
2995
|
+
kwargs = guides._flush_guide_kw(mappable, "colorbar", kwargs)
|
|
2996
|
+
loc = _not_none(loc=loc, location=location)
|
|
2997
|
+
if orientation is not None: # possibly infer loc from orientation
|
|
2998
|
+
if orientation not in ("vertical", "horizontal"):
|
|
2999
|
+
raise ValueError(
|
|
3000
|
+
f"Invalid colorbar orientation {orientation!r}. Must be 'vertical' or 'horizontal'."
|
|
3001
|
+
) # noqa: E501
|
|
3002
|
+
if loc is None:
|
|
3003
|
+
loc = {"vertical": "right", "horizontal": "bottom"}[orientation]
|
|
3004
|
+
loc = _translate_loc(loc, "colorbar", default=rc["colorbar.loc"])
|
|
3005
|
+
align = kwargs.pop("align", None)
|
|
3006
|
+
align = _translate_loc(align, "align", default="center")
|
|
3007
|
+
|
|
3008
|
+
# Either draw right now or queue up for later. The queue option lets us
|
|
3009
|
+
# successively append objects (e.g. lines) to a colorbar artist list.
|
|
3010
|
+
queue = kwargs.pop("queue", False)
|
|
3011
|
+
if queue:
|
|
3012
|
+
self._register_guide("colorbar", (mappable, values), (loc, align), **kwargs)
|
|
3013
|
+
else:
|
|
3014
|
+
return self._add_colorbar(mappable, values, loc=loc, align=align, **kwargs)
|
|
3015
|
+
|
|
3016
|
+
@docstring._concatenate_inherited # also obfuscates params
|
|
3017
|
+
@docstring._snippet_manager
|
|
3018
|
+
def legend(self, handles=None, labels=None, loc=None, location=None, **kwargs):
|
|
3019
|
+
"""
|
|
3020
|
+
Add an inset legend or outer legend along the edge of the axes.
|
|
3021
|
+
|
|
3022
|
+
Parameters
|
|
3023
|
+
----------
|
|
3024
|
+
%(axes.legend_args)s
|
|
3025
|
+
loc, location : int or str, default: :rc:`legend.loc`
|
|
3026
|
+
The legend location. Valid location keys are shown in the below table.
|
|
3027
|
+
|
|
3028
|
+
.. _legend_table:
|
|
3029
|
+
|
|
3030
|
+
================== =======================================
|
|
3031
|
+
Location Valid keys
|
|
3032
|
+
================== =======================================
|
|
3033
|
+
outer left ``'left'``, ``'l'``
|
|
3034
|
+
outer right ``'right'``, ``'r'``
|
|
3035
|
+
outer bottom ``'bottom'``, ``'b'``
|
|
3036
|
+
outer top ``'top'``, ``'t'``
|
|
3037
|
+
"best" inset ``'best'``, ``'inset'``, ``'i'``, ``0``
|
|
3038
|
+
upper right inset ``'upper right'``, ``'ur'``, ``1``
|
|
3039
|
+
upper left inset ``'upper left'``, ``'ul'``, ``2``
|
|
3040
|
+
lower left inset ``'lower left'``, ``'ll'``, ``3``
|
|
3041
|
+
lower right inset ``'lower right'``, ``'lr'``, ``4``
|
|
3042
|
+
center left inset ``'center left'``, ``'cl'``, ``5``
|
|
3043
|
+
center right inset ``'center right'``, ``'cr'``, ``6``
|
|
3044
|
+
lower center inset ``'lower center'``, ``'lc'``, ``7``
|
|
3045
|
+
upper center inset ``'upper center'``, ``'uc'``, ``8``
|
|
3046
|
+
center inset ``'center'``, ``'c'``, ``9``
|
|
3047
|
+
"filled" ``'fill'``
|
|
3048
|
+
================== =======================================
|
|
3049
|
+
|
|
3050
|
+
width : unit-spec, optional
|
|
3051
|
+
For outer legends only. The space allocated for the legend
|
|
3052
|
+
box. This does nothing if the :ref:`tight layout algorithm
|
|
3053
|
+
<ug_tight>` is active for the figure.
|
|
3054
|
+
%(units.in)s
|
|
3055
|
+
%(axes.legend_space)s
|
|
3056
|
+
|
|
3057
|
+
Other parameters
|
|
3058
|
+
----------------
|
|
3059
|
+
%(axes.legend_kwargs)s
|
|
3060
|
+
|
|
3061
|
+
See also
|
|
3062
|
+
--------
|
|
3063
|
+
ultraplot.figure.Figure.legend
|
|
3064
|
+
matplotlib.axes.Axes.legend
|
|
3065
|
+
"""
|
|
3066
|
+
# Translate location and possibly infer from orientation. Also optionally
|
|
3067
|
+
# infer align setting from keywords stored on object.
|
|
3068
|
+
kwargs = guides._flush_guide_kw(handles, "legend", kwargs)
|
|
3069
|
+
loc = _not_none(loc=loc, location=location)
|
|
3070
|
+
loc = _translate_loc(loc, "legend", default=rc["legend.loc"])
|
|
3071
|
+
align = kwargs.pop("align", None)
|
|
3072
|
+
align = _translate_loc(align, "align", default="center")
|
|
3073
|
+
|
|
3074
|
+
# Either draw right now or queue up for later. Handles can be successively
|
|
3075
|
+
# added to a single location this way. Used for on-the-fly legends.
|
|
3076
|
+
queue = kwargs.pop("queue", False)
|
|
3077
|
+
if queue:
|
|
3078
|
+
self._register_guide("legend", (handles, labels), (loc, align), **kwargs)
|
|
3079
|
+
else:
|
|
3080
|
+
return self._add_legend(handles, labels, loc=loc, align=align, **kwargs)
|
|
3081
|
+
|
|
3082
|
+
@docstring._concatenate_inherited
|
|
3083
|
+
@docstring._snippet_manager
|
|
3084
|
+
def text(
|
|
3085
|
+
self,
|
|
3086
|
+
*args,
|
|
3087
|
+
border=False,
|
|
3088
|
+
bbox=False,
|
|
3089
|
+
bordercolor="w",
|
|
3090
|
+
borderwidth=2,
|
|
3091
|
+
borderinvert=False,
|
|
3092
|
+
borderstyle="miter",
|
|
3093
|
+
bboxcolor="w",
|
|
3094
|
+
bboxstyle="round",
|
|
3095
|
+
bboxalpha=0.5,
|
|
3096
|
+
bboxpad=None,
|
|
3097
|
+
**kwargs,
|
|
3098
|
+
):
|
|
3099
|
+
"""
|
|
3100
|
+
Add text to the axes.
|
|
3101
|
+
|
|
3102
|
+
Parameters
|
|
3103
|
+
----------
|
|
3104
|
+
x, y, [z] : float
|
|
3105
|
+
The coordinates for the text. `~ultraplot.axes.ThreeAxes` accept an
|
|
3106
|
+
optional third coordinate. If only two are provided this automatically
|
|
3107
|
+
redirects to the `~mpl_toolkits.mplot3d.Axes3D.text2D` method.
|
|
3108
|
+
s, text : str
|
|
3109
|
+
The string for the text.
|
|
3110
|
+
%(axes.transform)s
|
|
3111
|
+
|
|
3112
|
+
Other parameters
|
|
3113
|
+
----------------
|
|
3114
|
+
border : bool, default: False
|
|
3115
|
+
Whether to draw border around text.
|
|
3116
|
+
borderwidth : float, default: 2
|
|
3117
|
+
The width of the text border.
|
|
3118
|
+
bordercolor : color-spec, default: 'w'
|
|
3119
|
+
The color of the text border.
|
|
3120
|
+
borderinvert : bool, optional
|
|
3121
|
+
If ``True``, the text and border colors are swapped.
|
|
3122
|
+
borderstyle : {'miter', 'round', 'bevel'}, optional
|
|
3123
|
+
The `line join style \
|
|
3124
|
+
<https://matplotlib.org/stable/gallery/lines_bars_and_markers/joinstyle.html>`__
|
|
3125
|
+
used for the border.
|
|
3126
|
+
bbox : bool, default: False
|
|
3127
|
+
Whether to draw a bounding box around text.
|
|
3128
|
+
bboxcolor : color-spec, default: 'w'
|
|
3129
|
+
The color of the text bounding box.
|
|
3130
|
+
bboxstyle : boxstyle, default: 'round'
|
|
3131
|
+
The style of the bounding box.
|
|
3132
|
+
bboxalpha : float, default: 0.5
|
|
3133
|
+
The alpha for the bounding box.
|
|
3134
|
+
bboxpad : float, default: :rc:`title.bboxpad`
|
|
3135
|
+
The padding for the bounding box.
|
|
3136
|
+
%(artist.text)s
|
|
3137
|
+
|
|
3138
|
+
**kwargs
|
|
3139
|
+
Passed to `matplotlib.axes.Axes.text`.
|
|
3140
|
+
|
|
3141
|
+
See also
|
|
3142
|
+
--------
|
|
3143
|
+
matplotlib.axes.Axes.text
|
|
3144
|
+
"""
|
|
3145
|
+
# Translate positional args
|
|
3146
|
+
# Audo-redirect to text2D for 3D axes if not enough arguments passed
|
|
3147
|
+
# NOTE: The transform must be passed positionally for 3D axes with 2D coords
|
|
3148
|
+
keys = "xy"
|
|
3149
|
+
func = super().text
|
|
3150
|
+
if self._name == "three":
|
|
3151
|
+
if len(args) >= 4 or "z" in kwargs:
|
|
3152
|
+
keys += "z"
|
|
3153
|
+
else:
|
|
3154
|
+
func = self.text2D
|
|
3155
|
+
keys = (*keys, ("s", "text"), "transform")
|
|
3156
|
+
args, kwargs = _kwargs_to_args(keys, *args, **kwargs)
|
|
3157
|
+
*args, transform = args
|
|
3158
|
+
if any(arg is None for arg in args):
|
|
3159
|
+
raise TypeError("Missing required positional argument.")
|
|
3160
|
+
if transform is None:
|
|
3161
|
+
transform = self.transData
|
|
3162
|
+
else:
|
|
3163
|
+
transform = self._get_transform(transform)
|
|
3164
|
+
with warnings.catch_warnings(): # ignore duplicates (internal issues?)
|
|
3165
|
+
warnings.simplefilter("ignore", warnings.UltraplotWarning)
|
|
3166
|
+
kwargs.update(_pop_props(kwargs, "text"))
|
|
3167
|
+
|
|
3168
|
+
# Update the text object using a monkey patch
|
|
3169
|
+
obj = func(*args, transform=transform, **kwargs)
|
|
3170
|
+
obj.update = labels._update_label.__get__(obj)
|
|
3171
|
+
obj.update(
|
|
3172
|
+
{
|
|
3173
|
+
"border": border,
|
|
3174
|
+
"bordercolor": bordercolor,
|
|
3175
|
+
"borderinvert": borderinvert,
|
|
3176
|
+
"borderwidth": borderwidth,
|
|
3177
|
+
"borderstyle": borderstyle,
|
|
3178
|
+
"bbox": bbox,
|
|
3179
|
+
"bboxcolor": bboxcolor,
|
|
3180
|
+
"bboxstyle": bboxstyle,
|
|
3181
|
+
"bboxalpha": bboxalpha,
|
|
3182
|
+
"bboxpad": bboxpad,
|
|
3183
|
+
}
|
|
3184
|
+
)
|
|
3185
|
+
return obj
|
|
3186
|
+
|
|
3187
|
+
def _iter_axes(self, hidden=False, children=False, panels=True):
|
|
3188
|
+
"""
|
|
3189
|
+
Return a list of visible axes, panel axes, and child axes of both.
|
|
3190
|
+
|
|
3191
|
+
Parameters
|
|
3192
|
+
----------
|
|
3193
|
+
hidden : bool, optional
|
|
3194
|
+
Whether to include "hidden" panels.
|
|
3195
|
+
children : bool, optional
|
|
3196
|
+
Whether to include children. Note this now includes "twin" axes.
|
|
3197
|
+
panels : bool or str or sequence of str, optional
|
|
3198
|
+
Whether to include panels or the panels to include.
|
|
3199
|
+
"""
|
|
3200
|
+
# Parse panels
|
|
3201
|
+
if panels is False:
|
|
3202
|
+
panels = ()
|
|
3203
|
+
elif panels is True or panels is None:
|
|
3204
|
+
panels = ("left", "right", "bottom", "top")
|
|
3205
|
+
elif isinstance(panels, str):
|
|
3206
|
+
panels = (panels,)
|
|
3207
|
+
if not set(panels) <= {"left", "right", "bottom", "top"}:
|
|
3208
|
+
raise ValueError(f"Invalid sides {panels!r}.")
|
|
3209
|
+
# Iterate
|
|
3210
|
+
axs = (self, *(ax for side in panels for ax in self._panel_dict[side]))
|
|
3211
|
+
for iax in axs:
|
|
3212
|
+
if not hidden and iax._panel_hidden:
|
|
3213
|
+
continue # ignore hidden panel and its colorbar/legend child
|
|
3214
|
+
iaxs = (iax, *(iax.child_axes if children else ()))
|
|
3215
|
+
for jax in iaxs:
|
|
3216
|
+
if not jax.get_visible():
|
|
3217
|
+
continue # safety first
|
|
3218
|
+
yield jax
|
|
3219
|
+
|
|
3220
|
+
@property
|
|
3221
|
+
def number(self):
|
|
3222
|
+
"""
|
|
3223
|
+
The axes number. This controls the order of a-b-c labels and the
|
|
3224
|
+
order of appearance in the `~ultraplot.gridspec.SubplotGrid` returned
|
|
3225
|
+
by `~ultraplot.figure.Figure.subplots`.
|
|
3226
|
+
"""
|
|
3227
|
+
return self._number
|
|
3228
|
+
|
|
3229
|
+
@number.setter
|
|
3230
|
+
def number(self, num):
|
|
3231
|
+
if num is None or isinstance(num, Integral) and num > 0:
|
|
3232
|
+
self._number = num
|
|
3233
|
+
else:
|
|
3234
|
+
raise ValueError(f"Invalid number {num!r}. Must be integer >=1.")
|
|
3235
|
+
|
|
3236
|
+
|
|
3237
|
+
# Apply signature obfuscation after storing previous signature
|
|
3238
|
+
# NOTE: This is needed for __init__
|
|
3239
|
+
Axes._format_signatures = {Axes: inspect.signature(Axes.format)}
|
|
3240
|
+
Axes.format = docstring._obfuscate_kwargs(Axes.format)
|