Comment puis-je spécifier un linestyle de type flèche dans Matplotlib?

je voudrais afficher un ensemble de xy-données dans Matplotlib de manière à indiquer un chemin particulier. Idéalement, le linestyle serait modifié pour utiliser un patch de type flèche. J'ai créé une maquette, illustrée ci-dessous (en utilisant Omnigraphsketcher). Il semble que je devrais être en mesure d'outrepasser l'un des communs linestyle déclarations ('-','--', ':', etc) à cet effet.

notez que je ne veux pas simplement connecter chaque point de données avec une flèche unique- - - les données réelles les points ne sont pas uniformément espacés et j'ai besoin d'un espacement constant entre les flèches.

enter image description here

10
demandé sur Deaton 2011-11-23 23:28:44

4 réponses

Voici un point de départ:

  1. marchez le long de votre ligne à pas fixes (aspace dans mon exemple ci-dessous) .

    A. Il s'agit de prendre des mesures le long des segments de ligne créés par deux ensembles de points (x1,y1) et (x2,y2).

    B. Si votre Pas est plus long que le segment de ligne, passez à la série suivante de points.

  2. à ce point, déterminez l'angle de la ligne.

  3. Dessiner une flèche avec une inclinaison correspondant à l'angle.

j'ai écrit un petit script pour montrer ceci:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
axes = fig.add_subplot(111)

# my random data
scale = 10 
np.random.seed(101)
x = np.random.random(10)*scale
y = np.random.random(10)*scale

# spacing of arrows
aspace = .1 # good value for scale of 1
aspace *= scale

# r is the distance spanned between pairs of points
r = [0]
for i in range(1,len(x)):
    dx = x[i]-x[i-1]
    dy = y[i]-y[i-1]
    r.append(np.sqrt(dx*dx+dy*dy))
r = np.array(r)

# rtot is a cumulative sum of r, it's used to save time
rtot = []
for i in range(len(r)):
    rtot.append(r[0:i].sum())
rtot.append(r.sum())

arrowData = [] # will hold tuples of x,y,theta for each arrow
arrowPos = 0 # current point on walk along data
rcount = 1 
while arrowPos < r.sum():
    x1,x2 = x[rcount-1],x[rcount]
    y1,y2 = y[rcount-1],y[rcount]
    da = arrowPos-rtot[rcount] 
    theta = np.arctan2((x2-x1),(y2-y1))
    ax = np.sin(theta)*da+x1
    ay = np.cos(theta)*da+y1
    arrowData.append((ax,ay,theta))
    arrowPos+=aspace
    while arrowPos > rtot[rcount+1]: 
        rcount+=1
        if arrowPos > rtot[-1]:
            break

# could be done in above block if you want
for ax,ay,theta in arrowData:
    # use aspace as a guide for size and length of things
    # scaling factors were chosen by experimenting a bit
    axes.arrow(ax,ay,
               np.sin(theta)*aspace/10,np.cos(theta)*aspace/10, 
               head_width=aspace/8)


axes.plot(x,y)
axes.set_xlim(x.min()*.9,x.max()*1.1)
axes.set_ylim(y.min()*.9,y.max()*1.1)

plt.show()

cet exemple donne cette figure: enter image description here

Il y a beaucoup de place à l'amélioration, pour commencer:

  1. On peut utiliser FancyArrowPatch pour personnaliser l'apparence des flèches.
  2. on peut ajouter un test supplémentaire quand créer les flèches pour s'assurer qu'elles ne s'étendent pas au-delà de la ligne. Cela s'applique aux flèches créées au sommet ou près d'un sommet où la ligne change brusquement de direction. C'est le cas pour la plupart des points ci-dessus.
  3. on peut faire une méthode à partir de ce script qui fonctionnera dans un plus grand nombre de cas, c'est-à-dire la rendre plus portable.

en regardant cela, j'ai découvert le carquois méthode de traçage. Il pourrait être en mesure de remplacer le ci-dessus travail, mais il n'était pas immédiatement évident que cela était garanti.

7
répondu Yann 2011-11-24 07:55:21

Très belle réponse par Yann, mais en utilisant la flèche les flèches résultantes peuvent être affectées par le rapport d'aspect des axes et les limites. J'ai fait une version qui utilise des haches.annotate() à la place d'axes.flèche.)( Je l'inclus ici pour les autres à utiliser.

en bref ceci est utilisé pour tracer des flèches le long de vos lignes dans matplotlib. Le code est indiqué ci-dessous. Il peut encore être amélioré en ajoutant la possibilité d'avoir différentes pointes de flèche. Ici, j'ai seulement inclus le contrôle pour le largeur et longueur de la pointe de flèche.

