All docs
Custom views
Subclass View to render your own data: set kind, implement feed() and to_text()/to_image(), and register the GTK shell with set_view_shell.
The three built-in data views cover most cases, but you can render anything by
subclassing View. A custom view keeps the split that makes every view
testable: the representation (data + text/image) is headless, and the GTK shell that draws
it lives apart, in the one place a package is allowed to write GTK.
Subclass View#
Set a class-level kind (its registry key), implement feed(data) to take the data, and
to_text() / to_image() to render it. Config that changes presentation stays in param()
fields. Defining the subclass registers it by kind (redefining a kind overrides it):
# grade_band/view.py
from lsdtools import View, param
class GradeBandView(View):
kind = "grade_band" # its registry key
cutoff = param(float, default=0.5, min=0.0, max=1.0, label="Cut-off")
def feed(self, data):
"""Store the data (a Table of {name, grade}) and notify any shell to redraw."""
self._rows = data.to_dicts()
self._data_changed()
def to_text(self):
above = [r for r in self._rows if r["grade"] >= self.cutoff]
return f"{len(above)}/{len(self._rows)} rows at or above {self.cutoff}"
_data_changed() fires a view event that a shell subscribes to (redraw). Add to_image(self, path=None, *, width=1024, height=768) when you want a PNG rendering; the base raises
NotImplementedError for whichever you don't provide.
Run it headless#
The view imports no GTK, so it renders anywhere — feed it a Table and read
to_text(), or attach it to an Engine:
from lsdtools import Table
from grade_band.view import GradeBandView
w = GradeBandView(cutoff=0.6)
w.feed(Table({"name": ["ann", "bob", "cy"], "grade": [0.8, 0.4, 0.65]}))
print(w.to_text())
# → 2/3 rows at or above 0.6
w.show("text")
Register the GTK shell#
The drawing code lives in your view's gtk module — the one sanctioned place a package writes
GTK. It calls set_view_shell(kind, factory), where the factory turns a view instance into a
control. Import GTK inside the factory so the module stays safe to load headless:
# grade_band/gtk.py — loaded by the desktop when a display is present
from lsdtools.views import set_view_shell
def build_grade_band(view):
import gi # GTK stays inside the factory
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk
area = Gtk.DrawingArea()
# draw view._rows against view.cutoff; subscribe to redraw:
view.on_view_event("data-changed", area.queue_draw)
return area
def register():
set_view_shell("grade_band", build_grade_band)
The desktop calls register() when it loads your view package. Until a shell exists for a kind,
headless consumers still work through to_text() / to_image(); only building the live control
needs the shell. That is the design: check and ship the data half first, add pixels later.
Related#
Learn — Data views · Everything you show is a View · Params & actions
Start — How an LSD package fits together · Naming: which is which
Examples — A dockable panel · A complete package </content>
Frequently asked questions
Why is the GTK shell registered separately from the view?
So the view stays headless. The View subclass — its data, its to_text()/to_image() — imports no GTK, so it runs in the CLI, in CI, and from an agent. The drawing shell is a separate concern registered with set_view_shell in the view's gtk module, the one sanctioned place a package writes GTK; the desktop loads it only when a display is present.
What happens if no shell is registered for my kind?
Headless consumers still work — they call your to_text() / to_image(). Only building the live GTK control needs the shell; without it that path raises a clear error telling you to install the view package (whose gtk module registers the shell). Data and text first, pixels later.