holoviz / holoviews

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

DynamicMap as source of stream #4700

Open HugoMichi opened 3 years ago

HugoMichi commented 3 years ago

Hello I tried to use a DynamicMap as the source of a stream, without using output of the stream in the callback of the DynamicMap. It works as intended when I use a Curve, but not with a DynamicMap.

ALL software version info python 3.8.5 holoviews 1.13.5 bokeh 2.2.3
hvplot 0.6.0
panel 0.10.1

import hvplot.streamz
import streamz
import pandas as pd
import numpy as np
from streamz.dataframe import Random
import holoviews as hv
import panel as pn

df = streamz.dataframe.DataFrame(example=pd.DataFrame({'x': [], 'y': []}))
line = df.hvplot(y=['x', 'y'], backlog=200, framewise=False)

def reset_following(resetting):
    print(resetting)
    print("reset_pressed")

stream = hv.streams.PlotReset(source=line)
stream.add_subscriber(reset_following)

curve = hv.Curve([])
stream2 = hv.streams.PlotReset(source=curve)
stream2.add_subscriber(reset_following)

def emit():
    df.emit(pd.DataFrame({'x': [np.random.randn()], 'y': [np.random.randn()]}))

pn.state.add_periodic_callback(emit, period=100, count=500)
app = pn.Column(line, curve)
pn.panel(app).show()

Pressing the reset button on the curve leads to the expected behaviour, but not for the dynamicMap plot generated by the hvplot call.

When looking at the bokeh plot by:

plot = BokehRenderer.instance(mode='server').get_plot(line)
curve_plot = BokehRenderer.instance(mode='server').get_plot(curve)
plot.callbacks

plot has no registered callbacks curveplot has the expected holoviews.plotting.bokeh.callbacks.ResetCallback

The issue seems to be related to how DynamicMaps/GenericOverlayPlot are treated in the _construct_callbacks() function of CallbackPlot in:

https://github.com/holoviz/holoviews/blob/cca52fc39476cccf0dfc773588d82e88fd7cd442/holoviews/plotting/plot.py#L944-L960

if the following line is changed https://github.com/holoviz/holoviews/blob/cca52fc39476cccf0dfc773588d82e88fd7cd442/holoviews/plotting/plot.py#L978 to:

           sources = [self.hmap]

the example works as expected. But I dont know what other side effects this has.

HugoMichi commented 3 years ago
import hvplot.streamz
import streamz
import pandas as pd
import numpy as np
from streamz.dataframe import Random
import holoviews as hv
import panel as pn
import time
from functools import partial

df = streamz.dataframe.DataFrame(example=pd.DataFrame({'x': [], 'y': []}))
line = df.hvplot(y=['x', 'y'], backlog=200, framewise=False)

reset_line = hv.streams.PlotReset(source=line)
range_line = hv.streams.RangeXY(source=line)

def enable_following(plot, resetting):
    print(resetting)
    if resetting:
        for stream in plot.streams:
            if isinstance(stream, hv.streams.Buffer):
                print("enabled")
                stream.following = True
                stream.last_follow_enabled = int(time.time())

def disable_following(plot, x_range, y_range):
    for stream in plot.streams:
        if isinstance(stream, hv.streams.Buffer):
            current_time = int(time.time())
            if hasattr(stream, 'last_follow_enabled') and (
                    stream.last_follow_enabled == current_time or stream.last_follow_enabled+1 == current_time):
                print("not disabled")
                stream.following = True
            else:
                print("disabled")
                stream.following = False

index = 0
def emit():
    global index
    df.emit(pd.DataFrame({'x': [np.random.randn()], 'y': [np.random.randn()]}, index=[index]))
    index = index + 1

reset_line.add_subscriber(partial(enable_following, line), precedence=1)
range_line.add_subscriber(partial(disable_following, line))
pn.state.add_periodic_callback(emit, period=100, count=10000)
app = pn.Column(line)
pn.panel(app).show()

Using the mentioned patch I was able use the streams for starting and stopping the "following" behaviour of the streaming plot. This works fine most of the time, but sometimes the setting of stream.following =True also triggers the rangeXY stream and therefore disables the "following" again. I tried to prevent that by ignoring the stream for 1 second, but the stream just gets triggered again with every new datapoint. Only refreshing the plot resolves the issue. This is werid, because normaly the RangeXY stream is not triggered by the following behaviour of the plot. Also the period of the callback seems to influence the bahavior. The behaviour can be replicated relatively reliably by stopping the stream by zooming or panning and then hitting refresh multiple times as quickly as possible. There seems to be some sort of race condition between the different callbacks. Until now I have not found a relaible way to prevent it. A higher period reduces the probability, but does not reliably prevent it.