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/colors.py
ADDED
|
@@ -0,0 +1,3241 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Various colormap classes and colormap normalization classes.
|
|
4
|
+
"""
|
|
5
|
+
# NOTE: To avoid name conflicts between registered colormaps and colors, print
|
|
6
|
+
# set(pplt.colors._cmap_database) & set(pplt.colors._color_database) whenever
|
|
7
|
+
# you add new colormaps. v0.8 result is {'gray', 'marine', 'ocean', 'pink'} due
|
|
8
|
+
# to the MATLAB and GNUPlot colormaps. Want to minimize conflicts.
|
|
9
|
+
# NOTE: We feel that LinearSegmentedColormap should always be used for smooth color
|
|
10
|
+
# transitions while ListedColormap should always be used for qualitative color sets.
|
|
11
|
+
# Other sources use ListedColormap for dense "perceptually uniform" colormaps possibly
|
|
12
|
+
# seeking optimization. However testing reveals that initialization of even very
|
|
13
|
+
# dense 256-level colormaps is only 1.25ms vs. 0.25ms for a ListedColormap with the
|
|
14
|
+
# same data (+1ms). Also ListedColormap was designed for qualitative transitions
|
|
15
|
+
# because specifying N different from len(colors) will cyclically loop around the
|
|
16
|
+
# colors or truncate colors. So we translate the relevant ListedColormaps to
|
|
17
|
+
# LinearSegmentedColormaps for consistency. See :rc:`cmap.listedthresh`
|
|
18
|
+
import functools
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from collections.abc import MutableMapping
|
|
23
|
+
from numbers import Integral, Number
|
|
24
|
+
from xml.etree import ElementTree
|
|
25
|
+
|
|
26
|
+
import matplotlib.cm as mcm
|
|
27
|
+
import matplotlib as mpl
|
|
28
|
+
import matplotlib.colors as mcolors
|
|
29
|
+
import numpy as np
|
|
30
|
+
import numpy.ma as ma
|
|
31
|
+
|
|
32
|
+
from .config import rc
|
|
33
|
+
from .internals import ic # noqa: F401
|
|
34
|
+
from .internals import (
|
|
35
|
+
_kwargs_to_args,
|
|
36
|
+
_not_none,
|
|
37
|
+
_pop_props,
|
|
38
|
+
docstring,
|
|
39
|
+
inputs,
|
|
40
|
+
warnings,
|
|
41
|
+
)
|
|
42
|
+
from .utils import set_alpha, to_hex, to_rgb, to_rgba, to_xyz, to_xyza
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"DiscreteColormap",
|
|
46
|
+
"ContinuousColormap",
|
|
47
|
+
"PerceptualColormap",
|
|
48
|
+
"DiscreteNorm",
|
|
49
|
+
"DivergingNorm",
|
|
50
|
+
"SegmentedNorm",
|
|
51
|
+
"ColorDatabase",
|
|
52
|
+
"ColormapDatabase",
|
|
53
|
+
"ListedColormap", # deprecated
|
|
54
|
+
"LinearSegmentedColormap", # deprecated
|
|
55
|
+
"PerceptuallyUniformColormap", # deprecated
|
|
56
|
+
"LinearSegmentedNorm", # deprecated
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# Default colormap properties
|
|
60
|
+
DEFAULT_NAME = "_no_name"
|
|
61
|
+
DEFAULT_SPACE = "hsl"
|
|
62
|
+
|
|
63
|
+
# Color regexes
|
|
64
|
+
# NOTE: We do not compile hex regex because config.py needs this surrounded by \A\Z
|
|
65
|
+
_regex_hex = r"#(?:[0-9a-fA-F]{3,4}){2}" # 6-8 digit hex
|
|
66
|
+
REGEX_HEX_MULTI = re.compile(_regex_hex)
|
|
67
|
+
REGEX_HEX_SINGLE = re.compile(rf"\A{_regex_hex}\Z")
|
|
68
|
+
REGEX_ADJUST = re.compile(r"\A(light|dark|medium|pale|charcoal)?\s*(gr[ea]y[0-9]?)?\Z")
|
|
69
|
+
|
|
70
|
+
# Colormap constants
|
|
71
|
+
CMAPS_CYCLIC = tuple( # cyclic colormaps loaded from rgb files
|
|
72
|
+
key.lower()
|
|
73
|
+
for key in (
|
|
74
|
+
"MonoCycle",
|
|
75
|
+
"twilight",
|
|
76
|
+
"Phase",
|
|
77
|
+
"romaO",
|
|
78
|
+
"brocO",
|
|
79
|
+
"corkO",
|
|
80
|
+
"vikO",
|
|
81
|
+
"bamO",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
CMAPS_DIVERGING = { # mirrored dictionary mapping for reversed names
|
|
85
|
+
key.lower(): value.lower()
|
|
86
|
+
for key1, key2 in (
|
|
87
|
+
("BR", "RB"),
|
|
88
|
+
("NegPos", "PosNeg"),
|
|
89
|
+
("CoolWarm", "WarmCool"),
|
|
90
|
+
("ColdHot", "HotCold"),
|
|
91
|
+
("DryWet", "WetDry"),
|
|
92
|
+
("PiYG", "GYPi"),
|
|
93
|
+
("PRGn", "GnRP"),
|
|
94
|
+
("BrBG", "GBBr"),
|
|
95
|
+
("PuOr", "OrPu"),
|
|
96
|
+
("RdGy", "GyRd"),
|
|
97
|
+
("RdBu", "BuRd"),
|
|
98
|
+
("RdYlBu", "BuYlRd"),
|
|
99
|
+
("RdYlGn", "GnYlRd"),
|
|
100
|
+
)
|
|
101
|
+
for key, value in ((key1, key2), (key2, key1))
|
|
102
|
+
}
|
|
103
|
+
for _cmap_diverging in ( # remaining diverging cmaps (see PlotAxes._parse_cmap)
|
|
104
|
+
"Div",
|
|
105
|
+
"Vlag",
|
|
106
|
+
"Spectral",
|
|
107
|
+
"Balance",
|
|
108
|
+
"Delta",
|
|
109
|
+
"Curl",
|
|
110
|
+
"roma",
|
|
111
|
+
"broc",
|
|
112
|
+
"cork",
|
|
113
|
+
"vik",
|
|
114
|
+
"bam",
|
|
115
|
+
"lisbon",
|
|
116
|
+
"tofino",
|
|
117
|
+
"berlin",
|
|
118
|
+
"vanimo",
|
|
119
|
+
):
|
|
120
|
+
CMAPS_DIVERGING[_cmap_diverging.lower()] = _cmap_diverging.lower()
|
|
121
|
+
CMAPS_REMOVED = {
|
|
122
|
+
"Blue0": "0.6.0",
|
|
123
|
+
"Cool": "0.6.0",
|
|
124
|
+
"Warm": "0.6.0",
|
|
125
|
+
"Hot": "0.6.0",
|
|
126
|
+
"Floral": "0.6.0",
|
|
127
|
+
"Contrast": "0.6.0",
|
|
128
|
+
"Sharp": "0.6.0",
|
|
129
|
+
"Viz": "0.6.0",
|
|
130
|
+
}
|
|
131
|
+
CMAPS_RENAMED = {
|
|
132
|
+
"GrayCycle": ("MonoCycle", "0.6.0"),
|
|
133
|
+
"Blue1": ("Blues1", "0.7.0"),
|
|
134
|
+
"Blue2": ("Blues2", "0.7.0"),
|
|
135
|
+
"Blue3": ("Blues3", "0.7.0"),
|
|
136
|
+
"Blue4": ("Blues4", "0.7.0"),
|
|
137
|
+
"Blue5": ("Blues5", "0.7.0"),
|
|
138
|
+
"Blue6": ("Blues6", "0.7.0"),
|
|
139
|
+
"Blue7": ("Blues7", "0.7.0"),
|
|
140
|
+
"Blue8": ("Blues8", "0.7.0"),
|
|
141
|
+
"Blue9": ("Blues9", "0.7.0"),
|
|
142
|
+
"Green1": ("Greens1", "0.7.0"),
|
|
143
|
+
"Green2": ("Greens2", "0.7.0"),
|
|
144
|
+
"Green3": ("Greens3", "0.7.0"),
|
|
145
|
+
"Green4": ("Greens4", "0.7.0"),
|
|
146
|
+
"Green5": ("Greens5", "0.7.0"),
|
|
147
|
+
"Green6": ("Greens6", "0.7.0"),
|
|
148
|
+
"Green7": ("Greens7", "0.7.0"),
|
|
149
|
+
"Green8": ("Greens8", "0.7.0"),
|
|
150
|
+
"Orange1": ("Yellows1", "0.7.0"),
|
|
151
|
+
"Orange2": ("Yellows2", "0.7.0"),
|
|
152
|
+
"Orange3": ("Yellows3", "0.7.0"),
|
|
153
|
+
"Orange4": ("Oranges2", "0.7.0"),
|
|
154
|
+
"Orange5": ("Oranges1", "0.7.0"),
|
|
155
|
+
"Orange6": ("Oranges3", "0.7.0"),
|
|
156
|
+
"Orange7": ("Oranges4", "0.7.0"),
|
|
157
|
+
"Orange8": ("Yellows4", "0.7.0"),
|
|
158
|
+
"Brown1": ("Browns1", "0.7.0"),
|
|
159
|
+
"Brown2": ("Browns2", "0.7.0"),
|
|
160
|
+
"Brown3": ("Browns3", "0.7.0"),
|
|
161
|
+
"Brown4": ("Browns4", "0.7.0"),
|
|
162
|
+
"Brown5": ("Browns5", "0.7.0"),
|
|
163
|
+
"Brown6": ("Browns6", "0.7.0"),
|
|
164
|
+
"Brown7": ("Browns7", "0.7.0"),
|
|
165
|
+
"Brown8": ("Browns8", "0.7.0"),
|
|
166
|
+
"Brown9": ("Browns9", "0.7.0"),
|
|
167
|
+
"RedPurple1": ("Reds1", "0.7.0"),
|
|
168
|
+
"RedPurple2": ("Reds2", "0.7.0"),
|
|
169
|
+
"RedPurple3": ("Reds3", "0.7.0"),
|
|
170
|
+
"RedPurple4": ("Reds4", "0.7.0"),
|
|
171
|
+
"RedPurple5": ("Reds5", "0.7.0"),
|
|
172
|
+
"RedPurple6": ("Purples1", "0.7.0"),
|
|
173
|
+
"RedPurple7": ("Purples2", "0.7.0"),
|
|
174
|
+
"RedPurple8": ("Purples3", "0.7.0"),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Color constants
|
|
178
|
+
COLORS_OPEN = {} # populated during register_colors
|
|
179
|
+
COLORS_XKCD = {} # populated during register_colors
|
|
180
|
+
COLORS_KEEP = (
|
|
181
|
+
*( # always load these XKCD colors regardless of settings
|
|
182
|
+
"charcoal",
|
|
183
|
+
"tomato",
|
|
184
|
+
"burgundy",
|
|
185
|
+
"maroon",
|
|
186
|
+
"burgundy",
|
|
187
|
+
"lavendar",
|
|
188
|
+
"taupe",
|
|
189
|
+
"sand",
|
|
190
|
+
"stone",
|
|
191
|
+
"earth",
|
|
192
|
+
"sand brown",
|
|
193
|
+
"sienna",
|
|
194
|
+
"terracotta",
|
|
195
|
+
"moss",
|
|
196
|
+
"crimson",
|
|
197
|
+
"mauve",
|
|
198
|
+
"rose",
|
|
199
|
+
"teal",
|
|
200
|
+
"forest",
|
|
201
|
+
"grass",
|
|
202
|
+
"sage",
|
|
203
|
+
"pine",
|
|
204
|
+
"vermillion",
|
|
205
|
+
"russet",
|
|
206
|
+
"cerise",
|
|
207
|
+
"avocado",
|
|
208
|
+
"wine",
|
|
209
|
+
"brick",
|
|
210
|
+
"umber",
|
|
211
|
+
"mahogany",
|
|
212
|
+
"puce",
|
|
213
|
+
"grape",
|
|
214
|
+
"blurple",
|
|
215
|
+
"cranberry",
|
|
216
|
+
"sand",
|
|
217
|
+
"aqua",
|
|
218
|
+
"jade",
|
|
219
|
+
"coral",
|
|
220
|
+
"olive",
|
|
221
|
+
"magenta",
|
|
222
|
+
"turquoise",
|
|
223
|
+
"sea blue",
|
|
224
|
+
"royal blue",
|
|
225
|
+
"slate blue",
|
|
226
|
+
"slate grey",
|
|
227
|
+
"baby blue",
|
|
228
|
+
"salmon",
|
|
229
|
+
"beige",
|
|
230
|
+
"peach",
|
|
231
|
+
"mustard",
|
|
232
|
+
"lime",
|
|
233
|
+
"indigo",
|
|
234
|
+
"cornflower",
|
|
235
|
+
"marine",
|
|
236
|
+
"cloudy blue",
|
|
237
|
+
"tangerine",
|
|
238
|
+
"scarlet",
|
|
239
|
+
"navy",
|
|
240
|
+
"cool grey",
|
|
241
|
+
"warm grey",
|
|
242
|
+
"chocolate",
|
|
243
|
+
"raspberry",
|
|
244
|
+
"denim",
|
|
245
|
+
"gunmetal",
|
|
246
|
+
"midnight",
|
|
247
|
+
"chartreuse",
|
|
248
|
+
"ivory",
|
|
249
|
+
"khaki",
|
|
250
|
+
"plum",
|
|
251
|
+
"silver",
|
|
252
|
+
"tan",
|
|
253
|
+
"wheat",
|
|
254
|
+
"buff",
|
|
255
|
+
"bisque",
|
|
256
|
+
"cerulean",
|
|
257
|
+
),
|
|
258
|
+
*( # common combinations
|
|
259
|
+
"red orange",
|
|
260
|
+
"yellow orange",
|
|
261
|
+
"yellow green",
|
|
262
|
+
"blue green",
|
|
263
|
+
"blue violet",
|
|
264
|
+
"red violet",
|
|
265
|
+
"bright red", # backwards compatibility
|
|
266
|
+
),
|
|
267
|
+
*( # common names
|
|
268
|
+
prefix + color
|
|
269
|
+
for color in (
|
|
270
|
+
"red",
|
|
271
|
+
"orange",
|
|
272
|
+
"yellow",
|
|
273
|
+
"green",
|
|
274
|
+
"blue",
|
|
275
|
+
"indigo",
|
|
276
|
+
"violet",
|
|
277
|
+
"brown",
|
|
278
|
+
"grey",
|
|
279
|
+
"gray",
|
|
280
|
+
)
|
|
281
|
+
for prefix in ("", "light ", "dark ", "medium ", "pale ")
|
|
282
|
+
),
|
|
283
|
+
)
|
|
284
|
+
COLORS_REMOVE = (
|
|
285
|
+
# filter these out, let's try to be professional here...
|
|
286
|
+
"shit",
|
|
287
|
+
"poop",
|
|
288
|
+
"poo",
|
|
289
|
+
"pee",
|
|
290
|
+
"piss",
|
|
291
|
+
"puke",
|
|
292
|
+
"vomit",
|
|
293
|
+
"snot",
|
|
294
|
+
"booger",
|
|
295
|
+
"bile",
|
|
296
|
+
"diarrhea",
|
|
297
|
+
"icky",
|
|
298
|
+
"sickly",
|
|
299
|
+
)
|
|
300
|
+
COLORS_REPLACE = (
|
|
301
|
+
# prevent registering similar-sounding names
|
|
302
|
+
# these can all be combined
|
|
303
|
+
("/", " "), # convert [color1]/[color2] to compound (e.g. grey/blue to grey blue)
|
|
304
|
+
("'s", "s"), # robin's egg
|
|
305
|
+
("egg blue", "egg"), # robin's egg blue
|
|
306
|
+
("grey", "gray"), # 'Murica
|
|
307
|
+
("ochre", "ocher"), # ...
|
|
308
|
+
("forrest", "forest"), # ...
|
|
309
|
+
("ocre", "ocher"), # correct spelling
|
|
310
|
+
("kelley", "kelly"), # ...
|
|
311
|
+
("reddish", "red"), # remove [color]ish where it modifies the spelling of color
|
|
312
|
+
("purplish", "purple"), # ...
|
|
313
|
+
("pinkish", "pink"),
|
|
314
|
+
("yellowish", "yellow"),
|
|
315
|
+
("bluish", "blue"),
|
|
316
|
+
("greyish", "grey"),
|
|
317
|
+
("ish", ""), # these are all [color]ish ('ish' substring appears nowhere else)
|
|
318
|
+
("bluey", "blue"), # remove [color]y trailing y
|
|
319
|
+
("greeny", "green"), # ...
|
|
320
|
+
("reddy", "red"),
|
|
321
|
+
("pinky", "pink"),
|
|
322
|
+
("purply", "purple"),
|
|
323
|
+
("purpley", "purple"),
|
|
324
|
+
("yellowy", "yellow"),
|
|
325
|
+
("orangey", "orange"),
|
|
326
|
+
("browny", "brown"),
|
|
327
|
+
("minty", "mint"), # now remove [object]y trailing y
|
|
328
|
+
("grassy", "grass"), # ...
|
|
329
|
+
("mossy", "moss"),
|
|
330
|
+
("dusky", "dusk"),
|
|
331
|
+
("rusty", "rust"),
|
|
332
|
+
("muddy", "mud"),
|
|
333
|
+
("sandy", "sand"),
|
|
334
|
+
("leafy", "leaf"),
|
|
335
|
+
("dusty", "dust"),
|
|
336
|
+
("dirty", "dirt"),
|
|
337
|
+
("peachy", "peach"),
|
|
338
|
+
("stormy", "storm"),
|
|
339
|
+
("cloudy", "cloud"),
|
|
340
|
+
("grayblue", "gray blue"), # separate merge compounds
|
|
341
|
+
("bluegray", "gray blue"), # ...
|
|
342
|
+
("lightblue", "light blue"),
|
|
343
|
+
("yellowgreen", "yellow green"),
|
|
344
|
+
("yelloworange", "yellow orange"),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Simple snippets
|
|
348
|
+
_N_docstring = """
|
|
349
|
+
N : int, default: :rc:`image.lut`
|
|
350
|
+
Number of points in the colormap lookup table.
|
|
351
|
+
"""
|
|
352
|
+
_alpha_docstring = """
|
|
353
|
+
alpha : float, optional
|
|
354
|
+
The opacity for the entire colormap. This overrides
|
|
355
|
+
the input opacities.
|
|
356
|
+
"""
|
|
357
|
+
_cyclic_docstring = """
|
|
358
|
+
cyclic : bool, optional
|
|
359
|
+
Whether the colormap is cyclic. If ``True``, this changes how the leftmost
|
|
360
|
+
and rightmost color levels are selected, and `extend` can only be
|
|
361
|
+
``'neither'`` (a warning will be issued otherwise).
|
|
362
|
+
"""
|
|
363
|
+
_gamma_docstring = """
|
|
364
|
+
gamma : float, optional
|
|
365
|
+
Set `gamma1` and `gamma2` to this identical value.
|
|
366
|
+
gamma1 : float, optional
|
|
367
|
+
If greater than 1, make low saturation colors more prominent. If
|
|
368
|
+
less than 1, make high saturation colors more prominent. Similar to
|
|
369
|
+
the `HCLWizard <http://hclwizard.org:64230/hclwizard/>`_ option.
|
|
370
|
+
gamma2 : float, optional
|
|
371
|
+
If greater than 1, make high luminance colors more prominent. If
|
|
372
|
+
less than 1, make low luminance colors more prominent. Similar to
|
|
373
|
+
the `HCLWizard <http://hclwizard.org:64230/hclwizard/>`_ option.
|
|
374
|
+
"""
|
|
375
|
+
_space_docstring = """
|
|
376
|
+
space : {'hsl', 'hpl', 'hcl', 'hsv'}, optional
|
|
377
|
+
The hue, saturation, luminance-style colorspace to use for interpreting
|
|
378
|
+
the channels. See `this page <http://www.hsluv.org/comparison/>`__ for
|
|
379
|
+
a full description.
|
|
380
|
+
"""
|
|
381
|
+
_name_docstring = """
|
|
382
|
+
name : str, default: '_no_name'
|
|
383
|
+
The colormap name. This can also be passed as the first
|
|
384
|
+
positional string argument.
|
|
385
|
+
"""
|
|
386
|
+
_ratios_docstring = """
|
|
387
|
+
ratios : sequence of float, optional
|
|
388
|
+
Relative extents of each color transition. Must have length
|
|
389
|
+
``len(colors) - 1``. Larger numbers indicate a slower
|
|
390
|
+
transition, smaller numbers indicate a faster transition.
|
|
391
|
+
"""
|
|
392
|
+
docstring._snippet_manager["colors.N"] = _N_docstring
|
|
393
|
+
docstring._snippet_manager["colors.alpha"] = _alpha_docstring
|
|
394
|
+
docstring._snippet_manager["colors.cyclic"] = _cyclic_docstring
|
|
395
|
+
docstring._snippet_manager["colors.gamma"] = _gamma_docstring
|
|
396
|
+
docstring._snippet_manager["colors.space"] = _space_docstring
|
|
397
|
+
docstring._snippet_manager["colors.ratios"] = _ratios_docstring
|
|
398
|
+
docstring._snippet_manager["colors.name"] = _name_docstring
|
|
399
|
+
|
|
400
|
+
# List classmethod snippets
|
|
401
|
+
_from_list_docstring = """
|
|
402
|
+
colors : sequence of color-spec or tuple
|
|
403
|
+
If a sequence of RGB[A] tuples or color strings, the colormap
|
|
404
|
+
transitions evenly from ``colors[0]`` at the left-hand side
|
|
405
|
+
to ``colors[-1]`` at the right-hand side.
|
|
406
|
+
|
|
407
|
+
If a sequence of (float, color-spec) tuples, the float values are the
|
|
408
|
+
coordinate of each transition and must range from 0 to 1. This
|
|
409
|
+
can be used to divide the colormap range unevenly.
|
|
410
|
+
%(colors.name)s
|
|
411
|
+
%(colors.ratios)s
|
|
412
|
+
For example, ``('red', 'blue', 'green')`` with ``ratios=(2, 1)``
|
|
413
|
+
creates a colormap with the transition from red to blue taking
|
|
414
|
+
*twice as long* as the transition from blue to green.
|
|
415
|
+
"""
|
|
416
|
+
docstring._snippet_manager["colors.from_list"] = _from_list_docstring
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _clip_colors(colors, clip=True, gray=0.2, warn=False):
|
|
420
|
+
"""
|
|
421
|
+
Clip impossible colors rendered in an HSL-to-RGB colorspace
|
|
422
|
+
conversion. Used by `PerceptualColormap`.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
colors : sequence of 3-tuple
|
|
427
|
+
The RGB colors.
|
|
428
|
+
clip : bool, optional
|
|
429
|
+
If `clip` is ``True`` (the default), RGB channel values >1 are
|
|
430
|
+
clipped to 1. Otherwise, the color is masked out as gray.
|
|
431
|
+
gray : float, optional
|
|
432
|
+
The identical RGB channel values (gray color) to be used if
|
|
433
|
+
`clip` is ``True``.
|
|
434
|
+
warn : bool, optional
|
|
435
|
+
Whether to issue warning when colors are clipped.
|
|
436
|
+
"""
|
|
437
|
+
colors = np.asarray(colors)
|
|
438
|
+
under = colors < 0
|
|
439
|
+
over = colors > 1
|
|
440
|
+
if clip:
|
|
441
|
+
colors[under], colors[over] = 0, 1
|
|
442
|
+
else:
|
|
443
|
+
colors[under | over] = gray
|
|
444
|
+
if warn:
|
|
445
|
+
msg = "Clipped" if clip else "Invalid"
|
|
446
|
+
for i, name in enumerate("rgb"):
|
|
447
|
+
if np.any(under[:, i]) or np.any(over[:, i]):
|
|
448
|
+
warnings._warn_ultraplot(f"{msg} {name!r} channel.")
|
|
449
|
+
return colors
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _get_channel(color, channel, space="hcl"):
|
|
453
|
+
"""
|
|
454
|
+
Get the hue, saturation, or luminance channel value from the input color. The
|
|
455
|
+
color name `color` can optionally be a string with the format ``'color+x'``
|
|
456
|
+
or ``'color-x'``, where `x` is the offset from the channel value.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
color : color-spec
|
|
461
|
+
The color. Sanitized with `to_rgba`.
|
|
462
|
+
channel : optional
|
|
463
|
+
The HCL channel to be retrieved.
|
|
464
|
+
space : optional
|
|
465
|
+
The colorspace for the corresponding channel value.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
value : float
|
|
470
|
+
The channel value.
|
|
471
|
+
"""
|
|
472
|
+
# Interpret channel
|
|
473
|
+
if callable(color) or isinstance(color, Number):
|
|
474
|
+
return color
|
|
475
|
+
if channel == "hue":
|
|
476
|
+
channel = 0
|
|
477
|
+
elif channel in ("chroma", "saturation"):
|
|
478
|
+
channel = 1
|
|
479
|
+
elif channel == "luminance":
|
|
480
|
+
channel = 2
|
|
481
|
+
else:
|
|
482
|
+
raise ValueError(f"Unknown channel {channel!r}.")
|
|
483
|
+
# Interpret string or RGB tuple
|
|
484
|
+
offset = 0
|
|
485
|
+
if isinstance(color, str):
|
|
486
|
+
m = re.search("([-+][0-9.]+)$", color)
|
|
487
|
+
if m:
|
|
488
|
+
offset = float(m.group(0))
|
|
489
|
+
color = color[: m.start()]
|
|
490
|
+
return offset + to_xyz(color, space)[channel]
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _make_segment_data(values, coords=None, ratios=None):
|
|
494
|
+
"""
|
|
495
|
+
Return a segmentdata array or callable given the input colors
|
|
496
|
+
and coordinates.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
values : sequence of float
|
|
501
|
+
The channel values.
|
|
502
|
+
coords : sequence of float, optional
|
|
503
|
+
The segment coordinates.
|
|
504
|
+
ratios : sequence of float, optional
|
|
505
|
+
The relative length of each segment transition.
|
|
506
|
+
"""
|
|
507
|
+
# Allow callables
|
|
508
|
+
if callable(values):
|
|
509
|
+
return values
|
|
510
|
+
values = np.atleast_1d(values)
|
|
511
|
+
if len(values) == 1:
|
|
512
|
+
value = values[0]
|
|
513
|
+
return [(0, value, value), (1, value, value)]
|
|
514
|
+
|
|
515
|
+
# Get coordinates
|
|
516
|
+
if not np.iterable(values):
|
|
517
|
+
raise TypeError("Colors must be iterable, got {values!r}.")
|
|
518
|
+
if coords is not None:
|
|
519
|
+
coords = np.atleast_1d(coords)
|
|
520
|
+
if ratios is not None:
|
|
521
|
+
warnings._warn_ultraplot(
|
|
522
|
+
f"Segment coordinates were provided, ignoring " f"ratios={ratios!r}."
|
|
523
|
+
)
|
|
524
|
+
if len(coords) != len(values) or coords[0] != 0 or coords[-1] != 1:
|
|
525
|
+
raise ValueError(f"Coordinates must range from 0 to 1, got {coords!r}.")
|
|
526
|
+
elif ratios is not None:
|
|
527
|
+
coords = np.atleast_1d(ratios)
|
|
528
|
+
if len(coords) != len(values) - 1:
|
|
529
|
+
raise ValueError(
|
|
530
|
+
f"Need {len(values) - 1} ratios for {len(values)} colors, "
|
|
531
|
+
f"but got {len(coords)} ratios."
|
|
532
|
+
)
|
|
533
|
+
coords = np.concatenate(([0], np.cumsum(coords)))
|
|
534
|
+
coords = coords / np.max(coords) # normalize to 0-1
|
|
535
|
+
else:
|
|
536
|
+
coords = np.linspace(0, 1, len(values))
|
|
537
|
+
|
|
538
|
+
# Build segmentdata array
|
|
539
|
+
array = []
|
|
540
|
+
for c, value in zip(coords, values):
|
|
541
|
+
array.append((c, value, value))
|
|
542
|
+
return array
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _make_lookup_table(N, data, gamma=1.0, inverse=False):
|
|
546
|
+
r"""
|
|
547
|
+
Generate lookup tables of HSL values given specified gradations. Similar to
|
|
548
|
+
`~matplotlib.colors.makeMappingArray` but permits *circular* hue gradations,
|
|
549
|
+
disables clipping of out-of-bounds values, and uses fancier "gamma" scaling.
|
|
550
|
+
|
|
551
|
+
Parameters
|
|
552
|
+
----------
|
|
553
|
+
N : int
|
|
554
|
+
Number of points in the colormap lookup table.
|
|
555
|
+
data : array-like
|
|
556
|
+
Sequence of `(x, y_0, y_1)` tuples specifying channel jumps
|
|
557
|
+
(from `y_0` to `y_1`) and `x` coordinate of those jumps
|
|
558
|
+
(ranges between 0 and 1). See `~matplotlib.colors.LinearSegmentedColormap`.
|
|
559
|
+
gamma : float or sequence of float, optional
|
|
560
|
+
To obtain channel values between coordinates `x_i` and `x_{i+1}`
|
|
561
|
+
in rows `i` and `i+1` of `data` we use the formula:
|
|
562
|
+
|
|
563
|
+
.. math::
|
|
564
|
+
|
|
565
|
+
y = y_{1,i} + w_i^{\gamma_i}*(y_{0,i+1} - y_{1,i})
|
|
566
|
+
|
|
567
|
+
where `\gamma_i` corresponds to `gamma` and the weight `w_i` ranges from
|
|
568
|
+
0 to 1 between rows ``i`` and ``i+1``. If `gamma` is float, it applies
|
|
569
|
+
to every transition. Otherwise, its length must equal ``data.shape[0]-1``.
|
|
570
|
+
|
|
571
|
+
This is similar to the `matplotlib.colors.makeMappingArray` `gamma` except
|
|
572
|
+
it controls the weighting for transitions *between* each segment data
|
|
573
|
+
coordinate rather than the coordinates themselves. This makes more sense
|
|
574
|
+
for `PerceptualColormap`\ s because they usually contain just a
|
|
575
|
+
handful of transitions representing chained segments.
|
|
576
|
+
inverse : bool, optional
|
|
577
|
+
If ``True``, `w_i^{\gamma_i}` is replaced with `1 - (1 - w_i)^{\gamma_i}` --
|
|
578
|
+
that is, when `gamma` is greater than 1, this weights colors toward *higher*
|
|
579
|
+
channel values instead of lower channel values.
|
|
580
|
+
|
|
581
|
+
This is implemented in case we want to apply *equal* "gamma scaling"
|
|
582
|
+
to different HSL channels in different directions. Usually, this
|
|
583
|
+
is done to weight low data values with higher luminance *and* lower
|
|
584
|
+
saturation, thereby emphasizing "extreme" data values.
|
|
585
|
+
"""
|
|
586
|
+
# Allow for *callable* instead of linearly interpolating between segments
|
|
587
|
+
gammas = np.atleast_1d(gamma)
|
|
588
|
+
if np.any(gammas < 0.01) or np.any(gammas > 10):
|
|
589
|
+
raise ValueError("Gamma can only be in range [0.01,10].")
|
|
590
|
+
if callable(data):
|
|
591
|
+
if len(gammas) > 1:
|
|
592
|
+
raise ValueError("Only one gamma allowed for functional segmentdata.")
|
|
593
|
+
x = np.linspace(0, 1, N) ** gamma
|
|
594
|
+
lut = np.array(data(x), dtype=float)
|
|
595
|
+
return lut
|
|
596
|
+
|
|
597
|
+
# Get array
|
|
598
|
+
data = np.array(data)
|
|
599
|
+
shape = data.shape
|
|
600
|
+
if len(shape) != 2 or shape[1] != 3:
|
|
601
|
+
raise ValueError("Mapping data must have shape N x 3.")
|
|
602
|
+
if len(gammas) != 1 and len(gammas) != shape[0] - 1:
|
|
603
|
+
raise ValueError(
|
|
604
|
+
f"Expected {shape[0] - 1} gammas for {shape[0]} coords. Got {len(gamma)}."
|
|
605
|
+
) # noqa: E501
|
|
606
|
+
if len(gammas) == 1:
|
|
607
|
+
gammas = np.repeat(gammas, shape[:1])
|
|
608
|
+
|
|
609
|
+
# Get indices
|
|
610
|
+
x = data[:, 0]
|
|
611
|
+
y0 = data[:, 1]
|
|
612
|
+
y1 = data[:, 2]
|
|
613
|
+
if x[0] != 0.0 or x[-1] != 1.0:
|
|
614
|
+
raise ValueError("Data mapping points must start with x=0 and end with x=1.")
|
|
615
|
+
if np.any(np.diff(x) < 0):
|
|
616
|
+
raise ValueError("Data mapping points must have x in increasing order.")
|
|
617
|
+
x = x * (N - 1)
|
|
618
|
+
|
|
619
|
+
# Get distances from the segmentdata entry to the *left* for each requested
|
|
620
|
+
# level, excluding ends at (0, 1), which must exactly match segmentdata ends.
|
|
621
|
+
# NOTE: numpy.searchsorted returns where xq[i] must be inserted so it is
|
|
622
|
+
# larger than x[ind[i]-1] but smaller than x[ind[i]].
|
|
623
|
+
xq = (N - 1) * np.linspace(0, 1, N)
|
|
624
|
+
ind = np.searchsorted(x, xq)[1:-1]
|
|
625
|
+
offsets = (xq[1:-1] - x[ind - 1]) / (x[ind] - x[ind - 1])
|
|
626
|
+
|
|
627
|
+
# Scale distances in each segment by input gamma
|
|
628
|
+
# The ui are starting-points, the ci are counts from that point over which
|
|
629
|
+
# segment applies (i.e. where to apply the gamma), the relevant 'segment'
|
|
630
|
+
# is to the *left* of index returned by searchsorted
|
|
631
|
+
_, uind, cind = np.unique(ind, return_index=True, return_counts=True)
|
|
632
|
+
for ui, ci in zip(uind, cind): # length should be N-1
|
|
633
|
+
gamma = gammas[ind[ui] - 1] # the relevant segment is *left* of this number
|
|
634
|
+
if gamma == 1:
|
|
635
|
+
continue
|
|
636
|
+
if ci == 0: # no lookup table coordinates fall inside this segment
|
|
637
|
+
reverse = False
|
|
638
|
+
else: # reverse if we are transitioning to *lower* channel value
|
|
639
|
+
reverse = (y0[ind[ui]] - y1[ind[ui] - 1]) < 0
|
|
640
|
+
if inverse: # reverse if we are transitioning to *higher* channel value
|
|
641
|
+
reverse = not reverse
|
|
642
|
+
if reverse:
|
|
643
|
+
offsets[ui : ui + ci] = 1 - (1 - offsets[ui : ui + ci]) ** gamma
|
|
644
|
+
else:
|
|
645
|
+
offsets[ui : ui + ci] **= gamma
|
|
646
|
+
|
|
647
|
+
# Perform successive linear interpolations rolled up into one equation
|
|
648
|
+
lut = np.zeros((N,), float)
|
|
649
|
+
lut[1:-1] = y1[ind - 1] + offsets * (y0[ind] - y1[ind - 1])
|
|
650
|
+
lut[0] = y1[0]
|
|
651
|
+
lut[-1] = y0[-1]
|
|
652
|
+
return lut
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _load_colors(path, warn_on_failure=True):
|
|
656
|
+
"""
|
|
657
|
+
Read colors from the input file.
|
|
658
|
+
|
|
659
|
+
Parameters
|
|
660
|
+
----------
|
|
661
|
+
warn_on_failure : bool, optional
|
|
662
|
+
If ``True``, issue a warning when loading fails instead of raising an error.
|
|
663
|
+
"""
|
|
664
|
+
# Warn or raise error (matches Colormap._from_file behavior)
|
|
665
|
+
if not os.path.exists(path):
|
|
666
|
+
message = f"Failed to load color data file {path!r}. File not found."
|
|
667
|
+
if warn_on_failure:
|
|
668
|
+
warnings._warn_ultraplot(message)
|
|
669
|
+
else:
|
|
670
|
+
raise FileNotFoundError(message)
|
|
671
|
+
|
|
672
|
+
# Iterate through lines
|
|
673
|
+
loaded = {}
|
|
674
|
+
with open(path, "r") as fh:
|
|
675
|
+
for count, line in enumerate(fh):
|
|
676
|
+
stripped = line.strip()
|
|
677
|
+
if not stripped or stripped[0] == "#":
|
|
678
|
+
continue
|
|
679
|
+
pair = tuple(item.strip().lower() for item in line.split(":"))
|
|
680
|
+
if len(pair) != 2 or not REGEX_HEX_SINGLE.match(pair[1]):
|
|
681
|
+
warnings._warn_ultraplot(
|
|
682
|
+
f"Illegal line #{count + 1} in color file {path!r}:\n"
|
|
683
|
+
f"{line!r}\n"
|
|
684
|
+
f'Lines must be formatted as "name: hexcolor".'
|
|
685
|
+
)
|
|
686
|
+
continue
|
|
687
|
+
loaded[pair[0]] = pair[1]
|
|
688
|
+
return loaded
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _standardize_colors(input, space, margin):
|
|
692
|
+
"""
|
|
693
|
+
Standardize the input colors.
|
|
694
|
+
|
|
695
|
+
Parameters
|
|
696
|
+
----------
|
|
697
|
+
input : dict
|
|
698
|
+
The colors.
|
|
699
|
+
space : optional
|
|
700
|
+
The colorspace used to filter colors.
|
|
701
|
+
margin : optional
|
|
702
|
+
The proportional margin required for unique colors (e.g. 0.1
|
|
703
|
+
is 36 hue units, 10 saturation units, 10 luminance units).
|
|
704
|
+
"""
|
|
705
|
+
output = {}
|
|
706
|
+
colors = []
|
|
707
|
+
channels = []
|
|
708
|
+
|
|
709
|
+
# Always add these colors and ignore other colors that are too close
|
|
710
|
+
# We do this for colors with nice names or that ultraplot devs really like
|
|
711
|
+
for name in COLORS_KEEP:
|
|
712
|
+
color = input.pop(name, None)
|
|
713
|
+
if color is None:
|
|
714
|
+
continue
|
|
715
|
+
if "grey" in name:
|
|
716
|
+
name = name.replace("grey", "gray")
|
|
717
|
+
colors.append((name, color))
|
|
718
|
+
channels.append(to_xyz(color, space=space))
|
|
719
|
+
output[name] = color # required in case "kept" colors are close to each other
|
|
720
|
+
|
|
721
|
+
# Translate remaining colors and remove bad names
|
|
722
|
+
# WARNING: Unique axis argument requires numpy version >=1.13
|
|
723
|
+
for name, color in input.items():
|
|
724
|
+
for sub, rep in COLORS_REPLACE:
|
|
725
|
+
if sub in name:
|
|
726
|
+
name = name.replace(sub, rep)
|
|
727
|
+
if any(sub in name for sub in COLORS_REMOVE):
|
|
728
|
+
continue # remove "unpofessional" names
|
|
729
|
+
if name in output:
|
|
730
|
+
continue # prioritize names that come first
|
|
731
|
+
colors.append((name, color)) # category name pair
|
|
732
|
+
channels.append(to_xyz(color, space=space))
|
|
733
|
+
|
|
734
|
+
# Get locations of "perceptually distinct" colors
|
|
735
|
+
channels = np.asarray(channels)
|
|
736
|
+
if not channels.size:
|
|
737
|
+
return output
|
|
738
|
+
channels = channels / np.array([360, 100, 100])
|
|
739
|
+
channels = np.round(channels / margin).astype(np.int64)
|
|
740
|
+
_, idxs = np.unique(channels, return_index=True, axis=0)
|
|
741
|
+
|
|
742
|
+
# Return only "distinct" colors
|
|
743
|
+
for idx in idxs:
|
|
744
|
+
name, color = colors[idx]
|
|
745
|
+
output[name] = color
|
|
746
|
+
return output
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class _Colormap(object):
|
|
750
|
+
"""
|
|
751
|
+
Mixin class used to add some helper methods.
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
def _get_data(self, ext, alpha=True):
|
|
755
|
+
"""
|
|
756
|
+
Return a string containing the colormap colors for saving.
|
|
757
|
+
|
|
758
|
+
Parameters
|
|
759
|
+
----------
|
|
760
|
+
ext : {'hex', 'txt', 'rgb'}
|
|
761
|
+
The filename extension.
|
|
762
|
+
alpha : bool, optional
|
|
763
|
+
Whether to include an opacity column.
|
|
764
|
+
"""
|
|
765
|
+
# Get lookup table colors and filter out bad ones
|
|
766
|
+
if not self._isinit:
|
|
767
|
+
self._init()
|
|
768
|
+
colors = self._lut[:-3, :]
|
|
769
|
+
|
|
770
|
+
# Get data string
|
|
771
|
+
if ext == "hex":
|
|
772
|
+
data = ", ".join(mcolors.to_hex(color) for color in colors)
|
|
773
|
+
elif ext in ("txt", "rgb"):
|
|
774
|
+
rgb = mcolors.to_rgba if alpha else mcolors.to_rgb
|
|
775
|
+
data = [rgb(color) for color in colors]
|
|
776
|
+
data = "\n".join(" ".join(f"{num:0.6f}" for num in line) for line in data)
|
|
777
|
+
else:
|
|
778
|
+
raise ValueError(
|
|
779
|
+
f"Invalid extension {ext!r}. Options are: "
|
|
780
|
+
"'hex', 'txt', 'rgb', 'rgba'."
|
|
781
|
+
)
|
|
782
|
+
return data
|
|
783
|
+
|
|
784
|
+
def _make_name(self, suffix=None):
|
|
785
|
+
"""
|
|
786
|
+
Generate a default colormap name. Do not append more than one
|
|
787
|
+
leading underscore or more than one identical suffix.
|
|
788
|
+
"""
|
|
789
|
+
name = self.name
|
|
790
|
+
name = name or ""
|
|
791
|
+
if name[:1] != "_":
|
|
792
|
+
name = "_" + name
|
|
793
|
+
suffix = suffix or "copy"
|
|
794
|
+
suffix = "_" + suffix
|
|
795
|
+
if name[-len(suffix) :] != suffix:
|
|
796
|
+
name = name + suffix
|
|
797
|
+
return name
|
|
798
|
+
|
|
799
|
+
def _parse_path(self, path, ext=None, subfolder=None):
|
|
800
|
+
"""
|
|
801
|
+
Parse the user input path.
|
|
802
|
+
|
|
803
|
+
Parameters
|
|
804
|
+
----------
|
|
805
|
+
path : path-like, optional
|
|
806
|
+
The file path.
|
|
807
|
+
ext : str
|
|
808
|
+
The default extension.
|
|
809
|
+
subfolder : str, optional
|
|
810
|
+
The subfolder.
|
|
811
|
+
"""
|
|
812
|
+
# Get the folder
|
|
813
|
+
folder = rc.user_folder(subfolder=subfolder)
|
|
814
|
+
if path is not None:
|
|
815
|
+
path = os.path.expanduser(path or ".") # interpret empty string as '.'
|
|
816
|
+
if os.path.isdir(path):
|
|
817
|
+
folder, path = path, None
|
|
818
|
+
# Get the filename
|
|
819
|
+
if path is None:
|
|
820
|
+
path = os.path.join(folder, self.name)
|
|
821
|
+
if not os.path.splitext(path)[1]:
|
|
822
|
+
path = path + "." + ext # default file extension
|
|
823
|
+
return path
|
|
824
|
+
|
|
825
|
+
@staticmethod
|
|
826
|
+
def _pop_args(*args, names=None, **kwargs):
|
|
827
|
+
"""
|
|
828
|
+
Pop the name as a first positional argument or keyword argument.
|
|
829
|
+
Supports matplotlib-style ``Colormap(name, data, N)`` input
|
|
830
|
+
algongside more intuitive ``Colormap(data, name, N)`` input.
|
|
831
|
+
"""
|
|
832
|
+
names = names or ()
|
|
833
|
+
if isinstance(names, str):
|
|
834
|
+
names = (names,)
|
|
835
|
+
names = ("name", *names)
|
|
836
|
+
args, kwargs = _kwargs_to_args(names, *args, **kwargs)
|
|
837
|
+
if args[0] is not None and args[1] is None:
|
|
838
|
+
args[:2] = (None, args[0])
|
|
839
|
+
if args[0] is None:
|
|
840
|
+
args[0] = DEFAULT_NAME
|
|
841
|
+
return (*args, kwargs)
|
|
842
|
+
|
|
843
|
+
@classmethod
|
|
844
|
+
def _from_file(cls, path, warn_on_failure=False):
|
|
845
|
+
"""
|
|
846
|
+
Read generalized colormap and color cycle files.
|
|
847
|
+
"""
|
|
848
|
+
path = os.path.expanduser(path)
|
|
849
|
+
name, ext = os.path.splitext(os.path.basename(path))
|
|
850
|
+
listed = issubclass(cls, mcolors.ListedColormap)
|
|
851
|
+
reversed = name[-2:] == "_r"
|
|
852
|
+
|
|
853
|
+
# Warn if loading failed during `register_cmaps` or `register_cycles`
|
|
854
|
+
# but raise error if user tries to load a file.
|
|
855
|
+
def _warn_or_raise(descrip, error=RuntimeError):
|
|
856
|
+
prefix = f"Failed to load colormap or color cycle file {path!r}."
|
|
857
|
+
if warn_on_failure:
|
|
858
|
+
warnings._warn_ultraplot(prefix + " " + descrip)
|
|
859
|
+
else:
|
|
860
|
+
raise error(prefix + " " + descrip)
|
|
861
|
+
|
|
862
|
+
if not os.path.exists(path):
|
|
863
|
+
return _warn_or_raise("File not found.", FileNotFoundError)
|
|
864
|
+
|
|
865
|
+
# Directly read segmentdata json file
|
|
866
|
+
# NOTE: This is special case! Immediately return name and cmap
|
|
867
|
+
ext = ext[1:]
|
|
868
|
+
if ext == "json":
|
|
869
|
+
if listed:
|
|
870
|
+
return _warn_or_raise("Cannot load cycles from JSON files.")
|
|
871
|
+
try:
|
|
872
|
+
with open(path, "r") as fh:
|
|
873
|
+
data = json.load(fh)
|
|
874
|
+
except json.JSONDecodeError:
|
|
875
|
+
return _warn_or_raise("JSON decoding error.", json.JSONDecodeError)
|
|
876
|
+
kw = {}
|
|
877
|
+
for key in ("cyclic", "gamma", "gamma1", "gamma2", "space"):
|
|
878
|
+
if key in data:
|
|
879
|
+
kw[key] = data.pop(key, None)
|
|
880
|
+
if "red" in data:
|
|
881
|
+
cmap = ContinuousColormap(name, data)
|
|
882
|
+
else:
|
|
883
|
+
cmap = PerceptualColormap(name, data, **kw)
|
|
884
|
+
if reversed:
|
|
885
|
+
cmap = cmap.reversed(name[:-2])
|
|
886
|
+
return cmap
|
|
887
|
+
|
|
888
|
+
# Read .rgb and .rgba files
|
|
889
|
+
if ext in ("txt", "rgb"):
|
|
890
|
+
# Load file
|
|
891
|
+
# NOTE: This appears to be biggest import time bottleneck! Increases
|
|
892
|
+
# time from 0.05s to 0.2s, with numpy loadtxt or with this regex thing.
|
|
893
|
+
delim = re.compile(r"[,\s]+")
|
|
894
|
+
data = [
|
|
895
|
+
delim.split(line.strip())
|
|
896
|
+
for line in open(path)
|
|
897
|
+
if line.strip() and line.strip()[0] != "#"
|
|
898
|
+
]
|
|
899
|
+
try:
|
|
900
|
+
data = [[float(num) for num in line] for line in data]
|
|
901
|
+
except ValueError:
|
|
902
|
+
return _warn_or_raise(
|
|
903
|
+
"Expected a table of comma or space-separated floats."
|
|
904
|
+
)
|
|
905
|
+
# Build x-coordinates and standardize shape
|
|
906
|
+
data = np.array(data)
|
|
907
|
+
if data.shape[1] not in (3, 4):
|
|
908
|
+
return _warn_or_raise(
|
|
909
|
+
f"Expected 3 or 4 columns of floats. Got {data.shape[1]} columns."
|
|
910
|
+
)
|
|
911
|
+
if ext[0] != "x": # i.e. no x-coordinates specified explicitly
|
|
912
|
+
x = np.linspace(0, 1, data.shape[0])
|
|
913
|
+
else:
|
|
914
|
+
x, data = data[:, 0], data[:, 1:]
|
|
915
|
+
|
|
916
|
+
# Load XML files created with scivizcolor
|
|
917
|
+
# Adapted from script found here:
|
|
918
|
+
# https://sciviscolor.org/matlab-matplotlib-pv44/
|
|
919
|
+
elif ext == "xml":
|
|
920
|
+
try:
|
|
921
|
+
doc = ElementTree.parse(path)
|
|
922
|
+
except ElementTree.ParseError:
|
|
923
|
+
return _warn_or_raise("XML parsing error.", ElementTree.ParseError)
|
|
924
|
+
x, data = [], []
|
|
925
|
+
for s in doc.getroot().findall(".//Point"):
|
|
926
|
+
# Verify keys
|
|
927
|
+
if any(key not in s.attrib for key in "xrgb"):
|
|
928
|
+
return _warn_or_raise(
|
|
929
|
+
"Missing an x, r, g, or b key inside one or more <Point> tags."
|
|
930
|
+
)
|
|
931
|
+
# Get data
|
|
932
|
+
color = []
|
|
933
|
+
for key in "rgbao": # o for opacity
|
|
934
|
+
if key not in s.attrib:
|
|
935
|
+
continue
|
|
936
|
+
color.append(float(s.attrib[key]))
|
|
937
|
+
x.append(float(s.attrib["x"]))
|
|
938
|
+
data.append(color)
|
|
939
|
+
# Convert to array
|
|
940
|
+
if not all(
|
|
941
|
+
len(data[0]) == len(color) and len(color) in (3, 4) for color in data
|
|
942
|
+
):
|
|
943
|
+
return _warn_or_raise(
|
|
944
|
+
"Unexpected channel number or mixed channels across <Point> tags."
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
# Read hex strings
|
|
948
|
+
elif ext == "hex":
|
|
949
|
+
# Read arbitrary format
|
|
950
|
+
string = open(path).read() # into single string
|
|
951
|
+
data = REGEX_HEX_MULTI.findall(string)
|
|
952
|
+
if len(data) < 2:
|
|
953
|
+
return _warn_or_raise("Failed to find 6-digit or 8-digit HEX strings.")
|
|
954
|
+
# Convert to array
|
|
955
|
+
x = np.linspace(0, 1, len(data))
|
|
956
|
+
data = [to_rgb(color) for color in data]
|
|
957
|
+
|
|
958
|
+
# Invalid extension
|
|
959
|
+
else:
|
|
960
|
+
return _warn_or_raise(
|
|
961
|
+
"Unknown colormap file extension {ext!r}. Options are: "
|
|
962
|
+
+ ", ".join(map(repr, ("json", "txt", "rgb", "hex")))
|
|
963
|
+
+ "."
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
# Standardize and reverse if necessary to cmap
|
|
967
|
+
# TODO: Document the fact that filenames ending in _r return a reversed
|
|
968
|
+
# version of the colormap stored in that file.
|
|
969
|
+
x = np.array(x)
|
|
970
|
+
x = (x - x.min()) / (x.max() - x.min()) # ensure they span 0-1
|
|
971
|
+
data = np.array(data)
|
|
972
|
+
if np.any(data > 2): # from 0-255 to 0-1
|
|
973
|
+
data = data / 255
|
|
974
|
+
if reversed:
|
|
975
|
+
name = name[:-2]
|
|
976
|
+
data = data[::-1, :]
|
|
977
|
+
x = 1 - x[::-1]
|
|
978
|
+
if listed:
|
|
979
|
+
return DiscreteColormap(data, name)
|
|
980
|
+
else:
|
|
981
|
+
data = [(x, color) for x, color in zip(x, data)]
|
|
982
|
+
return ContinuousColormap.from_list(name, data)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
class ContinuousColormap(mcolors.LinearSegmentedColormap, _Colormap):
|
|
986
|
+
r"""
|
|
987
|
+
Replacement for `~matplotlib.colors.LinearSegmentedColormap`.
|
|
988
|
+
"""
|
|
989
|
+
|
|
990
|
+
def __str__(self):
|
|
991
|
+
return type(self).__name__ + f"(name={self.name!r})"
|
|
992
|
+
|
|
993
|
+
def __repr__(self):
|
|
994
|
+
string = f" 'name': {self.name!r},\n"
|
|
995
|
+
if hasattr(self, "_space"):
|
|
996
|
+
string += f" 'space': {self._space!r},\n"
|
|
997
|
+
if hasattr(self, "_cyclic"):
|
|
998
|
+
string += f" 'cyclic': {self._cyclic!r},\n"
|
|
999
|
+
for key, data in self._segmentdata.items():
|
|
1000
|
+
if callable(data):
|
|
1001
|
+
string += f" {key!r}: <function>,\n"
|
|
1002
|
+
else:
|
|
1003
|
+
stop = data[-1][1]
|
|
1004
|
+
start = data[0][2]
|
|
1005
|
+
string += f" {key!r}: [{start:.2f}, ..., {stop:.2f}],\n"
|
|
1006
|
+
return type(self).__name__ + "({\n" + string + "})"
|
|
1007
|
+
|
|
1008
|
+
@docstring._snippet_manager
|
|
1009
|
+
def __init__(self, *args, gamma=1, alpha=None, cyclic=False, **kwargs):
|
|
1010
|
+
"""
|
|
1011
|
+
Parameters
|
|
1012
|
+
----------
|
|
1013
|
+
segmentdata : dict-like
|
|
1014
|
+
Dictionary containing the keys ``'red'``, ``'green'``, ``'blue'``, and
|
|
1015
|
+
(optionally) ``'alpha'``. The shorthands ``'r'``, ``'g'``, ``'b'``,
|
|
1016
|
+
and ``'a'`` are also acceptable. The key values can be callable
|
|
1017
|
+
functions that return channel values given a colormap index, or
|
|
1018
|
+
3-column arrays indicating the coordinates and channel transitions. See
|
|
1019
|
+
`matplotlib.colors.LinearSegmentedColormap` for a detailed explanation.
|
|
1020
|
+
%(colors.name)s
|
|
1021
|
+
%(colors.N)s
|
|
1022
|
+
gamma : float, optional
|
|
1023
|
+
Gamma scaling used for the *x* coordinates.
|
|
1024
|
+
%(colors.alpha)s
|
|
1025
|
+
%(colors.cyclic)s
|
|
1026
|
+
|
|
1027
|
+
Other parameters
|
|
1028
|
+
----------------
|
|
1029
|
+
**kwargs
|
|
1030
|
+
Passed to `matplotlib.colors.LinearSegmentedColormap`.
|
|
1031
|
+
|
|
1032
|
+
See also
|
|
1033
|
+
--------
|
|
1034
|
+
DiscreteColormap
|
|
1035
|
+
matplotlib.colors.LinearSegmentedColormap
|
|
1036
|
+
ultraplot.constructor.Colormap
|
|
1037
|
+
"""
|
|
1038
|
+
# NOTE: Additional keyword args should raise matplotlib error
|
|
1039
|
+
name, segmentdata, N, kwargs = self._pop_args(
|
|
1040
|
+
*args, names=("segmentdata", "N"), **kwargs
|
|
1041
|
+
)
|
|
1042
|
+
if not isinstance(segmentdata, dict):
|
|
1043
|
+
raise ValueError(f"Invalid segmentdata {segmentdata}. Must be a dict.")
|
|
1044
|
+
N = _not_none(N, rc["image.lut"])
|
|
1045
|
+
data = _pop_props(segmentdata, "rgba", "hsla")
|
|
1046
|
+
if segmentdata:
|
|
1047
|
+
raise ValueError(f"Invalid segmentdata keys {tuple(segmentdata)}.")
|
|
1048
|
+
super().__init__(name, data, N=N, gamma=gamma, **kwargs)
|
|
1049
|
+
self._cyclic = cyclic
|
|
1050
|
+
if alpha is not None:
|
|
1051
|
+
self.set_alpha(alpha)
|
|
1052
|
+
|
|
1053
|
+
def append(self, *args, ratios=None, name=None, N=None, **kwargs):
|
|
1054
|
+
"""
|
|
1055
|
+
Return the concatenation of this colormap with the
|
|
1056
|
+
input colormaps.
|
|
1057
|
+
|
|
1058
|
+
Parameters
|
|
1059
|
+
----------
|
|
1060
|
+
*args
|
|
1061
|
+
Instances of `ContinuousColormap`.
|
|
1062
|
+
ratios : sequence of float, optional
|
|
1063
|
+
Relative extent of each component colormap in the
|
|
1064
|
+
merged colormap. Length must equal ``len(args) + 1``.
|
|
1065
|
+
For example, ``cmap1.append(cmap2, ratios=(2, 1))`` generates
|
|
1066
|
+
a colormap with the left two-thrids containing colors from
|
|
1067
|
+
``cmap1`` and the right one-third containing colors from ``cmap2``.
|
|
1068
|
+
name : str, optional
|
|
1069
|
+
The colormap name. Default is to merge each name with underscores and
|
|
1070
|
+
prepend a leading underscore, for example ``_name1_name2``.
|
|
1071
|
+
N : int, optional
|
|
1072
|
+
The number of points in the colormap lookup table. Default is
|
|
1073
|
+
to sum the length of each lookup table.
|
|
1074
|
+
|
|
1075
|
+
Other parameters
|
|
1076
|
+
----------------
|
|
1077
|
+
**kwargs
|
|
1078
|
+
Passed to `ContinuousColormap.copy`
|
|
1079
|
+
or `PerceptualColormap.copy`.
|
|
1080
|
+
|
|
1081
|
+
Returns
|
|
1082
|
+
-------
|
|
1083
|
+
ContinuousColormap
|
|
1084
|
+
The colormap.
|
|
1085
|
+
|
|
1086
|
+
See also
|
|
1087
|
+
--------
|
|
1088
|
+
DiscreteColormap.append
|
|
1089
|
+
"""
|
|
1090
|
+
# Parse input args
|
|
1091
|
+
if not args:
|
|
1092
|
+
return self
|
|
1093
|
+
if not all(isinstance(cmap, mcolors.LinearSegmentedColormap) for cmap in args):
|
|
1094
|
+
raise TypeError(f"Arguments {args!r} must be LinearSegmentedColormaps.")
|
|
1095
|
+
|
|
1096
|
+
# PerceptualColormap --> ContinuousColormap conversions
|
|
1097
|
+
cmaps = [self, *args]
|
|
1098
|
+
spaces = {getattr(cmap, "_space", None) for cmap in cmaps}
|
|
1099
|
+
to_continuous = len(spaces) > 1 # mixed colorspaces *or* mixed types
|
|
1100
|
+
if to_continuous:
|
|
1101
|
+
for i, cmap in enumerate(cmaps):
|
|
1102
|
+
if isinstance(cmap, PerceptualColormap):
|
|
1103
|
+
cmaps[i] = cmap.to_continuous()
|
|
1104
|
+
|
|
1105
|
+
# Combine the segmentdata, and use the y1/y2 slots at merge points so
|
|
1106
|
+
# we never interpolate between end colors of different colormaps
|
|
1107
|
+
segmentdata = {}
|
|
1108
|
+
if name is None:
|
|
1109
|
+
name = "_" + "_".join(cmap.name for cmap in cmaps)
|
|
1110
|
+
if not np.iterable(ratios):
|
|
1111
|
+
ratios = [1] * len(cmaps)
|
|
1112
|
+
ratios = np.asarray(ratios) / np.sum(ratios)
|
|
1113
|
+
x0 = np.append(0, np.cumsum(ratios)) # coordinates for edges
|
|
1114
|
+
xw = x0[1:] - x0[:-1] # widths between edges
|
|
1115
|
+
for key in cmaps[0]._segmentdata.keys(): # not self._segmentdata
|
|
1116
|
+
# Callable segments
|
|
1117
|
+
# WARNING: If just reference a global 'funcs' list from inside the
|
|
1118
|
+
# 'data' function it can get overwritten in this loop. Must
|
|
1119
|
+
# embed 'funcs' into the definition using a keyword argument.
|
|
1120
|
+
datas = [cmap._segmentdata[key] for cmap in cmaps]
|
|
1121
|
+
if all(map(callable, datas)): # expand range from x-to-w to 0-1
|
|
1122
|
+
|
|
1123
|
+
def xyy(ix, funcs=datas): # noqa: E306
|
|
1124
|
+
ix = np.atleast_1d(ix)
|
|
1125
|
+
kx = np.empty(ix.shape)
|
|
1126
|
+
for j, jx in enumerate(ix.flat):
|
|
1127
|
+
idx = max(np.searchsorted(x0, jx) - 1, 0)
|
|
1128
|
+
kx.flat[j] = funcs[idx]((jx - x0[idx]) / xw[idx])
|
|
1129
|
+
return kx
|
|
1130
|
+
|
|
1131
|
+
# Concatenate segment arrays and make the transition at the
|
|
1132
|
+
# seam instant so we *never interpolate* between end colors
|
|
1133
|
+
# of different maps.
|
|
1134
|
+
elif not any(map(callable, datas)):
|
|
1135
|
+
datas = []
|
|
1136
|
+
for x, w, cmap in zip(x0[:-1], xw, cmaps):
|
|
1137
|
+
xyy = np.array(cmap._segmentdata[key])
|
|
1138
|
+
xyy[:, 0] = x + w * xyy[:, 0]
|
|
1139
|
+
datas.append(xyy)
|
|
1140
|
+
for i in range(len(datas) - 1):
|
|
1141
|
+
datas[i][-1, 2] = datas[i + 1][0, 2]
|
|
1142
|
+
datas[i + 1] = datas[i + 1][1:, :]
|
|
1143
|
+
xyy = np.concatenate(datas, axis=0)
|
|
1144
|
+
xyy[:, 0] = xyy[:, 0] / xyy[:, 0].max(axis=0) # fix fp errors
|
|
1145
|
+
|
|
1146
|
+
else:
|
|
1147
|
+
raise TypeError(
|
|
1148
|
+
"Cannot merge colormaps with mixed callable "
|
|
1149
|
+
"and non-callable segment data."
|
|
1150
|
+
)
|
|
1151
|
+
segmentdata[key] = xyy
|
|
1152
|
+
|
|
1153
|
+
# Handle gamma values
|
|
1154
|
+
ikey = None
|
|
1155
|
+
if key == "saturation":
|
|
1156
|
+
ikey = "gamma1"
|
|
1157
|
+
elif key == "luminance":
|
|
1158
|
+
ikey = "gamma2"
|
|
1159
|
+
if not ikey or ikey in kwargs:
|
|
1160
|
+
continue
|
|
1161
|
+
gamma = []
|
|
1162
|
+
callable_ = all(map(callable, datas))
|
|
1163
|
+
for cmap in cmaps:
|
|
1164
|
+
igamma = getattr(cmap, "_" + ikey)
|
|
1165
|
+
if not np.iterable(igamma):
|
|
1166
|
+
if callable_:
|
|
1167
|
+
igamma = (igamma,)
|
|
1168
|
+
else:
|
|
1169
|
+
igamma = (igamma,) * (len(cmap._segmentdata[key]) - 1)
|
|
1170
|
+
gamma.extend(igamma)
|
|
1171
|
+
if callable_:
|
|
1172
|
+
if any(igamma != gamma[0] for igamma in gamma[1:]):
|
|
1173
|
+
warnings._warn_ultraplot(
|
|
1174
|
+
"Cannot use multiple segment gammas when concatenating "
|
|
1175
|
+
f"callable segments. Using the first gamma of {gamma[0]}."
|
|
1176
|
+
)
|
|
1177
|
+
gamma = gamma[0]
|
|
1178
|
+
kwargs[ikey] = gamma
|
|
1179
|
+
|
|
1180
|
+
# Return copy or merge mixed types
|
|
1181
|
+
if to_continuous and isinstance(self, PerceptualColormap):
|
|
1182
|
+
return ContinuousColormap(name, segmentdata, N, **kwargs)
|
|
1183
|
+
else:
|
|
1184
|
+
return self.copy(name, segmentdata, N, **kwargs)
|
|
1185
|
+
|
|
1186
|
+
def cut(self, cut=None, name=None, left=None, right=None, **kwargs):
|
|
1187
|
+
"""
|
|
1188
|
+
Return a version of the colormap with the center "cut out".
|
|
1189
|
+
This is great for making the transition from "negative" to "positive"
|
|
1190
|
+
in a diverging colormap more distinct.
|
|
1191
|
+
|
|
1192
|
+
Parameters
|
|
1193
|
+
----------
|
|
1194
|
+
cut : float, optional
|
|
1195
|
+
The proportion to cut from the center of the colormap. For example,
|
|
1196
|
+
``cut=0.1`` cuts the central 10%, or ``cut=-0.1`` fills the ctranl 10%
|
|
1197
|
+
of the colormap with the current central color (usually white).
|
|
1198
|
+
name : str, default: '_name_copy'
|
|
1199
|
+
The new colormap name.
|
|
1200
|
+
left, right : float, default: 0, 1
|
|
1201
|
+
The colormap indices for the "leftmost" and "rightmost"
|
|
1202
|
+
colors. See `~ContinuousColormap.truncate` for details.
|
|
1203
|
+
right : float, optional
|
|
1204
|
+
The colormap index for the new "rightmost" color. Must fall between
|
|
1205
|
+
|
|
1206
|
+
Other parameters
|
|
1207
|
+
----------------
|
|
1208
|
+
**kwargs
|
|
1209
|
+
Passed to `ContinuousColormap.copy` or `PerceptualColormap.copy`.
|
|
1210
|
+
|
|
1211
|
+
Returns
|
|
1212
|
+
-------
|
|
1213
|
+
ContinuousColormap
|
|
1214
|
+
The colormap.
|
|
1215
|
+
|
|
1216
|
+
See also
|
|
1217
|
+
--------
|
|
1218
|
+
ContinuousColormap.truncate
|
|
1219
|
+
DiscreteColormap.truncate
|
|
1220
|
+
"""
|
|
1221
|
+
# Parse input args
|
|
1222
|
+
left = max(_not_none(left, 0), 0)
|
|
1223
|
+
right = min(_not_none(right, 1), 1)
|
|
1224
|
+
cut = _not_none(cut, 0)
|
|
1225
|
+
offset = 0.5 * cut
|
|
1226
|
+
if offset < 0: # add extra 'white' later on
|
|
1227
|
+
offset = 0
|
|
1228
|
+
elif offset == 0:
|
|
1229
|
+
return self.truncate(left, right)
|
|
1230
|
+
|
|
1231
|
+
# Decompose cut into two truncations followed by concatenation
|
|
1232
|
+
if 0.5 - offset < left or 0.5 + offset > right:
|
|
1233
|
+
raise ValueError(f"Invalid cut={cut} for left={left} and right={right}.")
|
|
1234
|
+
if name is None:
|
|
1235
|
+
name = self._make_name()
|
|
1236
|
+
cmap_left = self.truncate(left, 0.5 - offset)
|
|
1237
|
+
cmap_right = self.truncate(0.5 + offset, right)
|
|
1238
|
+
|
|
1239
|
+
# Permit adding extra 'white' to colormap center
|
|
1240
|
+
# NOTE: Rely on channel abbreviations to simplify code here
|
|
1241
|
+
args = []
|
|
1242
|
+
if cut < 0:
|
|
1243
|
+
ratio = 0.5 - 0.5 * abs(cut) # ratio for flanks on either side
|
|
1244
|
+
space = getattr(self, "_space", None) or "rgb"
|
|
1245
|
+
xyza = to_xyza(self(0.5), space=space)
|
|
1246
|
+
segmentdata = {
|
|
1247
|
+
key: _make_segment_data(x) for key, x in zip(space + "a", xyza)
|
|
1248
|
+
}
|
|
1249
|
+
args.append(type(self)(DEFAULT_NAME, segmentdata, self.N))
|
|
1250
|
+
kwargs.setdefault("ratios", (ratio, abs(cut), ratio))
|
|
1251
|
+
args.append(cmap_right)
|
|
1252
|
+
|
|
1253
|
+
return cmap_left.append(*args, name=name, **kwargs)
|
|
1254
|
+
|
|
1255
|
+
def reversed(self, name=None, **kwargs):
|
|
1256
|
+
"""
|
|
1257
|
+
Return a reversed copy of the colormap.
|
|
1258
|
+
|
|
1259
|
+
Parameters
|
|
1260
|
+
----------
|
|
1261
|
+
name : str, default: '_name_r'
|
|
1262
|
+
The new colormap name.
|
|
1263
|
+
|
|
1264
|
+
Other parameters
|
|
1265
|
+
----------------
|
|
1266
|
+
**kwargs
|
|
1267
|
+
Passed to `ContinuousColormap.copy`
|
|
1268
|
+
or `PerceptualColormap.copy`.
|
|
1269
|
+
|
|
1270
|
+
See also
|
|
1271
|
+
--------
|
|
1272
|
+
matplotlib.colors.LinearSegmentedColormap.reversed
|
|
1273
|
+
"""
|
|
1274
|
+
# Reverse segments
|
|
1275
|
+
segmentdata = {
|
|
1276
|
+
key: (
|
|
1277
|
+
(lambda x, func=data: func(x))
|
|
1278
|
+
if callable(data)
|
|
1279
|
+
else [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)]
|
|
1280
|
+
)
|
|
1281
|
+
for key, data in self._segmentdata.items()
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
# Reverse gammas
|
|
1285
|
+
if name is None:
|
|
1286
|
+
name = self._make_name(suffix="r")
|
|
1287
|
+
for key in ("gamma1", "gamma2"):
|
|
1288
|
+
if key in kwargs:
|
|
1289
|
+
continue
|
|
1290
|
+
gamma = getattr(self, "_" + key, None)
|
|
1291
|
+
if gamma is not None and np.iterable(gamma):
|
|
1292
|
+
kwargs[key] = gamma[::-1]
|
|
1293
|
+
|
|
1294
|
+
cmap = self.copy(name, segmentdata, **kwargs)
|
|
1295
|
+
cmap._rgba_under, cmap._rgba_over = cmap._rgba_over, cmap._rgba_under
|
|
1296
|
+
return cmap
|
|
1297
|
+
|
|
1298
|
+
@docstring._snippet_manager
|
|
1299
|
+
def save(self, path=None, alpha=True):
|
|
1300
|
+
"""
|
|
1301
|
+
Save the colormap data to a file.
|
|
1302
|
+
|
|
1303
|
+
Parameters
|
|
1304
|
+
----------
|
|
1305
|
+
path : path-like, optional
|
|
1306
|
+
The output filename. If not provided, the colormap is saved in the
|
|
1307
|
+
``cmaps`` subfolder in `~ultraplot.config.Configurator.user_folder`
|
|
1308
|
+
under the filename ``name.json`` (where ``name`` is the colormap
|
|
1309
|
+
name). Valid extensions are shown in the below table.
|
|
1310
|
+
|
|
1311
|
+
%(rc.cmap_exts)s
|
|
1312
|
+
|
|
1313
|
+
alpha : bool, optional
|
|
1314
|
+
Whether to include an opacity column for ``.rgb``
|
|
1315
|
+
and ``.txt`` files.
|
|
1316
|
+
|
|
1317
|
+
See also
|
|
1318
|
+
--------
|
|
1319
|
+
DiscreteColormap.save
|
|
1320
|
+
"""
|
|
1321
|
+
# NOTE: We sanitize segmentdata before saving to json. Convert numpy float to
|
|
1322
|
+
# builtin float, np.array to list of lists, and callable to list of lists.
|
|
1323
|
+
# We tried encoding func.__code__ with base64 and marshal instead, but when
|
|
1324
|
+
# cmap.append() embeds functions as keyword arguments, this seems to make it
|
|
1325
|
+
# *impossible* to load back up the function with FunctionType (error message:
|
|
1326
|
+
# arg 5 (closure) must be tuple). Instead use this brute force workaround.
|
|
1327
|
+
filename = self._parse_path(path, ext="json", subfolder="cmaps")
|
|
1328
|
+
_, ext = os.path.splitext(filename)
|
|
1329
|
+
if ext[1:] != "json":
|
|
1330
|
+
# Save lookup table colors
|
|
1331
|
+
data = self._get_data(ext[1:], alpha=alpha)
|
|
1332
|
+
with open(filename, "w") as fh:
|
|
1333
|
+
fh.write(data)
|
|
1334
|
+
else:
|
|
1335
|
+
# Save segment data itself
|
|
1336
|
+
data = {}
|
|
1337
|
+
for key, value in self._segmentdata.items():
|
|
1338
|
+
if callable(value):
|
|
1339
|
+
x = np.linspace(0, 1, rc["image.lut"]) # just save the transitions
|
|
1340
|
+
y = np.array([value(_) for _ in x]).squeeze()
|
|
1341
|
+
value = np.vstack((x, y, y)).T
|
|
1342
|
+
data[key] = np.asarray(value).astype(float).tolist()
|
|
1343
|
+
keys = ()
|
|
1344
|
+
if isinstance(self, PerceptualColormap):
|
|
1345
|
+
keys = ("cyclic", "gamma1", "gamma2", "space")
|
|
1346
|
+
elif isinstance(self, ContinuousColormap):
|
|
1347
|
+
keys = ("cyclic", "gamma")
|
|
1348
|
+
for key in keys: # add all attrs to dictionary
|
|
1349
|
+
data[key] = getattr(self, "_" + key)
|
|
1350
|
+
with open(filename, "w") as fh:
|
|
1351
|
+
json.dump(data, fh, indent=4)
|
|
1352
|
+
print(f"Saved colormap to {filename!r}.")
|
|
1353
|
+
|
|
1354
|
+
def set_alpha(self, alpha, coords=None, ratios=None):
|
|
1355
|
+
"""
|
|
1356
|
+
Set the opacity for the entire colormap or set up an opacity gradation.
|
|
1357
|
+
|
|
1358
|
+
Parameters
|
|
1359
|
+
----------
|
|
1360
|
+
alpha : float or sequence of float
|
|
1361
|
+
If float, this is the opacity for the entire colormap. If sequence of
|
|
1362
|
+
float, the colormap traverses these opacity values.
|
|
1363
|
+
coords : sequence of float, optional
|
|
1364
|
+
Colormap coordinates for the opacity values. The first and last
|
|
1365
|
+
coordinates must be ``0`` and ``1``. If `alpha` is not scalar, the
|
|
1366
|
+
default coordinates are ``np.linspace(0, 1, len(alpha))``.
|
|
1367
|
+
ratios : sequence of float, optional
|
|
1368
|
+
Relative extent of each opacity transition segment. Length should
|
|
1369
|
+
equal ``len(alpha) + 1``. For example
|
|
1370
|
+
``cmap.set_alpha((1, 1, 0), ratios=(2, 1))`` creates a transtion from
|
|
1371
|
+
100 percent to 0 percent opacity in the right *third* of the colormap.
|
|
1372
|
+
|
|
1373
|
+
See also
|
|
1374
|
+
--------
|
|
1375
|
+
DiscreteColormap.set_alpha
|
|
1376
|
+
"""
|
|
1377
|
+
alpha = _make_segment_data(alpha, coords=coords, ratios=ratios)
|
|
1378
|
+
self._segmentdata["alpha"] = alpha
|
|
1379
|
+
self._isinit = False
|
|
1380
|
+
|
|
1381
|
+
def set_cyclic(self, b):
|
|
1382
|
+
"""
|
|
1383
|
+
Set whether this colormap is "cyclic". See `ContinuousColormap` for details.
|
|
1384
|
+
"""
|
|
1385
|
+
self._cyclic = bool(b)
|
|
1386
|
+
self._isinit = False
|
|
1387
|
+
|
|
1388
|
+
def shifted(self, shift=180, name=None, **kwargs):
|
|
1389
|
+
"""
|
|
1390
|
+
Return a cyclicaly shifted version of the colormap. If the colormap
|
|
1391
|
+
cyclic property is set to ``False`` a warning will be raised.
|
|
1392
|
+
|
|
1393
|
+
Parameters
|
|
1394
|
+
----------
|
|
1395
|
+
shift : float, default: 180
|
|
1396
|
+
The number of degrees to shift, out of 360 degrees.
|
|
1397
|
+
name : str, default: '_name_s'
|
|
1398
|
+
The new colormap name.
|
|
1399
|
+
|
|
1400
|
+
Other parameters
|
|
1401
|
+
----------------
|
|
1402
|
+
**kwargs
|
|
1403
|
+
Passed to `ContinuousColormap.copy` or `PerceptualColormap.copy`.
|
|
1404
|
+
|
|
1405
|
+
See also
|
|
1406
|
+
--------
|
|
1407
|
+
DiscreteColormap.shifted
|
|
1408
|
+
"""
|
|
1409
|
+
shift = shift or 0
|
|
1410
|
+
shift %= 360
|
|
1411
|
+
shift /= 360
|
|
1412
|
+
if shift == 0:
|
|
1413
|
+
return self
|
|
1414
|
+
if name is None:
|
|
1415
|
+
name = self._make_name(suffix="s")
|
|
1416
|
+
if not self._cyclic:
|
|
1417
|
+
warnings._warn_ultraplot(
|
|
1418
|
+
f"Shifting non-cyclic colormap {self.name!r}. To suppress this "
|
|
1419
|
+
"warning use cmap.set_cyclic(True) or Colormap(..., cyclic=True)."
|
|
1420
|
+
)
|
|
1421
|
+
self._cyclic = True
|
|
1422
|
+
ratios = (1 - shift, shift)
|
|
1423
|
+
cmap_left = self.truncate(shift, 1)
|
|
1424
|
+
cmap_right = self.truncate(0, shift)
|
|
1425
|
+
return cmap_left.append(cmap_right, ratios=ratios, name=name, **kwargs)
|
|
1426
|
+
|
|
1427
|
+
def truncate(self, left=None, right=None, name=None, **kwargs):
|
|
1428
|
+
"""
|
|
1429
|
+
Return a truncated version of the colormap.
|
|
1430
|
+
|
|
1431
|
+
Parameters
|
|
1432
|
+
----------
|
|
1433
|
+
left : float, default: 0
|
|
1434
|
+
The colormap index for the new "leftmost" color. Must fall between ``0``
|
|
1435
|
+
and ``1``. For example, ``left=0.1`` cuts the leftmost 10%% of the colors.
|
|
1436
|
+
right : float, default: 1
|
|
1437
|
+
The colormap index for the new "rightmost" color. Must fall between ``0``
|
|
1438
|
+
and ``1``. For example, ``right=0.9`` cuts the leftmost 10%% of the colors.
|
|
1439
|
+
name : str, default: '_name_copy'
|
|
1440
|
+
The new colormap name.
|
|
1441
|
+
|
|
1442
|
+
Other parameters
|
|
1443
|
+
----------------
|
|
1444
|
+
**kwargs
|
|
1445
|
+
Passed to `ContinuousColormap.copy`
|
|
1446
|
+
or `PerceptualColormap.copy`.
|
|
1447
|
+
|
|
1448
|
+
See also
|
|
1449
|
+
--------
|
|
1450
|
+
DiscreteColormap.truncate
|
|
1451
|
+
"""
|
|
1452
|
+
# Bail out
|
|
1453
|
+
left = max(_not_none(left, 0), 0)
|
|
1454
|
+
right = min(_not_none(right, 1), 1)
|
|
1455
|
+
if left == 0 and right == 1:
|
|
1456
|
+
return self
|
|
1457
|
+
if name is None:
|
|
1458
|
+
name = self._make_name()
|
|
1459
|
+
|
|
1460
|
+
# Resample the segmentdata arrays
|
|
1461
|
+
segmentdata = {}
|
|
1462
|
+
for key, data in self._segmentdata.items():
|
|
1463
|
+
# Callable array
|
|
1464
|
+
# WARNING: If just reference a global 'xyy' callable from inside
|
|
1465
|
+
# the lambda function it gets overwritten in the loop! Must embed
|
|
1466
|
+
# the old callable in the new one as a default keyword arg.
|
|
1467
|
+
if callable(data):
|
|
1468
|
+
|
|
1469
|
+
def xyy(x, func=data):
|
|
1470
|
+
return func(left + x * (right - left))
|
|
1471
|
+
|
|
1472
|
+
# Slice
|
|
1473
|
+
# l is the first point where x > 0 or x > left, should be >= 1
|
|
1474
|
+
# r is the last point where r < 1 or r < right
|
|
1475
|
+
else:
|
|
1476
|
+
xyy = np.asarray(data)
|
|
1477
|
+
x = xyy[:, 0]
|
|
1478
|
+
l = np.searchsorted(x, left) # first x value > left # noqa
|
|
1479
|
+
r = np.searchsorted(x, right) - 1 # last x value < right
|
|
1480
|
+
xc = xyy[l : r + 1, :].copy()
|
|
1481
|
+
xl = xyy[l - 1, 1:] + (left - x[l - 1]) * (
|
|
1482
|
+
(xyy[l, 1:] - xyy[l - 1, 1:]) / (x[l] - x[l - 1])
|
|
1483
|
+
)
|
|
1484
|
+
xr = xyy[r, 1:] + (right - x[r]) * (
|
|
1485
|
+
(xyy[r + 1, 1:] - xyy[r, 1:]) / (x[r + 1] - x[r])
|
|
1486
|
+
)
|
|
1487
|
+
xyy = np.vstack(((left, *xl), xc, (right, *xr)))
|
|
1488
|
+
xyy[:, 0] = (xyy[:, 0] - left) / (right - left)
|
|
1489
|
+
|
|
1490
|
+
# Retain the corresponding gamma *segments*
|
|
1491
|
+
segmentdata[key] = xyy
|
|
1492
|
+
if key == "saturation":
|
|
1493
|
+
ikey = "gamma1"
|
|
1494
|
+
elif key == "luminance":
|
|
1495
|
+
ikey = "gamma2"
|
|
1496
|
+
else:
|
|
1497
|
+
continue
|
|
1498
|
+
if ikey in kwargs:
|
|
1499
|
+
continue
|
|
1500
|
+
gamma = getattr(self, "_" + ikey)
|
|
1501
|
+
if np.iterable(gamma):
|
|
1502
|
+
if callable(xyy):
|
|
1503
|
+
if any(igamma != gamma[0] for igamma in gamma[1:]):
|
|
1504
|
+
warnings._warn_ultraplot(
|
|
1505
|
+
"Cannot use multiple segment gammas when "
|
|
1506
|
+
"truncating colormap. Using the first gamma "
|
|
1507
|
+
f"of {gamma[0]}."
|
|
1508
|
+
)
|
|
1509
|
+
gamma = gamma[0]
|
|
1510
|
+
else:
|
|
1511
|
+
igamma = gamma[l - 1 : r + 1]
|
|
1512
|
+
if len(igamma) == 0: # TODO: issue warning?
|
|
1513
|
+
gamma = gamma[0]
|
|
1514
|
+
else:
|
|
1515
|
+
gamma = igamma
|
|
1516
|
+
kwargs[ikey] = gamma
|
|
1517
|
+
|
|
1518
|
+
return self.copy(name, segmentdata, **kwargs)
|
|
1519
|
+
|
|
1520
|
+
def copy(
|
|
1521
|
+
self,
|
|
1522
|
+
name=None,
|
|
1523
|
+
segmentdata=None,
|
|
1524
|
+
N=None,
|
|
1525
|
+
*,
|
|
1526
|
+
alpha=None,
|
|
1527
|
+
gamma=None,
|
|
1528
|
+
cyclic=None,
|
|
1529
|
+
):
|
|
1530
|
+
"""
|
|
1531
|
+
Return a new colormap with relevant properties copied from this one
|
|
1532
|
+
if they were not provided as keyword arguments.
|
|
1533
|
+
|
|
1534
|
+
Parameters
|
|
1535
|
+
----------
|
|
1536
|
+
name : str, default: '_name_copy'
|
|
1537
|
+
The new colormap name.
|
|
1538
|
+
segmentdata, N, alpha, gamma, cyclic : optional
|
|
1539
|
+
See `ContinuousColormap`. If not provided, these are copied
|
|
1540
|
+
from the current colormap.
|
|
1541
|
+
|
|
1542
|
+
See also
|
|
1543
|
+
--------
|
|
1544
|
+
DiscreteColormap.copy
|
|
1545
|
+
PerceptualColormap.copy
|
|
1546
|
+
"""
|
|
1547
|
+
if name is None:
|
|
1548
|
+
name = self._make_name()
|
|
1549
|
+
if segmentdata is None:
|
|
1550
|
+
segmentdata = self._segmentdata.copy()
|
|
1551
|
+
if gamma is None:
|
|
1552
|
+
gamma = self._gamma
|
|
1553
|
+
if cyclic is None:
|
|
1554
|
+
cyclic = self._cyclic
|
|
1555
|
+
if N is None:
|
|
1556
|
+
N = self.N
|
|
1557
|
+
cmap = ContinuousColormap(
|
|
1558
|
+
name, segmentdata, N, alpha=alpha, gamma=gamma, cyclic=cyclic
|
|
1559
|
+
)
|
|
1560
|
+
cmap._rgba_bad = self._rgba_bad
|
|
1561
|
+
cmap._rgba_under = self._rgba_under
|
|
1562
|
+
cmap._rgba_over = self._rgba_over
|
|
1563
|
+
return cmap
|
|
1564
|
+
|
|
1565
|
+
def to_discrete(self, samples=10, name=None, **kwargs):
|
|
1566
|
+
"""
|
|
1567
|
+
Convert the `ContinuousColormap` to a `DiscreteColormap` by drawing
|
|
1568
|
+
samples from the colormap.
|
|
1569
|
+
|
|
1570
|
+
Parameters
|
|
1571
|
+
----------
|
|
1572
|
+
samples : int or sequence of float, optional
|
|
1573
|
+
If integer, draw samples at the colormap coordinates
|
|
1574
|
+
``np.linspace(0, 1, samples)``. If sequence of float,
|
|
1575
|
+
draw samples at the specified points.
|
|
1576
|
+
name : str, default: '_name_copy'
|
|
1577
|
+
The new colormap name.
|
|
1578
|
+
|
|
1579
|
+
Other parameters
|
|
1580
|
+
----------------
|
|
1581
|
+
**kwargs
|
|
1582
|
+
Passed to `DiscreteColormap`.
|
|
1583
|
+
|
|
1584
|
+
See also
|
|
1585
|
+
--------
|
|
1586
|
+
PerceptualColormap.to_continuous
|
|
1587
|
+
"""
|
|
1588
|
+
if isinstance(samples, Integral):
|
|
1589
|
+
samples = np.linspace(0, 1, samples)
|
|
1590
|
+
elif not np.iterable(samples):
|
|
1591
|
+
raise TypeError("Samples must be integer or iterable.")
|
|
1592
|
+
samples = np.asarray(samples)
|
|
1593
|
+
colors = self(samples)
|
|
1594
|
+
if name is None:
|
|
1595
|
+
name = self._make_name()
|
|
1596
|
+
return DiscreteColormap(colors, name=name, **kwargs)
|
|
1597
|
+
|
|
1598
|
+
@classmethod
|
|
1599
|
+
@docstring._snippet_manager
|
|
1600
|
+
def from_file(cls, path, *, warn_on_failure=False):
|
|
1601
|
+
"""
|
|
1602
|
+
Load colormap from a file.
|
|
1603
|
+
|
|
1604
|
+
Parameters
|
|
1605
|
+
----------
|
|
1606
|
+
path : path-like
|
|
1607
|
+
The file path. Valid file extensions are shown in the below table.
|
|
1608
|
+
|
|
1609
|
+
%(rc.cmap_exts)s
|
|
1610
|
+
|
|
1611
|
+
warn_on_failure : bool, optional
|
|
1612
|
+
If ``True``, issue a warning when loading fails instead of
|
|
1613
|
+
raising an error.
|
|
1614
|
+
|
|
1615
|
+
See also
|
|
1616
|
+
--------
|
|
1617
|
+
DiscreteColormap.from_file
|
|
1618
|
+
"""
|
|
1619
|
+
return cls._from_file(path, warn_on_failure=warn_on_failure)
|
|
1620
|
+
|
|
1621
|
+
@classmethod
|
|
1622
|
+
@docstring._snippet_manager
|
|
1623
|
+
def from_list(cls, *args, **kwargs):
|
|
1624
|
+
"""
|
|
1625
|
+
Make a `ContinuousColormap` from a sequence of colors.
|
|
1626
|
+
|
|
1627
|
+
Parameters
|
|
1628
|
+
----------
|
|
1629
|
+
%(colors.from_list)s
|
|
1630
|
+
|
|
1631
|
+
Other parameters
|
|
1632
|
+
----------------
|
|
1633
|
+
**kwargs
|
|
1634
|
+
Passed to `ContinuousColormap`.
|
|
1635
|
+
|
|
1636
|
+
Returns
|
|
1637
|
+
-------
|
|
1638
|
+
ContinuousColormap
|
|
1639
|
+
The colormap.
|
|
1640
|
+
|
|
1641
|
+
See also
|
|
1642
|
+
--------
|
|
1643
|
+
matplotlib.colors.LinearSegmentedColormap.from_list
|
|
1644
|
+
PerceptualColormap.from_list
|
|
1645
|
+
"""
|
|
1646
|
+
# Get coordinates
|
|
1647
|
+
name, colors, ratios, kwargs = cls._pop_args(
|
|
1648
|
+
*args, names=("colors", "ratios"), **kwargs
|
|
1649
|
+
)
|
|
1650
|
+
coords = None
|
|
1651
|
+
if not np.iterable(colors):
|
|
1652
|
+
raise TypeError("Colors must be iterable.")
|
|
1653
|
+
if (
|
|
1654
|
+
np.iterable(colors[0])
|
|
1655
|
+
and len(colors[0]) == 2
|
|
1656
|
+
and not isinstance(colors[0], str)
|
|
1657
|
+
):
|
|
1658
|
+
coords, colors = zip(*colors)
|
|
1659
|
+
colors = [to_rgba(color) for color in colors]
|
|
1660
|
+
|
|
1661
|
+
# Build segmentdata
|
|
1662
|
+
keys = ("red", "green", "blue", "alpha")
|
|
1663
|
+
cdict = {}
|
|
1664
|
+
for key, values in zip(keys, zip(*colors)):
|
|
1665
|
+
cdict[key] = _make_segment_data(values, coords, ratios)
|
|
1666
|
+
return cls(name, cdict, **kwargs)
|
|
1667
|
+
|
|
1668
|
+
# Deprecated
|
|
1669
|
+
to_listed = warnings._rename_objs("0.8.0", to_listed=to_discrete)
|
|
1670
|
+
concatenate, punched, truncated, updated = warnings._rename_objs(
|
|
1671
|
+
"0.6.0",
|
|
1672
|
+
concatenate=append,
|
|
1673
|
+
punched=cut,
|
|
1674
|
+
truncated=truncate,
|
|
1675
|
+
updated=copy,
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
class DiscreteColormap(mcolors.ListedColormap, _Colormap):
|
|
1680
|
+
r"""
|
|
1681
|
+
Replacement for `~matplotlib.colors.ListedColormap`.
|
|
1682
|
+
"""
|
|
1683
|
+
|
|
1684
|
+
def __str__(self):
|
|
1685
|
+
return f"DiscreteColormap(name={self.name!r})"
|
|
1686
|
+
|
|
1687
|
+
def __repr__(self):
|
|
1688
|
+
colors = [c if isinstance(c, str) else to_hex(c) for c in self.colors]
|
|
1689
|
+
string = "DiscreteColormap({\n"
|
|
1690
|
+
string += f" 'name': {self.name!r},\n"
|
|
1691
|
+
string += f" 'colors': {colors!r},\n"
|
|
1692
|
+
string += "})"
|
|
1693
|
+
return string
|
|
1694
|
+
|
|
1695
|
+
def __init__(self, colors, name=None, N=None, alpha=None, **kwargs):
|
|
1696
|
+
"""
|
|
1697
|
+
Parameters
|
|
1698
|
+
----------
|
|
1699
|
+
colors : sequence of color-spec, optional
|
|
1700
|
+
The colormap colors.
|
|
1701
|
+
name : str, default: '_no_name'
|
|
1702
|
+
The colormap name.
|
|
1703
|
+
N : int, default: ``len(colors)``
|
|
1704
|
+
The number of levels. The color list is truncated or wrapped
|
|
1705
|
+
to match this length.
|
|
1706
|
+
alpha : float, optional
|
|
1707
|
+
The opacity for the colormap colors. This overrides the
|
|
1708
|
+
input color opacities.
|
|
1709
|
+
|
|
1710
|
+
Other parameters
|
|
1711
|
+
----------------
|
|
1712
|
+
**kwargs
|
|
1713
|
+
Passed to `~matplotlib.colors.ListedColormap`.
|
|
1714
|
+
|
|
1715
|
+
See also
|
|
1716
|
+
--------
|
|
1717
|
+
ContinuousColormap
|
|
1718
|
+
matplotlib.colors.ListedColormap
|
|
1719
|
+
ultraplot.constructor.Colormap
|
|
1720
|
+
"""
|
|
1721
|
+
# NOTE: This also improves 'monochrome' detection to test all items
|
|
1722
|
+
# in the list. Otherwise ContourSet does not apply negative_linestyle
|
|
1723
|
+
# to monochromatic colormaps generated by passing a 'colors' keyword.
|
|
1724
|
+
# Also note that under the hood, just like ultraplot, ContourSet builds
|
|
1725
|
+
# identical monochromatic ListedColormaps when it receives scalar colors.
|
|
1726
|
+
N = _not_none(N, len(colors))
|
|
1727
|
+
name = _not_none(name, DEFAULT_NAME)
|
|
1728
|
+
super().__init__(colors, name=name, N=N, **kwargs)
|
|
1729
|
+
if alpha is not None:
|
|
1730
|
+
self.set_alpha(alpha)
|
|
1731
|
+
for i, color in enumerate(self.colors):
|
|
1732
|
+
if isinstance(color, np.ndarray):
|
|
1733
|
+
self.colors[i] = color.tolist()
|
|
1734
|
+
if self.colors and all(self.colors[0] == color for color in self.colors):
|
|
1735
|
+
self.monochrome = True # for contour negative dash style
|
|
1736
|
+
|
|
1737
|
+
def append(self, *args, name=None, N=None, **kwargs):
|
|
1738
|
+
"""
|
|
1739
|
+
Append arbitrary colormaps onto this colormap.
|
|
1740
|
+
|
|
1741
|
+
Parameters
|
|
1742
|
+
----------
|
|
1743
|
+
*args
|
|
1744
|
+
Instances of `DiscreteColormap`.
|
|
1745
|
+
name : str, optional
|
|
1746
|
+
The new colormap name. Default is to merge each name with underscores and
|
|
1747
|
+
prepend a leading underscore, for example ``_name1_name2``.
|
|
1748
|
+
N : int, optional
|
|
1749
|
+
The number of points in the colormap lookup table. Default is
|
|
1750
|
+
the number of colors in the concatenated lists.
|
|
1751
|
+
|
|
1752
|
+
Other parameters
|
|
1753
|
+
----------------
|
|
1754
|
+
**kwargs
|
|
1755
|
+
Passed to `~DiscreteColormap.copy`.
|
|
1756
|
+
|
|
1757
|
+
See also
|
|
1758
|
+
--------
|
|
1759
|
+
ContinuousColormap.append
|
|
1760
|
+
"""
|
|
1761
|
+
if not args:
|
|
1762
|
+
return self
|
|
1763
|
+
if not all(isinstance(cmap, mcolors.ListedColormap) for cmap in args):
|
|
1764
|
+
raise TypeError(f"Arguments {args!r} must be DiscreteColormap.")
|
|
1765
|
+
cmaps = (self, *args)
|
|
1766
|
+
if name is None:
|
|
1767
|
+
name = "_" + "_".join(cmap.name for cmap in cmaps)
|
|
1768
|
+
colors = [color for cmap in cmaps for color in cmap.colors]
|
|
1769
|
+
N = _not_none(N, len(colors))
|
|
1770
|
+
return self.copy(colors, name, N, **kwargs)
|
|
1771
|
+
|
|
1772
|
+
@docstring._snippet_manager
|
|
1773
|
+
def save(self, path=None, alpha=True):
|
|
1774
|
+
"""
|
|
1775
|
+
Save the colormap data to a file.
|
|
1776
|
+
|
|
1777
|
+
Parameters
|
|
1778
|
+
----------
|
|
1779
|
+
path : path-like, optional
|
|
1780
|
+
The output filename. If not provided, the colormap is saved in the
|
|
1781
|
+
``cycles`` subfolder in `~ultraplot.config.Configurator.user_folder`
|
|
1782
|
+
under the filename ``name.hex`` (where ``name`` is the color cycle
|
|
1783
|
+
name). Valid extensions are described in the below table.
|
|
1784
|
+
|
|
1785
|
+
%(rc.cycle_exts)s
|
|
1786
|
+
|
|
1787
|
+
alpha : bool, optional
|
|
1788
|
+
Whether to include an opacity column for ``.rgb``
|
|
1789
|
+
and ``.txt`` files.
|
|
1790
|
+
|
|
1791
|
+
See also
|
|
1792
|
+
--------
|
|
1793
|
+
ContinuousColormap.save
|
|
1794
|
+
"""
|
|
1795
|
+
filename = self._parse_path(path, ext="hex", subfolder="cycles")
|
|
1796
|
+
_, ext = os.path.splitext(filename)
|
|
1797
|
+
data = self._get_data(ext[1:], alpha=alpha)
|
|
1798
|
+
with open(filename, "w") as fh:
|
|
1799
|
+
fh.write(data)
|
|
1800
|
+
print(f"Saved colormap to {filename!r}.")
|
|
1801
|
+
|
|
1802
|
+
def set_alpha(self, alpha):
|
|
1803
|
+
"""
|
|
1804
|
+
Set the opacity for the entire colormap.
|
|
1805
|
+
|
|
1806
|
+
Parameters
|
|
1807
|
+
----------
|
|
1808
|
+
alpha : float
|
|
1809
|
+
The opacity.
|
|
1810
|
+
|
|
1811
|
+
See also
|
|
1812
|
+
--------
|
|
1813
|
+
ContinuousColormap.set_alpha
|
|
1814
|
+
"""
|
|
1815
|
+
self.colors = [set_alpha(color, alpha) for color in self.colors]
|
|
1816
|
+
self._init()
|
|
1817
|
+
|
|
1818
|
+
def reversed(self, name=None, **kwargs):
|
|
1819
|
+
"""
|
|
1820
|
+
Return a reversed version of the colormap.
|
|
1821
|
+
|
|
1822
|
+
Parameters
|
|
1823
|
+
----------
|
|
1824
|
+
name : str, default: '_name_r'
|
|
1825
|
+
The new colormap name.
|
|
1826
|
+
|
|
1827
|
+
Other parameters
|
|
1828
|
+
----------------
|
|
1829
|
+
**kwargs
|
|
1830
|
+
Passed to `DiscreteColormap.copy`
|
|
1831
|
+
|
|
1832
|
+
See also
|
|
1833
|
+
--------
|
|
1834
|
+
matplotlib.colors.ListedColormap.reversed
|
|
1835
|
+
"""
|
|
1836
|
+
if name is None:
|
|
1837
|
+
name = self._make_name(suffix="r")
|
|
1838
|
+
colors = self.colors[::-1]
|
|
1839
|
+
cmap = self.copy(colors, name, **kwargs)
|
|
1840
|
+
cmap._rgba_under, cmap._rgba_over = cmap._rgba_over, cmap._rgba_under
|
|
1841
|
+
return cmap
|
|
1842
|
+
|
|
1843
|
+
def shifted(self, shift=1, name=None):
|
|
1844
|
+
"""
|
|
1845
|
+
Return a cyclically shifted version of the colormap.
|
|
1846
|
+
|
|
1847
|
+
Parameters
|
|
1848
|
+
----------
|
|
1849
|
+
shift : float, default: 1
|
|
1850
|
+
The number of list indices to shift.
|
|
1851
|
+
name : str, eefault: '_name_s'
|
|
1852
|
+
The new colormap name.
|
|
1853
|
+
|
|
1854
|
+
See also
|
|
1855
|
+
--------
|
|
1856
|
+
ContinuousColormap.shifted
|
|
1857
|
+
"""
|
|
1858
|
+
if not shift:
|
|
1859
|
+
return self
|
|
1860
|
+
if name is None:
|
|
1861
|
+
name = self._make_name(suffix="s")
|
|
1862
|
+
shift = shift % len(self.colors)
|
|
1863
|
+
colors = list(self.colors)
|
|
1864
|
+
colors = colors[shift:] + colors[:shift]
|
|
1865
|
+
return self.copy(colors, name, len(colors))
|
|
1866
|
+
|
|
1867
|
+
def truncate(self, left=None, right=None, name=None):
|
|
1868
|
+
"""
|
|
1869
|
+
Return a truncated version of the colormap.
|
|
1870
|
+
|
|
1871
|
+
Parameters
|
|
1872
|
+
----------
|
|
1873
|
+
left : float, default: None
|
|
1874
|
+
The colormap index for the new "leftmost" color. Must fall between ``0``
|
|
1875
|
+
and ``self.N``. For example, ``left=2`` drops the first two colors.
|
|
1876
|
+
right : float, default: None
|
|
1877
|
+
The colormap index for the new "rightmost" color. Must fall between ``0``
|
|
1878
|
+
and ``self.N``. For example, ``right=4`` keeps the first four colors.
|
|
1879
|
+
name : str, default: '_name_copy'
|
|
1880
|
+
The new colormap name.
|
|
1881
|
+
|
|
1882
|
+
See also
|
|
1883
|
+
--------
|
|
1884
|
+
ContinuousColormap.truncate
|
|
1885
|
+
"""
|
|
1886
|
+
if left is None and right is None:
|
|
1887
|
+
return self
|
|
1888
|
+
if name is None:
|
|
1889
|
+
name = self._make_name()
|
|
1890
|
+
colors = self.colors[left:right]
|
|
1891
|
+
return self.copy(colors, name, len(colors))
|
|
1892
|
+
|
|
1893
|
+
def copy(self, colors=None, name=None, N=None, *, alpha=None):
|
|
1894
|
+
"""
|
|
1895
|
+
Return a new colormap with relevant properties copied from this one
|
|
1896
|
+
if they were not provided as keyword arguments.
|
|
1897
|
+
|
|
1898
|
+
Parameters
|
|
1899
|
+
----------
|
|
1900
|
+
name : str, default: '_name_copy'
|
|
1901
|
+
The new colormap name.
|
|
1902
|
+
colors, N, alpha : optional
|
|
1903
|
+
See `DiscreteColormap`. If not provided,
|
|
1904
|
+
these are copied from the current colormap.
|
|
1905
|
+
|
|
1906
|
+
See also
|
|
1907
|
+
--------
|
|
1908
|
+
ContinuousColormap.copy
|
|
1909
|
+
PerceptualColormap.copy
|
|
1910
|
+
"""
|
|
1911
|
+
if name is None:
|
|
1912
|
+
name = self._make_name()
|
|
1913
|
+
if colors is None:
|
|
1914
|
+
colors = list(self.colors) # copy
|
|
1915
|
+
if N is None:
|
|
1916
|
+
N = self.N
|
|
1917
|
+
cmap = DiscreteColormap(colors, name, N=N, alpha=alpha)
|
|
1918
|
+
cmap._rgba_bad = self._rgba_bad
|
|
1919
|
+
cmap._rgba_under = self._rgba_under
|
|
1920
|
+
cmap._rgba_over = self._rgba_over
|
|
1921
|
+
return cmap
|
|
1922
|
+
|
|
1923
|
+
@classmethod
|
|
1924
|
+
@docstring._snippet_manager
|
|
1925
|
+
def from_file(cls, path, *, warn_on_failure=False):
|
|
1926
|
+
"""
|
|
1927
|
+
Load color cycle from a file.
|
|
1928
|
+
|
|
1929
|
+
Parameters
|
|
1930
|
+
----------
|
|
1931
|
+
path : path-like
|
|
1932
|
+
The file path. Valid file extensions are shown in the below table.
|
|
1933
|
+
|
|
1934
|
+
%(rc.cycle_exts)s
|
|
1935
|
+
|
|
1936
|
+
warn_on_failure : bool, optional
|
|
1937
|
+
If ``True``, issue a warning when loading fails instead of
|
|
1938
|
+
raising an error.
|
|
1939
|
+
|
|
1940
|
+
See also
|
|
1941
|
+
--------
|
|
1942
|
+
ContinuousColormap.from_file
|
|
1943
|
+
"""
|
|
1944
|
+
return cls._from_file(path, warn_on_failure=warn_on_failure)
|
|
1945
|
+
|
|
1946
|
+
# Rename methods
|
|
1947
|
+
concatenate, truncated, updated = warnings._rename_objs(
|
|
1948
|
+
"0.6.0",
|
|
1949
|
+
concatenate=append,
|
|
1950
|
+
truncated=truncate,
|
|
1951
|
+
updated=copy,
|
|
1952
|
+
)
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
class PerceptualColormap(ContinuousColormap):
|
|
1956
|
+
"""
|
|
1957
|
+
A `ContinuousColormap` with linear transitions across hue, saturation,
|
|
1958
|
+
and luminance rather than red, blue, and green.
|
|
1959
|
+
"""
|
|
1960
|
+
|
|
1961
|
+
@docstring._snippet_manager
|
|
1962
|
+
def __init__(
|
|
1963
|
+
self,
|
|
1964
|
+
*args,
|
|
1965
|
+
space=None,
|
|
1966
|
+
clip=True,
|
|
1967
|
+
gamma=None,
|
|
1968
|
+
gamma1=None,
|
|
1969
|
+
gamma2=None,
|
|
1970
|
+
**kwargs,
|
|
1971
|
+
):
|
|
1972
|
+
"""
|
|
1973
|
+
Parameters
|
|
1974
|
+
----------
|
|
1975
|
+
segmentdata : dict-like
|
|
1976
|
+
Dictionary containing the keys ``'hue'``, ``'saturation'``,
|
|
1977
|
+
``'luminance'``, and (optionally) ``'alpha'``. The key ``'chroma'`` is
|
|
1978
|
+
treated as a synonym for ``'saturation'``. The shorthands ``'h'``,
|
|
1979
|
+
``'s'``, ``'l'``, ``'a'``, and ``'c'`` are also acceptable. The key
|
|
1980
|
+
values can be callable functions that return channel values given a
|
|
1981
|
+
colormap index, or 3-column arrays indicating the coordinates and
|
|
1982
|
+
channel transitions. See `~matplotlib.colors.LinearSegmentedColormap`
|
|
1983
|
+
for a more detailed explanation.
|
|
1984
|
+
%(colors.name)s
|
|
1985
|
+
%(colors.N)s
|
|
1986
|
+
%(colors.space)s
|
|
1987
|
+
clip : bool, optional
|
|
1988
|
+
Whether to "clip" impossible colors (i.e. truncate HCL colors with
|
|
1989
|
+
RGB channels with values greater than 1) or mask them out as gray.
|
|
1990
|
+
%(colors.gamma)s
|
|
1991
|
+
%(colors.alpha)s
|
|
1992
|
+
%(colors.cyclic)s
|
|
1993
|
+
|
|
1994
|
+
Other parameters
|
|
1995
|
+
----------------
|
|
1996
|
+
**kwargs
|
|
1997
|
+
Passed to `matploitlib.colors.LinearSegmentedColormap`.
|
|
1998
|
+
|
|
1999
|
+
Example
|
|
2000
|
+
-------
|
|
2001
|
+
The below example generates a `PerceptualColormap` from a
|
|
2002
|
+
`segmentdata` dictionary that uses color names for the hue data,
|
|
2003
|
+
instead of channel values between ``0`` and ``360``.
|
|
2004
|
+
|
|
2005
|
+
>>> import ultraplot as pplt
|
|
2006
|
+
>>> data = {
|
|
2007
|
+
>>> 'h': [[0, 'red', 'red'], [1, 'blue', 'blue']],
|
|
2008
|
+
>>> 's': [[0, 100, 100], [1, 100, 100]],
|
|
2009
|
+
>>> 'l': [[0, 100, 100], [1, 20, 20]],
|
|
2010
|
+
>>> }
|
|
2011
|
+
>>> cmap = pplt.PerceptualColormap(data)
|
|
2012
|
+
|
|
2013
|
+
See also
|
|
2014
|
+
--------
|
|
2015
|
+
ContinuousColormap
|
|
2016
|
+
ultraplot.constructor.Colormap
|
|
2017
|
+
"""
|
|
2018
|
+
# Checks
|
|
2019
|
+
name, segmentdata, N, kwargs = self._pop_args(
|
|
2020
|
+
*args, names=("segmentdata", "N"), **kwargs
|
|
2021
|
+
)
|
|
2022
|
+
data = _pop_props(segmentdata, "hsla")
|
|
2023
|
+
if segmentdata:
|
|
2024
|
+
raise ValueError(f"Invalid segmentdata keys {tuple(segmentdata)}.")
|
|
2025
|
+
space = _not_none(space, DEFAULT_SPACE).lower()
|
|
2026
|
+
if space not in ("rgb", "hsv", "hpl", "hsl", "hcl"):
|
|
2027
|
+
raise ValueError(f"Unknown colorspace {space!r}.")
|
|
2028
|
+
# Convert color strings to channel values
|
|
2029
|
+
for key, array in data.items():
|
|
2030
|
+
if callable(array): # permit callable
|
|
2031
|
+
continue
|
|
2032
|
+
for i, xyy in enumerate(array):
|
|
2033
|
+
xyy = list(xyy) # make copy!
|
|
2034
|
+
for j, y in enumerate(xyy[1:]): # modify the y values
|
|
2035
|
+
xyy[j + 1] = _get_channel(y, key, space)
|
|
2036
|
+
data[key][i] = xyy
|
|
2037
|
+
# Initialize
|
|
2038
|
+
super().__init__(name, data, gamma=1.0, N=N, **kwargs)
|
|
2039
|
+
self._gamma1 = _not_none(gamma1, gamma, 1.0)
|
|
2040
|
+
self._gamma2 = _not_none(gamma2, gamma, 1.0)
|
|
2041
|
+
self._space = space
|
|
2042
|
+
self._clip = clip
|
|
2043
|
+
|
|
2044
|
+
def _init(self):
|
|
2045
|
+
"""
|
|
2046
|
+
As with `~matplotlib.colors.LinearSegmentedColormap`, but convert
|
|
2047
|
+
each value in the lookup table from ``self._space`` to RGB.
|
|
2048
|
+
"""
|
|
2049
|
+
# First generate the lookup table
|
|
2050
|
+
channels = ("hue", "saturation", "luminance")
|
|
2051
|
+
inverses = (False, False, True) # weight low chroma, high luminance
|
|
2052
|
+
gammas = (1.0, self._gamma1, self._gamma2)
|
|
2053
|
+
self._lut_hsl = np.ones((self.N + 3, 4), float) # fill
|
|
2054
|
+
for i, (channel, gamma, inverse) in enumerate(zip(channels, gammas, inverses)):
|
|
2055
|
+
self._lut_hsl[:-3, i] = _make_lookup_table(
|
|
2056
|
+
self.N, self._segmentdata[channel], gamma, inverse
|
|
2057
|
+
)
|
|
2058
|
+
if "alpha" in self._segmentdata:
|
|
2059
|
+
self._lut_hsl[:-3, 3] = _make_lookup_table(
|
|
2060
|
+
self.N, self._segmentdata["alpha"]
|
|
2061
|
+
)
|
|
2062
|
+
self._lut_hsl[:-3, 0] %= 360
|
|
2063
|
+
|
|
2064
|
+
# Make hues circular, set extremes i.e. copy HSL values
|
|
2065
|
+
self._lut = self._lut_hsl.copy()
|
|
2066
|
+
self._set_extremes() # generally just used end values in segmentdata
|
|
2067
|
+
self._isinit = True
|
|
2068
|
+
|
|
2069
|
+
# Now convert values to RGB and clip colors
|
|
2070
|
+
for i in range(self.N + 3):
|
|
2071
|
+
self._lut[i, :3] = to_rgb(self._lut[i, :3], self._space)
|
|
2072
|
+
self._lut[:, :3] = _clip_colors(self._lut[:, :3], self._clip)
|
|
2073
|
+
|
|
2074
|
+
@docstring._snippet_manager
|
|
2075
|
+
def set_gamma(self, gamma=None, gamma1=None, gamma2=None):
|
|
2076
|
+
"""
|
|
2077
|
+
Set the gamma value(s) for the luminance and saturation transitions.
|
|
2078
|
+
|
|
2079
|
+
Parameters
|
|
2080
|
+
----------
|
|
2081
|
+
%(colors.gamma)s
|
|
2082
|
+
"""
|
|
2083
|
+
gamma1 = _not_none(gamma1, gamma)
|
|
2084
|
+
gamma2 = _not_none(gamma2, gamma)
|
|
2085
|
+
if gamma1 is not None:
|
|
2086
|
+
self._gamma1 = gamma1
|
|
2087
|
+
if gamma2 is not None:
|
|
2088
|
+
self._gamma2 = gamma2
|
|
2089
|
+
self._init()
|
|
2090
|
+
|
|
2091
|
+
def copy(
|
|
2092
|
+
self,
|
|
2093
|
+
name=None,
|
|
2094
|
+
segmentdata=None,
|
|
2095
|
+
N=None,
|
|
2096
|
+
*,
|
|
2097
|
+
alpha=None,
|
|
2098
|
+
gamma=None,
|
|
2099
|
+
cyclic=None,
|
|
2100
|
+
clip=None,
|
|
2101
|
+
gamma1=None,
|
|
2102
|
+
gamma2=None,
|
|
2103
|
+
space=None,
|
|
2104
|
+
):
|
|
2105
|
+
"""
|
|
2106
|
+
Return a new colormap with relevant properties copied from this one
|
|
2107
|
+
if they were not provided as keyword arguments.
|
|
2108
|
+
|
|
2109
|
+
Parameters
|
|
2110
|
+
----------
|
|
2111
|
+
name : str, default: '_name_copy'
|
|
2112
|
+
The new colormap name.
|
|
2113
|
+
segmentdata, N, alpha, clip, cyclic, gamma, gamma1, gamma2, space : optional
|
|
2114
|
+
See `PerceptualColormap`. If not provided,
|
|
2115
|
+
these are copied from the current colormap.
|
|
2116
|
+
|
|
2117
|
+
See also
|
|
2118
|
+
--------
|
|
2119
|
+
DiscreteColormap.copy
|
|
2120
|
+
ContinuousColormap.copy
|
|
2121
|
+
"""
|
|
2122
|
+
if name is None:
|
|
2123
|
+
name = self._make_name()
|
|
2124
|
+
if segmentdata is None:
|
|
2125
|
+
segmentdata = self._segmentdata.copy()
|
|
2126
|
+
if space is None:
|
|
2127
|
+
space = self._space
|
|
2128
|
+
if clip is None:
|
|
2129
|
+
clip = self._clip
|
|
2130
|
+
if gamma is not None:
|
|
2131
|
+
gamma1 = gamma2 = gamma
|
|
2132
|
+
if gamma1 is None:
|
|
2133
|
+
gamma1 = self._gamma1
|
|
2134
|
+
if gamma2 is None:
|
|
2135
|
+
gamma2 = self._gamma2
|
|
2136
|
+
if cyclic is None:
|
|
2137
|
+
cyclic = self._cyclic
|
|
2138
|
+
if N is None:
|
|
2139
|
+
N = self.N
|
|
2140
|
+
cmap = PerceptualColormap(
|
|
2141
|
+
name,
|
|
2142
|
+
segmentdata,
|
|
2143
|
+
N,
|
|
2144
|
+
alpha=alpha,
|
|
2145
|
+
clip=clip,
|
|
2146
|
+
cyclic=cyclic,
|
|
2147
|
+
gamma1=gamma1,
|
|
2148
|
+
gamma2=gamma2,
|
|
2149
|
+
space=space,
|
|
2150
|
+
)
|
|
2151
|
+
cmap._rgba_bad = self._rgba_bad
|
|
2152
|
+
cmap._rgba_under = self._rgba_under
|
|
2153
|
+
cmap._rgba_over = self._rgba_over
|
|
2154
|
+
return cmap
|
|
2155
|
+
|
|
2156
|
+
def to_continuous(self, name=None, **kwargs):
|
|
2157
|
+
"""
|
|
2158
|
+
Convert the `PerceptualColormap` to a standard `ContinuousColormap`.
|
|
2159
|
+
This is used to merge such colormaps.
|
|
2160
|
+
|
|
2161
|
+
Parameters
|
|
2162
|
+
----------
|
|
2163
|
+
name : str, default: '_name_copy'
|
|
2164
|
+
The new colormap name.
|
|
2165
|
+
|
|
2166
|
+
Other parameters
|
|
2167
|
+
----------------
|
|
2168
|
+
**kwargs
|
|
2169
|
+
Passed to `ContinuousColormap`.
|
|
2170
|
+
|
|
2171
|
+
See also
|
|
2172
|
+
--------
|
|
2173
|
+
ContinuousColormap.to_discrete
|
|
2174
|
+
"""
|
|
2175
|
+
if not self._isinit:
|
|
2176
|
+
self._init()
|
|
2177
|
+
if name is None:
|
|
2178
|
+
name = self._make_name()
|
|
2179
|
+
return ContinuousColormap.from_list(name, self._lut[:-3, :], **kwargs)
|
|
2180
|
+
|
|
2181
|
+
@classmethod
|
|
2182
|
+
@docstring._snippet_manager
|
|
2183
|
+
@warnings._rename_kwargs("0.7.0", fade="saturation", shade="luminance")
|
|
2184
|
+
def from_color(cls, *args, **kwargs):
|
|
2185
|
+
"""
|
|
2186
|
+
Return a simple monochromatic "sequential" colormap that blends from white
|
|
2187
|
+
or near-white to the input color.
|
|
2188
|
+
|
|
2189
|
+
Parameters
|
|
2190
|
+
----------
|
|
2191
|
+
color : color-spec
|
|
2192
|
+
RGB tuple, hex string, or named color string.
|
|
2193
|
+
%(colors.name)s
|
|
2194
|
+
%(colors.space)s
|
|
2195
|
+
l, s, a, c
|
|
2196
|
+
Shorthands for `luminance`, `saturation`, `alpha`, and `chroma`.
|
|
2197
|
+
luminance : float or color-spec, default: 100
|
|
2198
|
+
If float, this is the luminance channel strength on the left-hand
|
|
2199
|
+
side of the colormap. If RGB[A] tuple, hex string, or named color
|
|
2200
|
+
string, the luminance is inferred from the color.
|
|
2201
|
+
saturation, alpha : float or color-spec, optional
|
|
2202
|
+
As with `luminance`, except the default `saturation` and the default
|
|
2203
|
+
`alpha` are the channel values taken from `color`.
|
|
2204
|
+
chroma
|
|
2205
|
+
Alias for `saturation`.
|
|
2206
|
+
|
|
2207
|
+
Other parameters
|
|
2208
|
+
----------------
|
|
2209
|
+
**kwargs
|
|
2210
|
+
Passed to `PerceptualColormap.from_hsl`.
|
|
2211
|
+
|
|
2212
|
+
Returns
|
|
2213
|
+
-------
|
|
2214
|
+
PerceptualColormap
|
|
2215
|
+
The colormap.
|
|
2216
|
+
|
|
2217
|
+
See also
|
|
2218
|
+
--------
|
|
2219
|
+
PerceptualColormap.from_hsl
|
|
2220
|
+
PerceptualColormap.from_list
|
|
2221
|
+
"""
|
|
2222
|
+
name, color, space, kwargs = cls._pop_args(
|
|
2223
|
+
*args, names=("color", "space"), **kwargs
|
|
2224
|
+
)
|
|
2225
|
+
space = _not_none(space, DEFAULT_SPACE).lower()
|
|
2226
|
+
props = _pop_props(kwargs, "hsla")
|
|
2227
|
+
if props.get("hue", None) is not None:
|
|
2228
|
+
raise TypeError("from_color() got an unexpected keyword argument 'hue'")
|
|
2229
|
+
hue, saturation, luminance, alpha = to_xyza(color, space)
|
|
2230
|
+
alpha_fade = props.pop("alpha", 1)
|
|
2231
|
+
luminance_fade = props.pop("luminance", 100)
|
|
2232
|
+
saturation_fade = props.pop("saturation", saturation)
|
|
2233
|
+
return cls.from_hsl(
|
|
2234
|
+
name,
|
|
2235
|
+
hue=hue,
|
|
2236
|
+
space=space,
|
|
2237
|
+
alpha=(alpha_fade, alpha),
|
|
2238
|
+
saturation=(saturation_fade, saturation),
|
|
2239
|
+
luminance=(luminance_fade, luminance),
|
|
2240
|
+
**kwargs,
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
@classmethod
|
|
2244
|
+
@docstring._snippet_manager
|
|
2245
|
+
def from_hsl(cls, *args, **kwargs):
|
|
2246
|
+
"""
|
|
2247
|
+
Make a `~PerceptualColormap` by specifying the hue,
|
|
2248
|
+
saturation, and luminance transitions individually.
|
|
2249
|
+
|
|
2250
|
+
Parameters
|
|
2251
|
+
----------
|
|
2252
|
+
%(colors.space)s
|
|
2253
|
+
%(colors.name)s
|
|
2254
|
+
%(colors.ratios)s
|
|
2255
|
+
For example, ``luminance=(100, 50, 0)`` with ``ratios=(2, 1)`` results
|
|
2256
|
+
in a colormap with the transition from luminance ``100`` to ``50`` taking
|
|
2257
|
+
*twice as long* as the transition from luminance ``50`` to ``0``.
|
|
2258
|
+
h, s, l, a, c
|
|
2259
|
+
Shorthands for `hue`, `saturation`, `luminance`, `alpha`, and `chroma`.
|
|
2260
|
+
hue : float or color-spec or sequence, default: 0
|
|
2261
|
+
Hue channel value or sequence of values. The shorthand keyword `h` is also
|
|
2262
|
+
acceptable. Values can be any of the following.
|
|
2263
|
+
|
|
2264
|
+
1. Numbers, within the range 0 to 360 for hue and 0 to 100 for
|
|
2265
|
+
saturation and luminance.
|
|
2266
|
+
2. Color string names or hex strings, in which case the channel
|
|
2267
|
+
value for that color is looked up.
|
|
2268
|
+
saturation : float or color-spec or sequence, default: 50
|
|
2269
|
+
As with `hue`, but for the saturation channel.
|
|
2270
|
+
luminance : float or color-spec or sequence, default: ``(100, 20)``
|
|
2271
|
+
As with `hue`, but for the luminance channel.
|
|
2272
|
+
alpha : float or color-spec or sequence, default: 1
|
|
2273
|
+
As with `hue`, but for the alpha (opacity) channel.
|
|
2274
|
+
chroma
|
|
2275
|
+
Alias for `saturation`.
|
|
2276
|
+
|
|
2277
|
+
Other parameters
|
|
2278
|
+
----------------
|
|
2279
|
+
**kwargs
|
|
2280
|
+
Passed to `PerceptualColormap`.
|
|
2281
|
+
|
|
2282
|
+
Returns
|
|
2283
|
+
-------
|
|
2284
|
+
PerceptualColormap
|
|
2285
|
+
The colormap.
|
|
2286
|
+
|
|
2287
|
+
See also
|
|
2288
|
+
--------
|
|
2289
|
+
PerceptualColormap.from_color
|
|
2290
|
+
PerceptualColormap.from_list
|
|
2291
|
+
"""
|
|
2292
|
+
name, space, ratios, kwargs = cls._pop_args(
|
|
2293
|
+
*args, names=("space", "ratios"), **kwargs
|
|
2294
|
+
)
|
|
2295
|
+
cdict = {}
|
|
2296
|
+
props = _pop_props(kwargs, "hsla")
|
|
2297
|
+
for key, default in (
|
|
2298
|
+
("hue", 0),
|
|
2299
|
+
("saturation", 100),
|
|
2300
|
+
("luminance", (100, 20)),
|
|
2301
|
+
("alpha", 1),
|
|
2302
|
+
):
|
|
2303
|
+
value = props.pop(key, default)
|
|
2304
|
+
cdict[key] = _make_segment_data(value, ratios=ratios)
|
|
2305
|
+
return cls(name, cdict, space=space, **kwargs)
|
|
2306
|
+
|
|
2307
|
+
@classmethod
|
|
2308
|
+
@docstring._snippet_manager
|
|
2309
|
+
def from_list(cls, *args, adjust_grays=True, **kwargs):
|
|
2310
|
+
"""
|
|
2311
|
+
Make a `PerceptualColormap` from a sequence of colors.
|
|
2312
|
+
|
|
2313
|
+
Parameters
|
|
2314
|
+
----------
|
|
2315
|
+
%(colors.from_list)s
|
|
2316
|
+
adjust_grays : bool, optional
|
|
2317
|
+
Whether to adjust the hues of grayscale colors (including ``'white'``,
|
|
2318
|
+
``'black'``, and the ``'grayN'`` open-color colors) to the hues of the
|
|
2319
|
+
preceding and subsequent colors in the sequence. This facilitates the
|
|
2320
|
+
construction of diverging colormaps with monochromatic segments using
|
|
2321
|
+
e.g. ``PerceptualColormap.from_list(['blue', 'white', 'red'])``.
|
|
2322
|
+
|
|
2323
|
+
Other parameters
|
|
2324
|
+
----------------
|
|
2325
|
+
**kwargs
|
|
2326
|
+
Passed to `PerceptualColormap`.
|
|
2327
|
+
|
|
2328
|
+
Returns
|
|
2329
|
+
-------
|
|
2330
|
+
PerceptualColormap
|
|
2331
|
+
The colormap.
|
|
2332
|
+
|
|
2333
|
+
See also
|
|
2334
|
+
--------
|
|
2335
|
+
matplotlib.colors.LinearSegmentedColormap.from_list
|
|
2336
|
+
ContinuousColormap.from_list
|
|
2337
|
+
PerceptualColormap.from_color
|
|
2338
|
+
PerceptualColormap.from_hsl
|
|
2339
|
+
"""
|
|
2340
|
+
# Get coordinates
|
|
2341
|
+
coords = None
|
|
2342
|
+
space = kwargs.get("space", DEFAULT_SPACE).lower()
|
|
2343
|
+
name, colors, ratios, kwargs = cls._pop_args(
|
|
2344
|
+
*args, names=("colors", "ratios"), **kwargs
|
|
2345
|
+
)
|
|
2346
|
+
if not np.iterable(colors):
|
|
2347
|
+
raise ValueError(f"Colors must be iterable, got colors={colors!r}")
|
|
2348
|
+
if (
|
|
2349
|
+
np.iterable(colors[0])
|
|
2350
|
+
and len(colors[0]) == 2
|
|
2351
|
+
and not isinstance(colors[0], str)
|
|
2352
|
+
):
|
|
2353
|
+
coords, colors = zip(*colors)
|
|
2354
|
+
|
|
2355
|
+
# Build segmentdata
|
|
2356
|
+
keys = ("hue", "saturation", "luminance", "alpha")
|
|
2357
|
+
hslas = [to_xyza(color, space) for color in colors]
|
|
2358
|
+
cdict = {}
|
|
2359
|
+
for key, values in zip(keys, zip(*hslas)):
|
|
2360
|
+
cdict[key] = _make_segment_data(values, coords, ratios)
|
|
2361
|
+
|
|
2362
|
+
# Adjust grays
|
|
2363
|
+
if adjust_grays:
|
|
2364
|
+
hues = cdict["hue"] # segment data
|
|
2365
|
+
for i, color in enumerate(colors):
|
|
2366
|
+
rgb = to_rgb(color)
|
|
2367
|
+
if isinstance(color, str) and REGEX_ADJUST.match(color):
|
|
2368
|
+
pass
|
|
2369
|
+
elif not np.allclose(np.array(rgb), rgb[0]):
|
|
2370
|
+
continue
|
|
2371
|
+
hues[i] = list(hues[i]) # enforce mutability
|
|
2372
|
+
if i > 0:
|
|
2373
|
+
hues[i][1] = hues[i - 1][2]
|
|
2374
|
+
if i < len(hues) - 1:
|
|
2375
|
+
hues[i][2] = hues[i + 1][1]
|
|
2376
|
+
|
|
2377
|
+
return cls(name, cdict, **kwargs)
|
|
2378
|
+
|
|
2379
|
+
# Deprecated
|
|
2380
|
+
to_linear_segmented = warnings._rename_objs(
|
|
2381
|
+
"0.8.0", to_linear_segmented=to_continuous
|
|
2382
|
+
)
|
|
2383
|
+
|
|
2384
|
+
|
|
2385
|
+
def _interpolate_scalar(x, x0, x1, y0, y1):
|
|
2386
|
+
"""
|
|
2387
|
+
Interpolate between two points.
|
|
2388
|
+
"""
|
|
2389
|
+
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
def _interpolate_extrapolate_vector(xq, x, y):
|
|
2393
|
+
"""
|
|
2394
|
+
Interpolate between two vectors. Similar to `numpy.interp` except this
|
|
2395
|
+
does not truncate out-of-bounds values (i.e. this is reversible).
|
|
2396
|
+
"""
|
|
2397
|
+
# Follow example of _make_lookup_table for efficient, vectorized
|
|
2398
|
+
# linear interpolation across multiple segments.
|
|
2399
|
+
# * Normal test puts values at a[i] if a[i-1] < v <= a[i]; for
|
|
2400
|
+
# left-most data, satisfy a[0] <= v <= a[1]
|
|
2401
|
+
# * searchsorted gives where xq[i] must be inserted so it is larger
|
|
2402
|
+
# than x[ind[i]-1] but smaller than x[ind[i]]
|
|
2403
|
+
# yq = ma.masked_array(np.interp(xq, x, y), mask=ma.getmask(xq))
|
|
2404
|
+
x = np.asarray(x)
|
|
2405
|
+
y = np.asarray(y)
|
|
2406
|
+
xq = np.atleast_1d(xq)
|
|
2407
|
+
idx = np.searchsorted(x, xq)
|
|
2408
|
+
idx[idx == 0] = 1 # get normed value <0
|
|
2409
|
+
idx[idx == len(x)] = len(x) - 1 # get normed value >0
|
|
2410
|
+
distance = (xq - x[idx - 1]) / (x[idx] - x[idx - 1])
|
|
2411
|
+
yq = distance * (y[idx] - y[idx - 1]) + y[idx - 1]
|
|
2412
|
+
yq = ma.masked_array(yq, mask=ma.getmask(xq))
|
|
2413
|
+
return yq
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
def _sanitize_levels(levels, minsize=2):
|
|
2417
|
+
"""
|
|
2418
|
+
Ensure the levels are monotonic. If they are descending, reverse them.
|
|
2419
|
+
"""
|
|
2420
|
+
# NOTE: Matplotlib does not support datetime colormap levels as of 3.5
|
|
2421
|
+
levels = inputs._to_numpy_array(levels)
|
|
2422
|
+
if levels.ndim != 1 or levels.size < minsize:
|
|
2423
|
+
raise ValueError(f"Levels {levels} must be a 1D array with size >= {minsize}.")
|
|
2424
|
+
if isinstance(levels, ma.core.MaskedArray):
|
|
2425
|
+
levels = levels.filled(np.nan)
|
|
2426
|
+
if not inputs._is_numeric(levels) or not np.all(np.isfinite(levels)):
|
|
2427
|
+
raise ValueError(f"Levels {levels} does not support non-numeric cmap levels.")
|
|
2428
|
+
diffs = np.sign(np.diff(levels))
|
|
2429
|
+
if np.all(diffs == 1):
|
|
2430
|
+
descending = False
|
|
2431
|
+
elif np.all(diffs == -1):
|
|
2432
|
+
descending = True
|
|
2433
|
+
levels = levels[::-1]
|
|
2434
|
+
else:
|
|
2435
|
+
raise ValueError(f"Levels {levels} must be monotonic.")
|
|
2436
|
+
return levels, descending
|
|
2437
|
+
|
|
2438
|
+
|
|
2439
|
+
class DiscreteNorm(mcolors.BoundaryNorm):
|
|
2440
|
+
"""
|
|
2441
|
+
Meta-normalizer that discretizes the possible color values returned by
|
|
2442
|
+
arbitrary continuous normalizers given a sequence of level boundaries.
|
|
2443
|
+
"""
|
|
2444
|
+
|
|
2445
|
+
# See this post: https://stackoverflow.com/a/48614231/4970632
|
|
2446
|
+
# WARNING: Must be child of BoundaryNorm. Many methods in ColorBarBase
|
|
2447
|
+
# test for class membership, crucially including _process_values(), which
|
|
2448
|
+
# if it doesn't detect BoundaryNorm will try to use DiscreteNorm.inverse().
|
|
2449
|
+
@warnings._rename_kwargs(
|
|
2450
|
+
"0.7.0", extend="unique", descending="DiscreteNorm(descending_levels)"
|
|
2451
|
+
)
|
|
2452
|
+
def __init__(
|
|
2453
|
+
self,
|
|
2454
|
+
levels,
|
|
2455
|
+
norm=None,
|
|
2456
|
+
unique=None,
|
|
2457
|
+
step=None,
|
|
2458
|
+
clip=False,
|
|
2459
|
+
ticks=None,
|
|
2460
|
+
labels=None,
|
|
2461
|
+
):
|
|
2462
|
+
"""
|
|
2463
|
+
Parameters
|
|
2464
|
+
----------
|
|
2465
|
+
levels : sequence of float
|
|
2466
|
+
The level boundaries. Must be monotonically increasing or decreasing.
|
|
2467
|
+
If the latter then `~DiscreteNorm.descending` is set to ``True`` and the
|
|
2468
|
+
colorbar axis drawn with this normalizer will be reversed.
|
|
2469
|
+
norm : `~matplotlib.colors.Normalize`, optional
|
|
2470
|
+
The normalizer used to transform `levels` and data values passed to
|
|
2471
|
+
`~DiscreteNorm.__call__` before discretization. The ``vmin`` and ``vmax``
|
|
2472
|
+
of the normalizer are set to the minimum and maximum values in `levels`.
|
|
2473
|
+
unique : {'neither', 'both', 'min', 'max'}, optional
|
|
2474
|
+
Which out-of-bounds regions should be assigned unique colormap colors.
|
|
2475
|
+
Possible values are equivalent to the `extend` values. Internally, ultraplot
|
|
2476
|
+
sets this depending on the user-input `extend`, whether the colormap is
|
|
2477
|
+
cyclic, and whether `~matplotlib.colors.Colormap.set_under`
|
|
2478
|
+
or `~matplotlib.colors.Colormap.set_over` were called for the colormap.
|
|
2479
|
+
step : float, optional
|
|
2480
|
+
The intensity of the transition to out-of-bounds colors as a fraction
|
|
2481
|
+
of the adjacent step between in-bounds colors. Internally, ultraplot sets
|
|
2482
|
+
this to ``0.5`` for cyclic colormaps and ``1`` for all other colormaps.
|
|
2483
|
+
This only has an effect on lower colors when `unique` is ``'min'`` or
|
|
2484
|
+
``'both'``, and on upper colors when `unique` is ``'max'`` or ``'both'``.
|
|
2485
|
+
clip : bool, optional
|
|
2486
|
+
Whether to clip values falling outside of the level bins. This only
|
|
2487
|
+
has an effect on lower colors when `unique` is ``'min'`` or ``'both'``,
|
|
2488
|
+
and on upper colors when `unique` is ``'max'`` or ``'both'``.
|
|
2489
|
+
|
|
2490
|
+
Other parameters
|
|
2491
|
+
----------------
|
|
2492
|
+
ticks : array-like, default: `levels`
|
|
2493
|
+
Default tick values to use for colorbars drawn with this normalizer. This
|
|
2494
|
+
is set to the level centers when `values` is passed to a plotting command.
|
|
2495
|
+
labels : array-like, optional
|
|
2496
|
+
Default tick labels to use for colorbars drawn with this normalizer. This
|
|
2497
|
+
is set to values when drawing on-the-fly colorbars.
|
|
2498
|
+
|
|
2499
|
+
Note
|
|
2500
|
+
----
|
|
2501
|
+
This normalizer makes sure that levels always span the full range of
|
|
2502
|
+
colors in the colormap, whether `extend` is set to ``'min'``, ``'max'``,
|
|
2503
|
+
``'neither'``, or ``'both'``. In matplotlib, when `extend` is not ``'both'``,
|
|
2504
|
+
the most intense colors are cut off (reserved for "out of bounds" data),
|
|
2505
|
+
even though they are not being used.
|
|
2506
|
+
|
|
2507
|
+
See also
|
|
2508
|
+
--------
|
|
2509
|
+
ultraplot.constructor.Norm
|
|
2510
|
+
ultraplot.colors.SegmentedNorm
|
|
2511
|
+
ultraplot.ticker.DiscreteLocator
|
|
2512
|
+
"""
|
|
2513
|
+
# Parse input arguments
|
|
2514
|
+
# NOTE: This must be a subclass BoundaryNorm, so ColorbarBase will
|
|
2515
|
+
# detect it... even though we completely override it.
|
|
2516
|
+
if step is None:
|
|
2517
|
+
step = 1.0
|
|
2518
|
+
if unique is None:
|
|
2519
|
+
unique = "neither"
|
|
2520
|
+
if not norm:
|
|
2521
|
+
norm = mcolors.Normalize()
|
|
2522
|
+
elif isinstance(norm, mcolors.BoundaryNorm):
|
|
2523
|
+
raise ValueError("Normalizer cannot be instance of BoundaryNorm.")
|
|
2524
|
+
elif not isinstance(norm, mcolors.Normalize):
|
|
2525
|
+
raise ValueError("Normalizer must be instance of Normalize.")
|
|
2526
|
+
uniques = ("min", "max", "both", "neither")
|
|
2527
|
+
if unique not in uniques:
|
|
2528
|
+
raise ValueError(
|
|
2529
|
+
f"Unknown unique setting {unique!r}. Options are: "
|
|
2530
|
+
+ ", ".join(map(repr, uniques))
|
|
2531
|
+
+ "."
|
|
2532
|
+
)
|
|
2533
|
+
|
|
2534
|
+
# Process level boundaries and centers
|
|
2535
|
+
# NOTE: Currently there are no normalizers that reverse direction
|
|
2536
|
+
# of levels. Tried that with SegmentedNorm but colorbar ticks fail.
|
|
2537
|
+
# Instead user-reversed levels will always get passed here just as
|
|
2538
|
+
# they are passed to SegmentedNorm inside plot.py
|
|
2539
|
+
levels, descending = _sanitize_levels(levels)
|
|
2540
|
+
vmin = norm.vmin = np.min(levels)
|
|
2541
|
+
vmax = norm.vmax = np.max(levels)
|
|
2542
|
+
bins, _ = _sanitize_levels(norm(levels))
|
|
2543
|
+
vcenter = getattr(norm, "vcenter", None)
|
|
2544
|
+
mids = np.zeros((levels.size + 1,))
|
|
2545
|
+
mids[1:-1] = 0.5 * (levels[1:] + levels[:-1])
|
|
2546
|
+
mids[0], mids[-1] = mids[1], mids[-2]
|
|
2547
|
+
|
|
2548
|
+
# Adjust color coordinate for each bin
|
|
2549
|
+
# For same out-of-bounds colors, looks like [0 - eps, 0, ..., 1, 1 + eps]
|
|
2550
|
+
# For unique out-of-bounds colors, looks like [0 - eps, X, ..., 1 - X, 1 + eps]
|
|
2551
|
+
# NOTE: Critical that we scale the bin centers in "physical space" and *then*
|
|
2552
|
+
# translate to color coordinates so that nonlinearities in the normalization
|
|
2553
|
+
# stay intact. If we scaled the bin centers in *normalized space* to have
|
|
2554
|
+
# minimum 0 maximum 1, would mess up color distribution. However this is still
|
|
2555
|
+
# not perfect... get asymmetric color intensity either side of central point.
|
|
2556
|
+
# So we add special handling for diverging norms below to improve symmetry.
|
|
2557
|
+
if unique in ("min", "both"):
|
|
2558
|
+
mids[0] += step * (mids[1] - mids[2])
|
|
2559
|
+
if unique in ("max", "both"):
|
|
2560
|
+
mids[-1] += step * (mids[-2] - mids[-3])
|
|
2561
|
+
mmin, mmax = np.min(mids), np.max(mids)
|
|
2562
|
+
if vcenter is None:
|
|
2563
|
+
mids = _interpolate_scalar(mids, mmin, mmax, vmin, vmax)
|
|
2564
|
+
else:
|
|
2565
|
+
mask1, mask2 = mids < vcenter, mids >= vcenter
|
|
2566
|
+
mids[mask1] = _interpolate_scalar(mids[mask1], mmin, vcenter, vmin, vcenter)
|
|
2567
|
+
mids[mask2] = _interpolate_scalar(mids[mask2], vcenter, mmax, vcenter, vmax)
|
|
2568
|
+
|
|
2569
|
+
# Instance attributes
|
|
2570
|
+
# NOTE: If clip is True, we clip values to the centers of the end bins
|
|
2571
|
+
# rather than vmin/vmax to prevent out-of-bounds colors from getting an
|
|
2572
|
+
# in-bounds bin color due to landing on a bin edge.
|
|
2573
|
+
# NOTE: With unique='min' the minimimum in-bounds and out-of-bounds
|
|
2574
|
+
# colors are the same so clip=True will have no effect. Same goes
|
|
2575
|
+
# for unique='max' with maximum colors.
|
|
2576
|
+
eps = 1e-10
|
|
2577
|
+
dest = norm(mids)
|
|
2578
|
+
dest[0] -= eps # dest guaranteed to be numpy.float64
|
|
2579
|
+
dest[-1] += eps
|
|
2580
|
+
self._ticks = _not_none(ticks, levels)
|
|
2581
|
+
self._labels = labels
|
|
2582
|
+
self._descending = descending
|
|
2583
|
+
self._bmin = np.min(mids)
|
|
2584
|
+
self._bmax = np.max(mids)
|
|
2585
|
+
self._bins = bins
|
|
2586
|
+
self._dest = dest
|
|
2587
|
+
self._norm = norm
|
|
2588
|
+
self.N = levels.size
|
|
2589
|
+
self.boundaries = levels
|
|
2590
|
+
mcolors.Normalize.__init__(self, vmin=vmin, vmax=vmax, clip=clip)
|
|
2591
|
+
|
|
2592
|
+
# Add special clipping
|
|
2593
|
+
# WARNING: For some reason must clip manually for LogNorm, or end
|
|
2594
|
+
# up with unpredictable fill value, weird "out-of-bounds" colors
|
|
2595
|
+
self._norm_clip = None
|
|
2596
|
+
if isinstance(norm, mcolors.LogNorm):
|
|
2597
|
+
self._norm_clip = (1e-249, None)
|
|
2598
|
+
|
|
2599
|
+
def __call__(self, value, clip=None):
|
|
2600
|
+
"""
|
|
2601
|
+
Normalize data values to 0-1.
|
|
2602
|
+
|
|
2603
|
+
Parameters
|
|
2604
|
+
----------
|
|
2605
|
+
value : numeric
|
|
2606
|
+
The data to be normalized.
|
|
2607
|
+
clip : bool, default: ``self.clip``
|
|
2608
|
+
Whether to clip values falling outside of the level bins.
|
|
2609
|
+
"""
|
|
2610
|
+
# Follow example of SegmentedNorm, but perform no interpolation,
|
|
2611
|
+
# just use searchsorted to bin the data.
|
|
2612
|
+
norm_clip = self._norm_clip
|
|
2613
|
+
if norm_clip: # special extra clipping due to normalizer
|
|
2614
|
+
value = np.clip(value, *norm_clip)
|
|
2615
|
+
if clip is None: # builtin clipping
|
|
2616
|
+
clip = self.clip
|
|
2617
|
+
if clip: # note that np.clip can handle masked arrays
|
|
2618
|
+
value = np.clip(value, self._bmin, self._bmax)
|
|
2619
|
+
xq, is_scalar = self.process_value(value)
|
|
2620
|
+
xq = self._norm(xq)
|
|
2621
|
+
yq = self._dest[np.searchsorted(self._bins, xq)]
|
|
2622
|
+
yq = ma.array(yq, mask=ma.getmask(xq))
|
|
2623
|
+
if is_scalar:
|
|
2624
|
+
yq = np.atleast_1d(yq)[0]
|
|
2625
|
+
if self.descending:
|
|
2626
|
+
yq = 1 - yq
|
|
2627
|
+
return yq
|
|
2628
|
+
|
|
2629
|
+
def inverse(self, value): # noqa: U100
|
|
2630
|
+
"""
|
|
2631
|
+
Raise an error.
|
|
2632
|
+
|
|
2633
|
+
Raises
|
|
2634
|
+
------
|
|
2635
|
+
ValueError
|
|
2636
|
+
Inversion after discretization is impossible.
|
|
2637
|
+
"""
|
|
2638
|
+
raise ValueError("DiscreteNorm is not invertible.")
|
|
2639
|
+
|
|
2640
|
+
@property
|
|
2641
|
+
def descending(self):
|
|
2642
|
+
"""
|
|
2643
|
+
Boolean indicating whether the levels are descending.
|
|
2644
|
+
"""
|
|
2645
|
+
return self._descending
|
|
2646
|
+
|
|
2647
|
+
|
|
2648
|
+
class SegmentedNorm(mcolors.Normalize):
|
|
2649
|
+
"""
|
|
2650
|
+
Normalizer that scales data linearly with respect to the
|
|
2651
|
+
interpolated index in an arbitrary monotonic level sequence.
|
|
2652
|
+
"""
|
|
2653
|
+
|
|
2654
|
+
def __init__(self, levels, vmin=None, vmax=None, clip=False):
|
|
2655
|
+
"""
|
|
2656
|
+
Parameters
|
|
2657
|
+
----------
|
|
2658
|
+
levels : sequence of float
|
|
2659
|
+
The level boundaries. Must be monotonically increasing
|
|
2660
|
+
or decreasing.
|
|
2661
|
+
vmin : float, optional
|
|
2662
|
+
Ignored but included for consistency with other normalizers.
|
|
2663
|
+
Set to the minimum of `levels`.
|
|
2664
|
+
vmax : float, optional
|
|
2665
|
+
Ignored but included for consistency with other normalizers.
|
|
2666
|
+
Set to the minimum of `levels`.
|
|
2667
|
+
clip : bool, optional
|
|
2668
|
+
Whether to clip values falling outside of the minimum
|
|
2669
|
+
and maximum of `levels`.
|
|
2670
|
+
|
|
2671
|
+
See also
|
|
2672
|
+
--------
|
|
2673
|
+
ultraplot.constructor.Norm
|
|
2674
|
+
ultraplot.colors.DiscreteNorm
|
|
2675
|
+
|
|
2676
|
+
Note
|
|
2677
|
+
----
|
|
2678
|
+
The algorithm this normalizer uses to select normalized values
|
|
2679
|
+
in-between level list indices is adapted from the algorithm
|
|
2680
|
+
`~matplotlib.colors.LinearSegmentedColormap` uses to select channel
|
|
2681
|
+
values in-between segment data points (hence the name `SegmentedNorm`).
|
|
2682
|
+
|
|
2683
|
+
Example
|
|
2684
|
+
-------
|
|
2685
|
+
In the below example, unevenly spaced levels are passed to
|
|
2686
|
+
`~matplotlib.axes.Axes.contourf`, resulting in the automatic
|
|
2687
|
+
application of `SegmentedNorm`.
|
|
2688
|
+
|
|
2689
|
+
>>> import ultraplot as pplt
|
|
2690
|
+
>>> import numpy as np
|
|
2691
|
+
>>> levels = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]
|
|
2692
|
+
>>> data = 10 ** (3 * np.random.rand(10, 10))
|
|
2693
|
+
>>> fig, ax = pplt.subplots()
|
|
2694
|
+
>>> ax.contourf(data, levels=levels)
|
|
2695
|
+
"""
|
|
2696
|
+
# WARNING: Tried using descending levels by adding 1 - yq to __call__() and
|
|
2697
|
+
# inverse() but then tick labels fail. Instead just silently reverse here and
|
|
2698
|
+
# the corresponding DiscreteLocator should enforce the descending axis.
|
|
2699
|
+
levels, _ = _sanitize_levels(levels)
|
|
2700
|
+
dest = np.linspace(0, 1, len(levels))
|
|
2701
|
+
vmin = np.min(levels)
|
|
2702
|
+
vmax = np.max(levels)
|
|
2703
|
+
super().__init__(vmin=vmin, vmax=vmax, clip=clip)
|
|
2704
|
+
self._x = self.boundaries = levels # 'boundaries' are used in PlotAxes
|
|
2705
|
+
self._y = dest
|
|
2706
|
+
|
|
2707
|
+
def __call__(self, value, clip=None):
|
|
2708
|
+
"""
|
|
2709
|
+
Normalize the data values to 0-1. Inverse of `~SegmentedNorm.inverse`.
|
|
2710
|
+
|
|
2711
|
+
Parameters
|
|
2712
|
+
----------
|
|
2713
|
+
value : numeric
|
|
2714
|
+
The data to be normalized.
|
|
2715
|
+
clip : bool, default: ``self.clip``
|
|
2716
|
+
Whether to clip values falling outside of the minimum and maximum levels.
|
|
2717
|
+
"""
|
|
2718
|
+
if clip is None: # builtin clipping
|
|
2719
|
+
clip = self.clip
|
|
2720
|
+
if clip: # numpy.clip can handle masked arrays
|
|
2721
|
+
value = np.clip(value, self.vmin, self.vmax)
|
|
2722
|
+
xq, is_scalar = self.process_value(value)
|
|
2723
|
+
yq = _interpolate_extrapolate_vector(xq, self._x, self._y)
|
|
2724
|
+
if is_scalar:
|
|
2725
|
+
yq = np.atleast_1d(yq)[0]
|
|
2726
|
+
return yq
|
|
2727
|
+
|
|
2728
|
+
def inverse(self, value):
|
|
2729
|
+
"""
|
|
2730
|
+
Inverse of `~SegmentedNorm.__call__`.
|
|
2731
|
+
|
|
2732
|
+
Parameters
|
|
2733
|
+
----------
|
|
2734
|
+
value : numeric
|
|
2735
|
+
The data to be un-normalized.
|
|
2736
|
+
"""
|
|
2737
|
+
yq, is_scalar = self.process_value(value)
|
|
2738
|
+
xq = _interpolate_extrapolate_vector(yq, self._y, self._x)
|
|
2739
|
+
if is_scalar:
|
|
2740
|
+
xq = np.atleast_1d(xq)[0]
|
|
2741
|
+
return xq
|
|
2742
|
+
|
|
2743
|
+
|
|
2744
|
+
class DivergingNorm(mcolors.Normalize):
|
|
2745
|
+
"""
|
|
2746
|
+
Normalizer that ensures some central data value lies at the central
|
|
2747
|
+
colormap color. The default central value is ``0``.
|
|
2748
|
+
"""
|
|
2749
|
+
|
|
2750
|
+
def __str__(self):
|
|
2751
|
+
return type(self).__name__ + f"(center={self.vcenter!r})"
|
|
2752
|
+
|
|
2753
|
+
def __init__(self, vcenter=0, vmin=None, vmax=None, fair=True, clip=None):
|
|
2754
|
+
"""
|
|
2755
|
+
Parameters
|
|
2756
|
+
----------
|
|
2757
|
+
vcenter : float, default: 0
|
|
2758
|
+
The data value corresponding to the central colormap position.
|
|
2759
|
+
vmin : float, optional
|
|
2760
|
+
The minimum data value.
|
|
2761
|
+
vmax : float, optional
|
|
2762
|
+
The maximum data value.
|
|
2763
|
+
fair : bool, optional
|
|
2764
|
+
If ``True`` (default), the speeds of the color gradations on either side
|
|
2765
|
+
of the center point are equal, but colormap colors may be omitted. If
|
|
2766
|
+
``False``, all colormap colors are included, but the color gradations on
|
|
2767
|
+
one side may be faster than the other side. ``False`` should be used with
|
|
2768
|
+
great care, as it may result in a misleading interpretation of your data.
|
|
2769
|
+
clip : bool, optional
|
|
2770
|
+
Whether to clip values falling outside of `vmin` and `vmax`.
|
|
2771
|
+
|
|
2772
|
+
See also
|
|
2773
|
+
--------
|
|
2774
|
+
ultraplot.constructor.Norm
|
|
2775
|
+
"""
|
|
2776
|
+
# NOTE: This post is an excellent summary of matplotlib's DivergingNorm history:
|
|
2777
|
+
# https://github.com/matplotlib/matplotlib/issues/15336#issuecomment-535291287
|
|
2778
|
+
# NOTE: This is a stale PR that plans to implement the same features.
|
|
2779
|
+
# https://github.com/matplotlib/matplotlib/pull/15333#issuecomment-537545430
|
|
2780
|
+
# Since ultraplot is starting without matplotlib's baggage we can just implement
|
|
2781
|
+
# a diverging norm like they would prefer if they didn't have to worry about
|
|
2782
|
+
# confusing users: single class, default "fair" scaling that can be turned off.
|
|
2783
|
+
super().__init__(vmin, vmax, clip)
|
|
2784
|
+
self.vcenter = vcenter
|
|
2785
|
+
self.fair = fair
|
|
2786
|
+
|
|
2787
|
+
def __call__(self, value, clip=None):
|
|
2788
|
+
"""
|
|
2789
|
+
Normalize the data values to 0-1.
|
|
2790
|
+
|
|
2791
|
+
Parameters
|
|
2792
|
+
----------
|
|
2793
|
+
value : numeric
|
|
2794
|
+
The data to be normalized.
|
|
2795
|
+
clip : bool, default: ``self.clip``
|
|
2796
|
+
Whether to clip values falling outside of `vmin` and `vmax`.
|
|
2797
|
+
"""
|
|
2798
|
+
xq, is_scalar = self.process_value(value)
|
|
2799
|
+
self.autoscale_None(xq) # sets self.vmin, self.vmax if None
|
|
2800
|
+
if clip is None: # builtin clipping
|
|
2801
|
+
clip = self.clip
|
|
2802
|
+
if clip: # note that np.clip can handle masked arrays
|
|
2803
|
+
value = np.clip(value, self.vmin, self.vmax)
|
|
2804
|
+
if self.vmin > self.vmax:
|
|
2805
|
+
raise ValueError("vmin must be less than or equal to vmax.")
|
|
2806
|
+
elif self.vmin == self.vmax:
|
|
2807
|
+
x = [self.vmin, self.vmax]
|
|
2808
|
+
y = [0.0, 0.0]
|
|
2809
|
+
elif self.vcenter >= self.vmax:
|
|
2810
|
+
x = [self.vmin, self.vcenter]
|
|
2811
|
+
y = [0.0, 0.5]
|
|
2812
|
+
elif self.vcenter <= self.vmin:
|
|
2813
|
+
x = [self.vcenter, self.vmax]
|
|
2814
|
+
y = [0.5, 1.0]
|
|
2815
|
+
elif self.fair:
|
|
2816
|
+
offset = max(abs(self.vcenter - self.vmin), abs(self.vmax - self.vcenter))
|
|
2817
|
+
x = [self.vcenter - offset, self.vcenter + offset]
|
|
2818
|
+
y = [0.0, 1.0]
|
|
2819
|
+
else:
|
|
2820
|
+
x = [self.vmin, self.vcenter, self.vmax]
|
|
2821
|
+
y = [0.0, 0.5, 1.0]
|
|
2822
|
+
yq = _interpolate_extrapolate_vector(xq, x, y)
|
|
2823
|
+
if is_scalar:
|
|
2824
|
+
yq = np.atleast_1d(yq)[0]
|
|
2825
|
+
return yq
|
|
2826
|
+
|
|
2827
|
+
def autoscale_None(self, z):
|
|
2828
|
+
"""
|
|
2829
|
+
Get vmin and vmax, and then clip at vcenter.
|
|
2830
|
+
"""
|
|
2831
|
+
super().autoscale_None(z)
|
|
2832
|
+
if self.vmin > self.vcenter:
|
|
2833
|
+
self.vmin = self.vcenter
|
|
2834
|
+
if self.vmax < self.vcenter:
|
|
2835
|
+
self.vmax = self.vcenter
|
|
2836
|
+
|
|
2837
|
+
|
|
2838
|
+
def _init_color_database():
|
|
2839
|
+
"""
|
|
2840
|
+
Initialize the subclassed database.
|
|
2841
|
+
"""
|
|
2842
|
+
database = mcolors._colors_full_map
|
|
2843
|
+
if not isinstance(database, ColorDatabase):
|
|
2844
|
+
database = mcolors._colors_full_map = ColorDatabase(database)
|
|
2845
|
+
if hasattr(mcolors, "colorConverter"): # suspect deprecation is coming soon
|
|
2846
|
+
mcolors.colorConverter.cache = database.cache
|
|
2847
|
+
mcolors.colorConverter.colors = database
|
|
2848
|
+
return database
|
|
2849
|
+
|
|
2850
|
+
|
|
2851
|
+
def _init_cmap_database():
|
|
2852
|
+
"""
|
|
2853
|
+
Initialize the subclassed database.
|
|
2854
|
+
"""
|
|
2855
|
+
# We override the matplotlib base class
|
|
2856
|
+
# to add some functionality to it. Key features includes
|
|
2857
|
+
# - key insensitive lookup
|
|
2858
|
+
# - allow for dynamically generated shifted or reversed colormaps
|
|
2859
|
+
# with the extensions _r and _s(hifted)
|
|
2860
|
+
# This means we have to collect the base colormaps
|
|
2861
|
+
# and register them under the new object
|
|
2862
|
+
database = mcm._colormaps # shallow copy of mpl's colormaps
|
|
2863
|
+
if not isinstance(database, ColormapDatabase):
|
|
2864
|
+
# Collect the mpl colormaps and include them
|
|
2865
|
+
# in ultraplot's registry
|
|
2866
|
+
database = {
|
|
2867
|
+
key: value
|
|
2868
|
+
for key, value in database.items()
|
|
2869
|
+
if not key.endswith("_r") and not key.endswith("_shifted")
|
|
2870
|
+
}
|
|
2871
|
+
database = ColormapDatabase(database)
|
|
2872
|
+
setattr(
|
|
2873
|
+
mcm, "_colormaps", database
|
|
2874
|
+
) # not sure if this is necessary since colormaps is a (shallow?) copy of _colormaps
|
|
2875
|
+
setattr(mpl, "colormaps", database) # this is necessary
|
|
2876
|
+
return database
|
|
2877
|
+
|
|
2878
|
+
|
|
2879
|
+
def _get_cmap_subtype(name, subtype):
|
|
2880
|
+
"""
|
|
2881
|
+
Get a colormap belonging to a particular class. If none are found then raise
|
|
2882
|
+
a useful error message that omits colormaps from other classes.
|
|
2883
|
+
"""
|
|
2884
|
+
# NOTE: Right now this is just used in rc validation but could be used elsewhere
|
|
2885
|
+
if subtype == "discrete":
|
|
2886
|
+
cls = DiscreteColormap
|
|
2887
|
+
elif subtype == "continuous":
|
|
2888
|
+
cls = ContinuousColormap
|
|
2889
|
+
elif subtype == "perceptual":
|
|
2890
|
+
cls = PerceptualColormap
|
|
2891
|
+
else:
|
|
2892
|
+
raise RuntimeError(f"Invalid subtype {subtype!r}.")
|
|
2893
|
+
cmap = _cmap_database.get_cmap(name)
|
|
2894
|
+
if not isinstance(cmap, cls):
|
|
2895
|
+
names = sorted(k for k, v in _cmap_database.items() if isinstance(v, cls))
|
|
2896
|
+
raise ValueError(
|
|
2897
|
+
f"Invalid {subtype} colormap name {name!r}. Options are: "
|
|
2898
|
+
+ ", ".join(map(repr, names))
|
|
2899
|
+
+ "."
|
|
2900
|
+
)
|
|
2901
|
+
return cmap
|
|
2902
|
+
|
|
2903
|
+
|
|
2904
|
+
def _translate_cmap(cmap, lut=None, cyclic=None, listedthresh=None):
|
|
2905
|
+
"""
|
|
2906
|
+
Translate the input argument to a ultraplot colormap subclass. Auto-detect
|
|
2907
|
+
cyclic colormaps based on names and re-apply default lookup table size.
|
|
2908
|
+
"""
|
|
2909
|
+
# Parse args
|
|
2910
|
+
# WARNING: Apply default 'cyclic' property to native matplotlib colormaps
|
|
2911
|
+
# based on known names. Maybe slightly dangerous but cleanest approach
|
|
2912
|
+
lut = _not_none(lut, rc["image.lut"])
|
|
2913
|
+
cyclic = _not_none(cyclic, cmap.name and cmap.name.lower() in CMAPS_CYCLIC)
|
|
2914
|
+
listedthresh = _not_none(listedthresh, rc["cmap.listedthresh"])
|
|
2915
|
+
|
|
2916
|
+
# Translate the colormap
|
|
2917
|
+
# WARNING: Here we ignore 'N' in order to respect ultraplotrc lut sizes
|
|
2918
|
+
# when initializing ultraplot.
|
|
2919
|
+
bad = cmap._rgba_bad
|
|
2920
|
+
under = cmap._rgba_under
|
|
2921
|
+
over = cmap._rgba_over
|
|
2922
|
+
name = cmap.name
|
|
2923
|
+
if isinstance(cmap, (DiscreteColormap, ContinuousColormap)):
|
|
2924
|
+
pass
|
|
2925
|
+
elif isinstance(cmap, mcolors.LinearSegmentedColormap):
|
|
2926
|
+
data = dict(cmap._segmentdata)
|
|
2927
|
+
cmap = ContinuousColormap(name, data, N=lut, gamma=cmap._gamma, cyclic=cyclic)
|
|
2928
|
+
elif isinstance(cmap, mcolors.ListedColormap):
|
|
2929
|
+
colors = list(cmap.colors)
|
|
2930
|
+
if len(colors) > listedthresh: # see notes at top of file
|
|
2931
|
+
cmap = ContinuousColormap.from_list(name, colors, N=lut, cyclic=cyclic)
|
|
2932
|
+
else:
|
|
2933
|
+
cmap = DiscreteColormap(colors, name)
|
|
2934
|
+
elif isinstance(cmap, mcolors.Colormap): # base class
|
|
2935
|
+
pass
|
|
2936
|
+
else:
|
|
2937
|
+
raise ValueError(
|
|
2938
|
+
f"Invalid colormap type {type(cmap).__name__!r}. "
|
|
2939
|
+
"Must be instance of matplotlib.colors.Colormap."
|
|
2940
|
+
)
|
|
2941
|
+
|
|
2942
|
+
# Apply hidden settings
|
|
2943
|
+
cmap._rgba_bad = bad
|
|
2944
|
+
cmap._rgba_under = under
|
|
2945
|
+
cmap._rgba_over = over
|
|
2946
|
+
|
|
2947
|
+
return cmap
|
|
2948
|
+
|
|
2949
|
+
|
|
2950
|
+
class _ColorCache(dict):
|
|
2951
|
+
"""
|
|
2952
|
+
Replacement for the native color cache.
|
|
2953
|
+
"""
|
|
2954
|
+
|
|
2955
|
+
def __getitem__(self, key):
|
|
2956
|
+
"""
|
|
2957
|
+
Get the standard color, colormap color, or color cycle color.
|
|
2958
|
+
"""
|
|
2959
|
+
# NOTE: Matplotlib 'color' args are passed to to_rgba, which tries to read
|
|
2960
|
+
# directly from cache and if that fails, sanitizes input, which raises
|
|
2961
|
+
# error on receiving (colormap, idx) tuple. So we have to override cache.
|
|
2962
|
+
return self._get_rgba(*key)
|
|
2963
|
+
|
|
2964
|
+
def _get_rgba(self, arg, alpha):
|
|
2965
|
+
"""
|
|
2966
|
+
Try to get the color from the registered colormap or color cycle.
|
|
2967
|
+
"""
|
|
2968
|
+
key = (arg, alpha)
|
|
2969
|
+
if isinstance(arg, str) or not np.iterable(arg) or len(arg) != 2:
|
|
2970
|
+
return dict.__getitem__(self, key)
|
|
2971
|
+
if not isinstance(arg[0], str) or not isinstance(arg[1], Number):
|
|
2972
|
+
return dict.__getitem__(self, key)
|
|
2973
|
+
# Try to get the colormap
|
|
2974
|
+
try:
|
|
2975
|
+
cmap = _cmap_database[arg[0]]
|
|
2976
|
+
except (KeyError, TypeError):
|
|
2977
|
+
return dict.__getitem__(self, key)
|
|
2978
|
+
# Read the colormap value
|
|
2979
|
+
if isinstance(cmap, DiscreteColormap):
|
|
2980
|
+
if not 0 <= arg[1] < len(cmap.colors):
|
|
2981
|
+
raise ValueError(
|
|
2982
|
+
f"Color cycle sample for {arg[0]!r} cycle must be "
|
|
2983
|
+
f"between 0 and {len(cmap.colors) - 1}, got {arg[1]}."
|
|
2984
|
+
)
|
|
2985
|
+
rgba = cmap.colors[arg[1]] # draw from list of colors
|
|
2986
|
+
else:
|
|
2987
|
+
if not 0 <= arg[1] <= 1:
|
|
2988
|
+
raise ValueError(
|
|
2989
|
+
f"Colormap sample for {arg[0]!r} colormap must be "
|
|
2990
|
+
f"between 0 and 1, got {arg[1]}."
|
|
2991
|
+
)
|
|
2992
|
+
rgba = cmap(arg[1]) # get color selection
|
|
2993
|
+
# Return the colormap value
|
|
2994
|
+
rgba = to_rgba(rgba)
|
|
2995
|
+
a = _not_none(alpha, rgba[3])
|
|
2996
|
+
return (*rgba[:3], a)
|
|
2997
|
+
|
|
2998
|
+
|
|
2999
|
+
class ColorDatabase(MutableMapping, dict):
|
|
3000
|
+
"""
|
|
3001
|
+
Dictionary subclass used to replace the builtin matplotlib color database.
|
|
3002
|
+
See `~ColorDatabase.__getitem__` for details.
|
|
3003
|
+
"""
|
|
3004
|
+
|
|
3005
|
+
_colors_replace = (
|
|
3006
|
+
("grey", "gray"), # British --> American synonyms
|
|
3007
|
+
("ochre", "ocher"), # ...
|
|
3008
|
+
("kelley", "kelly"), # backwards compatibility to correct spelling
|
|
3009
|
+
)
|
|
3010
|
+
|
|
3011
|
+
def __delitem__(self, key):
|
|
3012
|
+
key = self._parse_key(key)
|
|
3013
|
+
dict.__delitem__(self, key)
|
|
3014
|
+
self.cache.clear()
|
|
3015
|
+
|
|
3016
|
+
def __init__(self, mapping=None):
|
|
3017
|
+
"""
|
|
3018
|
+
Parameters
|
|
3019
|
+
----------
|
|
3020
|
+
mapping : dict-like, optional
|
|
3021
|
+
The colors.
|
|
3022
|
+
"""
|
|
3023
|
+
# NOTE: Tested with and without standardization and speedup is marginal
|
|
3024
|
+
self._cache = _ColorCache()
|
|
3025
|
+
mapping = mapping or {}
|
|
3026
|
+
for key, value in mapping.items():
|
|
3027
|
+
self.__setitem__(key, value)
|
|
3028
|
+
|
|
3029
|
+
def __getitem__(self, key):
|
|
3030
|
+
"""
|
|
3031
|
+
Get a color. Translates ``grey`` into ``gray`` and supports retrieving
|
|
3032
|
+
colors "on-the-fly" from registered colormaps and color cycles.
|
|
3033
|
+
|
|
3034
|
+
* For a colormap, use e.g. ``color=('Blues', 0.8)``.
|
|
3035
|
+
The number is the colormap index, and must be between 0 and 1.
|
|
3036
|
+
* For a color cycle, use e.g. ``color=('colorblind', 2)``.
|
|
3037
|
+
The number is the color list index.
|
|
3038
|
+
|
|
3039
|
+
This works everywhere that colors are used in matplotlib, for
|
|
3040
|
+
example as `color`, `edgecolor', or `facecolor` keyword arguments
|
|
3041
|
+
passed to `~ultraplot.axes.PlotAxes` commands.
|
|
3042
|
+
"""
|
|
3043
|
+
key = self._parse_key(key)
|
|
3044
|
+
return dict.__getitem__(self, key)
|
|
3045
|
+
|
|
3046
|
+
def __setitem__(self, key, value):
|
|
3047
|
+
"""
|
|
3048
|
+
Add a color. Translates ``grey`` into ``gray`` and clears the
|
|
3049
|
+
cache. The color must be a string.
|
|
3050
|
+
"""
|
|
3051
|
+
# Always standardize assignments.
|
|
3052
|
+
key = self._parse_key(key)
|
|
3053
|
+
dict.__setitem__(self, key, value)
|
|
3054
|
+
self.cache.clear()
|
|
3055
|
+
|
|
3056
|
+
def _parse_key(self, key):
|
|
3057
|
+
"""
|
|
3058
|
+
Parse the color key. Currently this just translates grays.
|
|
3059
|
+
"""
|
|
3060
|
+
if not isinstance(key, str):
|
|
3061
|
+
raise ValueError(f"Invalid color name {key!r}. Must be string.")
|
|
3062
|
+
if isinstance(key, str) and len(key) > 1: # ignore base colors
|
|
3063
|
+
key = key.lower()
|
|
3064
|
+
for sub, rep in self._colors_replace:
|
|
3065
|
+
key = key.replace(sub, rep)
|
|
3066
|
+
return key
|
|
3067
|
+
|
|
3068
|
+
@property
|
|
3069
|
+
def cache(self):
|
|
3070
|
+
# Matplotlib uses 'cache' but treat '_cache' as synonym
|
|
3071
|
+
# to guard against private API changes.
|
|
3072
|
+
return self._cache
|
|
3073
|
+
|
|
3074
|
+
|
|
3075
|
+
class ColormapDatabase(mcm.ColormapRegistry):
|
|
3076
|
+
"""
|
|
3077
|
+
Dictionary subclass used to replace the matplotlib
|
|
3078
|
+
colormap registry. See `~ColormapDatabase.__getitem__` and
|
|
3079
|
+
`~ColormapDatabase.__setitem__` for details.
|
|
3080
|
+
"""
|
|
3081
|
+
|
|
3082
|
+
_regex_grays = re.compile(r"\A(grays)(_r|_s)*\Z", flags=re.IGNORECASE)
|
|
3083
|
+
_regex_suffix = re.compile(r"(_r|_s)*\Z", flags=re.IGNORECASE)
|
|
3084
|
+
|
|
3085
|
+
def __init__(self, kwargs):
|
|
3086
|
+
"""
|
|
3087
|
+
Parameters
|
|
3088
|
+
----------
|
|
3089
|
+
kwargs : dict-like
|
|
3090
|
+
The source dictionary.
|
|
3091
|
+
"""
|
|
3092
|
+
super().__init__(kwargs)
|
|
3093
|
+
# The colormap is initialized with all the base colormaps
|
|
3094
|
+
# We have to change the classes internally to Perceptual, Continuous or Discrete
|
|
3095
|
+
# such that ultraplot knows what these objects are. We piggy back on the registering mechanism
|
|
3096
|
+
# by overriding matplotlib's behavior
|
|
3097
|
+
for name in tuple(self._cmaps.keys()):
|
|
3098
|
+
self.register(self._cmaps[name], name=name)
|
|
3099
|
+
|
|
3100
|
+
def _translate_deprecated(self, key):
|
|
3101
|
+
"""
|
|
3102
|
+
Check if a colormap has been deprecated.
|
|
3103
|
+
"""
|
|
3104
|
+
# WARNING: Must search only for case-sensitive *capitalized* names or we would
|
|
3105
|
+
# helpfully "redirect" user to SciVisColor cmap when they are trying to
|
|
3106
|
+
# generate open-color monochromatic cmaps and would disallow some color names
|
|
3107
|
+
if isinstance(key, str):
|
|
3108
|
+
test = self._regex_suffix.sub("", key)
|
|
3109
|
+
else:
|
|
3110
|
+
test = None
|
|
3111
|
+
if not self._has_item(test) and test in CMAPS_REMOVED:
|
|
3112
|
+
version = CMAPS_REMOVED[test]
|
|
3113
|
+
raise ValueError(
|
|
3114
|
+
f"The colormap name {key!r} was removed in version {version}."
|
|
3115
|
+
)
|
|
3116
|
+
if not self._has_item(test) and test in CMAPS_RENAMED:
|
|
3117
|
+
test_new, version = CMAPS_RENAMED[test]
|
|
3118
|
+
warnings._warn_ultraplot(
|
|
3119
|
+
f"The colormap name {test!r} was deprecated in version {version} "
|
|
3120
|
+
f"and may be removed in {warnings.next_release()}. Please use "
|
|
3121
|
+
f"the colormap name {test_new!r} instead."
|
|
3122
|
+
)
|
|
3123
|
+
key = re.sub(test, test_new, key, flags=re.IGNORECASE)
|
|
3124
|
+
return key
|
|
3125
|
+
|
|
3126
|
+
def _translate_key(self, original_key, mirror=True):
|
|
3127
|
+
"""
|
|
3128
|
+
Return the sanitized colormap name. Used for lookups and assignments.
|
|
3129
|
+
"""
|
|
3130
|
+
# Sanitize key
|
|
3131
|
+
if not isinstance(original_key, str):
|
|
3132
|
+
raise KeyError(f"Invalid key {original_key!r}. Key must be a string.")
|
|
3133
|
+
|
|
3134
|
+
key = original_key.lower()
|
|
3135
|
+
key = self._regex_grays.sub(r"greys\2", key)
|
|
3136
|
+
|
|
3137
|
+
# Handle reversal
|
|
3138
|
+
reverse = key.endswith("_r")
|
|
3139
|
+
if reverse:
|
|
3140
|
+
key = key.rstrip("_r")
|
|
3141
|
+
|
|
3142
|
+
# Check if the key exists in builtin colormaps
|
|
3143
|
+
if self._has_item(key):
|
|
3144
|
+
return key + "_r" if reverse else key
|
|
3145
|
+
|
|
3146
|
+
# Mirror diverging colormaps
|
|
3147
|
+
if mirror:
|
|
3148
|
+
# Check for diverging colormaps
|
|
3149
|
+
key_mirror = CMAPS_DIVERGING.get(key, None)
|
|
3150
|
+
if key_mirror and self._has_item(key_mirror):
|
|
3151
|
+
return key_mirror + "_r" if not reverse else key_mirror
|
|
3152
|
+
|
|
3153
|
+
# Check for reversed builtin colormaps
|
|
3154
|
+
if self._has_item(key + "_r"):
|
|
3155
|
+
return key if reverse else key + "_r"
|
|
3156
|
+
|
|
3157
|
+
# Try mirroring the non-lowered key
|
|
3158
|
+
if reverse:
|
|
3159
|
+
original_key = original_key.strip("_r")
|
|
3160
|
+
half = len(original_key) // 2
|
|
3161
|
+
mirrored_key = original_key[half:] + original_key[:half]
|
|
3162
|
+
if self._has_item(mirrored_key):
|
|
3163
|
+
return mirrored_key + "_r" if not reverse else mirrored_key
|
|
3164
|
+
# Restore key
|
|
3165
|
+
if reverse:
|
|
3166
|
+
original_key = original_key + "_r"
|
|
3167
|
+
# If no match found, return the original key
|
|
3168
|
+
return key
|
|
3169
|
+
|
|
3170
|
+
def _has_item(self, key):
|
|
3171
|
+
return key in self._cmaps
|
|
3172
|
+
|
|
3173
|
+
def get_cmap(self, cmap):
|
|
3174
|
+
return self.__getitem__(cmap)
|
|
3175
|
+
|
|
3176
|
+
def __getitem__(self, key):
|
|
3177
|
+
"""
|
|
3178
|
+
Get the colormap with flexible input keys.
|
|
3179
|
+
"""
|
|
3180
|
+
# Sanitize key
|
|
3181
|
+
key = self._translate_deprecated(key)
|
|
3182
|
+
key = self._translate_key(key, mirror=True)
|
|
3183
|
+
shift = key.endswith("_s") and not self._has_item(key)
|
|
3184
|
+
if shift:
|
|
3185
|
+
key = key.rstrip("_s")
|
|
3186
|
+
reverse = key.endswith("_r") and not self._has_item(key)
|
|
3187
|
+
|
|
3188
|
+
if reverse:
|
|
3189
|
+
key = key.rstrip("_r")
|
|
3190
|
+
# Retrieve colormap
|
|
3191
|
+
if self._has_item(key):
|
|
3192
|
+
value = self._cmaps[key].copy()
|
|
3193
|
+
else:
|
|
3194
|
+
raise KeyError(
|
|
3195
|
+
f"Invalid colormap or color cycle name {key!r}. Options are: "
|
|
3196
|
+
+ ", ".join(map(repr, self))
|
|
3197
|
+
+ "."
|
|
3198
|
+
)
|
|
3199
|
+
# Modify colormap
|
|
3200
|
+
if reverse:
|
|
3201
|
+
value = value.reversed()
|
|
3202
|
+
if shift:
|
|
3203
|
+
value = value.shifted(180)
|
|
3204
|
+
return value
|
|
3205
|
+
|
|
3206
|
+
def register(self, cmap, *, name=None, force=False):
|
|
3207
|
+
"""
|
|
3208
|
+
Add the colormap after validating and converting.
|
|
3209
|
+
"""
|
|
3210
|
+
if name is None and cmap.name is None:
|
|
3211
|
+
raise ValueError("Please register the cmap under a string")
|
|
3212
|
+
elif name is None and cmap.name is not None:
|
|
3213
|
+
name = cmap.name
|
|
3214
|
+
name = self._translate_key(name, mirror=False)
|
|
3215
|
+
cmap = _translate_cmap(cmap)
|
|
3216
|
+
# The builtin cmaps are a different class
|
|
3217
|
+
# ultraplot internally uses different classes for the different colormaps
|
|
3218
|
+
if force and name in self._cmaps:
|
|
3219
|
+
# surpress warning if the colormap is not generate by ultraplot
|
|
3220
|
+
if name not in self._builtin_cmaps:
|
|
3221
|
+
print(f"Overwriting {name!r} that was already registered")
|
|
3222
|
+
self._cmaps[name] = cmap.copy()
|
|
3223
|
+
|
|
3224
|
+
|
|
3225
|
+
# Initialize databases
|
|
3226
|
+
_cmap_database = _init_cmap_database()
|
|
3227
|
+
_color_database = _init_color_database()
|
|
3228
|
+
|
|
3229
|
+
# Deprecated
|
|
3230
|
+
(
|
|
3231
|
+
ListedColormap,
|
|
3232
|
+
LinearSegmentedColormap,
|
|
3233
|
+
PerceptuallyUniformColormap,
|
|
3234
|
+
LinearSegmentedNorm,
|
|
3235
|
+
) = warnings._rename_objs( # noqa: E501
|
|
3236
|
+
"0.8.0",
|
|
3237
|
+
ListedColormap=DiscreteColormap,
|
|
3238
|
+
LinearSegmentedColormap=ContinuousColormap,
|
|
3239
|
+
PerceptuallyUniformColormap=PerceptualColormap,
|
|
3240
|
+
LinearSegmentedNorm=SegmentedNorm,
|
|
3241
|
+
)
|