Colorbar binning based on unknown colorscale

Hello,

I have a question regarding binning, plotly colorbars, and the plotly colorscales.

I’m looking to create colorbars with distinct color binnings, however, I’m also using the in-built colorscales from plotly.

I have seen a few examples regarding binning, but they use customly set color values. I was wondering if there was a simpler way to do so with the preset colorscales, as opposed to having to manually set multiple color combinations for each of the ~90 colorscales.

Thank you,
Claire

Hey @clairew , are you referring to discrete colormaps with that?

I’m referring to this:

1 Like

Hello,

Thank you! Yes, that is similar to what I was referring to. I have been trying to find if there is a way to take the native colorscales (aggrnyl, bluered, etc) of an non-express Scatter plot, and divide them into discrete sections for binning. I will take a look at the link suggested, hopefully I won’t have to resort to creating associated color bins manually for each native scale.

Thank you,
Claire

Like so?

import plotly.graph_objects as go
import numpy as np
import plotly.express as px
from typing import Iterable, List

def discrete_colorscale(num: int, pallete: Iterable)-> List:
    """
    function creates a discrete colorscale for a given number of values
    """
               
    # create spaced numbers between 0 and 1
    # double the values, i.e. 0, 0, 1, 1, 2, 2 
    color_bar_values = [val for val in np.linspace(0, 1, num) for _ in range(2)]

    # for pallete you could use a list of strings such as ['red', 'green'...] or a built-in colormap such as px.colors.qualitative.Alphabet
    # double the values, i.e. 'red', 'red', 'green', 'green'
    discrete_colors = [val for val in pallete for _ in range(2)]

    # combine values and colors, start colors at index 1
    colorscale = [[value, color] for value, color in zip(color_bar_values, discrete_colors[1:])]

    # delete first and last list item
    colorscale.pop(0)
    colorscale.pop(-1)
    return colorscale


# data
points = 10
x = np.arange(points)
y = np.random.randint(0,10,size=points)

# values for the second color variable 
custom = np.arange(100,1100,100)

fig = go.Figure()

# trace 0, this trace defines the "lines" for the markers and it's colors
fig.add_trace(
    go.Scatter(
        x=x,
        y=y,
        showlegend=False,
        mode='markers',
        customdata=custom,
        marker=dict(
            size=24,
            color=custom, #set color equal to a 2nd variable
            colorscale=discrete_colorscale(points+1,px.colors.qualitative.Alphabet),
            showscale=True,
        ),
        hovertemplate='<b>%{customdata}</b><extra></extra>'
    )
)

# trace 1, defines the "inner part" of the markers
fig.add_trace(
    go.Scatter(
        x=x,
        y=y,
        showlegend=False,
        mode='markers',
        marker=dict(
            size=16,
            color=y, #set color equal to a variable
            colorscale='reds', # one of plotly colorscales
            showscale=True,
        ),
    )
)

# move the first colorbar to the left
fig.update_traces({"marker_colorbar_x":-0.15}, selector=0)

fig.update_layout(width=800, height=800)

2 Likes

@clairew As I understood you need a discrete colorscale derived from a continuous one.
Unlike the matplotlib, a plotly continuous colorscale is defined by a number of colors much smaller than 256 (the usual number of colors in a matplotlib colormap).
To get a binned colorscale from a plotly continuous colorscale we have to find the colors of the new
discrete colorscale from those of the continuous one, through linear interpolation, using the function np.interp.

We can get the colors of a continuous colorscale by calling:
px.colors.sequential.ColorscaleName or px.colors.diverging.ColorscaleName.
As we can notice below these colors are either hex or rgb colors, and their number differ from colorscale to colorscale.

I define below a function that display the swatches corresponding to colors returned by px.colors…

from  plotly.colors import unlabel_rgb, hex_to_rgb
import plotly.graph_objects as go
import plotly.express as px
import numpy as np

def colorscale_swatches(seq):
    data = [go.Bar(x=[k], y=[1], marker_color=c, hovertext=seq[k]) for k, c in enumerate(seq)]
    fig = go.Figure(data=data)
    fig.update_layout(bargap=0.02, template="none",
                      showlegend=False,
                      xaxis=dict(showticklabels=False, showgrid=False),
                      yaxis_showticklabels=False,    
                      height=250) 
    return fig

Let us plot the swatches corresponding to Viridis:

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

fig1 = colorscale_swatches(seq1)

Notice that we have 10 swatches, and their color is given in hex.

Now take the colors for the colormap cmocean.matter

seq2 = px.colors.sequential.matter
seq2
['rgb(253, 237, 176)',
 'rgb(250, 205, 145)',
 'rgb(246, 173, 119)',
 'rgb(240, 142, 98)',
 'rgb(231, 109, 84)',
 'rgb(216, 80, 83)',
 'rgb(195, 56, 90)',
 'rgb(168, 40, 96)',
 'rgb(138, 29, 99)',
 'rgb(107, 24, 93)',
 'rgb(76, 21, 80)',
 'rgb(47, 15, 61)']

In this case there are 12 rgb colors, and their swatches are:

fig2 = colorscale_swatches(seq2)

But in our projects we need a discrete colorscale derived from a continuous one containing different number of colors.
The following function does the job, i.e. from a given sequence of colors derived from a continuous colorscale it returns a discrete colorscale of nr_swatches colors.

def binned_colorscale(seq, nr_swatches=5):
    """
    seq: a sequence of hex colors or rgb colors returned by  px.colors.sequential.ColorscaleName
     or px.colors.diverging.ColorscaleName
    nr_swatches: the number of swatches for the discrete colorscale, derived from ColorscaleName     
    """
    
    if seq[0][0] == '#':
        arr_colors=np.array([hex_to_rgb(s) for s in seq])/255
    elif seq[0][0:3] == 'rgb':
        arr_colors = np.array([unlabel_rgb(s) for s in seq])/255 
    else:
        raise ValueError("a plotly colorscale is given either with hex colors or as rgb colors")
    n = len(seq)
    svals = [k/(n-1) for k in range(n)] #the scale values corresponding to the colors in seq
    grid = [k/(nr_swatches-1) for k in range(nr_swatches)]# define the scale values corresponding nr_swatches
    r, g, b = [np.interp(grid, svals, arr_colors[:, k]) for k in range(3)]  #np.interp interpolates linearly
    cmap_arr = np.clip(np.vstack((r, g, b)).T, 0, 1)
    new_colors = np.array(cmap_arr*255, dtype=int)
    discrete_colorscale = []
    N = len(new_colors+1)
    for k in range(N):
        discrete_colorscale.extend([[k/N, f'rgb{tuple(new_colors[k])}'], 
                                    [(k+1)/N,  f'rgb{tuple(new_colors[k])}']]) 
    return discrete_colorscale 

example:

dcolorsc = binned_colorscale(seq2, nr_swatches=7) 
[[0.0, 'rgb(253, 237, 176)'],
 [0.14285714285714285, 'rgb(253, 237, 176)'],
 [0.14285714285714285, 'rgb(246, 178, 123)'],
 [0.2857142857142857, 'rgb(246, 178, 123)'],
 [0.2857142857142857, 'rgb(234, 120, 88)'],
 [0.42857142857142855, 'rgb(234, 120, 88)'],
 [0.42857142857142855, 'rgb(205, 67, 86)'],
 [0.5714285714285714, 'rgb(205, 67, 86)'],
 [0.5714285714285714, 'rgb(158, 36, 97)'],
 [0.7142857142857143, 'rgb(158, 36, 97)'],
 [0.7142857142857143, 'rgb(101, 23, 90)'],
 [0.8571428571428571, 'rgb(101, 23, 90)'],
 [0.8571428571428571, 'rgb(47, 15, 61)'],
 [1.0, 'rgb(47, 15, 61)']]

I hope that this is what you required. The colorbar associated to such a colorscale is also binned. Now to use such a discrete colorscale of `nr_swatches’ colors your data values should be binned as follows:

boundary_vals=[minval+k*(maxval-minval)/nr_swatches for k in range(nr_swatches+1]
1 Like

Hello,

Thanks to both of you for your assistance. I was able to create the method I needed based on your contributions. Not knowing that plotly express could sequentialize the colorscales had me set back.

Thank you again,
Claire