Coverage for src/jyquickhelper/jspy/render_nb_js_vis.py: 95%

60 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-02 00:05 +0200

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Renders a network in Javascript. 

5""" 

6import os 

7from .render_nb_js import RenderJS, JavascriptScriptError 

8 

9 

10class JavascriptVisError(JavascriptScriptError): 

11 """ 

12 Raised when the code does not contain what the class expects to find. 

13 """ 

14 pass 

15 

16 

17class RenderJsVis(RenderJS): 

18 """ 

19 Renders a network in a :epkg:`notebook` 

20 with :epkg:`vis.js`. 

21 """ 

22 

23 def __init__(self, js=None, local=False, width="100%", height="100%", divid=None, 

24 style=None, only_html=True, div_class=None, check_urls=True, 

25 class_vis='Network', dot=None, layout=None, direction='UD'): 

26 """ 

27 @param js (str) javascript 

28 @param local (bool) use local path to javascript dependencies 

29 @param script (str) script 

30 @param width (str) width 

31 @param height (str) height 

32 @param style (str) style (added in ``<style>...</style>``) 

33 @param divid (str|None) id of the div 

34 @param only_html (bool) use only function 

35 `display_html <http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html? 

36 highlight=display_html#IPython.display.display_html>`_ 

37 and not `display_javascript 

38 <http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html? 

39 highlight=display_html#IPython.display.display_javascript>`_ to add 

40 javascript to the page. 

41 @param div_class (str) class of the section ``div`` which will host the results 

42 of the javascript 

43 @param check_urls (bool) by default, check url exists 

44 @param class_vis (str) visualization class (*Network*, *Timeline*, ...) 

45 @param dot (str) either *js* or *dot* must be specified. 

46 @param layout (str) layout see `layout <http://visjs.org/docs/network/layout.html>`_ 

47 @param direction (str) if ``layout=='hiearchical'``, a string among 

48 `'UD'`, `'DU'`, `'LR'`, `'RL'`. 

49 

50 The script must defined variables *options* and *data* if 

51 ``class_vis=='Network'``. 

52 """ 

53 script = RenderJsVis._build_script( 

54 js, dot, layout=layout, direction=direction) 

55 libs, css = RenderJsVis._get_libs_css(local, class_vis) 

56 RenderJS.__init__(self, script, width=width, height=height, divid=divid, 

57 only_html=only_html, div_class=div_class, check_urls=True, 

58 libs=libs, css=css, local=local) 

59 

60 @staticmethod 

61 def _get_libs_css(local, class_vis): 

62 """ 

63 Returns the dependencies. 

64 

65 @param local use local file (True) or remote urls (False) 

66 @param lite use lite version 

67 @return tuple *(libs, css)* 

68 """ 

69 if class_vis == 'Network': 

70 libs = [ # 'vis-timeline-graph2d.min.js', 

71 # 'vis-network.min.js', 

72 # 'vis-graph3d.min.js', 

73 'vis.min.js' 

74 ] 

75 css = [ # 'vis-timeline-graph2d.min.css', 

76 'vis-network.min.css', 

77 'vis.min.css' 

78 ] 

79 else: 

80 raise NotImplementedError( 

81 f"Unable to generate a script for class '{class_vis}'") 

82 

83 if local: 

84 this = os.path.dirname(__file__) 

85 libs = [dict(path=os.path.join(this, '..', 'js', 'visjs', j), 

86 name=j.split('.')[0]) for j in libs] # pylint: disable=C0207 

87 css = [os.path.join(this, '..', 'js', 'visjs', j) for j in css] 

88 else: 

89 libs = [dict(path='http://www.xavierdupre.fr/js/visjs/' + j, 

90 name=j.split('.')[0]) for j in libs] # pylint: disable=C0207 

91 css = ['http://www.xavierdupre.fr/js/visjs/' + j for j in css] 

92 return libs, css 

93 

94 @staticmethod 

95 def _build_script(js, dot, **options): 

96 """ 

97 Builds the javascript script. 

98 

99 @param js javascript 

100 @param dot dot scripts 

101 @param options graph options 

102 @return javascript 

103 """ 

104 if js is None: 

105 if dot is None: 

106 raise ValueError("js or dot must be specified") 

107 else: 

108 dot = dot.replace("'", "\\'").replace("\n", "\\n") 

109 jsadd = """ 

110 var DOTstring = '%s'; 

111 var parsedData = vis.network.convertDot(DOTstring); 

112 var data = { nodes: parsedData.nodes, edges: parsedData.edges }; 

113 var options = parsedData.options; 

114 """.replace(" ", "") % dot 

115 else: 

116 if dot is not None: 

117 raise ValueError("js or dot must be specified not both") 

118 jsadd = js 

119 

120 if options or 'var options =' not in jsadd: 

121 opts = {} 

122 if 'layout' in options and options['layout'] is not None: 

123 opts['layout'] = {options['layout']: {'direction': options.get('direction', 'UD'), 

124 'sortMethod': "directed"}} 

125 else: 

126 opts = {k: v for k, v in options.items() if v is not None} 

127 st = f'var options = {RenderJsVis._options2js(opts)};' 

128 jsadd += "\n" + st + "\n" 

129 

130 checks = ['var data =', 'var options ='] 

131 for ch in checks: 

132 if ch not in jsadd: 

133 raise JavascriptVisError( 

134 f"String '{ch}' was not found in\n{js}") 

135 script = jsadd + "\nvar container = document.getElementById('__ID__');" + \ 

136 "\nvar network = new vis.Network(container, data, options);\n" 

137 return script 

138 

139 @staticmethod 

140 def _options2js(data): 

141 """ 

142 Converts *data* into a string. 

143 """ 

144 rows = ['{'] 

145 for k, v in data.items(): 

146 if k is None: 

147 raise ValueError("k cannot be None") 

148 rows.append(k) 

149 rows.append(':') 

150 if isinstance(v, dict): 

151 rows.append(RenderJsVis._options2js(v)) 

152 else: 

153 rows.append(f'"{v}"') 

154 rows.append(', ') 

155 rows.append('}') 

156 return "".join(rows)