Spaces:
Running
Running
Commit ·
962353d
1
Parent(s): ece90cc
Fix deprecation warnings and reorder Strategy cards
Browse files- Move Break Even + Margin above Revenue + Cost with spacing
- Remove deprecated use_column_width from all plotly_chart calls
- Fix deprecated use_column_width on sidebar image
- Remove emoji from Portfolio Parameters header
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -101,7 +101,7 @@ selected_product = st.sidebar.selectbox(
|
|
| 101 |
config = products[selected_product]
|
| 102 |
|
| 103 |
if "image_url" in products[selected_product].get("product", {}):
|
| 104 |
-
st.sidebar.image(products[selected_product]["product"]["image_url"],
|
| 105 |
|
| 106 |
st.sidebar.markdown("---")
|
| 107 |
st.sidebar.header("View")
|
|
@@ -162,7 +162,7 @@ if "strategy_customer_base" not in st.session_state:
|
|
| 162 |
|
| 163 |
# Render sidebar based on selected view
|
| 164 |
if selected_view == "Strategy":
|
| 165 |
-
st.sidebar.header("
|
| 166 |
st.sidebar.caption("Adjust to see impact on profitability")
|
| 167 |
|
| 168 |
st.sidebar.slider(
|
|
@@ -376,11 +376,34 @@ if selected_view == "Strategy":
|
|
| 376 |
)
|
| 377 |
|
| 378 |
# ============================================================
|
| 379 |
-
# TIER 1:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
# ============================================================
|
| 381 |
blended_cost_portfolio = portfolio_size * blended_cost_per_product
|
| 382 |
|
| 383 |
-
# Row
|
| 384 |
r1_left, r1_right = st.columns(2)
|
| 385 |
with r1_left:
|
| 386 |
st.markdown(f"""
|
|
@@ -399,7 +422,7 @@ if selected_view == "Strategy":
|
|
| 399 |
|
| 400 |
st.markdown('<div style="margin-top: 0.75rem;"></div>', unsafe_allow_html=True)
|
| 401 |
|
| 402 |
-
# Row
|
| 403 |
r2_left, r2_right = st.columns(2)
|
| 404 |
with r2_left:
|
| 405 |
st.markdown(f"""
|
|
@@ -418,29 +441,6 @@ if selected_view == "Strategy":
|
|
| 418 |
|
| 419 |
st.markdown(f'<p style="text-align: center; color: {COLORS["text_muted"]}; font-size: 0.95rem; margin-top: 1rem;">Assuming {lease_term_months}-month cycle length.</p>', unsafe_allow_html=True)
|
| 420 |
|
| 421 |
-
# ============================================================
|
| 422 |
-
# TIER 2: Break Even + Operating Margin
|
| 423 |
-
# ============================================================
|
| 424 |
-
be_display = f"{breakeven_month}" if breakeven_month else "> 120"
|
| 425 |
-
|
| 426 |
-
st.markdown('<div style="margin-top: 0.75rem;"></div>', unsafe_allow_html=True)
|
| 427 |
-
|
| 428 |
-
t2_left, t2_right = st.columns(2)
|
| 429 |
-
with t2_left:
|
| 430 |
-
st.markdown(f"""
|
| 431 |
-
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']}; border-radius: 10px; border: 1px solid {COLORS['border']};">
|
| 432 |
-
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.3rem 0; font-size: 0.95rem; font-weight: 700;">Break Even Period</p>
|
| 433 |
-
<p style="color: {COLORS['primary']}; margin: 0; font-size: 2.2rem; font-weight: 700;">{be_display} <span style="font-size: 1rem; font-weight: 400; color: {COLORS['text_subtle']};">months</span></p>
|
| 434 |
-
</div>
|
| 435 |
-
""", unsafe_allow_html=True)
|
| 436 |
-
with t2_right:
|
| 437 |
-
st.markdown(f"""
|
| 438 |
-
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']}; border-radius: 10px; border: 1px solid {COLORS['border']};">
|
| 439 |
-
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.3rem 0; font-size: 0.95rem; font-weight: 700;">Operating Margin</p>
|
| 440 |
-
<p style="color: {COLORS['primary']}; margin: 0; font-size: 2.2rem; font-weight: 700;">{operating_margin_strat:.0f}%</p>
|
| 441 |
-
</div>
|
| 442 |
-
""", unsafe_allow_html=True)
|
| 443 |
-
|
| 444 |
st.markdown("---")
|
| 445 |
|
| 446 |
# ============================================================
|
|
@@ -490,7 +490,7 @@ if selected_view == "Strategy":
|
|
| 490 |
xaxis_title="Months", yaxis_title="Cumulative Profit (NOK)",
|
| 491 |
showlegend=False, yaxis=dict(separatethousands=True),
|
| 492 |
)
|
| 493 |
-
st.plotly_chart(fig_be
|
| 494 |
|
| 495 |
st.markdown("---")
|
| 496 |
|
|
@@ -543,7 +543,7 @@ if selected_view == "Strategy":
|
|
| 543 |
yaxis_title="NOK", showlegend=False,
|
| 544 |
yaxis=dict(separatethousands=True),
|
| 545 |
)
|
| 546 |
-
st.plotly_chart(fig_rc
|
| 547 |
|
| 548 |
mfg_share = s_mfg_cost / reuse_cycles if reuse_cycles > 0 else s_mfg_cost
|
| 549 |
st.caption(f"Blended cost breakdown: Mfg share {sfmt(mfg_share)} + logistics {sfmt(logistics_per_cycle)} + repair {sfmt(repair_per_cycle)} = **{sfmt(blended_cost_per_product)} NOK** per product per cycle")
|
|
@@ -622,49 +622,13 @@ if selected_view == "Strategy":
|
|
| 622 |
xaxis_title="Months", yaxis_title="Cumulative Profit (NOK)",
|
| 623 |
showlegend=False, yaxis=dict(separatethousands=True),
|
| 624 |
)
|
| 625 |
-
st.plotly_chart(fig_trajectory
|
| 626 |
|
| 627 |
final_profit = cumulative_profit[-1]
|
| 628 |
st.caption(f"5-year projection with {sfmt(fleet_needed)} unit fleet. Final cumulative profit: **{sfmt(final_profit)} NOK**")
|
| 629 |
|
| 630 |
st.markdown("---")
|
| 631 |
|
| 632 |
-
# ============================================================
|
| 633 |
-
# Fleet Efficiency: Customers Served vs Products Manufactured
|
| 634 |
-
# ============================================================
|
| 635 |
-
st.subheader("Fleet Efficiency: Circular vs Linear")
|
| 636 |
-
|
| 637 |
-
cycles_range = list(range(1, reuse_cycles + 1))
|
| 638 |
-
customers_served = [portfolio_size * c for c in cycles_range]
|
| 639 |
-
products_manufactured_circular = [portfolio_size] * len(cycles_range)
|
| 640 |
-
products_manufactured_linear = customers_served # 1 product per customer
|
| 641 |
-
|
| 642 |
-
fig_fleet = go.Figure()
|
| 643 |
-
fig_fleet.add_trace(go.Scatter(
|
| 644 |
-
x=cycles_range, y=customers_served, mode="lines+markers",
|
| 645 |
-
name="Customers Served",
|
| 646 |
-
line=dict(color=COLORS["primary"], width=3),
|
| 647 |
-
marker=dict(size=8),
|
| 648 |
-
))
|
| 649 |
-
fig_fleet.add_trace(go.Scatter(
|
| 650 |
-
x=cycles_range, y=products_manufactured_linear, mode="lines",
|
| 651 |
-
name="Products Manufactured (Linear)",
|
| 652 |
-
line=dict(color=COLORS["negative"], width=2, dash="dot"),
|
| 653 |
-
))
|
| 654 |
-
fig_fleet.add_trace(go.Scatter(
|
| 655 |
-
x=cycles_range, y=products_manufactured_circular, mode="lines",
|
| 656 |
-
name="Products Manufactured (Circular)",
|
| 657 |
-
line=dict(color=COLORS["positive"], width=2, dash="dot"),
|
| 658 |
-
))
|
| 659 |
-
fig_fleet.update_layout(
|
| 660 |
-
xaxis_title="Cycle", yaxis_title="Units",
|
| 661 |
-
xaxis=dict(dtick=1),
|
| 662 |
-
yaxis=dict(separatethousands=True, rangemode="tozero"),
|
| 663 |
-
legend=dict(orientation="h", yanchor="bottom", y=1.02),
|
| 664 |
-
)
|
| 665 |
-
st.plotly_chart(fig_fleet, use_column_width=True)
|
| 666 |
-
savings = customers_served[-1] - portfolio_size
|
| 667 |
-
st.caption(f"{sfmt(portfolio_size)} products serve **{sfmt(customers_served[-1])} families** over {reuse_cycles} cycles — **{sfmt(savings)} fewer products** manufactured vs linear model.")
|
| 668 |
|
| 669 |
# ============================================================
|
| 670 |
# VIEW: DESIGN
|
|
@@ -705,7 +669,7 @@ elif selected_view == "Design":
|
|
| 705 |
)
|
| 706 |
fig_grades.update_traces(texttemplate='%{text:.0f}%', textposition='outside')
|
| 707 |
fig_grades.update_layout(showlegend=False, yaxis_title="% of Returns")
|
| 708 |
-
st.plotly_chart(fig_grades
|
| 709 |
|
| 710 |
st.info(f"**Design insight:** Reducing repair rate from {grade_b_pct:.0%} to {max(5, grade_b_pct*100-10):.0f}% would save €{(grade_b_pct - max(0.05, grade_b_pct-0.10)) * refurb_b:.0f} per returned product.")
|
| 711 |
|
|
@@ -758,7 +722,7 @@ elif selected_view == "Design":
|
|
| 758 |
yaxis_title="Material Composition (%)", yaxis_range=[0, 120],
|
| 759 |
legend=dict(orientation="h", yanchor="bottom", y=1.02),
|
| 760 |
)
|
| 761 |
-
st.plotly_chart(fig_materials
|
| 762 |
|
| 763 |
final_original = lifecycle_df.iloc[-1]["Original Material %"]
|
| 764 |
final_new = lifecycle_df.iloc[-1]["New Parts Added %"]
|
|
@@ -805,7 +769,7 @@ elif selected_view == "Logistics":
|
|
| 805 |
)
|
| 806 |
fig_grades.update_traces(texttemplate='%{text:.0f}%', textposition='outside')
|
| 807 |
fig_grades.update_layout(showlegend=False, yaxis_title="% of Returns")
|
| 808 |
-
st.plotly_chart(fig_grades
|
| 809 |
|
| 810 |
with grade_col2:
|
| 811 |
st.subheader("Cost per Cycle")
|
|
@@ -827,7 +791,7 @@ elif selected_view == "Logistics":
|
|
| 827 |
fig_cycle.add_trace(go.Bar(name="Logistics", x=df_cycle_costs["Cycle"], y=df_cycle_costs["Logistics"],
|
| 828 |
marker_color=COLORS["secondary"], text=[f"€{v:.0f}" for v in df_cycle_costs["Logistics"]], textposition="inside"))
|
| 829 |
fig_cycle.update_layout(barmode="stack", yaxis_title="Cost (EUR)", legend=dict(orientation="h", yanchor="bottom", y=1.02))
|
| 830 |
-
st.plotly_chart(fig_cycle
|
| 831 |
|
| 832 |
st.markdown(f"**Key insight:** Cycle 1 costs **€{new_unit_cost:.0f}**. Cycles 2+ cost **€{refurb_unit_cost:.0f}** ({(1-refurb_unit_cost/new_unit_cost)*100:.0f}% reduction)")
|
| 833 |
|
|
@@ -875,7 +839,7 @@ elif selected_view == "Logistics":
|
|
| 875 |
xaxis_title="",
|
| 876 |
legend=dict(orientation="h", yanchor="bottom", y=1.02),
|
| 877 |
)
|
| 878 |
-
st.plotly_chart(fig_products
|
| 879 |
|
| 880 |
final_in_service = lifecycle_df.iloc[-1]["Products in Service"]
|
| 881 |
final_lost = lifecycle_df.iloc[-1]["Products Lost"]
|
|
@@ -974,7 +938,7 @@ elif selected_view == "Sourcing":
|
|
| 974 |
totals={"marker": {"color": COLORS["info"]}},
|
| 975 |
))
|
| 976 |
fig_waterfall.update_layout(title=f"Manufacturing Cost: €{mfg_cost:.0f}", yaxis_title="EUR", showlegend=False)
|
| 977 |
-
st.plotly_chart(fig_waterfall
|
| 978 |
|
| 979 |
with cost_col2:
|
| 980 |
st.subheader("Bill of Materials")
|
|
@@ -982,7 +946,7 @@ elif selected_view == "Sourcing":
|
|
| 982 |
st.dataframe(mat_df.style.format({
|
| 983 |
"Weight (kg)": "{:.1f}", "Cost/kg": "€{:.2f}",
|
| 984 |
"Total Cost": "€{:.2f}", "Recycled %": "{:.0f}%",
|
| 985 |
-
}),
|
| 986 |
|
| 987 |
# Summary
|
| 988 |
recyclable_weight = sum(m["Weight (kg)"] for m in material_data if m["Recyclable"] == "Yes")
|
|
@@ -1092,7 +1056,7 @@ elif selected_view == "Market":
|
|
| 1092 |
))
|
| 1093 |
fig_customer.update_layout(title="Customer Cost vs Retail Price", xaxis_title="Monthly Lease Price (EUR)",
|
| 1094 |
yaxis_title="Total Customer Cost (EUR)")
|
| 1095 |
-
st.plotly_chart(fig_customer
|
| 1096 |
|
| 1097 |
if retail_price > 0 and total_lease_cost < retail_price:
|
| 1098 |
st.success(f"At €{lease_price}/mo, customers **save €{retail_price - total_lease_cost}** ({(retail_price - total_lease_cost)/retail_price*100:.0f}% less than buying)")
|
|
|
|
| 101 |
config = products[selected_product]
|
| 102 |
|
| 103 |
if "image_url" in products[selected_product].get("product", {}):
|
| 104 |
+
st.sidebar.image(products[selected_product]["product"]["image_url"], width=None)
|
| 105 |
|
| 106 |
st.sidebar.markdown("---")
|
| 107 |
st.sidebar.header("View")
|
|
|
|
| 162 |
|
| 163 |
# Render sidebar based on selected view
|
| 164 |
if selected_view == "Strategy":
|
| 165 |
+
st.sidebar.header("Portfolio Parameters")
|
| 166 |
st.sidebar.caption("Adjust to see impact on profitability")
|
| 167 |
|
| 168 |
st.sidebar.slider(
|
|
|
|
| 376 |
)
|
| 377 |
|
| 378 |
# ============================================================
|
| 379 |
+
# TIER 1: Break Even + Operating Margin
|
| 380 |
+
# ============================================================
|
| 381 |
+
be_display = f"{breakeven_month}" if breakeven_month else "> 120"
|
| 382 |
+
|
| 383 |
+
t1_left, t1_right = st.columns(2)
|
| 384 |
+
with t1_left:
|
| 385 |
+
st.markdown(f"""
|
| 386 |
+
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']}; border-radius: 10px; border: 1px solid {COLORS['border']};">
|
| 387 |
+
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.3rem 0; font-size: 0.95rem; font-weight: 700;">Break Even Period</p>
|
| 388 |
+
<p style="color: {COLORS['primary']}; margin: 0; font-size: 2.2rem; font-weight: 700;">{be_display} <span style="font-size: 1rem; font-weight: 400; color: {COLORS['text_subtle']};">months</span></p>
|
| 389 |
+
</div>
|
| 390 |
+
""", unsafe_allow_html=True)
|
| 391 |
+
with t1_right:
|
| 392 |
+
st.markdown(f"""
|
| 393 |
+
<div style="text-align: center; padding: 1.5rem; background: {COLORS['surface']}; border-radius: 10px; border: 1px solid {COLORS['border']};">
|
| 394 |
+
<p style="color: {COLORS['text_muted']}; margin: 0 0 0.3rem 0; font-size: 0.95rem; font-weight: 700;">Operating Margin</p>
|
| 395 |
+
<p style="color: {COLORS['primary']}; margin: 0; font-size: 2.2rem; font-weight: 700;">{operating_margin_strat:.0f}%</p>
|
| 396 |
+
</div>
|
| 397 |
+
""", unsafe_allow_html=True)
|
| 398 |
+
|
| 399 |
+
st.markdown('<div style="margin-top: 1.5rem;"></div>', unsafe_allow_html=True)
|
| 400 |
+
|
| 401 |
+
# ============================================================
|
| 402 |
+
# TIER 2: Revenue + Blended Cost (per product & portfolio)
|
| 403 |
# ============================================================
|
| 404 |
blended_cost_portfolio = portfolio_size * blended_cost_per_product
|
| 405 |
|
| 406 |
+
# Row: Revenue
|
| 407 |
r1_left, r1_right = st.columns(2)
|
| 408 |
with r1_left:
|
| 409 |
st.markdown(f"""
|
|
|
|
| 422 |
|
| 423 |
st.markdown('<div style="margin-top: 0.75rem;"></div>', unsafe_allow_html=True)
|
| 424 |
|
| 425 |
+
# Row: Blended Cost
|
| 426 |
r2_left, r2_right = st.columns(2)
|
| 427 |
with r2_left:
|
| 428 |
st.markdown(f"""
|
|
|
|
| 441 |
|
| 442 |
st.markdown(f'<p style="text-align: center; color: {COLORS["text_muted"]}; font-size: 0.95rem; margin-top: 1rem;">Assuming {lease_term_months}-month cycle length.</p>', unsafe_allow_html=True)
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
st.markdown("---")
|
| 445 |
|
| 446 |
# ============================================================
|
|
|
|
| 490 |
xaxis_title="Months", yaxis_title="Cumulative Profit (NOK)",
|
| 491 |
showlegend=False, yaxis=dict(separatethousands=True),
|
| 492 |
)
|
| 493 |
+
st.plotly_chart(fig_be)
|
| 494 |
|
| 495 |
st.markdown("---")
|
| 496 |
|
|
|
|
| 543 |
yaxis_title="NOK", showlegend=False,
|
| 544 |
yaxis=dict(separatethousands=True),
|
| 545 |
)
|
| 546 |
+
st.plotly_chart(fig_rc)
|
| 547 |
|
| 548 |
mfg_share = s_mfg_cost / reuse_cycles if reuse_cycles > 0 else s_mfg_cost
|
| 549 |
st.caption(f"Blended cost breakdown: Mfg share {sfmt(mfg_share)} + logistics {sfmt(logistics_per_cycle)} + repair {sfmt(repair_per_cycle)} = **{sfmt(blended_cost_per_product)} NOK** per product per cycle")
|
|
|
|
| 622 |
xaxis_title="Months", yaxis_title="Cumulative Profit (NOK)",
|
| 623 |
showlegend=False, yaxis=dict(separatethousands=True),
|
| 624 |
)
|
| 625 |
+
st.plotly_chart(fig_trajectory)
|
| 626 |
|
| 627 |
final_profit = cumulative_profit[-1]
|
| 628 |
st.caption(f"5-year projection with {sfmt(fleet_needed)} unit fleet. Final cumulative profit: **{sfmt(final_profit)} NOK**")
|
| 629 |
|
| 630 |
st.markdown("---")
|
| 631 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
# ============================================================
|
| 634 |
# VIEW: DESIGN
|
|
|
|
| 669 |
)
|
| 670 |
fig_grades.update_traces(texttemplate='%{text:.0f}%', textposition='outside')
|
| 671 |
fig_grades.update_layout(showlegend=False, yaxis_title="% of Returns")
|
| 672 |
+
st.plotly_chart(fig_grades)
|
| 673 |
|
| 674 |
st.info(f"**Design insight:** Reducing repair rate from {grade_b_pct:.0%} to {max(5, grade_b_pct*100-10):.0f}% would save €{(grade_b_pct - max(0.05, grade_b_pct-0.10)) * refurb_b:.0f} per returned product.")
|
| 675 |
|
|
|
|
| 722 |
yaxis_title="Material Composition (%)", yaxis_range=[0, 120],
|
| 723 |
legend=dict(orientation="h", yanchor="bottom", y=1.02),
|
| 724 |
)
|
| 725 |
+
st.plotly_chart(fig_materials)
|
| 726 |
|
| 727 |
final_original = lifecycle_df.iloc[-1]["Original Material %"]
|
| 728 |
final_new = lifecycle_df.iloc[-1]["New Parts Added %"]
|
|
|
|
| 769 |
)
|
| 770 |
fig_grades.update_traces(texttemplate='%{text:.0f}%', textposition='outside')
|
| 771 |
fig_grades.update_layout(showlegend=False, yaxis_title="% of Returns")
|
| 772 |
+
st.plotly_chart(fig_grades)
|
| 773 |
|
| 774 |
with grade_col2:
|
| 775 |
st.subheader("Cost per Cycle")
|
|
|
|
| 791 |
fig_cycle.add_trace(go.Bar(name="Logistics", x=df_cycle_costs["Cycle"], y=df_cycle_costs["Logistics"],
|
| 792 |
marker_color=COLORS["secondary"], text=[f"€{v:.0f}" for v in df_cycle_costs["Logistics"]], textposition="inside"))
|
| 793 |
fig_cycle.update_layout(barmode="stack", yaxis_title="Cost (EUR)", legend=dict(orientation="h", yanchor="bottom", y=1.02))
|
| 794 |
+
st.plotly_chart(fig_cycle)
|
| 795 |
|
| 796 |
st.markdown(f"**Key insight:** Cycle 1 costs **€{new_unit_cost:.0f}**. Cycles 2+ cost **€{refurb_unit_cost:.0f}** ({(1-refurb_unit_cost/new_unit_cost)*100:.0f}% reduction)")
|
| 797 |
|
|
|
|
| 839 |
xaxis_title="",
|
| 840 |
legend=dict(orientation="h", yanchor="bottom", y=1.02),
|
| 841 |
)
|
| 842 |
+
st.plotly_chart(fig_products)
|
| 843 |
|
| 844 |
final_in_service = lifecycle_df.iloc[-1]["Products in Service"]
|
| 845 |
final_lost = lifecycle_df.iloc[-1]["Products Lost"]
|
|
|
|
| 938 |
totals={"marker": {"color": COLORS["info"]}},
|
| 939 |
))
|
| 940 |
fig_waterfall.update_layout(title=f"Manufacturing Cost: €{mfg_cost:.0f}", yaxis_title="EUR", showlegend=False)
|
| 941 |
+
st.plotly_chart(fig_waterfall)
|
| 942 |
|
| 943 |
with cost_col2:
|
| 944 |
st.subheader("Bill of Materials")
|
|
|
|
| 946 |
st.dataframe(mat_df.style.format({
|
| 947 |
"Weight (kg)": "{:.1f}", "Cost/kg": "€{:.2f}",
|
| 948 |
"Total Cost": "€{:.2f}", "Recycled %": "{:.0f}%",
|
| 949 |
+
}), hide_index=True)
|
| 950 |
|
| 951 |
# Summary
|
| 952 |
recyclable_weight = sum(m["Weight (kg)"] for m in material_data if m["Recyclable"] == "Yes")
|
|
|
|
| 1056 |
))
|
| 1057 |
fig_customer.update_layout(title="Customer Cost vs Retail Price", xaxis_title="Monthly Lease Price (EUR)",
|
| 1058 |
yaxis_title="Total Customer Cost (EUR)")
|
| 1059 |
+
st.plotly_chart(fig_customer)
|
| 1060 |
|
| 1061 |
if retail_price > 0 and total_lease_cost < retail_price:
|
| 1062 |
st.success(f"At €{lease_price}/mo, customers **save €{retail_price - total_lease_cost}** ({(retail_price - total_lease_cost)/retail_price*100:.0f}% less than buying)")
|