21

I need some help making a set of stacked bar charts in python with matlibplot. My basic code is below but my problems is how to generate the value for bottom for any element beyond the 2nd one efficiently. I can get the example graph to stack correctly (always a,b,c,d from bottom to top)

import numpy as np
import matplotlib.pyplot as plt

ind = np.arange(3)

a = [3,6,9]
b = [2,7,1]
c = [0,3,1]
d = [4,0,3]

p1 = plt.bar(ind, a, 1, color='#ff3333')
p2 = plt.bar(ind, b, 1, color='#33ff33', bottom=a)
p3 = plt.bar(ind, c, 1, color='#3333ff', bottom=[a[j] +b[j] for j in range(len(a))])
p4 = plt.bar(ind, d, 1, color='#33ffff', bottom=[a[j] +b[j] +c[j] for j in range(len(a))])

plt.show()

My final code could have very large number of bars and the ever expanding function bottom = [...] cannot be the best solution. It would be great if you could also explain how I need to derive the value. Is there a numpy function.

Thank you very much!!! PS I have searched for an answer but I did not understand what I could find.

2705114-john
  • 762
  • 1
  • 6
  • 10

4 Answers4

30

I have just recently faced the same problem. Afterwards I decided to wrap it all up in a nice class. For anyone interested you get an implementation of a stacked bar graph class here:

https://github.com/minillinim/stackedBarGraph

It allows scaled stacked graphs as well as setting bar widths and set heights (with scaled inners).

Given a data set like this:

    d = np.array([[101.,0.,0.,0.,0.,0.,0.],
                  [92.,3.,0.,4.,5.,6.,0.],
                  [56.,7.,8.,9.,23.,4.,5.],
                  [81.,2.,4.,5.,32.,33.,4.],
                  [0.,45.,2.,3.,45.,67.,8.],
                  [99.,5.,0.,0.,0.,43.,56.]])

    d_heights = [1.,2.,3.,4.,5.,6.]
    d_widths = [.5,1.,3.,2.,1.,2.]
    d_labels = ["fred","julie","sam","peter","rob","baz"]
    d_colors = ['#2166ac',
                '#fee090',
                '#fdbb84',
                '#fc8d59',
                '#e34a33',
                '#b30000',
                '#777777']

It can make images like this:

stacked bar graph

GPLv3 with love.

minillinim
  • 690
  • 6
  • 10
  • Thanks - how can I get spaces between the bars? – Matt Sep 02 '14 at 21:15
  • I updated the code to allow for gaps. It's actually pretty simple, if you deduct a fixed amount from the widths of the bars then it effectively shrinks them. After that it's just a matter of playing with the xlims. The main function call now has two new paramters, gap and endGaps, The bottom two pictures show examples of these in use. – minillinim Sep 04 '14 at 01:31
  • Love @minillinim's package. It felt too easy. To add a legend, if you set the colors with an array such as `stacked_colors = ['#2166ac', '#fee090', '#fdbb84']` and `cols=stacked_colors`, then it's easy to add a legend to a plot made from a pandas DataFrame: `legends = [] i = 0 for column in df.columns: legends.append(mpatches.Patch(color=stacked_colors[i], label=column)) i+=1 plt.legend(handles=legends) ` – canary_in_the_data_mine Aug 25 '15 at 21:56
16

Converting your values to numpy arrays will make your life easier:

data = np.array([a, b, c, d])
bottom = np.cumsum(data, axis=0)
colors = ('#ff3333', '#33ff33', '#3333ff', '#33ffff')

plt.bar(ind, data[0], color=colors[0])
for j in xrange(1, data.shape[0]):
    plt.bar(ind, data[1], color=colors[j], bottom=bottom[i-1])

Alternatively, to get rid of the nasty particular case for the first bar:

data = np.array([a, b, c, d])
bottom = np.vstack((np.zeros((data.shape[1],), dtype=data.dtype),
                    np.cumsum(data, axis=0)[:-1]))
colors = ('#ff3333', '#33ff33', '#3333ff', '#33ffff')
for dat, col, bot in zip(data, colors, bottom):
    plt.bar(ind, dat, color=col, bottom=bot)
Jaime
  • 65,696
  • 17
  • 124
  • 159
  • 4
    If you are using matplotlib, everything ends up as a ndarray underneath _anyway_. Might as well make your life pleasant as well ;) – tacaswell Sep 27 '13 at 22:19
  • thanks, how can I add labels into that? I have a list of label/names for each of the series I am stacking up, but although I have tried I cannot get them to come out properly. I have also tried to run a simple legend like the below but it has not really worked.: `code`plt.legend((pl[0], pm[0],ph[0],pa[0]),('L','M','H','At'),bbox_to_anchor=[1.05, 0.5], loc='center') – 2705114-john Jan 22 '14 at 20:53
7
[sum(values) for values in zip(a, b, c)]

In Python 2 you can also do

map(sum, zip(a, b, c))

but Python 3 would need

list(map(sum, zip(a, b, c)))

which is less nice.


You could encapsulate this:

def sumzip(*items):
    return [sum(values) for values in zip(*items)]

and then do

p1 = plt.bar(ind, a, 1, color='#ff3333')
p2 = plt.bar(ind, b, 1, color='#33ff33', bottom=sumzip(a))
p3 = plt.bar(ind, c, 1, color='#3333ff', bottom=sumzip(a, b))
p4 = plt.bar(ind, d, 1, color='#33ffff', bottom=sumzip(a, b, c))

too.


If a, b, c and d are numpy arrays you can also do sum([a, b, c]):

a = np.array([3,6,9])
b = np.array([2,7,1])
c = np.array([0,3,1])
d = np.array([4,0,3])

p1 = plt.bar(ind, a, 1, color='#ff3333')
p2 = plt.bar(ind, b, 1, color='#33ff33', bottom=sum([a]))
p3 = plt.bar(ind, c, 1, color='#3333ff', bottom=sum([a, b]))
p4 = plt.bar(ind, d, 1, color='#33ffff', bottom=sum([a, b, c]))
Veedrac
  • 58,273
  • 15
  • 112
  • 169
2

I solved it like this:

import numpy as np

dates = # somehow get a list of dates
labels = # a list of various labels
colors = # somehow get a list of colors

margin_bottom = np.zeros(dates)

for index, label in enumerate(labels):
    values = # get your values for the label at index-th position from somewhere
    ax.bar(
        dates, values, 
        align='center', label=label, color=colors[index], bottom=margin_bottom
    )
    margin_bottom += values # here you simply add it to the previous margin
    # margin_bottom is a numpy array, adding a list will not change that

It's similar to some other solutions, but it doesn't require all of the margins being stored at all time. Instead it "builds" the stacks from bottom up, adding more and more margin with each iteration.

Zelphir Kaltstahl
  • 5,722
  • 10
  • 57
  • 86