Colors for discrete ranges in heatmaps

Iā€™m trying to make a heatmap using different colors for discreet ranges of data, using Plotly in python/pandas. My colleague sent me an image of the color ranges she would like used for the data.


I used the ā€œCustom Discretized Heatmap Colorscaleā€ example at https://plot.ly/python/colorscales/ but rather than discrete bars, the heatmap has a range of colors.

Here is the script I am using:
def makePlotlyHeatmap(input_df, output_file):
ā€˜ā€™ā€˜make a Plotly heatmap from a dataframeā€™ā€™ā€™

    # initialize for offline use
    offline.init_notebook_mode(connected=True)

    
    # set layout options
    layout = go.Layout(
    autosize=True,
    height=1200,
    width=1200,
    margin=go.Margin(
        l=500,
        r=200,
        b=100,
        t=100,
        pad=4
    )) 
    
    # create heatmap file
    heatmap = input_df.iplot(kind='heatmap',
        layout=layout,
        colors=[
            # Let 0-10.0 fold change be white:
            [10.0, 'cmyk(0,0,0,0)'],
            # Let 10.0-50.0 fold change be orange:
            [10.0, 'cmyk(0,61,100,0)'],
            [50.0, 'cmyk(0,61,100,0)'],
            # Let 50.0-100.0 fold change be red:
            [50.0, 'cmyk(88,77,0,0)'],
            [100.0, 'cmyk(88,77,0,0)'],
            # Let 100.0 and greater fold change be blue:
            [100, 'cmyk(24,100,100,22)']],
        asFigure=True)

    
    # write heatmap to file
    offline.plot(heatmap, filename=output_file, auto_open=True)

makePlotlyHeatmap(df_data, "output.html")

The data frame (df_data) contains a read-in csv.

I have tried ā€œcolorscaleā€ in place of ā€œcolorsā€ but that gives the error ā€œAttributeError: ā€˜listā€™ object has no attribute ā€˜lowerā€™ā€.

Thanks for your assistance!

1 Like

@heff The heatmap trace selects the color from a colorscale, according to the normalized values in the lists of lists z (or a numpy array of shape (m,n)).

The discrete colorscale should be defined as follows:

  • find the max value for z, let us say that it is 125:

  • map the interval [0, max_val] to [0,1] by t--> t/max_val

  • mapping your thresholds [0, 10, 50, 100, 125] to [0,1], you get
    [0, 0.08, 0.4, 0.8, 1].

  • define the discrete colorscale:

    my_colorsc=[[0, 'rgb(255,255,255)'],#white
                [0.08, 'rgb(255,255,255'], 
                [0.08, 'rgb(255,165,0)'],#orange
                [0.4, 'rgb(255,165,0)'],
                [0.4, 'rgb(255,0,0'], #red
                [0.8, 'rgb(255,0,0)'],
                [0.8, 'rgb(0,0,255)'], #blue
                [1, 'rgb(0,0,255)']]
    

Plotly doesnā€™t recognize cmyk colors.

3 Likes

@empet - thanks for this, it is a very handy guide. Do you have any inclination if an ā€œabsoluteā€ colorscale is possible? My use case is that Iā€™m reading data in from a DB, and showing on a scatter with a discrete colorscale based off a value in the DB. The rub is that Iā€™m querying from the DB and limiting to one client each time. Each client has a different range of values to color based on (one might be from 30-70, another might be from 45-55, 16-62, etcā€¦ you get the idea). Iā€™d like the colorscales to take to the absolute range like @heff was originaly trying to code it.

I welcome any thoughts hereā€¦thx

1 Like

@jezlax The normalization of range ends, above, was necessary to define the discrete colorscale. The original values are also normalized by plotly.js in order to map them to the coresponding color.

You only have to insert a suitable text as a colorbar ticktext (see the image below).

I detailed all ingredients needed to plot a heatmap with a discrete colorscale in this notebook https://plot.ly/~empet/15229.

1 Like

thanks @emp for the work. the only thing iā€™m running into is that plotly still normalized my Z inputs, when I want them to be treated absolutely. I provided an example here: https://plot.ly/~coryjez/3

Basically the scale is from 0 to 100, and I feed in say values between 34 and 60. It normalizes those values I feed in to be 0-100 now and plots them as such. Basically, Iā€™ve created a percentile score for my client on the backend, and just want to place that percentile score on the range Iā€™ve provided in that notebook. Itā€™s seeming like this wonā€™t be possible in plotlyā€™s current manifestation of this, but wanted to see what you thought. thanks for the help.

@jezlax, you can force the inputs to be treated absolutely by setting a zmin and zmax value in your heatmap.

heatmap = go.Heatmap(z=z, 
                     colorscale = dcolorsc,
                     zmin=0,
                     zmax=100,
                     colorbar = dict(thickness=25, 
                                     tickvals=tickvals, 
                                     ticktext=ticktext))

@michaelbabyn - right, those are super helpful! the rub i really run into here is that I canā€™t use those arguments in a go.Scatter() scenario, trying to plot some points and color by the scale I referenced in my post.

@jezlax, In that case you would use cmax and cmin like in this example https://plot.ly/python/colorscales/#colorscale-for-scatter-plots

Edited to fix example link.

@jezlax

I really donā€™t understand your complaint. Why did you say that data with range in some subinterval ([34, 60]) of the interval [min(bvals), max(bvals)] (within the function discrete_colorscale() body
bvals[0] is the min value, and bvals[-1] is the max value, after sorting) is normalized with respect to that subinterval? It is normalized with respect to
min and max bvals. If you set some cmin, cmax values in a go.Scatter instance that are different from min(bvals), max(bvals), the associated color can be wrong
because the discrete colorscale was defined such that to map the interval [min (bvals), max(bvals)] to [0,1], [not cmin, cmax].

