CarlyneB commited on
Commit
3182ce2
·
1 Parent(s): a58aedd

[FEAT] Objective 3: Export graph data individually and in full + Added data on the amount of resources consumed and remaining

Browse files
config/config.py CHANGED
@@ -1,4 +1,5 @@
1
  import pandas as pd
 
2
 
3
  CO2_UNIT = "MtCO₂"
4
 
@@ -117,4 +118,37 @@ method_tooltips = {
117
  "Moisture swing DACCS": "Direct air carbon capture and storage using moisture swing methods.",
118
  "pH swing": "Direct ocean carbon capture and storage using pH swing methods.",
119
  "Mineralization ex-situ": "Ex-situ mineralization methods for carbon capture."
120
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import pandas as pd
2
+ import plotly.express as px
3
 
4
  CO2_UNIT = "MtCO₂"
5
 
 
118
  "Moisture swing DACCS": "Direct air carbon capture and storage using moisture swing methods.",
119
  "pH swing": "Direct ocean carbon capture and storage using pH swing methods.",
120
  "Mineralization ex-situ": "Ex-situ mineralization methods for carbon capture."
121
+ }
122
+
123
+ CRRA_COLORS = {
124
+ "off_white": "#eae9d8",
125
+ "red": "#ff5f4d",
126
+ "green_dark": "#298231",
127
+ "green": "#55c95b",
128
+ "green_light": "#92ef9b",
129
+ "yellow": "#d9e210",
130
+ "white": "#ffffff",
131
+ "brown_grey": "#665d5b",
132
+ "grey": "#a0a0a0",
133
+ "black": "#000000",
134
+ }
135
+
136
+ CRRA_FONT = "Segoe UI, sans-serif"
137
+
138
+ PLOTLY_COLOR_SEQUENCE = [
139
+ CRRA_COLORS["red"],
140
+ CRRA_COLORS["green"],
141
+ CRRA_COLORS["yellow"],
142
+ CRRA_COLORS["green_dark"],
143
+ CRRA_COLORS["brown_grey"],
144
+ CRRA_COLORS["green_light"],
145
+ CRRA_COLORS["grey"],
146
+ ]
147
+
148
+ PLOTLY_HEATMAP_SCALE = [
149
+ [0.0, CRRA_COLORS["white"]],
150
+ [0.5, CRRA_COLORS["green_light"]],
151
+ [1.0, CRRA_COLORS["green_dark"]],
152
+ ]
153
+
154
+ PLOTLY_TEMPLATE = "plotly_white"
optimization/optimization.py CHANGED
@@ -101,7 +101,37 @@ def run_optimization(resource_caps, method_constraints, method_costs, custom_res
101
  x = result.x
102
  total_removed = x.sum()
103
  method_usage = {m: x[i] for i, m in enumerate(methods) if x[i] > 0}
104
- return True, {"total_removed": total_removed, "method_usage": method_usage}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  else:
106
  return False, {"message": result.message}
107
  except Exception as e:
 
101
  x = result.x
102
  total_removed = x.sum()
103
  method_usage = {m: x[i] for i, m in enumerate(methods) if x[i] > 0}
104
+ resource_usage = {}
105
+ for i, r in enumerate(resources):
106
+ resource_usage[r] = {
107
+ m: float(A_resource[i, j] * x[j])
108
+ for j, m in enumerate(methods) if A_resource[i, j] * x[j] > 0
109
+ }
110
+
111
+ resource_used_total = {r: sum(resource_usage[r].values()) for r in resources}
112
+ resource_remaining = {r: resource_caps[r] - resource_used_total[r] for r in resources}
113
+
114
+ # Potentiel théorique par méthode (si seule, contrainte ressources + son propre cap)
115
+ method_potential = {}
116
+ for j, m in enumerate(methods):
117
+ limits = []
118
+ for i, r in enumerate(resources):
119
+ coef = A_resource[i, j]
120
+ if coef > 0:
121
+ limits.append(resource_caps[r] / coef)
122
+ constraint = method_constraints.get(m, {})
123
+ if constraint.get("cap_type") == "absolute":
124
+ limits.append(constraint.get("cap_value", float("inf")))
125
+ method_potential[m] = float(min(limits)) if limits else 0.0
126
+
127
+ return True, {
128
+ "total_removed": total_removed,
129
+ "method_usage": method_usage,
130
+ "method_potential": method_potential,
131
+ "resource_usage": resource_usage,
132
+ "resource_used_total": resource_used_total,
133
+ "resource_remaining": resource_remaining,
134
+ }
135
  else:
136
  return False, {"message": result.message}
137
  except Exception as e:
tabs/tab4_optimization.py CHANGED
@@ -6,15 +6,13 @@ import unicodedata
6
  import zipfile
7
  import openpyxl
8
 
9
- import matplotlib.pyplot as plt
10
  import pandas as pd
11
- import seaborn as sns
12
  import streamlit as st
13
- import plotly.graph_objects as go
14
 
15
- from config.config import method_order, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT
16
  from optimization.optimization import run_optimization
17
- from utils.ui_helpers import format_table_summary, show_bar
18
  from utils.data_utils import is_valid_scenario, load_inputs_from_excel, strip_unit_suffix
19
 
20
 
@@ -62,7 +60,6 @@ def panel_load_from_excel():
62
  st.session_state.setdefault("input_file_name_suggestion", default_name)
63
  try:
64
  resource_caps, constraints, costs, custom_resources = load_inputs_from_excel(uploaded)
65
- print(custom_resources)
66
  st.session_state.resource_caps = resource_caps
67
  st.session_state.constraints = constraints
68
  st.session_state.costs = costs
@@ -140,7 +137,7 @@ def parse_zip_scenario(uploaded) -> dict | None:
140
  custom_resources = []
141
  if "custom_resources.csv" in names:
142
  df_custom = pd.read_csv(io.BytesIO(zf.read("custom_resources.csv")))
143
- required_custom = {"name", "group", "amount", "method", "min", "median", "max"}
144
  if required_custom.issubset(df_custom.columns):
145
  for bname in df_custom["name"].unique():
146
  df_b = df_custom[df_custom["name"] == bname]
@@ -241,7 +238,7 @@ def run_and_store_result():
241
 
242
  # Check that at least some resource is non-zero (standard OR custom)
243
  std_total = sum(st.session_state.resource_caps.values())
244
- print(custom_resources)
245
  custom_total = sum(float(b["amount"]) for b in custom_resources)
246
  if std_total <= 0 and custom_total <= 0:
247
  st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
@@ -274,6 +271,10 @@ def run_and_store_result():
274
  st.session_state.latest_result = {
275
  "summary": {"total_co2_removed": total_removed, "total_cost": total_cost},
276
  "allocations": allocations,
 
 
 
 
277
  "resource_caps": st.session_state.resource_caps.copy(),
278
  "method_constraints": st.session_state.constraints,
279
  "method_costs": st.session_state.costs,
@@ -292,28 +293,66 @@ def show_metrics(latest):
292
  cols[0].metric("Total CO₂ Removed", f"{latest['summary']['total_co2_removed']:,.2f} {CO2_UNIT}")
293
  cols[1].metric("Methods Used", len(latest["allocations"]))
294
 
 
 
 
 
 
295
  # Show which custom batches were included and how much of each was available
296
  if custom_resources:
297
  with st.expander(f"✅ {len(custom_resources)} new resource(s) included in this optimisation", expanded=False):
298
  rows = []
299
  for batch in custom_resources:
300
- unit = batch.get("group", "?")
 
301
  rows.append({
302
  "Batch name": batch["name"],
303
  "Group": batch["group"],
304
- "Amount available": f"{float(batch['amount']):,.2f}",
305
  "Methods linked": ", ".join(batch["methods"].keys())
306
  })
307
  st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
308
 
309
-
310
- def build_results_df(allocations, method_constraints=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  df = pd.DataFrame.from_dict(allocations, orient="index", columns=[f"{CO2_UNIT} removed"])
312
- print(df)
313
  df[f"{CO2_UNIT} removed"] = pd.to_numeric(df[f"{CO2_UNIT} removed"], errors="coerce")
314
  total = df[f"{CO2_UNIT} removed"].sum()
315
  if total > 0:
316
- df["Share (%)"] = (df[f"{CO2_UNIT} removed"] / total * 100).round(2)
 
 
 
 
 
 
 
 
 
317
 
318
  if method_constraints:
319
  caps = {}
@@ -330,18 +369,14 @@ def build_results_df(allocations, method_constraints=None):
330
 
331
 
332
  def show_donut(df_result):
333
- fig = go.Figure(data=[go.Pie(
334
- labels=df_result.index.tolist(),
335
- values=df_result[f"{CO2_UNIT} removed"].tolist(),
336
- hole=0.4,
337
- textinfo="label+percent",
338
- textposition="outside",
339
- marker=dict(line=dict(color="#000000", width=1)),
340
- showlegend=False,
341
- )])
342
- fig.update_layout(margin=dict(t=10, b=10))
343
  st.plotly_chart(fig, use_container_width=True)
344
-
345
 
346
  def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None):
347
  # Normalize standard method_costs keys (strip legacy unit suffixes)
@@ -379,20 +414,22 @@ def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None):
379
  matrix.append(row)
380
  y_labels.append(method)
381
 
382
- height = max(400, 60 + 30 * len(y_labels))
383
-
384
- fig = go.Figure(data=go.Heatmap(
385
- z=matrix, x=available, y=y_labels,
386
- colorscale="Blues",
387
- hovertemplate="Method: %{y}<br>Resource: %{x}<br>% Used: %{z:.2f}%<extra></extra>",
388
- ))
389
- fig.update_layout(
390
- title="Resource Usage vs. Cap",
391
- xaxis_title="Resource", yaxis_title="Method", height=height,
392
- xaxis=dict(tickangle=-35),
393
  )
 
394
  st.plotly_chart(fig, use_container_width=True)
395
 
 
 
 
 
 
 
 
396
  def show_absolute_usage(allocations, method_costs, custom_resources=None):
397
  # Normalize method_costs keys: strip legacy unit suffixes
398
  normalized_costs = {
@@ -438,17 +475,70 @@ def show_absolute_usage(allocations, method_costs, custom_resources=None):
438
  return
439
 
440
  df = pd.DataFrame(rows)
441
- print(df)
442
  for unit in df["Unit"].unique():
443
- st.markdown(f"#### Resources in {unit}")
444
  df_unit = df[df["Unit"] == unit]
445
- fig, ax = plt.subplots(figsize=(8, 4 + 0.3 * df_unit["Resource"].nunique()))
446
- sns.barplot(data=df_unit, x="Resource", y="Used", hue="Method", ax=ax)
447
- ax.set_ylabel(f"Used ({unit})")
448
- ax.tick_params(axis="x", rotation=45)
449
- ax.legend(title="Method", bbox_to_anchor=(1.05, 1), loc="upper left")
450
- st.pyplot(fig)
 
 
 
 
 
 
 
 
 
 
451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
  # ─── Save / export / reset ───────────────────────────────────────────────────────────
454
 
@@ -626,6 +716,68 @@ def export_inputs_only():
626
  icon=":material/download:"
627
  )
628
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
  def clear_all_data():
630
  for key in ["resource_caps", "constraints", "costs", "scenarios",
631
  "latest_result", "scenario_loaded_file", "scenario_name",
@@ -732,6 +884,7 @@ def render_tab():
732
  show_metrics(latest)
733
  df_result = build_results_df(
734
  allocations,
 
735
  method_constraints=latest.get("method_constraints")
736
  )
737
 
@@ -742,9 +895,23 @@ def render_tab():
742
  )
743
 
744
  st.subheader("Optimization Summary")
745
- st.markdown(
746
- format_table_summary(df_display, "cdr-table-res"),
747
- unsafe_allow_html=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  )
749
 
750
  st.markdown("### Method allocation breakdown")
@@ -756,8 +923,15 @@ def render_tab():
756
  st.markdown("### Absolute resource usage by unit")
757
  show_absolute_usage(allocations, method_costs, custom_resources=latest.get("custom_resources", []))
758
 
 
 
 
 
 
 
759
  # ── Save & export & reset ───────────────────────────────────────────────────────
760
  save_scenario(latest)
761
  export_scenario()
762
  export_inputs_only()
 
763
  reset_scenario()
 
6
  import zipfile
7
  import openpyxl
8
 
 
9
  import pandas as pd
 
10
  import streamlit as st
11
+ import plotly.express as px
12
 
13
+ from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, method_order, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT
14
  from optimization.optimization import run_optimization
15
+ from utils.ui_helpers import apply_chart_style, format_table_summary, register_chart_data, show_bar
16
  from utils.data_utils import is_valid_scenario, load_inputs_from_excel, strip_unit_suffix
17
 
18
 
 
60
  st.session_state.setdefault("input_file_name_suggestion", default_name)
61
  try:
62
  resource_caps, constraints, costs, custom_resources = load_inputs_from_excel(uploaded)
 
63
  st.session_state.resource_caps = resource_caps
64
  st.session_state.constraints = constraints
65
  st.session_state.costs = costs
 
137
  custom_resources = []
138
  if "custom_resources.csv" in names:
139
  df_custom = pd.read_csv(io.BytesIO(zf.read("custom_resources.csv")))
140
+ required_custom = {"name", "group", "amount", "unit", "method", "min", "median", "max"}
141
  if required_custom.issubset(df_custom.columns):
142
  for bname in df_custom["name"].unique():
143
  df_b = df_custom[df_custom["name"] == bname]
 
238
 
239
  # Check that at least some resource is non-zero (standard OR custom)
240
  std_total = sum(st.session_state.resource_caps.values())
241
+
242
  custom_total = sum(float(b["amount"]) for b in custom_resources)
243
  if std_total <= 0 and custom_total <= 0:
244
  st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
 
271
  st.session_state.latest_result = {
272
  "summary": {"total_co2_removed": total_removed, "total_cost": total_cost},
273
  "allocations": allocations,
274
+ "method_potential": result["method_potential"],
275
+ "resource_usage": result["resource_usage"],
276
+ "resource_used_total": result["resource_used_total"],
277
+ "resource_remaining": result["resource_remaining"],
278
  "resource_caps": st.session_state.resource_caps.copy(),
279
  "method_constraints": st.session_state.constraints,
280
  "method_costs": st.session_state.costs,
 
293
  cols[0].metric("Total CO₂ Removed", f"{latest['summary']['total_co2_removed']:,.2f} {CO2_UNIT}")
294
  cols[1].metric("Methods Used", len(latest["allocations"]))
295
 
296
+ resource_to_group = {
297
+ r: group
298
+ for group, resources in resource_groups.items()
299
+ for r in resources
300
+ }
301
  # Show which custom batches were included and how much of each was available
302
  if custom_resources:
303
  with st.expander(f"✅ {len(custom_resources)} new resource(s) included in this optimisation", expanded=False):
304
  rows = []
305
  for batch in custom_resources:
306
+ group = resource_to_group.get(batch["group"])
307
+ unit = resource_unit_map.get(group, '?')
308
  rows.append({
309
  "Batch name": batch["name"],
310
  "Group": batch["group"],
311
+ "Amount available": f"{float(batch['amount']):,.2f} {unit}",
312
  "Methods linked": ", ".join(batch["methods"].keys())
313
  })
314
  st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
315
 
316
+ def build_resource_usage_df(resource_usage):
317
+ """resource_usage = {resource: {method: qty}}"""
318
+ resource_to_group = {
319
+ r: group
320
+ for group, resources in resource_groups.items()
321
+ for r in resources
322
+ }
323
+ rows = []
324
+ for r, methods in resource_usage.items():
325
+ unit = resource_unit_map.get(resource_to_group.get(r))
326
+ if unit is None:
327
+ for batch in st.session_state.custom_resources:
328
+ group = resource_to_group.get(batch["group"])
329
+ unit = resource_unit_map.get(group, '?')
330
+
331
+ for m, qty in methods.items():
332
+ rows.append({
333
+ "Resource": f"{r} ({unit})",
334
+ "Method": m,
335
+ "Used": qty
336
+ })
337
+
338
+ return pd.DataFrame(rows)
339
+
340
+ def build_results_df(allocations, method_potential=None, method_constraints=None):
341
  df = pd.DataFrame.from_dict(allocations, orient="index", columns=[f"{CO2_UNIT} removed"])
342
+
343
  df[f"{CO2_UNIT} removed"] = pd.to_numeric(df[f"{CO2_UNIT} removed"], errors="coerce")
344
  total = df[f"{CO2_UNIT} removed"].sum()
345
  if total > 0:
346
+ df["Total share (%)"] = (df[f"{CO2_UNIT} removed"] / total * 100).round(2)
347
+
348
+ if method_potential:
349
+ df["Potential (" + CO2_UNIT + ")"] = pd.Series(method_potential)
350
+ # df["Allocated / Potential (%)"] = (
351
+ # df[f"{CO2_UNIT} removed"] / df["Potential (" + CO2_UNIT + ")"] * 100
352
+ # ).round(1)
353
+ df["Potential (" + CO2_UNIT + ")"] = df[
354
+ "Potential (" + CO2_UNIT + ")"
355
+ ].map(lambda x: f"{x:,.2f}" if pd.notna(x) else x)
356
 
357
  if method_constraints:
358
  caps = {}
 
369
 
370
 
371
  def show_donut(df_result):
372
+ df_plot = df_result.reset_index().rename(columns={"index": "Method"})
373
+ fig = px.pie(df_plot, names="Method", values=f"{CO2_UNIT} removed", hole=0.4,
374
+ color_discrete_sequence=PLOTLY_COLOR_SEQUENCE)
375
+ fig.update_traces(textinfo="label+percent", textposition="outside",
376
+ marker=dict(line=dict(color=CRRA_COLORS["white"], width=1)))
377
+ apply_chart_style(fig)
 
 
 
 
378
  st.plotly_chart(fig, use_container_width=True)
379
+ register_chart_data("Method allocation (donut)", df_plot)
380
 
381
  def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None):
382
  # Normalize standard method_costs keys (strip legacy unit suffixes)
 
414
  matrix.append(row)
415
  y_labels.append(method)
416
 
417
+ df_heat = pd.DataFrame(matrix, index=y_labels, columns=available)
418
+
419
+ fig = px.imshow(
420
+ df_heat, color_continuous_scale=PLOTLY_HEATMAP_SCALE, text_auto=".1f",
421
+ labels=dict(x="Resource", y="Method", color="% Used"),
 
 
 
 
 
 
422
  )
423
+ apply_chart_style(fig, height=max(400, 60 + 30 * len(df_heat.index)))
424
  st.plotly_chart(fig, use_container_width=True)
425
 
426
+ df_export = df_heat.reset_index().rename(columns={"index": "Method"})
427
+
428
+ with st.expander("📊 View / download chart data"):
429
+ st.dataframe(df_export, use_container_width=True, hide_index=True)
430
+
431
+ register_chart_data("Resource usage heatmap (% of cap)", df_export)
432
+
433
  def show_absolute_usage(allocations, method_costs, custom_resources=None):
434
  # Normalize method_costs keys: strip legacy unit suffixes
435
  normalized_costs = {
 
475
  return
476
 
477
  df = pd.DataFrame(rows)
 
478
  for unit in df["Unit"].unique():
 
479
  df_unit = df[df["Unit"] == unit]
480
+ fig = px.bar(
481
+ df_unit, x="Resource", y="Used", color="Method", barmode="group",
482
+ color_discrete_sequence=PLOTLY_COLOR_SEQUENCE,
483
+ )
484
+ apply_chart_style(fig)
485
+ fig.update_layout(yaxis_title=f"Used ({unit})", xaxis_tickangle=-45)
486
+ st.plotly_chart(fig, use_container_width=True)
487
+ with st.expander("📊 View / download chart data"):
488
+ st.dataframe(df, use_container_width=True, hide_index=True)
489
+ register_chart_data("Absolute resource usage by method", df)
490
+
491
+ def show_resource_balance(latest):
492
+ st.markdown("### Resource balance: used vs available")
493
+ resource_caps = latest["resource_caps"]
494
+ used_total = latest["resource_used_total"]
495
+ remaining = latest["resource_remaining"]
496
 
497
+ resource_to_group = {
498
+ r: group
499
+ for group, resources in resource_groups.items()
500
+ for r in resources
501
+ }
502
+
503
+ rows = []
504
+ for r in resource_caps:
505
+ group = resource_to_group.get(r)
506
+ unit = f"{resource_unit_map.get(group, '?')}"
507
+ cap = resource_caps[r]
508
+ if cap <= 0:
509
+ continue
510
+ used = used_total.get(r, 0.0)
511
+ rows.append({
512
+ "Resource": f"{r} ({unit})",
513
+ "Available amount": cap,
514
+ "Quantity used": used,
515
+ "Quantity remaining": remaining.get(r, cap),
516
+ "% used": round(used / cap * 100, 1) if cap > 0 else 0,
517
+ })
518
+
519
+ # inclure aussi les ressources personnalisées
520
+ for batch in latest.get("custom_resources", []):
521
+ r = batch["name"]
522
+ cap = float(batch["amount"])
523
+ used = used_total.get(r, 0.0)
524
+ rows.append({
525
+ "Resource": r, "Available amount": cap, "Quantity used": used,
526
+ "Quantity remaining": remaining.get(r, cap),
527
+ "% used": round(used / cap * 100, 1) if cap > 0 else 0,
528
+ })
529
+
530
+ df = pd.DataFrame(rows).sort_values("% used", ascending=False)
531
+
532
+ fig = px.bar(
533
+ df, x="Resource", y=["Quantity used", "Quantity remaining"], barmode="stack",
534
+ color_discrete_map={"Quantity used": CRRA_COLORS["green"], "Quantity remaining": CRRA_COLORS["off_white"]},
535
+ )
536
+ apply_chart_style(fig)
537
+ fig.update_layout(yaxis_title="Quantity", xaxis_tickangle=-45)
538
+ st.plotly_chart(fig, use_container_width=True)
539
+ with st.expander("📊 View / download chart data"):
540
+ st.dataframe(df, use_container_width=True, hide_index=True)
541
+ register_chart_data("Resource balance (used vs remaining)", df)
542
 
543
  # ─── Save / export / reset ───────────────────────────────────────────────────────────
544
 
 
716
  icon=":material/download:"
717
  )
718
 
719
+ import zipfile
720
+
721
+ def export_all_charts():
722
+ st.markdown("### Export all chart data")
723
+ st.markdown(
724
+ '<p class="caption">Download a single file containing the underlying data '
725
+ 'of every chart shown in this session.</p>',
726
+ unsafe_allow_html=True
727
+ )
728
+
729
+ registry = st.session_state.get("chart_data_registry", {})
730
+ if not registry:
731
+ st.info("No charts have been generated yet in this session.")
732
+ return
733
+
734
+ col_name, col_fmt = st.columns([2, 1])
735
+ with col_name:
736
+ base_name = st.text_input(
737
+ "File name",
738
+ value="charts_data",
739
+ key="charts_export_name",
740
+ help="The extension (.xlsx or .zip) is added automatically.",
741
+ )
742
+ with col_fmt:
743
+ fmt = st.radio(
744
+ "Format", ["Excel (.xlsx, one sheet per chart)", "ZIP (one CSV per chart)"],
745
+ horizontal=True, key="chart_export_format",
746
+ )
747
+
748
+ safe_name = base_name.strip() or "charts_data"
749
+
750
+ if fmt.startswith("Excel"):
751
+ buf = io.BytesIO()
752
+ with pd.ExcelWriter(buf, engine="openpyxl") as writer:
753
+ for name, df in registry.items():
754
+ sheet_name = name[:31] # limite Excel
755
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
756
+ st.download_button(
757
+ f"Download {safe_name}.xlsx",
758
+ data=buf.getvalue(),
759
+ file_name=f"{safe_name}.xlsx",
760
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
761
+ icon=":material/download:"
762
+ )
763
+ else:
764
+ buf = io.BytesIO()
765
+ with zipfile.ZipFile(buf, "w") as zf:
766
+ for name, df in registry.items():
767
+ # nom de fichier sûr : remplace espaces/caractères spéciaux
768
+ safe_chart_name = (
769
+ name.replace(" ", "_").replace("/", "-").replace("(", "").replace(")", "")
770
+ )
771
+ zf.writestr(f"{safe_chart_name}.csv", df.to_csv(index=False))
772
+
773
+ st.download_button(
774
+ f"Download {safe_name}.zip",
775
+ data=buf.getvalue(),
776
+ file_name=f"{safe_name}.zip",
777
+ mime="application/zip",
778
+ icon=":material/download:"
779
+ )
780
+
781
  def clear_all_data():
782
  for key in ["resource_caps", "constraints", "costs", "scenarios",
783
  "latest_result", "scenario_loaded_file", "scenario_name",
 
884
  show_metrics(latest)
885
  df_result = build_results_df(
886
  allocations,
887
+ method_potential=latest.get("method_potential"),
888
  method_constraints=latest.get("method_constraints")
889
  )
890
 
 
895
  )
896
 
897
  st.subheader("Optimization Summary")
898
+ # st.markdown(
899
+ # format_table_summary(df_display, "cdr-table-res"),
900
+ # unsafe_allow_html=True
901
+ # )
902
+
903
+ st.dataframe(
904
+ df_display,
905
+ column_config={
906
+ "Potential (MtCO₂)": st.column_config.NumberColumn(
907
+ "Potential (MtCO₂)",
908
+ help="Maximum CO₂ this method could remove if limited only by "
909
+ "resources and its own cap, without competition from other "
910
+ "methods. For % caps, this value ignores the cap.",
911
+ format="%.0f",
912
+ ),
913
+ },
914
+ use_container_width=True,
915
  )
916
 
917
  st.markdown("### Method allocation breakdown")
 
923
  st.markdown("### Absolute resource usage by unit")
924
  show_absolute_usage(allocations, method_costs, custom_resources=latest.get("custom_resources", []))
925
 
926
+ show_resource_balance(latest)
927
+ df_usage = build_resource_usage_df(latest["resource_usage"])
928
+
929
+ st.markdown("### Detailed resource usage by method")
930
+ st.dataframe(df_usage.pivot(index="Resource", columns="Method", values="Used").fillna(0), use_container_width=True)
931
+
932
  # ── Save & export & reset ───────────────────────────────────────────────────────
933
  save_scenario(latest)
934
  export_scenario()
935
  export_inputs_only()
936
+ export_all_charts()
937
  reset_scenario()
tabs/tab5_sensitivity.py CHANGED
@@ -2,10 +2,11 @@ import matplotlib.pyplot as plt
2
  import pandas as pd
3
  import seaborn as sns
4
  import streamlit as st
 
5
 
6
  from config.config import CO2_UNIT, method_order, resource_order
7
  from optimization.optimization import run_optimization
8
- from utils.ui_helpers import show_bar
9
 
10
 
11
  def render_tab():
@@ -41,6 +42,10 @@ def render_tab():
41
  )
