26

I understand that if I want to set all of the subplot titles then I can do that when I declare the figure.

import pandas as pd
import plotly.graph_objs as go
from plotly.subplots import make_subplots

fig = make_subplots(
    ###
    subplot_titles=['Plot 1', 'Plot 2', 'Plot 3', 'Plot 4', etc.]
)

However, I create each subplot graph within a loop, and I think it would be easiest to set each subplot's title during that process. In my case, I created a groupby object based on date, and then I loop through each group (date) dataframe. Each dataframe is used to create the subplot graph, and I'd like the title to be the date. I understand that I can figure this out before the loop and set the subplot_titles as normal, but it seems like it'd be a one-liner within the loop. If it matters, the traces I'm adding are choropleths. I'm not going to include a working dataset since my code works fine- I just want to know what line of code I need to add.

#Create groupby object, splitting data frame into separate data frames based on 'State'
death_counts_gbo = death_counts_by_week.groupby('End Week') 

#Prepare subplots for choropleths
rows = 4
cols = 7
fig = make_subplots(
    rows=rows, cols=cols,
    specs = [[{'type': 'choropleth'} for c in np.arange(cols)] for r in np.arange(rows)],
)
    
#Graph each state's data
for i, (date, df) in enumerate(death_counts_gbo):
    fig.add_trace(go.Choropleth(
        locations=df['State Abbr'], # Spatial coordinates
        z = df['COVID-19 Deaths per 100k'].astype(float), # Data to be color-coded
        locationmode = 'USA-states', # set of locations match entries in `locations`
        colorscale = 'Reds',
        zmin = 0,
        zmax = 30,
        colorbar_title = "Deaths per 100k",
        text = date.strftime('%m-%d-%Y')
    ), row=i//7+1, col=i%7+1)
    ### This is where I'd like to set the subplot's title to 'date' ###

# Set title of entire figure
# Set each subplot map for the USA map instead of default world map
# Set each subplot map's lake color to light blue
fig.update_layout(
    title_text = 'COVID-19 Deaths per 100k at week ending',
    **{'geo' + str(i) + '_scope': 'usa' for i in [''] + np.arange(2,len(death_counts_gbo)+1).tolist()},
    **{'geo' + str(i) + '_lakecolor': 'lightblue' for i in [''] + np.arange(2,len(death_counts_gbo)+1).tolist()},
)
    
fig.show()

EDIT: For my example, I can set the subplot titles during the figure declaration with the following kwarg:

subplot_titles = [date.strftime('%m-%d-%Y') for date in list(death_counts_gbo.groups.keys())]

However, for my own edification and if I have a future case where the determination of the subtitle is more involved, I would still like to know how to set it during the loop / after the figure declaration.

EDIT2: Thanks to @rpanai I have a solution, though it required a change in my make_subplots declaration, in addition to a one-liner in the loop. He informed me that subtitles are annotations stored in fig.layout.annotations and that, while a list of subtitles may be provided to it, calculating the appropriate x and y coordinates might be a hassle. I worked around this issue by creating temporary subtitles and forcing make_subplots to calculate the x and y coordinates. I then updated the text in the loop.

The updated make_subplots figure declaration (subplot_titles added):

fig = make_subplots(
    rows=rows, cols=cols,
    specs = [[{'type': 'choropleth'} for c in np.arange(cols)] for r in np.arange(rows)],
    subplot_titles = ['temp_subtitle' for date in np.arange(len(death_counts_gbo))]
)

One-liner (first line of the for-loop):

fig.layout.annotations[i]['text'] = date.strftime('%m-%d-%Y')
wex52
  • 475
  • 1
  • 4
  • 11
  • 2
    HI wex52, the subplot titles are stored as `fig.layout.annotations` when you declared them within `make_subplots` I think that is possible to replicate it creating an `annotations` list during the for loop but i don't think it's worth to deal with `x` and `y` position for each annotation. – rpanai Aug 03 '20 at 13:56
  • 1
    Thank you, @rpanai. I used the information you gave me, wrote a workaround for the issue you raised, and edited my question to include my solution. In addition to a one-liner I needed to add a temporary list of subtitles to my `make_subplots` call so it would calculate the `x` and `y` positions for me. – wex52 Aug 04 '20 at 14:20
  • 3
    @rpanai is right. # Initiate a figure with random names with 2x2 subplot `figure_pl = make_subplots(rows=2, cols=2, start_cell="bottom-left", subplot_titles=("randname0","randname1","randname2", "randname3"))` `# Iteratively change the names` `new_names = [new_name0,new_name1,new_name2,new_name3]` `for i, new_name in enumerate(new_names):` `figure_pl.layout.annotations[i]["text"] = new_name` – Arvind Kumar Mar 05 '21 at 10:04
  • 1
    @Arvind Kumar This worked fantastically with latest plotly ast of early 2021. To handle any possible future API changes, print out `figure_pl.layout` and look for the dictionary entrires that define the entries above. – Contango Apr 18 '21 at 08:40

1 Answers1

15

You mentioned that you'd like to be able to edit the titles (that are annotations) after you'be built your figures as well. And if you already have some title in place, and would like to change them, you can use fig.for_each_annotation() in combination with a dict like this:

names = {'Plot 1':'2016', 'Plot 2':'2017', 'Plot 3':'2018', 'Plot 4':'2019'}
fig.for_each_annotation(lambda a: a.update(text = names[a.text]))

Which will turn this:

enter image description here

into this:

enter image description here

You can also easily combine existing titles with an addition of your choice with:

fig.for_each_annotation(lambda a: a.update(text = a.text + ': ' + names[a.text]))

Plot 3

enter image description here

Complete code:

from plotly.subplots import make_subplots
import plotly.graph_objects as go

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=("Plot 1", "Plot 2", "Plot 3", "Plot 4"))


fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 5, 6]),
              row=1, col=1)

fig.add_trace(go.Scatter(x=[20, 30, 40], y=[50, 60, 70]),
              row=1, col=2)

fig.add_trace(go.Scatter(x=[300, 400, 500], y=[600, 700, 800]),
              row=2, col=1)

fig.add_trace(go.Scatter(x=[4000, 5000, 6000], y=[7000, 8000, 9000]),
              row=2, col=2)

fig.update_layout(height=500, width=700,
                  title_text="Multiple Subplots with Titles")

names = {'Plot 1':'2016', 'Plot 2':'2017', 'Plot 3':'2018', 'Plot 4':'2019'}

# fig.for_each_annotation(lambda a: a.update(text = names[a.text]))

fig.for_each_annotation(lambda a: a.update(text = a.text + ': ' + names[a.text]))
         
fig.show()
vestland
  • 55,229
  • 37
  • 187
  • 305