Categorical x-axis spacing with multiple traces

I have 2 graphs with the solution I require existing somewhere between the two graphs.

Example data:
hour PRODUCT_NAME LOTS hex_with_transparency
523 08:00 Propane FEI -12 #30a2da80
136 09:00 Brent (Fut) 4 #fc4f30FF
527 09:00 Brent (Fut) -207 #fc4f3080

I need to create a plot with positive/negative trades plotted in the same bar, with the negative volume being less opaque, with grouping around the same hours.

The first plot I have creates a legend based on the rgb colours of my data to prevent making the legend include the various rgba less opaque products, and I do this by creating a trace per df row, then adding some formatting. The issue is, apart from offsetting the hour data around each hour for each product, I am not sure if there are any bult-in arguments.

Code for plot 1:
five_thirty_eight_colors = [
β€˜#30a2da’, β€˜#fc4f30’, β€˜#e5ae38’, β€˜#6d904f’, β€˜#8b8b8b’, β€˜#b96db8’, β€˜#ff9e27’, # Original colors
β€˜#1f77b4’, β€˜#d62728’, β€˜#9467bd’, β€˜#2ca02c’, β€˜#bcbd22’, β€˜#17becf’, β€˜#ff7f0e’, # Extra contrasting colors
β€˜#ff6347’, β€˜#9370db’, β€˜#3cb371’, β€˜#f4a460’, β€˜#4682b4’, β€˜#daa520’, β€˜#ff4500’ # Additional harmonious colors
]
unique_products = df_quantity[β€˜PRODUCT_NAME’].unique()

# probably need more colours
product_colors = {product: five_thirty_eight_colors[i % len(five_thirty_eight_colors)] for i, product in
                  enumerate(unique_products)}

df_quantity["hex_with_transparency"] = df_quantity.apply(
    lambda x:
    add_opacity_to_hex_colours(product_colors[x["PRODUCT_NAME"]], 100)
    if x["LOTS"] > 0
    else add_opacity_to_hex_colours(product_colors[x["PRODUCT_NAME"]], 50),
    axis=1)
df_quantity["rgba"] = df_quantity["hex_with_transparency"].apply(hex_colours_to_rbga)

fig_quan_tran = go.Figure()

for product in unique_products:
    rgb_color = product_colors[product]  # Get RGB color for the product
    fig_quan_tran.add_trace(go.Bar(
        x=[None],
        y=[None],
        name=product,
        marker_color=rgb_color,
        showlegend=True,
        width=0,
    ))

for _, row in df_quantity.iterrows():
    fig_quan_tran.add_trace(go.Bar(
        x=[row['hour']],
        y=[row['LOTS']],
        name=row['PRODUCT_NAME'],
        marker_color=row['rgba'],
        showlegend=False,
        hovertemplate=f"{row['PRODUCT_NAME']}<br>LOTS: {row['LOTS']}<br>Hour: {row['hour']}<extra></extra>",
    ))

fig_quan_tran.update_layout(
    title="Trade Actions",
    xaxis_title="Hour",
    yaxis_title="LOTS",
    xaxis=dict(
        type='category',
    ),
    legend_title="Product Name",
    barmode='group',  # Ensure bars are displayed side-by-side by hour without stacking
    bargap=0,  # Remove spacing between bars
    bargroupgap=0.1,
    margin={"pad": 0},
    width=1000
)

this produces this:

the other data is a multicatgeory but I cannot seem to resolve the legend:

x = [
    df_quantity['hour'],
    df_quantity['PRODUCT_NAME']
]
fig_quan_tran_1 = go.Figure()

fig_quan_tran_1.add_bar(x=x, y=df_quantity['LOTS'])
fig_quan_tran_1.data[0].marker.color = df_quantity['rgba']
fig_quan_tran_1.update_layout(height=800,
                              margin=dict(b=250)
                              )

which generates the following graph:

but I want the legend to have the products and want only the hours at the bottom.

ignoring the hex->hex with opacity β†’ rgba, which I will resolve, are there any easily configurable layout settings to resolve my issue?

Main reason for the legend is the streamlit functionality that make sit easier for a user to read/understand the data

Hi @bceka ,

Welcome to the community!

the trial you already made before is close enough to get your result especially the first one, but you need to set multi-categorical axis to group the product that has same time (on x axis).

But the gap of bar in the first plot seems to much, so we need to work with that.

Here is the options to achieve the plots that you described.

By using groupby method from dataframe you can create multi traces based on the name of the products.

And here is the tricks I came up with, use barmode='stack' instead of β€˜group’. By using β€˜stack’ your group of product bar will not have space with other groups. To add space every group of products, bar, I add extra empty trace that have None values.

import plotly.graph_objects as go
import pandas as pd 

months = (['Jan'] *20 )+(['Feb']*20)
lots = [20, 14, 25, 16, 18, 22, 19, 15, 12, 16,
           -2, -4, -5, -6, -8, -2, -9, -5, -2, -6,
           19, 14, 22, 14, 16, 19, 15, 14, 10, 12,
           -19, -14, -22, -14, -16, -19, -15, -14, -10, -12
        ]
products = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]*4
colors_positif = ["rgba(230,80,80,0.8)","rgba(0,219,0,0.8)","rgba(219,219,0,0.8)","rgba(219,0,219,0.8)","rgba(0,0,219,0.8)",
          "rgba(0,219,219,0.8)","rgba(50,255,180,0.8)","rgba(219,88,0,0.8)","rgba(255,192,203,0.8)","rgba(135,206,235,0.8)"]
colors_negatif = [col.replace("0.8","0.4") for col in colors_positif]

colors = (colors_positif+colors_negatif)*2
df = pd.DataFrame({'months': months,
                   'lots': lots,
                   'products': products,
                   'colors':colors})


fig = go.Figure()


# because you want to show legend as names of product,
# you need to set name of trace as the name of product
# to make easy filtering data by name of product you can use `groupby`
for product, df_product in df.groupby(['products']):
    fig.add_trace(go.Bar(
        x=[df_product["months"],df_product["products"]],
        y=df_product["lots"],
        base=0,
        name=product[0],
        marker_line_width=0,
        marker_color=df_product['colors'],
    ))


# optional empty trace, to create space between groups of products     
fig.add_trace(go.Bar(
  x = [df_product["months"], [" "]* len(df_product["products"])],
  y = [None]* len(df_product),
  base=0,
  name = " ",
  showlegend=False))

fig.update_layout(
                font=dict(color="white"),
                barmode='stack',  # Ensure bars are displayed at stack not group, because there is positif and negarif in sampe products
                bargap=0.05,  # Remove spacing between bars
                legend_traceorder="normal", # ordered top-bottom like the input
                xaxis=dict(showgrid=False,linecolor="white",title="Months",dividercolor="white"),
                yaxis=dict(showgrid=False,linecolor="white",title="Lots",dividercolor="white"),
                plot_bgcolor="black",
                paper_bgcolor="black",)
fig.show()

Hey @farispriadi ,

Really appreciate the response.

From what I can tell, trying to replicate your graph, I still have the issue where the multi-categorical x-axis displays the products (which I do not want), and the Hours (your Months) are not displaying when I view it (may be resolution based and I’m not tech-literate enough to resolve this).

I did manage to sort the issue, however. Using something similar to my initial response but applying offsets to move the bars around the central point, which probably isn’t dissimilar to offsetting the times as well.

Code below:

unique_product_hours = sorted(
    list(
        set(f"{a}{b}" for a, b in zip(df_quantity['hour'], df_quantity['PRODUCT_NAME']))
    )
)
num_products_per_hour = df_quantity.groupby('hour')['PRODUCT_NAME'].nunique()

for _, row in df_quantity.iterrows():
    offset_mult = 0.07  # todo generate dynamically
    num_products = num_products_per_hour[row['hour']]
    offset_value = (unique_product_hours.index(f"{row['hour']}{row['PRODUCT_NAME']}") % num_products - (
                num_products - 1) / 2) * offset_mult

    fig_quan_tran.add_trace(go.Bar(
        x=[row['hour']],
        y=[row['LOTS']],
        name=row['PRODUCT_NAME'],
        marker_color=row['rgba'],
        offset=offset_value,
        showlegend=False,
        hovertemplate=f"{row['PRODUCT_NAME']}<br>LOTS: {row['LOTS']}<br>Hour: {row['hour']}<extra></extra>",
        width=0.08  # TODO generate width dynamically
    ))

Image produced from this:

image produced from me applying your code to my solution and the code of me applying it below:

fig_example = go.Figure()

for product, df_product in df_quantity.groupby(["PRODUCT_NAME"]):
    fig_example.add_trace(go.Bar(
        x=[df_product["hour"], df_product["PRODUCT_NAME"]],
        y=df_product["LOTS"],
        base=0,
        name=product[0],
        marker_line_width=0,
        marker_color=df_product['rgba'],
    ))
fig_example.update_layout(
    font=dict(color="white"),
    barmode='stack',
    # Ensure bars are displayed at stack not group, because there is positif and negarif in sampe products
    bargap=0.05,  # Remove spacing between bars
    legend_traceorder="normal",  # ordered top-bottom like the input
    xaxis=dict(showgrid=False, linecolor="white", title="Months", dividercolor="white"),
    yaxis=dict(showgrid=False, linecolor="white", title="Lots", dividercolor="white"),
    plot_bgcolor="black",
    paper_bgcolor="black", )
fig_example.show()

Hi @bceka ,

Try to reduce/modify width and height attributes, you can put inside update_layout

fig_example.update_layout(
    width = 800, # width 800 px
    height= 600, # height 600 px
    font=dict(color="white"),
    barmode='stack',

So yh, the issue with the products remaining on the x-axis remained in that solution.

Hi @bceka,

It seems I missed some points from your previous post.
The output x axis label is just hours and the legend is the name of products, isn’t it ?

If the hours that only you need in xaxis , you can hide product on x axis tick text using labelalias

...
xaxis=dict(showgrid=False,linecolor="white",title="Months",dividercolor="white",
              labelalias={prod: '' for prod in df["products"]}), # hide product tick text using labelalias
yaxis=dict(showgrid=False,linecolor="white",title="Lots",dividercolor="white"),
...

Hope this help.