Figure Friday 2025 - week 13

join the Figure Friday session on April 4, at noon Eastern Time, to showcase your creation and receive feedback from the community.

Which product category has more saturated fat: dairy-ice-creams or cookies-biscuit? Which has more dietary fiber: shakes or cereals?

Try to answer these and several other questions by using Plotly and Dash to visualize the Grocery ingredients dataset.

This is an edited version of the dataset. The original dataset on GroceryDB has additional columns that are used to estimate each product’s degree of processing.

Things to consider:

  • what can you improve in the app or sample figure below (radar chart)?
  • would you like to tell a different data story using a different graph?
  • can you create a different Dash app?

Sample figure:

Code for sample figure:
from dash import Dash, dcc
import dash_ag_grid as dag
import pandas as pd
import plotly.graph_objects as go

# Download CSV sheet at: https://drive.google.com/file/d/1EoFTpSJOIYmVzemoMLj7vMTqeM1zMy0o/view?usp=sharing
df = pd.read_csv('GroceryDB_foods.csv')

df_filtered = df[df['harmonized single category'].isin(['drink-tea','drink-soft-energy-mixes','drink-juice','drink-shakes-other'])]
df_filtered = df_filtered.groupby(["harmonized single category"])[['Fatty acids, total saturated', 'Fiber, total dietary']].mean().reset_index()

fig = go.Figure()
fig.add_trace(go.Scatterpolar(
      r=df_filtered['Fiber, total dietary'],
      theta=df_filtered['harmonized single category'],
      fill='toself',
      name='Dietary Fiber'
))
fig.add_trace(go.Scatterpolar(
      r=df_filtered['Fatty acids, total saturated'],
      theta=df_filtered['harmonized single category'],
      fill='toself',
      name='Saturated Fatty Acids'
))

fig.update_layout(
  polar=dict(
    radialaxis=dict(
      visible=True,
    )),
  showlegend=True,
    legend=dict(
        title=dict(
            text="Average Ingredient Count"
        )
    )
)

grid = dag.AgGrid(
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i, 'filter': True, 'sortable': True} for i in df.columns],
    dashGridOptions={"pagination": True},
    columnSize="sizeToFit"
)

app = Dash()
app.layout = [
    grid,
    dcc.Graph(figure=fig)
]


if __name__ == "__main__":
    app.run(debug=True)

Participation Instructions:

  • Create - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
  • Submit - post your creation to LinkedIn or Twitter with the hashtags #FigureFriday and #plotly by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
  • Celebrate - join the Figure Friday sessions to showcase your creation and receive feedback from the community.

:point_right: If you prefer to collaborate with others on Discord, join the Plotly Discord channel.

Data Source:

Thank you to Data Is Plural and Babak Ravandi et al. for the data.

For citation of the data, please add the following:

@misc{GroceryDB,
      title={Prevalence of processed foods in major US grocery stores}, 
      author={Babak Ravandi and Gordana Ispirova and Michael Sebek and Peter Mehler and Albert-László Barabási and Giulia Menichetti},
      journal={Nature Food}
      year={2025},
      dio={10.1038/s43016-024-01095-7},
      url = {https://www.nature.com/articles/s43016-024-01095-7}
}
2 Likes

My submission for this weeks figure friday. I must admit, I still have no idea what the
values stand for and mean exactly. You can give feedback but I’m afraid this is also the time
I had available this week.

4 Likes

Hi Marieanne, I don’t know if it helps and changes much of what you want to do. But based on what I understood, why don’t you apply a sklearn MixMax Scaler, and then a dimensionality reduction technique, to reduce all these features to 2 or 3 and you can plot a scatter plot identifying 4 quadrants. Note, I don’t know if this is what you are looking for but this way you could see the groups in each quadrant. Of course, the scales would go from the healthiest to the least healthy, for example.

Excellent Sunday

2 Likes

Thank you, @avacsiglo21 for probably putting me on the right track. And reminding me that there is a reason why I have no idea what you’re talking about , although I can follow the logic of it -). As of today I have "proceed study Maven " in my agenda, I actually wrote it down this morning, let’s go to myself and thank you!

