Skip to content

Commit

Permalink
Merge pull request #1387 from vidartf/embed
Browse files Browse the repository at this point in the history
Embed code generation from Python
  • Loading branch information
jasongrout authored Jun 29, 2017
2 parents fcebc51 + ce13271 commit 6a28bdf
Show file tree
Hide file tree
Showing 6 changed files with 470 additions and 9 deletions.
63 changes: 57 additions & 6 deletions docs/source/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,57 @@ The second option, `Download Widget State`, triggers the downloading of a JSON
file containing the serialized state of all the widget models currently in use,
corresponding to the same JSON schema.

## Python interface

Embeddable code for the widgets can also be produced from the Python side.
The following functions are available in the module `ipywidgets.embed`:

- `embed_snippet`:
```py
from ipywidgets.embed import embed_snippet

s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40)
print(embed_snippet(views=[s1, s2]))
```

- `embed_data`:
```py
s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40)
data = embed_data(views=[s1, s2])
print(data['manager_state'])
print(data['view_specs'])
```

- `embed_minimal_html`:
```py
s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40)
embed_minimal_html('my_export.html', views=[s1, s2])
```

Here, `embed_snippet` will return an embeddable HTML snippet similar to the Notebook
interface detailed above, while `embed_data` will return the widget state JSON as
well as the view specs of the given views. `embed_minimal_html` is a utility
function for saving a HTML file with minimal wrapping around the HTML snippet,
allowing for easy validation of the saved state.

In all functions, the state of all widgets known to the widget manager is
included by default. You can alternatively pass a reduced state to use instead.
This can be particularly relevant if you have many independent widgets with a
large state, but only want to include the relevant ones in your export. To
include only the state of the views and their dependencies, use the function
`dependency_state`:

```py
s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40)
print(embed_snippet(
views=[s1, s2],
state=dependency_state([s1, s2]),
))
```




## Embedding Widgets in the Sphinx HTML Documentation

As of ipywidgets 6.0, Jupyter interactive widgets can be rendered and
Expand All @@ -70,18 +121,18 @@ Two directives are provided: `ipywidgets-setup` and `ipywidgets-display`.
`ipywidgets-setup` code is used to run potential boilerplate and configuration
code prior to running the display code. For example:

```rst
.. ipywidgets-setup::
- `ipywidgets-setup`:
```py
from ipywidgets import VBox, jsdlink, IntSlider, Button
```

.. ipywidgets-display::
- `ipywidgets-display`:
```py
s1, s2 = IntSlider(max=200, value=100), IntSlider(value=40)
b = Button(icon='legal')
jsdlink((s1, 'value'), (s2, 'max'))
VBox([s1, s2, b])
```
```

In the case of the `ipywidgets-display` code, the *last statement* of the
code-block should contain the widget object you wish to be rendered.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ipywidgets
==========

Full Table of Contents
--------
----------------------

.. toctree::
:maxdepth: 2
Expand Down
253 changes: 253 additions & 0 deletions ipywidgets/embed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
#
#
# Parts of this code is from IPyVolume (24.05.2017), used here under
# this copyright and license with permission from the author
# (see https://github.com/jupyter-widgets/ipywidgets/pull/1387)

"""
Functions for generating embeddable HTML/javascript of a widget.
"""

import json
from .widgets import Widget, DOMWidget
from .widgets.widget_link import Link


snippet_template = u"""<script src="{embed_url}"></script>
<script type="application/vnd.jupyter.widget-state+json">
{json_data}
</script>
{widget_views}
"""


html_template = u"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{title}</title>
</head>
<body>
{snippet}
</body>
</html>
"""

widget_view_template = u"""<script type="application/vnd.jupyter.widget-view+json">
{view_spec}
</script>"""

# TODO: This always points to the latest version of the html-manager. A better strategy
# would be to point to a version of the html-manager that has been tested with this
# release of jupyter-widgets/controls.
DEFAULT_EMBED_SCRIPT_URL = u'https://unpkg.com/@jupyter-widgets/html-manager@*/dist/index.js'


def _find_widget_refs_by_state(widget, state):
"""Find references to other widgets in a widget's state"""
# Copy keys to allow changes to state during iteration:
keys = tuple(state.keys())
for key in keys:
value = getattr(widget, key)
# Trivial case: Direct references to other widgets:
if isinstance(value, Widget):
yield value
# Also check for buried references in known, JSON-able structures
# Note: This might miss references buried in more esoteric structures
elif isinstance(value, (list, tuple)):
for item in value:
if isinstance(item, Widget):
yield item
elif isinstance(value, dict):
for item in value.values():
if isinstance(item, Widget):
yield item


def _get_recursive_state(widget, store=None, drop_defaults=False):
"""Gets the embed state of a widget, and all other widgets it refers to as well"""
if store is None:
store = dict()
state = widget._get_embed_state(drop_defaults=drop_defaults)
store[widget.model_id] = state

# Loop over all values included in state (i.e. don't consider excluded values):
for ref in _find_widget_refs_by_state(widget, state['state']):
if ref.model_id not in store:
_get_recursive_state(ref, store, drop_defaults=drop_defaults)
return store


