Plot arrow on each point towards the line in graph

96 Views Asked by At

I am trying to plot arrows from each data point towards the line in the graph using matplotlib.

Graph

I want the arrow to represent the distance between each point and the line. How can I do this?

Here's my code:

import matplotlib.pyplot as plt
import numpy as np

# Create a straight line (45-degree angle)
x_line = np.linspace(0, 10, 100)
y_line = x_line

# Add some random points around the line
num_points = 20
x_points = np.linspace(2, 8, num_points)  # Adjust the range as needed
y_points = x_points + np.random.normal(0, 0.5, num_points)  # Add some randomness

# Plot the line
plt.plot(x_line, y_line, label='Line', color='blue')

# Plot the points
plt.scatter(x_points, y_points, label='Points', color='red')

# Set labels and title
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.title('Scatter Plot Around a Line')

# Show legend
plt.legend()

# Display the plot
plt.show()

I tried doing this myself but failed: enter image description here

Code:

import matplotlib.pyplot as plt
import numpy as np

# Create a straight line (45-degree angle)
x_line = np.linspace(0, 10, 100)
y_line = x_line

# Add some random points around the line
num_points = 20
x_points = np.linspace(2, 8, num_points)  # Adjust the range as needed
y_points = x_points + np.random.normal(0, 0.5, num_points)  # Add some randomness

# Plot the line
plt.plot(x_line, y_line, label='Line', color='blue')

# Plot the points
plt.scatter(x_points, y_points, label='Points', color='red')

# Add arrows from each point to the line
for x, y in zip(x_points, y_points):
    plt.arrow(x, y, 0, y - x, color='black', linestyle='dashed', linewidth=0.5, head_width=0.2)

# Set labels and title
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.title('Scatter Plot Around a Line')

# Show legend
plt.legend()

# Display the plot
plt.show()

As you can see the data points shifted and the arrows point outwards rather than inwards or towards the line.

3

There are 3 best solutions below

9
root_coding On BEST ANSWER

You could edit this line so that it works in the reverse direction by replacing y - x with x - y:

# Add arrows from each point to the line
for x, y in zip(x_points, y_points):
    plt.arrow(x, y, 0, y- x, color='black', linestyle='dashed', linewidth=0.5, head_width=0.2)

To:

# Add arrows from each point to the line
for x, y in zip(x_points, y_points):
    plt.arrow(x, y, 0, x - y, color='black', linestyle='dashed', linewidth=0.5, head_width=0.2)

or you may use this code it might solve your issue.

import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)

# Create a straight line (45-degree angle)
x_line = np.linspace(0, 10, 100)
y_line = x_line

# Add some random points around the line
num_points = 20
x_points = np.linspace(2, 8, num_points)  # Adjust the range as needed
y_points = x_points + np.random.normal(0, 0.5, num_points)  # Add some randomness

# Plot the line
plt.plot(x_line, y_line, label='Line', color='blue')

# Plot the points
plt.scatter(x_points, y_points, label='Points', color='red')

# Add outward arrows from each point away from the line
for x, y in zip(x_points, y_points):
    plt.arrow(x, y, 0, x - y, color='black', linestyle='dashed', linewidth=0.5, head_width=0.2)

# Set labels and title
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.title('Scatter Plot Around a Line')

# Show legend
plt.legend()

# Display the plot
plt.show()

0
jared On

In the comments, you said you want the orthogonal distance to the line (though I'm not sure why you would want that, since typical distance metrics use the vertical distance). You can take advantage of the fact that the line has a slope of +1 since that means the perpendicular lines (which the arrows will point along) should have a slope of -1. The first line has equation y = x and the lines along which the arrows will lie are given by y = -(x - xp) + yp where xp and yp are the x and y coordinates of the point, respectively. We can find where these lines intersect by setting y = x in the second equation. Solving for x gives p = 0.5*(xp + yp). Since y = x, the coordinate of the point on the line orthogonal to the point off the line is (p, p).

Since matplotlib wants the dx and dy, that will just be p - x and p - y. You probably don't want the arrow to extend past the line, so we should set length_includes_head=True.

for x, y in zip(x_points, y_points):
    p = 0.5*(x + y)
    plt.arrow(x, y, p - x, p - y,
              color='black',
              linestyle='dashed',
              linewidth=0.5,
              head_width=0.2,
              length_includes_head=True)

Also, you commented on another answer that the points weren't the same, but that is a consequence of using a random number generator. To make it repeatable, let's use the more modern numpy RandomGenerator and set the seed. (I also increased the standard deviation of the random numbers so they weren't all too close to the line.)

rng = np.random.default_rng(42)
y_points = x_points + rng.normal(0, 1, num_points)

Lastly, because the default plot is not square, the arrows won't look orthogonal, so we can set the aspect ratio to be square using the following line:

plt.gca().set_aspect(1)

Here is everything together:

import matplotlib.pyplot as plt
import numpy as np

plt.close("all")

x_line = np.linspace(0, 10, 100)
y_line = x_line

num_points = 20
x_points = np.linspace(2, 8, num_points)
rng = np.random.default_rng(42)
y_points = x_points + rng.normal(0, 1, num_points)

plt.plot(x_line, y_line, label="Line", color="blue")
plt.scatter(x_points, y_points, label="Points", color="red")

for x, y in zip(x_points, y_points):
    p = 0.5*(x + y)  # coordinate of the orthogonal point
    plt.arrow(x, y, p - x, p - y,
              color="black",
              linestyle="dashed",
              linewidth=0.5,
              head_width=0.2,
              length_includes_head=True)


plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Scatter Plot Around a Line")
plt.legend()
plt.gca().set_aspect(1)

plt.show()

Plot result:

0
Daraan On

I am gonna calculate the orthogonal and anchor points explicitly through orthogonal projection so it works for all kinds of slopes.


import matplotlib.pyplot as plt
import numpy as np
np.random.seed(1)

# Create a straight line (45-degree angle)
x_line = np.linspace(0, 10, 100)
y_line = x_line


# Add some random points around the line
num_points = 20
x_points = np.linspace(2, 8, num_points)  # Adjust the range as needed
y_points = x_points + np.random.normal(0, 0.8, num_points)  # Add some randomness

XY = np.stack((x_points, y_points), axis=1) # for easier numpy stack them in one array.

# Direction vector of the line
dir_vec = np.array([x_line[-1] - x_line[0], y_line[-1] - y_line[0]])
dir_vec = dir_vec / np.sqrt(np.sum(dir_vec**2)) # norm

# Apply some geometry, i.e. project the points to the line
scalarProduct = XY @ dir_vec

# calculate the anchor points on the line
pointsOnLine  = np.outer(scalarProduct, dir_vec) # outer product to all points

# Difference between anchors and given points
vecToPoints = XY - pointsOnLine 

#-----------

# Plot the line
plt.plot(x_line, y_line, label='Line', color='blue')

# Plot the points
plt.scatter(x_points, y_points, label='Points', color='red')

# Now the arrows; also include the head length
for (x, y), (dx, dy) in zip(pointsOnLine, vecToPoints):
    plt.arrow(x, y, dx, dy, color='black', linestyle='dashed', linewidth=0.5, head_width=0.2, length_includes_head=True)


# Set labels and title
plt.gca().set_aspect('equal')
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.title('Scatter Plot Around a Line')

# Show legend
plt.legend()

# Display the plot
plt.show()

enter image description here