Skip to content

Commit 82ac40f

Browse files
authored
Add edge tooltips, new query magic option for specifying edge label length (#218)
* Display tooltips on visualized edges * Fix unit tests * ChangeLog.md * Truncate edge labels based on --label-max-length value, add this parameter as %%sparql option * Add unit tests * Use separate query parameter for edge label length * Update Changelog Co-authored-by: Michael Chin <chnmch@amazon.com>
1 parent b2b016c commit 82ac40f

File tree

11 files changed

+276
-41
lines changed

11 files changed

+276
-41
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Starting with v1.31.6, this file will contain a record of major features and updates made in each release of graph-notebook.
44

55
## Upcoming
6+
- Added edge tooltips, and options for specifying edge label length ([Link to PR](https://github.com/aws/graph-notebook/pull/218))
67

78
## Release 3.0.7 (October 25, 2021)
89
- Added full support for NeptuneML API command parameters to `%neptune_ml` ([Link to PR](https://github.com/aws/graph-notebook/pull/202))

src/graph_notebook/magics/graph_magic.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ def sparql(self, line='', cell='', local_ns: dict = None):
248248
choices=['dynamic', 'static', 'details'])
249249
parser.add_argument('--explain-format', default='text/html', help='response format for explain query mode',
250250
choices=['text/csv', 'text/html'])
251+
parser.add_argument('-l', '--label-max-length', type=int, default=10,
252+
help='Specifies max length of vertex labels, in characters. Default is 10')
253+
parser.add_argument('-le', '--edge-label-max-length', type=int, default=10,
254+
help='Specifies max length of edge labels, in characters. Default is 10')
251255
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
252256
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
253257
help="Disable visualization physics after the initial simulation stabilizes.")
@@ -304,7 +308,8 @@ def sparql(self, line='', cell='', local_ns: dict = None):
304308
sparql_metadata = build_sparql_metadata_from_query(query_type='query', res=query_res,
305309
results=results, scd_query=True)
306310

307-
sn = SPARQLNetwork(expand_all=args.expand_all)
311+
sn = SPARQLNetwork(label_max_length=args.label_max_length,
312+
edge_label_max_length=args.edge_label_max_length, expand_all=args.expand_all)
308313
sn.extract_prefix_declarations_from_query(cell)
309314
try:
310315
sn.add_results(results)
@@ -420,6 +425,8 @@ def gremlin(self, line, cell, local_ns: dict = None):
420425
help='Property to display the value of on each edge, default is T.label')
421426
parser.add_argument('-l', '--label-max-length', type=int, default=10,
422427
help='Specifies max length of vertex label, in characters. Default is 10')
428+
parser.add_argument('-le', '--edge-label-max-length', type=int, default=10,
429+
help='Specifies max length of edge labels, in characters. Default is 10')
423430
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
424431
parser.add_argument('--ignore-groups', action='store_true', default=False, help="Ignore all grouping options")
425432
parser.add_argument('--no-results', action='store_false', default=True,
@@ -500,7 +507,9 @@ def gremlin(self, line, cell, local_ns: dict = None):
500507
logger.debug(f'ignore_groups: {args.ignore_groups}')
501508
gn = GremlinNetwork(group_by_property=args.group_by, display_property=args.display_property,
502509
edge_display_property=args.edge_display_property,
503-
label_max_length=args.label_max_length, ignore_groups=args.ignore_groups)
510+
label_max_length=args.label_max_length,
511+
edge_label_max_length=args.edge_label_max_length,
512+
ignore_groups=args.ignore_groups)
504513

505514
if args.path_pattern == '':
506515
gn.add_results(query_res)
@@ -1542,6 +1551,8 @@ def handle_opencypher_query(self, line, cell, local_ns):
15421551
help='Property to display the value of on each edge, default is ~type')
15431552
parser.add_argument('-l', '--label-max-length', type=int, default=10,
15441553
help='Specifies max length of vertex label, in characters. Default is 10')
1554+
parser.add_argument('-rel', '--rel-label-max-length', type=int, default=10,
1555+
help='Specifies max length of edge labels, in characters. Default is 10')
15451556
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
15461557
parser.add_argument('--ignore-groups', action='store_true', default=False, help="Ignore all grouping options")
15471558
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
@@ -1571,7 +1582,9 @@ def handle_opencypher_query(self, line, cell, local_ns):
15711582
try:
15721583
gn = OCNetwork(group_by_property=args.group_by, display_property=args.display_property,
15731584
edge_display_property=args.edge_display_property,
1574-
label_max_length=args.label_max_length, ignore_groups=args.ignore_groups)
1585+
label_max_length=args.label_max_length,
1586+
rel_label_max_length=args.rel_label_max_length,
1587+
ignore_groups=args.ignore_groups)
15751588
gn.add_results(res)
15761589
logger.debug(f'number of nodes is {len(gn.graph.nodes)}')
15771590
if len(gn.graph.nodes) > 0:

src/graph_notebook/network/EventfulNetwork.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ def __init__(self, graph: MultiDiGraph = None, callbacks: dict = None):
4343
super().__init__(graph)
4444

4545
def strip_and_truncate_label_and_title(self, old_label, max_len: int) -> Tuple[str, str]:
46-
if isinstance(old_label, list) and len(old_label) > 1:
47-
title = str(old_label)
48-
else:
46+
if isinstance(old_label, list) and len(old_label) == 1:
4947
title = str(old_label).strip("[]'")
48+
else:
49+
title = str(old_label)
5050
if len(title) <= max_len:
5151
label = title
5252
else:
@@ -140,7 +140,7 @@ def add_node(self, node_id: str, data: dict = None):
140140
}
141141
self.dispatch_callbacks(EVENT_ADD_NODE, payload)
142142

143-
def add_edge(self, from_id: str, to_id: str, edge_id: str, label: str, data: dict = None):
143+
def add_edge(self, from_id: str, to_id: str, edge_id: str, label: str, title: str = None, data: dict = None):
144144
if data is None:
145145
data = {}
146146
super().add_edge(from_id, to_id, edge_id, label, data)
@@ -149,6 +149,7 @@ def add_edge(self, from_id: str, to_id: str, edge_id: str, label: str, data: dic
149149
'to_id': to_id,
150150
'edge_id': edge_id,
151151
'label': label,
152+
'title': title,
152153
'data': data
153154
}
154155
self.dispatch_callbacks(EVENT_ADD_EDGE, payload)

