11

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 one:

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:
#
#     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.

# 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'
  style: 'dot-color'
  showPerspective: true
  showGrid: true
  keepAspectRatio: true
  verticalRatio: 1.0
  showLegend: false
  cameraPosition:
    horizontal: -0.35
    vertical: 0.22
    distance: 1.8

  dotSizeRatio: 0.01

  tooltip: true    #(point) -> return 'value: <b>' + point.z + '</b><br>' + point.data.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 <div> elements by default, accessible as @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.
    @_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 implementatin. 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.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 (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:
    #
    #    https://docs.bokeh.org/en/latest/docs/reference/core.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, 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)

Given this, my idealized output from the project should be:

  1. Produce the correct tooltips, with the gene corresponding to the value.
  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.

Thank you in advance.

bigreddot
  • 33,642
  • 5
  • 69
  • 122

2 Answers2

9

So there are two small tweaks to make to acheive what you want.

I think the most important is the version of visjs being used.

Change the url to url = "https://visjs.github.io/vis-graph3d/standalone/umd/vis-graph3d.min.js"

Secondly, the function declaration for tooltip should be changed to :

tooltip: (point) -> return 'value: <b>' + point.z + '</b><br>' + 'extra: <b>' + point.data.extra

Not a coffeescript user, but that seems to correct the syntax for using the custom tooltip html.

Here is the updated example, if needed. (Note, this is for bokeh version 0.12.9)

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:
#
#     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.

# 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'
  style: 'dot-color'
  showPerspective: true
  showGrid: true
  keepAspectRatio: true
  verticalRatio: 1.0
  showLegend: false
  cameraPosition:
    horizontal: -0.35
    vertical: 0.22
    distance: 1.8
  dotSizeRatio: 0.01
  tooltip: (point) -> return 'value: <b>' + point.z + '</b><br>' + 'extra: <b>' + point.data.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://visjs.github.io/vis-graph3d/standalone/umd/vis-graph3d.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 <div> elements by default, accessible as @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.
    @_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 implementatin. 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.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 (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:
    #
    #    https://docs.bokeh.org/en/latest/docs/reference/core.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, 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)
sh37211
  • 1,411
  • 1
  • 17
  • 39
Anthonydouc
  • 3,334
  • 1
  • 16
  • 29
  • not a problem, I had the same issue with the colorbar a while ago - guess it just comes down to either a bug in the version being used in the example, or the feature wasn't implemented yet. – Anthonydouc Sep 24 '17 at 15:40
  • Thanks for this. Is there any way to get this to run in a Jupyter notebook? It spins up a separate tab in the browser and runs there. – ironv Mar 30 '18 at 18:12
  • I have a suspicion that it might not be supported in a notebook. If you followed the process outlined here : https://bokeh.pydata.org/en/latest/docs/user_guide/notebook.html, and it doesn't work try ask in the bokeh gitter chat room to get feedback from a dev. – Anthonydouc Mar 31 '18 at 10:27
  • I'm getting "SyntaxError: EOF while scanning triple-quoted string literal" error :( – Raksha Apr 16 '19 at 20:09
  • When I run this code (standalone, from bash) all I see is a blank screen in the browser window that opens. Console shows list of errors: "Uncaught TypeError: Cannot read property 'map' of undefined at Object.n.build_views (bokeh-1.3.4.min.js:31) at Surface3dView.e.build_child_views (bokeh-1.3.4.min.js:31) at Surface3dView.e.initialize (bokeh-1.3.4.min.js:31) at Surface3dView.exports.Surface3dView.Surface3dView.initialize (3d_scatter_bokeh.html:114) at Surface3dView.e [as constructor] (bokeh-1.3.4.min.js:31) at new Surface3dView (3d_scatter_bokeh.html:109) ..." – sh37211 Mar 27 '21 at 23:22
  • Thats almost certainly due to the differing versions of bokeh. At the time of writing my response it was 2017 and bokeh would have been around version 0.12.9. Perhaps try run with 0.12.9 or earlier, then port over if possible – Anthonydouc Mar 30 '21 at 06:55
  • Thanks @Anthonydouc. Installing bokeh 0.12.9, was helpful! One other necessary change is that the URL for vis.js needs to be updated because the old one 404's. Newer code should use `url = "https://visjs.github.io/vis-graph3d/standalone/umd/vis-graph3d.min.js"` With that, it works! Now...perhaps someone can tell us how to update this for newer versions of bokeh? – sh37211 Apr 04 '21 at 19:51
-1

Although not the best answer, a fully implemented Scatter in 3D can be now obtained using plotly.

import plotly.plotly as py
import plotly.graph_objs as go

import numpy as np

x, y, z = np.random.multivariate_normal(np.array([0,0,0]), np.eye(3), 200).transpose()
trace1 = go.Scatter3d(
    x=x,
    y=y,
    z=z,
    mode='markers',
    marker=dict(
        size=12,
        line=dict(
            color='rgba(217, 217, 217, 0.14)',
            width=0.5
        ),
        opacity=0.8
    )
)

x2, y2, z2 = np.random.multivariate_normal(np.array([0,0,0]), np.eye(3), 200).transpose()
trace2 = go.Scatter3d(
    x=x2,
    y=y2,
    z=z2,
    mode='markers',
    marker=dict(
        color='rgb(127, 127, 127)',
        size=12,
        symbol='circle',
        line=dict(
            color='rgb(204, 204, 204)',
            width=1
        ),
        opacity=0.9
    )
)
data = [trace1, trace2]
layout = go.Layout(
    margin=dict(
        l=0,
        r=0,
        b=0,
        t=0
    )
)
fig = go.Figure(data=data, layout=layout)
py.plot(fig, filename='simple-3d-scatter')

The code will produce an html file, and allows full personalization.

  • Thanks for this suggestion, indeed it's not bokeh, but it sometimes usefull to discover new tools which can to what is requested ! – Louc Oct 18 '20 at 09:39
  • Error message: "The plotly.plotly module is deprecated, please install the chart-studio package and use the chart_studio.plotly module instead." – Hubert Schölnast Dec 26 '20 at 18:07