| | import pandas as pd |
| | import plotly.express as px |
| | import plotly.graph_objects as go |
| | from dash import Dash, dcc, html, Input, Output, State, ALL, MATCH |
| | import numpy as np |
| | import random |
| | import math |
| | from collections import defaultdict |
| | import colorsys |
| | from fastapi import HTTPException |
| | from pydantic import BaseModel |
| | from dash import Dash |
| | import dash_bootstrap_components as dbc |
| | from fastapi import HTTPException, APIRouter, Request |
| |
|
| | router = APIRouter() |
| |
|
| | |
| | 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_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') |
| | ) |
| | 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, |
| | ) |
| | return fig |
| |
|
| | |
| | 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): |
| | 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)} |
| | |
| | create_and_run_dashboard(df, request.topic) |
| | base_url = str(req.base_url) |
| | venue_redirect_url = f"{base_url}venue_redirect/{request.userId}/{request.topic}/{request.year}" |
| | |
| | return { |
| | "status": "success", |
| | "message": "Dashboard ready at /venues/", |
| | "redirect": "/venues/", |
| | "open_url": venue_redirect_url |
| | } |
| | except Exception as e: |
| | raise HTTPException(status_code=400, detail=str(e)) |
| |
|
| | venue_dash_app = None |
| |
|
| | def create_and_run_dashboard(df, topic): |
| | global venue_dash_app |
| | from app import get_or_create_venue_dash_app |
| | venue_dash_app = get_or_create_venue_dash_app() |
| | |
| | if hasattr(venue_dash_app, 'cluster_metadata'): |
| | venue_dash_app.cluster_metadata.clear() |
| | |
| | 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} |
| | |
| | venue_dash_app.layout = html.Div([ |
| | html.Div([ |
| | html.H1(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=f"trigger-{pd.Timestamp.now().timestamp()}", style={'display': 'none'}) |
| | ], style={ |
| | 'fontFamily': '"Poppins", "Segoe UI", Arial, sans-serif', |
| | 'backgroundColor': '#121212', |
| | 'padding': '30px', |
| | 'maxWidth': '1800px', |
| | 'margin': '0 auto', |
| | 'minHeight': '100vh', |
| | 'color': color_palette['text_light'], |
| | 'paddingBottom': '10px', |
| | }) |
| | |
| | @venue_dash_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 |
| |
|
| | @venue_dash_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')}" |
| |
|
| | @venue_dash_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) |
| | venue_dash_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 |
| | ) |
| |
|
| | @venue_dash_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 venue_dash_app.cluster_metadata: |
| | return hidden_style, "", [] |
| | entities = venue_dash_app.cluster_metadata[count]['entities'] |
| | color = venue_dash_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'})) |
| | ] |
| | rows = [] |
| | for entity in sorted(entities, key=lambda x: x['paper_count'], reverse=True): |
| | 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'}) |
| | ])) |
| | 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 = venue_dash_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 _, paper in entity_papers.sort_values('publication_date', ascending=False).iterrows(): |
| | 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'}) |
| | ])) |
| | 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} Papers", header + [table] |
| | return hidden_style, "", [] |
| |
|
| | return None |