Plots linked via js_link

Hi,

I have an application which uses a plot for graphical selection of a point of interest and an associated plot that constrains a result to be on a curve (or surface in higher dimensions). In the simplest case of a univariate function, the selector is 1D and works like a slider.

The interactions between the selector and the constrained-point viewer all behave as expected.

However, if I have multiple pairs of selectors/viewer, and try to link their selected operating/evaluation point, unexpected results occur. Specifically, I link them via js_link callbacks on their data sources. It appears the linkage works for the first interaction, but subsequent interactions fail to update the selectors to be at the same point.

I can see that the callback is being invoked for each source, but the data patch doesn’t seem to update the slider-like selector plot.

A simplified example illustrating the problem follows. I am running as a bokeh server. The bokeh version is 2.0.1. Two screen shots of the simplified problem are attached. The first shows the plots in a consistent state (after the first user-interaction to select a desired operating/evaluation point). The second shows the plots in an inconsistent state (after a subsequent user interaction to select a different desired operating/evaluation point). Note: that the lower-plot selector has not moved to be at the same point as the upper-plot selector, but its associated constrained curve output has.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
"""
import sys
import numpy as np

from bokeh.plotting import figure, curdoc
from bokeh.transform import transform

from bokeh.layouts import column

from bokeh.models import LinearInterpolator
from bokeh.models import ColumnDataSource, PointDrawTool


# Lower/upper quadratic-function constrained-point viewers
p0L = figure(x_range=(-1.333,1.333), y_range=(-1.333,1.333), width=500, height=500)
p0L.toolbar_location = None

p0U = figure(x_range=(-1.333,1.333), y_range=(-1.333,1.333), width=500, height=500)
p0U.toolbar_location = None


# Lower/upper linked x- selector figures
p1L= figure(x_range=p0L.x_range, y_range=p0L.y_range, width=500, height=30, background_fill_color='#000000')

p1L.xaxis.major_label_text_color = None
p1L.xaxis.major_label_text_font_size = '0pt'

p1L.xaxis.ticker.desired_num_ticks = 0
p1L.yaxis.ticker.desired_num_ticks = 0

p1L.toolbar_location = None


p1U= figure(x_range=p0U.x_range, y_range=p0U.y_range, width=500, height=30, background_fill_color='#000000')

p1U.xaxis.major_label_text_color = None
p1U.xaxis.major_label_text_font_size = '0pt'

p1U.xaxis.ticker.desired_num_ticks = 0
p1U.yaxis.ticker.desired_num_ticks = 0

p1U.toolbar_location = None


# Quadratic reference curves
xq = np.arange(-1.000,1.001,0.001)
yq = 2.000*(xq ** 2) - 1.000
SqL = ColumnDataSource(data=dict(xq=xq, yq=yq))
p0L.line(x='xq', y='yq', source=SqL, line_color='#AAAAAA')

SqU = ColumnDataSource(data=dict(xq=xq, yq=-yq))
p0U.line(x='xq', y='yq', source=SqU, line_color='#AAAAAA')


# Point on curve
# Linear interpolators constrain interactive taps to reference curve
li_xL = LinearInterpolator(x='xq', y='xq', data=SqL, clip=False)
li_yL = LinearInterpolator(x='xq', y='yq', data=SqL, clip=False)

li_xU = LinearInterpolator(x='xq', y='xq', data=SqU, clip=False)
li_yU = LinearInterpolator(x='xq', y='yq', data=SqU, clip=False)

Spt0L = ColumnDataSource(data=dict(xpt=[xq[500]]))
Spt1L = ColumnDataSource(data=dict(xpt=[xq[500]]))

Spt0U = ColumnDataSource(data=dict(xpt=[xq[500]]))
Spt1U = ColumnDataSource(data=dict(xpt=[xq[500]]))

def lim_cb(attr, old, new, src):
    print("source [{:}] op OLD:{:+.3f} NEW:{:+.3f}".format(src, old['xpt'][0],new['xpt'][0]))
    print("")
    sys.stdout.flush()

    op = min(max(new['xpt'][0], -1.0), 1.0)
    data_patch = {}
    data_patch.update(xpt=[(0, op)])
    src.patch(data_patch)

f0L = lambda attr, old, new, src=Spt0L: lim_cb(attr, old, new, src)
f0U = lambda attr, old, new, src=Spt0U: lim_cb(attr, old, new, src)
Spt1L.on_change('data', f0L)
Spt1U.on_change('data', f0U)

r0L = p0L.circle(x=transform('xpt', li_xL), y=transform('xpt', li_yL), source=Spt0L, size=12, fill_color='#000000', line_color='#000000')
r1L = p1L.circle(x=transform('xpt', li_xL), y=0.0, source=Spt1L, size=12, fill_color='#FFFFFF', line_color='#FFFFFF')

_drawtool = PointDrawTool(renderers=[r1L], add=True, num_objects=1)
p1L.add_tools(_drawtool)
p1L.toolbar.active_tap = _drawtool

r0U = p0U.circle(x=transform('xpt', li_xU), y=transform('xpt', li_yU), source=Spt0U, size=12, fill_color='#000000', line_color='#000000')
r1U = p1U.circle(x=transform('xpt', li_xU), y=0.0, source=Spt1U, size=12, fill_color='#FFFFFF', line_color='#FFFFFF')

_drawtool = PointDrawTool(renderers=[r1U], add=True, num_objects=1)
p1U.add_tools(_drawtool)
p1U.toolbar.active_tap = _drawtool

# Link lower/upper selectors
link_sel = True 
if link_sel:
    Spt1U.js_link('data', Spt1L, 'data')
    Spt1L.js_link('data', Spt1U, 'data')

curdoc().add_root(column(p0U,p1U,p1L,p0L))

js_link is not suitable for direct linking of ColumnDataSource.data properties. It should only be used for scalar properties.

The issue is that on the first update, Spt1U.data and Spt1L.data become the same object. The subsequent updates are not triggered because it already is the same object.

Hi @p-himik

Thanks for an explanation.

Something still seems perplexing.

I added logic to the Python callback function that gets invoked (see following), which generates representative output on successive calls …

INITIAL INTERACTION
source [ColumnDataSource(id=‘1143’, …)] op OLD:-0.500 NEW:+0.747

SELECTOR DATA SAME EQUAL

source [ColumnDataSource(id=‘1141’, …)] op OLD:-0.500 NEW:+0.747

SELECTOR DATA SAME EQUAL

.
.
.

SUBSEQUENT INTERACTION
source [ColumnDataSource(id=‘1143’, …)] op OLD:+0.747 NEW:-0.724

SELECTOR DATA SAME EQUAL

source [ColumnDataSource(id=‘1141’, …)] op OLD:+0.747 NEW:-0.724

SELECTOR DATA SAME EQUAL

CALLBACK w/ PRINTOUT SUPPORT

def lim_cb(attr, old, new, src):
    print("source [{:}] op OLD:{:+.3f} NEW:{:+.3f}".format(src, old['xpt'][0],new['xpt'][0]))
    sys.stdout.flush()

    op = min(max(new['xpt'][0], -1.0), 1.0)
    data_patch = {}
    data_patch.update(xpt=[(0, op)])
    src.patch(data_patch)
    
    if Spt1U.data is Spt1L.data:
        _same = 'x'
    else:
        _same = ' '
    
    if Spt1U.data == Spt1L.data:
        _equal = 'x'
    else:
        _equal = ' '

    print("SELECTOR DATA SAME [{:}] EQUAL [{:}]".format(_same,_equal))
    print("")
    sys.stdout.flush()

Hi @p-himik

I will accept your explanation as the solution given that it answers what is and is not possible via js_link.

For my particular application, I can satisfy the underlying requirement via the glyph-renderer’s update() mechanism to share CDSView and ColumnDataSource properties iff a linkage needs to be established after the plots have been rendered. Otherwise, I shall maintain independent sources for decoupled interaction.

Thanks again.

1 Like