Advanced View Operations
Beyond the standard query configuration, View provides additional methods for
interacting with hierarchical results and introspecting data.
Tree Hierarchy Operations
When a View has group_by applied, the results form a tree hierarchy.
Perspective provides methods to control which levels of the tree are expanded or
collapsed:
const view = await table.view({ group_by: ["Region", "Country", "City"] });
// Collapse the tree at row index 5
await view.collapse(5);
// Expand the tree at row index 5
await view.expand(5);
// Set the expansion depth (0 = fully collapsed, 1 = first level, etc.)
await view.set_depth(1);
Using the sync API
view = table.view(group_by=["Region", "Country", "City"])
view.collapse(5)
view.expand(5)
view.set_depth(1)
#![allow(unused)]
fn main() {
let view = table.view(Some(ViewConfigUpdate {
group_by: Some(vec!["Region".into(), "Country".into(), "City".into()]),
..ViewConfigUpdate::default()
})).await?;
view.collapse(5).await?;
view.expand(5).await?;
view.set_depth(1).await?;
}
Perspective’s built-in engine is lazy — aggregates for
collapsed rows are not recalculated when the underlying Table is updated.
Updates are only computed for rows that are currently visible (expanded). When a
collapsed row is later expanded, its aggregates are calculated at that
point.
Column Range Queries
View::get_min_max returns the minimum and maximum values for a given column,
which is useful for setting up scales in custom visualizations:
const [min, max] = await view.get_min_max("Sales");
min_val, max_val = view.get_min_max("Sales")
Describing a View Config
Table::describe validates a complete view config against a table and reports
the schema a View built from it would have - without creating one. describe
reports a view_schema if and only if Table::view with the same config would
succeed, and the two schemas are equal. It costs no engine resources, so it is
the right way to check a config before applying it.
const verdict = await table.describe({
columns: ["Sales", "margin"],
group_by: ["Region"],
expressions: { margin: '"Profit" / "Sales"' },
aggregates: { margin: "avg" },
});
if ("view_schema" in verdict) {
// { expression_schema: { margin: "float" },
// view_schema: { Sales: "float", margin: "float" } }
} else if ("expression_errors" in verdict) {
// { expression_schema: {...}, expression_errors: { margin: { error_message, line, column } } }
} else {
// { config_error: "Invalid column 'Sales' found in View columns." }
}
verdict = table.describe(
columns=["Sales", "margin"],
group_by=["Region"],
expressions={"margin": '"Profit" / "Sales"'},
aggregates={"margin": "avg"},
)
if "view_schema" in verdict:
...
elif "expression_errors" in verdict:
...
else:
verdict["config_error"]
Expression Validation
Table::validate_expressions is a specialization of Table::describe over a
config that selects no columns, so only the expressions are checked. It returns
which expressions are valid and their inferred types, plus an expression_alias
map echoing the request:
const result = await table.validate_expressions({
expr1: '"Sales" + "Profit"',
expr2: "invalid_column + 1",
});
// result.expression_schema contains valid expressions and their types
// result.errors contains invalid expressions and error messages
result = table.validate_expressions({
"expr1": '"Sales" + "Profit"',
"expr2": "invalid_column + 1",
})
View Dimensions
View::dimensions returns the number of rows and columns in the current view,
including information about group-by header rows:
const dims = await view.dimensions();
// { num_view_rows, num_view_columns, num_table_rows, num_table_columns, ... }
dims = view.dimensions()
View Configuration Introspection
View::get_config returns the full configuration used to create the view:
const config = await view.get_config();
// { group_by: [...], split_by: [...], sort: [...], filter: [...], ... }
config = view.get_config()
Update Callbacks
Register a callback to be notified whenever the underlying Table is updated
and the View has been recalculated:
view.on_update(
(updated) => {
console.log("View updated", updated.port_id);
},
{ mode: "row" },
);
// Later, remove the callback
view.remove_update(callback);
def on_update(port_id, delta, reset):
print("View updated", port_id)
view.on_update(on_update, mode="row")
view.remove_update(on_update)
When mode is set to "row", the callback receives a delta of only the rows
that changed (as Apache Arrow), which is useful for efficiently synchronizing
tables across clients. The callback also receives reset, which is true when
the update step began with clear() or replace(); the delta is then relative
to an empty Table, so a replica should apply it with replace() rather than
update().
Remove Callbacks
Register a callback to be notified whenever rows are removed from the underlying
Table by remove(), which requires an index. The callback receives the
port_id and the removed index column values as an Apache Arrow with one
column per index column, named after that column. It fires once per update step,
only for rows which existed before that step; replace() reports the keys it
does not re-supply, and clear() reports every key:
const callback = await view.on_remove(({ indices, port_id }) => {
replica.remove(indices);
});
// Later, remove the callback
await view.remove_remove(callback);
def on_remove(port_id, indices):
replica.remove(indices)
callback = view.on_remove(on_remove)
view.remove_remove(callback)
Constructing a Table from a View
A Table can be constructed on a View, producing a read-only Table of that
View’s output which the engine keeps current as the source is updated. See
Derived Tables for its columns, its constraints and
what it is good for.
const worker = await perspective.worker();
const table = await worker.table(data);
const view = await table.view({ filter: [["State", "==", "Texas"]] });
const table2 = await worker.table(view);
table.update([{ State: "Texas", City: "Austin" }]);
table = client.table(data)
view = table.view(filter=[["State", "==", "Texas"]])
table2 = client.table(view)
table.update([{"State": "Texas", "City": "Austin"}])
#![allow(unused)]
fn main() {
let opts = TableInitOptions::default();
let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
let table = client.table(data, opts).await?;
let view = table.view(None).await?;
let table2 = client.table(TableData::View(view), TableInitOptions::default()).await?;
table.update(data).await?;
}