From d55eeef87e066741cbf79a6a0ba14b64f1f4952e Mon Sep 17 00:00:00 2001 From: Tristan Nolde Date: Sun, 7 Jan 2024 13:53:32 +0100 Subject: [PATCH] feat(ui): Include graph on person page and fix neighbor calculation (#533) Includes the company/person relation graph on the persondetails page incl. the number of relations on lvl 1,2 and 3, the calculation of which was also fixed as part of the development. The graph displayed on the company details page now also includes **all** relations (company + person) until the 3rd lvl. --------- Co-authored-by: Philipp Horstenkamp --- .../ui/company_elements.py | 19 +-- .../ui/pages/person.py | 7 +- .../ui/person_elements.py | 59 +++++++- .../utils/networkx/network_2d.py | 40 +++-- .../utils/networkx/networkx_data.py | 140 ++++++++++++++++-- 5 files changed, 217 insertions(+), 48 deletions(-) diff --git a/src/aki_prj23_transparenzregister/ui/company_elements.py b/src/aki_prj23_transparenzregister/ui/company_elements.py index 938f42f..3859417 100644 --- a/src/aki_prj23_transparenzregister/ui/company_elements.py +++ b/src/aki_prj23_transparenzregister/ui/company_elements.py @@ -15,10 +15,11 @@ from aki_prj23_transparenzregister.ui import ( sentiment_elements, ) from aki_prj23_transparenzregister.utils.networkx.network_2d import create_2d_graph -from aki_prj23_transparenzregister.utils.networkx.network_base import initialize_network +from aki_prj23_transparenzregister.utils.networkx.network_base import ( + initialize_network_without_metrics, +) from aki_prj23_transparenzregister.utils.networkx.networkx_data import ( - create_edge_and_node_list_for_company, - find_company_relations, + get_relations_until_level_3, ) COLORS = { @@ -414,20 +415,16 @@ def network_layout(selected_company_id: int) -> html.Div: Returns: The html div to create the network tab of the company page. """ - person_relations, company_relations = find_company_relations(selected_company_id) - - # Create Edge and Node List from data - nodes, edges = create_edge_and_node_list_for_company(company_relations) + nodes, edges = get_relations_until_level_3(f"c_{str(selected_company_id)}") # Initialize the Network and receive the Graph and a DataFrame with Metrics if nodes != {}: - graph, metrics = initialize_network(nodes=nodes, edges=edges) - metric = "None" + graph = initialize_network_without_metrics(nodes=nodes, edges=edges) figure = create_2d_graph( graph, nodes, edges, - metrics, - metric, + pd.DataFrame(), + metric="None", layout="Spring", edge_annotation=True, edge_thickness=1, diff --git a/src/aki_prj23_transparenzregister/ui/pages/person.py b/src/aki_prj23_transparenzregister/ui/pages/person.py index dff55e3..2391ea6 100644 --- a/src/aki_prj23_transparenzregister/ui/pages/person.py +++ b/src/aki_prj23_transparenzregister/ui/pages/person.py @@ -31,12 +31,7 @@ def layout(value: str = "1") -> html.Div: # get all necessary data of the selected person selected_person_stats = data_elements.get_person_data(session).loc[person_id] selected_person_name = f"{selected_person_stats['person_firstname']} {selected_person_stats['person_lastname']}" - return ( - person_elements.create_person_widgets(person_id, selected_person_name), - html.Div( - className="person-network", children=["Hier kann der Social Graph hin"] - ), - ) + return person_elements.create_person_widgets(person_id, selected_person_name) @callback( diff --git a/src/aki_prj23_transparenzregister/ui/person_elements.py b/src/aki_prj23_transparenzregister/ui/person_elements.py index 0bfcb7c..ee13f7a 100644 --- a/src/aki_prj23_transparenzregister/ui/person_elements.py +++ b/src/aki_prj23_transparenzregister/ui/person_elements.py @@ -1,9 +1,15 @@ """Dash elements for person page.""" import networkx as nx -from dash import html +import pandas as pd +from dash import dcc, html +from aki_prj23_transparenzregister.utils.networkx.network_2d import create_2d_graph +from aki_prj23_transparenzregister.utils.networkx.network_base import ( + initialize_network_without_metrics, +) from aki_prj23_transparenzregister.utils.networkx.networkx_data import ( get_relations_number_from_id, + get_relations_until_level_3, ) @@ -44,7 +50,7 @@ def create_person_widgets(selected_person_id: int, selected_person_name: str) -> """ try: first_level, second_level, third_level = get_relations_number_from_id( - selected_person_id + f"p_{selected_person_id}" ) except nx.exception.NetworkXError: @@ -67,13 +73,56 @@ def create_person_widgets(selected_person_id: int, selected_person_name: str) -> ], ), create_small_widget( - ["Anzahl Verbindungen", html.Br(), " - erste Ebene"], [first_level] + ["Anzahl Verbindungen", html.Br(), "1. Ebene"], [first_level] ), create_small_widget( - ["Anzahl Verbindungen", html.Br(), " - dritte Ebene"], [second_level] + ["Anzahl Verbindungen", html.Br(), "2. Ebene"], [second_level] ), create_small_widget( - ["Anzahl Verbindungen", html.Br(), " - dritte Ebene"], [third_level] + ["Anzahl Verbindungen", html.Br(), "3. Ebene"], [third_level] ), + network_layout(selected_person_id), + ], + ) + + +def network_layout(selected_person_id: int) -> html.Div: + """Create network tab. + + Args: + selected_person_id: Id of the chosen person in the dropdown. + + Returns: + The html div to create the network tab of the person page. + """ + nodes, edges = get_relations_until_level_3(f"p_{selected_person_id}") + # Initialize the Network and receive the Graph and a DataFrame with Metrics + if nodes != {}: + graph = initialize_network_without_metrics(nodes=nodes, edges=edges) + figure = create_2d_graph( + graph, + nodes, + edges, + pd.DataFrame([]), + metric="None", + layout="Spring", + edge_annotation=True, + edge_thickness=1, + ) + return html.Div( + children=[ + dcc.Graph(figure=figure, id="person-graph", className="graph-style") + ] + ) + + return html.Div( + className="choose-metric", + children=[ + html.H3( + className="metrics-title", + children=[ + "Für diese Person wurden leider keine Verflechtungen gefunden." + ], + ) ], ) diff --git a/src/aki_prj23_transparenzregister/utils/networkx/network_2d.py b/src/aki_prj23_transparenzregister/utils/networkx/network_2d.py index 8bd993d..c6ee701 100644 --- a/src/aki_prj23_transparenzregister/utils/networkx/network_2d.py +++ b/src/aki_prj23_transparenzregister/utils/networkx/network_2d.py @@ -72,6 +72,8 @@ def create_2d_graph( # noqa PLR0913 node_x.append(x) node_y.append(y) + edge_type_list = [row["type"] for row in edges] + # Add the Edges to the scatter plot according to their Positions. edge_trace = go.Scatter( x=edge_x, @@ -80,16 +82,29 @@ def create_2d_graph( # noqa PLR0913 hoverinfo="none", mode="lines", ) + # Add the Edge description text to the scatter plot according to its Position. - edge_weights_trace = go.Scatter( - x=edge_weight_x, - y=edge_weight_y, - mode="text", - marker_size=0.5, - text=[0.45, 0.7, 0.34], - textposition="top center", - hovertemplate="Relation: %{text}", - ) + if edge_annotation is True: + edge_weights_trace = go.Scatter( + x=edge_weight_x, + y=edge_weight_y, + mode="text", + marker_size=0.5, + text=[0.45, 0.7, 0.34], + textposition="top center", + hoverinfo="none", + ) + else: + edge_weights_trace = go.Scatter( + x=edge_weight_x, + y=edge_weight_y, + mode="text", + marker_size=0.5, + text=[0.45, 0.7, 0.34], + textposition="top center", + hovertemplate="Relation: %{hovertext}", + hovertext=edge_type_list, + ) # Add the Nodes to the scatter plot according to their Positions. node_trace = go.Scatter( @@ -106,9 +121,7 @@ def create_2d_graph( # noqa PLR0913 # # Set Color by using the nodes DataFrame with its Color Attribute. The sequence matters! colors = list(nx.get_node_attributes(graph, "color").values()) node_names = list(nx.get_node_attributes(graph, "name").values()) - # ids = list(nx.get_node_attributes(graph, "id").values()) - # # Get the Node Text node_trace.marker.color = colors node_trace.marker.line = {"color": "#2e2c2f", "width": 0.5} node_trace.text = node_names @@ -118,10 +131,7 @@ def create_2d_graph( # noqa PLR0913 node_trace.marker.size = list(np.sqrt(metrics[metric]) * 200) # Add Relation_Type as a Description for the edges. - if edge_annotation: - edge_type_list = [ - row["type"] for row in edges - ] # this code be moved and used as hover data + if edge_annotation is True: # this code be moved and used as hover data edge_weights_trace.text = edge_type_list # Return the Plotly Figure diff --git a/src/aki_prj23_transparenzregister/utils/networkx/networkx_data.py b/src/aki_prj23_transparenzregister/utils/networkx/networkx_data.py index 19f3b9c..47aebd8 100644 --- a/src/aki_prj23_transparenzregister/utils/networkx/networkx_data.py +++ b/src/aki_prj23_transparenzregister/utils/networkx/networkx_data.py @@ -283,6 +283,52 @@ def find_company_relations( return pd.DataFrame(), company_relations # company_relations +def find_person_relations( + selected_person_id: int, +) -> pd.DataFrame: + """Finds all Relations for the given Person id. + + Args: + selected_person_id: Id of the Person which Relations should be returned. + + Returns: + DataFrame + """ + session = SessionHandler.session + assert session # noqa: S101 + relations_person_data = ( + session.query( + entities.Company.id.label("id_company"), + entities.Company.name.label("name_company"), + entities.PersonRelation.relation.label("relation_type"), + entities.Person.id.label("id_person"), + entities.Person.lastname.label("lastname"), + entities.Person.firstname.label("firstname"), + entities.Person.date_of_birth.label("date_of_birth"), + ) + .join( + entities.PersonRelation, + entities.PersonRelation.company_id == entities.Company.id, + ) + .join( + entities.Person, + entities.PersonRelation.person_id == entities.Person.id, + ) + .filter(entities.Person.id == selected_person_id) + .all() + ) + person_relations = pd.DataFrame(relations_person_data) # type: ignore + + person_relations["id_company"] = person_relations["id_company"].apply( + lambda x: f"c_{x}" + ) + person_relations["id_person"] = person_relations["id_person"].apply( + lambda x: f"p_{x}" + ) + + return person_relations + + def create_edge_and_node_list_for_company( company_relations: pd.DataFrame, ) -> tuple[dict, list]: @@ -347,6 +393,18 @@ def get_all_metrics_from_id(company_id: int) -> pd.Series: return filtered_metrics.iloc[0] +def filter_sets(sets: list[set]) -> set: + """Filters the list of sets given to only contain unique entries. + + Args: + sets (list[set]): List of sets to be filtered. + + Returns: + list[set]: List of sets with only unique entries. + """ + return set.union(*sets) + + @lru_cache def get_relations_number_from_id(id: str) -> tuple[int, int, int]: """Returns all Relation in 1, 2 and 3 lvl of one Node. @@ -365,20 +423,80 @@ def get_relations_number_from_id(id: str) -> tuple[int, int, int]: nodes_tmp, edges_tmp = create_edge_and_node_list(person_df, company_df) graph = initialize_network_without_metrics(nodes=nodes_tmp, edges=edges_tmp) - neighbors = nx.all_neighbors(graph, id) + try: + neighbors = nx.all_neighbors(graph, id) - relations_lv1 = set(neighbors) - relations_lv2 = set() - relations_lv3 = set() + relations_lv1 = set(neighbors) + relations_lv2 = set() + relations_lv3 = set() - for node in relations_lv1: - relations_lv2 |= set(nx.all_neighbors(graph, node)) + for node in relations_lv1: + relations_lv2 |= set(nx.all_neighbors(graph, node)) - relations_lv2.discard(id) + relations_lv2.discard(id) - for sub_node in relations_lv2: - relations_lv3 |= set(nx.all_neighbors(graph, sub_node)) + for sub_node in relations_lv2: + relations_lv3 |= set(nx.all_neighbors(graph, sub_node)) - relations_lv2.difference(relations_lv3) + unique_entires = filter_sets([relations_lv1, relations_lv2]) + relations_lv3 = {key for key in relations_lv3 if key not in unique_entires} - return len(relations_lv1), len(relations_lv2), len(relations_lv3) + return len(relations_lv1), len(relations_lv2), len(relations_lv3) + except nx.exception.NetworkXError: + return 0, 0, 0 + + +@lru_cache +def get_relations_until_level_3(id: str) -> tuple[dict, list]: + """Returns all Relation in 1, 2 and 3 lvl of one Node. + + Args: + id: String of the Company or Person Id. + + Returns: + tuple[dict, list]: nodes, edges + """ + # Get Data + person_df = get_all_person_relations() + company_df = get_all_company_relations() + + # Create Edge and Node List from data + nodes_tmp, edges_tmp = create_edge_and_node_list(person_df, company_df) + graph = initialize_network_without_metrics(nodes=nodes_tmp, edges=edges_tmp) + + try: + neighbors = nx.all_neighbors(graph, id) + + relations_lv1 = set(neighbors) + relations_lv2 = set() + relations_lv3 = set() + + for node in relations_lv1: + relations_lv2 |= set(nx.all_neighbors(graph, node)) + + relations_lv2.discard(id) + + for sub_node in relations_lv2: + relations_lv3 |= set(nx.all_neighbors(graph, sub_node)) + + unique_entires = filter_sets([relations_lv1, relations_lv2]) + relations_lv3 = {key for key in relations_lv3 if key not in unique_entires} + + nodes = { + key: value + for key, value in nodes_tmp.items() + if ( + key in relations_lv1 + or key in relations_lv2 + or key in relations_lv3 + or key == id + ) + } + edges = [ + edge + for edge in edges_tmp + if (edge["from"] in nodes and edge["to"] in nodes) + ] + return nodes, edges + except nx.exception.NetworkXError: + return {}, []