Custom Widgets
A widget is a Python class building a payload from events, and a JavaScript renderer drawing it. Every built-in widget is made this way. This guide builds a gauge.
1- Write the class
Subclass Widget with a type of its own. The class is registered when it's defined.
# gauge.py
from dataclasses import KW_ONLY, dataclass
from pathlib import Path
from typing import ClassVar
from dachshund.widgets import Widget
@dataclass
class Gauge(Widget):
_: KW_ONLY
y: str = "value"
type: ClassVar[str] = "gauge"
op: ClassVar[str] = "replace"
script: ClassVar[Path] = Path(__file__).parent / "gauge.js"
op = "replace"redraws the card on every event.op = "append"adds one point per event, throughupdate(event). See LineChart for an example.
2- Build the payload
render(events) gets every kept event of the widget's source and returns what the renderer draws. self.value(event, field) reads a field as a number, None when missing or not numeric.
def render(self, events):
return {"value": self.value(events[-1], self.y) if events else None}
3- Draw it in the browser
script is served with the page. It registers a renderer under the widget's type, with mount(card, payload) for the first draw and replace(state, payload) or append(state, point) to match op.
// gauge.js
(() => {
"use strict";
window.Dachshund.widget("gauge", {
mount(card, payload) {
const container = card.querySelector(".card-content");
container.textContent = payload.value ?? "-";
return { container };
},
replace(state, payload) {
state.container.textContent = payload.value ?? "-";
},
});
})();
mount returns whatever state the later calls need. A style class attribute ships a stylesheet the same way:
style: ClassVar[Path] = Path(__file__).parent / "gauge.css"
4- Add it
import dachshund
from gauge import Gauge
with dachshund.row():
dachshund.add(Gauge("cpu"))
dachshund.emit("cpu", value=42)
The built-in charts, in dachshund/widgets/chart/, make a good second example.
Aggregating
A widget showing one number can subclass AggregatedWidget instead. It takes y, aggregate and over like Number, works out the number, and only asks for the payload:
from dataclasses import dataclass
from datetime import timedelta
from pathlib import Path
from typing import ClassVar
from dachshund.widgets import AggregatedWidget
@dataclass
class Gauge(AggregatedWidget):
type: ClassVar[str] = "gauge"
script: ClassVar[Path] = Path(__file__).parent / "gauge.js"
def render_value(self, value):
return {"value": value}
with dachshund.row():
dachshund.add(
Gauge("latency", y="ms", aggregate="average", over=timedelta(hours=1))
)
value is None before there's anything to show. See Aggregating for the aggregates.
Persisting
To keep a widget's state across restarts, subclass PersistableWidget and write its three hooks. They're only called on a widget built with persist=True.
from dataclasses import dataclass, field
from dachshund.widgets import PersistableWidget
@dataclass
class Gauge(PersistableWidget):
...
last: float | None = field(default=None, init=False, repr=False, compare=False)
def accumulate(self, event):
value = self.value(event, self.y)
if value is not None:
self.last = value
def snapshot(self):
return {"last": self.last}
def restore(self, data):
self.last = data.get("last")
def render(self, events):
if self.persist:
return {"value": self.last}
return {"value": self.value(events[-1], self.y) if events else None}
It's built with persist=True, on a dashboard with a storage, see Persistent Storage:
with dachshund.row():
dachshund.add(Gauge("cpu", persist=True))
For state with one entry per key, subclass KeyedWidget instead. It provides the three hooks.