All docs
Everything you show is a View
The show side of LSD is one declarative View class: with a kind it represents data, with no kind it collects input, and show() presents either. None import GTK.
LSD splits everything an Engine holds into two kinds of thing: entities you run (pipelines)
and views you show (UI). This page is the show side. See
How an LSD package fits together for the whole picture and
Naming: which is which for the exact words.
The show side is one declarative class, View. A View does one of two things — it represents
data (a table, chart, or flowchart) or collects input (params + actions) — and you present
it with show(). There is no separate "panel" type; the difference is simply whether the View has a
kind. None of it imports GTK — the desktop registers a renderer and LSD stays front-end-agnostic,
so the same View works in a window or as console text.
One View, two shapes#
| The View | Is | You author it with | Headless rendering |
|---|---|---|---|
| Represents data | a View with a kind — a table, chart, flowchart, or custom |
@tool.view → a View |
to_text() / to_image() |
| Collects input | a View with no kind — param() fields + @action (the base View; informally a panel when docked in the sidebar) |
@tool.view → a View |
to_text() (params + actions) |
Where a View lands is set by location= (editor / sidebar / overlay), and any View is
presented with a_view.show() or engine.add(a_view) + engine.show().
Data view — represent data#
A data view is a View with a kind. It takes data through feed(data) and renders it. The
three built-in kinds come from the views façade — views.table, views.chart,
views.flowchart — and you can subclass View for a custom one. Every data
view answers to_text() and to_image(), so an agent or the CLI sees it with no display:
from lsdtools import views
w = views.chart(kind="line", title="Loss")
w.feed({"series": [{"name": "train", "x": [0, 1, 2, 3], "y": [0.9, 0.6, 0.4, 0.25]}]})
print(w.to_text())
# → [line] Loss
# train: 4 pts, min=0.25 max=0.9 last=0.25
feed(data) is for literal data. To make a data view follow a pipeline, hand it an entity with
one verb — watch — instead of feeding a one-shot snapshot:
grid = views.table(title="Grades").watch(entity) # holds the entity's output; refreshes on re-run
watch reads the entity's live output and refreshes on every re-run (views.table(entity.output)
would read once and never update). watch returns the view, so it chains as one phrase, and the
framework wires the event bus for you. It is the same verb a deliver step's
ViewerPayload is routed through.
Collect input — a View with no kind#
A View with no kind is param() fields plus @action buttons — the base View, for collecting
input and running actions. Where it shows is location= (the desktop docks a sidebar one at the
side — what people informally call a panel — but that's just placement). An action handler reads the
fields as typed attributes; there is no values dict to keep in sync. See
A View of params and actions for the full story.
from lsdtools import View, action, param
class ClipPanel(View):
cutoff = param(float, default=0.5, min=0.0, max=1.0, label="Cut-off")
@action(label="Apply", style="suggested")
def apply(self):
return f"keeping grades ≥ {self.cutoff}"
ClipPanel(cutoff=0.7).invoke("apply") # → 'keeping grades ≥ 0.7'
Presenting a View#
Every View has the base verb show(): a window when a front-end is loaded, a console rendering
otherwise. Attaching a view to the engine mirrors attaching an entity — engine.add(a_view) then
engine.show() is the exact partner of engine.add(entity) then engine.run():
from lsdtools import Engine, views, Table
t = Table({"name": ["ann", "bob"], "score": [12, 30]})
engine = Engine()
engine.add(views.table(t, title="Grades"))
engine.show() # a window, or console text when headless
a_view.show(mode="text") always prints the text rendering, which is what makes a view testable
without a display.
The inheritance chain#
One short chain underlies it all, which is why param() behaves the same everywhere:
Configurable— anything withparam()fields (steps, entities, the engine).View— aConfigurablethat also has@actionbuttons and optional data (feed+to_text/to_image). With akindit represents data; with none it is an input form.
Steps are Configurables, not Views — they have parameters but no actions. See
Naming: which is which for the overloaded words (View the class vs
@tool.view the decorator, views.table vs the Table class).
Related#
Learn — Params & actions · Data views · Custom views
Start — How an LSD package fits together · Naming: which is which
Examples — A dockable panel · A complete package
Frequently asked questions
Are panels and views two different things?
No — there is one View class. A View with a kind (table/chart/flowchart/custom) represents data via feed() + to_text()/to_image(); a View with no kind collects input — param() fields plus @action buttons. "Panel" is just informal shorthand for that no-kind View when it is docked in the sidebar; there is no separate Panel class or @tool.panel decorator. show() presents either; neither imports GTK.
Do I need a display to build or test a surface?
No. A View introspects headless (get_parameters / get_actions) and, when it has a kind, renders with to_text() / to_image(). View.show(mode="text") prints the console rendering, so the same code runs in the CLI, in CI, or from an agent. GTK only enters when the desktop registers the renderer.