Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagecell
Path: blob/master/interact_sagecell.py
447 views
1
#########################################################################################
2
# Copyright (C) 2013 Jason Grout, Ira Hanson, Alex Kramer #
3
# #
4
# Distributed under the terms of the GNU General Public License (GPL), version 2+ #
5
# #
6
# http://www.gnu.org/licenses/ #
7
#########################################################################################
8
9
# the only reason this file is distributed under GPLv2+ is because it
10
# imports functions from Sage GPLv2+ code. The actual code in this
11
# file is under the modified BSD license, which means that if the Sage
12
# imports are replaced with BSD-compatible functions, this file can be
13
# distributed under the modified BSD license.
14
15
"""
16
Interacts
17
18
The camelcase controls (like Selector or ButtonBar) have experimental APIs and
19
may change. The stable API is still the backwards-compatible API.
20
21
Examples
22
--------
23
24
25
Radio button example::
26
27
@interact
28
def f(n = Selector(values=["Option1","Option2"], selector_type="radio", label=" ")):
29
print(n)
30
31
32
Push button example::
33
34
result = 0
35
@interact
36
def f(n = Button(text="Increment", default=0, value=1, width="10em", label=" ")):
37
global result
38
result = result + n
39
print("Result: {}".format(result))
40
41
42
Button bar example::
43
44
result = 0
45
@interact
46
def f(n = ButtonBar(values=[(1,"Increment"),(-1,"Decrement")], default=0, width="10em", label=" ")):
47
global result
48
result = result + n
49
print("Result: {}".format(result))
50
51
Multislider example::
52
53
sliders = 5
54
interval = [(0,10)]*sliders
55
default = [3]*sliders
56
@interact
57
def f(n = MultiSlider(sliders = sliders, interval = interval, default = default), c = (1,100)):
58
print("Sum of cn for all n: %s" % float(sum(c * i for i in n)))
59
60
Nested interacts::
61
62
@interact
63
def f(n=(0,10,1)):
64
print(n)
65
@interact
66
def transformation(c=(0,n)):
67
print(c)
68
69
70
Nested interacts where the number of controls is changed::
71
72
@interact
73
def f(n=(0,10,1)):
74
@interact(controls=[('x%d'%i, (0,10)) for i in range(n)])
75
def s(multiplier=2, **kwargs):
76
print(sum(kwargs.values()) * multiplier)
77
78
79
Recursively nested interact::
80
81
c=1
82
@interact
83
def f(n=(0,10,1)):
84
global c
85
c+=1
86
print('f evaluated %d times' % c)
87
for i in range(n):
88
interact(f)
89
"""
90
91
92
import inspect
93
import json
94
import sys
95
import uuid
96
97
98
from sage.misc.decorators import decorator_defaults
99
100
101
from misc import session_metadata
102
103
104
__interacts = {}
105
106
107
def update_interact(interact_id, name=None, value=None, do_update=True):
108
interact_info = __interacts[interact_id]
109
controls = interact_info["controls"]
110
proxy = interact_info["proxy"]
111
if name is not None:
112
controls[name].value = value
113
if name not in proxy._changed:
114
proxy._changed.append(str(name))
115
if do_update and (name is None or controls[name].update):
116
kwargs = {n: c.adapter(c.value) for n, c in controls.items()}
117
interact_info["function"](control_vals=kwargs)
118
for c in controls.values():
119
c.reset()
120
proxy._changed = []
121
122
def update_interact_msg(stream, ident, msg):
123
content = msg["content"]
124
interact_id = content["interact_id"]
125
for name in content["values"]:
126
if name in __interacts[interact_id]["controls"]:
127
update_interact(interact_id, name, content["values"][name], not content["update_last"])
128
if content["update_last"]:
129
update_interact(interact_id)
130
131
class InteractProxy(object):
132
133
def __init__(self, interact_id, function):
134
self.__interact_id = interact_id
135
self.__interact = globals()["__interacts"][self.__interact_id]
136
self.__function = function
137
self._changed = list(self.__interact["controls"])
138
139
def __setattr__(self, name, value):
140
if name.startswith("_"):
141
super(InteractProxy, self).__setattr__(name, value)
142
return
143
if name not in self.__interact["controls"]:
144
control = automatic_control(value, var=name)
145
self.__interact["controls"][name] = control
146
control.globals = self.__function.__globals__
147
msg = control.message()
148
msg["label"] = control.label if control.label is not None else name
149
msg["update"] = control.update = not any(
150
isinstance(c, UpdateButton) for c in self.__interact["controls"].values()
151
)
152
sys._sage_.display_message({
153
"application/sage-interact-new-control": {
154
"interact_id": self.__interact_id,
155
"name": name,
156
"control": msg
157
},
158
"text/plain": "New interact control %s" % (name,)
159
})
160
if name not in self._changed:
161
self._changed.append(name)
162
return
163
if isinstance(self.__interact["controls"][name].value, list):
164
for i,v in enumerate(value):
165
getattr(self, name)[i]=v
166
return
167
self.__interact["controls"][name].value = value
168
self.__send_update(name)
169
170
def __dir__(self):
171
items = list(self.__interact["controls"])
172
for a in self.__dict__:
173
if a.startswith("_") and not a.startswith("_InteractProxy__"):
174
items.append(a)
175
items.append("_update")
176
items.sort()
177
return items
178
179
def __getattr__(self, name):
180
if name not in self.__interact["controls"]:
181
raise AttributeError("Interact has no control '%s'" % (name,))
182
if isinstance(self.__interact["controls"][name].value, list):
183
return InteractProxy.ListProxy(self, name)
184
return self.__interact["controls"][name].value
185
186
def __delattr__(self, name):
187
del self.__interact["controls"][name]
188
sys._sage_.display_message({
189
"application/sage-interact-del-control": {
190
"interact_id": self.__interact_id,
191
"name": name
192
},
193
"text/plain": "Deleting interact control %s" % (name,)
194
})
195
if name not in self._changed:
196
self._changed.append(name)
197
198
def __call__(self, *args, **kwargs):
199
return self.__function(*args, **kwargs)
200
201
def __send_update(self, name, items={}):
202
msg = {
203
"application/sage-interact-update": {
204
"interact_id": self.__interact_id,
205
"control": name,
206
"value": self.__interact["controls"][name].value
207
},
208
"text/plain": "Sage Interact Update"
209
}
210
msg["application/sage-interact-update"].update(items)
211
sys._sage_.display_message(msg)
212
if name not in self._changed:
213
self._changed.append(name)
214
215
def _update(self):
216
update_interact(self.__interact_id)
217
218
def _state(self, state=None):
219
if state is None:
220
return {k:v.value for k,v in self.__interact["controls"].items()}
221
else:
222
for k,v in state.items():
223
setattr(self, k, v)
224
225
def _bookmark(self, name, state=None):
226
if state is None:
227
state = self._state()
228
else:
229
state = {n: self.__interact["controls"][n].constrain(v) for n, v in state.items()}
230
msg = {
231
"application/sage-interact-bookmark": {
232
"interact_id": self.__interact_id,
233
"name": name,
234
"values": state
235
},
236
"text/plain": "Creating bookmark %s" % (name,)
237
}
238
sys._sage_.display_message(msg)
239
240
def _set_bookmarks(self, bookmarks):
241
if isinstance(bookmarks, str):
242
bookmarks = json.loads(bookmarks)
243
for name, state in bookmarks:
244
self._bookmark(name, state)
245
246
class ListProxy(object):
247
def __init__(self, iproxy, name, index=[]):
248
self.iproxy = iproxy
249
self.name = name
250
self.control = self.iproxy._InteractProxy__interact["controls"][self.name]
251
self.list = self.control.value
252
self.index = index[:]
253
for i in self.index:
254
self.list = self.list[i]
255
256
def __getitem__(self, index):
257
if isinstance(self.list[index], list):
258
return InteractProxy.ListProxy(self.iproxy, self.name, self.index + [int(index)])
259
return self.list[index]
260
261
def __setitem__(self, index, value):
262
if isinstance(index, slice):
263
raise TypeError("object does not support slice assignment")
264
if isinstance(self.list[index], list):
265
for i,v in enumerate(value):
266
self[index][i] = v
267
return
268
index = int(index)
269
self.list[index] = self.control.constrain_elem(value, index)
270
self.iproxy._InteractProxy__send_update(self.name, {
271
"value": self.list[index],
272
"index": self.index + [index]
273
})
274
if self.name not in self.iproxy._changed:
275
self.iproxy._changed.append(self.name)
276
277
def __len__(self):
278
return len(self.list)
279
280
def __repr__(self):
281
return "[%s]" % (", ".join(repr(e) for e in self.list),)
282
283
284
try:
285
sys._sage_.register_handler("sagenb.interact.update_interact", update_interact_msg)
286
except AttributeError:
287
pass
288
289
290
@decorator_defaults
291
def interact(f, controls=None, update=None, layout=None, locations=None,
292
output=True, readonly=False, automatic_labels=True):
293
"""
294
A decorator that creates an interact.
295
296
Each control can be given as an :class:`.InteractControl` object
297
or a value, defined in :func:`.automatic_control`, that will be
298
interpreted as the parameters for some control.
299
300
The decorator can be used in several ways::
301
302
@interact([name1, (name2, control2), (name3, control3)])
303
def f(**kwargs):
304
...
305
306
@interact
307
def f(name1, name2=control2, name3=control3):
308
...
309
310
311
The two ways can also be combined::
312
313
@interact([name1, (name2, control2)])
314
def f(name3, name4=control4, name5=control5, **kwargs):
315
...
316
317
In each example, ``name1``, with no associated control,
318
will default to a text box.
319
320
If ``output=False``, then changed controls will not be
321
highlighted.
322
323
:arg function f: the function to make into an interact
324
:arg list controls: a list of tuples of the form ``("name",control)``
325
:arg boolean output: whether any output should be shown
326
:returns: the original function
327
:rtype: function
328
"""
329
if isinstance(f, InteractProxy):
330
f = f._InteractProxy__function
331
332
controls = [] if controls is None else list(controls)
333
for i, name in enumerate(controls):
334
if isinstance(name, str):
335
controls[i] = (name, None)
336
elif not isinstance(name[0], str):
337
raise ValueError(
338
"interact control must have a string name, "
339
"but %r isn't a string" % name[0])
340
params = list(inspect.signature(f).parameters.values())
341
if params and params[0].default is params[0].empty:
342
pass_proxy = True
343
params.pop(0)
344
else:
345
pass_proxy = False
346
controls = [(p.name, p.default if p.default is not p.empty else None)
347
for p in params] + controls
348
names = [c[0] for c in controls]
349
if len(set(names)) != len(names):
350
raise ValueError("duplicate argument in interact definition")
351
controls = {n: automatic_control(c, var=n) for n, c in controls}
352
353
update = set() if update is None else set(update)
354
for n, c in controls.items():
355
if n.startswith("_"):
356
raise ValueError("invalid control name: %s" % n)
357
if isinstance(c, UpdateButton):
358
update.add(n)
359
if not update:
360
update = names
361
for n in update:
362
controls[n].update = True
363
364
if isinstance(layout, dict):
365
rows = layout.get("top", [])
366
for pos, ctrls in layout.items():
367
if pos not in ("bottom", "top"):
368
rows.extend(ctrls)
369
if output:
370
rows.append([("_output", 1)])
371
rows.extend(layout.get("bottom", []))
372
layout = rows
373
elif layout is None:
374
layout = []
375
376
if locations is True:
377
locations = "" # empty prefix
378
if isinstance(locations, str):
379
prefix = '#' + locations
380
locations = {n: prefix + n for n in names + ["_output", "_bookmarks"]}
381
382
placed = set()
383
if locations:
384
placed.update(locations)
385
if layout:
386
for row in layout:
387
for i, c in enumerate(row):
388
if not isinstance(c, (list, tuple)):
389
c = (c, 1)
390
row[i] = c = (c[0], int(c[1]))
391
if c[0] is not None:
392
if c[0] in placed:
393
raise ValueError("duplicate item %s in layout" % c[0])
394
placed.add(c[0])
395
layout.extend([(n, 1)] for n in names if n not in placed)
396
if output and "_output" not in placed:
397
layout.append([("_output", 1)])
398
399
interact_id = str(uuid.uuid4())
400
msgs = {n: c.message() for n, c in controls.items()}
401
for n, m in msgs.items():
402
if controls[n].label is not None:
403
m["label"] = controls[n].label
404
elif automatic_labels:
405
m["label"] = n
406
else:
407
m["label"]= ""
408
m["update"] = controls[n].update
409
msg = {
410
"application/sage-interact": {
411
"new_interact_id": interact_id,
412
"controls": msgs,
413
"layout": layout,
414
"locations": locations,
415
"readonly": readonly,
416
},
417
"text/plain": "Sage Interact"
418
}
419
sys._sage_.display_message(msg)
420
sys._sage_.reset_kernel_timeout(float('inf'))
421
422
def adapted_f(control_vals):
423
args = [__interacts[interact_id]["proxy"]] if pass_proxy else []
424
with session_metadata({'interact_id': interact_id}):
425
sys._sage_.clear(__interacts[interact_id]["proxy"]._changed)
426
try:
427
returned = f(*args, **control_vals)
428
except:
429
print("Interact state: %r"
430
% (__interacts[interact_id]["proxy"]._state()))
431
raise
432
return returned
433
434
# update global __interacts
435
__interacts[interact_id] = {
436
"function": adapted_f,
437
"controls": controls,
438
"update": update
439
}
440
for n, c in controls.items():
441
c.globals = f.__globals__
442
proxy = InteractProxy(interact_id, f)
443
__interacts[interact_id]["proxy"] = proxy
444
update_interact(interact_id)
445
return proxy
446
447
448
def safe_sage_eval(code, globs):
449
"""
450
Evaluate an expression using sage_eval,
451
returning an ``Exception`` object if an exception occurs
452
453
:arg str code: the expression to evaluate
454
:arg dict globs: the global dictionary in which to evaluate the expression
455
:returns: the value of the expression. If an exception occurs, the
456
``Exception`` object is returned instead.
457
"""
458
try:
459
try:
460
from sage.all import sage_eval
461
return sage_eval(code, globs)
462
except ImportError:
463
return eval(code, globs)
464
except Exception as e:
465
return e
466
467
class InteractControl(object):
468
"""
469
Base class for all interact controls.
470
471
:arg default: the default value for the control
472
:arg function adapter: a function that will be called on the value every
473
time the interact is evaluated. This function should take one argument
474
(the value) and return a value that will be passed to the function.
475
"""
476
477
def __init__(self, default, label, adapter=None):
478
self.value = default
479
self.label = label
480
self.update = False
481
self.adapter = adapter if adapter is not None else lambda value: value
482
483
def __setattr__(self, name, value):
484
super(InteractControl, self).__setattr__(name, self.constrain(value) if name == "value" else value)
485
486
def message(self):
487
"""
488
Get a control configuration message for an
489
``interact_prepare`` message
490
491
:returns: configuration message
492
:rtype: dict
493
"""
494
raise NotImplementedError
495
496
def constrain(self, value):
497
"""
498
A function that is called on each value to which the control is set.
499
This is called once, whenever the value is set, and may be overriden
500
by a decendant of this class.
501
502
:arg value: the value to constrain
503
:returns: the constrained value to be stored
504
"""
505
return value
506
507
def reset(self):
508
"""
509
This method is called after every interact update.
510
"""
511
pass
512
513
class Checkbox(InteractControl):
514
"""
515
A checkbox control
516
517
:arg bool default: ``True`` if the checkbox is checked by default
518
:arg bool raw: ``True`` if the value should be treated as "unquoted"
519
(raw), so it can be used in control structures. There are few
520
conceivable situations in which raw should be set to ``False``,
521
but it is available.
522
:arg str label: the label of the control, ``""`` for no label, and
523
a default value (None) of the control's variable.
524
"""
525
def __init__(self, default=True, label=None, raw=True):
526
self.raw=raw
527
super(Checkbox, self).__init__(default, label)
528
529
def message(self):
530
"""
531
Get a checkbox control configuration message for an
532
``interact_prepare`` message
533
534
:returns: configuration message
535
:rtype: dict
536
"""
537
return {
538
'control_type':'checkbox',
539
'default':self.value,
540
'raw':self.raw
541
}
542
543
def constrain(self, value):
544
return bool(value)
545
546
class InputBox(InteractControl):
547
"""
548
An input box control
549
550
:arg default: default value of the input box. If this is not a string, repr is
551
called on it to get a string, which is then the default input.
552
:arg int width: character width of the input box.
553
:arg int height: character height of the input box. If this is greater than
554
one, an HTML textarea will be rendered, while if it is less than one,
555
an input box form element will be rendered.
556
:arg str label: the label of the control, ``""`` for no label, and
557
a default value (None) of the control's variable.
558
:arg bool keypress: update the value of the interact when the user presses
559
any key, rather than when the user presses Enter or unfocuses the textbox
560
"""
561
def __init__(self, default=u"", label=None, width=0, height=1, keypress=False, adapter=None):
562
super(InputBox, self).__init__(default, label, adapter)
563
self.width=int(width)
564
self.height=int(height)
565
self.keypress = keypress
566
if self.height > 1:
567
self.subtype = "textarea"
568
else:
569
self.subtype = "input"
570
571
def message(self):
572
"""
573
Get an input box control configuration message for an
574
``interact_prepare`` message
575
576
:returns: configuration message
577
:rtype: dict
578
"""
579
return {'control_type':'input_box',
580
'subtype':self.subtype,
581
'default':self.value,
582
'width':self.width,
583
'height':self.height,
584
'evaluate': False,
585
'keypress': self.keypress}
586
587
def constrain(self, value):
588
return value if isinstance(value, str) else repr(value)
589
590
591
class ExpressionBox(InputBox):
592
"""
593
An ``InputBox`` whose value is the result of evaluating its contents with Sage
594
:arg default: default value of the input box. If this is not a string, repr is
595
called on it to get a string, which is then the default input.
596
:arg int width: character width of the input box.
597
:arg int height: character height of the input box. If this is greater than
598
one, an HTML textarea will be rendered, while if it is less than one,x
599
an input box form element will be rendered.
600
:arg str label: the label of the control, ``""`` for no label, and
601
a default value (None) of the control's variable.
602
:arg adapter: a callable which will be passed the input before
603
sending it into the function. This might ensure that the
604
input to the function is of a specific type, for example. The
605
function should take as input the value of the control and
606
should return something that is then passed into the interact
607
function as the value of the control.
608
"""
609
def __init__(self, default=u"0", label=None, width=0, height=1, adapter=None):
610
if adapter is not None:
611
full_adapter = lambda x: adapter(safe_sage_eval(x, self.globals))
612
else:
613
full_adapter = lambda x: safe_sage_eval(x, self.globals)
614
super(ExpressionBox, self).__init__(default, label, width, height, adapter=full_adapter)
615
616
def message(self):
617
"""
618
Get an input box control configuration message for an
619
``interact_prepare`` message
620
621
:returns: configuration message
622
:rtype: dict
623
"""
624
return {"control_type": "input_box",
625
"subtype": self.subtype,
626
"default": self.value,
627
"width": self.width,
628
"height": self.height,
629
"evaluate": True,
630
"keypress": False}
631
632
class InputGrid(InteractControl):
633
"""
634
An input grid control
635
636
:arg int nrows: number of rows in the grid
637
:arg int ncols: number of columns in the grid
638
:arg default: default values of the control. A multi-dimensional
639
list specifies the values of individual inputs; a single value
640
sets the same value to all inputs
641
:arg int width: character width of each input box
642
:arg str label: the label of the control, ``""`` for no label, and
643
a default value (None) of the control's variable.
644
:arg evaluate: whether or not the strings returned from the front end
645
are first sage_eval'd (default: ``True``).
646
:arg adapter: a callable which will be passed the input before
647
sending it into the function. This might ensure that the
648
input to the function is of a specific type, for example. The
649
function should take as input a list of lists (the value
650
of the control).
651
:arg element_adapter: a callable which takes an element value
652
and returns an adapted value. A nested list of these adapted elements
653
is what is given to the main adapter function.
654
"""
655
def __init__(self, nrows=1, ncols=1, default=u'0', width=5, label=None,
656
evaluate=True, adapter=None, element_adapter=None):
657
self.nrows = int(nrows)
658
self.ncols = int(ncols)
659
self.width = int(width)
660
self.evaluate = evaluate
661
if self.evaluate:
662
if element_adapter is not None:
663
self.element_adapter = lambda x: element_adapter(safe_sage_eval(x, self.globals))
664
else:
665
self.element_adapter = lambda x: safe_sage_eval(x, self.globals)
666
else:
667
if element_adapter is not None:
668
self.element_adapter = element_adapter
669
else:
670
self.element_adapter = lambda value: value
671
if adapter is None:
672
full_adapter = lambda x: [[self.element_adapter(i) for i in xi] for xi in x]
673
else:
674
full_adapter = lambda x: adapter([[self.element_adapter(i) for i in xi] for xi in x])
675
super(InputGrid, self).__init__(default, label, full_adapter)
676
677
def message(self):
678
"""
679
Get an input grid control configuration message for an
680
``interact_prepare`` message
681
682
:returns: configuration message
683
:rtype: dict
684
"""
685
return {'control_type': 'input_grid',
686
'nrows': self.nrows,
687
'ncols': self.ncols,
688
'default': self.value,
689
'width':self.width,
690
'raw': True,
691
'evaluate': self.evaluate}
692
693
def constrain(self, value):
694
from types import GeneratorType
695
if isinstance(value, GeneratorType):
696
return [[self.constrain_elem(next(value)) for _ in range(self.ncols)] for _ in range(self.nrows)]
697
elif not isinstance(value, (list, tuple)):
698
return [[self.constrain_elem(value) for _ in range(self.ncols)] for _ in range(self.nrows)]
699
elif not all(isinstance(entry, (list, tuple)) for entry in value):
700
return [[self.constrain_elem(value[i * self.ncols + j]) for j in range(self.ncols)] for i in range(self.nrows)]
701
return [[self.constrain_elem(v) for v in row] for row in value]
702
703
def constrain_elem(self, value, i=None):
704
return value if isinstance(value, str) else repr(value)
705
706
707
class Selector(InteractControl):
708
"""
709
A selector interact control
710
711
:arg list values: list of values from which the user can select. A value can
712
also be represented as a tuple of the form ``(value, label)``, where the
713
value is the name of the variable and the label is the text displayed to
714
the user.
715
:arg int default: initially selected item in the list of values
716
:arg string selector_type: Type of selector. Currently supported options
717
are "button" (Buttons), "radio" (Radio buttons), and "list"
718
(Dropdown list), with "list" being the default. If "list" is used,
719
``ncols`` and ``nrows`` will be ignored. If "radio" is used, ``width``
720
will be ignored.
721
:arg int nrows: number of rows of selectable objects. If this is given, it
722
must cleanly divide the number of objects, else this value will be set
723
to 1 and ``ncols`` will be set to the number of objects. If both
724
``ncols`` and ``nrows`` are given, ``nrows * ncols`` must equal the
725
number of objects, else ``nrows`` will be set to 1 and ``ncols`` will
726
be set to the number of objects.
727
:arg int ncols: number of columns of selectable objects. If this is given,
728
it must cleanly divide the number of objects, else this value will be
729
set to the number of objects and ``nrows`` will be set to 1.
730
:arg string width: CSS width of each button. This should be specified in
731
px or em.
732
:arg str label: the label of the control, ``""`` for no label, and
733
a default value (None) of the control's variable.
734
"""
735
736
def __init__(self, values, default=None, selector_type="list", nrows=None, ncols=None, width="", label=None):
737
self.selector_type=selector_type
738
self.nrows=nrows
739
self.ncols=ncols
740
self.width=str(width)
741
if self.selector_type != "button" and self.selector_type != "radio":
742
self.selector_type = "list"
743
if not(values):
744
raise ValueError("values list cannot be empty")
745
# Assign selector labels and values.
746
if all(isinstance(v, tuple) and len(v) == 2 for v in values):
747
self.values = [v[0] for v in values]
748
self.value_labels = [str(v[1]) for v in values]
749
else:
750
self.values = values[:]
751
self.value_labels = [str(v) for v in self.values]
752
default = 0 if default is None else self.values.index(default)
753
super(Selector, self).__init__(default, label, self.values.__getitem__)
754
# If not using a dropdown list,
755
# check/set rows and columns for layout.
756
if self.selector_type != "list":
757
if self.nrows is None and self.ncols is None:
758
self.nrows = 1
759
self.ncols = len(self.values)
760
elif self.nrows is None:
761
self.ncols = int(self.ncols)
762
if self.ncols <= 0:
763
self.ncols = len(values)
764
self.nrows = int(len(self.values) / self.ncols)
765
if self.ncols * self.nrows < len(self.values):
766
self.nrows = 1
767
self.ncols = len(self.values)
768
elif self.ncols is None:
769
self.nrows = int(self.nrows)
770
if self.nrows <= 0:
771
self.nrows = 1
772
self.ncols = int(len(self.values) / self.nrows)
773
if self.ncols * self.nrows < len(self.values):
774
self.nrows = 1
775
self.ncols = len(self.values)
776
else:
777
self.ncols = int(self.ncols)
778
self.nrows = int(self.nrows)
779
if self.ncols * self.nrows != len(self.values):
780
self.nrows = 1
781
self.ncols = len(self.values)
782
783
def message(self):
784
"""
785
Get a selector control configuration message for an
786
``interact_prepare`` message
787
788
:returns: configuration message
789
:rtype: dict
790
"""
791
return {'control_type': 'selector',
792
'subtype': self.selector_type,
793
'values': len(self.values),
794
'value_labels': self.value_labels,
795
'default': self.value,
796
'nrows': int(self.nrows) if self.nrows is not None else None,
797
'ncols': int(self.ncols) if self.ncols is not None else None,
798
'raw': True,
799
'width': self.width}
800
801
def constrain(self, value):
802
return int(constrain_to_range(value, 0, len(self.values) - 1))
803
804
class DiscreteSlider(InteractControl):
805
"""
806
A discrete slider interact control.
807
808
The slider value correlates with the index of an array of values.
809
810
:arg int default: initial value (index) of the slider; if ``None``, the
811
slider defaults to the 0th index. The default will be the
812
closest values to this parameter.
813
:arg list values: list of values to which the slider position refers.
814
:arg bool range_slider: toggles whether the slider should select
815
one value (False, default) or a range of values (True).
816
:arg bool display_value: toggles whether the slider value sould be displayed (default = True)
817
:arg str label: the label of the control, ``""`` for no label, and
818
a default value (None) of the control's variable.
819
"""
820
821
def __init__(self, values=[0,1], default=None, range_slider=False, display_value=True, label=None):
822
from types import GeneratorType
823
if isinstance(values, GeneratorType):
824
self.values = take(10000, values)
825
else:
826
self.values = values[:]
827
if len(self.values) < 2:
828
raise ValueError("discrete slider must have at least 2 values")
829
self.range_slider = range_slider
830
if self.range_slider:
831
default = (0, len(self.values) - 1) if default is None else \
832
[closest_index(self.values, default[i]) for i in (0, 1)]
833
else:
834
default = closest_index(self.values, default)
835
super(DiscreteSlider, self).__init__(default, label, \
836
lambda v: tuple(self.values[i] for i in v) if self.range_slider else self.values[v])
837
self.display_value = display_value
838
839
def message(self):
840
"""
841
Get a discrete slider control configuration message for an
842
``interact_prepare`` message
843
844
:returns: configuration message
845
:rtype: dict
846
"""
847
return {'control_type':'slider',
848
'subtype': 'discrete_range' if self.range_slider else 'discrete',
849
'display_value':self.display_value,
850
'default': self.value,
851
'range':[0, len(self.values)-1],
852
'values':[repr(i) for i in self.values],
853
'step':1,
854
'raw':True}
855
856
def constrain(self, value):
857
if self.range_slider:
858
return tuple(sorted(int(constrain_to_range(value[i], 0, len(self.values) - 1)) for i in (0, 1)))
859
return int(constrain_to_range(value, 0, len(self.values) - 1))
860
861
class ContinuousSlider(InteractControl):
862
"""
863
A continuous slider interact control.
864
865
The slider value moves between a range of numbers.
866
867
:arg tuple interval: range of the slider, in the form ``(min, max)``
868
:arg int default: initial value of the slider; if ``None``, the
869
slider defaults to its minimum
870
:arg int steps: number of steps the slider should have between min and max
871
:arg Number stepsize: size of step for the slider. If both step and stepsize
872
are specified, stepsize takes precedence so long as it is valid.
873
:arg str label: the label of the control, ``""`` for no label, and
874
a default value (None) of the control's variable.
875
:arg bool range_slider: toggles whether the slider should select one value
876
(default = False) or a range of values (True).
877
:arg bool display_value: toggles whether the slider value sould be displayed
878
(default = True)
879
880
Note that while "number of steps" and/or "stepsize" can be specified for the
881
slider, this is to enable snapping, rather than a restriction on the
882
slider's values. The only restrictions placed on the values of the slider
883
are the endpoints of its range.
884
"""
885
886
def __init__(
887
self, interval=(0, 100), default=None, steps=250, stepsize=0,
888
label=None, range_slider=False, display_value=True, adapter=None):
889
self.range_slider = range_slider
890
self.display_value = display_value
891
if len(interval) != 2 or interval[0] == interval[1]:
892
raise ValueError("invalid interval: %r" % (interval,))
893
self.interval = tuple(sorted((float(interval[0]), float(interval[1]))))
894
super(ContinuousSlider, self).__init__(default, label, adapter)
895
self.steps = int(steps) if steps > 0 else 250
896
self.stepsize = float(stepsize
897
if stepsize > 0 and stepsize <= self.interval[1] - self.interval[0]
898
else float(self.interval[1] - self.interval[0]) / self.steps)
899
900
def message(self):
901
"""
902
Get a continuous slider control configuration message for an
903
``interact_prepare`` message
904
905
:returns: configuration message
906
:rtype: dict
907
"""
908
return {'control_type':'slider',
909
'subtype': 'continuous_range' if self.range_slider else 'continuous',
910
'display_value':self.display_value,
911
'default':self.value,
912
'step':self.stepsize,
913
'range':[float(i) for i in self.interval],
914
'raw':True}
915
916
def constrain(self, value):
917
if self.range_slider:
918
if isinstance(value, (list, tuple)) and len(value) == 2:
919
return tuple(sorted(float(constrain_to_range(value[i], self.interval[0], self.interval[1])) for i in (0, 1)))
920
return self.interval
921
return float(constrain_to_range(value, self.interval[0], self.interval[1]))
922
923
class MultiSlider(InteractControl):
924
"""
925
A multiple-slider interact control.
926
927
Defines a bank of vertical sliders (either discrete or continuous sliders,
928
but not both in the same control).
929
930
:arg int sliders: Number of sliders to generate
931
:arg list values: Values for each value slider in a multi-dimensional list
932
for the form [[slider_1_val_1..slider_1_val_n], ... ,[slider_n_val_1,
933
.. ,slider_n_val_n]]. The length of the first dimension of the list
934
should be equivalent to the number of sliders, but if all sliders are to
935
contain the same values, the outer list only needs to contain that one
936
list of values.
937
:arg list interval: Intervals for each continuous slider in a list of tuples
938
of the form [(min_1, max_1), ... ,(min_n, max_n)]. This parameter cannot
939
be set if value sliders are specified. The length of the first dimension
940
of the list should be equivalent to the number of sliders, but if all
941
sliders are to have the same interval, the list only needs to contain
942
that one tuple.
943
:arg string slider_type: type of sliders to generate. Currently, only
944
"continuous" and "discrete" are valid, and other input defaults to
945
"continuous."
946
:arg list default: Default value of each slider. The length of the list
947
should be equivalent to the number of sliders, but if all sliders are to
948
have the same default value, the list only needs to contain that one
949
value.
950
:arg list stepsize: List of numbers representing the stepsize for each
951
continuous slider. The length of the list should be equivalent to the
952
number of sliders, but if all sliders are to have the same stepsize, the
953
list only needs to contain that one value.
954
:arg list steps: List of numbers representing the number of steps for each
955
continuous slider. Note that (as in the case of the regular continuous
956
slider), specifying a valid stepsize will always take precedence over
957
any specification of number of steps, valid or not. The length of this
958
list should be equivalent to the number of sliders, but if all sliders
959
are to have the same number of steps, the list only neesd to contain
960
that one value.
961
:arg bool display_values: toggles whether the slider values sould be
962
displayed (default = True)
963
:arg str label: the label of the control, ``""`` for no label, and
964
a default value (None) of the control's variable.
965
"""
966
967
def __init__(
968
self, sliders=1, values=[[0, 1]], interval=[(0, 1)],
969
slider_type="continuous", default=None, stepsize=[0], steps=[250],
970
display_values=True, label=None):
971
from types import GeneratorType
972
self.number = int(sliders)
973
self.slider_type = slider_type
974
self.display_values = display_values
975
if not isinstance(default, (list, tuple)):
976
default = [default]
977
if len(default) == 1:
978
default *= self.number
979
if self.slider_type == "discrete":
980
self.stepsize = 1
981
if len(values) == self.number:
982
self.values = values[:]
983
for i, v in enumerate(self.values):
984
if isinstance(v, GeneratorType):
985
self.values[i] = take(10000, i)
986
elif len(values) == 1 and len(values[0]) >= 2:
987
self.values = [values[0][:]] * self.number
988
else:
989
self.values = [[0,1]] * self.number
990
self.interval = [(0, len(self.values[i]) - 1) for i in range(self.number)]
991
default = [closest_index(self.values[i], d) for i, d in enumerate(default)]
992
super(MultiSlider, self).__init__(default, label,
993
lambda v: [self.values[i][v[i]] for i in range(self.number)])
994
else:
995
self.slider_type = "continuous"
996
if len(interval) == self.number:
997
self.interval = list(interval)
998
for i, ival in enumerate(self.interval):
999
if len(ival) != 2 or ival[0] == ival[1]:
1000
raise ValueError("invalid interval: %r" % (ival,))
1001
self.interval[i] = tuple(sorted([float(ival[0]), float(ival[1])]))
1002
elif len(interval) == 1 and len(interval[0]) == 2 and interval[0][0] != interval[0][1]:
1003
self.interval = [tuple(sorted([float(interval[0][0]), float(interval[0][1])]))] * self.number
1004
else:
1005
self.interval = [(0, 1)] * self.number
1006
super(MultiSlider, self).__init__(default, label)
1007
if len(steps) == 1:
1008
self.steps = [steps[0]] * self.number if steps[0] > 0 else [250] * self.number
1009
else:
1010
self.steps = ([int(i) if i > 0 else 250 for i in steps]
1011
if len(steps) == self.number
1012
else [250 for _ in self.interval])
1013
if len(stepsize) == self.number:
1014
self.stepsize = [float(stepsize[i])
1015
if stepsize[i] > 0
1016
and stepsize[i] <= self.interval[i][1] - self.interval[i][0]
1017
else float(self.interval[i][1] - self.interval[i][0]) / self.steps[i]
1018
for i in range(self.number)]
1019
elif len(stepsize) == 1:
1020
self.stepsize = [float(stepsize[0])
1021
if stepsize[0] > 0
1022
and stepsize[0] <= self.interval[i][1] - self.interval[i][0]
1023
else float(self.interval[i][1] - self.interval[i][0]) / self.steps[i]
1024
for i in range(self.number)]
1025
else:
1026
self.stepsize = [float(self.interval[i][1] - self.interval[i][0]) / self.steps[i]
1027
for i in self.number]
1028
1029
def message(self):
1030
"""
1031
Get a multi_slider control configuration message for an
1032
``interact_prepare`` message
1033
1034
:returns: configuration message
1035
:rtype: dict
1036
"""
1037
return_message = {'control_type':'multi_slider',
1038
'subtype':self.slider_type,
1039
'display_values':self.display_values,
1040
'sliders': self.number,
1041
'raw':True,
1042
'default':self.value,
1043
'range': self.interval,
1044
'step': self.stepsize}
1045
if self.slider_type == "discrete":
1046
return_message["values"] = [[repr(v) for v in val] for val in self.values]
1047
return return_message
1048
1049
def constrain(self, value):
1050
if isinstance(value, (list, tuple)) and len(value) == self.number:
1051
return [self.constrain_elem(v, i) for i, v in enumerate(value)]
1052
else:
1053
return [self.constrain_elem(value, i) for i in range(self.number)]
1054
1055
def constrain_elem(self, value, index):
1056
if self.slider_type == "discrete":
1057
return int(constrain_to_range(value, 0, len(self.values[index]) - 1))
1058
else:
1059
return float(constrain_to_range(value, self.interval[index][0], self.interval[index][1]))
1060
1061
class ColorSelector(InteractControl):
1062
"""
1063
A color selector interact control
1064
1065
:arg default: initial color (either as an html hex string or a Sage Color
1066
object, if sage is installed.
1067
:arg bool hide_input: Toggles whether the hex value of the color picker
1068
should be displayed in an input box beside the control.
1069
:arg bool sage_color: Toggles whether the return value should be a Sage
1070
Color object (True) or html hex string (False). If Sage is unavailable
1071
or if the user has deselected "sage mode" for the computation, this
1072
value will always end up False, regardless of whether the user specified
1073
otherwise in the interact.
1074
:arg str label: the label of the control, ``""`` for no label, and
1075
a default value (None) of the control's variable.
1076
"""
1077
1078
def __init__(self, default="#000000", hide_input=False, sage_color=True, label=None):
1079
try:
1080
from sage.plot.colors import Color
1081
self.Color = Color
1082
except ImportError:
1083
self.Color = None
1084
self.sage_color = self.Color and sage_color
1085
super(ColorSelector, self).__init__(default, label, self.Color if sage_color else None)
1086
self.hide_input = hide_input
1087
1088
def constrain(self, value):
1089
if self.Color:
1090
return self.Color(value).html_color()
1091
if isinstance(value, str):
1092
return value
1093
return "#000000"
1094
1095
def message(self):
1096
"""
1097
Get a color selector control configuration message for an
1098
``interact_prepare`` message
1099
1100
:returns: configuration message
1101
:rtype: dict
1102
"""
1103
return {
1104
"control_type": "color_selector",
1105
"default": self.value,
1106
"hide_input": self.hide_input,
1107
"raw": False
1108
}
1109
1110
class Button(InteractControl):
1111
"""
1112
A button interact control
1113
1114
:arg string text: button text
1115
:arg value: value of the button, when pressed.
1116
:arg default: default value that should be used if the button is not
1117
pushed. This **must** be specified.
1118
:arg string width: CSS width of the button. This should be specified in
1119
px or em.
1120
:arg str label: the label of the control, ``""`` for no label, and
1121
a default value (None) of the control's variable.
1122
"""
1123
1124
def __init__(self, default="", value ="", text="Button", width="", label=None):
1125
super(Button, self).__init__(
1126
False, label, lambda v: self.clicked_value if v else self.default_value)
1127
self.text = text
1128
self.width = width
1129
self.default_value = default
1130
self.clicked_value = value
1131
1132
def message(self):
1133
return {'control_type':'button',
1134
'width':self.width,
1135
'text':self.text,
1136
'raw': True,}
1137
1138
def constrain(self, value):
1139
return bool(value)
1140
1141
def reset(self):
1142
self.value = False
1143
1144
class ButtonBar(InteractControl):
1145
"""
1146
A button bar interact control
1147
1148
:arg list values: list of values from which the user can select. A value can
1149
also be represented as a tuple of the form ``(value, label)``, where the
1150
value is the name of the variable and the label is the text displayed to
1151
the user.
1152
:arg default: default value that should be used if no button is pushed.
1153
This **must** be specified.
1154
:arg int ncols: number of columns of selectable buttons. If this is given,
1155
it must cleanly divide the number of buttons, else this value will be
1156
set to the number of buttons and ``nrows`` will be set to 1.
1157
:arg int nrows: number of rows of buttons. If this is given, it must cleanly
1158
divide the total number of objects, else this value will be set to 1 and
1159
``ncols`` will be set to the number of buttosn. If both ``ncols`` and
1160
``nrows`` are given, ``nrows * ncols`` must equal the number of buttons,
1161
else ``nrows`` will be set to 1 and ``ncols`` will be set to the number
1162
of objects.
1163
:arg string width: CSS width of each button. This should be specified in
1164
px or em.
1165
:arg str label: the label of the control, ``""`` for no label, and
1166
a default value (None) of the control's variable.
1167
"""
1168
1169
def __init__(self, values=[0], default="", nrows=None, ncols=None, width="", label=None):
1170
super(ButtonBar, self).__init__(
1171
None, label, lambda v: self.default_value if v is None else self.values[int(v)])
1172
self.default_value = default
1173
self.values = values[:]
1174
self.nrows = nrows
1175
self.ncols = ncols
1176
self.width = str(width)
1177
1178
# Assign button labels and values.
1179
self.value_labels=[str(v[1]) if isinstance(v,tuple) and
1180
len(v)==2 else str(v) for v in values]
1181
self.values = [v[0] if isinstance(v,tuple) and
1182
len(v)==2 else v for v in values]
1183
1184
# Check/set rows and columns for layout
1185
if self.nrows is None and self.ncols is None:
1186
self.nrows = 1
1187
self.ncols = len(self.values)
1188
elif self.nrows is None:
1189
self.ncols = int(self.ncols)
1190
if self.ncols <= 0:
1191
self.ncols = len(values)
1192
self.nrows = int(len(self.values) / self.ncols)
1193
if self.ncols * self.nrows < len(self.values):
1194
self.nrows = 1
1195
self.ncols = len(self.values)
1196
elif self.ncols is None:
1197
self.nrows = int(self.nrows)
1198
if self.nrows <= 0:
1199
self.nrows = 1
1200
self.ncols = int(len(self.values) / self.nrows)
1201
if self.ncols * self.nrows < len(self.values):
1202
self.nrows = 1
1203
self.ncols = len(self.values)
1204
else:
1205
self.ncols = int(self.ncols)
1206
self.nrows = int(self.nrows)
1207
if self.ncols * self.nrows != len(self.values):
1208
self.nrows = 1
1209
self.ncols = len(self.values)
1210
1211
def message(self):
1212
"""
1213
Get a button bar control configuration message for an
1214
``interact_prepare`` message
1215
1216
:returns: configuration message
1217
:rtype: dict
1218
"""
1219
return {'control_type': 'button_bar',
1220
'values': len(self.values),
1221
'value_labels': self.value_labels,
1222
'nrows': self.nrows,
1223
'ncols': self.ncols,
1224
'raw': True,
1225
'width': self.width,}
1226
1227
def constrain(self, value):
1228
return None if value is None else constrain_to_range(int(value), 0, len(self.values) - 1)
1229
1230
def reset(self):
1231
self.value = None
1232
1233
class HtmlBox(InteractControl):
1234
"""
1235
An html box interact control
1236
1237
:arg string value: Html code to be inserted. This should be given in quotes.
1238
:arg str label: the label of the control, ``None`` for the control's
1239
variable, and ``""`` (default) for no label.
1240
"""
1241
def __init__(self, value="", label=""):
1242
super(HtmlBox, self).__init__(value, label)
1243
1244
def message(self):
1245
"""
1246
Get an html box control configuration message for an
1247
``interact_prepare`` message
1248
1249
:returns: configuration message
1250
:rtype: dict
1251
"""
1252
return {'control_type': 'html_box',
1253
'value': self.value,}
1254
1255
def constrain(self, value):
1256
return str(value)
1257
1258
class UpdateButton(Button):
1259
"""
1260
An update button interact control
1261
1262
:arg list update: List of vars (all of which should be quoted) that the
1263
update button updates when pressed.
1264
:arg string text: button text
1265
:arg value: value of the button, when pressed.
1266
:arg default: default value that should be used if the button is not
1267
pushed. This **must** be specified.
1268
:arg string width: CSS width of the button. This should be specified in
1269
px or em.
1270
:arg str label: the label of the control, ``None`` for the control's
1271
variable, and ``""`` (default) for no label.
1272
"""
1273
1274
def __init__(self, text="Update", value="", default="", width="", label=""):
1275
super(UpdateButton, self).__init__(default, value, text, width, label)
1276
1277
def automatic_control(control, var=None):
1278
"""
1279
Guess the desired interact control from the syntax of the parameter.
1280
1281
:arg control: Parameter value.
1282
1283
:returns: An InteractControl object.
1284
:rtype: InteractControl
1285
"""
1286
from types import GeneratorType
1287
from sage.all import parent
1288
from sage.plot.colors import Color
1289
from sage.structure.element import Matrix, Vector
1290
1291
label = None
1292
default_value = None
1293
# For backwards compatibility, we check to see if
1294
# auto_update=False as passed in. If so, we set up an
1295
# UpdateButton. This should be deprecated.
1296
if var=="auto_update" and control is False:
1297
return UpdateButton()
1298
# Checks for labels and control values
1299
for _ in range(2):
1300
if isinstance(control, tuple) and len(control) == 2 and isinstance(control[0], str):
1301
label, control = control
1302
if (isinstance(control, tuple)
1303
and len(control) == 2
1304
and isinstance(control[1], (tuple, list, GeneratorType))):
1305
# TODO: default_value isn't used effectively below in all instances
1306
default_value, control = control
1307
# Checks for interact controls that are verbosely defined
1308
if isinstance(control, InteractControl):
1309
if label:
1310
control.label = label
1311
return control
1312
if isinstance(control, str):
1313
return InputBox(default=control, label=label)
1314
if isinstance(control, bool):
1315
return Checkbox(default=control, label=label, raw=True)
1316
if isinstance(control, range):
1317
control = list(control)
1318
if isinstance(control, list):
1319
if len(control) == 1:
1320
if isinstance(control[0], (list, tuple)) and len(control[0]) == 2:
1321
buttonvalue, buttontext = control[0]
1322
else:
1323
buttonvalue, buttontext = control[0], str(control[0])
1324
return Button(value=buttonvalue, text=buttontext,
1325
default=buttonvalue, label=label)
1326
return Selector(control, default=default_value, label=label,
1327
selector_type="button" if len(control) <= 5 else "list")
1328
if isinstance(control, GeneratorType) or inspect.isgenerator(control):
1329
return DiscreteSlider(take(10000, control),
1330
default=default_value, label=label)
1331
if isinstance (control, tuple):
1332
if len(control) == 2:
1333
return ContinuousSlider(interval=(control[0], control[1]),
1334
default=default_value, label=label)
1335
if len(control) == 3:
1336
from sage.arith.srange import srange
1337
return DiscreteSlider(
1338
srange(control[0], control[1], control[2],
1339
include_endpoint=True),
1340
default=default_value, label=label)
1341
return DiscreteSlider(list(control), default=default_value, label=label)
1342
if isinstance(control, Matrix):
1343
nrows = control.nrows()
1344
ncols = control.ncols()
1345
default_value = control.list()
1346
default_value = [[default_value[j * ncols + i]
1347
for i in range(ncols)] for j in range(nrows)]
1348
return InputGrid(nrows=nrows, ncols=ncols, label=label,
1349
default=default_value, adapter=parent(control))
1350
if isinstance(control, Vector):
1351
nrows = 1
1352
ncols = len(control)
1353
default_value = [control.list()]
1354
return InputGrid(nrows=nrows, ncols=ncols, label=label,
1355
default=default_value,
1356
adapter=lambda x: parent(control)(x[0]))
1357
if isinstance(control, Color):
1358
return ColorSelector(default=control, label=label)
1359
return ExpressionBox(default=control, label=label)
1360
1361
1362
def closest_index(values, value):
1363
if value is None:
1364
return 0
1365
try:
1366
return values.index(value)
1367
except ValueError:
1368
try:
1369
return min(range(len(values)), key=lambda i: abs(value - values[i]))
1370
except TypeError:
1371
return 0
1372
1373
def constrain_to_range(v, rmin, rmax):
1374
if v is None or v < rmin:
1375
return rmin
1376
if v > rmax:
1377
return rmax
1378
return v
1379
1380
def take(n, iterable):
1381
"""
1382
Return the first n elements of an iterator as a list.
1383
1384
This is from the `Python itertools documentation <http://docs.python.org/library/itertools.html#recipes>`_.
1385
1386
:arg int n: Number of elements through which v should be iterated.
1387
:arg iterable: An iterator.
1388
1389
:returns: First n elements of iterable.
1390
:rtype: list
1391
"""
1392
1393
from itertools import islice
1394
return list(islice(iterable, n))
1395
1396
def flatten(listOfLists):
1397
"""
1398
Flatten one level of nesting
1399
1400
This is from the `Python itertools documentation <http://docs.python.org/library/itertools.html#recipes>`_.
1401
"""
1402
from itertools import chain
1403
return chain.from_iterable(listOfLists)
1404
1405
imports = {"interact": interact,
1406
"Checkbox": Checkbox,
1407
"InputBox": InputBox,
1408
"ExpressionBox": ExpressionBox,
1409
"InputGrid": InputGrid,
1410
"Selector": Selector,
1411
"DiscreteSlider": DiscreteSlider,
1412
"ContinuousSlider": ContinuousSlider,
1413
"MultiSlider": MultiSlider,
1414
"ColorSelector": ColorSelector,
1415
"Button": Button,
1416
"ButtonBar": ButtonBar,
1417
"HtmlBox": HtmlBox,
1418
"UpdateButton": UpdateButton}
1419
1420