├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE.txt ├── README.md ├── basic.png ├── jupyterlab_bokeh_server ├── __init__.py └── server.py ├── package.json ├── resources.png ├── setup.py ├── src ├── dashboard.tsx └── index.ts ├── style ├── chart-dark.svg ├── chart-light.svg └── index.css ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.egg-info/ 5 | .ipynb_checkpoints 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/lib 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Ian Rose and Matthew Rocklin 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JupyterLab and Bokeh Server 2 | =========================== 3 | 4 | A JupyterLab extension for displaying the contents of a Bokeh server. 5 | 6 | This project serves as an example for how to integrate a Bokeh server 7 | application into JupyterLab. This makes it easy for Python developers to 8 | develop rich dashboards and integrate them directly into Jupyter environments. 9 | 10 | Motivation 11 | ---------- 12 | 13 | We want to give Jupyter users rich and real-time dashboards while they work. 14 | This can be useful for tracking quantities like the following: 15 | 16 | - Computational resources like CPU, Memory, and Network 17 | - External instruments or detectors 18 | - Other resources like GPU accelerators 19 | - Web APIs 20 | - ... 21 | 22 | [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) extensions provide 23 | a platform for these dashboards, but require some JavaScript expertise. For 24 | Python-only developers, this requirement may prove to be a bottleneck. 25 | Fortunately the [Bokeh](https://bokeh.pydata.org) plotting library makes it 26 | easy to produce rich and interactive browser-based visuals from Python, 27 | effectively crossing this boundary. 28 | 29 | This repository serves as an example on how to create rich dashboards in Python 30 | with Bokeh and then smoothly integrate those dashboards into JupyterLab for a 31 | first-class dashboarding experience that is accessible to Python-only 32 | developers. 33 | 34 | 35 | What's here 36 | ----------- 37 | 38 | This repository contains two sets of code: 39 | 40 | - Python code defining a Bokeh Server application that generates a couple of 41 | live plots in the `jupyterlab_bokeh_server/` directory 42 | - TypeScript code integrating these plots into JupyterLab in the `src/` 43 | directory 44 | 45 | You should be able to modify only the Python code to produce a dashboard system 46 | that works well for you without modifying the TypeScript code. 47 | 48 | There are also two branches in this repository: 49 | 50 | 1. **master** contains a basic dashboard with two plots, a line plot and 51 | a histogram, that display randomly varying data: 52 | ![basic](./basic.png) 53 | And is available in a live notebook here: `TODO binder link` 54 | 55 | - **system-resouces** expands on the toy system above to create a real-world example 56 | that uses the `psutil` module to show CPU, memory, network, and storage 57 | activity: 58 | ![resources](./resources.png) 59 | And is available in a live notebook here: `TODO binder link` 60 | 61 | You can view the [difference between these two branches](https://github.com/ian-r-rose/jupyterlab-bokeh-server/compare/system-resources). 62 | This should give a sense for what you need to do to construct your own 63 | JupyterLab enabled dashboard. 64 | 65 | 66 | History 67 | ------- 68 | 69 | This project is a generalization of the approach taken by the 70 | [Dask JupyterLab Extension](https://github.com/dask/dask-labextension) which 71 | integrates a rich dashboard for distributed computing into JupyterLab, which 72 | has demonstrated value for many Dask and Jupyter users in the past. 73 | 74 | ![](https://github.com/dask/dask-org/raw/master/images/dask-labextension.png) 75 | 76 | 77 | ## Prerequisites 78 | 79 | * JupyterLab 1.0.0a3 80 | * bokeh 81 | 82 | ## Installation 83 | 84 | This extension has a server-side (Python) and a client-side (Typescript) component, 85 | and we must install both in order for it to work. 86 | 87 | To install the server-side component, run the following in your terminal 88 | 89 | ```bash 90 | pip install jupyterlab-bokeh-server 91 | ``` 92 | 93 | To install the client-side component, run 94 | 95 | ```bash 96 | jupyter labextension install jupyterlab-bokeh-server 97 | ``` 98 | 99 | ## Development 100 | 101 | To install the server-side part, run the following in your terminal from the repository directory: 102 | 103 | ```bash 104 | pip install -e . 105 | ``` 106 | 107 | In order to install the client-side component (requires node version 8 or later), run the following in the repository directory: 108 | 109 | ```bash 110 | jlpm install 111 | jlpm run build 112 | jupyter labextension install . 113 | ``` 114 | 115 | To rebuild the package and the JupyterLab app: 116 | 117 | ```bash 118 | jlpm run build 119 | jupyter lab build 120 | ``` 121 | 122 | ## Publishing 123 | 124 | In order to distribute your bokeh dashboard application, 125 | you must publish the two subpackages. 126 | The JupyterLab frontend part should be published to [npm](https://npmjs.org), 127 | and the server-side part to [PyPI](https://pypi.org) 128 | or [conda-forge](https://conda-forge.org) (or both). 129 | 130 | Instructions for publishing the JupyterLab extension can be found 131 | [here](https://jupyterlab.readthedocs.io/en/stable/developer/xkcd_extension_tutorial.html#publish-your-extension-to-npmjs-org). 132 | A nice write-up for how to publish a package to PyPI can be found in the 133 | [nbconvert documentation](https://nbconvert.readthedocs.io/en/latest/development_release.html). 134 | -------------------------------------------------------------------------------- /basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ian-r-rose/jupyterlab-bokeh-server/3c20d8159173d091c2186125560274ac12be642e/basic.png -------------------------------------------------------------------------------- /jupyterlab_bokeh_server/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Return config on servers to start for bokeh 3 | 4 | See https://jupyter-server-proxy.readthedocs.io/en/latest/server-process.html 5 | for more information. 6 | """ 7 | import os 8 | import sys 9 | 10 | serverfile = os.path.join(os.path.dirname(__file__), "server.py") 11 | 12 | 13 | def launch_server(): 14 | return {"command": [sys.executable, serverfile, '{port}']} 15 | -------------------------------------------------------------------------------- /jupyterlab_bokeh_server/server.py: -------------------------------------------------------------------------------- 1 | from bokeh.server.server import Server 2 | from bokeh.plotting import figure, ColumnDataSource 3 | from tornado import web 4 | 5 | import random 6 | import sys 7 | import time 8 | 9 | 10 | def lineplot(doc): 11 | fig = figure(title="Line plot!", sizing_mode="stretch_both", x_axis_type="datetime") 12 | source = ColumnDataSource({"x": [], "y": []}) 13 | fig.line(source=source, x="x", y="y") 14 | 15 | doc.title = "Line Plot!" 16 | doc.add_root(fig) 17 | 18 | y = 0 19 | 20 | def cb(): 21 | nonlocal y 22 | now = time.time() * 1000 # bokeh measures in ms 23 | y += random.random() - 0.5 24 | 25 | source.stream({"x": [now], "y": [y]}, 100) 26 | 27 | doc.add_periodic_callback(cb, 100) 28 | 29 | 30 | def histogram(doc): 31 | fig = figure(title="Histogram!", sizing_mode="stretch_both") 32 | 33 | left = [0, 1, 2, 3, 4, 5] 34 | right = [l + 0.9 for l in left] 35 | ys = [0 for _ in left] 36 | 37 | source = ColumnDataSource({"left": left, "right": right, "y": [ys]}) 38 | 39 | fig.quad(source=source, left="left", right="right", bottom=0, top="y", color="blue") 40 | 41 | doc.title = "Histogram!" 42 | doc.add_root(fig) 43 | 44 | def cb(): 45 | nonlocal ys 46 | 47 | for i, y in enumerate(ys): 48 | ys[i] = 0.9 * y + 0.1 * random.random() 49 | 50 | source.data.update({"y": ys}) 51 | 52 | doc.add_periodic_callback(cb, 100) 53 | 54 | routes = { 55 | "/line": lineplot, 56 | "/histogram": histogram, 57 | } 58 | 59 | 60 | class RouteIndex(web.RequestHandler): 61 | """ A JSON index of all routes present on the Bokeh Server """ 62 | 63 | def get(self): 64 | self.write({route: route.strip("/").title() for route in routes}) 65 | 66 | 67 | if __name__ == "__main__": 68 | from tornado.ioloop import IOLoop 69 | 70 | server = Server(routes, port=int(sys.argv[1]), allow_websocket_origin=["*"]) 71 | server.start() 72 | 73 | server._tornado.add_handlers( 74 | r".*", [(server.prefix + "/" + "index.json", RouteIndex, {})] 75 | ) 76 | 77 | IOLoop.current().start() 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab-bokeh-server", 3 | "version": "0.1.0", 4 | "description": "A JupyterLab extension for displaying the contents of a Bokeh server", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/my_name/myextension", 11 | "bugs": { 12 | "url": "https://github.com/my_name/myextension/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": "Matt Rocklin and Ian Rose", 16 | "files": [ 17 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 18 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 19 | ], 20 | "main": "lib/index.js", 21 | "types": "lib/index.d.ts", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/my_name/myextension.git" 25 | }, 26 | "scripts": { 27 | "build": "tsc", 28 | "clean": "rimraf lib", 29 | "watch": "tsc -w" 30 | }, 31 | "dependencies": { 32 | "@jupyterlab/application": "^1.0.0-alpha.6", 33 | "@jupyterlab/apputils": "^1.0.0-alpha.6", 34 | "@jupyterlab/coreutils": "3.0.0-alpha.6", 35 | "react": "^16.4.2", 36 | "react-dom": "^16.4.2" 37 | }, 38 | "devDependencies": { 39 | "@types/react": "~16.4.13", 40 | "@types/react-dom": "~16.0.7", 41 | "rimraf": "^2.6.1", 42 | "typescript": "~3.3.1" 43 | }, 44 | "jupyterlab": { 45 | "extension": true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ian-r-rose/jupyterlab-bokeh-server/3c20d8159173d091c2186125560274ac12be642e/resources.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name="jupyterlab-bokeh-server", 5 | version='0.1.0', 6 | url="https://github.com/ian-r-rose/jupyterlab-bokeh-server", 7 | author="Matt Rocklin and Ian Rose", 8 | description="projectjupyter@gmail.com", 9 | license="BSD", 10 | packages=setuptools.find_packages(), 11 | keywords=['Jupyter'], 12 | classifiers=['Framework :: Jupyter'], 13 | install_requires=[ 14 | 'jupyter-server-proxy' 15 | ], 16 | entry_points={ 17 | 'jupyter_serverproxy_servers': [ 18 | 'bokeh-dashboard = jupyterlab_bokeh_server:launch_server', 19 | ] 20 | }, 21 | package_data={ 22 | 'jupyterlab_bokeh_server': ['icons/*'], 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /src/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { IFrame, MainAreaWidget } from '@jupyterlab/apputils'; 2 | 3 | import { URLExt } from '@jupyterlab/coreutils'; 4 | 5 | import { ServerConnection } from '@jupyterlab/services'; 6 | 7 | import { JSONExt, JSONObject } from '@phosphor/coreutils'; 8 | 9 | import { Message } from '@phosphor/messaging'; 10 | 11 | import { Widget, PanelLayout } from '@phosphor/widgets'; 12 | 13 | import * as React from 'react'; 14 | import * as ReactDOM from 'react-dom'; 15 | 16 | /** 17 | * A class for hosting a Bokeh dashboard in an iframe. 18 | */ 19 | export class BokehDashboard extends MainAreaWidget