File size: 24,473 Bytes
b9a0f21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# Responding to Events


```python
import numpy as np
import holoviews as hv
from holoviews import opts

hv.extension('bokeh')
```

In the [Live Data](./07-Live_Data.ipynb) guide we saw how ``DynamicMap`` allows us to explore high dimensional data using the widgets in the same style as ``HoloMaps``. Although suitable for unbounded exploration of large parameter spaces, the ``DynamicMaps`` described in that notebook support exactly the same mode of interaction as ``HoloMaps``. In particular, the key dimensions are used to specify a set of widgets that when manipulated apply the appropriate indexing to invoke the user-supplied callable.

In this user guide we will explore the HoloViews streams system that allows *any* sort of value to be supplied from *anywhere*. This system opens a huge set of new possible visualization types, including continuously updating plots that reflect live data as well as dynamic visualizations that can be interacted with directly, as described in the [Custom Interactivity](./13-Custom_Interactivity.ipynb) guide.

<center><div class="alert alert-info" role="alert">To use visualize and use a <b>DynamicMap</b> you need to be running a live Jupyter server.<br>When viewing this user guide as part of the documentation DynamicMaps will be sampled with a limited number of states.<br></div></center>


```python
# Styles and plot options used in this user guide

opts.defaults(
    opts.Area(fill_color='cornsilk', line_width=2,
              line_color='black'),
    opts.Ellipse(bgcolor='white', color='black'),
    opts.HLine(color='red', line_width=2),
    opts.Image(cmap='viridis'),
    opts.Path(bgcolor='white', color='black', line_dash='dashdot',
              show_grid=False),
    opts.VLine(color='red', line_width=2))
```

## A simple ``DynamicMap``

