Project M - Bouw een Data Dashboard met Flask
01 - Doel en voorkennis
Je bouwt een Flask-website die ordertotalen uit een bestaande SQLite-database toont als tabel en één staafgrafiek. Je doorloopt eerst Python Basics 1 t/m 11 en projecten A t/m L. Daarmee ken je lokale scripts, .venv, packages, paden, CSV, JSON en API’s.
Nieuw zijn de webapp, routes, HTML/CSS, Jinja, SQLite/SQL en een grafiek in de browser. Ieder onderdeel wordt hieronder uitgelegd voordat je ermee werkt. Je hoeft geen aparte SQL- of webcursus gevolgd te hebben voor deze basisopdracht.
Een webapp is een programma dat aanvragen van een browser beantwoordt. De browser is de client; het Python-programma is de server. Flask is het Python-pakket waarmee we zo’n serverprogramma bouwen.
Bekijk een uitgebreider dashboard als inspiratie. De basisopdracht hieronder maakt één grafiek. Voeg meer grafieken pas toe nadat die werkt.
02 - Een eigen projectomgeving voorbereiden
Het doel van een virtual environment is al uitgelegd in project A, blok 02. Hier pas je dezelfde werkwijze toe. Maak een nieuwe map project_m, open die in VS Code en open daar een terminal.
Maak de omgeving één keer aan. Gebruik op macOS/Linux python3 als python buiten een omgeving niet bestaat:
python -m venv .venv
Activeren in Windows PowerShell:
.venv\Scripts\Activate.ps1
In Windows cmd:
.venv\Scripts\activate.bat
Op macOS/Linux:
source .venv/bin/activate
Kies in VS Code de interpreter uit .venv. Werk bij alle volgende terminalopdrachten in project_m met deze omgeving actief. In een nieuwe terminal activeer je opnieuw; je maakt de omgeving niet telkens opnieuw aan.
03 - Flask installeren en dependencies bewaren
pip ken je van het installeren van Pillow en Requests. Flask installeren doe je nu op dezelfde manier:
python -m pip install flask
python -m pip show flask
Een dependency is een pakket waarvan je project afhankelijk is. In requirements.txt bewaren we de namen en versies, zodat we ze op een andere computer opnieuw kunnen installeren. pip freeze toont de geïnstalleerde pakketten; het terminalteken > schrijft die uitvoer naar een bestand en vervangt een eventueel bestaand bestand:
python -m pip freeze > requirements.txt
Voor opnieuw installeren gebruik je later python -m pip install -r requirements.txt. -r betekent dat pip de lijst uit dat bestand leest. sqlite3 zit al in Python; daarvoor installeer je niets.
04 - Routes begrijpen en de eerste webpagina starten
Een route koppelt een URL-pad aan een Python-functie. Het pad / is de startpagina. De functie die het antwoord maakt heet een view-functie.
Flask(__name__) maakt een app-object. __name__ geeft Flask de locatie van de module als uitgangspunt. Een decorator is de regel met @ boven een functie; @app.route("/") registreert die functie voor de startpagina. Je schrijft de decorator meteen boven de bijbehorende def.
Maak app.py naast requirements.txt en zet hierin:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Mijn eerste Flask-pagina werkt."
if __name__ == "__main__":
app.run(debug=True)
Het startblok is uitgelegd in project A, blok 04. app.run start Flask. debug=True toont fouten en herlaadt code tijdens lokaal oefenen. Start nu:
python app.py
Open http://127.0.0.1:5000/ in je browser. 127.0.0.1 is je eigen computer, 5000 de poort waarop de server luistert. Je ziet de tekst uit return. Laat de terminal open zolang je de site bekijkt; stoppen doe je met Ctrl+C. De ontwikkelserver gebruiken we lokaal. Hosting komt pas in project O.
05 - HTML, CSS en templates vóór het dashboard
HTML beschrijft de inhoud met tags: <h1> is een kop en <p> een alinea. Een eindtag zoals </p> sluit het onderdeel. CSS bepaalt de opmaak; body { ... } kiest het pagina-element en stelt eigenschappen zoals kleur en marge in.
Een template is een HTML-bestand met invulplaatsen. Flask gebruikt de template-engine Jinja: {{ titel }} vult de meegestuurde waarde in. render_template leest zo’n bestand uit templates. url_for('static', filename='style.css') maakt de URL voor een bestand uit static.
Maak nu deze mappen en bestanden naast .venv:
project_m/
├── app.py
├── requirements.txt
├── templates/
│ └── index.html
└── static/
└── style.css
Zet in templates/index.html:
<!doctype html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Orderdashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main>
<h1>{{ titel }}</h1>
<p>De template en de opmaak werken.</p>
</main>
</body>
</html>
head bevat paginagegevens en de link naar CSS; body bevat de zichtbare inhoud. charset kiest de tekstcodering. viewport laat de pagina ook op een smal scherm goed schalen. Zet in static/style.css:
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 32px;
background: #f4f4f4;
color: #222;
}
main { max-width: 960px; margin: 0 auto; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #aaa; padding: 8px; text-align: left; }
canvas { max-height: 440px; }
Vervang nu de volledige inhoud van app.py door deze versie. De route stuurt een titel mee naar de template:
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html", titel="Orderdashboard")
if __name__ == "__main__":
app.run(debug=True)
Start opnieuw als je de server had gestopt en herlaad de browser. Je ziet de titel en opmaak. Ga pas verder als dit werkt; bij TemplateNotFound controleer je de mapnaam templates en de bestandsnaam.
06 - SQLite en het databasebestand voorbereiden
Een database bewaart gegevens in tabellen met kolommen en rijen. SQLite bewaart die tabellen in één bestand. Met de ingebouwde module sqlite3 opent Python een verbinding naar dat bestand.
Maak de map data in project_m. Download tysql.sqlite en plaats het daarin. Maak geen leeg bestand met die naam: je hebt de gedownloade database met bestaande tabellen nodig.
In de tabel OrderItems staan onder meer order_num (ordernummer), quantity (aantal) en item_price (prijs per stuk). In dit project lezen we alleen gegevens.
Maak naast app.py een apart bestand controle_db.py. connect opent een verbinding. We controleren eerst het pad, omdat SQLite anders ongemerkt een lege database kan maken. try/finally voert het finally-blok altijd uit, ook als er een fout optreedt; zo sluiten we de verbinding.
from pathlib import Path
import sqlite3
DB_PATH = Path(__file__).resolve().parent / "data" / "tysql.sqlite"
if not DB_PATH.is_file():
raise FileNotFoundError(f"Database ontbreekt: {DB_PATH}")
connection = sqlite3.connect(DB_PATH)
try:
print("Database geopend:", DB_PATH)
finally:
connection.close()
python controle_db.py
Ga pas verder als het bestaande bestand wordt gevonden en geopend.
07 - SQL begrijpen en ordertotalen controleren
SQL is de taal om databasetabellen te bevragen. Dit zijn de onderdelen van onze query:
SELECTkiest de kolommen en berekeningen die je terug wilt krijgen;FROMkiest de tabel.quantity * item_priceberekent het bedrag van een orderregel.SUM(...)telt die bedragen op.GROUP BY order_nummaakt één totaal per order.AS order_totalgeeft de berekende kolom een naam.ORDER BY order_numsorteert de uitkomst. Een query leest de tabel; er wordt geen brondata gewijzigd.
SELECT order_num, SUM(quantity * item_price) AS order_total
FROM OrderItems
GROUP BY order_num
ORDER BY order_num
execute voert de SQL-tekst uit en fetchall haalt de rijen op. sqlite3.Row maakt kolomnamen beschikbaar en dict(row) zet zo’n rij om naar een gewone dictionary. Vervang controle_db.py volledig door:
from pathlib import Path
import sqlite3
DB_PATH = Path(__file__).resolve().parent / "data" / "tysql.sqlite"
if not DB_PATH.is_file():
raise FileNotFoundError(f"Database ontbreekt: {DB_PATH}")
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
try:
rows = connection.execute("""
SELECT order_num, SUM(quantity * item_price) AS order_total
FROM OrderItems
GROUP BY order_num
ORDER BY order_num
""").fetchall()
for row in rows:
print(dict(row))
finally:
connection.close()
python controle_db.py
De terminal toont per order de keys order_num en order_total. Bewaar die uitvoer voor de controle van je webpagina.
08 - De query verbinden met een HTML-tabel
We brengen nu de bekende onderdelen samen: de route roept een functie aan die de query uitvoert en geeft de lijst dictionaries door aan Jinja. List comprehensions uit hoofdstuk 4 maken ook lijsten met orderlabels en totalen; die gebruiken we straks voor de grafiek.
Vervang app.py volledig door:
from pathlib import Path
import sqlite3
from flask import Flask, render_template
app = Flask(__name__)
DB_PATH = Path(__file__).resolve().parent / "data" / "tysql.sqlite"
def get_orders():
if not DB_PATH.is_file():
raise FileNotFoundError(f"Download eerst de database naar {DB_PATH}")
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
try:
rows = connection.execute("""
SELECT order_num, SUM(quantity * item_price) AS order_total
FROM OrderItems
GROUP BY order_num
ORDER BY order_num
""").fetchall()
return [dict(row) for row in rows]
finally:
connection.close()
@app.route("/")
def index():
orders = get_orders()
labels = ["Order " + str(order["order_num"]) for order in orders]
totals = [order["order_total"] for order in orders]
return render_template("index.html", titel="Orderdashboard", orders=orders,
labels=labels, totals=totals)
if __name__ == "__main__":
app.run(debug=True)
Voor de tabel heb je enkele nieuwe HTML-tags nodig: table begint de tabel, thead de kop, tbody de gegevens, tr een rij, th een kopcel en td een gewone cel.
In Jinja voert {% for order in orders %} een lus uit en sluit {% endfor %} die af. {{ ... }} toont een waarde. Een Jinja-filter staat achter |; "%.2f"|format(...) toont een bedrag met twee decimalen. De gewone Python-f-string en Jinja zijn verschillende opmaaktalen.
Vervang in templates/index.html de alinea onder de titel door deze tabel:
<table>
<thead><tr><th>Ordernummer</th><th>Ordertotaal</th></tr></thead>
<tbody>
{% for order in orders %}
<tr>
<td>{{ order["order_num"] }}</td>
<td>EUR {{ "%.2f"|format(order["order_total"]) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Herlaad de pagina en vergelijk de ordernummers en bedragen met controle_db.py. Als dat overeenkomt, werken Flask, SQL en Jinja samen.
09 - JavaScript en een eerste grafiek
JavaScript is de programmeertaal die in de browser draait. Python voert de query op de server uit; JavaScript tekent de grafiek nadat de browser de HTML heeft ontvangen. Chart.js is een JavaScript-bibliotheek die het tekenwerk verzorgt.
Een canvas is het HTML-tekenvlak. Een id geeft een element een naam. document.getElementById(...) zoekt dat element in de pagina. const maakt een JavaScript-variabele waarvan we de verwijzing niet opnieuw instellen. new Chart(...) maakt het grafiekobject.
JavaScript gebruikt [...] voor lijsten en {naam: waarde} voor objecten met instellingen. Bij Chart.js kiest type het grafiektype, bevat data de labels en gegevensreeksen en regelt options onder meer de schaal. bar is een staafgrafiek.
Jinja’s filter tojson zet onze Python-lijsten om naar JSON die JavaScript kan lezen. De externe script src laadt Chart.js vóór onze eigen code. Daarvoor is internet nodig.
Voeg dit onder de tabel toe, binnen main in templates/index.html. Het moet ná het canvas worden uitgevoerd:
<h2>Ordertotalen per order</h2>
<canvas id="ordersChart"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script>
const labels = {{ labels|tojson }};
const totals = {{ totals|tojson }};
const canvas = document.getElementById("ordersChart");
new Chart(canvas, {
type: "bar",
data: {
labels: labels,
datasets: [{
label: "Ordertotaal",
data: totals,
backgroundColor: "#1f3c88"
}]
},
options: {
responsive: true,
scales: {y: {beginAtZero: true}}
}
});
</script>
Herlaad de browser. Iedere order heeft een staaf waarvan de hoogte overeenkomt met het ordertotaal. De tabel blijft staan als controle.
10 - Controleren, fouten oplossen en uitbreiden
- Controleer eerst de route uit blok 04, daarna de template uit blok 05, de database uit blok 06 en de query uit blok 07. Zo weet je bij welke stap een fout ontstaat.
- Vergelijk bij de eindversie minstens twee ordertotalen in de terminal, HTML-tabel en grafiek. Alle drie komen uit dezelfde query en moeten overeenkomen.
no such table: OrderItemsbetekent meestal dat je een leeg of verkeerd databasebestand gebruikt. Download het juiste bestand naarproject_m/data/tysql.sqlite.- Wel een tabel maar geen grafiek? Controleer internet voor Chart.js en open de browserconsole via de ontwikkelaarstools. Die console toont JavaScript-fouten, terwijl Python-fouten in de terminal staan.
- Kies als uitbreiding eerst een tweede query met dezelfde bekende onderdelen, bijvoorbeeld een totaal per product uit
OrderItems. Controleer de query apart voordat je een tweede grafiek toevoegt.
Het project is afgerond als de tabel en grafiek dezelfde databasegegevens tonen en je het pad browser → route → query → template → grafiek kunt uitleggen. In project N hergebruik je de API-kennis uit L en leer je nieuwe metingen in SQLite op te slaan.
Naslag: Flask quickstart en sqlite3.