##// END OF EJS Templates
Pad interact widgets.
Jonathan Frederic -
Show More
@@ -1,272 +1,279 b''
1 1 @widget-width: 350px;
2 2 @widget-width-short: 150px;
3 3
4 // Pad interact widgets by default.
5 .widget-interact {
6 div, input {
7 padding: 2.5px;
8 }
9 }
10
4 11 .widget-area {
5 12 /*
6 13 LESS file that styles IPython notebook widgets and the area they sit in.
7 14
8 15 The widget area typically looks something like this:
9 16 +------------------------------------------+
10 17 | widget-area |
11 18 | +--------+---------------------------+ |
12 19 | | prompt | widget-subarea | |
13 20 | | | +--------+ +--------+ | |
14 21 | | | | widget | | widget | | |
15 22 | | | +--------+ +--------+ | |
16 23 | +--------+---------------------------+ |
17 24 +------------------------------------------+
18 25 */
19 26
20 27 page-break-inside : avoid;
21 28 .hbox();
22 29
23 30 .widget-subarea {
24 31 padding : 0.44em 0.4em 0.4em 1px;
25 32 margin-left : 6px;
26 33
27 34 .border-box-sizing();
28 35 .vbox();
29 36 .box-flex2();
30 37 .align-start();
31 38 }
32 39
33 40 &.connection-problems .prompt:after {
34 41 content: @fa-var-chain-broken;
35 42 font-family: 'FontAwesome';
36 43 color: @brand-danger;
37 44 font-size: @notebook_font_size;
38 45 top: 3px;
39 46 padding: 3px;
40 47 }
41 48 }
42 49
43 50 /* THE CLASSES BELOW CAN APPEAR ANYWHERE IN THE DOM (POSSIBLEY OUTSIDE OF
44 51 THE WIDGET AREA). */
45 52
46 53 .slide-track {
47 54 /* Slider Track */
48 55 border : 1px solid #CCCCCC;
49 56 background : #FFFFFF;
50 57
51 58 .corner-all(); /* Round the corners of the slide track */
52 59 }
53 60
54 61 .widget-hslider {
55 62 /* Horizontal jQuery Slider
56 63
57 64 Both the horizontal and vertical versions of the slider are characterized
58 65 by a styled div that contains an invisible jQuery slide div which
59 66 contains a visible slider handle div. This is requred so we can control
60 67 how the slider is drawn and 'fix' the issue where the slide handle
61 68 doesn't stop at the end of the slide.
62 69
63 70 Both horizontal and vertical sliders have this div nesting:
64 71 +------------------------------------------+
65 72 | widget-(h/v)slider |
66 73 | +--------+---------------------------+ |
67 74 | | ui-slider | |
68 75 | | +------------------+ | |
69 76 | | | ui-slider-handle | | |
70 77 | | +------------------+ | |
71 78 | +--------+---------------------------+ |
72 79 +------------------------------------------+
73 80 */
74 81
75 82 /* Fix the padding of the slide track so the ui-slider is sized
76 83 correctly. */
77 84 padding-left : 8px;
78 85 padding-right : 2px;
79 86 overflow : visible;
80 87
81 88 /* Default size of the slider */
82 89 width : @widget-width;
83 90 height : 5px;
84 91 max-height : 5px;
85 92 margin-top : 13px;
86 93 margin-bottom: 10px;
87 94
88 95 /* Style the slider track */
89 96 .slide-track();
90 97
91 98 /* Make the div a flex box (makes FF behave correctly). */
92 99 .hbox();
93 100
94 101 .ui-slider {
95 102 /* Inner, invisible slide div */
96 103 border : 0px;
97 104 background : none;
98 105
99 106 .hbox();
100 107 .box-flex1();
101 108
102 109 .ui-slider-handle {
103 110 width: 12px;
104 111 height: 28px;
105 112 margin-top: -8px;
106 113 border-radius: @border-radius-base;
107 114 }
108 115
109 116 .ui-slider-range {
110 117 height : 12px;
111 118 margin-top : -4px;
112 119 background : @page-backdrop-color;
113 120 }
114 121 }
115 122 }
116 123
117 124 .widget-vslider {
118 125 /* Vertical jQuery Slider */
119 126
120 127 /* Fix the padding of the slide track so the ui-slider is sized
121 128 correctly. */
122 129 padding-bottom : 5px;
123 130 overflow : visible;
124 131
125 132 /* Default size of the slider */
126 133 width : 5px;
127 134 max-width : 5px;
128 135 height : 250px;
129 136 margin-left : 12px;
130 137
131 138 /* Style the slider track */
132 139 .slide-track();
133 140
134 141 /* Make the div a flex box (makes FF behave correctly). */
135 142 .vbox();
136 143
137 144 .ui-slider {
138 145 /* Inner, invisible slide div */
139 146 border : 0px;
140 147 background : none;
141 148 margin-left : -4px;
142 149 margin-top : 5px;
143 150
144 151 .vbox();
145 152 .box-flex1();
146 153
147 154 .ui-slider-handle {
148 155 width : 28px;
149 156 height : 12px;
150 157 margin-left : -9px;
151 158 border-radius: @border-radius-base;
152 159 }
153 160
154 161 .ui-slider-range {
155 162 width : 12px;
156 163 margin-left : -1px;
157 164 background : @page-backdrop-color;
158 165 }
159 166 }
160 167 }
161 168
162 169 .widget-text {
163 170 /* String Textbox - used for TextBoxView and TextAreaView */
164 171 width : @widget-width;
165 172 margin : 0px;
166 173 }
167 174
168 175 .widget-listbox {
169 176 /* Listbox */
170 177 width : @widget-width;
171 178 margin-bottom : 0px;
172 179 }
173 180
174 181 .widget-numeric-text {
175 182 /* Single Line Textbox - used for IntTextView and FloatTextView */
176 183 width : @widget-width-short;
177 184 margin : 0px;
178 185 }
179 186
180 187 .widget-progress {
181 188 /* Progress Bar */
182 189 margin-top: 6px;
183 190 min-width : @widget-width;
184 191
185 192 .progress-bar {
186 193 /* Disable progress bar animation */
187 194 -webkit-transition : none;
188 195 -moz-transition : none;
189 196 -ms-transition : none;
190 197 -o-transition : none;
191 198 transition : none;
192 199 }
193 200 }
194 201
195 202 .widget-combo-btn {
196 203 /* ComboBox Main Button */
197 204 /* Subtract 25px to account for the drop arrow button */
198 205 min-width : @widget-width-short - 25px;
199 206 }
200 207
201 208 .widget_item .dropdown-menu li a {
202 209 color: inherit;
203 210 }
204 211
205 212 .widget-hbox {
206 213 /* Horizontal widgets */
207 214 .hbox();
208 215
209 216 input[type="checkbox"] {
210 217 margin-top: 9px;
211 218 margin-bottom: 10px;
212 219 }
213 220
214 221 .widget-label {
215 222 /* Horizontal Label */
216 223 min-width : 10ex;
217 224 padding-right : 8px;
218 225 padding-top : 5px;
219 226 text-align : right;
220 227 vertical-align : text-top;
221 228 }
222 229
223 230 .widget-readout {
224 231 padding-left : 8px;
225 232 padding-top : 5px;
226 233 text-align : left;
227 234 vertical-align : text-top;
228 235 }
229 236 }
230 237
231 238 .widget-vbox {
232 239 /* Vertical widgets */
233 240 .vbox();
234 241
235 242
236 243 .widget-label {
237 244 /* Vertical Label */
238 245 padding-bottom : 5px;
239 246 text-align : center;
240 247 vertical-align : text-bottom;
241 248 }
242 249
243 250 .widget-readout {
244 251 /* Vertical Label */
245 252 padding-top : 5px;
246 253 text-align : center;
247 254 vertical-align : text-top;
248 255 }
249 256
250 257 }
251 258
252 259 .widget-box {
253 260 /* Box */
254 261 .border-box-sizing();
255 262 .align-start();
256 263 }
257 264
258 265 .widget-radio-box {
259 266 /* Contains RadioButtonsWidget */
260 267 .vbox();
261 268 .border-box-sizing();
262 269
263 270 padding-top: 4px;
264 271
265 272 label {
266 273 margin-top: 0px;
267 274 }
268 275 }
269 276
270 277 .widget-radio {
271 278 margin-left: 20px;
272 279 }
@@ -1,344 +1,344 b''
1 1 """Interact with functions using widgets."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 try: # Python >= 3.3
9 9 from inspect import signature, Parameter
10 10 except ImportError:
11 11 from IPython.utils.signatures import signature, Parameter
12 12 from inspect import getcallargs
13 13
14 14 from IPython.core.getipython import get_ipython
15 15 from IPython.html.widgets import (Widget, Text,
16 16 FloatSlider, IntSlider, Checkbox, Dropdown,
17 17 Box, Button, DOMWidget)
18 18 from IPython.display import display, clear_output
19 19 from IPython.utils.py3compat import string_types, unicode_type
20 20 from IPython.utils.traitlets import HasTraits, Any, Unicode
21 21
22 22 empty = Parameter.empty
23 23
24 24
25 25 def _matches(o, pattern):
26 26 """Match a pattern of types in a sequence."""
27 27 if not len(o) == len(pattern):
28 28 return False
29 29 comps = zip(o,pattern)
30 30 return all(isinstance(obj,kind) for obj,kind in comps)
31 31
32 32
33 33 def _get_min_max_value(min, max, value=None, step=None):
34 34 """Return min, max, value given input values with possible None."""
35 35 if value is None:
36 36 if not max > min:
37 37 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
38 38 value = min + abs(min-max)/2
39 39 value = type(min)(value)
40 40 elif min is None and max is None:
41 41 if value == 0.0:
42 42 min, max, value = 0.0, 1.0, 0.5
43 43 elif value == 0:
44 44 min, max, value = 0, 1, 0
45 45 elif isinstance(value, (int, float)):
46 46 min, max = (-value, 3*value) if value > 0 else (3*value, -value)
47 47 else:
48 48 raise TypeError('expected a number, got: %r' % value)
49 49 else:
50 50 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
51 51 if step is not None:
52 52 # ensure value is on a step
53 53 r = (value - min) % step
54 54 value = value - r
55 55 return min, max, value
56 56
57 57 def _widget_abbrev_single_value(o):
58 58 """Make widgets from single values, which can be used as parameter defaults."""
59 59 if isinstance(o, string_types):
60 60 return Text(value=unicode_type(o))
61 61 elif isinstance(o, dict):
62 62 return Dropdown(options=o)
63 63 elif isinstance(o, bool):
64 64 return Checkbox(value=o)
65 65 elif isinstance(o, float):
66 66 min, max, value = _get_min_max_value(None, None, o)
67 67 return FloatSlider(value=o, min=min, max=max)
68 68 elif isinstance(o, int):
69 69 min, max, value = _get_min_max_value(None, None, o)
70 70 return IntSlider(value=o, min=min, max=max)
71 71 else:
72 72 return None
73 73
74 74 def _widget_abbrev(o):
75 75 """Make widgets from abbreviations: single values, lists or tuples."""
76 76 float_or_int = (float, int)
77 77 if isinstance(o, (list, tuple)):
78 78 if o and all(isinstance(x, string_types) for x in o):
79 79 return Dropdown(options=[unicode_type(k) for k in o])
80 80 elif _matches(o, (float_or_int, float_or_int)):
81 81 min, max, value = _get_min_max_value(o[0], o[1])
82 82 if all(isinstance(_, int) for _ in o):
83 83 cls = IntSlider
84 84 else:
85 85 cls = FloatSlider
86 86 return cls(value=value, min=min, max=max)
87 87 elif _matches(o, (float_or_int, float_or_int, float_or_int)):
88 88 step = o[2]
89 89 if step <= 0:
90 90 raise ValueError("step must be >= 0, not %r" % step)
91 91 min, max, value = _get_min_max_value(o[0], o[1], step=step)
92 92 if all(isinstance(_, int) for _ in o):
93 93 cls = IntSlider
94 94 else:
95 95 cls = FloatSlider
96 96 return cls(value=value, min=min, max=max, step=step)
97 97 else:
98 98 return _widget_abbrev_single_value(o)
99 99
100 100 def _widget_from_abbrev(abbrev, default=empty):
101 101 """Build a Widget instance given an abbreviation or Widget."""
102 102 if isinstance(abbrev, Widget) or isinstance(abbrev, fixed):
103 103 return abbrev
104 104
105 105 widget = _widget_abbrev(abbrev)
106 106 if default is not empty and isinstance(abbrev, (list, tuple, dict)):
107 107 # if it's not a single-value abbreviation,
108 108 # set the initial value from the default
109 109 try:
110 110 widget.value = default
111 111 except Exception:
112 112 # ignore failure to set default
113 113 pass
114 114 if widget is None:
115 115 raise ValueError("%r cannot be transformed to a Widget" % (abbrev,))
116 116 return widget
117 117
118 118 def _yield_abbreviations_for_parameter(param, kwargs):
119 119 """Get an abbreviation for a function parameter."""
120 120 name = param.name
121 121 kind = param.kind
122 122 ann = param.annotation
123 123 default = param.default
124 124 not_found = (name, empty, empty)
125 125 if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
126 126 if name in kwargs:
127 127 value = kwargs.pop(name)
128 128 elif ann is not empty:
129 129 value = ann
130 130 elif default is not empty:
131 131 value = default
132 132 else:
133 133 yield not_found
134 134 yield (name, value, default)
135 135 elif kind == Parameter.VAR_KEYWORD:
136 136 # In this case name=kwargs and we yield the items in kwargs with their keys.
137 137 for k, v in kwargs.copy().items():
138 138 kwargs.pop(k)
139 139 yield k, v, empty
140 140
141 141 def _find_abbreviations(f, kwargs):
142 142 """Find the abbreviations for a function and kwargs passed to interact."""
143 143 new_kwargs = []
144 144 for param in signature(f).parameters.values():
145 145 for name, value, default in _yield_abbreviations_for_parameter(param, kwargs):
146 146 if value is empty:
147 147 raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
148 148 new_kwargs.append((name, value, default))
149 149 return new_kwargs
150 150
151 151 def _widgets_from_abbreviations(seq):
152 152 """Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
153 153 result = []
154 154 for name, abbrev, default in seq:
155 155 widget = _widget_from_abbrev(abbrev, default)
156 156 if not widget.description:
157 157 widget.description = name
158 158 widget._kwarg = name
159 159 result.append(widget)
160 160 return result
161 161
162 162 def interactive(__interact_f, **kwargs):
163 163 """
164 164 Builds a group of interactive widgets tied to a function and places the
165 165 group into a Box container.
166 166
167 167 Returns
168 168 -------
169 169 container : a Box instance containing multiple widgets
170 170
171 171 Parameters
172 172 ----------
173 173 __interact_f : function
174 174 The function to which the interactive widgets are tied. The **kwargs
175 175 should match the function signature.
176 176 **kwargs : various, optional
177 177 An interactive widget is created for each keyword argument that is a
178 178 valid widget abbreviation.
179 179 """
180 180 f = __interact_f
181 181 co = kwargs.pop('clear_output', True)
182 182 manual = kwargs.pop('__manual', False)
183 183 kwargs_widgets = []
184 container = Box()
184 container = Box(_dom_classes=['widget-interact'])
185 185 container.result = None
186 186 container.args = []
187 187 container.kwargs = dict()
188 188 kwargs = kwargs.copy()
189 189
190 190 new_kwargs = _find_abbreviations(f, kwargs)
191 191 # Before we proceed, let's make sure that the user has passed a set of args+kwargs
192 192 # that will lead to a valid call of the function. This protects against unspecified
193 193 # and doubly-specified arguments.
194 194 getcallargs(f, **{n:v for n,v,_ in new_kwargs})
195 195 # Now build the widgets from the abbreviations.
196 196 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
197 197
198 198 # This has to be done as an assignment, not using container.children.append,
199 199 # so that traitlets notices the update. We skip any objects (such as fixed) that
200 200 # are not DOMWidgets.
201 201 c = [w for w in kwargs_widgets if isinstance(w, DOMWidget)]
202 202
203 203 # If we are only to run the function on demand, add a button to request this
204 204 if manual:
205 205 manual_button = Button(description="Run %s" % f.__name__)
206 206 c.append(manual_button)
207 207 container.children = c
208 208
209 209 # Build the callback
210 210 def call_f(name=None, old=None, new=None):
211 211 container.kwargs = {}
212 212 for widget in kwargs_widgets:
213 213 value = widget.value
214 214 container.kwargs[widget._kwarg] = value
215 215 if co:
216 216 clear_output(wait=True)
217 217 if manual:
218 218 manual_button.disabled = True
219 219 try:
220 220 container.result = f(**container.kwargs)
221 221 except Exception as e:
222 222 ip = get_ipython()
223 223 if ip is None:
224 224 container.log.warn("Exception in interact callback: %s", e, exc_info=True)
225 225 else:
226 226 ip.showtraceback()
227 227 finally:
228 228 if manual:
229 229 manual_button.disabled = False
230 230
231 231 # Wire up the widgets
232 232 # If we are doing manual running, the callback is only triggered by the button
233 233 # Otherwise, it is triggered for every trait change received
234 234 # On-demand running also suppresses running the function with the initial parameters
235 235 if manual:
236 236 manual_button.on_click(call_f)
237 237 else:
238 238 for widget in kwargs_widgets:
239 239 widget.on_trait_change(call_f, 'value')
240 240
241 241 container.on_displayed(lambda _: call_f(None, None, None))
242 242
243 243 return container
244 244
245 245 def interact(__interact_f=None, **kwargs):
246 246 """
247 247 Displays interactive widgets which are tied to a function.
248 248 Expects the first argument to be a function. Parameters to this function are
249 249 widget abbreviations passed in as keyword arguments (**kwargs). Can be used
250 250 as a decorator (see examples).
251 251
252 252 Returns
253 253 -------
254 254 f : __interact_f with interactive widget attached to it.
255 255
256 256 Parameters
257 257 ----------
258 258 __interact_f : function
259 259 The function to which the interactive widgets are tied. The **kwargs
260 260 should match the function signature. Passed to :func:`interactive()`
261 261 **kwargs : various, optional
262 262 An interactive widget is created for each keyword argument that is a
263 263 valid widget abbreviation. Passed to :func:`interactive()`
264 264
265 265 Examples
266 266 --------
267 267 Renders an interactive text field that shows the greeting with the passed in
268 268 text.
269 269
270 270 1. Invocation of interact as a function
271 271 def greeting(text="World"):
272 272 print "Hello {}".format(text)
273 273 interact(greeting, text="IPython Widgets")
274 274
275 275 2. Invocation of interact as a decorator
276 276 @interact
277 277 def greeting(text="World"):
278 278 print "Hello {}".format(text)
279 279
280 280 3. Invocation of interact as a decorator with named parameters
281 281 @interact(text="IPython Widgets")
282 282 def greeting(text="World"):
283 283 print "Hello {}".format(text)
284 284
285 285 Renders an interactive slider widget and prints square of number.
286 286
287 287 1. Invocation of interact as a function
288 288 def square(num=1):
289 289 print "{} squared is {}".format(num, num*num)
290 290 interact(square, num=5)
291 291
292 292 2. Invocation of interact as a decorator
293 293 @interact
294 294 def square(num=2):
295 295 print "{} squared is {}".format(num, num*num)
296 296
297 297 3. Invocation of interact as a decorator with named parameters
298 298 @interact(num=5)
299 299 def square(num=2):
300 300 print "{} squared is {}".format(num, num*num)
301 301 """
302 302 # positional arg support in: https://gist.github.com/8851331
303 303 if __interact_f is not None:
304 304 # This branch handles the cases 1 and 2
305 305 # 1. interact(f, **kwargs)
306 306 # 2. @interact
307 307 # def f(*args, **kwargs):
308 308 # ...
309 309 f = __interact_f
310 310 w = interactive(f, **kwargs)
311 311 try:
312 312 f.widget = w
313 313 except AttributeError:
314 314 # some things (instancemethods) can't have attributes attached,
315 315 # so wrap in a lambda
316 316 f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
317 317 f.widget = w
318 318 display(w)
319 319 return f
320 320 else:
321 321 # This branch handles the case 3
322 322 # @interact(a=30, b=40)
323 323 # def f(*args, **kwargs):
324 324 # ...
325 325 def dec(f):
326 326 return interact(f, **kwargs)
327 327 return dec
328 328
329 329 def interact_manual(__interact_f=None, **kwargs):
330 330 """interact_manual(f, **kwargs)
331 331
332 332 As `interact()`, generates widgets for each argument, but rather than running
333 333 the function after each widget change, adds a "Run" button and waits for it
334 334 to be clicked. Useful if the function is long-running and has several
335 335 parameters to change.
336 336 """
337 337 return interact(__interact_f, __manual=True, **kwargs)
338 338
339 339 class fixed(HasTraits):
340 340 """A pseudo-widget whose value is fixed and never synced to the client."""
341 341 value = Any(help="Any Python object")
342 342 description = Unicode('', help="Any Python object")
343 343 def __init__(self, value, **kwargs):
344 344 super(fixed, self).__init__(value=value, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now