Spaces:
Sleeping
Sleeping
[FIX] Try fixing the position of the sidebar
Browse files- app.py +11 -11
- assets/styles.css +12 -0
- config/config.py +13 -13
- tabs/tab1_inputs.py +6 -6
- tabs/tab4_optimization.py +16 -17
- tabs/tab6_scenarios.py +6 -6
app.py
CHANGED
|
@@ -10,30 +10,30 @@ from streamlit_option_menu import option_menu
|
|
| 10 |
# ── Auto-update: regenerate JSON files if inputs_aggregated.csv has changed ───
|
| 11 |
# Uses a hash file to detect changes — works both locally and on Hugging Face.
|
| 12 |
# Must run before importing config/tabs, which read the generated JSON files.
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
"data/cost_sliders.json",
|
| 17 |
"data/method_groups.json",
|
| 18 |
"data/secondary_resources.json",
|
| 19 |
]
|
| 20 |
|
| 21 |
-
def
|
| 22 |
hasher = hashlib.md5()
|
| 23 |
with open(path, "rb") as f:
|
| 24 |
hasher.update(f.read())
|
| 25 |
return hasher.hexdigest()
|
| 26 |
|
| 27 |
-
if os.path.exists(
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
-
if
|
| 33 |
print("inputs_aggregated.csv has changed — running data/update.py ...")
|
| 34 |
subprocess.run([sys.executable, "data/update.py"], check=True)
|
| 35 |
-
with open(
|
| 36 |
-
|
| 37 |
print("Data files updated successfully.")
|
| 38 |
|
| 39 |
from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
|
|
|
|
| 10 |
# ── Auto-update: regenerate JSON files if inputs_aggregated.csv has changed ───
|
| 11 |
# Uses a hash file to detect changes — works both locally and on Hugging Face.
|
| 12 |
# Must run before importing config/tabs, which read the generated JSON files.
|
| 13 |
+
csv = "data/inputs_aggregated.csv"
|
| 14 |
+
hash_file = "data/.csv_hash"
|
| 15 |
+
generated = [
|
| 16 |
"data/cost_sliders.json",
|
| 17 |
"data/method_groups.json",
|
| 18 |
"data/secondary_resources.json",
|
| 19 |
]
|
| 20 |
|
| 21 |
+
def md5(path):
|
| 22 |
hasher = hashlib.md5()
|
| 23 |
with open(path, "rb") as f:
|
| 24 |
hasher.update(f.read())
|
| 25 |
return hasher.hexdigest()
|
| 26 |
|
| 27 |
+
if os.path.exists(csv):
|
| 28 |
+
current_hash = md5(csv)
|
| 29 |
+
stored_hash = open(hash_file).read().strip() if os.path.exists(hash_file) else ""
|
| 30 |
+
missing = any(not os.path.exists(f) for f in generated)
|
| 31 |
|
| 32 |
+
if current_hash != stored_hash or missing:
|
| 33 |
print("inputs_aggregated.csv has changed — running data/update.py ...")
|
| 34 |
subprocess.run([sys.executable, "data/update.py"], check=True)
|
| 35 |
+
with open(hash_file, "w") as file:
|
| 36 |
+
file.write(current_hash)
|
| 37 |
print("Data files updated successfully.")
|
| 38 |
|
| 39 |
from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
|
assets/styles.css
CHANGED
|
@@ -53,6 +53,18 @@ input, textarea, select,
|
|
| 53 |
background: var(--off-white) !important;
|
| 54 |
}
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
/* ── 4. Markdown headings (st.markdown "### …") ────────────── */
|
| 57 |
.stMarkdown h1 {
|
| 58 |
font-weight: 700;
|
|
|
|
| 53 |
background: var(--off-white) !important;
|
| 54 |
}
|
| 55 |
|
| 56 |
+
/* Default width — no !important so Streamlit's drag-to-resize JS can override it */
|
| 57 |
+
section[data-testid="stSidebar"] {
|
| 58 |
+
width: 350px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Keep the sidebar fixed on screen while the main content scrolls */
|
| 62 |
+
section[data-testid="stSidebar"] > div:first-child {
|
| 63 |
+
position: fixed;
|
| 64 |
+
overflow-y: auto;
|
| 65 |
+
width: inherit;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
/* ── 4. Markdown headings (st.markdown "### …") ────────────── */
|
| 69 |
.stMarkdown h1 {
|
| 70 |
font-weight: 700;
|
config/config.py
CHANGED
|
@@ -7,20 +7,20 @@ CO2_UNIT = "MtCO₂"
|
|
| 7 |
with open("data/method_groups.json") as _f:
|
| 8 |
method_groups: dict[str, list[str]] = json.load(_f)
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
# resource_groups: group → [resources], order follows first appearance in CSV
|
| 13 |
resource_groups: dict[str, list[str]] = {}
|
| 14 |
-
for _,
|
| 15 |
-
|
| 16 |
-
resource_groups.setdefault(
|
| 17 |
-
if
|
| 18 |
-
resource_groups[
|
| 19 |
|
| 20 |
# resource_unit_map: group → base unit (e.g. "Mha/MtCO2" → "Mha")
|
| 21 |
resource_unit_map: dict[str, str] = {}
|
| 22 |
-
for _,
|
| 23 |
-
resource_unit_map[
|
| 24 |
|
| 25 |
resource_order = sum(resource_groups.values(), [])
|
| 26 |
method_order = sum(method_groups.values(), [])
|
|
@@ -35,17 +35,17 @@ except FileNotFoundError:
|
|
| 35 |
# To add or edit a tooltip, open that file — no code change needed.
|
| 36 |
# If the file is missing, tooltips are silently disabled (no hover text shown).
|
| 37 |
try:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
resource_tooltips: dict[str, str] = {
|
| 42 |
row["Resource"]: row["Tooltip"]
|
| 43 |
-
for _, row in
|
| 44 |
if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
|
| 45 |
}
|
| 46 |
method_tooltips: dict[str, str] = {
|
| 47 |
row["Method"]: row["Tooltip"]
|
| 48 |
-
for _, row in
|
| 49 |
if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
|
| 50 |
}
|
| 51 |
except FileNotFoundError:
|
|
|
|
| 7 |
with open("data/method_groups.json") as _f:
|
| 8 |
method_groups: dict[str, list[str]] = json.load(_f)
|
| 9 |
|
| 10 |
+
csv_taxonomy = pd.read_csv("data/inputs_aggregated.csv")
|
| 11 |
|
| 12 |
# resource_groups: group → [resources], order follows first appearance in CSV
|
| 13 |
resource_groups: dict[str, list[str]] = {}
|
| 14 |
+
for _, row in csv_taxonomy.drop_duplicates(subset=["Resource group", "Resource"]).iterrows():
|
| 15 |
+
group, resource = row["Resource group"], row["Resource"]
|
| 16 |
+
resource_groups.setdefault(group, [])
|
| 17 |
+
if resource not in resource_groups[group]:
|
| 18 |
+
resource_groups[group].append(resource)
|
| 19 |
|
| 20 |
# resource_unit_map: group → base unit (e.g. "Mha/MtCO2" → "Mha")
|
| 21 |
resource_unit_map: dict[str, str] = {}
|
| 22 |
+
for _, row in csv_taxonomy.drop_duplicates(subset=["Resource group"]).iterrows():
|
| 23 |
+
resource_unit_map[row["Resource group"]] = str(row["Unit"]).split("/")[0]
|
| 24 |
|
| 25 |
resource_order = sum(resource_groups.values(), [])
|
| 26 |
method_order = sum(method_groups.values(), [])
|
|
|
|
| 35 |
# To add or edit a tooltip, open that file — no code change needed.
|
| 36 |
# If the file is missing, tooltips are silently disabled (no hover text shown).
|
| 37 |
try:
|
| 38 |
+
tooltips_xl = pd.read_excel("data/tooltips.xlsx", sheet_name=None)
|
| 39 |
+
df_res = tooltips_xl.get("Resources", pd.DataFrame(columns=["Resource", "Tooltip"]))
|
| 40 |
+
df_met = tooltips_xl.get("Methods", pd.DataFrame(columns=["Method", "Tooltip"]))
|
| 41 |
resource_tooltips: dict[str, str] = {
|
| 42 |
row["Resource"]: row["Tooltip"]
|
| 43 |
+
for _, row in df_res.iterrows()
|
| 44 |
if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
|
| 45 |
}
|
| 46 |
method_tooltips: dict[str, str] = {
|
| 47 |
row["Method"]: row["Tooltip"]
|
| 48 |
+
for _, row in df_met.iterrows()
|
| 49 |
if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
|
| 50 |
}
|
| 51 |
except FileNotFoundError:
|
tabs/tab1_inputs.py
CHANGED
|
@@ -279,7 +279,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 279 |
if st.button("Cancel", width='stretch', key="btn-reset-cancel"):
|
| 280 |
st.rerun()
|
| 281 |
|
| 282 |
-
def
|
| 283 |
errors = []
|
| 284 |
if not batch_name.strip():
|
| 285 |
errors.append("Resource name is required.")
|
|
@@ -303,7 +303,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 303 |
|
| 304 |
with col_save:
|
| 305 |
if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key="btn-new-add"):
|
| 306 |
-
errors =
|
| 307 |
if errors:
|
| 308 |
with error_placeholder.container():
|
| 309 |
for e in errors:
|
|
@@ -356,7 +356,7 @@ def to_excel_bytes(batches: list) -> bytes:
|
|
| 356 |
return excel_buffer.getvalue()
|
| 357 |
|
| 358 |
|
| 359 |
-
def
|
| 360 |
"""Parse a custom resource Excel file into a list of batch dicts.
|
| 361 |
|
| 362 |
Expects columns: name, group, amount, unit, method, min, median, max.
|
|
@@ -417,7 +417,7 @@ def dialog_load_custom_batch():
|
|
| 417 |
return
|
| 418 |
|
| 419 |
try:
|
| 420 |
-
new_batches =
|
| 421 |
except Exception as e:
|
| 422 |
st.error(f"Error reading file: {e}")
|
| 423 |
return
|
|
@@ -460,7 +460,7 @@ def dialog_load_custom_batch():
|
|
| 460 |
|
| 461 |
|
| 462 |
|
| 463 |
-
def
|
| 464 |
"""Render a single custom batch inside its resource-group expander."""
|
| 465 |
n_methods = len(batch["methods"])
|
| 466 |
amount_val = float(batch["amount"])
|
|
@@ -733,7 +733,7 @@ def render_tab(cost_sliders):
|
|
| 733 |
if resource_to_group.get(b["group"]) == group
|
| 734 |
]
|
| 735 |
for global_idx, batch in group_batches:
|
| 736 |
-
|
| 737 |
|
| 738 |
_, col_button = st.columns([3,1])
|
| 739 |
with col_button:
|
|
|
|
| 279 |
if st.button("Cancel", width='stretch', key="btn-reset-cancel"):
|
| 280 |
st.rerun()
|
| 281 |
|
| 282 |
+
def validate():
|
| 283 |
errors = []
|
| 284 |
if not batch_name.strip():
|
| 285 |
errors.append("Resource name is required.")
|
|
|
|
| 303 |
|
| 304 |
with col_save:
|
| 305 |
if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key="btn-new-add"):
|
| 306 |
+
errors = validate()
|
| 307 |
if errors:
|
| 308 |
with error_placeholder.container():
|
| 309 |
for e in errors:
|
|
|
|
| 356 |
return excel_buffer.getvalue()
|
| 357 |
|
| 358 |
|
| 359 |
+
def parse_custom_excel(file) -> list:
|
| 360 |
"""Parse a custom resource Excel file into a list of batch dicts.
|
| 361 |
|
| 362 |
Expects columns: name, group, amount, unit, method, min, median, max.
|
|
|
|
| 417 |
return
|
| 418 |
|
| 419 |
try:
|
| 420 |
+
new_batches = parse_custom_excel(uploaded)
|
| 421 |
except Exception as e:
|
| 422 |
st.error(f"Error reading file: {e}")
|
| 423 |
return
|
|
|
|
| 460 |
|
| 461 |
|
| 462 |
|
| 463 |
+
def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
|
| 464 |
"""Render a single custom batch inside its resource-group expander."""
|
| 465 |
n_methods = len(batch["methods"])
|
| 466 |
amount_val = float(batch["amount"])
|
|
|
|
| 733 |
if resource_to_group.get(b["group"]) == group
|
| 734 |
]
|
| 735 |
for global_idx, batch in group_batches:
|
| 736 |
+
render_custom_batch_inline(global_idx, batch, cost_sliders, group_unit)
|
| 737 |
|
| 738 |
_, col_button = st.columns([3,1])
|
| 739 |
with col_button:
|
tabs/tab4_optimization.py
CHANGED
|
@@ -16,12 +16,12 @@ from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
|
| 16 |
from utils.data_utils import is_valid_scenario, is_complete_scenario, load_inputs_from_excel, neutralize_zero_coefficient_methods, strip_unit_suffix, get_resource_to_group, get_resource_to_unit
|
| 17 |
|
| 18 |
|
| 19 |
-
def
|
| 20 |
"""Recursively convert numpy/pandas types to plain Python so json.dumps works."""
|
| 21 |
if isinstance(obj, dict):
|
| 22 |
-
return {k:
|
| 23 |
if isinstance(obj, list):
|
| 24 |
-
return [
|
| 25 |
if isinstance(obj, pd.Series):
|
| 26 |
return obj.iloc[0] if len(obj) == 1 else obj.tolist()
|
| 27 |
if isinstance(obj, pd.DataFrame):
|
|
@@ -114,7 +114,7 @@ def parse_zip_scenario(uploaded) -> dict | None:
|
|
| 114 |
with zipfile.ZipFile(io.BytesIO(uploaded.read())) as zf:
|
| 115 |
names = zf.namelist()
|
| 116 |
|
| 117 |
-
def
|
| 118 |
return pd.read_csv(io.BytesIO(zf.read(fname)), index_col=0)
|
| 119 |
|
| 120 |
required = {"resource_caps.csv", "method_constraints.csv",
|
|
@@ -122,10 +122,10 @@ def parse_zip_scenario(uploaded) -> dict | None:
|
|
| 122 |
if not required.issubset(names):
|
| 123 |
return None
|
| 124 |
|
| 125 |
-
resource_caps_raw =
|
| 126 |
resource_caps = {strip_unit_suffix(k): v for k, v in resource_caps_raw.items()}
|
| 127 |
|
| 128 |
-
df_c =
|
| 129 |
|
| 130 |
# Compatibility with older files that used `max_share`
|
| 131 |
if "cap_type (percent or absolute)" in df_c.columns:
|
|
@@ -147,11 +147,11 @@ def parse_zip_scenario(uploaded) -> dict | None:
|
|
| 147 |
for m, row in df_c.iterrows()
|
| 148 |
}
|
| 149 |
|
| 150 |
-
df_costs =
|
| 151 |
|
| 152 |
costs = {m: row.dropna().to_dict() for m, row in df_costs.iterrows()}
|
| 153 |
|
| 154 |
-
df =
|
| 155 |
col = f"{CO2_UNIT} removed"
|
| 156 |
if col not in df.columns:
|
| 157 |
old_col = "tCO₂ removed"
|
|
@@ -550,7 +550,6 @@ def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None,
|
|
| 550 |
full_caps.setdefault(batch["name"], float(batch["amount"]))
|
| 551 |
|
| 552 |
standard_available = [r for r in resource_order if full_caps.get(r, 0) > 0]
|
| 553 |
-
custom_names = {b["name"] for b in (custom_resources or [])}
|
| 554 |
extra_available = [b["name"] for b in (custom_resources or []) if float(b["amount"]) > 0]
|
| 555 |
available = standard_available + extra_available
|
| 556 |
|
|
@@ -632,10 +631,10 @@ def show_absolute_usage(allocations, method_costs, custom_resources=None, resour
|
|
| 632 |
for m, resources in method_costs.items()
|
| 633 |
}
|
| 634 |
# Build resource_units from standard groups
|
| 635 |
-
|
| 636 |
resource_to_group = get_resource_to_group()
|
| 637 |
for cat, resources in resource_groups.items():
|
| 638 |
-
|
| 639 |
|
| 640 |
# Inject custom resource batches using the correct unit from their reference group
|
| 641 |
custom_batch_names = set()
|
|
@@ -643,10 +642,10 @@ def show_absolute_usage(allocations, method_costs, custom_resources=None, resour
|
|
| 643 |
custom_batch_names.add(batch["name"])
|
| 644 |
group_name = resource_to_group.get(batch["group"])
|
| 645 |
correct_unit = resource_unit_map.get(group_name, "?")
|
| 646 |
-
if batch["name"] not in
|
| 647 |
-
|
| 648 |
|
| 649 |
-
local_resource_units = dict(
|
| 650 |
|
| 651 |
rows = []
|
| 652 |
for m, co2 in allocations.items():
|
|
@@ -838,7 +837,7 @@ def export_scenario():
|
|
| 838 |
col_json, col_csv = st.columns(2)
|
| 839 |
with col_json:
|
| 840 |
st.download_button(
|
| 841 |
-
"Export JSON", data=json.dumps(
|
| 842 |
file_name=f"{safe}.json", mime="application/json",
|
| 843 |
width='stretch',
|
| 844 |
icon=":material/download:"
|
|
@@ -1013,7 +1012,7 @@ def export_inputs_only():
|
|
| 1013 |
|
| 1014 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
| 1015 |
|
| 1016 |
-
def
|
| 1017 |
return enabled_methods.get(resource, {}).get(method, True)
|
| 1018 |
|
| 1019 |
wb = openpyxl.Workbook()
|
|
@@ -1035,7 +1034,7 @@ def export_inputs_only():
|
|
| 1035 |
ws3.append(["method", "resource", "value", "secondary"])
|
| 1036 |
for m, resources in st.session_state.costs.items():
|
| 1037 |
for r, v in resources.items():
|
| 1038 |
-
value = 0.0 if not
|
| 1039 |
ws3.append([m, r, value, (m, r) in secondary_resources])
|
| 1040 |
|
| 1041 |
ws4 = wb.create_sheet("custom_resources")
|
|
|
|
| 16 |
from utils.data_utils import is_valid_scenario, is_complete_scenario, load_inputs_from_excel, neutralize_zero_coefficient_methods, strip_unit_suffix, get_resource_to_group, get_resource_to_unit
|
| 17 |
|
| 18 |
|
| 19 |
+
def to_json_safe(obj):
|
| 20 |
"""Recursively convert numpy/pandas types to plain Python so json.dumps works."""
|
| 21 |
if isinstance(obj, dict):
|
| 22 |
+
return {k: to_json_safe(v) for k, v in obj.items()}
|
| 23 |
if isinstance(obj, list):
|
| 24 |
+
return [to_json_safe(v) for v in obj]
|
| 25 |
if isinstance(obj, pd.Series):
|
| 26 |
return obj.iloc[0] if len(obj) == 1 else obj.tolist()
|
| 27 |
if isinstance(obj, pd.DataFrame):
|
|
|
|
| 114 |
with zipfile.ZipFile(io.BytesIO(uploaded.read())) as zf:
|
| 115 |
names = zf.namelist()
|
| 116 |
|
| 117 |
+
def read(fname):
|
| 118 |
return pd.read_csv(io.BytesIO(zf.read(fname)), index_col=0)
|
| 119 |
|
| 120 |
required = {"resource_caps.csv", "method_constraints.csv",
|
|
|
|
| 122 |
if not required.issubset(names):
|
| 123 |
return None
|
| 124 |
|
| 125 |
+
resource_caps_raw = read("resource_caps.csv")["Available Amount"].to_dict()
|
| 126 |
resource_caps = {strip_unit_suffix(k): v for k, v in resource_caps_raw.items()}
|
| 127 |
|
| 128 |
+
df_c = read("method_constraints.csv")
|
| 129 |
|
| 130 |
# Compatibility with older files that used `max_share`
|
| 131 |
if "cap_type (percent or absolute)" in df_c.columns:
|
|
|
|
| 147 |
for m, row in df_c.iterrows()
|
| 148 |
}
|
| 149 |
|
| 150 |
+
df_costs = read("method_costs.csv") # index=method, columns=resources
|
| 151 |
|
| 152 |
costs = {m: row.dropna().to_dict() for m, row in df_costs.iterrows()}
|
| 153 |
|
| 154 |
+
df = read("allocations.csv")
|
| 155 |
col = f"{CO2_UNIT} removed"
|
| 156 |
if col not in df.columns:
|
| 157 |
old_col = "tCO₂ removed"
|
|
|
|
| 550 |
full_caps.setdefault(batch["name"], float(batch["amount"]))
|
| 551 |
|
| 552 |
standard_available = [r for r in resource_order if full_caps.get(r, 0) > 0]
|
|
|
|
| 553 |
extra_available = [b["name"] for b in (custom_resources or []) if float(b["amount"]) > 0]
|
| 554 |
available = standard_available + extra_available
|
| 555 |
|
|
|
|
| 631 |
for m, resources in method_costs.items()
|
| 632 |
}
|
| 633 |
# Build resource_units from standard groups
|
| 634 |
+
resource_units = defaultdict(list)
|
| 635 |
resource_to_group = get_resource_to_group()
|
| 636 |
for cat, resources in resource_groups.items():
|
| 637 |
+
resource_units[resource_unit_map[cat]].extend(resources)
|
| 638 |
|
| 639 |
# Inject custom resource batches using the correct unit from their reference group
|
| 640 |
custom_batch_names = set()
|
|
|
|
| 642 |
custom_batch_names.add(batch["name"])
|
| 643 |
group_name = resource_to_group.get(batch["group"])
|
| 644 |
correct_unit = resource_unit_map.get(group_name, "?")
|
| 645 |
+
if batch["name"] not in resource_units[correct_unit]:
|
| 646 |
+
resource_units[correct_unit].append(batch["name"])
|
| 647 |
|
| 648 |
+
local_resource_units = dict(resource_units)
|
| 649 |
|
| 650 |
rows = []
|
| 651 |
for m, co2 in allocations.items():
|
|
|
|
| 837 |
col_json, col_csv = st.columns(2)
|
| 838 |
with col_json:
|
| 839 |
st.download_button(
|
| 840 |
+
"Export JSON", data=json.dumps(to_json_safe(scenario), indent=2),
|
| 841 |
file_name=f"{safe}.json", mime="application/json",
|
| 842 |
width='stretch',
|
| 843 |
icon=":material/download:"
|
|
|
|
| 1012 |
|
| 1013 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
| 1014 |
|
| 1015 |
+
def is_allowed(method, resource):
|
| 1016 |
return enabled_methods.get(resource, {}).get(method, True)
|
| 1017 |
|
| 1018 |
wb = openpyxl.Workbook()
|
|
|
|
| 1034 |
ws3.append(["method", "resource", "value", "secondary"])
|
| 1035 |
for m, resources in st.session_state.costs.items():
|
| 1036 |
for r, v in resources.items():
|
| 1037 |
+
value = 0.0 if not is_allowed(m, r) else v
|
| 1038 |
ws3.append([m, r, value, (m, r) in secondary_resources])
|
| 1039 |
|
| 1040 |
ws4 = wb.create_sheet("custom_resources")
|
tabs/tab6_scenarios.py
CHANGED
|
@@ -42,7 +42,7 @@ def panel_load_scenario():
|
|
| 42 |
except Exception as e:
|
| 43 |
st.error(f"Error loading {uploaded_file.name}: {e}")
|
| 44 |
|
| 45 |
-
def
|
| 46 |
"""Build comparison DataFrames from the currently selected scenarios.
|
| 47 |
|
| 48 |
Args:
|
|
@@ -101,7 +101,7 @@ def _prepare_comparison_data(selected):
|
|
| 101 |
)
|
| 102 |
|
| 103 |
|
| 104 |
-
def
|
| 105 |
"""Render a grouped bar chart of method allocations across scenarios.
|
| 106 |
|
| 107 |
Args:
|
|
@@ -120,7 +120,7 @@ def _show_method_allocations(df):
|
|
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
-
def
|
| 124 |
"""Render grouped bar charts of resource usage per method and total per scenario.
|
| 125 |
|
| 126 |
Args:
|
|
@@ -184,7 +184,7 @@ def render_tab():
|
|
| 184 |
return
|
| 185 |
|
| 186 |
# Comparison
|
| 187 |
-
df_summary, df_methods, df_resources =
|
| 188 |
|
| 189 |
st.markdown("""<p class="crra-sub-heading">Summary Table</p>""", unsafe_allow_html=True)
|
| 190 |
st.dataframe(df_summary.style.format(na_rep="-", thousands=","), width='stretch')
|
|
@@ -195,5 +195,5 @@ def render_tab():
|
|
| 195 |
icon=":material/download:"
|
| 196 |
)
|
| 197 |
|
| 198 |
-
|
| 199 |
-
|
|
|
|
| 42 |
except Exception as e:
|
| 43 |
st.error(f"Error loading {uploaded_file.name}: {e}")
|
| 44 |
|
| 45 |
+
def prepare_comparison_data(selected):
|
| 46 |
"""Build comparison DataFrames from the currently selected scenarios.
|
| 47 |
|
| 48 |
Args:
|
|
|
|
| 101 |
)
|
| 102 |
|
| 103 |
|
| 104 |
+
def show_method_allocations(df):
|
| 105 |
"""Render a grouped bar chart of method allocations across scenarios.
|
| 106 |
|
| 107 |
Args:
|
|
|
|
| 120 |
|
| 121 |
|
| 122 |
|
| 123 |
+
def show_resource_usage(df):
|
| 124 |
"""Render grouped bar charts of resource usage per method and total per scenario.
|
| 125 |
|
| 126 |
Args:
|
|
|
|
| 184 |
return
|
| 185 |
|
| 186 |
# Comparison
|
| 187 |
+
df_summary, df_methods, df_resources = prepare_comparison_data(selected)
|
| 188 |
|
| 189 |
st.markdown("""<p class="crra-sub-heading">Summary Table</p>""", unsafe_allow_html=True)
|
| 190 |
st.dataframe(df_summary.style.format(na_rep="-", thousands=","), width='stretch')
|
|
|
|
| 195 |
icon=":material/download:"
|
| 196 |
)
|
| 197 |
|
| 198 |
+
show_method_allocations(df_methods)
|
| 199 |
+
show_resource_usage(df_resources)
|