chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:07 +08:00
commit a26e856398
1681 changed files with 296950 additions and 0 deletions
+39
View File
@@ -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"
+176
View File
@@ -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
View File
@@ -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();
+16
View File
@@ -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"
+68
View File
@@ -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.`);
+1
View File
@@ -0,0 +1 @@
<!-- Empty page to allow HTML injection from `build.js` screenshot generator-->
+571
View File
@@ -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)_ -->
+83
View File
@@ -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)
+22
View File
@@ -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/)
+4
View File
@@ -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.
+15
View File
@@ -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">&lt;html&gt;</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"> &#160;&#160;&#160;group_by: [&quot;Sub&#45;Category&quot;]</text>
<text text-anchor="start" x="144" y="-35.1" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table1&#45;&gt;view2 -->
<g id="edge1" class="edge">
<title>table1&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="360" y="-44.1" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;xy_scatter&quot;</text>
<text text-anchor="start" x="360" y="-35.1" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;row&#45;pivots=&#39;[&quot;Sub&#45;Category&quot;]&#39;&gt;</text>
</g>
<!-- view2&#45;&gt;viewer2 -->
<g id="edge2" class="edge">
<title>view2&#45;&gt;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 &#45; 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">&lt;html&gt;</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"> &#160;&#160;&#160;group_by:&#39;[&quot;State&quot;]</text>
<text text-anchor="start" x="144" y="-43.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;split_by:&#39;[&quot;Segment&quot;]&#39;</text>
<text text-anchor="start" x="144" y="-34.6" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table_thread_1&#45;&gt;view_thread_1 -->
<g id="edge1" class="edge">
<title>table_thread_1&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="372" y="-52.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;heatmap&quot;</text>
<text text-anchor="start" x="372" y="-43.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;row&#45;pivots=&#39;[&quot;State&quot;]</text>
<text text-anchor="start" x="372" y="-34.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;column&#45;pivots=&#39;[&quot;Segment&quot;]&#39;&gt;</text>
</g>
<!-- view_thread_1&#45;&gt;viewer4 -->
<g id="edge2" class="edge">
<title>view_thread_1&#45;&gt;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 &#45; 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">&lt;html&gt;</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"> &#160;&#160;&#160;group_by: [&quot;State&quot;]]</text>
<text text-anchor="start" x="144" y="-39.1" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table_thread_2&#45;&gt;view_thread_2 -->
<g id="edge1" class="edge">
<title>table_thread_2&#45;&gt;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&#45;&gt;table12 -->
<g id="edge3" class="edge">
<title>view_thread_2&#45;&gt;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"> &#160;&#160;&#160;group_by: [&quot;Category&quot;]</text>
<text text-anchor="start" x="480" y="-43.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;filter: [[&quot;State&quot;,&quot;==&quot;,&quot;Texas&quot;]]</text>
<text text-anchor="start" x="480" y="-34.6" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table12&#45;&gt;view12 -->
<g id="edge2" class="edge">
<title>table12&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;heatmap&quot;</text>
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;row&#45;pivots=&#39;[&quot;State&quot;]</text>
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;column&#45;pivots=&#39;[&quot;Segment&quot;]&#39;&gt;</text>
</g>
<!-- view12&#45;&gt;viewer5 -->
<g id="edge4" class="edge">
<title>view12&#45;&gt;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 &#45; 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 &#45; 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">&lt;html&gt;</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"> &#160;&#160;&#160;group_by: [&quot;State&quot;]]</text>
<text text-anchor="start" x="144" y="-141.1" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table_thread_2&#45;&gt;view_thread_2 -->
<g id="edge2" class="edge">
<title>table_thread_2&#45;&gt;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"> &#160;&#160;&#160;group_by: [&quot;City&quot;]]</text>
<text text-anchor="start" x="144" y="-195.1" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table_thread_2&#45;&gt;view_thread_2_2 -->
<g id="edge1" class="edge">
<title>table_thread_2&#45;&gt;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&#45;&gt;table12 -->
<g id="edge8" class="edge">
<title>view_thread_2&#45;&gt;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"> &#160;&#160;&#160;group_by:&#39;[&quot;State&quot;]</text>
<text text-anchor="start" x="144" y="-44.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;split_by:&#39;[&quot;Segment&quot;]&#39;</text>
<text text-anchor="start" x="144" y="-35.6" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table_thread_1&#45;&gt;view_thread_1 -->
<g id="edge3" class="edge">
<title>table_thread_1&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;heatmap&quot;</text>
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;row&#45;pivots=&#39;[&quot;State&quot;]</text>
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;column&#45;pivots=&#39;[&quot;Segment&quot;]&#39;&gt;</text>
</g>
<!-- view_thread_1&#45;&gt;viewer4 -->
<g id="edge12" class="edge">
<title>view_thread_1&#45;&gt;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"> &#160;&#160;&#160;group_by: [&quot;Category&quot;]</text>
<text text-anchor="start" x="480" y="-310.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;filter: [[&quot;State&quot;,&quot;==&quot;,&quot;Texas&quot;]]</text>
<text text-anchor="start" x="480" y="-301.6" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table1&#45;&gt;view1 -->
<g id="edge4" class="edge">
<title>table1&#45;&gt;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"> &#160;&#160;&#160;group_by: [&quot;Sub&#45;Category&quot;]</text>
<text text-anchor="start" x="480" y="-364.1" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table1&#45;&gt;view2 -->
<g id="edge5" class="edge">
<title>table1&#45;&gt;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&#45;&gt;view3 -->
<g id="edge6" class="edge">
<title>table_remote_view&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="696" y="-324.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;Y Bar&quot;</text>
<text text-anchor="start" x="696" y="-315.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;row&#45;pivots=&#39;[&quot;Category&quot;]&#39;</text>
<text text-anchor="start" x="696" y="-306.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;filters=&#39;[[&quot;State&quot;,&quot;==&quot;,&quot;Texas&quot;]]&#39;&gt;</text>
</g>
<!-- view1&#45;&gt;viewer1 -->
<g id="edge9" class="edge">
<title>view1&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="696" y="-378.1" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;xy_scatter&quot;</text>
<text text-anchor="start" x="696" y="-369.1" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;row&#45;pivots=&#39;[&quot;Sub&#45;Category&quot;]&#39;&gt;</text>
</g>
<!-- view2&#45;&gt;viewer2 -->
<g id="edge10" class="edge">
<title>view2&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="696" y="-257.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;grid&quot;&gt;</text>
</g>
<!-- view3&#45;&gt;viewer3 -->
<g id="edge11" class="edge">
<title>view3&#45;&gt;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"> &#160;&#160;&#160;group_by: [&quot;Category&quot;]</text>
<text text-anchor="start" x="480" y="-145.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;filter: [[&quot;State&quot;,&quot;==&quot;,&quot;Texas&quot;]]</text>
<text text-anchor="start" x="480" y="-136.6" font-family="monospace" font-size="8.00">})</text>
</g>
<!-- table12&#45;&gt;view12 -->
<g id="edge7" class="edge">
<title>table12&#45;&gt;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">&lt;perspective&#45;viewer</text>
<text text-anchor="start" x="696" y="-154.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;view=&quot;heatmap&quot;</text>
<text text-anchor="start" x="696" y="-145.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;row&#45;pivots=&#39;[&quot;State&quot;]</text>
<text text-anchor="start" x="696" y="-136.6" font-family="monospace" font-size="8.00"> &#160;&#160;&#160;column&#45;pivots=&#39;[&quot;Segment&quot;]&#39;&gt;</text>
</g>
<!-- view12&#45;&gt;viewer5 -->
<g id="edge13" class="edge">
<title>view12&#45;&gt;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);
```
+12
View File
@@ -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`.
+25
View File
@@ -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 |
+35
View File
@@ -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.
+46
View File
@@ -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.
+77
View File
@@ -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.
+14
View File
@@ -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>
+87
View File
@@ -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" }
```
+59
View File
@@ -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>
+41
View File
@@ -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>
+70
View File
@@ -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>
+231
View File
@@ -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 &times;
24 &times; 60 &times; 60 &times; 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`.
+50
View File
@@ -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>
+83
View File
@@ -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 |
+5
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
# How To
Step-by-step guides for common Perspective tasks, organized by language.
+4
View File
@@ -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).
+25
View File
@@ -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();
```
+40
View File
@@ -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);
});
```
+172
View File
@@ -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.
+54
View File
@@ -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
```
+65
View File
@@ -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
```
+76
View File
@@ -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;
```
+57
View File
@@ -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} />
);
}
```
+99
View File
@@ -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());
```
+21
View File
@@ -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());
```
+96
View File
@@ -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.
+62
View File
@@ -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)
+44
View File
@@ -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");
```
+4
View File
@@ -0,0 +1,4 @@
# Python
Guides for using `perspective-python`, including data loading, callbacks,
multithreading, WebSocket servers, and JupyterLab integration.
+34
View File
@@ -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.
+27
View File
@@ -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
``` -->
+64
View File
@@ -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")
```
+61
View File
@@ -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. -->
+67
View File
@@ -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)
```
+90
View File
@@ -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.
+81
View File
@@ -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")
```
+20
View File
@@ -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)
+129
View File
@@ -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);
```
+89
View File
@@ -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?;
```
+3
View File
@@ -0,0 +1,3 @@
# JavaScript
Overview and module structure for Perspective's JavaScript packages.
+15
View File
@@ -0,0 +1,15 @@
perspective-viewer {
height: 500px;
}
div.javascript:before {
content: "JavaScript:";
}
div.python:before {
content: "Python:";
}
div.rust:before {
content: "Rust:";
}
+29
View File
@@ -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);
}
+1
View File
@@ -0,0 +1 @@
{{#include ../../README.md}}
+109
View File
@@ -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`.
+32
View File
@@ -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:"
}
}
+25
View File
@@ -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`,
],
});
+58
View File
@@ -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 &copy; 2017 OpenJS Foundation and
Perspective contributors.
</div>
</div>
</footer>
</div>
<script type="module" src="block.js"></script>
</body>
</html>
+93
View File
@@ -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);
}
});
}
+132
View File
@@ -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() });
}
+77
View File
@@ -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);
}
+107
View File
@@ -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";
}
+653
View File
@@ -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
+122
View File
@@ -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: {},
},
};
+75
View File
@@ -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++,
};
}
+25
View File
@@ -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" }),
);
})();
+34
View File
@@ -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;
}
+59
View File
@@ -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 &copy; 2017 OpenJS Foundation and
Perspective contributors.
</div>
</div>
</footer>
</div>
<script type="module" src="examples.js"></script>
</body>
</html>
+75
View File
@@ -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);
+354
View File
@@ -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>&lt;perspective-viewer&gt;</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>&lt;perspective-viewer&gt;</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 &copy; 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&trade; or registered&reg; 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>
+37
View File
@@ -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