1 | var widgets = require("@jupyter-widgets/base");
|
2 | var _ = require("lodash");
|
3 |
|
4 | window.PlotlyConfig = { MathJaxConfig: "local" };
|
5 | var Plotly = require("plotly.js/dist/plotly");
|
6 | var semver_range = "^" + require("../package.json").version;
|
7 |
|
8 | // Model
|
9 | // =====
|
10 | /**
|
11 | * A FigureModel holds a mirror copy of the state of a FigureWidget on
|
12 | * the Python side. There is a one-to-one relationship between JavaScript
|
13 | * FigureModels and Python FigureWidgets. The JavaScript FigureModel is
|
14 | * initialized as soon as a Python FigureWidget initialized, this happens
|
15 | * even before the widget is first displayed in the Notebook
|
16 | * @type {widgets.DOMWidgetModel}
|
17 | */
|
18 | var FigureModel = widgets.DOMWidgetModel.extend(
|
19 | {
|
20 | defaults: _.extend(widgets.DOMWidgetModel.prototype.defaults(), {
|
21 | // Model metadata
|
22 | // --------------
|
23 | _model_name: "FigureModel",
|
24 | _view_name: "FigureView",
|
25 | _model_module: "plotlywidget",
|
26 | _view_module: "plotlywidget",
|
27 | _view_module_version: semver_range,
|
28 | _model_module_version: semver_range,
|
29 |
|
30 | // Data and Layout
|
31 | // ---------------
|
32 | // The _data and _layout properties are synchronized with the
|
33 | // Python side on initialization only. After initialization, these
|
34 | // properties are kept in sync through the use of the _py2js_*
|
35 | // messages
|
36 | _data: [],
|
37 | _layout: {},
|
38 | _config: {},
|
39 |
|
40 | // Python -> JS messages
|
41 | // ---------------------
|
42 | // Messages are implemented using trait properties. This is done so
|
43 | // that we can take advantage of ipywidget's binary serialization
|
44 | // protocol.
|
45 | //
|
46 | // Messages are sent by the Python side by assigning the message
|
47 | // contents to the appropriate _py2js_* property, and then immediately
|
48 | // setting it to None. Messages are received by the JavaScript
|
49 | // side by registering property change callbacks in the initialize
|
50 | // methods for FigureModel and FigureView. e.g. (where this is a
|
51 | // FigureModel):
|
52 | //
|
53 | // this.on('change:_py2js_addTraces', this.do_addTraces, this);
|
54 | //
|
55 | // Message handling methods, do_addTraces, are responsible for
|
56 | // performing the appropriate action if the message contents are
|
57 | // not null
|
58 |
|
59 | /**
|
60 | * @typedef {null|Object} Py2JsAddTracesMsg
|
61 | * @property {Array.<Object>} trace_data
|
62 | * Array of traces to append to the end of the figure's current traces
|
63 | * @property {Number} trace_edit_id
|
64 | * Edit ID to use when returning trace deltas using
|
65 | * the _js2py_traceDeltas message.
|
66 | * @property {Number} layout_edit_id
|
67 | * Edit ID to use when returning layout deltas using
|
68 | * the _js2py_layoutDelta message.
|
69 | */
|
70 | _py2js_addTraces: null,
|
71 |
|
72 | /**
|
73 | * @typedef {null|Object} Py2JsDeleteTracesMsg
|
74 | * @property {Array.<Number>} delete_inds
|
75 | * Array of indexes of traces to be deleted, in ascending order
|
76 | * @property {Number} trace_edit_id
|
77 | * Edit ID to use when returning trace deltas using
|
78 | * the _js2py_traceDeltas message.
|
79 | * @property {Number} layout_edit_id
|
80 | * Edit ID to use when returning layout deltas using
|
81 | * the _js2py_layoutDelta message.
|
82 | */
|
83 | _py2js_deleteTraces: null,
|
84 |
|
85 | /**
|
86 | * @typedef {null|Object} Py2JsMoveTracesMsg
|
87 | * @property {Array.<Number>} current_trace_inds
|
88 | * Array of the current indexes of traces to be moved
|
89 | * @property {Array.<Number>} new_trace_inds
|
90 | * Array of the new indexes that traces should be moved to.
|
91 | */
|
92 | _py2js_moveTraces: null,
|
93 |
|
94 | /**
|
95 | * @typedef {null|Object} Py2JsRestyleMsg
|
96 | * @property {Object} restyle_data
|
97 | * Restyle data as accepted by Plotly.restyle
|
98 | * @property {null|Array.<Number>} restyle_traces
|
99 | * Array of indexes of the traces that the resytle operation applies
|
100 | * to, or null to apply the operation to all traces
|
101 | * @property {Number} trace_edit_id
|
102 | * Edit ID to use when returning trace deltas using
|
103 | * the _js2py_traceDeltas message
|
104 | * @property {Number} layout_edit_id
|
105 | * Edit ID to use when returning layout deltas using
|
106 | * the _js2py_layoutDelta message
|
107 | * @property {null|String} source_view_id
|
108 | * view_id of the FigureView that triggered the original restyle
|
109 | * event (e.g. by clicking the legend), or null if the restyle was
|
110 | * triggered from Python
|
111 | */
|
112 | _py2js_restyle: null,
|
113 |
|
114 | /**
|
115 | * @typedef {null|Object} Py2JsRelayoutMsg
|
116 | * @property {Object} relayout_data
|
117 | * Relayout data as accepted by Plotly.relayout
|
118 | * @property {Number} layout_edit_id
|
119 | * Edit ID to use when returning layout deltas using
|
120 | * the _js2py_layoutDelta message
|
121 | * @property {null|String} source_view_id
|
122 | * view_id of the FigureView that triggered the original relayout
|
123 | * event (e.g. by clicking the zoom button), or null if the
|
124 | * relayout was triggered from Python
|
125 | */
|
126 | _py2js_relayout: null,
|
127 |
|
128 | /**
|
129 | * @typedef {null|Object} Py2JsUpdateMsg
|
130 | * @property {Object} style_data
|
131 | * Style data as accepted by Plotly.update
|
132 | * @property {Object} layout_data
|
133 | * Layout data as accepted by Plotly.update
|
134 | * @property {Array.<Number>} style_traces
|
135 | * Array of indexes of the traces that the update operation applies
|
136 | * to, or null to apply the operation to all traces
|
137 | * @property {Number} trace_edit_id
|
138 | * Edit ID to use when returning trace deltas using
|
139 | * the _js2py_traceDeltas message
|
140 | * @property {Number} layout_edit_id
|
141 | * Edit ID to use when returning layout deltas using
|
142 | * the _js2py_layoutDelta message
|
143 | * @property {null|String} source_view_id
|
144 | * view_id of the FigureView that triggered the original update
|
145 | * event (e.g. by clicking a button), or null if the update was
|
146 | * triggered from Python
|
147 | */
|
148 | _py2js_update: null,
|
149 |
|
150 | /**
|
151 | * @typedef {null|Object} Py2JsAnimateMsg
|
152 | * @property {Object} style_data
|
153 | * Style data as accepted by Plotly.animate
|
154 | * @property {Object} layout_data
|
155 | * Layout data as accepted by Plotly.animate
|
156 | * @property {Array.<Number>} style_traces
|
157 | * Array of indexes of the traces that the animate operation applies
|
158 | * to, or null to apply the operation to all traces
|
159 | * @property {Object} animation_opts
|
160 | * Animation options as accepted by Plotly.animate
|
161 | * @property {Number} trace_edit_id
|
162 | * Edit ID to use when returning trace deltas using
|
163 | * the _js2py_traceDeltas message
|
164 | * @property {Number} layout_edit_id
|
165 | * Edit ID to use when returning layout deltas using
|
166 | * the _js2py_layoutDelta message
|
167 | * @property {null|String} source_view_id
|
168 | * view_id of the FigureView that triggered the original animate
|
169 | * event (e.g. by clicking a button), or null if the update was
|
170 | * triggered from Python
|
171 | */
|
172 | _py2js_animate: null,
|
173 |
|
174 | /**
|
175 | * @typedef {null|Object} Py2JsRemoveLayoutPropsMsg
|
176 | * @property {Array.<Array.<String|Number>>} remove_props
|
177 | * Array of property paths to remove. Each propery path is an
|
178 | * array of property names or array indexes that locate a property
|
179 | * inside the _layout object
|
180 | */
|
181 | _py2js_removeLayoutProps: null,
|
182 |
|
183 | /**
|
184 | * @typedef {null|Object} Py2JsRemoveTracePropsMsg
|
185 | * @property {Number} remove_trace
|
186 | * The index of the trace from which to remove properties
|
187 | * @property {Array.<Array.<String|Number>>} remove_props
|
188 | * Array of property paths to remove. Each propery path is an
|
189 | * array of property names or array indexes that locate a property
|
190 | * inside the _data[remove_trace] object
|
191 | */
|
192 | _py2js_removeTraceProps: null,
|
193 |
|
194 | // JS -> Python messages
|
195 | // ---------------------
|
196 | // Messages are sent by the JavaScript side by assigning the
|
197 | // message contents to the appropriate _js2py_* property and then
|
198 | // calling the `touch` method on the view that triggered the
|
199 | // change. e.g. (where this is a FigureView):
|
200 | //
|
201 | // this.model.set('_js2py_restyle', data);
|
202 | // this.touch();
|
203 | //
|
204 | // The Python side is responsible for setting the property to None
|
205 | // after receiving the message.
|
206 | //
|
207 | // Message trigger logic is described in the corresponding
|
208 | // handle_plotly_* methods of FigureView
|
209 |
|
210 | /**
|
211 | * @typedef {null|Object} Js2PyRestyleMsg
|
212 | * @property {Object} style_data
|
213 | * Style data that was passed to Plotly.restyle
|
214 | * @property {Array.<Number>} style_traces
|
215 | * Array of indexes of the traces that the restyle operation
|
216 | * was applied to, or null if applied to all traces
|
217 | * @property {String} source_view_id
|
218 | * view_id of the FigureView that triggered the original restyle
|
219 | * event (e.g. by clicking the legend)
|
220 | */
|
221 | _js2py_restyle: null,
|
222 |
|
223 | /**
|
224 | * @typedef {null|Object} Js2PyRelayoutMsg
|
225 | * @property {Object} relayout_data
|
226 | * Relayout data that was passed to Plotly.relayout
|
227 | * @property {String} source_view_id
|
228 | * view_id of the FigureView that triggered the original relayout
|
229 | * event (e.g. by clicking the zoom button)
|
230 | */
|
231 | _js2py_relayout: null,
|
232 |
|
233 | /**
|
234 | * @typedef {null|Object} Js2PyUpdateMsg
|
235 | * @property {Object} style_data
|
236 | * Style data that was passed to Plotly.update
|
237 | * @property {Object} layout_data
|
238 | * Layout data that was passed to Plotly.update
|
239 | * @property {Array.<Number>} style_traces
|
240 | * Array of indexes of the traces that the update operation applied
|
241 | * to, or null if applied to all traces
|
242 | * @property {String} source_view_id
|
243 | * view_id of the FigureView that triggered the original relayout
|
244 | * event (e.g. by clicking the zoom button)
|
245 | */
|
246 | _js2py_update: null,
|
247 |
|
248 | /**
|
249 | * @typedef {null|Object} Js2PyLayoutDeltaMsg
|
250 | * @property {Object} layout_delta
|
251 | * The layout delta object that contains all of the properties of
|
252 | * _fullLayout that are not identical to those in the
|
253 | * FigureModel's _layout property
|
254 | * @property {Number} layout_edit_id
|
255 | * Edit ID of message that triggered the creation of layout delta
|
256 | */
|
257 | _js2py_layoutDelta: null,
|
258 |
|
259 | /**
|
260 | * @typedef {null|Object} Js2PyTraceDeltasMsg
|
261 | * @property {Array.<Object>} trace_deltas
|
262 | * Array of trace delta objects. Each trace delta contains the
|
263 | * trace's uid along with all of the properties of _fullData that
|
264 | * are not identical to those in the FigureModel's _data property
|
265 | * @property {Number} trace_edit_id
|
266 | * Edit ID of message that triggered the creation of trace deltas
|
267 | */
|
268 | _js2py_traceDeltas: null,
|
269 |
|
270 | /**
|
271 | * Object representing a collection of points for use in click, hover,
|
272 | * and selection events
|
273 | * @typedef {Object} Points
|
274 | * @property {Array.<Number>} trace_indexes
|
275 | * Array of the trace index for each point
|
276 | * @property {Array.<Number>} point_indexes
|
277 | * Array of the index of each point in its own trace
|
278 | * @property {null|Array.<Number>} xs
|
279 | * Array of the x coordinate of each point (for cartesian trace types)
|
280 | * or null (for non-cartesian trace types)
|
281 | * @property {null|Array.<Number>} ys
|
282 | * Array of the y coordinate of each point (for cartesian trace types)
|
283 | * or null (for non-cartesian trace types
|
284 | * @property {null|Array.<Number>} zs
|
285 | * Array of the z coordinate of each point (for 3D cartesian
|
286 | * trace types)
|
287 | * or null (for non-3D-cartesian trace types)
|
288 | */
|
289 |
|
290 | /**
|
291 | * Object representing the state of the input devices during a
|
292 | * plotly event
|
293 | * @typedef {Object} InputDeviceState
|
294 | * @property {boolean} alt - true if alt key pressed,
|
295 | * false otherwise
|
296 | * @property {boolean} ctrl - true if ctrl key pressed,
|
297 | * false otherwise
|
298 | * @property {boolean} meta - true if meta key pressed,
|
299 | * false otherwise
|
300 | * @property {boolean} shift - true if shift key pressed,
|
301 | * false otherwise
|
302 | *
|
303 | * @property {boolean} button
|
304 | * Indicates which button was pressed on the mouse to trigger the
|
305 | * event.
|
306 | * 0: Main button pressed, usually the left button or the
|
307 | * un-initialized state
|
308 | * 1: Auxiliary button pressed, usually the wheel button or
|
309 | * the middle button (if present)
|
310 | * 2: Secondary button pressed, usually the right button
|
311 | * 3: Fourth button, typically the Browser Back button
|
312 | * 4: Fifth button, typically the Browser Forward button
|
313 | *
|
314 | * @property {boolean} buttons
|
315 | * Indicates which buttons were pressed on the mouse when the event
|
316 | * is triggered.
|
317 | * 0 : No button or un-initialized
|
318 | * 1 : Primary button (usually left)
|
319 | * 2 : Secondary button (usually right)
|
320 | * 4 : Auxilary button (usually middle or mouse wheel button)
|
321 | * 8 : 4th button (typically the "Browser Back" button)
|
322 | * 16 : 5th button (typically the "Browser Forward" button)
|
323 | *
|
324 | * Combinations of buttons are represented by the sum of the codes
|
325 | * above. e.g. a value of 7 indicates buttons 1 (primary),
|
326 | * 2 (secondary), and 4 (auxilary) were pressed during the event
|
327 | */
|
328 |
|
329 | /**
|
330 | * @typedef {Object} BoxSelectorState
|
331 | * @property {Array.<Number>} xrange
|
332 | * Two element array containing the x-range of the box selection
|
333 | * @property {Array.<Number>} yrange
|
334 | * Two element array containing the y-range of the box selection
|
335 | */
|
336 |
|
337 | /**
|
338 | * @typedef {Object} LassoSelectorState
|
339 | * @property {Array.<Number>} xs
|
340 | * Array of the x-coordinates of the lasso selection region
|
341 | * @property {Array.<Number>} ys
|
342 | * Array of the y-coordinates of the lasso selection region
|
343 | */
|
344 |
|
345 | /**
|
346 | * Object representing the state of the selection tool during a
|
347 | * plotly_select event
|
348 | * @typedef {Object} Selector
|
349 | * @property {String} type
|
350 | * Selection type. One of: 'box', or 'lasso'
|
351 | * @property {BoxSelectorState|LassoSelectorState} selector_state
|
352 | */
|
353 |
|
354 | /**
|
355 | * @typedef {null|Object} Js2PyPointsCallbackMsg
|
356 | * @property {string} event_type
|
357 | * Name of the triggering event. One of 'plotly_click',
|
358 | * 'plotly_hover', 'plotly_unhover', or 'plotly_selected'
|
359 | * @property {null|Points} points
|
360 | * Points object for event
|
361 | * @property {null|InputDeviceState} device_state
|
362 | * InputDeviceState object for event
|
363 | * @property {null|Selector} selector
|
364 | * State of the selection tool for 'plotly_selected' events, null
|
365 | * for other event types
|
366 | */
|
367 | _js2py_pointsCallback: null,
|
368 |
|
369 | // Message tracking
|
370 | // ----------------
|
371 | /**
|
372 | * @type {Number}
|
373 | * layout_edit_id of the last layout modification operation
|
374 | * requested by the Python side
|
375 | */
|
376 | _last_layout_edit_id: 0,
|
377 |
|
378 | /**
|
379 | * @type {Number}
|
380 | * trace_edit_id of the last trace modification operation
|
381 | * requested by the Python side
|
382 | */
|
383 | _last_trace_edit_id: 0,
|
384 | }),
|
385 |
|
386 | /**
|
387 | * Initialize FigureModel. Called when the Python FigureWidget is first
|
388 | * constructed
|
389 | */
|
390 | initialize: function () {
|
391 | FigureModel.__super__.initialize.apply(this, arguments);
|
392 |
|
393 | this.on("change:_data", this.do_data, this);
|
394 | this.on("change:_layout", this.do_layout, this);
|
395 | this.on("change:_py2js_addTraces", this.do_addTraces, this);
|
396 | this.on("change:_py2js_deleteTraces", this.do_deleteTraces, this);
|
397 | this.on("change:_py2js_moveTraces", this.do_moveTraces, this);
|
398 | this.on("change:_py2js_restyle", this.do_restyle, this);
|
399 | this.on("change:_py2js_relayout", this.do_relayout, this);
|
400 | this.on("change:_py2js_update", this.do_update, this);
|
401 | this.on("change:_py2js_animate", this.do_animate, this);
|
402 | this.on(
|
403 | "change:_py2js_removeLayoutProps",
|
404 | this.do_removeLayoutProps,
|
405 | this
|
406 | );
|
407 | this.on("change:_py2js_removeTraceProps", this.do_removeTraceProps, this);
|
408 | },
|
409 |
|
410 | /**
|
411 | * Input a trace index specification and return an Array of trace
|
412 | * indexes where:
|
413 | *
|
414 | * - null|undefined -> Array of all traces
|
415 | * - Trace index as Number -> Single element array of input index
|
416 | * - Array of trace indexes -> Input array unchanged
|
417 | *
|
418 | * @param {undefined|null|Number|Array.<Number>} trace_indexes
|
419 | * @returns {Array.<Number>}
|
420 | * Array of trace indexes
|
421 | * @private
|
422 | */
|
423 | _normalize_trace_indexes: function (trace_indexes) {
|
424 | if (trace_indexes === null || trace_indexes === undefined) {
|
425 | var numTraces = this.get("_data").length;
|
426 | trace_indexes = _.range(numTraces);
|
427 | }
|
428 | if (!Array.isArray(trace_indexes)) {
|
429 | // Make sure idx is an array
|
430 | trace_indexes = [trace_indexes];
|
431 | }
|
432 | return trace_indexes;
|
433 | },
|
434 |
|
435 | /**
|
436 | * Log changes to the _data trait
|
437 | *
|
438 | * This should only happed on FigureModel initialization
|
439 | */
|
440 | do_data: function () {},
|
441 |
|
442 | /**
|
443 | * Log changes to the _layout trait
|
444 | *
|
445 | * This should only happed on FigureModel initialization
|
446 | */
|
447 | do_layout: function () {},
|
448 |
|
449 | /**
|
450 | * Handle addTraces message
|
451 | */
|
452 | do_addTraces: function () {
|
453 | // add trace to plot
|
454 | /** @type {Py2JsAddTracesMsg} */
|
455 | var msgData = this.get("_py2js_addTraces");
|
456 |
|
457 | if (msgData !== null) {
|
458 | var currentTraces = this.get("_data");
|
459 | var newTraces = msgData.trace_data;
|
460 | _.forEach(newTraces, function (newTrace) {
|
461 | currentTraces.push(newTrace);
|
462 | });
|
463 | }
|
464 | },
|
465 |
|
466 | /**
|
467 | * Handle deleteTraces message
|
468 | */
|
469 | do_deleteTraces: function () {
|
470 | // remove traces from plot
|
471 |
|
472 | /** @type {Py2JsDeleteTracesMsg} */
|
473 | var msgData = this.get("_py2js_deleteTraces");
|
474 |
|
475 | if (msgData !== null) {
|
476 | var delete_inds = msgData.delete_inds;
|
477 | var tracesData = this.get("_data");
|
478 |
|
479 | // Remove del inds in reverse order so indexes remain valid
|
480 | // throughout loop
|
481 | delete_inds
|
482 | .slice()
|
483 | .reverse()
|
484 | .forEach(function (del_ind) {
|
485 | tracesData.splice(del_ind, 1);
|
486 | });
|
487 | }
|
488 | },
|
489 |
|
490 | /**
|
491 | * Handle moveTraces message
|
492 | */
|
493 | do_moveTraces: function () {
|
494 | /** @type {Py2JsMoveTracesMsg} */
|
495 | var msgData = this.get("_py2js_moveTraces");
|
496 |
|
497 | if (msgData !== null) {
|
498 | var tracesData = this.get("_data");
|
499 | var currentInds = msgData.current_trace_inds;
|
500 | var newInds = msgData.new_trace_inds;
|
501 |
|
502 | performMoveTracesLike(tracesData, currentInds, newInds);
|
503 | }
|
504 | },
|
505 |
|
506 | /**
|
507 | * Handle restyle message
|
508 | */
|
509 | do_restyle: function () {
|
510 | /** @type {Py2JsRestyleMsg} */
|
511 | var msgData = this.get("_py2js_restyle");
|
512 | if (msgData !== null) {
|
513 | var restyleData = msgData.restyle_data;
|
514 | var restyleTraces = this._normalize_trace_indexes(
|
515 | msgData.restyle_traces
|
516 | );
|
517 | performRestyleLike(this.get("_data"), restyleData, restyleTraces);
|
518 | }
|
519 | },
|
520 |
|
521 | /**
|
522 | * Handle relayout message
|
523 | */
|
524 | do_relayout: function () {
|
525 | /** @type {Py2JsRelayoutMsg} */
|
526 | var msgData = this.get("_py2js_relayout");
|
527 |
|
528 | if (msgData !== null) {
|
529 | performRelayoutLike(this.get("_layout"), msgData.relayout_data);
|
530 | }
|
531 | },
|
532 |
|
533 | /**
|
534 | * Handle update message
|
535 | */
|
536 | do_update: function () {
|
537 | /** @type {Py2JsUpdateMsg} */
|
538 | var msgData = this.get("_py2js_update");
|
539 |
|
540 | if (msgData !== null) {
|
541 | var style = msgData.style_data;
|
542 | var layout = msgData.layout_data;
|
543 | var styleTraces = this._normalize_trace_indexes(msgData.style_traces);
|
544 | performRestyleLike(this.get("_data"), style, styleTraces);
|
545 | performRelayoutLike(this.get("_layout"), layout);
|
546 | }
|
547 | },
|
548 |
|
549 | /**
|
550 | * Handle animate message
|
551 | */
|
552 | do_animate: function () {
|
553 | /** @type {Py2JsAnimateMsg} */
|
554 | var msgData = this.get("_py2js_animate");
|
555 | if (msgData !== null) {
|
556 | var styles = msgData.style_data;
|
557 | var layout = msgData.layout_data;
|
558 | var trace_indexes = this._normalize_trace_indexes(msgData.style_traces);
|
559 |
|
560 | for (var i = 0; i < styles.length; i++) {
|
561 | var style = styles[i];
|
562 | var trace_index = trace_indexes[i];
|
563 | var trace = this.get("_data")[trace_index];
|
564 | performRelayoutLike(trace, style);
|
565 | }
|
566 |
|
567 | performRelayoutLike(this.get("_layout"), layout);
|
568 | }
|
569 | },
|
570 |
|
571 | /**
|
572 | * Handle removeLayoutProps message
|
573 | */
|
574 | do_removeLayoutProps: function () {
|
575 | /** @type {Py2JsRemoveLayoutPropsMsg} */
|
576 | var msgData = this.get("_py2js_removeLayoutProps");
|
577 |
|
578 | if (msgData !== null) {
|
579 | var keyPaths = msgData.remove_props;
|
580 | var layout = this.get("_layout");
|
581 | performRemoveProps(layout, keyPaths);
|
582 | }
|
583 | },
|
584 |
|
585 | /**
|
586 | * Handle removeTraceProps message
|
587 | */
|
588 | do_removeTraceProps: function () {
|
589 | /** @type {Py2JsRemoveTracePropsMsg} */
|
590 | var msgData = this.get("_py2js_removeTraceProps");
|
591 | if (msgData !== null) {
|
592 | var keyPaths = msgData.remove_props;
|
593 | var traceIndex = msgData.remove_trace;
|
594 | var trace = this.get("_data")[traceIndex];
|
595 |
|
596 | performRemoveProps(trace, keyPaths);
|
597 | }
|
598 | },
|
599 | },
|
600 | {
|
601 | serializers: _.extend(
|
602 | {
|
603 | _data: { deserialize: py2js_deserializer, serialize: js2py_serializer },
|
604 | _layout: {
|
605 | deserialize: py2js_deserializer,
|
606 | serialize: js2py_serializer,
|
607 | },
|
608 | _py2js_addTraces: {
|
609 | deserialize: py2js_deserializer,
|
610 | serialize: js2py_serializer,
|
611 | },
|
612 | _py2js_deleteTraces: {
|
613 | deserialize: py2js_deserializer,
|
614 | serialize: js2py_serializer,
|
615 | },
|
616 | _py2js_moveTraces: {
|
617 | deserialize: py2js_deserializer,
|
618 | serialize: js2py_serializer,
|
619 | },
|
620 | _py2js_restyle: {
|
621 | deserialize: py2js_deserializer,
|
622 | serialize: js2py_serializer,
|
623 | },
|
624 | _py2js_relayout: {
|
625 | deserialize: py2js_deserializer,
|
626 | serialize: js2py_serializer,
|
627 | },
|
628 | _py2js_update: {
|
629 | deserialize: py2js_deserializer,
|
630 | serialize: js2py_serializer,
|
631 | },
|
632 | _py2js_animate: {
|
633 | deserialize: py2js_deserializer,
|
634 | serialize: js2py_serializer,
|
635 | },
|
636 | _py2js_removeLayoutProps: {
|
637 | deserialize: py2js_deserializer,
|
638 | serialize: js2py_serializer,
|
639 | },
|
640 | _py2js_removeTraceProps: {
|
641 | deserialize: py2js_deserializer,
|
642 | serialize: js2py_serializer,
|
643 | },
|
644 | _js2py_restyle: {
|
645 | deserialize: py2js_deserializer,
|
646 | serialize: js2py_serializer,
|
647 | },
|
648 | _js2py_relayout: {
|
649 | deserialize: py2js_deserializer,
|
650 | serialize: js2py_serializer,
|
651 | },
|
652 | _js2py_update: {
|
653 | deserialize: py2js_deserializer,
|
654 | serialize: js2py_serializer,
|
655 | },
|
656 | _js2py_layoutDelta: {
|
657 | deserialize: py2js_deserializer,
|
658 | serialize: js2py_serializer,
|
659 | },
|
660 | _js2py_traceDeltas: {
|
661 | deserialize: py2js_deserializer,
|
662 | serialize: js2py_serializer,
|
663 | },
|
664 | _js2py_pointsCallback: {
|
665 | deserialize: py2js_deserializer,
|
666 | serialize: js2py_serializer,
|
667 | },
|
668 | },
|
669 | widgets.DOMWidgetModel.serializers
|
670 | ),
|
671 | }
|
672 | );
|
673 |
|
674 | // View
|
675 | // ====
|
676 | /**
|
677 | * A FigureView manages the visual presentation of a single Plotly.js
|
678 | * figure for a single notebook output cell. Each FigureView has a
|
679 | * reference to FigureModel. Multiple views may share a single model
|
680 | * instance, as is the case when a Python FigureWidget is displayed in
|
681 | * multiple notebook output cells.
|
682 | *
|
683 | * @type {widgets.DOMWidgetView}
|
684 | */
|
685 | var FigureView = widgets.DOMWidgetView.extend({
|
686 | /**
|
687 | * The perform_render method is called by processPhosphorMessage
|
688 | * after the widget's DOM element has been attached to the notebook
|
689 | * output cell. This happens after the initialize of the
|
690 | * FigureModel, and it won't happen at all if the Python FigureWidget
|
691 | * is never displayed in a notebook output cell
|
692 | */
|
693 | perform_render: function () {
|
694 | var that = this;
|
695 |
|
696 | // Wire up message property callbacks
|
697 | // ----------------------------------
|
698 | // Python -> JS event properties
|
699 | this.model.on("change:_py2js_addTraces", this.do_addTraces, this);
|
700 | this.model.on("change:_py2js_deleteTraces", this.do_deleteTraces, this);
|
701 | this.model.on("change:_py2js_moveTraces", this.do_moveTraces, this);
|
702 | this.model.on("change:_py2js_restyle", this.do_restyle, this);
|
703 | this.model.on("change:_py2js_relayout", this.do_relayout, this);
|
704 | this.model.on("change:_py2js_update", this.do_update, this);
|
705 | this.model.on("change:_py2js_animate", this.do_animate, this);
|
706 |
|
707 | // MathJax configuration
|
708 | // ---------------------
|
709 | if (window.MathJax) {
|
710 | MathJax.Hub.Config({ SVG: { font: "STIX-Web" } });
|
711 | }
|
712 |
|
713 | // Get message ids
|
714 | // ---------------------
|
715 | var layout_edit_id = this.model.get("_last_layout_edit_id");
|
716 | var trace_edit_id = this.model.get("_last_trace_edit_id");
|
717 |
|
718 | // Set view UID
|
719 | // ------------
|
720 | this.viewID = randstr();
|
721 |
|
722 | // Initialize Plotly.js figure
|
723 | // ---------------------------
|
724 | // We must clone the model's data and layout properties so that
|
725 | // the model is not directly mutated by the Plotly.js library.
|
726 | var initialTraces = _.cloneDeep(this.model.get("_data"));
|
727 | var initialLayout = _.cloneDeep(this.model.get("_layout"));
|
728 | var config = this.model.get("_config");
|
729 |
|
730 | Plotly.newPlot(that.el, initialTraces, initialLayout, config).then(
|
731 | function () {
|
732 | // ### Send trace deltas ###
|
733 | // We create an array of deltas corresponding to the new
|
734 | // traces.
|
735 | that._sendTraceDeltas(trace_edit_id);
|
736 |
|
737 | // ### Send layout delta ###
|
738 | that._sendLayoutDelta(layout_edit_id);
|
739 |
|
740 | // Wire up plotly event callbacks
|
741 | that.el.on("plotly_restyle", function (update) {
|
742 | that.handle_plotly_restyle(update);
|
743 | });
|
744 | that.el.on("plotly_relayout", function (update) {
|
745 | that.handle_plotly_relayout(update);
|
746 | });
|
747 | that.el.on("plotly_update", function (update) {
|
748 | that.handle_plotly_update(update);
|
749 | });
|
750 | that.el.on("plotly_click", function (update) {
|
751 | that.handle_plotly_click(update);
|
752 | });
|
753 | that.el.on("plotly_hover", function (update) {
|
754 | that.handle_plotly_hover(update);
|
755 | });
|
756 | that.el.on("plotly_unhover", function (update) {
|
757 | that.handle_plotly_unhover(update);
|
758 | });
|
759 | that.el.on("plotly_selected", function (update) {
|
760 | that.handle_plotly_selected(update);
|
761 | });
|
762 | that.el.on("plotly_deselect", function (update) {
|
763 | that.handle_plotly_deselect(update);
|
764 | });
|
765 | that.el.on("plotly_doubleclick", function (update) {
|
766 | that.handle_plotly_doubleclick(update);
|
767 | });
|
768 |
|
769 | // Emit event indicating that the widget has finished
|
770 | // rendering
|
771 | var event = new CustomEvent("plotlywidget-after-render", {
|
772 | detail: { element: that.el, viewID: that.viewID },
|
773 | });
|
774 |
|
775 | // Dispatch/Trigger/Fire the event
|
776 | document.dispatchEvent(event);
|
777 | }
|
778 | );
|
779 | },
|
780 |
|
781 | /**
|
782 | * Respond to phosphorjs events
|
783 | */
|
784 | processPhosphorMessage: function (msg) {
|
785 | FigureView.__super__.processPhosphorMessage.apply(this, arguments);
|
786 | var that = this;
|
787 | switch (msg.type) {
|
788 | case "before-attach":
|
789 | // Render an initial empty figure. This establishes with
|
790 | // the page that the element will not be empty, avoiding
|
791 | // some occasions where the dynamic sizing behavior leads
|
792 | // to collapsed figure dimensions.
|
793 | var axisHidden = {
|
794 | showgrid: false,
|
795 | showline: false,
|
796 | tickvals: [],
|
797 | };
|
798 |
|
799 | Plotly.newPlot(that.el, [], {
|
800 | xaxis: axisHidden,
|
801 | yaxis: axisHidden,
|
802 | });
|
803 |
|
804 | window.addEventListener("resize", function () {
|
805 | that.autosizeFigure();
|
806 | });
|
807 | break;
|
808 | case "after-attach":
|
809 | // Rendering actual figure in the after-attach event allows
|
810 | // Plotly.js to size the figure to fill the available element
|
811 | this.perform_render();
|
812 | break;
|
813 | case "resize":
|
814 | this.autosizeFigure();
|
815 | break;
|
816 | }
|
817 | },
|
818 |
|
819 | autosizeFigure: function () {
|
820 | var that = this;
|
821 | var layout = that.model.get("_layout");
|
822 | if (_.isNil(layout) || _.isNil(layout.width)) {
|
823 | Plotly.Plots.resize(that.el).then(function () {
|
824 | var layout_edit_id = that.model.get("_last_layout_edit_id");
|
825 | that._sendLayoutDelta(layout_edit_id);
|
826 | });
|
827 | }
|
828 | },
|
829 |
|
830 | /**
|
831 | * Purge Plotly.js data structures from the notebook output display
|
832 | * element when the view is destroyed
|
833 | */
|
834 | destroy: function () {
|
835 | Plotly.purge(this.el);
|
836 | },
|
837 |
|
838 | /**
|
839 | * Return the figure's _fullData array merged with its data array
|
840 | *
|
841 | * The merge ensures that for any properties that el._fullData and
|
842 | * el.data have in common, we return the version from el.data
|
843 | *
|
844 | * Named colorscales are one example of why this is needed. The el.data
|
845 | * array will hold named colorscale strings (e.g. 'Viridis'), while the
|
846 | * el._fullData array will hold the actual colorscale array. e.g.
|
847 | *
|
848 | * el.data[0].marker.colorscale == 'Viridis' but
|
849 | * el._fullData[0].marker.colorscale = [[..., ...], ...]
|
850 | *
|
851 | * Performing the merge allows our FigureModel to retain the 'Viridis'
|
852 | * string, rather than having it overridded by the colorscale array.
|
853 | *
|
854 | */
|
855 | getFullData: function () {
|
856 | return _.mergeWith(
|
857 | {},
|
858 | this.el._fullData,
|
859 | this.el.data,
|
860 | fullMergeCustomizer
|
861 | );
|
862 | },
|
863 |
|
864 | /**
|
865 | * Return the figure's _fullLayout object merged with its layout object
|
866 | *
|
867 | * See getFullData documentation for discussion of why the merge is
|
868 | * necessary
|
869 | */
|
870 | getFullLayout: function () {
|
871 | return _.mergeWith(
|
872 | {},
|
873 | this.el._fullLayout,
|
874 | this.el.layout,
|
875 | fullMergeCustomizer
|
876 | );
|
877 | },
|
878 |
|
879 | /**
|
880 | * Build Points data structure from data supplied by the plotly_click,
|
881 | * plotly_hover, or plotly_select events
|
882 | * @param {Object} data
|
883 | * @returns {null|Points}
|
884 | */
|
885 | buildPointsObject: function (data) {
|
886 | var pointsObject;
|
887 | if (data.hasOwnProperty("points")) {
|
888 | // Most cartesian plots
|
889 | var pointObjects = data["points"];
|
890 | var numPoints = pointObjects.length;
|
891 |
|
892 | var hasNestedPointObjects = true;
|
893 | for (let i = 0; i < numPoints; i++) {
|
894 | hasNestedPointObjects = (hasNestedPointObjects && pointObjects[i].hasOwnProperty("pointNumbers"));
|
895 | if (!hasNestedPointObjects) break;
|
896 | }
|
897 | var numPointNumbers = numPoints;
|
898 | if (hasNestedPointObjects) {
|
899 | numPointNumbers = 0;
|
900 | for (let i = 0; i < numPoints; i++) {
|
901 | numPointNumbers += pointObjects[i]["pointNumbers"].length;
|
902 | }
|
903 | }
|
904 | pointsObject = {
|
905 | trace_indexes: new Array(numPointNumbers),
|
906 | point_indexes: new Array(numPointNumbers),
|
907 | xs: new Array(numPointNumbers),
|
908 | ys: new Array(numPointNumbers),
|
909 | };
|
910 |
|
911 | if (hasNestedPointObjects) {
|
912 | var flatPointIndex = 0;
|
913 | for (var p = 0; p < numPoints; p++) {
|
914 | for (let i = 0; i < pointObjects[p]["pointNumbers"].length; i++, flatPointIndex++) {
|
915 | pointsObject["point_indexes"][flatPointIndex] = pointObjects[p]["pointNumbers"][i]
|
916 | // also add xs, ys and traces so that the array doesn't get truncated later
|
917 | pointsObject["xs"][flatPointIndex] = pointObjects[p]["x"];
|
918 | pointsObject["ys"][flatPointIndex] = pointObjects[p]["y"];
|
919 | pointsObject["trace_indexes"][flatPointIndex] = pointObjects[p]["curveNumber"];
|
920 | }
|
921 | }
|
922 | pointsObject["point_indexes"].sort(function(a, b) {
|
923 | return a - b;
|
924 | });
|
925 | } else {
|
926 | for (var p = 0; p < numPoints; p++) {
|
927 | pointsObject["trace_indexes"][p] = pointObjects[p]["curveNumber"];
|
928 | pointsObject["point_indexes"][p] = pointObjects[p]["pointNumber"];
|
929 | pointsObject["xs"][p] = pointObjects[p]["x"];
|
930 | pointsObject["ys"][p] = pointObjects[p]["y"];
|
931 | }
|
932 | }
|
933 |
|
934 | // Add z if present
|
935 | var hasZ =
|
936 | pointObjects[0] !== undefined && pointObjects[0].hasOwnProperty("z");
|
937 | if (hasZ) {
|
938 | pointsObject["zs"] = new Array(numPoints);
|
939 | for (p = 0; p < numPoints; p++) {
|
940 | pointsObject["zs"][p] = pointObjects[p]["z"];
|
941 | }
|
942 | }
|
943 |
|
944 | return pointsObject;
|
945 | } else {
|
946 | return null;
|
947 | }
|
948 | },
|
949 |
|
950 | /**
|
951 | * Build InputDeviceState data structure from data supplied by the
|
952 | * plotly_click, plotly_hover, or plotly_select events
|
953 | * @param {Object} data
|
954 | * @returns {null|InputDeviceState}
|
955 | */
|
956 | buildInputDeviceStateObject: function (data) {
|
957 | var event = data["event"];
|
958 | if (event === undefined) {
|
959 | return null;
|
960 | } else {
|
961 | /** @type {InputDeviceState} */
|
962 | var inputDeviceState = {
|
963 | // Keyboard modifiers
|
964 | alt: event["altKey"],
|
965 | ctrl: event["ctrlKey"],
|
966 | meta: event["metaKey"],
|
967 | shift: event["shiftKey"],
|
968 |
|
969 | // Mouse buttons
|
970 | button: event["button"],
|
971 | buttons: event["buttons"],
|
972 | };
|
973 | return inputDeviceState;
|
974 | }
|
975 | },
|
976 |
|
977 | /**
|
978 | * Build Selector data structure from data supplied by the
|
979 | * plotly_select event
|
980 | * @param data
|
981 | * @returns {null|Selector}
|
982 | */
|
983 | buildSelectorObject: function (data) {
|
984 | var selectorObject;
|
985 |
|
986 | if (data.hasOwnProperty("range")) {
|
987 | // Box selection
|
988 | selectorObject = {
|
989 | type: "box",
|
990 | selector_state: {
|
991 | xrange: data["range"]["x"],
|
992 | yrange: data["range"]["y"],
|
993 | },
|
994 | };
|
995 | } else if (data.hasOwnProperty("lassoPoints")) {
|
996 | // Lasso selection
|
997 | selectorObject = {
|
998 | type: "lasso",
|
999 | selector_state: {
|
1000 | xs: data["lassoPoints"]["x"],
|
1001 | ys: data["lassoPoints"]["y"],
|
1002 | },
|
1003 | };
|
1004 | } else {
|
1005 | selectorObject = null;
|
1006 | }
|
1007 | return selectorObject;
|
1008 | },
|
1009 |
|
1010 | /**
|
1011 | * Handle ploty_restyle events emitted by the Plotly.js library
|
1012 | * @param data
|
1013 | */
|
1014 | handle_plotly_restyle: function (data) {
|
1015 | if (data === null || data === undefined) {
|
1016 | // No data to report to the Python side
|
1017 | return;
|
1018 | }
|
1019 |
|
1020 | if (data[0] && data[0].hasOwnProperty("_doNotReportToPy")) {
|
1021 | // Restyle originated on the Python side
|
1022 | return;
|
1023 | }
|
1024 |
|
1025 | // Unpack data
|
1026 | var styleData = data[0];
|
1027 | var styleTraces = data[1];
|
1028 |
|
1029 | // Construct restyle message to send to the Python side
|
1030 | /** @type {Js2PyRestyleMsg} */
|
1031 | var restyleMsg = {
|
1032 | style_data: styleData,
|
1033 | style_traces: styleTraces,
|
1034 | source_view_id: this.viewID,
|
1035 | };
|
1036 |
|
1037 | this.model.set("_js2py_restyle", restyleMsg);
|
1038 | this.touch();
|
1039 | },
|
1040 |
|
1041 | /**
|
1042 | * Handle plotly_relayout events emitted by the Plotly.js library
|
1043 | * @param data
|
1044 | */
|
1045 | handle_plotly_relayout: function (data) {
|
1046 | if (data === null || data === undefined) {
|
1047 | // No data to report to the Python side
|
1048 | return;
|
1049 | }
|
1050 |
|
1051 | if (data.hasOwnProperty("_doNotReportToPy")) {
|
1052 | // Relayout originated on the Python side
|
1053 | return;
|
1054 | }
|
1055 |
|
1056 | /** @type {Js2PyRelayoutMsg} */
|
1057 | var relayoutMsg = {
|
1058 | relayout_data: data,
|
1059 | source_view_id: this.viewID,
|
1060 | };
|
1061 |
|
1062 | this.model.set("_js2py_relayout", relayoutMsg);
|
1063 | this.touch();
|
1064 | },
|
1065 |
|
1066 | /**
|
1067 | * Handle plotly_update events emitted by the Plotly.js library
|
1068 | * @param data
|
1069 | */
|
1070 | handle_plotly_update: function (data) {
|
1071 | if (data === null || data === undefined) {
|
1072 | // No data to report to the Python side
|
1073 | return;
|
1074 | }
|
1075 |
|
1076 | if (data["data"] && data["data"][0].hasOwnProperty("_doNotReportToPy")) {
|
1077 | // Update originated on the Python side
|
1078 | return;
|
1079 | }
|
1080 |
|
1081 | /** @type {Js2PyUpdateMsg} */
|
1082 | var updateMsg = {
|
1083 | style_data: data["data"][0],
|
1084 | style_traces: data["data"][1],
|
1085 | layout_data: data["layout"],
|
1086 | source_view_id: this.viewID,
|
1087 | };
|
1088 |
|
1089 | // Log message
|
1090 | this.model.set("_js2py_update", updateMsg);
|
1091 | this.touch();
|
1092 | },
|
1093 |
|
1094 | /**
|
1095 | * Handle plotly_click events emitted by the Plotly.js library
|
1096 | * @param data
|
1097 | */
|
1098 | handle_plotly_click: function (data) {
|
1099 | this._send_points_callback_message(data, "plotly_click");
|
1100 | },
|
1101 |
|
1102 | /**
|
1103 | * Handle plotly_hover events emitted by the Plotly.js library
|
1104 | * @param data
|
1105 | */
|
1106 | handle_plotly_hover: function (data) {
|
1107 | this._send_points_callback_message(data, "plotly_hover");
|
1108 | },
|
1109 |
|
1110 | /**
|
1111 | * Handle plotly_unhover events emitted by the Plotly.js library
|
1112 | * @param data
|
1113 | */
|
1114 | handle_plotly_unhover: function (data) {
|
1115 | this._send_points_callback_message(data, "plotly_unhover");
|
1116 | },
|
1117 |
|
1118 | /**
|
1119 | * Handle plotly_selected events emitted by the Plotly.js library
|
1120 | * @param data
|
1121 | */
|
1122 | handle_plotly_selected: function (data) {
|
1123 | this._send_points_callback_message(data, "plotly_selected");
|
1124 | },
|
1125 |
|
1126 | /**
|
1127 | * Handle plotly_deselect events emitted by the Plotly.js library
|
1128 | * @param data
|
1129 | */
|
1130 | handle_plotly_deselect: function (data) {
|
1131 | data = {
|
1132 | points: [],
|
1133 | };
|
1134 | this._send_points_callback_message(data, "plotly_deselect");
|
1135 | },
|
1136 |
|
1137 | /**
|
1138 | * Build and send a points callback message to the Python side
|
1139 | *
|
1140 | * @param {Object} data
|
1141 | * data object as provided by the plotly_click, plotly_hover,
|
1142 | * plotly_unhover, or plotly_selected events
|
1143 | * @param {String} event_type
|
1144 | * Name of the triggering event. One of 'plotly_click',
|
1145 | * 'plotly_hover', 'plotly_unhover', or 'plotly_selected'
|
1146 | * @private
|
1147 | */
|
1148 | _send_points_callback_message: function (data, event_type) {
|
1149 | if (data === null || data === undefined) {
|
1150 | // No data to report to the Python side
|
1151 | return;
|
1152 | }
|
1153 |
|
1154 | /** @type {Js2PyPointsCallbackMsg} */
|
1155 | var pointsMsg = {
|
1156 | event_type: event_type,
|
1157 | points: this.buildPointsObject(data),
|
1158 | device_state: this.buildInputDeviceStateObject(data),
|
1159 | selector: this.buildSelectorObject(data),
|
1160 | };
|
1161 |
|
1162 | if (pointsMsg["points"] !== null && pointsMsg["points"] !== undefined) {
|
1163 | this.model.set("_js2py_pointsCallback", pointsMsg);
|
1164 | this.touch();
|
1165 | }
|
1166 | },
|
1167 |
|
1168 | /**
|
1169 | * Stub for future handling of plotly_doubleclick
|
1170 | * @param data
|
1171 | */
|
1172 | handle_plotly_doubleclick: function (data) {},
|
1173 |
|
1174 | /**
|
1175 | * Handle Plotly.addTraces request
|
1176 | */
|
1177 | do_addTraces: function () {
|
1178 | /** @type {Py2JsAddTracesMsg} */
|
1179 | var msgData = this.model.get("_py2js_addTraces");
|
1180 |
|
1181 | if (msgData !== null) {
|
1182 | // Save off original number of traces
|
1183 | var prevNumTraces = this.el.data.length;
|
1184 |
|
1185 | var that = this;
|
1186 | Plotly.addTraces(this.el, msgData.trace_data).then(function () {
|
1187 | // ### Send trace deltas ###
|
1188 | that._sendTraceDeltas(msgData.trace_edit_id);
|
1189 |
|
1190 | // ### Send layout delta ###
|
1191 | var layout_edit_id = msgData.layout_edit_id;
|
1192 | that._sendLayoutDelta(layout_edit_id);
|
1193 | });
|
1194 | }
|
1195 | },
|
1196 |
|
1197 | /**
|
1198 | * Handle Plotly.deleteTraces request
|
1199 | */
|
1200 | do_deleteTraces: function () {
|
1201 | /** @type {Py2JsDeleteTracesMsg} */
|
1202 | var msgData = this.model.get("_py2js_deleteTraces");
|
1203 |
|
1204 | if (msgData !== null) {
|
1205 | var delete_inds = msgData.delete_inds;
|
1206 | var that = this;
|
1207 | Plotly.deleteTraces(this.el, delete_inds).then(function () {
|
1208 | // ### Send trace deltas ###
|
1209 | var trace_edit_id = msgData.trace_edit_id;
|
1210 | that._sendTraceDeltas(trace_edit_id);
|
1211 |
|
1212 | // ### Send layout delta ###
|
1213 | var layout_edit_id = msgData.layout_edit_id;
|
1214 | that._sendLayoutDelta(layout_edit_id);
|
1215 | });
|
1216 | }
|
1217 | },
|
1218 |
|
1219 | /**
|
1220 | * Handle Plotly.moveTraces request
|
1221 | */
|
1222 | do_moveTraces: function () {
|
1223 | /** @type {Py2JsMoveTracesMsg} */
|
1224 | var msgData = this.model.get("_py2js_moveTraces");
|
1225 |
|
1226 | if (msgData !== null) {
|
1227 | // Unpack message
|
1228 | var currentInds = msgData.current_trace_inds;
|
1229 | var newInds = msgData.new_trace_inds;
|
1230 |
|
1231 | // Check if the new trace indexes are actually different than
|
1232 | // the current indexes
|
1233 | var inds_equal = _.isEqual(currentInds, newInds);
|
1234 |
|
1235 | if (!inds_equal) {
|
1236 | Plotly.moveTraces(this.el, currentInds, newInds);
|
1237 | }
|
1238 | }
|
1239 | },
|
1240 |
|
1241 | /**
|
1242 | * Handle Plotly.restyle request
|
1243 | */
|
1244 | do_restyle: function () {
|
1245 | /** @type {Py2JsRestyleMsg} */
|
1246 | var msgData = this.model.get("_py2js_restyle");
|
1247 | if (msgData !== null) {
|
1248 | var restyleData = msgData.restyle_data;
|
1249 | var traceIndexes = this.model._normalize_trace_indexes(
|
1250 | msgData.restyle_traces
|
1251 | );
|
1252 |
|
1253 | restyleData["_doNotReportToPy"] = true;
|
1254 | Plotly.restyle(this.el, restyleData, traceIndexes);
|
1255 |
|
1256 | // ### Send trace deltas ###
|
1257 | // We create an array of deltas corresponding to the restyled
|
1258 | // traces.
|
1259 | this._sendTraceDeltas(msgData.trace_edit_id);
|
1260 |
|
1261 | // ### Send layout delta ###
|
1262 | var layout_edit_id = msgData.layout_edit_id;
|
1263 | this._sendLayoutDelta(layout_edit_id);
|
1264 | }
|
1265 | },
|
1266 |
|
1267 | /**
|
1268 | * Handle Plotly.relayout request
|
1269 | */
|
1270 | do_relayout: function () {
|
1271 | /** @type {Py2JsRelayoutMsg} */
|
1272 | var msgData = this.model.get("_py2js_relayout");
|
1273 | if (msgData !== null) {
|
1274 | if (msgData.source_view_id !== this.viewID) {
|
1275 | var relayoutData = msgData.relayout_data;
|
1276 | relayoutData["_doNotReportToPy"] = true;
|
1277 | Plotly.relayout(this.el, msgData.relayout_data);
|
1278 | }
|
1279 |
|
1280 | // ### Send layout delta ###
|
1281 | var layout_edit_id = msgData.layout_edit_id;
|
1282 | this._sendLayoutDelta(layout_edit_id);
|
1283 | }
|
1284 | },
|
1285 |
|
1286 | /**
|
1287 | * Handle Plotly.update request
|
1288 | */
|
1289 | do_update: function () {
|
1290 | /** @type {Py2JsUpdateMsg} */
|
1291 | var msgData = this.model.get("_py2js_update");
|
1292 |
|
1293 | if (msgData !== null) {
|
1294 | var style = msgData.style_data || {};
|
1295 | var layout = msgData.layout_data || {};
|
1296 | var traceIndexes = this.model._normalize_trace_indexes(
|
1297 | msgData.style_traces
|
1298 | );
|
1299 |
|
1300 | style["_doNotReportToPy"] = true;
|
1301 | Plotly.update(this.el, style, layout, traceIndexes);
|
1302 |
|
1303 | // ### Send trace deltas ###
|
1304 | // We create an array of deltas corresponding to the updated
|
1305 | // traces.
|
1306 | this._sendTraceDeltas(msgData.trace_edit_id);
|
1307 |
|
1308 | // ### Send layout delta ###
|
1309 | var layout_edit_id = msgData.layout_edit_id;
|
1310 | this._sendLayoutDelta(layout_edit_id);
|
1311 | }
|
1312 | },
|
1313 |
|
1314 | /**
|
1315 | * Handle Plotly.animate request
|
1316 | */
|
1317 | do_animate: function () {
|
1318 | /** @type {Py2JsAnimateMsg} */
|
1319 | var msgData = this.model.get("_py2js_animate");
|
1320 |
|
1321 | if (msgData !== null) {
|
1322 | // Unpack params
|
1323 | // var animationData = msgData[0];
|
1324 | var animationOpts = msgData.animation_opts;
|
1325 |
|
1326 | var styles = msgData.style_data;
|
1327 | var layout = msgData.layout_data;
|
1328 | var traceIndexes = this.model._normalize_trace_indexes(
|
1329 | msgData.style_traces
|
1330 | );
|
1331 |
|
1332 | var animationData = {
|
1333 | data: styles,
|
1334 | layout: layout,
|
1335 | traces: traceIndexes,
|
1336 | };
|
1337 |
|
1338 | animationData["_doNotReportToPy"] = true;
|
1339 | var that = this;
|
1340 |
|
1341 | Plotly.animate(this.el, animationData, animationOpts).then(function () {
|
1342 | // ### Send trace deltas ###
|
1343 | // We create an array of deltas corresponding to the
|
1344 | // animated traces.
|
1345 | that._sendTraceDeltas(msgData.trace_edit_id);
|
1346 |
|
1347 | // ### Send layout delta ###
|
1348 | var layout_edit_id = msgData.layout_edit_id;
|
1349 | that._sendLayoutDelta(layout_edit_id);
|
1350 | });
|
1351 | }
|
1352 | },
|
1353 |
|
1354 | /**
|
1355 | * Construct layout delta object and send layoutDelta message to the
|
1356 | * Python side
|
1357 | *
|
1358 | * @param layout_edit_id
|
1359 | * Edit ID of message that triggered the creation of the layout delta
|
1360 | * @private
|
1361 | */
|
1362 | _sendLayoutDelta: function (layout_edit_id) {
|
1363 | // ### Handle layout delta ###
|
1364 | var layout_delta = createDeltaObject(
|
1365 | this.getFullLayout(),
|
1366 | this.model.get("_layout")
|
1367 | );
|
1368 |
|
1369 | /** @type{Js2PyLayoutDeltaMsg} */
|
1370 | var layoutDeltaMsg = {
|
1371 | layout_delta: layout_delta,
|
1372 | layout_edit_id: layout_edit_id,
|
1373 | };
|
1374 |
|
1375 | this.model.set("_js2py_layoutDelta", layoutDeltaMsg);
|
1376 | this.touch();
|
1377 | },
|
1378 |
|
1379 | /**
|
1380 | * Construct trace deltas array for the requested trace indexes and
|
1381 | * send traceDeltas message to the Python side
|
1382 | * Array of indexes of traces for which to compute deltas
|
1383 | * @param trace_edit_id
|
1384 | * Edit ID of message that triggered the creation of trace deltas
|
1385 | * @private
|
1386 | */
|
1387 | _sendTraceDeltas: function (trace_edit_id) {
|
1388 | var trace_data = this.model.get("_data");
|
1389 | var traceIndexes = _.range(trace_data.length);
|
1390 | var trace_deltas = new Array(traceIndexes.length);
|
1391 |
|
1392 | var fullData = this.getFullData();
|
1393 | for (var i = 0; i < traceIndexes.length; i++) {
|
1394 | var traceInd = traceIndexes[i];
|
1395 | trace_deltas[i] = createDeltaObject(
|
1396 | fullData[traceInd],
|
1397 | trace_data[traceInd]
|
1398 | );
|
1399 | }
|
1400 |
|
1401 | /** @type{Js2PyTraceDeltasMsg} */
|
1402 | var traceDeltasMsg = {
|
1403 | trace_deltas: trace_deltas,
|
1404 | trace_edit_id: trace_edit_id,
|
1405 | };
|
1406 |
|
1407 | this.model.set("_js2py_traceDeltas", traceDeltasMsg);
|
1408 | this.touch();
|
1409 | },
|
1410 | });
|
1411 |
|
1412 | // Serialization
|
1413 | /**
|
1414 | * Create a mapping from numpy dtype strings to corresponding typed array
|
1415 | * constructors
|
1416 | */
|
1417 | var numpy_dtype_to_typedarray_type = {
|
1418 | int8: Int8Array,
|
1419 | int16: Int16Array,
|
1420 | int32: Int32Array,
|
1421 | uint8: Uint8Array,
|
1422 | uint16: Uint16Array,
|
1423 | uint32: Uint32Array,
|
1424 | float32: Float32Array,
|
1425 | float64: Float64Array,
|
1426 | };
|
1427 |
|
1428 | function serializeTypedArray(v) {
|
1429 | var numpyType;
|
1430 | if (v instanceof Int8Array) {
|
1431 | numpyType = "int8";
|
1432 | } else if (v instanceof Int16Array) {
|
1433 | numpyType = "int16";
|
1434 | } else if (v instanceof Int32Array) {
|
1435 | numpyType = "int32";
|
1436 | } else if (v instanceof Uint8Array) {
|
1437 | numpyType = "uint8";
|
1438 | } else if (v instanceof Uint16Array) {
|
1439 | numpyType = "uint16";
|
1440 | } else if (v instanceof Uint32Array) {
|
1441 | numpyType = "uint32";
|
1442 | } else if (v instanceof Float32Array) {
|
1443 | numpyType = "float32";
|
1444 | } else if (v instanceof Float64Array) {
|
1445 | numpyType = "float64";
|
1446 | } else {
|
1447 | // Don't understand it, return as is
|
1448 | return v;
|
1449 | }
|
1450 | var res = {
|
1451 | dtype: numpyType,
|
1452 | shape: [v.length],
|
1453 | value: v.buffer,
|
1454 | };
|
1455 | return res;
|
1456 | }
|
1457 |
|
1458 | /**
|
1459 | * ipywidget JavaScript -> Python serializer
|
1460 | */
|
1461 | function js2py_serializer(v, widgetManager) {
|
1462 | var res;
|
1463 |
|
1464 | if (_.isTypedArray(v)) {
|
1465 | res = serializeTypedArray(v);
|
1466 | } else if (Array.isArray(v)) {
|
1467 | // Serialize array elements recursively
|
1468 | res = new Array(v.length);
|
1469 | for (var i = 0; i < v.length; i++) {
|
1470 | res[i] = js2py_serializer(v[i]);
|
1471 | }
|
1472 | } else if (_.isPlainObject(v)) {
|
1473 | // Serialize object properties recursively
|
1474 | res = {};
|
1475 | for (var p in v) {
|
1476 | if (v.hasOwnProperty(p)) {
|
1477 | res[p] = js2py_serializer(v[p]);
|
1478 | }
|
1479 | }
|
1480 | } else if (v === undefined) {
|
1481 | // Translate undefined into '_undefined_' sentinal string. The
|
1482 | // Python _js_to_py deserializer will convert this into an
|
1483 | // Undefined object
|
1484 | res = "_undefined_";
|
1485 | } else {
|
1486 | // Primitive value to transfer directly
|
1487 | res = v;
|
1488 | }
|
1489 | return res;
|
1490 | }
|
1491 |
|
1492 | /**
|
1493 | * ipywidget Python -> Javascript deserializer
|
1494 | */
|
1495 | function py2js_deserializer(v, widgetManager) {
|
1496 | var res;
|
1497 |
|
1498 | if (Array.isArray(v)) {
|
1499 | // Deserialize array elements recursively
|
1500 | res = new Array(v.length);
|
1501 | for (var i = 0; i < v.length; i++) {
|
1502 | res[i] = py2js_deserializer(v[i]);
|
1503 | }
|
1504 | } else if (_.isPlainObject(v)) {
|
1505 | if (
|
1506 | (_.has(v, "value") || _.has(v, "buffer")) &&
|
1507 | _.has(v, "dtype") &&
|
1508 | _.has(v, "shape")
|
1509 | ) {
|
1510 | // Deserialize special buffer/dtype/shape objects into typed arrays
|
1511 | // These objects correspond to numpy arrays on the Python side
|
1512 | //
|
1513 | // Note plotly.py<=3.1.1 called the buffer object `buffer`
|
1514 | // This was renamed `value` in 3.2 to work around a naming conflict
|
1515 | // when saving widget state to a notebook.
|
1516 | var typedarray_type = numpy_dtype_to_typedarray_type[v.dtype];
|
1517 | var buffer = _.has(v, "value") ? v.value.buffer : v.buffer.buffer;
|
1518 | res = new typedarray_type(buffer);
|
1519 | } else {
|
1520 | // Deserialize object properties recursively
|
1521 | res = {};
|
1522 | for (var p in v) {
|
1523 | if (v.hasOwnProperty(p)) {
|
1524 | res[p] = py2js_deserializer(v[p]);
|
1525 | }
|
1526 | }
|
1527 | }
|
1528 | } else if (v === "_undefined_") {
|
1529 | // Convert the _undefined_ sentinal into undefined
|
1530 | res = undefined;
|
1531 | } else {
|
1532 | // Accept primitive value directly
|
1533 | res = v;
|
1534 | }
|
1535 | return res;
|
1536 | }
|
1537 |
|
1538 | /**
|
1539 | * Return whether the input value is a typed array
|
1540 | * @param potentialTypedArray
|
1541 | * Value to examine
|
1542 | * @returns {boolean}
|
1543 | */
|
1544 | function isTypedArray(potentialTypedArray) {
|
1545 | return (
|
1546 | ArrayBuffer.isView(potentialTypedArray) &&
|
1547 | !(potentialTypedArray instanceof DataView)
|
1548 | );
|
1549 | }
|
1550 |
|
1551 | /**
|
1552 | * Customizer for use with lodash's mergeWith function
|
1553 | *
|
1554 | * The customizer ensures that typed arrays are not converted into standard
|
1555 | * arrays during the recursive merge
|
1556 | *
|
1557 | * See: https://lodash.com/docs/latest#mergeWith
|
1558 | */
|
1559 | function fullMergeCustomizer(objValue, srcValue, key) {
|
1560 | if (key[0] === "_") {
|
1561 | // Don't recurse into private properties
|
1562 | return null;
|
1563 | } else if (isTypedArray(srcValue)) {
|
1564 | // Return typed arrays directly, don't recurse inside
|
1565 | return srcValue;
|
1566 | }
|
1567 | }
|
1568 |
|
1569 | /**
|
1570 | * Reform a Plotly.relayout like operation on an input object
|
1571 | *
|
1572 | * @param {Object} parentObj
|
1573 | * The object that the relayout operation should be applied to
|
1574 | * @param {Object} relayoutData
|
1575 | * An relayout object as accepted by Plotly.relayout
|
1576 | *
|
1577 | * Examples:
|
1578 | * var d = {foo {bar [5, 10]}};
|
1579 | * performRelayoutLike(d, {'foo.bar': [0, 1]});
|
1580 | * d -> {foo: {bar: [0, 1]}}
|
1581 | *
|
1582 | * var d = {foo {bar [5, 10]}};
|
1583 | * performRelayoutLike(d, {'baz': 34});
|
1584 | * d -> {foo: {bar: [5, 10]}, baz: 34}
|
1585 | *
|
1586 | * var d = {foo: {bar: [5, 10]};
|
1587 | * performRelayoutLike(d, {'foo.baz[1]': 17});
|
1588 | * d -> {foo: {bar: [5, 17]}}
|
1589 | *
|
1590 | */
|
1591 | function performRelayoutLike(parentObj, relayoutData) {
|
1592 | // Perform a relayout style operation on a given parent object
|
1593 | for (var rawKey in relayoutData) {
|
1594 | if (!relayoutData.hasOwnProperty(rawKey)) {
|
1595 | continue;
|
1596 | }
|
1597 |
|
1598 | // Extract value for this key
|
1599 | var relayoutVal = relayoutData[rawKey];
|
1600 |
|
1601 | // Set property value
|
1602 | if (relayoutVal === null) {
|
1603 | _.unset(parentObj, rawKey);
|
1604 | } else {
|
1605 | _.set(parentObj, rawKey, relayoutVal);
|
1606 | }
|
1607 | }
|
1608 | }
|
1609 |
|
1610 | /**
|
1611 | * Perform a Plotly.restyle like operation on an input object array
|
1612 | *
|
1613 | * @param {Array.<Object>} parentArray
|
1614 | * The object that the restyle operation should be applied to
|
1615 | * @param {Object} restyleData
|
1616 | * A restyle object as accepted by Plotly.restyle
|
1617 | * @param {Array.<Number>} restyleTraces
|
1618 | * Array of indexes of the traces that the resytle operation applies to
|
1619 | *
|
1620 | * Examples:
|
1621 | * var d = [{foo: {bar: 1}}, {}, {}]
|
1622 | * performRestyleLike(d, {'foo.bar': 2}, [0])
|
1623 | * d -> [{foo: {bar: 2}}, {}, {}]
|
1624 | *
|
1625 | * var d = [{foo: {bar: 1}}, {}, {}]
|
1626 | * performRestyleLike(d, {'foo.bar': 2}, [0, 1, 2])
|
1627 | * d -> [{foo: {bar: 2}}, {foo: {bar: 2}}, {foo: {bar: 2}}]
|
1628 | *
|
1629 | * var d = [{foo: {bar: 1}}, {}, {}]
|
1630 | * performRestyleLike(d, {'foo.bar': [2, 3, 4]}, [0, 1, 2])
|
1631 | * d -> [{foo: {bar: 2}}, {foo: {bar: 3}}, {foo: {bar: 4}}]
|
1632 | *
|
1633 | */
|
1634 | function performRestyleLike(parentArray, restyleData, restyleTraces) {
|
1635 | // Loop over the properties of restyleData
|
1636 | for (var rawKey in restyleData) {
|
1637 | if (!restyleData.hasOwnProperty(rawKey)) {
|
1638 | continue;
|
1639 | }
|
1640 |
|
1641 | // Extract value for property and normalize into a value list
|
1642 | var valArray = restyleData[rawKey];
|
1643 | if (!Array.isArray(valArray)) {
|
1644 | valArray = [valArray];
|
1645 | }
|
1646 |
|
1647 | // Loop over the indexes of the traces being restyled
|
1648 | for (var i = 0; i < restyleTraces.length; i++) {
|
1649 | // Get trace object
|
1650 | var traceInd = restyleTraces[i];
|
1651 | var trace = parentArray[traceInd];
|
1652 |
|
1653 | // Extract value for this trace
|
1654 | var singleVal = valArray[i % valArray.length];
|
1655 |
|
1656 | // Set property value
|
1657 | if (singleVal === null) {
|
1658 | _.unset(trace, rawKey);
|
1659 | } else if (singleVal !== undefined) {
|
1660 | _.set(trace, rawKey, singleVal);
|
1661 | }
|
1662 | }
|
1663 | }
|
1664 | }
|
1665 |
|
1666 | /**
|
1667 | * Perform a Plotly.moveTraces like operation on an input object array
|
1668 | * @param parentArray
|
1669 | * The object that the moveTraces operation should be applied to
|
1670 | * @param currentInds
|
1671 | * Array of the current indexes of traces to be moved
|
1672 | * @param newInds
|
1673 | * Array of the new indexes that traces selected by currentInds should be
|
1674 | * moved to.
|
1675 | *
|
1676 | * Examples:
|
1677 | * var d = [{foo: 0}, {foo: 1}, {foo: 2}]
|
1678 | * performMoveTracesLike(d, [0, 1], [2, 0])
|
1679 | * d -> [{foo: 1}, {foo: 2}, {foo: 0}]
|
1680 | *
|
1681 | * var d = [{foo: 0}, {foo: 1}, {foo: 2}]
|
1682 | * performMoveTracesLike(d, [0, 2], [1, 2])
|
1683 | * d -> [{foo: 1}, {foo: 0}, {foo: 2}]
|
1684 | */
|
1685 | function performMoveTracesLike(parentArray, currentInds, newInds) {
|
1686 | // ### Remove by currentInds in reverse order ###
|
1687 | var movingTracesData = [];
|
1688 | for (var ci = currentInds.length - 1; ci >= 0; ci--) {
|
1689 | // Insert moving parentArray at beginning of the list
|
1690 | movingTracesData.splice(0, 0, parentArray[currentInds[ci]]);
|
1691 | parentArray.splice(currentInds[ci], 1);
|
1692 | }
|
1693 |
|
1694 | // ### Sort newInds and movingTracesData by newInds ###
|
1695 | var newIndexSortedArrays = _(newInds)
|
1696 | .zip(movingTracesData)
|
1697 | .sortBy(0)
|
1698 | .unzip()
|
1699 | .value();
|
1700 |
|
1701 | newInds = newIndexSortedArrays[0];
|
1702 | movingTracesData = newIndexSortedArrays[1];
|
1703 |
|
1704 | // ### Insert by newInds in forward order ###
|
1705 | for (var ni = 0; ni < newInds.length; ni++) {
|
1706 | parentArray.splice(newInds[ni], 0, movingTracesData[ni]);
|
1707 | }
|
1708 | }
|
1709 |
|
1710 | /**
|
1711 | * Remove nested properties from a parent object
|
1712 | * @param {Object} parentObj
|
1713 | * Parent object from which properties or nested properties should be removed
|
1714 | * @param {Array.<Array.<Number|String>>} keyPaths
|
1715 | * Array of key paths for properties that should be removed. Each key path
|
1716 | * is an array of properties names or array indexes that reference a
|
1717 | * property to be removed
|
1718 | *
|
1719 | * Examples:
|
1720 | * var d = {foo: [{bar: 0}, {bar: 1}], baz: 32}
|
1721 | * performRemoveProps(d, ['baz'])
|
1722 | * d -> {foo: [{bar: 0}, {bar: 1}]}
|
1723 | *
|
1724 | * var d = {foo: [{bar: 0}, {bar: 1}], baz: 32}
|
1725 | * performRemoveProps(d, ['foo[1].bar', 'baz'])
|
1726 | * d -> {foo: [{bar: 0}, {}]}
|
1727 | *
|
1728 | */
|
1729 | function performRemoveProps(parentObj, keyPaths) {
|
1730 | for (var i = 0; i < keyPaths.length; i++) {
|
1731 | var keyPath = keyPaths[i];
|
1732 | _.unset(parentObj, keyPath);
|
1733 | }
|
1734 | }
|
1735 |
|
1736 | /**
|
1737 | * Return object that contains all properties in fullObj that are not
|
1738 | * identical to the corresponding properties in removeObj
|
1739 | *
|
1740 | * Properties of fullObj and removeObj may be objects or arrays of objects
|
1741 | *
|
1742 | * Returned object is a deep clone of the properties of the input objects
|
1743 | *
|
1744 | * @param {Object} fullObj
|
1745 | * @param {Object} removeObj
|
1746 | *
|
1747 | * Examples:
|
1748 | * var fullD = {foo: [{bar: 0}, {bar: 1}], baz: 32}
|
1749 | * var removeD = {baz: 32}
|
1750 | * createDeltaObject(fullD, removeD)
|
1751 | * -> {foo: [{bar: 0}, {bar: 1}]}
|
1752 | *
|
1753 | * var fullD = {foo: [{bar: 0}, {bar: 1}], baz: 32}
|
1754 | * var removeD = {baz: 45}
|
1755 | * createDeltaObject(fullD, removeD)
|
1756 | * -> {foo: [{bar: 0}, {bar: 1}], baz: 32}
|
1757 | *
|
1758 | * var fullD = {foo: [{bar: 0}, {bar: 1}], baz: 32}
|
1759 | * var removeD = {foo: [{bar: 0}, {bar: 1}]}
|
1760 | * createDeltaObject(fullD, removeD)
|
1761 | * -> {baz: 32}
|
1762 | *
|
1763 | */
|
1764 | function createDeltaObject(fullObj, removeObj) {
|
1765 | // Initialize result as object or array
|
1766 | var res;
|
1767 | if (Array.isArray(fullObj)) {
|
1768 | res = new Array(fullObj.length);
|
1769 | } else {
|
1770 | res = {};
|
1771 | }
|
1772 |
|
1773 | // Initialize removeObj to empty object if not specified
|
1774 | if (removeObj === null || removeObj === undefined) {
|
1775 | removeObj = {};
|
1776 | }
|
1777 |
|
1778 | // Iterate over object properties or array indices
|
1779 | for (var p in fullObj) {
|
1780 | if (
|
1781 | p[0] !== "_" && // Don't consider private properties
|
1782 | fullObj.hasOwnProperty(p) && // Exclude parent properties
|
1783 | fullObj[p] !== null // Exclude cases where fullObj doesn't
|
1784 | // have the property
|
1785 | ) {
|
1786 | // Compute object equality
|
1787 | var props_equal;
|
1788 | props_equal = _.isEqual(fullObj[p], removeObj[p]);
|
1789 |
|
1790 | // Perform recursive comparison if props are not equal
|
1791 | if (!props_equal || p === "uid") {
|
1792 | // Let uids through
|
1793 |
|
1794 | // property has non-null value in fullObj that doesn't
|
1795 | // match the value in removeObj
|
1796 | var fullVal = fullObj[p];
|
1797 | if (removeObj.hasOwnProperty(p) && typeof fullVal === "object") {
|
1798 | // Recurse over object properties
|
1799 | if (Array.isArray(fullVal)) {
|
1800 | if (fullVal.length > 0 && typeof fullVal[0] === "object") {
|
1801 | // We have an object array
|
1802 | res[p] = new Array(fullVal.length);
|
1803 | for (var i = 0; i < fullVal.length; i++) {
|
1804 | if (!Array.isArray(removeObj[p]) || removeObj[p].length <= i) {
|
1805 | res[p][i] = fullVal[i];
|
1806 | } else {
|
1807 | res[p][i] = createDeltaObject(fullVal[i], removeObj[p][i]);
|
1808 | }
|
1809 | }
|
1810 | } else {
|
1811 | // We have a primitive array or typed array
|
1812 | res[p] = fullVal;
|
1813 | }
|
1814 | } else {
|
1815 | // object
|
1816 | var full_obj = createDeltaObject(fullVal, removeObj[p]);
|
1817 | if (Object.keys(full_obj).length > 0) {
|
1818 | // new object is not empty
|
1819 | res[p] = full_obj;
|
1820 | }
|
1821 | }
|
1822 | } else if (typeof fullVal === "object" && !Array.isArray(fullVal)) {
|
1823 | // Return 'clone' of fullVal
|
1824 | // We don't use a standard clone method so that we keep
|
1825 | // the special case handling of this method
|
1826 | res[p] = createDeltaObject(fullVal, {});
|
1827 | } else if (fullVal !== undefined && typeof fullVal !== "function") {
|
1828 | // No recursion necessary, Just keep value from fullObj.
|
1829 | // But skip values with function type
|
1830 | res[p] = fullVal;
|
1831 | }
|
1832 | }
|
1833 | }
|
1834 | }
|
1835 | return res;
|
1836 | }
|
1837 |
|
1838 | function randstr(existing, bits, base, _recursion) {
|
1839 | if (!base) base = 16;
|
1840 | if (bits === undefined) bits = 24;
|
1841 | if (bits <= 0) return "0";
|
1842 |
|
1843 | var digits = Math.log(Math.pow(2, bits)) / Math.log(base);
|
1844 | var res = "";
|
1845 | var i, b, x;
|
1846 |
|
1847 | for (i = 2; digits === Infinity; i *= 2) {
|
1848 | digits = (Math.log(Math.pow(2, bits / i)) / Math.log(base)) * i;
|
1849 | }
|
1850 |
|
1851 | var rem = digits - Math.floor(digits);
|
1852 |
|
1853 | for (i = 0; i < Math.floor(digits); i++) {
|
1854 | x = Math.floor(Math.random() * base).toString(base);
|
1855 | res = x + res;
|
1856 | }
|
1857 |
|
1858 | if (rem) {
|
1859 | b = Math.pow(base, rem);
|
1860 | x = Math.floor(Math.random() * b).toString(base);
|
1861 | res = x + res;
|
1862 | }
|
1863 |
|
1864 | var parsed = parseInt(res, base);
|
1865 | if (
|
1866 | (existing && existing[res]) ||
|
1867 | (parsed !== Infinity && parsed >= Math.pow(2, bits))
|
1868 | ) {
|
1869 | if (_recursion > 10) {
|
1870 | lib.warn("randstr failed uniqueness");
|
1871 | return res;
|
1872 | }
|
1873 | return randstr(existing, bits, base, (_recursion || 0) + 1);
|
1874 | } else return res;
|
1875 | }
|
1876 |
|
1877 | module.exports = {
|
1878 | FigureView: FigureView,
|
1879 | FigureModel: FigureModel,
|
1880 | };
|