Issues with 3D scatter plotting

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:

  1. 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.

  2. 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).

  3. 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.

1 Like