Skip to content

Dashboards and data apps

Screens full of numbers, tables and lists: the kind of internal tool, admin panel or media app that many desktop programs are. The first six are Blocks, complete screens written with the components library that you can copy and adapt; each block’s page has its full code and notes on what to change. Every capture follows this site’s theme: switch between light and dark at the top of the page to see them in both.

An admin dashboard: sidebar navigation, KPI cards with trends, a revenue chart, traffic sources and a table of recent orders.

A dashboard with KPI cards, a revenue chart and recent ordersA dashboard with KPI cards, a revenue chart and recent orders
  • KPI cards are ui.stat with a delta and a small trend line. The charts are ready-made components too: ui.bar_chart for revenue (its range is a ui.segmented control) and ui.donut for traffic sources, with a ui.progress bar for the monthly goal.
  • The sidebar switches pages by changing one attribute of the state: see A multi-view app.
Windows, macOS and Linux
uv run --with pyzed-ui python examples/blocks/dashboard.py

The code: Dashboard block.

A team members table with search, status filters, sortable columns, row selection, paging and an export action.

A table of team members with filters, selection and pagingA table of team members with filters, selection and paging
Windows, macOS and Linux
uv run --with pyzed-ui python examples/blocks/data_table.py

The code: Data table block. To learn the parts one by one (search, sort, select, show the details), follow Lists and tables with data, which builds this small book library:

A book library: a searchable, sortable table and the details of the selected book

A project board with columns, labelled cards, assignees, progress, and buttons to move cards along.

A kanban board with three columns of cardsA kanban board with three columns of cards
Windows, macOS and Linux
uv run --with pyzed-ui python examples/blocks/kanban.py

The code: Kanban block.

A messenger with channels, direct messages, a message thread and a composer.

A team chat with channels and a conversationA team chat with channels and a conversation
Windows, macOS and Linux
uv run --with pyzed-ui python examples/blocks/chat.py

The code: Chat block. For a chat that streams answers from an AI model, see the AI chat.

A library sidebar, an album page with its track list, and a player bar whose progress moves while a song plays.

A music player with an album and its tracksA music player with an album and its tracks
Windows, macOS and Linux
uv run --with pyzed-ui python examples/blocks/music_player.py

The code: Music player block.

The layout of a code editor: a project tree, editor tabs with breadcrumbs, a terminal, an outline panel and a status bar.

An IDE layout with a project tree, editor and terminalAn IDE layout with a project tree, editor and terminal
Windows, macOS and Linux
uv run --with pyzed-ui python examples/blocks/file_explorer.py

The code: File explorer block. A working editor on the same idea is the Workspace editor.

A contacts app on SQLAlchemy 2.0 with 20,000 rows in a virtual table. Searching and sorting run as SQL queries in a worker thread (Command.perform), so the window never waits for the database.

