Consider the example of a simple foreign exchange calculator app.
I can define my model using traitlets
:
from traitlets import HasTraits, Float, observe, Enum
import math
class FXModel(HasTraits):
domestic_qty = Float()
foreign_qty = Float()
fx_rate = Float(float('nan')) # in units of domestic_qty/foreign_qty
lock = Enum(['domestic', 'foreign'], default_value='domestic')
_calculating = Enum([None, 'domestic', 'foreign'], default_value=None)
def calc_foreign(self):
if not math.isnan(self.fx_rate):
self._calculating = 'foreign'
self.foreign_qty = self.domestic_qty / self.fx_rate
self._calculating = None
def calc_domestic(self):
if not math.isnan(self.fx_rate):
self._calculating = 'domestic'
self.domestic_qty = self.foreign_qty * self.fx_rate
self._calculating = None
@observe('domestic_qty')
def on_domestic(self, change):
if self._calculating is None:
self.calc_foreign()
@observe('foreign_qty')
def on_foreign(self, change):
if self._calculating is None:
self.calc_domestic()
@observe('fx_rate')
def on_fxrate(self, change):
if self.lock == 'domestic':
self.calc_foreign()
else:
self.calc_domestic()
And a corresponding simple "print" based view:
class FXView:
def __init__(self, model):
self.model = model
def show(self):
print("""
domestic_qty: {:.4g}
foreign_qty: {:.4g}
fx_rate: {:.4g}
lock: {}""".format(
self.model.domestic_qty,
self.model.foreign_qty,
self.model.fx_rate,
self.model.lock
))
Here's how it works:
>> fx_model = FXModel(domestic_qty = 100., fx_rate = 200.)
>> fx_view = FXView(fx_model)
>> fx_view.show()
domestic_qty: 100
foreign_qty: 0.5
fx_rate: 200
lock: domestic
>> fx_model.fx_rate = 195.
>> fx_view.show()
domestic_qty: 100
foreign_qty: 0.5128
fx_rate: 195
lock: domestic
I have also created a view using ipywidgets:
import ipywidgets as widgets
domestic_label = widgets.Label("Domestic quantity")
domestic_field = widgets.FloatText()
foreign_label = widgets.Label("Foreign quantity")
foreign_field = widgets.FloatText()
fx_label = widgets.Label("Exchange rate (domestic/foreign)")
fx_field = widgets.FloatText()
lock_label = widgets.Label("If rates change, keep ")
lock_field = widgets.Dropdown(options=["domestic", "foreign"])
lock_label_post = widgets.Label('fixed')
ipyview = widgets.HBox([widgets.VBox([domestic_label, foreign_label, fx_label, lock_label]),
widgets.VBox([domestic_field, foreign_field, fx_field, widgets.HBox([lock_field, lock_label_post])])])
It looks really good:
My question is; how can I "bind" my model and my ipyview together? I have some experience with enaml
where this is possible via the operator :=
and friends.
What's the best way of doing this with ipywidgets
?
Great question! I posted an answer in an ipywidgets issue: https://github.com/jupyter-widgets/ipywidgets/issues/2296
So here's how to make a custom view using ipywidget views, using the widgets from the post. The key is the link calls to bind attributes to widget values.
import ipywidgets as widgets
from traitlets import link
from IPython.display import display
class FXWidgetView:
def __init__(self, model):
self.model = model
self.domestic_label = widgets.Label("Domestic quantity")
self.domestic_field = widgets.FloatText()
self.foreign_label = widgets.Label("Foreign quantity")
self.foreign_field = widgets.FloatText()
self.fx_label = widgets.Label("Exchange rate (domestic/foreign)")
self.fx_field = widgets.FloatText()
self.lock_label = widgets.Label("If rates change, keep ")
self.lock_field = widgets.Dropdown(options=["domestic", "foreign"])
self.lock_label_post = widgets.Label('fixed')
self.ipyview = widgets.HBox([widgets.VBox([self.domestic_label, self.foreign_label, self.fx_label, self.lock_label]),
widgets.VBox([self.domestic_field, self.foreign_field, self.fx_field, widgets.HBox([self.lock_field, self.lock_label_post])])])
link((model, 'domestic_qty'), (self.domestic_field, 'value'))
link((model, 'foreign_qty'), (self.foreign_field, 'value'))
link((model, 'fx_rate'), (self.fx_field, 'value'))
link((model, 'lock'), (self.lock_field, 'value'))
def _ipython_display_(self):
display(self.ipyview)
I'll point out that Param has been advocating this sort of separation for a while, and @jbednar has pointed out that ipywidgets could implement convenience functions to support this pattern as well. I think that's a good idea - to have some simple convenience functions that are a step up from the interact functions that take a HasTraits class, introspect it, and provide default widgets for different traits for common cases.