GitHub

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);
Edit this page on GitHub