chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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");
|
||||
```
|
||||
Reference in New Issue
Block a user