Überblick#

In einem Panel könnt ihr interaktive Steuerelemente hinzufügen. Dies erlaubt euch, einfache interaktive Apps, aber auch komplexe mehrseitige Dashboards zu erstellen.Wir beginnen zunächst mit einem einfachen Beispiel einer Funktion zum Zeichnen einer Sinuswelle mit Matplotlib:

[1]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


%matplotlib inline


def mplplot(df, **kwargs):
    fig = df.plot().get_figure()
    plt.close(fig)
    return fig


def sine(frequency=1.0, amplitude=1.0, n=200, view_fn=mplplot):
    xs = np.arange(n) / n * 20.0
    ys = amplitude * np.sin(frequency * xs)
    df = pd.DataFrame(dict(y=ys), index=xs)
    return view_fn(df, frequency=frequency, amplitude=amplitude, n=n)


sine(1.5, 2.5)
[1]:
../../_images/dashboards_panel_overview_1_0.png

Interaktive Panels#

Wenn wir viele Kombinationen dieser Werte ausprobieren möchten, um zu verstehen, wie sich Frequenz und Amplitude auf dieses Diagramm auswirken, könnten wir die oben genannte Zelle viele Male neu bewerten. Dies wäre jedoch ein langsamer und aufwändiger Prozess. Stattdessen die Werte im Code jedesmal neu angeben zu müssen, empfiehlt sich, die Werte mithilfe von Schiebereglern interaktiv anzupassen. Mit einer solchen Panel-App könnt ihr einfach die Parameter einer Funktion untersuchen. Dabei ähnelt pn.interact der Funktion von ipywidgets interact:

[2]:
import panel as pn


pn.extension()

pn.interact(sine)
[2]:

