Send onclick event from wrapped javascript code to bokeh python function

in the documented example of wrapping vis.js the plot is dynamically updated by the javascript code when the ColumnDataSource in python is changed. i want to go the other way. specifically, i want to capture a javascript event generated by vis.js and have a python function executed. basically, how does one define Surface3d.on_click() in that example?

for those interested, i have refactored that example to use plotly, which i find much more performant. code follows, albeit uses a 3D scatter instead of surface. it also captures on click events and dumps them to the javascript console.

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

from bokeh.plotting import output_file
output_file("plotly.html")


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 Plotly {
  class newPlot {
    constructor(el: HTMLElement, data: object, OPTIONS: object)
  }

  class update {
    constructor(el: HTMLElement, data: object, OPTIONS: object)
  }
}

// This defines some default options for the Graph3d feature of vis.js
// See: http://visjs.org/graph3d_examples.html for more details.
const OPTIONS = {
  margin: {
    l: 0,
    r: 0,
    b: 0,
    t: 0
  },
  hovermode: false,
  showlegend: false,
  scene: {
    xaxis: { visible: false },
    yaxis: { visible: false },
    zaxis: { visible: false },
  }
}
// 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

  initialize(): void {
    super.initialize()

    const url = "https://cdn.plot.ly/plotly-latest.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.
    new Plotly.newPlot(this.el,
                                     [{x:this.model.data_source.data[this.model.x],
                                       y:this.model.data_source.data[this.model.y],
                                       z:this.model.data_source.data[this.model.z],
                                       mode: 'markers',
                                       marker: {
                                         size: 12,
                                         opacity: 0.1},
                                       type: 'scatter3d'
                                      }], 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, () => {
      new Plotly.update(this.el, [{x:this.model.data_source.data[this.model.x],
                              y:this.model.data_source.data[this.model.y],
                              z:this.model.data_source.data[this.model.z],
                              mode: 'markers',
                              marker: {
                                size: 12,
                                opacity: 0.1},
                              type: 'scatter3d'
                             }], OPTIONS)
    });

    // @ts-ignore
    (<HTMLDivElement>this.el).on('plotly_click', function (data){
      console.log('x: '+data.points[0].x+', y: '+data.points[0].y+', z: '+data.points[0].z)
    });

  }

  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

  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
    // ``p.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>({
      x:            [ p.String   ],
      y:            [ p.String   ],
      z:            [ p.String   ],
      data_source:  [ p.Instance ],
    })
  }
}
"""

# 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)

@bjarthur70 are planning to use this in a Bokeh server application? You are currently generating standalone HTML content with output_file and show. In this scenario, there is no Python process to run any on_click callbacks. If you want to use on_change with real Python callbacks, you would first need to convert this to a Bokeh server application (the Bokeh server itself is the Python process that would execute your callbacks)

indeed i do plan to use a bokeh server. the above code was just to see whether i could get plotly to work at all, and to capture its events. sorry, should have mentioned this.

so within class Surface3d i add a def on_click(self, callbackf) maybe? and then what do i have it do?

so within class Surface3d i add a def on_click(self, callbackf) maybe? and then what do i have it do?

No, on_change is a generic facility that Bokeh itself provides for any properties that you define. There’s a few options. This simplest is probably to add a new property to Surface3d e.g. foo = Int(...) (or String or whatever type you want) then you can add a Python callback in the standard way:

surface.on_change('foo', ...)

and it will respond to any changes you make to foo on the JS side.