2

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)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
NicoH
  • 1,240
  • 3
  • 12
  • 23
  • 2
    If you have two points `(x0,y0)` and `(x1,y1)`, the middle between those is `((x0+y0)/2, (y0+y1)/2)`. This would be the position of the arrows. The angle is arctan(x1-x0, y1-y0)` (use numpy.arctan2 for that). Arrows can be plotted with `plt.quiver`. – ImportanceOfBeingErnest Oct 11 '19 at 13:41
  • also this: https://stackoverflow.com/questions/34017866/arrow-on-a-line-plot-with-matplotlib though all these solutions put an arrow at a vertex rather than in the middle of a line. – Aaron Oct 11 '19 at 13:53

4 Answers4

12

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

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
3

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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
NicoH
  • 1,240
  • 3
  • 12
  • 23
2

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

SirRacha
  • 33
  • 5
1

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

Derek Eden
  • 4,403
  • 3
  • 18
  • 31