I have some patches on which I apply different Affine2D transformations in matplotlib.
Is there a possibility to add them as a collections.PatchCollection? Somehow I am only able to draw them if I call ax.add_patch() separately for each of them.
from matplotlib import pyplot as plt, patches, collections, transforms
fig, ax = plt.subplots()
trafo1 = transforms.Affine2D().translate(+0.3, -0.3).rotate_deg_around(0, 0, 45) + ax.transData
trafo2 = transforms.Affine2D().translate(+0.3, -0.3).rotate_deg_around(0, 0, 65) + ax.transData
rec1 = patches.Rectangle(xy=(0.1, 0.1), width=0.2, height=0.3, transform=trafo1, color='blue')
rec2 = patches.Rectangle(xy=(0.2, 0.2), width=0.3, height=0.2, transform=trafo2, color='green')
ax.add_collection(collections.PatchCollection([rec1, rec2], color='red', zorder=10))
# ax.add_patch(rec1)
# ax.add_patch(rec2)
It looks like PatchCollection does not support individually transformed elements. From the Matplotlib documentation, we can read a Collection is a
class for the efficient drawing of large collections of objects that share most properties, e.g., a large number of line segments or polygons.
You can understand this with creating the collection without any individually transformed patches:
rec1 = patches.Rectangle(xy=(0.1, 0.1), width=0.2, height=0.3, color='blue')
rec2 = patches.Rectangle(xy=(0.2, 0.2), width=0.3, height=0.2, color='green')
col = collections.PatchCollection([rec1, rec2], color='red', zorder=10)
print(col.get_transform())
That prints IdentityTransform() for the last statement, and correctly displays the (non-transformed) patches. These patches can be transformed all-at-once from the PatchCollection, without individual specification.
On the contrary, when you apply the individual transform for each patch (like in your case), the .get_transform() method returns an empty list. This is probably due to the fact that PatchCollection classes are made to gather patches with a lot of common attributes in order to accelerate the drawing efficiency (as mentioned above), including the transform attribute.
Note: on this answer, you can find a workaround with a patch to path conversion, then to a PathCollection with an increase drawing efficiency compared to individual patch draw.
Related
I am looking through the matplotlib api and can't seem to find a way to change the space between legend markers. I came across a way to change the space between a marker and its respective handle with handletextpad, but I want to change the space between each marker.
Ideally, I'd like to have the markers touching eachother with the labels above (or on top of) the markers.
My legend:
What I am trying to model:
Is there a way to do this?
I am not sure if this matches your expectations. We have used the standard features to create a graph that is similar to your objectives. Since the code and data are unknown to me, I customized the example in the official reference to create it, using handletextpad and columnspacing, and since the numbers are in font units, I achieved this with a negative value.
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(19680801)
fig, ax = plt.subplots(figsize=(8,8))
for color in ['tab:blue', 'tab:orange', 'tab:green']:
n = 750
x, y = np.random.rand(2, n)
scale = 200.0 * np.random.rand(n)
ax.scatter(x, y, c=color, s=scale, label=color.split(':')[1][0],
alpha=0.5, edgecolors='none')
handlers, labels = ax.get_legend_handles_labels()
print(labels)
ax.legend(handletextpad=-1.2, columnspacing=-0.5, ncol=3,loc="upper left", bbox_to_anchor=(0.75, 1.08))
ax.grid(True)
plt.show()
I would like to link a graphic with a another one, both in the same figure, and "connect" the smaller one with the larger one, due to they share X axis, but not Y axis. The problem comes when I use that function, that I really don't know very well how it works.
The function ax2.set_axes_locator(ip) calls another one ip=InsetPosition(ax1,[0.2,0.7,0.5,0.25]), where ax1 represents the bigger graphic. The problem is that function automatically generates lines which links the bigger one with the smaller, but I can't redirect them and I want to, because both graphics don't share Y axis.
I hope someone could understand the problem, definitely my english is not the best.
ax2=plt.axes([0,0,1,1])
ip=InsetPosition(ax1,[0.2,0.7,0.5,0.25])
ax2.set_axes_locator(ip)
mark_inset(ax1,ax2,loc1=3,loc2=4,fc="none",ec='0.5')
You cannot use mark_inset because that will show the marker at the same data coordinates as the view limits of the inset axes.
Instead you can create a rectangle and two connectors that will just be positionned arbitrarily on the axes. (The following code will require matplotlib 3.1 or higher)
import matplotlib.pyplot as plt
from matplotlib.patches import ConnectionPatch
fig, ax = plt.subplots()
ax.plot([1,3,5], [2,4,1])
ax.set_ylim([0, 10])
ax.set_ylabel("Some units")
axins = ax.inset_axes([.2, .7, .4, .25])
axins.plot([100, 200], [5456, 4650])
axins.set_ylabel("Other units")
rect = [2.1, 2.6, 1, 2]
kw = dict(linestyle="--", facecolor="none", edgecolor="k", linewidth=0.8)
ax.add_patch(plt.Rectangle(rect[:2], *rect[2:], **kw))
cp1 = ConnectionPatch((rect[0], rect[1]+rect[3]), (0,0), coordsA="data", axesA=ax,
coordsB="axes fraction", axesB=axins, clip_on=False, **kw)
cp2 = ConnectionPatch((rect[0]+rect[2], rect[1]+rect[3]), (1,0), coordsA="data", axesA=ax,
coordsB="axes fraction", axesB=axins, clip_on=False, **kw)
ax.add_patch(cp1)
ax.add_patch(cp2)
plt.show()
It is very straight forward to plot a line between two points (x1, y1) and (x2, y2) in Matplotlib using Line2D:
Line2D(xdata=(x1, x2), ydata=(y1, y2))
But in my particular case I have to draw Line2D instances using Points coordinates on top of the regular plots that are all using Data coordinates. Is that possible?
As #tom mentioned, the key is the transform kwarg. If you want an artist's data to be interpreted as being in "pixel" coordinates, specify transform=IdentityTransform().
Using Transforms
Transforms are a key concept in matplotlib. A transform takes coordinates that the artist's data is in and converts them to display coordinates -- in other words, pixels on the screen.
If you haven't already seen it, give the matplotlib transforms tutorial a quick read. I'm going to assume a passing familiarity with the first few paragraphs of that tutorial, so if you're
For example, if we want to draw a line across the entire figure, we'd use something like:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
# The "clip_on" here specifies that we _don't_ want to clip the line
# to the extent of the axes
ax.plot([0, 1], [0, 1], lw=3, color='salmon', clip_on=False,
transform=fig.transFigure)
plt.show()
This line will always extend from the lower-left corner of the figure to the upper right corner, no matter how we interactively resize/zoom/pan the plot.
Drawing in pixels
The most common transforms you'll use are ax.transData, ax.transAxes, and fig.transFigure. However, to draw in points/pixels, you actually want no transform at all. In that case, you'll make a new transform instance that does nothing: the IdentityTransform. This specifies that the data for the artist is in "raw" pixels.
Any time you'd like to plot in "raw" pixels, specify transform=IdentityTransform() to the artist.
If you'd like to work in points, recall that there are 72 points to an inch, and that for matplotlib, fig.dpi controls the number of pixels in an "inch" (it's actually independent of the physical display). Therefore, we can convert points to pixels with a simple formula.
As an example, let's place a marker 30 points from the bottom-left edge of the figure:
import matplotlib.pyplot as plt
from matplotlib.transforms import IdentityTransform
fig, ax = plt.subplots()
points = 30
pixels = fig.dpi * points / 72.0
ax.plot([pixels], [pixels], marker='o', color='lightblue', ms=20,
transform=IdentityTransform(), clip_on=False)
plt.show()
Composing Transforms
One of the more useful things about matplotlib's transforms is that they can be added to create a new transform. This makes it easy to create shifts.
For example, let's plot a line, then add another line shifted by 15 pixels in the x-direction:
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
fig, ax = plt.subplots()
ax.plot(range(10), color='lightblue', lw=4)
ax.plot(range(10), color='gray', lw=4,
transform=ax.transData + Affine2D().translate(15, 0))
plt.show()
A key thing to keep in mind is that the order of the additions matters. If we did Affine2D().translate(15, 0) + ax.transData instead, we'd shift things by 15 data units instead of 15 pixels. The added transforms are "chained" (composed would be a more accurate term) in order.
This also makes it easy to define things like "20 pixels from the right hand side of the figure". For example:
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
fig, ax = plt.subplots()
ax.plot([1, 1], [0, 1], lw=3, clip_on=False, color='salmon',
transform=fig.transFigure + Affine2D().translate(-20, 0))
plt.show()
You can use the transform keyword to change between data coordinate (the default) and axes coordinates. For example:
import matplotlib.pyplot as plt
import matplotlib.lines as lines
plt.plot(range(10),range(10),'ro-')
myline = lines.Line2D((0,0.5,1),(0.5,0.5,0),color='b') # data coords
plt.gca().add_artist(myline)
mynewline = lines.Line2D((0,0.5,1),(0.5,0.5,0),color='g',transform=plt.gca().transAxes) # Axes coordinates
plt.gca().add_artist(mynewline)
plt.show()
I draw a graph but unfortunately my legend falls out of the figure. How can I correct it?
I put a dummy code to illustrate it:
from matplotlib import pyplot as plt
from bisect import bisect_left,bisect_right
import numpy as np
global ranOnce
ranOnce=False
def threeLines(x):
"""Draws a function which is a combination of two lines intersecting in
a point one with a large slope and one with a small slope.
"""
start=0
mid=5
end=20
global ranOnce,slopes,intervals,intercepts;
if(not ranOnce):
slopes=np.array([5,0.2,1]);
intervals=[start,mid,end]
intercepts=[start,(mid-start)*slopes[0]+start,(end-mid)*slopes[1]+(mid-start)*slopes[0]+start]
ranOnce=True;
place=bisect_left(intervals,x)
if place==0:
y=(x-intervals[place])*slopes[place]+intercepts[place];
else:
y=(x-intervals[place-1])*slopes[place-1]+intercepts[place-1];
return y;
def threeLinesDrawer(minimum,maximum):
t=np.arange(minimum,maximum,1)
fig=plt.subplot(111)
markerSize=400;
fig.scatter([minimum,maximum],[threeLines(minimum),threeLines(maximum)],marker='+',s=markerSize)
y=np.zeros(len(t));
for i in range(len(t)):
y[i]=int(threeLines(t[i]))
fig.scatter(t,y)
fig.grid(True)
fig.set_xlabel('Y')
fig.set_ylabel('X')
legend1 = plt.Circle((0, 0), 1, fc="r")
legend2 = plt.Circle((0, 0), 1, fc="b")
legend3 = plt.Circle((0, 0), 1, fc="g")
fig.legend([legend1,legend2,legend3], ["p(y|x) likelihood","Max{p(y|x)} for a specific x","Y distribution"],
bbox_to_anchor=(0., 1.02, 1., .102), loc=3,ncol=2, mode="expand", borderaxespad=0.)
threeLinesDrawer(0,20)
plt.show()
You can adjust the space that a set of axes takes within a figure by modifying the subplot parameters. For example, add the following line just before plt.show():
plt.subplots_adjust(top=.9, bottom=.1, hspace=.1, left=.1, right=.9, wspace=.1)
You should tweak the above values as you see fit in the range of [0, 1]. Feel free to get rid of the parameters you're not interested in tweaking (e.g. since you only have one axis in your figure, you won't care about the hspace and wspace parameters, which modify the spacing between subplots). These settings can also be modified through the plt.show() GUI, but you'd have to do it every time you run the script. A set of good settings for your case is the following:
plt.subplots_adjust(top=.83, bottom=.08, left=.08, right=.98)
For doing this adjustment automatically, you can try using tight_layout(). Add the following line just before plt.show():
plt.tight_layout()
This won't necessarily give the intended results in every case, though.
Is there a way of telling pyplot.text() a location like you can with pyplot.legend()?
Something like the legend argument would be excellent:
plt.legend(loc="upper left")
I am trying to label subplots with different axes using letters (e.g. "A","B"). I figure there's got to be a better way than manually estimating the position.
Thanks
Just use annotate and specify axis coordinates. For example, "upper left" would be:
plt.annotate('Something', xy=(0.05, 0.95), xycoords='axes fraction')
You could also get fancier and specify a constant offset in points:
plt.annotate('Something', xy=(0, 1), xytext=(12, -12), va='top'
xycoords='axes fraction', textcoords='offset points')
For more explanation see the examples here and the more detailed examples here.
I'm not sure if this was available when I originally posted the question but using the loc parameter can now actually be used. Below is an example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnchoredText
# make some data
x = np.arange(10)
y = x
# set up figure and axes
f, ax = plt.subplots(1,1)
# loc works the same as it does with figures (though best doesn't work)
# pad=5 will increase the size of padding between the border and text
# borderpad=5 will increase the distance between the border and the axes
# frameon=False will remove the box around the text
anchored_text = AnchoredText("Test", loc=2)
ax.plot(x,y)
ax.add_artist(anchored_text)
plt.show()
The question is quite old but as there is no general solution to the problem till now (2019) according to Add loc=best kwarg to pyplot.text(), I'm using legend() and the following workaround to obtain auto-placement for simple text boxes:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpl_patches
x = np.linspace(-1,1)
fig, ax = plt.subplots()
ax.plot(x, x*x)
# create a list with two empty handles (or more if needed)
handles = [mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white",
lw=0, alpha=0)] * 2
# create the corresponding number of labels (= the text you want to display)
labels = []
labels.append("pi = {0:.4g}".format(np.pi))
labels.append("root(2) = {0:.4g}".format(np.sqrt(2)))
# create the legend, supressing the blank space of the empty line symbol and the
# padding between symbol and label by setting handlelenght and handletextpad
ax.legend(handles, labels, loc='best', fontsize='small',
fancybox=True, framealpha=0.7,
handlelength=0, handletextpad=0)
plt.show()
The general idea is to create a legend with a blank line symbol and to remove the resulting empty space afterwards. How to adjust the size of matplotlib legend box? helped me with the legend formatting.