Zum Inhalt

Flask + Jinja2

Einführung

Mit den bereits vorgstellten Python-Werkzeugen Flask und Jinja2 soll nun die Realisierung eines dynamischen Dashboard gezeigt werden.

Der Verarbeitungsablauf zeigt das nachfolgende Bild.

app

In diesem Beispiel werden folgende Prinzipien demonstriert:

  • Datenliste mit Meta-Daten
  • Automatisiertes Jinja2-Rendering der Metadaten
  • Zyklischer Refresh der anzuzeigenden Daten über RESTapi
  • Senden von Daten
  • Dynamische Anzeige durch Html-Elemente
  • Verwendung der Apache eChart-Komponente Line-Chart
  • Modifikation der Simulationsdaten durch POST-Aktion

Datenstruktur

Für Benutzerinteraktion und die Visualisierung sind neben dem eigentlichen Datenpunkt erweiterte Daten - sogenannte Metadaten - hilfreich. Ein sehr einfaches Datenmodell kann wie folgt aussehen:

Datenstruktur
name
value
unit
id

Der Identifier id muss nicht explizit vorgegeben werden, sondern lässt sich automatisch generieren. Er wird benötigt, um auf der Webseite Elemente eindeutig identifizierten und adressieren zu können.

1. Beispiel: Anzeige der dynamischen Daten mit JS

Responsive Weblayout mit W3.CSS

Moderne Webseiten sollen auf allen Endgräten gleich gut funktionieren. Daher sind für einne PC und einem Smartphone unterschiedliche Darstellungen erforderlich. Dies wird heute durch zahlreiche Boilerplates recht einfach möglich. Neben der sehr weit verbreiteten Bootstrap-Bibliothek soll in diesem Demo-Projekt das W3.css-System eingesezt werden. Ausführliche und interaktive Quelle: w3schools.com

Um diese Funktionen nutzen zu können, müssen die Bibliotheken

  • w3.css und opt.
  • w3.js

im Header der Html-Datei eingebunden werden:

<html lang="de"> <!-- (1) -->
<head>
    <meta charset="UTF-8"> <!-- (2) -->
    <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- (3) -->
    <link rel="stylesheet" href="https://www.w3schools.com/w3css/5/w3.css"> <!-- (4) -->
    <script src="https://www.w3schools.com/lib/w3.js"></script>  <!-- (5) -->
</head>

  1. Sprache einstellen
  2. Zeichensatz einstellen
  3. Responsive-Scaling einstellen
  4. w3.csseinbinden
  5. die Javascript-Bibliothek wird nur benötigt, wenn JS-Funktionen, wie z.B. w3.slideshow(..) genutzt werden sollen.
Tip

In der Entwicklungsphase (und durchgängig in dieser Webseite) werden die externen Bibliotheken vom der Webseite oder entsprechenden Distributionsseiten heruntergeladen. Wenn die Entwicklung abgeschlossen ist, dann sollten diese Bibliotheken lokal in das Unterverzeichnis /static abgelegt werden, damit sie

  • nicht immer Trafic im Web erzeugen
  • unabhängig von einer Internetverbindung in lokalen Netzwerkumgebungen genutzt werden können
  • damit die Nutzung der Bibliotheken nicht von dem Betreiber nachverfolgt werden kann (Datenschutz) Dies gilt insbesondere für Google-Dienste (Bibliotheken, Fonts, Maps, etc.!!)

Aufbau der Web-Seite

Im <body>-Bereich werden dynamisch die Anzeigeelemente aufgebaut:

<div class="w3-container">
    <p id="temperature">12.34</p>
</div>

Das Element <p> soll den sich zyklisch ändernden Anzeigetext enthalten. Zur eindeutigen Kennzeichnung und Adressierung durch ein JS bekommt es eine id.

Das JS wird im Zeitraster 1000ms aufgerufen:

    <script>
        let t_ms = 1000;    // Zykluszeit in ms
        document.addEventListener("DOMContentLoaded", () => {    // (1)
            setInterval(function () {    // (2)
                fetch('/onlineV', {    // (3)
                    method: 'GET'    // (4)
                })
                    .then(function (response) { return response.json(); })
                    .then(function (data) {
                        // use the received json:
                        document.getElementById("temperature").innerHTML = data.val;    // (5)
                    });

            }, t_ms);
        });
    </script>

  1. Wenn das Dokument vollständig im Client geladenb wurde, dann...
  2. Zyklische Aktion einrichten
  3. fetch-Methode für den Html-Rquest benutzen (ist durch den Browser verfügbar).
  4. GET-Methode (== Anforderung senden)
  5. In dem Json-Objekt data ist das Datum val enthalten, das mit der Methode document.getElementById() in das html-Element kopiert wird.
