Hi,
I am reviving a 2016 project built with Bokeh 0.12.4 and run into breaking changes with (current) version 1.0.4 and even 0.12.14.
The project adds an “AnnotationHoverTool” that shows tooltips over BoxAnnotations and Spans.
Full code code at the end.
Debugging the resulting Java code I encounter 2 breaking changes.
- Adding a dictionary to the ColumnDataSource.data to transfer data to the coffee script gives a null reference. It seems it only handles arrays now?
The source object is set to a dictionary with a key ‘annotations’ which is a list of dictionaries. See Python code below.
-
The logic to convert coordinates seems to have gone though a rewrite. This code:
# vx/vy is the position in pixels vx = canvas.sx_to_vx(e.bokeh.sx) vy = canvas.sy_to_vy(e.bokeh.sy) # x/y is the position based on the units from the plot x = frame.x_mappers['default'].map_from_target(vx) y = frame.y_mappers['default'].map_from_target(vy)
``
gives a lot of errors. See full Coffee script below.
I would appreciate help or examples to upgrade my project to continue development using the latest Bokeh version.
Thanks!
-Emile.
Python code:
from bokeh.core.properties import Instance
from bokeh.models import Tool, ColumnDataSource, BoxAnnotation, Span
import os
class AnnotationHoverTool(Tool):
def init(self):
super(Tool, self).init()
self.source = ColumnDataSource(data=dict(annotations=))
def addAnnotation(self, annotation, label):
“”"
Add an annotation to hover on.
The text in label will be displayed in the tooltip
“”"
annotationDict = self._annotationToDict(annotation, label)
self.source.data.get(‘annotations’).append(annotationDict)
def _annotationToDict(self, annotation, label):
“”"
This converts an annotation object to a dictionary for use in the
coffee script code.
‘label’, ‘type’ and ‘priority’ are always in the dictionary.
Depending on the type also other values will be added to the dictionary
Currently this supports the BoxAnnotation and Span
“”"
annotationDict = dict(label=label, type=annotation.class.name)
if isinstance(annotation, BoxAnnotation):
annotationDict[‘right’] = annotation.right
annotationDict[‘left’] = annotation.left
annotationDict[‘priority’] = annotation.right - annotation.left
elif isinstance(annotation, Span):
annotationDict[‘location’] = annotation.location
annotationDict[‘priority’] = annotation.line_width
else:
raise Exception(‘Unsupported class %s’ %
(annotation.class.name))
return annotationDict
def _readCoffeeImplementation():
“”"
Private function to read the coffee script implementation of the
AnnotationHoverTool
“”"
location = os.path.realpath(os.path.join(os.getcwd(),
os.path.dirname(file)))
content = open(os.path.join(location,
“annotationhovertool.coffee”)).read()
return content
source = Instance(ColumnDataSource)
implementation = _readCoffeeImplementation()
``
Coffee script code:
import {HoverTool, HoverToolView} from “models/tools/inspectors/hover_tool”
import * as p from “core/properties”
export class AnnotationHoverToolView extends HoverToolView
_move: (e) →
tooltip = document.getElementsByClassName(“bk-tooltip”)[0]
if not @model.active
tooltip.style.display = “none”
return
frame = @plot_model.frame
canvas = @plot_view.canvas
# vx/vy is the position in pixels
vx = canvas.sx_to_vx(e.bokeh.sx)
vy = canvas.sy_to_vy(e.bokeh.sy)
# x/y is the position based on the units from the plot
x = frame.x_mappers[‘default’].map_from_target(vx)
y = frame.y_mappers[‘default’].map_from_target(vy)
# Hide tooltip if out of bounds
if y < 0
tooltip.style.display = “none”
return
# Clear tooltip text
tooltip.innerHTML = “”
# Find intersecting annotations
intersectingAnnotations = this.findIntersectingAnnotations(x, vx, frame)
if intersectingAnnotations.length > 0
this.showAnnotationsInTooltip(tooltip, e.bokeh, intersectingAnnotations)
else
tooltip.style.display = “none”
removeDuplicates = (stringArray) →
if stringArray.length == 0
return
unique = {};
unique[stringArray[key]] = stringArray[key] for key in [0…stringArray.length-1]
value for key, value of unique
showAnnotationsInTooltip: (tooltip, location, annotations) →
tooltip.style.display = “”
tooltip.style.left = location.sx + “px”
tooltip.style.top = location.sy + “px”
# Sort the annotation based on priority.
# Lower priority values will be shown on top in the tooltip
annotations = annotations.sort ((a, b) → return a.priority - b.priority)
# Don’t show annotations with the same label twice
labels = removeDuplicates(annotation.label for annotation in annotations)
for i in [0 … labels.length]
if i > 0 then tooltip.innerHTML += “
”
tooltip.innerHTML += labels[i]
findIntersectingAnnotations: (x, vx, frame) ->
intersectingAnnotations = []
# Find intersecting annotations
for annotation in @model.source.data.annotations
if annotation.type == 'BoxAnnotation'
if this.isWithin(x, annotation.left, annotation.right)
intersectingAnnotations.push annotation
else if annotation.type == 'Span'
box = this.spanAnnotationCalculateBox(frame, annotation)
if this.isWithin(vx, box.left, box.right)
intersectingAnnotations.push annotation
return intersectingAnnotations
spanAnnotationCalculateBox: (frame, annotation) ->
# The Span annotation is a line and therefore has no box
# to intersect with.
# Convert location (in unit from plot) to pixels
px = frame.x_mappers['default'].map_to_target(annotation.location)
# Create a box to intersect with
hitboxSizePx = 5
return {left: px - hitboxSizePx, right: px + hitboxSizePx}
isWithin: (x, left, right) ->
return (x >= left && x <= right)
export class AnnotationHoverTool extends HoverTool
default_view: AnnotationHoverToolView
type: “AnnotationHoverTool”
tool_name: “Annotation Hover”
icon: “bk-tool-icon-hover”
@define { source: [ p.Instance ] }
``