|
import pandas as pd
|
|
import plotly.express as px
|
|
import plotly.graph_objects as go
|
|
from dash import Dash, dcc, html, Input, Output, State
|
|
import numpy as np
|
|
import random
|
|
import math
|
|
from collections import defaultdict
|
|
import colorsys
|
|
from fastapi import HTTPException
|
|
from pydantic import BaseModel
|
|
import threading
|
|
import webbrowser
|
|
import os
|
|
import psutil
|
|
import socket
|
|
from fastapi import HTTPException, APIRouter, Request
|
|
router = APIRouter()
|
|
|
|
|
|
dashboard_port = 8050
|
|
dashboard_process = None
|
|
|
|
|
|
async def load_data_from_mongodb(userId, topic, year, request:Request):
|
|
query = {
|
|
"userId": userId,
|
|
"topic": topic,
|
|
"year": year
|
|
}
|
|
collection = request.app.state.collection2
|
|
document = await collection.find_one(query)
|
|
if not document:
|
|
raise ValueError(f"No data found for userId={userId}, topic={topic}, year={year}")
|
|
|
|
metadata = document.get("metadata", [])
|
|
df = pd.DataFrame(metadata)
|
|
df['publication_date'] = pd.to_datetime(df['publication_date'])
|
|
return df
|
|
|
|
|
|
def filter_by_date_range(dataframe, start_idx, end_idx):
|
|
start_date = date_range[start_idx]
|
|
end_date = date_range[end_idx]
|
|
return dataframe[(dataframe['publication_date'] >= start_date) &
|
|
(dataframe['publication_date'] <= end_date)]
|
|
|
|
def generate_vibrant_colors(n):
|
|
base_colors = []
|
|
for i in range(n):
|
|
hue = (i / n) % 1.0
|
|
saturation = random.uniform(0.7, 0.9)
|
|
value = random.uniform(0.7, 0.9)
|
|
r, g, b = colorsys.hsv_to_rgb(hue, saturation, value)
|
|
vibrant_color = '#{:02x}{:02x}{:02x}'.format(
|
|
int(r * 255),
|
|
int(g * 255),
|
|
int(b * 255)
|
|
)
|
|
end_color_r = min(255, int(r * 255 * 1.1))
|
|
end_color_g = min(255, int(g * 255 * 1.1))
|
|
end_color_b = min(255, int(b * 255 * 1.1))
|
|
gradient_end = '#{:02x}{:02x}{:02x}'.format(end_color_r, end_color_g, end_color_b)
|
|
base_colors.append({
|
|
'start': vibrant_color,
|
|
'end': gradient_end
|
|
})
|
|
extended_colors = base_colors * math.ceil(n/10)
|
|
final_colors = []
|
|
for i in range(n):
|
|
color = extended_colors[i]
|
|
jitter = random.uniform(0.9, 1.1)
|
|
def jitter_color(hex_color):
|
|
r, g, b = [min(255, max(0, int(int(hex_color[j:j+2], 16) * jitter))) for j in (1, 3, 5)]
|
|
return f'rgba({r}, {g}, {b}, 0.9)'
|
|
final_colors.append({
|
|
'start': jitter_color(color['start']),
|
|
'end': jitter_color(color['end']).replace('0.9', '0.8')
|
|
})
|
|
return final_colors
|
|
|
|
|
|
def create_knowledge_map(filtered_df, view_type='host'):
|
|
color_palette = {
|
|
'background': '#1E1E1E',
|
|
'card_bg': '#1A2238',
|
|
'accent1': '#FF6A3D',
|
|
'accent2': '#4ECCA3',
|
|
'accent3': '#9D84B7',
|
|
'text_light': '#FFFFFF',
|
|
'text_dark': '#E0E0E0',
|
|
}
|
|
|
|
if view_type == 'host':
|
|
group_col = 'host_organization_name'
|
|
id_col = 'host_organization_id'
|
|
title = "Host Organization Clusters"
|
|
else:
|
|
group_col = 'venue'
|
|
id_col = 'venue_id'
|
|
title = "Publication Venue Clusters"
|
|
summary = filtered_df.groupby(group_col).agg(
|
|
paper_count=('id', 'count'),
|
|
is_oa=('is_oa', 'mean'),
|
|
oa_status=('oa_status', lambda x: x.mode()[0] if not x.mode().empty else None),
|
|
entity_id=(id_col, 'first')
|
|
).reset_index()
|
|
paper_count_groups = defaultdict(list)
|
|
for _, row in summary.iterrows():
|
|
paper_count_groups[row['paper_count']].append(row)
|
|
knowledge_map_fig = go.Figure()
|
|
sorted_counts = sorted(paper_count_groups.keys(), reverse=True)
|
|
vibrant_colors = generate_vibrant_colors(len(sorted_counts))
|
|
golden_angle = np.pi * (3 - np.sqrt(5))
|
|
spiral_coef = 150
|
|
cluster_metadata = {}
|
|
max_x, max_y = 500, 500
|
|
for i, count in enumerate(sorted_counts):
|
|
radius = np.sqrt(i) * spiral_coef
|
|
theta = golden_angle * i
|
|
cluster_x, cluster_y = radius * np.cos(theta), radius * np.sin(theta)
|
|
label_offset_angle = theta + np.pi/4
|
|
label_offset_distance = 80 + 4 * np.sqrt(len(paper_count_groups[count]))
|
|
label_x = cluster_x + label_offset_distance * np.cos(label_offset_angle)
|
|
label_y = cluster_y + label_offset_distance * np.sin(label_offset_angle)
|
|
cluster_metadata[count] = {
|
|
'center_x': cluster_x,
|
|
'center_y': cluster_y,
|
|
'entities': paper_count_groups[count],
|
|
'color': vibrant_colors[i]
|
|
}
|
|
entities = paper_count_groups[count]
|
|
num_entities = len(entities)
|
|
cluster_size = min(200, max(80, 40 + 8 * np.sqrt(num_entities)))
|
|
color = vibrant_colors[i]
|
|
knowledge_map_fig.add_shape(
|
|
type="circle",
|
|
x0=cluster_x - cluster_size/2, y0=cluster_y - cluster_size/2,
|
|
x1=cluster_x + cluster_size/2, y1=cluster_y + cluster_size/2,
|
|
fillcolor=color['end'].replace("0.8", "0.15"),
|
|
line=dict(color=color['start'], width=1.5),
|
|
opacity=0.7
|
|
)
|
|
knowledge_map_fig.add_trace(go.Scatter(
|
|
x=[cluster_x], y=[cluster_y],
|
|
mode='markers',
|
|
marker=dict(size=cluster_size, color=color['start'], opacity=0.3),
|
|
customdata=[[count, "cluster"]],
|
|
hoverinfo='skip'
|
|
))
|
|
knowledge_map_fig.add_trace(go.Scatter(
|
|
x=[cluster_x, label_x], y=[cluster_y, label_y],
|
|
mode='lines',
|
|
line=dict(color=color['start'], width=1, dash='dot'),
|
|
hoverinfo='skip'
|
|
))
|
|
knowledge_map_fig.add_annotation(
|
|
x=label_x, y=label_y,
|
|
text=f"{count} papers<br>{num_entities} {'orgs' if view_type == 'host' else 'venues'}",
|
|
showarrow=False,
|
|
font=dict(size=11, color='white'),
|
|
bgcolor=color['start'],
|
|
bordercolor='white',
|
|
borderwidth=1,
|
|
opacity=0.9
|
|
)
|
|
entities_sorted = sorted(entities, key=lambda x: x[group_col])
|
|
inner_spiral_coef = 0.4
|
|
for j, entity_data in enumerate(entities_sorted):
|
|
spiral_radius = np.sqrt(j) * cluster_size * inner_spiral_coef / np.sqrt(num_entities + 1)
|
|
spiral_angle = golden_angle * j
|
|
jitter_radius = random.uniform(0.9, 1.1) * spiral_radius
|
|
jitter_angle = spiral_angle + random.uniform(-0.1, 0.1)
|
|
entity_x = cluster_x + jitter_radius * np.cos(jitter_angle)
|
|
entity_y = cluster_y + jitter_radius * np.sin(jitter_angle)
|
|
node_size = min(18, max(8, np.sqrt(entity_data['paper_count']) * 1.5))
|
|
knowledge_map_fig.add_trace(go.Scatter(
|
|
x=[entity_x], y=[entity_y],
|
|
mode='markers',
|
|
marker=dict(
|
|
size=node_size,
|
|
color=color['start'],
|
|
line=dict(color='rgba(255, 255, 255, 0.9)', width=1.5)
|
|
),
|
|
customdata=[[
|
|
entity_data[group_col],
|
|
entity_data['paper_count'],
|
|
entity_data['is_oa'],
|
|
entity_data['entity_id'],
|
|
count,
|
|
"entity"
|
|
]],
|
|
hovertemplate=(
|
|
f"<b>{entity_data[group_col]}</b><br>"
|
|
f"Papers: {entity_data['paper_count']}<br>"
|
|
f"Open Access: {entity_data['is_oa']:.1%}<extra></extra>"
|
|
)
|
|
))
|
|
max_x = max([abs(cluster['center_x']) for cluster in cluster_metadata.values()]) + 150 if cluster_metadata else 500
|
|
max_y = max([abs(cluster['center_y']) for cluster in cluster_metadata.values()]) + 150 if cluster_metadata else 500
|
|
|
|
knowledge_map_fig.update_layout(
|
|
title=dict(
|
|
text=title,
|
|
font=dict(size=22, family='"Poppins", sans-serif', color=color_palette['accent1'])
|
|
),
|
|
plot_bgcolor='rgba(26, 34, 56, 1)',
|
|
paper_bgcolor='rgba(26, 34, 56, 0.7)',
|
|
xaxis=dict(range=[-max(700, max_x), max(700, max_x)], showticklabels=False, showgrid=False),
|
|
yaxis=dict(range=[-max(500, max_y), max(500, max_y)], showticklabels=False, showgrid=False),
|
|
margin=dict(l=10, r=10, t=60, b=10),
|
|
height=700,
|
|
hovermode='closest',
|
|
showlegend=False,
|
|
font=dict(family='"Poppins", sans-serif', color=color_palette['text_light']),
|
|
)
|
|
return knowledge_map_fig, cluster_metadata
|
|
|
|
|
|
def create_oa_pie_fig(filtered_df):
|
|
color_palette = {
|
|
'background': '#1A2238',
|
|
'card_bg': '#1A2238',
|
|
'accent1': '#FF6A3D',
|
|
'accent2': '#4ECCA3',
|
|
'accent3': '#9D84B7',
|
|
'text_light': '#FFFFFF',
|
|
'text_dark': '#FFFFFF',
|
|
}
|
|
|
|
fig = px.pie(
|
|
filtered_df, names='is_oa', title="Overall Open Access Status",
|
|
labels={True: "Open Access", False: "Not Open Access"},
|
|
color_discrete_sequence=[color_palette['accent2'], color_palette['accent1']]
|
|
)
|
|
|
|
fig.update_traces(
|
|
textinfo='label+percent',
|
|
textfont=dict(size=14, family='"Poppins", sans-serif'),
|
|
marker=dict(line=dict(color='#1A2238', width=2))
|
|
)
|
|
|
|
fig.update_layout(
|
|
title=dict(
|
|
text="Overall Open Access Status",
|
|
font=dict(size=18, family='"Poppins", sans-serif', color=color_palette['accent1'])
|
|
),
|
|
font=dict(family='"Poppins", sans-serif', color=color_palette['text_light']),
|
|
paper_bgcolor=color_palette['background'],
|
|
plot_bgcolor=color_palette['background'],
|
|
margin=dict(t=50, b=20, l=20, r=20),
|
|
legend=dict(
|
|
orientation="h",
|
|
yanchor="bottom",
|
|
y=-0.2,
|
|
xanchor="center",
|
|
x=0.5,
|
|
font=dict(size=12, color=color_palette['text_light'])
|
|
)
|
|
)
|
|
|
|
return fig
|
|
def create_oa_status_pie_fig(filtered_df):
|
|
custom_colors = [
|
|
"#9D84B7",
|
|
'#4DADFF',
|
|
'#FFD166',
|
|
'#06D6A0',
|
|
'#EF476F'
|
|
]
|
|
fig = px.pie(
|
|
filtered_df,
|
|
names='oa_status',
|
|
title="Open Access Status Distribution",
|
|
color_discrete_sequence=custom_colors
|
|
)
|
|
fig.update_traces(
|
|
textinfo='label+percent',
|
|
insidetextorientation='radial',
|
|
textfont=dict(size=14, family='"Poppins", sans-serif'),
|
|
marker=dict(line=dict(color='#FFFFFF', width=2))
|
|
)
|
|
fig.update_layout(
|
|
title=dict(
|
|
text="Open Access Status Distribution",
|
|
font=dict(size=18, family='"Poppins", sans-serif', color="#FF6A3D")
|
|
),
|
|
font=dict(family='"Poppins", sans-serif', color='#FFFFFF'),
|
|
paper_bgcolor='#1A2238',
|
|
plot_bgcolor='#1A2238',
|
|
margin=dict(t=50, b=20, l=20, r=20),
|
|
legend=dict(
|
|
orientation="h",
|
|
yanchor="bottom",
|
|
y=-0.2,
|
|
xanchor="center",
|
|
x=0.5,
|
|
font=dict(size=12, color='#FFFFFF')
|
|
)
|
|
)
|
|
return fig
|
|
def create_type_bar_fig(filtered_df):
|
|
type_counts = filtered_df['type'].value_counts()
|
|
vibrant_colors = [
|
|
'#4361EE', '#3A0CA3', '#4CC9F0',
|
|
'#F72585', '#7209B7', '#B5179E',
|
|
'#480CA8', '#560BAD', '#F77F00'
|
|
]
|
|
fig = px.bar(
|
|
type_counts,
|
|
title="Publication Types",
|
|
labels={'value': 'Count', 'index': 'Type'},
|
|
color=type_counts.index,
|
|
color_discrete_sequence=vibrant_colors[:len(type_counts)]
|
|
)
|
|
fig.update_layout(
|
|
title=dict(
|
|
text="Publication Types",
|
|
font=dict(size=20, family='"Poppins", sans-serif', color="#FF6A3D")
|
|
),
|
|
xaxis_title="Type",
|
|
yaxis_title="Count",
|
|
font=dict(family='"Poppins", sans-serif', color="#FFFFFF", size=14),
|
|
paper_bgcolor='#1A2238',
|
|
plot_bgcolor='#1A2238',
|
|
margin=dict(t=70, b=60, l=60, r=40),
|
|
xaxis=dict(
|
|
tickfont=dict(size=14, color="#FFFFFF"),
|
|
tickangle=-45,
|
|
gridcolor='rgba(255, 255, 255, 0.1)'
|
|
),
|
|
yaxis=dict(
|
|
tickfont=dict(size=14, color="#FFFFFF"),
|
|
gridcolor='rgba(255, 255, 255, 0.1)'
|
|
),
|
|
bargap=0.3,
|
|
)
|
|
fig.update_traces(
|
|
marker_line_width=1,
|
|
marker_line_color='rgba(0, 0, 0, 0.5)',
|
|
opacity=0.9,
|
|
hovertemplate='%{y} publications<extra></extra>',
|
|
texttemplate='%{y}',
|
|
textposition='outside',
|
|
textfont=dict(size=14, color='white')
|
|
)
|
|
return fig
|
|
|
|
|
|
def is_port_in_use(port):
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
return s.connect_ex(('localhost', port)) == 0
|
|
|
|
|
|
def find_free_port(start_port=8050):
|
|
port = start_port
|
|
while is_port_in_use(port):
|
|
port += 1
|
|
return port
|
|
|
|
|
|
def shutdown_existing_dashboard():
|
|
global dashboard_process
|
|
|
|
|
|
if is_port_in_use(dashboard_port):
|
|
try:
|
|
|
|
for proc in psutil.process_iter(['pid', 'name', 'connections']):
|
|
try:
|
|
for conn in proc.connections():
|
|
if conn.laddr.port == dashboard_port:
|
|
print(f"Terminating process {proc.pid} using port {dashboard_port}")
|
|
proc.terminate()
|
|
proc.wait(timeout=3)
|
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
pass
|
|
except Exception as e:
|
|
print(f"Error freeing port {dashboard_port}: {e}")
|
|
|
|
|
|
if dashboard_process is not None:
|
|
try:
|
|
|
|
if dashboard_process.is_alive():
|
|
parent = psutil.Process(os.getpid())
|
|
children = parent.children(recursive=True)
|
|
for process in children:
|
|
try:
|
|
process.terminate()
|
|
except:
|
|
pass
|
|
dashboard_process = None
|
|
except Exception as e:
|
|
print(f"Error terminating dashboard process: {e}")
|
|
dashboard_process = None
|
|
|
|
|
|
class DashboardRequest(BaseModel):
|
|
userId: str
|
|
topic: str
|
|
year: int
|
|
|
|
@router.post("/load_and_display_dashboard/")
|
|
async def load_and_display_dashboard(request: DashboardRequest, req:Request):
|
|
global dashboard_process, dashboard_port
|
|
|
|
|
|
shutdown_existing_dashboard()
|
|
|
|
|
|
dashboard_port = find_free_port()
|
|
|
|
try:
|
|
|
|
df = await load_data_from_mongodb(request.userId, request.topic, request.year, req)
|
|
|
|
|
|
global min_date, max_date, date_range, date_marks
|
|
min_date = df['publication_date'].min()
|
|
max_date = df['publication_date'].max()
|
|
date_range = pd.date_range(start=min_date, end=max_date, freq='MS')
|
|
date_marks = {i: date.strftime('%b %Y') for i, date in enumerate(date_range)}
|
|
|
|
|
|
def create_and_run_dashboard():
|
|
|
|
app = Dash(__name__, suppress_callback_exceptions=True)
|
|
app.cluster_metadata = {}
|
|
color_palette = {
|
|
'background': '#1A2238',
|
|
'card_bg': '#F8F8FF',
|
|
'accent1': '#FF6A3D',
|
|
'accent2': '#4ECCA3',
|
|
'accent3': '#9D84B7',
|
|
'text_light': '#FFFFFF',
|
|
'text_dark': '#2D3748',
|
|
}
|
|
|
|
|
|
container_style = {
|
|
'padding': '5px',
|
|
'backgroundColor': color_palette['text_dark'],
|
|
'borderRadius': '12px',
|
|
'boxShadow': '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
'marginBottom': '25px',
|
|
'border': f'1px solid rgba(255, 255, 255, 0.2)',
|
|
|
|
}
|
|
|
|
hidden_style = {**container_style, 'display': 'none'}
|
|
visible_style = {**container_style}
|
|
|
|
|
|
app.layout = html.Div([
|
|
|
|
html.Div([
|
|
html.H1(request.topic.capitalize() + " Analytics Dashboard", style={
|
|
'textAlign': 'center',
|
|
'marginBottom': '10px',
|
|
'color': color_palette['accent1'],
|
|
'fontSize': '2.5rem',
|
|
'fontWeight': '700',
|
|
'letterSpacing': '0.5px',
|
|
}),
|
|
html.Div([
|
|
html.P("Research Publication Analysis & Knowledge Mapping", style={
|
|
'textAlign': 'center',
|
|
'color': color_palette['text_light'],
|
|
'opacity': '0.8',
|
|
'fontSize': '1.2rem',
|
|
'marginTop': '0',
|
|
})
|
|
])
|
|
], style={
|
|
'background': f'linear-gradient(135deg, {color_palette["background"]}, #364156)',
|
|
'padding': '30px 20px',
|
|
'borderRadius': '12px',
|
|
'marginBottom': '25px',
|
|
'boxShadow': '0 4px 20px rgba(0, 0, 0, 0.2)',
|
|
}),
|
|
|
|
|
|
html.Div([
|
|
html.Div([
|
|
html.Button(
|
|
id='view-toggle',
|
|
children='Switch to Venue View',
|
|
style={
|
|
'padding': '12px 20px',
|
|
'fontSize': '1rem',
|
|
'borderRadius': '8px',
|
|
'border': 'none',
|
|
'backgroundColor': color_palette['accent1'],
|
|
'color': 'white',
|
|
'cursor': 'pointer',
|
|
'boxShadow': '0 2px 5px rgba(0, 0, 0, 0.1)',
|
|
'transition': 'all 0.3s ease',
|
|
'marginRight': '20px',
|
|
'fontWeight': '500',
|
|
}
|
|
),
|
|
html.H3("Filter by Publication Date", style={
|
|
'marginBottom': '15px',
|
|
'color': color_palette['text_dark'],
|
|
'fontSize': '1.3rem',
|
|
'fontWeight': '600',
|
|
}),
|
|
], style={'display': 'flex', 'alignItems': 'center', 'marginBottom': '15px'}),
|
|
|
|
dcc.RangeSlider(
|
|
id='date-slider',
|
|
min=0,
|
|
max=len(date_range) - 1,
|
|
value=[0, len(date_range) - 1],
|
|
marks=date_marks if len(date_marks) <= 12 else {
|
|
i: date_marks[i] for i in range(0, len(date_range), max(1, len(date_range) // 12))
|
|
},
|
|
step=1,
|
|
tooltip={"placement": "bottom", "always_visible": True},
|
|
updatemode='mouseup'
|
|
),
|
|
html.Div(id='date-range-display', style={
|
|
'textAlign': 'center',
|
|
'marginTop': '12px',
|
|
'fontSize': '1.1rem',
|
|
'fontWeight': '500',
|
|
'color': color_palette['accent1'],
|
|
})
|
|
], style={**container_style, 'marginBottom': '25px'}),
|
|
|
|
|
|
html.Div([
|
|
dcc.Graph(
|
|
id='knowledge-map',
|
|
style={'width': '100%', 'height': '700px'},
|
|
config={'scrollZoom': True, 'displayModeBar': True, 'responsive': True}
|
|
)
|
|
], style={
|
|
**container_style,
|
|
'height': '750px',
|
|
'marginBottom': '25px',
|
|
'background': f'linear-gradient(to bottom right, {color_palette["card_bg"]}, #F0F0F8)',
|
|
}),
|
|
|
|
|
|
html.Div([
|
|
html.H3(id='details-title', style={
|
|
'marginBottom': '15px',
|
|
'color': color_palette['accent1'],
|
|
'fontSize': '1.4rem',
|
|
'fontWeight': '600',
|
|
}),
|
|
html.Div(id='details-content', style={
|
|
'maxHeight': '350px',
|
|
'overflowY': 'auto',
|
|
'padding': '10px',
|
|
'borderRadius': '8px',
|
|
'backgroundColor': 'rgba(255, 255, 255, 0.7)',
|
|
})
|
|
], id='details-container', style=hidden_style),
|
|
|
|
|
|
html.Div([
|
|
html.Div([
|
|
dcc.Graph(
|
|
id='oa-pie-chart',
|
|
style={'width': '100%', 'height': '350px'},
|
|
config={'displayModeBar': False, 'responsive': True}
|
|
)
|
|
], style={
|
|
'flex': 1,
|
|
**container_style,
|
|
'margin': '0 10px',
|
|
'height': '400px',
|
|
'transition': 'transform 0.3s ease',
|
|
':hover': {'transform': 'translateY(-5px)'},
|
|
}),
|
|
html.Div([
|
|
dcc.Graph(
|
|
id='oa-status-pie-chart',
|
|
style={'width': '100%', 'height': '350px'},
|
|
config={'displayModeBar': False, 'responsive': True}
|
|
)
|
|
], style={
|
|
'flex': 1,
|
|
**container_style,
|
|
'margin': '0 10px',
|
|
'height': '400px',
|
|
'transition': 'transform 0.3s ease',
|
|
':hover': {'transform': 'translateY(-5px)'},
|
|
})
|
|
], style={'display': 'flex', 'marginBottom': '25px', 'height': '420px'}),
|
|
|
|
|
|
|
|
html.Div([
|
|
dcc.Graph(
|
|
id='type-bar-chart',
|
|
style={'width': '100%', 'height': '50vh'},
|
|
config={'displayModeBar': False, 'responsive': True}
|
|
)
|
|
], style={
|
|
**container_style,
|
|
'height': '500px',
|
|
'background': 'rgba(26, 34, 56, 1)',
|
|
'marginBottom': '10px',
|
|
}),
|
|
|
|
dcc.Store(id='filtered-df-info'),
|
|
dcc.Store(id='current-view', data='host'),
|
|
html.Div(id='load-trigger', children='trigger-initial-load', style={'display': 'none'})
|
|
], style={
|
|
'fontFamily': '"Poppins", "Segoe UI", Arial, sans-serif',
|
|
'backgroundColor': '#121212',
|
|
'backgroundImage': 'none',
|
|
'padding': '30px',
|
|
'maxWidth': '1800px',
|
|
'margin': '0 auto',
|
|
'minHeight': '100vh',
|
|
'color': color_palette['text_light'],
|
|
'paddingBottom': '10px',
|
|
})
|
|
|
|
|
|
|
|
@app.callback(
|
|
[Output('current-view', 'data'),
|
|
Output('view-toggle', 'children')],
|
|
[Input('view-toggle', 'n_clicks')],
|
|
[State('current-view', 'data')]
|
|
)
|
|
def toggle_view(n_clicks, current_view):
|
|
if not n_clicks:
|
|
return current_view, 'Switch to Venue View' if current_view == 'host' else 'Switch to Host View'
|
|
new_view = 'venue' if current_view == 'host' else 'host'
|
|
new_button_text = 'Switch to Host View' if new_view == 'venue' else 'Switch to Venue View'
|
|
return new_view, new_button_text
|
|
|
|
@app.callback(
|
|
Output('date-range-display', 'children'),
|
|
[Input('date-slider', 'value')]
|
|
)
|
|
def update_date_range_display(date_range_indices):
|
|
start_date = date_range[date_range_indices[0]]
|
|
end_date = date_range[date_range_indices[1]]
|
|
return f"Selected period: {start_date.strftime('%b %Y')} to {end_date.strftime('%b %Y')}"
|
|
|
|
@app.callback(
|
|
[Output('knowledge-map', 'figure'),
|
|
Output('oa-pie-chart', 'figure'),
|
|
Output('oa-status-pie-chart', 'figure'),
|
|
Output('type-bar-chart', 'figure'),
|
|
Output('filtered-df-info', 'data'),
|
|
Output('details-container', 'style')],
|
|
[Input('date-slider', 'value'),
|
|
Input('current-view', 'data'),
|
|
Input('load-trigger', 'children')]
|
|
)
|
|
def update_visualizations(date_range_indices, current_view, _):
|
|
filtered_df = filter_by_date_range(df, date_range_indices[0], date_range_indices[1])
|
|
knowledge_map_fig, cluster_metadata = create_knowledge_map(filtered_df, current_view)
|
|
app.cluster_metadata = cluster_metadata
|
|
filtered_info = {
|
|
'start_idx': date_range_indices[0],
|
|
'end_idx': date_range_indices[1],
|
|
'start_date': date_range[date_range_indices[0]].strftime('%Y-%m-%d'),
|
|
'end_date': date_range[date_range_indices[1]].strftime('%Y-%m-%d'),
|
|
'record_count': len(filtered_df),
|
|
'view_type': current_view
|
|
}
|
|
return (
|
|
knowledge_map_fig,
|
|
create_oa_pie_fig(filtered_df),
|
|
create_oa_status_pie_fig(filtered_df),
|
|
create_type_bar_fig(filtered_df),
|
|
filtered_info,
|
|
hidden_style
|
|
)
|
|
|
|
@app.callback(
|
|
[Output('details-container', 'style', allow_duplicate=True),
|
|
Output('details-title', 'children'),
|
|
Output('details-content', 'children')],
|
|
[Input('knowledge-map', 'clickData')],
|
|
[State('filtered-df-info', 'data')],
|
|
prevent_initial_call=True
|
|
)
|
|
def display_details(clickData, filtered_info):
|
|
if not clickData or not filtered_info:
|
|
return hidden_style, "", []
|
|
customdata = clickData['points'][0]['customdata']
|
|
view_type = filtered_info['view_type']
|
|
entity_type = "Organization" if view_type == 'host' else "Venue"
|
|
if len(customdata) >= 2 and customdata[-1] == "cluster":
|
|
count = customdata[0]
|
|
if count not in app.cluster_metadata:
|
|
return hidden_style, "", []
|
|
entities = app.cluster_metadata[count]['entities']
|
|
color = app.cluster_metadata[count]['color']['start']
|
|
table_header = [
|
|
html.Thead(html.Tr([
|
|
html.Th(f"{entity_type} Name", style={'padding': '8px'}),
|
|
html.Th(f"{entity_type} ID", style={'padding': '8px'}),
|
|
html.Th("Papers", style={'padding': '8px', 'textAlign': 'center'}),
|
|
html.Th("Open Access %", style={'padding': '8px', 'textAlign': 'center'})
|
|
], style={'backgroundColor': color_palette['accent1'], 'color': 'white'}))
|
|
]
|
|
|
|
|
|
row_style = {'backgroundColor': '#232D42'} if i % 2 == 0 else {'backgroundColor': '#1A2238'}
|
|
rows = []
|
|
for i, entity in enumerate(sorted(entities, key=lambda x: x['paper_count'], reverse=True)):
|
|
row_style = {'backgroundColor': '#f9f9f9'} if i % 2 == 0 else {'backgroundColor': 'white'}
|
|
entity_name_link = html.A(
|
|
entity[f"{view_type}_organization_name" if view_type == 'host' else "venue"],
|
|
href=entity['entity_id'],
|
|
target="_blank",
|
|
style={'color': color, 'textDecoration': 'underline'}
|
|
)
|
|
entity_id_link = html.A(
|
|
entity['entity_id'].split('/')[-1],
|
|
href=entity['entity_id'],
|
|
target="_blank",
|
|
style={'color': color, 'textDecoration': 'underline'}
|
|
)
|
|
rows.append(html.Tr([
|
|
html.Td(entity_name_link, style={'padding': '8px'}),
|
|
html.Td(entity_id_link, style={'padding': '8px'}),
|
|
html.Td(entity['paper_count'], style={'padding': '8px', 'textAlign': 'center'}),
|
|
html.Td(f"{entity['is_oa']:.1%}", style={'padding': '8px', 'textAlign': 'center'})
|
|
], style=row_style))
|
|
table = html.Table(table_header + [html.Tbody(rows)], style={
|
|
'width': '100%',
|
|
'borderCollapse': 'collapse',
|
|
'boxShadow': '0 1px 3px rgba(0,0,0,0.1)'
|
|
})
|
|
return (
|
|
visible_style,
|
|
f"{entity_type}s with {count} papers",
|
|
[html.P(f"Showing {len(entities)} {entity_type.lower()}s during selected period"), table]
|
|
)
|
|
elif len(customdata) >= 6 and customdata[-1] == "entity":
|
|
entity_name = customdata[0]
|
|
entity_id = customdata[3]
|
|
cluster_count = customdata[4]
|
|
color = app.cluster_metadata[cluster_count]['color']['start']
|
|
if view_type == 'host':
|
|
entity_papers = df[df['host_organization_name'] == entity_name].copy()
|
|
else:
|
|
entity_papers = df[df['venue'] == entity_name].copy()
|
|
entity_papers = entity_papers[
|
|
(entity_papers['publication_date'] >= pd.to_datetime(filtered_info['start_date'])) &
|
|
(entity_papers['publication_date'] <= pd.to_datetime(filtered_info['end_date']))
|
|
]
|
|
entity_name_link = html.A(
|
|
entity_name,
|
|
href=entity_id,
|
|
target="_blank",
|
|
style={'color': color, 'textDecoration': 'underline', 'fontSize': '1.2em'}
|
|
)
|
|
entity_id_link = html.A(
|
|
entity_id.split('/')[-1],
|
|
href=entity_id,
|
|
target="_blank",
|
|
style={'color': color, 'textDecoration': 'underline'}
|
|
)
|
|
header = [
|
|
html.Div([
|
|
html.Span("Name: ", style={'fontWeight': 'bold'}),
|
|
entity_name_link
|
|
], style={'marginBottom': '10px'}),
|
|
html.Div([
|
|
html.Span("ID: ", style={'fontWeight': 'bold'}),
|
|
entity_id_link
|
|
], style={'marginBottom': '10px'}),
|
|
html.Div([
|
|
html.Span(f"Papers: {len(entity_papers)}", style={'marginRight': '20px'}),
|
|
], style={'marginBottom': '20px'})
|
|
]
|
|
table_header = [
|
|
html.Thead(html.Tr([
|
|
html.Th("Paper ID", style={'padding': '8px'}),
|
|
html.Th("Type", style={'padding': '8px'}),
|
|
html.Th("OA Status", style={'padding': '8px', 'textAlign': 'center'}),
|
|
html.Th("Publication Date", style={'padding': '8px', 'textAlign': 'center'})
|
|
], style={'backgroundColor': color, 'color': 'white'}))
|
|
]
|
|
rows = []
|
|
for i, (_, paper) in enumerate(entity_papers.sort_values('publication_date', ascending=False).iterrows()):
|
|
row_style = {'backgroundColor': '#232D42'} if i % 2 == 0 else {'backgroundColor': '#1A2238'}
|
|
paper_link = html.A(
|
|
paper['id'],
|
|
href=paper['id'],
|
|
target="_blank",
|
|
style={'color': color, 'textDecoration': 'underline'}
|
|
)
|
|
rows.append(html.Tr([
|
|
html.Td(paper_link, style={'padding': '8px'}),
|
|
html.Td(paper['type'], style={'padding': '8px'}),
|
|
html.Td(paper['oa_status'], style={'padding': '8px', 'textAlign': 'center'}),
|
|
html.Td(paper['publication_date'].strftime('%Y-%m-%d'), style={'padding': '8px', 'textAlign': 'center'})
|
|
], style=row_style))
|
|
table = html.Table(table_header + [html.Tbody(rows)], style={
|
|
'width': '100%',
|
|
'borderCollapse': 'collapse',
|
|
'boxShadow': '0 1px 3px rgba(0,0,0,0.1)'
|
|
})
|
|
with open("dashboard.html", "w") as f:
|
|
f.write(app.index())
|
|
print("yup saved!!")
|
|
return visible_style, f"{entity_type} Papers", header + [table]
|
|
return hidden_style, "", []
|
|
|
|
|
|
app.run_server(debug=False, port=dashboard_port, use_reloader=False)
|
|
|
|
|
|
dashboard_process = threading.Thread(target=create_and_run_dashboard)
|
|
dashboard_process.daemon = True
|
|
dashboard_process.start()
|
|
|
|
|
|
def open_browser():
|
|
try:
|
|
webbrowser.open_new(f"http://127.0.0.1:{dashboard_port}/")
|
|
except:
|
|
pass
|
|
|
|
threading.Timer(1.5, open_browser).start()
|
|
|
|
return {"status": "success", "message": f"Dashboard loaded successfully on port {dashboard_port}."}
|
|
|
|
except Exception as e:
|
|
|
|
shutdown_existing_dashboard()
|
|
raise HTTPException(status_code=400, detail=str(e)) |