diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md
index 3259822956..b43566aebb 100644
--- a/.github/ISSUE_TEMPLATE/Bug_report.md
+++ b/.github/ISSUE_TEMPLATE/Bug_report.md
@@ -1,24 +1,126 @@
---
-name: 🐛 Bug Report
-about: If something isn't working as expected 🤔.
-
+name: Bug Report
+about: If something isn't working as expected.
---
## Bug Report
### Steps to Reproduce:
- 1. ...step 1 description...
- 2. ...step 2 description...
- 3. ...step 3 description...
+
+Please provide a full reproduction of the issue. There are three ways we accept
+repros:
+
+1. If the issue you are reporting is a UX/UI issue which can be recreated by
+ visiting a Perspective demo _hosted by the project itself_, and any dataset
+ required to reproduce the error can be included in the report. In this case,
+ please provide detailed step-by-step instructions on how to reproduce,
+ including any screenshots which help illustrate, as well as including any
+ fully-encoded test data we may need.
+
+2. If you are reporting a build or installation issue with the library itself,
+ which can be recreated from a shell. In this case, please provided detailed
+ code blocks describing how you tried to install, which commands were issued,
+ including and dependencies you needed to install and hwo you installed them.
+
+3. If you are reporting a _anything else_, including but not limited to:
+
+ - Build issues which require _any_ metadata files e.g. a `package.json`,
+ `Cargo.toml`, etc
+ - Bundler or packaging errors with JavaScript
+ - Library functions which return the wrong results or error
+ - CPU or memory usage performance regressions, or regressions in thread
+ utilization
+
+ In this case, we require a _complete reproduction_ of the issue in the form
+ of a repository. Quoting this exceptional definition from
+ [@Rich-Harris's micro-essay on Repros](https://gist.github.com/Rich-Harris/88c5fc2ac6dc941b22e7996af05d70ff),
+ please follow these guidelines:
+
+ > 1. Create a sample repo on GitHub (or wherever)
+ > 2. Demonstrate the problem, and nothing but the problem. If the app where
+ > you're experiencing the issue happens to use Gulp, I don't care,
+ > unless the problem involves Gulp. Remove that stuff. Whittle it down
+ > to the _bare minimum_ of code that reliably demonstrates the issue.
+ > Get rid of any dependencies that aren't _directly_ related to the
+ > problem.
+ > 3. Install all your dependencies to `package.json`. If I can't clone the
+ > repo and do `npm install && npm run build` (or similar – see point 4)
+ > to see the problem, because I need some globally installed CLI tool or
+ > whatever, then you've made it harder to get to the bottom of the
+ > issue.
+ > 4. Include instructions in the repo, along with a description of the
+ > expected and actual behaviour. Obviously the issue should include
+ > information about the bug as well, but it's really helpful if
+ > `README.md` includes that information, plus a link back to the issue.
+ > If there are any instructions beyond `npm install && npm run build`,
+ > they should go here.
+
+ Some examples which _do not_ qualify as _complete_ and are mostly useless to
+ us for debugging:
+
+ - Instructions which ask us to visit a website or download an application,
+ even if it is _completely_ open source (and expecially if it is not)
+ - Instructions which just describe how to create a project, e.g. with a
+ specific build tool or template
+ - Screenshots of exceptions
+ - Screenshots of code
+ - Code snippets copied from a larger application context
### Expected Result:
-...description of what you expected to see...
+
+Describe what you expected to see. If you are reporting a UX/UI error, this may
+include screenshots with annotations.
### Actual Result:
-...what actually happened, including full exceptions (please include the entire stack trace, including "caused by" entries), log entries, screen shots etc. where appropriate...
+
+Describe what actually happened, with special attention to the errant behavior.
+Always include:
+
+- OS and version
+- Platform/language + version
+
+If you are reporting a UX/UI error:
+
+- (if websocket) Platform/language + version of remote perspective server.
+- Full exception/error message if applicable.
+- Any potentially relevent JavaScript developer console error logs.
+- Screenshots of the UI in an obviously broken state. (but please try to avoid
+ screenshots of your code, see below)
+
+If you are reporting a library error:
+
+- (if websocket) Platform/language + version of remote perspective server.
+- Full exception error capture (please include the entire stack trace,
+ including "caused by" entries), log entries, etc. where appropriate. Please
+ avoid posting screenshots of code (which we may need to debug).
+
+If you are reporting a build or install error:
+
+- Full error output from running your repro, formatted as a code block (please
+ _do not_ include screenshots of build logs).
### Environment:
-...version and build of the project, OS and runtime versions, virtualised environment (if any), etc. ...
+
+For JavaScript (browser):
+
+- `@finos/perspective` version
+- Browser and version
+- OS
+- (if websocket) Language/version/OS of perspective server
+
+For Node.js:
+
+- `node` version
+- OS
+
+For Python
+
+- `python` interpreter version (Only CPython).
+- package manager and version (conda/pip/\*)
+ - Are you compiling from an sdist of wheel?
+- Platform and version (Jupyter/tornado/lib/\*)
+- OS
### Additional Context:
-...add any other context about the problem here. If applicable, add screenshots to help explain...
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md
index b01f152f0b..7996e9312d 100644
--- a/.github/ISSUE_TEMPLATE/Feature_request.md
+++ b/.github/ISSUE_TEMPLATE/Feature_request.md
@@ -1,17 +1,22 @@
---
-name: 🚀 Feature Request
-about: I have a suggestion (and may want to implement it 🙂)!
-
+name: Feature Request
+about: I have a suggestion.
---
## Feature Request
### Description of Problem:
-...what *problem* are you trying to solve that the project doesn't currently solve?
-...please resist the temptation to describe your request in terms of a solution. Job Story form ("When [triggering condition], I want to [motivation/goal], so I can [outcome].") can help ensure you're expressing a problem statement.
+...what _problem_ are you trying to solve that the project doesn't currently
+solve?
+
+...please resist the temptation to describe your request in terms of a solution.
+Job Story form ("When [triggering condition], I want to [motivation/goal], so I
+can [outcome].") can help ensure you're expressing a problem statement.
### Potential Solutions:
-...clearly and concisely describe what you want to happen. Add any considered drawbacks.
+
+...clearly and concisely describe what you want to happen. Add any considered
+drawbacks.
... if you've considered alternatives, clearly and concisely describe those too.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..0a3b172cb5
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,10 @@
+### Pull Request Checklist
+
+- [ ] Description which clearly states what problems the PR solves.
+- [ ] Description contains a link to the Github Issue, and any relevent
+ Discussions, this PR applies to.
+- [ ] Include new tests that fail without this PR but passes with it.
+- [ ] Include any relevent Documentation changes related to this change.
+- [ ] Verify all commits have been _signed_ in accordance with the DCO policy.
+- [ ] Reviewed PR commit history to remove unnecessary changes.
+- [ ] Make sure your PR passes _build_, _test_ and _lint_ steps _completely_.
diff --git a/.github/PULL_REQUEST_TEMPLATE/Bugfix.md b/.github/PULL_REQUEST_TEMPLATE/Bugfix.md
deleted file mode 100644
index c1627eeca9..0000000000
--- a/.github/PULL_REQUEST_TEMPLATE/Bugfix.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: 🐛 Bug Fix
-about: Fixes a bug reported in our issues list 📕
-
----
-
-# Bugfix
-
-
-
-## Testing
-
-
-
-## Screenshots (if appropriate)
-
-
-
-## Checklist
-
-
-
-- [ ] I have read the [`CONTRIBUTING.md`](https://github.com/finos/perspective/blob/master/CONTRIBUTING.md) and followed its [Guidelines](https://github.com/finos/perspective/blob/master/CONTRIBUTING.md#guidelines)
-- [ ] I have linted my code locally, following the project's code style
-- [ ] I have tested my changes locally, and changes pass on the Azure Pipelines CI.
diff --git a/.github/PULL_REQUEST_TEMPLATE/Documentation.md b/.github/PULL_REQUEST_TEMPLATE/Documentation.md
deleted file mode 100644
index 9574b2df55..0000000000
--- a/.github/PULL_REQUEST_TEMPLATE/Documentation.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-name: 📄 Documentation
-about: Improves documentation for the API or on the docs website.
-
----
-
-# Documentation
-
-
-
-## Validation
-
-
-
-- [ ] I have linted, spell-checked, and grammar-checked my documentation additions.
-- [ ] I have run `pnpm run docs` to regenerate the API documentation from source.
-- [ ] Within `/docs`, I have run `pnpm run start` to regenerate the Docusaurus site, and I have validated the changes
-- [ ] My additions are styled and structured correctly on the Docusaurus site
-
-## Checklist
-
-
-
-- [ ] I have read the [`CONTRIBUTING.md`](https://github.com/finos/perspective/blob/master/CONTRIBUTING.md) and followed its [Guidelines](https://github.com/finos/perspective/blob/master/CONTRIBUTING.md#guidelines)
diff --git a/.github/PULL_REQUEST_TEMPLATE/Feature.md b/.github/PULL_REQUEST_TEMPLATE/Feature.md
deleted file mode 100644
index bff5355e60..0000000000
--- a/.github/PULL_REQUEST_TEMPLATE/Feature.md
+++ /dev/null
@@ -1,54 +0,0 @@
----
-name: New Feature
-about: Adds a new feature to Perspective 🙂!
-
----
-
-# Feature
-
-
-
-
-
-- [ ] Breaking feature
-- [ ] Non-breaking feature
-
-
-## Changelog
-
-- {A bullet-pointed changelog that outlines the contents of your PR.}
-
-## Testing
-
-
-
-## Screenshots (if appropriate)
-
-
-
-## Checklist
-
-
-
-- [ ] I have read the [`CONTRIBUTING.md`](https://github.com/finos/perspective/blob/master/CONTRIBUTING.md) and followed its [Guidelines](https://github.com/finos/perspective/blob/master/CONTRIBUTING.md#guidelines)
-- [ ] I have linted my code locally, following the project's code style
-- [ ] I have tested my changes locally, and changes pass on the Azure Pipelines CI.
diff --git a/.github/actions/install-deps/action.yaml b/.github/actions/install-deps/action.yaml
index de7816b83a..74f615d0d8 100644
--- a/.github/actions/install-deps/action.yaml
+++ b/.github/actions/install-deps/action.yaml
@@ -32,6 +32,9 @@ inputs:
python:
default: "true"
description: "Install Python dependencies?"
+ playwright:
+ default: "false"
+ description: "Install browsers for playwright testing"
clean:
default: "false"
description: "Clean unused deps. This is helpful if we run out of HD but slow!"
@@ -48,7 +51,7 @@ runs:
using: "composite"
steps:
- name: Clean System
- uses: AdityaGarg8/remove-unwanted-software@v3
+ uses: AdityaGarg8/remove-unwanted-software@v4.1
if: ${{ inputs.clean == 'true' && runner.os != 'Windows' }}
with:
remove-android: "true"
@@ -117,6 +120,8 @@ runs:
vcpkg.exe integrate install
echo "VCPKG_INSTALLATION_ROOT=${env:VCPKG_INSTALLATION_ROOT}"
echo "VCPKG_INSTALLATION_ROOT=${env:VCPKG_INSTALLATION_ROOT}" >> $env:GITHUB_OUTPUT
+ echo "${env:VCPKG_INSTALLATION_ROOT}" >> $env:GITHUB_PATH
+ echo "VCPKG_ROOT=${env:VCPKG_INSTALLATION_ROOT}" >> $env:GITHUB_ENV
dir env:
env:
PYTHON_VERSION: ${{ matrix.python-version }}
@@ -124,6 +129,16 @@ runs:
VCPKG_PLATFORM_TOOLSET: v143
if: ${{ runner.os == 'Windows' && inputs.cpp == 'true' }}
+ # https://github.com/apache/arrow/issues/38391
+ - if: ${{ runner.os == 'macOS' }}
+ shell: bash
+ run: echo "MACOSX_DEPLOYMENT_TARGET=$(sw_vers -productVersion)" >> $GITHUB_ENV
+
+ # Use python 3.9 from manylinu
+ - run: echo "/opt/python/cp39-cp39/bin" >> $GITHUB_PATH
+ shell: bash
+ if: ${{ runner.os == 'Linux' }}
+
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
if: ${{ inputs.python == 'true' && runner.os != 'Linux' }}
@@ -179,12 +194,12 @@ runs:
- name: Install JS dependencies
shell: bash
- if: ${{ inputs.javascript == 'true' }}
+ if: ${{ inputs.javascript == 'true' && inputs.playwright == 'true' }}
run: pnpm install
- name: Install JS dependencies
shell: bash
- if: ${{ inputs.javascript == 'false' }}
+ if: ${{ inputs.javascript == 'false' || inputs.playwright == 'false'}}
run: pnpm install --ignore-scripts
- name: Install Python dependencies
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index aca567b473..e9ea605a0b 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -59,6 +59,58 @@ concurrency:
cancel-in-progress: true
jobs:
+ # , . . .-,--.
+ # ) . ,-. |- ,-. ,-. ,-| ' | \ ,-. ,-. ,-.
+ # / | | | | ,-| | | | | , | / | | | `-.
+ # `--' ' ' ' `' `-^ ' ' `-^ `-^--' `-' `-' `-'
+ #
+ lint_and_docs:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-22.04
+ python-version:
+ - 3.9
+ node-version: [20.x]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Config
+ id: config-step
+ uses: ./.github/actions/config
+
+ - name: Initialize Build
+ id: init-step
+ uses: ./.github/actions/install-deps
+ with:
+ skip_cache: ${{ steps.config-step.outputs.SKIP_CACHE }}
+
+ - name: Metadata Build
+ run: pnpm run build --ci
+ env:
+ PACKAGE: "perspective-metadata"
+
+ - name: Lint
+ run: pnpm run lint
+
+ # - name: Docs Build
+ # run: pnpm run docs
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: perspective-metadata
+ path: |
+ rust/perspective-server/cpp/
+ rust/perspective-server/cmake/
+ rust/perspective-client/src/
+ rust/perspective-client/docs/
+ rust/perspective-js/src/ts/
+ rust/perspective-viewer/src/ts/
+
# ,-,---. . . ,-_/ .---. .
# '|___/ . . . | ,-| ' | ,-. . , ,-. \___ ,-. ,-. . ,-. |-
# ,| \ | | | | | | | ,-| | / ,-| \ | | | | | |
@@ -67,6 +119,7 @@ jobs:
# `--' '
build_js:
runs-on: ${{ matrix.os }}
+ needs: [lint_and_docs]
strategy:
fail-fast: false
matrix:
@@ -84,29 +137,26 @@ jobs:
id: config-step
uses: ./.github/actions/config
+ - uses: actions/download-artifact@v4
+ with:
+ name: perspective-metadata
+ path: rust/
+
- name: Initialize Build
id: init-step
uses: ./.github/actions/install-deps
with:
clean: "true"
python: "false"
+ playwright: "true"
skip_cache: ${{ steps.config-step.outputs.SKIP_CACHE }}
- name: WebAssembly Build
run: pnpm run build --ci
env:
- PACKAGE: "!perspective-python,!perspective-jupyterlab"
+ PACKAGE: "perspective-cpp,perspective,perspective-viewer,perspective-viewer-datagrid,perspective-viewer-d3fc,perspective-viewer-openlayers,perspective-workspace,perspective-cli"
# PSP_USE_CCACHE: 1
- - name: Lint
- run: pnpm run lint
-
- # - name: Docs Build
- # run: pnpm run docs
-
- # env:
- # PACKAGE: "!perspective-python,!perspective-jupyterlab"
-
- uses: actions/upload-artifact@v4
with:
name: perspective-js-dist
@@ -130,6 +180,7 @@ jobs:
# `-'
build_python:
runs-on: ${{ matrix.os }}
+ needs: [lint_and_docs]
container: ${{ matrix.container }}
strategy:
fail-fast: false
@@ -169,6 +220,11 @@ jobs:
id: config-step
uses: ./.github/actions/config
+ - uses: actions/download-artifact@v4
+ with:
+ name: perspective-metadata
+ path: rust/
+
- name: Initialize Build
id: init-step
uses: ./.github/actions/install-deps
@@ -178,16 +234,6 @@ jobs:
manylinux: ${{ matrix.container && 'true' || 'false' }}
skip_cache: ${{ steps.config-step.outputs.SKIP_CACHE }}
- - run: echo "/opt/python/cp39-cp39/bin" >> $GITHUB_PATH
- if: ${{ runner.os == 'Linux' }}
-
- # https://github.com/apache/arrow/issues/38391
- - if: ${{ runner.os == 'macOS' }}
- run: echo "MACOSX_DEPLOYMENT_TARGET=$(sw_vers -productVersion)" >> $GITHUB_ENV
-
- - run: echo "${{ steps.init-step.outputs.VCPKG_INSTALLATION_ROOT }}" >> $env:GITHUB_PATH
- if: ${{ runner.os == 'Windows' }}
-
- name: Python Build
run: pnpm run build
if: ${{ !contains(matrix.os, 'windows') }}
@@ -205,7 +251,6 @@ jobs:
env:
CARGO_TARGET_DIR: D:\psp-rust
PSP_ROOT_DIR: ${{ github.workspace }}
- VCPKG_ROOT: ${{ steps.init-step.outputs.VCPKG_INSTALLATION_ROOT }}
PACKAGE: "perspective-python"
PSP_ARCH: ${{ matrix.arch }}
PSP_BUILD_WHEEL: 1
@@ -235,6 +280,7 @@ jobs:
#
build_and_test_rust:
runs-on: ${{ matrix.os }}
+ needs: [lint_and_docs]
strategy:
fail-fast: false
matrix:
@@ -257,6 +303,11 @@ jobs:
id: config-step
uses: ./.github/actions/config
+ - uses: actions/download-artifact@v4
+ with:
+ name: perspective-metadata
+ path: rust/
+
- name: Initialize Build
id: init-step
uses: ./.github/actions/install-deps
@@ -266,9 +317,6 @@ jobs:
manylinux: "false"
skip_cache: ${{ steps.config-step.outputs.SKIP_CACHE }}
- - run: echo "${{ steps.init-step.outputs.VCPKG_INSTALLATION_ROOT }}" >> $env:GITHUB_PATH
- if: ${{ runner.os == 'Windows' }}
-
- name: Rust Build
run: pnpm run build
if: ${{ !contains(matrix.os, 'windows') }}
@@ -276,7 +324,7 @@ jobs:
PACKAGE: "perspective-rs"
PSP_ROOT_DIR: ${{ github.workspace }}
- - name: Python Build (Windows)
+ - name: Rust Build (Windows)
run: |
New-Item -ItemType Directory -Path $env:CARGO_TARGET_DIR -Force
pnpm run build
@@ -284,7 +332,6 @@ jobs:
env:
CARGO_TARGET_DIR: D:\psp-rust
PSP_ROOT_DIR: ${{ github.workspace }}
- VCPKG_ROOT: ${{ steps.init-step.outputs.VCPKG_INSTALLATION_ROOT }}
PACKAGE: "perspective-rs"
- name: Rust Test
@@ -302,7 +349,6 @@ jobs:
env:
CARGO_TARGET_DIR: D:\psp-rust
PSP_ROOT_DIR: ${{ github.workspace }}
- VCPKG_ROOT: ${{ steps.init-step.outputs.VCPKG_INSTALLATION_ROOT }}
PACKAGE: "perspective-rs"
- name: Package
@@ -323,6 +369,7 @@ jobs:
# `-'
build_emscripten_wheel:
runs-on: ${{ matrix.os }}
+ needs: [lint_and_docs]
# if: ${{ startsWith(github.ref, 'refs/tags/v') }}
strategy:
fail-fast: false
@@ -342,6 +389,11 @@ jobs:
id: config-step
uses: ./.github/actions/config
+ - uses: actions/download-artifact@v4
+ with:
+ name: perspective-metadata
+ path: rust/
+
- name: Initialize Build
id: init-step
uses: ./.github/actions/install-deps
@@ -470,6 +522,7 @@ jobs:
- name: Run Jupyter Tests
if: ${{ false }}
+ # if: ${{ runner.os == 'Linux' }}
run: |
jupyter lab --generate-config
pnpm run test --jupyter
@@ -509,6 +562,7 @@ jobs:
python: "false"
rust: "false"
cpp: "false"
+ playwright: "true"
skip_cache: ${{ steps.config-step.outputs.SKIP_CACHE }}
- uses: actions/download-artifact@v4
@@ -519,7 +573,7 @@ jobs:
- name: Run Tests
run: pnpm run test
env:
- PACKAGE: "!perspective-python,!perspective-jupyterlab"
+ PACKAGE: "perspective-cpp,perspective,perspective-viewer,perspective-viewer-datagrid,perspective-viewer-d3fc,perspective-viewer-openlayers,perspective-workspace,perspective-cli"
# PSP_USE_CCACHE: 1
# ,--,--' . .-,--. . .
@@ -704,6 +758,7 @@ jobs:
benchmark_python,
build_emscripten_wheel,
build_and_test_rust,
+ lint_and_docs,
]
if: startsWith(github.ref, 'refs/tags/v')
strategy:
@@ -767,10 +822,6 @@ jobs:
with:
name: perspective-rust
- # - uses: actions/download-artifact@v4
- # with:
- # name: perspective-python-benchmarks
-
- run: pnpm pack --pack-destination=../..
working-directory: ./rust/perspective-js
diff --git a/.gitignore b/.gitignore
index 924a7fba98..0a1e58ad72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -249,3 +249,4 @@ rust/perspective-python/*.data
# rust/perspective-python/perspective.data/data/share/jupyter/labextensions/@finos/perspective-jupyterlab/package.json
rust/perspective-viewer/docs/exprtk.md
rust/perspective-server/docs/lib_gen.md
+rust/perspective-client/docs/expression_gen.md
diff --git a/.prettierignore b/.prettierignore
index 2824129a97..f01a74e3a8 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -9,3 +9,5 @@ ts-rs/
rust/perspective-python/perspective/labextension/
rust/perspective-python/perspective.data/
rust/perspective-python/*/data
+expression_gen.md
+rust/perspective-viewer/docs/exprtk.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3f06a513fc..f9fdf02eaa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,12 +14,19 @@ When submitting or commenting on an Issue, please respect the following
guidelines. Github Issues are Perspective's project record of bugs and feature
development, e.g. for publishing a release's Changelog, and as such it is
important to keep them informative and on-topic. As such, please understand that
-we may remove or reclassify comments or Issues which violate the guidelines.
+we may remove or reclassify comments, Issues or PRs which violate the
+guidelines.
+
+Please note that due to the we may close your Issue or Pull Request for one of
+the reasons listed here or in the associated contribution template. If you find
+your contribution closed with a link to this document or a contribution
+template, please make sure you've followed the instructions closely before
+re-submitting.
- Be respectful and civil!
-- Use the provided Issue templates. If the templates don't fit your need,
- please open a [discussion](https://github.com/finos/perspective/discussions)
- instead.
+- Use the provided Issue and Pull Request templates. If the templates don't
+ fit your need, please open a
+ [discussion](https://github.com/finos/perspective/discussions) instead.
- Don't ask for issues to be assigned to you if you're a first-time
contributor. If you need help picking an issue to work on, please open a
[discussion](https://github.com/finos/perspective/discussions).
@@ -27,17 +34,25 @@ we may remove or reclassify comments or Issues which violate the guidelines.
issue fixed. The Issue will link any in-progress draft PRs or Milestones (if
known).
-When submitting Pull Request (PR), please respect the following coding
+When submitting a Pull Request (PR), please respect the following coding
guidelines:
+- Don't open a PR without an associated
+ [Open Issue](https://github.com/finos/perspective/issues) which has been
+ tagged by a project maintainer.
+- Make sure your PR passes _build_, _test_ and _lint_ steps _completely_
+ before opening a PR. Make _sure_ you've run these locally, even if you think
+ your change will not impact this step!
+- Don't open a PR for auto-generated, AI-assisted or otherwise inauthentic
+ contributions.
- Sign commits (e.g. with `-s`) in accordance with the DCO policy detailed
below, _before_ opening a PR.
-- Please make sure PRs include:
+- Please make sure PRs include the following _not optional_ components:
- Tests asserting behavior of any new or modified features.
- Docs for any new or modified public APIs.
- [Benchmarks](https://perspective.finos.org/docs/development/#benchmark)
- for any C++ changes.
+ for any performance-critical changes.
- Keep PRs clean, simple and to-the-point:
- Squash "WIP", "Reverting ..", etc., commits.
@@ -46,9 +61,6 @@ guidelines:
- Try to organize commits as functional components (as opposed to
timeline-of-development).
-Please note that non substantive changes, large changes without prior
-discussion, etc, are not accepted and pull requests may be closed.
-
## DCO
The Perspective project requires contributors to affirm their contributions via
diff --git a/Cargo.lock b/Cargo.lock
index 66f112ac81..a4bf4c0b41 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1880,6 +1880,17 @@ dependencies = [
"yew-fmt",
]
+[[package]]
+name = "perspective-metadata"
+version = "0.0.0"
+dependencies = [
+ "perspective-client",
+ "perspective-js",
+ "perspective-server",
+ "perspective-viewer",
+ "ts-rs",
+]
+
[[package]]
name = "perspective-python"
version = "3.1.0"
@@ -3125,10 +3136,11 @@ dependencies = [
[[package]]
name = "ts-rs"
-version = "9.0.1"
+version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c"
+checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9"
dependencies = [
+ "lazy_static",
"serde_json",
"thiserror",
"ts-rs-macros",
@@ -3136,9 +3148,9 @@ dependencies = [
[[package]]
name = "ts-rs-macros"
-version = "9.0.1"
+version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130"
+checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
"proc-macro2 1.0.83",
"quote 1.0.36",
diff --git a/Cargo.toml b/Cargo.toml
index 5ae7fed518..85c0f54580 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,18 +12,27 @@
[workspace]
resolver = "2"
+default-members = [
+ "rust/perspective",
+ "rust/perspective-client",
+ "rust/perspective-js",
+ "rust/perspective-python",
+ "rust/perspective-server",
+ "rust/perspective-viewer",
+]
members = [
"rust/lint",
+ "rust/generate-metadata",
"rust/bootstrap",
"rust/bootstrap-runtime",
- "rust/perspective-viewer",
"rust/bundle",
"rust/perspective",
"rust/perspective-client",
"rust/perspective-js",
"rust/perspective-python",
"rust/perspective-server",
- "examples/rust-axum",
+ "rust/perspective-viewer",
+ "examples/rust-axum",
]
[profile.dev]
diff --git a/docs/docs/development.md b/DEVELOPMENT.md
similarity index 97%
rename from docs/docs/development.md
rename to DEVELOPMENT.md
index c449bd7b4d..1a4ec4ea3a 100644
--- a/docs/docs/development.md
+++ b/DEVELOPMENT.md
@@ -1,11 +1,6 @@
----
-id: development
-title: Developer Guide
----
-
-Thank you for your interest in contributing to Perspective! This guide will
-teach you everything you need to know to get started hacking on the Perspective
-codebase.
+This guide will teach you everything you need to know to get started hacking on
+the Perspective codebase. Please see [`CONTRIBUTING.md`](CONTRIBUTING.md) for
+contribution guidelines.
If you're coming to this project as principally a JavaScript developer, please
be aware that Perspective is quite a bit more complex than a typical NPM package
diff --git a/README.md b/README.md
index 68e03d4b30..4333c57e1a 100644
--- a/README.md
+++ b/README.md
@@ -37,24 +37,31 @@ and/or [Jupyterlab](https://jupyterlab.readthedocs.io/en/stable/).
### Documentation
- [Project Site](https://perspective.finos.org/)
-- User Guides
- - [Javascript User Guide](https://perspective.finos.org/docs/js.html)
- - [Python User Guide](https://perspective.finos.org/docs/python.html)
- - [Developer Guide](https://perspective.finos.org/docs/development.html)
-- Concepts
- - [Table](https://perspective.finos.org/docs/table.html)
- - [View](https://perspective.finos.org/docs/view.html)
- - [Expression Columns](https://perspective.finos.org/docs/expressions.html)
- - [Data Binding](https://perspective.finos.org/docs/table.html)
-- API
- - [Perspective API](https://github.com/finos/perspective/blob/master/packages/perspective/README.md)
- - [Perspective Viewer API](https://perspective.finos.org/docs/obj/perspective-viewer/)
- - [Perspective Python API](https://perspective.finos.org/docs/obj/perspective-python.html)
+- JavaScript (NPM)
+ - [`@finos/perspective-viewer`, JavaScript UI API](https://docs.rs/perspective-viewer/latest/perspective_viewer/)
+ - [`@finos/perspective`, JavaScript Client/Server API](https://docs.rs/perspective-js/latest/perspective_js/)
+ - [`Table` API](https://docs.rs/perspective-js/latest/perspective_js/struct.Table.html)
+ - [`View` API](https://docs.rs/perspective-js/latest/perspective_js/struct.View.html)
+ - [Installation Guide](https://docs.rs/perspective-js/latest/perspective_js/#installation)
+- Python (PyPI)
+ - [`perspective-python`, Python Client/Server API](https://docs.rs/perspective-python/latest/perspective_python/)
+ - [`PerspectiveWidget` Jupyter Plugin](https://docs.rs/perspective-python/3.1.0/perspective_python/#perspectivewidget)
+ - [`Table` API](https://docs.rs/perspective-python/latest/perspective_python/struct.Table.html)
+ - [`View` API](https://docs.rs/perspective-python/latest/perspective_python/struct.View.html)
+- Rust (Crates.io)
+ - [`perspective`, Rust API](https://docs.rs/perspective-rs/latest/perspective_rs/)
+ - [`perspective-client`, Rust Client API](https://docs.rs/perspective-client/latest/perspective_client/)
+ - [`perspective-server`, Rust Server API](https://docs.rs/perspective-server/latest/perspective_server/)
+ - [`Table` API](https://docs.rs/perspective-client/latest/perspective_client/struct.Table.html)
+ - [`View` API](https://docs.rs/perspective-client/latest/perspective_client/struct.View.html)
+- Appendix
+ - [Data Binding](https://docs.rs/perspective-server/latest/perspective_server/)
+ - [Expression Columns](https://docs.rs/perspective-client/latest/perspective_client/config/expressions/)
### Examples
-
editable
file
fractal
market
raycasting
evictions
nypd
streaming
covid
webcam
movies
superstore
citibike
olympics
jupyterlab
+
editable
file
fractal
market
raycasting
evictions
nypd
streaming
covid
webcam
movies
superstore
citibike
olympics
### Media
@@ -65,22 +72,19 @@ and/or [Jupyterlab](https://jupyterlab.readthedocs.io/en/stable/).
+
+When passing data directly to the [`crate::Client::table`] constructor, the type
+of each column is inferred automatically. In some cases, the inference algorithm
+may not return exactly what you'd like. For example, a column may be interpreted
+as a `datetime` when you intended it to be a `string`, or a column may have no
+values at all (yet), as it will be updated with values from a real-time data
+source later on. In these cases, create a `table()` with a _schema_.
+
+Once the [`Table`] has been created with a schema, further `update()` calls will
+no longer perform type inference, so columns must only include values supported
+by the column's [`ColumnType`].
+
+## Data Formats
+
+A [`Table`] may also be created-or-updated by data in CSV,
+[Apache Arrow](https://arrow.apache.org/), JSON row-oriented or JSON
+column-oriented formats.
+
+
+
+In addition to these core formats, `perspective-python` additionally supports
+`pyarrow.Table` and `pandas.DataFrame` objects directly. These formats are
+otherwise identical to the built-in formats and don't exhibit any additional
+support or type-awareness; e.g., `pandas.DataFrame` support is _just_
+`pyarrow.Table.from_pandas` piped into Perspective's Arrow reader.
+
+
+
+[`crate::Client::table`] and [`Table::update`] perform _coercion_ on their input
+for all input formats _except_ Arrow (which comes with its own schema and has no
+need for coercion).
+
+`"date"` and `"datetime"` column types do not have native JSON representations,
+so these column types _cannot_ be inferred from JSON input. Instead, for columns
+of these types for JSON input, a [`Table`] must first be constructed with a
+_schema_. Next, call [`Table::update`] with the JSON input - Perspective's JSON
+reader may _coerce_ a `date` or `datetime` from these native JSON types:
+
+- `integer` as milliseconds-since-epoch.
+- `string` as a any of Perspective's built-in date format formats.
+- JavaScript `Date` and Python `datetime.date` and `datetime.datetime` are
+ _not_ supported directly. However, in JavaScript `Date` types are
+ automatically coerced to correct `integer` timestamps by default when
+ converted to JSON.
+
+For CSV input types, Perspective relies on Apache Arrow's CSV parser, and as
+such uses the same column-type inference logic as Arrow itself.
+
+## Index and Limit
+
+Initializing a [`Table`] with an `index` tells Perspective to treat a column as
+the primary key, allowing in-place updates of rows. Only a single column (of any
+type) can be used as an `index`. Indexed [`Table`] instances allow:
+
+- In-place _updates_ whenever a new row shares an `index` values with an
+ existing row
+- _Partial updates_ when a data batch omits some column.
+- _Removes_ to delete a row by `index`.
+
+To create an indexed `Table`, provide the `index` property with a string column
+name to be used as an index:
+
+
+
+Initializing a [`Table`] with a `limit` sets the total number of rows the
+[`Table`] is allowed to have. When the [`Table`] is updated, and the resulting
+size of the [`Table`] would exceed its `limit`, rows that exceed `limit`
+overwrite the oldest rows in the [`Table`]. To create a [`Table`] with a
+`limit`, provide the `limit` property with an integer indicating the maximum
+rows:
+
+
+
+## [`Table::update`] and [`Table::remove`]
+
+Once a [`Table`] has been created, it can be updated with new data conforming to
+the [`Table`]'s schema. [`Table::update`] supports the same data formats as
+[`crate::Client::table`], minus _schema_.
+
+
+
+Without an `index` set, calls to `update()` _append_ new data to the end of the
+`Table`. Otherwise, Perspective allows
+[_partial updates_ (in-place)](#index-and-limit) using the `index` to determine
+which rows to update:
+
+
+
+Any value on a [`Client::table`] can be unset using the value `null` in JSON or
+Arrow input formats. Values may be unset on construction, as any `null` in the
+dataset will be treated as an unset value. [`Table::update`] calls do not need
+to provide _all columns_ in the [`Table`]'s schema; missing columns will be
+omitted from the [`Table`]'s updated rows.
+
+
+
+# [`Table::clear`] and [`Table::replace`]
+
+Calling [`Table::clear`] will remove all data from the underlying [`Table`].
+Calling [`Table::replace`] with new data will clear the [`Table`], and update it
+with a new dataset that conforms to Perspective's data types and the existing
+schema on the `Table`.
+
+
`limit` cannot be used in conjunction with `index`.
diff --git a/rust/perspective-client/docs/table/remove_delete.md b/rust/perspective-client/docs/table/remove_delete.md
index 2120217a0f..789807c8fc 100644
--- a/rust/perspective-client/docs/table/remove_delete.md
+++ b/rust/perspective-client/docs/table/remove_delete.md
@@ -1 +1,2 @@
-Removes a listener with a given ID, as returned by a previous call to [`Table::on_delete`].
+Removes a listener with a given ID, as returned by a previous call to
+[`Table::on_delete`].
diff --git a/rust/perspective-client/docs/view.md b/rust/perspective-client/docs/view.md
index dd8b6d67a9..746967f4d6 100644
--- a/rust/perspective-client/docs/view.md
+++ b/rust/perspective-client/docs/view.md
@@ -2,10 +2,30 @@ The [`View`] struct is Perspective's query and serialization interface. It
represents a query on the `Table`'s dataset and is always created from an
existing `Table` instance via the [`Table::view`] method.
+[`View`]s are immutable with respect to the arguments provided to the
+[`Table::view`] method; to change these parameters, you must create a new
+[`View`] on the same [`Table`]. However, each [`View`] is _live_ with respect to
+the [`Table`]'s data, and will (within a conflation window) update with the
+latest state as its parent [`Table`] updates, including incrementally
+recalculating all aggregates, pivots, filters, etc. [`View`] query parameters
+are composable, in that each parameter works independently _and_ in conjunction
+with each other, and there is no limit to the number of pivots, filters, etc.
+which can be applied.
+
The examples in this module are in JavaScript. See perspective docs for the Rust API.
+
+
+
+The examples in this module are in Python. See perspective docs for the Rust API.
+
-[`View`]s are immutable with respect to the arguments provided to the
-[`Table::view`] method; to change these parameters, you must create a new
-[`View`] on the same [`Table`]. However, each [`View`] is _live_ with respect to
-the [`Table`]'s data, and will (within a conflation window) update with the
-latest state as its parent [`Table`] updates, including incrementally
-recalculating all aggregates, pivots, filters, etc. [`View`] query parameters
-are composable, in that each parameter works independently _and_ in conjunction
-with each other, and there is no limit to the number of pivots, filters, etc.
-which can be applied.
-
-# Querying data with [`Table::view`]
+## Querying data with [`Table::view`]
To query the table, create a [`Table::view`] on the table instance with an
optional configuration object. A [`Table`] can have as many [`View`]s associated
@@ -111,7 +118,7 @@ let view = table
-## Group By
+### Group By
A group by _groups_ the dataset by the unique values of each column used as a
group by - a close analogue in SQL to the `GROUP BY` statement. The underlying
@@ -124,8 +131,6 @@ string column names to pivot, are applied in the order provided; For example, a
group by of `["State", "City", "Postal Code"]` shows the values for each Postal
Code, which are grouped by City, which are in turn grouped by State.
-#### Example
-
```javascript
@@ -151,7 +156,7 @@ let view = table.view(Some(ViewConfigUpdate {
-## Split By
+### Split By
A split by _splits_ the dataset by the unique values of each column used as a
split by. The underlying dataset is not aggregated, and a new column is created
@@ -161,8 +166,6 @@ has `["State"]` as its split by will have a new column for each state. In
Perspective, Split By are represented as an array of string column names to
pivot:
-#### Example
-
```javascript
@@ -188,7 +191,7 @@ let view = table.view(Some(ViewConfigUpdate {
-## Aggregates
+### Aggregates
Aggregates perform a calculation over an entire column, and are displayed when
one or more [Group By](#group-by) are applied to the `View`. Aggregates can be
@@ -202,8 +205,6 @@ Perspective provides a selection of aggregate functions that can be applied to
columns in the `View` constructor using a dictionary of column name to aggregate
function name.
-#### Example
-
-## Columns
+### Columns
The `columns` property specifies which columns should be included in the
`View`'s output. This allows users to show or hide a specific subset of columns,
as well as control the order in which columns appear to the user. This is
represented in Perspective as an array of string column names:
-#### Example
-
-## Sort
+### Sort
The `sort` property specifies columns on which the query should be sorted,
analogous to `ORDER BY` in SQL. A column can be sorted regardless of its data
@@ -271,8 +270,6 @@ being a string column name and a string sort direction. When `column-pivots` are
applied, the additional sort directions `"col asc"` and `"col desc"` will
determine the order of pivot columns groups.
-#### Example
-
-## Filter
+### Filter
The `filter` property specifies columns on which the query can be filtered,
returning rows that pass the specified filter condition. This is analogous to
@@ -302,8 +299,6 @@ Perspective represents `filter` as an array of arrays, with the values of each
inner array being a string column name, a string filter operator, and a filter
operand in the type of the column:
-#### Example
-
-## Expressions
+### Expressions
The `expressions` property specifies _new_ columns in Perspective that are
created using existing column values or arbitary scalar values defined within
@@ -333,8 +328,6 @@ Column" button in the side panel.
A custom name can be added to an expression by making the first line a comment:
-#### Example
-
```javascript
@@ -364,8 +357,6 @@ all future updates that affect the [`Table::view`] will be forwarded to the new
[Client/Server Replicated](server.md#clientserver-replicated) design, by
serializing the `View` to an arrow and setting up an `on_update` callback.
-# Example
-
```javascript
diff --git a/rust/perspective-client/docs/view/column_paths.md b/rust/perspective-client/docs/view/column_paths.md
index 44b7c76c70..b48bef0a51 100644
--- a/rust/perspective-client/docs/view/column_paths.md
+++ b/rust/perspective-client/docs/view/column_paths.md
@@ -1,3 +1,5 @@
-Returns an array of strings containing the column paths of the View without any of the source columns.
+Returns an array of strings containing the column paths of the View without any
+of the source columns.
-A column path shows the columns that a given cell belongs to after pivots are applied.
+A column path shows the columns that a given cell belongs to after pivots are
+applied.
diff --git a/rust/perspective-client/docs/view/delete.md b/rust/perspective-client/docs/view/delete.md
index 21c29d73ad..ea9d1c5783 100644
--- a/rust/perspective-client/docs/view/delete.md
+++ b/rust/perspective-client/docs/view/delete.md
@@ -1 +1,3 @@
-Delete this [`View`] and clean up all resources associated with it. View objects do not stop consuming resources or processing updates when they are garbage collected - you must call this method to reclaim these.
+Delete this [`View`] and clean up all resources associated with it. View objects
+do not stop consuming resources or processing updates when they are garbage
+collected - you must call this method to reclaim these.
diff --git a/rust/perspective-client/package.json b/rust/perspective-client/package.json
new file mode 100644
index 0000000000..fa0c0e0ef0
--- /dev/null
+++ b/rust/perspective-client/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@finos/perspective-client",
+ "version": "3.1.0",
+ "description": "",
+ "private": true,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/finos/perspective"
+ },
+ "type": "module",
+ "license": "Apache-2.0",
+ "scripts": {
+ "clean": "rimraf src/rust/proto.rs"
+ },
+ "devDependencies": {
+ "rimraf": "^6"
+ }
+}
diff --git a/rust/perspective-client/src/rust/client.rs b/rust/perspective-client/src/rust/client.rs
index 57804a0dad..d6aee3eddd 100644
--- a/rust/perspective-client/src/rust/client.rs
+++ b/rust/perspective-client/src/rust/client.rs
@@ -34,6 +34,7 @@ use crate::table_data::{TableData, UpdateData};
use crate::utils::*;
use crate::view::ViewWindow;
+/// Metadata about the engine runtime (such as total heap utilization).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SystemInfo {
pub heap_size: f64,
@@ -144,7 +145,7 @@ impl Client {
pub async fn handle_response<'a>(&'a self, msg: &'a [u8]) -> ClientResult {
let msg = Response::decode(msg)?;
tracing::debug!("RECV {}", msg);
- let mut wr = self.subscriptions_once.try_write().unwrap();
+ let mut wr = self.subscriptions_once.write().await;
if let Some(handler) = (*wr).remove(&msg.msg_id) {
drop(wr);
handler(msg)?;
@@ -180,11 +181,11 @@ impl Client {
.fetch_add(1, std::sync::atomic::Ordering::Acquire)
}
- pub(crate) fn unsubscribe(&self, update_id: u32) -> ClientResult<()> {
+ pub(crate) async fn unsubscribe(&self, update_id: u32) -> ClientResult<()> {
let callback = self
.subscriptions
- .try_write()
- .unwrap()
+ .write()
+ .await
.remove(&update_id)
.ok_or(ClientError::Unknown("remove_update".to_string()))?;
@@ -199,12 +200,17 @@ impl Client {
on_update: Box ClientResult<()> + Send + Sync + 'static>,
) -> ClientResult<()> {
self.subscriptions_once
- .try_write()
- .unwrap()
+ .write()
+ .await
.insert(msg.msg_id, on_update);
tracing::debug!("SEND {}", msg);
- Ok((self.send)(msg).await?)
+ if let Err(e) = (self.send)(msg).await {
+ self.subscriptions_once.write().await.remove(&msg.msg_id);
+ Err(e.into())
+ } else {
+ Ok(())
+ }
}
pub(crate) async fn subscribe(
@@ -213,11 +219,16 @@ impl Client {
on_update: BoxFn>>,
) -> ClientResult<()> {
self.subscriptions
- .try_write()
- .unwrap()
+ .write()
+ .await
.insert(msg.msg_id, on_update);
tracing::debug!("SEND {}", msg);
- Ok((self.send)(msg).await?)
+ if let Err(e) = (self.send)(msg).await {
+ self.subscriptions.write().await.remove(&msg.msg_id);
+ Err(e.into())
+ } else {
+ Ok(())
+ }
}
/// Send a `ClientReq` and await both the successful completion of the
diff --git a/rust/perspective-client/src/rust/config/expressions.rs b/rust/perspective-client/src/rust/config/expressions.rs
index d90d8ffe64..db9603c8ed 100644
--- a/rust/perspective-client/src/rust/config/expressions.rs
+++ b/rust/perspective-client/src/rust/config/expressions.rs
@@ -10,6 +10,9 @@
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+#![doc = include_str!("../../../docs/expressions.md")]
+#![cfg_attr(not(feature = "omit_metadata"), doc = include_str!("../../../docs/expression_gen.md"))]
+
use std::borrow::Cow;
use std::collections::HashMap;
@@ -102,3 +105,415 @@ impl Expressions {
);
}
}
+
+#[doc(hidden)]
+#[derive(Serialize, Clone, Copy)]
+pub struct CompletionItemSuggestion {
+ pub label: &'static str,
+ pub insert_text: &'static str,
+ pub documentation: &'static str,
+}
+
+#[doc(hidden)]
+pub static COMPLETIONS: [CompletionItemSuggestion; 77] = [
+ CompletionItemSuggestion {
+ label: "var",
+ insert_text: "var ${1:x := 1}",
+ documentation: "Declare a new local variable",
+ },
+ CompletionItemSuggestion {
+ label: "abs",
+ insert_text: "abs(${1:x})",
+ documentation: "Absolute value of x",
+ },
+ CompletionItemSuggestion {
+ label: "avg",
+ insert_text: "avg(${1:x})",
+ documentation: "Average of all inputs",
+ },
+ CompletionItemSuggestion {
+ label: "bucket",
+ insert_text: "bucket(${1:x}, ${2:y})",
+ documentation: "Bucket x by y",
+ },
+ CompletionItemSuggestion {
+ label: "ceil",
+ insert_text: "ceil(${1:x})",
+ documentation: "Smallest integer >= x",
+ },
+ CompletionItemSuggestion {
+ label: "exp",
+ insert_text: "exp(${1:x})",
+ documentation: "Natural exponent of x (e ^ x)",
+ },
+ CompletionItemSuggestion {
+ label: "floor",
+ insert_text: "floor(${1:x})",
+ documentation: "Largest integer <= x",
+ },
+ CompletionItemSuggestion {
+ label: "frac",
+ insert_text: "frac(${1:x})",
+ documentation: "Fractional portion (after the decimal) of x",
+ },
+ CompletionItemSuggestion {
+ label: "iclamp",
+ insert_text: "iclamp(${1:x})",
+ documentation: "Inverse clamp x within a range",
+ },
+ CompletionItemSuggestion {
+ label: "inrange",
+ insert_text: "inrange(${1:x})",
+ documentation: "Returns whether x is within a range",
+ },
+ CompletionItemSuggestion {
+ label: "log",
+ insert_text: "log(${1:x})",
+ documentation: "Natural log of x",
+ },
+ CompletionItemSuggestion {
+ label: "log10",
+ insert_text: "log10(${1:x})",
+ documentation: "Base 10 log of x",
+ },
+ CompletionItemSuggestion {
+ label: "log1p",
+ insert_text: "log1p(${1:x})",
+ documentation: "Natural log of 1 + x where x is very small",
+ },
+ CompletionItemSuggestion {
+ label: "log2",
+ insert_text: "log2(${1:x})",
+ documentation: "Base 2 log of x",
+ },
+ CompletionItemSuggestion {
+ label: "logn",
+ insert_text: "logn(${1:x}, ${2:N})",
+ documentation: "Base N log of x where N >= 0",
+ },
+ CompletionItemSuggestion {
+ label: "max",
+ insert_text: "max(${1:x})",
+ documentation: "Maximum value of all inputs",
+ },
+ CompletionItemSuggestion {
+ label: "min",
+ insert_text: "min(${1:x})",
+ documentation: "Minimum value of all inputs",
+ },
+ CompletionItemSuggestion {
+ label: "mul",
+ insert_text: "mul(${1:x})",
+ documentation: "Product of all inputs",
+ },
+ CompletionItemSuggestion {
+ label: "percent_of",
+ insert_text: "percent_of(${1:x})",
+ documentation: "Percent y of x",
+ },
+ CompletionItemSuggestion {
+ label: "pow",
+ insert_text: "pow(${1:x}, ${2:y})",
+ documentation: "x to the power of y",
+ },
+ CompletionItemSuggestion {
+ label: "root",
+ insert_text: "root(${1:x}, ${2:N})",
+ documentation: "N-th root of x where N >= 0",
+ },
+ CompletionItemSuggestion {
+ label: "round",
+ insert_text: "round(${1:x})",
+ documentation: "Round x to the nearest integer",
+ },
+ CompletionItemSuggestion {
+ label: "sgn",
+ insert_text: "sgn(${1:x})",
+ documentation: "Sign of x: -1, 1, or 0",
+ },
+ CompletionItemSuggestion {
+ label: "sqrt",
+ insert_text: "sqrt(${1:x})",
+ documentation: "Square root of x",
+ },
+ CompletionItemSuggestion {
+ label: "sum",
+ insert_text: "sum(${1:x})",
+ documentation: "Sum of all inputs",
+ },
+ CompletionItemSuggestion {
+ label: "trunc",
+ insert_text: "trunc(${1:x})",
+ documentation: "Integer portion of x",
+ },
+ CompletionItemSuggestion {
+ label: "acos",
+ insert_text: "acos(${1:x})",
+ documentation: "Arc cosine of x in radians",
+ },
+ CompletionItemSuggestion {
+ label: "acosh",
+ insert_text: "acosh(${1:x})",
+ documentation: "Inverse hyperbolic cosine of x in radians",
+ },
+ CompletionItemSuggestion {
+ label: "asin",
+ insert_text: "asin(${1:x})",
+ documentation: "Arc sine of x in radians",
+ },
+ CompletionItemSuggestion {
+ label: "asinh",
+ insert_text: "asinh(${1:x})",
+ documentation: "Inverse hyperbolic sine of x in radians",
+ },
+ CompletionItemSuggestion {
+ label: "atan",
+ insert_text: "atan(${1:x})",
+ documentation: "Arc tangent of x in radians",
+ },
+ CompletionItemSuggestion {
+ label: "atanh",
+ insert_text: "atanh(${1:x})",
+ documentation: "Inverse hyperbolic tangent of x in radians",
+ },
+ CompletionItemSuggestion {
+ label: "cos",
+ insert_text: "cos(${1:x})",
+ documentation: "Cosine of x",
+ },
+ CompletionItemSuggestion {
+ label: "cosh",
+ insert_text: "cosh(${1:x})",
+ documentation: "Hyperbolic cosine of x",
+ },
+ CompletionItemSuggestion {
+ label: "cot",
+ insert_text: "cot(${1:x})",
+ documentation: "Cotangent of x",
+ },
+ CompletionItemSuggestion {
+ label: "sin",
+ insert_text: "sin(${1:x})",
+ documentation: "Sine of x",
+ },
+ CompletionItemSuggestion {
+ label: "sinc",
+ insert_text: "sinc(${1:x})",
+ documentation: "Sine cardinal of x",
+ },
+ CompletionItemSuggestion {
+ label: "sinh",
+ insert_text: "sinh(${1:x})",
+ documentation: "Hyperbolic sine of x",
+ },
+ CompletionItemSuggestion {
+ label: "tan",
+ insert_text: "tan(${1:x})",
+ documentation: "Tangent of x",
+ },
+ CompletionItemSuggestion {
+ label: "tanh",
+ insert_text: "tanh(${1:x})",
+ documentation: "Hyperbolic tangent of x",
+ },
+ CompletionItemSuggestion {
+ label: "deg2rad",
+ insert_text: "deg2rad(${1:x})",
+ documentation: "Convert x from degrees to radians",
+ },
+ CompletionItemSuggestion {
+ label: "deg2grad",
+ insert_text: "deg2grad(${1:x})",
+ documentation: "Convert x from degrees to gradians",
+ },
+ CompletionItemSuggestion {
+ label: "rad2deg",
+ insert_text: "rad2deg(${1:x})",
+ documentation: "Convert x from radians to degrees",
+ },
+ CompletionItemSuggestion {
+ label: "grad2deg",
+ insert_text: "grad2deg(${1:x})",
+ documentation: "Convert x from gradians to degrees",
+ },
+ CompletionItemSuggestion {
+ label: "concat",
+ insert_text: "concat(${1:x}, ${2:y})",
+ documentation: "Concatenate string columns and string literals, such \
+ as:\nconcat(\"State\" ', ', \"City\")",
+ },
+ CompletionItemSuggestion {
+ label: "order",
+ insert_text: "order(${1:input column}, ${2:value}, ...)",
+ documentation: "Generates a sort order for a string column based on the input order of \
+ the parameters, such as:\norder(\"State\", 'Texas', 'New York')",
+ },
+ CompletionItemSuggestion {
+ label: "upper",
+ insert_text: "upper(${1:x})",
+ documentation: "Uppercase of x",
+ },
+ CompletionItemSuggestion {
+ label: "lower",
+ insert_text: "lower(${1:x})",
+ documentation: "Lowercase of x",
+ },
+ CompletionItemSuggestion {
+ label: "hour_of_day",
+ insert_text: "hour_of_day(${1:x})",
+ documentation: "Return a datetime's hour of the day as a string",
+ },
+ CompletionItemSuggestion {
+ label: "month_of_year",
+ insert_text: "month_of_year(${1:x})",
+ documentation: "Return a datetime's month of the year as a string",
+ },
+ CompletionItemSuggestion {
+ label: "day_of_week",
+ insert_text: "day_of_week(${1:x})",
+ documentation: "Return a datetime's day of week as a string",
+ },
+ CompletionItemSuggestion {
+ label: "now",
+ insert_text: "now()",
+ documentation: "The current datetime in local time",
+ },
+ CompletionItemSuggestion {
+ label: "today",
+ insert_text: "today()",
+ documentation: "The current date in local time",
+ },
+ CompletionItemSuggestion {
+ label: "is_null",
+ insert_text: "is_null(${1:x})",
+ documentation: "Whether x is a null value",
+ },
+ CompletionItemSuggestion {
+ label: "is_not_null",
+ insert_text: "is_not_null(${1:x})",
+ documentation: "Whether x is not a null value",
+ },
+ CompletionItemSuggestion {
+ label: "not",
+ insert_text: "not(${1:x})",
+ documentation: "not x",
+ },
+ CompletionItemSuggestion {
+ label: "true",
+ insert_text: "true",
+ documentation: "Boolean value true",
+ },
+ CompletionItemSuggestion {
+ label: "false",
+ insert_text: "false",
+ documentation: "Boolean value false",
+ },
+ CompletionItemSuggestion {
+ label: "if",
+ insert_text: "if (${1:condition}) {} else if (${2:condition}) {} else {}",
+ documentation: "An if/else conditional, which evaluates a condition such as:\n if \
+ (\"Sales\" > 100) { true } else { false }",
+ },
+ CompletionItemSuggestion {
+ label: "for",
+ insert_text: "for (${1:expression}) {}",
+ documentation: "A for loop, which repeatedly evaluates an incrementing expression such \
+ as:\nvar x := 0; var y := 1; for (x < 10; x += 1) { y := x + y }",
+ },
+ CompletionItemSuggestion {
+ label: "string",
+ insert_text: "string(${1:x})",
+ documentation: "Converts the given argument to a string",
+ },
+ CompletionItemSuggestion {
+ label: "integer",
+ insert_text: "integer(${1:x})",
+ documentation: "Converts the given argument to a 32-bit integer. If the result \
+ over/under-flows, null is returned",
+ },
+ CompletionItemSuggestion {
+ label: "float",
+ insert_text: "float(${1:x})",
+ documentation: "Converts the argument to a float",
+ },
+ CompletionItemSuggestion {
+ label: "date",
+ insert_text: "date(${1:year}, ${1:month}, ${1:day})",
+ documentation: "Given a year, month (1-12) and day, create a new date",
+ },
+ CompletionItemSuggestion {
+ label: "datetime",
+ insert_text: "datetime(${1:timestamp})",
+ documentation: "Given a POSIX timestamp of milliseconds since epoch, create a new datetime",
+ },
+ CompletionItemSuggestion {
+ label: "boolean",
+ insert_text: "boolean(${1:x})",
+ documentation: "Converts the given argument to a boolean",
+ },
+ CompletionItemSuggestion {
+ label: "random",
+ insert_text: "random()",
+ documentation: "Returns a random float between 0 and 1, inclusive.",
+ },
+ CompletionItemSuggestion {
+ label: "match",
+ insert_text: "match(${1:string}, ${2:pattern})",
+ documentation: "Returns True if any part of string matches pattern, and False otherwise.",
+ },
+ CompletionItemSuggestion {
+ label: "match_all",
+ insert_text: "match_all(${1:string}, ${2:pattern})",
+ documentation: "Returns True if the whole string matches pattern, and False otherwise.",
+ },
+ CompletionItemSuggestion {
+ label: "search",
+ insert_text: "search(${1:string}, ${2:pattern})",
+ documentation: "Returns the substring that matches the first capturing group in pattern, \
+ or null if there are no capturing groups in the pattern or if there are \
+ no matches.",
+ },
+ CompletionItemSuggestion {
+ label: "indexof",
+ insert_text: "indexof(${1:string}, ${2:pattern}, ${3:output_vector})",
+ documentation: "Writes into index 0 and 1 of output_vector the start and end indices of \
+ the substring that matches the first capturing group in \
+ pattern.\n\nReturns true if there is a match and output was written, or \
+ false if there are no capturing groups in the pattern, if there are no \
+ matches, or if the indices are invalid.",
+ },
+ CompletionItemSuggestion {
+ label: "substring",
+ insert_text: "substring(${1:string}, ${2:start_idx}, ${3:length})",
+ documentation: "Returns a substring of string from start_idx with the given length. If \
+ length is not passed in, returns substring from start_idx to the end of \
+ the string. Returns null if the string or any indices are invalid.",
+ },
+ CompletionItemSuggestion {
+ label: "replace",
+ insert_text: "replace(${1:string}, ${2:pattern}, ${3:replacer})",
+ documentation: "Replaces the first match of pattern in string with replacer, or return \
+ the original string if no replaces were made.",
+ },
+ CompletionItemSuggestion {
+ label: "replace_all",
+ insert_text: "replace(${1:string}, ${2:pattern}, ${3:replacer})",
+ documentation: "Replaces all non-overlapping matches of pattern in string with replacer, \
+ or return the original string if no replaces were made.",
+ },
+ CompletionItemSuggestion {
+ label: "index",
+ insert_text: "index()",
+ documentation: "Looks up the index value of the current row",
+ },
+ CompletionItemSuggestion {
+ label: "col",
+ insert_text: "col(${1:string})",
+ documentation: "Looks up a column value by name",
+ },
+ CompletionItemSuggestion {
+ label: "vlookup",
+ insert_text: "vlookup(${1:string}, ${2:uint64})",
+ documentation: "Looks up a value in another column by index",
+ },
+];
diff --git a/rust/perspective-client/src/rust/config/filters.rs b/rust/perspective-client/src/rust/config/filters.rs
index 9a5d9659e8..8f32ee6a22 100644
--- a/rust/perspective-client/src/rust/config/filters.rs
+++ b/rust/perspective-client/src/rust/config/filters.rs
@@ -19,17 +19,18 @@ use ts_rs::TS;
use crate::proto;
use crate::proto::scalar;
+/// This type represents the ViewConfig serializable type, which must be JSON
+/// safe.
#[derive(Clone, Deserialize, Debug, PartialEq, Serialize, TS)]
#[serde(untagged)]
pub enum Scalar {
Float(f64),
String(String),
Bool(bool),
- DateTime(f64),
+ // DateTime(i64),
+ // Date(String),
+ // Int(i32),
Null,
- // // Can only have one u64 representation ...
- // Date(u64)
- // Int(u32)
}
impl From<&str> for Scalar {
@@ -50,7 +51,6 @@ impl Display for Scalar {
Self::Float(x) => write!(fmt, "{}", x),
Self::String(x) => write!(fmt, "{}", x),
Self::Bool(x) => write!(fmt, "{}", x),
- Self::DateTime(x) => write!(fmt, "{}", x),
Self::Null => write!(fmt, ""),
}
}
@@ -159,10 +159,6 @@ impl From for proto::Scalar {
Scalar::Bool(x) => proto::Scalar {
scalar: Some(scalar::Scalar::Bool(x)),
},
- // Scalar::Date(_) => todo!(),
- Scalar::DateTime(x) => proto::Scalar {
- scalar: Some(scalar::Scalar::Datetime(x as i64)),
- },
Scalar::Null => proto::Scalar {
scalar: Some(scalar::Scalar::Null(0)),
},
@@ -175,10 +171,7 @@ impl From for Scalar {
match value.scalar {
Some(scalar::Scalar::Bool(x)) => Scalar::Bool(x),
Some(scalar::Scalar::String(x)) => Scalar::String(x),
- Some(scalar::Scalar::Int(x)) => Scalar::Float(x as f64),
- Some(scalar::Scalar::Date(x)) => Scalar::DateTime(x as f64),
Some(scalar::Scalar::Float(x)) => Scalar::Float(x),
- Some(scalar::Scalar::Datetime(x)) => Scalar::DateTime(x as f64),
Some(scalar::Scalar::Null(_)) => Scalar::Null,
None => Scalar::Null,
}
diff --git a/rust/perspective-client/src/rust/config/mod.rs b/rust/perspective-client/src/rust/config/mod.rs
index 3f3417a5ef..60857cdcd4 100644
--- a/rust/perspective-client/src/rust/config/mod.rs
+++ b/rust/perspective-client/src/rust/config/mod.rs
@@ -10,12 +10,13 @@
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
-//! A collection of (de-)serializable structs which capture the application
-//! state, suitable for persistence, history, etc. features.
+//! A collection of [`serde::Serialize`]/[`serde::Deserialize`] structs which
+//! capture the application state, suitable for persistence, history, etc.
+//! features.
mod aggregates;
mod column_type;
-mod expressions;
+pub mod expressions;
mod filters;
mod plugin;
mod sort;
diff --git a/rust/perspective-client/src/rust/config/view_config.rs b/rust/perspective-client/src/rust/config/view_config.rs
index 2ef8655a12..ad8338f7b3 100644
--- a/rust/perspective-client/src/rust/config/view_config.rs
+++ b/rust/perspective-client/src/rust/config/view_config.rs
@@ -102,7 +102,7 @@ pub struct ViewConfigUpdate {
#[ts(optional)]
pub aggregates: Option>,
- #[serde(skip_serializing_if = "Option::is_none")]
+ #[serde(skip_serializing)]
#[serde(default)]
#[ts(optional)]
pub group_by_depth: Option,
diff --git a/rust/perspective-client/src/rust/lib.rs b/rust/perspective-client/src/rust/lib.rs
index e5c6d81db0..f3eb48c54b 100644
--- a/rust/perspective-client/src/rust/lib.rs
+++ b/rust/perspective-client/src/rust/lib.rs
@@ -23,10 +23,14 @@ mod table_data;
mod view;
pub mod config;
-pub mod proto;
+
+#[allow(unknown_lints)]
+#[allow(clippy::all)]
+mod proto;
pub mod utils;
pub use crate::client::{Client, ClientHandler, Features, SystemInfo};
+pub use crate::proto::{ColumnType, SortOp, ViewOnUpdateResp};
pub use crate::session::{ProxySession, Session};
pub use crate::table::{
Schema, Table, TableInitOptions, TableReadFormat, UpdateOptions, ValidateExpressionsData,
@@ -35,7 +39,6 @@ pub use crate::table_data::{TableData, UpdateData};
pub use crate::view::{OnUpdateMode, OnUpdateOptions, View, ViewWindow};
pub type ClientError = utils::ClientError;
-pub type ColumnType = proto::ColumnType;
pub type ExprValidationError = crate::proto::table_validate_expr_resp::ExprValidationError;
#[doc(hidden)]
diff --git a/rust/perspective-client/src/rust/utils/mod.rs b/rust/perspective-client/src/rust/utils/mod.rs
index ad6d91f57a..8445bad9d1 100644
--- a/rust/perspective-client/src/rust/utils/mod.rs
+++ b/rust/perspective-client/src/rust/utils/mod.rs
@@ -10,6 +10,8 @@
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+//! Utility functions that are common to the Perspective crates.
+
mod clone;
mod logging;
diff --git a/rust/perspective-client/src/rust/view.rs b/rust/perspective-client/src/rust/view.rs
index 2295d106a5..264340aef6 100644
--- a/rust/perspective-client/src/rust/view.rs
+++ b/rust/perspective-client/src/rust/view.rs
@@ -102,6 +102,7 @@ impl From for ViewPort {
}
}
+#[doc = include_str!("../../docs/view.md")]
#[derive(Clone, Debug)]
pub struct View {
pub name: String,
@@ -312,7 +313,7 @@ impl View {
id: update_id,
}));
- self.client.unsubscribe(update_id)?;
+ self.client.unsubscribe(update_id).await?;
match self.client.oneshot(&msg).await? {
ClientResp::ViewRemoveOnUpdateResp(_) => Ok(()),
resp => Err(resp.into()),
diff --git a/rust/perspective-js/Cargo.toml b/rust/perspective-js/Cargo.toml
index b2116f7811..3a45e49fcf 100644
--- a/rust/perspective-js/Cargo.toml
+++ b/rust/perspective-js/Cargo.toml
@@ -26,10 +26,6 @@ include = ["src/**/*", "Cargo.toml", "./package.json", "docs/**/*", "build.rs"]
rustc-args = ["--cfg", "web_sys_unstable_apis"]
rustdoc-args = ["--html-in-header", "docs/index.html"]
-[[bin]]
-name = "perspective-js-metadata"
-path = "src/rust/main.rs"
-
[lib]
crate-type = ["cdylib", "rlib"]
path = "src/rust/lib.rs"
@@ -65,7 +61,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_bytes = "0.11"
serde_json = { version = "1.0.107", features = ["raw_value"] }
serde-wasm-bindgen = "0.6.0"
-ts-rs = { version = "9.0.1", features = [
+ts-rs = { version = "10.0.0", features = [
"serde-json-impl",
"no-serde-warnings",
] }
diff --git a/rust/perspective-js/build.js b/rust/perspective-js/build.js
index 20ba7e9240..2f47fdc3c2 100644
--- a/rust/perspective-js/build.js
+++ b/rust/perspective-js/build.js
@@ -68,22 +68,10 @@ function get_host() {
return /host\: (.+?)$/gm.exec(execSync(`rustc -vV`).toString())[1];
}
-// Rust compile-time metadata
-function build_metadata() {
- execSync(
- `PSP_ROOT_DIR=../.. cargo build -p perspective-js --bin perspective-js-metadata --target=${get_host()} --features=external-cpp`,
- INHERIT
- );
-
- execSync(
- `TS_RS_EXPORT_DIR='./src/ts/ts-rs' ../target/${get_host()}/debug/perspective-js-metadata`
- );
-}
-
function build_rust() {
const release_flag = IS_DEBUG ? "" : "--release";
execSync(
- `PSP_ROOT_DIR=../.. cargo bundle --target=${get_host()} -- perspective_js ${release_flag} --features=export-init,external-cpp`,
+ `PSP_ROOT_DIR=../.. cargo bundle --target=${get_host()} -- perspective_js ${release_flag} --features=export-init`,
INHERIT
);
}
@@ -96,7 +84,6 @@ async function build_web_assets() {
async function build_all() {
build_rust();
await build_web_assets();
- build_metadata();
// "files": ["./src/ts/perspective.node.ts", "./src/ts/perspective.ts"]
// Typecheck
diff --git a/rust/perspective-js/docs/index.html b/rust/perspective-js/docs/index.html
index b3842a318d..c543976a04 100644
--- a/rust/perspective-js/docs/index.html
+++ b/rust/perspective-js/docs/index.html
@@ -7,8 +7,11 @@
});
diff --git a/rust/perspective-viewer/docs/viewer.md b/rust/perspective-viewer/docs/viewer.md
new file mode 100644
index 0000000000..b23c60c4d8
--- /dev/null
+++ b/rust/perspective-viewer/docs/viewer.md
@@ -0,0 +1,312 @@
+The JavaScript language bindings for`` Custom Element, the
+main UI for [Perspective](https://perspective.finos.org).
+
+
+The examples in this module are in JavaScript. See perspective docs for the Rust API.
+
+
+## `` Custom Element library
+
+`` provides a complete graphical UI for configuring the
+`perspective` library and formatting its output to the provided visualization
+plugins.
+
+If you are using `esbuild` or another bundler which supports ES6 modules, you
+only need to import the `perspective-viewer` libraries somewhere in your
+application - these modules export nothing, but rather register the components
+for use within your site's regular HTML:
+
+```javascript
+import "@finos/perspective-viewer";
+import "@finos/perspective-viewer-datagrid";
+import "@finos/perspective-viewer-d3fc";
+```
+
+Once imported, the `` Web Component will be available in any
+standard HTML on your site. A simple example:
+
+```html
+
+```
+
+or
+
+```javascript
+const viewer = document.createElement("perspective-viewer");
+```
+
+### 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" });
+```
+
+### Loading data into ``
+
+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 `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 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 }]);
+```
+
+### Server-only via `WebSocketServer()` and Node.js
+
+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);
+
+// Or load data from a table using a view. The browser now also has a copy of
+// this view in its own `table`, as well as its updates transferred to the
+// browser using Apache Arrow.
+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.
+
+### Persistent `` configuration via `save()`/`restore()`.
+
+`` 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());
+```
+
+### Update events
+
+Whenever a ``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 ``'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
+`` 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);
+});
+```
diff --git a/rust/perspective-viewer/package.json b/rust/perspective-viewer/package.json
index 5a9e8c87b4..8786d2ce92 100644
--- a/rust/perspective-viewer/package.json
+++ b/rust/perspective-viewer/package.json
@@ -41,6 +41,7 @@
"devDependencies": {
"@finos/perspective-esbuild-plugin": "workspace:^",
"@finos/perspective-test": "workspace:^",
+ "@finos/perspective-metadata": "workspace:^",
"@types/react": "^17.0.2",
"cpy": "^9.0.1",
"@prospective.co/procss": "0.1.16"
diff --git a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs
index 6ad640ec0a..55edbbb161 100644
--- a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs
+++ b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs
@@ -12,7 +12,7 @@
use std::collections::HashSet;
-use chrono::{NaiveDate, TimeZone, Utc};
+use chrono::{Datelike, NaiveDate, TimeZone, Utc};
use perspective_client::config::*;
use perspective_client::ColumnType;
use wasm_bindgen::JsCast;
@@ -96,8 +96,7 @@ impl FilterColumnProps {
fn get_filter_input(&self) -> Option {
let filter_type = self.get_filter_type()?;
match (&filter_type, &self.filter.term()) {
- (ColumnType::Date, FilterTerm::Scalar(Scalar::Float(x)))
- | (ColumnType::Date, FilterTerm::Scalar(Scalar::DateTime(x))) => {
+ (ColumnType::Date, FilterTerm::Scalar(Scalar::Float(x))) => {
if *x > 0_f64 {
Some(
Utc.timestamp_opt(*x as i64 / 1000, (*x as u32 % 1000) * 1000)
@@ -109,7 +108,7 @@ impl FilterColumnProps {
None
}
},
- (ColumnType::Datetime, FilterTerm::Scalar(Scalar::Float(x) | Scalar::DateTime(x))) => {
+ (ColumnType::Datetime, FilterTerm::Scalar(Scalar::Float(x))) => {
posix_to_utc_str(*x).ok()
},
(ColumnType::Boolean, FilterTerm::Scalar(Scalar::Bool(x))) => {
@@ -181,13 +180,16 @@ impl FilterColumnProps {
}
},
Some(ColumnType::Date) => match NaiveDate::parse_from_str(&val, "%Y-%m-%d") {
- Ok(ref posix) => posix.and_hms_opt(0, 0, 0).map(|x| {
- FilterTerm::Scalar(Scalar::DateTime(x.and_utc().timestamp_millis() as f64))
- }),
+ Ok(ref posix) => Some(FilterTerm::Scalar(Scalar::String(format!(
+ "{:0>4}-{:0>2}-{:0>2}",
+ posix.year(),
+ posix.month(),
+ posix.day(),
+ )))),
_ => None,
},
Some(ColumnType::Datetime) => match str_to_utc_posix(&val) {
- Ok(x) => Some(FilterTerm::Scalar(Scalar::DateTime(x))),
+ Ok(x) => Some(FilterTerm::Scalar(Scalar::Float(x))),
_ => None,
},
Some(ColumnType::Boolean) => Some(FilterTerm::Scalar(match val.as_str() {
diff --git a/rust/perspective-viewer/src/rust/components/function_dropdown.rs b/rust/perspective-viewer/src/rust/components/function_dropdown.rs
index 426e74224b..8cb3bf50cb 100644
--- a/rust/perspective-viewer/src/rust/components/function_dropdown.rs
+++ b/rust/perspective-viewer/src/rust/components/function_dropdown.rs
@@ -10,11 +10,11 @@
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+use perspective_client::config::CompletionItemSuggestion;
use web_sys::*;
use yew::prelude::*;
use super::modal::*;
-use crate::exprtk::CompletionItemSuggestion;
use crate::utils::WeakScope;
static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/function-dropdown.css"));
diff --git a/rust/perspective-viewer/src/rust/custom_elements/function_dropdown.rs b/rust/perspective-viewer/src/rust/custom_elements/function_dropdown.rs
index 329df5d09f..c168152e8a 100644
--- a/rust/perspective-viewer/src/rust/custom_elements/function_dropdown.rs
+++ b/rust/perspective-viewer/src/rust/custom_elements/function_dropdown.rs
@@ -13,6 +13,7 @@
use std::cell::RefCell;
use std::rc::Rc;
+use perspective_client::config::{CompletionItemSuggestion, COMPLETIONS};
use perspective_js::utils::global;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
@@ -22,7 +23,6 @@ use yew::*;
use crate::components::function_dropdown::*;
use crate::custom_elements::modal::*;
-use crate::exprtk::{CompletionItemSuggestion, COMPLETIONS};
use crate::*;
#[wasm_bindgen]
@@ -107,10 +107,9 @@ impl Default for FunctionDropDownElement {
fn filter_values(input: &str) -> Vec {
let input = input.to_lowercase();
- COMPLETIONS.with(|x| {
- x.iter()
- .filter(|x| x.label.to_lowercase().starts_with(&input))
- .cloned()
- .collect::>()
- })
+ COMPLETIONS
+ .iter()
+ .filter(|x| x.label.to_lowercase().starts_with(&input))
+ .cloned()
+ .collect::>()
}
diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs
index 2cdb01eb23..970578a258 100644
--- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs
+++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs
@@ -537,7 +537,7 @@ impl PerspectiveViewerElement {
/// viewer.setAutoSize(false);
/// ```
#[wasm_bindgen]
- pub fn setAutoSize(&mut self, autosize: bool) {
+ pub fn setAutoSize(&self, autosize: bool) {
if autosize {
let handle = Some(ResizeObserverHandle::new(
&self.elem,
@@ -569,7 +569,7 @@ impl PerspectiveViewerElement {
/// viewer.setAutoPause(false);
/// ```
#[wasm_bindgen]
- pub fn setAutoPause(&mut self, autopause: bool) {
+ pub fn setAutoPause(&self, autopause: bool) {
if autopause {
let handle = Some(IntersectionObserverHandle::new(
&self.elem,
@@ -689,7 +689,7 @@ impl PerspectiveViewerElement {
/// viewer.setThrottle(1000);
/// ```
#[wasm_bindgen]
- pub fn setThrottle(&mut self, val: Option) {
+ pub fn setThrottle(&self, val: Option) {
self.renderer.set_throttle(val);
}
diff --git a/rust/perspective-viewer/src/rust/exprtk/language.rs b/rust/perspective-viewer/src/rust/exprtk/language.rs
deleted file mode 100644
index 94258c65d9..0000000000
--- a/rust/perspective-viewer/src/rust/exprtk/language.rs
+++ /dev/null
@@ -1,415 +0,0 @@
-// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
-// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
-// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
-// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
-// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
-// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
-// ┃ Copyright (c) 2017, the Perspective Authors. ┃
-// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
-// ┃ This file is part of the Perspective library, distributed under the terms ┃
-// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
-// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
-
-use std::cell::RefCell;
-
-use serde::Serialize;
-
-#[derive(Serialize, Clone, Copy)]
-pub struct CompletionItemSuggestion {
- pub label: &'static str,
- pub insert_text: &'static str,
- pub documentation: &'static str,
-}
-
-thread_local! {
- pub static COMPLETION_COLUMN_NAMES: RefCell> = const { RefCell::new(vec![]) };
-
- pub static COMPLETIONS: Vec = vec![
- CompletionItemSuggestion {
- label: "var",
- insert_text: "var ${1:x := 1}",
- documentation: "Declare a new local variable",
- },
- CompletionItemSuggestion {
- label: "abs",
- insert_text: "abs(${1:x})",
- documentation: "Absolute value of x",
- },
- CompletionItemSuggestion {
- label: "avg",
- insert_text: "avg(${1:x})",
- documentation: "Average of all inputs",
- },
- CompletionItemSuggestion {
- label: "bucket",
- insert_text: "bucket(${1:x}, ${2:y})",
- documentation: "Bucket x by y",
- },
- CompletionItemSuggestion {
- label: "ceil",
- insert_text: "ceil(${1:x})",
- documentation: "Smallest integer >= x",
- },
- CompletionItemSuggestion {
- label: "exp",
- insert_text: "exp(${1:x})",
- documentation: "Natural exponent of x (e ^ x)",
- },
- CompletionItemSuggestion {
- label: "floor",
- insert_text: "floor(${1:x})",
- documentation: "Largest integer <= x",
- },
- CompletionItemSuggestion {
- label: "frac",
- insert_text: "frac(${1:x})",
- documentation: "Fractional portion (after the decimal) of x",
- },
- CompletionItemSuggestion {
- label: "iclamp",
- insert_text: "iclamp(${1:x})",
- documentation: "Inverse clamp x within a range",
- },
- CompletionItemSuggestion {
- label: "inrange",
- insert_text: "inrange(${1:x})",
- documentation: "Returns whether x is within a range",
- },
- CompletionItemSuggestion {
- label: "log",
- insert_text: "log(${1:x})",
- documentation: "Natural log of x",
- },
- CompletionItemSuggestion {
- label: "log10",
- insert_text: "log10(${1:x})",
- documentation: "Base 10 log of x",
- },
- CompletionItemSuggestion {
- label: "log1p",
- insert_text: "log1p(${1:x})",
- documentation: "Natural log of 1 + x where x is very small",
- },
- CompletionItemSuggestion {
- label: "log2",
- insert_text: "log2(${1:x})",
- documentation: "Base 2 log of x",
- },
- CompletionItemSuggestion {
- label: "logn",
- insert_text: "logn(${1:x}, ${2:N})",
- documentation: "Base N log of x where N >= 0",
- },
- CompletionItemSuggestion {
- label: "max",
- insert_text: "max(${1:x})",
- documentation: "Maximum value of all inputs",
- },
- CompletionItemSuggestion {
- label: "min",
- insert_text: "min(${1:x})",
- documentation: "Minimum value of all inputs",
- },
- CompletionItemSuggestion {
- label: "mul",
- insert_text: "mul(${1:x})",
- documentation: "Product of all inputs",
- },
- CompletionItemSuggestion {
- label: "percent_of",
- insert_text: "percent_of(${1:x})",
- documentation: "Percent y of x",
- },
- CompletionItemSuggestion {
- label: "pow",
- insert_text: "pow(${1:x}, ${2:y})",
- documentation: "x to the power of y",
- },
- CompletionItemSuggestion {
- label: "root",
- insert_text: "root(${1:x}, ${2:N})",
- documentation: "N-th root of x where N >= 0",
- },
- CompletionItemSuggestion {
- label: "round",
- insert_text: "round(${1:x})",
- documentation: "Round x to the nearest integer",
- },
- CompletionItemSuggestion {
- label: "sgn",
- insert_text: "sgn(${1:x})",
- documentation: "Sign of x: -1, 1, or 0",
- },
- CompletionItemSuggestion {
- label: "sqrt",
- insert_text: "sqrt(${1:x})",
- documentation: "Square root of x",
- },
- CompletionItemSuggestion {
- label: "sum",
- insert_text: "sum(${1:x})",
- documentation: "Sum of all inputs",
- },
- CompletionItemSuggestion {
- label: "trunc",
- insert_text: "trunc(${1:x})",
- documentation: "Integer portion of x",
- },
- CompletionItemSuggestion {
- label: "acos",
- insert_text: "acos(${1:x})",
- documentation: "Arc cosine of x in radians",
- },
- CompletionItemSuggestion {
- label: "acosh",
- insert_text: "acosh(${1:x})",
- documentation: "Inverse hyperbolic cosine of x in radians",
- },
- CompletionItemSuggestion {
- label: "asin",
- insert_text: "asin(${1:x})",
- documentation: "Arc sine of x in radians",
- },
- CompletionItemSuggestion {
- label: "asinh",
- insert_text: "asinh(${1:x})",
- documentation: "Inverse hyperbolic sine of x in radians",
- },
- CompletionItemSuggestion {
- label: "atan",
- insert_text: "atan(${1:x})",
- documentation: "Arc tangent of x in radians",
- },
- CompletionItemSuggestion {
- label: "atanh",
- insert_text: "atanh(${1:x})",
- documentation: "Inverse hyperbolic tangent of x in radians",
- },
- CompletionItemSuggestion {
- label: "cos",
- insert_text: "cos(${1:x})",
- documentation: "Cosine of x",
- },
- CompletionItemSuggestion {
- label: "cosh",
- insert_text: "cosh(${1:x})",
- documentation: "Hyperbolic cosine of x",
- },
- CompletionItemSuggestion {
- label: "cot",
- insert_text: "cot(${1:x})",
- documentation: "Cotangent of x",
- },
- CompletionItemSuggestion {
- label: "sin",
- insert_text: "sin(${1:x})",
- documentation: "Sine of x",
- },
- CompletionItemSuggestion {
- label: "sinc",
- insert_text: "sinc(${1:x})",
- documentation: "Sine cardinal of x",
- },
- CompletionItemSuggestion {
- label: "sinh",
- insert_text: "sinh(${1:x})",
- documentation: "Hyperbolic sine of x",
- },
- CompletionItemSuggestion {
- label: "tan",
- insert_text: "tan(${1:x})",
- documentation: "Tangent of x",
- },
- CompletionItemSuggestion {
- label: "tanh",
- insert_text: "tanh(${1:x})",
- documentation: "Hyperbolic tangent of x",
- },
- CompletionItemSuggestion {
- label: "deg2rad",
- insert_text: "deg2rad(${1:x})",
- documentation: "Convert x from degrees to radians",
- },
- CompletionItemSuggestion {
- label: "deg2grad",
- insert_text: "deg2grad(${1:x})",
- documentation: "Convert x from degrees to gradians",
- },
- CompletionItemSuggestion {
- label: "rad2deg",
- insert_text: "rad2deg(${1:x})",
- documentation: "Convert x from radians to degrees",
- },
- CompletionItemSuggestion {
- label: "grad2deg",
- insert_text: "grad2deg(${1:x})",
- documentation: "Convert x from gradians to degrees",
- },
- CompletionItemSuggestion {
- label: "concat",
- insert_text: "concat(${1:x}, ${2:y})",
- documentation: "Concatenate string columns and string literals, such as:\nconcat(\"State\" ', ', \"City\")",
- },
- CompletionItemSuggestion {
- label: "order",
- insert_text: "order(${1:input column}, ${2:value}, ...)",
- documentation: "Generates a sort order for a string column based on the input order of the parameters, such as:\norder(\"State\", 'Texas', 'New York')",
- },
- CompletionItemSuggestion {
- label: "upper",
- insert_text: "upper(${1:x})",
- documentation: "Uppercase of x",
- },
- CompletionItemSuggestion {
- label: "lower",
- insert_text: "lower(${1:x})",
- documentation: "Lowercase of x",
- },
- CompletionItemSuggestion {
- label: "hour_of_day",
- insert_text: "hour_of_day(${1:x})",
- documentation: "Return a datetime's hour of the day as a string",
- },
- CompletionItemSuggestion {
- label: "month_of_year",
- insert_text: "month_of_year(${1:x})",
- documentation: "Return a datetime's month of the year as a string",
- },
- CompletionItemSuggestion {
- label: "day_of_week",
- insert_text: "day_of_week(${1:x})",
- documentation: "Return a datetime's day of week as a string",
- },
- CompletionItemSuggestion {
- label: "now",
- insert_text: "now()",
- documentation: "The current datetime in local time",
- },
- CompletionItemSuggestion {
- label: "today",
- insert_text: "today()",
- documentation: "The current date in local time",
- },
- CompletionItemSuggestion {
- label: "is_null",
- insert_text: "is_null(${1:x})",
- documentation: "Whether x is a null value",
- },
- CompletionItemSuggestion {
- label: "is_not_null",
- insert_text: "is_not_null(${1:x})",
- documentation: "Whether x is not a null value",
- },
- CompletionItemSuggestion {
- label: "not",
- insert_text: "not(${1:x})",
- documentation: "not x",
- },
- CompletionItemSuggestion {
- label: "true",
- insert_text: "true",
- documentation: "Boolean value true",
- },
- CompletionItemSuggestion {
- label: "false",
- insert_text: "false",
- documentation: "Boolean value false",
- },
- CompletionItemSuggestion {
- label: "if",
- insert_text: "if (${1:condition}) {} else if (${2:condition}) {} else {}",
- documentation: "An if/else conditional, which evaluates a condition such as:\n if (\"Sales\" > 100) { true } else { false }",
- },
- CompletionItemSuggestion {
- label: "for",
- insert_text: "for (${1:expression}) {}",
- documentation: "A for loop, which repeatedly evaluates an incrementing expression such as:\nvar x := 0; var y := 1; for (x < 10; x += 1) { y := x + y }",
- },
- CompletionItemSuggestion {
- label: "string",
- insert_text: "string(${1:x})",
- documentation: "Converts the given argument to a string",
- },
- CompletionItemSuggestion {
- label: "integer",
- insert_text: "integer(${1:x})",
- documentation: "Converts the given argument to a 32-bit integer. If the result over/under-flows, null is returned",
- },
- CompletionItemSuggestion {
- label: "float",
- insert_text: "float(${1:x})",
- documentation: "Converts the argument to a float",
- },
- CompletionItemSuggestion {
- label: "date",
- insert_text: "date(${1:year}, ${1:month}, ${1:day})",
- documentation: "Given a year, month (1-12) and day, create a new date",
- },
- CompletionItemSuggestion {
- label: "datetime",
- insert_text: "datetime(${1:timestamp})",
- documentation: "Given a POSIX timestamp of milliseconds since epoch, create a new datetime",
- },
- CompletionItemSuggestion {
- label: "boolean",
- insert_text: "boolean(${1:x})",
- documentation: "Converts the given argument to a boolean",
- },
- CompletionItemSuggestion {
- label: "random",
- insert_text: "random()",
- documentation: "Returns a random float between 0 and 1, inclusive.",
- },
- CompletionItemSuggestion {
- label: "match",
- insert_text: "match(${1:string}, ${2:pattern})",
- documentation: "Returns True if any part of string matches pattern, and False otherwise.",
- },
- CompletionItemSuggestion {
- label: "match_all",
- insert_text: "match_all(${1:string}, ${2:pattern})",
- documentation: "Returns True if the whole string matches pattern, and False otherwise.",
- },
- CompletionItemSuggestion {
- label: "search",
- insert_text: "search(${1:string}, ${2:pattern})",
- documentation: "Returns the substring that matches the first capturing group in pattern, or null if there are no capturing groups in the pattern or if there are no matches.",
- },
- CompletionItemSuggestion {
- label: "indexof",
- insert_text: "indexof(${1:string}, ${2:pattern}, ${3:output_vector})",
- documentation: "Writes into index 0 and 1 of output_vector the start and end indices of the substring that matches the first capturing group in pattern.\n\nReturns true if there is a match and output was written, or false if there are no capturing groups in the pattern, if there are no matches, or if the indices are invalid.",
- },
- CompletionItemSuggestion {
- label: "substring",
- insert_text: "substring(${1:string}, ${2:start_idx}, ${3:length})",
- documentation: "Returns a substring of string from start_idx with the given length. If length is not passed in, returns substring from start_idx to the end of the string. Returns null if the string or any indices are invalid.",
- },
- CompletionItemSuggestion {
- label: "replace",
- insert_text: "replace(${1:string}, ${2:pattern}, ${3:replacer})",
- documentation: "Replaces the first match of pattern in string with replacer, or return the original string if no replaces were made.",
- },
- CompletionItemSuggestion {
- label: "replace_all",
- insert_text: "replace(${1:string}, ${2:pattern}, ${3:replacer})",
- documentation: "Replaces all non-overlapping matches of pattern in string with replacer, or return the original string if no replaces were made.",
- },
- CompletionItemSuggestion {
- label: "index",
- insert_text: "index()",
- documentation: "Looks up the index value of the current row",
- },
- CompletionItemSuggestion {
- label: "col",
- insert_text: "col(${1:string})",
- documentation: "Looks up a column value by name",
- },
- CompletionItemSuggestion {
- label: "vlookup",
- insert_text: "vlookup(${1:string}, ${2:uint64})",
- documentation: "Looks up a value in another column by index",
- },
- ]
- ;
-}
diff --git a/rust/perspective-viewer/src/rust/exprtk/mod.rs b/rust/perspective-viewer/src/rust/exprtk/mod.rs
index bde7466434..db0d79d7f1 100644
--- a/rust/perspective-viewer/src/rust/exprtk/mod.rs
+++ b/rust/perspective-viewer/src/rust/exprtk/mod.rs
@@ -11,9 +11,7 @@
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
mod cursor;
-mod language;
mod tokenize;
pub use cursor::*;
-pub use language::*;
pub use tokenize::*;
diff --git a/rust/perspective-viewer/src/rust/lib.rs b/rust/perspective-viewer/src/rust/lib.rs
index fed51215b7..285daca7fa 100644
--- a/rust/perspective-viewer/src/rust/lib.rs
+++ b/rust/perspective-viewer/src/rust/lib.rs
@@ -22,6 +22,7 @@
clippy::panic_in_result_fn,
clippy::await_holding_refcell_ref
)]
+#![doc = include_str!("../../docs/viewer.md")]
pub mod components;
pub mod config;
diff --git a/rust/perspective/build.mjs b/rust/perspective/build.mjs
index 378fe95f24..47692a76ed 100644
--- a/rust/perspective/build.mjs
+++ b/rust/perspective/build.mjs
@@ -40,5 +40,5 @@ if (process.env.PSP_ARCH === "x86_64" && process.platform === "darwin") {
target = "--target=aarch64-unknown-linux-gnu";
}
-cmd.sh(`cargo build ${flags} ${target} --features=external-cpp`);
+cmd.sh(`cargo build ${flags} ${target}`);
cmd.runSync();
diff --git a/rust/perspective/src/axum.rs b/rust/perspective/src/axum.rs
index 090a7fc377..c86a68afa2 100644
--- a/rust/perspective/src/axum.rs
+++ b/rust/perspective/src/axum.rs
@@ -111,6 +111,7 @@ pub fn websocket_handler() -> MethodRouter {
tracing::error!("Internal error {}", msg);
}
+ tracing::info!("{addr} Disconnected.");
session.close().await;
})
}
diff --git a/tools/perspective-bench/package.json b/tools/perspective-bench/package.json
index ef83094f7f..5b2f080575 100644
--- a/tools/perspective-bench/package.json
+++ b/tools/perspective-bench/package.json
@@ -25,7 +25,7 @@
"express": "4.18.2",
"@finos/perspective": "workspace:^",
"express-ws": "^5.0.2",
- "zx": "8.1.8"
+ "zx": "^8.1.8"
},
"dependencies": {
"perspective-3-0-0": "npm:@finos/perspective@3.0.0",
diff --git a/tools/perspective-bench/python_suite.mjs b/tools/perspective-bench/python_suite.mjs
index 5bfc5364eb..89f159edef 100644
--- a/tools/perspective-bench/python_suite.mjs
+++ b/tools/perspective-bench/python_suite.mjs
@@ -20,8 +20,6 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as url from "node:url";
-import "zx/globals";
-
import * as python from "./src/js/servers/python.mjs";
import * as all_benchmarks from "./cross_platform_suite.mjs";
import * as perspective_bench from "./src/js/benchmark.mjs";
diff --git a/tools/perspective-bench/src/js/superstore.mjs b/tools/perspective-bench/src/js/superstore.mjs
index 4cbd26720f..26e1adce25 100644
--- a/tools/perspective-bench/src/js/superstore.mjs
+++ b/tools/perspective-bench/src/js/superstore.mjs
@@ -12,7 +12,6 @@
import * as fs from "node:fs";
import { createRequire } from "node:module";
-import "zx/globals";
/**
* Load a file as an `ArrayBuffer`, which is useful for loading Apache Arrow
diff --git a/tools/perspective-scripts/clean.mjs b/tools/perspective-scripts/clean.mjs
index d23b097e25..4e8175b202 100644
--- a/tools/perspective-scripts/clean.mjs
+++ b/tools/perspective-scripts/clean.mjs
@@ -12,45 +12,42 @@
import { clean, get_scope } from "./sh_perspective.mjs";
import { execSync } from "child_process";
+import * as fs from "node:fs";
const PACKAGES = get_scope();
const JS_PKGS = [];
const RUST_PKGS = [];
+
+const CRATE_NAMES = fs.readdirSync("rust");
+
for (const pkg of PACKAGES) {
if (pkg === "perspective-cpp") {
console.log("-- Cleaning perspective-cpp");
clean("cpp/perspective/dist", "cpp/perspective/build");
- } else if (
- [
- "perspective-cli",
- "perspective-esbuild-plugin",
- "perspective-jupyterlab",
- "perspective-viewer-d3fc",
- "perspective-viewer-datagrid",
- "perspective-viewer-openlayers",
- "perspective-webpack-plugin",
- "perspective-workspace",
- ].indexOf(pkg) > -1
- ) {
- JS_PKGS.push(pkg);
- } else {
+ } else if (CRATE_NAMES.indexOf(pkg) > -1) {
RUST_PKGS.push(pkg);
+ } else {
+ JS_PKGS.push(pkg);
}
}
-if (JS_PKGS.length > 0) {
+if (JS_PKGS.length > 0 || RUST_PKGS.length > 0) {
console.log(`-- Cleaning ${JS_PKGS.join(", ")} via pnpm`);
- execSync(
- `pnpm run ${JS_PKGS.map((x) => `--filter ${x} --if-present`).join(
- " "
- )} clean`,
- { stdio: "inherit" }
- );
+ const flags = JS_PKGS.concat(RUST_PKGS)
+ .map((x) => `--filter @finos/${x} --if-present`)
+ .join(" ");
+
+ execSync(`pnpm run ${flags} clean`, { stdio: "inherit" });
}
if (RUST_PKGS.length > 0) {
- console.log(`-- Cleaning ${RUST_PKGS.join(", ")} via cargo`);
- execSync(`cargo clean ${RUST_PKGS.map((x) => `-p ${x}`).join(" ")}`);
+ if (process.env.PACKAGE?.length > 1) {
+ console.log(`-- Cleaning ${RUST_PKGS.join(", ")} via cargo`);
+ execSync(`cargo clean ${RUST_PKGS.map((x) => `-p ${x}`).join(" ")}`);
+ } else {
+ console.log(`-- Cleaning all crates via cargo`);
+ execSync(`cargo clean`);
+ }
}
clean("docs/build", "docs/python", "docs/obj");
diff --git a/rust/perspective-js/src/rust/main.rs b/tools/perspective-scripts/docs.mjs
similarity index 90%
rename from rust/perspective-js/src/rust/main.rs
rename to tools/perspective-scripts/docs.mjs
index 48ab2bb092..536334ccf0 100644
--- a/rust/perspective-js/src/rust/main.rs
+++ b/tools/perspective-scripts/docs.mjs
@@ -10,11 +10,10 @@
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
-#![warn(clippy::all)]
+import * as dotenv from "dotenv";
+import "zx/globals";
-use perspective_js::generate_type_bindings;
+dotenv.config({ path: "./.perspectiverc" });
-fn main() -> Result<(), Box> {
- generate_type_bindings();
- Ok(())
-}
+const HOST = /host\: (.+?)$/gm.exec(await $`rustc -vV`)[1];
+await $`cargo doc --no-deps --target=${HOST}`;
diff --git a/tools/perspective-scripts/lint.mjs b/tools/perspective-scripts/lint.mjs
index 8142016538..305ae962a4 100644
--- a/tools/perspective-scripts/lint.mjs
+++ b/tools/perspective-scripts/lint.mjs
@@ -18,8 +18,8 @@ import * as cppLint from "./lint_cpp.mjs";
export function lint_js(is_fix = false) {
const prettier_flags = is_fix ? "--write" : "--check";
const cmd = sh`prettier ${prettier_flags} "examples/**/*.js" "examples/**/*.tsx" "tools/perspective-scripts/*.mjs" "rust/**/*.ts" "rust/**/*.js" "packages/**/*.js" "packages/**/*.ts" "cpp/**/*.js"`;
- cmd.sh`prettier --prose-wrap=always ${prettier_flags} "docs/docs/*.md"`;
- cmd.sh`prettier ${prettier_flags} "**/*.yml"`;
+ cmd.sh`prettier --prose-wrap=always ${prettier_flags} "rust/*/docs/**/*.md"`;
+ // cmd.sh`prettier ${prettier_flags} "**/*.yaml"`;
cmd.sh`prettier ${prettier_flags} "**/less/*.less"`;
cmd.sh`prettier ${prettier_flags} "**/html/*.html"`;
cmd.sh`prettier ${prettier_flags} "packages/**/package.json" "rust/**/package.json" "examples/**/package.json" "docs/package.json"`;
diff --git a/tools/perspective-scripts/package.json b/tools/perspective-scripts/package.json
index bcc0ee2109..4c90aa0a75 100644
--- a/tools/perspective-scripts/package.json
+++ b/tools/perspective-scripts/package.json
@@ -11,5 +11,8 @@
"url": "https://github.com/finos/perspective/tools/perspective-scripts"
},
"author": "",
- "license": "Apache-2.0"
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "zx": "^8.1.8"
+ }
}
diff --git a/tools/perspective-scripts/setup.mjs b/tools/perspective-scripts/setup.mjs
index a6437a8e87..2308cb28ff 100644
--- a/tools/perspective-scripts/setup.mjs
+++ b/tools/perspective-scripts/setup.mjs
@@ -89,6 +89,78 @@ async function choose_docker() {
}
async function focus_package() {
+ const choices = [
+ {
+ key: "r",
+ name: "perspective-docs",
+ value: "perspective-docs",
+ },
+ {
+ key: "c",
+ name: "perspective-cpp",
+ value: "perspective-cpp",
+ },
+ {
+ key: "p",
+ name: "perspective (perspective-js)",
+ value: "perspective",
+ },
+ {
+ key: "m",
+ name: "perspective-metadata",
+ value: "perspective-metadata",
+ },
+ {
+ key: "y",
+ name: "perspective-python",
+ value: "perspective-python",
+ },
+ {
+ key: "q",
+ name: "perspective-pyodide",
+ value: "perspective-pyodide",
+ },
+ {
+ key: "r",
+ name: "perspective-rs",
+ value: "perspective-rs",
+ },
+ {
+ key: "v",
+ name: "perspective-viewer",
+ value: "perspective-viewer",
+ },
+ {
+ key: "e",
+ name: "perspective-viewer-datagrid",
+ value: "perspective-viewer-datagrid",
+ },
+ {
+ key: "d",
+ name: "perspective-viewer-d3fc",
+ value: "perspective-viewer-d3fc",
+ },
+ {
+ key: "i",
+ name: "perspective-jupyterlab",
+ value: "perspective-jupyterlab",
+ },
+ {
+ key: "o",
+ name: "perspective-viewer-openlayers",
+ value: "perspective-viewer-openlayers",
+ },
+ {
+ key: "w",
+ name: "perspective-workspace",
+ value: "perspective-workspace",
+ },
+ {
+ key: "l",
+ name: "perspective-cli",
+ value: "perspective-cli",
+ },
+ ];
const new_config = await inquirer.prompt([
{
type: "checkbox",
@@ -102,76 +174,15 @@ async function focus_package() {
}
},
filter: (answer) => {
- if (!answer || answer.length === 9) {
+ if (!answer || answer.length === choices.length) {
return "";
} else {
return answer;
}
},
loop: false,
- pageSize: 12,
- choices: [
- {
- key: "c",
- name: "perspective-cpp",
- value: "perspective-cpp",
- },
- {
- key: "p",
- name: "perspective (perspective-js)",
- value: "perspective",
- },
- {
- key: "y",
- name: "perspective-python",
- value: "perspective-python",
- },
- {
- key: "q",
- name: "perspective-pyodide (Python)",
- value: "perspective-pyodide",
- },
- {
- key: "r",
- name: "perspective-rs",
- value: "perspective-rs",
- },
- {
- key: "v",
- name: "perspective-viewer",
- value: "perspective-viewer",
- },
- {
- key: "e",
- name: "perspective-viewer-datagrid",
- value: "perspective-viewer-datagrid",
- },
- {
- key: "d",
- name: "perspective-viewer-d3fc",
- value: "perspective-viewer-d3fc",
- },
- {
- key: "i",
- name: "perspective-jupyterlab",
- value: "perspective-jupyterlab",
- },
- {
- key: "m",
- name: "perspective-viewer-openlayers",
- value: "perspective-viewer-openlayers",
- },
- {
- key: "w",
- name: "perspective-workspace",
- value: "perspective-workspace",
- },
- {
- key: "l",
- name: "perspective-cli",
- value: "perspective-cli",
- },
- ],
+ pageSize: 20,
+ choices,
},
]);
diff --git a/tools/perspective-scripts/sh_perspective.mjs b/tools/perspective-scripts/sh_perspective.mjs
index c68fcc7bb5..0c9a04dcde 100644
--- a/tools/perspective-scripts/sh_perspective.mjs
+++ b/tools/perspective-scripts/sh_perspective.mjs
@@ -94,7 +94,7 @@ export function get_scope() {
(acc, x) => {
if (x.startsWith("!")) {
acc.exclude.push(x);
- } else {
+ } else if (x != "") {
acc.include.push(x);
}
diff --git a/tools/perspective-scripts/test_js.mjs b/tools/perspective-scripts/test_js.mjs
index 313fe50124..bb37ed35bb 100644
--- a/tools/perspective-scripts/test_js.mjs
+++ b/tools/perspective-scripts/test_js.mjs
@@ -11,7 +11,7 @@
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import sh from "./sh.mjs";
-import { getarg, run_with_scope } from "./sh_perspective.mjs";
+import { getarg, run_with_scope, get_scope } from "./sh_perspective.mjs";
import minimatch from "minimatch";
// Unfortunately we have to handle parts of the Jupyter test case here,
@@ -24,10 +24,11 @@ if (getarg("--debug")) {
console.log("-- Running tests in debug mode.");
}
-const IS_PLAYWRIGHT = process.env.PACKAGE.split(",").reduce(
+const IS_PLAYWRIGHT = get_scope().reduce(
(is_playwright, pkg) =>
is_playwright ||
[
+ "perspective-docs",
"perspective-cli",
"perspective-js",
"perspective",
@@ -36,12 +37,13 @@ const IS_PLAYWRIGHT = process.env.PACKAGE.split(",").reduce(
"perspective-viewer-d3fc",
"perspective-viewer-openlayers",
"perspective-viewer-workspace",
+ "perspective-workspace",
"perspective-jupyter",
].includes(pkg),
false
);
-const IS_RUST = process.env.PACKAGE.split(",").reduce(
+const IS_RUST = get_scope().reduce(
(is_playwright, pkg) => is_playwright || ["perspective-rs"].includes(pkg),
false
);
diff --git a/tools/perspective-test/package.json b/tools/perspective-test/package.json
index b86987ad8c..55ee73265d 100644
--- a/tools/perspective-test/package.json
+++ b/tools/perspective-test/package.json
@@ -20,6 +20,7 @@
"author": "",
"license": "Apache-2.0",
"dependencies": {
+ "glob": "^11",
"xml-formatter": "2.4.0"
}
}
diff --git a/tools/perspective-test/playwright.config.ts b/tools/perspective-test/playwright.config.ts
index c80b978a32..1f42a4cdb1 100644
--- a/tools/perspective-test/playwright.config.ts
+++ b/tools/perspective-test/playwright.config.ts
@@ -16,6 +16,7 @@ import * as dotenv from "dotenv";
import { createRequire } from "node:module";
import url from "node:url";
import { execSync } from "child_process";
+import { get_scope } from "../perspective-scripts/sh_perspective.mjs";
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -31,7 +32,7 @@ const TEST_SERVER_PORT = 6598;
const RUN_JUPYTERLAB = !!process.env.PSP_JUPYTERLAB_TESTS;
// TODO use this from core
-const package_venn = (process.env.PACKAGE || "").split(",").reduce(
+const package_venn = get_scope().reduce(
(acc, x) => {
if (x.startsWith("!")) {
acc.exclude.push(x);
@@ -104,6 +105,10 @@ const BROWSER_PACKAGES = [
packageName: "perspective-cli",
testDir: "packages/perspective-cli/test/js",
},
+ {
+ packageName: "perspective-docs",
+ testDir: "docs/test/js",
+ },
{
packageName: "docs",
testDir: "docs/test/js",
@@ -194,9 +199,9 @@ const GLOBAL_TEARDOWN_PATH = __require.resolve(
// See https://playwright.dev/docs/test-configuration.
export default defineConfig({
- timeout: 30_000,
+ timeout: 360_000,
expect: {
- timeout: 10_000,
+ timeout: 360_000,
},
forbidOnly: !!process.env.CI,
retries: 0,
@@ -211,6 +216,7 @@ export default defineConfig({
// screenshot: "only-on-failure",
// video: "retain-on-failure",
},
+ updateSnapshots: "none",
globalSetup: RUN_JUPYTERLAB
? GLOBAL_SETUP_PATH
: path.join(__dirname, "src/js/global_startup.ts"),
diff --git a/tools/perspective-test/results.tar.gz b/tools/perspective-test/results.tar.gz
index 3ea71c1185..afc0bc14b9 100644
Binary files a/tools/perspective-test/results.tar.gz and b/tools/perspective-test/results.tar.gz differ
diff --git a/tools/perspective-test/src/js/global_startup.ts b/tools/perspective-test/src/js/global_startup.ts
index 595f696442..da48d5a165 100644
--- a/tools/perspective-test/src/js/global_startup.ts
+++ b/tools/perspective-test/src/js/global_startup.ts
@@ -20,8 +20,9 @@ const __dirname = path.dirname(__filename);
export default async function run() {
const RESULTS_PATH = path.join(__dirname, "../../results.tar.gz");
+ const cwd = path.join(__dirname, "..", "..");
if (fs.existsSync(RESULTS_PATH)) {
console.log("Using results.tar.gz");
- await tar.extract({ file: RESULTS_PATH, gzip: true });
+ await tar.extract({ file: RESULTS_PATH, gzip: true, cwd });
}
}
diff --git a/tools/perspective-test/src/js/global_teardown.ts b/tools/perspective-test/src/js/global_teardown.ts
index 4be892f1bf..e52c8a7f50 100644
--- a/tools/perspective-test/src/js/global_teardown.ts
+++ b/tools/perspective-test/src/js/global_teardown.ts
@@ -27,14 +27,17 @@ export default async function run() {
console.log("\nCreating results.tar.gz");
}
+ const cwd = path.join(__dirname, "..", "..");
await new Promise((x) =>
tar.create(
{
+ cwd,
gzip: true,
file: RESULTS_PATH,
sync: false,
portable: true,
noMtime: true,
+ strip: 2,
filter: (path, stat) => {
stat.mtime = null;
stat.atime = null;
@@ -44,18 +47,8 @@ export default async function run() {
},
},
[
- ...glob.sync(
- path.join(
- __dirname,
- "../../../../tools/perspective-test/dist/snapshots/**/*.txt"
- )
- ),
- ...glob.sync(
- path.join(
- __dirname,
- "../../../../tools/perspective-test/dist/snapshots/**/*.html"
- )
- ),
+ ...glob.sync("dist/snapshots/**/*.txt", { cwd }),
+ ...glob.sync("dist/snapshots/**/*.html", { cwd }),
],
x
)