Tijdreeksgrafiek met Flask, Bootstrap en Chart.js
Een testpagina voor tijdreeksgrafieken maken met Flask, Bootstrap en Chart.js
We hebben al een applicatie gebouwd met Flask en Bootstrap en we willen wat grafieken toevoegen. In deze korte post maken we een enkele webpagina met een tijdreeksgrafiek met Flask, Bootstrap en Chart.js. Deze pagina is een testpagina die we kunnen plakken en aanpassen voor onze toepassing.
Zoals altijd doe ik dit op Ubuntu 22.04 Desktop.
Overzicht
De testpagina bevat een grafiek met tijdreeksdemogegevens:
- De tijdreeksgegevens zijn één sinus per uur, één gegevenspunt per minuut, 24 uur herhaald.
- De tijdreeksgegevens kunnen enkele ontbrekende gegevenspunten bevatten.
- Het moet mogelijk zijn om in en uit te zoomen.
- De grafiek moet responsief zijn, alleen x-as.
- Gebruik milliseconden of ISO8601.
- Het moet meertalig zijn.
Bijna alles is erg standaard. Voor het type x-as kunnen we kiezen uit:
- 'time': de x-as is consistent met de tijd, datapunten die ontbreken zijn duidelijk zichtbaar.
- 'timeseries': de x-as is gecomprimeerd waar datapunten ontbreken.
Hier gebruiken we het type 'time' .
We genereren sinusgegevens, één sinus per uur, met waarden tussen -10 en +10, en voegen opzettelijk ontbrekende gegevenspunten toe. Vervolgens stellen we 'spanGaps' in op een waarde, 2 minuten, om ervoor te zorgen dat de gegevenspunten niet met elkaar verbonden zijn.
spanGaps: 2 * 60 * 1000
De grafiek reageert, maar alleen in de x-richting. De hoogte van de grafiek is vast en we maken een lineaire y-as met een minimumwaarde (-10) en een maximumwaarde (10).
Om in te zoomen gebruiken we het muiswiel (scrollwiel) of door een gebied te selecteren met de muis. We voegen een knop toe om de zoom te resetten en terug te keren naar de oorspronkelijke grafiek.
Om verschillende talen te ondersteunen, laden we de juiste module van moment.js, bijvoorbeeld voor Duits:
https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/locale/de.min.js
De code
Maak een virtual environment en installeer Flask. Maak vervolgens een projectmap met de volgende structuur:
├── app
│ ├── factory.py
│ └── templates
│ ├── base.html
│ └── home.html
└── run.py
Zoals altijd beginnen we met 'run.py', merk op dat we hier poort 5050 gebruiken:
# 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' maken we de gegevenspunten aan en specificeren we de taal:
# 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
We hebben twee sjabloonbestanden, de basispaginasjabloon en de grafiekpaginasjabloon. De basispaginasjabloon kan door alle pagina's worden gebruikt, de grafiekpaginasjabloon bevat de pagina met onze grafiek.
{# 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 -%}
Om het uit te voeren, ga naar de projectmap en typ:
python run.py
Richt vervolgens je browser op:
http://127.0.0.1:5050
Samenvatting
We hebben een enkele webpagina gemaakt met Flask, Bootstrap en Chart.js. De grafiek toont een herhaalde sinus. Hoewel het niet erg moeilijk is, kost het tijd om alle beschikbare configuratieopties te doorlopen. We kunnen nu onze grafiektestpagina aanpassen en toevoegen aan onze toepassing.
Recent
Meest bekeken
- Basis taakplanning met APScheduler
- Voorkomen dat dubbele berichten naar een extern systeem worden gestuurd
- LSTM meerstappen-optimalisatie hyperparameter met Keras Tuner
- Documenteren van een Flask RESTful API met OpenAPI (Swagger) met gebruikmaking van APISpec
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- IPv6 gebruiken met Microk8s