html-Template-Seite index_fetch_R.j2

    <html lang="de">

    <head>
        <title>{{ title }}</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://www.w3schools.com/w3css/5/w3.css">
    </head>

    <body>

        <div class="w3-container w3-teal">
            <h1>Dashboard {{title}}</h1>
        </div>

      <div class="w3-bar w3-black">
        <a href="/" class="w3-bar-item w3-button">Home</a>
        <a href="/board" class="w3-bar-item w3-button">Board</a>
      </div>

        <p id="temperature"></p>

        <script>
            let t_ms = 1000;    // Zykluszeit in ms
            document.addEventListener("DOMContentLoaded", () => {
                setInterval(function () {
                    fetch('/onlineV', {
                        method: 'GET'
                    })
                        .then(function (response) { return response.json(); })
                        .then(function (data) {
                            // use the json
                            document.getElementById("temperature").innerHTML = data.val;
                        });

                }, t_ms);
            });
        </script>
    </body>

    </html>
Download: File

py-Skript (Flask)

Die Flask-App muss zwei GET-Endpoints zur Verfügung stellen:

  • Root \
  • Board \board
  • cycl. response \onlineV

Endpoint Root

Die Root-Seite entspricht der index.html und wird vom Webserver gesteuert automatisch aufgerufen. Hier wird in der Funktion index() die Liste namedefiniert, die dem Renderer neben dem title übergeben wird:

