How to align several subplot in matplotlib? - python

I'd like to have four plots with images. All channels, and one for Red, Green and Blue channel. For RGB I'd like to plot a color profile across columns and rows. I would like to have the profile plots aligned exactly to the corresponding image. I mean their width and length should match to width and length, of an image. Their heights should be equal.
For now I achieved this effect using GridSpec. I know I can manipualte by changing fig size proportions, but it is not the solution. I need exact fit.
Here is my code.
def show_signal_across_image(self):
fig = plt.figure(figsize=(8, 9), dpi=109)
gs = GridSpec(4, 4,
height_ratios=[10, 2, 10, 2],
width_ratios=[10, 2, 10, 2],
left=0.05,
right=1 - 0.03,
top=0.95,
bottom=0.06,
hspace=0.3,
wspace=0.4)
ax_all = fig.add_subplot(gs[0])
ax_red_img = fig.add_subplot(gs[2])
ax_green_img = fig.add_subplot(gs[8])
ax_blue_img = fig.add_subplot(gs[10])
ax_red_signal_horizontal = fig.add_subplot(gs[6], sharex=ax_red_img)
ax_red_signal_vertical = fig.add_subplot(gs[3], sharey=ax_red_img)
ax_green_signal_horizontal = fig.add_subplot(gs[12], sharex=ax_green_img)
ax_green_signal_vertical = fig.add_subplot(gs[9], sharey=ax_green_img)
ax_blue_signal_horizontal = fig.add_subplot(gs[14], sharex=ax_blue_img)
ax_blue_signal_vertical = fig.add_subplot(gs[11], sharey=ax_blue_img)
signals = self.get_signal_values()
red_horizontal, red_vertical, green_horizontal, green_vertical, blue_horizontal, blue_vertical = signals
horizontal_signals = [red_horizontal, green_horizontal, blue_horizontal]
vertical_signals = [red_vertical, green_vertical, blue_vertical]
max_value_horizontal = max([item for sublist in horizontal_signals for item in sublist])
max_value_vertical = max([item for sublist in vertical_signals for item in sublist])
ax_red_signal_horizontal.plot(red_horizontal)
ax_green_signal_horizontal.plot(green_horizontal)
ax_blue_signal_horizontal.plot(blue_horizontal)
ax_red_signal_vertical.plot(red_vertical, np.arange(len(red_vertical)))
ax_green_signal_vertical.plot(green_vertical, np.arange(len(green_vertical)))
ax_blue_signal_vertical.plot(blue_vertical, np.arange(len(blue_vertical)))
ax_red_signal_vertical.invert_xaxis()
ax_green_signal_vertical.invert_xaxis()
ax_blue_signal_vertical.invert_xaxis()
for ax in [ax_red_signal_horizontal, ax_green_signal_horizontal, ax_blue_signal_horizontal]:
ax.set_ylim(0, max_value_horizontal * 1.1)
for ax in [ax_red_signal_vertical, ax_green_signal_vertical, ax_blue_signal_vertical]:
ax.set_xlim(max_value_vertical * 1.1, 0)
imshow_args = dict(vmin=0, vmax=1, cmap='gray')
plt.subplot(ax_all)
plt.title('All channels')
plt.imshow(self.img, **imshow_args)
plt.subplot(ax_red_img)
plt.title('Red')
plt.imshow(self.red, **imshow_args)
plt.subplot(ax_green_img)
plt.title('Green')
plt.imshow(self.green, **imshow_args)
plt.subplot(ax_blue_img)
plt.title('Blue')
plt.imshow(self.blue, **imshow_args)
plt.show()
I'm using matplotlib 3.1.1 and python 3.7.1.

Related

Sizing figure with variable number of subplots and 2 legends

