chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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 base64
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
|
||||
from string import Template
|
||||
from ipywidgets import DOMWidget
|
||||
from traitlets import Unicode, observe
|
||||
from .viewer import PerspectiveViewer
|
||||
|
||||
__version__ = re.sub(".dev[0-9]+", "", importlib.metadata.version("perspective-python"))
|
||||
|
||||
__all__ = ["PerspectiveWidget"]
|
||||
|
||||
__doc__ = """
|
||||
`PerspectiveWidget` is a JupyterLab widget that implements the same API as
|
||||
`<perspective-viewer>`, allowing for fast, intuitive
|
||||
transformations/visualizations of various data formats within JupyterLab.
|
||||
|
||||
`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.
|
||||
|
||||
## `PerspectiveWidget`
|
||||
|
||||
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)
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class PerspectiveWidget(DOMWidget, PerspectiveViewer):
|
||||
"""`PerspectiveWidget` allows for Perspective to be used as a Jupyter
|
||||
widget.
|
||||
|
||||
Using `perspective.Table`, you can create a widget that extends the full
|
||||
functionality of `perspective-viewer`. Changes on the viewer can be
|
||||
programatically set on the `PerspectiveWidget` instance.
|
||||
|
||||
# Examples
|
||||
|
||||
>>> from perspective.widget import PerspectiveWidget
|
||||
>>> data = {
|
||||
... "a": [1, 2, 3],
|
||||
... "b": [
|
||||
... "2019/07/11 7:30PM",
|
||||
... "2019/07/11 8:30PM",
|
||||
... "2019/07/11 9:30PM"
|
||||
... ]
|
||||
... }
|
||||
>>> widget = PerspectiveWidget(
|
||||
... data,
|
||||
... group_by=["a"],
|
||||
... sort=[["b", "desc"]],
|
||||
... filter=[["a", ">", 1]]
|
||||
... )
|
||||
>>> widget.sort
|
||||
[["b", "desc"]]
|
||||
>>> widget.sort.append(["a", "asc"])
|
||||
>>> widget.sort
|
||||
[["b", "desc"], ["a", "asc"]]
|
||||
>>> widget.table.update({"a": [4, 5]}) # Browser UI updates
|
||||
"""
|
||||
|
||||
# Required by ipywidgets for proper registration of the backend
|
||||
_model_name = Unicode("PerspectiveModel").tag(sync=True)
|
||||
_model_module = Unicode("@perspective-dev/jupyterlab").tag(sync=True)
|
||||
_model_module_version = Unicode("~{}".format(__version__)).tag(sync=True)
|
||||
_view_name = Unicode("PerspectiveView").tag(sync=True)
|
||||
_view_module = Unicode("@perspective-dev/jupyterlab").tag(sync=True)
|
||||
_view_module_version = Unicode("~{}".format(__version__)).tag(sync=True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data,
|
||||
index=None,
|
||||
limit=None,
|
||||
binding_mode="server",
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize an instance of `PerspectiveWidget`
|
||||
with the given table/data and viewer configuration.
|
||||
|
||||
If an `AsyncTable` is passed in, then certain widget methods like
|
||||
`update()` and `delete()` return coroutines which must be awaited.
|
||||
|
||||
# Arguments
|
||||
|
||||
- `data` (`Table`|`AsyncTable`|`dict`|`list`|`pandas.DataFrame`|`bytes`|`str`): a
|
||||
`perspective.Table` instance, a `perspective.AsyncTable` instance, or
|
||||
a dataset to be loaded in the widget.
|
||||
|
||||
# Keyword Arguments
|
||||
|
||||
- `index` (`str`): A column name to be used as the primary key.
|
||||
Ignored if `server` is True.
|
||||
- `binding_mode` (`str`): "client-server" or "server"
|
||||
- `limit` (`int`): A upper limit on the number of rows in the Table.
|
||||
Cannot be set at the same time as `index`, ignored if `server`
|
||||
is True.
|
||||
- `kwargs` (`dict`): configuration options for the `PerspectiveViewer`,
|
||||
and `Table` constructor if `data` is a dataset.
|
||||
|
||||
# Examples
|
||||
|
||||
>>> widget = PerspectiveWidget(
|
||||
... {"a": [1, 2, 3]},
|
||||
... aggregates={"a": "avg"},
|
||||
... group_by=["a"],
|
||||
... sort=[["b", "desc"]],
|
||||
... filter=[["a", ">", 1]],
|
||||
... expressions=["\"a\" + 100"])
|
||||
"""
|
||||
|
||||
self.binding_mode = binding_mode
|
||||
|
||||
# Pass table load options to the front-end, unless in server mode
|
||||
self._options = {}
|
||||
|
||||
if index is not None and limit is not None:
|
||||
raise TypeError("Index and Limit cannot be set at the same time!")
|
||||
|
||||
# Parse the dataset we pass in - if it's Pandas, preserve pivots
|
||||
# if isinstance(data, pandas.DataFrame) or isinstance(data, pandas.Series):
|
||||
# data, config = deconstruct_pandas(data)
|
||||
|
||||
# if config.get("group_by", None) and "group_by" not in kwargs:
|
||||
# kwargs.update({"group_by": config["group_by"]})
|
||||
|
||||
# if config.get("split_by", None) and "split_by" not in kwargs:
|
||||
# kwargs.update({"split_by": config["split_by"]})
|
||||
|
||||
# if config.get("columns", None) and "columns" not in kwargs:
|
||||
# kwargs.update({"columns": config["columns"]})
|
||||
|
||||
# Initialize the viewer
|
||||
super(PerspectiveWidget, self).__init__(**kwargs)
|
||||
|
||||
# Handle messages from the the front end
|
||||
self.on_msg(self.handle_message)
|
||||
self._sessions = {}
|
||||
|
||||
# If an empty dataset is provided, don't call `load()` and wait
|
||||
# for the user to call `load()`.
|
||||
if data is None:
|
||||
if index is not None or limit is not None:
|
||||
raise TypeError(
|
||||
"Cannot initialize PerspectiveWidget `index` or `limit` without a Table, data, or schema!"
|
||||
)
|
||||
else:
|
||||
if index is not None:
|
||||
self._options.update({"index": index})
|
||||
|
||||
if limit is not None:
|
||||
self._options.update({"limit": limit})
|
||||
|
||||
loading = self.load(data, **self._options)
|
||||
if inspect.isawaitable(loading):
|
||||
import asyncio
|
||||
|
||||
asyncio.create_task(loading)
|
||||
|
||||
def load(self, data, **options):
|
||||
"""Load the widget with data."""
|
||||
# Viewer will ignore **options if `data` is a Table or View.
|
||||
return super(PerspectiveWidget, self).load(data, **options)
|
||||
|
||||
def update(self, data):
|
||||
"""Update the widget with new data."""
|
||||
return super(PerspectiveWidget, self).update(data)
|
||||
|
||||
def clear(self):
|
||||
"""Clears the widget's underlying `Table`."""
|
||||
return super(PerspectiveWidget, self).clear()
|
||||
|
||||
def replace(self, data):
|
||||
"""Replaces the widget's `Table` with new data conforming to the same
|
||||
schema. Does not clear user-set state. If in client mode, serializes
|
||||
the data and sends it to the browser.
|
||||
"""
|
||||
return super(PerspectiveWidget, self).replace(data)
|
||||
|
||||
def delete(self, delete_table=True):
|
||||
"""Delete the Widget's data and clears its internal state.
|
||||
|
||||
# Arguments
|
||||
|
||||
- `delete_table` (`bool`): whether the underlying `Table` will be
|
||||
deleted. Defaults to True.
|
||||
"""
|
||||
ret = super(PerspectiveWidget, self).delete(delete_table)
|
||||
|
||||
# Close the underlying comm and remove widget from the front-end
|
||||
self.close()
|
||||
return ret
|
||||
|
||||
@observe("value")
|
||||
def handle_message(self, widget, content, buffers):
|
||||
"""Given a message from `PerspectiveJupyterClient.send`, process the
|
||||
message and return the result to `self.post`.
|
||||
|
||||
# Arguments
|
||||
|
||||
- `widget`: a reference to the `Widget` instance that received the
|
||||
message.
|
||||
- `content` (dict): - the message from the front-end. Automatically
|
||||
de-serialized by ipywidgets.
|
||||
- `buffers`: optional arraybuffers from the front-end, if any.
|
||||
"""
|
||||
if content["type"] == "connect":
|
||||
client_id = content["client_id"]
|
||||
logging.debug("view {} connected", client_id)
|
||||
|
||||
def send_response(msg):
|
||||
self.send({"type": "binary_msg", "client_id": client_id}, [msg])
|
||||
|
||||
self._sessions[client_id] = self.new_proxy_session(send_response)
|
||||
elif content["type"] == "binary_msg":
|
||||
[binary_msg] = buffers
|
||||
client_id = content["client_id"]
|
||||
session = self._sessions[client_id]
|
||||
if session is not None:
|
||||
import asyncio
|
||||
|
||||
asyncio.create_task(session.handle_request_async(binary_msg))
|
||||
else:
|
||||
logging.error("No session for client_id {}".format(client_id))
|
||||
elif content["type"] == "hangup":
|
||||
# XXX(tom): client won't reliably send this so shouldn't rely on it
|
||||
# to clean up; does jupyter notify us when the client on the
|
||||
# websocket, i.e. the view, disconnects?
|
||||
client_id = content["client_id"]
|
||||
logging.debug("view {} hangup", client_id)
|
||||
session = self._sessions.pop(client_id, None)
|
||||
if session:
|
||||
session.close()
|
||||
|
||||
def _repr_mimebundle_(self, **kwargs):
|
||||
super_bundle = super(DOMWidget, self)._repr_mimebundle_(**kwargs)
|
||||
if not _jupyter_html_export_enabled():
|
||||
return super_bundle
|
||||
|
||||
# Serialize viewer attrs + view data to be rendered in the template
|
||||
viewer_attrs = self.save()
|
||||
data = self.table.view().to_arrow()
|
||||
b64_data = base64.encodebytes(data)
|
||||
template_path = os.path.join(
|
||||
os.path.dirname(__file__), "../templates/exported_widget.html.template"
|
||||
)
|
||||
with open(template_path, "r") as template_data:
|
||||
template = Template(template_data.read())
|
||||
|
||||
def psp_cdn(module, path=None):
|
||||
if path is None:
|
||||
path = f"cdn/{module}.js"
|
||||
|
||||
# perspective developer affordance: works with your local `pnpm run start blocks`
|
||||
# return f"http://localhost:8080/node_modules/@perspective-dev/{module}/dist/{path}"
|
||||
return f"https://cdn.jsdelivr.net/npm/@perspective-dev/{module}@{__version__}/dist/{path}"
|
||||
|
||||
return super(DOMWidget, self)._repr_mimebundle_(**kwargs) | {
|
||||
"text/html": template.substitute(
|
||||
psp_cdn_perspective=psp_cdn("perspective"),
|
||||
psp_cdn_perspective_viewer=psp_cdn("perspective-viewer"),
|
||||
psp_cdn_perspective_viewer_datagrid=psp_cdn(
|
||||
"perspective-viewer-datagrid"
|
||||
),
|
||||
psp_cdn_perspective_viewer_charts=psp_cdn("perspective-viewer-charts"),
|
||||
psp_cdn_perspective_viewer_themes=psp_cdn(
|
||||
"perspective-viewer-themes", "css/themes.css"
|
||||
),
|
||||
viewer_id=self.model_id,
|
||||
viewer_attrs=viewer_attrs,
|
||||
b64_data=b64_data.decode("utf-8"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _jupyter_html_export_enabled():
|
||||
return os.environ.get("PSP_JUPYTER_HTML_EXPORT", None) == "1"
|
||||
|
||||
|
||||
def set_jupyter_html_export(val):
|
||||
"""Enables HTML export for Jupyter widgets, when set to True.
|
||||
HTML export can also be enabled by setting the environment variable
|
||||
`PSP_JUPYTER_HTML_EXPORT` to the string `1`.
|
||||
"""
|
||||
os.environ["PSP_JUPYTER_HTML_EXPORT"] = "1" if val else "0"
|
||||
@@ -0,0 +1,15 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
from .viewer import PerspectiveViewer
|
||||
|
||||
__all__ = ["PerspectiveViewer"]
|
||||
@@ -0,0 +1,22 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
|
||||
def validate_version(version):
|
||||
# basic semver of form \d+\.\d+\.\d+(\+.+)?
|
||||
spl = version.split(".", 2)
|
||||
return (
|
||||
len(spl) == 3
|
||||
and spl[0].isdigit()
|
||||
and spl[1].isdigit()
|
||||
and (spl[2].split("+")[0]).isdigit()
|
||||
)
|
||||
@@ -0,0 +1,343 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
from random import random
|
||||
from .viewer_traitlets import PerspectiveTraitlets
|
||||
import perspective
|
||||
|
||||
# from .. import Server
|
||||
|
||||
# ─ │ ┌ ┬ ┐
|
||||
# ┄ ┆ ├ ┼ ┤ ╲ ╱
|
||||
# ┈ ┊ └ ┴ ┘
|
||||
# ━ ┃ ┏ ┳ ┓ ┏ ┯ ┓ ┏ ┳ ┓ ┏ ┯ ┓
|
||||
# ┅ ┇ ┣ ╋ ┫ ┣ ┿ ┫ ┠ ╂ ┨ ┠ ┼ ┨
|
||||
# ┉ ┋ ┗ ┻ ┛ ┗ ┷ ┛ ┗ ┻ ┛ ┗ ┷ ┛
|
||||
|
||||
# global_server = PySyncServer()
|
||||
|
||||
|
||||
class PerspectiveViewer(PerspectiveTraitlets, object):
|
||||
"""PerspectiveViewer wraps the `perspective.Table` API and exposes an API
|
||||
around creating views, loading data, and updating data.
|
||||
"""
|
||||
|
||||
# Viewer attributes that should be saved in `save()` and restored using
|
||||
# `restore()`. Symmetric to `PERSISTENT_ATTRIBUTES` in `perspective-viewer`.
|
||||
PERSISTENT_ATTRIBUTES = (
|
||||
"group_by",
|
||||
"split_by",
|
||||
"filter",
|
||||
"sort",
|
||||
"aggregates",
|
||||
"columns",
|
||||
"expressions",
|
||||
"plugin",
|
||||
"plugin_config",
|
||||
"theme",
|
||||
"settings",
|
||||
"title",
|
||||
"version",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugin="Datagrid",
|
||||
columns=None,
|
||||
group_by=None,
|
||||
split_by=None,
|
||||
aggregates=None,
|
||||
sort=None,
|
||||
filter=None,
|
||||
expressions=None,
|
||||
plugin_config=None,
|
||||
settings=True,
|
||||
theme=None,
|
||||
title=None,
|
||||
# ignored, here for restore compatibility
|
||||
version=None,
|
||||
):
|
||||
"""Initialize an instance of `PerspectiveViewer` with the given viewer
|
||||
configuration. Do not pass a `Table` or data into the constructor -
|
||||
use the :func:`load()` method to provide the viewer with data.
|
||||
|
||||
Keyword Arguments:
|
||||
columns (:obj:`list` of :obj:`str`): A list of column names to be
|
||||
visible to the user.
|
||||
group_by (:obj:`list` of :obj:`str`): A list of column names to
|
||||
use as group by.
|
||||
split_by (:obj:`list` of :obj:`str`): A list of column names
|
||||
to use as split by.
|
||||
aggregates (:obj:`dict` of :obj:`str` to :obj:`str`): A dictionary
|
||||
of column names to aggregate types, which specify aggregates
|
||||
for individual columns.
|
||||
sort (:obj:`list` of :obj:`list` of :obj:`str`): A list of lists,
|
||||
each list containing a column name and a sort direction
|
||||
(``asc``, ``desc``, ``asc abs``, ``desc abs``, ``col asc``,
|
||||
``col desc``, ``col asc abs``, ``col desc abs``).
|
||||
filter (:obj:`list` of :obj:`list` of :obj:`str`): A list of lists,
|
||||
each list containing a column name, a filter comparator, and a
|
||||
value to filter by.
|
||||
expressions (:obj:`list` of :obj:`str`): A list of string
|
||||
expressions which are applied to the view.
|
||||
plugin (:obj:`str`/:obj:`perspective.Plugin`): Which plugin to
|
||||
select by default.
|
||||
plugin_config (:obj:`dict`): A configuration for the plugin, i.e.
|
||||
the datagrid plugin or a chart plugin.
|
||||
settings(:obj:`bool`): Whether the perspective query settings
|
||||
panel should be open.
|
||||
theme (:obj:`str`): The color theme to use.
|
||||
version (:obj:`str`): The version this configuration is restored from.
|
||||
This should only be used when restoring a configuration,
|
||||
and should not be set manually.
|
||||
|
||||
Examples:
|
||||
>>> viewer = PerspectiveViewer(
|
||||
... aggregates={"a": "avg"},
|
||||
... group_by=["a"],
|
||||
... sort=[["b", "desc"]],
|
||||
... filter=[["a", ">", 1]],
|
||||
... expressions=["\"a\" + 100"]
|
||||
... )
|
||||
"""
|
||||
|
||||
# The Table under management by this viewer and its
|
||||
# attached PerspectiveManager
|
||||
self._table = None
|
||||
self._client = None
|
||||
|
||||
# Viewer configuration
|
||||
self.plugin = plugin # validate_plugin(plugin)
|
||||
self.columns = columns or [] # validate_columns(columns) or []
|
||||
self.group_by = group_by or [] # validate_group_by(group_by) or []
|
||||
self.split_by = split_by or [] # validate_split_by(split_by) or []
|
||||
self.aggregates = aggregates or {} # validate_aggregates(aggregates) or {}
|
||||
self.sort = sort or [] # validate_sort(sort) or []
|
||||
self.filter = filter or [] # validate_filter(filter) or []
|
||||
self.expressions = expressions or {} # validate_expressions(expressions) or {}
|
||||
self.plugin_config = (
|
||||
plugin_config or {}
|
||||
) # validate_plugin_config(plugin_config) or {}
|
||||
self.settings = settings
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
|
||||
def new_proxy_session(self, cb):
|
||||
return perspective.ProxySession(self._client, cb)
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""Returns the ``perspective.Client`` or ``perspective.AsyncClient`` under management by the viewer."""
|
||||
return self._client
|
||||
|
||||
@property
|
||||
def table(self):
|
||||
"""Returns the ``perspective.Table`` or ``perspective.AsyncTable`` under management by the viewer."""
|
||||
return self._table
|
||||
|
||||
def is_async(self):
|
||||
"""Returns whether this widget has an async interface or synchronous"""
|
||||
return isinstance(self._table, perspective.perspective.AsyncTable)
|
||||
|
||||
def load(self, data, **options):
|
||||
"""Given a ``perspective.Table``, a ``perspective.AsyncTable``,
|
||||
or data that can be handled by ``perspective.Table``, pass it to the
|
||||
viewer. Like `__init__`, load accepts a `perspective.Table`, a dataset,
|
||||
or a schema.
|
||||
|
||||
``load()`` resets the state of the viewer: if a ``perspective.Table``
|
||||
has already been loaded, ``**options`` is ignored as the options
|
||||
already set on the ``Table`` take precedence.
|
||||
|
||||
If data is passed in, a ``perspective.Table`` is automatically created
|
||||
by this method, and the options passed to ``**config`` are extended to
|
||||
the new Table. If the widget already has a dataset, and the new data
|
||||
has different columns to the old one, then the widget state (pivots,
|
||||
sort, etc.) is cleared to prevent applying settings on columns that
|
||||
don't exist.
|
||||
|
||||
When a ``perspective.AsyncTable`` is loaded, the widget's interface
|
||||
becomes async. Methods which operate on the underlying Perspective
|
||||
view, inclusive of the ``load()`` call itself, return coroutine values
|
||||
which must be awaited.
|
||||
|
||||
Loading a ``perspective.Table`` or plain data will make the interface
|
||||
synchronous again.
|
||||
|
||||
Args:
|
||||
data (:obj:`Table`|:obj:`AsyncTable`|:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`|:obj:`bytes`|:obj:`str`): a
|
||||
`perspective.Table` instance, a `perspective.AsyncTable`
|
||||
instance, or a dataset to be loaded in the viewer.
|
||||
|
||||
Keyword Arguments:
|
||||
name (:obj:`str`): An optional name to reference the table by so it can
|
||||
be accessed from the front-end. If not provided, a name will
|
||||
be generated.
|
||||
index (:obj:`str`): A column name to be used as the primary key.
|
||||
Ignored if a ``Table`` or ``AsyncTable`` is supplied.
|
||||
limit (:obj:`int`): A upper limit on the number of rows in the Table.
|
||||
Cannot be set at the same time as `index`. Ignored if a
|
||||
``Table`` or ``AsyncTable`` is supplied.
|
||||
|
||||
Returns:
|
||||
coro (:obj:`coroutine`): when `AsyncTable` is passed, the `load()` call must be awaited
|
||||
"""
|
||||
name = options.pop("name", str(random()))
|
||||
|
||||
# Reset the viewer when `load()` is called multiple times.
|
||||
if self.table is not None:
|
||||
self.reset()
|
||||
|
||||
if isinstance(data, perspective.perspective.AsyncTable):
|
||||
self._table = data
|
||||
async def load_table():
|
||||
self._client = await self.table.get_client()
|
||||
self.table_name = self.table.get_name()
|
||||
# If the user does not set columns to show, synchronize viewer state
|
||||
# with dataset.
|
||||
if len(self.columns) == 0:
|
||||
self.columns = await self.table.columns()
|
||||
|
||||
return load_table()
|
||||
elif isinstance(data, perspective.perspective.Table):
|
||||
self._table = data
|
||||
self._client = data.get_client()
|
||||
elif isinstance(data, perspective.perspective.View) or isinstance(data, perspective.perspective.AsyncView):
|
||||
raise TypeError(
|
||||
"Views cannot be loaded directly, load a table or raw data instead"
|
||||
)
|
||||
else:
|
||||
self._table = perspective.table(data, name=name, **options)
|
||||
self._client = perspective.GLOBAL_CLIENT
|
||||
|
||||
# If the user does not set columns to show, synchronize viewer state
|
||||
# with dataset.
|
||||
if len(self.columns) == 0:
|
||||
self.columns = self.table.columns()
|
||||
|
||||
self.table_name = self.table.get_name()
|
||||
|
||||
def update(self, data):
|
||||
"""Update the table under management by the viewer with new data.
|
||||
This function follows the semantics of `Table.update()`, and will be
|
||||
affected by whether an index is set on the underlying table.
|
||||
|
||||
When this widget has loaded an ``AsyncTable``, returns a coroutine
|
||||
which must be awaited.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`): the
|
||||
update data for the table.
|
||||
|
||||
Returns:
|
||||
coro (:obj:`coroutine`): when async, must be awaited
|
||||
"""
|
||||
return self.table.update(data)
|
||||
|
||||
def clear(self):
|
||||
"""Clears the rows of this viewer's ``Table``."""
|
||||
if self.table is not None:
|
||||
return self.table.clear()
|
||||
|
||||
def replace(self, data):
|
||||
"""Replaces the rows of this viewer's `Table` with new data.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`|:obj:`list`|:obj:`pandas.DataFrame`): new data
|
||||
to set into the table - must conform to the table's schema.
|
||||
|
||||
Returns:
|
||||
coro (:obj:`coroutine`): when async, must be awaited
|
||||
"""
|
||||
if self.table is not None:
|
||||
return self.table.replace(data)
|
||||
|
||||
def save(self):
|
||||
"""Get the viewer's attributes as a dictionary, symmetric with `restore`
|
||||
so that a viewer's configuration can be reproduced."""
|
||||
return {
|
||||
attr: getattr(self, attr)
|
||||
for attr in PerspectiveViewer.PERSISTENT_ATTRIBUTES
|
||||
}
|
||||
|
||||
def restore(self, **kwargs):
|
||||
"""Restore a given set of attributes, passed as kwargs
|
||||
(e.g. dictionary). Symmetric with `save` so that a given viewer's
|
||||
configuration can be reproduced."""
|
||||
for k, v in kwargs.items():
|
||||
if k in PerspectiveViewer.PERSISTENT_ATTRIBUTES:
|
||||
setattr(self, k, v)
|
||||
|
||||
def to_kwargs(self):
|
||||
"""Get the viewer's attributes as a list of kwargs, which can be passed to
|
||||
the viewer constructor"""
|
||||
attrs = self.save()
|
||||
defaults = {
|
||||
"columns": [],
|
||||
"group_by": [],
|
||||
"split_by": [],
|
||||
"aggregates": {},
|
||||
"sort": [],
|
||||
"filter": [],
|
||||
"expressions": {},
|
||||
"plugin_config": {},
|
||||
"version": "2.10.0",
|
||||
}
|
||||
kwargs = {}
|
||||
for key, default in defaults.items():
|
||||
if attrs.get(key) != default:
|
||||
kwargs[key] = attrs[key]
|
||||
return ", ".join(
|
||||
["{}={}".format(attr, repr(val)) for attr, val in kwargs.items()]
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
"""Resets the viewer's attributes and state, but does not delete or
|
||||
modify the underlying `Table`.
|
||||
|
||||
Example:
|
||||
widget = PerspectiveWidget(data, group_by=["date"], plugin=Plugin.XBAR)
|
||||
widget.reset()
|
||||
widget.plugin #
|
||||
"""
|
||||
self.group_by = []
|
||||
self.split_by = []
|
||||
self.filter = []
|
||||
self.sort = []
|
||||
self.expressions = {}
|
||||
self.aggregates = {}
|
||||
self.columns = []
|
||||
self.plugin = "Datagrid"
|
||||
self.plugin_config = {}
|
||||
|
||||
def delete(self, delete_table=True):
|
||||
"""Delete the Viewer's data and clears its internal state. If
|
||||
``delete_table`` is True, the underlying `perspective.Table` and the
|
||||
internal `View` object will be deleted.
|
||||
|
||||
Args:
|
||||
delete_table (:obj:`bool`) : whether the underlying `Table` will be
|
||||
deleted. Defaults to True.
|
||||
|
||||
Returns:
|
||||
coro (:obj:`coroutine`): when async and `delete_table` is `True`,
|
||||
must be awaited
|
||||
"""
|
||||
ret = None
|
||||
if delete_table:
|
||||
# Delete table
|
||||
ret = self.table.delete()
|
||||
self.table_name = None
|
||||
self._table = None
|
||||
|
||||
self.reset()
|
||||
return ret
|
||||
@@ -0,0 +1,101 @@
|
||||
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||||
# ┃ 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). ┃
|
||||
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
|
||||
from traitlets import HasTraits, Unicode, List, Bool, Dict, validate, Enum
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
__version__ = importlib.metadata.version("perspective-python")
|
||||
|
||||
from .validate import (
|
||||
validate_version,
|
||||
)
|
||||
|
||||
|
||||
class PerspectiveTraitlets(HasTraits):
|
||||
"""Define the traitlet interface with `PerspectiveJupyterWidget` on the
|
||||
front end. Attributes which are set here are synchronized between the
|
||||
front-end and back-end.
|
||||
|
||||
Examples:
|
||||
>>> widget = perspective.PerspectiveWidget(
|
||||
... data, group_by=["a", "b", "c"])
|
||||
PerspectiveWidget(group_by=["a", "b", "c"])
|
||||
>>> widget.split_by=["b"]
|
||||
>>> widget
|
||||
PerspectiveWidget(group_by=["a", "b", "c"], split_by=["b"])
|
||||
"""
|
||||
|
||||
# `perspective-viewer` options
|
||||
plugin = Unicode("Datagrid").tag(sync=True)
|
||||
columns = List(default_value=[]).tag(sync=True)
|
||||
group_by = List(trait=Unicode(), default_value=[]).tag(sync=True, o=True)
|
||||
split_by = List(trait=Unicode(), default_value=[]).tag(sync=True)
|
||||
aggregates = Dict(default_value={}).tag(sync=True)
|
||||
sort = List(default_value=[]).tag(sync=True)
|
||||
filter = List(default_value=[]).tag(sync=True)
|
||||
expressions = Dict(default_value=[]).tag(sync=True)
|
||||
plugin_config = Dict(default_value={}).tag(sync=True)
|
||||
settings = Bool(True).tag(sync=True)
|
||||
theme = Unicode("Pro Light", allow_none=True).tag(sync=True)
|
||||
|
||||
# used to tell the frontend which table to connect to
|
||||
table_name = Unicode(None, allow_none=True).tag(sync=True)
|
||||
|
||||
server = Bool(False).tag(sync=True)
|
||||
binding_mode = Enum(("server", "client-server")).tag(default="server", sync=True)
|
||||
title = Unicode(None, allow_none=True).tag(sync=True)
|
||||
version = Unicode(__version__).tag(sync=True)
|
||||
|
||||
# @validate("plugin")
|
||||
# def _validate_plugin(self, proposal):
|
||||
# return validate_plugin(proposal.value)
|
||||
|
||||
# @validate("columns")
|
||||
# def _validate_columns(self, proposal):
|
||||
# return validate_columns(proposal.value)
|
||||
|
||||
# @validate("group_by")
|
||||
# def _validate_group_by(self, proposal):
|
||||
# return validate_group_by(proposal.value)
|
||||
|
||||
# @validate("split_by")
|
||||
# def _validate_split_by(self, proposal):
|
||||
# return validate_split_by(proposal.value)
|
||||
|
||||
# @validate("aggregates")
|
||||
# def _validate_aggregates(self, proposal):
|
||||
# return validate_aggregates(proposal.value)
|
||||
|
||||
# @validate("sort")
|
||||
# def _validate_sort(self, proposal):
|
||||
# return validate_sort(proposal.value)
|
||||
|
||||
# @validate("filter")
|
||||
# def _validate_filter(self, proposal):
|
||||
# return validate_filter(proposal.value)
|
||||
|
||||
# @validate("expressions")
|
||||
# def _validate_expressions(self, proposal):
|
||||
# return validate_expressions(proposal.value)
|
||||
|
||||
# @validate("plugin_config")
|
||||
# def _validate_plugin_config(self, proposal):
|
||||
# return validate_plugin_config(proposal.value)
|
||||
|
||||
# @validate("title")
|
||||
# def _validate_title(self, proposal):
|
||||
# return validate_title(proposal.value)
|
||||
|
||||
@validate("version")
|
||||
def _validate_version(self, proposal):
|
||||
return validate_version(proposal.value)
|
||||
Reference in New Issue
Block a user