import dash
from dash import Input, Output, State, callback, html
import dash_mantine_components as dmc
from dash_iconify import DashIconify
import pandas as pd
import numpy as np
# Load and preprocess the data
df = pd.read_csv(
"https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-22/Marvel-Movies.csv")
# Clean the data
for col in ['critics % score', 'audience % score', '% budget recovered',
'audience vs critics % deviance', '1st vs 2nd weekend drop off', '% budget opening weekend']:
if col in df.columns:
df[col] = pd.to_numeric(df[col].str.rstrip('%'), errors='coerce')
# Calculate ROI
df['roi'] = ((df['worldwide gross'] - df['budget']) / df['budget'] * 100).round(1)
# Create color mapping for categories
category_colors = {
'Marvel Cinematic Universe Avengers': 'red.6',
'MCU': 'blue.6',
'Phase 4 MCU': 'violet.6'
}
# Theme configuration
theme = {
"primaryColor": "red",
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
"fontSizes": {
"xs": "0.75rem",
"sm": "0.875rem",
"md": "1rem",
"lg": "1.125rem",
"xl": "1.25rem"
},
"spacing": {
"xs": "0.5rem",
"sm": "0.75rem",
"md": "1rem",
"lg": "1.5rem",
"xl": "2rem"
},
"radius": {
"sm": "0.25rem",
"md": "0.5rem",
"lg": "1rem"
},
"defaultRadius": "md",
"shadows": {
"sm": "0 1px 3px rgba(0,0,0,0.05), 0 1px 2px rgba(0,0,0,0.1)",
"md": "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06)"
},
"headings": {
"fontWeight": 700,
"sizes": {
"h1": {"fontSize": "2.5rem", "lineHeight": 1.3},
"h2": {"fontSize": "2rem", "lineHeight": 1.35},
"h3": {"fontSize": "1.5rem", "lineHeight": 1.4},
"h4": {"fontSize": "1.25rem", "lineHeight": 1.45}
}
},
"components": {
"Card": {
"defaultProps": {
"shadow": "sm",
"radius": "md",
"withBorder": True
}
},
"Button": {
"defaultProps": {
"radius": "md"
}
}
}
}
app = dash.Dash(__name__, suppress_callback_exceptions=True)
# Header with theme toggle
header = dmc.AppShellHeader(
dmc.Group(
[
dmc.Group(
[
dmc.ThemeIcon(
DashIconify(icon="mdi:marvel", width=30),
size=45,
radius="md",
variant="light",
color="red"
),
dmc.Stack(
[
dmc.Title("Marvel Movies Dashboard", order=3, size="h4"),
dmc.Text("Analyzing the Marvel Cinematic Universe", size="sm", c="dimmed")
],
gap=0
)
],
gap="md"
),
dmc.Group(
[
dmc.Tooltip(
label="Toggle theme",
children=dmc.ActionIcon(
[
DashIconify(
icon="tabler:sun",
width=20,
id="light-theme-icon",
style={"display": "none"}
),
DashIconify(
icon="tabler:moon",
width=20,
id="dark-theme-icon"
),
],
variant="subtle",
size="lg",
id="theme-toggle"
)
)
],
gap="md"
)
],
justify="space-between",
style={"width": "100%"}
),
style={"backgroundColor": "var(--mantine-color-body)"},
p="md"
)
# KPI Cards with sparklines
def create_kpi_card(title, value, icon, color="blue", sparkline_data=None, trend=None):
card_content = [
dmc.Group(
[
dmc.ThemeIcon(
DashIconify(icon=icon, width=25),
size=45,
radius="md",
color=color,
variant="light"
),
dmc.Stack(
[
dmc.Text(title, size="sm", c="dimmed", fw=500),
dmc.Group(
[
dmc.Title(value, order=3, size="h3"),
trend if trend else None
],
gap="xs"
)
],
gap=4
)
],
gap="md"
)
]
if sparkline_data is not None:
card_content.append(
dmc.Sparkline(
data=sparkline_data,
curveType="natural",
color=color,
fillOpacity=0.6,
strokeWidth=2,
h=40,
mt="md"
)
)
return dmc.Card(children=card_content, p="lg")
# Calculate KPI values
total_revenue = df['worldwide gross'].sum()
avg_roi = df['roi'].mean()
best_movie = df.loc[df['worldwide gross'].idxmax(), 'film']
avg_score = df[['critics % score', 'audience % score']].mean().mean()
# Get sparkline data
revenue_by_year = df.groupby('year')['worldwide gross'].sum().values.tolist()
roi_by_year = df.groupby('year')['roi'].mean().values.tolist()
kpi_section = dmc.SimpleGrid(
[
create_kpi_card(
"Total Revenue",
f"${total_revenue / 1000:.1f}B",
"tabler:currency-dollar",
"green",
sparkline_data=revenue_by_year
),
create_kpi_card(
"Average ROI",
f"{avg_roi:.0f}%",
"tabler:trending-up",
"blue",
sparkline_data=roi_by_year,
trend=dmc.Badge("↑ 15.3%", color="teal", variant="light", size="sm")
),
create_kpi_card(
"Top Grossing Film",
best_movie,
"tabler:trophy",
"yellow"
),
create_kpi_card(
"Average Score",
f"{avg_score:.0f}%",
"tabler:star",
"red"
)
],
cols={"base": 1, "sm": 2, "lg": 4},
spacing="lg",
mt="lg"
)
# Prepare data for charts
top_movies = df.nlargest(10, 'worldwide gross')
chart_data = []
for _, row in top_movies.iterrows():
chart_data.append({
'film': row['film'][:20] + '...' if len(row['film']) > 20 else row['film'],
'domestic': row['domestic gross ($m)'],
'international': row['international gross ($m)'],
'total': row['worldwide gross']
})
# Category distribution data
category_data = []
for cat in df['category'].unique():
cat_df = df[df['category'] == cat]
category_data.append({
'name': cat,
'value': len(cat_df),
'color': category_colors.get(cat, 'gray.6')
})
# Score comparison data - Fixed format for ScatterChart
score_data = []
for _, row in df.iterrows():
if pd.notna(row['critics % score']) and pd.notna(row['audience % score']):
score_data.append({
'x': row['critics % score'],
'y': row['audience % score'],
'film': row['film']
})
# Timeline data
timeline_data = []
yearly_stats = df.groupby('year').agg({
'worldwide gross': 'sum',
'film': 'count',
'budget': 'mean'
}).reset_index()
for _, row in yearly_stats.iterrows():
timeline_data.append({
'year': str(row['year']),
'revenue': row['worldwide gross'],
'count': row['film'],
'avgBudget': row['budget']
})
# Charts Section
charts_section = dmc.Stack(
[
dmc.SimpleGrid(
[
# Revenue Distribution Chart
dmc.Card(
[
dmc.Title("Top 10 Movies: Domestic vs International Revenue", order=4, mb="md"),
dmc.BarChart(
h=300,
dataKey="film",
data=chart_data,
type="stacked",
series=[
{"name": "domestic", "label": "Domestic", "color": "red.6"},
{"name": "international", "label": "International", "color": "blue.6"}
],
xAxisProps={"angle": -45},
withLegend=True,
legendProps={"verticalAlign": "top", "height": 50}
)
],
p="lg"
),
# Category Distribution
dmc.Card(
[
dmc.Title("Movies by Category", order=4, mb="md"),
dmc.DonutChart(
data=category_data,
h=300,
chartLabel=f"{len(df)} Movies",
paddingAngle=2,
withLabels=True
)
],
p="lg"
)
],
cols={"base": 1, "lg": 2},
spacing="lg"
),
# Timeline Chart
dmc.Card(
[
dmc.Group(
[
dmc.Title("Revenue Timeline", order=4),
dmc.SegmentedControl(
id="timeline-metric",
value="revenue",
data=[
{"label": "Revenue", "value": "revenue"},
{"label": "Movie Count", "value": "count"},
{"label": "Avg Budget", "value": "avgBudget"}
],
size="sm"
)
],
justify="space-between",
mb="md"
),
dmc.AreaChart(
id="timeline-chart",
h=250,
dataKey="year",
data=timeline_data,
series=[
{"name": "revenue", "label": "Total Revenue ($M)", "color": "red.6"}
],
curveType="natural",
fillOpacity=0.2,
withGradient=True,
gridAxis="xy"
)
],
p="lg"
),
# Score Comparison - Fixed ScatterChart implementation
dmc.Card(
[
dmc.Title("Critics vs Audience Scores", order=4, mb="md"),
dmc.Text("Movies above the diagonal line are better received by audiences", c="dimmed", size="sm", mb="lg"),
dmc.ScatterChart(
h=350,
data=score_data[:20], # Limit to 20 for clarity
dataKey="film",
xAxisLabel="Critics Score (%)",
yAxisLabel="Audience Score (%)",
referenceLines=[
{
"x": 50,
"label": "50% Critics",
"color": "gray.5"
},
{
"y": 50,
"label": "50% Audience",
"color": "gray.5"
}
]
)
],
p="lg"
)
],
gap="lg"
)
# Movie Cards Section
movie_cards = dmc.Card(
[
dmc.Group(
[
dmc.Title("Movie Details", order=4),
dmc.Group(
[
dmc.TextInput(
placeholder="Search movies...",
# icon=DashIconify(icon="tabler:search"),
id="movie-search",
w=300
),
dmc.SegmentedControl(
id="sort-by",
value="gross",
data=[
{"label": "Revenue", "value": "gross"},
{"label": "ROI", "value": "roi"},
{"label": "Score", "value": "score"}
],
size="sm"
)
],
gap="md"
)
],
justify="space-between",
mb="lg"
),
html.Div(id="movie-cards-container")
],
p="lg"
)
# Main layout
app.layout = dmc.MantineProvider(
theme=theme,
id="mantine-provider",
children=[
dmc.AppShell(
[
header,
dmc.AppShellMain(
dmc.Container(
[
kpi_section,
charts_section,
movie_cards,
dmc.Space(h="xl")
],
size="xl",
p={"base": "sm", "sm": "md"}
)
)
],
header={"height": {"base": 100, "sm": 70}},
padding="md"
),
html.Div(id="theme-store", style={"display": "none"}, children="light")
]
)
# Callbacks
@callback(
Output("mantine-provider", "forceColorScheme"),
Output("theme-store", "children"),
Output("light-theme-icon", "style"),
Output("dark-theme-icon", "style"),
Input("theme-toggle", "n_clicks"),
State("theme-store", "children"),
prevent_initial_call=True
)
def toggle_theme(n_clicks, current_theme):
new_theme = "dark" if current_theme == "light" else "light"
light_style = {"display": "block"} if new_theme == "dark" else {"display": "none"}
dark_style = {"display": "none"} if new_theme == "dark" else {"display": "block"}
return new_theme, new_theme, light_style, dark_style
@callback(
Output("timeline-chart", "series"),
Input("timeline-metric", "value")
)
def update_timeline_metric(metric):
series_config = {
"revenue": {"name": "revenue", "label": "Total Revenue ($M)", "color": "red.6"},
"count": {"name": "count", "label": "Number of Movies", "color": "blue.6"},
"avgBudget": {"name": "avgBudget", "label": "Average Budget ($M)", "color": "green.6"}
}
return [series_config[metric]]
@callback(
Output("movie-cards-container", "children"),
Input("movie-search", "value"),
Input("sort-by", "value")
)
def update_movie_cards(search_value, sort_by):
filtered_df = df.copy()
# Apply search filter
if search_value:
filtered_df = filtered_df[
filtered_df['film'].str.contains(search_value, case=False, na=False)
]
# Sort data
if sort_by == "gross":
filtered_df = filtered_df.sort_values('worldwide gross', ascending=False)
elif sort_by == "roi":
filtered_df = filtered_df.sort_values('roi', ascending=False)
else: # score
filtered_df['avg_score'] = filtered_df[['critics % score', 'audience % score']].mean(axis=1)
filtered_df = filtered_df.sort_values('avg_score', ascending=False)
# Create movie cards
if len(filtered_df) == 0:
return dmc.Stack(
[
dmc.ThemeIcon(
DashIconify(icon="tabler:movie-off", width=40),
size=60,
radius="xl",
color="gray",
variant="light"
),
dmc.Title("No movies found", order=4, ta="center"),
dmc.Text("Try adjusting your search criteria", c="dimmed", ta="center")
],
align="center",
justify="center",
h=200
)
cards = []
for _, movie in filtered_df.head(12).iterrows():
# Calculate performance indicators
roi_color = "green" if movie['roi'] > 200 else "yellow" if movie['roi'] > 100 else "red"
score_diff = movie['audience % score'] - movie['critics % score'] if pd.notna(
movie['audience % score']) and pd.notna(movie['critics % score']) else 0
card = dmc.Card(
[
dmc.Stack(
[
dmc.Group(
[
dmc.Title(movie['film'], order=5, lineClamp=1),
dmc.Badge(
movie['category'].replace('Marvel Cinematic Universe', 'MCU'),
size="sm",
variant="light"
)
],
justify="space-between",
align="flex-start"
),
dmc.Group(
[
dmc.Stack(
[
dmc.Text("Revenue", size="xs", c="dimmed"),
dmc.Text(f"${movie['worldwide gross']:.0f}M", fw=600)
],
gap=2
),
dmc.Stack(
[
dmc.Text("ROI", size="xs", c="dimmed"),
dmc.Badge(
f"{movie['roi']:.0f}%",
color=roi_color,
variant="light"
)
],
gap=2
),
dmc.Stack(
[
dmc.Text("Year", size="xs", c="dimmed"),
dmc.Text(str(movie['year']), fw=600)
],
gap=2
)
],
grow=True
),
dmc.Divider(variant="dashed"),
dmc.Group(
[
dmc.RingProgress(
sections=[
{"value": movie['critics % score'] or 0, "color": "blue.6"},
{"value": movie['audience % score'] or 0, "color": "red.6"}
],
label=dmc.Stack(
[
dmc.Text(f"{movie['critics % score']:.0f}%" if pd.notna(
movie['critics % score']) else "N/A",
size="xs", ta="center"),
dmc.Text("Critics", size="xs", c="dimmed", ta="center")
],
gap=0
),
size=80,
thickness=8
),
dmc.Stack(
[
dmc.Text("Budget", size="xs", c="dimmed"),
dmc.Text(f"${movie['budget']:.0f}M", size="sm"),
dmc.Text("Opening", size="xs", c="dimmed", mt="xs"),
dmc.Text(f"${movie['opening weekend ($m)']:.0f}M", size="sm")
],
gap=2
)
],
justify="space-between"
)
],
gap="sm"
)
],
p="md",
h=280
)
cards.append(card)
return dmc.SimpleGrid(
cards,
cols={"base": 1, "sm": 2, "lg": 3, "xl": 4},
spacing="lg"
)
if __name__ == "__main__":
app.run(debug=True, port=3332)