import dash
from dash import dcc, html, Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd
import random
# Cargar datos
df = pd.read_csv("pixar_data.csv", parse_dates=['release_date']).drop(['Unnamed: 0', 'number'], axis=1)
# Definir clusters y colores
CLUSTER_INFO = {
"Box Office Titans": {
"description": "High commercial success with balanced audience/critic reception.",
"color": "#FF6B6B" # Coral red
},
"Pixar Underdogs": {
"description": "Films that performed more modestly commercially and critically.",
"color": "#4ECDC4" # Teal
},
"Critics/Users Favorites": {
"description": "Strong audience/critic acclaim with moderate box office results.",
"color": "#FFD166" # Golden yellow
}
}
CLUSTER_ORDER = ["Box Office Titans", "Pixar Underdogs", "Critics/Users Favorites"]
# Datos trivia
PIXAR_TRIVIA = [
"Toy Story was the first feature-length film created entirely by computer animation.",
"Finding Nemo won the Academy Award for Best Animated Feature in 2004.",
"Luxo Jr. was the first 3D computer-animated film nominated for an Academy Award.",
"Monsters, Inc. was almost called 'Monsters in the Closet' during development.",
"Pixar's Up was the first animated film to open the Cannes Film Festival.",
"The Pizza Planet truck has made a cameo in nearly every Pixar movie since Toy Story.",
"The code 'A113' in Pixar movies refers to a classroom at the California Institute of the Arts.",
"Pixar created a new algorithm to animate water for Finding Nemo, a landmark in animation technology.",
"Ratatouille's animators worked in real kitchens to study chefs' movements and cooking styles.",
"Wall-E's name is an homage to Walt Disney: 'Waste Allocation Load Lifter: Earth-Class.'"
]
# Inicializar app
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SKETCHY, 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css'])
app.title = "Pixar Films"
# Función para estilizar gráficos
def style_figure(fig):
fig.update_layout(
plot_bgcolor='rgba(240, 240, 240, 0.5)',
paper_bgcolor='white',
font=dict(family='Nunito, sans-serif', size=14),
margin=dict(l=40, r=40, t=40, b=60),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5, font=dict(size=16)),
xaxis=dict(showgrid=True, gridcolor='rgba(220, 220, 220, 0.8)', tickfont=dict(size=12), title_font=dict(size=16, color='#505050')),
yaxis=dict(showgrid=True, gridcolor='rgba(220, 220, 220, 0.8)', tickfont=dict(size=12), title_font=dict(size=16, color='#505050')),
hoverlabel=dict(bgcolor="white", font_size=14, font_family="Nunito, sans-serif", bordercolor="gray")
)
return fig
# Componentes de UI reutilizables
def create_header():
return html.Div([
html.Div([
html.H2("Pixar Films Analytics", className="display-4 fw-bold"),
html.P("Exploring the numbers behind the magic", className="lead")
], className="container py-4"),
html.Div([
html.I(className="fas fa-lightbulb me-2"),
html.I(className="fa-solid fa-robot me-2"),
html.I(className="fas fa-car me-2")
], className="d-flex justify-content-end align-items-center")
], className="bg-primary text-white mb-4 d-flex justify-content-between")
def create_sidebar():
return html.Div([
dbc.Card([
dbc.CardHeader(html.H4("Data Visualizations", className="text-primary fw-bold"), id='data-visualizations'),
dbc.CardBody([
# Botones principales
html.Div([
dbc.Button([html.I(className="fas fa-chart-pie me-2"), "Pixar Film Groupings"],
id="btn-cluster-overview", color="primary", outline=True, className="mb-2 w-100 text-start shadow-sm"),
dbc.Button([html.I(className="fas fa-chart-line me-2"), "Film Revenue Over Time"],
id="btn-trend-metrics", color="primary", outline=True, className="mb-2 w-100 text-start shadow-sm"),
# Opciones de tendencias (inicialmente ocultas)
html.Div([
html.Div(className="ps-3 mt-2 mb-3 border-start border-3 border-primary", children=[
dbc.Button("Worldwide Revenue", id="btn-worldwide", color="link", className="text-decoration-none d-block text-start ps-2 py-2 fw-normal"),
dbc.Button("US/Canada Revenue", id="btn-us-canada", color="link", className="text-decoration-none d-block text-start ps-2 py-2 fw-normal"),
dbc.Button("International Revenue", id="btn-international", color="link", className="text-decoration-none d-block text-start ps-2 py-2 fw-normal")
])
], id="trend-options", style={"display": "none"}),
dbc.Button([html.I(className="fas fa-chart-bar me-2"), "Performance Indicators"],
id="btn-film-metrics", color="primary", outline=True, className="mb-2 w-100 text-start shadow-sm"),
# Opciones de métricas (inicialmente ocultas)
html.Div([
html.Div(className="ps-3 mt-2 mb-3 border-start border-3 border-primary", children=[
dbc.Button("Revenue", id="btn-box-office", color="link", className="text-decoration-none d-block text-start ps-2 py-2 fw-normal"),
dbc.Button("Return on Investment", id="btn-roi", color="link", className="text-decoration-none d-block text-start ps-2 py-2 fw-normal"),
dbc.Button("Profit", id="btn-profit", color="link", className="text-decoration-none d-block text-start ps-2 py-2 fw-normal")
])
], id="metrics-options", style={"display": "none"}),
dbc.Button([html.I(className="fas fa-clock me-2"), "Pixar Film Timeline"],
id="btn-movie-timeline", color="primary", outline=True, className="mb-2 w-100 text-start shadow-sm"),
], className="mb-4"),
html.Hr(),
# Sección de trivia
html.Div([
html.H5("Pixar Trivia", className="text-primary mb-3 fw-bold"),
dbc.Button("Random Fact", id='trivia-button', color="primary", className="w-100 shadow-sm"),
html.Div(id='trivia-section', className="p-3 mt-3 border rounded bg-light shadow-sm")
])
])
], className="sticky-top shadow-sm"),
dbc.Tooltip("To see the charts, click on the information that interests you.",
target="data-visualizations", placement="right", className="fw-normal text-muted")
], style={'position': 'sticky', 'top': '0', 'height': '100vh', 'overflowY': 'auto'})
# Layout
app.layout = html.Div([
create_header(),
dbc.Container([
dbc.Row([
# Sidebar para controles
dbc.Col(create_sidebar(), width=3, className="mb-4"),
# Área principal de contenido
dbc.Col([
html.H3(id="graph-title", className="mb-4 text-primary fw-bold"),
html.Div(id="content-area", className="mb-4 shadow-sm")
], width=9)
])
], fluid=True),
# Footer
html.Footer([
html.Div("© 2025 Pixar Films Analysis Dashboard", className="text-center py-3")
], className="bg-light mt-4 border-top")
], style={'backgroundColor': '#F8F9FA', 'fontFamily': '"Nunito", sans-serif', 'boxShadow': '0 4px 6px rgba(0, 0, 0, 0.1)', 'transition': 'all 0.3s ease'})
# Funciones para generar gráficos
def generate_cluster_overview():
# Crear tarjetas para cada cluster
cluster_cards = []
for cluster_name in CLUSTER_ORDER:
cluster_df = df[df['model1_clusters'] == cluster_name]
# Calcular métricas
films_count = len(cluster_df['film'].unique())
avg_boxoffice = cluster_df['box_office_worldwide'].mean() / 1e6
avg_imdb = cluster_df['imdb_score'].mean()
top_film = cluster_df.loc[cluster_df['box_office_worldwide'].idxmax()]['film'] if len(cluster_df) > 0 else "N/A"
# Crear tarjeta
card = dbc.Col(
dbc.Card([
dbc.CardHeader(
html.H5(cluster_name, className="fw-bold m-0"),
style={"backgroundColor": CLUSTER_INFO[cluster_name]["color"], "color": "#212529"}
),
dbc.CardBody([
html.P(CLUSTER_INFO[cluster_name]["description"], className="mb-4"),
html.Div([
html.Div([
html.H3(f"{films_count}", className="fw-bold text-primary text-center mb-1"),
html.P("Films", className="text-muted text-center small")
], className="col"),
html.Div([
html.H3(f"${avg_boxoffice:.1f}M", className="fw-bold text-primary text-center mb-1"),
html.P("Avg. Revenue", className="text-muted text-center small")
], className="col"),
html.Div([
html.H3(f"{avg_imdb:.1f}/10", className="fw-bold text-primary text-center mb-1"),
html.P("Avg. IMDB", className="text-muted text-center small")
], className="col")
], className="row mb-3"),
html.Div(className="border-top pt-3 mt-2", children=[
html.P([html.Span("Top performer: ", className="text-muted"),
html.Span(f"{top_film}", className="fw-bold")], className="text-center mb-0")
])
])
], className="h-100 shadow"),
width=4,
className="mb-4"
)
cluster_cards.append(card)
# Crear scatter plot
scatter_fig = px.scatter(
df,
x='profit',
y='imdb_score',
color='model1_clusters',
size='box_office_worldwide',
size_max=30,
category_orders={'model1_clusters': CLUSTER_ORDER},
color_discrete_map={name: info["color"] for name, info in CLUSTER_INFO.items()},
labels={
'profit': 'Profit ($)',
'imdb_score': 'IMDb Score',
'model1_clusters': 'Cluster Group',
'box_office_worldwide': 'World Wide Revenue ($)',
},
hover_name='film',
hover_data={
'film': False,
'profit': ':.2s',
'imdb_score': True,
'box_office_worldwide': ':.3s',
'model1_clusters': False
}
)
scatter_fig.update_traces(
marker=dict(opacity=0.85, line=dict(width=1, color='white'))
)
scatter_fig = style_figure(scatter_fig)
scatter_fig.update_layout(legend_title_text='Cluster Groups', legend_title_font=dict(size=16))
scatter_card = dbc.Card([
dbc.CardBody([
dcc.Graph(figure=scatter_fig, style={"height": "600px"})
])
], className="shadow")
return html.Div([dbc.Row(cluster_cards, className="g-3 mb-4"), scatter_card])
def generate_trend_chart(metric_key):
metric_labels = {
'box_office_worldwide': 'Worldwide Revenue ($)',
'box_office_us_canada': 'US/Canada Revenue ($)',
'box_office_other': 'International Revenue ($)'
}
trend_df = df.drop_duplicates(subset='film').sort_values(by='release_year')
fig = px.line(
trend_df,
x='release_year',
y=metric_key,
color='model1_clusters',
category_orders={'model1_clusters': CLUSTER_ORDER},
color_discrete_map={name: info["color"] for name, info in CLUSTER_INFO.items()},
line_shape='spline',
markers=True,
labels={
'release_year': 'Release Year',
metric_key: metric_labels[metric_key],
'model1_clusters': 'Cluster Group'
},
hover_name='film',
hover_data={
'film': False,
'release_year': True,
metric_key: ':.2s',
'model1_clusters': False
}
)
fig.update_traces(
line=dict(width=4),
marker=dict(size=12, line=dict(width=1, color='white'))
)
fig = style_figure(fig)
fig.update_layout(legend_title_text='Cluster Groups', legend_title_font=dict(size=16))
return dbc.Card(dbc.CardBody([
dcc.Graph(figure=fig, style={"height": "600px"})
]), className="shadow")
def generate_performance_chart(metric_key):
metric_labels = {
'box_office_worldwide': 'Worldwide Revenue ($)',
'roi': 'Return on Investment (%)',
'profit': 'Profit ($)'
}
df_plot = df.drop_duplicates(subset='film').sort_values(by=metric_key, ascending=False)
fig = px.bar(
df_plot,
x='film',
y=metric_key,
color='model1_clusters',
category_orders={'model1_clusters': CLUSTER_ORDER},
color_discrete_map={name: info["color"] for name, info in CLUSTER_INFO.items()},
labels={
'film': 'Movie',
metric_key: metric_labels[metric_key],
'model1_clusters': 'Cluster Group',
'release_year': 'Release Year',
'imdb_score': 'Imdb Score'
},
hover_data={
'release_year': True,
metric_key: ':.2s',
'imdb_score': True,
'model1_clusters': False,
'film': False
}
)
fig.update_traces(
marker=dict(line=dict(width=1, color='white'), opacity=0.9),
width=0.7
)
fig = style_figure(fig)
fig.update_xaxes(tickangle=45, tickfont=dict(size=12))
fig.update_layout(legend_title_text='Cluster Groups', legend_title_font=dict(size=16))
return dbc.Card(dbc.CardBody([
dcc.Graph(figure=fig, style={"height": "600px"})
]), className="shadow")
def generate_timeline():
fig = px.scatter(
df,
x='release_year',
y='imdb_score',
size='box_office_worldwide',
size_max=35,
color='model1_clusters',
category_orders={'model1_clusters': CLUSTER_ORDER},
color_discrete_map={name: info["color"] for name, info in CLUSTER_INFO.items()},
labels={
'release_year': 'Release Year',
'box_office_worldwide': 'World Wide Revenue ($)',
'imdb_score': 'IMDB Score',
'model1_clusters': 'Cluster Group'
},
hover_name='film',
hover_data={
'release_year': True,
'box_office_worldwide': ':.3s',
'imdb_score': True,
'film': False,
'model1_clusters': False
}
)
fig.update_traces(marker=dict(opacity=0.9), line=dict(width=1.5, color='white'))
fig = style_figure(fig)
fig.update_layout(legend_title_text='Cluster Groups', legend_title_font=dict(size=16))
return dbc.Card(dbc.CardBody([
dcc.Graph(figure=fig, style={"height": "600px"})
]), className="shadow")
# Callbacks simplificados
@app.callback(
[Output("trend-options", "style"), Output("metrics-options", "style"),
Output("btn-cluster-overview", "color"), Output("btn-trend-metrics", "color"),
Output("btn-film-metrics", "color"), Output("btn-movie-timeline", "color"),
Output("btn-cluster-overview", "outline"), Output("btn-trend-metrics", "outline"),
Output("btn-film-metrics", "outline"), Output("btn-movie-timeline", "outline")],
[Input("btn-trend-metrics", "n_clicks"), Input("btn-film-metrics", "n_clicks"),
Input("btn-cluster-overview", "n_clicks"), Input("btn-movie-timeline", "n_clicks")],
prevent_initial_call=True
)
def toggle_options(n1, n2, n3, n4):
ctx = dash.callback_context
button_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
# Valores por defecto
trend_style = {"display": "none"}
metrics_style = {"display": "none"}
colors = ["primary"] * 4
outlines = [True] * 4
# Configurar según el botón presionado
if button_id == "btn-trend-metrics":
trend_style = {"display": "block"}
colors[1] = "primary"
outlines[1] = False
elif button_id == "btn-film-metrics":
metrics_style = {"display": "block"}
colors[2] = "primary"
outlines[2] = False
elif button_id == "btn-cluster-overview":
colors[0] = "primary"
outlines[0] = False
elif button_id == "btn-movie-timeline":
colors[3] = "primary"
outlines[3] = False
return trend_style, metrics_style, *colors, *outlines
@app.callback(
Output('graph-title', 'children'),
[Input('btn-cluster-overview', 'n_clicks'), Input('btn-trend-metrics', 'n_clicks'),
Input('btn-worldwide', 'n_clicks'), Input('btn-us-canada', 'n_clicks'),
Input('btn-international', 'n_clicks'), Input('btn-film-metrics', 'n_clicks'),
Input('btn-box-office', 'n_clicks'), Input('btn-roi', 'n_clicks'),
Input('btn-profit', 'n_clicks'), Input('btn-movie-timeline', 'n_clicks')],
prevent_initial_call=True
)
def update_graph_title(n1, n2, n3, n4, n5, n6, n7, n8, n9, n10):
ctx = dash.callback_context
button_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
titles = {
'btn-cluster-overview': 'Pixar Film Grouping Overview',
'btn-trend-metrics': 'Revenue Trends Over Time',
'btn-worldwide': 'Worldwide Revenue Trends',
'btn-us-canada': 'US/Canada Revenue Trends',
'btn-international': 'International Box Revenue Trends',
'btn-film-metrics': 'Film Performance Analysis',
'btn-box-office': 'Revenue Performance',
'btn-roi': 'Return on Investment Analysis',
'btn-profit': 'Profit Analysis',
'btn-movie-timeline': 'Pixar Movies Timeline'
}
return titles.get(button_id, 'Pixar Films Analytics')
@app.callback(
Output('trivia-section', 'children'),
Input('trivia-button', 'n_clicks')
)
def update_trivia(n_clicks):
if n_clicks:
return html.P(random.choice(PIXAR_TRIVIA), className="mb-0 fst-italic")
return html.P("Click for a fun Pixar fact!", className="mb-0 text-muted")
@app.callback(
Output('content-area', 'children'),
[Input('btn-cluster-overview', 'n_clicks'), Input('btn-worldwide', 'n_clicks'),
Input('btn-us-canada', 'n_clicks'), Input('btn-international', 'n_clicks'),
Input('btn-box-office', 'n_clicks'), Input('btn-roi', 'n_clicks'),
Input('btn-profit', 'n_clicks'), Input('btn-movie-timeline', 'n_clicks')],
prevent_initial_call=True
)
def update_content(n1, n2, n3, n4, n5, n6, n7, n8):
ctx = dash.callback_context
button_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
# Mapeo de botones a funciones y parámetros
content_mapping = {
'btn-cluster-overview': generate_cluster_overview,
'btn-worldwide': lambda: generate_trend_chart('box_office_worldwide'),
'btn-us-canada': lambda: generate_trend_chart('box_office_us_canada'),
'btn-international': lambda: generate_trend_chart('box_office_other'),
'btn-box-office': lambda: generate_performance_chart('box_office_worldwide'),
'btn-roi': lambda: generate_performance_chart('roi'),
'btn-profit': lambda: generate_performance_chart('profit'),
'btn-movie-timeline': generate_timeline
}
if button_id in content_mapping:
return content_mapping[button_id]()
return html.Div("Select a visualization using the buttons", className="text-center text-muted my-5 py-5")