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
+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);