diff --git a/.travis.yml b/.travis.yml
index dc4c966dad..fd68cdf5eb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -142,7 +142,7 @@ jobs:
- &doc_build
<<: *default
stage: docs_dev
- env: DESC="docs" CHANS_DEV="-c pyviz/label/dev" HV_DOC_GALLERY='false' HV_REQUIREMENTS="doc"
+ env: DESC="docs" CHANS_DEV="-c pyviz/label/dev" HV_DOC_GALLERY='false' HV_DOC_REF_GALLERY='false' HV_REQUIREMENTS="doc" PANEL_EMBED='true' PANEL_EMBED_JSON='true' PANEL_EMBED_JSON_PREFIX='json'
script:
- bokeh sampledata
- conda install -c conda-forge awscli
@@ -171,7 +171,7 @@ jobs:
- <<: *gallery_build
stage: gallery_daily
- env: DESC="gallery" CHANS_DEV="-c pyviz/label/dev" HV_DOC_GALLERY='true' HV_REQUIREMENTS="doc" BUCKET="build."
+ env: DESC="gallery" CHANS_DEV="-c pyviz/label/dev" HV_DOC_GALLERY='false' HV_DOC_REF_GALLERY='false' HV_REQUIREMENTS="doc" BUCKET="build."
- <<: *doc_build
stage: docs
diff --git a/doc/nbpublisher b/doc/nbpublisher
index 90ed382834..0ffe6a0fde 160000
--- a/doc/nbpublisher
+++ b/doc/nbpublisher
@@ -1 +1 @@
-Subproject commit 90ed3828347afd8bb93cd3183733fedb26a214a4
+Subproject commit 0ffe6a0fde289cffe51efa3776565bfd75b5633d
diff --git a/examples/user_guide/17-Dashboards.ipynb b/examples/user_guide/17-Dashboards.ipynb
index f2fdcd7229..5ecadaa492 100644
--- a/examples/user_guide/17-Dashboards.ipynb
+++ b/examples/user_guide/17-Dashboards.ipynb
@@ -155,7 +155,7 @@
"\n",
"smoothed = rolling(stock_dmap, rolling_window=rolling_window.param.value)\n",
"\n",
- "pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, window), smoothed.opts(width=500))"
+ "pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, rolling_window), smoothed.opts(width=500))"
]
},
{
diff --git a/examples/user_guide/Plots_and_Renderers.ipynb b/examples/user_guide/Plots_and_Renderers.ipynb
index 71d7ed608b..280d593def 100644
--- a/examples/user_guide/Plots_and_Renderers.ipynb
+++ b/examples/user_guide/Plots_and_Renderers.ipynb
@@ -374,7 +374,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Rendering plots containing ``HoloMap`` and ``DynamicMap`` objects will automatically generate a widget class instance, which you can get a handle on with the ``get_widget`` method:"
+ "Rendering plots containing ``HoloMap`` and ``DynamicMap`` objects will automatically generate a Panel HoloViews pane which can be rendered in the notebook, saved or rendered as a server app:"
]
},
{
@@ -401,7 +401,7 @@
"metadata": {},
"outputs": [],
"source": [
- "display_html(renderer.static_html(holomap), raw=True)"
+ "html = renderer.static_html(holomap)"
]
},
{
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index 0cece0767b..0027e04785 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -1,23 +1,23 @@
-import os, sys, warnings, operator
+import sys, warnings, operator
+import json
import time
import types
import numbers
import inspect
import itertools
-import string, fnmatch
+import string
import unicodedata
import datetime as dt
-from collections import defaultdict
+
+from distutils.version import LooseVersion as _LooseVersion
from functools import partial
+from collections import defaultdict
from contextlib import contextmanager
-from distutils.version import LooseVersion as _LooseVersion
-
from threading import Thread, Event
+
import numpy as np
import param
-import json
-
try:
from cyordereddict import OrderedDict
except:
@@ -1407,22 +1407,6 @@ def get_spec(obj):
obj.group, obj.label)
-def find_file(folder, filename):
- """
- Find a file given folder and filename. If the filename can be
- resolved directly returns otherwise walks the supplied folder.
- """
- matches = []
- if os.path.isabs(filename) and os.path.isfile(filename):
- return filename
- for root, _, filenames in os.walk(folder):
- for fn in fnmatch.filter(filenames, filename):
- matches.append(os.path.join(root, fn))
- if not matches:
- raise IOError('File %s could not be found' % filename)
- return matches[-1]
-
-
def is_dataframe(data):
"""
Checks whether the supplied data is of DataFrame type.
diff --git a/holoviews/ipython/__init__.py b/holoviews/ipython/__init__.py
index d14a71a1c4..c2e2a877ff 100644
--- a/holoviews/ipython/__init__.py
+++ b/holoviews/ipython/__init__.py
@@ -8,14 +8,13 @@
from IPython.core.completer import IPCompleter
from IPython.display import HTML, publish_display_data
from param import ipython as param_ext
-from pyviz_comms import nb_mime_js
from ..core.dimension import LabelledData
from ..core.tree import AttrTree
from ..core.options import Store
from ..element.comparison import ComparisonTestCase
from ..util import extension
-from ..plotting.renderer import Renderer, MIME_TYPES
+from ..plotting.renderer import Renderer
from .magics import load_magics
from .display_hooks import display # noqa (API import)
from .display_hooks import pprint_display, png_display, svg_display
@@ -172,15 +171,16 @@ def __call__(self, *args, **params):
css += '' % p.width
if p.css:
css += '' % p.css
+
if css:
display(HTML(css))
resources = list(resources)
if len(resources) == 0: return
- Renderer.load_nb()
for r in [r for r in resources if r != 'holoviews']:
Store.renderers[r].load_nb(inline=p.inline)
+ Renderer.load_nb()
if hasattr(ip, 'kernel') and not loaded:
Renderer.comm_manager.get_client_comm(notebook_extension._process_comm_msg,
@@ -191,8 +191,7 @@ def __call__(self, *args, **params):
bokeh_logo= p.logo and ('bokeh' in resources),
mpl_logo= p.logo and (('matplotlib' in resources)
or resources==['holoviews']),
- plotly_logo= p.logo and ('plotly' in resources),
- JS=('holoviews' in resources))
+ plotly_logo= p.logo and ('plotly' in resources))
@classmethod
def completions_sorting_key(cls, word):
@@ -246,35 +245,17 @@ def load_hvjs(cls, logo=False, bokeh_logo=False, mpl_logo=False, plotly_logo=Fal
Displays javascript and CSS to initialize HoloViews widgets.
"""
import jinja2
- # Evaluate load_notebook.html template with widgetjs code
- if JS:
- widgetjs, widgetcss = Renderer.html_assets(extras=False, backends=[], script=True)
- else:
- widgetjs, widgetcss = '', ''
-
- # Add classic notebook MIME renderer
- widgetjs += nb_mime_js
templateLoader = jinja2.FileSystemLoader(os.path.dirname(os.path.abspath(__file__)))
jinjaEnv = jinja2.Environment(loader=templateLoader)
template = jinjaEnv.get_template('load_notebook.html')
- html = template.render({'widgetcss': widgetcss,
- 'logo': logo,
+ html = template.render({'logo': logo,
'bokeh_logo': bokeh_logo,
'mpl_logo': mpl_logo,
'plotly_logo': plotly_logo,
'message': message})
publish_display_data(data={'text/html': html})
- # Vanilla JS mime type is only consumed by classic notebook
- # Custom mime type is only consumed by JupyterLab
- if JS:
- mimebundle = {
- MIME_TYPES['js'] : widgetjs,
- MIME_TYPES['jlab-hv-load'] : widgetjs
- }
- publish_display_data(data=mimebundle)
-
@param.parameterized.bothmethod
def tab_completion_docstring(self_or_cls):
diff --git a/holoviews/ipython/load_notebook.html b/holoviews/ipython/load_notebook.html
index cc5371d18c..72deaecc80 100644
--- a/holoviews/ipython/load_notebook.html
+++ b/holoviews/ipython/load_notebook.html
@@ -1,5 +1,3 @@
-{{ widgetcss }}
-
{% if logo %}
- {plot_script}
-
-"""
default_theme = Theme(json={
'attrs': {
@@ -45,77 +29,36 @@
class BokehRenderer(Renderer):
- theme = param.ClassSelector(default=default_theme, class_=(Theme, str),
- allow_None=True, doc="""
- The applicable Bokeh Theme object (if any).""")
-
backend = param.String(default='bokeh', doc="The backend name.")
+
fig = param.ObjectSelector(default='auto', objects=['html', 'json', 'auto', 'png'], doc="""
Output render format for static figures. If None, no figure
rendering will occur. """)
holomap = param.ObjectSelector(default='auto',
- objects=['widgets', 'scrubber', 'server',
+ objects=['widgets', 'scrubber',
None, 'auto'], doc="""
Output render multi-frame (typically animated) format. If
None, no multi-frame rendering will occur.""")
- mode = param.ObjectSelector(default='default',
- objects=['default', 'server'], doc="""
- Whether to render the object in regular or server mode. In server
- mode a bokeh Document will be returned which can be served as a
- bokeh server app. By default renders all output is rendered to HTML.""")
-
- # Defines the valid output formats for each mode.
- mode_formats = {'fig': {'default': ['html', 'json', 'auto', 'png'],
- 'server': ['html', 'json', 'auto']},
- 'holomap': {'default': ['widgets', 'scrubber', 'auto', None],
- 'server': ['server', 'auto', None]}}
+ theme = param.ClassSelector(default=default_theme, class_=(Theme, str),
+ allow_None=True, doc="""
+ The applicable Bokeh Theme object (if any).""")
webgl = param.Boolean(default=False, doc="""
Whether to render plots with WebGL if available""")
- widgets = {'scrubber': BokehScrubberWidget,
- 'widgets': BokehSelectionWidget,
- 'server': BokehServerWidgets}
-
- backend_dependencies = {'js': CDN.js_files if CDN.js_files else tuple(INLINE.js_raw),
- 'css': CDN.css_files if CDN.css_files else tuple(INLINE.css_raw)}
+ # Defines the valid output formats for each mode.
+ mode_formats = {'fig': ['html', 'auto', 'png'],
+ 'holomap': ['widgets', 'scrubber', 'auto', None]}
_loaded = False
-
- # Define the handler for updating bokeh plots
- comm_msg_handler = bokeh_msg_handler
-
- def __call__(self, obj, fmt=None, doc=None):
- """
- Render the supplied HoloViews component using the appropriate
- backend. The output is not a file format but a suitable,
- in-memory byte stream together with any suitable metadata.
- """
- plot, fmt = self._validate(obj, fmt, doc=doc)
- info = {'file-ext': fmt, 'mime_type': MIME_TYPES[fmt]}
-
- if self.mode == 'server':
- return self.server_doc(plot, doc), info
- elif isinstance(plot, tuple(self.widgets.values())):
- return plot(), info
- elif fmt == 'png':
- png = self._figure_data(plot, fmt=fmt, doc=doc)
- return png, info
- elif fmt == 'html':
- html = self._figure_data(plot, doc=doc)
- html = "
%s
" % html
- return self._apply_post_render_hooks(html, obj, fmt), info
- elif fmt == 'json':
- return self.diff(plot), info
+ _render_with_panel = True
@bothmethod
def _save_prefix(self_or_cls, ext):
"Hook to prefix content for instance JS when saving HTML"
- if ext == 'html':
- return '\n'.join(self_or_cls.html_assets()).encode('utf8')
return
@@ -126,131 +69,14 @@ def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
Allows supplying a document attach the plot to, useful when
combining the bokeh model with another plot.
"""
- if doc is None:
- doc = Document() if self_or_cls.notebook_context else curdoc()
-
- if self_or_cls.notebook_context:
- curdoc().theme = self_or_cls.theme
- doc.theme = self_or_cls.theme
- plot = super(BokehRenderer, self_or_cls).get_plot(obj, renderer, **kwargs)
- plot.document = doc
+ plot = super(BokehRenderer, self_or_cls).get_plot(obj, doc, renderer, **kwargs)
+ if plot.document is None:
+ plot.document = Document() if self_or_cls.notebook_context else curdoc()
+ plot.document.theme = self_or_cls.theme
return plot
- @bothmethod
- def get_widget(self_or_cls, plot, widget_type, doc=None, **kwargs):
- if not isinstance(plot, Plot):
- plot = self_or_cls.get_plot(plot, doc)
- if self_or_cls.mode == 'server':
- return BokehServerWidgets(plot, renderer=self_or_cls.instance(), **kwargs)
- else:
- return super(BokehRenderer, self_or_cls).get_widget(plot, widget_type, **kwargs)
-
-
- @bothmethod
- def app(self_or_cls, plot, show=False, new_window=False, websocket_origin=None, port=0):
- """
- Creates a bokeh app from a HoloViews object or plot. By
- default simply attaches the plot to bokeh's curdoc and returns
- the Document, if show option is supplied creates an
- Application instance and displays it either in a browser
- window or inline if notebook extension has been loaded. Using
- the new_window option the app may be displayed in a new
- browser tab once the notebook extension has been loaded. A
- websocket origin is required when launching from an existing
- tornado server (such as the notebook) and it is not on the
- default port ('localhost:8888').
- """
- if not isinstance(self_or_cls, BokehRenderer) or self_or_cls.mode != 'server':
- renderer = self_or_cls.instance(mode='server')
- else:
- renderer = self_or_cls
-
- def modify_doc(doc):
- renderer(plot, doc=doc)
- handler = FunctionHandler(modify_doc)
- app = Application(handler)
-
- if not show:
- # If not showing and in notebook context return app
- return app
- elif self_or_cls.notebook_context and not new_window:
- # If in notebook, show=True and no new window requested
- # display app inline
- if isinstance(websocket_origin, list):
- if len(websocket_origin) > 1:
- raise ValueError('In the notebook only a single websocket origin '
- 'may be defined, which must match the URL of the '
- 'notebook server.')
- websocket_origin = websocket_origin[0]
- opts = dict(notebook_url=websocket_origin) if websocket_origin else {}
- return bkshow(app, **opts)
-
- # If app shown outside notebook or new_window requested
- # start server and open in new browser tab
- from tornado.ioloop import IOLoop
- loop = IOLoop.current()
- if websocket_origin and not isinstance(websocket_origin, list):
- websocket_origin = [websocket_origin]
- opts = dict(allow_websocket_origin=websocket_origin) if websocket_origin else {}
- opts['io_loop'] = loop
- server = Server({'/': app}, port=port, **opts)
- def show_callback():
- server.show('/')
- server.io_loop.add_callback(show_callback)
- server.start()
-
- def sig_exit(*args, **kwargs):
- loop.add_callback_from_signal(do_stop)
-
- def do_stop(*args, **kwargs):
- loop.stop()
-
- signal.signal(signal.SIGINT, sig_exit)
- try:
- loop.start()
- except RuntimeError:
- pass
- return server
-
-
- @bothmethod
- def server_doc(self_or_cls, obj, doc=None):
- """
- Get a bokeh Document with the plot attached. May supply
- an existing doc, otherwise bokeh.io.curdoc() is used to
- attach the plot to the global document instance.
- """
- if not isinstance(obj, (Plot, BokehServerWidgets)):
- if not isinstance(self_or_cls, BokehRenderer) or self_or_cls.mode != 'server':
- renderer = self_or_cls.instance(mode='server')
- else:
- renderer = self_or_cls
- plot, _ = renderer._validate(obj, 'auto')
- else:
- plot = obj
-
- root = plot.state
- if isinstance(plot, BokehServerWidgets):
- plot = plot.plot
-
- if doc is None:
- doc = plot.document
- else:
- plot.document = doc
-
- plot.traverse(lambda x: attach_periodic(x), [GenericElementPlot])
- doc.add_root(root)
- return doc
-
-
- def components(self, obj, fmt=None, comm=True, **kwargs):
- # Bokeh has to handle comms directly in <0.12.15
- comm = False if bokeh_version < '0.12.15' else comm
- return super(BokehRenderer, self).components(obj,fmt, comm, **kwargs)
-
-
- def _figure_data(self, plot, fmt='html', doc=None, as_script=False, **kwargs):
+ def _figure_data(self, plot, fmt, doc=None, as_script=False, **kwargs):
"""
Given a plot instance, an output format and an optional bokeh
document, return the corresponding data. If as_script is True,
@@ -284,36 +110,14 @@ def _figure_data(self, plot, fmt='html', doc=None, as_script=False, **kwargs):
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
div = tag.format(src=src, mime_type=mime_type, css='')
- js = ''
else:
- try:
- with silence_warnings(EMPTY_LAYOUT, MISSING_RENDERERS):
- js, div, _ = notebook_content(model)
- html = NOTEBOOK_DIV.format(plot_script=js, plot_div=div)
- data = encode_utf8(html)
- doc.hold()
- except:
- logger.disabled = False
- raise
- logger.disabled = False
+ div = render_mimebundle(plot.state, doc, plot.comm)[0]['text/html']
plot.document = doc
if as_script:
- return div, js
- return data
-
-
- def diff(self, plot, binary=True):
- """
- Returns a json diff required to update an existing plot with
- the latest plot data.
- """
- events = list(plot.document._held_events)
- if not events:
- return None
- msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary)
- plot.document._held_events = []
- return msg
+ return div
+ else:
+ return data
@classmethod
@@ -364,11 +168,4 @@ def get_size(self_or_cls, plot):
@classmethod
def load_nb(cls, inline=True):
- """
- Loads the bokeh notebook resources.
- """
- LOAD_MIME_TYPE = bokeh.io.notebook.LOAD_MIME_TYPE
- bokeh.io.notebook.LOAD_MIME_TYPE = MIME_TYPES['jlab-hv-load']
- load_notebook(hide_banner=True, resources=INLINE if inline else CDN)
- bokeh.io.notebook.LOAD_MIME_TYPE = LOAD_MIME_TYPE
- bokeh.io.notebook.curstate().output_notebook()
+ cls._loaded = True
diff --git a/holoviews/plotting/bokeh/widgets.py b/holoviews/plotting/bokeh/widgets.py
deleted file mode 100644
index 283da4adc5..0000000000
--- a/holoviews/plotting/bokeh/widgets.py
+++ /dev/null
@@ -1,324 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-import math
-import json
-from functools import partial
-
-import param
-import numpy as np
-from bokeh.models.widgets import Select, Slider, AutocompleteInput, TextInput, Div
-from bokeh.layouts import widgetbox, row, column
-
-from ...core import Store, NdMapping, OrderedDict
-from ...core.util import (drop_streams, unique_array, isnumeric,
- wrap_tuple_streams, unicode)
-from ..renderer import MIME_TYPES
-from ..widgets import NdWidget, SelectionWidget, ScrubberWidget
-from .util import serialize_json
-
-
-
-class BokehServerWidgets(param.Parameterized):
- """
- BokehServerWidgets create bokeh widgets corresponding to all the
- key dimensions found on a BokehPlot instance. It currently supports
- two types of widgets: sliders (which may be discrete or continuous),
- and dropdown widgets letting you select non-numeric values.
- """
-
- display_options = param.Dict(default={}, doc="""
- Additional options controlling display options of the widgets.""")
-
- editable = param.Boolean(default=False, doc="""
- Whether the slider text fields should be editable. Disabled
- by default for a more compact widget layout.""")
-
- position = param.ObjectSelector(default='right',
- objects=['right', 'left', 'above', 'below'])
-
- sizing_mode = param.ObjectSelector(default='fixed',
- objects=['fixed', 'stretch_both', 'scale_width',
- 'scale_height', 'scale_both'])
-
- width = param.Integer(default=250, doc="""
- Width of the widget box in pixels""")
-
- basejs = param.String(default=None, precedence=-1, doc="""
- Defines the local CSS file to be loaded for this widget.""")
-
- extensionjs = param.String(default=None, precedence=-1, doc="""
- Optional javascript extension file for a particular backend.""")
-
- css = param.String(default=None, precedence=-1, doc="""
- Defines the local CSS file to be loaded for this widget.""")
-
- def __init__(self, plot, renderer=None, **params):
- super(BokehServerWidgets, self).__init__(**params)
- self.plot = plot
- streams = []
- for stream in plot.streams:
- if any(k in plot.dimensions for k in stream.contents):
- streams.append(stream)
- self.dimensions, self.keys = drop_streams(streams,
- plot.dimensions,
- plot.keys)
- if renderer is None:
- backend = Store.current_backend
- self.renderer = Store.renderers[backend]
- else:
- self.renderer = renderer
- # Create mock NdMapping to hold the common dimensions and keys
- self.mock_obj = NdMapping([(k, None) for k in self.keys],
- kdims=list(self.dimensions))
- self.widgets, self.lookups = self.get_widgets()
- self.subplots = {}
- if self.plot.renderer.mode == 'default':
- self.attach_callbacks()
- self.state = self.init_layout()
- self._queue = []
- self._active = False
-
- if hasattr(self.plot.document, 'on_session_destroyed'):
- self.plot.document.on_session_destroyed(self.plot._session_destroy)
-
-
- @classmethod
- def create_widget(self, dim, holomap=None, editable=False):
- """"
- Given a Dimension creates bokeh widgets to select along that
- dimension. For numeric data a slider widget is created which
- may be either discrete, if a holomap is supplied or the
- Dimension.values are set, or a continuous widget for
- DynamicMaps. If the slider is discrete the returned mapping
- defines a mapping between values and labels making it possible
- sync the two slider and label widgets. For non-numeric data
- a simple dropdown selection widget is generated.
- """
- label, mapping = None, None
- if holomap is None:
- if dim.values:
- if dim.default is None:
- default = dim.values[0]
- elif dim.default not in dim.values:
- raise ValueError("%s dimension default %r is not in dimension values: %s"
- % (dim, dim.default, dim.values))
- else:
- default = dim.default
- value = dim.values.index(default)
-
- if all(isnumeric(v) for v in dim.values):
- values = sorted(dim.values)
- labels = [unicode(dim.pprint_value(v)) for v in values]
- if editable:
- label = AutocompleteInput(value=labels[value], completions=labels,
- title=dim.pprint_label)
- else:
- label = Div(text='%s' % dim.pprint_value_string(labels[value]))
- widget = Slider(value=value, start=0, end=len(dim.values)-1, title=None, step=1)
- mapping = list(enumerate(zip(values, labels)))
- else:
- values = [(v, dim.pprint_value(v)) for v in dim.values]
- widget = Select(title=dim.pprint_label, value=values[value][0],
- options=values)
- else:
- start = dim.soft_range[0] if dim.soft_range[0] else dim.range[0]
- end = dim.soft_range[1] if dim.soft_range[1] else dim.range[1]
- dim_range = end - start
- int_type = isinstance(dim.type, type) and issubclass(dim.type, int)
- if dim.step is not None:
- step = dim.step
- elif isinstance(dim_range, int) or int_type:
- step = 1
- else:
- step = 10**((round(math.log10(dim_range))-3))
-
- if dim.default is None:
- default = start
- elif (dim.default < start or dim.default > end):
- raise ValueError("%s dimension default %r is not in the provided range: %s"
- % (dim, dim.default, (start, end)))
- else:
- default = dim.default
-
- if editable:
- label = TextInput(value=str(default), title=dim.pprint_label)
- else:
- label = Div(text='%s' % dim.pprint_value_string(default))
- widget = Slider(value=default, start=start,
- end=end, step=step, title=None)
- else:
- values = (dim.values if dim.values else
- list(unique_array(holomap.dimension_values(dim.name))))
- if dim.default is None:
- default = values[0]
- elif dim.default not in values:
- raise ValueError("%s dimension default %r is not in dimension values: %s"
- % (dim, dim.default, values))
- else:
- default = dim.default
- if isinstance(values[0], np.datetime64) or isnumeric(values[0]):
- values = sorted(values)
- labels = [dim.pprint_value(v) for v in values]
- value = values.index(default)
- if editable:
- label = AutocompleteInput(value=labels[value], completions=labels,
- title=dim.pprint_label)
- else:
- label = Div(text='%s' % (dim.pprint_value_string(labels[value])))
- widget = Slider(value=value, start=0, end=len(values)-1, title=None, step=1)
- else:
- labels = [dim.pprint_value(v) for v in values]
- widget = Select(title=dim.pprint_label, value=default,
- options=list(zip(values, labels)))
- mapping = list(enumerate(zip(values, labels)))
- return widget, label, mapping
-
-
- def get_widgets(self):
- """
- Creates a set of widgets representing the dimensions on the
- plot object used to instantiate the widgets class.
- """
- widgets = OrderedDict()
- mappings = {}
- for dim in self.mock_obj.kdims:
- holomap = None if self.plot.dynamic else self.mock_obj
- widget, label, mapping = self.create_widget(dim, holomap, self.editable)
- if label is not None and not isinstance(label, Div):
- label.on_change('value', partial(self.on_change, dim, 'label'))
- widget.on_change('value', partial(self.on_change, dim, 'widget'))
- widgets[dim.pprint_label] = (label, widget)
- if mapping:
- mappings[dim.pprint_label] = OrderedDict(mapping)
- return widgets, mappings
-
-
- def init_layout(self):
- widgets = [widget for d in self.widgets.values()
- for widget in d if widget]
- wbox = widgetbox(widgets, width=self.width)
- if self.position in ['right', 'below']:
- plots = [self.plot.state, wbox]
- else:
- plots = [wbox, self.plot.state]
- layout_fn = row if self.position in ['left', 'right'] else column
- layout = layout_fn(plots, sizing_mode=self.sizing_mode)
- return layout
-
-
- def attach_callbacks(self):
- """
- Attach callbacks to interact with Comms.
- """
- pass
-
-
- def on_change(self, dim, widget_type, attr, old, new):
- self._queue.append((dim, widget_type, attr, old, new))
- if not self._active:
- self.plot.document.add_timeout_callback(self.update, 50)
- self._active = True
-
-
- def update(self):
- """
- Handle update events on bokeh server.
- """
- if not self._queue:
- return
-
- dim, widget_type, attr, old, new = self._queue[-1]
- self._queue = []
- dim_label = dim.pprint_label
-
- label, widget = self.widgets[dim_label]
- if widget_type == 'label':
- if isinstance(label, AutocompleteInput):
- value = [new]
- widget.value = value
- else:
- widget.value = float(new)
- elif label:
- lookups = self.lookups.get(dim_label)
- if not self.editable:
- if lookups:
- new = lookups[widget.value][1]
- label.text = '%s' % dim.pprint_value_string(new)
- elif isinstance(label, AutocompleteInput):
- text = lookups[new][1]
- label.value = text
- else:
- label.value = dim.pprint_value(new)
-
- key = []
- for dim, (label, widget) in self.widgets.items():
- lookups = self.lookups.get(dim)
- if label and lookups:
- val = lookups[widget.value][0]
- else:
- val = widget.value
- key.append(val)
- key = wrap_tuple_streams(tuple(key), self.plot.dimensions,
- self.plot.streams)
- self.plot.update(key)
- self._active = False
-
-
-
-class BokehWidget(NdWidget):
-
- css = param.String(default='bokehwidgets.css', doc="""
- Defines the local CSS file to be loaded for this widget.""")
-
- extensionjs = param.String(default='bokehwidgets.js', doc="""
- Optional javascript extension file for a particular backend.""")
-
- def _get_data(self):
- msg, metadata = self.renderer.components(self.plot, comm=False)
- data = super(BokehWidget, self)._get_data()
- return dict(data, init_html=msg['text/html'],
- init_js=msg[MIME_TYPES['js']],
- plot_id=self.plot.state._id)
-
- def encode_frames(self, frames):
- if self.export_json:
- self.save_json(frames)
- frames = {}
- else:
- frames = json.dumps(frames).replace('', r'<\/')
- return frames
-
- def get_frames(self):
- nframes = len(self.plot)
- if self.embed:
- self.plot.update(nframes-1)
- frames = OrderedDict([(idx, self._plot_figure(idx))
- for idx in range(nframes)])
- else:
- frames = {}
- return self.encode_frames(frames)
-
- def _plot_figure(self, idx, fig_format='json'):
- """
- Returns the figure in html format on the
- first call and
- """
- self.plot.update(idx)
- if self.embed:
- patch = self.renderer.diff(self.plot, binary=False)
- msg = serialize_json(dict(content=patch.content,
- root=self.plot.state._id))
- return msg
-
-
-class BokehSelectionWidget(BokehWidget, SelectionWidget):
-
- def _get_data(self):
- if not self.plot.dynamic:
- widgets, _, _ = self.get_widgets()
- key = tuple(w['value'] for w in widgets)
- self.plot.update(key)
- return super(BokehSelectionWidget, self)._get_data()
-
-class BokehScrubberWidget(BokehWidget, ScrubberWidget):
- pass
diff --git a/holoviews/plotting/mpl/mplwidgets.js b/holoviews/plotting/mpl/mplwidgets.js
deleted file mode 100644
index 21a3486daf..0000000000
--- a/holoviews/plotting/mpl/mplwidgets.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Define MPL specific subclasses
-function MPLSelectionWidget() {
- SelectionWidget.apply(this, arguments);
-}
-
-function MPLScrubberWidget() {
- ScrubberWidget.apply(this, arguments);
-}
-
-// Let them inherit from the baseclasses
-MPLSelectionWidget.prototype = Object.create(SelectionWidget.prototype);
-MPLScrubberWidget.prototype = Object.create(ScrubberWidget.prototype);
-
-// Define methods to override on widgets
-var MPLMethods = {
- init_slider : function(init_val){
- if(this.load_json) {
- this.from_json()
- } else {
- this.update_cache();
- }
- if (this.dynamic | !this.cached | (this.current_vals === undefined)) {
- this.update(0)
- } else {
- this.set_frame(this.current_vals[0], 0)
- }
- },
- process_msg : function(msg) {
- var data = msg.content.data;
- this.frames[this.current] = data;
- this.update_cache(true);
- this.update(this.current);
- }
-}
-// Extend MPL widgets with backend specific methods
-extend(MPLSelectionWidget.prototype, MPLMethods);
-extend(MPLScrubberWidget.prototype, MPLMethods);
-
-window.HoloViews.MPLSelectionWidget = MPLSelectionWidget
-window.HoloViews.MPLScrubberWidget = MPLScrubberWidget
diff --git a/holoviews/plotting/mpl/plot.py b/holoviews/plotting/mpl/plot.py
index 442075e06b..f57d71667d 100644
--- a/holoviews/plotting/mpl/plot.py
+++ b/holoviews/plotting/mpl/plot.py
@@ -17,7 +17,7 @@
from ...core.options import Store, SkipRendering
from ...core.util import int_to_roman, int_to_alpha, basestring, wrap_tuple_streams
from ..plot import (DimensionedPlot, GenericLayoutPlot, GenericCompositePlot,
- GenericElementPlot)
+ GenericElementPlot, GenericAdjointLayoutPlot)
from ..util import attach_streams, collate, displayable
from .util import compute_ratios, fix_aspect, mpl_version
@@ -613,14 +613,9 @@ def _adjust_subplots(self, axis, subaxes):
-class AdjointLayoutPlot(MPLPlot):
+class AdjointLayoutPlot(MPLPlot, GenericAdjointLayoutPlot):
"""
- LayoutPlot allows placing up to three Views in a number of
- predefined and fixed layouts, which are defined by the layout_dict
- class attribute. This allows placing subviews next to a main plot
- in either a 'top' or 'right' position.
-
- Initially, a LayoutPlot computes an appropriate layout based for
+ Initially, a AdjointLayoutPlot computes an appropriate layout based for
the number of Views in the AdjointLayout object it has been given, but
when embedded in a NdLayout, it can recompute the layout to
match the number of rows and columns as part of a larger grid.
diff --git a/holoviews/plotting/mpl/renderer.py b/holoviews/plotting/mpl/renderer.py
index 10e864564b..b74228176a 100644
--- a/holoviews/plotting/mpl/renderer.py
+++ b/holoviews/plotting/mpl/renderer.py
@@ -9,6 +9,7 @@
from itertools import chain
import param
+
import matplotlib as mpl
from matplotlib import pyplot as plt
@@ -17,7 +18,6 @@
from ...core import HoloMap
from ...core.options import Store
from ..renderer import Renderer, MIME_TYPES, HTML_TAGS
-from .widgets import MPLSelectionWidget, MPLScrubberWidget
from .util import get_tight_bbox, mpl_version
class OutputWarning(param.Parameterized):pass
@@ -82,38 +82,12 @@ class MPLRenderer(Renderer):
mode = param.ObjectSelector(default='default', objects=['default'])
- mode_formats = {'fig': {'default': ['png', 'svg', 'pdf', 'html', None, 'auto']},
- 'holomap': {'default': ['widgets', 'scrubber', 'webm','mp4', 'gif',
- 'html', None, 'auto']}}
+ mode_formats = {'fig': ['png', 'svg', 'pdf', 'html', None, 'auto'],
+ 'holomap': ['widgets', 'scrubber', 'webm','mp4', 'gif',
+ 'html', None, 'auto']}
counter = 0
- # Define appropriate widget classes
- widgets = {'scrubber': MPLScrubberWidget,
- 'widgets': MPLSelectionWidget}
-
- # Define the handler for updating matplotlib plots
- comm_msg_handler = mpl_msg_handler
-
- def __call__(self, obj, fmt='auto'):
- """
- Render the supplied HoloViews component or MPLPlot instance
- using matplotlib.
- """
- plot, fmt = self._validate(obj, fmt)
- if plot is None: return
-
- if isinstance(plot, tuple(self.widgets.values())):
- data = plot()
- else:
- with mpl.rc_context(rc=plot.fig_rcparams):
- data = self._figure_data(plot, fmt, **({'dpi':self.dpi} if self.dpi else {}))
-
- data = self._apply_post_render_hooks(data, obj, fmt)
- return data, {'file-ext':fmt,
- 'mime_type':MIME_TYPES[fmt]}
-
-
def show(self, obj):
"""
Renders the supplied object and displays it using the active
@@ -170,18 +144,7 @@ def get_size(self_or_cls, plot):
return (int(w*dpi), int(h*dpi))
- def diff(self, plot):
- """
- Returns the latest plot data to update an existing plot.
- """
- if self.fig == 'auto':
- figure_format = self.params('fig').objects[0]
- else:
- figure_format = self.fig
- return self.html(plot, figure_format)
-
-
- def _figure_data(self, plot, fmt='png', bbox_inches='tight', as_script=False, **kwargs):
+ def _figure_data(self, plot, fmt, bbox_inches='tight', as_script=False, **kwargs):
"""
Render matplotlib figure object and return the corresponding
data. If as_script is True, the content will be split in an
@@ -227,7 +190,7 @@ def _figure_data(self, plot, fmt='png', bbox_inches='tight', as_script=False, **
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
html = tag.format(src=src, mime_type=mime_type, css='')
- return html, ''
+ return html
if fmt == 'svg':
data = data.decode('utf-8')
return data
diff --git a/holoviews/plotting/mpl/widgets.py b/holoviews/plotting/mpl/widgets.py
deleted file mode 100644
index 97a27faf59..0000000000
--- a/holoviews/plotting/mpl/widgets.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-import json
-import param
-
-from ..widgets import NdWidget, SelectionWidget, ScrubberWidget
-
-
-class MPLWidget(NdWidget):
-
- extensionjs = param.String(default='mplwidgets.js', doc="""
- Optional javascript extension file for a particular backend.""")
-
-
- def get_frames(self):
- if self.embed:
- return super(MPLWidget, self).get_frames()
- else:
- frames = {0: self._plot_figure(self.init_key)}
- return self.encode_frames(frames)
-
-
- def _plot_figure(self, idx):
- with self.renderer.state():
- self.plot.update(idx)
- if self.renderer.fig == 'auto':
- figure_format = self.renderer.params('fig').objects[0]
- else:
- figure_format = self.renderer.fig
- return self.renderer._figure_data(self.plot, figure_format, as_script=True)[0]
-
-
- def encode_frames(self, frames):
- if self.export_json:
- self.save_json(frames)
- return {}
- elif not isinstance(frames, dict):
- pass
- else:
- frames = dict(frames)
- return json.dumps(frames)
-
-
-
-class MPLSelectionWidget(MPLWidget, SelectionWidget):
- pass
-
-class MPLScrubberWidget(MPLWidget, ScrubberWidget):
- pass
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index 5077058c1f..c248129771 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -3,14 +3,20 @@
plotting package or backend. Every plotting classes must be a subclass
of this Plot baseclass.
"""
+from __future__ import absolute_import
+import threading
import warnings
+
from itertools import groupby, product
from collections import Counter, defaultdict
import numpy as np
import param
+from panel.io.notebook import push
+from panel.io.state import state
+
from ..core import OrderedDict
from ..core import util, traversal
from ..core.element import Element, Element3D
@@ -42,6 +48,46 @@ class Plot(param.Parameterized):
# Use this list to disable any invalid style options
_disabled_opts = []
+ @property
+ def state(self):
+ """
+ The plotting state that gets updated via the update method and
+ used by the renderer to generate output.
+ """
+ raise NotImplementedError
+
+ @property
+ def root(self):
+ if self._root:
+ return self._root
+ elif 'plot' in self.handles and self.top_level:
+ return self.state
+ else:
+ return None
+
+ @property
+ def document(self):
+ return self._document
+
+ @document.setter
+ def document(self, doc):
+ if (doc and hasattr(doc, 'on_session_destroyed') and
+ self.root is self.handles.get('plot') and
+ not isinstance(self, GenericAdjointLayoutPlot)):
+ doc.on_session_destroyed(self._session_destroy)
+ if self._document:
+ if isinstance(self._document._session_destroyed_callbacks, set):
+ self._document._session_destroyed_callbacks.discard(self._session_destroy)
+ else:
+ self._document._session_destroyed_callbacks.pop(self._session_destroy, None)
+
+ self._document = doc
+ if self.subplots:
+ for plot in self.subplots.values():
+ if plot is not None:
+ plot.document = doc
+
+
def initialize_plot(self, ranges=None):
"""
Initialize the matplotlib figure.
@@ -57,14 +103,6 @@ def update(self, key):
"""
return self.state
- @property
- def state(self):
- """
- The plotting state that gets updated via the update method and
- used by the renderer to generate output.
- """
- raise NotImplementedError
-
def cleanup(self):
"""
@@ -83,6 +121,75 @@ def cleanup(self):
self.comm.close()
+ def _session_destroy(self, session_context):
+ self.cleanup()
+
+
+ def refresh(self, **kwargs):
+ """
+ Refreshes the plot by rerendering it and then pushing
+ the updated data if the plot has an associated Comm.
+ """
+ if self.renderer.mode == 'server':
+ from bokeh.io import curdoc
+ thread = threading.current_thread()
+ thread_id = thread.ident if thread else None
+ if (curdoc() is not self.document or (state._thread_id is not None and
+ thread_id != state._thread_id)):
+ # If we do not have the Document lock, schedule refresh as callback
+ self.document.add_next_tick_callback(self.refresh)
+ return
+
+ traverse_setter(self, '_force', True)
+ key = self.current_key if self.current_key else self.keys[0]
+ dim_streams = [stream for stream in self.streams
+ if any(c in self.dimensions for c in stream.contents)]
+ stream_params = stream_parameters(dim_streams)
+ key = tuple(None if d in stream_params else k
+ for d, k in zip(self.dimensions, key))
+ stream_key = util.wrap_tuple_streams(key, self.dimensions, self.streams)
+
+ self._trigger_refresh(stream_key)
+ if self.comm is not None and self.top_level:
+ self.push()
+
+
+ def _trigger_refresh(self, key):
+ "Triggers update to a plot on a refresh event"
+ # Update if not top-level, batched or an ElementPlot
+ if not self.top_level or isinstance(self, GenericElementPlot):
+ self.update(key)
+
+
+ def push(self):
+ """
+ Pushes plot updates to the frontend.
+ """
+ root = self._root
+ if (root and self._pane is not None and
+ root.ref['id'] in self._pane._plots):
+ child_pane = self._pane._plots[root.ref['id']][1]
+ else:
+ child_pane = None
+
+ if self.renderer.backend != 'bokeh' and child_pane is not None:
+ child_pane.object = self.state
+ elif self.renderer.mode != 'server' or (root and 'embedded' in root.tags):
+ push(self.document, self.comm)
+
+
+ def init_comm(self):
+ """
+ Initializes comm and attaches streams.
+ """
+ if self.comm:
+ return self.comm
+ comm = None
+ if self.dynamic or self.renderer.widget_mode == 'live':
+ comm = self.renderer.comm_manager.get_server_comm()
+ return comm
+
+
@property
def id(self):
return self.comm.id if self.comm else id(self.state)
@@ -94,6 +201,7 @@ def __len__(self):
"""
raise NotImplementedError
+
@classmethod
def lookup_options(cls, obj, group):
plot_class = None
@@ -224,7 +332,7 @@ class DimensionedPlot(Plot):
def __init__(self, keys=None, dimensions=None, layout_dimensions=None,
uniform=True, subplot=False, adjoined=None, layout_num=0,
style=None, subplots=None, dynamic=False, renderer=None,
- comm=None, **params):
+ comm=None, root=None, pane=None, **params):
self.subplots = subplots
self.adjoined = adjoined
self.dimensions = dimensions
@@ -244,6 +352,9 @@ def __init__(self, keys=None, dimensions=None, layout_dimensions=None,
self.renderer = renderer if renderer else Store.renderers[self.backend].instance()
self.comm = comm
self._force = False
+ self._document = None
+ self._root = root
+ self._pane = pane
self._updated = False # Whether the plot should be marked as updated
params = {k: v for k, v in params.items()
if k in self.params()}
@@ -615,54 +726,6 @@ def update(self, key):
return item
- def refresh(self, **kwargs):
- """
- Refreshes the plot by rerendering it and then pushing
- the updated data if the plot has an associated Comm.
- """
- traverse_setter(self, '_force', True)
- key = self.current_key if self.current_key else self.keys[0]
- dim_streams = [stream for stream in self.streams
- if any(c in self.dimensions for c in stream.contents)]
- stream_params = stream_parameters(dim_streams)
- key = tuple(None if d in stream_params else k
- for d, k in zip(self.dimensions, key))
- stream_key = util.wrap_tuple_streams(key, self.dimensions, self.streams)
-
- self._trigger_refresh(stream_key)
- if self.comm is not None and self.top_level:
- self.push()
-
-
- def _trigger_refresh(self, key):
- "Triggers update to a plot on a refresh event"
- # Update if not top-level, batched or an ElementPlot
- if not self.top_level or isinstance(self, GenericElementPlot):
- self.update(key)
-
-
- def push(self):
- """
- Pushes updated plot data via the Comm.
- """
- if self.comm is None:
- raise Exception('Renderer does not have a comm.')
- diff = self.renderer.diff(self)
- self.comm.send(diff)
-
-
- def init_comm(self):
- """
- Initializes comm and attaches streams.
- """
- if self.comm:
- return self.comm
- comm = None
- if self.dynamic or self.renderer.widget_mode == 'live':
- comm = self.renderer.comm_manager.get_server_comm()
- return comm
-
-
def __len__(self):
"""
Returns the total number of available frames.
@@ -1539,3 +1602,16 @@ def __init__(self, layout, **params):
self.rows, self.cols = layout.shape[::-1] if self.transpose else layout.shape
self.coords = list(product(range(self.rows),
range(self.cols)))
+
+
+class GenericAdjointLayoutPlot(Plot):
+ """
+ AdjointLayoutPlot allows placing up to three Views in a number of
+ predefined and fixed layouts, which are defined by the layout_dict
+ class attribute. This allows placing subviews next to a main plot
+ in either a 'top' or 'right' position.
+ """
+
+ layout_dict = {'Single': {'positions': ['main']},
+ 'Dual': {'positions': ['main', 'right']},
+ 'Triple': {'positions': ['main', 'right', 'top']}}
diff --git a/holoviews/plotting/plotly/plot.py b/holoviews/plotting/plotly/plot.py
index 5357367331..e2200a3e6e 100644
--- a/holoviews/plotting/plotly/plot.py
+++ b/holoviews/plotting/plotly/plot.py
@@ -8,8 +8,9 @@
from ...element import Histogram
from ...core.options import Store
from ...core.util import wrap_tuple
-from ..plot import DimensionedPlot, GenericLayoutPlot, GenericCompositePlot, \
- GenericElementPlot
+from ..plot import (
+ DimensionedPlot, GenericLayoutPlot, GenericCompositePlot,
+ GenericElementPlot, GenericAdjointLayoutPlot)
from .util import figure_grid
@@ -242,11 +243,7 @@ def generate_plot(self, key, ranges=None):
-class AdjointLayoutPlot(PlotlyPlot):
-
- layout_dict = {'Single': {'positions': ['main']},
- 'Dual': {'positions': ['main', 'right']},
- 'Triple': {'positions': ['main', 'right', 'top']}}
+class AdjointLayoutPlot(PlotlyPlot, GenericAdjointLayoutPlot):
registry = {}
diff --git a/holoviews/plotting/plotly/plotlywidgets.js b/holoviews/plotting/plotly/plotlywidgets.js
deleted file mode 100644
index 8e246f41f8..0000000000
--- a/holoviews/plotting/plotly/plotlywidgets.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// Define Plotly specific subclasses
-function PlotlySelectionWidget() {
- SelectionWidget.apply(this, arguments);
-}
-
-function PlotlyScrubberWidget() {
- ScrubberWidget.apply(this, arguments);
-}
-
-// Let them inherit from the baseclasses
-PlotlySelectionWidget.prototype = Object.create(SelectionWidget.prototype);
-PlotlyScrubberWidget.prototype = Object.create(ScrubberWidget.prototype);
-
-// Define methods to override on widgets
-var PlotlyMethods = {
- init_slider : function(init_val){
- for (var index in this.frames) {
- this.frames[index] = JSON.parse(this.frames[index]);
- }
- },
- process_msg : function(msg) {
- var data = JSON.parse(msg.content.data);
- this.frames[this.current] = data;
- this.update_cache(true);
- this.update(this.current);
- },
- update : function(current){
- var data = this.frames[current];
- var plot = $('#'+this.id)[0];
- for (var i = 0; i < data.data.length; i++) {
- for (var key in data.data[i]) {
- plot.data[i][key] = data.data[i][key];
- }
- }
- var plotly = window._Plotly || window.Plotly;
- plotly.relayout(plot, data.layout);
- plotly.redraw(plot);
- }
-}
-
-// Extend Plotly widgets with backend specific methods
-extend(PlotlySelectionWidget.prototype, PlotlyMethods);
-extend(PlotlyScrubberWidget.prototype, PlotlyMethods);
-
-window.HoloViews.PlotlySelectionWidget = PlotlySelectionWidget;
-window.HoloViews.PlotlyScrubberWidget = PlotlyScrubberWidget;
diff --git a/holoviews/plotting/plotly/renderer.py b/holoviews/plotting/plotly/renderer.py
index 36d1821252..1ba2d3e970 100644
--- a/holoviews/plotting/plotly/renderer.py
+++ b/holoviews/plotting/plotly/renderer.py
@@ -1,80 +1,33 @@
from __future__ import absolute_import, division, unicode_literals
import base64
-import json
import param
with param.logging_level('CRITICAL'):
- from plotly import utils
- from plotly.offline import get_plotlyjs, init_notebook_mode
import plotly.graph_objs as go
from ..renderer import Renderer, MIME_TYPES, HTML_TAGS
from ...core.options import Store
from ...core import HoloMap
-from .widgets import PlotlyScrubberWidget, PlotlySelectionWidget
-
-
-plotly_msg_handler = """
-/* Backend specific body of the msg_handler, updates displayed frame */
-var plot = $('#{plot_id}')[0];
-var data = JSON.parse(msg);
-$.each(data.data, function(i, obj) {{
- $.each(Object.keys(obj), function(j, key) {{
- plot.data[i][key] = obj[key];
- }});
-}});
-var plotly = window._Plotly || window.Plotly;
-plotly.relayout(plot, data.layout);
-plotly.redraw(plot);
-"""
class PlotlyRenderer(Renderer):
backend = param.String(default='plotly', doc="The backend name.")
- fig = param.ObjectSelector(default='auto', objects=['html', 'json', 'png', 'svg', 'auto'], doc="""
+ fig = param.ObjectSelector(default='auto', objects=['html', 'png', 'svg', 'auto'], doc="""
Output render format for static figures. If None, no figure
rendering will occur. """)
- mode_formats = {'fig': {'default': ['html', 'png', 'svg', 'json']},
- 'holomap': {'default': ['widgets', 'scrubber', 'auto']}}
-
- widgets = {'scrubber': PlotlyScrubberWidget,
- 'widgets': PlotlySelectionWidget}
+ mode_formats = {'fig': ['html', 'png', 'svg'],
+ 'holomap': ['widgets', 'scrubber', 'auto']}
- backend_dependencies = {'js': (get_plotlyjs(),)}
-
- comm_msg_handler = plotly_msg_handler
+ widgets = ['scrubber', 'widgets']
_loaded = False
+ _render_with_panel = True
- def __call__(self, obj, fmt='html', divuuid=None):
- plot, fmt = self._validate(obj, fmt)
- mime_types = {'file-ext':fmt, 'mime_type': MIME_TYPES[fmt]}
-
- if isinstance(plot, tuple(self.widgets.values())):
- return plot(), mime_types
- elif fmt in ('html', 'png', 'svg'):
- return self._figure_data(plot, fmt, divuuid=divuuid), mime_types
- elif fmt == 'json':
- return self.diff(plot), mime_types
-
-
- def diff(self, plot, serialize=True):
- """
- Returns a json diff required to update an existing plot with
- the latest plot data.
- """
- diff = plot.state
- if serialize:
- return json.dumps(diff, cls=utils.PlotlyJSONEncoder)
- else:
- return diff
-
-
- def _figure_data(self, plot, fmt=None, divuuid=None, comm=True, as_script=False, width=800, height=600):
+ def _figure_data(self, plot, fmt, as_script=False, **kwargs):
# Wrapping plot.state in go.Figure here performs validation
# and applies any default theme.
figure = go.Figure(plot.state)
@@ -82,57 +35,20 @@ def _figure_data(self, plot, fmt=None, divuuid=None, comm=True, as_script=False,
if fmt in ('png', 'svg'):
import plotly.io as pio
data = pio.to_image(figure, fmt)
+
+ if fmt == 'svg':
+ data = data.decode('utf-8')
+
if as_script:
b64 = base64.b64encode(data).decode("utf-8")
(mime_type, tag) = MIME_TYPES[fmt], HTML_TAGS[fmt]
src = HTML_TAGS['base64'].format(mime_type=mime_type, b64=b64)
div = tag.format(src=src, mime_type=mime_type, css='')
- js = ''
- return div, js
- return data
-
- if divuuid is None:
- divuuid = plot.id
-
- jdata = json.dumps(figure.data, cls=utils.PlotlyJSONEncoder)
- jlayout = json.dumps(figure.layout, cls=utils.PlotlyJSONEncoder)
-
- config = {}
- config['showLink'] = False
- jconfig = json.dumps(config)
-
- if as_script:
- header = 'window.PLOTLYENV=window.PLOTLYENV || {};'
+ return div
+ else:
+ return data
else:
- header = ('')
-
- script = '\n'.join([
- 'var plotly = window._Plotly || window.Plotly;'
- 'plotly.plot("{id}", {data}, {layout}, {config}).then(function() {{',
- ' var elem = document.getElementById("{id}.loading"); elem.parentNode.removeChild(elem);',
- '}})']).format(id=divuuid,
- data=jdata,
- layout=jlayout,
- config=jconfig)
-
- html = ('
'
- 'Drawing...
'
- '
'
- '
'.format(id=divuuid, height=height, width=width))
- if as_script:
- return html, header + script
-
- content = (
- '{html}'
- ''
- ).format(html=html, script=script)
- return '\n'.join([header, content])
-
+ raise ValueError("Unsupported format: {fmt}".format(fmt=fmt))
@classmethod
def plot_options(cls, obj, percent_size):
@@ -150,8 +66,5 @@ def load_nb(cls, inline=True):
"""
Loads the plotly notebook resources.
"""
- from IPython.display import publish_display_data
+ import panel.models.plotly # noqa
cls._loaded = True
- init_notebook_mode(connected=not inline)
- publish_display_data(data={MIME_TYPES['jlab-hv-load']:
- get_plotlyjs()})
diff --git a/holoviews/plotting/plotly/widgets.py b/holoviews/plotting/plotly/widgets.py
deleted file mode 100644
index 50057721f6..0000000000
--- a/holoviews/plotting/plotly/widgets.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from __future__ import absolute_import, division, unicode_literals
-
-import json
-
-import param
-
-from ..widgets import NdWidget, SelectionWidget, ScrubberWidget
-
-class PlotlyWidget(NdWidget):
-
- extensionjs = param.String(default='plotlywidgets.js', doc="""
- Optional javascript extension file for a particular backend.""")
-
- def _get_data(self):
- msg, metadata = self.renderer.components(self.plot, divuuid=self.id, comm=False)
- data = super(PlotlyWidget, self)._get_data()
- return dict(data, init_html=msg['text/html'],
- init_js=msg['application/javascript'])
-
- def encode_frames(self, frames):
- frames = json.dumps(frames).replace('', r'<\/')
- return frames
-
- def _plot_figure(self, idx, fig_format='json'):
- """
- Returns the figure in html format on the
- first call and
- """
- self.plot.update(idx)
- if self.embed:
- return self.renderer.diff(self.plot)
-
-
-class PlotlySelectionWidget(PlotlyWidget, SelectionWidget):
-
- def _get_data(self):
- if not self.plot.dynamic:
- widgets, _, _ = self.get_widgets()
- key = tuple(w['value'] for w in widgets)
- self.plot.update(tuple(key))
- return super(PlotlySelectionWidget, self)._get_data()
-
-
-class PlotlyScrubberWidget(PlotlyWidget, ScrubberWidget):
- pass
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index ac5ada5295..3cdd2c4994 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -2,9 +2,9 @@
Public API for all plotting renderers supported by HoloViews,
regardless of plotting package or backend.
"""
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
-import os, base64
+import base64
from io import BytesIO
try:
from StringIO import StringIO
@@ -13,11 +13,17 @@
from contextlib import contextmanager
import param
+
+from panel import config
+from panel.io.notebook import load_notebook, render_model, render_mimebundle
+from panel.pane import HoloViews as HoloViewsPane
+from panel.widgets.player import PlayerBase
+from panel.viewable import Viewable
+
from ..core.io import Exporter
from ..core.options import Store, StoreOptions, SkipRendering, Compositor
-from ..core.util import find_file, unicode, unbound_dimensions, basestring
+from ..core.util import unbound_dimensions
from .. import Layout, HoloMap, AdjointLayout, DynamicMap
-from .widgets import NdWidget, ScrubberWidget, SelectionWidget
from . import Plot
from pyviz_comms import CommManager, JupyterCommManager
@@ -63,10 +69,6 @@
static_template = """
-
- {css}
- {js}
-
{html}
@@ -90,14 +92,14 @@ class Renderer(Exporter):
The full, lowercase name of the rendering backend or third
part plotting package used e.g 'matplotlib' or 'cairo'.""")
- dpi=param.Integer(None, doc="""
+ dpi = param.Integer(None, doc="""
The render resolution in dpi (dots per inch)""")
fig = param.ObjectSelector(default='auto', objects=['auto'], doc="""
Output render format for static figures. If None, no figure
rendering will occur. """)
- fps=param.Number(20, doc="""
+ fps = param.Number(20, doc="""
Rendered fps (frames per second) for animated formats.""")
holomap = param.ObjectSelector(default='auto',
@@ -105,19 +107,27 @@ class Renderer(Exporter):
Output render multi-frame (typically animated) format. If
None, no multi-frame rendering will occur.""")
- mode = param.ObjectSelector(default='default', objects=['default'], doc="""
- The available rendering modes. As a minimum, the 'default'
- mode must be supported.""")
+ mode = param.ObjectSelector(default='default',
+ objects=['default', 'server'], doc="""
+ Whether to render the object in regular or server mode. In server
+ mode a bokeh Document will be returned which can be served as a
+ bokeh server app. By default renders all output is rendered to HTML.""")
- size=param.Integer(100, doc="""
+ size = param.Integer(100, doc="""
The rendered size as a percentage size""")
+ widget_location = param.ObjectSelector(default=None, allow_None=True, objects=[
+ 'left', 'bottom', 'right', 'top', 'top_left', 'top_right',
+ 'bottom_left', 'bottom_right', 'left_top', 'left_bottom',
+ 'right_top', 'right_bottom'], doc="""
+ The position of the widgets relative to the plot.""")
+
widget_mode = param.ObjectSelector(default='embed', objects=['embed', 'live'], doc="""
The widget mode determining whether frames are embedded or generated
'live' when interacting with the widget.""")
- css = param.Dict(default={},
- doc="Dictionary of CSS attributes and values to apply to HTML output")
+ css = param.Dict(default={}, doc="""
+ Dictionary of CSS attributes and values to apply to HTML output.""")
info_fn = param.Callable(None, allow_None=True, constant=True, doc="""
Renderers do not support the saving of object info metadata""")
@@ -134,29 +144,15 @@ class Renderer(Exporter):
data before output is saved to file or displayed.""")
# Defines the valid output formats for each mode.
- mode_formats = {'fig': {'default': [None, 'auto']},
- 'holomap': {'default': [None, 'auto']}}
+ mode_formats = {'fig': [None, 'auto'],
+ 'holomap': [None, 'auto']}
# The comm_manager handles the creation and registering of client,
# and server side comms
comm_manager = CommManager
- # JS code which handles comm messages and updates the plot
- comm_msg_handler = None
-
# Define appropriate widget classes
- widgets = {'scrubber': ScrubberWidget, 'widgets': SelectionWidget}
-
- core_dependencies = {'jQueryUI': {'js': ['https://code.jquery.com/ui/1.10.4/jquery-ui.min.js'],
- 'css': ['https://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css']}}
-
- extra_dependencies = {'jQuery': {'js': ['https://code.jquery.com/jquery-2.1.4.min.js']},
- 'underscore': {'js': ['https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js']},
- 'require': {'js': ['https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.20/require.min.js']},
- 'bootstrap': {'css': ['https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css']}}
-
- # Any additional JS and CSS dependencies required by a specific backend
- backend_dependencies = {}
+ widgets = ['scrubber', 'widgets']
# Whether in a notebook context, set when running Renderer.load_nb
notebook_context = False
@@ -164,13 +160,32 @@ class Renderer(Exporter):
# Plot registry
_plots = {}
+ # Whether to render plots with Panel
+ _render_with_panel = False
+
def __init__(self, **params):
self.last_plot = None
super(Renderer, self).__init__(**params)
+ def __call__(self, obj, fmt='auto', **kwargs):
+ plot, fmt = self._validate(obj, fmt)
+ info = {'file-ext': fmt, 'mime_type': MIME_TYPES[fmt]}
+
+ if plot is None:
+ return None, info
+ elif self.mode == 'server':
+ return self.server_doc(plot, doc=kwargs.get('doc')), info
+ elif isinstance(plot, Viewable):
+ return self.static_html(plot), info
+ else:
+ data = self._figure_data(plot, fmt, **kwargs)
+ data = self._apply_post_render_hooks(data, obj, fmt)
+ return data, info
+
+
@bothmethod
- def get_plot(self_or_cls, obj, renderer=None, **kwargs):
+ def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
"""
Given a HoloViews Viewable return a corresponding plot instance.
"""
@@ -207,6 +222,16 @@ def get_plot(self_or_cls, obj, renderer=None, **kwargs):
plot.update(init_key)
else:
plot = obj
+
+ if isinstance(self_or_cls, Renderer):
+ self_or_cls.last_plot = plot
+
+ if plot.comm or self_or_cls.mode == 'server':
+ from bokeh.document import Document
+ from bokeh.io import curdoc
+ if doc is None:
+ doc = Document() if self_or_cls.notebook_context else curdoc()
+ plot.document = doc
return plot
@@ -215,25 +240,29 @@ def _validate(self, obj, fmt, **kwargs):
Helper method to be used in the __call__ method to get a
suitable plot or widget object and the appropriate format.
"""
- if isinstance(obj, tuple(self.widgets.values())):
+ if isinstance(obj, Viewable):
return obj, 'html'
- plot = self.get_plot(obj, renderer=self, **kwargs)
- fig_formats = self.mode_formats['fig'][self.mode]
- holomap_formats = self.mode_formats['holomap'][self.mode]
+ fig_formats = self.mode_formats['fig']
+ holomap_formats = self.mode_formats['holomap']
+
+ holomaps = obj.traverse(lambda x: x, [HoloMap])
+ dynamic = any(isinstance(m, DynamicMap) for m in holomaps)
if fmt in ['auto', None]:
- if (((len(plot) == 1 and not plot.dynamic)
- or (len(plot) > 1 and self.holomap is None) or
- (plot.dynamic and len(plot.keys[0]) == 0)) or
- not unbound_dimensions(plot.streams, plot.dimensions, no_duplicates=False)):
- fmt = fig_formats[0] if self.fig=='auto' else self.fig
+ if any(len(o) > 1 or (isinstance(o, DynamicMap) and unbound_dimensions(o.streams, o.kdims))
+ for o in holomaps):
+ fmt = holomap_formats[0] if self.holomap in ['auto', None] else self.holomap
else:
- fmt = holomap_formats[0] if self.holomap=='auto' else self.holomap
+ fmt = fig_formats[0] if self.fig == 'auto' else self.fig
if fmt in self.widgets:
- plot = self.get_widget(plot, fmt, display_options={'fps': self.fps})
+ plot = self.get_widget(obj, fmt)
fmt = 'html'
+ elif dynamic or (self._render_with_panel and fmt == 'html'):
+ plot, fmt = HoloViewsPane(obj, center=True, backend=self.backend, renderer=self), fmt
+ else:
+ plot = self.get_plot(obj, renderer=self, **kwargs)
all_formats = set(fig_formats + holomap_formats)
if fmt not in all_formats:
@@ -243,24 +272,6 @@ def _validate(self, obj, fmt, **kwargs):
return plot, fmt
- def __call__(self, obj, fmt=None):
- """
- Render the supplied HoloViews component or plot instance using
- the appropriate backend. The output is not a file format but a
- suitable, in-memory byte stream together with any suitable
- metadata.
- """
- plot, fmt = self._validate(obj, fmt)
- if plot is None: return
- # [Backend specific code goes here to generate data]
- data = None
-
- # Example of how post_render_hooks are applied
- data = self._apply_post_render_hooks(data, obj, fmt)
- # Example of the return format where the first value is the rendered data.
- return data, {'file-ext':fmt, 'mime_type':MIME_TYPES[fmt]}
-
-
def _apply_post_render_hooks(self, data, obj, fmt):
"""
Apply the post-render hooks to the data.
@@ -275,7 +286,7 @@ def _apply_post_render_hooks(self, data, obj, fmt):
return data
- def html(self, obj, fmt=None, css=None, **kwargs):
+ def html(self, obj, fmt=None, css=None, resources='CDN', **kwargs):
"""
Renders plot or data structure and wraps the output in HTML.
The comm argument defines whether the HTML output includes
@@ -285,7 +296,18 @@ def html(self, obj, fmt=None, css=None, **kwargs):
figdata, _ = self(plot, fmt, **kwargs)
if css is None: css = self.css
- if fmt in ['html', 'json']:
+ if isinstance(plot, Viewable):
+ from bokeh.document import Document
+ from bokeh.embed import file_html
+ from bokeh.resources import CDN, INLINE
+ doc = Document()
+ plot._render_model(doc)
+ if resources == 'cdn':
+ resources = CDN
+ elif resources == 'inline':
+ resources = INLINE
+ return file_html(doc, resources)
+ elif fmt in ['html', 'json']:
return figdata
else:
if fmt == 'svg':
@@ -310,39 +332,27 @@ def components(self, obj, fmt=None, comm=True, **kwargs):
"""
Returns data and metadata dictionaries containing HTML and JS
components to include render in app, notebook, or standalone
- document. Depending on the backend the fmt defines the format
- embedded in the HTML, e.g. png or svg. If comm is enabled the
- JS code will set up a Websocket comm channel using the
- currently defined CommManager.
+ document.
"""
- if isinstance(obj, (Plot, NdWidget)):
+ if isinstance(obj, Plot):
plot = obj
else:
plot, fmt = self._validate(obj, fmt)
data, metadata = {}, {}
- if isinstance(plot, NdWidget):
- js, html = plot(as_script=True)
- plot_id = plot.plot_id
+ if isinstance(plot, Viewable):
+ from bokeh.document import Document
+ dynamic = bool(plot.object.traverse(lambda x: x, [DynamicMap]))
+ embed = (not (dynamic or self.widget_mode == 'live') or config.embed)
+ comm = self.comm_manager.get_server_comm() if comm else None
+ doc = Document()
+ with config.set(embed=embed):
+ model = plot.layout._render_model(doc, comm)
+ return render_model(model, comm) if embed else render_mimebundle(model, doc, comm)
else:
- html, js = self._figure_data(plot, fmt, as_script=True, **kwargs)
- plot_id = plot.id
- if comm and plot.comm is not None and self.comm_msg_handler:
- msg_handler = self.comm_msg_handler.format(plot_id=plot_id)
- html = plot.comm.html_template.format(init_frame=html,
- plot_id=plot_id)
- comm_js = plot.comm.js_template.format(msg_handler=msg_handler,
- comm_id=plot.comm.id,
- plot_id=plot_id)
- js = '\n'.join([js, comm_js])
- html = "
%s
" % (plot_id, html)
-
+ html = self._figure_data(plot, fmt, as_script=True, **kwargs)
data['text/html'] = html
- if js:
- data[MIME_TYPES['js']] = js
- data[MIME_TYPES['jlab-hv-exec']] = ''
- metadata['id'] = plot_id
- self._plots[plot_id] = plot
+
return (data, {MIME_TYPES['jlab-hv-exec']: metadata})
@@ -352,38 +362,26 @@ def static_html(self, obj, fmt=None, template=None):
supplied format. Allows supplying a template formatting string
with fields to interpolate 'js', 'css' and the main 'html'.
"""
- js_html, css_html = self.html_assets()
- if template is None: template = static_template
- html = self.html(obj, fmt)
- return template.format(js=js_html, css=css_html, html=html)
+ html_bytes = StringIO()
+ self.save(obj, html_bytes, fmt)
+ html_bytes.seek(0)
+ return html_bytes.read()
@bothmethod
def get_widget(self_or_cls, plot, widget_type, **kwargs):
- if not isinstance(plot, Plot):
- plot = self_or_cls.get_plot(plot)
- dynamic = plot.dynamic
- # Whether dimensions define discrete space
- discrete = all(d.values for d in plot.dimensions)
- if widget_type == 'auto':
- isuniform = plot.uniform
- if not isuniform:
- widget_type = 'scrubber'
- else:
- widget_type = 'widgets'
- elif dynamic and not discrete:
- widget_type = 'widgets'
-
- if widget_type in [None, 'auto']:
- holomap_formats = self_or_cls.mode_formats['holomap'][self_or_cls.mode]
- widget_type = holomap_formats[0] if self_or_cls.holomap=='auto' else self_or_cls.holomap
+ if widget_type == 'scrubber':
+ widget_location = self_or_cls.widget_location or 'bottom'
+ else:
+ widget_type = 'individual'
+ widget_location = self_or_cls.widget_location or 'right'
- widget_cls = self_or_cls.widgets[widget_type]
- renderer = self_or_cls
- if not isinstance(self_or_cls, Renderer):
- renderer = self_or_cls.instance()
- embed = self_or_cls.widget_mode == 'embed'
- return widget_cls(plot, renderer=renderer, embed=embed, **kwargs)
+ layout = HoloViewsPane(plot, widget_type=widget_type, center=True,
+ widget_location=widget_location, renderer=self_or_cls)
+ interval = int((1./self_or_cls.fps) * 1000)
+ for player in layout.layout.select(PlayerBase):
+ player.interval = interval
+ return layout
@bothmethod
@@ -397,33 +395,60 @@ def export_widgets(self_or_cls, obj, filename, fmt=None, template=None,
data to a json file in the supplied json_path (defaults to
current path).
"""
- if fmt not in list(self_or_cls.widgets.keys())+['auto', None]:
+ if fmt not in self_or_cls.widgets+['auto', None]:
raise ValueError("Renderer.export_widget may only export "
"registered widget types.")
+ self_or_cls.get_widget(obj, fmt).save(filename)
- if not isinstance(obj, NdWidget):
- if not isinstance(filename, (BytesIO, StringIO)):
- filedir = os.path.dirname(filename)
- current_path = os.getcwd()
- html_path = os.path.abspath(filedir)
- rel_path = os.path.relpath(html_path, current_path)
- save_path = os.path.join(rel_path, json_path)
- else:
- save_path = json_path
- kwargs['json_save_path'] = save_path
- kwargs['json_load_path'] = json_path
- widget = self_or_cls.get_widget(obj, fmt, **kwargs)
+
+ @bothmethod
+ def _widget_kwargs(self_or_cls):
+ if self_or_cls.holomap in ('auto', 'widgets'):
+ widget_type = 'individual'
+ loc = self_or_cls.widget_location or 'right'
else:
- widget = obj
+ widget_type = 'scrubber'
+ loc = self_or_cls.widget_location or 'bottom'
+ return {'widget_location': loc, 'widget_type': widget_type, 'center': True}
+
- html = self_or_cls.static_html(widget, fmt, template)
- encoded = self_or_cls.encode((html, {'mime_type': 'text/html'}))
- if isinstance(filename, (BytesIO, StringIO)):
- filename.write(encoded)
- filename.seek(0)
+ @bothmethod
+ def app(self_or_cls, plot, show=False, new_window=False, websocket_origin=None, port=0):
+ """
+ Creates a bokeh app from a HoloViews object or plot. By
+ default simply attaches the plot to bokeh's curdoc and returns
+ the Document, if show option is supplied creates an
+ Application instance and displays it either in a browser
+ window or inline if notebook extension has been loaded. Using
+ the new_window option the app may be displayed in a new
+ browser tab once the notebook extension has been loaded. A
+ websocket origin is required when launching from an existing
+ tornado server (such as the notebook) and it is not on the
+ default port ('localhost:8888').
+ """
+ if isinstance(plot, HoloViewsPane):
+ pane = plot
else:
- with open(filename, 'wb') as f:
- f.write(encoded)
+ pane = HoloViewsPane(plot, backend=self_or_cls.backend, renderer=self_or_cls,
+ **self_or_cls._widget_kwargs())
+ if new_window:
+ return pane._get_server(port, websocket_origin, show=show)
+ else:
+ kwargs = {'notebook_url': websocket_origin} if websocket_origin else {}
+ return pane.app(port=port, **kwargs)
+
+
+ @bothmethod
+ def server_doc(self_or_cls, obj, doc=None):
+ """
+ Get a bokeh Document with the plot attached. May supply
+ an existing doc, otherwise bokeh.io.curdoc() is used to
+ attach the plot to the global document instance.
+ """
+ if not isinstance(obj, HoloViewsPane):
+ obj = HoloViewsPane(obj, renderer=self_or_cls, backend=self_or_cls.backend,
+ **self_or_cls._widget_kwargs())
+ return obj.layout.server_doc(doc)
@classmethod
@@ -449,72 +474,11 @@ class needed to render it with the current renderer.
@classmethod
def html_assets(cls, core=True, extras=True, backends=None, script=False):
"""
- Returns JS and CSS and for embedding of widgets.
- """
- if backends is None:
- backends = [cls.backend] if cls.backend else []
-
- # Get all the widgets and find the set of required js widget files
- widgets = [wdgt for r in [Renderer]+Renderer.__subclasses__()
- for wdgt in r.widgets.values()]
- css = list({wdgt.css for wdgt in widgets})
- basejs = list({wdgt.basejs for wdgt in widgets})
- extensionjs = list({wdgt.extensionjs for wdgt in widgets})
-
- # Join all the js widget code into one string
- path = os.path.dirname(os.path.abspath(__file__))
-
- def open_and_read(path, f):
- with open(find_file(path, f), 'r') as f:
- txt = f.read()
- return txt
-
- widgetjs = '\n'.join(open_and_read(path, f)
- for f in basejs + extensionjs if f is not None)
- widgetcss = '\n'.join(open_and_read(path, f)
- for f in css if f is not None)
-
- dependencies = {}
- if core:
- dependencies.update(cls.core_dependencies)
- if extras:
- dependencies.update(cls.extra_dependencies)
- for backend in backends:
- dependencies[backend] = Store.renderers[backend].backend_dependencies
-
- js_html, css_html = '', ''
- for _, dep in sorted(dependencies.items(), key=lambda x: x[0]):
- js_data = dep.get('js', [])
- if isinstance(js_data, tuple):
- for js in js_data:
- if script:
- js_html += js
- else:
- js_html += '\n' % js
- elif not script:
- for js in js_data:
- js_html += '\n' % js
- css_data = dep.get('css', [])
- if isinstance(js_data, tuple):
- for css in css_data:
- css_html += '\n' % css
- else:
- for css in css_data:
- css_html += '\n' % css
- if script:
- js_html += widgetjs
- else:
- js_html += '\n' % widgetjs
- css_html += '\n' % widgetcss
-
- comm_js = cls.comm_manager.js_manager
- if script:
- js_html += comm_js
- else:
- js_html += '\n' % comm_js
-
- return unicode(js_html), unicode(css_html)
-
+ Deprecated: No longer needed
+ """
+ param.main.warning("Renderer.html_assets is deprecated as all "
+ "JS and CSS dependencies are now handled by "
+ "Panel.")
@classmethod
def plot_options(cls, obj, percent_size):
@@ -530,7 +494,8 @@ def plot_options(cls, obj, percent_size):
@bothmethod
- def save(self_or_cls, obj, basename, fmt='auto', key={}, info={}, options=None, **kwargs):
+ def save(self_or_cls, obj, basename, fmt='auto', key={}, info={},
+ options=None, resources='inline', **kwargs):
"""
Save a HoloViews object to file, either using an explicitly
supplied format or to the appropriate default.
@@ -538,17 +503,16 @@ def save(self_or_cls, obj, basename, fmt='auto', key={}, info={}, options=None,
if info or key:
raise Exception('Renderer does not support saving metadata to file.')
- if isinstance(obj, (Plot, NdWidget)):
- plot = obj
- else:
- with StoreOptions.options(obj, options, **kwargs):
- plot = self_or_cls.get_plot(obj)
-
- if (fmt in list(self_or_cls.widgets.keys())+['auto']) and len(plot) > 1:
- with StoreOptions.options(obj, options, **kwargs):
- if isinstance(basename, basestring):
- basename = basename+'.html'
- self_or_cls.export_widgets(plot, basename, fmt)
+ with StoreOptions.options(obj, options, **kwargs):
+ plot, fmt = self_or_cls._validate(obj, fmt)
+
+ if isinstance(plot, Viewable):
+ from bokeh.resources import CDN, INLINE
+ if resources.lower() == 'cdn':
+ resources = CDN
+ elif resources.lower() == 'inline':
+ resources = INLINE
+ plot.layout.save(basename, embed=True, resources=resources)
return
rendered = self_or_cls(plot, fmt)
@@ -607,6 +571,7 @@ def load_nb(cls, inline=True):
Loads any resources required for display of plots
in the Jupyter notebook
"""
+ load_notebook(inline)
with param.logging_level('ERROR'):
cls.notebook_context = True
cls.comm_manager = JupyterCommManager
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 7e6b07c164..8a5f6c1764 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -11,7 +11,7 @@
import param
from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout,
- Overlay, GridSpace, NdLayout, Store, NdOverlay)
+ Overlay, GridSpace, NdLayout, NdOverlay)
from ..core.options import Cycle
from ..core.ndmapping import item_check
from ..core.spaces import get_nested_streams
@@ -482,20 +482,6 @@ def initialize_unbounded(obj, dimensions, key):
pass
-def save_frames(obj, filename, fmt=None, backend=None, options=None):
- """
- Utility to export object to files frame by frame, numbered individually.
- Will use default backend and figure format by default.
- """
- backend = Store.current_backend if backend is None else backend
- renderer = Store.renderers[backend]
- fmt = renderer.params('fig').objects[0] if fmt is None else fmt
- plot = renderer.get_plot(obj)
- for i in range(len(plot)):
- plot.update(i)
- renderer.save(plot, '%s_%s' % (filename, i), fmt=fmt, options=options)
-
-
def dynamic_update(plot, subplot, key, overlay, items):
"""
Given a plot, subplot and dynamically generated (Nd)Overlay
diff --git a/holoviews/plotting/widgets/__init__.py b/holoviews/plotting/widgets/__init__.py
deleted file mode 100644
index f292911457..0000000000
--- a/holoviews/plotting/widgets/__init__.py
+++ /dev/null
@@ -1,489 +0,0 @@
-from __future__ import unicode_literals
-
-import os, uuid, json, math
-
-import param
-import numpy as np
-
-from ...core import OrderedDict, NdMapping
-from ...core.options import Store
-from ...core.ndmapping import item_check
-from ...core.util import (
- dimension_sanitizer, bytes_to_unicode, unique_iterator, unicode,
- isnumeric, cross_index, wrap_tuple_streams, drop_streams, datetime_types
-)
-from ...core.traversal import hierarchical
-
-def escape_vals(vals, escape_numerics=True):
- """
- Escapes a list of values to a string, converting to
- unicode for safety.
- """
- # Ints formatted as floats to disambiguate with counter mode
- ints, floats = "%.1f", "%.10f"
-
- escaped = []
- for v in vals:
- if isinstance(v, np.timedelta64):
- v = "'"+str(v)+"'"
- elif isinstance(v, np.datetime64):
- v = "'"+str(v.astype('datetime64[ns]'))+"'"
- elif not isnumeric(v):
- v = "'"+unicode(bytes_to_unicode(v))+"'"
- else:
- if v % 1 == 0:
- v = ints % v
- else:
- v = (floats % v)[:-1]
- if escape_numerics:
- v = "'"+v+"'"
- escaped.append(v)
- return escaped
-
-def escape_tuple(vals):
- return "(" + ", ".join(vals) + (",)" if len(vals) == 1 else ")")
-
-def escape_list(vals):
- return "[" + ", ".join(vals) + "]"
-
-def escape_dict(vals):
- vals = [': '.join([k, escape_list(v)]) for k, v in
- zip(escape_vals(vals.keys()), vals.values())]
- return "{" + ", ".join(vals) + "}"
-
-
-subdirs = [p[0] for p in os.walk(os.path.join(os.path.split(__file__)[0], '..'))]
-
-class NdWidget(param.Parameterized):
- """
- NdWidget is an abstract base class implementing a method to find
- the dimensions and keys of any ViewableElement, GridSpace or
- UniformNdMapping type. In the process it creates a mock_obj to
- hold the dimensions and keys.
- """
-
- display_options = param.Dict(default={}, doc="""
- The display options used to generate individual frames""")
-
- embed = param.Boolean(default=True, doc="""
- Whether to embed all plots in the Javascript, generating
- a static widget not dependent on the IPython server.""")
-
- #######################
- # JSON export options #
- #######################
-
- export_json = param.Boolean(default=False, doc="""
- Whether to export plots as JSON files, which can be
- dynamically loaded through a callback from the slider.""")
-
- json_save_path = param.String(default='./json_figures', doc="""
- If export_json is enabled the widget will save the JSON
- data to this path. If None data will be accessible via the
- json_data attribute.""")
-
- json_load_path = param.String(default=None, doc="""
- If export_json is enabled the widget JS code will load the data
- from this path, if None defaults to json_save_path. For loading
- the data from within the notebook the path must be relative,
- when exporting the notebook the path can be set to another
- location like a webserver where the JSON files can be uploaded to.""")
-
- ##############################
- # Javascript include options #
- ##############################
-
- css = param.String(default=None, doc="""
- Defines the local CSS file to be loaded for this widget.""")
-
- basejs = param.String(default='widgets.js', doc="""
- JS file containing javascript baseclasses for the widget.""")
-
- extensionjs = param.String(default=None, doc="""
- Optional javascript extension file for a particular backend.""")
-
- widgets = {}
- counter = 0
-
- def __init__(self, plot, renderer=None, **params):
- super(NdWidget, self).__init__(**params)
- self.id = plot.comm.id if plot.comm else uuid.uuid4().hex
- self.plot = plot
- self.plot_id = plot.id
- streams = []
- for stream in plot.streams:
- if any(k in plot.dimensions for k in stream.contents):
- streams.append(stream)
-
- keys = plot.keys[:1] if self.plot.dynamic else plot.keys
- self.dimensions, self.keys = drop_streams(streams,
- plot.dimensions,
- keys)
- defaults = [kd.default for kd in self.dimensions]
- self.init_key = tuple(v if d is None else d for v, d in
- zip(self.keys[0], defaults))
-
- self.json_data = {}
- if self.plot.dynamic: self.embed = False
- if renderer is None:
- backend = Store.current_backend
- self.renderer = Store.renderers[backend]
- else:
- self.renderer = renderer
-
- # Create mock NdMapping to hold the common dimensions and keys
- sorted_dims = []
- for dim in self.dimensions:
- if dim.values and all(isnumeric(v) for v in dim.values):
- dim = dim.clone(values=sorted(dim.values))
- sorted_dims.append(dim)
-
- if self.plot.dynamic:
- self.length = np.product([len(d.values) for d in sorted_dims if d.values])
- else:
- self.length = len(self.plot)
-
- with item_check(False):
- self.mock_obj = NdMapping([(k, None) for k in self.keys],
- kdims=sorted_dims, sort=False)
-
- NdWidget.widgets[self.id] = self
-
- # Set up jinja2 templating
- import jinja2
- templateLoader = jinja2.FileSystemLoader(subdirs)
- self.jinjaEnv = jinja2.Environment(loader=templateLoader)
- if not self.embed:
- comm_manager = self.renderer.comm_manager
- self.comm = comm_manager.get_client_comm(id=self.id+'_client',
- on_msg=self._process_update)
-
-
- def cleanup(self):
- self.plot.cleanup()
- del NdWidget.widgets[self.id]
-
-
- def _process_update(self, msg):
- if 'content' not in msg:
- raise ValueError('Received widget comm message has no content.')
- self.update(msg['content'])
-
-
- def __call__(self, as_script=False):
- data = self._get_data()
- html = self.render_html(data)
- js = self.render_js(data)
- if as_script:
- return js, html
- js = '' % js
- html = '\n'.join([html, js])
- return html
-
-
- def _get_data(self):
- delay = int(1000./self.display_options.get('fps', 5))
- CDN = {}
- for name, resources in self.plot.renderer.core_dependencies.items():
- if 'js' in resources:
- CDN[name] = resources['js'][0]
- for name, resources in self.plot.renderer.extra_dependencies.items():
- if 'js' in resources:
- CDN[name] = resources['js'][0]
- name = type(self).__name__
- cached = str(self.embed).lower()
- load_json = str(self.export_json).lower()
- mode = str(self.renderer.mode)
- json_path = (self.json_save_path if self.json_load_path is None
- else self.json_load_path)
- if json_path and json_path[-1] != '/':
- json_path = json_path + '/'
- dynamic = json.dumps(self.plot.dynamic) if self.plot.dynamic else 'false'
- return dict(CDN=CDN, frames=self.get_frames(), delay=delay,
- cached=cached, load_json=load_json, mode=mode, id=self.id,
- Nframes=self.length, widget_name=name, json_path=json_path,
- dynamic=dynamic, plot_id=self.plot_id)
-
-
- def render_html(self, data):
- template = self.jinjaEnv.get_template(self.html_template)
- return template.render(**data)
-
-
- def render_js(self, data):
- template = self.jinjaEnv.get_template(self.js_template)
- return template.render(**data)
-
-
- def get_frames(self):
- if self.embed:
- frames = OrderedDict([(idx, self._plot_figure(idx))
- for idx in range(len(self.plot))])
- else:
- frames = {}
- return self.encode_frames(frames)
-
-
- def encode_frames(self, frames):
- if isinstance(frames, dict):
- frames = dict(frames)
- return json.dumps(frames)
-
-
- def save_json(self, frames):
- """
- Saves frames data into a json file at the
- specified json_path, named with the widget uuid.
- """
- if self.json_save_path is None: return
- path = os.path.join(self.json_save_path, '%s.json' % self.id)
- if not os.path.isdir(self.json_save_path):
- os.mkdir(self.json_save_path)
- with open(path, 'w') as f:
- json.dump(frames, f)
- self.json_data = frames
-
-
- def _plot_figure(self, idx):
- with self.renderer.state():
- self.plot.update(idx)
- css = self.display_options.get('css', {})
- figure_format = self.display_options.get('figure_format',
- self.renderer.fig)
- return self.renderer.html(self.plot, figure_format, css=css)
-
-
- def update(self, key):
- pass
-
-
-
-
-class ScrubberWidget(NdWidget):
- """
- ScrubberWidget generates a basic animation widget with a slider
- and various play/rewind/stepping options. It has been adapted
- from Jake Vanderplas' JSAnimation library, which was released
- under BSD license.
-
- Optionally the individual plots can be exported to json, which can
- be dynamically loaded by serving the data the data for each frame
- on a simple server.
- """
-
- html_template = param.String('htmlscrubber.jinja', doc="""
- The jinja2 template used to generate the html output.""")
-
- js_template = param.String('jsscrubber.jinja', doc="""
- The jinja2 template used to generate the html output.""")
-
- def update(self, key):
- if not self.plot.dimensions:
- self.plot.refresh()
- else:
- if self.plot.dynamic:
- key = cross_index([d.values for d in self.mock_obj.kdims], key)
- self.plot.update(key)
- self.plot.push()
-
-
-class SelectionWidget(NdWidget):
- """
- Javascript based widget to select and view ViewableElement objects
- contained in an NdMapping. For each dimension in the NdMapping a
- slider or dropdown selection widget is created and can be used to
- select the html output associated with the selected
- ViewableElement type. The widget maybe set to embed all frames in
- the supplied object into the rendered html or to dynamically
- update the widget with a live IPython kernel.
-
- The widget supports all current HoloViews figure backends
- including png and svg output..
-
- Just like the ScrubberWidget the data can be optionally saved
- to json and dynamically loaded from a server.
- """
-
- css = param.String(default='jsslider.css', doc="""
- Defines the local CSS file to be loaded for this widget.""")
-
- html_template = param.String('htmlslider.jinja', doc="""
- The jinja2 template used to generate the html output.""")
-
- js_template = param.String('jsslider.jinja', doc="""
- The jinja2 template used to generate the html output.""")
-
- ##############################
- # Javascript include options #
- ##############################
-
- throttle = {True: 0, False: 100}
-
- def get_widgets(self):
- # Generate widget data
- widgets, dimensions, init_dim_vals = [], [], []
- if self.plot.dynamic:
- hierarchy = None
- else:
- hierarchy = hierarchical(list(self.mock_obj.data.keys()))
- for idx, dim in enumerate(self.mock_obj.kdims):
- # Hide widget if it has 1-to-1 mapping to next widget
- if self.plot.dynamic:
- widget_data = self._get_dynamic_widget(idx, dim)
- else:
- widget_data = self._get_static_widget(idx, dim, self.mock_obj, hierarchy,
- init_dim_vals)
- init_dim_vals.append(widget_data['init_val'])
- visibility = '' if widget_data.get('visible', True) else 'display: none'
- dim_str = dim.pprint_label
- escaped_dim = dimension_sanitizer(dim_str)
- widget_data = dict(widget_data, dim=escaped_dim, dim_label=dim_str,
- dim_idx=idx, visibility=visibility)
- widgets.append(widget_data)
- dimensions.append(escaped_dim)
- return widgets, dimensions, init_dim_vals
-
-
- @classmethod
- def _get_static_widget(cls, idx, dim, mock_obj, hierarchy, init_dim_vals):
- next_dim = ''
- next_vals = {}
- visible = True
- if next_vals:
- values = next_vals[init_dim_vals[idx-1]]
- else:
- values = (list(dim.values) if dim.values else
- list(unique_iterator(mock_obj.dimension_values(dim.name))))
- visible = visible and len(values) > 1
-
- if idx < mock_obj.ndims-1:
- next_vals = hierarchy[idx]
- next_dim = bytes_to_unicode(mock_obj.kdims[idx+1])
- else:
- next_vals = {}
-
- if isinstance(values[0], datetime_types):
- values = sorted(values)
- dim_vals = [str(v.astype('datetime64[ns]'))
- if isinstance(v, np.datetime64) else str(v)
- for v in values]
- widget_type = 'slider'
- elif isnumeric(values[0]):
- values = sorted(values)
- dim_vals = [round(v, 10) for v in values]
- if next_vals:
- next_vals = {round(k, 10): [round(v, 10) if isnumeric(v) else v
- for v in vals]
- for k, vals in next_vals.items()}
- widget_type = 'slider'
- else:
- dim_vals = values
- next_vals = dict(next_vals)
- widget_type = 'dropdown'
-
- value = values[0] if dim.default is None else dim.default
- value_labels = escape_list(escape_vals([dim.pprint_value(v)
- for v in dim_vals]))
-
- if dim.default is None:
- default = 0
- init_val = dim_vals[0];
- elif dim.default not in dim_vals:
- raise ValueError("%s dimension default %r is not in dimension values: %s"
- % (dim, dim.default, dim.values))
- else:
- default = repr(dim_vals.index(dim.default))
- init_val = dim.default
-
- dim_vals = escape_list(escape_vals(dim_vals))
- next_vals = escape_dict({k: escape_vals(v) for k, v in next_vals.items()})
- return {'type': widget_type, 'vals': dim_vals, 'labels': value_labels,
- 'step': 1, 'default': default, 'next_vals': next_vals,
- 'next_dim': next_dim or None, 'init_val': init_val,
- 'visible': visible, 'value': value}
-
-
- @classmethod
- def _get_dynamic_widget(cls, idx, dim):
- step = 1
- if dim.values:
- if all(isnumeric(v) or isinstance(v, datetime_types) for v in dim.values):
- # Widgets currently detect dynamic mode by type
- # this value representation is now redundant
- # and should be removed in a refactor
- values = dim.values
- dim_vals = {i: i for i, v in enumerate(values)}
- widget_type = 'slider'
- else:
- values = list(dim.values)
- dim_vals = list(range(len(values)))
- widget_type = 'dropdown'
-
- value_labels = escape_list(escape_vals([dim.pprint_value(v)
- for v in values]))
-
- if dim.default is None:
- default = dim_vals[0]
- elif widget_type == 'slider':
- default = values.index(dim.default)
- else:
- default = repr(values.index(dim.default))
- init_val = default
- else:
- widget_type = 'slider'
- value_labels = []
- dim_vals = [dim.soft_range[0] if dim.soft_range[0] else dim.range[0],
- dim.soft_range[1] if dim.soft_range[1] else dim.range[1]]
- dim_range = dim_vals[1] - dim_vals[0]
- int_type = isinstance(dim.type, type) and issubclass(dim.type, int)
- if dim.step is not None:
- step = dim.step
- elif isinstance(dim_range, int) or int_type:
- step = 1
- else:
- step = 10**(round(math.log10(dim_range))-3)
-
- if dim.default is None:
- default = dim_vals[0]
- else:
- default = dim.default
- init_val = default
- dim_vals = escape_list(escape_vals(sorted(dim_vals)))
- return {'type': widget_type, 'vals': dim_vals, 'labels': value_labels,
- 'step': step, 'default': default, 'next_vals': {},
- 'next_dim': None, 'init_val': init_val}
-
-
- def get_key_data(self):
- # Generate key data
- key_data = OrderedDict()
- for i, k in enumerate(self.mock_obj.data.keys()):
- key = escape_tuple(escape_vals(k))
- key_data[key] = i
- return json.dumps(key_data)
-
-
- def _get_data(self):
- data = super(SelectionWidget, self)._get_data()
- widgets, dimensions, init_dim_vals = self.get_widgets()
- init_dim_vals = escape_list(escape_vals(init_dim_vals, not self.plot.dynamic))
- key_data = {} if self.plot.dynamic else self.get_key_data()
- notfound_msg = "