Skip to content

Commit cf6f1cf

Browse files
authored
Add new visualization physics options, and toggle button to enable/disable physics (#190)
* Add physics toggle and duration option * Modify vis options unit test * Update Changelog * Add equivalent flags to magic commands Co-authored-by: Michael Chin <chnmch@amazon.com>
1 parent 262454a commit cf6f1cf

File tree

5 files changed

+89
-24
lines changed

5 files changed

+89
-24
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Starting with v1.31.6, this file will contain a record of major features and upd
55
## Upcoming
66

77
- Added support for multi-property values in vertex and edge labels ([Link to PR](https://github.com/aws/graph-notebook/pull/186))
8+
- Added new visualization physics options, toggle button ([Link to PR](https://github.com/aws/graph-notebook/pull/190))
89

910
## Release 3.0.5 (August 27, 2021)
1011

src/graph_notebook/magics/graph_magic.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ def sparql(self, line='', cell='', local_ns: dict = None):
225225
parser.add_argument('--explain-format', default='text/html', help='response format for explain query mode',
226226
choices=['text/csv', 'text/html'])
227227
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
228+
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
229+
help="Disable visualization physics after the initial simulation stabilizes.")
230+
parser.add_argument('-sd', '--simulation-duration', type=int, default=1500,
231+
help='Specifies maximum duration of visualization physics simulation. Default is 1500ms')
228232
args = parser.parse_args(line.split())
229233
mode = str_to_query_mode(args.query_mode)
230234
tab = widgets.Tab()
@@ -280,6 +284,9 @@ def sparql(self, line='', cell='', local_ns: dict = None):
280284

281285
logger.debug(f'number of nodes is {len(sn.graph.nodes)}')
282286
if len(sn.graph.nodes) > 0:
287+
self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \
288+
= args.stop_physics
289+
self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration
283290
f = Force(network=sn, options=self.graph_notebook_vis_options)
284291
titles.append('Graph')
285292
children.append(f)
@@ -394,6 +401,11 @@ def gremlin(self, line, cell, local_ns: dict = None):
394401
'TinkerPop driver "Serializers" enum values. Default is application/json')
395402
parser.add_argument('--indexOps', action='store_true', default=False,
396403
help='Show a detailed report of all index operations.')
404+
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
405+
help="Disable visualization physics after the initial simulation stabilizes.")
406+
parser.add_argument('-sd', '--simulation-duration', type=int, default=1500,
407+
help='Specifies maximum duration of visualization physics simulation. Default is 1500ms')
408+
397409
args = parser.parse_args(line.split())
398410
mode = str_to_query_mode(args.query_mode)
399411
logger.debug(f'Arguments {args}')
@@ -461,6 +473,9 @@ def gremlin(self, line, cell, local_ns: dict = None):
461473
gn.add_results_with_pattern(query_res, pattern)
462474
logger.debug(f'number of nodes is {len(gn.graph.nodes)}')
463475
if len(gn.graph.nodes) > 0:
476+
self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \
477+
= args.stop_physics
478+
self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration
464479
f = Force(network=gn, options=self.graph_notebook_vis_options)
465480
titles.append('Graph')
466481
children.append(f)
@@ -1317,7 +1332,6 @@ def disable_debug(self, line):
13171332
def graph_notebook_version(self, line):
13181333
print(graph_notebook.__version__)
13191334

1320-
# TODO: find out where we call this, then add local_ns param and variable decorator
13211335
@line_cell_magic
13221336
@display_exceptions
13231337
def graph_notebook_vis_options(self, line='', cell=''):
@@ -1363,6 +1377,10 @@ def handle_opencypher_query(self, line, cell, local_ns):
13631377
help='Specifies max length of vertex label, in characters. Default is 10')
13641378
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
13651379
parser.add_argument('--ignore-groups', action='store_true', default=False, help="Ignore all grouping options")
1380+
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
1381+
help="Disable visualization physics after the initial simulation stabilizes.")
1382+
parser.add_argument('-sd', '--simulation-duration', type=int, default=1500,
1383+
help='Specifies maximum duration of visualization physics simulation. Default is 1500ms')
13661384
args = parser.parse_args(line.split())
13671385
tab = widgets.Tab()
13681386
logger.debug(args)
@@ -1385,6 +1403,9 @@ def handle_opencypher_query(self, line, cell, local_ns):
13851403
gn.add_results(res)
13861404
logger.debug(f'number of nodes is {len(gn.graph.nodes)}')
13871405
if len(gn.graph.nodes) > 0:
1406+
self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \
1407+
= args.stop_physics
1408+
self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration
13881409
force_graph_output = Force(network=gn, options=self.graph_notebook_vis_options)
13891410
except ValueError as value_error:
13901411
logger.debug(f'unable to create network from result. Skipping from result set: {value_error}')

