Saving and restoring UI state.
<perspective-viewer> is persistent, in that its entire state (sans the data
itself) can be serialized or deserialized. This include all column, filter,
pivot, expressions, etc. properties, as well as datagrid style settings, config
panel visibility, and more. This overloaded feature covers a range of use cases:
- Setting a
<perspective-viewer>’s initial state after aload()call. - Updating a single or subset of properties, without modifying others.
- Resetting some or all properties to their data-relative default.
- Persisting a user’s configuration to
localStorageor a server.
Serializing and deserializing the viewer state
To retrieve the entire state as a JSON-ready JavaScript object, use the save()
method.
const token = await elem.save();
The token can be restored to any <perspective-viewer> with a Table of
identical schema, via the restore() method. Note that while the data for a
token returned from save() may differ, generally its schema may not, as many
other settings depend on column names and types.
await elem.restore(token);
restore() validates its argument, rejecting for example a key the config
format does not define; when a rejected token is stamped with another release’s
version, the error names that release. Tokens saved by older releases should
be upgraded first — see Migrating saved configs.
// This will error!
await elem.restore(JSON.stringify(json_token));
Updating individual properties
Using the JSON format, every facet of a <perspective-viewer>’s configuration
can be manipulated from JavaScript using the restore() method. The valid
structure of properties is described via the
ViewerConfigUpdate
and embedded
ViewConfigUpdate
type declarations (both generated from the Rust definitions), and the
View chapter of the documentation which has
several examples for each ViewConfig property.
// Set the plugin (will also update `columns` to plugin-defaults)
await elem.restore({ plugin: "X Bar" });
// Update plugin and columns (only draws once)
await elem.restore({ plugin: "X Bar", columns: ["Sales"] });
// Open the config panel
await elem.restore({ settings: true });
// Create an expression
await elem.restore({
columns: ['"Sales" + 100'],
expressions: { "New Column": '"Sales" + 100' },
});
// ERROR if the column does not exist in the schema or expressions
// await elem.restore({columns: ["\"Sales\" + 100"], expressions: {}});
// Add a filter
await elem.restore({ filter: [["Sales", "<", 100]] });
// Add a sort, don't remove filter
await elem.restore({ sort: [["Prodit", "desc"]] });
// Reset just filter, preserve sort
await elem.restore({ filter: undefined });
// Reset all properties to default e.g. after `load()`
await elem.reset();
Another effective way to quickly create a token for a desired configuration is
to simply copy the token returned from save() after settings the view manually
in the browser. The JSON format is human-readable and should be quite easy to
tweak once generated, as save() will return even the default settings for all
properties. You can call save() in your application code, or e.g. through the
Chrome developer console:
// Copy to clipboard
copy(await document.querySelector("perspective-viewer").save());
Multi-panel viewers
save() and restore() operate on a single panel — the active one by
default, or a specific panel via their optional { panel } selector (e.g.
await elem.save({ panel: "my-panel" })). If restore()’s panel names no
existing panel, a new panel is created with that id.
A <perspective-viewer> may host multiple panels. To serialize or restore the
whole element — every panel plus the layout and cross-filter state — use
saveWorkspace() and restoreWorkspace() instead:
const workspace_token = await elem.saveWorkspace();
await elem.restoreWorkspace(workspace_token);
A saveWorkspace() token is a WorkspaceConfig
({ version, layout, panels, ... }), not a ViewerConfig — passing it to the
single-panel restore() is rejected (its panels/layout keys are not part
of a ViewerConfig).
The layout field is the tree arranging the panels into a grid of rows and
columns or stacks of tabs, naming every panel id exactly once. See
Arranging panels: layouts and dashboards for its format and for
rearranging, resizing and stacking existing panels.
Like restore(), restoreWorkspace() applies an update: a field that is
absent leaves that part of the element unchanged, null resets it to its
default, and a value replaces it. panels is the whole panel set — when present
every existing panel is replaced ({} empties the element); when absent the
panels are kept, and layout, active and masters name them by their
existing ids. So a cross-filter can be applied to the current panels without
re-creating them:
await elem.restoreWorkspace({ global_filters: [["Region", "==", "West"]] });
await elem.restoreWorkspace({ global_filters: null }); // clear
Derived tables
A workspace token carries a top-level derived_tables map, one entry per
derived Table the element owns, each naming the table its source View
reads plus that view’s config:
await elem.restoreWorkspace({
panels: { one: { table: "Region Totals", plugin: "Datagrid" } },
derived_tables: {
"Region Totals": { table: "superstore", group_by: ["Region"] },
},
});
Within a workspace token, panels refer to tables by name, so a panel entry never
contains a derivation of its own — one above simply binds "Region Totals".
Entries may name another derived table as their table, and they are built
parent-first regardless of the order they appear in.
derived_tables follows the same update rule as panels: absent keeps the
current set, a value replaces it entirely, and {} deletes every derived table.
An entry whose definition has not changed is reused rather than rebuilt.
A derived table whose table is not hosted yet does not fail the restore — it
and anything under it wait, along with the panels bound to them, and are built
when the parent table appears. A dependency cycle is rejected outright.
The single-panel format inlines instead: save() of a panel bound to a derived
table writes the whole definition recursively in the table field, so the token
is self-contained:
await elem.restore({
plugin: "Datagrid",
table: { table: "superstore", group_by: ["Region"] },
});
restore() rebinds to an existing derived table when one was built from a
structurally identical definition, and otherwise creates one under a generated
name. Only the single-panel restore() accepts the inline form — in
restoreWorkspace() every table field is a name.
See Derived tables.
Datetime formats
A date or datetime column’s date_format is a subset of
Intl.DateTimeFormatOptions: either the dateStyle/timeStyle presets or
per-part fields (weekday, year, month, day, hour, minute, second,
fractionalSecondDigits), never both, plus optional timeZone and hour12. As
in Intl, a display field that is left out is not shown:
await viewer.restore({
columns_config: {
"Order Date": { date_format: { dateStyle: "long", timeZone: "UTC" } },
},
});
A column inherits the plugin’s display fields until it sets any of them, and
inherits timeZone and hour12 individually.
Configs saved before 5.7.0 used a "disabled" value and a format: "custom"
marker, and a missing preset field meant short date or medium time.
restore() rejects the removed forms, and restores any other date_format
with its current meaning — { dateStyle: "long" } shows the date alone,
whatever its version. perspective-migrate rewrites
older tokens into the current form with their original rendering.
Colors, palettes and gradients
Per-column color styling lives in a panel’s columns_config, keyed by column
name, and every color-scale value is a string usable verbatim in CSS:
| Kind | Value |
|---|---|
| color | "#rrggbb" (#rgb, rgb() and rgba() are accepted on input) |
| palette | "linear-gradient(to right, #rrggbb, #rrggbb, …)" — N colors, no positions |
| gradient | "linear-gradient(to right, #rrggbb 0%, #rrggbb 37.5%, …)" — every stop positioned |
Which reader applies is decided by the style control’s kind, never by inspecting
the string. The datagrid’s fg_color/bg_color are read according to the
column’s type and its fg_mode/bg_mode: a gradient for numeric columns, a
color for string and datetime columns in "color" mode, and a palette for
string columns in "series" mode; the charts’ gradient is a gradient and
palette a palette. A position anywhere in a palette is rejected, while a
gradient may omit positions on input (the CSS implicit-position rules fill them)
and may carry any direction token, which is normalized to to right. A mode the
column’s type does not accept (say fg_mode: "series" on a float column)
rejects the restore() call, as does a value that does not parse under the
mode’s reader. Values equal to the plugin’s default are not serialized.
await viewer.restore({
plugin: "Datagrid",
columns_config: {
Profit: {
bg_mode: "gradient",
bg_color: "linear-gradient(to right, #ff0000, #ffffff, #0000ff)",
},
},
});
Any of these may instead be a reference to a CSS custom property of the same
kind — "var(--psp-user--color-<name>)", "var(--psp-user--palette-<name>)" or
"var(--psp-user--gradient-<name>)". References are resolved when the config is
written, against the element’s computed style: the palette of the last
restoreWorkspace() (below) takes precedence, then any --psp-user--* property
a theme or the page defines on the element. An unresolvable reference is dropped
(the plugin’s default renders). Panels hold literals from then on — save()
always emits literals, and the column style tab always edits a literal.
saveWorkspace() emits a palette: every color value in use across the
panels is written in panels as a var() reference, and the top-level
palette map (custom property name → value) carries each referenced definition
once. Names are stable — a value keeps the name the last restoreWorkspace()
gave it when the values match, reuses a theme entry’s name when it matches one
(--psp-user--<kind>-1, -2, … are discovered by contiguous numbering), and
otherwise takes a fresh --psp-user--<kind>-N. restoreWorkspace() applies
palette to the element as inline custom properties (replacing any previously
restored palette) before the panels’ references resolve — which also makes it
the way to inject a brand or theme variation for a workspace to draw on.
By default only the values the panels reference are serialized; a restored
palette’s unused entries, and values pinned during a session, are in-session
state. Pass { full_palette: true } to emit the element’s whole set — in-use
values unioned with the last restored palette and anything pinned since — for a
symmetric round trip:
const used_only = await elem.saveWorkspace();
const everything = await elem.saveWorkspace({ full_palette: true });
In the column style tab, each color field’s Load control lists the element’s set (plus theme entries) for every panel and applies a chosen entry’s value to the field; Pin — offered while the field holds a value the restored set lacks — adds that value to the set for the rest of the session.
await elem.restoreWorkspace({
palette: {
"--psp-user--gradient-heat":
"linear-gradient(to right, #0366d6, #ff7f0e)",
"--psp-user--palette-brand":
"linear-gradient(to right, #2771a8, #8b86ff, #ff471e)",
},
panels: {
sales: {
table: "superstore",
plugin: "Heatmap",
columns: ["Sales"],
columns_config: {
Sales: { gradient: "var(--psp-user--gradient-heat)" },
},
},
},
});
A malformed palette entry (a key outside
--psp-user--{gradient,palette,color}-, or a value its kind rejects) fails the
whole restoreWorkspace() before any panel changes.