Plotly 旭日形图周围的值注释

在这个取自 the docs 的 Plotly sunburst 图示例中,如何将值显示为最外层之外的文本?我想为每个段添加一个注释,显示确定其径向宽度的值。

My actual plots 在最外层有几百个段,因此任何解决方案都应该是全自动的,不需要硬编码值。

import plotly.express as px
df = px.data.tips()
fig = px.sunburst(df, path=['day', 'time', 'sex'], values='total_bill')
fig.show()

plotly sunburst

stack overflow Value annotations around Plotly sunburst diagram
原文答案

答案:

作者头像

我认为 Plotly 旭日形图没有任何内置注释,因此您需要手动添加注释。

在您的情况下,我认为使用带有参数 go.Scatter()mode='text' 将允许您将注释放在旭日形图上。这种方法的优点是可以将旭日形图放置在最方便的任何坐标上。

例如,如果您将 x 轴和 y 轴的范围设置为 [-1,1] 这将确保旭日形图以 (0,0) 为中心,半径约为 1(编辑:如 @Rene 所建议,如果固定图形的宽度和高度,可以保证旭日图是圆形的,并且浏览器窗口的纵横比不会改变旭日图的 eccentricity)。您可能还需要在这些范围上进行一些填充,以确保文本在接近范围的上端或下端时不会被截断。

然后您可以使用极坐标根据 r 和 theta 确定 x 和 y 坐标。因此,如果您想将注释 "1227" 放置在 45 度,然后设置 x=r*cos(45˚)y=r*sin(45˚). 并重复此过程,并将所有要放置的注释向下。

更新:尽管 Plotly 以正确的顺序呈现带有类别的旭日形图,但似乎这些信息并没有存储在可访问的对象中,这让我们不得不自己确定类别的顺序及其各自的角度。

对于 Plotly 旭日形图,其父类别中的类别( daytimesex )的总和决定了它们从 0 度开始在图表上的放置顺序。例如,类别 total_tipsday 总和最大的是 Sat ,其次是 Sun, Thur, Fri ,这就是这些类别及其值在图表上的排列顺序。对于父类别中的子类别,同样的模式适用:例如, total_tipsSat/Dinner/Male 之和大于 Sat/Dinner/Female ,因此对应于 Sat/Dinner/Male 的值放在前面周六/晚餐/女性。

我们可以使用 groupbysort_values 的组合来重现此排序:

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from math import sin,cos,pi

df = px.data.tips()
fig = px.sunburst(df, path=['day', 'time', 'sex'], values='total_bill', width=600, height=600)

totals_groupby =  df.groupby(['day', 'time', 'sex']).sum()
totals_groupby["day_sum"] = df.groupby(['day', 'time', 'sex']).total_bill.sum().groupby(level='day').transform('sum')
totals_groupby["day_time_sum"] = df.groupby(['day', 'time', 'sex']).total_bill.sum().groupby(level=['day','time']).transform('sum')
totals_groupby["day_time_sex_sum"] = df.groupby(['day', 'time', 'sex']).total_bill.sum().groupby(level=['day','time','sex']).transform('sum')
totals_groupby = totals_groupby.sort_values(by=["day_sum","day_time_sum","day_time_sex_sum"], ascending=[0,0,0])

下面是 totals_groupby DataFrame,我们在其中复制了与 Plotly express sunburst 图表相同的类别顺序:

>>> totals_groupby
                    total_bill     tip  size  day_sum  day_time_sum  day_time_sex_sum
day  time   sex                                                                      
Sat  Dinner Male       1227.35  181.95   156  1778.40       1778.40           1227.35
            Female      551.05   78.45    63  1778.40       1778.40            551.05
Sun  Dinner Male       1269.46  186.78   163  1627.16       1627.16           1269.46
            Female      357.70   60.61    53  1627.16       1627.16            357.70
Thur Lunch  Male        561.44   89.41    73  1096.33       1077.55            561.44
            Female      516.11   79.42    77  1096.33       1077.55            516.11
     Dinner Female       18.78    3.00     2  1096.33         18.78             18.78
Fri  Dinner Male        164.41   21.23    16   325.88        235.96            164.41
            Female       71.55   14.05    10   325.88        235.96             71.55
     Lunch  Female       55.76   10.98     9   325.88         89.92             55.76
            Male         34.16    5.70     5   325.88         89.92             34.16

我们想要的注释是 total_billtotals_groupby 列中的值,并且与 plotly.express sunburst 图形对应的顺序。

然后我们可以通过将 subtended 列除以total_bill 的总数并乘以360 以度数为单位来计算每个类别的角度 total_bill 。请注意,这不是我们要放置注释的最终角度:要获得它,我们需要从 0 开始对这些角度取滚动平均值。

annotations = [format(v,".0f") for v in totals_groupby.total_bill.values]

## calculate the angle subtended by each category
sum_total_bill = df.total_bill.sum()
delta_angles = 360*totals_groupby["total_bill"] / sum_total_bill

## calculate cumulative sum starting from 0, then take a rolling mean 
## to get the angle where the annotations should go
angles_in_degrees = pd.concat([pd.DataFrame(data=[0]),delta_angles]).cumsum().rolling(window=2).mean().dropna().values

>>> annotations
['1227', '551', '1269', '358', '561', '516', '19', '164', '72', '56', '34']
>>> list(angles_in_degrees[:,0])
[45.76087924652581, 112.06726915325291, 179.94370071482274, 240.6112138730718, 274.8807006133266, 315.0563924959142, 334.9993889518348, 341.82949891979104, 350.6271011253642, 355.3737646988153, 358.726368488971]

现在我们可以使用辅助函数将所有这些信息放在旭日图上,将角度转换为 x,y 坐标。

def get_xy_coordinates(angles_in_degrees, r=1):
    return [r*cos(angle*pi/180) for angle in angles_in_degrees], [r*sin(angle*pi/180) for angle in angles_in_degrees]

x_coordinates, y_coordinates = get_xy_coordinates(angles_in_degrees, r=1.13)
fig.add_trace(go.Scatter(
    x=x_coordinates,
    y=y_coordinates,
    mode="text",
    text=annotations,
    hoverinfo="skip",
    textfont=dict(size=14)
))

padding = 0.20
fig.update_layout(
    width=600, 
    height=600,
    xaxis=dict(
        range=[-1 - padding, 1 + padding], 
        showticklabels=False
    ), 
    yaxis=dict(
        range=[-1 - padding, 1 + padding],
        showticklabels=False
    ),
    plot_bgcolor='rgba(0,0,0,0)'
)

fig.show()

enter image description here

作者头像

This isn't a perfect answer to this question specifically, but a similar outcome can be reached by editing the starburst figure's data field.

df = px.data.tips()
p = px.sunburst(df, path=['day', 'time', 'sex'], values='total_bill')
p.update_layout(
    margin=dict(l=0, r=0, t=0, b=0),
)
p.data[0].labels=np.array(list(zip(p.data[0].labels, p.data[0].values)))
p.show(renderer='svg')

Certainly, this doesn't position the labels outside the outer ring, but it is fully automatic, keeps consistent fonts and styling, and also labels the inner partitions by default. If value labels must be restricted to the outer ring, the zip can be modified.

unfortunately it appears the text positioning cannot work with carriage returns in strings. At least, I ran into unexpected behavior when using "\n".join in various ways.