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 -
editablefilefractal
marketraycastingevictions
nypdstreamingcovid
webcammoviessuperstore
citibikeolympicsjupyterlab
+
editablefilefractal
marketraycastingevictions
nypdstreamingcovid
webcammoviessuperstore
citibikeolympics
### Media @@ -65,22 +72,19 @@ and/or [Jupyterlab](https://jupyterlab.readthedocs.io/en/stable/). @timbess @sc1f - - - - - - - + + + + + @texodus @texodus - - - - - - + + + + + diff --git a/cpp/perspective/src/cpp/server.cpp b/cpp/perspective/src/cpp/server.cpp index 98f221e9fc..7ad1946ee2 100644 --- a/cpp/perspective/src/cpp/server.cpp +++ b/cpp/perspective/src/cpp/server.cpp @@ -453,6 +453,10 @@ ServerResources::get_view(const t_id& id) { void ServerResources::delete_view(const std::uint32_t& client_id, const t_id& id) { + if (!m_view_to_table.contains(id)) { + throw PerspectiveViewNotFoundException(); + } + { PSP_WRITE_LOCK(m_write_lock); auto table_id = m_view_to_table.at(id); @@ -1068,22 +1072,49 @@ coerce_to(const t_dtype dtype, const A& val) { ); } } else if constexpr (std::is_same_v) { + t_tscalar scalar; + scalar.clear(); switch (dtype) { + case DTYPE_BOOL: + scalar.set(val == 1); + return scalar; + case DTYPE_UINT32: + scalar.set((std::uint32_t)val); + return scalar; + case DTYPE_UINT64: + scalar.set((std::uint64_t)val); + return scalar; + case DTYPE_INT32: + scalar.set(val); + return scalar; + case DTYPE_INT64: + scalar.set((std::int64_t)val); + return scalar; case DTYPE_FLOAT32: - return t_tscalar(static_cast(val)); - case DTYPE_FLOAT64: - return t_tscalar(val); - default: - PSP_COMPLAIN_AND_ABORT("Unsupported type"); - } - } else if constexpr (std::is_same_v) { - switch (dtype) { - case DTYPE_FLOAT32: - return t_tscalar(val); + scalar.set(static_cast(val)); + return scalar; case DTYPE_FLOAT64: - return t_tscalar(static_cast(val)); + scalar.set(val); + return scalar; + case DTYPE_DATE: { + const auto time = static_cast(val / 1000); + std::tm* tm = std::gmtime(&time); + t_date date{ + static_cast(tm->tm_year + 1900), + static_cast(tm->tm_mon), + static_cast(tm->tm_mday) + }; + + scalar.set(date); + return scalar; + } + case DTYPE_TIME: { + t_time time{std::chrono::milliseconds((long)val).count()}; + scalar.set(time); + return scalar; + } default: - PSP_COMPLAIN_AND_ABORT("Unsupported type"); + PSP_COMPLAIN_AND_ABORT("Unsupported double type"); } } else if constexpr (std::is_same_v) { return t_tscalar(val); @@ -1593,7 +1624,6 @@ ProtoServer::_handle_request(std::uint32_t client_id, const Request& req) { for (const auto& arg : f.value()) { t_tscalar a; a.clear(); - switch (arg.scalar_case()) { case proto::Scalar::kBool: { a.set(arg.bool_()); @@ -1601,16 +1631,13 @@ ProtoServer::_handle_request(std::uint32_t client_id, const Request& req) { break; } case proto::Scalar::kFloat: { - a.set(arg.float_()); - args.push_back(a); - break; - } - case proto::Scalar::kInt: { - a.set(arg.int_()); + a = coerce_to( + schema->get_dtype(f.column()), arg.float_() + ); + args.push_back(a); break; } - case proto::Scalar::kString: { if (!schema->has_column(f.column())) { PSP_COMPLAIN_AND_ABORT( @@ -1623,38 +1650,6 @@ ProtoServer::_handle_request(std::uint32_t client_id, const Request& req) { args.push_back(a); break; } - - case proto::Scalar::kDate: { - auto date_ts = arg.date(); - // convert ts to date - auto tt = std::chrono::system_clock::to_time_t( - std::chrono::system_clock::time_point( - std::chrono::seconds(date_ts) - ) - ); - - auto* date = std::localtime(&tt); - - t_date d{ - static_cast(date->tm_year + 1900), - static_cast(date->tm_mon), - static_cast(date->tm_mday) - }; - a.set(d); - args.push_back(a); - break; - } - case proto::Scalar::kDatetime: { - auto datetime_ts = arg.datetime(); - // convert ts to date - auto tt = t_time(datetime_ts); - - a.set(tt); - args.push_back(a); - - break; - } - case proto::Scalar::kNull: a.set(t_none()); args.push_back(a); @@ -1915,6 +1910,7 @@ ProtoServer::_handle_request(std::uint32_t client_id, const Request& req) { vals.push_back(scalar); } } + for (const auto& scalar : vals) { auto* s = f->mutable_value()->Add(); switch (scalar.get_dtype()) { @@ -1925,20 +1921,25 @@ ProtoServer::_handle_request(std::uint32_t client_id, const Request& req) { s->set_float_(scalar.get()); break; case DTYPE_INT64: - s->set_int_(static_cast( - scalar.get() - )); + s->set_float_((double)scalar.get()); break; case DTYPE_STR: s->set_string(scalar.get()); break; case DTYPE_DATE: { - auto tm = scalar.get().get_tm(); - s->set_date(std::mktime(&tm)); + auto tm = scalar.get(); + std::stringstream ss; + ss << std::setfill('0') << std::setw(4) << tm.year() + << "-" << std::setfill('0') << std::setw(2) + << tm.month() << "-" << std::setfill('0') + << std::setw(2) << tm.day(); + s->set_string(ss.str()); break; } case DTYPE_TIME: - s->set_datetime(scalar.get().raw_value()); + s->set_float_( + (double)scalar.get().raw_value() + ); break; case DTYPE_NONE: s->set_null( diff --git a/cpp/protos/perspective.proto b/cpp/protos/perspective.proto index 34230e7809..08940cee1b 100644 --- a/cpp/protos/perspective.proto +++ b/cpp/protos/perspective.proto @@ -53,14 +53,15 @@ message MakeTableData { }; } -// View type scalars +// Filter type scalars - this is _not_ the same as a Columns scalar, as this +// value is used in the view config and must be JSON safe! message Scalar { oneof scalar { bool bool = 1; - int64 date = 2; // TODO these are the wrong type - int64 datetime = 3; + // int64 date = 2; // TODO these are the wrong type + // int64 datetime = 3; double float = 4; - int32 int = 5; + // int32 int = 5; string string = 6; google.protobuf.NullValue null = 7; } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 6bb8a032a1..f589311900 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -75,16 +75,17 @@ const config = { "classic", /** @type {import('@docusaurus/preset-classic').Options} */ ({ - docs: { - sidebarPath: require.resolve("./sidebars.js"), - docItemComponent: require.resolve( - "./src/components/DocItem" - ), - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - // editUrl: - // "https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/", - }, + docs: false, + // docs: { + // // sidebarPath: require.resolve("./sidebars.js"), + // docItemComponent: require.resolve( + // "./src/components/DocItem" + // ), + // // Please change this to your repo. + // // Remove this to remove the "edit this page" links. + // // editUrl: + // // "https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/", + // }, // blog: { // showReadingTime: true, // // Please change this to your repo. @@ -112,12 +113,84 @@ const config = { src: "svg/perspective-logo-light.svg", }, items: [ - // {to: "/blog", label: "News", position: "right"}, { - type: "doc", - docId: "js", + type: "dropdown", position: "right", label: "Docs", + items: [ + { + type: "html", + value: "JavaScript", + }, + { + href: "https://docs.rs/perspective-viewer/latest/perspective_viewer/", + label: "`@finos/perspective-viewer` JavaScript UI API", + }, + { + href: "https://docs.rs/perspective-js/latest/perspective_js/", + label: "`@finos/perspective` JavaScript Client/Server API", + }, + { + href: "https://docs.rs/perspective-js/latest/perspective_js/struct.Table.html", + label: "`Table` API", + }, + { + href: "https://docs.rs/perspective-js/latest/perspective_js/struct.View.html", + label: "`View` API", + }, + { + href: "https://docs.rs/perspective-js/latest/perspective_js/#installation", + label: "Installation Guide", + }, + { + type: "html", + value: "Python", + }, + { + href: "https://docs.rs/perspective-python/latest/perspective_python/", + label: "`perspective-python` Python Client/Server API", + }, + { + href: "https://docs.rs/perspective-python/3.1.0/perspective_python/#perspectivewidget", + label: "`PerspectiveWidget` Jupyter Plugin", + }, + { + href: "https://docs.rs/perspective-python/latest/perspective_python/struct.Table.html", + label: "`Table` API", + }, + { + href: "https://docs.rs/perspective-python/latest/perspective_python/struct.View.html", + label: "`View` API", + }, + { + type: "html", + value: "Rust", + }, + { + href: "https://docs.rs/perspective/latest/perspective/", + label: "`perspective`, Rust API", + }, + { + href: "https://docs.rs/perspective-client/latest/perspective_client/struct.Table.html", + label: "`Table` API", + }, + { + href: "https://docs.rs/perspective-client/latest/perspective_client/struct.View.html", + label: "`View` API", + }, + { + type: "html", + value: "Appendix", + }, + { + href: "https://docs.rs/perspective-server/latest/perspective_server/", + label: "Data Binding", + }, + { + href: "https://docs.rs/perspective-client/latest/perspective_client/config/expressions/", + label: "Expression Columns", + }, + ], }, { to: "/examples", @@ -138,33 +211,33 @@ const config = { }, footer: { links: [ - { - title: "Docs", - items: [ - { - label: "JavaScript User Guide", - to: "/docs/js", - }, - { - label: "Python User Guide", - to: "/docs/python", - }, - ], - }, - { - title: "More", - items: [ - { - label: "GitHub", - href: "https://github.com/finos/perspective", - }, - { - href: "https://www.prospective.co/blog", - label: "Blog", - position: "right", - }, - ], - }, + // { + // title: "Docs", + // items: [ + // { + // label: "JavaScript User Guide", + // to: "/docs/js", + // }, + // { + // label: "Python User Guide", + // to: "/docs/python", + // }, + // ], + // }, + // { + // title: "More", + // items: [ + // { + // label: "GitHub", + // href: "https://github.com/finos/perspective", + // }, + // { + // href: "https://www.prospective.co/blog", + // label: "Blog", + // position: "right", + // }, + // ], + // }, ], copyright: `Copyright © ${new Date().getFullYear()} The Perspective Authors`, }, diff --git a/docs/package.json b/docs/package.json index a8ac156242..f1d3c12b35 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,6 +3,7 @@ "version": "3.1.0", "private": true, "scripts": { + "build": "node build.js && docusaurus build", "docusaurus": "docusaurus", "start": "docusaurus start", "docs": "node build.js && docusaurus build", diff --git a/docs/sidebars.js b/docs/sidebars.js index 3ff2a09026..b0f43b24ec 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -17,17 +17,17 @@ const sidebars = { // By default, Docusaurus generates a sidebar from the docs folder structure // tutorialSidebar: [{type: "autogenerated", dirName: "."}], tutorialSidebar: [ - { - type: "category", - label: "Language Guides", - items: ["js", "python"], - }, + // { + // type: "category", + // label: "Language Guides", + // items: ["js", "python"], + // }, { type: "category", label: "API", items: [ "expressions", - "server", + // "server", { type: "link", label: "`perspective` Rust API", @@ -55,7 +55,7 @@ const sidebars = { }, ], }, - "development", + // "development", ], // ["js", "python", "table", "view", "server", "development"], diff --git a/docs/static/404.html b/docs/static/404.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/blocks/examples.js b/examples/blocks/examples.js index 6078a0e90d..4b176dc74f 100644 --- a/examples/blocks/examples.js +++ b/examples/blocks/examples.js @@ -33,11 +33,11 @@ exports.get_examples = function get_examples( root = "https://perspective.finos.org/" ) { const standalone = [ - { - img: "https://perspective.finos.org/img/jupyterlab.png?", - url: "http://beta.mybinder.org/v2/gh/finos/perspective/master?urlpath=lab/tree/examples/jupyter-notebooks", - name: "jupyterlab", - }, + // { + // img: "https://perspective.finos.org/img/jupyterlab.png?", + // url: "http://beta.mybinder.org/v2/gh/finos/perspective/master?urlpath=lab/tree/examples/jupyter-notebooks", + // name: "jupyterlab", + // }, ]; const hashes = LOCAL_EXAMPLES.map((x) => ({ diff --git a/examples/blocks/src/nypd/index.js b/examples/blocks/src/nypd/index.js index b09d975ae2..6f439dc053 100644 --- a/examples/blocks/src/nypd/index.js +++ b/examples/blocks/src/nypd/index.js @@ -109,21 +109,20 @@ function set_layout_options() { const layout_names = Object.keys(LAYOUTS); window.layouts.innerHTML = ""; for (const layout of layout_names) { - console.log("LAYOUT: ", layout); window.layouts.innerHTML += `${layout}`; } } -console.log("Doing?"); set_layout_options(); -console.log("Done?"); + window.name_input.value = layout_names[0]; window.layouts.addEventListener("change", async () => { if (window.layouts.value.trim().length === 0) { return; } + window.workspace.innerHTML = ""; await window.workspace.restore(LAYOUTS[window.layouts.value]); window.name_input.value = window.layouts.value; diff --git a/examples/blocks/src/nypd/layout.json b/examples/blocks/src/nypd/layout.json index d4ed61e70e..99b7145098 100644 --- a/examples/blocks/src/nypd/layout.json +++ b/examples/blocks/src/nypd/layout.json @@ -87,7 +87,10 @@ "group_by": ["bucket(\"IncidentDate\", 'M')"], "split_by": [], "columns": ["ComplaintID"], - "filter": [["IncidentDate", ">", 473385600000]], + "filter": [ + ["IncidentDate", ">", "1985-01-01"], + ["IncidentDate", "<", "2025-01-01"] + ], "sort": [], "expressions": { "bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')" @@ -130,7 +133,10 @@ "group_by": ["bucket(\"IncidentDate\", 'M')"], "split_by": ["FADOType"], "columns": ["AllegationID"], - "filter": [["IncidentDate", ">", 473385600000]], + "filter": [ + ["IncidentDate", ">", "1985-01-01"], + ["IncidentDate", "<", "2025-01-01"] + ], "sort": [["AllegationID", "col asc"]], "expressions": { "bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')" @@ -148,7 +154,10 @@ "group_by": ["bucket(\"IncidentDate\", 'M')"], "split_by": ["FADOType"], "columns": ["AllegationID"], - "filter": [["IncidentDate", ">", 473385600000]], + "filter": [ + ["IncidentDate", ">", "1985-01-01"], + ["IncidentDate", "<", "2025-01-01"] + ], "sort": [["AllegationID", "col asc"]], "expressions": { "bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')" @@ -264,7 +273,10 @@ "group_by": ["bucket(\"IncidentDate\", 'M')"], "split_by": ["FADOType"], "columns": ["ComplaintID"], - "filter": [["IncidentDate", ">", 473385600000]], + "filter": [ + ["IncidentDate", ">", "1985-01-01"], + ["IncidentDate", "<", "2025-01-01"] + ], "sort": [], "expressions": { "bucket(\"IncidentDate\", 'M')": "bucket(\"IncidentDate\", 'M')" @@ -301,7 +313,10 @@ "group_by": ["bucket(\"IncidentDate\", '3M')"], "split_by": ["Allegation"], "columns": ["AllegationID"], - "filter": [["IncidentDate", ">", 473385600000]], + "filter": [ + ["IncidentDate", ">", "1985-01-01"], + ["IncidentDate", "<", "2025-01-01"] + ], "sort": [["IncidentDate", "col asc"]], "expressions": { "bucket(\"IncidentDate\", '3M')": "bucket(\"IncidentDate\", '3M')" @@ -369,7 +384,10 @@ "group_by": ["bucket(\"IncidentDate\", 'Y')"], "split_by": ["month_of_year(\"IncidentDate\")"], "columns": ["ComplaintID"], - "filter": [["IncidentDate", ">", 473385600000]], + "filter": [ + ["IncidentDate", ">", "1985-01-01"], + ["IncidentDate", "<", "2025-01-01"] + ], "sort": [], "expressions": { "bucket(\"IncidentDate\", 'Y')": "bucket(\"IncidentDate\", 'Y')", @@ -388,7 +406,10 @@ "group_by": ["month_of_year(\"IncidentDate\")"], "split_by": ["day_of_week(\"IncidentDate\")"], "columns": ["ComplaintID"], - "filter": [["IncidentDate", ">", 473385600000]], + "filter": [ + ["IncidentDate", ">", "1985-01-01"], + ["IncidentDate", "<", "2025-01-01"] + ], "sort": [], "expressions": { "bucket(\"IncidentDate\", 'Y')": "bucket(\"IncidentDate\", 'Y')", diff --git a/examples/jupyter-notebooks/README.md b/examples/jupyter-notebooks/README.md deleted file mode 100644 index 5f3f7622cf..0000000000 --- a/examples/jupyter-notebooks/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Jupyter Notebook examples for `perspective-python` - -This folder contains several notebooks designed as an introduction to the various features of `perspective-python`: - -- `table_tutorial.ipynb` shows how to load, update, query, and serialize data using `Table` and `View`, and how to connect multiple `Table` instances together using `on_update`. -- `widget_tutorial.ipynb` demonstrates how to use `PerspectiveWidget` as a powerful interactive visualization component within a Jupyter notebook. -- `pandas_pivots.ipynb` displays Perspective's ability to read pivots from a pivoted DataFrame and automatically apply it as part of a `PerspectiveWidget`. - -Each notebook is fully self-contained and should offer a good place to start for those interested in using `perspective-python` whether within a Jupyter environment or in a pure-Python context. - -For examples pertaining to `perspective-python` Tornado servers, check out: - -- [tornado-python](https://github.com/finos/perspective/tree/master/examples/tornado-python): a simple Tornado server that delivers a static dataset to the user using `perspective-python` and ``. -- [tornado-streaming-python](https://github.com/finos/perspective/tree/master/examples/tornado-streaming-python): a streaming Tornado server that demonstrates `perspective-python`'s high throughput and performance in streaming scenarios. -- [workspace-editing-python](https://github.com/finos/perspective/tree/master/examples/workspace-editing-python): a full-featured example using `` that illustrates a deep and powerful integration between `` and `perspective-python`. \ No newline at end of file diff --git a/examples/jupyter-notebooks/pandas_pivot.ipynb b/examples/jupyter-notebooks/pandas_pivot.ipynb deleted file mode 100644 index 1c062ceb90..0000000000 --- a/examples/jupyter-notebooks/pandas_pivot.ipynb +++ /dev/null @@ -1,376 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using Pivoted DataFrames with Perspective\n", - "\n", - "Perspective tries to infer as much information as possible from already-pivoted dataframes:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "from perspective import Table, PerspectiveWidget" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For the dataset, we'll use `superstore.arrow` which is used in various Perspective demos:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import requests\n", - "\n", - "# Download the arrow\n", - "url = \"https://unpkg.com/@jpmorganchase/perspective-examples@0.2.0-beta.2/build/superstore.arrow\"\n", - "req = requests.get(url)\n", - "arrow = req.content" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a table\n", - "table = Table(arrow)\n", - "view = table.view()\n", - "df = view.to_df()\n", - "display(df.shape, df.dtypes)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Group By\n", - "\n", - "Create a group byed dataframe, and pass it into `PerspectiveWidget`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df_pivoted = df.set_index(['Country', 'Region'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df_pivoted.head()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Pivots will be read from the df and applied\n", - "PerspectiveWidget(df_pivoted)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Split By\n", - "\n", - "The same is true with split by:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "arrays = [np.array(['bar', 'bar', 'bar', 'bar', 'baz', 'baz', 'baz', 'baz', 'foo', 'foo', 'foo', 'foo', 'qux', 'qux', 'qux', 'qux']),\n", - " np.array(['one', 'one', 'two', 'two', 'one', 'one', 'two', 'two', 'one', 'one', 'two', 'two', 'one', 'one', 'two', 'two']),\n", - " np.array(['X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y'])]\n", - "tuples = list(zip(*arrays))\n", - "index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second', 'third',])\n", - "\n", - "df_col = pd.DataFrame(np.random.randn(3, 16), index=['A', 'B', 'C'], columns=index)\n", - "df_col" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Pivots again automatically applied\n", - "PerspectiveWidget(df_col)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Pivot Table (Row and Column Pivots)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pt = pd.pivot_table(df, values = 'Discount', index=['Country','Region'], columns = ['Category', 'Segment'])\n", - "pt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "PerspectiveWidget(pt)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### More pivot examples" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "arrays = {'A':['bar', 'bar', 'bar', 'bar', 'baz', 'baz', 'baz', 'baz', 'foo', 'foo', 'foo', 'foo', 'qux', 'qux', 'qux', 'qux'],\n", - " 'B':['one', 'one', 'two', 'two', 'one', 'one', 'two', 'two', 'one', 'one', 'two', 'two', 'one', 'one', 'two', 'two'],\n", - " 'C':['X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y'],\n", - " 'D':np.arange(16)}\n", - "\n", - "df2 = pd.DataFrame(arrays)\n", - "df2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df2_pivot = df2.pivot_table(values=['D'], index=['A'], columns=['B','C'], aggfunc={'D':'count'})\n", - "df2_pivot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "PerspectiveWidget(df2_pivot)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pt = pd.pivot_table(df, values = ['Discount','Sales'], index=['Country','Region'],aggfunc={'Discount':'count','Sales':'sum'},columns=[\"State\",\"Quantity\"])\n", - "pt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "PerspectiveWidget(pt)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.9" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": { - "a9a4112df5ab4063a111fe092dcad2c0": { - "model_module": "@finos/perspective-jupyterlab", - "model_module_version": "~0.5", - "model_name": "PerspectiveModel", - "state": { - "_model_module_version": "~0.5", - "_view_module_version": "~0.5", - "aggregates": null, - "split_by": [ - "first", - "second", - "third" - ], - "columns": [ - "value" - ], - "dark": null, - "group_by": [ - "index" - ], - "sort": null - } - }, - "cee49dbaf8bd4aca9a2d28f933707e47": { - "model_module": "@finos/perspective-jupyterlab", - "model_module_version": "~0.5", - "model_name": "PerspectiveModel", - "state": { - "_model_module_version": "~0.5", - "_view_module_version": "~0.5", - "aggregates": null, - "split_by": [ - "State", - "Quantity" - ], - "columns": [ - "Discount", - "Sales" - ], - "dark": null, - "group_by": [ - "Country", - "Region" - ], - "sort": null - } - }, - "d04d6a42b97349cab667619913527df0": { - "model_module": "@finos/perspective-jupyterlab", - "model_module_version": "~0.5", - "model_name": "PerspectiveModel", - "state": { - "_model_module_version": "~0.5", - "_view_module_version": "~0.5", - "aggregates": null, - "split_by": [ - "Category", - "Segment" - ], - "columns": [ - "value" - ], - "dark": null, - "group_by": [ - "Country", - "Region" - ], - "sort": null - } - }, - "f3a5f4e1569f431da25c4e2274f8c662": { - "model_module": "@finos/perspective-jupyterlab", - "model_module_version": "~0.5", - "model_name": "PerspectiveModel", - "state": { - "_model_module_version": "~0.5", - "_view_module_version": "~0.5", - "aggregates": null, - "split_by": null, - "columns": [ - "index", - "Country", - "Region", - "Row ID", - "Order ID", - "Order Date", - "Ship Date", - "Ship Mode", - "Customer ID", - "Segment", - "City", - "State", - "Postal Code", - "Product ID", - "Category", - "Sub-Category", - "Sales", - "Quantity", - "Discount", - "Profit" - ], - "dark": null, - "group_by": [ - "Country", - "Region" - ], - "sort": null - } - }, - "f5ad0e69bd6f4b47b66d7cc079892d28": { - "model_module": "@finos/perspective-jupyterlab", - "model_module_version": "~0.5", - "model_name": "PerspectiveModel", - "state": { - "_model_module_version": "~0.5", - "_view_module_version": "~0.5", - "aggregates": null, - "split_by": [ - "B", - "C" - ], - "columns": [ - "value" - ], - "dark": null, - "group_by": [ - "A" - ], - "sort": null - } - } - }, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/jupyter-notebooks/table_tutorial.ipynb b/examples/jupyter-notebooks/table_tutorial.ipynb deleted file mode 100644 index c45c6ba108..0000000000 --- a/examples/jupyter-notebooks/table_tutorial.ipynb +++ /dev/null @@ -1,422 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using `perspective.Table`\n", - "\n", - "This notebook provides an overview of `perspective.Table`, Perspective's core component that allows for lightning-fast data loading, query, update, and transformation.\n", - "\n", - "Tables can be used alone to manage datasets, or connected for data to flow quickly between multiple tables. Outside of a Jupyter context, Perspective tables can be used to create efficient [servers](https://github.com/finos/perspective/tree/master/examples/tornado-python) which allow data to be hosted and viewed by clients in the browser using Perspective's Javascript library." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from perspective import Table\n", - "from datetime import date, datetime\n", - "import numpy as np\n", - "import pandas as pd\n", - "import requests" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Supported Data Formats\n", - "\n", - "Perspective supports 6 core data types: `int`, `float`, `str`, `bool`, `datetime`, and `date`, and several data formats:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Pandas DataFrames\n", - "df = pd.DataFrame({\n", - " \"a\": np.arange(0, 2),\n", - " \"b\": np.array([\"a\", \"b\"], dtype=object),\n", - " \"nullable\": [1.5, np.nan], # perspective handles `None` and `np.nan` values\n", - " \"mixed\": [None, 1]\n", - "})\n", - "\n", - "# Column-oriented\n", - "data = {\n", - " \"int\": [i for i in range(4)],\n", - " \"float\": [i * 1.25 for i in range(4)],\n", - " \"str\": [\"a\", \"b\", \"c\", \"d\"],\n", - " \"bool\": [True, False, True, False],\n", - " \"date\": [date.today() for i in range(4)],\n", - " \"datetime\": [datetime.now() for i in range(4)]\n", - "}\n", - "\n", - "# Row-oriented\n", - "rows = [{\"a\": 1, \"b\": True}, {\"a\": 2, \"b\": False}]\n", - "\n", - "# CSV strings\n", - "csv = df.to_csv()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Schemas\n", - "\n", - "To explicitly specify data types for columns, create a schema (a `dict` of `str` column names to data types):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "schema = {\n", - " \"int\": float,\n", - " \"float\": int,\n", - " \"str\": str,\n", - " \"bool\": bool,\n", - " \"date\": datetime,\n", - " \"datetime\": datetime\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Creating a Table\n", - "\n", - "A Table can be created by passing in a dataset or a schema, like the ones created above:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# From a dataset\n", - "table = Table(data)\n", - "\n", - "# Or a dataframe\n", - "df_table = Table(df)\n", - "\n", - "# Or a CSV\n", - "csv_table = Table(csv)\n", - "\n", - "# tables can be created from schema\n", - "table2 = Table(schema)\n", - "assert table2.size() == 0\n", - "\n", - "# constructing a table with an index, which is a column name to be used as the primary key\n", - "indexed = Table(data, index=\"str\")\n", - "\n", - "# or a limit, which is a total cap on the number of rows in the table - updates past `limit` overwite at row 0\n", - "limited = Table(data, limit=2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using the Table\n", - "\n", - "A Table has several queryable properties:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# schema() returns a mapping of column names to data types\n", - "display(\"Table schema:\", table.schema())\n", - "\n", - "# size() returns the number of rows in the table\n", - "display(\"Table has {} rows\".format(table.size()))\n", - "\n", - "# columns() returns a List of the table's column names\n", - "display(\"Table columns:\", table.columns())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Updating with new data\n", - "\n", - "To update or stream new data into the Table, call `table.update(data)`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# you can update all columns\n", - "table.update(data)\n", - "print(\"after update:\", table.size())\n", - "\n", - "# or however many you'd like\n", - "table.update({\n", - " \"int\": [5, 6, 7],\n", - " \"str\": [\"x\", \"y\", \"z\"]\n", - "})\n", - "\n", - "# but you cannot add new columns through updating - create a new Table instead\n", - "try:\n", - " table.update({\n", - " \"abcd\": [1]\n", - " })\n", - "except:\n", - " pass\n", - "\n", - "# updates on unindexed tables always append\n", - "print(\"after append:\", table.size())\n", - "\n", - "# updates on indexed tables should include the primary key - the new data overwrites at the row specified by the primary key\n", - "indexed.update([{\"str\": \"b\", \"int\": 100}])\n", - "print(\"after indexed partial update:\", indexed.size())\n", - "\n", - "# without a primary key, the update appends to the end of the dataset\n", - "indexed.update([{\"int\": 101}])\n", - "print(\"after indexed append:\", indexed.size())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Queries and transformations using `View`\n", - "\n", - "`table.view()` allows you to apply various pivots, aggregates, sorts, filters, column selections, and expression computations on the Table, as well as return the results in a variety of output data formats.\n", - "\n", - "To create a view, call the `view()` method on an existing table:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "view = table.view() # a view with zero transformations - returns the dataset as passed in\n", - "\n", - "# view metadata\n", - "print(\"View has {} rows and {} columns\".format(view.num_rows(), view.num_columns()))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Applying transformations\n", - "\n", - "To apply transformations, pass in the relevant `kwargs` into the constructor:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pivoted = table.view(group_by=[\"int\"], split_by=[\"str\"]) # group and split the underlying dataset\n", - "\n", - "aggregated = table.view(group_by=[\"int\"], aggregates={\"float\": \"avg\"}) # specify aggregations for individual columns\n", - "\n", - "subset = table.view(columns=[\"float\"]) # show only the columns you're interested in\n", - "\n", - "sorted_view = table.view(sort=[[\"str\", \"desc\"], [\"int\", \"asc\"]]) # sort on a specific column, or multiple columns\n", - "\n", - "filtered = table.view(filter=[[\"int\", \">\", 2]]) # filter the dataset on a specific value\n", - "\n", - "expressions = table.view(expressions={ '\"int\" + \"float\"': '\"int\" + \"float\" / 100' }) # calculate arbitrary expressions over the dataset" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Serializing Data\n", - "\n", - "Views are used to serialize data to the user in several formats:\n", - "- `to_records`: outputs a list of dictionaries, each of which is a single row\n", - "- `to_dict`: outputs a dictionary of lists, each string key the name of a column\n", - "- `to_numpy`: outputs a dictionary of numpy arrays\n", - "- `to_df`: outputs a `pandas.DataFrame`\n", - "- `to_arrow`: outputs an Apache Arrow binary, which can be passed into another `perspective.Table` to create a copy of the first table" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "rows = view.to_records()\n", - "\n", - "columnar = view.to_dict()\n", - "\n", - "np_out = view.to_numpy()\n", - "\n", - "df_out = view.to_df()\n", - "\n", - "arrow_out = view.to_arrow()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Data from pivoted or otherwise transformed views reflect the state of the transformed dataset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "filtered_df = filtered.to_df()\n", - "filtered_df" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If the table is updated with data, views are automatically notified of the updates:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "v1 = table.view()\n", - "print(\"v1 has {} rows and {} columns\".format(v1.num_rows(), v1.num_columns()))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "table.update({\"int\": [100, 200, 300, 400]})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"v1 has {} rows and {} columns\".format(v1.num_rows(), v1.num_columns()))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using callbacks to connect `Table` instances\n", - "\n", - "Custom callback functions can be applied on the `Table` and `View` instances.\n", - "\n", - "The most useful is `View.on_update`, which triggers a callback after the Table has been updated:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# The `delta` property is an Arrow binary containing updated rows\n", - "def callback(port_id, delta):\n", - " new_table = Table(delta)\n", - " display(new_table.view().to_dict())\n", - " \n", - "table = Table(data)\n", - "view = table.view()\n", - "\n", - "# Register the callback with `mode=\"row\"` to enable pushing back updated data\n", - "view.on_update(callback, mode=\"row\")\n", - "\n", - "# Update will trigger the callback\n", - "table.update({\n", - " \"int\": [1, 3],\n", - " \"str\": [\"abc\", \"def\"]\n", - "})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Because the callback can be triggered with a _copy_ of the updated data, `on_update` allows you to connect together multiple tables that all share state quickly and dependably:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a table and a view\n", - "t1 = Table(data)\n", - "v1 = t1.view()\n", - "\n", - "# And a new table that feeds from `t1`\n", - "t2 = Table(t1.schema())\n", - "\n", - "# And a callback that updates `t2` whenever `t1` updates\n", - "def cb(port_id, delta):\n", - " t2.update(delta)\n", - " \n", - "# register the callback\n", - "v1.on_update(cb, mode=\"row\")\n", - " \n", - "# update t1, which updates t2 automatically\n", - "t1.update(data)\n", - "\n", - "# t2 now has data after t1 is updated\n", - "t2.view().to_df()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/jupyter-notebooks/widget_tutorial.ipynb b/examples/jupyter-notebooks/widget_tutorial.ipynb deleted file mode 100644 index 32ad6c7ff0..0000000000 --- a/examples/jupyter-notebooks/widget_tutorial.ipynb +++ /dev/null @@ -1,257 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualizing Data with `PerspectiveWidget`\n", - "\n", - "`PerspectiveWidget` offers a powerful widget that allows you to display, query, transform, and visualize your data interactively within a Jupyter Notebook. Both technical and non-technical users can use `PerspectiveWidget` to interact with datasets with a minimal amount of code, while developers can take advantage of the simple and powerful API to create their own notebooks and Voila applications using `PerspectiveWidget`.\n", - "\n", - "For an overview of Perspective's core concepts such as the Table, View, Schema, etc., see `table_tutorial.ipynb` in the [`jupyter-notebooks`](https://github.com/finos/perspective/tree/master/examples/jupyter-notebooks) example folder." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from perspective import Table, PerspectiveWidget\n", - "from datetime import date, datetime\n", - "import pandas as pd" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Creating a `PerspectiveWidget`\n", - "\n", - "To create a widget instance, simply call the PerspectiveWidget constructor with your dataset:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Use superstore.arrow as our example dataset\n", - "import requests\n", - "\n", - "# Download the arrow\n", - "url = \"https://unpkg.com/@jpmorganchase/perspective-examples@0.2.0-beta.2/build/superstore.arrow\"\n", - "req = requests.get(url)\n", - "arrow = req.content" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a table\n", - "table = Table(arrow)\n", - "view = table.view()\n", - "df = view.to_df()\n", - "display(df.shape, df.dtypes)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once the widget is created, you can interact with it like any other ``, using drag-and-drop controls to apply pivots, filters, sorts, etc." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a widget, which will default to showing a datagrid\n", - "widget = PerspectiveWidget(df)\n", - "widget" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For more control over your dataset (if you need to update the data frequently, etc.), create a `perspective.Table` and pass its handle into the `PerspectiveWidget` container:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a table so we can index the dataset\n", - "table = Table(df, index=\"Row ID\")\n", - "widget2 = PerspectiveWidget(table)\n", - "widget2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When the `table` instance is updated with new data, the widget will reflect the new data:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Widget will display new data when this cell is called\n", - "table.update({\n", - " \"Row ID\": [1],\n", - " \"Order ID\": [\"new order!\"]\n", - "})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Configuring `PerspectiveWidget`\n", - "\n", - "`PerspectiveWidget`'s initializer can be configured with the following options to control its output:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Apply pivots and aggregates, and specify columns to show\n", - "PerspectiveWidget(df, columns=[\"Sales\", \"Category\"], group_by=[\"State\", \"City\"], split_by=[\"Segment\"], aggregates={\"Category\": \"dominant\"})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Sorts and filters\n", - "PerspectiveWidget(df, columns=[\"Order Date\", \"State\", \"Sales\", \"Profit\"], sort=[[\"Order Date\", \"desc\"]], filter=[[\"State\", \"==\", \"Texas\"]])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Calculate expression columns\n", - "PerspectiveWidget(df, columns=[\"expression\"], expressions={ 'expression': '// expression\\n\"Sales\" * \"Profit\" * (sqrt(144))' })" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Show a chart by default\n", - "PerspectiveWidget(df, plugin=\"Y Bar\", columns=[\"expression\"], group_by=[\"State\"], expressions={ 'expression': '//expression\\n\"Sales\" / \"Profit\"' }, sort=[[\"expression\", \"desc\"]])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`save()` and `restore()` can be called to serialize a `PerspectiveWidget`'s entire state, and restore that state later on:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a complex widget\n", - "w = PerspectiveWidget(\n", - " df,\n", - " plugin=\"Heatmap\",\n", - " columns=[\"Sales\"],\n", - " group_by=[\"State\"],\n", - " sort=[[\"Sales\", \"desc\"]]\n", - ")\n", - "\n", - "config = w.save()\n", - "\n", - "# Restore the config\n", - "PerspectiveWidget(df, **config)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using `PerspectiveWidget` with `ipywidgets`\n", - "\n", - "Because `PerspectiveWidget` is itself an `ipywidget`, it can be easily included in any custom widget layouts and inside `ipywidget` elements:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import ipywidgets as widgets\n", - "\n", - "w1 = PerspectiveWidget(df, **config)\n", - "w2 = PerspectiveWidget(df, **config)\n", - "\n", - "w2.columns = [\"Profit\"]\n", - "w2.sort = [[\"Profit\", \"desc\"]]\n", - "\n", - "# Create a tab widget with some PerspectiveWidgets inside\n", - "tab = widgets.Tab()\n", - "tab.children = [w1, w2]\n", - "tab.titles = [\"Sales by State\", \"Profit by State\"]\n", - "tab" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`PerspectiveWidget` also works well with Jupyterlab's built in layout system - right click on any widget and click `Create New View for Output`, and you can resize and reposition that widget to anywhere inside the Jupyterlab window.\n", - "\n", - "For more complex layouts that leverage the high performance and throughput of `perspective-python`, `perspective-workspace` allows the creation of powerful and complex front-end layouts, and integrates perfectly with `perspective-python` through the use of a Tornado server. For more details, see [this example](https://github.com/finos/perspective/tree/master/examples/workspace-editing-python)." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples/python-aiohttp/package.json b/examples/python-aiohttp/package.json index cb20e9374e..36044f3a92 100644 --- a/examples/python-aiohttp/package.json +++ b/examples/python-aiohttp/package.json @@ -19,6 +19,6 @@ "devDependencies": { "@finos/perspective-webpack-plugin": "workspace:^", "npm-run-all": "^4.1.3", - "rimraf": "^2.5.2" + "rimraf": "^6" } } diff --git a/examples/python-starlette/package.json b/examples/python-starlette/package.json index b9ac8c0fee..fc3449c06e 100644 --- a/examples/python-starlette/package.json +++ b/examples/python-starlette/package.json @@ -19,6 +19,6 @@ "devDependencies": { "@finos/perspective-webpack-plugin": "workspace:^", "npm-run-all": "^4.1.3", - "rimraf": "^2.5.2" + "rimraf": "^6" } } diff --git a/examples/python-tornado-streaming/package.json b/examples/python-tornado-streaming/package.json index 406fb3a62e..aa079a56ed 100644 --- a/examples/python-tornado-streaming/package.json +++ b/examples/python-tornado-streaming/package.json @@ -19,6 +19,6 @@ "devDependencies": { "@finos/perspective-webpack-plugin": "workspace:^", "npm-run-all": "^4.1.3", - "rimraf": "^2.5.2" + "rimraf": "^6" } } diff --git a/examples/python-tornado/package.json b/examples/python-tornado/package.json index 756b992ff1..848fd5cba3 100644 --- a/examples/python-tornado/package.json +++ b/examples/python-tornado/package.json @@ -19,6 +19,6 @@ "devDependencies": { "@finos/perspective-webpack-plugin": "workspace:^", "npm-run-all": "^4.1.3", - "rimraf": "^2.5.2" + "rimraf": "^6" } } diff --git a/examples/workspace/package.json b/examples/workspace/package.json index b3525bc452..418f44b652 100644 --- a/examples/workspace/package.json +++ b/examples/workspace/package.json @@ -20,6 +20,6 @@ "@finos/perspective-webpack-plugin": "workspace:^", "http-server": "^14.1.1", "npm-run-all": "^4.1.3", - "rimraf": "^2.5.2" + "rimraf": "^6" } } diff --git a/examples/workspace/src/index.css b/examples/workspace/src/index.css index 7e13ee874f..06ad799ee6 100644 --- a/examples/workspace/src/index.css +++ b/examples/workspace/src/index.css @@ -1,3 +1,6 @@ +@import "~@finos/perspective-workspace/dist/css/pro-dark.css"; +@import "~@finos/perspective-viewer/dist/css/themes.css"; + body { display: flex; flex-direction: column; diff --git a/package.json b/package.json index 34b3d1f7db..2433999d73 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "preinstall": "npx only-allow pnpm", "postinstall": "npm-run-all postinstall:*", "postinstall:emsdk": "node tools/perspective-scripts/install_emsdk.mjs", - "postinstall:playwright": "npx playwright install --with-deps", + "postinstall:playwright": "npx playwright install --with-deps chromium", "postinstall:vscode": "cp -n ./.vscode/settings.default.json ./.vscode/settings.json || true", "install_llvm": "node tools/perspective-scripts/install_llvm.mjs", "install_pyodide": "node tools/perspective-scripts/install_pyodide.mjs", @@ -95,7 +95,7 @@ "_wheel_python": "node tools/perspective-scripts/_wheel_python.mjs", "_requires_python": "node tools/perspective-scripts/_requires_python.mjs", "setup": "node tools/perspective-scripts/setup.mjs", - "docs": "cargo doc --no-deps", + "docs": "node tools/perspective-scripts/docs.mjs", "test": "node tools/perspective-scripts/test.mjs", "test:jupyter": "pnpm run --recursive --filter @finos/perspective-jupyterlab test:jupyter", "test_js": "node tools/perspective-scripts/test_js.mjs", diff --git a/packages/perspective-esbuild-plugin/wasm.js b/packages/perspective-esbuild-plugin/wasm.js index 1110ab34fd..b2403c22e5 100644 --- a/packages/perspective-esbuild-plugin/wasm.js +++ b/packages/perspective-esbuild-plugin/wasm.js @@ -90,7 +90,7 @@ exports.WasmPlugin = function WasmPlugin(inline) { let updated = false; for (const key of KEYSET) { const symbol = contents.match( - new RegExp(`${key}\\(([a-zA-Z0-9_]+?)\\)`) + new RegExp(`${key}\\(([a-zA-Z0-9_\$]+?)\\)`) ); if (symbol?.[1]) { @@ -100,7 +100,10 @@ exports.WasmPlugin = function WasmPlugin(inline) { ); contents = contents.replace( - new RegExp(`${key}\\(([a-zA-Z0-9_]+?)\\)`, "g"), + new RegExp( + `${key}\\(([a-zA-Z0-9_\$]+?)\\)`, + "g" + ), `"${filename[1]}"` ); } diff --git a/packages/perspective-esbuild-plugin/worker.js b/packages/perspective-esbuild-plugin/worker.js index c87e73009d..1606cc043f 100644 --- a/packages/perspective-esbuild-plugin/worker.js +++ b/packages/perspective-esbuild-plugin/worker.js @@ -161,7 +161,7 @@ exports.WorkerPlugin = function WorkerPlugin(options = {}) { if (file.endsWith(".js")) { let contents = fs.readFileSync(file).toString(); const symbol = contents.match( - /__PSP_INLINE_WORKER__\(([a-zA-Z0-9_]+?)\)/ + /__PSP_INLINE_WORKER__\(([a-zA-Z0-9_\$]+?)\)/ ); if (symbol?.[1]) { const filename = contents.match( @@ -169,7 +169,7 @@ exports.WorkerPlugin = function WorkerPlugin(options = {}) { ); contents = contents.replace( - /__PSP_INLINE_WORKER__\([a-zA-Z0-9_]+?\)/, + /__PSP_INLINE_WORKER__\([a-zA-Z0-9_\$]+?\)/, `"${filename[1]}"` ); diff --git a/packages/perspective-jupyterlab/package.json b/packages/perspective-jupyterlab/package.json index 6924fe768e..61ed6892c8 100644 --- a/packages/perspective-jupyterlab/package.json +++ b/packages/perspective-jupyterlab/package.json @@ -49,7 +49,8 @@ "@jupyterlab/builder": "^4", "copy-webpack-plugin": "~12", "@prospective.co/procss": "^0.1.16", - "cpy": "^9.0.1" + "cpy": "^9.0.1", + "zx": "^8.1.8" }, "jupyterlab": { "webpackConfig": "./webpack.config.js", diff --git a/packages/perspective-jupyterlab/test/config/jupyter/globalSetup.ts b/packages/perspective-jupyterlab/test/config/jupyter/globalSetup.ts index 2000913a0a..37bea2904a 100644 --- a/packages/perspective-jupyterlab/test/config/jupyter/globalSetup.ts +++ b/packages/perspective-jupyterlab/test/config/jupyter/globalSetup.ts @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { start_jlab, kill_jlab } from "./jlab_start.mjs"; +import { start_jlab, kill_jlab } from "./jlab_start.ts"; async function globalSetup() { // Start Jupyterlab in the background diff --git a/packages/perspective-jupyterlab/test/config/jupyter/globalTeardown.ts b/packages/perspective-jupyterlab/test/config/jupyter/globalTeardown.ts index 8b88839b8c..a8420607ab 100644 --- a/packages/perspective-jupyterlab/test/config/jupyter/globalTeardown.ts +++ b/packages/perspective-jupyterlab/test/config/jupyter/globalTeardown.ts @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { kill_jlab } from "./jlab_start.mjs"; +import { kill_jlab } from "./jlab_start.ts"; async function globalTeardown() { await kill_jlab(); diff --git a/packages/perspective-jupyterlab/test/config/jupyter/jlab_start.mjs b/packages/perspective-jupyterlab/test/config/jupyter/jlab_start.ts similarity index 95% rename from packages/perspective-jupyterlab/test/config/jupyter/jlab_start.mjs rename to packages/perspective-jupyterlab/test/config/jupyter/jlab_start.ts index 85f56f4e8d..d62be2df02 100644 --- a/packages/perspective-jupyterlab/test/config/jupyter/jlab_start.mjs +++ b/packages/perspective-jupyterlab/test/config/jupyter/jlab_start.ts @@ -13,7 +13,7 @@ import * as path from "path"; import { get } from "http"; import { spawn } from "child_process"; -import sh from "@finos/perspective-scripts/sh.mjs"; +import "zx/globals"; const PACKAGE_ROOT = path.join(__dirname, "..", "..", ".."); @@ -22,7 +22,7 @@ const PACKAGE_ROOT = path.join(__dirname, "..", "..", ".."); */ export const kill_jlab = () => { console.log("\n-- Cleaning up Jupyterlab process"); - sh`ps aux | grep -i '[j]upyter-lab --no-browser' | awk '{print $2}' | xargs kill -9 && echo "[perspective-jupyterlab] JupyterLab process terminated"`.runSync(); + $`ps aux | grep -i '[j]upyter-lab --no-browser' | awk '{print $2}' | xargs kill -9 && echo "[perspective-jupyterlab] JupyterLab process terminated"`; }; /** diff --git a/packages/perspective-jupyterlab/test/jupyter/utils.mjs b/packages/perspective-jupyterlab/test/jupyter/utils.mjs index c315da0d8d..2e47a0393b 100644 --- a/packages/perspective-jupyterlab/test/jupyter/utils.mjs +++ b/packages/perspective-jupyterlab/test/jupyter/utils.mjs @@ -13,7 +13,7 @@ import { test, expect } from "@playwright/test"; import fs from "fs"; import path from "path"; -import rimraf from "rimraf"; +import * as rimraf from "rimraf"; import { fileURLToPath } from "url"; import { dirname } from "path"; @@ -54,6 +54,7 @@ const generate_notebook = (notebook_name, cells) => { outputs: [], source: [ "import perspective\n", + "import perspective.widget\n", "import pandas as pd\n", "import numpy as np\n", "arrow_data = None\n", diff --git a/packages/perspective-jupyterlab/test/jupyter/widget.spec.mts b/packages/perspective-jupyterlab/test/jupyter/widget.spec.mts index 32edc09bde..cdcffae342 100644 --- a/packages/perspective-jupyterlab/test/jupyter/widget.spec.mts +++ b/packages/perspective-jupyterlab/test/jupyter/widget.spec.mts @@ -39,7 +39,7 @@ describe_jupyter( test_jupyter( "Loads data", [ - "w = perspective.PerspectiveWidget(arrow_data, columns=['f64', 'str', 'datetime'])", + "w = perspective.widget.PerspectiveWidget(arrow_data, columns=['f64', 'str', 'datetime'])", "w", ], async ({ page }) => { @@ -63,7 +63,7 @@ describe_jupyter( "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table, columns=['f64', 'str', 'datetime'])", + "w = perspective.widget.PerspectiveWidget(table, columns=['f64', 'str', 'datetime'])", ].join("\n"), "w", "table.update(arrow_data)", @@ -90,7 +90,7 @@ describe_jupyter( "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table, columns=['f64', 'str', 'datetime'])", + "w = perspective.widget.PerspectiveWidget(table, columns=['f64', 'str', 'datetime'])", ].join("\n"), "w", ], @@ -119,7 +119,7 @@ describe_jupyter( "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table, columns=['f64', 'str', 'datetime'], settings=False)", + "w = perspective.widget.PerspectiveWidget(table, columns=['f64', 'str', 'datetime'], settings=False)", ].join("\n"), "w", ], @@ -140,7 +140,7 @@ describe_jupyter( "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table, plugin_config={'edit_mode': 'EDIT'})", + "w = perspective.widget.PerspectiveWidget(table, plugin_config={'edit_mode': 'EDIT'})", ].join("\n"), "w", ], @@ -158,7 +158,7 @@ describe_jupyter( "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table)", + "w = perspective.widget.PerspectiveWidget(table)", ].join("\n"), "w", ], @@ -184,7 +184,7 @@ describe_jupyter( "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table)", + "w = perspective.widget.PerspectiveWidget(table)", ].join("\n"), "w", ], @@ -213,7 +213,7 @@ describe_jupyter( "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table)", + "w = perspective.widget.PerspectiveWidget(table)", ].join("\n"), "w", ], @@ -304,7 +304,7 @@ w.theme = "Pro Dark"` "server = perspective.Server()", "client = server.new_local_client()", "table = client.table(arrow_data)", - "w = perspective.PerspectiveWidget(table)", + "w = perspective.widget.PerspectiveWidget(table)", ].join("\n"), "w", ], @@ -389,7 +389,7 @@ assert w.theme == "Pro Dark" test_jupyter( "Edit from frontend - end to end", [ - 'w = perspective.PerspectiveWidget({"a": [True, False, True], "b": ["abc", "def", "ghi"]}, index="b", plugin_config={"edit_mode": "EDIT"})', + 'w = perspective.widget.PerspectiveWidget({"a": [True, False, True], "b": ["abc", "def", "ghi"]}, index="b", plugin_config={"edit_mode": "EDIT"})', "w", ], async ({ page }) => { @@ -434,7 +434,7 @@ assert w.theme == "Pro Dark" server = perspective.Server() client = server.new_local_client() table = client.table(arrow_data) -w = perspective.PerspectiveWidget(table) +w = perspective.widget.PerspectiveWidget(table) config = w.save() perpsective.PerspectiveWidget(df, **config) ` diff --git a/packages/perspective-workspace/src/js/workspace/workspace.js b/packages/perspective-workspace/src/js/workspace/workspace.js index dec95991de..95cb1899ab 100644 --- a/packages/perspective-workspace/src/js/workspace/workspace.js +++ b/packages/perspective-workspace/src/js/workspace/workspace.js @@ -464,14 +464,20 @@ export class PerspectiveWorkspace extends SplitPanel { async duplicate(widget) { if (this.dockpanel.mode === "single-document") { - this.toggleSingleDocument(widget); + const _task = await this._maximizedWidget.viewer.toggleConfig( + false + ); + this._unmaximize(); } + const config = await widget.save(); + config.settings = false; config.title = config.title ? `${config.title} (*)` : ""; const duplicate = this._createWidgetAndNode({ config }); if (config.linked) { this._linkWidget(duplicate); } + if (widget.master) { const index = this.masterPanel.widgets.indexOf(widget) + 1; this.masterPanel.insertWidget(index, duplicate); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b69c5f1367..2e55bf093f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -316,8 +316,8 @@ importers: specifier: ^4.1.3 version: 4.1.5 rimraf: - specifier: ^2.5.2 - version: 2.7.1 + specifier: ^6 + version: 6.0.1 examples/python-starlette: dependencies: @@ -347,8 +347,8 @@ importers: specifier: ^4.1.3 version: 4.1.5 rimraf: - specifier: ^2.5.2 - version: 2.7.1 + specifier: ^6 + version: 6.0.1 examples/python-tornado: dependencies: @@ -378,8 +378,8 @@ importers: specifier: ^4.1.3 version: 4.1.5 rimraf: - specifier: ^2.5.2 - version: 2.7.1 + specifier: ^6 + version: 6.0.1 examples/python-tornado-streaming: dependencies: @@ -409,8 +409,8 @@ importers: specifier: ^4.1.3 version: 4.1.5 rimraf: - specifier: ^2.5.2 - version: 2.7.1 + specifier: ^6 + version: 6.0.1 examples/react-example: dependencies: @@ -516,8 +516,8 @@ importers: specifier: ^4.1.3 version: 4.1.5 rimraf: - specifier: ^2.5.2 - version: 2.7.1 + specifier: ^6 + version: 6.0.1 packages/perspective-cli: dependencies: @@ -603,6 +603,9 @@ importers: cpy: specifier: ^9.0.1 version: 9.0.1 + zx: + specifier: ^8.1.8 + version: 8.1.9 packages/perspective-viewer-d3fc: dependencies: @@ -767,6 +770,8 @@ importers: specifier: ^0.1.16 version: 0.1.16 + rust/generate-metadata: {} + rust/perspective: {} rust/perspective-js: @@ -784,6 +789,9 @@ importers: '@finos/perspective-esbuild-plugin': specifier: workspace:^ version: link:../../packages/perspective-esbuild-plugin + '@finos/perspective-metadata': + specifier: workspace:^ + version: link:../generate-metadata '@finos/perspective-test': specifier: workspace:^ version: link:../../tools/perspective-test @@ -831,6 +839,9 @@ importers: '@finos/perspective-esbuild-plugin': specifier: workspace:^ version: link:../../packages/perspective-esbuild-plugin + '@finos/perspective-metadata': + specifier: workspace:^ + version: link:../generate-metadata '@finos/perspective-test': specifier: workspace:^ version: link:../../tools/perspective-test @@ -950,13 +961,20 @@ importers: specifier: 3.0.0 version: 3.0.0 zx: - specifier: 8.1.8 - version: 8.1.8 + specifier: ^8.1.8 + version: 8.1.9 - tools/perspective-scripts: {} + tools/perspective-scripts: + devDependencies: + zx: + specifier: ^8.1.8 + version: 8.1.9 tools/perspective-test: dependencies: + glob: + specifier: ^11 + version: 11.0.0 xml-formatter: specifier: 2.4.0 version: 2.4.0 @@ -7547,11 +7565,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -8710,8 +8723,8 @@ packages: zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - zx@8.1.8: - resolution: {integrity: sha512-m8s48skYQ8EcRz9KXfc7rZCjqlZevOGiNxq5tNhDiGnhOvXKRGxVr+ajUma9B6zxMdHGSSbnjV/R/r7Ue2xd+A==} + zx@8.1.9: + resolution: {integrity: sha512-UHuLHphHmsBYKkAchkSrEN4nzDyagafqC9HUxtc1J7eopaScW6H9dsLJ1lmkAntnLtDTGoM8fa+jrJrXiIfKFA==} engines: {node: '>= 12.17.0'} hasBin: true @@ -17642,10 +17655,6 @@ snapshots: reusify@1.0.4: {} - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -19029,7 +19038,7 @@ snapshots: zwitch@2.0.4: {} - zx@8.1.8: + zx@8.1.9: optionalDependencies: '@types/fs-extra': 11.0.4 '@types/node': 22.5.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 365b1b95c2..09201bb8d3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - "tools/perspective-scripts" - "tools/perspective-bench" - "cpp/perspective" + - "packages/perspective-client" - "packages/perspective-esbuild-plugin" - "packages/perspective-viewer-datagrid" - "packages/perspective-viewer-d3fc" @@ -11,6 +12,7 @@ packages: - "packages/perspective-jupyterlab" - "packages/perspective-webpack-plugin" - "packages/perspective-cli" + - "rust/generate-metadata" - "rust/perspective" - "rust/perspective-js" - "rust/perspective-viewer" diff --git a/rust/generate-metadata/Cargo.toml b/rust/generate-metadata/Cargo.toml new file mode 100644 index 0000000000..c660f95c37 --- /dev/null +++ b/rust/generate-metadata/Cargo.toml @@ -0,0 +1,41 @@ +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +# ┃ 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). ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +[package] +name = "perspective-metadata" +description = "A CLI utility for generating Perspective project metadata like TypeScript definitions." +edition = "2021" +publish = false + +[[bin]] +name = "perspective_metadata" +path = "main.rs" +bench = false + +[dependencies.ts-rs] +version = "10.0.0" +features = ["serde-json-impl", "no-serde-warnings"] + +[dependencies.perspective-server] +path = "../perspective-server" +features = ["external-cpp", "disable-cpp"] + +[dependencies.perspective-client] +path = "../perspective-client" +features = ["external-proto", "omit_metadata"] + +[dependencies.perspective-js] +path = "../perspective-js" +features = ["external-cpp"] + +[dependencies.perspective-viewer] +path = "../perspective-viewer" diff --git a/rust/perspective-viewer/src/rust/main.rs b/rust/generate-metadata/build.mjs similarity index 71% rename from rust/perspective-viewer/src/rust/main.rs rename to rust/generate-metadata/build.mjs index 1e9f70ab4a..7e7f32a9c7 100644 --- a/rust/perspective-viewer/src/rust/main.rs +++ b/rust/generate-metadata/build.mjs @@ -10,42 +10,15 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -#![recursion_limit = "1024"] +import { execSync } from "child_process"; -use std::fmt::Write; +const INHERIT = { + stdio: "inherit", + stderr: "inherit", +}; -use perspective_viewer::config::ViewerConfigUpdate; -use ts_rs::TS; - -pub fn generate_type_bindings() { - ViewerConfigUpdate::export_all().unwrap() -} - -fn generate_exprtk_docs() -> String { - perspective_viewer::exprtk::COMPLETIONS.with(|x| { - x.iter().fold(String::new(), |mut output, rec| { - let _ = write!( - output, - "#### `{}` - -{} - -``` -{} -``` - - ", - rec.label, rec.documentation, rec.insert_text - ); - output - }) - }) +function get_host() { + return /host\: (.+?)$/gm.exec(execSync(`rustc -vV`).toString())[1]; } -fn main() { - if std::env::args().collect::>().len() > 1 { - println!("{}", generate_exprtk_docs()); - } else { - generate_type_bindings(); - } -} +execSync(`PSP_ROOT_DIR=../.. cargo run --target=${get_host()}`, INHERIT); diff --git a/rust/generate-metadata/main.rs b/rust/generate-metadata/main.rs new file mode 100644 index 0000000000..db28d96baf --- /dev/null +++ b/rust/generate-metadata/main.rs @@ -0,0 +1,78 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ 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). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +#![recursion_limit = "1024"] + +//! This module generates metadata for other crates: +//! +//! - `perspective-client` +//! - Add `protobuf-src` dependency +//! - Generate `proto.rs` protobuf client bindings. +//! - `perspective-js` +//! - TypeScript types +//! - Recurisvely set external proto on `perspective-client` +//! - `perspective-server` +//! - Copy `cpp` and `cmake` to local root +//! +//! The `generate-metadata` binary must be run for these assets to be updated in +//! your local dev tree! + +use std::error::Error; +use std::fmt::Write; +use std::fs; + +use perspective_client::config::*; +use perspective_client::{ + OnUpdateOptions, TableInitOptions, UpdateOptions, ViewOnUpdateResp, ViewWindow, +}; +use perspective_viewer::config::ViewerConfigUpdate; +use ts_rs::TS; + +pub fn generate_type_bindings_viewer() -> Result<(), Box> { + Ok(ViewerConfigUpdate::export_all_to( + std::env::current_dir()?.join("../perspective-viewer/src/ts/ts-rs"), + )?) +} + +fn generate_exprtk_docs() -> Result<(), Box> { + let mut txt = "
\n\n# Perspective ExprTK Extensions\n\n".to_string(); + for rec in perspective_client::config::COMPLETIONS { + writeln!( + txt, + "- `{}` {}", + rec.insert_text, + rec.documentation.replace("\n", " "), + )?; + } + + fs::write("../perspective-client/docs/expression_gen.md", txt)?; + Ok(()) +} + +#[doc(hidden)] +pub fn generate_type_bindings_js() -> Result<(), Box> { + let path = std::env::current_dir()?.join("../perspective-js/src/ts/ts-rs"); + ViewWindow::export_all_to(&path)?; + TableInitOptions::export_all_to(&path)?; + ViewConfigUpdate::export_all_to(&path)?; + ViewOnUpdateResp::export_all_to(&path)?; + OnUpdateOptions::export_all_to(&path)?; + UpdateOptions::export_all_to(&path)?; + Ok(()) +} + +fn main() -> Result<(), Box> { + generate_type_bindings_js()?; + generate_type_bindings_viewer()?; + generate_exprtk_docs()?; + Ok(()) +} diff --git a/rust/generate-metadata/package.json b/rust/generate-metadata/package.json new file mode 100644 index 0000000000..6b3db69b7c --- /dev/null +++ b/rust/generate-metadata/package.json @@ -0,0 +1,20 @@ +{ + "name": "@finos/perspective-metadata", + "version": "2.0.1", + "description": "Benchmark utility based on perspective", + "private": true, + "files": [ + "src/**/*", + "perspective-metadata" + ], + "bin": "perspective-metadata", + "repository": { + "type": "git", + "url": "https://github.com/finos/perspective/packages/perspective-metadata" + }, + "scripts": { + "build": "node build.mjs" + }, + "author": "", + "license": "Apache-2.0" +} diff --git a/rust/perspective-client/Cargo.toml b/rust/perspective-client/Cargo.toml index ab3a89ee7f..cf0c8cc811 100644 --- a/rust/perspective-client/Cargo.toml +++ b/rust/perspective-client/Cargo.toml @@ -35,8 +35,14 @@ rustdoc-args = ["--html-in-header", "docs/index.html"] [features] default = [] + +# Should the project build the `proto.rs` via protoc from source or assume it +# already exists? external-proto = ["protobuf-src"] -external-protoc = [] + +# When generating metadata, we can't rely on its existence - so enable this +# to skip metadata generation. This currently only affects docs. +omit_metadata = [] [lib] crate-type = ["rlib"] @@ -66,5 +72,5 @@ default-features = false features = ["prost-derive", "std"] [dependencies.ts-rs] -version = "9.0.1" +version = "10.0.0" features = ["serde-json-impl", "no-serde-warnings"] diff --git a/rust/perspective-client/docs/client/set_loop_callback.md b/rust/perspective-client/docs/client/set_loop_callback.md index d7fbb4f7c8..6fe76e85b5 100644 --- a/rust/perspective-client/docs/client/set_loop_callback.md +++ b/rust/perspective-client/docs/client/set_loop_callback.md @@ -3,5 +3,5 @@ which may be invoked by the Perspective runtime when updates occur. If provided a _loop callback_ function via [`Client::set_loop_callback`], such callback function invocations be passed to the _loop callback_ instead. -[`Client::set_loop_callback`] can be used to control scheduling/conflation -(e.g. by adding a delay), as well as executor integration. +[`Client::set_loop_callback`] can be used to control scheduling/conflation (e.g. +by adding a delay), as well as executor integration. diff --git a/docs/docs/expressions.md b/rust/perspective-client/docs/expressions.md similarity index 58% rename from docs/docs/expressions.md rename to rust/perspective-client/docs/expressions.md index 70d83b03f2..36fec1726c 100644 --- a/docs/docs/expressions.md +++ b/rust/perspective-client/docs/expressions.md @@ -1,28 +1,15 @@ ---- -id: expressions -title: Expression Columns ---- - - - Perspective supports _expression columns_, which are virtual columns calculated -as part of the `View`, optionally using values from its underlying `Table`'s -columns. Such expression columns are defined in Perspective's expression -language, an extended version of +as part of the [`crate::View`], optionally using values from its underlying +[`crate::Table`]'s columns. Such expression columns are defined in Perspective's +expression language, an extended version of [ExprTK](https://github.com/ArashPartow/exprtk), which is itself quite similar (in design and features) to expressions in Excel. ## UI Expression columns can be created in `` by clicking the "New -Column" button at the bottom of the column list (in red below), or via the API by adding the expression -to the `expressions` config key when calling `restore()`. - -
- -
-
+Column" button at the bottom of the column list, or via the API by adding the +expression to the `expressions` config key when calling `viewer.restore()`. By default, such expression columns are not "used", and will appear above the `Table`'s other deselected columns in the column list, with an additional set of @@ -36,10 +23,8 @@ buttons for: expression column i unused. To use the column, just drag/select the column as you would a normal column, -e.g. as a "Filter", "Group By", etc. Expression columns cannot be edited or -updated (as they exist on the `View()` and are generated from the `Table()`'s -_real_ columns). However, they will automatically update whenever their -dependent columns update. +e.g. as a "Filter", "Group By", etc. Expression columns will recalculate +whenever their dependent columns update. ## Perspective Extensions to ExprTK @@ -65,7 +50,7 @@ e.g. `string(x)` to cast a variable `x` to a `string`. Expressions can be _named_ by providing a comment as the first line of the expression. This name will be used in the `` UI when referring to the column, but will also be used in the API when specifying e.g. -`group_by` or `order_by` fields. When creating a new column via +`group_by` or `sort` fields. When creating a new column via ``'s expression editor, new columns will get a default name (which you may delete or change): @@ -76,29 +61,21 @@ referring to the column, but will also be used in the API when specifying e.g. Without such a comment, an expression will show up in the `` API and UI as itself (clipped to a reasonable length for the latter). -#### Referencing `Table()` Columns +#### Referencing [`crate::Table`] Columns -Columns from the `Table()` can be referenced in an expression with _double -quotes_. +Columns from the [`crate::Table`] can be referenced in an expression with +_double quotes_. -``` -// Expected Sales -("Sales" * 10) + "Profit" +```text +// Expected Sales ("Sales" * 10) + "Profit" ``` -
- -
- #### String Literals In contrast to standard ExprTK, string literals are declared with _single quotes_: -``` +```text // Profitable if ("Profit" > 0) { 'Stonks' @@ -107,13 +84,6 @@ if ("Profit" > 0) { } ``` -
- -
- #### Extended Library Perspective adds many of its own functions in addition to `ExprTK`'s standard @@ -129,36 +99,20 @@ functions is available in the Just `2`, as an `integer` (numeric literals currently default to `float` unless cast). -``` +```text integer(2) ``` -
- -
-
- #### Variables -``` +```text // My Column Name var incrementedBy200 := "Sales" + 200; var half := incrementedBy200 / 2; half ``` -
- -
-
- -``` +```text // Complex Expression var upperCustomer := upper("Customer Name"); var separator := concat(upperCustomer, ' | '); @@ -168,17 +122,9 @@ var percentDisplay := concat(combined, '%'); percentDisplay ``` -
- -
-
- #### Conditionals -``` +```text // Conditional var priceAdjustmentDate := date(2016, 6, 18); var finalPrice := "Sales" - "Discount"; @@ -193,10 +139,3 @@ else finalPrice + additionalModifier ``` - -
- -
diff --git a/rust/perspective-client/docs/table.md b/rust/perspective-client/docs/table.md index ed8c798638..2217a0abe6 100644 --- a/rust/perspective-client/docs/table.md +++ b/rust/perspective-client/docs/table.md @@ -23,3 +23,259 @@ The examples in this module are in JavaScript. See perspective docs for the Rust API. + +## Schema and Types + +The mapping of a `Table`'s column names to data types is referred to as a +`schema`. Each column has a unique name and a single data type: + +
+ +```javascript +var schema = { + x: "integer", + y: "string", + z: "boolean", +}; + +const table2 = await worker.table(schema); +``` + +
+
+ +```python +from datetime import date, datetime + +schema = { + "x": "integer", + "y": "string", + "z": "boolean", +} + +table2 = perspective.Table(schema) +``` + +
+
+ +```rust +let data = TableData::Schema(vec![(" a".to_string(), ColumnType::FLOAT)]); +let options = TableInitOptions::default(); +let table = client.table(data.into(), options).await?; +``` + +
+ +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: + +
+ +```javascript +const indexed_table = await perspective.table(data, { index: "a" }); +``` + +
+
+ +```python +indexed_table = perspective.Table(data, index="a"); +``` + +
+ +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: + +
+ +```javascript +const limit_table = await perspective.table(data, { limit: 1000 }); +``` + +
+
+ +```python +limit_table = perspective.Table(data, limit=1000); +``` + +
+ +## [`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_. + +
+ +```javascript +const schema = { + a: "integer", + b: "float", +}; + +const table = await perspective.table(schema); +table.update(new_data); +``` + +
+
+ +```python +schema = {"a": "integer", "b": "float"} + +table = perspective.Table(schema) +table.update(new_data) +``` + +
+ +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: + +
+ +```javascript +indexed_table.update({ id: [1, 4], name: ["x", "y"] }); +``` + +
+
+ +```python +indexed_table.update({"id": [1, 4], "name": ["x", "y"]}) +``` + +
+ +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. + +
+ +```javascript +table.update([{ x: 3, y: null }]); // `z` missing +``` + +
+
+ +```python +table.update([{"x": 3, "y": None}]) // `z` missing +``` + +
+ +Rows can also be removed from an indexed [`Table`], by calling [`Table::remove`] +with an array of index values: + +
+ +```javascript +indexed_table.remove([1, 4]); +``` + +
+
+ +```python +indexed_table.remove([1, 4]) +``` + +
+ +# [`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`. + +
+ +```javascript +table.clear(); +table.replace(json); +``` + +
+
+ +```python +table.clear() +table.replace(df) +``` + +
+ +
`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. +
+
+ +# Examples + +
```javascript const table = await perspective.table({ @@ -20,9 +40,6 @@ await view.delete();
-
-The examples in this module are in Python. See perspective docs for the Rust API. -
```python table = perspective.Table({ @@ -50,17 +67,7 @@ view.delete().await?;
-[`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 -
```javascript @@ -232,15 +233,13 @@ view = table.view(
-## 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 -
```javascript @@ -261,7 +260,7 @@ view = table.view(columns=["a"])
-## 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 -
```javascript @@ -290,7 +287,7 @@ view = table.view(sort=[["a", "asc"]])
-## 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 -
```javascript @@ -324,7 +319,7 @@ view = table.view(filter=[["a", "<", 100]])
-## 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 )