The contacts app: totals, a chart of top cities, the table of 20,000 contacts and the edit panel
Windows, macOS and Linux
cd examples/integrations/sqlalchemy_contacts
uv run main.py
Full code: db.py and main.py
examples/integrations/sqlalchemy_contacts/db.py
"""The database: SQLAlchemy 2.0 models on SQLite, plus the queries the window runs."""
from __future__ import annotations
import random
from datetime import datetime
from pathlib import Path
from sqlalchemy import String, create_engine, func, or_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
DATABASE = Path(__file__).with_name("contacts.db")
engine = create_engine(f"sqlite:///{DATABASE}")
class Base(DeclarativeBase):
pass
class Contact(Base):
__tablename__ = "contacts"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(80), index=True)
email: Mapped[str] = mapped_column(String(120), unique=True)
company: Mapped[str] = mapped_column(String(80), default="")
city: Mapped[str] = mapped_column(String(60), default="")
created: Mapped[datetime] = mapped_column(default=datetime.now)
# Sorting by a column title of the table is an ORDER BY on this column.
COLUMNS = {"Name": Contact.name, "Email": Contact.email, "Company": Contact.company, "City": Contact.city}
def setup(rows: int = 20_000) -> None:
"""Creates the table and fills it with made-up contacts the first time."""
Base.metadata.create_all(engine)
with Session(engine) as session:
if session.scalar(select(func.count()).select_from(Contact)):
return
rng = random.Random(7)
first = ["Ada", "Alan", "Grace", "Linus", "Guido", "Margaret", "Ken", "Barbara", "Dennis", "Radia",
"Tim", "Frances", "Bjarne", "Anita", "John", "Hedy", "Yukihiro", "Sophie", "Brendan", "Katherine"]
last = ["Lovelace", "Turing", "Hopper", "Torvalds", "van Rossum", "Hamilton", "Thompson", "Liskov",
"Ritchie", "Perlman", "Berners-Lee", "Allen", "Stroustrup", "Borg", "McCarthy", "Lamarr",
"Matsumoto", "Wilson", "Eich", "Johnson"]
companies = ["Acme", "Globex", "Initech", "Umbrella", "Hooli", "Stark", "Wayne", "Tyrell", "Cyberdyne", "Aperture"]
cities = ["Buenos Aires", "Madrid", "Mexico City", "Bogotá", "Lima", "Santiago", "Montevideo", "Barcelona",
"Berlin", "Tokyo", "Toronto", "Lisbon"]
weights = [12, 10, 9, 7, 6, 5, 4, 4, 3, 3, 2, 2]
session.add_all(
Contact(name=f"{rng.choice(first)} {rng.choice(last)}", email=f"user{i}@example.com",
company=rng.choice(companies), city=rng.choices(cities, weights)[0])
for i in range(rows)
)
session.commit()
def search(text: str, sort: str, descending: bool) -> list[Contact]:
"""Contacts whose name, email or company contains ``text``, sorted in SQL."""
column = COLUMNS[sort]
query = select(Contact).order_by(column.desc() if descending else column, Contact.id)
if text:
pattern = f"%{text}%"
query = query.where(or_(Contact.name.ilike(pattern), Contact.email.ilike(pattern),
Contact.company.ilike(pattern)))
with Session(engine) as session:
return list(session.scalars(query)) # plain objects keep their loaded values after the session closes
def stats() -> dict:
"""Totals and the five cities with more contacts, for the header."""
with Session(engine) as session:
total = session.scalar(select(func.count()).select_from(Contact))
companies = session.scalar(select(func.count(func.distinct(Contact.company))))
cities = session.execute(select(Contact.city, func.count()).group_by(Contact.city)
.order_by(func.count().desc()).limit(5)).all()
return {"total": total, "companies": companies, "cities": [(city, count) for city, count in cities]}
def save(contact_id: int | None, values: dict) -> Contact | dict:
"""Inserts or updates a contact. Returns it, or ``{field: message}`` when it can't be saved."""
errors = {}
if not values["name"].strip():
errors["name"] = "A contact needs a name"
if "@" not in values["email"]:
errors["email"] = "Not an email address"
if errors:
return errors
with Session(engine, expire_on_commit=False) as session:
contact = session.get(Contact, contact_id) if contact_id else Contact()
for key, value in values.items():
setattr(contact, key, value.strip())
session.add(contact)
try:
session.commit()
except IntegrityError:
return {"email": "Another contact already uses this email"}
return contact
def delete(contact_id: int) -> None:
with Session(engine) as session:
contact = session.get(Contact, contact_id)
if contact is not None:
session.delete(contact)
session.commit()
examples/integrations/sqlalchemy_contacts/main.py
"""A contacts manager on SQLAlchemy 2.0: 20,000 rows in a virtual table, searched and sorted in SQL.
uv run main.py
The database (contacts.db) is created next to this file on the first run.
Walkthrough: https://albakore.github.io/pyzed/integrations/sqlalchemy/
"""
from __future__ import annotations
import pyzed as pz
from pyzed import Command, ui
import db
FIELDS = [("name", "Name", "Ada Lovelace"), ("email", "Email", "ada@example.com"),
("company", "Company", "Analytical Engines"), ("city", "City", "London")]
class Contacts(pz.App):
title = "Contacts · SQLAlchemy"
size = (1180, 720)
def __init__(self):
self.rows: list[db.Contact] = []
self.stats = {"total": 0, "companies": 0, "cities": []}
self.query = ""
self.sort, self.descending = "Name", False
self.loading = True
self.editing = None # a Contact, "new" or None
self.draft: dict[str, str] = {}
self.errors: dict[str, str] = {}
# -- database work runs in a thread with Command.perform ------------------------------
def on_start(self):
return Command.perform(db.setup, lambda _: self.reload())
def reload(self):
self.loading = True
query, sort, descending = self.query, self.sort, self.descending
return [
Command.perform(lambda: db.search(query, sort, descending), lambda rows: self.loaded(query, rows)),
Command.perform(db.stats, lambda stats: setattr(self, "stats", stats)),
]
def loaded(self, query: str, rows: list[db.Contact]):
if query == self.query: # a newer search is on its way: keep waiting for it
self.rows, self.loading = rows, False
def search(self, text: str):
self.query = text
return self.reload()
def sort_by(self, title: str):
self.descending = not self.descending if title == self.sort else False
self.sort = title
return self.reload()
# -- the edit panel ------------------------------------------------------------------
def open(self, contact):
self.editing, self.errors = contact, {}
self.draft = {key: "" if contact == "new" else getattr(contact, key) for key, _, _ in FIELDS}
def save(self):
contact_id = None if self.editing == "new" else self.editing.id
values = dict(self.draft)
return Command.perform(lambda: db.save(contact_id, values), self.saved)
def saved(self, result):
if isinstance(result, dict):
self.errors = result
return None
self.editing = None
return self.reload()
def delete(self):
contact_id = self.editing.id
self.editing = None
return Command.perform(lambda: db.delete(contact_id), lambda _: self.reload())
def on_key(self, combo):
if combo == "escape":
self.editing = None
elif combo == "ctrl-n":
self.open("new")
# -- view ----------------------------------------------------------------------------
def header(self):
cities = self.stats["cities"]
return pz.row([
ui.stat("Contacts", f"{self.stats['total']:,}"),
ui.stat("Companies", self.stats["companies"]),
ui.card(pz.column([ui.muted("Top cities", size="sm"),
ui.bar_chart([count for _, count in cities], labels=[c.split()[0] for c, _ in cities],
width=360, height=90)], spacing=6)),
], spacing=12)
def panel(self, t):
if self.editing is None:
return None
rows = [ui.field(title, ui.input(self.draft[key], lambda v, k=key: self.draft.__setitem__(k, v),
placeholder=hint, width=220),
error=self.errors.get(key))
for key, title, hint in FIELDS]
new = self.editing == "new"
return pz.container(
pz.column([
pz.row([ui.heading("New contact" if new else "Edit contact", level=3), pz.hspace(),
ui.icon_button("x", lambda: setattr(self, "editing", None), tooltip="Close (Esc)")],
align="center"),
*rows,
pz.row([None if new else ui.button("Delete", self.delete, icon="trash-2", variant="ghost", tint="error"),
pz.hspace(), ui.button("Save", self.save, icon="check", variant="solid")], spacing=8),
], spacing=14),
padding=16, width=420, height="fill", bg=t.panel,
)
def view(self):
t = self.resolved_theme()
columns = [
ui.Column("Name", "name", width="fill:3"),
ui.Column("Email", "email", width="fill:3"),
ui.Column("Company", "company", width="fill:2"),
ui.Column("City", "city", width="fill:2"),
]
toolbar = pz.row([
ui.search_input(self.query, self.search, placeholder="Search by name, email or company…", width=360),
ui.spinner() if self.loading else ui.muted(f"{len(self.rows):,} found", size="sm"),
pz.hspace(),
ui.button("New contact", lambda: self.open("new"), icon="user-plus", keys="ctrl-n"),
], spacing=10, align="center")
# Rows come sorted from SQL; sort_by only draws the arrow (re-sorting a sorted list is cheap).
table = ui.table(columns, self.rows, sort_by=self.sort, descending=self.descending, on_sort=self.sort_by,
on_row_press=self.open, selected=self.editing, striped=True, height="fill")
main = pz.column([self.header(), toolbar, table], spacing=14, padding=16)
return pz.container(pz.row([main, self.panel(t)]), width="fill", height="fill", bg=t.editor)
if __name__ == "__main__":
pz.run(Contacts)

The walkthrough is in SQLAlchemy. Without any library at all, the Connecting a backend tutorial builds a smaller contacts app over the sqlite3 module that comes with Python, with a repository class, a spinner and friendly errors:

A contacts list loaded from SQLite, with a search box and an add form
  • Live data: a price ticker fed by an async generator.
  • Glass control center: a dashboard with frosted-glass cards over a moving shader.
  • Integrations: FastAPI, Django, SQLModel, httpx and more, each with a desktop app.