How do I get the HoloViews box selection tool to synchronize multiple connected plots?

How do I get the HoloViews box selection tool to synchronize multiple connected plots?

For example, with

import numpy as np
import pandas as pd
import holoviews as hv

from holoviews import opts 

hv.extension('bokeh')

def scatter_hist(src, x, y):
    p = hv.Scatter(src, x, y).hist(num_bins=20, dimension=[x, y]).opts(
            opts.Scatter(show_title=False, tools=['hover','box_select']), 
            opts.Histogram(tools=['hover','box_select']),
            opts.Layout(shared_axes=True, shared_datasource=True, merge_tools=True)
        )
    return p

scatter_hist(ds, 'x', 'y')

I can get box_zoom to work as expected (zooming in any one of the three figures imposes the corresponding zoom on the other two) but box_select does not behave in this way. I can only box-select in the top histogram (though the toolbar doesn’t highlight the tool), and this has no effect on the other plots.

I’ve experimented with different values for tools (including omission) for both opts.Scatter and opts.Histogram but can’t discern any pattern with what’s going in when I do (in fact I’m a bit vague on what I need to specify for tools since some appear without being listed).

How do I get the HoloViews box selection tool to synchronize multiple connected plots, so that selection in one, selects the corresponding values in the others?

1 Like

Are you looking for something like Linked Brushing — HoloViews 1.14.5 documentation?

1 Like

Are you looking for something like Linked Brushing?

Maybe. I’m a noob so I was expecting this to work with the code pretty much as it is or at least not much more. I.e., that it would be the basic behavior of the tools. Is something more complex necessary?

Put another way, shouldn’t this work with the plots as composed in the MWE, rather than requiring gridmatrix? The linked docs say " … linked brushing, … can be enabled for Bokeh plots sharing a common data source by simply adding a selection tool", which is what I thought I was doing in the MWE.

Hi @orome

Linked brushing is something like the below. I’ve wrapped it into a nice Panel FastListTemplate. Save the code in a file called script.py and serve it using panel serve script.py --autoreload. The app is then served at http://localhost:5006/script.

import holoviews as hv
import panel as pn
from holoviews.operation.element import histogram
from holoviews.selection import link_selections
from bokeh.sampledata.autompg import autompg

pn.extension();hv.extension('bokeh', 'plotly')

autompg_ds = hv.Dataset(autompg, ['yr', 'name', 'origin'])
color="#A01346"

w_accel_scatter = hv.Scatter(autompg_ds, 'weight', 'accel').opts(color=color, responsive=True, )
mpg_hist = histogram(autompg_ds, dimension='accel', normed=False).opts(color=color, responsive=True, )
top_hist = histogram(autompg_ds, dimension='weight', normed=False).opts(color=color, responsive=True, )

mpg_ls = link_selections.instance()

plot = w_accel_scatter + mpg_hist + top_hist
plot=mpg_ls(plot)

# Serve using: 'panel serve script.py --autoreload'
import panel as pn

pn.template.FastListTemplate(
    site="HoloViews",
    title="Linked Brushing",
    main=[pn.Column("## HoloViews - Linked Brushing", plot, min_height=500, sizing_mode="stretch_both")],
).servable()
1 Like

@Marc So linked brushing for histograms doesn’t work with my MWE? I’m still not clear what the diff is between what I’ve got, and what I need to add/change to have selection synchronized among all three plots; or even to be able to box-select the scatter, for that matter.

1 Like

I’m also a bit confused. But I believe there must a bug. Because if I change

plot = w_accel_scatter + mpg_hist + top_hist

to

plot = w_accel_scatter << mpg_hist << top_hist

It looks right but box_select does not work.

I’ve reported it as a bug

box select of scatter with adjoint histograms does not work. · Issue #5066 · holoviz/holoviews (github.com)

Hmm. If I change << to + I can now box-select each plot (whereas before I could only box-select the top histogram) but there is still no linkage (and the box-zoom linkage from the Scatter to the Histograms no longer works).

 def scatter_histH(src, x, y, dims):
    p = (hv.Scatter(src, x, y) + hv.Histogram(np.histogram(src[dims[1]], 20)) + hv.Histogram(np.histogram(src[dims[0]], 20)) ).opts(
            opts.Scatter(show_title=False, tools=['hover','box_select','lasso_select']), 
            opts.Histogram(tools=['hover','box_select']),
            opts.Layout(shared_axes=True, shared_datasource=True, merge_tools=True)
        )
    return p
