diff --git a/src/graph_notebook/magics/graph_magic.py b/src/graph_notebook/magics/graph_magic.py index 515657bd..16c99056 100644 --- a/src/graph_notebook/magics/graph_magic.py +++ b/src/graph_notebook/magics/graph_magic.py @@ -39,7 +39,8 @@ from graph_notebook.configuration.get_config import get_config, get_config_from_dict from graph_notebook.seed.load_query import get_data_sets, get_queries, normalize_model_name from graph_notebook.widgets import Force -from graph_notebook.options import OPTIONS_DEFAULT_DIRECTED, vis_options_merge +from graph_notebook.options import OPTIONS_DEFAULT_DIRECTED, OPTIONS_DEFAULT_SCALING_NODES, \ + OPTIONS_DEFAULT_SCALING_EDGES_ONLY, vis_options_merge from graph_notebook.magics.metadata import build_sparql_metadata_from_query, build_gremlin_metadata_from_query, \ build_opencypher_metadata_from_query @@ -224,6 +225,10 @@ def sparql(self, line='', cell='', local_ns: dict = None): choices=['dynamic', 'static', 'details']) parser.add_argument('--explain-format', default='text/html', help='response format for explain query mode', choices=['text/csv', 'text/html']) + parser.add_argument('-sn', '--node-scaling-property', type=str, default=None, + help='Optional property to specify what node property to use for node size scaling.') + parser.add_argument('-se', '--edge-scaling-property', type=str, default=None, + help='Optional property to specify what edge property to use for edge width scaling.') 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.") @@ -275,7 +280,9 @@ def sparql(self, line='', cell='', local_ns: dict = None): sparql_metadata = build_sparql_metadata_from_query(query_type='query', res=query_res, results=results, scd_query=True) - sn = SPARQLNetwork(expand_all=args.expand_all) + sn = SPARQLNetwork(expand_all=args.expand_all, + node_scaling_property=args.node_scaling_property, + edge_scaling_property=args.edge_scaling_property) sn.extract_prefix_declarations_from_query(cell) try: sn.add_results(results) @@ -284,6 +291,12 @@ 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: + if args.node_scaling_property: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_SCALING_NODES + elif args.edge_scaling_property: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_SCALING_EDGES_ONLY + else: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_DIRECTED self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \ = args.stop_physics self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration @@ -387,6 +400,10 @@ def gremlin(self, line, cell, local_ns: dict = None): help='Property to display the value of on each node, default is T.label') parser.add_argument('-de', '--edge-display-property', type=str, default='T.label', help='Property to display the value of on each edge, default is T.label') + parser.add_argument('-sn', '--node-scaling-property', type=str, default=None, + help='Optional property to specify what node property to use for node size scaling.') + parser.add_argument('-se', '--edge-scaling-property', type=str, default=None, + help='Optional property to specify what edge property to use for edge width scaling.') parser.add_argument('-l', '--label-max-length', type=int, default=10, 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') @@ -464,6 +481,8 @@ def gremlin(self, line, cell, local_ns: dict = None): logger.debug(f'ignore_groups: {args.ignore_groups}') gn = GremlinNetwork(group_by_property=args.group_by, display_property=args.display_property, edge_display_property=args.edge_display_property, + node_scaling_property=args.node_scaling_property, + edge_scaling_property=args.edge_scaling_property, label_max_length=args.label_max_length, ignore_groups=args.ignore_groups) if args.path_pattern == '': @@ -473,6 +492,12 @@ 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: + if args.node_scaling_property: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_SCALING_NODES + elif args.edge_scaling_property: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_SCALING_EDGES_ONLY + else: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_DIRECTED self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \ = args.stop_physics self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration @@ -1373,6 +1398,10 @@ def handle_opencypher_query(self, line, cell, local_ns): help='Property to display the value of on each node, default is ~labels') parser.add_argument('-de', '--edge-display-property', type=str, default='~labels', help='Property to display the value of on each edge, default is ~type') + parser.add_argument('-sn', '--node-scaling-property', type=str, default=None, + help='Optional property to specify what node property to use for node size scaling.') + parser.add_argument('-se', '--edge-scaling-property', type=str, default=None, + help='Optional property to specify what edge property to use for edge width scaling.') parser.add_argument('-l', '--label-max-length', type=int, default=10, 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') @@ -1386,7 +1415,7 @@ def handle_opencypher_query(self, line, cell, local_ns): logger.debug(args) titles = [] children = [] - force_graph_output=None + force_graph_output = None res = None if args.mode == 'query': query_start = time.time() * 1000 # time.time() returns time in seconds w/high precision; x1000 to get in ms @@ -1398,11 +1427,19 @@ def handle_opencypher_query(self, line, cell, local_ns): query_time=query_time) try: gn = OCNetwork(group_by_property=args.group_by, display_property=args.display_property, - edge_display_property=args.edge_display_property, - label_max_length=args.label_max_length, ignore_groups=args.ignore_groups) + edge_display_property=args.edge_display_property, label_max_length=args.label_max_length, + node_scaling_property=args.node_scaling_property, + edge_scaling_property=args.edge_scaling_property, + ignore_groups=args.ignore_groups) gn.add_results(res) logger.debug(f'number of nodes is {len(gn.graph.nodes)}') if len(gn.graph.nodes) > 0: + if args.node_scaling_property: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_SCALING_NODES + elif args.edge_scaling_property: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_SCALING_EDGES_ONLY + else: + self.graph_notebook_vis_options = OPTIONS_DEFAULT_DIRECTED self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \ = args.stop_physics self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration diff --git a/src/graph_notebook/network/EventfulNetwork.py b/src/graph_notebook/network/EventfulNetwork.py index d3a24dfa..4057fb08 100644 --- a/src/graph_notebook/network/EventfulNetwork.py +++ b/src/graph_notebook/network/EventfulNetwork.py @@ -130,17 +130,18 @@ def add_node_property(self, node_id: str, key: str, value: str): } self.dispatch_callbacks(EVENT_ADD_NODE_PROPERTY, data) - def add_node(self, node_id: str, data: dict = None): + def add_node(self, node_id: str, value: float = None, data: dict = None): if data is None: data = {} super().add_node(node_id, data) payload = { 'node_id': node_id, + 'value': value, 'data': data } self.dispatch_callbacks(EVENT_ADD_NODE, payload) - def add_edge(self, from_id: str, to_id: str, edge_id: str, label: str, data: dict = None): + def add_edge(self, from_id: str, to_id: str, edge_id: str, label: str, value: float = None, data: dict = None): if data is None: data = {} super().add_edge(from_id, to_id, edge_id, label, data) @@ -149,6 +150,7 @@ def add_edge(self, from_id: str, to_id: str, edge_id: str, label: str, data: dic 'to_id': to_id, 'edge_id': edge_id, 'label': label, + 'value': value, 'data': data } self.dispatch_callbacks(EVENT_ADD_EDGE, payload) diff --git a/src/graph_notebook/network/gremlin/GremlinNetwork.py b/src/graph_notebook/network/gremlin/GremlinNetwork.py index 664893a6..aab3f826 100644 --- a/src/graph_notebook/network/gremlin/GremlinNetwork.py +++ b/src/graph_notebook/network/gremlin/GremlinNetwork.py @@ -100,7 +100,7 @@ class GremlinNetwork(EventfulNetwork): def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length=DEFAULT_LABEL_MAX_LENGTH, group_by_property=T_LABEL, display_property=T_LABEL, edge_display_property=T_LABEL, - ignore_groups=False): + node_scaling_property=None, edge_scaling_property=None, ignore_groups=False): if graph is None: graph = MultiDiGraph() if label_max_length < 3: @@ -119,6 +119,8 @@ def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length= self.edge_display_property = self.convert_multiproperties_to_tuples(json.loads(edge_display_property)) except ValueError: self.edge_display_property = self.convert_multiproperties_to_tuples(edge_display_property) + self.node_scaling_property = self.convert_multiproperties_to_tuples(node_scaling_property) + self.edge_scaling_property = self.convert_multiproperties_to_tuples(edge_scaling_property) self.ignore_groups = ignore_groups super().__init__(graph, callbacks) @@ -285,6 +287,8 @@ def add_vertex(self, v): :param v: The vertex taken from a path traversal object. """ node_id = '' + has_value = False + scaling_value = None if type(v) is Vertex: node_id = v.id title = v.label @@ -401,7 +405,28 @@ def add_vertex(self, v): if label == '': label = title if len(title) <= self.label_max_length else title[:self.label_max_length - 3] + '...' + if self.node_scaling_property: + if isinstance(self.node_scaling_property, tuple): + if self.node_scaling_property[0] in v and isinstance(v[self.node_scaling_property[0]], list): + try: + node_scaling_value = v[self.node_scaling_property[0]][self.node_scaling_property[1]] + if isinstance(node_scaling_value, (int, float)): + has_value = True + scaling_value = float(node_scaling_value) + except IndexError: + pass + elif self.node_scaling_property in v: + if isinstance(v[self.node_scaling_property], list): + node_scaling_value = v[self.node_scaling_property][0] + else: + node_scaling_value = v[self.node_scaling_property] + if isinstance(node_scaling_value, (int, float)): + has_value = True + scaling_value = float(node_scaling_value) + data = {'properties': properties, 'label': label, 'title': title, 'group': group} + if has_value: + data['value'] = scaling_value else: node_id = str(v) title = str(v) @@ -410,7 +435,7 @@ def add_vertex(self, v): if self.ignore_groups: data['group'] = DEFAULT_GRP - self.add_node(node_id, data) + self.add_node(node_id=node_id, value=scaling_value, data=data) def add_path_edge(self, edge, from_id='', to_id='', data=None): if data is None: @@ -433,7 +458,7 @@ def add_path_edge(self, edge, from_id='', to_id='', data=None): display_label = data['properties'][self.edge_display_property] except KeyError: display_label = edge.label - self.add_edge(from_id, to_id, edge.id, display_label, data) + self.add_edge(from_id=from_id, to_id=to_id, edge_id=edge.id, label=display_label, data=data) elif type(edge) is dict: properties = {} edge_id = '' @@ -478,10 +503,25 @@ def add_path_edge(self, edge, from_id='', to_id='', data=None): edge_label = str(edge[k]) display_is_set = True + if self.edge_scaling_property: + if isinstance(self.edge_scaling_property, tuple): + if self.edge_scaling_property[0] in properties and \ + isinstance(properties[self.edge_scaling_property[0]], list): + try: + node_scaling_value = properties[self.edge_scaling_property[0]][self.edge_scaling_property[1]] + if isinstance(node_scaling_value, (int, float)): + data['value'] = float(node_scaling_value) + except IndexError: + pass + elif self.edge_scaling_property in properties: + if isinstance(properties[self.edge_scaling_property], (int, float)): + value = properties[self.edge_scaling_property] + data['value'] = float(value) + data['properties'] = properties - self.add_edge(from_id, to_id, edge_id, edge_label, data) + self.add_edge(from_id=from_id, to_id=to_id, edge_id=edge_id, label=edge_label, data=data) else: - self.add_edge(from_id, to_id, edge, str(edge), data) + self.add_edge(from_id=from_id, to_id=to_id, edge_id=edge, label=str(edge), data=data) def add_blank_edge(self, from_id, to_id, edge_id=None, undirected=True, label=''): """ @@ -497,7 +537,7 @@ def add_blank_edge(self, from_id, to_id, edge_id=None, undirected=True, label='' if edge_id is None: edge_id = str(uuid.uuid4()) edge_data = UNDIRECTED_EDGE if undirected else {} - self.add_edge(from_id, to_id, edge_id, label, edge_data) + self.add_edge(from_id=from_id, to_id=to_id, edge_id=edge_id, label=label, data=edge_data) def insert_path_element(self, path, i): if i == 0: diff --git a/src/graph_notebook/network/opencypher/OCNetwork.py b/src/graph_notebook/network/opencypher/OCNetwork.py index 5645d456..292fece6 100644 --- a/src/graph_notebook/network/opencypher/OCNetwork.py +++ b/src/graph_notebook/network/opencypher/OCNetwork.py @@ -33,8 +33,8 @@ class OCNetwork(EventfulNetwork): """ def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length=DEFAULT_LABEL_MAX_LENGTH, - group_by_property=LABEL_KEY, display_property=LABEL_KEY, - edge_display_property=EDGE_TYPE_KEY, ignore_groups=False): + group_by_property=LABEL_KEY, display_property=LABEL_KEY, edge_display_property=EDGE_TYPE_KEY, + node_scaling_property=None, edge_scaling_property=None, ignore_groups=False): if graph is None: graph = MultiDiGraph() if label_max_length < 3: @@ -53,6 +53,8 @@ def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length= self.edge_display_property = self.convert_multiproperties_to_tuples(json.loads(edge_display_property)) except ValueError: self.edge_display_property = self.convert_multiproperties_to_tuples(edge_display_property) + self.node_scaling_property = self.convert_multiproperties_to_tuples(node_scaling_property) + self.edge_scaling_property = self.convert_multiproperties_to_tuples(edge_scaling_property) self.ignore_groups = ignore_groups super().__init__(graph, callbacks) @@ -62,6 +64,8 @@ def parse_node(self, node: dict): Args: node (dict): The node dictionary to parse """ + value = None + has_value = False if LABEL_KEY in node.keys(): title = node[LABEL_KEY][0] else: @@ -131,13 +135,35 @@ def parse_node(self, node: dict): logger.debug(e) label = title + if self.node_scaling_property: + if isinstance(self.node_scaling_property, tuple): + if self.node_scaling_property[0] in props and isinstance(props[self.node_scaling_property[0]], list): + try: + node_scaling_value = props[self.node_scaling_property[0]][self.node_scaling_property[1]] + if isinstance(node_scaling_value, (int, float)): + has_value = True + value = float(node_scaling_value) + except IndexError: + pass + elif self.node_scaling_property in props: + if isinstance(props[self.node_scaling_property], list): + node_scaling_value = props[self.node_scaling_property][0] + else: + node_scaling_value = props[self.node_scaling_property] + if isinstance(node_scaling_value, (int, float)): + has_value = True + value = float(node_scaling_value) + title, label = self.strip_and_truncate_label_and_title(label, self.label_max_length) data = {'properties': props, 'label': label, 'title': title, 'group': group} + if has_value: + data['value'] = float(value) if self.ignore_groups: data['group'] = DEFAULT_GRP - self.add_node(node[ID_KEY], data) + self.add_node(node[ID_KEY], value, data) def parse_rel(self, rel): + value = None data = {'properties': self.flatten(rel), 'label': rel[EDGE_TYPE_KEY]} if self.edge_display_property is not EDGE_TYPE_KEY: try: @@ -163,7 +189,24 @@ def parse_rel(self, rel): display_label = rel[EDGE_TYPE_KEY] else: display_label = rel[EDGE_TYPE_KEY] - self.add_edge(rel[START_KEY], rel[END_KEY], rel[ID_KEY], str(display_label), data) + + if self.edge_scaling_property: + if isinstance(self.edge_scaling_property, tuple): + if self.edge_scaling_property[0] in data['properties'] and \ + isinstance(data['properties'][self.edge_scaling_property[0]], list): + try: + node_scaling_value = data['properties'][self.edge_scaling_property[0]][self.edge_scaling_property[1]] + if isinstance(node_scaling_value, (int, float)): + value = float(node_scaling_value) + data['value'] = value + except IndexError: + pass + elif self.edge_scaling_property in data['properties']: + if isinstance(data['properties'][self.edge_scaling_property], (int, float)): + value = float(data['properties'][self.edge_scaling_property]) + data['value'] = value + + self.add_edge(rel[START_KEY], rel[END_KEY], rel[ID_KEY], str(display_label), value, data) def process_result(self, res: dict): """Determines the type of element passed in and processes it appropriately diff --git a/src/graph_notebook/network/sparql/SPARQLNetwork.py b/src/graph_notebook/network/sparql/SPARQLNetwork.py index 7afb8692..79d9efcb 100644 --- a/src/graph_notebook/network/sparql/SPARQLNetwork.py +++ b/src/graph_notebook/network/sparql/SPARQLNetwork.py @@ -50,12 +50,16 @@ def __init__(self, graph: MultiDiGraph = None, callbacks: list = None, label_max_length: int = DEFAULT_LABEL_MAX_LENGTH, + node_scaling_property=None, + edge_scaling_property=None, expand_all: bool = False): if graph is None: graph = MultiDiGraph() self.expand_all = expand_all self.label_max_length = label_max_length + self.node_scaling_property = node_scaling_property + self.edge_scaling_property = edge_scaling_property super().__init__(graph, callbacks) self.namespace_to_prefix = { # http://foo/bar/ -> bar NAMESPACE_RDFS: PREFIX_RDFS, @@ -94,12 +98,13 @@ def extract_prefix_declarations_from_query(self, query: str): self.namespace_to_prefix[namespace] = shorthand self.prefix_to_namespace[shorthand] = namespace - def add_node(self, node_id: str, data: dict = None): + def add_node(self, node_id: str, value: float = None, data: dict = None): """ overriding parent add_node class to automatically parse the uri for a node and add data to the node for prefix and shortened name :param node_id: the full uri :param data: dict to set node initial node properties + :param value: optional property value to use for node size scaling """ if data is None: data = {} @@ -116,8 +121,9 @@ def add_node(self, node_id: str, data: dict = None): label = title if len(title) <= self.label_max_length else title[:self.label_max_length - 3] + '...' data['label'] = label data['title'] = title + data['value'] = value - super().add_node(node_id, data) + super().add_node(node_id, value, data) @staticmethod def extract_value(uri: str) -> str: @@ -183,7 +189,10 @@ def add_results(self, results): with the variables "subject" ,"predicate", "object" or "s", "p", "o" :param results: """ - + has_scaling_value = False + subject_value = None + predicate_value = None + object_value = None # validate that we can process this result.. vars = [] if 'head' in results and 'vars' in results['head']: @@ -266,7 +275,7 @@ def add_results(self, results): obj = b[object_binding] if sub['value'] != current_subject: - self.add_node(current_subject, data) + self.add_node(node_id=current_subject, data=data) data = {'properties': {}} current_subject = sub['value'] @@ -291,7 +300,7 @@ def add_results(self, results): data['title'] = title data['label'] = label - # object is a literal. Check if data has this preciate already. If it does, turn its value into an + # object is a literal. Check if data has this predicate already. If it does, turn its value into an # array and append the new value to it. if 'properties' in data and f'{prefix}:{value}' in data['properties']: if type(data['properties'][f'{prefix}:{value}']) is list: @@ -312,7 +321,9 @@ def add_results(self, results): data['properties'][pred['value']] = obj['value'] # add the last node and all our edges - self.add_node(current_subject, data) + # TODO: figure out where we can get the scaling value + # self.add_node(node_id=current_subject, value=, data=data) + self.add_node(node_id=current_subject, data=data) self.process_edge_bindings(edge_bindings, use_spo) return @@ -320,6 +331,7 @@ def process_edge_bindings(self, bindings, use_spo=False): subject_binding = 'subject' predicate_binding = 'predicate' object_binding = 'object' + scaling_value = None if use_spo: subject_binding = 's' @@ -337,8 +349,10 @@ def process_edge_bindings(self, bindings, use_spo=False): if pred['type'] == 'uri': prefix = self.extract_prefix(pred['value']) value = self.extract_value(pred['value']) + scaling_value = value edge_label = f'{prefix}:{value}' if not self.graph.has_node(b[object_binding]['value']): self.add_node(b[object_binding]['value']) - self.add_edge(b[subject_binding]['value'], b[object_binding]['value'], pred['value'], edge_label) + self.add_edge(from_id=b[subject_binding]['value'], to_id=b[object_binding]['value'], edge_id=pred['value'], + label=edge_label, value=scaling_value) diff --git a/src/graph_notebook/options/__init__.py b/src/graph_notebook/options/__init__.py index 9d674bbb..ca1837be 100644 --- a/src/graph_notebook/options/__init__.py +++ b/src/graph_notebook/options/__init__.py @@ -3,4 +3,5 @@ SPDX-License-Identifier: Apache-2.0 """ -from .options import OPTIONS_DEFAULT_DIRECTED, vis_options_merge # noqa F401 +from .options import OPTIONS_DEFAULT_DIRECTED, OPTIONS_DEFAULT_SCALING_NODES, OPTIONS_DEFAULT_SCALING_EDGES_ONLY, \ + vis_options_merge # noqa F401 diff --git a/src/graph_notebook/options/options.py b/src/graph_notebook/options/options.py index d6920d56..f8ebc28c 100644 --- a/src/graph_notebook/options/options.py +++ b/src/graph_notebook/options/options.py @@ -75,6 +75,165 @@ } } +OPTIONS_DEFAULT_SCALING_EDGES_ONLY = { + "nodes": { + "borderWidthSelected": 0, + "borderWidth": 0, + "color": { + "background": "rgba(210, 229, 255, 1)", + "border": "transparent", + "highlight": { + "background": "rgba(9, 104, 178, 1)", + "border": "rgba(8, 62, 100, 1)" + } + }, + "shadow": { + "enabled": False + }, + "shape": "circle", + "widthConstraint": { + "minimum": 70, + "maximum": 70 + }, + "font": { + "face": "courier new", + "color": "black", + "size": 12 + }, + }, + "edges": { + "arrowStrikethrough": False, + "color": { + "inherit": False + }, + "smooth": { + "enabled": True, + "type": "straightCross" + }, + "arrows": { + "to": { + "enabled": True, + "type": "arrow" + } + }, + "font": { + "face": "courier new" + }, + "scaling": { + "min": 3, + "max": 9, + "label": { + "min": 12, + "max": 12 + } + } + }, + "interaction": { + "hover": True, + "hoverConnectedEdges": True, + "selectConnectedEdges": False + }, + "physics": { + "minVelocity": 0.75, + "barnesHut": { + "centralGravity": 0.1, + "gravitationalConstant": -50450, + "springLength": 95, + "springConstant": 0.04, + "damping": 0.09, + "avoidOverlap": 0.1 + }, + "solver": "barnesHut", + "enabled": True, + "adaptiveTimestep": True, + "stabilization": { + "enabled": True, + "iterations": 1 + } + } +} + +OPTIONS_DEFAULT_SCALING_NODES = { + "nodes": { + "borderWidthSelected": 0, + "borderWidth": 0, + "color": { + "background": "rgba(210, 229, 255, 1)", + "border": "transparent", + "highlight": { + "background": "rgba(9, 104, 178, 1)", + "border": "rgba(8, 62, 100, 1)" + } + }, + "shadow": { + "enabled": False + }, + "shape": "circle", + "font": { + "face": "courier new", + "color": "black" + }, + "scaling": { + "min": 20, + "max": 10000, + "label": { + "min": 8, + "max": 24 + } + } + }, + "edges": { + "arrowStrikethrough": False, + "color": { + "inherit": False + }, + "smooth": { + "enabled": True, + "type": "straightCross" + }, + "arrows": { + "to": { + "enabled": True, + "type": "arrow" + } + }, + "font": { + "face": "courier new" + }, + "scaling": { + "min": 3, + "max": 9, + "label": { + "min": 12, + "max": 12 + } + } + }, + "interaction": { + "hover": True, + "hoverConnectedEdges": True, + "selectConnectedEdges": False + }, + "physics": { + "minVelocity": 0.75, + "barnesHut": { + "centralGravity": 0.1, + "gravitationalConstant": -50450, + "springLength": 95, + "springConstant": 0.04, + "damping": 0.09, + "avoidOverlap": 0.1 + }, + "solver": "barnesHut", + "enabled": True, + "adaptiveTimestep": True, + "stabilization": { + "enabled": True, + "iterations": 1 + } + } +} + def vis_options_merge(original, target): """Merge the target dict with the original dict, without modifying the input dicts. diff --git a/src/graph_notebook/widgets/src/force_widget.ts b/src/graph_notebook/widgets/src/force_widget.ts index b5ee3fae..ce38242d 100644 --- a/src/graph_notebook/widgets/src/force_widget.ts +++ b/src/graph_notebook/widgets/src/force_widget.ts @@ -131,9 +131,11 @@ export class ForceView extends DOMWidgetView { returned, not json. */ + console.log("FORCE: Generating new network."); const network = this.model.get("network"); this.visOptions = this.model.get("options"); this.populateDatasets(network); + console.log("FORCE: Populated datasets from network."); const dataset = { nodes: this.nodeDataset, edges: this.edgeDataset, @@ -175,7 +177,11 @@ export class ForceView extends DOMWidgetView { */ populateDatasets(network: ForceNetwork): void { const edges = this.linksToEdges(network.graph.links); + console.log("Nodes:"); + console.log(network.graph.nodes); this.nodeDataset.update(network.graph.nodes); + console.log("Edges:"); + console.log(edges); this.edgeDataset.update(edges); } @@ -212,6 +218,7 @@ export class ForceView extends DOMWidgetView { */ interceptCustom(msg: Message): void { const msgData = msg["data"]; + console.log(msgData); switch (msg["method"]) { case "add_node": this.addNode(msgData); @@ -250,6 +257,8 @@ export class ForceView extends DOMWidgetView { } */ addNode(msgData: DynamicObject): void { + console.log("addNode: adding new node"); + console.log(msgData); if (!msgData.hasOwnProperty("node_id")) { // message data must have an id to add a node return; @@ -270,6 +279,12 @@ export class ForceView extends DOMWidgetView { // no label found, using node id node["label"] = id; } + + if (!node.hasOwnProperty("value")) { + // no value found, using default + node["value"] = 0; + } + this.nodeDataset.update([node]); return; } @@ -351,6 +366,8 @@ export class ForceView extends DOMWidgetView { addEdge(msgData: DynamicObject): void { // To be able to add an edge, we require the message to have: // 'from_id', 'to_id', and 'edge_id' + console.log("addEdge: adding new edge"); + console.log(msgData); if ( !msgData.hasOwnProperty("from_id") || !msgData.hasOwnProperty("to_id") || @@ -360,14 +377,20 @@ export class ForceView extends DOMWidgetView { } // check if we have a label. if we do not, use the edge id. + const label = msgData.hasOwnProperty("label") ? msgData["label"] : msgData["edge_id"]; + const innerData = msgData.hasOwnProperty("data") ? msgData["data"] : {}; const edgeID = msgData["from_id"] + ":" + msgData["to_id"] + ":" + msgData["edge_id"]; + const value = msgData.hasOwnProperty("value") + ? msgData["value"] + : 0; + // rearrange the data to add to a node to ensure it conforms to the format of a vis-edge. // More info found here: https://github.com/visjs/vis-network const copiedData = { @@ -375,6 +398,7 @@ export class ForceView extends DOMWidgetView { to: msgData["to_id"], id: edgeID, label: label, + value: value, ...innerData, }; let edge = this.edgeDataset.get(edgeID); diff --git a/test/unit/network/gremlin/test_gremlin_network.py b/test/unit/network/gremlin/test_gremlin_network.py index ea84b9cb..7b6574dc 100644 --- a/test/unit/network/gremlin/test_gremlin_network.py +++ b/test/unit/network/gremlin/test_gremlin_network.py @@ -32,7 +32,8 @@ def test_add_vertex_with_callback(self): 'runways': '4', 'type': 'Airport'}, 'title': 'airport'}, - 'node_id': '1234'} + 'node_id': '1234', + 'value': None} def add_node_callback(network, event_name, data): self.assertEqual(event_name, EVENT_ADD_NODE) diff --git a/test/unit/network/opencypher/test_opencypher_network.py b/test/unit/network/opencypher/test_opencypher_network.py index ed717006..a897ca34 100644 --- a/test/unit/network/opencypher/test_opencypher_network.py +++ b/test/unit/network/opencypher/test_opencypher_network.py @@ -39,8 +39,10 @@ def test_add_node_with_callback(self): "~labels": ['airport'], 'code': 'SEA', 'runways': 3}, - 'title': "airport"}, - 'node_id': '22'} + 'title': "airport" + }, + 'node_id': '22', + 'value': None} def add_node_callback(network, event_name, data): self.assertEqual(event_name, EVENT_ADD_NODE) @@ -51,6 +53,8 @@ def add_node_callback(network, event_name, data): gn.add_results(res) self.assertTrue(reached_callback[EVENT_ADD_NODE]) node = gn.graph.nodes.get("22") + print(expected_data['data']['properties']) + print(node['properties']) self.assertEqual(expected_data['data']['properties'], node['properties']) def test_add_edge_with_callback(self): @@ -87,6 +91,7 @@ def test_add_edge_with_callback(self): 'label': 'route', 'from_id': "22", 'to_id': '151', + 'value': None, 'edge_id': '7389' } diff --git a/test/unit/network/sparql/test_sparql_network.py b/test/unit/network/sparql/test_sparql_network.py index 36f37f34..7daf3fe5 100644 --- a/test/unit/network/sparql/test_sparql_network.py +++ b/test/unit/network/sparql/test_sparql_network.py @@ -100,9 +100,11 @@ def add_node_callback(network, event_name, data): 'data': { 'label': 'resourc...', 'prefix': 'resource', - 'title': 'resource:24' + 'title': 'resource:24', + 'value': '24' }, - 'node_id': 'http://kelvinlawrence.net/air-routes/resource/24' + 'node_id': 'http://kelvinlawrence.net/air-routes/resource/24', + 'value': '24' } self.assertEqual(expected_data, data) node = network.graph.nodes.get(node_id) diff --git a/test/unit/network/test_eventful_network.py b/test/unit/network/test_eventful_network.py index 4affae67..e1abee1f 100644 --- a/test/unit/network/test_eventful_network.py +++ b/test/unit/network/test_eventful_network.py @@ -12,6 +12,7 @@ class TestEventfulNetwork(TestCase): def test_add_node_callback_dispatch(self): node_id = '1' + node_value = '0.0' node_data = { 'foo': 'bar' } @@ -26,7 +27,7 @@ def add_node_callback(network, event_name, data): en = EventfulNetwork() en.register_callback(EVENT_ADD_NODE, add_node_callback) - en.add_node(node_id, node_data) + en.add_node(node_id, node_value, node_data) self.assertIsNotNone(en.graph.nodes.get(node_id)) self.assertTrue(callbacks_reached[EVENT_ADD_NODE]) @@ -59,6 +60,7 @@ def test_add_edge_callback_dispatched(self): edge_label = edge_id edge_data = dict() edge_data['foo'] = 'bar' + edge_value = 0.0 callback_reached = {} @@ -69,6 +71,7 @@ def add_edge_callback(network, event_name, data): 'to_id': to_id, 'edge_id': edge_id, 'label': edge_label, + 'value': edge_value, 'data': edge_data } self.assertEqual(expected_payload, data) @@ -78,7 +81,7 @@ def add_edge_callback(network, event_name, data): en = EventfulNetwork(callbacks={EVENT_ADD_EDGE: [add_edge_callback]}) en.add_node(from_id) en.add_node(to_id) - en.add_edge(from_id, to_id, edge_id, edge_label, edge_data) + en.add_edge(from_id, to_id, edge_id, edge_label, edge_value, edge_data) self.assertTrue(callback_reached[EVENT_ADD_EDGE]) def test_add_node_data_callback_dispatched(self):