1
"""
2
Defines a PlotlyPane which renders a plotly plot using PlotlyPlot
3
bokeh model.
4
"""
5 7
from __future__ import absolute_import, division, unicode_literals
6

7 7
import sys
8

9 7
import numpy as np
10

11 7
from bokeh.models import ColumnDataSource
12 7
from pyviz_comms import JupyterComm
13 7
import param
14

15 7
from .base import PaneBase
16 7
from ..util import isdatetime
17 7
from ..viewable import Layoutable
18

19

20

21 7
class Plotly(PaneBase):
22
    """
23
    Plotly panes allow rendering plotly Figures and traces.
24

25
    For efficiency any array objects found inside a Figure are added
26
    to a ColumnDataSource which allows using binary transport to sync
27
    the figure on bokeh server and via Comms.
28
    """
29

30 7
    click_data = param.Dict(doc="Click callback data")
31

32 7
    clickannotation_data = param.Dict(doc="Clickannotation callback data")
33

34 7
    config = param.Dict(doc="Config data")
35

36 7
    hover_data = param.Dict(doc="Hover callback data")
37

38 7
    relayout_data = param.Dict(doc="Relayout callback data")
39

40 7
    restyle_data = param.List(doc="Restyle callback data")
41

42 7
    selected_data = param.Dict(doc="Selected callback data")
43

44 7
    viewport = param.Dict(doc="Current viewport state")
45

46 7
    viewport_update_policy = param.Selector(default="mouseup", doc="""
47
        Policy by which the viewport parameter is updated during user interactions.
48

49
        * "mouseup": updates are synchronized when mouse button is
50
          released after panning
51
        * "continuous": updates are synchronized continually while panning
52
        * "throttle": updates are synchronized while panning, at 
53
          intervals determined by the viewport_update_throttle parameter
54
        """, objects=["mouseup", "continuous", "throttle"])
55

56 7
    viewport_update_throttle = param.Integer(default=200, bounds=(0, None), doc="""
57
        Time interval in milliseconds at which viewport updates are
58
        synchronized when viewport_update_policy is "throttle".""")
59

60 7
    _render_count = param.Integer(default=0, doc="""
61
        Number of renders, increment to trigger re-render""")
62

63 7
    priority = 0.8
64

65 7
    _updates = True
66

67 7
    @classmethod
68 2
    def applies(cls, obj):
69 7
        return ((isinstance(obj, list) and obj and all(cls.applies(o) for o in obj)) or
70
                hasattr(obj, 'to_plotly_json') or (isinstance(obj, dict)
71
                                                   and 'data' in obj and 'layout' in obj))
72

73 7
    def __init__(self, object=None, **params):
74 7
        super(Plotly, self).__init__(object, **params)
75 7
        self._figure = None
76 7
        self._update_figure()
77

78 7
    def _to_figure(self, obj):
79 7
        import plotly.graph_objs as go
80 7
        if isinstance(obj, go.Figure):
81 7
            return obj
82 7
        elif isinstance(obj, dict):
83 7
            data, layout = obj['data'], obj['layout']
84 0
        elif isinstance(obj, tuple):
85 0
            data, layout = obj
86
        else:
87 0
            data, layout = obj, {}
88 7
        data = data if isinstance(data, list) else [data]
89 7
        return go.Figure(data=data, layout=layout)
90

91 7
    @staticmethod
92 2
    def _get_sources(json):
93 7
        sources = []
94 7
        traces = json.get('data', [])
95 7
        for trace in traces:
96 7
            data = {}
97 7
            Plotly._get_sources_for_trace(trace, data)
98 7
            sources.append(ColumnDataSource(data))
99 7
        return sources
100

101 7
    @staticmethod
102 7
    def _get_sources_for_trace(json, data, parent_path=''):
103 7
        for key, value in list(json.items()):
104 7
            full_path = key if not parent_path else (parent_path + '.' + key)
