Perspective is an interactive analytics and data visualization component, which is especially well-suited for large and/or streaming datasets. Use it to create user-configurable reports, dashboards, notebooks and applications.
Features
-
A fast, memory efficient streaming query engine, written in C++ and compiled for WebAssembly, Python and Rust, with read/write/streaming for Apache Arrow, and a high-performance columnar expression language based on ExprTK.
-
A framework-agnostic User Interface packaged as a Custom Element, powered either in-browser via WebAssembly or virtually via WebSocket server (Python/Node/Rust).
-
A JupyterLab widget and Python client library, for interactive data analysis in a notebook, as well as scalable production applications.
Documentation
- Project Site
- User Guide
@finos/perspective
, JavaScript Client API@finos/perspective-viewer
, JavaScript UI APIperspective-python
, Python APIperspective
, Rust API
Examples
editable | file | fractal |
market | raycasting | evictions |
nypd | streaming | covid |
webcam | movies | superstore |
citibike | olympics | |
Media
@timkpaine |
@timbess |
@sc1f |
@texodus |
@texodus |
|
Data Architecture
Application developers can choose from Client (WebAssembly), Server (Python/Node) or Client/Server Replicated designs to bind data, and a web application can use one or a mix of these designs as needed. By serializing to Apache Arrow, tables are duplicated and synchronized across runtimes efficiently.
Perspective is a multi-language platform. The examples in this section use
Python and JavaScript as an example, but the same general principles apply to
any Client
/Server
combination.
Client-only
For static datasets, datasets provided by the user, and simple server-less and read-only web applications.
In this design, Perspective is run as a client Browser WebAssembly library, the dataset is downloaded entirely to the client and all calculations and UI interactions are performed locally. Interactive performance is very good, using WebAssembly engine for near-native runtime plus WebWorker isolation for parallel rendering within the browser. Operations like scrolling and creating new views are responsive. However, the entire dataset must be downloaded to the client. Perspective is not a typical browser component, and datset sizes of 1gb+ in Apache Arrow format will load fine with good interactive performance!
Horizontal scaling is a non-issue, since here is no concurrent state to scale, and only uses client-side computation via WebAssembly client. Client-only perspective can support as many concurrent users as can download the web application itself. Once the data is loaded, no server connection is needed and all operations occur in the client browser, imparting no additional runtime cost on the server beyond initial load. This also means updates and edits are local to the browser client and will be lost when the page is refreshed, unless otherwise persisted by your application.
As the client-only design starts with creating a client-side Perspective
Table
, data can be provided by any standard web service in any Perspective
compatible format (JSON, CSV or Apache Arrow).
Javascript client
const worker = await perspective.worker();
const table = await worker.table(csv);
const viewer = document.createElement("perspective-viewer");
document.body.appendChild(viewer);
await viewer.load(table);
Client/Server replicated
For medium-sized, real-time, synchronized and/or editable data sets with many concurrent users.
The dataset is instantiated in-memory with a Python or Node.js Perspective server, and web applications create duplicates of these tables in a local WebAssembly client in the browser, synchonized efficiently to the server via Apache Arrow. This design scales well with additional concurrent users, as browsers only need to download the initial data set and subsequent update deltas, while operations like scrolling, pivots, sorting, etc. are performed on the client.
Python servers can make especially good use of additional threads, as Perspective will release the GIL for almost all operations. Interactive performance on the client is very good and identical to client-only architecture. Updates and edits are seamlessly synchonized across clients via their virtual server counterparts using websockets and Apache Arrow.
Python and Tornado server
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.
const websocket = await perspective.websocket("ws://localhost:8080");
const server_table = await websocket.open_table("my_table");
const server_view = await server_table.view();
const worker = await perspective.worker();
const client_table = await worker.table(server_view);
const viewer = document.createElement("perspective-viewer");
document.body.appendChild(viewer);
await viewer.load(client_table);
Server-only
For extremely large datasets with a small number of concurrent users.
The dataset is instantiated in-memory with a Python or Node.js server, and web applications connect virtually. Has very good initial load performance, since no data is downloaded. Group-by and other operations will run column-parallel if configured.
But interactive performance is poor, as every user interaction must page the server to render. Operations like scrolling are not as responsive and can be impacted by network latency. Web applications must be "always connected" to the server via WebSocket. Disconnecting will prevent any interaction, scrolling, etc. of the UI. Does not use WebAssembly.
Each connected browser will impact server performance as long as the connection
is open, which in turn impacts interactive performance of every client. This
ultimately limits the horizontal scalabity of this architecture. Since each
client reads the perspective Table
virtually, changes like edits and updates
are automatically reflected to all clients and persist across browser refresh.
Using the same Python server as the previous design, we can simply skip the
intermediate WebAssembly Table
and pass the virtual table directly to load()
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);
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
.
Construct a Table
Examples of constructing an empty Table
from a schema.
JavaScript:
var schema = {
x: "integer",
y: "string",
z: "boolean",
};
const table2 = await worker.table(schema);
Python:
from datetime import date, datetime
schema = {
"x": "integer",
"y": "string",
"z": "boolean",
}
table2 = perspective.table(schema)
Rust:
#![allow(unused)] fn main() { let data = TableData::Schema(vec![(" a".to_string(), ColumnType::FLOAT)]); let options = TableInitOptions::default(); let table = client.table(data.into(), options).await?; }
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 (which is also the format
Perspective will output these types for CSV).
Loading data
A Table
may also be created-or-updated by data in CSV,
Apache Arrow, 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 Pythondatetime.date
anddatetime.datetime
are not supported directly. However, in JavaScriptDate
types are automatically coerced to correctinteger
timestamps by default when converted to JSON.
Apache Arrow
The most efficient way to load data into Perspective, encoded as Apache Arrow IPC format. In 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:
[
{ "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.
{
"a": [86, 0, 12345],
"b": [false, true, false],
"c": ["words", "", "here"]
}
NDJSON
NDJSON is a format.
{ "a": 86, "b": false, "c": "words" }
{ "a": 0, "b": true, "c": "" }
{ "a": 12345, "b": false, "c": "here" }
Index and Limit
Initializing a Table
with an index
tells Perspective to treat a column as
the primary key, allowing in-place updates of rows. Only a single column (of any
type) can be used as an index
. Indexed Table
instances allow:
- In-place updates whenever a new row shares an
index
values with an existing row - Partial updates when a data batch omits some column.
- Removes to delete a row by
index
.
To create an indexed Table
, provide the index
property with a string column
name to be used as an index:
JavaScript:
const indexed_table = await perspective.table(data, { index: "a" });
Python
indexed_table = perspective.Table(data, index="a");
Initializing a Table
with a limit
sets the total number of rows the Table
is allowed to have. When the Table
is updated, and the resulting size of the
Table
would exceed its limit
, rows that exceed limit
overwrite the oldest
rows in the Table
. To create a Table
with a limit
, provide the limit
property with an integer indicating the maximum rows:
JavaScript:
const limit_table = await perspective.table(data, { limit: 1000 });
Python:
limit_table = perspective.Table(data, limit=1000);
Table::update
and Table::remove
Once a Table
has been created, it can be updated with new data conforming to
the Table
's schema. Table::update
supports the same data formats as
Client::table
, minus schema.
const schema = {
a: "integer",
b: "float",
};
const table = await perspective.table(schema);
table.update(new_data);
schema = {"a": "integer", "b": "float"}
table = perspective.Table(schema)
table.update(new_data)
Without an index
set, calls to update()
append new data to the end of the
Table
. Otherwise, Perspective allows
partial updates (in-place) using the index
to determine
which rows to update:
indexed_table.update({ id: [1, 4], name: ["x", "y"] });
indexed_table.update({"id": [1, 4], "name": ["x", "y"]})
Any value on a Client::table
can be unset using the value null
in JSON or
Arrow input formats. Values may be unset on construction, as any null
in the
dataset will be treated as an unset value. Table::update
calls do not need to
provide all columns in the Table
's schema; missing columns will be omitted
from the Table
's updated rows.
table.update([{ x: 3, y: null }]); // `z` missing
table.update([{"x": 3, "y": None}]) // `z` missing
Rows can also be removed from an indexed Table
, by calling Table::remove
with an array of index values:
indexed_table.remove([1, 4]);
// Python
indexed_table.remove([1, 4])
Table::clear
and Table::replace
Calling Table::clear
will remove all data from the underlying Table
. Calling
Table::replace
with new data will clear the Table
, and update it with a new
dataset that conforms to Perspective's data types and the existing schema on the
Table
.
table.clear();
table.replace(json);
table.clear()
table.replace(df)
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.
perspective
docs for the Rust API.
perspective
docs for the Rust API.
Examples
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();
table = perspective.Table({
"id": [1, 2, 3, 4],
"name": ["a", "b", "c", "d"]
});
view = table.view(columns=["name"])
arrow = view.to_arrow()
view.delete()
#![allow(unused)] fn main() { 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?; }
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:
const view = await table.view({
columns: ["Sales"],
aggregates: { Sales: "sum" },
group_by: ["Region", "Country"],
filter: [["Category", "in", ["Furniture", "Technology"]]],
});
view = table.view(
columns=["Sales"],
aggregates={"Sales": "sum"},
group_by=["Region", "Country"],
filter=[["Category", "in", ["Furniture", "Technology"]]]
)
#![allow(unused)] fn main() { 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?; }
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.
const view = await table.view({ group_by: ["a", "c"] });
view = table.view(group_by=["a", "c"])
#![allow(unused)] fn main() { let view = table.view(Some(ViewConfigUpdate { group_by: Some(vec!["a".into(), "c".into()]), ..ViewConfigUpdate::default() })).await?; }
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:
const view = await table.view({ split_by: ["a", "c"] });
view = table.view(split_by=["a", "c"])
#![allow(unused)] fn main() { let view = table.view(Some(ViewConfigUpdate { split_by: Some(vec!["a".into(), "c".into()]), ..ViewConfigUpdate::default() })).await?; }
Aggregates
Aggregates perform a calculation over an entire column, and are displayed when
one or more 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
andfloat
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.
const view = await table.view({
aggregates: {
a: "avg",
b: "distinct count",
},
});
view = table.view(
aggregates={
"a": "avg",
"b": "distinct count"
}
)
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:
const view = await table.view({
columns: ["a"],
});
view = table.view(columns=["a"])
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 column-pivots
are
applied, the additional sort directions "col asc"
and "col desc"
will
determine the order of pivot columns groups.
const view = await table.view({
sort: [["a", "asc"]],
});
view = table.view(sort=[["a", "asc"]])
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.
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:
const view = await table.view({
filter: [["a", "<", 100]],
});
view = table.view(filter=[["a", "<", 100]])
Expressions
The expressions
property specifies new columns in Perspective that are
created using existing column values or arbitary scalar values defined within
the expression. In <perspective-viewer>
, expressions are added using the "New
Column" button in the side panel.
A custom name can be added to an expression by making the first line a comment:
const view = await table.view({
expressions: { '"a" + "b"': '"a" + "b"' },
});
view = table.view(expressions=['"a" + "b"'])
Flattening a [Table::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 design, by
serializing the View
to an arrow and setting up an on_update
callback.
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" }]);
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"}])
#![allow(unused)] fn main() { 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?; }
Perspective's JavaScript library offers a configurable UI powered by the same fast streaming data engine, just re-compiled to WebAssembly. A simple example which loads an Apache Arrow and computes a "Group By" operation, returning a new Arrow:
import perspective from "@finos/perspective";
const table = await perspective.table(apache_arrow_data);
const view = await table.view({ group_by: ["CounterParty", "Security"] });
const arrow = await view.to_arrow();
More Examples are available on GitHub.
Module Structure
Perspective is designed for flexibility, allowing developers to pick and choose which modules they need for their specific use case. The main modules are:
-
@finos/perspective
The data engine library, as both a browser ES6 and Node.js module. Provides a WebAssembly, WebWorker (browser) and Process (node.js) runtime. -
@finos/perspective-viewer
A user-configurable visualization widget, bundled as a Web Component. 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 for popular
JavaScript libraries, such as d3fc, are packaged separately
and must be imported individually.
Perspective offers these plugin modules:
-
@finos/perspective-viewer-datagrid
A custom high-performance data-grid component based on HTML<table>
. -
@finos/perspective-viewer-d3fc
A<perspective-viewer>
plugin for the d3fc charting library.
When imported after @finos/perspective-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.
Which modules should I import?
Depending on your requirements, you may need just one, or all, Perspective modules. Here are some basic guidelines to help you decide what is most appropriate for your project:
-
For Perspective's high-performance streaming data engine (in WebAssembly), or for a purely Node.js based application, import:
@finos/perspective
, as detailed here
-
For Perspective as a simple, browser-based data visualization widget, you will need to import:
-
For more complex cases, such as sharing tables between viewers and binding a viewer to a remote view in Node.js, you will likely need all Perspective modules.
JavaScript Builds
Perspective requires the browser to have access to Perspective's .wasm
binaries in addition to the bundled .js
files, and as a result the build
process requires a few extra steps. To ease integration, Perspective's NPM
releases come with multiple prebuilt configurations.
Browser
ESM Builds
The recommended builds for production use are packaged as ES Modules and require
a bootstrapping step in order to acquire the .wasm
binaries and initialize
Perspective's JavaScript with them. However, because they have no hard-coded
dependencies on the .wasm
paths, they are ideal for use with JavaScript
bundlers such as ESBuild, Rollup, Vite or Webpack.
CDN Builds
Perspective's CDN builds are good for non-bundled scenarios, such as importing
directly from a <script>
tag with a browser-side import
. CDN builds do not
require bootstrapping the WebAssembly binaries, but they also generally do
not work with bundlers such as WebPack
.
Inline Builds
Inline builds are deprecated and will be removed in a future release.
Perspective's Inline Builds are a last-ditch effort at compatibility. They 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 when using this inefficient build method.
Prefer your bundler's inlining features and Perspective ESM builds to this one where possible.
Node.js
There is a Node.js build as well for @finos/perspective
data engine, which
shouldn't require any special instructions to use.
What is perspective-python
Perspective for Python uses the exact same C++ data engine used by the WebAssembly version and Rust version. The library consists of many of the same abstractions and API as in JavaScript, as well as Python-specific data loading support for NumPy, Pandas (and Apache Arrow, as in JavaScript).
Additionally, perspective-python
provides a session manager suitable for
integration into server systems such as
Tornado websockets,
AIOHTTP, or
Starlette/FastAPI,
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 and computes a "Group By" operation, returning a new Arrow.
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 are available on GitHub.
What's included
The perspective
module exports several tools:
Server
the constructor for a new isntance of the Perspective data engine.- The
perspective.widget
module exportsPerspectiveWidget
, the JupyterLab widget for interactive visualization in a notebook cell. - The
perspective.handlers
modules exports web frameworks handlers that interface with aperspective-client
in JavaScript.
Virtual UI server
As <perspective-viewer>
or any other Perspective Client
will only consume
the data necessary to render the current screen (or wahtever 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 in either server or
client (via WebAssembly) mode. PerspectiveWidget
is compatible with Jupyterlab
3 and Jupyter Notebook 6 via a
prebuilt extension.
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.
Rust
Install via cargo
:
cargo add perspective
Example
Initialize a server and client
#![allow(unused)] fn main() { let server = Server::default(); let client = server.new_local_client(); }
Load an Arrow
#![allow(unused)] fn main() { let mut file = File::open(std::path::Path::new(ROOT_PATH).join(ARROW_FILE_PATH))?; let mut feather = Vec::with_capacity(file.metadata()?.len() as usize); file.read_to_end(&mut feather)?; let data = UpdateData::Arrow(feather.into()); let mut options = TableInitOptions::default(); options.set_name("my_data_source"); client.table(data.into(), options).await?; }
JavaScript
JavaScript NPM Installation
Perspective releases contain several different builds for use in most environments.
Browser
Perspective's WebAssembly data engine is available via NPM in the same package
as its Node.js counterpart, @finos/perspective
. The Perspective Viewer UI
(which has no Node.js component) must be installed separately:
$ npm add @finos/perspective @finos/perspective-viewer
By itself, @finos/perspective-viewer
does not provide any visualizations, only
the UI framework. Perspective Plugins provide visualizations and must be
installed separately. All Plugins are optional - but a <perspective-viewer>
without Plugins would be rather boring!
$ npm add @finos/perspective-viewer-d3fc @finos/perspective-viewer-datagrid @finos/perspective-viewer-openlayers
Node.js
To use Perspective from a Node.js server, simply install via NPM.
$ npm add @finos/perspective
JavaScript - Importing with or without a bundler
ESM builds with a bundler
ESM builds must be bootstrapped with their .wasm
binaries to initialize. The
wasm
binaries can be found in their respective dist/wasm
directories.
import perspective_viewer from "@finos/perspective-viewer";
import perspective from "@finos/perspective";
// TODO These paths must be provided by the bundler!
const SERVER_WASM = ... // "@finos/perspective/dist/wasm/perspective-server.wasm"
const CLIENT_WASM = ... // "@finos/perspective-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
import SERVER_WASM from "@finos/perspective/dist/wasm/perspective-server.wasm?url";
import CLIENT_WASM from "@finos/perspective-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:
import { defineConfig } from "vite";
export default defineConfig({
build: {
target: "esnext",
},
});
ESBuild
import SERVER_WASM from "@finos/perspective/dist/wasm/perspective-server.wasm";
import CLIENT_WASM from "@finos/perspective-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
:
{
// ...
"loader": {
// ...
".wasm": "file"
}
}
Webpack
import * as SERVER_WASM from "@finos/perspective/dist/wasm/perspective-server.wasm";
import * as CLIENT_WASM from "@finos/perspective-viewer/dist/wasm/perspective-viewer.wasm";
await Promise.all([
perspective.init_server(SERVER_WASM),
perspective_viewer.init_client(CLIENT_WASM),
]);
Webpack config:
{
// ...
experiments: {
asyncWebAssembly: true,
syncWebAssembly: false,
},
}
Inline builds with a bundler
import "@finos/perspective-viewer/dist/esm/perspective-viewer.inline.js";
import psp from "@finos/perspective/dist/esm/perspective.inline.js";
CDN builds
Perspective CDN builds are in ES Module format, thus to include them via a CDN
they must be imported from a <script type="module">
. While this will work fine
downloading Perspective's assets directly as a src
attribute, as you'll
generally want to do something with the library its best to use an import
statement:
<script type="module">
import "https://cdn.jsdelivr.net/npm/@finos/perspective-viewer/dist/cdn/perspective-viewer.js";
import "https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
import "https://cdn.jsdelivr.net/npm/@finos/perspective-viewer-d3fc/dist/cdn/perspective-viewer-d3fc.js";
import perspective from "https://cdn.jsdelivr.net/npm/@finos/perspective/dist/cdn/perspective.js";
// .. Do stuff here ..
</script>
Node.js builds
The Node.js runtime for the @finos/perspective
module runs in-process by
default and does not implement a child_process
interface. Hence, there is no
worker()
method, and the module object itself directly exports the full
perspective
API.
const perspective = require("@finos/perspective");
In Node.js, perspective does not run in a WebWorker (as this API does not exist
in Node.js), so no need to call the .worker()
factory function - the
perspective
library exports the functions directly and run synchronously in
the main process.
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.
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.
const client = perspective.websocket("http://localhost:8080/");
Node.js
The Node.js runtime for the @finos/perspective
module runs in-process by
default and does not implement a child_process
interface, so no need to call
the .worker()
factory function. Instead, the perspective
library exports the
functions directly and run synchronously in the main process.
const client = require("@finos/perspective");
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:
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());
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:
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.
await viewer.delete();
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:
const { WebSocketServer, table } = require("@finos/perspective");
const fs = require("fs");
// Start a WS/HTTP host on port 8080. The `assets` property allows
// the `WebSocketServer()` to also serves the file structure rooted in this
// module's directory.
const host = new WebSocketServer({ assets: [__dirname], port: 8080 });
// Read an arrow file from the file system and host it as a named table.
const arr = fs.readFileSync(__dirname + "/superstore.lz4.arrow");
await table(arr, { name: "table_one" });
... and the [Client
] implementation in the browser:
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");
<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:
<perspective-viewer id="view1"></perspective-viewer>
<script type="module">
const viewer = document.createElement("perspective-viewer");
await viewer.load(table);
</script>
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:
// Themes based on Thought Merchants's Prospective design
import "@finos/perspective-viewer/dist/css/pro.css";
import "@finos/perspective-viewer/dist/css/pro-dark.css";
// Other themes
import "@finos/perspective-viewer/dist/css/solarized.css";
import "@finos/perspective-viewer/dist/css/solarized-dark.css";
import "@finos/perspective-viewer/dist/css/monokai.css";
import "@finos/perspective-viewer/dist/css/vaporwave.css";
Alternatively, you may use themes.css
, which bundles all default themes
import "@finos/perspective-viewer/dist/css/themes.css";
If you choose not to bundle the themes yourself, they are available through CDN. These can be directly linked in your HTML file:
<link
rel="stylesheet"
crossorigin="anonymous"
href="https://cdn.jsdelivr.net/npm/@finos/perspective-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.
// 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:
<perspective-viewer theme="Pro Light"></perspective-viewer>
or
const viewer = document.querySelector("perspective-viewer");
await viewer.restore({ theme: "Pro Dark" });
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.
// 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:
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 data from a virtual Table
Loading a virtual (server-only) [Table
] works just like loading a local/Web
Worker [Table
] - just pass the virtual [Table
] to viewer.load()
. In the
browser:
const elem = document.getElementsByTagName("perspective-viewer")[0];
// Bind to the server's worker instead of instantiating a Web Worker.
const websocket = await perspective.websocket(
window.location.origin.replace("http", "ws")
);
// Bind the viewer to the preloaded data source. `table` and `view` objects
// live on the server.
const server_table = await websocket.open_table("table_one");
await elem.load(server_table);
Alternatively, data can be cloned from a server-side virtual Table
into a
client-side WebAssemblt Table
. The browser clone will be synced via delta
updates transferred via Apache Arrow IPC format, but local View
s created will
be calculated locally on the client browser.
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.
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 aload()
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.
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.
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.
// 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
and embedded
ViewConfig
type declarations, and View
chapter of the documentation which has
several interactive examples for each ViewConfig
property.
// 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:
// Copy to clipboard
copy(await document.querySelector("perspective-viewer").save());
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:
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.
elem.addEventListener("perspective-click", function (event) {
var config = event.detail.config;
elem.restore(config);
});
Python
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 via pip
:
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.
Loading data into a Table
A Table
can be created from a dataset or a schema, the specifics of which are
discussed 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:
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:
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.
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
,
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:
data.set_index("datetime")
table = perspective.table(data, index="index")
Time Zone Handling
When parsing "datetime"
strings, times are assumed local time unless an
explicit timezone offset is parsed. All "datetime"
columns (regardless of
input time zone) are output to the user as datetime.datetime
objects in
local time according to the Python runtime.
This behavior is consistent with Perspective's behavior in JavaScript. For more
details, see this in-depth
explanation of
perspective-python
semantics around time zone handling.
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:
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
:
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.
Multi-threading
Perspective's server API releases the GIL when called (though it may be retained
for some portion of the Client
call to encode RPC messages). It also
dispatches to an internal thread pool for some operations, enabling better
parallelism and overall better server performance. However, Perspective's Python
interface itself will still process queries in a single queue. To enable
parallel query processing, call set_loop_callback
with a multi-threaded
executor such as concurrent.futures.ThreadPoolExecutor
:
def perspective_thread():
server = perspective.Server()
loop = tornado.ioloop.IOLoop()
with concurrent.futures.ThreadPoolExecutor() as executor:
server.set_loop_callback(loop.run_in_executor, executor)
loop.start()
Hosting a WebSocket server
An in-memory Server
"hosts" all perspective.Table
and perspective.View
instances created by its connected Client
s. Hosted tables/views can have their
methods called from other sources than the Python server, i.e. by a
perspective-viewer
running in a JavaScript client over the network,
interfacing with perspective-python
through the websocket API.
The server has full control of all hosted Table
and View
instances, and can
call any public API method on hosted instances. This makes it extremely easy to
stream data to a hosted Table
using .update()
:
server = perspective.Server()
client = server.new_local_client()
table = client.table(data, name="data_source")
for i in range(10):
# updates continue to propagate automatically
table.update(new_data)
The name
provided is important, as it enables Perspective in JavaScript to
look up a Table
and get a handle to it over the network. Otherwise, name
will be assigned randomlu and the Client
must look this up with
CLient.get_hosted_table_names()
Client/Server Replicated Mode
Using Tornado and
PerspectiveTornadoHandler
, as well as
Perspective
's JavaScript library, we can set up "distributed" Perspective
instances that allows multiple browser perspective-viewer
clients to read from
a common perspective-python
server, as in the
Tornado Example Project.
This architecture works by maintaining two Tables
—one on the server, and one
on the client that mirrors the server's Table
automatically using on_update
.
All updates to the table on the server are automatically applied to each client,
which makes this architecture a natural fit for streaming dashboards and other
distributed use-cases. In conjunction with multithreading,
distributed Perspective offers consistently high performance over large numbers
of clients and large datasets.
server.py
from perspective import Server
from perspective.hadnlers.tornado import PerspectiveTornadoHandler
# Create an instance of Server, and host a Table
SERVER = Server()
CLIENT = SERVER.new_local_client()
# The Table is exposed at `localhost:8888/websocket` with the name `data_source`
client.table(data, name = "data_source")
app = tornado.web.Application([
# create a websocket endpoint that the client JavaScript can access
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": SERVER})
])
# Start the Tornado server
app.listen(8888)
loop = tornado.ioloop.IOLoop.current()
loop.start()
Instead of calling load(server_table)
, create a View
using server_table
and pass that into viewer.load()
. This will automatically register an
on_update
callback that synchronizes state between the server and the client.
index.html
<perspective-viewer id="viewer" editable></perspective-viewer>
<script type="module">
// Create a client that expects a Perspective server
// to accept connections at the specified URL.
const websocket = await perspective.websocket(
"ws://localhost:8888/websocket"
);
// Get a handle to the Table on the server
const server_table = await websocket.open_table("data_source_one");
// Create a new view
const server_view = await table.view();
// Create a Table on the client using `perspective.worker()`
const worker = await perspective.worker();
const client_table = await worker.table(view);
// Load the client table in the `<perspective-viewer>`.
document.getElementById("viewer").load(client_table);
</script>
For a more complex example that offers distributed editing of the server dataset, see client_server_editing.html.
We also provide examples for Starlette/FastAPI and AIOHTTP:
Server-only Mode
The server setup is identical to
Client/Server Replicated Mode above, but
instead of creating a View
, the client calls load(server_table)
: In Python,
use Server
and PerspectiveTornadoHandler
to create a websocket server that
exposes a Table
. In this example, table
is a proxy for the Table
we
created on the server. All API methods are available on proxies, the.g.us
calling view()
, schema()
, update()
on table
will pass those operations
to the Python Table
, execute the commands, and return the result back to
Javascript.
<perspective-viewer id="viewer" editable></perspective-viewer>
const websocket = perspective.websocket("ws://localhost:8888/websocket");
const table = websocket.open_table("data_source");
document.getElementById("viewer").load(table);
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
:
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:
from perspective.widget import PerspectiveWidget
PerspectiveWidget(data, group_by=["date"])
.. or a schema:
PerspectiveWidget({"a": int, "b": str})
.. or an instance of a perspective.Table
:
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.
Tutorial: A tornado server in Python
Perspective ships with a pre-built Tornado handler that makes integration with
tornado.websockets
extremely easy. This allows you to run an instance of
Perspective
on a server using Python, open a websocket to a Table
, and
access the Table
in JavaScript and through <perspective-viewer>
. All
instructions sent to the Table
are processed in Python, which executes the
commands, and returns its output through the websocket back to Javascript.
Python setup
Make sure Perspective and Tornado are installed!
pip install perspective-python tornado
To use the handler, we need to first have a Server
, a Client
and an instance
of a Table
:
import perspective
SERVER = perspective.Server()
CLIENT = SERVER.new_local_client()
Once the server has been created, create a Table
instance with a name. The
name that you host the table under is important — it acts as a unique accessor
on the JavaScript side, which will look for a Table hosted at the websocket with
the name you specify.
TABLE = client.table(data, name="data_source_one")
After the server and table setup is complete, create a websocket endpoint and
provide it a reference to PerspectiveTornadoHandler
. You must provide the
configuration object in the route tuple, and it must contain
"perspective_server"
, which is a reference to the Server
you just created.
from perspective.handlers.tornado import PerspectiveTornadoHandler
app = tornado.web.Application([
# ... other handlers ...
# Create a websocket endpoint that the client JavaScript can access
(r"/websocket", PerspectiveTornadoHandler, {"perspective_server": SERVER, "check_origin": True})
])
Optionally, the configuration object can also include check_origin
, a boolean
that determines whether the websocket accepts requests from origins other than
where the server is hosted. See
Tornado docs
for more details.
JavaScript setup
Once the server is up and running, you can access the Table you just hosted
using perspective.websocket
and open_table()
. First, create a client that
expects a Perspective server to accept connections at the specified URL:
import "@finos/perspective-viewer";
import "@finos/perspective-viewer-datagrid";
import perspective from "@finos/perspective";
const websocket = await perspective.websocket("ws://localhost:8888/websocket");
Next open the Table
we created on the server by name:
const table = await websocket.open_table("data_source_one");
table
is a proxy for the Table
we created on the server. All operations that
are possible through the JavaScript API are possible on the Python API as well,
thus calling view()
, schema()
, update()
etc. on const table
will pass
those operations to the Python Table
, execute the commands, and return the
result back to JavaScript. Similarly, providing this table
to a
<perspective-viewer>
instance will allow virtual rendering:
const viewer = document.createElement("perspective-viewer");
viewer.style.height = "500px";
document.body.appendChild(viewer);
await viewer.load(table);
perspective.websocket
expects a Websocket URL where it will send instructions.
When open_table
is called, the name to a hosted Table is passed through, and a
request is sent through the socket to fetch the Table. No actual Table
instance is passed inbetween the runtimes; all instructions are proxied through
websockets.
This provides for great flexibility — while Perspective.js
is full of
features, browser WebAssembly runtimes currently have some performance
restrictions on memory and CPU feature utilization, and the architecture in
general suffers when the dataset itself is too large to download to the client
in full.
The Python runtime does not suffer from memory limitations, utilizes Apache
Arrow internal threadpools for threading and parallel processing, and generates
architecture optimized code, which currently makes it more suitable as a
server-side runtime than node.js
.
API Reference
Perspective's complete API is hosted on docs.rs
:
perspective-client
coversTable
andView
data engine API methods common for Rust, JavaScript and Python.perspective-rs
adds Rust-specific documentation for the Rust crate entrypoint.