import dash from dash import dcc, html, Input, Output, State, ctx import pandas as pd import plotly.express as px from dash.dependencies import ALL # Load dataset df = pd.read_csv("data/moths_combined.csv") # df['country'] = "UK" # Fields to evaluate completeness description_fields = [ 'main colors', 'pattern description', 'details colors', 'antennae description', 'antennae colors', 'head color', 'abdomen color', 'forewings description', 'forewing colors', 'hindwing description', 'hindwing colors' ] # Compute completeness def compute_completeness(row): filled = sum([pd.notna(row[col]) and str(row[col]).strip() != "" for col in description_fields]) return filled / len(description_fields) df["completeness_score"] = df.apply(compute_completeness, axis=1) def classify(score): if score >= 0.8: return "High" elif score >= 0.6: return "Medium" else: return "Low" df["completeness_label"] = df["completeness_score"].apply(classify) # Country-level completeness def completeness_color(score): if score >= 0.8: return 'green' elif score >= 0.6: return 'orange' else: return 'red' country_completeness = df.groupby('country')['completeness_score'].mean().reset_index() country_completeness['color'] = country_completeness['completeness_score'].apply(completeness_color) # Dash app app = dash.Dash(__name__, suppress_callback_exceptions=True) server = app.server app.title = "Moth Explorer" app.layout = html.Div([ html.Div([ html.H1("🦋 Moth Description Explorer 🦋", style={ 'textAlign': 'center', 'padding': '20px 0', 'color': '#1c3b6f' }), html.P( "Explore the morphological completeness of moth species, from order to species level. " "Click on the sunburst chart to view detailed color and wing pattern information.", style={ 'textAlign': 'center', 'fontSize': '16px', 'maxWidth': '900px', 'margin': '0 auto', 'paddingBottom': '20px', 'color': '#333' } ), dcc.Store(id='selected-country', data=None), html.Div(id='country-boxes', style={ 'display': 'flex', 'flexWrap': 'wrap', 'gap': '10px', 'justifyContent': 'center', 'marginBottom': '20px' }) ], style={'fontFamily': '"Segoe UI", Roboto, Inter, sans-serif'}), html.Div([ html.Div([ dcc.Graph(id='sunburst-plot', config={'scrollZoom': True}) ], style={'flex': '1', 'padding': '10px'}), html.Div(id='info-box', style={ 'flex': '1', 'padding': '20px', 'fontSize': '16px', 'lineHeight': '1.5', 'overflowY': 'auto' }) ], style={ 'display': 'flex', 'flexDirection': 'row', 'flexWrap': 'nowrap', 'height': '80vh', 'gap': '20px', 'fontFamily': '"Segoe UI", Roboto, Inter, sans-serif' }), html.Div([ html.Img(src='/assets/images/logo1.png', style={'height': '60px', 'margin': '10px'}), html.Img(src='/assets/images/logo2.png', style={'height': '60px', 'margin': '10px'}) ], style={ 'textAlign': 'center', 'padding': '20px 0' }) ]) # Country selection boxes @app.callback( Output('country-boxes', 'children'), Input('selected-country', 'data') ) def render_country_boxes(selected_country): boxes = [] for _, row in country_completeness.iterrows(): border = '3px solid black' if row['country'] == selected_country else '1px solid #ccc' boxes.append(html.Div( row['country'], id={'type': 'country-box', 'index': row['country']}, style={ 'backgroundColor': row['color'], 'padding': '10px 15px', 'borderRadius': '8px', 'border': border, 'cursor': 'pointer', 'color': 'white', 'fontWeight': 'bold', 'textAlign': 'center' } )) boxes.append(html.Div( "All Countries", id={'type': 'country-box', 'index': 'all'}, style={ 'backgroundColor': '#999', 'padding': '10px 15px', 'borderRadius': '8px', 'border': '1px solid #ccc', 'cursor': 'pointer', 'color': 'white', 'fontWeight': 'bold', 'textAlign': 'center' } )) return boxes # Handle box click @app.callback( Output('selected-country', 'data'), Input({'type': 'country-box', 'index': ALL}, 'n_clicks'), State({'type': 'country-box', 'index': ALL}, 'id'), prevent_initial_call=True ) def select_country(n_clicks, ids): if ctx.triggered_id: return ctx.triggered_id['index'] return dash.no_update # Sunburst plot @app.callback( Output('sunburst-plot', 'figure'), # Input('sunburst-plot', 'clickData'), Input('selected-country', 'data') ) def update_sunburst(selected_country): print(f"Selected country: {selected_country}") filtered_df = df[df['country'] == selected_country] if selected_country else df if selected_country != 'UK' and selected_country != 'Costa Rica': print('no UK or Costa Rica selected, showing all data') filtered_df = df # Filter out low completeness print(f"Filtered data size: {filtered_df.shape}") fig = px.sunburst( filtered_df, path=['order_name', 'family_name', 'genus_name', 'species_name'], values=None, # Use counts automatically color='completeness_score', color_continuous_scale=['red', 'orange', 'green'], hover_data={'completeness_score': ':.2f'} ) fig.update_traces(insidetextorientation='radial') fig.update_layout(margin=dict(t=0, l=0, r=0, b=0)) return fig # Info panel with table @app.callback( Output('info-box', 'children'), Input('sunburst-plot', 'clickData') ) def display_info(clickData): if clickData: label = clickData['points'][0]['label'] matched = df[df['species_name'] == label] if not matched.empty: row = matched.iloc[0] fields = { "Main colors": row['main colors'], "Pattern": row['pattern description'], "Details colors": row['details colors'], "Antennae": f"{row['antennae description']} ({row['antennae colors']})", "Head color": row['head color'], "Abdomen color": row['abdomen color'], "Forewings": f"{row['forewings description']} ({row['forewing colors']})", "Hindwings": f"{row['hindwing description']} ({row['hindwing colors']})" } table_rows = [ html.Tr([ html.Th("Field", style={'textAlign': 'left', 'border': '1px solid #ccc', 'padding': '6px', 'backgroundColor': '#f0f0f0'}), html.Th("Value", style={'textAlign': 'left', 'border': '1px solid #ccc', 'padding': '6px', 'backgroundColor': '#f0f0f0'}) ]) ] for key, value in fields.items(): table_rows.append( html.Tr([ html.Td(key, style={'border': '1px solid #ccc', 'padding': '6px'}), html.Td(value if value and str(value).strip() else "N/A", style={'border': '1px solid #ccc', 'padding': '6px'}) ]) ) return html.Div([ html.H2(f"{row['species_name']}", style={'color': '#1c3b6f'}), html.P(f"Order: {row['order_name']}"), html.P(f"Family: {row['family_name']}"), html.P(f"Genus: {row['genus_name']}"), html.P(f"Completeness level: {row['completeness_label']}"), html.Table(table_rows, style={ 'borderCollapse': 'collapse', 'width': '100%', 'marginTop': '20px' }) ]) return html.Div("Click on a species to view its descriptions.") if __name__ == '__main__': app.run_server(debug=True)