From 0735300158ffdfc796809637b32e413817548ebf Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 8 Mar 2026 16:15:03 -0400 Subject: [PATCH 1/3] Add `total` mode Signed-off-by: Andrew Stein --- .../src/ts/custom_elements/datagrid.ts | 2 +- rust/perspective-client/perspective.proto | 2 +- .../src/rust/config/view_config.rs | 41 +++++- .../test/js/group_rollup_mode.spec.js | 121 ++++++++++++++++++ .../cpp/perspective/src/cpp/context_one.cpp | 12 +- .../cpp/perspective/src/cpp/context_two.cpp | 10 +- .../cpp/perspective/src/cpp/server.cpp | 28 +++- .../cpp/perspective/src/cpp/traversal.cpp | 26 ++++ .../src/cpp/tree_context_common.cpp | 8 +- .../cpp/perspective/src/cpp/view.cpp | 6 +- .../cpp/perspective/src/cpp/view_config.cpp | 11 +- .../src/include/perspective/context_one.h | 2 + .../src/include/perspective/context_two.h | 2 + .../src/include/perspective/traversal.h | 5 + .../src/include/perspective/view_config.h | 5 +- .../src/less/column-selector.less | 4 +- .../src/less/config-selector.less | 38 ++++++ .../src/rust/components/column_selector.rs | 38 ++++-- .../column_selector/config_selector.rs | 111 +++++++++------- .../column_selector/pivot_column.rs | 19 ++- .../components/containers/dragdrop_list.rs | 33 ++++- .../components/containers/scroll_panel.rs | 9 +- .../rust/components/containers/split_panel.rs | 4 +- 23 files changed, 436 insertions(+), 101 deletions(-) diff --git a/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts b/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts index 6b4a219fc5..4a1e0913f1 100644 --- a/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts +++ b/packages/viewer-datagrid/src/ts/custom_elements/datagrid.ts @@ -134,7 +134,7 @@ export class HTMLPerspectiveViewerDatagridPluginElement } get group_rollups(): string[] { - return ["rollup", "flat"]; + return ["rollup", "flat", "total"]; } /** diff --git a/rust/perspective-client/perspective.proto b/rust/perspective-client/perspective.proto index fbdf8a1b39..104ceaa175 100644 --- a/rust/perspective-client/perspective.proto +++ b/rust/perspective-client/perspective.proto @@ -544,7 +544,7 @@ message ViewConfig { enum GroupRollupMode { ROLLUP = 0; FLAT = 1; - // TOTAL = 2; + TOTAL = 2; } } diff --git a/rust/perspective-client/src/rust/config/view_config.rs b/rust/perspective-client/src/rust/config/view_config.rs index 43a2d9c391..2968849e88 100644 --- a/rust/perspective-client/src/rust/config/view_config.rs +++ b/rust/perspective-client/src/rust/config/view_config.rs @@ -31,8 +31,9 @@ pub enum GroupRollupMode { #[serde(rename = "flat")] Flat, - // #[serde(rename = "total")] - // Total, + + #[serde(rename = "total")] + Total, } impl Display for GroupRollupMode { @@ -40,7 +41,7 @@ impl Display for GroupRollupMode { write!(fmt, "{}", match self { Self::Rollup => "Rollup", Self::Flat => "Flat", - // Self::Total => "Total", + Self::Total => "Total", }) } } @@ -50,7 +51,7 @@ impl From for GroupRollupMode { match value { proto::view_config::GroupRollupMode::Rollup => Self::Rollup, proto::view_config::GroupRollupMode::Flat => Self::Flat, - // proto::view_config::GroupRollupMode::Total => Self::Total, + proto::view_config::GroupRollupMode::Total => Self::Total, } } } @@ -60,7 +61,7 @@ impl From for proto::view_config::GroupRollupMode { match value { GroupRollupMode::Rollup => proto::view_config::GroupRollupMode::Rollup, GroupRollupMode::Flat => proto::view_config::GroupRollupMode::Flat, - // GroupRollupMode::Total => proto::view_config::GroupRollupMode::Total, + GroupRollupMode::Total => proto::view_config::GroupRollupMode::Total, } } } @@ -404,8 +405,28 @@ impl ViewConfig { /// Apply `ViewConfigUpdate` to a `ViewConfig`, ignoring any fields in /// `update` which were unset. - pub fn apply_update(&mut self, update: ViewConfigUpdate) -> bool { + pub fn apply_update(&mut self, mut update: ViewConfigUpdate) -> bool { let mut changed = false; + if ((self.group_rollup_mode == GroupRollupMode::Total + && update.group_rollup_mode.is_none()) + || update.group_rollup_mode == Some(GroupRollupMode::Total)) + && update + .group_by + .as_ref() + .map(|x| !x.is_empty()) + .unwrap_or_default() + { + tracing::warn!("`total` incompatible with `group_by`"); + changed = true; + update.group_rollup_mode = Some(GroupRollupMode::Rollup); + } + + if update.group_rollup_mode == Some(GroupRollupMode::Total) && !self.group_by.is_empty() { + tracing::warn!("`group_by` incompatible with `total`"); + changed = true; + update.group_by = Some(vec![]); + } + changed = Self::_apply(&mut self.group_by, update.group_by) || changed; changed = Self::_apply(&mut self.split_by, update.split_by) || changed; changed = Self::_apply(&mut self.columns, update.columns) || changed; @@ -414,11 +435,17 @@ impl ViewConfig { 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; + if self.group_rollup_mode == GroupRollupMode::Total && !self.group_by.is_empty() { + tracing::warn!("`total` incompatible with `group_by`"); + changed = true; + self.group_by = vec![]; + } + changed } pub fn is_aggregated(&self) -> bool { - !self.group_by.is_empty() + !self.group_by.is_empty() || self.group_rollup_mode == GroupRollupMode::Total } pub fn is_column_expression_in_use(&self, name: &str) -> bool { diff --git a/rust/perspective-js/test/js/group_rollup_mode.spec.js b/rust/perspective-js/test/js/group_rollup_mode.spec.js index 2a99f8f6be..60635c2bce 100644 --- a/rust/perspective-js/test/js/group_rollup_mode.spec.js +++ b/rust/perspective-js/test/js/group_rollup_mode.spec.js @@ -483,5 +483,126 @@ const data = { table.delete(); }); }); + + test.describe("total", function () { + test.skip("returns only grand total with group_by", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_by: ["y"], + group_rollup_mode: "total", + }); + const json = await view.to_json(); + expect(json).toStrictEqual([ + { __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 4 }, + ]); + view.delete(); + table.delete(); + }); + + test("returns only grand total schema", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_rollup_mode: "total", + }); + + const json = await view.schema(); + expect(json).toStrictEqual({ + w: "float", + x: "integer", + y: "integer", + z: "integer", + }); + + view.delete(); + table.delete(); + }); + + test("returns only grand total", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_rollup_mode: "total", + }); + + const json = await view.to_json(); + expect(json).toStrictEqual([ + { __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 8 }, + ]); + + view.delete(); + table.delete(); + }); + + test("num_rows returns 1 without group_by", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_rollup_mode: "total", + }); + + const num_rows = await view.num_rows(); + expect(num_rows).toEqual(1); + view.delete(); + table.delete(); + }); + + test("to_columns works", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_rollup_mode: "total", + }); + + const cols = await view.to_columns(); + expect(cols).toStrictEqual({ + w: [40], + x: [20], + y: [8], + z: [8], + }); + + view.delete(); + table.delete(); + }); + + test("with split_by", async function () { + const table = await perspective.table(data); + const view = await table.view({ + split_by: ["z"], + group_rollup_mode: "total", + }); + const json = await view.to_json(); + expect(json).toStrictEqual([ + { + __ROW_PATH__: [], + "false|w": 22, + "false|x": 10, + "false|y": 4, + "false|z": 4, + "true|w": 18, + "true|x": 10, + "true|y": 4, + "true|z": 4, + }, + ]); + view.delete(); + table.delete(); + }); + + test("updates after table.update()", async function () { + const table = await perspective.table(data); + const view = await table.view({ + group_rollup_mode: "total", + }); + const before = await view.to_json(); + expect(before).toStrictEqual([ + { __ROW_PATH__: [], w: 40, x: 20, y: 8, z: 8 }, + ]); + table.update([{ w: 10, x: 5, y: "e", z: true }]); + const after = await view.to_json(); + expect(after).toStrictEqual([ + { __ROW_PATH__: [], w: 50, x: 25, y: 9, z: 9 }, + ]); + view.delete(); + table.delete(); + }); + }); }); })(perspective); 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 7d4f7a3939..6f4e26fcb7 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/context_one.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/context_one.cpp @@ -346,7 +346,9 @@ void t_ctx1::step_end() { PSP_TRACE_SENTINEL(); PSP_VERBOSE_ASSERT(m_init, "touching uninited object"); - if (m_leaves_only) { + if (m_total_only) { + m_traversal->rebuild_for_total(); + } else if (m_leaves_only) { m_traversal->rebuild_from_leaves(m_sortby); } else { sort_by(m_sortby); @@ -436,6 +438,14 @@ t_ctx1::set_leaves_only(bool enabled) { m_traversal->set_leaves_only(enabled, m_config.get_num_rpivots()); } +void +t_ctx1::set_total_only(bool enabled) { + PSP_TRACE_SENTINEL(); + PSP_VERBOSE_ASSERT(m_init, "touching uninited object"); + m_total_only = enabled; + m_traversal->set_total_only(enabled); +} + std::vector t_ctx1::get_pkeys(const std::vector>& cells ) const { 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 d4c5980500..28c8be68a3 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/context_two.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/context_two.cpp @@ -110,7 +110,9 @@ t_ctx2::step_begin() { void t_ctx2::step_end() { - if (m_leaves_only) { + if (m_total_only) { + m_rtraversal->rebuild_for_total(); + } else if (m_leaves_only) { m_rtraversal->rebuild_from_leaves(m_sortby); } else { if (m_row_depth_set) { @@ -945,6 +947,12 @@ t_ctx2::set_leaves_only(bool enabled) { m_rtraversal->set_leaves_only(enabled, m_config.get_num_rpivots()); } +void +t_ctx2::set_total_only(bool enabled) { + m_total_only = enabled; + m_rtraversal->set_total_only(enabled); +} + std::vector t_ctx2::get_pkeys(const std::vector>& cells ) const { diff --git a/rust/perspective-server/cpp/perspective/src/cpp/server.cpp b/rust/perspective-server/cpp/perspective/src/cpp/server.cpp index 0680f4d6a9..1dc79e0b66 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 (view_config->is_leaves_only()) { + if (view_config->is_total_only()) { + ctx1->set_total_only(true); + } else if (view_config->is_leaves_only()) { ctx1->set_leaves_only(true); } else if (row_pivot_depth > -1) { ctx1->set_depth(row_pivot_depth - 1); @@ -195,7 +197,9 @@ make_context( ctx2->column_sort_by(col_sortspec); } - if (view_config->is_leaves_only() && !column_only) { + if (view_config->is_total_only() && !column_only) { + ctx2->set_total_only(true); + } else 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); @@ -1883,11 +1887,15 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { } bool column_only = false; + bool is_total = + cfg.has_group_rollup_mode() ? cfg.group_rollup_mode() == 2 : false; // make sure that primary keys are created for column-only views if (row_pivots.empty() && !column_pivots.empty()) { row_pivots.emplace_back("psp_okey"); - column_only = true; + if (!is_total) { + column_only = true; + } } std::vector> expressions; @@ -2097,6 +2105,8 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { bool leaves_only = cfg.has_group_rollup_mode() ? cfg.group_rollup_mode() == 1 : false; + bool total_only = + cfg.has_group_rollup_mode() ? cfg.group_rollup_mode() == 2 : false; auto config = std::make_shared( vocab, @@ -2109,7 +2119,8 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { expressions, filter_op, column_only, - leaves_only + leaves_only, + total_only ); config->init(schema); @@ -2125,6 +2136,8 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { } else { sides = 1; } + } else if (total_only) { + sides = 1; } else { sides = 0; } @@ -2414,7 +2427,10 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { ); } - if (view_config->is_leaves_only()) { + if (view_config->is_total_only()) { + const auto mode = proto::ViewConfig_GroupRollupMode::ViewConfig_GroupRollupMode_TOTAL; + view_config_proto->set_group_rollup_mode(mode); + } else if (view_config->is_leaves_only()) { const auto mode = proto::ViewConfig_GroupRollupMode::ViewConfig_GroupRollupMode_FLAT; view_config_proto->set_group_rollup_mode(mode); } else { @@ -2580,7 +2596,7 @@ ProtoServer::_handle_request(std::uint32_t client_id, Request&& req) { r.index(), r.id(), view->sides(), - view->sides() > 0 && !config->is_column_only(), + view->sides() > 0 && !config->is_column_only() && !config->get_row_pivots().empty(), nidx, config->get_columns().size(), config->get_row_pivots().size() diff --git a/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp b/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp index 71a492d6f3..7838b1472b 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/traversal.cpp @@ -794,4 +794,30 @@ t_traversal::rebuild_from_leaves(const std::vector& sortby) { collect_leaves(0, 0, sortby, sortby_agg_indices, sort_orders); } +void +t_traversal::set_total_only(bool enabled) { + m_total_only = enabled; + if (m_total_only) { + rebuild_for_total(); + } +} + +bool +t_traversal::is_total_only() const { + return m_total_only; +} + +void +t_traversal::rebuild_for_total() { + m_nodes = std::make_shared>(); + t_tvnode node; + node.m_expanded = false; + node.m_depth = 0; + node.m_rel_pidx = 0; + node.m_ndesc = 0; + node.m_nchild = 0; + node.m_tnid = 0; + m_nodes->push_back(node); +} + } // 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 59863f93b6..eaba09e5dd 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 @@ -72,8 +72,10 @@ notify_sparse_tree_common( bool is_leaves_only = process_traversal && traversal != nullptr && traversal->is_leaves_only(); + bool is_total_only = + process_traversal && traversal != nullptr && traversal->is_total_only(); - if (process_traversal && !is_leaves_only) { + if (process_traversal && !is_leaves_only && !is_total_only) { t_uindex t_osize = traversal->size(); traversal->drop_tree_indices(zero_strands); t_uindex t_nsize = traversal->size(); @@ -91,7 +93,9 @@ notify_sparse_tree_common( tree->update_aggs_from_static(dctx, gstate, expression_master_table); - if (is_leaves_only) { + if (is_total_only) { + traversal->rebuild_for_total(); + } else if (is_leaves_only) { traversal->rebuild_from_leaves(ctx_sortby); } else { std::set visited; diff --git a/rust/perspective-server/cpp/perspective/src/cpp/view.cpp b/rust/perspective-server/cpp/perspective/src/cpp/view.cpp index 545fad44e3..1b044b1579 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/view.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/view.cpp @@ -458,7 +458,7 @@ View::schema() const { std::string type_string = dtype_to_str(types[agg_name]); new_schema[agg_name] = type_string; - if (!m_row_pivots.empty() && !is_column_only()) { + if ((!m_row_pivots.empty() || m_view_config->is_total_only()) && !is_column_only()) { new_schema[agg_name] = _map_aggregate_types(agg_name, new_schema[agg_name]); } @@ -537,7 +537,7 @@ View::expression_schema() const { const std::string& expression_alias = expr->get_expression_alias(); new_schema[expression_alias] = dtype_to_str(expr->get_dtype()); - if (!m_row_pivots.empty() && !is_column_only()) { + if ((!m_row_pivots.empty() || m_view_config->is_total_only()) && !is_column_only()) { new_schema[expression_alias] = _map_aggregate_types( expression_alias, new_schema[expression_alias] ); @@ -2572,7 +2572,7 @@ View::to_columns( rapidjson::StringBuffer s; rapidjson::Writer writer(s); writer.StartObject(); - write_row_path(start_row, end_row, true, is_formatted, writer); + write_row_path(start_row, end_row, has_row_path, is_formatted, writer); if (get_ids) { writer.Key("__ID__"); writer.StartArray(); 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 be50f1a5ed..0bd5b4aea0 100644 --- a/rust/perspective-server/cpp/perspective/src/cpp/view_config.cpp +++ b/rust/perspective-server/cpp/perspective/src/cpp/view_config.cpp @@ -29,7 +29,8 @@ t_view_config::t_view_config( const std::vector>& expressions, std::string filter_op, bool column_only, - bool leaves_only + bool leaves_only, + bool total_only ) : m_init(false), m_vocab(std::move(vocab)), @@ -44,7 +45,8 @@ t_view_config::t_view_config( m_column_pivot_depth(-1), m_filter_op(std::move(filter_op)), m_column_only(column_only), - m_leaves_only(leaves_only) {} + m_leaves_only(leaves_only), + m_total_only(total_only) {} void t_view_config::init(const std::shared_ptr& schema) { @@ -272,6 +274,11 @@ t_view_config::is_leaves_only() const { return m_leaves_only; } +bool +t_view_config::is_total_only() const { + return m_total_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 46c496928f..6adf0807ca 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 @@ -44,6 +44,7 @@ class PERSPECTIVE_EXPORT t_ctx1 : public t_ctxbase { std::vector get_row_path(t_index idx) const; void set_depth(t_depth depth); void set_leaves_only(bool enabled); + void set_total_only(bool enabled); t_index get_row_idx(const std::vector& path) const; @@ -62,6 +63,7 @@ class PERSPECTIVE_EXPORT t_ctx1 : public t_ctxbase { t_depth m_depth; bool m_depth_set; bool m_leaves_only = false; + bool m_total_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 0a5eb6c988..1d5942b5fb 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 @@ -56,6 +56,7 @@ class PERSPECTIVE_EXPORT t_ctx2 : public t_ctxbase { void set_depth(t_header header, t_depth depth); void set_leaves_only(bool enabled); + void set_total_only(bool enabled); std::pair get_min_max(const std::string& colname ) const; @@ -95,6 +96,7 @@ class PERSPECTIVE_EXPORT t_ctx2 : public t_ctxbase { bool m_column_depth_set; std::shared_ptr m_expression_tables; bool m_leaves_only = false; + bool m_total_only = false; }; } // end namespace perspective 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 00fb379cc9..2a9bdb0810 100644 --- a/rust/perspective-server/cpp/perspective/src/include/perspective/traversal.h +++ b/rust/perspective-server/cpp/perspective/src/include/perspective/traversal.h @@ -136,6 +136,10 @@ class t_traversal { bool is_leaves_only() const; void rebuild_from_leaves(const std::vector& sortby); + void set_total_only(bool enabled); + bool is_total_only() const; + void rebuild_for_total(); + private: void collect_leaves( t_uindex tnid, @@ -149,6 +153,7 @@ class t_traversal { std::shared_ptr> m_nodes; bool m_leaves_only = false; t_uindex m_leaf_depth = 0; + bool m_total_only = false; }; /** 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 8c0bc35695..5a334b5def 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 @@ -58,7 +58,8 @@ class PERSPECTIVE_EXPORT t_view_config { const std::vector>& expressions, std::string filter_op, bool column_only, - bool leaves_only = false + bool leaves_only = false, + bool total_only = false ); /** @@ -121,6 +122,7 @@ class PERSPECTIVE_EXPORT t_view_config { bool is_column_only() const; bool is_leaves_only() const; + bool is_total_only() const; std::int32_t get_row_pivot_depth() const; std::int32_t get_column_pivot_depth() const; @@ -242,5 +244,6 @@ class PERSPECTIVE_EXPORT t_view_config { */ bool m_column_only; bool m_leaves_only; + bool m_total_only; }; } // end namespace perspective \ No newline at end of file diff --git a/rust/perspective-viewer/src/less/column-selector.less b/rust/perspective-viewer/src/less/column-selector.less index 5a041378e7..e0d14c523a 100644 --- a/rust/perspective-viewer/src/less/column-selector.less +++ b/rust/perspective-viewer/src/less/column-selector.less @@ -317,11 +317,11 @@ @include scrollbar; } - #sub-columns:before { + #sub-columns .scroll-panel-container:before { font-size: var(--label--font-size, 0.75em); padding: var(--column_type--padding, 0px 0px 0px 0px); position: absolute; - margin-top: 14px; + margin-top: -13px; top: 0; content: var(--all-columns-label--content, "All Columns"); } diff --git a/rust/perspective-viewer/src/less/config-selector.less b/rust/perspective-viewer/src/less/config-selector.less index 08d6dfd54f..7d1b07048b 100644 --- a/rust/perspective-viewer/src/less/config-selector.less +++ b/rust/perspective-viewer/src/less/config-selector.less @@ -30,6 +30,44 @@ } } + #top_panel.group-rollup-mode-total { + #group_by { + width: 100%; + // height: 26px; + .pivot-column { + .pivot-column-total { + min-height: 24px; + margin-bottom: 4px; + &:before { + background-color: var(--plugin--background); + } + } + + .column_name { + color: var(--inactive--color); + } + + .type-icon { + background-color: var(--inactive--color); + } + + &:hover .pivot-column-border { + border-color: var(--inactive--color, #ababab); + } + + // input { + // background-color: var(--plugin--background); + // pointer-events: none; + // border: 1px solid var(--inactive--color); + // color: var(--inactive--color) !important; + // // &:placeholder { + + // // } + // } + } + } + } + #top_panel { display: flex; flex-direction: column; diff --git a/rust/perspective-viewer/src/rust/components/column_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector.rs index d590cf8168..6911442bff 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector.rs @@ -28,6 +28,7 @@ use std::rc::Rc; pub use empty_column::*; pub use invalid_column::*; use perspective_js::utils::ApiFuture; +pub use pivot_column::*; use web_sys::*; use yew::prelude::*; @@ -77,6 +78,7 @@ pub enum ColumnSelectorMsg { TableLoaded, ViewCreated, HoverActiveIndex(Option), + SetWidth(f64), Drag(DragEffect), DragEnd, Drop((String, DragTarget, DragEffect, usize)), @@ -91,6 +93,7 @@ pub struct ColumnSelector { named_row_count: usize, drag_container: DragDropContainer, column_dropdown: ColumnDropDownElement, + viewport_width: f64, on_reset: Rc>, } @@ -147,6 +150,7 @@ impl Component for ColumnSelector { Self { _subscriptions: [table_sub, view_sub, drop_sub, drag_sub, dragend_sub], named_row_count, + viewport_width: 0f64, drag_container, column_dropdown, on_reset: Default::default(), @@ -157,6 +161,10 @@ impl Component for ColumnSelector { match msg { Drag(DragEffect::Move(DragTarget::Active)) => false, Drag(_) | DragEnd | TableLoaded => true, + SetWidth(w) => { + self.viewport_width = w; + false + }, ViewCreated => { let named = maybe! { let plugin = @@ -385,8 +393,8 @@ impl Component for ColumnSelector { inactive_children.insert(0, add_column); } - let selected_columns = html! { -
+ let mut selected_columns = vec![html! { +
>()} />
- }; + }]; + + if !inactive_children.is_empty() { + selected_columns.push(html! { + + }) + } html! { <> @@ -411,15 +433,7 @@ impl Component for ColumnSelector { skip_empty=true orientation={Orientation::Vertical} > - { selected_columns } - if !inactive_children.is_empty() { - - } + { for selected_columns } } 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 4e1e4b282e..4505d669ba 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 @@ -64,6 +64,7 @@ pub enum ConfigSelectorMsg { TransposePivots, ViewCreated, New(DragTarget, InPlaceColumn), + UpdateGroupRollupMode(GroupRollupMode), } #[derive(Clone)] @@ -136,11 +137,9 @@ impl Component for ConfigSelector { ctx.props().onselect.emit(()); false }, - ConfigSelectorMsg::Close(index, DragTarget::GroupBy) => { - let mut group_by = ctx.props().session.get_view_config().group_by.clone(); - group_by.remove(index); + ConfigSelectorMsg::UpdateGroupRollupMode(mode) => { let config = ViewConfigUpdate { - group_by: Some(group_by), + group_rollup_mode: Some(mode), ..ViewConfigUpdate::default() }; @@ -149,9 +148,38 @@ impl Component for ConfigSelector { .map(ApiFuture::spawn) .unwrap_or_log(); - ctx.props().onselect.emit(()); false }, + ConfigSelectorMsg::Close(index, DragTarget::GroupBy) => { + if ctx.props().session.get_view_config().group_rollup_mode == GroupRollupMode::Total + { + let requirements = ctx.props().renderer.metadata(); + ctx.link() + .send_message(ConfigSelectorMsg::UpdateGroupRollupMode( + requirements + .group_rollups + .as_ref() + .and_then(|x| x.first().cloned()) + .unwrap_or_default(), + )); + false + } else { + let mut group_by = ctx.props().session.get_view_config().group_by.clone(); + group_by.remove(index); + let config = ViewConfigUpdate { + group_by: Some(group_by), + ..ViewConfigUpdate::default() + }; + + ctx.props() + .update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); + + ctx.props().onselect.emit(()); + false + } + }, ConfigSelectorMsg::Close(index, DragTarget::SplitBy) => { let mut split_by = ctx.props().session.get_view_config().split_by.clone(); split_by.remove(index); @@ -228,6 +256,7 @@ impl Component for ConfigSelector { ctx.props().onselect.emit(()); false }, + ConfigSelectorMsg::SetFilterValue(index, input) => { let mut filter = ctx.props().session.get_view_config().filter.clone(); @@ -438,11 +467,15 @@ impl Component for ConfigSelector { let config = session.get_view_config(); let transpose = ctx.link().callback(|_| ConfigSelectorMsg::TransposePivots); let column_dropdown = self.column_dropdown.clone(); - let class = if dragdrop.get_drag_column().is_some() { - "dragdrop-highlight" - } else { - "" - }; + let mut class = classes!(); + + if dragdrop.get_drag_column().is_some() { + class.push("dragdrop-highlight"); + } + + if config.group_rollup_mode == GroupRollupMode::Total { + class.push("group-rollup-mode-total"); + } let dragend = Callback::from({ let dragdrop = dragdrop.clone(); @@ -452,32 +485,19 @@ 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(); - } - }); + let on_group_rollup_mode = ctx + .link() + .callback(ConfigSelectorMsg::UpdateGroupRollupMode); html! {
- 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( + 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() @@ -486,23 +506,23 @@ impl Component for ConfigSelector { .map(|x| SelectItem::Option(*x)) .collect(), )} - selected={config.group_rollup_mode} - on_select={on_group_rollup_mode} - /> - } - if config.split_by.is_empty() { - - } + selected={config.group_rollup_mode} + on_select={on_group_rollup_mode} + /> + } + if !config.group_by.is_empty() && config.split_by.is_empty() { + }
if features.group_by { >()} @@ -515,7 +535,7 @@ impl Component for ConfigSelector { action={DragTarget::GroupBy} column={group_by.clone()} {dragdrop} - {session} + opt_session={session} > } @@ -546,9 +566,8 @@ impl Component for ConfigSelector { + opt_session={session}> } }) } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs index 5812f875eb..71ecedab51 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs @@ -26,11 +26,15 @@ pub struct PivotColumnProps { /// Column name. pub column: String, + #[prop_or_default] + pub column_type: Option, + /// The drag starte of this column, if applicable. pub action: DragTarget, // State - pub session: Session, + #[prop_or_default] + pub opt_session: Option, pub dragdrop: DragDrop, } @@ -74,12 +78,13 @@ impl Component for PivotColumn { move |_event| dragdrop.notify_drag_end() }); - let col_type = ctx - .props() - .session - .metadata() - .get_column_table_type(&ctx.props().column) - .unwrap_or(ColumnType::Integer); + let col_type = ctx.props().column_type.unwrap_or_else(|| { + ctx.props() + .opt_session + .as_ref() + .and_then(|x| x.metadata().get_column_table_type(&ctx.props().column)) + .unwrap_or(ColumnType::Integer) + }); html! {
::Properties: DragDropListItemProps, { pub parent: Scope, + pub dragdrop: DragDrop, pub name: &'static str, pub column_dropdown: ColumnDropDownElement, pub exclude: HashSet, pub children: ChildrenWithProps, + #[prop_or_default] + pub disabled: bool, + #[prop_or_default] pub is_dragover: Option<( usize, @@ -59,6 +65,7 @@ where && self.children == other.children && self.allow_duplicates == other.allow_duplicates && self.is_dragover == other.is_dragover + && self.disabled == other.disabled } } @@ -283,20 +290,34 @@ where let column_dropdown = ctx.props().column_dropdown.clone(); let exclude = ctx.props().exclude.clone(); let on_select = ctx.props().parent.callback(V::create); + let class = classes!("rrow"); + let is_enabled = true; + html! { -
+