Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add back the mime renderer JL extension #5096

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
17 changes: 10 additions & 7 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@ commands:
source .venv/bin/activate
uv pip install .
uv pip install -r ./test_requirements/requirements_optional.txt
cd js
cd plotly/labextension
npm ci
npm run build
@@ -273,18 +273,20 @@ jobs:
- run:
name: initial NPM Build
command: |
python -m venv venv
. venv/bin/activate
cd js
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv
source .venv/bin/activate
uv pip install jupyter
cd plotly/labextension
npm ci
npm run build
git status
- run:
name: PyPI Build
command: |
. venv/bin/activate
pip install build
source .venv/bin/activate
uv pip install build
python -m build --sdist --wheel -o dist
cp -R dist output
git status
@@ -323,7 +325,8 @@ jobs:
uv pip uninstall plotly
cd ..
uv pip install -e .
cd js
uv pip install jupyter
cd plotly/labextension
npm ci
npm run build
cd ../doc
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -60,6 +60,8 @@ doc/.ipynb_checkpoints
tags
doc/check-or-enforce-order.py
plotly/package_data/widgetbundle.js
plotly/labextension/static
plotly/labextension/lib

tests/percy/*.html
tests/percy/pandas2/*.html
6 changes: 3 additions & 3 deletions commands.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
from distutils import log

project_root = os.path.dirname(os.path.abspath(__file__))
node_root = os.path.join(project_root, "js")
node_root = os.path.join(project_root, "plotly", "labextension")
is_repo = os.path.exists(os.path.join(project_root, ".git"))
node_modules = os.path.join(node_root, "node_modules")
targets = [
@@ -23,9 +23,9 @@
]
)

# Load plotly.js version from js/package.json
# Load plotly.js version from plotly/labextension/package.json
def plotly_js_version():
path = os.path.join(project_root, "js", "package.json")
path = os.path.join(project_root, "plotly", "labextension", "package.json")
with open(path, "rt") as f:
package_json = json.load(f)
version = package_json["dependencies"]["plotly.js"]
16 changes: 0 additions & 16 deletions js/package.json

This file was deleted.

9 changes: 9 additions & 0 deletions plotly/__init__.py
Original file line number Diff line number Diff line change
@@ -180,3 +180,12 @@ def hist_series(data_frame, **kwargs):
skip += ["figsize", "bins", "legend"]
new_kwargs = {k: kwargs[k] for k in kwargs if k not in skip}
return histogram(data_frame, **new_kwargs)

def _jupyter_labextension_paths():
"""Called by Jupyter Lab Server to detect if it is a valid labextension and
to install the extension.
"""
return [{
'src': 'labextension/static',
'dest': 'jupyterlab-plotly',
}]
3 changes: 1 addition & 2 deletions plotly/io/_base_renderers.py
Original file line number Diff line number Diff line change
@@ -76,8 +76,7 @@ def to_mimebundle(self, fig_dict):
class PlotlyRenderer(MimetypeRenderer):
"""
Renderer to display figures using the plotly mime type. This renderer is
compatible with JupyterLab (using the @jupyterlab/plotly-extension),
VSCode, and nteract.
compatible with VSCode and nteract.
mime type: 'application/vnd.plotly.v1+json'
"""
2 changes: 1 addition & 1 deletion plotly/io/_renderers.py
Original file line number Diff line number Diff line change
@@ -549,7 +549,7 @@ def show(fig, renderer=None, validate=True, **kwargs):
default_renderer = "browser"

# Fallback to renderer combination that will work automatically
# in the classic notebook (offline), jupyterlab, nteract, vscode, and
# in the classic notebook, jupyterlab, nteract, vscode, and
# nbconvert HTML export.
if not default_renderer:
default_renderer = "plotly_mimetype+notebook"
2,235 changes: 1,720 additions & 515 deletions js/package-lock.json → plotly/labextension/package-lock.json

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions plotly/labextension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "jupyterlab-plotly",
"version": "0.1.0",
"main": "lib/mimeExtension.js",
"scripts": {
"build:widget": "esbuild --bundle --alias:plotly.js=plotly.js/dist/plotly --format=esm --minify --outfile=../package_data/widgetbundle.js src/widget.ts",
"build:mimerenderer": "esbuild --bundle --alias:plotly.js=plotly.js/dist/plotly --format=esm --minify --outfile=lib/mimeExtension.js src/mimeExtension.ts",
"build:labextension": "jupyter labextension build .",
"build": "npm run build:widget && npm run build:mimerenderer && npm run build:labextension",
"watch": "npm run build -- --watch --sourcemap=inline",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"lodash-es": "^4.17.21",
"plotly.js": "3.0.1",
"@lumino/widgets": "~2.4.0"
},
"devDependencies": {
"@jupyterlab/builder": "^4.3.6",
"@types/plotly.js": "^2.33.4",
"esbuild": "^0.23.1",
"typescript": "^5.6.2"
},
"jupyterlab": {
"mimeExtension": true
}
}
248 changes: 248 additions & 0 deletions plotly/labextension/src/mimeExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
import { Widget } from '@lumino/widgets';
import type PlotlyType from "plotly.js";

import { Message } from "@lumino/messaging";

/**
* The CSS class to add to the Plotly Widget.
*/
const CSS_CLASS = "jp-RenderedPlotly";

/**
* The CSS class for a Plotly icon.
*/
const CSS_ICON_CLASS = "jp-MaterialIcon jp-PlotlyIcon";

/**
* The MIME type for Plotly.
* The version of this follows the major version of Plotly.
*/
export const MIME_TYPE = "application/vnd.plotly.v1+json";

interface IPlotlySpec {
data: PlotlyType.Data;
layout: PlotlyType.Layout;
frames?: PlotlyType.Frame[];
}

export class RenderedPlotly extends Widget implements IRenderMime.IRenderer {
/**
* Create a new widget for rendering Plotly.
*/
constructor(options: IRenderMime.IRendererOptions) {
super();
this.addClass(CSS_CLASS);
this._mimeType = options.mimeType;

// Create image element
this._img_el = <HTMLImageElement>document.createElement("img");
this._img_el.className = "plot-img";
this.node.appendChild(this._img_el);

// Install image hover callback
this._img_el.addEventListener("mouseenter", (event) => {
this.createGraph(this._model);
});
}

/**
* Render Plotly into this widget's node.
*/
renderModel(model: IRenderMime.IMimeModel): Promise<void> {
if (this.hasGraphElement()) {
// We already have a graph, don't overwrite it
return Promise.resolve();
}

// Save off reference to model so that we can regenerate the plot later
this._model = model;

// Check for PNG data in mime bundle
const png_data = <string>model.data["image/png"];
if (png_data !== undefined && png_data !== null) {
// We have PNG data, use it
this.updateImage(png_data);
return Promise.resolve();
} else {
// Create a new graph
return this.createGraph(model);
}
}

private hasGraphElement() {
// Check for the presence of the .plot-container element that plotly.js
// places at the top of the figure structure
return this.node.querySelector(".plot-container") !== null;
}

private updateImage(png_data: string) {
this.hideGraph();
this._img_el.src = "data:image/png;base64," + <string>png_data;
this.showImage();
}

private hideGraph() {
// Hide the graph if there is one
let el = <HTMLDivElement>this.node.querySelector(".plot-container");
if (el !== null && el !== undefined) {
el.style.display = "none";
}
}

private showGraph() {
// Show the graph if there is one
let el = <HTMLDivElement>this.node.querySelector(".plot-container");
if (el !== null && el !== undefined) {
el.style.display = "block";
}
}

private hideImage() {
// Hide the image element
let el = <HTMLImageElement>this.node.querySelector(".plot-img");
if (el !== null && el !== undefined) {
el.style.display = "none";
}
}

private showImage() {
// Show the image element
let el = <HTMLImageElement>this.node.querySelector(".plot-img");
if (el !== null && el !== undefined) {
el.style.display = "block";
}
}

private createGraph(model: IRenderMime.IMimeModel): Promise<void> {
const { data, layout, frames, config } = model.data[this._mimeType] as
| any
| IPlotlySpec;

if (!layout.height) {
layout.height = 360;
}

// Load plotly asynchronously
const loadPlotly = async (): Promise<void> => {
if (RenderedPlotly.Plotly === null) {
RenderedPlotly.Plotly = await import("plotly.js");
RenderedPlotly._resolveLoadingPlotly();
}
return RenderedPlotly.loadingPlotly;
};

return loadPlotly()
.then(() => RenderedPlotly.Plotly!.react(this.node, data, layout, config))
.then((plot) => {
this.showGraph();
this.hideImage();
this.update();
if (frames) {
RenderedPlotly.Plotly!.addFrames(this.node, frames);
}
if (this.node.offsetWidth > 0 && this.node.offsetHeight > 0) {
RenderedPlotly.Plotly!.toImage(plot, {
format: "png",
width: this.node.offsetWidth,
height: this.node.offsetHeight,
}).then((url: string) => {
const imageData = url.split(",")[1];
if (model.data["image/png"] !== imageData) {
model.setData({
data: {
...model.data,
"image/png": imageData,
},
});
}
});
}

// Handle webgl context lost events
(<PlotlyType.PlotlyHTMLElement>this.node).on(
"plotly_webglcontextlost",
() => {
const png_data = <string>model.data["image/png"];
if (png_data !== undefined && png_data !== null) {
// We have PNG data, use it
this.updateImage(png_data);
return Promise.resolve();
}
}
);
});
}

/**
* A message handler invoked on an `'after-show'` message.
*/
protected onAfterShow(msg: Message): void {
this.update();
}

/**
* A message handler invoked on a `'resize'` message.
*/
protected onResize(msg: Widget.ResizeMessage): void {
this.update();
}

/**
* A message handler invoked on an `'update-request'` message.
*/
protected onUpdateRequest(msg: Message): void {
if (RenderedPlotly.Plotly && this.isVisible && this.hasGraphElement()) {
RenderedPlotly.Plotly.redraw(this.node).then(() => {
RenderedPlotly.Plotly!.Plots.resize(this.node);
});
}
}

private _mimeType: string;
private _img_el: HTMLImageElement;
private _model: IRenderMime.IMimeModel;

private static Plotly: typeof PlotlyType | null = null;
private static _resolveLoadingPlotly: () => void;
private static loadingPlotly = new Promise<void>((resolve) => {
RenderedPlotly._resolveLoadingPlotly = resolve;
});
}

/**
* A mime renderer factory for Plotly data.
*/
export const rendererFactory: IRenderMime.IRendererFactory = {
safe: true,
mimeTypes: [MIME_TYPE],
createRenderer: (options) => new RenderedPlotly(options),
};

const extensions: IRenderMime.IExtension | IRenderMime.IExtension[] = [
{
id: "@jupyterlab/plotly-extension:factory",
rendererFactory,
rank: 0,
dataType: "json",
fileTypes: [
{
name: "plotly",
mimeTypes: [MIME_TYPE],
extensions: [".plotly", ".plotly.json"],
iconClass: CSS_ICON_CLASS,
},
],
documentWidgetFactoryOptions: {
name: "Plotly",
primaryFileType: "plotly",
fileTypes: ["plotly", "json"],
defaultFor: ["plotly"],
},
},
];

export default extensions;
File renamed without changes.
File renamed without changes.
20 changes: 19 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools"]
requires = ["setuptools", "jupyter_packaging~=0.10.0", "wheel"]
build-backend = "setuptools.build_meta"

[project.urls]
@@ -57,6 +57,8 @@ plotly = [
"package_data/*",
"package_data/templates/*",
"package_data/datasets/*",
"labextension/static/*",
"labextension/static/static/*",
]

[tool.black]
@@ -83,3 +85,19 @@ exclude = '''
| versioneer.py
)
'''

[tool.jupyter-packaging.options]
skip-if-exists = ["plotly/labextension/static/style.js"]

[tool.jupyter-packaging.builder]
factory = "jupyter_packaging.npm_builder"

[tool.jupyter-packaging.build-args]
build_cmd = "build:prod"
npm = ["jlpm"]

[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs]
build_cmd = "build"
npm = ["jlpm"]
source_dir = "js"
build_dir = "plotly/labextension"