All docs
Data views represent data
A data view renders data as a table, chart, or flowchart via to_text()/to_image(). Feed it, return one from @tool.view, and present it with show().
A data view is a View with a kind — a representation of data. You give it data with
feed(data) and it renders — as text for an agent or the CLI, as an image for a report, or as a live
control in the desktop. A data view is just a View with data on top of its
param() config and @action operations.
The three built-in kinds#
Get a ready-made data view from the views façade. Each factory imports its package lazily, so
importing lsdtools never pulls in cairo or a view package:
from lsdtools import views, Table
# a data grid
grid = views.table(Table({"name": ["ann", "bob"], "score": [12, 30]}))
# a chart — kind is bar / stacked_bar / line / area / scatter / histogram / box / heatmap
chart = views.chart(kind="line", title="Loss")
chart.feed({"series": [{"name": "train", "x": [0, 1, 2, 3], "y": [0.9, 0.6, 0.4, 0.25]}]})
# a flowchart from the indented text DSL
flow = views.flowchart("A\n B\n C")
print(grid.to_text())
print(chart.to_text())
views.table(data), views.chart(data, kind=...), and views.flowchart(source) all accept
their data inline or you feed it later with feed(...).
Interaction — the same state, in the desktop and headless#
A data view is not render-once. In the desktop each is a live, interactive widget; and because every
interaction is a param() or a method on the view, an agent, a test, or a script drives the exact
same behaviour with no display — set the param, read the result.
Table — sort, filter, select, export:
grid = views.table(t, selection="multi")
grid.sort_by, grid.sort_desc = "score", True # click a column header in the desktop
grid.filter = "an" # the search box; case-insensitive, across columns
grid.select_rows([0, 2]) # row selection (selection= none/single/multi)
picked = grid.get_selection() # a Table of the selected rows
csv = grid.to_csv() # copy / export the current sorted+filtered view
Sort, filter, and paging compose — page_table() / row_count() reflect the filtered set.
Chart — new stacked_bar and area kinds, axis titles, and a togglable legend:
chart = views.chart(kind="stacked_bar", title="Grades", x_label="bucket", y_label="count")
chart.toggle_series("train") # what a legend click does; or set_series_hidden(name, True)
chart.hidden_series # -> ["train"] — hidden series drop out of the ranges and marks,
# but stay greyed in the legend so you can toggle them back
In the desktop the chart also zooms (scroll), pans (drag), and shows a nearest-point hover tooltip.
Flowchart — node selection and hover:
flow.select(node_id) # what a click does; add additive=True for Ctrl-click multi-select
flow.selected_ids # -> ["n1"]; also clear_selection() and set_hovered(node_id)
Selecting emits a selection-changed view event, so a host or agent can react to it.
Headless renderings#
Every data view answers to_text() (a plain-text view for logs, agents, and tests) and to_image()
(a PNG — returns the bytes, or writes to a path):
print(grid.to_text())
# name score
# ---- -----
# ann 12
# bob 30
grid.to_image("grades.png") # or: png_bytes = grid.to_image()
Contribute one with @tool.view#
@tool.view decorates a factory (ctx, entity=None) -> View. The desktop mounts it as an editor
tab and resolves its shell (a headless host uses to_text() / to_image() instead). Returning
anything other than a View is a mount error — a package never touches GTK directly:
from lsdtools import Tool, Context, views
tool = Tool("grade", label="Grade tools")
@tool.view("grade.scores", title="Scores", location="editor")
def scores_view(ctx: Context, entity=None):
return views.table(title="Scores").watch(entity) # follows the entity — refreshes on re-run
Give the view the entity with watch(entity), not views.table(entity.output):
entity.output reads the table once at mount and never updates, while watch holds the entity's
live output and
refreshes on every re-run (and pulls immediately if the entity has already run). watch returns the
view, so views.table(title=...).watch(entity) is one phrase. It is the same verb a
deliver step's ViewerPayload is routed through.
Present one with the engine#
Standalone, show() the data view, or attach it to the engine. engine.add(a_view) then
engine.show() is the exact mirror of engine.add(entity) then engine.run():
from lsdtools import Engine, views, Table
t = Table({"name": ["ann", "bob"], "score": [12, 30]})
views.table(t, title="Grades").show() # standalone window (or text when headless)
engine = Engine()
engine.add(views.chart(kind="histogram", title="Scores"))
engine.show()
show(mode="text") prints the text rendering, so a view is testable without a display.
Related#
Learn — Everything you show is a View · Custom views · Params & actions
Start — How an LSD package fits together · Naming: which is which
Examples — A dockable panel · A complete package
Frequently asked questions
What kinds of data view ship with lsdtools?
Three, through the views façade — table, chart (kind = bar/stacked_bar/line/area/scatter/histogram/box/heatmap), and flowchart. Each lives in its own package that the façade imports lazily, so importing lsdtools never drags in cairo or a view package. For anything else, subclass View (see Custom views).
Are the data views interactive, or render-once?
Both. Each answers to_text()/to_image() headlessly, and in the desktop each is fully interactive — the table sorts, filters, selects, and exports; the chart zooms, pans, shows hover tooltips, and toggles series from the legend; the flowchart selects and highlights nodes. Every interaction is driven by a param or method on the view (sort_by, filter, toggle_series, select…), so an agent or a test drives the same behaviour without a display.
What makes a data view different from a plain View?
Both are a View. A View with no kind is param() fields plus @action buttons, controls with no data. A data view has a kind — it adds feed(data) plus to_text()/to_image() to represent a result. Use a data view to show a result, a plain View to take input.