Updatable Displays

Note: This feature requires notebook >= 5.0 or JupyterLab, and

IPython 6 implements a new API as part of the Jupyter Protocol version 5.1 for easily updating displays.

When you display something, you can now pass a display_id argument to attach an id to that output.

Any future display with the same ID will also update other displays that had the same ID.

display with a display_id will return a DisplayHandle object, which gives you easy access to update the output:

In [10]:
from IPython.display import display, update_display
In [13]:
handle = display('x', display_id='update-me')
handle
'z'
Out[13]:
<DisplayHandle display_id=update-me>

When we call handle.display('y'), we get a new display of 'y', but in addition to that, we updated the previous display.

In [14]:
handle.display('y')
'z'

We can also just update the existing displays, without creating a new display:

In [15]:
handle.update('z')

You don't have to generate display_ids yourself, if you specify display_id=True, then a unique ID will be assigned:

In [16]:
handle = display("hello", display_id=True)
handle
'hello'
Out[16]:
<DisplayHandle display_id=07fc47b2ef652ccb70addeee3eb0981a>

Calling handle.display(obj) is the same as calling display(obj, handle.display_id), so you don't need to use the handle objects if you don't want to:

In [17]:
display('x', display_id='here');
'z'
In [18]:
display('y', display_id='here');
'z'

And just like display, there is now update_display, which is what DisplayHandle.update calls:

In [19]:
update_display('z', display_id='here')

More detailed example

One of the motivating use cases for this is simple progress bars.

Here is an example ProgressBar using these APIs:

In [35]:
import os
from binascii import hexlify

class ProgressBar(object):
    def __init__(self, capacity):
        self.progress = 0
        self.capacity = capacity
        self.html_width = '60ex'
        self.text_width = 60
        self._display_id = hexlify(os.urandom(8)).decode('ascii')
        
    def __repr__(self):
        fraction = self.progress / self.capacity
        filled = '=' * int(fraction * self.text_width)
        rest = ' ' * (self.text_width - len(filled))
        return '[{}{}] {}/{}'.format(
            filled, rest,
            self.progress, self.capacity,
        )
    
    def _repr_html_(self):
        return """<progress
            value={progress}
            max={capacity}
            style="width: {width}"/>
            {progress} / {capacity}
        """.format(
            progress=self.progress,
            capacity=self.capacity,
            width=self.html_width,
        )
    
    def display(self):
        display(self, display_id=self._display_id)
    
    def update(self):
        update_display(self, display_id=self._display_id)

bar = ProgressBar(10)
bar.display()
10 / 10

And the ProgressBar has .display and .update methods:

In [36]:
import time

bar.display()

for i in range(11):
    bar.progress = i
    bar.update()
    time.sleep(0.25)
10 / 10

We would encourage any updatable-display objects that track their own display_ids to follow-suit with .display() and .update() or .update_display() methods.