Combine 2D and 3D plot in a doc

Hi, i am currently using Bokeh in Jupyter Notebook in roder to make a little App related to teeth plots. At first i started using plotly but i soon realized it lacked a very important feature for this project (You can not use mouse events over a plot in Plotly), so i moved to Bokeh.

For the moment i fit my needs, but it would be awesome if i could merge a 2D and a 3D plot in the same server app. This example is not working, but i discovered a Holoviews example that merges different elements form Holoviews and Bokeh Holoviews example but when changing the hv.Curve by a hv.Surface it does not work.

Here are some screenshots of the idea i’m trying. I can currently plot the points in 2D and perform all the mouse-click actions i need. The widgets are currently working, and when i click the RadioButton in the top of the figure (“Resultado 2D” - “Resultado 3D”) i close the 2D widgets and plots and i open another plot where i want to show and interact with a 3D plot

The idea is to click the Button that says “Resultado 3D” and the plot that will be shown would be a 3D plot of some points

Thanks in advance :smiley:

This is does not provide any information to go on. The example in the docs is working when I visit the page, and the example in the page was generated by that code in the page. Are you saying the example on the page is not working when visit the page? Or when you try to run it verbatim locally? Or you have tried to adapt it to your case and that is not working? Or something else?

but i discovered a Holoviews example … but when changing the hv.Curve by a hv.Surface it does not work.

Holoviews is a separate project maintained by a different group of people. Questions about HV should be directed at the HoloViz Discourse:

1 Like

Hi, when i copy that code and paste it into a jupyter notebook cell it just displays a blank page like this:

Okey, i tried it in a new Python script and it works but, is there a way of embbeding it into the response of a jupyter notebook cell instead of showing in a different page? (For example, in place of the Plot shown in this screenshot)

But still being inside a bokeh server app?

At a minimum, you would need to call output_notebook after defining the custom extensions, but before calling show. The call to output_notebook is what “publishes” the necessary JavaScript for Bokeh models inside the browser’s JavaScript runtime.

1 Like

Since the Surface3d extension is an extension of LayoutDOM, that means you can treat it like a figure/layout etc.

Basically just add this to the bottom of the above referenced example demoing this:

from bokeh.plotting import figure
from bokeh.models import Tabs, Panel

t1 = Panel(child=surface,title='3D')
f = figure()
f.line([1,2,3],[1,2,5])
t2 = Panel(child=f,title='Other Thing')
tabs = Tabs(tabs=[t1,t2])

show(tabs)

tabs3d

1 Like

Awesome!!!, this is exactly what i need :smiley: , but when i copy the example in a new jupyter cell it shows a white plot

The code i got is this one, no more cells have been ran:

import numpy as np
import bokeh

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 {
    // 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))

bokeh.io.output_notebook()
surface = Surface3d(x="x", y="y", z="z", data_source=source, width=600, height=600)

from bokeh.plotting import figure
from bokeh.models import Tabs, Panel

t1 = Panel(child=surface,title='3D')
f = figure()
f.line([1,2,3],[1,2,5])
t2 = Panel(child=f,title='Other Thing')
tabs = Tabs(tabs=[t1,t2])

show(tabs)

I should have been explicit that that output_notebook probably needs to be in its own cell, or at least, at different cell than the call to show.

All that said, using custom extensions in notebooks is not a common occurrence and accordingly not nearly as well-established practice as other uses of Bokeh. There may simply be an issue with that combination of usage. If it is still not working after splitting up cells, then you would need to provide more information about your actual environment (classic notebook? Or JupyterLab? What versions? etc)

1 Like

Still a blank page…

I’m currently using Jupyter Notebook v3.9.7 in Brave

Any errors/messages reported in the browser’s JavaScript console?

1 Like

Also I think you need to provide a more detailed package/env dump because I don’t really know what ot make of this. Notebook is currently on 7.x so 3.9.7 would be absolutely ancient at this point.

1 Like

This one

jsErros

Yeah that means the external vis.js library that actually does the little 3d plots has not loaded. Is Brave blocking it? If so there is nothing we can do about that on our end.

1 Like

My fault, i printed the Python version instead of the Jupyter Version, the Jupyter version is IPython : 7.29.0.

I’m going to try it in a different browser

@SDSauron_Programer Your code works for me with JupyterLab, but not classic notebook. All I can do is suggest that you use the newer JupyterLab instead of classic notebook (which I am sure the Jupyter team would like all of its users to do in any case).

1 Like

Okey, it also works for me in JupyterLab, i think i will continue the project there, thanks a lot :slight_smile:

1 Like