Hi all,
I’ve uploaded the issue to Stackoverflow as well, but I’ll put it here too.
I am currently working on a project where I would like to output a 3D interactive scatter plot using Bokeh. I would like to colour the dots based on 2 or 3 categories, and I would like to show the gene corresponding to the dot after hovering it. I am aware of the fact that Bokeh does not fully implement 3D plots, and I found the following script, which allows to produce such 3D plot with python (original code).
Although the original code produces a 3D surface, with some reading of the documentation I’ve managed to produce a 3D plot. I’ve also managed to color the dots based on categories. However, when I try to produce tooltips, whose information will be encoded in the ‘extra’ variable in python (or any other), I am unable to produce that information. I have no knowledge of JS, so I am just trying to tweak the variables to see what happens.
The code I produced is this:
from future import division
from bokeh.core.properties import Instance, String
from bokeh.models import ColumnDataSource, LayoutDOM
from bokeh.io import show
import numpy as np
JS_CODE = “”"
This file contains the JavaScript (CoffeeScript) implementation
for a Bokeh custom extension. The “surface3d.py” contains the
python counterpart.
···
This custom model wraps one part of the third-party vis.js library:
vis.js
Making it easy to hook up python data analytics tools (NumPy, SciPy,
Pandas, etc.) to web presentations using the Bokeh server.
These “require” lines are similar to python “import” statements
import * as p from “core/properties”
import {LayoutDOM, LayoutDOMView} from “models/layouts/layout_dom”
This defines some default options for the Graph3d feature of vis.js
See: http://visjs.org/graph3d_examples.html for more details.
OPTIONS =
width: ‘700px’
height: ‘700px’
showLegend: false
legendLabel: ‘’
xLabel: ‘PC1’
yLabel: ‘PC2’
zLabel: ‘PC3’
style: ‘dot-color’
showPerspective: true
showGrid: false
keepAspectRatio: true
verticalRatio: 1.0
cameraPosition:
horizontal: -0.35
vertical: 0.22
distance: 1.8
dotSizeRatio: 0.01
tooltip: (point) →
return 'value: ’ + point.z + ‘
’ + point.extra
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. Currently
Bokeh models and views are based on BackBone. More information about
using Backbone can be found here:
http://backbonejs.org/
In this case we will subclass from the existing BokehJS LayoutDOMView
,
corresponding to our
export class Surface3dView extends LayoutDOMView
initialize: (options) →
super(options)
url = “https://cdnjs.cloudflare.com/ajax/libs/vis/4.16.1/vis.min.js”
script = document.createElement(‘script’)
script.src = url
script.async = false
script.onreadystatechange = script.onload = () => @_init()
document.querySelector(“head”).appendChild(script)
_init: () →
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.
Backbone Views create elements by default, accessible as @el. Many
Bokeh views ignore this default , and instead do things like draw
to the HTML canvas. In this case though, we use the to attach a
Graph3d to the DOM.
@_graph = new vis.Graph3d(@el, @get_data(), OPTIONS)
Set Backbone listener so that when the Bokeh data source has a change
event, we can process the new data
@connect(@model.data_source.change, () =>
@_graph.setData(@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: () →
data = new vis.DataSet()
source = @model.data_source
for i in [0…source.get_length()]
data.add({
x: source.get_column(@model.x)[i]
y: source.get_column(@model.y)[i]
z: source.get_column(@model.z)[i]
extra: source.get_column(@model.extra)[i]
style: source.get_column(@model.color)[i]
})
return data
We must also create a corresponding JavaScript Backbone model sublcass 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 class Surface3d extends LayoutDOM
This is usually boilerplate. In some cases there may not be a view.
default_view: Surface3dView
The type
class attribute should generally match exactly the name
of the corresponding Python class.
type: “Surface3d”
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 implementation. Where the JS type system is not yet
as rich, you can use p.Any
as a “wildcard” property type.
@define {
x: [ p.String ]
y: [ p.String ]
z: [ p.String ]
color: [ p.String ]
extra: [ p.Any ]
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 (or CoffeeScript) code that implements the JavaScript side
of the custom extension model.
implementation = JS_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:
bokeh.core — Bokeh 2.4.2 Documentation
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, z, and
color. 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
extra = String
color = String
X_data = np.random.normal(0,10,100)
Y_data = np.random.normal(0,5,100)
Z_data = np.random.normal(0,3,100)
color = np.asarray([0 for x in range(50)]+[1 for x in range(50)])
extra = np.asarray([‘a’ for x in range(50)]+[‘b’ for x in range(50)])
source = ColumnDataSource(data=dict(x=X_data, y=Y_data, z=Z_data, color = color, extra=extra))
surface = Surface3d(x=“x”, y=“y”, z=“z”, extra=“extra”, color=“color”, data_source=source)
show(surface)
``
So far my idealized output from the project should be:
-
Produce the correct tooltips, with the gene corresponding to the value. Currently, I’ve been able to show X, Y or Z coordinates, but the extra or style values show as undefined.
-
Complementarily, add in the tooltip the category to which the dot belongs (if 1. can be done, I’d have no problem doing this one).
-
Somehow, remove the colorbar (legend), which I won’t need. It doesn’t dissapear when putting the showLegend value to false (and I don’t know why, I’m really concerned).
Thank you in advance.