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