The Right Way to Implement an IPython Progress Bar

Among the IPython examples is one showing how to implement a progress bar. I, however, think the implementation used in the example has some severe limitations which are not mentioned. Everything displayed in an IPython notebook will be saved in the notebook. It seem that display(Javascript(code)) just executes the JavaScript code. But in fact it stores that code invisibly in the notebook and adds an invisible <div> to the notebook.

Why is that a problem? If you have a long running process, each update of the progress bar increases the notebook’s size. Thus, it does not just waste disk space, but it might grow that much that your browser crashes. For short running processes or just a few updates with Javascript, this will not be a problem. But in Nengo, a neural network simulation software, it was. Unfortunately there is not really anything you can do about this in IPython versions prior to 2.0.

Luckily, IPython 2.0 introduced widgets allowing it to implement a progress bar without that flaw. First we need a Python class for our widget:

from IPython.html import widgets
import IPython.utils.traitlets as traitlets

class ProgressBar(widgets.DOMWidget):
    _view_name = traitlets.Unicode('ProgressBar', sync=True)
    progress = traitlets.Float(0., sync=True)

DOMWidget is the base class we have to use for any widget that is intended to be displayed in the notebook. In the widget class we define a number of class variables which will become attributes the widget. Each of those is an instance of a traitlet, a type-safe property. The sync keyword argument tells the traitlet to automatically synchronize with the front-end code which we will discuss next.

To JavaScript front-end code is linked by to the back-end by the _view_name traitlet. The basic structure looks like this:

require(["widgets/js/widget", "widgets/js/manager"], function(widget, manager) {
    // Make this work with both IPython 2 and 3
    if (typeof widget.DOMWidgetView == 'undefined') {
        widget = IPython;
    }
    if (typeof manager.WidgetManager == 'undefined') {
        manager = IPython;
    }

    var ProgressBar = widget.DOMWidgetView.extend({
        render: function() {
            // Code for initially displaying the progress bar
        },

        update: function() {
            // Code for updating the progress bar
        },
    });

    manager.WidgetManager.register_widget_view('ProgressBar', ProgressBar);
});

All of the code has to be wrapped in the require call. In IPython 3 (currently in development) the widget and manager will be passed to the function with the front-end code. However, in IPython 2 this in not the case. Therefore, we check first whether the values have been passed and otherwise use the IPython 2 method for access. This ensures the code will work with the current and upcoming version of IPython. After that we extend the DOMWidgetView class and add our front-end code. The final step is to register the derived class with the WidgetManager so that it gets connected to the back-end.

Now, let us fill in the actual front-end code. First we need a render function which adds the required HTML elements and sets a CSS property on the containing element:

        render: function() {
            this.$el.css({width: '100%'});
            this.$el.html([
                '<div style="',
                        'width: 100%;',
                        'border: 1px solid black;',
                        'position: relative;">',
                    '<div class="pb-bar" style="',
                            'background-color: black;',
                            'width: 0%;">',
                        '&nbsp;',
                    '</div>',
                '</div>'].join(''));
        },

The updates to the progress bar are performed by the update function:

        update: function() {
            this.$el.css({width: '100%'});
            var progress = 100 * this.model.get('progress');
            this.$el.find('div.pb-bar').width(progress.toString() + '%');
        },

First we set the width property of the containing element again because IPython might override after the first call to render. Then we read out the progress from the model (our Python widget class) and set the width of the inner div accordingly.

To use this progress bar widget, execute the front-end code as JavaScript in the IPython notebook, then instantiate and display the ProgressBar class. By setting the progress attribute of the instance the progress bar will be automatically updated. Moreover, you can do it as many times as you like without blowing up the notebook’s size.