Home > OS >  Group bars with different group sizes in Plotly Express bar plot
Group bars with different group sizes in Plotly Express bar plot

Time:11-05

Consider the following dataframe, called data:
dataframe

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:
output of px bar

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?

CodePudding user response:

According to enter image description here

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

enter image description here

  • Related