Running your notebook (with Python 3) and hovering some points in the heatmap led to right displayed values as you can see in these images.

@empet - not meant to be a complaint at all. i didnā€™t realize that cmin/cmax arguments were available in go.Scatter() the same way that zmin/zmax are in go.Heatmap(). It looks like this solution scales perfectly, as you mentioned. Without the cmin/cmax being set equal to the range, in my case [0,100]ā€¦ it did normalize on the subrange, but setting those arguments to 0, 100, respectively seems to work. appreciate the back and forth and the help.

2 Likes

Hi @empet, I am using custom colorscale for heatmap, but I am getting different color, shouldnā€™t the plot be giving limegreen and tomato color?

colorscale= [[0, 'whitesmoke'], [0.33, 'limegreen'], [0.67, 'tomato'], [1, 'teal']]

values = [[0,0,0,0,0,.67,.67,.67,.67,0,0,0,0,0,.33,.33,.33,.33,0,0,0,0],
          [0,0,0,0,0,.67,.67,.67,.67,0,0,0,0,0,.33,.33,.33,.33,0,0,0,0]
         ]

fig_bar = go.Figure(data=go.Heatmap(z=values, colorscale=colorscale,text=values,
                                     hoverinfo ='text',showscale=False)
                    
            )
                    

fig_bar.show()

Hi @dewshrs,

To understand why you get a heatmap colored like this you have to know how the colormapping is performed, i.e the definition of a heatmap:

  • the initial data are an array of z-values and a Plotly colorscale, consisting in a scale= [0, 0.33, 0.67, 1] (in your example), and the corresponding color names or color codes.

  • the z-values are normalized by plotlyjs, via the mapping val -->n_val=(val-z_min)/(z_max-z_min).
    In your case this mapping is: val--> n_val =(val-0)/(0.67-0)

  • to each normalized n_val one associates the corresponding color in the colorscale if n_val is an element in the list scale. Otherwise, the corresponding color is deduced by linear interpolation.

I think you expected to get mapped z=0.67, to the color 'tomato'. But by the above algorithm it isnā€™t because the color assigned to this z-value is derived as follows:

  • n_val =(0.67-0)/(0.67-0)=1
  • the color corresponding to 1 is teal.

If you want to associate to 0.67 the color tomato, just set in the Heatmap definition, zmin=0, zmax =1
otherwise (by default) plotly.js takes zmin = min(z-values), zmax= max(z-values).

1 Like

thank you so much @empet.

Hi @empet empet

What would be the proper definition if I wanted to use this with ff.create_annotated_heatmap ?
Creating the tuples without pre-pending rgb is easier but how would you pass the intervals toff.create_annotated_heatmap?

Hi @mycarta,

To use such a discrete colorscale for an annotated heatmap, define the colorscale like in the notebook at the link above (https://chart-studio.plotly.com/~empet/15229/heatmap-with-a-discrete-colorscale/#/) , based on your values in the array, z. If dcolorsc is your colorscale, then an annotated heatmap is defined as follows:

import plotly.figure_factory as ff
z1=  np.random.randint(bvals[0],  bvals[-1]+1, size=(8, 8))  #bvals are defined in the notebook 
fig1 = ff.create_annotated_heatmap(z1, colorscale=dcolorsc)
fig1.update_traces(showscale=True, colorbar = dict(thickness=25, 
                                     tickvals=tickvals, 
                                     ticktext=ticktext))
fig1.update_layout(width=500, height=500)

annotated_heat_d

1 Like

Outstanding, thank you.
Between what I had and what you showcased at that link, that got me really all the way to 90%. In actual facts I had my own code generating the colorscale, but it created a list with what you call bvals at that link, followed by a tuple with RGB values, like:

[[0.0, (64, 0, 75)],
 [0.03, (96, 18,100)]
...
...

So, all I needed to add was a bit that converted the tuple to hex values:
'#%02x%02x%02x' % (tuple(int(x) for x in(rgb[i])))

1 Like

Hello ,
I am pretty new to plotly and trying to generate a heatmap , where all positive values will have red color, all negative values will be of green color, zero will have grey. Essentially like ā€œIf value >0 color =Green, else if value <0 color= red, else color=greyā€. There can be only these three colors in the heatmap. Can this be done in plotly heatmap?
Thanks in advance

Hi @jayeetamukherjee,
Your case needs the min&max of the subset of negative, respectively positive values:

import plotly.graph_objects as go
import numpy as np

def three_colorscale(z, colors):
    if len(colors) !=3:
        raise ValueError("")
    neg=z[np.where(z<0)]
    pos=z[np.where(z>0)]    
    a, c = neg.min(), neg.max()
    d, b = pos.min(), pos.max()
    bvals= [a, c/2, d/2, b]
    nvals = [(v-bvals[0])/(bvals[-1]-bvals[0]) for v in bvals] 
    dcolorscale = []
    for k in range(len(colors)):
        dcolorscale.extend([[nvals[k], colors[k]], [nvals[k+1], colors[k]]])
    return dcolorscale     

z= np.random.randint(-5, 7, size=(8,8))
pl_colorscale= three_colorscale(z, ["#19BD1B", "#C0C0C0", "#DC3714"])

fig= go.Figure(go.Heatmap(z=z, colorscale=pl_colorscale, xgap=1, ygap=1, colorbar_thickness=24))
fig.update_layout(width=400, height=400)

heat-three-colors

Worked like magic. Thank you so much.

Hey @empet

It worked on my side; Thank you very much!!

1 Like