Py.Cafe

DocsPricing
  • app.py
  • represas_con_anomalias.csv
  • requirements.txt
  • us_dams_final_db.py
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
import dash
from dash import dcc, html, Input, Output, State, callback_context
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
import dash_bootstrap_components as dbc

# Load data
df = pd.read_csv("represas_con_anomalias.csv")  # Replace with the correct path

# Filter only dams with anomalies
df_anomalias = df[df['Is_Anomaly'] == 1]

# Get unique states for dropdown
estados_unicos = sorted(df_anomalias['State'].unique())

# Define metrics to compare in radar chart
metricas = [
    'Dam Height (Ft)',
    'NID Height (Ft)',
    'Dam Length (Ft)',
    'NID Storage (Acre-Ft)',
    'Normal Storage (Acre-Ft)',
    'Surface Area (Acres)',
    'Drainage Area (Sq Miles)',
]
hazard_colors = {
    'High': '#440154',      # Morado oscuro
    'Significant': '#FDE725', # Amarillo brillante (para destacar un riesgo considerable)
    'Low': '#21918C',       # Verde azulado
    'Undetermined': '#90D7EC'  # Azul claro (para indicar incertidumbre)
}

# Initialize Dash application with Bootstrap
app = dash.Dash(
    __name__, 
    suppress_callback_exceptions=True,
    external_stylesheets=[dbc.themes.SANDSTONE]  # Modern Bootstrap theme
)

app.title = "US Dams Anomalies Dashboard"

# Dashboard layout using Bootstrap
app.layout = dbc.Container([
    dbc.Row([
        dbc.Col([
            html.H1("🏞️ Distinct US Dams: A Comparative View", 
                    className="text-center my-4 text-primary")
        ]),
    ]),
    
    # Card with general dataset information
    dbc.Row([
        dbc.Col([
            dbc.Card([
                dbc.CardBody([
                    html.H5(f"Total of Distintc US Dams: {len(df_anomalias)}", className="card-title"),
                    html.P(f"Shown here are US dams flagged for singularities using the Isolation Forest machine learning model, designed to isolate atypical data points for analysis, not safety assessment.", 
                           className="card-text"),
                ])
            ], className="mb-4 shadow border border-primary")
        ])
    ]),
   # State filter dropdown and info message as floating elements
    dbc.Row([
        # State filter dropdown as a stylish button
        dbc.Col([
            dbc.DropdownMenu(label="Filter by State: All States",
                             id="state-dropdown-button",
                             className="shadow-lg",
                             children=[
                                 dbc.DropdownMenuItem("All States", id="all-states", active=True),
                                 dbc.DropdownMenuItem(divider=True),
                                 *[dbc.DropdownMenuItem(state, id=f"state-{state}") for state in estados_unicos],
                             ],color="primary",
                             toggle_style={"font-weight": "bold", "border-radius": "10px", 
                                           "padding": "12px 20px",
                                           "box-shadow": "0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08)"},
                             toggle_class_name="d-flex align-items-center"),
            # Hidden dropdown that stores the actual selected value
            dcc.Dropdown(id='state-dropdown',
                         options=[{'label': 'All States', 'value': 'all'},
                                  *[{'label': state, 'value': state} for state in estados_unicos]],
                         value='all',style={'display': 'none'}),], width=4, className="mb-3 d-flex align-items-center"),
        
        # Information message with improved styling
        dbc.Col([
            html.Div(
                html.P(
                    [html.I(className="fas fa-info-circle me-2"), 
                     "Click on any dam on the map to view detailed comparative analysis."],
                    className="text-muted fst-italic mb-0 px-3 py-2 rounded-pill bg-light shadow-sm"),
                className="d-flex justify-content-center align-items-center h-100"
            )
        ], width=8, className="mb-3"),
    ], className="mb-4"),
    
    # Two charts in the same row
    dbc.Row([
        # Left column for the map
        dbc.Col([
            dbc.Card([
                dbc.CardHeader(html.H4("🌐 Location of Singular Dams", className="text-center")),
                dbc.CardBody([
                    dcc.Graph(id='mapa-represas', style={'height': '70vh'}
                             ),
                ])
            ], className="shadow border border-primary"),
            html.Hr(), 
            # Agregar un botón de descarga para los datos filtrados
            html.Button("Download Filter data", id="btn-download"),
            dcc.Download(id="download-dataframe-csv"),
                    ], width=7),
        
        # Right column for information and radar chart
        dbc.Col([
            dbc.Card([
                dbc.CardHeader(html.H4("↔️  Comparing Distinct Features", className="text-center")),
                dbc.CardBody([
                    html.Div(id='info-represa-seleccionada', className="mb-3"),
                    dcc.Graph(id='radar-chart', style={'height': '50vh'}
                             ),
                ])
            ], className="shadow border border-primary")
        ], width=5),
    ], className="mb-4"),
    
    # Footer with additional information
    dbc.Row([
        dbc.Col([
            html.Div([
                    html.P([html.I(className="fas fa-info-circle me-2"), 
                        "US National Inventory of Dams Dashboard © 2025 source:Data is Plutal"],
                           className="text-muted fst-italic mb-0 px-3 py-2 rounded-pill bg-light shadow-sm")
                
            ],className="d-flex justify-content-center align-items-center h-100")
        ], className="mb-3")
    ]),
      
        # Stores to save state and dam selection
        dcc.Store(id='represa-seleccionada'),
        dcc.Store(id='estado-seleccionado', data='all'),
    
], fluid=True, className="px-4 py-3 bg-light")