42
  baseline_removed = baseline_result["total_removed"] if baseline_success else 0
43
 
 
 
 
 
44
  tab1, tab2 = st.tabs(["🔁 Resource sensitivity", "🔁 Method sensitivity"])
45
 
46
  with tab1:
@@ -78,11 +83,13 @@ def render_tab():
78
 
79
  df = pd.DataFrame(all_data)
80
  st.markdown("#### Sensitivity to resource availability")
81
- fig, ax = plt.subplots(figsize=(8, 5))
82
- sns.lineplot(data=df, x="% of cap", y=f"CO₂ removed ({CO2_UNIT})", hue="Input", ax=ax)
83
- ax.axhline(y=baseline_removed, linestyle="--", color="gray", label=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
84
- ax.legend()
85
- st.pyplot(fig)
 
 
86
 
87
  with tab2:
88
  st.subheader("Impact of max allocation per method")
@@ -119,11 +126,19 @@ def render_tab():
119
  })
120
 
121
  df = pd.DataFrame(all_data)
122
- fig, ax = plt.subplots(figsize=(8, 5))
123
- sns.lineplot(data=df, x="% max allocation", y=f"CO₂ removed ({CO2_UNIT})", hue="Input", ax=ax)
124
- ax.axhline(y=baseline_removed, linestyle="--", color="gray", label=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
125
- ax.legend()
126
- st.pyplot(fig)
 
 
 
 
 
 
 
 
127
 
128
  if absolute_methods:
129
  st.markdown(f"#### Sensitivity to absolute cap ({CO2_UNIT}/yr)")
@@ -158,8 +173,10 @@ def render_tab():
158
 
159
  if all_data:
160
  df = pd.DataFrame(all_data)
161
- fig, ax = plt.subplots(figsize=(8, 5))
162
- sns.lineplot(data=df, x=f"Cap ({CO2_UNIT}/yr)", y=f"CO₂ removed ({CO2_UNIT})", hue="Input", ax=ax)
163
- ax.axhline(y=baseline_removed, linestyle="--", color="gray", label=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
164
- ax.legend()
165
- st.pyplot(fig)
 
 
 
2
  import pandas as pd
3
  import seaborn as sns
4
  import streamlit as st
5
+ import plotly.express as px
6
 
7
  from config.config import CO2_UNIT, method_order, resource_order
8
  from optimization.optimization import run_optimization
9
+ from utils.ui_helpers import register_chart_data, show_bar
10
 
11
 
12
  def render_tab():
 
42
  )
43
  baseline_removed = baseline_result["total_removed"] if baseline_success else 0
44
 
45
+ all_resources = resource_order
46
+
47
+ print(resource_order)
48
+ print(st.session_state.get("custom_resources", []))
49
  tab1, tab2 = st.tabs(["🔁 Resource sensitivity", "🔁 Method sensitivity"])
50
 
51
  with tab1:
 
83
 
84
  df = pd.DataFrame(all_data)
85
  st.markdown("#### Sensitivity to resource availability")
86
+ fig = px.line(df, x="% of cap", y=f"CO₂ removed ({CO2_UNIT})", color="Input", markers=True)
87
+ fig.add_hline(y=baseline_removed, line_dash="dash", line_color="gray",
88
+ annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
89
+ st.plotly_chart(fig, use_container_width=True)
90
+ with st.expander("📊 View / download chart data"):
91
+ st.dataframe(df, use_container_width=True, hide_index=True)
92
+ register_chart_data("Sensitivity to resource availability)", df)
93
 
94
  with tab2:
95
  st.subheader("Impact of max allocation per method")
 
126
  })
