I have a facet wraped group of plotly express barplots , each with a title. How can I left align each subplot's title with the left of its plot window?
import lorem
import plotly.express as px
import numpy as np
import random
items = np.repeat([lorem.sentence() for i in range(10)], 5)
response = list(range(1,6)) * 10
n = [random.randint(0, 10) for i in range(50)]
(
px.bar(x=response, y=n, facet_col=items, facet_col_wrap=4, height=1300)
.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
.for_each_xaxis(lambda xaxis: xaxis.update(showticklabels=True))
.for_each_yaxis(lambda yaxis: yaxis.update(showticklabels=True))
.show()
)
I tried adding .for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1], x=0))
but it results in:
There's a few major challenges here:
{remainder} = {number of annotations} % {number of columns}
To help visualize:
x x x x
x x x x ⭠ followed by these ones left to right, ...
x x ⭠ these annotations are created first left to right
This means we would like to iterate through (annotation1, x_location_of_col1), (annotation2, x_location_of_col2), (annotation3, x_location_of_col1)...
because the placement of the title is based on knowing when the column restarts.
In your facet plot there are 10 annotations, 12 total subplots, and 4 columns. Therefore we want to keep track of the x values for each subplot which we can extract from the layouts: the information inside fig.layout['xaxis'], fig.layout['xaxis2']...
contain the starting x-values for each subplot in paper coordinates, and we can use the information up to 'xaxis4' (since we have 4 columns), and store this info inside x_axis_start_positions
which in our case is [0.0, 0.255, 0.51, 0.7649999999999999]
(this is derived in my code, we definitely don't want to hardcode this)
Then using the fact that there are 4 columns, 10 annotations, and 12 plots, we can work out that there are 10 // 4 = 2 full rows of plots
, and the first row has 10 % 4 = 2 plots
in the first row of annotations that are created.
We can distill this information into the x starting positions we iterate through for the placement of each title:
x_axis_start_positions_iterator = x_axis_start_positions[:remainder] + x_axis_start_positions*number_of_full_rows
# [0.0, 0.255, 0.0, 0.255, 0.51, 0.7649999999999999, 0.0, 0.255, 0.51, 0.7649999999999999]
Then we iterate through all 10 annotations and 10 starting positions for the titles, overwriting the positions of the automatically generated annotations.
Update: I have wrapped this in a function called left_align_facet_plot_titles
that takes a facet plot fig
as an input, figures out the number of columns, and left aligns each title.
import lorem
import plotly.express as px
import numpy as np
import random
items = np.repeat([lorem.sentence() for i in range(10)], 5)
response = list(range(1,6)) * 10
n = [random.randint(0, 10) for i in range(50)]
fig = (
px.bar(x=response, y=n, facet_col=items, facet_col_wrap=4, height=1300)
.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
.for_each_xaxis(lambda xaxis: xaxis.update(showticklabels=True))
.for_each_yaxis(lambda yaxis: yaxis.update(showticklabels=True))
)
def left_align_facet_plot_titles(fig):
## figure out number of columns in each facet
facet_col_wrap = len(np.unique([a['x'] for a in fig.layout.annotations]))
# x x x x
# x x x x <-- then these annotations
# x x <-- these annotations are created first
## we need to know the remainder
## because when we iterate through annotations
## they need to know what column they are in
## (and annotations natively contain no such information)
remainder = len(fig.data) % facet_col_wrap
number_of_full_rows = len(fig.data) // facet_col_wrap
annotations = fig.layout.annotations
xaxis_col_strings = list(range(1, facet_col_wrap+1))
xaxis_col_strings[0] = ''
x_axis_start_positions = [fig.layout[f'xaxis{i}']['domain'][0] for i in xaxis_col_strings]
if remainder == 0:
x_axis_start_positions_iterator = x_axis_start_positions*number_of_full_rows
else:
x_axis_start_positions_iterator = x_axis_start_positions[:remainder] + x_axis_start_positions*number_of_full_rows
for a, x in zip(annotations, x_axis_start_positions_iterator):
a['x'] = x
a['xanchor'] = 'left'
fig.layout.annotations = annotations
return fig
fig = left_align_facet_plot_titles(fig)
fig.show()
And if we change the number of columns in the figure with fig = px.bar(..., facet_col_wrap=3, ...)
and pass this figure to the function, the results are also as expected: