@perspective-dev/viewer 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/dracula.css +1 -1
  4. package/dist/css/gruvbox-dark.css +1 -1
  5. package/dist/css/gruvbox.css +1 -1
  6. package/dist/css/icons.css +1 -1
  7. package/dist/css/monokai.css +1 -1
  8. package/dist/css/pro-dark.css +1 -1
  9. package/dist/css/pro.css +1 -1
  10. package/dist/css/solarized-dark.css +1 -1
  11. package/dist/css/solarized.css +1 -1
  12. package/dist/css/themes.css +1 -1
  13. package/dist/css/vaporwave.css +1 -1
  14. package/dist/esm/extensions.d.ts +23 -2
  15. package/dist/esm/perspective-viewer.d.ts +2 -7
  16. package/dist/esm/perspective-viewer.inline.js +2 -2
  17. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  18. package/dist/esm/perspective-viewer.js +2 -2
  19. package/dist/esm/perspective-viewer.js.map +4 -4
  20. package/dist/esm/plugin.d.ts +1 -1
  21. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +1 -0
  22. package/dist/wasm/perspective-viewer.d.ts +218 -46
  23. package/dist/wasm/perspective-viewer.js +1251 -762
  24. package/dist/wasm/perspective-viewer.wasm +0 -0
  25. package/dist/wasm/perspective-viewer.wasm.d.ts +38 -19
  26. package/package.json +1 -1
  27. package/src/less/containers/scroll-panel.less +0 -1
  28. package/src/less/plugin-selector.less +15 -5
  29. package/src/less/status-bar.less +75 -27
  30. package/src/less/viewer.less +140 -58
  31. package/src/rust/components/column_dropdown.rs +21 -21
  32. package/src/rust/components/column_selector/active_column.rs +131 -120
  33. package/src/rust/components/column_selector/add_expression_button.rs +5 -0
  34. package/src/rust/components/column_selector/aggregate_selector.rs +8 -4
  35. package/src/rust/components/column_selector/config_selector.rs +170 -161
  36. package/src/rust/components/column_selector/empty_column.rs +16 -11
  37. package/src/rust/components/column_selector/{expression_toolbar.rs → expr_edit_button.rs} +7 -0
  38. package/src/rust/components/column_selector/filter_column.rs +195 -194
  39. package/src/rust/components/column_selector/inactive_column.rs +82 -67
  40. package/src/rust/components/column_selector/pivot_column.rs +16 -11
  41. package/src/rust/components/column_selector/sort_column.rs +9 -7
  42. package/src/rust/components/column_selector.rs +42 -37
  43. package/src/rust/components/column_settings_sidebar/save_settings.rs +3 -1
  44. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +58 -0
  45. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +6 -6
  46. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +2 -94
  47. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +111 -0
  48. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +3 -3
  49. package/src/rust/components/column_settings_sidebar/style_tab.rs +23 -83
  50. package/src/rust/components/{column_settings_sidebar/sidebar.rs → column_settings_sidebar.rs} +198 -171
  51. package/src/rust/components/containers/dragdrop_list.rs +20 -20
  52. package/src/rust/components/containers/dropdown_menu.rs +4 -6
  53. package/src/rust/components/containers/mod.rs +1 -4
  54. package/src/rust/components/containers/scroll_panel.rs +80 -80
  55. package/src/rust/components/containers/scroll_panel_item.rs +36 -36
  56. package/src/rust/components/containers/select.rs +46 -44
  57. package/src/rust/components/containers/sidebar.rs +3 -19
  58. package/src/rust/components/{column_settings_sidebar/style_tab/symbol/symbol_config.rs → containers/sidebar_close_button.rs} +15 -9
  59. package/src/rust/components/containers/split_panel.rs +212 -200
  60. package/src/rust/components/containers/tab_list.rs +11 -11
  61. package/src/rust/components/copy_dropdown.rs +22 -25
  62. package/src/rust/components/datetime_column_style/custom.rs +19 -19
  63. package/src/rust/components/datetime_column_style/simple.rs +13 -14
  64. package/src/rust/components/datetime_column_style.rs +75 -76
  65. package/src/rust/components/editable_header.rs +18 -14
  66. package/src/rust/components/empty_row.rs +5 -5
  67. package/src/rust/components/export_dropdown.rs +42 -42
  68. package/src/rust/components/expression_editor.rs +25 -19
  69. package/src/rust/components/filter_dropdown.rs +22 -22
  70. package/src/rust/components/font_loader.rs +11 -9
  71. package/src/rust/components/form/code_editor.rs +106 -105
  72. package/src/rust/components/form/color_range_selector.rs +14 -12
  73. package/src/rust/components/form/color_selector.rs +3 -1
  74. package/src/rust/components/form/debug.rs +95 -94
  75. package/src/rust/components/form/highlight.rs +5 -3
  76. package/src/rust/components/form/mod.rs +3 -2
  77. package/src/rust/components/form/optional_field.rs +2 -2
  78. package/src/rust/components/form/{select_field.rs → select_enum_field.rs} +1 -46
  79. package/src/rust/components/form/select_value_field.rs +64 -0
  80. package/src/rust/components/function_dropdown.rs +21 -21
  81. package/src/rust/components/main_panel.rs +219 -0
  82. package/src/rust/components/mod.rs +6 -6
  83. package/src/rust/components/modal.rs +42 -42
  84. package/src/rust/components/number_column_style.rs +34 -88
  85. package/src/rust/components/plugin_selector.rs +22 -25
  86. package/src/rust/components/render_warning.rs +9 -6
  87. package/src/rust/components/settings_panel.rs +82 -0
  88. package/src/rust/components/status_bar.rs +250 -146
  89. package/src/rust/components/status_bar_counter.rs +26 -119
  90. package/src/rust/components/status_indicator.rs +95 -79
  91. package/src/rust/components/string_column_style.rs +45 -45
  92. package/src/rust/components/style/style_provider.rs +1 -15
  93. package/src/rust/components/style_controls/number_string_format/digits_section.rs +1 -1
  94. package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
  95. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
  96. package/src/rust/components/style_controls/number_string_format.rs +45 -46
  97. package/src/rust/components/type_icon.rs +14 -11
  98. package/src/rust/components/viewer.rs +241 -384
  99. package/src/rust/config/columns_config.rs +2 -2
  100. package/src/rust/config/datetime_column_style.rs +1 -6
  101. package/src/rust/config/mod.rs +1 -0
  102. package/src/rust/config/number_column_style.rs +0 -6
  103. package/src/rust/config/number_string_format.rs +27 -4
  104. package/src/rust/config/viewer_config.rs +27 -167
  105. package/src/rust/custom_elements/copy_dropdown.rs +14 -6
  106. package/src/rust/custom_elements/export_dropdown.rs +15 -7
  107. package/src/rust/custom_elements/filter_dropdown.rs +4 -4
  108. package/src/rust/custom_elements/mod.rs +3 -0
  109. package/src/rust/custom_elements/viewer.rs +353 -161
  110. package/src/rust/custom_events.rs +55 -32
  111. package/src/rust/dragdrop.rs +4 -24
  112. package/src/rust/exprtk/cursor.rs +10 -1
  113. package/src/rust/exprtk/mod.rs +2 -0
  114. package/src/rust/exprtk/tokenize.rs +20 -3
  115. package/src/rust/js/clipboard.rs +2 -2
  116. package/src/rust/js/mimetype.rs +2 -7
  117. package/src/rust/js/mod.rs +0 -1
  118. package/src/rust/js/plugin.rs +7 -0
  119. package/src/rust/lib.rs +18 -5
  120. package/src/rust/model/column_locator.rs +82 -0
  121. package/src/rust/model/columns_iter_set.rs +1 -0
  122. package/src/rust/model/copy_export.rs +50 -14
  123. package/src/rust/model/edit_expression.rs +2 -5
  124. package/src/rust/model/eject.rs +41 -0
  125. package/src/rust/model/export_app.rs +3 -2
  126. package/src/rust/model/get_viewer_config.rs +4 -28
  127. package/src/rust/model/intersection_observer.rs +20 -8
  128. package/src/rust/model/mod.rs +11 -4
  129. package/src/rust/model/plugin_column_styles.rs +0 -31
  130. package/src/rust/model/reset_all.rs +38 -0
  131. package/src/rust/model/resize_observer.rs +34 -7
  132. package/src/rust/model/restore_and_render.rs +12 -7
  133. package/src/rust/{utils/scope.rs → model/send_plugin_config.rs} +32 -35
  134. package/src/rust/model/structural.rs +194 -23
  135. package/src/rust/model/update_and_render.rs +14 -4
  136. package/src/rust/{model/create_col.rs → presentation/column_locator.rs} +73 -42
  137. package/src/rust/{utils/wasm_abi.rs → presentation/sheets.rs} +54 -40
  138. package/src/rust/presentation.rs +60 -119
  139. package/src/rust/renderer/activate.rs +20 -5
  140. package/src/rust/renderer/limits.rs +0 -149
  141. package/src/rust/renderer/render_timer.rs +1 -1
  142. package/src/rust/renderer.rs +34 -18
  143. package/src/rust/root.rs +50 -0
  144. package/src/rust/session/column_defaults_update.rs +4 -4
  145. package/src/rust/session/drag_drop_update.rs +1 -1
  146. package/src/rust/session/metadata.rs +3 -17
  147. package/src/rust/session/replace_expression_update.rs +1 -2
  148. package/src/rust/session.rs +162 -82
  149. package/src/rust/utils/browser/blob.rs +16 -2
  150. package/src/rust/utils/browser/download.rs +1 -0
  151. package/src/rust/{components/column_settings_sidebar/mod.rs → utils/browser/dragdrop.rs} +14 -5
  152. package/src/rust/utils/browser/mod.rs +8 -4
  153. package/src/rust/utils/browser/selection.rs +5 -0
  154. package/src/rust/utils/custom_element.rs +28 -13
  155. package/src/rust/utils/datetime.rs +5 -0
  156. package/src/rust/utils/debounce.rs +7 -1
  157. package/src/rust/utils/hooks/use_async_callback.rs +7 -17
  158. package/src/rust/utils/mod.rs +28 -40
  159. package/src/rust/utils/number_format.rs +6 -5
  160. package/src/rust/utils/pubsub.rs +15 -10
  161. package/src/rust/utils/weak_scope.rs +11 -1
  162. package/src/svg/bookmark-icon.svg +4 -0
  163. package/src/svg/drag-handle copy.svg +10 -0
  164. package/src/svg/drawer-tab-hover.svg +5 -7
  165. package/src/svg/drawer-tab-invert-hover.svg +4 -8
  166. package/src/svg/drawer-tab-invert.svg +4 -7
  167. package/src/svg/drawer-tab.svg +4 -6
  168. package/src/svg/status_ok.svg +24 -24
  169. package/src/ts/extensions.ts +51 -3
  170. package/src/ts/perspective-viewer.ts +2 -14
  171. package/src/ts/plugin.ts +1 -1
  172. package/src/ts/ts-rs/ViewerConfigUpdate.ts +1 -1
  173. package/src/rust/components/column_settings_sidebar/style_tab/column_style.rs +0 -177
  174. package/src/rust/components/containers/tests/mod.rs +0 -11
  175. package/src/rust/components/containers/tests/split_panel.rs +0 -91
  176. package/src/rust/js/testing.rs +0 -149
  177. package/src/rust/utils/tee.rs +0 -88
  178. /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline0.js +0 -0
  179. /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline1.js +0 -0
  180. /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline2.js +0 -0
  181. /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline3.js +0 -0
  182. /package/dist/wasm/snippets/{perspective-viewer-c69283f6f62a5f14 → perspective-viewer-0d326a25c1022412}/inline4.js +0 -0
  183. /package/src/rust/components/{style_controls.rs → style_controls/mod.rs} +0 -0
  184. /package/src/rust/{components/containers → config}/kvpair.rs +0 -0
