chore: import upstream snapshot with attribution
@@ -0,0 +1,5 @@
|
||||
# Bloc.ks
|
||||
|
||||
Examples designed to be hosted on
|
||||
[GitHub Pages](https://perspective-dev.github.io). See the individual projects
|
||||
in the `src/` directory for more info.
|
||||
@@ -0,0 +1,52 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
const LOCAL_EXAMPLES = [
|
||||
"editable",
|
||||
"file",
|
||||
"duckdb",
|
||||
"fractal",
|
||||
"market",
|
||||
"raycasting",
|
||||
"evictions",
|
||||
"nypd",
|
||||
"streaming",
|
||||
"covid",
|
||||
"webcam",
|
||||
"movies",
|
||||
"superstore",
|
||||
"olympics",
|
||||
"dataset",
|
||||
];
|
||||
|
||||
exports.LOCAL_EXAMPLES = LOCAL_EXAMPLES;
|
||||
|
||||
exports.get_examples = function get_examples(
|
||||
root = "https://perspective-dev.github.io/",
|
||||
) {
|
||||
const standalone = [
|
||||
// {
|
||||
// img: "https://perspective-dev.github.io/img/jupyterlab.png?",
|
||||
// url: "http://beta.mybinder.org/v2/gh/perspective-dev/perspective/master?urlpath=lab/tree/examples/jupyter-notebooks",
|
||||
// name: "jupyterlab",
|
||||
// },
|
||||
];
|
||||
|
||||
const hashes = LOCAL_EXAMPLES.map((x) => ({
|
||||
img: `${root}blocks/${x}/preview.png?`,
|
||||
url: `${root}block?example=${x}`,
|
||||
|
||||
name: x,
|
||||
}));
|
||||
|
||||
return hashes.concat(standalone);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 * as fs from "fs";
|
||||
import { get_examples, LOCAL_EXAMPLES } from "./examples.js";
|
||||
import * as url from "url";
|
||||
import * as path from "node:path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const version = JSON.parse(fs.readFileSync("./package.json")).version;
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1);
|
||||
|
||||
// TODO jsdelivr has slightly different logic for trailing '/' that causes
|
||||
// the wasm assets to not load correctly when using aliases, hence we must link
|
||||
// directly to the assets.
|
||||
const replacements = {
|
||||
"/node_modules/": `https://cdn.jsdelivr.net/npm/`,
|
||||
"@perspective-dev/client/dist/cdn/perspective.js": `@perspective-dev/client@${version}/dist/cdn/perspective.js`,
|
||||
"@perspective-dev/viewer/dist/cdn/perspective-viewer.js": `@perspective-dev/viewer@${version}/dist/cdn/perspective-viewer.js`,
|
||||
"@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js": `@perspective-dev/viewer-datagrid@${version}/dist/cdn/perspective-viewer-datagrid.js`,
|
||||
"@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js": `@perspective-dev/viewer-charts@${version}/dist/cdn/perspective-viewer-charts.js`,
|
||||
"@perspective-dev/workspace/dist/cdn/perspective-workspace.js": `@perspective-dev/workspace@${version}/dist/cdn/perspective-workspace.js`,
|
||||
"@perspective-dev/server/dist/cdn/perspective.cpp.wasm": `@perspective-dev/client@${version}/dist/cdn/perspective.cpp.wasm`,
|
||||
"@perspective-dev/viewer/dist/cdn/perspective.rx.wasm": `@perspective-dev/viewer@${version}/dist/cdn/perspective.rx.wasm`,
|
||||
"@perspective-dev/client/dist/cdn/perspective.worker.js": `@perspective-dev/client@${version}/dist/cdn/perspective.worker.js`,
|
||||
};
|
||||
|
||||
export async function dist_examples(
|
||||
outpath = `${__dirname}/../../docs/static/blocks`,
|
||||
) {
|
||||
execSync(`mkdir -p ${outpath}`, { stdio: "inherit" });
|
||||
const readme = generate_readme();
|
||||
let existing = fs.readFileSync(`${__dirname}/../../README.md`).toString();
|
||||
existing = existing.replace(
|
||||
/<\!\-\- Examples \-\->([\s\S]+?)<\!\-\- Examples \-\->/gm,
|
||||
`<!-- Examples -->\n${readme}\n<!-- Examples -->`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(`${__dirname}/../../README.md`, existing);
|
||||
for (const name of LOCAL_EXAMPLES) {
|
||||
// Copy
|
||||
if (fs.existsSync(`${__dirname}/src/${name}`)) {
|
||||
// Copy
|
||||
for (const filename of fs.readdirSync(`${__dirname}/src/${name}`)) {
|
||||
execSync(`mkdir -p ${outpath}/${name}`, { stdio: "inherit" });
|
||||
if (
|
||||
filename.endsWith(".mjs") ||
|
||||
filename.endsWith(".js") ||
|
||||
filename.endsWith(".html")
|
||||
) {
|
||||
let filecontents = fs
|
||||
.readFileSync(`${__dirname}/src/${name}/${filename}`)
|
||||
.toString();
|
||||
for (const pattern of Object.keys(replacements)) {
|
||||
filecontents = filecontents.replace(
|
||||
new RegExp(pattern, "g"),
|
||||
replacements[pattern],
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
`${outpath}/${name}/${filename}`,
|
||||
filecontents,
|
||||
);
|
||||
} else if (filename !== ".git") {
|
||||
execSync(
|
||||
`cp ${__dirname}/src/${name}/${filename} ${outpath}/${name}/${filename}`,
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// build
|
||||
if (fs.existsSync(path.join(outpath, name, "build.mjs"))) {
|
||||
console.log("Building " + name);
|
||||
const script = `${outpath}/${name}/build.mjs`;
|
||||
execSync(`node ${script}`, { stdio: "inherit" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function partition(input, spacing) {
|
||||
let output = [];
|
||||
for (let i = 0; i < input.length; i += spacing) {
|
||||
output[output.length] = input.slice(i, i + spacing);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function generate_readme() {
|
||||
const all = get_examples();
|
||||
return `<table><tbody>${partition(all, 3)
|
||||
.map(
|
||||
(row) =>
|
||||
`<tr>${row
|
||||
.map((y) => `<td>${y.name}</td>`)
|
||||
.join("")}</tr><tr>${row
|
||||
.map(
|
||||
(y) =>
|
||||
`<td><a href="${y.url}"><img height="125" src="${y.img}" /></a></td>`,
|
||||
)
|
||||
.join("")}</tr>`,
|
||||
)
|
||||
.join("")}</tbody></table>`;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "blocks",
|
||||
"private": true,
|
||||
"version": "4.5.2",
|
||||
"description": "A collection of simple client-side Perspective examples for `http://bl.ocks.org`.",
|
||||
"scripts": {
|
||||
"start": "mkdir -p dist && node --experimental-modules server.mjs",
|
||||
"repl": "node --experimental-repl-await"
|
||||
},
|
||||
"main": "index.mjs",
|
||||
"keywords": [],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@perspective-dev/client": "workspace:",
|
||||
"@perspective-dev/server": "workspace:",
|
||||
"@perspective-dev/viewer": "workspace:",
|
||||
"@perspective-dev/viewer-charts": "workspace:",
|
||||
"@perspective-dev/viewer-datagrid": "workspace:",
|
||||
"@perspective-dev/workspace": "workspace:",
|
||||
"superstore-arrow": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@zip.js/zip.js": "catalog:",
|
||||
"esbuild": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 fs from "fs";
|
||||
import { WebSocketServer } from "@perspective-dev/client";
|
||||
import { dist_examples } from "./index.mjs";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
await dist_examples(`${__dirname}/dist`);
|
||||
|
||||
const template = (body) => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
|
||||
</head>
|
||||
<body>
|
||||
<ul id="list">
|
||||
${body}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const prod_template = (name) => `
|
||||
<li>
|
||||
<a href="src/${name}/index.html">local</a>
|
||||
<a href="dist/${name}/index.html">dist</a>
|
||||
${name}
|
||||
</li>
|
||||
`;
|
||||
|
||||
const dev_template = (name) => `
|
||||
<li>
|
||||
<a href="src/${name}/index.html">local</a>
|
||||
${name}
|
||||
</li>
|
||||
`;
|
||||
|
||||
const gists = [
|
||||
"duckdb",
|
||||
"fractal",
|
||||
"raycasting",
|
||||
"evictions",
|
||||
"streaming",
|
||||
"covid",
|
||||
"superstore",
|
||||
"editable",
|
||||
"file",
|
||||
"webcam",
|
||||
];
|
||||
|
||||
const lis = [];
|
||||
for (const key of fs.readdirSync("src")) {
|
||||
if (gists.indexOf(key) >= 0) {
|
||||
lis.push(prod_template(key));
|
||||
} else {
|
||||
lis.push(dev_template(key));
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync("dist/index.html", template(lis.join("\n")));
|
||||
|
||||
new WebSocketServer({ assets: [__dirname, "dist", "../../"] });
|
||||
@@ -0,0 +1 @@
|
||||
license: apache-2.0
|
||||
@@ -0,0 +1,16 @@
|
||||
Demo of [Perspective](https://github.com/perspective-dev/perspective), using US
|
||||
COVID data from [https://covidtracking.com](https://covidtracking.com/).
|
||||
|
||||
The `Group By` column is a good example of using Perspective's ExprTK column
|
||||
language to clean/reformat a column. The raw "date" column, as imported from
|
||||
[https://covidtracking.com](https://covidtracking.com/), use the date format
|
||||
`YYYYMMDD`. Using this expression, we can parse this format into a well-typed
|
||||
`date()` column, then bucket this by week to get a sensible graph:
|
||||
|
||||
```
|
||||
// Parsed \"date\" bucket by week
|
||||
var year := integer(floor(\"date\" / 10000));
|
||||
var month := integer(floor(\"date\" / 100)) - year * 100;
|
||||
var day := integer(\"date\" % 100);
|
||||
bucket(date(year, month, day), 'W')
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import { worker } from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const WORKER = await worker();
|
||||
const REQ = fetch("https://api.covidtracking.com/v1/states/daily.csv");
|
||||
const LAYOUT = {
|
||||
plugin: "Y Area",
|
||||
plugin_config: {
|
||||
legend: {
|
||||
height: "106px",
|
||||
left: "100px",
|
||||
top: "25px",
|
||||
width: "",
|
||||
},
|
||||
},
|
||||
settings: true,
|
||||
group_by: ['Parsed "date" bucket by week'],
|
||||
split_by: ["state"],
|
||||
columns: ["deathIncrease"],
|
||||
filter: [],
|
||||
sort: [["deathIncrease", "col desc"]],
|
||||
expressions: {
|
||||
[`Parsed "date" bucket by week`]: `var year := integer(floor("date" / 10000));
|
||||
var month := integer(floor("date" / 100)) - year * 100;
|
||||
var day := integer("date" % 100);
|
||||
bucket(date(year, month, day), \'W\')`,
|
||||
},
|
||||
aggregates: {},
|
||||
};
|
||||
|
||||
const resp = await REQ;
|
||||
const csv = await resp.text();
|
||||
const el = document.getElementsByTagName("perspective-viewer")[0];
|
||||
const table = WORKER.table(csv);
|
||||
el.load(table);
|
||||
el.restore(LAYOUT);
|
||||
</script>
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-viewer editable> </perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,3 @@
|
||||
A random dataset generator and system monitor based on the
|
||||
<a href="https://docs.rs/perspective-client/latest/perspective_client/struct.Client.html#method.system_info">perspective_client::Client::system_info</a>
|
||||
method.
|
||||
@@ -0,0 +1,230 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/workspace/dist/css/pro.css" />
|
||||
<style>
|
||||
perspective-workspace {
|
||||
flex: 1;
|
||||
overflow: visible;
|
||||
border: 1px solid #666;
|
||||
overflow: hidden;
|
||||
}
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #f2f4f6;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.range {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
span,
|
||||
input,
|
||||
button {
|
||||
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="run">Generate</button>
|
||||
<button id="del">Delete</button>
|
||||
<div class="range">
|
||||
<span>Rows</span>
|
||||
<input id="num_rows" min="25" max="1000000" type="number" placeholder="#" value="100000" />
|
||||
<span>Update batches</span>
|
||||
<input id="num_batches" min="1" max="100" type="number" placeholder="#" value="1000" />
|
||||
<span>Update delay (ms)</span>
|
||||
<input id="batch_delay" min="100" max="10000" type="number" placeholder="#" value="200" />
|
||||
</div>
|
||||
<div class="range">
|
||||
<span>Float columns</span>
|
||||
<input id="num_float" min="0" max="50" type="number" placeholder="#" value="5" />
|
||||
</div>
|
||||
<div class="range">
|
||||
<span>Integer columns</span>
|
||||
<input id="num_integer" min="0" max="50" type="number" placeholder="#" value="5" />
|
||||
</div>
|
||||
<div class="range">
|
||||
<span>String columns</span>
|
||||
<input id="num_string" min="0" max="50" type="number" placeholder="#" value="5" />
|
||||
<span>Dictionary size</span>
|
||||
<input id="num_strings" min="1" max="500" type="number" placeholder="Unique Strings" value="50" />
|
||||
</div>
|
||||
<div class="range">
|
||||
<span>Datetime columns</span>
|
||||
<input id="num_datetime" min="0" max="50" type="number" placeholder="#" value="5" />
|
||||
</div>
|
||||
<div class="range">
|
||||
<span>Bool columns</span>
|
||||
<input id="num_boolean" min="0" max="50" type="number" placeholder="#" value="1" />
|
||||
</div>
|
||||
</div>
|
||||
<perspective-workspace id="psp_workspace">
|
||||
<!-- <perspective-viewer editable id="viewer"></perspective-viewer>
|
||||
<perspective-viewer editable id="stats"></perspective-viewer> -->
|
||||
</perspective-workspace>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/workspace/dist/cdn/perspective-workspace.js";
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const client = await perspective.worker();
|
||||
|
||||
const choose = (x) => x[Math.floor(Math.random() * x.length)];
|
||||
const range = (x, y) => Math.random() * (y - x) + x;
|
||||
const rand_string = () => Math.random().toString(36).substring(7);
|
||||
const strings = (choices) => new Array(choices).fill(null).map(rand_string);
|
||||
const colname = (name, x) => `${name.charAt(0).toUpperCase() + name.slice(1)} ${x}`;
|
||||
|
||||
function* col_iter() {
|
||||
for (const name of ["float", "integer", "string", "datetime", "boolean"]) {
|
||||
const ncols = window[`num_${name}`].value;
|
||||
for (let x = 0; x < ncols; x++) {
|
||||
yield [name, x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assign = (fn) => {
|
||||
const obj = {};
|
||||
for (const [name, x] of col_iter()) {
|
||||
obj[colname(name, x)] = fn(name, x);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
const new_schema = () => assign((x) => x);
|
||||
|
||||
let string_cache;
|
||||
function reset_strings_cache() {
|
||||
string_cache = {};
|
||||
}
|
||||
|
||||
reset_strings_cache();
|
||||
|
||||
const get_dict = (xs) => {
|
||||
string_cache[xs] = string_cache[xs] || strings(parseInt(num_strings.value));
|
||||
return string_cache[xs];
|
||||
};
|
||||
|
||||
const cell_args = {
|
||||
float: () => range(-10, 10),
|
||||
integer: () => Math.floor(range(-10, 10)),
|
||||
string: (xs) => choose(get_dict(xs)),
|
||||
datetime: () => new Date(),
|
||||
boolean: () => choose([true, false, null]),
|
||||
};
|
||||
|
||||
const new_row = () => assign((name, x) => cell_args[name](x));
|
||||
const gen_data = async () => {
|
||||
reset_strings_cache();
|
||||
let nrows = num_rows.value;
|
||||
let rows = [];
|
||||
const batch_size = Math.floor(nrows / num_batches.value);
|
||||
const batch_freq = batch_delay.value;
|
||||
const tbl = await client.table(new_schema(), { name: "superstore" });
|
||||
(function batch() {
|
||||
while (nrows > 0) {
|
||||
rows.push(new_row());
|
||||
nrows--;
|
||||
if (nrows % batch_size === 0) {
|
||||
tbl.update(rows);
|
||||
rows = [];
|
||||
setTimeout(batch, batch_freq);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return tbl;
|
||||
};
|
||||
|
||||
// GUI
|
||||
|
||||
const make_run_click_callback = (state) => async () => {
|
||||
state.table?.delete?.({ lazy: true });
|
||||
state.table = gen_data();
|
||||
// await window.psp_workspace.addTable("superstore", state.table);
|
||||
};
|
||||
|
||||
const make_del_click_callback = (state) => async () => {
|
||||
if (state.table) {
|
||||
// await viewer.eject();
|
||||
// await window.psp_workspace.removeTable("superstore");
|
||||
for (const viewer of window.psp_workspace.children) {
|
||||
const cfg = await viewer.save();
|
||||
if (cfg.table === "superstore") {
|
||||
await viewer.eject();
|
||||
}
|
||||
}
|
||||
|
||||
await state.table.then((x) => x.delete({ lazy: true }));
|
||||
state.table = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const state = {};
|
||||
|
||||
// Main
|
||||
run.addEventListener("click", make_run_click_callback(state));
|
||||
del.addEventListener("click", make_del_click_callback(state));
|
||||
run.dispatchEvent(new Event("click"));
|
||||
|
||||
const stats_table = await client.table(
|
||||
{
|
||||
heap_size: "float",
|
||||
used_size: "float",
|
||||
cpu_time: "integer",
|
||||
cpu_time_epoch: "integer",
|
||||
version: "integer",
|
||||
timestamp: "datetime",
|
||||
client_used: "float",
|
||||
client_heap: "float",
|
||||
},
|
||||
{
|
||||
limit: 2000,
|
||||
name: "stats",
|
||||
},
|
||||
);
|
||||
|
||||
(async function check_mem_loop() {
|
||||
await stats_table.update([await client.system_info()]);
|
||||
setTimeout(check_mem_loop, 200);
|
||||
})();
|
||||
|
||||
await window.psp_workspace.load(client);
|
||||
await window.psp_workspace.restore(await fetch("layout.json").then((x) => x.json()));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"sizes": [1],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "split-area",
|
||||
"orientation": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_0"],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "split-area",
|
||||
"orientation": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_1"],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_2"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
],
|
||||
"sizes": [0.5, 0.5]
|
||||
}
|
||||
],
|
||||
"sizes": [0.5, 0.5]
|
||||
}
|
||||
},
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_0": {
|
||||
"version": "4.5.2",
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {},
|
||||
"edit_mode": "READ_ONLY",
|
||||
"scroll_lock": false
|
||||
},
|
||||
"columns_config": {},
|
||||
"title": "Generated Data",
|
||||
"group_by": ["String 0", "String 1"],
|
||||
"split_by": ["String 2"],
|
||||
"sort": [],
|
||||
"filter": [],
|
||||
"expressions": {},
|
||||
"columns": [
|
||||
"Float 2",
|
||||
"Float 3",
|
||||
"Float 4",
|
||||
"Integer 0",
|
||||
"Integer 1",
|
||||
"Datetime 4",
|
||||
"Boolean 0"
|
||||
],
|
||||
"aggregates": {
|
||||
"Boolean 0": "dominant",
|
||||
"Datetime 4": "dominant"
|
||||
},
|
||||
"table": "superstore",
|
||||
"settings": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_1": {
|
||||
"version": "4.5.2",
|
||||
"plugin": "Y Area",
|
||||
"plugin_config": {},
|
||||
"columns_config": {},
|
||||
"table": "stats",
|
||||
"title": "Server Memory",
|
||||
"group_by": ["timestamp"],
|
||||
"split_by": [],
|
||||
"sort": [],
|
||||
"filter": [],
|
||||
"expressions": {
|
||||
"Free (mb)": "\"heap_size\" / 1024 / 1024",
|
||||
"Heap (mb)": "\"used_size\" / 1024 / 1024"
|
||||
},
|
||||
"columns": ["Free (mb)", "Heap (mb)"],
|
||||
"aggregates": {}
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_2": {
|
||||
"version": "4.5.2",
|
||||
"plugin": "Y Line",
|
||||
"plugin_config": {},
|
||||
"columns_config": {},
|
||||
"title": "Server CPU",
|
||||
"group_by": ["timestamp"],
|
||||
"split_by": [],
|
||||
"sort": [],
|
||||
"filter": [],
|
||||
"expressions": {
|
||||
"CPU (%)": "\"cpu_time\" / \"cpu_time_epoch\""
|
||||
},
|
||||
"columns": ["CPU (%)"],
|
||||
"aggregates": {},
|
||||
"table": "stats",
|
||||
"settings": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,15 @@
|
||||
An example of [Perspective](https://github.com/perspective-dev/perspective)
|
||||
using [DuckDB WASM](https://duckdb.org/docs/api/wasm/overview) as a virtual
|
||||
server backend via the `DuckDBHandler` adapter.
|
||||
|
||||
Instead of using Perspective's built-in WebAssembly query engine, this example
|
||||
demonstrates how to use DuckDB as the data processing layer while still
|
||||
leveraging Perspective's visualization components. The `DuckDBHandler` translates
|
||||
Perspective's view configuration (group by, split by, sort, filter, expressions,
|
||||
aggregates) into DuckDB SQL queries, enabling Perspective to query data stored
|
||||
in DuckDB tables.
|
||||
|
||||
This example loads the Superstore sample dataset into a DuckDB table, then
|
||||
creates a Perspective viewer that queries the data through the DuckDB virtual
|
||||
server. A separate log viewer displays the SQL queries being generated in
|
||||
real-time, along with a timeline chart showing query frequency.
|
||||
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/superstore-arrow/superstore.lz4.arrow" as="fetch" type="arraybuffer" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script type="module" src="index.js"></script>
|
||||
<style>
|
||||
#query {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 30%;
|
||||
}
|
||||
|
||||
#logger {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 70%;
|
||||
bottom: 0%;
|
||||
right: 0;
|
||||
}
|
||||
#logger2 {
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
left: 70%;
|
||||
bottom: 60%;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-viewer id="query"></perspective-viewer>
|
||||
<perspective-viewer id="logger"></perspective-viewer>
|
||||
<perspective-viewer id="logger2"></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
import { DuckDBHandler } from "/node_modules/@perspective-dev/client/dist/esm/virtual_servers/duckdb.js";
|
||||
|
||||
// Need to use jsDelivr's ESM features to load this as packaged.
|
||||
import * as duckdb from "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.33.1-dev18.0/+esm";
|
||||
|
||||
const LOGGER = {
|
||||
log(entry) {
|
||||
table2.update([{ timestamp: entry.timestamp, sql: entry.value }]);
|
||||
},
|
||||
};
|
||||
|
||||
const db = await initializeDuckDB();
|
||||
const server = perspective.createMessageHandler(new DuckDBHandler(db));
|
||||
const client = await perspective.worker(server);
|
||||
|
||||
const logworker = await perspective.worker();
|
||||
const table2 = await logworker.table(
|
||||
{ timestamp: "datetime", sql: "string" },
|
||||
{ name: "logs", limit: 10_000 },
|
||||
);
|
||||
|
||||
const log_element = document.querySelector("#logger");
|
||||
log_element.load(logworker);
|
||||
log_element.restore({
|
||||
table: "logs",
|
||||
sort: [["timestamp", "desc"]],
|
||||
title: "SQL Log",
|
||||
});
|
||||
|
||||
const log_element2 = document.querySelector("#logger2");
|
||||
log_element2.load(logworker);
|
||||
log_element2.restore({
|
||||
table: "logs",
|
||||
sort: [["timestamp", "desc"]],
|
||||
columns: ["sql"],
|
||||
group_by: ["1s"],
|
||||
plugin: "Y Bar",
|
||||
expressions: { "1s": `bucket("timestamp",'1s')` },
|
||||
title: "SQL Timeline",
|
||||
});
|
||||
|
||||
async function initializeDuckDB() {
|
||||
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
|
||||
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
|
||||
const worker_url = URL.createObjectURL(
|
||||
new Blob([`importScripts("${bundle.mainWorker}");`], {
|
||||
type: "text/javascript",
|
||||
}),
|
||||
);
|
||||
|
||||
const duckdb_worker = new Worker(worker_url);
|
||||
const db = new duckdb.AsyncDuckDB(LOGGER, duckdb_worker);
|
||||
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
|
||||
URL.revokeObjectURL(worker_url);
|
||||
const conn = await db.connect();
|
||||
return conn;
|
||||
}
|
||||
|
||||
async function loadSampleData(db) {
|
||||
const response = await fetch(
|
||||
"/node_modules/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
const text = await response.arrayBuffer();
|
||||
await db.insertArrowFromIPCStream(new Uint8Array(text), {
|
||||
name: "data_source_one",
|
||||
create: true,
|
||||
});
|
||||
}
|
||||
|
||||
await loadSampleData(db);
|
||||
|
||||
const viewer = document.querySelector("#query");
|
||||
viewer.load(client);
|
||||
viewer.restore({
|
||||
table: "memory.data_source_one",
|
||||
group_by: ["Region", "State", "City"],
|
||||
columns: ["Sales", "Profit", "Quantity", "Discount"],
|
||||
plugin: "Datagrid",
|
||||
theme: "Pro Dark",
|
||||
settings: true,
|
||||
});
|
||||
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1 @@
|
||||
license: apache-2.0
|
||||
@@ -0,0 +1,6 @@
|
||||
A simple example of
|
||||
[Perspective](https://github.com/perspective-dev/perspective) with superstore
|
||||
data, and editability enabled (not default but easy to toggle at runtime). This
|
||||
example has no server component, and the edits occur only within the browser
|
||||
session; refreshing the page will forget any edits and revert to the original
|
||||
dataset.
|
||||
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/superstore-arrow/superstore.lz4.arrow" as="fetch" type="arraybuffer" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const resp = await fetch("/node_modules/superstore-arrow/superstore.lz4.arrow");
|
||||
const arrow = await resp.arrayBuffer();
|
||||
const viewer = document.getElementsByTagName("perspective-viewer")[0];
|
||||
await worker.table(arrow, { name: "superstore" });
|
||||
await viewer.load(worker);
|
||||
await viewer.restore({ table: "superstore", settings: true, plugin_config: { edit_mode: "EDIT" } });
|
||||
</script>
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1 @@
|
||||
license: apache-2.0
|
||||
@@ -0,0 +1,7 @@
|
||||
Demo of [Perspective](https://github.com/perspective-dev/perspective), using SF
|
||||
eviciton data from 1997-present provided by
|
||||
[DataSF](https://data.sfgov.org/Housing-and-Buildings/Eviction-Notices/5cei-gny5).
|
||||
|
||||
This example has no server component, and connects directly to DataSF's public
|
||||
API from your browser. These 44,311 rows are ~2.5mb gzipped (at time of
|
||||
writing), but are cached in `LocalStorage` on subsequent page visits.
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/fflate@0.7.3/umd/index.min.js"></script>
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import { worker } from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const WORKER = await worker();
|
||||
const URL = `https://data.sfgov.org/resource/5cei-gny5.csv?$limit=50000`;
|
||||
|
||||
async function get_evictions() {
|
||||
const arrow = localStorage.getItem(URL);
|
||||
if (arrow !== null) {
|
||||
console.log("Using cached data from localStorage");
|
||||
try {
|
||||
const buffer = fflate.strToU8(arrow, true);
|
||||
return await WORKER.table(fflate.decompressSync(buffer).buffer);
|
||||
} catch (e) {
|
||||
console.error("Can't load cached data", e);
|
||||
localStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Downloading data from data.sfgov.org");
|
||||
const resp = await fetch(
|
||||
URL,
|
||||
// "5cei-gny5.csv"
|
||||
// "evictions.all.arrow"
|
||||
);
|
||||
const csv = await resp.text();
|
||||
const table = await WORKER.table(csv);
|
||||
(async () => {
|
||||
console.log("Caching data");
|
||||
const view = await table.view();
|
||||
const arrow = await view.to_arrow();
|
||||
try {
|
||||
const x = fflate.compressSync(new Uint8Array(arrow));
|
||||
const zipped = fflate.strFromU8(x, true);
|
||||
localStorage.setItem(URL, zipped);
|
||||
} catch (e) {
|
||||
console.error("Can't cache data from data.sfgov.org", e);
|
||||
}
|
||||
})();
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const el = document.getElementsByTagName("perspective-viewer")[0];
|
||||
|
||||
const evictions = get_evictions();
|
||||
const layout_json = await fetch("layout.json");
|
||||
const layout = await layout_json.json();
|
||||
|
||||
el.load(await evictions);
|
||||
el.restore(layout);
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<perspective-viewer editable> </perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"plugin": "Map Scatter",
|
||||
"plugin_config": {},
|
||||
"settings": true,
|
||||
"theme": "Pro Dark",
|
||||
"group_by": ["bucket lon", "bucket lat"],
|
||||
"split_by": ["neighborhood"],
|
||||
"columns": ["lon", "lat", null, null, null],
|
||||
"filter": [["lon", "is not null", null]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"lon": "var x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", 7, x[1] - 7);\nfloat(y)",
|
||||
"lat": "var x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", x[0], length(\"shape\") - x[1]);\nfloat(y)",
|
||||
"bucket lon": "var x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", 7, x[1] - 7);\nbucket(float(y), 0.0025) + 0.00125",
|
||||
"bucket lat": "var x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", x[0], length(\"shape\") - x[1]);\nbucket(float(y), 0.0025) + 0.00125"
|
||||
},
|
||||
"aggregates": {
|
||||
"lon": "avg",
|
||||
"lat": "avg",
|
||||
"bucket lon": "avg",
|
||||
"bucket lat": "avg"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1 @@
|
||||
license: apache-2.0
|
||||
@@ -0,0 +1,13 @@
|
||||
A simple file loader demo of
|
||||
[Perspective](https://github.com/perspective-dev/perspective). The given
|
||||
CSV/Feather must be in a form Perspective's `Table()` constructor supports:
|
||||
|
||||
- First row must be column names.
|
||||
- Subsequent rows should be mono-typed (but if not you will just get `string`
|
||||
columns and you'll need to use ExprTK expressions to cast).
|
||||
- Well-formed (Consistent # of `,` per line, strings should be quoted,
|
||||
especially if they contain `,` themselves).
|
||||
|
||||
This example does not have a server component, all
|
||||
calculation/loading/manipulation of the data is done in the browser, and the
|
||||
data "uploaded" never leaves your browser/computer.
|
||||
@@ -0,0 +1,108 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const worker = await perspective.worker();
|
||||
|
||||
// Get `dropArea` element from the DOM.
|
||||
var dropArea = document.getElementById("drop-area");
|
||||
|
||||
// Get `input` element from the DOM.
|
||||
var input = document.getElementById("fileElem");
|
||||
|
||||
// Add event listeners to `dropArea`.
|
||||
dropArea.addEventListener("dragenter", () => {}, false);
|
||||
dropArea.addEventListener("dragleave", () => {}, false);
|
||||
dropArea.addEventListener("dragover", () => {}, false);
|
||||
dropArea.addEventListener("drop", (x) => console.log(x), false);
|
||||
|
||||
// Prevent defaults for drag / drop events.
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||
dropArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Highlight `dropArea` on drag enter and over.
|
||||
["dragenter", "dragover"].forEach(function (eventName) {
|
||||
dropArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
// Remove highlight `dropArea` on drag leave and drop.
|
||||
["dragleave", "drop"].forEach(function (eventName) {
|
||||
dropArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
// Add class for highlighting drop area.
|
||||
function highlight() {
|
||||
dropArea.classList.add("highlight");
|
||||
}
|
||||
|
||||
// Remove class for highlighting drop area.
|
||||
function unhighlight() {
|
||||
dropArea.classList.remove("highlight");
|
||||
}
|
||||
|
||||
// Add event listener for drop.
|
||||
dropArea.addEventListener("drop", handleDrop, false);
|
||||
|
||||
// Add event listener for file change on `input`.
|
||||
input.addEventListener("change", function () {
|
||||
handleFiles(this.files);
|
||||
});
|
||||
|
||||
// Handle files attached to the drop.
|
||||
function handleDrop(e) {
|
||||
let dt = e.dataTransfer;
|
||||
let files = dt.files;
|
||||
|
||||
handleFiles(files);
|
||||
}
|
||||
|
||||
// Iterate over files and call upload on each.
|
||||
function handleFiles(files) {
|
||||
[...files].forEach(uploadFile);
|
||||
}
|
||||
|
||||
// On file load, remove the `dropArea` and replace it with a `<perspective-viewer>`.
|
||||
function uploadFile(file) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (fileLoadedEvent) {
|
||||
let data = fileLoadedEvent.target.result;
|
||||
|
||||
// Remove the `dropArea` from the DOM.
|
||||
const parent = dropArea.parentElement;
|
||||
parent.removeChild(dropArea);
|
||||
|
||||
// Create a `<perspective-viewer>` and append it to the DOM.
|
||||
const psp = document.createElement("perspective-viewer");
|
||||
parent.appendChild(psp);
|
||||
|
||||
// Load the file data into `<perspective-viewer>`.
|
||||
if (file.name.endsWith(".feather") || file.name.endsWith(".arrow")) {
|
||||
psp.load(worker.table(data));
|
||||
} else {
|
||||
psp.load(worker.table(data, { format: "csv" }));
|
||||
}
|
||||
};
|
||||
|
||||
// Read the contents of the file - triggering the onload when finished.
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
* ┃ 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). ┃
|
||||
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
*/
|
||||
|
||||
#drop-area {
|
||||
border: 5px dashed #ccc;
|
||||
border-radius: 15px;
|
||||
width: 480px;
|
||||
font-family: sans-serif;
|
||||
margin: 100px auto;
|
||||
padding: 48px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#drop-area.highlight {
|
||||
border-color: cornflowerblue;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.my-form {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#gallery {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#gallery img {
|
||||
width: 150px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
background: #ccc;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: cornflowerblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#fileElem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script type="module" src="file.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="drop-area">
|
||||
<form class="my-form">
|
||||
<p>Upload a CSV/Arrow file by dragging from your desktop and dropping onto the dashed region.</p>
|
||||
<p>(Data is processed in browser, and never sent to any server).</p>
|
||||
<input type="file" id="fileElem" multiple accept=".feather,.arrow,.csv" />
|
||||
<label class="button" for="fileElem">Select a file</label>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,2 @@
|
||||
license: apache-2.0
|
||||
height: 800
|
||||
@@ -0,0 +1,12 @@
|
||||
A classic [fractal](https://en.wikipedia.org/wiki/Mandelbrot_set) implemented
|
||||
entirely in
|
||||
ExprTK/[Perspective](https://github.com/perspective-dev/perspective). This
|
||||
example's dataset is just a single column, `"index"`, with 40,000 integers [0,
|
||||
39999]. From this column, the columns `x` and `y` are derived forming a 200x200
|
||||
grid, and finally the column `color` is calculated from the standard Mandelbrot
|
||||
formula, using the complex point at `x + yi`. The controls at the top of the
|
||||
page allow the programmatic redefinition of this column to e.g. modify the
|
||||
Mandelbrot function's max iterations, window, resolution, etc.
|
||||
|
||||
This examples has no server component and the data is generated in-memory on
|
||||
load. All calculation is done in the Perspective WebAssembly engine itself.
|
||||
@@ -0,0 +1,224 @@
|
||||
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
* ┃ 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). ┃
|
||||
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
*/
|
||||
|
||||
perspective-viewer {
|
||||
flex: 1;
|
||||
overflow: visible;
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
perspective-viewer[theme="Pro Light"],
|
||||
perspective-viewer[theme="Pro Dark"] {
|
||||
--psp-charts--gradient--background: linear-gradient(
|
||||
#000 0%,
|
||||
#94d0ff 50%,
|
||||
#8795e8,
|
||||
#966bff,
|
||||
#ad8cff,
|
||||
#c774e8,
|
||||
#c774a9,
|
||||
#ff6ad5,
|
||||
#ff6a8b,
|
||||
#ff8b8b,
|
||||
#ffa58b,
|
||||
#ffde8b,
|
||||
#cdde8b,
|
||||
#8bde8b,
|
||||
#20de8b
|
||||
);
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #f2f4f6;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
/* padding: 12px; */
|
||||
border-bottom: 1px solid #ababab;
|
||||
}
|
||||
|
||||
#controls2 {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 60px;
|
||||
bottom: 0;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.range {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.range.xl {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.range.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
span,
|
||||
input,
|
||||
button {
|
||||
font-family:
|
||||
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
|
||||
"Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
background: none;
|
||||
margin: 0px;
|
||||
border-color: #ccc;
|
||||
color: #666;
|
||||
padding: 6px 12px 6px 0px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-width: 0px;
|
||||
border-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
font-family:
|
||||
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
|
||||
"Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #1a7da1;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0px solid #ccc;
|
||||
border-right-width: 1px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
padding-left: 12px;
|
||||
height: 28px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#run {
|
||||
justify-self: center;
|
||||
margin-right: 24px;
|
||||
height: 50px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
#run:disabled {
|
||||
/* opacity: 0.2; */
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
pointer-events: all;
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 0 0 1px #c6c6c6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
pointer-events: all;
|
||||
width: 6px;
|
||||
height: 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 0px;
|
||||
box-shadow: 0 0 0 1px #c6c6c6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:hover {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:active {
|
||||
box-shadow:
|
||||
inset 0 0 3px #387bbe,
|
||||
0 0 9px #387bbe;
|
||||
-webkit-box-shadow:
|
||||
inset 0 0 3px #387bbe,
|
||||
0 0 9px #387bbe;
|
||||
}
|
||||
/*
|
||||
input[type="number"] {
|
||||
color: #8a8383;
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
font-size: 20px;
|
||||
border: none;
|
||||
} */
|
||||
/*
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
opacity: 1;
|
||||
} */
|
||||
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
background-color: #c6c6c6;
|
||||
pointer-events: none;
|
||||
padding: 0;
|
||||
top: 25px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#ymin,
|
||||
#xmin {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#ymin,
|
||||
#ymax {
|
||||
/* -webkit-appearance: slider-vertical; */
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: top left;
|
||||
top: 220px;
|
||||
left: 30px;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<link rel='stylesheet' href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="controls">
|
||||
<button id="run" disabled>Run</button>
|
||||
<div class="range xl">
|
||||
<input id="xmin" min="-2" max="1.0" step="0.1" value="-0.4" type="range"></input>
|
||||
<input id="xmax" min="-2" max="1.0" step="0.1" value="-0.3" type="range"></input>
|
||||
</div>
|
||||
<div class="range right">
|
||||
<span>Resolution</span>
|
||||
<input id="resolution" min="25" max="700" type="number" placeholder="Resolution" value="200"></input>
|
||||
</div>
|
||||
<div class="range right">
|
||||
<span>Iterations</span>
|
||||
<input id="iterations" min="1" max="1000" type="number" placeholder="Iterations" value="100"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div id="controls2">
|
||||
<div class="range xl">
|
||||
<input id="ymin" min="-1" max="1.0" step="0.1" value="0.6" type="range" orient="vertical"></input>
|
||||
<input id="ymax" min="-1" max="1.0" step="0.1" value="0.7" type="range" orient="vertical"></input>
|
||||
</div>
|
||||
</div>
|
||||
<perspective-viewer id="viewer"></perspective-viewer>
|
||||
</div>
|
||||
<script type="module" src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,163 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
function generate_mandelbrot(params) {
|
||||
return `
|
||||
// color
|
||||
var resolution := ${params.resolution};
|
||||
var xmin := ${params.xmin};
|
||||
var xmax := ${params.xmax};
|
||||
var ymin := ${params.ymin};
|
||||
var ymax := ${params.ymax};
|
||||
var iterations := ${params.iterations};
|
||||
|
||||
var x := floor("index" / resolution);
|
||||
var y := "index" % resolution;
|
||||
var c := iterations;
|
||||
|
||||
var cx := xmin + ((xmax - xmin) * x) / (resolution - 1);
|
||||
var cy := ymin + ((ymax - ymin) * y) / (resolution - 1);
|
||||
|
||||
var vx := 0;
|
||||
var vy := 0;
|
||||
var vxx := 0;
|
||||
var vyy := 0;
|
||||
var vxy := 0;
|
||||
|
||||
for (var ii := 0; ii < iterations; ii += 1) {
|
||||
if (vxx + vyy <= float(4)) {
|
||||
vxy := vx * vy;
|
||||
vxx := vx * vx;
|
||||
vyy := vy * vy;
|
||||
vx := vxx - vyy + cx;
|
||||
vy := vxy + vxy + cy;
|
||||
c -= 1;
|
||||
}
|
||||
};
|
||||
|
||||
c`;
|
||||
}
|
||||
|
||||
function generate_layout(params) {
|
||||
return {
|
||||
plugin: "Heatmap",
|
||||
table: "raw_data",
|
||||
settings: true,
|
||||
group_by: [`floor("index" / ${params.resolution})`],
|
||||
split_by: [`"index" % ${params.resolution}`],
|
||||
columns: ["color"],
|
||||
expressions: {
|
||||
color: generate_mandelbrot(params).trim(),
|
||||
[`floor("index" / ${params.resolution})`]: `floor("index" / ${params.resolution})`,
|
||||
[`"index" % ${params.resolution}`]: `"index" % ${params.resolution}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function generate_data(table) {
|
||||
const run = document.getElementById("run");
|
||||
let json = new Array(Math.pow(resolution, 2));
|
||||
for (let x = 0; x < resolution; ++x) {
|
||||
for (let y = 0; y < resolution; ++y) {
|
||||
const index = x * resolution + y;
|
||||
json[index] = {
|
||||
index,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await table.replace(json);
|
||||
run.innerHTML = `Run`;
|
||||
}
|
||||
|
||||
// GUI
|
||||
|
||||
function get_gui_params() {
|
||||
return ["xmin", "xmax", "ymin", "ymax", "resolution", "iterations"].reduce(
|
||||
(acc, x) => {
|
||||
acc[x] = window[x].valueAsNumber;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
function make_range(x, y, name) {
|
||||
x.addEventListener("input", () => {
|
||||
window.run.disabled = false;
|
||||
x.value = Math.min(x.valueAsNumber, y.valueAsNumber - 0.1);
|
||||
});
|
||||
|
||||
y.addEventListener("input", () => {
|
||||
window.run.disabled = false;
|
||||
y.value = Math.max(x.valueAsNumber + 0.1, y.valueAsNumber);
|
||||
});
|
||||
}
|
||||
|
||||
const make_run_click_callback = (worker, state) => async () => {
|
||||
if (window.run.innerHTML.trim() !== "Run") {
|
||||
window.run.innerHTML = "Run";
|
||||
return;
|
||||
}
|
||||
|
||||
window.run.disabled = true;
|
||||
if (!state.table) {
|
||||
state.table = await worker.table(
|
||||
{
|
||||
index: "integer",
|
||||
},
|
||||
{
|
||||
name: "raw_data",
|
||||
},
|
||||
);
|
||||
|
||||
window.viewer.load(worker);
|
||||
}
|
||||
|
||||
const run = document.getElementById("run");
|
||||
const params = get_gui_params();
|
||||
const new_size = Math.pow(params.resolution, 2);
|
||||
if (!state.size || state.size !== new_size) {
|
||||
let json = { index: new Array(new_size) };
|
||||
for (let x = 0; x < new_size; ++x) {
|
||||
json.index[x] = x;
|
||||
}
|
||||
|
||||
state.table.replace(json);
|
||||
}
|
||||
|
||||
state.size = new_size;
|
||||
run.innerHTML = `Run`;
|
||||
window.viewer.restore(generate_layout(params));
|
||||
};
|
||||
|
||||
function set_runnable() {
|
||||
window.run.disabled = false;
|
||||
}
|
||||
|
||||
make_range(xmin, xmax, "X");
|
||||
make_range(ymin, ymax, "Y");
|
||||
window.resolution.addEventListener("input", set_runnable);
|
||||
window.iterations.addEventListener("input", set_runnable);
|
||||
|
||||
run.addEventListener(
|
||||
"click",
|
||||
make_run_click_callback(await perspective.worker(), {}),
|
||||
);
|
||||
|
||||
run.dispatchEvent(new Event("click"));
|
||||
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,55 @@
|
||||
|
||||
A simple market simulation implemented in Perspective, designed to be simple
|
||||
rather than accurate. The simulation proceeds in steps:
|
||||
|
||||
1) Insert a batch of random orders in a range around the market's bid/ask
|
||||
spread. For each side of the order book, we can calculate the best open price
|
||||
by querying the orders `Table` using the `"max"` or `"min"` aggregates
|
||||
(respectively).
|
||||
|
||||
```json
|
||||
{
|
||||
columns: ["price"],
|
||||
group_by: ["security"],
|
||||
aggregates: { price: "max" },
|
||||
filter: [
|
||||
["side", "==", "buy"],
|
||||
["status", "==", "open"],
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
1) Clear any matched orders in the `Table`. To match orders, we fetch all open
|
||||
orders on both sides which are outside of the best price, then update an
|
||||
_equal_ number of both `"buy"` and `"sell"` side orders to `"closed"`. The
|
||||
`sort` field guarantees that orders are closed in best, then oldest, order.
|
||||
|
||||
```json
|
||||
{
|
||||
columns: ["id"],
|
||||
filter: [
|
||||
["side", "==", "buy"],
|
||||
["status", "==", "open"],
|
||||
["price", ">", price],
|
||||
],
|
||||
sort: [
|
||||
["price", "desc"],
|
||||
["timestamp", "asc"],
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
3) Expire any elapsed orders which are still `"open"` by fetching orders older
|
||||
than the expiration ID and update thair status' to `"expired"`.
|
||||
|
||||
```json
|
||||
{
|
||||
columns: ["id"],
|
||||
filter: [
|
||||
["status", "==", "open"],
|
||||
["id", "<", 12345],
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
4) Sleep for a bit and repeat (1).
|
||||
@@ -0,0 +1,93 @@
|
||||
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
* ┃ 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). ┃
|
||||
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
*/
|
||||
|
||||
body {
|
||||
background: #242526;
|
||||
color: white;
|
||||
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
|
||||
"Consolas", "Liberation Mono", monospace;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#header a {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
perspective-viewer {
|
||||
border-top: 1px solid #666;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
label {
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
padding: 6px 0px;
|
||||
margin-right: 4px;
|
||||
margin-left: 14px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
select,
|
||||
button {
|
||||
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
|
||||
"Consolas", "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid #666;
|
||||
border-radius: 2px;
|
||||
padding: 6px 10px;
|
||||
color: #f4f5f6;
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
height: 32px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
select:hover,
|
||||
button:hover {
|
||||
color: #242526;
|
||||
background-color: #f4f5f6;
|
||||
border-color: #f4f5f6;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script type="module" src="market.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="header">
|
||||
<a href="https://perspective-dev.github.io">
|
||||
<img height="12" src="https://raw.githubusercontent.com/perspective-dev/perspective/master/docs/static/svg/perspective-logo-dark.svg" />
|
||||
</a>
|
||||
<label>Market Simulation Demo</label>
|
||||
<select></select>
|
||||
<button>Reset</button>
|
||||
</div>
|
||||
<perspective-viewer theme="Pro Dark"></perspective-viewer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,290 @@
|
||||
[
|
||||
{
|
||||
"plugin": "Heatmap",
|
||||
"plugin_config": {},
|
||||
"title": "Heatmap",
|
||||
"group_by": ["bucket(\"timestamp\", 'm')"],
|
||||
"split_by": ["bucket(\"price\", 0.5)"],
|
||||
"columns": ["if(\"side\"=='buy'){-1}else{1}"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"timestamp\", 'm')": "bucket(\"timestamp\", 'm')",
|
||||
"bucket(\"price\", 0.5)": "bucket(\"price\", 0.5)",
|
||||
"if(\"side\"=='buy'){-1}else{1}": "if(\"side\"=='buy'){-1}else{1}"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Heatmap",
|
||||
"plugin_config": {},
|
||||
"title": "Heatmap (Closed)",
|
||||
"group_by": ["bucket(\"timestamp\", 'm')"],
|
||||
"split_by": ["bucket(\"price\", 0.25)"],
|
||||
"columns": ["if(\"side\"=='buy'){-1}else{1}"],
|
||||
"filter": [["status", "==", "closed"]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"timestamp\", 'm')": "bucket(\"timestamp\", 'm')",
|
||||
"bucket(\"price\", 0.25)": "bucket(\"price\", 0.25)",
|
||||
"if(\"side\"=='buy'){-1}else{1}": "if(\"side\"=='buy'){-1}else{1}"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Y Scatter",
|
||||
"plugin_config": {},
|
||||
"title": "Closed Orders ((s) bucket)",
|
||||
"group_by": ["bucket(\"timestamp\", 's')"],
|
||||
"split_by": ["if(\"status\"!='open'){\"status\"}else{\"side\"}"],
|
||||
"columns": ["price"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"if(\"status\"=='closed'){1}else if(\"status\"=='expired'){2}else{0}": "if(\"status\"=='closed'){1}else if(\"status\"=='expired'){2}else{0}",
|
||||
"if(\"status\"!='open'){\"status\"}else{\"side\"}": "if(\"status\"!='open'){\"status\"}else{\"side\"}",
|
||||
"bucket(\"timestamp\", 's')": "bucket(\"timestamp\", 's')"
|
||||
},
|
||||
"aggregates": { "price": "avg", "timestamp": "dominant" }
|
||||
},
|
||||
{
|
||||
"plugin": "X/Y Scatter",
|
||||
"plugin_config": {},
|
||||
"title": "Closed Orders",
|
||||
"group_by": [],
|
||||
"split_by": [],
|
||||
"columns": [
|
||||
"timestamp",
|
||||
"price",
|
||||
"if(\"status\"!='open'){\"status\"}else{\"side\"}"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"if(\"status\"!='open'){\"status\"}else{\"side\"}": "if(\"status\"!='open'){\"status\"}else{\"side\"}",
|
||||
"if(\"status\"=='closed'){1}else if(\"status\"=='expired'){2}else{0}": "if(\"status\"=='closed'){1}else if(\"status\"=='expired'){2}else{0}"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Y Line",
|
||||
"plugin_config": { "zoom": { "k": 1, "x": 1, "y": -1 } },
|
||||
"title": "Mid Timeseries",
|
||||
"group_by": ["bucket(\"timestamp\", 's')"],
|
||||
"split_by": ["status", "side"],
|
||||
"columns": ["price"],
|
||||
"filter": [["status", "==", "closed"]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"timestamp\", 's')": "bucket(\"timestamp\", 's')"
|
||||
},
|
||||
"aggregates": { "price": "avg" }
|
||||
},
|
||||
{
|
||||
"plugin": "Y Bar",
|
||||
"plugin_config": {},
|
||||
"title": "Depth Timeseries",
|
||||
"group_by": ["bucket(\"timestamp\", 'm')"],
|
||||
"split_by": ["side", "status"],
|
||||
"columns": ["if(\"side\"=='buy'){1}else{-1}"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"timestamp\", 'm')": "bucket(\"timestamp\", 'm')",
|
||||
"if(\"side\"=='buy'){1}else{-1}": "if(\"side\"=='buy'){1}else{-1}",
|
||||
"if(\"side\"=='sell'){-1}else{integer(null)}": "if(\"side\"=='sell'){-1}else{integer(null)}"
|
||||
},
|
||||
"aggregates": { "bucket(\"timestamp\", 'm')": "dominant" }
|
||||
},
|
||||
{
|
||||
"version": "4.5.2",
|
||||
"columns_config": {
|
||||
"Diff from market open": {
|
||||
"number_fg_mode": "bar",
|
||||
"fg_gradient": 12.9
|
||||
},
|
||||
"if(\"side\"=='buy'){-1}else{1}": {
|
||||
"number_fg_mode": "bar",
|
||||
"fg_gradient": 8,
|
||||
"aggregate_depth": 1
|
||||
},
|
||||
"status": {
|
||||
"string_color_mode": "series",
|
||||
"color": "#94aa27"
|
||||
},
|
||||
"side": {
|
||||
"string_color_mode": "series"
|
||||
}
|
||||
},
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {},
|
||||
"settings": true,
|
||||
"title": "Blotter",
|
||||
"group_by": [],
|
||||
"split_by": [],
|
||||
"sort": [["timestamp", "desc"]],
|
||||
"filter": [],
|
||||
"expressions": {
|
||||
"bucket(\"price\", 1)": "bucket(\"price\", 1)",
|
||||
"bucket(\"timestamp\", 'm')": "bucket(\"timestamp\", 'm')",
|
||||
"Diff from market open": "\"price\" - 20"
|
||||
},
|
||||
"columns": [
|
||||
"timestamp",
|
||||
"id",
|
||||
"Diff from market open",
|
||||
"price",
|
||||
"status",
|
||||
"side"
|
||||
],
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"version": "4.5.2",
|
||||
"columns_config": {
|
||||
"if(\"side\"=='buy'){-1}else{1}": {
|
||||
"aggregate_depth": 1,
|
||||
"number_fg_mode": "bar",
|
||||
"fg_gradient": 8
|
||||
}
|
||||
},
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {},
|
||||
"settings": true,
|
||||
"title": "Order Book",
|
||||
"group_by": ["bucket(\"price\", 0.5)"],
|
||||
"split_by": ["side"],
|
||||
"sort": [["bucket(\"price\", 0.5)", "desc"]],
|
||||
"filter": [["status", "==", "open"]],
|
||||
"expressions": {
|
||||
"if(\"side\"=='buy'){-1}else{1}": "if(\"side\"=='buy'){-1}else{1}",
|
||||
"bucket(\"price\", 0.5)": "bucket(\"price\", 0.5)"
|
||||
},
|
||||
"columns": ["if(\"side\"=='buy'){-1}else{1}"],
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "X Bar",
|
||||
"plugin_config": {},
|
||||
"title": "Order Book (Chart)",
|
||||
"group_by": ["bucket(\"price\", 0.5)"],
|
||||
"split_by": ["side"],
|
||||
"columns": ["if(\"side\"=='buy'){-1}else{1}"],
|
||||
"filter": [["status", "==", "open"]],
|
||||
"sort": [["bucket(\"price\", 0.5)", "desc"]],
|
||||
"expressions": {
|
||||
"if(\"side\"=='buy'){-1}else{1}": "if(\"side\"=='buy'){-1}else{1}",
|
||||
"bucket(\"price\", 0.5)": "bucket(\"price\", 0.5)"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Candlestick",
|
||||
"plugin_config": {},
|
||||
"title": "Candlestick",
|
||||
"group_by": ["bucket(\"timestamp\", 'm')"],
|
||||
"split_by": [],
|
||||
"columns": ["price", null, "price 2", "price 3"],
|
||||
"filter": [["status", "==", "closed"]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"timestamp\", 'm')": "bucket(\"timestamp\", 'm')",
|
||||
"price 2": "\"price\"",
|
||||
"price 3": "\"price\""
|
||||
},
|
||||
"aggregates": {
|
||||
"price": "avg",
|
||||
"price 2": "high",
|
||||
"price 3": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"plugin": "Candlestick",
|
||||
"plugin_config": {},
|
||||
"title": "Candlestick 2",
|
||||
"group_by": ["bucket(\"timestamp\", 'm')"],
|
||||
"split_by": [],
|
||||
"columns": ["price", null, null, null],
|
||||
"filter": [["status", "==", "closed"]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"timestamp\", 'm')": "bucket(\"timestamp\", 'm')",
|
||||
"price 2": "\"price\"",
|
||||
"price 3": "\"price\""
|
||||
},
|
||||
"aggregates": {
|
||||
"price": "avg",
|
||||
"price 2": "high",
|
||||
"price 3": "low"
|
||||
}
|
||||
},
|
||||
{
|
||||
"plugin": "X Bar",
|
||||
"plugin_config": {},
|
||||
"title": "Order Book (All Status)",
|
||||
"group_by": ["bucket(\"price\", 0.5)"],
|
||||
"split_by": [],
|
||||
"columns": [
|
||||
"if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}",
|
||||
"if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}",
|
||||
"if(\"status\"=='closed'){1}else{0}"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [["bucket(\"price\", 0.5)", "desc"]],
|
||||
"expressions": {
|
||||
"if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}": "if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}",
|
||||
"bucket(\"timestamp\", 's')": "bucket(\"timestamp\", 's')",
|
||||
"bucket(\"price\", 0.5)": "bucket(\"price\", 0.5)",
|
||||
"if(\"status\"=='open'){\"price\"}else{float(null)}": "if(\"status\"=='open'){\"price\"}else{float(null)}",
|
||||
"if(\"status\"=='closed'){1}else{0}": "if(\"status\"=='closed'){1}else{0}",
|
||||
"if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}": "if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Y Scatter",
|
||||
"plugin_config": {},
|
||||
"title": "Order Book (Scatter All Status)",
|
||||
"group_by": ["bucket(\"price\", 0.5)"],
|
||||
"split_by": [],
|
||||
"columns": [
|
||||
"if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}",
|
||||
"if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}",
|
||||
"if(\"status\"=='closed'){1}else{0}"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}": "if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}",
|
||||
"bucket(\"timestamp\", 's')": "bucket(\"timestamp\", 's')",
|
||||
"bucket(\"price\", 0.5)": "bucket(\"price\", 0.5)",
|
||||
"if(\"status\"=='open'){\"price\"}else{float(null)}": "if(\"status\"=='open'){\"price\"}else{float(null)}",
|
||||
"if(\"status\"=='closed'){1}else{0}": "if(\"status\"=='closed'){1}else{0}",
|
||||
"if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}": "if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "X Bar",
|
||||
"plugin_config": {},
|
||||
"title": "Order Book (Stack All Status)",
|
||||
"group_by": ["bucket(\"price\", 0.5)"],
|
||||
"split_by": ["if(\"status\"=='closed'){'closed'}else{\"side\"}"],
|
||||
"columns": ["price"],
|
||||
"filter": [["status", "!=", "expired"]],
|
||||
"sort": [
|
||||
["bucket(\"price\", 0.5)", "desc"],
|
||||
["status", "col asc"]
|
||||
],
|
||||
"expressions": {
|
||||
"if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}": "if(\"status\"=='open' and \"side\"=='buy'){1}else{float(null)}",
|
||||
"bucket(\"timestamp\", 's')": "bucket(\"timestamp\", 's')",
|
||||
"bucket(\"price\", 0.5)": "bucket(\"price\", 0.5)",
|
||||
"if(\"status\"=='open'){\"price\"}else{float(null)}": "if(\"status\"=='open'){\"price\"}else{float(null)}",
|
||||
"if(\"status\"=='closed'){1}else{0}": "if(\"status\"=='closed'){1}else{0}",
|
||||
"if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}": "if(\"status\"=='open' and \"side\"=='sell'){1}else{float(null)}",
|
||||
"if(\"status\"=='closed'){'closed'}else{\"side\"}": "if(\"status\"=='closed'){'closed'}else{\"side\"}"
|
||||
},
|
||||
"aggregates": {}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,292 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const MSG_BATCH_TIMEOUT = 50;
|
||||
const MSG_PER_BATCH = 10;
|
||||
const MSG_TIME_DELTA = 20 * (100 / MSG_PER_BATCH);
|
||||
const MARKET_OPEN_PRICE = 20;
|
||||
const MARKET_MAX_TRADES = 20000;
|
||||
const MARKET_MIN_TRADES = 5000;
|
||||
const MARKET_OPEN = performance.now();
|
||||
const TRADE_EXPIRATION = 10;
|
||||
|
||||
const SCHEMA = {
|
||||
id: "integer",
|
||||
side: "string",
|
||||
security: "string",
|
||||
price: "float",
|
||||
timestamp: "datetime",
|
||||
status: "string",
|
||||
};
|
||||
|
||||
async function query_columns(table, config) {
|
||||
const view = await table.view(config);
|
||||
const columns = await view.to_columns();
|
||||
await view.delete();
|
||||
return columns;
|
||||
}
|
||||
|
||||
class OrderBook {
|
||||
constructor(table, side) {
|
||||
this._memo = undefined;
|
||||
this._side = side;
|
||||
this._table = table;
|
||||
this._price_view = table.view({
|
||||
columns: ["price"],
|
||||
group_by: ["security"],
|
||||
aggregates: { price: side === "buy" ? "max" : "min" },
|
||||
filter: [
|
||||
["side", "==", side],
|
||||
["status", "==", "open"],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async best_open_price() {
|
||||
if (this._memo === undefined) {
|
||||
const view = await this._price_view;
|
||||
const { price } = await view.to_columns({ leaves_only: true });
|
||||
this._memo = price.length === 0 ? MARKET_OPEN_PRICE : price[0];
|
||||
}
|
||||
|
||||
return this._memo;
|
||||
}
|
||||
|
||||
async matched_orders(price) {
|
||||
const sort_dir = this._side === "buy" ? "desc" : "asc";
|
||||
const op = this._side === "buy" ? ">" : "<";
|
||||
return await query_columns(this._table, {
|
||||
columns: ["id"],
|
||||
filter: [
|
||||
["side", "==", this._side],
|
||||
["status", "==", "open"],
|
||||
["price", op, price],
|
||||
],
|
||||
sort: [
|
||||
["price", sort_dir],
|
||||
["timestamp", "asc"],
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._memo = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class Channel {
|
||||
async consume() {
|
||||
await new Promise((resolve) => {
|
||||
this._resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
emit() {
|
||||
if (this._resolve) {
|
||||
this._resolve();
|
||||
this._resolve = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Market {
|
||||
constructor(table, model) {
|
||||
this._id = 1;
|
||||
this._table = table;
|
||||
this._model = model;
|
||||
this._buy_book = new OrderBook(table, "buy");
|
||||
this._sell_book = new OrderBook(table, "sell");
|
||||
this._stop_signal = new Channel();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this._id < MARKET_MAX_TRADES) {
|
||||
this._id = MARKET_MAX_TRADES;
|
||||
await this._stop_signal.consume();
|
||||
}
|
||||
|
||||
this._id = 1;
|
||||
}
|
||||
|
||||
async poll(progress) {
|
||||
await this._run_market_step();
|
||||
while (this._id < MARKET_MIN_TRADES) {
|
||||
await this._run_market_step(progress);
|
||||
}
|
||||
|
||||
progress?.();
|
||||
this._stop_signal.emit();
|
||||
if (this._id < MARKET_MAX_TRADES) {
|
||||
setTimeout(this.poll.bind(this), MSG_BATCH_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
async _run_market_step(progress) {
|
||||
if (await this._generate_trades()) {
|
||||
await this._clear_trades();
|
||||
await this._expire_trades();
|
||||
}
|
||||
|
||||
this._buy_book.reset();
|
||||
this._sell_book.reset();
|
||||
progress?.(this._id / MARKET_MIN_TRADES);
|
||||
}
|
||||
|
||||
async _generate_trades() {
|
||||
const trades = [];
|
||||
const timestamp = new Date(MARKET_OPEN + this._id * MSG_TIME_DELTA);
|
||||
for (let i = 0; i < MSG_PER_BATCH; i++) {
|
||||
const { side, discount } = this._model();
|
||||
const book = side === "buy" ? this._sell_book : this._buy_book;
|
||||
const best_open_price = await book.best_open_price();
|
||||
trades.push({
|
||||
security: "Prospective Co",
|
||||
status: "open",
|
||||
id: this._id++,
|
||||
price: discount + best_open_price,
|
||||
side,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
if (trades.length > 0) {
|
||||
await this._table.update(trades);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async _clear_trades() {
|
||||
const sell_price = await this._sell_book.best_open_price();
|
||||
const buy_price = await this._buy_book.best_open_price();
|
||||
const { id: buys } = await this._buy_book.matched_orders(sell_price);
|
||||
const { id: sells } = await this._sell_book.matched_orders(buy_price);
|
||||
const num_clear = Math.min(buys.length, sells.length);
|
||||
const status = Array(num_clear * 2).fill("closed");
|
||||
const id = buys.slice(0, num_clear).concat(sells.slice(0, num_clear));
|
||||
if (id.length > 0) {
|
||||
await this._table.update({ status, id });
|
||||
}
|
||||
}
|
||||
|
||||
async _expire_trades() {
|
||||
const expired = await query_columns(this._table, {
|
||||
columns: ["id"],
|
||||
filter: [
|
||||
["status", "==", "open"],
|
||||
["id", "<", this._id - MSG_PER_BATCH * TRADE_EXPIRATION],
|
||||
],
|
||||
});
|
||||
|
||||
if (expired.id.length > 0) {
|
||||
expired.status = Array(expired.id.length).fill("expired");
|
||||
await this._table.update(expired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SKEW_MODEL_OFFSET = 2;
|
||||
const SKEW_MODEL_STDDEV = 2;
|
||||
const SKEW_MODEL_SKEW = 0;
|
||||
|
||||
function random_skew_normal(bias) {
|
||||
const u3 = Math.random(),
|
||||
u2 = Math.random();
|
||||
const R = Math.sqrt(-2.0 * Math.log(u3));
|
||||
const O = 2.0 * Math.PI * u2;
|
||||
const u0 = R * Math.cos(O);
|
||||
const v = R * Math.sin(O);
|
||||
if (SKEW_MODEL_SKEW === 0) {
|
||||
return bias * SKEW_MODEL_OFFSET + SKEW_MODEL_STDDEV * u0;
|
||||
} else {
|
||||
const n = -bias * SKEW_MODEL_SKEW;
|
||||
const s = n / Math.sqrt(1 + Math.pow(SKEW_MODEL_SKEW, 2));
|
||||
const u1 = s * u0 + Math.sqrt(1 - s * s) * v;
|
||||
const z = u0 >= 0 ? u1 : -u1;
|
||||
return bias * SKEW_MODEL_OFFSET + SKEW_MODEL_STDDEV * z;
|
||||
}
|
||||
}
|
||||
|
||||
function skew_model() {
|
||||
const [side, bias] = Math.random() > 0.5 ? ["buy", -1] : ["sell", 1];
|
||||
const discount = random_skew_normal(bias);
|
||||
return { side, discount };
|
||||
}
|
||||
|
||||
function progress(x) {
|
||||
const button = document.querySelector("button");
|
||||
if (x !== undefined) {
|
||||
const y = (x * 10).toFixed(0) * 10;
|
||||
button.textContent = `Generating trades ${y}%`;
|
||||
} else {
|
||||
button.textContent = "Reset";
|
||||
}
|
||||
}
|
||||
|
||||
async function reset_tables(market, market_table, gui_table) {
|
||||
await market.stop();
|
||||
await gui_table.size();
|
||||
await market_table.clear();
|
||||
await gui_table.clear();
|
||||
await market.poll(progress);
|
||||
}
|
||||
|
||||
async function init_tables() {
|
||||
const market_worker = await perspective.worker();
|
||||
const gui_worker = await perspective.worker();
|
||||
const market_table = await market_worker.table(SCHEMA, { index: "id" });
|
||||
const market_view = await market_table.view();
|
||||
const gui_table = await gui_worker.table(market_view, {
|
||||
index: "id",
|
||||
name: "gui",
|
||||
});
|
||||
|
||||
return { market_table, gui_table, gui_worker };
|
||||
}
|
||||
|
||||
async function init_layouts() {
|
||||
const req = await fetch("layouts.json");
|
||||
return await req.json();
|
||||
}
|
||||
|
||||
const INIT_TASK = [init_tables(), init_layouts()];
|
||||
|
||||
const [{ market_table, gui_table, gui_worker }, layouts] =
|
||||
await Promise.all(INIT_TASK);
|
||||
const market = new Market(market_table, skew_model);
|
||||
const settings = !/(iPad|iPhone|iPod)/g.test(navigator.userAgent);
|
||||
const select = document.querySelector("select");
|
||||
const button = document.querySelector("button");
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
viewer.load(gui_worker);
|
||||
viewer.restore({ theme: "Pro Dark", table: "gui", settings, ...layouts[0] });
|
||||
await market.poll(progress);
|
||||
for (const layout of layouts) {
|
||||
const option = document.createElement("option");
|
||||
option.value = layout.title;
|
||||
option.textContent = layout.title;
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
reset_tables(market, market_table, gui_table);
|
||||
});
|
||||
|
||||
select.addEventListener("change", async (event) => {
|
||||
const layout = layouts.find((x) => x.title === event.target.value);
|
||||
await viewer.restore(layout);
|
||||
});
|
||||
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,2 @@
|
||||
license: apache-2.0
|
||||
height: 800
|
||||
@@ -0,0 +1,11 @@
|
||||
Demo of a [Perspective](https://github.com/perspective-dev/perspective)
|
||||
visualizing a dataset of 3,201 films. Each row in the dataset is a film, with
|
||||
dimensional data such as production studio, rating and genre, as well as
|
||||
observations such as box-office gross.
|
||||
|
||||
This dataset is bootstrapped off the [Vega](https://vega.github.io/vega/)
|
||||
[demo page](https://vega.github.io/editor/data/movies.json"), and has no server
|
||||
component itself, showing how easy it is to use Perspective's client-side
|
||||
architecture to get a fully-interactive BI experience, on top of
|
||||
already-deployed web data artifacts without modification or intermediate
|
||||
servers.
|
||||
@@ -0,0 +1,77 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/pro-dark.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/workspace/dist/css/pro-dark.css" />
|
||||
<style>
|
||||
perspective-workspace {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
perspective-viewer {
|
||||
--d3fc-positive--gradient: linear-gradient(#94d0ff, #8795e8, #966bff, #ad8cff, #c774e8, #c774a9, #ff6ad5, #ff6a8b, #ff8b8b, #ffa58b, #ffde8b, #cdde8b, #8bde8b, #20de8b) !important;
|
||||
--d3fc-negative--gradient: linear-gradient(#94d0ff, #8795e8, #966bff, #ad8cff, #c774e8, #c774a9, #ff6ad5, #ff6a8b, #ff8b8b, #ffa58b, #ffde8b, #cdde8b, #8bde8b, #20de8b) !important;
|
||||
--d3fc-full--gradient: linear-gradient(#94d0ff, #8795e8, #966bff, #ad8cff, #c774e8, #c774a9, #ff6ad5, #ff6a8b, #ff8b8b, #ffa58b, #ffde8b, #cdde8b, #8bde8b, #20de8b) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-workspace id="workspace"> </perspective-workspace>
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/workspace/dist/cdn/perspective-workspace.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const SCHEMA = {
|
||||
Title: "string",
|
||||
"US Gross": "float",
|
||||
"Worldwide Gross": "float",
|
||||
"US DVD Sales": "float",
|
||||
"Production Budget": "float",
|
||||
"Release Date": "date",
|
||||
"MPAA Rating": "string",
|
||||
"Running Time min": "integer",
|
||||
Distributor: "string",
|
||||
Source: "string",
|
||||
"Major Genre": "string",
|
||||
"Creative Type": "string",
|
||||
Director: "string",
|
||||
"Rotten Tomatoes Rating": "integer",
|
||||
"IMDB Rating": "float",
|
||||
"IMDB Votes": "integer",
|
||||
};
|
||||
|
||||
const MOVIES_URL = "https://vega.github.io/editor/data/movies.json";
|
||||
|
||||
(async () => {
|
||||
const request = fetch(MOVIES_URL);
|
||||
const worker = await perspective.worker();
|
||||
const response = await request;
|
||||
const json = await response.json();
|
||||
for (const row of json) {
|
||||
row["Release Date"] = row["Release Date"] ? new Date(row["Release Date"]) || null : null;
|
||||
}
|
||||
|
||||
const table = await worker.table(SCHEMA, { name: "movies" });
|
||||
table.update(json);
|
||||
let req = await fetch("layout.json");
|
||||
let layout = await req.json();
|
||||
await window.workspace.load(worker);
|
||||
await window.workspace.restore(layout);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"sizes": [0.34555410083131466, 0.6544458991686853],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "tab-area",
|
||||
"widgets": [
|
||||
"PERSPECTIVE_GENERATED_ID_0",
|
||||
"PERSPECTIVE_GENERATED_ID_1",
|
||||
"PERSPECTIVE_GENERATED_ID_3"
|
||||
],
|
||||
"currentIndex": 0
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"master": {
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_2"],
|
||||
"sizes": [1]
|
||||
},
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_2": {
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {
|
||||
"IMDB Rating": {
|
||||
"gradient": 8.1,
|
||||
"neg_color": "#FF9485",
|
||||
"number_color_mode": "gradient",
|
||||
"pos_color": "#b87ff0"
|
||||
},
|
||||
"US Gross": {
|
||||
"fg_gradient": 19729862602,
|
||||
"number_fg_mode": "bar"
|
||||
}
|
||||
},
|
||||
"editable": false,
|
||||
"scroll_lock": true
|
||||
},
|
||||
"settings": false,
|
||||
"theme": "Pro Dark",
|
||||
"group_by": ["Distributor"],
|
||||
"split_by": [],
|
||||
"columns": ["US Gross"],
|
||||
"filter": [],
|
||||
"sort": [["US Gross", "desc"]],
|
||||
"expressions": {},
|
||||
"aggregates": {},
|
||||
"master": true,
|
||||
"name": null,
|
||||
"table": "movies",
|
||||
"linked": false,
|
||||
"selectable": "true"
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_0": {
|
||||
"plugin": "Heatmap",
|
||||
"plugin_config": {},
|
||||
"settings": false,
|
||||
"theme": "Pro Dark",
|
||||
"group_by": ["bucket(\"Rotten Tomatoes Rating\", 2.5)"],
|
||||
"split_by": ["bucket(\"IMDB Rating\", 0.25)"],
|
||||
"columns": ["US Gross"],
|
||||
"filter": [["bucket(\"IMDB Rating\", 0.25)", "is not null", null]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"IMDB Rating\", 0.25)": "bucket(\"IMDB Rating\", 0.25)",
|
||||
"bucket(\"Rotten Tomatoes Rating\", 2.5)": "bucket(\"Rotten Tomatoes Rating\", 2.5)"
|
||||
},
|
||||
"aggregates": {
|
||||
"bucket(\"Rotten Tomatoes Rating\", 2.5)": "avg"
|
||||
},
|
||||
"master": false,
|
||||
"name": null,
|
||||
"table": "movies",
|
||||
"linked": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_1": {
|
||||
"plugin": "Y Line",
|
||||
"plugin_config": {
|
||||
"legend": {
|
||||
"left": "79px",
|
||||
"top": "174px"
|
||||
},
|
||||
"splitMainValues": ["Rotten Tomatoes Rating"]
|
||||
},
|
||||
"settings": false,
|
||||
"theme": "Pro Dark",
|
||||
"group_by": ["bucket(\"Release Date\", 'Y')"],
|
||||
"split_by": [],
|
||||
"columns": [
|
||||
"US Gross",
|
||||
"Rotten Tomatoes Rating",
|
||||
"Production Budget",
|
||||
"Worldwide Gross",
|
||||
"US DVD Sales"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"Release Date\", 'Y')": "bucket(\"Release Date\", 'Y')"
|
||||
},
|
||||
"aggregates": {
|
||||
"Rotten Tomatoes Rating": "avg"
|
||||
},
|
||||
"master": false,
|
||||
"name": "Ratings vs Sales",
|
||||
"table": "movies",
|
||||
"linked": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_3": {
|
||||
"plugin": "Y Area",
|
||||
"plugin_config": {
|
||||
"legend": {
|
||||
"left": "91px",
|
||||
"top": "25px"
|
||||
}
|
||||
},
|
||||
"settings": false,
|
||||
"theme": "Pro Dark",
|
||||
"group_by": ["bucket(\"Release Date\", 'Y')"],
|
||||
"split_by": ["Major Genre"],
|
||||
"columns": ["US Gross"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"Release Date\", 'Y')": "bucket(\"Release Date\", 'Y')"
|
||||
},
|
||||
"aggregates": {},
|
||||
"master": false,
|
||||
"name": "US Gross by Genre",
|
||||
"table": "movies",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,3 @@
|
||||
Visualizations that utilize
|
||||
[NYPD CCRB Data](https://github.com/new-york-civil-liberties-union/NYPD-Misconduct-Complaint-Database-Updated)
|
||||
that was obtained via a FOIL Request.
|
||||
@@ -0,0 +1,78 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
// Gets the updated data from the NYCLU and prepare it for Perspective.
|
||||
|
||||
import perspective from "@perspective-dev/client";
|
||||
import { Uint8ArrayReader, ZipReader, TextWriter } from "@zip.js/zip.js";
|
||||
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as url from "url";
|
||||
import * as fss from "node:fs";
|
||||
|
||||
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
let DATA_URL =
|
||||
"https://rawcdn.githack.com/new-york-civil-liberties-union/NYPD-Misconduct-Complaint-Database-Updated/f6cea944b347c96eb26b76323013640dff4b3d00/CCRB%20Complaint%20Database%20Raw%2004.28.2023.zip?min=1";
|
||||
|
||||
async function fetch_progress(url) {
|
||||
const response = await fetch(url);
|
||||
const reader = response.body.getReader();
|
||||
const contentLength = +response.headers.get("Content-Length");
|
||||
let receivedLength = 0;
|
||||
let chunks = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
let pct = ((100 * receivedLength) / contentLength).toFixed(1);
|
||||
console.log(`Downloading (${pct}%)`);
|
||||
}
|
||||
|
||||
let chunksAll = new Uint8Array(receivedLength);
|
||||
let position = 0;
|
||||
for (let chunk of chunks) {
|
||||
chunksAll.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
const zipFileReader = new Uint8ArrayReader(chunksAll);
|
||||
const zipReader = new ZipReader(zipFileReader);
|
||||
const entries = await zipReader.getEntries();
|
||||
const csv = await entries[0].getData(new TextWriter(), {
|
||||
onprogress: (p, t) => console.log(`(${p}b / ${t}b)`),
|
||||
});
|
||||
|
||||
zipReader.close();
|
||||
|
||||
let t = await perspective.table(csv);
|
||||
let view = await t.view();
|
||||
|
||||
return new Uint8Array(await view.to_arrow());
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fss.existsSync(`${__dirname}/nypdccrb.arrow`)) {
|
||||
await fs.writeFile(
|
||||
`${__dirname}/nypdccrb.arrow`,
|
||||
await fetch_progress(DATA_URL),
|
||||
);
|
||||
|
||||
console.log("Wrote nypdccrb.arrow");
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,14 @@
|
||||
.dark button {
|
||||
background-color: inherit;
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.dark #buttons {
|
||||
background-color: #242526;
|
||||
}
|
||||
|
||||
.dark select {
|
||||
background-color: #242526;
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
perspective-viewer[theme="Pro Light"],
|
||||
perspective-viewer[theme="Pro Dark"] {
|
||||
--d3fc-positive--gradient: linear-gradient(#94d0ff,
|
||||
#8795e8,
|
||||
#966bff,
|
||||
#ad8cff,
|
||||
#c774e8,
|
||||
#c774a9,
|
||||
#ff6ad5,
|
||||
#ff6a8b,
|
||||
#ff8b8b,
|
||||
#ffa58b,
|
||||
#ffde8b,
|
||||
#cdde8b,
|
||||
#8bde8b,
|
||||
#20de8b);
|
||||
}
|
||||
|
||||
perspective-workspace {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#buttons>* {
|
||||
padding: 6px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border: 1px solid #00f !important;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#message {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
#workspace {
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
button, select {
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/pro-dark.css" />
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="./dark.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="./light.css" />
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.6.81/+esm"></script>
|
||||
<script type="module" src="index.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -0,0 +1,186 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/workspace/dist/cdn/perspective-workspace.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
let DATA_URL = "nypdccrb.arrow";
|
||||
|
||||
let LAYOUTS = localStorage.getItem("layouts")
|
||||
? JSON.parse(localStorage.getItem("layouts"))
|
||||
: undefined;
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const theme_style_node = document.createElement("style");
|
||||
document.head.appendChild(theme_style_node);
|
||||
let DARK_THEME;
|
||||
let LIGHT_THEME;
|
||||
|
||||
function toggle_theme() {
|
||||
if (theme_style_node.dataset.theme === "Pro Light") {
|
||||
theme_style_node.textContent = DARK_THEME;
|
||||
document.body.classList.add("dark");
|
||||
window.theme.textContent = "Light Theme";
|
||||
for (const view of document.querySelectorAll("perspective-viewer")) {
|
||||
view.setAttribute("theme", "Pro Dark");
|
||||
view.restyleElement();
|
||||
}
|
||||
|
||||
theme_style_node.dataset.theme = "Pro Dark";
|
||||
} else {
|
||||
theme_style_node.textContent = LIGHT_THEME;
|
||||
document.body.classList.remove("dark");
|
||||
window.theme.textContent = "Dark Theme";
|
||||
for (const view of document.querySelectorAll("perspective-viewer")) {
|
||||
view.setAttribute("theme", "Pro Light");
|
||||
view.restyleElement();
|
||||
}
|
||||
theme_style_node.dataset.theme = "Pro Light";
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch_progress(url) {
|
||||
window.message.textContent = "Downloading...";
|
||||
const response = await fetch(url);
|
||||
const ab = await new Response(await response.blob()).arrayBuffer();
|
||||
window.message.style.display = "none";
|
||||
return ab;
|
||||
}
|
||||
|
||||
DARK_THEME = await fetch(
|
||||
"/node_modules/@perspective-dev/workspace/dist/css/pro-dark.css",
|
||||
).then((x) => x.text());
|
||||
|
||||
LIGHT_THEME = await fetch(
|
||||
"/node_modules/@perspective-dev/workspace/dist/css/pro.css",
|
||||
).then((x) => x.text());
|
||||
|
||||
document.body.innerHTML = `
|
||||
<style>
|
||||
</style>
|
||||
<div id='buttons'>
|
||||
<span id="message"></span>
|
||||
<select id="layouts"></select>
|
||||
<button id="save_as">Save As</button>
|
||||
<input id="name_input" style="display: none"></input>
|
||||
<button id="save" style="display: none">Save</button>
|
||||
<button id="cancel" style="display: none">Cancel</button>
|
||||
<button id="theme" style="float: right">Light Theme</button>
|
||||
<button id="copy" style="float: right">Debug to Clipboard</button>
|
||||
<button id="reset" style="float: right">Reset LocalStorage</button>
|
||||
<a href="https://github.com/texodus/nypd-ccrb">NYCLU/CCRB Data</a>
|
||||
<a href="https://github.com/perspective-dev/perspective">Built With Perspective</a>
|
||||
</div>
|
||||
<perspective-workspace id='workspace'></perspective-workspace>
|
||||
`.trim();
|
||||
|
||||
toggle_theme();
|
||||
|
||||
window.workspace.addEventListener(
|
||||
"workspace-new-view",
|
||||
({ detail: { widget } }) => {
|
||||
widget.viewer.setAttribute("theme", theme_style_node.dataset.theme);
|
||||
},
|
||||
);
|
||||
|
||||
window.workspace.addTable(
|
||||
"ccrb",
|
||||
(async () => worker.table(await fetch_progress(DATA_URL)))(),
|
||||
// worker.table(await fetch_progress(DATA_URL))
|
||||
);
|
||||
|
||||
if (LAYOUTS == undefined) {
|
||||
LAYOUTS = await (await fetch("./layout.json")).json();
|
||||
}
|
||||
|
||||
const layout_names = Object.keys(LAYOUTS);
|
||||
let selected_layout = LAYOUTS[layout_names[0]];
|
||||
await window.workspace.restore(selected_layout);
|
||||
|
||||
function set_layout_options() {
|
||||
const layout_names = Object.keys(LAYOUTS);
|
||||
window.layouts.innerHTML = "";
|
||||
for (const layout of layout_names) {
|
||||
window.layouts.innerHTML += `<option${
|
||||
layout === selected_layout ? " selected='true'" : ""
|
||||
}>${layout}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
set_layout_options();
|
||||
|
||||
window.name_input.value = layout_names[0];
|
||||
window.layouts.addEventListener("change", async () => {
|
||||
if (window.layouts.value.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.workspace.innerHTML = "";
|
||||
await window.workspace.restore(LAYOUTS[window.layouts.value]);
|
||||
window.name_input.value = window.layouts.value;
|
||||
});
|
||||
|
||||
window.save_as.addEventListener("click", async () => {
|
||||
window.save_as.style.display = "none";
|
||||
window.save.style.display = "inline-block";
|
||||
window.cancel.style.display = "inline-block";
|
||||
window.name_input.style.display = "inline-block";
|
||||
window.copy.style.display = "none";
|
||||
window.layouts.style.display = "none";
|
||||
});
|
||||
|
||||
function cancel() {
|
||||
window.save_as.style.display = "inline-block";
|
||||
window.save.style.display = "none";
|
||||
window.cancel.style.display = "none";
|
||||
window.name_input.style.display = "none";
|
||||
window.copy.style.display = "inline-block";
|
||||
window.layouts.style.display = "inline-block";
|
||||
}
|
||||
|
||||
window.cancel.addEventListener("click", cancel);
|
||||
|
||||
window.reset.addEventListener("click", () => {
|
||||
localStorage.clear();
|
||||
window.reset.innerText = "Reset!";
|
||||
setTimeout(() => {
|
||||
window.reset.innerText = "Reset LocalStorage";
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
window.save.addEventListener("click", async () => {
|
||||
const token = await window.workspace.save();
|
||||
const new_name = window.name_input.value;
|
||||
LAYOUTS[new_name] = token;
|
||||
set_layout_options();
|
||||
window.layouts.value = new_name;
|
||||
window.save_as.innerText = "Saved!";
|
||||
setTimeout(() => {
|
||||
window.save_as.innerText = "Save As";
|
||||
}, 1000);
|
||||
localStorage.setItem("layouts", JSON.stringify(LAYOUTS));
|
||||
cancel();
|
||||
});
|
||||
|
||||
window.copy.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(JSON.stringify(LAYOUTS));
|
||||
window.copy.innerText = "Copied!";
|
||||
setTimeout(() => {
|
||||
window.copy.innerText = "Debug to Clipboard";
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
window.theme.addEventListener("click", toggle_theme);
|
||||
@@ -0,0 +1,426 @@
|
||||
{
|
||||
"Raw": {
|
||||
"sizes": [1],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_3"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_3": {
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {},
|
||||
"editable": false,
|
||||
"scroll_lock": false
|
||||
},
|
||||
"settings": false,
|
||||
"title": "Raw",
|
||||
"group_by": [],
|
||||
"split_by": [],
|
||||
"columns": [
|
||||
"AsOfDate",
|
||||
"AllegationID",
|
||||
"LastName",
|
||||
"FirstName",
|
||||
"OfficerRace",
|
||||
"OfficerGender",
|
||||
"TaxID",
|
||||
"CurrentRank",
|
||||
"CurrentRankLong",
|
||||
"CurrentCommand",
|
||||
"IncidentRank",
|
||||
"IncidentRankLong",
|
||||
"IncidentCommand",
|
||||
"ShieldNo",
|
||||
"DaysOnForce",
|
||||
"Status",
|
||||
"LastActive",
|
||||
"ComplaintID",
|
||||
"IncidentDate",
|
||||
"FADOType",
|
||||
"Allegation",
|
||||
"CCRBDisposition",
|
||||
"BoardCat",
|
||||
"PenaltyRec",
|
||||
"PenaltyCat",
|
||||
"PenaltyDesc",
|
||||
"LocationType",
|
||||
"ContactReason",
|
||||
"ContactOutcome",
|
||||
"IncidentPrecinct",
|
||||
"ImpactedRace",
|
||||
"ImpactedGender",
|
||||
"ImpactedAge",
|
||||
"ReceivedDate",
|
||||
"CloseDate"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {},
|
||||
"aggregates": {},
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Incidents per Month": {
|
||||
"sizes": [1],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_3"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_3": {
|
||||
"plugin": "Y Line",
|
||||
"plugin_config": {},
|
||||
"settings": false,
|
||||
"title": "Incidents",
|
||||
"group_by": ["bucket(\"IncidentDate\", 'M')"],
|
||||
"split_by": [],
|
||||
"columns": ["ComplaintID"],
|
||||
"filter": [
|
||||
["IncidentDate", ">", "1985-01-01"],
|
||||
["IncidentDate", "<", "2025-01-01"]
|
||||
],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')"
|
||||
},
|
||||
"aggregates": { "ComplaintID": "distinct count" },
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Incidents per Month by FADO": {
|
||||
"sizes": [1],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "split-area",
|
||||
"orientation": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_0"],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_2"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
],
|
||||
"sizes": [0.5, 0.5]
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_0": {
|
||||
"plugin": "Y Line",
|
||||
"plugin_config": { "zoom": { "k": 1, "x": 0, "y": 0 } },
|
||||
"settings": false,
|
||||
"title": "Incidents (line)",
|
||||
"group_by": ["bucket(\"IncidentDate\", 'M')"],
|
||||
"split_by": ["FADOType"],
|
||||
"columns": ["AllegationID"],
|
||||
"filter": [
|
||||
["IncidentDate", ">", "1985-01-01"],
|
||||
["IncidentDate", "<", "2025-01-01"]
|
||||
],
|
||||
"sort": [["AllegationID", "col asc"]],
|
||||
"expressions": {
|
||||
"bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')"
|
||||
},
|
||||
"aggregates": { "AllegationID": "distinct count" },
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_2": {
|
||||
"plugin": "Y Area",
|
||||
"plugin_config": {},
|
||||
"settings": false,
|
||||
"title": "Incidents (area)",
|
||||
"group_by": ["bucket(\"IncidentDate\", 'M')"],
|
||||
"split_by": ["FADOType"],
|
||||
"columns": ["AllegationID"],
|
||||
"filter": [
|
||||
["IncidentDate", ">", "1985-01-01"],
|
||||
["IncidentDate", "<", "2025-01-01"]
|
||||
],
|
||||
"sort": [["AllegationID", "col asc"]],
|
||||
"expressions": {
|
||||
"bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')"
|
||||
},
|
||||
"aggregates": { "AllegationID": "distinct count" },
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Total Complaints vs Officer Count by Command and FADO": {
|
||||
"sizes": [1],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "split-area",
|
||||
"orientation": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_4"],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_7"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
],
|
||||
"sizes": [0.5, 0.5]
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_4": {
|
||||
"plugin": "X/Y Scatter",
|
||||
"plugin_config": {},
|
||||
"settings": false,
|
||||
"title": "Active Officer Incidents",
|
||||
"group_by": ["IncidentCommand"],
|
||||
"split_by": ["FADOType"],
|
||||
"columns": ["ShieldNo", "ComplaintID", null, null, null, null],
|
||||
"filter": [["Status", "==", "Active"]],
|
||||
"sort": [],
|
||||
"expressions": {},
|
||||
"aggregates": {
|
||||
"ShieldNo": "distinct count",
|
||||
"ComplaintID": "distinct count"
|
||||
},
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_7": {
|
||||
"plugin": "X/Y Scatter",
|
||||
"plugin_config": { "zoom": { "k": 1, "x": 0, "y": 0 } },
|
||||
"settings": false,
|
||||
"title": "Inactive Officer Incidents",
|
||||
"group_by": ["IncidentCommand"],
|
||||
"split_by": ["FADOType"],
|
||||
"columns": ["ShieldNo", "ComplaintID", null, null, null, null],
|
||||
"filter": [["Status", "==", "Retired"]],
|
||||
"sort": [],
|
||||
"expressions": {},
|
||||
"aggregates": {
|
||||
"ShieldNo": "distinct count",
|
||||
"ComplaintID": "distinct count"
|
||||
},
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Incidents by Allegation, time series": {
|
||||
"sizes": [0.3654752553024352, 0.6345247446975648],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_13"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"master": { "widgets": ["PERSPECTIVE_GENERATED_ID_11"], "sizes": [1] },
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_11": {
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {},
|
||||
"editable": false,
|
||||
"scroll_lock": false
|
||||
},
|
||||
"settings": false,
|
||||
"title": "Filter",
|
||||
"group_by": ["FADOType", "Allegation"],
|
||||
"split_by": [],
|
||||
"columns": ["ComplaintID"],
|
||||
"filter": [],
|
||||
"sort": [["ComplaintID", "desc"]],
|
||||
"expressions": {},
|
||||
"aggregates": { "ComplaintID": "distinct count" },
|
||||
"master": true,
|
||||
"table": "ccrb",
|
||||
"linked": false,
|
||||
"selectable": ""
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_13": {
|
||||
"plugin": "Y Line",
|
||||
"plugin_config": {},
|
||||
"settings": false,
|
||||
"title": "Incidents",
|
||||
"group_by": ["bucket(\"IncidentDate\", 'M')"],
|
||||
"split_by": ["FADOType"],
|
||||
"columns": ["ComplaintID"],
|
||||
"filter": [
|
||||
["IncidentDate", ">", "1985-01-01"],
|
||||
["IncidentDate", "<", "2025-01-01"]
|
||||
],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')"
|
||||
},
|
||||
"aggregates": { "ComplaintID": "distinct count" },
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Allegation reporting change, 1995-2000": {
|
||||
"sizes": [1],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_10"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_10": {
|
||||
"plugin": "Heatmap",
|
||||
"plugin_config": {
|
||||
"zoom": {
|
||||
"k": 0.959264119325269,
|
||||
"x": 23.20639207453189,
|
||||
"y": 10.58613905921925
|
||||
}
|
||||
},
|
||||
"settings": false,
|
||||
"title": "Incidents",
|
||||
"group_by": ["bucket(\"IncidentDate\", '3M')"],
|
||||
"split_by": ["Allegation"],
|
||||
"columns": ["AllegationID"],
|
||||
"filter": [
|
||||
["IncidentDate", ">", "1985-01-01"],
|
||||
["IncidentDate", "<", "2025-01-01"]
|
||||
],
|
||||
"sort": [["IncidentDate", "col asc"]],
|
||||
"expressions": {
|
||||
"bucket(\"IncidentDate\", '3M')": "bucket(\"IncidentDate\", '3M')"
|
||||
},
|
||||
"aggregates": {
|
||||
"IncidentDate": "median",
|
||||
"AllegationID": "distinct count"
|
||||
},
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"Day-of-Week Incident Heatmap": {
|
||||
"sizes": [0.3686174391201885, 0.6313825608798115],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "split-area",
|
||||
"orientation": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_13"],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_15"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
],
|
||||
"sizes": [0.5, 0.5]
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"master": { "widgets": ["PERSPECTIVE_GENERATED_ID_11"], "sizes": [1] },
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_11": {
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {},
|
||||
"editable": false,
|
||||
"scroll_lock": false
|
||||
},
|
||||
"settings": false,
|
||||
"title": "Filter",
|
||||
"group_by": ["FADOType", "Allegation"],
|
||||
"split_by": [],
|
||||
"columns": ["ComplaintID"],
|
||||
"filter": [],
|
||||
"sort": [["ComplaintID", "desc"]],
|
||||
"expressions": {},
|
||||
"aggregates": { "ComplaintID": "distinct count" },
|
||||
"master": true,
|
||||
"table": "ccrb",
|
||||
"linked": false,
|
||||
"selectable": ""
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_13": {
|
||||
"plugin": "Heatmap",
|
||||
"plugin_config": {},
|
||||
"settings": false,
|
||||
"title": "Year vs Month of Incident",
|
||||
"group_by": ["bucket(\"IncidentDate\", 'Y')"],
|
||||
"split_by": ["month_of_year(\"IncidentDate\")"],
|
||||
"columns": ["ComplaintID"],
|
||||
"filter": [
|
||||
["IncidentDate", ">", "1985-01-01"],
|
||||
["IncidentDate", "<", "2025-01-01"]
|
||||
],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"IncidentDate\", 'Y')": "bucket(\"IncidentDate\", 'Y')",
|
||||
"month_of_year(\"IncidentDate\")": "month_of_year(\"IncidentDate\")"
|
||||
},
|
||||
"aggregates": { "ComplaintID": "distinct count" },
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_15": {
|
||||
"plugin": "Heatmap",
|
||||
"plugin_config": {},
|
||||
"settings": false,
|
||||
"title": "Month vs Weekday of Incident",
|
||||
"group_by": ["month_of_year(\"IncidentDate\")"],
|
||||
"split_by": ["day_of_week(\"IncidentDate\")"],
|
||||
"columns": ["ComplaintID"],
|
||||
"filter": [
|
||||
["IncidentDate", ">", "1985-01-01"],
|
||||
["IncidentDate", "<", "2025-01-01"]
|
||||
],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"IncidentDate\", 'Y')": "bucket(\"IncidentDate\", 'Y')",
|
||||
"month_of_year(\"IncidentDate\")": "month_of_year(\"IncidentDate\")",
|
||||
"day_of_week(\"IncidentDate\")": "day_of_week(\"IncidentDate\")"
|
||||
},
|
||||
"aggregates": { "ComplaintID": "distinct count" },
|
||||
"master": false,
|
||||
"table": "ccrb",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
button {
|
||||
background-color: inherit;
|
||||
border: 1px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#buttons {
|
||||
background-color: #e1e6ea;
|
||||
}
|
||||
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,2 @@
|
||||
license: apache-2.0
|
||||
height: 800
|
||||
@@ -0,0 +1,6 @@
|
||||
Demo of [Perspective](https://github.com/perspective-dev/perspective) with
|
||||
~270,000 rows.
|
||||
|
||||
This dataset from
|
||||
[Kaggle](https://www.kaggle.com/datasets/heesoo37/120-years-of-olympic-history-athletes-and-results)
|
||||
provides a row for every athlete to attend the Olympics since 1896.
|
||||
@@ -0,0 +1,63 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 { execSync } from "child_process";
|
||||
import perspective from "@perspective-dev/client";
|
||||
import {
|
||||
Uint8ArrayReader,
|
||||
ZipReader,
|
||||
TextWriter,
|
||||
TextReader,
|
||||
} from "@zip.js/zip.js";
|
||||
|
||||
// import * as fs from "node:fs/promises";
|
||||
import * as url from "url";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
async function main() {
|
||||
const __dirname = url
|
||||
.fileURLToPath(new URL(".", import.meta.url))
|
||||
.slice(0, -1);
|
||||
|
||||
if (fs.existsSync(`${__dirname}/olympics.arrow`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
execSync(
|
||||
`cd ${__dirname} && kaggle datasets download -d heesoo37/120-years-of-olympic-history-athletes-and-results`,
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
|
||||
const zip = fs.readFileSync(
|
||||
`${__dirname}/120-years-of-olympic-history-athletes-and-results.zip`,
|
||||
);
|
||||
|
||||
const textReader = new TextReader(zip);
|
||||
const zipReader = new ZipReader(textReader);
|
||||
const entries = await zipReader.getEntries();
|
||||
const csv = await entries[0].getData(new TextWriter(), {
|
||||
onprogress: (p, t) => console.log(`(${p}b / ${t}b)`),
|
||||
});
|
||||
|
||||
zipReader.close();
|
||||
|
||||
const table = await perspective.table(csv);
|
||||
const view = await table.view();
|
||||
const arrow = await view.to_arrow();
|
||||
fs.writeFileSync(`${__dirname}/olympics.arrow`, Buffer.from(arrow));
|
||||
fs.unlinkSync(
|
||||
`${__dirname}/120-years-of-olympic-history-athletes-and-results.zip`,
|
||||
);
|
||||
await view.delete();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,90 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/pro.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/workspace/dist/css/pro.css" />
|
||||
<style>
|
||||
perspective-workspace {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
perspective-viewer[theme="Pro Light"] {
|
||||
--d3fc-positive--gradient: linear-gradient(#94d0ff, #8795e8, #966bff, #ad8cff, #c774e8, #c774a9, #ff6ad5, #ff6a8b, #ff8b8b, #ffa58b, #ffde8b, #cdde8b, #8bde8b, #20de8b);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-workspace id="workspace"></perspective-workspace>
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/workspace/dist/cdn/perspective-workspace.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const datasource = async () => {
|
||||
const request = fetch("./olympics.arrow");
|
||||
const worker = await perspective.worker();
|
||||
const response = await request;
|
||||
const arrow = await response.arrayBuffer();
|
||||
const table = await worker.table(arrow);
|
||||
return table;
|
||||
};
|
||||
|
||||
window.workspace.tables.set("olympics", datasource());
|
||||
window.workspace.restore({
|
||||
sizes: [1],
|
||||
detail: {
|
||||
main: {
|
||||
type: "tab-area",
|
||||
widgets: ["PERSPECTIVE_GENERATED_ID_1", "PERSPECTIVE_GENERATED_ID_0"],
|
||||
currentIndex: 1,
|
||||
},
|
||||
},
|
||||
mode: "globalFilters",
|
||||
viewers: {
|
||||
PERSPECTIVE_GENERATED_ID_1: {
|
||||
plugin: "X/Y Scatter",
|
||||
title: "Avg Height vs Weight by Sport",
|
||||
group_by: ["Sport"],
|
||||
columns: ["Height", "Weight", null, "City", null, "Sport", null],
|
||||
filter: [["Height", "==", null]],
|
||||
sort: [["Name", "desc"]],
|
||||
expressions: {},
|
||||
aggregates: {
|
||||
Name: "distinct count",
|
||||
Weight: "avg",
|
||||
Sport: "dominant",
|
||||
Height: "avg",
|
||||
},
|
||||
table: "olympics",
|
||||
},
|
||||
PERSPECTIVE_GENERATED_ID_0: {
|
||||
plugin: "Heatmap",
|
||||
title: "Age Distribution by Sport",
|
||||
group_by: ["Age"],
|
||||
split_by: ["Sport"],
|
||||
columns: ["Name"],
|
||||
filter: [
|
||||
["Medal", "!=", "NA"],
|
||||
["Age", "is not null", null],
|
||||
],
|
||||
sort: [["Name", "col asc"]],
|
||||
expressions: {},
|
||||
aggregates: {
|
||||
Age: "avg",
|
||||
},
|
||||
table: "olympics",
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,2 @@
|
||||
license: apache-2.0
|
||||
height: 800
|
||||
@@ -0,0 +1,7 @@
|
||||
A ray-casting engine written as an
|
||||
[ExprTK](https://github.com/ArashPartow/exprtk) Expression within
|
||||
[Perspective](https://github.com/perspective-dev/perspective). As presented at
|
||||
[Open Source in Finance 2022 NY](https://www.youtube.com/watch?v=0ut-ynvBpGI).
|
||||
|
||||
This example has no server component, and all ray-casting is done entirely
|
||||
within the Perspective WebAssembly engine.
|
||||
@@ -0,0 +1,41 @@
|
||||
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
* ┃ 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). ┃
|
||||
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
*/
|
||||
|
||||
perspective-viewer {
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
perspective-viewer[theme="Pro Dark"] {
|
||||
--d3fc-charts--gradient--background: linear-gradient(
|
||||
#242526 50%,
|
||||
#071952,
|
||||
#088395,
|
||||
#35a29f,
|
||||
#f2f7a1
|
||||
);
|
||||
}
|
||||
|
||||
perspective-viewer[theme="Pro Light"] {
|
||||
--d3fc-charts--gradient--background: linear-gradient(
|
||||
#ffffff 50%,
|
||||
#071952,
|
||||
#088395,
|
||||
#35a29f,
|
||||
#f2f7a1
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<script type="module" src="index.js"></script>
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<perspective-viewer id="viewer"><span id="rendering_label" style="display: inline-block; margin: 24px">Rendering ...</span></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,145 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
const RESOLUTION = 100;
|
||||
|
||||
const EXPRESSION = `
|
||||
// Scene constants
|
||||
var resolution := ${RESOLUTION};
|
||||
var fov := 30 * (pi / 180);
|
||||
var camera[3] := {0, 0, -600};
|
||||
var light[3] := {0.5, 1, 0};
|
||||
|
||||
// Torus constants
|
||||
var radius := 1;
|
||||
var tube := 0.4;
|
||||
var radialSegments := 12;
|
||||
var tubularSegments := 16;
|
||||
var rotation := 40 * (pi / 180);
|
||||
|
||||
// Mesh
|
||||
var arc := pi * 2;
|
||||
var vs[663];
|
||||
for (var j := 0; j <= radialSegments; j += 1) {
|
||||
for (var i := 0; i <= tubularSegments; i += 1) {
|
||||
|
||||
// Vertex
|
||||
var u := (i / tubularSegments) * arc;
|
||||
var v := (j / radialSegments) * pi * 2;
|
||||
var i0 := j * 3 * (tubularSegments + 1) + (i * 3);
|
||||
vs[i0] := (radius + tube * cos(v)) * cos(u) * 100;
|
||||
vs[i0 + 1] := (radius + tube * cos(v)) * sin(u) * 100;
|
||||
vs[i0 + 2] := tube * sin(v) * 100;
|
||||
|
||||
// Rotate
|
||||
var b := vs[i0 + 1];
|
||||
var bcos := cos(rotation);
|
||||
var bsin := sin(rotation);
|
||||
vs[i0 + 1] := vs[i0 + 1] * bcos - vs[i0 + 2] * bsin;
|
||||
vs[i0 + 2] := b * bsin + vs[i0 + 2] * bcos;
|
||||
b := vs[i0];
|
||||
vs[i0] := vs[i0] * bcos - vs[i0 + 2] * bsin;
|
||||
vs[i0 + 2] := b * bsin + vs[i0 + 2] * bcos;
|
||||
}
|
||||
};
|
||||
|
||||
// Render scene
|
||||
var scale := resolution / (tan(fov / 2) * 400);
|
||||
var x := (floor(index() / resolution) - resolution / 2) / scale;
|
||||
var y := (index() % resolution - resolution / 2) / scale;
|
||||
var d[3] := {x, y, 200};
|
||||
var color := 0;
|
||||
var depth := inf;
|
||||
var light_norm := norm3(light);
|
||||
for (var j := 1; j <= radialSegments; j += 1) {
|
||||
for (var i := 1; i <= tubularSegments; i += 1) {
|
||||
|
||||
// Index
|
||||
var aa := (tubularSegments + 1) * j + i - 1;
|
||||
var b := (tubularSegments + 1) * (j - 1) + i - 1;
|
||||
var c := (tubularSegments + 1) * (j - 1) + i;
|
||||
var dd := (tubularSegments + 1) * j + i;
|
||||
var face[6] := {aa, b, dd, b, c, dd};
|
||||
for (var ii:= 0; ii < 2; ii += 1) {
|
||||
var i0 := face[ii * 3];
|
||||
var i1 := face[ii * 3 + 1];
|
||||
var i2 := face[ii * 3 + 2];
|
||||
var v0[3] := {vs[i0 * 3], vs[i0 * 3 + 1], vs[i0 * 3 + 2]};
|
||||
var v1[3] := {vs[i1 * 3], vs[i1 * 3 + 1], vs[i1 * 3 + 2]};
|
||||
var v2[3] := {vs[i2 * 3], vs[i2 * 3 + 1], vs[i2 * 3 + 2]};
|
||||
|
||||
// Render triangle
|
||||
var e1[3] := v1 - v0;
|
||||
var e2[3] := v2 - v0;
|
||||
var h[3];
|
||||
cross_product3(d, e2, h);
|
||||
var a := dot_product3(e1, h);
|
||||
if (a != 0) {
|
||||
var f := 1 / a;
|
||||
var s[3] := camera - v0;
|
||||
var u := f * dot_product3(s, h);
|
||||
if (u > 0 and u < 1) {
|
||||
var q[3];
|
||||
cross_product3(s, e1, q);
|
||||
var v := f * dot_product3(d, q);
|
||||
if (v > 0 and u + v < 1) {
|
||||
var t := f * dot_product3(e2, q);
|
||||
if (t >= 0) {
|
||||
var t2 := 1 - u - v;
|
||||
var d1[3] := v0 * t2 + v1 * u + v2 * v;
|
||||
var dist := norm3(d1 - camera);
|
||||
if (dist < depth) {
|
||||
depth := dist;
|
||||
|
||||
// Lighting
|
||||
var n[3];
|
||||
cross_product3(v0 - v1, v2 - v1, n);
|
||||
color := acos(dot_product3(light, n) / (light_norm * norm3(n)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
color;
|
||||
`.trim();
|
||||
|
||||
const LAYOUT = {
|
||||
title: "Raycasting",
|
||||
plugin: "Heatmap",
|
||||
group_by: [`x`],
|
||||
split_by: [`y`],
|
||||
columns: ["color"],
|
||||
table: "raycasting",
|
||||
expressions: {
|
||||
color: EXPRESSION,
|
||||
x: `floor(index() / ${RESOLUTION}) - ${RESOLUTION} / 2`,
|
||||
y: `index() % ${RESOLUTION} - ${RESOLUTION} / 2`,
|
||||
},
|
||||
settings: true,
|
||||
theme: "Pro Dark",
|
||||
};
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const index = new Array(Math.pow(RESOLUTION, 2)).fill(0);
|
||||
worker.table({ index }, { name: "raycasting" });
|
||||
window.viewer.load(worker);
|
||||
await window.viewer.restore(LAYOUT);
|
||||
document.querySelector("#rendering_label").remove();
|
||||
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1 @@
|
||||
license: apache-2.0
|
||||
@@ -0,0 +1,8 @@
|
||||
An example of streaming data into the perspective viewer, simulating a simple market
|
||||
blotter. The example creates random rows of tick data updating the table every 50ms
|
||||
and limiting the data set to 500 rows.
|
||||
|
||||
This example has no server component, and all market data is randomly created in the
|
||||
browser. It is a good example of Perspective's optimized real-time windowing
|
||||
performance using the `limit` parameter, as memory usage will stay consistent, as well
|
||||
as its interactivity during frequent re-renders.
|
||||
@@ -0,0 +1,20 @@
|
||||
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
* ┃ 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). ┃
|
||||
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
*/
|
||||
|
||||
perspective-viewer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script type="module" src="streaming.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 224 KiB |
@@ -0,0 +1,109 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
var SECURITIES = [
|
||||
"AAPL.N",
|
||||
"AMZN.N",
|
||||
"QQQ.N",
|
||||
"NVDA.N",
|
||||
"TSLA.N",
|
||||
"FB.N",
|
||||
"MSFT.N",
|
||||
"TLT.N",
|
||||
"XIV.N",
|
||||
"YY.N",
|
||||
"CSCO.N",
|
||||
"GOOGL.N",
|
||||
"PCLN.N",
|
||||
];
|
||||
|
||||
var CLIENTS = [
|
||||
"Homer",
|
||||
"Marge",
|
||||
"Bart",
|
||||
"Lisa",
|
||||
"Maggie",
|
||||
"Moe",
|
||||
"Lenny",
|
||||
"Carl",
|
||||
"Krusty",
|
||||
];
|
||||
|
||||
// Create 5 random rows of data.
|
||||
function newRows() {
|
||||
var rows = [];
|
||||
for (var x = 0; x < 50; x++) {
|
||||
rows.push({
|
||||
name: SECURITIES[Math.floor(Math.random() * SECURITIES.length)],
|
||||
client: CLIENTS[Math.floor(Math.random() * CLIENTS.length)],
|
||||
lastUpdate: new Date(),
|
||||
chg: Math.random() * 20 - 10,
|
||||
bid: Math.random() * 10 + 90,
|
||||
ask: Math.random() * 10 + 100,
|
||||
vol: Math.random() * 10 + 100,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Get element from the DOM.
|
||||
var elem = document.getElementsByTagName("perspective-viewer")[0];
|
||||
|
||||
// Create a new Perspective WebWorker instance.
|
||||
var worker = await perspective.worker();
|
||||
|
||||
// Create a new Perspective table in our `worker`, and limit it it 500 rows.
|
||||
const table = await worker.table(newRows(), {
|
||||
name: "streaming",
|
||||
limit: 500,
|
||||
});
|
||||
|
||||
// Load the `table` in the `<perspective-viewer>` DOM reference.
|
||||
await elem.load(worker);
|
||||
|
||||
elem.restore({
|
||||
plugin: "Datagrid",
|
||||
columns_config: {
|
||||
"(+)chg": { fg_gradient: 7.93, number_fg_mode: "bar" },
|
||||
"(-)chg": { fg_gradient: 8.07, number_fg_mode: "bar" },
|
||||
chg: { bg_gradient: 9.97, number_bg_mode: "gradient" },
|
||||
},
|
||||
plugin_config: {
|
||||
editable: false,
|
||||
scroll_lock: true,
|
||||
},
|
||||
settings: true,
|
||||
table: "streaming",
|
||||
theme: "Pro Light",
|
||||
group_by: ["name"],
|
||||
split_by: ["client"],
|
||||
columns: ["(-)chg", "chg", "(+)chg"],
|
||||
filter: [],
|
||||
sort: [["chg", "desc"]],
|
||||
expressions: {
|
||||
"(-)chg": 'if("chg"<0){"chg"}else{0}',
|
||||
"(+)chg": 'if("chg">0){"chg"}else{0}',
|
||||
},
|
||||
aggregates: { "(-)chg": "avg", chg: "avg", "(+)chg": "avg" },
|
||||
});
|
||||
|
||||
// Add more rows every 50ms using the `update()` method on the `table` directly.
|
||||
(function postRow() {
|
||||
table.update(newRows());
|
||||
setTimeout(postRow, 10);
|
||||
})();
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1 @@
|
||||
license: apache-2.0
|
||||
@@ -0,0 +1,9 @@
|
||||
Demo of [Perspective](https://github.com/perspective-dev/perspective).
|
||||
|
||||
This example of a `<perspective-workspace>` dashboard is generated from one
|
||||
[Apache Arrow](https://arrow.apache.org/) source dataset, converted originally
|
||||
from this
|
||||
[XLS from a Tableau presentation](https://community.tableau.com/docs/DOC-1236).
|
||||
This layout is just an example; you can explore your own visualizations in the
|
||||
Perspective configuration menu, accessed by clicking the dropdown arrow in each
|
||||
pane's upper left, or advanced options in the pane's right-click context menu.
|
||||
@@ -0,0 +1,50 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/workspace/dist/css/pro.css" />
|
||||
<style>
|
||||
perspective-workspace {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<perspective-workspace id="workspace"></perspective-workspace>
|
||||
<script type="module">
|
||||
import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "/node_modules/@perspective-dev/workspace/dist/cdn/perspective-workspace.js";
|
||||
import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "/node_modules/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
|
||||
import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const DATA_URL = "/node_modules/superstore-arrow/superstore.lz4.arrow";
|
||||
|
||||
async function get_layout() {
|
||||
const req = await fetch("layout.json");
|
||||
const json = await req.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
const request = fetch(DATA_URL);
|
||||
const worker = await perspective.worker();
|
||||
const response = await request;
|
||||
const buffer = await response.arrayBuffer();
|
||||
await worker.table(buffer, { name: "superstore" });
|
||||
window.workspace.load(worker);
|
||||
const layout = await get_layout();
|
||||
await window.workspace.restore(layout);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,169 @@
|
||||
index.html: 48 {
|
||||
"sizes": [
|
||||
1
|
||||
],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "split-area",
|
||||
"orientation": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": [
|
||||
"PERSPECTIVE_GENERATED_ID_0",
|
||||
"PERSPECTIVE_GENERATED_ID_1"
|
||||
],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": [
|
||||
"PERSPECTIVE_GENERATED_ID_3"
|
||||
],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": [
|
||||
"PERSPECTIVE_GENERATED_ID_2"
|
||||
],
|
||||
"currentIndex": 0
|
||||
}
|
||||
],
|
||||
"sizes": [
|
||||
0.3765432098765432,
|
||||
0.24691358024691357,
|
||||
0.3765432098765432
|
||||
]
|
||||
}
|
||||
},
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_0": {
|
||||
"version": "3.5.0",
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {},
|
||||
"edit_mode": "READ_ONLY",
|
||||
"scroll_lock": false
|
||||
},
|
||||
"columns_config": {},
|
||||
"title": "Sales Report 2",
|
||||
"group_by": [
|
||||
"Region",
|
||||
"State"
|
||||
],
|
||||
"split_by": [
|
||||
"Category",
|
||||
"Sub-Category"
|
||||
],
|
||||
"columns": [
|
||||
"Sales",
|
||||
"Profit"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {},
|
||||
"aggregates": {},
|
||||
"table": "superstore",
|
||||
"settings": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_1": {
|
||||
"version": "3.5.0",
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {},
|
||||
"edit_mode": "READ_ONLY",
|
||||
"scroll_lock": false
|
||||
},
|
||||
"columns_config": {},
|
||||
"title": null,
|
||||
"group_by": [],
|
||||
"split_by": [],
|
||||
"columns": [
|
||||
"Row ID",
|
||||
"Order ID",
|
||||
"Ship Mode",
|
||||
"Customer ID",
|
||||
"Customer Name",
|
||||
"Segment",
|
||||
"Country",
|
||||
"City",
|
||||
"State",
|
||||
"Postal Code",
|
||||
"Region",
|
||||
"Product ID",
|
||||
"Category",
|
||||
"Sub-Category",
|
||||
"Product Name",
|
||||
"Sales",
|
||||
"Quantity",
|
||||
"Discount",
|
||||
"Profit",
|
||||
"Order Date",
|
||||
"Ship Date"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {},
|
||||
"aggregates": {},
|
||||
"table": "superstore",
|
||||
"settings": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_3": {
|
||||
"version": "3.5.0",
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"columns": {},
|
||||
"edit_mode": "READ_ONLY",
|
||||
"scroll_lock": false
|
||||
},
|
||||
"columns_config": {},
|
||||
"title": "Sales Report 2 (*)",
|
||||
"group_by": [
|
||||
"Region",
|
||||
"State"
|
||||
],
|
||||
"split_by": [
|
||||
"Category",
|
||||
"Sub-Category"
|
||||
],
|
||||
"columns": [
|
||||
"Sales",
|
||||
"Profit"
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {},
|
||||
"aggregates": {},
|
||||
"table": "superstore",
|
||||
"settings": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_2": {
|
||||
"version": "3.5.0",
|
||||
"plugin": "Treemap",
|
||||
"plugin_config": {},
|
||||
"columns_config": {},
|
||||
"title": "Sales Report (by State)",
|
||||
"group_by": [
|
||||
"State"
|
||||
],
|
||||
"split_by": [],
|
||||
"columns": [
|
||||
"Sales",
|
||||
"Profit",
|
||||
null
|
||||
],
|
||||
"filter": [],
|
||||
"sort": [
|
||||
[
|
||||
"Profit",
|
||||
"desc"
|
||||
]
|
||||
],
|
||||
"expressions": {},
|
||||
"aggregates": {},
|
||||
"table": "superstore",
|
||||
"settings": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"sizes": [1],
|
||||
"detail": {
|
||||
"main": {
|
||||
"type": "split-area",
|
||||
"orientation": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": [
|
||||
"PERSPECTIVE_GENERATED_ID_0",
|
||||
"PERSPECTIVE_GENERATED_ID_1"
|
||||
],
|
||||
"currentIndex": 0
|
||||
},
|
||||
{
|
||||
"type": "tab-area",
|
||||
"widgets": ["PERSPECTIVE_GENERATED_ID_2"],
|
||||
"currentIndex": 0
|
||||
}
|
||||
],
|
||||
"sizes": [0.5, 0.5]
|
||||
}
|
||||
},
|
||||
"mode": "globalFilters",
|
||||
"viewers": {
|
||||
"PERSPECTIVE_GENERATED_ID_0": {
|
||||
"plugin": "datagrid",
|
||||
"group_by": ["Region", "State"],
|
||||
"split_by": ["Category", "Sub-Category"],
|
||||
"columns": ["Sales", "Profit"],
|
||||
"master": false,
|
||||
"name": "Sales Report",
|
||||
"table": "superstore",
|
||||
"linked": false,
|
||||
"title": "Sales Report 2"
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_1": {
|
||||
"plugin": "datagrid",
|
||||
"columns": [
|
||||
"Row ID",
|
||||
"Order ID",
|
||||
"Ship Mode",
|
||||
"Customer ID",
|
||||
"Customer Name",
|
||||
"Segment",
|
||||
"Country",
|
||||
"City",
|
||||
"State",
|
||||
"Postal Code",
|
||||
"Region",
|
||||
"Product ID",
|
||||
"Category",
|
||||
"Sub-Category",
|
||||
"Product Name",
|
||||
"Sales",
|
||||
"Quantity",
|
||||
"Discount",
|
||||
"Profit",
|
||||
"Order Date",
|
||||
"Ship Date"
|
||||
],
|
||||
"master": false,
|
||||
"name": "superstore.csv",
|
||||
"table": "superstore",
|
||||
"linked": false
|
||||
},
|
||||
"PERSPECTIVE_GENERATED_ID_2": {
|
||||
"plugin": "Treemap",
|
||||
"group_by": ["State"],
|
||||
"columns": ["Sales", "Profit", null],
|
||||
"sort": [["Profit", "desc"]],
|
||||
"plugin_config": {
|
||||
"realValues": ["Sales", "Profit", null],
|
||||
"treemaps": {
|
||||
"": {
|
||||
"treemapLevel": 0,
|
||||
"treemapRoute": [""]
|
||||
}
|
||||
}
|
||||
},
|
||||
"master": false,
|
||||
"title": "Sales Report (by State)",
|
||||
"table": "superstore",
|
||||
"linked": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,2 @@
|
||||
|
||||
A Perspective example which uses your computer's webcam as a data source.
|
||||
@@ -0,0 +1,93 @@
|
||||
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
* ┃ 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). ┃
|
||||
* ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
*/
|
||||
|
||||
body {
|
||||
background: #242526;
|
||||
color: white;
|
||||
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
|
||||
"Consolas", "Liberation Mono", monospace;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#header a {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
perspective-viewer {
|
||||
border-top: 1px solid #666;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
label {
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
padding: 6px 0px;
|
||||
margin-right: 4px;
|
||||
margin-left: 14px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
select,
|
||||
button {
|
||||
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
|
||||
"Consolas", "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid #666;
|
||||
border-radius: 2px;
|
||||
padding: 6px 10px;
|
||||
color: #f4f5f6;
|
||||
cursor: pointer;
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
height: 32px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
select:hover,
|
||||
button:hover {
|
||||
color: #242526;
|
||||
background-color: #f4f5f6;
|
||||
border-color: #f4f5f6;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/node_modules/@perspective-dev/server/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link rel="stylesheet" crossorigin="anonymous" href="/node_modules/@perspective-dev/viewer/dist/css/themes.css" />
|
||||
<script type="module" src="webcam.js"></script>
|
||||
<style>
|
||||
perspective-workspace {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
video,
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="video" width="80" height="60" autoplay muted></video>
|
||||
<canvas id="canvas" width="80" height="60"></canvas>
|
||||
<div id="app">
|
||||
<div id="header">
|
||||
<a href="https://perspective-dev.github.io">
|
||||
<img height="12" src="https://raw.githubusercontent.com/perspective-dev/perspective/master/docs/static/svg/perspective-logo-dark.svg" />
|
||||
</a>
|
||||
<label>Webcam Demo</label>
|
||||
<select></select>
|
||||
</div>
|
||||
<perspective-viewer></perspective-viewer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"plugin": "Datagrid",
|
||||
"columns_config": {
|
||||
"color": {
|
||||
"bg_gradient": 251.04,
|
||||
"neg_bg_color": "#ffa38f",
|
||||
"number_bg_mode": "gradient",
|
||||
"number_fg_mode": "disabled",
|
||||
"pos_bg_color": "#346ead"
|
||||
}
|
||||
},
|
||||
"plugin_config": {
|
||||
"editable": false,
|
||||
"scroll_lock": false
|
||||
},
|
||||
"title": "Spreadsheet Cam",
|
||||
"group_by": ["y"],
|
||||
"split_by": ["x"],
|
||||
"columns": ["color"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"New Column 1": "bucket(\"color\", 5)",
|
||||
"y": "floor(\"index\" / 80)",
|
||||
"x": "-\"index\" % 80"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Heatmap",
|
||||
"title": "Heatmap Cam",
|
||||
"group_by": ["x"],
|
||||
"split_by": ["y"],
|
||||
"columns": ["color"],
|
||||
"expressions": {
|
||||
"y": "-floor(\"index\" / 80)",
|
||||
"x": "-\"index\" % 80"
|
||||
},
|
||||
"aggregates": {
|
||||
"New Column 1": "any"
|
||||
}
|
||||
},
|
||||
{
|
||||
"plugin": "Heatmap",
|
||||
"plugin_config": {},
|
||||
"settings": true,
|
||||
"theme": "Pro Light",
|
||||
"title": "Downsampled Heatmap Cam",
|
||||
"group_by": ["x"],
|
||||
"split_by": ["y"],
|
||||
"columns": ["color"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"y": "bucket(-floor(\"index\" / 80), 3)",
|
||||
"x": "bucket(-\"index\" % 80, 3)"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Y Bar",
|
||||
"plugin_config": {},
|
||||
"title": "Luminosity Histogram",
|
||||
"group_by": ["bucket(\"color\", 5)"],
|
||||
"split_by": [],
|
||||
"columns": ["color"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(\"color\", 5)": "bucket(\"color\", 5)",
|
||||
"y": "-floor(\"index\" / 80)",
|
||||
"x": "-\"index\" % 80"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Datagrid",
|
||||
"columns_config": {
|
||||
"color": {
|
||||
"bg_gradient": 2463.68,
|
||||
"neg_bg_color": "#ffa38f",
|
||||
"number_bg_mode": "gradient",
|
||||
"number_fg_mode": "disabled",
|
||||
"pos_bg_color": "#307bb0"
|
||||
}
|
||||
},
|
||||
"plugin_config": {
|
||||
"editable": false,
|
||||
"scroll_lock": false
|
||||
},
|
||||
"title": "Small Spreadsheet Cam",
|
||||
"group_by": ["bucket(y, 5)"],
|
||||
"split_by": ["bucket(x, 5)"],
|
||||
"columns": ["color"],
|
||||
"filter": [["bucket(x, 5)", "<", 0.0]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"bucket(y, 5)": "bucket(floor(\"index\" / 80), 2)",
|
||||
"New Column 1": "bucket(\"color\", 5)",
|
||||
"bucket(x, 5)": "bucket(-\"index\" % 80, 5)"
|
||||
},
|
||||
"aggregates": {}
|
||||
},
|
||||
{
|
||||
"plugin": "Y Line",
|
||||
"plugin_config": {},
|
||||
"title": "Max Headroom",
|
||||
"group_by": ["x"],
|
||||
"split_by": ["y"],
|
||||
"columns": ["New Column 2"],
|
||||
"filter": [["x", "<", 0.0]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"x": "-\"index\" % 80",
|
||||
"y": "floor(\"index\" / 80)",
|
||||
"New Column 2": "-floor(\"index\" / 80) * 20 - \"color\""
|
||||
},
|
||||
"aggregates": { "New Column 2": "avg" }
|
||||
},
|
||||
{
|
||||
"plugin": "X/Y Scatter",
|
||||
"plugin_config": {},
|
||||
"title": "Scatter Cam",
|
||||
"group_by": ["x", "y"],
|
||||
"split_by": [],
|
||||
"columns": ["x", "New Column 2", "color", null, null, null, null],
|
||||
"filter": [["x", "<", 0.0]],
|
||||
"sort": [],
|
||||
"expressions": {
|
||||
"New Column 2": "-floor(\"index\" / 80) * 50 - \"color\"",
|
||||
"x": "-\"index\" % 80",
|
||||
"y": "floor(\"index\" / 80)"
|
||||
},
|
||||
"aggregates": { "x": "avg", "New Column 2": "avg" }
|
||||
},
|
||||
{
|
||||
"plugin": "Datagrid",
|
||||
"plugin_config": {
|
||||
"editable": false,
|
||||
"scroll_lock": false
|
||||
},
|
||||
"title": "Raw Stream",
|
||||
"group_by": [],
|
||||
"split_by": [],
|
||||
"columns": ["index", "color"],
|
||||
"filter": [],
|
||||
"sort": [],
|
||||
"expressions": {},
|
||||
"aggregates": {}
|
||||
}
|
||||
]
|
||||