Related
This question already has answers here:
Plot a point on a line closest to a point
(1 answer)
Python: point on a line closest to third point
(3 answers)
Python: Closest Point to a line
(1 answer)
Closed 3 months ago.
This post was edited and submitted for review 3 months ago and failed to reopen the post:
Original close reason(s) were not resolved
I have the problem of finding the point which is closest to a line from an array of x- and y-data.
The line is semi-infinite originating from the origin at (0,0) and running into the direction of a given angle.
The x,y data of the points are given in relation to the origin.
How do I find the closest point (and its distance) to the line in line direction (not opposite)?
This is an example of the data I have:
import numpy as np
import matplotlib.pyplot as plt
def main():
depth = np.random.random((100))*20+50
angle = np.linspace(0, 2*np.pi, 100)
x,y = depth2xy(depth, angle)
line = np.random.random_sample()*2*np.pi
# fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
plt.scatter(x, y)
plt.plot([0,100*np.cos(line)], [0, 100*np.sin(line)], markersize=10, color = "r")
plt.show()
def depth2xy(depth, angle):
x, y = np.zeros(len(depth)), np.zeros(len(depth))
for i in range(len(depth)):
x[i] = depth[i]*np.cos(angle[i])
y[i] = depth[i]*np.sin(angle[i])
return x,y
if __name__ == "__main__": main()
I could try a brute force approach, iterating over different distances along the line to find the ultimate smallest distance.
But as time efficiency is critical my case and the algorithm would not perform as well as I think it could, I would rather try an analytical approach.
I also thought about scipy.spatial.distance, but I am not sure how this would work for a line.
Your assigned line passes through the origin, its parametric equation is
x = u cos(a)
y = u sin(a)
and you can see the parameter u is simply the (oriented) distance beteween the origin and a point on the assigned line.
Now, consider a point of coordinates X and Y, a line perpendicular to the assigned one has the parametric equation
x = X - v sin(a)
y = Y + v cos(a)
and again, the parameter v is simply the (oriented) distance between (X, Y) and a point on a line passing per (X, Y) and perpendicular to the assigned one.
The intersection is given by the equation
X = u cos(a) + v sin(a)
Y = u sin(a) - v cos(a)
you can check by inspection that the solution of the system is
u = X cos(a) + Y sin(a)
v = X sin(a) - Y cos(a)
The distance of the point (X, Y) from the assigned line is hence
d = | X sin(a) - Y cos(a) |
A Python Implementation
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(20221126)
X = 2*np.random.random(32)-1
Y = 2*np.random.random(32)-1
fig, ax = plt.subplots()
ax.set_xlim((-1.2, 1.2))
ax.set_ylim((-1.2, 1.2))
ax.grid(1)
ax.set_aspect(1)
ax.scatter(X, Y, s=80, ec='k', color='y')
a = 2*np.random.random()*np.pi
s, c = np.sin(a), np.cos(a)
plt.plot((0, c), (0, s), color='k')
plt.plot((-s, s), (c, -c), color='r')
# strike out "bad" points
bad = X*c+Y*s<0
plt.scatter(X[bad], Y[bad], marker='x', color='k')
# consider only good (i.e., not bad) points
Xg, Yg = X[~bad], Y[~bad]
# compute all distances (but for good points only)
d = np.abs(Xg*s-Yg*c)
# find the nearest point and hilight it
imin = np.argmin(d)
plt.scatter(Xg[imin], Yg[imin], ec='k', color='r')
plt.show()
An OVERDONE Example
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(20221126)
X = 2*np.random.random(32)-1
Y = 2*np.random.random(32)-1
fig, axs = plt.subplots(2, 4, figsize=(10,5), layout='constrained')
for ax, a in zip(axs.flat,
(2.8, 1.8, 1.4, 0.2,
3.4, 4.5, 4.9, 6.0)):
ax.set_xlim((-1.2, 1.2))
ax.set_xticks((-1, -0.5, 0, 0.5, 1.0))
ax.set_ylim((-1.2, 1.2))
ax.grid(1)
ax.set_aspect(1)
ax.set_title('$\\alpha \\approx %d^o$'%round(np.rad2deg(a)))
ax.scatter(X, Y, s=80, ec='k', color='yellow')
s, c = np.sin(a), np.cos(a)
ax.arrow(0, 0, 1.2*c, 1.2*s, fc='k',
length_includes_head=True,
head_width=0.08, head_length=0.1)
# divide the drawing surface in two semiplanes
if abs(c)>abs(s):
if c>0:
ax.plot((1.2*s, -1.2*s), (-1.2, 1.2))
else:
ax.plot((-1.2*s, 1.2*s), (-1.2, 1.2))
elif abs(s)>=abs(c):
if s>0:
ax.plot((-1.2, 1.2), (1.2*c, -1.2*c))
else:
ax.plot((-1.2, 1.2), (-1.2*c, 1.2*c))
# strike out "bad" points
bad = X*c+Y*s<0
ax.scatter(X[bad], Y[bad], marker='x', color='k')
# consider only good (i.e., not bad) points
Xg, Yg = X[~bad], Y[~bad]
# compute all distances (but for good points only)
d = np.abs(Xg*s-Yg*c)
# find the nearest point and hilight it
imin = np.argmin(d)
ax.scatter(Xg[imin], Yg[imin], s=80, ec='k', color='yellow')
ax.scatter(Xg[imin], Yg[imin], s= 10, color='k', alpha=1.0)
plt.show()
Let P be a point from your know data set. Let Q be the projection of this point on the line. You can use an analytic approach to determine the exact location of Q:
OQ is the segment from the origin to the Q point. It is aligned to the line.
PQ is the distance of the point P to the line.
from geometry, the dot product between QP and OQ is zero (the two segments are orthogonal to each other). From this equation we can compute the point Q.
After that, you simply compute all distances and find the shortest one.
I'm going to use SymPy for the analytical part, Numpy for the numerical part and Matplotlib for plotting:
from sympy import *
import numpy as np
import matplotlib.pyplot as plt
xq, xp, yq, yp, m = symbols("x_Q, x_P, y_Q, y_P, m")
A = Matrix([xq - xp, yq - yp])
B = Matrix([xq, yq])
# this equations contains two unkowns: xq, yq
eq = A.dot(B)
# but we know the line equation: yq = m * xq, so we substitute it into
# eq and solve for xq
xq_expr = solve(eq.subs(yq, m * xq), xq)[1]
print(xq_expr)
# (m*y_P + x_P)/(m**2 + 1)
# generate data
mv = -0.5
xp_vals = np.random.uniform(2, 10, 30)
yp_vals = np.random.uniform(2, 10, 30)
# convert the symbolic expression to a numerical function
f = lambdify([m, xp, yp], xq_expr)
# compute the projections on the line
xq_vals = f(mv, xp_vals, yp_vals)
yq_vals = mv * xq_vals
# compute the distance
d = np.sqrt((xp_vals - xq_vals)**2 + (yp_vals - yq_vals)**2)
# find the index of the shortest distance
idx = d.argmin()
fig, ax = plt.subplots()
xline = np.linspace(0, 10)
yline = mv * xline
ax.plot(xline, yline, "k:", label="line")
ax.scatter(xq_vals, yq_vals, label="Q", marker=".")
ax.scatter(xp_vals, yp_vals, label="P", marker="*")
ax.plot([xp_vals[idx], xq_vals[idx]], [yp_vals[idx], yq_vals[idx]], "r", label="min distance")
ax.set_aspect("equal")
ax.legend()
plt.show()
This question already has answers here:
Best way to plot an angle between two lines in Matplotlib
(5 answers)
Closed 3 years ago.
This is my code for making lines on a graph in a xy plane. I want to show an arc for each line with angle representing on it.
sim_score =[0.993832,0.543218,0.234745 ,0.873513,0.234565,0.789212]
plt.figure()
for i in sim_score:
for j in sim_label:
a= math.acos(i)
b = a * 180 / math.pi
point=(0,0)
x,y = point
length=10
# find the end point
endy = length * math.sin(math.radians(b))
endx = length * math.cos(math.radians(b))
# plot the points
ax = plt.subplot(111)
# set the bounds to be 10, 10
ax.set_ylim([0, 10])
ax.set_xlim([0, 10])
ax.plot([x, endx], [y,endy] )
I want to make arc with angle representing on it as a label.
following this post, i have adapted the code source to have a good basis:
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
from matplotlib.lines import Line2D
import math
def get_angle_text(angle_plot):
angle = angle_plot.get_label()[:-1] # Excluding the degree symbol
angle = "%0.2f" % float(angle) + u"\u00b0" # Display angle upto 2 decimal places
# Set the label position
x_width = angle_plot.width / 2
y_width = math.sin(math.radians(angle_plot.theta2))
return [x_width, y_width, angle]
def get_angle_plot(line1, offset=1, color=None, origin=[0, 0], len_x_axis=1, len_y_axis=1):
l1xy = line1.get_xydata()
# Angle between line1 and x-axis
slope = (l1xy[1][1] - l1xy[0][1]) / float(l1xy[1][0] - l1xy[0][0])
angle = abs(math.degrees(math.atan(slope))) # Taking only the positive angle
if color is None:
color = line1.get_color() # Uses the color of line 1 if color parameter is not passed.
return Arc(origin, len_x_axis * offset, len_y_axis * offset, 0, 0, angle, color=color,
label=str(angle) + u"\u00b0")
sim_score = [0.993832, 0.543218, 0.234745, 0.873513]
fig = plt.figure()
ax = fig.add_subplot(111)
offset = 0
for i in sim_score:
offset += 2 # you play with this value to draw arc spaced from the previous
a = math.acos(i)
b = a * 180 / math.pi
point = (0, 0)
x, y = point
length = 10
# find the end point
endy = length * math.sin(math.radians(b))
endx = length * math.cos(math.radians(b))
line = Line2D([x, endx], [y, endy], linewidth=1, linestyle="-", color="blue")
angle_plot = get_angle_plot(line, offset)
angle_text = get_angle_text(angle_plot)
ax.add_line(line )
ax.add_patch(angle_plot)
ax.text(*angle_text) # To display the angle value
ax.set_ylim([0, 10])
ax.set_xlim([0, 10])
plt.show()
you should adapt the program to place the label, i have just done a quick calculus of position
graph result:
Using: [python] [numpy] [matplotlib]
So I have a 3D array to create a scatter plot making a n * n * n cube. Those points have different values of potential represented by colors.
size = 11
z = y = x = size
potential = np.zeros((z, y, x))
Positive = 10
Negative = -10
""" ------- Positive Polo --------- """
polox = poloy = poloz = [1,2]
polos=[polox,poloy,poloz]
polop = [list(x) for x in np.stack(np.meshgrid(*polos)).T.reshape(-1,len(polos))] # Positive polos list
for coord in polop:
potential[coord] = Positive
""" ------- Negative Polo --------- """
polo2x = polo2y = polo2z = [size-3,size-2]
polos2=[polo2x,polo2y,polo2z]
polon = [list(x) for x in np.stack(np.meshgrid(*polos2)).T.reshape(-1,len(polos2))] # Negative polos list
for coord in polon:
potential[coord] = Negative
I have 2 polos of values -10 and 10 at the start and the rest of the points are calculated like this: (the mean of the surrounding points, no diagonals):
for z in range(1,size):
for y in range(1,size):
for x in range(1,size):
if [z,y,x] in polop:
potential[z,y,x] = Positive # If positive polo, keeps potential
elif [z,y,x] in polon:
potential[z,y,x] = Negative # If negative polo, keeps potential
elif z!=size-1 and y!=size-1 and x!=size-1: # Sets the potential to the mean potential of neighbors
potential[z][y][x] = (potential[z][y][x+1] + potential[z][y][x-1] + potential[z][y+1][x] + potential[z][y-1][x] + potential[z+1][y][x] + potential[z-1][y][x]) / 6
And for the outer cells:
for z in range(0,size):
for y in range(0,size):
for x in range(0,size):
potential[z,y,0] = potential[z,y,2]
potential[z,0,x] = potential[z,2,x]
potential[0,y,x] = potential[2,y,x]
if z == size-1:
potential[size-1,y,x] = potential[size-3,y,x]
elif y == size-1:
potential[z,size-1,x] = potential[z,size-3,x]
elif x == size-1:
potential[z,y,size-1] = potential[z,y,size-3]
What I need is to show a surface connecting the points that have the same value interval 'same colors' (like from 0 to 2.5).
I know that there are a lot of questions like this, but I can't adapt to my code, it either doesn't show (such as this) or it's not the same problem or it's not with python (as this one), that's why I'm asking again.
It can also be shown as a lot of subplots each with a surface.
Note: My 3D array is such that if I type print(potential[1,1,1]) it shows the value of that cell that, as you can see in the image below, is 10. And that's what I use to show the colors.
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
z,y,x = potential.nonzero()
cube = ax.scatter(x, y, z, zdir='z', c=potential[z,y,x], cmap=plt.cm.rainbow) # Plot the cube
cbar = fig.colorbar(cube, shrink=0.6, aspect=5) # Add a color bar which maps values to colors.
It would be beneficial for you to create a Minimum, Complete and Verifiable Example to make assistance easier.
It's still not clear to me how you mean to calculate your potential, nor how you mean to generate your surface, so I have included trivial functions.
The code below will generate a 3D Scatterplot of coloured points and a Surface with the average value of the colour.
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
def fn(x, y):
"""Custom fuction to determine the colour (potential?) of the point"""
return (x + y) / 2 # use average as a placeholder
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
size = 11 # range 0 to 10
# Make the 3D grid
X, Y, Z = np.meshgrid(np.arange(0, size, 1),
np.arange(0, size, 1),
np.arange(0, size, 1))
# calculate a colour for point(x,y,z)
zs = np.array([fn(x, y) for x, y in zip(np.ravel(X), np.ravel(Y))])
ZZ = zs.reshape(X.shape) # this is used below
# create the surface
xx, yy = np.meshgrid(np.arange(0, size, 1), np.arange(0, size, 1))
# Calcule the surface Z value, e.g. average of the colours calculated above
zzs = np.array([np.average(ZZ[x][y]) for x, y in zip(np.ravel(xx), np.ravel(yy))])
zz= zzs.reshape(xx.shape)
cube = ax.scatter(X, Y, Z, zdir='z', c=zs, cmap=plt.cm.rainbow)
surf = ax.plot_surface(xx, yy, zz, cmap=plt.cm.rainbow)
cbar = fig.colorbar(cube, shrink=0.6, aspect=5) # Add a color bar
plt.show()
The image generated will look something like this:
EDIT: With your additional code, I'm able to replicate your cube.
Then use the following code to generate a surface:
xx, yy = np.meshgrid(np.arange(0, size, 1), np.arange(0, size, 1))
#define potential range
min_p = 1.0
max_p = 4.0
zz = np.zeros((size, size))
for i in range(size): # X
for j in range(size): # Y
for k in range(size): # Z
p = potential[k,j,i]
if min_p < p < max_p:
zz[j][i] = p # stop at the first element to meet the conditions
break # break to use the first value in range
Then to plot this surface:
surf = ax.plot_surface(xx, yy, zz, cmap=plt.cm.rainbow)
Note: include vmin and vmax keyword args to keep the same scale, I've left those out so the surface deviations are more visible. I also set the alpha on the cube to 0.2 to make it easier to see the surface.
I'm working on learning python more in depth than from my previous questions now that my internship is over and I hit an issue
I'm using a book "Doing Math With Python" by Amit Saha which what I decided to jump to was 'Animating a Projectile's Trajectory. I spent an hour trying to figure it out on my own then another 2 days on the internet and I still can't understand why I'm getting the error I'm getting
AttributeError: 'float' object has no attribute 'append'
if I don't have the float in the code then it doesn't work at all and I get this
TypeError: a float is required
I want to get this project done hopefully before we leave the projectile motion unit in my high school physics just as a cool thing I learned to do. please help. I can get it to draw the trajectory, just not animate it :(
from matplotlib import pyplot as plt
from matplotlib import animation
import math
g = 9.8
def get_intervals(u, theta):
t_flight = 2*u*math.sin(theta)/g
intervals = []
start = 0
intervals = 0.005
while start < t_flight:
intervals.append(start)
start = start + interval
return intervals
def update_position(i, circle, intervals, u, theta):
t = intervals[i]
x = u*math.cos(theta)*t
y = u*math.sin(theta)*t - 0.5*g*t*t
circle.center = x, y
return circle,
def create_animation(u, theta):
intervals = get_intervals(u,theta)
xmin = 0
xmax = u*math.cos(theta)*intervals[-1]
ymin = 0
t_max = u*math.sin(theta)/g
ymax = u*math.sin(theta)*t_max - 0.5*g*t_max**2
fig = plt.gcf()
ax = plt.axes(xlim=(xmin, xmax), ylim=(ymin, ymax))
circle = plt.Circle((xmin, ymin), 1.0)
ax.add_patch(circle)
anim = animation.FuncAnimation(fig, update_position,
fargs=(circle, intervals, u, theta),
frames=len(intervals), interval=1,
repeat=False)
plt.title('Projectile Motion')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()
if __name__ == '__main__':
try:
u = float(input('Enter the initial velocity (m/s): '))
theta = float(input('Enter the angle of projection (degrees): '))
except ValueError:
print('You Entered an invalid input')
else:
theta = (math.radians(theta))
create_animation(u, theta)
Your code is very close! Right now there is an error based on the variable intervals being defined twice and the variable interval never being defined. So change intervals = 0.005 to interval = 0.005 as in the following code:
def get_intervals(u, theta):
t_flight = 2*u*math.sin(theta)/g
intervals = []
start = 0
interval = 0.005
while start < t_flight:
intervals.append(start)
start = start + interval
return intervals
Now the code will run but the plot will look very different for various velocities and thetas. In fact for many initial conditions, you will only see a plot of blue. Let's look at the problems one by one:
The radius of the ball, rad, is 1 m. If the ball travels less than 1 m in the x-direction or y-direction, the the blue ball will dominate the screen.
The x-axis and y-axis have very different sizes and scales. This will make the ball an oblong oval rather than a circle.
I changed your create_animation() function to fix these small issues. Please read the comments I've placed to understand the subtle changes
def create_animation(u, theta):
intervals = get_intervals(u,theta)
xmin = 0
xmax = u*math.cos(theta)*intervals[-1]
ymin = 0
t_max = u*math.sin(theta)/g
ymax = u*math.sin(theta)*t_max - 0.5*g*t_max**2
plotmax = max(xmax, ymax) # Pick the largest dimension of the two
fig = plt.gcf()
# Set both maxima to the same value to make a square plot
ax = plt.axes(xlim=(xmin, plotmax), ylim=(ymin, plotmax))
# Make sure the two axes are scaled the same...
# (we want a circle.. not a messed up oval)
ax.set_aspect('equal')
rad = plotmax/20. # Make sure the circle doesn't dominate the plot
circle = plt.Circle((xmin, ymin), rad) # Use rad instead of 1.0
ax.add_patch(circle)
anim = animation.FuncAnimation(fig, update_position,
fargs=(circle, intervals, u, theta),
frames=len(intervals), interval=1,
repeat=False)
plt.title('Projectile Motion')
plt.xlabel('X [m]') # Units are nice :)
plt.ylabel('Y [m]')
plt.show()
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