From e5b61bc19c4f5588bfe152f1ad1394692af91b98 Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Sat, 11 Nov 2023 13:47:46 +0100 Subject: [PATCH] Added multi relation dropdowns to dashbord (#363) This change allows for a more complete combination of relation combinations to be filtered. --- .github/workflows/test-and-build-action.yaml | 2 +- .../ui/assets/networkx_style.css | 4 +- .../ui/pages/home.py | 208 +++++++++++------- .../utils/networkx/network_3d.py | 12 +- .../utils/networkx/network_base.py | 7 +- .../utils/networkx/networkx_data.py | 15 +- tests/ui/home_page_test.py | 3 +- tests/utils/networkx/networkx_data_test.py | 2 +- 8 files changed, 147 insertions(+), 106 deletions(-) diff --git a/.github/workflows/test-and-build-action.yaml b/.github/workflows/test-and-build-action.yaml index 50a1213..e9ed349 100644 --- a/.github/workflows/test-and-build-action.yaml +++ b/.github/workflows/test-and-build-action.yaml @@ -154,7 +154,7 @@ jobs: run: exit 0 - name: Exit workflow on not main branch - if: ${{ github.ref != 'refs/heads/main'}} + if: ${{ github.ref != 'refs/heads/main' }} run: exit 0 - name: Login to GitHub Container Registry diff --git a/src/aki_prj23_transparenzregister/ui/assets/networkx_style.css b/src/aki_prj23_transparenzregister/ui/assets/networkx_style.css index a00fd21..949ee57 100644 --- a/src/aki_prj23_transparenzregister/ui/assets/networkx_style.css +++ b/src/aki_prj23_transparenzregister/ui/assets/networkx_style.css @@ -10,7 +10,7 @@ height: 100%; } -.top_companytable_style { +.top_company_table_style { float: left; margin-top: 20px; margin-left: 20px; @@ -91,7 +91,7 @@ } .networkx_style .graph-style { - + margin-top: 10px; padding-bottom: 0px; display: inline-block; diff --git a/src/aki_prj23_transparenzregister/ui/pages/home.py b/src/aki_prj23_transparenzregister/ui/pages/home.py index f643323..c20e8e3 100644 --- a/src/aki_prj23_transparenzregister/ui/pages/home.py +++ b/src/aki_prj23_transparenzregister/ui/pages/home.py @@ -4,7 +4,6 @@ from functools import lru_cache import dash import dash_daq as daq import networkx as nx -import numpy as np import pandas as pd import plotly.graph_objects as go from cachetools import TTLCache, cached @@ -40,50 +39,113 @@ dash.register_page( # ], ) -metric = "None" -switch_edge_annotaion_value = False -egde_thickness = 1 -network = None + +def person_relation_type_filter() -> list[str]: + """Returns a Numpy Array of String with Person relation types.""" + return get_all_person_relations()["relation_type"].unique().tolist() -def person_relation_type_filter() -> np.ndarray: - """Returns an Numpy Array of String with Person telation types.""" - return get_all_person_relations()["relation_type"].unique() - - -def company_relation_type_filter() -> np.ndarray: - """Returns an Numpy Array of String with Company relation types.""" - return get_all_company_relations()["relation_type"].unique() +def company_relation_type_filter() -> list[str]: + """Returns a Numpy Array of String with Company relation types.""" + return get_all_company_relations()["relation_type"].unique().tolist() def update_table( metric_dropdown_value: str, metrics: pd.DataFrame ) -> tuple[dict, list]: - """_summary_. - - Args: - metric_dropdown_value (str): _description_ - metrics (pd.DataFrame): _description_ - - Returns: - tuple[dict, list]: _description_ - """ + """_summary_.""" table_df = metrics.sort_values(metric_dropdown_value, ascending=False).head(10) table_df = table_df[["designation", "category", metric_dropdown_value]] columns = [{"name": i, "id": i} for i in table_df.columns] return table_df.to_dict("records"), columns # type: ignore -def layout() -> list[html.Div]: +@cached(TTLCache(20, ttl=600)) +def _update_figure( # noqa: PLR0913 + selected_metric: str, + switch_value: bool, + switch_edge_annotation_value: bool, + c_relation_filter_value: frozenset[str], + p_relation_filter_value: frozenset[str], + layout: str, + slider_value: float, + metric_dropdown_value: str, +) -> tuple[dict, list, go.Figure]: + """In this Callback the Value of the Dropdown is used to filter the Data. + + In Addition, it takes the filter for the Graph metrics and creates a new graph, or switches between 3D and 2D. + + Args: + selected_metric: Selected Value + switch_value: True if 2D, False if 3D + switch_edge_annotation_value: True if Edge should have a description, False = No Description + c_relation_filter_value: Variable with String value of Relation Type for Companies + p_relation_filter_value: Variable with String value of Relation Type for Persons + layout: String of the Layout Dropdown + metric_dropdown_value: String of the Metric Dropdown + slider_value: Sets the size of the Edge Connections + + + Returns: + Network Graph(Plotly Figure): Plotly Figure in 3 or 2D + """ + _ = c_relation_filter_value, p_relation_filter_value + + graph, metrics, nodes, edges = update_graph_data( + person_relation_type=p_relation_filter_value, + company_relation_type=c_relation_filter_value, + ) + + table_dict, table_columns = update_table(metric_dropdown_value, metrics) + + if switch_value: + return ( + table_dict, + table_columns, + create_2d_graph( + graph, + nodes, + edges, + metrics, + selected_metric, + layout, + switch_edge_annotation_value, + slider_value, # type: ignore + ), + ) + + return ( + table_dict, + table_columns, + create_3d_graph( + graph, + nodes, + edges, + metrics, + selected_metric, + layout, + switch_edge_annotation_value, + slider_value, # type: ignore + ), + ) + + +def layout() -> list[html]: """Generates the Layout of the Homepage.""" person_relation_types = person_relation_type_filter() company_relation_types = company_relation_type_filter() - top_companies_dict, top_companies_columns, figure = update_figure( + selected_company_relation_types: frozenset[str] = frozenset( + {company_relation_types[1]} + ) + selected_person_relation_types: frozenset[str] = frozenset( + {} + ) # frozenset({person_relation_types[1]}) + top_companies_dict, top_companies_columns, figure = _update_figure( "None", False, False, - company_relation_types[0], - person_relation_types[0], + selected_company_relation_types, + selected_person_relation_types, "Spring", 1, "degree", @@ -92,7 +154,7 @@ def layout() -> list[html.Div]: children=html.Div( children=[ html.Div( - className="top_companytable_style", + className="top_company_table_style", children=[ html.H1( title="Top Ten Nodes in Graph by Metric", @@ -143,9 +205,11 @@ def layout() -> list[html.Div]: ), dcc.Dropdown( company_relation_types, - company_relation_types[0], + list(selected_company_relation_types), id="dropdown_company_relation_filter", className="dropdown_style", + multi=True, + persistence=True, ), ], ), @@ -159,9 +223,11 @@ def layout() -> list[html.Div]: ), dcc.Dropdown( person_relation_types, - person_relation_types[0], + list(selected_person_relation_types), id="dropdown_person_relation_filter", className="dropdown_style", + multi=True, + persistence=True, ), ], ), @@ -282,7 +348,10 @@ def layout() -> list[html.Div]: ], ), dcc.Graph( - figure=figure, id="my-graph", className="graph-style" + figure=figure, + id="my-graph", + className="graph-style", + config={"displaylogo": False}, ), ], ), @@ -293,8 +362,8 @@ def layout() -> list[html.Div]: @lru_cache(200) def update_graph_data( - person_relation_type: str = "HAFTENDER_GESELLSCHAFTER", - company_relation_type: str = "GESCHAEFTSFUEHRER", + person_relation_type: frozenset[str] | None = None, + company_relation_type: frozenset[str] | None = None, ) -> tuple[nx.Graph, pd.DataFrame, dict, list]: """_summary_. @@ -305,7 +374,6 @@ def update_graph_data( Returns: tuple[nx.Graph, pd.DataFrame, dict, list]: _description_ """ - # Get Data person_df = get_all_person_relations() company_df = get_all_company_relations() @@ -339,71 +407,41 @@ def update_graph_data( prevent_initial_call=True, allow_duplicate=True, ) -# @lru_cache(20) -@cached(cache=TTLCache(maxsize=100, ttl=500)) def update_figure( # noqa: PLR0913 selected_metric: str, switch_value: bool, - # switch_node_annotaion_value: bool, - switch_edge_annotaion_value: bool, - c_relation_filter_value: str, - p_relation_filter_value: str, + switch_edge_annotation_value: bool, + c_relation_filter_value: list[str], + p_relation_filter_value: list[str], layout: str, slider_value: float, metric_dropdown_value: str, -) -> go.Figure: - """In this Callback the Value of the Dropdown is used to filter the Data. In Addition it takes the filter for the Graph metrics and creates a new graph, or switches between 3D and 2D. +) -> tuple[dict, list, go.Figure]: + """In this Callback the Value of the Dropdown is used to filter the Data. + + In Addition, it takes the filter for the Graph metrics and creates a new graph, or switches between 3D and 2D. Args: - selected_metric (_type_): Selected Value - switch_value (bool): True if 2D, False if 3D - switch_edge_annotaion_value: True if Edge should have a description, Flase = No Descritpion - c_relation_filter_value (_type_): Variable with String value of Relation Type for Companies - p_relation_filter_value (_type_): Variable with String value of Relation Type for Persons + selected_metric: Selected Value + switch_value: True if 2D, False if 3D + switch_edge_annotation_value: True if Edge should have a description, False = No Description + c_relation_filter_value: Variable with String value of Relation Type for Companies + p_relation_filter_value: Variable with String value of Relation Type for Persons layout: String of the Layout Dropdown metric_dropdown_value: String of the Metric Dropdown slider_value: Sets the size of the Edge Connections Returns: - Network Graph(Plotly Figure): Plotly Figure in 3 or 2D + Network Graph: Plotly Figure in 3D or 2D """ - _ = c_relation_filter_value, p_relation_filter_value - - graph, metrics, nodes, edges = update_graph_data( - person_relation_type=p_relation_filter_value, - company_relation_type=c_relation_filter_value, - ) - - table_dict, table_columns = update_table(metric_dropdown_value, metrics) - - if switch_value: - return ( - table_dict, - table_columns, - create_2d_graph( - graph, - nodes, - edges, - metrics, - selected_metric, - layout, - switch_edge_annotaion_value, - slider_value, # type: ignore - ), - ) - - return ( - table_dict, - table_columns, - create_3d_graph( - graph, - nodes, - edges, - metrics, - selected_metric, - layout, - switch_edge_annotaion_value, - slider_value, # type: ignore - ), + return _update_figure( + selected_metric, + switch_value, + switch_edge_annotation_value, + frozenset(c_relation_filter_value), + frozenset(p_relation_filter_value), + layout, + slider_value, + metric_dropdown_value, ) diff --git a/src/aki_prj23_transparenzregister/utils/networkx/network_3d.py b/src/aki_prj23_transparenzregister/utils/networkx/network_3d.py index b2262be..ce984a6 100644 --- a/src/aki_prj23_transparenzregister/utils/networkx/network_3d.py +++ b/src/aki_prj23_transparenzregister/utils/networkx/network_3d.py @@ -18,11 +18,11 @@ def create_3d_graph( # noqa : PLR0913 """This Method creates a 3D Network in Plotly with a Scatter Graph and retuns it. Args: - graph (_type_): NetworkX Graph. - nodes (_type_): List of Nodes - edges (_type_): List of Edges - metrics (_type_): DataFrame with the MEtrics - metric (_type_): Selected Metric + graph: NetworkX Graph. + nodes: List of Nodes + edges: List of Edges + metrics: DataFrame with the Metrics + metric: Selected Metric Returns: _type_: Plotly Figure @@ -180,7 +180,7 @@ def create_3d_graph( # noqa : PLR0913 node_trace.marker.color = colors node_trace.text = node_names - # Highlight the Node Size in regards to the selected Metric. + # Highlight the Node Size with regard to the selected Metric. if metric != "None": node_trace.marker.size = list(np.cbrt(metrics[metric]) * 200) diff --git a/src/aki_prj23_transparenzregister/utils/networkx/network_base.py b/src/aki_prj23_transparenzregister/utils/networkx/network_base.py index bf7b29a..c3a8e3e 100644 --- a/src/aki_prj23_transparenzregister/utils/networkx/network_base.py +++ b/src/aki_prj23_transparenzregister/utils/networkx/network_base.py @@ -23,7 +23,6 @@ def initialize_network(edges: list, nodes: dict) -> tuple[nx.Graph, pd.DataFrame # update node attributes from dataframe nx.set_node_attributes(graph, nodes) # Create a DataFrame with all Metrics - # Create a DataFrame with all Metrics metrics = pd.DataFrame( columns=[ "eigenvector", @@ -36,7 +35,7 @@ def initialize_network(edges: list, nodes: dict) -> tuple[nx.Graph, pd.DataFrame "id", ] ) - metrics["eigenvector"] = nx.eigenvector_centrality(graph).values() + # metrics["eigenvector"] = nx.eigenvector_centrality(graph, 200).values() metrics["degree"] = nx.degree_centrality(graph).values() metrics["betweenness"] = nx.betweenness_centrality(graph).values() metrics["closeness"] = nx.closeness_centrality(graph).values() @@ -54,8 +53,8 @@ def initialize_network_with_reduced_metrics( """This Method creates a Network from the Framework NetworkX with the help of a Node and Edge List. Furthemore it creates a DataFrame with the most important Metrics. Args: - edges (list): List with the connections between Nodes. - nodes (dict): Dict with all Nodes. + edges: List with the connections between Nodes. + nodes: Dict with all Nodes. Returns: Graph: 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 2349eb2..dba0356 100644 --- a/src/aki_prj23_transparenzregister/utils/networkx/networkx_data.py +++ b/src/aki_prj23_transparenzregister/utils/networkx/networkx_data.py @@ -3,6 +3,7 @@ from functools import lru_cache import networkx as nx import pandas as pd +from loguru import logger from sqlalchemy.orm import aliased from aki_prj23_transparenzregister.ui.session_handler import SessionHandler @@ -103,7 +104,7 @@ def get_all_company_relations() -> pd.DataFrame: def get_all_person_relations() -> pd.DataFrame: - """This Methods makes a Database Request for all Persons and their relations, modifies the ID Column and returns the Result as an DataFrame. + """These method makes a Database Request for all Persons and their relations, modifies the ID Column and returns the Result as an DataFrame. Returns: DataFrame: DataFrame with all Relations between Persons and Companies. @@ -142,7 +143,7 @@ def get_all_person_relations() -> pd.DataFrame: def filter_relation_type( - relation_dataframe: pd.DataFrame, selected_relation_type: str + relation_dataframe: pd.DataFrame, selected_relation_type: frozenset[str] | None ) -> pd.DataFrame: """This Method filters the given DataFrame based on the selected Relation Type and returns it. @@ -153,9 +154,13 @@ def filter_relation_type( Returns: relation_dataframe (pd.DataFrame): The filtered DataFrame which now only contains entries with the selected Relation Type. """ - return relation_dataframe.loc[ - relation_dataframe["relation_type"] == selected_relation_type - ] + if selected_relation_type is not None: + filtered = relation_dataframe.loc[ + relation_dataframe["relation_type"].isin(selected_relation_type) + ] + logger.info(f"Filtered! {len(filtered.index) / len(relation_dataframe)}") + return filtered + return relation_dataframe def create_edge_and_node_list( diff --git a/tests/ui/home_page_test.py b/tests/ui/home_page_test.py index 0aacb21..655fceb 100644 --- a/tests/ui/home_page_test.py +++ b/tests/ui/home_page_test.py @@ -187,9 +187,8 @@ def _get_company_relations() -> Generator: yield -@pytest.mark.tim() def test_update_graph_data() -> None: graph_result, metrics_result, nodes_result, edges_result = home.update_graph_data( - "HAFTENDER_GESELLSCHAFTER", "GESCHAEFTSFUEHRER" + frozenset({"HAFTENDER_GESELLSCHAFTER"}), frozenset("GESCHAEFTSFUEHRER") ) assert graph_result is not None diff --git a/tests/utils/networkx/networkx_data_test.py b/tests/utils/networkx/networkx_data_test.py index 8c52c6c..e7cf889 100644 --- a/tests/utils/networkx/networkx_data_test.py +++ b/tests/utils/networkx/networkx_data_test.py @@ -185,7 +185,7 @@ def test_filter_relation_type() -> None: relation_dataframe = networkx_data.get_all_company_relations() selected_relation_type = "HAFTENDER_GESELLSCHAFTER" company_relations_df = networkx_data.filter_relation_type( - relation_dataframe, selected_relation_type + relation_dataframe, frozenset({selected_relation_type}) ) assert type(company_relations_df) is pd.DataFrame