Skip to content

Commit 2b54b67

Browse files
authored
V4 merge plotly jupyterlab extensions (#1623)
* Merge jupyterlab mime renderer extension into plotlywidget * Rename combined extension jupyterlab-plotly * Update plotly logo * Render static image versions of figures on notebook load and replace them with full interactive version on hover. * Revert figures to static images on WebGL context loss * Disable auto-animate on figures with frames * Comments / Review * Fix dev build artifact paths * Bumpy jupyterlab-plotly extension version to 1.0.0-alpha.1
2 parents 4bcd671 + 7be7f19 commit 2b54b67

File tree

20 files changed

+658
-68
lines changed

20 files changed

+658
-68
lines changed

.circleci/config.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ jobs:
299299
name: Install tox
300300
command: 'sudo pip install tox requests yapf pytz decorator retrying inflect'
301301
- run:
302-
name: Update plotlywidget version
302+
name: Update jupyterlab-plotly version
303303
command: 'cd packages/python/plotly; python setup.py updateplotlywidgetversion'
304304
- run:
305305
name: Update plotly.js to dev
@@ -331,10 +331,10 @@ jobs:
331331
- run:
332332
name: npm-pack widget
333333
command: |
334-
cd packages/javascript/plotlywidget/
334+
cd packages/javascript/jupyterlab-plotly/
335335
npm install webpack
336336
npm pack
337-
sudo cp ./plotlywidget* /dist
337+
sudo cp ./jupyterlab-plotly* /dist
338338
when: always
339339
- store_artifacts:
340340
path: packages/python/plotly/dist

packages/javascript/plotlywidget/package-lock.json renamed to packages/javascript/jupyterlab-plotly/package-lock.json

+309-45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/javascript/plotlywidget/package.json renamed to packages/javascript/jupyterlab-plotly/package.json

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "plotlywidget",
3-
"version": "0.11.0",
4-
"description": "The plotly.py ipywidgets library",
2+
"name": "jupyterlab-plotly",
3+
"version": "1.0.0-alpha.1",
4+
"description": "The plotly JupyterLab extension",
55
"author": "The plotly.py team",
66
"license": "MIT",
77
"main": "src/index.js",
@@ -18,24 +18,33 @@
1818
],
1919
"files": [
2020
"src/**/*.js",
21-
"dist/*.js"
21+
"dist/*.js",
22+
"style/*.*"
2223
],
2324
"scripts": {
24-
"clean": "rimraf dist/ && rimraf ../plotlywidget/static",
25+
"build": "npm run build:src",
26+
"build:src": "rimraf dist && tsc",
27+
"clean": "rimraf dist/ && rimraf ../../python/plotly/jupyterlab_plotly/static'",
2528
"prepublish": "webpack",
2629
"test": "echo \"Error: no test specified\" && exit 1"
2730
},
2831
"devDependencies": {
2932
"webpack": "^3.10.0",
3033
"rimraf": "^2.6.1",
31-
"ify-loader": "^1.1.0"
34+
"ify-loader": "^1.1.0",
35+
"typescript": "~3.1.1"
3236
},
3337
"dependencies": {
3438
"plotly.js": "1.48.1",
39+
"@types/plotly.js": "^1.44.9",
3540
"@jupyter-widgets/base": "^1.0.0",
41+
"@jupyterlab/rendermime-interfaces": "^1.2.1",
42+
"@phosphor/messaging": "^1.2.2",
43+
"@phosphor/widgets": "^1.6.0",
3644
"lodash": "^4.17.4"
3745
},
3846
"jupyterlab": {
39-
"extension": "src/jupyterlab-plugin"
47+
"extension": "src/jupyterlab-plugin.js",
48+
"mimeExtension": "dist/javascript-renderer-extension.js"
4049
}
4150
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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;

packages/javascript/plotlywidget/src/jupyterlab-plugin.js renamed to packages/javascript/jupyterlab-plotly/src/jupyterlab-plugin.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var base = require('@jupyter-widgets/base');
55
* The widget manager provider.
66
*/
77
module.exports = {
8-
id: 'plotlywidget',
8+
id: 'jupyterlab-plotly',
99
requires: [base.IJupyterWidgetRegistry],
1010
activate: function(app, widgets) {
1111
widgets.registerWidget({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
declare module 'plotly.js/dist/plotly' {
2+
export * from 'plotly.js';
3+
export type Frame = { [key: string]: any };
4+
export function addFrames(root: Plotly.Root, frames: Frame[]): Promise<void>;
5+
export function animate(root: Plotly.Root): void;
6+
7+
export interface PlotlyHTMLElement extends HTMLElement {
8+
on(event: 'plotly_webglcontextlost', callback: () => void): void;
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
Copyright (c) Jupyter Development Team.
3+
Distributed under the terms of the Modified BSD License.
4+
*/
5+
6+
/* Add CSS variables to :root */
7+
:root {
8+
--jp-icon-plotly: url('./plotly.svg');
9+
}
10+
11+
/* Base styles */
12+
.jp-RenderedPlotly {
13+
width: 100%;
14+
height: 100%;
15+
padding: 0;
16+
overflow: hidden;
17+
}
18+
19+
/* Document styles */
20+
.jp-MimeDocument .jp-RenderedPlotly {
21+
overflow: hidden;
22+
}
23+
24+
/* Output styles */
25+
.jp-OutputArea .jp-RenderedPlotly {
26+
min-height: 360px;
27+
}
28+
29+
/* Document icon */
30+
.jp-PlotlyIcon {
31+
background-image: var(--jp-icon-plotly);
32+
}
Loading

0 commit comments

Comments
 (0)