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 {}, []