I am plotting a double plot with two y-axes. The second axis ax2 is created by twinx. The problem is that the coloring of the second y-axis via yticks is not working anymore. Instead I have to set_color the labels individually. Here is the relevant code:
fig = plt.figure()
fill_between(data[:,0], 0, (data[:,2]), color='yellow')
yticks(arange(0.2,1.2,0.2), ['.2', '.4', '.6', '.8', ' 1'], color='yellow')
ax2 = twinx()
ax2.plot(data[:,0], (data[:,1]), 'green')
yticks(arange(0.1,0.6,0.1), ['.1 ', '.2', '.3', '.4', '.5'], color='green')
# color='green' has no effect here ?!
# instead this is needed:
for t in ax2.yaxis.get_ticklabels(): t.set_color('green')
show()
Resulting in:
This issue only occurs if I set the tick strings.
yticks(arange(0.1,0.6,0.1), ['.1 ', '.2', '.3', '.4', '.5'], color='green')
Omit it, like here
yticks(arange(0.1,0.6,0.1), color='green')
and the coloring works fine.
Is that a bug (could not find any reports to this), a feature (?!) or
am I missing something here? I am using python 2.6.5 with matplotlib 0.99.1.1 on ubuntu.
For whatever it's worth, you code works fine on my system even without the for loop to set the label colors. Just as a reference, here's a stand-alone example trying to follow essentially exactly what you posted:
import matplotlib.pyplot as plt
import numpy as np
# Generate some data
num = 200
x = np.linspace(501, 1200, num)
yellow_data, green_data = np.random.random((2,num))
green_data -= np.linspace(0, 3, yellow_data.size)
# Plot the yellow data
plt.fill_between(x, yellow_data, 0, color='yellow')
plt.yticks([0.0, 0.5, 1.0], color='yellow')
# Plot the green data
ax2 = plt.twinx()
ax2.plot(x, green_data, 'g-')
plt.yticks([-4, -3, -2, -1, 0, 1], color='green')
plt.show()
My guess is that your problem is mostly coming from mixing up references to different objects. I'm guessing that your code is a bit more complex, and that when you call plt.yticks, ax2 is not the current axis. You can test that idea by explicitly calling sca(ax2) (set the current axis to ax2) before calling yticks and see if that changes things.
Generally speaking, it's best to stick to either entirely the matlab-ish state machine interface or the OO interface, and don't mix them too much. (Personally, I prefer just sticking to the OO interface. Use pyplot to set up figure objects and for show, and use the axes methods otherwise. To each his own, though.)
At any rate, with matplotlib >= 1.0, the tick_params function makes this a bit more convenient. (I'm also using plt.subplots here, which is only in >= 1.0, as well.)
import matplotlib.pyplot as plt
import numpy as np
# Generate some data
yellow_data, green_data = np.random.random((2,2000))
yellow_data += np.linspace(0, 3, yellow_data.size)
green_data -= np.linspace(0, 3, yellow_data.size)
# Plot the data
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
ax1.plot(yellow_data, 'y-')
ax2.plot(green_data, 'g-')
# Change the axis colors...
ax1.tick_params(axis='y', labelcolor='yellow')
ax2.tick_params(axis='y', labelcolor='green')
plt.show()
The equivalent code for older versions of matplotlib would look more like this:
import matplotlib.pyplot as plt
import numpy as np
# Generate some data
yellow_data, green_data = np.random.random((2,2000))
yellow_data += np.linspace(0, 3, yellow_data.size)
green_data -= np.linspace(0, 3, yellow_data.size)
# Plot the data
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
ax2 = ax1.twinx()
ax1.plot(yellow_data, 'y-')
ax2.plot(green_data, 'g-')
# Change the axis colors...
for ax, color in zip([ax1, ax2], ['yellow', 'green']):
for label in ax.yaxis.get_ticklabels():
label.set_color(color)
plt.show()
Related
The problem is a bit hard to describe, but very easy to show. I create a grid with subplots on it, where the right column is filled by a tall subplot (approximately following this) which I want to use for the colourbar. Creating a new axis of a given size and using it for a colourbar is done in many code samples (see for example here), but it's not working for me.
Here's an example with a plot layout the same as my real plot that reproduces the problem:
import matplotlib.pyplot as plt
import matplotlib.colors as clt
import numpy as np
fig, axes = plt.subplots(3, 2, figsize=(15,8), tight_layout=True,
gridspec_kw={'width_ratios': [1, 0.02],
'height_ratios': [2, 1, 1]})
x, y = np.random.rand(500000), np.random.rand(500000)
counts, xedges, yedges, im = axes[0, 0].hist2d(x, y, bins=(149, 336), norm=clt.LogNorm(), cmap='inferno_r')
axes[1, 0].plot(np.random.rand(2184))
axes[2, 0].plot(np.random.rand(2184))
gs = axes[0, 1].get_gridspec()
for ax in axes[:, 1]:
ax.remove()
axbig = fig.add_subplot(gs[0:, -1])
bar = fig.colorbar(im, ax=axbig)
axes[0, 0].set_ylabel("2D histogram")
axes[1, 0].set_ylabel("unrelated data")
axes[2, 0].set_ylabel("other unrelated")
bar.set_label("colourbar")
(note that I use add_subplot(gs[0:, -1]) to make the tall subplot, but something like add_axes([0.8, 0.1, 0.03, 0.8]) has the same effect)
And the output:
Notice how the colourbar is added as a tiny little new axis, onto the existing axis which I created for it. I would expect it to fill in the existing axis, as in this or this example. What's going wrong? I'm running matplotlib 3.3.1 from inside spyder 5.0.0 with python 3.8.
Your original problem, that you didn't want one of three axes squished is explicitly taken care of with constrained_layout. https://matplotlib.org/stable/tutorials/intermediate/constrainedlayout_guide.html#suptitle
I think people are for some reason scared off by the warning on the CL guide, but that is really for folks running production code that must be pixel identical each run. For most users CL is a better option than tight_layout.
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
fig, axs = plt.subplots(3, 1, figsize=(7,4), constrained_layout=True,
gridspec_kw={'height_ratios': [2, 1, 1]})
x, y = np.random.rand(500000), np.random.rand(500000)
res = axs[0].hist2d(x, y, bins=(149, 336),
norm=mcolors.LogNorm(), cmap='inferno_r')
axs[1].plot(np.random.rand(2184))
axs[2].plot(np.random.rand(2184))
fig.colorbar(res[3], ax=axs[0])
plt.show()
I decided to play around with this example code a bit. I was able to figure out how to draw a straight line between the two subplots, even when the line is outside the bounds of one of the subplots.
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
axs = [ax1, ax2]
# Fixing random state for reproducibility
np.random.seed(19680801)
# generate some random test data
all_data = [np.random.normal(0, std, 100) for std in range(6, 10)]
# plot violin plot
axs[0].violinplot(all_data,
showmeans=False,
showmedians=True)
axs[0].set_title('Violin plot')
# plot box plot
axs[1].boxplot(all_data)
axs[1].set_title('Box plot')
# adding horizontal grid lines
for ax in axs:
ax.yaxis.grid(True)
ax.set_xticks([y + 1 for y in range(len(all_data))])
ax.set_xlabel('Four separate samples')
ax.set_ylabel('Observed values')
for tick in ax.xaxis.get_major_ticks():
tick.label.set_fontsize(20)
plt.setp(axs[0], xticklabels=['x1', 'x2', 'x3', 'x4'])
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform([5,10]))
coord2 = transFigure.transform(ax2.transData.transform([2,-10]))
line = mpl.lines.Line2D((coord1[0],coord2[0]),(coord1[1],coord2[1]),
c='k', lw=5, transform=fig.transFigure)
fig.lines.append(line)
Yes that added line is ugly but I just wanted to get it functional.
However, what I'd really like to do is make an arrow between the subplots, and I can't figure out how without jury-rigging my own arrow tails. Is there a way to do this that uses the matplotlib.pyplot.arrow class?
I also wanted to draw an arrow between two subplots but I didn't even know where to start! However, the line between subplots example in the original question gave me enough of a clue to get started...
First, I reduced the code in the original question to a minimal working example:
from matplotlib import lines, pyplot as plt
fig = plt.figure()
# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])
# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])
# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform(xyA))
coord2 = transFigure.transform(ax2.transData.transform(xyB))
line = lines.Line2D(
(coord1[0], coord2[0]), # xdata
(coord1[1], coord2[1]), # ydata
transform=fig.transFigure,
color="black",
)
fig.lines.append(line)
# Show figure
plt.show()
This produces the following output:
Then, using this blog post, I thought the answer was to create a matplotlib.patches.FancyArrowPatch and append it to fig.patches (instead of creating a matplotlib.lines.Line2D and appending it to fig.lines). After consulting the matplotlib.patches.FancyArrowPatch documentation, plus some trial and error, I came up with something that works in matplotlib 3.1.2:
from matplotlib import patches, pyplot as plt
fig = plt.figure()
# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])
# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])
# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform(xyA))
coord2 = transFigure.transform(ax2.transData.transform(xyB))
arrow = patches.FancyArrowPatch(
coord1, # posA
coord2, # posB
shrinkA=0, # so tail is exactly on posA (default shrink is 2)
shrinkB=0, # so head is exactly on posB (default shrink is 2)
transform=fig.transFigure,
color="black",
arrowstyle="-|>", # "normal" arrow
mutation_scale=30, # controls arrow head size
linewidth=3,
)
fig.patches.append(arrow)
# Show figure
plt.show()
However, as per the comments below, this does not work in matplotlib 3.4.2, where you get this:
Notice that the ends of the arrow do not line up with the target points (orange circles), which they should do.
This matplotlib version change also causes the original line example to fail in the same way.
However, there is a better patch! Use ConnectionPatch (docs), which is a subclass of FancyArrowPatch, instead of using FancyArrowPatch directly as the ConnectionPatch is designed specifically for this use case and deals with the transform more correctly, as shown in this matplotlib documentation example:
fig = plt.figure()
# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])
# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])
# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
# ConnectionPatch handles the transform internally so no need to get fig.transFigure
arrow = patches.ConnectionPatch(
xyA,
xyB,
coordsA=ax1.transData,
coordsB=ax2.transData,
# Default shrink parameter is 0 so can be omitted
color="black",
arrowstyle="-|>", # "normal" arrow
mutation_scale=30, # controls arrow head size
linewidth=3,
)
fig.patches.append(arrow)
# Show figure
plt.show()
This produces the correct output in both matplotlib 3.1.2 and matplotlib 3.4.2, which looks like this:
To draw a correctly positioned line connecting across two subplots in matplotlib 3.4.2, use a ConnectionPatch as above but with arrowstyle="-" (i.e. no arrow heads, so just a line).
NB: You cannot use:
plt.arrow as it is automatically added to the current axes so only appears in one subplot
matplotlib.patches.Arrow as the axes-figure transform skews the arrow-head
matplotlib.patches.FancyArrow as this also results in a skewed arrow-head
In pyplot, you can change the order of different graphs using the zorder option or by changing the order of the plot() commands. However, when you add an alternative axis via ax2 = twinx(), the new axis will always overlay the old axis (as described in the documentation).
Is it possible to change the order of the axis to move the alternative (twinned) y-axis to background?
In the example below, I would like to display the blue line on top of the histogram:
import numpy as np
import matplotlib.pyplot as plt
import random
# Data
x = np.arange(-3.0, 3.01, 0.1)
y = np.power(x,2)
y2 = 1/np.sqrt(2*np.pi) * np.exp(-y/2)
data = [random.gauss(0.0, 1.0) for i in range(1000)]
# Plot figure
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax2.hist(data, bins=40, normed=True, color='g',zorder=0)
ax2.plot(x, y2, color='r', linewidth=2, zorder=2)
ax1.plot(x, y, color='b', linewidth=2, zorder=5)
ax1.set_ylabel("Parabola")
ax2.set_ylabel("Normal distribution")
ax1.yaxis.label.set_color('b')
ax2.yaxis.label.set_color('r')
plt.show()
Edit: For some reason, I am unable to upload the image generated by this code. I will try again later.
You can set the zorder of an axes, ax.set_zorder(). One would then need to remove the background of that axes, such that the axes below is still visible.
ax2 = ax1.twinx()
ax1.set_zorder(10)
ax1.patch.set_visible(False)
In pyplot, you can change the order of different graphs using the zorder option or by changing the order of the plot() commands. However, when you add an alternative axis via ax2 = twinx(), the new axis will always overlay the old axis (as described in the documentation).
Is it possible to change the order of the axis to move the alternative (twinned) y-axis to background?
In the example below, I would like to display the blue line on top of the histogram:
import numpy as np
import matplotlib.pyplot as plt
import random
# Data
x = np.arange(-3.0, 3.01, 0.1)
y = np.power(x,2)
y2 = 1/np.sqrt(2*np.pi) * np.exp(-y/2)
data = [random.gauss(0.0, 1.0) for i in range(1000)]
# Plot figure
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax2.hist(data, bins=40, normed=True, color='g',zorder=0)
ax2.plot(x, y2, color='r', linewidth=2, zorder=2)
ax1.plot(x, y, color='b', linewidth=2, zorder=5)
ax1.set_ylabel("Parabola")
ax2.set_ylabel("Normal distribution")
ax1.yaxis.label.set_color('b')
ax2.yaxis.label.set_color('r')
plt.show()
Edit: For some reason, I am unable to upload the image generated by this code. I will try again later.
You can set the zorder of an axes, ax.set_zorder(). One would then need to remove the background of that axes, such that the axes below is still visible.
ax2 = ax1.twinx()
ax1.set_zorder(10)
ax1.patch.set_visible(False)
I'm attempting to create a plot with a legend to the side of it using matplotlib. I can see that the plot is being created, but the image bounds do not allow the entire legend to be displayed.
lines = []
ax = plt.subplot(111)
for filename in args:
lines.append(plt.plot(y_axis, x_axis, colors[colorcycle], linestyle='steps-pre', label=filename))
ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
This produces:
Eventhough that it is late, I want to refer to a nice recently introduced alternative:
New matplotlib feature: The tight bounding box
If you are interested in the output file of plt.savefig: in this case the flag bbox_inches='tight' is your friend!
import matplotlib.pyplot as plt
fig = plt.figure(1)
plt.plot([1, 2, 3], [1, 0, 1], label='A')
plt.plot([1, 2, 3], [1, 2, 2], label='B')
plt.legend(loc='center left', bbox_to_anchor=(1, 0))
fig.savefig('samplefigure', bbox_inches='tight')
I want to refer also to a more detailed answer: Moving matplotlib legend outside of the axis makes it cutoff by the figure box
Advantages
There is no need to adjust the actual data/picture.
It is compatible with plt.subplots as-well where as the others are not!
It applies at least to the mostly used output files, e.g. png, pdf.
As pointed by Adam, you need to make space on the side of your graph.
If you want to fine tune the needed space, you may want to look at the add_axes method of matplotlib.pyplot.artist.
Below is a rapid example:
import matplotlib.pyplot as plt
import numpy as np
# some data
x = np.arange(0, 10, 0.1)
y1 = np.sin(x)
y2 = np.cos(x)
# plot of the data
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.6, 0.75])
ax.plot(x, y1,'-k', lw=2, label='black sin(x)')
ax.plot(x, y2,'-r', lw=2, label='red cos(x)')
ax.set_xlabel('x', size=22)
ax.set_ylabel('y', size=22)
ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.show()
and the resulting image:
Just use plt.tight_layout()
import matplotlib.pyplot as plt
fig = plt.figure(1)
plt.plot([1, 2, 3], [1, 0, 1], label='A')
plt.plot([1, 2, 3], [1, 2, 2], label='B')
plt.legend(loc='center left', bbox_to_anchor=(1, 0))
plt.tight_layout()
This is probably introduced in the newer matplotlib version and neatly does the job.
Here is another way of making space (shrinking an axis):
# get the current axis
ax = plt.gca()
# Shink current axis by 20%
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
where 0.8 scales the width of the axis by 20%. On my win7 64 machine, using a factor greater than 1 will make room for the legend if it's outside the plot.
This code was referenced from: How to put the legend out of the plot
Edit: #gcalmettes posted a better answer.
His solution should probably be used instead of the method shown below.
Nonetheless I'll leave this since it sometimes helps to see different ways of doing things.
As shown in the legend plotting guide, you can make room for another subplot and place the legend there.
import matplotlib.pyplot as plt
ax = plt.subplot(121) # <- with 2 we tell mpl to make room for an extra subplot
ax.plot([1,2,3], color='red', label='thin red line')
ax.plot([1.5,2.5,3.5], color='blue', label='thin blue line')
ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.show()
Produces:
Store your legend call instance to a variable. e.g:
rr = sine_curve_plot.legend(loc=(0.0,1.1))
Then, include the bbox_extra_artists, bbox_inches keyword argument to plt.savefig. i.e:
plt.savefig('output.pdf', bbox_inches='tight', bbox_extra_artists=(rr))
bbox_extra_artists accepts an iterable, so you can include as many legend instances into it. The bbox_extra_artists automatically tells plt to cover every extra info passed into bbox_extra_artists.
DISCLAIMER: The loc variable simply defines the position of the legend, you can tweak the values for better flexibility in positioning. Of course, strings like upper left, upper right, etc. are also valid.