diff --git a/mcass-dashboard.py b/mcass-dashboard.py index 0e091d7..297f93c 100644 --- a/mcass-dashboard.py +++ b/mcass-dashboard.py @@ -50,7 +50,7 @@ def read_snow_situation_file(filepath): df = pd.read_csv(filepath) # Make sure the Date column is of type datetime df['date'] = pd.to_datetime(df['date']) - print(f"read_snow_situation_file: df.head: \n{df.columns}\n{df.head()}") + # print(f"read_snow_situation_file: df.head: \n{df.columns}\n{df.head()}") return df def read_basin_geometry(filepath): @@ -66,6 +66,32 @@ def read_basin_geometry(filepath): #gdf = gpd.read_file('www/CA-discharge_basins_for_viz.gpkg') # No overlaps gdf = gpd.read_file(filepath) # With overlaps + # Print column names and head of the geodataframe + print(f"read_basin_geometry: gdf.head: \n{gdf.columns}\n{gdf.head()}") + + # Print unique values of gauges_COUNTRY + print(f"read_basin_geometry: unique values of gauges_COUNTRY: {gdf['gauges_COUNTRY'].unique()}") + + # Drop basins of the country of Kyrgystan or don't have a country reference + gdf = gdf[gdf['gauges_COUNTRY'] != 'KYG'] + gdf = gdf[gdf['gauges_COUNTRY'] != 'None'] + + # Further drop selected basins like 16936 + gdf = gdf[gdf['CODE'] != '16936'] + + # Also drop the river with name "Kulduk" + gdf = gdf[gdf['gauges_RIVER'] != 'Kulduk'] + + # And drop the rivers with name "Karadarya" and "Chatkal" + gdf = gdf[gdf['gauges_RIVER'] != 'Karadarya'] + + # Now we drop transboundary rivers in the south of the Ferghana valley + #gdf = gdf[gdf['gauges_RIVER'] != 'Isfara'] + gdf = gdf[gdf['CODE'] != '16175'] # River Koksu + + # Drop Kyzylsu West river + gdf = gdf[gdf['gauges_RIVER'] != 'Kyzylsu West'] + # Convert Multipolygons to Polygons, keeping the largest polygon gdf['geometry'] = gdf['geometry'].apply( lambda x: max(x.geoms, key=lambda a: a.area) if x.geom_type == 'MultiPolygon' else x) @@ -88,35 +114,48 @@ def read_basin_geometry(filepath): # Replace gauges_RIVER=='None' with 'Name unknown' gdf['gauges_RIVER'] = gdf['gauges_RIVER'].replace('None', '') - # Create a column with labels for the basins - gdf['label'] = gdf['CODE'] + ' - ' + gdf['gauges_RIVER'] + # Create a display_name column with river name and basin area + gdf['display_name'] = gdf.apply( + lambda row: f"{row['gauges_RIVER']} ({int(row['area_km2'])} km²)" + if pd.notna(row['area_km2']) + else row['gauges_RIVER'], + axis=1 + ) + + # Create a column with labels for the basins (used for map hover) + gdf['label'] = gdf['display_name'] # Sort rows by area_km2 in descending order (plot smallest last) gdf = gdf.sort_values('area_km2', ascending=False) - # Read subbasins snow situation file - # Currently, not operational. The file shows snow situation data from end of - # March 2024. - df = read_snow_situation_file('data/subbasins_merged_data.csv') - - # Merge columns 'swe_threshold' and 'hs_threshold' from df into gdf by 'CODE' - gdf = gdf.merge(df[['basin_id', 'swe_threshold', 'hs_threshold']], - left_on='CODE', right_on='basin_id', how='left') - - # Read the regional snow situation file - df_regional = read_snow_situation_file('data/regions_merged_data.csv') - # Rename the swe and hs threshold columns to avoid conflicts - df_regional = df_regional.rename(columns={'swe_threshold': 'swe_threshold_regional', - 'hs_threshold': 'hs_threshold_regional'}) - # Merge columns 'swe_threshold_regional' and 'hs_threshold_regional' from - # df_regional into gdf by 'REGION'. - # We need to do a fuzzy merge because the 'REGION' column in gdf contains - # the full name of the region, while the 'REGION' column in df_regional - # contains the abbreviation of the region. - # Split the values in 'REGION' in gdf by '_' and take the first part - gdf['REG'] = gdf['REGION'].str.split('_').str[0] - gdf = gdf.merge(df_regional[['basin_id', 'swe_threshold_regional', 'hs_threshold_regional']], - left_on='REG', right_on='basin_id', how='left') + + # Only merge subbasins_merged_data.csv if it exists and contains current year data + subbasins_file = 'data/subbasins_merged_data.csv' + if os.path.exists(subbasins_file): + try: + df = read_snow_situation_file(subbasins_file) + # Check if current year is present in the data + current_year = dt.datetime.now().year + if (df['date'].dt.year == current_year).any(): + gdf = gdf.merge(df[['basin_id', 'swe_threshold', 'hs_threshold']], + left_on='CODE', right_on='basin_id', how='left') + except Exception as e: + print(f"Warning: Could not merge {subbasins_file}: {e}") + + # Only merge regions_merged_data.csv if it exists and contains current year data + regions_file = 'data/regions_merged_data.csv' + if os.path.exists(regions_file): + try: + df_regional = read_snow_situation_file(regions_file) + current_year = dt.datetime.now().year + if (df_regional['date'].dt.year == current_year).any(): + df_regional = df_regional.rename(columns={'swe_threshold': 'swe_threshold_regional', + 'hs_threshold': 'hs_threshold_regional'}) + gdf['REG'] = gdf['REGION'].str.split('_').str[0] + gdf = gdf.merge(df_regional[['basin_id', 'swe_threshold_regional', 'hs_threshold_regional']], + left_on='REG', right_on='basin_id', how='left') + except Exception as e: + print(f"Warning: Could not merge {regions_file}: {e}") print(gdf.head()) @@ -131,10 +170,10 @@ def get_basin_selector_names_in_list(gdf): gdf (geodataframe) basin geometry Return: - list of basin codes and basin names + list of display names (river names with basin area, no CODEs) """ - gdf_for_names = gdf.sort_values(['REGION', 'CODE'], ascending=[True, True]) - options_list = list(gdf_for_names['label'].values) + gdf_for_names = gdf.sort_values(['REGION', 'gauges_RIVER'], ascending=[True, True]) + options_list = list(gdf_for_names['display_name'].values) return options_list def get_region_selector_names_in_list(gdf): @@ -160,13 +199,13 @@ def update_gdf_with_selected_basin(selected_basin, view_option): Arguments: gdf (geodataframe): GeoDataFrame with basin geometries - selected_basin (list): list of the selected basins + selected_basin (list): list of the selected basins (display names for sub-basin, region names for regional) Return: geodataframe with updated selected column """ - #print("DEBUG: calling update_gdf_with_selected_basin with selected_basin: ", selected_basin) - #print("DEBUG: calling update_gdf_with_selected_basin with view_option: ", view_option) + print("DEBUG: calling update_gdf_with_selected_basin with selected_basin: ", selected_basin) + print("DEBUG: calling update_gdf_with_selected_basin with view_option: ", view_option) try: if selected_basin is not None and len(selected_basin) > 0: if view_option == 'Regional': @@ -177,8 +216,11 @@ def update_gdf_with_selected_basin(selected_basin, view_option): else: # Set all basins to False gdf['selected'] = False - # Set the selected basin to True only if it is not empty - gdf.loc[gdf['label'] == selected_basin[0], 'selected'] = True + # Convert display name to CODE for matching + basin_code = display_name_to_basin_code.get(selected_basin[0]) + if basin_code: + # Set the selected basin to True only if it is not empty + gdf.loc[gdf['CODE'] == basin_code, 'selected'] = True return gdf except Exception as e: print(f'Error in update_gdf_with_selected_basin: \n {e}') @@ -223,10 +265,10 @@ def get_subbasin_code_from_tap(x, y): # Get the polygon with the minimum distance clicked = gdf_projected.iloc[clicked_polygon_index] #output.object=output.object+f'\nclicked: {clicked}' - #print("DEBUG: clicked['label']: ", clicked['label']) - basin_code = clicked['label'] - #basin_name = clicked['BASIN'] - return basin_code + #print("DEBUG: clicked['display_name']: ", clicked['display_name']) + # Return the display name (river name with basin area) + display_name = clicked['display_name'] + return display_name return None def get_region_from_tap(x, y): @@ -275,7 +317,7 @@ def read_current_data_for_basin(basin_code): delimiter='\t') # Make sure the Date column is of type datetime dfcurrent['date'] = pd.to_datetime(dfcurrent['date']) - print(f"read_current_data_for_basin: dfcurrent.head: \n{dfcurrent.columns}\n{dfcurrent.tail()}") + #print(f"read_current_data_for_basin: dfcurrent.head: \n{dfcurrent.columns}\n{dfcurrent.head()}") return dfcurrent except Exception as e: return f'Error in read_current_data_for_basin: \n {e}' @@ -295,6 +337,7 @@ def read_previous_year_data_for_basin(basin_code): # Add the difference in years between the first date in dfcurrent and # the first date in dfprevious to the dates in dfprevious dfprevious['date'] = dfprevious['date'] + pd.DateOffset(years=dfcurrent['date'].dt.year.values[0] - dfprevious['date'].dt.year.values[0]) + print(f"read_previous_year_data_for_basin: dfprevious.head: \n{dfprevious.columns}\n{dfprevious.head()}") return dfprevious except Exception as e: return f'Error in read_previous_year_data_for_basin: \n {e}' @@ -315,6 +358,7 @@ def read_climate_data_for_basin(basin_code): # Add the difference in years between the first date in dfcurrent and # the first date in dfclimate to the dates in dfclimate dfclimate['date'] = dfclimate['date'] + pd.DateOffset(years=dfcurrent['date'].dt.year.values[0] - dfclimate['date'].dt.year.values[0]) + print(f"read_climate_data_for_basin: dfclimate.head: \n{dfclimate.columns}\n{dfclimate.head()}") return dfclimate except Exception as e: return f'Error in read_climate_data_for_basin: \n {e}' @@ -330,9 +374,13 @@ def read_climate_data_for_basin(basin_code): basins_list = get_basin_selector_names_in_list(gdf) regions_list = get_region_selector_names_in_list(gdf) +# Create mapping dictionaries for CODE <-> display name conversion +basin_code_to_display_name = dict(zip(gdf['CODE'], gdf['display_name'])) +display_name_to_basin_code = dict(zip(gdf['display_name'], gdf['CODE'])) + # Create a StaticText widget -basin_code_widget = pn.widgets.StaticText(name='Basin Code', value='') -output = pn.pane.Str("Default message. Prints: Basin code and name upon click on a basin.") +basin_code_widget = pn.widgets.StaticText(name='Basin name', value='') +output = pn.pane.Str("Default message. Prints: Basin name upon click on a basin.") # Toggle variable in a widget variable_options = pn.widgets.RadioButtonGroup( @@ -491,9 +539,8 @@ def update_basin_selection_widget_with_region_selection(view_option): basin_selection.param.value) def plot_subbasin_data(variable, basin): try: - # Read the basin code - #basin_code = get_subbasin_code(x, y) - basin_code = basin[0].split(' - ')[0] + # Read the basin code from display name + basin_code = display_name_to_basin_code.get(basin[0]) # Read the current data for the basin dfcurrent = read_current_data_for_basin(basin_code) # Get forecast data for the basin @@ -529,7 +576,7 @@ def plot_subbasin_data(variable, basin): dfforecast, vdims=['Q50_SWE'], label='Forecast', kdims=['date']).opts(line_dash='dashed', color=current_year_color, tools=['hover']) - title_str = f'SWE situation for basin of river {river_name} (gauge {basin_code})' + title_str = f'SWE situation for basin of river {river_name}' ylabel_str = 'SWE (mm)' elif variable == 'HS': # Create an empty hv.curve object @@ -559,7 +606,7 @@ def plot_subbasin_data(variable, basin): dfforecast, vdims=['Q50_HS'], label='Forecast', kdims=['date']).opts(line_dash='dashed', color=current_year_color, tools=['hover']) - title_str = f'HS situation for basin of river {river_name} (gauge {basin_code})' + title_str = f'HS situation for basin of river {river_name}' ylabel_str = 'HS (m)' else: area_climate = hv.Area( @@ -582,7 +629,7 @@ def plot_subbasin_data(variable, basin): dfforecast, vdims=['Q50_ROF'], label='Forecast', kdims=['date']).opts(line_dash='dashed', color=current_year_color, tools=['hover']) - title_str = f'Melt situation for basin of river {river_name} (gauge {basin_code})' + title_str = f'Melt situation for basin of river {river_name}' ylabel_str = 'SM (mm)' fig = (area_climate * curve_previous * curve_current * curve_forecast)\ .opts( @@ -611,14 +658,17 @@ def plot_region_data(variable, basin): basin_code = basin[0].split(' - ')[0] # Read the current data for the basin dfcurrent = read_current_data_for_basin(basin_code) + print(f"current data for basin {basin_code}: dfcurrent.head: \n{dfcurrent.columns}\n{dfcurrent.head()}") # Get forecast data for the basin dfforecast = dfcurrent[dfcurrent['FC'] == True] dfcurrent = dfcurrent[dfcurrent['FC'] == False] #output.object=f'\n\n{dfcurrent.head()}' # Read previous year data for the basin dfprevious = read_previous_year_data_for_basin(basin_code) + print(f"previous year data for basin {basin_code}: dfprevious.head: \n{dfprevious.columns}\n{dfprevious.head()}") # Read the climate data for the basin dfclimate = read_climate_data_for_basin(basin_code) + print(f"climate data for basin {basin_code}: dfclimate.head: \n{dfclimate.columns}\n{dfclimate.head()}") #output.object=output.object+f'\n\n{dfclimate.head()}' # Adapt the name of the river basin for the title if basin_code == 'AMU_DARYA': @@ -790,7 +840,7 @@ def get_snow_plot(value, variable, basin): variable_options, pn.pane.Markdown("Select granularity of view:\nRegional view: Show snow development in a regional basin.\nSub-basin view: Show snow development in a sub-basin."), view_options, - pn.pane.Markdown("Search for sub-basin by hydropost code:"), + pn.pane.Markdown("Search for sub-basin by river name:"), basin_selection, pn.pane.Markdown(""), refs],