From 7b6a594a3c64204ad6d4498cb69ac7fa00096e92 Mon Sep 17 00:00:00 2001 From: Carl Andersson Date: Wed, 7 Apr 2021 11:17:45 +0200 Subject: [PATCH 1/5] Adds function to sample colorscales --- .../plotly/_plotly_utils/colors/__init__.py | 51 +++++++++++++++++++ .../python/plotly/plotly/colors/__init__.py | 1 + 2 files changed, 52 insertions(+) diff --git a/packages/python/plotly/_plotly_utils/colors/__init__.py b/packages/python/plotly/_plotly_utils/colors/__init__.py index b7669ce4d21..be4c75bddf4 100644 --- a/packages/python/plotly/_plotly_utils/colors/__init__.py +++ b/packages/python/plotly/_plotly_utils/colors/__init__.py @@ -806,3 +806,54 @@ def named_colorscales(): from _plotly_utils.basevalidators import ColorscaleValidator return [c for c in ColorscaleValidator("", "").named_colorscales] + + +def sample_colorscale(colorscale, samplepoints, low=0.0, high=1.0, colortype="rgb"): + """ + Samples a colorscale at specific points. + + Interpolates between colors in a colorscale to find the specific colors + corresponding to the specified sample values. The colorscale can be specified + as a list of `[scale, color]` pairs, as a list of colors, or as a named + plotly colorscale. The samplepoints can be specefies an iterable of specific + points in the range [0.0, 1.0], or as an integer number of points which will + 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. + """ + from bisect import bisect_left + + try: + validate_colorscale(colorscale) + except exceptions.PlotlyError: + if isinstance(colorscale, str): + if colorscale in PLOTLY_SCALES: + colorscale = PLOTLY_SCALES[colorscale] + else: + raise exceptions.PlotlyError( + "If your colors variable is a string, it must be a " + "Plotly scale, an rgb color or a hex color." + ) + else: + colorscale = make_colorscale(colorscale) + + scale = colorscale_to_scale(colorscale) + validate_scale_values(scale) + colors = colorscale_to_colors(colorscale) + colors = validate_colors(colors, colortype="tuple") + + if isinstance(samplepoints, int): + samplepoints = [ + low + idx / (samplepoints - 1) * (high - low) for idx in range(samplepoints) + ] + elif isinstance(samplepoints, float): + samplepoints = [samplepoints] + + sampled_colors = [] + for point in samplepoints: + high = bisect_left(scale, point) + low = high - 1 + interpolant = (point - scale[low]) / (scale[high] - scale[low]) + sampled_color = find_intermediate_color(colors[low], colors[high], interpolant) + sampled_colors.append(sampled_color) + return validate_colors(sampled_colors, colortype=colortype) diff --git a/packages/python/plotly/plotly/colors/__init__.py b/packages/python/plotly/plotly/colors/__init__.py index 2e3dc753e6b..3fb25825b3f 100644 --- a/packages/python/plotly/plotly/colors/__init__.py +++ b/packages/python/plotly/plotly/colors/__init__.py @@ -36,6 +36,7 @@ "label_rgb", "make_colorscale", "n_colors", + "sample_colorscale", "unconvert_from_RGB_255", "unlabel_rgb", "validate_colors", From 1b419ad624b08510abb4c3ec999a56cb05c0c040 Mon Sep 17 00:00:00 2001 From: Carl Andersson Date: Fri, 7 May 2021 09:52:12 +0200 Subject: [PATCH 2/5] Minor fixes A typo in the docstring and a confusing error message. --- packages/python/plotly/_plotly_utils/colors/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/python/plotly/_plotly_utils/colors/__init__.py b/packages/python/plotly/_plotly_utils/colors/__init__.py index be4c75bddf4..2cc2c6a9c7e 100644 --- a/packages/python/plotly/_plotly_utils/colors/__init__.py +++ b/packages/python/plotly/_plotly_utils/colors/__init__.py @@ -815,7 +815,7 @@ def sample_colorscale(colorscale, samplepoints, low=0.0, high=1.0, colortype="rg Interpolates between colors in a colorscale to find the specific colors corresponding to the specified sample values. The colorscale can be specified as a list of `[scale, color]` pairs, as a list of colors, or as a named - plotly colorscale. The samplepoints can be specefies an iterable of specific + plotly colorscale. The samplepoints can be specefied as an iterable of specific points in the range [0.0, 1.0], or as an integer number of points which will 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 @@ -831,8 +831,8 @@ def sample_colorscale(colorscale, samplepoints, low=0.0, high=1.0, colortype="rg colorscale = PLOTLY_SCALES[colorscale] else: raise exceptions.PlotlyError( - "If your colors variable is a string, it must be a " - "Plotly scale, an rgb color or a hex color." + "If your colorscale variable is a string, " + "it must be a Plotly scale." ) else: colorscale = make_colorscale(colorscale) From e774d3fbe7a6296534a8f9b7279d067bb7172e09 Mon Sep 17 00:00:00 2001 From: Carl Andersson Date: Fri, 7 May 2021 10:56:53 +0200 Subject: [PATCH 3/5] Adds tests A simple test that checks the consistency of sampling a colorscale on the actual definition points, as well as sampling a very simple scale at simple points. --- .../test_core/test_colors/test_colors.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py b/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py index e382e9bfd7d..dbd197dea2a 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py +++ b/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py @@ -137,3 +137,22 @@ def test_make_colorscale(self): self.assertRaisesRegexp( PlotlyError, pattern2, colors.make_colorscale, color_list2, scale ) + + def test_sample_colorscale(self): + + # test that sampling a colorscale at the defined points returns the same + defined_colors = colors.sequential.Inferno + sampled_colors = colors.sample_colorscale(defined_colors, len(defined_colors), colortype="rgb") + defined_colors_rgb = colors.convert_colors_to_same_type(defined_colors, colortype='rgb')[0] + self.assertEqual(sampled_colors, defined_colors_rgb) + + # test sampling an easy colorscale that goes [red, green, blue] + defined_colors = ['rgb(255,0,0)', 'rgb(0,255,0)', 'rgb(0,0,255)'] + samplepoints = [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0] + expected_output = [ + (1.0, 0.0, 0.0), (0.75, 0.25, 0.0), (0.5, 0.5, 0.0), + (0.25, 0.75, 0.0), (0.0, 1.0, 0.0), (0.0, 0.75, 0.25), + (0.0, 0.5, 0.5), (0.0, 0.25, 0.75), (0.0, 0.0, 1.0) + ] + output = colors.sample_colorscale(defined_colors, samplepoints, colortype="tuple") + self.assertEqual(expected_output, output) From fb2c1396ddbe4f37c2b8456a3a15eae3e060bc99 Mon Sep 17 00:00:00 2001 From: Carl Andersson Date: Fri, 7 May 2021 11:31:49 +0200 Subject: [PATCH 4/5] Code formatting Had some issues with running black locally due to version problems. Now resolved and formatted the test code accordingly :) --- .../test_core/test_colors/test_colors.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py b/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py index dbd197dea2a..fca4fcb0a5d 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py +++ b/packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py @@ -142,17 +142,29 @@ def test_sample_colorscale(self): # test that sampling a colorscale at the defined points returns the same defined_colors = colors.sequential.Inferno - sampled_colors = colors.sample_colorscale(defined_colors, len(defined_colors), colortype="rgb") - defined_colors_rgb = colors.convert_colors_to_same_type(defined_colors, colortype='rgb')[0] + sampled_colors = colors.sample_colorscale( + defined_colors, len(defined_colors), colortype="rgb" + ) + defined_colors_rgb = colors.convert_colors_to_same_type( + defined_colors, colortype="rgb" + )[0] self.assertEqual(sampled_colors, defined_colors_rgb) # test sampling an easy colorscale that goes [red, green, blue] - defined_colors = ['rgb(255,0,0)', 'rgb(0,255,0)', 'rgb(0,0,255)'] + defined_colors = ["rgb(255,0,0)", "rgb(0,255,0)", "rgb(0,0,255)"] samplepoints = [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0] expected_output = [ - (1.0, 0.0, 0.0), (0.75, 0.25, 0.0), (0.5, 0.5, 0.0), - (0.25, 0.75, 0.0), (0.0, 1.0, 0.0), (0.0, 0.75, 0.25), - (0.0, 0.5, 0.5), (0.0, 0.25, 0.75), (0.0, 0.0, 1.0) + (1.0, 0.0, 0.0), + (0.75, 0.25, 0.0), + (0.5, 0.5, 0.0), + (0.25, 0.75, 0.0), + (0.0, 1.0, 0.0), + (0.0, 0.75, 0.25), + (0.0, 0.5, 0.5), + (0.0, 0.25, 0.75), + (0.0, 0.0, 1.0), ] - output = colors.sample_colorscale(defined_colors, samplepoints, colortype="tuple") + output = colors.sample_colorscale( + defined_colors, samplepoints, colortype="tuple" + ) self.assertEqual(expected_output, output) From 1b8ec25f55ee4472176d79045a8bcdedc4c68b5a Mon Sep 17 00:00:00 2001 From: Carl Andersson Date: Mon, 10 May 2021 16:12:34 +0200 Subject: [PATCH 5/5] Handle string inputs with get_colorscale --- packages/python/plotly/_plotly_utils/colors/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/python/plotly/_plotly_utils/colors/__init__.py b/packages/python/plotly/_plotly_utils/colors/__init__.py index 087e3151e19..00856b782f6 100644 --- a/packages/python/plotly/_plotly_utils/colors/__init__.py +++ b/packages/python/plotly/_plotly_utils/colors/__init__.py @@ -854,13 +854,7 @@ def sample_colorscale(colorscale, samplepoints, low=0.0, high=1.0, colortype="rg validate_colorscale(colorscale) except exceptions.PlotlyError: if isinstance(colorscale, str): - if colorscale in PLOTLY_SCALES: - colorscale = PLOTLY_SCALES[colorscale] - else: - raise exceptions.PlotlyError( - "If your colorscale variable is a string, " - "it must be a Plotly scale." - ) + colorscale = get_colorscale(colorscale) else: colorscale = make_colorscale(colorscale)