scatter_histH(ds, [('x', 'Apples')], [('y', 'Oranges'), ('z', 'Sauce')], ['x', 'y'])

Overall this just feels very buggy and unpredictable.

1 Like

My group is also looking for this functionality. I think this code (modified from https://holoviews.org/reference/streams/bokeh/Bounds.html) comes very close to providing a workaround until bug 5066 has been fixed).

import numpy as np
import holoviews as hv
from holoviews import opts
from holoviews import streams
hv.extension('bokeh')

opts.defaults(opts.Points(tools=['box_select', 'lasso_select']))

# Declare some points
points = hv.Points(np.random.randn(1000,2 ))

# Declare points as source of selection stream
selection = streams.Selection1D(source=points)

def select_points(index):
    if index:
        selected = points.iloc[index]
    else:
        selected = points.iloc[:]
    return selected
                         
# Declare DynamicMap to apply bounds selection
dmap = hv.DynamicMap(select_points, streams=[selection])
xhist = hv.operation.histogram(dmap, bin_range=points.range('x'), dimension='x', dynamic=True, normed=False)
yhist = hv.operation.histogram(dmap, bin_range=points.range('y'), dimension='y', dynamic=True, normed=False)

# Combine points and histograms
points << yhist << xhist
1 Like

I was finally able to put together an example that does everything I needed:

  1. lowlighting of unselected data
  2. dynamic brushing works regardless of which of the three plots was selected.

In order to get the lowlighting working, I ended up drawing each plot twice. Once with all the points drawn in lowlight color, and then an overlay with the just points selected drawn in normal color. I’m sure there must be a better way to do this, and I’d appreciate any advice. I’m really looking forward to when the team is able to fix bug 5066.

import numpy as np
import pandas as pd
import holoviews as hv
from holoviews import dim
from holoviews import opts
from holoviews import streams
hv.extension('bokeh')

default_blue = '#30a2d9'
opts.defaults(opts.Points(color=default_blue, tools=['box_select'], active_tools=['box_select']),
              opts.Histogram(color=default_blue, tools=['box_select'], active_tools=['box_select'])
             )

# Declare some points
df = pd.DataFrame(data=np.random.randn(1000,2 ), columns=['x', 'y'])
points = hv.Points(df).opts(opts.Points(alpha=0.2))

# Declare points as source of selection stream
selection = streams.Selection1D(source=points)
reset = streams.PlotReset()
                         
xhist_all = hv.operation.histogram(points, bin_range=points.range('x'), dimension='x', dynamic=True, normed=False).opts(
    opts.Histogram(alpha=0.2, width=300, height=100))
x_hist_selection = streams.BoundsX(source=xhist_all, boundsx=(0,0))

yhist_all = hv.operation.histogram(points, bin_range=points.range('y'), dimension='y', dynamic=True, normed=False).opts(
    opts.Histogram(alpha=0.2, width=100, height=300))
# y_hist_selection = streams.BoundsX(source=yhist_all, boundsx=(0,0), rename={'boundsx': 'boundsy'})  # if not rotated
y_hist_selection = streams.BoundsY(source=yhist_all, boundsy=(0,0))

def select_points(index, boundsx, boundsy, resetting):
    selected = points.iloc[:]
    if boundsx and (boundsx[0] or boundsx[1]):
        selected = points.select(selection_expr=(dim('x') >= boundsx[0])&(dim('x') <= boundsx[1]))
    if boundsy and (boundsy[0] or boundsy[1]):
        selected = points.select(selection_expr=(dim('y') >= boundsy[0])&(dim('y') <= boundsy[1]))
    if index:
        selected = points.iloc[index]
    if resetting:
        selected = points.iloc[:]
        x_hist_selection.reset()
        y_hist_selection.reset()
    return selected

# Declare DynamicMap to apply bounds selection
dmap = hv.DynamicMap(select_points, streams=[selection, x_hist_selection, y_hist_selection, reset])
xhist = hv.operation.histogram(dmap, bin_range=points.range('x'), dimension='x', dynamic=True, normed=False)
yhist = hv.operation.histogram(dmap, bin_range=points.range('y'), dimension='y', dynamic=True, normed=False)

points_plot = dmap.apply(hv.Points)

# Combine points and histograms
(points * points_plot) << (yhist_all * yhist) << (xhist_all * xhist)
1 Like