Skip to content

Commit 583dea9

Browse files
committed
Use original code to maintain image load
1 parent e5e2994 commit 583dea9

14 files changed

+257
-5448
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ doc/.ipynb_checkpoints
6060
tags
6161
doc/check-or-enforce-order.py
6262
plotly/package_data/widgetbundle.js
63+
plotly/labextension/static
64+
js/mimerenderer.js
6365

6466
tests/percy/*.html
6567
tests/percy/pandas2/*.html

js/index.ts

Lines changed: 220 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,248 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
14
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
25
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";
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+
*/
1413
const CSS_CLASS = "jp-RenderedPlotly";
1514

1615
/**
17-
* The CSS class for a Plotly icon.
18-
*/
16+
* The CSS class for a Plotly icon.
17+
*/
1918
const 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+
*/
52219
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),
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

Comments
 (0)