gismap 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gismap/__init__.py CHANGED
@@ -14,6 +14,7 @@ from gismap.search import (
14
14
  search_to_html as search_to_html,
15
15
  search_to_text as search_to_text,
16
16
  )
17
+ from gismap.gisgraphs.widget import GismapWidget as GismapWidget
17
18
 
18
19
 
19
20
  infos = metadata(__name__)
File without changes
@@ -0,0 +1,105 @@
1
+ import json
2
+ import uuid
3
+ from domonic import tags
4
+
5
+ from gismap.gisgraphs.graph import lab_to_graph
6
+ from gismap.gisgraphs.groups import auto_groups
7
+ from gismap.gisgraphs.options import (
8
+ physics as def_physics,
9
+ nodes as def_nodes,
10
+ edges as def_edges,
11
+ interaction as def_interaction,
12
+ )
13
+ from gismap.gisgraphs.style import default_style
14
+ from gismap.gisgraphs.js import default_script
15
+ from gismap.gisgraphs.groups import make_legend
16
+
17
+
18
+ default_vis_url = '"https://unpkg.com/vis-network/standalone/esm/vis-network.min.js"'
19
+
20
+ gislink = tags.a(
21
+ "© GisMap 2025",
22
+ _href="https://balouf.github.io/gismap/",
23
+ _target="_blank",
24
+ _class="watermark gislink",
25
+ )
26
+
27
+
28
+ def make_vis(lab, **kwargs):
29
+ """
30
+ Parameters
31
+ ----------
32
+ lab: :class:`~gismap.lab.labmap.LabMap`
33
+ Lab to display.
34
+
35
+ Other parameters
36
+ ----------------
37
+ uid: :class:`str`
38
+ Unique identifier.
39
+ vis_url: :class:`str`
40
+ Location of visJS network.
41
+
42
+ Returns
43
+ -------
44
+ :class:`str`
45
+ HTML code.
46
+ """
47
+ uid = kwargs.pop("uid", None)
48
+ if uid is None:
49
+ uid = str(uuid.uuid4())[:8]
50
+ vis_url = kwargs.pop("vis_url", default_vis_url)
51
+ groups = kwargs.pop("groups", None)
52
+ groups = auto_groups(lab, groups)
53
+ draw_legend = kwargs.pop("draw_legend", len(groups) > 1)
54
+ physics = kwargs.pop("physics", def_physics)
55
+ nodes_options = kwargs.pop("nodes_options", def_nodes)
56
+ edges_options = kwargs.pop("edges_options", def_edges)
57
+ interaction_options = kwargs.pop("interaction_option", def_interaction)
58
+ options = {
59
+ "physics": physics,
60
+ "groups": groups,
61
+ "nodes": nodes_options,
62
+ "edges": edges_options,
63
+ "interaction": interaction_options,
64
+ }
65
+ style = kwargs.pop("style", default_style)
66
+ script = kwargs.pop("script", default_script)
67
+ if kwargs:
68
+ raise TypeError(f"unexpected keyword arguments: {repr(kwargs)}")
69
+
70
+ nodes, edges = lab_to_graph(lab)
71
+
72
+ parameters = {
73
+ "vis_url": vis_url,
74
+ "uid": uid,
75
+ "nodes": json.dumps(nodes),
76
+ "edges": json.dumps(edges),
77
+ "options": json.dumps(options),
78
+ }
79
+ div = tags.div(_class="gisgraph", _id=f"box-{uid}")
80
+ div.appendChild(tags.div(_id=f"vis-{uid}"))
81
+ div.appendChild(gislink)
82
+ div.appendChild(
83
+ tags.button("Redraw()", _id=f"redraw-{uid}", _class="watermark button redraw")
84
+ )
85
+ div.appendChild(
86
+ tags.button(
87
+ "Full Screen", _id=f"fullscreen-{uid}", _class="watermark button fullscreen"
88
+ )
89
+ )
90
+ comets = not all(n.get("connected") for n in nodes)
91
+ if draw_legend or comets:
92
+ div.appendChild(make_legend(groups, uid))
93
+ modal = tags.div(_class="modal", _id=f"modal-{uid}")
94
+ modal_content = tags.div(_class="modal-content")
95
+ modal_content.appendChild(
96
+ tags.span("×", _class="close", _id=f"modal-close-{uid}")
97
+ )
98
+ modal_content.appendChild(tags.div(_id=f"modal-body-{uid}"))
99
+ modal.appendChild(modal_content)
100
+ div.appendChild(modal)
101
+
102
+ style = tags.style(style.substitute(**parameters))
103
+ script = tags.script(script.substitute(**parameters), _type="module")
104
+
105
+ return "\n".join(f"{content}" for content in [div, style, script])
@@ -1,7 +1,7 @@
1
- import numpy as np
2
- from collections import defaultdict
1
+ from domonic import tags
3
2
  from itertools import combinations
