Spaces:
Sleeping
Sleeping
| 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 | |
| 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 | |
| def select_country(n_clicks, ids): | |
| if ctx.triggered_id: | |
| return ctx.triggered_id['index'] | |
| return dash.no_update | |
| # Sunburst plot | |
| 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 | |
| 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) |