# Callback to update the state-dropdown based on the button clicks
@app.callback(
    [Output('state-dropdown', 'value'),
     Output('estado-seleccionado', 'data'),
     Output('state-dropdown-button', 'label')],
    [Input('all-states', 'n_clicks')] +
    [Input(f'state-{state}', 'n_clicks') for state in estados_unicos],
    [State('estado-seleccionado', 'data')]
)
def actualizar_estado(*args):
    # Determine which item was clicked
    ctx = dash.callback_context
    
    if not ctx.triggered:
        # No clicks yet, return default
        return 'all', 'all', "Filter by State: All States"
    
    # Get the ID of the component that triggered the callback
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]
    
    if button_id == 'all-states':
        return 'all', 'all', "Filter by State: All States"
    
    # Remove the 'state-' prefix to get the state name
    for state in estados_unicos:
        if button_id == f'state-{state}':
            return state, state, f"Filter by State: {state}"
    
    # If we get here, no specific button was matched
    return dash.no_update, dash.no_update, dash.no_update

# Callback to update the map based on selected state
@app.callback(
    Output('mapa-represas', 'figure'),
    [Input('estado-seleccionado', 'data'),
     Input('represa-seleccionada', 'data')]
)
def actualizar_mapa(estado_seleccionado, represa_seleccionada):
    # Filter by state if a specific state is selected
    if estado_seleccionado != 'all':
        datos_mapa = df_anomalias[df_anomalias['State'] == estado_seleccionado]
    else:
        datos_mapa = df_anomalias
    
    # Create scatter map
    fig = px.scatter_map(
        datos_mapa,
        lat='Latitude',
        lon='Longitude',
        hover_name='Dam Name',
        custom_data=['NID ID','County','Owner Names', 'Primary Owner Type', 'Year Completed'],
        color='Hazard Potential Classification',
        color_discrete_map=hazard_colors,
        size='Anomaly_Score',
        size_max=15,
        zoom=3,
        map_style='light',
        opacity=0.8,
    )
    fig.update_traces(hovertemplate="<b>%{hovertext}</b><br>" +
                                "NID ID: %{customdata[0]}<br>" +
                                "County: %{customdata[1]}<br>" +
                                "Owner: %{customdata[2]}<br>" +
                                "Owner Type: %{customdata[3]}<br>" +
                                "Completed: %{customdata[4]}<extra></extra>")
    
    # Highlight selected dam if there is one
    if represa_seleccionada:
        represa = df_anomalias[df_anomalias['NID ID'] == represa_seleccionada]
        if not represa.empty and (estado_seleccionado == 'all' or represa['State'].values[0] == estado_seleccionado):
            fig.add_trace(go.Scattermap(
                lat=[represa['Latitude'].values[0]],
                lon=[represa['Longitude'].values[0]],
                mode='markers',
                marker=dict(size=35, color='red', opacity=1),
                hoverinfo='none',
                showlegend=False
            ))
    
    # Adjust the center of the map based on the data
    if len(datos_mapa) > 0:
        lat_centro = datos_mapa['Latitude'].mean()
        lon_centro = datos_mapa['Longitude'].mean()
        zoom_level = 3 if estado_seleccionado == 'all' else 5
    else:
        lat_centro = 39  # Default center of US
        lon_centro = -98
        zoom_level = 3
    
    fig.update_layout(
        margin=dict(l=0, r=0, t=0, b=0),
        mapbox=dict(center=dict(lat=lat_centro, lon=lon_centro), zoom=zoom_level),
        clickmode='event+select',
        legend=dict(orientation="h",
                   yanchor="bottom",y=-0.20,xanchor="center",x=0.5)
    )
    
    return fig

