UNPKG

59.7 kBJavaScriptView Raw
1var widgets = require("@jupyter-widgets/base");
2var _ = require("lodash");
3
4window.PlotlyConfig = { MathJaxConfig: "local" };
5var Plotly = require("plotly.js/dist/plotly");
6var 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 */
18var 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 */
685var 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 */
1417var 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
1428function 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 */
1461function 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 */
1495function 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 */
1544function 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 */
1559function 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 */
1591function 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 */
1634function 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 */
1685function 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 */
1729function 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 */
1764function 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
1838function 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
1877module.exports = {
1878 FigureView: FigureView,
1879 FigureModel: FigureModel,
1880};