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 +1 -0
- gismap/gisgraphs/__init__.py +0 -0
- gismap/gisgraphs/builder.py +105 -0
- gismap/{lab → gisgraphs}/graph.py +70 -66
- gismap/gisgraphs/groups.py +70 -0
- gismap/gisgraphs/js.py +190 -0
- gismap/gisgraphs/options.py +37 -0
- gismap/gisgraphs/style.py +119 -0
- gismap/gisgraphs/widget.py +145 -0
- gismap/lab/__init__.py +0 -4
- gismap/lab/egomap.py +6 -7
- gismap/lab/expansion.py +7 -6
- gismap/lab/filters.py +1 -1
- gismap/lab/lab_author.py +51 -4
- gismap/lab/labmap.py +4 -2
- gismap/lab_examples/__init__.py +0 -0
- gismap/lab_examples/cedric.py +46 -0
- gismap/{lab → lab_examples}/lincs.py +2 -2
- gismap/{lab → lab_examples}/toulouse.py +20 -3
- gismap/sources/dblp.py +15 -17
- gismap/sources/hal.py +17 -8
- gismap/sources/models.py +7 -0
- gismap/sources/multi.py +23 -15
- gismap/utils/requests.py +4 -2
- {gismap-0.2.2.dist-info → gismap-0.3.0.dist-info}/METADATA +21 -5
- gismap-0.3.0.dist-info/RECORD +38 -0
- gismap/lab/vis.py +0 -329
- gismap-0.2.2.dist-info/RECORD +0 -30
- /gismap/{lab → lab_examples}/lip6.py +0 -0
- {gismap-0.2.2.dist-info → gismap-0.3.0.dist-info}/WHEEL +0 -0
- {gismap-0.2.2.dist-info → gismap-0.3.0.dist-info}/licenses/AUTHORS.md +0 -0
gismap/__init__.py
CHANGED
|
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
|
-
|
|
2
|
-
from collections import defaultdict
|
|
1
|
+
from domonic import tags
|
|
3
2
|
from itertools import combinations
|
|
4
|
-
from
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
102
|
+
:class:`~domonic.html.ul`
|
|
98
103
|
"""
|
|
99
|
-
|
|
104
|
+
lis = []
|
|
100
105
|
for i, pub in enumerate(publications):
|
|
101
|
-
item = publication_to_html(pub)
|
|
102
106
|
if i < n:
|
|
103
|
-
|
|
107
|
+
lis.append(f"<li>{pub_html(pub)}</li>")
|
|
104
108
|
else:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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"
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
-
>>>
|
|
210
|
+
>>> 330 < len(lab.publications) < 430
|
|
210
211
|
True
|
|
211
|
-
>>>
|
|
212
|
-
>>>
|
|
213
|
-
'
|
|
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
|
|
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}
|