I'm having a really hard time attempting to properly size a figure with a variable number of subplots (between 3 and 8) and 2 legends that should appear glued to each other.
I also checked every related issue here in stack overflow, but couldn't get any answer to this specific case, due to my need for 2 legends.
The important to me is to get an optimal figure that I save as pdf to include in a report. I tried everything, and in the end the closes I got was with using tight: fig.savefig(f'variations{len(list_variations)}_B.pdf', bbox_inches='tight').
Here is a fully reproducible example (that emulates my code and figures):
list_variations = [0, 1, 2, 3, 4, 5, 6, 7, 8] # Does not work for any option
list_variations = [0, 1, 2] # Works Fine for Option A
n_subplots = len(list_variations)
fig_size = (5.457, n_subplots*3.5/3)
fig, axs = plt.subplots(n_subplots, 1, figsize=fig_size, sharex=True, sharey=True)
labels_upp = ('abdications', 'liner wint.ol.:$\\pm$0.19e', 'liner wint.ol.:$\\pm$0.1e')
labels_low = ('apportions', 'bisections', 'ablations', 'saktis')
for idx in list_variations:
for i, lab_upp in enumerate(labels_upp):
axs[idx].plot(60+i, 0.2, label=lab_upp)
for lab_low in labels_low:
axs[idx].plot(60+i, -0.2, label=lab_low)
axs[idx].set_title(f'Variation {idx}', fontsize=8)
axs[-1].set_xlim((60, 80))
axs[-1].set(ylim=(-1, 1))
axs[-1].set(xlabel='elasticity (e)')
plt.subplots_adjust(hspace=0.25)
# Make Legends (PROBLEM IS HERE)
# Option A - relative to fig
props_leg_upp = dict(facecolor='white', bbox_to_anchor=(0, -0.102, 1, 0.1), mode='expand', loc='upper center')
props_leg_low = dict(facecolor='lightgrey', bbox_to_anchor=(0, -0.172, 1, 0.1), mode='expand', loc='upper center')
upper_leg = fig.legend(labels_upp, ncol=len(labels_upp), **props_leg_upp)
lower_leg = fig.legend(labels_low, ncol=len(labels_low), **props_leg_low)
axs[-1].add_artist(upper_leg)
# Option B - relative to axs[-1]
props_leg_upp = dict(facecolor='white', bbox_to_anchor=(0, -0.262, 1, 0.1), mode='expand', loc='upper center')
props_leg_low = dict(facecolor='lightgrey', bbox_to_anchor=(0, -0.322, 1, 0.1), mode='expand', loc='upper center')
upper_leg = axs[-1].legend(labels_upp, ncol=len(labels_upp), **props_leg_upp)
lower_leg = axs[-1].legend(labels_low, ncol=len(labels_low), **props_leg_low)
axs[-1].add_artist(upper_leg)
I tried every combination of matplotlib.legend properties that I could think of, and in the end I got to these 2 options: A-apply the legend to figure; B-apply the legend to the last axis.
Option A works pretty well for 3 subplots:
In Option B (adding the legend to last axis), that I tried to force the legend to be the same width of the axis, the legends appear on top of each other (although I tried to finetune the bbox_to_anchor properties).
Yet, the biggest problem is when I use a greater number of subplots (e.g. 9 which is the maximum). For these case none of the options work.
Option A:
Option B:
Is there any way that I can make it work for different numbers of subplots, while (ideally) keeping the width of the legends the same as the width of the axis?
To align the legend in the subplot, I would need to set the transform coordinate axis of the legend box. In this case, the settings are added to match the last axis of the subplot. The box values were adjusted manually.
Since the box value parameters are bbox_to_anchor=(x0,y0,x1,y1), in this case y0,y1 are the same value.
import matplotlib.pyplot as plt
list_variations = [0, 1, 2, 3, 4, 5, 6, 7, 8] # Does not work for any option
#list_variations = [0, 1, 2] # Works Fine for Option A
n_subplots = len(list_variations)
fig_size = (5.457, n_subplots*3.5/3)
fig, axs = plt.subplots(n_subplots, 1, figsize=fig_size, sharex=True, sharey=True)
labels_upp = ('abdications', 'liner wint.ol.:$\\pm$0.19e', 'liner wint.ol.:$\\pm$0.1e')
labels_low = ('apportions', 'bisections', 'ablations', 'saktis')
for idx in list_variations:
for i, lab_upp in enumerate(labels_upp):
axs[idx].plot(60+i, 0.2, label=lab_upp)
for lab_low in labels_low:
axs[idx].plot(60+i, -0.2, label=lab_low)
axs[idx].set_title(f'Variation {idx}', fontsize=8)
axs[-1].set_xlim((60, 80))
axs[-1].set(ylim=(-1, 1))
axs[-1].set(xlabel='elasticity (e)')
plt.subplots_adjust(hspace=0.25)
# Make Legends (PROBLEM IS HERE)
# # Option A - relative to fig
props_leg_upp = dict(facecolor='white', bbox_to_anchor=(-0.1, -0.350, 1.2, 0.-0.350), mode='expand', loc='upper center')
props_leg_low = dict(facecolor='lightgrey', bbox_to_anchor=(-0.1, -0.650, 1.2, -0.650), mode='expand', loc='upper center')
upper_leg = fig.legend(labels_upp, ncol=len(labels_upp), bbox_transform=axs[-1].transAxes, **props_leg_upp)
lower_leg = fig.legend(labels_low, ncol=len(labels_low), bbox_transform=axs[-1].transAxes, **props_leg_low)
axs[-1].add_artist(upper_leg)
plt.show()
If you enable the following: list_variations = [0, 1, 2]

