chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
[alias]
|
||||
bundle = "run -p perspective-bundle"
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg=web_sys_unstable_apis", "-Csymbol-mangling-version=v0"]
|
||||
# rustflags = [
|
||||
# "--cfg=web_sys_unstable_apis",
|
||||
# "--cfg=pyo3_disable_reference_pool",
|
||||
# "-Csymbol-mangling-version=legacy",
|
||||
# "-Zunstable-options",
|
||||
# ]
|
||||
target-dir = "rust/target"
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"--cfg=getrandom_backend=\"wasm_js\"",
|
||||
"--cfg=web_sys_unstable_apis",
|
||||
"-Ctarget-feature=+bulk-memory,+simd128,+relaxed-simd,+reference-types",
|
||||
]
|
||||
|
||||
[target.i686-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static", "--cfg=web_sys_unstable_apis"]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static", "--cfg=web_sys_unstable_apis"]
|
||||
|
||||
[future-incompat-report]
|
||||
frequency = 'never'
|
||||
|
||||
# TODO This is required to synth public releases on GH Actions, which doesn't
|
||||
# use the root `Cargo.toml`.
|
||||
[patch.crates-io]
|
||||
perspective-client = { path = "rust/perspective-client" }
|
||||
perspective-server = { path = "rust/perspective-server" }
|
||||
perspective-js = { path = "rust/perspective-js" }
|
||||
perspective-python = { path = "rust/perspective-python" }
|
||||
perspective-viewer = { path = "rust/perspective-viewer" }
|
||||
perspective = { path = "rust/perspective" }
|
||||
|
||||
[unstable]
|
||||
bindeps = true
|
||||
|
||||
[term]
|
||||
quiet = false
|
||||
verbose = false
|
||||
color = 'always'
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
---
|
||||
Language: Cpp
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: BlockIndent
|
||||
AlignArrayOfStructures: None
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveShortCaseStatements:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCaseColons: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: DontAlign
|
||||
AlignTrailingComments:
|
||||
Kind: Always
|
||||
OverEmptyLines: 0
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Empty
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortEnumsOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: All
|
||||
AlwaysBreakAfterReturnType: AllDefinitions
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BitFieldColonSpacing: Both
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterExternBlock: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakAfterAttributes: Always
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakArrays: true
|
||||
BreakBeforeBinaryOperators: NonAssignment
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInlineASMColon: OnlyMultiline
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 80
|
||||
CommentPragmas: "^ IWYU pragma:"
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: ".*"
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
IncludeIsMainRegex: "(Test)?$"
|
||||
IncludeIsMainSourceRegex: ""
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseBlocks: false
|
||||
IndentCaseLabels: true
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentRequiresClause: true
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: true
|
||||
InsertNewlineAtEOF: false
|
||||
InsertTrailingCommas: None
|
||||
IntegerLiteralSeparator:
|
||||
Binary: 0
|
||||
BinaryMinDigits: 0
|
||||
Decimal: 0
|
||||
DecimalMinDigits: 0
|
||||
Hex: 0
|
||||
HexMinDigits: 0
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
KeepEmptyLinesAtEOF: false
|
||||
LambdaBodyIndentation: Signature
|
||||
LineEnding: LF
|
||||
MacroBlockBegin: ""
|
||||
MacroBlockEnd: ""
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: Inner
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: true
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PackConstructorInitializers: CurrentLine
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Left
|
||||
PPIndentWidth: -1
|
||||
QualifierAlignment: Leave
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: true
|
||||
RemoveBracesLLVM: false
|
||||
RemoveParentheses: Leave
|
||||
RemoveSemicolon: false
|
||||
RequiresClausePosition: OwnLine
|
||||
RequiresExpressionIndentation: OuterScope
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SortIncludes: Never
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: LexicographicNumeric
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeJsonColon: false
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: true
|
||||
AfterForeachMacros: true
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: true
|
||||
AfterOverloadedOperator: false
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceBeforeSquareBrackets: false
|
||||
SpaceInEmptyBlock: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParens: Never
|
||||
SpacesInParensOptions:
|
||||
InCStyleCasts: false
|
||||
InConditionalStatements: false
|
||||
InEmptyParentheses: false
|
||||
Other: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: c++17
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
VerilogBreakBetweenInstancePorts: true
|
||||
WhitespaceSensitiveMacros:
|
||||
- BOOST_PP_STRINGIZE
|
||||
- CF_SWIFT_NAME
|
||||
- NS_SWIFT_NAME
|
||||
- PP_STRINGIZE
|
||||
- STRINGIZE
|
||||
---
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
Checks: >
|
||||
-*,
|
||||
modernize-*,
|
||||
performance-*,
|
||||
readability-*,
|
||||
clang-analyzer-*,
|
||||
bugprone-*,
|
||||
portability-*,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-identifier-length,
|
||||
-readability-magic-numbers,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-implicit-widening-of-multiplication-result,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-misc-const-correctness,
|
||||
-readability-function-cognitive-complexity
|
||||
|
||||
# WarningsAsErrors: "*"
|
||||
@@ -0,0 +1,21 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/universal
|
||||
{
|
||||
"name": "Ubuntu: pnpm, rust, cmake, for JS dev",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"ms-playwright.playwright"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/rust:1": {},
|
||||
"ghcr.io/devcontainers-extra/features/pnpm:2": {}
|
||||
},
|
||||
|
||||
"postCreateCommand": "./.devcontainer/postcreate.sh"
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
# Install cmake
|
||||
test -f /usr/share/doc/kitware-archive-keyring/copyright ||
|
||||
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
|
||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y kitware-archive-keyring
|
||||
sudo apt-get install -y cmake
|
||||
|
||||
# Install uv
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Repo setup
|
||||
pnpm install
|
||||
@@ -0,0 +1,15 @@
|
||||
*.ipynb linguist-documentation
|
||||
docs/**/* linguist-documentation
|
||||
python/perspective/notebooks/* linguist-documentation
|
||||
python/perspective/docs/* linguist-documentation
|
||||
results/*.json linguist-documentation
|
||||
**/test/**/* linguist-documentation
|
||||
**/tests/**/* linguist-documentation
|
||||
examples/**/* linguist-documentation
|
||||
scripts/* linguist-documentation
|
||||
tools/**/* linguist-documentation
|
||||
cmake/** linguist-documentation
|
||||
CMakeLists.txt linguist-documentation
|
||||
**/*.sh linguist-documentation
|
||||
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: If something isn't working as expected.
|
||||
---
|
||||
|
||||
## Bug Report
|
||||
|
||||
### Steps to Reproduce:
|
||||
|
||||
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:
|
||||
|
||||
Describe what you expected to see. If you are reporting a UX/UI error, this may
|
||||
include screenshots with annotations.
|
||||
|
||||
### Actual Result:
|
||||
|
||||
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:
|
||||
|
||||
For JavaScript (browser):
|
||||
|
||||
- `@perspective-dev/client` 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.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
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.
|
||||
|
||||
### Potential Solutions:
|
||||
|
||||
...clearly and concisely describe what you want to happen. Add any considered
|
||||
drawbacks.
|
||||
|
||||
... if you've considered alternatives, clearly and concisely describe those too.
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
|
||||
Please make sure you've read the
|
||||
[`CONTRIBUTING.md`](https://github.com/perspective-dev/perspective/blob/master/CONTRIBUTING.md)
|
||||
and followed the instructions precisely before opening a Pull Request. Pull
|
||||
Requests from new contributors which do not may be closed without comment.
|
||||
|
||||
## Final Pull Request Checklist
|
||||
|
||||
As a reminder from [`CONTRIBUTING.md`](https://github.com/perspective-dev/perspective/blob/master/CONTRIBUTING.md). Please do not _literally_ inlude this list in your PR!
|
||||
|
||||
- Includes a thorough 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.
|
||||
- Disclosed AI tooling assistance.
|
||||
- Reviewed PR commit history to remove unnecessary changes.
|
||||
- Make sure your PR passes _build_, _test_ and _lint_ steps _completely_.
|
||||
|
||||
-->
|
||||
@@ -0,0 +1,122 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
name: "Parse build configuration"
|
||||
description: "Parses the build configuration into something easy to consume"
|
||||
|
||||
outputs:
|
||||
SKIP_CI:
|
||||
value: ${{ steps.setuppush.outputs.SKIP_CI || steps.setuppr.outputs.SKIP_CI || steps.setupmanual.outputs.SKIP_CI }}
|
||||
SKIP_CACHE:
|
||||
value: ${{ steps.setuppush.outputs.SKIP_CACHE || steps.setuppr.outputs.SKIP_CACHE || steps.setupmanual.outputs.SKIP_CACHE }}
|
||||
FULL_RUN:
|
||||
value: ${{ steps.setuppush.outputs.FULL_RUN || steps.setuppr.outputs.FULL_RUN || steps.setupmanual.outputs.FULL_RUN }}
|
||||
PUBLISH_RELEASE:
|
||||
value: ${{ steps.setuppush.outputs.PUBLISH_RELEASE || steps.setuppr.outputs.PUBLISH_RELEASE || steps.setupmanual.outputs.PUBLISH_RELEASE }}
|
||||
SKIP_PYTHON:
|
||||
value: ${{ steps.setuppush.outputs.SKIP_PYTHON || steps.setuppr.outputs.SKIP_PYTHON || steps.setupmanual.outputs.SKIP_PYTHON }}
|
||||
INCLUDE_WINDOWS:
|
||||
value: ${{ steps.setuppush.outputs.INCLUDE_WINDOWS || steps.setuppr.outputs.INCLUDE_WINDOWS || steps.setupmanual.outputs.INCLUDE_WINDOWS }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Get Commit Message
|
||||
shell: bash
|
||||
run: echo "COMMIT_MSG=$(git log -1 --pretty=%B HEAD | tr '\n' ' ')" >> $GITHUB_ENV
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
- name: Get Commit Message
|
||||
shell: bash
|
||||
run: echo "COMMIT_MSG=$(git log -1 --pretty=%B HEAD^2 | tr '\n' ' ')" >> $GITHUB_ENV
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
- name: Display and Setup Build Args (Push)
|
||||
shell: bash
|
||||
id: setuppush
|
||||
run: |
|
||||
echo "Commit Message: $COMMIT_MSG"
|
||||
echo "Skip CI: $SKIP_CI"
|
||||
echo "Skip Cache: $SKIP_CACHE"
|
||||
echo "Full Run: $FULL_RUN"
|
||||
echo "Publish Release: $PUBLISH_RELEASE"
|
||||
echo "Skip Python: $SKIP_PYTHON"
|
||||
echo "Include Windows: $INCLUDE_WINDOWS"
|
||||
echo "COMMIT_MSG=$COMMIT_MSG" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_CI=$SKIP_CI" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_CACHE=$SKIP_CACHE" >> $GITHUB_OUTPUT
|
||||
echo "FULL_RUN=$FULL_RUN" >> $GITHUB_OUTPUT
|
||||
echo "PUBLISH_RELEASE=$PUBLISH_RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_PYTHON=$SKIP_PYTHON" >> $GITHUB_OUTPUT
|
||||
echo "INCLUDE_WINDOWS=$INCLUDE_WINDOWS" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
SKIP_CI: ${{ contains(github.event.head_commit.message, '[ci-skip]') }}
|
||||
SKIP_CACHE: ${{ contains(github.event.head_commit.message, '[ci-skip-cache]') }}
|
||||
FULL_RUN: ${{ startsWith(github.ref_name, 'v') || contains(github.event.head_commit.message, '[ci-full]') || github.ref_name == 'master' }}
|
||||
PUBLISH_RELEASE: ${{ startsWith(github.ref_name, 'v') }}
|
||||
SKIP_PYTHON: ${{ contains(github.event.head_commit.message, '[ci-skip-python]') }}
|
||||
INCLUDE_WINDOWS: ${{ contains(github.event.head_commit.message, '[ci-include-windows]') }}
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
- name: Display and Setup Build Args (PR)
|
||||
shell: bash
|
||||
id: setuppr
|
||||
run: |
|
||||
echo "Commit Message: $COMMIT_MSG"
|
||||
echo "Skip CI: $SKIP_CI"
|
||||
echo "Skip Cache: $SKIP_CACHE"
|
||||
echo "Full Run: $FULL_RUN"
|
||||
echo "Publish Release: $PUBLISH_RELEASE"
|
||||
echo "Skip Python: $SKIP_PYTHON"
|
||||
echo "Include Windows: $INCLUDE_WINDOWS"
|
||||
echo "COMMIT_MSG=$COMMIT_MSG" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_CI=$SKIP_CI" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_CACHE=$SKIP_CACHE" >> $GITHUB_OUTPUT
|
||||
echo "FULL_RUN=$FULL_RUN" >> $GITHUB_OUTPUT
|
||||
echo "PUBLISH_RELEASE=$PUBLISH_RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_PYTHON=$SKIP_PYTHON" >> $GITHUB_OUTPUT
|
||||
echo "INCLUDE_WINDOWS=$INCLUDE_WINDOWS" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
SKIP_CI: ${{ contains(github.event.pull_request.title, '[ci-skip]') || contains(github.event.head_commit.message, '[ci-skip]') }}
|
||||
SKIP_CACHE: ${{ contains(github.event.pull_request.title, '[ci-skip-cache]') || contains(github.event.head_commit.message, '[ci-skip-cache]') }}
|
||||
FULL_RUN: ${{ contains(github.event.pull_request.title, '[ci-full]') || contains(github.event.head_commit.message, '[ci-full]') }}
|
||||
PUBLISH_RELEASE: ${{ startsWith(github.ref_name, 'v') }}
|
||||
SKIP_PYTHON: ${{ contains(github.event.pull_request.title, '[ci-skip-python]') || contains(github.event.head_commit.message, '[ci-skip-python]') }}
|
||||
INCLUDE_WINDOWS: ${{ contains(github.event.pull_request.title, '[ci-include-windows]') || contains(github.event.head_commit.message, '[ci-include-windows]') }}
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
- name: Display and Setup Build Args (Manual)
|
||||
id: setupmanual
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Commit Message: $COMMIT_MSG"
|
||||
echo "Skip CI: $SKIP_CI"
|
||||
echo "Skip Cache: $SKIP_CACHE"
|
||||
echo "Full Run: $FULL_RUN"
|
||||
echo "Publish Release: $PUBLISH_RELEASE"
|
||||
echo "Skip Python: $SKIP_PYTHON"
|
||||
echo "Include Windows: $INCLUDE_WINDOWS"
|
||||
echo "COMMIT_MSG=$COMMIT_MSG" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_CI=$SKIP_CI" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_CACHE=$SKIP_CACHE" >> $GITHUB_OUTPUT
|
||||
echo "FULL_RUN=$FULL_RUN" >> $GITHUB_OUTPUT
|
||||
echo "PUBLISH_RELEASE=$PUBLISH_RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "SKIP_PYTHON=$SKIP_PYTHON" >> $GITHUB_OUTPUT
|
||||
echo "INCLUDE_WINDOWS=$INCLUDE_WINDOWS" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
SKIP_CI: false
|
||||
SKIP_CACHE: ${{ github.event.inputs.ci-skip-cache }}
|
||||
FULL_RUN: ${{ github.event.inputs.ci-full }}
|
||||
PUBLISH_RELEASE: ${{ startsWith(github.ref_name, 'v') }}
|
||||
SKIP_PYTHON: ${{ github.event.inputs.ci-skip-python }}
|
||||
INCLUDE_WINDOWS: ${{ github.event.inputs.ci-include-windows }}
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
@@ -0,0 +1,247 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
name: "Install Dependencies"
|
||||
description: "Install and cache the project's myraid dependencies"
|
||||
|
||||
inputs:
|
||||
javascript:
|
||||
default: "true"
|
||||
description: "Install pnpm postinstall steps, playwright browsers and emsdk?"
|
||||
arch:
|
||||
default: ""
|
||||
description: "Architecture"
|
||||
manylinux:
|
||||
default: "false"
|
||||
description: "Deal with manylinux exceptions"
|
||||
cpp:
|
||||
default: "true"
|
||||
description: "Install Boost and LLVM?"
|
||||
rust:
|
||||
default: "true"
|
||||
description: "Install Rust toolchain?"
|
||||
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!"
|
||||
skip_cache:
|
||||
default: "false"
|
||||
description: "Don't use cache from previous builds"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Clean System
|
||||
uses: AdityaGarg8/remove-unwanted-software@v5
|
||||
if: ${{ inputs.clean == 'true' && runner.os != 'Windows' }}
|
||||
with:
|
||||
remove-android: "true"
|
||||
remove-dotnet: "true"
|
||||
remove-haskell: "true"
|
||||
remove-codeql: "true"
|
||||
remove-docker-images: "true"
|
||||
remove-large-packages: "true"
|
||||
remove-cached-tools: "true"
|
||||
|
||||
# Sticking to 3.29 because of:
|
||||
# https://github.com/open-telemetry/opentelemetry-cpp/issues/2998
|
||||
- name: Setup cmake
|
||||
uses: jwlawson/actions-setup-cmake@v2
|
||||
with:
|
||||
cmake-version: "3.29.6"
|
||||
|
||||
- name: Setup MSVC environment (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: ${{ inputs.arch == 'aarch64' && 'amd64_arm64' || 'x64' }}
|
||||
|
||||
- name: Force Ninja generator (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: bash
|
||||
run: echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup emsdk cache
|
||||
uses: actions/cache@v4
|
||||
id: emsdk-cache
|
||||
if: ${{ inputs.skip_cache == 'false' && inputs.javascript == 'true' }}
|
||||
with:
|
||||
path: |
|
||||
~/boost_1_82_0/
|
||||
~/.emsdk/
|
||||
~/.llvm/
|
||||
key: ${{ runner.os }}-emsdk-${{ hashFiles('package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-emsdk-
|
||||
|
||||
- name: Setup pip cache
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.skip_cache == 'false' && inputs.python == 'true' }}
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Setup cargo cache
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.skip_cache == 'false' && inputs.rust == 'true' }}
|
||||
with:
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
# - name: ccache
|
||||
# uses: hendrikmuhs/ccache-action@v1.2
|
||||
# if: ${{ inputs.skip_cache == 'false' }}
|
||||
# with:
|
||||
# key: ${{ github.job }}-${{ matrix.os }}
|
||||
|
||||
# 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.12 from manylinu
|
||||
- run: echo "/opt/python/cp311-cp311/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' && inputs.manylinux == 'false' }}
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
|
||||
# - run: |
|
||||
# curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
||||
# python get-pip.py --ignore-installed
|
||||
# pip debug --verbose
|
||||
# shell: bash
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Install rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
if: ${{ inputs.rust == 'true' && inputs.arch != 'aarch64' }}
|
||||
with:
|
||||
toolchain: nightly-2026-01-01
|
||||
targets: wasm32-unknown-unknown
|
||||
components: rustfmt, clippy, rust-src
|
||||
|
||||
- name: Install rust (aarch64 OSX)
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
if: ${{ inputs.rust == 'true' && inputs.arch == 'aarch64' && runner.os == 'macOS' }}
|
||||
with:
|
||||
toolchain: nightly-2026-01-01
|
||||
targets: aarch64-apple-darwin
|
||||
components: rust-src
|
||||
|
||||
- name: Install rust (aarch64 Linux)
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
if: ${{ inputs.rust == 'true' && inputs.arch == 'aarch64' && runner.os == 'Linux' }}
|
||||
with:
|
||||
toolchain: nightly-2026-01-01
|
||||
targets: aarch64-unknown-linux-gnu
|
||||
components: rust-src
|
||||
|
||||
# Did you see a CI error of the form:
|
||||
#
|
||||
# error: failed to install component: 'clippy-preview-aarch64-unknown-linux-gnu',
|
||||
# detected conflict: 'bin/cargo-clippy'
|
||||
#
|
||||
# See https://github.com/rust-lang/rustup/issues/988#issuecomment-1820438467
|
||||
- name: Stupid cargo hack
|
||||
shell: bash
|
||||
run: cargo version
|
||||
|
||||
# # TODO doesn't work.
|
||||
# - name: Install LLVM 17
|
||||
# if: ${{ inputs.cpp == 'true' }}
|
||||
# uses: KyleMayes/install-llvm-action@v2
|
||||
# with:
|
||||
# version: "17"
|
||||
# directory: "./.llvm"
|
||||
# cached: true
|
||||
|
||||
- name: Install JS dependencies
|
||||
shell: bash
|
||||
if: ${{ inputs.javascript == 'true' && inputs.playwright == 'true' }}
|
||||
run: pnpm install
|
||||
|
||||
- name: Install JS dependencies
|
||||
shell: bash
|
||||
if: ${{ inputs.javascript == 'false' || inputs.playwright == 'false'}}
|
||||
run: pnpm install --ignore-scripts
|
||||
|
||||
- name: Template version
|
||||
shell: bash
|
||||
if: ${{ !startsWith(github.ref_name, 'v') }}
|
||||
run: |
|
||||
git config --global --add safe.directory $GITHUB_WORKSPACE
|
||||
node tools/scripts/version.mjs --nightly
|
||||
|
||||
- name: Install Python dependencies
|
||||
shell: bash
|
||||
if: ${{ inputs.python == 'true' && inputs.manylinux == 'false' }}
|
||||
run: python -m pip install -r rust/perspective-python/requirements.txt
|
||||
|
||||
- name: Install Python dependencies
|
||||
shell: bash
|
||||
if: ${{ inputs.python == 'true' && inputs.manylinux == 'true' }}
|
||||
run: /opt/python/cp311-cp311/bin/python -m pip install -r rust/perspective-python/requirements.txt
|
||||
|
||||
- name: manylinux deps
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -x "$(command -v dnf)" ]; then
|
||||
dnf install wget -y
|
||||
fi
|
||||
if: ${{ runner.os == 'Linux' && inputs.cpp == 'true' && inputs.javascript == 'false' }}
|
||||
|
||||
# - name: Install CCache
|
||||
# shell: bash
|
||||
# run: sudo apt install -y ccache
|
||||
# if: ${{ runner.os == 'Linux' }}
|
||||
|
||||
# Arrow cannot install on windows without this
|
||||
# https://stackoverflow.com/questions/22575662/filename-too-long-in-git-for-windows
|
||||
# https://github.com/orgs/community/discussions/26952
|
||||
- run: git config --system core.longpaths true
|
||||
shell: pwsh
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
|
||||
# - name: Free up disk space
|
||||
# if: ${{ runner.os == 'Linux' }}
|
||||
# run: |
|
||||
# rm -rf /__t/*
|
||||
@@ -0,0 +1,42 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
name: "Install wheel"
|
||||
description: "Installs the wheel depending on build platform, because Python"
|
||||
|
||||
inputs:
|
||||
inplace:
|
||||
default: "true"
|
||||
description: "Install in-place?"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install wheel (Linux)
|
||||
shell: sh
|
||||
run: python -m pip install -U --no-dependencies *.whl --target rust/perspective-python
|
||||
if: ${{ inputs.inplace == 'true' && runner.os == 'Linux' }}
|
||||
|
||||
- name: Install wheel (Linux)
|
||||
shell: sh
|
||||
run: python -m pip install -U --no-dependencies *.whl
|
||||
if: ${{ inputs.inplace != 'true' && runner.os == 'Linux' }}
|
||||
|
||||
- name: Install wheel (OSX)
|
||||
shell: sh
|
||||
run: python -m pip install -U --no-dependencies *.whl --target rust/perspective-python
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
|
||||
- name: Install wheel (Windows)
|
||||
shell: pwsh
|
||||
run: python -m pip install -U --no-dependencies (Get-ChildItem .\*.whl | Select-Object -Expand FullName) --target rust/perspective-python
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
File diff suppressed because it is too large
Load Diff
+71
@@ -0,0 +1,71 @@
|
||||
# -*- mode: gitignore; -*-
|
||||
__pycache__/
|
||||
!.vscode/extensions.json
|
||||
!.vscode/perspective.code-snippets
|
||||
!.vscode/settings.default.json
|
||||
!.vscode/tasks.json
|
||||
!website/i18n/en.json
|
||||
.cache
|
||||
.clangd
|
||||
.DS_Store
|
||||
.emsdk
|
||||
.plan
|
||||
.pnpm-store
|
||||
.ipynb_checkpoints
|
||||
.perspectiverc
|
||||
.vscode/*
|
||||
*.so
|
||||
*~
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
/.idea
|
||||
/auto/
|
||||
/docs/build
|
||||
/elpa/
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
/examples/*/build
|
||||
/jsconfig.json
|
||||
/packages/*/build
|
||||
/packages/sigma
|
||||
/server/
|
||||
/src/include/boost
|
||||
benchmark_venv
|
||||
dist/
|
||||
docs/.docusaurus
|
||||
docs/i18n/en.json
|
||||
docs/modules.rst
|
||||
docs/perspective.*.rst
|
||||
docs/static/arrow/
|
||||
docs/static/blocks
|
||||
docs/static/features
|
||||
docs/static/guide
|
||||
examples/blocks/src/nypd/nypdccrb.arrow
|
||||
node_modules
|
||||
packages/jupyterlab/test/config/jupyter/migrated
|
||||
py_modules
|
||||
python/cmake
|
||||
python/cpp
|
||||
rust/perspective-client/docs/expression_gen.md
|
||||
rust/perspective-client/src/rust/proto.rs
|
||||
rust/perspective-js/src/ts/ts-rs
|
||||
rust/perspective-server/build
|
||||
rust/perspective-python/LICENSE_THIRDPARTY_cargo.yml
|
||||
rust/perspective-python/LICENSE.md
|
||||
rust/perspective-server/docs/lib_gen.md
|
||||
rust/perspective-viewer/src/ts/ts-rs
|
||||
rust/perspective/src/ts/ts-rs
|
||||
rust/target*
|
||||
Vagrantfile
|
||||
vcpkg
|
||||
venv/
|
||||
rust/perspective-python/perspective_python-*.data
|
||||
.pytest-cache/
|
||||
docs/static/python
|
||||
docs/static/node
|
||||
docs/static/browser
|
||||
docs/static/viewer
|
||||
docs/static/react
|
||||
rust/perspective-server/build
|
||||
target/
|
||||
dist-gh-pages
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
pnpm run prepush
|
||||
@@ -0,0 +1,14 @@
|
||||
dist/
|
||||
build/
|
||||
node_modules/
|
||||
target/
|
||||
.emsdk/
|
||||
.venv*/
|
||||
boost*/
|
||||
py_modules/
|
||||
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
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"options": {
|
||||
"printWidth": 200,
|
||||
"tabWidth": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.md"],
|
||||
"options": {
|
||||
"proseWrap": "always"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+3810
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
# Code of Conduct
|
||||
|
||||
Perspective is an [OpenJS Foundation](https://openjsf.org/) project. Please be
|
||||
mindful of and adhere to the OpenJS Foundation's
|
||||
[Code of Conduct](https://github.com/openjs-foundation/cross-project-council/blob/main/CODE_OF_CONDUCT.md)
|
||||
when contributing to Perspective.
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in contributing to Perspective!
|
||||
|
||||
## Guidelines
|
||||
|
||||
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, 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 and Pull Request templates. If the templates don't fit
|
||||
your need, please open a
|
||||
[discussion](https://github.com/perspective-dev/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/perspective-dev/perspective/discussions).
|
||||
- Don't add comments asking when a feature will be delivered or a reported issue
|
||||
fixed. The Issue will link any in-progress draft PRs or Milestones (if known).
|
||||
|
||||
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/perspective-dev/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!
|
||||
- Sign commits (e.g. with `-s`) in accordance with the DCO policy detailed
|
||||
below, _before_ opening a PR.
|
||||
- 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-dev.github.io/docs/development/#benchmark)
|
||||
for any performance-critical changes.
|
||||
|
||||
- Keep PRs clean, simple and to-the-point:
|
||||
- Squash "WIP", "Reverting ..", etc., commits.
|
||||
- No merge commits (`git merge master`), prefer `rebase` to resolve
|
||||
conflicts with the `master` branch.
|
||||
- Try to organize commits as functional components (as opposed to
|
||||
timeline-of-development).
|
||||
|
||||
## DCO
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Signed commits are required for all PRs.
|
||||
|
||||
The Perspective project requires contributors to affirm their contributions via
|
||||
a [Developer Certificate of Origin](https://developercertificate.org), which
|
||||
certifies that developers are authorized to make their contribution, either on
|
||||
their own behalf or on behalf of their employer.
|
||||
|
||||
In practice, this means that all commits to Perspective must be signed (e.g.
|
||||
with `-s`/`-S`). Pull requests with any unsigned commits, or where the signer
|
||||
does not match the commit author, can not be merged by Perspective's committers.
|
||||
We ask that you check that you have a signed all your commits before making a
|
||||
pull request. A [DCO enforcement bot](https://github.com/apps/dco) will
|
||||
automatically scan and flag any pull requests that lack a valid sign-off.
|
||||
|
||||
If you have any general questions about contributing to Perspective, please feel
|
||||
free to open a discussion on
|
||||
[github](https://github.com/perspective-dev/perspective/discussions)
|
||||
|
||||
## AI Assistance Notice
|
||||
|
||||
This section was forked from
|
||||
[`ghostty`](https://github.com/ghostty-org/ghostty),
|
||||
[MIT License](https://github.com/ghostty-org/ghostty/blob/main/LICENSE)
|
||||
([File](https://github.com/ghostty-org/ghostty/pull/8289/files))
|
||||
([PR](https://github.com/ghostty-org/ghostty/pull/8289)). Thanks @mitchellh!
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> If you are using **any kind of AI assistance** to contribute to Perspective,
|
||||
> it must be disclosed in the pull request.
|
||||
|
||||
If you are using any kind of AI assistance while contributing to Perspective,
|
||||
**this must be disclosed in the pull request**, along with the extent to which
|
||||
AI assistance was used (e.g. docs only vs. code generation). If PR responses are
|
||||
being generated by an AI, disclose that as well. As a small exception, trivial
|
||||
tab-completion doesn't need to be disclosed, so long as it is limited to single
|
||||
keywords or short phrases.
|
||||
|
||||
An example disclosure:
|
||||
|
||||
> This PR was written primarily by Claude Code.
|
||||
|
||||
Or a more detailed disclosure:
|
||||
|
||||
> I consulted ChatGPT to understand the codebase but the solution was fully
|
||||
> authored manually by myself.
|
||||
|
||||
Failure to disclose this is first and foremost rude to the human operators on
|
||||
the other end of the pull request, but it also makes it difficult to determine
|
||||
how much scrutiny to apply to the contribution.
|
||||
|
||||
In a perfect world, AI assistance would produce equal or higher quality work
|
||||
than any human. That isn't the world we live in today, and in most cases it's
|
||||
generating slop. I say this despite being a fan of and using them successfully
|
||||
myself (with heavy supervision)!
|
||||
|
||||
Please be respectful to maintainers and disclose AI assistance.
|
||||
|
||||
## Final Pull Request Checklist
|
||||
|
||||
Before opening a Pull Request, be sure:
|
||||
|
||||
- Includes a thorough 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.
|
||||
- Disclosed AI tooling assistance.
|
||||
- Reviewed PR commit history to remove unnecessary changes.
|
||||
- Make sure your PR passes _build_, _test_ and _lint_ steps _completely_.
|
||||
Generated
+4345
File diff suppressed because it is too large
Load Diff
+56
@@ -0,0 +1,56 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
[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/metadata",
|
||||
"rust/bundle",
|
||||
"rust/perspective",
|
||||
"rust/perspective-client",
|
||||
"rust/perspective-js",
|
||||
"rust/perspective-python",
|
||||
"rust/perspective-server",
|
||||
"rust/perspective-viewer",
|
||||
"examples/rust-axum",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
opt-level = "s"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
strip = true
|
||||
|
||||
# These are only respected when `cargo` is invoked from the project root
|
||||
[patch.crates-io]
|
||||
# simd-adler32 = { git = "https://github.com/mcountryman/simd-adler32.git", rev = "b279034d9eb554c3e5e0af523db044f08d8297ba" }
|
||||
protobuf-src = { git = "https://github.com/carlocorradini/rust-protobuf-native.git", rev = "1aba500e469f8bdc384a0fe9e69c189fda72e059" }
|
||||
perspective-client = { path = "rust/perspective-client" }
|
||||
perspective-server = { path = "rust/perspective-server" }
|
||||
perspective-js = { path = "rust/perspective-js" }
|
||||
perspective = { path = "rust/perspective" }
|
||||
perspective-viewer = { path = "rust/perspective-viewer" }
|
||||
perspective-python = { path = "rust/perspective-python" }
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
# Developer Guide (How to build Perspective from this repo)
|
||||
|
||||
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
|
||||
due to the mixed-language nature of the project; we've done quite a bit to make
|
||||
sure the newcomer experience is as straightforward as possible, but some things
|
||||
might not work the way you're used to!
|
||||
|
||||
Perspective is organized as a
|
||||
[monorepo](https://github.com/babel/babel/blob/master/doc/design/monorepo.md),
|
||||
and uses [lerna](https://lernajs.io/) to manage dependencies.
|
||||
|
||||
This guide provides instructions for both the JavaScript and Python libraries.
|
||||
To switch your development toolchain between the two, use `pnpm run setup`. Once
|
||||
the setup script has been run, common commands like `pnpm run build` and
|
||||
`pnpm run test` automatically call the correct build and test tools.
|
||||
|
||||
### System Dependencies
|
||||
|
||||
`Perspective.js` and `perspective-python` **require** the following system
|
||||
dependencies to be installed:
|
||||
|
||||
- [CMake](https://cmake.org/) (version 3.29.5 or higher)
|
||||
- [pnpm](https://pnpm.io/).
|
||||
|
||||
**_This list may be non-exhaustive depending on your OS/environment; please open
|
||||
a thread in
|
||||
[Discussions](https://github.com/perspective-dev/perspective/discussions) if you
|
||||
have any questions_**
|
||||
|
||||
## Build
|
||||
|
||||
Make sure you have the system dependencies installed. For specifics depending on
|
||||
your OS, check the [system-specific instructions](#system-specific-instructions)
|
||||
below.
|
||||
|
||||
To run a build, use
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
If this is the first time you've built Perspective, you'll be asked to generate
|
||||
a `.perspectiverc` via a short survey. This can be later re-configured via
|
||||
|
||||
```bash
|
||||
pnpm run setup
|
||||
```
|
||||
|
||||
If everything is successful, you should be able to run any of the `examples/`
|
||||
packages, e.g. `examples/blocks` like so:
|
||||
|
||||
```bash
|
||||
pnpm run start blocks
|
||||
```
|
||||
|
||||
## `Perspective.js`
|
||||
|
||||
To build the JavaScript library, which includes WebAssembly compilation,
|
||||
[Emscripten](https://github.com/kripken/emscripten) and its prerequisites are
|
||||
required.
|
||||
|
||||
`Perspective.js` specifies its Emscripten version dependency in `package.json`,
|
||||
and the correct version of Emscripten will be installed with other JS
|
||||
dependencies by running `pnpm install`.
|
||||
|
||||
#### Building via local EMSDK
|
||||
|
||||
To build using an Emscripten install on your local system and not the Emscripten
|
||||
bundled with Perspective in its `package.json`,
|
||||
[install](https://emscripten.org/docs/getting_started/downloads.html) the
|
||||
Emscripten SDK, then activate and export the latest `emsdk` environment via
|
||||
[`emsdk_env.sh`](https://github.com/juj/emsdk):
|
||||
|
||||
```bash
|
||||
source emsdk/emsdk_env.sh
|
||||
```
|
||||
|
||||
Deviating from this specific version of Emscripten specified in the project's
|
||||
`package.json` can introduce various errors that are extremely difficult to
|
||||
debug.
|
||||
|
||||
To install a specific version of Emscripten (e.g. `2.0.6`):
|
||||
|
||||
```bash
|
||||
./emsdk install 2.0.6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `perspective-python`
|
||||
|
||||
To build the Python library, first configure your project to build Python via
|
||||
`pnpm run setup`. Then, install the requirements corresponding to your version
|
||||
of python, e.g.
|
||||
|
||||
```bash
|
||||
pip install -r rust/perspective-python/requirements.txt
|
||||
```
|
||||
|
||||
`perspective-python` supports Python 3.11 and upwards.
|
||||
|
||||
### `perspective-jupyterlab`
|
||||
|
||||
To install the Jupyterlab/Jupyter Notebook plugins from your local working
|
||||
directory, simply install `python/perspective` with `pip` as you might normally
|
||||
do.
|
||||
|
||||
```bash
|
||||
# builds labextension to the perspective-python python package root directory
|
||||
PACKAGE=perspective-jupyterlab pnpm run build
|
||||
# editable install of the python package
|
||||
pnpm -F @perspective-dev/python develop:maturin
|
||||
# set up symlink of our labextension to jupyter share directory
|
||||
# this directory's path is in the output of `jupyter labextension list`
|
||||
pnpm -F @perspective-dev/python develop:labextension
|
||||
```
|
||||
|
||||
Afterwards, you should see it listed as a "local extension" when you run
|
||||
`jupyter labextension list` and as a normal extension when you run
|
||||
`jupyter nbextension list`.
|
||||
|
||||
---
|
||||
|
||||
## System-Specific Instructions
|
||||
|
||||
### MacOS/OSX
|
||||
|
||||
Install system dependencies through Homebrew:
|
||||
|
||||
```bash
|
||||
brew install cmake llvm@17
|
||||
brew link llvm@17 # optional, see below
|
||||
```
|
||||
|
||||
On M1 (Apple Silicon) systems, make sure your brew-installed dependencies are in
|
||||
`/opt/homebrew` (the default location), and that `/opt/homebrew/bin` is on the
|
||||
`PATH`.
|
||||
|
||||
If you do not want to link the llvm@17 keg, then while developing ensure it is
|
||||
on your PATH too, like this:
|
||||
|
||||
```
|
||||
PATH=$(brew --prefix llvm@17)/bin:$PATH
|
||||
```
|
||||
|
||||
**Note**: Perspective vendors its C++ extensions, so you may run into trouble
|
||||
building if you have `brew`-installed versions of libraries, such as
|
||||
`flatbuffers`.
|
||||
|
||||
### Windows 10+
|
||||
|
||||
You need to use bash in order to build Perspective packages. To successfully
|
||||
build on Windows 10+, enable
|
||||
[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/install-win10)
|
||||
(WSL) and install the Linux distribution of your choice.
|
||||
|
||||
Create symbolic links to easily access Windows directories and projects modified
|
||||
via Windows. This way, you can modify any of the Perspective files using your
|
||||
favorite editors on Windows and build via Linux.
|
||||
|
||||
Follow the Linux specific instructions to install Emscripten and all
|
||||
prerequisite tools.
|
||||
|
||||
### Ubuntu/Debian
|
||||
|
||||
On Ubuntu, CMake will mistakenly resolve the system headers in `/usr/include`
|
||||
rather than the emscripten supplied versions. You can resolve this by moving
|
||||
`boost` dependencies to somewhere other than `/usr/include` - into Perspective's
|
||||
own `src` dir (as per
|
||||
[here](http://vclf.blogspot.com/2014/08/emscripten-linking-to-boost-libraries.html)).
|
||||
|
||||
```bash
|
||||
apt-get install libboost-all-dev
|
||||
cp -r /usr/include/boost ./packages/perspective/src/include/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test
|
||||
|
||||
You can run the test suite simply with the standard NPM command, which will both
|
||||
build the test suite for every package and run them.
|
||||
|
||||
```bash
|
||||
pnpm run test
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
The JavaScript test suite is composed of two sections: a Node.js test, which
|
||||
asserts behavior of the `@perspective-dev/client` library, and a suite of
|
||||
[Playwright](https://playwright.dev/) tests, which assert the behavior of the
|
||||
rest of the UI facing packages.
|
||||
|
||||
```bash
|
||||
pnpm run test --update-snapshots
|
||||
```
|
||||
|
||||
### Troubleshooting installation from source
|
||||
|
||||
If you are installing from a source distribution (sdist), make sure you have the
|
||||
[System Dependencies](#system-dependencies) installed.
|
||||
|
||||
Try installing in verbose mode:
|
||||
|
||||
```bash
|
||||
pip install -vv perspective-python
|
||||
```
|
||||
|
||||
The most common culprits are:
|
||||
|
||||
- CMake version is too old
|
||||
- Boost headers are missing or too old
|
||||
|
||||
---
|
||||
|
||||
## Benchmark
|
||||
|
||||
You can generate benchmarks specific to your machine's OS and CPU architecture
|
||||
with Perspective's benchmark suite, which will host a live dashboard at
|
||||
http://localhost:8080 as well as output a result `benchmark.arrow` file.
|
||||
|
||||
```bash
|
||||
pnpm run bench
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# `perspective` Project Governance
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [@texodus](https://github.com/texodus)
|
||||
- [@timkpaine](https://github.com/timkpaine)
|
||||
|
||||
Maintainers are responsible for issue/PR triage, feature additions, maintenance,
|
||||
bugfixes, security fixes, releases, promoting existing contributors to
|
||||
maintainers, managing repo and CI configuration, etc.
|
||||
|
||||
## Contributors
|
||||
|
||||
Anyone who contributes code or content or time, via issues or pull requests or
|
||||
otherwise. Contributors do not have any additional permissions on the project.
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
# Apache License
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
- **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
- **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
- **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright 2019 The Perspective Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,72 @@
|
||||
# `perspective` Charter
|
||||
|
||||
`perspective` is a data visualization and analytics component, especially
|
||||
well-suited for large and/or streaming datasets.
|
||||
|
||||
## Section 0: Guiding Principles
|
||||
|
||||
The `perspective` project is part of the [OpenJS Foundation][openjs foundation],
|
||||
which operates transparently, openly, collaboratively, and ethically. Project
|
||||
proposals, timelines, and status must not merely be open, but also easily
|
||||
visible to outsiders.
|
||||
|
||||
## Section 1: Scope
|
||||
|
||||
`perspective` is a data visualization component, any features related to data
|
||||
visualization on any platform are potentially in scope.
|
||||
|
||||
## Section 2: Relationship with OpenJS Foundation CPC.
|
||||
|
||||
Technical leadership for the projects within the [OpenJS
|
||||
Foundation][openjs foundation] is delegated to the projects through their
|
||||
project charters by the
|
||||
[OpenJS Foundation Cross-Project Council](https://openjsf.org/about/governance/)
|
||||
(CPC). In the case of the `perspective` project, it is delegated to the
|
||||
[`perspective` Maintainers](GOVERNANCE.md#maintainers) (the “Maintainers”). The
|
||||
OpenJS Foundation's business leadership is the Board of Directors (the “Board”).
|
||||
|
||||
This `perspective` Charter reflects a carefully constructed balanced role for
|
||||
the Maintainers and the CPC in the governance of the OpenJS Foundation. The
|
||||
charter amendment process is for the Maintainers to propose changes using simple
|
||||
majority of the full Maintainers, the proposed changes being subject to review
|
||||
and approval by the CPC. The CPC may additionally make amendments to the project
|
||||
charter at any time, though the CPC will not interfere with day-to-day
|
||||
discussions, votes or meetings of the Maintainers.
|
||||
|
||||
### 2.1 Other Formal Project Relationships
|
||||
|
||||
Section Intentionally Left Blank
|
||||
|
||||
## Section 3: `perspective`'s Maintainers Governing Body
|
||||
|
||||
`perspective` is governed by its [maintainers](GOVERNANCE.md#maintainers).
|
||||
|
||||
## Section 4: Roles & Responsibilities
|
||||
|
||||
The roles and responsibilities of `perspective`'s Maintainers are described in
|
||||
[GOVERNANCE.md](./GOVERNANCE.md).
|
||||
|
||||
### Section 4.1 Project Operations & Management
|
||||
|
||||
Section Intentionally Left Blank
|
||||
|
||||
### Section 4.2: Decision-making, Voting, and/or Elections
|
||||
|
||||
Section Intentionally Left Blank
|
||||
|
||||
### Section 4.3: Other Project Roles
|
||||
|
||||
Section Intentionally Left Blank
|
||||
|
||||
## Section 5: Definitions
|
||||
|
||||
- _Contributors_: contribute code or other artifacts, but do not have the right
|
||||
to commit to the codebase. Contributors work with the project’s maintainers to
|
||||
have code committed to the code base. A Contributor may be promoted to a
|
||||
Maintainer by the Maintainers. Contributors should rarely be encumbered by the
|
||||
Maintainers and never by the CPC or OpenJS Foundation Board.
|
||||
|
||||
- _Maintainers_: Contributors with any kind of decision-making authority in the
|
||||
project.
|
||||
|
||||
[openjs foundation]: https://openjsf.org
|
||||
@@ -0,0 +1,135 @@
|
||||
<br />
|
||||
|
||||
<a href="https://perspective-dev.github.io">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/perspective-dev/perspective/raw/master/docs/static/svg/perspective-logo-dark.svg?raw=true">
|
||||
<img width="260" src="https://github.com/perspective-dev/perspective/raw/master/docs/static/svg/perspective-logo-light.svg?raw=true" />
|
||||
</picture>
|
||||
</a>
|
||||
<br/><br/>
|
||||
|
||||
[](https://github.com/perspective-dev/perspective/actions/workflows/build.yaml)
|
||||
[](https://www.npmjs.com/package/@perspective-dev/client)
|
||||
[](https://pypi.python.org/pypi/perspective-python)
|
||||
[](https://crates.io/crates/perspective)
|
||||
|
||||
<br/>
|
||||
|
||||
Perspective is an interactive analytics and data visualization component for
|
||||
large and streaming datasets. Build user-configurable reports, dashboards,
|
||||
notebooks, and applications with a high-performance query engine compiled to
|
||||
WebAssembly, Python, and Rust.
|
||||
|
||||
## Features
|
||||
|
||||
- A framework-agnostic user interface packaged as a
|
||||
[Custom Element](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements),
|
||||
which connects to a Data Model in-browser (via WebAssembly) or remotely (via
|
||||
WebSocket, with integration in Python, Node.js and Rust). Includes a data
|
||||
grid, 10+ chart types line, bar, area, scatter, heatmap, treemap, sunburst,
|
||||
candlestick, and more.
|
||||
|
||||
- A Data Model API for pluggable engines, enabling Perspective's UI to query
|
||||
external data sources like [DuckDB](https://duckdb.org/) while translating
|
||||
view configurations into native queries.
|
||||
|
||||
- A fast, memory-efficient streaming Data Model built-in, written in C++ and
|
||||
compiled for [WebAssembly](https://webassembly.org/),
|
||||
[Python](https://www.python.org/), and [Rust](https://www.rust-lang.org/).
|
||||
Supports read/write/streaming for [Apache Arrow](https://arrow.apache.org/),
|
||||
with a columnar expression language based on
|
||||
[ExprTK](https://github.com/ArashPartow/exprtk).
|
||||
|
||||
- A [JupyterLab](https://jupyter.org/) widget and Python client library for
|
||||
interactive data analysis in notebooks.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Project Site](https://perspective-dev.github.io/)
|
||||
- [User Guide](https://perspective-dev.github.io/guide/)
|
||||
- JavaScript API
|
||||
- [`@perspective-dev/client` Browser](https://perspective-dev.github.io/browser/modules/src_ts_perspective.browser.ts.html)
|
||||
- [`@perspective-dev/client` Node.js](https://perspective-dev.github.io/node/modules/src_ts_perspective.node.ts.html)
|
||||
- [`@perspective-dev/client` Clickhouse Virtual Server](https://perspective-dev.github.io/browser/modules/dist_esm_virtual_servers_clickhouse.js.html)
|
||||
- [`@perspective-dev/client` DuckDB Virtual Server](https://perspective-dev.github.io/browser/modules/dist_esm_virtual_servers_duckdb.js.html)
|
||||
- [`@perspective-dev/viewer` Web Component](https://perspective-dev.github.io/viewer/modules/perspective-viewer.html)
|
||||
- Python API
|
||||
- [`perspective`](https://perspective-dev.github.io/python/index.html)
|
||||
- [`perspective.widget`](https://perspective-dev.github.io/python/perspective/widget.html)
|
||||
- [`perspective.handlers.aiohttp`](https://perspective-dev.github.io/python/perspective/handlers/aiohttp.html)
|
||||
- [`perspective.handlers.starlette`](https://perspective-dev.github.io/python/perspective/handlers/starlett.html)
|
||||
- [`perspective.handlers.tornado`](https://perspective-dev.github.io/python/perspective/handlers/tornado.html)
|
||||
- [`perspective.virtual_servers.clickhouse`](https://perspective-dev.github.io/python/perspective/virtual_servers/clickhouse.html)
|
||||
- [`perspective.virtual_servers.duckdb`](https://perspective-dev.github.io/python/perspective/virtual_servers/duckdb.html)
|
||||
- Rust API
|
||||
- [`perspective`](https://docs.rs/perspective/latest/perspective/)
|
||||
- [`perspective-client`](https://docs.rs/perspective-client/latest/perspective_client/)
|
||||
- [`perspective-server`](https://docs.rs/perspective-server/latest/perspective_server/)
|
||||
- [`perspective-python`](https://docs.rs/perspective-python/latest/perspective_python/)
|
||||
- [`perspective-js`](https://docs.rs/perspective-js/latest/perspective_js/)
|
||||
- [`perspective-viewer`](https://docs.rs/perspective-viewer/latest/perspective_viewer/)
|
||||
|
||||
## Examples
|
||||
|
||||
<!-- Examples -->
|
||||
<table><tbody><tr><td>editable</td><td>file</td><td>duckdb</td></tr><tr><td><a href="https://perspective-dev.github.io/block?example=editable"><img height="125" src="https://perspective-dev.github.io/blocks/editable/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=file"><img height="125" src="https://perspective-dev.github.io/blocks/file/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=duckdb"><img height="125" src="https://perspective-dev.github.io/blocks/duckdb/preview.png?" /></a></td></tr><tr><td>fractal</td><td>market</td><td>raycasting</td></tr><tr><td><a href="https://perspective-dev.github.io/block?example=fractal"><img height="125" src="https://perspective-dev.github.io/blocks/fractal/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=market"><img height="125" src="https://perspective-dev.github.io/blocks/market/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=raycasting"><img height="125" src="https://perspective-dev.github.io/blocks/raycasting/preview.png?" /></a></td></tr><tr><td>evictions</td><td>nypd</td><td>streaming</td></tr><tr><td><a href="https://perspective-dev.github.io/block?example=evictions"><img height="125" src="https://perspective-dev.github.io/blocks/evictions/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=nypd"><img height="125" src="https://perspective-dev.github.io/blocks/nypd/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=streaming"><img height="125" src="https://perspective-dev.github.io/blocks/streaming/preview.png?" /></a></td></tr><tr><td>covid</td><td>webcam</td><td>movies</td></tr><tr><td><a href="https://perspective-dev.github.io/block?example=covid"><img height="125" src="https://perspective-dev.github.io/blocks/covid/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=webcam"><img height="125" src="https://perspective-dev.github.io/blocks/webcam/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=movies"><img height="125" src="https://perspective-dev.github.io/blocks/movies/preview.png?" /></a></td></tr><tr><td>superstore</td><td>olympics</td><td>dataset</td></tr><tr><td><a href="https://perspective-dev.github.io/block?example=superstore"><img height="125" src="https://perspective-dev.github.io/blocks/superstore/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=olympics"><img height="125" src="https://perspective-dev.github.io/blocks/olympics/preview.png?" /></a></td><td><a href="https://perspective-dev.github.io/block?example=dataset"><img height="125" src="https://perspective-dev.github.io/blocks/dataset/preview.png?" /></a></td></tr></tbody></table>
|
||||
<!-- Examples -->
|
||||
|
||||
## Media
|
||||
|
||||
<table><tbody>
|
||||
<tr>
|
||||
<td><a href="https://github.com/timkpaine"><code>@timkpaine</code></a></td>
|
||||
<td><a href="https://github.com/timbess"><code>@timbess</code></a></td>
|
||||
<td><a href="https://github.com/sc1f"><code>@sc1f</code></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.youtube.com/watch?v=v5Y5ftlGNhU"><img width="240" src="https://img.youtube.com/vi/v5Y5ftlGNhU/0.jpg" /></a></td>
|
||||
<td><a href="https://www.youtube.com/watch?v=lDpIu4dnp78"><img width="240" src="https://img.youtube.com/vi/lDpIu4dnp78/0.jpg" /></a></td>
|
||||
<td><a href="https://www.youtube.com/watch?v=IO-HJsGdleE"><img width="240" src="https://img.youtube.com/vi/IO-HJsGdleE/0.jpg" /></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/texodus"><code>@texodus</code></a></td>
|
||||
<td><a href="https://github.com/texodus"><code>@texodus</code></a></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.youtube.com/watch?v=no0qChjvdgQ"><img width="240" src="https://img.youtube.com/vi/no0qChjvdgQ/0.jpg" /></a></td>
|
||||
<td><a href="https://www.youtube.com/watch?v=0ut-ynvBpGI"><img width="240" src="https://img.youtube.com/vi/0ut-ynvBpGI/0.jpg" /></a></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody></table><br/><br/>
|
||||
|
||||
---
|
||||
|
||||
<br/>
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/openjs-foundation/artwork/raw/master/openjs_foundation/openjs_foundation-logo-horizontal-white.svg?raw=true">
|
||||
<img width="200" src="https://github.com/openjs-foundation/artwork/raw/master/openjs_foundation/openjs_foundation-logo-horizontal-black.svg?raw=true">
|
||||
</picture>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
The Perspective project is a member of the
|
||||
[The OpenJS Foundation](https://openjsf.org/).
|
||||
|
||||
Copyright [OpenJS Foundation](https://openjsf.org) and Perspective contributors.
|
||||
All rights reserved. The [OpenJS Foundation](https://openjsf.org) has registered
|
||||
trademarks and uses trademarks. For a list of trademarks of the
|
||||
[OpenJS Foundation](https://openjsf.org), please see our
|
||||
[Trademark Policy](https://trademark-policy.openjsf.org/) and
|
||||
[Trademark List](https://trademark-list.openjsf.org/). Trademarks and logos not
|
||||
indicated on the
|
||||
[list of OpenJS Foundation trademarks](https://trademark-list.openjsf.org) are
|
||||
trademarks™ or registered® trademarks of their respective holders. Use of them
|
||||
does not imply any affiliation with or endorsement by them.
|
||||
|
||||
[The OpenJS Foundation](https://openjsf.org/) |
|
||||
[Terms of Use](https://terms-of-use.openjsf.org/) |
|
||||
[Privacy Policy](https://privacy-policy.openjsf.org/) |
|
||||
[Bylaws](https://bylaws.openjsf.org/) |
|
||||
[Code of Conduct](https://code-of-conduct.openjsf.org) |
|
||||
[Trademark Policy](https://trademark-policy.openjsf.org/) |
|
||||
[Trademark List](https://trademark-list.openjsf.org/) |
|
||||
[Cookie Policy](https://www.linuxfoundation.org/cookies/)
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`perspective-dev/perspective`
|
||||
- 原始仓库:https://github.com/perspective-dev/perspective
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are applied only to the latest release.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a security issue, please use the GitHub Security Advisory
|
||||
["Report a Vulnerability"](https://github.com/perspective-dev/perspective/security/advisories/new)
|
||||
tab.
|
||||
|
||||
Report security bugs in third-party modules to the person or team maintaining
|
||||
the module. You can also report a vulnerability through the
|
||||
[npm contact form](https://www.npmjs.com/support) by selecting "I'm reporting a
|
||||
security vulnerability".
|
||||
|
||||
## Escalation
|
||||
|
||||
If you do not receive an acknowledgement of your report within 6 business days,
|
||||
or if you cannot find a private security contact for the project, you may
|
||||
escalate to the OpenJS Foundation CNA at `security@lists.openjsf.org`.
|
||||
|
||||
If the project acknowledges your report but does not provide any further
|
||||
response or engagement within 14 days, escalation is also appropriate.
|
||||
|
||||
## Threat Model
|
||||
|
||||
The Perspective WebSocket `Server` (the Python `tornado.py`/`aiohttp.py`/
|
||||
`starlette.py` adapters and the Node `WebSocketServer`) is not a security
|
||||
boundary against its `Client`. Any `Client` that can send messages to a
|
||||
`Server` is treated as the author of the queries it submits, and is permitted
|
||||
to create or delete `Table`/`View` resources, author arbitrary
|
||||
[expression columns](./docs/md/explanation/view/config/expressions.md), and —
|
||||
for `Virtual Server` backends (DuckDB, ClickHouse, Polars, custom
|
||||
`VirtualServerHandler`) — author SQL fragments executed under the configured
|
||||
database role. The `Virtual Server` SQL builder does not parameterize or
|
||||
validate client-supplied identifiers, expressions, or operators, because
|
||||
there is no privilege boundary inside the engine for it to enforce.
|
||||
|
||||
The bundled WebSocket adapters above are reference integrations: they do not
|
||||
implement authentication, authorization, CSRF protection, rate limiting, or
|
||||
origin enforcement, and are not intended to be exposed directly to untrusted
|
||||
networks. Production deployments must place an authenticating reverse proxy,
|
||||
application-framework middleware, or API gateway between the network and the
|
||||
`Server`.
|
||||
|
||||
### In-browser WASM deployments are not affected
|
||||
|
||||
This applies only when the `Server` runs in a separate process reached
|
||||
over a network transport (WebSocket). In-browser deployments — including
|
||||
`perspective` running entirely in a Web Worker, the
|
||||
[`perspective-server` WASM build](./docs/md/explanation/architecture.md),
|
||||
[`duckdb-wasm`](./docs/md/how_to/javascript/virtual_server/duckdb.md),
|
||||
and any other `Virtual Server` whose backend executes inside the browser
|
||||
tab — do not have this concern. The `Client` and `Server` share a single
|
||||
security context (the browser tab, under the same-origin policy of the
|
||||
embedding page), there is no network transport for a third-party principal
|
||||
to reach, and the only principal who can submit queries is the same user who
|
||||
loaded the page. SQL or expression "injection" by that user against a backend
|
||||
running inside their own tab is not a privilege escalation.
|
||||
|
||||
### In scope
|
||||
|
||||
The following remain in scope for security reports:
|
||||
|
||||
- Memory-safety bugs in the C++ engine, Rust crates, or WASM module.
|
||||
- Bugs in the `<perspective-viewer>` Shadow DOM, CSS, or sanitization paths
|
||||
that allow injected markup or styles to escape the component or affect
|
||||
the embedding page.
|
||||
- Crashes, hangs, panics, or denial-of-service in the engine reachable from
|
||||
well-formed protobuf messages.
|
||||
- Breaches of the trust model above — for example, a `Client` causing effects
|
||||
on a different `Client`'s `Server` state in a configuration where those
|
||||
`Client`s share a `Server` but are intended to be isolated, or an
|
||||
expression column reaching state outside the `Server` it was authored
|
||||
against.
|
||||
- Vulnerabilities in the published artifacts themselves (supply-chain).
|
||||
@@ -0,0 +1,39 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
||||
# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
||||
# ┃ This file is part of the Perspective library, distributed under the terms ┃
|
||||
# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
[book]
|
||||
authors = ["Andrew Stein"]
|
||||
language = "en"
|
||||
src = "md"
|
||||
title = "Perspective"
|
||||
|
||||
[build]
|
||||
build-dir = "static/guide"
|
||||
|
||||
[output.html]
|
||||
# theme = "my-theme"
|
||||
# default-theme = "light"
|
||||
# preferred-dark-theme = "navy"
|
||||
# smart-punctuation = true
|
||||
# mathjax-support = false
|
||||
git-repository-url = "https://github.com/perspective-dev/perspective"
|
||||
site-url = "https://perspective-dev.github.io/guide/"
|
||||
additional-css = [
|
||||
"md/perspective.css",
|
||||
"node_modules/@perspective-dev/viewer/dist/css/themes.css",
|
||||
]
|
||||
# additional-js = []
|
||||
# no-section-label = false
|
||||
# edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||
# site-url = "/guide/"
|
||||
# cname = "myproject.rs"
|
||||
# input-404 = "not-found.md"
|
||||
@@ -0,0 +1,176 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as esbuild from "esbuild";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { createRequire } from "module";
|
||||
import { bundleAsync as bundleCssAsync, composeVisitors } from "lightningcss";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DIST = path.join(__dirname, "dist");
|
||||
|
||||
function copyRecursive(src, dest) {
|
||||
if (!fs.existsSync(src)) return;
|
||||
const stat = fs.statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const child of fs.readdirSync(src)) {
|
||||
copyRecursive(path.join(src, child), path.join(dest, child));
|
||||
}
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
// Inline url() asset references as data URIs.
|
||||
export function inlineUrlVisitor(fromFile) {
|
||||
const dir = path.dirname(fromFile);
|
||||
return composeVisitors([
|
||||
{
|
||||
Url(url) {
|
||||
const ext = path.extname(url.url).toLowerCase();
|
||||
if (![".svg", ".png", ".gif"].includes(ext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = path.resolve(dir, url.url);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
throw new Error(`File not found ${url.url}`);
|
||||
// return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(resolved);
|
||||
const mime =
|
||||
ext === ".svg"
|
||||
? "image/svg+xml"
|
||||
: ext === ".png"
|
||||
? "image/png"
|
||||
: "image/gif";
|
||||
|
||||
const new_content = content
|
||||
.toString("base64")
|
||||
.split("\n")
|
||||
.map((x) => x.trim())
|
||||
.join("");
|
||||
|
||||
return {
|
||||
url: `data:${mime};base64,${new_content}`,
|
||||
loc: url.loc,
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export const resolveNPM = (url) => ({
|
||||
read(filePath) {
|
||||
if (filePath.startsWith("http")) {
|
||||
return `@import url("${filePath}");`;
|
||||
}
|
||||
|
||||
return fs.readFileSync(filePath, "utf8");
|
||||
},
|
||||
resolve(specifier, from) {
|
||||
if (specifier.startsWith("http")) {
|
||||
return { external: specifier };
|
||||
}
|
||||
|
||||
const _require = createRequire(url);
|
||||
|
||||
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
||||
return path.resolve(path.dirname(from), specifier);
|
||||
}
|
||||
|
||||
return _require.resolve(specifier);
|
||||
},
|
||||
});
|
||||
|
||||
async function build() {
|
||||
// Clean and create dist
|
||||
fs.mkdirSync(DIST, { recursive: true });
|
||||
|
||||
// Bundle CSS
|
||||
const { code: cssCode } = await bundleCssAsync({
|
||||
filename: path.join(__dirname, "./src/css/style.css"),
|
||||
minify: true,
|
||||
resolver: resolveNPM(import.meta.url),
|
||||
visitor: inlineUrlVisitor("./src/css/style.css"),
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(DIST, "css"), { recursive: true });
|
||||
fs.writeFileSync(path.join(DIST, "style.css"), cssCode);
|
||||
|
||||
// Bundle JS entry points
|
||||
await esbuild.build({
|
||||
entryPoints: [
|
||||
path.join(__dirname, "src/index.ts"),
|
||||
path.join(__dirname, "src/examples.ts"),
|
||||
path.join(__dirname, "src/block.ts"),
|
||||
],
|
||||
bundle: true,
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
outdir: DIST,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
target: ["es2022"],
|
||||
define: {
|
||||
global: "window",
|
||||
},
|
||||
loader: {
|
||||
".wasm": "file",
|
||||
".arrow": "file",
|
||||
},
|
||||
});
|
||||
|
||||
// Copy HTML files
|
||||
for (const html of ["index.html", "examples.html", "block.html"]) {
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, "src", html),
|
||||
path.join(DIST, html),
|
||||
);
|
||||
}
|
||||
|
||||
// Copy static assets
|
||||
copyRecursive(path.join(__dirname, "static"), DIST);
|
||||
|
||||
// Generate blocks manifest
|
||||
const blocksDir = path.join(DIST, "blocks");
|
||||
if (fs.existsSync(blocksDir)) {
|
||||
const manifest = {};
|
||||
for (const example of fs.readdirSync(blocksDir)) {
|
||||
const exDir = path.join(blocksDir, example);
|
||||
if (!fs.statSync(exDir).isDirectory()) continue;
|
||||
manifest[example] = fs
|
||||
.readdirSync(exDir)
|
||||
.filter(
|
||||
(f) =>
|
||||
!f.startsWith(".") &&
|
||||
!f.endsWith(".png") &&
|
||||
!f.endsWith(".arrow"),
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(blocksDir, "manifest.json"),
|
||||
JSON.stringify(manifest),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Build complete: dist/");
|
||||
}
|
||||
|
||||
build().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import puppeteer from "puppeteer";
|
||||
import * as fs from "node:fs";
|
||||
import * as cp from "node:child_process";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import EXAMPLES from "./src/data/features.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// features.js uses CJS exports.default, import it dynamically
|
||||
// const EXAMPLES = (await import("./src/data/features.ts")).default;
|
||||
|
||||
const perspective = import(
|
||||
"@perspective-dev/client/dist/esm/perspective.node.js"
|
||||
);
|
||||
|
||||
const DEFAULT_VIEWPORT = {
|
||||
width: 400,
|
||||
height: 300,
|
||||
};
|
||||
|
||||
async function run_with_theme(page, is_dark = false, order) {
|
||||
await page.goto("http://localhost:8080/");
|
||||
await page.setContent(template(is_dark));
|
||||
await page.setViewport(DEFAULT_VIEWPORT);
|
||||
await page.evaluate(async () => {
|
||||
while (!window.__TEST_PERSPECTIVE_READY__) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
});
|
||||
|
||||
await page.evaluate(async function () {
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
await viewer.flush();
|
||||
await viewer.toggleConfig();
|
||||
});
|
||||
|
||||
for (const idx in EXAMPLES) {
|
||||
const { config, viewport } = EXAMPLES[idx];
|
||||
await page.setViewport(viewport || DEFAULT_VIEWPORT);
|
||||
const new_config = Object.assign(
|
||||
{
|
||||
plugin: "Datagrid",
|
||||
group_by: [],
|
||||
expressions: {},
|
||||
split_by: [],
|
||||
sort: [],
|
||||
aggregates: {},
|
||||
},
|
||||
config,
|
||||
);
|
||||
console.log(JSON.stringify(new_config));
|
||||
|
||||
await page.evaluate(async (config) => {
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
await viewer.reset();
|
||||
await viewer.restore(config);
|
||||
}, new_config);
|
||||
|
||||
const screenshot = await page.screenshot({
|
||||
captureBeyondViewport: false,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
const name = `static/features/feature_${idx}${
|
||||
is_dark ? "_dark" : ""
|
||||
}.png`;
|
||||
|
||||
fs.writeFileSync(name, screenshot);
|
||||
cp.execSync(`convert ${name} -resize 200x150 ${name}`);
|
||||
}
|
||||
|
||||
const suffix = is_dark ? "_dark" : "";
|
||||
const montage_files = order.map(
|
||||
(idx) => `static/features/feature_${idx}${suffix}.png`,
|
||||
);
|
||||
|
||||
cp.execSync(
|
||||
`montage -mode concatenate -background none -tile 5x ${montage_files.join(
|
||||
" ",
|
||||
)} static/features/montage${is_dark ? "_dark" : "_light"}.png`,
|
||||
);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (
|
||||
!fs.existsSync("static/features") ||
|
||||
fs.readdirSync("static/features").length === 0
|
||||
) {
|
||||
console.log("Generating feature screenshots!");
|
||||
fs.mkdirSync(path.join(__dirname, "static/features"), {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const x = await perspective;
|
||||
const server = new x.WebSocketServer({
|
||||
assets: [
|
||||
path.join(__dirname, "."),
|
||||
path.join(__dirname, "../node_modules"),
|
||||
],
|
||||
});
|
||||
|
||||
const indices = Array.from({ length: EXAMPLES.length }, (_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[indices[i], indices[j]] = [indices[j], indices[i]];
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
await run_with_theme(page, false, indices);
|
||||
await run_with_theme(page, true, indices);
|
||||
await page.close();
|
||||
await browser.close();
|
||||
await server.close();
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, "static/features/montage_map.json"),
|
||||
JSON.stringify({
|
||||
tile_width: 200,
|
||||
tile_height: 150,
|
||||
columns: 5,
|
||||
order: indices,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!fs.existsSync("static/blocks")) {
|
||||
fs.mkdirSync("static/blocks");
|
||||
}
|
||||
|
||||
const { dist_examples } = await import("../examples/blocks/index.mjs");
|
||||
await dist_examples(`${__dirname}/static/blocks`);
|
||||
}
|
||||
|
||||
function template(is_dark) {
|
||||
return fs
|
||||
.readFileSync(path.join(__dirname, "template.html"))
|
||||
.toString()
|
||||
.replace("/css/pro.css", is_dark ? "/css/pro-dark.css" : "/css/pro.css")
|
||||
.trim();
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
mdbook:
|
||||
container_name: mdbook
|
||||
image: peaceiris/mdbook:v0.5.0
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 3001:3001
|
||||
volumes:
|
||||
- ${PWD}/..:/repo
|
||||
working_dir: /repo/docs
|
||||
command:
|
||||
- serve
|
||||
- --hostname
|
||||
- "0.0.0.0"
|
||||
@@ -0,0 +1,68 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
// ┃ 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). ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, "..");
|
||||
const DIST = path.join(__dirname, "dist");
|
||||
const STAGING = path.join(REPO_ROOT, "dist-gh-pages");
|
||||
const BRANCH = "gh-pages";
|
||||
|
||||
function git(args, opts = {}) {
|
||||
return execFileSync("git", args, {
|
||||
stdio: "inherit",
|
||||
cwd: REPO_ROOT,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
function copyRecursive(src, dest) {
|
||||
const stat = fs.statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const child of fs.readdirSync(src)) {
|
||||
copyRecursive(path.join(src, child), path.join(dest, child));
|
||||
}
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(DIST)) {
|
||||
console.error(`Missing ${DIST} — run \`npm run build\` first.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(STAGING)) {
|
||||
git(["worktree", "add", STAGING, BRANCH]);
|
||||
} else {
|
||||
git(["fetch", "origin", BRANCH]);
|
||||
git(["checkout", `origin/${BRANCH}`], { cwd: STAGING });
|
||||
}
|
||||
|
||||
// Clear tracked + untracked content in the staging worktree, preserving
|
||||
// the worktree's `.git` link.
|
||||
git(["rm", "-rf", "--quiet", "--ignore-unmatch", "."], { cwd: STAGING });
|
||||
git(["clean", "-fdx"], { cwd: STAGING });
|
||||
|
||||
for (const entry of fs.readdirSync(DIST)) {
|
||||
copyRecursive(path.join(DIST, entry), path.join(STAGING, entry));
|
||||
}
|
||||
|
||||
git(["add", "-A"], { cwd: STAGING });
|
||||
|
||||
console.log(`Staged dist/ onto ${BRANCH} at ${STAGING}`);
|
||||
console.log(`Review with \`git -C ${STAGING} status\`, then commit and push.`);
|
||||
@@ -0,0 +1 @@
|
||||
<!-- Empty page to allow HTML injection from `build.js` screenshot generator-->
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
# FAQ
|
||||
|
||||
## Installation
|
||||
|
||||
### Python installation fails on Windows
|
||||
|
||||
Python wheels are published for supported Python versions and platforms. On
|
||||
Windows, ensure you have a compatible Python version and architecture. Install
|
||||
with:
|
||||
|
||||
```bash
|
||||
pip install perspective-python
|
||||
```
|
||||
|
||||
If you encounter C++ binding errors or link errors, make sure you are using a
|
||||
supported Python version and that your `pip` is up to date. Pre-built wheels
|
||||
eliminate the need for a C++ compiler in most cases.
|
||||
|
||||
<!-- _Related: [#928](https://github.com/perspective-dev/perspective/issues/928),
|
||||
[#1325](https://github.com/perspective-dev/perspective/issues/1325),
|
||||
[#1025](https://github.com/perspective-dev/perspective/issues/1025)_ -->
|
||||
|
||||
### Python `import perspective` fails with `ImportError` or undefined symbol
|
||||
|
||||
This typically happens when the C++ shared library (`libpsp.so`) cannot be found
|
||||
or was built against a different Python version. Ensure your Python version
|
||||
matches the installed wheel. On Linux, verify that required system libraries are
|
||||
present. If you see errors about `libpsp.so` or undefined symbols, try
|
||||
reinstalling in a clean virtual environment.
|
||||
|
||||
<!-- _Related: [#937](https://github.com/perspective-dev/perspective/issues/937),
|
||||
[#1120](https://github.com/perspective-dev/perspective/issues/1120),
|
||||
[#1216](https://github.com/perspective-dev/perspective/issues/1216),
|
||||
[#1332](https://github.com/perspective-dev/perspective/issues/1332)_ -->
|
||||
|
||||
### Python installation fails on macOS
|
||||
|
||||
On Apple Silicon (M1/M2/M3), make sure you are using a native ARM Python build,
|
||||
not one running under Rosetta. The published wheels include `aarch64` variants
|
||||
for supported platforms.
|
||||
|
||||
<!-- _Related: [#938](https://github.com/perspective-dev/perspective/issues/938),
|
||||
[#1170](https://github.com/perspective-dev/perspective/issues/1170)_ -->
|
||||
|
||||
### How do I install Perspective in a Docker container?
|
||||
|
||||
Perspective's Python wheels are built against `manylinux_2_28` containers (see
|
||||
[`.github/workflows/build.yaml`](../../.github/workflows/build.yaml)), so they
|
||||
are compatible with most Linux distributions based on glibc 2.28+ (e.g., Debian
|
||||
10+, Ubuntu 20.04+, RHEL 8+). Use a compatible base image:
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
RUN pip install perspective-python
|
||||
```
|
||||
|
||||
Alpine Linux uses musl instead of glibc and is **not** compatible with the
|
||||
published wheels.
|
||||
|
||||
<!-- _Related: [#1201](https://github.com/perspective-dev/perspective/issues/1201)_ -->
|
||||
|
||||
## JavaScript Bundling
|
||||
|
||||
### How do I use Perspective with Vite, Webpack, or esbuild?
|
||||
|
||||
Perspective no longer exports bundler plugins. Instead, you must manually
|
||||
bootstrap the WASM binaries using your bundler's asset handling. See
|
||||
[Importing with or without a bundler](./how_to/javascript/importing.md) for
|
||||
complete examples for Vite, Webpack, esbuild, CDN, and inline builds.
|
||||
|
||||
<!-- _Related: [#1734](https://github.com/perspective-dev/perspective/issues/1734),
|
||||
[#2725](https://github.com/perspective-dev/perspective/issues/2725),
|
||||
[#857](https://github.com/perspective-dev/perspective/issues/857),
|
||||
[#1497](https://github.com/perspective-dev/perspective/issues/1497),
|
||||
[#1655](https://github.com/perspective-dev/perspective/issues/1655)_ -->
|
||||
|
||||
## Framework Integration
|
||||
|
||||
### How do I use Perspective with React?
|
||||
|
||||
Perspective provides a dedicated
|
||||
[React component](./how_to/javascript/react.md). You must also still initialize
|
||||
Perspective's WebAssembly as per your bundler — see
|
||||
[Importing with or without a bundler](./how_to/javascript/importing.md).
|
||||
|
||||
<!-- _Related: [#865](https://github.com/perspective-dev/perspective/issues/865),
|
||||
[#931](https://github.com/perspective-dev/perspective/issues/931),
|
||||
[#3023](https://github.com/perspective-dev/perspective/discussions/3023)_ -->
|
||||
|
||||
### How do I use Perspective with Next.js?
|
||||
|
||||
Perspective relies on Web Workers and WASM, which require client-side rendering.
|
||||
Use dynamic imports with `ssr: false` in Next.js to load Perspective components
|
||||
only on the client.
|
||||
|
||||
<!-- _Related:
|
||||
[#2947](https://github.com/perspective-dev/perspective/discussions/2947),
|
||||
[#2181](https://github.com/perspective-dev/perspective/discussions/2181)_ -->
|
||||
|
||||
### How do I use Perspective with Vue.js/Angular/etc?
|
||||
|
||||
As a standard Web Component, `<perspective-viewer>` works in most JavaScript web
|
||||
frameworks directly via standard HTML/DOM APIs, but does not have dedicated
|
||||
integration libraries for these frameworks.
|
||||
|
||||
<!-- _Related:
|
||||
[#2787](https://github.com/perspective-dev/perspective/discussions/2787)_ -->
|
||||
|
||||
## Expressions
|
||||
|
||||
### How do I create computed/expression columns?
|
||||
|
||||
Use the [`expressions`](./explanation/view/config/expressions.md) config option
|
||||
in your `View` to define new columns with ExprTK syntax, which must then be
|
||||
_used_ somewhere else in your config (like `columns`) to actually be visible &
|
||||
calculated. In `<perspective-viewer>`, expression columns can be created from
|
||||
the UI column sidebar by clicking the "New Column" button.
|
||||
|
||||
<!-- _Related: [#1981](https://github.com/perspective-dev/perspective/issues/1981),
|
||||
[#2148](https://github.com/perspective-dev/perspective/issues/2148),
|
||||
[#1493](https://github.com/perspective-dev/perspective/issues/1493)_ -->
|
||||
|
||||
### Can I reference one expression column from another?
|
||||
|
||||
No, you must duplicate calculations that are shared between expression columns.
|
||||
|
||||
<!-- _Related: [#2148](https://github.com/perspective-dev/perspective/issues/2148)_ -->
|
||||
|
||||
### Can I do date arithmetic in expressions?
|
||||
|
||||
Yes, but they must be converted to `float` values first (`integer` is an `i32`
|
||||
which is too small). See
|
||||
[Expressions](./explanation/view/config/expressions.md).
|
||||
|
||||
<!-- _Related: [#3026](https://github.com/perspective-dev/perspective/issues/3026),
|
||||
[#1768](https://github.com/perspective-dev/perspective/issues/1768)_ -->
|
||||
|
||||
### Can I do rolling sums or cumulative calculations?
|
||||
|
||||
Not in Perspective's built-in engine, but as an alternative, DuckDB supports
|
||||
[rolling and cumulative sums via `WINDOW` functions](https://duckdb.org/docs/stable/sql/functions/window_functions),
|
||||
and DuckDB now has
|
||||
[native Perspective Virtual Server support](./explanation/virtual_servers.md)
|
||||
which allows arbitrary DuckDB queries (as a `TABLE` or `VIEW`) to be
|
||||
`<perspective-viewer>` `Table`s.
|
||||
|
||||
<!-- _Related:
|
||||
[#2600](https://github.com/perspective-dev/perspective/discussions/2600),
|
||||
[#2624](https://github.com/perspective-dev/perspective/issues/2624)_ -->
|
||||
|
||||
## Filters
|
||||
|
||||
### Can I compose filters with OR logic?
|
||||
|
||||
Perspective
|
||||
[filters](./explanation/view/config/selection_and_ordering.md#filter) are
|
||||
composed with AND logic by default. As an alternative, you can use
|
||||
[expression columns](./explanation/view/config/expressions.md) to create a
|
||||
boolean column that encodes your OR logic (or any arbitrary multi-column
|
||||
predicate), then filter on that column:
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
expressions: {
|
||||
or_filter:
|
||||
"if (\"State\" == 'Texas') true; else if (\"State\" == 'California') true; else false",
|
||||
},
|
||||
filter: [["or_filter", "==", true]],
|
||||
});
|
||||
```
|
||||
|
||||
<!-- _Related: [#1192](https://github.com/perspective-dev/perspective/issues/1192)_ -->
|
||||
|
||||
### How do I update filters programmatically?
|
||||
|
||||
Set the [`filter`](./explanation/view/config/selection_and_ordering.md#filter)
|
||||
property on a `View` config, or use the `<perspective-viewer>`
|
||||
[`.restore()`](./how_to/javascript/save_restore.md) method to update filters at
|
||||
runtime.
|
||||
|
||||
<!-- _Related: [#935](https://github.com/perspective-dev/perspective/issues/935)_ -->
|
||||
|
||||
### Does date filtering support ranges?
|
||||
|
||||
Date columns can be
|
||||
[filtered](./explanation/view/config/selection_and_ordering.md#filter) with
|
||||
comparison operators (`>`, `<`, `>=`, `<=`) to achieve range-based filtering.
|
||||
Apply two filters on the same date column for a range.
|
||||
|
||||
<!-- _Related:
|
||||
[#3100](https://github.com/perspective-dev/perspective/discussions/3100),
|
||||
[#2023](https://github.com/perspective-dev/perspective/issues/2023)_ -->
|
||||
|
||||
## JupyterLab
|
||||
|
||||
### `PerspectiveWidget` is not loading in JupyterLab
|
||||
|
||||
See the [`PerspectiveWidget` guide](./how_to/python/jupyterlab.md) for full
|
||||
setup details. Ensure the JupyterLab extension version matches your
|
||||
`perspective-python` version. Make sure you are using a compatible JupyterLab
|
||||
for your Perspective version (JupyterLab 4+ currently).
|
||||
|
||||
Check that the extension is enabled with `jupyter labextension list`.
|
||||
|
||||
<!-- _Related: [#1392](https://github.com/perspective-dev/perspective/issues/1392),
|
||||
[#2059](https://github.com/perspective-dev/perspective/issues/2059),
|
||||
[#2307](https://github.com/perspective-dev/perspective/issues/2307)_ -->
|
||||
|
||||
## Memory and Performance
|
||||
|
||||
### Perspective has a memory leak
|
||||
|
||||
Maybe, but please review the
|
||||
[Cleaning up resources](./how_to/javascript/deleting.md) docs carefully before
|
||||
opening an Issue reporting it (and of course review
|
||||
[`CONTRIBUTING.md`](https://github.com/perspective-dev/perspective/blob/master/CONTRIBUTING.md)
|
||||
before opening _any_ Issue). Ensure you call `.delete()` on Views, Tables, and
|
||||
`<perspective-viewer>` instances when they are no longer needed, in reverse
|
||||
dependency order.
|
||||
|
||||
<!-- _Related: [#1037](https://github.com/perspective-dev/perspective/issues/1037),
|
||||
[#1723](https://github.com/perspective-dev/perspective/issues/1723),
|
||||
[#3035](https://github.com/perspective-dev/perspective/issues/3035),
|
||||
[#1329](https://github.com/perspective-dev/perspective/issues/1329)_ -->
|
||||
|
||||
### How many rows can Perspective's built-in engine handle?
|
||||
|
||||
Perspective is designed for large datasets and can handle millions of rows
|
||||
depending on the number of columns and available memory. Performance also
|
||||
significantly depends on column types (`"string"` being slower and larger than
|
||||
other types due to dictionary interning).
|
||||
|
||||
For larger datasets or out-of-memory virtualized datasets, see
|
||||
[Virtual Servers](./explanation/virtual_servers.md).
|
||||
|
||||
<!-- _Related: [#341](https://github.com/perspective-dev/perspective/issues/341),
|
||||
[#1719](https://github.com/perspective-dev/perspective/issues/1719),
|
||||
[#1089](https://github.com/perspective-dev/perspective/issues/1089)_ -->
|
||||
|
||||
### How do I control threading in `perspective-python`?
|
||||
|
||||
The Python library uses a thread pool internally. For advanced threading
|
||||
control, consult the
|
||||
[multithreading documentation](./how_to/python/multithreading.md).
|
||||
|
||||
<!-- _Related: [#1145](https://github.com/perspective-dev/perspective/issues/1145),
|
||||
[#1313](https://github.com/perspective-dev/perspective/issues/1313)_ -->
|
||||
|
||||
## Theming and Styling
|
||||
|
||||
### How do I enable dark theme?
|
||||
|
||||
Import `themes.css` (see [Theming](./how_to/javascript/theming.md)) and set the
|
||||
theme via `restore()`:
|
||||
|
||||
```javascript
|
||||
await viewer.restore({ theme: "Pro Dark" });
|
||||
```
|
||||
|
||||
Or import just the dark theme directly:
|
||||
`import "@perspective-dev/viewer/dist/css/pro-dark.css";`
|
||||
|
||||
<!-- _Related: [#950](https://github.com/perspective-dev/perspective/issues/950),
|
||||
[#882](https://github.com/perspective-dev/perspective/issues/882)_ -->
|
||||
|
||||
### Can I create a custom cell renderer for the datagrid?
|
||||
|
||||
The datagrid plugin supports custom styling via
|
||||
[`column_config`](https://perspective-dev.github.io/viewer/types/src_ts_ts-rs_ColumnConfigValues.ts.ColumnConfigValues.html)
|
||||
and CSS custom properties, but custom cell renderers require building a custom
|
||||
plugin.
|
||||
|
||||
<!-- _Related: [#1508](https://github.com/perspective-dev/perspective/issues/1508)_ -->
|
||||
|
||||
### How do I customize chart colors?
|
||||
|
||||
Chart colors can be customized via
|
||||
[CSS custom properties](./how_to/javascript/theming.md#custom-themes) on the
|
||||
`<perspective-viewer>` element.
|
||||
|
||||
<!-- _Related:
|
||||
[#2810](https://github.com/perspective-dev/perspective/discussions/2810),
|
||||
[#2859](https://github.com/perspective-dev/perspective/discussions/2859),
|
||||
[#2000](https://github.com/perspective-dev/perspective/discussions/2000)_ -->
|
||||
|
||||
## Streaming and Real-Time Updates
|
||||
|
||||
### How do I stream data into a Perspective table?
|
||||
|
||||
Use [`table.update()`](./explanation/table/update_and_remove.md) to push new
|
||||
data incrementally. For [indexed](./explanation/table/options.md) tables,
|
||||
updates with matching index values will replace existing rows.
|
||||
|
||||
<!-- _Related: [#1133](https://github.com/perspective-dev/perspective/issues/1133),
|
||||
[#1054](https://github.com/perspective-dev/perspective/issues/1054)_ -->
|
||||
|
||||
### `table.update()` raises "No Running Event Loop"
|
||||
|
||||
Perspective 3+ is now threadsafe by default and no longer requires special loop
|
||||
integration.
|
||||
|
||||
<!-- _Related:
|
||||
[#2801](https://github.com/perspective-dev/perspective/discussions/2801)_ -->
|
||||
|
||||
### How do I listen for data updates?
|
||||
|
||||
Use `view.on_update()` to register a callback that fires when the underlying
|
||||
table data changes. See [Listening for events](./how_to/javascript/events.md)
|
||||
and [Advanced View Operations](./explanation/view/advanced.md#update-callbacks).
|
||||
|
||||
<!-- _Related: [#1152](https://github.com/perspective-dev/perspective/issues/1152),
|
||||
[#2912](https://github.com/perspective-dev/perspective/discussions/2912)_ -->
|
||||
|
||||
## Server Architecture
|
||||
|
||||
### What is the difference between Client-only, Client/Server, and Server-only modes?
|
||||
|
||||
- **Client-only**: The Perspective engine runs entirely in the browser via WASM.
|
||||
Best for small to medium datasets.
|
||||
- **Client/Server (replicated)**: Data is hosted on a server and replicated to
|
||||
the client. The client has a full copy and performs queries locally.
|
||||
- **Server-only**: All queries are executed on the server. The client only
|
||||
renders results. Best for very large datasets.
|
||||
|
||||
See [Data Architecture](./explanation/architecture.md) for detailed explanations
|
||||
of each mode.
|
||||
|
||||
<!-- _Related:
|
||||
[#2916](https://github.com/perspective-dev/perspective/discussions/2916)_ -->
|
||||
|
||||
### Is the WebSocket Perspective `Server` safe to expose to untrusted clients?
|
||||
|
||||
No. The WebSocket `Server` is not a security boundary. Every connected `Client`
|
||||
is treated as the author of the queries it submits, and is permitted to create
|
||||
and delete `Table`/`View` resources, author arbitrary
|
||||
[expression columns](./explanation/view/config/expressions.md), and — for
|
||||
[Virtual Server](./explanation/virtual_servers.md) backends like DuckDB or
|
||||
ClickHouse — author SQL fragments executed under the configured database
|
||||
role. The bundled WebSocket adapters
|
||||
(`tornado.py`/`aiohttp.py`/`starlette.py`/`WebSocketServer`) are reference
|
||||
integrations and do not authenticate, authorize, or enforce origin policy.
|
||||
|
||||
WebSocket Deployments that need per-user isolation must put an authenticating
|
||||
proxy in front of the `Server`, run a least-privileged database role for any
|
||||
`Virtual Server` backend, and/or isolate users into separate `Server`
|
||||
instances. See [`SECURITY.md`](../../SECURITY.md) for the full threat model
|
||||
and deployment guidance.
|
||||
|
||||
Obviously, none of this applies to WASM DBs like Perspective and DuckDB.
|
||||
|
||||
### Does Perspective sanitize SQL `Virtual Server`s?
|
||||
|
||||
No, by design. [Virtual Server](./explanation/virtual_servers.md) backends
|
||||
interpolate client-supplied `view_id`, `table_id`, `column_name`, expression
|
||||
strings, and filter operators directly into SQL templates without
|
||||
parameterization or whitelist validation. The `Client` is the author of the
|
||||
queries — there is no privilege boundary inside the engine for sanitization
|
||||
to enforce. If your deployment needs to restrict the SQL surface area exposed
|
||||
to a `Client`, the supported boundary is the database role the `Virtual Server`
|
||||
is configured with (read-only etc), or better complete isolation via WASM
|
||||
backend.
|
||||
|
||||
### How do I set up WebSocket authentication?
|
||||
|
||||
The [`WebSocketServer`](./how_to/javascript/nodejs_server.md) does not include
|
||||
built-in authentication. Implement authentication at the transport layer (e.g.,
|
||||
via middleware in your HTTP server) before the WebSocket upgrade. For more
|
||||
complex needs, `WebSocketServer` is a simple example server based on the
|
||||
`node:http` module which can serve as a starting point for a custom server.
|
||||
|
||||
<!-- _Related:
|
||||
[#2788](https://github.com/perspective-dev/perspective/discussions/2788)_ -->
|
||||
|
||||
### Can I bind Perspective to a database?
|
||||
|
||||
Perspective supports [Virtual Servers](./explanation/virtual_servers.md) that
|
||||
proxy queries to external data sources, with built-in implementations for e.g.
|
||||
[DuckDB](./how_to/javascript/virtual_server/duckdb.md).
|
||||
|
||||
<!-- _Related: [#1255](https://github.com/perspective-dev/perspective/issues/1255),
|
||||
[#1361](https://github.com/perspective-dev/perspective/discussions/1361)_ -->
|
||||
|
||||
## Aggregation
|
||||
|
||||
### Can I apply multiple aggregates to the same column?
|
||||
|
||||
Yes, by creating a duplicate/alias for your column via
|
||||
[`expressions`](./explanation/view/config/expressions.md):
|
||||
|
||||
```javascript
|
||||
await viewer.restore({
|
||||
columns: ["Sales", "Sales 2"],
|
||||
expresions: { "Sales 2": '"Sales"' },
|
||||
aggregate: {
|
||||
Sales: "sum",
|
||||
"Sales 2": "avg",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<!-- _Related: [#272](https://github.com/perspective-dev/perspective/issues/272)_ -->
|
||||
|
||||
### Can I compute a ratio between aggregated columns?
|
||||
|
||||
Use [expression columns](./explanation/view/config/expressions.md) on an
|
||||
aggregated View to compute ratios. Define an expression that divides one column
|
||||
by another.
|
||||
|
||||
<!-- _Related:
|
||||
[#2994](https://github.com/perspective-dev/perspective/discussions/2994),
|
||||
[#3096](https://github.com/perspective-dev/perspective/discussions/3096)_ -->
|
||||
|
||||
## Data Loading and Arrow
|
||||
|
||||
### How do I load Apache Arrow data into Perspective?
|
||||
|
||||
Perspective natively accepts
|
||||
[Apache Arrow format](./explanation/table/loading_data.md). Pass an
|
||||
`ArrayBuffer` containing Arrow IPC data directly to `table()` or
|
||||
`table.update()`.
|
||||
|
||||
<!-- _Related: [#1157](https://github.com/perspective-dev/perspective/issues/1157),
|
||||
[#929](https://github.com/perspective-dev/perspective/issues/929)_ -->
|
||||
|
||||
### What data formats does Perspective accept?
|
||||
|
||||
Perspective accepts (see [Loading data](./explanation/table/loading_data.md)):
|
||||
|
||||
- **JavaScript**: JSON (row-oriented or column-oriented objects), CSV strings,
|
||||
Apache Arrow `ArrayBuffer`
|
||||
- **Python**: `dict`, `list`, `pandas.DataFrame`, `pyarrow.Table`, CSV strings,
|
||||
Apache Arrow bytes
|
||||
|
||||
<!-- _Related: [#929](https://github.com/perspective-dev/perspective/issues/929),
|
||||
[#2524](https://github.com/perspective-dev/perspective/issues/2524)_ -->
|
||||
|
||||
### CSV update fails but CSV creation works
|
||||
|
||||
When updating a table created with a schema, ensure the CSV column names and
|
||||
types match the schema exactly. Mismatched column names or types will cause
|
||||
update failures.
|
||||
|
||||
<!-- _Related: [#2524](https://github.com/perspective-dev/perspective/issues/2524)_ -->
|
||||
|
||||
## Export
|
||||
|
||||
### Can I export the viewer to HTML, PNG or PDF?
|
||||
|
||||
HTML and PNG exports are available via `viewer.export("html")` and
|
||||
`viewer.export("png")`, respectively. For PDF, render the viewer and use browser
|
||||
or headless browser screenshot capabilities.
|
||||
|
||||
<!-- _Related: [#2836](https://github.com/perspective-dev/perspective/issues/2836),
|
||||
[#2770](https://github.com/perspective-dev/perspective/discussions/2770),
|
||||
[#2772](https://github.com/perspective-dev/perspective/issues/2772)_ -->
|
||||
|
||||
### Can I export data to Excel?
|
||||
|
||||
Perspective does not have built-in Excel export. Export data via
|
||||
`view.to_csv()`, `view.to_json()`, or `view.to_arrow()` (see
|
||||
[Serializing data](./how_to/javascript/serializing.md)) and convert to Excel
|
||||
using a library like `xlsx` (JavaScript) or `openpyxl` (Python).
|
||||
|
||||
<!-- _Related:
|
||||
[#2738](https://github.com/perspective-dev/perspective/discussions/2738)_ -->
|
||||
|
||||
### How do I copy data from a cell or row?
|
||||
|
||||
Use the `"text"` export mode when data is selected:
|
||||
`await viewer.export("text")`.
|
||||
|
||||
<!-- _Related: [#2765](https://github.com/perspective-dev/perspective/issues/2765),
|
||||
[#2356](https://github.com/perspective-dev/perspective/discussions/2356)_ -->
|
||||
|
||||
## Table Operations
|
||||
|
||||
### `table.remove()` does not update the viewer
|
||||
|
||||
The [`remove()`](./explanation/table/update_and_remove.md) method requires an
|
||||
[indexed](./explanation/table/options.md) table. Ensure your table was created
|
||||
with an `index` option, and pass the index values to remove.
|
||||
|
||||
<!-- _Related: [#1597](https://github.com/perspective-dev/perspective/issues/1597),
|
||||
[#2293](https://github.com/perspective-dev/perspective/issues/2293)_ -->
|
||||
|
||||
## Viewer Configuration
|
||||
|
||||
### How do I save and restore the viewer state?
|
||||
|
||||
Use
|
||||
[`viewer.save()` and `viewer.restore()`](./how_to/javascript/save_restore.md) to
|
||||
serialize and deserialize the full viewer configuration.
|
||||
|
||||
<!-- _Related: [#1501](https://github.com/perspective-dev/perspective/issues/1501),
|
||||
[#1560](https://github.com/perspective-dev/perspective/issues/1560)_ -->
|
||||
|
||||
### Can I hide the configuration panel?
|
||||
|
||||
The settings panel can be toggled programmatically via
|
||||
`await viewer.restore({ settings: false })`.
|
||||
|
||||
<!-- _Related:
|
||||
[#2581](https://github.com/perspective-dev/perspective/discussions/2581),
|
||||
[#1085](https://github.com/perspective-dev/perspective/issues/1085)_ -->
|
||||
|
||||
### Can I collapse row groups by default?
|
||||
|
||||
Row group can be closed imperatively via
|
||||
[`view.set_depth()`](./explanation/view/advanced.md). Expansion state is not
|
||||
persisted or configurable via the `save`/`restore` API currently.
|
||||
|
||||
<!-- _Related:
|
||||
[#2695](https://github.com/perspective-dev/perspective/discussions/2695),
|
||||
[#2861](https://github.com/perspective-dev/perspective/issues/2861)_ -->
|
||||
|
||||
## Internationalization
|
||||
|
||||
### Can I change the UI language?
|
||||
|
||||
Perspective's UI text is defined via CSS variables, which can be customized per
|
||||
theme. See the
|
||||
[Icons and Translation](./how_to/javascript/theming.md#icons-and-translation)
|
||||
section of the theming guide for details.
|
||||
|
||||
<!-- _Related: [#1934](https://github.com/perspective-dev/perspective/issues/1934),
|
||||
[#2358](https://github.com/perspective-dev/perspective/issues/2358)_ -->
|
||||
|
||||
## Rust
|
||||
|
||||
### How do I build Perspective from Rust?
|
||||
|
||||
See the [Getting Started](./how_to/rust.md) guide for Rust. The Rust crate wraps
|
||||
the C++ engine and requires a C++ toolchain. You need `cmake` installed and on
|
||||
your path to build the engine.
|
||||
|
||||
<!-- _Related:
|
||||
[#3121](https://github.com/perspective-dev/perspective/discussions/3121),
|
||||
[#3080](https://github.com/perspective-dev/perspective/discussions/3080),
|
||||
[#2684](https://github.com/perspective-dev/perspective/discussions/2684)_ -->
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Can I use Perspective without `<perspective-viewer>`?
|
||||
|
||||
Yes. The `perspective` library (data engine) can be used independently for
|
||||
server-side data processing without any UI. Use
|
||||
[`table()` and `view()`](./how_to/javascript/worker.md) directly to query data.
|
||||
|
||||
<!-- _Related:
|
||||
[#2933](https://github.com/perspective-dev/perspective/discussions/2933),
|
||||
[#2644](https://github.com/perspective-dev/perspective/discussions/2644)_ -->
|
||||
|
||||
### Can I use Perspective in Pyodide?
|
||||
|
||||
There is an emscripten wheel
|
||||
[published via Releases](https://github.com/perspective-dev/perspective/releases),
|
||||
but it must be downloaded and hosted manually and is only built for specific
|
||||
pyodide versions.
|
||||
|
||||
<!-- _Related:
|
||||
[#2880](https://github.com/perspective-dev/perspective/discussions/2880)_ -->
|
||||
|
||||
### How do I handle row selection events?
|
||||
|
||||
Listen for
|
||||
[`perspective-click` and `perspective-select`](./how_to/javascript/events.md)
|
||||
events on the `<perspective-viewer>` element.
|
||||
|
||||
<!-- _Related:
|
||||
[#2589](https://github.com/perspective-dev/perspective/discussions/2589),
|
||||
[#1076](https://github.com/perspective-dev/perspective/issues/1076)_ -->
|
||||
@@ -0,0 +1,83 @@
|
||||
# Summary
|
||||
|
||||
[What is Perspective](./perspective.md)
|
||||
|
||||
# Concepts
|
||||
|
||||
- [Data Architecture](./explanation/architecture.md)
|
||||
- [Client-only](./explanation/architecture/client_only.md)
|
||||
- [Client/Server replicated](./explanation/architecture/client_server.md)
|
||||
- [Server only](./explanation/architecture/server_only.md)
|
||||
- [Virtual Servers](./explanation/virtual_servers.md)
|
||||
- [`Table`](./explanation/table.md)
|
||||
- [Schema and column types](./explanation/table/schema.md)
|
||||
- [Loading data](./explanation/table/loading_data.md)
|
||||
- [Construct an empty `Table` from a schema](./explanation/table/constructing_schema.md)
|
||||
- [`index` and `limit` options](./explanation/table/options.md)
|
||||
- [`update()` and `remove()` streaming methods](./explanation/table/update_and_remove.md)
|
||||
- [`clear()` and `replace()` start-over methods](./explanation/table/clear_and_replace.md)
|
||||
- [`View`](./explanation/view.md)
|
||||
- [Querying data](./explanation/view/querying.md)
|
||||
- [Grouping and Pivots](./explanation/view/config/grouping_and_pivots.md)
|
||||
- [Selection and Ordering](./explanation/view/config/selection_and_ordering.md)
|
||||
- [`expressions`](./explanation/view/config/expressions.md)
|
||||
- [Advanced View Operations](./explanation/view/advanced.md)
|
||||
- [`Join`](./explanation/join.md)
|
||||
- [Join Types](./explanation/join/join_types.md)
|
||||
- [Join Options](./explanation/join/options.md)
|
||||
- [Reactivity and Constraints](./explanation/join/reactivity.md)
|
||||
|
||||
# JavaScript
|
||||
|
||||
- [Installation and Module Structure](./how_to/javascript/installation.md)
|
||||
- [Importing with or without a bundler](./how_to/javascript/importing.md)
|
||||
- [`perspective` data engine library](./how_to/javascript/worker.md)
|
||||
- [Serializing data](./how_to/javascript/serializing.md)
|
||||
- [Cleaning up resources](./how_to/javascript/deleting.md)
|
||||
- [Hosting a `WebSocketServer` in Node.js](./how_to/javascript/nodejs_server.md)
|
||||
- [Customizing `perspective.worker()`](./how_to/javascript/custom_worker.md)
|
||||
- [Joining Tables](./how_to/javascript/join.md)
|
||||
- [`perspective-viewer` Custom Element library](./how_to/javascript/viewer.md)
|
||||
- [Loading data](./how_to/javascript/loading_data.md)
|
||||
- [Theming](./how_to/javascript/theming.md)
|
||||
- [Saving and restoring UI state](./how_to/javascript/save_restore.md)
|
||||
- [Listening for events](./how_to/javascript/events.md)
|
||||
- [Plugin render limits](./how_to/javascript/plugin_settings.md)
|
||||
- [Virtual Servers](./how_to/javascript/virtual_server.md)
|
||||
- [DuckDB](./how_to/javascript/virtual_server/duckdb.md)
|
||||
- [ClickHouse](./how_to/javascript/virtual_server/clickhouse.md)
|
||||
- [Custom](./how_to/javascript/virtual_server/custom.md)
|
||||
- [React Component](./how_to/javascript/react.md)
|
||||
|
||||
# Python
|
||||
|
||||
- [Overview](./explanation/python.md)
|
||||
- [Installation](./how_to/python/installation.md)
|
||||
- [Loading data into a `Table`](./how_to/python/table.md)
|
||||
- [`pandas`, `polars` and `pyarrow` integration](./how_to/python/table_data.md)
|
||||
- [Callbacks and events](./how_to/python/callbacks.md)
|
||||
- [Multithreading](./how_to/python/multithreading.md)
|
||||
- [Hosting a WebSocket server](./how_to/python/websocket.md)
|
||||
- [Joining Tables](./how_to/python/join.md)
|
||||
- [`PerspectiveWidget` for JupyterLab](./how_to/python/jupyterlab.md)
|
||||
- [Virtual Servers](./how_to/python/virtual_server.md)
|
||||
- [DuckDB](./how_to/python/virtual_server/duckdb.md)
|
||||
- [ClickHouse](./how_to/python/virtual_server/clickhouse.md)
|
||||
- [Polars](./how_to/python/virtual_server/polars.md)
|
||||
- [Custom](./how_to/python/virtual_server/custom.md)
|
||||
|
||||
# Rust
|
||||
|
||||
- [Getting Started](./how_to/rust.md)
|
||||
|
||||
# Tutorials
|
||||
|
||||
- [A `tornado` server in Python](./tutorials/python/tornado.md)
|
||||
|
||||
# API Reference
|
||||
|
||||
- [API Reference](./api_reference.md)
|
||||
|
||||
# FAQ
|
||||
|
||||
- [FAQ](./FAQ.md)
|
||||
@@ -0,0 +1,22 @@
|
||||
# API Reference
|
||||
|
||||
Perspective's complete API is hosted on `docs.rs`:
|
||||
|
||||
- Python API
|
||||
- [`perspective`](https://perspective-dev.github.io/python/index.html)
|
||||
- [`perspective.widget`](https://perspective-dev.github.io/python/perspective/widget.html)
|
||||
- [`perspective.handlers.aiohttp`](https://perspective-dev.github.io/python/perspective/handlers/aiohttp.htm)
|
||||
- [`perspective.handlers.starlette`](https://perspective-dev.github.io/python/perspective/handlers/starlett.htm)
|
||||
- [`perspective.handlers.tornado`](https://perspective-dev.github.io/python/perspective/handlers/tornado.htm)
|
||||
- JavaScript API
|
||||
- [`@perspective-dev/client` Browser](https://perspective-dev.github.io/browser/modules/src_ts_perspective.browser.ts.html)
|
||||
- [`@perspective-dev/client` Node.js](https://perspective-dev.github.io/node/modules/src_ts_perspective.node.ts.html)
|
||||
- [`@perspective-dev/viewer`](https://perspective-dev.github.io/viewer/modules/perspective-viewer.html)
|
||||
- [`@perspective-dev/react`](https://perspective-dev.github.io/react/index.html)
|
||||
- Rust API
|
||||
- [`perspective`](https://docs.rs/perspective/latest/perspective/)
|
||||
- [`perspective-client`](https://docs.rs/perspective-client/latest/perspective_client/)
|
||||
- [`perspective-server`](https://docs.rs/perspective-server/latest/perspective_server/)
|
||||
- [`perspective-python`](https://docs.rs/perspective-python/latest/perspective_python/)
|
||||
- [`perspective-js`](https://docs.rs/perspective-js/latest/perspective_js/)
|
||||
- [`perspective-viewer`](https://docs.rs/perspective-viewer/latest/perspective_viewer/)
|
||||
@@ -0,0 +1,4 @@
|
||||
# Overview
|
||||
|
||||
This section covers Perspective's core concepts: data architecture patterns,
|
||||
the `Table` and `View` data model, and language-specific module details.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Data Architecture
|
||||
|
||||
Application developers can choose from
|
||||
[Client (WebAssembly)](./architecture/client_only.md),
|
||||
[Server (Python/Node)](./architecture/server_only.md) or
|
||||
[Client/Server Replicated](./architecture/client_server.md) designs to bind
|
||||
data, and a web application can use one or a mix of these designs as needed. By
|
||||
serializing to Apache Arrow, tables are duplicated and synchronized across
|
||||
runtimes efficiently.
|
||||
|
||||
Perspective is a multi-language platform. The examples in this section use
|
||||
Python and JavaScript as an example, but the same general principles apply to
|
||||
any `Client`/`Server` combination.
|
||||
|
||||
<img src="./architecture/architecture.svg" />
|
||||
@@ -0,0 +1,197 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
subgraph cluster_thread_2 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 2";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_2 [
|
||||
label="table(df)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_2 [
|
||||
label="view({\l group_by: [\"State\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view_thread_2_2 [
|
||||
label="view({\l group_by: [\"City\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_2 -> view_thread_2_2;
|
||||
table_thread_2 -> view_thread_2;
|
||||
}
|
||||
subgraph cluster_thread_1 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 1";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_1 [
|
||||
label="table(arrow)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_1 [
|
||||
label="view({\l group_by:'[\"State\"]\l split_by:'[\"Segment\"]'\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_1 -> view_thread_1;
|
||||
}
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
subgraph cluster_2 {
|
||||
graph [
|
||||
label="\lWebWorker 1";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table1 [
|
||||
label="table(csv)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
table_remote_view [
|
||||
label="table(json)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view1 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view2 [
|
||||
label="view({\l group_by: [\"Sub-Category\"]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
view3 [
|
||||
label="view()\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table1 -> {view1 view2};
|
||||
table_remote_view -> view3;
|
||||
}
|
||||
|
||||
subgraph cluster_webworker2 {
|
||||
graph [
|
||||
label="\lWebWorker 2";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table12 [
|
||||
label="table(...)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
view12 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table12 -> {view12} [color="#E6E2DA"];
|
||||
}
|
||||
|
||||
view_thread_2 -> table12 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
|
||||
view1 -> viewer1 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
view2 -> viewer2 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
view3 -> viewer3 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
viewer1 [
|
||||
label = "<perspective-viewer\l view=\"Y Bar\"\l row-pivots='[\"Category\"]'\l filters='[[\"State\",\"==\",\"Texas\"]]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
viewer2 [
|
||||
label = "<perspective-viewer\l view=\"xy_scatter\"\l row-pivots='[\"Sub-Category\"]'>\l"
|
||||
width=2.8
|
||||
color=lightgrey
|
||||
];
|
||||
viewer3 [
|
||||
label = "<perspective-viewer\l view=\"grid\">\l"
|
||||
width=2.8
|
||||
];
|
||||
viewerN [
|
||||
style=invis
|
||||
]
|
||||
|
||||
viewer5 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
viewerZ [
|
||||
style=invis
|
||||
height=0.3
|
||||
]
|
||||
viewer4 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view_thread_1 -> viewer4 [penwidth=2 style=dashed arrowhead=none color="#D1A043" constraint=false];
|
||||
view12 -> viewer5 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
subgraph cluster_2 {
|
||||
graph [
|
||||
label="\lWebWorker 1";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table1 [
|
||||
label="table(csv)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
|
||||
view2 [
|
||||
label="view({\l group_by: [\"Sub-Category\"]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table1 -> {view2};
|
||||
}
|
||||
|
||||
view2 -> viewer2 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer2 [
|
||||
label = "<perspective-viewer\l view=\"xy_scatter\"\l row-pivots='[\"Sub-Category\"]'>\l"
|
||||
width=2.8
|
||||
color=lightgrey
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="590pt" height="170pt"
|
||||
viewBox="0.00 0.00 590.00 170.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 166)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="8,-8 8,-154 574,-154 574,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="41" y="-122.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="18,-18 18,-105 326,-105 326,-18 18,-18"/>
|
||||
<text text-anchor="middle" x="55.5" y="-81" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 1</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="342,-18 342,-100 564,-100 564,-18 342,-18"/>
|
||||
<text text-anchor="middle" x="364" y="-80.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-64 28,-64 28,-28 100,-28 100,-64"/>
|
||||
<text text-anchor="middle" x="64" y="-44.1" font-family="monospace" font-size="8.00">table(csv)</text>
|
||||
</g>
|
||||
<!-- view2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-64 136,-64 136,-28 316,-28 316,-64"/>
|
||||
<text text-anchor="start" x="144" y="-53.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-44.1" font-family="monospace" font-size="8.00">    group_by: ["Sub-Category"]</text>
|
||||
<text text-anchor="start" x="144" y="-35.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table1->view2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-46C108.52,-46 117.91,-46 127.66,-46"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-48.8 135.91,-46 127.91,-43.2 127.91,-48.8"/>
|
||||
</g>
|
||||
<!-- viewer2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>viewer2</title>
|
||||
<polygon fill="white" stroke="lightgrey" points="554,-64 352,-64 352,-28 554,-28 554,-64"/>
|
||||
<text text-anchor="start" x="360" y="-53.1" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="360" y="-44.1" font-family="monospace" font-size="8.00">    view="xy_scatter"</text>
|
||||
<text text-anchor="start" x="360" y="-35.1" font-family="monospace" font-size="8.00">    row-pivots='["Sub-Category"]'></text>
|
||||
</g>
|
||||
<!-- view2->viewer2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>view2->viewer2</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M316.25,-46C327.9,-46 339.91,-46 351.73,-46"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,66 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
|
||||
subgraph cluster_thread_1 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 1";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_1 [
|
||||
label="table(arrow)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_1 [
|
||||
label="view({\l group_by:'[\"State\"]\l split_by:'[\"Segment\"]'\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
table_thread_1 -> view_thread_1;
|
||||
}
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer4 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view_thread_1 -> viewer4 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="602pt" height="178pt"
|
||||
viewBox="0.00 0.00 602.00 178.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 174)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-8 8,-162 336,-162 336,-8 8,-8"/>
|
||||
<text text-anchor="middle" x="59" y="-130.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_1</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-18 18,-113 326,-113 326,-18 18,-18"/>
|
||||
<text text-anchor="middle" x="94.5" y="-89" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 1</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-157 586,-157 586,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-125.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="354,-18 354,-108 576,-108 576,-18 354,-18"/>
|
||||
<text text-anchor="middle" x="376" y="-88.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_1 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-68 28,-68 28,-32 100,-32 100,-68"/>
|
||||
<text text-anchor="middle" x="64" y="-48.1" font-family="monospace" font-size="8.00">table(arrow)</text>
|
||||
</g>
|
||||
<!-- view_thread_1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-72 136,-72 136,-28 316,-28 316,-72"/>
|
||||
<text text-anchor="start" x="144" y="-61.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-52.6" font-family="monospace" font-size="8.00">    group_by:'["State"]</text>
|
||||
<text text-anchor="start" x="144" y="-43.6" font-family="monospace" font-size="8.00">    split_by:'["Segment"]'</text>
|
||||
<text text-anchor="start" x="144" y="-34.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_1->view_thread_1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_1->view_thread_1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-50C108.52,-50 117.91,-50 127.66,-50"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-52.8 135.91,-50 127.91,-47.2 127.91,-52.8"/>
|
||||
</g>
|
||||
<!-- viewer4 -->
|
||||
<g id="node3" class="node">
|
||||
<title>viewer4</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="566,-72 364,-72 364,-28 566,-28 566,-72"/>
|
||||
<text text-anchor="start" x="372" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="372" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="372" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="372" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view_thread_1->viewer4 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>view_thread_1->viewer4</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.26,-50C331.75,-50 347.94,-50 363.69,-50"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,97 @@
|
||||
digraph G {
|
||||
bgcolor=transparent
|
||||
graph [rankdir="LR" fontname="helvetica" labeljust="l"]
|
||||
node [shape="box" fontname="monospace" fontsize=8 color=gray70 style=filled fillcolor=white];
|
||||
edge [color="#EDEBDF" arrowsize=0.8]
|
||||
subgraph cluster_11 {
|
||||
label="\lPython Server";
|
||||
fontcolor=gray30
|
||||
margin=10
|
||||
color=none
|
||||
subgraph cluster_thread_2 {
|
||||
graph [
|
||||
label="\lPerspectiveManager - Thread 2";
|
||||
style=filled
|
||||
fillcolor="#91A4A8"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
margin=10
|
||||
]
|
||||
table_thread_2 [
|
||||
label="table(df)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
view_thread_2 [
|
||||
label="view({\l group_by: [\"State\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
|
||||
table_thread_2 -> view_thread_2;
|
||||
}
|
||||
|
||||
}
|
||||
subgraph cluster_browser {
|
||||
graph [
|
||||
label="\lBrowser";
|
||||
color="#CADEE1";
|
||||
margin=10
|
||||
style=filled;
|
||||
fontcolor=gray30
|
||||
]
|
||||
|
||||
subgraph cluster_webworker2 {
|
||||
graph [
|
||||
label="\lWebWorker 2";
|
||||
style=filled
|
||||
margin=10
|
||||
fillcolor="#2D4C68"
|
||||
color=none
|
||||
fontcolor="#EDEBDF"
|
||||
fontsize=10
|
||||
]
|
||||
table12 [
|
||||
label="table(...)"
|
||||
width=1
|
||||
color=none
|
||||
fillcolor="#E6E2DA"
|
||||
]
|
||||
|
||||
view12 [
|
||||
label="view({\l group_by: [\"Category\"]\l filter: [[\"State\",\"==\",\"Texas\"]]\l})\l"
|
||||
width=2.5
|
||||
color=none
|
||||
fillcolor="#EDEBDF"
|
||||
]
|
||||
|
||||
table12 -> {view12} [color="#E6E2DA"];
|
||||
}
|
||||
|
||||
view_thread_2 -> table12 [penwidth=2 style=dashed arrowhead=none color="#D1A043"];
|
||||
|
||||
subgraph cluster_41 {
|
||||
graph [
|
||||
label="\l<html>";
|
||||
color=none
|
||||
fillcolor=white
|
||||
fontcolor=gray30
|
||||
fontsize=10
|
||||
fontname="monospace" fontsize=8 color=none
|
||||
|
||||
]
|
||||
|
||||
viewer5 [
|
||||
label = "<perspective-viewer\l view=\"heatmap\"\l row-pivots='[\"State\"]\l column-pivots='[\"Segment\"]'>\l"
|
||||
width=2.8
|
||||
];
|
||||
view12 -> viewer5 [penwidth=2 style=dashed arrowhead=none color="#666"];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="926pt" height="178pt"
|
||||
viewBox="0.00 0.00 926.00 178.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 174)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-12 8,-158 336,-158 336,-12 8,-12"/>
|
||||
<text text-anchor="middle" x="59" y="-126.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_2</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-22 18,-109 326,-109 326,-22 18,-22"/>
|
||||
<text text-anchor="middle" x="94.5" y="-85" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 2</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-162 910,-162 910,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-130.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_webworker2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-18 354,-113 662,-113 662,-18 354,-18"/>
|
||||
<text text-anchor="middle" x="391.5" y="-89" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 2</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="678,-18 678,-108 900,-108 900,-18 678,-18"/>
|
||||
<text text-anchor="middle" x="700" y="-88.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_2 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_2</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-68 28,-68 28,-32 100,-32 100,-68"/>
|
||||
<text text-anchor="middle" x="64" y="-48.1" font-family="monospace" font-size="8.00">table(df)</text>
|
||||
</g>
|
||||
<!-- view_thread_2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-68 136,-68 136,-32 316,-32 316,-68"/>
|
||||
<text text-anchor="start" x="144" y="-57.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-48.1" font-family="monospace" font-size="8.00">    group_by: ["State"]]</text>
|
||||
<text text-anchor="start" x="144" y="-39.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_2->view_thread_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-50C108.52,-50 117.91,-50 127.66,-50"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-52.8 135.91,-50 127.91,-47.2 127.91,-52.8"/>
|
||||
</g>
|
||||
<!-- table12 -->
|
||||
<g id="node3" class="node">
|
||||
<title>table12</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-68 364,-68 364,-32 436,-32 436,-68"/>
|
||||
<text text-anchor="middle" x="400" y="-48.1" font-family="monospace" font-size="8.00">table(...)</text>
|
||||
</g>
|
||||
<!-- view_thread_2->table12 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>view_thread_2->table12</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.02,-50C332.91,-50 349.64,-50 363.61,-50"/>
|
||||
</g>
|
||||
<!-- view12 -->
|
||||
<g id="node4" class="node">
|
||||
<title>view12</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-72 472,-72 472,-28 652,-28 652,-72"/>
|
||||
<text text-anchor="start" x="480" y="-61.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-52.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-43.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-34.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table12->view12 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>table12->view12</title>
|
||||
<path fill="none" stroke="#e6e2da" d="M436.11,-50C444.52,-50 453.91,-50 463.66,-50"/>
|
||||
<polygon fill="#e6e2da" stroke="#e6e2da" points="463.91,-52.8 471.91,-50 463.91,-47.2 463.91,-52.8"/>
|
||||
</g>
|
||||
<!-- viewer5 -->
|
||||
<g id="node5" class="node">
|
||||
<title>viewer5</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-72 688,-72 688,-28 890,-28 890,-72"/>
|
||||
<text text-anchor="start" x="696" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view12->viewer5 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>view12->viewer5</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-50C663.9,-50 675.91,-50 687.73,-50"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 5.0.0 (20220707.1540)
|
||||
-->
|
||||
<!-- Title: G Pages: 1 -->
|
||||
<svg width="926pt" height="499pt"
|
||||
viewBox="0.00 0.00 926.00 499.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 495)">
|
||||
<title>G</title>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_11</title>
|
||||
<polygon fill="transparent" stroke="transparent" points="8,-9 8,-314 336,-314 336,-9 8,-9"/>
|
||||
<text text-anchor="middle" x="59" y="-282.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Python Server</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_thread_2</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-124 18,-265 326,-265 326,-124 18,-124"/>
|
||||
<text text-anchor="middle" x="94.5" y="-241" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 2</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_thread_1</title>
|
||||
<polygon fill="#91a4a8" stroke="transparent" points="18,-19 18,-114 326,-114 326,-19 18,-19"/>
|
||||
<text text-anchor="middle" x="94.5" y="-90" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">PerspectiveManager - Thread 1</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_browser</title>
|
||||
<polygon fill="#cadee1" stroke="#cadee1" points="344,-8 344,-483 910,-483 910,-8 344,-8"/>
|
||||
<text text-anchor="middle" x="377" y="-451.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#4d4d4d">Browser</text>
|
||||
</g>
|
||||
<g id="clust5" class="cluster">
|
||||
<title>cluster_2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-231 354,-434 662,-434 662,-231 354,-231"/>
|
||||
<text text-anchor="middle" x="391.5" y="-410" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 1</text>
|
||||
</g>
|
||||
<g id="clust7" class="cluster">
|
||||
<title>cluster_webworker2</title>
|
||||
<polygon fill="#2d4c68" stroke="transparent" points="354,-120 354,-215 662,-215 662,-120 354,-120"/>
|
||||
<text text-anchor="middle" x="391.5" y="-191" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#edebdf">WebWorker 2</text>
|
||||
</g>
|
||||
<g id="clust9" class="cluster">
|
||||
<title>cluster_41</title>
|
||||
<polygon fill="white" stroke="transparent" points="678,-18 678,-434 900,-434 900,-18 678,-18"/>
|
||||
<text text-anchor="middle" x="700" y="-414.6" font-family="monospace" font-size="8.00" fill="#4d4d4d"><html></text>
|
||||
</g>
|
||||
<!-- table_thread_2 -->
|
||||
<g id="node1" class="node">
|
||||
<title>table_thread_2</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-197 28,-197 28,-161 100,-161 100,-197"/>
|
||||
<text text-anchor="middle" x="64" y="-177.1" font-family="monospace" font-size="8.00">table(df)</text>
|
||||
</g>
|
||||
<!-- view_thread_2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>view_thread_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-170 136,-170 136,-134 316,-134 316,-170"/>
|
||||
<text text-anchor="start" x="144" y="-159.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-150.1" font-family="monospace" font-size="8.00">    group_by: ["State"]]</text>
|
||||
<text text-anchor="start" x="144" y="-141.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>table_thread_2->view_thread_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-173.07C108.61,-171.64 118.1,-170.04 127.95,-168.38"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="128.49,-171.13 135.91,-167.03 127.56,-165.6 128.49,-171.13"/>
|
||||
</g>
|
||||
<!-- view_thread_2_2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>view_thread_2_2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-224 136,-224 136,-188 316,-188 316,-224"/>
|
||||
<text text-anchor="start" x="144" y="-213.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-204.1" font-family="monospace" font-size="8.00">    group_by: ["City"]]</text>
|
||||
<text text-anchor="start" x="144" y="-195.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_2->view_thread_2_2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>table_thread_2->view_thread_2_2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-184.93C108.61,-186.36 118.1,-187.96 127.95,-189.62"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.56,-192.4 135.91,-190.97 128.49,-186.87 127.56,-192.4"/>
|
||||
</g>
|
||||
<!-- table12 -->
|
||||
<g id="node11" class="node">
|
||||
<title>table12</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-170 364,-170 364,-134 436,-134 436,-170"/>
|
||||
<text text-anchor="middle" x="400" y="-150.1" font-family="monospace" font-size="8.00">table(...)</text>
|
||||
</g>
|
||||
<!-- view_thread_2->table12 -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>view_thread_2->table12</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.02,-152C332.91,-152 349.64,-152 363.61,-152"/>
|
||||
</g>
|
||||
<!-- table_thread_1 -->
|
||||
<g id="node4" class="node">
|
||||
<title>table_thread_1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="100,-69 28,-69 28,-33 100,-33 100,-69"/>
|
||||
<text text-anchor="middle" x="64" y="-49.1" font-family="monospace" font-size="8.00">table(arrow)</text>
|
||||
</g>
|
||||
<!-- view_thread_1 -->
|
||||
<g id="node5" class="node">
|
||||
<title>view_thread_1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="316,-73 136,-73 136,-29 316,-29 316,-73"/>
|
||||
<text text-anchor="start" x="144" y="-62.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="144" y="-53.6" font-family="monospace" font-size="8.00">    group_by:'["State"]</text>
|
||||
<text text-anchor="start" x="144" y="-44.6" font-family="monospace" font-size="8.00">    split_by:'["Segment"]'</text>
|
||||
<text text-anchor="start" x="144" y="-35.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table_thread_1->view_thread_1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>table_thread_1->view_thread_1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M100.11,-51C108.52,-51 117.91,-51 127.66,-51"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="127.91,-53.8 135.91,-51 127.91,-48.2 127.91,-53.8"/>
|
||||
</g>
|
||||
<!-- viewer4 -->
|
||||
<g id="node19" class="node">
|
||||
<title>viewer4</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-72 688,-72 688,-28 890,-28 890,-72"/>
|
||||
<text text-anchor="start" x="696" y="-61.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-52.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-43.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-34.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view_thread_1->viewer4 -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>view_thread_1->viewer4</title>
|
||||
<path fill="none" stroke="#d1a043" stroke-width="2" stroke-dasharray="5,2" d="M316.2,-50.84C417.45,-50.66 582.21,-50.37 687.8,-50.18"/>
|
||||
</g>
|
||||
<!-- table1 -->
|
||||
<g id="node6" class="node">
|
||||
<title>table1</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-364 364,-364 364,-328 436,-328 436,-364"/>
|
||||
<text text-anchor="middle" x="400" y="-344.1" font-family="monospace" font-size="8.00">table(csv)</text>
|
||||
</g>
|
||||
<!-- view1 -->
|
||||
<g id="node8" class="node">
|
||||
<title>view1</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-339 472,-339 472,-295 652,-295 652,-339"/>
|
||||
<text text-anchor="start" x="480" y="-328.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-319.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-310.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-301.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view1 -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>table1->view1</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-339.64C444.61,-338.1 454.1,-336.38 463.95,-334.59"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="464.54,-337.33 471.91,-333.15 463.54,-331.82 464.54,-337.33"/>
|
||||
</g>
|
||||
<!-- view2 -->
|
||||
<g id="node9" class="node">
|
||||
<title>view2</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-393 472,-393 472,-357 652,-357 652,-393"/>
|
||||
<text text-anchor="start" x="480" y="-382.1" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-373.1" font-family="monospace" font-size="8.00">    group_by: ["Sub-Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-364.1" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table1->view2 -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>table1->view2</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-352.36C444.61,-353.9 454.1,-355.62 463.95,-357.41"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="463.54,-360.18 471.91,-358.85 464.54,-354.67 463.54,-360.18"/>
|
||||
</g>
|
||||
<!-- table_remote_view -->
|
||||
<g id="node7" class="node">
|
||||
<title>table_remote_view</title>
|
||||
<polygon fill="#e6e2da" stroke="transparent" points="436,-277 364,-277 364,-241 436,-241 436,-277"/>
|
||||
<text text-anchor="middle" x="400" y="-257.1" font-family="monospace" font-size="8.00">table(json)</text>
|
||||
</g>
|
||||
<!-- view3 -->
|
||||
<g id="node10" class="node">
|
||||
<title>view3</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-277 472,-277 472,-241 652,-241 652,-277"/>
|
||||
<text text-anchor="start" x="480" y="-257.1" font-family="monospace" font-size="8.00">view()</text>
|
||||
</g>
|
||||
<!-- table_remote_view->view3 -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>table_remote_view->view3</title>
|
||||
<path fill="none" stroke="#edebdf" d="M436.11,-259C444.52,-259 453.91,-259 463.66,-259"/>
|
||||
<polygon fill="#edebdf" stroke="#edebdf" points="463.91,-261.8 471.91,-259 463.91,-256.2 463.91,-261.8"/>
|
||||
</g>
|
||||
<!-- viewer1 -->
|
||||
<g id="node13" class="node">
|
||||
<title>viewer1</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-344 688,-344 688,-300 890,-300 890,-344"/>
|
||||
<text text-anchor="start" x="696" y="-333.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-324.6" font-family="monospace" font-size="8.00">    view="Y Bar"</text>
|
||||
<text text-anchor="start" x="696" y="-315.6" font-family="monospace" font-size="8.00">    row-pivots='["Category"]'</text>
|
||||
<text text-anchor="start" x="696" y="-306.6" font-family="monospace" font-size="8.00">    filters='[["State","==","Texas"]]'></text>
|
||||
</g>
|
||||
<!-- view1->viewer1 -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>view1->viewer1</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-318.98C663.9,-319.24 675.91,-319.51 687.73,-319.77"/>
|
||||
</g>
|
||||
<!-- viewer2 -->
|
||||
<g id="node14" class="node">
|
||||
<title>viewer2</title>
|
||||
<polygon fill="white" stroke="lightgrey" points="890,-398 688,-398 688,-362 890,-362 890,-398"/>
|
||||
<text text-anchor="start" x="696" y="-387.1" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-378.1" font-family="monospace" font-size="8.00">    view="xy_scatter"</text>
|
||||
<text text-anchor="start" x="696" y="-369.1" font-family="monospace" font-size="8.00">    row-pivots='["Sub-Category"]'></text>
|
||||
</g>
|
||||
<!-- view2->viewer2 -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>view2->viewer2</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-376.98C663.9,-377.24 675.91,-377.51 687.73,-377.77"/>
|
||||
</g>
|
||||
<!-- viewer3 -->
|
||||
<g id="node15" class="node">
|
||||
<title>viewer3</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-282 688,-282 688,-246 890,-246 890,-282"/>
|
||||
<text text-anchor="start" x="696" y="-266.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-257.6" font-family="monospace" font-size="8.00">    view="grid"></text>
|
||||
</g>
|
||||
<!-- view3->viewer3 -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>view3->viewer3</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-260.98C663.9,-261.24 675.91,-261.51 687.73,-261.77"/>
|
||||
</g>
|
||||
<!-- view12 -->
|
||||
<g id="node12" class="node">
|
||||
<title>view12</title>
|
||||
<polygon fill="#edebdf" stroke="transparent" points="652,-174 472,-174 472,-130 652,-130 652,-174"/>
|
||||
<text text-anchor="start" x="480" y="-163.6" font-family="monospace" font-size="8.00">view({</text>
|
||||
<text text-anchor="start" x="480" y="-154.6" font-family="monospace" font-size="8.00">    group_by: ["Category"]</text>
|
||||
<text text-anchor="start" x="480" y="-145.6" font-family="monospace" font-size="8.00">    filter: [["State","==","Texas"]]</text>
|
||||
<text text-anchor="start" x="480" y="-136.6" font-family="monospace" font-size="8.00">})</text>
|
||||
</g>
|
||||
<!-- table12->view12 -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>table12->view12</title>
|
||||
<path fill="none" stroke="#e6e2da" d="M436.11,-152C444.52,-152 453.91,-152 463.66,-152"/>
|
||||
<polygon fill="#e6e2da" stroke="#e6e2da" points="463.91,-154.8 471.91,-152 463.91,-149.2 463.91,-154.8"/>
|
||||
</g>
|
||||
<!-- viewer5 -->
|
||||
<g id="node17" class="node">
|
||||
<title>viewer5</title>
|
||||
<polygon fill="white" stroke="#b3b3b3" points="890,-174 688,-174 688,-130 890,-130 890,-174"/>
|
||||
<text text-anchor="start" x="696" y="-163.6" font-family="monospace" font-size="8.00"><perspective-viewer</text>
|
||||
<text text-anchor="start" x="696" y="-154.6" font-family="monospace" font-size="8.00">    view="heatmap"</text>
|
||||
<text text-anchor="start" x="696" y="-145.6" font-family="monospace" font-size="8.00">    row-pivots='["State"]</text>
|
||||
<text text-anchor="start" x="696" y="-136.6" font-family="monospace" font-size="8.00">    column-pivots='["Segment"]'></text>
|
||||
</g>
|
||||
<!-- view12->viewer5 -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>view12->viewer5</title>
|
||||
<path fill="none" stroke="#000000" stroke-width="2" stroke-dasharray="5,2" d="M652.25,-152C663.9,-152 675.91,-152 687.73,-152"/>
|
||||
</g>
|
||||
<!-- viewerN -->
|
||||
<!-- viewerZ -->
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,39 @@
|
||||
# Client-only
|
||||
|
||||
<img src="./architecture.sub1.svg" />
|
||||
|
||||
_For static datasets, datasets provided by the user, and simple server-less and
|
||||
read-only web applications._
|
||||
|
||||
In this design, Perspective is run as a client Browser WebAssembly library, the
|
||||
dataset is downloaded entirely to the client and all calculations and UI
|
||||
interactions are performed locally. Interactive performance is very good, using
|
||||
WebAssembly engine for near-native runtime plus WebWorker isolation for parallel
|
||||
rendering within the browser. Operations like scrolling and creating new views
|
||||
are responsive. However, the entire dataset must be downloaded to the client.
|
||||
Perspective is not a typical browser component, and datset sizes of 1gb+ in
|
||||
Apache Arrow format will load fine with good interactive performance!
|
||||
|
||||
Horizontal scaling is a non-issue, since here is no concurrent state to scale,
|
||||
and only uses client-side computation via WebAssembly client. Client-only
|
||||
perspective can support as many concurrent users as can download the web
|
||||
application itself. Once the data is loaded, no server connection is needed and
|
||||
all operations occur in the client browser, imparting no additional runtime cost
|
||||
on the server beyond initial load. This also means updates and edits are local
|
||||
to the browser client and will be lost when the page is refreshed, unless
|
||||
otherwise persisted by your application.
|
||||
|
||||
As the client-only design starts with creating a client-side Perspective
|
||||
`Table`, data can be provided by any standard web service in any Perspective
|
||||
compatible format (JSON, CSV or Apache Arrow).
|
||||
|
||||
## Javascript client
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(csv);
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(table);
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
# Client/Server replicated
|
||||
|
||||
<img src="./architecture.sub2.svg" />
|
||||
|
||||
_For medium-sized, real-time, synchronized and/or editable data sets with many
|
||||
concurrent users._
|
||||
|
||||
The dataset is instantiated in-memory with a Python or Node.js Perspective
|
||||
server, and web applications create duplicates of these tables in a local
|
||||
WebAssembly client in the browser, synchonized efficiently to the server via
|
||||
Apache Arrow. This design scales well with additional concurrent users, as
|
||||
browsers only need to download the initial data set and subsequent update
|
||||
deltas, while operations like scrolling, pivots, sorting, etc. are performed on
|
||||
the client.
|
||||
|
||||
Python servers can make especially good use of additional threads, as
|
||||
Perspective will release the GIL for almost all operations. Interactive
|
||||
performance on the client is very good and identical to client-only
|
||||
architecture. Updates and edits are seamlessly synchonized across clients via
|
||||
their virtual server counterparts using websockets and Apache Arrow.
|
||||
|
||||
## Python and Tornado server
|
||||
|
||||
```python
|
||||
from perspective import Server, PerspectiveTornadoHandler
|
||||
|
||||
server = Server()
|
||||
client = server.new_local_client()
|
||||
client.table(csv, name="my_table")
|
||||
routes = [(
|
||||
r"/websocket",
|
||||
perspective.handlers.tornado.PerspectiveTornadoHandler,
|
||||
{"perspective_server": server},
|
||||
)]
|
||||
|
||||
app = tornado.web.Application(routes)
|
||||
app.listen(8080)
|
||||
loop = tornado.ioloop.IOLoop.current()
|
||||
loop.start()
|
||||
```
|
||||
|
||||
## Javascript client
|
||||
|
||||
Perspective's websocket client interfaces with the Python server, then
|
||||
_replicates_ the server-side Table.
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080");
|
||||
const server_table = await websocket.open_table("my_table");
|
||||
const server_view = await server_table.view();
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const client_table = await worker.table(server_view);
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(client_table);
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Server-only
|
||||
|
||||
<img src="./architecture.sub3.svg" />
|
||||
|
||||
_For extremely large datasets with a small number of concurrent users._
|
||||
|
||||
The dataset is instantiated in-memory with a Python or Node.js server, and web
|
||||
applications connect virtually. Has very good initial load performance, since no
|
||||
data is downloaded. Group-by and other operations will run column-parallel if
|
||||
configured.
|
||||
|
||||
But interactive performance is poor, as every user interaction must page the
|
||||
server to render. Operations like scrolling are not as responsive and can be
|
||||
impacted by network latency. Web applications must be "always connected" to the
|
||||
server via WebSocket. Disconnecting will prevent any interaction, scrolling,
|
||||
etc. of the UI. Does not use WebAssembly.
|
||||
|
||||
Each connected browser will impact server performance as long as the connection
|
||||
is open, which in turn impacts interactive performance of every client. This
|
||||
ultimately limits the horizontal scalabity of this architecture. Since each
|
||||
client reads the perspective `Table` virtually, changes like edits and updates
|
||||
are automatically reflected to all clients and persist across browser refresh.
|
||||
Using the same Python server as the previous design, we can simply skip the
|
||||
intermediate WebAssembly `Table` and pass the virtual table directly to `load()`
|
||||
|
||||
```javascript
|
||||
const websocket = await perspective.websocket("ws://localhost:8080");
|
||||
const server_table = await websocket.open_table("my_table");
|
||||
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
document.body.appendChild(viewer);
|
||||
await viewer.load(server_table);
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
# Join
|
||||
|
||||
`Client::join` creates a read-only `Table` by joining two source tables on a
|
||||
shared key column. The `left` and `right` arguments can be `Table` objects or
|
||||
string table names (as returned by `get_hosted_table_names()`). The resulting
|
||||
table is _reactive_: whenever either source table is updated, the join is
|
||||
automatically recomputed and any `View` derived from the joined table will
|
||||
update accordingly.
|
||||
|
||||
Joined tables support the full `View` API — you can apply `group_by`,
|
||||
`split_by`, `sort`, `filter`, `expressions`, and all other `View` operations on
|
||||
the result, just as you would with any other `Table`.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Join Types
|
||||
|
||||
`Client::join` supports three join types, specified via the `join_type` option.
|
||||
The default is `"inner"`.
|
||||
|
||||
## Inner Join (default)
|
||||
|
||||
An inner join includes only rows where the key column exists in _both_ source
|
||||
tables. Rows from either table that have no match in the other are excluded.
|
||||
|
||||
## Left Join
|
||||
|
||||
A left join includes all rows from the left table. For left rows that have no
|
||||
match in the right table, right-side columns are filled with `null`.
|
||||
|
||||
## Outer Join
|
||||
|
||||
An outer join includes all rows from both tables. Unmatched rows on either side
|
||||
have their missing columns filled with `null`.
|
||||
|
||||
| `join_type` | Left-only rows | Right-only rows |
|
||||
| ----------- | -------------- | --------------- |
|
||||
| `"inner"` | excluded | excluded |
|
||||
| `"left"` | included | excluded |
|
||||
| `"outer"` | included | included |
|
||||
@@ -0,0 +1,35 @@
|
||||
# Join Options
|
||||
|
||||
## `on` — Join Key Column
|
||||
|
||||
The `on` parameter specifies the column name used to match rows between the left
|
||||
and right tables. This column must exist in the left table and, by default, must
|
||||
also exist in the right table with the same name and compatible type.
|
||||
|
||||
The join key column becomes the index of the resulting table.
|
||||
|
||||
## `right_on` — Different Right Key Column
|
||||
|
||||
When the join key has a different name in the right table, use `right_on` to
|
||||
specify the right table's column name. The left table's column name (`on`) is
|
||||
used in the output schema; the right key column is excluded from the result.
|
||||
|
||||
The `on` and `right_on` columns must have compatible types. An error is thrown
|
||||
if the types do not match.
|
||||
|
||||
## `join_type` — Join Type
|
||||
|
||||
Controls which rows are included in the result. See
|
||||
[Join Types](./join_types.md) for details.
|
||||
|
||||
| Value | Behavior |
|
||||
| ----------- | ----------------------------------------------------- |
|
||||
| `"inner"` | Only rows with matching keys in both tables (default) |
|
||||
| `"left"` | All left rows; unmatched right columns are `null` |
|
||||
| `"outer"` | All rows from both tables; unmatched columns are `null` |
|
||||
|
||||
## `name` — Table Name
|
||||
|
||||
An optional name for the resulting joined table. If omitted, a random name is
|
||||
generated. This name is used to identify the table in the server's hosted table
|
||||
registry.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Reactivity and Constraints
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
Joined tables are fully reactive. When either source table receives an
|
||||
`update()`, the join is automatically recomputed and any `View` created from the
|
||||
joined table will reflect the new data. This includes:
|
||||
|
||||
- Updates that modify existing rows in either source table.
|
||||
- New rows added to either source table that create new matches.
|
||||
- Chained joins — if a joined table is itself used as input to another join,
|
||||
updates propagate through the entire chain.
|
||||
|
||||
## Duplicate Keys
|
||||
|
||||
Like SQL, `join()` produces a cross-product for each matching key value. When
|
||||
multiple rows in the left table share the same key, each is paired with every
|
||||
matching row in the right table (and vice versa). The number of output rows for
|
||||
a given key is `left_count × right_count`.
|
||||
|
||||
This behavior depends on whether the source tables are _indexed_:
|
||||
|
||||
- **Unindexed tables** (no `index` option) — rows are appended, so duplicate
|
||||
keys accumulate naturally. Each `update()` appends new rows, which may
|
||||
introduce additional duplicates.
|
||||
- **Indexed tables** (`index` set to the join key) — each key appears at most
|
||||
once per table, so the join produces at most one row per key. Updates replace
|
||||
existing rows in-place rather than appending.
|
||||
|
||||
## Read-Only
|
||||
|
||||
Joined tables are read-only. Calling `update()`, `remove()`, `clear()`, or
|
||||
`replace()` on a joined table will throw an error. Data can only change
|
||||
indirectly, by updating the source tables.
|
||||
|
||||
## Column Name Conflicts
|
||||
|
||||
The left and right tables must not have overlapping column names (other than the
|
||||
join key). If a non-key column name appears in both tables, `join()` throws an
|
||||
error. Rename columns in your source data or use `View` expressions to avoid
|
||||
conflicts.
|
||||
|
||||
## Source Table Deletion
|
||||
|
||||
A source table cannot be deleted while a joined table depends on it. You must
|
||||
delete the joined table first, then delete the source tables.
|
||||
@@ -0,0 +1,77 @@
|
||||
# What is `perspective-python`
|
||||
|
||||
Perspective for Python uses the exact same C++ data engine used by the
|
||||
[WebAssembly version](https://docs.rs/perspective-js/latest/perspective_js/) and
|
||||
[Rust version](https://docs.rs/crate/perspective/latest). The library consists
|
||||
of many of the same abstractions and API as in JavaScript, as well as
|
||||
Python-specific data loading support for [NumPy](https://numpy.org/),
|
||||
[Pandas](https://pandas.pydata.org/) (and
|
||||
[Apache Arrow](https://arrow.apache.org/), as in JavaScript).
|
||||
|
||||
Additionally, `perspective-python` provides a session manager suitable for
|
||||
integration into server systems such as
|
||||
[Tornado websockets](https://www.tornadoweb.org/en/stable/websocket.html),
|
||||
[AIOHTTP](https://docs.aiohttp.org/en/stable/web_quickstart.html#websockets), or
|
||||
[Starlette](https://www.starlette.io/websockets/)/[FastAPI](https://fastapi.tiangolo.com/advanced/websockets/),
|
||||
which allows fully _virtual_ Perspective tables to be interacted with by
|
||||
multiple `<perspective-viewer>` in a web browser. You can also interact with a
|
||||
Perspective table from python clients, and to that end client libraries are
|
||||
implemented for both Tornado and AIOHTTP.
|
||||
|
||||
## Example
|
||||
|
||||
A simple example which loads an [Apache Arrow](https://arrow.apache.org/) and
|
||||
computes a "Group By" operation, returning a new Arrow.
|
||||
|
||||
```python
|
||||
from perspective import Server
|
||||
|
||||
client = Server().new_local_client()
|
||||
table = client.table(arrow_bytes_data)
|
||||
view = table.view(group_by = ["CounterParty", "Security"])
|
||||
arrow = view.to_arrow()
|
||||
```
|
||||
|
||||
[More Examples](https://github.com/perspective-dev/perspective/tree/master/examples)
|
||||
are available on GitHub.
|
||||
|
||||
## What's included
|
||||
|
||||
The `perspective` module exports several tools:
|
||||
|
||||
- `Server` the constructor for a new instance of the Perspective data engine.
|
||||
- The `perspective.widget` module exports `PerspectiveWidget`, the JupyterLab
|
||||
widget for interactive visualization in a notebook cell.
|
||||
- The `perspective.handlers` modules exports web frameworks handlers that
|
||||
interface with a `perspective-client` in JavaScript.
|
||||
- `perspective.handlers.tornado.PerspectiveTornadoHandler` for
|
||||
[Tornado](https://www.tornadoweb.org/)
|
||||
- `perspective.handlers.starlette.PerspectiveStarletteHandler` for
|
||||
[Starlette](https://www.starlette.io/) and
|
||||
[FastAPI](https://fastapi.tiangolo.com)
|
||||
- `perspective.handlers.aiohttp.PerspectiveAIOHTTPHandler` for
|
||||
[AIOHTTP](https://docs.aiohttp.org),
|
||||
|
||||
### Virtual UI server
|
||||
|
||||
As `<perspective-viewer>` or any other Perspective `Client` will only consume
|
||||
the data necessary to render the current screen (or whatever else was requested
|
||||
via the API), this runtime mode allows large datasets without the need to copy
|
||||
them entirely to the Browser, at the expense of network latency on UI
|
||||
interaction/API calls.
|
||||
|
||||
### Jupyterlab
|
||||
|
||||
`PerspectiveWidget` is a JupyterLab widget that implements the same API as
|
||||
`<perspective-viewer>`, allows running such a viewer in
|
||||
[JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) in either server or
|
||||
client (via WebAssembly) mode. `PerspectiveWidget` is compatible with Jupyterlab
|
||||
3 and Jupyter Notebook 6 via a
|
||||
[prebuilt extension](https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#prebuilt-extensions).
|
||||
To use it, simply install `perspective-python` and the extensions should be
|
||||
available.
|
||||
|
||||
`perspective-python`'s JupyterLab extension also provides convenient builtin
|
||||
viewers for `csv`, `json`, or `arrow` files. Simply right-click on a file with
|
||||
this extension and choose the appropriate `Perpective` option from the context
|
||||
menu.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Table
|
||||
|
||||
`Table` is Perspective's columnar data frame, analogous to a Pandas `DataFrame`
|
||||
or Apache Arrow, supporting append & in-place updates, removal by index, and
|
||||
update notifications.
|
||||
|
||||
A `Table` contains columns, each of which have a unique name, are strongly and
|
||||
consistently typed, and contains rows of data conforming to the column's type.
|
||||
Each column in a `Table` must have the same number of rows, though not every row
|
||||
must contain data; null-values are used to indicate missing values in the
|
||||
dataset. The schema of a `Table` is _immutable after creation_, which means the
|
||||
column names and data types cannot be changed after the `Table` has been
|
||||
created. Columns cannot be added or deleted after creation either, but a `View`
|
||||
can be used to select an arbitrary set of columns from the `Table`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# `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`.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
table.clear();
|
||||
table.replace(json);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table.clear()
|
||||
table.replace(df)
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
# Construct a Table
|
||||
|
||||
Examples of constructing an empty `Table` from a schema.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
var schema = {
|
||||
x: "integer",
|
||||
y: "string",
|
||||
z: "boolean",
|
||||
};
|
||||
|
||||
const table2 = await worker.table(schema);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python:
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
|
||||
schema = {
|
||||
"x": "integer",
|
||||
"y": "string",
|
||||
"z": "boolean",
|
||||
}
|
||||
|
||||
table2 = perspective.table(schema)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
Rust:
|
||||
|
||||
```rust
|
||||
let data = TableData::Schema(vec![(" a".to_string(), ColumnType::FLOAT)]);
|
||||
let options = TableInitOptions::default();
|
||||
let table = client.table(data.into(), options).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
# Loading data
|
||||
|
||||
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, `perspective-python` additionally
|
||||
supports `pyarrow.Table`, `polars.DataFrame` 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.
|
||||
|
||||
`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.
|
||||
|
||||
## Apache Arrow
|
||||
|
||||
The most efficient way to load data into Perspective, encoded as
|
||||
[Apache Arrow IPC format](https://arrow.apache.org/docs/python/ipc.html). In
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const resp = await fetch(
|
||||
"https://cdn.jsdelivr.net/npm/superstore-arrow/superstore.lz4.arrow",
|
||||
);
|
||||
|
||||
const arrow = await resp.arrayBuffer();
|
||||
```
|
||||
|
||||
Apache Arrow input do not support type coercion, preferring Arrow's internal
|
||||
self-describing schema.
|
||||
|
||||
## CSV
|
||||
|
||||
Perspective relies on Apache Arrow's CSV parser, and as such uses mostly the
|
||||
same column-type inference logic as Arrow itself would use for parsing CSV.
|
||||
|
||||
## Row Oriented JSON
|
||||
|
||||
Row-oriented JSON is in the form of a list of objects. Each object in the list
|
||||
corresponds to a row in the table. For example:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "a": 86, "b": false, "c": "words" },
|
||||
{ "a": 0, "b": true, "c": "" },
|
||||
{ "a": 12345, "b": false, "c": "here" }
|
||||
]
|
||||
```
|
||||
|
||||
## Column Oriented JSON
|
||||
|
||||
Column-Oriented JSON comes in the form of an object of lists. Each key of the
|
||||
object is a column name, and each element of the list is the corresponding value
|
||||
in the row.
|
||||
|
||||
```json
|
||||
{
|
||||
"a": [86, 0, 12345],
|
||||
"b": [false, true, false],
|
||||
"c": ["words", "", "here"]
|
||||
}
|
||||
```
|
||||
|
||||
## NDJSON
|
||||
|
||||
[NDJSON](https://github.com/ndjson/ndjson-spec) (sometimes also referred to as
|
||||
JSONL) is a streaming-friendly format where each line is a valid JSON object,
|
||||
separated by newlines. It is commonly used in data streaming and messaging
|
||||
queues.
|
||||
|
||||
```json
|
||||
{ "a": 86, "b": false, "c": "words" }
|
||||
{ "a": 0, "b": true, "c": "" }
|
||||
{ "a": 12345, "b": false, "c": "here" }
|
||||
```
|
||||
@@ -0,0 +1,59 @@
|
||||
## Index and Limit
|
||||
|
||||
<div class="warning">`limit` cannot be used in conjunction with `index`.</div>
|
||||
|
||||
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:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const indexed_table = await perspective.table(data, { index: "a" });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python
|
||||
|
||||
```python
|
||||
indexed_table = perspective.Table(data, index="a");
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
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:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
JavaScript:
|
||||
|
||||
```javascript
|
||||
const limit_table = await perspective.table(data, { limit: 1000 });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Python:
|
||||
|
||||
```python
|
||||
limit_table = perspective.Table(data, limit=1000);
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
# Schema and column 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, one of
|
||||
|
||||
- `float`
|
||||
- `integer`
|
||||
- `boolean`
|
||||
- `date`
|
||||
- `datetime`
|
||||
- `string`
|
||||
|
||||
A `Table` schema is fixed at construction, either by explicitly passing a schema
|
||||
dictionary to the `Client::table` method, or by passing _data_ to this method
|
||||
from which the schema is _inferred_ (if CSV or JSON format) or inherited (if
|
||||
Arrow).
|
||||
|
||||
## Type inference
|
||||
|
||||
When passing CSV or JSON data to the `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, further `Table::update` calls will perform
|
||||
limited type _coercion_ based on the schema. While _coercion_ works similarly to
|
||||
_inference_, in that input data may be parsed based on the expected column type,
|
||||
`Table::update` will not _change_ the column's type further. For example, a
|
||||
number literal `1234` would be _inferred_ as an `"integer"`, but _in the context
|
||||
of an `Table::update` call on a known `"string"` column_, this will be parsed as
|
||||
the _string_ `"1234"`.
|
||||
|
||||
## `date` and `datetime` inference
|
||||
|
||||
Various string representations of `date` and `datetime` format columns can be
|
||||
_inferred_ as well _coerced_ from strings if they match one of Perspective's
|
||||
internal known datetime parsing formats, for example
|
||||
[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) (which is also the format
|
||||
Perspective will _output_ these types for CSV).
|
||||
@@ -0,0 +1,88 @@
|
||||
# `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
|
||||
`Client::table`, minus _schema_.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const schema = {
|
||||
a: "integer",
|
||||
b: "float",
|
||||
};
|
||||
|
||||
const table = await perspective.table(schema);
|
||||
table.update(new_data);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
schema = {"a": "integer", "b": "float"}
|
||||
|
||||
table = perspective.Table(schema)
|
||||
table.update(new_data)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
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:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
indexed_table.update({ id: [1, 4], name: ["x", "y"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
indexed_table.update({"id": [1, 4], "name": ["x", "y"]})
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
table.update([{ x: 3, y: null }]); // `z` missing
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table.update([{"x": 3, "y": None}]) # `z` missing
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Rows can also be removed from an indexed `Table`, by calling `Table::remove`
|
||||
with an array of index values:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
indexed_table.remove([1, 4]);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
indexed_table.remove([1, 4])
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
# View
|
||||
|
||||
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.
|
||||
|
||||
<div class="javascript">
|
||||
<div class="warning">
|
||||
The examples in this module are in JavaScript. See <a href="https://docs.rs/crate/perspective/latest"><code>perspective</code></a> docs for the Rust API.
|
||||
</div>
|
||||
</div>
|
||||
<div class="python">
|
||||
<div class="warning">
|
||||
The examples in this module are in Python. See <a href="https://docs.rs/crate/perspective/latest"><code>perspective</code></a> docs for the Rust API.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
# Examples
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const table = await perspective.table({
|
||||
id: [1, 2, 3, 4],
|
||||
name: ["a", "b", "c", "d"],
|
||||
});
|
||||
|
||||
const view = await table.view({ columns: ["name"] });
|
||||
const json = await view.to_json();
|
||||
await view.delete();
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table = perspective.Table({
|
||||
"id": [1, 2, 3, 4],
|
||||
"name": ["a", "b", "c", "d"]
|
||||
});
|
||||
|
||||
view = table.view(columns=["name"])
|
||||
arrow = view.to_arrow()
|
||||
view.delete()
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let opts = TableInitOptions::default();
|
||||
let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
let table = client.table(data, opts).await?;
|
||||
|
||||
let view = table.view(None).await?;
|
||||
let arrow = view.to_arrow().await?;
|
||||
view.delete().await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,231 @@
|
||||
# Advanced View Operations
|
||||
|
||||
Beyond the standard query configuration, `View` provides additional methods for
|
||||
interacting with hierarchical results and introspecting data.
|
||||
|
||||
## Tree Hierarchy Operations
|
||||
|
||||
When a `View` has `group_by` applied, the results form a tree hierarchy.
|
||||
Perspective provides methods to control which levels of the tree are expanded or
|
||||
collapsed:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["Region", "Country", "City"] });
|
||||
|
||||
// Collapse the tree at row index 5
|
||||
await view.collapse(5);
|
||||
|
||||
// Expand the tree at row index 5
|
||||
await view.expand(5);
|
||||
|
||||
// Set the expansion depth (0 = fully collapsed, 1 = first level, etc.)
|
||||
await view.set_depth(1);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
Using the sync API
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["Region", "Country", "City"])
|
||||
|
||||
view.collapse(5)
|
||||
view.expand(5)
|
||||
view.set_depth(1)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
group_by: Some(vec!["Region".into(), "Country".into(), "City".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
|
||||
view.collapse(5).await?;
|
||||
view.expand(5).await?;
|
||||
view.set_depth(1).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
<span class="warning">Perspective's built-in engine is lazy — aggregates for
|
||||
collapsed rows are not recalculated when the underlying `Table` is updated.
|
||||
Updates are only computed for rows that are currently visible (expanded). When a
|
||||
collapsed row is later expanded, its aggregates are calculated at that
|
||||
point.</span>
|
||||
|
||||
## Column Range Queries
|
||||
|
||||
`View::get_min_max` returns the minimum and maximum values for a given column,
|
||||
which is useful for setting up scales in custom visualizations:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const [min, max] = await view.get_min_max("Sales");
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
min_val, max_val = view.get_min_max("Sales")
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Expression Validation
|
||||
|
||||
Before creating a `View` with expressions, you can validate them against the
|
||||
table's schema using `Table::validate_expressions`. This returns information
|
||||
about which expressions are valid and their inferred types:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const result = await table.validate_expressions({
|
||||
expr1: '"Sales" + "Profit"',
|
||||
expr2: "invalid_column + 1",
|
||||
});
|
||||
// result.expression_schema contains valid expressions and their types
|
||||
// result.errors contains invalid expressions and error messages
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
result = table.validate_expressions(['"Sales" + "Profit"', 'invalid + 1'])
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## View Dimensions
|
||||
|
||||
`View::dimensions` returns the number of rows and columns in the current view,
|
||||
including information about group-by header rows:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const dims = await view.dimensions();
|
||||
// { num_view_rows, num_view_columns, num_table_rows, num_table_columns, ... }
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
dims = view.dimensions()
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## View Configuration Introspection
|
||||
|
||||
`View::get_config` returns the full configuration used to create the view:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const config = await view.get_config();
|
||||
// { group_by: [...], split_by: [...], sort: [...], filter: [...], ... }
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
config = view.get_config()
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Update Callbacks
|
||||
|
||||
Register a callback to be notified whenever the underlying `Table` is updated
|
||||
and the `View` has been recalculated:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
view.on_update(
|
||||
(updated) => {
|
||||
console.log("View updated", updated.port_id);
|
||||
},
|
||||
{ mode: "row" },
|
||||
);
|
||||
|
||||
// Later, remove the callback
|
||||
view.remove_update(callback);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
def on_update(port_id, delta):
|
||||
print("View updated", port_id)
|
||||
|
||||
view.on_update(on_update, mode="row")
|
||||
view.remove_update(on_update)
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
When `mode` is set to `"row"`, the callback receives a delta of only the rows
|
||||
that changed (as Apache Arrow), which is useful for efficiently synchronizing
|
||||
tables across clients.
|
||||
|
||||
## Flattening a View into a Table
|
||||
|
||||
In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
|
||||
which will return a new [`Table`] based on the [`Table::view`]'s dataset, and
|
||||
all future updates that affect the [`Table::view`] will be forwarded to the new
|
||||
[`Table`]. This is particularly useful for implementing a
|
||||
[Client/Server Replicated](server.md#clientserver-replicated) design, by
|
||||
serializing the `View` to an arrow and setting up an `on_update` callback.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const worker1 = perspective.worker();
|
||||
const table = await worker.table(data);
|
||||
const view = await table.view({ filter: [["State", "==", "Texas"]] });
|
||||
const table2 = await worker.table(view);
|
||||
table.update([{ State: "Texas", City: "Austin" }]);
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
table = perspective.Table(data);
|
||||
view = table.view(filter=[["State", "==", "Texas"]])
|
||||
table2 = perspective.Table(view.to_arrow());
|
||||
|
||||
def updater(port, delta):
|
||||
table2.update(delta)
|
||||
|
||||
view.on_update(updater, mode="Row")
|
||||
table.update([{"State": "Texas", "City": "Austin"}])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let opts = TableInitOptions::default();
|
||||
let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
|
||||
let table = client.table(data, opts).await?;
|
||||
let view = table.view(None).await?;
|
||||
let table2 = client.table(TableData::View(view)).await?;
|
||||
table.update(data).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,149 @@
|
||||
# Expressions
|
||||
|
||||
The `expressions` property specifies _new_ columns in Perspective that are
|
||||
created using existing column values or arbitrary scalar values defined within
|
||||
the expression. In `<perspective-viewer>`, expressions are added using the "New
|
||||
Column" button in the side panel.
|
||||
|
||||
Expressions are strings parsed by Perspective's expression engine (based on
|
||||
[ExprTK](https://github.com/ArashPartow/exprtk)). Column names are referenced by
|
||||
wrapping them in double quotes, e.g. `"Sales"`:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
expressions: {
|
||||
"Profit Ratio": '"Profit" / "Sales"',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(expressions={'Profit Ratio': '"Profit" / "Sales"'})
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
expressions: Some(Expressions([
|
||||
("Profit Ratio", "\"Profit\" / \"Sales\"".into())
|
||||
].into_iter().collect())),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Type Conversion and Coercion
|
||||
|
||||
Perspective expressions are strongly typed — each column and literal has a fixed
|
||||
type, and most operators require matching types on both sides. To work across
|
||||
types, use the conversion functions:
|
||||
|
||||
| Function | Description |
|
||||
| --------------- | ------------------------------------------------------------ |
|
||||
| `to_string(x)` | Convert any type to string |
|
||||
| `to_integer(x)` | Convert to integer (null if not parsable) |
|
||||
| `to_float(x)` | Convert to float (null if not parsable) |
|
||||
| `to_boolean(x)` | Convert to boolean (truthy/falsy) |
|
||||
| `integer(x)` | Alias for `to_integer(x)` |
|
||||
| `float(x)` | Alias for `to_float(x)` |
|
||||
| `datetime(x)` | Construct a datetime from a POSIX timestamp (ms since epoch) |
|
||||
| `date(y, m, d)` | Construct a date from year, month, day |
|
||||
|
||||
### How coercion works
|
||||
|
||||
Perspective does not implicitly coerce types. For example, you cannot directly
|
||||
add an `integer` to a `float` — you must cast one side explicitly. Similarly,
|
||||
`datetime` and `date` values are not numeric: to perform arithmetic on them, you
|
||||
must first convert to a numeric representation, do the math, then convert back.
|
||||
|
||||
Internally, `datetime` values are stored as milliseconds since the Unix epoch
|
||||
(1970-01-01T00:00:00Z). Converting a `datetime` to a `float` yields this
|
||||
millisecond timestamp, and `datetime()` accepts a millisecond timestamp to
|
||||
produce a `datetime`.
|
||||
|
||||
### Example: offsetting a datetime by 7 days
|
||||
|
||||
This expression takes a `"Shipped Date"` column, converts it to its
|
||||
millisecond-epoch representation, adds 7 days worth of milliseconds (7 ×
|
||||
24 × 60 × 60 × 1000 = 604800000), and converts the result back
|
||||
to a `datetime`:
|
||||
|
||||
```
|
||||
// Due Date
|
||||
datetime(float("Shipped Date") + 604800000)
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
Standard arithmetic and comparison operators are supported:
|
||||
|
||||
| Operator | Description |
|
||||
| -------------------------------- | ----------- |
|
||||
| `+`, `-`, `*`, `/` | Arithmetic |
|
||||
| `%` | Modulo |
|
||||
| `==`, `!=`, `<`, `>`, `<=`, `>=` | Comparison |
|
||||
| `and`, `or`, `not` | Logical |
|
||||
| `if ... else ...` | Conditional |
|
||||
|
||||
## Numeric Functions
|
||||
|
||||
ExprTK provides a rich set of built-in numeric functions including `abs`,
|
||||
`ceil`, `floor`, `round`, `exp`, `log`, `log10`, `sqrt`, `min`, `max`, `pow`,
|
||||
`clamp`, `iclamp`, `inrange`, and trigonometric functions (`sin`, `cos`, `tan`,
|
||||
`asin`, `acos`, `atan`).
|
||||
|
||||
## String Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------------- | ------------------------------------------------------- |
|
||||
| `concat(a, b, ...)` | Concatenate strings |
|
||||
| `upper(s)` | Convert to uppercase |
|
||||
| `lower(s)` | Convert to lowercase |
|
||||
| `length(s)` | String length |
|
||||
| `contains(s, substr)` | Whether `s` contains `substr` |
|
||||
| `order(col, 'B', 'C', 'A')` | Custom sort order for a string column |
|
||||
| `match(s, pattern)` | Regex partial match (returns boolean) |
|
||||
| `match_all(s, pattern)` | Regex full match (returns boolean) |
|
||||
| `search(s, pattern)` | First capturing group match |
|
||||
| `indexof(s, pattern)` | Start index of first regex match |
|
||||
| `substring(s, start, end)` | Substring from `start` (inclusive) to `end` (exclusive) |
|
||||
| `replace(s, repl, pattern)` | Replace first regex match |
|
||||
| `replace_all(s, repl, pattern)` | Replace all regex matches |
|
||||
|
||||
## Date/Datetime Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `today()` | Current date |
|
||||
| `now()` | Current datetime |
|
||||
| `date(year, month, day)` | Construct a date |
|
||||
| `datetime(timestamp_ms)` | Construct a datetime from a POSIX timestamp (ms since epoch) |
|
||||
| `hour_of_day(dt)` | Hour component (0-23) |
|
||||
| `day_of_week(dt)` | Day of the week as a string |
|
||||
| `month_of_year(dt)` | Month of the year as a string |
|
||||
| `bucket(dt, unit)` | Bucket datetime by unit: `'s'`, `'m'`, `'h'`, `'D'`, `'W'`, `'M'`, `'Y'` |
|
||||
|
||||
`bucket` also works on numeric columns: `bucket("Price", 10)` rounds values down
|
||||
to the nearest multiple of 10.
|
||||
|
||||
## Other Functions
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------- | ----------------------------------------------------- |
|
||||
| `is_null(x)` | Whether the value is null |
|
||||
| `is_not_null(x)` | Whether the value is not null |
|
||||
| `percent_of(a, b)` | `a` as a percentage of `b` |
|
||||
| `inrange(low, val, high)` | Whether `val` is between `low` and `high` (inclusive) |
|
||||
| `min(a, b, ...)` | Minimum of inputs |
|
||||
| `max(a, b, ...)` | Maximum of inputs |
|
||||
| `random()` | Random float between 0.0 and 1.0 |
|
||||
| `col(name)` | Look up a column by string name at runtime |
|
||||
| `vlookup(col, key)` | Look up a value in another column by row key |
|
||||
@@ -0,0 +1,147 @@
|
||||
# Grouping and Pivots
|
||||
|
||||
## 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
|
||||
dataset is aggregated to show the values belonging to each group, and a total
|
||||
row is calculated for each group, showing the currently selected aggregated
|
||||
value (e.g. `sum`) of the column. Group by are useful for hierarchies,
|
||||
categorizing data and attributing values, i.e. showing the number of units sold
|
||||
based on State and City. In Perspective, group by are represented as an array of
|
||||
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.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["a", "c"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["a", "c"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
group_by: Some(vec!["a".into(), "c".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## 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
|
||||
for each unique value of the split by. Each newly created column contains the
|
||||
parts of the dataset that correspond to the column header, i.e. a `View` that
|
||||
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:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ split_by: ["a", "c"] });
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(split_by=["a", "c"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
split_by: Some(vec!["a".into(), "c".into()]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## 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
|
||||
specified by the user, or Perspective will use the following sensible default
|
||||
aggregates based on column type:
|
||||
|
||||
- "sum" for `integer` and `float` columns
|
||||
- "count" for all other columns
|
||||
|
||||
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.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
aggregates: {
|
||||
a: "avg",
|
||||
b: "distinct count",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(
|
||||
aggregates={
|
||||
"a": "avg",
|
||||
"b": "distinct count"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
aggregates: Some(HashMap::from([
|
||||
("a".into(), "avg".into()),
|
||||
("b".into(), "distinct count".into()),
|
||||
])),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available aggregate functions depend on the column type:
|
||||
|
||||
**Numeric columns** (`integer`, `float`): `sum`, `abs sum`, `sum abs`,
|
||||
`sum not null`, `any`, `avg`, `mean`, `count`, `distinct count`, `dominant`,
|
||||
`first`, `last`, `last by index`, `high`, `low`, `max`, `min`,
|
||||
`high minus low`, `last minus first`, `median`, `q1`, `q3`,
|
||||
`pct sum parent`, `pct sum total`, `stddev`, `var`, `unique`,
|
||||
`weighted mean`, `min by`, `max by`.
|
||||
|
||||
**String columns**: `count`, `any`, `distinct count`, `dominant`, `first`,
|
||||
`last`, `last by index`, `join`, `median`, `q1`, `q3`, `unique`, `min by`,
|
||||
`max by`.
|
||||
|
||||
**Date/Datetime columns**: `count`, `any`, `avg`, `distinct count`, `dominant`,
|
||||
`first`, `last`, `last by index`, `high`, `low`, `max`, `min`, `median`,
|
||||
`q1`, `q3`, `unique`.
|
||||
|
||||
**Boolean columns**: `count`, `any`, `distinct count`, `dominant`, `first`,
|
||||
`last`, `last by index`, `unique`.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Selection and Ordering
|
||||
|
||||
## 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:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
columns: ["a"],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(columns=["a"])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
columns: Some(vec![Some("a".into())]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## 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
|
||||
type, and sorts can be applied in ascending or descending order. Perspective
|
||||
represents `sort` as an array of arrays, with the values of each inner array
|
||||
being a string column name and a string sort direction. When `split_by` are
|
||||
applied, the additional sort directions `"col asc"` and `"col desc"` will
|
||||
determine the order of pivot column groups.
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
sort: [["a", "asc"]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(sort=[["a", "asc"]])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
sort: Some(vec![Sort("a".into(), SortDir::Asc)]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available sort directions are:
|
||||
|
||||
| Direction | Description |
|
||||
|---|---|
|
||||
| `"asc"` | Ascending order |
|
||||
| `"desc"` | Descending order |
|
||||
| `"asc abs"` | Ascending by absolute value |
|
||||
| `"desc abs"` | Descending by absolute value |
|
||||
| `"col asc"` | Ascending order for pivot column groups (requires `split_by`) |
|
||||
| `"col desc"` | Descending order for pivot column groups (requires `split_by`) |
|
||||
| `"col asc abs"` | Ascending by absolute value for pivot column groups |
|
||||
| `"col desc abs"` | Descending by absolute value for pivot column groups |
|
||||
|
||||
## 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
|
||||
the `WHERE` clause in SQL. There is no limit on the number of columns where
|
||||
`filter` is applied, but the resulting dataset is one that passes all the filter
|
||||
conditions, i.e. the filters are joined with an `AND` condition. The join
|
||||
condition can be changed to `OR` via the `filter_op` property.
|
||||
|
||||
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:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
filter: [["a", "<", 100]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(filter=[["a", "<", 100]])
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
let view = table.view(Some(ViewConfigUpdate {
|
||||
filter: Some(vec![Filter::new("a", "<", FilterTerm::Scalar(Scalar::Float(100.0)))]),
|
||||
..ViewConfigUpdate::default()
|
||||
})).await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
The available filter operators depend on the column type:
|
||||
|
||||
**String columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `begins with`,
|
||||
`contains`, `ends with`, `in`, `not in`, `is not null`, `is null`.
|
||||
|
||||
**Numeric columns** (`integer`, `float`): `==`, `!=`, `>`, `>=`, `<`, `<=`,
|
||||
`is not null`, `is null`.
|
||||
|
||||
**Boolean columns**: `==`, `is not null`, `is null`.
|
||||
|
||||
**Date/Datetime columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `is not null`,
|
||||
`is null`.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Querying data
|
||||
|
||||
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
|
||||
with it as you need - Perspective conserves memory by relying on a single
|
||||
[`Table`] to power multiple [`View`]s concurrently:
|
||||
|
||||
<div class="javascript">
|
||||
|
||||
```javascript
|
||||
const view = await table.view({
|
||||
columns: ["Sales"],
|
||||
aggregates: { Sales: "sum" },
|
||||
group_by: ["Region", "Country"],
|
||||
filter: [["Category", "in", ["Furniture", "Technology"]]],
|
||||
});
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="python">
|
||||
|
||||
```python
|
||||
view = table.view(
|
||||
columns=["Sales"],
|
||||
aggregates={"Sales": "sum"},
|
||||
group_by=["Region", "Country"],
|
||||
filter=[["Category", "in", ["Furniture", "Technology"]]]
|
||||
)
|
||||
```
|
||||
|
||||
</div>
|
||||
<div class="rust">
|
||||
|
||||
```rust
|
||||
use crate::config::*;
|
||||
let view = table
|
||||
.view(Some(ViewConfigUpdate {
|
||||
columns: Some(vec![Some("Sales".into())]),
|
||||
aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
|
||||
group_by: Some(vec!["Region".into(), "Country".into()]),
|
||||
filter: Some(vec![Filter::new("Category", "in", &[
|
||||
"Furniture",
|
||||
"Technology",
|
||||
])]),
|
||||
..ViewConfigUpdate::default()
|
||||
}))
|
||||
.await?;
|
||||
```
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,83 @@
|
||||
# Virtual Servers
|
||||
|
||||
A Virtual Server allows Perspective to query external data sources (such as
|
||||
DuckDB or ClickHouse) without loading the entire dataset into Perspective's
|
||||
built-in data engine. Instead, Perspective translates its query operations
|
||||
(group by, sort, filter, etc.) into queries the external data source can execute
|
||||
natively, and only transfers the data needed for the current view.
|
||||
|
||||
The Virtual Server API works on any platform that has a Perspective Client —
|
||||
including JavaScript (both Node.js and the browser via WebAssembly), Python, and
|
||||
Rust. In the browser, this means a virtual server can front a WASM-based engine
|
||||
like `@duckdb/duckdb-wasm`, giving `<perspective-viewer>` the ability to query a
|
||||
database running entirely client-side without loading data into Perspective's
|
||||
own engine.
|
||||
|
||||
This is useful when:
|
||||
|
||||
- The dataset is too large to fit in browser memory or a single process.
|
||||
- Data already lives in a database and you want to avoid duplicating it.
|
||||
- You want to leverage a database's native query optimizations.
|
||||
- A WASM build of the data source is available in the browser (e.g.
|
||||
`@duckdb/duckdb-wasm`) and you want to query it directly.
|
||||
|
||||
## How it works
|
||||
|
||||
A virtual server implements a handler interface that Perspective calls to
|
||||
satisfy `Table` and `View` operations. The handler translates Perspective's view
|
||||
configuration into the external system's query language (typically SQL),
|
||||
executes the query, and returns the results as columnar data. Because the
|
||||
handler speaks the standard Perspective Client protocol, it can run anywhere a
|
||||
Client can — in-process, in a WebWorker, or on a remote server.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ <perspective-viewer> │
|
||||
└──┬───────────────────────────────────────────────┘
|
||||
│ ┌──────────────────────────────────────────────────┐
|
||||
└──►│ Perspective Virtual Server Handler │
|
||||
└──┬───────────────────────────────────────────────┘
|
||||
│ ┌──────────────────────────────────────────────────┐
|
||||
└──►│ External DB (DuckDB, ClickHouse, …). │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The viewer communicates with the virtual server handler the same way it would
|
||||
with a regular Perspective server. The handler advertises its capabilities
|
||||
(which operations it supports) via a _features_ object, and the viewer UI adapts
|
||||
accordingly — disabling controls for unsupported operations.
|
||||
|
||||
## Built-in implementations
|
||||
|
||||
Perspective ships with virtual server implementations for:
|
||||
|
||||
- **DuckDB** — query DuckDB databases in-browser via WASM
|
||||
([JavaScript](../how_to/javascript/virtual_server/duckdb.md)) or server-side
|
||||
([Python](../how_to/python/virtual_server/duckdb.md)).
|
||||
- **ClickHouse** — query a ClickHouse server from the browser
|
||||
([JavaScript](../how_to/javascript/virtual_server/clickhouse.md)) or from
|
||||
Python ([Python](../how_to/python/virtual_server/clickhouse.md)).
|
||||
|
||||
## Custom implementations
|
||||
|
||||
You can implement your own virtual server to connect Perspective to any data
|
||||
source. See the language-specific guides:
|
||||
|
||||
- [JavaScript: Implementing a custom Virtual Server](../how_to/javascript/virtual_server/custom.md)
|
||||
- [Python: Implementing a custom Virtual Server](../how_to/python/virtual_server/custom.md)
|
||||
|
||||
## Features declaration
|
||||
|
||||
The `get_features()` / `getFeatures()` method returns an object that tells
|
||||
Perspective which query operations the virtual server supports. The viewer will
|
||||
only show controls for supported operations:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | ------ | ----------------------------------------------------------- |
|
||||
| `group_by` | `bool` | Whether group-by aggregation is supported |
|
||||
| `split_by` | `bool` | Whether split-by (pivot) is supported |
|
||||
| `sort` | `bool` | Whether sorting is supported |
|
||||
| `expressions` | `bool` | Whether computed expressions are supported |
|
||||
| `filter_ops` | `dict` | Map of column type to list of supported filter operators |
|
||||
| `aggregates` | `dict` | Map of column type to list of supported aggregate functions |
|
||||
| `on_update` | `bool` | Whether update callbacks are supported |
|
||||
@@ -0,0 +1,5 @@
|
||||
# Getting Started
|
||||
|
||||
Guides for installing and using Perspective in JavaScript (Browser & Node.js),
|
||||
Python and Rust. Each section includes installation steps, basic usage examples,
|
||||
and language-specific integration details.
|
||||
@@ -0,0 +1,3 @@
|
||||
# How To
|
||||
|
||||
Step-by-step guides for common Perspective tasks, organized by language.
|
||||
@@ -0,0 +1,4 @@
|
||||
# JavaScript
|
||||
|
||||
Guides for using Perspective in the browser and Node.js, including the
|
||||
`perspective` data engine and the `<perspective-viewer>` UI component.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Customizing `perspective.worker()`
|
||||
|
||||
`perspective.worker()` creates a `Client` that connects to a Perspective data
|
||||
engine. By default it spins up a dedicated `Worker` running the built-in
|
||||
WebAssembly engine, but you can pass an argument to change this behavior:
|
||||
|
||||
- A **`Worker`**, **`SharedWorker`**, or **`ServiceWorker`** — runs the
|
||||
built-in engine in a different worker context.
|
||||
- A **`MessagePort`** from `createMessageHandler()` — connects to a
|
||||
[Virtual Server](virtual_server/custom.md) instead of the built-in engine.
|
||||
|
||||
## Built-in engine with a custom Worker
|
||||
|
||||
Pass a `Worker`, `SharedWorker`, or `ServiceWorker` that loads the worker script
|
||||
distributed at
|
||||
`"@perspective-dev/client/dist/cdn/perspective-server.worker.js"`.
|
||||
|
||||
<span class="warning">`SharedWorker` and `ServiceWorker` have more complicated
|
||||
behavior compared to a dedicated `Worker`, and will need special consideration
|
||||
to integrate (or debug).</span>
|
||||
|
||||
### Dedicated `Worker`
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker(new Worker(url));
|
||||
```
|
||||
|
||||
### `SharedWorker`
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker(new SharedWorker(url));
|
||||
```
|
||||
|
||||
### `ServiceWorker`
|
||||
|
||||
```javascript
|
||||
const registration = await navigator.serviceWorker.register(url, {
|
||||
scope: "", // Your scope here
|
||||
});
|
||||
|
||||
const worker = await perspective.worker(registration.active);
|
||||
```
|
||||
|
||||
## Virtual Server
|
||||
|
||||
Instead of the built-in WebAssembly engine, `perspective.worker()` can connect
|
||||
to a Virtual Server — an adapter that translates Perspective queries into
|
||||
operations on an external data source such as
|
||||
[DuckDB](virtual_server/duckdb.md) or
|
||||
[ClickHouse](virtual_server/clickhouse.md).
|
||||
|
||||
Use `perspective.createMessageHandler()` with a `VirtualServerHandler` to create
|
||||
a `MessagePort`, then pass it to `worker()`:
|
||||
|
||||
```javascript
|
||||
import perspective from "@perspective-dev/client";
|
||||
|
||||
const handler = {
|
||||
/* VirtualServerHandler implementation */
|
||||
};
|
||||
|
||||
const server = perspective.createMessageHandler(handler);
|
||||
const client = await perspective.worker(server);
|
||||
const table = await client.open_table("my_table");
|
||||
```
|
||||
|
||||
The returned `Client` works identically to one backed by the built-in engine —
|
||||
you can pass it to `<perspective-viewer>.load()`, call `open_table()`, etc. The
|
||||
difference is that queries are fulfilled by your handler rather than the WASM
|
||||
engine.
|
||||
|
||||
For the full `VirtualServerHandler` interface and a worked example, see
|
||||
[Implementing a custom Virtual Server](virtual_server/custom.md).
|
||||
@@ -0,0 +1,25 @@
|
||||
# Deleting a `table()` or `view()`
|
||||
|
||||
Unlike standard JavaScript objects, Perspective objects such as `table()` and
|
||||
`view()` store their associated data in the WebAssembly heap. Because of this,
|
||||
as well as the current lack of a hook into the JavaScript runtime's garbage
|
||||
collector from WebAssembly, the memory allocated to these Perspective objects
|
||||
does not automatically get cleaned up when the object falls out of scope.
|
||||
|
||||
In order to prevent memory leaks and reclaim the memory associated with a
|
||||
Perspective `table()` or `view()`, you must call the `delete()` method:
|
||||
|
||||
```javascript
|
||||
await view.delete();
|
||||
|
||||
// This method will throw an exception if there are still `view()`s depending
|
||||
// on this `table()`!
|
||||
await table.delete();
|
||||
```
|
||||
|
||||
Similarly, `<perspective-viewer>` Custom Elements do not delete the memory
|
||||
allocated for the UI when they are removed from the DOM.
|
||||
|
||||
```javascript
|
||||
await viewer.delete();
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# Listening for events
|
||||
|
||||
The `<perspective-viewer>` Custom Element fires all the same HTML `Event`s that
|
||||
standard DOM `HTMLElement` objects fire, in addition to a few custom
|
||||
`CustomEvent`s which relate to UI updates including those initiaed through user
|
||||
interaction.
|
||||
|
||||
## Update events
|
||||
|
||||
Whenever a `<perspective-viewer>`s underlying `table()` is changed via the
|
||||
`load()` or `update()` methods, a `perspective-view-update` DOM event is fired.
|
||||
Similarly, `view()` updates instigated either through the Attribute API or
|
||||
through user interaction will fire a `perspective-config-update` event:
|
||||
|
||||
```javascript
|
||||
elem.addEventListener("perspective-config-update", function (event) {
|
||||
var config = elem.save();
|
||||
console.log("The view() config has changed to " + JSON.stringify(config));
|
||||
});
|
||||
```
|
||||
|
||||
## Click events
|
||||
|
||||
Whenever a `<perspective-viewer>`'s grid or chart is clicked, a
|
||||
`perspective-click` DOM event is fired containing a detail object with `config`,
|
||||
`column_names`, and `row`.
|
||||
|
||||
The `config` object contains an array of `filters` that can be applied to a
|
||||
`<perspective-viewer>` through the use of `restore()` updating it to show the
|
||||
filtered subset of data.
|
||||
|
||||
The `column_names` property contains an array of matching columns, and the `row`
|
||||
property returns the associated row data.
|
||||
|
||||
```javascript
|
||||
elem.addEventListener("perspective-click", function (event) {
|
||||
var config = event.detail.config;
|
||||
elem.restore(config);
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,172 @@
|
||||
# JavaScript - Importing with or without a bundler
|
||||
|
||||
Perspective requires the browser to have access to Perspective's `.wasm`
|
||||
binaries _in addition_ to the bundled `.js` files, and as a result the build
|
||||
process requires a few extra steps. Perspective's NPM releases come with
|
||||
multiple prebuilt configurations.
|
||||
|
||||
## ESM builds with a bundler
|
||||
|
||||
The recommended builds for production use are packaged as ES Modules and require
|
||||
a _bootstrapping_ step in order to acquire the `.wasm` binaries and initialize
|
||||
Perspective's JavaScript with them. Because they have no hard-coded dependencies
|
||||
on the `.wasm` paths, they are ideal for use with JavaScript bundlers such as
|
||||
ESBuild, Rollup, Vite or Webpack.
|
||||
|
||||
ESM builds must be _bootstrapped_ with their `.wasm` binaries to initialize. The
|
||||
`wasm` binaries can be found in their respective `dist/wasm` directories.
|
||||
|
||||
```javascript
|
||||
import perspective_viewer from "@perspective-dev/viewer";
|
||||
import perspective from "@perspective-dev/client";
|
||||
|
||||
// TODO These paths must be provided by the bundler!
|
||||
const SERVER_WASM = ... // "@perspective-dev/server/dist/wasm/perspective-server.wasm"
|
||||
const CLIENT_WASM = ... // "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(SERVER_WASM),
|
||||
perspective_viewer.init_client(CLIENT_WASM),
|
||||
]);
|
||||
|
||||
// Now Perspective API will work!
|
||||
const worker = await perspective.worker();
|
||||
const viewer = document.createElement("perspective-viewer");
|
||||
```
|
||||
|
||||
The exact syntax will vary slightly depending on the bundler.
|
||||
|
||||
### Vite
|
||||
|
||||
```javascript
|
||||
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm?url";
|
||||
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm?url";
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(fetch(SERVER_WASM)),
|
||||
perspective_viewer.init_client(fetch(CLIENT_WASM)),
|
||||
]);
|
||||
```
|
||||
|
||||
You'll also need to target `esnext` in your `vite.config.js` in order to run the
|
||||
`build` step:
|
||||
|
||||
```javascript
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: "esnext",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### ESBuild
|
||||
|
||||
```javascript
|
||||
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
|
||||
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(fetch(SERVER_WASM)),
|
||||
perspective_viewer.init_client(fetch(CLIENT_WASM)),
|
||||
]);
|
||||
```
|
||||
|
||||
ESBuild config JSON to encode this asset as a `file`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
// ...
|
||||
"loader": {
|
||||
// ...
|
||||
".wasm": "file"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webpack
|
||||
|
||||
```javascript
|
||||
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
|
||||
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
|
||||
|
||||
await Promise.all([
|
||||
perspective.init_server(SERVER_WASM),
|
||||
perspective_viewer.init_client(CLIENT_WASM),
|
||||
]);
|
||||
```
|
||||
|
||||
Webpack config:
|
||||
|
||||
```javascript
|
||||
{
|
||||
// ...
|
||||
module: {
|
||||
// ...
|
||||
rules: [
|
||||
// ...
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
type: "asset/resource"
|
||||
},
|
||||
]
|
||||
},
|
||||
experiments: {
|
||||
// ...
|
||||
asyncWebAssembly: false,
|
||||
syncWebAssembly: false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Inline builds with a bundler
|
||||
|
||||
<span class="warning">Inline builds are deprecated and will be removed in a
|
||||
future release.</span>
|
||||
|
||||
Perspective's _Inline_ Builds work by _inlining_ WebAssembly binary content as
|
||||
a base64-encoded string. While inline builds work with most bundlers and _do
|
||||
not_ require bootstrapping, there is an inherent file-size and boot-performance
|
||||
penalty. Prefer your bundler's inlining features and Perspective ESM builds
|
||||
where possible.
|
||||
|
||||
```javascript
|
||||
import "@perspective-dev/viewer/dist/esm/perspective-viewer.inline.js";
|
||||
import psp from "@perspective-dev/client/dist/esm/perspective.inline.js";
|
||||
```
|
||||
|
||||
## CDN builds
|
||||
|
||||
Perspective's CDN builds are good for non-bundled scenarios, such as importing
|
||||
directly from a `<script>` tag. CDN builds _do not_ require _bootstrapping_ the
|
||||
WebAssembly binaries, but they also generally _do not_ work with bundlers.
|
||||
|
||||
CDN builds are in ES Module format, thus to include them via a CDN they must be
|
||||
imported from a `<script type="module">`:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
|
||||
import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
|
||||
import perspective from "https://cdn.jsdelivr.net/npm/@perspective-dev/client/dist/cdn/perspective.js";
|
||||
|
||||
// .. Do stuff here ..
|
||||
</script>
|
||||
```
|
||||
|
||||
## Node.js builds
|
||||
|
||||
The Node.js runtime for the `@perspective-dev/client` module runs in-process by
|
||||
default and does not implement a `child_process` interface. Hence, there is no
|
||||
`worker()` method, and the module object itself directly exports the full
|
||||
`perspective` API.
|
||||
|
||||
```javascript
|
||||
const perspective = require("@perspective-dev/client");
|
||||
```
|
||||
|
||||
In Node.js, perspective does not run in a WebWorker (as this API does not exist
|
||||
in Node.js), so no need to call the `.worker()` factory function - the
|
||||
`perspective` library exports the functions directly and run synchronously in
|
||||
the main process.
|
||||
@@ -0,0 +1,54 @@
|
||||
# JavaScript Installation and Module Structure
|
||||
|
||||
Perspective is designed for flexibility, allowing developers to pick and choose
|
||||
which modules they need. The main modules are:
|
||||
|
||||
- `@perspective-dev/client`
|
||||
The data engine library, as both a browser ES6 and Node.js module. Provides a
|
||||
WebAssembly, WebWorker (browser) and Process (node.js) runtime.
|
||||
|
||||
- `@perspective-dev/viewer`
|
||||
A user-configurable visualization widget, bundled as a
|
||||
[Web Component](https://www.webcomponents.org/introduction). This module
|
||||
includes the core data engine module as a dependency.
|
||||
|
||||
`<perspective-viewer>` by itself only implements a trivial debug renderer, which
|
||||
prints the currently configured `view()` as a CSV. Plugin modules are packaged
|
||||
separately and must be imported individually.
|
||||
|
||||
- `@perspective-dev/viewer-datagrid`
|
||||
A custom high-performance data-grid component based on HTML `<table>`.
|
||||
|
||||
- `@perspective-dev/viewer-charts`
|
||||
A set of charting components base on WebGL.
|
||||
|
||||
When imported after `@perspective-dev/viewer`, the plugin modules will register
|
||||
themselves automatically, and the renderers they export will be available in the
|
||||
`plugin` dropdown in the `<perspective-viewer>` UI.
|
||||
|
||||
## Browser
|
||||
|
||||
Perspective's WebAssembly data engine is available via NPM in the same package
|
||||
as its Node.js counterpart, `@perspective-dev/client`. The Perspective Viewer UI
|
||||
(which has no Node.js component) must be installed separately:
|
||||
|
||||
```bash
|
||||
$ npm add @perspective-dev/client @perspective-dev/viewer
|
||||
```
|
||||
|
||||
By itself, `@perspective-dev/viewer` does not provide any visualizations, only
|
||||
the UI framework. Perspective _Plugins_ provide visualizations and must be
|
||||
installed separately. All Plugins are optional - but a `<perspective-viewer>`
|
||||
without Plugins would be rather boring!
|
||||
|
||||
```bash
|
||||
$ npm add @perspective-dev/viewer-charts @perspective-dev/viewer-datagrid
|
||||
```
|
||||
|
||||
## Node.js
|
||||
|
||||
To use Perspective from a Node.js server, simply install via NPM.
|
||||
|
||||
```bash
|
||||
$ npm add @perspective-dev/client
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# Joining Tables
|
||||
|
||||
`perspective.join()` creates a read-only `Table` by joining two source tables on
|
||||
a shared key column. The result is reactive — it updates automatically when
|
||||
either source table changes. See [`Join`](../../explanation/join.md) for
|
||||
conceptual details.
|
||||
|
||||
## Basic Inner Join
|
||||
|
||||
```javascript
|
||||
const orders = await perspective.table([
|
||||
{ id: 1, product_id: 101, qty: 5 },
|
||||
{ id: 2, product_id: 102, qty: 3 },
|
||||
{ id: 3, product_id: 101, qty: 7 },
|
||||
]);
|
||||
|
||||
const products = await perspective.table([
|
||||
{ product_id: 101, name: "Widget" },
|
||||
{ product_id: 102, name: "Gadget" },
|
||||
]);
|
||||
|
||||
const joined = await perspective.join(orders, products, "product_id");
|
||||
const view = await joined.view();
|
||||
const json = await view.to_json();
|
||||
// [
|
||||
// { product_id: 101, id: 1, qty: 5, name: "Widget" },
|
||||
// { product_id: 101, id: 3, qty: 7, name: "Widget" },
|
||||
// { product_id: 102, id: 2, qty: 3, name: "Gadget" },
|
||||
// ]
|
||||
```
|
||||
|
||||
## Join Types
|
||||
|
||||
Pass `join_type` in the options to select inner, left, or outer join behavior:
|
||||
|
||||
```javascript
|
||||
// Left join: all left rows, nulls for unmatched right columns
|
||||
const left_joined = await perspective.join(left, right, "id", {
|
||||
join_type: "left",
|
||||
});
|
||||
|
||||
// Outer join: all rows from both tables
|
||||
const outer_joined = await perspective.join(left, right, "id", {
|
||||
join_type: "outer",
|
||||
});
|
||||
```
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
The joined table recomputes automatically when either source table is updated:
|
||||
|
||||
```javascript
|
||||
const left = await perspective.table([{ id: 1, x: 10 }]);
|
||||
const right = await perspective.table([{ id: 2, y: "b" }]);
|
||||
|
||||
const joined = await perspective.join(left, right, "id");
|
||||
const view = await joined.view();
|
||||
|
||||
let json = await view.to_json();
|
||||
// [] — no matching keys yet
|
||||
|
||||
await right.update([{ id: 1, y: "a" }]);
|
||||
json = await view.to_json();
|
||||
// [{ id: 1, x: 10, y: "a" }] — new match detected
|
||||
```
|
||||
@@ -0,0 +1,76 @@
|
||||
# Loading data from a Table
|
||||
|
||||
Data can be loaded into `<perspective-viewer>` in the form of a `Table()` or a
|
||||
`Promise<Table>` via the `load()` method.
|
||||
|
||||
```javascript
|
||||
// Create a new worker, then a new table promise on that worker.
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(data);
|
||||
|
||||
// Bind a viewer element to this table.
|
||||
await viewer.load(table);
|
||||
```
|
||||
|
||||
## Sharing a `Table` between multiple `<perspective-viewer>`s
|
||||
|
||||
Multiple `<perspective-viewer>`s can share a `table()` by passing the `table()`
|
||||
into the `load()` method of each viewer. Each `perspective-viewer` will update
|
||||
when the underlying `table()` is updated, but `table.delete()` will fail until
|
||||
all `perspective-viewer` instances referencing it are also deleted:
|
||||
|
||||
```javascript
|
||||
const viewer1 = document.getElementById("viewer1");
|
||||
const viewer2 = document.getElementById("viewer2");
|
||||
|
||||
// Create a new WebWorker
|
||||
const worker = await perspective.worker();
|
||||
|
||||
// Create a table in this worker
|
||||
const table = await worker.table(data);
|
||||
|
||||
// Load the same table in 2 different <perspective-viewer> elements
|
||||
await viewer1.load(table);
|
||||
await viewer2.load(table);
|
||||
|
||||
// Both `viewer1` and `viewer2` will reflect this update
|
||||
await table.update([{ x: 5, y: "e", z: true }]);
|
||||
```
|
||||
|
||||
## Loading from a virtual `Table`
|
||||
|
||||
Loading a virtual (server-only) `Table` works just like loading a local/Web
|
||||
Worker `Table` — just pass the virtual `Table` to `viewer.load()`. In the
|
||||
browser:
|
||||
|
||||
```javascript
|
||||
const elem = document.getElementsByTagName("perspective-viewer")[0];
|
||||
|
||||
// Bind to the server's worker instead of instantiating a Web Worker.
|
||||
const websocket = await perspective.websocket(
|
||||
window.location.origin.replace("http", "ws")
|
||||
);
|
||||
|
||||
// Bind the viewer to the preloaded data source. `table` and `view` objects
|
||||
// live on the server.
|
||||
const server_table = await websocket.open_table("table_one");
|
||||
await elem.load(server_table);
|
||||
```
|
||||
|
||||
Alternatively, data can be _cloned_ from a server-side virtual `Table` into a
|
||||
client-side WebAssembly `Table`. The browser clone will be synced via delta
|
||||
updates transferred via Apache Arrow IPC format, but local `View`s created will
|
||||
be calculated locally on the client browser.
|
||||
|
||||
```javascript
|
||||
const worker = await perspective.worker();
|
||||
const server_view = await server_table.view();
|
||||
const client_table = worker.table(server_view);
|
||||
await elem.load(client_table);
|
||||
```
|
||||
|
||||
`<perspective-viewer>` instances bound in this way are otherwise no different
|
||||
than `<perspective-viewer>`s which rely on a Web Worker, and can even share a
|
||||
host application with Web Worker-bound `table()`s. The same `promise`-based API
|
||||
is used to communicate with the server-instantiated `view()`, only in this case
|
||||
it is over a websocket.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Server-only via `WebSocketServer()` and Node.js
|
||||
|
||||
For exceptionally large datasets, a `Client` can be bound to a
|
||||
`perspective.table()` instance running in Node.js/Python/Rust remotely, rather
|
||||
than creating one in a Web Worker and downloading the entire data set. This
|
||||
trades off network bandwidth and server resource requirements for a smaller
|
||||
browser memory and CPU footprint.
|
||||
|
||||
An example in Node.js:
|
||||
|
||||
```javascript
|
||||
const { WebSocketServer, table } = require("@perspective-dev/client");
|
||||
const fs = require("fs");
|
||||
|
||||
// Start a WS/HTTP host on port 8080. The `assets` property allows
|
||||
// the `WebSocketServer()` to also serves the file structure rooted in this
|
||||
// module's directory.
|
||||
const host = new WebSocketServer({ assets: [__dirname], port: 8080 });
|
||||
|
||||
// Read an arrow file from the file system and host it as a named table.
|
||||
const arr = fs.readFileSync(__dirname + "/superstore.lz4.arrow");
|
||||
await table(arr, { name: "table_one" });
|
||||
```
|
||||
|
||||
... and the [`Client`] implementation in the browser:
|
||||
|
||||
```javascript
|
||||
const elem = document.getElementsByTagName("perspective-viewer")[0];
|
||||
|
||||
// Bind to the server's worker instead of instantiating a Web Worker.
|
||||
const websocket = await perspective.websocket(
|
||||
window.location.origin.replace("http", "ws"),
|
||||
);
|
||||
|
||||
// Create a virtual `Table` to the preloaded data source. `table` and `view`
|
||||
// objects live on the server.
|
||||
const server_table = await websocket.open_table("table_one");
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
# Plugin render limits
|
||||
|
||||
`<perspective-viewer>` plugins (especially charts) may in some cases generate
|
||||
extremely large output which may lock up the browser. In order to prevent
|
||||
accidents (which generally require a browser refresh to fix), each plugin has a
|
||||
`max_cells` and `max_columns` heuristic which requires the user to opt-in to
|
||||
fully rendering `View`s which exceed these limits. To override this behavior,
|
||||
set these values for each plugin type individually, _before_ the plugin itself
|
||||
is rendered (e.g. calling `HTMLPerspectiveViewerElement::restore` with the
|
||||
respective `plugin` name).
|
||||
|
||||
If you have a `<perspective-viewer>` instance, you can configure plugins via
|
||||
`HTMLPerspectiveViewerElement::getPlugin` and
|
||||
`HTMLPerspectiveViewerElement::getAllPlugins`:
|
||||
|
||||
```javascript
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
const plugin = viewer.getPlugin("Treemap");
|
||||
plugin.max_cells = 1_000_000;
|
||||
plugin.max_columns = 1000;
|
||||
```
|
||||
|
||||
... Or alternatively, you can look up the Custom Element classes and set the
|
||||
static variants if you know the element name (you can e.g. look this up in your
|
||||
browser's DOM inspector):
|
||||
|
||||
```javascript
|
||||
const plugin = customElements.get("perspective-viewer-charts-treemap");
|
||||
plugin.max_cells = 1_000_000;
|
||||
plugin.max_columns = 1000;
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# React Component
|
||||
|
||||
We provide a React wrapper to prevent common issues and mistakes associated with
|
||||
using the perspective-viewer web component in the context of React.
|
||||
|
||||
Before trying this example, please take a look at
|
||||
[how to bootstrap perspective](./importing.md).
|
||||
|
||||
## `PerspectiveViewer`
|
||||
|
||||
A simple example using the `PerspectiveViewer` component:
|
||||
|
||||
```typescript
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
PerspectiveViewer,
|
||||
} from "@perspective-dev/react";
|
||||
import perspective from "@perspective-dev/client";
|
||||
|
||||
function App() {
|
||||
const worker = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
worker.current = await perspective.worker();
|
||||
const resp = await fetch("data.arrow");
|
||||
const arrow = await resp.arrayBuffer();
|
||||
await worker.current.table(arrow, { name: "my_table" });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PerspectiveViewer
|
||||
client={worker.current}
|
||||
config={{group_by: ["State"], columns: ["Sales"]}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## `PerspectiveWorkspace`
|
||||
|
||||
For multi-viewer layouts, use `PerspectiveWorkspace`:
|
||||
|
||||
```typescript
|
||||
import { PerspectiveWorkspace } from "@perspective-dev/react";
|
||||
|
||||
const WORKSPACE_CONFIG = // ...
|
||||
|
||||
function Dashboard() {
|
||||
return (
|
||||
<PerspectiveWorkspace
|
||||
client={perspective.worker()}
|
||||
config={WORKSPACE_CONFIG} />
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# Saving and restoring UI state.
|
||||
|
||||
`<perspective-viewer>` is _persistent_, in that its entire state (sans the data
|
||||
itself) can be serialized or deserialized. This include all column, filter,
|
||||
pivot, expressions, etc. properties, as well as datagrid style settings, config
|
||||
panel visibility, and more. This overloaded feature covers a range of use cases:
|
||||
|
||||
- Setting a `<perspective-viewer>`'s initial state after a `load()` call.
|
||||
- Updating a single or subset of properties, without modifying others.
|
||||
- Resetting some or all properties to their data-relative default.
|
||||
- Persisting a user's configuration to `localStorage` or a server.
|
||||
|
||||
## Serializing and deserializing the viewer state
|
||||
|
||||
To retrieve the entire state as a JSON-ready JavaScript object, use the `save()`
|
||||
method. `save()` also supports a few other formats such as `"arraybuffer"` and
|
||||
`"string"` (base64, not JSON), which you may choose for size at the expense of
|
||||
easy migration/manual-editing.
|
||||
|
||||
```javascript
|
||||
const json_token = await elem.save();
|
||||
const string_token = await elem.save("string");
|
||||
```
|
||||
|
||||
For any format, the serialized token can be restored to any
|
||||
`<perspective-viewer>` with a `Table` of identical schema, via the `restore()`
|
||||
method. Note that while the data for a token returned from `save()` may differ,
|
||||
generally its schema may not, as many other settings depend on column names and
|
||||
types.
|
||||
|
||||
```javascript
|
||||
await elem.restore(json_token);
|
||||
await elem.restore(string_token);
|
||||
```
|
||||
|
||||
As `restore()` dispatches on the token's type, it is important to make sure that
|
||||
these types match! A common source of error occurs when passing a
|
||||
JSON-stringified token to `restore()`, which will assume base64-encoded msgpack
|
||||
when a string token is used.
|
||||
|
||||
```javascript
|
||||
// This will error!
|
||||
await elem.restore(JSON.stringify(json_token));
|
||||
```
|
||||
|
||||
### Updating individual properties
|
||||
|
||||
Using the JSON format, every facet of a `<perspective-viewer>`'s configuration
|
||||
can be manipulated from JavaScript using the `restore()` method. The valid
|
||||
structure of properties is described via the
|
||||
[`ViewerConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa/rust/perspective-viewer/src/ts/viewer.ts#L16)
|
||||
and embedded
|
||||
[`ViewConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa19435a2a57d4687be7e428a4efc759b/packages/perspective/index.d.ts#L140)
|
||||
type declarations, and [`View`](view.md) chapter of the documentation which has
|
||||
several interactive examples for each `ViewConfig` property.
|
||||
|
||||
```javascript
|
||||
// Set the plugin (will also update `columns` to plugin-defaults)
|
||||
await elem.restore({ plugin: "X Bar" });
|
||||
|
||||
// Update plugin and columns (only draws once)
|
||||
await elem.restore({ plugin: "X Bar", columns: ["Sales"] });
|
||||
|
||||
// Open the config panel
|
||||
await elem.restore({ settings: true });
|
||||
|
||||
// Create an expression
|
||||
await elem.restore({
|
||||
columns: ['"Sales" + 100'],
|
||||
expressions: { "New Column": '"Sales" + 100' },
|
||||
});
|
||||
|
||||
// ERROR if the column does not exist in the schema or expressions
|
||||
// await elem.restore({columns: ["\"Sales\" + 100"], expressions: {}});
|
||||
|
||||
// Add a filter
|
||||
await elem.restore({ filter: [["Sales", "<", 100]] });
|
||||
|
||||
// Add a sort, don't remove filter
|
||||
await elem.restore({ sort: [["Prodit", "desc"]] });
|
||||
|
||||
// Reset just filter, preserve sort
|
||||
await elem.restore({ filter: undefined });
|
||||
|
||||
// Reset all properties to default e.g. after `load()`
|
||||
await elem.reset();
|
||||
```
|
||||
|
||||
Another effective way to quickly create a token for a desired configuration is
|
||||
to simply copy the token returned from `save()` after settings the view manually
|
||||
in the browser. The JSON format is human-readable and should be quite easy to
|
||||
tweak once generated, as `save()` will return even the default settings for all
|
||||
properties. You can call `save()` in your application code, or e.g. through the
|
||||
Chrome developer console:
|
||||
|
||||
```javascript
|
||||
// Copy to clipboard
|
||||
copy(await document.querySelector("perspective-viewer").save());
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
### Serializing data
|
||||
|
||||
The `view()` allows for serialization of data to JavaScript through the
|
||||
`to_json()`, `to_ndjson()`, `to_columns()`, `to_csv()`, and `to_arrow()` methods
|
||||
(the same data formats supported by the `Client::table` factory function). These
|
||||
methods return a `promise` for the calculated data:
|
||||
|
||||
```javascript
|
||||
const view = await table.view({ group_by: ["State"], columns: ["Sales"] });
|
||||
|
||||
// JavaScript Objects
|
||||
console.log(await view.to_json());
|
||||
console.log(await view.to_columns());
|
||||
|
||||
// String
|
||||
console.log(await view.to_csv());
|
||||
console.log(await view.to_ndjson());
|
||||
|
||||
// ArrayBuffer
|
||||
console.log(await view.to_arrow());
|
||||
```
|
||||
@@ -0,0 +1,96 @@
|
||||
# Theming
|
||||
|
||||
Theming is supported in `perspective-viewer` and its accompanying plugins. A
|
||||
number of themes come bundled with `perspective-viewer`; you can import any of
|
||||
these themes directly into your app, and the `perspective-viewer`s will be
|
||||
themed accordingly:
|
||||
|
||||
```javascript
|
||||
// Themes based on Thought Merchants's Prospective design
|
||||
import "@perspective-dev/viewer/dist/css/pro.css";
|
||||
import "@perspective-dev/viewer/dist/css/pro-dark.css";
|
||||
|
||||
// Other themes
|
||||
import "@perspective-dev/viewer/dist/css/solarized.css";
|
||||
import "@perspective-dev/viewer/dist/css/solarized-dark.css";
|
||||
import "@perspective-dev/viewer/dist/css/monokai.css";
|
||||
import "@perspective-dev/viewer/dist/css/vaporwave.css";
|
||||
```
|
||||
|
||||
Alternatively, you may use `themes.css`, which bundles all default themes
|
||||
|
||||
```javascript
|
||||
import "@perspective-dev/viewer/dist/css/themes.css";
|
||||
```
|
||||
|
||||
If you choose not to bundle the themes yourself, they are available through
|
||||
[CDN](https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/). These can
|
||||
be directly linked in your HTML file:
|
||||
|
||||
```html
|
||||
<link
|
||||
rel="stylesheet"
|
||||
crossorigin="anonymous"
|
||||
href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/pro.css"
|
||||
/>
|
||||
```
|
||||
|
||||
Note the `crossorigin="anonymous"` attribute. When including a theme from a
|
||||
cross-origin context, this attribute may be required to allow
|
||||
`<perspective-viewer>` to detect the theme. If this fails, additional themes are
|
||||
added to the `document` after `<perspective-viewer>` init, or for any other
|
||||
reason theme auto-detection fails, you may manually inform
|
||||
`<perspective-viewer>` of the available theme names with the `.resetThemes()`
|
||||
method.
|
||||
|
||||
```javascript
|
||||
// re-auto-detect themes
|
||||
viewer.resetThemes();
|
||||
|
||||
// Set available themes explicitly (they still must be imported as CSS!)
|
||||
viewer.resetThemes(["Pro Light", "Pro Dark"]);
|
||||
```
|
||||
|
||||
`<perspective-viewer>` will default to the first loaded theme when initialized.
|
||||
You may override this via `.restore()`, or provide an initial theme by setting
|
||||
the `theme` attribute:
|
||||
|
||||
```html
|
||||
<perspective-viewer theme="Pro Light"></perspective-viewer>
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```javascript
|
||||
const viewer = document.querySelector("perspective-viewer");
|
||||
await viewer.restore({ theme: "Pro Dark" });
|
||||
```
|
||||
|
||||
## Custom Themes
|
||||
|
||||
The best way to write a new theme is to
|
||||
[fork and modify an existing theme](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes),
|
||||
which are _just_ collections of regular CSS variables (no preprocessor is
|
||||
required, though Perspective's own themes use one). `<perspective-viewer>` is
|
||||
not "themed" by default and will lack icons and label text in addition to colors
|
||||
and fonts, so starting from an empty theme forces you to define _every_
|
||||
theme-able variable to get a functional UI.
|
||||
|
||||
### Icons and Translation
|
||||
|
||||
UI icons are defined by CSS variables provided by
|
||||
[`@perspective-dev/viewer/dist/css/icons.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/icons.css).
|
||||
These variables must be defined for the UI icons to work - there are no default
|
||||
icons without a theme.
|
||||
|
||||
UI text is also defined in CSS variables provided by
|
||||
[`@perspective-dev/viewer/dist/css/intl.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/intl.css),
|
||||
and has identical import requirements. Some _example definitions_
|
||||
(automatically-translated sans-editing) can be found
|
||||
[`@perspective-dev/viewer/dist/css/intl/` folder](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes/intl).
|
||||
|
||||
Importing the pre-built `themes.css` stylesheet as well as a custom theme will
|
||||
define Icons and Translation globally as a side-effect. You can still customize
|
||||
icons in this mode with rules (of the appropriate specificity), _but_ if you do
|
||||
not still remember to define these variables yourself, your theme will not work
|
||||
without the base `themes.css` package available.
|
||||
@@ -0,0 +1,62 @@
|
||||
# `<perspective-viewer>` Custom Element library
|
||||
|
||||
`<perspective-viewer>` provides a complete graphical UI for configuring the
|
||||
`perspective` library and formatting its output to the provided visualization
|
||||
plugins.
|
||||
|
||||
Once imported and initialized in JavaScript, the `<perspective-viewer>` Web
|
||||
Component will be available in any standard HTML on your site. A simple example:
|
||||
|
||||
```html
|
||||
<perspective-viewer id="view1"></perspective-viewer>
|
||||
<script type="module">
|
||||
import perspective from "@perspective-dev/client";
|
||||
import "@perspective-dev/viewer";
|
||||
|
||||
const worker = await perspective.worker();
|
||||
const table = await worker.table(data);
|
||||
document.getElementById("view1").load(table);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
`<perspective-viewer>` can be configured via HTML attributes or JavaScript
|
||||
properties. When set as attributes, the viewer will apply the configuration on
|
||||
initialization:
|
||||
|
||||
```html
|
||||
<perspective-viewer
|
||||
columns='["Sales", "Profit"]'
|
||||
group-by='["Region"]'
|
||||
sort='[["Sales", "desc"]]'>
|
||||
</perspective-viewer>
|
||||
```
|
||||
|
||||
## UI Features
|
||||
|
||||
The viewer provides an interactive side panel with:
|
||||
|
||||
- **Column list** - drag and drop columns to configure `group_by`, `split_by`,
|
||||
`sort`, and `filter` fields.
|
||||
- **New Column** button - opens an expression editor for creating computed
|
||||
columns via the [expression language](../../explanation/view/config/expressions.md).
|
||||
- **Plugin selector** - switch between visualization plugins such as Datagrid,
|
||||
X/Y Line, X/Y Scatter, Treemap, Sunburst, and Heatmap.
|
||||
- **Theme** selector - toggle between available themes.
|
||||
- **Export** - download the current view as CSV or Arrow.
|
||||
- **Copy** - copy the current view to the clipboard.
|
||||
- **Reset** - restore the viewer to its default configuration.
|
||||
|
||||
## Methods
|
||||
|
||||
Key methods on the `<perspective-viewer>` element:
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `load(table)` | Bind a `Table` to the viewer |
|
||||
| `restore(config)` | Apply a saved configuration object |
|
||||
| `save()` | Serialize the current configuration |
|
||||
| `reset(all)` | Reset configuration (pass `true` to also reset expressions) |
|
||||
| `getTable()` | Get the bound `Table` |
|
||||
| `flush()` | Wait for any pending UI updates to complete |
|
||||
@@ -0,0 +1,19 @@
|
||||
# Virtual Servers
|
||||
|
||||
Perspective's Virtual Server feature lets you connect `<perspective-viewer>` to
|
||||
external data sources without loading data into Perspective's built-in engine.
|
||||
Instead, queries are translated and executed natively by the external database.
|
||||
|
||||
For a detailed explanation of how virtual servers work, see the
|
||||
[Virtual Servers](../../explanation/virtual_servers.md) concepts page.
|
||||
|
||||
Perspective ships with built-in virtual server implementations for:
|
||||
|
||||
- [**DuckDB**](./virtual_server/duckdb.md) — query DuckDB databases in-browser
|
||||
via `@duckdb/duckdb-wasm`, or on the server via Node.js.
|
||||
- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server
|
||||
directly from the browser or from Node.js.
|
||||
|
||||
You can also [**implement your own**](./virtual_server/custom.md) virtual server
|
||||
to connect Perspective to any data source by implementing the
|
||||
`VirtualServerHandler` interface.
|
||||
@@ -0,0 +1,43 @@
|
||||
# ClickHouse Virtual Server
|
||||
|
||||
Perspective provides a built-in virtual server for
|
||||
[ClickHouse](https://clickhouse.com/), allowing `<perspective-viewer>` to query
|
||||
ClickHouse tables directly from the browser.
|
||||
|
||||
For server-side Python usage, see the
|
||||
[Python ClickHouse guide](../../python/virtual_server/clickhouse.md).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @perspective-dev/client @perspective-dev/viewer @clickhouse/client-web
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Connect to a ClickHouse instance and bind it to a Perspective viewer:
|
||||
|
||||
```javascript
|
||||
import perspective from "@perspective-dev/client";
|
||||
import "@perspective-dev/viewer";
|
||||
import { createClient } from "@clickhouse/client-web";
|
||||
|
||||
// Connect to ClickHouse
|
||||
const clickhouseClient = createClient({
|
||||
url: "http://localhost:8123",
|
||||
database: "default",
|
||||
});
|
||||
|
||||
// Create a Perspective virtual server backed by ClickHouse
|
||||
const handler = perspective.ClickhouseHandler(clickhouseClient);
|
||||
const messageHandler = perspective.createMessageHandler(handler);
|
||||
|
||||
// Connect a viewer
|
||||
const client = await perspective.worker(messageHandler);
|
||||
const table = await client.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Browser ClickHouse example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-clickhouse-virtual)
|
||||
@@ -0,0 +1,80 @@
|
||||
# Implementing a custom Virtual Server
|
||||
|
||||
You can connect Perspective to any data source by implementing the
|
||||
`VirtualServerHandler` interface and passing it to `createMessageHandler()`.
|
||||
|
||||
For background on virtual servers, see the
|
||||
[Virtual Servers overview](../../../explanation/virtual_servers.md).
|
||||
|
||||
## Example
|
||||
|
||||
```typescript
|
||||
import perspective from "@perspective-dev/client";
|
||||
import type {
|
||||
VirtualServerHandler,
|
||||
ColumnType,
|
||||
ViewConfig,
|
||||
ViewWindow,
|
||||
VirtualDataSlice,
|
||||
} from "@perspective-dev/client";
|
||||
|
||||
const handler = {
|
||||
async getHostedTables(): Promise<string[]> {
|
||||
return ["my_table"];
|
||||
},
|
||||
|
||||
async tableSchema(tableId: string): Promise<Record<string, ColumnType>> {
|
||||
return { name: "string", price: "float", date: "date" };
|
||||
},
|
||||
|
||||
async tableSize(tableId: string): Promise<number> {
|
||||
return 1000;
|
||||
},
|
||||
|
||||
async tableMakeView(
|
||||
tableId: string,
|
||||
viewId: string,
|
||||
config: ViewConfig,
|
||||
): Promise<void> {
|
||||
// Translate `config` (group_by, sort, filter, etc.) into a query
|
||||
// against your data source. Store the query keyed by `viewId`
|
||||
// for later data retrieval.
|
||||
},
|
||||
|
||||
async viewDelete(viewId: string): Promise<void> {
|
||||
// Clean up resources for this view
|
||||
},
|
||||
|
||||
async viewGetData(
|
||||
viewId: string,
|
||||
config: ViewConfig,
|
||||
schema: Record<string, ColumnType>,
|
||||
viewport: ViewWindow,
|
||||
dataSlice: VirtualDataSlice,
|
||||
): Promise<void> {
|
||||
// Query your data source using `config` and `viewport` for the
|
||||
// row/column window. Push columnar results via `dataSlice.setCol()`.
|
||||
},
|
||||
|
||||
getFeatures() {
|
||||
return {
|
||||
group_by: true,
|
||||
sort: true,
|
||||
filter_ops: {
|
||||
string: ["==", "!=", "contains", "is null", "is not null"],
|
||||
float: ["==", "!=", ">", "<", ">=", "<="],
|
||||
},
|
||||
aggregates: {
|
||||
float: ["sum", "avg", "count", "min", "max"],
|
||||
string: ["count", "any"],
|
||||
},
|
||||
};
|
||||
},
|
||||
} satisfies VirtualServerHandler;
|
||||
|
||||
// Create a message handler and use it like a worker
|
||||
const messageHandler = perspective.createMessageHandler(handler);
|
||||
const client = await perspective.worker(messageHandler);
|
||||
const table = await client.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# DuckDB Virtual Server
|
||||
|
||||
Perspective provides a built-in virtual server for
|
||||
[DuckDB](https://duckdb.org/), allowing `<perspective-viewer>` to query
|
||||
DuckDB-WASM databases directly in the browser.
|
||||
|
||||
For server-side Python usage, see the
|
||||
[Python DuckDB guide](../../python/virtual_server/duckdb.md).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @perspective-dev/client @perspective-dev/viewer @duckdb/duckdb-wasm
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Initialize DuckDB-WASM, load data, and connect it to a Perspective viewer:
|
||||
|
||||
```javascript
|
||||
import perspective from "@perspective-dev/client";
|
||||
import "@perspective-dev/viewer";
|
||||
import * as duckdb from "@duckdb/duckdb-wasm";
|
||||
|
||||
// Initialize DuckDB-WASM
|
||||
const DUCKDB_BUNDLES = duckdb.getJsDelivrBundles();
|
||||
const bundle = await duckdb.selectBundle(DUCKDB_BUNDLES);
|
||||
const worker = await duckdb.createWorker(bundle.mainWorker);
|
||||
const logger = new duckdb.ConsoleLogger();
|
||||
const db = new duckdb.AsyncDuckDB(logger, worker);
|
||||
await db.instantiate(bundle.mainModule);
|
||||
|
||||
// Load data into DuckDB
|
||||
const conn = await db.connect();
|
||||
await conn.query(`CREATE TABLE my_table AS SELECT * FROM 'data.parquet'`);
|
||||
|
||||
// Create a Perspective virtual server backed by DuckDB
|
||||
const handler = perspective.DuckDBHandler(db);
|
||||
const messageHandler = perspective.createMessageHandler(handler);
|
||||
|
||||
// Connect a viewer
|
||||
const client = await perspective.worker(messageHandler);
|
||||
const table = await client.open_table("my_table");
|
||||
document.getElementById("viewer").load(table);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- [Browser DuckDB example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-duckdb-virtual)
|
||||
@@ -0,0 +1,44 @@
|
||||
# Accessing the Perspective engine via a `Client` instance
|
||||
|
||||
An instance of a `Client` is needed to talk to a Perspective `Server`, of which
|
||||
there are a few varieties available in JavaScript.
|
||||
|
||||
## Web Worker (Browser)
|
||||
|
||||
Perspective's Web Worker client is actually a `Client` and `Server` rolled into
|
||||
one. Instantiating this `Client` will also create a _dedicated_ Perspective
|
||||
`Server` in a Web Worker process.
|
||||
|
||||
To use it, you'll need to instantiate a Web Worker `perspective` engine via the
|
||||
`worker()` method. This will create a new Web Worker (browser) and load the
|
||||
WebAssembly binary. All calculation and data accumulation will occur in this
|
||||
separate process.
|
||||
|
||||
```javascript
|
||||
const client = await perspective.worker();
|
||||
```
|
||||
|
||||
The `worker` symbol will expose the full `perspective` API for one managed Web
|
||||
Worker process. You are free to create as many as your browser supports, but be
|
||||
sure to keep track of the `worker` instances themselves, as you'll need them to
|
||||
interact with your data in each instance.
|
||||
|
||||
## Websocket (Browser)
|
||||
|
||||
Alternatively, with a Perspective server running in Node.js, Python or Rust, you
|
||||
can create a _virtual_ `Client` via the `websocket()` method.
|
||||
|
||||
```javascript
|
||||
const client = perspective.websocket("http://localhost:8080/");
|
||||
```
|
||||
|
||||
## Node.js
|
||||
|
||||
The Node.js runtime for the `@perspective-dev/client` module runs in-process by
|
||||
default and does not implement a `child_process` interface, so no need to call
|
||||
the `.worker()` factory function. Instead, the `perspective` library exports the
|
||||
functions directly and run synchronously in the main process.
|
||||
|
||||
```javascript
|
||||
const client = require("@perspective-dev/client");
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
# Python
|
||||
|
||||
Guides for using `perspective-python`, including data loading, callbacks,
|
||||
multithreading, WebSocket servers, and JupyterLab integration.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Callbacks and Events
|
||||
|
||||
`perspective.Table` allows for `on_update` and `on_delete` callbacks to be
|
||||
set—simply call `on_update` or `on_delete` with a reference to a function or a
|
||||
lambda without any parameters:
|
||||
|
||||
```python
|
||||
def update_callback():
|
||||
print("Updated!")
|
||||
|
||||
# set the update callback
|
||||
on_update_id = view.on_update(update_callback)
|
||||
|
||||
|
||||
def delete_callback():
|
||||
print("Deleted!")
|
||||
|
||||
# set the delete callback
|
||||
on_delete_id = view.on_delete(delete_callback)
|
||||
|
||||
# set a lambda as a callback
|
||||
view.on_delete(lambda: print("Deleted x2!"))
|
||||
```
|
||||
|
||||
If the callback is a named reference to a function, it can be removed with
|
||||
`remove_update` or `remove_delete`:
|
||||
|
||||
```python
|
||||
view.remove_update(on_update_id)
|
||||
view.remove_delete(on_delete_id)
|
||||
```
|
||||
|
||||
Callbacks defined with a lambda function cannot be removed, as lambda functions
|
||||
have no identifier.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Installation
|
||||
|
||||
`perspective-python` contains full bindings to the Perspective API, a JupyterLab
|
||||
widget, and WebSocket handlers for several webserver libraries that allow you to
|
||||
host Perspective using server-side Python.
|
||||
|
||||
## PyPI
|
||||
|
||||
`perspective-python` can be installed from [PyPI](https://pypi.org) via `pip`:
|
||||
|
||||
```bash
|
||||
pip install perspective-python
|
||||
```
|
||||
|
||||
That's it! If JupyterLab is installed in this Python environment, you'll also
|
||||
get the `perspective.widget.PerspectiveWidget` class when you import
|
||||
`perspective` in a Jupyter Lab kernel.
|
||||
|
||||
<!--
|
||||
### Anaconda
|
||||
|
||||
`perspective-python` can also be installed for [Anaconda](https://anaconda.org/)
|
||||
via [Conda Forge](https://conda-forge.org)
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge perspective
|
||||
``` -->
|
||||
@@ -0,0 +1,64 @@
|
||||
# Joining Tables
|
||||
|
||||
`perspective.join()` creates a read-only `Table` by joining two source tables on
|
||||
a shared key column. The result is reactive — it updates automatically when
|
||||
either source table changes. See [`Join`](../../explanation/join.md) for
|
||||
conceptual details.
|
||||
|
||||
## Basic Inner Join
|
||||
|
||||
```python
|
||||
orders = perspective.table([
|
||||
{"id": 1, "product_id": 101, "qty": 5},
|
||||
{"id": 2, "product_id": 102, "qty": 3},
|
||||
{"id": 3, "product_id": 101, "qty": 7},
|
||||
])
|
||||
|
||||
products = perspective.table([
|
||||
{"product_id": 101, "name": "Widget"},
|
||||
{"product_id": 102, "name": "Gadget"},
|
||||
])
|
||||
|
||||
joined = perspective.join(orders, products, "product_id")
|
||||
view = joined.view()
|
||||
json = view.to_json()
|
||||
```
|
||||
|
||||
## Join Types
|
||||
|
||||
Pass `join_type` to select inner, left, or outer join behavior:
|
||||
|
||||
```python
|
||||
# Left join: all left rows, nulls for unmatched right columns
|
||||
left_joined = perspective.join(left, right, "id", join_type="left")
|
||||
|
||||
# Outer join: all rows from both tables
|
||||
outer_joined = perspective.join(left, right, "id", join_type="outer")
|
||||
```
|
||||
|
||||
## Reactive Updates
|
||||
|
||||
The joined table recomputes automatically when either source table is updated:
|
||||
|
||||
```python
|
||||
left = perspective.table([{"id": 1, "x": 10}])
|
||||
right = perspective.table([{"id": 2, "y": "b"}])
|
||||
|
||||
joined = perspective.join(left, right, "id")
|
||||
view = joined.view()
|
||||
|
||||
json = view.to_json()
|
||||
# [] — no matching keys yet
|
||||
|
||||
right.update([{"id": 1, "y": "a"}])
|
||||
json = view.to_json()
|
||||
# [{"id": 1, "x": 10, "y": "a"}] — new match detected
|
||||
```
|
||||
|
||||
## Async Client
|
||||
|
||||
The async client has the same API:
|
||||
|
||||
```python
|
||||
joined = await client.join(orders, products, "product_id", join_type="left")
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# `PerspectiveWidget` for JupyterLab
|
||||
|
||||
Building on top of the API provided by `perspective.Table`, the
|
||||
`PerspectiveWidget` is a JupyterLab plugin that offers the entire functionality
|
||||
of Perspective within the Jupyter environment. It supports the same API
|
||||
semantics of `<perspective-viewer>`, along with the additional data types
|
||||
supported by `perspective.Table`. `PerspectiveWidget` takes keyword arguments
|
||||
for the managed `View`:
|
||||
|
||||
```python
|
||||
from perspective.widget import PerspectiveWidget
|
||||
w = perspective.PerspectiveWidget(
|
||||
data,
|
||||
plugin="X Bar",
|
||||
aggregates={"datetime": "any"},
|
||||
sort=[["date", "desc"]]
|
||||
)
|
||||
```
|
||||
|
||||
## Creating a widget
|
||||
|
||||
A widget is created through the `PerspectiveWidget` constructor, which takes as
|
||||
its first, required parameter a `perspective.Table`, a dataset, a schema, or
|
||||
`None`, which serves as a special value that tells the Widget to defer loading
|
||||
any data until later. In maintaining consistency with the Javascript API,
|
||||
Widgets cannot be created with empty dictionaries or lists — `None` should be
|
||||
used if the intention is to await data for loading later on. A widget can be
|
||||
constructed from a dataset:
|
||||
|
||||
```python
|
||||
from perspective.widget import PerspectiveWidget
|
||||
PerspectiveWidget(data, group_by=["date"])
|
||||
```
|
||||
|
||||
.. or a schema:
|
||||
|
||||
```python
|
||||
PerspectiveWidget({"a": int, "b": str})
|
||||
```
|
||||
|
||||
.. or an instance of a `perspective.Table`:
|
||||
|
||||
```python
|
||||
table = perspective.table(data)
|
||||
PerspectiveWidget(table)
|
||||
```
|
||||
|
||||
## Updating a widget
|
||||
|
||||
`PerspectiveWidget` shares a similar API to the `<perspective-viewer>` Custom
|
||||
Element, and has similar `save()` and `restore()` methods that
|
||||
serialize/deserialize UI state for the widget.
|
||||
|
||||
<!--
|
||||
## `PerspectiveRenderer`
|
||||
|
||||
Perspective also exposes a JS-only `mimerender-extension`. This lets you view
|
||||
`csv`, `json`, and `arrow` files directly from the file browser. You can see
|
||||
this by right clicking one of these files and `Open With->CSVPerspective` (or
|
||||
`JSONPerspective` or `ArrowPerspective`). Perspective will also install itself
|
||||
as the default handler for opening `.arrow` files. -->
|
||||
@@ -0,0 +1,67 @@
|
||||
# Multi-threading
|
||||
|
||||
Perspective's API is thread-safe, so methods may be called from different
|
||||
threads without additional consideration for safety/exclusivity/correctness. All
|
||||
`perspective.Client` and `perspective.Server` API methods release the GIL, which
|
||||
can be exploited for parallelism.
|
||||
|
||||
Interally, `perspective.Server` also dispatches to a thread pool for some
|
||||
operations, enabling better parallelism and overall better query performance.
|
||||
This independent threadpool size can be controlled via
|
||||
`perspective.set_num_cpus()`, or the `OMP_NUM_THREADS` environment variable.
|
||||
|
||||
```python
|
||||
import perspective
|
||||
|
||||
perspective.set_num_cpus(2)
|
||||
```
|
||||
|
||||
## Server handlers
|
||||
|
||||
Perspective's server handler implementations each take an optional `executor`
|
||||
constructor argument, which (when provided) will configure the handler to
|
||||
process WebSocket `Client` requests on a thread pool.
|
||||
|
||||
```python
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from tornado.web import Application
|
||||
from perspective.handlers.tornado import PerspectiveTornadoHandler
|
||||
from perspective import Server
|
||||
|
||||
args = {"perspective_server": Server(), "executor": ThreadPoolExecutor()}
|
||||
|
||||
app = Application(
|
||||
[
|
||||
(r"/websocket", PerspectiveTornadoHandler, args),
|
||||
|
||||
# ...
|
||||
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## `on_poll_request`
|
||||
|
||||
`on_poll_request` is an optional keyword argument for `Server()`, which which
|
||||
can be applied in cases where overlapping `Table.update` calls can be safely
|
||||
deferred.
|
||||
|
||||
When providing a callback function to `on_poll_request`, the `Server` will
|
||||
invoke your callback when there are updates that need to be flushed, after which
|
||||
you must _eventually_ call `Server.poll` (or else no updates will be processed).
|
||||
|
||||
The exact implementation of `on_poll_request` will depend on the context. A
|
||||
simple example which batches calls via `threading.Lock`:
|
||||
|
||||
```python
|
||||
lock = threading.Lock()
|
||||
|
||||
def on_poll_request(perspective_server):
|
||||
if lock.acquire(blocking=False):
|
||||
try:
|
||||
perspective_server.poll()
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
server = Server(on_poll_request=on_poll_request)
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Loading data into a Table
|
||||
|
||||
A `Table` can be created from a dataset or a schema, the specifics of which are
|
||||
[discussed](#loading-data-with-table) in the JavaScript section of the user's
|
||||
guide. In Python, however, Perspective supports additional data types that are
|
||||
commonly used when processing data:
|
||||
|
||||
- `pandas.DataFrame`
|
||||
- `polars.DataFrame`
|
||||
- `bytes` (encoding an Apache Arrow)
|
||||
- `objects` (either extracting a repr or via reference)
|
||||
- `str` (encoding as a CSV)
|
||||
|
||||
A `Table` is created in a similar fashion to its JavaScript equivalent:
|
||||
|
||||
```python
|
||||
from datetime import date, datetime
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import perspective
|
||||
|
||||
data = pd.DataFrame({
|
||||
"int": np.arange(100),
|
||||
"float": [i * 1.5 for i in range(100)],
|
||||
"bool": [True for i in range(100)],
|
||||
"date": [date.today() for i in range(100)],
|
||||
"datetime": [datetime.now() for i in range(100)],
|
||||
"string": [str(i) for i in range(100)]
|
||||
})
|
||||
|
||||
table = perspective.table(data, index="float")
|
||||
```
|
||||
|
||||
Likewise, a `View` can be created via the `view()` method:
|
||||
|
||||
```python
|
||||
view = table.view(group_by=["float"], filter=[["bool", "==", True]])
|
||||
column_data = view.to_columns()
|
||||
row_data = view.to_json()
|
||||
```
|
||||
|
||||
## Polars Support
|
||||
|
||||
Polars `DataFrame` types work similarly to Apache Arrow input, which Perspective
|
||||
uses to interface with Polars.
|
||||
|
||||
```python
|
||||
df = polars.DataFrame({"a": [1,2,3,4,5]})
|
||||
table = perspective.table(df)
|
||||
```
|
||||
|
||||
## Pandas Support
|
||||
|
||||
Perspective's `Table` can be constructed from `pandas.DataFrame` objects.
|
||||
Internally, this just uses
|
||||
[`pyarrow::from_pandas`](https://arrow.apache.org/docs/python/pandas.html),
|
||||
which dictates behavior of this feature including type support.
|
||||
|
||||
If the dataframe does not have an index set, an integer-typed column named
|
||||
`"index"` is created. If you want to preserve the indexing behavior of the
|
||||
dataframe passed into Perspective, simply create the `Table` with
|
||||
`index="index"` as a keyword argument. This tells Perspective to once again
|
||||
treat the index as a primary key:
|
||||
|
||||
```python
|
||||
data.set_index("datetime")
|
||||
table = perspective.table(data, index="index")
|
||||
```
|
||||
|
||||
## Time Zone Handling
|
||||
|
||||
When parsing `"datetime"` strings, times without an explicit timezone offset are
|
||||
interpreted as _UTC_. Strings with a timezone offset (e.g., `+05:00`) are
|
||||
converted to UTC. All `"datetime"` values are stored internally as milliseconds
|
||||
since the Unix epoch, and are _output_ as integer timestamps (milliseconds since
|
||||
epoch) from methods like `to_columns()` and `to_json()`.
|
||||
|
||||
Python `datetime` objects are serialized to strings before parsing. Naive
|
||||
`datetime` objects (without `tzinfo`) produce strings without timezone
|
||||
information and are therefore treated as UTC. Timezone-aware `datetime` objects
|
||||
include their offset in the serialized string, which is used to convert to UTC.
|
||||
|
||||
`"date"` values are timezone-agnostic calendar days with no time component.
|
||||
They are _output_ as integer timestamps at _UTC midnight_ of the calendar day
|
||||
(equivalent to Arrow `date32` day arithmetic), and integer timestamp _input_ to
|
||||
a `"date"` column is likewise interpreted as UTC. The host process timezone
|
||||
never affects `"date"` values — a `Viewer` renders them in UTC, recovering the
|
||||
stored calendar day exactly. Datetime expression functions such as
|
||||
`bucket("x", 'D')`, `day_of_week("x")` and `hour_of_day("x")` also compute in
|
||||
UTC.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user