pythonpandasmatplotlibseabornhistplot

Set bar with lower value to foreground in histplot


When creating a histogram plot with seaborn, I would like to dynamically put the bar with the lower count to the front. Below is a minimal example where now the blue bar is always in the front, no matter its count. For example, in the second bin, I would like the orange bar to be in the front. Basically, I am looking for something similar to multiple="stack", however without adding the columns up. Is that possible?

import numpy as np
import pandas as pd
import seaborn as sns
sns.set()
df_A = pd.DataFrame(np.random.randint(0, 10, 100), columns=["value"])
df_A["label"] = "A"
df_B = pd.DataFrame(np.random.randint(0, 10, 100), columns=["value"])
df_B["label"] = "B"
df = pd.concat([df_A, df_B])
sns.histplot(df, x="value", bins=np.arange(0, 10, 1), hue="label")

histogram


Solution

  • You could loop through the generated bar containers and compare the heights of corresponding bars. The order of the heights can then be used to set their z-order. Optionally, you could set alpha=1 to make them opaque.

    import numpy as np
    import pandas as pd
    import seaborn as sns
    
    sns.set()
    df_A = pd.DataFrame(np.random.randint(0, 10, 100), columns=["value"])
    df_A["label"] = "A"
    df_B = pd.DataFrame(np.random.randint(0, 10, 100), columns=["value"])
    df_B["label"] = "B"
    df = pd.concat([df_A, df_B])
    ax = sns.histplot(df, x="value", bins=np.arange(0, 10, 1), hue="label")
    
    for bar0, bar1 in zip(ax.containers[0], ax.containers[1]):
        order = np.argsort([bar0.get_height(), bar1.get_height()])
        bar0.set_zorder(4 - order[0])
        bar1.set_zorder(4 - order[1])
    

    z-order of histograms

    Here is an example with more hue values. np.argsort needs to be called twice to get the inverse order.

    import numpy as np
    import pandas as pd
    import seaborn as sns
    
    sns.set()
    df = pd.DataFrame({"value": np.random.normal(0.01, 1, 4000).cumsum(),
                       "label": np.repeat([*"ABCD"], 1000)})
    ax = sns.histplot(df, x="value", bins=30, hue="label", kde=True,
                      line_kws={'zorder': 10, 'lw': 3, 'ls': ':'})
    num_groups = len(df["label"].unique())
    
    for bars in zip(*ax.containers):
        order = np.argsort(np.argsort([b.get_height() for b in bars]))
        for bar, bar_order in zip(bars, order):
            bar.set_zorder(2 + num_groups - bar_order)
    

    ordering histograms depending on heights

    Here is how it would look like for matplotlib histograms (all created using the same bins):

    import matplotlib.pyplot as plt
    import numpy as np
    
    fig, ax = plt.subplots()
    np.random.seed(123)
    bins = np.arange(0, 1.001, 0.1)
    ax.hist(np.random.rand(200) ** 0.5, bins=bins, label='A')
    ax.hist(np.random.rand(100) ** 0.4, bins=bins, label='B')
    ax.hist(np.random.rand(80) ** 0.6, bins=bins, label='C')
    for bars in zip(*ax.containers):
        order = np.argsort(np.argsort([b.get_height() for b in bars]))
        for bar, bar_order in zip(bars, order):
            bar.set_zorder(2 + len(ax.containers) - bar_order)
    ax.legend(loc='upper left')
    plt.show()
    

    matplotlib histogram with lowest in front