Thank you @adamschroeder and @Avacsiglo21 for your encouragement and tips for deploying on Plotly Cloud. My dashboard for this week is now working there, here is the link: Candy
Also, it has been a disappointment to me that my polars-based apps could not be deployed to PyCafe (I am too stubborn to switch back to pandas). So Plotly Cloud is a game changer for me. I would have been happy if it had similar performance as PyCafe, but actually it is much faster, so I recommend using it. No more waiting while the coffee image blows off steam before you can see and use a dashboard. I appreciate the help I received from @nathandrezner
Really cool, now I can play around with your dashboard/apps, well done
Thanks for joining Plotly Cloud @Mike_Purtell . I look forward to seeing all the apps you plan to create for Figure Friday.
This Dash app is an interactive candy comparison dashboard that visualizes sugar, price, and popularity metrics for different candies. The main chart is a bubble chart where each bubble represents a candy, with size, color, and position based on its attributes. Clicking on a bubble triggers a clickData
event, (green bubble),which updates the dropdown menu to reflect the selected candy. This selection then updates the donut chart, showing which ingredients are present in that candy. The app also features KPI cards highlighting the most sugary, expensive, and popular candies overall.
(Updated!)
Link2: https://standout-ivory-salmon-c5ccd76b.plotly.app - with Plotly Cloud
Code
from dash import Dash, dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
import plotly.graph_objects as go
import pandas as pd
df = pd.read_csv("candy.csv")
ingredients = [
"chocolate", "fruity", "caramel", "peanutyalmondy",
"nougat", "crispedricewafer", "hard"
]
ingredient_labels = [
"Chocolate", "Fruity", "Caramel", "Peanuty/Almondy",
"Nougat", "Crisped Rice/Wafer", "Hard"
]
CARD_STYLE = {
"border": "2px solid white",
"borderRadius": "18px",
"boxShadow": "0 4px 16px 0 rgba(0,0,0,0.10)",
"background": "white",
"marginBottom": "24px"
}
def kpi_card(title, name, value, color):
return dbc.Card(
dbc.CardBody([
html.H6(
f"{title}: {name}",
className="card-title",
style={"fontWeight": "bold"}
),
html.Div(
f"{value:.2f} %",
className=f"text-{color}",
style={"fontSize": "1.5rem", "fontWeight": "bold"}
)
]),
className="text-center",
style={**CARD_STYLE, "padding": "0.5rem", "marginBottom": "0"}
)
app = Dash(__name__, external_stylesheets=[dbc.themes.FLATLY])
server = app.server
app.layout = dbc.Container([
dbc.Row([
dbc.Col(html.H2("🍬 Candy Dashboard"), width=12, className="my-3")
]),
# KPI cards row
dbc.Row([
dbc.Col(id="kpi-sugar", md=4, xs=12, className="mb-2"),
dbc.Col(id="kpi-price", md=4, xs=12, className="mb-2"),
dbc.Col(id="kpi-win", md=4, xs=12, className="mb-2"),
], className="mb-2"),
# --- Filter row: radio buttons left, dropdown right (stacked on mobile) ---
dbc.Row([
# Radio buttons (left on desktop, top on mobile)
dbc.Col([
html.Label(
"X-axis:",
style={
"fontWeight": "bold",
"fontSize": "16px",
"marginRight": "8px"
}
),
dcc.RadioItems(
id="xaxis-radio",
options=[
{"label": "Sugar %", "value": "sugarpercent"},
{"label": "Win %", "value": "winpercent"},
{"label": "Price %", "value": "pricepercent"},
],
value="sugarpercent",
labelStyle={
"display": "inline-block",
"marginRight": "18px",
"fontSize": "18px"
}
)
],
md=8, xs=12, className="order-md-1 order-1", # Left on desktop, top on mobile
style={"marginBottom": "12px"}
),
# Dropdown (right on desktop, bottom on mobile)
dbc.Col([
html.Label(
"Candy:",
style={
"fontWeight": "bold",
"fontSize": "16px",
"marginRight": "8px"
}
),
dcc.Dropdown(
id="candy-dropdown",
options=[
{"label": name, "value": name}
for name in sorted(df["competitorname"].unique())
],
value=None,
clearable=True,
placeholder="Select a candy...",
style={"width": "100%", "fontSize": "16px"}
)
],
md=4, xs=12, className="order-md-2 order-2", # Right on desktop, bottom on mobile
style={"marginBottom": "12px"}
),
], className="mb-3"),
dbc.Row([
dbc.Col(
dbc.Card([
dbc.CardHeader(
"Click a bubble or select from dropdown",
style={"background": "white", "borderBottom": "1px solid #eee"}
),
dbc.CardBody([
dcc.Graph(
id="bubble-chart",
style={"height": "490px", "cursor": "pointer"},
config={"displayModeBar": False}
)
])
], style={**CARD_STYLE, "height": "600px"}),
md=6, xs=12
),
dbc.Col(
dbc.Card([
dbc.CardHeader(
"Ingredients (not in %, only shows which types are present)",
style={"background": "white", "borderBottom": "1px solid #eee"}
),
dbc.CardBody([
html.Div(
id="donut-container",
style={"position": "relative", "height": "500px"}
)
])
], style={**CARD_STYLE, "height": "600px"}),
md=6, xs=12
)
])
], fluid=True, style={
"minHeight": "100vh",
"background": "white",
"padding": "48px"
})
# --- KPI CARDS CALLBACK ---
@app.callback(
Output("kpi-sugar", "children"),
Output("kpi-price", "children"),
Output("kpi-win", "children"),
Input("candy-dropdown", "value")
)
def update_kpis(_):
idx_sugar = df["sugarpercent"].idxmax()
name_sugar = df.loc[idx_sugar, "competitorname"]
val_sugar = df.loc[idx_sugar, "sugarpercent"]
idx_price = df["pricepercent"].idxmax()
name_price = df.loc[idx_price, "competitorname"]
val_price = df.loc[idx_price, "pricepercent"]
idx_win = df["winpercent"].idxmax()
name_win = df.loc[idx_win, "competitorname"]
val_win = df.loc[idx_win, "winpercent"]
return (
kpi_card("Most Sugary", name_sugar, val_sugar, "danger"),
kpi_card("Most Expensive", name_price, val_price, "info"),
kpi_card("Most Popular", name_win, val_win, "success"),
)
# --- Bubble chart: highlight selected candy ---
@app.callback(
Output("bubble-chart", "figure"),
Input("xaxis-radio", "value"),
Input("candy-dropdown", "value"),
)
def update_bubble_chart(xaxis_col, selected_candy):
filtered_df = df.copy()
color_vals = filtered_df["winpercent"].fillna(0)
colorscale = "Sunsetdark"
marker_colors = color_vals.tolist()
marker_sizes = filtered_df["pricepercent"].fillna(0) * 30 + 10
marker_line_colors = ["white"] * len(filtered_df)
marker_line_widths = [2] * len(filtered_df)
marker_opacities = [0.85] * len(filtered_df)
# Highlight if a candy is selected
if selected_candy is not None and selected_candy in filtered_df["competitorname"].values:
idx = filtered_df.index[filtered_df["competitorname"] == selected_candy]
if not idx.empty:
idx = idx[0] - filtered_df.index[0]
marker_colors = list(marker_colors)
marker_colors[idx] = "lightgreen"
marker_opacities[idx] = 1.0
fig = go.Figure()
fig.add_trace(go.Scatter(
x=filtered_df[xaxis_col].fillna(0),
y=filtered_df["winpercent"].fillna(0),
mode="markers",
marker=dict(
size=marker_sizes,
color=marker_colors,
colorscale=colorscale,
showscale=True,
colorbar=dict(title="Win %"),
line=dict(width=marker_line_widths, color=marker_line_colors),
opacity=marker_opacities,
sizemode='area',
sizeref=2.*max(marker_sizes)/(40.**2) if len(marker_sizes) > 0 else 1,
sizemin=6,
),
text=filtered_df["competitorname"],
customdata=filtered_df["competitorname"],
hovertemplate="<b>%{text}</b><br>Sugar %: %{x:.2f}<br>Win %: %{y:.2f}<br>Price %: %{marker.size:.2f}<extra></extra>",
name="Candies"
))
fig.update_layout(
xaxis_title={
"sugarpercent": "Sugar %",
"winpercent": "Win %",
"pricepercent": "Price %"
}[xaxis_col],
yaxis_title="Win %",
margin=dict(t=20, l=40, r=20, b=40),
template="plotly_white",
dragmode=False,
hovermode="closest",
plot_bgcolor="white",
paper_bgcolor="white"
)
return fig
# --- Dropdown and bubble chart sync: clicking a bubble sets the dropdown ---
@app.callback(
Output("candy-dropdown", "value"),
Input("bubble-chart", "clickData"),
State("candy-dropdown", "value"),
prevent_initial_call=True
)
def sync_dropdown(clickData, current_value):
if clickData and "points" in clickData and len(clickData["points"]) > 0:
selected_candy = clickData["points"][0]["customdata"]
return selected_candy
return current_value
# --- Donut chart + annotation or icon ---
@app.callback(
Output("donut-container", "children"),
Input("candy-dropdown", "value")
)
def update_donut(selected_candy):
if selected_candy is None or selected_candy not in df["competitorname"].values:
return html.Div(
"🍬",
style={
"fontSize": "120px",
"textAlign": "center",
"lineHeight": "470px",
"height": "470px"
}
)
clicked_row = df[df["competitorname"] == selected_candy].iloc[0]
values = []
labels = []
orange_gradient = [
"#FF9800", "#FFA726", "#FFB74D", "#FFCC80",
"#FFE0B2", "#FFF3E0", "#FFD180"
]
marker_colors = []
for i, (ing, label) in enumerate(zip(ingredients, ingredient_labels)):
if clicked_row[ing] == 1:
values.append(1)
labels.append(label)
marker_colors.append(orange_gradient[i % len(orange_gradient)])
if not values:
values = [1]
labels = ["No ingredients"]
marker_colors = ["#eee"]
hovertemplate = "<b>%{label}</b><br>No ingredients<extra></extra>"
else:
hovertemplate = "<b>%{label}</b><br>Present: Yes<extra></extra>"
# Break name into lines if too long
name = selected_candy
max_line_length = 14
if len(name) > max_line_length:
# Try to break at spaces, otherwise just cut
if " " in name:
words = name.split(" ")
lines = []
current = ""
for word in words:
if len(current + " " + word) <= max_line_length:
current = (current + " " + word).strip()
else:
lines.append(current)
current = word
if current:
lines.append(current)
name_center = "<br>".join(lines)
else:
name_center = "<br>".join([name[i:i+max_line_length] for i in range(0, len(name), max_line_length)])
else:
name_center = name
pie_fig = go.Figure(go.Pie(
labels=labels,
values=values,
marker=dict(colors=marker_colors),
textinfo="label",
textposition="inside",
showlegend=False,
hole=0.45,
hovertemplate=hovertemplate
))
# Center annotation with candy name
pie_fig.update_layout(
margin=dict(t=30, l=30, r=30, b=30),
template="plotly_white",
annotations=[
dict(
text=f"<b>{name_center}</b>",
x=0.5, y=0.5, font_size=20, showarrow=False,
font_color="#FF9800",
align="center"
)
]
)
return dcc.Graph(
figure=pie_fig,
style={"height": "470px"},
config={"displayModeBar": False}
)
if __name__ == "__main__":
app.run(debug=True)
Hello Ester,
your apps is private can not be accessed.
Regards
Thank you @Avacsiglo21 , I’ve just changed it.
What a nice data app, @Ester . Thanks for sharing.
Was this made with Plotly Studio and uploaded to Plotly Cloud, or was this a Dash app that you uploaded to Plotly Cloud?
One thing that I would recommend is moving the dropdown to the right to be above the pie chart and keeping the radio buttons to the left above the scatter plots.
Hello @Ester, great job on your dashboard, and happy to see that you and @Avacsiglo21 and now me are using Plotly Cloud. I really like how fast the apps load, and how fast they perform compared to PyCafe. It’s funny to me that you have a donut chart in an app about candy, as these are 2 of my favorite food groups. I ran your app a few hours ago on my iPhone, and it rendered there very nicely. When I run my own app on the same phone, nothing scales properly, and it looks pretty bad. Is there anything you put in your app that makes it scales properly on big monitors or on small iPhones?
Hi @Mike_Purtell!
I used dash bootstrap components and responsive layout:
- Using dbc.Container(fluid=True, …)
- Defining columns with md=…, xs=… in dbc.Col
- Structuring your layout with dbc.Row and dbc.Col
I hope it is useful but I will develop it, it is not very mobil-friendly yet.
Hi @adamschroeder! I made my Dash app and uploaded to Plotly Cloud, It is not from Plotly Studio.
With dmc you can set different column widths based on screen size with a dictionary in the span
prop. I recommend removing all the the offset
props and doing something like this for the cards – but feel free to experiment with different numbers if you want it to behave differently:
dmc.Grid(children = [
dmc.GridCol(dmc_select_attribute, span={"base": 6, "lg":3}),
dmc.GridCol(attribute_card , span={"base": 6, "lg":3}),
dmc.GridCol(dmc_select_outcome, span={"base": 6, "lg":3}),
dmc.GridCol(outcome_card , span={"base": 6, "lg":3}),
]),
Here’s an example of for the graphs and grid:
dmc.Grid(
children = [
dmc.GridCol(dcc.Graph(id='boxplot'),span={"base": 6, "lg":4} ),
dmc.GridCol(dcc.Graph(id='histogram'),span={"base": 6, "lg":4}),
dmc.GridCol(
[
dmc.Text(
'Data for each value of {attribute}, {outcome}',
style=style_h3,
id='table-desc'
),
dag.AgGrid(id='ag-grid'),
],
span={"base": 12, "lg":4}
),
]
),
See more info on this (and other ways to make a responsive layout )in the docs
Hello @AnnMarieW , thank you for these thorough and thoughtful suggestions. I can’t wait to try them out in the next few days.
I had similar issues naming my app for week 31. Looks like these have been fixed, had no problem with naming for week 32.