The code
import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
CSS for the background animation
custom_css = ââ"
@keyframes gradientAnimation {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animated-background {
background: linear-gradient(135deg, #1DB954, #191414, #4ECDC4, #FF6B35);
background-size: 400% 400%;
animation: gradientAnimation 15s ease infinite; /* 15s duration, subtle and infinite movement */
}
ââ"
app = dash.Dash(name, external_stylesheets=[dbc.themes.MINTY],
suppress_callback_exceptions=True)
app.title=âBillboard Hot 100 Dashboardâ
The custom CSS
app.index_string = f"ââ
{{%metas%}}
{{%title%}}
{{%favicon%}}
{{%css%}}
{custom_css}
{{%app_entry%}}
{{%config%}}
{{%scripts%}}
{{%renderer%}}
"""
Consistent color palette for artists
ARTIST_COLORS = [â#1DB954â, â#FF6B35â, â#4ECDC4â] # Spotify Green, Orange, Turquoise
â DATA LOGIC â
def correct_two_digit_year(dt):
if pd.isna(dt): return dt
if dt.year > 2050: return dt - pd.DateOffset(years=100)
return dt
def min_max_normalize(series, min_val=None, max_val=None):
series = pd.to_numeric(series, errors=âcoerceâ).dropna()
if series.empty: return pd.Series([0] * len(series.index), index=series.index)
if min_val is None: min_val = series.min()
if max_val is None: max_val = series.max()
if max_val == min_val: return (series * 0) + 50
return ((series - min_val) / (max_val - min_val)) * 100
def safe_mode(series):
series = series.dropna()
if series.empty: return âN/Aâ
mode_result = series.mode()
if mode_result.empty: return âN/Aâ
return mode_result.iloc[0]
Load data
try:
df = pd.read_csv(âBillboard Hot 100 Number Ones Database - Data.csvâ)
df[âDateâ] = pd.to_datetime(df[âDateâ], errors=âcoerceâ)
valid_dates = df[âDateâ].dropna()
df.loc[valid_dates.index, âDateâ] = valid_dates.apply(correct_two_digit_year)
except Exception as e:
print(f"Error loading data: {e}")
METRIC_Y = âWeeks at Number Oneâ
AUDIO_PARAMS = [âEnergyâ, âDanceabilityâ, âHappinessâ, âAcousticnessâ, âLoudness (dB)â]
DISPLAY_PARAMS = [âEnergyâ, âDanceabilityâ, âHappinessâ, âAcousticnessâ, âLoudness_Normâ]
ALL_REQUIRED_COLS = AUDIO_PARAMS + [âSongâ, âArtistâ, âYearâ, METRIC_Y, âOverall Ratingâ, âCDR Genreâ, âArtist is a Songwriterâ]
BINARY_FEATURES = {
âArtist Whiteâ: âArtist is Whiteâ,
âArtist Blackâ: âArtist is Blackâ,
âProducer Maleâ: âProducer is Maleâ,
âArtist is a Songwriterâ: âArtist is a Songwriterâ,
âVocally Basedâ: âVocally Basedâ,
âFalsetto Vocalâ: âIncludes Falsetto Vocalâ,
âCowbellâ: âIncludes Cowbellâ
}
BINARY_COLS = list(BINARY_FEATURES.keys())
df[âYearâ] = df[âDateâ].dt.year
Include a check for the existence of âdfâ in case the load fails
if âdfâ in locals():
df_clean = df.dropna(subset=ALL_REQUIRED_COLS).copy()
df_clean[âLoudness_Normâ] = min_max_normalize(df_clean[âLoudness (dB)â])
artist_options = sorted(df_clean[âArtistâ].unique())
dropdown_options = [{âlabelâ: artist, âvalueâ: artist} for artist in artist_options]
else:
# Error handling if df is not loaded
df_clean = pd.DataFrame(columns=ALL_REQUIRED_COLS + [âLoudness_Normâ, âYearâ, âDateâ])
artist_options =
dropdown_options =
â DEFAULT STATE LOGIC â
Define a default selection (must exist in artist_options)
DEFAULT_ARTISTS = [âThe Beatlesâ, âElvis Presleyâ, âMadonnaâ]
INITIAL_SELECTION = [artist for artist in DEFAULT_ARTISTS if artist in artist_options]
if not INITIAL_SELECTION and len(artist_options) >= 3:
INITIAL_SELECTION = artist_options[:3]
elif not INITIAL_SELECTION and artist_options:
INITIAL_SELECTION = artist_options[:1]
â APP LAYOUT â
app.layout = html.Div([
# Container with animated gradient background
dbc.Container([
# 1. Title and Descriptor
dbc.Row([
dbc.Col([
html.Div([
html.H1("đż Billboard Hot 100: Comparative Elite Analysis",
style={'fontWeight': '900', 'fontSize': '2.8rem',
'color': '#1DB954',
'textShadow': '2px 2px 4px rgba(0,0,0,0.3)',
'marginBottom': '20px'}), # Increased bottom margin
], style={'textAlign': 'center', 'padding': '30px 0 20px 0'}) # Increased top/bottom padding
])
], className="dbc-row"),
# 2. Artist Selector
dbc.Row([
# Column 1: Artist Selector (small)
dbc.Col([
html.H4("đľ Artists to Compare (Max 3)", className="card-title", style={'color': '#1DB954','marginBottom': '15px'}),
# Button Group for Selection and Reset
html.Div([
dbc.Button("Open Artist Selector", id="open-modal-button",
color="success", className="me-2",
style={'fontWeight': 'bold', 'backgroundColor': '#1DB954', 'border': 'none'}),
# --- RESET/CLEAR BUTTON ---
dbc.Button("Clear Selection", id="reset-button",
color="secondary", className="mb-3",
style={'fontWeight': 'bold', 'border': 'none', 'backgroundColor': '#6c757d'}),
], className="d-flex mb-3"),
html.Div(id='artist-warning-text', style={'color': '#ff6b6b', 'marginBottom': '10px'})
], width=12, md=4, className="mb-4"),
# Column 2: Dashboard Descriptive Text
dbc.Col([
html.Div([
html.P([
html.Strong("Analyze the Success Formula:", style={'color': '#1DB954', 'fontWeight': 'bold'}),
" đ Unlock the Code to Chart Dominance. Every legendary artist has a unique Success Formula. "
"This tool moves beyond listening to reveal the Elite Blueprint behind their biggest hits. ",
"Compare the intensity of their reign, analyze their unique Sonic Signature (the balance of Energy, Danceability, "
"and that critical, competitive Loudness), and discover the precise production traits that consistently defined a number one sound."
], style={'fontSize': '1.0rem', 'color': '#333', 'margin': '0'})
], style={'paddingLeft': '10px', 'borderLeft': '3px solid #1DB954'})
], width=12, md=8, className="mb-4 d-none d-md-block"),
dcc.Store(id='artists-to-compare', data=INITIAL_SELECTION)
], className="g-3 mb-4 dbc-row align-items-center",
style={'backgroundColor': 'rgba(255, 255, 255, 0.95)', 'padding': '15px', 'borderRadius': '12px', 'boxShadow': '0 4px 6px rgba(0,0,0,0.1)'}),
# MODAL FOR SELECTION
dbc.Modal(
[
dbc.ModalHeader(dbc.ModalTitle("Select Artists to Compare (Max 3)")),
dbc.ModalBody(
dcc.Dropdown(
id='artist-checklist',
options=dropdown_options,
value=INITIAL_SELECTION,
multi=True,
placeholder="Select up to 3 artists...",
style={'color': '#333', 'minHeight': '150px', 'fontSize': '1.05rem','zIndex': '9999'},
optionHeight=35
)
),
dbc.ModalFooter(
dbc.Button("Apply and Close", id="close-modal-button", className="ms-auto", n_clicks=0,
style={'backgroundColor': '#1DB954', 'border': 'none'})
),
],
id="modal-artists",
is_open=False,
size="lg",
scrollable=True,
),
# 3. Charts - EQUAL WIDTHS
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader(html.H4("đ Historical Success: Weeks at #1 by Year", className="mb-0 text-dark")),
dbc.CardBody([
dcc.Graph(id='historical-chart', style={'height': '500px'}, config={'displayModeBar': False}),
], style={'padding': '15px'})
], style={'background': 'white', 'border': 'none', 'marginBottom': '15px', 'borderRadius': '12px', 'boxShadow': '0 4px 6px rgba(0,0,0,0.1)'})
], width=12, lg=6),
dbc.Col([
dbc.Card([
dbc.CardHeader(html.H4("⥠PRODUCTION FORMULA", className="mb-0 text-dark")),
dbc.CardBody([
dcc.Graph(id='production-chart', style={'height': '500px'}, config={'displayModeBar': False}),
], style={'padding': '15px'})
], style={'background': 'white', 'border': 'none', 'marginBottom': '15px', 'borderRadius': '12px', 'boxShadow': '0 4px 6px rgba(0,0,0,0.1)'})
], width=12, lg=6),
], style={'marginBottom': '20px'}, className="dbc-row"),
# 4. Summary Cards
dbc.Row(id='artist-summary-cards', className="g-3 mb-4 dbc-row"),
# 5. FOOTER
dbc.Row([
dbc.Col([
html.Div([
html.Hr(style={'borderColor': 'rgba(255,255,255,0.3)', 'margin': '20px 0'}),
html.Div([
html.P([
"đľ ",
html.Strong("đ Billboard Hot 100", style={'color': '#1DB954'}),
" | Interactive Hit Analysis Dashboard"
], style={'margin': '5px 0', 'color': '#FFFFFF'}),
html.P([
"Data courtesy of Chris Dalla Riva",
"đť Developed with ",
html.Span("Dash & Plotly | by Avacsiglo 21", style={'color': '#1DB954', 'fontWeight': 'bold'}),
f" | Š 2025"
], style={'margin': '5px 0', 'fontSize': '0.9rem', 'color': 'rgba(255,255,255,0.7)'}),
], style={'textAlign': 'center'})
])
], width=12)
], style={'marginTop': '40px', 'paddingBottom': '20px'})
], fluid=True, style={'padding': '15px'})
], className=âanimated-backgroundâ, style={
âminHeightâ: â100vhâ,
âmarginâ: â0â,
âpaddingâ: â0â
})
â CALLBACK LOGIC) â
@app.callback(
Output(âmodal-artistsâ, âis_openâ),
[Input(âopen-modal-buttonâ, ân_clicksâ), Input(âclose-modal-buttonâ, ân_clicksâ)],
[State(âmodal-artistsâ, âis_openâ)],
)
def toggle_modal(n_open, n_close, is_open):
if n_open or n_close:
return not is_open
return is_open
1. Reset/Clear Button Callback
@app.callback(
[Output(âartist-checklistâ, âvalueâ, allow_duplicate=True),
Output(âartists-to-compareâ, âdataâ, allow_duplicate=True),
Output(âartist-warning-textâ, âchildrenâ, allow_duplicate=True)],
[Input(âreset-buttonâ, ân_clicksâ)],
prevent_initial_call=True
)
def reset_selection(n_clicks):
if n_clicks and n_clicks > 0:
# Clear the selection by returning an empty list
return , , ââ
raise dash.exceptions.PreventUpdate
2. DropDown Selection Callback
@app.callback(
[Output(âartist-checklistâ, âvalueâ, allow_duplicate=True),
Output(âartists-to-compareâ, âdataâ, allow_duplicate=True),
Output(âartist-warning-textâ, âchildrenâ)],
[Input(âartist-checklistâ, âvalueâ)],
prevent_initial_call=True
)
def update_selection_and_store(selected_list):
warning = ââ
if selected_list is None:
raise dash.exceptions.PreventUpdate
if len(selected_list) > 3:
selected_list = selected_list[:3]
warning = html.Span("â Only a maximum of 3 artists can be selected.", style={'fontWeight': 'bold'})
return selected_list, selected_list, warning
@app.callback(
[Output(âhistorical-chartâ, âfigureâ),
Output(âproduction-chartâ, âfigureâ),
Output(âartist-summary-cardsâ, âchildrenâ)],
[Input(âartists-to-compareâ, âdataâ)]
)
def update_analysis(selected_artists):
# Check for empty selection or store data
if not selected_artists or len(selected_artists) == 0:
empty_msg = html.Div("Select 1 to 3 artists to activate the comparative analysis.",
style={'color': '#FFFFFF', 'textAlign': 'center', 'marginTop': '200px', 'fontSize': '1.2rem'})
# Create empty figures
fig_hist = go.Figure().update_layout(plot_bgcolor='white', paper_bgcolor='white', font=dict(color='#333'), margin=dict(l=20, r=20, t=20, b=20))
fig_prod = go.Figure().update_layout(plot_bgcolor='white', paper_bgcolor='white', font=dict(color='#333'))
return fig_hist, fig_prod, dbc.Col(empty_msg, width=12)
if df_clean.empty:
error_msg = html.Div("Error: Could not load data. Check the CSV file.",
style={'color': '#ff6b6b', 'textAlign': 'center', 'marginTop': '200px', 'fontSize': '1.2rem'})
fig_hist = go.Figure().update_layout(plot_bgcolor='white', paper_bgcolor='white', font=dict(color='#333'), margin=dict(l=20, r=20, t=20, b=20))
fig_prod = go.Figure().update_layout(plot_bgcolor='white', paper_bgcolor='white', font=dict(color='#333'))
return fig_hist, fig_prod, dbc.Col(error_msg, width=12)
artists_to_compare = selected_artists[:3]
df_filtered = df_clean[df_clean['Artist'].isin(artists_to_compare)].copy()
if df_filtered.empty:
empty_msg = html.Div(f"The selected artists have no valid hits in the clean dataset.",
style={'color': '#ff6b6b', 'textAlign': 'center', 'marginTop': '200px', 'fontSize': '1.2rem'})
fig_hist = go.Figure().update_layout(plot_bgcolor='white', paper_bgcolor='white', font=dict(color='#333'), margin=dict(l=20, r=20, t=20, b=20))
fig_prod = go.Figure().update_layout(plot_bgcolor='white', paper_bgcolor='white', font=dict(color='#333'))
return fig_hist, fig_prod, dbc.Col(empty_msg, width=12)
# Create consistent color mapping for both charts
color_map = {artist: ARTIST_COLORS[i % len(ARTIST_COLORS)] for i, artist in enumerate(artists_to_compare)}
# 1. DUMBBELL CHART HORIZONTAL - Period of Dominance
fig_hist = go.Figure()
# Sort artists by first hit year
artist_first_year = {artist: df_filtered[df_filtered['Artist'] == artist]['Year'].min()
for artist in artists_to_compare}
sorted_artists = sorted(artists_to_compare, key=lambda x: artist_first_year[x])
# Calculate metrics for each artist
artist_data = []
for artist in sorted_artists:
df_artist = df_filtered[df_filtered['Artist'] == artist]
first_year = df_artist['Year'].min()
last_year = df_artist['Year'].max()
total_weeks = df_artist[METRIC_Y].sum()
total_hits = len(df_artist)
artist_data.append({
'artist': artist,
'first_year': first_year,
'last_year': last_year,
'total_weeks': total_weeks,
'total_hits': total_hits,
'years_active': last_year - first_year + 1
})
# Create dumbbells (lines connecting start and end years)
for idx, data in enumerate(artist_data):
artist = data['artist']
# Line connecting first and last year
fig_hist.add_trace(go.Scatter(
x=[data['first_year'], data['last_year']],
y=[artist, artist],
mode='lines',
line=dict(color=color_map[artist], width=4),
showlegend=False,
hoverinfo='skip'
))
# Start point (first hit)
fig_hist.add_trace(go.Scatter(
x=[data['first_year']],
y=[artist],
mode='markers',
marker=dict(
size=12,
color='white',
line=dict(color=color_map[artist], width=3)
),
showlegend=False,
hovertemplate=f'<b>{artist}</b><br>' +
f'First Hit: {data["first_year"]:.0f}<br>' +
f'Total Hits: {data["total_hits"]}<br>' +
'<extra></extra>',
name=artist
))
# End point (last hit) - filled
fig_hist.add_trace(go.Scatter(
x=[data['last_year']],
y=[artist],
mode='markers',
marker=dict(
size=12,
color=color_map[artist]
),
showlegend=False,
hovertemplate=f'<b>{artist}</b><br>' +
f'Last Hit: {data["last_year"]:.0f}<br>' +
f'Total Weeks at #1: {data["total_weeks"]:.0f}<br>' +
'<extra></extra>',
name=artist
))
# Add individual hits as small markers along the line
df_artist = df_filtered[df_filtered['Artist'] == artist]
for _, row in df_artist.iterrows():
fig_hist.add_trace(go.Scatter(
x=[row['Year']],
y=[artist],
mode='markers',
marker=dict(
size=row[METRIC_Y] * 2, # Size based on weeks at #1
color=color_map[artist],
opacity=0.6,
line=dict(color='white', width=1),
sizemin=4
),
showlegend=False,
hovertemplate=f'<b>{row["Song"]}</b><br>' +
f'{artist}<br>' +
f'Year: {row["Year"]:.0f}<br>' +
f'Weeks at #1: {row[METRIC_Y]:.0f}<br>' +
f'Rating: {row["Overall Rating"]:.2f}<br>' +
'<extra></extra>',
name=artist
))
# Add legend manually
for artist in sorted_artists:
fig_hist.add_trace(go.Scatter(
x=[None],
y=[None],
mode='markers',
marker=dict(size=10, color=color_map[artist]),
name=artist,
showlegend=True
))
# Get year range
all_years = df_filtered['Year'].dropna()
min_year = int(all_years.min())
max_year = int(all_years.max())
fig_hist.update_layout(
plot_bgcolor='white',
paper_bgcolor='white',
font=dict(color='#333'),
xaxis=dict(
showgrid=True,
gridcolor='#e0e0e0',
title='Year',
title_font_color='#333',
tickfont_color='#333',
range=[min_year - 2, max_year + 2],
dtick=5,
tick0=int(min_year/5)*5
),
yaxis=dict(
showgrid=True,
gridcolor='#f0f0f0',
title='',
tickfont=dict(size=13, color='#333', family='Arial Black'),
categoryorder='array',
categoryarray=sorted_artists
),
legend=dict(
title='Artists',
orientation="h",
yanchor="bottom",
y=-0.2,
xanchor="center",
x=0.5
),
margin=dict(l=120, r=20, t=20, b=60),
height=500,
hovermode='closest'
)
# 2. Production Chart (Grouped Polar Bar) - WHITE BACKGROUND
df_radar = df_filtered.groupby('Artist')[DISPLAY_PARAMS].mean().reset_index()
categories = ['Energy', 'Danceability', 'Happiness', 'Acousticness', 'Loudness (Scaled)']
fig_prod = go.Figure()
num_artists = len(artists_to_compare)
num_categories = len(categories)
angle_per_category = 360 / num_categories
bar_width = (angle_per_category * 0.9) / num_artists
artist_indices = {artist: i for i, artist in enumerate(artists_to_compare)}
for i, row in df_radar.iterrows():
artist = row['Artist']
values = row[DISPLAY_PARAMS].tolist()
artist_idx = artist_indices[artist]
base_angles = np.linspace(0, 360, num_categories, endpoint=False)
center_adjustment = (num_artists - 1) * bar_width / 2
shifted_angles = base_angles + (artist_idx * bar_width) - center_adjustment
fig_prod.add_trace(go.Barpolar(
r=values,
theta=shifted_angles,
name=artist,
marker_color=color_map[artist],
opacity=0.8,
marker_line_width=2,
marker_line_color='white',
width=bar_width
))
fig_prod.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 110],
gridcolor='#BDBDBD',
tickfont=dict(color='#333'),
),
angularaxis=dict(
tickvals=np.linspace(0, 360, num_categories, endpoint=False),
ticktext=categories,
tickfont=dict(size=12, color='#333'),
gridcolor='#BDBDBD'
),
hole=0.25,
bgcolor='white'
),
paper_bgcolor='white',
plot_bgcolor='white',
font=dict(color='#333'),
legend=dict(
orientation="h",
yanchor="bottom",
y=-0.2,
xanchor="center",
x=0.5
),
margin=dict(l=40, r=40, t=20, b=60)
)
# 3. Improved Summary Cards
summary_cards = []
for i, artist in enumerate(artists_to_compare):
df_artist = df_clean[df_clean['Artist'] == artist]
# Basic metrics
total_hits = len(df_artist)
avg_weeks = df_artist[METRIC_Y].mean() if total_hits > 0 else 0
total_weeks = df_artist[METRIC_Y].sum() if total_hits > 0 else 0
avg_rating = df_artist['Overall Rating'].mean() if total_hits > 0 else 0
max_weeks = df_artist[METRIC_Y].max() if total_hits > 0 else 0
# Additional information
is_songwriter_mode = safe_mode(df_artist['Artist is a Songwriter'])
songwriter_text = "Yes" if is_songwriter_mode == 1 else "No" if is_songwriter_mode == 0 else "N/A"
dominant_genre = safe_mode(df_artist['CDR Genre'])
# Years of activity
first_year = df_artist['Year'].min()
last_year = df_artist['Year'].max()
years_active = last_year - first_year + 1 if total_hits > 1 else 1
# Most successful song
best_song_idx = df_artist[METRIC_Y].idxmax()
best_song = df_artist.loc[best_song_idx, 'Song'] if total_hits > 0 else "N/A"
best_song_weeks = df_artist.loc[best_song_idx, METRIC_Y] if total_hits > 0 else 0
# Average audio parameters
avg_energy = df_artist['Energy'].mean() if total_hits > 0 else 0
avg_danceability = df_artist['Danceability'].mean() if total_hits > 0 else 0
avg_loudness = df_artist['Loudness (dB)'].mean() if total_hits > 0 else 0
# Dominant traits
trait_scores = {}
for col in BINARY_COLS:
if col in df_artist.columns and df_artist[col].count() > 0:
score = df_artist[col].sum() / df_artist[col].count()
trait_scores[col] = score
sorted_traits = sorted(trait_scores.items(), key=lambda item: item[1], reverse=True)
trait_text = "Neutral Formula"
top_traits = []
for top_trait_key, top_trait_score in sorted_traits:
if top_trait_score >= 0.5:
top_trait_label = BINARY_FEATURES.get(top_trait_key, top_trait_key)
top_traits.append(f"⢠{top_trait_label} ({top_trait_score * 100:.0f}%)")
if len(top_traits) >= 2:
break
if top_traits:
trait_text = "\n".join(top_traits)
card_content = [
html.Div([
html.H4(artist, className="card-title text-center",
style={'color': color_map[artist], 'fontWeight': 'bold', 'marginBottom': '15px'}),
]),
# Success Section
html.Div([
html.H6("đ SUCCESS STATISTICS", style={'color': color_map[artist], 'fontWeight': 'bold', 'borderBottom': f'2px solid {color_map[artist]}', 'paddingBottom': '5px', 'marginBottom': '10px'}),
html.Div([
html.Span("Total #1 Hits: ", style={'fontWeight': 'bold'}),
html.Span(f"{total_hits}", style={'color': color_map[artist], 'fontSize': '1.1rem', 'fontWeight': 'bold'})
], style={'marginBottom': '5px'}),
html.Div([
html.Span("Total Weeks at #1: ", style={'fontWeight': 'bold'}),
html.Span(f"{total_weeks:.0f}", style={'color': color_map[artist], 'fontSize': '1.1rem', 'fontWeight': 'bold'})
], style={'marginBottom': '5px'}),
html.Div([
html.Span("Avg. Weeks/#1: ", style={'fontWeight': 'bold'}),
html.Span(f"{avg_weeks:.1f}", style={'color': '#333'})
], style={'marginBottom': '5px'}),
html.Div([
html.Span("Personal Record: ", style={'fontWeight': 'bold'}),
html.Span(f"{max_weeks:.0f} weeks", style={'color': '#333'})
], style={'marginBottom': '5px'}),
html.Div([
html.Span("Average Rating: ", style={'fontWeight': 'bold'}),
html.Span(f"â {avg_rating:.2f}/10", style={'color': '#333'})
], style={'marginBottom': '10px'}),
]),
# Most Successful Hit Section
html.Div([
html.H6("đ MOST SUCCESSFUL HIT", style={'color': color_map[artist], 'fontWeight': 'bold', 'borderBottom': f'2px solid {color_map[artist]}', 'paddingBottom': '5px', 'marginBottom': '10px'}),
html.Div([
html.Span(f'"{best_song}"', style={'fontStyle': 'italic', 'fontWeight': 'bold', 'color': '#333'}),
], style={'marginBottom': '5px'}),
html.Div([
html.Span(f"{best_song_weeks:.0f} weeks at #1", style={'color': '#666', 'fontSize': '0.9rem'})
], style={'marginBottom': '10px'}),
]),
# Period of Activity Section
html.Div([
html.H6("đ
PERIOD OF DOMINANCE", style={'color': color_map[artist], 'fontWeight': 'bold', 'borderBottom': f'2px solid {color_map[artist]}', 'paddingBottom': '5px', 'marginBottom': '10px'}),
html.Div([
html.Span(f"{first_year:.0f} - {last_year:.0f} ", style={'fontWeight': 'bold', 'color': '#333'}),
html.Span(f"({years_active:.0f} years)", style={'color': '#666', 'fontSize': '0.9rem'})
], style={'marginBottom': '10px'}),
]),
# Audio Characteristics Section
html.Div([
html.H6("đď¸ SONIC SIGNATURE", style={'color': color_map[artist], 'fontWeight': 'bold', 'borderBottom': f'2px solid {color_map[artist]}', 'paddingBottom': '5px', 'marginBottom': '10px'}),
html.Div([
html.Span("Average Energy: ", style={'fontWeight': 'bold'}),
html.Span(f"{avg_energy:.1f}%", style={'color': '#333'})
], style={'marginBottom': '5px'}),
html.Div([
html.Span("Average Danceability: ", style={'fontWeight': 'bold'}),
html.Span(f"{avg_danceability:.1f}%", style={'color': '#333'})
], style={'marginBottom': '5px'}),
html.Div([
html.Span("Average Loudness: ", style={'fontWeight': 'bold'}),
html.Span(f"{avg_loudness:.1f} dB", style={'color': '#333'})
], style={'marginBottom': '10px'}),
]),
# Signature Elements Section
html.Div([
html.H6("⨠SIGNATURE ELEMENTS", style={'color': color_map[artist], 'fontWeight': 'bold', 'borderBottom': f'2px solid {color_map[artist]}', 'paddingBottom': '5px', 'marginBottom': '10px'}),
html.Pre(trait_text, style={'fontSize': '0.85rem', 'color': '#333', 'marginBottom': '5px', 'whiteSpace': 'pre-line'}),
html.Div([
html.Span("Primary Genre: ", style={'fontWeight': 'bold'}),
html.Span(f"{dominant_genre}", style={'color': '#333'})
], style={'marginBottom': '5px'}),
html.Div([
html.Span("Songwriter: ", style={'fontWeight': 'bold'}),
html.Span(f"{songwriter_text}", style={'color': '#333'})
], style={'marginBottom': '5px'}),
]),
]
summary_cards.append(
dbc.Col(
dbc.Card(card_content,
style={
'background': 'rgba(255, 255, 255, 0.98)',
'border': f'3px solid {color_map[artist]}',
'borderRadius': '12px',
'boxShadow': f'0 6px 12px rgba(0,0,0,0.15), 0 0 20px {color_map[artist]}40',
'padding': '20px'
},
body=True),
width=12, lg=int(12/len(artists_to_compare)),
className="mb-3"
)
)
return fig_hist, fig_prod, summary_cards
server = app.server