Buttons mit Matplotlib

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Hallo zusammen,
ich möchte Buttons mit Matplotlib bauen nach diesem Vorbild: https://matplotlib.org/stable/gallery/a ... dulum.html, dabei hab ich mit dem Button "nächstes Bild" angefangen.

Als Beispiel wird das Newton-Verfahren zur Bestimmung von Nullstellen benutzt, die entsprechenden Daten wurden von einem anderen Skript bereits berechnet und sind unter "data" in der main-function eingefügt.
Der Code sieht wahrscheinlich nicht schön aus, funktioniert aber bisher ganz ok.
Der Knackpunkt ist jetzt, dass man eigentlich in der Funktion "next()" den Zähler i um 1 erhöhen sollte, damit durch Drücken des "Next-Buttons" immer eine neue Gerade gezeichnet wird, was bisher für die erste Gerade klappt. Gibt es da einen Weg, obwohl i nicht innerhalb von next() definiert wurde?

Alternativ müsste man eine Abfrage machen, á la "if Button pressed: i += 1" in der Funktion plot_Newton, aber ich hab noch nichts dazu gefunden, ob das mit matplotlib geht...
Es gibt zwar eine Methode get_active(), aber die gibt nur an, ob der Knopf überhaupt gedrückt werden kann oder nicht.

Hier der Code:

Code: Alles auswählen

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Button

def f(x):
    # Funktion, deren Nullstellen bestimmt werden soll (f(x) = 0).
    return x**2 - 3*x - 5

def df(x):
    # df/dx
    return 2*x - 3

def gerade(x0, fx0, dfx0, x):
    return dfx0*(x - x0) + fx0

def plot_Newton(data, i=0):
    # Vorbereitungen; bleiben in jedem Plot gleich
    data = np.array(data)
    xvals = data[:, 0]
    xmin, xmax = np.min(xvals), np.max(xvals)
    xrange = xmax - xmin
    x = np.linspace(xmin - .1*xrange, xmax + .1*xrange, 100)
    f_x = f(x)
    fmin, fmax = np.min(f_x), np.max(f_x)
    frange = fmax - fmin
    fig, ax = plt.subplots()
    ax.axis([x[0], x[-1], fmin - .1*frange, fmax + .1*frange])
    ax.plot(x, f_x, 'k--', label='f(x) = $x^2$ - 5x + 3')

    # Zeichnen der Gerade, ändert sich für jeden Tupel in data, angegeben mit i
    x0, fx0 = data[i, 0], data[i, 1]
    dfx0 = df(x0)
    y = gerade(x0, fx0, dfx0, x)
    line, = ax.plot(x, y, 'r-', label=f"y = f'($x_0)\cdot x$ + b")
    axnext = fig.add_axes([.05, .05, .1, .1])
    def next(event):
        x0, fx0 = data[i+1, 0], data[i+1, 1]
        dfx0 = df(x0)
        y = gerade(x0, fx0, dfx0, x)
        line.set_ydata(y)
        # hier soll die Variable i um 1 erhöht werden
        plt.draw()

    button_next = Button(axnext, 'Next')
    button_next.on_clicked(next)
    plt.show()

def main():
    data = [(2, -7), (9.0, 49.0), (5.733333333333333, 10.671111111111113), (4.472965879265091, 1.5885261192744569),
     (4.205804030830568, 0.07137545325894834), (4.1926147066138695, 0.0001739582732938061), (4.192582403761021, 1.0434764163846921e-09)]
    plot_Newton(data, 0)

if __name__ == '__main__':
    main()
Viele Grüße
tonikae
User
Beiträge: 90
Registriert: Sonntag 23. Februar 2020, 10:27

Buttons & Co läuft unter der Kategorie: "Widgets"...und dazu gibt es in
der offiziellen Doku auch genug Beispiele.

https://matplotlib.org/stable/gallery/i ... ml#widgets

Der Rest ist ein schlichtes RTFM.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@tonikae: das ist doch gar nicht das Problem des OPs. Der Button funktioniert ja wunderbar.

@mm96: statt Code zu wiederholen, benutzt man Funktionen. Wie bei anderen GUIs auch, braucht man eine Methode, um sich den Zustand zwischen den Button-Aufrufen zu merken. Funktionen verschachtelt man nicht in andere Funktionen, alles was eine Funktion braucht, muß sie über ihre Argumente bekommen.

In Deinem Fall ist das noch recht einfach durch einen Iterator lösbar:

Code: Alles auswählen

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Button
from functools import partial

def f(x):
    # Funktion, deren Nullstellen bestimmt werden soll (f(x) = 0).
    return x**2 - 3*x - 5

def df(x):
    # df/dx
    return 2*x - 3

def gerade(x0, fx0, dfx0, x):
    return dfx0*(x - x0) + fx0

def next_line(x, data_iterator, line, plt, event=None):
    x0, fx0 = next(data_iterator)
    y = gerade(x0, fx0, df(x0), x)
    line.set_ydata(y)
    plt.draw()

def extend_range(values):
    lower, upper = np.min(values), np.max(values)
    diff = upper - lower
    return lower - 0.1 * diff, upper + 0.1 * diff

def plot_newton(data):
    # Vorbereitungen; bleiben in jedem Plot gleich
    data = np.array(data)
    data_iterator = iter(data)
    xvals = data[:, 0]
    x = np.linspace(*extend_range(xvals), 100)
    f_x = f(x)
    fig, ax = plt.subplots()
    ax.axis([x[0], x[-1], *extend_range(f_x)])
    ax.plot(x, f_x, 'k--', label='f(x) = $x^2$ - 5x + 3')
    line, = ax.plot(x, 0*x, 'r-', label=r"y = f'($x_0)\cdot x$ + b")
    axnext = fig.add_axes([.05, .05, .1, .1])
    button_next = Button(axnext, 'Next')
    button_next.on_clicked(partial(next_line, x, data_iterator, line, plt))
    next_line(x, data_iterator, line, plt)
    plt.show()

def main():
    data = [(2, -7), (9.0, 49.0), (5.733333333333333, 10.671111111111113), (4.472965879265091, 1.5885261192744569),
     (4.205804030830568, 0.07137545325894834), (4.1926147066138695, 0.0001739582732938061), (4.192582403761021, 1.0434764163846921e-09)]
    plot_newton(data)

if __name__ == '__main__':
    main()
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Super, danke Dir!
Ich hab soweit alles nachvollzogen, nur eine Nachfrage noch:
Warum übergibts Du der Funktion nextline() extra den Wert plt?
mm96
User
Beiträge: 30
Registriert: Donnerstag 26. November 2020, 23:24

Ah, jetzt kommt doch noch ne Frage:
Hab gedacht, jetzt wär ein Knopf mit 'Play' ganz schön, der die ganze Reihe animiert durchlaufen lässt.
Das klappt auch soweit, nur muss man den Play-Button zwei Mal drücken, das ist mir nicht klar woher das kommt.
Wenn ich die Zeile mit dem anim = ... direkt im Programm laufen lasse, ohne den Knopf, dann klappt es direkt.
Im Debugger hat auch einmal Knopf drücken gereicht, dann konnte man es durchklicken und er machte die ganze Animation durch...
(nur der Erwähnung halber, falls das irgendwie ne Rolle spielen könnte: Den Iterator hab ich hier durch die Indizierung ersetzt, damit er zum Schluss das Fenster
offen lässt, das ging sonst gleich zu nach Durchlaufen der Animation...)

Code: Alles auswählen

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Button
from functools import partial
import matplotlib.animation as animation

def f(x):
    # Funktion, deren Nullstellen bestimmt werden soll (f(x) = 0).
    return x**2 - 3*x - 5

def df(x):
    # df/dx
    return 2*x - 3

def gerade(x0, fx0, dfx0, x):
    return dfx0*(x - x0) + fx0

def extend_range(values):
    lower, upper = np.min(values), np.max(values)
    diff = upper - lower
    return lower - 0.1 * diff, upper + 0.1 * diff

def next_line(x, line, data, i):
    x0, fx0 = data[i, 0], data[i, 1]
    y = gerade(x0, fx0, df(x0), x)
    line.set_data(x, y)
    return line,

def animate(fig, x, line, data, event=None):
    anim = animation.FuncAnimation(fig, partial(next_line, x, line, data), interval=1000)
    fig.canvas.draw()

def plot_newton(data):
    data = np.array(data)
    data_iterator = iter(data)
    xvals = data[:, 0]
    x = np.linspace(*extend_range(xvals), 100)
    f_x = f(x)
    fig, ax = plt.subplots()
    ax.axis([x[0], x[-1], *extend_range(f_x)])
    ax.plot(x, f_x, 'k--')
    line, = ax.plot(x, 0*x, 'r-')
    axplay = fig.add_axes([.05, .05, .1, .1])
    button_play = Button(axplay, 'Play')
    button_play.on_clicked(partial(animate, fig, x, line, data))     
    plt.show()

def main():
    data = [(2, -7), (9.0, 49.0), (5.733333333333333, 10.671111111111113), (4.472965879265091, 1.5885261192744569),
     (4.205804030830568, 0.07137545325894834), (4.1926147066138695, 0.0001739582732938061), (4.192582403761021, 1.0434764163846921e-09)]
    plot_newton(data)

if __name__ == '__main__':
    main()
Viele Grüße
Antworten