@@ -14,20 +14,19 @@
14
14
 
15
15
  use std::cell::RefCell;
16
16
  use std::rc::Rc;
17
- use std::str::FromStr;
18
17
 
19
- use ::perspective_js::{Table, View};
20
- use futures::future::join;
21
- use js_sys::*;
18
+ use futures::channel::oneshot::channel;
19
+ use js_sys::{Array, JsString};
22
20
  use perspective_client::config::ViewConfigUpdate;
23
- use perspective_js::{JsViewWindow, apierror};
21
+ use perspective_client::utils::PerspectiveResultExt;
22
+ use perspective_js::{JsViewConfig, JsViewWindow, Table, View, apierror};
24
23
  use wasm_bindgen::JsCast;
25
24
  use wasm_bindgen::prelude::*;
25
+ use wasm_bindgen_derive::try_from_js_option;
26
26
  use wasm_bindgen_futures::JsFuture;
27
- use web_sys::*;
28
- use yew::prelude::*;
27
+ use web_sys::HtmlElement;
29
28
 
30
- use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg, PerspectiveViewerProps};
29
+ use crate::components::viewer::{PerspectiveViewerMsg, PerspectiveViewerProps};
31
30
  use crate::config::*;
32
31
  use crate::custom_events::*;
33
32
  use crate::dragdrop::*;
