Black Lives Matter. Please consider donating to Black Girls Code today.

How to create annotated heatmaps in subplots?

I changed to a simpler data, just for debug. The code keeps the same.
That’s the data:

df_fr = pd.DataFrame([[1, 2, 3], [8, 6, 5]])
df_count = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
df_doid = pd.DataFrame([[1, 2, 3], [4, 5, 6]])

And that’s what I got:


This odd behaiour occurs because after fig definition via make_subplots, fig.layout.annotations is already initialized with subplot_titles (my initial example had no titles) .

In this case you have to add the new annotations, read from fig1, fig2, fig3 as follows:

new_annotations = annot1+annot2+annot3

for anno in new_annotations:

These lines of code replace:


When your subpolots contain many cells ,not only 2-3, define
new_annotations =[] and extend it by new_annotations.extend(), not by concatenating with +.

Thank you again. It worked!
Last one, I promise:

Do you know why, using that code, both of my plots are with white color and the centered one with black color?
What’s the logic for painting them differently?

Plotly maps z-values to a colorscale Depending on the colorscale you are setting in layout.coloraxis, if it is a sequential one, like in the last plot you posted here, the smaller z-values are mapped to lighter colors, while the bigger - to darker colors. Another colorscales have darker colors corresponding to smaller values.

But why in the first heatmap, bigger numbers and small numbers gets white (and illegible), and in the middle one, they respected what you said?

@rtadewald It seems that your z-data are missing in some cells. In one of the them we can see nan as an annotation. You can set 'connectgaps=True` in Heatmap definition to fill those cells with an interpolated color.

I don’t think it solved, the colors keep different, but now, ‘nan’ values are colored hehe


As I already said initially without having your data I cannot figure out what is happening :frowning:

Also I don’t know what colors are different. More precision would be useful…

On the image, you can see that the right graph is with white text colors (what makes it sometimes illegible), and the other ones are with black and white colors. You can see the same pattern on the first graphs I’ve send here, where you have my code and the data.

OMG!!! You are referring to font color!!! The annotation font_color is assigned by ff.create_annotated_heatmap

To change the colors to only one for eaple black you should update the annotations:

new_annotations = annot1+annot2+annot3

for anno in new_annotations:
    anno.update(font_color ='black')

Sorry for the misunderstood. Using your code set all colors to ‘black’, but how to make them smart, like the center graph (white text colors to darker background colors, and vice versa)?

Here is how the text color is set when the colorscale belongs to this list:

colorscales = [

Because your colorscale isn’t in this list, you should update annotation font color following the same rule like that implemented in the method get_text_color().


It seems that this example breaks if I add either of the following arguments to the call of make_subplots:

  • subplot_titles=[‘a’, ‘b’]
  • row_titles = [‘A’]

When I specify either argument, some of the heatmap cells are empty

Do you have any suggestions?

Thanks in advance!


Replacing fig=make_subplots() from with:

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

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?


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:

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!


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.


layout = go.Layout(
                go.layout.Annotation(text="Initial 1"),
                go.layout.Annotation(text="Initial 2"),
print(f'Initial annotations:\n\n {layout}')

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
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,

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([0], 1, 1)
fig.add_trace([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'


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:


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(recursive_extend( mylist, nr-1))
    return result

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

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([0], 1, 1)
fig.add_trace([0], 1, 2)
fig.add_trace([0], 2, 1)
fig.add_trace([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  

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


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

Does that seem reasonable?

I edited the code. Thanks!!