2 Likes

I like the way you used the scatter polar chart @marieanne . I actually used the dropdown to compare between food groups that I eat. Now I know why Mac & Cheese is my favorite: it’s high in everything, especially sugar and fat :face_savoring_food:

2 Likes

Hi Everyone, Excellent week for all,

This week I just follow a similar path like Adam/Marieanne(using the scatter polar/radar chart) create a web app called "Smart Food Recommender"is a simple app designed to help users find similar food products based on their nutritional profiles.
Its main functions are:

  1. Product Recommendations:

    • Allows users to select a food product from a list.
    • Uses similarity algorithms (cosine similarity) to find other products with similar nutritional profiles.
    • Displays recommendations of similar products, highlighting the percentage of similarity and relevant details like brand and price.
  2. Nutritional Analysis:

    • Provides nutritional details of the selected product, including information on proteins, fats, carbohydrates, sugars, fiber, vitamins, and minerals.
    • Evaluates the product’s nutritional profile and displays alerts about potential unfavorable nutritional factors (e.g., high fat, sugar, or sodium content).
      3.Visual Comparison:
    • Generates radar charts that visually compare the nutritional profile of the selected product with recommended products.


The code:

from dash import Dash, html, dcc, Input, Output, State
import dash_bootstrap_components as dbc
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import MinMaxScaler
import plotly.graph_objects as go
import plotly.express as px
import numpy as np

# Initialize the Dash app with a modern Bootstrap theme
# Add suppress_callback_exceptions=True to avoid errors with dynamic components
app = Dash(__name__, external_stylesheets=[dbc.themes.UNITED], suppress_callback_exceptions=True)

# Load the GroceryDB_foods dataset and remove rows with NaN
df = pd.read_csv("GroceryDB_foods.csv").dropna()

df = df.reset_index(drop=True)

# Select relevant columns
nutritional_columns = [
    'Protein', 'Total Fat', 'Carbohydrate', 'Sugars, total',
    'Fiber, total dietary', 'Calcium', 'Iron', 'Sodium', 'Vitamin C',
    'Cholesterol', 'Fatty acids, total saturated', 'Total Vitamin A'
]

# Scale the dataset
scaler = MinMaxScaler()
df_scaled = pd.DataFrame(scaler.fit_transform(df[nutritional_columns]), columns=nutritional_columns)

# Custom style for the entire application
CONTENT_STYLE = {
    "marginLeft": "1rem",
    "marginRight": "1rem",
    "padding": "1rem 1rem",
}

# Colors in RGBA format to use transparency correctly
radar_colors = [
    'rgba(31, 119, 180, 0.8)',   # blue
    'rgba(255, 127, 14, 0.8)',   # orange
    'rgba(44, 160, 44, 0.8)',    # green
    'rgba(214, 39, 40, 0.8)',    # red
    'rgba(148, 103, 189, 0.8)',  # purple
    'rgba(140, 86, 75, 0.8)',    # brown
    'rgba(227, 119, 194, 0.8)',  # pink
    'rgba(188, 189, 34, 0.8)',   # olive
    'rgba(23, 190, 207, 0.8)'    # cyan
]

transparent_radar_colors = [
    'rgba(31, 119, 180, 0.2)',   # transparent blue
    'rgba(255, 127, 14, 0.2)',   # transparent orange
    'rgba(44, 160, 44, 0.2)',    # transparent green
    'rgba(214, 39, 40, 0.2)',    # transparent red
    'rgba(148, 103, 189, 0.2)',  # transparent purple
    'rgba(140, 86, 75, 0.2)',    # transparent brown
    'rgba(227, 119, 194, 0.2)',  # transparent pink
    'rgba(188, 189, 34, 0.2)',   # transparent olive
    'rgba(23, 190, 207, 0.2)'    # transparent cyan
]

app.title = "Smart Food Recommender"

