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('' - 'window.PLOTLYENV=window.PLOTLYENV || {};' - '') - - 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(' - - {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 = "

-
- - - - - - - - - -
- Once - Loop - Reflect -
-

diff --git a/holoviews/plotting/widgets/htmlslider.jinja b/holoviews/plotting/widgets/htmlslider.jinja deleted file mode 100644 index 41e0b75b19..0000000000 --- a/holoviews/plotting/widgets/htmlslider.jinja +++ /dev/null @@ -1,36 +0,0 @@ -
-
-
- {% block init_frame %} - {{ init_html }} - {% endblock %} -
-
-
-
- {% for widget_data in widgets %} - {% if widget_data['type'] == 'slider' %} -
- -
-
- -
-
-
-
- {% elif widget_data['type']=='dropdown' %} -
- - -
- {% endif %} - {% endfor %} -
-
-
diff --git a/holoviews/plotting/widgets/jsscrubber.jinja b/holoviews/plotting/widgets/jsscrubber.jinja deleted file mode 100644 index b749f280a7..0000000000 --- a/holoviews/plotting/widgets/jsscrubber.jinja +++ /dev/null @@ -1,11 +0,0 @@ -/* Instantiate the {{ widget_name }} class. */ -/* The IDs given should match those used in the template above. */ -function create_widget() { - var frame_data = {{ frames | safe }}; - var anim = new HoloViews.{{ widget_name }}(frame_data, {{ Nframes }}, "{{ id }}", {{ delay }}, {{ load_json }}, "{{ mode }}", {{ cached }}, "{{ json_path }}", {{ dynamic }}, "{{ plot_id }}"); - HoloViews.index['{{ plot_id }}'] = anim; -} - -create_widget(); - -{{ init_js }} diff --git a/holoviews/plotting/widgets/jsslider.css b/holoviews/plotting/widgets/jsslider.css deleted file mode 100644 index 19fe1e810f..0000000000 --- a/holoviews/plotting/widgets/jsslider.css +++ /dev/null @@ -1,93 +0,0 @@ -div.hololayout { - display: flex; - align-items: center; - margin: 0; -} - -div.holoframe { - width: 75%; -} - -div.holowell { - display: flex; - align-items: center; -} - -form.holoform { - background-color: #fafafa; - border-radius: 5px; - overflow: hidden; - padding-left: 0.8em; - padding-right: 0.8em; - padding-top: 0.4em; - padding-bottom: 0.4em; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - margin-bottom: 20px; - border: 1px solid #e3e3e3; -} - -div.holowidgets { - padding-right: 0; - width: 25%; -} - -div.holoslider { - min-height: 0 !important; - height: 0.8em; - width: 100%; -} - -div.holoformgroup { - padding-top: 0.5em; - margin-bottom: 0.5em; -} - -div.hologroup { - padding-left: 0; - padding-right: 0.8em; - width: 100%; -} - -.holoselect { - width: 92%; - margin-left: 0; - margin-right: 0; -} - -.holotext { - padding-left: 0.5em; - padding-right: 0; - width: 100%; -} - -.holowidgets .ui-resizable-se { - visibility: hidden -} - -.holoframe > .ui-resizable-se { - visibility: hidden -} - -.holowidgets .ui-resizable-s { - visibility: hidden -} - - -/* CSS rules for noUISlider based slider used by JupyterLab extension */ - -.noUi-handle { - width: 20px !important; - height: 20px !important; - left: -5px !important; - top: -5px !important; -} - -.noUi-handle:before, .noUi-handle:after { - visibility: hidden; - height: 0px; -} - -.noUi-target { - margin-left: 0.5em; - margin-right: 0.5em; -} diff --git a/holoviews/plotting/widgets/jsslider.jinja b/holoviews/plotting/widgets/jsslider.jinja deleted file mode 100644 index 219f84776a..0000000000 --- a/holoviews/plotting/widgets/jsslider.jinja +++ /dev/null @@ -1,33 +0,0 @@ -/* Instantiate the {{ widget_name }} class. */ -/* The IDs given should match those used in the template above. */ -var widget_ids = new Array({{ Nwidget }}); - -{% for dim in dimensions %} -widget_ids[{{ loop.index0 }}] = "_anim_widget{{ id }}_{{ dim }}"; -{% endfor %} - -function create_widget() { - var frame_data = {{ frames | safe }}; - var dim_vals = {{ init_dim_vals }}; - var keyMap = {{ key_data }}; - var notFound = "{{ notFound }}"; - - var anim = new HoloViews.{{ widget_name }}(frame_data, "{{ id }}", widget_ids, - keyMap, dim_vals, notFound, {{ load_json }}, "{{ mode }}", - {{ cached }}, "{{ json_path}}", {{ dynamic }}, "{{ plot_id }}"); - - HoloViews.index['{{ plot_id }}'] = anim; -} - -{% for widget_data in widgets %} - -{% if widget_data['type'] == 'slider' %} -HoloViews.init_slider('{{ id }}', '{{ plot_id }}', '{{ widget_data['dim'] }}', {{ widget_data['vals'] }}, {{ widget_data['next_vals'] }}, {{ widget_data['labels'] }}, {{ dynamic }}, {{ widget_data['step'] }}, {{ widget_data['default'] }}, '{{ widget_data['next_dim'] }}', {{ widget_data['dim_idx'] }}, {{ delay }}, '{{ CDN['jQueryUI'] }}', '{{ CDN['underscore'] }}') -{% elif widget_data['type']=='dropdown' %} -HoloViews.init_dropdown('{{ id }}', '{{ plot_id }}', '{{ widget_data['dim'] }}', {{ widget_data['vals'] }}, {{ widget_data['default'] }}, {{ widget_data['next_vals'] }}, {{ widget_data['labels'] }}, '{{ widget_data['next_dim'] }}', {{ widget_data['dim_idx'] }}, {{ dynamic }}) -{% endif %} - -{% endfor %} - -create_widget(); -{{ init_js }} diff --git a/holoviews/plotting/widgets/widgets.js b/holoviews/plotting/widgets/widgets.js deleted file mode 100644 index e3288d6de9..0000000000 --- a/holoviews/plotting/widgets/widgets.js +++ /dev/null @@ -1,567 +0,0 @@ -function HoloViewsWidget() { -} - -HoloViewsWidget.prototype.init_slider = function(init_val){ - if(this.load_json) { - this.from_json() - } else { - this.update_cache(); - } -} - -HoloViewsWidget.prototype.populate_cache = function(idx){ - this.cache[idx].innerHTML = this.frames[idx]; - if (this.embed) { - delete this.frames[idx]; - } -} - -HoloViewsWidget.prototype.process_error = function(msg){ -} - -HoloViewsWidget.prototype.from_json = function() { - var data_url = this.json_path + this.id + '.json'; - $.getJSON(data_url, $.proxy(function(json_data) { - this.frames = json_data; - this.update_cache(); - this.update(0); - }, this)); -} - -HoloViewsWidget.prototype.dynamic_update = function(current){ - if (current === undefined) { - return - } - this.current = current; - if (this.comm) { - var msg = {comm_id: this.id+'_client', content: current} - this.comm.send(msg); - } -} - -HoloViewsWidget.prototype.update_cache = function(force){ - var frame_len = Object.keys(this.frames).length; - for (var i=0; i 0) { - that.time = Date.now(); - that.dynamic_update(that.queue[that.queue.length-1]); - that.queue = []; - } else { - that.wait = false; - } - if ((msg.msg_type == "Ready") && msg.content) { - console.log("Python callback returned following output:", msg.content); - } else if (msg.msg_type == "Error") { - console.log("Python failed with the following traceback:", msg.traceback) - } - } - var comm = HoloViews.comm_manager.get_client_comm(this.plot_id, this.id+'_client', ack_callback); - return comm - } -} - -HoloViewsWidget.prototype.msg_handler = function(msg) { - var metadata = msg.metadata; - if ((metadata.msg_type == "Ready")) { - if (metadata.content) { - console.log("Python callback returned following output:", metadata.content); - } - return; - } else if (metadata.msg_type == "Error") { - console.log("Python failed with the following traceback:", metadata.traceback) - return - } - this.process_msg(msg) -} - -HoloViewsWidget.prototype.process_msg = function(msg) { -} - -function SelectionWidget(frames, id, slider_ids, keyMap, dim_vals, notFound, load_json, mode, cached, json_path, dynamic, plot_id){ - this.frames = frames; - this.id = id; - this.plot_id = plot_id; - this.slider_ids = slider_ids; - this.keyMap = keyMap - this.current_frame = 0; - this.current_vals = dim_vals; - this.load_json = load_json; - this.mode = mode; - this.notFound = notFound; - this.cached = cached; - this.dynamic = dynamic; - this.cache = {}; - this.json_path = json_path; - this.init_slider(this.current_vals[0]); - this.queue = []; - this.wait = false; - if (!this.cached || this.dynamic) { - this.comm = this.init_comms(); - } -} - -SelectionWidget.prototype = new HoloViewsWidget; - - -SelectionWidget.prototype.get_key = function(current_vals) { - var key = "("; - for (var i=0; i Date.now()))) { - this.queue.push(key); - return - } - this.queue = []; - this.time = Date.now(); - this.current_frame = key; - this.wait = true; - this.dynamic_update(key) - } else if (key !== undefined) { - this.update(key) - } -} - - -/* Define the ScrubberWidget class */ -function ScrubberWidget(frames, num_frames, id, interval, load_json, mode, cached, json_path, dynamic, plot_id){ - this.slider_id = "_anim_slider" + id; - this.loop_select_id = "_anim_loop_select" + id; - this.id = id; - this.plot_id = plot_id; - this.interval = interval; - this.current_frame = 0; - this.direction = 0; - this.dynamic = dynamic; - this.timer = null; - this.load_json = load_json; - this.mode = mode; - this.cached = cached; - this.frames = frames; - this.cache = {}; - this.length = num_frames; - this.json_path = json_path; - document.getElementById(this.slider_id).max = this.length - 1; - this.init_slider(0); - this.wait = false; - this.queue = []; - if (!this.cached || this.dynamic) { - this.comm = this.init_comms() - } -} - -ScrubberWidget.prototype = new HoloViewsWidget; - -ScrubberWidget.prototype.set_frame = function(frame){ - this.current_frame = frame; - var widget = document.getElementById(this.slider_id); - if (widget === null) { - this.pause_animation(); - return - } - widget.value = this.current_frame; - if (this.dynamic || !this.cached) { - if ((this.time !== undefined) && ((this.wait) && ((this.time + 10000) > Date.now()))) { - this.queue.push(frame); - return - } - this.queue = []; - this.time = Date.now(); - this.wait = true; - this.dynamic_update(frame) - } else { - this.update(frame) - } -} - -ScrubberWidget.prototype.get_loop_state = function(){ - var button_group = document[this.loop_select_id].state; - for (var i = 0; i < button_group.length; i++) { - var button = button_group[i]; - if (button.checked) { - return button.value; - } - } - return undefined; -} - - -ScrubberWidget.prototype.next_frame = function() { - this.set_frame(Math.min(this.length - 1, this.current_frame + 1)); -} - -ScrubberWidget.prototype.previous_frame = function() { - this.set_frame(Math.max(0, this.current_frame - 1)); -} - -ScrubberWidget.prototype.first_frame = function() { - this.set_frame(0); -} - -ScrubberWidget.prototype.last_frame = function() { - this.set_frame(this.length - 1); -} - -ScrubberWidget.prototype.slower = function() { - this.interval /= 0.7; - if(this.direction > 0){this.play_animation();} - else if(this.direction < 0){this.reverse_animation();} -} - -ScrubberWidget.prototype.faster = function() { - this.interval *= 0.7; - if(this.direction > 0){this.play_animation();} - else if(this.direction < 0){this.reverse_animation();} -} - -ScrubberWidget.prototype.anim_step_forward = function() { - if(this.current_frame < this.length - 1){ - this.next_frame(); - }else{ - var loop_state = this.get_loop_state(); - if(loop_state == "loop"){ - this.first_frame(); - }else if(loop_state == "reflect"){ - this.last_frame(); - this.reverse_animation(); - }else{ - this.pause_animation(); - this.last_frame(); - } - } -} - -ScrubberWidget.prototype.anim_step_reverse = function() { - if(this.current_frame > 0){ - this.previous_frame(); - } else { - var loop_state = this.get_loop_state(); - if(loop_state == "loop"){ - this.last_frame(); - }else if(loop_state == "reflect"){ - this.first_frame(); - this.play_animation(); - }else{ - this.pause_animation(); - this.first_frame(); - } - } -} - -ScrubberWidget.prototype.pause_animation = function() { - this.direction = 0; - if (this.timer){ - clearInterval(this.timer); - this.timer = null; - } -} - -ScrubberWidget.prototype.play_animation = function() { - this.pause_animation(); - this.direction = 1; - var t = this; - if (!this.timer) this.timer = setInterval(function(){t.anim_step_forward();}, this.interval); -} - -ScrubberWidget.prototype.reverse_animation = function() { - this.pause_animation(); - this.direction = -1; - var t = this; - if (!this.timer) this.timer = setInterval(function(){t.anim_step_reverse();}, this.interval); -} - -function extend(destination, source) { - for (var k in source) { - if (source.hasOwnProperty(k)) { - destination[k] = source[k]; - } - } - return destination; -} - -function update_widget(widget, values) { - if (widget.hasClass("ui-slider")) { - widget.slider('option', { - min: 0, - max: values.length-1, - dim_vals: values, - value: 0, - dim_labels: values - }) - widget.slider('option', 'slide').call(widget, event, {value: 0}) - } else { - widget.empty(); - for (var i=0; i", { - value: i, - text: values[i] - })) - }; - widget.data('values', values); - widget.data('value', 0); - widget.trigger("change"); - }; -} - -function init_slider(id, plot_id, dim, values, next_vals, labels, dynamic, step, value, next_dim, - dim_idx, delay, jQueryUI_CDN, UNDERSCORE_CDN) { - // Slider JS Block START - function loadcssfile(filename){ - var fileref=document.createElement("link") - fileref.setAttribute("rel", "stylesheet") - fileref.setAttribute("type", "text/css") - fileref.setAttribute("href", filename) - document.getElementsByTagName("head")[0].appendChild(fileref) - } - loadcssfile("https://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css"); - /* Check if jQuery and jQueryUI have been loaded - otherwise load with require.js */ - var jQuery = window.jQuery, - // check for old versions of jQuery - oldjQuery = jQuery && !!jQuery.fn.jquery.match(/^1\.[0-4](\.|$)/), - jquery_path = '', - paths = {}, - noConflict; - var jQueryUI = jQuery.ui; - // check for jQuery - if (!jQuery || oldjQuery) { - // load if it's not available or doesn't meet min standards - paths.jQuery = jQuery; - noConflict = !!oldjQuery; - } else { - // register the current jQuery - define('jquery', [], function() { return jQuery; }); - } - if (!jQueryUI) { - paths.jQueryUI = jQueryUI_CDN.slice(null, -3); - } else { - define('jQueryUI', [], function() { return jQuery.ui; }); - } - paths.underscore = UNDERSCORE_CDN.slice(null, -3); - var jquery_require = { - paths: paths, - shim: { - "jQueryUI": { - exports:"$", - deps: ['jquery'] - }, - "underscore": { - exports: '_' - } - } - } - require.config(jquery_require); - require(["jQueryUI", "underscore"], function(jUI, _){ - if (noConflict) $.noConflict(true); - var vals = values; - if (dynamic && vals.constructor === Array) { - var default_value = parseFloat(value); - var min = parseFloat(vals[0]); - var max = parseFloat(vals[vals.length-1]); - var wstep = step; - var wlabels = [default_value]; - var init_label = default_value; - } else { - var min = 0; - if (dynamic) { - var max = Object.keys(vals).length - 1; - var init_label = labels[value]; - var default_value = values[value]; - } else { - var max = vals.length - 1; - var init_label = labels[value]; - var default_value = value; - } - var wstep = 1; - var wlabels = labels; - } - function adjustFontSize(text) { - var width_ratio = (text.parent().width()/8)/text.val().length; - var size = Math.min(0.9, Math.max(0.6, width_ratio))+'em'; - text.css('font-size', size); - } - var slider = $('#_anim_widget'+id+'_'+dim); - slider.slider({ - animate: "fast", - min: min, - max: max, - step: wstep, - value: default_value, - dim_vals: vals, - dim_labels: wlabels, - next_vals: next_vals, - slide: function(event, ui) { - var vals = slider.slider("option", "dim_vals"); - var next_vals = slider.slider("option", "next_vals"); - var dlabels = slider.slider("option", "dim_labels"); - if (dynamic) { - var dim_val = ui.value; - if (vals.constructor === Array) { - var label = ui.value; - } else { - var label = dlabels[ui.value]; - } - } else { - var dim_val = vals[ui.value]; - var label = dlabels[ui.value]; - } - var text = $('#textInput'+id+'_'+dim); - text.val(label); - adjustFontSize(text); - HoloViews.index[plot_id].set_frame(dim_val, dim_idx); - if (Object.keys(next_vals).length > 0) { - var new_vals = next_vals[dim_val]; - var next_widget = $('#_anim_widget'+id+'_'+next_dim); - update_widget(next_widget, new_vals); - } - } - }); - slider.keypress(function(event) { - if (event.which == 80 || event.which == 112) { - var start = slider.slider("option", "value"); - var stop = slider.slider("option", "max"); - for (var i=start; i<=stop; i++) { - var delay = i*delay; - $.proxy(function doSetTimeout(i) { setTimeout($.proxy(function() { - var val = {value:i}; - slider.slider('value',i); - slider.slider("option", "slide")(null, val); - }, slider), delay);}, slider)(i); - } - } - if (event.which == 82 || event.which == 114) { - var start = slider.slider("option", "value"); - var stop = slider.slider("option", "min"); - var count = 0; - for (var i=start; i>=stop; i--) { - var delay = count*delay; - count = count + 1; - $.proxy(function doSetTimeout(i) { setTimeout($.proxy(function() { - var val = {value:i}; - slider.slider('value',i); - slider.slider("option", "slide")(null, val); - }, slider), delay);}, slider)(i); - } - } - }); - var textInput = $('#textInput'+id+'_'+dim) - textInput.val(init_label); - adjustFontSize(textInput); - }); -} - -function init_dropdown(id, plot_id, dim, vals, value, next_vals, labels, next_dim, dim_idx, dynamic) { - var widget = $("#_anim_widget"+id+'_'+dim); - widget.data('values', vals) - for (var i=0; i", { - value: val, - text: labels[i] - })); - }; - widget.data("next_vals", next_vals); - widget.val(value); - widget.on('change', function(event, ui) { - if (dynamic) { - var dim_val = parseInt(this.value); - } else { - var dim_val = $.data(this, 'values')[this.value]; - } - var next_vals = $.data(this, "next_vals"); - if (Object.keys(next_vals).length > 0) { - var new_vals = next_vals[dim_val]; - var next_widget = $('#_anim_widget'+id+'_'+next_dim); - update_widget(next_widget, new_vals); - } - var widgets = HoloViews.index[plot_id] - if (widgets) { - widgets.set_frame(dim_val, dim_idx); - } - }); -} - - -if (window.HoloViews === undefined) { - window.HoloViews = {} - window.PyViz = window.HoloViews -} else if (window.PyViz === undefined) { - window.PyViz = window.HoloViews -} - - -var _namespace = { - init_slider: init_slider, - init_dropdown: init_dropdown, - comms: {}, - comm_status: {}, - index: {}, - plot_index: {}, - kernels: {}, - receivers: {} -} - -for (var k in _namespace) { - if (!(k in window.HoloViews)) { - window.HoloViews[k] = _namespace[k]; - } -} diff --git a/holoviews/tests/plotting/bokeh/testlayoutplot.py b/holoviews/tests/plotting/bokeh/testlayoutplot.py index d7d1116349..9679a182aa 100644 --- a/holoviews/tests/plotting/bokeh/testlayoutplot.py +++ b/holoviews/tests/plotting/bokeh/testlayoutplot.py @@ -2,7 +2,7 @@ from holoviews.core import (HoloMap, GridSpace, Layout, Empty, Dataset, NdOverlay, DynamicMap, Dimension) -from holoviews.element import Curve, Image, Points, HLine, VLine, Path, Histogram +from holoviews.element import Curve, Image, Points, Histogram from holoviews.streams import Stream from .testplot import TestBokehPlot, bokeh_renderer @@ -271,17 +271,7 @@ def test_layout_axis_not_linked_mismatching_unit(self): plot = bokeh_renderer.get_plot(layout) p1, p2 = (sp.subplots['main'] for sp in plot.subplots.values()) self.assertIsNot(p1.handles['y_range'], p2.handles['y_range']) - - def test_dimensioned_streams_with_dynamic_map_overlay_clone(self): - time = Stream.define('Time', time=-3.0)() - def crosshair(time): - return VLine(time) * HLine(time) - crosshair = DynamicMap(crosshair, kdims='time', streams=[time]) - path = Path([]) - t = crosshair * path - html, _ = bokeh_renderer(t) - self.assertIn('Bokeh Application', html) - + def test_dimensioned_streams_with_dynamic_callback_returns_layout(self): stream = Stream.define('aname', aname='a')() def cb(aname): @@ -296,4 +286,4 @@ def cb(aname): stream.event(aname=T) self.assertIn('aname: ' + T, p.handles['title'].text, p.handles['title'].text) p.cleanup() - self.assertEqual(stream._subscribers, []) \ No newline at end of file + self.assertEqual(stream._subscribers, []) diff --git a/holoviews/tests/plotting/bokeh/testrenderer.py b/holoviews/tests/plotting/bokeh/testrenderer.py index 47ccf58709..c756e388cb 100644 --- a/holoviews/tests/plotting/bokeh/testrenderer.py +++ b/holoviews/tests/plotting/bokeh/testrenderer.py @@ -1,45 +1,54 @@ from __future__ import unicode_literals +from collections import OrderedDict from io import BytesIO from unittest import SkipTest import numpy as np +import param -from holoviews import HoloMap, Image, GridSpace, Table, Curve, Store +from holoviews import DynamicMap, HoloMap, Image, GridSpace, Table, Curve, Store +from holoviews.streams import Stream from holoviews.plotting import Renderer from holoviews.element.comparison import ComparisonTestCase +from pyviz_comms import CommManager try: + import panel as pn + from bokeh.io import curdoc from holoviews.plotting.bokeh import BokehRenderer from bokeh.themes.theme import Theme + + from panel.widgets import DiscreteSlider, Player, FloatSlider except: - pass + pn = None class BokehRendererTest(ComparisonTestCase): def setUp(self): - if 'bokeh' not in Store.renderers: - raise SkipTest("Bokeh required to test widgets") + if 'bokeh' not in Store.renderers and pn is not None: + raise SkipTest("Bokeh and Panel required to test 'bokeh' renderer") self.image1 = Image(np.array([[0,1],[2,3]]), label='Image1') self.image2 = Image(np.array([[1,0],[4,-2]]), label='Image2') self.map1 = HoloMap({1:self.image1, 2:self.image2}, label='TestMap') self.renderer = BokehRenderer.instance() self.nbcontext = Renderer.notebook_context - Renderer.notebook_context = False + self.comm_manager = Renderer.comm_manager + with param.logging_level('ERROR'): + Renderer.notebook_context = False + Renderer.comm_manager = CommManager def tearDown(self): - Renderer.notebook_context = self.nbcontext + with param.logging_level('ERROR'): + Renderer.notebook_context = self.nbcontext + Renderer.comm_manager = self.comm_manager def test_save_html(self): bytesio = BytesIO() self.renderer.save(self.image1, bytesio) - def test_export_widgets(self): - bytesio = BytesIO() - self.renderer.export_widgets(self.map1, bytesio, fmt='widgets') - def test_render_get_plot_server_doc(self): renderer = self.renderer.instance(mode='server') plot = renderer.get_plot(self.image1) @@ -78,16 +87,6 @@ def test_get_size_tables_in_layout(self): w, h = self.renderer.get_size(plot) self.assertEqual((w, h), (800, 300)) - def test_render_to_png(self): - curve = Curve([]) - renderer = BokehRenderer.instance(fig='png') - try: - png, info = renderer(curve) - except RuntimeError: - raise SkipTest("Test requires selenium") - self.assertIsInstance(png, bytes) - self.assertEqual(info['file-ext'], 'png') - def test_theme_rendering(self): theme = Theme( json={ @@ -98,7 +97,117 @@ def test_theme_rendering(self): }) self.renderer.theme = theme plot = self.renderer.get_plot(Curve([])) - diff = self.renderer.diff(plot) - events = [e for e in diff.content['events'] if e.get('attr', None) == 'outline_line_color'] - self.assertTrue(bool(events)) - self.assertEqual(events[-1]['new']['value'], '#444444') + self.renderer.components(plot, 'html') + self.assertEqual(plot.state.outline_line_color, '#444444') + + def test_render_to_png(self): + curve = Curve([]) + renderer = BokehRenderer.instance(fig='png') + try: + png, info = renderer(curve) + except RuntimeError: + raise SkipTest("Test requires selenium") + self.assertIsInstance(png, bytes) + self.assertEqual(info['file-ext'], 'png') + + def test_render_static(self): + curve = Curve([]) + obj, _ = self.renderer._validate(curve, None) + self.assertIsInstance(obj, pn.pane.HoloViews) + self.assertEqual(obj.center, True) + self.assertIs(obj.renderer, self.renderer) + self.assertEqual(obj.backend, 'bokeh') + + def test_render_holomap_individual(self): + hmap = HoloMap({i: Curve([1, 2, i]) for i in range(5)}) + obj, _ = self.renderer._validate(hmap, None) + self.assertIsInstance(obj, pn.pane.HoloViews) + self.assertEqual(obj.center, True) + self.assertEqual(obj.widget_location, 'right') + self.assertEqual(obj.widget_type, 'individual') + widgets = obj.layout.select(DiscreteSlider) + self.assertEqual(len(widgets), 1) + slider = widgets[0] + self.assertEqual(slider.options, OrderedDict([(str(i), i) for i in range(5)])) + + def test_render_holomap_embedded(self): + hmap = HoloMap({i: Curve([1, 2, i]) for i in range(5)}) + data, _ = self.renderer.components(hmap) + self.assertIn('State', data['text/html']) + + def test_render_holomap_not_embedded(self): + hmap = HoloMap({i: Curve([1, 2, i]) for i in range(5)}) + data, _ = self.renderer.instance(widget_mode='live').components(hmap) + self.assertNotIn('State', data['text/html']) + + def test_render_holomap_scrubber(self): + hmap = HoloMap({i: Curve([1, 2, i]) for i in range(5)}) + obj, _ = self.renderer._validate(hmap, 'scrubber') + self.assertIsInstance(obj, pn.pane.HoloViews) + self.assertEqual(obj.center, True) + self.assertEqual(obj.widget_location, 'bottom') + self.assertEqual(obj.widget_type, 'scrubber') + widgets = obj.layout.select(Player) + self.assertEqual(len(widgets), 1) + player = widgets[0] + self.assertEqual(player.start, 0) + self.assertEqual(player.end, 4) + + def test_render_holomap_scrubber_fps(self): + hmap = HoloMap({i: Curve([1, 2, i]) for i in range(5)}) + obj, _ = self.renderer.instance(fps=2)._validate(hmap, 'scrubber') + self.assertIsInstance(obj, pn.pane.HoloViews) + widgets = obj.layout.select(Player) + self.assertEqual(len(widgets), 1) + player = widgets[0] + self.assertEqual(player.interval, 500) + + def test_render_holomap_individual_widget_position(self): + hmap = HoloMap({i: Curve([1, 2, i]) for i in range(5)}) + obj, _ = self.renderer.instance(widget_location='top')._validate(hmap, None) + self.assertIsInstance(obj, pn.pane.HoloViews) + self.assertEqual(obj.center, True) + self.assertEqual(obj.widget_location, 'top') + self.assertEqual(obj.widget_type, 'individual') + + def test_render_dynamicmap_with_dims(self): + dmap = DynamicMap(lambda y: Curve([1, 2, y]), kdims=['y']).redim.range(y=(0.1, 5)) + obj, _ = self.renderer._validate(dmap, None) + self.renderer.components(obj) + [(plot, pane)] = obj._plots.values() + cds = plot.handles['cds'] + + self.assertEqual(cds.data['y'][2], 0.1) + slider = obj.layout.select(FloatSlider)[0] + slider.value = 3.1 + self.assertEqual(cds.data['y'][2], 3.1) + + def test_render_dynamicmap_with_stream(self): + stream = Stream.define(str('Custom'), y=2)() + dmap = DynamicMap(lambda y: Curve([1, 2, y]), kdims=['y'], streams=[stream]) + obj, _ = self.renderer._validate(dmap, None) + self.renderer.components(obj) + [(plot, pane)] = obj._plots.values() + cds = plot.handles['cds'] + + self.assertEqual(cds.data['y'][2], 2) + stream.event(y=3) + self.assertEqual(cds.data['y'][2], 3) + + def test_render_dynamicmap_with_stream_dims(self): + stream = Stream.define(str('Custom'), y=2)() + dmap = DynamicMap(lambda x, y: Curve([x, 1, y]), kdims=['x', 'y'], + streams=[stream]).redim.values(x=[1, 2, 3]) + obj, _ = self.renderer._validate(dmap, None) + self.renderer.components(obj) + [(plot, pane)] = obj._plots.values() + cds = plot.handles['cds'] + + self.assertEqual(cds.data['y'][2], 2) + stream.event(y=3) + self.assertEqual(cds.data['y'][2], 3) + + self.assertEqual(cds.data['y'][0], 1) + slider = obj.layout.select(DiscreteSlider)[0] + slider.value = 3 + self.assertEqual(cds.data['y'][0], 3) diff --git a/holoviews/tests/plotting/bokeh/testserver.py b/holoviews/tests/plotting/bokeh/testserver.py index 103d356c22..740d735c07 100644 --- a/holoviews/tests/plotting/bokeh/testserver.py +++ b/holoviews/tests/plotting/bokeh/testserver.py @@ -1,11 +1,15 @@ +import time +import threading + from unittest import SkipTest +from threading import Event from holoviews.core.spaces import DynamicMap from holoviews.core.options import Store from holoviews.element import Curve, Polygons, Path, HLine from holoviews.element.comparison import ComparisonTestCase from holoviews.plotting import Renderer -from holoviews.streams import RangeXY, PlotReset +from holoviews.streams import Stream, RangeXY, PlotReset try: from bokeh.application.handlers import FunctionHandler @@ -13,12 +17,16 @@ from bokeh.client import pull_session from bokeh.document import Document from bokeh.io import curdoc + from bokeh.models import ColumnDataSource from bokeh.server.server import Server from holoviews.plotting.bokeh.callbacks import ( Callback, RangeXYCallback, ResetCallback ) from holoviews.plotting.bokeh.renderer import BokehRenderer + from panel.widgets import DiscreteSlider, FloatSlider + from panel.io.server import StoppableThread + from panel.io.state import state bokeh_renderer = BokehRenderer.instance(mode='server') except: bokeh_renderer = None @@ -39,6 +47,8 @@ def tearDown(self): bokeh_renderer.last_plot = None Callback._callbacks = {} Renderer.notebook_context = self.nbcontext + state.curdoc = None + curdoc().clear() def test_render_server_doc_element(self): obj = Curve([]) @@ -89,43 +99,77 @@ def setUp(self): if not bokeh_renderer: raise SkipTest("Bokeh required to test plot instantiation") Store.current_backend = 'bokeh' + self._loaded = Event() + self._port = None + self._thread = None + self._server = None def tearDown(self): Store.current_backend = self.previous_backend Callback._callbacks = {} - - def test_launch_simple_server(self): - obj = Curve([]) + if self._thread is not None: + try: + self._thread.stop() + except: + pass + state._thread_id = None + if self._server is not None: + try: + self._server.stop() + except: + pass + time.sleep(1) + + def _launcher(self, obj, threaded=False, io_loop=None): + if io_loop: + io_loop.make_current() launched = [] def modify_doc(doc): bokeh_renderer(obj, doc=doc) launched.append(True) - server.stop() handler = FunctionHandler(modify_doc) app = Application(handler) - server = Server({'/': app}, port=0) + server = Server({'/': app}, port=0, io_loop=io_loop) server.start() - url = "http://localhost:" + str(server.port) + "/" - pull_session(session_id='Test', url=url, io_loop=server.io_loop) - self.assertTrue(len(launched)==1) + self._port = server.port + self._server = server + if threaded: + server.io_loop.add_callback(self._loaded.set) + thread = threading.current_thread() + state._thread_id = thread.ident if thread else None + io_loop.start() + else: + url = "http://localhost:" + str(server.port) + "/" + session = pull_session(session_id='Test', url=url, io_loop=server.io_loop) + self.assertTrue(len(launched)==1) + return session, server + return None, server + + def _threaded_launcher(self, obj): + from tornado.ioloop import IOLoop + io_loop = IOLoop() + thread = StoppableThread(target=self._launcher, io_loop=io_loop, + args=(obj, True, io_loop)) + thread.setDaemon(True) + thread.start() + self._loaded.wait() + self._thread = thread + return self.session + + @property + def session(self): + url = "http://localhost:" + str(self._port) + "/" + return pull_session(session_id='Test', url=url) + + def test_launch_simple_server(self): + obj = Curve([]) + self._launcher(obj) def test_launch_server_with_stream(self): obj = Curve([]) stream = RangeXY(source=obj) - launched = [] - def modify_doc(doc): - bokeh_renderer(obj, doc=doc) - launched.append(True) - server.stop() - handler = FunctionHandler(modify_doc) - app = Application(handler) - server = Server({'/': app}, port=0) - server.start() - url = "http://localhost:" + str(server.port) + "/" - pull_session(session_id='Test', url=url, io_loop=server.io_loop) - - self.assertTrue(len(launched)==1) + _, server = self._launcher(obj) cb = bokeh_renderer.last_plot.callbacks[0] self.assertIsInstance(cb, RangeXYCallback) self.assertEqual(cb.streams, [stream]) @@ -135,7 +179,7 @@ def modify_doc(doc): y_range = bokeh_renderer.last_plot.handles['y_range'] self.assertIn(cb.on_change, y_range._callbacks['start']) self.assertIn(cb.on_change, y_range._callbacks['end']) - + server.stop() def test_launch_server_with_complex_plot(self): dmap = DynamicMap(lambda x_range, y_range: Curve([]), streams=[RangeXY()]) @@ -143,15 +187,64 @@ def test_launch_server_with_complex_plot(self): static = Polygons([]) * Path([]) * Curve([]) layout = overlay + static - launched = [] - def modify_doc(doc): - bokeh_renderer(layout, doc=doc) - launched.append(True) - server.stop() - handler = FunctionHandler(modify_doc) - app = Application(handler) - server = Server({'/': app}, port=0) - server.start() - url = "http://localhost:" + str(server.port) + "/" - pull_session(session_id='Test', url=url, io_loop=server.io_loop) - self.assertTrue(len(launched)==1) + _, server = self._launcher(layout) + server.stop() + + def test_server_dynamicmap_with_dims(self): + dmap = DynamicMap(lambda y: Curve([1, 2, y]), kdims=['y']).redim.range(y=(0.1, 5)) + obj, _ = bokeh_renderer._validate(dmap, None) + session = self._threaded_launcher(obj) + [(plot, _)] = obj._plots.values() + [(doc, _)] = obj.layout._documents.items() + + cds = session.document.roots[0].select_one({'type': ColumnDataSource}) + self.assertEqual(cds.data['y'][2], 0.1) + slider = obj.layout.select(FloatSlider)[0] + def run(): + slider.value = 3.1 + doc.add_next_tick_callback(run) + time.sleep(1) + cds = self.session.document.roots[0].select_one({'type': ColumnDataSource}) + self.assertEqual(cds.data['y'][2], 3.1) + + def test_server_dynamicmap_with_stream(self): + stream = Stream.define('Custom', y=2)() + dmap = DynamicMap(lambda y: Curve([1, 2, y]), kdims=['y'], streams=[stream]) + obj, _ = bokeh_renderer._validate(dmap, None) + session = self._threaded_launcher(obj) + [(doc, _)] = obj.layout._documents.items() + + cds = session.document.roots[0].select_one({'type': ColumnDataSource}) + self.assertEqual(cds.data['y'][2], 2) + def run(): + stream.event(y=3) + doc.add_next_tick_callback(run) + time.sleep(1) + cds = self.session.document.roots[0].select_one({'type': ColumnDataSource}) + self.assertEqual(cds.data['y'][2], 3) + + def test_server_dynamicmap_with_stream_dims(self): + stream = Stream.define('Custom', y=2)() + dmap = DynamicMap(lambda x, y: Curve([x, 1, y]), kdims=['x', 'y'], + streams=[stream]).redim.values(x=[1, 2, 3]) + obj, _ = bokeh_renderer._validate(dmap, None) + session = self._threaded_launcher(obj) + [(doc, _)] = obj.layout._documents.items() + + orig_cds = session.document.roots[0].select_one({'type': ColumnDataSource}) + self.assertEqual(orig_cds.data['y'][2], 2) + def run(): + stream.event(y=3) + doc.add_next_tick_callback(run) + time.sleep(1) + cds = self.session.document.roots[0].select_one({'type': ColumnDataSource}) + self.assertEqual(cds.data['y'][2], 3) + + self.assertEqual(orig_cds.data['y'][0], 1) + slider = obj.layout.select(DiscreteSlider)[0] + def run(): + slider.value = 3 + doc.add_next_tick_callback(run) + time.sleep(1) + cds = self.session.document.roots[0].select_one({'type': ColumnDataSource}) + self.assertEqual(cds.data['y'][0], 3) diff --git a/holoviews/tests/plotting/bokeh/testwidgets.py b/holoviews/tests/plotting/bokeh/testwidgets.py deleted file mode 100644 index a9843d2053..0000000000 --- a/holoviews/tests/plotting/bokeh/testwidgets.py +++ /dev/null @@ -1,439 +0,0 @@ -from unittest import SkipTest - -import numpy as np - -from holoviews import renderer -from holoviews.core import Dimension, NdMapping, DynamicMap, HoloMap -from holoviews.element import Curve -from holoviews.element.comparison import ComparisonTestCase - -try: - from holoviews.plotting.bokeh.widgets import BokehServerWidgets - from bokeh.models.widgets import Select, Slider, AutocompleteInput, TextInput, Div - bokeh_renderer = renderer('bokeh') -except: - BokehServerWidgets = None - - -class TestBokehServerWidgets(ComparisonTestCase): - - def setUp(self): - if not BokehServerWidgets: - raise SkipTest("Bokeh required to test BokehServerWidgets") - - def test_bokeh_widgets_server_mode(self): - dmap = DynamicMap(lambda X: Curve([]), kdims=['X']).redim.range(X=(0, 5)) - widgets = bokeh_renderer.instance(mode='server').get_widget(dmap, None) - div, widget = widgets.widgets['X'] - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 0) - self.assertEqual(widget.start, 0) - self.assertEqual(widget.end, 5) - self.assertEqual(widget.step, 1) - self.assertEqual(widgets.state.sizing_mode, 'fixed') - - def test_bokeh_server_dynamic_range_int(self): - dim = Dimension('x', range=(3, 11)) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 3) - self.assertEqual(widget.start, 3) - self.assertEqual(widget.end, 11) - self.assertEqual(widget.step, 1) - self.assertIsInstance(label, TextInput) - self.assertEqual(label.title, dim.pprint_label) - self.assertEqual(label.value, '3') - self.assertIs(mapping, None) - - def test_bokeh_server_dynamic_range_float(self): - dim = Dimension('x', range=(3.1, 11.2)) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 3.1) - self.assertEqual(widget.start, 3.1) - self.assertEqual(widget.end, 11.2) - self.assertEqual(widget.step, 0.01) - self.assertIsInstance(label, TextInput) - self.assertEqual(label.title, dim.pprint_label) - self.assertEqual(label.value, '3.1') - self.assertIs(mapping, None) - - def test_bokeh_server_dynamic_range_float_step(self): - dim = Dimension('x', range=(3.1, 11.2), step=0.1) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 3.1) - self.assertEqual(widget.start, 3.1) - self.assertEqual(widget.end, 11.2) - self.assertEqual(widget.step, 0.1) - self.assertIsInstance(label, TextInput) - self.assertEqual(label.title, dim.pprint_label) - self.assertEqual(label.value, '3.1') - self.assertIs(mapping, None) - - def test_bokeh_server_dynamic_range_not_editable(self): - dim = Dimension('x', range=(3.1, 11.2)) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=False) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 3.1) - self.assertEqual(widget.start, 3.1) - self.assertEqual(widget.end, 11.2) - self.assertEqual(widget.step, 0.01) - self.assertIsInstance(label, Div) - self.assertEqual(label.text, '%s' % dim.pprint_value_string(3.1)) - self.assertIs(mapping, None) - - def test_bokeh_server_dynamic_values_int(self): - values = list(range(3, 11)) - dim = Dimension('x', values=values) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 0) - self.assertEqual(widget.start, 0) - self.assertEqual(widget.end, 7) - self.assertEqual(widget.step, 1) - self.assertIsInstance(label, AutocompleteInput) - self.assertEqual(label.title, dim.pprint_label) - self.assertEqual(label.value, '3') - self.assertEqual(mapping, [(i, (v, dim.pprint_value(v))) for i, v in enumerate(values)]) - - def test_bokeh_server_dynamic_values_float_not_editable(self): - values = list(np.linspace(3.1, 11.2, 7)) - dim = Dimension('x', values=values) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=False) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 0) - self.assertEqual(widget.start, 0) - self.assertEqual(widget.end, 6) - self.assertEqual(widget.step, 1) - self.assertIsInstance(label, Div) - self.assertEqual(label.text, '%s' % dim.pprint_value_string(3.1)) - self.assertEqual(mapping, [(i, (v, dim.pprint_value(v))) for i, v in enumerate(values)]) - - def test_bokeh_server_dynamic_values_float_editable(self): - values = list(np.linspace(3.1, 11.2, 7)) - dim = Dimension('x', values=values) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 0) - self.assertEqual(widget.start, 0) - self.assertEqual(widget.end, 6) - self.assertEqual(widget.step, 1) - self.assertIsInstance(label, AutocompleteInput) - self.assertEqual(label.title, dim.pprint_label) - self.assertEqual(label.value, '3.1') - self.assertEqual(mapping, [(i, (v, dim.pprint_value(v))) for i, v in enumerate(values)]) - - def test_bokeh_server_dynamic_values_str_1(self): - values = [chr(65+i) for i in range(10)] - dim = Dimension('x', values=values) - widget, label, mapping = BokehServerWidgets.create_widget(dim, editable=True) - self.assertIsInstance(widget, Select) - self.assertEqual(widget.value, 'A') - self.assertEqual(widget.options, list(zip(values, values))) - self.assertEqual(widget.title, dim.pprint_label) - self.assertIs(mapping, None) - self.assertIs(label, None) - - def test_bokeh_server_dynamic_values_str_2(self): - keys = [chr(65+i) for i in range(10)] - ndmap = NdMapping({i: None for i in keys}, kdims=['x']) - dim = Dimension('x') - widget, label, mapping = BokehServerWidgets.create_widget(dim, ndmap, editable=True) - self.assertIsInstance(widget, Select) - self.assertEqual(widget.value, 'A') - self.assertEqual(widget.options, list(zip(keys, keys))) - self.assertEqual(widget.title, dim.pprint_label) - self.assertEqual(mapping, list(enumerate(zip(keys, keys)))) - - def test_bokeh_server_static_numeric_values(self): - dim = Dimension('x') - ndmap = NdMapping({i: None for i in range(3, 12)}, kdims=['x']) - widget, label, mapping = BokehServerWidgets.create_widget(dim, ndmap, editable=True) - self.assertIsInstance(widget, Slider) - self.assertEqual(widget.value, 0) - self.assertEqual(widget.start, 0) - self.assertEqual(widget.end, 8) - self.assertEqual(widget.step, 1) - self.assertIsInstance(label, AutocompleteInput) - self.assertEqual(label.title, dim.pprint_label) - self.assertEqual(label.value, '3') - self.assertEqual(mapping, [(i, (k, dim.pprint_value(k))) for i, k in enumerate(ndmap.keys())]) - - - -class TestSelectionWidget(ComparisonTestCase): - - def setUp(self): - if not BokehServerWidgets: - raise SkipTest("Bokeh required to test BokehServerWidgets") - - def test_holomap_slider(self): - hmap = HoloMap({i: Curve([1, 2, 3]) for i in range(10)}, 'X') - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - slider = widgets[0] - self.assertEqual(slider['type'], 'slider') - self.assertEqual(slider['dim'], 'X') - self.assertEqual(slider['dim_idx'], 0) - self.assertEqual(slider['vals'], repr([repr(float(v)) for v in range(10)])) - self.assertEqual(slider['labels'], repr([str(v) for v in range(10)])) - self.assertEqual(slider['step'], 1) - self.assertEqual(slider['default'], 0) - self.assertIs(slider['next_dim'], None) - self.assertEqual(dimensions, ['X']) - self.assertEqual(init_dim_vals, [0.0]) - - def test_holomap_cftime_slider(self): - try: - import cftime - except: - raise SkipTest('Test requires cftime library') - dates = [cftime.DatetimeGregorian(2000, 2, 28), - cftime.DatetimeGregorian(2000, 3, 1), - cftime.DatetimeGregorian(2000, 3, 2)] - hmap = HoloMap({d: Curve([1, 2, i]) for i, d in enumerate(dates)}, 'Date') - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - slider = widgets[0] - self.assertEqual(slider['type'], 'slider') - self.assertEqual(slider['dim'], 'Date') - self.assertEqual(slider['dim_idx'], 0) - self.assertEqual(slider['vals'], repr([str(d) for d in dates])) - self.assertEqual(slider['labels'], repr([str(d) for d in dates])) - self.assertEqual(slider['step'], 1) - self.assertEqual(slider['default'], 0) - self.assertIs(slider['next_dim'], None) - self.assertEqual(dimensions, ['Date']) - self.assertEqual(init_dim_vals, ['2000-02-28 00:00:00']) - - def test_holomap_slider_unsorted(self): - data = {(i, j): Curve([1, 2, 3]) for i in range(3) for j in range(3)} - del data[2, 2] - hmap = HoloMap(data, ['X', 'Y']) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 2) - slider = widgets[1] - self.assertEqual(slider['vals'], repr([repr(float(v)) for v in range(3)])) - self.assertEqual(slider['labels'], repr([str(v) for v in range(3)])) - - def test_holomap_slider_unsorted_initialization(self): - data = [(3, Curve([3, 2, 1])), (1, Curve([1, 2, 3]))] - hmap = HoloMap(data, ['X'], sort=False) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - slider = widgets[0] - self.assertEqual(slider['vals'], "['1.0', '3.0']") - self.assertEqual(slider['labels'], "['1', '3']") - - def test_holomap_slider_unsorted_datetime_values_initialization(self): - hmap = HoloMap([(np.datetime64(10005, 'D'), Curve([1, 2, 3])), - (np.datetime64(10000, 'D'), Curve([1, 2, 4]))], sort=False) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets() - self.assertEqual(widgets.plot.current_key, (np.datetime64(10000, 'D'),)) - self.assertEqual(widgets.plot.current_frame, hmap[np.datetime64(10000, 'D')]) - - def test_holomap_dropdown(self): - hmap = HoloMap({chr(65+i): Curve([1, 2, 3]) for i in range(10)}, 'X') - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - dropdown = widgets[0] - self.assertEqual(dropdown['type'], 'dropdown') - self.assertEqual(dropdown['dim'], 'X') - self.assertEqual(dropdown['dim_idx'], 0) - self.assertEqual(dropdown['vals'], repr([chr(65+v) for v in range(10)])) - self.assertEqual(dropdown['labels'], repr([chr(65+v) for v in range(10)])) - self.assertEqual(dropdown['step'], 1) - self.assertEqual(dropdown['default'], 0) - self.assertIs(dropdown['next_dim'], None) - self.assertEqual(dimensions, ['X']) - self.assertEqual(init_dim_vals, ['A']) - - def test_holomap_slider_and_dropdown(self): - hmap = HoloMap({(i, chr(65+i)): Curve([1, 2, 3]) for i in range(10)}, ['X', 'Y']) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 2) - - slider = widgets[0] - self.assertEqual(slider['type'], 'slider') - self.assertEqual(slider['dim'], 'X') - self.assertEqual(slider['dim_idx'], 0) - self.assertEqual(slider['vals'], repr([repr(float(v)) for v in range(10)])) - self.assertEqual(slider['labels'], repr([str(v) for v in range(10)])) - self.assertEqual(slider['step'], 1) - self.assertEqual(slider['default'], 0) - self.assertEqual(slider['next_dim'], Dimension('Y')) - self.assertEqual(eval(slider['next_vals']), - {str(float(i)): [chr(65+i)] for i in range(10)}) - - dropdown = widgets[1] - self.assertEqual(dropdown['type'], 'dropdown') - self.assertEqual(dropdown['dim'], 'Y') - self.assertEqual(dropdown['dim_idx'], 1) - self.assertEqual(dropdown['vals'], repr([chr(65+v) for v in range(10)])) - self.assertEqual(dropdown['labels'], repr([chr(65+v) for v in range(10)])) - self.assertEqual(dropdown['step'], 1) - self.assertEqual(dropdown['default'], 0) - self.assertIs(dropdown['next_dim'], None) - - self.assertEqual(dimensions, ['X', 'Y']) - self.assertEqual(init_dim_vals, [0.0, 'A']) - - def test_dynamicmap_int_range_slider(self): - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[Dimension('X', range=(0, 5))]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - slider = widgets[0] - self.assertEqual(slider['type'], 'slider') - self.assertEqual(slider['dim'], 'X') - self.assertEqual(slider['dim_idx'], 0) - self.assertEqual(slider['vals'], "['0.0', '5.0']") - self.assertEqual(slider['labels'], []) - self.assertEqual(slider['step'], 1) - self.assertEqual(slider['default'], 0) - self.assertIs(slider['next_dim'], None) - self.assertEqual(dimensions, ['X']) - self.assertEqual(init_dim_vals, [0.0]) - - def test_dynamicmap_float_range_slider(self): - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[Dimension('X', range=(0., 5.))]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - slider = widgets[0] - self.assertEqual(slider['type'], 'slider') - self.assertEqual(slider['dim'], 'X') - self.assertEqual(slider['dim_idx'], 0) - self.assertEqual(slider['vals'], "['0.0', '5.0']") - self.assertEqual(slider['labels'], []) - self.assertEqual(slider['step'], 0.01) - self.assertEqual(slider['default'], 0.0) - self.assertIs(slider['next_dim'], None) - self.assertEqual(dimensions, ['X']) - self.assertEqual(init_dim_vals, [0.0]) - - def test_dynamicmap_float_range_slider_with_step(self): - dimension = Dimension('X', range=(0., 5.), step=0.05) - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[dimension]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(widgets[0]['step'], 0.05) - - def test_dynamicmap_int_range_slider_with_step(self): - dimension = Dimension('X', range=(0, 10), step=2) - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[dimension]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(widgets[0]['step'], 2) - - def test_dynamicmap_values_slider(self): - dimension = Dimension('X', values=list(range(10))) - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[dimension]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - slider = widgets[0] - self.assertEqual(slider['type'], 'slider') - self.assertEqual(slider['dim'], 'X') - self.assertEqual(slider['dim_idx'], 0) - self.assertEqual(slider['vals'], {i: i for i in range(10)}) - self.assertEqual(slider['labels'], repr([str(i) for i in range(10)])) - self.assertEqual(slider['step'], 1) - self.assertEqual(slider['default'], 0) - self.assertIs(slider['next_dim'], None) - self.assertEqual(dimensions, ['X']) - self.assertEqual(init_dim_vals, [0.0]) - - def test_dynamicmap_values_dropdown(self): - values = [chr(65+i) for i in range(10)] - dimension = Dimension('X', values=values) - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[dimension]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(len(widgets), 1) - dropdown = widgets[0] - self.assertEqual(dropdown['type'], 'dropdown') - self.assertEqual(dropdown['dim'], 'X') - self.assertEqual(dropdown['dim_idx'], 0) - self.assertEqual(dropdown['vals'], list(range(10))) - self.assertEqual(dropdown['labels'], repr(values)) - self.assertEqual(dropdown['step'], 1) - self.assertEqual(dropdown['default'], 0) - self.assertIs(dropdown['next_dim'], None) - self.assertEqual(dimensions, ['X']) - self.assertEqual(init_dim_vals, [0.0]) - - def test_dynamicmap_values_default(self): - values = [chr(65+i) for i in range(10)] - dimension = Dimension('X', values=values, default='C') - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[dimension]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(widgets[0]['default'], '2') - self.assertEqual(init_dim_vals, ['2']) - - def test_dynamicmap_range_default(self): - dimension = Dimension('X', range=(0., 5.), default=0.05) - hmap = DynamicMap(lambda x: Curve([1, 2, 3]), kdims=[dimension]) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(widgets[0]['default'], 0.05) - self.assertEqual(init_dim_vals, [0.05]) - - def test_holomap_slider_default(self): - dim = Dimension('X', default=3) - hmap = HoloMap({i: Curve([1, 2, 3]) for i in range(1, 9)}, dim) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(widgets[0]['default'], '2') - self.assertEqual(init_dim_vals, [3.0]) - - def test_holomap_slider_bad_default(self): - dim = Dimension('X', default=42) - hmap = HoloMap({i: Curve([1, 2, 3]) for i in range(1, 9)}, dim) - with self.assertRaises(ValueError): - bokeh_renderer.get_widget(hmap, 'widgets').get_widgets() - - def test_holomap_dropdown_default(self): - dim = Dimension('X', default='C') - hmap = HoloMap({chr(65+i): Curve([1, 2, 3]) for i in range(10)}, dim) - widgets = bokeh_renderer.get_widget(hmap, 'widgets') - widgets, dimensions, init_dim_vals = widgets.get_widgets() - self.assertEqual(widgets[0]['default'], '2') - self.assertEqual(init_dim_vals, ['C']) - - def test_holomap_dropdown_bad_default(self): - dim = Dimension('X', default='Z') - hmap = HoloMap({chr(65+i): Curve([1, 2, 3]) for i in range(10)}, dim) - with self.assertRaises(ValueError): - bokeh_renderer.get_widget(hmap, 'widgets').get_widgets() - - def test_dynamicmap_default_value_slider_plot_initialization(self): - dims = [Dimension('N', default=5, range=(0, 10))] - dmap = DynamicMap(lambda N: Curve([1, N, 5]), kdims=dims) - widgets = bokeh_renderer.get_widget(dmap, 'widgets') - widgets.get_widgets() - self.assertEqual(widgets.plot.current_key, (5,)) - - def test_dynamicmap_unsorted_numeric_values_slider_plot_initialization(self): - dims = [Dimension('N', values=[10, 5, 0])] - dmap = DynamicMap(lambda N: Curve([1, N, 5]), kdims=dims) - widgets = bokeh_renderer.get_widget(dmap, 'widgets') - widgets.get_widgets() - self.assertEqual(widgets.plot.current_key, (0,)) - - def test_dynamicmap_unsorted_numeric_values_slider_plot_update(self): - dims = [Dimension('N', values=[10, 5, 0])] - dmap = DynamicMap(lambda N: Curve([1, N, 5]), kdims=dims) - widgets = bokeh_renderer.get_widget(dmap, 'widgets') - widgets.get_widgets() - widgets.update((2,)) - self.assertEqual(widgets.plot.current_key, (10,)) diff --git a/holoviews/tests/plotting/matplotlib/testrenderer.py b/holoviews/tests/plotting/matplotlib/testrenderer.py index c6360e862a..58443479d6 100644 --- a/holoviews/tests/plotting/matplotlib/testrenderer.py +++ b/holoviews/tests/plotting/matplotlib/testrenderer.py @@ -8,17 +8,26 @@ import sys import subprocess +from collections import OrderedDict from unittest import SkipTest import numpy as np +import param -from holoviews import HoloMap, Image, ItemTable, Store, GridSpace, Table +from holoviews import (DynamicMap, HoloMap, Image, ItemTable, Store, + GridSpace, Table, Curve) from holoviews.element.comparison import ComparisonTestCase +from holoviews.streams import Stream +from pyviz_comms import CommManager try: - from holoviews.plotting.mpl import MPLRenderer + import panel as pn + + from holoviews.plotting.mpl import MPLRenderer, CurvePlot + from holoviews.plotting.renderer import Renderer + from panel.widgets import DiscreteSlider, Player, FloatSlider except: - pass + pn = None class MPLRendererTest(ComparisonTestCase): @@ -28,8 +37,8 @@ class MPLRendererTest(ComparisonTestCase): """ def setUp(self): - if 'matplotlib' not in Store.renderers: - raise SkipTest("Matplotlib required to test widgets") + if 'matplotlib' not in Store.renderers and pn is not None: + raise SkipTest("Matplotlib and Panel required to test rendering.") self.basename = 'no-file' self.image1 = Image(np.array([[0,1],[2,3]]), label='Image1') @@ -40,6 +49,16 @@ def setUp(self): label='Poincaré', group='α Festkörperphysik') self.renderer = MPLRenderer.instance() + self.nbcontext = Renderer.notebook_context + self.comm_manager = Renderer.comm_manager + with param.logging_level('ERROR'): + Renderer.notebook_context = False + Renderer.comm_manager = CommManager + + def tearDown(self): + with param.logging_level('ERROR'): + Renderer.notebook_context = self.nbcontext + Renderer.comm_manager = self.comm_manager def test_get_size_single_plot(self): plot = self.renderer.get_plot(self.image1) @@ -83,3 +102,110 @@ def test_render_mp4(self): raise SkipTest('ffmpeg not available, skipping mp4 export test') data, metadata = self.renderer.components(self.map1, 'mp4') self.assertIn("