@@ -35,7 +34,8 @@ use crate::js::*;
35
34
  use crate::model::*;
36
35
  use crate::presentation::*;
37
36
  use crate::renderer::*;
38
- use crate::session::Session;
37
+ use crate::root::Root;
38
+ use crate::session::{ResetOptions, Session};
39
39
  use crate::utils::*;
40
40
  use crate::*;
41
41
 
@@ -49,21 +49,36 @@ use crate::*;
49
49
  /// const viewer = document.createElement("perspective-viewer");
50
50
  /// window.body.appendChild(viewer);
51
51
  /// ```
52
- #[derive(Clone)]
52
+ ///
53
+ /// Complete example including loading and restoring the [`Table`]:
54
+ ///
55
+ /// ```javascript
56
+ /// import perspective from "@perspective-dev/viewer";
57
+ /// import perspective from "@perspective-dev/client";
58
+ ///
59
+ /// const viewer = document.createElement("perspective-viewer");
60
+ /// const worker = await perspective.worker();
61
+ ///
62
+ /// await worker.table("x\n1", {name: "table_one"});
63
+ /// await viewer.load(worker);
64
+ /// await viewer.restore({table: "table_one"});
65
+ /// ```
66
+ #[derive(Clone, PerspectiveProperties!)]
53
67
  #[wasm_bindgen]
54
68
  pub struct PerspectiveViewerElement {
55
69
  elem: HtmlElement,
56
- root: Rc<RefCell<Option<AppHandle<PerspectiveViewer>>>>,
70
+ root: Root<components::viewer::PerspectiveViewer>,
57
71
  resize_handle: Rc<RefCell<Option<ResizeObserverHandle>>>,
58
72
  intersection_handle: Rc<RefCell<Option<IntersectionObserverHandle>>>,
59
73
  session: Session,
60
74
  renderer: Renderer,
61
75
  presentation: Presentation,
62
- events: CustomEvents,
63
- _subscriptions: Rc<Subscription>,
76
+ custom_events: CustomEvents,
77
+ _subscriptions: Rc<[Subscription; 2]>,
64
78
  }
65
79
 
66
- derive_model!( Renderer, Session, Presentation for PerspectiveViewerElement);
80
+ // derive_model!( Renderer, Root, Session, Presentation for
81
+ // PerspectiveViewerElement);
67
82
 
