import streamlit as st
import pandas as pd
import plotly.graph_objects as go
from nutriscore import NutriScoreCalculator
# Page config
st.set_page_config(
page_title="Nutri-Score Calculator",
page_icon="🥗",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS
st.markdown("""
""", unsafe_allow_html=True)
# Header
st.markdown('
', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
# Sidebar with information
with st.sidebar:
st.header("ℹ️ About Nutri-Score")
st.markdown("""
The **Nutri-Score** is a nutrition label that converts the nutritional value of products into a simple code consisting of **5 letters** (A to E), each with its own color.
### How it works:
- **A (Dark Green)**: Best nutritional quality
- **B (Light Green)**: Good quality
- **C (Yellow)**: Average quality
- **D (Orange)**: Poor quality
- **E (Red)**: Worst quality
### Calculation:
- **Negative Points (N)**: Energy, sugars, saturated fat, salt
- **Positive Points (P)**: Proteins, fiber, fruits/vegetables
- **Score = N - P**
Lower scores = Better nutrition
""")
st.divider()
st.header("Example Products")
st.markdown("""
### **1. Low-Fat Plain Yogurt — Nutri-Score A**
**Nutrients per 100g**
- Energy: **250 kJ**
- Saturated Fat: **0.2 g**
- Sugars: **3.5 g**
- Salt: **0.10 g**
- Protein: **5.5 g**
- Fiber: **0 g**
- Fruits/Vegetables/Nuts: **0%**
---
### **2. Milk Chocolate — Nutri-Score E**
**Nutrients per 100g**
- Energy: **2200 kJ**
- Saturated Fat: **18 g**
- Sugars: **50 g**
- Salt: **0.35 g**
- Protein: **7 g**
- Fiber: **3 g**
- Fruits/Vegetables/Nuts: **0%**
---
### **3. Frozen French Fries (Baked) — Nutri-Score A**
**Nutrients per 100g**
- Energy: **670 kJ**
- Saturated Fat: **1.2 g**
- Sugars: **0.3 g**
- Salt: **0.10 g**
- Protein: **2.5 g**
- Fiber: **2.0 g**
- Fruits/Vegetables/Nuts: **95% (Potato)**
""")
# Main content in two columns
col1, col2 = st.columns([1, 1], gap="large")
with col1:
st.markdown("### Input Nutritional Values (per 100g/100ml)")
with st.container():
st.markdown('', unsafe_allow_html=True)
st.markdown("**🔴 Nutrients to Limit**")
energy_kj = st.number_input(
"Energy (kJ) *",
min_value=0.0,
max_value=5000.0,
value=1500.0,
step=10.0,
help="Energy content in kilojoules"
)
saturated_fat = st.number_input(
"Saturated Fat (g) *",
min_value=0.0,
max_value=100.0,
value=5.0,
step=0.1,
help="Saturated fatty acids content"
)
sugars = st.number_input(
"Sugars (g) *",
min_value=0.0,
max_value=100.0,
value=10.0,
step=0.1,
help="Simple sugars content"
)
salt = st.number_input(
"Salt (g) *",
min_value=0.0,
max_value=10.0,
value=1.0,
step=0.01,
help="Salt (sodium chloride) content"
)
st.markdown('
', unsafe_allow_html=True)
with st.container():
st.markdown('', unsafe_allow_html=True)
st.markdown("**🟢 Nutrients to Favor**")
proteins = st.number_input(
"Proteins (g) *",
min_value=0.0,
max_value=100.0,
value=8.0,
step=0.1,
help="Protein content"
)
fiber = st.number_input(
"Fiber (g) *",
min_value=0.0,
max_value=100.0,
value=3.0,
step=0.1,
help="Dietary fiber content"
)
fruit_veg = st.number_input(
"Fruits/Vegetables/Nuts (%)",
min_value=0.0,
max_value=100.0,
value=20.0,
step=1.0,
help="Percentage of fruits, vegetables, legumes and nuts"
)
st.markdown('
', unsafe_allow_html=True)
# Calculate button
calculate_btn = st.button("Calculate Nutri-Score", type="primary", use_container_width=True)
with col2:
st.markdown("### Results")
if calculate_btn or st.session_state.get('auto_calculate', False):
# Calculate Nutri-Score
result = NutriScoreCalculator.calculate_nutriscore(
energy_kj=energy_kj,
saturated_fat_g=saturated_fat,
sugars_g=sugars,
salt_g=salt,
proteins_g=proteins,
fiber_g=fiber,
fruit_veg_pct=fruit_veg
)
# Display Nutri-Score grade with color
grade = result['class']
grade_class = f"grade-{grade}"
st.markdown(f"""
Nutri-Score: {grade}
""", unsafe_allow_html=True)
# Display visual Nutri-Score bar
st.markdown("#### Nutri-Score Scale")
# Create Plotly figure for visual scale
grades = ['A', 'B', 'C', 'D', 'E']
colors = {
'A': '#038141',
'B': '#85BB2F',
'C': '#FECB02',
'D': '#EE8100',
'E': '#E63E11'
}
fig = go.Figure()
for i, g in enumerate(grades):
# Larger bar for selected grade
height = 1.5 if g == grade else 1.0
y_pos = 0.5 if g == grade else 0.25
fig.add_trace(go.Bar(
x=[1],
y=[height],
name=g,
marker_color=colors[g],
text=g,
textposition='inside',
textfont=dict(size=24 if g == grade else 18, color='white', family='Arial Black'),
hovertemplate=f'{g}',
showlegend=False
))
fig.update_layout(
barmode='group',
height=150,
margin=dict(l=0, r=0, t=0, b=0),
xaxis=dict(showticklabels=False, showgrid=False),
yaxis=dict(showticklabels=False, showgrid=False),
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)'
)
st.plotly_chart(fig, use_container_width=True)
# Display breakdown in metrics
st.markdown("#### Score Breakdown")
metric_col1, metric_col2, metric_col3 = st.columns(3)
with metric_col1:
st.metric(
label="Negative Points (N)",
value=result['negative_points'],
delta=None,
help="Points from nutrients to limit"
)
with metric_col2:
st.metric(
label="Positive Points (P)",
value=result['positive_points'],
delta=None,
help="Points from nutrients to favor"
)
with metric_col3:
st.metric(
label="Final Score",
value=result['score'],
delta=None,
help="Score = N - P (lower is better)"
)
# Interpretation
st.markdown("#### Interpretation")
interpretations = {
'A': "**Excellent nutritional quality!** This product is among the best choices for your health.",
'B': "**Good nutritional quality.** A solid choice for a balanced diet.",
'C': "**Average nutritional quality.** Consume in moderation as part of a varied diet.",
'D': "**Poor nutritional quality.** Limit consumption of this product.",
'E': "**Very poor nutritional quality.** Avoid regular consumption."
}
st.info(interpretations[grade])
# Display calculation details
with st.expander(" View Calculation Details"):
st.markdown("##### Negative Component (N)")
st.markdown(f"""
- Energy: {energy_kj} kJ → Points: {NutriScoreCalculator._get_points_from_thresholds(energy_kj, NutriScoreCalculator.ENERGY_THRESHOLDS)}
- Saturated Fat: {saturated_fat} g → Points: {NutriScoreCalculator._get_points_from_thresholds(saturated_fat, NutriScoreCalculator.SATURATED_FAT_THRESHOLDS)}
- Sugars: {sugars} g → Points: {NutriScoreCalculator._get_points_from_thresholds(sugars, NutriScoreCalculator.SUGARS_THRESHOLDS)}
- Salt: {salt} g → Points: {NutriScoreCalculator._get_points_from_thresholds(salt, NutriScoreCalculator.SALT_THRESHOLDS)}
- **Total N: {result['negative_points']}**
""")
st.markdown("##### Positive Component (P)")
protein_note = " *(not counted due to N≥11 and fruit/veg≤80%)*" if (result['negative_points'] >= 11 and fruit_veg <= 80) else ""
st.markdown(f"""
- Proteins: {proteins} g{protein_note}
- Fiber: {fiber} g
- Fruits/Vegetables: {fruit_veg}%
- **Total P: {result['positive_points']}**
""")
st.markdown(f"##### Final Calculation")
st.markdown(f"**Score = {result['negative_points']} - {result['positive_points']} = {result['score']}**")
st.markdown(f"**Grade: {grade}** (Score range: {NutriScoreCalculator.CLASS_THRESHOLDS[grade][0]} to {NutriScoreCalculator.CLASS_THRESHOLDS[grade][1]})")
else:
st.info(" Enter nutritional values on the left and click 'Calculate' to see the Nutri-Score!")
# Footer
st.divider()
st.markdown("""
Nutri-Score Calculator | Decision Modeling Project 2025-2026
Based on the official French Nutri-Score algorithm (ANSES methodology)
⚠️ This calculator is for educational purposes. Always refer to official product labels.
""", unsafe_allow_html=True)