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