Solange ein Live-Python-Prozess ausgeführt wird, wird durch Ziehen dieser Widgets die sine-Callback-Funktion aufgerufen und auf die von euch ausgewählte Kombination von Parameter-Werten ausgewertet und die Ergebnisse angezeigt. Mit einem solchen Panel könnt ihr einfach alle Funktionen untersuchen, die ein visuelles Ergebnis eines unterstützten Objekttyps (s. Supported object types and libraries liefern, z.B. Matplotlib, Bokeh, Plotly, Altair oder verschiedene Text- und Bildtypen.

Komponenten von Panels#

interact ist praktisch, aber was ist, wenn Sie mehr Kontrolle darüber wünschen, wie es aussieht oder funktioniert? Lassen Sie uns zunächst sehen, was interact tatsächlich erstellt wird, indem Sie das Objekt greifen und seine Darstellung anzeigen:

[3]:
i = pn.interact(sine, n=(5, 100))
print(i)
Column
    [0] Column
        [0] FloatSlider(end=3.0, name='frequency', start=-1.0, value=1.0)
        [1] FloatSlider(end=3.0, name='amplitude', start=-1.0, value=1.0)
        [2] IntSlider(end=100, name='n', start=5, value=200)
    [1] Row
        [0] Matplotlib(Figure, name='interactive00154')

Wir sehen hier, dass der interact-Aufruf ein pn.Column-Objekt erstellt hat, das aus einer WidgetBox (mit 3 Widgets) und einer pn.Row-Matplotlib-Figure besteht. Das Bedienfeld ist kompositorisch, sodass ihr diese Komponenten beliebig mischen und zuordnen könnt, indem ihr bei Bedarf weitere Objekte hinzufügt:

[4]:
pn.Row(i[1][0], pn.Column("<br>\n# Sine waves", i[0][0], i[0][1]))
[4]:

Beachtet, dass die Widgets mit ihrem Plot verknüpft bleiben, auch wenn sie sich in einer anderen Notebook-Zelle befinden:

[5]:
i[0][2]
[5]:

Neue Panels#

Mit diesem kompositorischen Ansatz könnt ihr verschiedene Komponenten wie Widgets, Diagramme, Text und andere Elemente, die für eine App oder ein Dashboard benötigt werden, auf beliebige Weise kombinieren. Das interact-Beispiel baut auf einem reaktiven Programmiermodell auf, bei dem sich eine Eingabe für die Funktion ändert und das Bedienfeld die Ausgabe der Funktion reaktiv aktualisiert. interact ist eine praktische Möglichkeit, Widgets aus den Argumenten für eure Funktion automatisch zu erstellen. Panel bietet jedoch auch eine explizitere reaktive API, mit der ihr Verbindungen zwischen Widgets und Funktionsargumenten definieren und anschließend das resultierende Dashboard manuell von Grund auf neu erstellen können.

Im folgenden Beispiel deklarieren wir explizit jede Komponente einer App:

  1. Widgets

  2. eine Funktion zum Berechnen von Sinuswerten

  3. Spalten- und Zeilencontainer

  4. die fertige sine_panel-App.

Widget-Objekte haben mehrere Parameter (aktueller Wert, zulässige Bereiche usw.), und hier verwenden wir den depends-Decorator von Panel, um zu deklarieren, dass die Eingabewerte der Funktion von den value-Parametern der Widgets stammen sollen. Wenn nun die Funktion und die Widgets angezeigt werden, aktualisiert das Panel die angezeigte Ausgabe automatisch, wenn sich eine der Eingaben ändert:

[6]:
import panel.widgets as pnw


frequency = pnw.FloatSlider(name="frequency", value=1, start=1.0, end=5)
amplitude = pnw.FloatSlider(name="amplitude", value=1, start=0.1, end=10)


@pn.depends(frequency.param.value, amplitude.param.value)
def reactive_sine(frequency, amplitude):
    return sine(frequency, amplitude)


widgets = pn.Column("<br>\n# Sine waves", frequency, amplitude)
sine_panel = pn.Row(reactive_sine, widgets)

sine_panel
[6]:

Deploy-Panels#

Die obigen Panels funktionieren alle in einer Notebook-Zelle, aber im Gegensatz zu ipywidgets und anderen Ansätzen funktionieren Panel-Apps auch auf eigenständigen Servern. Die obige App kann beispielsweise als eigener Webserver gestartet werden, mit:

[7]:
sine_panel.show()
Launching server at http://localhost:61964
[7]:
<panel.io.server.Server at 0x167db3990>

Dies startet den Bokeh-Server und öffnet ein Browser-Fenster mit der Anwendung.

Oder ihr könnt einfach angeben, was ihr auf der Webseite sehen möchtet. servable(), und dann den Shell-Befehl pipenv run panel serve --show example.ipynb, um einen Server mit diesem Objekt zu starten:

[8]:
sine_panel.servable();

Das Semikolon vermeidet, dass hier im Notizbuch eine weitere Kopie des Sinusfelds angezeigt wird.

Deklarative Panels#

Der obige Kompositionsansatz ist sehr flexibel, verknüpft jedoch domänenspezifischen Code (die Teile über Sinuswellen) mit dem Widget-Anzeigecode. Das ist üblich in prototypischen Projekten, aber in Projekten, bei denen der Code in vielen verschiedenen Kontexten verwendet werden soll, sollen Teile des Codes, die sich auf die zugrunde liegende Domänen (d.h. die Anwendung oder den Forschungsbereich) beziehen, von denen getrennt werden, die an bestimmte Anzeigetechnologien gebunden sind (wie Jupyter-Notebooks oder Webserver).

Für solche Verwendungen unterstützt Panel Objekte, die mit der separaten Param-Bibliothek deklariert wurden. Dies bietet eine Möglichkeit, die Parameter eurer Objekte (Code, Parameter, Anwendung und Dashboard-Technologie) unabhängig zu erfassen und zu deklarieren. Der obige Code kann zum Beispiel in einem Objekt erfasst werden, das die Bereiche und Werte aller Parameter sowie die Generierung des Diagramms unabhängig von der Panel-Bibliothek oder einer anderen Art der Interaktion mit dem Objekt deklariert:

[9]:
import param


class Sine(param.Parameterized):
    amplitude = param.Number(default=1, bounds=(0, None), softbounds=(0, 5))
    frequency = param.Number(default=2, bounds=(0, 10))
    n = param.Integer(default=200, bounds=(1, 200))

    def view(self):
        return sine(self.frequency, self.amplitude, self.n)


sine_obj = Sine()

Die Sine-Klasse und die sine_obj-Instanz sind nicht abhängig von Panel, Jupyter oder einem anderen GUI- oder Web-Toolkit – sie deklarieren einfach Fakten über eine bestimmte Domäne (z.B., dass Sinuswellen Frequenz- und Amplitudenparameter annehmen und dass die Amplitude eine Zahl größer oder gleich Null ist). Diese Informationen reichen dann für Panel aus, um eine bearbeitbare und anzeigbare Darstellung für dieses Objekt zu erstellen, ohne dass etwas angegeben werden muss, das von den domänenspezifischen Details abhängt, die in Die Sine-Klasse und die sine_obj-Instanz sind nicht abhängig von Panel, Jupyter oder einem anderen GUI- oder Web-Toolkit. Sie deklarieren einfach Fakten über einen bestimmten Bereich (z. B., dass Sinuswellen Frequenz- und Amplitudenparameter annehmen und dass die Amplitude eine Zahl größer oder gleich Null ist). Diese Informationen reichen dann für Panel aus, um eine bearbeitbare und anzeigbare Darstellung für dieses Objekt zu erstellen, ohne dass etwas angegeben werden muss, das von den domänenspezifischen Details abhängt, die außerhalb von sine_obj enthalten sind:

[10]:
pn.Row(sine_obj.param, sine_obj.view)
[10]:

Um eine bestimmte Domäne zu unterstützen, könnt ihr Hierarchien solcher Klassen erstellen, in denen alle Parameter und Funktionen zusammengefasst sind, die ihr für verschiedene Objektfamilien benötigt. Dabei werden sowohl Parameter als auch Code in den Klassen übernommen, und zwar unabhängig von einer bestimmten GUI-Bibliothek oder sogar das Vorhandensein einer GUI überhaupt. Dieser Ansatz macht es praktisch, eine große Codebasis beizubehalten, die mit Panel vollständig angezeigt und bearbeitet werden kann, und zwar auf eine Weise, die im Laufe der Zeit beibehalten und angepasst werden kann.

Verknüpfen von Plots und Aktionen zwischen Panels#

Die oben genannten Ansätze arbeiten jeweils mit einer Vielzahl von anzeigbaren Objekten, einschließlich Bildern, Gleichungen, Tabellen und Diagrammen. In jedem Fall bietet das Panel interaktive Funktionen mithilfe von Widgets und aktualisiert die angezeigten Objekte entsprechend, wobei nur sehr wenige Annahmen darüber getroffen werden, was tatsächlich angezeigt wird. Panel unterstützt auch eine umfassendere und dynamischere Interaktivität, bei der das angezeigte Objekt selbst interaktiv ist, z.B. JavaScript-basierte Diagramme von Bokeh und Plotly.

Wenn wir beispielsweise den mit Pandas gelieferten Matplotlib-Wrapper durch denBokeh-Wrapper hvPlot ersetzen, erhalten wir automatisch interaktive Plots, die zooming, panning und hovering ermöglichen:

[11]:
import hvplot.pandas


def hvplot(df, **kwargs):
    return df.hvplot()


pn.interact(sine, view_fn=hvplot)
[11]:

Diese interaktiven Aktionen können mit komplexeren Interaktionen in einem Plot (z. B. tap, hover) kombiniert werden um das Erkunden von Daten und das Aufdecken von Zusammenhängen zu vereinfachen. Beispielsweise können wir HoloViews verwenden, um eine umfassendere Version des hvPlot-Beispiels zu erstellen, das dynamisch aktualisiert wird um die Position auf dem Kreis anzuzeigen, wenn wir mit der Maus über die Sinuskurve fahren:

[12]:
import holoviews as hv


tap = hv.streams.PointerX(x=0)


def hvplot2(df, frequency, **kwargs):
    plot = df.hvplot(width=500, padding=(0, 0.1))
    tap.source = plot

    def unit_circle(x):
        cx = np.cos(x * frequency)
        sx = np.sin(x * frequency)
        circle = hv.Path(
            [hv.Ellipse(0, 0, 2), [(-1, 0), (1, 0)], [(0, -1), (0, 1)]]
        ).opts(color="black")
        triangle = hv.Path(
            [[(0, 0), (cx, sx)], [(0, 0), (cx, 0)], [(cx, 0), (cx, sx)]]
        ).opts(color="red", line_width=2)
        labels = hv.Labels(
            [(cx / 2, 0, "%.2f" % cx), (cx, sx / 2.0, "%.2f" % sx)]
        )
        labels = labels.opts(
            padding=0.1, xaxis=None, yaxis=None, text_baseline="bottom"
        )
        return circle * triangle * labels

    vline = hv.DynamicMap(hv.VLine, streams=[tap]).opts(color="black")

    return (plot * vline).opts(toolbar="right")


unit_curve = pn.interact(
    sine, view_fn=hvplot2, n=(1, 200), frequency=(0, 10.0)
)

pn.Column(
    pn.Row(
        "# The Unit Circle",
        pn.Spacer(width=45),
        unit_curve[0][0],
        unit_curve[0][2],
    ),
    unit_curve[1],
)
[12]: