holoviz / holoviews

With Holoviews, your data visualizes itself.
https://holoviews.org
BSD 3-Clause "New" or "Revised" License
2.71k stars 403 forks source link

box select of scatter with adjoint histograms does not work. #5066

Open MarcSkovMadsen opened 3 years ago

MarcSkovMadsen commented 3 years ago

HoloViews: 1.14.5 Panel: 0.12.1

I'm trying to help a user on discord get box_select working for a scatter plot with adjoint histograms. https://discourse.holoviz.org/t/how-do-i-get-the-holoviews-box-selection-tool-to-synchronize-multiple-connected-plots/2662

So far I have been able to get it working when the layout is not adjoint. https://discourse.holoviz.org/t/how-do-i-get-the-holoviews-box-selection-tool-to-synchronize-multiple-connected-plots/2662/4?u=marc

When I change

plot = w_accel_scatter + mpg_hist + top_hist

to

plot = w_accel_scatter << mpg_hist << top_hist

it displays in an adjoint layout but linked selections unfortunately no longer works.

https://user-images.githubusercontent.com/42288570/129451450-b7ae2b04-ea83-42fb-8ea1-8db87823248d.mp4

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()
marcdhansenesi commented 1 year ago

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.).

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
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
marcdhansenesi commented 1 year ago

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']))

# 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)

# 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:
        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.opts(color=default_blue, alpha=1.0)

# 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, tools=['xbox_select'])
yhist_selected = histogram(points_selected, bin_range=points_all.range('y'), dimension='y').opts(color=default_blue, alpha=1.0, tools=['ybox_select'])

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