diff --git a/CHANGELOG.md b/CHANGELOG.md index c92c3ea6d64..3f7a6c4930f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Fixed +- Fix single-color requests in `n_colors` and `sample_colorscale` raising `ZeroDivisionError` [[#5805](https://github.com/plotly/plotly.py/issues/5805)]. - Fix `mpl_to_plotly` tick marker mirroring, axis positioning, and trace visibility for twinned and overlaid subplots (such as `twinx` and `twiny`) [[#5310](https://github.com/plotly/plotly.py/pull/5310)], with thanks to @robertoffmoura for the contribution! - Fix `mpl_to_plotly` losing matplotlib axis line colors by exporting the color of each axis's displayed spine to the plotly x and y axis `linecolor` [[#5311](https://github.com/plotly/plotly.py/pull/5311)], with thanks to @robertoffmoura for the contribution! - Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#5691](https://github.com/plotly/plotly.py/pull/5691)], with thanks to @hb1915 for the contribution! diff --git a/_plotly_utils/colors/__init__.py b/_plotly_utils/colors/__init__.py index a9689101e6b..524afecff59 100644 --- a/_plotly_utils/colors/__init__.py +++ b/_plotly_utils/colors/__init__.py @@ -677,18 +677,20 @@ def n_colors(lowcolor, highcolor, n_colors, colortype="tuple"): which form the intermediate colors between lowcolor and highcolor from linearly interpolating through RGB space. If colortype is 'rgb' the function will return a list of colors in the same form. + When one color is requested, the result contains only lowcolor. """ if colortype == "rgb": # convert to tuple lowcolor = unlabel_rgb(lowcolor) highcolor = unlabel_rgb(highcolor) + intervals = max(n_colors - 1, 1) diff_0 = float(highcolor[0] - lowcolor[0]) - incr_0 = diff_0 / (n_colors - 1) + incr_0 = diff_0 / intervals diff_1 = float(highcolor[1] - lowcolor[1]) - incr_1 = diff_1 / (n_colors - 1) + incr_1 = diff_1 / intervals diff_2 = float(highcolor[2] - lowcolor[2]) - incr_2 = diff_2 / (n_colors - 1) + incr_2 = diff_2 / intervals list_of_colors = [] def _constrain_color(c): @@ -874,6 +876,7 @@ def sample_colorscale(colorscale, samplepoints, low=0.0, high=1.0, colortype="rg be spaced equally between the low value (default 0.0) and the high value (default 1.0). The output is a list of colors, formatted according to the specified colortype. + When one point is requested, the color at low is returned. """ from bisect import bisect_left @@ -891,8 +894,9 @@ def sample_colorscale(colorscale, samplepoints, low=0.0, high=1.0, colortype="rg colors = validate_colors(colors, colortype="tuple") if isinstance(samplepoints, int): + intervals = max(samplepoints - 1, 1) samplepoints = [ - low + idx / (samplepoints - 1) * (high - low) for idx in range(samplepoints) + low + idx / intervals * (high - low) for idx in range(samplepoints) ] elif isinstance(samplepoints, float): samplepoints = [samplepoints] diff --git a/tests/test_core/test_colors/test_colors.py b/tests/test_core/test_colors/test_colors.py index 9f53344bd8d..bb44977bcab 100644 --- a/tests/test_core/test_colors/test_colors.py +++ b/tests/test_core/test_colors/test_colors.py @@ -1,5 +1,7 @@ from unittest import TestCase +import pytest + from plotly.exceptions import PlotlyError import plotly.colors as colors @@ -224,3 +226,65 @@ def test_n_colors(self): ] self.assertEqual(generated_colorscale, expected_colorscale) + + +@pytest.mark.parametrize( + "colorscale,low,high,colortype,expected", + [ + ("Viridis", 0.0, 1.0, "rgb", ["rgb(68, 1, 84)"]), + ("Viridis_r", 0.0, 1.0, "rgb", ["rgb(253, 231, 37)"]), + (["#000000", "#ffffff"], 0.0, 1.0, "tuple", [(0.0, 0.0, 0.0)]), + ( + [[0.0, "#ff0000"], [1.0, "#0000ff"]], + 0.25, + 0.75, + "tuple", + [(0.75, 0.0, 0.25)], + ), + (["#000000", "#ffffff"], 0.5, 0.5, "tuple", [(0.5, 0.5, 0.5)]), + (["#000000", "#ffffff"], 1.0, 0.0, "rgb", ["rgb(255, 255, 255)"]), + ], +) +def test_sample_colorscale_single_color(colorscale, low, high, colortype, expected): + assert ( + colors.sample_colorscale(colorscale, 1, low=low, high=high, colortype=colortype) + == expected + ) + + +@pytest.mark.parametrize( + "lowcolor,highcolor,colortype,expected", + [ + ((0, 0, 0), (1, 1, 1), "tuple", [(0.0, 0.0, 0.0)]), + ("rgb(255,0,0)", "rgb(0,255,0)", "rgb", ["rgb(255.0, 0.0, 0.0)"]), + ], +) +def test_n_colors_single_color(lowcolor, highcolor, colortype, expected): + assert colors.n_colors(lowcolor, highcolor, 1, colortype=colortype) == expected + + +@pytest.mark.parametrize( + "count,expected", + [ + (0, []), + (2, [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]), + (3, [(0.0, 0.0, 0.0), (0.5, 0.5, 0.5), (1.0, 1.0, 1.0)]), + ], +) +def test_sample_colorscale_empty_and_multiple_colors(count, expected): + assert ( + colors.sample_colorscale(["#000000", "#ffffff"], count, colortype="tuple") + == expected + ) + + +@pytest.mark.parametrize( + "count,expected", + [ + (0, []), + (2, [(0.0, 0.0, 0.0), (1.0, 1.0, 1.0)]), + (3, [(0.0, 0.0, 0.0), (0.5, 0.5, 0.5), (1.0, 1.0, 1.0)]), + ], +) +def test_n_colors_empty_and_multiple_colors(count, expected): + assert colors.n_colors((0, 0, 0), (1, 1, 1), count) == expected