perspective.widget

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. 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:

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)
  1#  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
  2#  ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
  3#  ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
  4#  ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
  5#  ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
  6#  ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
  7#  ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
  8#  ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
  9#  ┃ This file is part of the Perspective library, distributed under the terms ┃
 10#  ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
 11#  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
 12
 13import base64
 14import logging
 15import os
 16import re
 17import importlib.metadata
 18import inspect
 19
 20from string import Template
 21from ipywidgets import DOMWidget
 22from traitlets import Unicode, observe
 23from .viewer import PerspectiveViewer
 24
 25__version__ = re.sub(".dev[0-9]+", "", importlib.metadata.version("perspective-python"))
 26
 27__all__ = ["PerspectiveWidget"]
 28
 29__doc__ = """
 30`PerspectiveWidget` is a JupyterLab widget that implements the same API as
 31`<perspective-viewer>`, allowing for fast, intuitive
 32transformations/visualizations of various data formats within JupyterLab.
 33
 34`PerspectiveWidget` is compatible with Jupyterlab 3 and Jupyter Notebook 6 via a
 35[prebuilt extension](https://jupyterlab.readthedocs.io/en/stable/extension/extension_dev.html#prebuilt-extensions).
 36To use it, simply install `perspective-python` and the extensions should be
 37available.
 38
 39`perspective-python`'s JupyterLab extension also provides convenient builtin
 40viewers for `csv`, `json`, or `arrow` files. Simply right-click on a file with
 41this extension and choose the appropriate `Perpective` option from the context
 42menu.
 43
 44## `PerspectiveWidget`
 45
 46Building on top of the API provided by `perspective.Table`, the
 47`PerspectiveWidget` is a JupyterLab plugin that offers the entire functionality
 48of Perspective within the Jupyter environment. It supports the same API
 49semantics of `<perspective-viewer>`, along with the additional data types
 50supported by `perspective.Table`. `PerspectiveWidget` takes keyword arguments
 51for the managed `View`:
 52
 53```python
 54from perspective.widget import PerspectiveWidget
 55w = perspective.PerspectiveWidget(
 56    data,
 57    plugin="X Bar",
 58    aggregates={"datetime": "any"},
 59    sort=[["date", "desc"]]
 60)
 61```
 62
 63### Creating a widget
 64
 65A widget is created through the `PerspectiveWidget` constructor, which takes as
 66its first, required parameter a `perspective.Table`, a dataset, a schema, or
 67`None`, which serves as a special value that tells the Widget to defer loading
 68any data until later. In maintaining consistency with the Javascript API,
 69Widgets cannot be created with empty dictionaries or lists—`None` should be used
 70if the intention is to await data for loading later on. A widget can be
 71constructed from a dataset:
 72
 73```python
 74from perspective.widget import PerspectiveWidget
 75PerspectiveWidget(data, group_by=["date"])
 76```
 77
 78.. or a schema:
 79
 80```python
 81PerspectiveWidget({"a": int, "b": str})
 82```
 83
 84.. or an instance of a `perspective.Table`:
 85
 86```python
 87table = perspective.table(data)
 88PerspectiveWidget(table)
 89```
 90"""
 91
 92
 93class PerspectiveWidget(DOMWidget, PerspectiveViewer):
 94    """`PerspectiveWidget` allows for Perspective to be used as a Jupyter
 95    widget.
 96
 97    Using `perspective.Table`, you can create a widget that extends the full
 98    functionality of `perspective-viewer`.  Changes on the viewer can be
 99    programatically set on the `PerspectiveWidget` instance.
100
101    # Examples
102
103    >>> from perspective.widget import PerspectiveWidget
104    >>> data = {
105    ...     "a": [1, 2, 3],
106    ...     "b": [
107    ...         "2019/07/11 7:30PM",
108    ...         "2019/07/11 8:30PM",
109    ...         "2019/07/11 9:30PM"
110    ...     ]
111    ... }
112    >>> widget = PerspectiveWidget(
113    ...     data,
114    ...     group_by=["a"],
115    ...     sort=[["b", "desc"]],
116    ...     filter=[["a", ">", 1]]
117    ... )
118    >>> widget.sort
119    [["b", "desc"]]
120    >>> widget.sort.append(["a", "asc"])
121    >>> widget.sort
122    [["b", "desc"], ["a", "asc"]]
123    >>> widget.table.update({"a": [4, 5]}) # Browser UI updates
124    """
125
126    # Required by ipywidgets for proper registration of the backend
127    _model_name = Unicode("PerspectiveModel").tag(sync=True)
128    _model_module = Unicode("@finos/perspective-jupyterlab").tag(sync=True)
129    _model_module_version = Unicode("~{}".format(__version__)).tag(sync=True)
130    _view_name = Unicode("PerspectiveView").tag(sync=True)
131    _view_module = Unicode("@finos/perspective-jupyterlab").tag(sync=True)
132    _view_module_version = Unicode("~{}".format(__version__)).tag(sync=True)
133
134    def __init__(
135        self,
136        data,
137        index=None,
138        limit=None,
139        binding_mode="server",
140        **kwargs,
141    ):
142        """Initialize an instance of `PerspectiveWidget`
143        with the given table/data and viewer configuration.
144
145        If an `AsyncTable` is passed in, then certain widget methods like
146        `update()` and `delete()` return coroutines which must be awaited.
147
148        # Arguments
149
150        -   `data` (`Table`|`AsyncTable`|`dict`|`list`|`pandas.DataFrame`|`bytes`|`str`): a
151            `perspective.Table` instance, a `perspective.AsyncTable` instance, or
152            a dataset to be loaded in the widget.
153
154        # Keyword Arguments
155
156        -   `index` (`str`): A column name to be used as the primary key.
157            Ignored if `server` is True.
158        -   `binding_mode` (`str`): "client-server" or "server"
159        -   `limit` (`int`): A upper limit on the number of rows in the Table.
160            Cannot be set at the same time as `index`, ignored if `server`
161            is True.
162        -   `kwargs` (`dict`): configuration options for the `PerspectiveViewer`,
163            and `Table` constructor if `data` is a dataset.
164
165        # Examples
166
167        >>> widget = PerspectiveWidget(
168        ...     {"a": [1, 2, 3]},
169        ...     aggregates={"a": "avg"},
170        ...     group_by=["a"],
171        ...     sort=[["b", "desc"]],
172        ...     filter=[["a", ">", 1]],
173        ...     expressions=["\"a\" + 100"])
174        """
175
176        self.binding_mode = binding_mode
177
178        # Pass table load options to the front-end, unless in server mode
179        self._options = {}
180
181        if index is not None and limit is not None:
182            raise TypeError("Index and Limit cannot be set at the same time!")
183
184        # Parse the dataset we pass in - if it's Pandas, preserve pivots
185        # if isinstance(data, pandas.DataFrame) or isinstance(data, pandas.Series):
186        #     data, config = deconstruct_pandas(data)
187
188        #     if config.get("group_by", None) and "group_by" not in kwargs:
189        #         kwargs.update({"group_by": config["group_by"]})
190
191        #     if config.get("split_by", None) and "split_by" not in kwargs:
192        #         kwargs.update({"split_by": config["split_by"]})
193
194        #     if config.get("columns", None) and "columns" not in kwargs:
195        #         kwargs.update({"columns": config["columns"]})
196
197        # Initialize the viewer
198        super(PerspectiveWidget, self).__init__(**kwargs)
199
200        # Handle messages from the the front end
201        self.on_msg(self.handle_message)
202        self._sessions = {}
203
204        # If an empty dataset is provided, don't call `load()` and wait
205        # for the user to call `load()`.
206        if data is None:
207            if index is not None or limit is not None:
208                raise TypeError(
209                    "Cannot initialize PerspectiveWidget `index` or `limit` without a Table, data, or schema!"
210                )
211        else:
212            if index is not None:
213                self._options.update({"index": index})
214
215            if limit is not None:
216                self._options.update({"limit": limit})
217
218            loading = self.load(data, **self._options)
219            if inspect.isawaitable(loading):
220                import asyncio
221
222                asyncio.create_task(loading)
223
224    def load(self, data, **options):
225        """Load the widget with data."""
226        # Viewer will ignore **options if `data` is a Table or View.
227        return super(PerspectiveWidget, self).load(data, **options)
228
229    def update(self, data):
230        """Update the widget with new data."""
231        return super(PerspectiveWidget, self).update(data)
232
233    def clear(self):
234        """Clears the widget's underlying `Table`."""
235        return super(PerspectiveWidget, self).clear()
236
237    def replace(self, data):
238        """Replaces the widget's `Table` with new data conforming to the same
239        schema. Does not clear user-set state. If in client mode, serializes
240        the data and sends it to the browser.
241        """
242        return super(PerspectiveWidget, self).replace(data)
243
244    def delete(self, delete_table=True):
245        """Delete the Widget's data and clears its internal state.
246
247        # Arguments
248
249        -   `delete_table` (`bool`): whether the underlying `Table` will be
250            deleted. Defaults to True.
251        """
252        ret = super(PerspectiveWidget, self).delete(delete_table)
253
254        # Close the underlying comm and remove widget from the front-end
255        self.close()
256        return ret
257
258    @observe("value")
259    def handle_message(self, widget, content, buffers):
260        """Given a message from `PerspectiveJupyterClient.send`, process the
261        message and return the result to `self.post`.
262
263        # Arguments
264
265        -   `widget`: a reference to the `Widget` instance that received the
266            message.
267        -   `content` (dict): - the message from the front-end. Automatically
268            de-serialized by ipywidgets.
269        -   `buffers`: optional arraybuffers from the front-end, if any.
270        """
271        if content["type"] == "connect":
272            client_id = content["client_id"]
273            logging.debug("view {} connected", client_id)
274
275            def send_response(msg):
276                self.send({"type": "binary_msg", "client_id": client_id}, [msg])
277
278            self._sessions[client_id] = self.new_proxy_session(send_response)
279        elif content["type"] == "binary_msg":
280            [binary_msg] = buffers
281            client_id = content["client_id"]
282            session = self._sessions[client_id]
283            if session is not None:
284                import asyncio
285
286                asyncio.create_task(session.handle_request_async(binary_msg))
287            else:
288                logging.error("No session for client_id {}".format(client_id))
289        elif content["type"] == "hangup":
290            # XXX(tom): client won't reliably send this so shouldn't rely on it
291            # to clean up; does jupyter notify us when the client on the
292            # websocket, i.e. the view, disconnects?
293            client_id = content["client_id"]
294            logging.debug("view {} hangup", client_id)
295            session = self._sessions.pop(client_id, None)
296            if session:
297                session.close()
298
299    def _repr_mimebundle_(self, **kwargs):
300        super_bundle = super(DOMWidget, self)._repr_mimebundle_(**kwargs)
301        if not _jupyter_html_export_enabled():
302            return super_bundle
303
304        # Serialize viewer attrs + view data to be rendered in the template
305        viewer_attrs = self.save()
306        data = self.table.view().to_arrow()
307        b64_data = base64.encodebytes(data)
308        template_path = os.path.join(
309            os.path.dirname(__file__), "../templates/exported_widget.html.template"
310        )
311        with open(template_path, "r") as template_data:
312            template = Template(template_data.read())
313
314        def psp_cdn(module, path=None):
315            if path is None:
316                path = f"cdn/{module}.js"
317
318            # perspective developer affordance: works with your local `pnpm run start blocks`
319            # return f"http://localhost:8080/node_modules/@finos/{module}/dist/{path}"
320            return f"https://cdn.jsdelivr.net/npm/@finos/{module}@{__version__}/dist/{path}"
321
322        return super(DOMWidget, self)._repr_mimebundle_(**kwargs) | {
323            "text/html": template.substitute(
324                psp_cdn_perspective=psp_cdn("perspective"),
325                psp_cdn_perspective_viewer=psp_cdn("perspective-viewer"),
326                psp_cdn_perspective_viewer_datagrid=psp_cdn(
327                    "perspective-viewer-datagrid"
328                ),
329                psp_cdn_perspective_viewer_d3fc=psp_cdn("perspective-viewer-d3fc"),
330                psp_cdn_perspective_viewer_themes=psp_cdn(
331                    "perspective-viewer-themes", "css/themes.css"
332                ),
333                viewer_id=self.model_id,
334                viewer_attrs=viewer_attrs,
335                b64_data=b64_data.decode("utf-8"),
336            )
337        }
338
339
340def _jupyter_html_export_enabled():
341    return os.environ.get("PSP_JUPYTER_HTML_EXPORT", None) == "1"
342
343
344def set_jupyter_html_export(val):
345    """Enables HTML export for Jupyter widgets, when set to True.
346    HTML export can also be enabled by setting the environment variable
347    `PSP_JUPYTER_HTML_EXPORT` to the string `1`.
348    """
349    os.environ["PSP_JUPYTER_HTML_EXPORT"] = "1" if val else "0"
class PerspectiveWidget(ipywidgets.widgets.domwidget.DOMWidget, perspective.widget.viewer.viewer.PerspectiveViewer):
 94class PerspectiveWidget(DOMWidget, PerspectiveViewer):
 95    """`PerspectiveWidget` allows for Perspective to be used as a Jupyter
 96    widget.
 97
 98    Using `perspective.Table`, you can create a widget that extends the full
 99    functionality of `perspective-viewer`.  Changes on the viewer can be
100    programatically set on the `PerspectiveWidget` instance.
101
102    # Examples
103
104    >>> from perspective.widget import PerspectiveWidget
105    >>> data = {
106    ...     "a": [1, 2, 3],
107    ...     "b": [
108    ...         "2019/07/11 7:30PM",
109    ...         "2019/07/11 8:30PM",
110    ...         "2019/07/11 9:30PM"
111    ...     ]
112    ... }
113    >>> widget = PerspectiveWidget(
114    ...     data,
115    ...     group_by=["a"],
116    ...     sort=[["b", "desc"]],
117    ...     filter=[["a", ">", 1]]
118    ... )
119    >>> widget.sort
120    [["b", "desc"]]
121    >>> widget.sort.append(["a", "asc"])
122    >>> widget.sort
123    [["b", "desc"], ["a", "asc"]]
124    >>> widget.table.update({"a": [4, 5]}) # Browser UI updates
125    """
126
127    # Required by ipywidgets for proper registration of the backend
128    _model_name = Unicode("PerspectiveModel").tag(sync=True)
129    _model_module = Unicode("@finos/perspective-jupyterlab").tag(sync=True)
130    _model_module_version = Unicode("~{}".format(__version__)).tag(sync=True)
131    _view_name = Unicode("PerspectiveView").tag(sync=True)
132    _view_module = Unicode("@finos/perspective-jupyterlab").tag(sync=True)
133    _view_module_version = Unicode("~{}".format(__version__)).tag(sync=True)
134
135    def __init__(
136        self,
137        data,
138        index=None,
139        limit=None,
140        binding_mode="server",
141        **kwargs,
142    ):
143        """Initialize an instance of `PerspectiveWidget`
144        with the given table/data and viewer configuration.
145
146        If an `AsyncTable` is passed in, then certain widget methods like
147        `update()` and `delete()` return coroutines which must be awaited.
148
149        # Arguments
150
151        -   `data` (`Table`|`AsyncTable`|`dict`|`list`|`pandas.DataFrame`|`bytes`|`str`): a
152            `perspective.Table` instance, a `perspective.AsyncTable` instance, or
153            a dataset to be loaded in the widget.
154
155        # Keyword Arguments
156
157        -   `index` (`str`): A column name to be used as the primary key.
158            Ignored if `server` is True.
159        -   `binding_mode` (`str`): "client-server" or "server"
160        -   `limit` (`int`): A upper limit on the number of rows in the Table.
161            Cannot be set at the same time as `index`, ignored if `server`
162            is True.
163        -   `kwargs` (`dict`): configuration options for the `PerspectiveViewer`,
164            and `Table` constructor if `data` is a dataset.
165
166        # Examples
167
168        >>> widget = PerspectiveWidget(
169        ...     {"a": [1, 2, 3]},
170        ...     aggregates={"a": "avg"},
171        ...     group_by=["a"],
172        ...     sort=[["b", "desc"]],
173        ...     filter=[["a", ">", 1]],
174        ...     expressions=["\"a\" + 100"])
175        """
176
177        self.binding_mode = binding_mode
178
179        # Pass table load options to the front-end, unless in server mode
180        self._options = {}
181
182        if index is not None and limit is not None:
183            raise TypeError("Index and Limit cannot be set at the same time!")
184
185        # Parse the dataset we pass in - if it's Pandas, preserve pivots
186        # if isinstance(data, pandas.DataFrame) or isinstance(data, pandas.Series):
187        #     data, config = deconstruct_pandas(data)
188
189        #     if config.get("group_by", None) and "group_by" not in kwargs:
190        #         kwargs.update({"group_by": config["group_by"]})
191
192        #     if config.get("split_by", None) and "split_by" not in kwargs:
193        #         kwargs.update({"split_by": config["split_by"]})
194
195        #     if config.get("columns", None) and "columns" not in kwargs:
196        #         kwargs.update({"columns": config["columns"]})
197
198        # Initialize the viewer
199        super(PerspectiveWidget, self).__init__(**kwargs)
200
201        # Handle messages from the the front end
202        self.on_msg(self.handle_message)
203        self._sessions = {}
204
205        # If an empty dataset is provided, don't call `load()` and wait
206        # for the user to call `load()`.
207        if data is None:
208            if index is not None or limit is not None:
209                raise TypeError(
210                    "Cannot initialize PerspectiveWidget `index` or `limit` without a Table, data, or schema!"
211                )
212        else:
213            if index is not None:
214                self._options.update({"index": index})
215
216            if limit is not None:
217                self._options.update({"limit": limit})
218
219            loading = self.load(data, **self._options)
220            if inspect.isawaitable(loading):
221                import asyncio
222
223                asyncio.create_task(loading)
224
225    def load(self, data, **options):
226        """Load the widget with data."""
227        # Viewer will ignore **options if `data` is a Table or View.
228        return super(PerspectiveWidget, self).load(data, **options)
229
230    def update(self, data):
231        """Update the widget with new data."""
232        return super(PerspectiveWidget, self).update(data)
233
234    def clear(self):
235        """Clears the widget's underlying `Table`."""
236        return super(PerspectiveWidget, self).clear()
237
238    def replace(self, data):
239        """Replaces the widget's `Table` with new data conforming to the same
240        schema. Does not clear user-set state. If in client mode, serializes
241        the data and sends it to the browser.
242        """
243        return super(PerspectiveWidget, self).replace(data)
244
245    def delete(self, delete_table=True):
246        """Delete the Widget's data and clears its internal state.
247
248        # Arguments
249
250        -   `delete_table` (`bool`): whether the underlying `Table` will be
251            deleted. Defaults to True.
252        """
253        ret = super(PerspectiveWidget, self).delete(delete_table)
254
255        # Close the underlying comm and remove widget from the front-end
256        self.close()
257        return ret
258
259    @observe("value")
260    def handle_message(self, widget, content, buffers):
261        """Given a message from `PerspectiveJupyterClient.send`, process the
262        message and return the result to `self.post`.
263
264        # Arguments
265
266        -   `widget`: a reference to the `Widget` instance that received the
267            message.
268        -   `content` (dict): - the message from the front-end. Automatically
269            de-serialized by ipywidgets.
270        -   `buffers`: optional arraybuffers from the front-end, if any.
271        """
272        if content["type"] == "connect":
273            client_id = content["client_id"]
274            logging.debug("view {} connected", client_id)
275
276            def send_response(msg):
277                self.send({"type": "binary_msg", "client_id": client_id}, [msg])
278
279            self._sessions[client_id] = self.new_proxy_session(send_response)
280        elif content["type"] == "binary_msg":
281            [binary_msg] = buffers
282            client_id = content["client_id"]
283            session = self._sessions[client_id]
284            if session is not None:
285                import asyncio
286
287                asyncio.create_task(session.handle_request_async(binary_msg))
288            else:
289                logging.error("No session for client_id {}".format(client_id))
290        elif content["type"] == "hangup":
291            # XXX(tom): client won't reliably send this so shouldn't rely on it
292            # to clean up; does jupyter notify us when the client on the
293            # websocket, i.e. the view, disconnects?
294            client_id = content["client_id"]
295            logging.debug("view {} hangup", client_id)
296            session = self._sessions.pop(client_id, None)
297            if session:
298                session.close()
299
300    def _repr_mimebundle_(self, **kwargs):
301        super_bundle = super(DOMWidget, self)._repr_mimebundle_(**kwargs)
302        if not _jupyter_html_export_enabled():
303            return super_bundle
304
305        # Serialize viewer attrs + view data to be rendered in the template
306        viewer_attrs = self.save()
307        data = self.table.view().to_arrow()
308        b64_data = base64.encodebytes(data)
309        template_path = os.path.join(
310            os.path.dirname(__file__), "../templates/exported_widget.html.template"
311        )
312        with open(template_path, "r") as template_data:
313            template = Template(template_data.read())
314
315        def psp_cdn(module, path=None):
316            if path is None:
317                path = f"cdn/{module}.js"
318
319            # perspective developer affordance: works with your local `pnpm run start blocks`
320            # return f"http://localhost:8080/node_modules/@finos/{module}/dist/{path}"
321            return f"https://cdn.jsdelivr.net/npm/@finos/{module}@{__version__}/dist/{path}"
322
323        return super(DOMWidget, self)._repr_mimebundle_(**kwargs) | {
324            "text/html": template.substitute(
325                psp_cdn_perspective=psp_cdn("perspective"),
326                psp_cdn_perspective_viewer=psp_cdn("perspective-viewer"),
327                psp_cdn_perspective_viewer_datagrid=psp_cdn(
328                    "perspective-viewer-datagrid"
329                ),
330                psp_cdn_perspective_viewer_d3fc=psp_cdn("perspective-viewer-d3fc"),
331                psp_cdn_perspective_viewer_themes=psp_cdn(
332                    "perspective-viewer-themes", "css/themes.css"
333                ),
334                viewer_id=self.model_id,
335                viewer_attrs=viewer_attrs,
336                b64_data=b64_data.decode("utf-8"),
337            )
338        }

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
PerspectiveWidget(data, index=None, limit=None, binding_mode='server', **kwargs)
135    def __init__(
136        self,
137        data,
138        index=None,
139        limit=None,
140        binding_mode="server",
141        **kwargs,
142    ):
143        """Initialize an instance of `PerspectiveWidget`
144        with the given table/data and viewer configuration.
145
146        If an `AsyncTable` is passed in, then certain widget methods like
147        `update()` and `delete()` return coroutines which must be awaited.
148
149        # Arguments
150
151        -   `data` (`Table`|`AsyncTable`|`dict`|`list`|`pandas.DataFrame`|`bytes`|`str`): a
152            `perspective.Table` instance, a `perspective.AsyncTable` instance, or
153            a dataset to be loaded in the widget.
154
155        # Keyword Arguments
156
157        -   `index` (`str`): A column name to be used as the primary key.
158            Ignored if `server` is True.
159        -   `binding_mode` (`str`): "client-server" or "server"
160        -   `limit` (`int`): A upper limit on the number of rows in the Table.
161            Cannot be set at the same time as `index`, ignored if `server`
162            is True.
163        -   `kwargs` (`dict`): configuration options for the `PerspectiveViewer`,
164            and `Table` constructor if `data` is a dataset.
165
166        # Examples
167
168        >>> widget = PerspectiveWidget(
169        ...     {"a": [1, 2, 3]},
170        ...     aggregates={"a": "avg"},
171        ...     group_by=["a"],
172        ...     sort=[["b", "desc"]],
173        ...     filter=[["a", ">", 1]],
174        ...     expressions=["\"a\" + 100"])
175        """
176
177        self.binding_mode = binding_mode
178
179        # Pass table load options to the front-end, unless in server mode
180        self._options = {}
181
182        if index is not None and limit is not None:
183            raise TypeError("Index and Limit cannot be set at the same time!")
184
185        # Parse the dataset we pass in - if it's Pandas, preserve pivots
186        # if isinstance(data, pandas.DataFrame) or isinstance(data, pandas.Series):
187        #     data, config = deconstruct_pandas(data)
188
189        #     if config.get("group_by", None) and "group_by" not in kwargs:
190        #         kwargs.update({"group_by": config["group_by"]})
191
192        #     if config.get("split_by", None) and "split_by" not in kwargs:
193        #         kwargs.update({"split_by": config["split_by"]})
194
195        #     if config.get("columns", None) and "columns" not in kwargs:
196        #         kwargs.update({"columns": config["columns"]})
197
198        # Initialize the viewer
199        super(PerspectiveWidget, self).__init__(**kwargs)
200
201        # Handle messages from the the front end
202        self.on_msg(self.handle_message)
203        self._sessions = {}
204
205        # If an empty dataset is provided, don't call `load()` and wait
206        # for the user to call `load()`.
207        if data is None:
208            if index is not None or limit is not None:
209                raise TypeError(
210                    "Cannot initialize PerspectiveWidget `index` or `limit` without a Table, data, or schema!"
211                )
212        else:
213            if index is not None:
214                self._options.update({"index": index})
215
216            if limit is not None:
217                self._options.update({"limit": limit})
218
219            loading = self.load(data, **self._options)
220            if inspect.isawaitable(loading):
221                import asyncio
222
223                asyncio.create_task(loading)

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"])
binding_mode

An enum whose value must be in a given sequence.

def load(self, data, **options):
225    def load(self, data, **options):
226        """Load the widget with data."""
227        # Viewer will ignore **options if `data` is a Table or View.
228        return super(PerspectiveWidget, self).load(data, **options)

Load the widget with data.

def update(self, data):
230    def update(self, data):
231        """Update the widget with new data."""
232        return super(PerspectiveWidget, self).update(data)

Update the widget with new data.

def clear(self):
234    def clear(self):
235        """Clears the widget's underlying `Table`."""
236        return super(PerspectiveWidget, self).clear()

Clears the widget's underlying Table.

def replace(self, data):
238    def replace(self, data):
239        """Replaces the widget's `Table` with new data conforming to the same
240        schema. Does not clear user-set state. If in client mode, serializes
241        the data and sends it to the browser.
242        """
243        return super(PerspectiveWidget, self).replace(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.

def delete(self, delete_table=True):
245    def delete(self, delete_table=True):
246        """Delete the Widget's data and clears its internal state.
247
248        # Arguments
249
250        -   `delete_table` (`bool`): whether the underlying `Table` will be
251            deleted. Defaults to True.
252        """
253        ret = super(PerspectiveWidget, self).delete(delete_table)
254
255        # Close the underlying comm and remove widget from the front-end
256        self.close()
257        return ret

Delete the Widget's data and clears its internal state.

Arguments

  • delete_table (bool): whether the underlying Table will be deleted. Defaults to True.
def handle_message(unknown):

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.