Changing canvas size dynamically - python

I want to plot multiple time-series (each time-series in its own plot) using the plot()-method of matplotlib.
X-Axis: Time
Y-Axis: Parameter-Value
As the time-series have different lengths, I want to resize the canvas along the X-Axis dynamically, so that the time-series do not get stretched/compressed dependent on their total length. The size of the whole figure should stay the same, independent of the time-series-length. I know how to modify the figure size using
rcParams['figure.figsize'] = width, height
but I do only want to modify the canvas size (the part of the figure where the time-series is actually plotted in). Is there a similar way of just changing the figure's canvas?

I think you want to change the dimensions of the axes that your time-series is being plotted in, rather than the dimensions of the figure canvas (which as far as I'm aware can't be altered without changing the overall figure size).
You can do this using ax.set_position(), which takes a tuple of (left, bottom, width, height) values in normalized canvas coordinates between 0 and 1.

from pylab import *
nr = 4
nc = 1
fig,axes = subplots(nr,nc,sharex=True)
The sharex keyword tells the subplots to keep their x limits the same. Replace plot in your application with axes[ith index].plot, etc.

Related

Is there some method to fix the scale of an axis in matplotlib?

I have some math calculations and I want to visualize the result of this calculations. So I want to see changes of some line with time. And axes scale should be constant in whole period of time for best understanding of results. But in my program there are changes of axes scale:
initial frame, last frame
There is some part of my redraw function:
def redraw(k):
# math calculations
ax.clear()
xlocs = np.linspace(ball.r1, env.r2, 5)
ylocs = np.linspace(env.t0, ball.t0, 5)
ax.set_xticks(xlocs)
ax.set_yticks(ylocs)
# creating the line
So I create arrays in which there are marks which I want to see on axes. But functions ax.set_xticks() and ax.set_yticks() don't work or I misunderstand how they work
set_yticks sets the positions of the ticks on the y-axis. In order to set the size of the y-axis itself you want to use set_ylim:
ax.set_ylim(min(ylocs), max(ylocs))

How do I get a full-height vertical line with a legend label in holoviews + bokeh?

I want to plot a vertical line in holoviews with the bokeh backend which has a label that shows up in my legend. I need this line to be the full height of the plot, regardless of whether it is alone or overlaid with other elements. How can I achieve this?
Example
I'm adding in a curve plot in the example because otherwise even elements that can appear in a legend just use their label as a title.
import numpy as np
import holoviews as hv
hv.extension("bokeh")
x = np.linspace(0, 1)
curve = hv.Curve((x, np.sin(x)), label="sin(x)")
vline = hv.VLine(0.5, label="vline")
curve * vline
This gives the following plot:
which has no label for the vertical line. How do I get the label to show up?
As mentioned in this issue but not yet in the docs, VLine and HLine don't appear in legends, and there is no plan to add support for them (basically, in bokeh they're created differently, so there's not an easy way to put them in the legend). One can use Spikes instead. However, as documented in another issue, spikes don't overlay well. In particular, they don't adjust their height to be the full height of the plot if given no explicit height. Here are two workarounds that I've come up with.
Workaround 1
You can explicitly find out the height of the other element that the vertical line should be overlaid with and use this to create a spike of the proper height. This works, but it's rather brittle because you need to adapt it with full knowledge of everything that could be overlaid with the spike.
import numpy as np
import holoviews as hv
hv.extension("bokeh")
x = np.linspace(0, 1)
curve = hv.Curve((x, np.sin(x)), label="sin(x)")
height = curve.data["y"].max() - curve.data["y"].min()
spikes = hv.Spikes(([0.5], [height]), vdims="height", label="mid")
spikes * curve
Workaround 2
This uses both a VLine and a Spikes. The spike won't be visible except that it will contribute an entry to the legend. The vline will be on top of the spike, and vlines already adjust themselves to fill the whole height of the figure. This requires creating an extra element, but it is more robust because you can overlay the product of this spike and vline with any other elements and still get a line that fills the height of the plot and appears in the legend. As the legend entry is based on the spike, though, it will only look like the vline if you make sure that they have a similar appearance (e.g. the vline and the spike have the same color).
# need to make sure the colors are the same for spikes/vlines
# would look a bit better if I adjusted the spike thickness too
spikes = hv.Spikes([0.5], label="mid").opts(color="black")
vline = hv.VLine(0.5).opts(color="black")
spikes * curve * vline
In the future, Spikes will hopefully scale themselves to be full-height when not explicitly given a height, and then these workarounds won't be needed.
Based on Nathans 2 workarounds, there is a 3rd very easy workaround using Curve
import numpy as np
import holoviews as hv
hv.extension("bokeh")
x = np.linspace(0, 1)
curve = hv.Curve((x, np.sin(x)), label="sin(x)")
height = (curve.data["y"].min(), curve.data["y"].max())
xpos = 0.5
spikes = hv.Curve(([xpos]*2, height), label="mid")
spikes * curve

