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]:
../_images/gallery_streaming-plot_9_0.svg

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]:
../_images/gallery_streaming-plot_17_0.svg