Hi,
Firstly, apologies for the length of the example code below - some of it can be ignored but it was simpler for me to include it all to get the point across. It’s similar to a previous example I posted - that for the topic on Inheritance - which wasn’t working: the colour bar range values should have updated when the slider was moved. This seems to be due to the ColumnDataSource converting numpy arrays to lists. If I convert them back to numpy arrays (i.e. the convertData class method) it works fine. Is this a bug or am I not using the ColumnDataSource correctly?
Additional (unrelated) question: as you can see I’m attempting to add a HoverTool with some custom JS (which simply dumps the 3D array values in one dimension at the cursor position to the browser console). If I add this inside the ColourMap class definition (not shown in the code) it works fine, but adding it afterwards as shown doesn’t work: if the relevant line (2nd from last) is uncommented the plot doesn’t render. Should it work? The reason for wanting to do it this way is that I can re-use the same ColourMap class and add specialised tools afterwards.
(By the way I am aware of the HeatMap class - I just need something particular for my applications - mainly the colourbar plotted underneath and use of numpy arrays).
Many thanks,
Marcus.
···
import numpy
from bokeh.plotting import Figure
from bokeh.models import ColumnDataSource, Plot, LinearAxis
from bokeh.models.mappers import LinearColorMapper
from bokeh.models.ranges import Range1d
from bokeh.models.widgets import Slider
from bokeh.models.widgets.layouts import Column
from bokeh.core.properties import Instance
from bokeh.palettes import RdYlBu11
from bokeh.io import curdoc
from bokeh.models.callbacks import CustomJS
from bokeh.models.tools import HoverTool
Define the class
class ColourMap(Column):
__view_model__ = "Column"
__subtype__ = "ColourMap"
plot = Instance(Plot)
cbar = Instance(Plot)
power = Instance(Slider)
datasrc = Instance(ColumnDataSource)
cbarrange = Instance(ColumnDataSource)
cmap = Instance(LinearColorMapper)
def __init__(self,x,y,D):
super(ColourMap,self).__init__()
self.power = Slider(title = 'Power',name = 'Power',start = 1,end = numslices,step = 1,
value = round(numslices/2))
self.power.on_change('value',self.inputchange)
d = D[self.power.value]
self.datasrc = ColumnDataSource(data={'x':x,'y':y,'d':[d],'D':D})
self.cmap = LinearColorMapper(palette = RdYlBu11)
r = Range1d(start = d.min(),end = d.max())
self.cbarrange = ColumnDataSource(data = {'range':[r]})
toolset = ['reset,pan,resize,wheel_zoom,box_zoom,save']
self.plot = Figure(title="Colourmap plot",x_axis_label = 'x',y_axis_label = 'y',
x_range = [x[0],x[-1]],y_range=[y[0],y[-1]],
plot_height = 500,plot_width = 500,tools = toolset)
dx = x[1] - x[0]
dy = y[1] - y[0]
self.plot.image('d',source = self.datasrc,x = x[0]-dx/2, y = y[0]-dy/2,
dw = [x[-1]-x[0]+dx],dh = [y[-1]-y[0]+dy],
color_mapper = self.cmap)
self.generate_colorbar()
self.children.append(self.power)
self.children.append(self.plot)
self.children.append(self.cbar)
def generate_colorbar(self,cbarlength = 500,cbarwidth = 50):
pal = RdYlBu11
minVal = self.datasrc.data['d'][0].min()
maxVal = self.datasrc.data['d'][0].max()
vals = numpy.linspace(minVal,maxVal,len(pal))
self.cbar = Figure(tools = "",x_range = [minVal,maxVal],y_range = [0,1],
plot_width = cbarlength,plot_height = cbarwidth)
self.cbar.toolbar_location = None
self.cbar.min_border_left = 10
self.cbar.min_border_right = 10
self.cbar.min_border_top = 0
self.cbar.min_border_bottom = 0
self.cbar.xaxis.visible = False
self.cbar.yaxis.visible = False
self.cbar.extra_x_ranges = {'xrange':self.cbarrange.data['range'][0]}
self.cbar.add_layout(LinearAxis(x_range_name = 'xrange'),'below')
for r in self.cbar.renderers:
if type(r).__name__ == 'Grid':
r.grid_line_color = None
self.cbar.rect(x = vals,y = 0.5,color = pal,width = vals[1]-vals[0],height = 1)
def updatez(self):
data = self.datasrc.data
newdata = data
d = data['d']
d[0] = data['D'][self.power.value - 1]
newdata['d'] = d
self.datasrc.trigger('data',data,newdata)
def updatecbar(self):
minVal = self.datasrc.data['d'][0].min()
maxVal = self.datasrc.data['d'][0].max()
self.cbarrange.data['range'][0].start = minVal
self.cbarrange.data['range'][0].end = maxVal
def inputchange(self,attrname,old,new):
self.convertData()
self.updatez()
self.updatecbar()
def convertData(self): # Are numpy arrays getting converted to lists in ColumnDataSource?
data = self.datasrc.data
newdata = data
for k in ['x','y','d','D']:
if (type(newdata[k]) is list):
newdata[k] = numpy.array(newdata[k])
Instantiate
numslices = 6
x = numpy.linspace(1,2,11)
y = numpy.linspace(2,4,21)
D = numpy.ndarray([numslices,y.size,x.size])
for i in range(numslices):
for j in range(y.size):
for k in range(x.size):
D[i,j,k] = (y[j]*x[k])**(i+1) + y[j]*x[k]
Add a custom hover tool
cm = ColourMap(x,y,D)
jsCode = ‘’’
var geom = cb_data[‘geometry’];
var imdata = imsrc.get(‘data’);
var hx = geom.x;
var hy = geom.y;
var x = imdata[‘x’];
var y = imdata[‘y’];
var dx = x[1] - x[0]
var dy = y[1] - y[0]
var xind = Math.floor((hx + dx/2 - x[0])/dx)
var yind = Math.floor((hy + dy/2 - y[0])/dy)
if ((xind < x.length) && (yind < y.length)) {
var D = imdata[‘D’]
console.log(‘Data at cursor…’)
for (i = 0; i < D.length; i++) {
console.log(D[i][yind][xind])
}
}
‘’’
cJS = CustomJS(args = {‘imsrc’:cm.datasrc},code = jsCode)
htool = HoverTool(callback = cJS)
#cm.plot.tools.append(htool) # If uncommented plot does not render
curdoc().add_root(cm)