@Stefeni_Butterworth A bit late as you have specified a solution. But I just wanted to give you some comments to what you can do.
In the callback for your TapTool
you can check whether an actual index of the CDS has been inspected
const idx = source.inspected.line_indices;
Hence, if length is 0, the user has not clicked on the line glyph, but just some arbitrary point in the figure and no crosshair is shown (with the Spans). If there is an index value, it is always the first index of what defines the current segment of the line. So, one can use that to figure out which data point is nearest.
In the code below I use tags
argument of TapTool
to store the index of the snapped index. I use this to figure out if the HoverTool
should move the crosshair when moving the mouse along the line - if empty array in tags
then I assume no crosshair is shown. As you have specified mode = "mouse"
in HoverTool
one needs to keep the mouse on the line glyph in order to move the crosshair.
In the HoverTool
callback I use the same kind of approach as for the TapTool
to figure out which index is closest. However, for the hover one needs to use
const idx = cb_data.index.line_indices;
in order to get the index. Again, the index is always the first index of the particular segment of the line.
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models.tools import HoverTool, TapTool, TapTool
from bokeh.models import Span
from bokeh.models.callbacks import CustomJS
from bokeh.io import show, save
data= {'x':list(range(0,15)),
'y': [4, 4, 4, 4, 15, 13, 4, 4, 4, -10, 0, 4, 4, 4, 4]}
source = ColumnDataSource(data)
p = figure(
height = 400,
width = 500
)
line = p.line(
x = 'x',
y = 'y',
source = source
)
# just added circles to better see the data points
p.scatter(
x = 'x',
y = 'y',
line_width = 2,
fill_color = 'white',
size = 8,
source= source
)
span_width = Span(line_color = 'red', dimension = 'width')
span_height = Span(line_color = 'red', dimension = 'height')
p.add_layout(span_width)
p.add_layout(span_height)
hover = HoverTool(
tooltips = [("x", "@x"), ("y", "@y")],
point_policy = 'snap_to_data',
mode = 'mouse',
line_policy = 'nearest',
renderers = [line]
)
tap = TapTool()
p.add_tools(hover, tap)
tap_cb = CustomJS(
args = dict(
source = source,
tap = tap,
span_width = span_width,
span_height = span_height
),
code="""
const idx = source.inspected.line_indices;
var snap_x = NaN;
var snap_y = NaN;
const data_x = source.data.x;
const tags = [];
if (idx.length > 0) {
const data_dx = data_x[idx[0]+1]-data_x[idx[0]];
const dx = cb_obj.x-data_x[idx[0]];
var snap_idx = idx[0];
if (dx/data_dx > 0.5) {
snap_idx = idx[0]+1;
}
snap_x = data_x[snap_idx];
snap_y = source.data.y[snap_idx];
tags.push(snap_idx);
}
span_width.location = snap_y;
span_height.location = snap_x;
tap.tags = tags;
""")
p.js_on_event('tap', tap_cb)
hover_cb = CustomJS(
args = dict(
source = source,
tap = tap,
span_width = span_width,
span_height = span_height
),
code="""
const data_x = source.data.x;
if (tap.tags.length > 0) {
const idx = cb_data.index.line_indices;
if (idx.length > 0) {
const data_dx = data_x[idx[0]+1]-data_x[idx[0]];
const dx = cb_data.geometry.x-data_x[idx[0]];
var snap_idx = idx[0];
if (dx/data_dx > 0.5) {
snap_idx = idx[0]+1;
}
span_height.location = data_x[snap_idx];
span_width.location = source.data.y[snap_idx];
}
}
""")
hover.callback = hover_cb
save(p)