Spaces:
Sleeping
Sleeping
| # pandas plus plotly | |
| import pandas as pd | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| # Dash | |
| from dash import Dash, dcc, html, Input, Output, State, no_update | |
| import numpy as np | |
| import dash_bootstrap_components as dbc | |
| # load and prep the data | |
| raw_global = pd.read_csv("merged_global.csv") | |
| raw_global["Date"] = pd.to_datetime(raw_global[["year", "month"]].assign(day=15)) | |
| raw_hemi = pd.read_csv("hemispheric_merged.csv") | |
| raw_hemi["Date"] = pd.to_datetime(raw_hemi[["year", "month"]].assign(day=15)) | |
| # getting the seasons according to the month they belong | |
| def tag_season(month): | |
| return {12: "DJF", 1: "DJF", 2: "DJF", | |
| 3: "MAM", 4: "MAM", 5: "MAM", | |
| 6: "JJA", 7: "JJA", 8: "JJA", | |
| 9: "SON", 10: "SON", 11: "SON"}[month] | |
| raw_global["Season"] = raw_global["month"].apply(tag_season) | |
| # Donw a bit of aggregation for seasonal views ( useful for later) | |
| seasonal_avg = ( | |
| raw_global.groupby(["year", "Season"]) | |
| .mean(numeric_only=True) | |
| .reset_index() | |
| .assign(Season_Order=lambda d: d["Season"].map({"DJF": 0, "MAM": 1, "JJA": 2, "SON": 3})) | |
| .sort_values(["year", "Season_Order"]) | |
| ) | |
| # intializing the dash app now | |
| app = Dash(__name__, external_stylesheets=[dbc.themes.FLATLY], suppress_callback_exceptions=True) | |
| app.title = "Climate Dashboard" | |
| # deciding heading and structure of the top level layout of the dashboard | |
| app.layout = dbc.Container([ | |
| dbc.Row([ | |
| dbc.Col(html.H1("Climate Dashboard", className="text-center my-3"), width=12) | |
| ]), | |
| dbc.Row([ | |
| dbc.Col([ | |
| html.Label("Mode:"), | |
| dcc.RadioItems( | |
| id='theme-toggle',options=["Light", "Dark"],value="Light",labelStyle={'display': 'inline-block', 'margin-right': '10px'}) | |
| ], width=12, className="text-center mb-3") | |
| ]), | |
| # Main tabbed content | |
| dbc.Row([ | |
| dbc.Col([ | |
| dcc.Tabs(id="tabs", value='global', children=[ | |
| dcc.Tab(label='🌍 Global Trends', value='global'), | |
| dcc.Tab(label='🌍 NH vs SH Comparison', value='hemispheres') | |
| ]), | |
| html.Div(id='tabs-content') | |
| ], width=12) | |
| ]), | |
| # for downloading the csv | |
| dcc.Download(id="download-csv") | |
| ], fluid=True, style={"minWidth": "1100px", "height": "100vh", "padding": "10px"}) | |
| # to switch the tabs here it is logic | |
| def switch_tab(tab): | |
| # note: global is the default tab | |
| if tab == 'global': | |
| return dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader("🔧 Indicator & View Settings"), | |
| dbc.CardBody([ | |
| html.Label("Select Indicators:"), | |
| dcc.Checklist(id='global-checklist', options=[ | |
| {'label': 'CO₂ Anomaly (Mt)', 'value': 'norm_co2'}, | |
| {'label': 'Land-Ocean Temp Anomaly (°C)', 'value': 'norm_land_ocean_temp'}, | |
| {'label': 'Land Temp Anomaly (°C)', 'value': 'norm_land_temp'}, | |
| {'label': 'Sea Level (mm)', 'value': 'norm_sea_level'}, | |
| ], value=['norm_co2', 'norm_land_ocean_temp'], labelStyle={'display': 'block'}), | |
| html.Br(), | |
| html.Label("Data View:"), | |
| dcc.RadioItems(id='view-mode', options=["Monthly", "Seasonal"], value="Monthly"), | |
| html.Br(), | |
| html.Button("⬇ Download CSV", id="export-csv", className="btn btn-primary") | |
| ]) | |
| ]) | |
| ], width=3), | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader("📊 What You’re Seeing"), | |
| dbc.CardBody(id="explanation-box", style={ | |
| "backgroundColor":"#eef2f3", | |
| "borderRadius": "6px", | |
| "fontSize": "15px" | |
| }) | |
| ]), | |
| dcc.Graph(id='global-graph'), | |
| dcc.RangeSlider( | |
| id='global-slider', | |
| min=raw_global['year'].min(), max=raw_global['year'].max(), | |
| value=[1993, raw_global['year'].max()], | |
| marks={str(y): str(y) for y in range(raw_global['year'].min(), raw_global['year'].max()+1, 5)}, | |
| step=1 | |
| ), | |
| html.Div(id="summary-panel", style={ | |
| "backgroundColor": "#f9f9f9", "padding": "10px", "marginTop": "10px", | |
| "borderRadius": "10px", "boxShadow": "0 2px 5px rgba(0,0,0,0.1)" | |
| }), | |
| html.P( | |
| "Tooltips show normalized trends; hover to see original values. " | |
| "It demonstrates that the higher CO₂ often leads to increased temp & sea level rise.", | |
| style={"fontSize": "12px", "color": "gray", "marginTop": "10px"} | |
| ) | |
| ], width=9) | |
| ]) | |
| # Show hemisphere tab content | |
| elif tab == 'hemispheres': | |
| return dbc.Row([ | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader("🌎 Hemisphere & Indicators"), | |
| dbc.CardBody([ | |
| html.Label("Select Hemisphere:"), | |
| dcc.Dropdown( | |
| id='hemi-hemi-dropdown', | |
| options=[ | |
| {'label': 'Northern Hemisphere', 'value': 'north'}, | |
| {'label': 'Southern Hemisphere', 'value': 'south'} | |
| ], | |
| value='north' | |
| ), | |
| html.Br(), | |
| html.Label("Select Indicators to Animate:"), | |
| dcc.Checklist(id='hemi-checklist', options=[], value=[]) | |
| ]) | |
| ]) | |
| ], width=3), | |
| dbc.Col([ | |
| dbc.Card([ | |
| dbc.CardHeader(" What You’re Seeing – Hemisphere Comparison"), | |
| dbc.CardBody([ | |
| html.P("Use this section to analyze climate indicators separately for the Northern or Southern Hemisphere."), | |
| html.P("🌍 First, select a hemisphere using the dropdown above. Then, choose which indicators to animate."), | |
| html.P("📈 Values are normalized (0–1) for comparability across different units."), | |
| html.P("▶ The animated chart shows how each indicator evolves over time, year by year."), | |
| html.P("This helps reveal differences and trends between hemispheres.") | |
| ]) | |
| ]), | |
| dcc.Graph(id='hemi-animation') | |
| ], width=9) | |
| ]) | |
| def refresh_hemi_checklist(hemi): | |
| if hemi is None: | |
| return [], [] | |
| prefix = f"norm_{hemi}_" | |
| options_map = { | |
| f"{prefix}co2": "CO₂ Anomaly", | |
| f"{prefix}land": "Land Temp Anomaly", | |
| f"{prefix}land_ocean": "Land-Ocean Temp Anomaly", | |
| f"norm_msl_{hemi}": "Sea Level Anomaly" | |
| } | |
| options = [{"label": label, "value": key} for key, label in options_map.items()] | |
| default_values = [opt["value"] for opt in options] | |
| return options, default_values | |
| # Updates global graph explanation box and summary panel in real time | |
| def update_global(selected, year_range, mode, theme): | |
| # Raw value mapping for users to see when they hover tooltips | |
| raw_mapping = { | |
| 'norm_co2': 'co2_anomaly', | |
| 'norm_land_ocean_temp': 'land_ocean_anomaly', | |
| 'norm_land_temp': 'land_anomaly', | |
| 'norm_sea_level': 'msl_mm' | |
| } | |
| # to get the correct time window i use filtering | |
| dff = raw_global[(raw_global["year"] >= year_range[0]) & (raw_global["year"] <= year_range[1])] | |
| dff_season = seasonal_avg[(seasonal_avg["year"] >= year_range[0]) & (seasonal_avg["year"] <= year_range[1])] | |
| use_df = dff.copy() if mode == "Monthly" else dff_season.copy() | |
| # time axis for either the month view mode selected or the seasonal one | |
| use_df["X"] = use_df["Date"] if mode == "Monthly" else use_df["Season"] + " " + use_df["year"].astype(str) | |
| # setting up hover tooltips with raw values | |
| hover_data = {} | |
| for col in selected: | |
| if col in raw_mapping and raw_mapping[col] in use_df.columns: | |
| hover_data[raw_mapping[col]] = True | |
| # Final filtered DataFrame for plotting | |
| plot_df = use_df[["X", "year"] + selected + [raw_mapping[c] for c in selected if c in raw_mapping]].dropna() | |
| # Create the figure | |
| fig = px.line( | |
| plot_df, | |
| x="X", | |
| y=selected, | |
| labels={"X": "Year","value": "Normalized Value (0–1)", "variable": "Indicator"}, | |
| color_discrete_sequence=['#0072B2', '#D55E00', '#009E73', '#CC79A7'], | |
| title=f"Climate Trends Over Time ({mode})", | |
| hover_data=hover_data | |
| ) | |
| raw_global['Season_Order'] = raw_global['Season'].map({'DJF': 0, 'MAM': 1, 'JJA': 2, 'SON': 3}) | |
| raw_global['X'] = raw_global['Season'] + ' ' + raw_global['year'].astype(str) | |
| # Annotating global climate events in both Monthly and Seasonal modes | |
| policy_events = { | |
| 1997: {"label": "Kyoto Protocol", "month": 12}, | |
| 2015: {"label": "Paris Agreement", "month": 12}, | |
| 2020: {"label": "COVID Drop", "month": 4} | |
| } | |
| for year, event in policy_events.items(): | |
| label = event["label"] | |
| month = event["month"] | |
| season = tag_season(month) | |
| season_label = f"{season} {year}" | |
| if mode == "Monthly": | |
| # Add annotation only if year is in selected range | |
| if year_range[0] <= year <= year_range[1]: | |
| date = pd.to_datetime(f"{year}-{month:02d}-01") | |
| fig.add_vline(x=date, line_dash="dash", line_color="gray") | |
| fig.add_annotation( | |
| x=date, | |
| y=1.05, | |
| yref="paper", | |
| text=label, | |
| showarrow=True, | |
| ax=0, | |
| ay=-30, | |
| font=dict(size=10) | |
| ) | |
| else: # Seasonal mode | |
| if season_label in plot_df["X"].values: | |
| fig.add_vline(x=season_label, line_dash="dash", line_color="gray") | |
| fig.add_annotation( | |
| x=season_label, | |
| y=1.05, | |
| yref="paper", | |
| text=label, | |
| showarrow=True, | |
| ax=0, | |
| ay=-30, | |
| font=dict(size=10) | |
| ) | |
| # Explanation box to make it simpler to undertsand the data for users | |
| explanation = html.Div([ | |
| html.P("🌍 This dashboard compares key climate indicators that reflect human impact on the Earth’s climate system."), | |
| html.Ul([ | |
| html.Li(["🌱",html.B("CO₂ Emissions"),": Carbon dioxide ( e.g., from fossil fuels) traps heat in the atmosphere. More CO₂ → more warming."]), | |
| html.Li(["🌡️",html.B("Temperature Anomalies"),": How much temperatures deviate from historical normals, indicating warming trends."]), | |
| html.Li(["🌊",html.B("Sea Level Changes"),": Driven by ice melt and thermal expansion of seawater as it warms."]) | |
| ]), | |
| html.P("You are viewing normalized values (range from 0 to 1), which means each climate indicator—CO₂ emissions (Mt), temperature anomalies (°C), and sea level (mm)—has been scaled to the same range for easy comparison."), | |
| html.P("➤ This allows you to directly compare trends, even though the original units are very different."), | |
| html.P("➤ Hover over the lines to see the actual (raw) values for each year."), | |
| html.P("➤ The graph highlights how these indicators have changed over time, focuses on the last 30 years."), | |
| html.P([ | |
| "📍 Three key global events are marked on the chart:", | |
| html.Ul([ | |
| html.Li([ | |
| html.B("Kyoto Protocol (Dec 1997): "), | |
| "An international treaty committing industrialized countries to reduce emissions. You may notice a delayed but visible slowing in emission growth after this point." | |
| ]), | |
| html.Li([ | |
| html.B("Paris Agreement (Dec 2015): "), | |
| "A global framework to limit warming to below 2°C. Although targets were set, the data shows only minor reductions or stabilization in trends after this agreement." | |
| ]), | |
| html.Li([ | |
| html.B("COVID Drop (Spring 2020): "), | |
| "Global lockdowns may have led to temporary emissions drops and some irregularities in sea level due to the economic slowdown. These appear as short-term dips in the data." | |
| ]), | |
| ]), | |
| "These annotations help you connect major global actions or disruptions to changes in the data. Look for temporary changes or delayed responses in the trendlines near these years." | |
| ]), | |
| html.P("➤ You can use the legend side of the chart to show or hide each indicator.") | |
| ]) | |
| # short correlation summary between the indciators (normalized Pearson r) | |
| corr_texts = [] | |
| norm_corr_df = plot_df[["X"] + selected].copy() | |
| for col in selected: | |
| norm_corr_df[col] = (norm_corr_df[col] - norm_corr_df[col].mean()) / norm_corr_df[col].std() | |
| for i in range(len(selected)): | |
| for j in range(i + 1, len(selected)): | |
| a, b = selected[i], selected[j] | |
| try: | |
| r = np.corrcoef(norm_corr_df[a], norm_corr_df[b])[0, 1] | |
| strength = "strong" if abs(r) > 0.7 else "moderate" if abs(r) > 0.4 else "weak" | |
| direction = "positive" if r > 0 else "negative" | |
| corr_texts.append(f"• {a} & {b}: r = {r:.2f} ({strength}, {direction} correlation)") | |
| except Exception: | |
| corr_texts.append(f"• {a} & {b}: correlation unavailable") | |
| return fig, explanation, html.Ul([html.Li(text) for text in corr_texts]) | |
| # to download the csv file to see the preprocessed dataset | |
| def export_csv(n_clicks, year_range): | |
| if not n_clicks: | |
| return no_update # No button click, no download | |
| #filter the global data by year range | |
| try: | |
| export_df = raw_global[(raw_global['year'] >= year_range[0]) & (raw_global['year'] <= year_range[1])] | |
| return dcc.send_data_frame(export_df.to_csv, filename="filtered_climate_data.csv") | |
| except Exception as e: | |
| # incase failing to export the csv | |
| print(f"Export failed: {e}") | |
| return no_update | |
| # hemisphere animated graph callback to animaate over time | |
| def update_hemi_graph( selected_inds, theme , hemi): | |
| if not selected_inds: | |
| return go.Figure().update_layout( | |
| title="Please select at least one indicator to display.", | |
| template="plotly_dark" if theme == "Dark" else "plotly_white" | |
| ) | |
| raw_hemi = pd.read_csv("hemispheric_merged.csv") | |
| #take mean of monthly data | |
| hemi_df_grouped = raw_hemi.groupby([ "year"]).mean(numeric_only=True).reset_index() | |
| # Defining variable name for legend and tooltip | |
| label_map = { | |
| "norm_north_co2": "CO₂ Anomaly (NH)", | |
| "norm_north_land": "Land Temp (NH)", | |
| "norm_north_land_ocean": "Land-Ocean Temp (NH)", | |
| "norm_msl_north": "Sea Level (NH)", | |
| "norm_south_co2": "CO₂ Anomaly (SH)", | |
| "norm_south_land": "Land Temp (SH)", | |
| "norm_south_land_ocean": "Land-Ocean Temp (SH)", | |
| "norm_msl_south": "Sea Level (SH)" | |
| } | |
| color_map = { | |
| "norm_north_co2": "#0072B2", "norm_north_land": "#D55E00", "norm_north_land_ocean": "#009E73", "norm_msl_north": "#CC79A7", | |
| "norm_south_co2": "#0072B2", "norm_south_land": "#D55E00", "norm_south_land_ocean": "#009E73", "norm_msl_south": "#CC79A7" | |
| } | |
| fig = go.Figure() | |
| # Add lines for firdy years data for initial display | |
| initial_year = hemi_df_grouped["year"].min() | |
| for ind in selected_inds: | |
| subset = hemi_df_grouped[hemi_df_grouped["year"] <= initial_year] | |
| fig.add_trace(go.Scatter( | |
| x=subset["year"], | |
| y=subset[ind], | |
| mode="lines+markers", | |
| name=label_map.get(ind, ind), | |
| line=dict(color=color_map.get(ind, "#444")), | |
| hovertemplate="%{y:.2f} (normalized)<br>Year: %{x}" | |
| )) | |
| # Create animation frames progressively for each year | |
| frames = [] | |
| for year in hemi_df_grouped["year"]: | |
| data = [] | |
| for ind in selected_inds: | |
| subset = hemi_df_grouped[hemi_df_grouped["year"] <= year] | |
| data.append(go.Scatter( | |
| x=subset["year"], | |
| y=subset[ind], | |
| mode="lines+markers", | |
| name=label_map.get(ind, ind), | |
| line=dict(color=color_map.get(ind, "#444")) | |
| )) | |
| frames.append(go.Frame(data=data, name=str(year))) | |
| fig.frames = frames | |
| # Layout of the play pause button andd slider | |
| fig.update_layout( | |
| title=f"{hemi.upper()} Hemisphere – Normalized Indicators Over Time", | |
| xaxis_title="Year", | |
| yaxis_title="Normalized Value (0–1)", | |
| template="plotly_dark" if theme == "Dark" else "plotly_white", | |
| hovermode="x unified", | |
| updatemenus=[{ | |
| "buttons": [ | |
| {"args": [None, {"frame": {"duration": 500, "redraw": True}, "fromcurrent": True}], | |
| "label": "▶ Play", "method": "animate"}, | |
| {"args": [[None], {"frame": {"duration": 0}, "mode": "immediate"}], | |
| "label": "⏸ Pause", "method": "animate"} | |
| ], | |
| "type": "buttons", | |
| "x": 0.8, "xanchor": "left", | |
| "y":1.15, "yanchor": "top", | |
| }], | |
| sliders=[{ | |
| "steps": [ | |
| {"args": [[str(year)], {"frame": {"duration": 0, "redraw": True}, "mode": "immediate"}], | |
| "label": str(year), "method": "animate"} for year in hemi_df_grouped["year"] | |
| ], | |
| "x": 0.05, "len": 0.9, | |
| "xanchor": "left", "y": -0.2, "yanchor": "bottom" | |
| }] | |
| ) | |
| return fig | |
| # to be deployed adding the server port accordingly docker | |
| server = app.server | |
| if __name__ == '__main__': | |
| app.run_server(debug=True, host='0.0.0.0', port=7860) | |