import numpy as np
import matplotlib.pyplot as plt


def arrowplot(axes, x, y, narrs=30, dspace=0.5, direc='pos', \
                          hl=0.3, hw=6, c='black'): 
    ''' narrs  :  Number of arrows that will be drawn along the curve

        dspace :  Shift the position of the arrows along the curve.
                  Should be between 0. and 1.

        direc  :  can be 'pos' or 'neg' to select direction of the arrows

        hl     :  length of the arrow head 

        hw     :  width of the arrow head        

        c      :  color of the edge and face of the arrow head  
    '''

    # r is the distance spanned between pairs of points
    r = [0]
    for i in range(1,len(x)):
        dx = x[i]-x[i-1] 
        dy = y[i]-y[i-1] 
        r.append(np.sqrt(dx*dx+dy*dy))
    r = np.array(r)

    # rtot is a cumulative sum of r, it's used to save time
    rtot = []
    for i in range(len(r)):
        rtot.append(r[0:i].sum())
    rtot.append(r.sum())

    # based on narrs set the arrow spacing
    aspace = r.sum() / narrs

    if direc is 'neg':
        dspace = -1.*abs(dspace) 
    else:
        dspace = abs(dspace)

    arrowData = [] # will hold tuples of x,y,theta for each arrow
    arrowPos = aspace*(dspace) # current point on walk along data
                                 # could set arrowPos to 0 if you want
                                 # an arrow at the beginning of the curve

    ndrawn = 0
    rcount = 1 
    while arrowPos < r.sum() and ndrawn < narrs:
        x1,x2 = x[rcount-1],x[rcount]
        y1,y2 = y[rcount-1],y[rcount]
        da = arrowPos-rtot[rcount]
        theta = np.arctan2((x2-x1),(y2-y1))
        ax = np.sin(theta)*da+x1
        ay = np.cos(theta)*da+y1
        arrowData.append((ax,ay,theta))
        ndrawn += 1
        arrowPos+=aspace
        while arrowPos > rtot[rcount+1]: 
            rcount+=1
            if arrowPos > rtot[-1]:
                break

    # could be done in above block if you want
    for ax,ay,theta in arrowData:
        # use aspace as a guide for size and length of things
        # scaling factors were chosen by experimenting a bit

        dx0 = np.sin(theta)*hl/2. + ax
        dy0 = np.cos(theta)*hl/2. + ay
        dx1 = -1.*np.sin(theta)*hl/2. + ax
        dy1 = -1.*np.cos(theta)*hl/2. + ay

        if direc is 'neg' :
          ax0 = dx0 
          ay0 = dy0
          ax1 = dx1
          ay1 = dy1 
        else:
          ax0 = dx1 
          ay0 = dy1
          ax1 = dx0
          ay1 = dy0 

        axes.annotate('', xy=(ax0, ay0), xycoords='data',
                xytext=(ax1, ay1), textcoords='data',
                arrowprops=dict( headwidth=hw, frac=1., ec=c, fc=c))


    axes.plot(x,y, color = c)
    axes.set_xlim(x.min()*.9,x.max()*1.1)
    axes.set_ylim(y.min()*.9,y.max()*1.1)


if __name__ == '__main__':
    fig = plt.figure()
    axes = fig.add_subplot(111)

    # my random data
    scale = 10 
    np.random.seed(101)
    x = np.random.random(10)*scale
    y = np.random.random(10)*scale
    arrowplot(axes, x, y ) 

    plt.show()

Le résultat peut être vu ici:

enter image description here

6
répondu Pedro M Duarte 2017-05-23 12:17:23

version vectorisée de la réponse de Yann:

import numpy as np
import matplotlib.pyplot as plt

def distance(data):
    return np.sum((data[1:] - data[:-1]) ** 2, axis=1) ** .5

def draw_path(path):
    HEAD_WIDTH = 2
    HEAD_LEN = 3

    fig = plt.figure()
    axes = fig.add_subplot(111)

    x = path[:,0]
    y = path[:,1]
    axes.plot(x, y)

    theta = np.arctan2(y[1:] - y[:-1], x[1:] - x[:-1])
    dist = distance(path) - HEAD_LEN

    x = x[:-1]
    y = y[:-1]
    ax = x + dist * np.sin(theta)
    ay = y + dist * np.cos(theta)

    for x1, y1, x2, y2 in zip(x,y,ax-x,ay-y):
        axes.arrow(x1, y1, x2, y2, head_width=HEAD_WIDTH, head_length=HEAD_LEN)
    plt.show()
1
répondu user1769889 2016-07-23 01:27:00

