Matplotlib: Constrain plot width while allowing flexible height - python

What I would like to achive are plots with equal scale aspect ratio, and fixed width, but a dynamically chosen height.
To make this more concrete, consider the following plotting example:
import matplotlib as mpl
import matplotlib.pyplot as plt
def example_figure(slope):
# Create a new figure
fig = plt.figure()
ax = fig.add_subplot(111)
# Set axes to equal aspect ratio
ax.set_aspect('equal')
# Plot a line with a given slope,
# starting from the origin
ax.plot([x * slope for x in range(5)])
# Output the result
return fig
This example code will result in figures of different widths, depending on the data:
example_figure(1).show()
example_figure(2).show()
Matplotlib seems to fit the plots into a certain height, and then chooses the width to accomodate the aspect ratio. The ideal outcome for me would be the opposite -- the two plots above would have the same width, but the second plot would be twice as tall as the first.
Bonus — Difficulty level: Gridspec
In the long run, I would like to create a grid in which one of the plots has a fixed aspect ratio, and I would again like to align the graphs exactly.
# Create a 2x1 grid
import matplotlib.gridspec as gridspec
gs = gridspec.GridSpec(2, 1)
# Create the overall graphic, containing
# the top and bottom figures
fig = plt.figure()
ax1 = fig.add_subplot(gs[0, :], aspect='equal')
ax2 = fig.add_subplot(gs[1, :])
# Plot the lines as before
ax1.plot(range(5))
ax2.plot(range(5))
# Show the figure
fig.show()
The result is this:
So again, my question is: How does one create graphs that vary flexibly in height depending on the data, while having a fixed width?
Two points to avoid potential misunderstandings:
In the above example, both graphs have the same x-axis. This cannot be
taken for granted.
I am aware of the height_ratios option in the gridspec. I can compute
the dimensions of the data, and set the ratios, but this unfortunately
does not control the graphs directly, but rather their bounding boxes,
so (depending on the axis labels), graphs of different widths still occur.
Ideally, the plots' canvas would be aligned exactly.
Another unsolved question is similar, but slightly more convoluted.
Any ideas and suggestions are very welcome, and I'm happy to specify the question further, if required. Thank you very much for considering this!

Have you tried to fix the width with fig.set_figwidth()?

Related

Adjust figure height automatically, when the width is fixed and aspect ratio is defined by figure content

When using matplotlib to prepare publication-ready figures that include text, one wants to avoid re-scaling or stretching the image when including it into the (e.g. LaTeX) document. To achieve this, the standard procedure is to choose a figure width which matches the document in preparation (in LaTeX this is often the \textwidth or \columnwidth). On the other hand, it is usually less important to fix the figure height. If the figure content has a well-defined aspect ratio (for example, when plotting a grid of square images), there is an optimal figure height that maximally fills the figure, avoiding undue white space.
Ideally then, it should be possible to fix the figure width, tell matplotlib to use a constrained-layout, and let it figure out a good figure height.
Consider the following scenario:
import matplotlib.pyplot as plt
import numpy as np
ims = [np.random.random((20, 20)) for i in range(3)]
# Based on the \textwidth in our LaTeX document
width_inch = 4
# We plot the images in a 1x3 grid
fig, axes = plt.subplots(
nrows=1,
ncols=3,
layout="constrained",
linewidth=5,
edgecolor="black") # Uses the default figsize
for ax, im in zip(axes.flatten(), ims):
ax.imshow(im)
# Fix the figure width
fig.set_figwidth(width_inch)
This produces:
Clearly the figure height is much too large. On the other hand, making it too small leaves too much whitespace:
fig, axes = plt.subplots(
nrows=1,
ncols=3,
layout="constrained",
linewidth=5,
edgecolor="black",
figsize=(width_inch, 1))
Is it possible to get matplotlib to calculate and apply an optimal figure height, or does this need to be manually adjusted?

How to create python imshow subplots with same pixel size

I'm trying to create imshow subplots with the same pixel size without having the figure height automatically scaled, but I haven't been able to figure out how.
Ideally, I'm looking for a plot similar to the second picture, without the extra white space (ylim going from -0.5 to 4.5) and maybe centered vertically. My pictures will always have the same width, so maybe if I could fix the subplot width instead of the height that would help. Does anyone have any ideas?
close('all')
f,ax=subplots(1,2)
ax[0].imshow(random.rand(30,4),interpolation='nearest')
ax[1].imshow(random.rand(4,4),interpolation='nearest')
tight_layout()
f,ax=subplots(1,2)
ax[0].imshow(random.rand(30,4),interpolation='nearest')
ax[1].imshow(random.rand(4,4),interpolation='nearest')
ax[1].set_ylim((29.5,-0.5))
tight_layout()
Plot without ylim adjustment:
Plot with ylim adjustment:
In principle you can just make the figure size small enough in width, such that it constrains the widths of the subplots. E.g. figsize=(2,7) would work here.
For an automated solution, you may adjust the subplot parameters, such that the left and right margin constrain the subplot width. This is shown in the code below.
It assumes that there is one row of subplots, and that all images have the same pixel number in horizontal direction.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(1,2)
im1 = ax[0].imshow(np.random.rand(30,4))
im2 = ax[1].imshow(np.random.rand(4,4))
def adjustw(images, wspace="auto"):
fig = images[0].axes.figure
if wspace=="auto":
wspace = fig.subplotpars.wspace
top = fig.subplotpars.top
bottom = fig.subplotpars.bottom
shapes = np.array([im.get_array().shape for im in images])
w,h = fig.get_size_inches()
imw = (top-bottom)*h/shapes[:,0].max()*shapes[0,1] #inch
n = len(shapes)
left = -((n+(n-1)*wspace)*imw/w - 1)/2.
right = 1.-left
fig.subplots_adjust(left=left, right=right, wspace=wspace)
adjustw([im1, im2], wspace=1)
plt.show()
If you need to use tight_layout(), do so before calling the function. Also you would then definitely need to set the only free parameter here, wspace to something other than "auto". wspace=1 means to have as much space between the plots as their width.
The result is a figure where the subplots have the same size in width.

Using color scales as axes in matplotlib

I'm trying to create a visualization that varies color (specifically the H and V values of an HSV color scheme while keeping S constant), while representing the response of a given function to those colors.
Effectively, it's a heat map where the x and y axes are colors rather than numbers. Hunting through the matplotlib gallery I can find a lot of examples based on colorbars such as those found here, and here.
The colorbar implementation is close to what I'm looking for, with these important caveats:
I'm looking to align the colors as ticks on the main figure, rather than adding ticks to the colorbar itself. Principally this calls for making sure the plot and the colorbar are aligned, and I haven't found any way of actually guaranteeing this.
I'm trying to ensure that the color bar will display on the left of the figure (in place of the standard x-axis) rather than to the right.
The second point sounds trivial, but I haven't found any documented way of achieving it unfortunately.
Is there any way of creating a plot like this in matplotlib that would be considerably less effort than creating it from scratch in d3 or a similar lower-level visualization library?
I'm still not quite sure about it; but I'll give a try. Sorry if I misunderstood it.
Major thoughts are using GridSpec to solve your two requirements: aligning the "color axes" and put them beside the classic axes. The alignment should be correct because corresponding axes between ax_x/ax_y and the main ax are the same.
import matplotlib.pyplot as plt
from matplotlib.colors import hsv_to_rgb
from matplotlib.gridspec import GridSpec
import numpy as np
# Create a spectrum sample
# Convert HSV to RGB so that matplotlib can plot;
# hsv_to_rgb assumes values to be in range [0, 1]
N = 0.001
v_y, h_x = np.mgrid[0:1:N, 0:1:N]
c = hsv_to_rgb(np.stack([h_x, np.ones(h_x.shape), v_y], axis=2))
c_x = hsv_to_rgb(np.stack([h_x, np.ones(h_x.shape), np.zeros(v_y.shape)], axis=2))
c_y = hsv_to_rgb(np.stack([np.zeros(h_x.shape), np.ones(h_x.shape), v_y], axis=2))
fig = plt.figure()
# Ratio to adjust width for "x axis" and "y axis"
fig_ratio = np.divide(*fig.get_size_inches())
gs = GridSpec(2, 2, wspace=0.0, hspace=0.0,
width_ratios=[1, 20], height_ratios=[20/fig_ratio, 1])
# Lower-left corner is ignored
ax_y = plt.subplot(gs[0])
ax = plt.subplot(gs[1])
ax_x = plt.subplot(gs[3])
# Image are stretched to fit the ax since numbers are hided or not important in this figure.
img = ax.imshow(c, aspect='auto', origin='lower')
# Colorbar on img won't give correct results since it is plot with raw RGB values
img_x = ax_x.imshow(c_x, aspect='auto', origin='lower')
img_y = ax_y.imshow(c_y, aspect='auto', origin='lower')
# Remove ticks and ticklabels
for ax in [ax_y, ax, ax_x]:
ax.tick_params(left=False, bottom=False,
labelleft=False, labelbottom=False)
plt.show()
Response to the comment:
To clarify, you're making three plots, and using imshow plots as axes by assigning them to quadrants of the grid?
Yes, it's a 2x2 grid and I ignored the lower-left one. The documentation might not be great but what I did is similar to this part.
And presumably if I wanted to add space between the axes here and the main plot I would increase wspace and hspace?
Yes, it is briefly demonstrated in this part of documentation. Besides, I adjusted it with width_ratios and height_ratios so that 3 parts of the figure are not the same size, like this.
Also, just to confirm, there is a fully black axis on the bottom of this image, and it's not a misalignment of the left axis.
The bottom is the colored x axis. It is black because I thought it corresponds to v=0. If you change
c_x = hsv_to_rgb(np.stack([h_x, np.ones(h_x.shape), np.zeros(v_y.shape)], axis=2))
to
c_x = hsv_to_rgb(np.stack([h_x, np.ones(h_x.shape), np.ones(v_y.shape)], axis=2))
You would get this figure, proving it's not misaligned:
If it's easier, you can also ignore the whole hsv thing, use a gray box or something as the central image.
I'm sorry but I'm really slow on this. I'm still having no idea what you want to show in the figure. So I don't know how to help. If you remove or comment out the line
img = ax.imshow(c, aspect='auto', origin='lower')
You got this:

