Files
2026-07-13 12:25:07 +08:00

897 lines
35 KiB
JavaScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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);