""" Visualization module for RehabWatch. Creates maps and charts using Folium and Plotly. """ import numpy as np import xarray as xr import folium from folium import plugins from folium.raster_layers import ImageOverlay import plotly.graph_objects as go import streamlit as st from typing import Dict, Any, List, Optional, Tuple from matplotlib.colors import LinearSegmentedColormap import base64 from io import BytesIO from PIL import Image # NDVI color palette (brown to green) NDVI_COLORS = ['#8B4513', '#D2B48C', '#FFFF00', '#90EE90', '#228B22', '#006400'] # Change color palette (red-white-green diverging) CHANGE_COLORS = ['#B71C1C', '#EF9A9A', '#FFFFFF', '#A5D6A7', '#1B5E20'] def array_to_colored_image( data: np.ndarray, colors: List[str], vmin: float, vmax: float ) -> np.ndarray: """ Convert a 2D array to a colored RGBA image. Args: data: 2D numpy array colors: List of hex color strings for colormap vmin: Minimum value for normalization vmax: Maximum value for normalization Returns: RGBA numpy array (H, W, 4) with values 0-255 """ cmap = LinearSegmentedColormap.from_list('custom', colors) # Normalize data normalized = (data - vmin) / (vmax - vmin) normalized = np.clip(normalized, 0, 1) # Handle NaN values mask = np.isnan(data) # Apply colormap rgba = cmap(normalized) rgba = (rgba * 255).astype(np.uint8) # Set NaN pixels to transparent rgba[mask, 3] = 0 return rgba def create_image_overlay( data: xr.DataArray, colors: List[str], vmin: float, vmax: float, bounds: List[List[float]] ) -> str: """ Create a base64-encoded PNG image for Folium overlay. Args: data: xarray DataArray colors: Color palette vmin: Min value for normalization vmax: Max value for normalization bounds: [[south, west], [north, east]] Returns: Base64 encoded PNG string """ # Get the 2D array arr = data.values if arr.ndim > 2: arr = arr.squeeze() # Create colored image rgba = array_to_colored_image(arr, colors, vmin, vmax) # Flip vertically for correct orientation rgba = np.flipud(rgba) # Convert to PNG img = Image.fromarray(rgba, mode='RGBA') buffer = BytesIO() img.save(buffer, format='PNG') buffer.seek(0) # Encode to base64 img_base64 = base64.b64encode(buffer.getvalue()).decode() return f"data:image/png;base64,{img_base64}" def create_comparison_map( bbox: Tuple[float, float, float, float], ndvi_before: xr.DataArray, ndvi_after: xr.DataArray, ndvi_change: xr.DataArray, center_coords: Tuple[float, float], zoom: int = 12 ) -> folium.Map: """ Create an interactive comparison map with multiple layers. Args: bbox: Bounding box (min_lon, min_lat, max_lon, max_lat) ndvi_before: NDVI xarray at start date ndvi_after: NDVI xarray at end date ndvi_change: NDVI change xarray center_coords: Map center (lat, lon) zoom: Initial zoom level Returns: Folium Map object with all layers """ # Create base map m = folium.Map( location=center_coords, zoom_start=zoom, tiles=None ) # Add satellite basemap folium.TileLayer( tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Satellite Imagery', overlay=False ).add_to(m) # Add OpenStreetMap as alternative folium.TileLayer( tiles='openstreetmap', name='OpenStreetMap', overlay=False ).add_to(m) # Calculate bounds for image overlay min_lon, min_lat, max_lon, max_lat = bbox bounds = [[min_lat, min_lon], [max_lat, max_lon]] # Add NDVI Before layer try: ndvi_before_img = create_image_overlay( ndvi_before, NDVI_COLORS, -0.1, 0.8, bounds ) ImageOverlay( image=ndvi_before_img, bounds=bounds, opacity=0.7, name='NDVI Before', show=False ).add_to(m) except Exception as e: print(f"Error adding NDVI Before layer: {e}") # Add NDVI After layer try: ndvi_after_img = create_image_overlay( ndvi_after, NDVI_COLORS, -0.1, 0.8, bounds ) ImageOverlay( image=ndvi_after_img, bounds=bounds, opacity=0.7, name='NDVI After', show=False ).add_to(m) except Exception as e: print(f"Error adding NDVI After layer: {e}") # Add Change Map layer (shown by default) try: change_img = create_image_overlay( ndvi_change, CHANGE_COLORS, -0.3, 0.3, bounds ) ImageOverlay( image=change_img, bounds=bounds, opacity=0.7, name='Vegetation Change', show=True ).add_to(m) except Exception as e: print(f"Error adding Change layer: {e}") # Add tenement boundary boundary_coords = [ [min_lat, min_lon], [min_lat, max_lon], [max_lat, max_lon], [max_lat, min_lon], [min_lat, min_lon] ] folium.PolyLine( locations=boundary_coords, color='#000000', weight=3, fill=False, popup='Analysis Boundary' ).add_to(m) # Add layer control folium.LayerControl(position='topright').add_to(m) # Add legends _add_legends(m) return m def _add_legends(m: folium.Map) -> None: """Add color legends to the map.""" legend_html = '''

