Skip to content

Add new visualization physics options, and toggle button to enable/disable physics #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 14, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ Starting with v1.31.6, this file will contain a record of major features and upd
## Upcoming

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

## Release 3.0.5 (August 27, 2021)

23 changes: 22 additions & 1 deletion src/graph_notebook/magics/graph_magic.py
Original file line number Diff line number Diff line change
@@ -225,6 +225,10 @@ def sparql(self, line='', cell='', local_ns: dict = None):
parser.add_argument('--explain-format', default='text/html', help='response format for explain query mode',
choices=['text/csv', 'text/html'])
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
help="Disable visualization physics after the initial simulation stabilizes.")
parser.add_argument('-sd', '--simulation-duration', type=int, default=1500,
help='Specifies maximum duration of visualization physics simulation. Default is 1500ms')
args = parser.parse_args(line.split())
mode = str_to_query_mode(args.query_mode)
tab = widgets.Tab()
@@ -280,6 +284,9 @@ def sparql(self, line='', cell='', local_ns: dict = None):

logger.debug(f'number of nodes is {len(sn.graph.nodes)}')
if len(sn.graph.nodes) > 0:
self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \
= args.stop_physics
self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration
f = Force(network=sn, options=self.graph_notebook_vis_options)
titles.append('Graph')
children.append(f)
@@ -394,6 +401,11 @@ def gremlin(self, line, cell, local_ns: dict = None):
'TinkerPop driver "Serializers" enum values. Default is application/json')
parser.add_argument('--indexOps', action='store_true', default=False,
help='Show a detailed report of all index operations.')
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
help="Disable visualization physics after the initial simulation stabilizes.")
parser.add_argument('-sd', '--simulation-duration', type=int, default=1500,
help='Specifies maximum duration of visualization physics simulation. Default is 1500ms')

args = parser.parse_args(line.split())
mode = str_to_query_mode(args.query_mode)
logger.debug(f'Arguments {args}')
@@ -461,6 +473,9 @@ def gremlin(self, line, cell, local_ns: dict = None):
gn.add_results_with_pattern(query_res, pattern)
logger.debug(f'number of nodes is {len(gn.graph.nodes)}')
if len(gn.graph.nodes) > 0:
self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \
= args.stop_physics
self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration
f = Force(network=gn, options=self.graph_notebook_vis_options)
titles.append('Graph')
children.append(f)
@@ -1317,7 +1332,6 @@ def disable_debug(self, line):
def graph_notebook_version(self, line):
print(graph_notebook.__version__)

