Search code examples
pythonplotly-python

Access Color from Plotly Color Scale


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.


Solution

  • 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.