How to create annotated heatmaps in subplots?

@jvd23

Replacing fig=make_subplots() from https://community.plotly.com/t/how-to-create-annotated-heatmaps-in-subplots/36686/3 with:

fig = make_subplots(subplot_titles=('A', 'B'),
    rows=1, cols=2,
    horizontal_spacing=0.075,
    ) 

I got the the right plot:

It seems that you did not use the quotation mark ' for the subtitle definition, but `.

Hi, it seems that it is still missing the annotations in the lower left corner?

@jvd23

Yes, I inspected the list(tuple) fig.layout.annotations, and found out that the annotations for the two lower-left cells are not included in the list.

Hence replace the last line of code, fig.update_layout(annotations=annot1+annot2) by the following ones:

new_annotations = annot1+annot2
for anno in new_annotations:
    fig.add_annotation(anno)

and so the two heatmaps have annotation for each cell.

Thank you for getting back so quickly. This worked, but for a larger subplot (~10 heatmaps of size 3x7) it added about 5 seconds (increased from ~1 second with the update_layout to 6 seconds with the add_annotation) to the run time prior to rendering the figure. Any suggestions on how to speed it up? Not sure if it would work to only add the missing annotations rather than all of them, though this is not the cleanest approach. Thank you!

@jvd23

Here is the big surprise related to updating annotations, introduced, probably, in Plotly 4.0.0:
If the initial annotations in a fig.layout consist in list, annots, of length L, then when we are updating it
with a list, newannots, of length n>=L,
the first L annotations from newannots update successively the elements in annots, and the following n-L are new annotations that extend annots.

Example:


layout = go.Layout(
            annotations=[
                go.layout.Annotation(text="Initial 1"),
                go.layout.Annotation(text="Initial 2"),
            ]
        )
print(f'Initial annotations:\n\n {layout}')
layout.update(
            annotations=[
                go.layout.Annotation(width=10),
                go.layout.Annotation(width=20),
                go.layout.Annotation(width=30),
                go.layout.Annotation(width=40),
                go.layout.Annotation(width=50),
            ]
        )

print(f'updated annotations:\n\n{layout}')

Hence a more performant working code for the example with two subplots is like this:

import numpy as np
import plotly.graph_objs as go
import plotly.figure_factory as ff
from plotly.subplots import make_subplots
import string
#Define data for heatmap
N=5
x = np.array([10*k for k in range(N)])
y = np.linspace(0, 2, N) 
z1 = np.random.randint(5,15, (N,N))
z2 = np.random.randint(10,27, (N,N))
mytext = np.array(list(string.ascii_uppercase))[:25].reshape(N,N)


fig1 = ff.create_annotated_heatmap(z1, x.tolist(), y.tolist(),  colorscale='matter')
fig2 = ff.create_annotated_heatmap(z2, x.tolist(), y.tolist(), annotation_text=mytext, colorscale='Viridis')

fig = make_subplots(subplot_titles=('A', 'B'),
    rows=1, cols=2,
    horizontal_spacing=0.075,)

print(f'Annotations defined by make-subplots:\n\n{fig.layout.annotations}\n\n')


#update font size in subplot titles; This is the tricks that just overwrites  the font size,
# with the same value, and leads to the right final annotations:
newfont= [go.layout.Annotation(font_size=16)]*2
print(f'A superflous, but necessary update :\n\n{newfont}')

fig.add_trace(fig1.data[0], 1, 1)
fig.add_trace(fig2.data[0], 1, 2)

annot1 = list(fig1.layout.annotations)
annot2 = list(fig2.layout.annotations)
for k  in range(len(annot2)):
    annot2[k]['xref'] = 'x2'
    annot2[k]['yref'] = 'y2'

annot1.extend(annot2)

newfont.extend(annot1)
fig.update_layout(annotations=newfont)  
fig.update_layout(width=700, height=400)  

But if you define n subplots, with n >=3, it is cumbersome to extend succesively, annot1 = fig1.layout.annotations,
with fig.layout.annotations from the heatmap 2, 3, 4, … definition.

To avoid lines of code like these ones:

annot1.extend(annot2)
annot1.extend(annot3)
.
.
.

