Streaming events to a plot#
This notebook will illustrate how a plot can be continuously updated with new incoming data, without hogging the event loop, meaning that interacting with the plot (zooming, panning, …) is possible while the data is streaming in.
Working in other notebook cells is also possible while the plot is updating.
[1]:
import scipp as sc
import plopp as pp
import ipywidgets as ipw
%matplotlib widget
Create a data generator#
We create an object which generates new data points when its update
method is called.
[2]:
import numpy as np
class DataGen:
"""A data generator which makes new data when asked"""
def __init__(self):
# Make an empty container with 300x300 bins
nx, ny = 300, 300
xmin, xmax = -10.0, 10.0
ymin, ymax = -10.0, 10.0
self.data = sc.DataArray(data=sc.zeros(sizes={'y': ny, 'x': nx}),
coords={'x': sc.linspace('x', xmin, xmax, nx+1, unit='m'),
'y': sc.linspace('y', ymin, ymax, ny+1, unit='m')})
def __call__(self, iteration: int=0):
# Generate new data when called
npoints = 100
x = sc.array(dims=['event'], values=np.random.normal(scale=2, size=npoints), unit='m')
y = sc.array(dims=['event'], values=np.random.normal(scale=2, size=npoints), unit='m')
new_events = sc.DataArray(
data=sc.ones(sizes=x.sizes, unit=""),
coords={'x': x, 'y': y})
# Histogram and add to container
self.data += new_events.hist({xy: self.data.coords[xy] for xy in "xy"})
return self.data
data = DataGen()
Connecting the data generator to a Play widget#
We now use ipywidgets
’s Play widget to continuously send updates at regular intervals.
[3]:
# Create a play widget that fires every 100 ms
play = ipw.Play(min=0, max=500, interval=100)
# Wrap the widget in a widget node
play_node = pp.widget_node(play)
# Connect the widget to the data generator.
# Note that `data` here is not a function, but our generator
# class that has a `__call__` method defined.
stream_node = pp.Node(data, iteration=play_node)
# Plot on a figure
fig = pp.imagefigure(stream_node, norm="log", cbar=True)
[5]:
# Display figure and play widget
ipw.VBox([fig, play])
[5]:
After pressing play, data starts collecting on the plot.
Zooming and panning is still possible while the image is updating
Working elsewhere in the notebook is also not blocked by the updating plot
[6]:
pp.show_graph(fig)
[6]:
Streaming on a 3D figure#
The same works for 3D scatter plots.
We make a slightly different data generator:
[8]:
class DataGen:
"""A data generator which makes new data on a cylindrical detector panel"""
def __init__(self):
nphi = 100
nz = 20
r = sc.scalar(10.0, unit='m')
phi = sc.linspace('phi', 0, np.pi, nphi + 1, unit='rad')
z = sc.linspace('z', -3.0, 3.0, nz + 1, unit='m')
p = sc.midpoints(phi)
x = r * sc.cos(p)
y = r * sc.sin(p)
sizes = {'z': nz, 'phi': nphi}
self.data = sc.DataArray(
data=sc.zeros(sizes=sizes, unit='counts'),
coords={
'z': z,
'phi': phi,
'position': sc.spatial.as_vectors(
sc.broadcast(x, sizes=sizes),
sc.broadcast(sc.midpoints(z), sizes=sizes),
sc.broadcast(y, sizes=sizes)
),
},
)
def __call__(self, iteration: int=0):
# Generate new data when called
npoints = 100
phi = sc.array(dims=['event'], values=np.random.normal(scale=0.5, loc=0.5*np.pi, size=npoints), unit='rad')
z = sc.array(dims=['event'], values=np.random.normal(scale=2, size=npoints), unit='m')
new_events = sc.DataArray(
data=sc.ones(sizes=phi.sizes, unit="counts"),
coords={'phi': phi, 'z': z})
# Histogram and add to container
self.data += new_events.hist({x: self.data.coords[x] for x in ("phi", "z")})
return self.data
data = DataGen()
We then connect the streaming node to a 3D scatter plot:
[9]:
play = ipw.Play(min=0, max=500, interval=100)
play_node = pp.widget_node(play)
stream_node = pp.Node(data, iteration=play_node)
fig = pp.scatter3d(stream_node, pos='position', cbar=True, pixel_size=0.3, autoscale=False)
[11]:
# Display figure and play widget
ipw.VBox([fig, play])
[11]:
[12]:
pp.show_graph(fig)
[12]: