Skip to content

Commit bdd88d7

Browse files
authored
Groupby (#15)
* Initial version of allowing grouping for visualization for Gremlin * Added code to allow for grouping Gremlin output. When not specified it will group by the label (if it exists). Yo can also specify the property to groupby using the switch --groupby or -g followed by the property name * Initial version of allowing grouping for visualization for Gremlin * Added code to allow for grouping Gremlin output. When not specified it will group by the label (if it exists). Yo can also specify the property to groupby using the switch --groupby or -g followed by the property name * Fixed several issues that were causing CI tests to fail as well as found an incorrect case of grouping which was fixed and a test added * Fixed merge conflict that was missed in the last push * Fixed unit test test_add_vertex_with_callback which broke becasue the group field was not in the expected results * Addressed comments from akline PR review which included mainly cleaning up the code to simplify the group assignment and adjusting the switch to better match the standard. This required updating the unit tests as well * Fixed merge issues * Updated per PR feedback to simplify the groupby assignment to GremlinNetwork and bumped versions * Updated grouping to properly handle highlighting etc on selection and search as well as added option to ignore grouping * Added the functionality to sort the values in the details box by key * Updated Air-Routes-Visualization notebook to discuss the group by functionality Co-authored-by: Dave Bechberger <[email protected]>
1 parent 4674d13 commit bdd88d7

File tree

7 files changed

+287
-28
lines changed

7 files changed

+287
-28
lines changed

src/graph_notebook/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
SPDX-License-Identifier: Apache-2.0
44
"""
55

6-
__version__ = '2.0.4'
6+
__version__ = '2.0.5'

src/graph_notebook/magics/graph_magic.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,12 @@ def gremlin(self, line, cell, local_ns: dict = None):
294294
parser.add_argument('query_mode', nargs='?', default='query',
295295
help='query mode (default=query) [query|explain|profile]')
296296
parser.add_argument('-p', '--path-pattern', default='', help='path pattern')
297+
parser.add_argument('-g', '--group-by', default='T.label', help='Property used to group nodes (e.g. code, T.region) default is T.label')
297298
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
299+
parser.add_argument('--ignore-groups', action='store_true', help="Ignore all grouping options")
298300
args = parser.parse_args(line.split())
299301
mode = str_to_query_mode(args.query_mode)
302+
logger.debug(f'Arguments {args}')
300303

301304
tab = widgets.Tab()
302305
if mode == QueryMode.EXPLAIN:
@@ -345,7 +348,12 @@ def gremlin(self, line, cell, local_ns: dict = None):
345348
children.append(table_output)
346349

347350
try:
348-
gn = GremlinNetwork()
351+
logger.debug(f'groupby: {args.group_by}')
352+
if args.ignore_groups:
353+
gn = GremlinNetwork()
354+
else:
355+
gn = GremlinNetwork(group_by_property=args.group_by)
356+
349357
if args.path_pattern == '':
350358
gn.add_results(query_res)
351359
else:

src/graph_notebook/network/gremlin/GremlinNetwork.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
import hashlib
77
import json
88
import uuid
9+
import logging
910
from enum import Enum
1011

1112
from graph_notebook.network.EventfulNetwork import EventfulNetwork
1213
from gremlin_python.process.traversal import T
1314
from gremlin_python.structure.graph import Path, Vertex, Edge
1415
from networkx import MultiDiGraph
1516

17+
logging.basicConfig()
18+
logger = logging.getLogger(__file__)
19+
1620
T_LABEL = 'T.label'
1721
T_ID = 'T.id'
1822

@@ -84,10 +88,12 @@ class GremlinNetwork(EventfulNetwork):
8488
You can find more details on this in our design doc for visualization here: https://quip-amazon.com/R1jbA8eECdDd
8589
"""
8690

87-
def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length=DEFAULT_LABEL_MAX_LENGTH):
91+
def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length=DEFAULT_LABEL_MAX_LENGTH,
92+
group_by_property=T_LABEL):
8893
if graph is None:
8994
graph = MultiDiGraph()
9095
self.label_max_length = label_max_length
96+
self.group_by_property = group_by_property
9197
super().__init__(graph, callbacks)
9298

9399
def add_results_with_pattern(self, results, pattern_list: list):
@@ -264,20 +270,24 @@ def add_vertex(self, v):
264270
if type(v) is Vertex:
265271
node_id = v.id
266272
title = v.label
273+
group = v.label
267274
label = title if len(title) <= self.label_max_length else title[:self.label_max_length - 3] + '...'
268-
data = {'label': label, 'title': title, 'properties': {'id': node_id, 'label': title}}
275+
data = {'label': label, 'title': title, 'group': group, 'properties': {'id': node_id, 'label': title}}
269276
elif type(v) is dict:
270277
properties = {}
271278

272279
title = ''
273280
label = ''
281+
group = ''
274282
for k in v:
275283
if str(k) == T_LABEL:
276284
title = str(v[k])
277285
label = title if len(title) <= self.label_max_length else title[:self.label_max_length - 3] + '...'
278286
elif str(k) == T_ID:
279287
node_id = str(v[k])
280288
properties[k] = v[k]
289+
if str(k) == self.group_by_property:
290+
group = str(v[k])
281291

282292
# handle when there is no id in a node. In this case, we will generate one which
283293
# is consistently regenerated so that duplicate dicts will be dedubed to the same vertex.
@@ -291,12 +301,13 @@ def add_vertex(self, v):
291301
title += str(v[key])
292302
label = title if len(title) <= self.label_max_length else title[:self.label_max_length - 3] + '...'
293303

294-
data = {'properties': properties, 'label': label, 'title': title}
304+
data = {'properties': properties, 'label': label, 'title': title, 'group': group}
295305
else:
296306
node_id = str(v)
297307
title = str(v)
298308
label = title if len(title) <= self.label_max_length else title[:self.label_max_length - 3] + '...'
299-
data = {'title': title, 'label': label}
309+
data = {'title': title, 'label': label, 'group': ''}
310+
300311
self.add_node(node_id, data)
301312

302313
def add_path_edge(self, edge, from_id='', to_id='', data=None):

src/graph_notebook/notebooks/02-Visualization/Air-Routes-Gremlin.ipynb

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,37 @@
279279
" by(keys))"
280280
]
281281
},
282+
{
283+
"cell_type": "markdown",
284+
"metadata": {},
285+
"source": [
286+
"### Color Results by Group\n",
287+
"\n",
288+
"Run the query below, select the Graph tab, and you will see that the vertices in the graph are now colored by the country."
289+
]
290+
},
291+
{
292+
"cell_type": "code",
293+
"execution_count": null,
294+
"metadata": {},
295+
"outputs": [],
296+
"source": [
297+
"%%gremlin -g country\n",
298+
"g.V().has('airport','code','CZM').\n",
299+
" out('route').\n",
300+
" path().\n",
301+
" by(valueMap('code','city','region','desc','lat','lon', 'country').\n",
302+
" order(local).\n",
303+
" by(keys))"
304+
]
305+
},
306+
{
307+
"cell_type": "markdown",
308+
"metadata": {},
309+
"source": [
310+
"To group vertices together by color we us the `-g` or `--group-by` switch for the `%%gremlin` command. This specifies the property of the vertex to use to group items together. If no property is specified than items will be grouped by the `T.label` property if it exists. If it does not exist, or the property specified does not exist for a particular vertex then the vertex is added to a default group. Grouping of items is enabled by default but can be disabled using the `--ignore-groups` switch. "
311+
]
312+
},
282313
{
283314
"cell_type": "markdown",
284315
"metadata": {},
@@ -853,6 +884,55 @@
853884
" limit(5)"
854885
]
855886
},
887+
{
888+
"cell_type": "markdown",
889+
"metadata": {},
890+
"source": [
891+
"### Changing Group Colors and Adding Icons\n",
892+
"\n",
893+
"One of the features that is also available is the ability to change the color, add an image, or associate a particular icon representation for a group. Run the two cells below and you will see that all airports in Mexico are shown with the Mexican flag, all airports in the US are shown as a blue flag, and all airports in Canada are shown in red."
894+
]
895+
},
896+
{
897+
"cell_type": "code",
898+
"execution_count": null,
899+
"metadata": {},
900+
"outputs": [],
901+
"source": [
902+
"%%graph_notebook_vis_options\n",
903+
"{\n",
904+
" \"groups\": {\n",
905+
" \"['CA']\": {\"color\": \"red\"},\n",
906+
" \"['MX']\": {\"shape\": \"image\", \n",
907+
" \"image\":\"https://cdn.countryflags.com/thumbs/mexico/flag-round-250.png\"},\n",
908+
" \n",
909+
" \"['US']\": {\n",
910+
" \"shape\": \"icon\",\n",
911+
" \"icon\": {\n",
912+
" \"face\": \"FontAwesome\",\n",
913+
" \"code\": \"\\uf024\",\n",
914+
" \"color\": \"blue\"\n",
915+
" }\n",
916+
" }\n",
917+
"}\n",
918+
"}"
919+
]
920+
},
921+
{
922+
"cell_type": "code",
923+
"execution_count": null,
924+
"metadata": {},
925+
"outputs": [],
926+
"source": [
927+
"%%gremlin -g country\n",
928+
"g.V().has('airport','code','CZM').\n",
929+
" out('route').\n",
930+
" path().\n",
931+
" by(valueMap('code','city','region','desc','lat','lon', 'country').\n",
932+
" order(local).\n",
933+
" by(keys))"
934+
]
935+
},
856936
{
857937
"cell_type": "markdown",
858938
"metadata": {},
@@ -973,7 +1053,7 @@
9731053
"name": "python",
9741054
"nbconvert_exporter": "python",
9751055
"pygments_lexer": "ipython3",
976-
"version": "3.6.5"
1056+
"version": "3.7.7"
9771057
}
9781058
},
9791059
"nbformat": 4,

