SPEC.md: rewrite Pivot page description to cover named layouts, depth control, selection mode, inspector filtering, and layout persistence. docs/perspective-pivot.md: new file documenting all discovered Perspective v4.4.0 APIs — viewer/plugin/view methods, selection modes, set_depth mechanism, perspective-click event shape, full state save/restore pattern, and common pitfalls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
286 lines
10 KiB
Markdown
286 lines
10 KiB
Markdown
# Perspective Pivot — Technical Reference
|
|
|
|
Version tested: `@perspective-dev` v4.4.0 (client, viewer, viewer-datagrid, viewer-d3fc), loaded from CDN.
|
|
|
|
This document captures everything learned about controlling Perspective programmatically. The official docs are incomplete for some of these APIs — treat this as a ground-truth supplement.
|
|
|
|
---
|
|
|
|
## Loading from CDN
|
|
|
|
```js
|
|
const [{ default: perspective }] = await Promise.all([
|
|
import('https://cdn.jsdelivr.net/npm/@perspective-dev/client@4.4.0/dist/cdn/perspective.js'),
|
|
import('https://cdn.jsdelivr.net/npm/@perspective-dev/viewer@4.4.0/dist/cdn/perspective-viewer.js'),
|
|
import('https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-datagrid@4.4.0/dist/cdn/perspective-viewer-datagrid.js'),
|
|
import('https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-d3fc@4.4.0/dist/cdn/perspective-viewer-d3fc.js'),
|
|
])
|
|
```
|
|
|
|
Stylesheet:
|
|
```html
|
|
<link rel="stylesheet" crossorigin="anonymous"
|
|
href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/themes.css" />
|
|
```
|
|
|
|
---
|
|
|
|
## Core Objects
|
|
|
|
```
|
|
perspective — the module default export
|
|
.worker() — creates a Web Worker instance
|
|
|
|
worker
|
|
.table(rows, opts) — creates a named Table; returns the Table object
|
|
.open_table(name) — re-opens a previously created named table
|
|
|
|
table
|
|
.view(config) — creates a View (filtered/grouped projection)
|
|
.update(rows) — incremental row upsert/insert
|
|
|
|
view
|
|
.to_json() — returns rows as array of objects
|
|
.set_depth(n) — sets expansion depth for all grouped rows (see below)
|
|
.delete() — frees the view; always call when done
|
|
|
|
viewer (the <perspective-viewer> DOM element)
|
|
.load(worker) — attaches the worker to the viewer
|
|
.save() — returns full viewer config as plain object
|
|
.restore(config) — applies a config object to the viewer
|
|
.flush() — forces viewer to synchronize (limited effect on plugin state)
|
|
.getPlugin() — returns the active plugin element (e.g. datagrid)
|
|
.getView() — returns the current View object
|
|
.toggleConfig() — shows/hides the settings panel
|
|
|
|
plugin (datagrid element, from viewer.getPlugin())
|
|
.save() — returns plugin-specific state: { columns, scroll_lock, edit_mode }
|
|
.restore(config) — applies plugin-specific state
|
|
.draw(view) — redraws the plugin against the given View
|
|
```
|
|
|
|
---
|
|
|
|
## viewer.save() — Config Shape
|
|
|
|
```js
|
|
{
|
|
table: "source_name",
|
|
plugin: "datagrid", // or "d3_y_bar", etc.
|
|
plugin_config: { ... }, // NOT reliably populated — use plugin.save() instead
|
|
group_by: ["field1"],
|
|
split_by: ["field2"],
|
|
columns: ["Amount"],
|
|
filter: [["field", "op", "value"]],
|
|
sort: [["field", "asc"]],
|
|
expressions: { "ExprName": "// formula\n..." },
|
|
settings: false, // whether the config panel is open
|
|
}
|
|
```
|
|
|
|
**Important:** `plugin_config` in `viewer.save()` is NOT reliably populated in v4.4.0. Use `plugin.save()` separately to capture plugin state.
|
|
|
|
---
|
|
|
|
## plugin.save() — Plugin State Shape (datagrid)
|
|
|
|
```js
|
|
{
|
|
columns: {}, // per-column formatting overrides
|
|
scroll_lock: false,
|
|
edit_mode: "SELECT_REGION" // see valid values below
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Selection Modes (edit_mode)
|
|
|
|
Valid values for the datagrid plugin's `edit_mode` field:
|
|
|
|
| Value | Button label | Behavior |
|
|
|---|---|---|
|
|
| `READ_ONLY` | Read-Only | No selection highlight |
|
|
| `SELECT_ROW` | Select Row | Highlights full rows |
|
|
| `SELECT_COLUMN` | Select Column | Highlights full columns |
|
|
| `SELECT_REGION` | Select Region | Highlights clicked cell region |
|
|
| `EDIT` | Edit | Enables cell editing |
|
|
|
|
The built-in button in the viewer toolbar cycles through these in order.
|
|
|
|
**Setting the default:**
|
|
```js
|
|
// After viewer.restore(...), set it directly on the plugin:
|
|
const plugin = await viewer.getPlugin()
|
|
await plugin.restore({ edit_mode: 'SELECT_REGION' })
|
|
```
|
|
|
|
Setting via `viewer.restore({ plugin_config: { edit_mode: ... } })` does NOT reliably work in v4.4.0.
|
|
|
|
---
|
|
|
|
## Expand/Collapse Row Depth
|
|
|
|
Controls how many levels of the `group_by` hierarchy are expanded. This is the only working mechanism found in v4.4.0:
|
|
|
|
```js
|
|
const view = await viewer.getView()
|
|
await view.set_depth(depth) // 0 = collapse all, 1 = expand one level, etc.
|
|
const plugin = await viewer.getPlugin()
|
|
await plugin.draw(view) // required — viewer does not redraw automatically
|
|
```
|
|
|
|
**What does NOT work:**
|
|
- `viewer.restore({ plugin_config: { expand_depth: d } })` — silently ignored
|
|
- `view.set_depth(d)` alone — view state changes but display doesn't update
|
|
- `view.set_depth(d)` + `viewer.flush()` — still no visual update
|
|
- `plugin.restore({ expand_depth: d })` — "Unknown" field, ignored
|
|
|
|
**The `plugin.draw(view)` call is required** to make the datagrid re-render after `set_depth`.
|
|
|
|
---
|
|
|
|
## Saving and Restoring Full State
|
|
|
|
To capture complete state (viewer + plugin + expand depth):
|
|
|
|
```js
|
|
async function captureConfig(viewer, expandDepth) {
|
|
const plugin = await viewer.getPlugin()
|
|
const [viewerConfig, pluginConfig] = await Promise.all([viewer.save(), plugin.save()])
|
|
return { ...viewerConfig, plugin_config: pluginConfig, expand_depth: expandDepth }
|
|
}
|
|
```
|
|
|
|
To restore:
|
|
```js
|
|
async function restoreConfig(viewer, config, applyDepth) {
|
|
await viewer.restore(config)
|
|
if (config.plugin_config) {
|
|
const plugin = await viewer.getPlugin()
|
|
await plugin.restore(config.plugin_config)
|
|
}
|
|
if (config.expand_depth != null) {
|
|
await applyDepth(viewer, config.expand_depth)
|
|
}
|
|
await viewer.flush()
|
|
}
|
|
|
|
async function applyDepth(viewer, depth) {
|
|
const view = await viewer.getView()
|
|
await view.set_depth(depth)
|
|
const plugin = await viewer.getPlugin()
|
|
await plugin.draw(view)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## The perspective-click Event
|
|
|
|
Fires when the user clicks a cell. The event detail:
|
|
|
|
```js
|
|
viewer.addEventListener('perspective-click', async (e) => {
|
|
const { row, column_names, config } = e.detail
|
|
// row — aggregated values for the clicked cell (keyed by "split|metric" format)
|
|
// column_names — array of metric column names clicked
|
|
// config — { filter: [[field, op, value], ...] }
|
|
// filter includes:
|
|
// - group_by coordinate filters (field == value, one per group_by level)
|
|
// - split_by coordinate filters (field == value, one per split_by field)
|
|
// - user-set filters (any op)
|
|
})
|
|
```
|
|
|
|
`__ROW_PATH__` in `row` contains the group_by path as an array.
|
|
|
|
**The `config.filter` array is the reliable way to get cell coordinates.** Do not try to zip `__ROW_PATH__` with `group_by` — the filter approach handles all cases including partial paths.
|
|
|
|
---
|
|
|
|
## Filtering Rows for a Clicked Cell
|
|
|
|
The click event's `filter` array can be applied to the underlying table via a new View, which correctly evaluates expression/computed columns (unlike filtering raw JS rows):
|
|
|
|
```js
|
|
const config = await viewer.save()
|
|
const view = await table.view({
|
|
filter: eventFilters,
|
|
expressions: config.expressions || [],
|
|
})
|
|
const rows = await view.to_json()
|
|
await view.delete()
|
|
|
|
// Strip expression columns from results (they're computed, not source fields)
|
|
const exprNames = new Set(Object.keys(config.expressions || {}))
|
|
const clean = rows.map(r =>
|
|
Object.fromEntries(Object.entries(r).filter(([k]) => !exprNames.has(k)))
|
|
)
|
|
```
|
|
|
|
**Why not filter raw JS rows?** Expression columns (computed in Perspective) don't exist in the source data. `filterRowsByConfig` on raw rows will skip those filters, returning all rows for the group rather than the specific cell.
|
|
|
|
**Guard against no group_by:** Without `group_by`, the filter array has no coordinate filters and the view query returns the entire table (slow). Check first:
|
|
|
|
```js
|
|
const config = await viewer.save()
|
|
if ((config.group_by || []).length === 0) return // no hierarchy — skip inspector
|
|
```
|
|
|
|
---
|
|
|
|
## Viewer Methods (full list, v4.4.0)
|
|
|
|
From `Object.getOwnPropertyNames(Object.getPrototypeOf(viewer))`:
|
|
|
|
`constructor`, `__destroy_into_raw`, `free`, `__get_model`, `connectedCallback`, `copy`, `delete`, `download`, `eject`, `export`, `flush`, `getAllPlugins`, `getClient`, `getEditPort`, `getPlugin`, `getRenderStats`, `getSelection`, `getTable`, `getView`, `getViewConfig`, `load`, `openColumnSettings`, `reset`, `resetError`, `resetThemes`, `resize`, `restore`, `restyleElement`, `save`, `setAutoPause`, `setAutoSize`, `setSelection`, `setThrottle`, `toggleColumnSettings`, `toggleConfig`
|
|
|
|
## Plugin Methods (datagrid, full list)
|
|
|
|
From `Object.getOwnPropertyNames(Object.getPrototypeOf(plugin))`:
|
|
|
|
`constructor`, `connectedCallback`, `disconnectedCallback`, `activate`, `name`, `category`, `select_mode`, `min_config_columns`, `config_column_names`, `group_rollups`, `priority`, `can_render_column_styles`, `column_style_controls`, `draw`, `update`, `render`, `resize`, `clear`, `save`, `restore`, `restyle`, `delete`
|
|
|
|
## View Methods (full list)
|
|
|
|
From `Object.getOwnPropertyNames(Object.getPrototypeOf(view))` — includes `set_depth`, `expand`, `collapse`, `to_json`, `to_csv`, `to_arrow`, `schema`, `num_rows`, `num_columns`, `delete`, and others.
|
|
|
|
---
|
|
|
|
## settings Panel
|
|
|
|
The `settings` key in `viewer.restore()` controls whether the config panel (gear icon) is open:
|
|
|
|
```js
|
|
// Hide on load:
|
|
await viewer.restore({ table: "name", settings: false, plugin_config: DEFAULT_PLUGIN_CONFIG })
|
|
|
|
// Toggle programmatically:
|
|
viewer.toggleConfig()
|
|
```
|
|
|
|
The settings state is saved by `viewer.save()` and restored on `viewer.restore()`, so it persists across layout saves automatically.
|
|
|
|
---
|
|
|
|
## Incremental Updates
|
|
|
|
To update the table data without a full reload:
|
|
|
|
```js
|
|
table.update(newRows) // upserts by index (or by index_col if specified at table creation)
|
|
```
|
|
|
|
The viewer re-renders automatically after `table.update()`.
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
- **`plugin_config` in `viewer.restore()` is unreliable.** Always set plugin state via `plugin.restore()` separately after `viewer.restore()`.
|
|
- **`view.set_depth()` requires `plugin.draw(view)`.** The viewer won't redraw automatically.
|
|
- **Expression columns don't exist in raw data.** Filter via a Perspective View (`table.view({ filter, expressions })`), not against raw JS rows.
|
|
- **Always `await view.delete()`** after using a temporary view, or you'll leak worker memory.
|
|
- **Named tables:** `worker.table(rows, { name: 'foo' })` — the name is used by the viewer's `table` config key. Re-open with `worker.open_table('foo')`.
|