Added multi relation dropdowns to dashbord (#363)

This change allows for a more complete combination of relation
combinations to be filtered.
This commit is contained in:
Philipp Horstenkamp 2023-11-11 13:47:46 +01:00 committed by GitHub
parent ad8f5d0fb1
commit e5b61bc19c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 106 deletions

View File

@ -154,7 +154,7 @@ jobs:
run: exit 0 run: exit 0
- name: Exit workflow on not main branch - name: Exit workflow on not main branch
if: ${{ github.ref != 'refs/heads/main'}} if: ${{ github.ref != 'refs/heads/main' }}
run: exit 0 run: exit 0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry

View File

@ -10,7 +10,7 @@
height: 100%; height: 100%;
} }
.top_companytable_style { .top_company_table_style {
float: left; float: left;
margin-top: 20px; margin-top: 20px;
margin-left: 20px; margin-left: 20px;

View File

@ -4,7 +4,6 @@ from functools import lru_cache
import dash import dash
import dash_daq as daq import dash_daq as daq
import networkx as nx import networkx as nx
import numpy as np
import pandas as pd import pandas as pd
import plotly.graph_objects as go import plotly.graph_objects as go
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
@ -40,50 +39,113 @@ dash.register_page(
# ], # ],
) )
metric = "None"
switch_edge_annotaion_value = False def person_relation_type_filter() -> list[str]:
egde_thickness = 1 """Returns a Numpy Array of String with Person relation types."""
network = None return get_all_person_relations()["relation_type"].unique().tolist()
def person_relation_type_filter() -> np.ndarray: def company_relation_type_filter() -> list[str]:
"""Returns an Numpy Array of String with Person telation types.""" """Returns a Numpy Array of String with Company relation types."""
return get_all_person_relations()["relation_type"].unique() return get_all_company_relations()["relation_type"].unique().tolist()
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 update_table( def update_table(
metric_dropdown_value: str, metrics: pd.DataFrame metric_dropdown_value: str, metrics: pd.DataFrame
) -> tuple[dict, list]: ) -> tuple[dict, list]:
"""_summary_. """_summary_."""
Args:
metric_dropdown_value (str): _description_
metrics (pd.DataFrame): _description_
Returns:
tuple[dict, list]: _description_
"""
table_df = metrics.sort_values(metric_dropdown_value, ascending=False).head(10) table_df = metrics.sort_values(metric_dropdown_value, ascending=False).head(10)
table_df = table_df[["designation", "category", metric_dropdown_value]] table_df = table_df[["designation", "category", metric_dropdown_value]]
columns = [{"name": i, "id": i} for i in table_df.columns] columns = [{"name": i, "id": i} for i in table_df.columns]
return table_df.to_dict("records"), columns # type: ignore 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.""" """Generates the Layout of the Homepage."""
person_relation_types = person_relation_type_filter() person_relation_types = person_relation_type_filter()
company_relation_types = company_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", "None",
False, False,
False, False,
company_relation_types[0], selected_company_relation_types,
person_relation_types[0], selected_person_relation_types,
"Spring", "Spring",
1, 1,
"degree", "degree",
@ -92,7 +154,7 @@ def layout() -> list[html.Div]:
children=html.Div( children=html.Div(
children=[ children=[
html.Div( html.Div(
className="top_companytable_style", className="top_company_table_style",
children=[ children=[
html.H1( html.H1(
title="Top Ten Nodes in Graph by Metric", title="Top Ten Nodes in Graph by Metric",
@ -143,9 +205,11 @@ def layout() -> list[html.Div]:
), ),
dcc.Dropdown( dcc.Dropdown(
company_relation_types, company_relation_types,
company_relation_types[0], list(selected_company_relation_types),
id="dropdown_company_relation_filter", id="dropdown_company_relation_filter",
className="dropdown_style", className="dropdown_style",
multi=True,
persistence=True,
), ),
], ],
), ),
@ -159,9 +223,11 @@ def layout() -> list[html.Div]:
), ),
dcc.Dropdown( dcc.Dropdown(
person_relation_types, person_relation_types,
person_relation_types[0], list(selected_person_relation_types),
id="dropdown_person_relation_filter", id="dropdown_person_relation_filter",
className="dropdown_style", className="dropdown_style",
multi=True,
persistence=True,
), ),
], ],
), ),
@ -282,7 +348,10 @@ def layout() -> list[html.Div]:
], ],
), ),
dcc.Graph( 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) @lru_cache(200)
def update_graph_data( def update_graph_data(
person_relation_type: str = "HAFTENDER_GESELLSCHAFTER", person_relation_type: frozenset[str] | None = None,
company_relation_type: str = "GESCHAEFTSFUEHRER", company_relation_type: frozenset[str] | None = None,
) -> tuple[nx.Graph, pd.DataFrame, dict, list]: ) -> tuple[nx.Graph, pd.DataFrame, dict, list]:
"""_summary_. """_summary_.
@ -305,7 +374,6 @@ def update_graph_data(
Returns: Returns:
tuple[nx.Graph, pd.DataFrame, dict, list]: _description_ tuple[nx.Graph, pd.DataFrame, dict, list]: _description_
""" """
# Get Data
person_df = get_all_person_relations() person_df = get_all_person_relations()
company_df = get_all_company_relations() company_df = get_all_company_relations()
@ -339,71 +407,41 @@ def update_graph_data(
prevent_initial_call=True, prevent_initial_call=True,
allow_duplicate=True, allow_duplicate=True,
) )
# @lru_cache(20)
@cached(cache=TTLCache(maxsize=100, ttl=500))
def update_figure( # noqa: PLR0913 def update_figure( # noqa: PLR0913
selected_metric: str, selected_metric: str,
switch_value: bool, switch_value: bool,
# switch_node_annotaion_value: bool, switch_edge_annotation_value: bool,
switch_edge_annotaion_value: bool, c_relation_filter_value: list[str],
c_relation_filter_value: str, p_relation_filter_value: list[str],
p_relation_filter_value: str,
layout: str, layout: str,
slider_value: float, slider_value: float,
metric_dropdown_value: str, metric_dropdown_value: str,
) -> go.Figure: ) -> 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. """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: Args:
selected_metric (_type_): Selected Value selected_metric: Selected Value
switch_value (bool): True if 2D, False if 3D switch_value: True if 2D, False if 3D
switch_edge_annotaion_value: True if Edge should have a description, Flase = No Descritpion switch_edge_annotation_value: True if Edge should have a description, False = No Description
c_relation_filter_value (_type_): Variable with String value of Relation Type for Companies c_relation_filter_value: Variable with String value of Relation Type for Companies
p_relation_filter_value (_type_): Variable with String value of Relation Type for Persons p_relation_filter_value: Variable with String value of Relation Type for Persons
layout: String of the Layout Dropdown layout: String of the Layout Dropdown
metric_dropdown_value: String of the Metric Dropdown metric_dropdown_value: String of the Metric Dropdown
slider_value: Sets the size of the Edge Connections slider_value: Sets the size of the Edge Connections
Returns: 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 return _update_figure(
selected_metric,
graph, metrics, nodes, edges = update_graph_data( switch_value,
person_relation_type=p_relation_filter_value, switch_edge_annotation_value,
company_relation_type=c_relation_filter_value, frozenset(c_relation_filter_value),
) frozenset(p_relation_filter_value),
layout,
table_dict, table_columns = update_table(metric_dropdown_value, metrics) slider_value,
metric_dropdown_value,
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
),
) )

View File

@ -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. """This Method creates a 3D Network in Plotly with a Scatter Graph and retuns it.
Args: Args:
graph (_type_): NetworkX Graph. graph: NetworkX Graph.
nodes (_type_): List of Nodes nodes: List of Nodes
edges (_type_): List of Edges edges: List of Edges
metrics (_type_): DataFrame with the MEtrics metrics: DataFrame with the Metrics
metric (_type_): Selected Metric metric: Selected Metric
Returns: Returns:
_type_: Plotly Figure _type_: Plotly Figure
@ -180,7 +180,7 @@ def create_3d_graph( # noqa : PLR0913
node_trace.marker.color = colors node_trace.marker.color = colors
node_trace.text = node_names 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": if metric != "None":
node_trace.marker.size = list(np.cbrt(metrics[metric]) * 200) node_trace.marker.size = list(np.cbrt(metrics[metric]) * 200)

View File

@ -23,7 +23,6 @@ def initialize_network(edges: list, nodes: dict) -> tuple[nx.Graph, pd.DataFrame
# update node attributes from dataframe # update node attributes from dataframe
nx.set_node_attributes(graph, nodes) nx.set_node_attributes(graph, nodes)
# Create a DataFrame with all Metrics # Create a DataFrame with all Metrics
# Create a DataFrame with all Metrics
metrics = pd.DataFrame( metrics = pd.DataFrame(
columns=[ columns=[
"eigenvector", "eigenvector",
@ -36,7 +35,7 @@ def initialize_network(edges: list, nodes: dict) -> tuple[nx.Graph, pd.DataFrame
"id", "id",
] ]
) )
metrics["eigenvector"] = nx.eigenvector_centrality(graph).values() # metrics["eigenvector"] = nx.eigenvector_centrality(graph, 200).values()
metrics["degree"] = nx.degree_centrality(graph).values() metrics["degree"] = nx.degree_centrality(graph).values()
metrics["betweenness"] = nx.betweenness_centrality(graph).values() metrics["betweenness"] = nx.betweenness_centrality(graph).values()
metrics["closeness"] = nx.closeness_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. """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: Args:
edges (list): List with the connections between Nodes. edges: List with the connections between Nodes.
nodes (dict): Dict with all Nodes. nodes: Dict with all Nodes.
Returns: Returns:
Graph: Plotly Figure Graph: Plotly Figure

View File

@ -3,6 +3,7 @@ from functools import lru_cache
import networkx as nx import networkx as nx
import pandas as pd import pandas as pd
from loguru import logger
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from aki_prj23_transparenzregister.ui.session_handler import SessionHandler 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: 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: Returns:
DataFrame: DataFrame with all Relations between Persons and Companies. DataFrame: DataFrame with all Relations between Persons and Companies.
@ -142,7 +143,7 @@ def get_all_person_relations() -> pd.DataFrame:
def filter_relation_type( def filter_relation_type(
relation_dataframe: pd.DataFrame, selected_relation_type: str relation_dataframe: pd.DataFrame, selected_relation_type: frozenset[str] | None
) -> pd.DataFrame: ) -> pd.DataFrame:
"""This Method filters the given DataFrame based on the selected Relation Type and returns it. """This Method filters the given DataFrame based on the selected Relation Type and returns it.
@ -153,9 +154,13 @@ def filter_relation_type(
Returns: Returns:
relation_dataframe (pd.DataFrame): The filtered DataFrame which now only contains entries with the selected Relation Type. relation_dataframe (pd.DataFrame): The filtered DataFrame which now only contains entries with the selected Relation Type.
""" """
return relation_dataframe.loc[ if selected_relation_type is not None:
relation_dataframe["relation_type"] == selected_relation_type 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( def create_edge_and_node_list(

View File

@ -187,9 +187,8 @@ def _get_company_relations() -> Generator:
yield yield
@pytest.mark.tim()
def test_update_graph_data() -> None: def test_update_graph_data() -> None:
graph_result, metrics_result, nodes_result, edges_result = home.update_graph_data( 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 assert graph_result is not None

View File

@ -185,7 +185,7 @@ def test_filter_relation_type() -> None:
relation_dataframe = networkx_data.get_all_company_relations() relation_dataframe = networkx_data.get_all_company_relations()
selected_relation_type = "HAFTENDER_GESELLSCHAFTER" selected_relation_type = "HAFTENDER_GESELLSCHAFTER"
company_relations_df = networkx_data.filter_relation_type( 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 assert type(company_relations_df) is pd.DataFrame