Inline-Beschriftungen in Matplotlib


98

In Matplotlib ist es nicht allzu schwierig, eine Legende zu erstellen (siehe example_legend()unten), aber ich denke, es ist besser, Beschriftungen direkt auf die gezeichneten Kurven zu setzen (wie example_inline()unten). Dies kann sehr umständlich sein, da ich die Koordinaten von Hand angeben muss und wenn ich das Diagramm neu formatiere, muss ich wahrscheinlich die Beschriftungen neu positionieren. Gibt es eine Möglichkeit, Beschriftungen in Kurven in Matplotlib automatisch zu generieren? Bonuspunkte für die Ausrichtung des Textes in einem Winkel, der dem Winkel der Kurve entspricht.

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()

Figur mit Legende

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')

Abbildung mit Inline-Beschriftungen

Antworten:


28

Schöne Frage, vor einiger Zeit habe ich ein bisschen damit experimentiert, aber nicht viel benutzt, weil es immer noch nicht kugelsicher ist. Ich habe den Plotbereich in ein 32x32-Raster unterteilt und ein "potenzielles Feld" für die beste Position eines Etiketts für jede Linie nach den folgenden Regeln berechnet:

  • Leerraum ist ein guter Ort für ein Etikett
  • Das Etikett sollte sich in der Nähe der entsprechenden Linie befinden
  • Das Etikett sollte von den anderen Zeilen entfernt sein

Der Code war ungefähr so:

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()

Und die daraus resultierende Handlung: Geben Sie hier die Bildbeschreibung ein


Sehr schön. Ich habe jedoch ein Beispiel, das nicht vollständig funktioniert: plt.plot(x2, 3*x2**2, label="3x*x"); plt.plot(x2, 2*x2**2, label="2x*x"); plt.plot(x2, 0.5*x2**2, label="0.5x*x"); plt.plot(x2, -1*x2**2, label="-x*x"); plt.plot(x2, -2.5*x2**2, label="-2.5*x*x"); my_legend();Dadurch wird eine der Beschriftungen in der oberen linken Ecke platziert. Irgendwelche Ideen, wie man das behebt? Das Problem scheint zu sein, dass die Linien zu nahe beieinander liegen.
egpbos

Entschuldigung, vergessen x2 = np.linspace(0,0.5,100).
egpbos

Gibt es eine Möglichkeit, dies ohne scipy zu verwenden? Auf meinem aktuellen System ist die Installation schwierig.
AnnanFay

Dies funktioniert bei mir unter Python 3.6.4, Matplotlib 2.1.2 und Scipy 1.0.0 nicht. Nach dem Aktualisieren des printBefehls wird er ausgeführt und erstellt 4 Diagramme, von denen 3 pixeliger Kauderwelsch zu sein scheinen (wahrscheinlich etwas mit dem 32x32 zu tun), und der vierte mit Beschriftungen an ungeraden Stellen.
Y Davis

82

Update: Benutzer cphyc hat freundlicherweise ein Github-Repository für den Code in dieser Antwort erstellt (siehe hier ) und den Code in einem Paket gebündelt, das möglicherweise mit installiert wird pip install matplotlib-label-lines.


Schönes Bild:

halbautomatische Plotkennzeichnung

In matplotlibes ist ziemlich leicht zu Etikett Kontur - Plots (entweder automatisch oder manuell durch Etiketten mit Mausklicks Platzierung). Es scheint (noch) keine äquivalente Fähigkeit zu geben, Datenreihen auf diese Weise zu kennzeichnen! Es kann einen semantischen Grund dafür geben, diese Funktion, die mir fehlt, nicht aufzunehmen.

Unabhängig davon habe ich das folgende Modul geschrieben, das halbautomatische Plotbeschriftungen zulässt. Es werden nur numpyeinige Funktionen aus der Standardbibliothek benötigt math.

Beschreibung

Das Standardverhalten der labelLinesFunktion besteht darin, die Beschriftungen gleichmäßig entlang der xAchse zu platzieren ( ynatürlich automatisch mit dem richtigen Wert). Wenn Sie möchten, können Sie einfach ein Array der x-Koordinaten der einzelnen Beschriftungen übergeben. Sie können sogar die Position eines Etiketts anpassen (siehe Abbildung unten rechts) und den Rest gleichmäßig verteilen, wenn Sie möchten.

Darüber hinaus label_linesberücksichtigt die Funktion nicht die Zeilen, denen im plotBefehl keine Beschriftung zugewiesen wurde (oder genauer, wenn die Beschriftung enthält '_line').

Schlüsselwortargumente, die an den Funktionsaufruf übergeben labelLinesoder an diesen labelLineweitergegeben werden text(einige Schlüsselwortargumente werden festgelegt, wenn der aufrufende Code keine Angabe macht).

Probleme

  • Anmerkungsbegrenzungsrahmen stören manchmal unerwünschte andere Kurven. Wie durch die 1und 10Anmerkungen im oberen linken Diagramm gezeigt. Ich bin mir nicht mal sicher, ob dies vermieden werden kann.
  • Es wäre schön, ystattdessen manchmal eine Position anzugeben .
  • Es ist immer noch ein iterativer Prozess, um Anmerkungen an der richtigen Stelle abzurufen
  • Es funktioniert nur, wenn die x-axis-Werte floats sind

