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

Graphique de séries temporelles avec Flask, Bootstrap et Chart.js

Créer une page de test de graphique de séries temporelles avec Flask, Bootstrap et Chart.js.

16 décembre 2024
post main image

Nous avons déjà une application construite avec Flask et Bootstrap et nous voulons ajouter quelques graphiques. Dans ce court billet, nous créons une page web unique avec un graphique de série temporelle en utilisant Flask, Bootstrap et Chart.js. Cette page est une page de test que nous pouvons coller et adapter à notre application.

Comme toujours, je le fais sur Ubuntu 22.04 Desktop.

Vue d'ensemble

La page de test contient un graphique montrant des séries temporelles de données de démonstration :

  • Les données de la série temporelle sont une sinusoïde par heure, un point de données par minute, répétées pendant 24 heures.
  • Les données de la série temporelle peuvent comporter des points de données manquants.
  • Il doit être possible de faire un zoom avant et arrière.
  • Le graphique doit être réactif, uniquement sur l'axe des x.
  • Utilisez des millisecondes ou ISO8601.
  • Il doit être multilingue.

Presque tout est très standard. Pour le type d'axe x, nous pouvons choisir entre :

  • 'time' : l'axe des x est cohérent avec le temps, les points de données manquants sont clairement visibles.
  • 'timeseries' : l'axe des x est comprimé là où des points de données sont manquants.

Nous utilisons ici le type 'time' .

Nous générons des données sinusoïdales, une sinusoïde par heure, avec des valeurs comprises entre -10 et +10, et ajoutons intentionnellement des points de données manquants. Nous fixons ensuite la valeur de 'spanGaps' à 2 minutes, afin de nous assurer que les points de données ne sont pas connectés les uns aux autres.

spanGaps: 2 * 60 * 1000

Le graphique est réactif, mais uniquement dans la direction x. La hauteur du graphique est fixe et nous créons un axe des y linéaire avec une valeur minimale (-10) et une valeur maximale (10).

Pour zoomer, nous utilisons la molette de la souris (molette de défilement) ou nous sélectionnons une zone avec la souris. Nous ajoutons un bouton pour réinitialiser le zoom et revenir au graphique original.

Pour prendre en charge différentes langues, nous chargeons le module approprié de moment.js, par exemple pour l'allemand :

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

Le code

Créez un virtual environment et installez Flask. Créez ensuite un répertoire de projet avec la structure suivante :

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

Comme toujours, nous commençons par 'run.py', notez que nous utilisons le port 5050 ici :

# 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,
    )

Dans 'factory.py', nous créons les points de données et spécifions la langue :

# 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

Nous disposons de deux fichiers modèles, le modèle de page de base et le modèle de page de graphique. Le modèle de page de base peut être utilisé par toutes les pages, le modèle de page de graphique contient la page avec notre graphique.

{# 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 -%}

Pour l'exécuter, allez dans le répertoire du projet et tapez :

python run.py

Puis pointez votre navigateur sur :

http://127.0.0.1:5050

Résumé

Nous avons créé une page web unique en utilisant Flask, Bootstrap et Chart.js. Le graphique montre une onde sinusoïdale répétée. Bien que cela ne soit pas très difficile, il faut du temps pour passer en revue toutes les options de configuration disponibles. Nous pouvons maintenant personnaliser notre page de test graphique et l'ajouter à notre application.

Liens / crédits

Bootstrap
https://getbootstrap.com

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

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

En savoir plus...

Bootstrap Flask

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.