Arranging panels: layouts and dashboards
A multi-panel <perspective-viewer> is a dashboard. Its panels are arranged by
a layout: a tree that places each panel in a grid of rows and columns, or
stacks several panels as tabs in one cell. The layout is the layout field of a
saveWorkspace() token, and
restoreWorkspace() applies one.
The layout tree
A layout is built from two kinds of node:
split-layoutdivides its space among itschildren. Theorientationdecides the direction:"horizontal"places the children side by side as columns, left to right, and"vertical"stacks them as rows, top to bottom.sizesgives each child’s fraction of the split, one number per child, and the sizes must sum to 1 ([0.5, 0.5]is an even split,[0.25, 0.75]a narrow column next to a wide one).tab-layoutis a leaf cell holding a stack of panels, listed by panel id intabs. Only one panel of a stack is visible at a time;selectedis the index of the visible tab (default0). A cell with one panel is atab-layoutwith a single entry intabs.
Nesting splits makes a grid: a vertical split whose first child is a
horizontal split puts two panels side by side in the top row and one panel
across the bottom row. A split nested directly inside a split of the same
orientation is flattened into its parent, so saveWorkspace() may return an
equivalent but flatter tree than the one you restored.
Every panel must appear in the layout exactly once.
const layout = {
type: "split-layout",
orientation: "vertical",
sizes: [0.55, 0.45],
children: [
{
type: "split-layout",
orientation: "horizontal",
sizes: [0.5, 0.5],
children: [
{ type: "tab-layout", tabs: ["sales"], selected: 0 },
{ type: "tab-layout", tabs: ["regions"], selected: 0 },
],
},
{ type: "tab-layout", tabs: ["grid"], selected: 0 },
],
};
Building a grid dashboard
A complete dashboard is one restoreWorkspace() call: panels defines each
panel’s view (keyed by an id of your choosing), and layout arranges those ids
into a grid. This one puts a bar chart and a treemap side by side above a pivot
grid that spans the bottom row. Give each panel a title; it is shown in the
panel’s tab header.
await viewer.restoreWorkspace({
layout: {
type: "split-layout",
orientation: "vertical",
sizes: [0.55, 0.45],
children: [
{
type: "split-layout",
orientation: "horizontal",
sizes: [0.5, 0.5],
children: [
{ type: "tab-layout", tabs: ["sales"] },
{ type: "tab-layout", tabs: ["regions"] },
],
},
{ type: "tab-layout", tabs: ["grid"] },
],
},
panels: {
sales: {
table: "superstore",
title: "Sales by Sub-Category",
plugin: "Y Bar",
group_by: ["Sub-Category"],
split_by: ["Segment"],
sort: [["Sales", "desc"]],
columns: ["Sales"],
},
regions: {
table: "superstore",
title: "Sales by Region",
plugin: "Treemap",
group_by: ["Region", "State"],
columns: ["Sales", "Profit"],
},
grid: {
table: "superstore",
title: "Profit by Region",
plugin: "Datagrid",
group_by: ["Region", "State"],
split_by: ["Category"],
columns: ["Sales", "Profit"],
},
},
});
Panels added one at a time with addPanel() are each placed as a new column,
side by side; arrange them afterwards by restoring a layout, as below.
Rearranging existing panels
A restoreWorkspace() call with a layout but no panels keeps every panel
and only moves them. Name the panels by their existing ids, from
getPanelNames() or a saveWorkspace() token. This is how to move or swap a
panel, resize a row or column, change a row into a column, or stack panels as
tabs; nothing is re-created or re-queried.
const [a, b, c] = viewer.getPanelNames();
// Three columns, the first twice as wide as the others.
await viewer.restoreWorkspace({
layout: {
type: "split-layout",
orientation: "horizontal",
sizes: [0.5, 0.25, 0.25],
children: [
{ type: "tab-layout", tabs: [a] },
{ type: "tab-layout", tabs: [b] },
{ type: "tab-layout", tabs: [c] },
],
},
});
// Stack all three panels as tabs in one cell, showing the second.
await viewer.restoreWorkspace({
layout: { type: "tab-layout", tabs: [a, b, c], selected: 1 },
});
To resize panels, change sizes and keep the tree; to swap two panels, exchange
their ids in tabs.
Layout validation errors
restoreWorkspace() rejects a malformed layout before changing anything, so
the viewer is left exactly as it was. A layout is rejected when:
- a panel id in
tabsis unknown — withoutpanelsthe ids must be existing panel ids, and withpanelsthey must bepanelskeys; - a panel is missing from the layout, or appears more than once — every panel must be placed exactly once;
- a
split-layouthas nochildren, or itssizesdo not have one entry per child, or a size is zero or negative, or the sizes do not sum to 1; - a
tab-layouthas emptytabs, or itsselectedindex is past the end oftabs.
The error message names the offending node by its path, e.g.
layout.children[1].tabs, and lists the valid panel ids.
Master/detail cross-filtering
Panels can be linked so that clicking a row in one filters the others. A
master panel is a filter source: selecting a row in it (for example a group in
a pivoted grid) writes a filter clause for that row’s group-by values into the
element’s global_filters. Every other, detail, panel applies the
global_filters as a transient overlay on its own filter, which is never
written into the detail panel’s saved config. A master panel is not filtered by
global_filters itself.
Masters are listed by panel id in a workspace token’s masters field, and the
cross-filter set is its global_filters field. Both can be set without
re-creating the panels:
const [master, detail] = viewer.getPanelNames();
// Make the first panel a master; selecting a row filters the detail panel.
await viewer.restoreWorkspace({ masters: [master] });
// Apply a cross-filter directly, or clear it.
await viewer.restoreWorkspace({ global_filters: [["Region", "==", "West"]] });
await viewer.restoreWorkspace({ global_filters: null });
// Demote every master.
await viewer.restoreWorkspace({ masters: null });
The perspective-global-filter-update event fires whenever the cross-filter set
changes; see Listening for events.
Panel titles
Each panel’s tab header shows its title, a field of the panel’s own config.
Set it with the panel’s other fields in restoreWorkspace() or addPanel(), or
change it later with restore():
await viewer.restore({ title: "Sales by Region" }, { panel: "regions" });