1+ // Copyright (c) Jupyter Development Team.
2+ // Distributed under the terms of the Modified BSD License.
3+
14import { IRenderMime } from '@jupyterlab/rendermime-interfaces' ;
25import { 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" ;
97
8+ import { Message } from "@lumino/messaging" ;
109
1110/**
12- * The CSS class to add to the Plotly Widget.
13- */
11+ * The CSS class to add to the Plotly Widget.
12+ */
1413const CSS_CLASS = "jp-RenderedPlotly" ;
1514
1615/**
17- * The CSS class for a Plotly icon.
18- */
16+ * The CSS class for a Plotly icon.
17+ */
1918const CSS_ICON_CLASS = "jp-MaterialIcon jp-PlotlyIcon" ;
2019
2120/**
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 ( ) ;
3759 }
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 ) ;
4673 }
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+ } ) ;
47214}
48215
49216/**
50- * A mime renderer factory for mp4 data.
51- */
217+ * A mime renderer factory for Plotly data.
218+ */
52219export 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 ) ,
56223} ;
57224
58- /**
59- * Extension definition.
60- */
61- const extension : IRenderMime . IExtension = {
225+ const extensions : IRenderMime . IExtension | IRenderMime . IExtension [ ] = [
226+ {
62227 id : "@jupyterlab/plotly-extension:factory" ,
63228 rendererFactory,
64229 rank : 0 ,
65230 dataType : "json" ,
66231 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+ } ,
73238 ] ,
74239 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" ] ,
79244 } ,
80- }
245+ } ,
246+ ] ;
81247
82- export default extension ;
248+ export default extensions ;
0 commit comments