简体   繁体   中英

Group bars with different group sizes in Plotly Express bar plot

Consider the following dataframe, called data :
数据框

Only two elements of the "teacher" column appear twice, the others appear once only.
I make a bar plot with Plotly Express:

import plotly.express as px
px.bar(data.sort_values("start_time", ascending=False), x="teacher", y="start_time", color="start_time",
       color_continuous_scale="Bluered", barmode="group")

and the following is output:
px条的输出

I would like to have bars next to each other, rather than stacked. I think that px stacks them (contrary to the behaviour in their docs) because I do not have the same number of occurrences for each teacher.

  • Is that correct?
  • How can I fix it?

According to this forum post , what is happening is that plotly.express is interpreting start_time as a continuous variable which is why you get a colorbar, but then falls back onto stacking the bars instead of grouping them.

As suggested by @Emmanuelle, you could solve this by creating a new start_time column that is a string called start_time_str , then pass this column to the color argument. This forces plotly.express to interpret this variable as discrete. However, you would then lose the color bar and get a legend:

data['start_time_str'] = data['start_time'].astype('str')
fig = px.bar(data.sort_values("start_time", ascending=False), x="teacher", y="start_time", color="start_time_str",color_continuous_scale="Bluered", barmode="group")

在此处输入图片说明

So assuming you want to preserve the colorbar, and have stacked bars, you'll need a more complicated workaround.

You can use plotly.express to plot the first bar so that you get the colorbar, then use fig.add_trace to add the second bar as a graph_object . When you add the second bar, you will need to specify the color and to do that, you'll need some helper functions such as normalize_color_val that converts the y-value of this bar to a normalized color value relative to the data on a scale of 0 to 1, and get_color which returns the color of the bar (as an rgb string) when you pass the colorscale name and normalized value.

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

data = pd.DataFrame(
    {'teacher':['Lingrand','Milanesio','Menin','Malot','Malot','Schminke','Cornelli','Milanesio','Marchello','Menin','Huet'],
    'start_time':[12,12,5,0,5,0,4,8,-1,0,4]}
)

# This function allows you to retrieve colors from a continuous color scale
# by providing the name of the color scale, and the normalized location between 0 and 1
# Reference: https://stackoverflow.com/questions/62710057/access-color-from-plotly-color-scale

def get_color(colorscale_name, loc):
    from _plotly_utils.basevalidators import ColorscaleValidator
    # first parameter: Name of the property being validated
    # second parameter: a string, doesn't really matter in our use case
    cv = ColorscaleValidator("colorscale", "")
    # colorscale will be a list of lists: [[loc1, "rgb1"], [loc2, "rgb2"], ...] 
    colorscale = cv.validate_coerce(colorscale_name)
    
    if hasattr(loc, "__iter__"):
        return [get_continuous_color(colorscale, x) for x in loc]
    return get_continuous_color(colorscale, loc)
        

# Identical to Adam's answer
import plotly.colors
from PIL import ImageColor

def get_continuous_color(colorscale, intermed):
    """
    Plotly continuous colorscales assign colors to the range [0, 1]. This function computes the intermediate
    color for any value in that range.

    Plotly doesn't make the colorscales directly accessible in a common format.
    Some are ready to use:
    
        colorscale = plotly.colors.PLOTLY_SCALES["Greens"]

    Others are just swatches that need to be constructed into a colorscale:

        viridis_colors, scale = plotly.colors.convert_colors_to_same_type(plotly.colors.sequential.Viridis)
        colorscale = plotly.colors.make_colorscale(viridis_colors, scale=scale)

    :param colorscale: A plotly continuous colorscale defined with RGB string colors.
    :param intermed: value in the range [0, 1]
    :return: color in rgb string format
    :rtype: str
    """
    if len(colorscale) < 1:
        raise ValueError("colorscale must have at least one color")

    hex_to_rgb = lambda c: "rgb" + str(ImageColor.getcolor(c, "RGB"))

    if intermed <= 0 or len(colorscale) == 1:
        c = colorscale[0][1]
        return c if c[0] != "#" else hex_to_rgb(c)
    if intermed >= 1:
        c = colorscale[-1][1]
        return c if c[0] != "#" else hex_to_rgb(c)

    for cutoff, color in colorscale:
        if intermed > cutoff:
            low_cutoff, low_color = cutoff, color
        else:
            high_cutoff, high_color = cutoff, color
            break

    if (low_color[0] == "#") or (high_color[0] == "#"):
        # some color scale names (such as cividis) returns:
        # [[loc1, "hex1"], [loc2, "hex2"], ...]
        low_color = hex_to_rgb(low_color)
        high_color = hex_to_rgb(high_color)

    return plotly.colors.find_intermediate_color(
        lowcolor=low_color,
        highcolor=high_color,
        intermed=((intermed - low_cutoff) / (high_cutoff - low_cutoff)),
        colortype="rgb",
    )

def normalize_color_val(color_val, data=data):
    return (color_val - min(data.start_time)) / (max(data.start_time - min(data.start_time)))

## add the first bars
fig = px.bar(
    data.sort_values("start_time", ascending=False).loc[~data['teacher'].duplicated()],
    x="teacher", y="start_time", color="start_time",
    color_continuous_scale="Bluered", barmode="group"
)

## add the other bars, these will automatically be grouped
for x,y in data.sort_values("start_time", ascending=False).loc[data['teacher'].duplicated()].itertuples(index=False):
    fig.add_trace(go.Bar(
        x=[x],
        y=[y],
        marker=dict(color=get_color('Bluered', normalize_color_val(y))),
        hovertemplate="teacher=%{x}<br>start_time=%{y}<extra></extra>",
        showlegend=False
    ))

fig.show()

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM