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
+4
View File
@@ -0,0 +1,4 @@
# JavaScript
Guides for using Perspective in the browser and Node.js, including the
`perspective` data engine and the `<perspective-viewer>` UI component.
@@ -0,0 +1,73 @@
# Customizing `perspective.worker()`
`perspective.worker()` creates a `Client` that connects to a Perspective data
engine. By default it spins up a dedicated `Worker` running the built-in
WebAssembly engine, but you can pass an argument to change this behavior:
- A **`Worker`**, **`SharedWorker`**, or **`ServiceWorker`** — runs the
built-in engine in a different worker context.
- A **`MessagePort`** from `createMessageHandler()` — connects to a
[Virtual Server](virtual_server/custom.md) instead of the built-in engine.
## Built-in engine with a custom Worker
Pass a `Worker`, `SharedWorker`, or `ServiceWorker` that loads the worker script
distributed at
`"@perspective-dev/client/dist/cdn/perspective-server.worker.js"`.
<span class="warning">`SharedWorker` and `ServiceWorker` have more complicated
behavior compared to a dedicated `Worker`, and will need special consideration
to integrate (or debug).</span>
### Dedicated `Worker`
```javascript
const worker = await perspective.worker(new Worker(url));
```
### `SharedWorker`
```javascript
const worker = await perspective.worker(new SharedWorker(url));
```
### `ServiceWorker`
```javascript
const registration = await navigator.serviceWorker.register(url, {
scope: "", // Your scope here
});
const worker = await perspective.worker(registration.active);
```
## Virtual Server
Instead of the built-in WebAssembly engine, `perspective.worker()` can connect
to a Virtual Server — an adapter that translates Perspective queries into
operations on an external data source such as
[DuckDB](virtual_server/duckdb.md) or
[ClickHouse](virtual_server/clickhouse.md).
Use `perspective.createMessageHandler()` with a `VirtualServerHandler` to create
a `MessagePort`, then pass it to `worker()`:
```javascript
import perspective from "@perspective-dev/client";
const handler = {
/* VirtualServerHandler implementation */
};
const server = perspective.createMessageHandler(handler);
const client = await perspective.worker(server);
const table = await client.open_table("my_table");
```
The returned `Client` works identically to one backed by the built-in engine —
you can pass it to `<perspective-viewer>.load()`, call `open_table()`, etc. The
difference is that queries are fulfilled by your handler rather than the WASM
engine.
For the full `VirtualServerHandler` interface and a worked example, see
[Implementing a custom Virtual Server](virtual_server/custom.md).
+25
View File
@@ -0,0 +1,25 @@
# Deleting a `table()` or `view()`
Unlike standard JavaScript objects, Perspective objects such as `table()` and
`view()` store their associated data in the WebAssembly heap. Because of this,
as well as the current lack of a hook into the JavaScript runtime's garbage
collector from WebAssembly, the memory allocated to these Perspective objects
does not automatically get cleaned up when the object falls out of scope.
In order to prevent memory leaks and reclaim the memory associated with a
Perspective `table()` or `view()`, you must call the `delete()` method:
```javascript
await view.delete();
// This method will throw an exception if there are still `view()`s depending
// on this `table()`!
await table.delete();
```
Similarly, `<perspective-viewer>` Custom Elements do not delete the memory
allocated for the UI when they are removed from the DOM.
```javascript
await viewer.delete();
```
+40
View File
@@ -0,0 +1,40 @@
# Listening for events
The `<perspective-viewer>` Custom Element fires all the same HTML `Event`s that
standard DOM `HTMLElement` objects fire, in addition to a few custom
`CustomEvent`s which relate to UI updates including those initiaed through user
interaction.
## Update events
Whenever a `<perspective-viewer>`s underlying `table()` is changed via the
`load()` or `update()` methods, a `perspective-view-update` DOM event is fired.
Similarly, `view()` updates instigated either through the Attribute API or
through user interaction will fire a `perspective-config-update` event:
```javascript
elem.addEventListener("perspective-config-update", function (event) {
var config = elem.save();
console.log("The view() config has changed to " + JSON.stringify(config));
});
```
## Click events
Whenever a `<perspective-viewer>`'s grid or chart is clicked, a
`perspective-click` DOM event is fired containing a detail object with `config`,
`column_names`, and `row`.
The `config` object contains an array of `filters` that can be applied to a
`<perspective-viewer>` through the use of `restore()` updating it to show the
filtered subset of data.
The `column_names` property contains an array of matching columns, and the `row`
property returns the associated row data.
```javascript
elem.addEventListener("perspective-click", function (event) {
var config = event.detail.config;
elem.restore(config);
});
```
+172
View File
@@ -0,0 +1,172 @@
# JavaScript - Importing with or without a bundler
Perspective requires the browser to have access to Perspective's `.wasm`
binaries _in addition_ to the bundled `.js` files, and as a result the build
process requires a few extra steps. Perspective's NPM releases come with
multiple prebuilt configurations.
## ESM builds with a bundler
The recommended builds for production use are packaged as ES Modules and require
a _bootstrapping_ step in order to acquire the `.wasm` binaries and initialize
Perspective's JavaScript with them. Because they have no hard-coded dependencies
on the `.wasm` paths, they are ideal for use with JavaScript bundlers such as
ESBuild, Rollup, Vite or Webpack.
ESM builds must be _bootstrapped_ with their `.wasm` binaries to initialize. The
`wasm` binaries can be found in their respective `dist/wasm` directories.
```javascript
import perspective_viewer from "@perspective-dev/viewer";
import perspective from "@perspective-dev/client";
// TODO These paths must be provided by the bundler!
const SERVER_WASM = ... // "@perspective-dev/server/dist/wasm/perspective-server.wasm"
const CLIENT_WASM = ... // "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"
await Promise.all([
perspective.init_server(SERVER_WASM),
perspective_viewer.init_client(CLIENT_WASM),
]);
// Now Perspective API will work!
const worker = await perspective.worker();
const viewer = document.createElement("perspective-viewer");
```
The exact syntax will vary slightly depending on the bundler.
### Vite
```javascript
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm?url";
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm?url";
await Promise.all([
perspective.init_server(fetch(SERVER_WASM)),
perspective_viewer.init_client(fetch(CLIENT_WASM)),
]);
```
You'll also need to target `esnext` in your `vite.config.js` in order to run the
`build` step:
```javascript
import { defineConfig } from "vite";
export default defineConfig({
build: {
target: "esnext",
},
});
```
### ESBuild
```javascript
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
await Promise.all([
perspective.init_server(fetch(SERVER_WASM)),
perspective_viewer.init_client(fetch(CLIENT_WASM)),
]);
```
ESBuild config JSON to encode this asset as a `file`:
```javascript
{
// ...
"loader": {
// ...
".wasm": "file"
}
}
```
### Webpack
```javascript
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
await Promise.all([
perspective.init_server(SERVER_WASM),
perspective_viewer.init_client(CLIENT_WASM),
]);
```
Webpack config:
```javascript
{
// ...
module: {
// ...
rules: [
// ...
{
test: /\.wasm$/,
type: "asset/resource"
},
]
},
experiments: {
// ...
asyncWebAssembly: false,
syncWebAssembly: false,
},
}
```
## Inline builds with a bundler
<span class="warning">Inline builds are deprecated and will be removed in a
future release.</span>
Perspective's _Inline_ Builds work by _inlining_ WebAssembly binary content as
a base64-encoded string. While inline builds work with most bundlers and _do
not_ require bootstrapping, there is an inherent file-size and boot-performance
penalty. Prefer your bundler's inlining features and Perspective ESM builds
where possible.
```javascript
import "@perspective-dev/viewer/dist/esm/perspective-viewer.inline.js";
import psp from "@perspective-dev/client/dist/esm/perspective.inline.js";
```
## CDN builds
Perspective's CDN builds are good for non-bundled scenarios, such as importing
directly from a `<script>` tag. CDN builds _do not_ require _bootstrapping_ the
WebAssembly binaries, but they also generally _do not_ work with bundlers.
CDN builds are in ES Module format, thus to include them via a CDN they must be
imported from a `<script type="module">`:
```html
<script type="module">
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
import perspective from "https://cdn.jsdelivr.net/npm/@perspective-dev/client/dist/cdn/perspective.js";
// .. Do stuff here ..
</script>
```
## Node.js builds
The Node.js runtime for the `@perspective-dev/client` module runs in-process by
default and does not implement a `child_process` interface. Hence, there is no
`worker()` method, and the module object itself directly exports the full
`perspective` API.
```javascript
const perspective = require("@perspective-dev/client");
```
In Node.js, perspective does not run in a WebWorker (as this API does not exist
in Node.js), so no need to call the `.worker()` factory function - the
`perspective` library exports the functions directly and run synchronously in
the main process.
+54
View File
@@ -0,0 +1,54 @@
# JavaScript Installation and Module Structure
Perspective is designed for flexibility, allowing developers to pick and choose
which modules they need. The main modules are:
- `@perspective-dev/client`
The data engine library, as both a browser ES6 and Node.js module. Provides a
WebAssembly, WebWorker (browser) and Process (node.js) runtime.
- `@perspective-dev/viewer`
A user-configurable visualization widget, bundled as a
[Web Component](https://www.webcomponents.org/introduction). This module
includes the core data engine module as a dependency.
`<perspective-viewer>` by itself only implements a trivial debug renderer, which
prints the currently configured `view()` as a CSV. Plugin modules are packaged
separately and must be imported individually.
- `@perspective-dev/viewer-datagrid`
A custom high-performance data-grid component based on HTML `<table>`.
- `@perspective-dev/viewer-charts`
A set of charting components base on WebGL.
When imported after `@perspective-dev/viewer`, the plugin modules will register
themselves automatically, and the renderers they export will be available in the
`plugin` dropdown in the `<perspective-viewer>` UI.
## Browser
Perspective's WebAssembly data engine is available via NPM in the same package
as its Node.js counterpart, `@perspective-dev/client`. The Perspective Viewer UI
(which has no Node.js component) must be installed separately:
```bash
$ npm add @perspective-dev/client @perspective-dev/viewer
```
By itself, `@perspective-dev/viewer` does not provide any visualizations, only
the UI framework. Perspective _Plugins_ provide visualizations and must be
installed separately. All Plugins are optional - but a `<perspective-viewer>`
without Plugins would be rather boring!
```bash
$ npm add @perspective-dev/viewer-charts @perspective-dev/viewer-datagrid
```
## Node.js
To use Perspective from a Node.js server, simply install via NPM.
```bash
$ npm add @perspective-dev/client
```
+65
View File
@@ -0,0 +1,65 @@
# Joining Tables
`perspective.join()` creates a read-only `Table` by joining two source tables on
a shared key column. The result is reactive — it updates automatically when
either source table changes. See [`Join`](../../explanation/join.md) for
conceptual details.
## Basic Inner Join
```javascript
const orders = await perspective.table([
{ id: 1, product_id: 101, qty: 5 },
{ id: 2, product_id: 102, qty: 3 },
{ id: 3, product_id: 101, qty: 7 },
]);
const products = await perspective.table([
{ product_id: 101, name: "Widget" },
{ product_id: 102, name: "Gadget" },
]);
const joined = await perspective.join(orders, products, "product_id");
const view = await joined.view();
const json = await view.to_json();
// [
// { product_id: 101, id: 1, qty: 5, name: "Widget" },
// { product_id: 101, id: 3, qty: 7, name: "Widget" },
// { product_id: 102, id: 2, qty: 3, name: "Gadget" },
// ]
```
## Join Types
Pass `join_type` in the options to select inner, left, or outer join behavior:
```javascript
// Left join: all left rows, nulls for unmatched right columns
const left_joined = await perspective.join(left, right, "id", {
join_type: "left",
});
// Outer join: all rows from both tables
const outer_joined = await perspective.join(left, right, "id", {
join_type: "outer",
});
```
## Reactive Updates
The joined table recomputes automatically when either source table is updated:
```javascript
const left = await perspective.table([{ id: 1, x: 10 }]);
const right = await perspective.table([{ id: 2, y: "b" }]);
const joined = await perspective.join(left, right, "id");
const view = await joined.view();
let json = await view.to_json();
// [] — no matching keys yet
await right.update([{ id: 1, y: "a" }]);
json = await view.to_json();
// [{ id: 1, x: 10, y: "a" }] — new match detected
```
+76
View File
@@ -0,0 +1,76 @@
# Loading data from a Table
Data can be loaded into `<perspective-viewer>` in the form of a `Table()` or a
`Promise<Table>` via the `load()` method.
```javascript
// Create a new worker, then a new table promise on that worker.
const worker = await perspective.worker();
const table = await worker.table(data);
// Bind a viewer element to this table.
await viewer.load(table);
```
## Sharing a `Table` between multiple `<perspective-viewer>`s
Multiple `<perspective-viewer>`s can share a `table()` by passing the `table()`
into the `load()` method of each viewer. Each `perspective-viewer` will update
when the underlying `table()` is updated, but `table.delete()` will fail until
all `perspective-viewer` instances referencing it are also deleted:
```javascript
const viewer1 = document.getElementById("viewer1");
const viewer2 = document.getElementById("viewer2");
// Create a new WebWorker
const worker = await perspective.worker();
// Create a table in this worker
const table = await worker.table(data);
// Load the same table in 2 different <perspective-viewer> elements
await viewer1.load(table);
await viewer2.load(table);
// Both `viewer1` and `viewer2` will reflect this update
await table.update([{ x: 5, y: "e", z: true }]);
```
## Loading from a virtual `Table`
Loading a virtual (server-only) `Table` works just like loading a local/Web
Worker `Table` — just pass the virtual `Table` to `viewer.load()`. In the
browser:
```javascript
const elem = document.getElementsByTagName("perspective-viewer")[0];
// Bind to the server's worker instead of instantiating a Web Worker.
const websocket = await perspective.websocket(
window.location.origin.replace("http", "ws")
);
// Bind the viewer to the preloaded data source. `table` and `view` objects
// live on the server.
const server_table = await websocket.open_table("table_one");
await elem.load(server_table);
```
Alternatively, data can be _cloned_ from a server-side virtual `Table` into a
client-side WebAssembly `Table`. The browser clone will be synced via delta
updates transferred via Apache Arrow IPC format, but local `View`s created will
be calculated locally on the client browser.
```javascript
const worker = await perspective.worker();
const server_view = await server_table.view();
const client_table = worker.table(server_view);
await elem.load(client_table);
```
`<perspective-viewer>` instances bound in this way are otherwise no different
than `<perspective-viewer>`s which rely on a Web Worker, and can even share a
host application with Web Worker-bound `table()`s. The same `promise`-based API
is used to communicate with the server-instantiated `view()`, only in this case
it is over a websocket.
@@ -0,0 +1,38 @@
# Server-only via `WebSocketServer()` and Node.js
For exceptionally large datasets, a `Client` can be bound to a
`perspective.table()` instance running in Node.js/Python/Rust remotely, rather
than creating one in a Web Worker and downloading the entire data set. This
trades off network bandwidth and server resource requirements for a smaller
browser memory and CPU footprint.
An example in Node.js:
```javascript
const { WebSocketServer, table } = require("@perspective-dev/client");
const fs = require("fs");
// Start a WS/HTTP host on port 8080. The `assets` property allows
// the `WebSocketServer()` to also serves the file structure rooted in this
// module's directory.
const host = new WebSocketServer({ assets: [__dirname], port: 8080 });
// Read an arrow file from the file system and host it as a named table.
const arr = fs.readFileSync(__dirname + "/superstore.lz4.arrow");
await table(arr, { name: "table_one" });
```
... and the [`Client`] implementation in the browser:
```javascript
const elem = document.getElementsByTagName("perspective-viewer")[0];
// Bind to the server's worker instead of instantiating a Web Worker.
const websocket = await perspective.websocket(
window.location.origin.replace("http", "ws"),
);
// Create a virtual `Table` to the preloaded data source. `table` and `view`
// objects live on the server.
const server_table = await websocket.open_table("table_one");
```
@@ -0,0 +1,31 @@
# Plugin render limits
`<perspective-viewer>` plugins (especially charts) may in some cases generate
extremely large output which may lock up the browser. In order to prevent
accidents (which generally require a browser refresh to fix), each plugin has a
`max_cells` and `max_columns` heuristic which requires the user to opt-in to
fully rendering `View`s which exceed these limits. To override this behavior,
set these values for each plugin type individually, _before_ the plugin itself
is rendered (e.g. calling `HTMLPerspectiveViewerElement::restore` with the
respective `plugin` name).
If you have a `<perspective-viewer>` instance, you can configure plugins via
`HTMLPerspectiveViewerElement::getPlugin` and
`HTMLPerspectiveViewerElement::getAllPlugins`:
```javascript
const viewer = document.querySelector("perspective-viewer");
const plugin = viewer.getPlugin("Treemap");
plugin.max_cells = 1_000_000;
plugin.max_columns = 1000;
```
... Or alternatively, you can look up the Custom Element classes and set the
static variants if you know the element name (you can e.g. look this up in your
browser's DOM inspector):
```javascript
const plugin = customElements.get("perspective-viewer-charts-treemap");
plugin.max_cells = 1_000_000;
plugin.max_columns = 1000;
```
+57
View File
@@ -0,0 +1,57 @@
# React Component
We provide a React wrapper to prevent common issues and mistakes associated with
using the perspective-viewer web component in the context of React.
Before trying this example, please take a look at
[how to bootstrap perspective](./importing.md).
## `PerspectiveViewer`
A simple example using the `PerspectiveViewer` component:
```typescript
import React, { useCallback, useEffect, useRef } from "react";
import {
PerspectiveViewer,
} from "@perspective-dev/react";
import perspective from "@perspective-dev/client";
function App() {
const worker = useRef(null);
useEffect(() => {
(async () => {
worker.current = await perspective.worker();
const resp = await fetch("data.arrow");
const arrow = await resp.arrayBuffer();
await worker.current.table(arrow, { name: "my_table" });
})();
}, []);
return (
<PerspectiveViewer
client={worker.current}
config={{group_by: ["State"], columns: ["Sales"]}}
/>
);
}
```
## `PerspectiveWorkspace`
For multi-viewer layouts, use `PerspectiveWorkspace`:
```typescript
import { PerspectiveWorkspace } from "@perspective-dev/react";
const WORKSPACE_CONFIG = // ...
function Dashboard() {
return (
<PerspectiveWorkspace
client={perspective.worker()}
config={WORKSPACE_CONFIG} />
);
}
```
+99
View File
@@ -0,0 +1,99 @@
# Saving and restoring UI state.
`<perspective-viewer>` is _persistent_, in that its entire state (sans the data
itself) can be serialized or deserialized. This include all column, filter,
pivot, expressions, etc. properties, as well as datagrid style settings, config
panel visibility, and more. This overloaded feature covers a range of use cases:
- Setting a `<perspective-viewer>`'s initial state after a `load()` call.
- Updating a single or subset of properties, without modifying others.
- Resetting some or all properties to their data-relative default.
- Persisting a user's configuration to `localStorage` or a server.
## Serializing and deserializing the viewer state
To retrieve the entire state as a JSON-ready JavaScript object, use the `save()`
method. `save()` also supports a few other formats such as `"arraybuffer"` and
`"string"` (base64, not JSON), which you may choose for size at the expense of
easy migration/manual-editing.
```javascript
const json_token = await elem.save();
const string_token = await elem.save("string");
```
For any format, the serialized token can be restored to any
`<perspective-viewer>` with a `Table` of identical schema, via the `restore()`
method. Note that while the data for a token returned from `save()` may differ,
generally its schema may not, as many other settings depend on column names and
types.
```javascript
await elem.restore(json_token);
await elem.restore(string_token);
```
As `restore()` dispatches on the token's type, it is important to make sure that
these types match! A common source of error occurs when passing a
JSON-stringified token to `restore()`, which will assume base64-encoded msgpack
when a string token is used.
```javascript
// This will error!
await elem.restore(JSON.stringify(json_token));
```
### Updating individual properties
Using the JSON format, every facet of a `<perspective-viewer>`'s configuration
can be manipulated from JavaScript using the `restore()` method. The valid
structure of properties is described via the
[`ViewerConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa/rust/perspective-viewer/src/ts/viewer.ts#L16)
and embedded
[`ViewConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa19435a2a57d4687be7e428a4efc759b/packages/perspective/index.d.ts#L140)
type declarations, and [`View`](view.md) chapter of the documentation which has
several interactive examples for each `ViewConfig` property.
```javascript
// Set the plugin (will also update `columns` to plugin-defaults)
await elem.restore({ plugin: "X Bar" });
// Update plugin and columns (only draws once)
await elem.restore({ plugin: "X Bar", columns: ["Sales"] });
// Open the config panel
await elem.restore({ settings: true });
// Create an expression
await elem.restore({
columns: ['"Sales" + 100'],
expressions: { "New Column": '"Sales" + 100' },
});
// ERROR if the column does not exist in the schema or expressions
// await elem.restore({columns: ["\"Sales\" + 100"], expressions: {}});
// Add a filter
await elem.restore({ filter: [["Sales", "<", 100]] });
// Add a sort, don't remove filter
await elem.restore({ sort: [["Prodit", "desc"]] });
// Reset just filter, preserve sort
await elem.restore({ filter: undefined });
// Reset all properties to default e.g. after `load()`
await elem.reset();
```
Another effective way to quickly create a token for a desired configuration is
to simply copy the token returned from `save()` after settings the view manually
in the browser. The JSON format is human-readable and should be quite easy to
tweak once generated, as `save()` will return even the default settings for all
properties. You can call `save()` in your application code, or e.g. through the
Chrome developer console:
```javascript
// Copy to clipboard
copy(await document.querySelector("perspective-viewer").save());
```
+21
View File
@@ -0,0 +1,21 @@
### Serializing data
The `view()` allows for serialization of data to JavaScript through the
`to_json()`, `to_ndjson()`, `to_columns()`, `to_csv()`, and `to_arrow()` methods
(the same data formats supported by the `Client::table` factory function). These
methods return a `promise` for the calculated data:
```javascript
const view = await table.view({ group_by: ["State"], columns: ["Sales"] });
// JavaScript Objects
console.log(await view.to_json());
console.log(await view.to_columns());
// String
console.log(await view.to_csv());
console.log(await view.to_ndjson());
// ArrayBuffer
console.log(await view.to_arrow());
```
+96
View File
@@ -0,0 +1,96 @@
# Theming
Theming is supported in `perspective-viewer` and its accompanying plugins. A
number of themes come bundled with `perspective-viewer`; you can import any of
these themes directly into your app, and the `perspective-viewer`s will be
themed accordingly:
```javascript
// Themes based on Thought Merchants's Prospective design
import "@perspective-dev/viewer/dist/css/pro.css";
import "@perspective-dev/viewer/dist/css/pro-dark.css";
// Other themes
import "@perspective-dev/viewer/dist/css/solarized.css";
import "@perspective-dev/viewer/dist/css/solarized-dark.css";
import "@perspective-dev/viewer/dist/css/monokai.css";
import "@perspective-dev/viewer/dist/css/vaporwave.css";
```
Alternatively, you may use `themes.css`, which bundles all default themes
```javascript
import "@perspective-dev/viewer/dist/css/themes.css";
```
If you choose not to bundle the themes yourself, they are available through
[CDN](https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/). These can
be directly linked in your HTML file:
```html
<link
rel="stylesheet"
crossorigin="anonymous"
href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/pro.css"
/>
```
Note the `crossorigin="anonymous"` attribute. When including a theme from a
cross-origin context, this attribute may be required to allow
`<perspective-viewer>` to detect the theme. If this fails, additional themes are
added to the `document` after `<perspective-viewer>` init, or for any other
reason theme auto-detection fails, you may manually inform
`<perspective-viewer>` of the available theme names with the `.resetThemes()`
method.
```javascript
// re-auto-detect themes
viewer.resetThemes();
// Set available themes explicitly (they still must be imported as CSS!)
viewer.resetThemes(["Pro Light", "Pro Dark"]);
```
`<perspective-viewer>` will default to the first loaded theme when initialized.
You may override this via `.restore()`, or provide an initial theme by setting
the `theme` attribute:
```html
<perspective-viewer theme="Pro Light"></perspective-viewer>
```
or
```javascript
const viewer = document.querySelector("perspective-viewer");
await viewer.restore({ theme: "Pro Dark" });
```
## Custom Themes
The best way to write a new theme is to
[fork and modify an existing theme](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes),
which are _just_ collections of regular CSS variables (no preprocessor is
required, though Perspective's own themes use one). `<perspective-viewer>` is
not "themed" by default and will lack icons and label text in addition to colors
and fonts, so starting from an empty theme forces you to define _every_
theme-able variable to get a functional UI.
### Icons and Translation
UI icons are defined by CSS variables provided by
[`@perspective-dev/viewer/dist/css/icons.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/icons.css).
These variables must be defined for the UI icons to work - there are no default
icons without a theme.
UI text is also defined in CSS variables provided by
[`@perspective-dev/viewer/dist/css/intl.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/intl.css),
and has identical import requirements. Some _example definitions_
(automatically-translated sans-editing) can be found
[`@perspective-dev/viewer/dist/css/intl/` folder](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes/intl).
Importing the pre-built `themes.css` stylesheet as well as a custom theme will
define Icons and Translation globally as a side-effect. You can still customize
icons in this mode with rules (of the appropriate specificity), _but_ if you do
not still remember to define these variables yourself, your theme will not work
without the base `themes.css` package available.
+62
View File
@@ -0,0 +1,62 @@
# `<perspective-viewer>` Custom Element library
`<perspective-viewer>` provides a complete graphical UI for configuring the
`perspective` library and formatting its output to the provided visualization
plugins.
Once imported and initialized in JavaScript, the `<perspective-viewer>` Web
Component will be available in any standard HTML on your site. A simple example:
```html
<perspective-viewer id="view1"></perspective-viewer>
<script type="module">
import perspective from "@perspective-dev/client";
import "@perspective-dev/viewer";
const worker = await perspective.worker();
const table = await worker.table(data);
document.getElementById("view1").load(table);
</script>
```
## Attributes
`<perspective-viewer>` can be configured via HTML attributes or JavaScript
properties. When set as attributes, the viewer will apply the configuration on
initialization:
```html
<perspective-viewer
columns='["Sales", "Profit"]'
group-by='["Region"]'
sort='[["Sales", "desc"]]'>
</perspective-viewer>
```
## UI Features
The viewer provides an interactive side panel with:
- **Column list** - drag and drop columns to configure `group_by`, `split_by`,
`sort`, and `filter` fields.
- **New Column** button - opens an expression editor for creating computed
columns via the [expression language](../../explanation/view/config/expressions.md).
- **Plugin selector** - switch between visualization plugins such as Datagrid,
X/Y Line, X/Y Scatter, Treemap, Sunburst, and Heatmap.
- **Theme** selector - toggle between available themes.
- **Export** - download the current view as CSV or Arrow.
- **Copy** - copy the current view to the clipboard.
- **Reset** - restore the viewer to its default configuration.
## Methods
Key methods on the `<perspective-viewer>` element:
| Method | Description |
|---|---|
| `load(table)` | Bind a `Table` to the viewer |
| `restore(config)` | Apply a saved configuration object |
| `save()` | Serialize the current configuration |
| `reset(all)` | Reset configuration (pass `true` to also reset expressions) |
| `getTable()` | Get the bound `Table` |
| `flush()` | Wait for any pending UI updates to complete |
@@ -0,0 +1,19 @@
# Virtual Servers
Perspective's Virtual Server feature lets you connect `<perspective-viewer>` to
external data sources without loading data into Perspective's built-in engine.
Instead, queries are translated and executed natively by the external database.
For a detailed explanation of how virtual servers work, see the
[Virtual Servers](../../explanation/virtual_servers.md) concepts page.
Perspective ships with built-in virtual server implementations for:
- [**DuckDB**](./virtual_server/duckdb.md) — query DuckDB databases in-browser
via `@duckdb/duckdb-wasm`, or on the server via Node.js.
- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server
directly from the browser or from Node.js.
You can also [**implement your own**](./virtual_server/custom.md) virtual server
to connect Perspective to any data source by implementing the
`VirtualServerHandler` interface.
@@ -0,0 +1,43 @@
# ClickHouse Virtual Server
Perspective provides a built-in virtual server for
[ClickHouse](https://clickhouse.com/), allowing `<perspective-viewer>` to query
ClickHouse tables directly from the browser.
For server-side Python usage, see the
[Python ClickHouse guide](../../python/virtual_server/clickhouse.md).
## Installation
```bash
npm install @perspective-dev/client @perspective-dev/viewer @clickhouse/client-web
```
## Usage
Connect to a ClickHouse instance and bind it to a Perspective viewer:
```javascript
import perspective from "@perspective-dev/client";
import "@perspective-dev/viewer";
import { createClient } from "@clickhouse/client-web";
// Connect to ClickHouse
const clickhouseClient = createClient({
url: "http://localhost:8123",
database: "default",
});
// Create a Perspective virtual server backed by ClickHouse
const handler = perspective.ClickhouseHandler(clickhouseClient);
const messageHandler = perspective.createMessageHandler(handler);
// Connect a viewer
const client = await perspective.worker(messageHandler);
const table = await client.open_table("my_table");
document.getElementById("viewer").load(table);
```
## Examples
- [Browser ClickHouse example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-clickhouse-virtual)
@@ -0,0 +1,80 @@
# Implementing a custom Virtual Server
You can connect Perspective to any data source by implementing the
`VirtualServerHandler` interface and passing it to `createMessageHandler()`.
For background on virtual servers, see the
[Virtual Servers overview](../../../explanation/virtual_servers.md).
## Example
```typescript
import perspective from "@perspective-dev/client";
import type {
VirtualServerHandler,
ColumnType,
ViewConfig,
ViewWindow,
VirtualDataSlice,
} from "@perspective-dev/client";
const handler = {
async getHostedTables(): Promise<string[]> {
return ["my_table"];
},
async tableSchema(tableId: string): Promise<Record<string, ColumnType>> {
return { name: "string", price: "float", date: "date" };
},
async tableSize(tableId: string): Promise<number> {
return 1000;
},
async tableMakeView(
tableId: string,
viewId: string,
config: ViewConfig,
): Promise<void> {
// Translate `config` (group_by, sort, filter, etc.) into a query
// against your data source. Store the query keyed by `viewId`
// for later data retrieval.
},
async viewDelete(viewId: string): Promise<void> {
// Clean up resources for this view
},
async viewGetData(
viewId: string,
config: ViewConfig,
schema: Record<string, ColumnType>,
viewport: ViewWindow,
dataSlice: VirtualDataSlice,
): Promise<void> {
// Query your data source using `config` and `viewport` for the
// row/column window. Push columnar results via `dataSlice.setCol()`.
},
getFeatures() {
return {
group_by: true,
sort: true,
filter_ops: {
string: ["==", "!=", "contains", "is null", "is not null"],
float: ["==", "!=", ">", "<", ">=", "<="],
},
aggregates: {
float: ["sum", "avg", "count", "min", "max"],
string: ["count", "any"],
},
};
},
} satisfies VirtualServerHandler;
// Create a message handler and use it like a worker
const messageHandler = perspective.createMessageHandler(handler);
const client = await perspective.worker(messageHandler);
const table = await client.open_table("my_table");
document.getElementById("viewer").load(table);
```
@@ -0,0 +1,49 @@
# DuckDB Virtual Server
Perspective provides a built-in virtual server for
[DuckDB](https://duckdb.org/), allowing `<perspective-viewer>` to query
DuckDB-WASM databases directly in the browser.
For server-side Python usage, see the
[Python DuckDB guide](../../python/virtual_server/duckdb.md).
## Installation
```bash
npm install @perspective-dev/client @perspective-dev/viewer @duckdb/duckdb-wasm
```
## Usage
Initialize DuckDB-WASM, load data, and connect it to a Perspective viewer:
```javascript
import perspective from "@perspective-dev/client";
import "@perspective-dev/viewer";
import * as duckdb from "@duckdb/duckdb-wasm";
// Initialize DuckDB-WASM
const DUCKDB_BUNDLES = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(DUCKDB_BUNDLES);
const worker = await duckdb.createWorker(bundle.mainWorker);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule);
// Load data into DuckDB
const conn = await db.connect();
await conn.query(`CREATE TABLE my_table AS SELECT * FROM 'data.parquet'`);
// Create a Perspective virtual server backed by DuckDB
const handler = perspective.DuckDBHandler(db);
const messageHandler = perspective.createMessageHandler(handler);
// Connect a viewer
const client = await perspective.worker(messageHandler);
const table = await client.open_table("my_table");
document.getElementById("viewer").load(table);
```
## Examples
- [Browser DuckDB example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-duckdb-virtual)
+44
View File
@@ -0,0 +1,44 @@
# Accessing the Perspective engine via a `Client` instance
An instance of a `Client` is needed to talk to a Perspective `Server`, of which
there are a few varieties available in JavaScript.
## Web Worker (Browser)
Perspective's Web Worker client is actually a `Client` and `Server` rolled into
one. Instantiating this `Client` will also create a _dedicated_ Perspective
`Server` in a Web Worker process.
To use it, you'll need to instantiate a Web Worker `perspective` engine via the
`worker()` method. This will create a new Web Worker (browser) and load the
WebAssembly binary. All calculation and data accumulation will occur in this
separate process.
```javascript
const client = await perspective.worker();
```
The `worker` symbol will expose the full `perspective` API for one managed Web
Worker process. You are free to create as many as your browser supports, but be
sure to keep track of the `worker` instances themselves, as you'll need them to
interact with your data in each instance.
## Websocket (Browser)
Alternatively, with a Perspective server running in Node.js, Python or Rust, you
can create a _virtual_ `Client` via the `websocket()` method.
```javascript
const client = perspective.websocket("http://localhost:8080/");
```
## Node.js
The Node.js runtime for the `@perspective-dev/client` module runs in-process by
default and does not implement a `child_process` interface, so no need to call
the `.worker()` factory function. Instead, the `perspective` library exports the
functions directly and run synchronously in the main process.
```javascript
const client = require("@perspective-dev/client");
```
+4
View File
@@ -0,0 +1,4 @@
# Python
Guides for using `perspective-python`, including data loading, callbacks,
multithreading, WebSocket servers, and JupyterLab integration.
+34
View File
@@ -0,0 +1,34 @@
# Callbacks and Events
`perspective.Table` allows for `on_update` and `on_delete` callbacks to be
set—simply call `on_update` or `on_delete` with a reference to a function or a
lambda without any parameters:
```python
def update_callback():
print("Updated!")
# set the update callback
on_update_id = view.on_update(update_callback)
def delete_callback():
print("Deleted!")
# set the delete callback
on_delete_id = view.on_delete(delete_callback)
# set a lambda as a callback
view.on_delete(lambda: print("Deleted x2!"))
```
If the callback is a named reference to a function, it can be removed with
`remove_update` or `remove_delete`:
```python
view.remove_update(on_update_id)
view.remove_delete(on_delete_id)
```
Callbacks defined with a lambda function cannot be removed, as lambda functions
have no identifier.
+27
View File
@@ -0,0 +1,27 @@
# Installation
`perspective-python` contains full bindings to the Perspective API, a JupyterLab
widget, and WebSocket handlers for several webserver libraries that allow you to
host Perspective using server-side Python.
## PyPI
`perspective-python` can be installed from [PyPI](https://pypi.org) via `pip`:
```bash
pip install perspective-python
```
That's it! If JupyterLab is installed in this Python environment, you'll also
get the `perspective.widget.PerspectiveWidget` class when you import
`perspective` in a Jupyter Lab kernel.
<!--
### Anaconda
`perspective-python` can also be installed for [Anaconda](https://anaconda.org/)
via [Conda Forge](https://conda-forge.org)
```bash
conda install -c conda-forge perspective
``` -->
+64
View File
@@ -0,0 +1,64 @@
# Joining Tables
`perspective.join()` creates a read-only `Table` by joining two source tables on
a shared key column. The result is reactive — it updates automatically when
either source table changes. See [`Join`](../../explanation/join.md) for
conceptual details.
## Basic Inner Join
```python
orders = perspective.table([
{"id": 1, "product_id": 101, "qty": 5},
{"id": 2, "product_id": 102, "qty": 3},
{"id": 3, "product_id": 101, "qty": 7},
])
products = perspective.table([
{"product_id": 101, "name": "Widget"},
{"product_id": 102, "name": "Gadget"},
])
joined = perspective.join(orders, products, "product_id")
view = joined.view()
json = view.to_json()
```
## Join Types
Pass `join_type` to select inner, left, or outer join behavior:
```python
# Left join: all left rows, nulls for unmatched right columns
left_joined = perspective.join(left, right, "id", join_type="left")
# Outer join: all rows from both tables
outer_joined = perspective.join(left, right, "id", join_type="outer")
```
## Reactive Updates
The joined table recomputes automatically when either source table is updated:
```python
left = perspective.table([{"id": 1, "x": 10}])
right = perspective.table([{"id": 2, "y": "b"}])
joined = perspective.join(left, right, "id")
view = joined.view()
json = view.to_json()
# [] — no matching keys yet
right.update([{"id": 1, "y": "a"}])
json = view.to_json()
# [{"id": 1, "x": 10, "y": "a"}] — new match detected
```
## Async Client
The async client has the same API:
```python
joined = await client.join(orders, products, "product_id", join_type="left")
```
+61
View File
@@ -0,0 +1,61 @@
# `PerspectiveWidget` for JupyterLab
Building on top of the API provided by `perspective.Table`, the
`PerspectiveWidget` is a JupyterLab plugin that offers the entire functionality
of Perspective within the Jupyter environment. It supports the same API
semantics of `<perspective-viewer>`, along with the additional data types
supported by `perspective.Table`. `PerspectiveWidget` takes keyword arguments
for the managed `View`:
```python
from perspective.widget import PerspectiveWidget
w = perspective.PerspectiveWidget(
data,
plugin="X Bar",
aggregates={"datetime": "any"},
sort=[["date", "desc"]]
)
```
## Creating a widget
A widget is created through the `PerspectiveWidget` constructor, which takes as
its first, required parameter a `perspective.Table`, a dataset, a schema, or
`None`, which serves as a special value that tells the Widget to defer loading
any data until later. In maintaining consistency with the Javascript API,
Widgets cannot be created with empty dictionaries or lists — `None` should be
used if the intention is to await data for loading later on. A widget can be
constructed from a dataset:
```python
from perspective.widget import PerspectiveWidget
PerspectiveWidget(data, group_by=["date"])
```
.. or a schema:
```python
PerspectiveWidget({"a": int, "b": str})
```
.. or an instance of a `perspective.Table`:
```python
table = perspective.table(data)
PerspectiveWidget(table)
```
## Updating a widget
`PerspectiveWidget` shares a similar API to the `<perspective-viewer>` Custom
Element, and has similar `save()` and `restore()` methods that
serialize/deserialize UI state for the widget.
<!--
## `PerspectiveRenderer`
Perspective also exposes a JS-only `mimerender-extension`. This lets you view
`csv`, `json`, and `arrow` files directly from the file browser. You can see
this by right clicking one of these files and `Open With->CSVPerspective` (or
`JSONPerspective` or `ArrowPerspective`). Perspective will also install itself
as the default handler for opening `.arrow` files. -->
+67
View File
@@ -0,0 +1,67 @@
# Multi-threading
Perspective's API is thread-safe, so methods may be called from different
threads without additional consideration for safety/exclusivity/correctness. All
`perspective.Client` and `perspective.Server` API methods release the GIL, which
can be exploited for parallelism.
Interally, `perspective.Server` also dispatches to a thread pool for some
operations, enabling better parallelism and overall better query performance.
This independent threadpool size can be controlled via
`perspective.set_num_cpus()`, or the `OMP_NUM_THREADS` environment variable.
```python
import perspective
perspective.set_num_cpus(2)
```
## Server handlers
Perspective's server handler implementations each take an optional `executor`
constructor argument, which (when provided) will configure the handler to
process WebSocket `Client` requests on a thread pool.
```python
from concurrent.futures import ThreadPoolExecutor
from tornado.web import Application
from perspective.handlers.tornado import PerspectiveTornadoHandler
from perspective import Server
args = {"perspective_server": Server(), "executor": ThreadPoolExecutor()}
app = Application(
[
(r"/websocket", PerspectiveTornadoHandler, args),
# ...
]
)
```
## `on_poll_request`
`on_poll_request` is an optional keyword argument for `Server()`, which which
can be applied in cases where overlapping `Table.update` calls can be safely
deferred.
When providing a callback function to `on_poll_request`, the `Server` will
invoke your callback when there are updates that need to be flushed, after which
you must _eventually_ call `Server.poll` (or else no updates will be processed).
The exact implementation of `on_poll_request` will depend on the context. A
simple example which batches calls via `threading.Lock`:
```python
lock = threading.Lock()
def on_poll_request(perspective_server):
if lock.acquire(blocking=False):
try:
perspective_server.poll()
finally:
lock.release()
server = Server(on_poll_request=on_poll_request)
```
+90
View File
@@ -0,0 +1,90 @@
# Loading data into a Table
A `Table` can be created from a dataset or a schema, the specifics of which are
[discussed](#loading-data-with-table) in the JavaScript section of the user's
guide. In Python, however, Perspective supports additional data types that are
commonly used when processing data:
- `pandas.DataFrame`
- `polars.DataFrame`
- `bytes` (encoding an Apache Arrow)
- `objects` (either extracting a repr or via reference)
- `str` (encoding as a CSV)
A `Table` is created in a similar fashion to its JavaScript equivalent:
```python
from datetime import date, datetime
import numpy as np
import pandas as pd
import perspective
data = pd.DataFrame({
"int": np.arange(100),
"float": [i * 1.5 for i in range(100)],
"bool": [True for i in range(100)],
"date": [date.today() for i in range(100)],
"datetime": [datetime.now() for i in range(100)],
"string": [str(i) for i in range(100)]
})
table = perspective.table(data, index="float")
```
Likewise, a `View` can be created via the `view()` method:
```python
view = table.view(group_by=["float"], filter=[["bool", "==", True]])
column_data = view.to_columns()
row_data = view.to_json()
```
## Polars Support
Polars `DataFrame` types work similarly to Apache Arrow input, which Perspective
uses to interface with Polars.
```python
df = polars.DataFrame({"a": [1,2,3,4,5]})
table = perspective.table(df)
```
## Pandas Support
Perspective's `Table` can be constructed from `pandas.DataFrame` objects.
Internally, this just uses
[`pyarrow::from_pandas`](https://arrow.apache.org/docs/python/pandas.html),
which dictates behavior of this feature including type support.
If the dataframe does not have an index set, an integer-typed column named
`"index"` is created. If you want to preserve the indexing behavior of the
dataframe passed into Perspective, simply create the `Table` with
`index="index"` as a keyword argument. This tells Perspective to once again
treat the index as a primary key:
```python
data.set_index("datetime")
table = perspective.table(data, index="index")
```
## Time Zone Handling
When parsing `"datetime"` strings, times without an explicit timezone offset are
interpreted as _UTC_. Strings with a timezone offset (e.g., `+05:00`) are
converted to UTC. All `"datetime"` values are stored internally as milliseconds
since the Unix epoch, and are _output_ as integer timestamps (milliseconds since
epoch) from methods like `to_columns()` and `to_json()`.
Python `datetime` objects are serialized to strings before parsing. Naive
`datetime` objects (without `tzinfo`) produce strings without timezone
information and are therefore treated as UTC. Timezone-aware `datetime` objects
include their offset in the serialized string, which is used to convert to UTC.
`"date"` values are timezone-agnostic calendar days with no time component.
They are _output_ as integer timestamps at _UTC midnight_ of the calendar day
(equivalent to Arrow `date32` day arithmetic), and integer timestamp _input_ to
a `"date"` column is likewise interpreted as UTC. The host process timezone
never affects `"date"` values — a `Viewer` renders them in UTC, recovering the
stored calendar day exactly. Datetime expression functions such as
`bucket("x", 'D')`, `day_of_week("x")` and `hour_of_day("x")` also compute in
UTC.
+81
View File
@@ -0,0 +1,81 @@
# DataFrame and Arrow Compatibility
`perspective-python` accepts a `Table` constructor argument from any of the
common Python columnar data libraries. In all three cases, `perspective.table`
(and `Table.update()`) consume the input directly — there is no need to
serialize to Apache Arrow IPC bytes yourself. However, note is
still the most efficient way to bulk load data into `Table`.
## PyArrow
```python
import pyarrow as pa
import perspective
arrow_table = pa.table({
"int": pa.array([1, 2, 3], type=pa.int64()),
"float": pa.array([1.5, 2.5, 3.5], type=pa.float64()),
"string": pa.array(["a", "b", "c"], type=pa.string()),
})
table = perspective.table(arrow_table)
```
The same applies to `Table.update()`:
```python
table.update(arrow_table)
```
If you have Arrow data already in IPC format (e.g. read from disk, received
over the wire, or produced by another tool), pass the raw `bytes` directly —
both stream and file formats are auto-detected:
```python
with open("data.arrow", "rb") as f:
table = perspective.table(f.read())
```
## Polars
```python
import polars as pl
import perspective
df = pl.DataFrame({
"a": [1, 2, 3, 4, 5],
"b": ["x", "y", "z", "x", "y"],
})
table = perspective.table(df)
```
Internally, the `DataFrame` is converted to a `pyarrow.Table` before
ingestion, so Polars columns inherit the Arrow type mapping above.
See also Perspective [Virtual Server support for `polars.DataFrame`](./virtual_server/polars.md)
## Pandas
`pandas.DataFrame` is supported via `pyarrow.Table.from_pandas`, which
dictates behavior including type support — see the
[pyarrow pandas docs](https://arrow.apache.org/docs/python/pandas.html) for
details on which pandas dtypes round-trip cleanly.
```python
from datetime import date, datetime
import numpy as np
import pandas as pd
import perspective
data = pd.DataFrame({
"int": np.arange(100),
"float": [i * 1.5 for i in range(100)],
"bool": [True for i in range(100)],
"date": [date.today() for i in range(100)],
"datetime": [datetime.now() for i in range(100)],
"string": [str(i) for i in range(100)],
})
table = perspective.table(data, index="float")
```
+20
View File
@@ -0,0 +1,20 @@
# Virtual Servers
Perspective's Virtual Server feature lets you connect `<perspective-viewer>` to
external data sources without loading data into Perspective's built-in engine.
Instead, queries are translated and executed natively by the external database.
For a detailed explanation of how virtual servers work, see the
[Virtual Servers](../../explanation/virtual_servers.md) concepts page.
Perspective ships with built-in virtual server implementations for:
- [**DuckDB**](./virtual_server/duckdb.md) — query DuckDB databases using the
`duckdb` Python package.
- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server
using the `clickhouse-connect` Python package.
- [**Polars**](./virtual_server/polars.md) — query in-memory Polars DataFrames
using the `polars` Python package.
You can also [**implement your own**](./virtual_server/custom.md) virtual server
to connect Perspective to any data source by subclassing `VirtualServerHandler`.
@@ -0,0 +1,52 @@
# ClickHouse Virtual Server
Perspective provides a built-in virtual server for
[ClickHouse](https://clickhouse.com/), allowing `<perspective-viewer>` clients
to query a ClickHouse server over WebSocket.
For browser-only usage, see the
[JavaScript ClickHouse guide](../../javascript/virtual_server/clickhouse.md).
## Installation
```bash
pip install perspective-python clickhouse-connect
```
## Usage
Create a server that exposes ClickHouse tables to browser clients:
```python
import clickhouse_connect
import tornado.web
import tornado.ioloop
from perspective import ClickhouseVirtualServer
from perspective.handlers.tornado import PerspectiveTornadoHandler
# Connect to ClickHouse
client = clickhouse_connect.get_client(host="localhost")
# Create virtual server backed by ClickHouse
server = ClickhouseVirtualServer(client)
# Serve over WebSocket
app = tornado.web.Application([
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
])
app.listen(8080)
tornado.ioloop.IOLoop.current().start()
```
Connect from the browser:
```javascript
const websocket = await perspective.websocket("ws://localhost:8080/websocket");
const table = await websocket.open_table("my_table");
document.getElementById("viewer").load(table);
```
## Examples
- [Python ClickHouse example](https://github.com/perspective-dev/perspective/tree/master/examples/python-clickhouse-virtual)
@@ -0,0 +1,64 @@
# Implementing a custom Virtual Server
You can connect Perspective to any data source by subclassing
`VirtualServerHandler` and wrapping it with `VirtualServer`.
For background on virtual servers, see the
[Virtual Servers overview](../../../explanation/virtual_servers.md).
## Example
```python
from perspective import VirtualServerHandler, VirtualServer
class MyModel(VirtualServerHandler):
def get_features(self):
return {
"group_by": True,
"split_by": False,
"sort": True,
"filter_ops": {
"string": ["==", "!=", "contains"],
"float": ["==", "!=", ">", "<"],
},
"aggregates": {
"float": ["sum", "avg", "count"],
"string": ["count"],
},
}
def get_hosted_tables(self):
return ["my_table"]
def table_schema(self, table_name):
return {"name": "string", "price": "float"}
def table_size(self, table_name):
return 1000
def table_make_view(self, table_name, view_id, config):
# Translate `config` (group_by, sort, filter, etc.) into a
# query against your data source. Store the query keyed by
# `view_id` for later data retrieval.
pass
def view_delete(self, view_id):
# Clean up resources for this view
pass
def view_get_data(self, view_id, start_row, end_row, start_col, end_col, ctx):
# Execute the stored query with the given row/column window.
# Push results via `ctx`.
pass
```
The `VirtualServer` instance can then be passed to a Tornado, Starlette, or
AIOHTTP handler just like a regular `Server`:
```python
from perspective.handlers.tornado import PerspectiveTornadoHandler
app = tornado.web.Application([
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": VirtualServer(MyModel)}),
])
```
@@ -0,0 +1,53 @@
# DuckDB Virtual Server
Perspective provides a built-in virtual server for
[DuckDB](https://duckdb.org/), allowing `<perspective-viewer>` clients to query
a server-side DuckDB database over WebSocket.
For browser-only usage via DuckDB-WASM, see the
[JavaScript DuckDB guide](../../javascript/virtual_server/duckdb.md).
## Installation
```bash
pip install perspective-python duckdb
```
## Usage
Create a server that exposes a DuckDB database to browser clients:
```python
import duckdb
import tornado.web
import tornado.ioloop
from perspective import DuckDBVirtualServer
from perspective.handlers.tornado import PerspectiveTornadoHandler
# Create DuckDB connection and load data
conn = duckdb.connect()
conn.execute("CREATE TABLE my_table AS SELECT * FROM 'data.parquet'")
# Create virtual server backed by DuckDB
server = DuckDBVirtualServer(conn)
# Serve over WebSocket
app = tornado.web.Application([
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
])
app.listen(8080)
tornado.ioloop.IOLoop.current().start()
```
Connect from the browser:
```javascript
const websocket = await perspective.websocket("ws://localhost:8080/websocket");
const table = await websocket.open_table("my_table");
document.getElementById("viewer").load(table);
```
## Examples
- [Python DuckDB example](https://github.com/perspective-dev/perspective/tree/master/examples/python-duckdb-virtual)
@@ -0,0 +1,49 @@
# Polars Virtual Server
Perspective provides a built-in virtual server for
[Polars](https://pola.rs/), allowing `<perspective-viewer>` clients to query
in-memory Polars DataFrames over WebSocket.
## Installation
```bash
pip install perspective-python polars
```
## Usage
Create a server that exposes Polars DataFrames to browser clients:
```python
import polars as pl
import tornado.web
import tornado.ioloop
from perspective.virtual_servers.polars import PolarsVirtualServer
from perspective.handlers.tornado import PerspectiveTornadoHandler
# Load data into Polars DataFrames
df = pl.read_parquet("data.parquet")
# Create virtual server backed by Polars (dict of name -> DataFrame)
server = PolarsVirtualServer({"my_table": df})
# Serve over WebSocket
app = tornado.web.Application([
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
])
app.listen(8080)
tornado.ioloop.IOLoop.current().start()
```
Connect from the browser:
```javascript
const websocket = await perspective.websocket("ws://localhost:8080/websocket");
const table = await websocket.open_table("my_table");
document.getElementById("viewer").load(table);
```
## Examples
- [Python Polars example](https://github.com/perspective-dev/perspective/tree/master/examples/python-polars-virtual)
+129
View File
@@ -0,0 +1,129 @@
# Hosting a WebSocket server
An in-memory `Server` "hosts" all `perspective.Table` and `perspective.View`
instances created by its connected `Client`s. Hosted tables/views can have their
methods called from other sources than the Python server, i.e. by a
`perspective-viewer` running in a JavaScript client over the network,
interfacing with `perspective-python` through the websocket API.
The server has full control of all hosted `Table` and `View` instances, and can
call any public API method on hosted instances. This makes it extremely easy to
stream data to a hosted `Table` using `.update()`:
```python
server = perspective.Server()
client = server.new_local_client()
table = client.table(data, name="data_source")
for i in range(10):
# updates continue to propagate automatically
table.update(new_data)
```
The `name` provided is important, as it enables Perspective in JavaScript to
look up a `Table` and get a handle to it over the network. Otherwise, `name`
will be assigned randomly and the `Client` must look this up with
`Client.get_hosted_table_names()`
## Client/Server Replicated Mode
Using Tornado and
[`PerspectiveTornadoHandler`](python.md#perspectivetornadohandler), as well as
`Perspective`'s JavaScript library, we can set up "distributed" Perspective
instances that allows multiple browser `perspective-viewer` clients to read from
a common `perspective-python` server, as in the
[Tornado Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-tornado).
This architecture works by maintaining two `Tables`—one on the server, and one
on the client that mirrors the server's `Table` automatically using `on_update`.
All updates to the table on the server are automatically applied to each client,
which makes this architecture a natural fit for streaming dashboards and other
distributed use-cases. In conjunction with [multithreading](#multi-threading),
distributed Perspective offers consistently high performance over large numbers
of clients and large datasets.
_*server.py*_
```python
from perspective import Server
from perspective.handlers.tornado import PerspectiveTornadoHandler
# Create an instance of Server, and host a Table
SERVER = Server()
CLIENT = SERVER.new_local_client()
# The Table is exposed at `localhost:8888/websocket` with the name `data_source`
client.table(data, name = "data_source")
app = tornado.web.Application([
# create a websocket endpoint that the client JavaScript can access
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": SERVER})
])
# Start the Tornado server
app.listen(8888)
loop = tornado.ioloop.IOLoop.current()
loop.start()
```
Instead of calling `load(server_table)`, create a `View` using `server_table`
and pass that into `viewer.load()`. This will automatically register an
`on_update` callback that synchronizes state between the server and the client.
_*index.html*_
```html
<perspective-viewer id="viewer" editable></perspective-viewer>
<script type="module">
// Create a client that expects a Perspective server
// to accept connections at the specified URL.
const websocket = await perspective.websocket(
"ws://localhost:8888/websocket",
);
// Get a handle to the Table on the server
const server_table = await websocket.open_table("data_source_one");
// Create a new view
const server_view = await table.view();
// Create a Table on the client using `perspective.worker()`
const worker = await perspective.worker();
const client_table = await worker.table(view);
// Load the client table in the `<perspective-viewer>`.
document.getElementById("viewer").load(client_table);
</script>
```
For a more complex example that offers distributed editing of the server
dataset, see
[client_server_editing.html](https://github.com/perspective-dev/perspective/blob/master/examples/python-tornado/client_server_editing.html).
We also provide examples for Starlette/FastAPI and AIOHTTP:
- [Starlette Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-starlette).
- [AIOHTTP Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-aiohttp).
## Server-only Mode
The server setup is identical to
[Client/Server Replicated Mode](#client-server-replicated-mode) above, but
instead of creating a `View`, the client calls `load(server_table)`: In Python,
use `Server` and `PerspectiveTornadoHandler` to create a websocket server that
exposes a `Table`. In this example, `table` is a proxy for the `Table` we
created on the server. All API methods are available on _proxies_, e.g.
calling `view()`, `schema()`, `update()` on `table` will pass those operations
to the Python `Table`, execute the commands, and return the result back to
Javascript.
```html
<perspective-viewer id="viewer" editable></perspective-viewer>
```
```javascript
const websocket = perspective.websocket("ws://localhost:8888/websocket");
const table = websocket.open_table("data_source");
document.getElementById("viewer").load(table);
```
+89
View File
@@ -0,0 +1,89 @@
# Rust
Install via `cargo`:
```bash
cargo add perspective
```
# Example
Initialize a server and client
```rust
let server = Server::default();
let client = server.new_local_client();
```
Load an Arrow
```rust
let mut file = File::open(std::path::Path::new(ROOT_PATH).join(ARROW_FILE_PATH))?;
let mut feather = Vec::with_capacity(file.metadata()?.len() as usize);
file.read_to_end(&mut feather)?;
let data = UpdateData::Arrow(feather.into());
let mut options = TableInitOptions::default();
options.set_name("my_data_source");
client.table(data.into(), options).await?;
```
# Joining Tables
`Client::join` creates a read-only `Table` by joining two source tables on a
shared key column. The result is reactive — it updates automatically when
either source table changes. See [`Join`](../explanation/join.md) for
conceptual details.
```rust
let orders = client.table(
TableData::Update(UpdateData::JsonRows(
"[{\"id\":1,\"product_id\":101,\"qty\":5},{\"id\":2,\"product_id\":102,\"qty\":3}]".into(),
)),
TableInitOptions::default(),
).await?;
let products = client.table(
TableData::Update(UpdateData::JsonRows(
"[{\"product_id\":101,\"name\":\"Widget\"},{\"product_id\":102,\"name\":\"Gadget\"}]".into(),
)),
TableInitOptions::default(),
).await?;
let joined = client.join(
(&orders).into(),
(&products).into(),
"product_id",
JoinOptions::default(),
).await?;
let view = joined.view(None).await?;
let json = view.to_json().await?;
```
Use `JoinOptions` to configure the join type, table name, or `right_on` column:
```rust
let options = JoinOptions {
join_type: Some(JoinType::Left),
name: Some("orders_with_products".into()),
right_on: None,
};
let joined = client.join(
(&orders).into(),
(&products).into(),
"product_id",
options,
).await?;
```
You can also join by table name strings instead of `Table` references:
```rust
let joined = client.join(
"orders".into(),
"products".into(),
"product_id",
JoinOptions::default(),
).await?;
```