Example: Sales dashboard
Live queries and a Chart.js chart from npm, updating in every open window.
- 86 lines of Python
- 1 page
- 2 server functions
- 1099 B page JS (gzip)
This example needs its server (database, sessions or server functions). Run it locally:
Terminal
pyweb dev examples/dashboard/app.pyweb#Source
examples/dashboard/app.pyweb
"""Sales dashboard: live queries and a Chart.js chart from npm.
Open it in two windows and record a sale: both update at once.
Set up with `pyweb add chart.js/auto` (already done here: see pyweb.lock).
"""
import random
from pyweb import App, live, npm, server
from pyweb.browser import watch
from pyweb.db import connect
Chart = npm("chart.js/auto")
app = App(title="Sales dashboard", stylesheets=["/static/app.css"])
db = connect("sqlite:///dashboard.db")
db.execute("create table if not exists sales (id integer primary key, product text, amount real)")
PRODUCTS = ["Lamp", "Desk", "Chair", "Shelf"]
if not db.execute("select count(*) from sales").fetchone()[0]:
with db.transaction():
for i in range(12):
db.execute("insert into sales (product, amount) values (?, ?)", (PRODUCTS[i % 4], 20 + i * 7))
@server
def record(product: str, amount: float) -> None:
if product not in PRODUCTS or not 0 < amount < 10_000:
return
db.execute("insert into sales (product, amount) values (?, ?)", (product, round(amount, 2)))
@server
def simulate() -> None:
"""Ten random sales in one transaction: pages get one update."""
with db.transaction():
for _ in range(10):
db.execute("insert into sales (product, amount) values (?, ?)",
(random.choice(PRODUCTS), round(random.uniform(10, 200), 2)))
@app.page("/")
def Dashboard():
totals = live(db, "select count(*) as n, coalesce(sum(amount), 0) as revenue from sales")
by_product = live(db, "select product, sum(amount) as revenue from sales group by product order by product")
recent = live(db, "select id, product, amount from sales order by id desc limit 6")
canvas = None
chart = None
product = "Lamp"
amount = "49"
def on_mount():
chart = Chart(canvas, {"type": "bar", "options": {"animation": False, "plugins": {"legend": {"display": False}}},
"data": {"labels": [r["product"] for r in by_product],
"datasets": [{"data": [r["revenue"] for r in by_product]}]}})
watch(lambda: by_product, redraw)
def redraw(rows):
chart.data.labels = [r["product"] for r in rows]
chart.data.datasets[0].data = [r["revenue"] for r in rows]
chart.update()
def add():
record(product, float(amount or 0))
<main>
<h1>Sales</h1>
<div class="row">
<div class="card-stat"><b id="count">{totals[0]["n"]}</b> sales</div>
<div class="card-stat"><b id="revenue">{f"${totals[0]['revenue']:,.2f}"}</b> revenue</div>
</div>
<canvas id="chart" ref={canvas} height="160"></canvas>
<form onsubmit={add}>
<select bind={product}>
for p in PRODUCTS:
<option value={p}>{p}</option>
</select>
<input bind={amount} type="number" step="0.01" min="0" style={{"max-width": "120px"}} />
<button>Record sale</button>
</form>
<button id="simulate" onclick={lambda: simulate()}>Simulate 10 sales</button>
<h2>Latest</h2>
<ul id="recent">
for s in recent:
<li><span>{s["product"]}</span><span class="muted">{f"${s['amount']:.2f}"}</span></li>
</ul>
</main>
#What runs where
Output of pyweb inspect: every page variable, handler and server function, where it runs and why.
Output
page Dashboard route=/
browser totals reactive state: a live query: the server sends new rows when its tables change; initial value computed on the server and sent
browser by_product reactive state: a live query: the server sends new rows when its tables change; initial value computed on the server and sent
browser recent reactive state: a live query: the server sends new rows when its tables change; initial value computed on the server and sent
browser canvas reactive state: set to an element by ref= (line 71); literal initial value
browser chart reactive state: assigned in on_mount(); literal initial value
browser product reactive state: bound to an input (line 73); literal initial value
browser amount reactive state: bound to an input (line 77); literal initial value
browser on_mount event handler (compiled to JavaScript)
browser redraw event handler (compiled to JavaScript)
browser add event handler (compiled to JavaScript)
rpc POST /__pyweb/rpc/record (product: str, amount: float) -> None
rpc POST /__pyweb/rpc/simulate () -> None#Generated JavaScript
The browser modules the compiler wrote (before minification). They import the shared runtime.
JavaScript
// Dashboard.js
import { h as $h, t as $t, dyn as $dyn, list as $list, when as $when, signal as $signal, computed as $computed, mount as $mount, onMount as $onMount, py as $py, rpc as $rpc, subscribe as $subscribe, slot as $slot, onCleanup as $onCleanup, live as $live } from "./runtime.js";
import $npm_chart_js_auto_default_336847 from "chart.js/auto";
const PRODUCTS = ["Lamp", "Desk", "Chair", "Shelf"];
function Dashboard($s) {
const totals = $signal("totals" in $s ? $s["totals"] : null);
const by_product = $signal("by_product" in $s ? $s["by_product"] : null);
const recent = $signal("recent" in $s ? $s["recent"] : null);
const canvas = $signal("canvas" in $s ? $s["canvas"] : null);
const chart = $signal("chart" in $s ? $s["chart"] : null);
const product = $signal("product" in $s ? $s["product"] : "Lamp");
const amount = $signal("amount" in $s ? $s["amount"] : "49");
function on_mount() {
chart($py.call($npm_chart_js_auto_default_336847, [canvas(), {"type": "bar", "options": {"animation": false, "plugins": {"legend": {"display": false}}}, "data": {"labels": $py.iter(by_product()).map((r) => ($py.at(r, "product"))), "datasets": [{"data": $py.iter(by_product()).map((r) => ($py.at(r, "revenue")))}]}}]));
$py.watch((() => (by_product())), redraw);
}
function redraw(rows) {
$py.setp(chart, ["data", "labels"], $py.iter(rows).map((r) => ($py.at(r, "product"))));
$py.setp(chart, ["data", "datasets", 0, "data"], $py.iter(rows).map((r) => ($py.at(r, "revenue"))));
$py.mut(chart, [], ($v) => $py.m($v, "update"));
}
async function add() {
(await $rpc("record", {"product": product(), "amount": $py.float($py.or(amount(), () => 0))}));
}
$live(totals, $s["$live:totals"]);
$live(by_product, $s["$live:by_product"]);
$live(recent, $s["$live:recent"]);
$onMount(on_mount);
return [$h("main", null, () => [
$h("h1", null, () => [$t("Sales")]),
$h("div", {"class": "row"}, () => [
$h("div", {"class": "card-stat"}, () => [
$h("b", {"id": "count"}, () => [$dyn(() => $py.at($py.at(totals(), 0), "n"))]),
$t(" sales")
]),
$h("div", {"class": "card-stat"}, () => [
$h("b", {"id": "revenue"}, () => [$dyn(() => `$${$py.format($py.at($py.at(totals(), 0), "revenue"), ",.2f")}`)]),
$t(" revenue")
])
]),
$h("canvas", {"id": "chart", "$ref": canvas, "height": "160"}),
$h("form", {"onsubmit": add}, () => [
$h("select", {"$bind": product}, () => [$list(() => PRODUCTS, (p) => [$h("option", {"value": p}, () => [$t($py.text(p))])])]),
$h("input", {"$bind": amount, "type": "number", "step": "0.01", "min": "0", "style": {"max-width": "120px"}}),
$h("button", null, () => [$t("Record sale")])
]),
$h("button", {"id": "simulate", "onclick": (async () => ((await $rpc("simulate", {}))))}, () => [$t("Simulate 10 sales")]),
$h("h2", null, () => [$t("Latest")]),
$h("ul", {"id": "recent"}, () => [$list(() => recent(), (s) => [$h("li", null, () => [
$h("span", null, () => [$t($py.text($py.at(s, "product")))]),
$h("span", {"class": "muted"}, () => [$t($py.text(`$${$py.format($py.at(s, "amount"), ".2f")}`))])
])])])
])];
}
$mount("Dashboard", Dashboard);