we define a recursive function to extend an empty list with the sublists from a lists of lists:

def recursive_extend (mylist, nr):
    #mylist is a list of lists
    # initial nr =len(mylist)
    result = []
    
    if nr> 1:
        result.extend(mylist[nr-1])
        result.extend(recursive_extend( mylist, nr-1))
    else: 
        result.extend(mylist[nr-1])
    
    return result

Let us illustrate how is defined a figure consisting in 4 subplots of annotated heatmaps:

N=5
x = np.array([10*k for k in range(N)])
y = np.linspace(0, 2, N) 
z1 = np.random.randint(5,15, (N,N))
z2 = np.random.randint(10,27, (N,N))
mytext = np.array(list(string.ascii_uppercase))[:25].reshape(N,N)


fig1 = ff.create_annotated_heatmap(z1, x.tolist(), y.tolist(),  colorscale='matter')
fig2 = ff.create_annotated_heatmap(z2, x.tolist(), y.tolist(), annotation_text=mytext, colorscale='Viridis')
fig3 = ff.create_annotated_heatmap(z1, x.tolist(), y.tolist(),  colorscale='deep')
fig4 = ff.create_annotated_heatmap(z2, x.tolist(), y.tolist(), annotation_text=mytext, colorscale='Plasma')

fig = make_subplots(subplot_titles=('A', 'B', 'C', 'D'),
    rows=2, cols=2,
    horizontal_spacing=0.075, vertical_spacing=0.11)


fig.add_trace(fig1.data[0], 1, 1)
fig.add_trace(fig2.data[0], 1, 2)
fig.add_trace(fig3.data[0], 2, 1)
fig.add_trace(fig4.data[0], 2, 2)


newfont = [go.layout.Annotation(font_size=16)]*len(fig.layout.annotations)
#print(f'A superflous update that forces the right updates of the annotations:\n\n{newfont}')
fig_annots = [newfont, fig1.layout.annotations, fig2.layout.annotations, fig3.layout.annotations, fig4.layout.annotations]

for j in range(2, len(fig_annots)):
    for k  in range(len(fig_annots[j])):
        fig_annots[j][k]['xref'] = f'x{j}'
        fig_annots[j][k]['yref'] = f'y{j}'
    
new_annotations = recursive_extend (fig_annots[::-1], len(fig_annots))# Note that fig_annots is reverted to ensure that
                                                                      # the elements of newfonts are the first 4 annotations  
fig.update_layout(annotations=new_annotations)

Note: When we are concatenating a relative big number of lists it is recomended to use extend, not +, because + is less performant.

Thank you! This worked and is much faster. For the general case, I changed:

newfont= [go.layout.Annotation(font_size=16)]*4

to

newfont= [go.layout.Annotation(font_size=16)]*len(fig.layout.annotations)

Does that seem reasonable?

I edited the code. Thanks!!

Thank you for the help!
this part of the code helped with multiple plots:
for k in range(len(annot2)):
annot2[k][β€˜xref’] = β€˜x2’
annot2[k][β€˜yref’] = β€˜y2’

If I want to do multiple rows, how do I add annotations for additional subplot rows? Let’s say I wanted a 3X3 subplot?

@katiemharding

When you create a go.Figure, via make_subplots(), set print_grid=True, i.e.:

fig = make_subplots(rows=3, cols=3, print_grid=True)
and get:

This is the format of your plot grid:
[ (1,1) x,y   ]  [ (1,2) x2,y2 ]  [ (1,3) x3,y3 ]
[ (2,1) x4,y4 ]  [ (2,2) x5,y5 ]  [ (2,3) x6,y6 ]
[ (3,1) x7,y7 ]  [ (3,2) x8,y8 ]  [ (3,3) x9,y9 ]

So you know the name of axes to which each subplot is referenced to, and can perform the right update for xref, yref .

2 Likes

@empet Hi! Small help truly appreciated. I have successfully plotted some annotated heatmaps with subplots. Just one issue. It seems that when removing data, subplots is not correctly updated, although I am in pure Dash, returning everytime a completely new figure to dcc.:
imagen
Dropdown used, removed ~AutomΓ³viles~
imagen

Anybody might help? Truly appreciated.