it-swarm.com.de

Inline-Labels in Matplotlib

In Matplotlib ist es nicht allzu schwierig, eine Legende zu erstellen (example_legend(), siehe unten), aber ich denke, es ist besser, Beschriftungen direkt auf den Kurven zu platzieren, die gezeichnet werden (wie in example_inline(), siehe unten) ). Dies kann sehr umständlich sein, da ich die Koordinaten von Hand angeben und die Beschriftungen wahrscheinlich neu positionieren muss, wenn ich die Zeichnung neu formatiere. Gibt es eine Möglichkeit, Beschriftungen auf Kurven in Matplotlib automatisch zu generieren? Bonuspunkte, um den Text in einem Winkel ausrichten zu können, 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()

Figure with legend

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

Figure with inline labels

77
Alex Szatmary

Schöne Frage, vor einiger Zeit habe ich ein bisschen damit experimentiert, es aber nicht oft benutzt, weil es immer noch nicht kugelsicher ist. Ich habe die Plotfläche in ein 32x32-Raster unterteilt und für jede Zeile ein 'potentielles Feld' für die beste Position einer Beschriftung nach folgenden Regeln berechnet:

  • leerraum ist ein guter Ort für ein Etikett
  • Die Beschriftung sollte sich in der Nähe der entsprechenden Linie befinden
  • Die Beschriftung 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 resultierende Handlung: enter image description here

25
Jan Kuiken

Update: Benutzer cphyc hat freundlicherweise ein Github-Repository für den Code in dieser Antwort erstellt (siehe hier =) und bündelte den Code in einem Paket, das mit pip install matplotlib-label-lines installiert werden kann.


Schönes Bild:

semi-automatic plot-labeling

In matplotlib ist es ziemlich einfach Beschriftungskonturdiagramme (entweder automatisch oder durch manuelles Platzieren von Beschriftungen mit Mausklicks). Es scheint (noch) keine vergleichbare Möglichkeit zu geben, Datenreihen auf diese Weise zu kennzeichnen! Es kann einen semantischen Grund dafür geben, dieses Feature nicht aufzunehmen, den ich vermisse.

Unabhängig davon habe ich das folgende Modul geschrieben, das eine halbautomatische Plotbeschriftung zulässt. Es werden nur numpy und einige Funktionen aus der Standardbibliothek math benötigt.

Beschreibung

Das Standardverhalten der Funktion labelLines besteht darin, die Beschriftungen gleichmäßig entlang der Achse x zu verteilen (wobei natürlich automatisch der richtige Wert für y angegeben wird). 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 optimieren (siehe Grafik unten rechts) und den Rest gleichmäßig verteilen, wenn Sie möchten.

Außerdem berücksichtigt die Funktion label_lines Nicht die Zeilen, denen im Befehl plot keine Beschriftung zugewiesen wurde (oder genauer gesagt, wenn die Beschriftung '_line' Enthält).

An labelLines oder labelLine übergebene Schlüsselwortargumente werden an den Funktionsaufruf text übergeben (einige Schlüsselwortargumente werden festgelegt, wenn der aufrufende Code keine Angabe macht).

Probleme

  • Begrenzungsrahmen für Anmerkungen stören manchmal andere Kurven in unerwünschter Weise. Wie in den Anmerkungen 1 Und 10 Oben links dargestellt. Ich bin mir nicht mal sicher, ob das vermieden werden kann.
  • Es wäre schön, stattdessen manchmal eine y -Position anzugeben.
  • Es ist immer noch ein iterativer Prozess, Anmerkungen an der richtigen Stelle abzurufen
  • Dies funktioniert nur, wenn die Werte der x- Achse floats sind

Fallstricke

  • Standardmäßig geht die Funktion labelLines davon aus, dass sich alle Datenreihen über den durch die Achsengrenzen festgelegten Bereich erstrecken. Schauen Sie sich die blaue Kurve im oberen linken Diagramm des hübschen Bildes an. Wenn es nur Daten für den Bereich x0.5 - 1 Gäbe, dann könnten wir möglicherweise keine Beschriftung an der gewünschten Stelle platzieren (was etwas weniger als 0.2). Siehe diese Frage für ein besonders unangenehmes Beispiel. Im Moment identifiziert der Code dieses Szenario nicht intelligent und ordnet die Beschriftungen neu an, es gibt jedoch eine angemessene Problemumgehung. Die Funktion labelLines verwendet das Argument xvals. Eine Liste der vom Benutzer angegebenen x- Werte anstelle der linearen Standardverteilung über die Breite. Auf diese Weise kann der Benutzer entscheiden, welche x - Werte für die Beschriftung der einzelnen Datenreihen verwendet werden sollen.

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

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 generieren:

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

Die Antwort von @Jan Kuiken ist sicherlich gut durchdacht und gründlich, aber es gibt einige Vorbehalte:

  • es funktioniert nicht in allen Fällen
  • es erfordert eine ganze Menge zusätzlichen Codes
  • es kann von einer Parzelle zur nächsten 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 ax.annotate .

41