What is going wrong with this stacked bar plot?

I really don't understand what's going wrong with this... I've looked through what is pretty simple data several times and have restarted the kernel (running on Jupyter Notebook) and nothing seems to be solving it.
Here's the data frame I have (sorry I know the numbers look a bit silly, this is a really sparse dataset over a long time period, original is reindexed for 20 years):
DATE NODP NVP VP VDP
03/08/2002 0.083623 0.10400659 0.81235517 1.52458E-05
14/09/2003 0.24669167 0.24806379 0.5052293 1.52458E-05
26/07/2005 0.15553726 0.13324796 0.7111538 0.000060983
20/05/2006 0 0.23 0.315 0.455
05/06/2007 0.21280034 0.29139224 0.49579217 1.52458E-05
21/02/2010 0 0.55502195 0.4449628 1.52458E-05
09/04/2011 0.09531311 0.17514162 0.72954527 0
14/02/2012 0.19213217 0.12866237 0.67920546 0
17/01/2014 0.12438848 0.10297326 0.77263826 0
24/02/2017 0.01541347 0.09897548 0.88561105 0
Note that all of the rows add up to 1! I have triple, quadruple checked this...XD
I am trying to produce a stacked bar chart of this data, with the following code, which seems to have worked perfectly for everything else I have been using it for:
NODP = df['NODP']
NVP = df['NVP']
VDP = df['VDP']
VP = df['VP']
ind = np.arange(len(df.index))
width = 5.0
p1 = plt.bar(ind, NODP, width, label = 'NODP', bottom=NVP, color= 'grey')
p2 = plt.bar(ind, NVP, width, label = 'NVP', bottom=VDP, color= 'tan')
p3 = plt.bar(ind, VDP, width, label = 'VDP', bottom=VP, color= 'darkorange')
p4 = plt.bar(ind, VP, width, label = 'VP', color= 'darkgreen')
plt.ylabel('Ratio')
plt.xlabel('Year')
plt.title('Ratio change',x=0.06,y=0.8)
plt.xticks(np.arange(min(ind), max(ind)+1, 6.0), labels=xlabels) #the xticks were cumbersome so not included in this example code
plt.legend()
Which gives me the following plot:
As is evident, 1) NODP is not showing up at all, and 2) the remainder of them are being plotted with the wrong proportions...
I really don't understand what is wrong, it should be really simple, right?! I'm sorry if it is really simple, it's probably right under my nose. Any ideas greatly appreciated!
If you want to create stacked bars this way (so standard matplotlib without using pandas or seaborn for plotting), the bottom needs to be the sum of all the lower bars.
Here is an example with the given data.
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
columns = ['DATE', 'NODP', 'NVP', 'VP', 'VDP']
data = [['03/08/2002', 0.083623, 0.10400659, 0.81235517, 1.52458E-05],
['14/09/2003', 0.24669167, 0.24806379, 0.5052293, 1.52458E-05],
['26/07/2005', 0.15553726, 0.13324796, 0.7111538, 0.000060983],
['20/05/2006', 0, 0.23, 0.315, 0.455],
['05/06/2007', 0.21280034, 0.29139224, 0.49579217, 1.52458E-05],
['21/02/2010', 0, 0.55502195, 0.4449628, 1.52458E-05],
['09/04/2011', 0.09531311, 0.17514162, 0.72954527, 0],
['14/02/2012', 0.19213217, 0.12866237, 0.67920546, 0],
['17/01/2014', 0.12438848, 0.10297326, 0.77263826, 0],
['24/02/2017', 0.01541347, 0.09897548, 0.88561105, 0]]
df = pd.DataFrame(data=data, columns=columns)
ind = pd.to_datetime(df.DATE)
NODP = df.NODP.to_numpy()
NVP = df.NVP.to_numpy()
VP = df.VP.to_numpy()
VDP = df.VDP.to_numpy()
width = 120
p1 = plt.bar(ind, NODP, width, label='NODP', bottom=NVP+VDP+VP, color='grey')
p2 = plt.bar(ind, NVP, width, label='NVP', bottom=VDP+VP, color='tan')
p3 = plt.bar(ind, VDP, width, label='VDP', bottom=VP, color='darkorange')
p4 = plt.bar(ind, VP, width, label='VP', color='darkgreen')
plt.ylabel('Ratio')
plt.xlabel('Year')
plt.title('Ratio change')
plt.yticks(np.arange(0, 1.001, 0.1))
plt.legend()
plt.show()
Note that in this case the x-axis measured in days, and each bar is located at its date. This helps to know the relative spacing between the dates, in case this is important. If it isn't important, the x-positions could be chosen equidistant and labeled via the dates column.
To do so with standard matplotlib, following code would change:
ind = range(len(df))
width = 0.8
plt.xticks(ind, df.DATE, rotation=20)
plt.tight_layout() # needed to show the full labels of the x-axis
Plot the dataframe
# using your data above
df.DATE = pd.to_datetime(df.DATE)
df.set_index('DATE', inplace=True)
ax = df.plot(stacked=True, kind='bar', figsize=(12, 8))
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
# sets the tick labels so time isn't included
ax.xaxis.set_major_formatter(plt.FixedFormatter(df.index.to_series().dt.strftime("%Y-%m-%d")))
plt.show()
Add labels for clarity
By adding the following code before plt.show() you can add text annotations to the bars
# .patches is everything inside of the chart
for rect in ax.patches:
# Find where everything is located
height = rect.get_height()
width = rect.get_width()
x = rect.get_x()
y = rect.get_y()
# The width of the bar is the data value and can used as the label
label_text = f'{height:.2f}' # f'{height:.2f}' if you have decimal values as labels
label_x = x + width - 0.125
label_y = y + height / 2
# don't include label if it's equivalently 0
if height > 0.001:
ax.text(label_x, label_y, label_text, ha='right', va='center', fontsize=8)
plt.show()