4
- from gismap.lab.vis import generate_html
3
+ from collections import defaultdict
4
+ import numpy as np
5
5
 
6
6
 
7
7
  def initials(name):
@@ -17,10 +17,17 @@ def initials(name):
17
17
  Person's initials (2 letters only).
18
18
  """
19
19
  first_letters = [w[0] for w in name.split()]
20
- return first_letters[0] + first_letters[-1]
20
+ return first_letters[0].upper() + first_letters[-1].upper()
21
+
22
+
23
+ def linkify(name, url):
24
+ if url:
25
+ return f'<a href="{url}" target="_blank">{name}</a>'
26
+ else:
27
+ return f"<span>{name}</span>"
21
28
 
22
29
 
23
- def author_to_html(author):
30
+ def author_html(author):
24
31
  """
25
32
  Parameters
26
33
  ----------
@@ -41,13 +48,10 @@ def author_to_html(author):
41
48
  url = meta_url
42
49
  elif hasattr(author.sources[0], "url"):
43
50
  url = author.sources[0].url
44
- if url:
45
- return f'<a href="{url.strip()}" target="_blank">{name}</a>'
46
- else:
47
- return name
51
+ return linkify(name, url)
48
52
 
49
53
 
50
- def publication_to_html(pub):
54
+ def pub_html(pub):
51
55
  """
52
56
  Parameters
53
57
  ----------
@@ -62,16 +66,12 @@ def publication_to_html(pub):
62
66
  url = getattr(pub, "url", None)
63
67
  if url is None and hasattr(pub, "sources"):
64
68
  url = getattr(pub.sources[0], "url", None)
65
- if url:
66
- title_html = f'<a href="{url}" target="_blank">{pub.title}</a>'
67
- else:
68
- title_html = pub.title
69
+ title_html = linkify(pub.title, url)
69
70
 
70
71
  # Authors: render in order, separated by comma
71
- author_html_list = [
72
- author_to_html(author) for author in getattr(pub, "authors", [])
73
- ]
74
- authors_html = ", ".join(author_html_list)
72
+ authors_html = ", ".join(
73
+ [author_html(author) for author in getattr(pub, "authors", [])]
74
+ )
75
75
 
76
76
  # Venue, Year
77
77
  venue = getattr(pub, "venue", "")
@@ -82,9 +82,14 @@ def publication_to_html(pub):
82
82
  return html.strip()
83
83
 
84
84
 
85
- def publications_list_html(publications, n=10):
86
- """
85
+ expand_script = """var elts = this.parentElement.parentElement.querySelectorAll('.extra-publication');
86
+ for (var i = 0; i < elts.length; ++i) {elts[i].style.display = 'list-item';}
87
+ this.parentElement.style.display = 'none';
88
+ return false;"""
89
+
87
90
 
91
+ def publications_list(publications, n=10):
92
+ """
88
93
  Parameters
89
94
  ----------
90
95
  publications: :class:`list` of :class:`~gismap.sources.models.Publication`
@@ -94,38 +99,19 @@ def publications_list_html(publications, n=10):
94
99
 
95
100
  Returns
96
101
  -------
97
- :class:`str`
102
+ :class:`~domonic.html.ul`
98
103
  """
99
- list_items = []
104
+ lis = []
100
105
  for i, pub in enumerate(publications):
101
- item = publication_to_html(pub)
102
106
  if i < n:
103
- li = f"<li>{item}</li>"
107
+ lis.append(f"<li>{pub_html(pub)}</li>")
104
108
  else:
105
- li = f'<li class="extra-publication" style="display:none;">{item}</li>'
106
- list_items.append(li)
107
- ul_content = "\n".join(list_items)
108
-
109
- if len(publications) <= n:
110
- show_more_part = ""
111
- else:
112
- # Add a "Show more" link and JavaScript for toggling
113
- show_more_part = """
114
- <li>
115
- <a href="#" onclick="
116
- var elts = this.parentElement.parentElement.querySelectorAll('.extra-publication');
117
- for (var i = 0; i < elts.length; ++i) {elts[i].style.display = 'list-item';}
118
- this.parentElement.style.display = 'none';
119
- return false;">Show more…</a>
120
- </li>
121
- """
122
-
123
- html = f"""<ul>
124
- {ul_content}
125
- {show_more_part}
126
- </ul>
127
- """
128
- return html
109
+ lis.append(
110
+ f'<li class="extra-publication" style="display:none;">{pub_html(pub)}</li>'
111
+ )
112
+ if len(publications) > n:
113
+ lis.append(f'<li><a href="#" onclick="{expand_script}">Show more…</a></li>')
114
+ return "<ul>\n" + "\n".join(lis) + "</ul>\n"
129
115
 
130
116
 
131
117
  def to_node(s, node_pubs):
@@ -142,10 +128,14 @@ def to_node(s, node_pubs):
142
128
  :class:`dict`
143
129
  A display-ready representation of the searcher.
144
130
  """
131
+ overlay = tags.div()
132
+ overlay.appendChild(tags.div(f"Publications of {author_html(s)}"))
133
+ overlay.appendChild(tags.div(publications_list(node_pubs[s.key])))
134
+
145
135
  res = {
146
136
  "id": s.key,
147
137
  "hover": f"Click for details on {s.name}.",
148
- "overlay": f"<div> Publications of {author_to_html(s)}:</div><div>{publications_list_html(node_pubs[s.key])}</div>",
138
+ "overlay": f"{overlay}",
149
139
  "group": s.metadata.group,
150
140
  }
151
141
  if s.metadata.img:
@@ -175,17 +165,28 @@ def to_edge(k, v, searchers):
175
165
  A display-ready representation of the collaboration edge.
176
166
  """
177
167
  strength = 1 + np.log2(len(v))