127
 
128
  df = pd.DataFrame(all_data)
129
+ # fig, ax = plt.subplots(figsize=(8, 5))
130
+ # sns.lineplot(data=df, x="% max allocation", y=f"CO₂ removed ({CO2_UNIT})", hue="Input", ax=ax)
131
+ # ax.axhline(y=baseline_removed, linestyle="--", color="gray", label=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
132
+ # ax.legend()
133
+ # st.pyplot(fig)
134
+
135
+ fig = px.line(df, x="% max allocation", y=f"CO₂ removed ({CO2_UNIT})", color="Input", markers=True)
136
+ fig.add_hline(y=baseline_removed, line_dash="dash", line_color="gray",
137
+ annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
138
+ st.plotly_chart(fig, use_container_width=True)
139
+ with st.expander("📊 View / download chart data"):
140
+ st.dataframe(df, use_container_width=True, hide_index=True)
141
+ register_chart_data("Sensitivity to % cap", df)
142
 
143
  if absolute_methods:
144
  st.markdown(f"#### Sensitivity to absolute cap ({CO2_UNIT}/yr)")
 
173
 
174
  if all_data:
175
  df = pd.DataFrame(all_data)
176
+ fig = px.line(df, x="Cap ({CO2_UNIT}/yr)", y=f"CO₂ removed ({CO2_UNIT})", color="Input", markers=True)
177
+ fig.add_hline(y=baseline_removed, line_dash="dash", line_color="gray",
178
+ annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
179
+ st.plotly_chart(fig, use_container_width=True)
180
+ with st.expander("📊 View / download chart data"):
181
+ st.dataframe(df, use_container_width=True, hide_index=True)
182
+ register_chart_data("Sensitivity to absolute cap", df)
tabs/tab6_scenarios.py CHANGED
@@ -102,6 +102,8 @@ def _show_method_allocations(df):
102
  plt.xticks(rotation=45, ha="right")
103
  st.pyplot(fig)
104
 
 
 
105
 
106
  def _show_resource_usage(df):
107
  if df.empty:
 
102
  plt.xticks(rotation=45, ha="right")
103
  st.pyplot(fig)
104
 
105
+
106
+
107
 
108
  def _show_resource_usage(df):
109
  if df.empty:
utils/ui_helpers.py CHANGED
@@ -1,11 +1,34 @@
1
  import numpy as np
2
  import pandas as pd
3
  import streamlit as st
4
- from config.config import CO2_UNIT
 
 
5
 
6
  # params
7
  background_color = "#3d86e9"
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  def show_bar():
10
  st.markdown(
11
  """
@@ -222,3 +245,4 @@ def clean_labels(labels):
222
  Cleans labels to remove zero-allocation items or clutter.
223
  """
224
  return [label for label in labels if label and label.strip() != ""]
 
 
1
  import numpy as np
2
  import pandas as pd
3
  import streamlit as st
4
+ from config.config import CO2_UNIT, CRRA_COLORS, CRRA_FONT
5
+ import plotly.express as px
6
+ from config.config import PLOTLY_TEMPLATE, PLOTLY_COLOR_SEQUENCE
7
 
8
  # params
9
  background_color = "#3d86e9"
10
 
11
+ def apply_chart_style(fig, height=None):
12
+ """Style cohérent CRRA pour tous les graphiques Plotly."""
13
+ fig.update_layout(
14
+ template=PLOTLY_TEMPLATE,
15
+ font=dict(family=CRRA_FONT, size=12, color=CRRA_COLORS["black"]),
16
+ margin=dict(t=40, b=40, l=40, r=20),
17
+ legend=dict(
18
+ orientation="v",
19
+ yanchor="middle", y=0.5,
20
+ xanchor="left", x=1.02,
21
+ ),
22
+ )
23
+ if height:
24
+ fig.update_layout(height=height)
25
+ return fig
26
+
27
+ def register_chart_data(name: str, df: pd.DataFrame):
28
+ """Enregistre les données d'un graphique pour l'export consolidé final."""
29
+ st.session_state.setdefault("chart_data_registry", {})
30
+ st.session_state["chart_data_registry"][name] = df.copy()
31
+
32
  def show_bar():
33
  st.markdown(
34
  """
 
245
  Cleans labels to remove zero-allocation items or clutter.
246
  """
247
  return [label for label in labels if label and label.strip() != ""]
248
+