Making plot less crowded in Matplotlib

I have made a error-bar plot using matplotlib but it is too crowded to see everything without zooming in. I can of course do this but when I save the plot I can only save the zoomed in bit and lose the rest of the data.
Is there a way to get a scrollable plot with matplotlib so that when I save it as a png everything is included or any other format such that no data is lost? Essentially I would like the length of the plot to be much greater than the width.
The code I used to plot is:
plot1_dataerr = get_plot_data_errbars(processed_answers[0][plot_low:]) #the data to be plotted, the zeroth element of this is the labels, the first is the means and the second is the errorbar size
fig, axs = plt.subplots()
fig.subplots_adjust(left=0.2)
axs.set_xlim([1,5])
axs.grid()
axs.errorbar(plot1_dataerr[1],range(len(plot1_dataerr[1])),xerr = plot1_dataerr[2], fmt = 'k o')
axs.yaxis.set_ticks(np.arange(len(plot1_dataerr[1])))
axs.set_yticklabels(plot1_dataerr[0])
And here is the plot I am getting, as you can see it is very crowded and unclear:
You may increase the size of the plot
plt.subplots(figsize=(18,10))
This of course has a limitation for showing the plot on the screen. So you may as well or additionally, decrease the dots per inch,
plt.subplots(figsize=(36,20), dpi=50)
Then saving the plot in a vector format like pdf will allow you not to loose any details in the saved figure.
You may also keep the dpi, increase the figuresize and finally show your plot in a window with scrollbars. This is shown e.g. in the question Scrollbar on Matplotlib showing page
You can specify a bigger figure size in the subplots() constructor.
fig, axs = plt.subplots(figsize=(w, h))
Where w and h are the width and height in inches.

Precise control over subplot locations in matplotlib

