diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 54c5fda8bf..2cf6cf0b09 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -168,8 +168,6 @@ jobs: packages/perspective-viewer-d3fc/dist packages/perspective-viewer-datagrid/dist packages/perspective-viewer-openlayers/dist - packages/perspective-esbuild-plugin/dist - packages/perspective-webpack-plugin/dist packages/perspective-cli/dist packages/perspective-workspace/dist @@ -947,12 +945,6 @@ jobs: - run: pnpm pack --pack-destination=../.. working-directory: ./packages/perspective-cli - - run: pnpm pack --pack-destination=../.. - working-directory: ./packages/perspective-webpack-plugin - - - run: pnpm pack --pack-destination=../.. - working-directory: ./packages/perspective-esbuild-plugin - - run: pnpm pack --pack-destination=../.. working-directory: ./packages/perspective-jupyterlab diff --git a/Cargo.lock b/Cargo.lock index 180035c1b4..a90a8ea995 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2094,13 +2094,11 @@ name = "perspective-server" version = "3.2.1" dependencies = [ "async-lock", - "base64 0.22.1", "cmake", "futures", "link-cplusplus", "num_cpus", "perspective-client", - "regex", "shlex", "tracing", ] diff --git a/README.md b/README.md index bddda90b32..9008309000 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@
-Perspective -Perspective + + + + + +