105 7
            if isinstance(value, np.ndarray):
106
                # Extract numpy array
107 7
                data[full_path] = [json.pop(key)]
108 7
            elif isinstance(value, dict):
109
                # Recurse into dictionaries:
110 0
                Plotly._get_sources_for_trace(value, data=data, parent_path=full_path)
111 7
            elif isinstance(value, list) and value and isinstance(value[0], dict):
112
                # recurse into object arrays:
113 0
                for i, element in enumerate(value):
114 0
                    element_path = full_path + '.' + str(i)
115 0
                    Plotly._get_sources_for_trace(
116
                        element, data=data, parent_path=element_path
117
                    )
118

119 7
    @param.depends('object', watch=True)
120 2
    def _update_figure(self):
121 7
        import plotly.graph_objs as go
122

123 7
        if (self.object is None or
124
                type(self.object) is not go.Figure or
125
                self.object is self._figure):
126 7
            return
127

128
        # Monkey patch the message stubs used by FigureWidget.
129
        # We only patch `Figure` objects (not subclasses like FigureWidget) so
130
        # we don't interfere with subclasses that override these methods.
131 7
        fig = self.object
132 7
        fig._send_addTraces_msg = lambda *_, **__: self.param.trigger('object')
133 7
        fig._send_moveTraces_msg = lambda *_, **__: self.param.trigger('object')
134 7
        fig._send_deleteTraces_msg = lambda *_, **__: self.param.trigger('object')
135 7
        fig._send_restyle_msg = lambda *_, **__: self.param.trigger('object')
136 7
        fig._send_relayout_msg = lambda *_, **__: self.param.trigger('object')
137 7
        fig._send_update_msg = lambda *_, **__: self.param.trigger('object')
138 7
        fig._send_animate_msg = lambda *_, **__: self.param.trigger('object')
139 7
        self._figure = fig
140

141 7
    def _update_data_sources(self, cds, trace):
142 7
        trace_arrays = {}
143 7
        Plotly._get_sources_for_trace(trace, trace_arrays)
144

145 7
        update_sources = False
146 7
        for key, new_col in trace_arrays.items():
147 7
            new = new_col[0]
148

149 7
            try:
150 7
                old = cds.data.get(key)[0]
151 7
                update_array = (
152
                    (type(old) != type(new)) or
153
                    (new.shape != old.shape) or
154
                    (new != old).any())
155 7
            except Exception:
156 7
                update_array = True
157

158 7
            if update_array:
159 7
                update_sources = True
160 7
                cds.data[key] = [new]
161

162 7
        return update_sources
163

164 7
    @staticmethod
165 2
    def _plotly_json_wrapper(fig):
166
        """Wraps around to_plotly_json and applies necessary fixes.
167

168
        For #382: Map datetime elements to strings.
169
        """
170 7
        json = fig.to_plotly_json()
171 7
        data = json['data']
172

173 7
        for idx in range(len(data)):
174 7
            for key in data[idx]:
175 7
                if isdatetime(data[idx][key]):
176 7
                    arr = data[idx][key]
177 7
                    if isinstance(arr, np.ndarray):
178 7
                        arr = arr.astype(str) 
179
                    else:
180 7
                        arr = [str(v) for v in arr]
181 7
                    data[idx][key] = arr
182 7
        return json
183

184 7
    def _get_model(self, doc, root=None, parent=None, comm=None):
185
        """
186
        Should return the bokeh model to be rendered.
187
        """
188 7
        if 'panel.models.plotly' not in sys.modules:
189 0
            if isinstance(comm, JupyterComm):
190 0
                self.param.warning('PlotlyPlot was not imported on instantiation '
191
                                   'and may not render in a notebook. Restart '
192
                                   'the notebook kernel and ensure you load '
193
                                   'it as part of the extension using:'
194
                                   '\n\npn.extension(\'plotly\')\n')
