Plot with rect and multiple color mapping

I’m trying to create a heatmap-like plot where every column has its own color mapping.

It works fine when I use numeric range for the x-axis (fig1), but if I try to use the factor which would be the more intuitive way (fig2) it won’t plot anything. Is there a reason for it?

I also tried the with CategoricalColorMapper and ColorSpec but the same occurs, so I guess is not related to the coloring itself. Follow minimal example:

from bokeh.io import show
from bokeh.plotting import figure
from bokeh.palettes import Category20
from bokeh.layouts import layout
from bokeh.transform import factor_cmap

data = {"index": ["row1","row2","row3","row4","row5"],
        "colA": ["a","aa","aaa","aaa","aaa"], 
        "colB": ["b","bb","bbb","bbbb","bbbb"], 
        "colC": ["c","cc","cccc","ccccc","cccccc"]}

data_rows = data["index"]
data_cols = list(data.keys())[1:]

fig1 = figure(x_range=data_cols, y_range=data_rows)
fig2 = figure(x_range=data_cols, y_range=data_rows)

for i,col in enumerate(data_cols):
	unique_factors = list(set(data[col]))

	fig1.rect(x=i+.5, y="index", 
		width=1, height=1,
		source=data,
		fill_color=factor_cmap(field_name=col, palette=Category20[len(unique_factors)], factors=unique_factors))

	fig2.rect(x=col, y="index",
		width=1, height=1,
		source=data,
		fill_color=factor_cmap(field_name=col, palette=Category20[len(unique_factors)], factors=unique_factors))

show(layout([fig1,fig2]))

I took a look at this, and I think it’s because on figure 2 you are pointing to a singular value for x (given as a string) and a field name for y (also given as a string), but bokeh is probably thinking your x arg is also a field name. In contrast, in fig1 you are passing a single float number for x instead, so bokeh probably doesn’t get “confused” from your instructions in fig 1 but it does in fig 2.

If you can live with python-side colormapping (and maybe you can’t if you have callbacks happening that change the datasource) + some pandas preprocessing… I’d be making a dataframe, melting it (i.e. flattening it), assigning a color column, passing that to a single rect glyph pointing to the color column field name for fill color.

The only tricky part is the way you want to do the color map i.e. based on the unique values present in each column. You’ve attacked that problem by looping through each column and making a new rect glyph for each one and grabbing the unique column values each time. But you can also implement this mapping on just one glyph with some pandas groupby–>agg -->lambda voodoo to “premap” colors based on unique values in each column ‘group’:

from bokeh.io import show
from bokeh.plotting import figure
from bokeh.palettes import Category20
from bokeh.layouts import layout
from bokeh.transform import factor_cmap
from bokeh.models import ColumnDataSource

import pandas as pd

data = {"index": ["row1","row2","row3","row4","row5"],
        "colA": ["a","aa","aaa","aaa","aaa"], 
        "colB": ["b","bb","bbb","bbbb","bbbb"], 
        "colC": ["c","cc","cccc","ccccc","cccccc"]}
df = pd.DataFrame(data=data)

#flatten the dataframe
df = df.melt(id_vars='index',value_vars = [x for x in df.columns if x!='index'],var_name='column')
#some pandas groupby voodoo to color map based on the unique values in each column
cmapping = df.groupby('column').agg({'value':lambda x: x.map({v:Category20[len(set(x))][ii] for ii,v in enumerate(set(x))})}).reset_index()
#could have made this one line but parsed it out a bit --> explode blows out the list of colors for each column into a nice flat table
#after groupby it's still named value but it's actually color as mapped so rename as well
cmapping = cmapping.explode(column='value').rename(columns={'value':'color'})
#can just directly attach this to the main df
df['color'] = cmapping['color'].tolist()

src = ColumnDataSource({c:df[c].tolist() for c in df.columns})
fig1 = figure(x_range=df['column'].unique(), y_range=df['index'].unique())
fig1.rect(x='column',y='index',height=1,width=1,fill_color='color',source=src)
show(fig1)

Hopefully this helps!

Applying Bryan’s explanation he gave me here - Template-->Theme-->text_font. Putting it all together - #3 by Bryan , the reason your code isn’t behaving as intended is indeed as I suspected, but there’s a way easier fix than my suggestion/rewrite above.

Just replace:

fig2.rect(x=col, y="index", .....

with

fig2.rect(x=dict(value=col), y="index",

:slight_smile:

2 Likes

Thanks for the solution! It works with fig2.rect(x=dict(value=col), y="index",!

The solution I found for the color mapping was to pre-load a CategoricalColorMapper with all possible row/col value combinations and then use fill_color={'field': col, 'transform': metadata_colormap}, in the rect call.