[![Build Status](https://img.shields.io/github/actions/workflow/status/finos/perspective/build.yaml?event=push&style=for-the-badge)](https://github.com/finos/perspective/actions/workflows/build.yaml) diff --git a/cpp/perspective/CMakeLists.txt b/cpp/perspective/CMakeLists.txt index ae17e7238c..f4d076da28 100644 --- a/cpp/perspective/CMakeLists.txt +++ b/cpp/perspective/CMakeLists.txt @@ -231,7 +231,7 @@ if(PSP_WASM_BUILD) ") endif() else() - set(OPT_FLAGS " -O3 -g0 -flto --emit-tsd=perspective-server.d.ts ") + set(OPT_FLAGS " -O3 -g0 --emit-tsd=perspective-server.d.ts ") if (PSP_WASM_EXCEPTIONS) set(OPT_FLAGS "${OPT_FLAGS} -fwasm-exceptions ") endif() diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000000..2e5fc992ad --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,42 @@ +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +# ┃ Copyright (c) 2017, the Perspective Authors. ┃ +# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +# ┃ This file is part of the Perspective library, distributed under the terms ┃ +# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +[book] +authors = ["Andrew Stein"] +language = "en" +multilingual = false +src = "md" +title = "Perspective" + +[build] +build-dir = "static/guide" + +[output.html] +# theme = "my-theme" +# default-theme = "light" +# preferred-dark-theme = "navy" +# smart-punctuation = true +# mathjax-support = false +copy-fonts = true +git-repository-url = "https://github.com/finos/perspective" +git-repository-icon = "fa-github" +site-url = "https://perspective.finos.org/guide/" +additional-css = [ + "md/perspective.css", + "node_modules/@finos/perspective-viewer/dist/css/themes.css", +] +# additional-js = [] +# no-section-label = false +# edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" +# site-url = "/guide/" +# cname = "myproject.rs" +# input-404 = "not-found.md" diff --git a/docs/md/SUMMARY.md b/docs/md/SUMMARY.md new file mode 100644 index 0000000000..3a161513cc --- /dev/null +++ b/docs/md/SUMMARY.md @@ -0,0 +1,60 @@ +# Summary + +[What is Perspective](./perspective.md) + +# Overview + +- [Data Architecture](./explanation/architecture.md) + - [Client-only](./explanation/architecture/client_only.md) + - [Client/Server replicated](./explanation/architecture/client_server.md) + - [Server only](./explanation/architecture/server_only.md) +- [`Table`](./explanation/table.md) + - [Construct an empty `Table` from a schema](./explanation/table/constructing_schema.md) + - [Schema and column types](./explanation/table/schema.md) + - [Loading data](./explanation/table/loading_data.md) + - [`index` and `limit` options](./explanation/table/options.md) + - [`update()` and `remove()` streaming methods](./explanation/table/update_and_remove.md) + - [`clear()` and `replace()` start-over methods](./explanation/table/clear_and_replace.md) +- [`View`](./explanation/view.md) + - [Querying data](./explanation/view/querying.md) + - [`group_by`](./explanation/view/config/group_by.md) + - [`split_by`](./explanation/view/config/split_by.md) + - [`aggregates`](./explanation/view/config/aggregates.md) + - [`columns`](./explanation/view/config/columns.md) + - [`sort`](./explanation/view/config/sort.md) + - [`filter`](./explanation/view/config/filter.md) + - [`expressions`](./explanation/view/config/expressions.md) + - [Flattening a View into a Table](./explanation/view/config/flattening.md) +- [JavaScript](./explanation/javascript.md) + - [Module Structure](./explanation/javascript_module_structure.md) + - [Build options](./explanation/javascript_builds.md) +- [Python](./explanation/python.md) + +# Getting Started + +- [Rust](./how_to/rust.md) +- [JavaScript](./how_to/javascript.md) + - [Installation via NPM](./how_to/javascript/installation.md) + - [Importing with or without a bundler](./how_to/javascript/importing.md) + - [`perspective` data engine library](./how_to/javascript/worker.md) + - [Serializing data](./how_to/javascript/serializing.md) + - [Cleaning up resources](./how_to/javascript/deleting.md) + - [Hosting a `WebSocketServer` in Node.js](./how_to/javascript/nodejs_server.md) + - [`perspective-viewer` Custom Element library](./how_to/javascript/viewer.md) + - [Theming](./how_to/javascript/theming.md) + - [Loading data from a `Table`](./how_to/javascript/loading_data.md) + - [Loading data from a virtual `Table`](./how_to/javascript/loading_virtual_data.md) + - [Saving and restoring UI state](./how_to/javascript/save_restore.md) + - [Listening for events](./how_to/javascript/events.md) +- [Python](./how_to/python.md) + - [Installation](./how_to/python/installation.md) + - [Loading data into a `Table`](./how_to/python/table.md) + - [Callbacks and events](./how_to/python/callbacks.md) + - [Multithreading](./how_to/python/multithreading.md) + - [Hosting a WebSocket server](./how_to/python/websocket.md) + - [`PerspectiveWidget` for JupyterLab](./how_to/python/jupyterlab.md) + - [Tutorial: A `tornado` server with virtual `perspective-viewer`](./tutorials/python/tornado.md) + +# API + +- [Crate documentation on `docs.rs` ](./api_reference.md) diff --git a/docs/md/api_reference.md b/docs/md/api_reference.md new file mode 100644 index 0000000000..c5836cf30b --- /dev/null +++ b/docs/md/api_reference.md @@ -0,0 +1,9 @@ +# API Reference + +Perspective's complete API is hosted on `docs.rs`: + +- [`perspective-client`](https://docs.rs/perspective-client/latest/perspective_client/index.html) + covers `Table` and `View` data engine API methods common for Rust, + JavaScript and Python. +- [`perspective-rs`](https://docs.rs/perspective-client/latest/perspective_client/index.html) + adds Rust-specific documentation for the Rust crate entrypoint. diff --git a/docs/md/explanation.md b/docs/md/explanation.md new file mode 100644 index 0000000000..b1303b40c8 --- /dev/null +++ b/docs/md/explanation.md @@ -0,0 +1 @@ +# Explanation diff --git a/docs/md/explanation/architecture.md b/docs/md/explanation/architecture.md new file mode 100644 index 0000000000..2739c61fd4 --- /dev/null +++ b/docs/md/explanation/architecture.md @@ -0,0 +1,15 @@ +# Data Architecture + +Application developers can choose from +[Client (WebAssembly)](./architecture/client_only.md), +[Server (Python/Node)](./architecture/server_only.md) or +[Client/Server Replicated](./architecture/client_server.md) designs to bind +data, and a web application can use one or a mix of these designs as needed. By +serializing to Apache Arrow, tables are duplicated and synchronized across +runtimes efficiently. + +Perspective is a multi-language platform. The examples in this section use +Python and JavaScript as an example, but the same general principles apply to +any `Client`/`Server` combination. + + diff --git a/rust/perspective-server/docs/architecture.dot b/docs/md/explanation/architecture/architecture.dot similarity index 100% rename from rust/perspective-server/docs/architecture.dot rename to docs/md/explanation/architecture/architecture.dot diff --git a/rust/perspective-server/docs/architecture.sub1.dot b/docs/md/explanation/architecture/architecture.sub1.dot similarity index 100% rename from rust/perspective-server/docs/architecture.sub1.dot rename to docs/md/explanation/architecture/architecture.sub1.dot diff --git a/rust/perspective-server/docs/architecture.sub1.svg b/docs/md/explanation/architecture/architecture.sub1.svg similarity index 100% rename from rust/perspective-server/docs/architecture.sub1.svg rename to docs/md/explanation/architecture/architecture.sub1.svg diff --git a/rust/perspective-server/docs/architecture.sub2.dot b/docs/md/explanation/architecture/architecture.sub2.dot similarity index 100% rename from rust/perspective-server/docs/architecture.sub2.dot rename to docs/md/explanation/architecture/architecture.sub2.dot diff --git a/rust/perspective-server/docs/architecture.sub2.svg b/docs/md/explanation/architecture/architecture.sub2.svg similarity index 100% rename from rust/perspective-server/docs/architecture.sub2.svg rename to docs/md/explanation/architecture/architecture.sub2.svg diff --git a/rust/perspective-server/docs/architecture.sub3.dot b/docs/md/explanation/architecture/architecture.sub3.dot similarity index 100% rename from rust/perspective-server/docs/architecture.sub3.dot rename to docs/md/explanation/architecture/architecture.sub3.dot diff --git a/rust/perspective-server/docs/architecture.sub3.svg b/docs/md/explanation/architecture/architecture.sub3.svg similarity index 100% rename from rust/perspective-server/docs/architecture.sub3.svg rename to docs/md/explanation/architecture/architecture.sub3.svg diff --git a/rust/perspective-server/docs/architecture.svg b/docs/md/explanation/architecture/architecture.svg similarity index 100% rename from rust/perspective-server/docs/architecture.svg rename to docs/md/explanation/architecture/architecture.svg diff --git a/docs/md/explanation/architecture/client_only.md b/docs/md/explanation/architecture/client_only.md new file mode 100644 index 0000000000..7db35eca0e --- /dev/null +++ b/docs/md/explanation/architecture/client_only.md @@ -0,0 +1,39 @@ +# Client-only + + + +_For static datasets, datasets provided by the user, and simple server-less and +read-only web applications._ + +In this design, Perspective is run as a client Browser WebAssembly library, the +dataset is downloaded entirely to the client and all calculations and UI +interactions are performed locally. Interactive performance is very good, using +WebAssembly engine for near-native runtime plus WebWorker isolation for parallel +rendering within the browser. Operations like scrolling and creating new views +are responsive. However, the entire dataset must be downloaded to the client. +Perspective is not a typical browser component, and datset sizes of 1gb+ in +Apache Arrow format will load fine with good interactive performance! + +Horizontal scaling is a non-issue, since here is no concurrent state to scale, +and only uses client-side computation via WebAssembly client. Client-only +perspective can support as many concurrent users as can download the web +application itself. Once the data is loaded, no server connection is needed and +all operations occur in the client browser, imparting no additional runtime cost +on the server beyond initial load. This also means updates and edits are local +to the browser client and will be lost when the page is refreshed, unless +otherwise persisted by your application. + +As the client-only design starts with creating a client-side Perspective +`Table`, data can be provided by any standard web service in any Perspective +compatible format (JSON, CSV or Apache Arrow). + +#### Javascript client + +```javascript +const worker = await perspective.worker(); +const table = await worker.table(csv); + +const viewer = document.createElement("perspective-viewer"); +document.body.appendChild(viewer); +await viewer.load(table); +``` diff --git a/docs/md/explanation/architecture/client_server.md b/docs/md/explanation/architecture/client_server.md new file mode 100644 index 0000000000..f6df4408eb --- /dev/null +++ b/docs/md/explanation/architecture/client_server.md @@ -0,0 +1,58 @@ +# Client/Server replicated + + + +_For medium-sized, real-time, synchronized and/or editable data sets with many +concurrent users._ + +The dataset is instantiated in-memory with a Python or Node.js Perspective +server, and web applications create duplicates of these tables in a local +WebAssembly client in the browser, synchonized efficiently to the server via +Apache Arrow. This design scales well with additional concurrent users, as +browsers only need to download the initial data set and subsequent update +deltas, while operations like scrolling, pivots, sorting, etc. are performed on +the client. + +Python servers can make especially good use of additional threads, as +Perspective will release the GIL for almost all operations. Interactive +performance on the client is very good and identical to client-only +architecture. Updates and edits are seamlessly synchonized across clients via +their virtual server counterparts using websockets and Apache Arrow. + +#### Python and Tornado server + +```python +from perspective import Server, PerspectiveTornadoHandler + +server = Server() +client = server.new_local_client() +client.table(csv, name="my_table") +routes = [( + r"/websocket", + perspective.handlers.tornado.PerspectiveTornadoHandler, + {"perspective_server": server}, +)] + +app = tornado.web.Application(routes) +app.listen(8080) +loop = tornado.ioloop.IOLoop.current() +loop.start() +``` + +#### Javascript client + +Perspective's websocket client interfaces with the Python server. then +_replicates_ the server-side Table. + +```javascript +const websocket = await perspective.websocket("ws://localhost:8080"); +const server_table = await websocket.open_table("my_table"); +const server_view = await server_table.view(); + +const worker = await perspective.worker(); +const client_table = await worker.table(server_view); + +const viewer = document.createElement("perspective-viewer"); +document.body.appendChild(viewer); +await viewer.load(client_table); +``` diff --git a/docs/md/explanation/architecture/server_only.md b/docs/md/explanation/architecture/server_only.md new file mode 100644 index 0000000000..09084c4b70 --- /dev/null +++ b/docs/md/explanation/architecture/server_only.md @@ -0,0 +1,33 @@ +# Server-only + + + +_For extremely large datasets with a small number of concurrent users._ + +The dataset is instantiated in-memory with a Python or Node.js server, and web +applications connect virtually. Has very good initial load performance, since no +data is downloaded. Group-by and other operations will run column-parallel if +configured. + +But interactive performance is poor, as every user interaction must page the +server to render. Operations like scrolling are not as responsive and can be +impacted by network latency. Web applications must be "always connected" to the +server via WebSocket. Disconnecting will prevent any interaction, scrolling, +etc. of the UI. Does not use WebAssembly. + +Each connected browser will impact server performance as long as the connection +is open, which in turn impacts interactive performance of every client. This +ultimately limits the horizontal scalabity of this architecture. Since each +client reads the perspective `Table` virtually, changes like edits and updates +are automatically reflected to all clients and persist across browser refresh. +Using the same Python server as the previous design, we can simply skip the +intermediate WebAssembly `Table` and pass the virtual table directly to `load()` + +```javascript +const websocket = await perspective.websocket("ws://localhost:8080"); +const server_table = await websocket.open_table("my_table"); + +const viewer = document.createElement("perspective-viewer"); +document.body.appendChild(viewer); +await viewer.load(server_table); +``` diff --git a/docs/md/explanation/javascript.md b/docs/md/explanation/javascript.md new file mode 100644 index 0000000000..ffea687dde --- /dev/null +++ b/docs/md/explanation/javascript.md @@ -0,0 +1,15 @@ +Perspective's JavaScript library offers a configurable UI powered by the same +fast streaming data engine, just re-compiled to WebAssembly. A simple example +which loads an [Apache Arrow](https://arrow.apache.org/) and computes a "Group +By" operation, returning a new Arrow: + +```javascript +import perspective from "@finos/perspective"; + +const table = await perspective.table(apache_arrow_data); +const view = await table.view({ group_by: ["CounterParty", "Security"] }); +const arrow = await view.to_arrow(); +``` + +[More Examples](https://github.com/finos/perspective/tree/master/examples) are +available on GitHub. diff --git a/docs/md/explanation/javascript_builds.md b/docs/md/explanation/javascript_builds.md new file mode 100644 index 0000000000..26efe0ae4c --- /dev/null +++ b/docs/md/explanation/javascript_builds.md @@ -0,0 +1,42 @@ +# JavaScript Builds + +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. To ease integration, Perspective's NPM +releases come with multiple prebuilt configurations. + +## Browser + +### ESM Builds + +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. However, 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. + +### CDN Builds + +Perspective's CDN builds are good for non-bundled scenarios, such as importing +directly from a ` +``` + +## Node.js builds + +The Node.js runtime for the `@finos/perspective` 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("@finos/perspective"); +``` + +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. diff --git a/docs/md/how_to/javascript/installation.md b/docs/md/how_to/javascript/installation.md new file mode 100644 index 0000000000..0083fecc9d --- /dev/null +++ b/docs/md/how_to/javascript/installation.md @@ -0,0 +1,31 @@ +# JavaScript NPM Installation + +Perspective releases contain several different builds for use in most +environments. + +## Browser + +Perspective's WebAssembly data engine is available via NPM in the same package +as its Node.js counterpart, `@finos/perspective`. The Perspective Viewer UI +(which has no Node.js component) must be installed separately: + +```bash +$ npm add @finos/perspective @finos/perspective-viewer +``` + +By itself, `@finos/perspective-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 `` +without Plugins would be rather boring! + +```bash +$ npm add @finos/perspective-viewer-d3fc @finos/perspective-viewer-datagrid @finos/perspective-viewer-openlayers +``` + +## Node.js + +To use Perspective from a Node.js server, simply install via NPM. + +```bash +$ npm add @finos/perspective +``` diff --git a/docs/md/how_to/javascript/loading_data.md b/docs/md/how_to/javascript/loading_data.md new file mode 100644 index 0000000000..5a4d76df91 --- /dev/null +++ b/docs/md/how_to/javascript/loading_data.md @@ -0,0 +1,38 @@ +# Loading data from a Table + +Data can be loaded into `` in the form of a `Table()` or a +`Promise` 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 ``s + +Multiple ``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 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 }]); +``` diff --git a/docs/md/how_to/javascript/loading_virtual_data.md b/docs/md/how_to/javascript/loading_virtual_data.md new file mode 100644 index 0000000000..da5ec808f4 --- /dev/null +++ b/docs/md/how_to/javascript/loading_virtual_data.md @@ -0,0 +1,37 @@ +### Loading data 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 WebAssemblt `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); +``` + +`` instances bound in this way are otherwise no different +than ``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. diff --git a/docs/md/how_to/javascript/nodejs_server.md b/docs/md/how_to/javascript/nodejs_server.md new file mode 100644 index 0000000000..9a0ce66496 --- /dev/null +++ b/docs/md/how_to/javascript/nodejs_server.md @@ -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("@finos/perspective"); +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"); +``` diff --git a/docs/md/how_to/javascript/save_restore.md b/docs/md/how_to/javascript/save_restore.md new file mode 100644 index 0000000000..32f79fde59 --- /dev/null +++ b/docs/md/how_to/javascript/save_restore.md @@ -0,0 +1,99 @@ +# Saving and restoring UI state. + +`` 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 ``'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 +`` 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 ``'s configuration +can be manipulated from JavaScript using the `restore()` method. The valid +structure of properties is described via the +[`ViewerConfig`](https://github.com/finos/perspective/blob/ebced4caa/rust/perspective-viewer/src/ts/viewer.ts#L16) +and embedded +[`ViewConfig`](https://github.com/finos/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()); +``` diff --git a/docs/md/how_to/javascript/serializing.md b/docs/md/how_to/javascript/serializing.md new file mode 100644 index 0000000000..7332fa836e --- /dev/null +++ b/docs/md/how_to/javascript/serializing.md @@ -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()); +``` diff --git a/docs/md/how_to/javascript/theming.md b/docs/md/how_to/javascript/theming.md new file mode 100644 index 0000000000..b3c1dc4d04 --- /dev/null +++ b/docs/md/how_to/javascript/theming.md @@ -0,0 +1,67 @@ +# 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 "@finos/perspective-viewer/dist/css/pro.css"; +import "@finos/perspective-viewer/dist/css/pro-dark.css"; + +// Other themes +import "@finos/perspective-viewer/dist/css/solarized.css"; +import "@finos/perspective-viewer/dist/css/solarized-dark.css"; +import "@finos/perspective-viewer/dist/css/monokai.css"; +import "@finos/perspective-viewer/dist/css/vaporwave.css"; +``` + +Alternatively, you may use `themes.css`, which bundles all default themes + +```javascript +import "@finos/perspective-viewer/dist/css/themes.css"; +``` + +If you choose not to bundle the themes yourself, they are available through +[CDN](https://cdn.jsdelivr.net/npm/@finos/perspective-viewer/dist/css/). These +can be directly linked in your HTML file: + +```html + +``` + +Note the `crossorigin="anonymous"` attribute. When including a theme from a +cross-origin context, this attribute may be required to allow +`` to detect the theme. If this fails, additional themes are +added to the `document` after `` init, or for any other +reason theme auto-detection fails, you may manually inform +`` 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"]); +``` + +`` 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 + +``` + +or + +```javascript +const viewer = document.querySelector("perspective-viewer"); +await viewer.restore({ theme: "Pro Dark" }); +``` diff --git a/docs/md/how_to/javascript/viewer.md b/docs/md/how_to/javascript/viewer.md new file mode 100644 index 0000000000..ef9c86ae5e --- /dev/null +++ b/docs/md/how_to/javascript/viewer.md @@ -0,0 +1,16 @@ +# `` Custom Element library + +`` 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 `` Web +Component will be available in any standard HTML on your site. A simple example: + +```html + + +``` diff --git a/docs/md/how_to/javascript/worker.md b/docs/md/how_to/javascript/worker.md new file mode 100644 index 0000000000..dc3d9adbe2 --- /dev/null +++ b/docs/md/how_to/javascript/worker.md @@ -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 `@finos/perspective` 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("@finos/perspective"); +``` diff --git a/docs/md/how_to/python.md b/docs/md/how_to/python.md new file mode 100644 index 0000000000..4193ef4bec --- /dev/null +++ b/docs/md/how_to/python.md @@ -0,0 +1 @@ +# Python diff --git a/docs/md/how_to/python/callbacks.md b/docs/md/how_to/python/callbacks.md new file mode 100644 index 0000000000..cc6b28d12c --- /dev/null +++ b/docs/md/how_to/python/callbacks.md @@ -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. diff --git a/docs/md/how_to/python/installation.md b/docs/md/how_to/python/installation.md new file mode 100644 index 0000000000..ae2558a9a6 --- /dev/null +++ b/docs/md/how_to/python/installation.md @@ -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. + + diff --git a/docs/md/how_to/python/jupyterlab.md b/docs/md/how_to/python/jupyterlab.md new file mode 100644 index 0000000000..33f6aea33b --- /dev/null +++ b/docs/md/how_to/python/jupyterlab.md @@ -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 ``, 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 `` Custom +Element, and has similar `save()` and `restore()` methods that +serialize/deserialize UI state for the widget. + + diff --git a/docs/md/how_to/python/multithreading.md b/docs/md/how_to/python/multithreading.md new file mode 100644 index 0000000000..5ad1e4d1c4 --- /dev/null +++ b/docs/md/how_to/python/multithreading.md @@ -0,0 +1,18 @@ +# Multi-threading + +Perspective's server API releases the GIL when called (though it may be retained +for some portion of the `Client` call to encode RPC messages). It also +dispatches to an internal thread pool for some operations, enabling better +parallelism and overall better server performance. However, Perspective's Python +interface itself will still process queries in a single queue. To enable +parallel query processing, call `set_loop_callback` with a multi-threaded +executor such as `concurrent.futures.ThreadPoolExecutor`: + +```python +def perspective_thread(): + server = perspective.Server() + loop = tornado.ioloop.IOLoop() + with concurrent.futures.ThreadPoolExecutor() as executor: + server.set_loop_callback(loop.run_in_executor, executor) + loop.start() +``` diff --git a/docs/md/how_to/python/table.md b/docs/md/how_to/python/table.md new file mode 100644 index 0000000000..c15ecbaf93 --- /dev/null +++ b/docs/md/how_to/python/table.md @@ -0,0 +1,84 @@ +# 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 are assumed _local time_ unless an +explicit timezone offset is parsed. All `"datetime"` columns (regardless of +input time zone) are _output_ to the user as `datetime.datetime` objects in +_local time_ according to the Python runtime. + +This behavior is consistent with Perspective's behavior in JavaScript. For more +details, see this in-depth +[explanation](https://github.com/finos/perspective/pull/867) of +`perspective-python` semantics around time zone handling. + +``` + +``` diff --git a/docs/md/how_to/python/websocket.md b/docs/md/how_to/python/websocket.md new file mode 100644 index 0000000000..bf60ae8a2e --- /dev/null +++ b/docs/md/how_to/python/websocket.md @@ -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 randomlu 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/finos/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.hadnlers.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 + + + +``` + +For a more complex example that offers distributed editing of the server +dataset, see +[client_server_editing.html](https://github.com/finos/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/finos/perspective/tree/master/examples/python-starlette). +- [AIOHTTP Example Project](https://github.com/finos/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_, the.g.us +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 + +``` + +```javascript +const websocket = perspective.websocket("ws://localhost:8888/websocket"); +const table = websocket.open_table("data_source"); +document.getElementById("viewer").load(table); +``` diff --git a/docs/md/how_to/rust.md b/docs/md/how_to/rust.md new file mode 100644 index 0000000000..837deaea9d --- /dev/null +++ b/docs/md/how_to/rust.md @@ -0,0 +1,28 @@ +# 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?; +``` diff --git a/docs/md/javascript.md b/docs/md/javascript.md new file mode 100644 index 0000000000..52258b5098 --- /dev/null +++ b/docs/md/javascript.md @@ -0,0 +1 @@ +# JavaScript diff --git a/docs/md/perspective.css b/docs/md/perspective.css new file mode 100644 index 0000000000..610762526e --- /dev/null +++ b/docs/md/perspective.css @@ -0,0 +1,15 @@ +perspective-viewer { + height: 500px; +} + +div.javascript:before { + content: "JavaScript:"; +} + +div.python:before { + content: "Python:"; +} + +div.rust:before { + content: "Rust:"; +} diff --git a/examples/react-example/webpack.config.js b/docs/md/perspective.js similarity index 65% rename from examples/react-example/webpack.config.js rename to docs/md/perspective.js index 1b2f1e8b6e..4365884f1c 100644 --- a/examples/react-example/webpack.config.js +++ b/docs/md/perspective.js @@ -10,43 +10,20 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -const PerspectivePlugin = require("@finos/perspective-webpack-plugin"); -const HtmlWebPackPlugin = require("html-webpack-plugin"); -const path = require("path"); +import "https://cdn.jsdelivr.net/npm/@finos/perspective-viewer/dist/cdn/perspective-viewer.js"; +import "https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js"; +import "https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-d3fc/dist/cdn/perspective-viewer-d3fc.js"; +import perspective from "https://cdn.jsdelivr.net/npm/@finos/perspective/dist/cdn/perspective.js"; -module.exports = { - mode: "development", - devtool: "source-map", - resolve: { - extensions: [".ts", ".tsx", ".js"], - }, +const WASM_URL = + "https://cdn.jsdelivr.net/npm/superstore-arrow/superstore.lz4.arrow"; - plugins: [ - new HtmlWebPackPlugin({ - title: "Perspective React Example", - template: "./src/index.html", - }), - new PerspectivePlugin(), - ], +const worker = await perspective.worker(); - module: { - rules: [ - { - test: /\.ts(x?)$/, - exclude: /node_modules/, - loader: "ts-loader", - }, - { - test: /\.css$/, - exclude: /node_modules/, - use: [{ loader: "style-loader" }, { loader: "css-loader" }], - }, - ], - }, - devServer: { - static: [ - path.join(__dirname, "dist"), - path.join(__dirname, "../../node_modules/superstore-arrow"), - ], - }, -}; +const table = await fetch(WASM_URL) + .then((x) => x.arrayBuffer()) + .then((x) => worker.table(x)); + +for (const viewer of document.querySelectorAll("perspective-viewer")) { + viewer.load(table); +} diff --git a/docs/md/perspective.md b/docs/md/perspective.md new file mode 100644 index 0000000000..676d71c102 --- /dev/null +++ b/docs/md/perspective.md @@ -0,0 +1 @@ +{{#include ../../README.md}} diff --git a/docs/md/tutorials/python/tornado.md b/docs/md/tutorials/python/tornado.md new file mode 100644 index 0000000000..0cfde7a1ef --- /dev/null +++ b/docs/md/tutorials/python/tornado.md @@ -0,0 +1,109 @@ +# Tutorial: A tornado server in Python + +Perspective ships with a pre-built Tornado handler that makes integration with +`tornado.websockets` extremely easy. This allows you to run an instance of +`Perspective` on a server using Python, open a websocket to a `Table`, and +access the `Table` in JavaScript and through ``. All +instructions sent to the `Table` are processed in Python, which executes the +commands, and returns its output through the websocket back to Javascript. + +### Python setup + +Make sure Perspective and Tornado are installed! + +```bash +pip install perspective-python tornado +``` + +To use the handler, we need to first have a `Server`, a `Client` and an instance +of a `Table`: + +```python +import perspective + +SERVER = perspective.Server() +CLIENT = SERVER.new_local_client() +``` + +Once the server has been created, create a `Table` instance with a name. The +name that you host the table under is important — it acts as a unique accessor +on the JavaScript side, which will look for a Table hosted at the websocket with +the name you specify. + +```python +TABLE = client.table(data, name="data_source_one") +``` + +After the server and table setup is complete, create a websocket endpoint and +provide it a reference to `PerspectiveTornadoHandler`. You must provide the +configuration object in the route tuple, and it must contain +`"perspective_server"`, which is a reference to the `Server` you just created. + +```python +from perspective.handlers.tornado import PerspectiveTornadoHandler + +app = tornado.web.Application([ + + # ... other handlers ... + + # Create a websocket endpoint that the client JavaScript can access + (r"/websocket", PerspectiveTornadoHandler, {"perspective_server": SERVER, "check_origin": True}) +]) +``` + +Optionally, the configuration object can also include `check_origin`, a boolean +that determines whether the websocket accepts requests from origins other than +where the server is hosted. See +[Tornado docs](https://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler.check_origin) +for more details. + +### JavaScript setup + +Once the server is up and running, you can access the Table you just hosted +using `perspective.websocket` and `open_table()`. First, create a client that +expects a Perspective server to accept connections at the specified URL: + +```javascript +import "@finos/perspective-viewer"; +import "@finos/perspective-viewer-datagrid"; +import perspective from "@finos/perspective"; + +const websocket = await perspective.websocket("ws://localhost:8888/websocket"); +``` + +Next open the `Table` we created on the server by name: + +```javascript +const table = await websocket.open_table("data_source_one"); +``` + +`table` is a proxy for the `Table` we created on the server. All operations that +are possible through the JavaScript API are possible on the Python API as well, +thus calling `view()`, `schema()`, `update()` etc. on `const table` will pass +those operations to the Python `Table`, execute the commands, and return the +result back to JavaScript. Similarly, providing this `table` to a +`` instance will allow virtual rendering: + +```javascript +const viewer = document.createElement("perspective-viewer"); +viewer.style.height = "500px"; +document.body.appendChild(viewer); +await viewer.load(table); +``` + +`perspective.websocket` expects a Websocket URL where it will send instructions. +When `open_table` is called, the name to a hosted Table is passed through, and a +request is sent through the socket to fetch the Table. No actual `Table` +instance is passed inbetween the runtimes; all instructions are proxied through +websockets. + +This provides for great flexibility — while `Perspective.js` is full of +features, browser WebAssembly runtimes currently have some performance +restrictions on memory and CPU feature utilization, and the architecture in +general suffers when the dataset itself is too large to download to the client +in full. + +The Python runtime does not suffer from memory limitations, utilizes Apache +Arrow internal threadpools for threading and parallel processing, and generates +architecture optimized code, which currently makes it more suitable as a +server-side runtime than `node.js`. diff --git a/docs/package.json b/docs/package.json index 6c0ba45ccc..4c3141da69 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,32 +15,31 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "^3.4.0", - "@docusaurus/preset-classic": "^3.4.0", + "@docusaurus/core": "^3.7.0", + "@docusaurus/preset-classic": "^3.7.0", "@finos/perspective": "workspace:^", "@finos/perspective-viewer": "workspace:^", "@finos/perspective-viewer-d3fc": "workspace:^", "@finos/perspective-viewer-datagrid": "workspace:^", - "@finos/perspective-webpack-plugin": "workspace:^", "@mdx-js/react": "^3.0.0", "blocks": "workspace:^", "clsx": "^2.0.0", "mkdirp": "3", "prism-react-renderer": "^1.3.3", "puppeteer": "^23", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^18", + "react-dom": "^18", "superstore-arrow": "3.0.0" }, "devDependencies": { "arraybuffer-loader": "^1.0.2", - "@docusaurus/module-type-aliases": "^3.4.0", - "@docusaurus/tsconfig": "^3.4.0", - "@docusaurus/types": "^3.4.0", + "@docusaurus/module-type-aliases": "^3.7.0", + "@docusaurus/tsconfig": "^3.7.0", + "@docusaurus/types": "^3.7.0", "typescript": "~5.2.2" }, "peerDependencies": { - "@docusaurus/theme-common": "^3.4.0" + "@docusaurus/theme-common": "^3.7.0" }, "browserslist": { "production": [ diff --git a/docs/plugins/perspective-loader/index.js b/docs/plugins/perspective-loader/index.js index 745329e450..b34ed7d650 100644 --- a/docs/plugins/perspective-loader/index.js +++ b/docs/plugins/perspective-loader/index.js @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -const PerspectiveWebpackPlugin = require("@finos/perspective-webpack-plugin"); +const webpack = require("webpack"); module.exports = function (context, options) { return { @@ -20,7 +20,10 @@ module.exports = function (context, options) { config.optimization.minimizer[0].options.minimizer.options.module = true; } - config.experiments = config.experiments || {}; + config.experiments = config.experiments || { + asyncWebAssembly: true, + }; + config.experiments.topLevelAwait = true; config.module.rules.map((x) => { if (x.test.toString() === "/\\.css$/i") { @@ -37,7 +40,14 @@ module.exports = function (context, options) { node: { __filename: false, }, - plugins: [new PerspectiveWebpackPlugin({})], + plugins: isServer + ? [ + new webpack.NormalModuleReplacementPlugin( + /@finos\/perspective/, + "@finos/perspective/dist/esm/perspective.js" + ), + ] + : [], }; }, }; diff --git a/docs/src/components/DocItem/browser.js b/docs/src/components/DocItem/browser.js index f5993c38a3..8d943cbc15 100644 --- a/docs/src/components/DocItem/browser.js +++ b/docs/src/components/DocItem/browser.js @@ -10,8 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { SUPERSTORE_TABLE } from "@site/src/data/superstore.js"; - export async function main(colorMode) { await import("@finos/perspective-viewer", { type: "module" }); await import("@finos/perspective-viewer-datagrid"); @@ -21,6 +19,8 @@ export async function main(colorMode) { "perspective-viewer:not(.nosuperstore)" ); + const { SUPERSTORE_TABLE } = await import("@site/src/data/superstore.js"); + for (const viewer of viewers) { viewer.load(SUPERSTORE_TABLE); const token = { diff --git a/docs/src/components/ExampleGallery/index.js b/docs/src/components/ExampleGallery/index.js index a951a7e91c..4e880a3c6c 100644 --- a/docs/src/components/ExampleGallery/index.js +++ b/docs/src/components/ExampleGallery/index.js @@ -63,7 +63,6 @@ export default function ExampleGallery(props) { } function OverlayDemo(props) { - const { SUPERSTORE_TABLE } = require("@site/src/data/superstore.js"); const ref = useRef(); const dismissCallback = useCallback( (event) => { @@ -77,6 +76,10 @@ function OverlayDemo(props) { const perspectiveRef = useCallback( (viewer) => { + const { + SUPERSTORE_TABLE, + } = require("@site/src/data/superstore.js"); + if (viewer !== null) { viewer.load(SUPERSTORE_TABLE); viewer.restore({ diff --git a/docs/src/data/worker.js b/docs/src/data/worker.js index 0c6123ec9f..7fde1a54ae 100644 --- a/docs/src/data/worker.js +++ b/docs/src/data/worker.js @@ -10,7 +10,21 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -const WORKER = import("@finos/perspective").then((x) => x.worker()); +const WORKER = (async () => { + const perspective = await import("@finos/perspective"); + const perspective_viewer = await import("@finos/perspective-viewer"); + const wasm = import("@finos/perspective/dist/wasm/perspective-server.wasm"); + const client_wasm = import( + "@finos/perspective-viewer/dist/wasm/perspective-viewer.wasm" + ); + + await Promise.all([ + perspective.init_server(wasm), + perspective_viewer.init_client(client_wasm), + ]); + + return await perspective.worker(); +})(); export function worker() { return WORKER; diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..8bb2dc5609 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,19 @@ +# Examples + +The projects in the directory are self-contained Perspective examples. However, +they are designed to run within the Perspective source repository tree. This +makes it easy for Perspective OSS developers to test changes and validate that +our Examples continue to work, but it also means these examples will most not +work without properly linking in the Perspective build environment. + +In order to _run_ a project in this directory as written: + +1. Install and build Perspective from source. +2. Run the project with `pnpm run start $PROJECT_NAME` from the repository root + (_not_ the `/examples` directory). + +# Optional + +Generally, the changes necessary to make these examples run _without_ the +Perspective source repository are minor path or metadata corrections. Your +results may vary. diff --git a/examples/blocks/README.md b/examples/blocks/README.md new file mode 100644 index 0000000000..203462c511 --- /dev/null +++ b/examples/blocks/README.md @@ -0,0 +1,4 @@ +# Bloc.ks + +Examples designed to be hosted on GitHub Pages. See the individual projects in +the `src/` directory for more info. diff --git a/examples/blocks/src/citibike/citibike.js b/examples/blocks/src/citibike/citibike.js index 6f73be549d..73e395603c 100644 --- a/examples/blocks/src/citibike/citibike.js +++ b/examples/blocks/src/citibike/citibike.js @@ -85,7 +85,7 @@ async function main() { } // Start a recurring asyn call to `get_feed` and update the `table` with the response. - get_feed("station_status", table.update); + get_feed("station_status", table.update.bind(table)); window.workspace.tables.set("citibike", Promise.resolve(table)); const layout = await get_layout(); diff --git a/examples/blocks/src/citibike/index.html b/examples/blocks/src/citibike/index.html index 8d03e0f15f..ee1c82d697 100644 --- a/examples/blocks/src/citibike/index.html +++ b/examples/blocks/src/citibike/index.html @@ -3,20 +3,14 @@ - diff --git a/examples/blocks/src/covid/index.html b/examples/blocks/src/covid/index.html index 2c19d0e007..e67fa518b3 100644 --- a/examples/blocks/src/covid/index.html +++ b/examples/blocks/src/covid/index.html @@ -2,8 +2,8 @@ - - + + diff --git a/examples/blocks/src/fractal/index.html b/examples/blocks/src/fractal/index.html index 5854e8a37c..46e0a1ad10 100644 --- a/examples/blocks/src/fractal/index.html +++ b/examples/blocks/src/fractal/index.html @@ -2,8 +2,8 @@ - - + + diff --git a/examples/blocks/src/market/index.html b/examples/blocks/src/market/index.html index 96141ef1c4..adb99186db 100644 --- a/examples/blocks/src/market/index.html +++ b/examples/blocks/src/market/index.html @@ -2,8 +2,8 @@ - - + + diff --git a/examples/blocks/src/movies/index.html b/examples/blocks/src/movies/index.html index 6919cb21e2..e7f45997ae 100644 --- a/examples/blocks/src/movies/index.html +++ b/examples/blocks/src/movies/index.html @@ -1,22 +1,17 @@ - - + + - -This crate contains the server/engine components of the -[Perspective](https://perspective.finos.org) data visualization suite. It is -meant to be used in conjunction with the other crates of this project, e.g. -`perspective-client` to create client connections to a server. - -The [`perspective`] crate provides a convenient frontend for Rust developers, -including both [`perspective_client`] and [`perspective_server`] as well as -other convenient integration helpers. - -# Data Binding Options - - - -Application developers can choose from [Client (WebAssembly)](#client-only), -[Server (Python/Node)](#server-only) or -[Client/Server Replicated](#clientserver-replicated) designs to bind data, and a -web application can use one or a mix of these designs as needed. By serializing -to Apache Arrow, tables are duplicated and synchronized across runtimes -efficiently. - -Perspective is a multi-language platform. The examples in this section use -Python and JavaScript as an example, but the same general principles apply to -any `Client`/`Server` combination. - -## Client-only - - - -_For static datasets, datasets provided by the user, and simple server-less and -read-only web applications._ - -In this design, Perspective is run as a client Browser WebAssembly library, the -dataset is downloaded entirely to the client and all calculations and UI -interactions are performed locally. Interactive performance is very good, using -WebAssembly engine for near-native runtime plus WebWorker isolation for parallel -rendering within the browser. Operations like scrolling and creating new views -are responsive. However, the entire dataset must be downloaded to the client. -Perspective is not a typical browser component, and datset sizes of 1gb+ in -Apache Arrow format will load fine with good interactive performance! - -Horizontal scaling is a non-issue, since here is no concurrent state to scale, -and only uses client-side computation via WebAssembly client. Client-only -perspective can support as many concurrent users as can download the web -application itself. Once the data is loaded, no server connection is needed and -all operations occur in the client browser, imparting no additional runtime cost -on the server beyond initial load. This also means updates and edits are local -to the browser client and will be lost when the page is refreshed, unless -otherwise persisted by your application. - -As the client-only design starts with creating a client-side Perspective -`Table`, data can be provided by any standard web service in any Perspective -compatible format (JSON, CSV or Apache Arrow). - -#### Javascript client - -```javascript -const worker = await perspective.worker(); -const table = await worker.table(csv); - -const viewer = document.createElement("perspective-viewer"); -document.body.appendChild(viewer); -await viewer.load(table); -``` - -## Client/Server Replicated - - - -_For medium-sized, real-time, synchronized and/or editable data sets with many -concurrent users._ - -The dataset is instantiated in-memory with a Python or Node.js Perspective -server, and web applications create duplicates of these tables in a local -WebAssembly client in the browser, synchonized efficiently to the server via -Apache Arrow. This design scales well with additional concurrent users, as -browsers only need to download the initial data set and subsequent update -deltas, while operations like scrolling, pivots, sorting, etc. are performed on -the client. - -Python servers can make especially good use of additional threads, as -Perspective will release the GIL for almost all operations. Interactive -performance on the client is very good and identical to client-only -architecture. Updates and edits are seamlessly synchonized across clients via -their virtual server counterparts using websockets and Apache Arrow. - -#### Python and Tornado server - -```python -from perspective import Server, PerspectiveTornadoHandler - -server = Server() -client = server.new_local_client() -client.table(csv, name="my_table") -routes = [( - r"/websocket", - perspective.handlers.tornado.PerspectiveTornadoHandler, - {"perspective_server": server}, -)] - -app = tornado.web.Application(routes) -app.listen(8080) -loop = tornado.ioloop.IOLoop.current() -loop.start() -``` - -#### Javascript client - -Perspective's websocket client interfaces with the Python server. then -_replicates_ the server-side Table. - -```javascript -const websocket = await perspective.websocket("ws://localhost:8080"); -const server_table = await websocket.open_table("my_table"); -const server_view = await server_table.view(); - -const worker = await perspective.worker(); -const client_table = await worker.table(server_view); - -const viewer = document.createElement("perspective-viewer"); -document.body.appendChild(viewer); -await viewer.load(client_table); -``` - -## Server-only - - - -_For extremely large datasets with a small number of concurrent users._ - -The dataset is instantiated in-memory with a Python or Node.js server, and web -applications connect virtually. Has very good initial load performance, since no -data is downloaded. Group-by and other operations will run column-parallel if -configured. - -But interactive performance is poor, as every user interaction must page the -server to render. Operations like scrolling are not as responsive and can be -impacted by network latency. Web applications must be "always connected" to the -server via WebSocket. Disconnecting will prevent any interaction, scrolling, -etc. of the UI. Does not use WebAssembly. - -Each connected browser will impact server performance as long as the connection -is open, which in turn impacts interactive performance of every client. This -ultimately limits the horizontal scalabity of this architecture. Since each -client reads the perspective `Table` virtually, changes like edits and updates -are automatically reflected to all clients and persist across browser refresh. -Using the same Python server as the previous design, we can simply skip the -intermediate WebAssembly `Table` and pass the virtual table directly to `load()` - -```javascript -const websocket = await perspective.websocket("ws://localhost:8080"); -const server_table = await websocket.open_table("my_table"); - -const viewer = document.createElement("perspective-viewer"); -document.body.appendChild(viewer); -await viewer.load(server_table); -``` - -# Feature Flags - -The following feature flags are available to enable in your `Cargo.toml`: - -- `external-cpp` Set this flag to configure this crate's compile process to - look for Perspective C++ source code in the environment rather than locally, - e.g. for when you build this crate in-place in the Perspective repo source - tree. diff --git a/rust/perspective-server/src/lib.rs b/rust/perspective-server/src/lib.rs index 98452d5152..09f9c33921 100644 --- a/rust/perspective-server/src/lib.rs +++ b/rust/perspective-server/src/lib.rs @@ -10,8 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -#![doc = include_str!("../docs/lib_gen.md")] - extern crate link_cplusplus; mod ffi; diff --git a/rust/perspective-viewer/src/less/plugin-selector.less b/rust/perspective-viewer/src/less/plugin-selector.less index 2973b6b7f0..4ed116d81d 100644 --- a/rust/perspective-viewer/src/less/plugin-selector.less +++ b/rust/perspective-viewer/src/less/plugin-selector.less @@ -18,7 +18,6 @@ #plugin_selector_container { min-height: 48px; - overflow: scroll; display: flex; white-space: nowrap; flex-direction: column; @@ -38,6 +37,10 @@ margin-top: 1px; } + .plugin-selector-options { + overflow-y: auto; + } + &.open { position: absolute; margin: 0; diff --git a/rust/perspective-viewer/src/rust/components/plugin_selector.rs b/rust/perspective-viewer/src/rust/components/plugin_selector.rs index 56b931fd93..5abb3643a3 100644 --- a/rust/perspective-viewer/src/rust/components/plugin_selector.rs +++ b/rust/perspective-viewer/src/rust/components/plugin_selector.rs @@ -129,7 +129,9 @@ impl Component for PluginSelector {
- if self.is_open { { items.collect::() } } + if self.is_open { +
{ items.collect::() }
+ }
} diff --git a/packages/perspective-esbuild-plugin/README.md b/tools/perspective-esbuild-plugin/README.md similarity index 77% rename from packages/perspective-esbuild-plugin/README.md rename to tools/perspective-esbuild-plugin/README.md index 290a4f1706..4e4fc88c40 100644 --- a/packages/perspective-esbuild-plugin/README.md +++ b/tools/perspective-esbuild-plugin/README.md @@ -2,11 +2,14 @@ Applications bundled with `esbuild` can make use of the `@finos/perspective-esbuild-plugin` module. A full example can be found in the -repo under [`examples/esbuild-example`](https://github.com/finos/perspective/tree/master/examples/esbuild-example). +repo under +[`examples/esbuild-example`](https://github.com/finos/perspective/tree/master/examples/esbuild-example). ```javascript const esbuild = require("esbuild"); -const {PerspectiveEsbuildPlugin} = require("@finos/perspective-esbuild-plugin"); +const { + PerspectiveEsbuildPlugin, +} = require("@finos/perspective-esbuild-plugin"); esbuild.build({ entryPoints: ["src/index.js"], @@ -15,6 +18,7 @@ esbuild.build({ bundle: true, loader: { ".ttf": "file", + ".wasm": "file", }, }); ``` diff --git a/packages/perspective-esbuild-plugin/amd.js b/tools/perspective-esbuild-plugin/amd.js similarity index 100% rename from packages/perspective-esbuild-plugin/amd.js rename to tools/perspective-esbuild-plugin/amd.js diff --git a/packages/perspective-esbuild-plugin/build.js b/tools/perspective-esbuild-plugin/build.js similarity index 100% rename from packages/perspective-esbuild-plugin/build.js rename to tools/perspective-esbuild-plugin/build.js diff --git a/packages/perspective-esbuild-plugin/external.js b/tools/perspective-esbuild-plugin/external.js similarity index 100% rename from packages/perspective-esbuild-plugin/external.js rename to tools/perspective-esbuild-plugin/external.js diff --git a/packages/perspective-esbuild-plugin/index.js b/tools/perspective-esbuild-plugin/index.js similarity index 88% rename from packages/perspective-esbuild-plugin/index.js rename to tools/perspective-esbuild-plugin/index.js index 5328ada8a2..d15d51c6bc 100644 --- a/packages/perspective-esbuild-plugin/index.js +++ b/tools/perspective-esbuild-plugin/index.js @@ -16,18 +16,23 @@ const { WorkerPlugin } = require("./worker.js"); exports.PerspectiveEsbuildPlugin = function PerspectiveEsbuildPlugin( options = {} ) { - const wasm_plugin = WasmPlugin( - !!options.wasm?.inline, - !options.wasm?.webpack_hack - ); + // const wasm_plugin = WasmPlugin( + // !!options.wasm?.inline, + // !options.wasm?.webpack_hack + // ); const worker_plugin = WorkerPlugin({ targetdir: options.worker?.targetdir, }); function setup(build) { - wasm_plugin.setup(build); - worker_plugin.setup(build); + // if (options.wasm !== false) { + // wasm_plugin.setup(build); + // } + + if (options.worker !== false) { + worker_plugin.setup(build); + } } return { diff --git a/packages/perspective-esbuild-plugin/package.json b/tools/perspective-esbuild-plugin/package.json similarity index 100% rename from packages/perspective-esbuild-plugin/package.json rename to tools/perspective-esbuild-plugin/package.json diff --git a/packages/perspective-esbuild-plugin/resolve.js b/tools/perspective-esbuild-plugin/resolve.js similarity index 100% rename from packages/perspective-esbuild-plugin/resolve.js rename to tools/perspective-esbuild-plugin/resolve.js diff --git a/packages/perspective-esbuild-plugin/wasm.js b/tools/perspective-esbuild-plugin/wasm.js similarity index 95% rename from packages/perspective-esbuild-plugin/wasm.js rename to tools/perspective-esbuild-plugin/wasm.js index e55a107292..7276048076 100644 --- a/packages/perspective-esbuild-plugin/wasm.js +++ b/tools/perspective-esbuild-plugin/wasm.js @@ -24,10 +24,13 @@ exports.WasmPlugin = function WasmPlugin(inline, webpack_hack) { args.namespace === "wasm-stub" || args.namespace === "wasm-inline" ) { - const entryPoint = path.join( - args.pluginData.resolveDir, - args.path - ); + let entryPoint = args.path; + if (args.path.startsWith(".")) { + entryPoint = path.join( + args.pluginData.resolveDir, + entryPoint + ); + } return { path: entryPoint, diff --git a/packages/perspective-esbuild-plugin/worker.js b/tools/perspective-esbuild-plugin/worker.js similarity index 100% rename from packages/perspective-esbuild-plugin/worker.js rename to tools/perspective-esbuild-plugin/worker.js diff --git a/tools/perspective-test/playwright.config.ts b/tools/perspective-test/playwright.config.ts index 1f42a4cdb1..6d0d430807 100644 --- a/tools/perspective-test/playwright.config.ts +++ b/tools/perspective-test/playwright.config.ts @@ -199,9 +199,9 @@ const GLOBAL_TEARDOWN_PATH = __require.resolve( // See https://playwright.dev/docs/test-configuration. export default defineConfig({ - timeout: 360_000, + timeout: 30_000, expect: { - timeout: 360_000, + timeout: 30_000, }, forbidOnly: !!process.env.CI, retries: 0,