4

I want to put multiple datasets on a bar graph and stop the smaller bars being obscured by the larger ones, and I don't want to offset them. For example,

bar(0, 1.)

bar(0, 2.)

only shows the second bar of height of 2.0, the first bar is hidden. Is there a way to get matplotlib to draw the bars with the smallest on top? NB I don't want a stacked bar graph or to offset the bars in x-directions.

I can order all the data, from all datasets, by bar height and plot each bar individually in this order, but I'd prefer to plot each bar individually instead plot each dataset in turn Does anyone know a way of doing this?

Many thanks

3 Answers3

6

I know this is an old question, but I came across it for my own purposes, and since it seemed like something I'd do over and over, I put together a wrapper for the hist function (which is what I'll be using; modification to bar should be trivial):

from matplotlib import pyplot as mpl
from numpy import argsort, linspace

def hist_sorted(*args, **kwargs):
    all_ns = []
    all_patches = []

    labels = kwargs.pop('labels', None)
    if not labels:
        labels = ['data %d' % (i+1) for i in range(len(args))]
    elif len(labels) != len(args):
        raise ValueError('length of labels not equal to length of data')

    bins = kwargs.pop('bins', linspace(min(min(a) for a in args),
                                       max(max(a) for a in args),
                                       num = 11))

    for data, label in zip(args, labels):
        ns, bins, patches = mpl.hist(data, bins=bins, label=label, **kwargs)
        all_ns.append(ns)
        all_patches.append(patches)
    z_orders = -argsort(all_ns, axis=0)

    for zrow, patchrow in zip(z_orders, all_patches):
        assert len(zrow) == len(patchrow)
        for z_val, patch in zip(zrow, patchrow): 
            patch.set_zorder(z_val)

    return all_ns, bins, all_patches

This takes the datasets as anonymous arguments, and any labels as keyword arguments (for the legend), as well as any other keyword argument usable with hist.

rflrob
  • 65
  • 1
  • 6
  • This is really nice, but what do you expect to happen if several distributions have the same value for one bin? Right now when I test it only one of them shows up... this makes sense to me, but I was wondering if there was an alternative that made it clear that two distributions are being plotted at a specific bin. Otherwise it's hard to tell if the hidden distribution has a value of 0 or just a matching value. – Yann Jan 06 '12 at 21:31
  • You have to apply argsort twice! When running it only once, argsort[0] will be the index of the smallest element in all_ns, but you want argsort[0] to be the index of all_ns[0] in the sorted array (i.e. z-index of the bar). See also this question: https://stackoverflow.com/questions/17901218/numpy-argsort-what-is-it-doing – NCode Dec 06 '16 at 22:54
3

The bar method will return a matplotlib.patches.Rectangle object. The object has a set_zorder method. Setting the zorder of the first one higher than the second will place it on top.

You could "easily" order the z-order of elements by checking if they are at the same x and zordering based by height.

from matplotlib import pylab
pylab.bar([0, 1], [1.0, 2.0])
pylab.bar([0, 1], [2.0, 1.0])

# loop through all patch objects and collect ones at same x
all_patches = pylab.axes().patches
patch_at_x = {}
for patch in all_patches:
    if patch.get_x() not in patch_at_x: patch_at_x[patch.get_x()] = []
    patch_at_x[patch.get_x()].append(patch)

# custom sort function, in reverse order of height
def yHeightSort(i,j):
    if j.get_height() > i.get_height(): return 1
    else: return -1

# loop through sort assign z-order based on sort
for x_pos, patches in patch_at_x.iteritems():
    if len(patches) == 1: continue
    patches.sort(cmp=yHeightSort)
    [patch.set_zorder(patches.index(patch)) for patch in patches]

pylab.show()

alt text http://img697.imageshack.us/img697/8381/tmpp.png

Mark
  • 106,305
  • 20
  • 172
  • 230
  • Thanks for your answer. If I had a more complicated example, where there are say each data set has two bars as follows: b1s = bar([0, 1], [1.0, 2.0], label=dataset1) b2s =bar([0, 1], [2.0, 1.0], label=dataset2) Then just plotting this the second dataset's bar at x=0 will obscure the first dataset's. So then have to go through each x value plotted and order the bars according to height and give them a new z_order, right? It's a shame there's not an easier way. –  Feb 03 '10 at 18:54
  • @rowanh, see edits, I added some code that'll auto z order all the rectangle patches if they are plotted at the same x coordinate. – Mark Feb 03 '10 at 20:00
  • thanks very much for this. You beat me to it, I was working on doing just this. Cheers. –  Feb 03 '10 at 20:05
1

Original:

>>> from matplotlib import pylab
>>> data1 = [0.3, 0.9, 0.1]
>>> data2 = [3.0, 0.2, 0.5]
>>> colors = ['b','magenta','cyan']
>>> data_list = [data1,data2]
>>> num_bars = len(data_list)
>>> for i, d in enumerate(data_list):
...     for j,value in enumerate(sorted(d,reverse=True)):
...         c = colors[j]
...         obj_list = pylab.bar(i*0.4,value,width=0.8/num_bars,color=c)
... 

You can draw them in order, like this, or do the zorder

Edit:

I spiffed this up a little. Basically, the key is to sort the data for each bar from largest to smallest before calling bar. But you could go back later and do set_zorder etc. In fact, I save the objects returned from bar () just in case you wanted to inspect them.

import numpy as np
from pylab import *

data = [[6.7, 1.5, 4.5], [2.0, 3.25, 5.7]]
w = 0.5
xlocations =  np.array(range(len(data)))+w
colors = ['r','b','cyan']

oL = list()
for x,d in zip(xlocations, data):
    for c,value in zip(colors, sorted(d,reverse=True)):
        b = bar(x, value, width=w, color=c)
        oL.extend(b)
show()
telliott99
  • 7,762
  • 4
  • 26
  • 26