Voici une version modifiée et simplifiée du code de Duarte. J'ai eu des problèmes quand j'ai lancé son code avec différents ensembles de données et rapports d'aspect, donc je l'ai nettoyé et utilisé FancyArrowPatches pour les flèches. Remarque l'exemple de la parcelle a une échelle de 1 000 000 de fois différent de x que de y.

j'ai aussi changé pour dessiner la flèche dans affichage coordonnées de façon à ce que les différences d'échelle sur les axes x et y ne changent pas la longueur des flèches.

en cours de route j'ai trouvé un bug dans le FancyArrowPatch de matplotlib qui explose en traçant une flèche purement verticale. J'ai trouvé une solution qui est dans mon code.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches


def arrowplot(axes, x, y, nArrs=30, mutateSize=10, color='gray', markerStyle='o'): 
    '''arrowplot : plots arrows along a path on a set of axes
        axes   :  the axes the path will be plotted on
        x      :  list of x coordinates of points defining path
        y      :  list of y coordinates of points defining path
        nArrs  :  Number of arrows that will be drawn along the path
        mutateSize :  Size parameter for arrows
        color  :  color of the edge and face of the arrow head
        markerStyle : Symbol

        Bugs: If a path is straight vertical, the matplotlab FanceArrowPatch bombs out.
          My kludge is to test for a vertical path, and perturb the second x value
          by 0.1 pixel. The original x & y arrays are not changed

        MHuster 2016, based on code by 
    '''
    # recast the data into numpy arrays
    x = np.array(x, dtype='f')
    y = np.array(y, dtype='f')
    nPts = len(x)

    # Plot the points first to set up the display coordinates
    axes.plot(x,y, markerStyle, ms=5, color=color)

    # get inverse coord transform
    inv = ax.transData.inverted()

    # transform x & y into display coordinates
    # Variable with a 'D' at the end are in display coordinates
    xyDisp = np.array(axes.transData.transform(zip(x,y)))
    xD = xyDisp[:,0]
    yD = xyDisp[:,1]

    # drD is the distance spanned between pairs of points
    # in display coordinates
    dxD = xD[1:] - xD[:-1]
    dyD = yD[1:] - yD[:-1]
    drD = np.sqrt(dxD**2 + dyD**2)

    # Compensating for matplotlib bug
    dxD[np.where(dxD==0.0)] = 0.1


    # rtotS is the total path length
    rtotD = np.sum(drD)

    # based on nArrs, set the nominal arrow spacing
    arrSpaceD = rtotD / nArrs

    # Loop over the path segments
    iSeg = 0
    while iSeg < nPts - 1:
        # Figure out how many arrows in this segment.
        # Plot at least one.
        nArrSeg = max(1, int(drD[iSeg] / arrSpaceD + 0.5))
        xArr = (dxD[iSeg]) / nArrSeg # x size of each arrow
        segSlope = dyD[iSeg] / dxD[iSeg]
        # Get display coordinates of first arrow in segment
        xBeg = xD[iSeg]
        xEnd = xBeg + xArr
        yBeg = yD[iSeg]
        yEnd = yBeg + segSlope * xArr
        # Now loop over the arrows in this segment
        for iArr in range(nArrSeg):
            # Transform the oints back to data coordinates
            xyData = inv.transform(((xBeg, yBeg),(xEnd,yEnd)))
            # Use a patch to draw the arrow
            # I draw the arrows with an alpha of 0.5
            p = patches.FancyArrowPatch( 
                xyData[0], xyData[1], 
                arrowstyle='simple',
                mutation_scale=mutateSize,
                color=color, alpha=0.5)
            axes.add_patch(p)
            # Increment to the next arrow
            xBeg = xEnd
            xEnd += xArr
            yBeg = yEnd
            yEnd += segSlope * xArr
        # Increment segment number
        iSeg += 1

if __name__ == '__main__':
    import numpy as np
    import matplotlib.pyplot as plt
    fig = plt.figure()
    ax = fig.add_subplot(111)
    # my random data
    xScale = 1e6
    np.random.seed(1)
    x = np.random.random(10) * xScale
    y = np.random.random(10)
    arrowplot(ax, x, y, nArrs=4*(len(x)-1), mutateSize=10, color='red')
    xRng = max(x) - min(x)
    ax.set_xlim(min(x) - 0.05*xRng, max(x) + 0.05*xRng)
    yRng = max(y) - min(y)
    ax.set_ylim(min(y) - 0.05*yRng, max(y) + 0.05*yRng)
    plt.show()

enter image description here

0
répondu Prof Huster 2016-10-02 09:58:27