// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { test, expect } from "@perspective-dev/test"; import perspective from "./perspective_client"; const data = { w: [1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5], x: [1, 2, 3, 4, 4, 3, 2, 1], y: ["a", "b", "c", "d", "a", "b", "c", "d"], z: [true, false, true, false, true, false, true, false], }; const data2 = { w: [3.5, 4.5, null, null, null, null, 1.5, 2.5], x: [1, 2, 3, 4, 4, 3, 2, 1], y: ["a", "b", "c", "d", "e", "f", "g", "h"], }; const data3 = { w: [3.5, 4.5, null, null, null, null, 1.5, 2.5], x: [1, 2, 3, 4, 4, 3, 2, 1], y: ["a", "a", "a", "a", "b", "b", "b", "b"], }; ((perspective) => { test.describe("Sorts", function () { test.describe("With nulls", () => { test("asc", async function () { var table = await perspective.table(data2); var view = await table.view({ columns: ["x", "w"], sort: [["w", "asc"]], }); const json = await view.to_columns(); expect(json).toEqual({ w: [null, null, null, null, 1.5, 2.5, 3.5, 4.5], x: [3, 4, 4, 3, 2, 1, 1, 2], }); view.delete(); table.delete(); }); test("desc", async function () { var table = await perspective.table(data2); var view = await table.view({ columns: ["x", "w"], sort: [["w", "desc"]], }); const json = await view.to_columns(); expect(json).toEqual({ w: [4.5, 3.5, 2.5, 1.5, null, null, null, null], x: [2, 1, 1, 2, 3, 4, 4, 3], }); view.delete(); table.delete(); }); test("asc datetime", async function () { var table = await perspective.table({ w: [ new Date(2020, 0, 1, 12, 30, 45), new Date(2020, 0, 1), null, null, null, null, new Date(2008, 0, 1, 12, 30, 45), new Date(2020, 12, 1, 12, 30, 45), ], x: [1, 2, 3, 4, 4, 3, 2, 1], y: ["a", "b", "c", "d", "e", "f", "g", "h"], }); var view = await table.view({ columns: ["x", "w"], sort: [["w", "asc"]], }); const json = await view.to_columns(); expect(json).toEqual({ w: [ null, null, null, null, new Date(2008, 0, 1, 12, 30, 45).getTime(), new Date(2020, 0, 1).getTime(), new Date(2020, 0, 1, 12, 30, 45).getTime(), new Date(2020, 12, 1, 12, 30, 45).getTime(), ], x: [3, 4, 4, 3, 2, 2, 1, 1], }); view.delete(); table.delete(); }); test("desc datetime", async function () { var table = await perspective.table({ w: [ new Date(2020, 0, 1, 12, 30, 45), new Date(2020, 0, 1), null, null, null, null, new Date(2008, 0, 1, 12, 30, 45), new Date(2020, 12, 1, 12, 30, 45), ], x: [1, 2, 3, 4, 4, 3, 2, 1], y: ["a", "b", "c", "d", "e", "f", "g", "h"], }); var view = await table.view({ columns: ["x", "w"], sort: [["w", "desc"]], }); const json = await view.to_columns(); expect(json).toEqual({ w: [ new Date(2020, 12, 1, 12, 30, 45).getTime(), new Date(2020, 0, 1, 12, 30, 45).getTime(), new Date(2020, 0, 1).getTime(), new Date(2008, 0, 1, 12, 30, 45).getTime(), null, null, null, null, ], x: [1, 1, 2, 2, 3, 4, 4, 3], }); view.delete(); table.delete(); }); }); test.describe("With aggregates", function () { test.describe("aggregates, in a sorted column with nulls", function () { test("sum", async function () { var table = await perspective.table(data2); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "sum", x: "unique", }, sort: [["w", "asc"]], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [ [], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["a"], ["b"], ], w: [12, 0, 0, 0, 0, 1.5, 2.5, 3.5, 4.5], x: [null, 3, 4, 4, 3, 2, 1, 1, 2], }); view.delete(); table.delete(); }); test("sum of floats", async function () { var table = await perspective.table({ w: [3.25, 4.51, null, null, null, null, 1.57, 2.59], x: [1, 2, 3, 4, 4, 3, 2, 1], y: ["a", "b", "c", "d", "e", "f", "g", "h"], }); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "sum", x: "unique", }, sort: [["w", "asc"]], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [ [], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["a"], ["b"], ], w: [11.92, 0, 0, 0, 0, 1.57, 2.59, 3.25, 4.51], x: [null, 3, 4, 4, 3, 2, 1, 1, 2], }); view.delete(); table.delete(); }); test("unique", async function () { var table = await perspective.table(data2); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "unique", x: "unique", }, sort: [["w", "asc"]], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [ [], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["a"], ["b"], ], w: [null, null, null, null, null, 1.5, 2.5, 3.5, 4.5], x: [null, 3, 4, 4, 3, 2, 1, 1, 2], }); view.delete(); table.delete(); }); test("avg", async function () { var table = await perspective.table(data2); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "avg", x: "unique", }, sort: [["w", "asc"]], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [ [], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["a"], ["b"], ], w: [3, null, null, null, null, 1.5, 2.5, 3.5, 4.5], x: [null, 3, 4, 4, 3, 2, 1, 1, 2], }); // Broken result: // { // __ROW_PATH__: [[], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"]], // w: [3, 3.5, 4.5, null, null, null, null, 1.5, 2.5], // x: [null, 1, 2, 3, 4, 4, 3, 2, 1] // }; view.delete(); table.delete(); }); test.describe("Multiple hidden sort", () => { test("sum", async function () { var table = await perspective.table(data3); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "sum", }, sort: [ ["x", "desc"], ["w", "desc"], ], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [[], ["a"], ["b"]], w: [12, 8, 4], x: [20, 10, 10], }); view.delete(); table.delete(); }); test("sum of floats", async function () { var table = await perspective.table({ w: [3.25, 4.51, null, null, null, null, 1.57, 2.59], x: [1, 2, 3, 4, 4, 3, 2, 1], y: ["a", "a", "a", "a", "b", "b", "b", "b"], }); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "sum", }, sort: [ ["x", "desc"], ["w", "desc"], ], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [[], ["a"], ["b"]], w: [11.92, 7.76, 4.16], x: [20, 10, 10], }); view.delete(); table.delete(); }); test("unique", async function () { var table = await perspective.table(data3); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "unique", }, sort: [ ["x", "desc"], ["w", "desc"], ], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [[], ["a"], ["b"]], w: [null, null, null], x: [20, 10, 10], }); view.delete(); table.delete(); }); test("avg", async function () { var table = await perspective.table(data3); var view = await table.view({ columns: ["x", "w"], group_by: ["y"], aggregates: { w: "avg", }, sort: [ ["x", "desc"], ["w", "desc"], ], }); const json = await view.to_columns(); expect(json).toEqual({ __ROW_PATH__: [[], ["a"], ["b"]], // 4 and 2 are the avg of the non-null rows w: [3, 4, 2], x: [20, 10, 10], }); view.delete(); table.delete(); }); }); }); }); test.describe("On hidden columns", function () { test("Column path should not emit hidden sorts", async function () { var table = await perspective.table(data); var view = await table.view({ columns: ["w", "y"], sort: [["x", "desc"]], }); const paths = await view.column_paths(); expect(paths).toEqual(["w", "y"]); view.delete(); table.delete(); }); test("Column path should not emit hidden column sorts", async function () { var table = await perspective.table(data); var view = await table.view({ columns: ["w"], group_by: ["y"], split_by: ["z"], sort: [["x", "col desc"]], }); const paths = await view.column_paths(); expect(paths).toEqual(["false|w", "true|w"]); view.delete(); table.delete(); }); test("Column path should not emit hidden regular and column sorts", async function () { var table = await perspective.table(data); var view = await table.view({ columns: ["w"], group_by: ["y"], split_by: ["z"], sort: [ ["x", "col desc"], ["y", "desc"], ], }); const paths = await view.column_paths(); expect(paths).toEqual(["false|w", "true|w"]); view.delete(); table.delete(); }); test("unpivoted", async function () { var table = await perspective.table(data); var view = await table.view({ columns: ["w", "y"], sort: [["x", "desc"]], }); var answer = [ { w: 4.5, y: "d" }, { w: 5.5, y: "a" }, { w: 3.5, y: "c" }, { w: 6.5, y: "b" }, { w: 2.5, y: "b" }, { w: 7.5, y: "c" }, { w: 1.5, y: "a" }, { w: 8.5, y: "d" }, ]; let result = await view.to_json(); expect(result).toEqual(answer); view.delete(); table.delete(); }); test("group by ['y']", async function () { var table = await perspective.table(data); var view = await table.view({ columns: ["w"], group_by: ["y"], sort: [["x", "desc"]], }); var answer = [ { __ROW_PATH__: [], w: 40 }, { __ROW_PATH__: ["a"], w: 7 }, { __ROW_PATH__: ["b"], w: 9 }, { __ROW_PATH__: ["c"], w: 11 }, { __ROW_PATH__: ["d"], w: 13 }, ]; let result = await view.to_json(); expect(result).toEqual(answer); view.delete(); table.delete(); }); test("group by and hidden sort ['y'] with aggregates specified", async function () { const table = await perspective.table({ x: [1, 2, 3, 4, 5], y: ["a", "b", "b", "b", "c"], }); // Aggregate should be overriden if the sort column is hidden // AND also in group by const view = await table.view({ aggregates: { x: "sum", y: "count", }, columns: ["x"], group_by: ["y"], sort: [["y", "desc"]], }); const answer = [ { __ROW_PATH__: [], x: 15 }, { __ROW_PATH__: ["c"], x: 5 }, { __ROW_PATH__: ["b"], x: 9 }, { __ROW_PATH__: ["a"], x: 1 }, ]; const result = await view.to_json(); expect(result).toEqual(answer); view.delete(); table.delete(); }); test("group by and hidden sort ['y'] without aggregates specified", async function () { const table = await perspective.table({ x: [1, 2, 3, 4, 5], y: ["a", "b", "b", "b", "c"], }); const view = await table.view({ columns: ["x"], group_by: ["y"], sort: [["y", "desc"]], }); const answer = [ { __ROW_PATH__: [], x: 15 }, { __ROW_PATH__: ["c"], x: 5 }, { __ROW_PATH__: ["b"], x: 9 }, { __ROW_PATH__: ["a"], x: 1 }, ]; const result = await view.to_json(); expect(result).toEqual(answer); view.delete(); table.delete(); }); test("group by with multiple hidden sorts emits all visible columns in to_columns()", async function () { // https://github.com/perspective-dev/perspective/issues/3195 const table = await perspective.table({ v1: [1, 2, 3, 4], v2: [10, 20, 30, 40], v3: [100, 200, 300, 400], h1: [1.5, 2.5, 3.5, 4.5], h2: [4.5, 3.5, 2.5, 1.5], g: ["a", "a", "b", "b"], }); const view = await table.view({ columns: ["v1", "v2", "v3"], group_by: ["g"], sort: [ ["h1", "desc"], ["h2", "desc"], ], }); const result = await view.to_columns(); expect(result).toEqual({ __ROW_PATH__: [[], ["b"], ["a"]], v1: [10, 7, 3], v2: [100, 70, 30], v3: [1000, 700, 300], }); view.delete(); table.delete(); }); test("group by with more hidden sorts than visible columns does not emit hidden columns in to_columns()", async function () { // https://github.com/perspective-dev/perspective/issues/3195 const table = await perspective.table({ v1: [1, 2, 3, 4], h1: [1.5, 2.5, 3.5, 4.5], h2: [4.5, 3.5, 2.5, 1.5], g: ["a", "a", "b", "b"], }); const view = await table.view({ columns: ["v1"], group_by: ["g"], sort: [ ["h1", "desc"], ["h2", "desc"], ], }); const result = await view.to_columns(); expect(result).toEqual({ __ROW_PATH__: [[], ["b"], ["a"]], v1: [10, 7, 3], }); view.delete(); table.delete(); }); test("split by ['y']", async function () { const table = await perspective.table(data); const view = await table.view({ columns: ["w"], split_by: ["y"], sort: [["x", "desc"]], }); const paths = await view.column_paths(); expect(paths).toEqual(["a|w", "b|w", "c|w", "d|w"]); const answer = [ { "a|w": null, "b|w": null, "c|w": null, "d|w": 4.5 }, { "a|w": 5.5, "b|w": null, "c|w": null, "d|w": null }, { "a|w": null, "b|w": null, "c|w": 3.5, "d|w": null }, { "a|w": null, "b|w": 6.5, "c|w": null, "d|w": null }, { "a|w": null, "b|w": 2.5, "c|w": null, "d|w": null }, { "a|w": null, "b|w": null, "c|w": 7.5, "d|w": null }, { "a|w": 1.5, "b|w": null, "c|w": null, "d|w": null }, { "a|w": null, "b|w": null, "c|w": null, "d|w": 8.5 }, ]; const result = await view.to_json(); expect(result).toEqual(answer); view.delete(); table.delete(); }); test("split by ['y'], col desc sort", async function () { const table = await perspective.table(data); const view = await table.view({ columns: ["w"], split_by: ["y"], sort: [["x", "col desc"]], }); const paths = await view.column_paths(); expect(paths).toEqual(["d|w", "c|w", "b|w", "a|w"]); const answer = { "d|w": [null, null, null, 4.5, null, null, null, 8.5], "c|w": [null, null, 3.5, null, null, null, 7.5, null], "b|w": [null, 2.5, null, null, null, 6.5, null, null], "a|w": [1.5, null, null, null, 5.5, null, null, null], }; const result = await view.to_columns(); expect(result).toEqual(answer); view.delete(); table.delete(); }); test("split by and hidden sort ['y']", async function () { const table = await perspective.table({ x: [1, 2, 3, 4], y: ["a", "a", "a", "b"], }); const view = await table.view({ columns: ["x"], split_by: ["y"], sort: [["y", "desc"]], }); const paths = await view.column_paths(); // regular non-col sort should not change order of column paths expect(paths).toEqual(["a|x", "b|x"]); const result = await view.to_columns(); expect(result).toEqual({ "a|x": [null, 1, 2, 3], "b|x": [4, null, null, null], }); view.delete(); table.delete(); }); test("split by and hidden col sort ['y']", async function () { const table = await perspective.table({ x: [1, 2, 3, 4], y: ["a", "a", "a", "b"], }); const view = await table.view({ columns: ["x"], split_by: ["y"], sort: [["y", "col desc"]], }); const paths = await view.column_paths(); expect(paths).toEqual(["b|x", "a|x"]); const result = await view.to_columns(); expect(result).toEqual({ "b|x": [null, null, null, 4], "a|x": [1, 2, 3, null], }); view.delete(); table.delete(); }); test("split by and hidden sort ['y'] with aggregates specified", async function () { const table = await perspective.table({ x: [1, 2, 3, 4], y: ["a", "a", "a", "b"], }); // Aggregate for hidden sort should be ignored in column-only, // so just make sure we stick to that. const view = await table.view({ columns: ["x"], split_by: ["y"], sort: [["y", "desc"]], aggregates: { y: "count", }, }); const paths = await view.column_paths(); // regular non-col sort should not change order of column paths expect(paths).toEqual(["a|x", "b|x"]); const result = await view.to_columns(); expect(result).toEqual({ "a|x": [null, 1, 2, 3], "b|x": [4, null, null, null], }); view.delete(); table.delete(); }); test("split by and hidden col sort ['y'] with aggregates specified", async function () { const table = await perspective.table({ x: [1, 2, 3, 4], y: ["a", "a", "a", "b"], }); // Aggregate for hidden sort should be ignored in column-only, // so just make sure we stick to that. const view = await table.view({ columns: ["x"], split_by: ["y"], sort: [["y", "col desc"]], aggregates: { y: "count", }, }); const paths = await view.column_paths(); expect(paths).toEqual(["b|x", "a|x"]); const result = await view.to_columns(); expect(result).toEqual({ "b|x": [null, null, null, 4], "a|x": [1, 2, 3, null], }); view.delete(); table.delete(); }); test("split by ['y'] with overridden aggregates", async function () { const table = await perspective.table({ x: [1, 2, 3, 4], y: ["a", "a", "a", "b"], }); const view = await table.view({ columns: ["x"], split_by: ["y"], aggregates: { y: "count" }, sort: [["y", "col desc"]], }); const paths = await view.column_paths(); // Col sort will override aggregate expect(paths).toEqual(["b|x", "a|x"]); let result = await view.to_columns(); expect(result).toEqual({ "b|x": [null, null, null, 4], "a|x": [1, 2, 3, null], }); view.delete(); table.delete(); }); test("split by ['y'] with extra aggregates", async function () { var table = await perspective.table(data); var view = await table.view({ columns: ["w"], split_by: ["y"], aggregates: { w: "sum", z: "last" }, sort: [["x", "desc"]], }); var answer = [ { "a|w": null, "b|w": null, "c|w": null, "d|w": 4.5 }, { "a|w": 5.5, "b|w": null, "c|w": null, "d|w": null }, { "a|w": null, "b|w": null, "c|w": 3.5, "d|w": null }, { "a|w": null, "b|w": 6.5, "c|w": null, "d|w": null }, { "a|w": null, "b|w": 2.5, "c|w": null, "d|w": null }, { "a|w": null, "b|w": null, "c|w": 7.5, "d|w": null }, { "a|w": 1.5, "b|w": null, "c|w": null, "d|w": null }, { "a|w": null, "b|w": null, "c|w": null, "d|w": 8.5 }, ]; let result = await view.to_json(); expect(result).toEqual(answer); view.delete(); table.delete(); }); test("group by ['x'], split by ['y'], both hidden and asc sorted", async function () { const table = await perspective.table({ x: ["a", "a", "b", "c"], y: ["x", "x", "y", "x"], z: [1, 2, 3, 4], }); const view = await table.view({ columns: ["z"], group_by: ["x"], split_by: ["y"], sort: [ ["x", "asc"], ["y", "col asc"], ], }); const paths = await view.column_paths(); expect(paths).toEqual(["x|z", "y|z"]); const expected = { __ROW_PATH__: [[], ["a"], ["b"], ["c"]], "x|z": [7, 3, null, 4], "y|z": [3, null, 3, null], }; const result = await view.to_columns(); expect(result).toEqual(expected); view.delete(); table.delete(); }); test("group by ['x'], split by ['y'], both hidden and desc sorted", async function () { const table = await perspective.table({ x: ["a", "a", "b", "c"], y: ["x", "x", "y", "x"], z: [1, 2, 3, 4], }); const view = await table.view({ columns: ["z"], group_by: ["x"], split_by: ["y"], sort: [ ["x", "desc"], ["y", "col desc"], ], }); const paths = await view.column_paths(); expect(paths).toEqual(["y|z", "x|z"]); const expected = { __ROW_PATH__: [[], ["c"], ["b"], ["a"]], "y|z": [3, null, 3, null], "x|z": [7, 4, null, 3], }; const result = await view.to_columns(); expect(result).toEqual(expected); view.delete(); table.delete(); }); test("group by ['x'], split by ['y'], hidden sorted on group by", async function () { const table = await perspective.table({ x: ["a", "a", "b", "c"], y: ["x", "x", "y", "x"], z: [1, 2, 3, 4], }); const view = await table.view({ columns: ["z"], group_by: ["y"], split_by: ["x"], sort: [["x", "desc"]], }); const paths = await view.column_paths(); expect(paths).toEqual(["a|z", "b|z", "c|z"]); const expected = { __ROW_PATH__: [[], ["x"], ["y"]], "a|z": [3, 3, null], "b|z": [3, null, 3], "c|z": [4, 4, null], }; const result = await view.to_columns(); expect(result).toEqual(expected); view.delete(); table.delete(); }); test("split by ['y'] has correct # of columns", async function () { var table = await perspective.table(data); var view = await table.view({ columns: ["w"], split_by: ["y"], sort: [["x", "desc"]], }); let to_cols = await view.to_columns(); let result = await view.num_columns(); expect(result).toEqual(4); view.delete(); table.delete(); }); }); }); })(perspective);