NDVI Scale

-0.10.8

Change

-0.3 +0.3

Red=Decline | Green=Growth

''' m.get_root().html.add_child(folium.Element(legend_html)) def create_simple_map( center_coords: Tuple[float, float], zoom: int = 10, bbox: Optional[Tuple[float, float, float, float]] = None ) -> folium.Map: """ Create a simple map for location preview. Args: center_coords: Map center (lat, lon) zoom: Zoom level bbox: Optional bounding box to display Returns: Folium Map object """ m = folium.Map(location=center_coords, zoom_start=zoom) # Add satellite imagery folium.TileLayer( tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Satellite', overlay=False ).add_to(m) if bbox is not None: min_lon, min_lat, max_lon, max_lat = bbox boundary_coords = [ [min_lat, min_lon], [min_lat, max_lon], [max_lat, max_lon], [max_lat, min_lon], [min_lat, min_lon] ] folium.Polygon( locations=boundary_coords, color='#1B5E20', weight=3, fill=True, fillColor='#2E7D32', fillOpacity=0.2, popup='Analysis Area' ).add_to(m) folium.LayerControl().add_to(m) return m def create_time_series_chart( timeseries_data: List[Dict[str, Any]], title: str = "NDVI Time Series" ) -> go.Figure: """ Create an interactive NDVI time series chart. Args: timeseries_data: List of dicts with 'date' and 'ndvi' keys title: Chart title Returns: Plotly Figure object """ if not timeseries_data: fig = go.Figure() fig.add_annotation( text="No time series data available", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=16) ) return fig dates = [d['date'] for d in timeseries_data] ndvi_values = [d['ndvi'] for d in timeseries_data] fig = go.Figure() # Add NDVI line fig.add_trace(go.Scatter( x=dates, y=ndvi_values, mode='lines+markers', name='NDVI', line=dict(color='#2E7D32', width=2), marker=dict(size=6), hovertemplate='Date: %{x}
NDVI: %{y:.3f}' )) # Add reference lines fig.add_hline(y=0.6, line_dash="dash", line_color="#4CAF50", annotation_text="Healthy Vegetation", annotation_position="right") fig.add_hline(y=0.2, line_dash="dash", line_color="#FF9800", annotation_text="Sparse Vegetation", annotation_position="right") fig.update_layout( title=dict(text=title, font=dict(size=18)), xaxis_title="Date", yaxis_title="NDVI", yaxis=dict(range=[0, 1]), template="plotly_white", hovermode="x unified", height=400, margin=dict(l=60, r=40, t=60, b=60) ) return fig def create_stats_display(stats: Dict[str, float], rehab_score: int) -> None: """ Display statistics using Streamlit components. Args: stats: Statistics dictionary rehab_score: Rehabilitation score (0-100) """ # Rehabilitation Score with large display st.markdown("### Rehabilitation Score") score_color = _get_score_color(rehab_score) st.markdown(f"""
{rehab_score} /100
""", unsafe_allow_html=True) # Progress bar st.progress(rehab_score / 100) # Key Metrics in columns # Logic: arrow direction = numeric delta; color = good/bad for nature st.markdown("### Key Metrics") col1, col2, col3 = st.columns(3) ndvi_change = stats.get('ndvi_change_mean', 0) percent_change = stats.get('percent_change', 0) with col1: st.metric( label="NDVI Before", value=f"{stats['ndvi_before_mean']:.3f}", help="Normalized Difference Vegetation Index: measures vegetation health (-1 to 1)" ) st.metric( label="Area Improved", value=f"{stats['area_improved_ha']:.1f} ha", delta=f"+{stats['percent_improved']:.1f}%", delta_color="normal" # improvement is always good ) with col2: # NDVI: increase = good (green), decrease = bad (red) st.metric( label="NDVI After", value=f"{stats['ndvi_after_mean']:.3f}", delta=f"{ndvi_change:+.3f}" if ndvi_change != 0 else None, delta_color="normal", # green for +, red for - help="Current vegetation index value" ) st.metric( label="Area Degraded", value=f"{stats['area_degraded_ha']:.1f} ha", delta=f"-{stats['percent_degraded']:.1f}%", delta_color="inverse" # degradation showing as negative is correct ) with col3: # Vegetation Change: increase = good (green), decrease = bad (red) st.metric( label="Vegetation Change", value=f"{percent_change:+.1f}%", delta=f"{percent_change:+.1f}%" if percent_change != 0 else None, delta_color="normal", # green for +, red for - help="Percentage change in vegetation cover" ) st.metric( label="Total Area", value=f"{stats['total_area_ha']:.1f} ha" ) def _get_score_color(score: int) -> str: """Get color based on rehabilitation score.""" if score >= 80: return "#1B5E20" elif score >= 60: return "#4CAF50" elif score >= 40: return "#FF9800" elif score >= 20: return "#F57C00" else: return "#B71C1C" def create_area_breakdown_chart(stats: Dict[str, float]) -> go.Figure: """ Create a pie chart showing area breakdown. Args: stats: Statistics dictionary with area values Returns: Plotly Figure object """ labels = ['Improved', 'Stable', 'Degraded'] values = [ stats['area_improved_ha'], stats['area_stable_ha'], stats['area_degraded_ha'] ] colors = ['#4CAF50', '#FFC107', '#F44336'] fig = go.Figure(data=[go.Pie( labels=labels, values=values, marker_colors=colors, hole=0.4, textinfo='label+percent', hovertemplate='%{label}
%{value:.1f} ha
%{percent}' )]) fig.update_layout( title="Area Breakdown", annotations=[dict(text='Area', x=0.5, y=0.5, font_size=16, showarrow=False)], showlegend=True, height=350 ) return fig def create_ndvi_comparison_chart(stats: Dict[str, float]) -> go.Figure: """ Create a bar chart comparing before/after NDVI. Args: stats: Statistics dictionary Returns: Plotly Figure object """ fig = go.Figure() fig.add_trace(go.Bar( x=['Before', 'After'], y=[stats['ndvi_before_mean'], stats['ndvi_after_mean']], marker_color=['#8B4513', '#228B22'], text=[f"{stats['ndvi_before_mean']:.3f}", f"{stats['ndvi_after_mean']:.3f}"], textposition='outside' )) fig.update_layout( title="NDVI Comparison", yaxis_title="NDVI", yaxis=dict(range=[0, max(stats['ndvi_after_mean'], stats['ndvi_before_mean']) * 1.3]), template="plotly_white", height=350 ) return fig def create_statistics_table(stats: Dict[str, float]) -> None: """ Display full statistics as a formatted table. Args: stats: Statistics dictionary """ import pandas as pd data = { 'Metric': [ 'NDVI Before (mean)', 'NDVI After (mean)', 'NDVI Change (mean)', 'NDVI Change (std dev)', 'Relative Change', 'Area Improved', 'Area Stable', 'Area Degraded', 'Total Area', '% Improved', '% Stable', '% Degraded' ], 'Value': [ f"{stats['ndvi_before_mean']:.4f}", f"{stats['ndvi_after_mean']:.4f}", f"{stats['ndvi_change_mean']:.4f}", f"{stats['ndvi_change_std']:.4f}", f"{stats['percent_change']:.2f}%", f"{stats['area_improved_ha']:.2f} ha", f"{stats['area_stable_ha']:.2f} ha", f"{stats['area_degraded_ha']:.2f} ha", f"{stats['total_area_ha']:.2f} ha", f"{stats['percent_improved']:.2f}%", f"{stats['percent_stable']:.2f}%", f"{stats['percent_degraded']:.2f}%" ], 'Description': [ 'Mean vegetation index at analysis start', 'Mean vegetation index at analysis end', 'Average change in vegetation index', 'Variation in vegetation change', 'Percentage change in mean NDVI', 'Area with NDVI increase > 0.05', 'Area with NDVI change between -0.05 and 0.05', 'Area with NDVI decrease > 0.05', 'Total analyzed area', 'Percentage of area showing improvement', 'Percentage of area remaining stable', 'Percentage of area showing degradation' ] } df = pd.DataFrame(data) st.dataframe(df, use_container_width=True, hide_index=True) # ============================================================================= # NEW EXTENDED VISUALIZATIONS # ============================================================================= # Color palettes for different indices BSI_COLORS = ['#228B22', '#90EE90', '#FFFF00', '#D2B48C', '#8B4513'] # Green to brown WATER_COLORS = ['#8B4513', '#D2B48C', '#87CEEB', '#4169E1', '#000080'] # Brown to blue MOISTURE_COLORS = ['#B71C1C', '#FF5722', '#FFEB3B', '#8BC34A', '#1B5E20'] # Dry to wet SLOPE_COLORS = ['#1B5E20', '#4CAF50', '#FFEB3B', '#FF9800', '#B71C1C'] # Flat to steep EROSION_COLORS = ['#1B5E20', '#4CAF50', '#FFEB3B', '#FF5722', '#B71C1C'] # Low to high risk # Land cover color mapping LULC_COLORS = { 1: '#0000FF', # Water - Blue 2: '#228B22', # Trees - Forest Green 4: '#006400', # Flooded Vegetation - Dark Green 5: '#FFD700', # Crops - Gold 7: '#808080', # Built Area - Gray 8: '#D2691E', # Bare Ground - Chocolate 9: '#FFFFFF', # Snow/Ice - White 10: '#C0C0C0', # Clouds - Silver 11: '#9ACD32' # Rangeland - Yellow Green } def create_multi_index_map( bbox: Tuple[float, float, float, float], indices_after: Dict[str, xr.DataArray], index_changes: Dict[str, xr.DataArray], center_coords: Tuple[float, float], zoom: int = 12 ) -> folium.Map: """ Create an interactive map with multiple index layers. """ m = folium.Map(location=center_coords, zoom_start=zoom, tiles=None) # Add basemaps folium.TileLayer( tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Satellite', overlay=False ).add_to(m) folium.TileLayer(tiles='openstreetmap', name='OpenStreetMap', overlay=False).add_to(m) min_lon, min_lat, max_lon, max_lat = bbox bounds = [[min_lat, min_lon], [max_lat, max_lon]] # Index configurations: (data, colors, vmin, vmax, name) index_configs = [ ('ndvi', NDVI_COLORS, -0.1, 0.8, 'NDVI'), ('savi', NDVI_COLORS, -0.1, 0.8, 'SAVI'), ('evi', NDVI_COLORS, -0.1, 0.8, 'EVI'), ('bsi', BSI_COLORS, -0.5, 0.5, 'Bare Soil Index'), ('ndwi', WATER_COLORS, -0.5, 0.5, 'Water Index (NDWI)'), ('ndmi', MOISTURE_COLORS, -0.5, 0.5, 'Moisture Index (NDMI)'), ] # Add current state layers for idx_key, colors, vmin, vmax, name in index_configs: if idx_key in indices_after: try: img = create_image_overlay(indices_after[idx_key], colors, vmin, vmax, bounds) ImageOverlay( image=img, bounds=bounds, opacity=0.7, name=f'{name} (Current)', show=(idx_key == 'ndvi') ).add_to(m) except Exception: pass # Add change layers for idx_key, _, _, _, name in index_configs: if idx_key in index_changes: try: img = create_image_overlay(index_changes[idx_key], CHANGE_COLORS, -0.3, 0.3, bounds) ImageOverlay( image=img, bounds=bounds, opacity=0.7, name=f'{name} Change', show=False ).add_to(m) except Exception: pass # Add boundary boundary_coords = [ [min_lat, min_lon], [min_lat, max_lon], [max_lat, max_lon], [max_lat, min_lon], [min_lat, min_lon] ] folium.PolyLine(locations=boundary_coords, color='#000000', weight=3).add_to(m) folium.LayerControl(position='topright').add_to(m) _add_legends(m) return m def create_terrain_map( bbox: Tuple[float, float, float, float], slope: xr.DataArray, aspect: Optional[xr.DataArray], erosion_risk: Optional[xr.DataArray], center_coords: Tuple[float, float], zoom: int = 12 ) -> folium.Map: """ Create an interactive terrain analysis map. """ m = folium.Map(location=center_coords, zoom_start=zoom, tiles=None) folium.TileLayer( tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Satellite', overlay=False ).add_to(m) min_lon, min_lat, max_lon, max_lat = bbox bounds = [[min_lat, min_lon], [max_lat, max_lon]] # Add slope layer try: slope_img = create_image_overlay(slope, SLOPE_COLORS, 0, 45, bounds) ImageOverlay( image=slope_img, bounds=bounds, opacity=0.7, name='Slope (degrees)', show=True ).add_to(m) except Exception: pass # Add erosion risk layer if erosion_risk is not None: try: erosion_img = create_image_overlay(erosion_risk, EROSION_COLORS, 0, 1, bounds) ImageOverlay( image=erosion_img, bounds=bounds, opacity=0.7, name='Erosion Risk', show=False ).add_to(m) except Exception: pass folium.LayerControl(position='topright').add_to(m) return m def create_land_cover_map( bbox: Tuple[float, float, float, float], lulc: xr.DataArray, center_coords: Tuple[float, float], zoom: int = 12, year: int = 2023 ) -> folium.Map: """ Create a land cover classification map. """ from matplotlib.colors import ListedColormap m = folium.Map(location=center_coords, zoom_start=zoom, tiles=None) folium.TileLayer( tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Satellite', overlay=False ).add_to(m) min_lon, min_lat, max_lon, max_lat = bbox bounds = [[min_lat, min_lon], [max_lat, max_lon]] # Create categorical colormap try: arr = lulc.values.squeeze() rgba = np.zeros((*arr.shape, 4), dtype=np.uint8) for class_id, color in LULC_COLORS.items(): mask = arr == class_id r = int(color[1:3], 16) g = int(color[3:5], 16) b = int(color[5:7], 16) rgba[mask] = [r, g, b, 200] rgba = np.flipud(rgba) img = Image.fromarray(rgba, mode='RGBA') buffer = BytesIO() img.save(buffer, format='PNG') buffer.seek(0) img_base64 = base64.b64encode(buffer.getvalue()).decode() img_url = f"data:image/png;base64,{img_base64}" ImageOverlay( image=img_url, bounds=bounds, opacity=0.7, name=f'Land Cover {year}', show=True ).add_to(m) except Exception: pass folium.LayerControl(position='topright').add_to(m) return m def create_multi_index_chart(stats: Dict[str, float]) -> go.Figure: """ Create a grouped bar chart comparing all indices before/after. """ indices = ['NDVI', 'SAVI', 'EVI', 'NDWI', 'NDMI', 'BSI'] before_values = [] after_values = [] for idx in ['ndvi', 'savi', 'evi', 'ndwi', 'ndmi', 'bsi']: before_values.append(stats.get(f'{idx}_before_mean', 0)) after_values.append(stats.get(f'{idx}_after_mean', 0)) fig = go.Figure() fig.add_trace(go.Bar( name='Before', x=indices, y=before_values, marker_color='#8B4513', text=[f'{v:.3f}' for v in before_values], textposition='outside' )) fig.add_trace(go.Bar( name='After', x=indices, y=after_values, marker_color='#228B22', text=[f'{v:.3f}' for v in after_values], textposition='outside' )) fig.update_layout( title='Multi-Index Comparison', barmode='group', yaxis_title='Index Value', template='plotly_white', height=400, legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99) ) return fig def create_terrain_stats_chart(terrain_stats: Dict[str, float]) -> go.Figure: """ Create a chart showing terrain slope distribution. """ labels = ['Flat (<5°)', 'Gentle (5-15°)', 'Moderate (15-30°)', 'Steep (>30°)'] values = [ terrain_stats.get('percent_flat', 0), terrain_stats.get('percent_gentle', 0), terrain_stats.get('percent_moderate', 0), terrain_stats.get('percent_steep', 0) ] colors = ['#1B5E20', '#4CAF50', '#FF9800', '#B71C1C'] fig = go.Figure(data=[go.Pie( labels=labels, values=values, marker_colors=colors, hole=0.4, textinfo='label+percent' )]) fig.update_layout( title='Slope Distribution', height=350 ) return fig def create_land_cover_chart(land_cover_stats: Dict[str, Any]) -> go.Figure: """ Create a grouped bar chart showing land cover change. """ if 'class_changes' not in land_cover_stats: return go.Figure() changes = land_cover_stats['class_changes'] classes = list(changes.keys()) before = [changes[c].get('before', 0) for c in classes] after = [changes[c].get('after', 0) for c in classes] # Convert to percentages total_before = sum(before) or 1 total_after = sum(after) or 1 before_pct = [b / total_before * 100 for b in before] after_pct = [a / total_after * 100 for a in after] fig = go.Figure() fig.add_trace(go.Bar( name=f"Year {land_cover_stats.get('year_before', 'Before')}", x=classes, y=before_pct, marker_color='#8B4513' )) fig.add_trace(go.Bar( name=f"Year {land_cover_stats.get('year_after', 'After')}", x=classes, y=after_pct, marker_color='#228B22' )) fig.update_layout( title='Land Cover Change', barmode='group', yaxis_title='Percentage (%)', template='plotly_white', height=400 ) return fig def create_vegetation_health_chart(stats: Dict[str, float]) -> go.Figure: """ Create a chart showing vegetation health distribution. """ labels = ['Sparse (0-0.2)', 'Low (0.2-0.4)', 'Moderate (0.4-0.6)', 'Dense (>0.6)'] values = [ stats.get('percent_sparse_veg', 0), stats.get('percent_low_veg', 0), stats.get('percent_moderate_veg', 0), stats.get('percent_dense_veg', 0) ] colors = ['#D2B48C', '#90EE90', '#228B22', '#006400'] fig = go.Figure(data=[go.Pie( labels=labels, values=values, marker_colors=colors, hole=0.4, textinfo='label+percent' )]) fig.update_layout( title='Vegetation Health Distribution', height=350 ) return fig def create_environmental_indicators_chart(stats: Dict[str, float]) -> go.Figure: """ Create a radar chart showing environmental indicators. """ categories = ['Vegetation', 'Moisture', 'Soil Stability', 'Water Presence', 'Dense Veg'] # Normalize values to 0-100 scale values = [ min(100, stats.get('ndvi_after_mean', 0) * 100 / 0.6), # NDVI max(0, 100 - stats.get('percent_moisture_stressed', 50)), # Moisture health max(0, 100 - stats.get('percent_bare_soil', 50)), # Soil stability min(100, stats.get('percent_water', 0) * 10), # Water presence stats.get('percent_dense_veg', 0) # Dense vegetation ] fig = go.Figure() fig.add_trace(go.Scatterpolar( r=values + [values[0]], # Close the polygon theta=categories + [categories[0]], fill='toself', fillcolor='rgba(46, 125, 50, 0.3)', line=dict(color='#2E7D32', width=2), name='Current State' )) fig.update_layout( polar=dict( radialaxis=dict(visible=True, range=[0, 100]) ), title='Environmental Health Indicators', height=400, showlegend=False ) return fig def create_comprehensive_stats_display( stats: Dict[str, float], rehab_score: int, terrain_stats: Optional[Dict] = None, land_cover_stats: Optional[Dict] = None ) -> None: """ Display comprehensive statistics with all new metrics. """ # Rehabilitation Score st.markdown("### Rehabilitation Score") score_color = _get_score_color(rehab_score) st.markdown(f"""
{rehab_score} /100
""", unsafe_allow_html=True) st.progress(rehab_score / 100) # Primary Metrics with tooltips # Logic: # - Arrow direction: based on numeric delta (positive=up, negative=down) # - Color: "normal" = green for increase (good), red for decrease (bad) # "inverse" = red for increase (bad), green for decrease (good) st.markdown("### Key Metrics") col1, col2, col3, col4 = st.columns(4) # Get change values for proper arrow direction ndvi_change = stats.get('ndvi_change_mean', 0) percent_change = stats.get('percent_change', 0) with col1: # NDVI: increase = good (green), decrease = bad (red) st.metric( "NDVI", f"{stats.get('ndvi_after_mean', 0):.3f}", delta=f"{ndvi_change:+.3f}" if ndvi_change != 0 else None, delta_color="normal", # green for +, red for - help="Normalized Difference Vegetation Index: measures vegetation health. Values range from -1 to 1, with >0.4 indicating healthy vegetation." ) with col2: # Vegetation Change: increase = good (green), decrease = bad (red) # Use numeric delta for correct arrow direction st.metric( "Vegetation Change", f"{percent_change:+.1f}%", delta=f"{percent_change:+.1f}%" if percent_change != 0 else None, delta_color="normal", # green for +, red for - help="Percentage change in vegetation cover between analysis dates." ) with col3: bsi_change = stats.get('bsi_change', 0) # Bare Soil: increase = bad (red), decrease = good (green) st.metric( "Bare Soil", f"{stats.get('percent_bare_soil', 0):.1f}%", delta=f"{bsi_change:+.3f}" if bsi_change != 0 else None, delta_color="inverse", # red for +, green for - help="Percentage of area with exposed bare soil. Lower values indicate better vegetation cover." ) with col4: st.metric( "Water Presence", f"{stats.get('percent_water', 0):.1f}%", help="Percentage of area with water bodies or saturated soil." ) # Secondary Metrics with tooltips st.markdown("### Additional Indices") col1, col2, col3 = st.columns(3) with col1: savi_change = stats.get('savi_change', 0) # SAVI: increase = good (green), decrease = bad (red) st.metric( "SAVI", f"{stats.get('savi_after_mean', 0):.3f}", delta=f"{savi_change:+.3f}" if savi_change != 0 else None, delta_color="normal", # green for +, red for - help="Soil Adjusted Vegetation Index: better for sparse vegetation as it accounts for soil brightness." ) evi_change = stats.get('evi_change', 0) # EVI: increase = good (green), decrease = bad (red) st.metric( "EVI", f"{stats.get('evi_after_mean', 0):.3f}", delta=f"{evi_change:+.3f}" if evi_change != 0 else None, delta_color="normal", # green for +, red for - help="Enhanced Vegetation Index: more sensitive in high-biomass areas and corrects for atmospheric effects." ) with col2: ndmi_change = stats.get('ndmi_change', 0) # NDMI: increase = good (green), decrease = bad (red) st.metric( "NDMI", f"{stats.get('ndmi_after_mean', 0):.3f}", delta=f"{ndmi_change:+.3f}" if ndmi_change != 0 else None, delta_color="normal", # green for +, red for - help="Normalized Difference Moisture Index: measures vegetation water content. Higher values = more moisture." ) bsi_val_change = stats.get('bsi_change', 0) # BSI: increase = bad (red), decrease = good (green) st.metric( "BSI", f"{stats.get('bsi_after_mean', 0):.3f}", delta=f"{bsi_val_change:+.3f}" if bsi_val_change != 0 else None, delta_color="inverse", # red for +, green for - help="Bare Soil Index: identifies bare soil areas. Higher values indicate more exposed soil (negative for rehab)." ) with col3: st.metric( "Moisture Stressed", f"{stats.get('percent_moisture_stressed', 0):.1f}%", help="Percentage of vegetation showing signs of water stress." ) st.metric( "Dense Vegetation", f"{stats.get('percent_dense_veg', 0):.1f}%", help="Percentage of area with dense, healthy vegetation (NDVI > 0.6)." ) # Terrain stats if available if terrain_stats and terrain_stats.get('slope_mean'): st.markdown("### Terrain Analysis") col1, col2, col3 = st.columns(3) with col1: st.metric("Mean Slope", f"{terrain_stats.get('slope_mean', 0):.1f}°") with col2: st.metric("Steep Areas", f"{terrain_stats.get('percent_steep', 0):.1f}%") with col3: if 'percent_high_erosion_risk' in terrain_stats: st.metric("High Erosion Risk", f"{terrain_stats.get('percent_high_erosion_risk', 0):.1f}%", delta_color="inverse") # Land cover stats if available if land_cover_stats and land_cover_stats.get('vegetation_cover_after'): st.markdown("### Land Cover") col1, col2 = st.columns(2) with col1: st.metric("Vegetation Cover", f"{land_cover_stats.get('vegetation_cover_after', 0):.1f}%", delta=f"{land_cover_stats.get('vegetation_cover_change', 0):.1f}%") with col2: st.metric("Bare Ground", f"{land_cover_stats.get('bare_ground_after', 0):.1f}%", delta=f"{land_cover_stats.get('bare_ground_change', 0):.1f}%", delta_color="inverse")