Extending Bokeh with JS library for heatmap

Hello,

This is a post redirected from https://github.com/bokeh/bokeh/issues/7377 to this forum.

I’m trying to extend Bokeh by wrapping a JavaScript library for heatmaps (https://github.com/pa7/heatmap.js).
Based on the documentation, I’ve created a CoffeeScript in a separate file named bokeh_extension_heatmap.coffee with the following contents:

import * as p from "core/properties"
import {LayoutDOM, LayoutDOMView} from "models/layouts/layout_dom"

export class HeatmapGeoView extends LayoutDOMView

    initialize: (options) ->
      super(options)

      url = "https://cdnjs.cloudflare.com/ajax/libs/heatmap.js/2.0.2/heatmap.js"

      script = document.createElement('script')
      script.src = url
      script.async = false
      script.onreadystatechange = script.onload = () => @_init()
      document.querySelector("head").appendChild(script)

    _init: () ->
      @_graph = h337.create({container: @el, radius: 10, blur: .75, maxOpacity: .5, minOpacity: 0});
      @_graph.setData({ max: 10, data: @populate_data()})

      @connect(@model.data_source.change, () =>
        @_graph.setData({ max: 10, data: @populate_data()})
        @_graph.repaint()

      )

    populate_data: () ->
      list_points = []
      source = @model.data_source
      for i in [0...source.get_length()]
        list_points.push({
          x:     source.get_column(@model.x_m)[i]
          y:     source.get_column(@model.y_m)[i]
          value: source.get_column(@model.val)[i]
        })

      return list_points

export class HeatmapGeo extends LayoutDOM

    #the view defined above
    default_view: HeatmapGeoView

    #the current wrapping Python class name
    type: "HeatmapGeo"

    #The @define block adds corresponding "properties" to the JS model. These
    # should basically line up 1-1 with the Python model class.
    @define {
        x_m:  [ p.String           ]
        y_m:  [ p.String           ]
        val:  [ p.String           ]
        data_source: [ p.Instance  ]
      }

Then, the Python model (in file bokeh_extension_heatmap.py):

from bokeh.core.properties import Instance, String
from bokeh.models import ColumnDataSource, LayoutDOM
from bokeh.util.compiler import FromFile

class HeatmapGeo(LayoutDOM):
    __javascript__ = "https://cdnjs.cloudflare.com/ajax/libs/heatmap.js/2.0.2/heatmap.js"
    __implementation__ = FromFile('bokeh_extension_heatmap.coffee')

    # This is a Bokeh ColumnDataSource that can be updated in the Bokeh
    # server by Python code
    #the names should be the same as in HeatmapGeo class (in the coffeescript script)
    data_source = Instance(ColumnDataSource)

    # strings representing names of useful columns
    x_m = String
    y_m = String
    val = String

I want to be able to add an HeatmapGeo instance on top of a Bokeh figure, but here I’m not sure which approach to implement.
The goal is to be able to add this heatmap layer on top of a map (Bokeh figure with tileset from some Web Map Tile Service).

import bokeh.tile_providers as tp
from bokeh.io import (
    curdoc
)
from bokeh.plotting import (
    figure
)
from bokeh.models import (
    ColumnDataSource,
    Range1d
)
import logging

from bokeh_extension_heatmap import HeatmapGeo

import numpy as np

#----------------------- logging for debug
logging.basicConfig(level=logging.DEBUG)

x = np.arange(11529166.0546, 11539166.0546, 1000)
y = np.arange(  125839.385417, 135838.385417, 1000)
values = np.ones(x.shape[0])

source = ColumnDataSource(data=dict(x=x, y=y, values=values))

x_range = Range1d(11529166.0546, 11588007.5559)
y_range = Range1d(125839.385417, 164456.343591)

WHITE = "#FFFFFF"
PLOT_FORMATS = dict(
        toolbar_location='right',
        outline_line_color=WHITE,
        title_location='above',
        tools="tap,box_zoom,pan,wheel_zoom,reset"
    )

my_map = figure(x_range=x_range, y_range=y_range, title="Plot",
                    plot_width=600, plot_height=600, output_backend="webgl", **PLOT_FORMATS)
my_map.axis.visible = False
#add the desired tileset
my_map.add_tile(tp.STAMEN_TONER)

myheatmap = HeatmapGeo(x_m="x", y_m="y", val="values", data_source=source)

##### NEXT COMES THE ISSUE:
my_map.add_layout(obj=myheatmap,place='center')
#ValueError: expected an element of List(Instance(Renderer)), got seq with invalid items
#  [HeatmapGeo(id='5ee18c4f-214f-4c56-94ce-b98636a82d72', ...)]
curdoc().add_root(my_map)

#MOREOVER, if instead of the last 2 lines of code, I add the custom layout as root directly
#then I get another error
#curdoc().add_root(myheatmap)

The referred screenshot is shown at https://github.com/bokeh/bokeh/issues/7377

The Bokeh app is served by Bokeh server.

When this first part will get working, I’ll have to address the task of synchronizing the displayed heatmap with the current map view (e.g. on box_zoom, wheel_zoom, pan). Any suggestions on that topic would be very helpful, too.

Thank you in advance for your advice!
Camelia

···

Hi Camelia,

In case of interest, I thought I’d point out that Bokeh has an image plot and ColorBar class which allows the sort of thing you’re talking about to be achieved. At the risk of promoting my own stuff :slight_smile: I’ve put together some classes which you might find useful - see here or install via pip (pip install bokcolmaps). I don’t know how it compares with heatmap.js in terms of functionality, but one difference is that you’ll already be in Bokeh data space, so you shouldn’t need to worry about synchronizing the displayed heatmap with the current map view. However, a current limitation is that it doesn’t seem possible to vary the alpha value for images at the moment, I don’t know how important that is for you.

Regards,
Marcus.