chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
[book]
|
||||
authors = ["Andrew Stein"]
|
||||
language = "en"
|
||||
src = "md"
|
||||
title = "Perspective"
|
||||
|
||||
[build]
|
||||
build-dir = "static/guide"
|
||||
|
||||
[output.html]
|
||||
# theme = "my-theme"
|
||||
# default-theme = "light"
|
||||
# preferred-dark-theme = "navy"
|
||||
# smart-punctuation = true
|
||||
# mathjax-support = false
|
||||
git-repository-url = "https://github.com/perspective-dev/perspective"
|
||||
site-url = "https://perspective-dev.github.io/guide/"
|
||||
additional-css = [
|
||||
"md/perspective.css",
|
||||
"node_modules/@perspective-dev/viewer/dist/css/themes.css",
|
||||
]
|
||||
# additional-js = []
|
||||
# no-section-label = false
|
||||
# edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
# site-url = "/guide/"
|
||||
# cname = "myproject.rs"
|
||||
# input-404 = "not-found.md"
|
||||
@@ -0,0 +1,176 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 esbuild from "esbuild";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { createRequire } from "module";
|
||||
import { bundleAsync as bundleCssAsync, composeVisitors } from "lightningcss";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DIST = path.join(__dirname, "dist");
|
||||
|
||||
function copyRecursive(src, dest) {
|
||||
if (!fs.existsSync(src)) return;
|
||||
const stat = fs.statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const child of fs.readdirSync(src)) {
|
||||
copyRecursive(path.join(src, child), path.join(dest, child));
|
||||
}
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
// Inline url() asset references as data URIs.
|
||||
export function inlineUrlVisitor(fromFile) {
|
||||
const dir = path.dirname(fromFile);
|
||||
return composeVisitors([
|
||||
{
|
||||
Url(url) {
|
||||
const ext = path.extname(url.url).toLowerCase();
|
||||
if (![".svg", ".png", ".gif"].includes(ext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = path.resolve(dir, url.url);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
throw new Error(`File not found ${url.url}`);
|
||||
// return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(resolved);
|
||||
const mime =
|
||||
ext === ".svg"
|
||||
? "image/svg+xml"
|
||||
: ext === ".png"
|
||||
? "image/png"
|
||||
: "image/gif";
|
||||
|
||||
const new_content = content
|
||||
.toString("base64")
|
||||
.split("\n")
|
||||
.map((x) => x.trim())
|
||||
.join("");
|
||||
|
||||
return {
|
||||
url: `data:${mime};base64,${new_content}`,
|
||||
loc: url.loc,
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export const resolveNPM = (url) => ({
|
||||
read(filePath) {
|
||||
if (filePath.startsWith("http")) {
|
||||
return `@import url("${filePath}");`;
|
||||
}
|
||||
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
},
|
||||
resolve(specifier, from) {
|
||||
if (specifier.startsWith("http")) {
|
||||
return { external: specifier };
|
||||
}
|
||||
|
||||
const _require = createRequire(url);
|
||||
|
||||
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
||||
return path.resolve(path.dirname(from), specifier);
|
||||
}
|
||||
|
||||
return _require.resolve(specifier);
|
||||
},
|
||||
});
|
||||
|
||||
async function build() {
|
||||
// Clean and create dist
|
||||
fs.mkdirSync(DIST, { recursive: true });
|
||||
|
||||
// Bundle CSS
|
||||
const { code: cssCode } = await bundleCssAsync({
|
||||
filename: path.join(__dirname, "./src/css/style.css"),
|
||||
minify: true,
|
||||
resolver: resolveNPM(import.meta.url),
|
||||
visitor: inlineUrlVisitor("./src/css/style.css"),
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(DIST, "css"), { recursive: true });
|
||||
fs.writeFileSync(path.join(DIST, "style.css"), cssCode);
|
||||
|
||||
// Bundle JS entry points
|
||||
await esbuild.build({
|
||||
entryPoints: [
|
||||
path.join(__dirname, "src/index.ts"),
|
||||
path.join(__dirname, "src/examples.ts"),
|
||||
path.join(__dirname, "src/block.ts"),
|
||||
],
|
||||
bundle: true,
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
outdir: DIST,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
target: ["es2022"],
|
||||
define: {
|
||||
global: "window",
|
||||
},
|
||||
loader: {
|
||||
".wasm": "file",
|
||||
".arrow": "file",
|
||||
},
|
||||
});
|
||||
|
||||
// Copy HTML files
|
||||
for (const html of ["index.html", "examples.html", "block.html"]) {
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, "src", html),
|
||||
path.join(DIST, html),
|
||||
);
|
||||
}
|
||||
|
||||
// Copy static assets
|
||||
copyRecursive(path.join(__dirname, "static"), DIST);
|
||||
|
||||
// Generate blocks manifest
|
||||
const blocksDir = path.join(DIST, "blocks");
|
||||
if (fs.existsSync(blocksDir)) {
|
||||
const manifest = {};
|
||||
for (const example of fs.readdirSync(blocksDir)) {
|
||||
const exDir = path.join(blocksDir, example);
|
||||
if (!fs.statSync(exDir).isDirectory()) continue;
|
||||
manifest[example] = fs
|
||||
.readdirSync(exDir)
|
||||
.filter(
|
||||
(f) =>
|
||||
!f.startsWith(".") &&
|
||||
!f.endsWith(".png") &&
|
||||
!f.endsWith(".arrow"),
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(blocksDir, "manifest.json"),
|
||||
JSON.stringify(manifest),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Build complete: dist/");
|
||||
}
|
||||
|
||||
build().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 puppeteer from "puppeteer";
|
||||
import * as fs from "node:fs";
|
||||
import * as cp from "node:child_process";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import EXAMPLES from "./src/data/features.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// features.js uses CJS exports.default, import it dynamically
|
||||
// const EXAMPLES = (await import("./src/data/features.ts")).default;
|
||||
|
||||
const perspective = import(
|
||||
"@perspective-dev/client/dist/esm/perspective.node.js"
|
||||
);
|
||||
|
||||
const DEFAULT_VIEWPORT = {
|
||||
width: 400,
|
||||
height: 300,
|
||||
};
|
||||
|
||||
async function run_with_theme(page, is_dark = false, order) {
|
||||
await page.goto("http://localhost:8080/");
|
||||
await page.setContent(template(is_dark));
|
||||
await page.setViewport(DEFAULT_VIEWPORT);
|
||||
await page.evaluate(async () => {
|
||||
while (!window.__TEST_PERSPECTIVE_READY__) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
});
|
||||
|
||||
await page.evaluate(async function () {
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
await viewer.flush();
|
||||
await viewer.toggleConfig();
|
||||
});
|
||||
|
||||
for (const idx in EXAMPLES) {
|
||||
const { config, viewport } = EXAMPLES[idx];
|
||||
await page.setViewport(viewport || DEFAULT_VIEWPORT);
|
||||
const new_config = Object.assign(
|
||||
{
|
||||
plugin: "Datagrid",
|
||||
group_by: [],
|
||||
expressions: {},
|
||||
split_by: [],
|
||||
sort: [],
|
||||
aggregates: {},
|
||||
},
|
||||
config,
|
||||
);
|
||||
console.log(JSON.stringify(new_config));
|
||||
|
||||
await page.evaluate(async (config) => {
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
await viewer.reset();
|
||||
await viewer.restore(config);
|
||||
}, new_config);
|
||||
|
||||
const screenshot = await page.screenshot({
|
||||
captureBeyondViewport: false,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
const name = `static/features/feature_${idx}${
|
||||
is_dark ? "_dark" : ""
|
||||
}.png`;
|
||||
|
||||
fs.writeFileSync(name, screenshot);
|
||||
cp.execSync(`convert ${name} -resize 200x150 ${name}`);
|
||||
}
|
||||
|
||||
const suffix = is_dark ? "_dark" : "";
|
||||
const montage_files = order.map(
|
||||
(idx) => `static/features/feature_${idx}${suffix}.png`,
|
||||
);
|
||||
|
||||
cp.execSync(
|
||||
`montage -mode concatenate -background none -tile 5x ${montage_files.join(
|
||||
" ",
|
||||
)} static/features/montage${is_dark ? "_dark" : "_light"}.png`,
|
||||
);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (
|
||||
!fs.existsSync("static/features") ||
|
||||
fs.readdirSync("static/features").length === 0
|
||||
) {
|
||||
console.log("Generating feature screenshots!");
|
||||
fs.mkdirSync(path.join(__dirname, "static/features"), {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const x = await perspective;
|
||||
const server = new x.WebSocketServer({
|
||||
assets: [
|
||||
path.join(__dirname, "."),
|
||||
path.join(__dirname, "../node_modules"),
|
||||
],
|
||||
});
|
||||
|
||||
const indices = Array.from({ length: EXAMPLES.length }, (_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[indices[i], indices[j]] = [indices[j], indices[i]];
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
await run_with_theme(page, false, indices);
|
||||
await run_with_theme(page, true, indices);
|
||||
await page.close();
|
||||
await browser.close();
|
||||
await server.close();
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, "static/features/montage_map.json"),
|
||||
JSON.stringify({
|
||||
tile_width: 200,
|
||||
tile_height: 150,
|
||||
columns: 5,
|
||||
order: indices,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!fs.existsSync("static/blocks")) {
|
||||
fs.mkdirSync("static/blocks");
|
||||
}
|
||||
|
||||
const { dist_examples } = await import("../examples/blocks/index.mjs");
|
||||
await dist_examples(`${__dirname}/static/blocks`);
|
||||
}
|
||||
|
||||
function template(is_dark) {
|
||||
return fs
|
||||
.readFileSync(path.join(__dirname, "template.html"))
|
||||
.toString()
|
||||
.replace("/css/pro.css", is_dark ? "/css/pro-dark.css" : "/css/pro.css")
|
||||
.trim();
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
mdbook:
|
||||
container_name: mdbook
|
||||
image: peaceiris/mdbook:v0.5.0
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 3001:3001
|
||||
volumes:
|
||||
- ${PWD}/..:/repo
|
||||
working_dir: /repo/docs
|
||||
command:
|
||||
- serve
|
||||
- --hostname
|
||||
- "0.0.0.0"
|
||||
@@ -0,0 +1,68 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, "..");
|
||||
const DIST = path.join(__dirname, "dist");
|
||||
const STAGING = path.join(REPO_ROOT, "dist-gh-pages");
|
||||
const BRANCH = "gh-pages";
|
||||
|
||||
function git(args, opts = {}) {
|
||||
return execFileSync("git", args, {
|
||||
stdio: "inherit",
|
||||
cwd: REPO_ROOT,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
function copyRecursive(src, dest) {
|
||||
const stat = fs.statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const child of fs.readdirSync(src)) {
|
||||
copyRecursive(path.join(src, child), path.join(dest, child));
|
||||
}
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(DIST)) {
|
||||
console.error(`Missing ${DIST} — run \`npm run build\` first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(STAGING)) {
|
||||
git(["worktree", "add", STAGING, BRANCH]);
|
||||
} else {
|
||||
git(["fetch", "origin", BRANCH]);
|
||||
git(["checkout", `origin/${BRANCH}`], { cwd: STAGING });
|
||||
}
|
||||
|
||||
// Clear tracked + untracked content in the staging worktree, preserving
|
||||
// the worktree's `.git` link.
|
||||
git(["rm", "-rf", "--quiet", "--ignore-unmatch", "."], { cwd: STAGING });
|
||||
git(["clean", "-fdx"], { cwd: STAGING });
|
||||
|
||||
for (const entry of fs.readdirSync(DIST)) {
|
||||
copyRecursive(path.join(DIST, entry), path.join(STAGING, entry));
|
||||
}
|
||||
|
||||
git(["add", "-A"], { cwd: STAGING });
|
||||
|
||||
console.log(`Staged dist/ onto ${BRANCH} at ${STAGING}`);
|
||||
console.log(`Review with \`git -C ${STAGING} status\`, then commit and push.`);
|
||||
@@ -0,0 +1 @@
|
||||
<!-- Empty page to allow HTML injection from `build.js` screenshot generator-->
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
# FAQ
|
||||
|
||||
## Installation
|
||||
|
||||
### Python installation fails on Windows
|
||||
|
||||
Python wheels are published for supported Python versions and platforms. On
|
||||
Windows, ensure you have a compatible Python version and architecture. Install
|
||||
with:
|
||||
|
||||
```bash
|
||||
pip install perspective-python
|
||||
```
|
||||
|
||||
If you encounter C++ binding errors or link errors, make sure you are using a
|
||||
supported Python version and that your `pip` is up to date. Pre-built wheels
|
||||
eliminate the need for a C++ compiler in most cases.
|
||||
|
||||
<!-- _Related: [#928](https://github.com/perspective-dev/perspective/issues/928),
|
||||
[#1325](https://github.com/perspective-dev/perspective/issues/1325),
|
||||
[#1025](https://github.com/perspective-dev/perspective/issues/1025)_ -->
|
||||
|
||||
### Python `import perspective` fails with `ImportError` or undefined symbol
|
||||
|
||||
This typically happens when the C++ shared library (`libpsp.so`) cannot be found
|
||||
or was built against a different Python version. Ensure your Python version
|
||||
matches the installed wheel. On Linux, verify that required system libraries are
|
||||
present. If you see errors about `libpsp.so` or undefined symbols, try
|
||||
reinstalling in a clean virtual environment.
|
||||
|
||||
<!-- _Related: [#937](https://github.com/perspective-dev/perspective/issues/937),
|
||||
[#1120](https://github.com/perspective-dev/perspective/issues/1120),
|
||||
[#1216](https://github.com/perspective-dev/perspective/issues/1216),
|
||||
[#1332](https://github.com/perspective-dev/perspective/issues/1332)_ -->
|
||||
|
||||
### Python installation fails on macOS
|
||||
|
||||
On Apple Silicon (M1/M2/M3), make sure you are using a native ARM Python build,
|
||||
not one running under Rosetta. The published wheels include `aarch64` variants
|
||||
for supported platforms.
|
||||
|
||||
<!-- _Related: [#938](https://github.com/perspective-dev/perspective/issues/938),
|
||||
[#1170](https://github.com/perspective-dev/perspective/issues/1170)_ -->
|
||||
|
||||
### How do I install Perspective in a Docker container?
|
||||
|
||||
Perspective's Python wheels are built against `manylinux_2_28` containers (see
|
||||
[`.github/workflows/build.yaml`](../../.github/workflows/build.yaml)), so they
|
||||
are compatible with most Linux distributions based on glibc 2.28+ (e.g., Debian
|
||||
10+, Ubuntu 20.04+, RHEL 8+). Use a compatible base image:
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
RUN pip install perspective-python
|
||||
```
|
||||
|
||||
Alpine Linux uses musl instead of glibc and is **not** compatible with the
|
||||
published wheels.
|
||||
|
||||
<!-- _Related: [#1201](https://github.com/perspective-dev/perspective/issues/1201)_ -->
|
||||
|
||||
## JavaScript Bundling
|
||||
|
||||
### How do I use Perspective with Vite, Webpack, or esbuild?
|
||||
|
||||
Perspective no longer exports bundler plugins. Instead, you must manually
|
||||
bootstrap the WASM binaries using your bundler's asset handling. See
|
||||
[Importing with or without a bundler](./how_to/javascript/importing.md) for
|
||||
complete examples for Vite, Webpack, esbuild, CDN, and inline builds.
|
||||
|
||||
<!-- _Related: [#1734](https://github.com/perspective-dev/perspective/issues/1734),
|
||||
[#2725](https://github.com/perspective-dev/perspective/issues/2725),
|
||||
[#857](https://github.com/perspective-dev/perspective/issues/857),
|
||||
[#1497](https://github.com/perspective-dev/perspective/issues/1497),
|
||||
[#1655](https://github.com/perspective-dev/perspective/issues/1655)_ -->
|
||||
|
||||
## Framework Integration
|
||||
|
||||
### How do I use Perspective with React?
|
||||
|
||||
Perspective provides a dedicated
|
||||
[React component](./how_to/javascript/react.md). You must also still initialize
|
||||
Perspective's WebAssembly as per your bundler — see
|
||||
[Importing with or without a bundler](./how_to/javascript/importing.md).
|
||||
|
||||
<!-- _Related: [#865](https://github.com/perspective-dev/perspective/issues/865),
|
||||
[#931](https://github.com/perspective-dev/perspective/issues/931),
|
||||
[#3023](https://github.com/perspective-dev/perspective/discussions/3023)_ -->
|
||||
|
||||
### How do I use Perspective with Next.js?
|
||||
|
||||
Perspective relies on Web Workers and WASM, which require client-side rendering.
|
||||
Use dynamic imports with `ssr: false` in Next.js to load Perspective components
|
||||
only on the client.
|
||||
|
||||
<!-- _Related:
|
||||
[#2947](https://github.com/perspective-dev/perspective/discussions/2947),
|
||||
[#2181](https://github.com/perspective-dev/perspective/discussions/2181)_ -->
|
||||
|
||||
### How do I use Perspective with Vue.js/Angular/etc?
|
||||
|
||||
As a standard Web Component, `<perspective-viewer>` works in most JavaScript web
|
||||
frameworks directly via standard HTML/DOM APIs, but does not have dedicated
|
||||
integration libraries for these frameworks.
|
||||
|
||||
<!-- _Related:
|
||||
[#2787](https://github.com/perspective-dev/perspective/discussions/2787)_ -->
|
||||
|
||||
## Expressions
|
||||
|
||||
### How do I create computed/expression columns?
|
||||
|
||||
Use the [`expressions`](./explanation/view/config/expressions.md) config option
|
||||
in your `View` to define new columns with ExprTK syntax, which must then be
|
||||
_used_ somewhere else in your config (like `columns`) to actually be visible &
|
||||
calculated. In `<perspective-viewer>`, expression columns can be created from
|
||||
the UI column sidebar by clicking the "New Column" button.
|
||||
|
||||
<!-- _Related: [#1981](https://github.com/perspective-dev/perspective/issues/1981),
|
||||
[#2148](https://github.com/perspective-dev/perspective/issues/2148),
|
||||
[#1493](https://github.com/perspective-dev/perspective/issues/1493)_ -->
|
||||
|
||||
### Can I reference one expression column from another?
|
||||
|
||||
No, you must duplicate calculations that are shared between expression columns.
|
||||
|
||||
<!-- _Related: [#2148](https://github.com/perspective-dev/perspective/issues/2148)_ -->
|
||||
|
||||
### Can I do date arithmetic in expressions?
|
||||
|
||||
Yes, but they must be converted to `float` values first (`integer` is an `i32`
|
||||
which is too small). See
|
||||
[Expressions](./explanation/view/config/expressions.md).
|
||||
|
||||
<!-- _Related: [#3026](https://github.com/perspective-dev/perspective/issues/3026),
|
||||
[#1768](https://github.com/perspective-dev/perspective/issues/1768)_ -->
|
||||
|
||||
### Can I do rolling sums or cumulative calculations?
|
||||
|
||||
Not in Perspective's built-in engine, but as an alternative, DuckDB supports
|
||||
[rolling and cumulative sums via `WINDOW` functions](https://duckdb.org/docs/stable/sql/functions/window_functions),
|
||||
and DuckDB now has
|
||||
[native Perspective Virtual Server support](./explanation/virtual_servers.md)
|
||||
which allows arbitrary DuckDB queries (as a `TABLE` or `VIEW`) to be
|
||||
`<perspective-viewer>` `Table`s.
|
||||
|
||||
<!-- _Related:
|
||||
[#2600](https://github.com/perspective-dev/perspective/discussions/2600),
|
||||
[#2624](https://github.com/perspective-dev/perspective/issues/2624)_ -->
|
||||
|
||||
## Filters
|
||||
|
||||
### Can I compose filters with OR logic?
|
||||
|
||||
Perspective
|
||||
[filters](./explanation/view/config/selection_and_ordering.md#filter) are
|
||||
composed with AND logic by default. As an alternative, you can use
|
||||
[expression columns](./explanation/view/config/expressions.md) to create a
|
||||
boolean column that encodes your OR logic (or any arbitrary multi-column
|
||||
predicate), then filter on that column:
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
expressions: {
|
||||
or_filter:
|
||||
"if (\"State\" == 'Texas') true; else if (\"State\" == 'California') true; else false",
|
||||
},
|
||||
filter: [["or_filter", "==", true]],
|
||||
});
|
||||
```
|
||||
|
||||
<!-- _Related: [#1192](https://github.com/perspective-dev/perspective/issues/1192)_ -->
|
||||
|
||||
### How do I update filters programmatically?
|
||||
|
||||
Set the [`filter`](./explanation/view/config/selection_and_ordering.md#filter)
|
||||
property on a `View` config, or use the `<perspective-viewer>`
|
||||
[`.restore()`](./how_to/javascript/save_restore.md) method to update filters at
|
||||
runtime.
|
||||
|
||||
<!-- _Related: [#935](https://github.com/perspective-dev/perspective/issues/935)_ -->
|
||||
|
||||
### Does date filtering support ranges?
|
||||
|
||||
Date columns can be
|
||||
[filtered](./explanation/view/config/selection_and_ordering.md#filter) with
|
||||
comparison operators (`>`, `<`, `>=`, `<=`) to achieve range-based filtering.
|
||||
Apply two filters on the same date column for a range.
|
||||
|
||||
<!-- _Related:
|
||||
[#3100](https://github.com/perspective-dev/perspective/discussions/3100),
|
||||
[#2023](https://github.com/perspective-dev/perspective/issues/2023)_ -->
|
||||
|
||||
## JupyterLab
|
||||
|
||||
### `PerspectiveWidget` is not loading in JupyterLab
|
||||
|
||||
See the [`PerspectiveWidget` guide](./how_to/python/jupyterlab.md) for full
|
||||
setup details. Ensure the JupyterLab extension version matches your
|
||||
`perspective-python` version. Make sure you are using a compatible JupyterLab
|
||||
for your Perspective version (JupyterLab 4+ currently).
|
||||
|
||||
Check that the extension is enabled with `jupyter labextension list`.
|
||||
|
||||
<!-- _Related: [#1392](https://github.com/perspective-dev/perspective/issues/1392),
|
||||
[#2059](https://github.com/perspective-dev/perspective/issues/2059),
|
||||
[#2307](https://github.com/perspective-dev/perspective/issues/2307)_ -->
|
||||
|
||||
## Memory and Performance
|
||||
|
||||
### Perspective has a memory leak
|
||||
|
||||
Maybe, but please review the
|
||||
[Cleaning up resources](./how_to/javascript/deleting.md) docs carefully before
|
||||
opening an Issue reporting it (and of course review
|
||||
[`CONTRIBUTING.md`](https://github.com/perspective-dev/perspective/blob/master/CONTRIBUTING.md)
|
||||
before opening _any_ Issue). Ensure you call `.delete()` on Views, Tables, and
|
||||
`<perspective-viewer>` instances when they are no longer needed, in reverse
|
||||
dependency order.
|
||||
|
||||
<!-- _Related: [#1037](https://github.com/perspective-dev/perspective/issues/1037),
|
||||
[#1723](https://github.com/perspective-dev/perspective/issues/1723),
|
||||
[#3035](https://github.com/perspective-dev/perspective/issues/3035),
|
||||
[#1329](https://github.com/perspective-dev/perspective/issues/1329)_ -->
|
||||
|
||||
### How many rows can Perspective's built-in engine handle?
|
||||
|
||||
Perspective is designed for large datasets and can handle millions of rows
|
||||
depending on the number of columns and available memory. Performance also
|
||||
significantly depends on column types (`"string"` being slower and larger than
|
||||
other types due to dictionary interning).
|
||||
|
||||
For larger datasets or out-of-memory virtualized datasets, see
|
||||
[Virtual Servers](./explanation/virtual_servers.md).
|
||||
|
||||
<!-- _Related: [#341](https://github.com/perspective-dev/perspective/issues/341),
|
||||
[#1719](https://github.com/perspective-dev/perspective/issues/1719),
|
||||
[#1089](https://github.com/perspective-dev/perspective/issues/1089)_ -->
|
||||
|
||||
### How do I control threading in `perspective-python`?
|
||||
|
||||
The Python library uses a thread pool internally. For advanced threading
|
||||
control, consult the
|
||||
[multithreading documentation](./how_to/python/multithreading.md).
|
||||
|
||||
<!-- _Related: [#1145](https://github.com/perspective-dev/perspective/issues/1145),
|
||||
[#1313](https://github.com/perspective-dev/perspective/issues/1313)_ -->
|
||||
|
||||
## Theming and Styling
|
||||
|
||||
### How do I enable dark theme?
|
||||
|
||||
Import `themes.css` (see [Theming](./how_to/javascript/theming.md)) and set the
|
||||
theme via `restore()`:
|
||||
|
||||
```javascript
|
||||
await viewer.restore({ theme: "Pro Dark" });
|
||||
```
|
||||
|
||||
Or import just the dark theme directly:
|
||||
`import "@perspective-dev/viewer/dist/css/pro-dark.css";`
|
||||
|
||||
<!-- _Related: [#950](https://github.com/perspective-dev/perspective/issues/950),
|
||||
[#882](https://github.com/perspective-dev/perspective/issues/882)_ -->
|
||||
|
||||
### Can I create a custom cell renderer for the datagrid?
|
||||
|
||||
The datagrid plugin supports custom styling via
|
||||
[`column_config`](https://perspective-dev.github.io/viewer/types/src_ts_ts-rs_ColumnConfigValues.ts.ColumnConfigValues.html)
|
||||
and CSS custom properties, but custom cell renderers require building a custom
|
||||
plugin.
|
||||
|
||||
<!-- _Related: [#1508](https://github.com/perspective-dev/perspective/issues/1508)_ -->
|
||||
|
||||
### How do I customize chart colors?
|
||||
|
||||
Chart colors can be customized via
|
||||
[CSS custom properties](./how_to/javascript/theming.md#custom-themes) on the
|
||||
`<perspective-viewer>` element.
|
||||
|
||||
<!-- _Related:
|
||||
[#2810](https://github.com/perspective-dev/perspective/discussions/2810),
|
||||
[#2859](https://github.com/perspective-dev/perspective/discussions/2859),
|
||||
[#2000](https://github.com/perspective-dev/perspective/discussions/2000)_ -->
|
||||
|
||||
## Streaming and Real-Time Updates
|
||||
|
||||
### How do I stream data into a Perspective table?
|
||||
|
||||
Use [`table.update()`](./explanation/table/update_and_remove.md) to push new
|
||||
data incrementally. For [indexed](./explanation/table/options.md) tables,
|
||||
updates with matching index values will replace existing rows.
|
||||
|
||||
<!-- _Related: [#1133](https://github.com/perspective-dev/perspective/issues/1133),
|
||||
[#1054](https://github.com/perspective-dev/perspective/issues/1054)_ -->
|
||||
|
||||
### `table.update()` raises "No Running Event Loop"
|
||||
|
||||
Perspective 3+ is now threadsafe by default and no longer requires special loop
|
||||
integration.
|
||||
|
||||
<!-- _Related:
|
||||
[#2801](https://github.com/perspective-dev/perspective/discussions/2801)_ -->
|
||||
|
||||
### How do I listen for data updates?
|
||||
|
||||
Use `view.on_update()` to register a callback that fires when the underlying
|
||||
table data changes. See [Listening for events](./how_to/javascript/events.md)
|
||||
and [Advanced View Operations](./explanation/view/advanced.md#update-callbacks).
|
||||
|
||||
<!-- _Related: [#1152](https://github.com/perspective-dev/perspective/issues/1152),
|
||||
[#2912](https://github.com/perspective-dev/perspective/discussions/2912)_ -->
|
||||
|
||||
## Server Architecture
|
||||
|
||||
### What is the difference between Client-only, Client/Server, and Server-only modes?
|
||||
|
||||
- **Client-only**: The Perspective engine runs entirely in the browser via WASM.
|
||||
Best for small to medium datasets.
|
||||
- **Client/Server (replicated)**: Data is hosted on a server and replicated to
|
||||
the client. The client has a full copy and performs queries locally.
|
||||
- **Server-only**: All queries are executed on the server. The client only
|
||||
renders results. Best for very large datasets.
|
||||
|
||||
See [Data Architecture](./explanation/architecture.md) for detailed explanations
|
||||
of each mode.
|
||||
|
||||
<!-- _Related:
|
||||
[#2916](https://github.com/perspective-dev/perspective/discussions/2916)_ -->
|
||||
|
||||
### Is the WebSocket Perspective `Server` safe to expose to untrusted clients?
|
||||
|
||||
No. The WebSocket `Server` is not a security boundary. Every connected `Client`
|
||||
is treated as the author of the queries it submits, and is permitted to create
|
||||
and delete `Table`/`View` resources, author arbitrary
|
||||
[expression columns](./explanation/view/config/expressions.md), and — for
|
||||
[Virtual Server](./explanation/virtual_servers.md) backends like DuckDB or
|
||||
ClickHouse — author SQL fragments executed under the configured database
|
||||
role. The bundled WebSocket adapters
|
||||
(`tornado.py`/`aiohttp.py`/`starlette.py`/`WebSocketServer`) are reference
|
||||
integrations and do not authenticate, authorize, or enforce origin policy.
|
||||
|
||||
WebSocket Deployments that need per-user isolation must put an authenticating
|
||||
proxy in front of the `Server`, run a least-privileged database role for any
|
||||
`Virtual Server` backend, and/or isolate users into separate `Server`
|
||||
instances. See [`SECURITY.md`](../../SECURITY.md) for the full threat model
|
||||
and deployment guidance.
|
||||
|
||||
Obviously, none of this applies to WASM DBs like Perspective and DuckDB.
|
||||
|
||||
### Does Perspective sanitize SQL `Virtual Server`s?
|
||||
|
||||
No, by design. [Virtual Server](./explanation/virtual_servers.md) backends
|
||||
interpolate client-supplied `view_id`, `table_id`, `column_name`, expression
|
||||
strings, and filter operators directly into SQL templates without
|
||||
parameterization or whitelist validation. The `Client` is the author of the
|
||||
queries — there is no privilege boundary inside the engine for sanitization
|
||||
to enforce. If your deployment needs to restrict the SQL surface area exposed
|
||||
to a `Client`, the supported boundary is the database role the `Virtual Server`
|
||||
is configured with (read-only etc), or better complete isolation via WASM
|
||||
backend.
|
||||
|
||||
### How do I set up WebSocket authentication?
|
||||
|
||||
The [`WebSocketServer`](./how_to/javascript/nodejs_server.md) does not include
|
||||
built-in authentication. Implement authentication at the transport layer (e.g.,
|
||||
via middleware in your HTTP server) before the WebSocket upgrade. For more
|
||||
complex needs, `WebSocketServer` is a simple example server based on the
|
||||
`node:http` module which can serve as a starting point for a custom server.
|
||||
|
||||
<!-- _Related:
|
||||
[#2788](https://github.com/perspective-dev/perspective/discussions/2788)_ -->
|
||||
|
||||
### Can I bind Perspective to a database?
|
||||
|
||||
Perspective supports [Virtual Servers](./explanation/virtual_servers.md) that
|
||||
proxy queries to external data sources, with built-in implementations for e.g.
|
||||
[DuckDB](./how_to/javascript/virtual_server/duckdb.md).
|
||||
|
||||
<!-- _Related: [#1255](https://github.com/perspective-dev/perspective/issues/1255),
|
||||
[#1361](https://github.com/perspective-dev/perspective/discussions/1361)_ -->
|
||||
|
||||
## Aggregation
|
||||
|
||||
### Can I apply multiple aggregates to the same column?
|
||||
|
||||
Yes, by creating a duplicate/alias for your column via
|
||||
[`expressions`](./explanation/view/config/expressions.md):
|
||||
|
||||
```javascript
|
||||
await viewer.restore({
|
||||
columns: ["Sales", "Sales 2"],
|
||||
expresions: { "Sales 2": '"Sales"' },
|
||||
aggregate: {
|
||||
Sales: "sum",
|
||||
"Sales 2": "avg",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<!-- _Related: [#272](https://github.com/perspective-dev/perspective/issues/272)_ -->
|
||||
|
||||
### Can I compute a ratio between aggregated columns?
|
||||
|
||||
Use [expression columns](./explanation/view/config/expressions.md) on an
|
||||
aggregated View to compute ratios. Define an expression that divides one column
|
||||
by another.
|
||||
|
||||
<!-- _Related:
|
||||
[#2994](https://github.com/perspective-dev/perspective/discussions/2994),
|
||||
[#3096](https://github.com/perspective-dev/perspective/discussions/3096)_ -->
|
||||
|
||||
## Data Loading and Arrow
|
||||
|
||||
### How do I load Apache Arrow data into Perspective?
|
||||
|
||||
Perspective natively accepts
|
||||
[Apache Arrow format](./explanation/table/loading_data.md). Pass an
|
||||
`ArrayBuffer` containing Arrow IPC data directly to `table()` or
|
||||
`table.update()`.
|
||||
|
||||
<!-- _Related: [#1157](https://github.com/perspective-dev/perspective/issues/1157),
|
||||
[#929](https://github.com/perspective-dev/perspective/issues/929)_ -->
|
||||
|
||||
### What data formats does Perspective accept?
|
||||
|
||||
Perspective accepts (see [Loading data](./explanation/table/loading_data.md)):
|
||||
|
||||
- **JavaScript**: JSON (row-oriented or column-oriented objects), CSV strings,
|
||||
Apache Arrow `ArrayBuffer`
|
||||
- **Python**: `dict`, `list`, `pandas.DataFrame`, `pyarrow.Table`, CSV strings,
|
||||
Apache Arrow bytes
|
||||
|
||||
<!-- _Related: [#929](https://github.com/perspective-dev/perspective/issues/929),
|
||||
[#2524](https://github.com/perspective-dev/perspective/issues/2524)_ -->
|
||||
|
||||
### CSV update fails but CSV creation works
|
||||
|
||||
When updating a table created with a schema, ensure the CSV column names and
|
||||
types match the schema exactly. Mismatched column names or types will cause
|
||||
update failures.
|
||||
|
||||
<!-- _Related: [#2524](https://github.com/perspective-dev/perspective/issues/2524)_ -->
|
||||
|
||||
## Export
|
||||
|
||||
### Can I export the viewer to HTML, PNG or PDF?
|
||||
|
||||
HTML and PNG exports are available via `viewer.export("html")` and
|
||||
`viewer.export("png")`, respectively. For PDF, render the viewer and use browser
|
||||
or headless browser screenshot capabilities.
|
||||
|
||||
<!-- _Related: [#2836](https://github.com/perspective-dev/perspective/issues/2836),
|
||||
[#2770](https://github.com/perspective-dev/perspective/discussions/2770),
|
||||
[#2772](https://github.com/perspective-dev/perspective/issues/2772)_ -->
|
||||
|
||||
### Can I export data to Excel?
|
||||
|
||||
Perspective does not have built-in Excel export. Export data via
|
||||
`view.to_csv()`, `view.to_json()`, or `view.to_arrow()` (see
|
||||
[Serializing data](./how_to/javascript/serializing.md)) and convert to Excel
|
||||
using a library like `xlsx` (JavaScript) or `openpyxl` (Python).
|
||||
|
||||
<!-- _Related:
|
||||
[#2738](https://github.com/perspective-dev/perspective/discussions/2738)_ -->
|
||||
|
||||
### How do I copy data from a cell or row?
|
||||
|
||||
Use the `"text"` export mode when data is selected:
|
||||
`await viewer.export("text")`.
|
||||
|
||||
<!-- _Related: [#2765](https://github.com/perspective-dev/perspective/issues/2765),
|
||||
[#2356](https://github.com/perspective-dev/perspective/discussions/2356)_ -->
|
||||
|
||||
## Table Operations
|
||||
|
||||
### `table.remove()` does not update the viewer
|
||||
|
||||
The [`remove()`](./explanation/table/update_and_remove.md) method requires an
|
||||
[indexed](./explanation/table/options.md) table. Ensure your table was created
|
||||
with an `index` option, and pass the index values to remove.
|
||||
|
||||
<!-- _Related: [#1597](https://github.com/perspective-dev/perspective/issues/1597),
|
||||
[#2293](https://github.com/perspective-dev/perspective/issues/2293)_ -->
|
||||
|
||||
## Viewer Configuration
|
||||
|
||||
### How do I save and restore the viewer state?
|
||||
|
||||
Use
|
||||
[`viewer.save()` and `viewer.restore()`](./how_to/javascript/save_restore.md) to
|
||||
serialize and deserialize the full viewer configuration.
|
||||
|
||||
<!-- _Related: [#1501](https://github.com/perspective-dev/perspective/issues/1501),
|
||||
[#1560](https://github.com/perspective-dev/perspective/issues/1560)_ -->
|
||||
|
||||
### Can I hide the configuration panel?
|
||||
|
||||
The settings panel can be toggled programmatically via
|
||||
`await viewer.restore({ settings: false })`.
|
||||
|
||||
<!-- _Related:
|
||||
[#2581](https://github.com/perspective-dev/perspective/discussions/2581),
|
||||
[#1085](https://github.com/perspective-dev/perspective/issues/1085)_ -->
|
||||
|
||||
### Can I collapse row groups by default?
|
||||
|
||||
Row group can be closed imperatively via
|
||||
[`view.set_depth()`](./explanation/view/advanced.md). Expansion state is not
|
||||
persisted or configurable via the `save`/`restore` API currently.
|
||||
|
||||
<!-- _Related:
|
||||
[#2695](https://github.com/perspective-dev/perspective/discussions/2695),
|
||||
[#2861](https://github.com/perspective-dev/perspective/issues/2861)_ -->
|
||||
|
||||
## Internationalization
|
||||
|
||||
### Can I change the UI language?
|
||||
|
||||
Perspective's UI text is defined via CSS variables, which can be customized per
|
||||
theme. See the
|
||||
[Icons and Translation](./how_to/javascript/theming.md#icons-and-translation)
|
||||
section of the theming guide for details.
|
||||
|
||||
<!-- _Related: [#1934](https://github.com/perspective-dev/perspective/issues/1934),
|
||||
[#2358](https://github.com/perspective-dev/perspective/issues/2358)_ -->
|
||||
|
||||
## Rust
|
||||
|
||||
### How do I build Perspective from Rust?
|
||||
|
||||
See the [Getting Started](./how_to/rust.md) guide for Rust. The Rust crate wraps
|
||||
the C++ engine and requires a C++ toolchain. You need `cmake` installed and on
|
||||
your path to build the engine.
|
||||
|
||||
<!-- _Related:
|
||||
[#3121](https://github.com/perspective-dev/perspective/discussions/3121),
|
||||
[#3080](https://github.com/perspective-dev/perspective/discussions/3080),
|
||||
[#2684](https://github.com/perspective-dev/perspective/discussions/2684)_ -->
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Can I use Perspective without `<perspective-viewer>`?
|
||||
|
||||
Yes. The `perspective` library (data engine) can be used independently for
|
||||
server-side data processing without any UI. Use
|
||||
[`table()` and `view()`](./how_to/javascript/worker.md) directly to query data.
|
||||
|
||||
<!-- _Related:
|
||||
[#2933](https://github.com/perspective-dev/perspective/discussions/2933),
|
||||
[#2644](https://github.com/perspective-dev/perspective/discussions/2644)_ -->
|
||||
|
||||
### Can I use Perspective in Pyodide?
|
||||
|
||||
There is an emscripten wheel
|
||||
[published via Releases](https://github.com/perspective-dev/perspective/releases),
|
||||
but it must be downloaded and hosted manually and is only built for specific
|
||||
pyodide versions.
|
||||
|
||||
<!-- _Related:
|
||||
[#2880](https://github.com/perspective-dev/perspective/discussions/2880)_ -->
|
||||
|
||||
### How do I handle row selection events?
|
||||
|
||||
Listen for
|
||||
[`perspective-click` and `perspective-select`](./how_to/javascript/events.md)
|
||||
events on the `<perspective-viewer>` element.
|
||||
|
||||
<!-- _Related:
|
||||
[#2589](https://github.com/perspective-dev/perspective/discussions/2589),
|
||||
[#1076](https://github.com/perspective-dev/perspective/issues/1076)_ -->
|
||||
@@ -0,0 +1,83 @@
|
||||
# Summary
|
||||
|
||||
[What is Perspective](./perspective.md)
|
||||
|
||||
# Concepts
|
||||
|
||||
- [Data Architecture](./explanation/architecture.md)
|
||||
- [Client-only](./explanation/architecture/client_only.md)
|
||||
- [Client/Server replicated](./explanation/architecture/client_server.md)
|
||||
- [Server only](./explanation/architecture/server_only.md)
|
||||
- [Virtual Servers](./explanation/virtual_servers.md)
|
||||
- [`Table`](./explanation/table.md)
|
||||
- [Schema and column types](./explanation/table/schema.md)
|
||||
- [Loading data](./explanation/table/loading_data.md)
|
||||
- [Construct an empty `Table` from a schema](./explanation/table/constructing_schema.md)
|
||||
- [`index` and `limit` options](./explanation/table/options.md)
|
||||
- [`update()` and `remove()` streaming methods](./explanation/table/update_and_remove.md)
|
||||
- [`clear()` and `replace()` start-over methods](./explanation/table/clear_and_replace.md)
|
||||
- [`View`](./explanation/view.md)
|
||||
- [Querying data](./explanation/view/querying.md)
|
||||
- [Grouping and Pivots](./explanation/view/config/grouping_and_pivots.md)
|
||||
- [Selection and Ordering](./explanation/view/config/selection_and_ordering.md)
|
||||
- [`expressions`](./explanation/view/config/expressions.md)
|
||||
- [Advanced View Operations](./explanation/view/advanced.md)
|
||||
- [`Join`](./explanation/join.md)
|
||||
- [Join Types](./explanation/join/join_types.md)
|
||||
- [Join Options](./explanation/join/options.md)
|
||||
- [Reactivity and Constraints](./explanation/join/reactivity.md)
|
||||
|
||||
# JavaScript
|
||||
|
||||
- [Installation and Module Structure](./how_to/javascript/installation.md)
|
||||
- [Importing with or without a bundler](./how_to/javascript/importing.md)
|
||||
- [`perspective` data engine library](./how_to/javascript/worker.md)
|
||||
- [Serializing data](./how_to/javascript/serializing.md)
|
||||
- [Cleaning up resources](./how_to/javascript/deleting.md)
|
||||
- [Hosting a `WebSocketServer` in Node.js](./how_to/javascript/nodejs_server.md)
|
||||
- [Customizing `perspective.worker()`](./how_to/javascript/custom_worker.md)
|
||||
- [Joining Tables](./how_to/javascript/join.md)
|
||||
- [`perspective-viewer` Custom Element library](./how_to/javascript/viewer.md)
|
||||
- [Loading data](./how_to/javascript/loading_data.md)
|
||||
- [Theming](./how_to/javascript/theming.md)
|
||||
- [Saving and restoring UI state](./how_to/javascript/save_restore.md)
|
||||
- [Listening for events](./how_to/javascript/events.md)
|
||||
- [Plugin render limits](./how_to/javascript/plugin_settings.md)
|
||||
- [Virtual Servers](./how_to/javascript/virtual_server.md)
|
||||
- [DuckDB](./how_to/javascript/virtual_server/duckdb.md)
|
||||
- [ClickHouse](./how_to/javascript/virtual_server/clickhouse.md)
|
||||
- [Custom](./how_to/javascript/virtual_server/custom.md)
|
||||
- [React Component](./how_to/javascript/react.md)
|
||||
|
||||
# Python
|
||||
|
||||
- [Overview](./explanation/python.md)
|
||||
- [Installation](./how_to/python/installation.md)
|
||||
- [Loading data into a `Table`](./how_to/python/table.md)
|
||||
- [`pandas`, `polars` and `pyarrow` integration](./how_to/python/table_data.md)
|
||||
- [Callbacks and events](./how_to/python/callbacks.md)
|
||||
- [Multithreading](./how_to/python/multithreading.md)
|
||||
- [Hosting a WebSocket server](./how_to/python/websocket.md)
|
||||
- [Joining Tables](./how_to/python/join.md)
|
||||
- [`PerspectiveWidget` for JupyterLab](./how_to/python/jupyterlab.md)
|
||||
- [Virtual Servers](./how_to/python/virtual_server.md)
|
||||
- [DuckDB](./how_to/python/virtual_server/duckdb.md)
|
||||
- [ClickHouse](./how_to/python/virtual_server/clickhouse.md)
|
||||
- [Polars](./how_to/python/virtual_server/polars.md)
|
||||
- [Custom](./how_to/python/virtual_server/custom.md)
|
||||
|
||||
# Rust
|
||||
|
||||
- [Getting Started](./how_to/rust.md)
|
||||
|
||||
# Tutorials
|
||||
|
||||
- [A `tornado` server in Python](./tutorials/python/tornado.md)
|
||||
|
||||
# API Reference
|
||||
|
||||
- [API Reference](./api_reference.md)
|
||||
|
||||
# FAQ
|
||||
|
||||
- [FAQ](./FAQ.md)
|
||||
@@ -0,0 +1,22 @@
|
||||
# API Reference
|
||||
|
||||
Perspective's complete API is hosted on `docs.rs`:
|
||||
|
||||
- Python API
|
||||
- [`perspective`](https://perspective-dev.github.io/python/index.html)
|
||||
- [`perspective.widget`](https://perspective-dev.github.io/python/perspective/widget.html)
|
||||
- [`perspective.handlers.aiohttp`](https://perspective-dev.github.io/python/perspective/handlers/aiohttp.htm)
|
||||
- [`perspective.handlers.starlette`](https://perspective-dev.github.io/python/perspective/handlers/starlett.htm)
|
||||
- [`perspective.handlers.tornado`](https://perspective-dev.github.io/python/perspective/handlers/tornado.htm)
|
||||
- JavaScript API
|
||||
- [`@perspective-dev/client` Browser](https://perspective-dev.github.io/browser/modules/src_ts_perspective.browser.ts.html)
|
||||
- [`@perspective-dev/client` Node.js](https://perspective-dev.github.io/node/modules/src_ts_perspective.node.ts.html)
|
||||
- [`@perspective-dev/viewer`](https://perspective-dev.github.io/viewer/modules/perspective-viewer.html)
|
||||
- [`@perspective-dev/react`](https://perspective-dev.github.io/react/index.html)
|
||||
- Rust API
|
||||
- [`perspective`](https://docs.rs/perspective/latest/perspective/)
|
||||
- [`perspective-client`](https://docs.rs/perspective-client/latest/perspective_client/)
|
||||
- [`perspective-server`](https://docs.rs/perspective-server/latest/perspective_server/)
|
||||
- [`perspective-python`](https://docs.rs/perspective-python/latest/perspective_python/)
|
||||
- [`perspective-js`](https://docs.rs/perspective-js/latest/perspective_js/)
|
||||
- [`perspective-viewer`](https://docs.rs/perspective-viewer/latest/perspective_viewer/)
|
||||
@@ -0,0 +1,4 @@
|
||||
# Overview
|
||||
|
||||
This section covers Perspective's core concepts: data architecture patterns,
|
||||
the `Table` and `View` data model, and language-specific module details.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Data Architecture
|
||||
|
||||
Application developers can choose from
|
||||
[Client (WebAssembly)](./architecture/client_only.md),
|
||||
[Server (Python/Node)](./architecture/server_only.md) or
|
||||
[Client/Server Replicated](./architecture/client_server.md) designs to bind
|
||||
data, and a web application can use one or a mix of these designs as needed. By
|
||||
serializing to Apache Arrow, tables are duplicated and synchronized across
|
||||
runtimes efficiently.
|
||||
|
||||
Perspective is a multi-language platform. The examples in this section use
|
||||
Python and JavaScript as an example, but the same general principles apply to
|
||||
any `Client`/`Server` combination.
|
||||
|
||||
<img src="./architecture/architecture.svg" />
|
||||
@@ -0,0 +1,197 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
subgraph cluster_thread_2 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 2";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_2 [
|
||||
label="table(df)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_2 [
|
||||
label="view({\l group_by: [\"State\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view_thread_2_2 [
|
||||
label="view({\l group_by: [\"City\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_2 -> view_thread_2_2;
|
||||
table_thread_2 -> view_thread_2;
|
||||
}
|
||||
subgraph cluster_thread_1 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 1";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_1 [
|
||||
label="table(arrow)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_1 [
|
||||
label="view({\l group_by:'[\"State\"]\l split_by:'[\"Segment\"]'\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_1 -> view_thread_1;
|
||||
}
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
subgraph cluster_2 {
|
||||
graph [
|
||||
label="\lWebWorker 1";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table1 [
|
||||
label="table(csv)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
table_remote_view [
|
||||
label="table(json)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view1 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view2 [
|
||||
label="view({\l group_by: [\"Sub-Category\"]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view3 [
|
||||
label="view()\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table1 -> {view1 view2};
|
||||
table_remote_view -> view3;
|
||||
}
|
||||
|
||||
subgraph cluster_webworker2 {
|
||||
graph [
|
||||
label="\lWebWorker 2";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table12 [
|
||||
label="table(...)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
view12 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table12 -> {view12} [color="#E6E2DA"];
|
||||
}
|
||||
|
||||
view_thread_2 -> table12 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
|
||||
view1 -> viewer1 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
view2 -> viewer2 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
view3 -> viewer3 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
viewer1 [
|
||||
label = "<perspective-viewer\l view=\"Y Bar\"\l row-pivots='[\"Category\"]'\l filters='[[\"State\",\"==\",\"Texas\"]]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
viewer2 [
|
||||
label = "<perspective-viewer\l view=\"xy_scatter\"\l row-pivots='[\"Sub-Category\"]'>\l"
|
||||
width=2.8
|
||||
color=lightgrey
|
||||
];
|
||||
viewer3 [
|
||||
label = "<perspective-viewer\l view=\"grid\">\l"
|
||||
width=2.8
|
||||
];
|
||||
viewerN [
|
||||
style=invis
|
||||
]
|
||||
|
||||
viewer5 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
viewerZ [
|
||||
style=invis
|
||||
height=0.3
|
||||
]
|
||||
viewer4 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view_thread_1 -> viewer4 [penwidth=2 style=dashed arrowhead=none color="#D1A043" constraint=false];
|
||||
view12 -> viewer5 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
subgraph cluster_2 {
|
||||
graph [
|
||||
label="\lWebWorker 1";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table1 [
|
||||
label="table(csv)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
|
||||
view2 [
|
||||
label="view({\l group_by: [\"Sub-Category\"]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table1 -> {view2};
|
||||
}
|
||||
|
||||
view2 -> viewer2 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer2 [
|
||||
label = "<perspective-viewer\l view=\"xy_scatter\"\l row-pivots='[\"Sub-Category\"]'>\l"
|
||||
width=2.8
|
||||
color=lightgrey
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="590pt" height="170pt"
|
||||
viewBox="0.00 0.00 590.00 170.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 166)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="8,-8 8,-154 574,-154 574,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="41" y="-122.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="18,-18 18,-105 326,-105 326,-18 18,-18"/>
|
||||
<text text-anchor="middle" x="55.5" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 1</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="342,-18 342,-100 564,-100 564,-18 342,-18"/>
|
||||
<text text-anchor="middle" x="364" y="-80.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-64 28,-64 28,-28 100,-28 100,-64"/>
|
||||
<text text-anchor="middle" x="64" y="-44.1" font-family="monospace" font-size="8.00">table(csv)</text>
|
||||
</g>
|
||||
<!-- view2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-64 136,-64 136,-28 316,-28 316,-64"/>
|
||||
<text text-anchor="start" x="144" y="-53.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-44.1" font-family="monospace" font-size="8.00">    group_by: ["Sub-Category"]</text>
|
||||
<text text-anchor="start" x="144" y="-35.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table1->view2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-46C108.52,-46 117.91,-46 127.66,-46"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-48.8 135.91,-46 127.91,-43.2 127.91,-48.8"/>
|
||||
</g>
|
||||
<!-- viewer2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>viewer2</title>
|
||||
<polygon fill="white" stroke="lightgrey" points="554,-64 352,-64 352,-28 554,-28 554,-64"/>
|
||||
<text text-anchor="start" x="360" y="-53.1" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="360" y="-44.1" font-family="monospace" font-size="8.00">    view="xy_scatter"</text>
|
||||
<text text-anchor="start" x="360" y="-35.1" font-family="monospace" font-size="8.00">    row-pivots='["Sub-Category"]'></text>
|
||||
</g>
|
||||
<!-- view2->viewer2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>view2->viewer2</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M316.25,-46C327.9,-46 339.91,-46 351.73,-46"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,66 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
|
||||
subgraph cluster_thread_1 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 1";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_1 [
|
||||
label="table(arrow)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_1 [
|
||||
label="view({\l group_by:'[\"State\"]\l split_by:'[\"Segment\"]'\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_1 -> view_thread_1;
|
||||
}
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer4 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view_thread_1 -> viewer4 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="602pt" height="178pt"
|
||||
viewBox="0.00 0.00 602.00 178.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 174)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-8 8,-162 336,-162 336,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="59" y="-130.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_1</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-18 18,-113 326,-113 326,-18 18,-18"/>
|
||||
<text text-anchor="middle" x="94.5" y="-89" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 1</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-157 586,-157 586,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-125.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="354,-18 354,-108 576,-108 576,-18 354,-18"/>
|
||||
<text text-anchor="middle" x="376" y="-88.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-68 28,-68 28,-32 100,-32 100,-68"/>
|
||||
<text text-anchor="middle" x="64" y="-48.1" font-family="monospace" font-size="8.00">table(arrow)</text>
|
||||
</g>
|
||||
<!-- view_thread_1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-72 136,-72 136,-28 316,-28 316,-72"/>
|
||||
<text text-anchor="start" x="144" y="-61.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-52.6" font-family="monospace" font-size="8.00">    group_by:'["State"]</text>
|
||||
<text text-anchor="start" x="144" y="-43.6" font-family="monospace" font-size="8.00">    split_by:'["Segment"]'</text>
|
||||
<text text-anchor="start" x="144" y="-34.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_1->view_thread_1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_1->view_thread_1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-50C108.52,-50 117.91,-50 127.66,-50"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-52.8 135.91,-50 127.91,-47.2 127.91,-52.8"/>
|
||||
</g>
|
||||
<!-- viewer4 -->
|
||||
<g id="node3" class="node">
|
||||
<title>viewer4</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="566,-72 364,-72 364,-28 566,-28 566,-72"/>
|
||||
<text text-anchor="start" x="372" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="372" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="372" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="372" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view_thread_1->viewer4 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>view_thread_1->viewer4</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.26,-50C331.75,-50 347.94,-50 363.69,-50"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,97 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
subgraph cluster_thread_2 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 2";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_2 [
|
||||
label="table(df)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_2 [
|
||||
label="view({\l group_by: [\"State\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
|
||||
table_thread_2 -> view_thread_2;
|
||||
}
|
||||
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
|
||||
subgraph cluster_webworker2 {
|
||||
graph [
|
||||
label="\lWebWorker 2";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table12 [
|
||||
label="table(...)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
view12 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table12 -> {view12} [color="#E6E2DA"];
|
||||
}
|
||||
|
||||
view_thread_2 -> table12 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer5 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view12 -> viewer5 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="926pt" height="178pt"
|
||||
viewBox="0.00 0.00 926.00 178.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 174)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-12 8,-158 336,-158 336,-12 8,-12"/>
|
||||
<text text-anchor="middle" x="59" y="-126.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_2</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-22 18,-109 326,-109 326,-22 18,-22"/>
|
||||
<text text-anchor="middle" x="94.5" y="-85" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 2</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-162 910,-162 910,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-130.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_webworker2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-18 354,-113 662,-113 662,-18 354,-18"/>
|
||||
<text text-anchor="middle" x="391.5" y="-89" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 2</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="678,-18 678,-108 900,-108 900,-18 678,-18"/>
|
||||
<text text-anchor="middle" x="700" y="-88.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_2 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_2</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-68 28,-68 28,-32 100,-32 100,-68"/>
|
||||
<text text-anchor="middle" x="64" y="-48.1" font-family="monospace" font-size="8.00">table(df)</text>
|
||||
</g>
|
||||
<!-- view_thread_2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-68 136,-68 136,-32 316,-32 316,-68"/>
|
||||
<text text-anchor="start" x="144" y="-57.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-48.1" font-family="monospace" font-size="8.00">    group_by: ["State"]]</text>
|
||||
<text text-anchor="start" x="144" y="-39.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_2->view_thread_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-50C108.52,-50 117.91,-50 127.66,-50"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-52.8 135.91,-50 127.91,-47.2 127.91,-52.8"/>
|
||||
</g>
|
||||
<!-- table12 -->
|
||||
<g id="node3" class="node">
|
||||
<title>table12</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-68 364,-68 364,-32 436,-32 436,-68"/>
|
||||
<text text-anchor="middle" x="400" y="-48.1" font-family="monospace" font-size="8.00">table(...)</text>
|
||||
</g>
|
||||
<!-- view_thread_2->table12 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>view_thread_2->table12</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.02,-50C332.91,-50 349.64,-50 363.61,-50"/>
|
||||
</g>
|
||||
<!-- view12 -->
|
||||
<g id="node4" class="node">
|
||||
<title>view12</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-72 472,-72 472,-28 652,-28 652,-72"/>
|
||||
<text text-anchor="start" x="480" y="-61.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-52.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-43.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-34.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table12->view12 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>table12->view12</title>
|
||||
<path fill="none" stroke="#e6e2da" d="M436.11,-50C444.52,-50 453.91,-50 463.66,-50"/>
|
||||
<polygon fill="#e6e2da" stroke="#e6e2da" points="463.91,-52.8 471.91,-50 463.91,-47.2 463.91,-52.8"/>
|
||||
</g>
|
||||
<!-- viewer5 -->
|
||||
<g id="node5" class="node">
|
||||
<title>viewer5</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-72 688,-72 688,-28 890,-28 890,-72"/>
|
||||
<text text-anchor="start" x="696" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view12->viewer5 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>view12->viewer5</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-50C663.9,-50 675.91,-50 687.73,-50"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="926pt" height="499pt"
|
||||
viewBox="0.00 0.00 926.00 499.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 495)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-9 8,-314 336,-314 336,-9 8,-9"/>
|
||||
<text text-anchor="middle" x="59" y="-282.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_2</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-124 18,-265 326,-265 326,-124 18,-124"/>
|
||||
<text text-anchor="middle" x="94.5" y="-241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 2</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_thread_1</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-19 18,-114 326,-114 326,-19 18,-19"/>
|
||||
<text text-anchor="middle" x="94.5" y="-90" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 1</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-483 910,-483 910,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-451.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust5" class="cluster">
|
||||
<title>cluster_2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-231 354,-434 662,-434 662,-231 354,-231"/>
|
||||
<text text-anchor="middle" x="391.5" y="-410" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 1</text>
|
||||
</g>
|
||||
<g id="clust7" class="cluster">
|
||||
<title>cluster_webworker2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-120 354,-215 662,-215 662,-120 354,-120"/>
|
||||
<text text-anchor="middle" x="391.5" y="-191" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 2</text>
|
||||
</g>
|
||||
<g id="clust9" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="678,-18 678,-434 900,-434 900,-18 678,-18"/>
|
||||
<text text-anchor="middle" x="700" y="-414.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_2 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_2</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-197 28,-197 28,-161 100,-161 100,-197"/>
|
||||
<text text-anchor="middle" x="64" y="-177.1" font-family="monospace" font-size="8.00">table(df)</text>
|
||||
</g>
|
||||
<!-- view_thread_2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-170 136,-170 136,-134 316,-134 316,-170"/>
|
||||
<text text-anchor="start" x="144" y="-159.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-150.1" font-family="monospace" font-size="8.00">    group_by: ["State"]]</text>
|
||||
<text text-anchor="start" x="144" y="-141.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>table_thread_2->view_thread_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-173.07C108.61,-171.64 118.1,-170.04 127.95,-168.38"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="128.49,-171.13 135.91,-167.03 127.56,-165.6 128.49,-171.13"/>
|
||||
</g>
|
||||
<!-- view_thread_2_2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>view_thread_2_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-224 136,-224 136,-188 316,-188 316,-224"/>
|
||||
<text text-anchor="start" x="144" y="-213.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-204.1" font-family="monospace" font-size="8.00">    group_by: ["City"]]</text>
|
||||
<text text-anchor="start" x="144" y="-195.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2_2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_2->view_thread_2_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-184.93C108.61,-186.36 118.1,-187.96 127.95,-189.62"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.56,-192.4 135.91,-190.97 128.49,-186.87 127.56,-192.4"/>
|
||||
</g>
|
||||
<!-- table12 -->
|
||||
<g id="node11" class="node">
|
||||
<title>table12</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-170 364,-170 364,-134 436,-134 436,-170"/>
|
||||
<text text-anchor="middle" x="400" y="-150.1" font-family="monospace" font-size="8.00">table(...)</text>
|
||||
</g>
|
||||
<!-- view_thread_2->table12 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>view_thread_2->table12</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.02,-152C332.91,-152 349.64,-152 363.61,-152"/>
|
||||
</g>
|
||||
<!-- table_thread_1 -->
|
||||
<g id="node4" class="node">
|
||||
<title>table_thread_1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-69 28,-69 28,-33 100,-33 100,-69"/>
|
||||
<text text-anchor="middle" x="64" y="-49.1" font-family="monospace" font-size="8.00">table(arrow)</text>
|
||||
</g>
|
||||
<!-- view_thread_1 -->
|
||||
<g id="node5" class="node">
|
||||
<title>view_thread_1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-73 136,-73 136,-29 316,-29 316,-73"/>
|
||||
<text text-anchor="start" x="144" y="-62.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-53.6" font-family="monospace" font-size="8.00">    group_by:'["State"]</text>
|
||||
<text text-anchor="start" x="144" y="-44.6" font-family="monospace" font-size="8.00">    split_by:'["Segment"]'</text>
|
||||
<text text-anchor="start" x="144" y="-35.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_1->view_thread_1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>table_thread_1->view_thread_1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-51C108.52,-51 117.91,-51 127.66,-51"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-53.8 135.91,-51 127.91,-48.2 127.91,-53.8"/>
|
||||
</g>
|
||||
<!-- viewer4 -->
|
||||
<g id="node19" class="node">
|
||||
<title>viewer4</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-72 688,-72 688,-28 890,-28 890,-72"/>
|
||||
<text text-anchor="start" x="696" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view_thread_1->viewer4 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>view_thread_1->viewer4</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.2,-50.84C417.45,-50.66 582.21,-50.37 687.8,-50.18"/>
|
||||
</g>
|
||||
<!-- table1 -->
|
||||
<g id="node6" class="node">
|
||||
<title>table1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-364 364,-364 364,-328 436,-328 436,-364"/>
|
||||
<text text-anchor="middle" x="400" y="-344.1" font-family="monospace" font-size="8.00">table(csv)</text>
|
||||
</g>
|
||||
<!-- view1 -->
|
||||
<g id="node8" class="node">
|
||||
<title>view1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-339 472,-339 472,-295 652,-295 652,-339"/>
|
||||
<text text-anchor="start" x="480" y="-328.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-319.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-310.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-301.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view1 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>table1->view1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-339.64C444.61,-338.1 454.1,-336.38 463.95,-334.59"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="464.54,-337.33 471.91,-333.15 463.54,-331.82 464.54,-337.33"/>
|
||||
</g>
|
||||
<!-- view2 -->
|
||||
<g id="node9" class="node">
|
||||
<title>view2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-393 472,-393 472,-357 652,-357 652,-393"/>
|
||||
<text text-anchor="start" x="480" y="-382.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-373.1" font-family="monospace" font-size="8.00">    group_by: ["Sub-Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-364.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view2 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>table1->view2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-352.36C444.61,-353.9 454.1,-355.62 463.95,-357.41"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="463.54,-360.18 471.91,-358.85 464.54,-354.67 463.54,-360.18"/>
|
||||
</g>
|
||||
<!-- table_remote_view -->
|
||||
<g id="node7" class="node">
|
||||
<title>table_remote_view</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-277 364,-277 364,-241 436,-241 436,-277"/>
|
||||
<text text-anchor="middle" x="400" y="-257.1" font-family="monospace" font-size="8.00">table(json)</text>
|
||||
</g>
|
||||
<!-- view3 -->
|
||||
<g id="node10" class="node">
|
||||
<title>view3</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-277 472,-277 472,-241 652,-241 652,-277"/>
|
||||
<text text-anchor="start" x="480" y="-257.1" font-family="monospace" font-size="8.00">view()</text>
|
||||
</g>
|
||||
<!-- table_remote_view->view3 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>table_remote_view->view3</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-259C444.52,-259 453.91,-259 463.66,-259"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="463.91,-261.8 471.91,-259 463.91,-256.2 463.91,-261.8"/>
|
||||
</g>
|
||||
<!-- viewer1 -->
|
||||
<g id="node13" class="node">
|
||||
<title>viewer1</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-344 688,-344 688,-300 890,-300 890,-344"/>
|
||||
<text text-anchor="start" x="696" y="-333.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-324.6" font-family="monospace" font-size="8.00">    view="Y Bar"</text>
|
||||
<text text-anchor="start" x="696" y="-315.6" font-family="monospace" font-size="8.00">    row-pivots='["Category"]'</text>
|
||||
<text text-anchor="start" x="696" y="-306.6" font-family="monospace" font-size="8.00">    filters='[["State","==","Texas"]]'></text>
|
||||
</g>
|
||||
<!-- view1->viewer1 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>view1->viewer1</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-318.98C663.9,-319.24 675.91,-319.51 687.73,-319.77"/>
|
||||
</g>
|
||||
<!-- viewer2 -->
|
||||
<g id="node14" class="node">
|
||||
<title>viewer2</title>
|
||||
<polygon fill="white" stroke="lightgrey" points="890,-398 688,-398 688,-362 890,-362 890,-398"/>
|
||||
<text text-anchor="start" x="696" y="-387.1" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-378.1" font-family="monospace" font-size="8.00">    view="xy_scatter"</text>
|
||||
<text text-anchor="start" x="696" y="-369.1" font-family="monospace" font-size="8.00">    row-pivots='["Sub-Category"]'></text>
|
||||
</g>
|
||||
<!-- view2->viewer2 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>view2->viewer2</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-376.98C663.9,-377.24 675.91,-377.51 687.73,-377.77"/>
|
||||
</g>
|
||||
<!-- viewer3 -->
|
||||
<g id="node15" class="node">
|
||||
<title>viewer3</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-282 688,-282 688,-246 890,-246 890,-282"/>
|
||||
<text text-anchor="start" x="696" y="-266.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-257.6" font-family="monospace" font-size="8.00">    view="grid"></text>
|
||||
</g>
|
||||
<!-- view3->viewer3 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>view3->viewer3</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-260.98C663.9,-261.24 675.91,-261.51 687.73,-261.77"/>
|
||||
</g>
|
||||
<!-- view12 -->
|
||||
<g id="node12" class="node">
|
||||
<title>view12</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-174 472,-174 472,-130 652,-130 652,-174"/>
|
||||
<text text-anchor="start" x="480" y="-163.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-154.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-145.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-136.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table12->view12 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>table12->view12</title>
|
||||
<path fill="none" stroke="#e6e2da" d="M436.11,-152C444.52,-152 453.91,-152 463.66,-152"/>
|
||||
<polygon fill="#e6e2da" stroke="#e6e2da" points="463.91,-154.8 471.91,-152 463.91,-149.2 463.91,-154.8"/>
|
||||
</g>
|
||||
<!-- viewer5 -->
|
||||
<g id="node17" class="node">
|
||||
<title>viewer5</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-174 688,-174 688,-130 890,-130 890,-174"/>
|
||||
<text text-anchor="start" x="696" y="-163.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-154.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-145.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-136.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view12->viewer5 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>view12->viewer5</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-152C663.9,-152 675.91,-152 687.73,-152"/>
|
||||
</g>
|
||||
<!-- viewerN -->
|
||||
<!-- viewerZ -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,39 @@
|
||||
# Client-only
|
||||
|
||||
<img src="./architecture.sub1.svg" />
|
||||
|
||||
_For static datasets, datasets provided by the user, and simple server-less and
|
||||
read-only web applications._
|
||||
|
||||
In this design, Perspective is run as a client Browser WebAssembly library, the
|
||||
dataset is downloaded entirely to the client and all calculations and UI
|
||||
interactions are performed locally. Interactive performance is very good, using
|
||||
WebAssembly engine for near-native runtime plus WebWorker isolation for parallel
|
||||
rendering within the browser. Operations like scrolling and creating new views
|
||||
are responsive. However, the entire dataset must be downloaded to the client.
|
||||
Perspective is not a typical browser component, and datset sizes of 1gb+ in
|
||||
Apache Arrow format will load fine with good interactive performance!
|
||||
|
||||
Horizontal scaling is a non-issue, since here is no concurrent state to scale,
|
||||
and only uses client-side computation via WebAssembly client. Client-only
|
||||
perspective can support as many concurrent users as can download the web
|
||||
application itself. Once the data is loaded, no server connection is needed and
|
||||
all operations occur in the client browser, imparting no additional runtime cost
|
||||
on the server beyond initial load. This also means updates and edits are local
|
||||
to the browser client and will be lost when the page is refreshed, unless
|
||||
otherwise persisted by your application.
|
||||
|
||||
As the client-only design starts with creating a client-side Perspective
|
||||
`Table`, data can be provided by any standard web service in any Perspective
|
||||
compatible format (JSON, CSV or Apache Arrow).
|
||||
|
||||
## Javascript client
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(csv);
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(table);
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
# Client/Server replicated
|
||||
|
||||
<img src="./architecture.sub2.svg" />
|
||||
|
||||
_For medium-sized, real-time, synchronized and/or editable data sets with many
|
||||
concurrent users._
|
||||
|
||||
The dataset is instantiated in-memory with a Python or Node.js Perspective
|
||||
server, and web applications create duplicates of these tables in a local
|
||||
WebAssembly client in the browser, synchonized efficiently to the server via
|
||||
Apache Arrow. This design scales well with additional concurrent users, as
|
||||
browsers only need to download the initial data set and subsequent update
|
||||
deltas, while operations like scrolling, pivots, sorting, etc. are performed on
|
||||
the client.
|
||||
|
||||
Python servers can make especially good use of additional threads, as
|
||||
Perspective will release the GIL for almost all operations. Interactive
|
||||
performance on the client is very good and identical to client-only
|
||||
architecture. Updates and edits are seamlessly synchonized across clients via
|
||||
their virtual server counterparts using websockets and Apache Arrow.
|
||||
|
||||
## Python and Tornado server
|
||||
|
||||
```python
|
||||
from perspective import Server, PerspectiveTornadoHandler
|
||||
|
||||
server = Server()
|
||||
client = server.new_local_client()
|
||||
client.table(csv, name="my_table")
|
||||
routes = [(
|
||||
r"/websocket",
|
||||
perspective.handlers.tornado.PerspectiveTornadoHandler,
|
||||
{"perspective_server": server},
|
||||
)]
|
||||
|
||||
app = tornado.web.Application(routes)
|
||||
app.listen(8080)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
loop.start()
|
||||
```
|
||||
|
||||
## Javascript client
|
||||
|
||||
Perspective's websocket client interfaces with the Python server, then
|
||||
_replicates_ the server-side Table.
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080");
|
||||
const server_table = await websocket.open_table("my_table");
|
||||
const server_view = await server_table.view();
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const client_table = await worker.table(server_view);
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(client_table);
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Server-only
|
||||
|
||||
<img src="./architecture.sub3.svg" />
|
||||
|
||||
_For extremely large datasets with a small number of concurrent users._
|
||||
|
||||
The dataset is instantiated in-memory with a Python or Node.js server, and web
|
||||
applications connect virtually. Has very good initial load performance, since no
|
||||
data is downloaded. Group-by and other operations will run column-parallel if
|
||||
configured.
|
||||
|
||||
But interactive performance is poor, as every user interaction must page the
|
||||
server to render. Operations like scrolling are not as responsive and can be
|
||||
impacted by network latency. Web applications must be "always connected" to the
|
||||
server via WebSocket. Disconnecting will prevent any interaction, scrolling,
|
||||
etc. of the UI. Does not use WebAssembly.
|
||||
|
||||
Each connected browser will impact server performance as long as the connection
|
||||
is open, which in turn impacts interactive performance of every client. This
|
||||
ultimately limits the horizontal scalabity of this architecture. Since each
|
||||
client reads the perspective `Table` virtually, changes like edits and updates
|
||||
are automatically reflected to all clients and persist across browser refresh.
|
||||
Using the same Python server as the previous design, we can simply skip the
|
||||
intermediate WebAssembly `Table` and pass the virtual table directly to `load()`
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080");
|
||||
const server_table = await websocket.open_table("my_table");
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(server_table);
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
# Join
|
||||
|
||||
`Client::join` creates a read-only `Table` by joining two source tables on a
|
||||
shared key column. The `left` and `right` arguments can be `Table` objects or
|
||||
string table names (as returned by `get_hosted_table_names()`). The resulting
|
||||
table is _reactive_: whenever either source table is updated, the join is
|
||||
automatically recomputed and any `View` derived from the joined table will
|
||||
update accordingly.
|
||||
|
||||
Joined tables support the full `View` API — you can apply `group_by`,
|
||||
`split_by`, `sort`, `filter`, `expressions`, and all other `View` operations on
|
||||
the result, just as you would with any other `Table`.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Join Types
|
||||
|
||||
`Client::join` supports three join types, specified via the `join_type` option.
|
||||
The default is `"inner"`.
|
||||
|
||||
## Inner Join (default)
|
||||
|
||||
An inner join includes only rows where the key column exists in _both_ source
|
||||
tables. Rows from either table that have no match in the other are excluded.
|
||||
|
||||
## Left Join
|
||||
|
||||
A left join includes all rows from the left table. For left rows that have no
|
||||
match in the right table, right-side columns are filled with `null`.
|
||||
|
||||
## Outer Join
|
||||
|
||||
An outer join includes all rows from both tables. Unmatched rows on either side
|
||||
have their missing columns filled with `null`.
|
||||
|
||||
| `join_type` | Left-only rows | Right-only rows |
|
||||
| ----------- | -------------- | --------------- |
|
||||
| `"inner"` | excluded | excluded |
|
||||
| `"left"` | included | excluded |
|
||||
| `"outer"` | included | included |
|
||||
@@ -0,0 +1,35 @@
|
||||
# Join Options
|
||||
|
||||
## `on` — Join Key Column
|
||||
|
||||
The `on` parameter specifies the column name used to match rows between the left
|
||||
and right tables. This column must exist in the left table and, by default, must
|
||||
also exist in the right table with the same name and compatible type.
|
||||
|
||||
The join key column becomes the index of the resulting table.
|
||||
|
||||
## `right_on` — Different Right Key Column
|
||||
|
||||
When the join key has a different name in the right table, use `right_on` to
|
||||
specify the right table's column name. The left table's column name (`on`) is
|
||||
used in the output schema; the right key column is excluded from the result.
|
||||
|
||||
The `on` and `right_on` columns must have compatible types. An error is thrown
|
||||
if the types do not match.
|
||||
|
||||
## `join_type` — Join Type
|
||||
|
||||
Controls which rows are included in the result. See
|
||||
[Join Types](./join_types.md) for details.
|
||||
|
||||
| Value | Behavior |
|
||||
| ----------- | ----------------------------------------------------- |
|
||||
| `"inner"` | Only rows with matching keys in both tables (default) |
|
||||
| `"left"` | All left rows; unmatched right columns are `null` |
|
||||
| `"outer"` | All rows from both tables; unmatched columns are `null` |
|
||||
|
||||
## `name` — Table Name
|
||||
|
||||
An optional name for the resulting joined table. If omitted, a random name is
|
||||
generated. This name is used to identify the table in the server's hosted table
|
||||
registry.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Reactivity and Constraints
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
Joined tables are fully reactive. When either source table receives an
|
||||
`update()`, the join is automatically recomputed and any `View` created from the
|
||||
joined table will reflect the new data. This includes:
|
||||
|
||||
- Updates that modify existing rows in either source table.
|
||||
- New rows added to either source table that create new matches.
|
||||
- Chained joins — if a joined table is itself used as input to another join,
|
||||
updates propagate through the entire chain.
|
||||
|
||||
## Duplicate Keys
|
||||
|
||||
Like SQL, `join()` produces a cross-product for each matching key value. When
|
||||
multiple rows in the left table share the same key, each is paired with every
|
||||
matching row in the right table (and vice versa). The number of output rows for
|
||||
a given key is `left_count × right_count`.
|
||||
|
||||
This behavior depends on whether the source tables are _indexed_:
|
||||
|
||||
- **Unindexed tables** (no `index` option) — rows are appended, so duplicate
|
||||
keys accumulate naturally. Each `update()` appends new rows, which may
|
||||
introduce additional duplicates.
|
||||
- **Indexed tables** (`index` set to the join key) — each key appears at most
|
||||
once per table, so the join produces at most one row per key. Updates replace
|
||||
existing rows in-place rather than appending.
|
||||
|
||||
## Read-Only
|
||||
|
||||
Joined tables are read-only. Calling `update()`, `remove()`, `clear()`, or
|
||||
`replace()` on a joined table will throw an error. Data can only change
|
||||
indirectly, by updating the source tables.
|
||||
|
||||
## Column Name Conflicts
|
||||
|
||||
The left and right tables must not have overlapping column names (other than the
|
||||
join key). If a non-key column name appears in both tables, `join()` throws an
|
||||
error. Rename columns in your source data or use `View` expressions to avoid
|
||||
conflicts.
|
||||
|
||||
## Source Table Deletion
|
||||
|
||||
A source table cannot be deleted while a joined table depends on it. You must
|
||||
delete the joined table first, then delete the source tables.
|
||||
@@ -0,0 +1,77 @@
|
||||
# What is `perspective-python`
|
||||
|
||||
Perspective for Python uses the exact same C++ data engine used by the
|
||||
[WebAssembly version](https://docs.rs/perspective-js/latest/perspective_js/) and
|
||||
[Rust version](https://docs.rs/crate/perspective/latest). The library consists
|
||||
of many of the same abstractions and API as in JavaScript, as well as
|
||||
Python-specific data loading support for [NumPy](https://numpy.org/),
|
||||
[Pandas](https://pandas.pydata.org/) (and
|
||||
[Apache Arrow](https://arrow.apache.org/), as in JavaScript).
|
||||
|
||||
Additionally, `perspective-python` provides a session manager suitable for
|
||||
integration into server systems such as
|
||||
[Tornado websockets](https://www.tornadoweb.org/en/stable/websocket.html),
|
||||
[AIOHTTP](https://docs.aiohttp.org/en/stable/web_quickstart.html#websockets), or
|
||||
[Starlette](https://www.starlette.io/websockets/)/[FastAPI](https://fastapi.tiangolo.com/advanced/websockets/),
|
||||
which allows fully _virtual_ Perspective tables to be interacted with by
|
||||
multiple `<perspective-viewer>` in a web browser. You can also interact with a
|
||||
Perspective table from python clients, and to that end client libraries are
|
||||
implemented for both Tornado and AIOHTTP.
|
||||
|
||||
## Example
|
||||
|
||||
A simple example which loads an [Apache Arrow](https://arrow.apache.org/) and
|
||||
computes a "Group By" operation, returning a new Arrow.
|
||||
|
||||
```python
|
||||
from perspective import Server
|
||||
|
||||
client = Server().new_local_client()
|
||||
table = client.table(arrow_bytes_data)
|
||||
view = table.view(group_by = ["CounterParty", "Security"])
|
||||
arrow = view.to_arrow()
|
||||
```
|
||||
|
||||
[More Examples](https://github.com/perspective-dev/perspective/tree/master/examples)
|
||||
are available on GitHub.
|
||||
|
||||
## What's included
|
||||
|
||||
The `perspective` module exports several tools:
|
||||
|
||||
- `Server` the constructor for a new instance of the Perspective data engine.
|
||||
- The `perspective.widget` module exports `PerspectiveWidget`, the JupyterLab
|
||||
widget for interactive visualization in a notebook cell.
|
||||
- The `perspective.handlers` modules exports web frameworks handlers that
|
||||
interface with a `perspective-client` in JavaScript.
|
||||
- `perspective.handlers.tornado.PerspectiveTornadoHandler` for
|
||||
[Tornado](https://www.tornadoweb.org/)
|
||||
- `perspective.handlers.starlette.PerspectiveStarletteHandler` for
|
||||
[Starlette](https://www.starlette.io/) and
|
||||
[FastAPI](https://fastapi.tiangolo.com)
|
||||
- `perspective.handlers.aiohttp.PerspectiveAIOHTTPHandler` for
|
||||
[AIOHTTP](https://docs.aiohttp.org),
|
||||
|
||||
### Virtual UI server
|
||||
|
||||
As `<perspective-viewer>` or any other Perspective `Client` will only consume
|
||||
the data necessary to render the current screen (or whatever else was requested
|
||||
via the API), this runtime mode allows large datasets without the need to copy
|
||||
them entirely to the Browser, at the expense of network latency on UI
|
||||
interaction/API calls.
|
||||
|
||||
### Jupyterlab
|
||||
|
||||
`PerspectiveWidget` is a JupyterLab widget that implements the same API as
|
||||
`<perspective-viewer>`, allows running such a viewer in
|
||||
[JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) in either server or
|
||||
client (via WebAssembly) mode. `PerspectiveWidget` is compatible with Jupyterlab
|
||||
3 and Jupyter Notebook 6 via a
|
||||
[prebuilt extension](https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#prebuilt-extensions).
|
||||
To use it, simply install `perspective-python` and the extensions should be
|
||||
available.
|
||||
|
||||
`perspective-python`'s JupyterLab extension also provides convenient builtin
|
||||
viewers for `csv`, `json`, or `arrow` files. Simply right-click on a file with
|
||||
this extension and choose the appropriate `Perpective` option from the context
|
||||
menu.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Table
|
||||
|
||||
`Table` is Perspective's columnar data frame, analogous to a Pandas `DataFrame`
|
||||
or Apache Arrow, supporting append & in-place updates, removal by index, and
|
||||
update notifications.
|
||||
|
||||
A `Table` contains columns, each of which have a unique name, are strongly and
|
||||
consistently typed, and contains rows of data conforming to the column's type.
|
||||
Each column in a `Table` must have the same number of rows, though not every row
|
||||
must contain data; null-values are used to indicate missing values in the
|
||||
dataset. The schema of a `Table` is _immutable after creation_, which means the
|
||||
column names and data types cannot be changed after the `Table` has been
|
||||
created. Columns cannot be added or deleted after creation either, but a `View`
|
||||
can be used to select an arbitrary set of columns from the `Table`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# `Table::clear` and `Table::replace`
|
||||
|
||||
Calling `Table::clear` will remove all data from the underlying `Table`. Calling
|
||||
`Table::replace` with new data will clear the `Table`, and update it with a new
|
||||
dataset that conforms to Perspective's data types and the existing schema on the
|
||||
`Table`.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
table.clear();
|
||||
table.replace(json);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table.clear()
|
||||
table.replace(df)
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
# Construct a Table
|
||||
|
||||
Examples of constructing an empty `Table` from a schema.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
var schema = {
|
||||
x: "integer",
|
||||
y: "string",
|
||||
z: "boolean",
|
||||
};
|
||||
|
||||
const table2 = await worker.table(schema);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python:
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
|
||||
schema = {
|
||||
"x": "integer",
|
||||
"y": "string",
|
||||
"z": "boolean",
|
||||
}
|
||||
|
||||
table2 = perspective.table(schema)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
Rust:
|
||||
|
||||
```rust
|
||||
let data = TableData::Schema(vec![(" a".to_string(), ColumnType::FLOAT)]);
|
||||
let options = TableInitOptions::default();
|
||||
let table = client.table(data.into(), options).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
# Loading data
|
||||
|
||||
A `Table` may also be created-or-updated by data in CSV,
|
||||
[Apache Arrow](https://arrow.apache.org/), JSON row-oriented or JSON
|
||||
column-oriented formats. In addition to these, `perspective-python` additionally
|
||||
supports `pyarrow.Table`, `polars.DataFrame` and `pandas.DataFrame` objects
|
||||
directly. These formats are otherwise identical to the built-in formats and
|
||||
don't exhibit any additional support or type-awareness; e.g., `pandas.DataFrame`
|
||||
support is _just_ `pyarrow.Table.from_pandas` piped into Perspective's Arrow
|
||||
reader.
|
||||
|
||||
`Client::table` and `Table::update` perform _coercion_ on their input for all
|
||||
input formats _except_ Arrow (which comes with its own schema and has no need
|
||||
for coercion). `"date"` and `"datetime"` column types do not have native JSON
|
||||
representations, so these column types _cannot_ be inferred from JSON input.
|
||||
Instead, for columns of these types for JSON input, a `Table` must first be
|
||||
constructed with a _schema_. Next, call `Table::update` with the JSON input -
|
||||
Perspective's JSON reader may _coerce_ a `date` or `datetime` from these native
|
||||
JSON types:
|
||||
|
||||
- `integer` as milliseconds-since-epoch.
|
||||
- `string` as a any of Perspective's built-in date format formats.
|
||||
- JavaScript `Date` and Python `datetime.date` and `datetime.datetime` are _not_
|
||||
supported directly. However, in JavaScript `Date` types are automatically
|
||||
coerced to correct `integer` timestamps by default when converted to JSON.
|
||||
|
||||
## Apache Arrow
|
||||
|
||||
The most efficient way to load data into Perspective, encoded as
|
||||
[Apache Arrow IPC format](https://arrow.apache.org/docs/python/ipc.html). In
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const resp = await fetch(
|
||||
"https://cdn.jsdelivr.net/npm/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
const arrow = await resp.arrayBuffer();
|
||||
```
|
||||
|
||||
Apache Arrow input do not support type coercion, preferring Arrow's internal
|
||||
self-describing schema.
|
||||
|
||||
## CSV
|
||||
|
||||
Perspective relies on Apache Arrow's CSV parser, and as such uses mostly the
|
||||
same column-type inference logic as Arrow itself would use for parsing CSV.
|
||||
|
||||
## Row Oriented JSON
|
||||
|
||||
Row-oriented JSON is in the form of a list of objects. Each object in the list
|
||||
corresponds to a row in the table. For example:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "a": 86, "b": false, "c": "words" },
|
||||
{ "a": 0, "b": true, "c": "" },
|
||||
{ "a": 12345, "b": false, "c": "here" }
|
||||
]
|
||||
```
|
||||
|
||||
## Column Oriented JSON
|
||||
|
||||
Column-Oriented JSON comes in the form of an object of lists. Each key of the
|
||||
object is a column name, and each element of the list is the corresponding value
|
||||
in the row.
|
||||
|
||||
```json
|
||||
{
|
||||
"a": [86, 0, 12345],
|
||||
"b": [false, true, false],
|
||||
"c": ["words", "", "here"]
|
||||
}
|
||||
```
|
||||
|
||||
## NDJSON
|
||||
|
||||
[NDJSON](https://github.com/ndjson/ndjson-spec) (sometimes also referred to as
|
||||
JSONL) is a streaming-friendly format where each line is a valid JSON object,
|
||||
separated by newlines. It is commonly used in data streaming and messaging
|
||||
queues.
|
||||
|
||||
```json
|
||||
{ "a": 86, "b": false, "c": "words" }
|
||||
{ "a": 0, "b": true, "c": "" }
|
||||
{ "a": 12345, "b": false, "c": "here" }
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
## Index and Limit
|
||||
|
||||
<div class="warning">`limit` cannot be used in conjunction with `index`.</div>
|
||||
|
||||
Initializing a `Table` with an `index` tells Perspective to treat a column as
|
||||
the primary key, allowing in-place updates of rows. Only a single column (of any
|
||||
type) can be used as an `index`. Indexed `Table` instances allow:
|
||||
|
||||
- In-place _updates_ whenever a new row shares an `index` values with an
|
||||
existing row
|
||||
- _Partial updates_ when a data batch omits some column.
|
||||
- _Removes_ to delete a row by `index`.
|
||||
|
||||
To create an indexed `Table`, provide the `index` property with a string column
|
||||
name to be used as an index:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const indexed_table = await perspective.table(data, { index: "a" });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python
|
||||
|
||||
```python
|
||||
indexed_table = perspective.Table(data, index="a");
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Initializing a `Table` with a `limit` sets the total number of rows the `Table`
|
||||
is allowed to have. When the `Table` is updated, and the resulting size of the
|
||||
`Table` would exceed its `limit`, rows that exceed `limit` overwrite the oldest
|
||||
rows in the `Table`. To create a `Table` with a `limit`, provide the `limit`
|
||||
property with an integer indicating the maximum rows:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const limit_table = await perspective.table(data, { limit: 1000 });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python:
|
||||
|
||||
```python
|
||||
limit_table = perspective.Table(data, limit=1000);
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
# Schema and column types
|
||||
|
||||
The mapping of a `Table`'s column names to data types is referred to as a
|
||||
`schema`. Each column has a unique name and a single data type, one of
|
||||
|
||||
- `float`
|
||||
- `integer`
|
||||
- `boolean`
|
||||
- `date`
|
||||
- `datetime`
|
||||
- `string`
|
||||
|
||||
A `Table` schema is fixed at construction, either by explicitly passing a schema
|
||||
dictionary to the `Client::table` method, or by passing _data_ to this method
|
||||
from which the schema is _inferred_ (if CSV or JSON format) or inherited (if
|
||||
Arrow).
|
||||
|
||||
## Type inference
|
||||
|
||||
When passing CSV or JSON data to the `Client::table` constructor, the type of
|
||||
each column is inferred automatically. In some cases, the inference algorithm
|
||||
may not return exactly what you'd like. For example, a column may be interpreted
|
||||
as a `datetime` when you intended it to be a `string`, or a column may have no
|
||||
values at all (yet), as it will be updated with values from a real-time data
|
||||
source later on. In these cases, create a `table()` with a _schema_.
|
||||
|
||||
Once the `Table` has been created, further `Table::update` calls will perform
|
||||
limited type _coercion_ based on the schema. While _coercion_ works similarly to
|
||||
_inference_, in that input data may be parsed based on the expected column type,
|
||||
`Table::update` will not _change_ the column's type further. For example, a
|
||||
number literal `1234` would be _inferred_ as an `"integer"`, but _in the context
|
||||
of an `Table::update` call on a known `"string"` column_, this will be parsed as
|
||||
the _string_ `"1234"`.
|
||||
|
||||
## `date` and `datetime` inference
|
||||
|
||||
Various string representations of `date` and `datetime` format columns can be
|
||||
_inferred_ as well _coerced_ from strings if they match one of Perspective's
|
||||
internal known datetime parsing formats, for example
|
||||
[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) (which is also the format
|
||||
Perspective will _output_ these types for CSV).
|
||||
@@ -0,0 +1,88 @@
|
||||
# `Table::update` and `Table::remove`
|
||||
|
||||
Once a `Table` has been created, it can be updated with new data conforming to
|
||||
the `Table`'s schema. `Table::update` supports the same data formats as
|
||||
`Client::table`, minus _schema_.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const schema = {
|
||||
a: "integer",
|
||||
b: "float",
|
||||
};
|
||||
|
||||
const table = await perspective.table(schema);
|
||||
table.update(new_data);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
schema = {"a": "integer", "b": "float"}
|
||||
|
||||
table = perspective.Table(schema)
|
||||
table.update(new_data)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Without an `index` set, calls to `update()` _append_ new data to the end of the
|
||||
`Table`. Otherwise, Perspective allows
|
||||
[_partial updates_ (in-place)](#index-and-limit) using the `index` to determine
|
||||
which rows to update:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
indexed_table.update({ id: [1, 4], name: ["x", "y"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
indexed_table.update({"id": [1, 4], "name": ["x", "y"]})
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Any value on a `Client::table` can be unset using the value `null` in JSON or
|
||||
Arrow input formats. Values may be unset on construction, as any `null` in the
|
||||
dataset will be treated as an unset value. `Table::update` calls do not need to
|
||||
provide _all columns_ in the `Table`'s schema; missing columns will be omitted
|
||||
from the `Table`'s updated rows.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
table.update([{ x: 3, y: null }]); // `z` missing
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table.update([{"x": 3, "y": None}]) # `z` missing
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Rows can also be removed from an indexed `Table`, by calling `Table::remove`
|
||||
with an array of index values:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
indexed_table.remove([1, 4]);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
indexed_table.remove([1, 4])
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
# View
|
||||
|
||||
The [`View`] struct is Perspective's query and serialization interface. It
|
||||
represents a query on the `Table`'s dataset and is always created from an
|
||||
existing `Table` instance via the [`Table::view`] method.
|
||||
|
||||
[`View`]s are immutable with respect to the arguments provided to the
|
||||
[`Table::view`] method; to change these parameters, you must create a new
|
||||
[`View`] on the same [`Table`]. However, each [`View`] is _live_ with respect to
|
||||
the [`Table`]'s data, and will (within a conflation window) update with the
|
||||
latest state as its parent [`Table`] updates, including incrementally
|
||||
recalculating all aggregates, pivots, filters, etc. [`View`] query parameters
|
||||
are composable, in that each parameter works independently _and_ in conjunction
|
||||
with each other, and there is no limit to the number of pivots, filters, etc.
|
||||
which can be applied.
|
||||
|
||||
<div class="javascript">
|
||||
<div class="warning">
|
||||
The examples in this module are in JavaScript. See <a href="https://docs.rs/crate/perspective/latest"><code>perspective</code></a> docs for the Rust API.
|
||||
</div>
|
||||
</div>
|
||||
<div class="python">
|
||||
<div class="warning">
|
||||
The examples in this module are in Python. See <a href="https://docs.rs/crate/perspective/latest"><code>perspective</code></a> docs for the Rust API.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
# Examples
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const table = await perspective.table({
|
||||
id: [1, 2, 3, 4],
|
||||
name: ["a", "b", "c", "d"],
|
||||
});
|
||||
|
||||
const view = await table.view({ columns: ["name"] });
|
||||
const json = await view.to_json();
|
||||
await view.delete();
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table = perspective.Table({
|
||||
"id": [1, 2, 3, 4],
|
||||
"name": ["a", "b", "c", "d"]
|
||||
});
|
||||
|
||||
view = table.view(columns=["name"])
|
||||
arrow = view.to_arrow()
|
||||
view.delete()
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let opts = TableInitOptions::default();
|
||||
let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
let table = client.table(data, opts).await?;
|
||||
|
||||
let view = table.view(None).await?;
|
||||
let arrow = view.to_arrow().await?;
|
||||
view.delete().await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,231 @@
|
||||
# Advanced View Operations
|
||||
|
||||
Beyond the standard query configuration, `View` provides additional methods for
|
||||
interacting with hierarchical results and introspecting data.
|
||||
|
||||
## Tree Hierarchy Operations
|
||||
|
||||
When a `View` has `group_by` applied, the results form a tree hierarchy.
|
||||
Perspective provides methods to control which levels of the tree are expanded or
|
||||
collapsed:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["Region", "Country", "City"] });
|
||||
|
||||
// Collapse the tree at row index 5
|
||||
await view.collapse(5);
|
||||
|
||||
// Expand the tree at row index 5
|
||||
await view.expand(5);
|
||||
|
||||
// Set the expansion depth (0 = fully collapsed, 1 = first level, etc.)
|
||||
await view.set_depth(1);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Using the sync API
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["Region", "Country", "City"])
|
||||
|
||||
view.collapse(5)
|
||||
view.expand(5)
|
||||
view.set_depth(1)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
group_by: Some(vec!["Region".into(), "Country".into(), "City".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
|
||||
view.collapse(5).await?;
|
||||
view.expand(5).await?;
|
||||
view.set_depth(1).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<span class="warning">Perspective's built-in engine is lazy — aggregates for
|
||||
collapsed rows are not recalculated when the underlying `Table` is updated.
|
||||
Updates are only computed for rows that are currently visible (expanded). When a
|
||||
collapsed row is later expanded, its aggregates are calculated at that
|
||||
point.</span>
|
||||
|
||||
## Column Range Queries
|
||||
|
||||
`View::get_min_max` returns the minimum and maximum values for a given column,
|
||||
which is useful for setting up scales in custom visualizations:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const [min, max] = await view.get_min_max("Sales");
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
min_val, max_val = view.get_min_max("Sales")
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Expression Validation
|
||||
|
||||
Before creating a `View` with expressions, you can validate them against the
|
||||
table's schema using `Table::validate_expressions`. This returns information
|
||||
about which expressions are valid and their inferred types:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const result = await table.validate_expressions({
|
||||
expr1: '"Sales" + "Profit"',
|
||||
expr2: "invalid_column + 1",
|
||||
});
|
||||
// result.expression_schema contains valid expressions and their types
|
||||
// result.errors contains invalid expressions and error messages
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
result = table.validate_expressions(['"Sales" + "Profit"', 'invalid + 1'])
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## View Dimensions
|
||||
|
||||
`View::dimensions` returns the number of rows and columns in the current view,
|
||||
including information about group-by header rows:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const dims = await view.dimensions();
|
||||
// { num_view_rows, num_view_columns, num_table_rows, num_table_columns, ... }
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
dims = view.dimensions()
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## View Configuration Introspection
|
||||
|
||||
`View::get_config` returns the full configuration used to create the view:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const config = await view.get_config();
|
||||
// { group_by: [...], split_by: [...], sort: [...], filter: [...], ... }
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
config = view.get_config()
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Update Callbacks
|
||||
|
||||
Register a callback to be notified whenever the underlying `Table` is updated
|
||||
and the `View` has been recalculated:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
view.on_update(
|
||||
(updated) => {
|
||||
console.log("View updated", updated.port_id);
|
||||
},
|
||||
{ mode: "row" },
|
||||
);
|
||||
|
||||
// Later, remove the callback
|
||||
view.remove_update(callback);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
def on_update(port_id, delta):
|
||||
print("View updated", port_id)
|
||||
|
||||
view.on_update(on_update, mode="row")
|
||||
view.remove_update(on_update)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
When `mode` is set to `"row"`, the callback receives a delta of only the rows
|
||||
that changed (as Apache Arrow), which is useful for efficiently synchronizing
|
||||
tables across clients.
|
||||
|
||||
## Flattening a View into a Table
|
||||
|
||||
In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
|
||||
which will return a new [`Table`] based on the [`Table::view`]'s dataset, and
|
||||
all future updates that affect the [`Table::view`] will be forwarded to the new
|
||||
[`Table`]. This is particularly useful for implementing a
|
||||
[Client/Server Replicated](server.md#clientserver-replicated) design, by
|
||||
serializing the `View` to an arrow and setting up an `on_update` callback.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const worker1 = perspective.worker();
|
||||
const table = await worker.table(data);
|
||||
const view = await table.view({ filter: [["State", "==", "Texas"]] });
|
||||
const table2 = await worker.table(view);
|
||||
table.update([{ State: "Texas", City: "Austin" }]);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table = perspective.Table(data);
|
||||
view = table.view(filter=[["State", "==", "Texas"]])
|
||||
table2 = perspective.Table(view.to_arrow());
|
||||
|
||||
def updater(port, delta):
|
||||
table2.update(delta)
|
||||
|
||||
view.on_update(updater, mode="Row")
|
||||
table.update([{"State": "Texas", "City": "Austin"}])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let opts = TableInitOptions::default();
|
||||
let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
let table = client.table(data, opts).await?;
|
||||
let view = table.view(None).await?;
|
||||
let table2 = client.table(TableData::View(view)).await?;
|
||||
table.update(data).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
# Expressions
|
||||
|
||||
The `expressions` property specifies _new_ columns in Perspective that are
|
||||
created using existing column values or arbitrary scalar values defined within
|
||||
the expression. In `<perspective-viewer>`, expressions are added using the "New
|
||||
Column" button in the side panel.
|
||||
|
||||
Expressions are strings parsed by Perspective's expression engine (based on
|
||||
[ExprTK](https://github.com/ArashPartow/exprtk)). Column names are referenced by
|
||||
wrapping them in double quotes, e.g. `"Sales"`:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
expressions: {
|
||||
"Profit Ratio": '"Profit" / "Sales"',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(expressions={'Profit Ratio': '"Profit" / "Sales"'})
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
expressions: Some(Expressions([
|
||||
("Profit Ratio", "\"Profit\" / \"Sales\"".into())
|
||||
].into_iter().collect())),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Type Conversion and Coercion
|
||||
|
||||
Perspective expressions are strongly typed — each column and literal has a fixed
|
||||
type, and most operators require matching types on both sides. To work across
|
||||
types, use the conversion functions:
|
||||
|
||||
| Function | Description |
|
||||
| --------------- | ------------------------------------------------------------ |
|
||||
| `to_string(x)` | Convert any type to string |
|
||||
| `to_integer(x)` | Convert to integer (null if not parsable) |
|
||||
| `to_float(x)` | Convert to float (null if not parsable) |
|
||||
| `to_boolean(x)` | Convert to boolean (truthy/falsy) |
|
||||
| `integer(x)` | Alias for `to_integer(x)` |
|
||||
| `float(x)` | Alias for `to_float(x)` |
|
||||
| `datetime(x)` | Construct a datetime from a POSIX timestamp (ms since epoch) |
|
||||
| `date(y, m, d)` | Construct a date from year, month, day |
|
||||
|
||||
### How coercion works
|
||||
|
||||
Perspective does not implicitly coerce types. For example, you cannot directly
|
||||
add an `integer` to a `float` — you must cast one side explicitly. Similarly,
|
||||
`datetime` and `date` values are not numeric: to perform arithmetic on them, you
|
||||
must first convert to a numeric representation, do the math, then convert back.
|
||||
|
||||
Internally, `datetime` values are stored as milliseconds since the Unix epoch
|
||||
(1970-01-01T00:00:00Z). Converting a `datetime` to a `float` yields this
|
||||
millisecond timestamp, and `datetime()` accepts a millisecond timestamp to
|
||||
produce a `datetime`.
|
||||
|
||||
### Example: offsetting a datetime by 7 days
|
||||
|
||||
This expression takes a `"Shipped Date"` column, converts it to its
|
||||
millisecond-epoch representation, adds 7 days worth of milliseconds (7 ×
|
||||
24 × 60 × 60 × 1000 = 604800000), and converts the result back
|
||||
to a `datetime`:
|
||||
|
||||
```
|
||||
// Due Date
|
||||
datetime(float("Shipped Date") + 604800000)
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
Standard arithmetic and comparison operators are supported:
|
||||
|
||||
| Operator | Description |
|
||||
| -------------------------------- | ----------- |
|
||||
| `+`, `-`, `*`, `/` | Arithmetic |
|
||||
| `%` | Modulo |
|
||||
| `==`, `!=`, `<`, `>`, `<=`, `>=` | Comparison |
|
||||
| `and`, `or`, `not` | Logical |
|
||||
| `if ... else ...` | Conditional |
|
||||
|
||||
## Numeric Functions
|
||||
|
||||
ExprTK provides a rich set of built-in numeric functions including `abs`,
|
||||
`ceil`, `floor`, `round`, `exp`, `log`, `log10`, `sqrt`, `min`, `max`, `pow`,
|
||||
`clamp`, `iclamp`, `inrange`, and trigonometric functions (`sin`, `cos`, `tan`,
|
||||
`asin`, `acos`, `atan`).
|
||||
|
||||
## String Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------------- | ------------------------------------------------------- |
|
||||
| `concat(a, b, ...)` | Concatenate strings |
|
||||
| `upper(s)` | Convert to uppercase |
|
||||
| `lower(s)` | Convert to lowercase |
|
||||
| `length(s)` | String length |
|
||||
| `contains(s, substr)` | Whether `s` contains `substr` |
|
||||
| `order(col, 'B', 'C', 'A')` | Custom sort order for a string column |
|
||||
| `match(s, pattern)` | Regex partial match (returns boolean) |
|
||||
| `match_all(s, pattern)` | Regex full match (returns boolean) |
|
||||
| `search(s, pattern)` | First capturing group match |
|
||||
| `indexof(s, pattern)` | Start index of first regex match |
|
||||
| `substring(s, start, end)` | Substring from `start` (inclusive) to `end` (exclusive) |
|
||||
| `replace(s, repl, pattern)` | Replace first regex match |
|
||||
| `replace_all(s, repl, pattern)` | Replace all regex matches |
|
||||
|
||||
## Date/Datetime Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `today()` | Current date |
|
||||
| `now()` | Current datetime |
|
||||
| `date(year, month, day)` | Construct a date |
|
||||
| `datetime(timestamp_ms)` | Construct a datetime from a POSIX timestamp (ms since epoch) |
|
||||
| `hour_of_day(dt)` | Hour component (0-23) |
|
||||
| `day_of_week(dt)` | Day of the week as a string |
|
||||
| `month_of_year(dt)` | Month of the year as a string |
|
||||
| `bucket(dt, unit)` | Bucket datetime by unit: `'s'`, `'m'`, `'h'`, `'D'`, `'W'`, `'M'`, `'Y'` |
|
||||
|
||||
`bucket` also works on numeric columns: `bucket("Price", 10)` rounds values down
|
||||
to the nearest multiple of 10.
|
||||
|
||||
## Other Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------- | ----------------------------------------------------- |
|
||||
| `is_null(x)` | Whether the value is null |
|
||||
| `is_not_null(x)` | Whether the value is not null |
|
||||
| `percent_of(a, b)` | `a` as a percentage of `b` |
|
||||
| `inrange(low, val, high)` | Whether `val` is between `low` and `high` (inclusive) |
|
||||
| `min(a, b, ...)` | Minimum of inputs |
|
||||
| `max(a, b, ...)` | Maximum of inputs |
|
||||
| `random()` | Random float between 0.0 and 1.0 |
|
||||
| `col(name)` | Look up a column by string name at runtime |
|
||||
| `vlookup(col, key)` | Look up a value in another column by row key |
|
||||
@@ -0,0 +1,147 @@
|
||||
# Grouping and Pivots
|
||||
|
||||
## Group By
|
||||
|
||||
A group by _groups_ the dataset by the unique values of each column used as a
|
||||
group by - a close analogue in SQL to the `GROUP BY` statement. The underlying
|
||||
dataset is aggregated to show the values belonging to each group, and a total
|
||||
row is calculated for each group, showing the currently selected aggregated
|
||||
value (e.g. `sum`) of the column. Group by are useful for hierarchies,
|
||||
categorizing data and attributing values, i.e. showing the number of units sold
|
||||
based on State and City. In Perspective, group by are represented as an array of
|
||||
string column names to pivot, are applied in the order provided; For example, a
|
||||
group by of `["State", "City", "Postal Code"]` shows the values for each Postal
|
||||
Code, which are grouped by City, which are in turn grouped by State.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["a", "c"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["a", "c"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
group_by: Some(vec!["a".into(), "c".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Split By
|
||||
|
||||
A split by _splits_ the dataset by the unique values of each column used as a
|
||||
split by. The underlying dataset is not aggregated, and a new column is created
|
||||
for each unique value of the split by. Each newly created column contains the
|
||||
parts of the dataset that correspond to the column header, i.e. a `View` that
|
||||
has `["State"]` as its split by will have a new column for each state. In
|
||||
Perspective, Split By are represented as an array of string column names to
|
||||
pivot:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ split_by: ["a", "c"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(split_by=["a", "c"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
split_by: Some(vec!["a".into(), "c".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Aggregates
|
||||
|
||||
Aggregates perform a calculation over an entire column, and are displayed when
|
||||
one or more [Group By](#group-by) are applied to the `View`. Aggregates can be
|
||||
specified by the user, or Perspective will use the following sensible default
|
||||
aggregates based on column type:
|
||||
|
||||
- "sum" for `integer` and `float` columns
|
||||
- "count" for all other columns
|
||||
|
||||
Perspective provides a selection of aggregate functions that can be applied to
|
||||
columns in the `View` constructor using a dictionary of column name to aggregate
|
||||
function name.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
aggregates: {
|
||||
a: "avg",
|
||||
b: "distinct count",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(
|
||||
aggregates={
|
||||
"a": "avg",
|
||||
"b": "distinct count"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
aggregates: Some(HashMap::from([
|
||||
("a".into(), "avg".into()),
|
||||
("b".into(), "distinct count".into()),
|
||||
])),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available aggregate functions depend on the column type:
|
||||
|
||||
**Numeric columns** (`integer`, `float`): `sum`, `abs sum`, `sum abs`,
|
||||
`sum not null`, `any`, `avg`, `mean`, `count`, `distinct count`, `dominant`,
|
||||
`first`, `last`, `last by index`, `high`, `low`, `max`, `min`,
|
||||
`high minus low`, `last minus first`, `median`, `q1`, `q3`,
|
||||
`pct sum parent`, `pct sum total`, `stddev`, `var`, `unique`,
|
||||
`weighted mean`, `min by`, `max by`.
|
||||
|
||||
**String columns**: `count`, `any`, `distinct count`, `dominant`, `first`,
|
||||
`last`, `last by index`, `join`, `median`, `q1`, `q3`, `unique`, `min by`,
|
||||
`max by`.
|
||||
|
||||
**Date/Datetime columns**: `count`, `any`, `avg`, `distinct count`, `dominant`,
|
||||
`first`, `last`, `last by index`, `high`, `low`, `max`, `min`, `median`,
|
||||
`q1`, `q3`, `unique`.
|
||||
|
||||
**Boolean columns**: `count`, `any`, `distinct count`, `dominant`, `first`,
|
||||
`last`, `last by index`, `unique`.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Selection and Ordering
|
||||
|
||||
## Columns
|
||||
|
||||
The `columns` property specifies which columns should be included in the
|
||||
`View`'s output. This allows users to show or hide a specific subset of columns,
|
||||
as well as control the order in which columns appear to the user. This is
|
||||
represented in Perspective as an array of string column names:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
columns: ["a"],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(columns=["a"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
columns: Some(vec![Some("a".into())]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Sort
|
||||
|
||||
The `sort` property specifies columns on which the query should be sorted,
|
||||
analogous to `ORDER BY` in SQL. A column can be sorted regardless of its data
|
||||
type, and sorts can be applied in ascending or descending order. Perspective
|
||||
represents `sort` as an array of arrays, with the values of each inner array
|
||||
being a string column name and a string sort direction. When `split_by` are
|
||||
applied, the additional sort directions `"col asc"` and `"col desc"` will
|
||||
determine the order of pivot column groups.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
sort: [["a", "asc"]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(sort=[["a", "asc"]])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
sort: Some(vec![Sort("a".into(), SortDir::Asc)]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available sort directions are:
|
||||
|
||||
| Direction | Description |
|
||||
|---|---|
|
||||
| `"asc"` | Ascending order |
|
||||
| `"desc"` | Descending order |
|
||||
| `"asc abs"` | Ascending by absolute value |
|
||||
| `"desc abs"` | Descending by absolute value |
|
||||
| `"col asc"` | Ascending order for pivot column groups (requires `split_by`) |
|
||||
| `"col desc"` | Descending order for pivot column groups (requires `split_by`) |
|
||||
| `"col asc abs"` | Ascending by absolute value for pivot column groups |
|
||||
| `"col desc abs"` | Descending by absolute value for pivot column groups |
|
||||
|
||||
## Filter
|
||||
|
||||
The `filter` property specifies columns on which the query can be filtered,
|
||||
returning rows that pass the specified filter condition. This is analogous to
|
||||
the `WHERE` clause in SQL. There is no limit on the number of columns where
|
||||
`filter` is applied, but the resulting dataset is one that passes all the filter
|
||||
conditions, i.e. the filters are joined with an `AND` condition. The join
|
||||
condition can be changed to `OR` via the `filter_op` property.
|
||||
|
||||
Perspective represents `filter` as an array of arrays, with the values of each
|
||||
inner array being a string column name, a string filter operator, and a filter
|
||||
operand in the type of the column:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
filter: [["a", "<", 100]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(filter=[["a", "<", 100]])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
filter: Some(vec![Filter::new("a", "<", FilterTerm::Scalar(Scalar::Float(100.0)))]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available filter operators depend on the column type:
|
||||
|
||||
**String columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `begins with`,
|
||||
`contains`, `ends with`, `in`, `not in`, `is not null`, `is null`.
|
||||
|
||||
**Numeric columns** (`integer`, `float`): `==`, `!=`, `>`, `>=`, `<`, `<=`,
|
||||
`is not null`, `is null`.
|
||||
|
||||
**Boolean columns**: `==`, `is not null`, `is null`.
|
||||
|
||||
**Date/Datetime columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `is not null`,
|
||||
`is null`.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Querying data
|
||||
|
||||
To query the table, create a [`Table::view`] on the table instance with an
|
||||
optional configuration object. A [`Table`] can have as many [`View`]s associated
|
||||
with it as you need - Perspective conserves memory by relying on a single
|
||||
[`Table`] to power multiple [`View`]s concurrently:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
columns: ["Sales"],
|
||||
aggregates: { Sales: "sum" },
|
||||
group_by: ["Region", "Country"],
|
||||
filter: [["Category", "in", ["Furniture", "Technology"]]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(
|
||||
columns=["Sales"],
|
||||
aggregates={"Sales": "sum"},
|
||||
group_by=["Region", "Country"],
|
||||
filter=[["Category", "in", ["Furniture", "Technology"]]]
|
||||
)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
use crate::config::*;
|
||||
let view = table
|
||||
.view(Some(ViewConfigUpdate {
|
||||
columns: Some(vec![Some("Sales".into())]),
|
||||
aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
|
||||
group_by: Some(vec!["Region".into(), "Country".into()]),
|
||||
filter: Some(vec![Filter::new("Category", "in", &[
|
||||
"Furniture",
|
||||
"Technology",
|
||||
])]),
|
||||
..ViewConfigUpdate::default()
|
||||
}))
|
||||
.await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,83 @@
|
||||
# Virtual Servers
|
||||
|
||||
A Virtual Server allows Perspective to query external data sources (such as
|
||||
DuckDB or ClickHouse) without loading the entire dataset into Perspective's
|
||||
built-in data engine. Instead, Perspective translates its query operations
|
||||
(group by, sort, filter, etc.) into queries the external data source can execute
|
||||
natively, and only transfers the data needed for the current view.
|
||||
|
||||
The Virtual Server API works on any platform that has a Perspective Client —
|
||||
including JavaScript (both Node.js and the browser via WebAssembly), Python, and
|
||||
Rust. In the browser, this means a virtual server can front a WASM-based engine
|
||||
like `@duckdb/duckdb-wasm`, giving `<perspective-viewer>` the ability to query a
|
||||
database running entirely client-side without loading data into Perspective's
|
||||
own engine.
|
||||
|
||||
This is useful when:
|
||||
|
||||
- The dataset is too large to fit in browser memory or a single process.
|
||||
- Data already lives in a database and you want to avoid duplicating it.
|
||||
- You want to leverage a database's native query optimizations.
|
||||
- A WASM build of the data source is available in the browser (e.g.
|
||||
`@duckdb/duckdb-wasm`) and you want to query it directly.
|
||||
|
||||
## How it works
|
||||
|
||||
A virtual server implements a handler interface that Perspective calls to
|
||||
satisfy `Table` and `View` operations. The handler translates Perspective's view
|
||||
configuration into the external system's query language (typically SQL),
|
||||
executes the query, and returns the results as columnar data. Because the
|
||||
handler speaks the standard Perspective Client protocol, it can run anywhere a
|
||||
Client can — in-process, in a WebWorker, or on a remote server.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ <perspective-viewer> │
|
||||
└──┬───────────────────────────────────────────────┘
|
||||
│ ┌──────────────────────────────────────────────────┐
|
||||
└──►│ Perspective Virtual Server Handler │
|
||||
└──┬───────────────────────────────────────────────┘
|
||||
│ ┌──────────────────────────────────────────────────┐
|
||||
└──►│ External DB (DuckDB, ClickHouse, …). │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The viewer communicates with the virtual server handler the same way it would
|
||||
with a regular Perspective server. The handler advertises its capabilities
|
||||
(which operations it supports) via a _features_ object, and the viewer UI adapts
|
||||
accordingly — disabling controls for unsupported operations.
|
||||
|
||||
## Built-in implementations
|
||||
|
||||
Perspective ships with virtual server implementations for:
|
||||
|
||||
- **DuckDB** — query DuckDB databases in-browser via WASM
|
||||
([JavaScript](../how_to/javascript/virtual_server/duckdb.md)) or server-side
|
||||
([Python](../how_to/python/virtual_server/duckdb.md)).
|
||||
- **ClickHouse** — query a ClickHouse server from the browser
|
||||
([JavaScript](../how_to/javascript/virtual_server/clickhouse.md)) or from
|
||||
Python ([Python](../how_to/python/virtual_server/clickhouse.md)).
|
||||
|
||||
## Custom implementations
|
||||
|
||||
You can implement your own virtual server to connect Perspective to any data
|
||||
source. See the language-specific guides:
|
||||
|
||||
- [JavaScript: Implementing a custom Virtual Server](../how_to/javascript/virtual_server/custom.md)
|
||||
- [Python: Implementing a custom Virtual Server](../how_to/python/virtual_server/custom.md)
|
||||
|
||||
## Features declaration
|
||||
|
||||
The `get_features()` / `getFeatures()` method returns an object that tells
|
||||
Perspective which query operations the virtual server supports. The viewer will
|
||||
only show controls for supported operations:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | ------ | ----------------------------------------------------------- |
|
||||
| `group_by` | `bool` | Whether group-by aggregation is supported |
|
||||
| `split_by` | `bool` | Whether split-by (pivot) is supported |
|
||||
| `sort` | `bool` | Whether sorting is supported |
|
||||
| `expressions` | `bool` | Whether computed expressions are supported |
|
||||
| `filter_ops` | `dict` | Map of column type to list of supported filter operators |
|
||||
| `aggregates` | `dict` | Map of column type to list of supported aggregate functions |
|
||||
| `on_update` | `bool` | Whether update callbacks are supported |
|
||||
@@ -0,0 +1,5 @@
|
||||
# Getting Started
|
||||
|
||||
Guides for installing and using Perspective in JavaScript (Browser & Node.js),
|
||||
Python and Rust. Each section includes installation steps, basic usage examples,
|
||||
and language-specific integration details.
|
||||
@@ -0,0 +1,3 @@
|
||||
# How To
|
||||
|
||||
Step-by-step guides for common Perspective tasks, organized by language.
|
||||
@@ -0,0 +1,4 @@
|
||||
# JavaScript
|
||||
|
||||
Guides for using Perspective in the browser and Node.js, including the
|
||||
`perspective` data engine and the `<perspective-viewer>` UI component.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Customizing `perspective.worker()`
|
||||
|
||||
`perspective.worker()` creates a `Client` that connects to a Perspective data
|
||||
engine. By default it spins up a dedicated `Worker` running the built-in
|
||||
WebAssembly engine, but you can pass an argument to change this behavior:
|
||||
|
||||
- A **`Worker`**, **`SharedWorker`**, or **`ServiceWorker`** — runs the
|
||||
built-in engine in a different worker context.
|
||||
- A **`MessagePort`** from `createMessageHandler()` — connects to a
|
||||
[Virtual Server](virtual_server/custom.md) instead of the built-in engine.
|
||||
|
||||
## Built-in engine with a custom Worker
|
||||
|
||||
Pass a `Worker`, `SharedWorker`, or `ServiceWorker` that loads the worker script
|
||||
distributed at
|
||||
`"@perspective-dev/client/dist/cdn/perspective-server.worker.js"`.
|
||||
|
||||
<span class="warning">`SharedWorker` and `ServiceWorker` have more complicated
|
||||
behavior compared to a dedicated `Worker`, and will need special consideration
|
||||
to integrate (or debug).</span>
|
||||
|
||||
### Dedicated `Worker`
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker(new Worker(url));
|
||||
```
|
||||
|
||||
### `SharedWorker`
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker(new SharedWorker(url));
|
||||
```
|
||||
|
||||
### `ServiceWorker`
|
||||
|
||||
```javascript
|
||||
const registration = await navigator.serviceWorker.register(url, {
|
||||
scope: "", // Your scope here
|
||||
});
|
||||
|
||||
const worker = await perspective.worker(registration.active);
|
||||
```
|
||||
|
||||
## Virtual Server
|
||||
|
||||
Instead of the built-in WebAssembly engine, `perspective.worker()` can connect
|
||||
to a Virtual Server — an adapter that translates Perspective queries into
|
||||
operations on an external data source such as
|
||||
[DuckDB](virtual_server/duckdb.md) or
|
||||
[ClickHouse](virtual_server/clickhouse.md).
|
||||
|
||||
Use `perspective.createMessageHandler()` with a `VirtualServerHandler` to create
|
||||
a `MessagePort`, then pass it to `worker()`:
|
||||
|
||||
```javascript
|
||||
import perspective from "@perspective-dev/client";
|
||||
|
||||
const handler = {
|
||||
/* VirtualServerHandler implementation */
|
||||
};
|
||||
|
||||
const server = perspective.createMessageHandler(handler);
|
||||
const client = await perspective.worker(server);
|
||||
const table = await client.open_table("my_table");
|
||||
```
|
||||
|
||||
The returned `Client` works identically to one backed by the built-in engine —
|
||||
you can pass it to `<perspective-viewer>.load()`, call `open_table()`, etc. The
|
||||
difference is that queries are fulfilled by your handler rather than the WASM
|
||||
engine.
|
||||
|
||||
For the full `VirtualServerHandler` interface and a worked example, see
|
||||
[Implementing a custom Virtual Server](virtual_server/custom.md).
|
||||
@@ -0,0 +1,25 @@
|
||||
# Deleting a `table()` or `view()`
|
||||
|
||||
Unlike standard JavaScript objects, Perspective objects such as `table()` and
|
||||
`view()` store their associated data in the WebAssembly heap. Because of this,
|
||||
as well as the current lack of a hook into the JavaScript runtime's garbage
|
||||
collector from WebAssembly, the memory allocated to these Perspective objects
|
||||
does not automatically get cleaned up when the object falls out of scope.
|
||||
|
||||
In order to prevent memory leaks and reclaim the memory associated with a
|
||||
Perspective `table()` or `view()`, you must call the `delete()` method:
|
||||
|
||||
```javascript
|
||||
await view.delete();
|
||||
|
||||
// This method will throw an exception if there are still `view()`s depending
|
||||
// on this `table()`!
|
||||
await table.delete();
|
||||
```
|
||||
|
||||
Similarly, `<perspective-viewer>` Custom Elements do not delete the memory
|
||||
allocated for the UI when they are removed from the DOM.
|
||||
|
||||
```javascript
|
||||
await viewer.delete();
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# Listening for events
|
||||
|
||||
The `<perspective-viewer>` Custom Element fires all the same HTML `Event`s that
|
||||
standard DOM `HTMLElement` objects fire, in addition to a few custom
|
||||
`CustomEvent`s which relate to UI updates including those initiaed through user
|
||||
interaction.
|
||||
|
||||
## Update events
|
||||
|
||||
Whenever a `<perspective-viewer>`s underlying `table()` is changed via the
|
||||
`load()` or `update()` methods, a `perspective-view-update` DOM event is fired.
|
||||
Similarly, `view()` updates instigated either through the Attribute API or
|
||||
through user interaction will fire a `perspective-config-update` event:
|
||||
|
||||
```javascript
|
||||
elem.addEventListener("perspective-config-update", function (event) {
|
||||
var config = elem.save();
|
||||
console.log("The view() config has changed to " + JSON.stringify(config));
|
||||
});
|
||||
```
|
||||
|
||||
## Click events
|
||||
|
||||
Whenever a `<perspective-viewer>`'s grid or chart is clicked, a
|
||||
`perspective-click` DOM event is fired containing a detail object with `config`,
|
||||
`column_names`, and `row`.
|
||||
|
||||
The `config` object contains an array of `filters` that can be applied to a
|
||||
`<perspective-viewer>` through the use of `restore()` updating it to show the
|
||||
filtered subset of data.
|
||||
|
||||
The `column_names` property contains an array of matching columns, and the `row`
|
||||
property returns the associated row data.
|
||||
|
||||
```javascript
|
||||
elem.addEventListener("perspective-click", function (event) {
|
||||
var config = event.detail.config;
|
||||
elem.restore(config);
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,172 @@
|
||||
# JavaScript - Importing with or without a bundler
|
||||
|
||||
Perspective requires the browser to have access to Perspective's `.wasm`
|
||||
binaries _in addition_ to the bundled `.js` files, and as a result the build
|
||||
process requires a few extra steps. Perspective's NPM releases come with
|
||||
multiple prebuilt configurations.
|
||||
|
||||
## ESM builds with a bundler
|
||||
|
||||
The recommended builds for production use are packaged as ES Modules and require
|
||||
a _bootstrapping_ step in order to acquire the `.wasm` binaries and initialize
|
||||
Perspective's JavaScript with them. Because they have no hard-coded dependencies
|
||||
on the `.wasm` paths, they are ideal for use with JavaScript bundlers such as
|
||||
ESBuild, Rollup, Vite or Webpack.
|
||||
|
||||
ESM builds must be _bootstrapped_ with their `.wasm` binaries to initialize. The
|
||||
`wasm` binaries can be found in their respective `dist/wasm` directories.
|
||||
|
||||
```javascript
|
||||
import perspective_viewer from "@perspective-dev/viewer";
|
||||
import perspective from "@perspective-dev/client";
|
||||
|
||||
// TODO These paths must be provided by the bundler!
|
||||
const SERVER_WASM = ... // "@perspective-dev/server/dist/wasm/perspective-server.wasm"
|
||||
const CLIENT_WASM = ... // "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(SERVER_WASM),
|
||||
perspective_viewer.init_client(CLIENT_WASM),
|
||||
]);
|
||||
|
||||
// Now Perspective API will work!
|
||||
const worker = await perspective.worker();
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
```
|
||||
|
||||
The exact syntax will vary slightly depending on the bundler.
|
||||
|
||||
### Vite
|
||||
|
||||
```javascript
|
||||
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm?url";
|
||||
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm?url";
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(fetch(SERVER_WASM)),
|
||||
perspective_viewer.init_client(fetch(CLIENT_WASM)),
|
||||
]);
|
||||
```
|
||||
|
||||
You'll also need to target `esnext` in your `vite.config.js` in order to run the
|
||||
`build` step:
|
||||
|
||||
```javascript
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: "esnext",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### ESBuild
|
||||
|
||||
```javascript
|
||||
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
|
||||
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(fetch(SERVER_WASM)),
|
||||
perspective_viewer.init_client(fetch(CLIENT_WASM)),
|
||||
]);
|
||||
```
|
||||
|
||||
ESBuild config JSON to encode this asset as a `file`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
// ...
|
||||
"loader": {
|
||||
// ...
|
||||
".wasm": "file"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webpack
|
||||
|
||||
```javascript
|
||||
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
|
||||
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(SERVER_WASM),
|
||||
perspective_viewer.init_client(CLIENT_WASM),
|
||||
]);
|
||||
```
|
||||
|
||||
Webpack config:
|
||||
|
||||
```javascript
|
||||
{
|
||||
// ...
|
||||
module: {
|
||||
// ...
|
||||
rules: [
|
||||
// ...
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
type: "asset/resource"
|
||||
},
|
||||
]
|
||||
},
|
||||
experiments: {
|
||||
// ...
|
||||
asyncWebAssembly: false,
|
||||
syncWebAssembly: false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Inline builds with a bundler
|
||||
|
||||
<span class="warning">Inline builds are deprecated and will be removed in a
|
||||
future release.</span>
|
||||
|
||||
Perspective's _Inline_ Builds work by _inlining_ WebAssembly binary content as
|
||||
a base64-encoded string. While inline builds work with most bundlers and _do
|
||||
not_ require bootstrapping, there is an inherent file-size and boot-performance
|
||||
penalty. Prefer your bundler's inlining features and Perspective ESM builds
|
||||
where possible.
|
||||
|
||||
```javascript
|
||||
import "@perspective-dev/viewer/dist/esm/perspective-viewer.inline.js";
|
||||
import psp from "@perspective-dev/client/dist/esm/perspective.inline.js";
|
||||
```
|
||||
|
||||
## CDN builds
|
||||
|
||||
Perspective's CDN builds are good for non-bundled scenarios, such as importing
|
||||
directly from a `<script>` tag. CDN builds _do not_ require _bootstrapping_ the
|
||||
WebAssembly binaries, but they also generally _do not_ work with bundlers.
|
||||
|
||||
CDN builds are in ES Module format, thus to include them via a CDN they must be
|
||||
imported from a `<script type="module">`:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
import perspective from "https://cdn.jsdelivr.net/npm/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
// .. Do stuff here ..
|
||||
</script>
|
||||
```
|
||||
|
||||
## Node.js builds
|
||||
|
||||
The Node.js runtime for the `@perspective-dev/client` module runs in-process by
|
||||
default and does not implement a `child_process` interface. Hence, there is no
|
||||
`worker()` method, and the module object itself directly exports the full
|
||||
`perspective` API.
|
||||
|
||||
```javascript
|
||||
const perspective = require("@perspective-dev/client");
|
||||
```
|
||||
|
||||
In Node.js, perspective does not run in a WebWorker (as this API does not exist
|
||||
in Node.js), so no need to call the `.worker()` factory function - the
|
||||
`perspective` library exports the functions directly and run synchronously in
|
||||
the main process.
|
||||
@@ -0,0 +1,54 @@
|
||||
# JavaScript Installation and Module Structure
|
||||
|
||||
Perspective is designed for flexibility, allowing developers to pick and choose
|
||||
which modules they need. The main modules are:
|
||||
|
||||
- `@perspective-dev/client`
|
||||
The data engine library, as both a browser ES6 and Node.js module. Provides a
|
||||
WebAssembly, WebWorker (browser) and Process (node.js) runtime.
|
||||
|
||||
- `@perspective-dev/viewer`
|
||||
A user-configurable visualization widget, bundled as a
|
||||
[Web Component](https://www.webcomponents.org/introduction). This module
|
||||
includes the core data engine module as a dependency.
|
||||
|
||||
`<perspective-viewer>` by itself only implements a trivial debug renderer, which
|
||||
prints the currently configured `view()` as a CSV. Plugin modules are packaged
|
||||
separately and must be imported individually.
|
||||
|
||||
- `@perspective-dev/viewer-datagrid`
|
||||
A custom high-performance data-grid component based on HTML `<table>`.
|
||||
|
||||
- `@perspective-dev/viewer-charts`
|
||||
A set of charting components base on WebGL.
|
||||
|
||||
When imported after `@perspective-dev/viewer`, the plugin modules will register
|
||||
themselves automatically, and the renderers they export will be available in the
|
||||
`plugin` dropdown in the `<perspective-viewer>` UI.
|
||||
|
||||
## Browser
|
||||
|
||||
Perspective's WebAssembly data engine is available via NPM in the same package
|
||||
as its Node.js counterpart, `@perspective-dev/client`. The Perspective Viewer UI
|
||||
(which has no Node.js component) must be installed separately:
|
||||
|
||||
```bash
|
||||
$ npm add @perspective-dev/client @perspective-dev/viewer
|
||||
```
|
||||
|
||||
By itself, `@perspective-dev/viewer` does not provide any visualizations, only
|
||||
the UI framework. Perspective _Plugins_ provide visualizations and must be
|
||||
installed separately. All Plugins are optional - but a `<perspective-viewer>`
|
||||
without Plugins would be rather boring!
|
||||
|
||||
```bash
|
||||
$ npm add @perspective-dev/viewer-charts @perspective-dev/viewer-datagrid
|
||||
```
|
||||
|
||||
## Node.js
|
||||
|
||||
To use Perspective from a Node.js server, simply install via NPM.
|
||||
|
||||
```bash
|
||||
$ npm add @perspective-dev/client
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# Joining Tables
|
||||
|
||||
`perspective.join()` creates a read-only `Table` by joining two source tables on
|
||||
a shared key column. The result is reactive — it updates automatically when
|
||||
either source table changes. See [`Join`](../../explanation/join.md) for
|
||||
conceptual details.
|
||||
|
||||
## Basic Inner Join
|
||||
|
||||
```javascript
|
||||
const orders = await perspective.table([
|
||||
{ id: 1, product_id: 101, qty: 5 },
|
||||
{ id: 2, product_id: 102, qty: 3 },
|
||||
{ id: 3, product_id: 101, qty: 7 },
|
||||
]);
|
||||
|
||||
const products = await perspective.table([
|
||||
{ product_id: 101, name: "Widget" },
|
||||
{ product_id: 102, name: "Gadget" },
|
||||
]);
|
||||
|
||||
const joined = await perspective.join(orders, products, "product_id");
|
||||
const view = await joined.view();
|
||||
const json = await view.to_json();
|
||||
// [
|
||||
// { product_id: 101, id: 1, qty: 5, name: "Widget" },
|
||||
// { product_id: 101, id: 3, qty: 7, name: "Widget" },
|
||||
// { product_id: 102, id: 2, qty: 3, name: "Gadget" },
|
||||
// ]
|
||||
```
|
||||
|
||||
## Join Types
|
||||
|
||||
Pass `join_type` in the options to select inner, left, or outer join behavior:
|
||||
|
||||
```javascript
|
||||
// Left join: all left rows, nulls for unmatched right columns
|
||||
const left_joined = await perspective.join(left, right, "id", {
|
||||
join_type: "left",
|
||||
});
|
||||
|
||||
// Outer join: all rows from both tables
|
||||
const outer_joined = await perspective.join(left, right, "id", {
|
||||
join_type: "outer",
|
||||
});
|
||||
```
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
The joined table recomputes automatically when either source table is updated:
|
||||
|
||||
```javascript
|
||||
const left = await perspective.table([{ id: 1, x: 10 }]);
|
||||
const right = await perspective.table([{ id: 2, y: "b" }]);
|
||||
|
||||
const joined = await perspective.join(left, right, "id");
|
||||
const view = await joined.view();
|
||||
|
||||
let json = await view.to_json();
|
||||
// [] — no matching keys yet
|
||||
|
||||
await right.update([{ id: 1, y: "a" }]);
|
||||
json = await view.to_json();
|
||||
// [{ id: 1, x: 10, y: "a" }] — new match detected
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# Loading data from a Table
|
||||
|
||||
Data can be loaded into `<perspective-viewer>` in the form of a `Table()` or a
|
||||
`Promise<Table>` via the `load()` method.
|
||||
|
||||
```javascript
|
||||
// Create a new worker, then a new table promise on that worker.
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(data);
|
||||
|
||||
// Bind a viewer element to this table.
|
||||
await viewer.load(table);
|
||||
```
|
||||
|
||||
## Sharing a `Table` between multiple `<perspective-viewer>`s
|
||||
|
||||
Multiple `<perspective-viewer>`s can share a `table()` by passing the `table()`
|
||||
into the `load()` method of each viewer. Each `perspective-viewer` will update
|
||||
when the underlying `table()` is updated, but `table.delete()` will fail until
|
||||
all `perspective-viewer` instances referencing it are also deleted:
|
||||
|
||||
```javascript
|
||||
const viewer1 = document.getElementById("viewer1");
|
||||
const viewer2 = document.getElementById("viewer2");
|
||||
|
||||
// Create a new WebWorker
|
||||
const worker = await perspective.worker();
|
||||
|
||||
// Create a table in this worker
|
||||
const table = await worker.table(data);
|
||||
|
||||
// Load the same table in 2 different <perspective-viewer> elements
|
||||
await viewer1.load(table);
|
||||
await viewer2.load(table);
|
||||
|
||||
// Both `viewer1` and `viewer2` will reflect this update
|
||||
await table.update([{ x: 5, y: "e", z: true }]);
|
||||
```
|
||||
|
||||
## Loading from a virtual `Table`
|
||||
|
||||
Loading a virtual (server-only) `Table` works just like loading a local/Web
|
||||
Worker `Table` — just pass the virtual `Table` to `viewer.load()`. In the
|
||||
browser:
|
||||
|
||||
```javascript
|
||||
const elem = document.getElementsByTagName("perspective-viewer")[0];
|
||||
|
||||
// Bind to the server's worker instead of instantiating a Web Worker.
|
||||
const websocket = await perspective.websocket(
|
||||
window.location.origin.replace("http", "ws")
|
||||
);
|
||||
|
||||
// Bind the viewer to the preloaded data source. `table` and `view` objects
|
||||
// live on the server.
|
||||
const server_table = await websocket.open_table("table_one");
|
||||
await elem.load(server_table);
|
||||
```
|
||||
|
||||
Alternatively, data can be _cloned_ from a server-side virtual `Table` into a
|
||||
client-side WebAssembly `Table`. The browser clone will be synced via delta
|
||||
updates transferred via Apache Arrow IPC format, but local `View`s created will
|
||||
be calculated locally on the client browser.
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker();
|
||||
const server_view = await server_table.view();
|
||||
const client_table = worker.table(server_view);
|
||||
await elem.load(client_table);
|
||||
```
|
||||
|
||||
`<perspective-viewer>` instances bound in this way are otherwise no different
|
||||
than `<perspective-viewer>`s which rely on a Web Worker, and can even share a
|
||||
host application with Web Worker-bound `table()`s. The same `promise`-based API
|
||||
is used to communicate with the server-instantiated `view()`, only in this case
|
||||
it is over a websocket.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Server-only via `WebSocketServer()` and Node.js
|
||||
|
||||
For exceptionally large datasets, a `Client` can be bound to a
|
||||
`perspective.table()` instance running in Node.js/Python/Rust remotely, rather
|
||||
than creating one in a Web Worker and downloading the entire data set. This
|
||||
trades off network bandwidth and server resource requirements for a smaller
|
||||
browser memory and CPU footprint.
|
||||
|
||||
An example in Node.js:
|
||||
|
||||
```javascript
|
||||
const { WebSocketServer, table } = require("@perspective-dev/client");
|
||||
const fs = require("fs");
|
||||
|
||||
// Start a WS/HTTP host on port 8080. The `assets` property allows
|
||||
// the `WebSocketServer()` to also serves the file structure rooted in this
|
||||
// module's directory.
|
||||
const host = new WebSocketServer({ assets: [__dirname], port: 8080 });
|
||||
|
||||
// Read an arrow file from the file system and host it as a named table.
|
||||
const arr = fs.readFileSync(__dirname + "/superstore.lz4.arrow");
|
||||
await table(arr, { name: "table_one" });
|
||||
```
|
||||
|
||||
... and the [`Client`] implementation in the browser:
|
||||
|
||||
```javascript
|
||||
const elem = document.getElementsByTagName("perspective-viewer")[0];
|
||||
|
||||
// Bind to the server's worker instead of instantiating a Web Worker.
|
||||
const websocket = await perspective.websocket(
|
||||
window.location.origin.replace("http", "ws"),
|
||||
);
|
||||
|
||||
// Create a virtual `Table` to the preloaded data source. `table` and `view`
|
||||
// objects live on the server.
|
||||
const server_table = await websocket.open_table("table_one");
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
# Plugin render limits
|
||||
|
||||
`<perspective-viewer>` plugins (especially charts) may in some cases generate
|
||||
extremely large output which may lock up the browser. In order to prevent
|
||||
accidents (which generally require a browser refresh to fix), each plugin has a
|
||||
`max_cells` and `max_columns` heuristic which requires the user to opt-in to
|
||||
fully rendering `View`s which exceed these limits. To override this behavior,
|
||||
set these values for each plugin type individually, _before_ the plugin itself
|
||||
is rendered (e.g. calling `HTMLPerspectiveViewerElement::restore` with the
|
||||
respective `plugin` name).
|
||||
|
||||
If you have a `<perspective-viewer>` instance, you can configure plugins via
|
||||
`HTMLPerspectiveViewerElement::getPlugin` and
|
||||
`HTMLPerspectiveViewerElement::getAllPlugins`:
|
||||
|
||||
```javascript
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const plugin = viewer.getPlugin("Treemap");
|
||||
plugin.max_cells = 1_000_000;
|
||||
plugin.max_columns = 1000;
|
||||
```
|
||||
|
||||
... Or alternatively, you can look up the Custom Element classes and set the
|
||||
static variants if you know the element name (you can e.g. look this up in your
|
||||
browser's DOM inspector):
|
||||
|
||||
```javascript
|
||||
const plugin = customElements.get("perspective-viewer-charts-treemap");
|
||||
plugin.max_cells = 1_000_000;
|
||||
plugin.max_columns = 1000;
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# React Component
|
||||
|
||||
We provide a React wrapper to prevent common issues and mistakes associated with
|
||||
using the perspective-viewer web component in the context of React.
|
||||
|
||||
Before trying this example, please take a look at
|
||||
[how to bootstrap perspective](./importing.md).
|
||||
|
||||
## `PerspectiveViewer`
|
||||
|
||||
A simple example using the `PerspectiveViewer` component:
|
||||
|
||||
```typescript
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
PerspectiveViewer,
|
||||
} from "@perspective-dev/react";
|
||||
import perspective from "@perspective-dev/client";
|
||||
|
||||
function App() {
|
||||
const worker = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
worker.current = await perspective.worker();
|
||||
const resp = await fetch("data.arrow");
|
||||
const arrow = await resp.arrayBuffer();
|
||||
await worker.current.table(arrow, { name: "my_table" });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PerspectiveViewer
|
||||
client={worker.current}
|
||||
config={{group_by: ["State"], columns: ["Sales"]}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## `PerspectiveWorkspace`
|
||||
|
||||
For multi-viewer layouts, use `PerspectiveWorkspace`:
|
||||
|
||||
```typescript
|
||||
import { PerspectiveWorkspace } from "@perspective-dev/react";
|
||||
|
||||
const WORKSPACE_CONFIG = // ...
|
||||
|
||||
function Dashboard() {
|
||||
return (
|
||||
<PerspectiveWorkspace
|
||||
client={perspective.worker()}
|
||||
config={WORKSPACE_CONFIG} />
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# Saving and restoring UI state.
|
||||
|
||||
`<perspective-viewer>` is _persistent_, in that its entire state (sans the data
|
||||
itself) can be serialized or deserialized. This include all column, filter,
|
||||
pivot, expressions, etc. properties, as well as datagrid style settings, config
|
||||
panel visibility, and more. This overloaded feature covers a range of use cases:
|
||||
|
||||
- Setting a `<perspective-viewer>`'s initial state after a `load()` call.
|
||||
- Updating a single or subset of properties, without modifying others.
|
||||
- Resetting some or all properties to their data-relative default.
|
||||
- Persisting a user's configuration to `localStorage` or a server.
|
||||
|
||||
## Serializing and deserializing the viewer state
|
||||
|
||||
To retrieve the entire state as a JSON-ready JavaScript object, use the `save()`
|
||||
method. `save()` also supports a few other formats such as `"arraybuffer"` and
|
||||
`"string"` (base64, not JSON), which you may choose for size at the expense of
|
||||
easy migration/manual-editing.
|
||||
|
||||
```javascript
|
||||
const json_token = await elem.save();
|
||||
const string_token = await elem.save("string");
|
||||
```
|
||||
|
||||
For any format, the serialized token can be restored to any
|
||||
`<perspective-viewer>` with a `Table` of identical schema, via the `restore()`
|
||||
method. Note that while the data for a token returned from `save()` may differ,
|
||||
generally its schema may not, as many other settings depend on column names and
|
||||
types.
|
||||
|
||||
```javascript
|
||||
await elem.restore(json_token);
|
||||
await elem.restore(string_token);
|
||||
```
|
||||
|
||||
As `restore()` dispatches on the token's type, it is important to make sure that
|
||||
these types match! A common source of error occurs when passing a
|
||||
JSON-stringified token to `restore()`, which will assume base64-encoded msgpack
|
||||
when a string token is used.
|
||||
|
||||
```javascript
|
||||
// This will error!
|
||||
await elem.restore(JSON.stringify(json_token));
|
||||
```
|
||||
|
||||
### Updating individual properties
|
||||
|
||||
Using the JSON format, every facet of a `<perspective-viewer>`'s configuration
|
||||
can be manipulated from JavaScript using the `restore()` method. The valid
|
||||
structure of properties is described via the
|
||||
[`ViewerConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa/rust/perspective-viewer/src/ts/viewer.ts#L16)
|
||||
and embedded
|
||||
[`ViewConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa19435a2a57d4687be7e428a4efc759b/packages/perspective/index.d.ts#L140)
|
||||
type declarations, and [`View`](view.md) chapter of the documentation which has
|
||||
several interactive examples for each `ViewConfig` property.
|
||||
|
||||
```javascript
|
||||
// Set the plugin (will also update `columns` to plugin-defaults)
|
||||
await elem.restore({ plugin: "X Bar" });
|
||||
|
||||
// Update plugin and columns (only draws once)
|
||||
await elem.restore({ plugin: "X Bar", columns: ["Sales"] });
|
||||
|
||||
// Open the config panel
|
||||
await elem.restore({ settings: true });
|
||||
|
||||
// Create an expression
|
||||
await elem.restore({
|
||||
columns: ['"Sales" + 100'],
|
||||
expressions: { "New Column": '"Sales" + 100' },
|
||||
});
|
||||
|
||||
// ERROR if the column does not exist in the schema or expressions
|
||||
// await elem.restore({columns: ["\"Sales\" + 100"], expressions: {}});
|
||||
|
||||
// Add a filter
|
||||
await elem.restore({ filter: [["Sales", "<", 100]] });
|
||||
|
||||
// Add a sort, don't remove filter
|
||||
await elem.restore({ sort: [["Prodit", "desc"]] });
|
||||
|
||||
// Reset just filter, preserve sort
|
||||
await elem.restore({ filter: undefined });
|
||||
|
||||
// Reset all properties to default e.g. after `load()`
|
||||
await elem.reset();
|
||||
```
|
||||
|
||||
Another effective way to quickly create a token for a desired configuration is
|
||||
to simply copy the token returned from `save()` after settings the view manually
|
||||
in the browser. The JSON format is human-readable and should be quite easy to
|
||||
tweak once generated, as `save()` will return even the default settings for all
|
||||
properties. You can call `save()` in your application code, or e.g. through the
|
||||
Chrome developer console:
|
||||
|
||||
```javascript
|
||||
// Copy to clipboard
|
||||
copy(await document.querySelector("perspective-viewer").save());
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
### Serializing data
|
||||
|
||||
The `view()` allows for serialization of data to JavaScript through the
|
||||
`to_json()`, `to_ndjson()`, `to_columns()`, `to_csv()`, and `to_arrow()` methods
|
||||
(the same data formats supported by the `Client::table` factory function). These
|
||||
methods return a `promise` for the calculated data:
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["State"], columns: ["Sales"] });
|
||||
|
||||
// JavaScript Objects
|
||||
console.log(await view.to_json());
|
||||
console.log(await view.to_columns());
|
||||
|
||||
// String
|
||||
console.log(await view.to_csv());
|
||||
console.log(await view.to_ndjson());
|
||||
|
||||
// ArrayBuffer
|
||||
console.log(await view.to_arrow());
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
# Theming
|
||||
|
||||
Theming is supported in `perspective-viewer` and its accompanying plugins. A
|
||||
number of themes come bundled with `perspective-viewer`; you can import any of
|
||||
these themes directly into your app, and the `perspective-viewer`s will be
|
||||
themed accordingly:
|
||||
|
||||
```javascript
|
||||
// Themes based on Thought Merchants's Prospective design
|
||||
import "@perspective-dev/viewer/dist/css/pro.css";
|
||||
import "@perspective-dev/viewer/dist/css/pro-dark.css";
|
||||
|
||||
// Other themes
|
||||
import "@perspective-dev/viewer/dist/css/solarized.css";
|
||||
import "@perspective-dev/viewer/dist/css/solarized-dark.css";
|
||||
import "@perspective-dev/viewer/dist/css/monokai.css";
|
||||
import "@perspective-dev/viewer/dist/css/vaporwave.css";
|
||||
```
|
||||
|
||||
Alternatively, you may use `themes.css`, which bundles all default themes
|
||||
|
||||
```javascript
|
||||
import "@perspective-dev/viewer/dist/css/themes.css";
|
||||
```
|
||||
|
||||
If you choose not to bundle the themes yourself, they are available through
|
||||
[CDN](https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/). These can
|
||||
be directly linked in your HTML file:
|
||||
|
||||
```html
|
||||
<link
|
||||
rel="stylesheet"
|
||||
crossorigin="anonymous"
|
||||
href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/pro.css"
|
||||
/>
|
||||
```
|
||||
|
||||
Note the `crossorigin="anonymous"` attribute. When including a theme from a
|
||||
cross-origin context, this attribute may be required to allow
|
||||
`<perspective-viewer>` to detect the theme. If this fails, additional themes are
|
||||
added to the `document` after `<perspective-viewer>` init, or for any other
|
||||
reason theme auto-detection fails, you may manually inform
|
||||
`<perspective-viewer>` of the available theme names with the `.resetThemes()`
|
||||
method.
|
||||
|
||||
```javascript
|
||||
// re-auto-detect themes
|
||||
viewer.resetThemes();
|
||||
|
||||
// Set available themes explicitly (they still must be imported as CSS!)
|
||||
viewer.resetThemes(["Pro Light", "Pro Dark"]);
|
||||
```
|
||||
|
||||
`<perspective-viewer>` will default to the first loaded theme when initialized.
|
||||
You may override this via `.restore()`, or provide an initial theme by setting
|
||||
the `theme` attribute:
|
||||
|
||||
```html
|
||||
<perspective-viewer theme="Pro Light"></perspective-viewer>
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```javascript
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
await viewer.restore({ theme: "Pro Dark" });
|
||||
```
|
||||
|
||||
## Custom Themes
|
||||
|
||||
The best way to write a new theme is to
|
||||
[fork and modify an existing theme](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes),
|
||||
which are _just_ collections of regular CSS variables (no preprocessor is
|
||||
required, though Perspective's own themes use one). `<perspective-viewer>` is
|
||||
not "themed" by default and will lack icons and label text in addition to colors
|
||||
and fonts, so starting from an empty theme forces you to define _every_
|
||||
theme-able variable to get a functional UI.
|
||||
|
||||
### Icons and Translation
|
||||
|
||||
UI icons are defined by CSS variables provided by
|
||||
[`@perspective-dev/viewer/dist/css/icons.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/icons.css).
|
||||
These variables must be defined for the UI icons to work - there are no default
|
||||
icons without a theme.
|
||||
|
||||
UI text is also defined in CSS variables provided by
|
||||
[`@perspective-dev/viewer/dist/css/intl.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/intl.css),
|
||||
and has identical import requirements. Some _example definitions_
|
||||
(automatically-translated sans-editing) can be found
|
||||
[`@perspective-dev/viewer/dist/css/intl/` folder](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes/intl).
|
||||
|
||||
Importing the pre-built `themes.css` stylesheet as well as a custom theme will
|
||||
define Icons and Translation globally as a side-effect. You can still customize
|
||||
icons in this mode with rules (of the appropriate specificity), _but_ if you do
|
||||
not still remember to define these variables yourself, your theme will not work
|
||||
without the base `themes.css` package available.
|
||||
@@ -0,0 +1,62 @@
|
||||
# `<perspective-viewer>` Custom Element library
|
||||
|
||||
`<perspective-viewer>` provides a complete graphical UI for configuring the
|
||||
`perspective` library and formatting its output to the provided visualization
|
||||
plugins.
|
||||
|
||||
Once imported and initialized in JavaScript, the `<perspective-viewer>` Web
|
||||
Component will be available in any standard HTML on your site. A simple example:
|
||||
|
||||
```html
|
||||
<perspective-viewer id="view1"></perspective-viewer>
|
||||
<script type="module">
|
||||
import perspective from "@perspective-dev/client";
|
||||
import "@perspective-dev/viewer";
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(data);
|
||||
document.getElementById("view1").load(table);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
`<perspective-viewer>` can be configured via HTML attributes or JavaScript
|
||||
properties. When set as attributes, the viewer will apply the configuration on
|
||||
initialization:
|
||||
|
||||
```html
|
||||
<perspective-viewer
|
||||
columns='["Sales", "Profit"]'
|
||||
group-by='["Region"]'
|
||||
sort='[["Sales", "desc"]]'>
|
||||
</perspective-viewer>
|
||||
```
|
||||
|
||||
## UI Features
|
||||
|
||||
The viewer provides an interactive side panel with:
|
||||
|
||||
- **Column list** - drag and drop columns to configure `group_by`, `split_by`,
|
||||
`sort`, and `filter` fields.
|
||||
- **New Column** button - opens an expression editor for creating computed
|
||||
columns via the [expression language](../../explanation/view/config/expressions.md).
|
||||
- **Plugin selector** - switch between visualization plugins such as Datagrid,
|
||||
X/Y Line, X/Y Scatter, Treemap, Sunburst, and Heatmap.
|
||||
- **Theme** selector - toggle between available themes.
|
||||
- **Export** - download the current view as CSV or Arrow.
|
||||
- **Copy** - copy the current view to the clipboard.
|
||||
- **Reset** - restore the viewer to its default configuration.
|
||||
|
||||
## Methods
|
||||
|
||||
Key methods on the `<perspective-viewer>` element:
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `load(table)` | Bind a `Table` to the viewer |
|
||||
| `restore(config)` | Apply a saved configuration object |
|
||||
| `save()` | Serialize the current configuration |
|
||||
| `reset(all)` | Reset configuration (pass `true` to also reset expressions) |
|
||||
| `getTable()` | Get the bound `Table` |
|
||||
| `flush()` | Wait for any pending UI updates to complete |
|
||||
@@ -0,0 +1,19 @@
|
||||
# Virtual Servers
|
||||
|
||||
Perspective's Virtual Server feature lets you connect `<perspective-viewer>` to
|
||||
external data sources without loading data into Perspective's built-in engine.
|
||||
Instead, queries are translated and executed natively by the external database.
|
||||
|
||||
For a detailed explanation of how virtual servers work, see the
|
||||
[Virtual Servers](../../explanation/virtual_servers.md) concepts page.
|
||||
|
||||
Perspective ships with built-in virtual server implementations for:
|
||||
|
||||
- [**DuckDB**](./virtual_server/duckdb.md) — query DuckDB databases in-browser
|
||||
via `@duckdb/duckdb-wasm`, or on the server via Node.js.
|
||||
- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server
|
||||
directly from the browser or from Node.js.
|
||||
|
||||
You can also [**implement your own**](./virtual_server/custom.md) virtual server
|
||||
to connect Perspective to any data source by implementing the
|
||||
`VirtualServerHandler` interface.
|
||||
@@ -0,0 +1,43 @@
|
||||
# ClickHouse Virtual Server
|
||||
|
||||
Perspective provides a built-in virtual server for
|
||||
[ClickHouse](https://clickhouse.com/), allowing `<perspective-viewer>` to query
|
||||
ClickHouse tables directly from the browser.
|
||||
|
||||
For server-side Python usage, see the
|
||||
[Python ClickHouse guide](../../python/virtual_server/clickhouse.md).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @perspective-dev/client @perspective-dev/viewer @clickhouse/client-web
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Connect to a ClickHouse instance and bind it to a Perspective viewer:
|
||||
|
||||
```javascript
|
||||
import perspective from "@perspective-dev/client";
|
||||
import "@perspective-dev/viewer";
|
||||
import { createClient } from "@clickhouse/client-web";
|
||||
|
||||
// Connect to ClickHouse
|
||||
const clickhouseClient = createClient({
|
||||
url: "http://localhost:8123",
|
||||
database: "default",
|
||||
});
|
||||
|
||||
// Create a Perspective virtual server backed by ClickHouse
|
||||
const handler = perspective.ClickhouseHandler(clickhouseClient);
|
||||
const messageHandler = perspective.createMessageHandler(handler);
|
||||
|
||||
// Connect a viewer
|
||||
const client = await perspective.worker(messageHandler);
|
||||
const table = await client.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Browser ClickHouse example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-clickhouse-virtual)
|
||||
@@ -0,0 +1,80 @@
|
||||
# Implementing a custom Virtual Server
|
||||
|
||||
You can connect Perspective to any data source by implementing the
|
||||
`VirtualServerHandler` interface and passing it to `createMessageHandler()`.
|
||||
|
||||
For background on virtual servers, see the
|
||||
[Virtual Servers overview](../../../explanation/virtual_servers.md).
|
||||
|
||||
## Example
|
||||
|
||||
```typescript
|
||||
import perspective from "@perspective-dev/client";
|
||||
import type {
|
||||
VirtualServerHandler,
|
||||
ColumnType,
|
||||
ViewConfig,
|
||||
ViewWindow,
|
||||
VirtualDataSlice,
|
||||
} from "@perspective-dev/client";
|
||||
|
||||
const handler = {
|
||||
async getHostedTables(): Promise<string[]> {
|
||||
return ["my_table"];
|
||||
},
|
||||
|
||||
async tableSchema(tableId: string): Promise<Record<string, ColumnType>> {
|
||||
return { name: "string", price: "float", date: "date" };
|
||||
},
|
||||
|
||||
async tableSize(tableId: string): Promise<number> {
|
||||
return 1000;
|
||||
},
|
||||
|
||||
async tableMakeView(
|
||||
tableId: string,
|
||||
viewId: string,
|
||||
config: ViewConfig,
|
||||
): Promise<void> {
|
||||
// Translate `config` (group_by, sort, filter, etc.) into a query
|
||||
// against your data source. Store the query keyed by `viewId`
|
||||
// for later data retrieval.
|
||||
},
|
||||
|
||||
async viewDelete(viewId: string): Promise<void> {
|
||||
// Clean up resources for this view
|
||||
},
|
||||
|
||||
async viewGetData(
|
||||
viewId: string,
|
||||
config: ViewConfig,
|
||||
schema: Record<string, ColumnType>,
|
||||
viewport: ViewWindow,
|
||||
dataSlice: VirtualDataSlice,
|
||||
): Promise<void> {
|
||||
// Query your data source using `config` and `viewport` for the
|
||||
// row/column window. Push columnar results via `dataSlice.setCol()`.
|
||||
},
|
||||
|
||||
getFeatures() {
|
||||
return {
|
||||
group_by: true,
|
||||
sort: true,
|
||||
filter_ops: {
|
||||
string: ["==", "!=", "contains", "is null", "is not null"],
|
||||
float: ["==", "!=", ">", "<", ">=", "<="],
|
||||
},
|
||||
aggregates: {
|
||||
float: ["sum", "avg", "count", "min", "max"],
|
||||
string: ["count", "any"],
|
||||
},
|
||||
};
|
||||
},
|
||||
} satisfies VirtualServerHandler;
|
||||
|
||||
// Create a message handler and use it like a worker
|
||||
const messageHandler = perspective.createMessageHandler(handler);
|
||||
const client = await perspective.worker(messageHandler);
|
||||
const table = await client.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# DuckDB Virtual Server
|
||||
|
||||
Perspective provides a built-in virtual server for
|
||||
[DuckDB](https://duckdb.org/), allowing `<perspective-viewer>` to query
|
||||
DuckDB-WASM databases directly in the browser.
|
||||
|
||||
For server-side Python usage, see the
|
||||
[Python DuckDB guide](../../python/virtual_server/duckdb.md).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @perspective-dev/client @perspective-dev/viewer @duckdb/duckdb-wasm
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Initialize DuckDB-WASM, load data, and connect it to a Perspective viewer:
|
||||
|
||||
```javascript
|
||||
import perspective from "@perspective-dev/client";
|
||||
import "@perspective-dev/viewer";
|
||||
import * as duckdb from "@duckdb/duckdb-wasm";
|
||||
|
||||
// Initialize DuckDB-WASM
|
||||
const DUCKDB_BUNDLES = duckdb.getJsDelivrBundles();
|
||||
const bundle = await duckdb.selectBundle(DUCKDB_BUNDLES);
|
||||
const worker = await duckdb.createWorker(bundle.mainWorker);
|
||||
const logger = new duckdb.ConsoleLogger();
|
||||
const db = new duckdb.AsyncDuckDB(logger, worker);
|
||||
await db.instantiate(bundle.mainModule);
|
||||
|
||||
// Load data into DuckDB
|
||||
const conn = await db.connect();
|
||||
await conn.query(`CREATE TABLE my_table AS SELECT * FROM 'data.parquet'`);
|
||||
|
||||
// Create a Perspective virtual server backed by DuckDB
|
||||
const handler = perspective.DuckDBHandler(db);
|
||||
const messageHandler = perspective.createMessageHandler(handler);
|
||||
|
||||
// Connect a viewer
|
||||
const client = await perspective.worker(messageHandler);
|
||||
const table = await client.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Browser DuckDB example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-duckdb-virtual)
|
||||
@@ -0,0 +1,44 @@
|
||||
# Accessing the Perspective engine via a `Client` instance
|
||||
|
||||
An instance of a `Client` is needed to talk to a Perspective `Server`, of which
|
||||
there are a few varieties available in JavaScript.
|
||||
|
||||
## Web Worker (Browser)
|
||||
|
||||
Perspective's Web Worker client is actually a `Client` and `Server` rolled into
|
||||
one. Instantiating this `Client` will also create a _dedicated_ Perspective
|
||||
`Server` in a Web Worker process.
|
||||
|
||||
To use it, you'll need to instantiate a Web Worker `perspective` engine via the
|
||||
`worker()` method. This will create a new Web Worker (browser) and load the
|
||||
WebAssembly binary. All calculation and data accumulation will occur in this
|
||||
separate process.
|
||||
|
||||
```javascript
|
||||
const client = await perspective.worker();
|
||||
```
|
||||
|
||||
The `worker` symbol will expose the full `perspective` API for one managed Web
|
||||
Worker process. You are free to create as many as your browser supports, but be
|
||||
sure to keep track of the `worker` instances themselves, as you'll need them to
|
||||
interact with your data in each instance.
|
||||
|
||||
## Websocket (Browser)
|
||||
|
||||
Alternatively, with a Perspective server running in Node.js, Python or Rust, you
|
||||
can create a _virtual_ `Client` via the `websocket()` method.
|
||||
|
||||
```javascript
|
||||
const client = perspective.websocket("http://localhost:8080/");
|
||||
```
|
||||
|
||||
## Node.js
|
||||
|
||||
The Node.js runtime for the `@perspective-dev/client` module runs in-process by
|
||||
default and does not implement a `child_process` interface, so no need to call
|
||||
the `.worker()` factory function. Instead, the `perspective` library exports the
|
||||
functions directly and run synchronously in the main process.
|
||||
|
||||
```javascript
|
||||
const client = require("@perspective-dev/client");
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
# Python
|
||||
|
||||
Guides for using `perspective-python`, including data loading, callbacks,
|
||||
multithreading, WebSocket servers, and JupyterLab integration.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Callbacks and Events
|
||||
|
||||
`perspective.Table` allows for `on_update` and `on_delete` callbacks to be
|
||||
set—simply call `on_update` or `on_delete` with a reference to a function or a
|
||||
lambda without any parameters:
|
||||
|
||||
```python
|
||||
def update_callback():
|
||||
print("Updated!")
|
||||
|
||||
# set the update callback
|
||||
on_update_id = view.on_update(update_callback)
|
||||
|
||||
|
||||
def delete_callback():
|
||||
print("Deleted!")
|
||||
|
||||
# set the delete callback
|
||||
on_delete_id = view.on_delete(delete_callback)
|
||||
|
||||
# set a lambda as a callback
|
||||
view.on_delete(lambda: print("Deleted x2!"))
|
||||
```
|
||||
|
||||
If the callback is a named reference to a function, it can be removed with
|
||||
`remove_update` or `remove_delete`:
|
||||
|
||||
```python
|
||||
view.remove_update(on_update_id)
|
||||
view.remove_delete(on_delete_id)
|
||||
```
|
||||
|
||||
Callbacks defined with a lambda function cannot be removed, as lambda functions
|
||||
have no identifier.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Installation
|
||||
|
||||
`perspective-python` contains full bindings to the Perspective API, a JupyterLab
|
||||
widget, and WebSocket handlers for several webserver libraries that allow you to
|
||||
host Perspective using server-side Python.
|
||||
|
||||
## PyPI
|
||||
|
||||
`perspective-python` can be installed from [PyPI](https://pypi.org) via `pip`:
|
||||
|
||||
```bash
|
||||
pip install perspective-python
|
||||
```
|
||||
|
||||
That's it! If JupyterLab is installed in this Python environment, you'll also
|
||||
get the `perspective.widget.PerspectiveWidget` class when you import
|
||||
`perspective` in a Jupyter Lab kernel.
|
||||
|
||||
<!--
|
||||
### Anaconda
|
||||
|
||||
`perspective-python` can also be installed for [Anaconda](https://anaconda.org/)
|
||||
via [Conda Forge](https://conda-forge.org)
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge perspective
|
||||
``` -->
|
||||
@@ -0,0 +1,64 @@
|
||||
# Joining Tables
|
||||
|
||||
`perspective.join()` creates a read-only `Table` by joining two source tables on
|
||||
a shared key column. The result is reactive — it updates automatically when
|
||||
either source table changes. See [`Join`](../../explanation/join.md) for
|
||||
conceptual details.
|
||||
|
||||
## Basic Inner Join
|
||||
|
||||
```python
|
||||
orders = perspective.table([
|
||||
{"id": 1, "product_id": 101, "qty": 5},
|
||||
{"id": 2, "product_id": 102, "qty": 3},
|
||||
{"id": 3, "product_id": 101, "qty": 7},
|
||||
])
|
||||
|
||||
products = perspective.table([
|
||||
{"product_id": 101, "name": "Widget"},
|
||||
{"product_id": 102, "name": "Gadget"},
|
||||
])
|
||||
|
||||
joined = perspective.join(orders, products, "product_id")
|
||||
view = joined.view()
|
||||
json = view.to_json()
|
||||
```
|
||||
|
||||
## Join Types
|
||||
|
||||
Pass `join_type` to select inner, left, or outer join behavior:
|
||||
|
||||
```python
|
||||
# Left join: all left rows, nulls for unmatched right columns
|
||||
left_joined = perspective.join(left, right, "id", join_type="left")
|
||||
|
||||
# Outer join: all rows from both tables
|
||||
outer_joined = perspective.join(left, right, "id", join_type="outer")
|
||||
```
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
The joined table recomputes automatically when either source table is updated:
|
||||
|
||||
```python
|
||||
left = perspective.table([{"id": 1, "x": 10}])
|
||||
right = perspective.table([{"id": 2, "y": "b"}])
|
||||
|
||||
joined = perspective.join(left, right, "id")
|
||||
view = joined.view()
|
||||
|
||||
json = view.to_json()
|
||||
# [] — no matching keys yet
|
||||
|
||||
right.update([{"id": 1, "y": "a"}])
|
||||
json = view.to_json()
|
||||
# [{"id": 1, "x": 10, "y": "a"}] — new match detected
|
||||
```
|
||||
|
||||
## Async Client
|
||||
|
||||
The async client has the same API:
|
||||
|
||||
```python
|
||||
joined = await client.join(orders, products, "product_id", join_type="left")
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# `PerspectiveWidget` for JupyterLab
|
||||
|
||||
Building on top of the API provided by `perspective.Table`, the
|
||||
`PerspectiveWidget` is a JupyterLab plugin that offers the entire functionality
|
||||
of Perspective within the Jupyter environment. It supports the same API
|
||||
semantics of `<perspective-viewer>`, along with the additional data types
|
||||
supported by `perspective.Table`. `PerspectiveWidget` takes keyword arguments
|
||||
for the managed `View`:
|
||||
|
||||
```python
|
||||
from perspective.widget import PerspectiveWidget
|
||||
w = perspective.PerspectiveWidget(
|
||||
data,
|
||||
plugin="X Bar",
|
||||
aggregates={"datetime": "any"},
|
||||
sort=[["date", "desc"]]
|
||||
)
|
||||
```
|
||||
|
||||
## Creating a widget
|
||||
|
||||
A widget is created through the `PerspectiveWidget` constructor, which takes as
|
||||
its first, required parameter a `perspective.Table`, a dataset, a schema, or
|
||||
`None`, which serves as a special value that tells the Widget to defer loading
|
||||
any data until later. In maintaining consistency with the Javascript API,
|
||||
Widgets cannot be created with empty dictionaries or lists — `None` should be
|
||||
used if the intention is to await data for loading later on. A widget can be
|
||||
constructed from a dataset:
|
||||
|
||||
```python
|
||||
from perspective.widget import PerspectiveWidget
|
||||
PerspectiveWidget(data, group_by=["date"])
|
||||
```
|
||||
|
||||
.. or a schema:
|
||||
|
||||
```python
|
||||
PerspectiveWidget({"a": int, "b": str})
|
||||
```
|
||||
|
||||
.. or an instance of a `perspective.Table`:
|
||||
|
||||
```python
|
||||
table = perspective.table(data)
|
||||
PerspectiveWidget(table)
|
||||
```
|
||||
|
||||
## Updating a widget
|
||||
|
||||
`PerspectiveWidget` shares a similar API to the `<perspective-viewer>` Custom
|
||||
Element, and has similar `save()` and `restore()` methods that
|
||||
serialize/deserialize UI state for the widget.
|
||||
|
||||
<!--
|
||||
## `PerspectiveRenderer`
|
||||
|
||||
Perspective also exposes a JS-only `mimerender-extension`. This lets you view
|
||||
`csv`, `json`, and `arrow` files directly from the file browser. You can see
|
||||
this by right clicking one of these files and `Open With->CSVPerspective` (or
|
||||
`JSONPerspective` or `ArrowPerspective`). Perspective will also install itself
|
||||
as the default handler for opening `.arrow` files. -->
|
||||
@@ -0,0 +1,67 @@
|
||||
# Multi-threading
|
||||
|
||||
Perspective's API is thread-safe, so methods may be called from different
|
||||
threads without additional consideration for safety/exclusivity/correctness. All
|
||||
`perspective.Client` and `perspective.Server` API methods release the GIL, which
|
||||
can be exploited for parallelism.
|
||||
|
||||
Interally, `perspective.Server` also dispatches to a thread pool for some
|
||||
operations, enabling better parallelism and overall better query performance.
|
||||
This independent threadpool size can be controlled via
|
||||
`perspective.set_num_cpus()`, or the `OMP_NUM_THREADS` environment variable.
|
||||
|
||||
```python
|
||||
import perspective
|
||||
|
||||
perspective.set_num_cpus(2)
|
||||
```
|
||||
|
||||
## Server handlers
|
||||
|
||||
Perspective's server handler implementations each take an optional `executor`
|
||||
constructor argument, which (when provided) will configure the handler to
|
||||
process WebSocket `Client` requests on a thread pool.
|
||||
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from tornado.web import Application
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
from perspective import Server
|
||||
|
||||
args = {"perspective_server": Server(), "executor": ThreadPoolExecutor()}
|
||||
|
||||
app = Application(
|
||||
[
|
||||
(r"/websocket", PerspectiveTornadoHandler, args),
|
||||
|
||||
# ...
|
||||
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## `on_poll_request`
|
||||
|
||||
`on_poll_request` is an optional keyword argument for `Server()`, which which
|
||||
can be applied in cases where overlapping `Table.update` calls can be safely
|
||||
deferred.
|
||||
|
||||
When providing a callback function to `on_poll_request`, the `Server` will
|
||||
invoke your callback when there are updates that need to be flushed, after which
|
||||
you must _eventually_ call `Server.poll` (or else no updates will be processed).
|
||||
|
||||
The exact implementation of `on_poll_request` will depend on the context. A
|
||||
simple example which batches calls via `threading.Lock`:
|
||||
|
||||
```python
|
||||
lock = threading.Lock()
|
||||
|
||||
def on_poll_request(perspective_server):
|
||||
if lock.acquire(blocking=False):
|
||||
try:
|
||||
perspective_server.poll()
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
server = Server(on_poll_request=on_poll_request)
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Loading data into a Table
|
||||
|
||||
A `Table` can be created from a dataset or a schema, the specifics of which are
|
||||
[discussed](#loading-data-with-table) in the JavaScript section of the user's
|
||||
guide. In Python, however, Perspective supports additional data types that are
|
||||
commonly used when processing data:
|
||||
|
||||
- `pandas.DataFrame`
|
||||
- `polars.DataFrame`
|
||||
- `bytes` (encoding an Apache Arrow)
|
||||
- `objects` (either extracting a repr or via reference)
|
||||
- `str` (encoding as a CSV)
|
||||
|
||||
A `Table` is created in a similar fashion to its JavaScript equivalent:
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import perspective
|
||||
|
||||
data = pd.DataFrame({
|
||||
"int": np.arange(100),
|
||||
"float": [i * 1.5 for i in range(100)],
|
||||
"bool": [True for i in range(100)],
|
||||
"date": [date.today() for i in range(100)],
|
||||
"datetime": [datetime.now() for i in range(100)],
|
||||
"string": [str(i) for i in range(100)]
|
||||
})
|
||||
|
||||
table = perspective.table(data, index="float")
|
||||
```
|
||||
|
||||
Likewise, a `View` can be created via the `view()` method:
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["float"], filter=[["bool", "==", True]])
|
||||
column_data = view.to_columns()
|
||||
row_data = view.to_json()
|
||||
```
|
||||
|
||||
## Polars Support
|
||||
|
||||
Polars `DataFrame` types work similarly to Apache Arrow input, which Perspective
|
||||
uses to interface with Polars.
|
||||
|
||||
```python
|
||||
df = polars.DataFrame({"a": [1,2,3,4,5]})
|
||||
table = perspective.table(df)
|
||||
```
|
||||
|
||||
## Pandas Support
|
||||
|
||||
Perspective's `Table` can be constructed from `pandas.DataFrame` objects.
|
||||
Internally, this just uses
|
||||
[`pyarrow::from_pandas`](https://arrow.apache.org/docs/python/pandas.html),
|
||||
which dictates behavior of this feature including type support.
|
||||
|
||||
If the dataframe does not have an index set, an integer-typed column named
|
||||
`"index"` is created. If you want to preserve the indexing behavior of the
|
||||
dataframe passed into Perspective, simply create the `Table` with
|
||||
`index="index"` as a keyword argument. This tells Perspective to once again
|
||||
treat the index as a primary key:
|
||||
|
||||
```python
|
||||
data.set_index("datetime")
|
||||
table = perspective.table(data, index="index")
|
||||
```
|
||||
|
||||
## Time Zone Handling
|
||||
|
||||
When parsing `"datetime"` strings, times without an explicit timezone offset are
|
||||
interpreted as _UTC_. Strings with a timezone offset (e.g., `+05:00`) are
|
||||
converted to UTC. All `"datetime"` values are stored internally as milliseconds
|
||||
since the Unix epoch, and are _output_ as integer timestamps (milliseconds since
|
||||
epoch) from methods like `to_columns()` and `to_json()`.
|
||||
|
||||
Python `datetime` objects are serialized to strings before parsing. Naive
|
||||
`datetime` objects (without `tzinfo`) produce strings without timezone
|
||||
information and are therefore treated as UTC. Timezone-aware `datetime` objects
|
||||
include their offset in the serialized string, which is used to convert to UTC.
|
||||
|
||||
`"date"` values are timezone-agnostic calendar days with no time component.
|
||||
They are _output_ as integer timestamps at _UTC midnight_ of the calendar day
|
||||
(equivalent to Arrow `date32` day arithmetic), and integer timestamp _input_ to
|
||||
a `"date"` column is likewise interpreted as UTC. The host process timezone
|
||||
never affects `"date"` values — a `Viewer` renders them in UTC, recovering the
|
||||
stored calendar day exactly. Datetime expression functions such as
|
||||
`bucket("x", 'D')`, `day_of_week("x")` and `hour_of_day("x")` also compute in
|
||||
UTC.
|
||||
@@ -0,0 +1,81 @@
|
||||
# DataFrame and Arrow Compatibility
|
||||
|
||||
`perspective-python` accepts a `Table` constructor argument from any of the
|
||||
common Python columnar data libraries. In all three cases, `perspective.table`
|
||||
(and `Table.update()`) consume the input directly — there is no need to
|
||||
serialize to Apache Arrow IPC bytes yourself. However, note is
|
||||
still the most efficient way to bulk load data into `Table`.
|
||||
|
||||
## PyArrow
|
||||
|
||||
```python
|
||||
import pyarrow as pa
|
||||
import perspective
|
||||
|
||||
arrow_table = pa.table({
|
||||
"int": pa.array([1, 2, 3], type=pa.int64()),
|
||||
"float": pa.array([1.5, 2.5, 3.5], type=pa.float64()),
|
||||
"string": pa.array(["a", "b", "c"], type=pa.string()),
|
||||
})
|
||||
|
||||
table = perspective.table(arrow_table)
|
||||
```
|
||||
|
||||
The same applies to `Table.update()`:
|
||||
|
||||
```python
|
||||
table.update(arrow_table)
|
||||
```
|
||||
|
||||
If you have Arrow data already in IPC format (e.g. read from disk, received
|
||||
over the wire, or produced by another tool), pass the raw `bytes` directly —
|
||||
both stream and file formats are auto-detected:
|
||||
|
||||
```python
|
||||
with open("data.arrow", "rb") as f:
|
||||
table = perspective.table(f.read())
|
||||
```
|
||||
|
||||
## Polars
|
||||
|
||||
```python
|
||||
import polars as pl
|
||||
import perspective
|
||||
|
||||
df = pl.DataFrame({
|
||||
"a": [1, 2, 3, 4, 5],
|
||||
"b": ["x", "y", "z", "x", "y"],
|
||||
})
|
||||
|
||||
table = perspective.table(df)
|
||||
```
|
||||
|
||||
Internally, the `DataFrame` is converted to a `pyarrow.Table` before
|
||||
ingestion, so Polars columns inherit the Arrow type mapping above.
|
||||
|
||||
See also Perspective [Virtual Server support for `polars.DataFrame`](./virtual_server/polars.md)
|
||||
|
||||
## Pandas
|
||||
|
||||
`pandas.DataFrame` is supported via `pyarrow.Table.from_pandas`, which
|
||||
dictates behavior including type support — see the
|
||||
[pyarrow pandas docs](https://arrow.apache.org/docs/python/pandas.html) for
|
||||
details on which pandas dtypes round-trip cleanly.
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import perspective
|
||||
|
||||
data = pd.DataFrame({
|
||||
"int": np.arange(100),
|
||||
"float": [i * 1.5 for i in range(100)],
|
||||
"bool": [True for i in range(100)],
|
||||
"date": [date.today() for i in range(100)],
|
||||
"datetime": [datetime.now() for i in range(100)],
|
||||
"string": [str(i) for i in range(100)],
|
||||
})
|
||||
|
||||
table = perspective.table(data, index="float")
|
||||
```
|
||||
@@ -0,0 +1,20 @@
|
||||
# Virtual Servers
|
||||
|
||||
Perspective's Virtual Server feature lets you connect `<perspective-viewer>` to
|
||||
external data sources without loading data into Perspective's built-in engine.
|
||||
Instead, queries are translated and executed natively by the external database.
|
||||
|
||||
For a detailed explanation of how virtual servers work, see the
|
||||
[Virtual Servers](../../explanation/virtual_servers.md) concepts page.
|
||||
|
||||
Perspective ships with built-in virtual server implementations for:
|
||||
|
||||
- [**DuckDB**](./virtual_server/duckdb.md) — query DuckDB databases using the
|
||||
`duckdb` Python package.
|
||||
- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server
|
||||
using the `clickhouse-connect` Python package.
|
||||
- [**Polars**](./virtual_server/polars.md) — query in-memory Polars DataFrames
|
||||
using the `polars` Python package.
|
||||
|
||||
You can also [**implement your own**](./virtual_server/custom.md) virtual server
|
||||
to connect Perspective to any data source by subclassing `VirtualServerHandler`.
|
||||
@@ -0,0 +1,52 @@
|
||||
# ClickHouse Virtual Server
|
||||
|
||||
Perspective provides a built-in virtual server for
|
||||
[ClickHouse](https://clickhouse.com/), allowing `<perspective-viewer>` clients
|
||||
to query a ClickHouse server over WebSocket.
|
||||
|
||||
For browser-only usage, see the
|
||||
[JavaScript ClickHouse guide](../../javascript/virtual_server/clickhouse.md).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install perspective-python clickhouse-connect
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Create a server that exposes ClickHouse tables to browser clients:
|
||||
|
||||
```python
|
||||
import clickhouse_connect
|
||||
import tornado.web
|
||||
import tornado.ioloop
|
||||
from perspective import ClickhouseVirtualServer
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
|
||||
# Connect to ClickHouse
|
||||
client = clickhouse_connect.get_client(host="localhost")
|
||||
|
||||
# Create virtual server backed by ClickHouse
|
||||
server = ClickhouseVirtualServer(client)
|
||||
|
||||
# Serve over WebSocket
|
||||
app = tornado.web.Application([
|
||||
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
|
||||
])
|
||||
|
||||
app.listen(8080)
|
||||
tornado.ioloop.IOLoop.current().start()
|
||||
```
|
||||
|
||||
Connect from the browser:
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080/websocket");
|
||||
const table = await websocket.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Python ClickHouse example](https://github.com/perspective-dev/perspective/tree/master/examples/python-clickhouse-virtual)
|
||||
@@ -0,0 +1,64 @@
|
||||
# Implementing a custom Virtual Server
|
||||
|
||||
You can connect Perspective to any data source by subclassing
|
||||
`VirtualServerHandler` and wrapping it with `VirtualServer`.
|
||||
|
||||
For background on virtual servers, see the
|
||||
[Virtual Servers overview](../../../explanation/virtual_servers.md).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from perspective import VirtualServerHandler, VirtualServer
|
||||
|
||||
class MyModel(VirtualServerHandler):
|
||||
def get_features(self):
|
||||
return {
|
||||
"group_by": True,
|
||||
"split_by": False,
|
||||
"sort": True,
|
||||
"filter_ops": {
|
||||
"string": ["==", "!=", "contains"],
|
||||
"float": ["==", "!=", ">", "<"],
|
||||
},
|
||||
"aggregates": {
|
||||
"float": ["sum", "avg", "count"],
|
||||
"string": ["count"],
|
||||
},
|
||||
}
|
||||
|
||||
def get_hosted_tables(self):
|
||||
return ["my_table"]
|
||||
|
||||
def table_schema(self, table_name):
|
||||
return {"name": "string", "price": "float"}
|
||||
|
||||
def table_size(self, table_name):
|
||||
return 1000
|
||||
|
||||
def table_make_view(self, table_name, view_id, config):
|
||||
# Translate `config` (group_by, sort, filter, etc.) into a
|
||||
# query against your data source. Store the query keyed by
|
||||
# `view_id` for later data retrieval.
|
||||
pass
|
||||
|
||||
def view_delete(self, view_id):
|
||||
# Clean up resources for this view
|
||||
pass
|
||||
|
||||
def view_get_data(self, view_id, start_row, end_row, start_col, end_col, ctx):
|
||||
# Execute the stored query with the given row/column window.
|
||||
# Push results via `ctx`.
|
||||
pass
|
||||
```
|
||||
|
||||
The `VirtualServer` instance can then be passed to a Tornado, Starlette, or
|
||||
AIOHTTP handler just like a regular `Server`:
|
||||
|
||||
```python
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
|
||||
app = tornado.web.Application([
|
||||
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": VirtualServer(MyModel)}),
|
||||
])
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# DuckDB Virtual Server
|
||||
|
||||
Perspective provides a built-in virtual server for
|
||||
[DuckDB](https://duckdb.org/), allowing `<perspective-viewer>` clients to query
|
||||
a server-side DuckDB database over WebSocket.
|
||||
|
||||
For browser-only usage via DuckDB-WASM, see the
|
||||
[JavaScript DuckDB guide](../../javascript/virtual_server/duckdb.md).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install perspective-python duckdb
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Create a server that exposes a DuckDB database to browser clients:
|
||||
|
||||
```python
|
||||
import duckdb
|
||||
import tornado.web
|
||||
import tornado.ioloop
|
||||
from perspective import DuckDBVirtualServer
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
|
||||
# Create DuckDB connection and load data
|
||||
conn = duckdb.connect()
|
||||
conn.execute("CREATE TABLE my_table AS SELECT * FROM 'data.parquet'")
|
||||
|
||||
# Create virtual server backed by DuckDB
|
||||
server = DuckDBVirtualServer(conn)
|
||||
|
||||
# Serve over WebSocket
|
||||
app = tornado.web.Application([
|
||||
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
|
||||
])
|
||||
|
||||
app.listen(8080)
|
||||
tornado.ioloop.IOLoop.current().start()
|
||||
```
|
||||
|
||||
Connect from the browser:
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080/websocket");
|
||||
const table = await websocket.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Python DuckDB example](https://github.com/perspective-dev/perspective/tree/master/examples/python-duckdb-virtual)
|
||||
@@ -0,0 +1,49 @@
|
||||
# Polars Virtual Server
|
||||
|
||||
Perspective provides a built-in virtual server for
|
||||
[Polars](https://pola.rs/), allowing `<perspective-viewer>` clients to query
|
||||
in-memory Polars DataFrames over WebSocket.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install perspective-python polars
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Create a server that exposes Polars DataFrames to browser clients:
|
||||
|
||||
```python
|
||||
import polars as pl
|
||||
import tornado.web
|
||||
import tornado.ioloop
|
||||
from perspective.virtual_servers.polars import PolarsVirtualServer
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
|
||||
# Load data into Polars DataFrames
|
||||
df = pl.read_parquet("data.parquet")
|
||||
|
||||
# Create virtual server backed by Polars (dict of name -> DataFrame)
|
||||
server = PolarsVirtualServer({"my_table": df})
|
||||
|
||||
# Serve over WebSocket
|
||||
app = tornado.web.Application([
|
||||
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
|
||||
])
|
||||
|
||||
app.listen(8080)
|
||||
tornado.ioloop.IOLoop.current().start()
|
||||
```
|
||||
|
||||
Connect from the browser:
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080/websocket");
|
||||
const table = await websocket.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Python Polars example](https://github.com/perspective-dev/perspective/tree/master/examples/python-polars-virtual)
|
||||
@@ -0,0 +1,129 @@
|
||||
# Hosting a WebSocket server
|
||||
|
||||
An in-memory `Server` "hosts" all `perspective.Table` and `perspective.View`
|
||||
instances created by its connected `Client`s. Hosted tables/views can have their
|
||||
methods called from other sources than the Python server, i.e. by a
|
||||
`perspective-viewer` running in a JavaScript client over the network,
|
||||
interfacing with `perspective-python` through the websocket API.
|
||||
|
||||
The server has full control of all hosted `Table` and `View` instances, and can
|
||||
call any public API method on hosted instances. This makes it extremely easy to
|
||||
stream data to a hosted `Table` using `.update()`:
|
||||
|
||||
```python
|
||||
server = perspective.Server()
|
||||
client = server.new_local_client()
|
||||
table = client.table(data, name="data_source")
|
||||
|
||||
for i in range(10):
|
||||
# updates continue to propagate automatically
|
||||
table.update(new_data)
|
||||
```
|
||||
|
||||
The `name` provided is important, as it enables Perspective in JavaScript to
|
||||
look up a `Table` and get a handle to it over the network. Otherwise, `name`
|
||||
will be assigned randomly and the `Client` must look this up with
|
||||
`Client.get_hosted_table_names()`
|
||||
|
||||
## Client/Server Replicated Mode
|
||||
|
||||
Using Tornado and
|
||||
[`PerspectiveTornadoHandler`](python.md#perspectivetornadohandler), as well as
|
||||
`Perspective`'s JavaScript library, we can set up "distributed" Perspective
|
||||
instances that allows multiple browser `perspective-viewer` clients to read from
|
||||
a common `perspective-python` server, as in the
|
||||
[Tornado Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-tornado).
|
||||
|
||||
This architecture works by maintaining two `Tables`—one on the server, and one
|
||||
on the client that mirrors the server's `Table` automatically using `on_update`.
|
||||
All updates to the table on the server are automatically applied to each client,
|
||||
which makes this architecture a natural fit for streaming dashboards and other
|
||||
distributed use-cases. In conjunction with [multithreading](#multi-threading),
|
||||
distributed Perspective offers consistently high performance over large numbers
|
||||
of clients and large datasets.
|
||||
|
||||
_*server.py*_
|
||||
|
||||
```python
|
||||
from perspective import Server
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
|
||||
# Create an instance of Server, and host a Table
|
||||
SERVER = Server()
|
||||
CLIENT = SERVER.new_local_client()
|
||||
|
||||
# The Table is exposed at `localhost:8888/websocket` with the name `data_source`
|
||||
client.table(data, name = "data_source")
|
||||
|
||||
app = tornado.web.Application([
|
||||
# create a websocket endpoint that the client JavaScript can access
|
||||
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": SERVER})
|
||||
])
|
||||
|
||||
# Start the Tornado server
|
||||
app.listen(8888)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
loop.start()
|
||||
```
|
||||
|
||||
Instead of calling `load(server_table)`, create a `View` using `server_table`
|
||||
and pass that into `viewer.load()`. This will automatically register an
|
||||
`on_update` callback that synchronizes state between the server and the client.
|
||||
|
||||
_*index.html*_
|
||||
|
||||
```html
|
||||
<perspective-viewer id="viewer" editable></perspective-viewer>
|
||||
|
||||
<script type="module">
|
||||
// Create a client that expects a Perspective server
|
||||
// to accept connections at the specified URL.
|
||||
const websocket = await perspective.websocket(
|
||||
"ws://localhost:8888/websocket",
|
||||
);
|
||||
|
||||
// Get a handle to the Table on the server
|
||||
const server_table = await websocket.open_table("data_source_one");
|
||||
|
||||
// Create a new view
|
||||
const server_view = await table.view();
|
||||
|
||||
// Create a Table on the client using `perspective.worker()`
|
||||
const worker = await perspective.worker();
|
||||
const client_table = await worker.table(view);
|
||||
|
||||
// Load the client table in the `<perspective-viewer>`.
|
||||
document.getElementById("viewer").load(client_table);
|
||||
</script>
|
||||
```
|
||||
|
||||
For a more complex example that offers distributed editing of the server
|
||||
dataset, see
|
||||
[client_server_editing.html](https://github.com/perspective-dev/perspective/blob/master/examples/python-tornado/client_server_editing.html).
|
||||
|
||||
We also provide examples for Starlette/FastAPI and AIOHTTP:
|
||||
|
||||
- [Starlette Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-starlette).
|
||||
- [AIOHTTP Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-aiohttp).
|
||||
|
||||
## Server-only Mode
|
||||
|
||||
The server setup is identical to
|
||||
[Client/Server Replicated Mode](#client-server-replicated-mode) above, but
|
||||
instead of creating a `View`, the client calls `load(server_table)`: In Python,
|
||||
use `Server` and `PerspectiveTornadoHandler` to create a websocket server that
|
||||
exposes a `Table`. In this example, `table` is a proxy for the `Table` we
|
||||
created on the server. All API methods are available on _proxies_, e.g.
|
||||
calling `view()`, `schema()`, `update()` on `table` will pass those operations
|
||||
to the Python `Table`, execute the commands, and return the result back to
|
||||
Javascript.
|
||||
|
||||
```html
|
||||
<perspective-viewer id="viewer" editable></perspective-viewer>
|
||||
```
|
||||
|
||||
```javascript
|
||||
const websocket = perspective.websocket("ws://localhost:8888/websocket");
|
||||
const table = websocket.open_table("data_source");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
# Rust
|
||||
|
||||
Install via `cargo`:
|
||||
|
||||
```bash
|
||||
cargo add perspective
|
||||
```
|
||||
|
||||
# Example
|
||||
|
||||
Initialize a server and client
|
||||
|
||||
```rust
|
||||
let server = Server::default();
|
||||
let client = server.new_local_client();
|
||||
```
|
||||
|
||||
Load an Arrow
|
||||
|
||||
```rust
|
||||
let mut file = File::open(std::path::Path::new(ROOT_PATH).join(ARROW_FILE_PATH))?;
|
||||
let mut feather = Vec::with_capacity(file.metadata()?.len() as usize);
|
||||
file.read_to_end(&mut feather)?;
|
||||
let data = UpdateData::Arrow(feather.into());
|
||||
let mut options = TableInitOptions::default();
|
||||
options.set_name("my_data_source");
|
||||
client.table(data.into(), options).await?;
|
||||
```
|
||||
|
||||
# Joining Tables
|
||||
|
||||
`Client::join` creates a read-only `Table` by joining two source tables on a
|
||||
shared key column. The result is reactive — it updates automatically when
|
||||
either source table changes. See [`Join`](../explanation/join.md) for
|
||||
conceptual details.
|
||||
|
||||
```rust
|
||||
let orders = client.table(
|
||||
TableData::Update(UpdateData::JsonRows(
|
||||
"[{\"id\":1,\"product_id\":101,\"qty\":5},{\"id\":2,\"product_id\":102,\"qty\":3}]".into(),
|
||||
)),
|
||||
TableInitOptions::default(),
|
||||
).await?;
|
||||
|
||||
let products = client.table(
|
||||
TableData::Update(UpdateData::JsonRows(
|
||||
"[{\"product_id\":101,\"name\":\"Widget\"},{\"product_id\":102,\"name\":\"Gadget\"}]".into(),
|
||||
)),
|
||||
TableInitOptions::default(),
|
||||
).await?;
|
||||
|
||||
let joined = client.join(
|
||||
(&orders).into(),
|
||||
(&products).into(),
|
||||
"product_id",
|
||||
JoinOptions::default(),
|
||||
).await?;
|
||||
|
||||
let view = joined.view(None).await?;
|
||||
let json = view.to_json().await?;
|
||||
```
|
||||
|
||||
Use `JoinOptions` to configure the join type, table name, or `right_on` column:
|
||||
|
||||
```rust
|
||||
let options = JoinOptions {
|
||||
join_type: Some(JoinType::Left),
|
||||
name: Some("orders_with_products".into()),
|
||||
right_on: None,
|
||||
};
|
||||
|
||||
let joined = client.join(
|
||||
(&orders).into(),
|
||||
(&products).into(),
|
||||
"product_id",
|
||||
options,
|
||||
).await?;
|
||||
```
|
||||
|
||||
You can also join by table name strings instead of `Table` references:
|
||||
|
||||
```rust
|
||||
let joined = client.join(
|
||||
"orders".into(),
|
||||
"products".into(),
|
||||
"product_id",
|
||||
JoinOptions::default(),
|
||||
).await?;
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# JavaScript
|
||||
|
||||
Overview and module structure for Perspective's JavaScript packages.
|
||||
@@ -0,0 +1,15 @@
|
||||
perspective-viewer {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
div.javascript:before {
|
||||
content: "JavaScript:";
|
||||
}
|
||||
|
||||
div.python:before {
|
||||
content: "Python:";
|
||||
}
|
||||
|
||||
div.rust:before {
|
||||
content: "Rust:";
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
import perspective from "https://cdn.jsdelivr.net/npm/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
const WASM_URL =
|
||||
"https://cdn.jsdelivr.net/npm/superstore-arrow/superstore.lz4.arrow";
|
||||
|
||||
const worker = await perspective.worker();
|
||||
|
||||
const table = await fetch(WASM_URL)
|
||||
.then((x) => x.arrayBuffer())
|
||||
.then((x) => worker.table(x));
|
||||
|
||||
for (const viewer of document.querySelectorAll("perspective-viewer")) {
|
||||
viewer.load(table);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{{#include ../../README.md}}
|
||||
@@ -0,0 +1,109 @@
|
||||
# Tutorial: A tornado server in Python
|
||||
|
||||
Perspective ships with a pre-built Tornado handler that makes integration with
|
||||
`tornado.websockets` extremely easy. This allows you to run an instance of
|
||||
`Perspective` on a server using Python, open a websocket to a `Table`, and
|
||||
access the `Table` in JavaScript and through `<perspective-viewer>`. All
|
||||
instructions sent to the `Table` are processed in Python, which executes the
|
||||
commands, and returns its output through the websocket back to Javascript.
|
||||
|
||||
### Python setup
|
||||
|
||||
Make sure Perspective and Tornado are installed!
|
||||
|
||||
```bash
|
||||
pip install perspective-python tornado
|
||||
```
|
||||
|
||||
To use the handler, we need to first have a `Server`, a `Client` and an instance
|
||||
of a `Table`:
|
||||
|
||||
```python
|
||||
import perspective
|
||||
|
||||
SERVER = perspective.Server()
|
||||
CLIENT = SERVER.new_local_client()
|
||||
```
|
||||
|
||||
Once the server has been created, create a `Table` instance with a name. The
|
||||
name that you host the table under is important — it acts as a unique accessor
|
||||
on the JavaScript side, which will look for a Table hosted at the websocket with
|
||||
the name you specify.
|
||||
|
||||
```python
|
||||
TABLE = client.table(data, name="data_source_one")
|
||||
```
|
||||
|
||||
After the server and table setup is complete, create a websocket endpoint and
|
||||
provide it a reference to `PerspectiveTornadoHandler`. You must provide the
|
||||
configuration object in the route tuple, and it must contain
|
||||
`"perspective_server"`, which is a reference to the `Server` you just created.
|
||||
|
||||
```python
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
|
||||
app = tornado.web.Application([
|
||||
|
||||
# ... other handlers ...
|
||||
|
||||
# Create a websocket endpoint that the client JavaScript can access
|
||||
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": SERVER, "check_origin": True})
|
||||
])
|
||||
```
|
||||
|
||||
Optionally, the configuration object can also include `check_origin`, a boolean
|
||||
that determines whether the websocket accepts requests from origins other than
|
||||
where the server is hosted. See
|
||||
[Tornado docs](https://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler.check_origin)
|
||||
for more details.
|
||||
|
||||
### JavaScript setup
|
||||
|
||||
Once the server is up and running, you can access the Table you just hosted
|
||||
using `perspective.websocket` and `open_table()`. First, create a client that
|
||||
expects a Perspective server to accept connections at the specified URL:
|
||||
|
||||
```javascript
|
||||
import "@perspective-dev/viewer";
|
||||
import "@perspective-dev/viewer-datagrid";
|
||||
import perspective from "@perspective-dev/client";
|
||||
|
||||
const websocket = await perspective.websocket("ws://localhost:8888/websocket");
|
||||
```
|
||||
|
||||
Next open the `Table` we created on the server by name:
|
||||
|
||||
```javascript
|
||||
const table = await websocket.open_table("data_source_one");
|
||||
```
|
||||
|
||||
`table` is a proxy for the `Table` we created on the server. All operations that
|
||||
are possible through the JavaScript API are possible on the Python API as well,
|
||||
thus calling `view()`, `schema()`, `update()` etc. on `const table` will pass
|
||||
those operations to the Python `Table`, execute the commands, and return the
|
||||
result back to JavaScript. Similarly, providing this `table` to a
|
||||
`<perspective-viewer>` instance will allow virtual rendering:
|
||||
|
||||
```javascript
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
viewer.style.height = "500px";
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(table);
|
||||
```
|
||||
|
||||
`perspective.websocket` expects a Websocket URL where it will send instructions.
|
||||
When `open_table` is called, the name to a hosted Table is passed through, and a
|
||||
request is sent through the socket to fetch the Table. No actual `Table`
|
||||
instance is passed inbetween the runtimes; all instructions are proxied through
|
||||
websockets.
|
||||
|
||||
This provides for great flexibility — while `Perspective.js` is full of
|
||||
features, browser WebAssembly runtimes currently have some performance
|
||||
restrictions on memory and CPU feature utilization, and the architecture in
|
||||
general suffers when the dataset itself is too large to download to the client
|
||||
in full.
|
||||
|
||||
The Python runtime does not suffer from memory limitations, utilizes Apache
|
||||
Arrow internal threadpools for threading and parallel processing, and generates
|
||||
architecture optimized code, which currently makes it more suitable as a
|
||||
server-side runtime than `node.js`.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@perspective-dev/docs",
|
||||
"version": "4.5.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "npm run mdbook && node build.mjs && node build.config.mjs",
|
||||
"start": "node server.mjs",
|
||||
"serve": "node server.mjs",
|
||||
"clean": "rm -rf dist",
|
||||
"deploy": "node deploy.mjs",
|
||||
"mdbook": "docker compose run --rm mdbook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@perspective-dev/viewer-charts": "workspace:",
|
||||
"@perspective-dev/viewer-datagrid": "workspace:",
|
||||
"@perspective-dev/viewer": "workspace:",
|
||||
"@perspective-dev/client": "workspace:",
|
||||
"@perspective-dev/server": "workspace:",
|
||||
"blocks": "workspace:",
|
||||
"puppeteer": "catalog:",
|
||||
"prismjs": "^1.29.0",
|
||||
"superstore-arrow": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@zip.js/zip.js": "catalog:",
|
||||
"esbuild": "catalog:",
|
||||
"lightningcss": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 { WebSocketServer } from "@perspective-dev/client";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
new WebSocketServer({
|
||||
assets: [
|
||||
`${__dirname}/dist`,
|
||||
`${__dirname}/static`,
|
||||
`${__dirname}/../node_modules`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
|
||||
/>
|
||||
<title>Perspective - Block</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Interactive analytics and data visualization component for large and streaming datasets."
|
||||
/>
|
||||
<link rel="icon" href="https://openjsf.org/favicon.ico" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container">
|
||||
<nav class="navbar">
|
||||
<div class="navbar__inner">
|
||||
<a href="/" class="navbar__logo"><img alt="Perspective" src="/svg/perspective-logo-dark.svg" /></a>
|
||||
<ul class="navbar__links">
|
||||
<li><a href="/guide/">Docs</a></li>
|
||||
<li><a href="/examples.html">Examples</a></li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/perspective-dev/perspective"
|
||||
>GitHub</a
|
||||
>
|
||||
</li>
|
||||
<li id="theme-toggle"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-wrapper">
|
||||
<div class="block-detail" id="block-detail"></div>
|
||||
<br /><br />
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer__inner">
|
||||
<img
|
||||
class="footer__logo"
|
||||
id="footer-logo"
|
||||
alt="OpenJS Foundation Logo"
|
||||
src="/img/openjs_foundation-logo-horizontal-white.png"
|
||||
/>
|
||||
<div class="footer__copyright">
|
||||
<br /><br />Copyright © 2017 OpenJS Foundation and
|
||||
Perspective contributors.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script type="module" src="block.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import { initTheme, createThemeToggle } from "./components/theme.js";
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-json";
|
||||
import "prismjs/components/prism-markdown";
|
||||
import "prismjs/components/prism-css";
|
||||
|
||||
initTheme();
|
||||
document.getElementById("theme-toggle")!.replaceWith(createThemeToggle());
|
||||
|
||||
const EXT_TO_LANG: Record<string, string> = {
|
||||
js: "javascript",
|
||||
mjs: "javascript",
|
||||
ts: "javascript",
|
||||
html: "markup",
|
||||
css: "css",
|
||||
json: "json",
|
||||
md: "markdown",
|
||||
};
|
||||
|
||||
const container = document.getElementById("block-detail")!;
|
||||
const params = new URL(document.location.href).searchParams;
|
||||
const example = params.get("example");
|
||||
|
||||
if (!example) {
|
||||
container.innerHTML = "<p>No example specified.</p>";
|
||||
} else {
|
||||
document.title = `Perspective - ${example}`;
|
||||
|
||||
const h1 = document.createElement("h1");
|
||||
h1.textContent = example;
|
||||
container.appendChild(h1);
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.width = "960";
|
||||
iframe.height = "640";
|
||||
iframe.src = `/blocks/${example}/index.html`;
|
||||
container.appendChild(iframe);
|
||||
|
||||
const br = document.createElement("br");
|
||||
container.appendChild(br);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = `/blocks/${example}/index.html`;
|
||||
link.className = "block-detail__link";
|
||||
link.textContent = "Open in New Tab";
|
||||
link.target = "_blank";
|
||||
container.appendChild(link);
|
||||
|
||||
const br2 = document.createElement("br");
|
||||
container.appendChild(br2);
|
||||
|
||||
// Fetch manifest and display all source files
|
||||
fetch("/blocks/manifest.json")
|
||||
.then((res) => res.json())
|
||||
.then(async (manifest: Record<string, string[]>) => {
|
||||
const files = manifest[example] || [];
|
||||
for (const filename of files) {
|
||||
const res = await fetch(`/blocks/${example}/${filename}`);
|
||||
if (!res.ok) continue;
|
||||
const contents = await res.text();
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "block-detail__file-title";
|
||||
title.textContent = filename;
|
||||
container.appendChild(title);
|
||||
|
||||
const pre = document.createElement("pre");
|
||||
const code = document.createElement("code");
|
||||
const ext = filename.split(".").pop() || "";
|
||||
const lang = EXT_TO_LANG[ext] || "plain";
|
||||
const grammar = Prism.languages[lang];
|
||||
if (grammar) {
|
||||
code.innerHTML = Prism.highlight(contents, grammar, lang);
|
||||
} else {
|
||||
code.textContent = contents;
|
||||
}
|
||||
pre.appendChild(code);
|
||||
container.appendChild(pre);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 { random_row } from "../data/random.js";
|
||||
import { LAYOUTS } from "../data/layouts.js";
|
||||
import { getPerspectiveTheme } from "./theme.js";
|
||||
|
||||
let TABLE: any;
|
||||
let VIEWER: any;
|
||||
let FREQ = 100;
|
||||
let REALTIME_PAUSED = true;
|
||||
let selectedId = "sparkgrid";
|
||||
|
||||
function update(table: any, viewer: any) {
|
||||
if (!REALTIME_PAUSED && FREQ <= 189.9) {
|
||||
const viewport_height = document.documentElement.clientHeight;
|
||||
if (viewport_height - window.scrollY > 0) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
arr.push(random_row());
|
||||
}
|
||||
table.update(arr);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => update(table, viewer), FREQ);
|
||||
}
|
||||
|
||||
function select(viewer: any, id: string, extra: any = {}) {
|
||||
selectedId = id;
|
||||
viewer.restore({ ...LAYOUTS[id], ...extra });
|
||||
}
|
||||
|
||||
async function startStreaming(perspective: any, viewer: any) {
|
||||
const data = [];
|
||||
for (let x = 0; x < 1000; x++) {
|
||||
data.push(random_row());
|
||||
}
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const tbl = worker.table(data, { index: "id" });
|
||||
setTimeout(async () => {
|
||||
const table = await tbl;
|
||||
update(table, viewer);
|
||||
});
|
||||
|
||||
return tbl;
|
||||
}
|
||||
|
||||
export async function initDemo(container: HTMLElement) {
|
||||
const [perspectiveMod] = await Promise.all([
|
||||
import("../data/worker.js"),
|
||||
import("@perspective-dev/viewer"),
|
||||
import("@perspective-dev/viewer-datagrid"),
|
||||
import("@perspective-dev/viewer-charts"),
|
||||
]);
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "demo";
|
||||
|
||||
const viewer = document.createElement("perspective-viewer") as any;
|
||||
viewer.className = "nosuperstore";
|
||||
wrapper.appendChild(viewer);
|
||||
|
||||
const visButtons = document.createElement("div");
|
||||
visButtons.className = "demo__vis-buttons";
|
||||
|
||||
for (const key of Object.keys(LAYOUTS)) {
|
||||
const btn = document.createElement("div");
|
||||
btn.className = "demo__vis-button";
|
||||
if (key === selectedId) {
|
||||
btn.classList.add("demo__vis-button--active");
|
||||
}
|
||||
btn.id = key;
|
||||
btn.textContent = key;
|
||||
btn.addEventListener("mouseover", () => {
|
||||
visButtons
|
||||
.querySelectorAll(".demo__vis-button")
|
||||
.forEach((b) => b.classList.remove("demo__vis-button--active"));
|
||||
btn.classList.add("demo__vis-button--active");
|
||||
select(viewer, key);
|
||||
});
|
||||
visButtons.appendChild(btn);
|
||||
}
|
||||
|
||||
wrapper.appendChild(visButtons);
|
||||
|
||||
const timeControls = document.createElement("div");
|
||||
timeControls.className = "demo__time-controls";
|
||||
|
||||
const freqLabel = document.createElement("span");
|
||||
freqLabel.textContent =
|
||||
FREQ >= 189 ? "paused" : `${((1000 / FREQ) * 10).toFixed(0)} msg/s`;
|
||||
timeControls.appendChild(freqLabel);
|
||||
|
||||
const slider = document.createElement("input");
|
||||
slider.type = "range";
|
||||
slider.className = "demo__freq-slider";
|
||||
slider.setAttribute(
|
||||
"aria-label",
|
||||
"Demo update rate in messages per second",
|
||||
);
|
||||
slider.value = String(Math.round((FREQ - 190) * (5 / -9)));
|
||||
slider.addEventListener("input", () => {
|
||||
FREQ = (-9 / 5) * Number(slider.value) + 190;
|
||||
freqLabel.textContent =
|
||||
FREQ >= 189 ? "paused" : `${((1000 / FREQ) * 10).toFixed(0)} msg/s`;
|
||||
});
|
||||
timeControls.appendChild(slider);
|
||||
|
||||
wrapper.appendChild(timeControls);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
REALTIME_PAUSED = false;
|
||||
|
||||
if (TABLE === undefined) {
|
||||
TABLE = await startStreaming(perspectiveMod, viewer);
|
||||
}
|
||||
|
||||
VIEWER = viewer;
|
||||
VIEWER.load(TABLE);
|
||||
select(viewer, selectedId, { theme: getPerspectiveTheme() });
|
||||
}
|
||||
@@ -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 EXAMPLES from "../data/features.js";
|
||||
import { WORKER, SUPERSTORE_TABLE } from "../data/superstore.js";
|
||||
import { getColorMode, getPerspectiveTheme } from "./theme.js";
|
||||
|
||||
function showOverlay(index: number) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "gallery-overlay";
|
||||
|
||||
const viewer = document.createElement("perspective-viewer") as any;
|
||||
viewer.setAttribute("theme", getPerspectiveTheme());
|
||||
overlay.appendChild(viewer);
|
||||
|
||||
overlay.addEventListener("click", (event) => {
|
||||
if (event.target === overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
SUPERSTORE_TABLE.then((table: any) => {
|
||||
viewer.load(WORKER);
|
||||
viewer.restore({
|
||||
plugin: "Datagrid",
|
||||
table: "superstore",
|
||||
group_by: [],
|
||||
expressions: {},
|
||||
split_by: [],
|
||||
sort: [],
|
||||
aggregates: {},
|
||||
...EXAMPLES[index].config,
|
||||
settings: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface MontageMap {
|
||||
tile_width: number;
|
||||
tile_height: number;
|
||||
columns: number;
|
||||
order: number[];
|
||||
}
|
||||
|
||||
export async function initGallery(container: HTMLElement) {
|
||||
const resp = await fetch("/features/montage_map.json");
|
||||
const map: MontageMap = await resp.json();
|
||||
const rows = Math.ceil(map.order.length / map.columns);
|
||||
const isDark = getColorMode() === "dark";
|
||||
const img = document.createElement("img");
|
||||
img.alt = "Perspective feature gallery";
|
||||
img.src = `/features/montage${isDark ? "_dark" : "_light"}.png`;
|
||||
img.addEventListener("click", (event: MouseEvent) => {
|
||||
const col = Math.floor((event.offsetX / img.offsetWidth) * map.columns);
|
||||
const row = Math.floor((event.offsetY / img.offsetHeight) * rows);
|
||||
const tileIndex = row * map.columns + col;
|
||||
const featureIndex = map.order[tileIndex];
|
||||
if (featureIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
showOverlay(featureIndex);
|
||||
});
|
||||
|
||||
container.appendChild(img);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
|
||||
|
||||
const SUN_SVG = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></svg>`;
|
||||
const MOON_SVG = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></svg>`;
|
||||
|
||||
export function getColorMode(): "dark" | "light" {
|
||||
const stored = localStorage.getItem("perspective-theme");
|
||||
if (stored === "dark" || stored === "light") {
|
||||
return stored;
|
||||
}
|
||||
|
||||
return "dark";
|
||||
}
|
||||
|
||||
export function applyTheme(mode?: "dark" | "light") {
|
||||
const theme = mode ?? getColorMode();
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("perspective-theme", theme);
|
||||
|
||||
const viewer = document.querySelector(
|
||||
".demo perspective-viewer",
|
||||
) as HTMLPerspectiveViewerElement;
|
||||
viewer?.restore?.({ theme: theme === "dark" ? "Pro Dark" : "Pro Light" });
|
||||
|
||||
// Swap navbar logo
|
||||
const navLogo = document.querySelector(
|
||||
".navbar__logo img",
|
||||
) as HTMLImageElement | null;
|
||||
if (navLogo) {
|
||||
navLogo.src =
|
||||
theme === "dark"
|
||||
? "/svg/perspective-logo-dark.svg"
|
||||
: "/svg/perspective-logo-light.svg";
|
||||
}
|
||||
|
||||
// Swap footer logo
|
||||
const footerLogo = document.getElementById("footer-logo") as
|
||||
| HTMLImageElement
|
||||
| undefined;
|
||||
if (footerLogo) {
|
||||
footerLogo.src =
|
||||
theme === "dark"
|
||||
? "/img/openjs_foundation-logo-horizontal-white.png"
|
||||
: "/img/openjs_foundation-logo-horizontal-black.png";
|
||||
}
|
||||
|
||||
// Swap gallery montage
|
||||
const galleryImg = document.querySelector(
|
||||
".gallery img",
|
||||
) as HTMLImageElement | null;
|
||||
if (galleryImg) {
|
||||
galleryImg.src =
|
||||
theme === "dark"
|
||||
? "/features/montage_dark.png"
|
||||
: "/features/montage_light.png";
|
||||
}
|
||||
|
||||
// Update toggle button icon
|
||||
const toggleBtn = document.getElementById("theme-toggle-btn");
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = theme === "dark" ? SUN_SVG : MOON_SVG;
|
||||
toggleBtn.setAttribute(
|
||||
"aria-label",
|
||||
theme === "dark" ? "Switch to light mode" : "Switch to dark mode",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function initTheme() {
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
export function createThemeToggle(): HTMLElement {
|
||||
const li = document.createElement("li");
|
||||
const btn = document.createElement("button");
|
||||
btn.id = "theme-toggle-btn";
|
||||
btn.className = "theme-toggle";
|
||||
btn.type = "button";
|
||||
const current = getColorMode();
|
||||
btn.innerHTML = current === "dark" ? SUN_SVG : MOON_SVG;
|
||||
btn.setAttribute(
|
||||
"aria-label",
|
||||
current === "dark" ? "Switch to light mode" : "Switch to dark mode",
|
||||
);
|
||||
btn.addEventListener("click", () => {
|
||||
const next = getColorMode() === "dark" ? "light" : "dark";
|
||||
applyTheme(next);
|
||||
});
|
||||
li.appendChild(btn);
|
||||
return li;
|
||||
}
|
||||
|
||||
export function getPerspectiveTheme(): string {
|
||||
return getColorMode() === "dark" ? "Pro Dark" : "Pro Light";
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
* ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
* ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
* ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
* ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
* ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
* ┃ 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 url("https://fonts.googleapis.com/css?display=block&family=Open+Sans:400");
|
||||
|
||||
@import "../../node_modules/@perspective-dev/viewer/dist/css/themes.css";
|
||||
|
||||
/* --- Reset --- */
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
"Open Sans",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- Theme Variables --- */
|
||||
|
||||
:root {
|
||||
--color-primary: #2e8555;
|
||||
--bg-color: #ffffff;
|
||||
--bg-surface: #f2f4f6;
|
||||
--text-color: #1c1e21;
|
||||
--text-muted: #666;
|
||||
--border-color: #dadde1;
|
||||
--navbar-bg: #ffffff;
|
||||
--navbar-height: 60px;
|
||||
--content-width: 590px;
|
||||
--logo-url: url("../../static/svg/perspective-logo-light.svg");
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-primary: #25c2a0;
|
||||
--bg-color: #1b1b1d;
|
||||
--bg-surface: #242526;
|
||||
--text-color: #e3e3e3;
|
||||
--text-muted: #999;
|
||||
--border-color: #444;
|
||||
--navbar-bg: #242526;
|
||||
--logo-url: url("../../static/svg/perspective-logo-dark.svg");
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* --- Regular Table Reset --- */
|
||||
|
||||
td,
|
||||
th,
|
||||
table,
|
||||
tbody,
|
||||
table thead {
|
||||
border: 0;
|
||||
font-size: 12px;
|
||||
font-family:
|
||||
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
|
||||
"Liberation Mono", monospace;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
table tr {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
table tr:nth-child(2n) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
border: none;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
/* --- Navbar --- */
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: var(--navbar-height);
|
||||
background-color: var(--navbar-bg);
|
||||
z-index: 100000;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
box-shadow: none;
|
||||
margin-bottom: -60px;
|
||||
}
|
||||
|
||||
.navbar__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: var(--content-width);
|
||||
}
|
||||
|
||||
.navbar__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.navbar__logo img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.navbar__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navbar__links a {
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.navbar__links a:hover {
|
||||
opacity: 1;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* --- Theme Toggle --- */
|
||||
|
||||
.theme-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --- Header shift (homepage only) --- */
|
||||
|
||||
.header-shift .navbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.header-shift .main-wrapper {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
background-color: var(--navbar-bg);
|
||||
}
|
||||
|
||||
/* --- Hero Banner --- */
|
||||
|
||||
.hero-banner {
|
||||
padding: 4rem 0;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--navbar-bg);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.hero-banner {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-banner__buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* --- Demo --- */
|
||||
|
||||
.demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.demo perspective-viewer {
|
||||
width: 1000px;
|
||||
height: 600px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.demo__vis-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.demo__vis-button {
|
||||
font-size: 12px;
|
||||
border: 1px solid;
|
||||
padding: 8px;
|
||||
border-radius: 3px;
|
||||
margin-right: 2px;
|
||||
opacity: 0.3;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.demo__vis-button--active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.demo__time-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.demo__time-controls > * {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.demo__time-controls span {
|
||||
text-align: end;
|
||||
width: 80px;
|
||||
font-family:
|
||||
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
|
||||
"Liberation Mono", monospace;
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.demo__freq-slider {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
border: 1px solid;
|
||||
width: 200px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.demo__freq-slider::-webkit-slider-thumb {
|
||||
background: var(--bg-color);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo__freq-slider::-moz-range-thumb {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
border-color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* --- perspective-viewer shared --- */
|
||||
|
||||
perspective-viewer {
|
||||
text-align: initial;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #bebebe;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="dark"] perspective-viewer {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
/* --- Features Section --- */
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
background-color: var(--bg-surface);
|
||||
}
|
||||
|
||||
.features__container {
|
||||
margin: auto;
|
||||
max-width: var(--content-width);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.features__container hr {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: inline-flex;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.feature-item__icon {
|
||||
min-width: 53px;
|
||||
margin-right: 50px;
|
||||
stroke: #242526;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .feature-item__icon {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.feature-item__text p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- Gallery Section --- */
|
||||
|
||||
.gallery-section {
|
||||
background-color: var(--bg-surface);
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
max-width: 638px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.gallery img {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* --- Gallery Overlay --- */
|
||||
|
||||
.gallery-overlay {
|
||||
position: fixed;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 80px;
|
||||
z-index: 200000;
|
||||
}
|
||||
|
||||
.gallery-overlay perspective-viewer {
|
||||
max-width: 1200px;
|
||||
max-height: 1000px;
|
||||
flex: 1 1 auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* --- Examples Page --- */
|
||||
|
||||
.examples-grid {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
width: 600px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.examples-grid table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.examples-grid td {
|
||||
max-width: 400px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.examples-grid a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.examples-grid h4 {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.examples-grid img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* --- Block Page --- */
|
||||
|
||||
.block-detail {
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.block-detail h1 {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.block-detail iframe {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.block-detail__link {
|
||||
align-self: flex-end;
|
||||
margin: 1rem 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.block-detail pre {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.block-detail pre code {
|
||||
font-family:
|
||||
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
|
||||
"Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
.block-detail__file-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 1.5rem 0 0 0;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
font-family:
|
||||
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
|
||||
"Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
.block-detail__file-title + pre {
|
||||
margin-top: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* --- Footer --- */
|
||||
|
||||
.footer {
|
||||
background-color: var(--bg-surface);
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.footer__inner {
|
||||
max-width: var(--content-width);
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.footer__logo {
|
||||
width: 160px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.footer__copyright {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.footer__copyright a {
|
||||
color: var(--text-muted);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* --- Page container --- */
|
||||
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-container > main {
|
||||
flex: 1;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.page-container.examples > main {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* --- Prism Syntax Highlighting --- */
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #708090;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url {
|
||||
color: #9a6e3a;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #dd4a68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.comment,
|
||||
[data-theme="dark"] .token.prolog,
|
||||
[data-theme="dark"] .token.doctype,
|
||||
[data-theme="dark"] .token.cdata {
|
||||
color: #6272a4;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.punctuation {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.property,
|
||||
[data-theme="dark"] .token.tag,
|
||||
[data-theme="dark"] .token.boolean,
|
||||
[data-theme="dark"] .token.number,
|
||||
[data-theme="dark"] .token.constant,
|
||||
[data-theme="dark"] .token.symbol,
|
||||
[data-theme="dark"] .token.deleted {
|
||||
color: #ff79c6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.selector,
|
||||
[data-theme="dark"] .token.attr-name,
|
||||
[data-theme="dark"] .token.string,
|
||||
[data-theme="dark"] .token.char,
|
||||
[data-theme="dark"] .token.builtin,
|
||||
[data-theme="dark"] .token.inserted {
|
||||
color: #50fa7b;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.operator,
|
||||
[data-theme="dark"] .token.entity,
|
||||
[data-theme="dark"] .token.url {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.atrule,
|
||||
[data-theme="dark"] .token.attr-value,
|
||||
[data-theme="dark"] .token.keyword {
|
||||
color: #8be9fd;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.function,
|
||||
[data-theme="dark"] .token.class-name {
|
||||
color: #f1fa8c;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .token.regex,
|
||||
[data-theme="dark"] .token.important,
|
||||
[data-theme="dark"] .token.variable {
|
||||
color: #ffb86c;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
export const LAYOUTS: Record<string, any> = {
|
||||
sparkgrid: {
|
||||
plugin: "Datagrid",
|
||||
plugin_config: {
|
||||
columns: {},
|
||||
scroll_lock: true,
|
||||
},
|
||||
columns_config: {
|
||||
"chg (-)": {
|
||||
number_fg_mode: "bar",
|
||||
fg_gradient: 14.34,
|
||||
},
|
||||
chg: {
|
||||
number_bg_mode: "gradient",
|
||||
bg_gradient: 29.17,
|
||||
},
|
||||
"chg (+)": {
|
||||
number_fg_mode: "bar",
|
||||
fg_gradient: 17.4,
|
||||
},
|
||||
},
|
||||
group_rollup_mode: "rollup",
|
||||
settings: true,
|
||||
title: "Market Monitor",
|
||||
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",
|
||||
},
|
||||
},
|
||||
datagrid: {
|
||||
plugin: "datagrid",
|
||||
title: "Blotter",
|
||||
columns: ["ask", "bid", "chg"],
|
||||
group_rollup_mode: "rollup",
|
||||
sort: [
|
||||
["name", "desc"],
|
||||
["lastUpdate", "desc"],
|
||||
],
|
||||
aggregates: { name: "last", lastUpdate: "last" },
|
||||
group_by: ["name", "lastUpdate"],
|
||||
split_by: ["client"],
|
||||
plugin_config: {},
|
||||
},
|
||||
"x bar": {
|
||||
title: "Px (Δ)",
|
||||
group_rollup_mode: "flat",
|
||||
columns: ["chg"],
|
||||
plugin: "X Bar",
|
||||
sort: [["chg", "asc"]],
|
||||
group_by: ["name"],
|
||||
split_by: ["client"],
|
||||
},
|
||||
"y line": {
|
||||
title: "Time Series (Px)",
|
||||
group_rollup_mode: "flat",
|
||||
plugin: "Y Line",
|
||||
group_by: ["lastUpdate"],
|
||||
sort: [["lastUpdate", "desc"]],
|
||||
split_by: ["client"],
|
||||
columns: ["bid"],
|
||||
aggregates: { bid: "avg", chg: "avg", name: "last" },
|
||||
},
|
||||
"xy scatter": {
|
||||
title: "Spread Scatter",
|
||||
group_rollup_mode: "flat",
|
||||
plugin: "X/Y Scatter",
|
||||
group_by: ["name"],
|
||||
split_by: [],
|
||||
columns: ["bid", "ask", "chg", "vol", null, "name"],
|
||||
aggregates: { bid: "avg", ask: "avg", vol: "avg", name: "dominant" },
|
||||
sort: [],
|
||||
},
|
||||
treemap: {
|
||||
plugin: "Treemap",
|
||||
group_rollup_mode: "flat",
|
||||
title: "Volume Map",
|
||||
group_by: ["name", "client"],
|
||||
split_by: [],
|
||||
columns: ["vol", "chg"],
|
||||
aggregates: { bid: "sum", chg: "sum", name: "last" },
|
||||
sort: [
|
||||
["name", "desc"],
|
||||
["chg", "desc"],
|
||||
],
|
||||
},
|
||||
heatmap: {
|
||||
group_rollup_mode: "flat",
|
||||
title: "Spread Heatmap",
|
||||
columns: ["name"],
|
||||
plugin: "Heatmap",
|
||||
expressions: {
|
||||
'bucket("bid",2)': 'bucket("bid",2)',
|
||||
'bucket("ask",2)': `bucket("ask",2)`,
|
||||
},
|
||||
group_by: [`bucket("bid",2)`],
|
||||
split_by: [`bucket("ask",2)`],
|
||||
sort: [],
|
||||
aggregates: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 SECURITIES = [
|
||||
"AAPL.N",
|
||||
"MSFT.N",
|
||||
"AMZN.N",
|
||||
"GOOGL.N",
|
||||
"FB.N",
|
||||
"TSLA.N",
|
||||
"BABA.N",
|
||||
"TSM.N",
|
||||
"V.N",
|
||||
"NVDA.N",
|
||||
"JPM.N",
|
||||
"JNJ.N",
|
||||
"WMT.N",
|
||||
"UNH.N",
|
||||
"MA.N",
|
||||
"BAC.N",
|
||||
"DIS.N",
|
||||
"ASML.N",
|
||||
"ADBE.N",
|
||||
"CMCSA.N",
|
||||
"NKE.N",
|
||||
"XOM.N",
|
||||
"TM.N",
|
||||
"KO.N",
|
||||
"ORCL.N",
|
||||
"NFLX.N",
|
||||
];
|
||||
|
||||
const CLIENTS = [
|
||||
"Homer",
|
||||
"Marge",
|
||||
"Bart",
|
||||
"Lisa",
|
||||
"Maggie",
|
||||
"Barney",
|
||||
"Ned",
|
||||
"Moe",
|
||||
];
|
||||
|
||||
let id = 0;
|
||||
|
||||
function randn_bm(): number {
|
||||
let u = 0,
|
||||
v = 0;
|
||||
while (u === 0) u = Math.random();
|
||||
while (v === 0) v = Math.random();
|
||||
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
||||
}
|
||||
|
||||
export function random_row() {
|
||||
id = id % 1000;
|
||||
return {
|
||||
name: SECURITIES[Math.floor(Math.random() * SECURITIES.length)],
|
||||
client: CLIENTS[Math.floor(Math.random() * CLIENTS.length)],
|
||||
lastUpdate: new Date(),
|
||||
chg: randn_bm() * 10,
|
||||
bid: randn_bm() * 5 + 95,
|
||||
ask: randn_bm() * 5 + 105,
|
||||
vol: randn_bm() * 5 + 105,
|
||||
id: id++,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 { worker } from "./worker.js";
|
||||
// @ts-ignore
|
||||
import SUPERSTORE_URL from "superstore-arrow/superstore.lz4.arrow";
|
||||
|
||||
export const WORKER = worker();
|
||||
|
||||
export const SUPERSTORE_TABLE = (async function () {
|
||||
const req = await fetch(SUPERSTORE_URL);
|
||||
const arrow = await req.arrayBuffer();
|
||||
return await WORKER.then((w) =>
|
||||
w.table(arrow.slice(), { name: "superstore" }),
|
||||
);
|
||||
})();
|
||||
@@ -0,0 +1,34 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 WORKER = (async () => {
|
||||
const perspective = await import("@perspective-dev/client");
|
||||
const perspective_viewer = await import("@perspective-dev/viewer");
|
||||
const server_wasm = import(
|
||||
"@perspective-dev/server/dist/wasm/perspective-server.wasm"
|
||||
);
|
||||
|
||||
const client_wasm = import(
|
||||
"@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(server_wasm.then((x: any) => x.default)),
|
||||
perspective_viewer.init_client(client_wasm.then((x: any) => x.default)),
|
||||
]);
|
||||
|
||||
return await perspective.worker();
|
||||
})();
|
||||
|
||||
export function worker() {
|
||||
return WORKER;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
|
||||
/>
|
||||
<title>Perspective - Examples</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Interactive analytics and data visualization component for large and streaming datasets."
|
||||
/>
|
||||
<link rel="icon" href="https://openjsf.org/favicon.ico" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container examples">
|
||||
<nav class="navbar">
|
||||
<div class="navbar__inner">
|
||||
<a href="/" class="navbar__logo"><img alt="Perspective" src="/svg/perspective-logo-dark.svg" /></a>
|
||||
<ul class="navbar__links">
|
||||
<li><a href="/guide/index.html">Docs</a></li>
|
||||
<li><a href="/examples.html">Examples</a></li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/perspective-dev/perspective"
|
||||
>GitHub</a
|
||||
>
|
||||
</li>
|
||||
<li id="theme-toggle"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-wrapper">
|
||||
<br />
|
||||
<div class="examples-grid" id="examples-grid"></div>
|
||||
<br /><br />
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer__inner">
|
||||
<img
|
||||
class="footer__logo"
|
||||
id="footer-logo"
|
||||
alt="OpenJS Foundation Logo"
|
||||
src="/img/openjs_foundation-logo-horizontal-white.png"
|
||||
/>
|
||||
<div class="footer__copyright">
|
||||
<br /><br />Copyright © 2017 OpenJS Foundation and
|
||||
Perspective contributors.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script type="module" src="examples.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 { initTheme, createThemeToggle } from "./components/theme.js";
|
||||
|
||||
initTheme();
|
||||
document.getElementById("theme-toggle")!.replaceWith(createThemeToggle());
|
||||
|
||||
const LOCAL_EXAMPLES = [
|
||||
"editable",
|
||||
"file",
|
||||
"duckdb",
|
||||
"fractal",
|
||||
"market",
|
||||
"raycasting",
|
||||
"evictions",
|
||||
"nypd",
|
||||
"streaming",
|
||||
"covid",
|
||||
"webcam",
|
||||
"movies",
|
||||
"superstore",
|
||||
"olympics",
|
||||
"dataset",
|
||||
];
|
||||
|
||||
function partition<T>(input: T[], spacing: number): T[][] {
|
||||
const output: T[][] = [];
|
||||
for (let i = 0; i < input.length; i += spacing) {
|
||||
output.push(input.slice(i, i + spacing));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
const grid = document.getElementById("examples-grid")!;
|
||||
|
||||
const table = document.createElement("table");
|
||||
const tbody = document.createElement("tbody");
|
||||
|
||||
for (const group of partition(LOCAL_EXAMPLES, 2)) {
|
||||
const tr = document.createElement("tr");
|
||||
for (const name of group) {
|
||||
const td = document.createElement("td");
|
||||
const a = document.createElement("a");
|
||||
a.href = `/block.html?example=${name}`;
|
||||
|
||||
const br = document.createElement("br");
|
||||
a.appendChild(br);
|
||||
|
||||
const h4 = document.createElement("h4");
|
||||
h4.textContent = name;
|
||||
a.appendChild(h4);
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.width = 400;
|
||||
img.src = `/blocks/${name}/preview.png`;
|
||||
a.appendChild(img);
|
||||
|
||||
td.appendChild(a);
|
||||
tr.appendChild(td);
|
||||
}
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
table.appendChild(tbody);
|
||||
grid.appendChild(table);
|
||||
@@ -0,0 +1,354 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
|
||||
/>
|
||||
<title>Perspective</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Interactive analytics and data visualization component for large and streaming datasets."
|
||||
/>
|
||||
<link rel="icon" href="https://openjsf.org/favicon.ico" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container header-shift" id="page">
|
||||
<nav class="navbar">
|
||||
<div class="navbar__inner">
|
||||
<a href="/" class="navbar__logo"><img alt="Perspective" src="/svg/perspective-logo-dark.svg"/></a>
|
||||
<ul class="navbar__links">
|
||||
<li><a href="/guide/index.html">Docs</a></li>
|
||||
<li><a href="/examples.html">Examples</a></li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/perspective-dev/perspective"
|
||||
>GitHub</a
|
||||
>
|
||||
</li>
|
||||
<li id="theme-toggle"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<header class="hero-banner">
|
||||
<div class="hero-banner__buttons" id="demo-container"></div>
|
||||
</header>
|
||||
|
||||
<section class="features">
|
||||
<div class="features__container">
|
||||
<hr />
|
||||
<br /><br /><br />
|
||||
<div>
|
||||
<h2>What is Perspective?</h2>
|
||||
<p>
|
||||
Perspective is an <i>interactive</i> analytics
|
||||
and data visualization component, which is
|
||||
especially well-suited for <i>large</i> and/or
|
||||
<i>streaming</i> datasets. Use it to create
|
||||
user-configurable reports, dashboards, notebooks
|
||||
and applications, then deploy stand-alone in the
|
||||
browser, or in concert with Python and/or
|
||||
<a
|
||||
href="https://jupyterlab.readthedocs.io/en/stable/"
|
||||
>Jupyterlab</a
|
||||
>.
|
||||
</p>
|
||||
<br />
|
||||
<h3>Features</h3>
|
||||
<br />
|
||||
<div class="feature-item">
|
||||
<svg
|
||||
class="feature-item__icon"
|
||||
width="53"
|
||||
height="67"
|
||||
viewBox="0 0 53 67"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6482 1.56063L1.6114 11.2435C1.22069 11.6204 1 12.14 1 12.6829V64C1 65.1046 1.89543 66 3 66H50C51.1046 66 52 65.1046 52 64V50.4471C52 49.6719 51.3716 49.0435 50.5963 49.0435C49.8211 49.0435 49.1927 48.415 49.1927 47.6398V39.1428C49.1927 38.3676 49.8211 37.7391 50.5963 37.7391C51.3716 37.7391 52 37.1107 52 36.3355V3C52 1.89543 51.1046 1 50 1H13.0368C12.5188 1 12.021 1.20098 11.6482 1.56063Z"
|
||||
/>
|
||||
<path
|
||||
d="M29.2391 29H20.5652L16 43.431H24.2174L21.4783 56L37 38.7759H26.0435L29.2391 29Z"
|
||||
/>
|
||||
<path d="M42 8H45V19H42V8Z" />
|
||||
<path d="M36 8H39V19H36V8Z" />
|
||||
<path d="M31 8H34V19H31V8Z" />
|
||||
<path d="M25 8H28V19H25V8Z" />
|
||||
<path d="M19 8H22V19H19V8Z" />
|
||||
<path d="M14 8H17V19H14V8Z" />
|
||||
<path d="M8 14H11V22H8V14Z" />
|
||||
</svg>
|
||||
<div class="feature-item__text">
|
||||
<p>
|
||||
A fast, memory efficient streaming query
|
||||
engine, written in C++ and compiled for
|
||||
both
|
||||
<a href="https://webassembly.org/"
|
||||
>WebAssembly</a
|
||||
>
|
||||
and
|
||||
<a href="https://www.python.org/"
|
||||
>Python</a
|
||||
>, with read/write/streaming for
|
||||
<a href="https://arrow.apache.org/"
|
||||
>Apache Arrow</a
|
||||
>, and a high-performance columnar
|
||||
expression language based on
|
||||
<a
|
||||
href="https://github.com/ArashPartow/exprtk"
|
||||
>ExprTK</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<svg
|
||||
class="feature-item__icon"
|
||||
width="49"
|
||||
height="71"
|
||||
viewBox="0 0 49 71"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1 23.5001L24.3345 6.16554L47.669 23.5001L24.3345 40.8346L1 23.5001Z"
|
||||
/>
|
||||
<path
|
||||
d="M9 41.5L1 47.3345L24.3345 64.669L47.669 47.3345L39.6518 41.3787"
|
||||
/>
|
||||
<path
|
||||
d="M9 29.5001L1 35.3345L24.3345 52.6691L47.669 35.3345L39.6518 29.3788"
|
||||
/>
|
||||
</svg>
|
||||
<div class="feature-item__text">
|
||||
<p>
|
||||
A framework-agnostic User Interface
|
||||
packaged as a
|
||||
<a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements"
|
||||
>Custom Element</a
|
||||
>, powered either in-browser via
|
||||
WebAssembly or virtually via WebSocket
|
||||
server (Python/Node).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<svg
|
||||
class="feature-item__icon"
|
||||
width="48"
|
||||
height="59"
|
||||
viewBox="0 0 48 59"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="1"
|
||||
width="45"
|
||||
height="57"
|
||||
rx="2"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="11"
|
||||
r="2"
|
||||
fill="#666"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="30"
|
||||
r="2"
|
||||
fill="#666"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="49"
|
||||
r="2"
|
||||
fill="#666"
|
||||
/>
|
||||
<path d="M10 11H0" />
|
||||
<path d="M39 10H17" />
|
||||
<path d="M39 14H17" />
|
||||
<path d="M39 18H17" />
|
||||
<path d="M39 22H17" />
|
||||
<path d="M39 26H17" />
|
||||
<path d="M39 30H17" />
|
||||
<path d="M39 34H17" />
|
||||
<path d="M39 38H17" />
|
||||
<path d="M39 42H17" />
|
||||
<path d="M39 46H17" />
|
||||
<path d="M39 50H17" />
|
||||
<path d="M10 30H0" />
|
||||
<path d="M10 49H0" />
|
||||
</svg>
|
||||
<div class="feature-item__text">
|
||||
<p>
|
||||
A
|
||||
<a href="https://jupyter.org/"
|
||||
>JupyterLab</a
|
||||
>
|
||||
widget and Python client library, for
|
||||
interactive data analysis in a notebook,
|
||||
as well as <i>scalable</i> production
|
||||
<a
|
||||
href="https://github.com/voila-dashboards/voila"
|
||||
>Voila</a
|
||||
>
|
||||
applications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<br /><br /><br />
|
||||
<div>
|
||||
<h2>JavaScript</h2>
|
||||
<p>
|
||||
Interactive dashboards built on Perspective.js
|
||||
<a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements"
|
||||
>Custom Elements</a
|
||||
>
|
||||
are easy to integrate into any web application
|
||||
framework.
|
||||
</p>
|
||||
<p>
|
||||
Using Perspective's simple query language,
|
||||
elements like
|
||||
<code><perspective-viewer></code> can be
|
||||
<i>symmetrically</i> configured via API or User
|
||||
interaction. Web Applications built with
|
||||
Perspective Custom Elements can be re-hydrated
|
||||
from their serialized state, driven from external
|
||||
Events, or persisted to any store. Workspaces
|
||||
can mix virtual, server-side Python data with
|
||||
in-browser client data seamlessly, and
|
||||
independent data Views can be cross-filtered,
|
||||
duplicated, exported, stacked and saved.
|
||||
</p>
|
||||
<p>
|
||||
To achieve Desktop-like performance in the
|
||||
Browser, Perspective.js relies on
|
||||
<a href="https://webassembly.org/"
|
||||
>WebAssembly</a
|
||||
>
|
||||
for excellent <i>query calculation</i> time, and
|
||||
<a href="https://arrow.apache.org/"
|
||||
>Apache Arrow</a
|
||||
>
|
||||
for its conservative <i>memory footprint</i> and
|
||||
efficient <i>data serialization</i>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<br /><br /><br />
|
||||
<div>
|
||||
<h2>Python</h2>
|
||||
<p>
|
||||
<code>perspective-python</code>, built on the
|
||||
same C++ data engine used by the
|
||||
<a
|
||||
href="https://perspective-dev.github.io/docs/md/js.html"
|
||||
>WebAssembly version</a
|
||||
>, implements the Perspective API directly in
|
||||
Python, either as a virtualized server for
|
||||
Production, or as an embedded JupyterLab Widget
|
||||
for Research.
|
||||
</p>
|
||||
<p>
|
||||
For Application Developers, virtualized
|
||||
<code><perspective-viewer></code> will only
|
||||
consume the data necessary to render the current
|
||||
screen, enabling <i>ludicrous size</i> datasets
|
||||
with nearly instant load. Or - efficiently stream
|
||||
the entire dataset to the WebAssembly runtime via
|
||||
Apache Arrow, and give your server a break!
|
||||
</p>
|
||||
<p>
|
||||
For Researchers and Data Scientists,
|
||||
<code>PerspectiveWidget</code> is available as a
|
||||
<a
|
||||
href="https://jupyterlab.readthedocs.io/en/stable/"
|
||||
>Jupyter/JupyterLab</a
|
||||
>
|
||||
widget, allowing interactive
|
||||
<a href="https://pandas.pydata.org/">Pandas</a>
|
||||
and
|
||||
<a href="https://arrow.apache.org/"
|
||||
>Apache Arrow</a
|
||||
>
|
||||
visualization within a notebook.
|
||||
</p>
|
||||
</div>
|
||||
<br />
|
||||
<hr />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gallery-section">
|
||||
<div class="gallery" id="gallery"></div>
|
||||
<div style="max-width: 638px; margin: 0 auto">
|
||||
<hr />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer__inner">
|
||||
<img
|
||||
class="footer__logo"
|
||||
id="footer-logo"
|
||||
alt="OpenJS Foundation Logo"
|
||||
src="/img/openjs_foundation-logo-horizontal-white.png"
|
||||
/>
|
||||
<div class="footer__copyright">
|
||||
<br /><br />Copyright © 2017 OpenJS Foundation and
|
||||
Perspective contributors.<br /><br />
|
||||
All rights reserved. The OpenJS Foundation has registered
|
||||
trademarks and uses trademarks. For a list of trademarks
|
||||
of the OpenJS Foundation, please see our Trademark Policy
|
||||
and Trademark List. Trademarks and logos not indicated on
|
||||
the list of OpenJS Foundation trademarks are
|
||||
trademarks™ or registered® trademarks of their
|
||||
respective holders. Use of them does not imply any
|
||||
affiliation with or endorsement by them.<br /><br />
|
||||
<a href="https://openjsf.org">The OpenJS Foundation</a>
|
||||
|
|
||||
<a href="https://terms-of-use.openjsf.org"
|
||||
>Terms of Use</a
|
||||
>
|
||||
|
|
||||
<a href="https://privacy-policy.openjsf.org"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
| <a href="https://bylaws.openjsf.org">Bylaws</a> |
|
||||
<a href="https://code-of-conduct.openjsf.org"
|
||||
>Code of Conduct</a
|
||||
>
|
||||
|
|
||||
<a href="https://trademark-policy.openjsf.org"
|
||||
>Trademark Policy</a
|
||||
>
|
||||
|
|
||||
<a href="https://trademark-list.openjsf.org"
|
||||
>Trademark List</a
|
||||
>
|
||||
|
|
||||
<a href="https://www.linuxfoundation.org/cookies"
|
||||
>Cookie Policy</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<script type="module" src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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 { initTheme, createThemeToggle } from "./components/theme.js";
|
||||
import { initDemo } from "./components/demo.js";
|
||||
import { initGallery } from "./components/gallery.js";
|
||||
|
||||
initTheme();
|
||||
document.getElementById("theme-toggle")!.replaceWith(createThemeToggle());
|
||||
|
||||
const page = document.getElementById("page")!;
|
||||
|
||||
function handleScroll() {
|
||||
const scrollTop = document.documentElement.scrollTop;
|
||||
if (scrollTop > 90 && page.classList.contains("header-shift")) {
|
||||
page.classList.remove("header-shift");
|
||||
} else if (scrollTop <= 90 && !page.classList.contains("header-shift")) {
|
||||
page.classList.add("header-shift");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("scroll", handleScroll);
|
||||
|
||||
const demoContainer = document.getElementById("demo-container")!;
|
||||
initDemo(demoContainer);
|
||||
|
||||
const gallery = document.getElementById("gallery")!;
|
||||
initGallery(gallery);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user