Conditional Formatting For hv.HeatMap Colours

Hi There,

I’m wondering if it is possible to apply a custom colouring to a hv.HeatMap that is based on conditions within the data. Please see example code below:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from datetime import datetime
import holoviews as hv
from holoviews import opts
from bokeh.resources import INLINE
from holoviews import dim
hv.extension('bokeh', 'matplotlib')

pd.options.plotting.backend = 'holoviews'

green = '#00FF00'
amber = '#FFFF00'
red = '#FF0000'

Data = [['A', 'Foo', 0.2] , ['B', 'Bar', 0.9], ['C', 'Cat', 0.7]]
df = pd.DataFrame(Data, columns = ['Name', 'Category', 'Value'])

df['colors'] = df.apply(lambda row: green if row['Value'] >= 0.9 else
                                    amber if row['Value'] < 0.9 and row['Value'] >= 0.7 else
                                    red if row['Value'] < 0.7 else '#8A2BE2', axis = 1)

df_hm = hv.HeatMap(df,kdims=['Category','Name'], vdims=['Value', 'colors']).opts(width=900, height=400, color = hv.dim('colors'),  tools=['hover'])

When I run this code, the plot defaults to the standard cmap, but when I hover over the data I can see the colours that match my conditional formatting. Is there a way of making is so that I can only see the conditional colouring. I’ve tried a few different options, including passing dim into cmap, and even a list of colours into cmap. When I pass the list of colours into cmap the order of the colours does not match up with the data at all, and appears random.

I’ve attached a picutre of what I am seeing from the data I am actually working on (the hovering behaviour), I cannot share the underlying data to that example (apologies).

Appreciate any help on a fix or alternative solution.

Kind Regards,

I’m unclear if you’re wanting separate cmaps for different conditions, or if you just want a single different color for the different conditions. If all you need is the latter, you can do:

green = '#00FF00'
amber = '#FFFF00'
red = '#FF0000'

Data = [['A', 'Foo', 0.2] , ['B', 'Bar', 0.9], ['C', 'Cat', 0.7]]
df = pd.DataFrame(Data, columns = ['Name', 'Category', 'Value'])

levels = [0, 0.7, 0.9, 1]
colors = [green, amber, red]

df_hm = hv.HeatMap(df, kdims=['Category','Name'], vdims=['Value'])
df_hm.opts(width=900, height=400, cmap=colors, color_levels=levels, tools=['hover'])

A similar question was asked here: Holoviews Heatmap specify color for each point - #5 by iRave

I’d guess that the reason why “the order of the colours does not match up with the data at all, and appears random” when you passed a list of colors to cmap before is because you were setting the color by the “colors” column, which is a string, and thus ordered according to string sorting rules.

If you need to be able to map separate cmaps altogether for each condition, then I think you’ll need a more complex solution, but I’d be happy to give it a go.

1 Like

Hey there! Thanks for your help! I think I’m after the more complex solution. I saw the post my iRave and tried to do the same but didn’t really understand the colour level, how those values are calculated and how they relate back to the conditions I want for the mapping.

However, what I’m after is a specific color code for each category. So in the picture I have a category called DIF and another called contact rate. I’d like a simple color coding essentially for when a value is > x for that category for any given person it is green and yellow when not. For example:

When DIF > 0.9 I would like the cell coloured green but when it is < 0.9 it is yellow. Additionally when contact rate is > 2 the cell is green and when it is < 2 it is yellow.

In my attempt I created a column that had all the correct colours for each category and person (based on an if statement). I declared this column in the opts as color = dim(‘color’). When I plotted it I got the standard cmap colour, however, when I hover over a cell in the heatmap it changes to the color coding I’m after.

I hope that makes it a bit clearer about what I’m after. Really appreciate the help! :blush:

Since you want to consider different levels according to the category of the data, I guess you can’t use color_levels and need to annotate your dataframe (like you tried to).
I think this code is what you were after except the hex colors are in cmap and the “index of color” dim has to be specified as the first vdim (before Value, otherwise the color mapping is made out of Value/cmap and things will appear random as you noticed)

green = '#00FF00'
amber = '#FFFF00'
red = '#FF0000'
colors = [green,amber,red]

Data = [['A', 'Foo', 0.2] , ['B', 'Bar', 0.9], ['C', 'Cat', 0.7]]
df = pd.DataFrame(Data, columns = ['Name', 'Category', 'Value'])

levels = [0,0.7,0.9,1]
df['color'] = pd.cut(df.Value,levels,right=False)
df['code'] = df.color.cat.codes

df_hm = hv.HeatMap(df,kdims=['Category','Name'], vdims=['code','Value'])
df_hm.opts(width=900, height=400,cmap=colors, tools=['hover'])
2 Likes

Just to expand on the answer from @marcbernot, here is a way to modify that approach such that you can map different levels for each “Category” in the data:

green = '#00FF00'
amber = '#FFFF00'

colors = [amber, green]

Data = [['A', 'Foo', 0.2] , ['A', 'Bar', 0.9], ['A', 'Cat', 0.7], 
        ['B', 'Foo', 0.5] , ['B', 'Bar', 2.0], ['B', 'Cat', 0.1],
        ['C', 'Foo', 0.9] , ['C', 'Bar', 2.5], ['C', 'Cat', 0.5]]

df = pd.DataFrame(Data, columns = ['Name', 'Category', 'Value'])

levels = {'Foo': [0, 0.9, 1], 'Bar': [0, 2, 3], 'Cat': [0, 0.5, 1]}
df['code'] = df.groupby(['Category'], group_keys=False).apply(lambda x: pd.cut(x.Value, levels[x.name], right=False).cat.codes)

df_hm = hv.HeatMap(df,kdims=['Category','Name'], vdims=['code','Value'])
df_hm.opts(width=900, height=400,cmap=colors, tools=['hover'])

In this approach, all “Foo” category values >=0.9 are green and any <0.9 are yellow, all “Bar” category values >=2 are green and any <2 are yellow, and all “Cat” category values >=0.5 are green and those <0.5 are yellow. You could then expand this same approach to a greater number of colors/levels for each category.

Note: If you want the upper bound to be non-inclusive (.e.g, >0.9 rather than >=0.9), you just have to set “right=True” for the pd.cut command.

3 Likes

@mak4515 and @marcbernot thank you so much for your solutions and effort. I got it to work great thanks to both of you, was banging my head against a wall on trying to figure it out and now it works!

Just one question though. On my version when I implement the solution, is there any reason why the cmap colours need to be in a specific order?

For example if I go cmap = [amber, green] I get the correct colours but if I go cmap. = [green, amber] it’s reversed. I know it seems obvious that it’s because the colours are reversed in the cmap, but I guess my question here is, how does holoviews determine which colour goes where and how they are linked to the code. :blush:

My understanding is that the order of the colors in the cmap are assumed to correlate with the ordered values being used for color mapping. In this case, “code” is either 0 or 1, and the cmap colors are assumed to be listed from “lowest” to “highest”. I haven’t done any deep dives into the code base to see exactly how cmap and vdims interact, but from personal experience, this is what I have observed.

Does that answer your question?

@mak4515 ahh yes that makes sense now! Thank you for explaining and all the help, really appreciated! :blush: