This example is a scaled-down standalone version of the demo at https://demo.bokeh.org/surface3d that does not involve a Bokeh server.
import numpy as np from bokeh.core.properties import Instance, String from bokeh.io import show from bokeh.models import ColumnDataSource, LayoutDOM from bokeh.util.compiler import TypeScript TS_CODE = """ // This custom model wraps one part of the third-party vis.js library: // // http://visjs.org/index.html // // Making it easy to hook up python data analytics tools (NumPy, SciPy, // Pandas, etc.) to web presentations using the Bokeh server. import {LayoutDOM, LayoutDOMView} from "models/layouts/layout_dom" import {ColumnDataSource} from "models/sources/column_data_source" import {LayoutItem} from "core/layout" import * as p from "core/properties" declare namespace vis { class Graph3d { constructor(el: HTMLElement, data: object, OPTIONS: object) setData(data: vis.DataSet): void } class DataSet { add(data: unknown): void } } // This defines some default options for the Graph3d feature of vis.js // See: http://visjs.org/graph3d_examples.html for more details. const OPTIONS = { width: '600px', height: '600px', style: 'surface', showPerspective: true, showGrid: true, keepAspectRatio: true, verticalRatio: 1.0, legendLabel: 'stuff', cameraPosition: { horizontal: -0.35, vertical: 0.22, distance: 1.8, }, } // To create custom model extensions that will render on to the HTML canvas // or into the DOM, we must create a View subclass for the model. // // In this case we will subclass from the existing BokehJS ``LayoutDOMView`` export class Surface3dView extends LayoutDOMView { model: Surface3d private _graph: vis.Graph3d initialize(): void { super.initialize() const url = "https://cdnjs.cloudflare.com/ajax/libs/vis/4.16.1/vis.min.js" const script = document.createElement("script") script.onload = () => this._init() script.async = false script.src = url document.head.appendChild(script) } private _init(): void { // Create a new Graph3s using the vis.js API. This assumes the vis.js has // already been loaded (e.g. in a custom app template). In the future Bokeh // models will be able to specify and load external scripts automatically. // // BokehJS Views create <div> elements by default, accessible as this.el. // Many Bokeh views ignore this default <div>, and instead do things like // draw to the HTML canvas. In this case though, we use the <div> to attach // a Graph3d to the DOM. this._graph = new vis.Graph3d(this.el, this.get_data(), OPTIONS) // Set a listener so that when the Bokeh data source has a change // event, we can process the new data this.connect(this.model.data_source.change, () => { this._graph.setData(this.get_data()) }) } // This is the callback executed when the Bokeh data has an change. Its basic // function is to adapt the Bokeh data source to the vis.js DataSet format. get_data(): vis.DataSet { const data = new vis.DataSet() const source = this.model.data_source for (let i = 0; i < source.get_length()!; i++) { data.add({ x: source.data[this.model.x][i], y: source.data[this.model.y][i], z: source.data[this.model.z][i], }) } return data } get child_models(): LayoutDOM[] { return [] } _update_layout(): void { this.layout = new LayoutItem() this.layout.set_sizing(this.box_sizing()) } } // We must also create a corresponding JavaScript BokehJS model subclass to // correspond to the python Bokeh model subclass. In this case, since we want // an element that can position itself in the DOM according to a Bokeh layout, // we subclass from ``LayoutDOM`` export namespace Surface3d { export type Attrs = p.AttrsOf<Props> export type Props = LayoutDOM.Props & { x: p.Property<string> y: p.Property<string> z: p.Property<string> data_source: p.Property<ColumnDataSource> } } export interface Surface3d extends Surface3d.Attrs {} export class Surface3d extends LayoutDOM { properties: Surface3d.Props __view_type__: Surface3dView constructor(attrs?: Partial<Surface3d.Attrs>) { super(attrs) } // The ``__name__`` class attribute should generally match exactly the name // of the corresponding Python class. Note that if using TypeScript, this // will be automatically filled in during compilation, so except in some // special cases, this shouldn't be generally included manually, to avoid // typos, which would prohibit serialization/deserialization of this model. static __name__ = "Surface3d" static init_Surface3d() { // This is usually boilerplate. In some cases there may not be a view. this.prototype.default_view = Surface3dView // The @define block adds corresponding "properties" to the JS model. These // should basically line up 1-1 with the Python model class. Most property // types have counterparts, e.g. ``bokeh.core.properties.String`` will be // ``String`` in the JS implementatin. Where the JS type system is not yet // as rich, you can use ``p.Any`` as a "wildcard" property type. this.define<Surface3d.Props>(({String, Ref}) => ({ x: [ String ], y: [ String ], z: [ String ], data_source: [ Ref(ColumnDataSource) ], })) } } """ # This custom extension model will have a DOM view that should layout-able in # Bokeh layouts, so use ``LayoutDOM`` as the base class. If you wanted to create # a custom tool, you could inherit from ``Tool``, or from ``Glyph`` if you # wanted to create a custom glyph, etc. class Surface3d(LayoutDOM): # The special class attribute ``__implementation__`` should contain a string # of JavaScript code that implements the browser side of the extension model. __implementation__ = TypeScript(TS_CODE) # Below are all the "properties" for this model. Bokeh properties are # class attributes that define the fields (and their types) that can be # communicated automatically between Python and the browser. Properties # also support type validation. More information about properties in # can be found here: # # https://docs.bokeh.org/en/latest/docs/reference/core/properties.html#bokeh-core-properties # This is a Bokeh ColumnDataSource that can be updated in the Bokeh # server by Python code data_source = Instance(ColumnDataSource) # The vis.js library that we are wrapping expects data for x, y, and z. # The data will actually be stored in the ColumnDataSource, but these # properties let us specify the *name* of the column that should be # used for each field. x = String y = String z = String x = np.arange(0, 300, 10) y = np.arange(0, 300, 10) xx, yy = np.meshgrid(x, y) xx = xx.ravel() yy = yy.ravel() value = np.sin(xx / 50) * np.cos(yy / 50) * 50 + 50 source = ColumnDataSource(data=dict(x=xx, y=yy, z=value)) surface = Surface3d(x="x", y="y", z="z", data_source=source, width=600, height=600) show(surface)