Search code examples
pythonpandasmatplotlibplotfigure

Matplotlib put x ticks above bar


Is there any way to put the x-ticks above each stacked bar in a stacked bar chart, rather than below the x-axis? To be clear, I don't mean place the x-tick above each individual bar in a single stacked bar, I mean place the x-tick above the stacked bar itself. Here's how I'm creating my plot:

df = pd.DataFrame(np.random.randint(1, 5, size=(3200, 3)))    
df.loc[np.random.choice(df.index, size=3190, replace=False), :] = 0

df_select = df[df.sum(axis=1)>1]
fig, ax = plt.subplots()

ax.bar(df_select.index, df_select.iloc[:,0], label = df_select.columns[0], wdith = 15)

if df_select.shape[1] > 1:
    for i in range(1, df_select.shape[1]):
        bottom = df_select.iloc[:,np.arange(0,i,1)].sum(axis=1)
        ax.bar(df_select.index, df_select.iloc[:,i], bottom=bottom, label = 
df_select.columns[i], width = 15)

ax.set_xticks(df_select.index)
plt.legend(loc='best', bbox_to_anchor=(1, 0.5))
plt.xticks(rotation=90, fontsize=8) #this puts the x ticks below the x axis

Additionally, I'd like to place some text at specific points along the x axis. I'm storing these sites in a list:

sites = [19, 173, 1002] # the number and elements of this list vary

So, for example, at x = 173, I'd like to place the text 'site (173)' along with a tick at position 173.

For your reference, I've posted images for what my current code produces, and what I would like to produce: current: https://i.sstatic.net/QDlEP.png goal: https://i.sstatic.net/IJJo4.png


Solution

  • This can be achieved by using the example that @Evan linked to in this comments, namely this one. The important bit is the function named autolabel in the linked example. Changing the values displayed from the y axis values to the x axis values is easy, replace height with rect.get_x(). The trickier bit is putting the values at the top of the bar. The total height of your bars can be found by summing the values in your DataFrame.

    heights = df_select.iloc[:,:].sum(axis=1)
    

    This then needs to be passed to the autolabel function and used as the height of the bars. The x tick labels can be removed using

    ax.get_xaxis().set_ticklabels([])
    

    And you can add further text below your x axis for specific sites by simply using ax.text using the contents of sites as the x location, and setting the y location to be below the axis (-0.5 or something).

    Putting this all together, we get the following working example:

    def autolabel(rects, heights):
        """
        Attach a text label above each bar displaying its height
        Modified for use with a stacked bar chart
        """
        for i, rect in enumerate(rects):
            x = rect.get_x()
            wid = rect.get_width()
            height = heights.values[i]
    
            ax.text(x + wid/2., 1.05*height,
                    '%d' % (int(x) + int((wid/2)+0.5)),
                    ha='center', va='bottom', rotation=90)
    
    
    df = pd.DataFrame(np.random.randint(1, 5, size=(3200, 3)))    
    df.loc[np.random.choice(df.index, size=3190, replace=False), :] = 0
    
    df_select = df[df.sum(axis=1)>1]
    fig, ax = plt.subplots()
    
    ax.bar(df_select.index, df_select.iloc[:,0], label = df_select.columns[0], width = 15)
    
    if df_select.shape[1] > 1:
        for i in range(1, df_select.shape[1]):
            bottom = df_select.iloc[:,np.arange(0,i,1)].sum(axis=1)
            rects1 = ax.bar(df_select.index, df_select.iloc[:,i], bottom=bottom, label = 
    df_select.columns[i], width = 15)
    
    ax.set_xticks(df_select.index)
    ax.get_xaxis().set_ticklabels([])   # turn off the x tick labels
    
    plt.legend(loc='best', bbox_to_anchor=(1, 0.5))
    
    heights = df_select.iloc[:, :].sum(axis=1)
    autolabel(rects1, heights)
    
    # Select 3 random value to add labels below the x axis. Make sure they are
    # within the axis limits
    sites = np.random.choice(df_select.index, 3)
    
    for site in sites:
        ax.text(site, -0.5,
            'site(%s)' % site,
            ha='center', va='bottom',fontsize=6)
    
    plt.show()
    

    Which gives the following graph:

    enter image description here

    Note: this can sometimes look messy because the bars are very thin and spread out, and can be placed close to each other, meaning values may start to overlap.