Setting spacing between grouped bar plots
Asked Answered
D

3

22

I'm trying to make a grouped bar plot in matplotlib, following the example in the gallery. I use the following:

import matplotlib.pyplot as plt
plt.figure(figsize=(7,7), dpi=300)
xticks = [0.1, 1.1]
groups = [[1.04, 0.96],
          [1.69, 4.02]]
group_labels = ["G1", "G2"]
num_items = len(group_labels)
ind = arange(num_items)
width = 0.1
s = plt.subplot(1,1,1)
for num, vals in enumerate(groups):
    print "plotting: ", vals
    group_len = len(vals)
    gene_rects = plt.bar(ind, vals, width,
                         align="center")
    ind = ind + width
num_groups = len(group_labels)
# Make label centered with respect to group of bars
# Is there a less complicated way?
offset = (num_groups / 2.) * width
xticks = arange(num_groups) + offset
s.set_xticks(xticks)
print "xticks: ", xticks
plt.xlim([0 - width, max(xticks) + (num_groups * width)])
s.set_xticklabels(group_labels)

enter image description here

My questions are:

  1. How can I control the space between the groups of bars? Right now the spacing is huge and it looks silly. Note that I do not want to make the bars wider - I want them to have the same width, but be closer together.

  2. How can I get the labels to be centered below the groups of bars? I tried to come up with some arithmetic calculations to position the xlabels in the right place (see code above) but it's still slightly off... it feels a bit like writing a plotting library rather than using one. How can this be fixed? (Is there a wrapper or built in utility for matplotlib where this is default behavior?)

EDIT: Reply to @mlgill: thank you for your answer. Your code is certainly much more elegant but still has the same issue, namely that the width of the bars and the spacing between the groups are not controlled separately. Your graph looks correct but the bars are far too wide -- it looks like an Excel graph -- and I wanted to make the bar thinner.

Width and margin are now linked, so if I try:

margin = 0.60
width = (1.-2.*margin)/num_items

It makes the bar skinnier, but brings the group far apart, so the plot again does not look right.

How can I make a grouped bar plot function that takes two parameters: the width of each bar, and the spacing between the bar groups, and plots it correctly like your code did, i.e. with the x-axis labels centered below the groups?

I think that since the user has to compute specific low-level layout quantities like margin and width, we are still basically writing a plotting library :)

Decorum answered 22/7, 2012 at 4:30 Comment(0)
R
25

Actually I think this problem is best solved by adjusting figsize and width; here is my output with figsize=(2,7) and width=0.3:

enter image description here

By the way, this type of thing becomes a lot simpler if you use pandas wrappers (i've also imported seaborn, not necessary for the solution, but makes the plot a lot prettier and more modern looking in my opinion):

import pandas as pd        
import seaborn 
seaborn.set() 

df = pd.DataFrame(groups, index=group_labels)
df.plot(kind='bar', legend=False, width=0.8, figsize=(2,5))
plt.show()

enter image description here

Rodrigues answered 7/7, 2015 at 1:35 Comment(0)
V
21

The trick to both of your questions is understanding that bar graphs in Matplotlib expect each series (G1, G2) to have a total width of "1.0", counting margins on either side. Thus, it's probably easiest to set margins up and then calculate the width of each bar depending on how many of them there are per series. In your case, there are two bars per series.

Assuming you left align each bar, instead of center aligning them as you had done, this setup will result in series which span from 0.0 to 1.0, 1.0 to 2.0, and so forth on the x-axis. Thus, the exact center of each series, which is where you want your labels to appear, will be at 0.5, 1.5, etc.

I've cleaned up your code as there were a lot of extraneous variables. See comments within.

import matplotlib.pyplot as plt
import numpy as np

plt.figure(figsize=(7,7), dpi=300)

groups = [[1.04, 0.96],
          [1.69, 4.02]]
group_labels = ["G1", "G2"]
num_items = len(group_labels)
# This needs to be a numpy range for xdata calculations
# to work.
ind = np.arange(num_items)

# Bar graphs expect a total width of "1.0" per group
# Thus, you should make the sum of the two margins
# plus the sum of the width for each entry equal 1.0.
# One way of doing that is shown below. You can make
# The margins smaller if they're still too big.
margin = 0.05
width = (1.-2.*margin)/num_items

s = plt.subplot(1,1,1)
for num, vals in enumerate(groups):
    print "plotting: ", vals
    # The position of the xdata must be calculated for each of the two data series
    xdata = ind+margin+(num*width)
    # Removing the "align=center" feature will left align graphs, which is what
    # this method of calculating positions assumes
    gene_rects = plt.bar(xdata, vals, width)


# You should no longer need to manually set the plot limit since everything 
# is scaled to one.
# Also the ticks should be much simpler now that each group of bars extends from
# 0.0 to 1.0, 1.0 to 2.0, and so forth and, thus, are centered at 0.5, 1.5, etc.
s.set_xticks(ind+0.5)
s.set_xticklabels(group_labels)

Output from my code.

Vaulting answered 22/7, 2012 at 20:37 Comment(6)
Also, note the greatly reduced number of commands, once my comments have been removed. While I think Matplotlib's bar plotting function could use minor improvement in some ways, this is certainly no longer like writing a plotting library. :)Vaulting
thank you for your comment, I replied to you in an edit to my main post.Decorum
From what you've written above, it sounds like you either want the width of the entire graph to be smaller (this can be set in the line which creates the figure) or want the margins themselves to be larger, which will keep the aspect ratio the same. You could also adjust the width and the xdata calculations so there is a margin between each of the bars. Accomplishing this requires only basic algebra. Beyond these three ideas, I have no clue what you're asking.Vaulting
With regards to your complaints about Matplotlib, it's an extremely powerful plotting library and I have made every figure in my last two scientific publications using it. But if you find it too complicated or "too much like writing your own plotting library," no one is stopping you from trying something else.Vaulting
I suppose you could also add an additional margin on the extreme left and extreme right edge of the graph. If we stop and thing about what this actually means, the easiest way to accomplish this would be to set the x-limits from (0.,2.) to something like (-1.,3.). You had the function to set your xlimits in your original script, so I presume that you can figure out how to do that.Vaulting
What I was asking was:how to make the bars skinnier, keeping everything else the same. Quite simple. If the solution is to add additional margins, I'm not sure how that would work. If you have an example, that would be great, if not I'll look for a different solutions.Decorum
T
3

I read an answer that Paul Ivanov posted on Nabble that might solve this problem with less complexity. Just set the index as below. This will increase the spacing between grouped columns.

ind = np.arange(0,12,2)
Traveller answered 22/6, 2014 at 19:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.