I am trying to build a graph using matplotlib, and I am having trouble placing descriptive text on the graph itself.
My y values range from .9 to 1.65, and x ranges from the dates 2001 to 2021 and are sourced from a datetime series.
Here are the basics of what I am working with:
fig, ax = plt.subplots(figsize=(10,7))
I know that I have to use ax.text() to place any text, but whenever I try to enter basically any values for the x and y coordinates of the text, the entire graph disappears when I re-run the cell. I have plotted the following line, but if I use the same coordinates in ax.text(), I get the output I just described. Why might this be happening?
plt.axhline(y=1.19, xmin=.032, xmax=.96)
By default, the y argument in the axhline method is in data coordinates, while the xmin and xmax arguments are in axis coordinates, with 0 corresponding to the far left of the plot, and 1 corresponding to the far right of the plot. See the axhline documentation for more information.
On the other hand, both the x and y arguments used in the text method are in data coordinates, so you position text relative to the data. However, you can change this to axis coordinates using the transform parameter. By setting this to ax.transAxes, you actually indicate that the x and y arguments should be interpreted as axis coordinates, again with 0 being the far left (or bottom) of the plot, and 1 being the far right (or top) of the plot. In this case, you would use ax.text as follows:
ax.text(x, y, 'text', transform=ax.transAxes)
Again, see the text documentation for more information.
However, it sounds like you might want to combine data and axis coordinates to place your text, because you want to reuse the arguments from axhline for your text. In this case, you need to create a transform object that interprets the x coordinate as axis coordinate, and the y coordinate as data coordinate. This is also possible by creating a blended transformation. For example:
import matplotlib.transforms as transforms
# create your ax object here
trans = transforms.blended_transform_factory(x_transform=ax.transAxes, y_transform=ax.transData)
ax.text(x, y, 'text', transform=trans)
See the Blended transformations section of the transformations tutorial for more information.
In short, you can refer to the following figure to compare the results of these various transformations:
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
fig, ax = plt.subplots()
ax.set_xlim(0, 2)
ax.set_ylim(0, 2)
# note that the line is plotted at y=1.5, but between x=1.6 and x=1.8
# because xmin/xmax are in axis coordinates
ax.axhline(1.5, xmin=.8, xmax=.9)
# x and y are in data coordinates
ax.text(0.5, 0.5, 'A')
# here, x and y are in axis coordinates
ax.text(0.5, 0.5, 'B', transform=ax.transAxes)
trans = transforms.blended_transform_factory(x_transform=ax.transAxes, y_transform=ax.transData)
# here, x is in axis coordinates, but y is in data coordinates
ax.text(0.5, 0.5, 'C', transform=trans)
Related
It appears to be impossible to change the y and x axis view limits during an ArtistAnimation, and have the frames replayed with different axis limits.
The limits seem to fixed to those set last before the animation function is called.
In the code below, I have two plotting stages. The input data in the second plot is a much smaller subset of the data in the 1st frame. The data in the 1st stage has a much wider range.
So, I need to "zoom in" when displaying the second plot (otherwise the plot would be very tiny if the axis limits remain the same).
The two plots are overlaid on two different images (that are of the same size, but different content).
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.image as mpimg
import random
# sample 640x480 image. Actual frame loops through
# many different images, but of same size
image = mpimg.imread('image_demo.png')
fig = plt.figure()
plt.axis('off')
ax = fig.gca()
artists = []
def plot_stage_1():
# both x, y axis limits automatically set to 0 - 100
# when we call ax.imshow with this extent
im_extent = (0, 100, 0, 100) # (xmin, xmax, ymin, ymax)
im = ax.imshow(image, extent=im_extent, animated=True)
# y axis is a list of 100 random numbers between 0 and 100
p, = ax.plot(range(100), random.choices(range(100), k=100))
# Text label at 90, 90
t = ax.text(im_extent[1]*0.9, im_extent[3]*0.9, "Frame 1")
artists.append([im, t, p])
def plot_stage_2():
# axes remain at the the 0 - 100 limit from the previous
# imshow extent so both the background image and plot are tiny
im_extent = (0, 10, 0, 10)
# so let's update the x, y axis limits
ax.set_xlim(im_extent[0], im_extent[1])
ax.set_ylim(im_extent[0], im_extent[3])
im = ax.imshow(image, extent=im_extent, animated=True)
p, = ax.plot(range(10), random.choices(range(10), k=10))
# Text label at 9, 9
t = ax.text(im_extent[1]*0.9, im_extent[3]*0.9, "Frame 2")
artists.append([im, t, p])
plot_stage_1()
plot_stage_2()
# clear white space around plot
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=None, hspace=None)
# set figure size
fig.set_size_inches(6.67, 5.0, True)
anim = animation.ArtistAnimation(fig, artists, interval=2000, repeat=False, blit=False)
plt.show()
If I call just one of the two functions above, the plot is fine. However, if I call both, the axis limits in both frames will be 0 - 10, 0 - 10. So frame 1 will be super zoomed in.
Also calling ax.set_xlim(0, 100), ax.set_ylim(0, 100) in plot_stage_1() doesn't help. The last set_xlim(), set_ylim() calls fix the axis limits throughout all frames in the animation.
I could keep the axis bounds fixed and apply a scaling function to the input data.
However, I'm curious to know whether I can simply change the axis limits -- my code will be better this way, because the actual code is complicated with multiple stages, zooming plots across many different ranges.
Or perhaps I have to rejig my code to use FuncAnimation, instead of ArtistAnimation?
FuncAnimation appears to result in the expected behavior. So I'm changing my code to use that instead of ArtistAnimation.
Still curious to know though, whether this can at all be done using ArtistAnimation.
I am new to this module. I have time series data for movement of particle against time. The movement has its X and Y component against the the time T. I want to plot these 3 parameters in the graph. The sample data looks like this. The first coloumn represent time, 2nd- Xcordinate , 3rd Y-cordinate.
1.5193 618.3349 487.5595
1.5193 619.3349 487.5595
2.5193 619.8688 489.5869
2.5193 620.8688 489.5869
3.5193 622.9027 493.3156
3.5193 623.9027 493.3156
If you want to add a 3rd info to a 2D curve, one possibility is to use a color mapping instituting a relationship between the value of the 3rd coordinate and a set of colors.
In Matplotlib we have not a direct way of plotting a curve with changing color, but we can fake one using matplotlib.collections.LineCollection.
In the following I've used some arbitrary curve but I have no doubt that you could adjust my code to your particular use case if my code suits your needs.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
# e.g., a Lissajous curve
t = np.linspace(0, 2*np.pi, 6280)
x, y = np.sin(4*t), np.sin(5*t)
# to use LineCollection we need an array of segments
# the canonical answer (to upvote...) is https://stackoverflow.com/a/58880037/2749397
points = np.array([x, y]).T.reshape(-1,1,2)
segments = np.concatenate([points[:-1],points[1:]], axis=1)
# instantiate the line collection with appropriate parameters,
# the associated array controls the color mapping, we set it to time
lc = LineCollection(segments, cmap='nipy_spectral', linewidth=6, alpha=0.85)
lc.set_array(t)
# usual stuff, just note ax.autoscale, not needed here because we
# replot the same data but tipically needed with ax.add_collection
fig, ax = plt.subplots()
plt.xlabel('x/mm') ; plt.ylabel('y/mm')
ax.add_collection(lc)
ax.autoscale()
cb = plt.colorbar(lc)
cb.set_label('t/s')
# we plot a thin line over the colormapped line collection, especially
# useful when our colormap contains white...
plt.plot(x, y, color='black', linewidth=0.5, zorder=3)
plt.show()
I want to add a scale indicator to a plot like the one labelled '10kpc' in the (otherwise) empty plot below. So basically, the axis use one unit of measure and I want to indicate a length in the plot in a different unit. It has to have the same style as below, i.e. a |----| bar with text above.
Is there a convenient way in matplotlib to do that or do I have to draw three lines (two small vertical, one horizontal) and add the text? An ideal solution would not even require me to set coordinates in the data dimensions, i.e. I just say something along the line of horizontalalignment='left', verticalalignment='bottom', transform=ax.transAxes and specify only the width in data coordinates.
I fought with annotate() and arrow() and their documentations for quiet a bit until I concluded, they were not exactly useful, but I might be wrong.
Edit:
The code below is the closest, I have come so far. I still don't like having to specify the x-coordinates in the data coordinate system. The only thing I want to specify in data is the width of the bar. The rest should be placed in the plot system and ideally the bar should be placed relative to the text (a few pixels above).
import matplotlib.pyplot as plt
import matplotlib.transforms as tfrms
plt.imshow(somedata)
plt.colorbar()
ax = plt.gca()
trans = tfrms.blended_transform_factory( ax.transData, ax.transAxes )
plt.errorbar( 5, 0.06, xerr=10*arcsecperkpc/2, color='k', capsize=5, transform=trans )
plt.text( 5, 0.05, '10kpc', horizontalalignment='center', verticalalignment='top', transform=trans )
Here is a code that adds a horizontal scale bar (or scale indicator or scalebar) to a plot. The bar's width is given in data units, while the height of the edges is in fraction of axes units.
The solution is based on an AnchoredOffsetbox, which contains a VPacker. The VPacker has a label in its lower row, and an AuxTransformBox in its upper row.
The key here is that the AnchoredOffsetbox is positioned relative to the axes, using the loc argument similar to the legend positioning (e.g. loc=4 denotes the lower right corner). However, the AuxTransformBox contains a set of elements, which are positioned inside the box using a transformation. As transformation we can choose a blended transform which transforms x coordinates according to the data transform of the axes and y coordinates according to the axes transform. A tranformation which does this is actually the xaxis_transform of the axes itself. Supplying this transform to the AuxTransformBox allows us to specify the artists within (which are Line2Ds in this case) in a useful way, e.g. the line of the bar will be Line2D([0,size],[0,0]).
All of this can be packed into a class, subclassing the AnchoredOffsetbox, such that it is easy to be used in an existing code.
import matplotlib.pyplot as plt
import matplotlib.offsetbox
from matplotlib.lines import Line2D
import numpy as np; np.random.seed(42)
x = np.linspace(-6,6, num=100)
y = np.linspace(-10,10, num=100)
X,Y = np.meshgrid(x,y)
Z = np.sin(X)/X+np.sin(Y)/Y
fig, ax = plt.subplots()
ax.contourf(X,Y,Z, alpha=.1)
ax.contour(X,Y,Z, alpha=.4)
class AnchoredHScaleBar(matplotlib.offsetbox.AnchoredOffsetbox):
""" size: length of bar in data units
extent : height of bar ends in axes units """
def __init__(self, size=1, extent = 0.03, label="", loc=2, ax=None,
pad=0.4, borderpad=0.5, ppad = 0, sep=2, prop=None,
frameon=True, linekw={}, **kwargs):
if not ax:
ax = plt.gca()
trans = ax.get_xaxis_transform()
size_bar = matplotlib.offsetbox.AuxTransformBox(trans)
line = Line2D([0,size],[0,0], **linekw)
vline1 = Line2D([0,0],[-extent/2.,extent/2.], **linekw)
vline2 = Line2D([size,size],[-extent/2.,extent/2.], **linekw)
size_bar.add_artist(line)
size_bar.add_artist(vline1)
size_bar.add_artist(vline2)
txt = matplotlib.offsetbox.TextArea(label, minimumdescent=False)
self.vpac = matplotlib.offsetbox.VPacker(children=[size_bar,txt],
align="center", pad=ppad, sep=sep)
matplotlib.offsetbox.AnchoredOffsetbox.__init__(self, loc, pad=pad,
borderpad=borderpad, child=self.vpac, prop=prop, frameon=frameon,
**kwargs)
ob = AnchoredHScaleBar(size=3, label="3 units", loc=4, frameon=True,
pad=0.6,sep=4, linekw=dict(color="crimson"),)
ax.add_artist(ob)
plt.show()
In order to achieve a result as desired in the question, you can set the frame off and adjust the linewidth. Of course the transformation from the units you want to show (kpc) into data units (km?) needs to be done by yourself.
ikpc = lambda x: x*3.085e16 #x in kpc, return in km
ob = AnchoredHScaleBar(size=ikpc(10), label="10kpc", loc=4, frameon=False,
pad=0.6,sep=4, linekw=dict(color="k", linewidth=0.8))
I am trying to create a plot with a colorbar, with custom tick labels. When I try to create a label for a colorbar, it reverts to the original tick labels.
z = np.add(relzvals, shift_val)
fig=plt.figure()
plt.contourf(x,y,z,25)
ax = plt.colorbar()
ax.set_label('cbar_label',rotation=270)
ax.set_ticklabels(labels,update_ticks=True)
How can I get both customizations?
UPDATE: For context, the main program require translated values, i.e. relzvals = z + shift_val, and the matrix has to be translated back to z for plotting.
I checked the values of z before coming inside plt.contourf() but it contains the original values.
I have Python 2.7.11 and Matplotlib 1.3.1.
I want to plot the location of some disks generated randomly on a scatter plot, and see whether the disks are 'connected' to each other. For this, I need to set the radius of each disk fixed/linked to the axis scale.
The 's' parameter in the plt.scatter function uses points, so the size is not fixed relative to the axis. If I dynamically zoom into the plot, the scatter marker size remains constant on the plot and does not scale up with the axis.
How do I set the radius so they have a definite value (relative to the axis)?
Instead of using plt.scatter, I suggest using patches.Circle to draw the plot (similar to this answer). These patches remain fixed in size so that you can dynamically zoom in to check for 'connections':
import matplotlib.pyplot as plt
from matplotlib.patches import Circle # for simplified usage, import this patch
# set up some x,y coordinates and radii
x = [1.0, 2.0, 4.0]
y = [1.0, 2.0, 2.0]
r = [1/(2.0**0.5), 1/(2.0**0.5), 0.25]
fig = plt.figure()
# initialize axis, important: set the aspect ratio to equal
ax = fig.add_subplot(111, aspect='equal')
# define axis limits for all patches to show
ax.axis([min(x)-1., max(x)+1., min(y)-1., max(y)+1.])
# loop through all triplets of x-,y-coordinates and radius and
# plot a circle for each:
for x, y, r in zip(x, y, r):
ax.add_artist(Circle(xy=(x, y),
radius=r))
plt.show()
The plot this generates looks like this:
Using the zoom-option from the plot window, one can obtain such a plot:
This zoomed in version has kept the original circle size so the 'connection' can be seen.
If you want to change the circles to be transparent, patches.Circle takes an alpha as argument. Just make sure you insert it with the call to Circle not add_artist:
ax.add_artist(Circle(xy=(x, y),
radius=r,
alpha=0.5))