I am currently producing a figure for a paper, which looks like this:
The above is pretty close to how I want it to look, but I have a strong feeling that I'm not doing this the "right way", since it was really fiddly to produce, and my code is full of all sorts of magic numbers where I fine-tuned the positioning by hand. Thus my question is, what is the right way to produce a plot like this?
Here are the important features of this plot that made it hard to produce:
The aspect ratios of the three subplots are fixed by the data, but the images are not all at the same resolution.
I wanted all three plots to take up the full height of the figure
I wanted (a) and (b) to be close together since they share their y axis, while (c) is further away
Ideally, I would like the top of the top colour bar to exactly match the top of the three images, and similarly with the bottom of the lower colour bar. (In fact they aren't quite aligned, because I did this by guessing numbers and re-compiling the image.)
In producing this figure, I first tried using GridSpec, but I wasn't able to control the relative spacing between the three main subplots. I then tried ImageGrid, which is part of the AxisGrid toolkit, but the differing resolutions between the three images caused that to behave strangely. Delving deeper into AxesGrid, I was able to position the three main subplots using the append_axes function, but I still had to position the three colourbars by hand. (I created the colourbars manually.)
I'd rather not post my existing code, because it's a horrible collection of hacks and magic numbers. Rather my question is, is there any way in MatPlotLib to just specify the logical layout of the figure (i.e. the content of the bullet points above) and have the layout calculated for me automatically?
Here is a possible solution. You'd start with the figure width (which makes sense when preparing a paper) and calculate your way through, using the aspects of the figures, some arbitrary spacings between the subplots and the margins. The formulas are similar to the ones I used in this answer. And the unequal aspects are taken care of by GridSpec's width_ratios argument.
You then end up with a figure height such that the subplots' are equal in height.
So you cannot avoid typing in some numbers, but they are not "magic". All are related to acessible quatities like fraction of figure size or fraction of mean subplots size. Since the system is closed, changing any number will simply produce a different figure height, but will not destroy the layout.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np; np.random.seed(42)
imgs = []
shapes = [(550,200), ( 550,205), (1100,274) ]
for shape in shapes:
imgs.append(np.random.random(shape))
# calculate inverse aspect(width/height) for all images
inva = np.array([ img.shape[1]/float(img.shape[0]) for img in imgs])
# set width of empty column used to stretch layout
emptycol = 0.02
r = np.array([inva[0],inva[1], emptycol, inva[2], 3*emptycol, emptycol])
# set a figure width in inch
figw = 8
# border, can be set independently of all other quantities
left = 0.1; right=1-left
bottom=0.1; top=1-bottom
# wspace (=average relative space between subplots)
wspace = 0.1
#calculate scale
s = figw*(right-left)/(len(r)+(len(r)-1)*wspace)
# mean aspect
masp = len(r)/np.sum(r)
#calculate figheight
figh = s*masp/float(top-bottom)
gs = gridspec.GridSpec(3,len(r), width_ratios=r)
fig = plt.figure(figsize=(figw,figh))
plt.subplots_adjust(left, bottom, right, top, wspace)
ax1 = plt.subplot(gs[:,0])
ax2 = plt.subplot(gs[:,1])
ax2.set_yticks([])
ax3 = plt.subplot(gs[:,3])
ax3.yaxis.tick_right()
ax3.yaxis.set_label_position("right")
cax1 = plt.subplot(gs[0,5])
cax2 = plt.subplot(gs[1,5])
cax3 = plt.subplot(gs[2,5])
im1 = ax1.imshow(imgs[0], cmap="viridis")
im2 = ax2.imshow(imgs[1], cmap="plasma")
im3 = ax3.imshow(imgs[2], cmap="RdBu")
fig.colorbar(im1, ax=ax1, cax=cax1)
fig.colorbar(im2, ax=ax2, cax=cax2)
fig.colorbar(im3, ax=ax3, cax=cax3)
ax1.set_title("image title")
ax1.set_xlabel("xlabel")
ax1.set_ylabel("ylabel")
plt.show()

Changing aspect ratio of subplots in matplotlib

I have created a series of simple greyscale images which I have plotted in a grid (unfortunately, can't upload an image because I don't have a high enough reputation :( ).
The pseudo-code is
# Define matplotlib PyPlot object
nrow = 8
ncol = 12
fig, axes = plt.subplots(nrow, ncol, subplot_kw={'xticks': [], 'yticks': []})
fig.subplots_adjust(hspace=0.05, wspace=0.05)
# Sample the fine scale model at random well locations
for ax in axes.flat:
plot_data = # some Python code here to create 2D grey scale array...
# ... create sub-plot
img = ax.imshow(plot_data, interpolation='none')
img.set_cmap('gray')
# Display the plot
plt.show()
I want to change the aspect ratio so that the plots are squashed vertically and stretched horizontally. I have tried using ax.set_aspect and passing 'aspect' as a subplot_kw argument but to no avail. I also switched 'autoscale' off but I can then only see a handful of pixels. All suggestions welcome!
Thanks in advance!!
#JoeKington - thank you! That was a great reply!! Still trying to get my head around it all. Thanks also to the other posters for their suggestions. So, the original plot looked like this: http://imgur.com/Wi6v4cs
When I set' aspect='auto'' the plot looks like this: http://imgur.com/eRBO6MZ
which is a big improvement. All I need to do now is adjust the subplot size so that sub-plots are plotted in a portrait aspect ratio of eg 2:1, but with the plot filling the entire sub-plot. I guess 'colspan' would do this?
The Short Answer
You're probably wanting to call:
ax.imshow(..., aspect='auto')
imshow will set the aspect ratio of the axes to 1 when it is called, by default. This will override any aspect you specify when you create the axes.
However, this is a common source of confusion in matplotlib. Let me back up and explain what's going on in detail.
Matplotlib's Layout Model
aspect in matplotlib refers to the ratio of the xscale and yscale in data coordinates. It doesn't directly control the ratio of the width and height of the axes.
There are three things that control the size and shape of the "outside box" of a matplotlib axes:
The size/shape of the Figure (shown in red in figures below)
The specified extent of the Axes in figure coordinates (e.g. the subplot location, shown in green in figures below)
The mechanism that the Axes uses to accommodate a fixed aspect ratio (the adjustable parameter).
Axes are always placed in figure coordinates in other words, their shape/size is always a ratio of the figure's shape/size. (Note: Some things such as axes_grid will change this at draw time to get around this limitation.)
However, the extent the axes is given (either from its subplot location or explicitly set extent) isn't necessarily the size it will take up. Depending on the aspect and adjustable parameters, the Axes will shrink inside of its given extent.
To understand how everything interacts, let's plot a circle in lots of different cases.
No Fixed Aspect
In the basic case (no fixed aspect ratio set for the axes), the axes will fill up the entire space allocated to it in figure coordinates (shown by the green box).
The x and y scales (as set by aspect) will be free to change independently, distorting the circle:
When we resize the figure (interactively or at figure creation), the axes will "squish" with it:
Fixed Aspect Ratio, adjustable='box'
However, if the aspect ratio of the plot is set (imshow will force the aspect ratio to 1, by default), the Axes will adjust the size of the outside of the axes to keep the x and y data ratios at the specified aspect.
A key point to understand here, though, is that the aspect of the plot is the aspect of the x and y data scales. It's not the aspect of the width and height of the plot. Therefore, if the aspect is 1, the circle will always be a circle.
As an example, let's say we had done something like:
fig, ax = plt.subplots()
# Plot circle, etc, then:
ax.set(xlim=[0, 10], ylim=[0, 20], aspect=1)
By default, adjustable will be "box". Let's see what happens:
The maximum space the Axes can take up is shown by the green box. However, it has to maintain the same x and y scales. There are two ways this could be accomplished: Change the x and y limits or change the shape/size of the Axes bounding box. Because the adjustable parameter of the Axes is set to the default "box", the Axes shrinks inside of its maximum space.
And as we resize the figure, it will keep shrinking, but maintain the x and y scales by making the Axes use up less of the maximum space allocated to the axes (green box):
Two quick side-notes:
If you're using shared axes, and want to have adjustable="box", use adjustable="box-forced" instead.
If you'd like to control where the axes is positioned inside of the "green box" set the anchor of the axes. E.g. ax.set_anchor('NE') to have it remain "pinned" to the upper right corner of the "green box" as it adjusts its size to maintain the aspect ratio.
Fixed Aspect, adjustable="datalim"
The other main option for adjustable is "datalim".
In this case, matplotlib will keep the x and y scales in data space by changing one of the axes limits. The Axes will fill up the entire space allocated to it. However, if you manually set the x or y limits, they may be overridden to allow the axes to both fill up the full space allocated to it and keep the x/y scale ratio to the specified aspect.
In this case, the x limits were set to 0-10 and the y-limits to 0-20, with aspect=1, adjustable='datalim'. Note that the y-limit was not honored:
And as we resize the figure, the aspect ratio says the same, but the data limits change (in this case, the x-limit is not honored).
On a side note, the code to generate all of the above figures is at: https://gist.github.com/joferkington/4fe0d9164b5e4fe1e247
What does this have to do with imshow?
When imshow is called, it calls ax.set_aspect(1.0), by default. Because adjustable="box" by default, any plot with imshow will behave like the 3rd/4th images above.
For example:
However, if we specify imshow(..., aspect='auto'), the aspect ratio of the plot won't be overridden, and the image will "squish" to take up the full space allocated to the Axes:
On the other hand, if you wanted the pixels to remain "square" (note: they may not be square depending on what's specified by the extent kwarg), you can leave out the aspect='auto' and set the adjustable parameter of the axes to "datalim" instead.
E.g.
ax.imshow(data, cmap='gist_earth', interpolation='none')
ax.set(adjustable="datalim")
Axes Shape is Controlled by Figure Shape
The final part to remember is that the axes shape/size is defined as a percentage of the figure's shape/size.
Therefore, if you want to preserve the aspect ratio of the axes and have a fixed spacing between adjacent subplots, you'll need to define the shape of the figure to match. plt.figaspect is extremely handy for this. It simply generates a tuple of width, height based on a specified aspect ratio or a 2D array (it will take the aspect ratio from the array's shape, not contents).
For your example of a grid of subplots, each with a constant 2x1 aspect ratio, you might consider something like the following (note that I'm not using aspect="auto" here, as we want the pixels in the images to remain square):
import numpy as np
import matplotlib.pyplot as plt
nrows, ncols = 8, 12
dx, dy = 1, 2
figsize = plt.figaspect(float(dy * nrows) / float(dx * ncols))
fig, axes = plt.subplots(nrows, ncols, figsize=figsize)
for ax in axes.flat:
data = np.random.random((10*dy, 10*dx))
ax.imshow(data, interpolation='none', cmap='gray')
ax.set(xticks=[], yticks=[])
pad = 0.05 # Padding around the edge of the figure
xpad, ypad = dx * pad, dy * pad
fig.subplots_adjust(left=xpad, right=1-xpad, top=1-ypad, bottom=ypad)
plt.show()

Categories