# Data views represent data
URL: https://lsd.tools/docs/ui-widgets
Updated: 2026-07-18

> A data view is a View with a kind that represents data. Get one from the views façade — views.table, views.chart(kind=...), views.flowchart — and give it data with feed() (for literal data) or, to follow a pipeline, watch(entity) — which holds the entity's live output and refreshes on every re-run. Every data view answers to_text() and to_image(), so an agent or the CLI sees it without a display. @tool.view returns a View for the desktop to draw; standalone, show() it, or attach it with engine.add(a_view) then engine.show() — the mirror of engine.add(entity) + engine.run().

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`](/docs/ui-overview) 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:

```python
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:

```python
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:

```python
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:

```python
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):

```python
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:

```python
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)`](/docs/api-view-watch), 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`](/docs/tools-deliver) 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()`:

```python
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](/docs/ui-overview) · [Custom views](/docs/ui-custom-views) · [Params & actions](/docs/ui-panels)

**Start** — [How an LSD package fits together](/docs/start-architecture) · [Naming: which is which](/docs/tools-glossary)

**Examples** — [A dockable panel](/docs/ex-panel) · [A complete package](/docs/ex-full-package)

## FAQ
### 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.
