Add Button.value Event parameter
Add test
Small fix
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 |
elif isinstance(obj, tuple): |
|
85 |
data, layout = obj |
|
86 |
else: |
|
87 |
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 |
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 |
for i, element in enumerate(value): |
|
114 |
element_path = full_path + '.' + str(i) |
|
115 |
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 |
if isinstance(comm, JupyterComm): |
|
190 |
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 |
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 |
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 |
model.update(data=[], layout={}) |
|
243 |
model._render_count += 1 |
|
244 |
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 |
except Exception: |
|
264 |
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 |
except Exception: |
|
277 |
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 |
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 .