angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Zeitreihenchart mit Flask, Bootstrap und Chart.js

Erstellen einer Testseite für ein Zeitreihen-Diagramm mit Flask, Bootstrap und Chart.js

16 Dezember 2024
post main image

Wir haben bereits eine Anwendung mit Flask und Bootstrap erstellt und möchten nun einige Diagramme hinzufügen. In diesem kurzen Beitrag erstellen wir eine einzelne Webseite mit einem Zeitreihendiagramm mit Flask, Bootstrap und Chart.js. Diese Seite ist eine Testseite, die wir einfügen und für unsere Anwendung anpassen können.

Wie immer mache ich dies auf Ubuntu 22.04 Desktop.

Übersicht

Die Testseite enthält ein Diagramm mit Zeitreihen-Demodaten:

  • Bei den Zeitreihendaten handelt es sich um einen Sinus pro Stunde, einen Datenpunkt pro Minute, der 24 Stunden lang wiederholt wird.
  • Die Zeitreihendaten können einige fehlende Datenpunkte enthalten.
  • Es muss möglich sein, das Diagramm zu vergrößern und zu verkleinern.
  • Das Diagramm sollte reaktionsfähig sein, nur die x-Achse.
  • Verwenden Sie Millisekunden oder ISO8601.
  • Es sollte mehrsprachig sein.

Fast alles ist sehr standardisiert. Für die Art der x-Achse, können wir wählen zwischen:

  • 'time': die x-Achse ist mit der Zeit konsistent, fehlende Datenpunkte sind deutlich sichtbar.
  • 'timeseries': Die x-Achse ist an den Stellen, an denen Datenpunkte fehlen, gestaucht.

Hier verwenden wir den Typ 'time' .

Wir erzeugen Sinusdaten, einen Sinus pro Stunde, mit Werten zwischen -10 und +10, und fügen absichtlich fehlende Datenpunkte hinzu. Dann setzen wir 'spanGaps' auf einen Wert, 2 Minuten, um sicherzustellen, dass die Datenpunkte nicht miteinander verbunden sind.

spanGaps: 2 * 60 * 1000

Das Diagramm ist reaktionsfähig, allerdings nur in x-Richtung. Die Höhe des Diagramms ist festgelegt und wir erstellen eine lineare y-Achse mit einem Mindestwert (-10) und einem Höchstwert (10).

Zum Vergrößern verwenden wir das Mausrad (Scrollrad) oder wählen einen Bereich mit der Maus aus. Wir fügen eine Schaltfläche hinzu, um den Zoom zurückzusetzen und zum ursprünglichen Diagramm zurückzukehren.

Um verschiedene Sprachen zu unterstützen, laden wir das entsprechende Modul von moment.js, zum Beispiel für Deutsch:

https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/locale/de.min.js

Der Code

Erstellen Sie ein virtual environment und installieren Sie Flask. Erstellen Sie dann ein Projektverzeichnis mit der folgenden Struktur:

├── app
│   ├── factory.py
│   └── templates
│       ├── base.html
│       └── home.html
└── run.py

Wie immer beginnen wir mit 'run.py', beachten Sie, dass wir hier den Port 5050 verwenden:

# run.py
from app.factory import create_app

app = create_app()

if __name__ == '__main__':
    app.run(
        host='127.0.0.1',
        port=5050,
        use_debugger=True,
        use_reloader=True,
    )

In 'factory.py' legen wir die Datenpunkte an und geben die Sprache an:

# app/factory.py
import datetime
import logging
import math

from flask import current_app, Flask, render_template

def create_app():
    app = Flask(__name__)

    app.jinja_env.auto_reload = True
    app.config['TEMPLATES_AUTO_RELOAD'] = True
    app.logger.setLevel(logging.DEBUG)

    @app.route('/', methods=['GET', 'POST'])
    def chart():
        current_app.logger.debug('()')

        # select locale
        locale = 'de-DE'
        #locale = 'en-US'
        #locale = 'nl-NL'

        chart_locales = {
            'de-DE': {'html': 'de', 'chartjs': 'de-DE', 'momentjs': 'de'},
            'en-US': {'html': 'en', 'chartjs': 'en-US', 'momentjs': None},
            'nl-NL': {'html': 'nl', 'chartjs': 'nl-NL', 'momentjs': 'nl'},
        }

        # 1 sine per hour, for 24 hours, starting now
        dt = datetime.datetime.utcnow()
        ts = int(dt.timestamp())
        chart_data = []
        dt_format_milliseconds = False
        for h in range(24):
            for minute in range(60):
                # x: in milliseconds or iso8601
                dt += datetime.timedelta(minutes=1)
                if dt_format_milliseconds:
                    x = int(dt.timestamp() * 1000)
                else:
                    x = dt.isoformat()
                # y: -10 to 10    
                rad = 6 * minute
                y = 10 * (math.sin(math.radians(rad)))
                # insert missing data points
                if rad > 120 and rad < 160:
                    continue
                chart_data.append({'x': x, 'y': y})

        return render_template(
            '/chart.html',
            page_title='Chart',
            page_contains_charts=True,
            chart_locale=chart_locales[locale],
            chart_data=chart_data,
        )

    return app

Wir haben zwei Vorlagendateien, die Basisseitenvorlage und die Diagrammseitenvorlage. Die Basisseitenvorlage kann von allen Seiten verwendet werden, die Diagrammseitenvorlage enthält die Seite mit unserem Diagramm.

{# app/templates/base.html #}
<!DOCTYPE html>
<html lang="{{ chart_locale['html'] }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ page_title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>

<main id="main" class="container -fluid">
    <div class="row">
        <div class="col">
        {% block main -%}{% endblock -%}
        </div>
    </div>
</main>

<!-- bootstrap + jquery -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>

{%- if page_contains_charts -%}
<!-- chartjs -->
<script src=" https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js "></script>
<!-- moment -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script>
<!-- moment with locale -->
{%- if chart_locale['momentjs'] is not none -%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/locale/{{ chart_locale['momentjs'] }}.min.js"></script>
{%- endif -%}
<!-- chartjs moment adapter -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.1/chartjs-adapter-moment.min.js" integrity="sha512-hVy4KxCKgnXi2ok7rlnlPma4JHXI1VPQeempoaclV1GwRHrDeaiuS1pI6DVldaj5oh6Opy2XJ2CTljQLPkaMrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- chartjs zoom -->
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-zoom/2.2.0/chartjs-plugin-zoom.min.js" integrity="sha512-FRGbE3pigbYamZnw4+uT4t63+QJOfg4MXSgzPn2t8AWg9ofmFvZ/0Z37ZpCawjfXLBSVX2p2CncsmUH2hzsgJg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<script>
$(document).ready(function() {
    moment.locale(chartLocale);
    const ctx = document.getElementById('chart-canvas');
    const chart = new Chart(ctx, {
        type: chartType,
        data: chartData,
        options: chartOptions,
    });
    window.chart = chart;
    function reset_zoom() {
        window.chart.resetZoom();
    }
    $('#reset-zoom-button').click(function(){
        reset_zoom();
    }); 
});
</script>
{%- endif -%}
</body>
</html>
{# app/templates/chart.html #}
{% extends "base.html" %}
{% block main %}
    <h2 class="mb-3">
        {{ page_title }}
    </h2>
    <div class="row border-top border-bottom border-2 border-tertiary">
        <div class="col my-4" style="height: 300px; ">
            <canvas id="chart-canvas"></canvas>
        </div>
    </div>
    <div class="row">
        <div class="col">
            <button id="reset-zoom-button" type="button" class="btn btn-outline-secondary btn-sm my-2 px-1 py-0">
                Reset Zoom
            </button>
        </div>
    </div>

<script>
const chartLocale = "{{ chart_locale['chartjs'] }}";

// colors from:
// https://github.com/chartjs/Chart.js/blob/master/docs/scripts/utils.js
const CHART_COLORS = {
    red: 'rgb(255, 99, 132)',
    orange: 'rgb(255, 159, 64)',
    yellow: 'rgb(255, 205, 86)',
    green: 'rgb(75, 192, 192)',
    blue: 'rgb(54, 162, 235)',
    purple: 'rgb(153, 102, 255)',
    grey: 'rgb(201, 203, 207)'
};

const chartType = 'line';

const dataSet1 = {
    label: 'dataSet1',
    data: {{ chart_data|safe }},
    spanGaps: 2 * 60 * 1000,
    // line & point color & width
    borderColor: CHART_COLORS.blue,
    borderWidth: 1,
    pointBackgroundColor: CHART_COLORS.red,
    pointBorderColor: CHART_COLORS.red,
    pointStyle: 'circle',
    pointRadius: 1,
    pointHoverRadius: 10
};

const chartData = {
    datasets: [
        dataSet1
    ]
};

const xScaleDisplayFormat = 'MMM DD HH:mm:ss';

const chartScales = {
    x: {
        type: 'time',
        ticks: {
            autoSkip: true,
            autoSkipPadding: 10,
        },
        time: {
            tooltipFormat: xScaleDisplayFormat,
            displayFormats: {
                millisecond: xScaleDisplayFormat,
                second: xScaleDisplayFormat,
                minute: xScaleDisplayFormat,
                hour: xScaleDisplayFormat,
            }
        },
    },
    y: {
        type: 'linear',
        min: -10,
        max: 10,
    }
};

const chartZoom = {
    pan: {
        enabled: true,
        mode: 'x',
        modifierKey: 'ctrl',
    },
    zoom: {
        wheel: {
            enabled: true,
        },
        drag: {
            enabled: true
        },
        mode: 'x',
    }
};

const chartOptions = {
    locale: chartLocale,
    responsive: true,
    maintainAspectRatio: false,
    scales: chartScales,
    plugins: {
        zoom: chartZoom,
        legend: {
            display: false
        }
    }
};
</script>
{%- endblock -%}

Zur Ausführung gehen Sie in das Projektverzeichnis und geben Sie ein:

python run.py

Rufen Sie dann Ihren Browser auf:

http://127.0.0.1:5050

Zusammenfassung

Wir haben eine einzelne Webseite mit Flask, Bootstrap und Chart.js erstellt. Das Diagramm zeigt eine wiederholte Sinuswelle. Obwohl es nicht sehr schwierig ist, braucht man Zeit, um alle verfügbaren Konfigurationsoptionen durchzugehen. Jetzt können wir unsere Diagramm-Testseite anpassen und zu unserer Anwendung hinzufügen.

Links / Impressum

Bootstrap
https://getbootstrap.com

Chart.js
https://www.chartjs.org/docs/latest

Flask
https://flask.palletsprojects.com/en/stable

Mehr erfahren

Bootstrap Flask

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.