Custom models disappear after modifying layout

Hi there,

I’ve been tinkering around with wrapping js-libraries and so far it worked great.
However, now that I embedded them into my tool, I’m starting to get problems with dissappearing elements.

In this example I used the Surface3d-wrapper example.
I am adding a new, empty Div on button-click; I would expect everything to look the same, but the surface3d-DOM disappears…

I looked through the bokehjs source a bit and am now thinking that I should maybe subclass widget instead of LayoutDOM? I thought I could use the Surface3d example and adapt it for my own needs easily.

Edit: On further investigation I found that, obviously, Surface3dView’s initialize does not fire, when the layout is updated (only on page-load). How should I modify the code to make it work?

Thank you in advance

# SURFACE-3D CODE
from bokeh.core.properties import Instance, String
from bokeh.models import LayoutDOM, ColumnDataSource
from bokeh.util.compiler import TypeScript
TS_CODE = """
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 vis {
  class Graph3d {
    constructor(el: HTMLElement, data: object, OPTIONS: object)
    setData(data: vis.DataSet): void
  }

  class DataSet {
    add(data: unknown): void
  }
}

const OPTIONS = {
  width: '600px',
  height: '600px',
  style: 'surface',
  showPerspective: true,
  showGrid: true,
  keepAspectRatio: true,
  verticalRatio: 1.0,
  legendLabel: 'stuff',
  cameraPosition: {
    horizontal: -0.35,
    vertical: 0.22,
    distance: 1.8,
  },
}
export class Surface3dView extends LayoutDOMView {
  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 {
    this._graph = new vis.Graph3d(this.el, this.get_data(), OPTIONS)
    this.connect(this.model.data_source.change, () => {
      this._graph.setData(this.get_data())
    })
  }

  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.data[this.model.x][i],
        y: source.data[this.model.y][i],
        z: source.data[this.model.z][i],
      })
    }
    return data
  }

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

  _update_layout(): void {
    this.layout = new LayoutItem()
    this.layout.set_sizing(this.box_sizing())
  }
}
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
  __view_type__: Surface3dView

  constructor(attrs?: Partial<Surface3d.Attrs>) {
    super(attrs)
  }
  static __name__ = "Surface3d"

  static init_Surface3d() {
    this.prototype.default_view = Surface3dView
    this.define<Surface3d.Props>(({String, Ref}) => ({
      x:            [ String ],
      y:            [ String ],
      z:            [ String ],
      data_source:  [ Ref(ColumnDataSource) ],
    }))
  }
}
"""
class Surface3d(LayoutDOM):

    __implementation__ = TypeScript(TS_CODE)
    data_source = Instance(ColumnDataSource)
    x = String
    y = String
    z = String

#########################################
############## Actual MRE ###############
#########################################

from bokeh.events import ButtonClick
from bokeh.io import curdoc
from bokeh.layouts import row
from bokeh.models import Div, Button, ColumnDataSource
import numpy as np

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)


button = Button()
def callback(event):
    layout.children.append(Div())
    print(layout.children)
button.on_event(ButtonClick, callback)

layout = row(surface, button)

curdoc().add_root(layout)

I solved this by simply switching to a widget, I don’t think LayoutDom's were made with my use-case in mind.

Maybe this helps someone in the future. Simply look how the FileInput-widget was coded in bokehjs and make the Surface3d example look like that.