Plotly Express: Prevent bars from stacking when Y-axis catgories have the same name

328 Views Asked by At

I'm new to plotly.

Working with:

  • Ubuntu 20.04
  • Python 3.8.10
  • plotly==5.10.0

I'm doing a comparative graph using a horizontal bar chart. Different instruments measuring the same chemical compounds. I want to be able to do an at-a-glance, head-to-head comparison if the measured value amongst all machines.

The problem is; if the compound has the same name amongst the different instruments - Plotly stacks the data bars into a single bar with segment markers. I very much want each bar to appear individually. Is there a way to prevent Plotly Express from automatically stacking the common bars??

Examples:

CODE

gobardata = []

for blended_name in _df[:20].blended_name: # should always be unique
    ##################################
    # Unaltered compound names
    compound_names = [str(c) for c in _df[_df.blended_name == blended_name]["injcompound_name"].tolist()]
    # Random number added to end of compound_names to make every string unique
    # compound_names = ["{} ({})".format(str(c),random.randint(0, 1000)) for c in _df[_df.blended_name == blended_name]["injcompound_name"].tolist()]
    ##################################
    deltas         = _df[_df.blended_name == blended_name]["delta_rettime"].to_list()

    gobardata.append(
    go.Bar(
        name = blended_name,
        x    = deltas,
        y    = compound_names,
        orientation='h',
    ))

fig = go.Figure(data = gobardata)

fig.update_traces(width=1)        

fig.update_layout(
    bargap=1,
    bargroupgap=.1,        
    xaxis_title="Delta Retention Time (Expected - actual)", 
    yaxis_title="Instrument name(Injection ID)"
)

fig.show()

What I'm getting (Using actual, but repeated, compound names)

Compound actual names

What I want (Adding random text to each compound name to make it unique)

enter image description here

1

There are 1 best solutions below

0
RightmireM On

OK. Figured it out. This is probably pretty klugy, but it consistently works.

Basically...

  • Use go.FigureWidget...
  • ...with make_subplots having a common x-axis...
  • ...controlling the height of each subplot based on number of bars.
  • Every bar in each subplot is added as an individual trace...
  • ...using a dictionary matching bar name to a common color.
  • The y-axis labels for each subplot is a list containing the machine name as [0], and then blank placeholders ('') so the length of the y-axis list matches the number of bars.
  • And manually manipulating the legend so each bar name appears only once.

Graph

# Get lists of total data
all_compounds = list(_df.injcompound_name.unique())
blended_names = list(_df.blended_name.unique())

#################################################################
# The heights of each subplot have to be set when fig is created. 
# fig has to be created before adding traces.  
# So, create a list of dfs, and use these to calculate the subplot heights
dfs = []
subplot_height_multiplier = 20
subplot_heights = []
for blended_name in blended_names:
        df = _df[(_df.blended_name == blended_name)]#[["delta_rettime", "injcompound_name"]]
        dfs.append(df)
        subplot_heights.append(df.shape[0] * subplot_height_multiplier)  
chart_height = sum(subplot_heights) # Prep for the height of the overall chart.
chart_width = 1000

# Make the figure
fig = make_subplots(
    rows=len(blended_names), 
    cols=1, 
    row_heights = subplot_heights, 
    shared_xaxes=True,     
)

# Create the color dictionary to match a color to each compound
_CSS_color = CSS_chart_color_list()
colors = {}
for compound in all_compounds:
    try: colors[compound] = _CSS_color.pop()
    except IndexError:
        # Probably ran out of colors, so just reuse
        _CSS_color = CSS_color.copy()
        colors[compound] = _CSS_color.pop()
    
rowcount = 1
for df in dfs:
        # Add bars individually to each subplot
        bars = []
        for label, labeldf in df.groupby('injcompound_name'):
            fig.add_trace(
                    go.Bar(x             = labeldf.delta_rettime,
                           y             = [labeldf.blended_name.iloc[0]]+[""]*(len(labeldf.delta_rettime)-1),
                           name          = label,
                           marker        = {'color': colors[label]},
                           orientation   = 'h', 
                           ),
                    row=rowcount, 
                    col=1, 
            )
        rowcount += 1
        
# Set figure to FigureWidget 
fig = go.FigureWidget(fig)

# Adding individual traces creates redundancies in the legend.
#  This removes redundancies from the legend
names = set()
fig.for_each_trace(
    lambda trace:
        trace.update(showlegend=False)
        if (trace.name in names) else names.add(trace.name))

fig.update_layout(
    height=chart_height, 
    width=chart_width, 
    title_text="∆ of observed RT to expected RT", 
    showlegend = True, 
)

fig.show()