diff --git a/packages/viewer-d3fc/src/ts/plugin/plugin.ts b/packages/viewer-d3fc/src/ts/plugin/plugin.ts index 8153210bb4..ccca5254b0 100644 --- a/packages/viewer-d3fc/src/ts/plugin/plugin.ts +++ b/packages/viewer-d3fc/src/ts/plugin/plugin.ts @@ -136,6 +136,10 @@ class HTMLPerspectiveViewerD3fcPluginElement extends HTMLElement { return this._chart.plugin.selectMode || "select"; } + get group_rollups(): string[] { + return ["flat"]; + } + get min_config_columns() { return ( this._chart.plugin.initial?.count || diff --git a/packages/viewer-d3fc/test/js/barWidth.spec.ts b/packages/viewer-d3fc/test/js/barWidth.spec.ts index d5b2a58077..96a0283dc9 100644 --- a/packages/viewer-d3fc/test/js/barWidth.spec.ts +++ b/packages/viewer-d3fc/test/js/barWidth.spec.ts @@ -47,6 +47,7 @@ test.describe("Bar Width", () => { group_by: ["Order Date"], split_by: ["Profit"], theme: "Pro Light", + group_rollup_mode: "flat", }); await compareSVGContentsToSnapshot( diff --git a/packages/viewer-datagrid/src/less/regular_table.less b/packages/viewer-datagrid/src/less/regular_table.less index 67604bf94d..b02b892cbe 100644 --- a/packages/viewer-datagrid/src/less/regular_table.less +++ b/packages/viewer-datagrid/src/less/regular_table.less @@ -128,10 +128,20 @@ perspective-viewer { border-color: var(--selected-row--background-color, #ea7319) !important; } +regular-table.flat-group-rollup-mode.vertical-row-headers + th.psp-tree-label:not(:last-of-type) { + writing-mode: vertical-lr; +} + .psp-row-selected.psp-tree-label:not(:hover):before { color: white; } +regular-table:not(.flat-group-rollup-mode) + .psp-row-selected.psp-tree-label:not(:hover):before { + color: white; +} + .psp-row-subselected, :hover .psp-row-subselected, :hover th.psp-tree-leaf.psp-row-subselected, @@ -254,29 +264,36 @@ tbody th:empty { max-width: 20px; pointer-events: none; } -.psp-tree-label { - max-width: 0px; - min-width: 0px; -} -.psp-tree-label:before { - color: var(--icon--color); - font-family: var(--button--font-family, inherit); - padding-right: 11px; -} -.psp-tree-label-expand:before { - content: var(--tree-label-expand--content, "+"); -} -.psp-tree-label-collapse:before { - content: var(--tree-label-collapse--content, "-"); -} -.psp-tree-label-expand, -.psp-tree-label-collapse { - cursor: pointer; -} -.psp-tree-label:hover:before { - color: var(--active--color); - text-shadow: 0px 0px 5px var(--active--color); +regular-table:not(.flat-group-rollup-mode) { + .psp-tree-label { + max-width: 0px; + min-width: 0px; + } + + .psp-tree-label:before { + color: var(--icon--color); + font-family: var(--button--font-family, inherit); + padding-right: 11px; + } + + .psp-tree-label-expand:before { + content: var(--tree-label-expand--content, "+"); + } + + .psp-tree-label-collapse:before { + content: var(--tree-label-collapse--content, "-"); + } + + .psp-tree-label-expand, + .psp-tree-label-collapse { + cursor: pointer; + } + + .psp-tree-label:hover:before { + color: var(--active--color); + text-shadow: 0px 0px 5px var(--active--color); + } } .psp-tree-leaf { diff --git a/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts b/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts index a32e9bb2c1..6b4a219fc5 100644 --- a/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts +++ b/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts @@ -133,6 +133,10 @@ export class HTMLPerspectiveViewerDatagridPluginElement return ["Columns"]; } + get group_rollups(): string[] { + return ["rollup", "flat"]; + } + /** * Give the Datagrid a higher priority so it is loaded * over the default charts by default. @@ -179,6 +183,7 @@ export class HTMLPerspectiveViewerDatagridPluginElement viewport?.start_row !== null ? viewport.end_row - viewport.start_row : await view.num_rows(); + let out = ""; for (let ridx = 0; ridx < nrows; ridx++) { for (const col_name of cols) { diff --git a/packages/viewer-datagrid/src/ts/data_listener/format_tree_header.ts b/packages/viewer-datagrid/src/ts/data_listener/format_tree_header.ts index 80c09afab5..05b3f2dece 100644 --- a/packages/viewer-datagrid/src/ts/data_listener/format_tree_header.ts +++ b/packages/viewer-datagrid/src/ts/data_listener/format_tree_header.ts @@ -52,6 +52,22 @@ export function* format_tree_header_row_path( } } +export function* format_flat_header_row_path( + this: DatagridModel, + paths: unknown[][] = [], + row_headers: string[], + regularTable: RegularTable, +): Generator { + const plugins: ColumnsConfig = + (regularTable as any)[PRIVATE_PLUGIN_SYMBOL] || {}; + + for (let path of paths) { + yield path.map((part, i) => + format_cell.call(this, row_headers[i], part, plugins, true), + ) as RowHeaderCell[]; + } +} + /** * Format a single cell of the `group_by` tree header. */ diff --git a/packages/viewer-datagrid/src/ts/data_listener/index.ts b/packages/viewer-datagrid/src/ts/data_listener/index.ts index 8dbe61582a..0c575a3979 100644 --- a/packages/viewer-datagrid/src/ts/data_listener/index.ts +++ b/packages/viewer-datagrid/src/ts/data_listener/index.ts @@ -13,6 +13,7 @@ import { PRIVATE_PLUGIN_SYMBOL } from "../types.js"; import { format_cell } from "./format_cell.js"; import { + format_flat_header_row_path, format_tree_header, format_tree_header_row_path, } from "./format_tree_header.js"; @@ -207,9 +208,12 @@ export function createDataListener( } const is_row_path = columns.__ROW_PATH__ !== undefined; + const is_flat = this._config.group_rollup_mode === "flat"; const row_headers = Array.from( (is_row_path - ? format_tree_header_row_path + ? is_flat + ? format_flat_header_row_path + : format_tree_header_row_path : format_tree_header ).call( this, diff --git a/packages/viewer-datagrid/src/ts/model/create.ts b/packages/viewer-datagrid/src/ts/model/create.ts index c81dfc9990..3ed2526101 100644 --- a/packages/viewer-datagrid/src/ts/model/create.ts +++ b/packages/viewer-datagrid/src/ts/model/create.ts @@ -122,6 +122,9 @@ export async function createModel( } } + const group_rollup_mode_changed = + old.group_rollup_mode !== config.group_rollup_mode; + this._reset_scroll_top = group_by_changed; this._reset_scroll_left = split_by_changed; this._reset_select = @@ -132,6 +135,7 @@ export async function createModel( columns_changed; this._reset_column_size = + group_rollup_mode_changed || split_by_changed || group_by_changed || columns_changed || diff --git a/packages/viewer-datagrid/src/ts/plugin/draw.ts b/packages/viewer-datagrid/src/ts/plugin/draw.ts index 83ff118a0c..3abe1af671 100644 --- a/packages/viewer-datagrid/src/ts/plugin/draw.ts +++ b/packages/viewer-datagrid/src/ts/plugin/draw.ts @@ -37,6 +37,7 @@ export async function draw( const drawPromise = this.regular_table.draw({ invalid_columns: true, } as any); + if (this._reset_scroll_top) { this.regular_table.scrollTop = 0; this._reset_scroll_top = false; diff --git a/packages/viewer-datagrid/src/ts/style_handlers/body.ts b/packages/viewer-datagrid/src/ts/style_handlers/body.ts index 91023b8ccf..c5b0d614e2 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/body.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/body.ts @@ -49,6 +49,11 @@ export function applyBodyCellStyles( const hasSelected = selectedRowsMap.has(regularTable); const selected = selectedRowsMap.get(regularTable); + regularTable.classList.toggle( + "flat-group-rollup-mode", + this._config.group_rollup_mode === "flat", + ); + for (const { element: td, metadata, isHeader } of cells) { const column_name = metadata.column_header?.[this._config.split_by.length]; diff --git a/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts b/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts index e68d7ba9e0..49890b68ff 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/column_header.ts @@ -97,9 +97,11 @@ export function styleColumnHeaderRow( regularTable: RegularTableElement, is_menu_row: boolean, ): void { - const header_depth = this._config.group_by.length; - const selectedColumn = this._column_settings_selected_column; + const header_depth = + this._config.group_by.length - + (this._config.group_rollup_mode === "flat" ? 1 : 0); + const selectedColumn = this._column_settings_selected_column; for (const { element: td, metadata } of headerRow.cells) { if ( !metadata || @@ -110,14 +112,13 @@ export function styleColumnHeaderRow( const column_name = metadata.column_header?.[this._config.split_by.length]; + const sort = this._config.sort.find((x) => x[0] === column_name); - let needs_border = - metadata.type === "corner" && - metadata.row_header_x === header_depth; const is_corner = typeof metadata.x === "undefined"; - needs_border = - needs_border || - (metadata.x !== undefined && + const needs_border = + (metadata.type === "corner" && + metadata.row_header_x === header_depth) || + (!is_corner && (metadata.x + 1) % this._config.columns.length === 0); td.classList.toggle("psp-header-border", needs_border); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ffb06a261..f6682d7e94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,8 +157,8 @@ catalogs: specifier: '>17 <20' version: 18.3.1 regular-table: - specifier: '=0.8.1' - version: 0.8.1 + specifier: '=0.8.3' + version: 0.8.3 stoppable: specifier: '=1.1.0' version: 1.1.0 @@ -898,7 +898,7 @@ importers: version: 3.1.2 regular-table: specifier: 'catalog:' - version: 0.8.1 + version: 0.8.3 devDependencies: '@perspective-dev/esbuild-plugin': specifier: 'workspace:' @@ -7900,8 +7900,8 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true - regular-table@0.8.1: - resolution: {integrity: sha512-+HZgc1CFER+M5dFhsL5MYEU6WKRhLwxRT85h7OCJM0kfuZiYZBi2kSH4/HRc4aCNGiczQbp1EvuEhTgjSdBl7w==} + regular-table@0.8.3: + resolution: {integrity: sha512-GANAV656dyTza89S1pz1NwQdcIZv+uUyV5z9k2mPnVfQxWVY150Ft75p35El0ICGpKvUG9mz6BP5vq4+d9BwUg==} engines: {node: '>=16'} rehype-raw@7.0.0: @@ -17841,7 +17841,7 @@ snapshots: dependencies: jsesc: 3.1.0 - regular-table@0.8.1: {} + regular-table@0.8.3: {} rehype-raw@7.0.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bee69d92c9..cf3f70e6bd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,7 +39,7 @@ catalog: "pro_self_extracting_wasm": "0.0.9" "react-dom": ">17 <20" "react": ">17 <20" - "regular-table": "=0.8.1" + "regular-table": "=0.8.3" "stoppable": "=1.1.0" "ws": "^8.17.0" diff --git a/rust/perspective-client/perspective.proto b/rust/perspective-client/perspective.proto index e0aff91c56..fbdf8a1b39 100644 --- a/rust/perspective-client/perspective.proto +++ b/rust/perspective-client/perspective.proto @@ -393,7 +393,6 @@ message ViewToColumnsStringReq { optional bool id = 2; optional bool index = 3; optional bool formatted = 4; - optional bool leaves_only = 5; } message ViewToColumnsStringResp { @@ -405,7 +404,6 @@ message ViewToRowsStringReq { optional bool id = 2; optional bool index = 3; optional bool formatted = 4; - optional bool leaves_only = 5; } message ViewToRowsStringResp { @@ -417,7 +415,6 @@ message ViewToNdjsonStringReq { optional bool id = 2; optional bool index = 3; optional bool formatted = 4; - optional bool leaves_only = 5; } message ViewToNdjsonStringResp { @@ -522,6 +519,7 @@ message ViewConfig { map aggregates = 7; FilterReducer filter_op = 8; optional uint32 group_by_depth = 9; + optional GroupRollupMode group_rollup_mode = 10; message AggList { repeated string aggregations = 1; @@ -542,6 +540,12 @@ message ViewConfig { AND = 0; OR = 1; } + + enum GroupRollupMode { + ROLLUP = 0; + FLAT = 1; + // TOTAL = 2; + } } message ColumnsUpdate { diff --git a/rust/perspective-client/src/rust/config/view_config.rs b/rust/perspective-client/src/rust/config/view_config.rs index 50372b2eea..43a2d9c391 100644 --- a/rust/perspective-client/src/rust/config/view_config.rs +++ b/rust/perspective-client/src/rust/config/view_config.rs @@ -11,6 +11,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use std::collections::HashMap; +use std::fmt::Display; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -22,6 +23,48 @@ use super::sort::*; use crate::proto; use crate::proto::columns_update; +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq, TS)] +pub enum GroupRollupMode { + #[default] + #[serde(rename = "rollup")] + Rollup, + + #[serde(rename = "flat")] + Flat, + // #[serde(rename = "total")] + // Total, +} + +impl Display for GroupRollupMode { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(fmt, "{}", match self { + Self::Rollup => "Rollup", + Self::Flat => "Flat", + // Self::Total => "Total", + }) + } +} + +impl From for GroupRollupMode { + fn from(value: proto::view_config::GroupRollupMode) -> Self { + match value { + proto::view_config::GroupRollupMode::Rollup => Self::Rollup, + proto::view_config::GroupRollupMode::Flat => Self::Flat, + // proto::view_config::GroupRollupMode::Total => Self::Total, + } + } +} + +impl From for proto::view_config::GroupRollupMode { + fn from(value: GroupRollupMode) -> Self { + match value { + GroupRollupMode::Rollup => proto::view_config::GroupRollupMode::Rollup, + GroupRollupMode::Flat => proto::view_config::GroupRollupMode::Flat, + // GroupRollupMode::Total => proto::view_config::GroupRollupMode::Total, + } + } +} + #[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize, TS)] #[serde(deny_unknown_fields)] pub struct ViewConfig { @@ -37,6 +80,10 @@ pub struct ViewConfig { #[serde(default)] pub filter: Vec, + // #[serde(skip_serializing_if = "is_default_value")] + #[serde(default)] + pub group_rollup_mode: GroupRollupMode, + #[serde(skip_serializing_if = "is_default_value")] #[serde(default)] pub filter_op: FilterReducer, @@ -164,6 +211,11 @@ pub struct ViewConfigUpdate { #[serde(default)] #[ts(optional)] pub filter_op: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + #[ts(optional)] + pub group_rollup_mode: Option, } impl From for proto::ViewConfig { @@ -202,6 +254,9 @@ impl From for proto::ViewConfig { .map(|(x, y)| (x, y.into())) .collect(), group_by_depth: value.group_by_depth, + group_rollup_mode: value + .group_rollup_mode + .map(|x| proto::view_config::GroupRollupMode::from(x).into()), } } } @@ -236,6 +291,7 @@ impl From for ViewConfigUpdate { expressions: Some(value.expressions), aggregates: Some(value.aggregates), group_by_depth: value.group_by_depth, + group_rollup_mode: Some(value.group_rollup_mode), } } } @@ -265,6 +321,12 @@ impl From for ViewConfig { .map(|(x, y)| (x, y.into())) .collect(), group_by_depth: value.group_by_depth, + group_rollup_mode: value + .group_rollup_mode + .map(proto::view_config::GroupRollupMode::try_from) + .and_then(|x| x.ok()) + .map(|x| x.into()) + .unwrap_or_default(), } } } @@ -281,6 +343,7 @@ impl From for ViewConfig { expressions: value.expressions.unwrap_or_default(), aggregates: value.aggregates.unwrap_or_default(), group_by_depth: value.group_by_depth, + group_rollup_mode: value.group_rollup_mode.unwrap_or_default(), } } } @@ -312,6 +375,10 @@ impl From for ViewConfigUpdate { .collect(), ), group_by_depth: value.group_by_depth, + group_rollup_mode: value + .group_rollup_mode + .and_then(|x| proto::view_config::GroupRollupMode::try_from(x).ok()) + .map(|x| x.into()), } } } @@ -346,6 +413,7 @@ impl ViewConfig { changed = Self::_apply(&mut self.sort, update.sort) || changed; changed = Self::_apply(&mut self.aggregates, update.aggregates) || changed; changed = Self::_apply(&mut self.expressions, update.expressions) || changed; + changed = Self::_apply(&mut self.group_rollup_mode, update.group_rollup_mode) || changed; changed } diff --git a/rust/perspective-client/src/rust/view.rs b/rust/perspective-client/src/rust/view.rs index 05c7c134f2..91e8a6a1a4 100644 --- a/rust/perspective-client/src/rust/view.rs +++ b/rust/perspective-client/src/rust/view.rs @@ -100,10 +100,6 @@ pub struct ViewWindow { #[serde(skip_serializing_if = "Option::is_none")] pub index: Option, - #[ts(optional)] - #[serde(skip_serializing_if = "Option::is_none")] - pub leaves_only: Option, - /// Only impacts [`View::to_csv`] #[ts(optional)] #[serde(skip_serializing_if = "Option::is_none")] @@ -383,7 +379,6 @@ impl View { id: window.id, index: window.index, formatted: window.formatted, - leaves_only: window.leaves_only, })); match self.client.oneshot(&msg).await? { @@ -402,7 +397,6 @@ impl View { id: window.id, index: window.index, formatted: window.formatted, - leaves_only: window.leaves_only, })); match self.client.oneshot(&msg).await? { @@ -422,7 +416,6 @@ impl View { id: window.id, index: window.index, formatted: window.formatted, - leaves_only: window.leaves_only, })); match self.client.oneshot(&msg).await? { diff --git a/rust/perspective-js/test/js/filters.spec.js b/rust/perspective-js/test/js/filters.spec.js index 4763e3deb3..613cd9af85 100644 --- a/rust/perspective-js/test/js/filters.spec.js +++ b/rust/perspective-js/test/js/filters.spec.js @@ -522,6 +522,7 @@ const datetime_data_local = [ group_by: [], sort: [], split_by: [], + group_rollup_mode: "rollup", }); view.delete(); diff --git a/rust/perspective-js/test/js/group_rollup_mode.spec.js b/rust/perspective-js/test/js/group_rollup_mode.spec.js new file mode 100644 index 0000000000..2a99f8f6be --- /dev/null +++ b/rust/perspective-js/test/js/group_rollup_mode.spec.js @@ -0,0 +1,487 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { test, expect } from "@perspective-dev/test"; +import perspective from "./perspective_client"; + +const data = { + w: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5], + x: [1, 2, 3, 4, 4, 3, 2, 1], + y: ["a", "b", "c", "d", "a", "b", "c", "d"], + z: [true, false, true, false, true, false, true, false], +}; + +((perspective) => { + test.describe("group_rollup_mode", function () { + test.describe("flat", function () { + test("only emits leaves", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + }); + const json = await view.to_json(); + expect(json).toStrictEqual([ + { __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 }, + ]); + view.delete(); + table.delete(); + }); + + test("num_rows returns leaf count", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + }); + const num_rows = await view.num_rows(); + expect(num_rows).toEqual(4); + view.delete(); + table.delete(); + }); + + test("to_columns works", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + }); + const cols = await view.to_columns(); + expect(cols).toStrictEqual({ + __ROW_PATH__: [["a"], ["b"], ["c"], ["d"]], + w: [7, 9, 11, 13], + x: [5, 5, 5, 5], + y: [2, 2, 2, 2], + z: [2, 2, 2, 2], + }); + view.delete(); + table.delete(); + }); + + test("sort asc", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + sort: [["w", "asc"]], + }); + const cols = await view.to_columns(); + expect(cols).toStrictEqual({ + __ROW_PATH__: [["a"], ["b"], ["c"], ["d"]], + w: [7, 9, 11, 13], + x: [5, 5, 5, 5], + y: [2, 2, 2, 2], + z: [2, 2, 2, 2], + }); + view.delete(); + table.delete(); + }); + + test("sort desc", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + sort: [["w", "desc"]], + }); + const cols = await view.to_columns(); + expect(cols).toStrictEqual({ + __ROW_PATH__: [["d"], ["c"], ["b"], ["a"]], + w: [13, 11, 9, 7], + x: [5, 5, 5, 5], + y: [2, 2, 2, 2], + z: [2, 2, 2, 2], + }); + view.delete(); + table.delete(); + }); + + test("sort with hidden column", async function () { + const table = await perspective.table(data); + const view = await table.view({ + columns: ["y"], + group_by: ["y"], + group_rollup_mode: "flat", + sort: [["x", "desc"]], + }); + const cols = await view.to_columns(); + expect(cols).toStrictEqual({ + __ROW_PATH__: [["a"], ["b"], ["c"], ["d"]], + y: [2, 2, 2, 2], + }); + view.delete(); + table.delete(); + }); + + test("multi-level group_by", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y", "z"], + group_rollup_mode: "flat", + }); + const json = await view.to_json(); + expect(json).toStrictEqual([ + { + __ROW_PATH__: ["a", true], + w: 7, + x: 5, + y: 2, + z: 2, + }, + { + __ROW_PATH__: ["b", false], + w: 9, + x: 5, + y: 2, + z: 2, + }, + { + __ROW_PATH__: ["c", true], + w: 11, + x: 5, + y: 2, + z: 2, + }, + { + __ROW_PATH__: ["d", false], + w: 13, + x: 5, + y: 2, + z: 2, + }, + ]); + view.delete(); + table.delete(); + }); + + test("multi-level group_by with sort matches expanded tree order", async function () { + const table = await perspective.table(data); + const flat_view = await table.view({ + group_by: ["y", "z"], + group_rollup_mode: "flat", + sort: [["w", "desc"]], + }); + const flat_cols = await flat_view.to_columns(); + expect(flat_cols).toStrictEqual({ + __ROW_PATH__: [ + ["d", false], + ["c", true], + ["b", false], + ["a", true], + ], + w: [13, 11, 9, 7], + x: [5, 5, 5, 5], + y: [2, 2, 2, 2], + z: [2, 2, 2, 2], + }); + flat_view.delete(); + table.delete(); + }); + + test("with split_by", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + split_by: ["z"], + group_rollup_mode: "flat", + }); + const json = await view.to_json(); + expect(json).toStrictEqual([ + { + __ROW_PATH__: ["a"], + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 7, + "true|x": 5, + "true|y": 2, + "true|z": 2, + }, + { + __ROW_PATH__: ["b"], + "false|w": 9, + "false|x": 5, + "false|y": 2, + "false|z": 2, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + { + __ROW_PATH__: ["c"], + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 11, + "true|x": 5, + "true|y": 2, + "true|z": 2, + }, + { + __ROW_PATH__: ["d"], + "false|w": 13, + "false|x": 5, + "false|y": 2, + "false|z": 2, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + ]); + view.delete(); + table.delete(); + }); + + test("split_by only", async function () { + const table = await perspective.table(data); + const view = await table.view({ + split_by: ["z"], + group_rollup_mode: "flat", + }); + const json = await view.to_json(); + expect(json).toStrictEqual([ + { + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 1.5, + "true|x": 1, + "true|y": "a", + "true|z": true, + }, + { + "false|w": 2.5, + "false|x": 2, + "false|y": "b", + "false|z": false, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + { + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 3.5, + "true|x": 3, + "true|y": "c", + "true|z": true, + }, + { + "false|w": 4.5, + "false|x": 4, + "false|y": "d", + "false|z": false, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + { + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 5.5, + "true|x": 4, + "true|y": "a", + "true|z": true, + }, + { + "false|w": 6.5, + "false|x": 3, + "false|y": "b", + "false|z": false, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + { + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 7.5, + "true|x": 2, + "true|y": "c", + "true|z": true, + }, + { + "false|w": 8.5, + "false|x": 1, + "false|y": "d", + "false|z": false, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + ]); + view.delete(); + table.delete(); + }); + + test("with split_by and sort", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + split_by: ["z"], + group_rollup_mode: "flat", + sort: [["w", "desc"]], + }); + const json = await view.to_json(); + expect(json).toStrictEqual([ + { + __ROW_PATH__: ["d"], + "false|w": 13, + "false|x": 5, + "false|y": 2, + "false|z": 2, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + { + __ROW_PATH__: ["c"], + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 11, + "true|x": 5, + "true|y": 2, + "true|z": 2, + }, + { + __ROW_PATH__: ["b"], + "false|w": 9, + "false|x": 5, + "false|y": 2, + "false|z": 2, + "true|w": null, + "true|x": null, + "true|y": null, + "true|z": null, + }, + { + __ROW_PATH__: ["a"], + "false|w": null, + "false|x": null, + "false|y": null, + "false|z": null, + "true|w": 7, + "true|x": 5, + "true|y": 2, + "true|z": 2, + }, + ]); + view.delete(); + table.delete(); + }); + + test("updates after table.update()", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + }); + const before = await view.to_json(); + expect(before).toStrictEqual([ + { __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 }, + ]); + table.update([{ w: 9.5, x: 5, y: "e", z: true }]); + const after = await view.to_json(); + expect(after).toStrictEqual([ + { __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["e"], w: 9.5, x: 5, y: 1, z: 1 }, + ]); + view.delete(); + table.delete(); + }); + + test("updates preserve sort order", async function () { + const table = await perspective.table(data); + const flat_view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + sort: [["w", "desc"]], + }); + table.update([{ w: 100, x: 5, y: "e", z: true }]); + const flat_cols = await flat_view.to_columns(); + expect(flat_cols).toStrictEqual({ + __ROW_PATH__: [["e"], ["d"], ["c"], ["b"], ["a"]], + w: [100, 13, 11, 9, 7], + x: [5, 5, 5, 5, 5], + y: [1, 2, 2, 2, 2], + z: [1, 2, 2, 2, 2], + }); + flat_view.delete(); + table.delete(); + }); + + test("viewport pagination", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + }); + const json = await view.to_json({ + start_row: 0, + end_row: 2, + }); + expect(json).toStrictEqual([ + { __ROW_PATH__: ["a"], w: 7, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["b"], w: 9, x: 5, y: 2, z: 2 }, + ]); + view.delete(); + table.delete(); + }); + + test("viewport pagination with sort", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "flat", + sort: [["w", "desc"]], + }); + const json = await view.to_json({ + start_row: 0, + end_row: 2, + }); + expect(json).toStrictEqual([ + { __ROW_PATH__: ["d"], w: 13, x: 5, y: 2, z: 2 }, + { __ROW_PATH__: ["c"], w: 11, x: 5, y: 2, z: 2 }, + ]); + view.delete(); + table.delete(); + }); + }); + }); +})(perspective); diff --git a/rust/perspective-js/test/js/to_format.spec.js b/rust/perspective-js/test/js/to_format.spec.js index 2f21262242..803ebb6884 100644 --- a/rust/perspective-js/test/js/to_format.spec.js +++ b/rust/perspective-js/test/js/to_format.spec.js @@ -525,8 +525,9 @@ const pivoted_output = [ let table = await perspective.table(int_float_string_data); let view = await table.view({ group_by: ["int"], + group_rollup_mode: "flat", }); - let json = await view.to_json({ leaves_only: true }); + let json = await view.to_json(); expect(json).toEqual([ { __ROW_PATH__: [1], @@ -560,6 +561,81 @@ const pivoted_output = [ view.delete(); table.delete(); }); + + test("num_rows returns correct leaf count", async function () { + let table = await perspective.table(int_float_string_data); + let view = await table.view({ + group_by: ["int"], + group_rollup_mode: "flat", + }); + let num_rows = await view.num_rows(); + expect(num_rows).toEqual(4); + view.delete(); + table.delete(); + }); + + test("viewport pagination returns exact page sizes", async function () { + let table = await perspective.table(int_float_string_data); + let view = await table.view({ + group_by: ["int"], + group_rollup_mode: "flat", + }); + let json = await view.to_json({ + start_row: 0, + end_row: 2, + }); + expect(json.length).toEqual(2); + expect(json[0].__ROW_PATH__).toEqual([1]); + expect(json[1].__ROW_PATH__).toEqual([2]); + view.delete(); + table.delete(); + }); + + test("to_columns works with leaves_only", async function () { + let table = await perspective.table(int_float_string_data); + let view = await table.view({ + group_by: ["int"], + group_rollup_mode: "flat", + }); + let cols = await view.to_columns(); + expect(cols["__ROW_PATH__"]).toEqual([[1], [2], [3], [4]]); + expect(cols["int"]).toEqual([1, 2, 3, 4]); + view.delete(); + table.delete(); + }); + + test("leaves_only with multi-level group_by", async function () { + let table = await perspective.table(int_float_string_data); + let view = await table.view({ + group_by: ["string", "int"], + group_rollup_mode: "flat", + }); + let num_rows = await view.num_rows(); + expect(num_rows).toEqual(4); + let json = await view.to_json(); + for (let row of json) { + expect(row.__ROW_PATH__.length).toEqual(2); + } + view.delete(); + table.delete(); + }); + + test("leaves_only updates after table.update()", async function () { + let table = await perspective.table(int_float_string_data); + let view = await table.view({ + group_by: ["int"], + group_rollup_mode: "flat", + }); + let num_rows = await view.num_rows(); + expect(num_rows).toEqual(4); + table.update([ + { int: 5, float: 6.0, string: "e", datetime: STD_DATE }, + ]); + let num_rows2 = await view.num_rows(); + expect(num_rows2).toEqual(5); + view.delete(); + table.delete(); + }); }); test.describe("to_arrow()", function () { diff --git a/rust/perspective-js/test/js/view_config.spec.js b/rust/perspective-js/test/js/view_config.spec.js index 80238c3b6a..56caa8ae6f 100644 --- a/rust/perspective-js/test/js/view_config.spec.js +++ b/rust/perspective-js/test/js/view_config.spec.js @@ -58,6 +58,7 @@ const data = [ group_by: ["y"], sort: [], split_by: [], + group_rollup_mode: "rollup", }); view.delete(); @@ -81,6 +82,7 @@ const data = [ group_by: ["y"], sort: [], split_by: [], + group_rollup_mode: "rollup", }); view.delete(); @@ -109,6 +111,7 @@ const data = [ group_by: ["y"], sort: [], split_by: [], + group_rollup_mode: "rollup", }); view.delete(); @@ -142,6 +145,7 @@ const data = [ group_by: ["y"], sort: [], split_by: [], + group_rollup_mode: "rollup", }); view.delete(); @@ -165,6 +169,7 @@ const data = [ group_by: ["y"], sort: [], split_by: [], + group_rollup_mode: "rollup", }); view.delete(); diff --git a/rust/perspective-server/cpp/perspective/src/cpp/context_one.cpp b/rust/perspective-server/cpp/perspective/src/cpp/context_one.cpp index 05b6b7c6f5..7d4f7a3939 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/context_one.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/context_one.cpp @@ -346,9 +346,13 @@ void t_ctx1::step_end() { PSP_TRACE_SENTINEL(); PSP_VERBOSE_ASSERT(m_init, "touching uninited object"); - sort_by(m_sortby); - if (m_depth_set) { - set_depth(m_depth); + if (m_leaves_only) { + m_traversal->rebuild_from_leaves(m_sortby); + } else { + sort_by(m_sortby); + if (m_depth_set) { + set_depth(m_depth); + } } } @@ -424,6 +428,14 @@ t_ctx1::set_depth(t_depth depth) { m_depth_set = true; } +void +t_ctx1::set_leaves_only(bool enabled) { + PSP_TRACE_SENTINEL(); + PSP_VERBOSE_ASSERT(m_init, "touching uninited object"); + m_leaves_only = enabled; + m_traversal->set_leaves_only(enabled, m_config.get_num_rpivots()); +} + std::vector t_ctx1::get_pkeys(const std::vector>& cells ) const { @@ -556,6 +568,9 @@ t_ctx1::reset(bool reset_expressions) { m_tree->init(); m_tree->set_deltas_enabled(get_feature_state(CTX_FEAT_DELTA)); m_traversal = std::make_shared(m_tree); + if (m_leaves_only) { + m_traversal->set_leaves_only(true, m_config.get_num_rpivots()); + } if (reset_expressions) { m_expression_tables->reset(); diff --git a/rust/perspective-server/cpp/perspective/src/cpp/context_two.cpp b/rust/perspective-server/cpp/perspective/src/cpp/context_two.cpp index a164e46427..d4c5980500 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/context_two.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/context_two.cpp @@ -110,8 +110,12 @@ t_ctx2::step_begin() { void t_ctx2::step_end() { - if (m_row_depth_set) { - set_depth(HEADER_ROW, m_row_depth); + if (m_leaves_only) { + m_rtraversal->rebuild_from_leaves(m_sortby); + } else { + if (m_row_depth_set) { + set_depth(HEADER_ROW, m_row_depth); + } } if (m_column_depth_set) { set_depth(HEADER_COLUMN, m_column_depth); @@ -738,7 +742,7 @@ t_ctx2::resolve_cells(const std::vector>& cells rval[idx].m_agg_index = agg_idx; - if (cell.first == 0) { + if (cell.first == 0 && !m_leaves_only) { rval[idx].m_idx = c_ptidx; rval[idx].m_treenum = 0; } else if (c_path.empty()) { @@ -935,6 +939,12 @@ t_ctx2::set_depth(t_header header, t_depth depth) { } } +void +t_ctx2::set_leaves_only(bool enabled) { + m_leaves_only = enabled; + m_rtraversal->set_leaves_only(enabled, m_config.get_num_rpivots()); +} + std::vector t_ctx2::get_pkeys(const std::vector>& cells ) const { @@ -1089,6 +1099,9 @@ t_ctx2::reset(bool reset_expressions) { m_rtraversal = std::make_shared(rtree()); m_ctraversal = std::make_shared(ctree()); + if (m_leaves_only) { + m_rtraversal->set_leaves_only(true, m_config.get_num_rpivots()); + } if (reset_expressions) { m_expression_tables->reset(); diff --git a/rust/perspective-server/cpp/perspective/src/cpp/server.cpp b/rust/perspective-server/cpp/perspective/src/cpp/server.cpp index 66251ba0d1..0680f4d6a9 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/server.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/server.cpp @@ -130,7 +130,9 @@ make_context( auto pool = table->get_pool(); auto gnode = table->get_gnode(); - if (row_pivot_depth > -1) { + if (view_config->is_leaves_only()) { + ctx1->set_leaves_only(true); + } else if (row_pivot_depth > -1) { ctx1->set_depth(row_pivot_depth - 1); } else { ctx1->set_depth(row_pivots.size()); @@ -193,7 +195,9 @@ make_context( ctx2->column_sort_by(col_sortspec); } - if (row_pivot_depth > -1) { + if (view_config->is_leaves_only() && !column_only) { + ctx2->set_leaves_only(true); + } else if (row_pivot_depth > -1) { ctx2->set_depth(t_header::HEADER_ROW, row_pivot_depth - 1); } else { ctx2->set_depth(t_header::HEADER_ROW, row_pivots.size()); @@ -2091,6 +2095,9 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { LOG_DEBUG("FILTER_OP: " << filter_op); + bool leaves_only = + cfg.has_group_rollup_mode() ? cfg.group_rollup_mode() == 1 : false; + auto config = std::make_shared( vocab, row_pivots, @@ -2101,7 +2108,8 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { sort_str, expressions, filter_op, - column_only + column_only, + leaves_only ); config->init(schema); @@ -2406,6 +2414,14 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { ); } + if (view_config->is_leaves_only()) { + const auto mode = proto::ViewConfig_GroupRollupMode::ViewConfig_GroupRollupMode_FLAT; + view_config_proto->set_group_rollup_mode(mode); + } else { + const auto mode = proto::ViewConfig_GroupRollupMode::ViewConfig_GroupRollupMode_ROLLUP; + view_config_proto->set_group_rollup_mode(mode); + } + for (const auto& expr : view_config->get_expressions()) { auto* proto_exprs = view_config_proto->mutable_expressions(); (*proto_exprs)[expr->get_expression_alias()] = @@ -2486,7 +2502,6 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { r.formatted(), r.index(), r.id(), - r.leaves_only(), view->sides(), view->sides() > 0 && !config->is_column_only(), nidx, @@ -2525,7 +2540,6 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { r.formatted(), r.index(), r.id(), - r.leaves_only(), view->sides(), view->sides() > 0 && !config->is_column_only(), nidx, @@ -2565,7 +2579,6 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { r.formatted(), r.index(), r.id(), - r.leaves_only(), view->sides(), view->sides() > 0 && !config->is_column_only(), nidx, diff --git a/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp b/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp index 2736105d5e..71a492d6f3 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp @@ -75,6 +75,10 @@ t_traversal::populate_root_children(const std::shared_ptr& tree t_index t_traversal::expand_node(t_index exp_idx) { + if (m_leaves_only) { + return 0; + } + t_tvnode& exp_tvnode = (*m_nodes)[exp_idx]; if (exp_tvnode.m_expanded) { @@ -120,6 +124,10 @@ t_index t_traversal::expand_node( const std::vector& sortby, t_index exp_idx, t_ctx2* ctx2 ) { + if (m_leaves_only) { + return 0; + } + t_tvnode& exp_tvnode = (*m_nodes)[exp_idx]; if (exp_tvnode.m_expanded) { @@ -197,6 +205,10 @@ t_traversal::expand_node( t_index t_traversal::collapse_node(t_index idx) { + if (m_leaves_only) { + return 0; + } + t_tvnode& node = (*m_nodes)[idx]; if (!node.m_expanded) { @@ -699,4 +711,87 @@ t_traversal::get_node_expanded(t_index idx) const { } return m_nodes->at(idx).m_expanded; } + +void +t_traversal::set_leaves_only(bool enabled, t_uindex leaf_depth) { + m_leaves_only = enabled; + m_leaf_depth = leaf_depth; + if (m_leaves_only) { + rebuild_from_leaves({}); + } +} + +bool +t_traversal::is_leaves_only() const { + return m_leaves_only; +} + +void +t_traversal::collect_leaves( + t_uindex tnid, + t_uindex current_depth, + const std::vector& sortby, + const std::vector& sortby_agg_indices, + const std::vector& sort_orders +) { + if (current_depth >= m_leaf_depth) { + t_tvnode node; + node.m_expanded = false; + node.m_depth = m_leaf_depth; + node.m_rel_pidx = 0; + node.m_ndesc = 0; + node.m_nchild = 0; + node.m_tnid = tnid; + m_nodes->push_back(node); + return; + } + + t_stnode_vec children; + m_tree->get_child_nodes(tnid, children); + + if (!sortby.empty() && children.size() > 1) { + auto n_children = children.size(); + auto sortelems = + std::make_shared>(n_children); + std::vector aggregates(sortby.size()); + + for (t_uindex i = 0; i < n_children; ++i) { + m_tree->get_aggregates_for_sorting( + children[i].m_idx, sortby_agg_indices, aggregates, nullptr + ); + (*sortelems)[i] = t_mselem(aggregates, i); + } + + std::vector sorted_idx(n_children); + t_multisorter sorter(sortelems, sort_orders); + argsort(sorted_idx, sorter); + + t_stnode_vec sorted_children(n_children); + for (t_uindex i = 0; i < n_children; ++i) { + sorted_children[i] = children[sorted_idx[i]]; + } + children = std::move(sorted_children); + } + + for (const auto& child : children) { + collect_leaves( + child.m_idx, current_depth + 1, sortby, sortby_agg_indices, + sort_orders + ); + } +} + +void +t_traversal::rebuild_from_leaves(const std::vector& sortby) { + m_nodes = std::make_shared>(); + + std::vector sortby_agg_indices(sortby.size()); + for (t_uindex i = 0; i < sortby.size(); ++i) { + sortby_agg_indices[i] = sortby[i].m_agg_index; + } + + std::vector sort_orders = get_sort_orders(sortby); + collect_leaves(0, 0, sortby, sortby_agg_indices, sort_orders); +} + } // end namespace perspective diff --git a/rust/perspective-server/cpp/perspective/src/cpp/tree_context_common.cpp b/rust/perspective-server/cpp/perspective/src/cpp/tree_context_common.cpp index d874ae9d53..59863f93b6 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/tree_context_common.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/tree_context_common.cpp @@ -70,13 +70,16 @@ notify_sparse_tree_common( auto zero_strands = tree->zero_strands(); - t_uindex t_osize = process_traversal ? traversal->size() : 0; - if (process_traversal) { + bool is_leaves_only = + process_traversal && traversal != nullptr && traversal->is_leaves_only(); + + if (process_traversal && !is_leaves_only) { + t_uindex t_osize = traversal->size(); traversal->drop_tree_indices(zero_strands); - } - t_uindex t_nsize = process_traversal ? traversal->size() : 0; - if (t_osize != t_nsize) { - tree->set_has_deltas(true); + t_uindex t_nsize = traversal->size(); + if (t_osize != t_nsize) { + tree->set_has_deltas(true); + } } auto non_zero_ids = tree->non_zero_ids(zero_strands); @@ -88,61 +91,68 @@ notify_sparse_tree_common( tree->update_aggs_from_static(dctx, gstate, expression_master_table); - std::set visited; - - struct t_leaf_path { - std::vector m_path; - t_uindex m_lfidx; - }; + if (is_leaves_only) { + traversal->rebuild_from_leaves(ctx_sortby); + } else { + std::set visited; - std::vector leaf_paths(non_zero_leaves.size()); + struct t_leaf_path { + std::vector m_path; + t_uindex m_lfidx; + }; - t_uindex count = 0; + std::vector leaf_paths(non_zero_leaves.size()); - for (auto lfidx : non_zero_leaves) { - leaf_paths[count].m_lfidx = lfidx; - tree->get_sortby_path(lfidx, leaf_paths[count].m_path); - std::reverse( - leaf_paths[count].m_path.begin(), leaf_paths[count].m_path.end() - ); - ++count; - } + t_uindex count = 0; - std::sort( - leaf_paths.begin(), - leaf_paths.end(), - [](const t_leaf_path& a, const t_leaf_path& b) { - return a.m_path < b.m_path; + for (auto lfidx : non_zero_leaves) { + leaf_paths[count].m_lfidx = lfidx; + tree->get_sortby_path(lfidx, leaf_paths[count].m_path); + std::reverse( + leaf_paths[count].m_path.begin(), + leaf_paths[count].m_path.end() + ); + ++count; } - ); - if (!leaf_paths.empty() && (traversal != nullptr) - && traversal->size() == 1) { - if (traversal->get_node(0).m_expanded) { - traversal->populate_root_children(tree); - } - } else { - for (const auto& lpath : leaf_paths) { - t_uindex lfidx = lpath.m_lfidx; - auto ancestry = tree->get_ancestry(lfidx); - - t_uindex num_tnodes_existed = 0; - - for (auto nidx : ancestry) { - if (non_zero_ids.find(nidx) == non_zero_ids.end() - || visited.find(nidx) != visited.end()) { - ++num_tnodes_existed; - } else { - break; - } + std::sort( + leaf_paths.begin(), + leaf_paths.end(), + [](const t_leaf_path& a, const t_leaf_path& b) { + return a.m_path < b.m_path; } + ); - if (process_traversal) { - traversal->add_node(ctx_sortby, ancestry, num_tnodes_existed); + if (!leaf_paths.empty() && (traversal != nullptr) + && traversal->size() == 1) { + if (traversal->get_node(0).m_expanded) { + traversal->populate_root_children(tree); } + } else { + for (const auto& lpath : leaf_paths) { + t_uindex lfidx = lpath.m_lfidx; + auto ancestry = tree->get_ancestry(lfidx); + + t_uindex num_tnodes_existed = 0; + + for (auto nidx : ancestry) { + if (non_zero_ids.find(nidx) == non_zero_ids.end() + || visited.find(nidx) != visited.end()) { + ++num_tnodes_existed; + } else { + break; + } + } + + if (process_traversal) { + traversal->add_node( + ctx_sortby, ancestry, num_tnodes_existed + ); + } - for (auto nidx : ancestry) { - visited.insert(nidx); + for (auto nidx : ancestry) { + visited.insert(nidx); + } } } } diff --git a/rust/perspective-server/cpp/perspective/src/cpp/view.cpp b/rust/perspective-server/cpp/perspective/src/cpp/view.cpp index 0634d2264c..545fad44e3 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/view.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/view.cpp @@ -1774,7 +1774,6 @@ View::write_row_path( t_uindex start_row, t_uindex end_row, bool has_row_path, - bool leaves_only, bool is_formatted, rapidjson::Writer& writer ) const { @@ -1782,21 +1781,13 @@ View::write_row_path( if (has_row_path) { writer.Key("__ROW_PATH__"); writer.StartArray(); - t_uindex depth = m_row_pivots.size(); for (auto r = start_row; r < end_row; ++r) { - if (leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } - writer.StartArray(); const auto row_path = get_row_path(r); // Question: Why are the row paths reversed? for (auto entry = row_path.size(); entry > 0; entry--) { const t_tscalar& scalar = row_path[entry - 1]; - write_scalar(scalar, is_formatted, writer); } @@ -1827,24 +1818,16 @@ View::write_column( t_uindex start_row, t_uindex end_row, bool has_row_path, - bool leaves_only, bool is_formatted, std::shared_ptr> slice, const std::vector>& col_names, rapidjson::Writer& writer ) const { - t_uindex depth = m_row_pivots.size(); writer.Key(col_path_to_legacy(col_names.at(c)).c_str()); writer.StartArray(); for (auto r = start_row; r < end_row; ++r) { - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } - auto scalar = slice->get(r, c); write_scalar(scalar, is_formatted, writer); @@ -1859,22 +1842,14 @@ View::write_index_column( t_uindex start_row, t_uindex end_row, bool has_row_path, - bool leaves_only, bool is_formatted, std::shared_ptr> slice, rapidjson::Writer& writer ) const { - t_uindex depth = m_row_pivots.size(); writer.Key("__INDEX__"); writer.StartArray(); for (auto r = start_row; r < end_row; ++r) { - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } - std::vector keys = slice->get_pkeys(r, 0); writer.StartArray(); @@ -1901,7 +1876,6 @@ View::to_rows( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -1929,8 +1903,6 @@ View::to_rows( ); } - t_uindex depth = m_row_pivots.size(); - // These columns don't exist as far as the view/table is concerned. They're // scoped to the serialization of the view itself. // @@ -1943,12 +1915,6 @@ View::to_rows( if (start_col <= (end_col + num_virtual_columns)) { for (auto r = start_row; r < end_row; ++r) { - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } - writer.StartObject(); if (get_ids) { std::pair pair{r, 0}; @@ -1998,7 +1964,6 @@ View::to_rows( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -2018,8 +1983,6 @@ View::to_rows( return s.GetString(); } - t_uindex depth = m_row_pivots.size(); - std::vector column_names; for (auto c = start_col + 1; c < end_col; ++c) { if (c > columns_length) { @@ -2032,12 +1995,6 @@ View::to_rows( } for (auto r = start_row; r < end_row; ++r) { - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } - // Row writer.StartObject(); @@ -2104,7 +2061,6 @@ View::to_rows( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -2129,15 +2085,8 @@ View::to_rows( column_names.emplace_back(col_path_to_legacy(col_names.at(c))); } - t_uindex depth = m_row_pivots.size(); bool column_only = is_column_only(); for (auto r = start_row; r < end_row; ++r) { - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } - // Row writer.StartObject(); @@ -2257,7 +2206,6 @@ View::to_ndjson( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -2279,8 +2227,6 @@ View::to_ndjson( ); } - t_uindex depth = m_row_pivots.size(); - // These columns don't exist as far as the view/table is concerned. They're // scoped to the serialization of the view itself. // @@ -2299,11 +2245,6 @@ View::to_ndjson( rapidjson::StringBuffer s; rapidjson::Writer writer(s); - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } writer.StartObject(); if (get_ids) { @@ -2354,7 +2295,6 @@ View::to_ndjson( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -2369,7 +2309,6 @@ View::to_ndjson( return ""; } - t_uindex depth = m_row_pivots.size(); std::vector column_names; for (auto c = start_col + 1; c < end_col; ++c) { if (c > columns_length) { @@ -2389,11 +2328,6 @@ View::to_ndjson( rapidjson::StringBuffer s; rapidjson::Writer writer(s); - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } // Row writer.StartObject(); @@ -2462,7 +2396,6 @@ View::to_ndjson( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -2482,7 +2415,6 @@ View::to_ndjson( column_names.emplace_back(col_path_to_legacy(col_names.at(c))); } - t_uindex depth = m_row_pivots.size(); bool column_only = is_column_only(); std::stringstream ndjson; for (auto r = start_row; r < end_row; ++r) { @@ -2492,11 +2424,6 @@ View::to_ndjson( rapidjson::StringBuffer s; rapidjson::Writer writer(s); - if (has_row_path && leaves_only) { - if (m_ctx->unity_get_row_depth(r) < depth) { - continue; - } - } // Row writer.StartObject(); @@ -2566,7 +2493,6 @@ View::to_columns( bool is_formatted, bool get_pkeys, bool get_ids, - bool _leaves_only, t_uindex num_sides, bool _has_row_path, const std::string& nidx, @@ -2588,7 +2514,6 @@ View::to_columns( start_row, end_row, false, - false, is_formatted, slice, col_names, @@ -2598,7 +2523,7 @@ View::to_columns( if (get_pkeys) { write_index_column( - start_row, end_row, false, false, is_formatted, slice, writer + start_row, end_row, false, is_formatted, slice, writer ); } @@ -2633,7 +2558,6 @@ View::to_columns( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -2648,7 +2572,7 @@ View::to_columns( rapidjson::StringBuffer s; rapidjson::Writer writer(s); writer.StartObject(); - write_row_path(start_row, end_row, true, leaves_only, is_formatted, writer); + write_row_path(start_row, end_row, true, is_formatted, writer); if (get_ids) { writer.Key("__ID__"); writer.StartArray(); @@ -2677,7 +2601,6 @@ View::to_columns( start_row, end_row, true, - leaves_only, is_formatted, slice, col_names, @@ -2687,7 +2610,7 @@ View::to_columns( if (get_pkeys) { write_index_column( - start_row, end_row, true, leaves_only, is_formatted, slice, writer + start_row, end_row, true, is_formatted, slice, writer ); } @@ -2706,7 +2629,6 @@ View::to_columns( bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -2721,7 +2643,7 @@ View::to_columns( rapidjson::Writer writer(s); writer.StartObject(); write_row_path( - start_row, end_row, has_row_path, leaves_only, is_formatted, writer + start_row, end_row, has_row_path, is_formatted, writer ); if (get_ids) { @@ -2756,7 +2678,6 @@ View::to_columns( start_row, end_row, has_row_path, - leaves_only, is_formatted, slice, col_names, @@ -2769,7 +2690,6 @@ View::to_columns( start_row, end_row, has_row_path, - leaves_only, is_formatted, slice, writer diff --git a/rust/perspective-server/cpp/perspective/src/cpp/view_config.cpp b/rust/perspective-server/cpp/perspective/src/cpp/view_config.cpp index 91414d6aca..be50f1a5ed 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/view_config.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/view_config.cpp @@ -28,7 +28,8 @@ t_view_config::t_view_config( const std::vector>& sort, const std::vector>& expressions, std::string filter_op, - bool column_only + bool column_only, + bool leaves_only ) : m_init(false), m_vocab(std::move(vocab)), @@ -42,7 +43,8 @@ t_view_config::t_view_config( m_row_pivot_depth(-1), m_column_pivot_depth(-1), m_filter_op(std::move(filter_op)), - m_column_only(column_only) {} + m_column_only(column_only), + m_leaves_only(leaves_only) {} void t_view_config::init(const std::shared_ptr& schema) { @@ -265,6 +267,11 @@ t_view_config::is_column_only() const { return m_column_only; } +bool +t_view_config::is_leaves_only() const { + return m_leaves_only; +} + std::int32_t t_view_config::get_row_pivot_depth() const { PSP_VERBOSE_ASSERT(m_init, "touching uninited object"); diff --git a/rust/perspective-server/cpp/perspective/src/include/perspective/context_one.h b/rust/perspective-server/cpp/perspective/src/include/perspective/context_one.h index bd23f7c7f3..46c496928f 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/context_one.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/context_one.h @@ -43,6 +43,7 @@ class PERSPECTIVE_EXPORT t_ctx1 : public t_ctxbase { std::vector get_aggregates() const; std::vector get_row_path(t_index idx) const; void set_depth(t_depth depth); + void set_leaves_only(bool enabled); t_index get_row_idx(const std::vector& path) const; @@ -60,6 +61,7 @@ class PERSPECTIVE_EXPORT t_ctx1 : public t_ctxbase { std::shared_ptr m_expression_tables; t_depth m_depth; bool m_depth_set; + bool m_leaves_only = false; }; } // end namespace perspective diff --git a/rust/perspective-server/cpp/perspective/src/include/perspective/context_two.h b/rust/perspective-server/cpp/perspective/src/include/perspective/context_two.h index 5fb16497f6..0a5eb6c988 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/context_two.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/context_two.h @@ -55,6 +55,7 @@ class PERSPECTIVE_EXPORT t_ctx2 : public t_ctxbase { void column_sort_by(const std::vector& sortby); void set_depth(t_header header, t_depth depth); + void set_leaves_only(bool enabled); std::pair get_min_max(const std::string& colname ) const; @@ -93,6 +94,7 @@ class PERSPECTIVE_EXPORT t_ctx2 : public t_ctxbase { t_depth m_column_depth; bool m_column_depth_set; std::shared_ptr m_expression_tables; + bool m_leaves_only = false; }; } // end namespace perspective diff --git a/rust/perspective-server/cpp/perspective/src/include/perspective/server.h b/rust/perspective-server/cpp/perspective/src/include/perspective/server.h index c6f3aef2bf..acbae6b5a1 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/server.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/server.h @@ -171,7 +171,6 @@ namespace server { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, std::string nidx, @@ -189,7 +188,6 @@ namespace server { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, std::string nidx, @@ -207,7 +205,6 @@ namespace server { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, std::string nidx, @@ -311,7 +308,6 @@ namespace server { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, std::string nidx, @@ -327,7 +323,6 @@ namespace server { is_formatted, get_pkeys, get_ids, - leaves_only, num_sides, has_row_path, nidx, @@ -347,7 +342,6 @@ namespace server { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, std::string nidx, @@ -363,7 +357,6 @@ namespace server { is_formatted, get_pkeys, get_ids, - leaves_only, num_sides, has_row_path, nidx, @@ -383,7 +376,6 @@ namespace server { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, std::string nidx, @@ -399,7 +391,6 @@ namespace server { is_formatted, get_pkeys, get_ids, - leaves_only, num_sides, has_row_path, nidx, diff --git a/rust/perspective-server/cpp/perspective/src/include/perspective/traversal.h b/rust/perspective-server/cpp/perspective/src/include/perspective/traversal.h index aa7dd9776e..00fb379cc9 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/traversal.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/traversal.h @@ -132,9 +132,23 @@ class t_traversal { void populate_root_children(const t_stnode_vec& rchildren); void populate_root_children(const std::shared_ptr& tree); + void set_leaves_only(bool enabled, t_uindex leaf_depth); + bool is_leaves_only() const; + void rebuild_from_leaves(const std::vector& sortby); + private: + void collect_leaves( + t_uindex tnid, + t_uindex current_depth, + const std::vector& sortby, + const std::vector& sortby_agg_indices, + const std::vector& sort_orders + ); + std::shared_ptr m_tree; std::shared_ptr> m_nodes; + bool m_leaves_only = false; + t_uindex m_leaf_depth = 0; }; /** diff --git a/rust/perspective-server/cpp/perspective/src/include/perspective/view.h b/rust/perspective-server/cpp/perspective/src/include/perspective/view.h index 97d3690758..f69a8bd43d 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/view.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/view.h @@ -155,7 +155,6 @@ class PERSPECTIVE_EXPORT View { t_uindex start_row, t_uindex end_row, bool has_row_path, - bool leaves_only, bool is_formatted, rapidjson::Writer& writer ) const; @@ -165,7 +164,6 @@ class PERSPECTIVE_EXPORT View { t_uindex start_row, t_uindex end_row, bool has_row_path, - bool leaves_only, bool is_formatted, std::shared_ptr> slice, const std::vector>& col_names, @@ -177,7 +175,6 @@ class PERSPECTIVE_EXPORT View { t_uindex start_col, t_uindex end_col, bool has_row_path, - bool leaves_only, bool ids, bool pkeys, bool is_formatted, @@ -190,7 +187,6 @@ class PERSPECTIVE_EXPORT View { t_uindex start_row, t_uindex end_row, bool has_row_path, - bool leaves_only, bool is_formatted, std::shared_ptr> slice, rapidjson::Writer& writer @@ -224,7 +220,6 @@ class PERSPECTIVE_EXPORT View { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -241,7 +236,6 @@ class PERSPECTIVE_EXPORT View { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, @@ -258,7 +252,6 @@ class PERSPECTIVE_EXPORT View { bool is_formatted, bool get_pkeys, bool get_ids, - bool leaves_only, t_uindex num_sides, bool has_row_path, const std::string& nidx, diff --git a/rust/perspective-server/cpp/perspective/src/include/perspective/view_config.h b/rust/perspective-server/cpp/perspective/src/include/perspective/view_config.h index 757715f57b..8c0bc35695 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/view_config.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/view_config.h @@ -57,7 +57,8 @@ class PERSPECTIVE_EXPORT t_view_config { const std::vector>& sort, const std::vector>& expressions, std::string filter_op, - bool column_only + bool column_only, + bool leaves_only = false ); /** @@ -119,6 +120,8 @@ class PERSPECTIVE_EXPORT t_view_config { bool is_column_only() const; + bool is_leaves_only() const; + std::int32_t get_row_pivot_depth() const; std::int32_t get_column_pivot_depth() const; @@ -238,5 +241,6 @@ class PERSPECTIVE_EXPORT t_view_config { * */ bool m_column_only; + bool m_leaves_only; }; } // end namespace perspective \ No newline at end of file diff --git a/rust/perspective-viewer/src/less/config-selector.less b/rust/perspective-viewer/src/less/config-selector.less index df4e96a6b2..08d6dfd54f 100644 --- a/rust/perspective-viewer/src/less/config-selector.less +++ b/rust/perspective-viewer/src/less/config-selector.less @@ -270,6 +270,30 @@ display: inline-block; } + .pivot_controls { + display: flex; + justify-content: flex-end; + height: 15px; + margin-bottom: -24px; + margin-right: 28px; + margin-top: 9px; + select:hover { + color: var(--icon--color, inherit); + } + } + + .group_rollup_wrapper { + width: 48px; + margin-bottom: -30px; + flex: 0 1 auto; + color: var(--inactive--color); + padding-top: 15px; + font-size: 9px; + select { + font-size: 9px !important; + } + } + #transpose_button { cursor: pointer; flex-grow: 0; @@ -278,12 +302,12 @@ user-select: none; padding: 0; align-self: center; - margin-bottom: -23px; - margin-top: 11.5px; - align-self: flex-end; + // margin-bottom: -23px; + // margin-top: 11.5px; + // align-self: flex-end; z-index: 1; min-height: 0px; - margin-right: 30px; + // margin-right: 30px; &:hover:before { color: var(--icon--color, inherit); diff --git a/rust/perspective-viewer/src/rust/components/column_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector.rs index daa16a9fcf..d590cf8168 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector.rs @@ -355,11 +355,7 @@ impl Component for ColumnSelector { }) .collect(); - let size = if !inactive_children.is_empty() { - 56.0 - } else { - 28.0 - }; + let size = 28.0; let add_column = if ctx .props() diff --git a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs index 7f97441a19..4e1e4b282e 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs @@ -23,6 +23,7 @@ use super::filter_column::*; use super::pivot_column::*; use super::sort_column::*; use crate::components::containers::dragdrop_list::*; +use crate::components::containers::select::{Select, SelectItem}; use crate::components::style::LocalStyle; use crate::custom_elements::{ColumnDropDownElement, FilterDropDownElement}; use crate::dragdrop::*; @@ -32,7 +33,7 @@ use crate::session::*; use crate::utils::*; use crate::{PerspectiveProperties, css}; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Clone, Properties, PerspectiveProperties!)] pub struct ConfigSelectorProps { pub onselect: Callback<()>, @@ -450,18 +451,55 @@ impl Component for ConfigSelector { let metadata = session.metadata(); let features = metadata.get_features().unwrap(); + let requirements = renderer.metadata(); + + let on_group_rollup_mode = Callback::from({ + let props = ctx.props().clone(); + move |x| { + let config = ViewConfigUpdate { + group_rollup_mode: Some(x), + ..ViewConfigUpdate::default() + }; + + props + .update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); + } + }); html! {
- if !config.group_by.is_empty() && config.split_by.is_empty() { - - } +
+ if !config.group_by.is_empty() { + if requirements.group_rollups.as_ref().map(|x| x.len()).unwrap_or_default() > 1 { + + id="group_rollup_mode_selector" + wrapper_class="group_rollup_wrapper" + values={Rc::new( + requirements + .group_rollups + .as_ref() + .unwrap() + .iter() + .map(|x| SelectItem::Option(*x)) + .collect(), + )} + selected={config.group_rollup_mode} + on_select={on_group_rollup_mode} + /> + } + if config.split_by.is_empty() { + + } + } +
if features.group_by { +
+ +
} } diff --git a/rust/perspective-viewer/src/rust/components/containers/select.rs b/rust/perspective-viewer/src/rust/components/containers/select.rs index aef0a649cc..e3af125533 100644 --- a/rust/perspective-viewer/src/rust/components/containers/select.rs +++ b/rust/perspective-viewer/src/rust/components/containers/select.rs @@ -220,9 +220,9 @@ where }; let value = if ctx.props().is_autosize { - self.selected.to_string() + Some(self.selected.to_string()) } else { - "".to_owned() + None }; html! { @@ -230,7 +230,7 @@ where -
{ select }
+
{ select }
} else {
{ select }
} diff --git a/rust/perspective-viewer/src/rust/components/plugin_selector.rs b/rust/perspective-viewer/src/rust/components/plugin_selector.rs index d3001ef823..219bdad458 100644 --- a/rust/perspective-viewer/src/rust/components/plugin_selector.rs +++ b/rust/perspective-viewer/src/rust/components/plugin_selector.rs @@ -83,11 +83,18 @@ impl Component for PluginSelector { let metadata = renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name)); - let mut update = ViewConfigUpdate::default(); - session.set_update_column_defaults( - &mut update, - metadata.as_ref().unwrap_or(&*renderer.metadata()), - ); + let prev_metadata = renderer.metadata(); + let requirements = metadata.as_ref().unwrap_or(&*prev_metadata); + let mut update = ViewConfigUpdate { + group_rollup_mode: requirements + .group_rollups + .as_ref() + .and_then(|x| x.first()) + .cloned(), + ..ViewConfigUpdate::default() + }; + + session.set_update_column_defaults(&mut update, requirements); if let Ok(task) = ctx.props().update_and_render(update) { ApiFuture::spawn(task); diff --git a/rust/perspective-viewer/src/rust/js/plugin.rs b/rust/perspective-viewer/src/rust/js/plugin.rs index 664660dd27..973e553998 100644 --- a/rust/perspective-viewer/src/rust/js/plugin.rs +++ b/rust/perspective-viewer/src/rust/js/plugin.rs @@ -10,6 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use perspective_client::config::GroupRollupMode; use perspective_js::utils::*; use serde::*; use wasm_bindgen::prelude::*; @@ -61,6 +62,9 @@ extern "C" { #[wasm_bindgen(method, getter)] pub fn priority(this: &JsPerspectiveViewerPlugin) -> Option; + #[wasm_bindgen(method, getter)] + pub fn group_rollups(this: &JsPerspectiveViewerPlugin) -> Option; + /// Don't call this method directly. Instead, call the corresponding method on the PluginColumnStyles model. #[wasm_bindgen(method, catch)] pub fn can_render_column_styles(this: &JsPerspectiveViewerPlugin, view_type: &str, group: Option<&str>) -> ApiResult; @@ -146,6 +150,7 @@ pub struct ViewConfigRequirements { pub max_cells: Option, pub name: String, pub render_warning: bool, + pub group_rollups: Option>, } impl ViewConfigRequirements { @@ -169,6 +174,9 @@ impl JsPerspectiveViewerPlugin { max_cells: self.max_cells(), name: self.name(), render_warning: self.render_warning().unwrap_or(true), + group_rollups: self + .group_rollups() + .map(|x| x.into_serde_ext::>().unwrap()), }) } } diff --git a/rust/perspective-viewer/src/rust/session/column_defaults_update.rs b/rust/perspective-viewer/src/rust/session/column_defaults_update.rs index 69e9141230..74ee6b45d7 100644 --- a/rust/perspective-viewer/src/rust/session/column_defaults_update.rs +++ b/rust/perspective-viewer/src/rust/session/column_defaults_update.rs @@ -31,6 +31,30 @@ pub impl ViewConfigUpdate { columns: &[Option], requirements: &ViewConfigRequirements, ) { + if requirements + .group_rollups + .as_ref() + .map(|x| { + !x.contains( + self.group_rollup_mode + .as_ref() + .unwrap_or(&GroupRollupMode::Rollup), + ) + }) + .unwrap_or_default() + { + self.group_rollup_mode = requirements + .group_rollups + .as_ref() + .and_then(|x| x.first()) + .cloned(); + + tracing::error!( + "Setting plugin-advised rollup mode {:?}", + self.group_rollup_mode + ); + } + if let ( None, ViewConfigRequirements { diff --git a/rust/perspective-viewer/src/rust/session/replace_expression_update.rs b/rust/perspective-viewer/src/rust/session/replace_expression_update.rs index ae3a506328..ac1a4087ea 100644 --- a/rust/perspective-viewer/src/rust/session/replace_expression_update.rs +++ b/rust/perspective-viewer/src/rust/session/replace_expression_update.rs @@ -125,6 +125,7 @@ pub impl ViewConfig { filter: Some(filter), filter_op: None, group_by_depth: None, + group_rollup_mode: None, } } } diff --git a/rust/perspective-viewer/test/js/dom.spec.js b/rust/perspective-viewer/test/js/dom.spec.js index 2a4ae630ca..75955b82d5 100644 --- a/rust/perspective-viewer/test/js/dom.spec.js +++ b/rust/perspective-viewer/test/js/dom.spec.js @@ -77,6 +77,7 @@ const RESULT = { table: "load-viewer-csv", theme: "Pro Light", title: null, + group_rollup_mode: "rollup", }; test.describe("DOM API", () => { diff --git a/rust/perspective-viewer/test/js/events.spec.ts b/rust/perspective-viewer/test/js/events.spec.ts index 1256a28e2b..60ef965ee4 100644 --- a/rust/perspective-viewer/test/js/events.spec.ts +++ b/rust/perspective-viewer/test/js/events.spec.ts @@ -80,6 +80,7 @@ test.describe("Events", () => { plugin: "Debug", plugin_config: {}, group_by: ["State"], + group_rollup_mode: "rollup", settings: true, sort: [], table: "load-viewer-csv", diff --git a/tools/test/results.tar.gz b/tools/test/results.tar.gz index df0165f9f0..31f2865855 100644 Binary files a/tools/test/results.tar.gz and b/tools/test/results.tar.gz differ diff --git a/tools/test/src/js/utils.ts b/tools/test/src/js/utils.ts index 4a1d567504..9da4a299bb 100644 --- a/tools/test/src/js/utils.ts +++ b/tools/test/src/js/utils.ts @@ -31,6 +31,7 @@ export const DEFAULT_CONFIG: ViewerConfigUpdate = { expressions: {}, filter: [], group_by: [], + group_rollup_mode: "rollup", plugin: "", plugin_config: {}, settings: false,