@app.route('/')
def index():
    name = ['Pumpe','Schieber', 'Temperatur']
    return render_template('index.j2.html, title='Welcome', members=name)

Die Sequenz

<h2>Member-Liste</h2>
<ul>
    {% for member in members: %}
    <li>{{ member }}</li>
    {% endfor %}
</ul>

iteriert über members und generiert mit {{ members }} den html-Output.

Endpoint Board

Der Endpoint /board definiert die Methode und den Render-Aufruf

@app.route('/board', methods=['GET'])
def board():
    return render_template('index_fetch_R.j2.html', title='fetch')

In dem html-Template wird wie schon oben gezeigt die Intervall-Funktion aktiviert und per fetch()-Aufruf der Endpoint /onlineV ein aktueller Wert an den Client "ausgeliefert" (response):

@app.route('/onlineV', methods=['GET'])
def onlineV():
    j = {"val":np.random.random()}  # (1)
    r = json.dumps(j)               # (2)
    return r

  1. Zufallszahl generiert
  2. dict -> json

Downloads

Download: app_R.py
Download: index_fetch_R.j2.html

Ajax + jQuery

fetch() ersetzt die Ajax-Variante eines http-request

<script>
    let t_ms = 1000;
    $(document).ready(function () {
    setInterval(function () {
        $.ajax({
        dataType: "json",
        url: "/onlineV",
        success: function (data) {
            $('#temperature').html(data.val);

        }
        });
    }, t_ms);
    });
</script>

In dieser Code-Sequenz wurde mit der JS-Bibliothek jQuery der Zugriff auf html-Elemente sehr stark vereinfacht. jQuery muss im Header der html-Datei eingebunden werden:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

jQuery-Home

W3-Schools

2. Beispiel: Erweiterung von Bsp.1 auf Liste von Datenelementen

Meistens sind nicht nur ein Datenpunkt sondern eine ganze Liste zu visualisieren. Diese Liste braucht ein Datenmodell.

Datenmodell

Ein Datenpunkt soll die folgenden Eigenschaften haben:

  • name
  • val
  • unit
  • id

Sie werden in einer Liste von Dictionary's definiert:

datalst = [{"name":"temp", "val":24, "unit":"°C", "id":"id0",
           {"name":"pump", "val":12.02, "unit":"m3", "id":"id1", 
           {"name":"valve","val":0.92, "unit":"mm", "id":"id2"]

die "id"s werden als *Identifer" für die Webseitenbearbeitung bewnötigt und müssen unbedingt eindeutig sein. Entweder gibt man sie manuel vor oder generiert sie:

import secrets  # (1)
datalst = [{"name":"temp", "val":24, "unit":"°C", "id":secrets.
                token_urlsafe(2)}, # (2)
           {"name":"pump", "val":12.02, "unit":"m3", "id":secrets.token_urlsafe(2)}, 
           {"name":"valve","val":0.92, "unit":"mm", "id":secrets.token_urlsafe(2)}]

  1. Die Bibliothek secrets ist eine Python-Standard-Bibliothek.
  2. token_urlsafe(2)generiert eine 2-Byte großen Code.

Endpoint root

Hier wird einfach die datalst als json-Objekt ausgegeben:

Browser-Inhalt für den Endpoint "root"

Endpoint Board

Der Endpoint /board sind nun ein neuer Parameter hinzugekommen: datalst:

1
2
3
4
@app.route("/board", methods=["GET"])
def board():
    return render_template("index_fetch_R_lst.j2.html", title="fetch Lst",
        datalst=datalst)

datalstwird im Template dazu verwendet, die Ausgabe vollkommen automatisch zu generieren:

Template-Ausschnitt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    <table class="w3-table w3-bordered">
      {% for ele in datalst %}
      <tr>
        <td>{{ ele.name }}</td>
        <td class="w3-theme-l4 w3-monospace w3-large" style="width: 100%; font-weight: bold">
          <span id="{{ele.id}}">{{ ele.val }} </span>
        </td>
        <td>{{ ele.unit }}</td>
        <td></td>
        <!-- leer -->
      </tr>
      {% endfor %}
    </table>

Quellcode im Browser des ersten Elements
<tr>
    <td>temp </td>  # (1)
    <td class="w3-theme-l4 w3-monospace w3-large" style="width:100%; 
        font-weight: bold;"><span id="Ttg">24 </span></td> # (2)
    <td>°C</td>     # (3)
    <td></td> <!-- leer -->
</tr>

  1. {{ ele.name }}
  2. {{ele.id}}
  3. {{ ele.unit }}

Beim Aufruf der Seite im Browser erscheint folgendes Bild:

Downloads

Download: app_lst.py
Download: index_fetch_R.j2.html

3. Beispiel: Erweiterung von Bsp.2 um grafischen Plot-Ausgabe

Bei dynamisch sich ändernden Daten ist die Ausgabe der "Zeitreihen" in einen Plot - z.B. Line-Chart - wünschenswert. Für die Darstellung in einem Browser sollten hierfür JS-Bibliotheklen genutzt werden. Hier einige Beispiel verbreiteter Open-Source-Lösungen:

Für dieses Beispiel-Projekt wird Apache eChart genutzt.

Einbinden der eChart-LinePLot-Komponente

Die Nutzung der eChart-Komponente gliedert sich in der html-Seite in die folgenden Blöcke:

Blöcke einer Webseite für eChart-Komponente

Ein einfaches Beispiel (aus der eChart-Webseite) für einen statischen Line-Plot:

line-function.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<!--
    THIS EXAMPLE WAS DOWNLOADED FROM https://echarts.apache.org/examples/en/editor.html?c=line-function&code=GYVwdgxgLglg9mABKSAKAHgSkQbwFCKLqID0AvIgIwAMA3AYgE4CmUIjSAsgIZQAWAOgDOMMBmwAqRD34CIcIRkRSATIgDUVSdN6CRY4lIDMGxCu0BWOngC-eFNHhIA5szDNGvZgBFe3VNj4hAA2rIgAJn6IFADaALr0hMBwjIiooVCIMNGIdFmIADwUACzUednqFNQClIEMhJFQ3AIADiBCfKgxMAA0yOAQqDCYcZiJiHaELGwcEX70dnAtsAg5QYjcYDAAtrxOAFzI3MFCzD14DOgAgugwQofrhGDc28yHAOTo7-eEhNuiKQAKjAIABrB71X4dOAAd0OUEYIGYkJsP1-_zAKQAyi1gjAoAAZURvXCQwjQuGIBFIlEMVEMACeNzuEN-iGerw-DO-kIxQJB4NJbPJfFh8MRyLZ9LZfMYOLxhOJrOFFPFNKldPODFOjBgzHuiBikMevygDJaJPeePcPOFiApWIZ2wARnBgodgMdTmi2RA8S01WcyXMmodXO5PFAfH4ArTCHFbLQgA
-->
<!DOCTYPE html>
<html lang="de" style="height: 100%">
  <head>
    <meta charset="utf-8" />
    <script
      type="text/javascript"
      src="https://fastly.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"
    ></script> <!-- (1) -->
  </head>
  <body style="height: 100%; margin: 0">
    <h1>eChart-Line</h1>

    <div id="container" style="height: 100%"></div> <!-- (2) -->

    <script type="text/javascript">
      var dom = document.getElementById("container");
      var myChart = echarts.init(dom, null, {
        renderer: "canvas",
        useDirtyRect: false,
      });
      var app = {};

      var option;

      function func(x) {
        x /= 10;
        return Math.sin(x) * Math.cos(x * 2 + 1) * Math.sin(x * 3 + 2) * 50;
      }
      function generateData() {
        let data = [];
        for (let i = 0; i <= 400; i += 0.1) {
          data.push([i, func(i)]);
        }
        return data;
      }
      option = {              // (3)
        animation: false,

        xAxis: {
          name: "x",
          minorTick: {
            show: true,
          },
          minorSplitLine: {
            show: true,
          },
        },
        yAxis: {
          name: "y",
          minorTick: {
            show: true,
          },
          minorSplitLine: {
            show: true,
          },
        },

        series: [     // (4)
          {
            type: "line",
            showSymbol: false,
            clip: true,
            data: generateData(), // (5)
          },
        ],
      };
      // (6)
      if (option && typeof option === "object") {
        myChart.setOption(option);
      }

      window.addEventListener("resize", myChart.resize);
    </script>
  </body>
</html>

  1. Einbinden der Bibliothek (hier: aus dem Web)
  2. Container für das eChart-Element definieren
  3. json-Objekt option definiert die Grundlegenden Eigenschaft und kann dynamisch verändert und aktiviert werden.
  4. series[] definieren einzelne Lines
  5. Zuweisung der Daten (die sich später danamisch ändern werden).
  6. Aktivieren der options

Download: File

Dynamischer Line-Plot

Für eine dynmischen Plot muss das Array data in options.series[] aktualisiert werden. Dies kann mit einem zyklischen js-Skript umgesetzt werden: