344 lines
14 KiB
Python
344 lines
14 KiB
Python
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||
# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
||
# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
||
# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
||
# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
||
# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
||
# ┃ 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
|