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

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.
Recientes
- Gráfico de series temporales con Flask, Bootstrap y Chart.js
- Utilización de IPv6 con Microk8s
- Uso de Ingress para acceder a RabbitMQ en un clúster Microk8s
- Galería de vídeo simple con Flask, Jinja, Bootstrap y JQuery
- Programación básica de trabajos con APScheduler
- Un conmutador de base de datos con HAProxy y el HAProxy Runtime API
Más vistos
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados