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
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Utilities for processing input data passed to plotting commands.
|
|
4
|
+
"""
|
|
5
|
+
import functools
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.ma as ma
|
|
10
|
+
|
|
11
|
+
from . import ic # noqa: F401
|
|
12
|
+
from . import _not_none, warnings
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from cartopy.crs import PlateCarree
|
|
16
|
+
except ModuleNotFoundError:
|
|
17
|
+
PlateCarree = object
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Constants
|
|
21
|
+
BASEMAP_FUNCS = ( # default latlon=True
|
|
22
|
+
"barbs",
|
|
23
|
+
"contour",
|
|
24
|
+
"contourf",
|
|
25
|
+
"hexbin",
|
|
26
|
+
"imshow",
|
|
27
|
+
"pcolor",
|
|
28
|
+
"pcolormesh",
|
|
29
|
+
"plot",
|
|
30
|
+
"quiver",
|
|
31
|
+
"scatter",
|
|
32
|
+
"streamplot",
|
|
33
|
+
"step",
|
|
34
|
+
)
|
|
35
|
+
CARTOPY_FUNCS = ( # default transform=PlateCarree()
|
|
36
|
+
"barbs",
|
|
37
|
+
"contour",
|
|
38
|
+
"contourf",
|
|
39
|
+
"fill",
|
|
40
|
+
"fill_between",
|
|
41
|
+
"fill_betweenx", # NOTE: not sure if these work
|
|
42
|
+
"imshow",
|
|
43
|
+
"pcolor",
|
|
44
|
+
"pcolormesh",
|
|
45
|
+
"plot",
|
|
46
|
+
"quiver",
|
|
47
|
+
"scatter",
|
|
48
|
+
"streamplot",
|
|
49
|
+
"step",
|
|
50
|
+
"tricontour",
|
|
51
|
+
"tricontourf",
|
|
52
|
+
"tripcolor", # NOTE: not sure why these work
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _load_objects():
|
|
57
|
+
"""
|
|
58
|
+
Load array-like objects.
|
|
59
|
+
"""
|
|
60
|
+
# NOTE: We just want to detect if *input arrays* belong to these types -- and if
|
|
61
|
+
# this is the case, it means the module has already been imported! So, we only
|
|
62
|
+
# try loading these classes within autoformat calls. This saves >500ms of import
|
|
63
|
+
# time. We use ndarray as the default value for unimported types and in loops we
|
|
64
|
+
# are careful to check membership to np.ndarray before anything else.
|
|
65
|
+
global ndarray, DataArray, DataFrame, Series, Index, Quantity
|
|
66
|
+
ndarray = np.ndarray
|
|
67
|
+
DataArray = getattr(sys.modules.get("xarray", None), "DataArray", ndarray)
|
|
68
|
+
DataFrame = getattr(sys.modules.get("pandas", None), "DataFrame", ndarray)
|
|
69
|
+
Series = getattr(sys.modules.get("pandas", None), "Series", ndarray)
|
|
70
|
+
Index = getattr(sys.modules.get("pandas", None), "Index", ndarray)
|
|
71
|
+
Quantity = getattr(sys.modules.get("pint", None), "Quantity", ndarray)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_load_objects()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Type utilities
|
|
78
|
+
def _is_numeric(data):
|
|
79
|
+
"""
|
|
80
|
+
Test whether input is numeric array rather than datetime or strings.
|
|
81
|
+
"""
|
|
82
|
+
array = _to_numpy_array(data)
|
|
83
|
+
return len(data) and (
|
|
84
|
+
np.issubdtype(array.dtype, np.number)
|
|
85
|
+
or np.issubdtype(array.dtype, object)
|
|
86
|
+
and all(isinstance(_, np.number) for _ in array.flat)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_categorical(data):
|
|
91
|
+
"""
|
|
92
|
+
Test whether input is array of strings.
|
|
93
|
+
"""
|
|
94
|
+
array = _to_numpy_array(data)
|
|
95
|
+
return len(data) and (
|
|
96
|
+
np.issubdtype(array.dtype, str)
|
|
97
|
+
or np.issubdtype(array.dtype, object)
|
|
98
|
+
and any(isinstance(_, str) for _ in array.flat)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_descending(data):
|
|
103
|
+
"""
|
|
104
|
+
Test whether the input data is descending. This is used for auto axis reversal.
|
|
105
|
+
"""
|
|
106
|
+
# NOTE: Want this to work with e.g. datetime object arrays and numpy datetime
|
|
107
|
+
# arrays so use try except clause.
|
|
108
|
+
data = _to_numpy_array(data)
|
|
109
|
+
if data.ndim > 1 or data.size < 2:
|
|
110
|
+
return False
|
|
111
|
+
try:
|
|
112
|
+
return all(x != abs(x) for x in np.diff(data))
|
|
113
|
+
except TypeError:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _to_duck_array(data, strip_units=False):
|
|
118
|
+
"""
|
|
119
|
+
Convert arbitrary input to duck array. Preserve array containers with metadata.
|
|
120
|
+
"""
|
|
121
|
+
_load_objects()
|
|
122
|
+
if data is None:
|
|
123
|
+
raise ValueError("Invalid data None.")
|
|
124
|
+
if not isinstance(
|
|
125
|
+
data, (ndarray, DataArray, DataFrame, Series, Index, Quantity)
|
|
126
|
+
) or not np.iterable(data):
|
|
127
|
+
# WARNING: this strips e.g. scalar DataArray metadata
|
|
128
|
+
data = _to_numpy_array(data)
|
|
129
|
+
if strip_units: # used for z coordinates that cannot have units
|
|
130
|
+
if isinstance(data, (ndarray, Quantity)):
|
|
131
|
+
if Quantity is not ndarray and isinstance(data, Quantity):
|
|
132
|
+
data = data.magnitude
|
|
133
|
+
elif isinstance(data, DataArray):
|
|
134
|
+
if Quantity is not ndarray and isinstance(data.data, Quantity):
|
|
135
|
+
data = data.copy(deep=False)
|
|
136
|
+
data.data = data.data.magnitude
|
|
137
|
+
return data
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _to_numpy_array(data, strip_units=False):
|
|
141
|
+
"""
|
|
142
|
+
Convert arbitrary input to numpy array. Preserve masked arrays and unit arrays.
|
|
143
|
+
"""
|
|
144
|
+
_load_objects()
|
|
145
|
+
if data is None:
|
|
146
|
+
raise ValueError("Invalid data None.")
|
|
147
|
+
if isinstance(data, ndarray):
|
|
148
|
+
pass
|
|
149
|
+
elif isinstance(data, DataArray):
|
|
150
|
+
data = data.data # support pint quantities that get unit-stripped later
|
|
151
|
+
elif isinstance(data, (DataFrame, Series, Index)):
|
|
152
|
+
data = data.values
|
|
153
|
+
if Quantity is not ndarray and isinstance(data, Quantity):
|
|
154
|
+
if strip_units:
|
|
155
|
+
return np.atleast_1d(data.magnitude)
|
|
156
|
+
else:
|
|
157
|
+
return np.atleast_1d(data.magnitude) * data.units
|
|
158
|
+
else:
|
|
159
|
+
return np.atleast_1d(data) # natively preserves masked arrays
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _to_masked_array(data, *, copy=False):
|
|
163
|
+
"""
|
|
164
|
+
Convert numpy array to masked array with consideration for datetimes and quantities.
|
|
165
|
+
"""
|
|
166
|
+
units = None
|
|
167
|
+
if ndarray is not Quantity and isinstance(data, Quantity):
|
|
168
|
+
data, units = data.magnitude, data.units
|
|
169
|
+
else:
|
|
170
|
+
data = _to_numpy_array(data)
|
|
171
|
+
if data.dtype == "O":
|
|
172
|
+
data = ma.array(data, mask=False)
|
|
173
|
+
else:
|
|
174
|
+
data = ma.masked_invalid(data, copy=copy)
|
|
175
|
+
if np.issubdtype(data.dtype, np.integer):
|
|
176
|
+
data = data.astype(np.float64)
|
|
177
|
+
if np.issubdtype(data.dtype, np.number):
|
|
178
|
+
data.fill_value *= np.nan # default float fill_value is 1e+20 or 1e+20 + 0j
|
|
179
|
+
else:
|
|
180
|
+
pass # leave with default fill_value (e.g. NaT for datetime data)
|
|
181
|
+
return data, units
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Input data transformations
|
|
185
|
+
def _to_edges(x, y, z):
|
|
186
|
+
"""
|
|
187
|
+
Enforce that coordinates are edges. Convert from centers if possible.
|
|
188
|
+
"""
|
|
189
|
+
from ..utils import edges, edges2d
|
|
190
|
+
|
|
191
|
+
xlen, ylen = x.shape[-1], y.shape[0]
|
|
192
|
+
if z.ndim == 2 and z.shape[1] == xlen and z.shape[0] == ylen:
|
|
193
|
+
# Get edges given centers
|
|
194
|
+
if all(z.ndim == 1 and z.size > 1 and _is_numeric(z) for z in (x, y)):
|
|
195
|
+
x = edges(x)
|
|
196
|
+
y = edges(y)
|
|
197
|
+
else:
|
|
198
|
+
if x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 and _is_numeric(x):
|
|
199
|
+
x = edges2d(x)
|
|
200
|
+
if y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 and _is_numeric(y):
|
|
201
|
+
y = edges2d(y)
|
|
202
|
+
elif z.shape[-1] != xlen - 1 or z.shape[0] != ylen - 1:
|
|
203
|
+
# Helpful error message
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"Input shapes x {x.shape} and y {y.shape} must match "
|
|
206
|
+
f"array centers {z.shape} or "
|
|
207
|
+
f"array borders {tuple(i + 1 for i in z.shape)}."
|
|
208
|
+
)
|
|
209
|
+
return x, y
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _to_centers(x, y, z):
|
|
213
|
+
"""
|
|
214
|
+
Enforce that coordinates are centers. Convert from edges if possible.
|
|
215
|
+
"""
|
|
216
|
+
xlen, ylen = x.shape[-1], y.shape[0]
|
|
217
|
+
if z.ndim == 2 and z.shape[1] == xlen - 1 and z.shape[0] == ylen - 1:
|
|
218
|
+
# Get centers given edges
|
|
219
|
+
if all(z.ndim == 1 and z.size > 1 and _is_numeric(z) for z in (x, y)):
|
|
220
|
+
x = 0.5 * (x[1:] + x[:-1])
|
|
221
|
+
y = 0.5 * (y[1:] + y[:-1])
|
|
222
|
+
else:
|
|
223
|
+
if x.ndim == 2 and x.shape[0] > 1 and x.shape[1] > 1 and _is_numeric(x):
|
|
224
|
+
x = 0.25 * (x[:-1, :-1] + x[:-1, 1:] + x[1:, :-1] + x[1:, 1:])
|
|
225
|
+
if y.ndim == 2 and y.shape[0] > 1 and y.shape[1] > 1 and _is_numeric(y):
|
|
226
|
+
y = 0.25 * (y[:-1, :-1] + y[:-1, 1:] + y[1:, :-1] + y[1:, 1:])
|
|
227
|
+
elif z.shape[-1] != xlen or z.shape[0] != ylen:
|
|
228
|
+
# Helpful error message
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"Input shapes x {x.shape} and y {y.shape} "
|
|
231
|
+
f"must match z centers {z.shape} "
|
|
232
|
+
f"or z borders {tuple(i+1 for i in z.shape)}."
|
|
233
|
+
)
|
|
234
|
+
return x, y
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Input argument processing
|
|
238
|
+
def _from_data(data, *args):
|
|
239
|
+
"""
|
|
240
|
+
Try to convert positional `key` arguments to `data[key]`. If argument is string
|
|
241
|
+
it could be a valid positional argument like `fmt` so do not raise error.
|
|
242
|
+
"""
|
|
243
|
+
if data is None:
|
|
244
|
+
return
|
|
245
|
+
args = list(args)
|
|
246
|
+
for i, arg in enumerate(args):
|
|
247
|
+
if isinstance(arg, str):
|
|
248
|
+
try:
|
|
249
|
+
array = data[arg]
|
|
250
|
+
except KeyError:
|
|
251
|
+
pass
|
|
252
|
+
else:
|
|
253
|
+
args[i] = array
|
|
254
|
+
return args
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _preprocess_or_redirect(*keys, keywords=None, allow_extra=True):
|
|
258
|
+
"""
|
|
259
|
+
Redirect internal plotting calls to native matplotlib methods. Also convert
|
|
260
|
+
keyword args to positional and pass arguments through 'data' dictionary.
|
|
261
|
+
"""
|
|
262
|
+
# Keyword arguments processed through 'data'
|
|
263
|
+
# Positional arguments are always processed through data
|
|
264
|
+
keywords = keywords or ()
|
|
265
|
+
if isinstance(keywords, str):
|
|
266
|
+
keywords = (keywords,)
|
|
267
|
+
|
|
268
|
+
def _decorator(func):
|
|
269
|
+
name = func.__name__
|
|
270
|
+
from . import _kwargs_to_args
|
|
271
|
+
|
|
272
|
+
@functools.wraps(func)
|
|
273
|
+
def _preprocess_or_redirect(self, *args, **kwargs):
|
|
274
|
+
if getattr(self, "_internal_call", None):
|
|
275
|
+
# Redirect internal matplotlib call to native function
|
|
276
|
+
from ..axes import PlotAxes
|
|
277
|
+
|
|
278
|
+
func_native = getattr(super(PlotAxes, self), name)
|
|
279
|
+
return func_native(*args, **kwargs)
|
|
280
|
+
else:
|
|
281
|
+
# Impose default coordinate system
|
|
282
|
+
from ..constructor import Proj
|
|
283
|
+
|
|
284
|
+
if self._name == "basemap" and name in BASEMAP_FUNCS:
|
|
285
|
+
if kwargs.get("latlon", None) is None:
|
|
286
|
+
kwargs["latlon"] = True
|
|
287
|
+
if self._name == "cartopy" and name in CARTOPY_FUNCS:
|
|
288
|
+
if kwargs.get("transform", None) is None:
|
|
289
|
+
kwargs["transform"] = PlateCarree()
|
|
290
|
+
else:
|
|
291
|
+
kwargs["transform"] = Proj(kwargs["transform"])
|
|
292
|
+
|
|
293
|
+
# Process data args
|
|
294
|
+
# NOTE: Raises error if there are more args than keys
|
|
295
|
+
args, kwargs = _kwargs_to_args(
|
|
296
|
+
keys, *args, allow_extra=allow_extra, **kwargs
|
|
297
|
+
)
|
|
298
|
+
data = kwargs.pop("data", None)
|
|
299
|
+
if data is not None:
|
|
300
|
+
args = _from_data(data, *args)
|
|
301
|
+
for key in set(keywords) & set(kwargs):
|
|
302
|
+
kwargs[key] = _from_data(data, kwargs[key])
|
|
303
|
+
|
|
304
|
+
# Auto-setup matplotlib with the input unit registry
|
|
305
|
+
_load_objects()
|
|
306
|
+
for arg in args:
|
|
307
|
+
if ndarray is not DataArray and isinstance(arg, DataArray):
|
|
308
|
+
arg = arg.data
|
|
309
|
+
if ndarray is not Quantity and isinstance(arg, Quantity):
|
|
310
|
+
ureg = getattr(arg, "_REGISTRY", None)
|
|
311
|
+
if hasattr(ureg, "setup_matplotlib"):
|
|
312
|
+
ureg.setup_matplotlib(True)
|
|
313
|
+
|
|
314
|
+
# Sanitize colors
|
|
315
|
+
# Lookup can be a 2-tuple with colormap, and integer denoting the color from that map
|
|
316
|
+
# it also sanitizes if the second color is a float. Note that for float, the color
|
|
317
|
+
# will be picked proportional to idx/255
|
|
318
|
+
from .. import colors as pcolor
|
|
319
|
+
|
|
320
|
+
color = kwargs.pop("color", None)
|
|
321
|
+
if isinstance(color, tuple) and len(color) == 2:
|
|
322
|
+
cmap, color = color
|
|
323
|
+
color = pcolor._cmap_database.get_cmap(cmap)(color)
|
|
324
|
+
if color is not None:
|
|
325
|
+
kwargs["color"] = color
|
|
326
|
+
# Call main function
|
|
327
|
+
return func(self, *args, **kwargs) # call unbound method
|
|
328
|
+
|
|
329
|
+
return _preprocess_or_redirect
|
|
330
|
+
|
|
331
|
+
return _decorator
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# Stats utiltiies
|
|
335
|
+
def _dist_clean(distribution):
|
|
336
|
+
"""
|
|
337
|
+
Clean the distribution data for processing by `boxplot` or `violinplot`.
|
|
338
|
+
Handles np.ndarrays where the ndarray is a list of lists of variable sizes.
|
|
339
|
+
"""
|
|
340
|
+
if isinstance(distribution, np.ndarray):
|
|
341
|
+
if distribution.dtype == object:
|
|
342
|
+
# Handle list of lists with variable sizes
|
|
343
|
+
return tuple(
|
|
344
|
+
np.array(sublist, dtype=float)
|
|
345
|
+
for sublist in distribution
|
|
346
|
+
if len(sublist) > 0
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
# Handle regular numpy arrays
|
|
350
|
+
if distribution.ndim == 1:
|
|
351
|
+
distribution = distribution[:, None]
|
|
352
|
+
distribution, units = _to_masked_array(distribution) # no copy needed
|
|
353
|
+
distribution = tuple(
|
|
354
|
+
distribution[..., i].compressed() for i in range(distribution.shape[-1])
|
|
355
|
+
)
|
|
356
|
+
if units is not None:
|
|
357
|
+
distribution = tuple(dist * units for dist in distribution)
|
|
358
|
+
return distribution
|
|
359
|
+
elif isinstance(distribution, list):
|
|
360
|
+
# Handle list of lists directly
|
|
361
|
+
return tuple(
|
|
362
|
+
np.array(sublist, dtype=float)
|
|
363
|
+
for sublist in distribution
|
|
364
|
+
if len(sublist) > 0
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
raise ValueError("Input must be a numpy array or a list of lists")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _dist_reduce(data, *, mean=None, means=None, median=None, medians=None, **kwargs):
|
|
371
|
+
"""
|
|
372
|
+
Reduce statistical distributions to means and medians. Tack on a
|
|
373
|
+
distribution keyword argument for processing down the line.
|
|
374
|
+
"""
|
|
375
|
+
# TODO: Permit 3D array with error dimension coming first
|
|
376
|
+
means = _not_none(mean=mean, means=means)
|
|
377
|
+
medians = _not_none(median=median, medians=medians)
|
|
378
|
+
if means and medians:
|
|
379
|
+
warnings._warn_ultraplot(
|
|
380
|
+
"Cannot have both means=True and medians=True. Using former."
|
|
381
|
+
)
|
|
382
|
+
medians = None
|
|
383
|
+
if means or medians:
|
|
384
|
+
distribution, units = _to_masked_array(data)
|
|
385
|
+
distribution = distribution.filled()
|
|
386
|
+
if distribution.ndim != 2:
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"Expected 2D array for means=True. Got {distribution.ndim}D."
|
|
389
|
+
)
|
|
390
|
+
if units is not None:
|
|
391
|
+
distribution = distribution * units
|
|
392
|
+
if means:
|
|
393
|
+
data = np.nanmean(distribution, axis=0)
|
|
394
|
+
else:
|
|
395
|
+
data = np.nanmedian(distribution, axis=0)
|
|
396
|
+
kwargs["distribution"] = distribution
|
|
397
|
+
|
|
398
|
+
# Save argument passed to _error_bars
|
|
399
|
+
return (data, kwargs)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _dist_range(
|
|
403
|
+
data,
|
|
404
|
+
distribution,
|
|
405
|
+
*,
|
|
406
|
+
errdata=None,
|
|
407
|
+
absolute=False,
|
|
408
|
+
label=False,
|
|
409
|
+
stds=None,
|
|
410
|
+
pctiles=None,
|
|
411
|
+
stds_default=None,
|
|
412
|
+
pctiles_default=None,
|
|
413
|
+
):
|
|
414
|
+
"""
|
|
415
|
+
Return a plottable characteristic range for the statistical distribution
|
|
416
|
+
relative to the input coordinate (generally a mean or median).
|
|
417
|
+
"""
|
|
418
|
+
# Parse stds arguments
|
|
419
|
+
# NOTE: Have to guard against "truth value of an array is ambiguous" errors
|
|
420
|
+
if stds is True:
|
|
421
|
+
stds = stds_default
|
|
422
|
+
elif stds is False or stds is None:
|
|
423
|
+
stds = None
|
|
424
|
+
else:
|
|
425
|
+
stds = np.atleast_1d(stds)
|
|
426
|
+
if stds.size == 1:
|
|
427
|
+
stds = sorted((-stds.item(), stds.item()))
|
|
428
|
+
elif stds.size != 2:
|
|
429
|
+
raise ValueError("Expected scalar or length-2 stdev specification.")
|
|
430
|
+
|
|
431
|
+
# Parse pctiles arguments
|
|
432
|
+
if pctiles is True:
|
|
433
|
+
pctiles = pctiles_default
|
|
434
|
+
elif pctiles is False or pctiles is None:
|
|
435
|
+
pctiles = None
|
|
436
|
+
else:
|
|
437
|
+
pctiles = np.atleast_1d(pctiles)
|
|
438
|
+
if pctiles.size == 1:
|
|
439
|
+
delta = (100 - pctiles.item()) / 2.0
|
|
440
|
+
pctiles = sorted((delta, 100 - delta))
|
|
441
|
+
elif pctiles.size != 2:
|
|
442
|
+
raise ValueError("Expected scalar or length-2 pctiles specification.")
|
|
443
|
+
|
|
444
|
+
# Incompatible settings
|
|
445
|
+
if distribution is None and any(_ is not None for _ in (stds, pctiles)):
|
|
446
|
+
raise ValueError(
|
|
447
|
+
"To automatically compute standard deviations or percentiles on "
|
|
448
|
+
"columns of data you must pass means=True or medians=True."
|
|
449
|
+
)
|
|
450
|
+
if stds is not None and pctiles is not None:
|
|
451
|
+
warnings._warn_ultraplot(
|
|
452
|
+
"Got both a standard deviation range and a percentile range for "
|
|
453
|
+
"auto error indicators. Using the standard deviation range."
|
|
454
|
+
)
|
|
455
|
+
pctiles = None
|
|
456
|
+
if distribution is not None and errdata is not None:
|
|
457
|
+
stds = pctiles = None
|
|
458
|
+
warnings._warn_ultraplot(
|
|
459
|
+
"You explicitly provided the error bounds but also requested "
|
|
460
|
+
"automatically calculating means or medians on data columns. "
|
|
461
|
+
'It may make more sense to use the "stds" or "pctiles" keyword args '
|
|
462
|
+
"and have *ultraplot* calculate the error bounds."
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Compute error data in format that can be passed to maxes.Axes.errorbar()
|
|
466
|
+
# NOTE: Include option to pass symmetric deviation from central points
|
|
467
|
+
if errdata is not None:
|
|
468
|
+
# Manual error data
|
|
469
|
+
if data.ndim != 1:
|
|
470
|
+
raise ValueError(
|
|
471
|
+
"Passing both 2D data coordinates and 'errdata' is not yet supported."
|
|
472
|
+
)
|
|
473
|
+
label_default = "uncertainty"
|
|
474
|
+
err = _to_numpy_array(errdata)
|
|
475
|
+
if (
|
|
476
|
+
err.ndim not in (1, 2)
|
|
477
|
+
or err.shape[-1] != data.size
|
|
478
|
+
or err.ndim == 2
|
|
479
|
+
and err.shape[0] != 2
|
|
480
|
+
):
|
|
481
|
+
raise ValueError(
|
|
482
|
+
f"Input 'errdata' has shape {err.shape}. Expected (2, {data.size})."
|
|
483
|
+
)
|
|
484
|
+
if err.ndim == 1:
|
|
485
|
+
abserr = err
|
|
486
|
+
err = np.empty((2, err.size))
|
|
487
|
+
err[0, :] = data - abserr # translated back to absolute deviations below
|
|
488
|
+
err[1, :] = data + abserr
|
|
489
|
+
elif stds is not None:
|
|
490
|
+
# Standard deviations
|
|
491
|
+
# NOTE: Invalid values were handled by _dist_reduce
|
|
492
|
+
label_default = rf"{abs(stds[1])}$\sigma$ range"
|
|
493
|
+
stds = _to_numpy_array(stds)[:, None]
|
|
494
|
+
err = data + stds * np.nanstd(distribution, axis=0)
|
|
495
|
+
elif pctiles is not None:
|
|
496
|
+
# Percentiles
|
|
497
|
+
# NOTE: Invalid values were handled by _dist_reduce
|
|
498
|
+
label_default = f"{pctiles[1] - pctiles[0]}% range"
|
|
499
|
+
err = np.nanpercentile(distribution, pctiles, axis=0)
|
|
500
|
+
else:
|
|
501
|
+
warnings._warn_ultraplot(
|
|
502
|
+
"Error indications are missing from the dataset reduced by a "
|
|
503
|
+
"mean or median operation. Consider passing e.g. bars=True."
|
|
504
|
+
)
|
|
505
|
+
err = None
|
|
506
|
+
|
|
507
|
+
# Adjust error bounds
|
|
508
|
+
if err is not None and not absolute: # for errorbar() ingestion
|
|
509
|
+
err = err - data
|
|
510
|
+
err[0, :] *= -1 # absolute deviations from central points
|
|
511
|
+
|
|
512
|
+
# Apply legend entry
|
|
513
|
+
if isinstance(label, str):
|
|
514
|
+
pass
|
|
515
|
+
elif label: # e.g. label=True says to use a default label
|
|
516
|
+
label = label_default
|
|
517
|
+
else:
|
|
518
|
+
label = None
|
|
519
|
+
|
|
520
|
+
return err, label
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _safe_mask(mask, *args):
|
|
524
|
+
"""
|
|
525
|
+
Safely apply the mask to the input arrays, accounting for existing masked
|
|
526
|
+
or invalid values. Values matching ``False`` are set to `np.nan`.
|
|
527
|
+
"""
|
|
528
|
+
# NOTE: Could also convert unmasked data to masked. But other way around is
|
|
529
|
+
# easier becase np.ma gives us correct fill values for data subtypes.
|
|
530
|
+
_load_objects()
|
|
531
|
+
invalid = ~mask # True if invalid
|
|
532
|
+
args_masked = []
|
|
533
|
+
for data in args:
|
|
534
|
+
data, units = _to_masked_array(data, copy=True)
|
|
535
|
+
nan = data.fill_value
|
|
536
|
+
data = data.filled()
|
|
537
|
+
if data.size > 1 and data.shape != invalid.shape:
|
|
538
|
+
raise ValueError(
|
|
539
|
+
f"Mask shape {mask.shape} incompatible with array shape {data.shape}."
|
|
540
|
+
)
|
|
541
|
+
if data.size == 1 or invalid.size == 1: # NOTE: happens with _restrict_inbounds
|
|
542
|
+
pass
|
|
543
|
+
elif invalid.size == 1:
|
|
544
|
+
data = nan if invalid.item() else data
|
|
545
|
+
elif data.size > 1:
|
|
546
|
+
data[invalid] = nan
|
|
547
|
+
if units is not None:
|
|
548
|
+
data = data * units
|
|
549
|
+
args_masked.append(data)
|
|
550
|
+
return args_masked[0] if len(args_masked) == 1 else args_masked
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _safe_range(data, lo=0, hi=100):
|
|
554
|
+
"""
|
|
555
|
+
Safely return the minimum and maximum (default) or percentile range accounting
|
|
556
|
+
for masked values. Use min and max functions when possible for speed. Return
|
|
557
|
+
``None`` if we fail to get a valid range.
|
|
558
|
+
"""
|
|
559
|
+
_load_objects()
|
|
560
|
+
data, units = _to_masked_array(data)
|
|
561
|
+
data = data.compressed() # remove all invalid values
|
|
562
|
+
min_ = max_ = None
|
|
563
|
+
if data.size:
|
|
564
|
+
min_ = np.min(data) if lo <= 0 else np.percentile(data, lo)
|
|
565
|
+
if hasattr(min_, "dtype") and np.issubdtype(min_.dtype, np.integer):
|
|
566
|
+
min_ = np.float64(min_)
|
|
567
|
+
try:
|
|
568
|
+
is_finite = np.isfinite(min_)
|
|
569
|
+
except TypeError:
|
|
570
|
+
is_finite = True
|
|
571
|
+
if not is_finite:
|
|
572
|
+
min_ = None
|
|
573
|
+
elif units is not None:
|
|
574
|
+
min_ *= units
|
|
575
|
+
if data.size:
|
|
576
|
+
max_ = np.max(data) if hi >= 100 else np.percentile(data, hi)
|
|
577
|
+
if hasattr(max_, "dtype") and np.issubdtype(max_.dtype, np.integer):
|
|
578
|
+
max_ = np.float64(max_)
|
|
579
|
+
try:
|
|
580
|
+
is_finite = np.isfinite(min_)
|
|
581
|
+
except TypeError:
|
|
582
|
+
is_finite = True
|
|
583
|
+
if not is_finite:
|
|
584
|
+
max_ = None
|
|
585
|
+
elif units is not None:
|
|
586
|
+
max_ *= units
|
|
587
|
+
return min_, max_
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# Metadata utilities
|
|
591
|
+
def _meta_coords(*args, which="x", **kwargs):
|
|
592
|
+
"""
|
|
593
|
+
Return the index arrays associated with string coordinates and
|
|
594
|
+
keyword arguments updated with index locators and formatters.
|
|
595
|
+
"""
|
|
596
|
+
# NOTE: Why FixedLocator and not IndexLocator? The ticks chosen by the latter
|
|
597
|
+
# depend on other plotted content.
|
|
598
|
+
# NOTE: Why IndexFormatter and not FixedFormatter? The former ensures labels
|
|
599
|
+
# correspond to indices while the latter can mysteriously truncate labels.
|
|
600
|
+
from ..constructor import Formatter, Locator
|
|
601
|
+
|
|
602
|
+
res = []
|
|
603
|
+
for data in args:
|
|
604
|
+
data = _to_duck_array(data)
|
|
605
|
+
if not _is_categorical(data):
|
|
606
|
+
res.append(data)
|
|
607
|
+
continue
|
|
608
|
+
if data.ndim > 1:
|
|
609
|
+
raise ValueError("Non-1D string coordinate input is unsupported.")
|
|
610
|
+
ticks = np.arange(len(data))
|
|
611
|
+
labels = list(map(str, data))
|
|
612
|
+
kwargs.setdefault(which + "locator", Locator(ticks))
|
|
613
|
+
kwargs.setdefault(which + "formatter", Formatter(labels, index=True))
|
|
614
|
+
kwargs.setdefault(which + "minorlocator", Locator("null"))
|
|
615
|
+
res.append(ticks) # use these as data coordinates
|
|
616
|
+
return (*res, kwargs)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _meta_labels(data, axis=0, always=True):
|
|
620
|
+
"""
|
|
621
|
+
Return the array-like "labels" along axis `axis`. If `always` is ``False``
|
|
622
|
+
we return ``None`` for simple ndarray input.
|
|
623
|
+
"""
|
|
624
|
+
# NOTE: Previously inferred 'axis 1' metadata of 1D variable using the
|
|
625
|
+
# data values metadata but that is incorrect. The paradigm for 1D plots
|
|
626
|
+
# is we have row coordinates representing x, data values representing y,
|
|
627
|
+
# and column coordinates representing individual series.
|
|
628
|
+
_load_objects()
|
|
629
|
+
labels = None
|
|
630
|
+
if axis not in (0, 1, 2):
|
|
631
|
+
raise ValueError(f"Invalid axis {axis}.")
|
|
632
|
+
if isinstance(data, (ndarray, Quantity)):
|
|
633
|
+
if not always:
|
|
634
|
+
pass
|
|
635
|
+
elif axis < data.ndim:
|
|
636
|
+
labels = np.arange(data.shape[axis])
|
|
637
|
+
else: # requesting 'axis 1' on a 1D array
|
|
638
|
+
labels = np.array([0])
|
|
639
|
+
# Xarray object
|
|
640
|
+
# NOTE: Even if coords not present .coords[dim] auto-generates indices
|
|
641
|
+
elif isinstance(data, DataArray):
|
|
642
|
+
if axis < data.ndim:
|
|
643
|
+
labels = data.coords[data.dims[axis]]
|
|
644
|
+
elif not always:
|
|
645
|
+
pass
|
|
646
|
+
else:
|
|
647
|
+
labels = np.array([0])
|
|
648
|
+
# Pandas object
|
|
649
|
+
elif isinstance(data, (DataFrame, Series, Index)):
|
|
650
|
+
if axis == 0 and isinstance(data, (DataFrame, Series)):
|
|
651
|
+
labels = data.index
|
|
652
|
+
elif axis == 1 and isinstance(data, (DataFrame,)):
|
|
653
|
+
labels = data.columns
|
|
654
|
+
elif not always:
|
|
655
|
+
pass
|
|
656
|
+
else: # beyond dimensionality
|
|
657
|
+
labels = np.array([0])
|
|
658
|
+
# Everything else
|
|
659
|
+
# NOTE: Ensure data is at least 1D in _to_duck_array so this covers everything
|
|
660
|
+
else:
|
|
661
|
+
raise ValueError(f"Unrecognized array type {type(data)}.")
|
|
662
|
+
return labels
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _meta_title(data, include_units=True):
|
|
666
|
+
"""
|
|
667
|
+
Return the "title" of an array-like object with metadata.
|
|
668
|
+
Include units in the title if `include_units` is ``True``.
|
|
669
|
+
"""
|
|
670
|
+
_load_objects()
|
|
671
|
+
title = units = None
|
|
672
|
+
if isinstance(data, ndarray):
|
|
673
|
+
pass
|
|
674
|
+
# Xarray object with possible long_name, standard_name, and units attributes.
|
|
675
|
+
# Output depends on if units is True. Prefer long_name (come last in loop).
|
|
676
|
+
elif isinstance(data, DataArray):
|
|
677
|
+
title = getattr(data, "name", None)
|
|
678
|
+
for key in ("standard_name", "long_name"):
|
|
679
|
+
title = data.attrs.get(key, title)
|
|
680
|
+
if include_units:
|
|
681
|
+
units = _meta_units(data)
|
|
682
|
+
# Pandas object. DataFrame has no native name attribute but user can add one
|
|
683
|
+
# See: https://github.com/pandas-dev/pandas/issues/447
|
|
684
|
+
elif isinstance(data, (DataFrame, Series, Index)):
|
|
685
|
+
title = getattr(data, "name", None) or None
|
|
686
|
+
# Pint Quantity
|
|
687
|
+
elif isinstance(data, Quantity):
|
|
688
|
+
if include_units:
|
|
689
|
+
units = _meta_units(data)
|
|
690
|
+
# Add units or return units alone
|
|
691
|
+
if title and units:
|
|
692
|
+
title = f"{title} ({units})"
|
|
693
|
+
else:
|
|
694
|
+
title = title or units
|
|
695
|
+
if title is not None:
|
|
696
|
+
title = str(title).strip()
|
|
697
|
+
return title
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _meta_units(data):
|
|
701
|
+
"""
|
|
702
|
+
Get the unit string from the `xarray.DataArray` attributes or the
|
|
703
|
+
`pint.Quantity`. Format the latter with :rcraw:`unitformat`.
|
|
704
|
+
"""
|
|
705
|
+
_load_objects()
|
|
706
|
+
# Get units from the attributes
|
|
707
|
+
if ndarray is not DataArray and isinstance(data, DataArray):
|
|
708
|
+
units = data.attrs.get("units", None)
|
|
709
|
+
data = data.data
|
|
710
|
+
if units is not None:
|
|
711
|
+
return units
|
|
712
|
+
# Get units from the quantity
|
|
713
|
+
if ndarray is not Quantity and isinstance(data, Quantity):
|
|
714
|
+
from ..config import rc
|
|
715
|
+
|
|
716
|
+
fmt = rc.unitformat
|
|
717
|
+
try:
|
|
718
|
+
units = format(data.units, fmt)
|
|
719
|
+
except (TypeError, ValueError):
|
|
720
|
+
warnings._warn_ultraplot(
|
|
721
|
+
f"Failed to format pint quantity with format string {fmt!r}."
|
|
722
|
+
)
|
|
723
|
+
else:
|
|
724
|
+
if "L" in fmt: # auto-apply LaTeX math indicator
|
|
725
|
+
units = "$" + units + "$"
|
|
726
|
+
return units
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
# Geographic utiltiies
|
|
730
|
+
def _geo_basemap_1d(x, *ys, xmin=-180, xmax=180):
|
|
731
|
+
"""
|
|
732
|
+
Fix basemap geographic 1D data arrays.
|
|
733
|
+
"""
|
|
734
|
+
ys = _geo_clip(*ys)
|
|
735
|
+
x_orig, ys_orig, ys = x, ys, []
|
|
736
|
+
for y_orig in ys_orig:
|
|
737
|
+
x, y = _geo_inbounds(x_orig, y_orig, xmin=xmin, xmax=xmax)
|
|
738
|
+
ys.append(y)
|
|
739
|
+
return (x, *ys)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _geo_basemap_2d(x, y, *zs, xmin=-180, xmax=180, globe=False):
|
|
743
|
+
"""
|
|
744
|
+
Fix basemap geographic 2D data arrays.
|
|
745
|
+
"""
|
|
746
|
+
y = _geo_clip(y)
|
|
747
|
+
x_orig, y_orig, zs_orig, zs = x, y, zs, []
|
|
748
|
+
for z_orig in zs_orig:
|
|
749
|
+
x, y, z = x_orig, y_orig, z_orig
|
|
750
|
+
x, z = _geo_inbounds(x, z, xmin=xmin, xmax=xmax)
|
|
751
|
+
if globe and z is not None and x.ndim == 1 and y.ndim == 1:
|
|
752
|
+
x, y, z = _geo_globe(x, y, z, xmin=xmin, modulo=False)
|
|
753
|
+
zs.append(z)
|
|
754
|
+
return (x, y, *zs)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _geo_cartopy_1d(x, *ys):
|
|
758
|
+
"""
|
|
759
|
+
Fix cartopy geographic 1D data arrays.
|
|
760
|
+
"""
|
|
761
|
+
ys = _geo_clip(ys)
|
|
762
|
+
return (x, *ys)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _geo_cartopy_2d(x, y, *zs, globe=False):
|
|
766
|
+
"""
|
|
767
|
+
Fix cartopy geographic 2D data arrays.
|
|
768
|
+
"""
|
|
769
|
+
y = _geo_clip(y)
|
|
770
|
+
x_orig, y_orig, zs_orig = x, y, zs
|
|
771
|
+
zs = []
|
|
772
|
+
for z_orig in zs_orig:
|
|
773
|
+
x, y, z = x_orig, y_orig, z_orig
|
|
774
|
+
if globe and z is not None and x.ndim == 1 and y.ndim == 1:
|
|
775
|
+
x, y, z = _geo_globe(x, y, z, modulo=True)
|
|
776
|
+
zs.append(z)
|
|
777
|
+
return (x, y, *zs)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _geo_clip(*ys):
|
|
781
|
+
"""
|
|
782
|
+
Ensure latitudes fall within ``-90`` to ``90``. Important if we
|
|
783
|
+
add graticule edges with `edges`.
|
|
784
|
+
"""
|
|
785
|
+
ys = tuple(np.clip(y, -90, 90) for y in ys)
|
|
786
|
+
return ys[0] if len(ys) == 1 else ys
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _geo_inbounds(x, y, xmin=-180, xmax=180):
|
|
790
|
+
"""
|
|
791
|
+
Fix conflicts with map coordinates by rolling the data to fall between the
|
|
792
|
+
minimum and maximum longitudes and masking out-of-bounds data points.
|
|
793
|
+
"""
|
|
794
|
+
# Roll in same direction if some points on right-edge extend
|
|
795
|
+
# more than 360 above min longitude; *they* should be on left side
|
|
796
|
+
if x.ndim != 1:
|
|
797
|
+
return x, y
|
|
798
|
+
lonroll = np.where(x > xmin + 360)[0] # tuple of ids
|
|
799
|
+
if lonroll.size: # non-empty
|
|
800
|
+
roll = x.size - lonroll.min()
|
|
801
|
+
x = np.roll(x, roll)
|
|
802
|
+
y = np.roll(y, roll, axis=-1)
|
|
803
|
+
x[:roll] -= 360 # make monotonic
|
|
804
|
+
# Set NaN where data not in range xmin, xmax. Must be done for regional smaller
|
|
805
|
+
# projections or get weird side-effects from valid data outside boundaries
|
|
806
|
+
y, units = _to_masked_array(y)
|
|
807
|
+
nan = y.fill_value
|
|
808
|
+
y = y.filled()
|
|
809
|
+
if not y.shape:
|
|
810
|
+
pass
|
|
811
|
+
elif x.size - 1 == y.shape[-1]: # test western/eastern grid cell edges
|
|
812
|
+
mask = (x[1:] < xmin) | (x[:-1] > xmax)
|
|
813
|
+
y[..., mask] = nan
|
|
814
|
+
elif x.size == y.shape[-1]: # test the centers and pad by one for safety
|
|
815
|
+
(where,) = np.where((x < xmin) | (x > xmax))
|
|
816
|
+
y[..., where[1:-1]] = nan
|
|
817
|
+
return x, y
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _geo_globe(x, y, z, xmin=-180, modulo=False):
|
|
821
|
+
"""
|
|
822
|
+
Ensure global coverage by fixing gaps over poles and across
|
|
823
|
+
longitude seams. Increases the size of the arrays.
|
|
824
|
+
"""
|
|
825
|
+
# Cover gaps over poles by appending polar data
|
|
826
|
+
with np.errstate(all="ignore"):
|
|
827
|
+
p1 = np.mean(z[0, :]) # do not ignore NaN if present
|
|
828
|
+
p2 = np.mean(z[-1, :])
|
|
829
|
+
ps = (-90, 90) if (y[0] < y[-1]) else (90, -90)
|
|
830
|
+
z1 = np.repeat(p1, z.shape[1])
|
|
831
|
+
z2 = np.repeat(p2, z.shape[1])
|
|
832
|
+
y = ma.concatenate((ps[:1], y, ps[1:]))
|
|
833
|
+
z = ma.concatenate((z1[None, :], z, z2[None, :]), axis=0)
|
|
834
|
+
# Cover gaps over cartopy longitude seam
|
|
835
|
+
# Ensure coordinates span 360 after modulus
|
|
836
|
+
if modulo:
|
|
837
|
+
if x[0] % 360 != (x[-1] + 360) % 360:
|
|
838
|
+
x = ma.concatenate((x, (x[0] + 360,)))
|
|
839
|
+
z = ma.concatenate((z, z[:, :1]), axis=1)
|
|
840
|
+
# Cover gaps over basemap longitude seam
|
|
841
|
+
# Ensure coordinates span exactly 360
|
|
842
|
+
else:
|
|
843
|
+
# Interpolate coordinate centers to seam. Size possibly augmented by 2
|
|
844
|
+
if x.size == z.shape[1]:
|
|
845
|
+
if x[0] + 360 != x[-1]:
|
|
846
|
+
xi = np.array([x[-1], x[0] + 360]) # input coordinates
|
|
847
|
+
xq = xmin + 360 # query coordinate
|
|
848
|
+
zq = ma.concatenate((z[:, -1:], z[:, :1]), axis=1)
|
|
849
|
+
zq = (zq[:, :1] * (xi[1] - xq) + zq[:, 1:] * (xq - xi[0])) / (
|
|
850
|
+
xi[1] - xi[0]
|
|
851
|
+
) # noqa: E501
|
|
852
|
+
x = ma.concatenate(((xmin,), x, (xmin + 360,)))
|
|
853
|
+
z = ma.concatenate((zq, z, zq), axis=1)
|
|
854
|
+
# Extend coordinate edges to seam. Size possibly augmented by 1.
|
|
855
|
+
elif x.size - 1 == z.shape[1]:
|
|
856
|
+
if x[0] != xmin:
|
|
857
|
+
x = ma.append(xmin, x)
|
|
858
|
+
x[-1] = xmin + 360
|
|
859
|
+
z = ma.concatenate((z[:, -1:], z), axis=1)
|
|
860
|
+
else:
|
|
861
|
+
raise ValueError("Unexpected shapes of coordinates or data arrays.")
|
|
862
|
+
return x, y, z
|