Based on this thread.
NOTE: This is built for the Notebook in IPython 3.0/master
from IPython.html.widgets import (
FlexBox, VBox, HBox, HTML, Box, RadioButtons,
FloatText, Dropdown, Checkbox, Image, IntSlider, Button,
)
from IPython.utils.traitlets import (
link, Unicode, Float, Int, Enum, Bool,
)
Use OrderedDict
for predictable display of key-value pairs.
from collections import OrderedDict
CSS helps keep your code concise, as well as make it easier to extend/override.
%%html
<style>
/*
This contents of this would go in a separate CSS file.
Note the namespacing: this is important for two reasons.
1) doesn't pollute the global namespace
2) is _more specific_ than the base styles.
*/
.widget-area .spectroscopy .panel-body{
padding: 0;
}
.widget-area .spectroscopy .widget-numeric-text{
width: 5em;
}
.widget-area .spectroscopy .widget-box.start{
margin-left: 0;
}
.widget-area .spectroscopy .widget-hslider{
width: 12em;
}
</style>
These few classes wrap up some Bootstrap components: these will be more consistent then coding up your own.
class PanelTitle(HTML):
def __init__(self, *args, **kwargs):
super(PanelTitle, self).__init__(*args, **kwargs)
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("panel-heading panel-title")
class PanelBody(Box):
def __init__(self, *args, **kwargs):
super(PanelBody, self).__init__(*args, **kwargs)
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("panel-body")
class ControlPanel(Box):
# A set of related controls, with an optional title, in a box (provided by CSS)
def __init__(self, title=None, *args, **kwargs):
super(ControlPanel, self).__init__(*args, **kwargs)
# add an option title widget
if title is not None:
self.children = [
PanelTitle(value=title),
PanelBody(children=self.children)
]
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("panel panel-info")
This notional Spectrogram
shows how one might make a widget that redraws based on the state of its data. By defining its external API, including allowed and default values, in the form of linked traitlets, it can be reused without replumbing any events, while a few simple methods like draw
make sure it is still easy to use in a programmatic way.
import re
from datetime import datetime
class Spectrogram(HTML):
"""
A notional "complex widget" that knows how to redraw itself when key properties change.
"""
# Utility
DONT_DRAW = re.compile(r'^(_.+|value|keys|comm|children|visible|parent|log|config|msg_throttle)$')
# Lookup tables: this would be a nice place to add i18n, perhaps
CORRELATION = OrderedDict([(x, x) for x in ["synchronous", "asynchronous", "modulus", "argument"]])
DRAW_MODE = OrderedDict([(x, x) for x in ["color", "black & white", "contour"]])
SPECTRUM_SCALE = OrderedDict([(x, x) for x in ["auto", "manual"]])
SPECTRUM_DIRECTIONS = OrderedDict([(x, x) for x in ["left", "right", "bottom", "top"]])
# pass-through traitlets
correlation = Enum(CORRELATION.values(), default_value=CORRELATION.values()[0], sync=True)
draw_mode = Enum(DRAW_MODE.values(), default_value=DRAW_MODE.values()[0], sync=True)
spectrum_direction_left = Float(1000, sync=True)
spectrum_direction_right = Float(1000, sync=True)
spectrum_direction_bottom = Float(1000, sync=True)
spectrum_direction_top = Float(1000, sync=True)
spectrum_contours = Int(4, sync=True)
spectrum_zmax = Float(0.0566468618, sync=True)
spectrum_scale = Enum(SPECTRUM_SCALE, default_value=SPECTRUM_SCALE.values()[0], sync=True)
axis_x = Float(50, sync=True)
axis_y = Float(50, sync=True)
axis_display = Bool(True, sync=True)
def __init__(self, *args, **kwargs):
"""
Creates a spectrogram
"""
super(Spectrogram, self).__init__(*args, **kwargs)
self.on_trait_change(lambda name, old, new: self.draw(name, old, new))
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("col-xs-9")
self.draw()
def draw(self, name=None, old=None, new=None):
if name is not None and self.DONT_DRAW.match(name):
return
value = "<h2>Imagine a picture here, drawn with...</h2>"
if name is None:
value += '<div class="alert alert-info">redraw forced at %s!</div>' % (
datetime.now().isoformat(' ')
)
value += "\n".join([
'<p><span class="label label-%s">%s</span> %s</p>' % (
'success' if traitlet == name else 'default',
traitlet,
getattr(self, traitlet)
)
for traitlet in sorted(self.trait_names())
if not self.DONT_DRAW.match(traitlet)
])
self.value = value
The actual GUI. Note that the individual components of the view are responsible for:
class Spectroscopy(Box):
"""
An example GUI for a spectroscopy application.
Note that `self.graph` is the owner of all of the "real" data, while this
class handles creating all of the GUI controls and links. This ensures
that the Graph itself remains embeddable and rem
"""
def __init__(self, graph=None, graph_config=None, *args, **kwargs):
self.graph = graph or Spectrogram(**(graph_config or {}))
# Create a GUI
kwargs["orientation"] = 'horizontal'
kwargs["children"] = [
self._controls(),
VBox(children=[
self._actions(),
self.graph
])
]
super(Spectroscopy, self).__init__(*args, **kwargs)
self.on_displayed(self.displayed)
def displayed(self, _):
# namespace and top-level bootstrap
self.add_class("spectroscopy row")
def _actions(self):
redraw = Button(description="Redraw")
redraw.on_click(lambda x: self.graph.draw())
return HBox(children=[redraw])
def _controls(self):
panels = VBox(children=[
HBox(children=[
self._correlation(),
self._draw_mode(),
]),
self._spectrum(),
self._axes()
])
panels.on_displayed(lambda x: panels.add_class("col-xs-3"))
return panels
def _correlation(self):
# create correlation controls. NOTE: should only be called once.
radios = RadioButtons(values=self.graph.CORRELATION)
link((self.graph, "correlation"), (radios, "value"))
return ControlPanel(title="correlation", children=[radios])
def _draw_mode(self):
# create draw mode controls. NOTE: should only be called once.
radios = RadioButtons(values=self.graph.DRAW_MODE)
link((self.graph, "draw_mode"), (radios, "value"))
return ControlPanel(title="draw", children=[radios])
def _spectrum(self):
# create spectrum controls. NOTE: should only be called once.
directions = []
for label in self.graph.SPECTRUM_DIRECTIONS:
direction = FloatText(description=label, value=1000.0)
link((self.graph, "spectrum_direction_" + label), (direction, "value"))
directions.append(direction)
direction_rows = [HBox(children=directions[x::2]) for x in range(2)]
contour = IntSlider(description="contours", min=1)
link((self.graph, "spectrum_contours"), (contour, "value"))
zmax = FloatText(description="z-max")
link((self.graph, "spectrum_zmax"), (zmax, "value"))
scale = RadioButtons(description="scale", values=self.graph.SPECTRUM_SCALE)
link((self.graph, "spectrum_scale"), (scale, "value"))
return ControlPanel(title="spectrum",
children=direction_rows + [
contour,
zmax,
scale
]
)
def _axes(self):
# create spectrum controls. NOTE: should only be called once.
axis_x = FloatText(description="X div.")
link((self.graph, "axis_x"), (axis_x, "value"))
axis_y = FloatText(description="Y div.")
link((self.graph, "axis_y"), (axis_y, "value"))
axes = HBox(children=[axis_x, axis_y])
axis_display = Checkbox(description="display")
link((self.graph, "axis_display"), (axis_display, "value"))
return ControlPanel(title="axes",
children=[
axis_display,
axes
]
)
Hooray, everything is defined, now we can try this out!
spectrogram = Spectrogram()
spectrogram
Its traits can be updated directly, causing immediate update:
spectrogram.axis_display = False
The graph can be passed directly to the interactive GUI, sharing the same data between the two views.
gui = Spectroscopy(graph=spectrogram)
gui