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

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`. 

6 

7Copyright (c) 2013, Jake Vanderplas 

8All rights reserved. 

9 

10Redistribution and use in source and binary forms, with or without modification, 

11are permitted provided that the following conditions are met: 

12 

13* Redistributions of source code must retain the above copyright notice, this 

14 list of conditions and the following disclaimer. 

15 

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. 

19 

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. 

23 

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 

38 

39 

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 

51 

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>" 

64 

65 rep = ip.display_formatter.formatters['text/html'](obj) 

66 

67 if rep is not None: 

68 return rep 

69 elif hasattr(obj, '_repr_html_'): 

70 return obj._repr_html_() 

71 

72 

73class StaticInteract(object): 

74 

75 """ 

76 Static Interact Object 

77 

78 See notebook :ref:`havingaforminanotebookrst`. 

79 

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 """ 

84 

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); 

98 

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 }} 

108 

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> 

119 

120 <div> 

121 {outputs} 

122 {widgets} 

123 </div> 

124 """ 

125 

126 subdiv_template = """ 

127 <div name="{name}" style="display:{display}"> 

128 {content} 

129 </div> 

130 """ 

131 

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) 

144 

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) 

154 

155 self.widgets = OrderedDict(kwargs) 

156 self.function = function 

157 

158 def _output_html(self): 

159 """ 

160 html output 

161 

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()]) 

168 

169 # Now reorder alphabetically by names so divnames match javascript 

170 names, values, defaults = zip(*sorted(zip(names, values, defaults))) 

171 

172 results = [self.function(**dict(zip(names, vals))) 

173 for vals in itertools.product(*values)] 

174 

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)] 

179 

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)) 

185 

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())]) 

192 

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. 

197 

198 @return string 

199 """ 

200 return self.template.format(outputs=self._output_html(), 

201 widgets=self._widget_html()) 

202 

203 def _repr_html_(self): 

204 """ 

205 Synonym for :meth:`html <pyquickhelper.ipythonhelper.interact.StaticInteract.html>`. 

206 """ 

207 return self.html()