Hi there,
I am attempting to link a scatter and time series plot so I can mouse over a point in the scatter plot and highlight the corresponding points in the time series plot. This does work for my second series (orange), but not for the blue series. Steps to reproduce:
- Run code below.
- Mouseover any orange point on the left hand scatter plot. Observe in the console that the circles have been updated with the data. Press F8 (Chrome) to continue and see two black rings appear in the right hand time series chart.
- Mouseover any blue point on the left hand scatter plot. Observe in the console that the circles have been updated with the data. Press F8 (Chrome) to continue and this time no black rings appear on the right hand chart. Why? It seems as if the data gets overwritten because if you remove the “debugger;” line from the callback code (or inspect the circles data outside of the callback) the circles data is empty when you mouseover a blue point.
Any help would be appreciated. Thanks!
# Imports
import numpy as np
import pandas as pd
# Plotting
from bokeh.palettes import Category20 as colours
from bokeh.models import ColumnDataSource, Legend, HoverTool, CustomJS, DatetimeTickFormatter
from bokeh.plotting import figure, show
from bokeh.layouts import row
# Make data frame of test data
df = pd.DataFrame(data = {'device': ['abc'] * 3 + ['def'] * 5, 'timestamp': [f'2017-01-01 {i:02.0f}:00:00' for i in range(0, 8)], 'value_x': 100 * np.random.random(size = (8,)), 'value_y': 100 * np.random.random(size = (8,))})
df.timestamp = pd.to_datetime(df.timestamp, format = '%Y-%m-%d %H:%M:%S')
# Shuffle the colours
num_colours = 20
colours = [colours[num_colours][(2 * i) + (0 if (2 * i) < num_colours else (-num_colours + 1))] for i in range(0, num_colours)]
# Get unique devices
unique_devices = df.device.unique()
# %% Time series plot
# Create figure object
p_ts = figure(
title = 'Time series plot',
plot_width = 1200,
plot_height = 800,
x_axis_type = 'datetime',
toolbar_location = None
)
# Add each scatter to chart keeping track of items for legend
legend_items = []
for i, device in enumerate(unique_devices):
# Get mask
mask = df.device == device
for col_name, alpha in [('value_x', 1.0), ('value_y', 0.6)]:
# Create source dictionary
d_source = {
'x': df.loc[mask, 'timestamp'].values,
'y': df.loc[mask, col_name].values,
'device': df.loc[mask, 'device'].values,
'timestamp': df.loc[mask, 'timestamp'].values
}
# Build data source
source = ColumnDataSource(d_source)
# Create dictionary for passing to circle plot
d_line = {'x': 'x', 'y': 'y', 'line_width': 3, 'color': colours[i], 'source': source, 'alpha': alpha, 'legend_label': f'{device} {col_name}'}
# Plot results
l = p_ts.line(**d_line)
# Add to legend
legend_items.append((device, [l]))
# Now create an empty data source highlighted points
source = ColumnDataSource({'x': [], 'y': []})
d_circles = {'x': 'x', 'y': 'y', 'size': 8, 'line_color': 'black', 'fill_color': None, 'line_width': 2, 'source': source}
highlight_circles = p_ts.circle(**d_circles)
# Add y axis label
p_ts.yaxis.axis_label = 'Value (units)'
# Format x axis
p_ts.xaxis.formatter = DatetimeTickFormatter(
hours = ['%Y-%m-%d %H:%M'],
days = ['%Y-%m-%d %H:%M'],
months = ['%Y-%m-%d'],
years = ['%Y-%m-%d'])
# %% Scatter plot
# Create figure object
p_scatter = figure(
title = 'Scatter Plot',
plot_width = 600,
plot_height = 800,
toolbar_location = None
)
# Add each scatter to chart keeping track of items for legend
legend_items = []
data_sources = []
for i, device in enumerate(unique_devices):
# Get mask
mask = df.device == device
# Create source dictionary
d_source = {
'x': df.loc[mask, 'value_x'].values,
'y': df.loc[mask, 'value_y'].values,
'device': df.loc[mask, 'device'].values,
'timestamp': df.loc[mask, 'timestamp'].values
}
# Build data source
source = ColumnDataSource(d_source)
# Create dictionary for passing to circle plot
d_circle = {'x': 'x', 'y': 'y', 'size': 5, 'color': colours[i], 'source': source}
# Plot results
c = p_scatter.circle(**d_circle)
# Add to legend
legend_items.append((device, [c]))
# Add to data sources
data_sources.append(c)
# Set up axis labels
p_scatter.xaxis.axis_label = 'Value for X'
p_scatter.yaxis.axis_label = 'Value for Y'
# Move legend
legend = Legend(items = legend_items, location = (0, 0))
p_scatter.add_layout(legend, 'above')
# Build Javascript callback
code = """
const indices = cb_data.index.indices;
const data = {'x': [], 'y': []};
for (var i = 0; i < indices.length; i++) {
var idx = indices[i];
var ts = cb_data.renderer.data_source.data.timestamp[idx];
var x = cb_data.renderer.data_source.data.x[idx];
var y = cb_data.renderer.data_source.data.y[idx];
data['x'].push(ts);
data['x'].push(ts);
data['y'].push(x);
data['y'].push(y);
console.log("Timestamp is " + ts);
console.log("X is " + x);
console.log("Y is " + y);
console.log({
"circles": circles,
"obj": cb_obj,
"data": cb_data
});
console.log(data);
}
circles.data = data;
//circles.change.emit();
if (indices.length > 0) {
console.log(circles);
debugger;
}
"""
callback = CustomJS(args = {'circles': highlight_circles.data_source}, code = code)
# Add hover tool with callback
ht = HoverTool(
show_arrow = False,
line_policy = 'next',
tooltips = [('Device', '@device'), ('Timestamp', '@timestamp{%Y-%m-%d %H:%M:%S}'), ('Value of X', '@x'), ('Value of Y', '@y')],
formatters = {'@timestamp': 'datetime'},
callback = callback)
# Add hovertool to chart
p_scatter.add_tools(ht)
# %% Plot chart
show(row(p_scatter, p_ts))