195 0
            from ..models.plotly import PlotlyPlot
196
        else:
197 7
            PlotlyPlot = getattr(sys.modules['panel.models.plotly'], 'PlotlyPlot')
198

199 7
        viewport_params = [p for p in self.param if 'viewport' in p]
200 7
        params = list(Layoutable.param)+viewport_params
201 7
        properties = {p : getattr(self, p) for p in params
202
                      if getattr(self, p) is not None}
203

204 7
        if self.object is None:
205 7
            json, sources = {}, []
206
        else:
207 7
            fig = self._to_figure(self.object)
208 7
            json = self._plotly_json_wrapper(fig)
209 7
            sources = Plotly._get_sources(json)
210

211 7
        data = json.get('data', [])
212 7
        layout = json.get('layout', {})
213 7
        if layout.get('autosize') and self.sizing_mode is self.param.sizing_mode.default:
214 7
            properties['sizing_mode'] = 'stretch_both'
215

216 7
        model = PlotlyPlot(
217
            data=data, layout=layout, config=self.config, data_sources=sources,
218
            _render_count=self._render_count, **properties
219
        )
220

221 7
        if root is None:
222 7
            root = model
223

224 7
        self._link_props(
225
            model, [
226
                'config', 'relayout_data', 'restyle_data', 'click_data',  'hover_data',
227
                'clickannotation_data', 'selected_data', 'viewport',
228
                'viewport_update_policy', 'viewport_update_throttle', '_render_count'
229
            ],
230
            doc,
231
            root,
232
            comm
233
        )
234

235 7
        if root is None:
236 0
            root = model
237 7
        self._models[root.ref['id']] = (model, parent)
238 7
        return model
239

240 7
    def _update(self, ref=None, model=None):
241 7
        if self.object is None:
242 0
            model.update(data=[], layout={})
243 0
            model._render_count += 1
244 0
            return
245

246 7
        fig = self._to_figure(self.object)
247 7
        json = self._plotly_json_wrapper(fig)
248 7
        layout = json.get('layout')
249

250 7
        traces = json['data']
251 7
        new_sources = []
252 7
        update_sources = False
253 7
        for i, trace in enumerate(traces):
254 7
            if i < len(model.data_sources):
255 7
                cds = model.data_sources[i]
256
            else:
257 7
                cds = ColumnDataSource()
258 7
                new_sources.append(cds)
259

260 7
            update_sources = self._update_data_sources(cds, trace) or update_sources
261 7
        try:
262 7
            update_layout = model.layout != layout
263 0
        except Exception:
264 0
            update_layout = True
265

266
        # Determine if model needs updates
267 7
        if (len(model.data) != len(traces)):
268 7
            update_data = True
269
        else:
270 7
            update_data = False
271 7
            for new, old in zip(traces, model.data):
272 7
                try:
273 7
                    update_data = (
274
                        {k: v for k, v in new.items() if k != 'uid'} !=
275
                        {k: v for k, v in old.items() if k != 'uid'})
276 0
                except Exception:
277 0
                    update_data = True
278 7
                if update_data:
279 7
                    break
280

281 7
        if self.sizing_mode is self.param.sizing_mode.default and 'autosize' in layout:
282 7
            autosize = layout.get('autosize')
283 7
            if autosize and model.sizing_mode != 'stretch_both':
284 0
                model.sizing_mode = 'stretch_both'
285 7
            elif not autosize and model.sizing_mode != 'fixed':
286 7
                model.sizing_mode = 'fixed'
287

288 7
        if new_sources:
289 7
            model.data_sources += new_sources
290

291 7
        if update_data:
292 7
            model.data = json.get('data')
293

294 7
        if update_layout:
295 7
            model.layout = layout
296

297
        # Check if we should trigger rendering
298 7
        if new_sources or update_sources or update_data or update_layout:
299 7
            model._render_count += 1

Read our documentation on viewing source code .

Loading