perspective 2.0.0

A data visualization and analytics component, especially well-suited for large and/or streaming datasets.
////////////////////////////////////////////////////////////////////////////////
//
// 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;
#[cfg(test)]
use {crate::utils::*, wasm_bindgen_futures::future_to_promise, wasm_bindgen_test::*};

use crate::js::perspective::*;
use crate::js::plugin::*;
#[cfg(test)]
use crate::*;

pub async fn get_row_and_col_limits(
    view: &JsPerspectiveView,
    plugin_metadata: &ViewConfigRequirements,
) -> Result<(usize, usize, Option<usize>, Option<usize>), JsValue> {
    let dimensions = view.dimensions().await?;
    let num_cols = dimensions.num_view_columns() as usize;
    let num_rows = dimensions.num_view_rows() as usize;
    match (plugin_metadata.max_columns, plugin_metadata.render_warning) {
        (Some(_), false) => Ok((num_cols, num_rows, None, None)),
        (max_columns, _) => {
            let schema = view.schema().await?;
            let keys = js_sys::Object::keys(schema.unchecked_ref::<js_sys::Object>());
            let num_schema_columns = std::cmp::max(1, keys.length() as usize);
            let max_cols = max_columns.and_then(|max_columns| {
                let column_group_diff = max_columns % num_schema_columns;
                let column_limit = max_columns + column_group_diff;
                if column_limit < num_cols {
                    Some(column_limit)
                } else {
                    None
                }
            });

            let max_rows = plugin_metadata.max_cells.map(|max_cells| {
                match max_cols {
                    Some(max_cols) => max_cells as f64 / max_cols as f64,
                    None => max_cells as f64 / num_cols as f64,
                }
                .ceil() as usize
            });

            Ok((num_cols, num_rows, max_cols, max_rows))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn closure_helper<T>(x: T) -> Closure<dyn Fn(JsValue) -> js_sys::Promise>
    where
        T: Into<JsValue>,
    {
        let val = x.into();
        (move |_: JsValue| {
            clone!(val);
            future_to_promise(async move { Ok(val.clone()) })
        })
        .into_closure()
    }

    #[wasm_bindgen_test]
    pub async fn test_emtpy_schema_no_columns() {
        let closure = closure_helper(0);
        let closure2 = closure_helper(json!({}));
        let view = json!({
            "num_columns": closure.as_ref().unchecked_ref::<JsValue>(),
            "num_rows": closure.as_ref().unchecked_ref::<JsValue>(),
            "schema": closure2.as_ref().unchecked_ref::<JsValue>()
        })
        .unchecked_into::<JsPerspectiveView>();

        let reqs = ViewConfigRequirements {
            render_warning: true,
            ..ViewConfigRequirements::default()
        };
        let (_, _, max_cols, max_rows) = get_row_and_col_limits(&view, &reqs).await.unwrap();
        assert_eq!(max_cols, None);
        assert_eq!(max_rows, None);
    }

    #[wasm_bindgen_test]
    pub async fn test_columns_do_not_exceed_max_columns() {
        let closure = closure_helper(1);
        let closure2 = closure_helper(0);
        let closure3 = closure_helper(json!({}));
        let view = json!({
            "num_columns": closure.as_ref().unchecked_ref::<JsValue>(),
            "num_rows": closure2.as_ref().unchecked_ref::<JsValue>(),
            "schema": closure3.as_ref().unchecked_ref::<JsValue>()
        })
        .unchecked_into::<JsPerspectiveView>();

        let reqs = ViewConfigRequirements {
            max_columns: Some(2),
            render_warning: true,
            ..ViewConfigRequirements::default()
        };

        let (_, _, max_cols, max_rows) = get_row_and_col_limits(&view, &reqs).await.unwrap();
        assert_eq!(max_cols, None);
        assert_eq!(max_rows, None);
    }

    #[wasm_bindgen_test]
    pub async fn test_columns_exceed_max_columns() {
        let closure = closure_helper(2);
        let closure2 = closure_helper(0);
        let closure3 = closure_helper(json!({}));
        let view = json!({
            "num_columns": closure.as_ref().unchecked_ref::<JsValue>(),
            "num_rows": closure2.as_ref().unchecked_ref::<JsValue>(),
            "schema": closure3.as_ref().unchecked_ref::<JsValue>()
        })
        .unchecked_into::<JsPerspectiveView>();

        let reqs = ViewConfigRequirements {
            max_columns: Some(1),
            render_warning: true,
            ..ViewConfigRequirements::default()
        };
        let (_, _, max_cols, max_rows) = get_row_and_col_limits(&view, &reqs).await.unwrap();
        assert_eq!(max_cols, Some(1));
        assert_eq!(max_rows, None);
    }

    #[wasm_bindgen_test]
    pub async fn test_when_schema_columns_are_present() {
        let closure = closure_helper(100);
        let closure2 = closure_helper(0);
        let closure3 = closure_helper(json!({"x": "string", "y": "string"}));
        let view = json!({
            "num_columns": closure.as_ref().unchecked_ref::<JsValue>(),
            "num_rows": closure2.as_ref().unchecked_ref::<JsValue>(),
            "schema": closure3.as_ref().unchecked_ref::<JsValue>()
        })
        .unchecked_into::<JsPerspectiveView>();

        let reqs = ViewConfigRequirements {
            max_columns: Some(3),
            render_warning: true,
            ..ViewConfigRequirements::default()
        };
        let (_, _, max_cols, max_rows) = get_row_and_col_limits(&view, &reqs).await.unwrap();
        assert_eq!(max_cols, Some(4));
        assert_eq!(max_rows, None);
    }

    #[wasm_bindgen_test]
    pub async fn test_when_max_cells_exits() {
        let closure = closure_helper(1);
        let closure2 = closure_helper(0);
        let closure3 = closure_helper(json!({}));
        let view = json!({
            "num_columns": closure.as_ref().unchecked_ref::<JsValue>(),
            "num_rows": closure2.as_ref().unchecked_ref::<JsValue>(),
            "schema": closure3.as_ref().unchecked_ref::<JsValue>()
        })
        .unchecked_into::<JsPerspectiveView>();

        let reqs = ViewConfigRequirements {
            max_cells: Some(2),
            render_warning: true,
            ..ViewConfigRequirements::default()
        };
        let (_, _, max_cols, max_rows) = get_row_and_col_limits(&view, &reqs).await.unwrap();
        assert_eq!(max_cols, None);
        assert_eq!(max_rows, Some(2));
    }

    #[wasm_bindgen_test]
    pub async fn test_when_columns_exceed_max_columns_and_max_cells_exists() {
        let closure = closure_helper(4);
        let closure2 = closure_helper(0);
        let closure3 = closure_helper(json!({}));
        let view = json!({
            "num_columns": closure.as_ref().unchecked_ref::<JsValue>(),
            "num_rows": closure2.as_ref().unchecked_ref::<JsValue>(),
            "schema": closure3.as_ref().unchecked_ref::<JsValue>()
        })
        .unchecked_into::<JsPerspectiveView>();

        let reqs = ViewConfigRequirements {
            max_columns: Some(2),
            max_cells: Some(10),
            render_warning: true,
            ..ViewConfigRequirements::default()
        };

        let (_, _, max_cols, max_rows) = get_row_and_col_limits(&view, &reqs).await.unwrap();
        assert_eq!(max_cols, Some(2));
        assert_eq!(max_rows, Some(5));
    }
}