将每个Plotly子图的标题左对齐

3
我有一个由 Plotly Express 条形图包装的面向对象组,每个都有一个标题。如何将每个子图的标题与其绘图窗口的左侧左对齐? enter image description here
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()
)

我尝试添加.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1], x=0)),但结果是: enter image description here

2个回答

5

这里有几个主要的挑战:

  • 注释不知道它们在分面图中的列(这使得确定x值变得困难)
  • 创建注释的顺序是从左到右,从下到上(因此,底部的注释首先被创建)
  • 如果子图的数量不能被列数整除,则前{remainder}个注释将在底行创建,然后后续注释从上一行开始,其中{remainder} = {number of annotations} % {number of columns}

为了帮助可视化:

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

这意味着我们想要遍历 (annotation1, x_location_of_col1), (annotation2, x_location_of_col2), (annotation3, x_location_of_col1)... 因为标题的位置是基于知道列何时重新开始。

在您的分面图中有10个注释,12个子图和4列。因此,我们希望跟踪每个子图的x值,我们可以从布局中提取: fig.layout['xaxis'],fig.layout['xaxis2']... 中的信息包含每个子图的起始x值(以纸张坐标表示),我们可以使用信息直到 'xaxis4'(因为我们有4列),并将此信息存储在 x_axis_start_positions 中,在我们的情况下是 [0.0, 0.255, 0.51, 0.7649999999999999](这是从我的代码中派生出来的,我们绝对不想硬编码这个)。

然后利用有4列,10个注释和12个图的事实,我们可以计算出有10 // 4 = 2个完整的图行,第一行有10 % 4 = 2个图在创建的第一行注释中。

我们可以将这些信息提炼成我们迭代每个标题放置的x起始位置:

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]

然后,我们遍历所有10个注释和10个标题的起始位置,覆盖自动生成注释的位置。

更新:我已经将此包装在一个名为left_align_facet_plot_titles的函数中,该函数以面板图fig作为输入,找出列数,并将每个标题左对齐。

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()

enter image description here

如果我们在图表中更改列数,使用 fig = px.bar(..., facet_col_wrap=3, ...)并将该图表传递给函数,结果也与预期相同:

enter image description here


1
谢谢!+1 这个方法可行且很有前途...是否有一种方法可以将 facet_col_wrap 传递到你分配给 fig 的部分中,然后稍后从 fig 中提取出来,并在需要它的地方进行传递(例如在 remainder 中)。我之所以这样问是因为将您的代码封装为一个方法并能够从传递给它的 fig 对象中了解 facet_col_wrap 将会很好。 - Tyler Rinker
1
据我所知,目前没有直接提取facet plot中列数的方法,但一个解决方法是查看所有的fig.layout['xaxis'],fig.layout['xaxis2']...以及找出唯一区间的数量 - 我会相应地更新我的答案。 - Derek O
1
@TylerRinker 实际上更容易查看fig.layout中每个原始注释中唯一x值的数量-答案已经更新,让我知道这看起来是否不错! - Derek O
1
谢谢,这个效果很好,我学到了很多东西!详细解释得非常好。 - Tyler Rinker
1
为什么Plotly要这样做呢?真是让人烦恼。但是这是一个很棒的解决方案!需要注意的是,将这个解决方案适应到px.scatter图形上需要进行一次小的更新:使用len(fig.data)计算remaindernumber_full_rows是不正确的,这导致最后一行的子图标题重叠在一起。这些代码需要更新为remainder = len(fig.layout.annotations) % facet_col_wrapnumber_of_full_rows = len(fig.layout.annotations) // facet_col_wrap。希望这能帮助某人避免一点小麻烦。 - undefined
显示剩余2条评论

2
一种方法是将xref设置为轴 ID,而不是“paper”,这样标题就可以相对于相应的 x 轴坐标左对齐。
然而,正如 @Derek O 所提到的,棘手的部分在于如何在迭代过程中识别注释。考虑到输入数据已经被索引(for i in range(10) 中的 i 是正确的),您可以使用此索引来检索相应的 xref,唯一的要求是保持(或创建)它们之间的映射。为了说明这一点,在子图标题中直接放置了索引:
K = 10
R = 5

items = np.repeat([f'{i}={lorem.sentence()}' for i in range(K)], R)
response = list(range(1,6)) * K
n = [random.randint(0, 10) for i in range(K*R)]

facet_col_wrap = 4
nrows = math.ceil(K / facet_col_wrap)
ncols = min(K, facet_col_wrap)

# Nb. We need to read fig._grid_ref for creating the xrefs
fig = px.bar(x=response, y=n, facet_col=items, facet_col_wrap=facet_col_wrap, height=1300)

xrefs = []
for r in range(nrows)[::-1]:
    for c in range(ncols):
        subplot_ref = fig._grid_ref[r][c][0]
        xrefs.append(subplot_ref.trace_kwargs['xaxis'])

def updateAnnotation(a):
    _, i, title = a.text.split("=")
    a.update(text=title, xanchor='left', x=0, xref=xrefs[int(i)])

(
    fig
    .for_each_annotation(updateAnnotation)
    .for_each_xaxis(lambda xaxis: xaxis.update(showticklabels=True))
    .for_each_yaxis(lambda xaxis: xaxis.update(showticklabels=True))
    .show()
)

检查fig._grid_ref中的SubplotRef非常有帮助,一个SubplotRef看起来像:

SubplotRef(subplot_type='xy', layout_keys=('xaxis', 'yaxis'), trace_kwargs={'xaxis': 'x', 'yaxis': 'y'})

更好的是:

>>> fig.print_grid()

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

2
哦,这是一个聪明的方法 - 我不知道私有方法fig._grid_ref - 这对于这个特定的问题肯定是有用的。 - Derek O

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接