Skip to content

Commit d87c0f3

Browse files
committed
First attempt at adding a download data option
1 parent 30195bd commit d87c0f3

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

app.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ def dot_row(color, text):
239239

240240
app.layout = html.Div([
241241
dcc.Store(id="refresh-store", data=0),
242+
dcc.Download(id="download-ts"),
243+
dcc.Download(id="download-forecast"),
242244
html.Div([
243245
html.Span("Environmental Monitor", style={"fontSize":"15px","fontWeight":"500","color":"rgba(26, 26, 24, 1)"}),
244246
html.Div([
@@ -262,6 +264,65 @@ def dot_row(color, text):
262264
html.Div([html.Div("Pipeline status", style=SEC), html.Div(id="status-panel")]),
263265
html.Div([html.Div("Model metrics", style=SEC), html.Div(id="metrics-panel")]),
264266
html.Div([html.Div("Index statistics", style=SEC), html.Div(id="stats-panel")]),
267+
268+
# ── Download ──────────────────────────────────────────────────────
269+
html.Div([
270+
html.Div("Download data", style=SEC),
271+
html.Button("⬇ Time series (CSV)", id="btn-dl-ts",
272+
n_clicks=0, style={
273+
"width":"100%","marginBottom":"6px","padding":"7px 10px",
274+
"fontSize":"12px","borderRadius":"6px","cursor":"pointer",
275+
"border":"0.5px solid rgba(232,231,226,1)",
276+
"background":"rgba(247,246,242,1)","color":"rgba(26,26,24,1)",
277+
"textAlign":"left",
278+
}),
279+
html.Button("⬇ Forecast (CSV)", id="btn-dl-forecast",
280+
n_clicks=0, style={
281+
"width":"100%","padding":"7px 10px",
282+
"fontSize":"12px","borderRadius":"6px","cursor":"pointer",
283+
"border":"0.5px solid rgba(232,231,226,1)",
284+
"background":"rgba(247,246,242,1)","color":"rgba(26,26,24,1)",
285+
"textAlign":"left",
286+
}),
287+
]),
288+
289+
# ── About / credits ───────────────────────────────────────────────
290+
html.Div(style={"marginTop":"auto","paddingTop":"16px",
291+
"borderTop":"0.5px solid rgba(232,231,226,1)"}, children=[
292+
html.Div("About", style=SEC),
293+
html.Div([
294+
html.Span("Data: ", style={"color":"rgba(136,136,136,1)","fontSize":"12px"}),
295+
html.A("Sentinel-2 via AWS Earth Search",
296+
href="https://earth-search.aws.element84.com/v1",
297+
target="_blank",
298+
style={"fontSize":"12px","color":"rgba(55,138,221,1)"}),
299+
], style={"marginBottom":"5px"}),
300+
html.Div([
301+
html.Span("Data: ", style={"color":"rgba(136,136,136,1)","fontSize":"12px"}),
302+
html.A("HLS via Microsoft Planetary Computer",
303+
href="https://planetarycomputer.microsoft.com",
304+
target="_blank",
305+
style={"fontSize":"12px","color":"rgba(55,138,221,1)"}),
306+
], style={"marginBottom":"10px"}),
307+
html.Div([
308+
html.A("⌥ GitHub repo",
309+
href="https://github.com/astroAycha/geospatial_mlops",
310+
target="_blank",
311+
style={"fontSize":"12px","color":"rgba(55,138,221,1)",
312+
"display":"block","marginBottom":"5px"}),
313+
html.A("◎ aychatammour.com",
314+
href="https://aychatammour.com",
315+
target="_blank",
316+
style={"fontSize":"12px","color":"rgba(55,138,221,1)",
317+
"display":"block","marginBottom":"5px"}),
318+
html.Span("Questions? ",
319+
style={"fontSize":"12px","color":"rgba(136,136,136,1)"}),
320+
html.A("Get in touch",
321+
href="mailto:contact@aychatammour.com",
322+
style={"fontSize":"12px","color":"rgba(55,138,221,1)"}),
323+
]),
324+
]),
325+
265326
], style=SIDEBAR),
266327

267328
html.Div([
@@ -461,5 +522,58 @@ def _cb(aoi_value, _r=None, _key=key):
461522
make_chart_callback(_key, _id)
462523

463524

525+
526+
# ── Download callbacks ─────────────────────────────────────────────────────────
527+
528+
@callback(
529+
Output("download-ts", "data"),
530+
Input("btn-dl-ts", "n_clicks"),
531+
Input("aoi-dropdown", "value"),
532+
prevent_initial_call=True,
533+
)
534+
def download_ts(n_clicks, aoi_value):
535+
"""Download full time series as CSV."""
536+
from dash import ctx
537+
if ctx.triggered_id != "btn-dl-ts" or not aoi_value:
538+
return dash.no_update
539+
parsed = parse_sel(aoi_value)
540+
if not parsed:
541+
return dash.no_update
542+
country, aoi_name = parsed
543+
ts_df = read_ts(country, aoi_name)
544+
if ts_df.empty:
545+
return dash.no_update
546+
filename = f"{aoi_name}_time_series.csv"
547+
return dcc.send_data_frame(ts_df.to_csv, filename, index=False)
548+
549+
550+
@callback(
551+
Output("download-forecast", "data"),
552+
Input("btn-dl-forecast", "n_clicks"),
553+
Input("aoi-dropdown", "value"),
554+
prevent_initial_call=True,
555+
)
556+
def download_forecast(n_clicks, aoi_value):
557+
"""Download latest forecast as CSV."""
558+
from dash import ctx
559+
if ctx.triggered_id != "btn-dl-forecast" or not aoi_value:
560+
return dash.no_update
561+
parsed = parse_sel(aoi_value)
562+
if not parsed:
563+
return dash.no_update
564+
country, aoi_name = parsed
565+
fc_df = read_forecasts(country, aoi_name)
566+
if fc_df.empty:
567+
return dash.no_update
568+
# Rename columns to be more user-friendly
569+
fc_df = fc_df.rename(columns={
570+
"unique_id": "index",
571+
"ds": "date",
572+
"XGBRegressor": "forecast_value",
573+
})
574+
filename = f"{aoi_name}_forecast.csv"
575+
return dcc.send_data_frame(fc_df.to_csv, filename, index=False)
576+
577+
464578
if __name__ == "__main__":
465579
app.run(host="0.0.0.0", port=7860, debug=False)

0 commit comments

Comments
 (0)