import time import webbrowser from pathlib import Path from threading import Thread from typing import cast import dash_cytoscape as cyto import pandas as pd import plotly.express as px from dash import ( Dash, Input, Output, State, callback, dash_table, dcc, html, ) from pandas import DataFrame from lang_main.analysis import graphs from lang_main.io import load_pickle from lang_main.types import ObjectID, TimelineCandidates from lang_main.analysis import tokens from lang_main.constants import SPCY_MODEL # df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder_unfiltered.csv') # ** data # p_df = Path(r'../Pipe-TargetFeature_Step-3_remove_NA.pkl').resolve() p_df = Path(r'../results/test_20240619/TIMELINE.pkl').resolve() # p_tl = Path(r'/Pipe-Timeline_Analysis_Step-4_get_timeline_candidates.pkl').resolve() p_tl = Path(r'../results/test_20240619/TIMELINE_POSTPROCESSING.pkl').resolve() ret = cast(tuple[DataFrame], load_pickle(p_df)) data = ret[0] ret = cast(tuple[TimelineCandidates, dict[ObjectID, str]], load_pickle(p_tl)) cands = ret[0] texts = ret[1] # p_df = Path(r'.\test-notebooks\dashboard\data.pkl') # p_cands = Path(r'.\test-notebooks\dashboard\map_candidates.pkl') # p_map = Path(r'.\test-notebooks\dashboard\map_texts.pkl') # data = cast(DataFrame, load_pickle(p_df)) # cands = cast(TimelineCandidates, load_pickle(p_cands)) # texts = cast(dict[ObjectID, str], load_pickle(p_map)) table_feats = [ 'ErstellungsDatum', 'ErledigungsDatum', 'VorgangsTypName', 'VorgangsBeschreibung', ] table_feats_dates = [ 'ErstellungsDatum', 'ErledigungsDatum', ] # ** figure config markers = { 'size': 12, 'color': 'yellow', 'line': { 'width': 2, 'color': 'red', }, } hover_data = { 'ErstellungsDatum': '|%d.%m.%Y', 'VorgangsBeschreibung': True, } # ** graphs target = '../results/test_20240529/Pipe-Token_Analysis_Step-1_build_token_graph.pkl' p = Path(target).resolve() ret = load_pickle(p) tk_graph = cast(graphs.TokenGraph, ret[0]) tk_graph_filtered = graphs.filter_graph_by_edge_weight(tk_graph, 150, None) tk_graph_filtered = graphs.filter_graph_by_node_degree(tk_graph_filtered, 1, None) # tk_graph_filtered = tk_graph.filter_by_edge_weight(150, None) # tk_graph_filtered = tk_graph_filtered.filter_by_node_degree(1, None) cyto_data_base, weight_data = graphs.convert_graph_to_cytoscape(tk_graph_filtered) MIN_WEIGHT = weight_data['min'] MAX_WEIGHT = weight_data['max'] cyto.load_extra_layouts() cose_layout = { 'name': 'cose', 'nodeOverlap': 500, 'refresh': 20, 'fit': True, 'padding': 20, 'randomize': False, 'componentSpacing': 1.2, 'nodeRepulsion': 1000, 'edgeElasticity': 1000, 'idealEdgeLength': 100, 'nestingFactor': 1.2, 'gravity': 50, 'numIter': 3000, 'initialTemp': 2000, 'coolingFactor': 0.7, 'minTemp': 1.0, 'nodeDimensionsIncludeLabels': True, } my_stylesheet = [ # Group selectors { 'selector': 'node', 'style': { 'shape': 'circle', 'content': 'data(label)', 'background-color': '#B10DC9', 'border-width': 2, 'border-color': 'black', 'border-opacity': 1, 'opacity': 1, 'color': 'black', 'text-opacity': 1, 'font-size': 12, 'z-index': 9999, }, }, { 'selector': 'edge', 'style': { #'width': f'mapData(weight, {MIN_WEIGHT}, {MAX_WEIGHT}, 1, 10)', # 'width': """function(ele) { # return ele.data('weight'); # """, 'curve-style': 'bezier', 'line-color': 'grey', 'line-style': 'solid', 'line-opacity': 1, }, }, # Class selectors # {'selector': '.red', 'style': {'background-color': 'red', 'line-color': 'red'}}, # {'selector': '.triangle', 'style': {'shape': 'triangle'}}, ] # ** app external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = Dash(__name__, external_stylesheets=external_stylesheets) graph_layout = html.Div( [ html.Button('Trigger JS Weight', id='test_js_weight'), html.Button('Trigger Candidate Graph', id='cand_graph'), html.Div(id='output'), html.Div( [ html.H2('Token Graph', style={'margin': 0}), html.Button( 'Reset Default', id='bt-reset', style={ 'marginLeft': 'auto', 'width': '300px', }, ), ], style={ 'display': 'flex', 'marginBottom': '1em', }, ), html.H3('Layout'), dcc.Dropdown( id='layout_choice', options=[ 'cose', 'cola', 'euler', 'random', ], value='cose', clearable=False, ), html.Div( [ html.H3('Graph Filter'), dcc.Input( id='weight_min', type='number', min=MIN_WEIGHT, max=MAX_WEIGHT, step=1, placeholder=f'Minimum edge weight: {MIN_WEIGHT} - {MAX_WEIGHT}', debounce=True, style={'width': '40%'}, ), dcc.Input( id='weight_max', type='number', min=MIN_WEIGHT, max=MAX_WEIGHT, step=1, placeholder=f'Maximum edge weight: {MIN_WEIGHT} - {MAX_WEIGHT}', debounce=True, style={'width': '40%'}, ), html.H3('Graph'), html.Button('Re-Layout', id='trigger_relayout'), html.Div( [ cyto.Cytoscape( id='cytoscape-graph', style={'width': '100%', 'height': '600px'}, layout=cose_layout, stylesheet=my_stylesheet, elements=cyto_data_base, zoom=1, ), ], style={ 'border': '3px solid black', 'borderRadius': '25px', 'marginTop': '1em', 'marginBottom': '2em', 'padding': '7px', }, ), ], style={'marginTop': '1em'}, ), ], ) app.layout = html.Div( [ html.H1(children='Demo Zeitreihenanalyse', style={'textAlign': 'center'}), html.Div( children=[ html.H2('Wählen Sie ein Objekt aus (ObjektID):'), dcc.Dropdown( list(cands.keys()), id='dropdown-selection', placeholder='ObjektID auswählen...', ), ] ), html.Div( children=[ html.H3(id='object_text'), dcc.Dropdown(id='choice-candidates'), dcc.Graph(id='graph-output'), ] ), html.Div( [dash_table.DataTable(id='table-candidates')], style={'marginBottom': '2em'} ), graph_layout, ], style={'margin': '2em'}, ) @callback( Output('object_text', 'children'), Input('dropdown-selection', 'value'), prevent_initial_call=True, ) def update_obj_text(obj_id): obj_id = int(obj_id) obj_text = texts[obj_id] headline = f'HObjektText: {obj_text}' return headline @callback( Output('choice-candidates', 'options'), Input('dropdown-selection', 'value'), prevent_initial_call=True, ) def update_choice_candidates(obj_id): obj_id = int(obj_id) cands_obj_id = cands[obj_id] choices = list(range(1, len(cands_obj_id) + 1)) return choices @callback( Output('graph-output', 'figure'), Input('choice-candidates', 'value'), State('dropdown-selection', 'value'), prevent_initial_call=True, ) def update_timeline(index, obj_id): obj_id = int(obj_id) # title obj_text = texts[obj_id] title = f'HObjektText: {obj_text}' # cands cands_obj_id = cands[obj_id] cands_choice = cands_obj_id[int(index) - 1] # data df = data.loc[list(cands_choice)].sort_index() # type: ignore # figure fig = px.line( data_frame=df, x='ErstellungsDatum', y='ObjektID', title=title, hover_data=hover_data, ) fig.update_traces(mode='markers+lines', marker=markers, marker_symbol='diamond') fig.update_xaxes( tickformat='%B\n%Y', rangeslider_visible=True, ) fig.update_yaxes(type='category') fig.update_layout(hovermode='x unified') return fig @callback( [Output('table-candidates', 'data'), Output('table-candidates', 'columns')], Input('choice-candidates', 'value'), State('dropdown-selection', 'value'), prevent_initial_call=True, ) def update_table_candidates(index, obj_id): # obj_id = int(obj_id) # # cands # cands_obj_id = cands[obj_id] # cands_choice = cands_obj_id[int(index) - 1] # # data # df = data.loc[list(cands_choice)].sort_index() # type: ignore df = pre_filter_data(data, idx=index, obj_id=obj_id) df = df.filter(items=table_feats, axis=1).sort_values( by='ErstellungsDatum', ascending=True ) cols = [{'name': i, 'id': i} for i in df.columns] # convert dates to strings for col in table_feats_dates: df[col] = df[col].dt.strftime(r'%Y-%m-%d') table_data = df.to_dict('records') return table_data, cols def pre_filter_data( data: DataFrame, idx: int, obj_id: ObjectID, ) -> DataFrame: obj_id = int(obj_id) data = data.copy() # cands cands_obj_id = cands[obj_id] cands_choice = cands_obj_id[int(idx) - 1] # data data = data.loc[list(cands_choice)].sort_index() # type: ignore return data # ** graph @app.callback( Output('cytoscape-graph', 'elements', allow_duplicate=True), Output('weight_min', 'min', allow_duplicate=True), Output('weight_min', 'max', allow_duplicate=True), Output('weight_min', 'placeholder', allow_duplicate=True), Output('weight_max', 'min', allow_duplicate=True), Output('weight_max', 'max', allow_duplicate=True), Output('weight_max', 'placeholder', allow_duplicate=True), Input('cand_graph', 'n_clicks'), State('choice-candidates', 'value'), State('dropdown-selection', 'value'), prevent_initial_call=True, ) def update_graph_candidates(_, index, obj_id): df = pre_filter_data(data, idx=index, obj_id=obj_id) tk_graph_cands, _ = tokens.build_token_graph( data=df, model=SPCY_MODEL, target_feature='VorgangsBeschreibung', build_map=False, ) cyto_data, weight_info = graphs.convert_graph_to_cytoscape(tk_graph_cands) weight_min = weight_info['min'] weight_max = weight_info['max'] placeholder_min = f'Minimum edge weight: {weight_min} - {weight_max}' placeholder_max = f'Minimum edge weight: {weight_min} - {weight_max}' return ( cyto_data, weight_min, weight_max, placeholder_min, weight_min, weight_max, placeholder_max, ) @app.callback( Output('cytoscape-graph', 'layout', allow_duplicate=True), Input('layout_choice', 'value'), prevent_initial_call=True, ) def update_layout_internal(layout_choice): # return {'name': layout_choice} return cose_layout # return cose_bilkent_layout # return cola_layout @app.callback( Output('cytoscape-graph', 'zoom'), Output('cytoscape-graph', 'elements', allow_duplicate=True), Output('weight_min', 'value'), Output('weight_max', 'value'), Input('bt-reset', 'n_clicks'), prevent_initial_call=True, ) def reset_layout(n_clicks): return (1, cyto_data_base, None, None) # update edge weight @app.callback( Output('cytoscape-graph', 'elements', allow_duplicate=True), Input('weight_min', 'value'), Input('weight_max', 'value'), prevent_initial_call=True, ) def update_edge_weight(weight_min, weight_max): if not any([weight_min, weight_max]): return cyto_data_base if weight_min is None: weight_min = MIN_WEIGHT if weight_max is None: weight_max = MAX_WEIGHT tk_graph_filtered = graphs.filter_graph_by_edge_weight(tk_graph, weight_min, weight_max) # tk_graph_filtered = tk_graph.filter_by_edge_weight(weight_min, weight_max) tk_graph_filtered = graphs.filter_graph_by_node_degree(tk_graph_filtered, 1, None) # tk_graph_filtered = tk_graph_filtered.filter_by_node_degree(1, None) cyto_data, _ = graphs.convert_graph_to_cytoscape(tk_graph_filtered) return cyto_data app.clientside_callback( """ function(n_clicks, layout) { layout.edgeElasticity = function(edge) { return edge.data().weight * 0.05; }; layout.idealEdgeLength = function(edge) { return edge.data().weight * 0.4; }; cy.layout(layout).run(); return layout; } """, Output('cytoscape-graph', 'layout', allow_duplicate=True), Input('trigger_relayout', 'n_clicks'), State('cytoscape-graph', 'layout'), prevent_initial_call=True, ) app.clientside_callback( """ function(n_clicks, stylesheet) { function edge_weight(ele) { let threshold = 1000; let weight = ele.data('weight'); if (weight > threshold) { weight = 12; } else { weight = weight / threshold * 10; weight = Math.max(1, weight); } return weight; } stylesheet[1].style.width = edge_weight; cy.style(stylesheet).update(); return stylesheet; } """, Output('cytoscape-graph', 'stylesheet'), Input('test_js_weight', 'n_clicks'), State('cytoscape-graph', 'stylesheet'), prevent_initial_call=False, ) def _start_webbrowser(): host = '127.0.0.1' port = '8050' adress = f'http://{host}:{port}/' time.sleep(2) webbrowser.open_new(adress) def main(): webbrowser_thread = Thread(target=_start_webbrowser, daemon=True) webbrowser_thread.start() app.run(debug=True) if __name__ == '__main__': main()