From b1f921f27b0c5bdab0a0b8caa67cf0c95843e266 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 6 Mar 2019 13:04:19 +0000 Subject: [PATCH] Moved panes into subpackage --- examples/user_guide/Panes.ipynb | 2 - panel/__init__.py | 6 +- panel/interact.py | 9 +- panel/links.py | 2 +- panel/models/__init__.py | 9 + panel/models/plots.py | 39 ++ panel/pane.py | 689 -------------------------------- panel/pane/__init__.py | 45 +++ panel/pane/base.py | 175 ++++++++ panel/pane/equation.py | 111 +++++ panel/{ => pane}/holoviews.py | 12 +- panel/pane/image.py | 157 ++++++++ panel/pane/markup.py | 133 ++++++ panel/pane/plot.py | 140 +++++++ panel/{ => pane}/plotly.py | 28 +- panel/{ => pane}/vega.py | 25 +- panel/param.py | 9 +- panel/pipeline.py | 3 +- panel/tests/test_holoviews.py | 3 +- panel/tests/test_links.py | 4 +- panel/tests/test_plotly.py | 5 +- panel/tests/test_vega.py | 5 +- panel/util.py | 15 +- panel/widgets.py | 9 +- 24 files changed, 863 insertions(+), 772 deletions(-) create mode 100644 panel/models/plots.py delete mode 100644 panel/pane.py create mode 100644 panel/pane/__init__.py create mode 100644 panel/pane/base.py create mode 100644 panel/pane/equation.py rename panel/{ => pane}/holoviews.py (97%) create mode 100644 panel/pane/image.py create mode 100644 panel/pane/markup.py create mode 100644 panel/pane/plot.py rename panel/{ => pane}/plotly.py (82%) rename panel/{ => pane}/vega.py (83%) diff --git a/examples/user_guide/Panes.ipynb b/examples/user_guide/Panes.ipynb index a7b6a71b9e..d3cb1aafcc 100644 --- a/examples/user_guide/Panes.ipynb +++ b/examples/user_guide/Panes.ipynb @@ -7,8 +7,6 @@ "outputs": [], "source": [ "import panel as pn\n", - "import panel.vega\n", - "import panel.plotly\n", "\n", "pn.extension()" ] diff --git a/panel/__init__.py b/panel/__init__.py index 22b288a52e..77fbba5f49 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -5,14 +5,12 @@ import param as _param from bokeh.document import Document as _Document -from . import holoviews # noqa from . import layout # noqa +from . import links # noqa +from . import pane # noqa from . import param # noqa from . import pipeline # noqa -from . import plotly # noqa -from . import vega # noqa from . import widgets # noqa -from . import links # noqa from .interact import interact # noqa from .layout import Row, Column, Tabs, Spacer # noqa diff --git a/panel/interact.py b/panel/interact.py index aaa873ae18..fdb6e1948d 100644 --- a/panel/interact.py +++ b/panel/interact.py @@ -11,10 +11,11 @@ from __future__ import absolute_import, division, unicode_literals import types -from numbers import Real, Integral -from collections import OrderedDict +from collections import OrderedDict from inspect import getcallargs +from numbers import Real, Integral +from six import string_types try: # Python >= 3.3 from inspect import signature, Parameter @@ -37,7 +38,7 @@ from .layout import Panel, Column, Row from .pane import PaneBase, Pane, HTML -from .util import basestring, as_unicode +from .util import as_unicode from .widgets import (Checkbox, TextInput, Widget, IntSlider, FloatSlider, Select, DiscreteSlider, Button) @@ -280,7 +281,7 @@ def widget_from_abbrev(cls, abbrev, name, default=empty): @staticmethod def widget_from_single_value(o, name): """Make widgets from single values, which can be used as parameter defaults.""" - if isinstance(o, basestring): + if isinstance(o, string_types): return TextInput(value=as_unicode(o), name=name) elif isinstance(o, bool): return Checkbox(value=o, name=name) diff --git a/panel/links.py b/panel/links.py index 381a992e69..62740cb4f4 100644 --- a/panel/links.py +++ b/panel/links.py @@ -8,7 +8,7 @@ import sys from .layout import Viewable, Panel -from .holoviews import HoloViews, generate_panel_bokeh_map, is_bokeh_element_plot +from .pane.holoviews import HoloViews, generate_panel_bokeh_map, is_bokeh_element_plot from .util import unicode_repr from bokeh.models import (CustomJS, Model as BkModel) diff --git a/panel/models/__init__.py b/panel/models/__init__.py index e69de29bb2..023ad0e05d 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -0,0 +1,9 @@ +""" +The models module defines custom bokeh models which extend upon the +functionality that is provided in bokeh by default. The models are +defined as pairs of Python classes and TypeScript models defined in .ts +files. +""" + +from .plots import PlotlyPlot, VegaPlot # noqa +from .widgets import Audio, FileInput, Player # noqa diff --git a/panel/models/plots.py b/panel/models/plots.py new file mode 100644 index 0000000000..f058e20048 --- /dev/null +++ b/panel/models/plots.py @@ -0,0 +1,39 @@ +""" +Defines custom bokeh models to render external Javascript based plots. +""" +import os + +from bokeh.core.properties import Dict, String, List, Any, Instance +from bokeh.models import LayoutDOM, ColumnDataSource + +from ..util import CUSTOM_MODELS + + +class PlotlyPlot(LayoutDOM): + """ + A bokeh model that wraps around a plotly plot and renders it inside + a bokeh plot. + """ + + __implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'plotly.ts') + + data = Dict(String, Any) + + data_sources = List(Instance(ColumnDataSource)) + + +class VegaPlot(LayoutDOM): + """ + A Bokeh model that wraps around a Vega plot and renders it inside + a Bokeh plot. + """ + + __implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vega.ts') + + data = Dict(String, Any) + + data_sources = Dict(String, Instance(ColumnDataSource)) + + +CUSTOM_MODELS['panel.plotly.PlotlyPlot'] = PlotlyPlot +CUSTOM_MODELS['panel.vega.VegaPlot'] = VegaPlot diff --git a/panel/pane.py b/panel/pane.py deleted file mode 100644 index cb1f26a2dd..0000000000 --- a/panel/pane.py +++ /dev/null @@ -1,689 +0,0 @@ -""" -Panes allow wrapping external objects and rendering them as part of -a dashboard. -""" -from __future__ import absolute_import, division, unicode_literals - -import os -import sys -import base64 -from io import BytesIO - -try: - from html import escape -except: - from cgi import escape - -import param - -from bokeh.models import LayoutDOM, CustomJS, Div as _BkDiv - -from .layout import Panel, Row -from .util import basestring, param_reprs, push, remove_root -from .viewable import Viewable, Reactive, Layoutable - - -def panel(obj, **kwargs): - """ - Creates a panel from any supplied object by wrapping it in a pane - and returning a corresponding Panel. - - Arguments - --------- - obj: object - Any object to be turned into a Panel - **kwargs: dict - Any keyword arguments to be passed to the applicable Pane - - Returns - ------- - layout: Viewable - A Viewable representation of the input object - """ - if isinstance(obj, Viewable): - return obj - if kwargs.get('name', False) is None: - kwargs.pop('name') - pane = PaneBase.get_pane_type(obj)(obj, **kwargs) - if len(pane.layout) == 1 and pane._unpack: - return pane.layout[0] - return pane.layout - - -def Pane(obj, **kwargs): - """ - Converts any object to a Pane if a matching Pane class exists. - """ - if isinstance(obj, Viewable): - return obj - return PaneBase.get_pane_type(obj)(obj, **kwargs) - - -class PaneBase(Reactive): - """ - PaneBase is the abstract baseclass for all atomic displayable units - in the panel library. Pane defines an extensible interface for - wrapping arbitrary objects and transforming them into Bokeh models. - - Panes are reactive in the sense that when the object they are - wrapping is changed any dashboard containing the pane will update - in response. - - To define a concrete Pane type subclass this class and implement - the applies classmethod and the _get_model private method. - """ - - default_layout = param.ClassSelector(default=Row, class_=(Panel), - is_instance=False) - - object = param.Parameter(default=None, doc=""" - The object being wrapped, which will be converted into a Bokeh model.""") - - # When multiple Panes apply to an object, the one with the highest - # numerical priority is selected. The default is an intermediate value. - # If set to None, applies method will be called to get a priority - # value for a specific object type. - priority = 0.5 - - # Declares whether Pane supports updates to the Bokeh model - _updates = False - - # Whether the Pane layout can be safely unpacked - _unpack = True - - __abstract = True - - @classmethod - def applies(cls, obj): - """ - Given the object return a boolean indicating whether the Pane - can render the object. If the priority of the pane is set to - None, this method may also be used to define a priority - depending on the object being rendered. - """ - return None - - @classmethod - def get_pane_type(cls, obj): - if isinstance(obj, Viewable): - return type(obj) - descendents = [] - for p in param.concrete_descendents(PaneBase).values(): - priority = p.applies(obj) if p.priority is None else p.priority - if isinstance(priority, bool) and priority: - raise ValueError('If a Pane declares no priority ' - 'the applies method should return a ' - 'priority value specific to the ' - 'object type or False, but the %s pane ' - 'declares no priority.' % p.__name__) - elif priority is None or priority is False: - continue - descendents.append((priority, p)) - pane_types = reversed(sorted(descendents, key=lambda x: x[0])) - for _, pane_type in pane_types: - applies = pane_type.applies(obj) - if isinstance(applies, bool) and not applies: continue - return pane_type - raise TypeError('%s type could not be rendered.' % type(obj).__name__) - - def __init__(self, object, **params): - applies = self.applies(object) - if isinstance(applies, bool) and not applies: - raise ValueError("%s pane does not support objects of type '%s'" % - (type(self).__name__, type(object).__name__)) - - super(PaneBase, self).__init__(object=object, **params) - kwargs = {k: v for k, v in params.items() if k in Layoutable.param} - self.layout = self.default_layout(self, **kwargs) - - def __repr__(self, depth=0): - cls = type(self).__name__ - params = param_reprs(self, ['object']) - obj = type(self.object).__name__ - template = '{cls}({obj}, {params})' if params else '{cls}({obj})' - return template.format(cls=cls, params=', '.join(params), obj=obj) - - def __getitem__(self, index): - """ - Allows pane objects to behave like the underlying layout - """ - return self.layout[index] - - def _get_root(self, doc, comm=None): - if self._updates: - root = self._get_model(doc, comm=comm) - else: - root = self.layout._get_model(doc, comm=comm) - self._preprocess(root) - return root - - def _cleanup(self, root=None, final=False): - super(PaneBase, self)._cleanup(root, final) - if final: - self.object = None - - def _update(self, model): - """ - If _updates=True this method is used to update an existing Bokeh - model instead of replacing the model entirely. The supplied model - should be updated with the current state. - """ - raise NotImplementedError - - def _link_object(self, doc, root, parent, comm=None): - """ - Links the object parameter to the rendered Bokeh model, triggering - an update when the object changes. - """ - from . import state - ref = root.ref['id'] - - def update_pane(change): - old_model = self._models[ref] - - if self._updates: - # Pane supports model updates - def update_models(): - self._update(old_model) - else: - # Otherwise replace the whole model - new_model = self._get_model(doc, root, parent, comm) - def update_models(): - try: - index = parent.children.index(old_model) - except IndexError: - self.warning('%s pane model %s could not be replaced ' - 'with new model %s, ensure that the ' - 'parent is not modified at the same ' - 'time the panel is being updated.' % - (type(self).__name__, old_model, new_model)) - else: - parent.children[index] = new_model - - if comm: - update_models() - push(doc, comm) - elif state.curdoc: - update_models() - else: - doc.add_next_tick_callback(update_models) - - if ref not in self._callbacks: - self._callbacks[ref].append(self.param.watch(update_pane, 'object')) - - -class Bokeh(PaneBase): - """ - Bokeh panes allow including any Bokeh model in a panel. - """ - - priority = 0.8 - - @classmethod - def applies(cls, obj): - return isinstance(obj, LayoutDOM) - - def _get_model(self, doc, root=None, parent=None, comm=None): - if root is None: - return self._get_root(doc, comm) - - model = self.object - ref = root.ref['id'] - for js in model.select({'type': CustomJS}): - js.code = js.code.replace(model.ref['id'], ref) - - if model._document and doc is not model._document: - remove_root(model, doc) - - self._models[ref] = model - self._link_object(doc, root, parent, comm) - return model - - - -class DivPaneBase(PaneBase): - """ - Baseclass for Panes which render HTML inside a Bokeh Div. - See the documentation for Bokeh Div for more detail about - the supported options like style and sizing_mode. - """ - - # DivPane supports updates to the model - _updates = True - - __abstract = True - - style = param.Dict(default=None, doc=""" - Dictionary of CSS property:value pairs to apply to this Div.""") - - _rename = {'object': 'text'} - - def _get_properties(self): - return {p : getattr(self,p) for p in list(Layoutable.param) + ['style'] - if getattr(self, p) is not None} - - def _get_model(self, doc, root=None, parent=None, comm=None): - model = _BkDiv(**self._get_properties()) - if root is None: - root = model - self._models[root.ref['id']] = model - self._link_object(doc, root, parent, comm) - return model - - def _update(self, model): - model.update(**self._get_properties()) - - -class Image(DivPaneBase): - """ - Encodes an image as base64 and wraps it in a Bokeh Div model. - This is an abstract base class that needs the image type - to be specified and specific code for determining the image shape. - - The imgtype determines the filetype, extension, and MIME type for - this image. Each image type (png,jpg,gif) has a base class that - supports anything with a `_repr_X_` method (where X is `png`, - `gif`, etc.), a local file with the given file extension, or a - HTTP(S) url with the given extension. Subclasses of each type can - provide their own way of obtaining or generating a PNG. - """ - - imgtype = 'None' - - @classmethod - def applies(cls, obj): - imgtype = cls.imgtype - return (hasattr(obj, '_repr_'+imgtype+'_') or - (isinstance(obj, basestring) and - ((os.path.isfile(obj) and obj.endswith('.'+imgtype)) or - ((obj.startswith('http://') or obj.startswith('https://')) - and obj.endswith('.'+imgtype))))) - - def _img(self): - if not isinstance(self.object, basestring): - return getattr(self.object, '_repr_'+self.imgtype+'_')() - elif os.path.isfile(self.object): - with open(self.object, 'rb') as f: - return f.read() - else: - import requests - r = requests.request(url=self.object, method='GET') - return r.content - - def _imgshape(self, data): - """Calculate and return image width,height""" - raise NotImplementedError - - def _get_properties(self): - p = super(Image,self)._get_properties() - data = self._img() - if isinstance(data, str): - data = base64.b64decode(data) - width, height = self._imgshape(data) - if self.width is not None: - if self.height is None: - height = int((self.width/width)*height) - else: - height = self.height - width = self.width - elif self.height is not None: - width = int((self.height/height)*width) - height = self.height - - b64 = base64.b64encode(data).decode("utf-8") - src = "data:image/"+self.imgtype+";base64,{b64}".format(b64=b64) - html = "".format( - src=src, width=width, height=height) - return dict(p, width=width, height=height, text=html) - - -class PNG(Image): - - imgtype = 'png' - - @classmethod - def _imgshape(cls, data): - import struct - w, h = struct.unpack('>LL', data[16:24]) - return int(w), int(h) - - -class GIF(Image): - - imgtype = 'gif' - - @classmethod - def _imgshape(cls, data): - import struct - w, h = struct.unpack("= 0xC0 and ord(c) <= 0xC3): - b.read(3) - h, w = struct.unpack(">HH", b.read(4)) - break - else: - b.read(int(struct.unpack(">H", b.read(2))[0])-2) - c = b.read(1) - return int(w), int(h) - - -class SVG(Image): - - imgtype = 'svg' - - @classmethod - def applies(cls, obj): - return (super(SVG, cls).applies(obj) or - (isinstance(obj, basestring) and obj.lstrip().startswith('') - - -class Markdown(DivPaneBase): - """ - A Markdown pane renders the markdown markup language to HTML and - displays it inside a bokeh Div model. It has no explicit - priority since it cannot be easily be distinguished from a - standard string, therefore it has to be invoked explicitly. - """ - - # Priority depends on the data type - priority = None - - @classmethod - def applies(cls, obj): - if hasattr(obj, '_repr_markdown_'): - return 0.3 - elif isinstance(obj, basestring): - return 0.1 - else: - return False - - def _get_properties(self): - import markdown - data = self.object - if not isinstance(data, basestring): - data = data._repr_markdown_() - properties = super(Markdown, self)._get_properties() - properties['style'] = dict(properties.get('style', {}), **{"white-space": "nowrap"}) - extensions = ['markdown.extensions.extra', 'markdown.extensions.smarty'] - html = markdown.markdown(self.object, extensions=extensions, - output_format='html5') - return dict(properties, text=html) - - -class YT(HTML): - """ - YT panes wrap plottable objects from the YT library. - By default, the height and width are calculated by summing all - contained plots, but can optionally be specified explicitly to - provide additional space. - """ - - priority = 0.5 - - @classmethod - def applies(cls, obj): - return ('yt' in repr(obj) and - hasattr(obj, "plots") and - hasattr(obj, "_repr_html_")) - - def _get_properties(self): - p = super(YT, self)._get_properties() - - width = height = 0 - if self.width is None or self.height is None: - for k,v in self.object.plots.items(): - if hasattr(v, "_repr_png_"): - img = v._repr_png_() - w,h = PNG._imgshape(img) - height += h - width = max(w, width) - - if self.width is None: p["width"] = width - if self.height is None: p["height"] = height - - return p diff --git a/panel/pane/__init__.py b/panel/pane/__init__.py new file mode 100644 index 0000000000..64d1143119 --- /dev/null +++ b/panel/pane/__init__.py @@ -0,0 +1,45 @@ +""" +The pane module contains PaneBase objects which may render any type of +object as a bokeh model so it can be embedded in a panel. The pane +objects are one of three main components in panel the other two being +layouts and widgets. Panes may render anything including plots, text, +images, equations etc. +""" +from __future__ import absolute_import, division, unicode_literals + +from ..viewable import Viewable +from .base import PaneBase, Pane # noqa +from .equation import LaTeX # noqa +from .holoviews import HoloViews # noqa +from .image import GIF, JPG, PNG, SVG # noqa +from .markup import HTML, Markdown, Str # noqa +from .plotly import Plotly # noqa +from .plot import Bokeh, Matplotlib, RGGPlot, YT # noqa +from .vega import Vega # noqa + + +def panel(obj, **kwargs): + """ + Creates a panel from any supplied object by wrapping it in a pane + and returning a corresponding Panel. + + Arguments + --------- + obj: object + Any object to be turned into a Panel + **kwargs: dict + Any keyword arguments to be passed to the applicable Pane + + Returns + ------- + layout: Viewable + A Viewable representation of the input object + """ + if isinstance(obj, Viewable): + return obj + if kwargs.get('name', False) is None: + kwargs.pop('name') + pane = PaneBase.get_pane_type(obj)(obj, **kwargs) + if len(pane.layout) == 1 and pane._unpack: + return pane.layout[0] + return pane.layout diff --git a/panel/pane/base.py b/panel/pane/base.py new file mode 100644 index 0000000000..8cf496b62e --- /dev/null +++ b/panel/pane/base.py @@ -0,0 +1,175 @@ +""" +Defines the PaneBase class defining the API for panes which convert +objects to a visual representation expressed as a bokeh model. +""" +from __future__ import absolute_import, division, unicode_literals + +import param + +from ..layout import Panel, Row +from ..viewable import Viewable, Reactive, Layoutable +from ..util import param_reprs, push + + +def Pane(obj, **kwargs): + """ + Converts any object to a Pane if a matching Pane class exists. + """ + if isinstance(obj, Viewable): + return obj + return PaneBase.get_pane_type(obj)(obj, **kwargs) + + +class PaneBase(Reactive): + """ + PaneBase is the abstract baseclass for all atomic displayable units + in the panel library. Pane defines an extensible interface for + wrapping arbitrary objects and transforming them into Bokeh models. + + Panes are reactive in the sense that when the object they are + wrapping is changed any dashboard containing the pane will update + in response. + + To define a concrete Pane type subclass this class and implement + the applies classmethod and the _get_model private method. + """ + + default_layout = param.ClassSelector(default=Row, class_=(Panel), + is_instance=False, doc=""" + Defines the layout the model(s) returned by the pane will + be placed in.""") + + object = param.Parameter(default=None, doc=""" + The object being wrapped, which will be converted to a Bokeh model.""") + + # When multiple Panes apply to an object, the one with the highest + # numerical priority is selected. The default is an intermediate value. + # If set to None, applies method will be called to get a priority + # value for a specific object type. + priority = 0.5 + + # Declares whether Pane supports updates to the Bokeh model + _updates = False + + # Whether the Pane layout can be safely unpacked + _unpack = True + + __abstract = True + + @classmethod + def applies(cls, obj): + """ + Given the object return a boolean indicating whether the Pane + can render the object. If the priority of the pane is set to + None, this method may also be used to define a priority + depending on the object being rendered. + """ + return None + + @classmethod + def get_pane_type(cls, obj): + if isinstance(obj, Viewable): + return type(obj) + descendents = [] + for p in param.concrete_descendents(PaneBase).values(): + priority = p.applies(obj) if p.priority is None else p.priority + if isinstance(priority, bool) and priority: + raise ValueError('If a Pane declares no priority ' + 'the applies method should return a ' + 'priority value specific to the ' + 'object type or False, but the %s pane ' + 'declares no priority.' % p.__name__) + elif priority is None or priority is False: + continue + descendents.append((priority, p)) + pane_types = reversed(sorted(descendents, key=lambda x: x[0])) + for _, pane_type in pane_types: + applies = pane_type.applies(obj) + if isinstance(applies, bool) and not applies: continue + return pane_type + raise TypeError('%s type could not be rendered.' % type(obj).__name__) + + def __init__(self, object, **params): + applies = self.applies(object) + if isinstance(applies, bool) and not applies: + raise ValueError("%s pane does not support objects of type '%s'" % + (type(self).__name__, type(object).__name__)) + + super(PaneBase, self).__init__(object=object, **params) + kwargs = {k: v for k, v in params.items() if k in Layoutable.param} + self.layout = self.default_layout(self, **kwargs) + + def __repr__(self, depth=0): + cls = type(self).__name__ + params = param_reprs(self, ['object']) + obj = type(self.object).__name__ + template = '{cls}({obj}, {params})' if params else '{cls}({obj})' + return template.format(cls=cls, params=', '.join(params), obj=obj) + + def __getitem__(self, index): + """ + Allows pane objects to behave like the underlying layout + """ + return self.layout[index] + + def _get_root(self, doc, comm=None): + if self._updates: + root = self._get_model(doc, comm=comm) + else: + root = self.layout._get_model(doc, comm=comm) + self._preprocess(root) + return root + + def _cleanup(self, root=None, final=False): + super(PaneBase, self)._cleanup(root, final) + if final: + self.object = None + + def _update(self, model): + """ + If _updates=True this method is used to update an existing Bokeh + model instead of replacing the model entirely. The supplied model + should be updated with the current state. + """ + raise NotImplementedError + + def _link_object(self, doc, root, parent, comm=None): + """ + Links the object parameter to the rendered Bokeh model, triggering + an update when the object changes. + """ + from .. import state + ref = root.ref['id'] + + def update_pane(change): + old_model = self._models[ref] + + if self._updates: + # Pane supports model updates + def update_models(): + self._update(old_model) + else: + # Otherwise replace the whole model + new_model = self._get_model(doc, root, parent, comm) + def update_models(): + try: + index = parent.children.index(old_model) + except IndexError: + self.warning('%s pane model %s could not be replaced ' + 'with new model %s, ensure that the ' + 'parent is not modified at the same ' + 'time the panel is being updated.' % + (type(self).__name__, old_model, new_model)) + else: + parent.children[index] = new_model + + if comm: + update_models() + push(doc, comm) + elif state.curdoc: + update_models() + else: + doc.add_next_tick_callback(update_models) + + if ref not in self._callbacks: + self._callbacks[ref].append(self.param.watch(update_pane, 'object')) diff --git a/panel/pane/equation.py b/panel/pane/equation.py new file mode 100644 index 0000000000..648137993e --- /dev/null +++ b/panel/pane/equation.py @@ -0,0 +1,111 @@ +""" +Renders objects representing equations including LaTeX strings and +SymPy objects. +""" +from __future__ import absolute_import, division, unicode_literals + +import sys + +from six import string_types + +import param + +from .image import PNG + + +def latex_to_img(text, size=25, dpi=100): + """ + Returns PIL image for LaTeX equation text, using matplotlib's rendering. + Usage: latex_to_img(r'$\frac(x}{y^2}$') + From https://stackoverflow.com/questions/1381741. + """ + import matplotlib.pyplot as plt + from PIL import Image, ImageChops + import io + + buf = io.BytesIO() + with plt.rc_context({'text.usetex': False, 'mathtext.fontset': 'stix'}): + fig = plt.figure() + ax = fig.add_subplot(111) + ax.axis('off') + ax.text(0.05, 0.5, '{text}'.format(text=text), size=size) + fig.set_dpi(dpi) + fig.canvas.print_figure(buf) + plt.close(fig) + + im = Image.open(buf) + bg = Image.new(im.mode, im.size, (255, 255, 255, 255)) + diff = ImageChops.difference(im, bg) + diff = ImageChops.add(diff, diff, 2.0, -100) + bbox = diff.getbbox() + return im.crop(bbox) + + +def make_transparent(img, bg=(255, 255, 255, 255)): + """Given a PIL image, makes the specified background color transparent.""" + img = img.convert("RGBA") + clear = bg[0:3]+(0,) + pixdata = img.load() + + width, height = img.size + for y in range(height): + for x in range(width): + if pixdata[x,y] == bg: + pixdata[x,y] = clear + return img + + +def is_sympy_expr(obj): + """Test for sympy.Expr types without usually needing to import sympy""" + if 'sympy' in sys.modules and 'sympy' in str(type(obj).__class__): + import sympy + if isinstance(obj, sympy.Expr): + return True + return False + + +class LaTeX(PNG): + """ + Matplotlib-based LaTeX-syntax equation. + Requires matplotlib and pillow. + See https://matplotlib.org/users/mathtext.html for what is supported. + """ + + # Priority is dependent on the data type + priority = None + + size = param.Number(default=25, bounds=(1, 100), doc=""" + Size of the rendered equation.""") + + dpi = param.Number(default=72, bounds=(1, 1900), doc=""" + Resolution per inch for the rendered equation.""") + + @classmethod + def applies(cls, obj): + if is_sympy_expr(obj) or hasattr(obj, '_repr_latex_'): + try: + import matplotlib, PIL # noqa + except ImportError: + return False + return 0.05 + elif isinstance(obj, string_types): + return None + else: + return False + + def _imgshape(self, data): + """Calculate and return image width,height""" + w, h = super(LaTeX, self)._imgshape(data) + w, h = (w/self.dpi), (h/self.dpi) + return int(w*72), int(h*72) + + def _img(self): + obj=self.object # Default: LaTeX string + + if hasattr(obj, '_repr_latex_'): + obj = obj._repr_latex_() + elif is_sympy_expr(obj): + import sympy + obj = r'$'+sympy.latex(obj)+'$' + + return make_transparent(latex_to_img(obj, self.size, self.dpi))._repr_png_() diff --git a/panel/holoviews.py b/panel/pane/holoviews.py similarity index 97% rename from panel/holoviews.py rename to panel/pane/holoviews.py index b4d0be6e33..0038fb5c7c 100644 --- a/panel/holoviews.py +++ b/panel/pane/holoviews.py @@ -9,10 +9,10 @@ import param -from .layout import Panel, Column -from .pane import PaneBase, Pane -from .viewable import Viewable -from .widgets import Player +from ..layout import Panel, Column +from ..viewable import Viewable +from ..widgets import Player +from .base import PaneBase, Pane class HoloViews(PaneBase): @@ -118,7 +118,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None): return model def _link_widgets(self, pane, root, comm): - from . import state + from .. import state def update_plot(change): from holoviews.core.util import cross_index @@ -155,7 +155,7 @@ def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individu from holoviews.core import Dimension from holoviews.core.util import isnumeric, unicode, datetime_types from holoviews.core.traversal import unique_dimkeys - from .widgets import Widget, DiscreteSlider, Select, FloatSlider, DatetimeInput + from ..widgets import Widget, DiscreteSlider, Select, FloatSlider, DatetimeInput dims, keys = unique_dimkeys(object) if dims == [Dimension('Frame')] and keys == [(0,)]: diff --git a/panel/pane/image.py b/panel/pane/image.py new file mode 100644 index 0000000000..0c737835fa --- /dev/null +++ b/panel/pane/image.py @@ -0,0 +1,157 @@ +""" +Contains Image panes including renderers for PNG, SVG, GIF and JPG +file types. +""" +from __future__ import absolute_import, division, unicode_literals + +import base64 +import os + +from io import BytesIO + +from six import string_types + +from .markup import DivPaneBase + + +class ImageBase(DivPaneBase): + """ + Encodes an image as base64 and wraps it in a Bokeh Div model. + This is an abstract base class that needs the image type + to be specified and specific code for determining the image shape. + + The imgtype determines the filetype, extension, and MIME type for + this image. Each image type (png,jpg,gif) has a base class that + supports anything with a `_repr_X_` method (where X is `png`, + `gif`, etc.), a local file with the given file extension, or a + HTTP(S) url with the given extension. Subclasses of each type can + provide their own way of obtaining or generating a PNG. + """ + + imgtype = 'None' + + __abstract = True + + @classmethod + def applies(cls, obj): + imgtype = cls.imgtype + return (hasattr(obj, '_repr_'+imgtype+'_') or + (isinstance(obj, string_types) and + ((os.path.isfile(obj) and obj.endswith('.'+imgtype)) or + ((obj.startswith('http://') or obj.startswith('https://')) + and obj.endswith('.'+imgtype))))) + + def _img(self): + if not isinstance(self.object, string_types): + return getattr(self.object, '_repr_'+self.imgtype+'_')() + elif os.path.isfile(self.object): + with open(self.object, 'rb') as f: + return f.read() + else: + import requests + r = requests.request(url=self.object, method='GET') + return r.content + + def _imgshape(self, data): + """Calculate and return image width,height""" + raise NotImplementedError + + def _get_properties(self): + p = super(ImageBase, self)._get_properties() + data = self._img() + if isinstance(data, str): + data = base64.b64decode(data) + width, height = self._imgshape(data) + if self.width is not None: + if self.height is None: + height = int((self.width/width)*height) + else: + height = self.height + width = self.width + elif self.height is not None: + width = int((self.height/height)*width) + height = self.height + + b64 = base64.b64encode(data).decode("utf-8") + src = "data:image/"+self.imgtype+";base64,{b64}".format(b64=b64) + html = "".format( + src=src, width=width, height=height) + return dict(p, width=width, height=height, text=html) + + +class PNG(ImageBase): + + imgtype = 'png' + + @classmethod + def _imgshape(cls, data): + import struct + w, h = struct.unpack('>LL', data[16:24]) + return int(w), int(h) + + +class GIF(ImageBase): + + imgtype = 'gif' + + @classmethod + def _imgshape(cls, data): + import struct + w, h = struct.unpack("= 0xC0 and ord(c) <= 0xC3): + b.read(3) + h, w = struct.unpack(">HH", b.read(4)) + break + else: + b.read(int(struct.unpack(">H", b.read(2))[0])-2) + c = b.read(1) + return int(w), int(h) + + +class SVG(ImageBase): + + imgtype = 'svg' + + @classmethod + def applies(cls, obj): + return (super(SVG, cls).applies(obj) or + (isinstance(obj, string_types) and obj.lstrip().startswith('') + + +class Markdown(DivPaneBase): + """ + A Markdown pane renders the markdown markup language to HTML and + displays it inside a bokeh Div model. It has no explicit + priority since it cannot be easily be distinguished from a + standard string, therefore it has to be invoked explicitly. + """ + + # Priority depends on the data type + priority = None + + @classmethod + def applies(cls, obj): + if hasattr(obj, '_repr_markdown_'): + return 0.3 + elif isinstance(obj, string_types): + return 0.1 + else: + return False + + def _get_properties(self): + import markdown + data = self.object + if not isinstance(data, string_types): + data = data._repr_markdown_() + properties = super(Markdown, self)._get_properties() + properties['style'] = properties.get('style', {}) + extensions = ['markdown.extensions.extra', 'markdown.extensions.smarty'] + html = markdown.markdown(self.object, extensions=extensions, + output_format='html5') + return dict(properties, text=html) diff --git a/panel/pane/plot.py b/panel/pane/plot.py new file mode 100644 index 0000000000..e9ab910623 --- /dev/null +++ b/panel/pane/plot.py @@ -0,0 +1,140 @@ +""" +Pane class which render plots from different libraries +""" +from __future__ import absolute_import, division, unicode_literals + +import sys + +from io import BytesIO + +import param + +from bokeh.models import LayoutDOM, CustomJS + +from ..util import remove_root +from .base import PaneBase +from .markup import HTML +from .image import PNG + + +class Bokeh(PaneBase): + """ + Bokeh panes allow including any Bokeh model in a panel. + """ + + priority = 0.8 + + @classmethod + def applies(cls, obj): + return isinstance(obj, LayoutDOM) + + def _get_model(self, doc, root=None, parent=None, comm=None): + if root is None: + return self._get_root(doc, comm) + + model = self.object + ref = root.ref['id'] + for js in model.select({'type': CustomJS}): + js.code = js.code.replace(model.ref['id'], ref) + + if model._document and doc is not model._document: + remove_root(model, doc) + + self._models[ref] = model + self._link_object(doc, root, parent, comm) + return model + + +class Matplotlib(PNG): + """ + A Matplotlib pane renders a matplotlib figure to png and wraps the + base64 encoded data in a bokeh Div model. The size of the image in + pixels is determined by scaling the size of the figure in inches + by a dpi of 72, increasing the dpi therefore controls the + resolution of the image not the displayed size. + """ + + dpi = param.Integer(default=144, bounds=(1, None), doc=""" + Scales the dpi of the matplotlib figure.""") + + @classmethod + def applies(cls, obj): + if 'matplotlib' not in sys.modules: + return False + from matplotlib.figure import Figure + is_fig = isinstance(obj, Figure) + if is_fig and obj.canvas is None: + raise ValueError('Matplotlib figure has no canvas and ' + 'cannot be rendered.') + return is_fig + + def _imgshape(self, data): + """Calculate and return image width,height""" + w, h = self.object.get_size_inches() + return int(w*72), int(h*72) + + def _img(self): + self.object.set_dpi(self.dpi) + b = BytesIO() + self.object.canvas.print_figure(b) + return b.getvalue() + + +class RGGPlot(PNG): + """ + An RGGPlot pane renders an r2py-based ggplot2 figure to png + and wraps the base64-encoded data in a bokeh Div model. + """ + + height = param.Integer(default=400) + + width = param.Integer(default=400) + + dpi = param.Integer(default=144, bounds=(1, None)) + + @classmethod + def applies(cls, obj): + return type(obj).__name__ == 'GGPlot' and hasattr(obj, 'r_repr') + + def _img(self): + from rpy2.robjects.lib import grdevices + from rpy2 import robjects + with grdevices.render_to_bytesio(grdevices.png, + type="cairo-png", width=self.width, height=self.height, + res=self.dpi, antialias="subpixel") as b: + robjects.r("print")(self.object) + return b.getvalue() + + +class YT(HTML): + """ + YT panes wrap plottable objects from the YT library. + By default, the height and width are calculated by summing all + contained plots, but can optionally be specified explicitly to + provide additional space. + """ + + priority = 0.5 + + @classmethod + def applies(cls, obj): + return ('yt' in repr(obj) and + hasattr(obj, "plots") and + hasattr(obj, "_repr_html_")) + + def _get_properties(self): + p = super(YT, self)._get_properties() + + width = height = 0 + if self.width is None or self.height is None: + for k,v in self.object.plots.items(): + if hasattr(v, "_repr_png_"): + img = v._repr_png_() + w,h = PNG._imgshape(img) + height += h + width = max(w, width) + + if self.width is None: p["width"] = width + if self.height is None: p["height"] = height + + return p diff --git a/panel/plotly.py b/panel/pane/plotly.py similarity index 82% rename from panel/plotly.py rename to panel/pane/plotly.py index 038fe5c121..3ba19bc2f5 100644 --- a/panel/plotly.py +++ b/panel/pane/plotly.py @@ -1,27 +1,16 @@ +""" +Defines a PlotlyPane which renders a plotly plot using PlotlyPlot +bokeh model. +""" from __future__ import absolute_import, division, unicode_literals -import os - import param import numpy as np -from bokeh.core.properties import Dict, String, List, Any, Instance -from bokeh.models import LayoutDOM, ColumnDataSource - -from .pane import PaneBase -from .util import CUSTOM_MODELS - -class PlotlyPlot(LayoutDOM): - """ - A bokeh model that wraps around a plotly plot and renders it inside - a bokeh plot. - """ +from bokeh.models import ColumnDataSource - __implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'models', 'plotly.ts') - - data = Dict(String, Any) - - data_sources = List(Instance(ColumnDataSource)) +from ..models import PlotlyPlot +from .base import PaneBase class Plotly(PaneBase): @@ -97,6 +86,3 @@ def _update(self, model): model.data = json if new_sources: model.data_sources += new_sources - - -CUSTOM_MODELS['panel.plotly.PlotlyPlot'] = PlotlyPlot diff --git a/panel/vega.py b/panel/pane/vega.py similarity index 83% rename from panel/vega.py rename to panel/pane/vega.py index 84baf7bad3..199c8b8626 100644 --- a/panel/vega.py +++ b/panel/pane/vega.py @@ -1,14 +1,14 @@ from __future__ import absolute_import, division, unicode_literals -import os import sys import numpy as np -from bokeh.core.properties import Dict, String, Any, Instance -from bokeh.models import LayoutDOM, ColumnDataSource -from .pane import PaneBase -from .util import CUSTOM_MODELS +from bokeh.models import ColumnDataSource + +from ..models import VegaPlot +from .base import PaneBase + def ds_as_cds(dataset): """ @@ -24,19 +24,6 @@ def ds_as_cds(dataset): return data -class VegaPlot(LayoutDOM): - """ - A Bokeh model that wraps around a Vega plot and renders it inside - a Bokeh plot. - """ - - __implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'models', 'vega.ts') - - data = Dict(String, Any) - - data_sources = Dict(String, Instance(ColumnDataSource)) - - class Vega(PaneBase): """ Vega panes allow rendering Vega plots and traces. @@ -110,5 +97,3 @@ def _update(self, model): self._get_sources(json, model.data_sources) model.data = json - -CUSTOM_MODELS['panel.vega.VegaPlot'] = VegaPlot diff --git a/panel/param.py b/panel/param.py index a44fce670d..ad12c8c52c 100644 --- a/panel/param.py +++ b/panel/param.py @@ -10,16 +10,19 @@ import types import inspect import itertools + from collections import OrderedDict, namedtuple +from six import string_types import param + from param.parameterized import classlist -from .pane import Pane, PaneBase from .layout import Row, Panel, Tabs, Column from .links import Link +from .pane.base import Pane, PaneBase from .util import ( - abbreviated_repr, basestring, default_label_formatter, full_groupby, + abbreviated_repr, default_label_formatter, full_groupby, get_method_owner, is_parameterized, param_name) from .viewable import Layoutable, Reactive from .widgets import ( @@ -176,7 +179,7 @@ def __repr__(self, depth=0): for p, v in sorted(self.get_param_values()): if v is self.param[p].default: continue elif v is None: continue - elif isinstance(v, basestring) and v == '': continue + elif isinstance(v, string_types) and v == '': continue elif p == 'object' or (p == 'name' and v.startswith(obj_cls)): continue elif p == 'parameters' and v == parameters: continue try: diff --git a/panel/pipeline.py b/panel/pipeline.py index d9a0d41cc8..ce44fb6346 100644 --- a/panel/pipeline.py +++ b/panel/pipeline.py @@ -5,9 +5,8 @@ import param import numpy as np -from .holoviews import HoloViews from .layout import Row, Column, HSpacer, VSpacer -from .pane import Markdown, Pane +from .pane import HoloViews, Markdown, Pane from .param import Param from .util import param_reprs diff --git a/panel/tests/test_holoviews.py b/panel/tests/test_holoviews.py index 0b6104ac1a..56c1d7840c 100644 --- a/panel/tests/test_holoviews.py +++ b/panel/tests/test_holoviews.py @@ -9,9 +9,8 @@ Scatter, Line, GridBox) from bokeh.plotting import Figure -from panel.holoviews import HoloViews from panel.layout import Column, Row -from panel.pane import Pane, PaneBase +from panel.pane import Pane, PaneBase, HoloViews from panel.widgets import FloatSlider, DiscreteSlider, Select try: diff --git a/panel/tests/test_links.py b/panel/tests/test_links.py index c736d8b3a3..f835cd1d0a 100644 --- a/panel/tests/test_links.py +++ b/panel/tests/test_links.py @@ -3,9 +3,9 @@ import pytest from panel.layout import Row -from panel.holoviews import HoloViews -from panel.widgets import FloatSlider, RangeSlider from panel.links import GenericLink +from panel.pane import HoloViews +from panel.widgets import FloatSlider, RangeSlider try: import holoviews as hv diff --git a/panel/tests/test_plotly.py b/panel/tests/test_plotly.py index 272ad93ec4..b1de73fc44 100644 --- a/panel/tests/test_plotly.py +++ b/panel/tests/test_plotly.py @@ -10,8 +10,9 @@ plotly_available = pytest.mark.skipif(plotly is None, reason="requires plotly") import numpy as np -from panel.pane import Pane, PaneBase -from panel.plotly import Plotly, PlotlyPlot + +from panel.models import PlotlyPlot +from panel.pane import Pane, PaneBase, Plotly @plotly_available diff --git a/panel/tests/test_vega.py b/panel/tests/test_vega.py index 8d84f6dd13..af2de89b7e 100644 --- a/panel/tests/test_vega.py +++ b/panel/tests/test_vega.py @@ -9,8 +9,9 @@ altair_available = pytest.mark.skipif(alt is None, reason="requires altair") import numpy as np -from panel.pane import Pane, PaneBase -from panel.vega import Vega, VegaPlot + +from panel.models import VegaPlot +from panel.pane import Pane, PaneBase, Vega vega_example = { 'config': {'view': {'width': 400, 'height': 300}}, diff --git a/panel/util.py b/panel/util.py index b5a7a4d8f0..aec94caa85 100644 --- a/panel/util.py +++ b/panel/util.py @@ -12,12 +12,14 @@ import textwrap from collections import defaultdict, MutableSequence, MutableMapping, OrderedDict -from datetime import datetime from contextlib import contextmanager +from datetime import datetime +from six import string_types import param import bokeh import bokeh.embed.notebook + from bokeh.document import Document from bokeh.io.notebook import load_notebook as bk_load_notebook from bokeh.models import Model, LayoutDOM, Box @@ -27,16 +29,13 @@ from pyviz_comms import (PYVIZ_PROXY, JupyterCommManager, bokeh_msg_handler, nb_mime_js, embed_js) -try: - unicode = unicode # noqa - basestring = basestring # noqa -except: - basestring = unicode = str - # Global variables CUSTOM_MODELS = {} BLOCKED = False +if sys.version_info.major > 2: + unicode = str + def load_compiled_models(custom_model, implementation): """ @@ -145,7 +144,7 @@ def param_reprs(parameterized, skip=[]): for p, v in sorted(parameterized.get_param_values()): if v is parameterized.param[p].default: continue elif v is None: continue - elif isinstance(v, basestring) and v == '': continue + elif isinstance(v, string_types) and v == '': continue elif isinstance(v, list) and v == []: continue elif isinstance(v, dict) and v == {}: continue elif p in skip or (p == 'name' and v.startswith(cls)): continue diff --git a/panel/widgets.py b/panel/widgets.py index 440f8bfd5a..d38f7b9132 100644 --- a/panel/widgets.py +++ b/panel/widgets.py @@ -11,6 +11,7 @@ from base64 import b64decode, b64encode from collections import OrderedDict from datetime import datetime +from six import string_types import param import numpy as np @@ -30,7 +31,7 @@ from .models.widgets import ( Player as _BkPlayer, FileInput as _BkFileInput, Audio as _BkAudio) from .viewable import Reactive -from .util import as_unicode, push, value_as_datetime, hashable, basestring +from .util import as_unicode, push, value_as_datetime, hashable class Widget(Reactive): @@ -120,7 +121,7 @@ def save(self, filename): ---------- filename (str): File path or file-like object """ - if isinstance(filename, basestring): + if isinstance(filename, string_types): with open(filename, 'wb') as f: f.write(self.value) else: @@ -312,7 +313,7 @@ class ColorPicker(Widget): class _ButtonBase(Widget): button_type = param.ObjectSelector(default='default', objects=[ - 'default', 'primary', 'success', 'info', 'danger']) + 'default', 'primary', 'success', 'warning', 'danger']) _rename = {'name': 'label'} @@ -685,7 +686,7 @@ def labels(self): if isinstance(self.options, dict): return [title + o for o in self.options] else: - return [title + (o if isinstance(o, basestring) else (self.formatter % o)) + return [title + (o if isinstance(o, string_types) else (self.formatter % o)) for o in self.options] @property def values(self):