From 69a0ba9b4f3a6dce4736930754bd6ae9b132d852 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 03:06:48 -0500 Subject: [PATCH 01/31] Disable Windows builds --- azure-pipelines.yml | 92 ++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7d5db23dcf..863924a8eb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -408,59 +408,59 @@ jobs: # - ${{ parameters.pythonBuildSteps }} # - ${{ parameters.pythonSDistSteps }} - - job: "Windows" - pool: - vmImage: "windows-2019" + # - job: "Windows" + # pool: + # vmImage: "windows-2019" - strategy: - matrix: - Python37: - python.version: "3.7" - python_flag: "" - artifact_name: "cp37-cp37m-win64_amd" - ? ${{ if or(startsWith(variables['build.sourceBranch'], 'refs/tags/v'), eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) }} - : Python38: - python.version: "3.8" - python_flag: "--python38" - artifact_name: "cp38-cp38m-win64_amd" - Python39: - python.version: "3.9" - python_flag: "--python39" - artifact_name: "cp39-cp39m-win64_amd" + # strategy: + # matrix: + # Python37: + # python.version: "3.7" + # python_flag: "" + # artifact_name: "cp37-cp37m-win64_amd" + # ? ${{ if or(startsWith(variables['build.sourceBranch'], 'refs/tags/v'), eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) }} + # : Python38: + # python.version: "3.8" + # python_flag: "--python38" + # artifact_name: "cp38-cp38m-win64_amd" + # Python39: + # python.version: "3.9" + # python_flag: "--python39" + # artifact_name: "cp39-cp39m-win64_amd" - steps: - - ${{ parameters.initSteps }} - - ${{ parameters.pythonInitSteps }} - - ${{ parameters.windowsInitSteps }} - - ${{ parameters.pythonBuildVS2019Steps }} + # steps: + # - ${{ parameters.initSteps }} + # - ${{ parameters.pythonInitSteps }} + # - ${{ parameters.windowsInitSteps }} + # - ${{ parameters.pythonBuildVS2019Steps }} # Separate out windows wheel builds as the windows # build takes 37 years to finish - - job: "Windows_Wheels" - pool: - vmImage: "windows-2019" - condition: or(startsWith(variables['build.sourceBranch'], 'refs/tags/v'), eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) + # - job: "Windows_Wheels" + # pool: + # vmImage: "windows-2019" + # condition: or(startsWith(variables['build.sourceBranch'], 'refs/tags/v'), eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) - strategy: - matrix: - Python37: - python.version: "3.7" - python_flag: "" - artifact_name: "cp37-cp37m-win64_amd" - Python38: - python.version: "3.8" - python_flag: "--python38" - artifact_name: "cp38-cp38m-win64_amd" - Python39: - python.version: "3.9" - python_flag: "--python39" - artifact_name: "cp39-cp39m-win64_amd" + # strategy: + # matrix: + # Python37: + # python.version: "3.7" + # python_flag: "" + # artifact_name: "cp37-cp37m-win64_amd" + # Python38: + # python.version: "3.8" + # python_flag: "--python38" + # artifact_name: "cp38-cp38m-win64_amd" + # Python39: + # python.version: "3.9" + # python_flag: "--python39" + # artifact_name: "cp39-cp39m-win64_amd" - steps: - - ${{ parameters.initSteps }} - - ${{ parameters.pythonInitSteps }} - - ${{ parameters.windowsInitSteps }} - - ${{ parameters.pythonWindowsWheelVS2019Steps }} + # steps: + # - ${{ parameters.initSteps }} + # - ${{ parameters.pythonInitSteps }} + # - ${{ parameters.windowsInitSteps }} + # - ${{ parameters.pythonWindowsWheelVS2019Steps }} - job: "MacOS_Catalina" pool: From 22908d8e473bf05ba2b3ac5ce0232312043681d9 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 01:54:37 -0500 Subject: [PATCH 02/31] D3FC render-to-canvas method --- .../src/js/plugin/plugin.js | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js index aae36fcc50..01ce1afc1c 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js @@ -126,6 +126,127 @@ export function register(...plugins) { chart.plugin.max_columns = x; } + async render() { + var canvas = document.createElement("canvas"); + var container = + this.shadowRoot.querySelector("#container"); + canvas.width = container.offsetWidth; + canvas.height = container.offsetHeight; + + const context = canvas.getContext("2d"); + context.fillStyle = + window + .getComputedStyle(this) + .getPropertyValue("--plugin--background") || + "white"; + context.fillRect(0, 0, canvas.width, canvas.height); + const text_color = window + .getComputedStyle(this) + .getPropertyValue("color"); + + const svgs = Array.from( + this.shadowRoot.querySelectorAll( + "svg:not(#dragHandles)" + ) + ); + + for (const svg of svgs.reverse()) { + var img = document.createElement("img"); + // document.body.appendChild(img); + img.width = svg.parentNode.offsetWidth; + img.height = svg.parentNode.offsetHeight; + + // Pretty sure this is a chrome bug - `drawImage()` call + // without this scales incorrectly. + const new_svg = svg.cloneNode(true); + if (!new_svg.hasAttribute("viewBox")) { + new_svg.setAttribute( + "viewBox", + `0 0 ${img.width} ${img.height}` + ); + } + + new_svg.setAttribute( + "xmlns", + "http://www.w3.org/2000/svg" + ); + + for (const text of new_svg.querySelectorAll( + "text" + )) { + text.setAttribute("fill", text_color); + } + + var xml = new XMLSerializer().serializeToString( + new_svg + ); + + xml = xml.replace(/[^\x00-\x7F]/g, ""); + + const done = new Promise((x, y) => { + img.onload = x; + img.onerror = y; + }); + + try { + img.src = `data:image/svg+xml;base64,${btoa( + xml + )}`; + await done; + } catch (e) { + const done = new Promise((x, y) => { + img.onload = x; + img.onerror = y; + }); + img.src = `data:image/svg+xml;utf8,${xml}`; + await done; + } + + context.drawImage( + img, + svg.parentNode.offsetLeft, + svg.parentNode.offsetTop, + img.width, + img.height + ); + } + + const canvases = Array.from( + this.shadowRoot.querySelectorAll("canvas") + ); + + for (const canvas of canvases.reverse()) { + context.drawImage( + canvas, + canvas.parentNode.offsetLeft, + canvas.parentNode.offsetTop, + canvas.width / window.devicePixelRatio, + canvas.height / window.devicePixelRatio + ); + } + + const button = document.createElement("a"); + button.setAttribute("download", "perspective.png"); + document.body.appendChild(button); + button.addEventListener( + "click", + function dlCanvas() { + var dt = canvas.toDataURL("image/png"); // << this fails in IE/Edge... + dt = dt.replace( + /^data:image\/[^;]*/, + "data:application/octet-stream" + ); + dt = dt.replace( + /^data:application\/octet-stream/, + "data:application/octet-stream;headers=Content-Disposition%3A%20attachment%3B%20filename=perspective.png" + ); + this.href = dt; + }, + false + ); + button.click(); + } + async draw(view, end_col, end_row) { if (!this.isConnected) { return; From 97327b64512dfa2f10066932138597efdd956e33 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 01:55:40 -0500 Subject: [PATCH 03/31] Fix treemap to inline style for render-to-canvas support --- .../src/js/series/treemap/treemapLabel.js | 37 ++++++++++--------- .../src/js/series/treemap/treemapSeries.js | 11 ++++-- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLabel.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLabel.js index d105cdc70e..cb9c81b6f7 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLabel.js +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLabel.js @@ -16,10 +16,10 @@ export const labelMapExists = (d) => d.target && d.target.textAttributes ? true : false; export const toggleLabels = (nodes, treemapLevel, crossValues) => { - nodes - .selectAll("text") - .style("font-size", null) - .attr("class", (d) => textLevelHelper(d, treemapLevel, crossValues)); + nodes.selectAll("text").each(function (d, i) { + const help = textLevelHelper(d, treemapLevel, crossValues); + this.style = help; + }); const visibleNodes = selectVisibleNodes(nodes); centerLabels(visibleNodes); @@ -32,8 +32,7 @@ export const restoreLabels = (nodes) => { label .attr("dx", d.target.textAttributes.dx) .attr("dy", d.target.textAttributes.dy) - .attr("class", d.target.textAttributes.class) - .style("font-size", d.target.textAttributes["font-size"]); + .attr("style", d.target.textAttributes.style); }); }; @@ -47,7 +46,7 @@ export const preventTextCollisions = (nodes) => { .selectAll("text") .filter( (_, i, nodes) => - select(nodes[i]).attr("class") === textVisability.high + select(nodes[i]).attr("style") === textVisibility.high ) .each((_, i, nodes) => topNodes.push(nodes[i])); @@ -55,7 +54,7 @@ export const preventTextCollisions = (nodes) => { .selectAll("text") .filter( (_, i, nodes) => - select(nodes[i]).attr("class") === textVisability.low + select(nodes[i]).attr("style") === textVisibility.low ) .each((_, i, nodes) => { const lowerNode = nodes[i]; @@ -93,8 +92,8 @@ export const textOpacity = {top: 1, mid: 0.7, lower: 0}; export const selectVisibleNodes = (nodes) => nodes.filter( (_, i, nodes) => - select(nodes[i]).selectAll("text").attr("class") !== - textVisability.zero + select(nodes[i]).selectAll("text").attr("style") !== + textVisibility.zero ); export const adjustLabelsThatOverflow = (nodes) => @@ -118,7 +117,7 @@ const shrinkOrHideText = (d) => { const rectRect = rect.getBBox(); if (!needsToShrinkOrHide(d, rectRect, textRect)) { - select(d).attr("class", select(d).attr("class")); + select(d).attr("style", select(d).attr("style")); } }; @@ -136,7 +135,7 @@ const needsToShrinkOrHide = (d, rectRect, textRect) => { centerText(d); } else { select(d).style("font-size", null); - select(d).attr("class", textVisability.zero); + select(d).style("opacity", "0"); } return true; } @@ -149,15 +148,19 @@ const textLevelHelper = (d, treemapLevel, crossValues) => { .filter((x) => x !== "") .every((x) => d.crossValue.includes(x)) ) - return textVisability.zero; + return textVisibility.zero; switch (d.depth) { case treemapLevel + 1: - return textVisability.high; + return textVisibility.high; case treemapLevel + 2: - return textVisability.low; + return textVisibility.low; default: - return textVisability.zero; + return textVisibility.zero; } }; -const textVisability = {high: "top", low: "mid", zero: "lower"}; +const textVisibility = { + high: "font-size:14px;z-index:5;pointer-events: none;", + low: "font-size:8px;opacity:0.7;z-index:4;", + zero: "font-size:0px;opacity:0;z-index:4;", +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapSeries.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapSeries.js index a34baa2a74..75ac6852a5 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapSeries.js @@ -72,12 +72,17 @@ export function treemapSeries() { .style("width", (d) => calcWidth(d)) .style("height", (d) => calcHeight(d)); - color && - rects.style("fill", (d) => { + rects.style("fill", (d) => { + if (nodeLevelHelper(maxDepth, d) === nodeLevel.leaf) { if (d.data.color) { return color(d.data.color); + } else { + return root_settings.colorStyles.series; } - }); + } else { + return "transparent"; + } + }); const labels = nodesMerge .filter((d) => d.value !== 0) From dda829f1abc0d256ad52ab95615e2a713e22a6be Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 01:58:42 -0500 Subject: [PATCH 04/31] Add export menu to `perspective-viewer` with CSV, Arrow, PNG, HTML --- rust/perspective-viewer/build.js | 1 + .../src/less/dropdown-menu.less | 71 ++++++++ .../src/less/{dropdown.less => select.less} | 0 .../src/less/status-bar.less | 3 +- rust/perspective-viewer/src/less/viewer.less | 2 +- .../src/rust/components/aggregate_selector.rs | 14 +- .../components/containers/dropdown_menu.rs | 127 +++++++++++++++ .../src/rust/components/containers/mod.rs | 3 +- .../containers/{dropdown.rs => select.rs} | 37 ++--- .../src/rust/components/export_dropdown.rs | 51 ++++++ .../src/rust/components/filter_item.rs | 6 +- .../src/rust/components/mod.rs | 1 + .../src/rust/components/plugin_selector.rs | 8 +- .../src/rust/components/status_bar.rs | 38 +++-- .../src/rust/components/tests/status_bar.rs | 2 +- .../rust/custom_elements/export_dropdown.rs | 153 ++++++++++++++++++ .../src/rust/custom_elements/mod.rs | 1 + .../src/rust/js/perspective.rs | 6 + rust/perspective-viewer/src/rust/model.rs | 73 +++++++++ rust/perspective-viewer/src/rust/session.rs | 34 +++- .../src/rust/session/download.rs | 47 +++++- 21 files changed, 619 insertions(+), 59 deletions(-) create mode 100644 rust/perspective-viewer/src/less/dropdown-menu.less rename rust/perspective-viewer/src/less/{dropdown.less => select.less} (100%) create mode 100644 rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs rename rust/perspective-viewer/src/rust/components/containers/{dropdown.rs => select.rs} (86%) create mode 100644 rust/perspective-viewer/src/rust/components/export_dropdown.rs create mode 100644 rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs diff --git a/rust/perspective-viewer/build.js b/rust/perspective-viewer/build.js index 5c58d688ad..651e16c32f 100644 --- a/rust/perspective-viewer/build.js +++ b/rust/perspective-viewer/build.js @@ -29,6 +29,7 @@ const PREBUILD = [ entryPoints: [ "viewer", "column-style", + "dropdown-menu", "filter-dropdown", "expression-editor", ].map((x) => `src/less/${x}.less`), diff --git a/rust/perspective-viewer/src/less/dropdown-menu.less b/rust/perspective-viewer/src/less/dropdown-menu.less new file mode 100644 index 0000000000..f2fbf8f6f6 --- /dev/null +++ b/rust/perspective-viewer/src/less/dropdown-menu.less @@ -0,0 +1,71 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +:host { + position: fixed; + z-index: 10000; + outline: none; + font-family: "Open Sans", Arial; + font-size: 14px; + font-weight: 300; + border: inherit; + box-shadow: 0 2px 4px 0 rgb(0 0 0 / 10%); + user-select: none; + background-color: white; + padding: 6px; + + display: flex; + flex-direction: column; + min-width: 80px; + + code { + font-family: var(--interface-monospace--font-family, "Roboto Mono"), + monospace; + } + + .selected { + background-color: rgba(0, 0, 0, 0.05); + } + + .dropdown-group-label { + font-size: 10px; + } + + .dropdown-group-container { + display: flex; + flex-direction: column; + margin-left: 12px; + } + + span { + min-height: 21px; + display: inline-flex; + align-items: center; + } + + .dropdown-group-container span { + cursor: pointer; + } + + .no-results { + font-style: italics; + padding: 6px 24px; + color: #ccc; + } +} + +:host(:hover) { + .selected { + background-color: transparent; + } + + .dropdown-group-container span:hover { + background-color: rgba(0, 0, 0, 0.05); + } +} diff --git a/rust/perspective-viewer/src/less/dropdown.less b/rust/perspective-viewer/src/less/select.less similarity index 100% rename from rust/perspective-viewer/src/less/dropdown.less rename to rust/perspective-viewer/src/less/select.less diff --git a/rust/perspective-viewer/src/less/status-bar.less b/rust/perspective-viewer/src/less/status-bar.less index d8176ef1c4..b7288115be 100644 --- a/rust/perspective-viewer/src/less/status-bar.less +++ b/rust/perspective-viewer/src/less/status-bar.less @@ -149,7 +149,8 @@ font-size: 14px; } - &:hover { + &:hover, + &.modal-target { min-width: 75px; color: inherit; cursor: pointer; diff --git a/rust/perspective-viewer/src/less/viewer.less b/rust/perspective-viewer/src/less/viewer.less index 08463ee0f0..ed4feb4202 100644 --- a/rust/perspective-viewer/src/less/viewer.less +++ b/rust/perspective-viewer/src/less/viewer.less @@ -9,7 +9,7 @@ */ @import "./checkbox.less"; -@import "./dropdown.less"; +@import "./select.less"; @import "./split-panel.less"; @import "./status-bar.less"; @import "./render-warning.less"; diff --git a/rust/perspective-viewer/src/rust/components/aggregate_selector.rs b/rust/perspective-viewer/src/rust/components/aggregate_selector.rs index 3b6fc02376..fb068ea709 100644 --- a/rust/perspective-viewer/src/rust/components/aggregate_selector.rs +++ b/rust/perspective-viewer/src/rust/components/aggregate_selector.rs @@ -6,7 +6,7 @@ // of the Apache License 2.0. The full license can be found in the LICENSE // file. -use super::containers::dropdown::*; +use super::containers::select::*; use crate::config::*; use crate::model::*; use crate::renderer::*; @@ -36,7 +36,7 @@ pub enum AggregateSelectorMsg { } pub struct AggregateSelector { - aggregates: Vec>, + aggregates: Vec>, aggregate: Option, } @@ -87,13 +87,13 @@ impl Component for AggregateSelector { html! {
- + class={ "aggregate-selector" } values={ values } selected={ selected_agg } on_select={ callback }> - > + >
} } @@ -113,7 +113,7 @@ impl AggregateSelector { pub fn get_dropdown_aggregates( &self, ctx: &Context, - ) -> Vec> { + ) -> Vec> { let aggregates = ctx .props() .session @@ -129,7 +129,7 @@ impl AggregateSelector { .collect::>(); let multi_aggregates2 = if !multi_aggregates.is_empty() { - vec![DropDownItem::OptGroup("weighted mean", multi_aggregates)] + vec![SelectItem::OptGroup("weighted mean", multi_aggregates)] } else { vec![] }; @@ -138,7 +138,7 @@ impl AggregateSelector { .iter() .filter(|x| matches!(x, Aggregate::SingleAggregate(_))) .cloned() - .map(DropDownItem::Option) + .map(SelectItem::Option) .chain(multi_aggregates2); s.collect::>() diff --git a/rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs b/rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs new file mode 100644 index 0000000000..8934aa22dc --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs @@ -0,0 +1,127 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use super::select::SelectItem; +use crate::*; +use std::marker::PhantomData; +use std::rc::Rc; +use web_sys::*; +use yew::prelude::*; + +pub static CSS: &str = include_str!("../../../../build/css/dropdown-menu.css"); + +pub type DropDownMenuItem = SelectItem; + +pub enum DropDownMenuMsg { + SetPos(i32, i32), +} + +#[derive(Properties, Clone, PartialEq)] +pub struct DropDownMenuProps +where + T: Into + Clone + PartialEq + 'static, +{ + pub values: Rc>>, + pub callback: Callback, +} + +pub struct DropDownMenu +where + T: Into + Clone + PartialEq + 'static, +{ + top: i32, + left: i32, + _props: PhantomData, +} + +impl Component for DropDownMenu +where + T: Into + Clone + PartialEq + 'static, +{ + type Message = DropDownMenuMsg; + type Properties = DropDownMenuProps; + + fn create(_ctx: &Context) -> Self { + DropDownMenu { + top: 0, + left: 0, + _props: Default::default(), + } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + DropDownMenuMsg::SetPos(top, left) => { + self.top = top; + self.left = left; + true + } + } + } + + fn changed(&mut self, _ctx: &Context) -> bool { + false + } + + fn view(&self, ctx: &Context) -> Html { + let values = &ctx.props().values; + let body = if !values.is_empty() { + values + .iter() + .map(|value| match value { + DropDownMenuItem::Option(x) => { + let click = ctx.props().callback.reform({ + let value = x.clone(); + move |_: MouseEvent| value.clone() + }); + + html! { + + { x.clone().into() } + + } + } + DropDownMenuItem::OptGroup(name, xs) => { + html_template! { + { name } + + } + } + }) + .collect::() + } else { + html! { + { "No Completions" } + } + }; + + html! { + <> + + { body } + + } + } +} diff --git a/rust/perspective-viewer/src/rust/components/containers/mod.rs b/rust/perspective-viewer/src/rust/components/containers/mod.rs index 1936138882..d8ace245f1 100644 --- a/rust/perspective-viewer/src/rust/components/containers/mod.rs +++ b/rust/perspective-viewer/src/rust/components/containers/mod.rs @@ -10,9 +10,10 @@ //! `Component` types. pub mod dragdrop_list; -pub mod dropdown; +pub mod dropdown_menu; pub mod radio_list; pub mod scroll_panel; +pub mod select; pub mod split_panel; #[cfg(test)] diff --git a/rust/perspective-viewer/src/rust/components/containers/dropdown.rs b/rust/perspective-viewer/src/rust/components/containers/select.rs similarity index 86% rename from rust/perspective-viewer/src/rust/components/containers/dropdown.rs rename to rust/perspective-viewer/src/rust/components/containers/select.rs index e173f8bb93..608aaab7c2 100644 --- a/rust/perspective-viewer/src/rust/components/containers/dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/containers/select.rs @@ -13,22 +13,22 @@ use wasm_bindgen::JsCast; use yew::prelude::*; #[derive(Clone, PartialEq)] -pub enum DropDownItem { +pub enum SelectItem { Option(T), OptGroup(&'static str, Vec), } -pub enum DropDownMsg { +pub enum SelectMsg { SelectedChanged(T), } #[derive(Properties, Clone)] -pub struct DropDownProps +pub struct SelectProps where T: Clone + Display + FromStr + PartialEq + 'static, T::Err: Clone + Debug + 'static, { - pub values: Vec>, + pub values: Vec>, pub selected: T, pub on_select: Callback, @@ -39,7 +39,7 @@ where pub class: Option, } -impl PartialEq for DropDownProps +impl PartialEq for SelectProps where T: Clone + Display + FromStr + PartialEq + 'static, T::Err: Clone + Debug + 'static, @@ -51,7 +51,7 @@ where /// A `` has its own state not refelcted by `DropDownProps`. + // The ` { for ctx.props().values.iter().map(|value| match value { - DropDownItem::Option(value) => { + SelectItem::Option(value) => { let selected = *value == ctx.props().selected; html! { { for group.iter().map(|value| { diff --git a/rust/perspective-viewer/src/rust/components/export_dropdown.rs b/rust/perspective-viewer/src/rust/components/export_dropdown.rs new file mode 100644 index 0000000000..44aaf085b1 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/export_dropdown.rs @@ -0,0 +1,51 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use super::containers::dropdown_menu::*; +use crate::*; +use yew::prelude::*; + +#[derive(Clone, Copy, PartialEq)] +pub enum ExportMethod { + Csv, + CsvAll, + Html, + Png, + Arrow, + ArrowAll, +} + +impl From for Html { + fn from(x: ExportMethod) -> Self { + match x { + ExportMethod::Csv => html_template! { + { ".csv " } + }, + ExportMethod::CsvAll => html_template! { + { ".csv " } + }, + ExportMethod::Html => html_template! { + { ".html " } + }, + ExportMethod::Png => html_template! { + { ".png " } + }, + ExportMethod::Arrow => html_template! { + { ".arrow " } + }, + ExportMethod::ArrowAll => html_template! { + { ".arrow " } + }, + } + } +} + +pub type ExportDropDownMenu = DropDownMenu; +pub type ExportDropDownMenuProps = DropDownMenuProps; +pub type ExportDropDownMenuMsg = DropDownMenuMsg; +pub type ExportDropDownMenuItem = DropDownMenuItem; diff --git a/rust/perspective-viewer/src/rust/components/filter_item.rs b/rust/perspective-viewer/src/rust/components/filter_item.rs index 39ac426f8b..dcf15fd4cf 100644 --- a/rust/perspective-viewer/src/rust/components/filter_item.rs +++ b/rust/perspective-viewer/src/rust/components/filter_item.rs @@ -16,7 +16,7 @@ use crate::utils::{posix_to_utc_str, str_to_utc_posix}; use crate::*; use super::containers::dragdrop_list::*; -use super::containers::dropdown::*; +use super::containers::select::*; use chrono::{NaiveDate, TimeZone, Utc}; use wasm_bindgen::JsCast; @@ -225,7 +225,7 @@ impl FilterItemProps { } } -type FilterOpSelector = DropDown; +type FilterOpSelector = Select; impl Component for FilterItem { type Message = FilterItemMsg; @@ -458,7 +458,7 @@ impl Component for FilterItem { .props() .get_filter_ops() .into_iter() - .map(DropDownItem::Option) + .map(SelectItem::Option) .collect::>(); html! { diff --git a/rust/perspective-viewer/src/rust/components/mod.rs b/rust/perspective-viewer/src/rust/components/mod.rs index 5e4e442af5..fed9af25b9 100644 --- a/rust/perspective-viewer/src/rust/components/mod.rs +++ b/rust/perspective-viewer/src/rust/components/mod.rs @@ -9,6 +9,7 @@ //! `components` contains all Yew `Component` types, but only exports the 4 necessary //! for public Custom Elements. The rest are internal components of these 4. +pub mod export_dropdown; pub mod expression_editor; pub mod filter_dropdown; pub mod number_column_style; diff --git a/rust/perspective-viewer/src/rust/components/plugin_selector.rs b/rust/perspective-viewer/src/rust/components/plugin_selector.rs index f4717fc93d..a684fa8c0c 100644 --- a/rust/perspective-viewer/src/rust/components/plugin_selector.rs +++ b/rust/perspective-viewer/src/rust/components/plugin_selector.rs @@ -14,7 +14,7 @@ use crate::session::*; use crate::utils::*; use crate::*; -use super::containers::dropdown::*; +use super::containers::select::*; use yew::prelude::*; @@ -88,18 +88,18 @@ impl Component for PluginSelector { .renderer .get_all_plugin_names() .into_iter() - .map(DropDownItem::Option) + .map(SelectItem::Option) .collect::>(); html! {
- + id="plugin_selector" values={ options } selected={ plugin_name } on_select={ callback }> - > + >
} } diff --git a/rust/perspective-viewer/src/rust/components/status_bar.rs b/rust/perspective-viewer/src/rust/components/status_bar.rs index a0feeec455..c7ebecc1b9 100644 --- a/rust/perspective-viewer/src/rust/components/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/status_bar.rs @@ -6,13 +6,15 @@ // of the Apache License 2.0. The full license can be found in the LICENSE // file. -use crate::components::containers::dropdown::*; +use crate::components::containers::select::*; use crate::components::status_bar_counter::StatusBarRowsCounter; +use crate::custom_elements::export_dropdown::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; use crate::*; use wasm_bindgen_futures::spawn_local; +use web_sys::*; use yew::prelude::*; #[cfg(test)] @@ -38,7 +40,7 @@ impl PartialEq for StatusBarProps { pub enum StatusBarMsg { Reset(bool), - Export(bool), + Export, Copy(bool), SetThemeConfig((Vec, Option)), SetTheme(String), @@ -51,6 +53,8 @@ pub struct StatusBar { is_updating: i32, theme: Option, themes: Vec, + export_ref: NodeRef, + export_dropdown: Option, _sub: [Subscription; 4], } @@ -87,6 +91,8 @@ impl Component for StatusBar { _sub, theme: None, themes: vec![], + export_dropdown: None, + export_ref: NodeRef::default(), is_updating: 0, } } @@ -121,12 +127,13 @@ impl Component for StatusBar { false } - StatusBarMsg::Export(flat) => { + StatusBarMsg::Export => { let session = ctx.props().session.clone(); - spawn_local(async move { - session.download_as_csv(flat).await.expect("Export failed"); - }); - + let renderer = ctx.props().renderer.clone(); + let export_dropdown = ExportDropDownMenuElement::new(session, renderer); + let target = self.export_ref.cast::().unwrap(); + export_dropdown.open(target); + self.export_dropdown = Some(export_dropdown); false } StatusBarMsg::Copy(flat) => { @@ -153,9 +160,7 @@ impl Component for StatusBar { .link() .callback(|event: MouseEvent| StatusBarMsg::Reset(event.shift_key())); - let export = ctx - .link() - .callback(|event: MouseEvent| StatusBarMsg::Export(event.shift_key())); + let export = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export); let copy = ctx .link() @@ -169,18 +174,18 @@ impl Component for StatusBar { .themes .iter() .cloned() - .map(DropDownItem::Option) + .map(SelectItem::Option) .collect::>(); if values.len() > 1 { html! { - + id="theme_selector" values={ values } selected={ selected.to_owned() } on_select={ ontheme }> - > + > } } else { @@ -198,7 +203,12 @@ impl Component for StatusBar { { "Reset" } - + + { "Export" } diff --git a/rust/perspective-viewer/src/rust/components/tests/status_bar.rs b/rust/perspective-viewer/src/rust/components/tests/status_bar.rs index 6a989744ec..1eff3f34ed 100644 --- a/rust/perspective-viewer/src/rust/components/tests/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/tests/status_bar.rs @@ -52,7 +52,7 @@ pub fn test_callbacks_invoked() { assert_eq!(token.get(), 0); let status_bar = link.borrow().clone().unwrap(); - status_bar.send_message(StatusBarMsg::Export(false)); + status_bar.send_message(StatusBarMsg::Export); assert_eq!(token.get(), 0); let status_bar = link.borrow().clone().unwrap(); status_bar.send_message(StatusBarMsg::Copy(false)); diff --git a/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs new file mode 100644 index 0000000000..32668d63e6 --- /dev/null +++ b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs @@ -0,0 +1,153 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use crate::components::export_dropdown::*; +use crate::custom_elements::modal::*; +use crate::model::*; +use crate::renderer::Renderer; +use crate::session::Session; + +use js_intern::*; +use js_sys::*; +use std::cell::RefCell; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::spawn_local; +use web_sys::*; +use yew::prelude::*; + +#[wasm_bindgen] +#[derive(Clone)] +pub struct ExportDropDownMenuElement { + modal: ModalElement, + target: Rc>>, +} + +impl ResizableMessage for ::Message { + fn resize(y: i32, x: i32, _: bool) -> Self { + ExportDropDownMenuMsg::SetPos(y, x) + } +} + +impl ExportDropDownMenuElement { + pub fn new(session: Session, renderer: Renderer) -> ExportDropDownMenuElement { + let document = window().unwrap().document().unwrap(); + let dropdown = document + .create_element("perspective-filter-dropdown") + .unwrap() + .unchecked_into::(); + + let plugin = renderer.get_active_plugin().unwrap(); + let opts = if Reflect::has(&plugin, js_intern!("render")).unwrap() { + vec![ + ExportMethod::Csv, + ExportMethod::Arrow, + ExportMethod::Html, + ExportMethod::Png, + ] + } else { + vec![ExportMethod::Csv, ExportMethod::Arrow, ExportMethod::Html] + }; + + let values = vec![ + ExportDropDownMenuItem::OptGroup("Current View", opts), + ExportDropDownMenuItem::OptGroup( + "All", + vec![ExportMethod::CsvAll, ExportMethod::ArrowAll], + ), + ]; + + let modal_rc: Rc>>> = + Default::default(); + + let callback = Callback::from({ + let modal_rc = modal_rc.clone(); + move |x| match x { + ExportMethod::Csv => { + let session = session.clone(); + let modal = modal_rc.borrow().clone().unwrap(); + spawn_local(async move { + session.download_as_csv(false).await.expect("Export failed"); + modal.hide().unwrap(); + }); + } + ExportMethod::CsvAll => { + let session = session.clone(); + let modal = modal_rc.borrow().clone().unwrap(); + spawn_local(async move { + session.download_as_csv(true).await.expect("Export failed"); + modal.hide().unwrap(); + }); + } + ExportMethod::Arrow => { + let session = session.clone(); + let modal = modal_rc.borrow().clone().unwrap(); + spawn_local(async move { + session + .download_as_arrow(false) + .await + .expect("Export failed"); + modal.hide().unwrap(); + }); + } + ExportMethod::ArrowAll => { + let session = session.clone(); + let modal = modal_rc.borrow().clone().unwrap(); + spawn_local(async move { + session + .download_as_arrow(true) + .await + .expect("Export failed"); + modal.hide().unwrap(); + }); + } + ExportMethod::Html => { + let session = session.clone(); + let renderer = renderer.clone(); + let modal = modal_rc.borrow().clone().unwrap(); + spawn_local(async move { + (&session, &renderer) + .download_as_html() + .await + .expect("Export failed"); + + modal.hide().unwrap(); + }); + } + ExportMethod::Png => { + let render = Reflect::get(&plugin, js_intern!("render")).unwrap(); + render.unchecked_into::().call0(&plugin).unwrap(); + modal_rc.borrow().clone().unwrap().hide().unwrap(); + } + } + }); + + let props = ExportDropDownMenuProps { + values: Rc::new(values), + callback, + }; + + let modal = ModalElement::new(dropdown, props, true); + *modal_rc.borrow_mut() = Some(modal.clone()); + ExportDropDownMenuElement { + modal, + target: Default::default(), + } + } + + pub fn open(&self, target: HtmlElement) { + self.modal.open(target, None); + } + + pub fn hide(&self) -> Result<(), JsValue> { + self.modal.hide() + } + + pub fn connected_callback(&self) {} +} diff --git a/rust/perspective-viewer/src/rust/custom_elements/mod.rs b/rust/perspective-viewer/src/rust/custom_elements/mod.rs index e029776d0f..81156a2f7b 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/mod.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/mod.rs @@ -6,6 +6,7 @@ // of the Apache License 2.0. The full license can be found in the LICENSE // file. +pub mod export_dropdown; pub mod expression_editor; pub mod filter_dropdown; pub mod modal; diff --git a/rust/perspective-viewer/src/rust/js/perspective.rs b/rust/perspective-viewer/src/rust/js/perspective.rs index 96c1153979..d433cc3e39 100644 --- a/rust/perspective-viewer/src/rust/js/perspective.rs +++ b/rust/perspective-viewer/src/rust/js/perspective.rs @@ -95,6 +95,11 @@ extern "C" { options: js_sys::Object, ) -> Result; + #[wasm_bindgen(method, catch, js_name = to_arrow)] + pub async fn _to_arrow( + this: &JsPerspectiveView, + ) -> Result; + #[wasm_bindgen(method, catch, js_name = num_rows)] pub async fn _num_rows(this: &JsPerspectiveView) -> Result; @@ -152,6 +157,7 @@ impl JsPerspectiveTable { impl JsPerspectiveView { async_typed!(_to_csv, to_csv(&self, options: js_sys::Object) -> js_sys::JsString); + async_typed!(_to_arrow, to_arrow(&self) -> js_sys::ArrayBuffer); async_typed!(_num_rows, num_rows(&self) -> f64); async_typed!(_num_columns, num_columns(&self) -> f64); async_typed!(_schema, schema(&self) -> JsPerspectiveViewSchema); diff --git a/rust/perspective-viewer/src/rust/model.rs b/rust/perspective-viewer/src/rust/model.rs index 19922c553c..e1e51bfdb2 100644 --- a/rust/perspective-viewer/src/rust/model.rs +++ b/rust/perspective-viewer/src/rust/model.rs @@ -15,9 +15,11 @@ use crate::dragdrop::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; +use futures::join; use std::future::Future; use std::pin::Pin; use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; use yew::prelude::*; /// A `SessionRendererModel` is any struct with `session` and `renderer` fields, as @@ -62,6 +64,67 @@ pub trait SessionRendererModel { }); } + fn download_as_html(&self) -> Pin>>> { + let view_config = self.get_viewer_config(); + let session = self.session().clone(); + Box::pin(async move { + if let (Ok(arrow), Ok(mut config)) = + join!(session.get_table_arrow(), view_config) + { + config.settings = false; + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let element: web_sys::HtmlElement = + document.create_element("a")?.unchecked_into(); + let js_config = serde_json::to_string(&config).into_jserror()?; + let blob_url = { + let html = + JsValue::from(format!(" + + + + + + + + + + + + + +", base64::encode(arrow), js_config)); + let array = [html].iter().collect::(); + let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; + web_sys::Url::create_object_url_with_blob(&blob)? + }; + + element.set_attribute("download", "perspective.html")?; + element.set_attribute("href", &blob_url)?; + element.style().set_property("display", "none")?; + document.body().unwrap().append_child(&element)?; + element.click(); + document.body().unwrap().remove_child(&element)?; + } + + Ok(()) + }) + } + fn get_viewer_config( &self, ) -> Pin>>> { @@ -89,6 +152,16 @@ pub trait SessionRendererModel { } } +impl crate::model::SessionRendererModel for (&Session, &Renderer) { + fn session(&self) -> &'_ Session { + self.0 + } + + fn renderer(&self) -> &'_ Renderer { + self.1 + } +} + #[macro_export] macro_rules! derive_session_renderer_model { ($key:ty) => { diff --git a/rust/perspective-viewer/src/rust/session.rs b/rust/perspective-viewer/src/rust/session.rs index 6c4524eda5..b4e4054a66 100644 --- a/rust/perspective-viewer/src/rust/session.rs +++ b/rust/perspective-viewer/src/rust/session.rs @@ -463,11 +463,41 @@ impl Session { Ok(()) } + pub async fn download_as_arrow(self, flat: bool) -> Result<(), JsValue> { + if flat { + let table = self.borrow().table.clone(); + if let Some(table) = table { + download_arrow_flat(&table).await?; + } + } else { + let view = self + .borrow() + .view_sub + .as_ref() + .map(|x| x.get_view().clone()); + + if let Some(view) = view { + download_arrow(&view).await?; + } + }; + + Ok(()) + } + + pub async fn get_table_arrow(&self) -> Result, JsValue> { + let table = self.borrow().table.clone().into_jserror()?; + let view = table.view(&js_object!().unchecked_into()).await?; + let csv_fut = view.to_arrow(); + let bytes = csv_fut.await?; + view.delete().await?; + Ok(js_sys::Uint8Array::new(&bytes).to_vec()) + } + pub async fn download_as_csv(self, flat: bool) -> Result<(), JsValue> { if flat { let table = self.borrow().table.clone(); if let Some(table) = table { - download_flat(&table).await?; + download_csv_flat(&table).await?; } } else { let view = self @@ -477,7 +507,7 @@ impl Session { .map(|x| x.get_view().clone()); if let Some(view) = view { - download(&view).await?; + download_csv(&view).await?; } }; diff --git a/rust/perspective-viewer/src/rust/session/download.rs b/rust/perspective-viewer/src/rust/session/download.rs index 3167e4f864..c583ec0567 100644 --- a/rust/perspective-viewer/src/rust/session/download.rs +++ b/rust/perspective-viewer/src/rust/session/download.rs @@ -6,7 +6,6 @@ // of the Apache License 2.0. The full license can be found in the LICENSE // file. -use crate::config::*; use crate::js::perspective::*; use crate::*; @@ -17,19 +16,19 @@ use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; /// Download a flat (unpivoted with all columns) CSV. -pub async fn download_flat(table: &JsPerspectiveTable) -> Result<(), JsValue> { - let view = table.view(&ViewConfig::default().as_jsvalue()?).await?; - download_async(&view).await?; +pub async fn download_csv_flat(table: &JsPerspectiveTable) -> Result<(), JsValue> { + let view = table.view(&js_object!().unchecked_into()).await?; + download_csv_async(&view).await?; view.delete().await } /// Download a CSV -pub async fn download(view: &View) -> Result<(), JsValue> { - download_async(view).await +pub async fn download_csv(view: &View) -> Result<(), JsValue> { + download_csv_async(view).await } /// Download a CSV, but not a `Promise`. Used to implement the public methods. -async fn download_async(view: &JsPerspectiveView) -> Result<(), JsValue> { +async fn download_csv_async(view: &JsPerspectiveView) -> Result<(), JsValue> { let csv_fut = view.to_csv(js_object!("formatted", true)); let window = web_sys::window().unwrap(); let document = window.document().unwrap(); @@ -51,3 +50,37 @@ async fn download_async(view: &JsPerspectiveView) -> Result<(), JsValue> { document.body().unwrap().remove_child(&element)?; Ok(()) } + +/// Download a flat (unpivoted with all columns) Arrow. +pub async fn download_arrow_flat(table: &JsPerspectiveTable) -> Result<(), JsValue> { + let view = table.view(&js_object!().unchecked_into()).await?; + download_arrow_async(&view).await?; + view.delete().await +} + +/// Download an Apache Arrow +pub async fn download_arrow(view: &View) -> Result<(), JsValue> { + download_arrow_async(view).await +} + +/// Download a CSV, but not a `Promise`. Used to implement the public methods. +async fn download_arrow_async(view: &JsPerspectiveView) -> Result<(), JsValue> { + let csv_fut = view.to_arrow(); + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let element: web_sys::HtmlElement = document.create_element("a")?.unchecked_into(); + let blob_url = { + let bytes = csv_fut.await.unwrap(); + let array = [Uint8Array::new(&bytes)].iter().collect::(); + let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; + web_sys::Url::create_object_url_with_blob(&blob)? + }; + + element.set_attribute("download", "perspective.arrow")?; + element.set_attribute("href", &blob_url)?; + element.style().set_property("display", "none")?; + document.body().unwrap().append_child(&element)?; + element.click(); + document.body().unwrap().remove_child(&element)?; + Ok(()) +} From 0b32780306cd108f392081b478d7b5dde341de3a Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 02:10:40 -0500 Subject: [PATCH 05/31] Fix tests --- .../perspective-viewer-d3fc/test/results/results.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index 57fc837fe3..4033088f1b 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -1,5 +1,5 @@ { - "__GIT_COMMIT__": "5da16de32e940766d75e53e589ea77950bf66659", + "__GIT_COMMIT__": "d776202ff728f808f1d07060e5ce73bb392402f1", "area_shows_a_grid_without_any_settings_applied": "3852532b8ee6abba3373a10d41bb4df2", "area_displays_visible_columns_": "919cc6f6c2a2f2ec13b90dfb60e1b7ef", "area_pivot_by_a_row": "7495976cfed69cfe9db2b3fdc6ef4707", @@ -121,10 +121,10 @@ "sunburst_pivot_by_two_rows": "38fe70a8f88a3e0df185f8775c88317d", "sunburst_pivot_by_a_row_and_a_column": "fa789a00c03e04567bd45a8451e7a00a", "sunburst_pivot_by_two_rows_and_two_columns": "e03658ff241f45837b9d61227bf023a4", - "treemap_pivot_by_a_row": "d9e965d744906a2320148b73dc1fcb29", - "treemap_pivot_by_two_rows": "28390c90695be1b38bd2a05a984dd242", - "treemap_pivot_by_a_row_and_a_column": "a36877ce32fed64d1ae16807de27e9de", - "treemap_pivot_by_two_rows_and_two_columns": "92203a8d4778c041836361b56ff39174", + "treemap_pivot_by_a_row": "7322fa73924e688662d36558a33ddb99", + "treemap_pivot_by_two_rows": "ce30c9196e830d54d4960d4f9a53ad32", + "treemap_pivot_by_a_row_and_a_column": "a9ee5c25d23fff12e57b7755ca310cb5", + "treemap_pivot_by_two_rows_and_two_columns": "fe5616df5b45c5b0b2f86489f7a1dc22", "treemap_shows_a_grid_without_any_settings_applied": "10d1208b485425756fcc932229386b02", "treemap_displays_visible_columns_": "10d1208b485425756fcc932229386b02", "treemap_pivot_by_a_column": "10d1208b485425756fcc932229386b02", From 5080a05528772421f2517de33181ba3f030c9623 Mon Sep 17 00:00:00 2001 From: shinny-yangyang Date: Sat, 5 Mar 2022 16:32:30 +0800 Subject: [PATCH 06/31] add two new aggtype "last minus first" and "high minus low". --- .gitignore | 4 +++ cpp/perspective/src/cpp/aggspec.cpp | 8 +++++ cpp/perspective/src/cpp/base.cpp | 4 +++ cpp/perspective/src/cpp/config.cpp | 1 + cpp/perspective/src/cpp/extract_aggregate.cpp | 2 ++ cpp/perspective/src/cpp/sparse_tree.cpp | 29 ++++++++++++++++--- cpp/perspective/src/cpp/view_config.cpp | 2 +- .../src/include/perspective/base.h | 2 ++ .../src/include/perspective/sparse_tree.h | 2 +- .../perspective/src/js/config/constants.js | 2 ++ .../perspective/perspective/core/aggregate.py | 2 ++ .../src/rust/config/aggregates.rs | 12 ++++++++ 12 files changed, 64 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 4ed4abdc1e..4d94e088d3 100644 --- a/.gitignore +++ b/.gitignore @@ -191,3 +191,7 @@ packages/perspective-jupyterlab/test/config/jupyter/lab packages/perspective-jupyterlab/test/config/jupyter/migrated docs/static/features results.debug.json + +boost* + +.vs diff --git a/cpp/perspective/src/cpp/aggspec.cpp b/cpp/perspective/src/cpp/aggspec.cpp index db75c860ae..ccdb96471a 100644 --- a/cpp/perspective/src/cpp/aggspec.cpp +++ b/cpp/perspective/src/cpp/aggspec.cpp @@ -145,6 +145,9 @@ t_aggspec::agg_str() const { case AGGTYPE_LAST_BY_INDEX: { return "last_by_index"; } break; + case AGGTYPE_LAST_MINUS_FIRST: { + return "last_minus_first"; + } break; case AGGTYPE_PY_AGG: { return "py_agg"; } break; @@ -163,6 +166,9 @@ t_aggspec::agg_str() const { case AGGTYPE_LOW_WATER_MARK: { return "low_water_mark"; } + case AGGTYPE_HIGH_MINUS_LOW: { + return "high_minus_low"; + } break; case AGGTYPE_UDF_COMBINER: { std::stringstream ss; ss << "udf_combiner_" << disp_name(); @@ -311,10 +317,12 @@ t_aggspec::get_output_specs(const t_schema& schema) const { case AGGTYPE_MEDIAN: case AGGTYPE_FIRST: case AGGTYPE_LAST_BY_INDEX: + case AGGTYPE_LAST_MINUS_FIRST: case AGGTYPE_OR: case AGGTYPE_LAST_VALUE: case AGGTYPE_HIGH_WATER_MARK: case AGGTYPE_LOW_WATER_MARK: + case AGGTYPE_HIGH_MINUS_LOW: case AGGTYPE_IDENTITY: case AGGTYPE_DISTINCT_LEAF: { t_dtype coltype = schema.get_dtype(m_dependencies[0].name()); diff --git a/cpp/perspective/src/cpp/base.cpp b/cpp/perspective/src/cpp/base.cpp index dd9dfcdac2..c36a8868a4 100644 --- a/cpp/perspective/src/cpp/base.cpp +++ b/cpp/perspective/src/cpp/base.cpp @@ -463,6 +463,8 @@ str_to_aggtype(const std::string& str) { return t_aggtype::AGGTYPE_FIRST; } else if (str == "last by index") { return t_aggtype::AGGTYPE_LAST_BY_INDEX; + } else if (str == "last minus first") { + return t_aggtype::AGGTYPE_LAST_MINUS_FIRST; } else if (str == "py_agg") { return t_aggtype::AGGTYPE_PY_AGG; } else if (str == "and") { @@ -475,6 +477,8 @@ str_to_aggtype(const std::string& str) { return t_aggtype::AGGTYPE_HIGH_WATER_MARK; } else if (str == "low" || str == "low_water_mark") { return t_aggtype::AGGTYPE_LOW_WATER_MARK; + } else if (str == "high minus low") { + return t_aggtype::AGGTYPE_HIGH_MINUS_LOW; } else if (str == "sum abs" || str == "sum_abs") { return t_aggtype::AGGTYPE_SUM_ABS; } else if (str == "abs sum" || str == "abs_sum") { diff --git a/cpp/perspective/src/cpp/config.cpp b/cpp/perspective/src/cpp/config.cpp index 2eea642dc5..34f9a6f39e 100644 --- a/cpp/perspective/src/cpp/config.cpp +++ b/cpp/perspective/src/cpp/config.cpp @@ -182,6 +182,7 @@ t_config::setup(const std::vector& detail_columns, case AGGTYPE_ANY: case AGGTYPE_FIRST: case AGGTYPE_LAST_BY_INDEX: + case AGGTYPE_LAST_MINUS_FIRST: case AGGTYPE_MEAN: case AGGTYPE_WEIGHTED_MEAN: case AGGTYPE_UNIQUE: diff --git a/cpp/perspective/src/cpp/extract_aggregate.cpp b/cpp/perspective/src/cpp/extract_aggregate.cpp index c8839ad727..2b539e367e 100644 --- a/cpp/perspective/src/cpp/extract_aggregate.cpp +++ b/cpp/perspective/src/cpp/extract_aggregate.cpp @@ -58,8 +58,10 @@ extract_aggregate(const t_aggspec& aggspec, const t_column* aggcol, case AGGTYPE_OR: case AGGTYPE_LAST_BY_INDEX: case AGGTYPE_LAST_VALUE: + case AGGTYPE_LAST_MINUS_FIRST: case AGGTYPE_HIGH_WATER_MARK: case AGGTYPE_LOW_WATER_MARK: + case AGGTYPE_HIGH_MINUS_LOW: case AGGTYPE_SCALED_DIV: case AGGTYPE_SCALED_ADD: case AGGTYPE_SCALED_MUL: diff --git a/cpp/perspective/src/cpp/sparse_tree.cpp b/cpp/perspective/src/cpp/sparse_tree.cpp index 4485ac4937..3510f4ada9 100644 --- a/cpp/perspective/src/cpp/sparse_tree.cpp +++ b/cpp/perspective/src/cpp/sparse_tree.cpp @@ -1224,7 +1224,16 @@ t_stree::update_agg_table(t_uindex nidx, t_agg_update_info& info, case AGGTYPE_LAST_BY_INDEX: { old_value.set(dst->get_scalar(dst_ridx)); new_value.set(first_last_helper( - nidx, spec, gstate, expression_master_table)); + nidx, spec, spec.agg(), gstate, expression_master_table)); + dst->set_scalar(dst_ridx, new_value); + } break; + case AGGTYPE_LAST_MINUS_FIRST: { + old_value.set(dst->get_scalar(dst_ridx)); + auto first_new_value = (first_last_helper( + nidx, spec, AGGTYPE_FIRST, gstate, expression_master_table)); + auto last_new_value = (first_last_helper( + nidx, spec, AGGTYPE_LAST_BY_INDEX, gstate, expression_master_table)); + new_value.set(last_new_value - first_new_value); dst->set_scalar(dst_ridx, new_value); } break; case AGGTYPE_AND: { @@ -1302,6 +1311,18 @@ t_stree::update_agg_table(t_uindex nidx, t_agg_update_info& info, } dst->set_scalar(dst_ridx, new_value); } break; + case AGGTYPE_HIGH_MINUS_LOW: { + t_tscalar dst_scalar = dst->get_scalar(dst_ridx); + old_value.set(dst_scalar); + auto pkeys = get_pkeys(nidx); + std::vector values; + read_column_from_gstate(gstate, expression_master_table, + spec.get_dependencies()[0].name(), pkeys, values, true); + auto low_high = std::minmax_element( + values.begin(), values.end()); + new_value.set(*(low_high.second) - *(low_high.first)); + dst->set_scalar(dst_ridx, new_value); + } break; case AGGTYPE_UDF_COMBINER: case AGGTYPE_UDF_REDUCER: { // these will be filled in later @@ -1991,7 +2012,7 @@ t_stree::get_deltas() const { } t_tscalar -t_stree::first_last_helper(t_uindex nidx, const t_aggspec& spec, +t_stree::first_last_helper(t_uindex nidx, const t_aggspec& spec, t_aggtype agg, const t_gstate& gstate, const t_data_table& expression_master_table) const { auto pkeys = get_pkeys(nidx); @@ -2012,7 +2033,7 @@ t_stree::first_last_helper(t_uindex nidx, const t_aggspec& spec, switch (spec.get_sort_type()) { case SORTTYPE_ASCENDING: case SORTTYPE_ASCENDING_ABS: { - if (spec.agg() == AGGTYPE_FIRST) { + if (agg == AGGTYPE_FIRST) { if (minmax_idx.m_min >= 0) { return values[minmax_idx.m_min]; } @@ -2024,7 +2045,7 @@ t_stree::first_last_helper(t_uindex nidx, const t_aggspec& spec, } break; case SORTTYPE_DESCENDING: case SORTTYPE_DESCENDING_ABS: { - if (spec.agg() == AGGTYPE_FIRST) { + if (agg == AGGTYPE_FIRST) { if (minmax_idx.m_max >= 0) { return values[minmax_idx.m_max]; } diff --git a/cpp/perspective/src/cpp/view_config.cpp b/cpp/perspective/src/cpp/view_config.cpp index c827367f72..c23e38483c 100644 --- a/cpp/perspective/src/cpp/view_config.cpp +++ b/cpp/perspective/src/cpp/view_config.cpp @@ -348,7 +348,7 @@ t_view_config::make_aggspec(const std::string& column, } } - if (agg_type == AGGTYPE_FIRST || agg_type == AGGTYPE_LAST_BY_INDEX) { + if (agg_type == AGGTYPE_FIRST || agg_type == AGGTYPE_LAST_BY_INDEX || agg_type == AGGTYPE_LAST_MINUS_FIRST) { dependencies.push_back(t_dep("psp_okey", DEPTYPE_COLUMN)); aggspec = t_aggspec( column, column, agg_type, dependencies, SORTTYPE_ASCENDING); diff --git a/cpp/perspective/src/include/perspective/base.h b/cpp/perspective/src/include/perspective/base.h index 855478d3c5..8e0a33bc22 100644 --- a/cpp/perspective/src/include/perspective/base.h +++ b/cpp/perspective/src/include/perspective/base.h @@ -242,12 +242,14 @@ enum t_aggtype { AGGTYPE_DOMINANT, AGGTYPE_FIRST, AGGTYPE_LAST_BY_INDEX, + AGGTYPE_LAST_MINUS_FIRST, AGGTYPE_PY_AGG, AGGTYPE_AND, AGGTYPE_OR, AGGTYPE_LAST_VALUE, AGGTYPE_HIGH_WATER_MARK, AGGTYPE_LOW_WATER_MARK, + AGGTYPE_HIGH_MINUS_LOW, AGGTYPE_UDF_COMBINER, AGGTYPE_UDF_REDUCER, AGGTYPE_SUM_ABS, diff --git a/cpp/perspective/src/include/perspective/sparse_tree.h b/cpp/perspective/src/include/perspective/sparse_tree.h index 059a93a918..cec80ce9ae 100644 --- a/cpp/perspective/src/include/perspective/sparse_tree.h +++ b/cpp/perspective/src/include/perspective/sparse_tree.h @@ -272,7 +272,7 @@ class PERSPECTIVE_EXPORT t_stree { void clear(); - t_tscalar first_last_helper(t_uindex nidx, const t_aggspec& spec, + t_tscalar first_last_helper(t_uindex nidx, const t_aggspec& spec, t_aggtype agg, const t_gstate& gstate, const t_data_table& expression_master_table) const; diff --git a/packages/perspective/src/js/config/constants.js b/packages/perspective/src/js/config/constants.js index 84c3371b54..55066a4a08 100644 --- a/packages/perspective/src/js/config/constants.js +++ b/packages/perspective/src/js/config/constants.js @@ -54,10 +54,12 @@ const NUMBER_AGGREGATES = [ "dominant", "first by index", "last by index", + "last minus first", "last", "high", "join", "low", + "high minus low", "mean", "median", "pct sum parent", diff --git a/python/perspective/perspective/core/aggregate.py b/python/perspective/perspective/core/aggregate.py index f4d9ca11df..7d0d31bdaf 100644 --- a/python/perspective/perspective/core/aggregate.py +++ b/python/perspective/perspective/core/aggregate.py @@ -26,10 +26,12 @@ class Aggregate(Enum): DOMINANT = "dominant" FIRST_BY_INDEX = "first by index" LAST_BY_INDEX = "last by index" + LAST_MINUS_FIRST = "last minus first" LAST = "last" HIGH = "high" JOIN = "join" LOW = "low" + HIGH_MINUS_LOW = "high minus low" MEAN = "mean" MEDIAN = "median" OR = "or" diff --git a/rust/perspective-viewer/src/rust/config/aggregates.rs b/rust/perspective-viewer/src/rust/config/aggregates.rs index 8637c1ad2c..5d79d8f7bc 100644 --- a/rust/perspective-viewer/src/rust/config/aggregates.rs +++ b/rust/perspective-viewer/src/rust/config/aggregates.rs @@ -55,6 +55,9 @@ pub enum SingleAggregate { #[serde(rename = "last by index")] LastByIndex, + #[serde(rename = "last minus first")] + LastMinusFirst, + #[serde(rename = "last")] Last, @@ -79,6 +82,9 @@ pub enum SingleAggregate { #[serde(rename = "low")] Low, + #[serde(rename = "high minus low")] + HighMinusLow, + #[serde(rename = "stddev")] StdDev, @@ -101,6 +107,7 @@ impl Display for SingleAggregate { SingleAggregate::Median => "median", SingleAggregate::First => "first", SingleAggregate::LastByIndex => "last by index", + SingleAggregate::LastMinusFirst => "last minus first", SingleAggregate::Last => "last", SingleAggregate::Count => "count", SingleAggregate::DistinctCount => "distinct count", @@ -109,6 +116,7 @@ impl Display for SingleAggregate { SingleAggregate::Join => "join", SingleAggregate::High => "high", SingleAggregate::Low => "low", + SingleAggregate::HighMinusLow => "high minus low", SingleAggregate::StdDev => "stddev", SingleAggregate::Var => "var", }; @@ -133,6 +141,7 @@ impl FromStr for SingleAggregate { "median" => Ok(SingleAggregate::Median), "first" => Ok(SingleAggregate::First), "last by index" => Ok(SingleAggregate::LastByIndex), + "last minus first" => Ok(SingleAggregate::LastMinusFirst), "last" => Ok(SingleAggregate::Last), "count" => Ok(SingleAggregate::Count), "distinct count" => Ok(SingleAggregate::DistinctCount), @@ -141,6 +150,7 @@ impl FromStr for SingleAggregate { "join" => Ok(SingleAggregate::Join), "high" => Ok(SingleAggregate::High), "low" => Ok(SingleAggregate::Low), + "high minus low" => Ok(SingleAggregate::HighMinusLow), "stddev" => Ok(SingleAggregate::StdDev), "var" => Ok(SingleAggregate::Var), x => Err(format!("Unknown aggregate `{}`", x).into()), @@ -216,7 +226,9 @@ const NUMBER_AGGREGATES: &[SingleAggregate] = &[ SingleAggregate::First, SingleAggregate::High, SingleAggregate::Low, + SingleAggregate::HighMinusLow, SingleAggregate::LastByIndex, + SingleAggregate::LastMinusFirst, SingleAggregate::Last, SingleAggregate::Mean, SingleAggregate::Median, From d116cb7bf7432e8ddb77820f7804eab10f41bf36 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 16:03:24 -0500 Subject: [PATCH 07/31] Fix missing case --- cpp/perspective/src/cpp/config.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/cpp/perspective/src/cpp/config.cpp b/cpp/perspective/src/cpp/config.cpp index 34f9a6f39e..9ff0b257ae 100644 --- a/cpp/perspective/src/cpp/config.cpp +++ b/cpp/perspective/src/cpp/config.cpp @@ -183,6 +183,7 @@ t_config::setup(const std::vector& detail_columns, case AGGTYPE_FIRST: case AGGTYPE_LAST_BY_INDEX: case AGGTYPE_LAST_MINUS_FIRST: + case AGGTYPE_HIGH_MINUS_LOW: case AGGTYPE_MEAN: case AGGTYPE_WEIGHTED_MEAN: case AGGTYPE_UNIQUE: From d70567a4e48995179123eb2c3d30479d54f42dd3 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 16:04:28 -0500 Subject: [PATCH 08/31] Fix type-preserving difference --- cpp/perspective/src/cpp/scalar.cpp | 95 +++++++++++++++++++ cpp/perspective/src/cpp/sparse_tree.cpp | 35 ++++--- .../src/include/perspective/scalar.h | 4 + 3 files changed, 121 insertions(+), 13 deletions(-) diff --git a/cpp/perspective/src/cpp/scalar.cpp b/cpp/perspective/src/cpp/scalar.cpp index 1c8d1b8f63..1d91c06dc1 100644 --- a/cpp/perspective/src/cpp/scalar.cpp +++ b/cpp/perspective/src/cpp/scalar.cpp @@ -403,6 +403,50 @@ t_tscalar::operator%=(const t_tscalar& rhs) { return *this; } +t_tscalar +t_tscalar::add_typesafe(const t_tscalar& rhs) const { + t_tscalar rval; + rval.clear(); + rval.m_type = DTYPE_FLOAT64; + if (!is_numeric() || !rhs.is_numeric()) { + rval.m_status = STATUS_CLEAR; + return rval; + } + if (!rhs.is_valid() || !is_valid()) { + return rval; + } + if (is_floating_point() || rhs.is_floating_point()) { + rval.m_type = DTYPE_FLOAT64; + rval.set(to_double() + rhs.to_double()); + return rval; + } + rval.m_type = DTYPE_INT64; + rval.set(to_int64() + rhs.to_int64()); + return rval; +} + +t_tscalar +t_tscalar::sub_typesafe(const t_tscalar& rhs) const { + t_tscalar rval; + rval.clear(); + rval.m_type = DTYPE_FLOAT64; + if (!is_numeric() || !rhs.is_numeric()) { + rval.m_status = STATUS_CLEAR; + return rval; + } + if (!rhs.is_valid() || !is_valid()) { + return rval; + } + if (is_floating_point()) { + rval.m_type = DTYPE_FLOAT64; + rval.set(to_double() - rhs.to_double()); + return rval; + } + rval.m_type = DTYPE_INT32; + rval.set(to_int32() - rhs.to_int32()); + return rval; +} + bool t_tscalar::is_numeric() const { return is_numeric_type(static_cast(m_type)); @@ -1216,6 +1260,57 @@ t_tscalar::to_int64() const { return 0; } +std::int32_t +t_tscalar::to_int32() const { + switch (m_type) { + case DTYPE_INT64: { + return get(); + } break; + case DTYPE_INT32: { + return get(); + } break; + case DTYPE_INT16: { + return get(); + } break; + case DTYPE_INT8: { + return get(); + } break; + case DTYPE_UINT64: { + return get(); + } break; + case DTYPE_UINT32: { + return get(); + } break; + case DTYPE_UINT16: { + return get(); + } break; + case DTYPE_UINT8: { + return get(); + } break; + case DTYPE_FLOAT64: { + return get(); + } break; + case DTYPE_FLOAT32: { + return get(); + } break; + case DTYPE_DATE: { + return get(); + } break; + case DTYPE_TIME: { + return get(); + } break; + case DTYPE_BOOL: { + return get(); + } break; + case DTYPE_NONE: + default: { + return 0; + } + } + + return 0; +} + std::uint64_t t_tscalar::to_uint64() const { switch (m_type) { diff --git a/cpp/perspective/src/cpp/sparse_tree.cpp b/cpp/perspective/src/cpp/sparse_tree.cpp index 3510f4ada9..405e5d4d1d 100644 --- a/cpp/perspective/src/cpp/sparse_tree.cpp +++ b/cpp/perspective/src/cpp/sparse_tree.cpp @@ -1220,20 +1220,25 @@ t_stree::update_agg_table(t_uindex nidx, t_agg_update_info& info, dst->set_scalar(dst_ridx, new_value); } break; - case AGGTYPE_FIRST: + case AGGTYPE_FIRST: { + old_value.set(dst->get_scalar(dst_ridx)); + auto pair = first_last_helper( + nidx, spec, gstate, expression_master_table); + new_value.set(pair.first); + dst->set_scalar(dst_ridx, new_value); + } break; case AGGTYPE_LAST_BY_INDEX: { old_value.set(dst->get_scalar(dst_ridx)); - new_value.set(first_last_helper( - nidx, spec, spec.agg(), gstate, expression_master_table)); + auto pair = first_last_helper( + nidx, spec, gstate, expression_master_table); + new_value.set(pair.second); dst->set_scalar(dst_ridx, new_value); } break; case AGGTYPE_LAST_MINUS_FIRST: { old_value.set(dst->get_scalar(dst_ridx)); - auto first_new_value = (first_last_helper( - nidx, spec, AGGTYPE_FIRST, gstate, expression_master_table)); - auto last_new_value = (first_last_helper( - nidx, spec, AGGTYPE_LAST_BY_INDEX, gstate, expression_master_table)); - new_value.set(last_new_value - first_new_value); + auto pair = (first_last_helper( + nidx, spec, gstate, expression_master_table)); + new_value.set(pair.second.sub_typesafe(pair.first)); dst->set_scalar(dst_ridx, new_value); } break; case AGGTYPE_AND: { @@ -1315,12 +1320,16 @@ t_stree::update_agg_table(t_uindex nidx, t_agg_update_info& info, t_tscalar dst_scalar = dst->get_scalar(dst_ridx); old_value.set(dst_scalar); auto pkeys = get_pkeys(nidx); - std::vector values; + std::vector values; read_column_from_gstate(gstate, expression_master_table, - spec.get_dependencies()[0].name(), pkeys, values, true); - auto low_high = std::minmax_element( - values.begin(), values.end()); - new_value.set(*(low_high.second) - *(low_high.first)); + spec.get_dependencies()[0].name(), pkeys, values); + auto low_high + = std::minmax_element(values.begin(), values.end()); + t_tscalar first; + first.set(*(low_high.first)); + t_tscalar second; + second.set(*(low_high.second)); + new_value.set(second.sub_typesafe(first)); dst->set_scalar(dst_ridx, new_value); } break; case AGGTYPE_UDF_COMBINER: diff --git a/cpp/perspective/src/include/perspective/scalar.h b/cpp/perspective/src/include/perspective/scalar.h index af0e331349..ee380c36fc 100644 --- a/cpp/perspective/src/include/perspective/scalar.h +++ b/cpp/perspective/src/include/perspective/scalar.h @@ -143,6 +143,9 @@ struct PERSPECTIVE_EXPORT t_tscalar { t_tscalar& operator/=(const t_tscalar& rhs); t_tscalar& operator%=(const t_tscalar& rhs); + t_tscalar add_typesafe(const t_tscalar& rhs) const; + t_tscalar sub_typesafe(const t_tscalar& rhs) const; + bool is_numeric() const; static t_tscalar canonical(t_dtype dtype); @@ -160,6 +163,7 @@ struct PERSPECTIVE_EXPORT t_tscalar { std::string to_string(bool for_expr = false) const; double to_double() const; std::int64_t to_int64() const; + std::int32_t to_int32() const; std::uint64_t to_uint64() const; bool begins_with(const t_tscalar& other) const; From 06dd21c3b7d1fd4e01203377f1c69eeb441ff295 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 16:04:42 -0500 Subject: [PATCH 09/31] Fix double-iteration --- cpp/perspective/src/cpp/sparse_tree.cpp | 44 +++++++++++-------- .../src/include/perspective/sparse_tree.h | 4 +- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/cpp/perspective/src/cpp/sparse_tree.cpp b/cpp/perspective/src/cpp/sparse_tree.cpp index 405e5d4d1d..9b44b69786 100644 --- a/cpp/perspective/src/cpp/sparse_tree.cpp +++ b/cpp/perspective/src/cpp/sparse_tree.cpp @@ -2020,13 +2020,13 @@ t_stree::get_deltas() const { return m_deltas; } -t_tscalar -t_stree::first_last_helper(t_uindex nidx, const t_aggspec& spec, t_aggtype agg, +std::pair +t_stree::first_last_helper(t_uindex nidx, const t_aggspec& spec, const t_gstate& gstate, const t_data_table& expression_master_table) const { auto pkeys = get_pkeys(nidx); if (pkeys.empty()) - return mknone(); + return std::make_pair(mknone(), mknone()); std::vector values; std::vector sort_values; @@ -2039,37 +2039,43 @@ t_stree::first_last_helper(t_uindex nidx, const t_aggspec& spec, t_aggtype agg, auto minmax_idx = get_minmax_idx(sort_values, spec.get_sort_type()); + std::pair ret; switch (spec.get_sort_type()) { case SORTTYPE_ASCENDING: case SORTTYPE_ASCENDING_ABS: { - if (agg == AGGTYPE_FIRST) { - if (minmax_idx.m_min >= 0) { - return values[minmax_idx.m_min]; - } + if (minmax_idx.m_min >= 0) { + ret.first = values[minmax_idx.m_min]; } else { - if (minmax_idx.m_max >= 0) { - return values[minmax_idx.m_max]; - } + ret.first = mknone(); + } + + if (minmax_idx.m_max >= 0) { + ret.second = values[minmax_idx.m_max]; + } else { + ret.second = mknone(); } } break; case SORTTYPE_DESCENDING: case SORTTYPE_DESCENDING_ABS: { - if (agg == AGGTYPE_FIRST) { - if (minmax_idx.m_max >= 0) { - return values[minmax_idx.m_max]; - } + if (minmax_idx.m_max >= 0) { + ret.first = values[minmax_idx.m_max]; } else { - if (minmax_idx.m_min >= 0) { - return values[minmax_idx.m_min]; - } + ret.first = mknone(); + } + + if (minmax_idx.m_min >= 0) { + ret.second = values[minmax_idx.m_min]; + } else { + ret.second = mknone(); } } break; default: { - // return none + ret.first = mknone(); + ret.second = mknone(); } } - return mknone(); + return ret; } bool diff --git a/cpp/perspective/src/include/perspective/sparse_tree.h b/cpp/perspective/src/include/perspective/sparse_tree.h index cec80ce9ae..0a14bdc83f 100644 --- a/cpp/perspective/src/include/perspective/sparse_tree.h +++ b/cpp/perspective/src/include/perspective/sparse_tree.h @@ -272,8 +272,8 @@ class PERSPECTIVE_EXPORT t_stree { void clear(); - t_tscalar first_last_helper(t_uindex nidx, const t_aggspec& spec, t_aggtype agg, - const t_gstate& gstate, + std::pair first_last_helper(t_uindex nidx, + const t_aggspec& spec, const t_gstate& gstate, const t_data_table& expression_master_table) const; bool node_exists(t_uindex nidx); From 97da6472bc0d61e866adbe0237cc2bae05cf32bd Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 16:04:48 -0500 Subject: [PATCH 10/31] Add tests --- .gitignore | 4 -- packages/perspective/test/js/pivots.js | 62 +++++++++++++++++++ .../test/results/results.json | 30 ++++----- 3 files changed, 77 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 4d94e088d3..4ed4abdc1e 100644 --- a/.gitignore +++ b/.gitignore @@ -191,7 +191,3 @@ packages/perspective-jupyterlab/test/config/jupyter/lab packages/perspective-jupyterlab/test/config/jupyter/migrated docs/static/features results.debug.json - -boost* - -.vs diff --git a/packages/perspective/test/js/pivots.js b/packages/perspective/test/js/pivots.js index b4e4206938..ce44d2787c 100644 --- a/packages/perspective/test/js/pivots.js +++ b/packages/perspective/test/js/pivots.js @@ -348,6 +348,68 @@ module.exports = (perspective) => { table.delete(); }); + it("['z'], last_minus_first", async function () { + var table = await perspective.table( + [ + {x: 1, y: "a", z: true}, + {x: 2, y: "b", z: false}, + {x: 3, y: "c", z: true}, + {x: 4, y: "d", z: false}, + ], + {index: "y"} + ); + var view = await table.view({ + group_by: ["z"], + columns: ["x"], + aggregates: {x: "last minus first"}, + }); + const answer = [ + {__ROW_PATH__: [], x: -1}, + {__ROW_PATH__: [false], x: 2}, + {__ROW_PATH__: [true], x: -2}, + ]; + table.update({ + x: [5], + y: ["a"], + z: [true], + }); + const result = await view.to_json(); + expect(result).toEqual(answer); + view.delete(); + table.delete(); + }); + + it("['z'], high_minus_low", async function () { + var table = await perspective.table( + [ + {x: 1, y: "a", z: true}, + {x: 2, y: "b", z: false}, + {x: 3, y: "c", z: true}, + {x: 4, y: "d", z: false}, + ], + {index: "y"} + ); + var view = await table.view({ + group_by: ["z"], + columns: ["x"], + aggregates: {x: "high minus low"}, + }); + const answer = [ + {__ROW_PATH__: [], x: 3}, + {__ROW_PATH__: [false], x: 2}, + {__ROW_PATH__: [true], x: 2}, + ]; + table.update({ + x: [5], + y: ["a"], + z: [true], + }); + const result = await view.to_json(); + expect(result).toEqual(answer); + view.delete(); + table.delete(); + }); + it("['z'], first by index with partial updates", async function () { var table = await perspective.table(data, {index: "y"}); var view = await table.view({ diff --git a/rust/perspective-viewer/test/results/results.json b/rust/perspective-viewer/test/results/results.json index 0b65d4ac2b..fa4c3ee05b 100644 --- a/rust/perspective-viewer/test/results/results.json +++ b/rust/perspective-viewer/test/results/results.json @@ -3,7 +3,7 @@ "superstore.html/doesn't leak elements.": "d0fd18b3d4d7c183c5ed155b4bf37972", "superstore.html/doesn't leak views when setting group by.": "54daaa4bbbe59f6ed4acc301ba871bab", "superstore.html/doesn't leak views when setting filters.": "6dfc1e505f1428424c3265f0236f22fc", - "__GIT_COMMIT__": "b0614dd7fc30b9fb1fd18403df6c8da5a9f5abab", + "__GIT_COMMIT__": "0c97a682f46d058c18b5e902db2ffd40add7e2fa", "blank.html/Handles reloading with a schema.": "e58c62f6e0ff16dc4d753f99e0fc39c3", "superstore_shows_a_grid_without_any_settings_applied_": "ae1c4690d978598ca14c8669244ce604", "superstore_Responsive_Layout_shows_horizontal_columns_on_small_vertical_viewports_": "57ba3ad341cf8a0e4df6ab96715ff2a0", @@ -62,23 +62,23 @@ "superstore_save()_returns_the_current_config": "ded04b5d6cb96a3651578334f189b20e", "superstore_restore()_restores_a_config_from_save()": "ded04b5d6cb96a3651578334f189b20e", "superstore_restore()_fires_the__perspective-config-update__event": "ded04b5d6cb96a3651578334f189b20e", - "superstore_restore_fires_the__perspective-config-update__event": "35d7b01bf3fd1a54b0db1d5cbe49c41b", - "superstore_save_returns_the_current_config": "35d7b01bf3fd1a54b0db1d5cbe49c41b", - "superstore_restore_restores_a_config_from_save": "6cb9eeeed19d93ccf1a8b9e8a756a456", + "superstore_restore_fires_the__perspective-config-update__event": "06b1128d94ddb953f8c5b6adf49f4756", + "superstore_save_returns_the_current_config": "06b1128d94ddb953f8c5b6adf49f4756", + "superstore_restore_restores_a_config_from_save": "43141e5cf7c0554cce759d3f7ea16133", "Expressions_Click_on_add_column_button_opens_the_expression_UI_": "40bb9b2f39e1cd296752d42c694a4f05", "Expressions_Resetting_the_viewer_partially_should_not_delete_all_expressions": "b901f4c275d4d23c27122abbfd2b78ac", "Expressions_Resetting_the_viewer_partially_when_expression_as_in_columns_field,_should_not_delete_all_expressions": "b901f4c275d4d23c27122abbfd2b78ac", "superstore-all_restore__Bucket_by_year_": "3243fc1f0c30d45d6f103b340547e044", "superstore-all_restore__Plugin_config_color_mode_": "e15272040eea436c123116c69c11a3e8", - "dragdrop_superstore_drop_from_inactive_to_active_should_add": "ef55833f802abd076f7332badaed11ef", - "dragdrop_superstore_drop_from_active_to_active_should_swap": "9e6ced06930ad8d950c59b92795734f6", - "dragdrop_column-selector-modes_drop_from_inactive_to_required_column_should_add": "1df327a7d7c49b132d2fea159177c3a4", - "dragdrop_column-selector-modes_drop_from_required_to_required_should_swap": "f320d58878c78e289a18b1d412a25ae0", - "dragdrop_column-selector-modes_drop_from_required_to_empty_column_should_fail": "8b4745a18a1b7657db8562be96683c6f", - "dragdrop_column-selector-modes_drop_from_inactive_to_empty_should_add": "499e30b38dc0363c3a4b4a4ca646465c", - "dragdrop_column-selector-modes_drop_from_named_to_required_should_swap": "04bf40275080925cfbe088307271db34", - "dragdrop_column-selector-modes_drop_from_optional_to_empty_columns_should_move": "68ede60f30c38a9b869fa30102d4cad9", - "dragdrop_column-selector-modes_dragover_from_named_to_required_columns_should_swap": "5f24bc15b8e9d79f06184a60beef29ef", - "dragdrop_column-selector-modes_dragover_from_optional_to_empty_columns_should_move": "9415fa9622e5a69cc860d09fc1f8cf5d", - "dragdrop_column-selector-modes_dragover_from_optional_to_required_columns_should_swap": "d50c610fc31d0a4356e31c470fa0c57d" + "dragdrop_superstore_drop_from_inactive_to_active_should_add": "d9647ef9ee823e5a755fd82f59bc17dd", + "dragdrop_superstore_drop_from_active_to_active_should_swap": "1bf37cc032feb970f7e13a8cfabd889e", + "dragdrop_column-selector-modes_drop_from_inactive_to_required_column_should_add": "cb444777492e047555dd2e7362342478", + "dragdrop_column-selector-modes_drop_from_required_to_required_should_swap": "fb99573ea6e099661c998768b7ba81a1", + "dragdrop_column-selector-modes_drop_from_required_to_empty_column_should_fail": "49315bb1d743aa90ed17ba765bfe62c6", + "dragdrop_column-selector-modes_drop_from_inactive_to_empty_should_add": "4d64295b828cb74471cb5aaa8427fe97", + "dragdrop_column-selector-modes_drop_from_named_to_required_should_swap": "579b194ae62ed4efb1734a6e629705b6", + "dragdrop_column-selector-modes_drop_from_optional_to_empty_columns_should_move": "47e0f1e227cef782b9540c85d26192fd", + "dragdrop_column-selector-modes_dragover_from_named_to_required_columns_should_swap": "2ab795f33898c98faf0a7ffa4494975e", + "dragdrop_column-selector-modes_dragover_from_optional_to_empty_columns_should_move": "2aefefd091efd14886722404178e967d", + "dragdrop_column-selector-modes_dragover_from_optional_to_required_columns_should_swap": "fa41e3421003f99999ff3568f838bf08" } \ No newline at end of file From d0a1823090fa18d648ecc65eb5328259cdf4543a Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 6 Mar 2022 16:14:00 -0500 Subject: [PATCH 11/31] Add filename option, JSON and Config formats to export menu --- .../src/js/plugin/plugin.js | 22 +-- .../src/less/dropdown-menu.less | 21 +++ .../components/containers/dropdown_menu.rs | 20 +-- .../src/rust/components/export_dropdown.rs | 142 ++++++++++++++---- rust/perspective-viewer/src/rust/config.rs | 8 +- .../rust/custom_elements/export_dropdown.rs | 87 ++--------- .../src/rust/custom_elements/viewer.rs | 3 +- rust/perspective-viewer/src/rust/dragdrop.rs | 10 +- .../src/rust/js/perspective.rs | 6 + rust/perspective-viewer/src/rust/model.rs | 104 +++++++++---- .../src/rust/model/export_method.rs | 69 +++++++++ rust/perspective-viewer/src/rust/session.rs | 66 ++++---- .../src/rust/session/download.rs | 86 ----------- .../src/rust/utils/download.rs | 29 ++++ rust/perspective-viewer/src/rust/utils/mod.rs | 19 +++ 15 files changed, 396 insertions(+), 296 deletions(-) create mode 100644 rust/perspective-viewer/src/rust/model/export_method.rs delete mode 100644 rust/perspective-viewer/src/rust/session/download.rs create mode 100644 rust/perspective-viewer/src/rust/utils/download.rs diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js index 01ce1afc1c..13e3504433 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js @@ -225,26 +225,10 @@ export function register(...plugins) { ); } - const button = document.createElement("a"); - button.setAttribute("download", "perspective.png"); - document.body.appendChild(button); - button.addEventListener( - "click", - function dlCanvas() { - var dt = canvas.toDataURL("image/png"); // << this fails in IE/Edge... - dt = dt.replace( - /^data:image\/[^;]*/, - "data:application/octet-stream" - ); - dt = dt.replace( - /^data:application\/octet-stream/, - "data:application/octet-stream;headers=Content-Disposition%3A%20attachment%3B%20filename=perspective.png" - ); - this.href = dt; - }, - false + return await new Promise( + (x) => canvas.toBlob((blob) => x(blob)), + "image/png" ); - button.click(); } async draw(view, end_col, end_row) { diff --git a/rust/perspective-viewer/src/less/dropdown-menu.less b/rust/perspective-viewer/src/less/dropdown-menu.less index f2fbf8f6f6..05e7447f1a 100644 --- a/rust/perspective-viewer/src/less/dropdown-menu.less +++ b/rust/perspective-viewer/src/less/dropdown-menu.less @@ -29,6 +29,26 @@ monospace; } + input { + margin-left: 12px; + margin-right: 12px; + padding: 0; + border: none; + border-bottom: 1px solid var(--inactive--color, #ccc); + background: transparent; + font-family: var(--interface-monospace--font-family, "Roboto Mono"), + monospace; + font-weight: 300; + font-size: 14px; + color: inherit; + outline: none; + } + + .invalid { + color: var(--error--color, #ff0000); + border-color: var(--error--color, #ff0000); + } + .selected { background-color: rgba(0, 0, 0, 0.05); } @@ -41,6 +61,7 @@ display: flex; flex-direction: column; margin-left: 12px; + margin-right: 12px; } span { diff --git a/rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs b/rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs index 8934aa22dc..04237dd499 100644 --- a/rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs +++ b/rust/perspective-viewer/src/rust/components/containers/dropdown_menu.rs @@ -34,8 +34,7 @@ pub struct DropDownMenu where T: Into + Clone + PartialEq + 'static, { - top: i32, - left: i32, + position: Option<(i32, i32)>, _props: PhantomData, } @@ -48,8 +47,7 @@ where fn create(_ctx: &Context) -> Self { DropDownMenu { - top: 0, - left: 0, + position: None, _props: Default::default(), } } @@ -57,16 +55,12 @@ where fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { match msg { DropDownMenuMsg::SetPos(top, left) => { - self.top = top; - self.left = left; + self.position = Some((top, left)); true } } } - fn changed(&mut self, _ctx: &Context) -> bool { - false - } fn view(&self, ctx: &Context) -> Html { let values = &ctx.props().values; @@ -118,8 +112,14 @@ where <> + { + self.position.map(|(top, left)| html! { + + }).unwrap_or_default() + } { body } } diff --git a/rust/perspective-viewer/src/rust/components/export_dropdown.rs b/rust/perspective-viewer/src/rust/components/export_dropdown.rs index 44aaf085b1..0152a9e966 100644 --- a/rust/perspective-viewer/src/rust/components/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/export_dropdown.rs @@ -7,45 +7,121 @@ // file. use super::containers::dropdown_menu::*; +use crate::model::*; +use crate::renderer::*; + use crate::*; +use js_intern::*; +use std::rc::Rc; use yew::prelude::*; -#[derive(Clone, Copy, PartialEq)] -pub enum ExportMethod { - Csv, - CsvAll, - Html, - Png, - Arrow, - ArrowAll, +pub type ExportDropDownMenuItem = DropDownMenuItem; + +#[derive(Properties, Clone, PartialEq)] +pub struct ExportDropDownMenuProps { + pub renderer: Renderer, + pub callback: Callback, } -impl From for Html { - fn from(x: ExportMethod) -> Self { - match x { - ExportMethod::Csv => html_template! { - { ".csv " } - }, - ExportMethod::CsvAll => html_template! { - { ".csv " } - }, - ExportMethod::Html => html_template! { - { ".html " } - }, - ExportMethod::Png => html_template! { - { ".png " } - }, - ExportMethod::Arrow => html_template! { - { ".arrow " } - }, - ExportMethod::ArrowAll => html_template! { - { ".arrow " } +#[derive(Default)] +pub struct ExportDropDownMenu { + top: i32, + left: i32, + title: String, + input_ref: NodeRef, + invalid: bool, +} + +pub enum ExportDropDownMenuMsg { + SetPos(i32, i32), + TitleChange, +} + +fn get_menu_items(name: &str, has_render: bool) -> Vec { + vec![ + ExportDropDownMenuItem::OptGroup( + "Current View", + if has_render { + vec![ + ExportMethod::Csv.new_file(name), + ExportMethod::Json.new_file(name), + ExportMethod::Arrow.new_file(name), + ExportMethod::Html.new_file(name), + ExportMethod::Png.new_file(name), + ] + } else { + vec![ + ExportMethod::Csv.new_file(name), + ExportMethod::Json.new_file(name), + ExportMethod::Arrow.new_file(name), + ExportMethod::Html.new_file(name), + ] }, + ), + ExportDropDownMenuItem::OptGroup( + "All", + vec![ + ExportMethod::CsvAll.new_file(name), + ExportMethod::JsonAll.new_file(name), + ExportMethod::ArrowAll.new_file(name), + ], + ), + ExportDropDownMenuItem::OptGroup( + "Config", + vec![ExportMethod::JsonConfig.new_file(name)], + ), + ] +} + +impl Component for ExportDropDownMenu { + type Properties = ExportDropDownMenuProps; + type Message = ExportDropDownMenuMsg; + + fn view(&self, ctx: &Context) -> yew::virtual_dom::VNode { + let callback = ctx.link().callback(|_| ExportDropDownMenuMsg::TitleChange); + let plugin = ctx.props().renderer.get_active_plugin().unwrap(); + let has_render = js_sys::Reflect::has(&plugin, js_intern!("render")).unwrap(); + html_template! { + + { "Save as" } + + + values={ Rc::new(get_menu_items(&self.title, has_render)) } + callback={ ctx.props().callback.clone() }> + > } } -} -pub type ExportDropDownMenu = DropDownMenu; -pub type ExportDropDownMenuProps = DropDownMenuProps; -pub type ExportDropDownMenuMsg = DropDownMenuMsg; -pub type ExportDropDownMenuItem = DropDownMenuItem; + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + ExportDropDownMenuMsg::SetPos(top, left) => { + self.top = top; + self.left = left; + true + } + ExportDropDownMenuMsg::TitleChange => { + self.title = self + .input_ref + .cast::() + .unwrap() + .value(); + + self.invalid = self.title.is_empty(); + true + } + } + } + + fn create(_ctx: &Context) -> Self { + ExportDropDownMenu { + title: "untitled".to_owned(), + ..Default::default() + } + } +} diff --git a/rust/perspective-viewer/src/rust/config.rs b/rust/perspective-viewer/src/rust/config.rs index 1caaecb180..2c9be0f5da 100644 --- a/rust/perspective-viewer/src/rust/config.rs +++ b/rust/perspective-viewer/src/rust/config.rs @@ -42,6 +42,9 @@ pub enum ViewerConfigEncoding { #[serde(rename = "arraybuffer")] ArrayBuffer, + + #[serde(skip_serializing)] + JSONString, } #[derive(Serialize, PartialEq)] @@ -90,8 +93,11 @@ impl ViewerConfig { .slice_with_end(start, start + len) .unchecked_into()) } + Some(ViewerConfigEncoding::JSONString) => { + Ok(JsValue::from(serde_json::to_string(self).into_jserror()?)) + } None | Some(ViewerConfigEncoding::JSON) => { - Ok(JsValue::from_serde(self).unwrap()) + JsValue::from_serde(self).into_jserror() } } } diff --git a/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs index 32668d63e6..3f789a9a2f 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs @@ -11,9 +11,8 @@ use crate::custom_elements::modal::*; use crate::model::*; use crate::renderer::Renderer; use crate::session::Session; +use crate::utils::*; -use js_intern::*; -use js_sys::*; use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::prelude::*; @@ -43,96 +42,30 @@ impl ExportDropDownMenuElement { .unwrap() .unchecked_into::(); - let plugin = renderer.get_active_plugin().unwrap(); - let opts = if Reflect::has(&plugin, js_intern!("render")).unwrap() { - vec![ - ExportMethod::Csv, - ExportMethod::Arrow, - ExportMethod::Html, - ExportMethod::Png, - ] - } else { - vec![ExportMethod::Csv, ExportMethod::Arrow, ExportMethod::Html] - }; - - let values = vec![ - ExportDropDownMenuItem::OptGroup("Current View", opts), - ExportDropDownMenuItem::OptGroup( - "All", - vec![ExportMethod::CsvAll, ExportMethod::ArrowAll], - ), - ]; - let modal_rc: Rc>>> = Default::default(); let callback = Callback::from({ let modal_rc = modal_rc.clone(); - move |x| match x { - ExportMethod::Csv => { - let session = session.clone(); - let modal = modal_rc.borrow().clone().unwrap(); - spawn_local(async move { - session.download_as_csv(false).await.expect("Export failed"); - modal.hide().unwrap(); - }); - } - ExportMethod::CsvAll => { - let session = session.clone(); - let modal = modal_rc.borrow().clone().unwrap(); - spawn_local(async move { - session.download_as_csv(true).await.expect("Export failed"); - modal.hide().unwrap(); - }); - } - ExportMethod::Arrow => { - let session = session.clone(); - let modal = modal_rc.borrow().clone().unwrap(); - spawn_local(async move { - session - .download_as_arrow(false) - .await - .expect("Export failed"); - modal.hide().unwrap(); - }); - } - ExportMethod::ArrowAll => { - let session = session.clone(); - let modal = modal_rc.borrow().clone().unwrap(); - spawn_local(async move { - session - .download_as_arrow(true) - .await - .expect("Export failed"); - modal.hide().unwrap(); - }); - } - ExportMethod::Html => { + let renderer = renderer.clone(); + move |x: ExportFile| { + if !x.name.is_empty() { let session = session.clone(); let renderer = renderer.clone(); let modal = modal_rc.borrow().clone().unwrap(); spawn_local(async move { - (&session, &renderer) - .download_as_html() + let val = (&session, &renderer) + .export_method_to_jsvalue(x.method) .await - .expect("Export failed"); - + .unwrap(); + download(&x.to_filename(), &val).unwrap(); modal.hide().unwrap(); - }); - } - ExportMethod::Png => { - let render = Reflect::get(&plugin, js_intern!("render")).unwrap(); - render.unchecked_into::().call0(&plugin).unwrap(); - modal_rc.borrow().clone().unwrap().hide().unwrap(); + }) } } }); - let props = ExportDropDownMenuProps { - values: Rc::new(values), - callback, - }; - + let props = ExportDropDownMenuProps { renderer, callback }; let modal = ModalElement::new(dropdown, props, true); *modal_rc.borrow_mut() = Some(modal.clone()); ExportDropDownMenuElement { diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index e747b2b7a7..e674aa9836 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -403,7 +403,8 @@ impl PerspectiveViewerElement { pub fn js_download(&self, flat: bool) -> js_sys::Promise { let session = self.session.clone(); future_to_promise(async move { - session.download_as_csv(flat).await?; + let val = session.csv_as_jsvalue(flat).await?; + download("untitled.csv", &val)?; Ok(JsValue::UNDEFINED) }) } diff --git a/rust/perspective-viewer/src/rust/dragdrop.rs b/rust/perspective-viewer/src/rust/dragdrop.rs index 8ca1e90b27..c927eabe2d 100644 --- a/rust/perspective-viewer/src/rust/dragdrop.rs +++ b/rust/perspective-viewer/src/rust/dragdrop.rs @@ -155,10 +155,12 @@ impl DragDrop { _ => true, }; - r.drag_state - .as_mut() - .expect("Hover index without hover") - .state = Some((action, index)); + crate::js_log_maybe! { + r.drag_state + .as_mut() + .into_jserror()? + .state = Some((action, index)) + }; should_render } diff --git a/rust/perspective-viewer/src/rust/js/perspective.rs b/rust/perspective-viewer/src/rust/js/perspective.rs index d433cc3e39..a33f97dbd4 100644 --- a/rust/perspective-viewer/src/rust/js/perspective.rs +++ b/rust/perspective-viewer/src/rust/js/perspective.rs @@ -100,6 +100,11 @@ extern "C" { this: &JsPerspectiveView, ) -> Result; + #[wasm_bindgen(method, catch, js_name = to_columns)] + pub async fn _to_columns( + this: &JsPerspectiveView, + ) -> Result; + #[wasm_bindgen(method, catch, js_name = num_rows)] pub async fn _num_rows(this: &JsPerspectiveView) -> Result; @@ -158,6 +163,7 @@ impl JsPerspectiveTable { impl JsPerspectiveView { async_typed!(_to_csv, to_csv(&self, options: js_sys::Object) -> js_sys::JsString); async_typed!(_to_arrow, to_arrow(&self) -> js_sys::ArrayBuffer); + async_typed!(_to_columns, to_columns(&self) -> js_sys::Object); async_typed!(_num_rows, num_rows(&self) -> f64); async_typed!(_num_columns, num_columns(&self) -> f64); async_typed!(_schema, schema(&self) -> JsPerspectiveViewSchema); diff --git a/rust/perspective-viewer/src/rust/model.rs b/rust/perspective-viewer/src/rust/model.rs index e1e51bfdb2..35e94263af 100644 --- a/rust/perspective-viewer/src/rust/model.rs +++ b/rust/perspective-viewer/src/rust/model.rs @@ -7,19 +7,22 @@ // file. mod columns_iter_set; +mod export_method; pub use self::columns_iter_set::*; - +pub use self::export_method::*; use crate::config::*; use crate::dragdrop::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; use futures::join; +use js_intern::*; use std::future::Future; use std::pin::Pin; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; use yew::prelude::*; /// A `SessionRendererModel` is any struct with `session` and `renderer` fields, as @@ -64,22 +67,18 @@ pub trait SessionRendererModel { }); } - fn download_as_html(&self) -> Pin>>> { + fn html_as_jsvalue( + &self, + ) -> Pin>>> { let view_config = self.get_viewer_config(); let session = self.session().clone(); Box::pin(async move { - if let (Ok(arrow), Ok(mut config)) = - join!(session.get_table_arrow(), view_config) - { - config.settings = false; - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let element: web_sys::HtmlElement = - document.create_element("a")?.unchecked_into(); - let js_config = serde_json::to_string(&config).into_jserror()?; - let blob_url = { - let html = - JsValue::from(format!(" + let (arrow, config) = join!(session.get_table_arrow(), view_config); + let arrow = arrow?; + let mut config = config?; + config.settings = false; + let js_config = serde_json::to_string(&config).into_jserror()?; + let html = JsValue::from(format!(" @@ -108,20 +107,8 @@ window.viewer.restore(JSON.parse(window.layout.textContent)); ", base64::encode(arrow), js_config)); - let array = [html].iter().collect::(); - let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; - web_sys::Url::create_object_url_with_blob(&blob)? - }; - - element.set_attribute("download", "perspective.html")?; - element.set_attribute("href", &blob_url)?; - element.style().set_property("display", "none")?; - document.body().unwrap().append_child(&element)?; - element.click(); - document.body().unwrap().remove_child(&element)?; - } - - Ok(()) + let array = [html].iter().collect::(); + Ok(web_sys::Blob::new_with_u8_array_sequence(&array)?.into()) }) } @@ -150,6 +137,67 @@ window.viewer.restore(JSON.parse(window.layout.textContent)); }) }) } + + fn config_as_jsvalue( + &self, + ) -> Pin>>> { + let viewer_config = self.get_viewer_config(); + Box::pin(async move { + viewer_config + .await? + .encode(&Some(ViewerConfigEncoding::JSONString)) + }) + } + + fn export_method_to_jsvalue( + &self, + method: ExportMethod, + ) -> Pin>>> { + match method { + ExportMethod::Csv => { + let session = self.session().clone(); + Box::pin(async move { session.csv_as_jsvalue(false).await }) + } + ExportMethod::CsvAll => { + let session = self.session().clone(); + Box::pin(async move { session.csv_as_jsvalue(true).await }) + } + ExportMethod::Json => { + let session = self.session().clone(); + Box::pin(async move { session.json_as_jsvalue(false).await }) + } + ExportMethod::JsonAll => { + let session = self.session().clone(); + Box::pin(async move { session.json_as_jsvalue(true).await }) + } + ExportMethod::Arrow => { + let session = self.session().clone(); + Box::pin(async move { session.arrow_as_jsvalue(false).await }) + } + ExportMethod::ArrowAll => { + let session = self.session().clone(); + Box::pin(async move { session.arrow_as_jsvalue(true).await }) + } + ExportMethod::Html => { + let html_task = self.html_as_jsvalue(); + Box::pin(async move { html_task.await }) + } + ExportMethod::Png => { + let renderer = self.renderer().clone(); + Box::pin(async move { + let plugin = renderer.get_active_plugin()?; + let render = js_sys::Reflect::get(&plugin, js_intern!("render"))?; + let render_fun = render.unchecked_into::(); + let png = render_fun.call0(&plugin)?; + JsFuture::from(png.unchecked_into::()).await + }) + } + ExportMethod::JsonConfig => { + let config_task = self.config_as_jsvalue(); + Box::pin(async move { config_task.await }) + } + } + } } impl crate::model::SessionRendererModel for (&Session, &Renderer) { diff --git a/rust/perspective-viewer/src/rust/model/export_method.rs b/rust/perspective-viewer/src/rust/model/export_method.rs new file mode 100644 index 0000000000..7c26de293a --- /dev/null +++ b/rust/perspective-viewer/src/rust/model/export_method.rs @@ -0,0 +1,69 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use crate::*; +use std::rc::Rc; +use yew::prelude::*; + +#[derive(Clone, Copy, PartialEq)] +pub enum ExportMethod { + Csv, + CsvAll, + Json, + JsonAll, + Html, + Png, + Arrow, + ArrowAll, + JsonConfig, +} + +impl ExportMethod { + pub fn new_file(&self, x: &str) -> ExportFile { + ExportFile { + name: Rc::new(x.to_owned()), + method: *self, + } + } +} + +#[derive(Clone, PartialEq)] +pub struct ExportFile { + pub name: Rc, + pub method: ExportMethod, +} + +impl ExportFile { + pub fn to_filename(&self) -> String { + match self.method { + ExportMethod::Csv => format!("{}.csv", self.name), + ExportMethod::CsvAll => format!("{}.all.csv", self.name), + ExportMethod::Json => format!("{}.json", self.name), + ExportMethod::JsonAll => format!("{}.all.json", self.name), + ExportMethod::Html => format!("{}.html", self.name), + ExportMethod::Png => format!("{}.png", self.name), + ExportMethod::Arrow => format!("{}.arrow", self.name), + ExportMethod::ArrowAll => format!("{}.all.arrow", self.name), + ExportMethod::JsonConfig => format!("{}.config.json", self.name), + } + } +} + +impl From for Html { + fn from(x: ExportFile) -> Self { + let class = if x.name.is_empty() { + Some("invalid") + } else { + None + }; + + html_template! { + { x.to_filename() } + } + } +} diff --git a/rust/perspective-viewer/src/rust/session.rs b/rust/perspective-viewer/src/rust/session.rs index b4e4054a66..e7c6f61096 100644 --- a/rust/perspective-viewer/src/rust/session.rs +++ b/rust/perspective-viewer/src/rust/session.rs @@ -7,12 +7,12 @@ // file. mod copy; -mod download; mod metadata; mod view; mod view_subscription; use self::metadata::*; +use self::view::PerspectiveOwned; use self::view::View; pub use self::view_subscription::TableStats; use self::view_subscription::*; @@ -24,7 +24,6 @@ use crate::utils::*; use crate::*; use copy::*; -use download::*; use futures::channel::oneshot::*; use itertools::Itertools; @@ -463,27 +462,6 @@ impl Session { Ok(()) } - pub async fn download_as_arrow(self, flat: bool) -> Result<(), JsValue> { - if flat { - let table = self.borrow().table.clone(); - if let Some(table) = table { - download_arrow_flat(&table).await?; - } - } else { - let view = self - .borrow() - .view_sub - .as_ref() - .map(|x| x.get_view().clone()); - - if let Some(view) = view { - download_arrow(&view).await?; - } - }; - - Ok(()) - } - pub async fn get_table_arrow(&self) -> Result, JsValue> { let table = self.borrow().table.clone().into_jserror()?; let view = table.view(&js_object!().unchecked_into()).await?; @@ -493,25 +471,39 @@ impl Session { Ok(js_sys::Uint8Array::new(&bytes).to_vec()) } - pub async fn download_as_csv(self, flat: bool) -> Result<(), JsValue> { - if flat { - let table = self.borrow().table.clone(); - if let Some(table) = table { - download_csv_flat(&table).await?; - } + async fn flat_as_jsvalue(&self, flat: bool) -> Result { + Ok(if flat { + let table = self.borrow().table.clone().into_jserror()?; + PerspectiveOwned::new(table.view(&js_object!().unchecked_into()).await?) } else { - let view = self - .borrow() + self.borrow() .view_sub .as_ref() - .map(|x| x.get_view().clone()); + .map(|x| x.get_view().clone()) + .into_jserror()? + }) + } - if let Some(view) = view { - download_csv(&view).await?; - } - }; + pub async fn arrow_as_jsvalue(self, flat: bool) -> Result { + let view = self.flat_as_jsvalue(flat).await?; + let arrow = view.to_arrow().await.unwrap(); + Ok(js_sys::Uint8Array::new(&arrow).into()) + } - Ok(()) + pub async fn json_as_jsvalue(self, flat: bool) -> Result { + let view = self.flat_as_jsvalue(flat).await?; + let json = view.to_columns().await.unwrap(); + Ok(js_sys::JSON::stringify(&json)?.into()) + } + + pub async fn csv_as_jsvalue(&self, flat: bool) -> Result { + let view = self.flat_as_jsvalue(flat).await?; + let csv_fut = view.to_csv(js_object!("formatted", true)); + let csv = csv_fut.await.unwrap(); + let csv_str = csv.as_string().unwrap(); + let bytes = csv_str.as_bytes(); + let value = unsafe { js_sys::Uint8Array::view(bytes) }; + Ok(value.unchecked_into()) } pub fn get_view(&self) -> Option { diff --git a/rust/perspective-viewer/src/rust/session/download.rs b/rust/perspective-viewer/src/rust/session/download.rs deleted file mode 100644 index c583ec0567..0000000000 --- a/rust/perspective-viewer/src/rust/session/download.rs +++ /dev/null @@ -1,86 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright (c) 2018, the Perspective Authors. -// -// This file is part of the Perspective library, distributed under the terms -// of the Apache License 2.0. The full license can be found in the LICENSE -// file. - -use crate::js::perspective::*; -use crate::*; - -use super::view::*; - -use js_sys::*; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; - -/// Download a flat (unpivoted with all columns) CSV. -pub async fn download_csv_flat(table: &JsPerspectiveTable) -> Result<(), JsValue> { - let view = table.view(&js_object!().unchecked_into()).await?; - download_csv_async(&view).await?; - view.delete().await -} - -/// Download a CSV -pub async fn download_csv(view: &View) -> Result<(), JsValue> { - download_csv_async(view).await -} - -/// Download a CSV, but not a `Promise`. Used to implement the public methods. -async fn download_csv_async(view: &JsPerspectiveView) -> Result<(), JsValue> { - let csv_fut = view.to_csv(js_object!("formatted", true)); - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let element: web_sys::HtmlElement = document.create_element("a")?.unchecked_into(); - let blob_url = { - let csv = csv_fut.await.unwrap(); - let csv_str = csv.as_string().unwrap(); - let bytes = csv_str.as_bytes(); - let array = unsafe { [Uint8Array::view(bytes)].iter().collect::() }; - let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; - web_sys::Url::create_object_url_with_blob(&blob)? - }; - - element.set_attribute("download", "perspective.csv")?; - element.set_attribute("href", &blob_url)?; - element.style().set_property("display", "none")?; - document.body().unwrap().append_child(&element)?; - element.click(); - document.body().unwrap().remove_child(&element)?; - Ok(()) -} - -/// Download a flat (unpivoted with all columns) Arrow. -pub async fn download_arrow_flat(table: &JsPerspectiveTable) -> Result<(), JsValue> { - let view = table.view(&js_object!().unchecked_into()).await?; - download_arrow_async(&view).await?; - view.delete().await -} - -/// Download an Apache Arrow -pub async fn download_arrow(view: &View) -> Result<(), JsValue> { - download_arrow_async(view).await -} - -/// Download a CSV, but not a `Promise`. Used to implement the public methods. -async fn download_arrow_async(view: &JsPerspectiveView) -> Result<(), JsValue> { - let csv_fut = view.to_arrow(); - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let element: web_sys::HtmlElement = document.create_element("a")?.unchecked_into(); - let blob_url = { - let bytes = csv_fut.await.unwrap(); - let array = [Uint8Array::new(&bytes)].iter().collect::(); - let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; - web_sys::Url::create_object_url_with_blob(&blob)? - }; - - element.set_attribute("download", "perspective.arrow")?; - element.set_attribute("href", &blob_url)?; - element.style().set_property("display", "none")?; - document.body().unwrap().append_child(&element)?; - element.click(); - document.body().unwrap().remove_child(&element)?; - Ok(()) -} diff --git a/rust/perspective-viewer/src/rust/utils/download.rs b/rust/perspective-viewer/src/rust/utils/download.rs new file mode 100644 index 0000000000..e284b05535 --- /dev/null +++ b/rust/perspective-viewer/src/rust/utils/download.rs @@ -0,0 +1,29 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +pub fn download(name: &str, value: &JsValue) -> Result<(), JsValue> { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let element: web_sys::HtmlElement = document.create_element("a")?.unchecked_into(); + let blob_url = { + let array = [value].iter().collect::(); + let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; + web_sys::Url::create_object_url_with_blob(&blob)? + }; + + element.set_attribute("download", name)?; + element.set_attribute("href", &blob_url)?; + element.style().set_property("display", "none")?; + document.body().unwrap().append_child(&element)?; + element.click(); + document.body().unwrap().remove_child(&element)?; + Ok(()) +} diff --git a/rust/perspective-viewer/src/rust/utils/mod.rs b/rust/perspective-viewer/src/rust/utils/mod.rs index 88a8e55671..84dd99e01f 100644 --- a/rust/perspective-viewer/src/rust/utils/mod.rs +++ b/rust/perspective-viewer/src/rust/utils/mod.rs @@ -10,6 +10,7 @@ mod async_callback; mod closure; mod datetime; mod debounce; +mod download; mod errors; mod future_to_promise; mod js_object; @@ -25,6 +26,7 @@ pub use self::async_callback::*; pub use self::closure::*; pub use self::datetime::*; pub use self::debounce::*; +pub use self::download::*; pub use self::errors::*; pub use self::future_to_promise::*; pub use self::pubsub::*; @@ -46,6 +48,23 @@ macro_rules! maybe { }}; } +#[macro_export] +macro_rules! js_log_maybe { + ($($exp:stmt);* $(;)*) => {{ + #[must_use] + let x = ({ + #[inline(always)] + || { + { + $($exp)* + }; + Ok(()) + } + })(); + x.unwrap_or_else(|e| web_sys::console::error_1(&e)) + }}; +} + /// A helper to for the pattern `let x2 = x;` necessary to clone structs destined /// for an `async` or `'static` closure stack. This is like `move || { .. }` or /// `move async { .. }`, but for clone semantics. From 33d9bda830099eba2b50e709de45cd0208cc9810 Mon Sep 17 00:00:00 2001 From: jkusa Date: Wed, 2 Mar 2022 09:43:18 -0600 Subject: [PATCH 12/31] fix: fire config-update evnt on d3fc updates --- .../src/js/axis/splitterLabels.js | 7 +- .../js/legend/styling/draggableComponent.js | 10 +- .../test/js/integration/events.spec.js | 144 ++++++++++++++++++ .../test/results/results.json | 38 ++--- 4 files changed, 175 insertions(+), 24 deletions(-) create mode 100644 packages/perspective-viewer-d3fc/test/js/integration/events.spec.js diff --git a/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js b/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js index 5001010c76..df2d1921fb 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js @@ -32,7 +32,7 @@ export const splitterLabels = (settings) => { .style("color", (d) => coloured ? withoutOpacity(color(d.name)) : undefined ) - .on("click", (_event, d) => { + .on("click", (event, d) => { if (disabled) return; if (alt) { @@ -45,6 +45,11 @@ export const splitterLabels = (settings) => { ); } + event.target + .getRootNode() + .host.closest("perspective-viewer") + ?.dispatchEvent(new Event("perspective-config-update")); + redrawChart(selection); }); }; diff --git a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js index ef460a1165..3e5c5c30fc 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js @@ -19,6 +19,7 @@ export function draggableComponent() { const draggable = (element) => { const node = element.node(); + const viewer = node.getRootNode().host.closest("perspective-viewer"); node.style.cursor = "move"; if (settings.legend) { node.style.left = settings.legend.left; @@ -39,12 +40,11 @@ export function draggableComponent() { }; settings.legend = {...settings.legend, ...position}; - if (isNodeInTopRight(node)) { - pinned = pinNodeToTopRight(node); - return; - } + pinned = isNodeInTopRight(node) + ? pinNodeToTopRight(node) + : unpinNodeFromTopRight(node, pinned); - pinned = unpinNodeFromTopRight(node, pinned); + viewer?.dispatchEvent(new Event("perspective-config-update")); }); element.call(drag); diff --git a/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js b/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js new file mode 100644 index 0000000000..5d6fb899d9 --- /dev/null +++ b/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js @@ -0,0 +1,144 @@ +/****************************************************************************** + * + * Copyright (c) 2022, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +const path = require("path"); + +const utils = require("@finos/perspective-test"); +const simple_tests = require("@finos/perspective-viewer/test/js/simple_tests.js"); + +const {withTemplate} = require("./simple-template"); +withTemplate("events", "Y Line"); + +async function get_contents(page) { + return await page.evaluate(async () => { + const viewer = document + .querySelector(`perspective-viewer perspective-viewer-d3fc-yline`) + .shadowRoot.querySelector("svg"); + return viewer.outerHTML || "MISSING"; + }); +} + +utils.with_server({}, () => { + describe.page( + "events.html", + () => { + test.capture( + "perspective-config-update event is fired when series axis is changed", + async (page) => { + // Await the viewer element to exist on the page + const viewer = await page.$("perspective-viewer"); + await page.evaluate(async (viewer) => { + // Await the table load + await viewer.getTable(); + + viewer.restore({ + plugin: "Y Line", + columns: ["Sales", "Profit"], + }); + + // Register a listener for `perspective-config-update` event + window.__series_events__ = []; + viewer.addEventListener( + "perspective-config-update", + (evt) => { + window.__series_events__.push(evt); + } + ); + }, viewer); + const axisLabel = ( + await page.waitForFunction(() => + document + .querySelector("perspective-viewer-d3fc-yline") + .shadowRoot.querySelector(".y-label") + ) + ).asElement(); + await axisLabel.click(axisLabel); + + const count = await page.evaluate(async (viewer) => { + // Await the plugin rendering + await viewer.flush(); + + // Count the events; + return window.__series_events__.length; + }, viewer); + + // Expect 1 event + expect(count).toEqual(1); + + // Return the chart contents + return get_contents(page); + } + ); + + test.capture( + "perspective-config-update event is fired when legend position is changed", + async (page) => { + // Await the viewer element to exist on the page + const viewer = await page.$("perspective-viewer"); + await page.evaluate(async (viewer) => { + // Await the table load + await viewer.getTable(); + + viewer.restore({ + plugin: "Y Line", + columns: ["Sales", "Profit"], + }); + + // Register a listener for `perspective-config-update` event + window.__legend_events__ = []; + viewer.addEventListener( + "perspective-config-update", + (evt) => { + window.__legend_events__.push(evt); + } + ); + }, viewer); + + const legend = ( + await page.waitForFunction(() => + document + .querySelector("perspective-viewer-d3fc-yline") + .shadowRoot.querySelector(".legend-container") + ) + ).asElement(); + + const boundingBox = await legend.boundingBox(); + + const start = { + x: boundingBox.x + boundingBox.width / 2, + y: boundingBox.y + boundingBox.height / 2, + }; + + const target = { + x: start.x - 300, + y: start.y, + }; + + await page.mouse.move(start.x, start.y); + await page.mouse.down(); + await page.mouse.move(target.x, target.y); + + const count = await page.evaluate(async (viewer) => { + // Await the plugin rendering + await viewer.flush(); + + // Count the events; + return window.__legend_events__.length; + }, viewer); + + expect(count).toBeGreaterThan(0); + + // Return the chart contents + return get_contents(page); + } + ); + }, + {reload_page: false, root: path.join(__dirname, "..", "..", "..")} + ); +}); diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index 4033088f1b..46e29d45f0 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -1,5 +1,5 @@ { - "__GIT_COMMIT__": "d776202ff728f808f1d07060e5ce73bb392402f1", + "__GIT_COMMIT__": "9e69fd6a64d79303a423d5390869e1cc165d1fe6", "area_shows_a_grid_without_any_settings_applied": "3852532b8ee6abba3373a10d41bb4df2", "area_displays_visible_columns_": "919cc6f6c2a2f2ec13b90dfb60e1b7ef", "area_pivot_by_a_row": "7495976cfed69cfe9db2b3fdc6ef4707", @@ -91,19 +91,19 @@ "heatmap_filters_filters_by_a_numeric_column": "b9e271668af3836baf5a5d51226910a3", "heatmap_filters_filters_by_an_alpha_column": "b9e271668af3836baf5a5d51226910a3", "heatmap_filters_filters_with__in__comparator": "b9e271668af3836baf5a5d51226910a3", - "scatter_shows_a_grid_without_any_settings_applied": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_shows_a_grid_without_any_settings_applied": "afb75b167c5ea6be21c7b2121901a17f", "scatter_displays_visible_columns_": "0519cc65006ebd80de93362817a7ce4f", - "scatter_pivot_by_a_row": "9cbb9843a44b830cf4a324706a7bd713", - "scatter_pivot_by_two_rows": "9cbb9843a44b830cf4a324706a7bd713", - "scatter_pivot_by_a_column": "9cbb9843a44b830cf4a324706a7bd713", - "scatter_pivot_by_a_row_and_a_column": "9cbb9843a44b830cf4a324706a7bd713", - "scatter_pivot_by_two_rows_and_two_columns": "9cbb9843a44b830cf4a324706a7bd713", - "scatter_sort_by_a_hidden_column": "9cbb9843a44b830cf4a324706a7bd713", - "scatter_sort_by_a_numeric_column": "9cbb9843a44b830cf4a324706a7bd713", - "scatter_sort_by_an_alpha_column": "01fa363fa5ef2e1aa66a77ed87e9225f", - "scatter_filters_filters_by_a_numeric_column": "37fd54838ea9dead5dc441bf1e62fbb9", - "scatter_filters_filters_by_an_alpha_column": "2b3728cca2c7518bb10382dd7ef0d4fd", - "scatter_filters_filters_with__in__comparator": "f44f7f144f9ee4827783a9b516f2a4e3", + "scatter_pivot_by_a_row": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_pivot_by_two_rows": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_pivot_by_a_column": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_pivot_by_a_row_and_a_column": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_pivot_by_two_rows_and_two_columns": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_sort_by_a_hidden_column": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_sort_by_a_numeric_column": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_sort_by_an_alpha_column": "3a0440b79d5a068fc3e2c48b3d15a01e", + "scatter_filters_filters_by_a_numeric_column": "adbc354df9b4f69db72160fcad7bde74", + "scatter_filters_filters_by_an_alpha_column": "cf64a7ffdc6f150d1f535cf08679f052", + "scatter_filters_filters_with__in__comparator": "16fbc02857ffd2df180274a19c51c4f2", "yscatter_shows_a_grid_without_any_settings_applied": "006aa96e87e006a01a0349f0c20ea27a", "yscatter_displays_visible_columns_": "0bff95a36d9f0a0029599bb7adcc6af8", "yscatter_pivot_by_a_row": "fb6504a26b1b859619a180b27d35f73b", @@ -121,10 +121,10 @@ "sunburst_pivot_by_two_rows": "38fe70a8f88a3e0df185f8775c88317d", "sunburst_pivot_by_a_row_and_a_column": "fa789a00c03e04567bd45a8451e7a00a", "sunburst_pivot_by_two_rows_and_two_columns": "e03658ff241f45837b9d61227bf023a4", - "treemap_pivot_by_a_row": "7322fa73924e688662d36558a33ddb99", - "treemap_pivot_by_two_rows": "ce30c9196e830d54d4960d4f9a53ad32", - "treemap_pivot_by_a_row_and_a_column": "a9ee5c25d23fff12e57b7755ca310cb5", - "treemap_pivot_by_two_rows_and_two_columns": "fe5616df5b45c5b0b2f86489f7a1dc22", + "treemap_pivot_by_a_row": "d9e965d744906a2320148b73dc1fcb29", + "treemap_pivot_by_two_rows": "28390c90695be1b38bd2a05a984dd242", + "treemap_pivot_by_a_row_and_a_column": "a36877ce32fed64d1ae16807de27e9de", + "treemap_pivot_by_two_rows_and_two_columns": "92203a8d4778c041836361b56ff39174", "treemap_shows_a_grid_without_any_settings_applied": "10d1208b485425756fcc932229386b02", "treemap_displays_visible_columns_": "10d1208b485425756fcc932229386b02", "treemap_pivot_by_a_column": "10d1208b485425756fcc932229386b02", @@ -143,5 +143,7 @@ "sunburst_filters_filters_by_a_numeric_column": "10d1208b485425756fcc932229386b02", "sunburst_filters_filters_by_an_alpha_column": "10d1208b485425756fcc932229386b02", "sunburst_filters_filters_with__in__comparator": "10d1208b485425756fcc932229386b02", - "bar_rendering_bugs_correctly_render_when_a_bar_chart_has_non_equidistant_times_on_a_datetime_axis": "9e7617a454d83e328aaa3d25c8145a0e" + "bar_rendering_bugs_correctly_render_when_a_bar_chart_has_non_equidistant_times_on_a_datetime_axis": "9e7617a454d83e328aaa3d25c8145a0e", + "events_perspective-config-update_event_is_fired_when_series_axis_is_changed": "c07cb12b00fe628aba8751721f8f5498", + "events_perspective-config-update_event_is_fired_when_legend_position_is_changed": "c07cb12b00fe628aba8751721f8f5498" } \ No newline at end of file From 6a2ab08a9cd71c03eeecb48cda5ecad5f35595ee Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Mon, 7 Mar 2022 23:33:07 -0500 Subject: [PATCH 13/31] Add copy dropdown menu --- .../src/rust/components/copy_dropdown.rs | 33 +++++++ .../src/rust/components/mod.rs | 1 + .../src/rust/components/status_bar.rs | 48 ++++++---- .../src/rust/components/tests/status_bar.rs | 2 +- .../src/rust/custom_elements/copy_dropdown.rs | 87 +++++++++++++++++++ .../rust/custom_elements/export_dropdown.rs | 2 +- .../src/rust/custom_elements/mod.rs | 1 + .../src/rust/custom_elements/viewer.rs | 11 ++- .../src/rust/js/clipboard_item.rs | 17 ++++ rust/perspective-viewer/src/rust/js/mod.rs | 1 + .../src/rust/js/resize_observer.rs | 4 +- rust/perspective-viewer/src/rust/model.rs | 22 +++-- .../src/rust/model/export_method.rs | 76 +++++++++++++--- rust/perspective-viewer/src/rust/session.rs | 54 +++++------- .../src/rust/session/copy.rs | 69 --------------- .../src/rust/utils/clipboard.rs | 64 ++++++++++++++ .../src/rust/utils/download.rs | 6 +- rust/perspective-viewer/src/rust/utils/mod.rs | 2 + .../src/themes/material-dark.less | 3 + .../src/themes/material.less | 3 + .../src/themes/monokai.less | 3 + .../src/themes/solarized-dark.less | 3 + .../src/themes/solarized.less | 3 + .../src/themes/vaporwave.less | 3 + 24 files changed, 371 insertions(+), 147 deletions(-) create mode 100644 rust/perspective-viewer/src/rust/components/copy_dropdown.rs create mode 100644 rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs create mode 100644 rust/perspective-viewer/src/rust/js/clipboard_item.rs delete mode 100644 rust/perspective-viewer/src/rust/session/copy.rs create mode 100644 rust/perspective-viewer/src/rust/utils/clipboard.rs diff --git a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs new file mode 100644 index 0000000000..b397c87878 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use super::containers::dropdown_menu::*; +use crate::model::*; + +pub fn get_menu_items(has_render: bool) -> Vec { + vec![ + CopyDropDownMenuItem::OptGroup( + "Current View", + if has_render { + vec![ExportMethod::Csv, ExportMethod::Json, ExportMethod::Png] + } else { + vec![ExportMethod::Csv, ExportMethod::Json] + }, + ), + CopyDropDownMenuItem::OptGroup( + "All", + vec![ExportMethod::CsvAll, ExportMethod::JsonAll], + ), + CopyDropDownMenuItem::OptGroup("Config", vec![ExportMethod::JsonConfig]), + ] +} + +pub type CopyDropDownMenu = DropDownMenu; +pub type CopyDropDownMenuProps = DropDownMenuProps; +pub type CopyDropDownMenuMsg = DropDownMenuMsg; +pub type CopyDropDownMenuItem = DropDownMenuItem; diff --git a/rust/perspective-viewer/src/rust/components/mod.rs b/rust/perspective-viewer/src/rust/components/mod.rs index fed9af25b9..f7049a44e2 100644 --- a/rust/perspective-viewer/src/rust/components/mod.rs +++ b/rust/perspective-viewer/src/rust/components/mod.rs @@ -9,6 +9,7 @@ //! `components` contains all Yew `Component` types, but only exports the 4 necessary //! for public Custom Elements. The rest are internal components of these 4. +pub mod copy_dropdown; pub mod export_dropdown; pub mod expression_editor; pub mod filter_dropdown; diff --git a/rust/perspective-viewer/src/rust/components/status_bar.rs b/rust/perspective-viewer/src/rust/components/status_bar.rs index c7ebecc1b9..688e8506c4 100644 --- a/rust/perspective-viewer/src/rust/components/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/status_bar.rs @@ -8,12 +8,13 @@ use crate::components::containers::select::*; use crate::components::status_bar_counter::StatusBarRowsCounter; +use crate::custom_elements::copy_dropdown::*; use crate::custom_elements::export_dropdown::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; use crate::*; -use wasm_bindgen_futures::spawn_local; + use web_sys::*; use yew::prelude::*; @@ -41,7 +42,7 @@ impl PartialEq for StatusBarProps { pub enum StatusBarMsg { Reset(bool), Export, - Copy(bool), + Copy, SetThemeConfig((Vec, Option)), SetTheme(String), TableStatsChanged, @@ -54,7 +55,9 @@ pub struct StatusBar { theme: Option, themes: Vec, export_ref: NodeRef, + copy_ref: NodeRef, export_dropdown: Option, + copy_dropdown: Option, _sub: [Subscription; 4], } @@ -91,6 +94,8 @@ impl Component for StatusBar { _sub, theme: None, themes: vec![], + copy_dropdown: None, + copy_ref: NodeRef::default(), export_dropdown: None, export_ref: NodeRef::default(), is_updating: 0, @@ -128,20 +133,25 @@ impl Component for StatusBar { false } StatusBarMsg::Export => { - let session = ctx.props().session.clone(); - let renderer = ctx.props().renderer.clone(); - let export_dropdown = ExportDropDownMenuElement::new(session, renderer); let target = self.export_ref.cast::().unwrap(); - export_dropdown.open(target); - self.export_dropdown = Some(export_dropdown); + self.export_dropdown + .get_or_insert_with(|| { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + ExportDropDownMenuElement::new(session, renderer) + }) + .open(target); false } - StatusBarMsg::Copy(flat) => { - let session = ctx.props().session.clone(); - spawn_local(async move { - session.copy_to_clipboard(flat).await.expect("Copy failed"); - }); - + StatusBarMsg::Copy => { + let target = self.copy_ref.cast::().unwrap(); + self.copy_dropdown + .get_or_insert_with(|| { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + CopyDropDownMenuElement::new(session, renderer) + }) + .open(target); false } } @@ -161,10 +171,7 @@ impl Component for StatusBar { .callback(|event: MouseEvent| StatusBarMsg::Reset(event.shift_key())); let export = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export); - - let copy = ctx - .link() - .callback(|event: MouseEvent| StatusBarMsg::Copy(event.shift_key())); + let copy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy); let theme_button = match &self.theme { None => html! {}, @@ -211,7 +218,12 @@ impl Component for StatusBar { { "Export" } - + + { "Copy" } { theme_button } diff --git a/rust/perspective-viewer/src/rust/components/tests/status_bar.rs b/rust/perspective-viewer/src/rust/components/tests/status_bar.rs index 1eff3f34ed..6df0081b68 100644 --- a/rust/perspective-viewer/src/rust/components/tests/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/tests/status_bar.rs @@ -55,7 +55,7 @@ pub fn test_callbacks_invoked() { status_bar.send_message(StatusBarMsg::Export); assert_eq!(token.get(), 0); let status_bar = link.borrow().clone().unwrap(); - status_bar.send_message(StatusBarMsg::Copy(false)); + status_bar.send_message(StatusBarMsg::Copy); assert_eq!(token.get(), 0); let status_bar = link.borrow().clone().unwrap(); status_bar.send_message(StatusBarMsg::Reset(false)); diff --git a/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs new file mode 100644 index 0000000000..a1ace97bcb --- /dev/null +++ b/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs @@ -0,0 +1,87 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use crate::components::copy_dropdown::*; +use crate::custom_elements::modal::*; +use crate::model::*; +use crate::renderer::Renderer; +use crate::session::Session; +use crate::utils::*; + +use js_intern::*; +use std::cell::RefCell; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::spawn_local; +use web_sys::*; +use yew::prelude::*; + +#[wasm_bindgen] +#[derive(Clone)] +pub struct CopyDropDownMenuElement { + modal: ModalElement, + target: Rc>>, +} + +impl ResizableMessage for ::Message { + fn resize(y: i32, x: i32, _: bool) -> Self { + CopyDropDownMenuMsg::SetPos(y, x) + } +} + +impl CopyDropDownMenuElement { + pub fn new(session: Session, renderer: Renderer) -> CopyDropDownMenuElement { + let document = window().unwrap().document().unwrap(); + let dropdown = document + .create_element("perspective-copy-dropdown") + .unwrap() + .unchecked_into::(); + + let modal_rc: Rc>>> = + Default::default(); + + let callback = Callback::from({ + let modal_rc = modal_rc.clone(); + let renderer = renderer.clone(); + move |x: ExportMethod| { + let js_task = (&session, &renderer).export_method_to_jsvalue(x); + let copy_task = copy_to_clipboard(js_task, x.mimetype()); + let modal = modal_rc.borrow().clone().unwrap(); + spawn_local(async move { + let result = copy_task.await; + crate::js_log_maybe! { + result?; + modal.hide()?; + } + }) + } + }); + + let plugin = renderer.get_active_plugin().unwrap(); + let has_render = js_sys::Reflect::has(&plugin, js_intern!("render")).unwrap(); + let values = Rc::new(get_menu_items(has_render)); + let props = CopyDropDownMenuProps { values, callback }; + let modal = ModalElement::new(dropdown, props, true); + *modal_rc.borrow_mut() = Some(modal.clone()); + CopyDropDownMenuElement { + modal, + target: Default::default(), + } + } + + pub fn open(&self, target: HtmlElement) { + self.modal.open(target, None); + } + + pub fn hide(&self) -> Result<(), JsValue> { + self.modal.hide() + } + + pub fn connected_callback(&self) {} +} diff --git a/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs index 3f789a9a2f..8c2e36f02e 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/export_dropdown.rs @@ -38,7 +38,7 @@ impl ExportDropDownMenuElement { pub fn new(session: Session, renderer: Renderer) -> ExportDropDownMenuElement { let document = window().unwrap().document().unwrap(); let dropdown = document - .create_element("perspective-filter-dropdown") + .create_element("perspective-export-dropdown") .unwrap() .unchecked_into::(); diff --git a/rust/perspective-viewer/src/rust/custom_elements/mod.rs b/rust/perspective-viewer/src/rust/custom_elements/mod.rs index 81156a2f7b..183ac74243 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/mod.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/mod.rs @@ -6,6 +6,7 @@ // of the Apache License 2.0. The full license can be found in the LICENSE // file. +pub mod copy_dropdown; pub mod export_dropdown; pub mod expression_editor; pub mod filter_dropdown; diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index e674aa9836..66d95ae7a6 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -415,9 +415,16 @@ impl PerspectiveViewerElement { /// - `flat` Whether to use the current `ViewConfig` to generate this data, or use /// the default. pub fn js_copy(&self, flat: bool) -> js_sys::Promise { - let session = self.session.clone(); + let method = if flat { + ExportMethod::CsvAll + } else { + ExportMethod::Csv + }; + + let js_task = self.export_method_to_jsvalue(method); + let copy_task = copy_to_clipboard(js_task, MimeType::TextPlain); future_to_promise(async move { - session.copy_to_clipboard(flat).await?; + copy_task.await?; Ok(JsValue::UNDEFINED) }) } diff --git a/rust/perspective-viewer/src/rust/js/clipboard_item.rs b/rust/perspective-viewer/src/rust/js/clipboard_item.rs new file mode 100644 index 0000000000..edeaffe259 --- /dev/null +++ b/rust/perspective-viewer/src/rust/js/clipboard_item.rs @@ -0,0 +1,17 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(inline_js = "export const ClipboardItem = window.ClipboardItem")] +extern "C" { + pub type ClipboardItem; + + #[wasm_bindgen(constructor, js_class = "ClipboardItem")] + pub fn new(files: &js_sys::Object) -> ClipboardItem; +} diff --git a/rust/perspective-viewer/src/rust/js/mod.rs b/rust/perspective-viewer/src/rust/js/mod.rs index f61104db58..a1f8f3cbe8 100644 --- a/rust/perspective-viewer/src/rust/js/mod.rs +++ b/rust/perspective-viewer/src/rust/js/mod.rs @@ -6,6 +6,7 @@ // of the Apache License 2.0. The full license can be found in the LICENSE // file. +pub mod clipboard_item; pub mod monaco; pub mod perspective; // pub mod perspective_viewer; diff --git a/rust/perspective-viewer/src/rust/js/resize_observer.rs b/rust/perspective-viewer/src/rust/js/resize_observer.rs index d4642d735e..4b3359e8d7 100644 --- a/rust/perspective-viewer/src/rust/js/resize_observer.rs +++ b/rust/perspective-viewer/src/rust/js/resize_observer.rs @@ -12,11 +12,11 @@ use wasm_bindgen::prelude::*; // use web_sys::HtmlElement; -#[wasm_bindgen(inline_js = "export default ResizeObserver")] +#[wasm_bindgen(inline_js = "export const ResizeObserver = window.ResizeObserver")] extern "C" { pub type ResizeObserver; - #[wasm_bindgen(constructor, js_class = "default")] + #[wasm_bindgen(constructor, js_class = "ResizeObserver")] pub fn new(callback: &js_sys::Function) -> ResizeObserver; #[wasm_bindgen(method)] diff --git a/rust/perspective-viewer/src/rust/model.rs b/rust/perspective-viewer/src/rust/model.rs index 35e94263af..998adc3653 100644 --- a/rust/perspective-viewer/src/rust/model.rs +++ b/rust/perspective-viewer/src/rust/model.rs @@ -69,7 +69,7 @@ pub trait SessionRendererModel { fn html_as_jsvalue( &self, - ) -> Pin>>> { + ) -> Pin>>> { let view_config = self.get_viewer_config(); let session = self.session().clone(); Box::pin(async move { @@ -108,7 +108,7 @@ window.viewer.restore(JSON.parse(window.layout.textContent)); ", base64::encode(arrow), js_config)); let array = [html].iter().collect::(); - Ok(web_sys::Blob::new_with_u8_array_sequence(&array)?.into()) + Ok(web_sys::Blob::new_with_u8_array_sequence(&array)?) }) } @@ -140,19 +140,23 @@ window.viewer.restore(JSON.parse(window.layout.textContent)); fn config_as_jsvalue( &self, - ) -> Pin>>> { + ) -> Pin>>> { let viewer_config = self.get_viewer_config(); Box::pin(async move { - viewer_config + let config = viewer_config .await? - .encode(&Some(ViewerConfigEncoding::JSONString)) + .encode(&Some(ViewerConfigEncoding::JSONString))?; + let array = [config].iter().collect::(); + let mut options = web_sys::BlobPropertyBag::new(); + options.type_("text/plain"); + web_sys::Blob::new_with_str_sequence_and_options(&array, &options) }) } fn export_method_to_jsvalue( &self, method: ExportMethod, - ) -> Pin>>> { + ) -> Pin>>> { match method { ExportMethod::Csv => { let session = self.session().clone(); @@ -189,7 +193,11 @@ window.viewer.restore(JSON.parse(window.layout.textContent)); let render = js_sys::Reflect::get(&plugin, js_intern!("render"))?; let render_fun = render.unchecked_into::(); let png = render_fun.call0(&plugin)?; - JsFuture::from(png.unchecked_into::()).await + let result = + JsFuture::from(png.unchecked_into::()) + .await? + .unchecked_into(); + Ok(result) }) } ExportMethod::JsonConfig => { diff --git a/rust/perspective-viewer/src/rust/model/export_method.rs b/rust/perspective-viewer/src/rust/model/export_method.rs index 7c26de293a..f24698efa5 100644 --- a/rust/perspective-viewer/src/rust/model/export_method.rs +++ b/rust/perspective-viewer/src/rust/model/export_method.rs @@ -7,9 +7,40 @@ // file. use crate::*; +use std::fmt::Display; use std::rc::Rc; use yew::prelude::*; +#[derive(Clone, Copy, PartialEq)] +pub enum MimeType { + TextPlain, + ImagePng, +} + +impl Default for MimeType { + fn default() -> Self { + MimeType::TextPlain + } +} + +impl From for JsValue { + fn from(x: MimeType) -> JsValue { + JsValue::from(format!("{}", x)) + } +} + +impl Display for MimeType { + fn fmt( + &self, + fmt: &mut std::fmt::Formatter<'_>, + ) -> std::result::Result<(), std::fmt::Error> { + fmt.write_str(match self { + MimeType::TextPlain => "text/plain", + MimeType::ImagePng => "image/png", + }) + } +} + #[derive(Clone, Copy, PartialEq)] pub enum ExportMethod { Csv, @@ -23,6 +54,37 @@ pub enum ExportMethod { JsonConfig, } +impl ExportMethod { + pub fn to_filename(&self) -> &'static str { + match self { + ExportMethod::Csv => ".csv", + ExportMethod::CsvAll => ".all.csv", + ExportMethod::Json => ".json", + ExportMethod::JsonAll => ".all.json", + ExportMethod::Html => ".html", + ExportMethod::Png => ".png", + ExportMethod::Arrow => ".arrow", + ExportMethod::ArrowAll => ".all.arrow", + ExportMethod::JsonConfig => ".config.json", + } + } + + pub fn mimetype(&self) -> MimeType { + match self { + ExportMethod::Png => MimeType::ImagePng, + _ => MimeType::TextPlain, + } + } +} + +impl From for Html { + fn from(x: ExportMethod) -> Html { + html! { + { x.to_filename() } + } + } +} + impl ExportMethod { pub fn new_file(&self, x: &str) -> ExportFile { ExportFile { @@ -40,17 +102,7 @@ pub struct ExportFile { impl ExportFile { pub fn to_filename(&self) -> String { - match self.method { - ExportMethod::Csv => format!("{}.csv", self.name), - ExportMethod::CsvAll => format!("{}.all.csv", self.name), - ExportMethod::Json => format!("{}.json", self.name), - ExportMethod::JsonAll => format!("{}.all.json", self.name), - ExportMethod::Html => format!("{}.html", self.name), - ExportMethod::Png => format!("{}.png", self.name), - ExportMethod::Arrow => format!("{}.arrow", self.name), - ExportMethod::ArrowAll => format!("{}.all.arrow", self.name), - ExportMethod::JsonConfig => format!("{}.config.json", self.name), - } + format!("{}{}", self.name, self.method.to_filename()) } } @@ -63,7 +115,7 @@ impl From for Html { }; html_template! { - { x.to_filename() } + { x.name }{ x.method } } } } diff --git a/rust/perspective-viewer/src/rust/session.rs b/rust/perspective-viewer/src/rust/session.rs index e7c6f61096..064b7519ac 100644 --- a/rust/perspective-viewer/src/rust/session.rs +++ b/rust/perspective-viewer/src/rust/session.rs @@ -6,7 +6,6 @@ // of the Apache License 2.0. The full license can be found in the LICENSE // file. -mod copy; mod metadata; mod view; mod view_subscription; @@ -23,8 +22,6 @@ use crate::js::plugin::*; use crate::utils::*; use crate::*; -use copy::*; - use futures::channel::oneshot::*; use itertools::Itertools; use js_intern::*; @@ -37,7 +34,6 @@ use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use wasm_bindgen_futures::future_to_promise; - use yew::prelude::*; /// The `Session` struct is the principal interface to the Perspective engine, the @@ -441,27 +437,6 @@ impl Session { } } - pub async fn copy_to_clipboard(self, flat: bool) -> Result<(), JsValue> { - if flat { - let table = self.borrow().table.clone(); - if let Some(table) = table { - copy_flat(&table).await?; - } - } else { - let view = self - .borrow() - .view_sub - .as_ref() - .map(|x| x.get_view().clone()); - - if let Some(view) = view { - copy(&view).await?; - } - }; - - Ok(()) - } - pub async fn get_table_arrow(&self) -> Result, JsValue> { let table = self.borrow().table.clone().into_jserror()?; let view = table.view(&js_object!().unchecked_into()).await?; @@ -484,26 +459,43 @@ impl Session { }) } - pub async fn arrow_as_jsvalue(self, flat: bool) -> Result { + pub async fn arrow_as_jsvalue(self, flat: bool) -> Result { let view = self.flat_as_jsvalue(flat).await?; let arrow = view.to_arrow().await.unwrap(); - Ok(js_sys::Uint8Array::new(&arrow).into()) + let array = [js_sys::Uint8Array::new(&arrow)] + .iter() + .collect::(); + let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; + Ok(blob) } - pub async fn json_as_jsvalue(self, flat: bool) -> Result { + pub async fn json_as_jsvalue(self, flat: bool) -> Result { let view = self.flat_as_jsvalue(flat).await?; let json = view.to_columns().await.unwrap(); - Ok(js_sys::JSON::stringify(&json)?.into()) + let array = [js_sys::JSON::stringify(&json)?] + .iter() + .collect::(); + let mut options = web_sys::BlobPropertyBag::new(); + options.type_("text/plain"); + let blob = web_sys::Blob::new_with_str_sequence_and_options(&array, &options)?; + Ok(blob) } - pub async fn csv_as_jsvalue(&self, flat: bool) -> Result { + pub async fn csv_as_jsvalue(&self, flat: bool) -> Result { let view = self.flat_as_jsvalue(flat).await?; let csv_fut = view.to_csv(js_object!("formatted", true)); let csv = csv_fut.await.unwrap(); let csv_str = csv.as_string().unwrap(); let bytes = csv_str.as_bytes(); let value = unsafe { js_sys::Uint8Array::view(bytes) }; - Ok(value.unchecked_into()) + let mut options = web_sys::BlobPropertyBag::new(); + options.type_("text/plain"); + let value = web_sys::Blob::new_with_u8_array_sequence_and_options( + &[value].iter().collect::(), + &options, + )?; + + Ok(value) } pub fn get_view(&self) -> Option { diff --git a/rust/perspective-viewer/src/rust/session/copy.rs b/rust/perspective-viewer/src/rust/session/copy.rs deleted file mode 100644 index 181c3bb00d..0000000000 --- a/rust/perspective-viewer/src/rust/session/copy.rs +++ /dev/null @@ -1,69 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// -// Copyright (c) 2018, the Perspective Authors. -// -// This file is part of the Perspective library, distributed under the terms -// of the Apache License 2.0. The full license can be found in the LICENSE -// file. - -use crate::config::*; -use crate::js::perspective::*; -use crate::*; - -use super::view::*; - -use js_sys::JsString; -use std::cell::RefCell; -use std::rc::Rc; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; - -/// Copy a flat (unpivoted with all columns) CSV to the clipboard. -pub async fn copy_flat(table: &JsPerspectiveTable) -> Result<(), JsValue> { - let csv_ref: Rc>> = Rc::new(RefCell::new(None)); - poll(0, csv_ref.clone())?; - let view = table.view(&ViewConfig::default().as_jsvalue()?).await?; - let csv = copy_async(&view).await?; - view.delete().await?; - *csv_ref.borrow_mut() = Some(csv.as_string().unwrap()); - Ok(()) -} - -/// Copy a `JsPerspectiveView` to the clipboard as a CSV. -pub async fn copy(view: &View) -> Result<(), JsValue> { - let csv_ref: Rc>> = Rc::new(RefCell::new(None)); - poll(0, csv_ref.clone())?; - let csv = copy_async(view).await?; - *csv_ref.borrow_mut() = Some(csv.as_string().unwrap()); - Ok(()) -} - -/// This method must be called from an event handler, subject to the browser's -/// restrictions on clipboard access. See -/// [ws](https://www.w3.org/TR/clipboard-apis/#allow-read-clipboard). -fn poll(count: u32, csv_ref: Rc>>) -> Result<(), JsValue> { - if let Some(csv) = csv_ref.borrow().as_ref() { - let _promise = web_sys::window() - .unwrap() - .navigator() - .clipboard() - .write_text(csv); - } else { - clone!(csv_ref); - let f: js_sys::Function = - Closure::once(Box::new(move || poll(count + 1, csv_ref))) - .into_js_value() - .unchecked_into(); - - web_sys::window() - .unwrap() - .set_timeout_with_callback_and_timeout_and_arguments_0(&f, 50)?; - } - Ok(()) -} - -/// Copy a CSV, but not a `Promise`. Used to implement the public methods. -async fn copy_async(view: &JsPerspectiveView) -> Result { - let csv = view.to_csv(js_object!("formatted", true)); - Ok(csv.await.unwrap()) -} diff --git a/rust/perspective-viewer/src/rust/utils/clipboard.rs b/rust/perspective-viewer/src/rust/utils/clipboard.rs new file mode 100644 index 0000000000..19f426f763 --- /dev/null +++ b/rust/perspective-viewer/src/rust/utils/clipboard.rs @@ -0,0 +1,64 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use crate::js::clipboard_item::*; +use crate::model::*; +use crate::*; + +use std::cell::RefCell; +use std::future::Future; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +/// Copy a `JsPerspectiveView` to the clipboard as a CSV. +pub fn copy_to_clipboard( + view: impl Future>, + mimetype: MimeType, +) -> impl Future> { + let js_ref: Rc>> = Rc::new(RefCell::new(None)); + poll(0, mimetype, js_ref.clone()).unwrap(); + async move { + let js_val = view.await?; + *js_ref.borrow_mut() = Some(js_val); + Ok(()) + } +} + +/// This method must be called from an event handler, subject to the browser's +/// restrictions on clipboard access. See +/// [ws](https://www.w3.org/TR/clipboard-apis/#allow-read-clipboard). +fn poll( + count: u32, + mimetype: MimeType, + js_ref: Rc>>, +) -> Result<(), JsValue> { + if let Some(js_val) = js_ref.borrow().as_ref() { + let options = js_sys::Object::new(); + js_sys::Reflect::set(&options, &mimetype.into(), &js_val); + let item = ClipboardItem::new(&options); + let items = [item].iter().collect::(); + let _promise = web_sys::window() + .unwrap() + .navigator() + .clipboard() + .write(&items.into()); + } else { + clone!(js_ref); + let f: js_sys::Function = + Closure::once(Box::new(move || poll(count + 1, mimetype, js_ref))) + .into_js_value() + .unchecked_into(); + + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0(&f, 50)?; + } + + Ok(()) +} diff --git a/rust/perspective-viewer/src/rust/utils/download.rs b/rust/perspective-viewer/src/rust/utils/download.rs index e284b05535..5e5d27145c 100644 --- a/rust/perspective-viewer/src/rust/utils/download.rs +++ b/rust/perspective-viewer/src/rust/utils/download.rs @@ -9,14 +9,12 @@ use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -pub fn download(name: &str, value: &JsValue) -> Result<(), JsValue> { +pub fn download(name: &str, value: &web_sys::Blob) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let element: web_sys::HtmlElement = document.create_element("a")?.unchecked_into(); let blob_url = { - let array = [value].iter().collect::(); - let blob = web_sys::Blob::new_with_u8_array_sequence(&array)?; - web_sys::Url::create_object_url_with_blob(&blob)? + web_sys::Url::create_object_url_with_blob(&value)? }; element.set_attribute("download", name)?; diff --git a/rust/perspective-viewer/src/rust/utils/mod.rs b/rust/perspective-viewer/src/rust/utils/mod.rs index 84dd99e01f..dc2a5876a5 100644 --- a/rust/perspective-viewer/src/rust/utils/mod.rs +++ b/rust/perspective-viewer/src/rust/utils/mod.rs @@ -7,6 +7,7 @@ // file. mod async_callback; +mod clipboard; mod closure; mod datetime; mod debounce; @@ -23,6 +24,7 @@ mod weak_scope; mod tests; pub use self::async_callback::*; +pub use self::clipboard::*; pub use self::closure::*; pub use self::datetime::*; pub use self::debounce::*; diff --git a/rust/perspective-viewer/src/themes/material-dark.less b/rust/perspective-viewer/src/themes/material-dark.less index 2eb37c7d03..c8fa1d0640 100644 --- a/rust/perspective-viewer/src/themes/material-dark.less +++ b/rust/perspective-viewer/src/themes/material-dark.less @@ -25,6 +25,9 @@ perspective-viewer[theme="Material Dark"], .perspective-viewer-material-dark--d3fc(); } +perspective-copy-dropdown[theme="Material Dark"], +perspective-export-dropdown[theme="Material Dark"], +perspective-filter-dropdown[theme="Material Dark"], perspective-number-column-style[theme="Material Dark"], perspective-string-column-style[theme="Material Dark"], perspective-expression-editor[theme="Material Dark"], diff --git a/rust/perspective-viewer/src/themes/material.less b/rust/perspective-viewer/src/themes/material.less index fa2f84dbc1..d53a7f6b46 100644 --- a/rust/perspective-viewer/src/themes/material.less +++ b/rust/perspective-viewer/src/themes/material.less @@ -26,6 +26,9 @@ perspective-viewer[theme="Material Light"], .perspective-viewer-material--datagrid(); } +perspective-copy-dropdown[theme="Material Light"], +perspective-export-dropdown[theme="Material Light"], +perspective-filter-dropdown[theme="Material Light"], perspective-number-column-style[theme="Material Light"], perspective-string-column-style[theme="Material Light"], perspective-expression-editor[theme="Material Light"], diff --git a/rust/perspective-viewer/src/themes/monokai.less b/rust/perspective-viewer/src/themes/monokai.less index d75fa28a43..f0c0d4c575 100644 --- a/rust/perspective-viewer/src/themes/monokai.less +++ b/rust/perspective-viewer/src/themes/monokai.less @@ -23,6 +23,9 @@ perspective-viewer[theme="Monokai"], .perspective-viewer-monokai--d3fc(); } +perspective-copy-dropdown[theme="Monokai"], +perspective-export-dropdown[theme="Monokai"], +perspective-filter-dropdown[theme="Monokai"], perspective-number-column-style[theme="Monokai"], perspective-string-column-style[theme="Monokai"], perspective-expression-editor[theme="Monokai"], diff --git a/rust/perspective-viewer/src/themes/solarized-dark.less b/rust/perspective-viewer/src/themes/solarized-dark.less index 2ece6b672f..9c88667bc0 100644 --- a/rust/perspective-viewer/src/themes/solarized-dark.less +++ b/rust/perspective-viewer/src/themes/solarized-dark.less @@ -25,6 +25,9 @@ perspective-viewer[theme="Solarized Dark"], .perspective-viewer-solarized-dark--d3fc(); } +perspective-copy-dropdown[theme="Solarized Dark"], +perspective-export-dropdown[theme="Solarized Dark"], +perspective-filter-dropdown[theme="Solarized Dark"], perspective-number-column-style[theme="Solarized Dark"], perspective-string-column-style[theme="Solarized Dark"], perspective-expression-editor[theme="Solarized Dark"], diff --git a/rust/perspective-viewer/src/themes/solarized.less b/rust/perspective-viewer/src/themes/solarized.less index bcad23a23e..f378919c9f 100644 --- a/rust/perspective-viewer/src/themes/solarized.less +++ b/rust/perspective-viewer/src/themes/solarized.less @@ -23,6 +23,9 @@ perspective-viewer[theme="Solarized"], .perspective-viewer-solarized--d3fc(); } +perspective-copy-dropdown[theme="Solarized"], +perspective-export-dropdown[theme="Solarized"], +perspective-filter-dropdown[theme="Solarized"], perspective-number-column-style[theme="Solarized"], perspective-string-column-style[theme="Solarized"], perspective-expression-editor[theme="Solarized"], diff --git a/rust/perspective-viewer/src/themes/vaporwave.less b/rust/perspective-viewer/src/themes/vaporwave.less index a636f3bd2e..48aedc4b20 100644 --- a/rust/perspective-viewer/src/themes/vaporwave.less +++ b/rust/perspective-viewer/src/themes/vaporwave.less @@ -27,6 +27,9 @@ perspective-viewer[theme="Vaporwave"], .perspective-viewer-vaporwave--d3fc(); } +perspective-copy-dropdown[theme="Vaporwave"], +perspective-export-dropdown[theme="Vaporwave"], +perspective-filter-dropdown[theme="Vaporwave"], perspective-number-column-style[theme="Vaporwave"], perspective-string-column-style[theme="Vaporwave"], perspective-expression-editor[theme="Vaporwave"], From e9ca6963a04b84b7ba66cc7fab7bca54c12382be Mon Sep 17 00:00:00 2001 From: Zeme Olotu Date: Tue, 8 Mar 2022 16:55:56 -0500 Subject: [PATCH 14/31] Add main field to package json --- packages/perspective-viewer-d3fc/package.json | 1 + packages/perspective-viewer-datagrid/package.json | 1 + packages/perspective-workspace/package.json | 1 + packages/perspective/package.json | 1 + rust/perspective-viewer/package.json | 1 + 5 files changed, 5 insertions(+) diff --git a/packages/perspective-viewer-d3fc/package.json b/packages/perspective-viewer-d3fc/package.json index 8cfd02438c..e943c17308 100644 --- a/packages/perspective-viewer-d3fc/package.json +++ b/packages/perspective-viewer-d3fc/package.json @@ -4,6 +4,7 @@ "description": "Perspective.js D3FC Plugin", "unpkg": "./dist/umd/perspective-viewer-d3fc.js", "jsdelivr": "./dist/umd/perspective-viewer-d3fc.js", + "main": "./dist/umd/perspective-viewer-d3fc.js", "exports": { ".": { "require": "./dist/umd/perspective-viewer-d3fc.js", diff --git a/packages/perspective-viewer-datagrid/package.json b/packages/perspective-viewer-datagrid/package.json index 3455a6342a..e4eb09c46e 100644 --- a/packages/perspective-viewer-datagrid/package.json +++ b/packages/perspective-viewer-datagrid/package.json @@ -3,6 +3,7 @@ "version": "1.3.3", "description": "Perspective datagrid plugin based on `regular-table`", "unpkg": "dist/umd/perspective-viewer-datagrid.js", + "main": "dist/umd/perspective-viewer-datagrid.js", "jsdelivr": "dist/umd/perspective-viewer-datagrid.js", "exports": { ".": { diff --git a/packages/perspective-workspace/package.json b/packages/perspective-workspace/package.json index 32c822d494..8029adf663 100644 --- a/packages/perspective-workspace/package.json +++ b/packages/perspective-workspace/package.json @@ -20,6 +20,7 @@ }, "unpkg": "./dist/umd/perspective-workspace.js", "jsdelivr": "./dist/umd/perspective-workspace.js", + "main": "./dist/umd/perspective-workspace.js", "scripts": { "bench": "npm-run-all bench:build bench:run", "bench:build": "echo \"No Benchmarks\"", diff --git a/packages/perspective/package.json b/packages/perspective/package.json index cd57573de9..8fdc95f80c 100644 --- a/packages/perspective/package.json +++ b/packages/perspective/package.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "unpkg": "dist/umd/perspective.js", "jsdelivr": "dist/umd/perspective.js", + "main": "dist/umd/perspective.js", "exports": { ".": { "node": "./dist/cjs/perspective.node.js", diff --git a/rust/perspective-viewer/package.json b/rust/perspective-viewer/package.json index d0a56e6807..17d3078307 100644 --- a/rust/perspective-viewer/package.json +++ b/rust/perspective-viewer/package.json @@ -8,6 +8,7 @@ }, "license": "Apache-2.0", "unpkg": "dist/umd/perspective-viewer.js", + "main": "dist/umd/perspective-viewer.js", "jsdelivr": "dist/umd/perspective-viewer.js", "exports": { ".": { From e1af67579c263a91fcdc60d312332f4566664296 Mon Sep 17 00:00:00 2001 From: zemeolotu Date: Tue, 8 Mar 2022 18:06:30 -0500 Subject: [PATCH 15/31] export datagrid plugin customElement and typings --- .../perspective-viewer-datagrid/index.d.ts | 36 ++ .../perspective-viewer-datagrid/package.json | 1 + .../src/js/plugin.js | 545 +++++++++--------- 3 files changed, 307 insertions(+), 275 deletions(-) create mode 100644 packages/perspective-viewer-datagrid/index.d.ts diff --git a/packages/perspective-viewer-datagrid/index.d.ts b/packages/perspective-viewer-datagrid/index.d.ts new file mode 100644 index 0000000000..8f84812748 --- /dev/null +++ b/packages/perspective-viewer-datagrid/index.d.ts @@ -0,0 +1,36 @@ +import {View} from "@finos/perspective"; +import {RegularTableElement} from "regular-table"; + +declare class PerspectiveViewerDatagridPluginElement extends HTMLElement { + public readonly datagrid: RegularTableElement; + + // private methods + private _toggle_edit_mode(force?: boolean): void; + private _toggle_scroll_lock(force?: boolean): void; + private _restore_column_size_overrides(old_sizes: any, cache: boolean); + private _save_column_size_overrides(): any; + + // getters accessors + public get name(): string; + public get select_mode(): string; + public get min_config_columns(): string; + public get config_column_names(): string; + + // customElements methods + protected connectedCallback(): void; + protected disconnectedCallback(): void; + + // view related methods + public activate(view: View): Promise; + public draw(view: View): Promise; + public update(view: View): Promise; + public restyle(view: View): Promise; + + // other public methods + public resize(): Promise; + public clear(): Promise; + public delete(): void; + public save(): Promise; + public restore(token: any): Promise; + public restore(token: any): Promise; +} diff --git a/packages/perspective-viewer-datagrid/package.json b/packages/perspective-viewer-datagrid/package.json index 3455a6342a..aa29c781ff 100644 --- a/packages/perspective-viewer-datagrid/package.json +++ b/packages/perspective-viewer-datagrid/package.json @@ -15,6 +15,7 @@ "files": [ "dist/**/*" ], + "types": "index.d.ts", "scripts": { "bench": "npm-run-all bench:build bench:run", "bench:build": ":", diff --git a/packages/perspective-viewer-datagrid/src/js/plugin.js b/packages/perspective-viewer-datagrid/src/js/plugin.js index 163043b06b..9400e44ad5 100644 --- a/packages/perspective-viewer-datagrid/src/js/plugin.js +++ b/packages/perspective-viewer-datagrid/src/js/plugin.js @@ -23,344 +23,339 @@ import {configureEditable} from "./editing.js"; import {configureSortable} from "./sorting.js"; import {PLUGIN_SYMBOL} from "./plugin_menu.js"; -customElements.define( - "perspective-viewer-datagrid", - class extends HTMLElement { - constructor() { - super(); - this.datagrid = document.createElement("regular-table"); - this.datagrid.formatters = formatters; - this._is_scroll_lock = true; - } +export class PerspectiveViewerDatagridPluginElement extends HTMLElement { + constructor() { + super(); + this.datagrid = document.createElement("regular-table"); + this.datagrid.formatters = formatters; + this._is_scroll_lock = true; + } - connectedCallback() { - if (!this._toolbar) { - this._toolbar = document.createElement( - "perspective-viewer-datagrid-toolbar" - ); - - this._toolbar.setAttribute("slot", "plugin-settings"); - this._toolbar.attachShadow({mode: "open"}); - this._toolbar.shadowRoot.innerHTML = ` - -
- Align Scroll - Read Only -
- `; - - this._scroll_lock = - this._toolbar.shadowRoot.querySelector("#scroll_lock"); - this._scroll_lock.addEventListener("click", () => - this._toggle_scroll_lock() - ); - - this._edit_mode = - this._toolbar.shadowRoot.querySelector("#edit_mode"); - this._edit_mode.addEventListener("click", () => { - this._toggle_edit_mode(); - this.datagrid.draw(); - }); - } + connectedCallback() { + if (!this._toolbar) { + this._toolbar = document.createElement( + "perspective-viewer-datagrid-toolbar" + ); - this.parentElement.appendChild(this._toolbar); - } + this._toolbar.setAttribute("slot", "plugin-settings"); + this._toolbar.attachShadow({mode: "open"}); + this._toolbar.shadowRoot.innerHTML = ` + +
+ Align Scroll + Read Only +
+ `; + + this._scroll_lock = + this._toolbar.shadowRoot.querySelector("#scroll_lock"); + this._scroll_lock.addEventListener("click", () => + this._toggle_scroll_lock() + ); - disconnectedCallback() { - this._toolbar.parentElement.removeChild(this._toolbar); + this._edit_mode = + this._toolbar.shadowRoot.querySelector("#edit_mode"); + this._edit_mode.addEventListener("click", () => { + this._toggle_edit_mode(); + this.datagrid.draw(); + }); } - _toggle_edit_mode(force = undefined) { - if (typeof force === "undefined") { - force = !this._is_edit_mode; - } - - this._is_edit_mode = force; - this.classList.toggle("editable", force); - this._edit_mode.classList.toggle("editable", force); - if (force) { - this._edit_mode.children[0].textContent = "Editable"; - } else { - this._edit_mode.children[0].textContent = "Read Only"; - } - } + this.parentElement.appendChild(this._toolbar); + } - _toggle_scroll_lock(force = undefined) { - if (typeof force === "undefined") { - force = !this._is_scroll_lock; - } + disconnectedCallback() { + this._toolbar.parentElement.removeChild(this._toolbar); + } - this._is_scroll_lock = force; - this.classList.toggle("sub-cell-scroll-enabled", !force); - this._scroll_lock.classList.toggle("lock-scroll", force); - if (!force) { - this._scroll_lock.children[0].textContent = "Free Scroll"; - } else { - this._scroll_lock.children[0].textContent = "Align Scroll"; - } + _toggle_edit_mode(force = undefined) { + if (typeof force === "undefined") { + force = !this._is_edit_mode; } - async activate(view) { - let viewer = this.parentElement; - let table = await viewer.getTable(true); - if (!this._initialized) { - this.innerHTML = ""; - this.appendChild(this.datagrid); - this.model = await createModel(this.datagrid, table, view); - configureRegularTable(this.datagrid, this.model); - await configureRowSelectable.call( - this.model, - this.datagrid, - viewer - ); - await configureClick.call(this.model, this.datagrid, viewer); - await configureEditable.call(this.model, this.datagrid, viewer); - await configureSortable.call(this.model, this.datagrid, viewer); - this._initialized = true; - } else { - await createModel(this.datagrid, table, view, this.model); - } + this._is_edit_mode = force; + this.classList.toggle("editable", force); + this._edit_mode.classList.toggle("editable", force); + if (force) { + this._edit_mode.children[0].textContent = "Editable"; + } else { + this._edit_mode.children[0].textContent = "Read Only"; } + } - get name() { - return "Datagrid"; + _toggle_scroll_lock(force = undefined) { + if (typeof force === "undefined") { + force = !this._is_scroll_lock; } - get select_mode() { - return "toggle"; + this._is_scroll_lock = force; + this.classList.toggle("sub-cell-scroll-enabled", !force); + this._scroll_lock.classList.toggle("lock-scroll", force); + if (!force) { + this._scroll_lock.children[0].textContent = "Free Scroll"; + } else { + this._scroll_lock.children[0].textContent = "Align Scroll"; } + } - get min_config_columns() { - return undefined; + async activate(view) { + let viewer = this.parentElement; + let table = await viewer.getTable(true); + if (!this._initialized) { + this.innerHTML = ""; + this.appendChild(this.datagrid); + this.model = await createModel(this.datagrid, table, view); + configureRegularTable(this.datagrid, this.model); + await configureRowSelectable.call( + this.model, + this.datagrid, + viewer + ); + await configureClick.call(this.model, this.datagrid, viewer); + await configureEditable.call(this.model, this.datagrid, viewer); + await configureSortable.call(this.model, this.datagrid, viewer); + this._initialized = true; + } else { + await createModel(this.datagrid, table, view, this.model); } + } - get config_column_names() { - return undefined; - } + get name() { + return "Datagrid"; + } - async draw(view) { - if (!this.isConnected) { - return; - } + get select_mode() { + return "toggle"; + } - const old_sizes = this._save_column_size_overrides(); - await this.activate(view); - let viewer = this.parentElement; - const draw = this.datagrid.draw({invalid_columns: true}); - if (!this.model._preserve_focus_state) { - this.datagrid.scrollTop = 0; - this.datagrid.scrollLeft = 0; - deselect(this.datagrid, viewer); - this.datagrid._resetAutoSize(); - } else { - this.model._preserve_focus_state = false; - } + get min_config_columns() { + return undefined; + } - this._restore_column_size_overrides(old_sizes); - await draw; + get config_column_names() { + return undefined; + } - this._toolbar.classList.toggle( - "aggregated", - this.model._config.group_by.length > 0 || - this.model._config.split_by.length > 0 - ); + async draw(view) { + if (!this.isConnected) { + return; } - async update(view) { - this.model._num_rows = await view.num_rows(); - await this.datagrid.draw(); + const old_sizes = this._save_column_size_overrides(); + await this.activate(view); + let viewer = this.parentElement; + const draw = this.datagrid.draw({invalid_columns: true}); + if (!this.model._preserve_focus_state) { + this.datagrid.scrollTop = 0; + this.datagrid.scrollLeft = 0; + deselect(this.datagrid, viewer); + this.datagrid._resetAutoSize(); + } else { + this.model._preserve_focus_state = false; } - async resize() { - if (this._initialized) { - await this.datagrid.draw(); - } - } + this._restore_column_size_overrides(old_sizes); + await draw; - async clear() { - this.datagrid._resetAutoSize(); - this.datagrid.clear(); + this._toolbar.classList.toggle( + "aggregated", + this.model._config.group_by.length > 0 || + this.model._config.split_by.length > 0 + ); + } + + async update(view) { + this.model._num_rows = await view.num_rows(); + await this.datagrid.draw(); + } + + async resize() { + if (this._initialized) { + await this.datagrid.draw(); } + } - save() { - if (this.datagrid) { - const datagrid = this.datagrid; - const token = { - columns: {}, - scroll_lock: !!this._is_scroll_lock, - editable: !!this._is_edit_mode, - }; - - for (const col of Object.keys(datagrid[PLUGIN_SYMBOL] || {})) { - const config = Object.assign( - {}, - datagrid[PLUGIN_SYMBOL][col] - ); - if (config?.pos_color) { - config.pos_color = config.pos_color[0]; - config.neg_color = config.neg_color[0]; - } + async clear() { + this.datagrid._resetAutoSize(); + this.datagrid.clear(); + } - if (config?.color) { - config.color = config.color[0]; - } + save() { + if (this.datagrid) { + const datagrid = this.datagrid; + const token = { + columns: {}, + scroll_lock: !!this._is_scroll_lock, + editable: !!this._is_edit_mode, + }; + + for (const col of Object.keys(datagrid[PLUGIN_SYMBOL] || {})) { + const config = Object.assign({}, datagrid[PLUGIN_SYMBOL][col]); + if (config?.pos_color) { + config.pos_color = config.pos_color[0]; + config.neg_color = config.neg_color[0]; + } - token.columns[col] = config; + if (config?.color) { + config.color = config.color[0]; } - const column_size_overrides = - this._save_column_size_overrides(); + token.columns[col] = config; + } - for (const col of Object.keys(column_size_overrides || {})) { - if (!token.columns[col]) { - token.columns[col] = {}; - } + const column_size_overrides = this._save_column_size_overrides(); - token.columns[col].column_size_override = - column_size_overrides[col]; + for (const col of Object.keys(column_size_overrides || {})) { + if (!token.columns[col]) { + token.columns[col] = {}; } - return JSON.parse(JSON.stringify(token)); + token.columns[col].column_size_override = + column_size_overrides[col]; } - return {}; + + return JSON.parse(JSON.stringify(token)); } + return {}; + } - restore(token) { - token = JSON.parse(JSON.stringify(token)); - const overrides = {}; - if (token.columns) { - for (const col of Object.keys(token.columns)) { - const col_config = token.columns[col]; - if (col_config.column_size_override !== undefined) { - overrides[col] = col_config.column_size_override; - delete col_config["column_size_override"]; - } - - if (col_config?.pos_color) { - col_config.pos_color = create_color_record( - col_config.pos_color - ); - col_config.neg_color = create_color_record( - col_config.neg_color - ); - } - - if (col_config?.color) { - col_config.color = create_color_record( - col_config.color - ); - } - - if (Object.keys(col_config).length === 0) { - delete token.columns[col]; - } + restore(token) { + token = JSON.parse(JSON.stringify(token)); + const overrides = {}; + if (token.columns) { + for (const col of Object.keys(token.columns)) { + const col_config = token.columns[col]; + if (col_config.column_size_override !== undefined) { + overrides[col] = col_config.column_size_override; + delete col_config["column_size_override"]; } - } - if ("editable" in token) { - this._toggle_edit_mode(token.editable); - } + if (col_config?.pos_color) { + col_config.pos_color = create_color_record( + col_config.pos_color + ); + col_config.neg_color = create_color_record( + col_config.neg_color + ); + } - if ("scroll_lock" in token) { - this._toggle_scroll_lock(token.scroll_lock); - } + if (col_config?.color) { + col_config.color = create_color_record(col_config.color); + } - const datagrid = this.datagrid; - try { - datagrid._resetAutoSize(); - } catch (e) { - // Do nothing; this may fail if no auto size info has been read. - // TODO fix this regular-table API + if (Object.keys(col_config).length === 0) { + delete token.columns[col]; + } } + } - this._restore_column_size_overrides(overrides, true); - datagrid[PLUGIN_SYMBOL] = token.columns; + if ("editable" in token) { + this._toggle_edit_mode(token.editable); } - async restyle(view) { - this.draw(view); + if ("scroll_lock" in token) { + this._toggle_scroll_lock(token.scroll_lock); } - delete() { - if (this.datagrid.table_model) { - this.datagrid._resetAutoSize(); - } - this.datagrid.clear(); + const datagrid = this.datagrid; + try { + datagrid._resetAutoSize(); + } catch (e) { + // Do nothing; this may fail if no auto size info has been read. + // TODO fix this regular-table API } - // Private - - /** - * Extract the current user-overriden column widths from - * `regular-table`. This functiond depends on the internal - * implementation of `regular-table` and may break! - * - * @returns An Object-as-dictionary keyed by column_path string, and - * valued by the column's user-overridden pixel width. - */ - _save_column_size_overrides() { - if (!this._initialized) { - return []; - } + this._restore_column_size_overrides(overrides, true); + datagrid[PLUGIN_SYMBOL] = token.columns; + } - if (this._cached_column_sizes) { - const x = this._cached_column_sizes; - this._cached_column_sizes = undefined; - return x; - } + async restyle(view) { + this.draw(view); + } - const overrides = this.datagrid._column_sizes.override; - const {group_by, columns} = this.model._config; - const tree_header_offset = - group_by?.length > 0 ? group_by.length + 1 : 0; - - const old_sizes = {}; - for (const key of Object.keys(overrides)) { - if (overrides[key] !== undefined) { - const index = key - tree_header_offset; - if (index > -1) { - old_sizes[this.model._column_paths[index]] = - overrides[key]; - } - } - } + delete() { + if (this.datagrid.table_model) { + this.datagrid._resetAutoSize(); + } + this.datagrid.clear(); + } - return old_sizes; + // Private + + /** + * Extract the current user-overriden column widths from + * `regular-table`. This functiond depends on the internal + * implementation of `regular-table` and may break! + * + * @returns An Object-as-dictionary keyed by column_path string, and + * valued by the column's user-overridden pixel width. + */ + _save_column_size_overrides() { + if (!this._initialized) { + return []; } - /** - * Restore a saved column width override token. - * - * @param {*} token An object previously returned by a call to - * `_save_column_size_overrides()` - * @param {*} [cache=false] A flag indicating whether this value should - * be cached so a future `resetAutoSize()` call does not clear it. - * @returns - */ - _restore_column_size_overrides(old_sizes, cache = false) { - if (!this._initialized) { - return; - } + if (this._cached_column_sizes) { + const x = this._cached_column_sizes; + this._cached_column_sizes = undefined; + return x; + } - if (cache) { - this._cached_column_sizes = old_sizes; + const overrides = this.datagrid._column_sizes.override; + const {group_by, columns} = this.model._config; + const tree_header_offset = + group_by?.length > 0 ? group_by.length + 1 : 0; + + const old_sizes = {}; + for (const key of Object.keys(overrides)) { + if (overrides[key] !== undefined) { + const index = key - tree_header_offset; + if (index > -1) { + old_sizes[this.model._column_paths[index]] = overrides[key]; + } } + } + + return old_sizes; + } - const overrides = {}; - const {group_by, columns} = this.model._config; - const tree_header_offset = - group_by?.length > 0 ? group_by.length + 1 : 0; + /** + * Restore a saved column width override token. + * + * @param {*} token An object previously returned by a call to + * `_save_column_size_overrides()` + * @param {*} [cache=false] A flag indicating whether this value should + * be cached so a future `resetAutoSize()` call does not clear it. + * @returns + */ + _restore_column_size_overrides(old_sizes, cache = false) { + if (!this._initialized) { + return; + } - for (const key of Object.keys(old_sizes)) { - const index = this.model._column_paths.indexOf(key); - overrides[index + tree_header_offset] = old_sizes[key]; - } + if (cache) { + this._cached_column_sizes = old_sizes; + } + + const overrides = {}; + const {group_by, columns} = this.model._config; + const tree_header_offset = + group_by?.length > 0 ? group_by.length + 1 : 0; - this.datagrid._column_sizes.override = overrides; + for (const key of Object.keys(old_sizes)) { + const index = this.model._column_paths.indexOf(key); + overrides[index + tree_header_offset] = old_sizes[key]; } + + this.datagrid._column_sizes.override = overrides; } +} + +customElements.define( + "perspective-viewer-datagrid", + PerspectiveViewerDatagridPluginElement ); /** From c9a07ad6ed519ebc6a4d7f0899ff86bbe3dec8c5 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Tue, 8 Mar 2022 21:17:13 -0500 Subject: [PATCH 16/31] Fix tests --- .../test/js/integration/events.spec.js | 8 +++-- .../test/results/results.json | 36 +++++++++---------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js b/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js index 5d6fb899d9..688f9c3e00 100644 --- a/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js +++ b/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js @@ -37,7 +37,7 @@ utils.with_server({}, () => { // Await the table load await viewer.getTable(); - viewer.restore({ + await viewer.restore({ plugin: "Y Line", columns: ["Sales", "Profit"], }); @@ -55,7 +55,9 @@ utils.with_server({}, () => { await page.waitForFunction(() => document .querySelector("perspective-viewer-d3fc-yline") - .shadowRoot.querySelector(".y-label") + .shadowRoot.querySelector( + ".y-label .splitter-label" + ) ) ).asElement(); await axisLabel.click(axisLabel); @@ -85,7 +87,7 @@ utils.with_server({}, () => { // Await the table load await viewer.getTable(); - viewer.restore({ + await viewer.restore({ plugin: "Y Line", columns: ["Sales", "Profit"], }); diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index 46e29d45f0..dc7fcf61c3 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -1,5 +1,5 @@ { - "__GIT_COMMIT__": "9e69fd6a64d79303a423d5390869e1cc165d1fe6", + "__GIT_COMMIT__": "d776202ff728f808f1d07060e5ce73bb392402f1", "area_shows_a_grid_without_any_settings_applied": "3852532b8ee6abba3373a10d41bb4df2", "area_displays_visible_columns_": "919cc6f6c2a2f2ec13b90dfb60e1b7ef", "area_pivot_by_a_row": "7495976cfed69cfe9db2b3fdc6ef4707", @@ -91,19 +91,19 @@ "heatmap_filters_filters_by_a_numeric_column": "b9e271668af3836baf5a5d51226910a3", "heatmap_filters_filters_by_an_alpha_column": "b9e271668af3836baf5a5d51226910a3", "heatmap_filters_filters_with__in__comparator": "b9e271668af3836baf5a5d51226910a3", - "scatter_shows_a_grid_without_any_settings_applied": "afb75b167c5ea6be21c7b2121901a17f", + "scatter_shows_a_grid_without_any_settings_applied": "9cbb9843a44b830cf4a324706a7bd713", "scatter_displays_visible_columns_": "0519cc65006ebd80de93362817a7ce4f", - "scatter_pivot_by_a_row": "afb75b167c5ea6be21c7b2121901a17f", - "scatter_pivot_by_two_rows": "afb75b167c5ea6be21c7b2121901a17f", - "scatter_pivot_by_a_column": "afb75b167c5ea6be21c7b2121901a17f", - "scatter_pivot_by_a_row_and_a_column": "afb75b167c5ea6be21c7b2121901a17f", - "scatter_pivot_by_two_rows_and_two_columns": "afb75b167c5ea6be21c7b2121901a17f", - "scatter_sort_by_a_hidden_column": "afb75b167c5ea6be21c7b2121901a17f", - "scatter_sort_by_a_numeric_column": "afb75b167c5ea6be21c7b2121901a17f", - "scatter_sort_by_an_alpha_column": "3a0440b79d5a068fc3e2c48b3d15a01e", - "scatter_filters_filters_by_a_numeric_column": "adbc354df9b4f69db72160fcad7bde74", - "scatter_filters_filters_by_an_alpha_column": "cf64a7ffdc6f150d1f535cf08679f052", - "scatter_filters_filters_with__in__comparator": "16fbc02857ffd2df180274a19c51c4f2", + "scatter_pivot_by_a_row": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_pivot_by_two_rows": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_pivot_by_a_column": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_pivot_by_a_row_and_a_column": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_pivot_by_two_rows_and_two_columns": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_sort_by_a_hidden_column": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_sort_by_a_numeric_column": "9cbb9843a44b830cf4a324706a7bd713", + "scatter_sort_by_an_alpha_column": "01fa363fa5ef2e1aa66a77ed87e9225f", + "scatter_filters_filters_by_a_numeric_column": "37fd54838ea9dead5dc441bf1e62fbb9", + "scatter_filters_filters_by_an_alpha_column": "2b3728cca2c7518bb10382dd7ef0d4fd", + "scatter_filters_filters_with__in__comparator": "f44f7f144f9ee4827783a9b516f2a4e3", "yscatter_shows_a_grid_without_any_settings_applied": "006aa96e87e006a01a0349f0c20ea27a", "yscatter_displays_visible_columns_": "0bff95a36d9f0a0029599bb7adcc6af8", "yscatter_pivot_by_a_row": "fb6504a26b1b859619a180b27d35f73b", @@ -121,10 +121,10 @@ "sunburst_pivot_by_two_rows": "38fe70a8f88a3e0df185f8775c88317d", "sunburst_pivot_by_a_row_and_a_column": "fa789a00c03e04567bd45a8451e7a00a", "sunburst_pivot_by_two_rows_and_two_columns": "e03658ff241f45837b9d61227bf023a4", - "treemap_pivot_by_a_row": "d9e965d744906a2320148b73dc1fcb29", - "treemap_pivot_by_two_rows": "28390c90695be1b38bd2a05a984dd242", - "treemap_pivot_by_a_row_and_a_column": "a36877ce32fed64d1ae16807de27e9de", - "treemap_pivot_by_two_rows_and_two_columns": "92203a8d4778c041836361b56ff39174", + "treemap_pivot_by_a_row": "7322fa73924e688662d36558a33ddb99", + "treemap_pivot_by_two_rows": "ce30c9196e830d54d4960d4f9a53ad32", + "treemap_pivot_by_a_row_and_a_column": "a9ee5c25d23fff12e57b7755ca310cb5", + "treemap_pivot_by_two_rows_and_two_columns": "fe5616df5b45c5b0b2f86489f7a1dc22", "treemap_shows_a_grid_without_any_settings_applied": "10d1208b485425756fcc932229386b02", "treemap_displays_visible_columns_": "10d1208b485425756fcc932229386b02", "treemap_pivot_by_a_column": "10d1208b485425756fcc932229386b02", @@ -144,6 +144,6 @@ "sunburst_filters_filters_by_an_alpha_column": "10d1208b485425756fcc932229386b02", "sunburst_filters_filters_with__in__comparator": "10d1208b485425756fcc932229386b02", "bar_rendering_bugs_correctly_render_when_a_bar_chart_has_non_equidistant_times_on_a_datetime_axis": "9e7617a454d83e328aaa3d25c8145a0e", - "events_perspective-config-update_event_is_fired_when_series_axis_is_changed": "c07cb12b00fe628aba8751721f8f5498", + "events_perspective-config-update_event_is_fired_when_series_axis_is_changed": "6bf8e592d3aa69e0c141a9caca01bae2", "events_perspective-config-update_event_is_fired_when_legend_position_is_changed": "c07cb12b00fe628aba8751721f8f5498" } \ No newline at end of file From 9c553d1b729180e7b81d05f7ac00a1b3ad0067dd Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Tue, 8 Mar 2022 21:17:31 -0500 Subject: [PATCH 17/31] Fire legend event only on `mouseup` --- .../src/js/legend/styling/draggableComponent.js | 2 ++ .../perspective-viewer-d3fc/test/js/integration/events.spec.js | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js index 3e5c5c30fc..3e6a188722 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js @@ -43,7 +43,9 @@ export function draggableComponent() { pinned = isNodeInTopRight(node) ? pinNodeToTopRight(node) : unpinNodeFromTopRight(node, pinned); + }); + drag.on("end", function (event) { viewer?.dispatchEvent(new Event("perspective-config-update")); }); diff --git a/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js b/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js index 688f9c3e00..32cb8bb703 100644 --- a/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js +++ b/packages/perspective-viewer-d3fc/test/js/integration/events.spec.js @@ -125,6 +125,7 @@ utils.with_server({}, () => { await page.mouse.move(start.x, start.y); await page.mouse.down(); await page.mouse.move(target.x, target.y); + await page.mouse.up(); const count = await page.evaluate(async (viewer) => { // Await the plugin rendering From d3aaf6e7d6b0836948668ab773fc64cfd5f7a6d5 Mon Sep 17 00:00:00 2001 From: zemeolotu Date: Wed, 9 Mar 2022 14:17:35 -0500 Subject: [PATCH 18/31] Add definitiion for custom elements registry --- packages/perspective-viewer-datagrid/index.d.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/perspective-viewer-datagrid/index.d.ts b/packages/perspective-viewer-datagrid/index.d.ts index 8f84812748..65eb2d0e04 100644 --- a/packages/perspective-viewer-datagrid/index.d.ts +++ b/packages/perspective-viewer-datagrid/index.d.ts @@ -1,7 +1,19 @@ import {View} from "@finos/perspective"; import {RegularTableElement} from "regular-table"; -declare class PerspectiveViewerDatagridPluginElement extends HTMLElement { +declare global { + interface CustomElementRegistry { + get( + tagName: "perspective-viewer-datagrid" + ): PerspectiveViewerDatagridPluginElement; + + whenDefined( + tagName: "perspective-viewer-datagrid" + ): Promise; + } +} + +export declare class PerspectiveViewerDatagridPluginElement extends HTMLElement { public readonly datagrid: RegularTableElement; // private methods From 219cf7d5ce19c641cd0b040a4351de026e55d3bd Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 19:44:23 -0500 Subject: [PATCH 19/31] Fix console error on legend unpin --- .../src/js/legend/styling/draggableComponent.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js index 3e6a188722..fa6262b196 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js @@ -46,6 +46,7 @@ export function draggableComponent() { }); drag.on("end", function (event) { + d3.select(window).on(resizeForDraggingEvent, null); viewer?.dispatchEvent(new Event("perspective-config-update")); }); From b58309f02d72c52c40eeca503c2c2ad3d310bbe9 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 19:44:48 -0500 Subject: [PATCH 20/31] Fix zoom origin on high-DPI canvas charts --- .../src/js/zoom/zoomableChart.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js b/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js index 66a168e028..779c46aa4e 100644 --- a/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js +++ b/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js @@ -115,11 +115,22 @@ export default () => { bound = true; // add the zoom interaction on the enter selection const plotArea = sel.select(chartPlotArea); + const device_pixel_factor = canvas + ? window.devicePixelRatio + : 1; plotArea .on("measure.zoom-range", (event) => { - if (xCopy) xCopy.range([0, event.detail.width]); - if (yCopy) yCopy.range([0, event.detail.height]); + if (xCopy) + xCopy.range([ + 0, + event.detail.width / device_pixel_factor, + ]); + if (yCopy) + yCopy.range([ + 0, + event.detail.height / device_pixel_factor, + ]); if (settings.zoom) { const initialTransform = d3.zoomIdentity From 32cd2a69bd225aae6f2242003b63f04c47532018 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 19:45:07 -0500 Subject: [PATCH 21/31] Fix zoom controls styling to conform to theme --- packages/perspective-viewer-d3fc/src/less/chart.less | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 18d3ff4d1d..39d5040882 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -14,6 +14,7 @@ @sans-serif-fonts: Arial, sans-serif; :host { + user-select: none; .chart { position: absolute; box-sizing: border-box; @@ -407,14 +408,15 @@ & button { -webkit-appearance: none; - background: rgb(247, 247, 247); - border: 1px solid rgb(204, 204, 204); - padding: 10px; + background: var(--plugin--background, rgb(247, 247, 247)); + border: 1px solid var(--inactive--color, rgb(204, 204, 204)); + color: var(--d3fc-label--color, inherit); + font-size: 12px; + padding: 8px; opacity: 0.5; cursor: pointer; - &:hover { - background: rgb(230, 230, 230); + opacity: 1; } } } From 9417b135dd4f84565c30ba0dc7913d185eb1d19d Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 19:45:20 -0500 Subject: [PATCH 22/31] Add adaptive stroke-width for line charts --- .../src/js/series/lineSeries.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js b/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js index ef6cec4276..60cca85d49 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js @@ -12,10 +12,21 @@ import {withoutOpacity} from "./seriesColors.js"; export function lineSeries(settings, color) { let series = fc.seriesSvgLine(); + const estimated_size = + settings.data.length * + (settings.data?.length > 0 + ? Object.keys(settings.data[0]).length - + (settings.crossValues?.length > 0 ? 1 : 0) + : 0); + const stroke_width = Math.max( + 1, + Math.min(3, Math.floor(settings.size.width / estimated_size / 2)) + ); + series = series.decorate((selection) => { - selection.style("stroke", (d) => - withoutOpacity(color(d[0] && d[0].key)) - ); + selection + .style("stroke", (d) => withoutOpacity(color(d[0] && d[0].key))) + .style("stroke-width", stroke_width); }); return series.crossValue((d) => d.crossValue).mainValue((d) => d.mainValue); From 91e2b053c8f760231c3123884b5aaf79986a9cd0 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 19:46:07 -0500 Subject: [PATCH 23/31] Fix CSS bugs --- rust/perspective-viewer/src/less/config-selector.less | 2 +- rust/perspective-viewer/src/themes/monokai.less | 1 + rust/perspective-viewer/src/themes/solarized-dark.less | 2 +- rust/perspective-viewer/src/themes/solarized.less | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rust/perspective-viewer/src/less/config-selector.less b/rust/perspective-viewer/src/less/config-selector.less index 7f82ab455d..8caf0fc83b 100644 --- a/rust/perspective-viewer/src/less/config-selector.less +++ b/rust/perspective-viewer/src/less/config-selector.less @@ -215,7 +215,7 @@ user-select: none; padding: 12px 24px 0 0; - &:hover { + &:hover:before { color: var(--active--color, inherit); } diff --git a/rust/perspective-viewer/src/themes/monokai.less b/rust/perspective-viewer/src/themes/monokai.less index f0c0d4c575..653fdb09d6 100644 --- a/rust/perspective-viewer/src/themes/monokai.less +++ b/rust/perspective-viewer/src/themes/monokai.less @@ -58,6 +58,7 @@ perspective-expression-editor[theme="Monokai"], .perspective-viewer-monokai--datagrid { regular-table { --rt-pos-cell--color: #78dce8 !important; + --rt-neg-cell--color: #ff6188 !important; } regular-table table tbody tr td, diff --git a/rust/perspective-viewer/src/themes/solarized-dark.less b/rust/perspective-viewer/src/themes/solarized-dark.less index 9c88667bc0..4ffc9baa34 100644 --- a/rust/perspective-viewer/src/themes/solarized-dark.less +++ b/rust/perspective-viewer/src/themes/solarized-dark.less @@ -18,8 +18,8 @@ perspective-viewer[theme="Solarized Dark"] { perspective-viewer[theme="Solarized Dark"], .perspective-viewer-solarized-dark { - .perspective-viewer-solarized(); .perspective-viewer-material-dark--colors(); + .perspective-viewer-solarized(); .perspective-viewer-solarized-dark--colors(); .perspective-viewer-solarized-dark--datagrid(); .perspective-viewer-solarized-dark--d3fc(); diff --git a/rust/perspective-viewer/src/themes/solarized.less b/rust/perspective-viewer/src/themes/solarized.less index f378919c9f..e0aedbae75 100644 --- a/rust/perspective-viewer/src/themes/solarized.less +++ b/rust/perspective-viewer/src/themes/solarized.less @@ -52,7 +52,8 @@ perspective-expression-editor[theme="Solarized"], .perspective-viewer-solarized--datagrid { regular-table { - --rt-pos-cell--color: rgb(66, 182, 230) !important; + --rt-pos-cell--color: #268bd2 !important; + --rt-neg-cell--color: #cb4b16 !important; } regular-table { From da49298369f6f5d0031c515dc30a1cd157326b47 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 19:47:10 -0500 Subject: [PATCH 24/31] Fix invalid filename highlight --- rust/perspective-viewer/src/rust/model/export_method.rs | 5 ++++- rust/perspective-viewer/src/themes/material-dark.less | 1 + rust/perspective-viewer/src/themes/material.less | 1 + rust/perspective-viewer/src/themes/monokai.less | 1 + rust/perspective-viewer/src/themes/solarized.less | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rust/perspective-viewer/src/rust/model/export_method.rs b/rust/perspective-viewer/src/rust/model/export_method.rs index f24698efa5..6dbf5ed1b7 100644 --- a/rust/perspective-viewer/src/rust/model/export_method.rs +++ b/rust/perspective-viewer/src/rust/model/export_method.rs @@ -115,7 +115,10 @@ impl From for Html { }; html_template! { - { x.name }{ x.method } + + { x.name } + { x.method.to_filename() } + } } } diff --git a/rust/perspective-viewer/src/themes/material-dark.less b/rust/perspective-viewer/src/themes/material-dark.less index c8fa1d0640..8f2b5b0564 100644 --- a/rust/perspective-viewer/src/themes/material-dark.less +++ b/rust/perspective-viewer/src/themes/material-dark.less @@ -44,6 +44,7 @@ perspective-expression-editor[theme="Material Dark"], color: white; --monaco-theme: vs-dark; --active--color: #2770a9; + --error--color: @red50; --inactive--color: @grey300; --plugin--background: @grey700; --modal-target--background: rgba(255, 255, 255, 0.05); diff --git a/rust/perspective-viewer/src/themes/material.less b/rust/perspective-viewer/src/themes/material.less index d53a7f6b46..985f971e2d 100644 --- a/rust/perspective-viewer/src/themes/material.less +++ b/rust/perspective-viewer/src/themes/material.less @@ -72,6 +72,7 @@ perspective-expression-editor[theme="Material Light"], background-color: #f2f4f6; --active--color: @blue500; + --error--color: @red300; --plugin--background: #ffffff; --overflow-hint-icon--color: rgba(0, 0, 0, 0.2); --select--background-color: none; diff --git a/rust/perspective-viewer/src/themes/monokai.less b/rust/perspective-viewer/src/themes/monokai.less index 653fdb09d6..0849f4c971 100644 --- a/rust/perspective-viewer/src/themes/monokai.less +++ b/rust/perspective-viewer/src/themes/monokai.less @@ -42,6 +42,7 @@ perspective-expression-editor[theme="Monokai"], background: #2d2a2e; --active--color: #78dce8; + --error--color: #ff6188; --inactive--color: #797979; --plugin--background: #2d2a2e; // --plugin--border: #797979; diff --git a/rust/perspective-viewer/src/themes/solarized.less b/rust/perspective-viewer/src/themes/solarized.less index e0aedbae75..62e2379150 100644 --- a/rust/perspective-viewer/src/themes/solarized.less +++ b/rust/perspective-viewer/src/themes/solarized.less @@ -40,6 +40,7 @@ perspective-expression-editor[theme="Solarized"], color: #586e75; background: #eee8d5; --active--color: #268bd2; + --error--color: #cb4b16; --inactive--color: #93a1a1; --plugin--background: #fdf6e3; From e23bb88be9eb0277eff7b313a2e8463d09788996 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 19:47:19 -0500 Subject: [PATCH 25/31] Fix lint errors --- rust/perspective-viewer/src/rust/model.rs | 2 +- rust/perspective-viewer/src/rust/utils/clipboard.rs | 2 +- rust/perspective-viewer/src/rust/utils/download.rs | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/rust/perspective-viewer/src/rust/model.rs b/rust/perspective-viewer/src/rust/model.rs index 998adc3653..7aa762a0a2 100644 --- a/rust/perspective-viewer/src/rust/model.rs +++ b/rust/perspective-viewer/src/rust/model.rs @@ -108,7 +108,7 @@ window.viewer.restore(JSON.parse(window.layout.textContent)); ", base64::encode(arrow), js_config)); let array = [html].iter().collect::(); - Ok(web_sys::Blob::new_with_u8_array_sequence(&array)?) + web_sys::Blob::new_with_u8_array_sequence(&array) }) } diff --git a/rust/perspective-viewer/src/rust/utils/clipboard.rs b/rust/perspective-viewer/src/rust/utils/clipboard.rs index 19f426f763..6875b15c3c 100644 --- a/rust/perspective-viewer/src/rust/utils/clipboard.rs +++ b/rust/perspective-viewer/src/rust/utils/clipboard.rs @@ -40,7 +40,7 @@ fn poll( ) -> Result<(), JsValue> { if let Some(js_val) = js_ref.borrow().as_ref() { let options = js_sys::Object::new(); - js_sys::Reflect::set(&options, &mimetype.into(), &js_val); + js_sys::Reflect::set(&options, &mimetype.into(), js_val)?; let item = ClipboardItem::new(&options); let items = [item].iter().collect::(); let _promise = web_sys::window() diff --git a/rust/perspective-viewer/src/rust/utils/download.rs b/rust/perspective-viewer/src/rust/utils/download.rs index 5e5d27145c..bd86498fa3 100644 --- a/rust/perspective-viewer/src/rust/utils/download.rs +++ b/rust/perspective-viewer/src/rust/utils/download.rs @@ -13,10 +13,7 @@ pub fn download(name: &str, value: &web_sys::Blob) -> Result<(), JsValue> { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let element: web_sys::HtmlElement = document.create_element("a")?.unchecked_into(); - let blob_url = { - web_sys::Url::create_object_url_with_blob(&value)? - }; - + let blob_url = web_sys::Url::create_object_url_with_blob(value)?; element.set_attribute("download", name)?; element.set_attribute("href", &blob_url)?; element.style().set_property("display", "none")?; From a566dcf8600bb2205046459e4b74c4b6853631f8 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 23:38:50 -0500 Subject: [PATCH 26/31] Remove (some) opacity from material D3FC --- rust/perspective-viewer/src/themes/material.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/perspective-viewer/src/themes/material.less b/rust/perspective-viewer/src/themes/material.less index 985f971e2d..20593ca802 100644 --- a/rust/perspective-viewer/src/themes/material.less +++ b/rust/perspective-viewer/src/themes/material.less @@ -140,7 +140,7 @@ perspective-expression-editor[theme="Material Light"], --d3fc-axis--lines: @grey60; --d3fc-legend--background: rgba(255, 255, 255, 0.8); - --d3fc-series: rgba(31, 119, 180, 0.5); + --d3fc-series: rgba(31, 119, 180, 0.8); --d3fc-series-1: #0366d6; --d3fc-series-2: #ff7f0e; --d3fc-series-3: #2ca02c; From f63556a1090b28dc66455bea61055e90a7012173 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 23:39:32 -0500 Subject: [PATCH 27/31] Fix copy dropdown to refresh on plugin change --- .../src/rust/components/containers/mod.rs | 1 + .../components/containers/modal_anchor.rs | 24 ++++++++ .../src/rust/components/copy_dropdown.rs | 60 +++++++++++++++++-- .../src/rust/custom_elements/copy_dropdown.rs | 6 +- 4 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 rust/perspective-viewer/src/rust/components/containers/modal_anchor.rs diff --git a/rust/perspective-viewer/src/rust/components/containers/mod.rs b/rust/perspective-viewer/src/rust/components/containers/mod.rs index d8ace245f1..1522fae7da 100644 --- a/rust/perspective-viewer/src/rust/components/containers/mod.rs +++ b/rust/perspective-viewer/src/rust/components/containers/mod.rs @@ -11,6 +11,7 @@ pub mod dragdrop_list; pub mod dropdown_menu; +pub mod modal_anchor; pub mod radio_list; pub mod scroll_panel; pub mod select; diff --git a/rust/perspective-viewer/src/rust/components/containers/modal_anchor.rs b/rust/perspective-viewer/src/rust/components/containers/modal_anchor.rs new file mode 100644 index 0000000000..d09de44900 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/containers/modal_anchor.rs @@ -0,0 +1,24 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +use yew::prelude::*; + +#[derive(Properties, Clone, PartialEq)] +pub struct ModalAnchorProps { + pub top: i32, + pub left: i32, +} + +#[function_component(ModalAnchor)] +pub fn split_panel_child(props: &ModalAnchorProps) -> Html { + html! { + + } +} diff --git a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs index b397c87878..e8c07812af 100644 --- a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs @@ -7,9 +7,62 @@ // file. use super::containers::dropdown_menu::*; +use super::containers::modal_anchor::*; use crate::model::*; +use crate::renderer::*; +use crate::*; -pub fn get_menu_items(has_render: bool) -> Vec { +use js_intern::*; +use std::rc::Rc; +use yew::prelude::*; + +pub type CopyDropDownMenuMsg = DropDownMenuMsg; +pub type CopyDropDownMenuItem = DropDownMenuItem; + +#[derive(Properties, Clone, PartialEq)] +pub struct CopyDropDownMenuProps { + pub renderer: Renderer, + pub callback: Callback, +} + +#[derive(Default)] +pub struct CopyDropDownMenu { + top: i32, + left: i32, +} + +impl Component for CopyDropDownMenu { + type Message = CopyDropDownMenuMsg; + type Properties = CopyDropDownMenuProps; + + fn view(&self, ctx: &Context) -> yew::virtual_dom::VNode { + let plugin = ctx.props().renderer.get_active_plugin().unwrap(); + let has_render = js_sys::Reflect::has(&plugin, js_intern!("render")).unwrap(); + html_template! { + + + values={ Rc::new(get_menu_items(has_render)) } + callback={ ctx.props().callback.clone() }> + > + } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + CopyDropDownMenuMsg::SetPos(top, left) => { + self.top = top; + self.left = left; + true + } + } + } + + fn create(_ctx: &Context) -> Self { + CopyDropDownMenu::default() + } +} + +fn get_menu_items(has_render: bool) -> Vec { vec![ CopyDropDownMenuItem::OptGroup( "Current View", @@ -26,8 +79,3 @@ pub fn get_menu_items(has_render: bool) -> Vec { CopyDropDownMenuItem::OptGroup("Config", vec![ExportMethod::JsonConfig]), ] } - -pub type CopyDropDownMenu = DropDownMenu; -pub type CopyDropDownMenuProps = DropDownMenuProps; -pub type CopyDropDownMenuMsg = DropDownMenuMsg; -pub type CopyDropDownMenuItem = DropDownMenuItem; diff --git a/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs index a1ace97bcb..3c5e386120 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/copy_dropdown.rs @@ -13,7 +13,6 @@ use crate::renderer::Renderer; use crate::session::Session; use crate::utils::*; -use js_intern::*; use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::prelude::*; @@ -63,10 +62,7 @@ impl CopyDropDownMenuElement { } }); - let plugin = renderer.get_active_plugin().unwrap(); - let has_render = js_sys::Reflect::has(&plugin, js_intern!("render")).unwrap(); - let values = Rc::new(get_menu_items(has_render)); - let props = CopyDropDownMenuProps { values, callback }; + let props = CopyDropDownMenuProps { renderer, callback }; let modal = ModalElement::new(dropdown, props, true); *modal_rc.borrow_mut() = Some(modal.clone()); CopyDropDownMenuElement { From ea71f137c7c8085c9a3ac09ba6984f794bb88e7d Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 9 Mar 2022 23:39:54 -0500 Subject: [PATCH 28/31] Refactor use modal_anchor --- .../src/rust/components/export_dropdown.rs | 5 ++--- .../src/rust/components/expression_editor.rs | 5 +++-- .../src/rust/components/filter_dropdown.rs | 3 ++- .../src/rust/components/number_column_style.rs | 3 ++- .../src/rust/components/string_column_style.rs | 3 ++- .../src/rust/components/tests/column_style.rs | 7 +++++++ 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/rust/perspective-viewer/src/rust/components/export_dropdown.rs b/rust/perspective-viewer/src/rust/components/export_dropdown.rs index 0152a9e966..c39ba34031 100644 --- a/rust/perspective-viewer/src/rust/components/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/export_dropdown.rs @@ -7,6 +7,7 @@ // file. use super::containers::dropdown_menu::*; +use super::containers::modal_anchor::*; use crate::model::*; use crate::renderer::*; @@ -82,9 +83,7 @@ impl Component for ExportDropDownMenu { let plugin = ctx.props().renderer.get_active_plugin().unwrap(); let has_render = js_sys::Reflect::has(&plugin, js_intern!("render")).unwrap(); html_template! { - + { "Save as" } + + { body } } diff --git a/rust/perspective-viewer/src/rust/components/number_column_style.rs b/rust/perspective-viewer/src/rust/components/number_column_style.rs index c6e96256be..a544f2318f 100644 --- a/rust/perspective-viewer/src/rust/components/number_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/number_column_style.rs @@ -7,6 +7,7 @@ // file. use super::color_range_selector::*; +use super::containers::modal_anchor::*; use super::containers::radio_list::RadioList; use crate::*; @@ -389,8 +390,8 @@ impl Component for NumberColumnStyle { <> +