68
83
  impl CustomElementMetadata for PerspectiveViewerElement {
69
84
  const CUSTOM_ELEMENT_NAME: &'static str = "perspective-viewer";
@@ -86,10 +101,10 @@ impl PerspectiveViewerElement {
86
101
 
87
102
  fn new_from_shadow(elem: web_sys::HtmlElement, shadow_root: web_sys::Element) -> Self {
88
103
  // Application State
89
- let session = Session::default();
104
+ let session = Session::new();
90
105
  let renderer = Renderer::new(&elem);
91
106
  let presentation = Presentation::new(&elem);
92
- let events = CustomEvents::new(&elem, &session, &renderer, &presentation);
107
+ let custom_events = CustomEvents::new(&elem, &session, &renderer, &presentation);
93
108
 
94
109
  // Create Yew App
95
110
  let props = yew::props!(PerspectiveViewerProps {
@@ -98,50 +113,84 @@ impl PerspectiveViewerElement {
98
113
  renderer: renderer.clone(),
99
114
  presentation: presentation.clone(),
100
115
  dragdrop: DragDrop::default(),
101
- custom_events: events.clone(),
102
- weak_link: WeakScope::default(),
116
+ custom_events: custom_events.clone(),
103
117
  });
104
118
 
105
- let root = yew::Renderer::with_root_and_props(shadow_root, props).render();
119
+ let state = props.clone_state();
120
+ let root = Root::new(shadow_root, props);
106
121
 
107
122
  // Create callbacks
108
123
  let update_sub = session.table_updated.add_listener({
109
124
  clone!(renderer, session);
110
125
  move |_| {
111
126
  clone!(renderer, session);
112
- ApiFuture::spawn(async move { renderer.update(&session).await })
127
+ ApiFuture::spawn(async move { renderer.update(session.get_view()).await })
113
128
  }
114
129
  });
115
130
 
116
- let resize_handle = ResizeObserverHandle::new(&elem, &renderer, &root);
131
+ let eject_sub = presentation.on_eject.add_listener({
132
+ let root = root.clone();
133
+ move |_| ApiFuture::spawn(state.delete_all(&root))
134
+ });
135
+
136
+ let resize_handle = ResizeObserverHandle::new(&elem, &renderer, &session, &root);
137
+ let intersect_handle =
138
+ IntersectionObserverHandle::new(&elem, &presentation, &session, &renderer);
139
+
117
140
  Self {
118
141
  elem,
119
- root: Rc::new(RefCell::new(Some(root))),
142
+ root,
120
143
  session,
121
144
  renderer,
122
145
  presentation,
123
146
  resize_handle: Rc::new(RefCell::new(Some(resize_handle))),
124
- intersection_handle: Rc::new(RefCell::new(None)),
125
- events,
126
- _subscriptions: Rc::new(update_sub),
147
+ intersection_handle: Rc::new(RefCell::new(Some(intersect_handle))),
148
+ custom_events,
149
+ _subscriptions: Rc::new([update_sub, eject_sub]),
127
150
  }
128
151
  }
129
152
 
130
153
  #[doc(hidden)]
131
154
  #[wasm_bindgen(js_name = "connectedCallback")]
132
- pub fn connected_callback(&self) {
155
+ pub fn connected_callback(&self) -> ApiResult<()> {
133
156
  tracing::debug!("Connected <perspective-viewer>");
157
+ Ok(())
134
158
  }
135
159
 
136
- /// Loads a [`Table`] (or rather, a Javascript `Promise` which returns a
137
- /// [`Table`]) in this viewer.
160
+ /// Loads a [`Client`], or optionally [`Table`], or optionally a Javascript
161
+ /// `Promise` which returns a [`Client`] or [`Table`], in this viewer.
162
+ ///
163
+ /// Loading a [`Client`] does not render, but subsequent calls to
164
+ /// [`PerspectiveViewerElement::restore`] will use this [`Client`] to look
165
+ /// up the proviced `table` name field for the provided
166
+ /// [`ViewerConfigUpdate`].
167
+ ///
168
+ /// Loading a [`Table`] is equivalent to subsequently calling
169
+ /// [`Self::restore`] with the `table` field set to [`Table::get_name`], and
170
+ /// will render the UI in its default state when [`Self::load`] resolves.
171
+ /// If you plan to call [`Self::restore`] anyway, prefer passing a
172
+ /// [`Client`] argument to [`Self::load`] as it will conserve one render.
138
173
  ///
139
174
  /// When [`PerspectiveViewerElement::load`] resolves, the first frame of the
140
175
  /// UI + visualization is guaranteed to have been drawn. Awaiting the result
141
176
  /// of this method in a `try`/`catch` block will capture any errors
142
- /// thrown during the loading process, or from the [`Table`] `Promise`
177
+ /// thrown during the loading process, or from the [`Client`] `Promise`
143
178
  /// itself.
144
179
  ///
180
+ /// [`PerspectiveViewerElement::load`] may also be called with a [`Table`],
181
+ /// which is equivalent to:
182
+ ///
183
+ /// ```javascript
184
+ /// await viewer.load(await table.get_client());
185
+ /// await viewer.restore({name: await table.get_name()})
186
+ /// ```
187
+ ///
188
+ /// If you plan to call [`PerspectiveViewerElement::restore`] immediately
189
+ /// after [`PerspectiveViewerElement::load`] yourself, as is commonly
190
+ /// done when loading and configuring a new `<perspective-viewer>`, you
191
+ /// should use a [`Client`] as an argument and set the `table` field in the
192
+ /// restore call as
193
+ ///
145
194
  /// A [`Table`] can be created using the
146
195
  /// [`@perspective-dev/client`](https://www.npmjs.com/package/@perspective-dev/client)
147
196
  /// library from NPM (see [`perspective_js`] documentation for details).
@@ -152,76 +201,104 @@ impl PerspectiveViewerElement {
152
201
  /// import perspective from "@perspective-dev/client";
153
202
  ///
154
203
  /// const worker = await perspective.worker();
155
- /// viewer.load(worker.table("x,y\n1,2"));
204
+ /// viewer.load(worker);
156
205
  /// ```
157
- pub fn load(&self, table: JsValue) -> ApiFuture<()> {
158
- tracing::info!("Loading Table");
159
- self.session.invalidate();
206
+ ///
207
+ /// ... or
208
+ ///
209
+ /// ```javascript
210
+ /// const table = await worker.table(data, {name: "superstore"});
211
+ /// viewer.load(table);
212
+ /// ```
213
+ ///
214
+ /// Complete example:
215
+ ///
216
+ /// ```javascript
217
+ /// const viewer = document.createElement("perspective-viewer");
218
+ /// const worker = await perspective.worker();
219
+ ///
220
+ /// await worker.table("x\n1", {name: "table_one"});
221
+ /// await viewer.load(worker);
222
+ /// await viewer.restore({table: "table_one", columns: ["x"]});
223
+ /// ```
224
+ ///
225
+ /// ... or, if you don't want to pass your own arguments to `restore`:
226
+ ///
227
+ /// ```javascript
228
+ /// const viewer = document.createElement("perspective-viewer");
229
+ /// const worker = await perspective.worker();
230
+ ///
231
+ /// const table = await worker.table("x\n1", {name: "table_one"});
232
+ /// await viewer.load(table);
233
+ /// ```
234
+ pub fn load(&self, table: JsValue) -> ApiResult<ApiFuture<()>> {
160
235
  let promise = table
161
236
  .clone()
162
237
  .dyn_into::<js_sys::Promise>()
163
238
  .unwrap_or_else(|_| js_sys::Promise::resolve(&table));
164
239
 
165
- self.session.reset_stats();
166
- let delete_task = self.session.reset(true);
240
+ let _plugin = self.renderer.get_active_plugin()?;
241
+ let task = self.session.reset(ResetOptions {
242
+ config: true,
243
+ expressions: true,
244
+ stats: true,
245
+ ..ResetOptions::default()
246
+ });
247
+
167
248
  let mut config = ViewConfigUpdate {
168
249
  columns: Some(self.session.get_view_config().columns.clone()),
169
250
  ..ViewConfigUpdate::default()
170
251
  };
171
252
 
253
+ let metadata = self.renderer.metadata();
172
254
  self.session
173
- .set_update_column_defaults(&mut config, &self.renderer.metadata());
255
+ .set_update_column_defaults(&mut config, &metadata);
256
+ self.session.update_view_config(config)?;
174
257
 
175
- let update_task = self.session.update_view_config(config);
176
258
  clone!(self.renderer, self.session);
177
- ApiFuture::new(async move {
259
+ Ok(ApiFuture::new(async move {
178
260
  let task = async {
179
- update_task?;
261
+ // Ignore this error, which is blown away by the table anyway.
262
+ let _ = task.await;
180
263
  let jstable = JsFuture::from(promise)
181
264
  .await
182
265
  .map_err(|x| apierror!(TableError(x)))?;
183
266
 
184
- if let Some(table) =
185
- wasm_bindgen_derive::try_from_js_option::<perspective_js::Table>(jstable)?
267
+ if let Ok(Some(table)) =
268
+ try_from_js_option::<perspective_js::Table>(jstable.clone())
186
269
  {
187
- if let Some(existing_table) = session.get_table() {
188
- if table.get_table() == &existing_table {
189
- tracing::info!(
190
- "Table `{}` already loaded, skipping",
191
- table.get_name().await
192
- );
193
-
194
- return Ok(&session);
195
- } else {
196
- tracing::debug!(
197
- "New table {} vs {}",
198
- table.get_table().get_name(),
199
- existing_table.get_name()
200
- );
201
- }
202
- }
203
-
270
+ let client = table.get_client().await;
271
+ session.set_client(client.get_client().clone());
272
+ let name = table.get_name().await;
204
273
  tracing::debug!(
205
- "Successfully loaded {:.0} rows from Table",
206
- table.size().await?
274
+ "Loading {:.0} rows from `Table` {}",
275
+ table.size().await?,
276
+ name
207
277
  );
208
278
 
209
- session.set_table(table.get_table().clone()).await?;
210
- session.validate().await?.create_view().await
279
+ if session.set_table(name).await? {
280
+ session.validate().await?.create_view().await?;
281
+ }
282
+
283
+ Ok(session.get_view())
284
+ } else if let Ok(Some(client)) =
285
+ wasm_bindgen_derive::try_from_js_option::<perspective_js::Client>(jstable)
286
+ {
287
+ session.set_client(client.get_client().clone());
288
+ Ok(session.get_view())
211
289
  } else {
212
- Err(ApiError::new("Invalid Table"))
290
+ Err(ApiError::new("Invalid argument"))
213
291
  }
214
292
  };
215
293
 
216
294
  renderer.set_throttle(None);
217
- let (draw, delete) = join(renderer.draw(task), delete_task).await;
218
- let result = draw.and(delete);
295
+ let result = renderer.draw(task).await;
219
296
  if let Err(e) = &result {
220
297
  session.set_error(false, e.clone()).await?;
221
298
  }
222
299
 
223
300
  result
224
- })
301
+ }))
225
302
  }
226
303
 
227
304
  /// Delete the internal [`View`] and all associated state, rendering this
@@ -246,17 +323,7 @@ impl PerspectiveViewerElement {
246
323
  /// await viewer.delete();
247
324
  /// ```
248
325
  pub fn delete(self) -> ApiFuture<()> {
249
- clone!(self.renderer, self.session, self.root);
250
- ApiFuture::new(self.renderer.clone().with_lock(async move {
251
- renderer.delete()?;
252
- root.borrow_mut()
253
- .take()
254
- .ok_or("Already deleted!")?
255
- .destroy();
256
- session.delete().await?;
257
- tracing::info!("Deleted <perspective-viewer>");
258
- Ok(())
259
- }))
326
+ self.delete_all(self.root())
260
327
  }
261
328
 
262
329
  /// Restart this `<perspective-viewer>` to its initial state, before
@@ -273,7 +340,7 @@ impl PerspectiveViewerElement {
273
340
  );
274
341
 
275
342
  std::mem::swap(self, &mut state);
276
- state.delete()
343
+ ApiFuture::new(state.delete())
277
344
  } else {
278
345
  ApiFuture::new(async move { Ok(()) })
279
346
  }
@@ -304,8 +371,23 @@ impl PerspectiveViewerElement {
304
371
  ApiFuture::new(async move { Ok(session.get_view().ok_or("No table set")?.into()) })
305
372
  }
306
373
 
374
+ /// Get a copy of the [`ViewConfig`] for the current [`View`]. This is
375
+ /// non-blocking as it does not need to access the plugin (unlike
376
+ /// [`PerspectiveViewerElement::save`]), and also makes no API calls to the
377
+ /// server (unlike [`PerspectiveViewerElement::getView`] followed by
378
+ /// [`View::get_config`])
379
+ #[wasm_bindgen]
380
+ pub fn getViewConfig(&self) -> ApiFuture<JsViewConfig> {
381
+ let session = self.session.clone();
382
+ ApiFuture::new(async move {
383
+ let config = session.get_view_config();
384
+ Ok(JsValue::from_serde_ext(&*config)?.unchecked_into())
385
+ })
386
+ }
387
+
307
388
  /// Get the underlying [`Table`] for this viewer (as passed to
308
- /// [`PerspectiveViewerElement::load`]).
389
+ /// [`PerspectiveViewerElement::load`] or as the `table` field to
390
+ /// [`PerspectiveViewerElement::restore`]).
309
391
  ///
310
392
  /// # Arguments
311
393
  ///
@@ -324,10 +406,40 @@ impl PerspectiveViewerElement {
324
406
  ApiFuture::new(async move {
325
407
  match session.get_table() {
326
408
  Some(table) => Ok(table.into()),
327
- None if !wait_for_table.unwrap_or_default() => Err("No table set".into()),
409
+ None if !wait_for_table.unwrap_or_default() => Err("No `Table` set".into()),
410
+ None => {
411
+ session.table_loaded.read_next().await?;
412
+ Ok(session.get_table().ok_or("No `Table` set")?.into())
413
+ },
414
+ }
415
+ })
416
+ }
417
+
418
+ /// Get the underlying [`Client`] for this viewer (as passed to, or
419
+ /// associated with the [`Table`] passed to,
420
+ /// [`PerspectiveViewerElement::load`]).
421
+ ///
422
+ /// # Arguments
423
+ ///
424
+ /// - `wait_for_client` - whether to wait for
425
+ /// [`PerspectiveViewerElement::load`] to be called, or fail immediately
426
+ /// if [`PerspectiveViewerElement::load`] has not yet been called.
427
+ ///
428
+ /// # JavaScript Examples
429
+ ///
430
+ /// ```javascript
431
+ /// const client = await viewer.getClient();
432
+ /// ```
433
+ #[wasm_bindgen]
434
+ pub fn getClient(&self, wait_for_client: Option<bool>) -> ApiFuture<perspective_js::Client> {
435
+ let session = self.session.clone();
436
+ ApiFuture::new(async move {
437
+ match session.get_client() {
438
+ Some(client) => Ok(client.into()),
439
+ None if !wait_for_client.unwrap_or_default() => Err("No `Client` set".into()),
328
440
  None => {
329
- session.table_loaded.listen_once().await?;
330
- Ok(session.get_table().ok_or("No table set")?.into())
441
+ session.table_loaded.read_next().await?;
442
+ Ok(session.get_client().ok_or("No `Client` set")?.into())
331
443
  },
332
444
  }
333
445
  })
@@ -370,13 +482,23 @@ impl PerspectiveViewerElement {
370
482
  pub fn flush(&self) -> ApiFuture<()> {
371
483
  clone!(self.renderer);
372
484
  ApiFuture::new(async move {
485
+ // We must let two AFs pass to guarantee listeners to the DOM state
486
+ // have themselves triggered, or else `request_animation_frame`
487
+ // may finish before a `ResizeObserver` triggered before is
488
+ // notifiedd.
489
+ //
490
+ // https://github.com/w3c/csswg-drafts/issues/9560
491
+ // https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering
492
+ request_animation_frame().await;
373
493
  request_animation_frame().await;
494
+ renderer.clone().with_lock(async { Ok(()) }).await?;
374
495
  renderer.with_lock(async { Ok(()) }).await
375
496
  })
376
497
  }
377
498
 
378
499
  /// Restores this element from a full/partial
379
- /// [`perspective_js::JsViewConfig`].
500
+ /// [`perspective_js::JsViewConfig`] (this element's user-configurable
501
+ /// state, including the `Table` name).
380
502
  ///
381
503
  /// One of the best ways to use [`Self::restore`] is by first configuring
382
504
  /// a `<perspective-viewer>` as you wish, then using either the `Debug`
@@ -390,42 +512,61 @@ impl PerspectiveViewerElement {
390
512
  ///
391
513
  /// # JavaScript Examples
392
514
  ///
393
- /// Apply a `group_by` to the current [`View`], without modifying/resetting
394
- /// other fields:
515
+ /// Loads a default plugin for the table named `"superstore"`:
516
+ ///
517
+ /// ```javascript
518
+ /// await viewer.restore({table: "superstore"});
519
+ /// ```
520
+ ///
521
+ /// Apply a `group_by` to the same `viewer` element, without
522
+ /// modifying/resetting other fields - you can omit the `table` field,
523
+ /// this has already been set once and is not modified:
395
524
  ///
396
525
  /// ```javascript
397
526
  /// await viewer.restore({group_by: ["State"]});
398
527
  /// ```
399
528
  pub fn restore(&self, update: JsValue) -> ApiFuture<()> {
400
- tracing::info!("Restoring ViewerConfig");
401
529
  let this = self.clone();
402
530
  ApiFuture::new(async move {
403
531
  let decoded_update = ViewerConfigUpdate::decode(&update)?;
532
+ tracing::info!("Restoring {}", decoded_update);
404
533
  let root = this.root.clone();
405
534
  let settings = decoded_update.settings.clone();
406
- let result = root
407
- .borrow()
408
- .as_ref()
409
- .into_apierror()?
410
- .send_message_async(move |x| {
411
- PerspectiveViewerMsg::ToggleSettingsComplete(settings, x)
412
- });
535
+ let (sender, receiver) = channel::<()>();
536
+ root.borrow().as_ref().into_apierror()?.send_message(
537
+ PerspectiveViewerMsg::ToggleSettingsComplete(settings, sender),
538
+ );
413
539
 
414
- let task = this
415
- .restore_and_render(decoded_update, async move { Ok(result.await?) })
540
+ let result = this
541
+ .restore_and_render(decoded_update.clone(), {
542
+ clone!(this, decoded_update.table);
543
+ async move {
544
+ if let OptionalUpdate::Update(name) = table {
545
+ this.session.set_table(name).await?;
546
+ this.session
547
+ .update_column_defaults(&this.renderer.metadata());
548
+ };
549
+
550
+ // Something abnormal in the DOM happened, e.g. the
551
+ // element was disconnected while rendering.
552
+ receiver.await.unwrap_or_log();
553
+ Ok(())
554
+ }
555
+ })
416
556
  .await;
417
557
 
418
- if let Err(e) = task {
558
+ if let Err(e) = &result {
419
559
  this.session().set_error(false, e.clone()).await?;
420
- Err(e)
421
- } else {
422
- Ok(())
423
560
  }
561
+ result
424
562
  })
425
563
  }
426
564
 
565
+ /// If this element is in an _errored_ state, this method will clear it and
566
+ /// re-render. Calling this method is equivalent to clicking the error reset
567
+ /// button in the UI.
427
568
  pub fn resetError(&self) -> ApiFuture<()> {
428
- self.session.invalidate();
569
+ ApiFuture::spawn(self.session.reset(ResetOptions::default()));
429
570
  let this = self.clone();
430
571
  ApiFuture::new(async move {
431
572
  this.update_and_render(ViewConfigUpdate::default())?.await?;
@@ -433,12 +574,8 @@ impl PerspectiveViewerElement {
433
574
  })
434
575
  }
435
576
 
436
- /// Save this element to serialized state object, one which can be restored
437
- /// via the [`Self::restore`] method.
438
- ///
439
- /// # Arguments
440
- ///
441
- /// - `format` - Supports "json" (default), "arraybuffer" or "string".
577
+ /// Save this element's user-configurable state to a serialized state
578
+ /// object, one which can be restored via the [`Self::restore`] method.
442
579
  ///
443
580
  /// # JavaScript Examples
444
581
  ///
@@ -456,26 +593,25 @@ impl PerspectiveViewerElement {
456
593
  /// await viewer.restore(token);
457
594
  /// });
458
595
  /// ```
459
- pub fn save(&self, format: Option<String>) -> ApiFuture<JsValue> {
596
+ pub fn save(&self) -> ApiFuture<JsValue> {
460
597
  let this = self.clone();
461
598
  ApiFuture::new(async move {
462
- let viewer_config_task = this.get_viewer_config();
463
- let format = format
464
- .as_ref()
465
- .map(|x| ViewerConfigEncoding::from_str(x))
466
- .transpose()?;
599
+ let viewer_config = this
600
+ .renderer
601
+ .clone()
602
+ .with_lock(async { this.get_viewer_config().await })
603
+ .await?;
467
604
 
468
- let viewer_config = viewer_config_task.await?;
469
- viewer_config.encode(&format)
605
+ viewer_config.encode()
470
606
  })
471
607
  }
472
608
 
473
- /// Download this viewer's internal [`View`] data as a `.csv` file.
609
+ /// Download this viewer's internal [`View`] data via a browser download
610
+ /// event.
474
611
  ///
475
612
  /// # Arguments
476
613
  ///
477
- /// - `flat` - Whether to use the current [`perspective_js::JsViewConfig`]
478
- /// to generate this data, or use the default.
614
+ /// - `method` - The `ExportMethod` to use to render the data to download.
479
615
  ///
480
616
  /// # JavaScript Examples
481
617
  ///
@@ -484,16 +620,58 @@ impl PerspectiveViewerElement {
484
620
  /// await viewer.download();
485
621
  /// })
486
622
  /// ```
487
- pub fn download(&self, flat: Option<bool>) -> ApiFuture<()> {
488
- let session = self.session.clone();
623
+ pub fn download(&self, method: Option<JsString>) -> ApiFuture<()> {
624
+ let this = self.clone();
489
625
  ApiFuture::new(async move {
490
- let val = session
491
- .csv_as_jsvalue(flat.unwrap_or_default(), None)
492
- .await?
493
- .as_blob()?;
626
+ let method = if let Some(method) = method
627
+ .map(|x| x.unchecked_into())
628
+ .map(serde_wasm_bindgen::from_value)
629
+ {
630
+ method?
631
+ } else {
632
+ ExportMethod::Csv
633
+ };
494
634
 
495
- // TODO name.as_deref().unwrap_or("untitled.csv")
496
- download("untitled.csv", &val)
635
+ let blob = this.export_method_to_blob(method).await?;
636
+ let is_chart = this.renderer.is_chart();
637
+ download(
638
+ format!("untitled{}", method.as_filename(is_chart)).as_ref(),
639
+ &blob,
640
+ )
641
+ })
642
+ }
643
+
644
+ /// Exports this viewer's internal [`View`] as a JavaSript data, the
645
+ /// exact type of which depends on the `method` but defaults to `String`
646
+ /// in CSV format.
647
+ ///
648
+ /// This method is only really useful for the `"plugin"` method, which
649
+ /// will use the configured plugin's export (e.g. PNG for
650
+ /// `@perspective-dev/viewer-d3fc`). Otherwise, prefer to call the
651
+ /// equivalent method on the underlying [`View`] directly.
652
+ ///
653
+ /// # Arguments
654
+ ///
655
+ /// - `method` - The `ExportMethod` to use to render the data to download.
656
+ ///
657
+ /// # JavaScript Examples
658
+ ///
659
+ /// ```javascript
660
+ /// const data = await viewer.export("plugin");
661
+ /// ```
662
+ pub fn export(&self, method: Option<JsString>) -> ApiFuture<JsValue> {
663
+ let this = self.clone();
664
+ ApiFuture::new(async move {
665
+ let method = if let Some(method) = method
666
+ .map(|x| x.unchecked_into())
667
+ .map(serde_wasm_bindgen::from_value)
668
+ {
669
+ method?
670
+ } else {
671
+ ExportMethod::Csv
672
+ };
673
+
674
+ this.export_method_to_jsvalue(method).await
497
675
  })
498
676
  }
499
677
 
@@ -524,7 +702,7 @@ impl PerspectiveViewerElement {
524
702
  ExportMethod::Csv
525
703
  };
526
704
 
527
- let js_task = this.export_method_to_jsvalue(method);
705
+ let js_task = this.export_method_to_blob(method);
528
706
  copy_to_clipboard(js_task, MimeType::TextPlain).await
529
707
  })
530
708
  }
@@ -542,17 +720,17 @@ impl PerspectiveViewerElement {
542
720
  /// await viewer.reset();
543
721
  /// ```
544
722
  pub fn reset(&self, reset_all: Option<bool>) -> ApiFuture<()> {
545
- tracing::info!("Resetting config");
723
+ tracing::debug!("Resetting config");
546
724
  let root = self.root.clone();
547
725
  let all = reset_all.unwrap_or_default();
548
726
  ApiFuture::new(async move {
549
- let task = root
550
- .borrow()
727
+ let (sender, receiver) = channel::<()>();
728
+ root.borrow()
551
729
  .as_ref()
552
730
  .ok_or("Already deleted")?
553
- .send_message_async(move |x| PerspectiveViewerMsg::Reset(all, Some(x)));
731
+ .send_message(PerspectiveViewerMsg::Reset(all, Some(sender)));
554
732
 
555
- Ok(task.await?)
733
+ Ok(receiver.await?)
556
734
  })
557
735
  }
558
736
 
@@ -584,8 +762,18 @@ impl PerspectiveViewerElement {
584
762
  *self.resize_handle.borrow_mut() = None;
585
763
  }
586
764
 
587
- let renderer = self.renderer.clone();
588
- ApiFuture::new(async move { renderer.resize().await })
765
+ let state = self.clone_state();
766
+ ApiFuture::new(async move {
767
+ if !state.renderer().is_plugin_activated()? {
768
+ state
769
+ .update_and_render(ViewConfigUpdate::default())?
770
+ .await?;
771
+ } else {
772
+ state.renderer().resize().await?;
773
+ }
774
+
775
+ Ok(())
776
+ })
589
777
  }
590
778
 
591
779
  /// Sets the auto-size behavior of this component.
@@ -614,7 +802,8 @@ impl PerspectiveViewerElement {
614
802
  let handle = Some(ResizeObserverHandle::new(
615
803
  &self.elem,
616
804
  &self.renderer,
617
- self.root.borrow().as_ref().unwrap(),
805
+ &self.session,
806
+ &self.root,
618
807
  ));
619
808
  *self.resize_handle.borrow_mut() = handle;
620
809
  } else {
@@ -645,9 +834,11 @@ impl PerspectiveViewerElement {
645
834
  if autopause {
646
835
  let handle = Some(IntersectionObserverHandle::new(
647
836
  &self.elem,
837
+ &self.presentation,
648
838
  &self.session,
649
839
  &self.renderer,
650
840
  ));
841
+
651
842
  *self.intersection_handle.borrow_mut() = handle;
652
843
  } else {
653
844
  *self.intersection_handle.borrow_mut() = None;
@@ -666,7 +857,7 @@ impl PerspectiveViewerElement {
666
857
  pub fn setSelection(&self, window: Option<JsViewWindow>) -> ApiResult<()> {
667
858
  let window = window.map(|x| x.into_serde_ext()).transpose()?;
668
859
  if self.renderer.get_selection() != window {
669
- self.events.dispatch_select(window.as_ref())?;
860
+ self.custom_events.dispatch_select(window.as_ref())?;
670
861
  }
671
862
 
672
863
  self.renderer.set_selection(window);
@@ -742,8 +933,10 @@ impl PerspectiveViewerElement {
742
933
  .cloned();
743
934
 
744
935
  changed = presentation.set_theme_name(reset_theme.as_deref()).await? || changed;
745
- if changed && let Some(view) = session.get_view() {
746
- return renderer.restyle_all(&view).await;
936
+ if changed {
937
+ if let Some(view) = session.get_view() {
938
+ return renderer.restyle_all(&view).await;
939
+ }
747
940
  }
748
941
 
749
942
  Ok(JsValue::UNDEFINED)
@@ -789,13 +982,12 @@ impl PerspectiveViewerElement {
789
982
  let root = self.root.clone();
790
983
  ApiFuture::new(async move {
791
984
  let force = force.map(SettingsUpdate::Update);
792
- let task = root
793
- .borrow()
794
- .as_ref()
795
- .into_apierror()?
796
- .send_message_async(|x| PerspectiveViewerMsg::ToggleSettingsInit(force, Some(x)));
985
+ let (sender, receiver) = channel::<ApiResult<wasm_bindgen::JsValue>>();
986
+ root.borrow().as_ref().into_apierror()?.send_message(
987
+ PerspectiveViewerMsg::ToggleSettingsInit(force, Some(sender)),
988
+ );
797
989
 
798
- task.await.map_err(|_| JsValue::from("Cancelled"))?
990
+ receiver.await.map_err(|_| JsValue::from("Cancelled"))?
799
991
  })
800
992
  }
801
993
 
@@ -826,7 +1018,7 @@ impl PerspectiveViewerElement {
826
1018
  #[doc(hidden)]
827
1019
  #[allow(clippy::use_self)]
828
1020
  #[wasm_bindgen]
829
- pub fn get_model(&self) -> PerspectiveViewerElement {
1021
+ pub fn __get_model(&self) -> PerspectiveViewerElement {
830
1022
  self.clone()
831
1023
  }
832
1024
 
@@ -840,17 +1032,17 @@ impl PerspectiveViewerElement {
840
1032
  pub fn toggleColumnSettings(&self, column_name: String) -> ApiFuture<()> {
841
1033
  clone!(self.session, self.root);
842
1034
  ApiFuture::new(async move {
843
- let locator = session.metadata().get_column_locator(Some(column_name));
844
- let task = root
845
- .borrow()
846
- .as_ref()
847
- .into_apierror()?
848
- .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
1035
+ let locator = session.get_column_locator(Some(column_name));
1036
+ let (sender, receiver) = channel::<()>();
1037
+ root.borrow().as_ref().into_apierror()?.send_message(
1038
+ PerspectiveViewerMsg::OpenColumnSettings {
849
1039
  locator,
850
1040
  sender: Some(sender),
851
1041
  toggle: true,
852
- });
853
- task.await.map_err(|_| ApiError::from("Cancelled"))
1042
+ },
1043
+ );
1044
+
1045
+ receiver.await.map_err(|_| ApiError::from("Cancelled"))
854
1046
  })
855
1047
  }
856
1048
 
@@ -862,19 +1054,19 @@ impl PerspectiveViewerElement {
862
1054
  column_name: Option<String>,
863
1055
  toggle: Option<bool>,
864
1056
  ) -> ApiFuture<()> {
865
- clone!(self.session, self.root);
1057
+ let locator = self.get_column_locator(column_name);
1058
+ clone!(self.root);
866
1059
  ApiFuture::new(async move {
867
- let locator = session.metadata().get_column_locator(column_name);
868
- let task = root
869
- .borrow()
870
- .as_ref()
871
- .into_apierror()?
872
- .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
1060
+ let (sender, receiver) = channel::<()>();
1061
+ root.borrow().as_ref().into_apierror()?.send_message(
1062
+ PerspectiveViewerMsg::OpenColumnSettings {
873
1063
  locator,
874
1064
  sender: Some(sender),
875
1065
  toggle: toggle.unwrap_or_default(),
876
- });
877
- task.await.map_err(|_| ApiError::from("Cancelled"))
1066
+ },
1067
+ );
1068
+
1069
+ receiver.await.map_err(|_| ApiError::from("Cancelled"))
878
1070
  })
879
1071
  }
880
1072
  }