Vis.js graph3d dot-color hex-colors always black

i need to display a 3d scatter plot in bokeh for which each dot is a different color as specified by a hex string. starting with the bokeh example on how to wrap javascript libraries, i modify two lines as shown in an example in the vis.js docs to arrive at the code below. specifically:

(1) style: 'surface' is now style: 'dot-color', and

(2) style: i % 2 == 0 ? source.get(this.model.z)[i] : "#ff00ff" has been added to get_data()

and instead of half the dots being fuchsia, they are black. am i doing something wrong, or should i file a github issue?

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

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 * as p from "core/properties"

declare namespace vis {
  class Graph3d {
    constructor(el: HTMLElement | DocumentFragment, 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: 'dot-color',
  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 {
  declare 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.shadow_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.get(this.model.x)[i],
        y: source.get(this.model.y)[i],
        z: source.get(this.model.z)[i],
        style: i % 2 == 0 ? source.get(this.model.z)[i] : "#ff00ff",
      })
    }
    return data
  }

  get child_models(): LayoutDOM[] {
    return []
  }
}

// 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 {
  declare properties: Surface3d.Props
  declare __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>(({Str, Ref}) => ({
      x:            [ Str ],
      y:            [ Str ],
      z:            [ Str ],
      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(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)

@arthurb You definitely should not file a Bokeh issue, because vis.js is a wholly separate project that is just what we coindicentally used for one example. If there is a bug or usage issue with vis.js, that’s not really something we can help with directly. Just to be completely clear: Bokeh itself does not have any supported 3-d plotting capability.

It seems like the style value you have added is not in a format that vis.js like or expects. I haven’t looked at vis.js in the better part of a decade. All I can suggest is to check the browser JS console in case vis.js is outputing any helpful errors or warnings.

Well I guess I can at least note by inspection that the two values in this:

this.model.z)[i] : "#ff00ff"

are completely different. In one case its an RGBA string and in antother case its an floating point number (the z-coordinate). You’ll need to figure out what vis.js expects, and make sure that you pass that kind of value. But that’s a vis.js question, not a Bokeh one.

i understand that vis.js is a wholly separate project. but if i use solely vis.js it renders correctly. only when done through bokeh is it messed up.

see line 25 and the corresponding output of the vis.js example here: Graph 3D | Cloud with Colored Dots

are you sure this couldn’t be a bokeh bug?

vis.js is the only thing doing any rendering here. The fact that it is rendering at all means that it is getting the values you are computing and passing to it. In situations like this the usual tactic is to use the debugger or add console.log statements to confirm exactly what values are being passed. The usual result is discovering you are not computing what you think you are computing. [1]


  1. alternatively: passing floating point values in this context seems like nonsense to me, since floating point values are not typically ever interpreted as RGBA color values. Perhaps this is undefined behavior that presents differently with different versions of vis.js. ↩︎

are you sure this couldn’t be a bokeh bug?

It’s also worth noting: if there were somehow some problem with Bokeh transmitting the z coordinate data correctly, then the positions of the dots would also have to be wrong. But the positions aren’t wrong, so the z-coordinate data must be arriving correctly (and that’s where Bokeh’s responsibility ends in this case).