pythonpandasplotly

Plotly: bar plot with color red<0, green>0, divided by groups


Given a dataframe with 2 groups: (group1, group2), that have values > and < than 0: plot:

My current code however is not coloring as i would expect, and the legend is shown with the same color:

import pandas as pd
import numpy as np
import plotly.express as px 

df = pd.DataFrame( {
    "x" : [1,2,3],
    "group1" : [np.nan, 1, -0.5],
    "group2" : [np.nan, -0.2, 1],  
}).set_index("x")


df_ = df.reset_index().melt(id_vars = 'x')
fig = px.bar(df_, x='x', y='value', color='variable', barmode='group')
fig.update_traces(marker_color=['red' if val < 0 else 'green' for val in df_['value']], marker_line_color='black', marker_line_width=1.5)
fig.show()

OUT with indications of what i want to achieve: enter image description here


Solution

  • To stick with plotly.express, I would add a column to your dataframe, e.g. df_["positive"] with a boolean, and then color your plot by this variable.

    It would look like this:

    import pandas as pd
    import numpy as np
    import plotly.express as px 
    
    df = pd.DataFrame(
        {
            "x": [1, 2, 3],
            "group1": [np.nan, 1, -0.5],
            "group2": [np.nan, -0.2, 1],  
        }
    ).set_index("x")
    
    df_ = df.reset_index().melt(id_vars = 'x')
    
    df_['positive'] = (df_['value'] >= 0)
    
    fig = px.bar(
        df_,
        x = "x",
        y = "value",
        barmode = "group",
        color = "positive",
        color_discrete_map = {
            True: "green",
            False: "red"
        }
    )
    
    fig.update_traces(
        marker_line_color = "black",
        marker_line_width = 1.5
    )
    
    fig.show("browser")
    

    which yields the following : enter image description here

    EDIT following comments

    If you want to keep the colors AND the group distinction within plotly.express, one way could be to add patterns...

    Solution 1 : Every combination has its legend entry

    df = pd.DataFrame(
        {
        "x" : [1, 2, 3],
        "group1" : [np.nan, 1, -0.5],
        "group2" : [np.nan, -0.2, 1],  
        }
    ).set_index("x")
    
    df_ = df.reset_index().melt(id_vars = "x")
    positive = (df_["value"] >= 0)
    df_["positive"] = positive
    df_["sign"] = ["positive" if x else "negative" for x in df_["positive"]]
    
    # Each compbination of color and patterns
    fig = px.bar(
        df_,
        x = "x",
        y = "value",
        barmode = "group",
        color = "sign",
        color_discrete_map = {
            "positive": "green",
            "negative": "red"
        },
        pattern_shape = "variable"
    )
    fig.update_layout(
        legend_title = "Groups & Signs",
        bargap = 0.5,
        bargroupgap = 0.1
    )
    fig.show("browser")
    

    which outputs the following enter image description here

    Solution 2 : Legend only reflects patterns

    # Only patterns in legend
    fig = px.bar(
        df_,
        x = "x",
        y = "value",
        color = "variable",
        barmode = "group",
        pattern_shape = "variable"
    )
    fig.update_layout(
        legend_title = "Groups",
        bargap = 0.5,
        bargroupgap = 0.1
    )
    fig.for_each_trace(
        lambda trace: trace.update(
            marker_color = np.where(
                df_.loc[df_["variable"].eq(trace.name), "value"] < 0,                 
                "red",
                "green"
            )
        )
    )
    fig.show("browser")
    

    which outputs : enter image description here However I was not able to 'remove' the green color from the legend...