508 lines
14 KiB
Python
508 lines
14 KiB
Python
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()
|