# Callback to update selected dam when clicked on map
@app.callback(
    Output('represa-seleccionada', 'data'),
    [Input('mapa-represas', 'clickData')],
    [State('represa-seleccionada', 'data')]
)
def actualizar_represa_seleccionada(click_data, represa_actual):
    if click_data is None:
        return represa_actual
    
    # Get the NID ID of the clicked dam
    nid_id = click_data['points'][0]['customdata'][0]
    return nid_id

# Callback to display selected dam information
@app.callback(
    Output('info-represa-seleccionada', 'children'),
    [Input('represa-seleccionada', 'data')]
)
def mostrar_info_represa(represa_seleccionada):
    if not represa_seleccionada:
        return dbc.Alert(
            "Select a dam on the map to view its comparative analysis",
            color="info",
            className="text-center"
        )
    
    represa = df[df['NID ID'] == represa_seleccionada]
    if represa.empty:
        return dbc.Alert(
            "No information found for the selected dam",
            color="warning",
            className="text-center"
        )
    
    # Create a table with the dam's basic information using Bootstrap
    info = [
        html.H4(f"{represa['Dam Name'].values[0]}", className="text-center text-primary mb-3"),
        dbc.Table([
            html.Tbody([
                html.Tr([
                    html.Td("Last Inspection Date:", className="font-weight-bold"),
                    html.Td(represa['Last Inspection Date'].values[0])
                ]),
                html.Tr([
                    html.Td("Inspection Frequency:", className="font-weight-bold"),
                    html.Td(represa['Inspection Frequency'].values[0])
                ]),
                html.Tr([
                    html.Td("Primary Purpose:", className="font-weight-bold"),
                    html.Td(represa['Primary Purpose'].values[0])
                ]),
                html.Tr([
                    html.Td("Hazard Classification:", className="font-weight-bold"),
                    html.Td(represa['Hazard Potential Classification'].values[0])
                ]),
                html.Tr([
                    html.Td("Condition Assessment:", className="font-weight-bold"),
                    html.Td(represa['Condition Assessment'].values[0] if not pd.isna(represa['Condition Assessment'].values[0]) else "Not available")
                ]),
                html.Tr([
                    html.Td("Anomaly Score:", className="font-weight-bold"),
                    html.Td(
                        html.Span(
                            f"{represa['Anomaly_Score'].values[0]:.4f}",
                            className="badge bg-danger text-white p-2" if represa['Anomaly_Score'].values[0] > 0.7 
                            else "badge bg-warning text-dark p-2" if represa['Anomaly_Score'].values[0] > 0.4 
                            else "badge bg-success text-white p-2"
                        )
                    )
                ]),
            ])
        ], bordered=True, hover=True, size="sm", className="mb-0")
    ]
    
    return html.Div(info)