src/graph_notebook/network/gremlin/GremlinNetwork.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,12 @@ class GremlinNetwork(EventfulNetwork):
9999
"""
100100

101101
def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length=DEFAULT_LABEL_MAX_LENGTH,
102-
group_by_property=T_LABEL, display_property=T_LABEL, edge_display_property=T_LABEL,
103-
ignore_groups=False):
102+
edge_label_max_length=DEFAULT_LABEL_MAX_LENGTH, group_by_property=T_LABEL, display_property=T_LABEL,
103+
edge_display_property=T_LABEL, ignore_groups=False):
104104
if graph is None:
105105
graph = MultiDiGraph()
106-
if label_max_length < 3:
107-
self.label_max_length = 3
108-
else:
109-
self.label_max_length = label_max_length
106+
self.label_max_length = 3 if label_max_length < 3 else label_max_length
107+
self.edge_label_max_length = 3 if edge_label_max_length < 3 else edge_label_max_length
110108
try:
111109
self.group_by_property = json.loads(group_by_property)
112110
except ValueError:
@@ -455,34 +453,37 @@ def add_path_edge(self, edge, from_id='', to_id='', data=None):
455453
if isinstance(self.edge_display_property[edge_label], tuple) and isinstance(edge[k],list):
456454
if str(k) == self.edge_display_property[edge_label][0]:
457455
try:
458-
edge_label = str(edge[k][self.edge_display_property[edge_label][1]])
456+
edge_label = edge[k][self.edge_display_property[edge_label][1]]
459457
display_is_set = True
460458
except (TypeError, IndexError) as e:
461459
logger.debug(f"Failed to index into edge sub-property for: {edge[k]} and "
462460
f"{self.edge_display_property[edge_label]}")
463461
continue
464462
elif str(k) == self.edge_display_property[edge_label]:
465-
edge_label = str(edge[k])
463+
edge_label = edge[k]
466464
display_is_set = True
467465
except KeyError:
468466
continue
469467
elif isinstance(self.edge_display_property, tuple):
470468
if str(k) == self.edge_display_property[0] and isinstance(edge[k], list):
471469
try:
472-
edge_label = str(edge[k][self.edge_display_property[1]])
470+
edge_label = edge[k][self.edge_display_property[1]]
473471
display_is_set = True
474472
except IndexError:
475473
logger.debug(f"Failed to index into edge sub-property for: {edge[k]} and "
476474
f"{self.edge_display_property[0]}")
477475
continue
478476
elif str(k) == self.edge_display_property:
479-
edge_label = str(edge[k])
477+
edge_label = edge[k]
480478
display_is_set = True
481-
482479
data['properties'] = properties
483-
self.add_edge(from_id, to_id, edge_id, edge_label, data)
480+
edge_title, edge_label = self.strip_and_truncate_label_and_title(edge_label, self.edge_label_max_length)
481+
data['title'] = edge_title
482+
self.add_edge(from_id=from_id, to_id=to_id, edge_id=edge_id, label=edge_label, title=edge_title, data=data)
484483
else:
485-
self.add_edge(from_id, to_id, edge, str(edge), data)
484+
edge_title, edge_label = self.strip_and_truncate_label_and_title(edge, self.edge_label_max_length)
485+
data['title'] = edge_title
486+
self.add_edge(from_id=from_id, to_id=to_id, edge_id=edge, label=edge_label, title=edge_title, data=data)
486487

487488
def add_blank_edge(self, from_id, to_id, edge_id=None, undirected=True, label=''):
488489
"""
@@ -498,7 +499,7 @@ def add_blank_edge(self, from_id, to_id, edge_id=None, undirected=True, label=''
498499
if edge_id is None:
499500
edge_id = str(uuid.uuid4())
500501
edge_data = UNDIRECTED_EDGE if undirected else {}
501-
self.add_edge(from_id, to_id, edge_id, label, edge_data)
502+
self.add_edge(from_id=from_id, to_id=to_id, edge_id=edge_id, label=label, title=label, data=edge_data)
502503

503504
def insert_path_element(self, path, i):
504505
if i == 0:

src/graph_notebook/network/opencypher/OCNetwork.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,13 @@ class OCNetwork(EventfulNetwork):
3333
"""
3434

3535
def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length=DEFAULT_LABEL_MAX_LENGTH,
36-
group_by_property=LABEL_KEY, display_property=LABEL_KEY,
37-
edge_display_property=EDGE_TYPE_KEY, ignore_groups=False):
36+
rel_label_max_length=DEFAULT_LABEL_MAX_LENGTH, group_by_property=LABEL_KEY,
37+
display_property=LABEL_KEY, edge_display_property=EDGE_TYPE_KEY, ignore_groups=False):
3838
if graph is None:
3939
graph = MultiDiGraph()
40-
if label_max_length < 3:
41-
self.label_max_length = 3
42-
else:
43-
self.label_max_length = label_max_length
40+
41+
self.label_max_length = 3 if label_max_length < 3 else label_max_length
42+
self.rel_label_max_length = 3 if rel_label_max_length < 3 else rel_label_max_length
4443
try:
4544
self.group_by_property = json.loads(group_by_property)
4645
except ValueError:
@@ -139,7 +138,7 @@ def parse_node(self, node: dict):
139138
self.add_node(node[ID_KEY], data)
140139

141140
def parse_rel(self, rel):
142-
data = {'properties': self.flatten(rel), 'label': rel[EDGE_TYPE_KEY]}
141+
data = {'properties': self.flatten(rel), 'label': rel[EDGE_TYPE_KEY], 'title': rel[EDGE_TYPE_KEY]}
143142
if self.edge_display_property is not EDGE_TYPE_KEY:
144143
try:
145144
if isinstance(self.edge_display_property, dict):
@@ -164,7 +163,11 @@ def parse_rel(self, rel):
164163
display_label = rel[EDGE_TYPE_KEY]
165164
else:
166165
display_label = rel[EDGE_TYPE_KEY]
167-
self.add_edge(rel[START_KEY], rel[END_KEY], rel[ID_KEY], str(display_label), data)
166+
edge_title, edge_label = self.strip_and_truncate_label_and_title(display_label, self.rel_label_max_length)
167+
data['title'] = edge_title
168+
data['label'] = edge_label
169+
self.add_edge(from_id=rel[START_KEY], to_id=rel[END_KEY], edge_id=rel[ID_KEY], label=edge_label,
170+
title=edge_title, data=data)
168171

169172
def process_result(self, res: dict):
170173
"""Determines the type of element passed in and processes it appropriately

src/graph_notebook/network/sparql/SPARQLNetwork.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,15 @@ def __init__(self,
5050
graph: MultiDiGraph = None,
5151
callbacks: list = None,
5252
label_max_length: int = DEFAULT_LABEL_MAX_LENGTH,
53+
edge_label_max_length: int = DEFAULT_LABEL_MAX_LENGTH,
5354
expand_all: bool = False):
5455
if graph is None:
5556
graph = MultiDiGraph()
5657

5758
self.expand_all = expand_all
58-
self.label_max_length = label_max_length
59+
self.label_max_length = 3 if label_max_length < 3 else label_max_length
60+
self.edge_label_max_length = 3 if edge_label_max_length < 3 else edge_label_max_length
61+
5962
super().__init__(graph, callbacks)
6063
self.namespace_to_prefix = { # http://foo/bar/ -> bar
6164
NAMESPACE_RDFS: PREFIX_RDFS,
@@ -291,7 +294,7 @@ def add_results(self, results):
291294
data['title'] = title
292295
data['label'] = label
293296

294-
# object is a literal. Check if data has this preciate already. If it does, turn its value into an
297+
# object is a literal. Check if data has this predicate already. If it does, turn its value into an
295298
# array and append the new value to it.
296299
if 'properties' in data and f'{prefix}:{value}' in data['properties']:
297300
if type(data['properties'][f'{prefix}:{value}']) is list:
@@ -301,7 +304,7 @@ def add_results(self, results):
301304
else:
302305
data['properties'][f'{prefix}:{value}'] = obj_entry
303306
else:
304-
# Check if data has this preciate already. If it does, turn its value into an
307+
# Check if data has this predicate already. If it does, turn its value into an
305308
# array and append the new value to it.
306309
if 'properties' in data and pred['value'] in data['properties']:
307310
if type(data['properties'][pred['value']]) is list:
@@ -341,4 +344,7 @@ def process_edge_bindings(self, bindings, use_spo=False):
341344

342345
if not self.graph.has_node(b[object_binding]['value']):
343346
self.add_node(b[object_binding]['value'])
344-
self.add_edge(b[subject_binding]['value'], b[object_binding]['value'], pred['value'], edge_label)
347+
edge_title, edge_label = self.strip_and_truncate_label_and_title(edge_label, self.edge_label_max_length)
348+
data = {'title': edge_title}
349+
self.add_edge(from_id=b[subject_binding]['value'], to_id=b[object_binding]['value'], edge_id=pred['value'],
350+
label=edge_label, title=edge_title, data=data)

src/graph_notebook/widgets/src/force_widget.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,6 @@ export class ForceView extends DOMWidgetView {
666666
if (edge === null) {
667667
return;
668668
}
669-
670669
if (edge.title !== undefined && edge.title !== "") {
671670
this.detailsText.innerText = "Details - " + edge.title;
672671
} else {

0 commit comments

Comments
 (0)