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