#Callback to update radar chart
@app.callback(
    Output('radar-chart', 'figure'),
    [Input('represa-seleccionada', 'data')]
)
def actualizar_radar_chart(represa_seleccionada):
    if not represa_seleccionada:
        # If no dam is selected, show empty chart
        fig = go.Figure()
        fig.update_layout(
            title="Select a dam to view comparison metrics",
            xaxis=dict(visible=False),
            yaxis=dict(visible=False),
            plot_bgcolor='rgba(0,0,0,0)',
            paper_bgcolor='rgba(0,0,0,0)',
            font=dict(color="#2C3E50")
        )
        return fig
    
    represa = df[df['NID ID'] == represa_seleccionada]
    if represa.empty:
        return go.Figure()
    
    # Get hazard category of selected dam
    categoria_peligro = represa['Hazard Potential Classification'].values[0]
    
    # Calculate average metrics for all dams in the same category
    df_categoria = df[df['Hazard Potential Classification'] == categoria_peligro]
    
    # Prepare data for radar chart
    datos_radar = []
    
    # Normalize values for each metric
    valores_represa = []
    valores_promedio = []
    etiquetas_metricas = []
    
    for metrica in metricas:
        # Create shorter label for the chart
        etiqueta_corta = metrica.replace(' (Ft)', '').replace(' (Acre-Ft)', '').replace(' (Acres)', '').replace(' (Sq Miles)', '')
        etiquetas_metricas.append(etiqueta_corta)
        
        # Value of selected dam (with NaN handling)
        valor_represa = represa[metrica].values[0] if not pd.isna(represa[metrica].values[0]) else 0
        
        # Average value of the category (with NaN handling)
        valor_promedio = df_categoria[metrica].mean() if not pd.isna(df_categoria[metrica].mean()) else 0
        
        # Add values to lists
        valores_represa.append(valor_represa)
        valores_promedio.append(valor_promedio)
    
    # Normalize values to be on a comparable scale
    max_valores = [max(a, b) for a, b in zip(valores_represa, valores_promedio)]
    max_valores = [val if val > 0 else 1 for val in max_valores]  # Avoid division by zero
    
    valores_represa_norm = [val / max_val for val, max_val in zip(valores_represa, max_valores)]
    valores_promedio_norm = [val / max_val for val, max_val in zip(valores_promedio, max_valores)]
    
    # Create radar chart with more attractive colors
    fig = go.Figure()
    
    fig.add_trace(go.Scatterpolar(
        r=valores_represa_norm,
        theta=etiquetas_metricas,
        fill='toself',
        name=f"{represa['Dam Name'].values[0]}",
        line=dict(color='#3498DB'),
        fillcolor='rgba(52, 152, 219, 0.3)'
    ))
    
    fig.add_trace(go.Scatterpolar(
        r=valores_promedio_norm,
        theta=etiquetas_metricas,
        fill='toself',
        name=f"Average - {categoria_peligro}",
        line=dict(color='#E74C3C'),
        fillcolor='rgba(231, 76, 60, 0.3)'
    ))
    
    fig.update_layout(
        polar=dict(
            radialaxis=dict(
                visible=True,
                range=[0, 1],
                showticklabels=False,
                showline=False,
                ticks='',
                gridcolor='#95a5a6'
            ),
            angularaxis=dict(
                gridcolor='#95a5a6'
            ),
            bgcolor='rgba(255, 255, 255, 0.9)'
        ),
        font=dict(color="#2C3E50"),
        paper_bgcolor='rgba(0,0,0,0)',
        plot_bgcolor='rgba(0,0,0,0)',
        showlegend=True,
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=-0.2,
            xanchor="center",
            x=0.5
        )
    )
    
    return fig

# Y el callback correspondiente
@app.callback(
    Output("download-dataframe-csv", "data"),
    Input("btn-download", "n_clicks"),
    State("estado-seleccionado", "data"),
    prevent_initial_call=True,
)
def func(n_clicks, estado_seleccionado):
    if estado_seleccionado != 'all':
        df_filtrado = df_anomalias[df_anomalias['State'] == estado_seleccionado]
    else:
        df_filtrado = df_anomalias
    return dcc.send_data_frame(df_filtrado.to_csv, "represas_filtradas.csv")