1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { Widget } from '@phosphor/widgets' ;
5
+
6
+ import { Message } from '@phosphor/messaging' ;
7
+
8
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces' ;
9
+
10
+ import Plotly from 'plotly.js/dist/plotly' ;
11
+
12
+ import '../style/index.css' ;
13
+
14
+ /**
15
+ * The CSS class to add to the Plotly Widget.
16
+ */
17
+ const CSS_CLASS = 'jp-RenderedPlotly' ;
18
+
19
+ /**
20
+ * The CSS class for a Plotly icon.
21
+ */
22
+ const CSS_ICON_CLASS = 'jp-MaterialIcon jp-PlotlyIcon' ;
23
+
24
+ /**
25
+ * The MIME type for Plotly.
26
+ * The version of this follows the major version of Plotly.
27
+ */
28
+ export const MIME_TYPE = 'application/vnd.plotly.v1+json' ;
29
+
30
+ interface IPlotlySpec {
31
+ data : Plotly . Data ;
32
+ layout : Plotly . Layout ;
33
+ frames ?: Plotly . Frame [ ] ;
34
+ }
35
+
36
+ export class RenderedPlotly extends Widget implements IRenderMime . IRenderer {
37
+ /**
38
+ * Create a new widget for rendering Plotly.
39
+ */
40
+ constructor ( options : IRenderMime . IRendererOptions ) {
41
+ super ( ) ;
42
+ this . addClass ( CSS_CLASS ) ;
43
+ this . _mimeType = options . mimeType ;
44
+
45
+ // Create image element
46
+ this . _img_el = < HTMLImageElement > ( document . createElement ( "img" ) ) ;
47
+ this . _img_el . className = 'plot-img' ;
48
+ this . node . appendChild ( this . _img_el ) ;
49
+
50
+ // Install image hover callback
51
+ this . _img_el . addEventListener ( 'mouseenter' , event => {
52
+ this . createGraph ( this . _model ) ;
53
+ } )
54
+ }
55
+
56
+ /**
57
+ * Render Plotly into this widget's node.
58
+ */
59
+ renderModel ( model : IRenderMime . IMimeModel ) : Promise < void > {
60
+
61
+ if ( this . hasGraphElement ( ) ) {
62
+ // We already have a graph, don't overwrite it
63
+ return Promise . resolve ( ) ;
64
+ }
65
+
66
+ // Save off reference to model so that we can regenerate the plot later
67
+ this . _model = model ;
68
+
69
+ // Check for PNG data in mime bundle
70
+ const png_data = < string > model . data [ 'image/png' ] ;
71
+ if ( png_data !== undefined && png_data !== null ) {
72
+ // We have PNG data, use it
73
+ this . updateImage ( png_data ) ;
74
+ return Promise . resolve ( ) ;
75
+ } else {
76
+ // Create a new graph
77
+ return this . createGraph ( model ) ;
78
+ }
79
+ }
80
+
81
+ private hasGraphElement ( ) {
82
+ // Check for the presence of the .plot-container element that plotly.js
83
+ // places at the top of the figure structure
84
+ return this . node . querySelector ( '.plot-container' ) !== null
85
+ }
86
+
87
+ private updateImage ( png_data : string ) {
88
+ this . hideGraph ( ) ;
89
+ this . _img_el . src = "data:image/png;base64," + < string > png_data ;
90
+ this . showImage ( ) ;
91
+ }
92
+
93
+ private hideGraph ( ) {
94
+ // Hide the graph if there is one
95
+ let el = < HTMLDivElement > this . node . querySelector ( '.plot-container' ) ;
96
+ if ( el !== null && el !== undefined ) {
97
+ el . style . display = "none"
98
+ }
99
+ }
100
+
101
+ private showGraph ( ) {
102
+ // Show the graph if there is one
103
+ let el = < HTMLDivElement > this . node . querySelector ( '.plot-container' ) ;
104
+ if ( el !== null && el !== undefined ) {
105
+ el . style . display = "block"
106
+ }
107
+ }
108
+
109
+ private hideImage ( ) {
110
+ // Hide the image element
111
+ let el = < HTMLImageElement > this . node . querySelector ( '.plot-img' ) ;
112
+ if ( el !== null && el !== undefined ) {
113
+ el . style . display = "none"
114
+ }
115
+ }
116
+
117
+ private showImage ( ) {
118
+ // Show the image element
119
+ let el = < HTMLImageElement > this . node . querySelector ( '.plot-img' ) ;
120
+ if ( el !== null && el !== undefined ) {
121
+ el . style . display = "block"
122
+ }
123
+ }
124
+
125
+ private createGraph ( model : IRenderMime . IMimeModel ) {
126
+ const { data, layout, frames, config } = model . data [ this . _mimeType ] as
127
+ | any
128
+ | IPlotlySpec ;
129
+
130
+ return Plotly . react ( this . node , data , layout , config ) . then ( plot => {
131
+ this . showGraph ( ) ;
132
+ this . hideImage ( ) ;
133
+ this . update ( ) ;
134
+ if ( frames ) {
135
+ Plotly . addFrames ( this . node , frames ) ;
136
+ }
137
+ if ( this . node . offsetWidth > 0 && this . node . offsetHeight > 0 ) {
138
+ Plotly . toImage ( plot , {
139
+ format : 'png' ,
140
+ width : this . node . offsetWidth ,
141
+ height : this . node . offsetHeight
142
+ } ) . then ( ( url : string ) => {
143
+ const imageData = url . split ( ',' ) [ 1 ] ;
144
+ if ( model . data [ 'image/png' ] !== imageData ) {
145
+ model . setData ( {
146
+ data : {
147
+ ...model . data ,
148
+ 'image/png' : imageData
149
+ }
150
+ } ) ;
151
+ }
152
+ } ) ;
153
+ }
154
+
155
+ // Handle webgl context lost events
156
+ ( < Plotly . PlotlyHTMLElement > ( this . node ) ) . on ( 'plotly_webglcontextlost' , ( ) => {
157
+ const png_data = < string > model . data [ 'image/png' ] ;
158
+ if ( png_data !== undefined && png_data !== null ) {
159
+ // We have PNG data, use it
160
+ this . updateImage ( png_data ) ;
161
+ return Promise . resolve ( ) ;
162
+ }
163
+ } ) ;
164
+ } ) ;
165
+ }
166
+
167
+ /**
168
+ * A message handler invoked on an `'after-show'` message.
169
+ */
170
+ protected onAfterShow ( msg : Message ) : void {
171
+ this . update ( ) ;
172
+ }
173
+
174
+ /**
175
+ * A message handler invoked on a `'resize'` message.
176
+ */
177
+ protected onResize ( msg : Widget . ResizeMessage ) : void {
178
+ this . update ( ) ;
179
+ }
180
+
181
+ /**
182
+ * A message handler invoked on an `'update-request'` message.
183
+ */
184
+ protected onUpdateRequest ( msg : Message ) : void {
185
+ if ( this . isVisible && this . hasGraphElement ( ) ) {
186
+ Plotly . redraw ( this . node ) . then ( ( ) => {
187
+ Plotly . Plots . resize ( this . node ) ;
188
+ } ) ;
189
+ }
190
+ }
191
+
192
+ private _mimeType : string ;
193
+ private _img_el : HTMLImageElement ;
194
+ private _model : IRenderMime . IMimeModel
195
+ }
196
+
197
+ /**
198
+ * A mime renderer factory for Plotly data.
199
+ */
200
+ export const rendererFactory : IRenderMime . IRendererFactory = {
201
+ safe : true ,
202
+ mimeTypes : [ MIME_TYPE ] ,
203
+ createRenderer : options => new RenderedPlotly ( options )
204
+ } ;
205
+
206
+ const extensions : IRenderMime . IExtension | IRenderMime . IExtension [ ] = [
207
+ {
208
+ id : '@jupyterlab/plotly-extension:factory' ,
209
+ rendererFactory,
210
+ rank : 0 ,
211
+ dataType : 'json' ,
212
+ fileTypes : [
213
+ {
214
+ name : 'plotly' ,
215
+ mimeTypes : [ MIME_TYPE ] ,
216
+ extensions : [ '.plotly' , '.plotly.json' ] ,
217
+ iconClass : CSS_ICON_CLASS
218
+ }
219
+ ] ,
220
+ documentWidgetFactoryOptions : {
221
+ name : 'Plotly' ,
222
+ primaryFileType : 'plotly' ,
223
+ fileTypes : [ 'plotly' , 'json' ] ,
224
+ defaultFor : [ 'plotly' ]
225
+ }
226
+ }
227
+ ] ;
228
+
229
+ export default extensions ;
0 commit comments