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/geo.py
ADDED
|
@@ -0,0 +1,1675 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Axes filled with cartographic projections.
|
|
4
|
+
"""
|
|
5
|
+
import copy
|
|
6
|
+
import inspect
|
|
7
|
+
|
|
8
|
+
import matplotlib.axis as maxis
|
|
9
|
+
import matplotlib.path as mpath
|
|
10
|
+
import matplotlib.text as mtext
|
|
11
|
+
import matplotlib.ticker as mticker
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from .. import constructor
|
|
15
|
+
from .. import proj as pproj
|
|
16
|
+
from ..config import rc
|
|
17
|
+
from ..internals import ic # noqa: F401
|
|
18
|
+
from ..internals import _not_none, _pop_rc, _version_cartopy, docstring, warnings
|
|
19
|
+
from . import plot
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import cartopy.crs as ccrs
|
|
23
|
+
import cartopy.feature as cfeature
|
|
24
|
+
import cartopy.mpl.gridliner as cgridliner
|
|
25
|
+
from cartopy.crs import Projection
|
|
26
|
+
from cartopy.mpl.geoaxes import GeoAxes as _GeoAxes
|
|
27
|
+
except ModuleNotFoundError:
|
|
28
|
+
ccrs = cfeature = cgridliner = None
|
|
29
|
+
_GeoAxes = Projection = object
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from mpl_toolkits.basemap import Basemap
|
|
33
|
+
except ModuleNotFoundError:
|
|
34
|
+
Basemap = object
|
|
35
|
+
|
|
36
|
+
__all__ = ["GeoAxes"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Format docstring
|
|
40
|
+
_format_docstring = """
|
|
41
|
+
round : bool, default: :rc:`geo.round`
|
|
42
|
+
*For polar cartopy axes only*.
|
|
43
|
+
Whether to bound polar projections with circles rather than squares. Note that outer
|
|
44
|
+
gridline labels cannot be added to circle-bounded polar projections. When basemap
|
|
45
|
+
is the backend this argument must be passed to `~ultraplot.constructor.Proj` instead.
|
|
46
|
+
extent : {'globe', 'auto'}, default: :rc:`geo.extent`
|
|
47
|
+
*For cartopy axes only*.
|
|
48
|
+
Whether to auto adjust the map bounds based on plotted content. If ``'globe'`` then
|
|
49
|
+
non-polar projections are fixed with `~cartopy.mpl.geoaxes.GeoAxes.set_global`,
|
|
50
|
+
non-Gnomonic polar projections are bounded at the equator, and Gnomonic polar
|
|
51
|
+
projections are bounded at 30 degrees latitude. If ``'auto'`` nothing is done.
|
|
52
|
+
lonlim, latlim : 2-tuple of float, optional
|
|
53
|
+
*For cartopy axes only.*
|
|
54
|
+
The approximate longitude and latitude boundaries of the map, applied
|
|
55
|
+
with `~cartopy.mpl.geoaxes.GeoAxes.set_extent`. When basemap is the backend
|
|
56
|
+
this argument must be passed to `~ultraplot.constructor.Proj` instead.
|
|
57
|
+
boundinglat : float, optional
|
|
58
|
+
*For cartopy axes only.*
|
|
59
|
+
The edge latitude for the circle bounding North Pole and South Pole-centered
|
|
60
|
+
projections. When basemap is the backend this argument must be passed to
|
|
61
|
+
`~ultraplot.constructor.Proj` instead.
|
|
62
|
+
longrid, latgrid, grid : bool, default: :rc:`grid`
|
|
63
|
+
Whether to draw longitude and latitude gridlines.
|
|
64
|
+
Use the keyword `grid` to toggle both at once.
|
|
65
|
+
longridminor, latgridminor, gridminor : bool, default: :rc:`gridminor`
|
|
66
|
+
Whether to draw "minor" longitude and latitude lines.
|
|
67
|
+
Use the keyword `gridminor` to toggle both at once.
|
|
68
|
+
latmax : float, default: 80
|
|
69
|
+
The maximum absolute latitude for gridlines. Longitude gridlines are cut off
|
|
70
|
+
poleward of this value (note this feature does not work in cartopy 0.18).
|
|
71
|
+
nsteps : int, default: :rc:`grid.nsteps`
|
|
72
|
+
*For cartopy axes only.*
|
|
73
|
+
The number of interpolation steps used to draw gridlines.
|
|
74
|
+
lonlines, latlines : optional
|
|
75
|
+
Aliases for `lonlocator`, `latlocator`.
|
|
76
|
+
lonlocator, latlocator : locator-spec, optional
|
|
77
|
+
Used to determine the longitude and latitude gridline locations.
|
|
78
|
+
Passed to the `~ultraplot.constructor.Locator` constructor. Can be
|
|
79
|
+
string, float, list of float, or `matplotlib.ticker.Locator` instance.
|
|
80
|
+
|
|
81
|
+
For basemap or cartopy < 0.18, the defaults are ``'deglon'`` and
|
|
82
|
+
``'deglat'``, which correspond to the `~ultraplot.ticker.LongitudeLocator`
|
|
83
|
+
and `~ultraplot.ticker.LatitudeLocator` locators (adapted from cartopy).
|
|
84
|
+
For cartopy >= 0.18, the defaults are ``'dmslon'`` and ``'dmslat'``,
|
|
85
|
+
which uses the same locators with ``dms=True``. This selects gridlines
|
|
86
|
+
at nice degree-minute-second intervals when the map extent is very small.
|
|
87
|
+
lonlines_kw, latlines_kw : optional
|
|
88
|
+
Aliases for `lonlocator_kw`, `latlocator_kw`.
|
|
89
|
+
lonlocator_kw, latlocator_kw : dict-like, optional
|
|
90
|
+
Keyword arguments passed to the `matplotlib.ticker.Locator` class.
|
|
91
|
+
lonminorlocator, latminorlocator, lonminorlines, latminorlines : optional
|
|
92
|
+
As with `lonlocator` and `latlocator` but for the "minor" gridlines.
|
|
93
|
+
lonminorlines_kw, latminorlines_kw : optional
|
|
94
|
+
Aliases for `lonminorlocator_kw`, `latminorlocator_kw`.
|
|
95
|
+
lonminorlocator_kw, latminorlocator_kw : optional
|
|
96
|
+
As with `lonlocator_kw`, and `latlocator_kw` but for the "minor" gridlines.
|
|
97
|
+
lonlabels, latlabels, labels : str, bool, or sequence, :rc:`grid.labels`
|
|
98
|
+
Whether to add non-inline longitude and latitude gridline labels, and on
|
|
99
|
+
which sides of the map. Use the keyword `labels` to set both at once. The
|
|
100
|
+
argument must conform to one of the following options:
|
|
101
|
+
|
|
102
|
+
* A boolean. ``True`` indicates the bottom side for longitudes and
|
|
103
|
+
the left side for latitudes, and ``False`` disables all labels.
|
|
104
|
+
* A string or sequence of strings indicating the side names, e.g.
|
|
105
|
+
``'top'`` for longitudes or ``('left', 'right')`` for latitudes.
|
|
106
|
+
* A string indicating the side names with single characters, e.g.
|
|
107
|
+
``'bt'`` for longitudes or ``'lr'`` for latitudes.
|
|
108
|
+
* A string matching ``'neither'`` (no labels), ``'both'`` (equivalent
|
|
109
|
+
to ``'bt'`` for longitudes and ``'lr'`` for latitudes), or ``'all'``
|
|
110
|
+
(equivalent to ``'lrbt'``, i.e. all sides).
|
|
111
|
+
* A boolean 2-tuple indicating whether to draw labels
|
|
112
|
+
on the ``(bottom, top)`` sides for longitudes,
|
|
113
|
+
and the ``(left, right)`` sides for latitudes.
|
|
114
|
+
* A boolean 4-tuple indicating whether to draw labels on the
|
|
115
|
+
``(left, right, bottom, top)`` sides, as with the basemap
|
|
116
|
+
`~mpl_toolkits.basemap.Basemap.drawmeridians` and
|
|
117
|
+
`~mpl_toolkits.basemap.Basemap.drawparallels` `labels` keyword.
|
|
118
|
+
|
|
119
|
+
loninline, latinline, inlinelabels : bool, default: :rc:`grid.inlinelabels`
|
|
120
|
+
*For cartopy axes only.*
|
|
121
|
+
Whether to add inline longitude and latitude gridline labels. Use
|
|
122
|
+
the keyword `inlinelabels` to set both at once.
|
|
123
|
+
rotatelabels : bool, default: :rc:`grid.rotatelabels`
|
|
124
|
+
*For cartopy axes only.*
|
|
125
|
+
Whether to rotate non-inline gridline labels so that they automatically
|
|
126
|
+
follow the map boundary curvature.
|
|
127
|
+
labelpad : unit-spec, default: :rc:`grid.labelpad`
|
|
128
|
+
*For cartopy axes only.*
|
|
129
|
+
The padding between non-inline gridline labels and the map boundary.
|
|
130
|
+
%(units.pt)s
|
|
131
|
+
dms : bool, default: :rc:`grid.dmslabels`
|
|
132
|
+
*For cartopy axes only.*
|
|
133
|
+
Whether the default locators and formatters should use "minutes" and "seconds"
|
|
134
|
+
for gridline labels on small scales rather than decimal degrees. Setting this to
|
|
135
|
+
``False`` is equivalent to ``ax.format(lonlocator='deglon', latlocator='deglat')``
|
|
136
|
+
and ``ax.format(lonformatter='deglon', latformatter='deglat')``.
|
|
137
|
+
lonformatter, latformatter : formatter-spec, optional
|
|
138
|
+
Formatter used to style longitude and latitude gridline labels.
|
|
139
|
+
Passed to the `~ultraplot.constructor.Formatter` constructor. Can be
|
|
140
|
+
string, list of string, or `matplotlib.ticker.Formatter` instance.
|
|
141
|
+
|
|
142
|
+
For basemap or cartopy < 0.18, the defaults are ``'deglon'`` and
|
|
143
|
+
``'deglat'``, which correspond to `~ultraplot.ticker.SimpleFormatter`
|
|
144
|
+
presets with degree symbols and cardinal direction suffixes.
|
|
145
|
+
For cartopy >= 0.18, the defaults are ``'dmslon'`` and ``'dmslat'``,
|
|
146
|
+
which uses cartopy's `~cartopy.mpl.ticker.LongitudeFormatter` and
|
|
147
|
+
`~cartopy.mpl.ticker.LatitudeFormatter` formatters with ``dms=True``.
|
|
148
|
+
This formats gridlines that do not fall on whole degrees as "minutes" and
|
|
149
|
+
"seconds" rather than decimal degrees. Use ``dms=False`` to disable this.
|
|
150
|
+
lonformatter_kw, latformatter_kw : dict-like, optional
|
|
151
|
+
Keyword arguments passed to the `matplotlib.ticker.Formatter` class.
|
|
152
|
+
land, ocean, coast, rivers, lakes, borders, innerborders : bool, optional
|
|
153
|
+
Toggles various geographic features. These are actually the
|
|
154
|
+
:rcraw:`land`, :rcraw:`ocean`, :rcraw:`coast`, :rcraw:`rivers`,
|
|
155
|
+
:rcraw:`lakes`, :rcraw:`borders`, and :rcraw:`innerborders`
|
|
156
|
+
settings passed to `~ultraplot.config.Configurator.context`.
|
|
157
|
+
The style can be modified using additional `rc` settings.
|
|
158
|
+
|
|
159
|
+
For example, to change :rcraw:`land.color`, use
|
|
160
|
+
``ax.format(landcolor='green')``, and to change
|
|
161
|
+
:rcraw:`land.zorder`, use ``ax.format(landzorder=4)``.
|
|
162
|
+
reso : {'lo', 'med', 'hi', 'x-hi', 'xx-hi'}, optional
|
|
163
|
+
*For cartopy axes only.*
|
|
164
|
+
The resolution of geographic features. When basemap is the backend this
|
|
165
|
+
must be passed to `~ultraplot.constructor.Proj` instead.
|
|
166
|
+
color : color-spec, default: :rc:`meta.color`
|
|
167
|
+
The color for the axes edge. Propagates to `labelcolor` unless specified
|
|
168
|
+
otherwise (similar to `ultraplot.axes.CartesianAxes.format`).
|
|
169
|
+
gridcolor : color-spec, default: :rc:`grid.color`
|
|
170
|
+
The color for the gridline labels.
|
|
171
|
+
labelcolor : color-spec, default: `color` or :rc:`grid.labelcolor`
|
|
172
|
+
The color for the gridline labels (`gridlabelcolor` is also allowed).
|
|
173
|
+
labelsize : unit-spec or str, default: :rc:`grid.labelsize`
|
|
174
|
+
The font size for the gridline labels (`gridlabelsize` is also allowed).
|
|
175
|
+
%(units.pt)s
|
|
176
|
+
labelweight : str, default: :rc:`grid.labelweight`
|
|
177
|
+
The font weight for the gridline labels (`gridlabelweight` is also allowed).
|
|
178
|
+
"""
|
|
179
|
+
docstring._snippet_manager["geo.format"] = _format_docstring
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _GeoLabel(object):
|
|
183
|
+
"""
|
|
184
|
+
Optionally omit overlapping check if an rc setting is disabled.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def check_overlapping(self, *args, **kwargs):
|
|
188
|
+
if rc["grid.checkoverlap"]:
|
|
189
|
+
return super().check_overlapping(*args, **kwargs)
|
|
190
|
+
else:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Add monkey patch to gridliner module
|
|
195
|
+
if cgridliner is not None and hasattr(cgridliner, "Label"): # only recent versions
|
|
196
|
+
_cls = type("Label", (_GeoLabel, cgridliner.Label), {})
|
|
197
|
+
cgridliner.Label = _cls
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class _GeoAxis(object):
|
|
201
|
+
"""
|
|
202
|
+
Dummy axis used by longitude and latitude locators and for storing view limits on
|
|
203
|
+
longitude and latitude coordinates. Modeled after how `matplotlib.ticker._DummyAxis`
|
|
204
|
+
and `matplotlib.ticker.TickHelper` are used to control tick locations and labels.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
# NOTE: Due to cartopy bug (https://github.com/SciTools/cartopy/issues/1564)
|
|
208
|
+
# we store presistent longitude and latitude locators on axes, then *call*
|
|
209
|
+
# them whenever set_extent is called and apply *fixed* locators.
|
|
210
|
+
def __init__(self, axes):
|
|
211
|
+
self.axes = axes
|
|
212
|
+
self.major = maxis.Ticker()
|
|
213
|
+
self.minor = maxis.Ticker()
|
|
214
|
+
self.isDefault_majfmt = True
|
|
215
|
+
self.isDefault_majloc = True
|
|
216
|
+
self.isDefault_minloc = True
|
|
217
|
+
self._interval = None
|
|
218
|
+
self._use_dms = (
|
|
219
|
+
ccrs is not None
|
|
220
|
+
and isinstance(
|
|
221
|
+
axes.projection, (ccrs._RectangularProjection, ccrs.Mercator)
|
|
222
|
+
) # noqa: E501
|
|
223
|
+
and _version_cartopy >= "0.18"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _get_extent(self):
|
|
227
|
+
# Try to get extent but bail out for projections where this is
|
|
228
|
+
# impossible. So far just transverse Mercator
|
|
229
|
+
try:
|
|
230
|
+
return self.axes.get_extent()
|
|
231
|
+
except Exception:
|
|
232
|
+
lon0 = self.axes._get_lon0()
|
|
233
|
+
return (-180 + lon0, 180 + lon0, -90, 90)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def _pad_ticks(ticks, vmin, vmax):
|
|
237
|
+
# Wrap up to the longitude/latitude range to avoid
|
|
238
|
+
# giant lists of 10,000 gridline locations.
|
|
239
|
+
if len(ticks) == 0:
|
|
240
|
+
return ticks
|
|
241
|
+
range_ = np.max(ticks) - np.min(ticks)
|
|
242
|
+
vmin = max(vmin, ticks[0] - range_)
|
|
243
|
+
vmax = min(vmax, ticks[-1] + range_)
|
|
244
|
+
|
|
245
|
+
# Pad the reported tick range up to specified range
|
|
246
|
+
step = ticks[1] - ticks[0] # MaxNLocator/AutoMinorLocator steps are equal
|
|
247
|
+
ticks_lo = np.arange(ticks[0], vmin, -step)[1:][::-1]
|
|
248
|
+
ticks_hi = np.arange(ticks[-1], vmax, step)[1:]
|
|
249
|
+
ticks = np.concatenate((ticks_lo, ticks, ticks_hi))
|
|
250
|
+
return ticks
|
|
251
|
+
|
|
252
|
+
def get_scale(self):
|
|
253
|
+
return "linear"
|
|
254
|
+
|
|
255
|
+
def get_tick_space(self):
|
|
256
|
+
return 9 # longstanding default of nbins=9
|
|
257
|
+
|
|
258
|
+
def get_major_formatter(self):
|
|
259
|
+
return self.major.formatter
|
|
260
|
+
|
|
261
|
+
def get_major_locator(self):
|
|
262
|
+
return self.major.locator
|
|
263
|
+
|
|
264
|
+
def get_minor_locator(self):
|
|
265
|
+
return self.minor.locator
|
|
266
|
+
|
|
267
|
+
def get_majorticklocs(self):
|
|
268
|
+
return self._get_ticklocs(self.major.locator)
|
|
269
|
+
|
|
270
|
+
def get_minorticklocs(self):
|
|
271
|
+
return self._get_ticklocs(self.minor.locator)
|
|
272
|
+
|
|
273
|
+
def set_major_formatter(self, formatter, default=False):
|
|
274
|
+
# NOTE: Cartopy formatters check Formatter.axis.axes.projection
|
|
275
|
+
# in order to implement special projection-dependent behavior.
|
|
276
|
+
self.major.formatter = formatter
|
|
277
|
+
formatter.set_axis(self)
|
|
278
|
+
self.isDefault_majfmt = default
|
|
279
|
+
|
|
280
|
+
def set_major_locator(self, locator, default=False):
|
|
281
|
+
self.major.locator = locator
|
|
282
|
+
if self.major.formatter:
|
|
283
|
+
self.major.formatter._set_locator(locator)
|
|
284
|
+
locator.set_axis(self)
|
|
285
|
+
self.isDefault_majloc = default
|
|
286
|
+
|
|
287
|
+
def set_minor_locator(self, locator, default=False):
|
|
288
|
+
self.minor.locator = locator
|
|
289
|
+
locator.set_axis(self)
|
|
290
|
+
self.isDefault_majfmt = default
|
|
291
|
+
|
|
292
|
+
def set_view_interval(self, vmin, vmax):
|
|
293
|
+
self._interval = (vmin, vmax)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class _LonAxis(_GeoAxis):
|
|
297
|
+
"""
|
|
298
|
+
Axis with default longitude locator.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
axis_name = "lon"
|
|
302
|
+
|
|
303
|
+
# NOTE: Basemap accepts tick formatters with drawmeridians(fmt=Formatter())
|
|
304
|
+
# Try to use cartopy formatter if cartopy installed. Otherwise use
|
|
305
|
+
# default builtin basemap formatting.
|
|
306
|
+
def __init__(self, axes):
|
|
307
|
+
super().__init__(axes)
|
|
308
|
+
if self._use_dms:
|
|
309
|
+
locator = formatter = "dmslon"
|
|
310
|
+
else:
|
|
311
|
+
locator = formatter = "deglon"
|
|
312
|
+
self.set_major_formatter(constructor.Formatter(formatter), default=True)
|
|
313
|
+
self.set_major_locator(constructor.Locator(locator), default=True)
|
|
314
|
+
self.set_minor_locator(mticker.AutoMinorLocator(), default=True)
|
|
315
|
+
|
|
316
|
+
def _get_ticklocs(self, locator):
|
|
317
|
+
# Prevent ticks from looping around
|
|
318
|
+
# NOTE: Cartopy 0.17 formats numbers offset by eps with the cardinal indicator
|
|
319
|
+
# (e.g. 0 degrees for map centered on 180 degrees). So skip in that case.
|
|
320
|
+
# NOTE: Common strange issue is e.g. MultipleLocator(60) starts out at
|
|
321
|
+
# -60 degrees for a map from 0 to 360 degrees. If always trimmed circular
|
|
322
|
+
# locations from right then would cut off rightmost gridline. Workaround is
|
|
323
|
+
# to trim on the side closest to central longitude (in this case the left).
|
|
324
|
+
eps = 1e-10
|
|
325
|
+
lon0 = self.axes._get_lon0()
|
|
326
|
+
ticks = np.sort(locator())
|
|
327
|
+
while ticks.size:
|
|
328
|
+
if np.isclose(ticks[0] + 360, ticks[-1]):
|
|
329
|
+
if _version_cartopy >= "0.18" or not np.isclose(ticks[0] % 360, 0):
|
|
330
|
+
ticks[-1] -= eps # ensure label appears on *right* not left
|
|
331
|
+
break
|
|
332
|
+
elif ticks[0] + 360 < ticks[-1]:
|
|
333
|
+
idx = (1, None) if lon0 - ticks[0] > ticks[-1] - lon0 else (None, -1)
|
|
334
|
+
ticks = ticks[slice(*idx)] # cut off ticks looped over globe
|
|
335
|
+
else:
|
|
336
|
+
break
|
|
337
|
+
|
|
338
|
+
# Append extra ticks in case longitude/latitude limits do not encompass
|
|
339
|
+
# the entire view range of map, e.g. for Lambert Conformal sectors.
|
|
340
|
+
# NOTE: Try to avoid making 10,000 element lists. Just wrap extra ticks
|
|
341
|
+
# up to the width of *reported* longitude range.
|
|
342
|
+
if isinstance(locator, (mticker.MaxNLocator, mticker.AutoMinorLocator)):
|
|
343
|
+
ticks = self._pad_ticks(ticks, lon0 - 180 + eps, lon0 + 180 - eps)
|
|
344
|
+
|
|
345
|
+
return ticks
|
|
346
|
+
|
|
347
|
+
def get_view_interval(self):
|
|
348
|
+
# NOTE: ultraplot tries to set its *own* view intervals to avoid dateline
|
|
349
|
+
# weirdness, but if rc['geo.extent'] is 'auto' the interval will be unset.
|
|
350
|
+
# In this case we use _get_extent() as a backup.
|
|
351
|
+
interval = self._interval
|
|
352
|
+
if interval is None:
|
|
353
|
+
extent = self._get_extent()
|
|
354
|
+
interval = extent[:2] # longitude extents
|
|
355
|
+
return interval
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class _LatAxis(_GeoAxis):
|
|
359
|
+
"""
|
|
360
|
+
Axis with default latitude locator.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
axis_name = "lat"
|
|
364
|
+
|
|
365
|
+
def __init__(self, axes, latmax=90):
|
|
366
|
+
# NOTE: Need to pass projection because lataxis/lonaxis are
|
|
367
|
+
# initialized before geoaxes is initialized, because format() needs
|
|
368
|
+
# the axes and format() is called by ultraplot.axes.Axes.__init__()
|
|
369
|
+
self._latmax = latmax
|
|
370
|
+
super().__init__(axes)
|
|
371
|
+
if self._use_dms:
|
|
372
|
+
locator = formatter = "dmslat"
|
|
373
|
+
else:
|
|
374
|
+
locator = formatter = "deglat"
|
|
375
|
+
self.set_major_formatter(constructor.Formatter(formatter), default=True)
|
|
376
|
+
self.set_major_locator(constructor.Locator(locator), default=True)
|
|
377
|
+
self.set_minor_locator(mticker.AutoMinorLocator(), default=True)
|
|
378
|
+
|
|
379
|
+
def _get_ticklocs(self, locator):
|
|
380
|
+
# Adjust latitude ticks to fix bug in some projections. Harmless for basemap.
|
|
381
|
+
# NOTE: Maybe this was fixed by cartopy 0.18?
|
|
382
|
+
eps = 1e-10
|
|
383
|
+
ticks = np.sort(locator())
|
|
384
|
+
if ticks.size:
|
|
385
|
+
if ticks[0] == -90:
|
|
386
|
+
ticks[0] += eps
|
|
387
|
+
if ticks[-1] == 90:
|
|
388
|
+
ticks[-1] -= eps
|
|
389
|
+
|
|
390
|
+
# Append extra ticks in case longitude/latitude limits do not encompass
|
|
391
|
+
# the entire view range of map, e.g. for Lambert Conformal sectors.
|
|
392
|
+
if isinstance(locator, (mticker.MaxNLocator, mticker.AutoMinorLocator)):
|
|
393
|
+
ticks = self._pad_ticks(ticks, -90 + eps, 90 - eps)
|
|
394
|
+
|
|
395
|
+
# Filter ticks to latmax range
|
|
396
|
+
latmax = self.get_latmax()
|
|
397
|
+
ticks = ticks[(ticks >= -latmax) & (ticks <= latmax)]
|
|
398
|
+
|
|
399
|
+
return ticks
|
|
400
|
+
|
|
401
|
+
def get_latmax(self):
|
|
402
|
+
return self._latmax
|
|
403
|
+
|
|
404
|
+
def get_view_interval(self):
|
|
405
|
+
interval = self._interval
|
|
406
|
+
if interval is None:
|
|
407
|
+
extent = self._get_extent()
|
|
408
|
+
interval = extent[2:] # latitudes
|
|
409
|
+
return interval
|
|
410
|
+
|
|
411
|
+
def set_latmax(self, latmax):
|
|
412
|
+
self._latmax = latmax
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class GeoAxes(plot.PlotAxes):
|
|
416
|
+
"""
|
|
417
|
+
Axes subclass for plotting in geographic projections. Uses either cartopy
|
|
418
|
+
or basemap as a "backend".
|
|
419
|
+
|
|
420
|
+
Note
|
|
421
|
+
----
|
|
422
|
+
This subclass uses longitude and latitude as the default coordinate system for all
|
|
423
|
+
plotting commands by internally passing ``transform=cartopy.crs.PlateCarree()`` to
|
|
424
|
+
cartopy commands and ``latlon=True`` to basemap commands. Also, when using basemap
|
|
425
|
+
as the "backend", plotting is still done "cartopy-style" by calling methods from
|
|
426
|
+
the axes instance rather than the `~mpl_toolkits.basemap.Basemap` instance.
|
|
427
|
+
|
|
428
|
+
Important
|
|
429
|
+
---------
|
|
430
|
+
This axes subclass can be used by passing ``proj='proj_name'``
|
|
431
|
+
to axes-creation commands like `~ultraplot.figure.Figure.add_axes`,
|
|
432
|
+
`~ultraplot.figure.Figure.add_subplot`, and `~ultraplot.figure.Figure.subplots`,
|
|
433
|
+
where ``proj_name`` is a registered :ref:`PROJ projection name <proj_table>`.
|
|
434
|
+
You can also pass a `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap`
|
|
435
|
+
instance instead of a projection name. Alternatively, you can pass any of the
|
|
436
|
+
matplotlib-recognized axes subclass names ``proj='cartopy'``, ``proj='geo'``, or
|
|
437
|
+
``proj='geographic'`` with a `~cartopy.crs.Projection` `map_projection` keyword
|
|
438
|
+
argument, or pass ``proj='basemap'`` with a `~mpl_toolkits.basemap.Basemap`
|
|
439
|
+
`map_projection` keyword argument.
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
@docstring._snippet_manager
|
|
443
|
+
def __init__(self, *args, **kwargs):
|
|
444
|
+
"""
|
|
445
|
+
Parameters
|
|
446
|
+
----------
|
|
447
|
+
*args
|
|
448
|
+
Passed to `matplotlib.axes.Axes`.
|
|
449
|
+
map_projection : `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap`
|
|
450
|
+
The cartopy or basemap projection instance. This is
|
|
451
|
+
passed automatically when calling axes-creation
|
|
452
|
+
commands like `~ultraplot.figure.Figure.add_subplot`.
|
|
453
|
+
%(geo.format)s
|
|
454
|
+
|
|
455
|
+
Other parameters
|
|
456
|
+
----------------
|
|
457
|
+
%(axes.format)s
|
|
458
|
+
%(rc.init)s
|
|
459
|
+
|
|
460
|
+
See also
|
|
461
|
+
--------
|
|
462
|
+
GeoAxes.format
|
|
463
|
+
ultraplot.constructor.Proj
|
|
464
|
+
ultraplot.axes.Axes
|
|
465
|
+
ultraplot.axes.PlotAxes
|
|
466
|
+
ultraplot.figure.Figure.subplot
|
|
467
|
+
ultraplot.figure.Figure.add_subplot
|
|
468
|
+
"""
|
|
469
|
+
super().__init__(*args, **kwargs)
|
|
470
|
+
|
|
471
|
+
def _get_lonticklocs(self, which="major"):
|
|
472
|
+
"""
|
|
473
|
+
Retrieve longitude tick locations.
|
|
474
|
+
"""
|
|
475
|
+
# Get tick locations from dummy axes
|
|
476
|
+
# NOTE: This is workaround for: https://github.com/SciTools/cartopy/issues/1564
|
|
477
|
+
# Since _axes_domain is wrong we determine tick locations ourselves with
|
|
478
|
+
# more accurate extent tracked by _LatAxis and _LonAxis.
|
|
479
|
+
axis = self._lonaxis
|
|
480
|
+
if which == "major":
|
|
481
|
+
lines = axis.get_majorticklocs()
|
|
482
|
+
else:
|
|
483
|
+
lines = axis.get_minorticklocs()
|
|
484
|
+
return lines
|
|
485
|
+
|
|
486
|
+
def _get_latticklocs(self, which="major"):
|
|
487
|
+
"""
|
|
488
|
+
Retrieve latitude tick locations.
|
|
489
|
+
"""
|
|
490
|
+
axis = self._lataxis
|
|
491
|
+
if which == "major":
|
|
492
|
+
lines = axis.get_majorticklocs()
|
|
493
|
+
else:
|
|
494
|
+
lines = axis.get_minorticklocs()
|
|
495
|
+
return lines
|
|
496
|
+
|
|
497
|
+
def _set_view_intervals(self, extent):
|
|
498
|
+
"""
|
|
499
|
+
Update view intervals for lon and lat axis.
|
|
500
|
+
"""
|
|
501
|
+
self._lonaxis.set_view_interval(*extent[:2])
|
|
502
|
+
self._lataxis.set_view_interval(*extent[2:])
|
|
503
|
+
|
|
504
|
+
@staticmethod
|
|
505
|
+
def _to_label_array(arg, lon=True):
|
|
506
|
+
"""
|
|
507
|
+
Convert labels argument to length-5 boolean array.
|
|
508
|
+
"""
|
|
509
|
+
array = arg
|
|
510
|
+
which = "lon" if lon else "lat"
|
|
511
|
+
array = np.atleast_1d(array).tolist()
|
|
512
|
+
if len(array) == 1 and array[0] is None:
|
|
513
|
+
array = [None] * 5
|
|
514
|
+
elif all(isinstance(_, str) for _ in array):
|
|
515
|
+
strings = array # iterate over list of strings
|
|
516
|
+
array = [False] * 5
|
|
517
|
+
opts = ("left", "right", "bottom", "top", "geo")
|
|
518
|
+
for string in strings:
|
|
519
|
+
if string == "all":
|
|
520
|
+
string = "lrbt"
|
|
521
|
+
elif string == "both":
|
|
522
|
+
string = "bt" if lon else "lr"
|
|
523
|
+
elif string == "neither":
|
|
524
|
+
string = ""
|
|
525
|
+
elif string in opts:
|
|
526
|
+
string = string[0]
|
|
527
|
+
if set(string) - set("lrbtg"):
|
|
528
|
+
raise ValueError(
|
|
529
|
+
f"Invalid {which}label string {string!r}. Must be one of "
|
|
530
|
+
+ ", ".join(map(repr, (*opts, "neither", "both", "all")))
|
|
531
|
+
+ " or a string of single-letter characters like 'lr'."
|
|
532
|
+
)
|
|
533
|
+
for char in string:
|
|
534
|
+
array["lrbtg".index(char)] = True
|
|
535
|
+
if rc["grid.geolabels"] and any(array):
|
|
536
|
+
array[4] = True # possibly toggle geo spine labels
|
|
537
|
+
elif not any(isinstance(_, str) for _ in array):
|
|
538
|
+
if len(array) == 1:
|
|
539
|
+
array.append(False) # default is to label bottom or left
|
|
540
|
+
if len(array) == 2:
|
|
541
|
+
array = [False, False, *array] if lon else [*array, False, False]
|
|
542
|
+
if len(array) == 4:
|
|
543
|
+
b = any(array) if rc["grid.geolabels"] else False
|
|
544
|
+
array.append(b) # possibly toggle geo spine labels
|
|
545
|
+
if len(array) != 5:
|
|
546
|
+
raise ValueError(f"Invald boolean label array length {len(array)}.")
|
|
547
|
+
array = list(map(bool, array))
|
|
548
|
+
else:
|
|
549
|
+
raise ValueError(f"Invalid {which}label spec: {arg}.")
|
|
550
|
+
return array
|
|
551
|
+
|
|
552
|
+
@docstring._snippet_manager
|
|
553
|
+
def format(
|
|
554
|
+
self,
|
|
555
|
+
*,
|
|
556
|
+
extent=None,
|
|
557
|
+
round=None,
|
|
558
|
+
lonlim=None,
|
|
559
|
+
latlim=None,
|
|
560
|
+
boundinglat=None,
|
|
561
|
+
longrid=None,
|
|
562
|
+
latgrid=None,
|
|
563
|
+
longridminor=None,
|
|
564
|
+
latgridminor=None,
|
|
565
|
+
latmax=None,
|
|
566
|
+
nsteps=None,
|
|
567
|
+
lonlocator=None,
|
|
568
|
+
lonlines=None,
|
|
569
|
+
latlocator=None,
|
|
570
|
+
latlines=None,
|
|
571
|
+
lonminorlocator=None,
|
|
572
|
+
lonminorlines=None,
|
|
573
|
+
latminorlocator=None,
|
|
574
|
+
latminorlines=None,
|
|
575
|
+
lonlocator_kw=None,
|
|
576
|
+
lonlines_kw=None,
|
|
577
|
+
latlocator_kw=None,
|
|
578
|
+
latlines_kw=None,
|
|
579
|
+
lonminorlocator_kw=None,
|
|
580
|
+
lonminorlines_kw=None,
|
|
581
|
+
latminorlocator_kw=None,
|
|
582
|
+
latminorlines_kw=None,
|
|
583
|
+
lonformatter=None,
|
|
584
|
+
latformatter=None,
|
|
585
|
+
lonformatter_kw=None,
|
|
586
|
+
latformatter_kw=None,
|
|
587
|
+
labels=None,
|
|
588
|
+
latlabels=None,
|
|
589
|
+
lonlabels=None,
|
|
590
|
+
rotatelabels=None,
|
|
591
|
+
loninline=None,
|
|
592
|
+
latinline=None,
|
|
593
|
+
inlinelabels=None,
|
|
594
|
+
dms=None,
|
|
595
|
+
labelpad=None,
|
|
596
|
+
labelcolor=None,
|
|
597
|
+
labelsize=None,
|
|
598
|
+
labelweight=None,
|
|
599
|
+
**kwargs,
|
|
600
|
+
):
|
|
601
|
+
"""
|
|
602
|
+
Modify map limits, longitude and latitude
|
|
603
|
+
gridlines, geographic features, and more.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
%(geo.format)s
|
|
608
|
+
|
|
609
|
+
Other parameters
|
|
610
|
+
----------------
|
|
611
|
+
%(axes.format)s
|
|
612
|
+
%(figure.format)s
|
|
613
|
+
%(rc.format)s
|
|
614
|
+
|
|
615
|
+
See also
|
|
616
|
+
--------
|
|
617
|
+
ultraplot.axes.Axes.format
|
|
618
|
+
ultraplot.config.Configurator.context
|
|
619
|
+
"""
|
|
620
|
+
# Initialize map boundary
|
|
621
|
+
# WARNING: Normal workflow is Axes.format() does 'universal' tasks including
|
|
622
|
+
# updating the map boundary (in the future may also handle gridlines). However
|
|
623
|
+
# drawing gridlines before basemap map boundary will call set_axes_limits()
|
|
624
|
+
# which initializes a boundary hidden from external access. So we must call
|
|
625
|
+
# it here. Must do this between mpl.Axes.__init__() and base.Axes.format().
|
|
626
|
+
if self._name == "basemap" and self._map_boundary is None:
|
|
627
|
+
if self.projection.projection in self._proj_non_rectangular:
|
|
628
|
+
patch = self.projection.drawmapboundary(ax=self)
|
|
629
|
+
self._map_boundary = patch
|
|
630
|
+
else:
|
|
631
|
+
self.projection.set_axes_limits(self) # initialize aspect ratio
|
|
632
|
+
self._map_boundary = object() # sentinel
|
|
633
|
+
|
|
634
|
+
# Initiate context block
|
|
635
|
+
rc_kw, rc_mode = _pop_rc(kwargs)
|
|
636
|
+
lonlabels = _not_none(lonlabels, labels)
|
|
637
|
+
latlabels = _not_none(latlabels, labels)
|
|
638
|
+
if "0.18" <= _version_cartopy < "0.20":
|
|
639
|
+
lonlabels = _not_none(lonlabels, loninline, inlinelabels)
|
|
640
|
+
latlabels = _not_none(latlabels, latinline, inlinelabels)
|
|
641
|
+
labelcolor = _not_none(labelcolor, kwargs.get("color", None))
|
|
642
|
+
if labelcolor is not None:
|
|
643
|
+
rc_kw["grid.labelcolor"] = labelcolor
|
|
644
|
+
if labelsize is not None:
|
|
645
|
+
rc_kw["grid.labelsize"] = labelsize
|
|
646
|
+
if labelweight is not None:
|
|
647
|
+
rc_kw["grid.labelweight"] = labelweight
|
|
648
|
+
with rc.context(rc_kw, mode=rc_mode):
|
|
649
|
+
# Apply extent mode first
|
|
650
|
+
# NOTE: We deprecate autoextent on _CartopyAxes with _rename_kwargs which
|
|
651
|
+
# does not translate boolean flag. So here apply translation.
|
|
652
|
+
if extent is not None and not isinstance(extent, str):
|
|
653
|
+
extent = ("globe", "auto")[int(bool(extent))]
|
|
654
|
+
self._update_boundary(round)
|
|
655
|
+
self._update_extent_mode(extent, boundinglat)
|
|
656
|
+
|
|
657
|
+
# Retrieve label toggles
|
|
658
|
+
# NOTE: Cartopy 0.18 and 0.19 inline labels require any of
|
|
659
|
+
# top, bottom, left, or right to be toggled then ignores them.
|
|
660
|
+
# Later versions of cartopy permit both or neither labels.
|
|
661
|
+
labels = _not_none(labels, rc.find("grid.labels", context=True))
|
|
662
|
+
lonlabels = _not_none(lonlabels, labels)
|
|
663
|
+
latlabels = _not_none(latlabels, labels)
|
|
664
|
+
lonarray = self._to_label_array(lonlabels, lon=True)
|
|
665
|
+
latarray = self._to_label_array(latlabels, lon=False)
|
|
666
|
+
|
|
667
|
+
# Update max latitude
|
|
668
|
+
latmax = _not_none(latmax, rc.find("grid.latmax", context=True))
|
|
669
|
+
if latmax is not None:
|
|
670
|
+
self._lataxis.set_latmax(latmax)
|
|
671
|
+
|
|
672
|
+
# Update major locators
|
|
673
|
+
lonlocator = _not_none(lonlocator=lonlocator, lonlines=lonlines)
|
|
674
|
+
latlocator = _not_none(latlocator=latlocator, latlines=latlines)
|
|
675
|
+
if lonlocator is not None:
|
|
676
|
+
lonlocator_kw = _not_none(
|
|
677
|
+
lonlocator_kw=lonlocator_kw,
|
|
678
|
+
lonlines_kw=lonlines_kw,
|
|
679
|
+
default={},
|
|
680
|
+
)
|
|
681
|
+
locator = constructor.Locator(lonlocator, **lonlocator_kw)
|
|
682
|
+
self._lonaxis.set_major_locator(locator)
|
|
683
|
+
if latlocator is not None:
|
|
684
|
+
latlocator_kw = _not_none(
|
|
685
|
+
latlocator_kw=latlocator_kw,
|
|
686
|
+
latlines_kw=latlines_kw,
|
|
687
|
+
default={},
|
|
688
|
+
)
|
|
689
|
+
locator = constructor.Locator(latlocator, **latlocator_kw)
|
|
690
|
+
self._lataxis.set_major_locator(locator)
|
|
691
|
+
|
|
692
|
+
# Update minor locators
|
|
693
|
+
lonminorlocator = _not_none(
|
|
694
|
+
lonminorlocator=lonminorlocator, lonminorlines=lonminorlines
|
|
695
|
+
)
|
|
696
|
+
latminorlocator = _not_none(
|
|
697
|
+
latminorlocator=latminorlocator, latminorlines=latminorlines
|
|
698
|
+
)
|
|
699
|
+
if lonminorlocator is not None:
|
|
700
|
+
lonminorlocator_kw = _not_none(
|
|
701
|
+
lonminorlocator_kw=lonminorlocator_kw,
|
|
702
|
+
lonminorlines_kw=lonminorlines_kw,
|
|
703
|
+
default={},
|
|
704
|
+
)
|
|
705
|
+
locator = constructor.Locator(lonminorlocator, **lonminorlocator_kw)
|
|
706
|
+
self._lonaxis.set_minor_locator(locator)
|
|
707
|
+
if latminorlocator is not None:
|
|
708
|
+
latminorlocator_kw = _not_none(
|
|
709
|
+
latminorlocator_kw=latminorlocator_kw,
|
|
710
|
+
latminorlines_kw=latminorlines_kw,
|
|
711
|
+
default={},
|
|
712
|
+
)
|
|
713
|
+
locator = constructor.Locator(latminorlocator, **latminorlocator_kw)
|
|
714
|
+
self._lataxis.set_minor_locator(locator)
|
|
715
|
+
|
|
716
|
+
# Update formatters
|
|
717
|
+
loninline = _not_none(
|
|
718
|
+
loninline, inlinelabels, rc.find("grid.inlinelabels", context=True)
|
|
719
|
+
) # noqa: E501
|
|
720
|
+
latinline = _not_none(
|
|
721
|
+
latinline, inlinelabels, rc.find("grid.inlinelabels", context=True)
|
|
722
|
+
) # noqa: E501
|
|
723
|
+
rotatelabels = _not_none(
|
|
724
|
+
rotatelabels, rc.find("grid.rotatelabels", context=True)
|
|
725
|
+
) # noqa: E501
|
|
726
|
+
labelpad = _not_none(labelpad, rc.find("grid.labelpad", context=True))
|
|
727
|
+
dms = _not_none(dms, rc.find("grid.dmslabels", context=True))
|
|
728
|
+
nsteps = _not_none(nsteps, rc.find("grid.nsteps", context=True))
|
|
729
|
+
if lonformatter is not None:
|
|
730
|
+
lonformatter_kw = lonformatter_kw or {}
|
|
731
|
+
formatter = constructor.Formatter(lonformatter, **lonformatter_kw)
|
|
732
|
+
self._lonaxis.set_major_formatter(formatter)
|
|
733
|
+
if latformatter is not None:
|
|
734
|
+
latformatter_kw = latformatter_kw or {}
|
|
735
|
+
formatter = constructor.Formatter(latformatter, **latformatter_kw)
|
|
736
|
+
self._lataxis.set_major_formatter(formatter)
|
|
737
|
+
if dms is not None: # harmless if these are not GeoLocators
|
|
738
|
+
self._lonaxis.get_major_formatter()._dms = dms
|
|
739
|
+
self._lataxis.get_major_formatter()._dms = dms
|
|
740
|
+
self._lonaxis.get_major_locator()._dms = dms
|
|
741
|
+
self._lataxis.get_major_locator()._dms = dms
|
|
742
|
+
|
|
743
|
+
# Apply worker extent, feature, and gridline functions
|
|
744
|
+
lonlim = _not_none(lonlim, default=(None, None))
|
|
745
|
+
latlim = _not_none(latlim, default=(None, None))
|
|
746
|
+
self._update_extent(lonlim=lonlim, latlim=latlim, boundinglat=boundinglat)
|
|
747
|
+
self._update_features()
|
|
748
|
+
self._update_major_gridlines(
|
|
749
|
+
longrid=longrid,
|
|
750
|
+
latgrid=latgrid, # gridline toggles
|
|
751
|
+
lonarray=lonarray,
|
|
752
|
+
latarray=latarray, # label toggles
|
|
753
|
+
loninline=loninline,
|
|
754
|
+
latinline=latinline,
|
|
755
|
+
rotatelabels=rotatelabels,
|
|
756
|
+
labelpad=labelpad,
|
|
757
|
+
nsteps=nsteps,
|
|
758
|
+
)
|
|
759
|
+
self._update_minor_gridlines(
|
|
760
|
+
longrid=longridminor,
|
|
761
|
+
latgrid=latgridminor,
|
|
762
|
+
nsteps=nsteps,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
# Parent format method
|
|
766
|
+
super().format(rc_kw=rc_kw, rc_mode=rc_mode, **kwargs)
|
|
767
|
+
|
|
768
|
+
@property
|
|
769
|
+
def gridlines_major(self):
|
|
770
|
+
"""
|
|
771
|
+
The cartopy `~cartopy.mpl.gridliner.Gridliner`
|
|
772
|
+
used for major gridlines or a 2-tuple containing the
|
|
773
|
+
(longitude, latitude) major gridlines returned by
|
|
774
|
+
basemap's `~mpl_toolkits.basemap.Basemap.drawmeridians`
|
|
775
|
+
and `~mpl_toolkits.basemap.Basemap.drawparallels`.
|
|
776
|
+
This can be used for customization and debugging.
|
|
777
|
+
"""
|
|
778
|
+
if self._name == "basemap":
|
|
779
|
+
return (self._lonlines_major, self._latlines_major)
|
|
780
|
+
else:
|
|
781
|
+
return self._gridlines_major
|
|
782
|
+
|
|
783
|
+
@property
|
|
784
|
+
def gridlines_minor(self):
|
|
785
|
+
"""
|
|
786
|
+
The cartopy `~cartopy.mpl.gridliner.Gridliner`
|
|
787
|
+
used for minor gridlines or a 2-tuple containing the
|
|
788
|
+
(longitude, latitude) minor gridlines returned by
|
|
789
|
+
basemap's `~mpl_toolkits.basemap.Basemap.drawmeridians`
|
|
790
|
+
and `~mpl_toolkits.basemap.Basemap.drawparallels`.
|
|
791
|
+
This can be used for customization and debugging.
|
|
792
|
+
"""
|
|
793
|
+
if self._name == "basemap":
|
|
794
|
+
return (self._lonlines_minor, self._latlines_minor)
|
|
795
|
+
else:
|
|
796
|
+
return self._gridlines_minor
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def projection(self):
|
|
800
|
+
"""
|
|
801
|
+
The cartopy `~cartopy.crs.Projection` or basemap `~mpl_toolkits.basemap.Basemap`
|
|
802
|
+
instance associated with this axes.
|
|
803
|
+
"""
|
|
804
|
+
return self._map_projection
|
|
805
|
+
|
|
806
|
+
@projection.setter
|
|
807
|
+
def projection(self, map_projection):
|
|
808
|
+
cls = self._proj_class
|
|
809
|
+
if not isinstance(map_projection, cls):
|
|
810
|
+
raise ValueError(f"Projection must be a {cls} instance.")
|
|
811
|
+
self._map_projection = map_projection
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class _CartopyAxes(GeoAxes, _GeoAxes):
|
|
815
|
+
"""
|
|
816
|
+
Axes subclass for plotting cartopy projections.
|
|
817
|
+
"""
|
|
818
|
+
|
|
819
|
+
_name = "cartopy"
|
|
820
|
+
_name_aliases = ("geo", "geographic") # default 'geographic' axes
|
|
821
|
+
_proj_class = Projection
|
|
822
|
+
_proj_north = (
|
|
823
|
+
pproj.NorthPolarStereo,
|
|
824
|
+
pproj.NorthPolarGnomonic,
|
|
825
|
+
pproj.NorthPolarAzimuthalEquidistant,
|
|
826
|
+
pproj.NorthPolarLambertAzimuthalEqualArea,
|
|
827
|
+
)
|
|
828
|
+
_proj_south = (
|
|
829
|
+
pproj.SouthPolarStereo,
|
|
830
|
+
pproj.SouthPolarGnomonic,
|
|
831
|
+
pproj.SouthPolarAzimuthalEquidistant,
|
|
832
|
+
pproj.SouthPolarLambertAzimuthalEqualArea,
|
|
833
|
+
)
|
|
834
|
+
_proj_polar = _proj_north + _proj_south
|
|
835
|
+
|
|
836
|
+
# NOTE: The rename argument wrapper belongs here instead of format() because
|
|
837
|
+
# these arguments were previously only accepted during initialization.
|
|
838
|
+
@warnings._rename_kwargs("0.10", circular="round", autoextent="extent")
|
|
839
|
+
def __init__(self, *args, map_projection=None, **kwargs):
|
|
840
|
+
"""
|
|
841
|
+
Parameters
|
|
842
|
+
----------
|
|
843
|
+
map_projection : ~cartopy.crs.Projection
|
|
844
|
+
The map projection.
|
|
845
|
+
*args, **kwargs
|
|
846
|
+
Passed to `GeoAxes`.
|
|
847
|
+
"""
|
|
848
|
+
# Initialize axes. Note that critical attributes like outline_patch
|
|
849
|
+
# needed by _format_apply are added before it is called.
|
|
850
|
+
import cartopy # noqa: F401 verify package is available
|
|
851
|
+
|
|
852
|
+
self.projection = map_projection # verify
|
|
853
|
+
polar = isinstance(self.projection, self._proj_polar)
|
|
854
|
+
latmax = 80 if polar else 90 # default latmax
|
|
855
|
+
self._is_round = False
|
|
856
|
+
self._boundinglat = None # NOTE: must start at None so _update_extent acts
|
|
857
|
+
self._gridlines_major = None
|
|
858
|
+
self._gridlines_minor = None
|
|
859
|
+
self._lonaxis = _LonAxis(self)
|
|
860
|
+
self._lataxis = _LatAxis(self, latmax=latmax)
|
|
861
|
+
# 'map_projection' argument is deprecated since cartopy 0.21 and
|
|
862
|
+
# replaced by 'projection'.
|
|
863
|
+
if _version_cartopy >= "0.21":
|
|
864
|
+
super().__init__(*args, projection=self.projection, **kwargs)
|
|
865
|
+
else:
|
|
866
|
+
super().__init__(*args, map_projection=self.projection, **kwargs)
|
|
867
|
+
for axis in (self.xaxis, self.yaxis):
|
|
868
|
+
axis.set_tick_params(which="both", size=0) # prevent extra label offset
|
|
869
|
+
|
|
870
|
+
def _apply_axis_sharing(self): # noqa: U100
|
|
871
|
+
"""
|
|
872
|
+
No-op for now. In future will hide labels on certain subplots.
|
|
873
|
+
"""
|
|
874
|
+
pass
|
|
875
|
+
|
|
876
|
+
@staticmethod
|
|
877
|
+
def _get_circle_path(N=100):
|
|
878
|
+
"""
|
|
879
|
+
Return a circle `~matplotlib.path.Path` used as the outline for polar
|
|
880
|
+
stereographic, azimuthal equidistant, Lambert conformal, and gnomonic
|
|
881
|
+
projections. This was developed from `this cartopy example \
|
|
882
|
+
<https://scitools.org.uk/cartopy/docs/v0.15/examples/always_circular_stereo.html>`__.
|
|
883
|
+
"""
|
|
884
|
+
theta = np.linspace(0, 2 * np.pi, N)
|
|
885
|
+
center, radius = [0.5, 0.5], 0.5
|
|
886
|
+
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
|
|
887
|
+
return mpath.Path(verts * radius + center)
|
|
888
|
+
|
|
889
|
+
def _get_global_extent(self):
|
|
890
|
+
"""
|
|
891
|
+
Return the global extent with meridian properly shifted.
|
|
892
|
+
"""
|
|
893
|
+
lon0 = self._get_lon0()
|
|
894
|
+
return [-180 + lon0, 180 + lon0, -90, 90]
|
|
895
|
+
|
|
896
|
+
def _get_lon0(self):
|
|
897
|
+
"""
|
|
898
|
+
Get the central longitude. Default is ``0``.
|
|
899
|
+
"""
|
|
900
|
+
return self.projection.proj4_params.get("lon_0", 0)
|
|
901
|
+
|
|
902
|
+
def _init_gridlines(self):
|
|
903
|
+
"""
|
|
904
|
+
Create monkey patched "major" and "minor" gridliners managed by ultraplot.
|
|
905
|
+
"""
|
|
906
|
+
|
|
907
|
+
# Cartopy < 0.18 monkey patch. Helps filter valid coordates to lon_0 +/- 180
|
|
908
|
+
def _axes_domain(self, *args, **kwargs):
|
|
909
|
+
x_range, y_range = type(self)._axes_domain(self, *args, **kwargs)
|
|
910
|
+
if _version_cartopy < "0.18":
|
|
911
|
+
lon_0 = self.axes.projection.proj4_params.get("lon_0", 0)
|
|
912
|
+
x_range = np.asarray(x_range) + lon_0
|
|
913
|
+
return x_range, y_range
|
|
914
|
+
|
|
915
|
+
# Cartopy >= 0.18 monkey patch. Fixes issue where cartopy draws an overlapping
|
|
916
|
+
# dateline gridline (e.g. polar maps). See the nx -= 1 line in _draw_gridliner
|
|
917
|
+
def _draw_gridliner(self, *args, **kwargs): # noqa: E306
|
|
918
|
+
result = type(self)._draw_gridliner(self, *args, **kwargs)
|
|
919
|
+
if _version_cartopy >= "0.18":
|
|
920
|
+
lon_lim, _ = self._axes_domain()
|
|
921
|
+
if abs(np.diff(lon_lim)) == abs(np.diff(self.crs.x_limits)):
|
|
922
|
+
for collection in self.xline_artists:
|
|
923
|
+
if not getattr(collection, "_cartopy_fix", False):
|
|
924
|
+
collection.get_paths().pop(-1)
|
|
925
|
+
collection._cartopy_fix = True
|
|
926
|
+
return result
|
|
927
|
+
|
|
928
|
+
# Return the gridliner with monkey patch
|
|
929
|
+
gl = self.gridlines(crs=ccrs.PlateCarree())
|
|
930
|
+
gl._axes_domain = _axes_domain.__get__(gl)
|
|
931
|
+
gl._draw_gridliner = _draw_gridliner.__get__(gl)
|
|
932
|
+
gl.xlines = gl.ylines = False
|
|
933
|
+
self._toggle_gridliner_labels(gl, False, False, False, False, False)
|
|
934
|
+
return gl
|
|
935
|
+
|
|
936
|
+
@staticmethod
|
|
937
|
+
def _toggle_gridliner_labels(
|
|
938
|
+
gl, left=None, right=None, bottom=None, top=None, geo=None
|
|
939
|
+
):
|
|
940
|
+
"""
|
|
941
|
+
Toggle gridliner labels across different cartopy versions.
|
|
942
|
+
"""
|
|
943
|
+
if _version_cartopy >= "0.18":
|
|
944
|
+
left_labels = "left_labels"
|
|
945
|
+
right_labels = "right_labels"
|
|
946
|
+
bottom_labels = "bottom_labels"
|
|
947
|
+
top_labels = "top_labels"
|
|
948
|
+
else: # cartopy < 0.18
|
|
949
|
+
left_labels = "ylabels_left"
|
|
950
|
+
right_labels = "ylabels_right"
|
|
951
|
+
bottom_labels = "xlabels_bottom"
|
|
952
|
+
top_labels = "xlabels_top"
|
|
953
|
+
if left is not None:
|
|
954
|
+
setattr(gl, left_labels, left)
|
|
955
|
+
if right is not None:
|
|
956
|
+
setattr(gl, right_labels, right)
|
|
957
|
+
if bottom is not None:
|
|
958
|
+
setattr(gl, bottom_labels, bottom)
|
|
959
|
+
if top is not None:
|
|
960
|
+
setattr(gl, top_labels, top)
|
|
961
|
+
if geo is not None: # only cartopy 0.20 supported but harmless
|
|
962
|
+
setattr(gl, "geo_labels", geo)
|
|
963
|
+
|
|
964
|
+
def _update_background(self, **kwargs):
|
|
965
|
+
"""
|
|
966
|
+
Update the map background patches. This is called in `Axes.format`.
|
|
967
|
+
"""
|
|
968
|
+
# TODO: Understand issue where setting global linewidth puts map boundary on
|
|
969
|
+
# top of land patches, but setting linewidth with format() (even with separate
|
|
970
|
+
# format() calls) puts map boundary underneath. Zorder seems to be totally
|
|
971
|
+
# ignored and using spines vs. patch makes no difference.
|
|
972
|
+
# NOTE: outline_patch is redundant, use background_patch instead
|
|
973
|
+
kw_face, kw_edge = rc._get_background_props(native=False, **kwargs)
|
|
974
|
+
kw_face["linewidth"] = 0
|
|
975
|
+
kw_edge["facecolor"] = "none"
|
|
976
|
+
if _version_cartopy >= "0.18":
|
|
977
|
+
self.patch.update(kw_face)
|
|
978
|
+
self.spines["geo"].update(kw_edge)
|
|
979
|
+
else:
|
|
980
|
+
self.background_patch.update(kw_face)
|
|
981
|
+
self.outline_patch.update(kw_edge)
|
|
982
|
+
|
|
983
|
+
def _update_boundary(self, round=None):
|
|
984
|
+
"""
|
|
985
|
+
Update the map boundary path.
|
|
986
|
+
"""
|
|
987
|
+
round = _not_none(round, rc.find("geo.round", context=True))
|
|
988
|
+
if round is None or not isinstance(self.projection, self._proj_polar):
|
|
989
|
+
pass
|
|
990
|
+
elif round:
|
|
991
|
+
self._is_round = True
|
|
992
|
+
self.set_boundary(self._get_circle_path(), transform=self.transAxes)
|
|
993
|
+
elif not round and self._is_round:
|
|
994
|
+
if hasattr(self, "_boundary"):
|
|
995
|
+
self._boundary()
|
|
996
|
+
else:
|
|
997
|
+
warnings._warn_ultraplot("Failed to reset round map boundary.")
|
|
998
|
+
|
|
999
|
+
def _update_extent_mode(self, extent=None, boundinglat=None):
|
|
1000
|
+
"""
|
|
1001
|
+
Update the extent mode.
|
|
1002
|
+
"""
|
|
1003
|
+
# NOTE: Use set_global rather than set_extent() or _update_extent() for
|
|
1004
|
+
# simplicity. Uses projection.[xy]_limits which may not be strictly global.
|
|
1005
|
+
# NOTE: For some reason initial call to _set_view_intervals may change the
|
|
1006
|
+
# default boundary with extent='auto'. Try this in a robinson projection:
|
|
1007
|
+
# ax.contour(np.linspace(-90, 180, N), np.linspace(0, 90, N), np.zeros(N, N))
|
|
1008
|
+
extent = _not_none(extent, rc.find("geo.extent", context=True))
|
|
1009
|
+
if extent is None:
|
|
1010
|
+
return
|
|
1011
|
+
if extent not in ("globe", "auto"):
|
|
1012
|
+
raise ValueError(
|
|
1013
|
+
f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'."
|
|
1014
|
+
)
|
|
1015
|
+
polar = isinstance(self.projection, self._proj_polar)
|
|
1016
|
+
if not polar:
|
|
1017
|
+
self.set_global()
|
|
1018
|
+
else:
|
|
1019
|
+
if isinstance(self.projection, pproj.NorthPolarGnomonic):
|
|
1020
|
+
default_boundinglat = 30
|
|
1021
|
+
elif isinstance(self.projection, pproj.SouthPolarGnomonic):
|
|
1022
|
+
default_boundinglat = -30
|
|
1023
|
+
else:
|
|
1024
|
+
default_boundinglat = 0
|
|
1025
|
+
boundinglat = _not_none(boundinglat, default_boundinglat)
|
|
1026
|
+
self._update_extent(boundinglat=boundinglat)
|
|
1027
|
+
if extent == "auto":
|
|
1028
|
+
# NOTE: This will work even if applied after plotting stuff
|
|
1029
|
+
# and fixing the limits. Very easy to toggle on and off.
|
|
1030
|
+
self.set_autoscalex_on(True)
|
|
1031
|
+
self.set_autoscaley_on(True)
|
|
1032
|
+
|
|
1033
|
+
def _update_extent(self, lonlim=None, latlim=None, boundinglat=None):
|
|
1034
|
+
"""
|
|
1035
|
+
Set the projection extent.
|
|
1036
|
+
"""
|
|
1037
|
+
# Projection extent
|
|
1038
|
+
# NOTE: Lon axis and lat axis extents are updated by set_extent.
|
|
1039
|
+
# WARNING: The set_extent method tries to set a *rectangle* between the *4*
|
|
1040
|
+
# (x, y) coordinate pairs (each corner), so something like (-180, 180, -90, 90)
|
|
1041
|
+
# will result in *line*, causing error! We correct this here.
|
|
1042
|
+
eps = 1e-10 # bug with full -180, 180 range when lon_0 != 0
|
|
1043
|
+
lon0 = self._get_lon0()
|
|
1044
|
+
proj = type(self.projection).__name__
|
|
1045
|
+
north = isinstance(self.projection, self._proj_north)
|
|
1046
|
+
south = isinstance(self.projection, self._proj_south)
|
|
1047
|
+
lonlim = _not_none(lonlim, (None, None))
|
|
1048
|
+
latlim = _not_none(latlim, (None, None))
|
|
1049
|
+
if north or south:
|
|
1050
|
+
if any(_ is not None for _ in (*lonlim, *latlim)):
|
|
1051
|
+
warnings._warn_ultraplot(
|
|
1052
|
+
f'{proj!r} extent is controlled by "boundinglat", '
|
|
1053
|
+
f"ignoring lonlim={lonlim!r} and latlim={latlim!r}."
|
|
1054
|
+
)
|
|
1055
|
+
if boundinglat is not None and boundinglat != self._boundinglat:
|
|
1056
|
+
lat0 = 90 if north else -90
|
|
1057
|
+
lon0 = self._get_lon0()
|
|
1058
|
+
extent = [lon0 - 180 + eps, lon0 + 180 - eps, boundinglat, lat0]
|
|
1059
|
+
self.set_extent(extent, crs=ccrs.PlateCarree())
|
|
1060
|
+
self._boundinglat = boundinglat
|
|
1061
|
+
|
|
1062
|
+
# Rectangular extent
|
|
1063
|
+
else:
|
|
1064
|
+
if boundinglat is not None:
|
|
1065
|
+
warnings._warn_ultraplot(
|
|
1066
|
+
f'{proj!r} extent is controlled by "lonlim" and "latlim", '
|
|
1067
|
+
f"ignoring boundinglat={boundinglat!r}."
|
|
1068
|
+
)
|
|
1069
|
+
if any(_ is not None for _ in (*lonlim, *latlim)):
|
|
1070
|
+
lonlim = list(lonlim)
|
|
1071
|
+
if lonlim[0] is None:
|
|
1072
|
+
lonlim[0] = lon0 - 180
|
|
1073
|
+
if lonlim[1] is None:
|
|
1074
|
+
lonlim[1] = lon0 + 180
|
|
1075
|
+
lonlim[0] += eps
|
|
1076
|
+
latlim = list(latlim)
|
|
1077
|
+
if latlim[0] is None:
|
|
1078
|
+
latlim[0] = -90
|
|
1079
|
+
if latlim[1] is None:
|
|
1080
|
+
latlim[1] = 90
|
|
1081
|
+
extent = lonlim + latlim
|
|
1082
|
+
self.set_extent(extent, crs=ccrs.PlateCarree())
|
|
1083
|
+
|
|
1084
|
+
def _update_features(self):
|
|
1085
|
+
"""
|
|
1086
|
+
Update geographic features.
|
|
1087
|
+
"""
|
|
1088
|
+
# NOTE: The e.g. cfeature.COASTLINE features are just for convenience,
|
|
1089
|
+
# lo res versions. Use NaturalEarthFeature instead.
|
|
1090
|
+
# WARNING: Seems cartopy features cannot be updated! Updating _kwargs
|
|
1091
|
+
# attribute does *nothing*.
|
|
1092
|
+
reso = rc["reso"] # resolution cannot be changed after feature created
|
|
1093
|
+
try:
|
|
1094
|
+
reso = constructor.RESOS_CARTOPY[reso]
|
|
1095
|
+
except KeyError:
|
|
1096
|
+
raise ValueError(
|
|
1097
|
+
f"Invalid resolution {reso!r}. Options are: "
|
|
1098
|
+
+ ", ".join(map(repr, constructor.RESOS_CARTOPY))
|
|
1099
|
+
+ "."
|
|
1100
|
+
)
|
|
1101
|
+
for name, args in constructor.FEATURES_CARTOPY.items():
|
|
1102
|
+
# Draw feature or toggle feature off
|
|
1103
|
+
b = rc.find(name, context=True)
|
|
1104
|
+
attr = f"_{name}_feature"
|
|
1105
|
+
feat = getattr(self, attr, None)
|
|
1106
|
+
drawn = feat is not None # if exists, apply *updated* settings
|
|
1107
|
+
if b is not None:
|
|
1108
|
+
if not b:
|
|
1109
|
+
if drawn: # toggle existing feature off
|
|
1110
|
+
feat.set_visible(False)
|
|
1111
|
+
else:
|
|
1112
|
+
if not drawn:
|
|
1113
|
+
feat = cfeature.NaturalEarthFeature(*args, reso)
|
|
1114
|
+
feat = self.add_feature(feat) # convert to FeatureArtist
|
|
1115
|
+
setattr(self, attr, feat)
|
|
1116
|
+
|
|
1117
|
+
# Update artist attributes (FeatureArtist._kwargs used back to v0.5).
|
|
1118
|
+
# For 'lines', need to specify edgecolor and facecolor
|
|
1119
|
+
# See: https://github.com/SciTools/cartopy/issues/803
|
|
1120
|
+
if feat is not None:
|
|
1121
|
+
kw = rc.category(name, context=drawn)
|
|
1122
|
+
if name in ("coast", "rivers", "borders", "innerborders"):
|
|
1123
|
+
if "color" in kw:
|
|
1124
|
+
kw.update({"edgecolor": kw.pop("color"), "facecolor": "none"})
|
|
1125
|
+
else:
|
|
1126
|
+
kw.update({"linewidth": 0})
|
|
1127
|
+
if "zorder" in kw:
|
|
1128
|
+
# NOTE: Necessary to update zorder directly because _kwargs
|
|
1129
|
+
# attributes are not applied until draw()... at which point
|
|
1130
|
+
# matplotlib is drawing in the order based on the *old* zorder.
|
|
1131
|
+
feat.set_zorder(kw["zorder"])
|
|
1132
|
+
if hasattr(feat, "_kwargs"):
|
|
1133
|
+
feat._kwargs.update(kw)
|
|
1134
|
+
if _version_cartopy >= "0.23":
|
|
1135
|
+
feat.set(**feat._kwargs)
|
|
1136
|
+
|
|
1137
|
+
def _update_gridlines(
|
|
1138
|
+
self,
|
|
1139
|
+
gl,
|
|
1140
|
+
which="major",
|
|
1141
|
+
longrid=None,
|
|
1142
|
+
latgrid=None,
|
|
1143
|
+
nsteps=None,
|
|
1144
|
+
):
|
|
1145
|
+
"""
|
|
1146
|
+
Update gridliner object with axis locators, and toggle gridlines on and off.
|
|
1147
|
+
"""
|
|
1148
|
+
# Update gridliner collection properties
|
|
1149
|
+
# WARNING: Here we use native matplotlib 'grid' rc param for geographic
|
|
1150
|
+
# gridlines. If rc mode is 1 (first format call) use context=False
|
|
1151
|
+
kwlines = rc._get_gridline_props(which=which, native=False)
|
|
1152
|
+
kwtext = rc._get_ticklabel_props(native=False)
|
|
1153
|
+
gl.collection_kwargs.update(kwlines)
|
|
1154
|
+
gl.xlabel_style.update(kwtext)
|
|
1155
|
+
gl.ylabel_style.update(kwtext)
|
|
1156
|
+
|
|
1157
|
+
# Apply tick locations from dummy _LonAxis and _LatAxis axes
|
|
1158
|
+
# NOTE: This will re-apply existing gridline locations if unchanged.
|
|
1159
|
+
if nsteps is not None:
|
|
1160
|
+
gl.n_steps = nsteps
|
|
1161
|
+
latmax = self._lataxis.get_latmax()
|
|
1162
|
+
if _version_cartopy >= "0.19":
|
|
1163
|
+
gl.ylim = (-latmax, latmax)
|
|
1164
|
+
longrid = rc._get_gridline_bool(longrid, axis="x", which=which, native=False)
|
|
1165
|
+
if longrid is not None:
|
|
1166
|
+
gl.xlines = longrid
|
|
1167
|
+
latgrid = rc._get_gridline_bool(latgrid, axis="y", which=which, native=False)
|
|
1168
|
+
if latgrid is not None:
|
|
1169
|
+
gl.ylines = latgrid
|
|
1170
|
+
lonlines = self._get_lonticklocs(which=which)
|
|
1171
|
+
latlines = self._get_latticklocs(which=which)
|
|
1172
|
+
if _version_cartopy >= "0.18": # see lukelbd/ultraplot#208
|
|
1173
|
+
lonlines = (np.asarray(lonlines) + 180) % 360 - 180 # only for cartopy
|
|
1174
|
+
gl.xlocator = mticker.FixedLocator(lonlines)
|
|
1175
|
+
gl.ylocator = mticker.FixedLocator(latlines)
|
|
1176
|
+
|
|
1177
|
+
def _update_major_gridlines(
|
|
1178
|
+
self,
|
|
1179
|
+
longrid=None,
|
|
1180
|
+
latgrid=None,
|
|
1181
|
+
lonarray=None,
|
|
1182
|
+
latarray=None,
|
|
1183
|
+
loninline=None,
|
|
1184
|
+
latinline=None,
|
|
1185
|
+
labelpad=None,
|
|
1186
|
+
rotatelabels=None,
|
|
1187
|
+
nsteps=None,
|
|
1188
|
+
):
|
|
1189
|
+
"""
|
|
1190
|
+
Update major gridlines.
|
|
1191
|
+
"""
|
|
1192
|
+
# Update gridline locations and style
|
|
1193
|
+
gl = self._gridlines_major
|
|
1194
|
+
if gl is None:
|
|
1195
|
+
gl = self._gridlines_major = self._init_gridlines()
|
|
1196
|
+
self._update_gridlines(
|
|
1197
|
+
gl,
|
|
1198
|
+
which="major",
|
|
1199
|
+
longrid=longrid,
|
|
1200
|
+
latgrid=latgrid,
|
|
1201
|
+
nsteps=nsteps,
|
|
1202
|
+
)
|
|
1203
|
+
gl.xformatter = self._lonaxis.get_major_formatter()
|
|
1204
|
+
gl.yformatter = self._lataxis.get_major_formatter()
|
|
1205
|
+
|
|
1206
|
+
# Update gridline label parameters
|
|
1207
|
+
# NOTE: Cartopy 0.18 and 0.19 can not draw both edge and inline labels. Instead
|
|
1208
|
+
# requires both a set 'side' and 'x_inline' is True (applied in GeoAxes.format).
|
|
1209
|
+
# NOTE: The 'xpadding' and 'ypadding' props were introduced in v0.16
|
|
1210
|
+
# with default 5 points, then set to default None in v0.18.
|
|
1211
|
+
# TODO: Cartopy has had two formatters for a while but we use the newer one.
|
|
1212
|
+
# See https://github.com/SciTools/cartopy/pull/1066
|
|
1213
|
+
if labelpad is not None:
|
|
1214
|
+
gl.xpadding = gl.ypadding = labelpad
|
|
1215
|
+
if loninline is not None:
|
|
1216
|
+
gl.x_inline = bool(loninline)
|
|
1217
|
+
if latinline is not None:
|
|
1218
|
+
gl.y_inline = bool(latinline)
|
|
1219
|
+
if rotatelabels is not None:
|
|
1220
|
+
gl.rotate_labels = bool(rotatelabels) # ignored in cartopy < 0.18
|
|
1221
|
+
if latinline is not None or loninline is not None:
|
|
1222
|
+
lon, lat = loninline, latinline
|
|
1223
|
+
b = True if lon and lat else "x" if lon else "y" if lat else None
|
|
1224
|
+
gl.inline_labels = b # ignored in cartopy < 0.20
|
|
1225
|
+
|
|
1226
|
+
# Gridline label toggling
|
|
1227
|
+
# Issue warning instead of error!
|
|
1228
|
+
if _version_cartopy < "0.18" and not isinstance(
|
|
1229
|
+
self.projection, (ccrs.Mercator, ccrs.PlateCarree)
|
|
1230
|
+
):
|
|
1231
|
+
if any(latarray):
|
|
1232
|
+
warnings._warn_ultraplot(
|
|
1233
|
+
"Cannot add gridline labels to cartopy "
|
|
1234
|
+
f"{type(self.projection).__name__} projection."
|
|
1235
|
+
)
|
|
1236
|
+
latarray = [False] * 5
|
|
1237
|
+
if any(lonarray):
|
|
1238
|
+
warnings._warn_ultraplot(
|
|
1239
|
+
"Cannot add gridline labels to cartopy "
|
|
1240
|
+
f"{type(self.projection).__name__} projection."
|
|
1241
|
+
)
|
|
1242
|
+
lonarray = [False] * 5
|
|
1243
|
+
array = [
|
|
1244
|
+
(
|
|
1245
|
+
True
|
|
1246
|
+
if lon and lat
|
|
1247
|
+
else (
|
|
1248
|
+
"x"
|
|
1249
|
+
if lon
|
|
1250
|
+
else (
|
|
1251
|
+
"y"
|
|
1252
|
+
if lat
|
|
1253
|
+
else False if lon is not None or lon is not None else None
|
|
1254
|
+
)
|
|
1255
|
+
)
|
|
1256
|
+
)
|
|
1257
|
+
for lon, lat in zip(lonarray, latarray)
|
|
1258
|
+
]
|
|
1259
|
+
self._toggle_gridliner_labels(gl, *array[:2], *array[2:4], array[4])
|
|
1260
|
+
|
|
1261
|
+
def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None):
|
|
1262
|
+
"""
|
|
1263
|
+
Update minor gridlines.
|
|
1264
|
+
"""
|
|
1265
|
+
gl = self._gridlines_minor
|
|
1266
|
+
if gl is None:
|
|
1267
|
+
gl = self._gridlines_minor = self._init_gridlines()
|
|
1268
|
+
self._update_gridlines(
|
|
1269
|
+
gl,
|
|
1270
|
+
which="minor",
|
|
1271
|
+
longrid=longrid,
|
|
1272
|
+
latgrid=latgrid,
|
|
1273
|
+
nsteps=nsteps,
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
def get_extent(self, crs=None):
|
|
1277
|
+
# Get extent and try to repair longitude bounds.
|
|
1278
|
+
if crs is None:
|
|
1279
|
+
crs = ccrs.PlateCarree()
|
|
1280
|
+
extent = super().get_extent(crs=crs)
|
|
1281
|
+
if isinstance(crs, ccrs.PlateCarree):
|
|
1282
|
+
if np.isclose(extent[0], -180) and np.isclose(extent[-1], 180):
|
|
1283
|
+
# Repair longitude bounds to reflect dateline position
|
|
1284
|
+
# NOTE: This is critical so we can prevent duplicate gridlines
|
|
1285
|
+
# on dateline. See _update_gridlines.
|
|
1286
|
+
lon0 = self._get_lon0()
|
|
1287
|
+
extent[:2] = [lon0 - 180, lon0 + 180]
|
|
1288
|
+
return extent
|
|
1289
|
+
|
|
1290
|
+
def get_tightbbox(self, renderer, *args, **kwargs):
|
|
1291
|
+
# Perform extra post-processing steps
|
|
1292
|
+
# For now this just draws the gridliners
|
|
1293
|
+
self._apply_axis_sharing()
|
|
1294
|
+
if self.get_autoscale_on() and self.ignore_existing_data_limits:
|
|
1295
|
+
self.autoscale_view()
|
|
1296
|
+
|
|
1297
|
+
# Adjust location
|
|
1298
|
+
if _version_cartopy >= "0.18":
|
|
1299
|
+
self.patch._adjust_location() # this does the below steps
|
|
1300
|
+
elif getattr(self.background_patch, "reclip", None) and hasattr(
|
|
1301
|
+
self.background_patch, "orig_path"
|
|
1302
|
+
):
|
|
1303
|
+
clipped_path = self.background_patch.orig_path.clip_to_bbox(self.viewLim)
|
|
1304
|
+
self.outline_patch._path = clipped_path
|
|
1305
|
+
self.background_patch._path = clipped_path
|
|
1306
|
+
|
|
1307
|
+
# Apply aspect
|
|
1308
|
+
self.apply_aspect()
|
|
1309
|
+
if _version_cartopy >= "0.23":
|
|
1310
|
+
gridliners = [
|
|
1311
|
+
a for a in self.artists if isinstance(a, cgridliner.Gridliner)
|
|
1312
|
+
]
|
|
1313
|
+
else:
|
|
1314
|
+
gridliners = self._gridliners
|
|
1315
|
+
|
|
1316
|
+
for gl in gridliners:
|
|
1317
|
+
if _version_cartopy >= "0.18":
|
|
1318
|
+
gl._draw_gridliner(renderer=renderer)
|
|
1319
|
+
else:
|
|
1320
|
+
gl._draw_gridliner(background_patch=self.background_patch)
|
|
1321
|
+
|
|
1322
|
+
# Remove gridliners
|
|
1323
|
+
if _version_cartopy < "0.18":
|
|
1324
|
+
self._gridliners = []
|
|
1325
|
+
|
|
1326
|
+
return super().get_tightbbox(renderer, *args, **kwargs)
|
|
1327
|
+
|
|
1328
|
+
def set_extent(self, extent, crs=None):
|
|
1329
|
+
# Fix paths, so axes tight bounding box gets correct box! From this issue:
|
|
1330
|
+
# https://github.com/SciTools/cartopy/issues/1207#issuecomment-439975083
|
|
1331
|
+
# Also record the requested longitude latitude extent so we can use these
|
|
1332
|
+
# values for LongitudeLocator and LatitudeLocator. Otherwise if longitude
|
|
1333
|
+
# extent is across dateline LongitudeLocator fails because get_extent()
|
|
1334
|
+
# reports -180 to 180: https://github.com/SciTools/cartopy/issues/1564
|
|
1335
|
+
# NOTE: This is *also* not perfect because if set_extent() was called
|
|
1336
|
+
# and extent crosses map boundary of rectangular projection, the *actual*
|
|
1337
|
+
# resulting extent is the opposite. But that means user has messed up anyway
|
|
1338
|
+
# so probably doesn't matter if gridlines are also wrong.
|
|
1339
|
+
if crs is None:
|
|
1340
|
+
crs = ccrs.PlateCarree()
|
|
1341
|
+
if isinstance(crs, ccrs.PlateCarree):
|
|
1342
|
+
self._set_view_intervals(extent)
|
|
1343
|
+
with rc.context(mode=2): # do not reset gridline properties!
|
|
1344
|
+
if self._gridlines_major is not None:
|
|
1345
|
+
self._update_gridlines(self._gridlines_major, which="major")
|
|
1346
|
+
if self._gridlines_minor is not None:
|
|
1347
|
+
self._update_gridlines(self._gridlines_minor, which="minor")
|
|
1348
|
+
if _version_cartopy < "0.18":
|
|
1349
|
+
clipped_path = self.outline_patch.orig_path.clip_to_bbox(self.viewLim)
|
|
1350
|
+
self.outline_patch._path = clipped_path
|
|
1351
|
+
self.background_patch._path = clipped_path
|
|
1352
|
+
return super().set_extent(extent, crs=crs)
|
|
1353
|
+
|
|
1354
|
+
def set_global(self):
|
|
1355
|
+
# Set up "global" extent and update _LatAxis and _LonAxis view intervals
|
|
1356
|
+
result = super().set_global()
|
|
1357
|
+
self._set_view_intervals(self._get_global_extent())
|
|
1358
|
+
return result
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
class _BasemapAxes(GeoAxes):
|
|
1362
|
+
"""
|
|
1363
|
+
Axes subclass for plotting basemap projections.
|
|
1364
|
+
"""
|
|
1365
|
+
|
|
1366
|
+
_name = "basemap"
|
|
1367
|
+
_proj_class = Basemap
|
|
1368
|
+
_proj_north = ("npaeqd", "nplaea", "npstere")
|
|
1369
|
+
_proj_south = ("spaeqd", "splaea", "spstere")
|
|
1370
|
+
_proj_polar = _proj_north + _proj_south
|
|
1371
|
+
_proj_non_rectangular = _proj_polar + ( # do not use axes spines as boundaries
|
|
1372
|
+
"ortho",
|
|
1373
|
+
"geos",
|
|
1374
|
+
"nsper",
|
|
1375
|
+
"moll",
|
|
1376
|
+
"hammer",
|
|
1377
|
+
"robin",
|
|
1378
|
+
"eck4",
|
|
1379
|
+
"kav7",
|
|
1380
|
+
"mbtfpq",
|
|
1381
|
+
"sinu",
|
|
1382
|
+
"vandg",
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
def __init__(self, *args, map_projection=None, **kwargs):
|
|
1386
|
+
"""
|
|
1387
|
+
Parameters
|
|
1388
|
+
----------
|
|
1389
|
+
map_projection : ~mpl_toolkits.basemap.Basemap
|
|
1390
|
+
The map projection.
|
|
1391
|
+
*args, **kwargs
|
|
1392
|
+
Passed to `GeoAxes`.
|
|
1393
|
+
"""
|
|
1394
|
+
# First assign projection and set axis bounds for locators
|
|
1395
|
+
# WARNING: Unlike cartopy projections basemaps cannot normally be reused.
|
|
1396
|
+
# To make syntax similar we make a copy.
|
|
1397
|
+
# WARNING: Investigated whether Basemap.__init__() could be called
|
|
1398
|
+
# twice with updated proj kwargs to modify map bounds after creation
|
|
1399
|
+
# and python immmediately crashes. Do not try again.
|
|
1400
|
+
import mpl_toolkits.basemap # noqa: F401 verify package is available
|
|
1401
|
+
|
|
1402
|
+
self.projection = copy.copy(map_projection) # verify
|
|
1403
|
+
lon0 = self._get_lon0()
|
|
1404
|
+
if self.projection.projection in self._proj_polar:
|
|
1405
|
+
latmax = 80 # default latmax for gridlines
|
|
1406
|
+
extent = [-180 + lon0, 180 + lon0]
|
|
1407
|
+
bound = getattr(self.projection, "boundinglat", 0)
|
|
1408
|
+
north = self.projection.projection in self._proj_north
|
|
1409
|
+
extent.extend([bound, 90] if north else [-90, bound])
|
|
1410
|
+
else:
|
|
1411
|
+
latmax = 90
|
|
1412
|
+
attrs = ("lonmin", "lonmax", "latmin", "latmax")
|
|
1413
|
+
extent = [getattr(self.projection, attr, None) for attr in attrs]
|
|
1414
|
+
if any(_ is None for _ in extent):
|
|
1415
|
+
extent = [180 - lon0, 180 + lon0, -90, 90] # fallback
|
|
1416
|
+
|
|
1417
|
+
# Initialize axes
|
|
1418
|
+
self._map_boundary = None # see format()
|
|
1419
|
+
self._has_recurred = False # use this to override plotting methods
|
|
1420
|
+
self._lonlines_major = None # store gridliner objects this way
|
|
1421
|
+
self._lonlines_minor = None
|
|
1422
|
+
self._latlines_major = None
|
|
1423
|
+
self._latlines_minor = None
|
|
1424
|
+
self._lonarray = 4 * [False] # cached label toggles
|
|
1425
|
+
self._latarray = 4 * [False] # cached label toggles
|
|
1426
|
+
self._lonaxis = _LonAxis(self)
|
|
1427
|
+
self._lataxis = _LatAxis(self, latmax=latmax)
|
|
1428
|
+
self._set_view_intervals(extent)
|
|
1429
|
+
super().__init__(*args, **kwargs)
|
|
1430
|
+
|
|
1431
|
+
def _get_lon0(self):
|
|
1432
|
+
"""
|
|
1433
|
+
Get the central longitude.
|
|
1434
|
+
"""
|
|
1435
|
+
return getattr(self.projection, "projparams", {}).get("lon_0", 0)
|
|
1436
|
+
|
|
1437
|
+
@staticmethod
|
|
1438
|
+
def _iter_gridlines(dict_):
|
|
1439
|
+
"""
|
|
1440
|
+
Iterate over longitude latitude lines.
|
|
1441
|
+
"""
|
|
1442
|
+
dict_ = dict_ or {}
|
|
1443
|
+
for pi in dict_.values():
|
|
1444
|
+
for pj in pi:
|
|
1445
|
+
for obj in pj:
|
|
1446
|
+
yield obj
|
|
1447
|
+
|
|
1448
|
+
def _update_background(self, **kwargs):
|
|
1449
|
+
"""
|
|
1450
|
+
Update the map boundary patches. This is called in `Axes.format`.
|
|
1451
|
+
"""
|
|
1452
|
+
# Non-rectangular projections
|
|
1453
|
+
# WARNING: Map boundary must be drawn before all other tasks. See __init__.
|
|
1454
|
+
# WARNING: With clipping on boundary lines are clipped by the axes bbox.
|
|
1455
|
+
if self.projection.projection in self._proj_non_rectangular:
|
|
1456
|
+
self.patch.set_facecolor("none") # make sure main patch is hidden
|
|
1457
|
+
kw_face, kw_edge = rc._get_background_props(native=False, **kwargs)
|
|
1458
|
+
kw = {**kw_face, **kw_edge, "rasterized": False, "clip_on": False}
|
|
1459
|
+
self._map_boundary.update(kw)
|
|
1460
|
+
# Rectangular projections
|
|
1461
|
+
else:
|
|
1462
|
+
kw_face, kw_edge = rc._get_background_props(native=False, **kwargs)
|
|
1463
|
+
self.patch.update({**kw_face, "edgecolor": "none"})
|
|
1464
|
+
for spine in self.spines.values():
|
|
1465
|
+
spine.update(kw_edge)
|
|
1466
|
+
|
|
1467
|
+
def _update_boundary(self, round=None):
|
|
1468
|
+
"""
|
|
1469
|
+
No-op. Boundary mode cannot be changed in basemap.
|
|
1470
|
+
"""
|
|
1471
|
+
# NOTE: Unlike the cartopy method we do not look up the rc setting here.
|
|
1472
|
+
if round is None:
|
|
1473
|
+
return
|
|
1474
|
+
else:
|
|
1475
|
+
warnings._warn_ultraplot(
|
|
1476
|
+
f"Got round={round!r}, but you cannot change the bounds of a polar "
|
|
1477
|
+
"basemap projection after creating it. Please pass 'round' to pplt.Proj " # noqa: E501
|
|
1478
|
+
"instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw')."
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
def _update_extent_mode(self, extent=None, boundinglat=None): # noqa: U100
|
|
1482
|
+
"""
|
|
1483
|
+
No-op. Extent mode cannot be changed in basemap.
|
|
1484
|
+
"""
|
|
1485
|
+
# NOTE: Unlike the cartopy method we do not look up the rc setting here.
|
|
1486
|
+
if extent is None:
|
|
1487
|
+
return
|
|
1488
|
+
if extent not in ("globe", "auto"):
|
|
1489
|
+
raise ValueError(
|
|
1490
|
+
f"Invalid extent mode {extent!r}. Must be 'auto' or 'globe'."
|
|
1491
|
+
)
|
|
1492
|
+
if extent == "auto":
|
|
1493
|
+
warnings._warn_ultraplot(
|
|
1494
|
+
f"Got extent={extent!r}, but you cannot use auto extent mode "
|
|
1495
|
+
"in basemap projections. Please consider switching to cartopy."
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
def _update_extent(self, lonlim=None, latlim=None, boundinglat=None):
|
|
1499
|
+
"""
|
|
1500
|
+
No-op. Map bounds cannot be changed in basemap.
|
|
1501
|
+
"""
|
|
1502
|
+
lonlim = _not_none(lonlim, (None, None))
|
|
1503
|
+
latlim = _not_none(latlim, (None, None))
|
|
1504
|
+
if boundinglat is not None or any(_ is not None for _ in (*lonlim, *latlim)):
|
|
1505
|
+
warnings._warn_ultraplot(
|
|
1506
|
+
f"Got lonlim={lonlim!r}, latlim={latlim!r}, boundinglat={boundinglat!r}"
|
|
1507
|
+
', but you cannot "zoom into" a basemap projection after creating it. '
|
|
1508
|
+
"Please pass any of the following keyword arguments to pplt.Proj "
|
|
1509
|
+
"instead (e.g. using the pplt.subplots() dictionary keyword 'proj_kw'):"
|
|
1510
|
+
"'boundinglat', 'lonlim', 'latlim', 'llcrnrlon', 'llcrnrlat', "
|
|
1511
|
+
"'urcrnrlon', 'urcrnrlat', 'llcrnrx', 'llcrnry', 'urcrnrx', 'urcrnry', "
|
|
1512
|
+
"'width', or 'height'."
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
def _update_features(self):
|
|
1516
|
+
"""
|
|
1517
|
+
Update geographic features.
|
|
1518
|
+
"""
|
|
1519
|
+
# NOTE: Also notable are drawcounties, blumarble, drawlsmask,
|
|
1520
|
+
# shadedrelief, and etopo methods.
|
|
1521
|
+
for name, method in constructor.FEATURES_BASEMAP.items():
|
|
1522
|
+
# Draw feature or toggle on and off
|
|
1523
|
+
b = rc.find(name, context=True)
|
|
1524
|
+
attr = f"_{name}_feature"
|
|
1525
|
+
feat = getattr(self, attr, None)
|
|
1526
|
+
drawn = feat is not None # if exists, apply *updated* settings
|
|
1527
|
+
if b is not None:
|
|
1528
|
+
if not b:
|
|
1529
|
+
if drawn: # toggle existing feature off
|
|
1530
|
+
for obj in feat:
|
|
1531
|
+
feat.set_visible(False)
|
|
1532
|
+
else:
|
|
1533
|
+
if not drawn:
|
|
1534
|
+
feat = getattr(self.projection, method)(ax=self)
|
|
1535
|
+
if not isinstance(feat, (list, tuple)): # list of artists?
|
|
1536
|
+
feat = (feat,)
|
|
1537
|
+
setattr(self, attr, feat)
|
|
1538
|
+
|
|
1539
|
+
# Update settings
|
|
1540
|
+
if feat is not None:
|
|
1541
|
+
kw = rc.category(name, context=drawn)
|
|
1542
|
+
for obj in feat:
|
|
1543
|
+
obj.update(kw)
|
|
1544
|
+
|
|
1545
|
+
def _update_gridlines(
|
|
1546
|
+
self,
|
|
1547
|
+
which="major",
|
|
1548
|
+
longrid=None,
|
|
1549
|
+
latgrid=None,
|
|
1550
|
+
lonarray=None,
|
|
1551
|
+
latarray=None,
|
|
1552
|
+
):
|
|
1553
|
+
"""
|
|
1554
|
+
Apply changes to the basemap axes.
|
|
1555
|
+
"""
|
|
1556
|
+
latmax = self._lataxis.get_latmax()
|
|
1557
|
+
for axis, name, grid, array, method in zip(
|
|
1558
|
+
("x", "y"),
|
|
1559
|
+
("lon", "lat"),
|
|
1560
|
+
(longrid, latgrid),
|
|
1561
|
+
(lonarray, latarray),
|
|
1562
|
+
("drawmeridians", "drawparallels"),
|
|
1563
|
+
):
|
|
1564
|
+
# Correct lonarray and latarray label toggles by changing from lrbt to lrtb.
|
|
1565
|
+
# Then update the cahced toggle array. This lets us change gridline locs
|
|
1566
|
+
# while preserving the label toggle setting from a previous format() call.
|
|
1567
|
+
grid = rc._get_gridline_bool(grid, axis=axis, which=which, native=False)
|
|
1568
|
+
axis = getattr(self, f"_{name}axis")
|
|
1569
|
+
if len(array) == 5: # should be always
|
|
1570
|
+
array = array[:4]
|
|
1571
|
+
bools = 4 * [False] if which == "major" else getattr(self, f"_{name}array")
|
|
1572
|
+
array = [*array[:2], *array[2:4][::-1]] # flip lrbt to lrtb and skip geo
|
|
1573
|
+
for i, b in enumerate(array):
|
|
1574
|
+
if b is not None:
|
|
1575
|
+
bools[i] = b # update toggles
|
|
1576
|
+
|
|
1577
|
+
# Get gridlines
|
|
1578
|
+
# NOTE: This may re-apply existing gridlines.
|
|
1579
|
+
lines = list(getattr(self, f"_get_{name}ticklocs")(which=which))
|
|
1580
|
+
if name == "lon" and np.isclose(lines[0] + 360, lines[-1]):
|
|
1581
|
+
lines = lines[:-1] # prevent double labels
|
|
1582
|
+
|
|
1583
|
+
# Figure out whether we have to redraw meridians/parallels
|
|
1584
|
+
# NOTE: Always update minor gridlines if major locator also changed
|
|
1585
|
+
attr = f"_{name}lines_{which}"
|
|
1586
|
+
objs = getattr(self, attr) # dictionary of previous objects
|
|
1587
|
+
attrs = ["isDefault_majloc"] # always check this one
|
|
1588
|
+
attrs.append("isDefault_majfmt" if which == "major" else "isDefault_minloc")
|
|
1589
|
+
rebuild = lines and (
|
|
1590
|
+
not objs
|
|
1591
|
+
or any(_ is not None for _ in array) # user-input or initial toggles
|
|
1592
|
+
or any(not getattr(axis, attr) for attr in attrs) # none tracked yet
|
|
1593
|
+
)
|
|
1594
|
+
if rebuild and objs and grid is None: # get *previous* toggle state
|
|
1595
|
+
grid = all(obj.get_visible() for obj in self._iter_gridlines(objs))
|
|
1596
|
+
|
|
1597
|
+
# Draw or redraw meridian or parallel lines
|
|
1598
|
+
# Also mark formatters and locators as 'default'
|
|
1599
|
+
if rebuild:
|
|
1600
|
+
kwdraw = {}
|
|
1601
|
+
formatter = axis.get_major_formatter()
|
|
1602
|
+
if formatter is not None: # use functional formatter
|
|
1603
|
+
kwdraw["fmt"] = formatter
|
|
1604
|
+
for obj in self._iter_gridlines(objs):
|
|
1605
|
+
obj.set_visible(False)
|
|
1606
|
+
objs = getattr(self.projection, method)(
|
|
1607
|
+
lines, ax=self, latmax=latmax, labels=bools, **kwdraw
|
|
1608
|
+
)
|
|
1609
|
+
setattr(self, attr, objs)
|
|
1610
|
+
|
|
1611
|
+
# Update gridline settings
|
|
1612
|
+
# We use native matplotlib 'grid' rc param for geographic gridlines
|
|
1613
|
+
kwlines = rc._get_gridline_props(which=which, native=False, rebuild=rebuild)
|
|
1614
|
+
kwtext = rc._get_ticklabel_props(native=False, rebuild=rebuild)
|
|
1615
|
+
for obj in self._iter_gridlines(objs):
|
|
1616
|
+
if isinstance(obj, mtext.Text):
|
|
1617
|
+
obj.update(kwtext)
|
|
1618
|
+
else:
|
|
1619
|
+
obj.update(kwlines)
|
|
1620
|
+
|
|
1621
|
+
# Toggle existing gridlines on and off
|
|
1622
|
+
if grid is not None:
|
|
1623
|
+
for obj in self._iter_gridlines(objs):
|
|
1624
|
+
obj.set_visible(grid)
|
|
1625
|
+
|
|
1626
|
+
def _update_major_gridlines(
|
|
1627
|
+
self,
|
|
1628
|
+
longrid=None,
|
|
1629
|
+
latgrid=None,
|
|
1630
|
+
lonarray=None,
|
|
1631
|
+
latarray=None,
|
|
1632
|
+
loninline=None,
|
|
1633
|
+
latinline=None,
|
|
1634
|
+
rotatelabels=None,
|
|
1635
|
+
labelpad=None,
|
|
1636
|
+
nsteps=None,
|
|
1637
|
+
):
|
|
1638
|
+
"""
|
|
1639
|
+
Update major gridlines.
|
|
1640
|
+
"""
|
|
1641
|
+
loninline, latinline, labelpad, rotatelabels, nsteps # avoid U100 error
|
|
1642
|
+
self._update_gridlines(
|
|
1643
|
+
which="major",
|
|
1644
|
+
longrid=longrid,
|
|
1645
|
+
latgrid=latgrid,
|
|
1646
|
+
lonarray=lonarray,
|
|
1647
|
+
latarray=latarray,
|
|
1648
|
+
)
|
|
1649
|
+
|
|
1650
|
+
def _update_minor_gridlines(self, longrid=None, latgrid=None, nsteps=None):
|
|
1651
|
+
"""
|
|
1652
|
+
Update minor gridlines.
|
|
1653
|
+
"""
|
|
1654
|
+
# Update gridline locations
|
|
1655
|
+
nsteps # avoid U100 error
|
|
1656
|
+
array = [None] * 4 # NOTE: must be None not False (see _update_gridlines)
|
|
1657
|
+
self._update_gridlines(
|
|
1658
|
+
which="minor",
|
|
1659
|
+
longrid=longrid,
|
|
1660
|
+
latgrid=latgrid,
|
|
1661
|
+
lonarray=array,
|
|
1662
|
+
latarray=array,
|
|
1663
|
+
)
|
|
1664
|
+
# Set isDefault_majloc, etc. to True for both axes
|
|
1665
|
+
# NOTE: This cannot be done inside _update_gridlines or minor gridlines
|
|
1666
|
+
# will not update to reflect new major gridline locations.
|
|
1667
|
+
for axis in (self._lonaxis, self._lataxis):
|
|
1668
|
+
axis.isDefault_majfmt = True
|
|
1669
|
+
axis.isDefault_majloc = True
|
|
1670
|
+
axis.isDefault_minloc = True
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
# Apply signature obfuscation after storing previous signature
|
|
1674
|
+
GeoAxes._format_signatures[GeoAxes] = inspect.signature(GeoAxes.format)
|
|
1675
|
+
GeoAxes.format = docstring._obfuscate_kwargs(GeoAxes.format)
|