src/graph_notebook/options/options.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
SPDX-License-Identifier: Apache-2.0
44
"""
55

6+
# Documentation for these options: https://visjs.github.io/vis-network/docs/network
67
OPTIONS_DEFAULT_DIRECTED = {
78
"nodes": {
89
"borderWidthSelected": 0,
@@ -53,6 +54,8 @@
5354
"selectConnectedEdges": False
5455
},
5556
"physics": {
57+
"simulationDuration": 1500,
58+
"disablePhysicsAfterInitialSimulation": False,
5659
"minVelocity": 0.75,
5760
"barnesHut": {
5861
"centralGravity": 0.1,

src/graph_notebook/widgets/src/force_widget.ts

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class ForceView extends DOMWidgetView {
7979
private expandDiv: HTMLDivElement = document.createElement("div");
8080
private searchDiv: HTMLDivElement = document.createElement("div");
8181
private detailsDiv: HTMLDivElement = document.createElement("div");
82+
private physicsDiv: HTMLDivElement = document.createElement("div");
8283
private nodeDataset: NodeDataSet = new NodeDataSet(new Array<VisNode>(), {});
8384
private edgeDataset: EdgeDataSet = new EdgeDataSet(new Array<VisEdge>(), {});
8485
private visOptions: DynamicObject = {};
@@ -105,6 +106,7 @@ export class ForceView extends DOMWidgetView {
105106
};
106107
private detailsBtn = document.createElement("button");
107108
private selectedNodeID: string | number = "";
109+
private physicsBtn = document.createElement("button");
108110

109111
render(): void {
110112
this.networkDiv.classList.add("network-div");
@@ -140,7 +142,7 @@ export class ForceView extends DOMWidgetView {
140142
this.vis = new Network(this.canvasDiv, dataset, this.visOptions);
141143
setTimeout(() => {
142144
this.vis?.stopSimulation();
143-
}, 1500); // TODO: make the timeout configurable
145+
}, this.visOptions.physics.simulationDuration);
144146

145147
/*
146148
To listen to messages sent from the kernel, you can register callback methods in the view class,
@@ -268,7 +270,6 @@ export class ForceView extends DOMWidgetView {
268270
// no label found, using node id
269271
node["label"] = id;
270272
}
271-
272273
this.nodeDataset.update([node]);
273274
return;
274275
}
@@ -345,7 +346,7 @@ export class ForceView extends DOMWidgetView {
345346
"data": {
346347
"label": "to"
347348
}
348-
}
349+
}
349350
*/
350351
addEdge(msgData: DynamicObject): void {
351352
// To be able to add an edge, we require the message to have:
@@ -465,14 +466,23 @@ export class ForceView extends DOMWidgetView {
465466
this.vis?.on("startStabilizing", () => {
466467
setTimeout(() => {
467468
this.vis?.stopSimulation();
468-
}, 1500); // TODO: make timeout configurable
469+
}, this.visOptions.physics.simulationDuration);
469470
});
470471

471472
this.vis?.on("selectEdge", (params) => {
472473
params.edges.forEach((value) => {
473474
this.handleEdgeClick(params.edges[0]);
474475
});
475476
});
477+
478+
this.vis?.on("stabilized", () => {
479+
if (
480+
this.visOptions.physics.disablePhysicsAfterInitialSimulation == true
481+
) {
482+
this.visOptions.physics.enabled = false;
483+
this.changeOptions();
484+
}
485+
});
476486
}
477487

478488
/**
@@ -495,7 +505,7 @@ export class ForceView extends DOMWidgetView {
495505
} else {
496506
node.color = this.visOptions.nodes.color;
497507
}
498-
node.borderWidth = 0
508+
node.borderWidth = 0;
499509
this.nodeDataset.update(node);
500510
this.vis?.stopSimulation();
501511
this.selectedNodeID = "";
@@ -569,7 +579,7 @@ export class ForceView extends DOMWidgetView {
569579
buildTableRows(data: DynamicObject): Array<HTMLElement> {
570580
const rows: Array<HTMLElement> = new Array<HTMLElement>();
571581
const sorted = Object.entries(data).sort((a, b) => {
572-
return a[0].localeCompare(b[0]);
582+
return a[0].localeCompare(b[0]);
573583
});
574584
sorted.forEach((entry: Array<any>) => {
575585
const row = document.createElement("tr");
@@ -620,11 +630,11 @@ export class ForceView extends DOMWidgetView {
620630

621631
this.buildGraphPropertiesTable(node);
622632
if (node.group) {
623-
node.font = { bold: true };
624-
node.opacity = 1
625-
node.borderWidth= 3
633+
node.font = { bold: true };
634+
node.opacity = 1;
635+
node.borderWidth = 3;
626636
} else {
627-
node.font = { color: "white" };
637+
node.font = { color: "white" };
628638
}
629639
this.nodeDataset.update(node);
630640
this.vis?.stopSimulation();
@@ -702,17 +712,17 @@ export class ForceView extends DOMWidgetView {
702712
}
703713
});
704714
} else {
705-
//Reset the opacity and border width
706-
this.nodeDataset.forEach((item, id) => {
707-
const nodeID = id.toString();
708-
nodeUpdate.push({
709-
id: nodeID,
710-
opacity: 1,
711-
borderWidth: 0
712-
});
713-
nodeIDs[id.toString()] = true;
714-
})
715-
};
715+
//Reset the opacity and border width
716+
this.nodeDataset.forEach((item, id) => {
717+
const nodeID = id.toString();
718+
nodeUpdate.push({
719+
id: nodeID,
720+
opacity: 1,
721+
borderWidth: 0,
722+
});
723+
nodeIDs[id.toString()] = true;
724+
});
725+
}
716726

717727
// check current matched nodes and clear all nodes which are no longer matches
718728
this.nodeIDSearchMatches.forEach((value) => {
@@ -725,7 +735,8 @@ export class ForceView extends DOMWidgetView {
725735
borderWidth: selected
726736
? this.visOptions["nodes"]["borderWidthSelected"]
727737
: 0,
728-
opacity: 0.35 });
738+
opacity: 0.35,
739+
});
729740
}
730741
});
731742

@@ -766,6 +777,7 @@ export class ForceView extends DOMWidgetView {
766777
this.expandDiv.classList.add("menu-action", "expand-div");
767778
this.searchDiv.classList.add("menu-action", "search-div");
768779
this.detailsDiv.classList.add("menu-action", "details-div");
780+
this.physicsDiv.classList.add("menu-action", "physics-div");
769781

770782
const searchInput = document.createElement("input");
771783
searchInput.classList.add("search-bar");
@@ -778,6 +790,29 @@ export class ForceView extends DOMWidgetView {
778790
this.searchDiv.append(searchInput);
779791
rightActions.append(this.searchDiv);
780792

793+
this.physicsBtn.title = "Enable/Disable Graph Physics";
794+
if (
795+
this.visOptions.physics.enabled == true &&
796+
this.visOptions.physics.disablePhysicsAfterInitialSimulation == false
797+
) {
798+
this.physicsBtn.innerHTML = feather.icons["unlock"].toSvg();
799+
} else {
800+
this.physicsBtn.innerHTML = feather.icons["lock"].toSvg();
801+
}
802+
this.physicsDiv.appendChild(this.physicsBtn);
803+
rightActions.append(this.physicsDiv);
804+
805+
this.physicsBtn.onclick = (): void => {
806+
this.visOptions.physics.disablePhysicsAfterInitialSimulation = false;
807+
this.visOptions.physics.enabled = !this.visOptions.physics.enabled;
808+
this.changeOptions();
809+
if (this.visOptions.physics.enabled == true) {
810+
this.physicsBtn.innerHTML = feather.icons["unlock"].toSvg();
811+
} else {
812+
this.physicsBtn.innerHTML = feather.icons["lock"].toSvg();
813+
}
814+
};
815+
781816
this.detailsBtn.innerHTML = feather.icons["list"].toSvg();
782817
this.detailsBtn.title = "Details";
783818
this.detailsDiv.appendChild(this.detailsBtn);
@@ -865,7 +900,6 @@ export class ForceView extends DOMWidgetView {
865900
animation: true,
866901
});
867902
};
868-
869903
const zoomOutButton = document.createElement("button");
870904
zoomOutButton.title = "Zoom Out";
871905
zoomOutButton.onclick = () => {

test/unit/options/test_options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def test_vis_options_merge_complete_config(self):
108108
'original': OPTIONS_DEFAULT_DIRECTED,
109109
'target': {
110110
"physics": {
111+
"simulationDuration": 1500,
112+
"disablePhysicsAfterInitialSimulation": False,
111113
"hierarchicalRepulsion": {
112114
"centralGravity": 0
113115
},
@@ -173,6 +175,8 @@ def test_vis_options_merge_complete_config(self):
173175
"selectConnectedEdges": False
174176
},
175177
"physics": {
178+
"simulationDuration": 1500,
179+
"disablePhysicsAfterInitialSimulation": False,
176180
"minVelocity": 0.75,
177181
"barnesHut": {
178182
"centralGravity": 0.1,
@@ -267,6 +271,8 @@ def test_vis_options_merge_complete_config(self):
267271
"selectConnectedEdges": False
268272
},
269273
"physics": {
274+
"simulationDuration": 1500,
275+
"disablePhysicsAfterInitialSimulation": False,
270276
"minVelocity": 0.75,
271277
"barnesHut": {
272278
"centralGravity": 0.1,

0 commit comments

Comments
 (0)