Search code examples
pythonpandasmatplotlibmulti-index

Chart with horizontal bar subcharts for each index bin in dataframe


I have dataframe with multi-index (sorted by "5min_intervals" and "price" indexes).

                           quantity
5min_intervals       price 
2023-07-27 17:40:00  172.20     330
                     172.19       1
2023-07-27 17:45:00  172.25       4
                     172.24      59
                     172.23     101
                     172.22     224
                     172.21      64
                     172.20     303
                     172.19     740
                     172.18      26
2023-07-27 17:50:00  172.17      30
                     172.16       2
                     172.15    1014
                     172.14     781
                     172.13    1285

I know about simple horizontal bar chart with df.plot.barh(). Also I can iterate dataframe by '5min_intervals' index with

for date in df.index.levels[0]:
    print(df.loc[date])

and get dataframes for each '5min_intervals' index like below

        quantity
price           
172.20       330
172.19         1

Is there any way to create one chart with matplotlib where for each '5min_intervals' index would be horizontal bar chart. Schematically it looks like in below picture enter image description here


Solution

  • Let's prepare some dummy data to work with:

    from pandas import DataFrame, Timestamp
    
    data = {
        'quantity': {
            (Timestamp('2023-07-27 17:40:00'), 172.2): 330,
            (Timestamp('2023-07-27 17:40:00'), 172.19): 1,
            (Timestamp('2023-07-27 17:45:00'), 172.25): 4,
            (Timestamp('2023-07-27 17:45:00'), 172.24): 59,
            (Timestamp('2023-07-27 17:45:00'), 172.23): 101,
            (Timestamp('2023-07-27 17:45:00'), 172.22): 224,
            (Timestamp('2023-07-27 17:45:00'), 172.21): 64,
            (Timestamp('2023-07-27 17:45:00'), 172.2): 303,
            (Timestamp('2023-07-27 17:45:00'), 172.19): 740,
            (Timestamp('2023-07-27 17:45:00'), 172.18): 26,
            (Timestamp('2023-07-27 17:50:00'), 172.17): 30,
            (Timestamp('2023-07-27 17:50:00'), 172.16): 2,
            (Timestamp('2023-07-27 17:50:00'), 172.15): 1014,
            (Timestamp('2023-07-27 17:50:00'), 172.14): 781,
            (Timestamp('2023-07-27 17:50:00'), 172.13): 1285
        }
    }
    
    df = DataFrame(data)
    

    As for the graphics, there's hardly anything like you asked ready from the box. So we need to build it manually. At first, let's do some basic preparation:

    unique_time = df.index.get_level_values(0).unique().sort_values()
    unique_price = df.index.get_level_values(1).unique().sort_values()
    
    spacing = 0.2   # a minimum distance between two consecutive horizontal lines
    values = (1-spacing) * df/df.max()   # relative lengths of horizontal lines
    base = DataFrame(index=unique_price)   # the widest blank frame with prices
    

    To build the graphics, we can use barh(y, width, height, left) - horizontal bar - with prices as the first parameter, time as left shifting, values in place of width, and some fixed small height. To make very small values visible, we can additionally mark the beginning (left end) of a line with a small tick using barh again.

    import matplotlib.pyplot as plt
    from matplotlib.colors import TABLEAU_COLORS
    from itertools import cycle
    
    fig, ax = plt.subplots(figsize=(7,7))
    
    xlabels = unique_time.astype(str)
    ylabels = unique_price.astype(str)
    
    ax.set_xticks(range(len(xlabels)), xlabels, rotation=45)
    ax.set_yticks(range(len(ylabels)), ylabels)
    ax.set_xlim([-0.5,len(xlabels)])
    ax.set_ylim([-1, len(ylabels)])
    
    for i, (t, c) in enumerate(zip(unique_time, cycle(TABLEAU_COLORS))):
        pr = base.join(values.loc[t]).squeeze()
        # draw horizontal lines, shifted left by i-th timepoint
        ax.barh(ylabels, pr, 0.1, i, color=c)
        # put tics at the left end of lines, i.e. their beginning
        ax.barh(ylabels, 0.01*pr.notna(), 0.3, i, color=c)
    
    ax.grid(axis='y', linestyle='--', linewidth=0.5)
    fig.tight_layout()
    plt.show()
    

    And here's the output: desired figure


    python           : 3.11
    pandas           : 1.5.1
    matplotlib       : 3.6.1