Avoid overlapping ticks in matplotlib

I am generating plots like this one:
When using less ticks, the plot fits nicely and the bars are wide enough to see them correctly. Nevertheless, when there are lots of ticks, instead of making the plot larger, it just compress the y axe, resulting in thin bars and overlapping tick text.
This is happening both for plt.show() and plt.save_fig().
Is there any solution so it plots the figure in a scale which guarantees that bars have the specified width, not more (if too few ticks) and not less (too many, overlapping)?
EDIT:
Yes, I'm using barh, and yes, I'm setting height to a fixed value (8):
height = 8
ax.barh(yvalues-width/2, xvalues, height=height, color='blue', align='center')
ax.barh(yvalues+width/2, xvalues, height=height, color='red', align='center')
I don't quite understand your code, it seems you do two plots with the same (only shifted) yvalues, but the image doesn't look so. And are you sure you want to shift by width/2 if you have align=center? Anyways, to changing the image size:
No, I am not sure there is no other way, but I don't see anything in the manual at a glance. To set image size by hand:
fig = plt.figure(figsize=(5, 80))
ax = fig.add_subplot(111)
...your_code
the size is in cm. You can compute it beforehand, try for example
import numpy as np
fig_height = (max(yvalues) - min(yvalues)) / np.diff(yvalue)
this would (approximately) set the minimum distance between ticks to a centimeter, which is too much, but try to adjust it.
I think of two solutions for your case:
If you are trying to plot a histogram, use hist function [1]. This will automatically bin your data. You can even plot multiple overlapping histograms as long as you set alpha value lower than 1. See this post
import matplotlib.pyplot as plt
import numpy as np
x = mu + sigma*np.random.randn(10000)
plt.hist(x, 50, normed=1, facecolor='green',
alpha=0.75, orientation='horizontal')
You can also identify interval of your axis ticks. This will place a tick every 10 items. But I doubt this will solve your problem.
import matplotlib.ticker as ticker
...
ax.yaxis.set_major_locator(ticker.MultipleLocator(10))

matplotlib - subplots with fixed aspect ratio

I have a problem with plotting multiple subplots. I would like to set the PHYSICAL aspect ratio of the subplots to a fixed value.
In my example I have 12 subplots (4 rows and 3 columns) on a landscape A4 figure. There all subplots are nicely placed on the whole figure, and for all subplots the height is nearly equal to the width.
But if I change the layout of my figure to portrait, the subplots are stretched vertically.
And this is exactly what should not happen. I would like to have the same height and width of the subplots as on the landscape figure. Is there a possibility that the aspect ratio of the subplots stay the same?
Thanks in advance,
Peter
EDIT:
I have found a workaround. But this just works for non-logarithmic axes...
aspectratio=1.0
ratio_default=(ax.get_xlim()[1]-ax.get_xlim()[0])/(ax.get_ylim()[1]-ax.get_ylim()[0])
ax.set_aspect(ratio_default*aspectratio)
Actually, what you're wanting is quite simple... You just need to make sure that adjustable is set to 'box' on your axes, and you have a set aspect ratio for the axes (anything other than 'auto').
You can either do this with the adjustable kwarg when you create the subplots. Alternatively, you can do this after their creation by calling ax.set_adjustable('box'), or by calling ax.set_aspect(aspect, adjustable='box') (where aspect is either 'equal' or a number).
Now, regardless of how the figure is resized, the subplots will maintain the same aspect ratio.
For example:
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(2,1,1, adjustable='box', aspect=0.3)
ax2 = fig.add_subplot(2,1,2)
ax1.plot(range(10))
ax2.plot(range(10))
plt.show()
Now, compare how the top subplot responds to resizing, vs. how the bottom subplot responds:
The initial plot
Resized to a vertical layout:
Resized to a horizontal layout:
Your workaround works for me. After plotting the data, I call the following function:
def fixed_aspect_ratio(ratio):
'''
Set a fixed aspect ratio on matplotlib plots
regardless of axis units
'''
xvals,yvals = gca().axes.get_xlim(),gca().axes.get_ylim()
xrange = xvals[1]-xvals[0]
yrange = yvals[1]-yvals[0]
gca().set_aspect(ratio*(xrange/yrange), adjustable='box')
In reply to the remark about the solution not working for logarithmic plots in the edit to the original question - you need to adapt as follows:
def fixed_aspect_ratio_loglog(ratio):
'''
Set a fixed aspect ratio on matplotlib loglog plots
regardless of axis units
'''
xvals,yvals = gca().axes.get_xlim(),gca().axes.get_ylim()
xrange = log(xvals[1])-log(xvals[0])
yrange = log(yvals[1])-log(yvals[0])
gca().set_aspect(ratio*(xrange/yrange), adjustable='box')
(Adaptation for semilog plots should now be obvious)

Categories

Resources