Fallstricke

  • Standardmäßig geht die labelLinesFunktion davon aus, dass alle Datenreihen den durch die Achsengrenzen angegebenen Bereich umfassen. Schauen Sie sich die blaue Kurve oben links im hübschen Bild an. Wenn nur Daten zur Verfügung standen für den xBereich 0.5- 1dann könnten wir nicht möglicherweise eine Markierung setzen an der gewünschten Stelle (was etwas weniger als ist 0.2). In dieser Frage finden Sie ein besonders unangenehmes Beispiel. Derzeit identifiziert der Code dieses Szenario nicht intelligent und ordnet die Beschriftungen neu an. Es gibt jedoch eine angemessene Problemumgehung. Die Funktion labelLines übernimmt das xvalsArgument. Eine Liste der xvom Benutzer angegebenen Werte anstelle der standardmäßigen linearen Verteilung über die Breite. So kann der Benutzer entscheiden, welchex-Werte, die für die Etikettenplatzierung jeder Datenreihe verwendet werden sollen.

Ich glaube auch, dass dies die erste Antwort ist, um das Bonusziel zu erreichen, die Beschriftungen an der Kurve auszurichten, auf der sie sich befinden. :) :)

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)

Testcode, um das hübsche Bild oben zu erzeugen:

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()

1
@blujay Ich bin froh, dass Sie es an Ihre Bedürfnisse anpassen konnten. Ich werde diese Einschränkung als Problem hinzufügen.
NauticalMile

1
@Liza Lies meine Gotcha Ich habe gerade hinzugefügt, warum dies geschieht. Für Ihren Fall (ich gehe davon aus, dass es wie in dieser Frage ist ) möchten xvalsSie den labelLinesCode möglicherweise ein wenig ändern, wenn Sie nicht manuell eine Liste erstellen möchten : Ändern Sie den Code im if xvals is None:Bereich, um eine Liste basierend auf anderen Kriterien zu erstellen. Sie könnten beginnen mitxvals = [(np.min(l.get_xdata())+np.max(l.get_xdata()))/2 for l in lines]
NauticalMile

1
@Liza Dein Graph fasziniert mich allerdings. Das Problem ist, dass Ihre Daten nicht gleichmäßig über das Diagramm verteilt sind und Sie viele Kurven haben, die fast übereinander liegen. Bei meiner Lösung kann es in vielen Fällen sehr schwierig sein, Etiketten voneinander zu unterscheiden. Ich denke, die beste Lösung besteht darin, Blöcke mit gestapelten Etiketten in verschiedenen leeren Teilen Ihres Grundstücks zu haben. In diesem Diagramm finden Sie ein Beispiel mit zwei Blöcken gestapelter Etiketten (ein Block mit 1 Etikett und ein weiterer Block mit 4). Dies umzusetzen wäre ein gutes Stück Arbeit, ich könnte es irgendwann in der Zukunft tun.
NauticalMile

1
Hinweis: seit Matplotlib 2.0 .get_axes()und .get_axis_bgcolor()sind veraltet. Bitte ersetzen Sie durch .axesund .get_facecolor()bzw.
Jiāgěng

1
Eine weitere großartige Sache labellinesist, dass Eigenschaften damit zusammenhängen plt.textoder ax.textgelten. Das heißt, Sie können fontsizeund bboxParameter in der labelLines()Funktion einstellen .
tionichm

52

@ Jan Kuikens Antwort ist sicherlich gut durchdacht und gründlich, aber es gibt einige Einschränkungen:

  • es funktioniert nicht in allen Fällen
  • Es erfordert eine ganze Menge zusätzlichen Codes
  • Sie kann von Grundstück zu Grundstück erheblich variieren

Ein viel einfacherer Ansatz besteht darin, den letzten Punkt jedes Diagramms mit Anmerkungen zu versehen. Der Punkt kann zur Hervorhebung auch eingekreist werden. Dies kann mit einer zusätzlichen Zeile erreicht werden:

from matplotlib import pyplot as plt

for i, (x, y) in enumerate(samples):
    plt.plot(x, y)
    plt.text(x[-1], y[-1], 'sample {i}'.format(i=i))

Eine Variante wäre zu verwenden ax.annotate.


1
+1! Es sieht nach einer schönen und einfachen Lösung aus. Entschuldigung für die Faulheit, aber wie würde das aussehen? Befindet sich der Text innerhalb des Diagramms oder über der rechten y-Achse?
Rocarvaj

1
@rocarvaj Es hängt von anderen Einstellungen ab. Es ist möglich, dass die Beschriftungen außerhalb des Plotfelds hervorstehen. Zwei Möglichkeiten, um dieses Verhalten zu vermeiden, sind: 1) Verwenden Sie einen anderen Index als -1. 2) Legen Sie geeignete Achsengrenzen fest, um Platz für die Beschriftungen zu schaffen.
Ioannis Filippidis

1
Es wird auch zu einem Chaos, wenn sich die Diagramme auf einen bestimmten Wert konzentrieren - die Endpunkte werden zu nahe, als dass der Text gut aussehen könnte
LazyCat

@ LazyCat: Das stimmt. Um dies zu beheben, können die Anmerkungen ziehbar gemacht werden. Ein bisschen schmerzhaft, denke ich, aber es würde den Trick machen.
PlacidLush

1

Ein einfacherer Ansatz wie der von Ioannis Filippidis:

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 auf sageCell

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.