Bokeh: How to add a legend and custom color boundaries to an image plot?

I have a two-dimensional array that I want to plot using bokeh's bokeh.plotting.figure.Figure.image. It works wonderful.
Now, I want to add a legend using the colors used for the image. I don't find any example for my case. The legend that I'd like to achieve is similar to the picture.
from bokeh.models import LinearColorMapper, ColorBar
from bokeh.plotting import figure, show
plot = figure(x_range=(0,1), y_range=(0,1), toolbar_location="right")
color_mapper = LinearColorMapper(palette="YlGn9", low=-1, high=1, nan_color="white")
plot.image(image=[ndvi], color_mapper=color_mapper,dh=[1.0], dw=[1.0], x=[0], y=[0])
color_bar = ColorBar(color_mapper=color_mapper,label_standoff=12, border_line_color=None, location=(0,0))
plot.add_layout(color_bar, 'right')
Additionally, I'd like to have some custom color boundaries, with non-fixed intervals. Here is an example how it would be done with matplotlib:
cmap = colors.ListedColormap(['#27821f', '#3fa336', '#6ce362','#ffffff','#e063a8' ,'#cc3b8b','#9e008c','#59044f'])
bounds = [-1000, -500, -100, 0, 50, 100, 300, 500, 10000000]
norm = colors.BoundaryNorm(bounds, cmap.N)
fig, ax = plt.subplots()
ax.imshow(data, cmap=cmap, norm=norm)
You can choose the red-yellow-green palette. In bokeh the name is 'RdYlGn5', where the digit at the end tells how many colors are needed. To use it in a legend, you'ld need to import RdYlGn5 from bokeh.palettes.
For creating the legend, I only know of employing some dummy glyphs as in the code below.
I updated my example with the new requirements of setting custom bounds with non-fixed intervals. This post offers some guidance. Basically, the idea is to use a larger colormap with repeated colors. Such a format doesn't fit for general types of boundaries, but it fits yours, at least when the lowest and highest bound are interpreted to be infinite.
I also tried to layout the legend with some custom spaces to get all labels aligned. A background color is chosen to contrast with the legend entries.
There is a colorbar to verify how the colormap bounds work internally. After verification, you may leave it out. The example image has values from -1000 to 1000 to show how the values outside the strict colormap limits are handled.
Here is an example with dummy data:
from bokeh.models import LinearColorMapper, Legend, LegendItem, ColorBar, SingleIntervalTicker
from bokeh.plotting import figure, show
import numpy as np
x, y = np.meshgrid(np.linspace(0, 10, 1000), np.linspace(0, 10, 1000))
z = 1000*np.sin(x + np.cos(y))
plot = figure(x_range=(0, 1), y_range=(0, 1), toolbar_location="right")
base_colors = ['#27821f', '#3fa336', '#6ce362','#ffffff','#e063a8' ,'#cc3b8b','#9e008c','#59044f']
bounds = [-1000, -500, -100, 0, 50, 100, 300, 500, 10000000]
low = -600
high = 600
bound_colors = []
j = 0
for i in range(low, high, 50):
if i >= bounds[j+1]:
j += 1
bound_colors.append(base_colors[j])
color_mapper = LinearColorMapper(palette=bound_colors, low=low, high=high, nan_color="white")
plot.image(image=[z], color_mapper=color_mapper, dh=[1.0], dw=[1.0], x=[0], y=[0])
# these are a dummy glyphs to help draw the legend
dummy_for_legend = [plot.line(x=[1, 1], y=[1, 1], line_width=15, color=c, name='dummy_for_legend')
for c in base_colors]
legend_labels = [f' < {bounds[1]}'] + \
[('' if l < 0 else ' ' if l < 10 else ' ' if l < 100 else ' ')
+ f'{l} ‒ {h}' for l, h in zip(bounds[1:], bounds[2:-1])] + \
[f' > {bounds[-2]}']
legend1 = Legend(title="NDVI", background_fill_color='gold',
items=[LegendItem(label=lab, renderers=[gly]) for lab, gly in zip(legend_labels, dummy_for_legend) ])
plot.add_layout(legend1)
color_bar = ColorBar(color_mapper=color_mapper, label_standoff=12, border_line_color=None, location=(0, 0),
ticker=SingleIntervalTicker(interval=50))
plot.add_layout(color_bar)
show(plot)

