Coverage for pyquickhelper/ipythonhelper/interact.py: 85%
47 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
1"""
2@file
3@brief To add interactive widgets in a notebook and connect it to Python function,
4Source: https://github.com/jakevdp/ipywidgets, the module was modified for Python 3
5See notebook :ref:`havingaforminanotebookrst`.
7Copyright (c) 2013, Jake Vanderplas
8All rights reserved.
10Redistribution and use in source and binary forms, with or without modification,
11are permitted provided that the following conditions are met:
13* Redistributions of source code must retain the above copyright notice, this
14 list of conditions and the following disclaimer.
16* Redistributions in binary form must reproduce the above copyright notice, this
17 list of conditions and the following disclaimer in the documentation and/or
18 other materials provided with the distribution.
20* Neither the name of the {organization} nor the names of its
21 contributors may be used to endorse or promote products derived from
22 this software without specific prior written permission.
24THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
28ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
31ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34"""
35from collections import OrderedDict
36import itertools
37import base64
40def _get_html(obj):
41 """
42 Get the HTML representation of an object
43 """
44 # TODO: use displaypub to make this more general
45 from IPython import get_ipython
46 ip = get_ipython()
47 if ip is not None:
48 png_rep = ip.display_formatter.formatters['image/png'](obj)
49 else:
50 png_rep = None
52 if png_rep is not None: # pragma: no cover
53 # do not move this import to the root or
54 # you will be exposed to the issue mentioned by
55 # function fix_tkinter_issues_virtualenv
56 import matplotlib.pyplot as plt
57 if isinstance(obj, plt.Figure):
58 plt.close(obj) # keep from displaying twice
59 new_bytes = base64.b64encode(png_rep)
60 new_str = new_bytes.decode("utf8")
61 return f'<img src="data:image/png;base64,{new_str}">'
62 else:
63 return f"<p> {str(obj)} </p>"
65 rep = ip.display_formatter.formatters['text/html'](obj)
67 if rep is not None:
68 return rep
69 elif hasattr(obj, '_repr_html_'):
70 return obj._repr_html_()
73class StaticInteract(object):
75 """
76 Static Interact Object
78 See notebook :ref:`havingaforminanotebookrst`.
80 @warning In order to be fast in the notebook, the function is called for every possible
81 combination of values the controls can return. If it is a graph,
82 all graphs are generared.
83 """
85 template = """
86 <script type="text/javascript">
87 var mergeNodes = function(a, b) {{
88 return [].slice.call(a).concat([].slice.call(b));
89 }}; // http://stackoverflow.com/questions/914783/javascript-nodelist/17262552#17262552
90 function interactUpdate(div){{
91 var outputs = div.getElementsByTagName("div");
92 //var controls = div.getElementsByTagName("input");
93 var controls = mergeNodes(div.getElementsByTagName("input"), div.getElementsByTagName("select"));
94 function nameCompare(a,b) {{
95 return a.getAttribute("name").localeCompare(b.getAttribute("name"));
96 }}
97 controls.sort(nameCompare);
99 var value = "";
100 for(i=0; i<controls.length; i++){{
101 if((controls[i].type == "range") || controls[i].checked){{
102 value = value + controls[i].getAttribute("name") + controls[i].value;
103 }}
104 if(controls[i].type == "select-one"){{
105 value = value + controls[i].getAttribute("name") + controls[i][controls[i].selectedIndex].value;
106 }}
107 }}
109 for(i=0; i<outputs.length; i++){{
110 var name = outputs[i].getAttribute("name");
111 if(name == value){{
112 outputs[i].style.display = 'block';
113 }} else if(name != "controls"){{
114 outputs[i].style.display = 'none';
115 }}
116 }}
117 }}
118 </script>
120 <div>
121 {outputs}
122 {widgets}
123 </div>
124 """
126 subdiv_template = """
127 <div name="{name}" style="display:{display}">
128 {content}
129 </div>
130 """
132 @staticmethod
133 def _get_strrep(val):
134 """
135 Need to match javascript string rep
136 """
137 # TODO: is there a better way to do this?
138 if isinstance(val, str):
139 return val
140 elif val % 1 == 0:
141 return str(int(val))
142 else:
143 return str(val)
145 def __init__(self, function, **kwargs):
146 """
147 constructor
148 """
149 # TODO: implement *args (difficult because of the name thing)
150 # update names
151 keys = list(kwargs.keys())
152 for name in keys:
153 kwargs[name] = kwargs[name].renamed(name)
155 self.widgets = OrderedDict(kwargs)
156 self.function = function
158 def _output_html(self):
159 """
160 html output
162 @return string
163 """
164 names = list(self.widgets)
165 values = [widget.values() for widget in self.widgets.values()]
166 defaults = tuple([widget.default # pylint: disable=R1728
167 for widget in self.widgets.values()])
169 # Now reorder alphabetically by names so divnames match javascript
170 names, values, defaults = zip(*sorted(zip(names, values, defaults)))
172 results = [self.function(**dict(zip(names, vals)))
173 for vals in itertools.product(*values)]
175 divnames = [''.join([f'{n}{self._get_strrep(v)}'
176 for n, v in zip(names, vals)])
177 for vals in itertools.product(*values)]
178 display = [vals == defaults for vals in itertools.product(*values)]
180 tmplt = self.subdiv_template
181 return "".join(tmplt.format(name=divname,
182 display="block" if disp else "none",
183 content=_get_html(result))
184 for divname, result, disp in zip(divnames, results, display))
186 def _widget_html(self):
187 """
188 @return string
189 """
190 return "\n<br>\n".join([widget.html()
191 for name, widget in sorted(self.widgets.items())])
193 def html(self):
194 """
195 Produce the HTML output, insert results from @see me _output_html and
196 @see me _widget_html and insert it into the template.
198 @return string
199 """
200 return self.template.format(outputs=self._output_html(),
201 widgets=self._widget_html())
203 def _repr_html_(self):
204 """
205 Synonym for :meth:`html <pyquickhelper.ipythonhelper.interact.StaticInteract.html>`.
206 """
207 return self.html()