Related
As part of a COVID/Lockdown/Geek project making a 3D Antikythera model I need to print a Saros Dial to be exactly 8.9 cm in width. I have muddled my way through playing around with the print scale. This works for the office printer. However I want to have it etched on plexiglas and I need to upload a DIN A4 jpeg or pdf with the (0,0) coordinate right in the centre
Code for the dial below:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# define parameters
a = 2 # determines the width of the turns - set to 1 means width is 2 Pi
thetaMin, thetaMax = 26*np.pi, 34*np.pi # Dial starts at the 13th turn and finishes at the 17th turn
steps = 223+1 # Number of Saros periods (+1 for end marker)
# Generate plotting values
# Main spiral
theta = np.linspace(thetaMin, thetaMax, steps)
r = theta * a
x = r*np.cos(theta)
y = r*np.sin(-theta)
# Main spiral outer rim completion
thetaMinComp, thetaMaxComp = thetaMax, thetaMax+(2*np.pi)
thetaComp= np.linspace(thetaMinComp, thetaMaxComp,steps)
rComp = thetaComp * a
xComp = rComp*np.cos(thetaComp)
yComp = rComp*np.sin(-thetaComp)
# Seperator lines
# Need to plot between the period startpoint accros the turn(out from the center (0,0))
l=np.sqrt(np.square(x)+np.square(y)+1.2) # calculate length from center, just shortening it makes it not overshoot
xnew= x*1/l*(l+2*np.pi*a) # get the x for the line extendeaccros the turn
ynew= y*1/l*(l+2*np.pi*a) # get the y for the line extendeaccros the turn
#start plotting
fig, ax = plt.subplots(figsize=(20,20))
plt.axis('off')
for i in range(len(x)):
plt.plot([x[i],xnew[i]],[y[i],ynew[i]], c='r', lw=2)
# plt.scatter(x[i], y[i], c = 'g') # Plots dial points for reference
steps = 223+1 # Number of Saros periods (+1 for end marker)
a = 2 # determines the width of the turns - set to 1 means width is 2 Pi
thetaMin, thetaMax = (26*np.pi+(2*np.pi)/(.5*steps)), (34*np.pi+(2*np.pi)/(.5*steps)) # Dial starts at the 13th turn and finishes at the 17th turn
theta = np.linspace(thetaMin, thetaMax, steps)
r = theta * a
xx = r*np.cos(theta)
yy = r*np.sin(-theta)
xxnew= xx*1/l*(l+2*np.pi*(a*.5)) # get the x for the line extendeaccros the turn
yynew= yy*1/l*(l+2*np.pi*(a*.5)) # get the y for the line extendeaccros the turn
plt.plot(x, y, c='r', lw=2)
plt.plot(xComp, yComp,c='r', lw=2)
plt.scatter(0,0)
plt.text(0,75,'Saros Dial', {'fontname': 'Herculanum',
'fontsize': '100',
'fontweight' : 'bold',
'verticalalignment': 'baseline',
'horizontalalignment': 'center'})
plt.show()
I have found Using matplotlib, how can I print something "actual size"? but is still fiddling. Is there an easier way?
This proved to be frustrating. It is printer dependent...and online printer specifications for the margins may not match the actual margins when printing. I have a hunch it may even be OS or driver specific.
import matplotlib.pyplot as plt
import matplotlib as mpl
# This example fits DIN A4 paper on a HP Laserjet Pro 200 MFP
# figure settings
left_margin = 0.2
right_margin = 0.9
top_margin = 0.6
bottom_margin = 0.6
left_right_margin = left_margin+right_margin
top_bottom_margin = top_margin+bottom_margin
figure_width = 21-left_right_margin # cm
figure_height = 29.7-top_bottom_margin
# Don't change
left = left_right_margin / figure_width # Percentage from height
bottom = top_bottom_margin / figure_height # Percentage from height
width = 1 - left*2
height = 1 - bottom*2
cm2inch = 1/2.54 # inch per cm
h_corr = 1.017
v_corr = 1.011
# specifying the width and the height of the box in inches
fig = plt.figure(figsize=(figure_width*cm2inch*h_corr,figure_height*cm2inch*v_corr))
ax = fig.add_axes((left, bottom, width, height))
# limits settings (important)
# plt.xlim(0, figure_width * width)#(0,0) at left bottom
# plt.ylim(0, figure_height * height)#(0,0) at left bottom
plt.xlim(-figure_width * width/2, figure_width * width/2) #centers (0,0)
plt.ylim(-figure_height * height/2, figure_height * height/2)#centers (0,0)
# your Plot (consider above limits)
# # define parameters
a = .03538 # determines the width of the turns - set to 1 means width is 2 Pi
thetaMin, thetaMax = 26*np.pi, 34*np.pi # Dial starts at the 13th turn and finishes at the 17th turn
steps = 223+1 # Number of Saros periods (+1 for end marker)
# Generate plotting values
# Main spiral
theta = np.linspace(thetaMin, thetaMax, steps)
r = theta * a
x = r*np.cos(theta)
y = r*np.sin(-theta)
# Main spiral outer rim completion
thetaMinComp, thetaMaxComp = thetaMax, thetaMax+(2*np.pi)
thetaComp= np.linspace(thetaMinComp, thetaMaxComp,steps)
rComp = thetaComp * a
xComp = rComp*np.cos(thetaComp)
yComp = rComp*np.sin(-thetaComp)
# Seperator lines
# Need to plot between the period startpoint accros the turn(out from the center (0,0))
l=np.sqrt(np.square(x)+np.square(y)+1.2) # calculate length from center, just shortening it makes it not overshoot
xnew= x*1/l*(l+2*np.pi*a) # get the x for the line extendeaccros the turn
ynew= y*1/l*(l+2*np.pi*a) # get the y for the line extendeaccros the turn
for i in range(len(x)):
plt.plot([x[i],xnew[i]],[y[i],ynew[i]], c='r', lw=1)
xx = r*np.cos(theta)
yy = r*np.sin(-theta)
xxnew= xx*1/l*(l+2*np.pi*(a*.5)) # get the x for the line extendeaccros the turn
yynew= yy*1/l*(l+2*np.pi*(a*.5)) # get the y for the line extendeaccros the turn
plt.plot(x, y, c='b', lw=1)
plt.plot(xComp, yComp,c='g', lw=1)
plt.scatter(0,0)
plt.text(0,4.75,'Saros Dial',
{'fontname': 'Herculanum',
'fontsize': '30',
'fontweight' : 'bold',
'verticalalignment': 'baseline',
'horizontalalignment': 'center'})
plt.plot([-8,-8],[-10,10])
plt.text(-7,0, ' 20 centimeters' , rotation=90, ha='center', va='center', fontsize=30, color='magenta')
plt.show()
plt.show()
# save figure ( printing png file had better resolution, pdf was lighter and better on screen)
fig.savefig('A4_grid_cm.png', dpi=1000)
fig.savefig('tA4_grid_cm.pdf')
I'm making a program to simulate the retrograde motion of mars visible from Earth.
So it's a plan view of Earth and Mars orbiting the sun
There is also a line going from Earth to Mars.
However, I need it to intersect the point that is Mars and keep going until it intersects the line x = 15
import math
import matplotlib.pyplot as plt
import matplotlib.animation as animation
def _update_plot(i, fig, scat, l):
scat.set_offsets(([math.cos(math.radians(i))*5, math.sin(math.radians(i))*5], [math.cos(math.radians(i/2))*10, math.sin(math.radians(i/2))*10], [0, 0]))
l.set_data(([math.cos(math.radians(i))*5,math.cos(math.radians(i/2))*10],[math.sin(math.radians(i))*5,math.sin(math.radians(i/2))*10]))
return [scat,l]
fig = plt.figure()
x = [0]
y = [0]
ax = fig.add_subplot(111)
ax.set_aspect('equal')
ax.grid(True, linestyle = '-', color = '0.10')
ax.set_xlim([-11, 11])
ax.set_ylim([-11, 11])
l, = plt.plot([],[], 'r--', zorder=1)
scat = plt.scatter(x, y, c = x, zorder=2)
scat.set_alpha(0.8)
anim = animation.FuncAnimation(fig, _update_plot, fargs = (fig, scat, l),
frames = 720, interval = 10)
plt.show()
I'm not sure I fully understand what you want to do, but I'll assume you want to extend the line that connects the two planets up to x = 15. In this case you can do the following:
Compute the Earth-Mars direction by subtracting their position and normalising the resulting vector. Take one of the planet as the starting point of the line.
Solve the first-order equation that gives you the distance you need to travel to get to the x = 15 axis.
Check whether the result is positive or negative. If it's positive then keep going, if it's negative we chose the wrong planet, as in order for the line to connect the two planes and then keep going towards x = 15 we need to take the other planet. We do so by inverting the direction and re-solving the first-order equation with the new conditions.
Find the y-coordinate of the point at which the line intersects the x = 15 axis
Draw a line from the planet to the intersection point on the x = 15 axis.
Something like this should do the trick:
def _update_plot(i, fig, scat, l):
angle = math.radians(i)
sun_pos = np.array([0., 0.])
earth_pos = np.array([math.cos(angle)*5, math.sin(angle)*5])
mars_pos = np.array([math.cos(angle / 2.) * 10, math.sin(angle / 2.) * 10])
# compute the unit vector that points from Earth to Mars
direction = mars_pos - earth_pos
direction /= np.sqrt(np.dot(direction, direction))
# find out where the line would intersect the x = 15 axis
start_from = earth_pos
alpha = (15 - start_from[0]) / direction[0]
# if alpha comes out to be negative we picked the "wrong" planet
if alpha < 0:
start_from = mars_pos
direction = -direction
alpha = (15 - start_from[0]) / direction[0]
y_line = start_from[1] + alpha * direction[1]
# draw the planets
scat.set_offsets((earth_pos, mars_pos, sun_pos))
# draw the line
l.set_data(([start_from[0], 15], [start_from[1], y_line]))
return [scat,l]
from here:
Animation using matplotlib with subplots and ArtistAnimation
I got some hints on my question - however not enough. My problem:
I have two animations both somehow coupled and want to show each of them in a different subplot.
the first animation in the first subplot works fine - however the second (coupled to the first) animation in the second subplot just influences the first animation ...
So how do I decouple the subplots in a way that the second subplot doew NOT influence the first one:
here is the code of the example:
import math
from ClimateUtilities import *
import phys
import numpy as nm
import matplotlib.animation as animation
import matplotlib.pyplot as plt
from matplotlib import patches
#from matplotlib import animation
#------------Constants and file data------------
#
printswitch = True
printswitch = False
printswitch2 = True
#printswitch2 = False
ECCabsoluteMax = 0.9
ECCmax = 0.067 # maximum value for this run -
# should not be greater than
# ECCabsoluteMax
#ECCmax = 0.9 # maximum value for this run - should not be greater
# than
# ECCabsoluteMax
if ECCmax >= ECCabsoluteMax:
ECCmax = ECCabsoluteMax
ECCdelta = 0.001 # interval for graph
eccentricity = nm.arange(0., ECCmax, ECCdelta, dtype=float)
semimajorA = 1.0 # astronomical unit =~ 150.000.000 km mean
# distance Sun Earth
totalRadN0 = 1370. # radiation of Sun at TOA in Watt/m**2
albedoEarth = 0.3 # presently albedo of Earth, geographically
# constant
T = 365.25 # duration of one orbit around central celestial
# body in days
# here: duration of one orbit of Earth around Sun
R = 6378100.0 # radius of Earth in meters
TOIdim = ECCmax/ECCdelta
TOI = nm.arange(0., TOIdim, dtype=float )
# total insolation at location of Earth summed over 1
# year
deltaT = 500 # ms interval of moving
# now define various "functions" like:
def computeTOI( ee, semimajorAxis, radiationAtStar, alpha ):
aa = semimajorAxis # semimajor axis of orbital ellipse
N0 = radiationAtStar# radiation of start at position of star (r = 0)
resultTOI = 2.*nm.pi*T*R**2*N0*alpha/(aa**2*math.sqrt(1 - ee**2))
return resultTOI
#
#####################################################################
#
print "start of ellipticity and absorbed insolation"
#
#
# Start of programme here
#
#####################################################################
# compute the various TOIs dependant on eccentricity "ecc"
#
ii = 0
for ecc in eccentricity:
if printswitch: print 'TOI = ', computeTOI( ecc, semimajorA,
totalRadN0, albedoEarth ), '\n'
TOI[ii] = computeTOI( ecc, semimajorA, totalRadN0, 1. - albedoEarth
)/10.0**19
ii = ii + 1
# TOI is an array consisting of TOIs depending on eccemtricity "ecc"
x = eccentricity
if printswitch: print 'TOI = ', TOI
##########################################################################
# almost the whole screen is filled with this plot ... :)
##########################################################################
Main = plt.figure(figsize=(15.0,15.0))
Main.subplots_adjust(top=0.95, left=0.09, right=0.95, hspace=0.20)
##########################################################################
axFigTOI = Main.add_subplot(211) # first subplot
# Plot ... TOI over ECC:
if ECCmax < 0.07:
plt.axis([0,0.07,8.9,9.0])
plt.title( 'Absorbed Irradiation and Orbital Eccentricity for Planet
Earth\n' )
plt.ylabel( 'Absorbed total \nsolar irradiation \n[Watt] *10**19' )
plt.xlabel( 'Eccentricity "e"' )
plt.plot( x, TOI, 'r-' ) # 'x' and 'TOI' are also center of "mini-
# ellipse"
# Now enter an ellipse here on Subplot 211 (first subplot) which slides
# along curve:
xcenter, ycenter = x[1],TOI[1] # center of ellipse to start with
width = 0.0025 # width of small ellipse
height = 0.01 # height of small ellipse
def init(): # in order to initialize animation
e1 = patches.Ellipse((xcenter, ycenter), width, height,\
angle=0.0, linewidth=2, fill=False )
axFigTOI.add_patch(e1)
e1.set_visible( False ) # do not show (if True then ellipse
# stays here
return [e1]
def animateEllipse(i):
xcenter = x[i]
ycenter = TOI[i]
e1 = patches.Ellipse( ( xcenter, ycenter ), width, height,\
angle = 0.0, linewidth = 2, fill = True )
if i == 1:
e1.set_visible( True )
axFigTOI.add_patch(e1)
if printswitch: print 'i = ', i
return [e1]
anim = animation.FuncAnimation( Main,
animateEllipse,
init_func=init,
frames= int( TOIdim ),
interval=deltaT,
blit=True )
#########################################################################
# the second subplot in the first figure for size of ellipse depending on
# ECC
#########################################################################
# we still have a problem to get the "patch" (Ellipse) into the 2nd
# subplot ...
axFigEllipse = Main.add_subplot(212)
plt.title( 'Shape of an Ellipse due to eccentricity' )
plt.ylabel( 'Height of Ellipse' )
plt.xlabel( 'Constant Semi-major Axis' )
"""
#
# create an ellipse with following parameters - to be changed later for
# curve
# values
#
xcenter2 = x[40]
ycenter2 = TOI[40] # center of ellipse 2 to start with
width2 = 0.0125
height2 = 0.0115
ell2 = patches.Ellipse( ( xcenter2, ycenter2 ), width2, height2,\
angle=0.0, linewidth=2, fill=False )
ell2.set_visible(True)
axFigEllipse.add_patch(ell2)
#"""
"""
def init212(): # in order to initialize animation
ell2 = patches.Ellipse((xcenter2, ycenter2), width2, height2,\
angle=0.0, linewidth=2, fill=False )
axFigEllipse.add_patch(ell2)
ell2.set_visible( False ) # do not show (if True then ellipse
# stays here
return [ell2]
def animateEllipse(jj):
#xcenter2 = xcenter2 + jj/10**4
#ycenter2 = ycenter2 + jj/10**4
ell2 = patches.Ellipse((xcenter2, ycenter2), width2, height2,\
angle=0.0, linewidth=2, fill=True, zorder=2)
if jj == 1:
ell2.set_visible(True)
axFigEllipse.add_patch(ell2)
return [ell2]
anim = animation.FuncAnimation( Main, animateEllipse,
init_func=init212,
frames=360,
interval=20,
blit=True )
#anim = animation.FuncAnimation(figEllipse, animateEllipse,
init_func=init_Ellipse, interval=1, blit=True)
#"""
plt.show()
Now when I remove the """ then there is only the red line visible ... no activity ...
In your code, you essentially redefine animateEllipse later. You should only call a single animate in a script which should update both ellipses (return both handles from the objects). For resizing you can just use the existing ellipse handle but moving appears to need a new ellipse to be added. I couldn;t get your code working but as a minimal example a function to update all subplots (these can each be in their own functions).
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
from matplotlib import animation
import numpy as np
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
e1 = Ellipse(xy=(0.5, 0.5), width=0.5, height=0.5, angle=0)
e2 = Ellipse(xy=(0.5, 0.5), width=0.5, height=0.5, angle=0)
ax1.add_patch(e1)
ax2.add_patch(e2)
def init():
e1.set_visible(False)
e2.set_visible(False)
return e1,e2
def animateEllipse211(i):
e1 = Ellipse(xy=(0.5+0.2*np.sin(i/200.), 0.5+0.2*np.sin(i/200.)), width=0.5, height=0.5, angle=0)
ax1.add_patch(e1)
if i==0:
e1.set_visible(True)
return e1
def animateEllipse212(i):
if i==0:
e2.set_visible(True)
e2.width = 0.5*np.sin(i/200.)
e2.height = 0.5*np.sin(i/200.)
return e2
def animate(i):
e1 = animateEllipse211(i)
e2 = animateEllipse212(i)
return e1,e2
anim = animation.FuncAnimation(fig, animate, init_func=init, interval=1, blit=True)
plt.show()
UPDATE: I'm not sure why this strange init problem occurs but think it has been noted on a few other posts (this and this) to be due to using blit=True. The animation on matplotlib is a little rough around the edges and certainly isn't very intuitive. Worse than that, the choice of backend (i.e. what plots the actual data) makes a difference to the way it works. Personally I normally run a loop, use interactive mode and save figures if I need a video.
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np
#Setup figure, add subplots and ellipses
fig = plt.figure()
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
e1 = Ellipse(xy=(0.5, 0.5), width=0.5, height=0.5, angle=0)
e2 = Ellipse(xy=(0.5, 0.5), width=0.5, height=0.5, angle=0)
ax1.add_patch(e1)
ax2.add_patch(e2)
#Plot Red line
ax1.plot(np.linspace(.3,.7,100),np.linspace(.3,.7,100),'r-')
#Turn on interactive plot
plt.ion()
plt.show()
#Define a loop and update various
for i in range(0, 10000, 10):
print(i)
#Update ellipse 1
e1.remove()
e1 = Ellipse(xy=(0.5+0.2*np.sin(i/200.),
0.5+0.2*np.sin(i/200.)),
width=0.5, height=0.5, angle=0)
ax1.add_patch(e1)
#Update ellipse 2
e2.width = 0.5*np.sin(i/200.)
e2.height = 0.5*np.sin(i/200.)
plt.draw()
plt.pause(0.0001)
I am fairly new to using matplotlib and cannot find any examples that show how to mark the angle of a point. I need to find the angle in all four quadrants i.e. if the point is (1,1) angle=45 degrees, (-1,1) angle= 135 degrees, (-1,-1) angle=225 degrees and for (1,-1) it should be 315 degrees.
Here is the function to which i need to add this to:
def visualize(val,ar):
plt.figure()
ax = plt.gca()
ax.plot([val-5],[ar-5], marker='o', color='r')
ax.set_xlim([-5,5])
ax.set_ylim([-5,5])
plt.draw()
plt.grid()
plt.show()
I think you need to do the maths on your points yourself and then annotate the points on the plot using annotate(). It's hard to tell from your example whether val and ar are single values or vectors - I think single values given the syntax you're using. Here's an example with a function to do the maths and an example use of annotate - I've tried to keep the plotting bits the same as your code and just add the bits to calculate degrees and then put them on the axes
import math
import matplotlib.pyplot as plt
#This function does the maths - turns co-ordinates into angles
#with 0 on +ve x axis, increasing anti-clockwise
def get_theta(x,y):
theta = math.atan(y*1.0/x) / (2*math.pi) * 360
if x < 0:
theta += 180
if theta < 0:
theta += 360
return theta
def visualize(val,ar):
ax = plt.gca()
ax.plot([val-5],[ar-5], marker='o', color='r')
ax.set_xlim([-5,5])
ax.set_ylim([-5,5])
#insert the following to calculate the angles and
#then annotate them on the plot
x,y = val-5,ar-5
label = get_theta(x, y)
ax.annotate(str(label)+' degrees', xy = (x, y), xytext = (-20, 20),textcoords = 'offset points')
if __name__ == '__main__':
plt.figure()
x = [6,4,4,6]
y = [6,6,4,4]
for (val, ar) in zip(x,y):
visualize(val,ar)
plt.draw()
plt.grid()
plt.show()
Further variations on what you can do with annotations are in the docs.
Output
In Matplotlib, it's not too tough to make a legend (example_legend(), below), but I think it's better style to put labels right on the curves being plotted (as in example_inline(), below). This can be very fiddly, because I have to specify coordinates by hand, and, if I re-format the plot, I probably have to reposition the labels. Is there a way to automatically generate labels on curves in Matplotlib? Bonus points for being able to orient the text at an angle corresponding to the angle of the curve.
import numpy as np
import matplotlib.pyplot as plt
def example_legend():
plt.clf()
x = np.linspace(0, 1, 101)
y1 = np.sin(x * np.pi / 2)
y2 = np.cos(x * np.pi / 2)
plt.plot(x, y1, label='sin')
plt.plot(x, y2, label='cos')
plt.legend()
def example_inline():
plt.clf()
x = np.linspace(0, 1, 101)
y1 = np.sin(x * np.pi / 2)
y2 = np.cos(x * np.pi / 2)
plt.plot(x, y1, label='sin')
plt.plot(x, y2, label='cos')
plt.text(0.08, 0.2, 'sin')
plt.text(0.9, 0.2, 'cos')
Update: User cphyc has kindly created a Github repository for the code in this answer (see here), and bundled the code into a package which may be installed using pip install matplotlib-label-lines.
Pretty Picture:
In matplotlib it's pretty easy to label contour plots (either automatically or by manually placing labels with mouse clicks). There does not (yet) appear to be any equivalent capability to label data series in this fashion! There may be some semantic reason for not including this feature which I am missing.
Regardless, I have written the following module which takes any allows for semi-automatic plot labelling. It requires only numpy and a couple of functions from the standard math library.
Description
The default behaviour of the labelLines function is to space the labels evenly along the x axis (automatically placing at the correct y-value of course). If you want you can just pass an array of the x co-ordinates of each of the labels. You can even tweak the location of one label (as shown in the bottom right plot) and space the rest evenly if you like.
In addition, the label_lines function does not account for the lines which have not had a label assigned in the plot command (or more accurately if the label contains '_line').
Keyword arguments passed to labelLines or labelLine are passed on to the text function call (some keyword arguments are set if the calling code chooses not to specify).
Issues
Annotation bounding boxes sometimes interfere undesirably with other curves. As shown by the 1 and 10 annotations in the top left plot. I'm not even sure this can be avoided.
It would be nice to specify a y position instead sometimes.
It's still an iterative process to get annotations in the right location
It only works when the x-axis values are floats
Gotchas
By default, the labelLines function assumes that all data series span the range specified by the axis limits. Take a look at the blue curve in the top left plot of the pretty picture. If there were only data available for the x range 0.5-1 then then we couldn't possibly place a label at the desired location (which is a little less than 0.2). See this question for a particularly nasty example. Right now, the code does not intelligently identify this scenario and re-arrange the labels, however there is a reasonable workaround. The labelLines function takes the xvals argument; a list of x-values specified by the user instead of the default linear distribution across the width. So the user can decide which x-values to use for the label placement of each data series.
Also, I believe this is the first answer to complete the bonus objective of aligning the labels with the curve they're on. :)
label_lines.py:
from math import atan2,degrees
import numpy as np
#Label line with line2D label data
def labelLine(line,x,label=None,align=True,**kwargs):
ax = line.axes
xdata = line.get_xdata()
ydata = line.get_ydata()
if (x < xdata[0]) or (x > xdata[-1]):
print('x label location is outside data range!')
return
#Find corresponding y co-ordinate and angle of the line
ip = 1
for i in range(len(xdata)):
if x < xdata[i]:
ip = i
break
y = ydata[ip-1] + (ydata[ip]-ydata[ip-1])*(x-xdata[ip-1])/(xdata[ip]-xdata[ip-1])
if not label:
label = line.get_label()
if align:
#Compute the slope
dx = xdata[ip] - xdata[ip-1]
dy = ydata[ip] - ydata[ip-1]
ang = degrees(atan2(dy,dx))
#Transform to screen co-ordinates
pt = np.array([x,y]).reshape((1,2))
trans_angle = ax.transData.transform_angles(np.array((ang,)),pt)[0]
else:
trans_angle = 0
#Set a bunch of keyword arguments
if 'color' not in kwargs:
kwargs['color'] = line.get_color()
if ('horizontalalignment' not in kwargs) and ('ha' not in kwargs):
kwargs['ha'] = 'center'
if ('verticalalignment' not in kwargs) and ('va' not in kwargs):
kwargs['va'] = 'center'
if 'backgroundcolor' not in kwargs:
kwargs['backgroundcolor'] = ax.get_facecolor()
if 'clip_on' not in kwargs:
kwargs['clip_on'] = True
if 'zorder' not in kwargs:
kwargs['zorder'] = 2.5
ax.text(x,y,label,rotation=trans_angle,**kwargs)
def labelLines(lines,align=True,xvals=None,**kwargs):
ax = lines[0].axes
labLines = []
labels = []
#Take only the lines which have labels other than the default ones
for line in lines:
label = line.get_label()
if "_line" not in label:
labLines.append(line)
labels.append(label)
if xvals is None:
xmin,xmax = ax.get_xlim()
xvals = np.linspace(xmin,xmax,len(labLines)+2)[1:-1]
for line,x,label in zip(labLines,xvals,labels):
labelLine(line,x,label,align,**kwargs)
Test code to generate the pretty picture above:
from matplotlib import pyplot as plt
from scipy.stats import loglaplace,chi2
from labellines import *
X = np.linspace(0,1,500)
A = [1,2,5,10,20]
funcs = [np.arctan,np.sin,loglaplace(4).pdf,chi2(5).pdf]
plt.subplot(221)
for a in A:
plt.plot(X,np.arctan(a*X),label=str(a))
labelLines(plt.gca().get_lines(),zorder=2.5)
plt.subplot(222)
for a in A:
plt.plot(X,np.sin(a*X),label=str(a))
labelLines(plt.gca().get_lines(),align=False,fontsize=14)
plt.subplot(223)
for a in A:
plt.plot(X,loglaplace(4).pdf(a*X),label=str(a))
xvals = [0.8,0.55,0.22,0.104,0.045]
labelLines(plt.gca().get_lines(),align=False,xvals=xvals,color='k')
plt.subplot(224)
for a in A:
plt.plot(X,chi2(5).pdf(a*X),label=str(a))
lines = plt.gca().get_lines()
l1=lines[-1]
labelLine(l1,0.6,label=r'$Re=${}'.format(l1.get_label()),ha='left',va='bottom',align = False)
labelLines(lines[:-1],align=False)
plt.show()
#Jan Kuiken's answer is certainly well-thought and thorough, but there are some caveats:
it does not work in all cases
it requires a fair amount of extra code
it may vary considerably from one plot to the next
A much simpler approach is to annotate the last point of each plot. The point can also be circled, for emphasis. This can be accomplished with one extra line:
import matplotlib.pyplot as plt
for i, (x, y) in enumerate(samples):
plt.plot(x, y)
plt.text(x[-1], y[-1], f'sample {i}')
A variant would be to use the method matplotlib.axes.Axes.annotate.
Nice question, a while ago I've experimented a bit with this, but haven't used it a lot because it's still not bulletproof. I divided the plot area into a 32x32 grid and calculated a 'potential field' for the best position of a label for each line according the following rules:
white space is a good place for a label
Label should be near corresponding line
Label should be away from the other lines
The code was something like this:
import matplotlib.pyplot as plt
import numpy as np
from scipy import ndimage
def my_legend(axis = None):
if axis == None:
axis = plt.gca()
N = 32
Nlines = len(axis.lines)
print Nlines
xmin, xmax = axis.get_xlim()
ymin, ymax = axis.get_ylim()
# the 'point of presence' matrix
pop = np.zeros((Nlines, N, N), dtype=np.float)
for l in range(Nlines):
# get xy data and scale it to the NxN squares
xy = axis.lines[l].get_xydata()
xy = (xy - [xmin,ymin]) / ([xmax-xmin, ymax-ymin]) * N
xy = xy.astype(np.int32)
# mask stuff outside plot
mask = (xy[:,0] >= 0) & (xy[:,0] < N) & (xy[:,1] >= 0) & (xy[:,1] < N)
xy = xy[mask]
# add to pop
for p in xy:
pop[l][tuple(p)] = 1.0
# find whitespace, nice place for labels
ws = 1.0 - (np.sum(pop, axis=0) > 0) * 1.0
# don't use the borders
ws[:,0] = 0
ws[:,N-1] = 0
ws[0,:] = 0
ws[N-1,:] = 0
# blur the pop's
for l in range(Nlines):
pop[l] = ndimage.gaussian_filter(pop[l], sigma=N/5)
for l in range(Nlines):
# positive weights for current line, negative weight for others....
w = -0.3 * np.ones(Nlines, dtype=np.float)
w[l] = 0.5
# calculate a field
p = ws + np.sum(w[:, np.newaxis, np.newaxis] * pop, axis=0)
plt.figure()
plt.imshow(p, interpolation='nearest')
plt.title(axis.lines[l].get_label())
pos = np.argmax(p) # note, argmax flattens the array first
best_x, best_y = (pos / N, pos % N)
x = xmin + (xmax-xmin) * best_x / N
y = ymin + (ymax-ymin) * best_y / N
axis.text(x, y, axis.lines[l].get_label(),
horizontalalignment='center',
verticalalignment='center')
plt.close('all')
x = np.linspace(0, 1, 101)
y1 = np.sin(x * np.pi / 2)
y2 = np.cos(x * np.pi / 2)
y3 = x * x
plt.plot(x, y1, 'b', label='blue')
plt.plot(x, y2, 'r', label='red')
plt.plot(x, y3, 'g', label='green')
my_legend()
plt.show()
And the resulting plot:
matplotx (which I wrote) has line_labels() which plots the labels to the right of the lines. It's also smart enough to avoid overlaps when too many lines are concentrated in one spot. (See stargraph for examples.) It does that by solving a particular non-negative-least-squares problem on the target positions of the labels. Anyway, in many cases where there's no overlap to begin with, such as the example below, that's not even necessary.
import matplotlib.pyplot as plt
import matplotx
import numpy as np
# create data
rng = np.random.default_rng(0)
offsets = [1.0, 1.50, 1.60]
labels = ["no balancing", "CRV-27", "CRV-27*"]
x0 = np.linspace(0.0, 3.0, 100)
y = [offset * x0 / (x0 + 1) + 0.1 * rng.random(len(x0)) for offset in offsets]
# plot
with plt.style.context(matplotx.styles.dufte):
for yy, label in zip(y, labels):
plt.plot(x0, yy, label=label)
plt.xlabel("distance [m]")
matplotx.ylabel_top("voltage [V]") # move ylabel to the top, rotate
matplotx.line_labels() # line labels to the right
plt.show()
# plt.savefig("out.png", bbox_inches="tight")
A simpler approach like the one Ioannis Filippidis do :
import matplotlib.pyplot as plt
import numpy as np
# evenly sampled time at 200ms intervals
tMin=-1 ;tMax=10
t = np.arange(tMin, tMax, 0.1)
# red dashes, blue points default
plt.plot(t, 22*t, 'r--', t, t**2, 'b')
factor=3/4 ;offset=20 # text position in view
textPosition=[(tMax+tMin)*factor,22*(tMax+tMin)*factor]
plt.text(textPosition[0],textPosition[1]+offset,'22 t',color='red',fontsize=20)
textPosition=[(tMax+tMin)*factor,((tMax+tMin)*factor)**2+20]
plt.text(textPosition[0],textPosition[1]+offset, 't^2', bbox=dict(facecolor='blue', alpha=0.5),fontsize=20)
plt.show()
code python 3 on sageCell