diff --git a/packages/perspective-viewer-datagrid/package.json b/packages/perspective-viewer-datagrid/package.json index 549606da68..fdc0efd277 100644 --- a/packages/perspective-viewer-datagrid/package.json +++ b/packages/perspective-viewer-datagrid/package.json @@ -41,9 +41,9 @@ "dependencies": { "@finos/perspective": "^0.6.0", "@finos/perspective-viewer": "^0.6.0", - "regular-table": "=0.1.5" + "regular-table": "=0.2.1" }, "devDependencies": { "@finos/perspective-test": "^0.6.0" } -} +} \ No newline at end of file diff --git a/packages/perspective-viewer-datagrid/src/js/plugin.js b/packages/perspective-viewer-datagrid/src/js/plugin.js index a7881f6890..d6f5c0bab7 100644 --- a/packages/perspective-viewer-datagrid/src/js/plugin.js +++ b/packages/perspective-viewer-datagrid/src/js/plugin.js @@ -10,7 +10,7 @@ import {registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; import "regular-table"; -import {createModel, configureRegularTable, formatters} from "regular-table/dist/examples/perspective.js"; +import {createModel, configureRegularTable, formatters} from "./regular_table_handlers.js"; import MATERIAL_STYLE from "../less/regular_table.less"; import {configureRowSelectable, deselect} from "./row_selection.js"; @@ -32,8 +32,6 @@ function lock(body) { try { lock = new Promise(x => (resolve = x)); await body.apply(this, args); - } catch (e) { - throw e; } finally { lock = undefined; resolve(); @@ -59,7 +57,7 @@ const datagridPlugin = lock(async function(regular, viewer, view) { } try { - const draw = regular.draw({swap: true}); + const draw = regular.draw({invalid_columns: true}); if (!model._preserve_focus_state) { regular.scrollTop = 0; regular.scrollLeft = 0; @@ -71,7 +69,9 @@ const datagridPlugin = lock(async function(regular, viewer, view) { await draw; } catch (e) { - console.error(e); + if (e.message !== "View is not initialized") { + throw e; + } } }); @@ -122,16 +122,20 @@ class DatagridPlugin { model._num_rows = await model._view.num_rows(); await datagrid.draw(); } catch (e) { - return; + if (e.message !== "View is not initialized") { + throw e; + } } } static async create(div, view) { - const datagrid = get_or_create_datagrid(this, div); try { + const datagrid = get_or_create_datagrid(this, div); await datagridPlugin(datagrid, this, view); } catch (e) { - return; + if (e.message !== "View is not initialized") { + throw e; + } } } @@ -141,7 +145,9 @@ class DatagridPlugin { try { await datagrid.draw(); } catch (e) { - return; + if (e.message !== "View is not initialized") { + throw e; + } } } } diff --git a/packages/perspective-viewer-datagrid/src/js/regular_table_handlers.js b/packages/perspective-viewer-datagrid/src/js/regular_table_handlers.js new file mode 100644 index 0000000000..cb7e89d448 --- /dev/null +++ b/packages/perspective-viewer-datagrid/src/js/regular_table_handlers.js @@ -0,0 +1,261 @@ +/****************************************************************************** + * + * 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. + * + */ + +import {get_type_config} from "@finos/perspective/dist/esm/config/index.js"; + +function styleListener(regularTable) { + const header_depth = regularTable._view_cache.config.row_pivots.length - 1; + let group_headers = Array.from(regularTable.children[0].children[0].children); + let [col_headers] = group_headers.splice(group_headers.length - 1, 1); + for (const td of col_headers?.children) { + const metadata = regularTable.getMeta(td); + const sort = this._config.sort.find(x => x[0] === metadata.column_header[metadata.column_header.length - 1]); + let needs_border = metadata.row_header_x === header_depth; + needs_border = needs_border || (metadata.x + 1) % this._config.columns.length === 0; + td.classList.toggle("psp-header-border", needs_border); + td.classList.toggle("psp-header-group", false); + td.classList.toggle("psp-header-leaf", true); + td.classList.toggle("psp-header-corner", typeof metadata.x === "undefined"); + td.classList.toggle("psp-header-sort-asc", !!sort && sort[1] === "asc"); + td.classList.toggle("psp-header-sort-desc", !!sort && sort[1] === "desc"); + td.classList.toggle("psp-header-sort-col-asc", !!sort && sort[1] === "col asc"); + td.classList.toggle("psp-header-sort-col-desc", !!sort && sort[1] === "col desc"); + + let type = get_psp_type.call(this, metadata); + const is_numeric = type === "integer" || type === "float"; + const float_val = is_numeric && parseFloat(metadata.value); + td.classList.toggle("psp-align-right", is_numeric); + td.classList.toggle("psp-align-left", !is_numeric); + td.classList.toggle("psp-positive", float_val > 0); + td.classList.toggle("psp-negative", float_val < 0); + } + + for (const tr of group_headers) { + for (const td of tr.children) { + const metadata = regularTable.getMeta(td); + let needs_border = metadata.row_header_x === header_depth || metadata.x >= 0; + td.classList.toggle("psp-header-group", true); + td.classList.toggle("psp-header-leaf", false); + td.classList.toggle("psp-header-border", needs_border); + } + } + + for (const tr of regularTable.children[0].children[1].children) { + for (const td of tr.children) { + const metadata = regularTable.getMeta(td); + if (td.tagName === "TH") { + const is_not_empty = !!metadata.value && metadata.value.toString().trim().length > 0; + const is_leaf = metadata.row_header_x >= this._config.row_pivots.length; + const next = regularTable.getMeta({dx: 0, dy: metadata.y - metadata.y0 + 1}); + const is_collapse = next && next.row_header && typeof next.row_header[metadata.row_header_x + 1] !== "undefined"; + td.classList.toggle("psp-tree-label", is_not_empty && !is_leaf); + td.classList.toggle("psp-tree-label-expand", is_not_empty && !is_leaf && !is_collapse); + td.classList.toggle("psp-tree-label-collapse", is_not_empty && !is_leaf && is_collapse); + td.classList.toggle("psp-tree-leaf", is_not_empty && is_leaf); + } + + let type = get_psp_type.call(this, metadata); + const is_numeric = type === "integer" || type === "float"; + const float_val = is_numeric && parseFloat(metadata.value); + td.classList.toggle("psp-align-right", is_numeric); + td.classList.toggle("psp-align-left", !is_numeric); + td.classList.toggle("psp-positive", float_val > 0); + td.classList.toggle("psp-negative", float_val < 0); + } + } +} + +function get_psp_type(metadata) { + if (metadata.x >= 0) { + const column_path = this._column_paths[metadata.x]; + const column_path_parts = column_path.split("|"); + return this._schema[column_path_parts[column_path_parts.length - 1]]; + } else { + const column_path = this._config.row_pivots[metadata.row_header_x - 1]; + return this._table_schema[column_path]; + } +} + +async function sortHandler(regularTable, event) { + const meta = regularTable.getMeta(event.target); + const column_name = meta.column_header[meta.column_header.length - 1]; + const sort_method = event.shiftKey ? append_sort : override_sort; + const sort = sort_method.call(this, column_name); + regularTable.dispatchEvent(new CustomEvent("regular-table-psp-sort", {detail: {sort}})); +} + +function append_sort(column_name) { + const sort = []; + let found = false; + for (const sort_term of this._config.sort) { + const [_column_name, _sort_dir] = sort_term; + if (_column_name === column_name) { + found = true; + const term = create_sort.call(this, column_name, _sort_dir); + if (term) { + sort.push(term); + } + } else { + sort.push(sort_term); + } + } + if (!found) { + sort.push([column_name, "desc"]); + } + return sort; +} +function override_sort(column_name) { + for (const [_column_name, _sort_dir] of this._config.sort) { + if (_column_name === column_name) { + const sort = create_sort.call(this, column_name, _sort_dir); + return sort ? [sort] : []; + } + } + return [[column_name, "desc"]]; +} +function create_sort(column_name, sort_dir) { + const is_col_sortable = this._config.column_pivots.length > 0; + const order = is_col_sortable ? ROW_COL_SORT_ORDER : ROW_SORT_ORDER; + const inc_sort_dir = sort_dir ? order[sort_dir] : "desc"; + if (inc_sort_dir) { + return [column_name, inc_sort_dir]; + } +} + +const ROW_SORT_ORDER = {desc: "asc", asc: undefined}; +const ROW_COL_SORT_ORDER = {desc: "asc", asc: "col desc", "col desc": "col asc", "col asc": undefined}; + +async function expandCollapseHandler(regularTable, event) { + const meta = regularTable.getMeta(event.target); + const is_collapse = event.target.classList.contains("psp-tree-label-collapse"); + if (event.shiftKey && is_collapse) { + this._view.set_depth(meta.row_header.filter(x => x !== undefined).length - 2); + } else if (event.shiftKey) { + this._view.set_depth(meta.row_header.filter(x => x !== undefined).length - 1); + } else if (is_collapse) { + this._view.collapse(meta.y); + } else { + this._view.expand(meta.y); + } + this._num_rows = await this._view.num_rows(); + this._num_columns = await this._view.num_columns(); + regularTable.draw(); +} + +function mousedownListener(regularTable, event) { + if (event.target.classList.contains("psp-tree-label") && event.offsetX < 26) { + expandCollapseHandler.call(this, regularTable, event); + event.handled = true; + } else if (event.target.classList.contains("psp-header-leaf") && !event.target.classList.contains("psp-header-corner")) { + sortHandler.call(this, regularTable, event); + event.handled = true; + } +} + +const FORMATTERS = {}; + +const FORMATTER_CONS = { + datetime: Intl.DateTimeFormat, + date: Intl.DateTimeFormat, + integer: Intl.NumberFormat, + float: Intl.NumberFormat +}; + +export const formatters = FORMATTERS; + +function _format(parts, val, use_table_schema = false) { + if (val === null) { + return "-"; + } + const title = parts[parts.length - 1]; + const type = (use_table_schema && this._table_schema[title]) || this._schema[title] || "string"; + if (FORMATTERS[type] === undefined) { + const type_config = get_type_config(type); + if (FORMATTER_CONS[type] && type_config.format) { + FORMATTERS[type] = new FORMATTER_CONS[type]("en-us", type_config.format); + } else { + FORMATTERS[type] = false; + } + } + + return FORMATTERS[type] ? FORMATTERS[type].format(val) : val; +} + +function* _tree_header(paths = [], row_headers) { + for (let path of paths) { + path = ["TOTAL", ...path]; + const last = path[path.length - 1]; + path = path.slice(0, path.length - 1).fill(""); + const formatted = _format.call(this, [row_headers[path.length - 1]], last, true); + path = path.concat({toString: () => formatted}); + path.length = row_headers.length + 1; + yield path; + } +} + +async function dataListener(x0, y0, x1, y1) { + let columns = {}; + if (x1 - x0 > 0 && y1 - y0 > 0) { + columns = await this._view.to_columns({ + start_row: y0, + start_col: x0, + end_row: y1, + end_col: x1, + id: true + }); + this._ids = columns.__ID__; + } + const data = []; + const column_headers = []; + for (const path of this._column_paths.slice(x0, x1)) { + const path_parts = path.split("|"); + const column = columns[path] || new Array(y1 - y0).fill(null); + data.push(column.map(x => _format.call(this, path_parts, x))); + column_headers.push(path_parts); + } + return { + num_rows: this._num_rows, + num_columns: this._column_paths.length, + row_headers: Array.from(_tree_header.call(this, columns.__ROW_PATH__, this._config.row_pivots)), + column_headers, + data + }; +} + +export async function createModel(regular, table, view, extend = {}) { + const config = await view.get_config(); + const [table_schema, table_computed_schema, num_rows, schema, computed_schema, column_paths] = await Promise.all([ + table.schema(), + table.computed_schema(config.computed_columns), + view.num_rows(), + view.schema(), + view.computed_schema(), + view.column_paths() + ]); + const model = Object.assign(extend, { + _view: view, + _table: table, + _table_schema: {...table_schema, ...table_computed_schema}, + _config: config, + _num_rows: num_rows, + _schema: {...schema, ...computed_schema}, + _ids: [], + _column_paths: column_paths.filter(path => { + return path !== "__ROW_PATH__" && path !== "__ID__"; + }) + }); + regular.setDataListener(dataListener.bind(model)); + return model; +} + +export async function configureRegularTable(regular, model) { + regular.addStyleListener(styleListener.bind(model, regular)); + regular.addEventListener("mousedown", mousedownListener.bind(model, regular)); + await regular.draw(); +} diff --git a/packages/perspective/src/config/common.config.js b/packages/perspective/src/config/common.config.js index d987669b1f..67809f02ea 100644 --- a/packages/perspective/src/config/common.config.js +++ b/packages/perspective/src/config/common.config.js @@ -8,6 +8,11 @@ function common({no_minify, inline} = {}) { plugins: plugins, module: { rules: [ + // { + // test: /\.js$/, + // enforce: "pre", + // use: ["source-map-loader"] + // }, { test: /\.less$/, exclude: /node_modules\/(?!regular-table)/, diff --git a/yarn.lock b/yarn.lock index 9a0dd713f5..b0169d9cd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2480,6 +2480,7 @@ write-file-atomic "^2.3.0" "@lumino/algorithm@^1.2.0", "@lumino/algorithm@^1.3.3", "@lumino/messaging@^1.2.1", "@lumino/messaging@^1.3.3": + name "@lumino/algorithm" version "1.3.3" resolved "https://registry.yarnpkg.com/@lumino/algorithm/-/algorithm-1.3.3.tgz#fdf4daa407a1ce6f233e173add6a2dda0c99eef4" integrity sha512-I2BkssbOSLq3rDjgAC3fzf/zAIwkRUnAh60MO0lYcaFdSGyI15w4K3gwZHGIO0p9cKEiNHLXKEODGmOjMLOQ3g== @@ -13990,10 +13991,10 @@ regjsparser@^0.6.4: dependencies: jsesc "~0.5.0" -regular-table@=0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regular-table/-/regular-table-0.1.5.tgz#11e4ba9fd799800006d27a9f5415bf33c0cd0246" - integrity sha512-cNBVD8Qw/o9rqs3h4bS/4cnDSyH5egAcBT2bJw7ihKQQFIz6MtJoE9iIC/kozTPlIK//EiFw5C4VKKkckElwmQ== +regular-table@=0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/regular-table/-/regular-table-0.2.1.tgz#af3d1efeeebe7d8071eda4e0dda689b4683ae156" + integrity sha512-24cKRtO/Gkj1p16mvqvkqWCTJSngAtFhYea7bax9XzNjTOy2DAF/507bQE+5zjq/qydfFXOUpTjnAjyIC1E5ug== relateurl@0.2.x, relateurl@^0.2.7: version "0.2.7"