# Modernized and reorganized application layout
app.layout = dbc.Container([
    # Header with colored background and centered text
    dbc.Row([
        dbc.Col([
            html.Div([
                html.H3("Smart Food Recommender", className="display-4 text-white"),
                html.P("Find similar products based on their nutritional profile", className="lead text-white")
            ], className="p-2 bg-secondary rounded-3 text-center my-4")
        ])
    ]),

    # Product selection section (now in a separate row)
    dbc.Row([
        dbc.Col([
            dbc.Button("Select Product", id="open-modal-btn", color="primary", className="mb-3"),
            html.Div(id="product-alert"),  # Alert display here
        ], className="text-center")
    ]),

    # Modal for product selection
    dbc.Modal([
        dbc.ModalHeader("Select a product"),
        dbc.ModalBody([
            dcc.Dropdown(
                id="product-dropdown",
                options=[{"label": row['name'], "value": idx} for idx, row in df.iterrows()],
                placeholder="Search and select product...",
                className="mb-4",
                style={"fontSize": "14px"}  
            ),
        ]),
        dbc.ModalFooter(
            dbc.Button("Close", id="close-modal-btn", className="ms-auto", color="secondary")
        ),
    ], id="modal", size="lg"),

    # Section to display the currently selected product
    dbc.Row([
        dbc.Col([
            html.Div(id="selected-product", className="text-center mb-3 fw-bold fs-4"),
        ])
    ]),

    # Visualization options (RadioItems)
    dbc.Row([
        dbc.Col([
            dbc.Card([
                dbc.CardBody([
                    dcc.RadioItems(
                        id="visualization-options",
                        options=[
                            {"label": "Details and Recommendations", "value": "details-recommendations"},
                            {"label": "Nutritional Comparison", "value": "nutritional-comparison"}
                        ],
                        value="details-recommendations",
                        inline=True,
                        className="mb-2",
                        inputClassName="me-2",
                        labelClassName="me-3"
                    )
                ])
            ], className="shadow-sm mb-4")
        ])
    ]),

    # Container for the selected visualization
    html.Div(id="dynamic-content"),

    # IMPORTANT: Add empty components with correct IDs to avoid callback errors
    # These elements will be initially empty, but are necessary for the callbacks to function
    html.Div(id="product-details", style={"display": "none"}),
    html.Div(id="recommendations", style={"display": "none"}),
    dcc.Graph(id="radar-chart", style={"display": "none"}),

    # Footer
    dbc.Row([
        dbc.Col([
            html.Footer([
                html.P("© 2025 Smart Food Recommender", className="text-center text-muted")
            ], className="mt-5 pt-4 border-top")
        ])
    ])
], fluid=True, #style=CONTENT_STYLE
                          )

