11

Is there a way in Plotly to access colormap colours at any value along its range?

I know I can access the defining colours for a colourscale from

plotly.colors.PLOTLY_SCALES["Viridis"]

but I am unable to find how to access intermediate / interpolated values.

The equivalent in Matplotlib is shown in this question. There is also another question that address a similar question from the colorlover library, but neither offers a nice solution.

JP-Ellis
  • 407
  • 5
  • 15

6 Answers6

15

Plotly does not appear to have such a method, so I wrote one:

import plotly.colors

def get_continuous_color(colorscale, intermed):
    """
    Plotly continuous colorscales assign colors to the range [0, 1]. This function computes the intermediate
    color for any value in that range.

    Plotly doesn't make the colorscales directly accessible in a common format.
    Some are ready to use:
    
        colorscale = plotly.colors.PLOTLY_SCALES["Greens"]

    Others are just swatches that need to be constructed into a colorscale:

        viridis_colors, scale = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Viridis)
        colorscale = plotly.colors.make_colorscale(viridis_colors, scale=scale)

    :param colorscale: A plotly continuous colorscale defined with RGB string colors.
    :param intermed: value in the range [0, 1]
    :return: color in rgb string format
    :rtype: str
    """
    if len(colorscale) < 1:
        raise ValueError("colorscale must have at least one color")

    if intermed <= 0 or len(colorscale) == 1:
        return colorscale[0][1]
    if intermed >= 1:
        return colorscale[-1][1]

    for cutoff, color in colorscale:
        if intermed > cutoff:
            low_cutoff, low_color = cutoff, color
        else:
            high_cutoff, high_color = cutoff, color
            break

    # noinspection PyUnboundLocalVariable
    return plotly.colors.find_intermediate_color(
        lowcolor=low_color, highcolor=high_color,
        intermed=((intermed - low_cutoff) / (high_cutoff - low_cutoff)),
        colortype="rgb")

The challenge is that the built-in Plotly colorscales are not consistently exposed. Some are defined as a colorscale already, others as just a list of color swatches that must be converted to a color scale first.

The Viridis colorscale is defined with hex values, which the Plotly color manipulation methods don't like, so it's easiest to construct it from swatches like this:

viridis_colors, _ = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Viridis)
colorscale = plotly.colors.make_colorscale(viridis_colors)

get_continuous_color(colorscale, intermed=0.25)
# rgb(58.75, 80.75, 138.25)
Adam
  • 16,808
  • 7
  • 52
  • 98
14

There is a built in method from plotly.express.colors to sample_colorscale which would provide the color samples:

from plotly.express.colors import sample_colorscale

import plotly.graph_objects as go
import numpy as np

x = np.linspace(0, 1, 25)
c = sample_colorscale('jet', list(x))

fig = go.FigureWidget()
fig.add_trace(
    go.Bar(x=x, y=y, marker_color=c)
)
fig.show()

See the output figure -> sampled_colors

matthias
  • 141
  • 1
  • 6
7

This answer extend the already good one provided by Adam. In particular, it deals with the inconsistency of Plotly's color scales.

In Plotly, you specify a built-in color scale by writing colorscale="name_of_the_colorscale". This suggests that Plotly already has a built-in tool that somehow convert the color scale to an appropriate value and is capable of dealing with these inconsistencies. By searching Plotly's source code we find the useful ColorscaleValidator class. Let's see how to use it:

def get_color(colorscale_name, loc):
    from _plotly_utils.basevalidators import ColorscaleValidator
    # first parameter: Name of the property being validated
    # second parameter: a string, doesn't really matter in our use case
    cv = ColorscaleValidator("colorscale", "")
    # colorscale will be a list of lists: [[loc1, "rgb1"], [loc2, "rgb2"], ...] 
    colorscale = cv.validate_coerce(colorscale_name)
    
    if hasattr(loc, "__iter__"):
        return [get_continuous_color(colorscale, x) for x in loc]
    return get_continuous_color(colorscale, loc)
        

# Identical to Adam's answer
import plotly.colors
from PIL import ImageColor