# TODO: find out where we call this, then add local_ns param and variable decorator
@line_cell_magic
@display_exceptions
def graph_notebook_vis_options(self, line='', cell=''):
@@ -1363,6 +1377,10 @@ def handle_opencypher_query(self, line, cell, local_ns):
help='Specifies max length of vertex label, in characters. Default is 10')
parser.add_argument('--store-to', type=str, default='', help='store query result to this variable')
parser.add_argument('--ignore-groups', action='store_true', default=False, help="Ignore all grouping options")
parser.add_argument('-sp', '--stop-physics', action='store_true', default=False,
help="Disable visualization physics after the initial simulation stabilizes.")
parser.add_argument('-sd', '--simulation-duration', type=int, default=1500,
help='Specifies maximum duration of visualization physics simulation. Default is 1500ms')
args = parser.parse_args(line.split())
tab = widgets.Tab()
logger.debug(args)
@@ -1385,6 +1403,9 @@ def handle_opencypher_query(self, line, cell, local_ns):
gn.add_results(res)
logger.debug(f'number of nodes is {len(gn.graph.nodes)}')
if len(gn.graph.nodes) > 0:
self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \
= args.stop_physics
self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration
force_graph_output = Force(network=gn, options=self.graph_notebook_vis_options)
except ValueError as value_error:
logger.debug(f'unable to create network from result. Skipping from result set: {value_error}')
3 changes: 3 additions & 0 deletions src/graph_notebook/options/options.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
SPDX-License-Identifier: Apache-2.0
"""

# Documentation for these options: https://visjs.github.io/vis-network/docs/network
OPTIONS_DEFAULT_DIRECTED = {
"nodes": {
"borderWidthSelected": 0,
@@ -53,6 +54,8 @@
"selectConnectedEdges": False
},
"physics": {
"simulationDuration": 1500,
"disablePhysicsAfterInitialSimulation": False,
"minVelocity": 0.75,
"barnesHut": {
"centralGravity": 0.1,
80 changes: 57 additions & 23 deletions src/graph_notebook/widgets/src/force_widget.ts
Original file line number Diff line number Diff line change
@@ -79,6 +79,7 @@ export class ForceView extends DOMWidgetView {
private expandDiv: HTMLDivElement = document.createElement("div");
private searchDiv: HTMLDivElement = document.createElement("div");
private detailsDiv: HTMLDivElement = document.createElement("div");
private physicsDiv: HTMLDivElement = document.createElement("div");
private nodeDataset: NodeDataSet = new NodeDataSet(new Array<VisNode>(), {});
private edgeDataset: EdgeDataSet = new EdgeDataSet(new Array<VisEdge>(), {});
private visOptions: DynamicObject = {};
@@ -105,6 +106,7 @@ export class ForceView extends DOMWidgetView {
};
private detailsBtn = document.createElement("button");
private selectedNodeID: string | number = "";
private physicsBtn = document.createElement("button");

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

/*
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 {
// no label found, using node id
node["label"] = id;
}

this.nodeDataset.update([node]);
return;
}
@@ -345,7 +346,7 @@ export class ForceView extends DOMWidgetView {
"data": {
"label": "to"
}
}
}
*/
addEdge(msgData: DynamicObject): void {
// To be able to add an edge, we require the message to have:
@@ -465,14 +466,23 @@ export class ForceView extends DOMWidgetView {
this.vis?.on("startStabilizing", () => {
setTimeout(() => {
this.vis?.stopSimulation();
}, 1500); // TODO: make timeout configurable
}, this.visOptions.physics.simulationDuration);
});

this.vis?.on("selectEdge", (params) => {
params.edges.forEach((value) => {
this.handleEdgeClick(params.edges[0]);
});
});

this.vis?.on("stabilized", () => {
if (
this.visOptions.physics.disablePhysicsAfterInitialSimulation == true
) {
this.visOptions.physics.enabled = false;
this.changeOptions();
}
});
}

/**
@@ -495,7 +505,7 @@ export class ForceView extends DOMWidgetView {
} else {
node.color = this.visOptions.nodes.color;
}
node.borderWidth = 0
node.borderWidth = 0;
this.nodeDataset.update(node);
this.vis?.stopSimulation();
this.selectedNodeID = "";
@@ -569,7 +579,7 @@ export class ForceView extends DOMWidgetView {
buildTableRows(data: DynamicObject): Array<HTMLElement> {
const rows: Array<HTMLElement> = new Array<HTMLElement>();
const sorted = Object.entries(data).sort((a, b) => {
return a[0].localeCompare(b[0]);
return a[0].localeCompare(b[0]);
});
sorted.forEach((entry: Array<any>) => {
const row = document.createElement("tr");
@@ -620,11 +630,11 @@ export class ForceView extends DOMWidgetView {

this.buildGraphPropertiesTable(node);
if (node.group) {
node.font = { bold: true };
node.opacity = 1
node.borderWidth= 3
node.font = { bold: true };
node.opacity = 1;
node.borderWidth = 3;
} else {
node.font = { color: "white" };
node.font = { color: "white" };
}
this.nodeDataset.update(node);
this.vis?.stopSimulation();
@@ -702,17 +712,17 @@ export class ForceView extends DOMWidgetView {
}
});
} else {
//Reset the opacity and border width
this.nodeDataset.forEach((item, id) => {
const nodeID = id.toString();
nodeUpdate.push({
id: nodeID,
opacity: 1,
borderWidth: 0
});
nodeIDs[id.toString()] = true;
})
};
//Reset the opacity and border width
this.nodeDataset.forEach((item, id) => {
const nodeID = id.toString();
nodeUpdate.push({
id: nodeID,
opacity: 1,
borderWidth: 0,
});
nodeIDs[id.toString()] = true;
});
}

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

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

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

this.physicsBtn.title = "Enable/Disable Graph Physics";
if (
this.visOptions.physics.enabled == true &&
this.visOptions.physics.disablePhysicsAfterInitialSimulation == false
) {
this.physicsBtn.innerHTML = feather.icons["unlock"].toSvg();
} else {
this.physicsBtn.innerHTML = feather.icons["lock"].toSvg();
}
this.physicsDiv.appendChild(this.physicsBtn);
rightActions.append(this.physicsDiv);

this.physicsBtn.onclick = (): void => {
this.visOptions.physics.disablePhysicsAfterInitialSimulation = false;
this.visOptions.physics.enabled = !this.visOptions.physics.enabled;
this.changeOptions();
if (this.visOptions.physics.enabled == true) {
this.physicsBtn.innerHTML = feather.icons["unlock"].toSvg();
} else {
this.physicsBtn.innerHTML = feather.icons["lock"].toSvg();
}
};

this.detailsBtn.innerHTML = feather.icons["list"].toSvg();
this.detailsBtn.title = "Details";
this.detailsDiv.appendChild(this.detailsBtn);
@@ -865,7 +900,6 @@ export class ForceView extends DOMWidgetView {
animation: true,
});
};

const zoomOutButton = document.createElement("button");
zoomOutButton.title = "Zoom Out";
zoomOutButton.onclick = () => {
6 changes: 6 additions & 0 deletions test/unit/options/test_options.py
Original file line number Diff line number Diff line change
@@ -108,6 +108,8 @@ def test_vis_options_merge_complete_config(self):
'original': OPTIONS_DEFAULT_DIRECTED,
'target': {
"physics": {
"simulationDuration": 1500,
"disablePhysicsAfterInitialSimulation": False,
"hierarchicalRepulsion": {
"centralGravity": 0
},
@@ -173,6 +175,8 @@ def test_vis_options_merge_complete_config(self):
"selectConnectedEdges": False
},
"physics": {
"simulationDuration": 1500,
"disablePhysicsAfterInitialSimulation": False,
"minVelocity": 0.75,
"barnesHut": {
"centralGravity": 0.1,
@@ -267,6 +271,8 @@ def test_vis_options_merge_complete_config(self):
"selectConnectedEdges": False
},
"physics": {
"simulationDuration": 1500,
"disablePhysicsAfterInitialSimulation": False,
"minVelocity": 0.75,
"barnesHut": {
"centralGravity": 0.1,