# Callback to open and close the modal
@app.callback(
    Output("modal", "is_open"),
    [Input("open-modal-btn", "n_clicks"), Input("close-modal-btn", "n_clicks")],
    [State("modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open

# Callback to update dynamic content based on the selected option
@app.callback(
    Output("dynamic-content", "children"),
    [Input("visualization-options", "value"),
     Input("product-dropdown", "value")]
)
def update_content(selected_option, product_id):
    if selected_option == "details-recommendations":
        return dbc.Row([
            # Left column - Product details
            dbc.Col([
                dbc.Card([
                    dbc.CardHeader([
                        html.H6("Product Details", className="card-title mb-0"),
                    ], className="bg-light"),
                    dbc.CardBody([
                        html.Div(id="product-details-container"),  # Container for details
                    ])
                ], className="h-100 shadow-sm")
            ], md=5),

            # Right column - Recommendations
            dbc.Col([
                dbc.Card([
                    dbc.CardHeader([
                        html.H6("Recommended Products", className="card-title mb-0"),
                    ], className="bg-light"),
                    dbc.CardBody([
                        dbc.Row(id="recommendations-container", className="row-cols-1 row-cols-md-2 g-4")
                    ])
                ], className="h-100 shadow-sm")
            ], md=7)
        ])
    else:
        return dbc.Row([
            dbc.Col([
                dbc.Card([
                    dbc.CardHeader([
                        html.H4("Nutritional Comparison", className="card-title mb-0"),
                    ], className="bg-light"),
                    dbc.CardBody([
                        dcc.Graph(id="radar-chart-container")
                    ])
                ], className="shadow-sm")
            ])
        ])

# Callback to display the selected product
@app.callback(
    Output("selected-product", "children"),
    [Input("product-dropdown", "value")]
)
def display_selected_product(product_id):
    if product_id is None:
        return "No product selected"
    try:
        product = df.loc[int(product_id)]
        return f"Selected product: {product['name']}"
    except Exception:
        return "Error loading product"

# Callback to display details, recommendations, charts and alerts
@app.callback(
    [Output("product-details", "children"),
     Output("recommendations", "children"),
     Output("radar-chart", "figure"),
     Output("product-alert", "children")],
    [Input("product-dropdown", "value")]
)
def update_recommendations(product_id):
    if product_id is None:
        # Return initial states when no product is selected
        return html.Div([
            html.P("Select a product to view its details and recommendations.", className="text-muted fst-italic")
        ]), [], go.Figure(layout=dict(title="Select a product to view the comparison")), None

    try:
        # Details of the selected product
        product = df.loc[int(product_id)]

        # Create nutritional information table
        nutrients_values = {
            'Nutrient': nutritional_columns,
            'Value': [round(product[col], 2) for col in nutritional_columns]
        }
        nutritional_table = dbc.Table.from_dataframe(
            pd.DataFrame(nutrients_values),
            striped=True,
            bordered=False,
            hover=True,
            responsive=True,
            className="mt-3"
        )

        details = html.Div([
            html.Div([
                html.H6(product['name'], className="mb-2"),
                html.P(f"Brand: {product['brand']}", className="mb-1 fs-5"),
                html.P([
                    html.Span("Price: ", className="fw-bold"),
                    html.Span(f"US${product['price']:.2f}", className="text-primary fs-4")
                ], className="mb-3"),
            ]),
            html.Hr(),
            html.H5("Nutritional Information", className="mt-4 mb-3"),
            nutritional_table
        ])

        # Evaluate product conditions for the alert
        unmet_criteria = 0
        criteria_text = []

        if product['Total Fat'] > 20:
            unmet_criteria += 1
            criteria_text.append("high in fats")
        if product['Sugars, total'] > 15:
            unmet_criteria += 1
            criteria_text.append("high in sugars")
        if product['Sodium'] > 500:
            unmet_criteria += 1
            criteria_text.append("high in sodium")

        # Determine alert color and message
        if unmet_criteria == 0:
            alert_color = "success"
            alert_message = "This product has a favorable nutritional profile."
            icon = "âś“"
        elif unmet_criteria == 1:
            alert_color = "warning"
            alert_message = f"Caution: Product {criteria_text[0]}."
            icon = "⚠️"
        elif unmet_criteria == 2:
            alert_color = "warning"
            alert_message = f"Attention: Product {' and '.join(criteria_text)}."
            icon = "⚠️"
        else:
            alert_color = "danger"
            alert_message = "Not recommended: Product with multiple unfavorable nutritional factors."
            icon = "âś—"

        alert = dbc.Alert([
            html.Span(icon, className="me-2"),
            html.Span(alert_message)
        ], color=alert_color, className="mt-3")

        # Calculate similarities for recommendations
        reference_product = df_scaled.iloc[[int(product_id)]]
        similarities = cosine_similarity(reference_product, df_scaled)
        df_temp = df.copy()
        df_temp['similarity'] = similarities[0]

       
        recommended = df_temp.sort_values(by='similarity', ascending=False).iloc[1:7]  # Show 6 recommendations

        cards = []
        for idx, row in recommended.iterrows():
            # Evaluate conditions for this recommended product
            unmet_conditions = 0
            if row['Total Fat'] > 20: unmet_conditions += 1
            if row['Sugars, total'] > 15: unmet_conditions += 1
            if row['Sodium'] > 500: unmet_conditions += 1

            # Determine card border color based on nutritional profile
            if unmet_conditions == 0:
                border_color = "success"
            elif unmet_conditions == 1:
                border_color = "warning"
            elif unmet_conditions == 2:
                border_color = "warning"
            else:
                border_color = "danger"

            # Calculate similarity percentage to display
            similarity_percentage = int(row['similarity'] * 100)

            card = dbc.Col(
                dbc.Card([
                    dbc.CardHeader([
                        html.Div([
                            html.Span(f"{similarity_percentage}% similar",
                                      className="badge bg-primary float-end")
                        ])
                    ], className=f"border-{border_color}"),
                    dbc.CardBody([
                        html.H6(row['name'], className="card-title text-truncate",
                                title=row['name']),
                        html.P(f"Brand: {row['brand']}", className="card-text"),
                        html.Div([
                            html.Span("US$", className="text-muted me-1"),
                            html.Span(f"{row['price']:.2f}", className="fw-bold fs-5")
                        ], className="mt-2")
                    ])
                ], className=f"h-100 shadow-sm border-{border_color}")
            )
            cards.append(card)

        # Create improved radar chart with correct RGBA format
        fig = go.Figure()

        # Add selected product with thicker line
        fig.add_trace(go.Scatterpolar(
            r=[product[col] for col in nutritional_columns],
            theta=nutritional_columns,
            fill='toself',
            name=product['name'],
            line=dict(color=radar_colors[0], width=3),
            fillcolor=transparent_radar_colors[0]  # Use RGBA color with transparency
        ))
        # Add all recommended products to the chart (limiting to available colors)
        max_products = min(len(recommended), len(radar_colors) - 1)  # -1 because we already used the first color
        for i, (_, row) in enumerate(recommended.iloc[:max_products].iterrows()):
            if i < len(radar_colors) - 1:  # Make sure we don't exceed available colors
                fig.add_trace(go.Scatterpolar(
                    r=[row[col] for col in nutritional_columns],
                    theta=nutritional_columns,
                    fill='toself',
                    name=row['name'],
                    line=dict(color=radar_colors[i + 1]),
                    fillcolor=transparent_radar_colors[i + 1]  # Use RGBA color with transparency
                ))

        # Improve chart design
        fig.update_layout(
            polar=dict(
                radialaxis=dict(
                    visible=True,
                    showticklabels=True,
                    gridcolor="rgba(0,0,0,0.1)",
                ),
                angularaxis=dict(
                    gridcolor="rgba(0,0,0,0.1)",
                ),
                bgcolor="rgba(0,0,0,0.02)"
            ),
            showlegend=True,
            legend=dict(
                orientation="h",
                yanchor="bottom",
                y=-0.3,
                xanchor="center",
                x=0.5
            ),
            title={
                'text': "Nutritional Profile Comparison",
                'y': 0.95,
                'x': 0.5,
                'xanchor': 'center',
                'yanchor': 'top'
            },
            margin=dict(l=80, r=80, t=100, b=100),
            paper_bgcolor='rgba(0,0,0,0)',
            plot_bgcolor='rgba(0,0,0,0)',
            height=600  # Fixed height for better visualization
        )

        return details, cards, fig, alert

    except Exception as e:
        # Error handling for debugging
        print(f"Error in callback: {e}")
        return html.Div([
            html.P(f"Error: Could not process request. {str(e)}", className="text-danger")
        ]), [], go.Figure(), dbc.Alert("Error processing data", color="danger")

# Additional callbacks to update dynamic containers
@app.callback(
    Output("product-details-container", "children"),
    [Input("product-details", "children")]
)
def update_details_container(details):
    return details

@app.callback(
    Output("recommendations-container", "children"),
    [Input("recommendations", "children")]
)
def update_recommendations_container(recommendations):
    return recommendations

@app.callback(
    Output("radar-chart-container", "figure"),
    [Input("radar-chart", "figure")]
)
def update_chart_container(figure):
    return figure

# Run the application
if __name__ == '__main__':
    app.run_server(debug=True,)

Any Comments/Suggestions are more than welcome
Best Regards

7 Likes

That’s cool, specifically the similar products. My head still crashes on the values and what it is supposed to mean if you would see them in the wild without the product mentioned which gives you an idea if it’s an unhealthy value. What I was wondering, have you seen products popping up in the similar products from another category?

2 Likes

I understand you, Marianne. In this case, no, because the similarity percentage is quite high, mostly above 95%. Perhaps by expanding to the 10 or 15 most similar, I could get the similarity percentage to drop to 80 or 70%., and products from other categories show up

1 Like

What a professional-looking app, @Avacsiglo21 . That warning/alert is a great addition to the app.

How did you decide on the numbers that define high ingredient value like sugar or fat in your example?

2 Likes

Thanks Adams ,

I did a quick research using AI, and found that these thresholds are generally consistent with recommendations from health organizations, which advise limiting the consumption of saturated fats, added sugars, and sodium. However, it’s important to keep in mind that these thresholds are general and do not account for individual dietary needs. Also, as you mentioned, I’d like to include this type of information to add value for the user.

3 Likes

Hi everyone,

I’m excited to share my Grocery Ingredients Dashboard. This dashboard allows you to dynamically filter data by category and brand, customize nutrient density calculations (with user-defined weight factors), and explore interactive visualizations such as radar charts, scatter plots, bar charts, and histograms.

Key features include:

  • Dynamic Filtering: Easily select categories and brands to focus your analysis.
  • Customizable Nutrient Density: Calculate nutrient density using z-scores for beneficial and negative nutrients with adjustable weights.
  • Flexible Visualizations: Choose metrics for the radar chart (z-score, absolute, or percentage of daily value) and customize scatter plot axes and color coding.

Feel free to explore the app at py.cafe/Grocery ingredients and check out the code on GitHub.

Feedback and suggestions are welcomeŃŽ

5 Likes

He @feanor_92 , impressive! I think usability would improve if brands were automatically filtered if you select a category. It reduces the “gamble” factor, is that a word?

4 Likes

Hi marieanne, thanks for your feedback! That’s a great suggestion.
Automatically filtering the brands based on the selected category would definitely enhance usability and reduce the uncertainty factor. I’ll work on implementing that feature.

1 Like

marieanne, I’ve updated the dashboard to automatically filter the brand options based on the selected category. Here’s how it works now:

  • Dynamic Filtering: When you choose a specific category, the brand dropdown updates to show only the brands relevant to that category.
  • “All” Option: If you select “All” for categories, the brand dropdown displays every available brand.
  • How It’s Done: I added a callback function to the code that adjusts the brand options dynamically whenever the category changes.

Let me know what you think.

3 Likes

Far better, you start now with visualised data on screen, which actually says, “he, I’m alive, filter me!”.

3 Likes

nice work, @feanor_92 . Your apps keep getting better every week :flexed_biceps:

2 Likes

I have 2 questions about the week 13.

The code sample imports graph_objs as shown here:

import plotly.graph_objs as go

I have only seen and used this form:

import plotly.graph_objects as go

Are graph_objs and graph_objects equivalent?

In the data set, some of the categories have wf after the name, like “coffee beans wf”, “rice-grains-wf”, 'nuts-seeds-wf", etc. Anyone know what “wf” means?

1 Like

Good question, @Mike_Purtell . That was my bad; just used an older version of graph_object by mistake. I updated the code. Here’s the explanation from the docs:

Import from graph_objects instead of graph_objs

The legacy plotly.graph_objs package has been aliased as plotly.graph_objects because the latter is much easier to communicate verbally. The plotly.graph_objs package is still available for backward compatibility.

3 Likes

If you (or if actually I, since results can differ) google this, “what does wf in a product category mean” the first answer mentions something about simulated weight fractions, whatever that means.

1 Like