diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 43f896e8ad..e9c4fa4ace 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -73,7 +73,7 @@ jobs:
- ubuntu-22.04
python-version:
- 3.9
- node-version: [20.x]
+ node-version: [22.x]
steps:
- name: Checkout
@@ -125,7 +125,7 @@ jobs:
- ubuntu-22.04
python-version:
- 3.9
- node-version: [20.x]
+ node-version: [22.x]
steps:
- name: Checkout
@@ -193,7 +193,7 @@ jobs:
- x86_64
python-version:
- 3.9
- node-version: [20.x]
+ node-version: [22.x]
include:
- os: ubuntu-22.04
arch: x86_64
@@ -305,7 +305,7 @@ jobs:
- windows-2022
arch:
- x86_64
- node-version: [20.x]
+ node-version: [22.x]
is-release:
- ${{ startsWith(github.ref, 'refs/tags/v') || github.ref_name == 'master' }}
exclude:
@@ -395,7 +395,7 @@ jobs:
- x86_64
python-version:
- 3.9
- node-version: [20.x]
+ node-version: [22.x]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -443,7 +443,7 @@ jobs:
- x86_64
python-version:
- 3.9
- node-version: [20.x]
+ node-version: [22.x]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -510,7 +510,7 @@ jobs:
- x86_64
python-version:
- 3.9
- node-version: [20.x]
+ node-version: [22.x]
is-release:
- ${{ startsWith(github.ref, 'refs/tags/v') || github.ref_name == 'master' }}
exclude:
@@ -621,7 +621,7 @@ jobs:
- ubuntu-22.04
python-version:
- 3.9
- node-version: [20.x]
+ node-version: [22.x]
steps:
- name: Checkout
@@ -674,7 +674,7 @@ jobs:
python-version:
- 3.9
# - 3.12
- node-version: [20.x]
+ node-version: [22.x]
is-release:
- ${{ startsWith(github.ref, 'refs/tags/v') || github.ref_name == 'master' }}
exclude:
@@ -823,7 +823,7 @@ jobs:
matrix:
os: [ubuntu-22.04]
python-version: [3.9]
- node-version: [20.x]
+ node-version: [22.x]
arch: [x86_64]
steps:
- name: Checkout
@@ -880,7 +880,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-22.04]
- node-version: [20.x]
+ node-version: [22.x]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
diff --git a/Cargo.lock b/Cargo.lock
index 6654e0161b..80815092ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -681,6 +681,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -873,7 +879,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
dependencies = [
- "fallible-iterator",
+ "fallible-iterator 0.2.0",
"indexmap 1.9.3",
"stable_deref_trait",
]
@@ -1729,9 +1735,14 @@ version = "4.0.1"
dependencies = [
"async-lock",
"axum",
+ "fallible-iterator 0.3.0",
"futures",
+ "indexmap 2.12.1",
"perspective-client",
"perspective-server",
+ "prost",
+ "serde",
+ "serde_json",
"tokio",
"tracing",
]
@@ -1752,6 +1763,7 @@ dependencies = [
"async-lock",
"futures",
"getrandom 0.3.4",
+ "indexmap 2.12.1",
"itertools 0.10.5",
"num-traits",
"paste",
@@ -1775,11 +1787,13 @@ name = "perspective-js"
version = "4.0.1"
dependencies = [
"anyhow",
+ "bytes",
"chrono",
"derivative",
"extend",
"futures",
"getrandom 0.2.16",
+ "indexmap 2.12.1",
"js-sys",
"perspective-client",
"prost",
@@ -1810,9 +1824,12 @@ name = "perspective-python"
version = "4.0.1"
dependencies = [
"async-lock",
+ "bytes",
+ "chrono",
"cmake",
"extend",
"futures",
+ "indexmap 2.12.1",
"macro_rules_attribute",
"num_cpus",
"perspective-client",
@@ -1823,6 +1840,7 @@ dependencies = [
"pyo3-build-config 0.22.6",
"python-config-rs",
"pythonize",
+ "serde",
"tokio",
"tracing",
"tracing-subscriber",
@@ -1835,11 +1853,16 @@ dependencies = [
"async-lock",
"cmake",
"futures",
+ "indexmap 2.12.1",
"link-cplusplus",
"num_cpus",
"perspective-client",
+ "prost",
"protobuf-src",
+ "serde",
+ "serde_json",
"shlex",
+ "thiserror 1.0.69",
"tracing",
]
@@ -1851,6 +1874,7 @@ dependencies = [
"async-lock",
"base64 0.13.1",
"chrono",
+ "console_error_panic_hook",
"derivative",
"extend",
"futures",
@@ -2107,9 +2131,9 @@ dependencies = [
[[package]]
name = "protobuf-src"
-version = "2.0.1+26.1"
+version = "2.1.1+27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ba1cfa4b9dc098926b8cce388bf434b93516db3ecf6e8b1a37eb643d733ee7"
+checksum = "6217c3504da19b85a3a4b2e9a5183d635822d83507ba0986624b5c05b83bfc40"
dependencies = [
"cmake",
]
diff --git a/README.md b/README.md
index 35327ac707..52cccc9771 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
+
@@ -14,27 +15,33 @@
-Perspective is an interactive analytics and data visualization component,
-which is especially well-suited for large and/or streaming
-datasets. Use it to create user-configurable reports, dashboards, notebooks and
-applications.
+Perspective is an interactive analytics and data visualization component for
+large and streaming datasets. Build user-configurable reports, dashboards,
+notebooks, and applications with a high-performance query engine compiled to
+WebAssembly, Python, and Rust.
### Features
-- A fast, memory efficient streaming query engine, written in C++ and compiled
- for [WebAssembly](https://webassembly.org/), [Python](https://www.python.org/)
- and [Rust](https://www.rust-lang.org/), with read/write/streaming for
- [Apache Arrow](https://arrow.apache.org/), and a high-performance columnar
- expression language based on [ExprTK](https://github.com/ArashPartow/exprtk).
-
-- A framework-agnostic User Interface packaged as a
+- A framework-agnostic user interface packaged as a
[Custom Element](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements),
- powered either in-browser via WebAssembly or virtually via WebSocket server
- (Python/Node/Rust).
+ which connects to a Data Model in-browser (via WebAssembly) or remotely (via
+ WebSocket, with integration in Python, Node.js and Rust). Includes a data
+ grid, 10+ chart types line, bar, area, scatter, heatmap, treemap, sunburst,
+ candlestick, and more.
+
+- A Data Model API for pluggable engines, enabling Perspective's UI to query
+ external data sources like [DuckDB](https://duckdb.org/) while translating
+ view configurations into native queries.
+
+- A fast, memory-efficient streaming Data Model built-in, written in C++ and
+ compiled for [WebAssembly](https://webassembly.org/),
+ [Python](https://www.python.org/), and [Rust](https://www.rust-lang.org/).
+ Supports read/write/streaming for [Apache Arrow](https://arrow.apache.org/),
+ with a columnar expression language based on
+ [ExprTK](https://github.com/ArashPartow/exprtk).
-- A [JupyterLab](https://jupyter.org/) widget and Python client library, for
- interactive data analysis in a notebook, as well as _scalable_ production
- applications.
+- A [JupyterLab](https://jupyter.org/) widget and Python client library for
+ interactive data analysis in notebooks.
### Documentation
@@ -62,7 +69,7 @@ applications.
### Examples
-| editable | file | fractal |
 |  |  |
| market | raycasting | evictions |
 |  |  |
| nypd | streaming | covid |
 |  |  |
| webcam | movies | superstore |
 |  |  |
| citibike | olympics | dataset |
 |  |  |
+| editable | file | duckdb |
 |  |  |
| fractal | market | raycasting |
 |  |  |
| evictions | nypd | streaming |
 |  |  |
| covid | webcam | movies |
 |  |  |
| superstore | citibike | olympics |
 |  |  |
| dataset |
 |
### Media
diff --git a/examples/README.md b/examples/README.md
index 8bb2dc5609..6c4174a689 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -12,6 +12,13 @@ In order to _run_ a project in this directory as written:
2. Run the project with `pnpm run start $PROJECT_NAME` from the repository root
(_not_ the `/examples` directory).
+## VirtualServer Examples
+
+These examples demonstrate custom data source implementations using the VirtualServer API, which allows you to create backends that serve data from any source without loading it into Perspective's tables:
+
+- **[nodejs-virtual-server](nodejs-virtual-server/)** - Node.js example with in-memory data and WebSocket server
+- **[python-duckdb-virtual](python-duckdb-virtual/)** - Python example using DuckDB as a data source
+
# Optional
Generally, the changes necessary to make these examples run _without_ the
diff --git a/examples/blocks/examples.js b/examples/blocks/examples.js
index 1a2c3a6b85..7ab7b2bfb1 100644
--- a/examples/blocks/examples.js
+++ b/examples/blocks/examples.js
@@ -13,6 +13,7 @@
const LOCAL_EXAMPLES = [
"editable",
"file",
+ "duckdb",
"fractal",
"market",
"raycasting",
diff --git a/examples/blocks/package.json b/examples/blocks/package.json
index f5b8e438aa..a6bc3ad5d4 100644
--- a/examples/blocks/package.json
+++ b/examples/blocks/package.json
@@ -4,7 +4,7 @@
"version": "4.0.1",
"description": "A collection of simple client-side Perspective examples for `http://bl.ocks.org`.",
"scripts": {
- "start": "mkdir -p dist && node --experimental-wasm-memory64 --experimental-modules server.mjs",
+ "start": "mkdir -p dist && node --experimental-modules server.mjs",
"repl": "node --experimental-repl-await"
},
"main": "index.mjs",
diff --git a/examples/blocks/src/duckdb/README.md b/examples/blocks/src/duckdb/README.md
new file mode 100644
index 0000000000..41d5291f70
--- /dev/null
+++ b/examples/blocks/src/duckdb/README.md
@@ -0,0 +1,15 @@
+An example of [Perspective](https://github.com/perspective-dev/perspective)
+using [DuckDB WASM](https://duckdb.org/docs/api/wasm/overview) as a virtual
+server backend via the `DuckDBHandler` adapter.
+
+Instead of using Perspective's built-in WebAssembly query engine, this example
+demonstrates how to use DuckDB as the data processing layer while still
+leveraging Perspective's visualization components. The `DuckDBHandler` translates
+Perspective's view configuration (group by, split by, sort, filter, expressions,
+aggregates) into DuckDB SQL queries, enabling Perspective to query data stored
+in DuckDB tables.
+
+This example loads the Superstore sample dataset into a DuckDB table, then
+creates a Perspective viewer that queries the data through the DuckDB virtual
+server. A separate log viewer displays the SQL queries being generated in
+real-time, along with a timeline chart showing query frequency.
diff --git a/examples/blocks/src/duckdb/index.css b/examples/blocks/src/duckdb/index.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/blocks/src/duckdb/index.html b/examples/blocks/src/duckdb/index.html
new file mode 100644
index 0000000000..f266a8e523
--- /dev/null
+++ b/examples/blocks/src/duckdb/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/blocks/src/duckdb/index.js b/examples/blocks/src/duckdb/index.js
new file mode 100644
index 0000000000..e35d56aa5c
--- /dev/null
+++ b/examples/blocks/src/duckdb/index.js
@@ -0,0 +1,99 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ Copyright (c) 2017, the Perspective Authors. ┃
+// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
+// ┃ This file is part of the Perspective library, distributed under the terms ┃
+// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
+// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+import "/node_modules/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
+import "/node_modules/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
+import "/node_modules/@perspective-dev/viewer-d3fc/dist/cdn/perspective-viewer-d3fc.js";
+
+import perspective from "/node_modules/@perspective-dev/client/dist/cdn/perspective.js";
+import { DuckDBHandler } from "/node_modules/@perspective-dev/client/dist/esm/virtual_servers/duckdb.js";
+
+// Need to use jsDelivr's ESM features to load this as packaged.
+import * as duckdb from "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.33.1-dev18.0/+esm";
+
+const LOGGER = {
+ log(entry) {
+ table2.update([{ timestamp: entry.timestamp, sql: entry.value }]);
+ },
+};
+
+const db = await initializeDuckDB();
+const server = perspective.createMessageHandler(new DuckDBHandler(db));
+const client = await perspective.worker(server);
+
+const logworker = await perspective.worker();
+const table2 = await logworker.table(
+ { timestamp: "datetime", sql: "string" },
+ { name: "logs", limit: 10_000 },
+);
+
+const log_element = document.querySelector("#logger");
+log_element.load(logworker);
+log_element.restore({
+ table: "logs",
+ sort: [["timestamp", "desc"]],
+ title: "SQL Log",
+});
+
+const log_element2 = document.querySelector("#logger2");
+log_element2.load(logworker);
+log_element2.restore({
+ table: "logs",
+ sort: [["timestamp", "desc"]],
+ columns: ["sql"],
+ group_by: ["1s"],
+ plugin: "Y Bar",
+ expressions: { "1s": `bucket("timestamp",'1s')` },
+ title: "SQL Timeline",
+});
+
+async function initializeDuckDB() {
+ const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
+ const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
+ const worker_url = URL.createObjectURL(
+ new Blob([`importScripts("${bundle.mainWorker}");`], {
+ type: "text/javascript",
+ }),
+ );
+
+ const duckdb_worker = new Worker(worker_url);
+ const db = new duckdb.AsyncDuckDB(LOGGER, duckdb_worker);
+ await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
+ URL.revokeObjectURL(worker_url);
+ const conn = await db.connect();
+ return conn;
+}
+
+async function loadSampleData(db) {
+ const response = await fetch(
+ "/node_modules/superstore-arrow/superstore.lz4.arrow",
+ );
+
+ const text = await response.arrayBuffer();
+ await db.insertArrowFromIPCStream(new Uint8Array(text), {
+ name: "data_source_one",
+ create: true,
+ });
+}
+
+await loadSampleData(db);
+
+const viewer = document.querySelector("#query");
+viewer.load(client);
+viewer.restore({
+ table: "data_source_one",
+ group_by: ["Region", "State", "City"],
+ columns: ["Sales", "Profit", "Quantity", "Discount"],
+ plugin: "Datagrid",
+ theme: "Pro Dark",
+ settings: true,
+});
diff --git a/examples/esbuild-duckdb-virtual/build.js b/examples/esbuild-duckdb-virtual/build.js
new file mode 100644
index 0000000000..4bae075f3c
--- /dev/null
+++ b/examples/esbuild-duckdb-virtual/build.js
@@ -0,0 +1,43 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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 esbuild from "esbuild";
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import { dirname } from "path";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+async function build() {
+ await esbuild.build({
+ entryPoints: ["src/index.ts"],
+ outdir: "dist",
+ format: "esm",
+ bundle: true,
+ sourcemap: "inline",
+ target: "es2022",
+ loader: {
+ ".ttf": "file",
+ ".wasm": "file",
+ ".arrow": "file",
+ },
+ assetNames: "[name]",
+ });
+
+ fs.writeFileSync(
+ path.join(__dirname, "dist/index.html"),
+ fs.readFileSync(path.join(__dirname, "src/index.html")).toString(),
+ );
+}
+
+build();
diff --git a/examples/esbuild-duckdb-virtual/package.json b/examples/esbuild-duckdb-virtual/package.json
new file mode 100644
index 0000000000..ce7c2bf428
--- /dev/null
+++ b/examples/esbuild-duckdb-virtual/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "esbuild-duckdb-virtual",
+ "private": true,
+ "version": "3.8.0",
+ "type": "module",
+ "description": "Example of a custom VirtualServer running in a Web Worker",
+ "scripts": {
+ "build": "node build.js",
+ "start": "node build.js && node server.mjs"
+ },
+ "keywords": [],
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@perspective-dev/client": "workspace:^",
+ "@perspective-dev/server": "workspace:^",
+ "@perspective-dev/viewer": "workspace:^",
+ "@perspective-dev/viewer-d3fc": "workspace:^",
+ "@perspective-dev/viewer-datagrid": "workspace:^",
+ "@duckdb/duckdb-wasm": "catalog:",
+ "superstore-arrow": "catalog:"
+ },
+ "devDependencies": {
+ "esbuild": "catalog:"
+ }
+}
diff --git a/examples/esbuild-duckdb-virtual/server.mjs b/examples/esbuild-duckdb-virtual/server.mjs
new file mode 100644
index 0000000000..49418e5c61
--- /dev/null
+++ b/examples/esbuild-duckdb-virtual/server.mjs
@@ -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). ┃
+// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+// This is just a file server, the implementation is in `src/index.js`.
+
+import http from "http";
+import { fileURLToPath } from "url";
+import { dirname } from "path";
+import { cwd_static_file_handler } from "@perspective-dev/client";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+// Create HTTP server for serving static files
+const httpServer = http.createServer((req, res) =>
+ cwd_static_file_handler(req, res, [`${__dirname}/dist`, __dirname]),
+);
+
+httpServer.listen(8080, () => {
+ console.log("Server listening on http://localhost:8080");
+});
diff --git a/examples/esbuild-duckdb-virtual/src/index.html b/examples/esbuild-duckdb-virtual/src/index.html
new file mode 100644
index 0000000000..f44c409e5d
--- /dev/null
+++ b/examples/esbuild-duckdb-virtual/src/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Web Worker VirtualServer Example
+
+
+
+
+
+
+
+
diff --git a/examples/esbuild-duckdb-virtual/src/index.ts b/examples/esbuild-duckdb-virtual/src/index.ts
new file mode 100644
index 0000000000..baa7646209
--- /dev/null
+++ b/examples/esbuild-duckdb-virtual/src/index.ts
@@ -0,0 +1,86 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ Copyright (c) 2017, the Perspective Authors. ┃
+// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
+// ┃ This file is part of the Perspective library, distributed under the terms ┃
+// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
+// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+import perspective from "@perspective-dev/client";
+import perspective_viewer from "@perspective-dev/viewer";
+import "@perspective-dev/viewer-datagrid";
+import "@perspective-dev/viewer-d3fc";
+
+import "@perspective-dev/viewer/dist/css/themes.css";
+import "@perspective-dev/viewer/dist/css/pro.css";
+
+// @ts-ignore
+import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
+
+// @ts-ignore
+import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
+
+import { DuckDBHandler } from "@perspective-dev/client/dist/esm/virtual_servers/duckdb.js";
+import * as duckdb from "@duckdb/duckdb-wasm";
+
+// @ts-ignore
+import SUPERSTORE_ARROW from "superstore-arrow/superstore.lz4.arrow";
+
+await Promise.all([
+ perspective.init_server(fetch(SERVER_WASM)),
+ perspective_viewer.init_client(fetch(CLIENT_WASM)),
+]);
+
+async function initializeDuckDB() {
+ const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
+ const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
+ const worker_url = URL.createObjectURL(
+ new Blob([`importScripts("${bundle.mainWorker}");`], {
+ type: "text/javascript",
+ }),
+ );
+
+ const duckdb_worker = new Worker(worker_url);
+ const logger = new duckdb.VoidLogger();
+ const db = new duckdb.AsyncDuckDB(logger, duckdb_worker);
+ await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
+ URL.revokeObjectURL(worker_url);
+ const conn = await db.connect();
+ await conn.query(`
+ SET default_null_order=NULLS_FIRST_ON_ASC_LAST_ON_DESC;
+ `);
+
+ console.log("DuckDB initialized");
+ return conn;
+}
+
+async function loadSampleData(db: duckdb.AsyncDuckDBConnection) {
+ // const c = await db.connect();
+ try {
+ const response = await fetch(SUPERSTORE_ARROW);
+ const arrayBuffer = await response.arrayBuffer();
+ await db.insertArrowFromIPCStream(new Uint8Array(arrayBuffer), {
+ name: "data_source_one",
+ create: true,
+ });
+ } catch (error) {
+ console.error("Error loading Arrow data:", error);
+ }
+}
+
+const db = await initializeDuckDB();
+await perspective.init_client(fetch(CLIENT_WASM));
+await loadSampleData(db);
+const server = perspective.createMessageHandler(new DuckDBHandler(db));
+const client = await perspective.worker(server);
+
+const viewer = document.querySelector("perspective-viewer")!;
+viewer.load(client);
+viewer.restore({
+ table: "data_source_one",
+ group_by: ["State"],
+});
diff --git a/examples/python-duckdb-virtual/index.html b/examples/python-duckdb-virtual/index.html
new file mode 100644
index 0000000000..c173a35350
--- /dev/null
+++ b/examples/python-duckdb-virtual/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/python-duckdb-virtual/package.json b/examples/python-duckdb-virtual/package.json
new file mode 100644
index 0000000000..fbdb042fec
--- /dev/null
+++ b/examples/python-duckdb-virtual/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "python-duckdb-virtual",
+ "private": true,
+ "version": "3.7.4",
+ "description": "An example of streaming a `perspective-python` server to the browser.",
+ "scripts": {
+ "start": "PYTHONPATH=../../python/perspective python3 server.py"
+ },
+ "keywords": [],
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@perspective-dev/client": "workspace:^",
+ "@perspective-dev/viewer": "workspace:^",
+ "@perspective-dev/viewer-d3fc": "workspace:^",
+ "@perspective-dev/viewer-datagrid": "workspace:^",
+ "@perspective-dev/workspace": "workspace:^",
+ "superstore-arrow": "catalog:"
+ },
+ "devDependencies": {
+ "npm-run-all": "catalog:"
+ }
+}
diff --git a/examples/python-duckdb-virtual/server.py b/examples/python-duckdb-virtual/server.py
new file mode 100644
index 0000000000..8538a92aa7
--- /dev/null
+++ b/examples/python-duckdb-virtual/server.py
@@ -0,0 +1,66 @@
+# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+# ┃ 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). ┃
+# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+from pathlib import Path
+
+import duckdb
+import perspective
+import perspective.handlers.tornado
+import perspective.virtual_servers.duckdb
+import tornado.ioloop
+import tornado.web
+import tornado.websocket
+
+from loguru import logger
+from tornado.web import StaticFileHandler
+
+
+INPUT_FILE = (
+ Path(__file__).parent.resolve()
+ / "node_modules"
+ / "superstore-arrow"
+ / "superstore.parquet"
+)
+
+
+if __name__ == "__main__":
+ db = duckdb.connect(":memory:perspective")
+ db.sql(
+ f"""
+ SET default_null_order=NULLS_FIRST_ON_ASC_LAST_ON_DESC;
+ CREATE TABLE data_source_one AS
+ SELECT * FROM '{INPUT_FILE}';
+ """,
+ )
+
+ virtual_server = perspective.virtual_servers.duckdb.DuckDBVirtualServer(db)
+ app = tornado.web.Application(
+ [
+ (
+ r"/websocket",
+ perspective.handlers.tornado.PerspectiveTornadoHandler,
+ {"perspective_server": virtual_server},
+ ),
+ (r"/node_modules/(.*)", StaticFileHandler, {"path": "../../node_modules/"}),
+ (
+ r"/(.*)",
+ StaticFileHandler,
+ {"path": "./", "default_filename": "index.html"},
+ ),
+ ],
+ websocket_max_message_size=100 * 1024 * 1024,
+ )
+
+ app.listen(3000)
+ logger.info("Listening on http://localhost:3000")
+ loop = tornado.ioloop.IOLoop.current()
+ loop.start()
diff --git a/packages/react/test/js/react.spec.tsx b/packages/react/test/js/react.spec.tsx
index 57dd2c6348..41db762eb1 100644
--- a/packages/react/test/js/react.spec.tsx
+++ b/packages/react/test/js/react.spec.tsx
@@ -15,22 +15,6 @@ import { test, expect } from "@playwright/experimental-ct-react";
import { App } from "./basic.story";
import { EmptyWorkspace, SingleView } from "./workspace.story";
-async function retryUntilSuccess(
- fn: () => Promise,
- { maxAttempts = 5, delay = 1000 } = {},
-): Promise {
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
- try {
- const r = await fn();
- if (r) {
- return true;
- }
- } catch {}
- await new Promise((r) => setTimeout(r, delay));
- }
- return false;
-}
-
test.describe("Perspective React", () => {
test("The viewer loads with data in it", async ({ page, mount }) => {
const comp = await mount();
@@ -57,6 +41,7 @@ test.describe("Perspective React", () => {
document.querySelector("perspective-workspace")!.children
.length === 3,
);
+
await expect(viewer).toHaveCount(3);
await toggleMount.click();
await workspace.waitFor({ state: "detached" });
diff --git a/packages/react/test/js/workspace.story.tsx b/packages/react/test/js/workspace.story.tsx
index 55f7be831d..4074de77a6 100644
--- a/packages/react/test/js/workspace.story.tsx
+++ b/packages/react/test/js/workspace.story.tsx
@@ -61,11 +61,10 @@ const WorkspaceApp: React.FC = (props) => {
mounted: true,
});
- const onClickAddViewer = async () => {
+ const onClickAddViewer = () => {
const name = window.crypto.randomUUID();
const data = `a,b,c\n${Math.random()},${Math.random()},${Math.random()}`;
- const t = await CLIENT.table(data, { name });
- console.log(await t.get_name());
+ CLIENT.table(data, { name });
const nextId = Workspace.genId(state.layout);
const layout = Workspace.addViewer(
state.layout,
diff --git a/packages/viewer-datagrid/src/less/regular_table.less b/packages/viewer-datagrid/src/less/regular_table.less
index 7a50886164..67604bf94d 100644
--- a/packages/viewer-datagrid/src/less/regular_table.less
+++ b/packages/viewer-datagrid/src/less/regular_table.less
@@ -27,22 +27,25 @@
mask-size: cover;
}
-perspective-viewer:not([settings]) {
- @include settings-not-open;
-}
-
-:host-context(perspective-viewer:not([settings])) {
- @include settings-not-open;
-}
-
-@mixin settings-not-open {
- regular-table table tr.rt-autosize + tr th {
- height: 0px;
- span {
- display: none;
- }
- }
-}
+// // TODO this makes the UI flash a CSS layout for a millsiecond when toggling
+// // settings butit could be fixed.
+
+// perspective-viewer:not([settings]) {
+// @include settings-not-open;
+// }
+
+// :host-context(perspective-viewer:not([settings])) {
+// @include settings-not-open;
+// }
+
+// @mixin settings-not-open {
+// regular-table table tr.rt-autosize + tr th {
+// height: 0px;
+// span {
+// display: none;
+// }
+// }
+// }
@mixin settings-open {
.psp-menu-enabled {
@@ -89,11 +92,11 @@ perspective-viewer:not([settings]) {
}
}
-perspective-viewer[settings] {
+perspective-viewer {
@include settings-open;
}
-:host-context(perspective-viewer[settings]) {
+:host {
@include settings-open;
}
diff --git a/packages/viewer-datagrid/src/ts/style_handlers/body.ts b/packages/viewer-datagrid/src/ts/style_handlers/body.ts
index cab30f35ad..6c479401f3 100644
--- a/packages/viewer-datagrid/src/ts/style_handlers/body.ts
+++ b/packages/viewer-datagrid/src/ts/style_handlers/body.ts
@@ -117,6 +117,8 @@ export function applyBodyCellStyles(
"psp-menu-open",
column_name === this._column_settings_selected_column,
);
+ } else {
+ td.classList.toggle("psp-menu-open", false);
}
td.classList.toggle(
diff --git a/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts b/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts
index 71baba4833..7a51bbc0a3 100644
--- a/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts
+++ b/packages/viewer-datagrid/src/ts/style_handlers/consolidated.ts
@@ -22,11 +22,6 @@ import type {
SelectedPosition,
} from "../types.js";
-import { cell_style_numeric } from "./table_cell/numeric.js";
-import { cell_style_string } from "./table_cell/string.js";
-import { cell_style_datetime } from "./table_cell/datetime.js";
-import { cell_style_boolean } from "./table_cell/boolean.js";
-import { cell_style_row_header } from "./table_cell/row_header.js";
import { applyFocusStyle } from "./focus.js";
import { styleColumnHeaderRow } from "./column_header.js";
import { applyColumnHeaderStyles } from "./editable.js";
@@ -117,13 +112,8 @@ export function createConsolidatedStyleListener(
// Toggle edit mode class on datagrid
datagrid.classList.toggle("edit-mode-allowed", isEditableAllowed);
-
- // ========== PHASE 1: Collect all metadata (READ PHASE) ==========
const bodyCells: CollectedCell[] = [];
- const headerCells: CollectedCell[] = [];
const groupHeaderRows: CollectedHeaderRow[] = [];
-
- // Collect body cells (tbody)
const tbody = regularTable.children[0]?.children[1];
if (tbody) {
for (const tr of tbody.children) {
@@ -131,6 +121,7 @@ export function createConsolidatedStyleListener(
const metadata = regularTable.getMeta(cell) as
| CellMetaExtended
| undefined;
+
if (metadata) {
const isHeader = cell.tagName === "TH";
bodyCells.push({
@@ -151,10 +142,12 @@ export function createConsolidatedStyleListener(
row: tr as HTMLTableRowElement,
cells: [],
};
+
for (const cell of tr.children) {
const metadata = regularTable.getMeta(cell) as
| CellMetadata
| undefined;
+
rowData.cells.push({
element: cell as HTMLTableCellElement,
metadata,
@@ -164,9 +157,6 @@ export function createConsolidatedStyleListener(
}
}
- // ========== PHASE 2: Apply all styles (WRITE PHASE) ==========
-
- // 2a. Style body cells
this._applyBodyCellStyles(
bodyCells,
plugins,
@@ -179,18 +169,12 @@ export function createConsolidatedStyleListener(
viewer,
);
- // 2b. Style group headers
this._applyGroupHeaderStyles(groupHeaderRows, regularTable);
-
- // 2c. Style column headers
this._applyColumnHeaderStyles(groupHeaderRows, regularTable, viewer);
-
- // 2d. Apply focus
this._applyFocusStyle(bodyCells, regularTable, selectedPositionMap);
};
}
-// Extend DatagridModel prototype with styling methods
declare module "../types.js" {
interface DatagridModel {
_applyBodyCellStyles(
diff --git a/packages/viewer-datagrid/test/js/superstore.spec.js b/packages/viewer-datagrid/test/js/superstore.spec.js
index fac74ef000..2e00eed91a 100644
--- a/packages/viewer-datagrid/test/js/superstore.spec.js
+++ b/packages/viewer-datagrid/test/js/superstore.spec.js
@@ -192,11 +192,12 @@ test.describe("Datagrid with superstore data set", () => {
await td.click();
await td.asElement().fill("Test");
await page.evaluate(() => document.activeElement.blur());
-
const result = await page.evaluate(async () => {
+ await document.querySelector("perspective-viewer").flush();
const view = await document
.querySelector("perspective-viewer")
.getView();
+
const json = await view.to_json_string({ end_row: 4 });
return json;
});
diff --git a/packages/workspace/src/less/viewer.less b/packages/workspace/src/less/viewer.less
index e2de711ad8..da4463c516 100644
--- a/packages/workspace/src/less/viewer.less
+++ b/packages/workspace/src/less/viewer.less
@@ -97,7 +97,7 @@
border: 1px solid red;
}
-::slotted(perspective-viewer:not([settings])) {
+::slotted(perspective-viewer:not(.widget-maximize)) {
--status-bar--padding: 0 36px 0 8px;
}
diff --git a/packages/workspace/src/themes/pro-dark.less b/packages/workspace/src/themes/pro-dark.less
index 396ffc5b82..7571e72d27 100644
--- a/packages/workspace/src/themes/pro-dark.less
+++ b/packages/workspace/src/themes/pro-dark.less
@@ -27,7 +27,7 @@ perspective-workspace perspective-viewer {
--plugin-selector--height: 47px;
}
-perspective-workspace perspective-viewer[settings] {
+perspective-workspace perspective-viewer.widget-maximize {
--modal-panel--margin: -4px 0 -4px 0;
--status-bar--border-radius: 6px 0 0 0;
--main-column--margin: 3px 0 3px 3px;
diff --git a/packages/workspace/src/themes/pro.less b/packages/workspace/src/themes/pro.less
index 2e64c1d07e..29008f31d9 100644
--- a/packages/workspace/src/themes/pro.less
+++ b/packages/workspace/src/themes/pro.less
@@ -28,7 +28,7 @@ perspective-workspace {
background-color: #dadada;
}
-perspective-workspace perspective-viewer[settings] {
+perspective-workspace perspective-viewer.widget-maximize {
--modal-panel--margin: -4px 0 -4px 0;
--status-bar--border-radius: 6px 0 0 0;
--main-column--margin: 3px 0 3px 3px;
diff --git a/packages/workspace/src/ts/workspace/workspace.ts b/packages/workspace/src/ts/workspace/workspace.ts
index e184c8045d..7d33165714 100644
--- a/packages/workspace/src/ts/workspace/workspace.ts
+++ b/packages/workspace/src/ts/workspace/workspace.ts
@@ -1034,7 +1034,7 @@ export class PerspectiveWorkspace extends SplitPanel {
for (const client of this.client) {
const tables = await client.get_hosted_table_names();
- if (tables.indexOf(table) > -1) {
+ if (table && tables.indexOf(table) > -1) {
await viewer.load(client);
return await this._createWidget({
config,
@@ -1166,7 +1166,7 @@ export class PerspectiveWorkspace extends SplitPanel {
);
widget.viewer.addEventListener(
- "perspective-toggle-settings-before",
+ "perspective-toggle-settings",
settings_after,
);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2812084a1d..2b3c4fd0c4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,6 +12,9 @@ catalogs:
'@d3fc/d3fc-element':
specifier: 6.2.0
version: 6.2.0
+ '@duckdb/duckdb-wasm':
+ specifier: ^1.30.0
+ version: 1.32.0
'@fontsource/roboto-mono':
specifier: 4.5.10
version: 4.5.10
@@ -151,8 +154,8 @@ catalogs:
specifier: ^18
version: 18.3.1
regular-table:
- specifier: '=0.7.1'
- version: 0.7.1
+ specifier: '=0.7.3'
+ version: 0.7.3
stoppable:
specifier: '=1.1.0'
version: 1.1.0
@@ -177,6 +180,9 @@ catalogs:
vite:
specifier: '>=6 <7'
version: 6.4.1
+ web-worker:
+ specifier: 1.4.1
+ version: 1.4.1
webpack:
specifier: '>=5 <6'
version: 5.102.1
@@ -364,6 +370,34 @@ importers:
specifier: 'catalog:'
version: 0.25.11
+ examples/esbuild-duckdb-virtual:
+ dependencies:
+ '@duckdb/duckdb-wasm':
+ specifier: 'catalog:'
+ version: 1.32.0
+ '@perspective-dev/client':
+ specifier: workspace:^
+ version: link:../../rust/perspective-js
+ '@perspective-dev/server':
+ specifier: workspace:^
+ version: link:../../rust/perspective-server
+ '@perspective-dev/viewer':
+ specifier: workspace:^
+ version: link:../../rust/perspective-viewer
+ '@perspective-dev/viewer-d3fc':
+ specifier: workspace:^
+ version: link:../../packages/viewer-d3fc
+ '@perspective-dev/viewer-datagrid':
+ specifier: workspace:^
+ version: link:../../packages/viewer-datagrid
+ superstore-arrow:
+ specifier: 'catalog:'
+ version: 3.2.0
+ devDependencies:
+ esbuild:
+ specifier: 'catalog:'
+ version: 0.25.11
+
examples/esbuild-example:
dependencies:
'@perspective-dev/client':
@@ -413,6 +447,31 @@ importers:
examples/python-aiohttp: {}
+ examples/python-duckdb-virtual:
+ dependencies:
+ '@perspective-dev/client':
+ specifier: workspace:^
+ version: link:../../rust/perspective-js
+ '@perspective-dev/viewer':
+ specifier: workspace:^
+ version: link:../../rust/perspective-viewer
+ '@perspective-dev/viewer-d3fc':
+ specifier: workspace:^
+ version: link:../../packages/viewer-d3fc
+ '@perspective-dev/viewer-datagrid':
+ specifier: workspace:^
+ version: link:../../packages/viewer-datagrid
+ '@perspective-dev/workspace':
+ specifier: workspace:^
+ version: link:../../packages/workspace
+ superstore-arrow:
+ specifier: 'catalog:'
+ version: 3.2.0
+ devDependencies:
+ npm-run-all:
+ specifier: 'catalog:'
+ version: 4.1.5
+
examples/python-starlette: {}
examples/python-tornado:
@@ -754,7 +813,7 @@ importers:
version: 3.1.2
regular-table:
specifier: 'catalog:'
- version: 0.7.1
+ version: 0.7.3
devDependencies:
'@perspective-dev/esbuild-plugin':
specifier: 'workspace:'
@@ -886,6 +945,9 @@ importers:
specifier: 'catalog:'
version: 8.18.3
devDependencies:
+ '@duckdb/duckdb-wasm':
+ specifier: 'catalog:'
+ version: 1.32.0
'@perspective-dev/esbuild-plugin':
specifier: 'workspace:'
version: link:../../tools/esbuild-plugin
@@ -928,6 +990,9 @@ importers:
typescript:
specifier: 'catalog:'
version: 5.9.3
+ web-worker:
+ specifier: 'catalog:'
+ version: 1.4.1
zx:
specifier: 'catalog:'
version: 8.8.5
@@ -2436,6 +2501,9 @@ packages:
resolution: {integrity: sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==}
engines: {node: '>=20.0'}
+ '@duckdb/duckdb-wasm@1.32.0':
+ resolution: {integrity: sha512-IewXTNYEjsZCPE9weUWgtjGxUlMRo7qhX0GF6tq/KjK8bnY+RAl4cyUdYUfcdzbyb4b9ZxPC+FOsCcxgaKFWMg==}
+
'@esbuild/aix-ppc64@0.25.11':
resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
engines: {node: '>=18'}
@@ -3991,6 +4059,10 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
+ apache-arrow@17.0.0:
+ resolution: {integrity: sha512-X0p7auzdnGuhYMVKYINdQssS4EcKec9TCXyez/qtJt32DrIMGbzqiaMiQ0X6fQlQpw8Fl0Qygcv4dfRAr5Gu9Q==}
+ hasBin: true
+
apache-arrow@18.1.0:
resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==}
hasBin: true
@@ -7722,8 +7794,8 @@ packages:
resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==}
hasBin: true
- regular-table@0.7.1:
- resolution: {integrity: sha512-dIt9z+ZIHEhLujDbDMcuwJPZK6HVKFL1qnvyaUHrAbFx8/SoHJyvktwJdNpMkRfFwEdkmBYmtd5dei5BymxvZg==}
+ regular-table@0.7.3:
+ resolution: {integrity: sha512-u0JgzFO/E2BDCQomvnREk2V8oa6E4PNK3KmWUB6lk1wK3v/1MrwEicdBkSZC9mwElQ1lMw0vbh8vn8J9/mdRtA==}
engines: {node: '>=16'}
rehype-raw@7.0.0:
@@ -8683,6 +8755,9 @@ packages:
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+ web-worker@1.4.1:
+ resolution: {integrity: sha512-bEYkHEaeUTjiWscVoW7UBLmAV1S9v0AELr9+3B94Ps1G6E5N/jmSth1e5RZoWbLZWqkI/eyb7KT3sto0ugRpLg==}
+
webdriver-bidi-protocol@0.3.8:
resolution: {integrity: sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==}
@@ -11070,6 +11145,10 @@ snapshots:
- uglify-js
- webpack-cli
+ '@duckdb/duckdb-wasm@1.32.0':
+ dependencies:
+ apache-arrow: 17.0.0
+
'@esbuild/aix-ppc64@0.25.11':
optional: true
@@ -13203,6 +13282,18 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
+ apache-arrow@17.0.0:
+ dependencies:
+ '@swc/helpers': 0.5.17
+ '@types/command-line-args': 5.2.3
+ '@types/command-line-usage': 5.0.4
+ '@types/node': 20.19.23
+ command-line-args: 5.2.1
+ command-line-usage: 7.0.3
+ flatbuffers: 24.12.23
+ json-bignum: 0.0.3
+ tslib: 2.8.1
+
apache-arrow@18.1.0:
dependencies:
'@swc/helpers': 0.5.17
@@ -17636,7 +17727,7 @@ snapshots:
dependencies:
jsesc: 3.1.0
- regular-table@0.7.1: {}
+ regular-table@0.7.3: {}
rehype-raw@7.0.0:
dependencies:
@@ -18733,6 +18824,8 @@ snapshots:
web-namespaces@2.0.1: {}
+ web-worker@1.4.1: {}
+
webdriver-bidi-protocol@0.3.8: {}
webidl-conversions@3.0.1: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 1db7d1ebcc..1e95bb7563 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -39,11 +39,12 @@ catalog:
"pro_self_extracting_wasm": "0.0.9"
"react-dom": "^18"
"react": "^18"
- "regular-table": "=0.7.1"
+ "regular-table": "=0.7.3"
"stoppable": "=1.1.0"
"ws": "^8.17.0"
# Dev Dependencies
+ "@duckdb/duckdb-wasm": "^1.30.0"
"@fontsource/roboto-mono": "4.5.10"
"@iarna/toml": "3.0.0"
"@jupyterlab/builder": "^4"
@@ -95,4 +96,5 @@ catalog:
"vite": ">=6 <7"
"webpack-cli": ">=5 <6"
"webpack": ">=5 <6"
+ "web-worker": "1.4.1"
"zx": ">=8 <9"
diff --git a/rust/bundle/main.rs b/rust/bundle/main.rs
index b6e779f398..28beb8a6aa 100644
--- a/rust/bundle/main.rs
+++ b/rust/bundle/main.rs
@@ -39,13 +39,24 @@ use wasm_opt::{Feature, OptimizationOptions};
/// field to the host platform.
fn build(pkg: Option<&str>, is_release: bool, features: Vec) {
let features = format!("tracing/release_max_level_warn,{}", features.join(","));
+
+ // Build RUSTFLAGS including target-specific flags from config.toml and new
+ // panic flags These are the flags from .cargo/config.toml for
+ // wasm32-unknown-unknown target
+ let target_flags = [
+ "--cfg=getrandom_backend=\"wasm_js\"",
+ "--cfg=web_sys_unstable_apis",
+ "-Ctarget-feature=+bulk-memory,+simd128,+relaxed-simd,+reference-types",
+ ];
+
+ let rustflags = target_flags.join(" ");
let mut cmd = Command::new("cargo");
- cmd.args(["build"])
+ cmd.env("RUSTFLAGS", rustflags)
+ .args(["build"])
.args(["--lib"])
.args(["--features", &features])
- .args(["--target", "wasm32-unknown-unknown"])
- .args(["-Z", "build-std=std,panic_abort"])
- .args(["-Z", "build-std-features=panic_immediate_abort"]);
+ .args(["--target", "wasm32-unknown-unknown"]);
+ // .args(["-Z", "build-std=std"]);
if is_release {
cmd.args(["--release"]);
diff --git a/rust/perspective-client/Cargo.toml b/rust/perspective-client/Cargo.toml
index 7600933cef..980961cb0b 100644
--- a/rust/perspective-client/Cargo.toml
+++ b/rust/perspective-client/Cargo.toml
@@ -57,11 +57,12 @@ path = "src/rust/lib.rs"
[build-dependencies]
prost-build = { version = "0.12.3" }
# https://github.com/abseil/abseil-cpp/issues/1241#issuecomment-2138616329
-protobuf-src = { version = "=2.0.1", optional = true }
+protobuf-src = { version = "=2.1.1", optional = true }
[dependencies]
async-lock = { version = "2.5.0" }
futures = { version = "0.3.28" }
+indexmap = { version = "2.2.6", features = ["serde"] }
itertools = { version = "0.10.1" }
paste = { version = "1.0.12" }
prost-types = { version = "0.12.3" }
diff --git a/rust/perspective-client/src/rust/lib.rs b/rust/perspective-client/src/rust/lib.rs
index 4fdc1ee333..2a7784a654 100644
--- a/rust/perspective-client/src/rust/lib.rs
+++ b/rust/perspective-client/src/rust/lib.rs
@@ -39,16 +39,18 @@ mod session;
mod table;
mod table_data;
mod view;
+pub mod virtual_server;
pub mod config;
#[rustfmt::skip]
#[allow(clippy::all)]
-mod proto;
+pub mod proto;
pub mod utils;
pub use crate::client::{Client, ClientHandler, Features, ReconnectCallback, SystemInfo};
+use crate::proto::HostedTable;
pub use crate::session::{ProxySession, Session};
pub use crate::table::{
DeleteOptions, ExprValidationResult, Table, TableInitOptions, TableReadFormat, UpdateOptions,
@@ -66,6 +68,16 @@ pub mod vendor {
pub use paste;
}
+impl From<&str> for HostedTable {
+ fn from(entity_id: &str) -> Self {
+ HostedTable {
+ entity_id: entity_id.to_string(),
+ index: None,
+ limit: None,
+ }
+ }
+}
+
/// Assert that an implementation of domain language wrapper for [`Table`]
/// implements the expected API. As domain languages have different API needs,
/// a trait isn't useful for asserting that the entire API is implemented,
diff --git a/rust/perspective-client/src/rust/virtual_server/data.rs b/rust/perspective-client/src/rust/virtual_server/data.rs
new file mode 100644
index 0000000000..5a7cbf4beb
--- /dev/null
+++ b/rust/perspective-client/src/rust/virtual_server/data.rs
@@ -0,0 +1,233 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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). ┃
+// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+use std::error::Error;
+use std::ops::{Deref, DerefMut};
+
+use indexmap::IndexMap;
+use serde::Serialize;
+
+use crate::config::Scalar;
+
+/// A column of data returned from a virtual server query.
+///
+/// Each variant represents a different column type, containing a vector
+/// of optional values. `None` values represent null/missing data.
+#[derive(Debug, Serialize)]
+#[serde(untagged)]
+pub enum VirtualDataColumn {
+ Boolean(Vec