Tool code migration from Bokeh 0.12.4

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.

  1. 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.

  1. 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 ] }

``

Hi,

Re: 1) the CDS has only ever been intended to map names to lists/arrays. If it ever functioned in some way with other structures (i.e. dicts) that was entirely unintentional, unsupported, and undocumented. Chances are this requirement became more concrete when the fast binary array protocol was introduced in 0.12.9

For 2) things were streamlined/simplified prior to 1.0. Specifically, the notion of "view" coordinates which turned out to be superfluous and complicating for no benefit was removed. Now, if you need to map two screen space, there is just "map_to_screen" on the canvas:

  https://github.com/bokeh/bokeh/blob/master/bokehjs/src/lib/models/plots/plot_canvas.ts#L596

It optionally accepts the names of extra ranges, if you don't want to use the default ranges for mapping. If you want to map *from* the screen, you'll have to call "v_invert" on the Scale objects directly, you can see an example of that here
  
  https://github.com/bokeh/bokeh/blob/master/bokehjs/src/lib/models/tools/gestures/lasso_select_tool.ts#L74-L80

Please also note that the next release will deprecate support for Coffeescript in Bokeh, and it will be removed completely in a 2.0 release at the end of the year. At this point, CoffeeScript is entirely a dead-end, and we simply don't have the resources to continue supporting it. We will support only pure JS or TypeScript implementations directly (though I suppose it will still be possible to compile CoffeeScript yourself by hand using the coffee compiler manually, if you really want).

Thanks,

Bryan

···

On Mar 11, 2019, at 9:17 AM, emile.vangerwen via Bokeh Discussion - Public <[email protected]> wrote:

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.

1. 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.

2. 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 += "<hr/>"
            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 ] }

--
You received this message because you are subscribed to the Google Groups "Bokeh Discussion - Public" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit https://groups.google.com/a/continuum.io/d/msgid/bokeh/c92cece2-50d9-4a45-9bf6-df8f0b385ec3%40continuum.io\.
For more options, visit https://groups.google.com/a/continuum.io/d/optout\.

Thanks a lot Bryan for your quick reply, and for the heads-up on the coffee script. It is working now (and reimplemented in javascript).

Regards,

-Emile.

···

On Monday, March 11, 2019 at 5:53:56 PM UTC+1, Bryan Van de ven wrote:

Hi,

Re: 1) the CDS has only ever been intended to map names to lists/arrays. If it ever functioned in some way with other structures (i.e. dicts) that was entirely unintentional, unsupported, and undocumented. Chances are this requirement became more concrete when the fast binary array protocol was introduced in 0.12.9

For 2) things were streamlined/simplified prior to 1.0. Specifically, the notion of “view” coordinates which turned out to be superfluous and complicating for no benefit was removed. Now, if you need to map two screen space, there is just “map_to_screen” on the canvas:

    [https://github.com/bokeh/bokeh/blob/master/bokehjs/src/lib/models/plots/plot_canvas.ts#L596](https://github.com/bokeh/bokeh/blob/master/bokehjs/src/lib/models/plots/plot_canvas.ts#L596)

It optionally accepts the names of extra ranges, if you don’t want to use the default ranges for mapping. If you want to map from the screen, you’ll have to call “v_invert” on the Scale objects directly, you can see an example of that here

    [https://github.com/bokeh/bokeh/blob/master/bokehjs/src/lib/models/tools/gestures/lasso_select_tool.ts#L74-L80](https://github.com/bokeh/bokeh/blob/master/bokehjs/src/lib/models/tools/gestures/lasso_select_tool.ts#L74-L80)

Please also note that the next release will deprecate support for Coffeescript in Bokeh, and it will be removed completely in a 2.0 release at the end of the year. At this point, CoffeeScript is entirely a dead-end, and we simply don’t have the resources to continue supporting it. We will support only pure JS or TypeScript implementations directly (though I suppose it will still be possible to compile CoffeeScript yourself by hand using the coffee compiler manually, if you really want).

Thanks,

Bryan

On Mar 11, 2019, at 9:17 AM, emile.vangerwen via Bokeh Discussion - Public [email protected] wrote:

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.

  1. 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.

  1. 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](http://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](http://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](http://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 += "<hr/>"
        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 ] }


You received this message because you are subscribed to the Google Groups “Bokeh Discussion - Public” group.

To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].

To post to this group, send email to [email protected].

To view this discussion on the web visit https://groups.google.com/a/continuum.io/d/msgid/bokeh/c92cece2-50d9-4a45-9bf6-df8f0b385ec3%40continuum.io.

For more options, visit https://groups.google.com/a/continuum.io/d/optout.