############ Imports ##############
import vizro.models as vm
from vizro.models.types import capture
from vizro import Vizro
import pandas as pd
from vizro.managers import data_manager
import plotly.graph_objects as go
import numpy as np
from vizro.models.types import capture
####### Function definitions ######
@capture("graph")
def horizontal_category_subcategory_sales(data_frame):
# Group by Product Category and Product Sub-Category to get total sales and profit
grouped = (
data_frame.groupby(["Product Category", "Product Sub-Category"])
.agg({"Sales": "sum", "Profit": "sum"})
.reset_index()
)
# Sort by Product Category, then by Sales DESCENDING within each category (highest first)
grouped = grouped.sort_values(
["Product Category", "Sales"], ascending=[True, False]
)
# Normalize profit values for color mapping (0 to 1 scale)
min_profit = grouped["Profit"].min()
max_profit = grouped["Profit"].max()
profit_range = max_profit - min_profit
# Create color scale from pale blue (low profit) to dark blue (high profit)
def get_color(profit_value):
if profit_range == 0:
return "#1f4e79" # Default dark blue if no variation
# Normalize profit to 0-1 scale
normalized = (profit_value - min_profit) / profit_range
# Interpolate between pale blue and dark blue
# Pale Blue: #add8e6, Dark Blue: #1f4e79
pale_r, pale_g, pale_b = 173, 216, 230
dark_r, dark_g, dark_b = 31, 78, 121
r = int(pale_r + (dark_r - pale_r) * normalized)
g = int(pale_g + (dark_g - pale_g) * normalized)
b = int(pale_b + (dark_b - pale_b) * normalized)
return f"rgb({r},{g},{b})"
# Create colors for each bar based on profit
colors = [get_color(profit) for profit in grouped["Profit"]]
# Create y-axis labels that maintain the sorted order (reverse for plotly display)
y_labels = grouped["Product Sub-Category"].tolist()
y_labels.reverse() # Reverse so highest sales appear at top
fig = go.Figure()
# Reverse the data order for display (highest sales at top)
grouped_reversed = grouped.iloc[::-1].reset_index(drop=True)
colors_reversed = colors[::-1]
fig.add_trace(
go.Bar(
y=grouped_reversed["Product Sub-Category"],
x=grouped_reversed["Sales"],
orientation="h",
marker=dict(
color=colors_reversed,
line=dict(width=0.5, color="rgba(255,255,255,0.4)"),
),
hovertemplate="<b>%{y}</b><br>Sales: $%{x:,.0f}<br>Profit: $%{customdata:,.0f}<br><extra></extra>",
customdata=grouped_reversed["Profit"],
name="Sales by Sub-Category",
)
)
# Add dividing lines between product categories (adjust for reversed order)
categories = grouped["Product Category"].unique()
total_items = len(grouped)
y_position = 0
for i, category in enumerate(categories):
category_count = len(grouped[grouped["Product Category"] == category])
# Calculate position from bottom for reversed display
if i > 0:
line_position = total_items - y_position - 0.5
fig.add_hline(
y=line_position,
line=dict(color="rgba(255,255,255,0.6)", width=2, dash="solid"),
)
y_position += category_count
# Create gradient bar for legend
gradient_x = np.linspace(0, 1, 100)
gradient_colors = [
get_color(min_profit + (max_profit - min_profit) * x) for x in gradient_x
]
# Add gradient bar as a separate subplot area
fig.add_trace(
go.Bar(
x=gradient_x,
y=["Profit Level"] * 100,
orientation="h",
marker=dict(color=gradient_colors, line=dict(width=0)),
showlegend=False,
hoverinfo="skip",
yaxis="y2",
xaxis="x2",
)
)
fig.update_layout(
xaxis_title="Sales ($)",
yaxis_title="Product Sub-Category",
height=800,
hovermode="closest",
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
showlegend=False,
yaxis=dict(categoryorder="array", categoryarray=y_labels, domain=[0, 0.85]),
# Second y-axis for gradient legend
yaxis2=dict(
domain=[0.9, 0.95],
anchor="x2",
showticklabels=False,
showgrid=False,
zeroline=False,
),
# Second x-axis for gradient legend
xaxis2=dict(
domain=[0.7, 0.98],
anchor="y2",
showticklabels=False,
showgrid=False,
zeroline=False,
),
annotations=[
# Gradient legend labels
dict(
x=0.68,
y=0.925,
xref="paper",
yref="paper",
text="Low Profit",
showarrow=False,
font=dict(size=10, color="white"),
xanchor="right",
),
dict(
x=1.0,
y=0.925,
xref="paper",
yref="paper",
text="High Profit",
showarrow=False,
font=dict(size=10, color="white"),
xanchor="left",
),
dict(
x=0.84,
y=0.97,
xref="paper",
yref="paper",
text="<b>Profit Level</b>",
showarrow=False,
font=dict(size=11, color="white"),
xanchor="center",
),
],
)
return fig
####### Data Manager Settings #####
data_manager["megastore_data"] = pd.read_csv(
"https://raw.githubusercontent.com/stichbury/vizro_projects/main/Megastore/MegastoreData.csv"
)
########### Model code ############
model = vm.Dashboard(
pages=[
vm.Page(
components=[
vm.Graph(
type="graph",
figure=horizontal_category_subcategory_sales(
data_frame="megastore_data"
),
title="Sales by Product Sub-Category (Color = Profit)",
)
],
title="Megastore Sales Analysis",
)
],
theme="vizro_dark",
title="Megastore Dashboard",
)
Vizro().build(model).run()