def add_resolved_links(store, drop_defaults):
"""Adds the state of any link models between two models in store"""
for widget_id, widget in Widget.widgets.items(): # go over all widgets
if isinstance(widget, Link) and widget_id not in store:
if widget.source[0].model_id in store and widget.target[0].model_id in store:
store[widget.model_id] = widget._get_embed_state(drop_defaults=drop_defaults)


def dependency_state(widgets, drop_defaults=True):
"""Get the state of all widgets specified, and their dependencies.
This uses a simple dependency finder, including:
- any widget directly referenced in the state of an included widget
- any widget in a list/tuple attribute in the state of an included widget
- any widget in a dict attribute in the state of an included widget
- any jslink/jsdlink between two included widgets
What this alogrithm does not do:
- Find widget references in nested list/dict structures
- Find widget references in other types of attributes
Note that this searches the state of the widgets for references, so if
a widget reference is not included in the serialized state, it won't
be considered as a dependency.
"""
# collect the state of all relevant widgets
if widgets is None:
# Get state of all widgets, no smart resolution needed.
state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=None)['state']
else:
try:
widgets[0]
except (IndexError, TypeError):
widgets = [widgets]
state = {}
for widget in widgets:
_get_recursive_state(widget, state, drop_defaults)
# Add any links between included widgets:
add_resolved_links(state, drop_defaults)
return state


def embed_data(views, drop_defaults=True, state=None):
"""Gets data for embedding.
Use this to get the raw data for embedding if you have special
formatting needs.
Parameters
----------
views: widget or collection of widgets or None
The widgets to include views for. If None, all DOMWidgets are
included (not just the displayed ones).
drop_defaults: boolean
Whether to drop default values from the widget states.
state: dict or None (default)
The state to include. When set to None, the state of all widgets
know to the widget manager is included. Otherwise it uses the
passed state directly. This allows for end users to include a
smaller state, under the responsibility that this state is
sufficient to reconstruct the embedded views.
Returns
-------
A dictionary with the following entries:
manager_state: dict of the widget manager state data
view_specs: a list of widget view specs
"""
if views is None:
views = [w for w in Widget.widgets.values() if isinstance(w, DOMWidget)]
else:
try:
views[0]
except (IndexError, TypeError):
views = [views]

if state is None:
# Get state of all known widgets
state = Widget.get_manager_state(drop_defaults=drop_defaults, widgets=None)['state']

# Rely on ipywidget to get the default values
json_data = Widget.get_manager_state(widgets=[])
# but plug in our own state
json_data['state'] = state

view_specs = [w.get_view_spec() for w in views]

return dict(manager_state=json_data, view_specs=view_specs)


def embed_snippet(views,
drop_defaults=True,
state=None,
indent=2,
embed_url=None,
):
"""Return a snippet that can be embedded in an HTML file.
Parameters
----------
views: widget or collection of widgets or None
The widgets to include views for. If None, all DOMWidgets are
included (not just the displayed ones).
drop_defaults: boolean
Whether to drop default values from the widget states.
state: dict or None (default)
The state to include. When set to None, the state of all widgets
know to the widget manager is included. Otherwise it uses the
passed state directly. This allows for end users to include a
smaller state, under the responsibility that this state is
sufficient to reconstruct the embedded views.
indent: integer, string or None
The indent to use for the JSON state dump. See `json.dumps` for
full description.
embed_url: string or None
Allows for overriding the URL used to fetch the widget manager
for the embedded code. This defaults (None) to an `unpkg` CDN url.
Returns
-------
A unicode string with an HTML snippet containing several `<script>` tags.
"""

data = embed_data(views, drop_defaults=drop_defaults, state=state)

widget_views = u'\n'.join(
widget_view_template.format(**dict(view_spec=json.dumps(view_spec)))
for view_spec in data['view_specs']
)

if embed_url is None:
embed_url = DEFAULT_EMBED_SCRIPT_URL

values = {
'embed_url': embed_url,
'json_data': json.dumps(data['manager_state'], indent=indent),
'widget_views': widget_views,
}

return snippet_template.format(**values)


def embed_minimal_html(fp, views, **kwargs):
"""Write a minimal HTML file with widget views embedded.
Parameters
----------
fp: filename or file-like object
The file to write the HTML output to.
views: widget or collection of widgets or None
The widgets to include views for. If None, all DOMWidgets are
included (not just the displayed ones).
Further it accepts keyword args similar to `embed_snippet`.
"""

snippet = embed_snippet(views, **kwargs)

values = {
'title': u'IPyWidget export',
'snippet': snippet,
}

html_code = html_template.format(**values)

# Check if fp is writable:
if hasattr(fp, 'write'):
fp.write(html_code)
else:
# Assume fp is a filename:
with open(fp, "w") as f:
f.write(html_code)
Empty file added ipywidgets/tests/__init__.py
Empty file.
Loading

0 comments on commit 6a28bdf

Please sign in to comment.