Okay, after digging more deeply into docs and examples, I'll probably answer this question by myself.
The hover effect on image (2d signal) data makes no sense in the way how this functionality is designed in Bokeh. If one needs to add some extra information attached to the data point it needs to put the data into the proper data model - the flat one.
tidying the data
Basically, one needs to tidy his data into a tabular format with x,y and value columns (see Tidy Data article by H.Wickham). Now every row represents a data point, and one can naturally add any contextual information as additional columns.
For example, the following code will do the work:
def flatten(matrix: np.ndarray,
extent: Optional[Tuple[float, float, float, float]] = None,
round_digits: Optional[int] = 0) -> pd.DataFrame:
if extent is None:
extent = (0, matrix.shape[1], 0, matrix.shape[0])
x_min, x_max, y_min, y_max = extent
df = pd.DataFrame(data=matrix)\
.stack()\
.reset_index()\
.rename(columns={'level_0': 'y', 'level_1': 'x', 0: 'value'})
df.x = df.x / df.x.max() * (x_max - x_min) + x_min
df.y = df.y / df.y.max() * (y_max - y_min) + y_min
if round_digits is not None:
df = df.round({'x': round_digits, 'y': round_digits})
return df
rect
glyph and ColumnDataSource
Then, use rect
glyph instead of image
with x,y mapped accordingly and the value column color-mapped properly to the color aesthetics of the glyph.
color mapping for values
- here you can use a min-max normalization with the following multiplication by the number of colors you want to use and the
round
- use bokeh builtin palettes to map from computed integer value to a particular color value.
With all being said, here's an example chart function:
def InteractiveImage(img: pd.DataFrame,
x: str,
y: str,
value: str,
width: Optional[int] = None,
height: Optional[int] = None,
color_pallete: Optional[List[str]] = None,
tooltips: Optional[List[Tuple[str]]] = None) -> Figure:
"""
Notes
-----
both x and y should be sampled with a constant rate
Parameters
----------
img
x
Column name to map on x axis coordinates
y
Column name to map on y axis coordinates
value
Column name to map color on
width
Image width
height
Image height
color_pallete
Optional. Color map to use for values
tooltips
Optional.
Returns
-------
bokeh figure
"""
if tooltips is None:
tooltips = [
(value, '@' + value),
(x, '@' + x),
(y, '@' + y)
]
if color_pallete is None:
color_pallete = bokeh.palettes.viridis(50)
x_min, x_max = img[x].min(), img[x].max()
y_min, y_max = img[y].min(), img[y].max()
if width is None:
width = 500 if height is None else int(round((x_max - x_min) / (y_max - y_min) * height))
if height is None:
height = int(round((y_max - y_min) / (x_max - x_min) * width))
img['color'] = (img[value] - img[value].min()) / (img[value].max() - img[value].min()) * (len(color_pallete) - 1)
img['color'] = img['color'].round().map(lambda x: color_pallete[int(x)])
source = ColumnDataSource(data={col: img[col] for col in img.columns})
fig = figure(width=width,
height=height,
x_range=(x_min, x_max),
y_range=(y_min, y_max),
tools='pan,wheel_zoom,box_zoom,reset,hover,save')
def sampling_period(values: pd.Series) -> float:
# @TODO think about more clever way
return next(filter(lambda x: not pd.isnull(x) and 0 < x, values.diff().round(2).unique()))
x_unit = sampling_period(img[x])
y_unit = sampling_period(img[y])
fig.rect(x=x, y=y, width=x_unit, height=y_unit, color='color', line_color='color', source=source)
fig.select_one(HoverTool).tooltips = tooltips
return fig
#### Note: however this comes with a quite high computational price