perspective_js/view.rs
1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use js_sys::{Array, ArrayBuffer, Function, Object};
14use perspective_client::{OnUpdateOptions, ViewWindow, assert_view_api};
15use wasm_bindgen::prelude::*;
16use wasm_bindgen_futures::spawn_local;
17
18#[cfg(doc)]
19use crate::table::Table;
20use crate::utils::{ApiFuture, ApiResult, JsValueSerdeExt, LocalPollLoop};
21
22#[wasm_bindgen]
23unsafe extern "C" {
24 #[wasm_bindgen(typescript_type = "ViewWindow")]
25 #[derive(Clone)]
26 pub type JsViewWindow;
27
28 #[wasm_bindgen(method, setter, js_name = "formatted")]
29 pub fn set_formatted(this: &JsViewWindow, x: bool);
30
31 #[wasm_bindgen(typescript_type = "OnUpdateOptions")]
32 pub type JsOnUpdateOptions;
33
34}
35
36impl From<ViewWindow> for JsViewWindow {
37 fn from(value: ViewWindow) -> Self {
38 JsViewWindow::from_serde_ext(&value)
39 .unwrap()
40 .unchecked_into()
41 }
42}
43
44/// The [`View`] struct is Perspective's query and serialization interface. It
45/// represents a query on the `Table`'s dataset and is always created from an
46/// existing `Table` instance via the [`Table::view`] method.
47///
48/// [`View`]s are immutable with respect to the arguments provided to the
49/// [`Table::view`] method; to change these parameters, you must create a new
50/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
51/// respect to the [`Table`]'s data, and will (within a conflation window)
52/// update with the latest state as its parent [`Table`] updates, including
53/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
54/// query parameters are composable, in that each parameter works independently
55/// _and_ in conjunction with each other, and there is no limit to the number of
56/// pivots, filters, etc. which can be applied.
57#[wasm_bindgen]
58#[derive(Clone)]
59pub struct View(pub(crate) perspective_client::View);
60
61assert_view_api!(View);
62
63impl From<perspective_client::View> for View {
64 fn from(value: perspective_client::View) -> Self {
65 View(value)
66 }
67}
68
69#[wasm_bindgen]
70impl View {
71 #[doc(hidden)]
72 pub fn __get_model(&self) -> View {
73 self.clone()
74 }
75
76 /// Returns an array of strings containing the column paths of the [`View`]
77 /// without any of the source columns.
78 ///
79 /// A column path shows the columns that a given cell belongs to after
80 /// pivots are applied.
81 #[wasm_bindgen]
82 pub async fn column_paths(&self) -> ApiResult<JsValue> {
83 let columns = self.0.column_paths().await?;
84 Ok(JsValue::from_serde_ext(&columns)?)
85 }
86
87 /// Delete this [`View`] and clean up all resources associated with it.
88 /// [`View`] objects do not stop consuming resources or processing
89 /// updates when they are garbage collected - you must call this method
90 /// to reclaim these.
91 #[wasm_bindgen]
92 pub async fn delete(self) -> ApiResult<()> {
93 self.0.delete().await?;
94 Ok(())
95 }
96
97 /// Returns this [`View`]'s _dimensions_, row and column count, as well as
98 /// those of the [`crate::Table`] from which it was derived.
99 ///
100 /// - `num_table_rows` - The number of rows in the underlying
101 /// [`crate::Table`].
102 /// - `num_table_columns` - The number of columns in the underlying
103 /// [`crate::Table`] (including the `index` column if this
104 /// [`crate::Table`] was constructed with one).
105 /// - `num_view_rows` - The number of rows in this [`View`]. If this
106 /// [`View`] has a `group_by` clause, `num_view_rows` will also include
107 /// aggregated rows.
108 /// - `num_view_columns` - The number of columns in this [`View`]. If this
109 /// [`View`] has a `split_by` clause, `num_view_columns` will include all
110 /// _column paths_, e.g. the number of `columns` clause times the number
111 /// of `split_by` groups.
112 #[wasm_bindgen]
113 pub async fn dimensions(&self) -> ApiResult<JsValue> {
114 let dimensions = self.0.dimensions().await?;
115 Ok(JsValue::from_serde_ext(&dimensions)?)
116 }
117
118 /// The expression schema of this [`View`], which contains only the
119 /// expressions created on this [`View`]. See [`View::schema`] for
120 /// details.
121 #[wasm_bindgen]
122 pub async fn expression_schema(&self) -> ApiResult<JsValue> {
123 let schema = self.0.expression_schema().await?;
124 Ok(JsValue::from_serde_ext(&schema)?)
125 }
126
127 /// A copy of the config object passed to the [`Table::view`] method which
128 /// created this [`View`].
129 #[wasm_bindgen]
130 pub async fn get_config(&self) -> ApiResult<JsValue> {
131 let config = self.0.get_config().await?;
132 Ok(JsValue::from_serde_ext(&config)?)
133 }
134
135 /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
136 ///
137 /// # Returns
138 ///
139 /// A tuple of [min, max], whose types are column and aggregate dependent.
140 #[wasm_bindgen]
141 pub async fn get_min_max(&self, name: String) -> ApiResult<Array> {
142 let result = self.0.get_min_max(name).await?;
143 Ok([result.0, result.1]
144 .iter()
145 .map(|x| js_sys::JSON::parse(x))
146 .collect::<Result<_, _>>()?)
147 }
148
149 /// The number of aggregated rows in this [`View`]. This is affected by the
150 /// "group_by" configuration parameter supplied to this view's contructor.
151 ///
152 /// # Returns
153 ///
154 /// The number of aggregated rows.
155 #[wasm_bindgen]
156 pub async fn num_rows(&self) -> ApiResult<i32> {
157 let size = self.0.num_rows().await?;
158 Ok(size as i32)
159 }
160
161 /// The schema of this [`View`].
162 ///
163 /// The [`View`] schema differs from the `schema` returned by
164 /// [`Table::schema`]; it may have different column names due to
165 /// `expressions` or `columns` configs, or it maye have _different
166 /// column types_ due to the application og `group_by` and `aggregates`
167 /// config. You can think of [`Table::schema`] as the _input_ schema and
168 /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
169 #[wasm_bindgen]
170 pub async fn schema(&self) -> ApiResult<JsValue> {
171 let schema = self.0.schema().await?;
172 Ok(JsValue::from_serde_ext(&schema)?)
173 }
174
175 /// Serializes a [`View`] to the Apache Arrow data format.
176 #[wasm_bindgen]
177 pub async fn to_arrow(&self, window: Option<JsViewWindow>) -> ApiResult<ArrayBuffer> {
178 let window = window.into_serde_ext::<Option<ViewWindow>>()?;
179 let arrow = self.0.to_arrow(window.unwrap_or_default()).await?;
180 Ok(js_sys::Uint8Array::from(&arrow[..])
181 .buffer()
182 .unchecked_into())
183 }
184
185 /// Serializes this [`View`] to a string of JSON data. Useful if you want to
186 /// save additional round trip serialize/deserialize cycles.
187 #[wasm_bindgen]
188 pub async fn to_columns_string(&self, window: Option<JsViewWindow>) -> ApiResult<String> {
189 let window = window.into_serde_ext::<Option<ViewWindow>>()?;
190 let json = self.0.to_columns_string(window.unwrap_or_default()).await?;
191 Ok(json)
192 }
193
194 /// Serializes this [`View`] to JavaScript objects in a column-oriented
195 /// format.
196 #[wasm_bindgen]
197 pub async fn to_columns(&self, window: Option<JsViewWindow>) -> ApiResult<Object> {
198 let json = self.to_columns_string(window).await?;
199 Ok(js_sys::JSON::parse(&json)?.unchecked_into())
200 }
201
202 /// Render this `View` as a JSON string.
203 #[wasm_bindgen]
204 pub async fn to_json_string(&self, window: Option<JsViewWindow>) -> ApiResult<String> {
205 let window = window.into_serde_ext::<Option<ViewWindow>>()?;
206 let json = self.0.to_json_string(window.unwrap_or_default()).await?;
207 Ok(json)
208 }
209
210 /// Serializes this [`View`] to JavaScript objects in a row-oriented
211 /// format.
212 #[wasm_bindgen]
213 pub async fn to_json(&self, window: Option<JsViewWindow>) -> ApiResult<Array> {
214 let json = self.to_json_string(window).await?;
215 Ok(js_sys::JSON::parse(&json)?.unchecked_into())
216 }
217
218 /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
219 /// formatted [`String`].
220 #[wasm_bindgen]
221 pub async fn to_ndjson(&self, window: Option<JsViewWindow>) -> ApiResult<String> {
222 let window = window.into_serde_ext::<Option<ViewWindow>>()?;
223 let ndjson = self.0.to_ndjson(window.unwrap_or_default()).await?;
224 Ok(ndjson)
225 }
226
227 /// Serializes this [`View`] to CSV data in a standard format.
228 #[wasm_bindgen]
229 pub async fn to_csv(&self, window: Option<JsViewWindow>) -> ApiResult<String> {
230 let window = window.into_serde_ext::<Option<ViewWindow>>()?;
231 Ok(self.0.to_csv(window.unwrap_or_default()).await?)
232 }
233
234 /// Register a callback with this [`View`]. Whenever the view's underlying
235 /// table emits an update, this callback will be invoked with an object
236 /// containing `port_id`, indicating which port the update fired on, and
237 /// optionally `delta`, which is the new data that was updated for each
238 /// cell or each row.
239 ///
240 /// # Arguments
241 ///
242 /// - `on_update` - A callback function invoked on update, which receives an
243 /// object with two keys: `port_id`, indicating which port the update was
244 /// triggered on, and `delta`, whose value is dependent on the mode
245 /// parameter.
246 /// - `options` - If this is provided as `OnUpdateOptions { mode:
247 /// Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
248 /// rows. Otherwise `delta` will be [`Option::None`].
249 ///
250 /// # JavaScript Examples
251 ///
252 /// ```javascript
253 /// // Attach an `on_update` callback
254 /// view.on_update((updated) => console.log(updated.port_id));
255 /// ```
256 ///
257 /// ```javascript
258 /// // `on_update` with row deltas
259 /// view.on_update((updated) => console.log(updated.delta), { mode: "row" });
260 /// ```
261 #[wasm_bindgen]
262 pub fn on_update(
263 &self,
264 on_update_js: Function,
265 options: Option<JsOnUpdateOptions>,
266 ) -> ApiFuture<u32> {
267 let poll_loop = LocalPollLoop::new(move |args| {
268 let js_obj = JsValue::from_serde_ext(&args)?;
269 on_update_js.call1(&JsValue::UNDEFINED, &js_obj)
270 });
271
272 let on_update = Box::new(move |msg| poll_loop.poll(msg));
273 let view = self.0.clone();
274 ApiFuture::new(async move {
275 let on_update_opts = options
276 .into_serde_ext::<Option<OnUpdateOptions>>()?
277 .unwrap_or_default();
278
279 let id = view.on_update(on_update, on_update_opts).await?;
280 Ok(id)
281 })
282 }
283
284 /// Unregister a previously registered update callback with this [`View`].
285 ///
286 /// # Arguments
287 ///
288 /// - `id` - A callback `id` as returned by a recipricol call to
289 /// [`View::on_update`].
290 #[wasm_bindgen]
291 pub async fn remove_update(&self, callback_id: u32) -> ApiResult<()> {
292 Ok(self.0.remove_update(callback_id).await?)
293 }
294
295 /// Register a callback with this [`View`]. Whenever the [`View`] is
296 /// deleted, this callback will be invoked.
297 #[wasm_bindgen]
298 pub fn on_delete(&self, on_delete: Function) -> ApiFuture<u32> {
299 let view = self.clone();
300 ApiFuture::new(async move {
301 let emit = LocalPollLoop::new(move |()| on_delete.call0(&JsValue::UNDEFINED));
302 let on_delete = Box::new(move || spawn_local(emit.poll(())));
303 Ok(view.0.on_delete(on_delete).await?)
304 })
305 }
306
307 /// The number of aggregated columns in this [`View`]. This is affected by
308 /// the "split_by" configuration parameter supplied to this view's
309 /// contructor.
310 ///
311 /// # Returns
312 ///
313 /// The number of aggregated columns.
314 #[wasm_bindgen]
315 pub async fn num_columns(&self) -> ApiResult<u32> {
316 // TODO: This is broken because of how split by creates a
317 // cartesian product of columns * unique values.
318 Ok(self.0.dimensions().await?.num_view_columns)
319 }
320
321 /// Unregister a previously registered [`View::on_delete`] callback.
322 #[wasm_bindgen]
323 pub fn remove_delete(&self, callback_id: u32) -> ApiFuture<()> {
324 let client = self.0.clone();
325 ApiFuture::new(async move {
326 client.remove_delete(callback_id).await?;
327 Ok(())
328 })
329 }
330
331 /// Collapses the `group_by` row at `row_index`.
332 #[wasm_bindgen]
333 pub async fn collapse(&self, row_index: u32) -> ApiResult<u32> {
334 Ok(self.0.collapse(row_index).await?)
335 }
336
337 /// Expand the `group_by` row at `row_index`.
338 #[wasm_bindgen]
339 pub async fn expand(&self, row_index: u32) -> ApiResult<u32> {
340 Ok(self.0.expand(row_index).await?)
341 }
342
343 /// Set expansion `depth` of the `group_by` tree.
344 #[wasm_bindgen]
345 pub async fn set_depth(&self, depth: u32) -> ApiResult<()> {
346 Ok(self.0.set_depth(depth).await?)
347 }
348}