def get_continuous_color(colorscale, intermed):
    """
    Plotly continuous colorscales assign colors to the range [0, 1]. This function computes the intermediate
    color for any value in that range.

    Plotly doesn't make the colorscales directly accessible in a common format.
    Some are ready to use:
    
        colorscale = plotly.colors.PLOTLY_SCALES["Greens"]

    Others are just swatches that need to be constructed into a colorscale:

        viridis_colors, scale = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Viridis)
        colorscale = plotly.colors.make_colorscale(viridis_colors, scale=scale)

    :param colorscale: A plotly continuous colorscale defined with RGB string colors.
    :param intermed: value in the range [0, 1]
    :return: color in rgb string format
    :rtype: str
    """
    if len(colorscale) < 1:
        raise ValueError("colorscale must have at least one color")

    hex_to_rgb = lambda c: "rgb" + str(ImageColor.getcolor(c, "RGB"))

    if intermed <= 0 or len(colorscale) == 1:
        c = colorscale[0][1]
        return c if c[0] != "#" else hex_to_rgb(c)
    if intermed >= 1:
        c = colorscale[-1][1]
        return c if c[0] != "#" else hex_to_rgb(c)

    for cutoff, color in colorscale:
        if intermed > cutoff:
            low_cutoff, low_color = cutoff, color
        else:
            high_cutoff, high_color = cutoff, color
            break

    if (low_color[0] == "#") or (high_color[0] == "#"):
        # some color scale names (such as cividis) returns:
        # [[loc1, "hex1"], [loc2, "hex2"], ...]
        low_color = hex_to_rgb(low_color)
        high_color = hex_to_rgb(high_color)

    return plotly.colors.find_intermediate_color(
        lowcolor=low_color,
        highcolor=high_color,
        intermed=((intermed - low_cutoff) / (high_cutoff - low_cutoff)),
        colortype="rgb",
    )

At this point, all you have to do is:

get_color("phase", 0.5)
# 'rgb(123.99999999999999, 112.00000000000001, 236.0)'

import numpy as np
get_color("phase", np.linspace(0, 1, 256))
# ['rgb(167, 119, 12)',
#  'rgb(168.2941176470588, 118.0078431372549, 13.68235294117647)',
#  ...

Edit: improvements to deal with special cases.

Davide_sd
  • 10,578
  • 3
  • 18
  • 30
0

The official reference explains. Here

import plotly.express as px

print(px.colors.sequential.Viridis)
['#440154', '#482878', '#3e4989', '#31688e', '#26828e', '#1f9e89', '#35b779', '#6ece58', '#b5de2b', '#fde725']

print(px.colors.sequential.Viridis[0])
#440154
r-beginners
  • 31,170
  • 3
  • 14
  • 32
  • As I said in my original post, I know how to access the individual colours. What I'm looking for is whether plotly allows access to an arbitrary point on the continuous colour scales (by interpolating between two if the colours above). – JP-Ellis Jul 03 '20 at 13:21
0

You may want to checkout plotly.colors.sample_colorscale (see docs here). It allows you to specify low and high values, and can also handle lists of sample points to convert to interpolated colors. Note, however, that a list will be returned even if you specify a single sample point.

As an example:

plotly.colors.sample_colorscale(plotly.colors.sequential.Viridis, samplepoints=0.25)

returns ['rgb(59, 81, 138)'], or

plotly.colors.sample_colorscale(plotly.colors.sequential.Viridis, samplepoints=0.25, colortype='tuple')

returns [(0.23039215686274508, 0.31666666666666665, 0.542156862745098)] if you prefer floats.

stroblme
  • 896
  • 5
  • 4
-2
import plotly.express as px
color_list = list(name_of_color_scale)

# name_of_color_scale could be any in-built colorscale like px.colors.qualitative.D3.

Output:
color_list = 
['#1F77B4',
 '#FF7F0E',
 '#2CA02C',
 '#D62728',
 '#9467BD',
 '#8C564B',
 '#E377C2',
 '#7F7F7F',
 '#BCBD22',
 '#17BECF']

BetterCallMe
  • 636
  • 7
  • 15
  • I doing see how this answer is any different to the one posted by @r-beginners, and as with their answer, this does not actually address the original question. – JP-Ellis Feb 14 '23 at 09:03