├── .cookiecutter.yaml ├── .gitignore ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── build.sh ├── component ├── README.md ├── index.css ├── index.js └── package.json ├── install.sh ├── jupyterlab_plotly ├── README.md ├── __init__.py └── utils.py ├── labextension ├── README.md ├── build_extension.js ├── package.json └── src │ ├── doc.js │ ├── index.css │ ├── output.js │ └── plugin.js ├── nbextension ├── README.md ├── package.json ├── src │ ├── embed.js │ ├── extension.js │ ├── index.css │ ├── index.js │ └── renderer.js └── webpack.config.js ├── setup.py ├── setupbase.py └── uninstall.sh /.cookiecutter.yaml: -------------------------------------------------------------------------------- 1 | default_context: 2 | author_name: "Grant Nestor" 3 | author_email: "grantnestor@gmail.com" 4 | mime_type: "application/vnd.plotly.v1+json" 5 | file_extension: "plotly" 6 | mime_short_name: "Plotly" 7 | extension_name: "jupyterlab_plotly" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | labextension/lib/ 2 | nbextension/lib/ 3 | nbextension/embed/ 4 | node_modules/ 5 | npm-debug.log 6 | *.egg-info/ 7 | __pycache__/ 8 | build/ 9 | dist/ 10 | 11 | # Compiled javascript 12 | jupyterlab_plotly/static/ 13 | 14 | # OS X 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft jupyterlab_plotly/static 2 | graft labextension/src 3 | graft nbextension/src 4 | include labextension/package.json 5 | include nbextension/package.json 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_plotly 2 | 3 | A JupyterLab and Jupyter Notebook extension for rendering [Plotly](https://plot.ly/python/) charts 4 | 5 | ![lab](http://g.recordit.co/CmiB0dfKUa.gif) 6 | 7 | ![notebook](http://g.recordit.co/AFtqwfIM9B.gif) 8 | 9 | ## Prerequisites 10 | 11 | * JupyterLab ^0.18.0 and/or Notebook >=4.3.0 12 | 13 | ## Usage 14 | 15 | To render Plotly JSON using IPython: 16 | 17 | ```python 18 | from jupyterlab_plotly import Plotly 19 | 20 | data = [ 21 | {'x': [1999, 2000, 2001, 2002], 'y': [10, 15, 13, 17], 'type': 'scatter'}, 22 | {'x': [1999, 2000, 2001, 2002], 'y': [16, 5, 11, 9], 'type': 'scatter'} 23 | ] 24 | 25 | layout = { 26 | 'title': 'Sales Growth', 27 | 'xaxis': { 'title': 'Year', 'showgrid': False, 'zeroline': False }, 28 | 'yaxis': { 'title': 'Percent', 'showline': False } 29 | } 30 | 31 | Plotly(data, layout) 32 | ``` 33 | 34 | To render a Plotly JSON (`.plotly` or `.plotly.json`) file in JupyterLab, simply open it. 35 | 36 | ## Install 37 | 38 | ```bash 39 | pip install jupyterlab_plotly 40 | # For JupyterLab 41 | jupyter labextension install --symlink --py --sys-prefix jupyterlab_plotly 42 | jupyter labextension enable --py --sys-prefix jupyterlab_plotly 43 | # For Notebook 44 | jupyter nbextension install --symlink --py --sys-prefix jupyterlab_plotly 45 | jupyter nbextension enable --py --sys-prefix jupyterlab_plotly 46 | ``` 47 | 48 | ## Development 49 | 50 | ```bash 51 | pip install -e . 52 | # For JupyterLab 53 | jupyter labextension install --symlink --py --sys-prefix jupyterlab_plotly 54 | jupyter labextension enable --py --sys-prefix jupyterlab_plotly 55 | # For Notebook 56 | jupyter nbextension install --symlink --py --sys-prefix jupyterlab_plotly 57 | jupyter nbextension enable --py --sys-prefix jupyterlab_plotly 58 | ``` 59 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a jupyterlab_plotly release 2 | 3 | This document guides an extension maintainer through creating and publishing a release of jupyterlab_plotly. This process creates a Python source package and a Python universal wheel and uploads them to PyPI. 4 | 5 | ## Update version number 6 | 7 | Update the version number in `setup.py`, `labextension/package.json`, and `nbextension/package.json`. 8 | 9 | Commit your changes, add git tag for this version, and push both commit and tag to your origin/remote repo. 10 | 11 | ## Remove generated files 12 | 13 | Remove old Javascript bundle and Python package builds: 14 | 15 | ```bash 16 | rm -rf jupyterlab_plotly/static 17 | ``` 18 | 19 | ## Build the package 20 | 21 | Build the Javascript extension bundle, then build the Python package and wheel: 22 | 23 | ```bash 24 | bash build.js 25 | python setup.py sdist 26 | python setup.py bdist_wheel --universal 27 | ``` 28 | 29 | ## Upload the package 30 | 31 | Upload the Python package and wheel with [twine](https://github.com/pypa/twine). See the Python documentation on [package uploading](https://packaging.python.org/distributing/#uploading-your-project-to-pypi) 32 | for [twine](https://github.com/pypa/twine) setup instructions and for why twine is the recommended uploading method. 33 | 34 | ```bash 35 | twine upload dist/* 36 | ``` 37 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm -v 4 | if [ $? -eq 0 ]; then 5 | echo npm is installed 6 | else 7 | echo "'npm -v' failed, therefore npm is not installed. In order to perform a 8 | developer install of jupyterlab_plotly you must have npm installed 9 | on your machine! See http://blog.npmjs.org/post/85484771375/how-to-install-npm 10 | for installation instructions." 11 | exit 1 12 | fi 13 | 14 | cd labextension 15 | npm install 16 | cd .. 17 | 18 | cd nbextension 19 | npm install 20 | cd .. 21 | -------------------------------------------------------------------------------- /component/README.md: -------------------------------------------------------------------------------- 1 | # component 2 | 3 | A React component for rendering Plotly 4 | 5 | ## Structure 6 | 7 | * `index.js`: Entry point for React component(s) 8 | * `index.css`: Optional CSS file for styling React component(s) 9 | * `package.json`: Node package configuration, use this to add npm depedencies 10 | -------------------------------------------------------------------------------- /component/index.css: -------------------------------------------------------------------------------- 1 | .Plotly { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /component/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Plotly from 'plotly.js/lib/core'; 3 | 4 | const DEFAULT_WIDTH = 840; 5 | const DEFAULT_HEIGHT = 336; 6 | 7 | export default class PlotlyComponent extends React.Component { 8 | componentDidMount() { 9 | // window.addEventListener('resize', this.handleResize); 10 | const { data, layout } = this.props; 11 | Plotly.newPlot(this.element, data, layout) 12 | .then(gd => { 13 | Plotly.Plots.resize(this.element); 14 | }); 15 | // .then(gd => 16 | // Plotly.toImage(gd, { 17 | // format: 'png', 18 | // width: this.props.width || DEFAULT_WIDTH, 19 | // height: this.props.height || DEFAULT_HEIGHT 20 | // })) 21 | // .then(url => { 22 | // const data = url.split(',')[1]; 23 | // this.props.callback(null, data); 24 | // this.handleResize(); 25 | // }); 26 | } 27 | 28 | componentDidUpdate() { 29 | const { data, layout } = this.props; 30 | Plotly.redraw(this.element) 31 | .then(gd => { 32 | Plotly.Plots.resize(this.element); 33 | }); 34 | // .then(gd => 35 | // Plotly.toImage(gd, { 36 | // format: 'png', 37 | // width: this.props.width || DEFAULT_WIDTH, 38 | // height: this.props.height || DEFAULT_HEIGHT 39 | // })) 40 | // .then(url => { 41 | // const data = url.split(',')[1]; 42 | // this.props.callback(null, data); 43 | // this.handleResize(); 44 | // }); 45 | } 46 | 47 | // componentWillUnmount() { 48 | // window.removeEventListener('resize', this.handleResize); 49 | // } 50 | 51 | // handleResize = event => { 52 | // Plotly.Plots.resize(this.element); 53 | // }; 54 | 55 | render() { 56 | const { layout } = this.props; 57 | const style = { 58 | width: '100%', 59 | height: layout && layout.height && !layout.autosize 60 | ? layout.height 61 | : DEFAULT_HEIGHT 62 | }; 63 | return ( 64 |
{ 67 | this.element = el; 68 | }} 69 | /> 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab_plotly_react", 3 | "version": "1.0.0", 4 | "description": "A React component for rendering Plotly", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "jupyter", 11 | "react" 12 | ], 13 | "author": "Grant Nestor ", 14 | "license": "ISC", 15 | "dependencies": { 16 | "plotly.js": "^1.22.0", 17 | "react": "^15.3.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | nbExtFlags=$1 4 | 5 | bash build.sh 6 | 7 | pip --version 8 | if [ $? -eq 0 ]; then 9 | echo pip is installed 10 | else 11 | echo "'pip --version' failed, therefore pip is not installed. In order to perform 12 | a developer install of jupyterlab_plotly you must have pip installed on 13 | your machine! See https://packaging.python.org/installing/ for installation instructions." 14 | exit 1 15 | fi 16 | 17 | pip install -v -e . 18 | 19 | jupyter lab --version 20 | if [ $? -eq 0 ]; then 21 | echo jupyter lab is installed 22 | if [[ "$OSTYPE" == "msys" ]]; then 23 | jupyter labextension install --py $nbExtFlags jupyterlab_plotly 24 | else 25 | jupyter labextension install --py --symlink $nbExtFlags jupyterlab_plotly 26 | fi 27 | jupyter labextension enable --py $nbExtFlags jupyterlab_plotly 28 | else 29 | echo "'jupyter lab --version' failed, therefore jupyter lab is not installed. In 30 | order to perform a developer install of jupyterlab_plotly you must 31 | have jupyter lab on your machine! Install using 'pip install jupyterlab' or 32 | follow instructions at https://github.com/jupyterlab/jupyterlab/blob/master/CONTRIBUTING.md#installing-jupyterlab for developer install." 33 | fi 34 | 35 | jupyter notebook --version 36 | if [ $? -eq 0 ]; then 37 | echo jupyter notebook is installed 38 | if [[ "$OSTYPE" == "msys" ]]; then 39 | jupyter nbextension install --py $nbExtFlags jupyterlab_plotly 40 | else 41 | jupyter nbextension install --py --symlink $nbExtFlags jupyterlab_plotly 42 | fi 43 | jupyter nbextension enable --py $nbExtFlags jupyterlab_plotly 44 | else 45 | echo "'jupyter notebook --version' failed, therefore jupyter notebook is not 46 | installed. In order to perform a developer install of 47 | jupyterlab_plotly you must have jupyter notebook on your machine! 48 | Install using 'pip install notebook' or follow instructions at 49 | https://github.com/jupyter/notebook/blob/master/CONTRIBUTING.rst#installing-the-jupyter-notebook 50 | for developer install." 51 | fi 52 | -------------------------------------------------------------------------------- /jupyterlab_plotly/README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_plotly 2 | 3 | Single Python package for lab and notebook extensions 4 | 5 | ## Structure 6 | 7 | * `static`: Built Javascript from `../labextension/` and `../nbextension/` 8 | * `__init__.py`: Exports paths and metadata of lab and notebook extensions and exports an optional `display` method that can be imported into a notebook and used to easily display data using this renderer 9 | -------------------------------------------------------------------------------- /jupyterlab_plotly/__init__.py: -------------------------------------------------------------------------------- 1 | from IPython.display import display, DisplayObject 2 | import json 3 | import pandas as pd 4 | 5 | 6 | # Running `npm run build` will create static resources in the static 7 | # directory of this Python package (and create that directory if necessary). 8 | 9 | def _jupyter_labextension_paths(): 10 | return [{ 11 | 'name': 'jupyterlab_plotly', 12 | 'src': 'static', 13 | }] 14 | 15 | def _jupyter_nbextension_paths(): 16 | return [{ 17 | 'section': 'notebook', 18 | 'src': 'static', 19 | 'dest': 'jupyterlab_plotly', 20 | 'require': 'jupyterlab_plotly/extension' 21 | }] 22 | 23 | def prepare_data(data=None): 24 | """Prepare JSONTable data from Pandas DataFrame.""" 25 | 26 | if isinstance(data, pd.DataFrame): 27 | data = data.to_json(orient='records') 28 | return json.loads(data) 29 | return data 30 | 31 | # A display class that can be used within a notebook. E.g.: 32 | # from jupyterlab_plotly import Plotly 33 | # Plotly(data) 34 | 35 | class Plotly(DisplayObject): 36 | """Plotly expects a data (JSON-able dict or list) and layout (a JSON-able dict) argument 37 | 38 | not an already-serialized JSON string. 39 | 40 | Scalar types (None, number, string) are not allowed, only dict containers. 41 | """ 42 | # wrap data in a property, which warns about passing already-serialized JSON 43 | _data = None 44 | _layout = None 45 | def __init__(self, data=None, layout=None, url=None, filename=None, metadata=None): 46 | """Create a Plotly display object given raw data. 47 | 48 | Parameters 49 | ---------- 50 | data : list 51 | Not an already-serialized JSON string. 52 | Scalar types (None, number, string) are not allowed, only list containers. 53 | layout : dict 54 | Plotly layout. Not an already-serialized JSON string. 55 | url : unicode 56 | A URL to download the data from. 57 | filename : unicode 58 | Path to a local file to load the data from. 59 | metadata: dict 60 | Specify extra metadata to attach to the json display object. 61 | """ 62 | self.layout = layout 63 | self.metadata = metadata 64 | super(Plotly, self).__init__(data=data, url=url, filename=filename) 65 | 66 | def _check_data(self): 67 | if self.layout is not None and not isinstance(self.layout, dict): 68 | raise TypeError("%s expects a JSONable dict, not %r" % (self.__class__.__name__, self.layout)) 69 | if self.data is not None and not isinstance(self.data, (list, pd.DataFrame)): 70 | raise TypeError("%s expects a JSONable list or pandas DataFrame, not %r" % (self.__class__.__name__, self.data)) 71 | 72 | @property 73 | def layout(self): 74 | return self._layout 75 | 76 | @property 77 | def data(self): 78 | return self._data 79 | 80 | @layout.setter 81 | def layout(self, layout): 82 | if isinstance(layout, str): 83 | # warnings.warn("Plotly expects a JSONable dict, not JSON strings") 84 | layout = json.loads(layout) 85 | self._layout = layout 86 | 87 | @data.setter 88 | def data(self, data): 89 | if isinstance(data, str): 90 | # warnings.warn("Plotly expects JSONable dict or list, not JSON strings") 91 | data = json.loads(data) 92 | self._data = data 93 | 94 | def _ipython_display_(self): 95 | bundle = { 96 | 'application/vnd.plotly.v1+json': { 97 | 'layout': self.layout, 98 | 'data': prepare_data(self.data) 99 | }, 100 | 'text/plain': '' 101 | } 102 | metadata = { 103 | 'application/vnd.plotly.v1+json': self.metadata 104 | } 105 | display(bundle, metadata=metadata, raw=True) 106 | -------------------------------------------------------------------------------- /jupyterlab_plotly/utils.py: -------------------------------------------------------------------------------- 1 | import cgi 2 | import codecs 3 | import collections 4 | import os.path 5 | import pandas as pd 6 | 7 | 8 | def nested_update(d, u): 9 | """Update nested dictionary d (in-place) with keys from u.""" 10 | for k, v in u.items(): 11 | if isinstance(v, collections.Mapping): 12 | d[k] = nested_update(d.get(k, {}), v) 13 | else: 14 | d[k] = v 15 | return d 16 | 17 | 18 | def abs_path(path): 19 | """Make path absolute.""" 20 | return os.path.join( 21 | os.path.dirname(os.path.abspath(__file__)), 22 | path) 23 | 24 | 25 | def get_content(path): 26 | """Get content of file.""" 27 | with codecs.open(abs_path(path), encoding='utf-8') as f: 28 | return f.read() 29 | 30 | 31 | def escape(string): 32 | """Escape the string.""" 33 | return cgi.escape(string, quote=True) 34 | 35 | 36 | def sanitize_dataframe(df): 37 | """Sanitize a DataFrame to prepare it for serialization. 38 | 39 | * Make a copy 40 | * Raise ValueError if it has a hierarchical index. 41 | * Convert categoricals to strings. 42 | * Convert np.int dtypes to Python int objects 43 | * Convert floats to objects and replace NaNs by None. 44 | * Convert DateTime dtypes into appropriate string representations 45 | """ 46 | import pandas as pd 47 | import numpy as np 48 | 49 | df = df.copy() 50 | 51 | if isinstance(df.index, pd.core.index.MultiIndex): 52 | raise ValueError('Hierarchical indices not supported') 53 | if isinstance(df.columns, pd.core.index.MultiIndex): 54 | raise ValueError('Hierarchical indices not supported') 55 | 56 | for col_name, dtype in df.dtypes.iteritems(): 57 | if str(dtype) == 'category': 58 | # XXXX: work around bug in to_json for categorical types 59 | # https://github.com/pydata/pandas/issues/10778 60 | df[col_name] = df[col_name].astype(str) 61 | elif np.issubdtype(dtype, np.integer): 62 | # convert integers to objects; np.int is not JSON serializable 63 | df[col_name] = df[col_name].astype(object) 64 | elif np.issubdtype(dtype, np.floating): 65 | # For floats, convert nan->None: np.float is not JSON serializable 66 | col = df[col_name].astype(object) 67 | df[col_name] = col.where(col.notnull(), None) 68 | elif str(dtype).startswith('datetime'): 69 | # Convert datetimes to strings 70 | # astype(str) will choose the appropriate resolution 71 | df[col_name] = df[col_name].astype(str).replace('NaT', '') 72 | return df 73 | 74 | 75 | def prepare_plotly_data(data=None): 76 | """Prepare Plotly data from Pandas DataFrame.""" 77 | 78 | if isinstance(data, list): 79 | return data 80 | data = sanitize_dataframe(data) 81 | return data.to_dict(orient='records') 82 | -------------------------------------------------------------------------------- /labextension/README.md: -------------------------------------------------------------------------------- 1 | # labextension 2 | 3 | A JupyterLab extension for rendering Plotly 4 | 5 | ## Prerequisites 6 | 7 | * `jupyterlab@^0.18.0` 8 | 9 | ## Development 10 | 11 | Install dependencies and build Javascript: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | Re-build Javascript: 18 | 19 | ```bash 20 | npm run build 21 | ``` 22 | 23 | Watch `/src` directory and re-build on changes: 24 | 25 | ```bash 26 | npm run watch 27 | ``` 28 | 29 | Manage extension 30 | 31 | ```bash 32 | # Install 33 | npm run extension:install 34 | # Enable 35 | npm run extension:enable 36 | # Disable 37 | npm run extension:disable 38 | # Uninstall 39 | npm run extension:uninstall 40 | ``` 41 | -------------------------------------------------------------------------------- /labextension/build_extension.js: -------------------------------------------------------------------------------- 1 | var buildExtension = require('@jupyterlab/extension-builder').buildExtension; 2 | var path = require('path'); 3 | 4 | buildExtension({ 5 | name: 'jupyterlab_plotly', 6 | entry: path.join(__dirname, 'src', 'plugin.js'), 7 | outputDir: path.join(__dirname, '..', 'jupyterlab_plotly', 'static'), 8 | useDefaultLoaders: false, 9 | config: { 10 | module: { 11 | loaders: [ 12 | { test: /\.html$/, loader: 'file-loader' }, 13 | { test: /\.(jpg|png|gif)$/, loader: 'file-loader' }, 14 | { 15 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 16 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 17 | }, 18 | { 19 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 20 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 21 | }, 22 | { 23 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 24 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream' 25 | }, 26 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 27 | { 28 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 29 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml' 30 | }, 31 | { test: /\.json$/, loader: 'json-loader' }, 32 | { 33 | test: /\.js$/, 34 | include: [ 35 | path.join(__dirname, 'src'), 36 | path.join( 37 | __dirname, 38 | 'node_modules', 39 | 'jupyterlab_plotly_react' 40 | ) 41 | ], 42 | loader: 'babel-loader', 43 | query: { presets: ['latest', 'stage-0', 'react'] } 44 | } 45 | ] 46 | } 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /labextension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "jupyterlab_plotly_labextension", 4 | "version": "0.18.0", 5 | "description": "A JupyterLab extension for rendering Plotly", 6 | "author": "Grant Nestor ", 7 | "main": "src/plugin.js", 8 | "keywords": [ 9 | "jupyter", 10 | "jupyterlab", 11 | "jupyterlab extension" 12 | ], 13 | "scripts": { 14 | "build": "node build_extension.js", 15 | "watch": "watch \"npm install\" src ../component --wait 10 --ignoreDotFiles", 16 | "preinstall": "npm install ../component", 17 | "prepublish": "npm run build", 18 | "extension:install": "jupyter labextension install --symlink --py --sys-prefix jupyterlab_plotly", 19 | "extension:uninstall": "jupyter labextension uninstall --py --sys-prefix jupyterlab_plotly", 20 | "extension:enable": "jupyter labextension enable --py --sys-prefix jupyterlab_plotly", 21 | "extension:disable": "jupyter labextension disable --py --sys-prefix jupyterlab_plotly" 22 | }, 23 | "dependencies": { 24 | "@jupyterlab/apputils": "^0.1.3", 25 | "@jupyterlab/codemirror": "^0.1.3", 26 | "@jupyterlab/docregistry": "^0.1.3", 27 | "@jupyterlab/rendermime": "^0.1.3", 28 | "@phosphor/algorithm": "^0.1.1", 29 | "@phosphor/virtualdom": "^0.1.1", 30 | "@phosphor/widgets": "^0.3.0", 31 | "react": "^15.3.2", 32 | "react-dom": "^15.3.2" 33 | }, 34 | "devDependencies": { 35 | "@jupyterlab/extension-builder": "^0.10.0", 36 | "babel-core": "^6.18.2", 37 | "babel-loader": "^6.2.7", 38 | "babel-preset-latest": "^6.16.0", 39 | "babel-preset-react": "^6.16.0", 40 | "babel-preset-stage-0": "^6.16.0", 41 | "watch": "^1.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /labextension/src/doc.js: -------------------------------------------------------------------------------- 1 | import { Widget } from '@phosphor/widgets'; 2 | import { ABCWidgetFactory } from '@jupyterlab/docregistry'; 3 | import { ActivityMonitor } from '@jupyterlab/coreutils'; 4 | import { runMode } from '@jupyterlab/codemirror'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import PlotlyComponent from 'jupyterlab_plotly_react'; 8 | 9 | const CLASS_NAME = 'jp-DocWidgetPlotly'; 10 | const RENDER_TIMEOUT = 1000; 11 | 12 | /** 13 | * A component for rendering error messages 14 | */ 15 | class ErrorDisplay extends React.Component { 16 | componentDidUpdate() { 17 | runMode(this.props.content, { name: 'javascript', json: true }, this.ref); 18 | } 19 | render() { 20 | return ( 21 |
22 |

{this.props.message}

23 |
 {
 25 |             this.ref = ref;
 26 |           }}
 27 |           className="CodeMirror cm-s-jupyter CodeMirror-wrap"
 28 |         />
 29 |       
30 | ); 31 | } 32 | } 33 | 34 | /** 35 | * A widget for rendering jupyterlab_plotly files 36 | */ 37 | export class DocWidget extends Widget { 38 | constructor(context) { 39 | super(); 40 | this._context = context; 41 | this._onPathChanged(); 42 | this.addClass(CLASS_NAME); 43 | context.ready.then(() => { 44 | this.update(); 45 | /* Re-render when the document content changes */ 46 | context.model.contentChanged.connect(this.update, this); 47 | /* Re-render when the document path changes */ 48 | context.fileChanged.connect(this.update, this); 49 | }); 50 | /* Update title when path changes */ 51 | context.pathChanged.connect(this._onPathChanged, this); 52 | /* Throttle re-renders until changes have stopped */ 53 | this._monitor = new ActivityMonitor({ 54 | signal: context.model.contentChanged, 55 | timeout: RENDER_TIMEOUT 56 | }); 57 | this._monitor.activityStopped.connect(this.update, this); 58 | } 59 | 60 | /** 61 | * The widget's context 62 | */ 63 | get context() { 64 | return this._context; 65 | } 66 | 67 | /** 68 | * Dispose of the resources used by the widget 69 | */ 70 | dispose() { 71 | if (!this.isDisposed) { 72 | this._context = null; 73 | this._monitor.dispose(); 74 | super.dispose(); 75 | } 76 | } 77 | 78 | /** 79 | * A message handler invoked on an `'after-attach'` message 80 | */ 81 | onAfterAttach(msg) { 82 | /* Render initial data */ 83 | this.update(); 84 | } 85 | 86 | /** 87 | * A message handler invoked on an `'before-detach'` message 88 | */ 89 | onBeforeDetach(msg) { 90 | /* Dispose of resources used by widget */ 91 | ReactDOM.unmountComponentAtNode(this.node); 92 | } 93 | 94 | /** 95 | * A message handler invoked on a `'resize'` message 96 | */ 97 | onResize(msg) { 98 | /* Re-render on resize */ 99 | this.update(); 100 | } 101 | 102 | /** 103 | * A message handler invoked on an `'update-request'` message 104 | */ 105 | onUpdateRequest(msg) { 106 | if (this.isAttached && this._context.isReady) this._render(); 107 | } 108 | 109 | _render() { 110 | const content = this._context.model.toString(); 111 | try { 112 | const props = { 113 | data: JSON.parse(content), 114 | width: this.node.offsetWidth, 115 | height: this.node.offsetHeight 116 | }; 117 | ReactDOM.render(, this.node); 118 | } catch (error) { 119 | ReactDOM.render( 120 | , 121 | this.node 122 | ); 123 | } 124 | } 125 | 126 | _onPathChanged() { 127 | this.title.label = this._context.path.split('/').pop(); 128 | } 129 | } 130 | 131 | /** 132 | * A widget factory for DocWidget 133 | */ 134 | export class DocWidgetFactory extends ABCWidgetFactory { 135 | /** 136 | * Create a new widget instance 137 | */ 138 | createNewWidget(context) { 139 | return new DocWidget(context); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /labextension/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Jupyter Development Team. 3 | Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | .jp-OutputWidgetPlotly, .jp-DocWidgetPlotly { 7 | width: 100%; 8 | padding: 0; 9 | } 10 | 11 | .jp-OutputWidgetPlotly { 12 | padding-top: 5px; 13 | height: 360px; 14 | } 15 | 16 | .jp-DocWidgetPlotly { 17 | overflow: auto; 18 | } 19 | 20 | .jp-DocWidgetPlotly .jp-mod-error { 21 | width: 100%; 22 | min-height: 100%; 23 | text-align: center; 24 | padding: 10px; 25 | box-sizing: border-box; 26 | } 27 | 28 | .jp-DocWidgetPlotly .jp-mod-error h2 { 29 | font-size: 18px; 30 | font-weight: 500; 31 | padding-bottom: 10px; 32 | } 33 | 34 | .jp-DocWidgetPlotly .jp-mod-error pre { 35 | text-align: left; 36 | padding: 10px; 37 | overflow: hidden; 38 | } 39 | -------------------------------------------------------------------------------- /labextension/src/output.js: -------------------------------------------------------------------------------- 1 | import { Widget } from '@phosphor/widgets'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import PlotlyComponent from 'jupyterlab_plotly_react'; 5 | 6 | const MIME_TYPE = 'application/vnd.plotly.v1+json'; 7 | const CLASS_NAME = 'jp-OutputWidgetPlotly'; 8 | 9 | /** 10 | * A Phosphor widget for rendering Plotly 11 | */ 12 | export class OutputWidget extends Widget { 13 | constructor(options) { 14 | super(); 15 | this._mimeType = options.mimeType; 16 | this._data = options.model.data; 17 | this._metadata = options.model.metadata; 18 | this.addClass(CLASS_NAME); 19 | } 20 | 21 | /** 22 | * A message handler invoked on an `'after-attach'` message 23 | */ 24 | onAfterAttach(msg) { 25 | /* Render initial data */ 26 | this._render(); 27 | } 28 | 29 | /** 30 | * A message handler invoked on an `'before-detach'` message 31 | */ 32 | onBeforeDetach(msg) { 33 | /* Dispose of resources used by this widget */ 34 | ReactDOM.unmountComponentAtNode(this.node); 35 | } 36 | 37 | /** 38 | * A message handler invoked on a `'child-added'` message 39 | */ 40 | onChildAdded(msg) { 41 | /* e.g. Inject a static image representation into the mime bundle for 42 | * endering on Github, etc. 43 | */ 44 | // renderLibrary.toPng(this.node).then(url => { 45 | // const data = url.split(',')[1]; 46 | // this._data.set('image/png', data); 47 | // }) 48 | } 49 | 50 | /** 51 | * A message handler invoked on a `'resize'` message 52 | */ 53 | onResize(msg) { 54 | /* Re-render on resize */ 55 | this._render(); 56 | } 57 | 58 | /** 59 | * Render data to DOM node 60 | */ 61 | _render() { 62 | const props = { 63 | data: this._data.get(this._mimeType), 64 | metadata: this._metadata.get(this._mimeType), 65 | width: this.node.offsetWidth, 66 | height: this.node.offsetHeight 67 | }; 68 | ReactDOM.render(, this.node); 69 | } 70 | } 71 | 72 | export class OutputRenderer { 73 | /** 74 | * The mime types that this OutputRenderer accepts 75 | */ 76 | mimeTypes = [MIME_TYPE]; 77 | 78 | /** 79 | * Whether the renderer can render given the render options 80 | */ 81 | canRender(options) { 82 | return this.mimeTypes.indexOf(options.mimeType) !== -1; 83 | } 84 | 85 | /** 86 | * Render the transformed mime bundle 87 | */ 88 | render(options) { 89 | return new OutputWidget(options); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /labextension/src/plugin.js: -------------------------------------------------------------------------------- 1 | import { IRenderMime } from '@jupyterlab/rendermime'; 2 | import { IDocumentRegistry } from '@jupyterlab/docregistry'; 3 | import { ILayoutRestorer, InstanceTracker } from '@jupyterlab/apputils'; 4 | import { toArray, ArrayExt } from '@phosphor/algorithm'; 5 | import { OutputRenderer } from './output'; 6 | import { DocWidgetFactory } from './doc'; 7 | import './index.css'; 8 | 9 | /** 10 | * The name of the factory 11 | */ 12 | const FACTORY = 'Plotly'; 13 | 14 | /** 15 | * Set the extensions associated with application/vnd.plotly.v1+json 16 | */ 17 | const EXTENSIONS = ['.plotly', '.plotly.json']; 18 | const DEFAULT_EXTENSIONS = ['.plotly', '.plotly.json']; 19 | 20 | /** 21 | * Activate the extension. 22 | */ 23 | function activatePlugin(app, rendermime, registry, restorer) { 24 | /** 25 | * Calculate the index of the renderer in the array renderers 26 | * e.g. Insert this renderer after any renderers with mime type that matches 27 | * "+json" 28 | */ 29 | // const index = ArrayExt.findLastIndex( 30 | // toArray(rendermime.mimeTypes()), 31 | // mime => mime.endsWith('+json') 32 | // ) + 1; 33 | /* ...or just insert it at the top */ 34 | const index = 0; 35 | 36 | /** 37 | * Add output renderer for application/vnd.plotly.v1+json data 38 | */ 39 | rendermime.addRenderer( 40 | { 41 | mimeType: 'application/vnd.plotly.v1+json', 42 | renderer: new OutputRenderer() 43 | }, 44 | index 45 | ); 46 | 47 | const factory = new DocWidgetFactory({ 48 | fileExtensions: EXTENSIONS, 49 | defaultFor: DEFAULT_EXTENSIONS, 50 | name: FACTORY 51 | }); 52 | 53 | /** 54 | * Add document renderer for .plotly files 55 | */ 56 | registry.addWidgetFactory(factory); 57 | 58 | const tracker = new InstanceTracker({ 59 | namespace: 'Plotly', 60 | shell: app.shell 61 | }); 62 | 63 | /** 64 | * Handle widget state deserialization 65 | */ 66 | restorer.restore(tracker, { 67 | command: 'file-operations:open', 68 | args: widget => ({ path: widget.context.path, factory: FACTORY }), 69 | name: widget => widget.context.path 70 | }); 71 | 72 | /** 73 | * Serialize widget state 74 | */ 75 | factory.widgetCreated.connect((sender, widget) => { 76 | tracker.add(widget); 77 | /* Notify the instance tracker if restore data needs to update */ 78 | widget.context.pathChanged.connect(() => { 79 | tracker.save(widget); 80 | }); 81 | }); 82 | } 83 | 84 | /** 85 | * Configure jupyterlab plugin 86 | */ 87 | const Plugin = { 88 | id: 'jupyter.extensions.Plotly', 89 | requires: [IRenderMime, IDocumentRegistry, ILayoutRestorer], 90 | activate: activatePlugin, 91 | autoStart: true 92 | }; 93 | 94 | export default Plugin; 95 | -------------------------------------------------------------------------------- /nbextension/README.md: -------------------------------------------------------------------------------- 1 | # nbextension 2 | 3 | A Jupyter Notebook extension for rendering Plotly 4 | 5 | ## Prerequisites 6 | 7 | * `notebook@>=4.3.0` 8 | 9 | ## Development 10 | 11 | Install dependencies and build Javascript: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | Re-build Javascript: 18 | 19 | ```bash 20 | npm run build 21 | ``` 22 | 23 | Watch `/src` directory and re-build on changes: 24 | 25 | ```bash 26 | npm run watch 27 | ``` 28 | 29 | Manage extension 30 | 31 | ```bash 32 | # Install 33 | npm run extension:install 34 | # Enable 35 | npm run extension:enable 36 | # Disable 37 | npm run extension:disable 38 | # Uninstall 39 | npm run extension:uninstall 40 | ``` 41 | -------------------------------------------------------------------------------- /nbextension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "jupyterlab_plotly_nbextension", 4 | "version": "0.18.0", 5 | "description": "A Jupyter Notebook extension for rendering Plotly", 6 | "author": "Grant Nestor ", 7 | "main": "src/index.js", 8 | "keywords": [ 9 | "jupyter", 10 | "jupyterlab", 11 | "jupyterlab extension" 12 | ], 13 | "scripts": { 14 | "build": "webpack", 15 | "watch": "watch \"npm install\" src ../component --wait 10 --ignoreDotFiles", 16 | "preinstall": "npm install ../component", 17 | "prepublish": "npm run build", 18 | "extension:install": "jupyter nbextension install --symlink --py --sys-prefix jupyterlab_plotly", 19 | "extension:uninstall": "jupyter nbextension uninstall --py --sys-prefix jupyterlab_plotly", 20 | "extension:enable": "jupyter nbextension enable --py --sys-prefix jupyterlab_plotly", 21 | "extension:disable": "jupyter nbextension disable --py --sys-prefix jupyterlab_plotly" 22 | }, 23 | "dependencies": { 24 | "react": "^15.3.2", 25 | "react-dom": "^15.3.2" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.18.2", 29 | "babel-loader": "^6.4.0", 30 | "babel-preset-latest": "^6.16.0", 31 | "babel-preset-react": "^6.16.0", 32 | "babel-preset-stage-0": "^6.22.0", 33 | "css-loader": "^0.25.0", 34 | "file-loader": "^0.9.0", 35 | "json-loader": "^0.5.4", 36 | "style-loader": "^0.13.1", 37 | "url-loader": "^0.5.7", 38 | "watch": "^1.0.1", 39 | "webpack": "^2.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nbextension/src/embed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the unpkg bundle containing custom model definitions. 3 | * 4 | * It differs from the notebook bundle in that it does not need to define a 5 | * dynamic baseURL for the static assets and may load some css that would 6 | * already be loaded by the notebook otherwise. 7 | */ 8 | 9 | export { version } from '../package.json'; 10 | -------------------------------------------------------------------------------- /nbextension/src/extension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the javascript that is run when the notebook is loaded. 3 | * It contains some requirejs configuration and the `load_ipython_extension` 4 | * which is required for any notebook extension. 5 | */ 6 | 7 | /** 8 | * Configure requirejs. 9 | */ 10 | if (window.require) { 11 | window.require.config({ 12 | map: { 13 | '*': { 14 | jupyterlab_plotly: 'nbextensions/jupyterlab_plotly/index' 15 | } 16 | } 17 | }); 18 | } 19 | 20 | /** 21 | * Export the required load_ipython_extention. 22 | */ 23 | export function load_ipython_extension() { 24 | define( 25 | [ 26 | 'nbextensions/jupyterlab_plotly/index', 27 | 'base/js/namespace', 28 | 'base/js/events', 29 | 'notebook/js/outputarea' 30 | ], 31 | (Extension, Jupyter, events, outputarea) => { 32 | const { notebook } = Jupyter; 33 | const { OutputArea } = outputarea; 34 | Extension.register_renderer(notebook, events, OutputArea); 35 | Extension.render_cells(notebook); 36 | } 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /nbextension/src/index.css: -------------------------------------------------------------------------------- 1 | div.output_subarea.output_Plotly { 2 | max-width: 100%; 3 | padding: 0; 4 | padding-top: 0.4em; 5 | } 6 | -------------------------------------------------------------------------------- /nbextension/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the notebook bundle containing custom model definitions. 3 | * Setup notebook base URL 4 | * Some static assets may be required by the custom widget javascript. The base 5 | * url for the notebook is not known at build time and is therefore computed 6 | * dynamically. 7 | */ 8 | 9 | __webpack_public_path__ = document 10 | .querySelector('body') 11 | .getAttribute('data-base-url') + 12 | 'nbextensions/jupyterlab_plotly/'; 13 | 14 | /** 15 | * Export widget models and views, and the npm package version number. 16 | */ 17 | export { register_renderer, render_cells } from './renderer.js'; 18 | export { version } from '../package.json'; 19 | -------------------------------------------------------------------------------- /nbextension/src/renderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PlotlyComponent from 'jupyterlab_plotly_react'; 4 | import './index.css'; 5 | 6 | const MIME_TYPE = 'application/vnd.plotly.v1+json'; 7 | const CLASS_NAME = 'output_Plotly rendered_html'; 8 | const DEFAULT_WIDTH = 840; 9 | const DEFAULT_HEIGHT = 360; 10 | 11 | /** 12 | * Render data to the DOM node 13 | */ 14 | function render(props, node) { 15 | ReactDOM.render(, node); 16 | } 17 | 18 | /** 19 | * Handle when an output is cleared or removed 20 | */ 21 | function handleClearOutput(event, { cell: { output_area } }) { 22 | /* Get rendered DOM node */ 23 | const toinsert = output_area.element.find(`.${CLASS_NAME.split(' ')[0]}`); 24 | /* e.g. Dispose of resources used by renderer library */ 25 | if (toinsert[0]) ReactDOM.unmountComponentAtNode(toinsert[0]); 26 | } 27 | 28 | /** 29 | * Handle when a new output is added 30 | */ 31 | function handleAddOutput(event, { output, output_area }) { 32 | /* Get rendered DOM node */ 33 | const toinsert = output_area.element.find(`.${CLASS_NAME.split(' ')[0]}`); 34 | /** e.g. Inject a static image representation into the mime bundle for 35 | * endering on Github, etc. 36 | */ 37 | // if (toinsert[0]) { 38 | // renderLibrary.toPng(toinsert[0]).then(url => { 39 | // const data = url.split(',')[1]; 40 | // output_area.outputs 41 | // .filter(output => output.data[MIME_TYPE]) 42 | // .forEach(output => { 43 | // output.data['image/png'] = data; 44 | // }); 45 | // }); 46 | // } 47 | } 48 | 49 | /** 50 | * Register the mime type and append_mime function with the notebook's 51 | * output area 52 | */ 53 | export function register_renderer(notebook, events, OutputArea) { 54 | /* A function to render output of 'application/vnd.plotly.v1+json' mime type */ 55 | const append_mime = function(data, metadata, element) { 56 | /* Create a DOM node to render to */ 57 | const toinsert = this.create_output_subarea( 58 | metadata, 59 | CLASS_NAME, 60 | MIME_TYPE 61 | ); 62 | this.keyboard_manager.register_events(toinsert); 63 | /* Render data to DOM node */ 64 | const props = { 65 | data, 66 | metadata: metadata[MIME_TYPE], 67 | width: element.width(), 68 | height: DEFAULT_HEIGHT 69 | }; 70 | render(props, toinsert[0]); 71 | element.append(toinsert); 72 | const output_area = this; 73 | this.element.on('changed', () => { 74 | if (output_area.outputs.length > 0) ReactDOM.unmountComponentAtNode(toinsert[0]); 75 | }); 76 | return toinsert; 77 | }; 78 | 79 | /* Handle when an output is cleared or removed */ 80 | events.on('clear_output.CodeCell', handleClearOutput); 81 | events.on('delete.Cell', handleClearOutput); 82 | 83 | /* Handle when a new output is added */ 84 | events.on('output_added.OutputArea', handleAddOutput); 85 | 86 | /** 87 | * Calculate the index of this renderer in `output_area.display_order` 88 | * e.g. Insert this renderer after any renderers with mime type that matches 89 | * "+json" 90 | */ 91 | // const mime_types = output_area.mime_types(); 92 | // const json_types = mime_types.filter(mimetype => mimetype.includes('+json')); 93 | // const index = mime_types.lastIndexOf(json_types.pop() + 1); 94 | 95 | /* ...or just insert it at the top */ 96 | const index = 0; 97 | 98 | /** 99 | * Register the mime type and append_mime function with output_area 100 | */ 101 | OutputArea.prototype.register_mime_type(MIME_TYPE, append_mime, { 102 | /* Is output safe? */ 103 | safe: true, 104 | /* Index of renderer in `output_area.display_order` */ 105 | index: index 106 | }); 107 | } 108 | 109 | /** 110 | * Re-render cells with output data of 'application/vnd.plotly.v1+json' mime type 111 | * on load notebook 112 | */ 113 | export function render_cells(notebook) { 114 | /* Get all cells in notebook */ 115 | notebook.get_cells().forEach(cell => { 116 | /* If a cell has output data of 'application/vnd.plotly.v1+json' mime type */ 117 | if ( 118 | cell.output_area && 119 | cell.output_area.outputs.find( 120 | output => output.data && output.data[MIME_TYPE] 121 | ) 122 | ) { 123 | /* Re-render the cell */ 124 | notebook.render_cell_output(cell); 125 | } 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /nbextension/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var version = require('./package.json').version; 3 | 4 | /** 5 | * Custom webpack loaders are generally the same for all webpack bundles, hence 6 | * stored in a separate local variable. 7 | */ 8 | var loaders = [ 9 | { 10 | test: /\.js$/, 11 | include: [ 12 | path.join(__dirname, 'src'), 13 | path.join(__dirname, 'node_modules', 'jupyterlab_plotly_react') 14 | ], 15 | loader: 'babel-loader', 16 | query: { presets: ['latest', 'stage-0', 'react'] } 17 | }, 18 | { test: /\.json$/, loader: 'json-loader' }, 19 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 20 | { test: /\.html$/, loader: 'file-loader' }, 21 | { test: /\.(jpg|png|gif)$/, loader: 'file-loader' }, 22 | { 23 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 24 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 25 | }, 26 | { 27 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 28 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 29 | }, 30 | { 31 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 32 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream' 33 | }, 34 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 35 | { 36 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 37 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml' 38 | } 39 | ]; 40 | 41 | var base = { 42 | output: { 43 | libraryTarget: 'amd', 44 | devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]' 45 | }, 46 | devtool: 'source-map', 47 | module: { loaders }, 48 | externals: [ 49 | 'nbextensions/jupyterlab_plotly/index', 50 | 'base/js/namespace', 51 | 'base/js/events', 52 | 'notebook/js/outputarea' 53 | ] 54 | }; 55 | 56 | module.exports = [ 57 | /** 58 | * Notebook extension 59 | * 60 | * This bundle only contains the part of the JavaScript that is run on 61 | * load of the notebook. This section generally only performs 62 | * some configuration for requirejs, and provides the legacy 63 | * "load_ipython_extension" function which is required for any notebook 64 | * extension. 65 | */ 66 | Object.assign({}, base, { 67 | entry: path.join(__dirname, 'src', 'extension.js'), 68 | output: Object.assign({}, base.output, { 69 | filename: 'extension.js', 70 | path: path.join( 71 | __dirname, 72 | '..', 73 | 'jupyterlab_plotly', 74 | 'static' 75 | ) 76 | }) 77 | }), 78 | /** 79 | * Bundle for the notebook containing the custom widget views and models 80 | * 81 | * This bundle contains the implementation for the custom widget views and 82 | * custom widget. 83 | * 84 | * It must be an amd module 85 | */ 86 | Object.assign({}, base, { 87 | entry: path.join(__dirname, 'src', 'index.js'), 88 | output: Object.assign({}, base.output, { 89 | filename: 'index.js', 90 | path: path.join( 91 | __dirname, 92 | '..', 93 | 'jupyterlab_plotly', 94 | 'static' 95 | ) 96 | }) 97 | }), 98 | /** 99 | * Embeddable jupyterlab_plotly bundle 100 | * 101 | * This bundle is generally almost identical to the notebook bundle 102 | * containing the custom widget views and models. 103 | * 104 | * The only difference is in the configuration of the webpack public path 105 | * for the static assets. 106 | * 107 | * It will be automatically distributed by unpkg to work with the static 108 | * widget embedder. 109 | * 110 | * The target bundle is always `lib/index.js`, which is the path required 111 | * by the custom widget embedder. 112 | */ 113 | Object.assign({}, base, { 114 | entry: './src/embed.js', 115 | output: Object.assign({}, base.output, { 116 | filename: 'index.js', 117 | path: path.join(__dirname, 'embed'), 118 | publicPath: 'https://unpkg.com/jupyterlab_plotly@' + 119 | version + 120 | '/lib/' 121 | }) 122 | }) 123 | ]; 124 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setupbase import create_cmdclass, install_npm 3 | 4 | cmdclass = create_cmdclass(['labextension', 'nbextension']) 5 | cmdclass['labextension'] = install_npm('labextension') 6 | cmdclass['nbextension'] = install_npm('nbextension') 7 | 8 | setup_args = dict( 9 | name = 'jupyterlab_plotly', 10 | version = '0.18.0', 11 | packages = ['jupyterlab_plotly'], 12 | author = 'Grant Nestor', 13 | author_email = 'grantnestor@gmail.com', 14 | url = 'http://jupyter.org', 15 | license = 'BSD', 16 | platforms = "Linux, Mac OS X, Windows", 17 | keywords = [ 18 | 'ipython', 19 | 'jupyter', 20 | 'jupyterlab', 21 | 'extension', 22 | 'renderer', 23 | 'plotly' 24 | ], 25 | classifiers = [ 26 | 'Intended Audience :: Developers', 27 | 'Intended Audience :: System Administrators', 28 | 'Intended Audience :: Science/Research', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | ], 37 | cmdclass = cmdclass, 38 | install_requires = [ 39 | 'jupyterlab>=0.18.0', 40 | 'notebook>=4.3.0', 41 | 'ipython>=1.0.0' 42 | ] 43 | ) 44 | 45 | if __name__ == '__main__': 46 | setup(**setup_args) 47 | -------------------------------------------------------------------------------- /setupbase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Jupyter Development Team. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | import os 8 | from os.path import join as pjoin 9 | import functools 10 | import pipes 11 | 12 | from setuptools import Command 13 | from setuptools.command.build_py import build_py 14 | from setuptools.command.sdist import sdist 15 | from setuptools.command.develop import develop 16 | from setuptools.command.bdist_egg import bdist_egg 17 | from distutils import log 18 | from subprocess import check_call 19 | import sys 20 | 21 | try: 22 | from wheel.bdist_wheel import bdist_wheel 23 | except ImportError: 24 | bdist_wheel = None 25 | 26 | if sys.platform == 'win32': 27 | from subprocess import list2cmdline 28 | else: 29 | def list2cmdline(cmd_list): 30 | return ' '.join(map(pipes.quote, cmd_list)) 31 | 32 | 33 | __version__ = '0.1.0' 34 | 35 | # --------------------------------------------------------------------------- 36 | # Top Level Variables 37 | # --------------------------------------------------------------------------- 38 | 39 | 40 | here = os.path.abspath(os.path.dirname(sys.argv[0])) 41 | is_repo = os.path.exists(pjoin(here, '.git')) 42 | node_modules = pjoin(here, 'node_modules') 43 | npm_path = ':'.join([ 44 | pjoin(here, 'node_modules', '.bin'), 45 | os.environ.get('PATH', os.defpath), 46 | ]) 47 | 48 | if "--skip-npm" in sys.argv: 49 | print("Skipping npm install as requested.") 50 | skip_npm = True 51 | sys.argv.remove("--skip-npm") 52 | else: 53 | skip_npm = False 54 | 55 | # --------------------------------------------------------------------------- 56 | # Public Functions 57 | # --------------------------------------------------------------------------- 58 | 59 | 60 | def get_data_files(top): 61 | """Get data files""" 62 | 63 | data_files = [] 64 | ntrim = len(here + os.path.sep) 65 | 66 | for (d, dirs, filenames) in os.walk(top): 67 | data_files.append(( 68 | d[ntrim:], 69 | [pjoin(d, f) for f in filenames] 70 | )) 71 | return data_files 72 | 73 | 74 | def find_packages(top): 75 | """ 76 | Find all of the packages. 77 | """ 78 | packages = [] 79 | for d, _, _ in os.walk(top): 80 | if os.path.exists(pjoin(d, '__init__.py')): 81 | packages.append(d.replace(os.path.sep, '.')) 82 | 83 | 84 | def create_cmdclass(wrappers=None, data_dirs=None): 85 | """Create a command class with the given optional wrappers. 86 | Parameters 87 | ---------- 88 | wrappers: list(str), optional 89 | The cmdclass names to run before running other commands 90 | data_dirs: list(str), optional. 91 | The directories containing static data. 92 | """ 93 | egg = bdist_egg if 'bdist_egg' in sys.argv else bdist_egg_disabled 94 | wrappers = wrappers or [] 95 | data_dirs = data_dirs or [] 96 | wrapper = functools.partial(wrap_command, wrappers, data_dirs) 97 | cmdclass = dict( 98 | build_py=wrapper(build_py, strict=is_repo), 99 | sdist=wrapper(sdist, strict=True), 100 | bdist_egg=egg, 101 | develop=wrapper(develop, strict=True) 102 | ) 103 | if bdist_wheel: 104 | cmdclass['bdist_wheel'] = wrapper(bdist_wheel, strict=True) 105 | return cmdclass 106 | 107 | 108 | def run(cmd, *args, **kwargs): 109 | """Echo a command before running it. Defaults to repo as cwd""" 110 | log.info('> ' + list2cmdline(cmd)) 111 | kwargs.setdefault('cwd', here) 112 | kwargs.setdefault('shell', sys.platform == 'win32') 113 | if not isinstance(cmd, list): 114 | cmd = cmd.split() 115 | return check_call(cmd, *args, **kwargs) 116 | 117 | 118 | def is_stale(target, source): 119 | """Test whether the target file/directory is stale based on the source 120 | file/directory. 121 | """ 122 | if not os.path.exists(target): 123 | return True 124 | return mtime(target) < mtime(source) 125 | 126 | 127 | class BaseCommand(Command): 128 | """Empty command because Command needs subclasses to override too much""" 129 | user_options = [] 130 | 131 | def initialize_options(self): 132 | pass 133 | 134 | def finalize_options(self): 135 | pass 136 | 137 | def get_inputs(self): 138 | return [] 139 | 140 | def get_outputs(self): 141 | return [] 142 | 143 | 144 | def combine_commands(*commands): 145 | """Return a Command that combines several commands.""" 146 | 147 | class CombinedCommand(Command): 148 | 149 | def initialize_options(self): 150 | self.commands = [] 151 | for C in commands: 152 | self.commands.append(C(self.distribution)) 153 | for c in self.commands: 154 | c.initialize_options() 155 | 156 | def finalize_options(self): 157 | for c in self.commands: 158 | c.finalize_options() 159 | 160 | def run(self): 161 | for c in self.commands: 162 | c.run() 163 | return CombinedCommand 164 | 165 | 166 | def mtime(path): 167 | """shorthand for mtime""" 168 | return os.stat(path).st_mtime 169 | 170 | 171 | def install_npm(path=None, build_dir=None, source_dir=None, build_cmd='build'): 172 | """Return a Command for managing an npm installation. 173 | Parameters 174 | ---------- 175 | path: str, optional 176 | The base path of the node package. Defaults to the repo root. 177 | build_dir: str, optional 178 | The target build directory. If this and source_dir are given, 179 | the JavaScript will only be build if necessary. 180 | source_dir: str, optional 181 | The source code directory. 182 | build_cmd: str, optional 183 | The npm command to build assets to the build_dir. 184 | """ 185 | 186 | class NPM(BaseCommand): 187 | description = 'install package.json dependencies using npm' 188 | 189 | def run(self): 190 | if skip_npm: 191 | log.info('Skipping npm-installation') 192 | return 193 | node_package = path or here 194 | node_modules = pjoin(node_package, 'node_modules') 195 | 196 | if not which("npm"): 197 | log.error("`npm` unavailable. If you're running this command " 198 | "using sudo, make sure `npm` is availble to sudo") 199 | return 200 | if is_stale(node_modules, pjoin(node_package, 'package.json')): 201 | log.info('Installing build dependencies with npm. This may ' 202 | 'take a while...') 203 | run(['npm', 'install'], cwd=node_package) 204 | if build_dir and source_dir: 205 | should_build = is_stale(build_dir, source_dir) 206 | else: 207 | should_build = True 208 | if should_build: 209 | run(['npm', 'run', build_cmd], cwd=node_package) 210 | 211 | return NPM 212 | 213 | 214 | # `shutils.which` function copied verbatim from the Python-3.3 source. 215 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 216 | """Given a command, mode, and a PATH string, return the path which 217 | conforms to the given mode on the PATH, or None if there is no such 218 | file. 219 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 220 | of os.environ.get("PATH"), or can be overridden with a custom search 221 | path. 222 | """ 223 | 224 | # Check that a given file can be accessed with the correct mode. 225 | # Additionally check that `file` is not a directory, as on Windows 226 | # directories pass the os.access check. 227 | def _access_check(fn, mode): 228 | return (os.path.exists(fn) and os.access(fn, mode) and 229 | not os.path.isdir(fn)) 230 | 231 | # Short circuit. If we're given a full path which matches the mode 232 | # and it exists, we're done here. 233 | if _access_check(cmd, mode): 234 | return cmd 235 | 236 | path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) 237 | 238 | if sys.platform == "win32": 239 | # The current directory takes precedence on Windows. 240 | if os.curdir not in path: 241 | path.insert(0, os.curdir) 242 | 243 | # PATHEXT is necessary to check on Windows. 244 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 245 | # See if the given file matches any of the expected path extensions. 246 | # This will allow us to short circuit when given "python.exe". 247 | matches = [cmd for ext in pathext if cmd.lower().endswith(ext.lower())] 248 | # If it does match, only test that one, otherwise we have to try 249 | # others. 250 | files = [cmd] if matches else [cmd + ext.lower() for ext in pathext] 251 | else: 252 | # On other platforms you don't have things like PATHEXT to tell you 253 | # what file suffixes are executable, so just pass on cmd as-is. 254 | files = [cmd] 255 | 256 | seen = set() 257 | for dir in path: 258 | dir = os.path.normcase(dir) 259 | if dir not in seen: 260 | seen.add(dir) 261 | for thefile in files: 262 | name = os.path.join(dir, thefile) 263 | if _access_check(name, mode): 264 | return name 265 | return None 266 | 267 | 268 | # --------------------------------------------------------------------------- 269 | # Private Functions 270 | # --------------------------------------------------------------------------- 271 | 272 | 273 | def wrap_command(cmds, data_dirs, cls, strict=True): 274 | """Wrap a setup command 275 | Parameters 276 | ---------- 277 | cmds: list(str) 278 | The names of the other commands to run prior to the command. 279 | strict: boolean, optional 280 | Wether to raise errors when a pre-command fails. 281 | """ 282 | class WrappedCommand(cls): 283 | 284 | def run(self): 285 | if not getattr(self, 'uninstall', None): 286 | try: 287 | [self.run_command(cmd) for cmd in cmds] 288 | except Exception: 289 | if strict: 290 | raise 291 | else: 292 | pass 293 | 294 | result = cls.run(self) 295 | data_files = [] 296 | for dname in data_dirs: 297 | data_files.extend(get_data_files(dname)) 298 | # update data-files in case this created new files 299 | self.distribution.data_files = data_files 300 | return result 301 | return WrappedCommand 302 | 303 | 304 | class bdist_egg_disabled(bdist_egg): 305 | """Disabled version of bdist_egg 306 | Prevents setup.py install performing setuptools' default easy_install, 307 | which it should never ever do. 308 | """ 309 | 310 | def run(self): 311 | sys.exit("Aborting implicit building of eggs. Use `pip install .` " + 312 | " to install from source.") 313 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | nbExtFlags=$1 3 | 4 | jupyter lab --version 5 | if [ $? -eq 0 ]; then 6 | jupyter labextension uninstall --py $nbExtFlags jupyterlab_test 7 | fi 8 | 9 | jupyter notebook --version 10 | if [ $? -eq 0 ]; then 11 | jupyter nbextension uninstall --py $nbExtFlags jupyterlab_test 12 | fi 13 | 14 | pip --version 15 | if [ $? -eq 0 ]; then 16 | pip uninstall -v . 17 | else 18 | echo "'pip --version' failed, therefore pip is not installed. In order to perform 19 | an install of jupyterlab_plotly you must have both pip and npm installed on 20 | your machine! See https://packaging.python.org/installing/ for installation instructions." 21 | fi 22 | --------------------------------------------------------------------------------