Muted option to lowlight unselected histogram bars from Selection1D stream?

My group needs dynamic brushing between scatter and adjoint histogram plots. 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.).

I think the only missing features are:

  1. to lowlight the histogram bars corresponding to the unselected points after a selection has been made
  2. enable linked selection originating from histograms

Any ideas on how to implement these capabilities would be much appreciated.

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

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.operation import histogram
from holoviews import opts
from holoviews import streams
hv.extension('bokeh')

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

# Declare some points
np.random.seed(1)
df = pd.DataFrame(data=np.random.randn(10,2 ), 
                  columns=['x', 'y'])
points_all = hv.Points(df).opts(alpha=0.2, 
                                tools=['box_select'], 
                                active_tools=['box_select'])

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

xhist_all = histogram(points_all,
                      bin_range=points_all.range('x'), 
                      dimension='x').opts(alpha=0.2, 
                                          width=300, 
                                          height=100)
xhist_bounds = streams.BoundsX(source=xhist_all, 
                               boundsx=(0,0))

yhist_all = histogram(points_all,
                      bin_range=points_all.range('y'), 
                      dimension='y').opts(alpha=0.2, 
                                          width=100, 
                                          height=300)
# yhist_bounds = streams.BoundsX(source=yhist_all, boundsx=(0,0), rename={'boundsx': 'boundsy'})  # if not rotated
yhist_bounds = streams.BoundsY(source=yhist_all, 
                               boundsy=(0,0))

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

# Declare DynamicMap to apply bounds selection
points_selected = hv.DynamicMap(select_points, 
                                streams=[selection,
                                         xhist_bounds,
                                         yhist_bounds,
                                         reset])
xhist_selected = histogram(points_selected, 
                           bin_range=points_all.range('x'), 
                           dimension='x').opts(color=default_blue, 
                                               alpha=1.0)
yhist_selected = histogram(points_selected,
                           bin_range=points_all.range('y'), 
                           dimension='y').opts(color=default_blue, 
                                               alpha=1.0)
points_selected.opts(alpha=1.0, color=default_blue)

# Combine points and histograms
((points_all * points_selected) 
 << (yhist_all * yhist_selected) 
 << (xhist_all * xhist_selected))
1 Like