Python/Matplotlib - Find the highest value of a group of bars

I have this image from Matplotlib :
I would like to write for each category (cat i with i in [1-10] in the figure) the highest value and its corresponding legend on the graphic.
Below you can find visually what I would like to achieve :
The thing is the fact that I don't know if it is possible because of the way of plotting from matplotlib.
Basically, this is the part of the code for drawing multiple bars :
# create plot
fig, ax = plt.subplots(figsize = (9,9))
index = np.arange(len_category)
if multiple:
bar_width = 0.3
else :
bar_width = 1.5
opacity = 1.0
#test_array contains test1 and test2
cmap = get_cmap(len(test_array))
for i in range(len(test_array)):
count = count + 1
current_label = test_array[i]
rects = plt.bar(index-0.2+bar_width*i, score_array[i], bar_width, alpha=opacity, color=np.random.rand(3,1),label=current_label )
plt.xlabel('Categories')
plt.ylabel('Scores')
plt.title('Scores by Categories')
plt.xticks(index + bar_width, categories_array)
plt.legend()
plt.tight_layout()
plt.show()
and this is the part I have added in order to do what I would like to achieve. But it searches the max across all the bars in the graphics. For example, the max of test1 will be in cat10 and the max of test2 will be cat2. Instead, I would like to have the max for each category.
for i in range(len(test_array)):
count = count + 1
current_label = test_array[i]
rects = plt.bar(index-0.2+bar_width*i, score_array[i], bar_width,alpha=opacity,color=np.random.rand(3,1),label=current_label )
max_score_current = max(score_array[i])
list_rect = list()
max_height = 0
#The id of the rectangle who get the highest score
max_idx = 0
for idx,rect in enumerate(rects):
list_rect.append(rect)
height = rect.get_height()
if height > max_height:
max_height = height
max_idx = idx
highest_rect = list_rect[max_idx]
plt.text(highest_rect.get_x() + highest_rect.get_width()/2.0, max_height, str(test_array[i]),color='blue', fontweight='bold')
del list_rect[:]
Do you have an idea about how I can achieve that ?
Thank you
It usually better to keep data generation and visualization separate. Instead of looping through the bars themselves, just get the necessary data prior to plotting. This makes everything a lot more simple.
So first create a list of labels to use and then loop over the positions to annotate then. In the code below the labels are created by mapping the argmax of a column array to the test set via a dictionary.
import numpy as np
import matplotlib.pyplot as plt
test1 = [6,4,5,8,3]
test2 = [4,5,3,4,6]
labeldic = {0:"test1", 1:"test2"}
a = np.c_[test1,test2]
maxi = np.max(a, axis=1)
l = ["{} {}".format(i,labeldic[j]) for i,j in zip(maxi, np.argmax(a, axis=1))]
for i in range(a.shape[1]):
plt.bar(np.arange(a.shape[0])+(i-1)*0.3, a[:,i], width=0.3, align="edge",
label = labeldic[i])
for i in range(a.shape[0]):
plt.annotate(l[i], xy=(i,maxi[i]), xytext=(0,10),
textcoords="offset points", ha="center")
plt.margins(y=0.2)
plt.legend()
plt.show()
From your question it is not entirely clear what you want to achieve, but assuming that you want the relative height of each bar in one group printed above that bar, here is one way to achieve that:
from matplotlib import pyplot as plt
import numpy as np
score_array = np.random.rand(2,10)
index = np.arange(score_array.shape[1])
test_array=['test1','test2']
opacity = 1
bar_width = 0.25
for i,label in enumerate(test_array):
rects = plt.bar(index-0.2+bar_width*i, score_array[i], bar_width,alpha=opacity,label=label)
heights = [r.get_height() for r in rects]
print(heights)
rel_heights = [h/max(heights) for h in heights]
idx = heights.index(max(heights))
for i,(r,h, rh) in enumerate(zip(rects, heights, rel_heights)):
plt.text(r.get_x() + r.get_width()/2.0, h, '{:.2}'.format(rh), color='b', fontweight ='bold', ha='center')
plt.show()
The result looks like this:

Matplotlib: How do I make my bar plot fill the entire x-axis?

I have a bar plot drawn in matplotlib as such:
The x-ticks do not span the entire range of x axis. How do I make that happen?
My code is here:
def counter_proportions(counter):
total = sum(counter.values())
proportions = dict()
for key, value in counter.items():
proportions[key] = float(value)/float(total)
return proportions
def categorical_counter_xlabels(counter):
idxs = dict()
for i, key in enumerate(counter.keys()):
idxs[key] = i
return idxs
# Use this dummy data
detailed_hosts = ['Species1' * 3, 'Species2' * 1000, 'Species3' * 20, 'Species4' * 20]
# Create a detailed version of the counter, which includes the exact species represented.
detailed_hosts = []
counts = Counter(detailed_hosts)
props = counter_proportions(counts)
xpos = categorical_counter_xlabels(counts)
fig = plt.figure(figsize=(16,10))
ax = fig.add_subplot(111)
plt.bar(xpos.values(), props.values(), align='center')
plt.xticks(xpos.values(), xpos.keys(), rotation=90)
plt.xlabel('Host Species')
plt.ylabel('Proportion')
plt.title("Proportion of Reassortant Viruses' Host Species")
plt.savefig('Proportion of Reassortant Viruses Host Species.pdf', bbox_inches='tight')
Manual bar spacing
You can gain manual control over where the locations of your bars are positioned (e.g. spacing between them), you did that but with a dictionary - instead try doing it with a list of integers.
Import scipy
xticks_pos = scipy.arange( len( counts.keys() )) +1
plt.bar( xticks_pos, props.values(), align='center')
If you lack scipy and cannot be bothered to install it, this is what arange() produces:
In [5]: xticks_pos
Out[5]: array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
Controlling the margins
Above deals with spacing between bars, and as #JoeKington mentioned in comments the other parts you can control (e.g. if you do not want to control spacing and instead want to restrict margins, etc.):
plt.axis('tight')
plt.margins(0.05, 0)
plt.xlim(x.min() - width, x.max() + width))

Categories