Outputs you can update by name

This notebook demonstrates the new name-based display functionality in the notebook. Previously, notebooks could only attach output to the cell that was currently being executed:


In [1]:
print("typical output")

There was no simple way to make code in one cell to write output to another cell. Now there is!

This feature is so new that we need to patch display until nteract depends on an IPython release which includes ipython/ipykernel#204 and ipython/ipython#10048. So as a temporary workaround, we will define display_with_id and its signature and behavior will be provided with display in future versions of IPython.

You do not need to understand any of the code in the next cell, just execute it so you can get to the fun stuff.


In [2]:
# a temporary patch of display for demonstration purposes
ip = get_ipython()

def display_with_id(obj, display_id=None, update=False):
    iopub = ip.kernel.iopub_socket
    session = ip.kernel.session
    data, md = ip.display_formatter.format(obj)
    transient = {'display_id': display_id}
    content = {'data': data, 'metadata': md, 'transient': transient}
    if display_id is None: content.pop('transient')  # make display_id option
    msg_type = 'update_display_data' if update else 'display_data'
    session.send(iopub, msg_type, content, parent=ip.parent_header)

display = display_with_id

The fun stuff

You made it! Pat yourself on the back and take a deep breath, the scariest part is over. The display function now has an optional display_id parameter. Let's give our next display the boring name and call it some_destination.


In [3]:
display('initial display', 'some_destination')

Ok, so far, nothing earth shattering. But what happens if you call display with the same display_id again?


In [4]:
display('spoiler alert: output updated in both', 'some_destination')

Fantastic! We have a way of mirroring output in multiple places. But what if you only want update the previously named displays, without creating a new one? Just call display with update=True, like this:


In [5]:
display('no output here, update above', 'some_destination', update=True)

Though we have been working with text so far, this also works for the all other output types. Let's make an HTML-based progress bar!


In [6]:
import os
from binascii import hexlify

class ProgressBar(object):
    def __init__(self, capacity):
        self._display_id = hexlify(os.urandom(8)).decode('ascii')
        self.capacity = capacity
        self.progress = 0
        
        
    def _repr_html_(self):
        return "<progress style='width:100%' max='{}' value='{}'></progress>".format(self.capacity, self.progress)
    
    def display(self):
        display_with_id(self, display_id=self._display_id)
    
    def update(self):
        display_with_id(self, display_id=self._display_id, update=True)

bar = ProgressBar(100)
bar.display()

The progress bar is drawn and it starts off at 0. Fill it up half way and call its update method to get a redraw.


In [7]:
bar.progress = 50
bar.update()

Now go half-way again


In [8]:
bar.progress = 75
bar.update()

Our original bar is kind of far away now, let's get another view of it below.


In [9]:
bar.display()

This is good, but it would be awesome to have a progress bar that would automatically update whenever its progress was modified - that would be truly progressive. We subclass ProgressBar and now we make progress into a Python property, which will allow us to set it and get it like an attribute, but do that using methods. In particular, whenever we assign a new value to progress, we also call update.


In [10]:
class AutoupdatingProgressBar(ProgressBar):
    @property
    def progress(self):
        return self._progress
    
    @progress.setter
    def progress(self, value):
        self._progress = value
        self.update()

In [11]:
better_bar = AutoupdatingProgressBar(100)
better_bar.display()

In [12]:
better_bar.progress = 40

Much better. No more pesky update calls. Let's make a little animation that Zeno would be proud of:


In [13]:
import time
better_bar.progress = 0
for _ in range(10):
    time.sleep(.5)
    better_bar.progress += (better_bar.capacity - better_bar.progress) / 2

You might have noticed that each ProgressBar autogenerates a random display_id which is handy if you want to have several of them.


In [14]:
num_bars = 5
bars = [AutoupdatingProgressBar(100) for _ in range(num_bars)]
for b in bars:
    b.display()

In [15]:
import random 
for x in range(40):
    time.sleep(.1)
    idx = random.randrange(num_bars)
    bars[idx].progress +=  random.randint(-2, 10)

In [16]:
for b in bars:
    b.display()