Show direction arrows in a scatterplot

Viewed 7294

I am plotting data in a scatterplot an I would like to see the direction of the hysteresis. Does anyone have a good idea how to implement arrows on each line that point into the direction of the next point?

Alternatively, the markers could be replaced by arrows pointing in the direction of the next point.

What I am looking for:

enter image description here

Code to obtain the plot (without arrows):

df = pd.DataFrame.from_dict({'x' : [0,3,8,7,5,3,2,1],
                             'y' : [0,1,3,5,9,8,7,5]})
x = df['x']
y = df['y']
fig, ax = plt.subplots()
ax.scatter(x,y)
ax.plot(x,y)
4 Answers

As commented, one can use plt.quiver to produce arrows along a line, e.g. like

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame.from_dict({'x' : [0,3,8,7,5,3,2,1],
                             'y' : [0,1,3,5,9,8,7,5]})
x = df['x'].values
y = df['y'].values

u = np.diff(x)
v = np.diff(y)
pos_x = x[:-1] + u/2
pos_y = y[:-1] + v/2
norm = np.sqrt(u**2+v**2) 

fig, ax = plt.subplots()
ax.plot(x,y, marker="o")
ax.quiver(pos_x, pos_y, u/norm, v/norm, angles="xy", zorder=5, pivot="mid")
plt.show()

enter image description here

Thanks for the useful hints! Here is my solution:

df = pd.DataFrame.from_dict({'x' : [0,3,8,7,5,3,2,1],
                             'y' : [0,1,3,5,9,8,7,5]})
x = df['x']
y = df['y']
# calculate position and direction vectors:
x0 = x.iloc[range(len(x)-1)].values
x1 = x.iloc[range(1,len(x))].values
y0 = y.iloc[range(len(y)-1)].values
y1 = y.iloc[range(1,len(y))].values
xpos = (x0+x1)/2
ypos = (y0+y1)/2
xdir = x1-x0
ydir = y1-y0
fig, ax = plt.subplots()
ax.scatter(x,y)
ax.plot(x,y)
# plot arrow on each line:
for X,Y,dX,dY in zip(xpos, ypos, xdir, ydir):
    ax.annotate("", xytext=(X,Y),xy=(X+0.001*dX,Y+0.001*dY), 
    arrowprops=dict(arrowstyle="->", color='k'), size = 20)

which gives this:

enter image description here

plt.quiver does not help in this case, as it creates a field of arrows. plt.arrow scales with the axis, so weird looking arrows if x and y units are not the same order of magnitude. Thus, ax.annotate was my choice. The arguments xytext & xy indicate beginning and end of the arrow respectively.

Thanks to @NicoH. this code is great. I used it to geoplot a TSP solver.

nyc_map_zoom = plt.imread('https://aiblog.nl/download/nyc_-74.3_-73.7_40.5_40.9.png')

def plot_on_map(df, BB, nyc_map, s=40, alpha=0.2):
    x = df['Longitude']
    y = df['Latitude']
    # calculate position and direction vectors:
    x0 = x.iloc[range(len(x)-1)].values
    x1 = x.iloc[range(1,len(x))].values
    y0 = y.iloc[range(len(y)-1)].values
    y1 = y.iloc[range(1,len(y))].values
    xpos = (x0+x1)/2
    ypos = (y0+y1)/2
    xdir = x1-x0
    ydir = y1-y0
    # plot map
    fig, ax = plt.subplots(figsize=(20,20))
    ax.scatter(x,y, marker='H',c='fuchsia',s=80,label=df["Name"])
    ax.set_xlim((BB[0], BB[1]))
    ax.set_ylim((BB[2], BB[3]))
    ax.set_title('Pizza Locations and Route Directions', fontsize=15)
    ax.imshow(nyc_map, zorder=0, extent=BB)
    ax.plot(x,y,linewidth=3)
    plt.legend(title='Pizza Joints', facecolor='white', framealpha=1,fontsize=15,title_fontsize=18, fancybox=True,edgecolor = 'k')
    # plot arrow on each line:
    for X,Y,dX,dY in zip(xpos, ypos, xdir, ydir):
        ax.annotate("", xytext=(X,Y),xy=(X+0.001*dX,Y+0.001*dY), 
        arrowprops=dict(arrowstyle="->", linewidth=3,color='k'), size = 40)
    plt.savefig('Pizza_Route.png',bbox_inches='tight');



BB_zoom = (-74.3, -73.7, 40.5, 40.9)
plot_on_map(TSP, BB_zoom, nyc_map_loc, s=20, alpha=0.3)

enter image description here

You could just use arrow annotations to represent the actual line segments like this:

df = pd.DataFrame.from_dict({'x' : [0,3,8,7,5,3,2,1],
                             'y' : [0,1,3,5,9,8,7,5]})

for i,row in df.iterrows():
    if i==0:
        pass
    else:
        plt.annotate('',xy=(row['x'],row['y']),xytext=(df.iloc[i-1]['x'],df.iloc[i-1]['y']),
        arrowprops=dict(facecolor='black',width=1,headwidth=5))

plt.xlim(0 ,10)
plt.ylim(0,10)

output

can play with the colors/widths to make it prettier

Related