src/graph_notebook/widgets/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graph_notebook_widgets",
3-
"version": "2.0.4",
3+
"version": "2.0.5",
44
"author": "amazon",
55
"description": "A Custom Jupyter Library for rendering NetworkX MultiDiGraphs using vis-network",
66
"dependencies": {

src/graph_notebook/widgets/src/force_widget.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -488,12 +488,13 @@ export class ForceView extends DOMWidgetView {
488488
}
489489

490490
if (nodeID !== undefined && nodeID !== null && node.id === nodeID) {
491-
node.color = this.searchMatchColorNode;
491+
if (!node.group) {
492+
node.color = this.searchMatchColorNode;
493+
}
492494
} else {
493495
node.color = this.visOptions.nodes.color;
494496
}
495-
496-
node.font = { color: "black" };
497+
node.borderWidth = 0
497498
this.nodeDataset.update(node);
498499
this.vis?.stopSimulation();
499500
this.selectedNodeID = "";
@@ -566,7 +567,10 @@ export class ForceView extends DOMWidgetView {
566567
*/
567568
buildTableRows(data: DynamicObject): Array<HTMLElement> {
568569
const rows: Array<HTMLElement> = new Array<HTMLElement>();
569-
Object.entries(data).forEach((entry: Array<any>) => {
570+
const sorted = Object.entries(data).sort((a, b) => {
571+
return a[0].localeCompare(b[0]);
572+
});
573+
sorted.forEach((entry: Array<any>) => {
570574
const row = document.createElement("tr");
571575
const property = document.createElement("td");
572576
const value = document.createElement("td");
@@ -607,15 +611,20 @@ export class ForceView extends DOMWidgetView {
607611
if (node === null) {
608612
return;
609613
}
610-
611614
if (node.label !== undefined && node.label !== "") {
612615
this.detailsText.innerText = "Details - " + node.title;
613616
} else {
614617
this.detailsText.innerText = "Details";
615618
}
616619

617620
this.buildGraphPropertiesTable(node);
618-
node.font = { color: "white" };
621+
if (node.group) {
622+
node.font = { bold: true };
623+
node.opacity = 1
624+
node.borderWidth= 3
625+
} else {
626+
node.font = { color: "white" };
627+
}
619628
this.nodeDataset.update(node);
620629
this.vis?.stopSimulation();
621630
this.selectedNodeID = nodeID;
@@ -672,11 +681,9 @@ export class ForceView extends DOMWidgetView {
672681
this.nodeDataset.forEach((item, id) => {
673682
if (this.search(text, item, 0)) {
674683
const nodeID = id.toString();
675-
// if (selectedNodes[nodeID])
676684
nodeUpdate.push({
677685
id: nodeID,
678686
borderWidth: 3,
679-
color: this.searchMatchColorNode,
680687
});
681688
nodeIDs[id.toString()] = true;
682689
}
@@ -693,7 +700,18 @@ export class ForceView extends DOMWidgetView {
693700
edgeIDs[id.toString()] = true;
694701
}
695702
});
696-
}
703+
} else {
704+
//Reset the opacity and border width
705+
this.nodeDataset.forEach((item, id) => {
706+
const nodeID = id.toString();
707+
nodeUpdate.push({
708+
id: nodeID,
709+
opacity: 1,
710+
borderWidth: 0
711+
});
712+
nodeIDs[id.toString()] = true;
713+
})
714+
};
697715

698716
// check current matched nodes and clear all nodes which are no longer matches
699717
this.nodeIDSearchMatches.forEach((value) => {
@@ -706,10 +724,7 @@ export class ForceView extends DOMWidgetView {
706724
borderWidth: selected
707725
? this.visOptions["nodes"]["borderWidthSelected"]
708726
: 0,
709-
color: selected
710-
? this.visOptions["nodes"]["color"]["highlight"]
711-
: this.searchMatchColorNode,
712-
});
727+
opacity: 0.35 });
713728
}
714729
});
715730

0 commit comments

Comments
 (0)