Before introducing streams, let us declare a simple ``DynamicMap`` of the sort discussed in the [Live Data](07-Live_Data.ipynb) user guide. This example consists of a ``Curve`` element showing a [Lissajous curve](https://en.wikipedia.org/wiki/Lissajous_curve) with ``VLine`` and ``HLine`` annotations to form a crosshair:


```python
lin = np.linspace(-np.pi,np.pi,300)

def lissajous(t, a=3, b=5, delta=np.pi/2.):
    return (np.sin(a * t + delta), np.sin(b * t))

def lissajous_crosshair(t, a=3, b=5, delta=np.pi/2):
    (x,y) = lissajous(t,a,b,delta)
    return hv.VLine(x) * hv.HLine(y)

crosshair = hv.DynamicMap(lissajous_crosshair, kdims='t').redim.range(t=(-3.,3.))

path = hv.Path(lissajous(lin))

path * crosshair
```

As expected, the declared key dimension (``kdims``) has turned into a slider widget that lets us move the crosshair along the curve. Now let's see how to position the crosshair using streams.

## Introducing streams



The core concept behind a stream is simple: it defines one or more parameters that can change over time that automatically refreshes code depending on those parameter values. 

Like all objects in HoloViews, these parameters are declared using [param](https://param.holoviz.org/) and streams are defined as a parameterized subclass of the ``holoviews.streams.Stream``. A more convenient way is to use the ``Stream.define`` classmethod:


```python
from holoviews.streams import Stream, param
Time = Stream.define('Time', t=0.0)
```

This results in a ``Time`` class with a numeric ``t`` parameter that defaults to zero. As this object is parameterized, we can use ``hv.help`` to view its parameters:


```python
hv.help(Time)
```

This parameter is a ``param.Number`` as we supplied a float, if we had supplied an integer it would have been a ``param.Integer``. Notice that there is no docstring in the help output above but we can add one by explicitly defining the parameter as follows:


```python
Time = Stream.define('Time', t=param.Number(default=0.0, doc='A time parameter'))
hv.help(Time)
```

Now we have defined this ``Time`` stream class, we can make of an instance of it and look at its parameters:


```python
time_dflt = Time()
print('This Time instance has parameter t={t}'.format(t=time_dflt.t))
```

As with all parameterized classes, we can choose to instantiate our parameters with suitable values instead of relying on defaults.


```python
time = Time(t=np.pi/4)
print('This Time instance has parameter t={t}'.format(t=time.t))
```

For more information on defining ``Stream`` classes this way, use ``hv.help(Stream.define)``.

### Simple streams example

We can now supply this streams object to a ``DynamicMap`` using the same ``lissajous_crosshair`` callback from above by adding it to the ``streams`` list:


```python
dmap = hv.DynamicMap(lissajous_crosshair, streams=[time])
path * dmap + path * lissajous_crosshair(t=np.pi/4.)
```

Immediately we see that the crosshair position of the ``DynamicMap`` reflects the ``t`` parameter values we set on the ``Time`` stream. This means that the ``t`` parameter  was supplied as the argument  to the ``lissajous_curve`` callback. As we now have no key dimensions, there is no longer a widget for the ``t`` dimensions.

Although we have what looks like a static plot, it is in fact dynamic and can be updated in place at any time. To see this, we can call the ``event`` method on our ``DynamicMap``:



```python
dmap.event(t=0.2)
```

Running this cell will have updated the crosshair from its original position where $t=\frac{\pi}{4}$ to a new position where ``t=0.2``. Try running the cell above with different values of ``t`` and watch the plot update!

This ``event`` method is the recommended way of updating the stream parameters on a ``DynamicMap`` but if you have a handle on the relevant stream instance, you can also call the ``event`` method on that:


```python
time.event(t=-0.2)
```

Running the cell above also moves the crosshair to a new position. As there are no key dimensions, there is only a single valid (empty) key that can be accessed with ``dmap[()]`` or ``dmap.select()`` making ``event`` the only way to explore new parameters.

We will examine the ``event`` method and the machinery that powers streams in more detail later in the user guide after we have looked at more examples of how streams are used in practice.

### Working with multiple streams

The previous example showed a curve parameterized by a single dimension ``t``. Often you will have multiple stream parameters you would like to declare as follows:


```python
ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)

XY = Stream.define('XY',x=0.0,y=0.0)

def marker(x,y):
    return hv.VLine(x) * hv.HLine(y)

image = hv.Image(np.sin(xx)*np.cos(yy))

dmap = hv.DynamicMap(marker, streams=[XY()])

image * dmap
```

You can update both ``x`` and ``y`` by passing multiple keywords to the ``event`` method:


```python
dmap.event(x=-0.2, y=0.1)
```

Note that the definition above behaves the same as the following definition where we define separate ``X`` and ``Y`` stream classes:

```python
X = Stream.define('X',x=0.0)
Y = Stream.define('Y',y=0.0)
hv.DynamicMap(marker, streams=[X(), Y()])
```

The reason why you might want to list multiple streams instead of always defining a single stream containing all the required stream parameters will be made clear in the [Custom Interactivity](./13-Custom_Interactivity.ipynb) guide.

## Using Parameterized classes as a stream

Creating a custom ``Stream`` class is one easy way to declare parameters. However, there's no need to make a Stream if you have already expressed your domain knowledge on a ``Parameterized`` class. For instance, let's assume you have made a simple parameterized `BankAccount` class:



```python
class BankAccount(param.Parameterized):
    balance = param.Number(default=0, doc="Bank balance in USD")
    overdraft = param.Number(default=200, doc="Overdraft limit")

account = BankAccount(name='Jane', balance=300)
```

You can link parameter changes straight to DynamicMap callable parameters by passing a keyword:param dictionary to the `streams` argument (for HoloViews version >= 1.14.2):


```python
streams = dict(total=account.param.balance, overdraft=account.param.overdraft, owner=account.param.name)

def table(owner, total, overdraft):
    return hv.Table([(owner, overdraft, total)], ['Owner', 'Overdraft ($)', 'Total ($)'])

bank_dmap = hv.DynamicMap(table, streams=streams)
bank_dmap.opts(height=100)
```

Now as you set the `balance` parameter on the `janes_account` instance, the DynamicMap above updates. Note that the dictionary specifies that the `balance` parameter is mapped to the `total` argument of the callable.


```python
account.balance=65.4
account.overdraft=350
```

### Use with `panel`

This dictionary format is particularly useful when used with the [Panel](http://panel.pyviz.org/) library (a dependency of HoloViews that should always be available), because `panel` widgets always reflect their values on the `value` parameter. This means that if you declare two Panel widgets as follows:


```python
import panel as pn

slider = pn.widgets.FloatSlider(start=0, end=500, name='Balance')
checkbox = pn.widgets.Select(options=['student','regular', 'savings'], name='Account Type')
pn.Row(slider, checkbox)
```

You can map both widget values into a `DynamicMap` callback without having a name clash as follows:


```python
overdraft_limits = {'student':300, 'regular':100, 'savings':0} # Overdraft limits for different account types
streams = dict(owner=account.param.name, total=slider.param.value, acc=checkbox.param.value)

def account_info(owner, total, acc):
    return hv.Table([(owner, acc, overdraft_limits[acc], total)], 
                    ['Owner', 'Account Type', 'Overdraft ($)', 'Total ($)'])

widget_dmap = hv.DynamicMap(account_info, streams=streams)
widget_dmap.opts(height=100)
```


You can now update the plot above using the slider and dropdown widgets. Note that for all these examples, a `Params` stream is created internally. This type of stream can wrap Parameterized objects or sets of Parameters but (since HoloViews 1.10.8) it is rare that an explicit stream object like that needs to be used directly at the user level.  To see more examples of how to use Panel with HoloViews, see the [Dashboards user guide](./16-Dashboards.ipynb).

### Using `.apply.opts`

You can supplying Parameters in a similar manner to the `.apply.opts` method. In the following example, a `Style` class has Parameters that indicate the desired colorma and color levels for the `image` instance defined earlier. We can link these together as follows:


```python
class Style(param.Parameterized):

    colormap = param.ObjectSelector(default='viridis', objects=['viridis', 'plasma', 'magma'])

    color_levels = param.Integer(default=255, bounds=(1, 255))

style = Style()
image.apply.opts(colorbar=True, width=400, cmap=style.param.colormap, color_levels=style.param.color_levels)
```

Using the `.apply` accessor in this automatically makes the resulting `DynamicMap` depend on the streams specified by the Parameters. Unlike a regular streams class, the plot will update whenever a Parameter on the instance or class changes. For instance, we can update the ``cmap`` and ``color_level`` parameters and watch the plot update in response:


```python
style.color_levels = 10
style.colormap = 'plasma' # Note that this is mapped to the 'cmap' keyword in .apply.opts
```

## Combining streams and key dimensions


All the ``DynamicMap`` examples above can't be indexed with anything other than ``dmap[()]`` or ``dmap.select()`` as none of them had any key dimensions. This was to focus exclusively on the streams system at the start of the user guide and not because you can't combine key dimensions and streams:


```python
xs = np.linspace(-3, 3, 400)

def function(xs, time):
    "Some time varying function"
    return np.exp(np.sin(xs+np.pi/time))

def integral(limit, time):
    curve = hv.Curve((xs, function(xs, time)))[limit:]
    area  = hv.Area ((xs, function(xs, time)))[:limit]
    summed = area.dimension_values('y').sum() * 0.015  # Numeric approximation
    return (area * curve * hv.VLine(limit) * hv.Text(limit + 0.5, 2.0, '%.2f' % summed))

Time = Stream.define('Time', time=1.0)
dmap = hv.DynamicMap(integral, kdims='limit', streams=[Time()]).redim.range(limit=(-3,2))
dmap
```

In this example, you can drag the slider to see a numeric approximation to the integral on the left side on the ``VLine``.

As ``'limit'`` is declared as a key dimension, it is given a normal HoloViews slider. As we have also defined a ``time`` stream, we can update the displayed curve for any time value:


```python
dmap.event(time=8)
```

We now see how to control the ``time`` argument of the integral function by triggering an event with a new time value, and how to control the ``limit`` argument by moving a slider. Controlling ``limit`` with a slider this way is valid but also a little unintuitive: what if you could control ``limit`` just by hovering over the plot?

In the [Custom Interactivity](13-Custom_Interactivity.ipynb) user guide, we will see how we can do exactly this by switching to the bokeh backend and using the linked streams system.

### Matching names to arguments

Note that in the example above, the key dimension names and the stream parameter names match the arguments to the callable. This *must* be true for stream parameters but this isn't a requirement for key dimensions: if you replace the word 'radius' with 'size' in the example above after ``XY`` is defined, the example still works. 

Here are the rules regarding the callback argument names:

* If your key dimensions and stream parameters match the callable argument names, the definition is valid.
* If your callable accepts mandatory positional arguments and their number matches the number of key dimensions, the names don't need to match and these arguments will be passed key dimensions values.

As stream parameters always need to match the argument names, there is a method to allow them to be easily renamed. Let's say you imported a stream class as shown in  [Custom_Interactivity](13-Custom_Interactivity.ipynb) or for this example, reuse the existing ``XY`` stream class. You can then use the ``rename`` method allowing the following definition:


```python
def integral2(lim, t): 
    'Same as integral with different argument names'
    return integral(lim, t)

dmap = hv.DynamicMap(integral2, kdims='limit', streams=[Time().rename(time='t')]).redim.range(limit=(-3.,3.))
dmap
```

Occasionally, it is useful to suppress some of the stream parameters of a stream class, especially when using the *linked streams* described in [Custom_Interactivity](13-Custom_Interactivity.ipynb). To do this you can rename the stream parameter to ``None`` so that you no longer need to worry about it being passed as an argument to the callable. To re-enable a stream parameter, it is sufficient to either give the stream parameter its original string name or a new string name.

## Overlapping stream and key dimensions

In the above example above, the stream parameters do not overlap with the declared key dimension. What happens if we add 'time' to the declared key dimensions?



```python
dmap=hv.DynamicMap(integral, kdims=['time','limit'], streams=[Time()]).redim.range(limit=(-3.,3.))
dmap
```

First you might notice that the 'time' value is now shown in the title but that there is no corresponding time slider as its value is supplied by the stream.

The 'time' parameter is now an instance of what are called 'dimensioned streams' which re-enable indexing of these dimensions:


```python
dmap[1,0] + dmap.select(time=3,limit=1.5) + dmap[None,1.5]
```

In **A**, we supply our own values for the 'time and 'limit' parameters. This doesn't change the values of the 'time' parameters on the stream itself but it does allow us to see what would happen when the time value is one. Note the use of ``None`` in **C** as a way of leaving an explicit value unspecified, allowing the current stream value to be used.

This is one good reason to use dimensioned streams - it restores access to convenient indexing and selecting operation as a way of exploring your visualizations. The other reason it is useful is that if you keep all your parameters dimensioned, it re-enables the ``DynamicMap`` cache described in the [Live Data](07-Live_Data.ipynb), allowing you to record your interaction with streams and allowing you to cast to ``HoloMap`` for export:


```python
dmap.reset()  # Reset the cache, we don't want the values from the cell above
# TODO: redim the limit dimension to a default of 0
dmap.event(time=1)
dmap.event(time=1.5)
dmap.event(time=2)
hv.HoloMap(dmap)
```

One use of this would be to have a simulator drive a visualization forward using ``event`` in a loop. You could then stop your simulation and retain the recent history of the output as long as the allowed ``DynamicMap`` cache.

## Generators and argument-free callables

In addition to callables, Python supports [generators](https://docs.python.org/3/glossary.html#term-generator) that can be defined with the ``yield`` keyword. Calling a function that uses yield returns a [generator iterator](https://docs.python.org/3/glossary.html#term-generator-iterator) object that accepts no arguments but returns new values when iterated or when ``next()`` is applied to it.

HoloViews supports Python generators for completeness and [generator expressions](https://docs.python.org/3/glossary.html#term-generator-expression) can be a convenient way to define code inline instead of using lambda functions. As generators expressions don't accept arguments and can get 'exhausted' ***we recommend using callables with ``DynamicMap``*** - exposing the relevant arguments also exposes control over your visualization.

Unlike generators, callables that have arguments allow you to re-visit portions of your parameter space instead of always being forced in one direction via calls to ``next()``. With this caveat in mind, here is an example of a generator and the corresponding generator iterator that returns a ``BoxWhisker`` element:


```python
def sample_distributions(samples=10, tol=0.04):
    np.random.seed(42)
    while True:
        gauss1 = np.random.normal(size=samples)
        gauss2 = np.random.normal(size=samples)
        data = (['A']*samples + ['B']*samples, np.hstack([gauss1, gauss2]))
        yield hv.BoxWhisker(data, 'Group', 'Value')
        samples+=1
        
sample_generator = sample_distributions()
```

This returns two box whiskers representing samples from two Gaussian distributions of 10 samples. Iterating over this generator simply resamples from these distributions using an additional sample each time.

As with a callable, we can pass our generator iterator to ``DynamicMap``:


```python
hv.DynamicMap(sample_generator)
```

Without using streams, we now have a problem as there is no way to trigger the generator to view the next distribution in the sequence. We can solve this by defining a stream with no parameters:


```python
dmap = hv.DynamicMap(sample_generator, streams=[Stream.define('Next')()])
dmap
```

### Stream event update loops

Now we can simply use ``event()`` to drive the generator forward and update the plot, showing how the two Gaussian distributions converge as the number of samples increase.


```python
for i in range(40):
    dmap.event()
```

Note that there is a better way to run loops that drive ``dmap.event()`` which supports a ``period`` (in seconds) between updates and a ``timeout`` argument (also in seconds):


```python
dmap.periodic(0.1, 1000, timeout=3)
```

In this generator example, ``event`` does not require any arguments but you can set the ``param_fn`` argument to a callable that takes an iteration counter and returns a dictionary for setting the stream parameters. In addition you can use ``block=False`` to avoid blocking the notebook using a threaded loop. This can be very useful although it has two downsides 1. all running visualizations using non-blocking updates will be competing for computing resources 2. if you override a variable that the thread is actively using, there can be issues with maintaining consistent state in the notebook.

Generally, the ``periodic`` utility is recommended for all such event update loops and it will be used instead of explicit loops in the rest of the user guides involving streams.


### Using ``next()``

The approach shown above of using an empty stream works in an exactly analogous fashion for callables that take no arguments. In both cases, the ``DynamicMap`` ``next()`` method is enabled:


```python
hv.HoloMap({i:next(dmap) for i in range(10)}, kdims='Iteration')
```

## Next steps

The streams system allows you to update plots in place making it possible to build live visualizations that update in response to incoming live data or any other type of event. As we have seen in this user guide, you can use streams together with key dimensions to add additional interactivity to your plots while retaining the familiar widgets.

This user guide used examples that work with either the matplotlib or bokeh backends. In the [Custom Interactivity](13-Custom_Interactivity.ipynb) user guide, you will see how you can directly interact with dynamic visualizations when using the bokeh backend.

## [Advanced] How streams work



This optional section is not necessary for users who simply want to use the streams system, but it does describe how streams actually work in more detail.

A stream class is one that inherits from ``Stream`` that typically defines some new parameters. We have already seen one convenient way of defining a stream class:


```python
defineXY = Stream.define('defineXY', x=0.0, y=0.0)
```

This is equivalent to the following definition which would be more appropriate in library code or for complex stream class requiring lots of parameters that need to be documented:


```python
class XY(Stream):
    x = param.Number(default=0.0, constant=True, doc='An X position.')
    y = param.Number(default=0.0, constant=True, doc='A Y position.')
```

As we have already seen, we can make an instance of ``XY`` with some initial values for ``x`` and ``y``.


```python
xy = XY(x=2,y=3)
```

However, trying to modify these parameters directly will result in an exception as they have been declared constant (e.g ``xy.x=4`` will throw an error). This is because there are two allowed ways of modifying these parameters, the simplest one being ``update``:


```python
xy.update(x=4,y=50)
xy.rename(x='xpos', y='ypos').contents
```

This shows how you can update the parameters and also shows the correct way to view the stream parameter values via the ``contents`` property as this will apply any necessary renaming.

So far, using ``update`` has done nothing but force us to access parameter a certain way. What makes streams work are the side-effects you can trigger when changing a value via the ``event`` method. The relevant side-effect is to invoke callables called 'subscribers'

### Subscribers

Without defining any subscribes, the ``event`` method is identical to ``update``:


```python
xy = XY()
xy.event(x=4,y=50)
xy.contents
```

Now let's add a subscriber:


```python
def subscriber(xpos,ypos):
    print('The subscriber received xpos={xpos} and ypos={ypos}'.format(xpos=xpos,ypos=ypos))

xy = XY().rename(x='xpos', y='ypos')
xy.add_subscriber(subscriber)
xy.event(x=4,y=50)
```

As we can see, now when you call ``event``, our subscriber is called with the updated parameter values, renamed as appropriate. The ``event`` method accepts the original parameter names and the subscriber receives the new values after any renaming is applied. You can add as many subscribers as you want and you can clear them using the ``clear`` method:


```python
xy.clear()
xy.event(x=0,y=0)
```

When you define a ``DynamicMap`` using streams, the HoloViews plotting system installs the necessary callbacks as subscribers to update the plot when the stream parameters change. The above example clears all subscribers (it is equivalent to ``clear('all')``. To clear only the subscribers you define yourself use ``clear('user')`` and to clear any subscribers installed by the HoloViews plotting system use ``clear('internal')``.

When using linked streams as described in the [Custom Interactivity](13-Custom_Interactivity.ipynb) user guide, the plotting system recognizes the stream class and registers the necessary machinery with Bokeh to update the stream values based on direct interaction with the plot.