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"
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
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
): aperspective.Table
instance, aperspective.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 ifserver
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 asindex
, ignored ifserver
is True.kwargs
(dict
): configuration options for thePerspectiveViewer
, andTable
constructor ifdata
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"])
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.
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.
234 def clear(self): 235 """Clears the widget's underlying `Table`.""" 236 return super(PerspectiveWidget, self).clear()
Clears the widget's underlying Table
.
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.
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 underlyingTable
will be deleted. Defaults to True.
Given a message from PerspectiveJupyterClient.send
, process the
message and return the result to self.post
.
Arguments
widget
: a reference to theWidget
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.