mathiassandnes Claude Opus 4.5 commited on
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>

Files changed (1) hide show
  1. app.py +39 -75
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"], use_column_width=True)
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("💼 Portfolio Parameters")
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: Revenue + Blended Cost (per product & portfolio)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  # ============================================================
381
  blended_cost_portfolio = portfolio_size * blended_cost_per_product
382
 
383
- # Row 1: Revenue
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 2: Blended Cost
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, use_column_width=True)
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, use_column_width=True)
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, use_column_width=True)
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, use_column_width=True)
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, use_column_width=True)
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, use_column_width=True)
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, use_column_width=True)
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, use_column_width=True)
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, use_column_width=True)
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
- }), use_column_width=True, hide_index=True)
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, use_column_width=True)
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)")