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

Gráfico de series temporales con Flask, Bootstrap y Chart.js

Crear una página de prueba de gráfico de series temporales con Flask, Bootstrap y Chart.js

16 diciembre 2024
post main image

Ya tenemos una aplicación construida con Flask y Bootstrap y queremos añadir algunos gráficos. En este breve post crearemos una página web con un gráfico de series temporales utilizando Flask, Bootstrap y Chart.js. Esta página una página de prueba que podemos pegar y personalizar para nuestra aplicación.

Como siempre estoy haciendo esto en Ubuntu 22.04 Desktop.

Resumen

La página de prueba contiene un gráfico que muestra datos de demostración de series temporales:

  • Los datos de la serie temporal son un seno por hora, un punto de datos por minuto, repetido 24 horas.
  • Pueden faltar algunos puntos.
  • Debe ser posible acercar y alejar la imagen.
  • El gráfico debe ser sensible, sólo el eje x.
  • Utilice milisegundos o ISO8601.
  • Debe ser multilingüe.

Casi todo es muy estándar. Para el tipo de eje x, podemos elegir entre:

  • 'time': el eje x es coherente con el tiempo, los puntos de datos que faltan son claramente visibles.
  • 'timeseries': el eje x se comprime cuando faltan puntos de datos.

Aquí utilizamos el tipo 'time' .

Generamos datos sinusoidales, un seno por hora, con valores entre -10 y +10, y añadimos intencionadamente los puntos de datos que faltan. A continuación, fijamos 'spanGaps' en un valor, 2 minutos, para garantizar que los puntos de datos no estén conectados entre sí.

spanGaps: 2 * 60 * 1000

El gráfico responde, pero sólo en la dirección x. La altura del gráfico es fija y creamos un eje y lineal con un valor mínimo (-10) y un valor máximo (10).

Para hacer zoom, usamos la rueda del ratón (scroll wheel) o seleccionando un área con el ratón. Añadimos un botón para restablecer el zoom y volver al gráfico original.

Para soportar diferentes idiomas, cargamos el módulo apropiado de moment.js, por ejemplo, para el alemán:

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

El código

Crea un virtual environment e instala Flask. A continuación, cree un directorio de proyecto con la siguiente estructura:

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

Como siempre empezamos con 'run.py', ten en cuenta que aquí estamos usando el puerto 5050:

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

En 'factory.py' creamos los puntos de datos y especificamos el idioma:

# 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

Tenemos dos archivos de plantilla, la plantilla de página base y la plantilla de página de gráfico. La plantilla de página base puede ser utilizada por todas las páginas, la plantilla de página de gráfico contiene la página con nuestro gráfico.

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

Para ejecutar, vaya al directorio del proyecto y escriba:

python run.py

Luego dirija su navegador a:

http://127.0.0.1:5050

Resumen

Hemos creado una única página web utilizando Flask, Bootstrap y Chart.js. El gráfico muestra una onda sinusoidal repetida. Aunque no es muy difícil, lleva tiempo recorrer todas las opciones de configuración disponibles. Ahora podemos personalizar nuestra página de prueba de gráficos y añadirla a nuestra aplicación.

Enlaces / créditos

Bootstrap
https://getbootstrap.com

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

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.