178
- return {
168
+ overlay = tags.div()
169
+ overlay.appendChild(
170
+ tags.div(
171
+ f"Joint publications from {author_html(searchers[k[0]])} and {author_html(searchers[k[1]])}:"
172
+ )
173
+ )
174
+ overlay.appendChild(tags.div(f"{publications_list(v)}"))
175
+ res = {
179
176
  "from": k[0],
180
177
  "to": k[1],
181
178
  "hover": f"Show joint publications from {searchers[k[0]].name} and {searchers[k[1]].name}",
182
- "overlay": f"<div> Joint publications from {author_to_html(searchers[k[0]])} and {author_to_html(searchers[k[1]])}:</div><div>{publications_list_html(v)}</div>",
179
+ "overlay": f"{overlay}",
183
180
  "width": int(strength),
184
181
  "length": int(200 / strength),
185
182
  }
183
+ g1, g2 = searchers[k[0]].metadata.group, searchers[k[1]].metadata.group
184
+ if g1 and g2 and g1 != g2:
185
+ res["color"] = "rgba(0,0,0,0)"
186
+ return res
186
187
 
187
188
 
188
- def lab2graph(lab):
189
+ def lab_to_graph(lab):
189
190
  """
190
191
  Parameters
191
192
  ----------
@@ -201,18 +202,21 @@ def lab2graph(lab):
201
202
  --------
202
203
 
203
204
  >>> from gismap.lab import ListMap as Map
204
- >>> lab = Map(author_list=['Tixeuil Sébastien', 'Mathieu Fabien'], name='mini')
205
+ >>> lab = Map(author_list=['Tixeuil Sébastien', 'Mathieu Fabien'], name='mini', dbs="hal")
205
206
  >>> lab.update_authors()
206
207
  >>> lab.update_publis()
207
208
  >>> len(lab.authors)
208
209
  2
209
- >>> 380 < len(lab.publications) < 440
210
+ >>> 330 < len(lab.publications) < 430
210
211
  True
211
- >>> html = lab2graph(lab)
212
- >>> html[:80] # doctest: +ELLIPSIS
213
- '\\n<div class="gismap-content">\\n<div id="mynetwork_..."></div>\\n<a\\n href="htt'
212
+ >>> nodes, edges = lab_to_graph(lab)
213
+ >>> nodes[0]['group']
214
+ 'mini'
215
+ >>> edges[0]['hover']
216
+ 'Show joint publications from Mathieu Fabien and Tixeuil Sébastien'
217
+ >>> html = lab.html(groups={"mini": {"color": "#777"}})
214
218
  """
215
- node_pubs = {k: [] for k in lab.authors}
219
+ node_pubs = defaultdict(list) # {k: [] for k in lab.authors}
216
220
  edges_dict = defaultdict(list)
217
221
  for p in lab.publications.values():
218
222
  # Strange things can happen with multiple sources. This should take care of it.
@@ -228,16 +232,16 @@ def lab2graph(lab):
228
232
  for a1, a2 in combinations(lauths, 2):
229
233
  edges_dict[a1.key, a2.key].append(p)
230
234
  # connected = {k for kl in edges_dict for k in kl}
231
-
232
235
  for k, v in node_pubs.items():
233
236
  node_pubs[k] = sorted(v, key=lambda p: -p.year)
234
237
  for k, v in edges_dict.items():
235
238
  edges_dict[k] = sorted(v, key=lambda p: -p.year)
239
+ nodes = [
240
+ to_node(s, node_pubs)
241
+ for s in lab.authors.values() # if s.key in connected
242
+ ]
243
+ edges = [to_edge(k, v, lab.authors) for k, v in edges_dict.items()]
244
+ # for node in nodes:
245
+ # node['connected'] = node['id'] in connected
236
246
 
237
- return generate_html(
238
- nodes=[
239
- to_node(s, node_pubs)
240
- for s in lab.authors.values() # if s.key in connected
241
- ],
242
- edges=[to_edge(k, v, lab.authors) for k, v in edges_dict.items()],
243
- )
247
+ return nodes, edges
@@ -0,0 +1,70 @@
1
+ from domonic import tags
2
+ import distinctipy
3
+
4
+
5
+ def auto_groups(lab, groups=None, rng=None, pastel_factor=0.3):
6
+ if groups is None:
7
+ groups = ego_groups
8
+ else:
9
+ for k, v in ego_groups.items():
10
+ if k not in groups:
11
+ groups[k] = v
12
+ else:
13
+ groups[k] = {**v, **groups[k]}
14
+ res = {
15
+ group: groups.get(group, {"hidden": False})
16
+ for i, group in enumerate(
17
+ {
18
+ a.metadata.group: None
19
+ for a in lab.authors.values()
20
+ if a and a.metadata.group
21
+ }
22
+ )
23
+ }
24
+ n_colors = len([None for g in res.values() if "color" not in g])
25
+ colors = distinctipy.get_colors(n_colors, pastel_factor=pastel_factor, rng=rng)
26
+ colors = [
27
+ f"rgb({int(r * 255)},{int(g * 255)},{int(b * 255)})" for r, g, b in colors
28
+ ]
29
+ i = 0
30
+ for group in res.values():
31
+ if "color" not in group:
32
+ group["color"] = colors[i]
33
+ i += 1
34
+ return res
35
+
36
+
37
+ ego_groups = {
38
+ "star": {"display": "Star", "color": "rgb(210, 190, 70)", "hidden": False},
39
+ "planet": {"display": "Planets", "color": "rgb(80, 120, 200)", "hidden": False},
40
+ "moon": {"display": "Moons", "color": "rgb(140, 140, 140)", "hidden": False},
41
+ }
42
+
43
+
44
+ def make_legend(groups, uid):
45
+ legend = tags.div(_id=f"legend-{uid}", _class="legend")
46
+ if len(groups) > 1:
47
+ for group_name, props in groups.items():
48
+ color = props.get("color", "#cccccc")
49
+ display_name = props.get("display", group_name)
50
+ entry = tags.label(display_name, _class="legend-entry")
51
+ entry.appendChild(
52
+ tags.input(
53
+ **{
54
+ "type": "checkbox",
55
+ "class": "legend-checkbox",
56
+ "data-group": group_name,
57
+ },
58
+ checked=True,
59
+ )
60
+ )
61
+ entry.appendChild(
62
+ tags.span(
63
+ _style=f"background-color: {color}; width: 14px; height: 14px; display: inline-block; margin-right: 5px; vertical-align: middle;"
64
+ )
65
+ )
66
+ legend.appendChild(entry)
67
+ entry = tags.label("Show Comets", _class="comet-entry")
68
+ entry.appendChild(tags.input(**{"type": "checkbox", "id": f"comet-{uid}"}))
69
+ legend.appendChild(entry)
70
+ return legend
gismap/gisgraphs/js.py ADDED
@@ -0,0 +1,190 @@
1
+ from string import Template
2
+
3
+
4
+ # language=javascript
5
+ draw_script = """
6
+ import { DataSet, Network } from $vis_url;
7
+
8
+ const nodes = new DataSet($nodes);
9
+ const edges = new DataSet($edges);
10
+ const options = $options;
11
+ const container = document.getElementById('vis-$uid');
12
+ let hoveredEdgeId = null;
13
+ let hoveredNodeId = null;
14
+
15
+ // Get the group color and position of a node. Useful for gradient edges
16
+ function getNodeInfos(network, node) {
17
+ if (node && !options.groups?.[node.group]?.hidden) {
18
+ return [options.groups[node.group]?.color, network.getPositions([node.id])[node.id]]
19
+ }
20
+ return [false, false];
21
+ }
22
+
23
+
24
+ // main course
25
+ function draw_graph() {
26
+ // No clean redraw so far, so we re-create everything :(
27
+ // Set hidden groups according to legend, if any
28
+ document.querySelectorAll('#legend-$uid .legend-checkbox').forEach(cb => {
29
+ const group = cb.getAttribute('data-group');
30
+ options.groups[group].hidden = !cb.checked;
31
+ });
32
+
33
+
34
+ // First compute the nodes to display.
35
+ var visibleNodes = new DataSet(nodes.get({
36
+ filter: node => !options.groups?.[node.group]?.hidden}));
37
+ var visibleNodeIds = new Set(visibleNodes.map(node => node.id));
38
+ // Reduce edges
39
+ const visibleEdges = new DataSet(edges.get({
40
+ filter: edge => visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to)}));
41
+ // Optiional: remove comets
42
+ if (!document.getElementById("comet-$uid")?.checked) {
43
+ visibleNodeIds = new Set();
44
+ visibleEdges.forEach(edge => {
45
+ visibleNodeIds.add(edge.from);
46
+ visibleNodeIds.add(edge.to);
47
+ });
48
+ visibleNodes = new DataSet(nodes.get({filter: node => visibleNodeIds.has(node.id)}));
49
+ }
50
+
51
+
52
+ // Set graph, nodes, and edges
53
+ const network = new Network(container, {nodes: visibleNodes, edges: visibleEdges}, options);
54
+ network.once("afterDrawing", function () {
55
+ network.fit({ maxZoomLevel: 3 });
56
+ });
57
+ const netNodes = network.body.data.nodes;
58
+ const netEdges = network.body.data.edges;
59
+
60
+ // Gradient edges
61
+ network.on("beforeDrawing", function(ctx) {
62
+ const selectedEdgeIds = network.getSelectedEdges();
63
+ netEdges.forEach(edge => {
64
+ const [fromColor, fromPos] = getNodeInfos(network, netNodes.get(edge.from))
65
+ const [toColor, toPos] = getNodeInfos(network, netNodes.get(edge.to))
66
+
67
+ if (fromColor && toColor && (fromColor !== toColor) && fromPos && toPos ) {
68
+ let width = edge.width || 2;
69
+ if (selectedEdgeIds.includes(edge.id) || hoveredEdgeId === edge.id || hoveredNodeId === edge.from || hoveredNodeId === edge.to) width *= 1.8;
70
+
71
+ // Gradient
72
+ const grad = ctx.createLinearGradient(fromPos.x, fromPos.y, toPos.x, toPos.y);
73
+ grad.addColorStop(0, fromColor);
74
+ grad.addColorStop(1, toColor);
75
+
76
+ // Draw line
77
+ ctx.save();
78
+ ctx.strokeStyle = grad;
79
+ ctx.lineWidth = width;
80
+ ctx.beginPath();
81
+ ctx.moveTo(fromPos.x, fromPos.y);
82
+ ctx.lineTo(toPos.x, toPos.y);
83
+ ctx.stroke();
84
+ ctx.restore();
85
+ }
86
+ });
87
+ });
88
+
89
+ // Hover tooltip & record
90
+ network.on("hoverEdge", params => {
91
+ const edge = netEdges.get(params.edge);
92
+ network.body.container.title = edge.hover || '';
93
+ hoveredEdgeId = params.edge;
94
+ });
95
+
96
+ network.on("blurEdge", params => {
97
+ network.body.container.title = '';
98
+ hoveredEdgeId = null;
99
+ });
100
+
101
+ network.on("hoverNode", params => {
102
+ const node = netNodes.get(params.node);
103
+ netNodes.update({id: node.id, borderWidth: 10})
104
+ network.body.container.title = node.hover || '';
105
+ hoveredNodeId = params.node;
106
+ });
107
+ network.on("blurNode", params => {
108
+ const node = netNodes.get(params.node);
109
+ netNodes.update({id: node.id, borderWidth: 5})
110
+ network.body.container.title = '';
111
+ hoveredNodeId = null;
112
+ });
113
+
114
+
115
+ // Modal overlay
116
+ const modal = document.getElementById('modal-$uid');
117
+ const modalBody = document.getElementById('modal-body-$uid');
118
+ const modalClose = document.getElementById('modal-close-$uid');
119
+ network.on("click", function(params) {
120
+ if (params.nodes.length === 1) {
121
+ const node = netNodes.get(params.nodes[0]);
122
+ modalBody.innerHTML = node.overlay || '';
123
+ modal.style.display = "block";
124
+ } else if (params.edges.length === 1) {
125
+ const edge = netEdges.get(params.edges[0]);
126
+ modalBody.innerHTML = edge.overlay || '';
127
+ modal.style.display = "block";
128
+ } else {
129
+ modal.style.display = "none";
130
+ }
131
+ });
132
+ modalClose.onclick = function() { modal.style.display = "none"; };
133
+ window.onclick = function(event) {
134
+ if (event.target == modal) { modal.style.display = "none"; }
135
+ };
136
+ }
137
+
138
+ draw_graph();
139
+
140
+ """
141
+
142
+ # language=javascript
143
+ redraw_script = """
144
+ document.getElementById('redraw-$uid').addEventListener('click', function(event) {
145
+ event.preventDefault(); // Prevent page jump
146
+ draw_graph();
147
+ });
148
+
149
+ """
150
+
151
+ # language=javascript
152
+ fs_script = """
153
+ document.getElementById('fullscreen-$uid').addEventListener('click', function(event) {
154
+ event.preventDefault();
155
+ let elem = document.getElementById('box-$uid');
156
+ if (!document.fullscreenElement) {
157
+ // Request fullscreen mode
158
+ if (elem.requestFullscreen) {
159
+ elem.requestFullscreen();
160
+ } else if (elem.webkitRequestFullscreen) { /* Safari */
161
+ elem.webkitRequestFullscreen();
162
+ } else if (elem.msRequestFullscreen) { /* IE11 */
163
+ elem.msRequestFullscreen();
164
+ }
165
+ } else {
166
+ // Exit fullscreen mode
167
+ if (document.exitFullscreen) {
168
+ document.exitFullscreen();
169
+ } else if (document.webkitExitFullscreen) { /* Safari */
170
+ document.webkitExitFullscreen();
171
+ } else if (document.msExitFullscreen) { /* IE11 */
172
+ document.msExitFullscreen();
173
+ }
174
+ }
175
+ });
176
+
177
+ """
178
+
179
+ # language=javascript
180
+ legend_script = """
181
+ // Refresh when boxes are changed
182
+ document.querySelectorAll("#legend-$uid input").forEach(cb => {
183
+ cb.addEventListener('change', draw_graph);
184
+ });
185
+
186
+
187
+ """
188
+
189
+
190
+ default_script = Template(draw_script + redraw_script + fs_script + legend_script)
@@ -0,0 +1,37 @@
1
+ physics = {
2
+ "solver": "forceAtlas2Based",
3
+ "forceAtlas2Based": {
4
+ "gravitationalConstant": -50,
5
+ "centralGravity": 0.01,
6
+ "springLength": 200,
7
+ "springConstant": 0.08,
8
+ "damping": 0.98,
9
+ "avoidOverlap": 1,
10
+ },
11
+ "maxVelocity": 10,
12
+ "minVelocity": 0.9,
13
+ "stabilization": {
14
+ "enabled": True,
15
+ "iterations": 500,
16
+ "updateInterval": 50,
17
+ "onlyDynamicEdges": False,
18
+ "fit": True,
19
+ },
20
+ "timestep": 0.25,
21
+ }
22
+
23
+ nodes = {
24
+ "shape": "circle",
25
+ "size": 20,
26
+ "font": {"size": 16, "color": "#111"},
27
+ "color": "rgb(59, 101, 178)",
28
+ "borderWidth": 5,
29
+ }
30
+
31
+ edges = {
32
+ "width": 2,
33
+ # 'color': {'color': '#888', 'highlight': '#f5a25d'},
34
+ "smooth": False, # {"type": 'continuous'}
35
+ }
36
+
37
+ interaction = {"hover": True}