import pandas as pd
import dash
from dash import dcc, html, Input, Output, State, callback
import dash_mantine_components as dmc
from dash_iconify import DashIconify
import plotly.graph_objects as go
# Load and clean data
marvel = pd.read_csv('https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-22/Marvel-Movies.csv')
marvel = marvel.drop(columns=['source', 'domestic gross ($m)', 'international gross ($m)'])
# Clean data and assign correct dtype
marvel['% budget recovered'] = marvel['% budget recovered'].str.replace('%', '').astype(float)
marvel['critics % score'] = marvel['critics % score'].str.replace('%', '').astype(int)
marvel['audience % score'] = marvel['audience % score'].str.replace('%', '').astype(int)
marvel['audience vs critics % deviance'] = marvel['audience vs critics % deviance'].str.replace('%', '').astype(int)
marvel['1st vs 2nd weekend drop off'] = marvel['1st vs 2nd weekend drop off'].str.replace('%', '').astype(int)
marvel['% budget opening weekend'] = marvel['% budget opening weekend'].str.replace('%', '').astype(float)
# Get unique categories
categories = ["All categories"] + sorted(marvel['category'].unique().tolist())
# Initialize the Dash app
app = dash.Dash(__name__)
# Define theme
theme = {
"primaryColor": "indigo",
"colors": {
"dark": ["#C1C2C5", "#A6A7AB", "#909296", "#5c5f66", "#373A40", "#2C2E33", "#25262b", "#1A1B1E", "#141517", "#101113"],
"gray": ["#f8f9fa", "#f1f3f5", "#e9ecef", "#dee2e6", "#ced4da", "#adb5bd", "#868e96", "#495057", "#343a40", "#212529"],
},
"fontFamily": "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
"headings": {
"fontFamily": "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
"fontWeight": 700,
"sizes": {
"h1": {"fontSize": "2rem", "lineHeight": 1.3},
"h2": {"fontSize": "1.5rem", "lineHeight": 1.35},
"h3": {"fontSize": "1.25rem", "lineHeight": 1.4},
"h4": {"fontSize": "1.125rem", "lineHeight": 1.45},
}
},
"spacing": {"xs": "0.5rem", "sm": "1rem", "md": "1.5rem", "lg": "2rem", "xl": "3rem"},
"defaultRadius": "md",
"radius": {"sm": "0.25rem", "md": "0.5rem", "lg": "1rem"},
"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)"
},
}
# App layout
app.layout = dmc.MantineProvider(
theme=theme,
forceColorScheme="light", # Default to dark mode
children=[
dmc.AppShell(
header={"height": 80},
padding="md",
children=[
dmc.AppShellHeader(
children=[
dmc.Group(
justify="space-between",
h={
"sm": 70,
"base": 80
},
px="md",
children=[
dmc.Group(
gap="xs",
children=[
dmc.ThemeIcon(
DashIconify(icon="tabler:brand-marvel", width=30),
size=40,
radius="md",
variant="gradient",
gradient={"from": "red", "to": "blue", "deg": 45}
),
dmc.Title(
"Marvel Movies Analysis",
order=1,
size={
"sm": "h3",
"base": "h2"
}
)
]
),
dmc.Switch(
id="theme-switch",
size="lg",
onLabel=DashIconify(icon="tabler:sun", width=16),
offLabel=DashIconify(icon="tabler:moon", width=16),
checked=False # False = dark mode (default)
)
]
)
]
),
dmc.AppShellMain(
children=[
dmc.Container(
size="xl",
children=[
dmc.Stack(
gap="lg",
children=[
# Filter section
dmc.Paper(
shadow="sm",
p="md",
radius="md",
withBorder=True,
children=[
dmc.Group(
align="flex-end",
children=[
dmc.Select(
id="category-filter",
label="Select Category",
data=categories,
value="All categories",
w={
"base": "100%",
"sm": 300
},
leftSection=DashIconify(icon="tabler:filter", width=16),
clearable=False
)
]
)
]
),
# Chart section
dmc.Paper(
shadow="md",
p="lg",
radius="md",
withBorder=True,
children=[
dmc.Stack(
gap="md",
children=[
dmc.Title(
"Budget Recovery Performance Over Years",
order=2,
size="h3"
),
dcc.Graph(
id="budget-scatter",
config={"displayModeBar": True, "displaylogo": False},
style={"height": "500px"}
)
]
)
]
)
]
)
]
)
]
)
]
)
]
)
# Callback for theme switching
@callback(
Output("theme-switch", "checked"),
Input("theme-switch", "checked"),
State("theme-switch", "checked")
)
def update_theme(checked, current_state):
return checked
# Update the MantineProvider forceColorScheme based on switch
app.clientside_callback(
"""
function(checked) {
const colorScheme = checked ? 'light' : 'dark';
document.documentElement.setAttribute('data-mantine-color-scheme', colorScheme);
return window.dash_clientside.no_update;
}
""",
Output("theme-switch", "id"), # Dummy output
Input("theme-switch", "checked")
)
# Callback for updating the scatter plot
@callback(
Output("budget-scatter", "figure"),
Input("category-filter", "value")
)
def update_scatter(selected_category):
# Filter data based on selection
if selected_category == "All categories":
filtered_df = marvel.copy()
else:
filtered_df = marvel[marvel['category'] == selected_category].copy()
# Assign colors based on % budget recovered
def get_color(value):
if value < 200:
return "#ff922b" # red
elif value > 300:
return "#40c057" # blue
else:
return "#868e96" # purple
filtered_df['color'] = filtered_df['% budget recovered'].apply(get_color)
# Create the scatter plot
fig = go.Figure()
# Group by color to create separate traces for the legend
for color, color_name in [(
"#ff922b", "< 200% recovered"
), (
"#40c057", "> 300% recovered"
), (
"#868e96", "200-300% recovered"
)]:
color_df = filtered_df[filtered_df['color'] == color]
if not color_df.empty:
fig.add_trace(
go.Scatter(
x=color_df['year'],
y=color_df['% budget recovered'],
mode='markers',
marker=dict(
color=color,
size=12,
line=dict(width=1, color=color)
),
name=color_name,
customdata=color_df[['film', 'category', 'worldwide gross', 'budget']],
hovertemplate=(
"<b>%{customdata[0]}</b><br>" +
"Category: %{customdata[1]}<br>" +
"Worldwide Gross: $%{customdata[2]}M<br>" +
"Budget: $%{customdata[3]}M<br>" +
"Budget Recovered: %{y:.1f}%<br>" +
"Year: %{x}<br>" +
"<extra></extra>"
)
)
)
# Update layout
fig.update_layout(
xaxis_title="Year",
yaxis_title="% Budget Recovered",
hovermode='closest',
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
font=dict(family=theme["fontFamily"]),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
margin=dict(l=60, r=20, t=60, b=60),
xaxis=dict(
gridcolor='rgba(128,128,128,0.2)',
showgrid=True,
zeroline=False,
tickmode='linear',
tick0=filtered_df['year'].min() if not filtered_df.empty else 2000,
dtick=2
),
yaxis=dict(
gridcolor='rgba(128,128,128,0.2)',
showgrid=True,
zeroline=True,
zerolinecolor='rgba(128,128,128,0.3)'
)
)
# Add a reference line at 100% (break-even)
fig.add_hline(
y=200,
line_dash="dash",
line_color="rgba(128,128,128,0.5)",
annotation_text="200",
annotation_position="right"
)
fig.add_hline(
y=300,
line_dash="dash",
line_color="rgba(128,128,128,0.5)",
annotation_text="300",
annotation_position="right"
)
return fig
if __name__ == "__main__":
app.run(debug=True)