Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bokeh): Server side HoverTool for rasterized/datashaded plots with selector #6422

Open
wants to merge 59 commits into
base: main
Choose a base branch
from

Conversation

hoxbro
Copy link
Member

@hoxbro hoxbro commented Oct 25, 2024

Previously, we sent all hover data to the front end, which could lead to significant overhead on larger plots. With this PR, the data is pushed server-side to the hover tool with a custom data model.

script
import itertools

import datashader as ds
import numpy as np
import pandas as pd
import panel as pn

import holoviews as hv
from holoviews.operation.datashader import datashade, dynspread, rasterize

hv.extension("bokeh")

num = 10000
seed = np.random.default_rng(1)


dists = {
    cat: pd.DataFrame(
        {
            "x": seed.normal(x, s, num),
            "y": seed.normal(y, s, num),
            "s": s,
            "val": val,
            "cat": cat,
        }
    )
    for x, y, s, val, cat in [
        (2, 2, 0.03, 0, "d1"),
        (2, -2, 0.10, 1, "d2"),
        (-2, -2, 0.50, 2, "d3"),
        (-2, 2, 1.00, 3, "d4"),
        (0, 0, 3.00, 4, "d5"),
    ]
}

df = pd.concat(dists, ignore_index=True)
df["y"] += 100  # Offset to distinguish x from y
points = hv.Points(df)


def dynspread_datashade(*args, **kwargs):
    return dynspread(datashade(*args, **kwargs))


def dynspread_rasterize(*args, **kwargs):
    return dynspread(rasterize(*args, **kwargs))


ops = (rasterize, dynspread_rasterize, datashade, dynspread_datashade)
aggs = (ds.min("s"), ds.by("cat"))
sels = (None, ds.min("s"))
# ops = (datashade,)
# aggs = (ds.by("cat"),)
# sels = (None,)
combinations = itertools.product(ops, aggs, sels)
plots = []
for op, agg, sel in combinations:
    title = f"{op.__name__.replace('_','+')}(agg={type(agg).__name__}, sel={type(sel).__name__ if sel else None})"
    plot = op(points, aggregator=agg, selector=sel).opts(
        tools=["hover"], title=title, width=400, height=400
    )
    plots.append(plot)

pn.panel(hv.Layout(plots).cols(4).opts(shared_axes=False)).servable()

image

@hoxbro hoxbro added the type: enhancement Minor feature or improvement to an existing feature label Oct 25, 2024
Copy link

codecov bot commented Oct 25, 2024

Codecov Report

Attention: Patch coverage is 87.45387% with 34 lines in your changes missing coverage. Please review.

Project coverage is 88.49%. Comparing base (ee19136) to head (e818c4d).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
holoviews/tests/ui/bokeh/test_hover.py 48.78% 21 Missing ⚠️
holoviews/plotting/bokeh/raster.py 87.50% 7 Missing ⚠️
holoviews/operation/datashader.py 95.41% 5 Missing ⚠️
holoviews/element/raster.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6422      +/-   ##
==========================================
- Coverage   88.50%   88.49%   -0.02%     
==========================================
  Files         323      323              
  Lines       68631    68819     +188     
==========================================
+ Hits        60741    60899     +158     
- Misses       7890     7920      +30     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


🚨 Try these New Features:

@hoxbro hoxbro changed the title enh: Update hovertool server side for rasterized plots enh: Make HoverTool update server side for rasterized/datashaded plots Oct 28, 2024
@hoxbro hoxbro changed the title enh: Make HoverTool update server side for rasterized/datashaded plots feat: Make HoverTool update server side for rasterized/datashaded plots Nov 5, 2024
@hoxbro hoxbro changed the title feat: Server side HoverTool for rasterized/datashaded plots feat: Server side HoverTool for rasterized/datashaded plots with selector Nov 5, 2024
raise ValueError("The spread should not be equal to the original image")

if isinstance(agg_fn, ds.count):
data = spread_img.vdims[-1].name # FIXME: Last one as it is alpha for datashade, shouldn't matter
Copy link
Member Author

@hoxbro hoxbro Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the last thing that I need to look into. RGBA gives a non-zero value when alpha is zero. This was a bug before this PR, though.

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have resolved this in 9e71910. I think it is correct to set it to 0 when alpha is zero, but it is likely to be a somewhat subjective opinion.

@hoxbro hoxbro marked this pull request as ready for review November 19, 2024 19:29
@hoxbro hoxbro changed the title feat: Server side HoverTool for rasterized/datashaded plots with selector feat(bokeh): Server side HoverTool for rasterized/datashaded plots with selector Nov 21, 2024
data = element.clone(datatype=['xarray']).data[element.vdims[0].name]
if element.interface.datatype != 'xarray':
element = element.clone(datatype=['xarray'])
data = shade._extract_data(element)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Image is no longer cloned by default.

Comment on lines +1763 to +1766
# TODO: Investigate why this does not work
# element = element.clone(data=new_data, kdims=element.vdims.copy(), vdims=element.vdims.copy())
element = element.clone()
element.data = new_data
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't understand this.

)

def on_change(attr, old, new):
if np.isinf(new).all():
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely we can just check the first value here.

image

expect(hv_plot).to_have_count(1)
bbox = hv_plot.bounding_box()

# Hover over the plot, first time the hovertool only have null
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make it so it does not return null, but a random point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant