import dash
from dash import dcc, html, Input, Output, callback, State
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
import dash_bootstrap_components as dbc
# Initialize the Dash app
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUMEN])
app.title = "NYC Traffic Fine Lottery"
# --- Load and Process Data Once at Startup ---
try:
df_fines = pd.read_csv('nyc_parkings_violation.csv') # Adjust this to your file name and path!
# Convert 'Issue Date' and 'Violation Time' to appropriate formats
df_fines['Issue Date'] = pd.to_datetime(df_fines['Issue Date'])
# Convert 'Violation Time' to hour for plotting (assumes HHMM or HH:MM format)
# Convert to string and pad with zeros to ensure HHMM format if needed
df_fines['Violation Time'] = df_fines['Violation Time'].astype(str).str.zfill(4)
df_fines['Violation Hour'] = df_fines['Violation Time'].apply(lambda x: int(x[:2]))
# Create 'Day of Week' column and set categorical order for consistent plotting
df_fines['Day of Week'] = df_fines['Issue Date'].dt.day_name()
day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
df_fines['Day of Week'] = pd.Categorical(df_fines['Day of Week'], categories=day_order, ordered=True)
except FileNotFoundError:
print("Error: 'nyc_parkings_violation.csv' not found. Ensure the file is in the correct path.")
# Create a minimal fallback DataFrame if the file isn't found
print("Using minimal fallback data.")
# Usando pandas para crear las fechas en lugar de datetime
dates = pd.date_range(start='2023-01-01', periods=12, freq='D')
df_fines = pd.DataFrame({
'Fine Amount': [50, 250, 100, 50, 250, 100, 115, 650, 180, 50, 250, 100],
'Violation': ['MOBILE BUS LANE VIOLATION', 'NO STAND TAXI/FHV RELIEF STAND', 'MISUSE PARKING PERMIT',
'MOBILE BUS LANE VIOLATION', 'NO STAND TAXI/FHV RELIEF STAND', 'MISUSE PARKING PERMIT',
'NO PARKING-EXC. DSBLTY PERMIT', 'WEIGH IN MOTION VIOLATION', 'FRAUDULENT USE PARKING PERMIT',
'MOBILE BUS LANE VIOLATION', 'NO STAND TAXI/FHV RELIEF STAND', 'MISUSE PARKING PERMIT'],
'Issue Date': dates,
'Violation Time': ['0900', '1430', '1100', '1000', '1500', '1200', '1300', '0800', '1700', '0930', '1400', '1130'],
'Penalty Amount': [10, 50, 20, 10, 50, 20, 25, 100, 35, 10, 50, 20],
'Interest Amount': [5, 25, 10, 5, 25, 10, 12, 50, 18, 5, 25, 10],
'Reduction Amount': [0, 25, 0, 0, 0, 10, 0, 50, 0, 0, 25, 0],
'Payment Amount': [50, 225, 100, 50, 250, 90, 115, 600, 180, 50, 225, 100],
'Amount Due': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
})
df_fines['Violation Hour'] = df_fines['Violation Time'].apply(lambda x: int(x[:2]))
df_fines['Day of Week'] = df_fines['Issue Date'].dt.day_name()
day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
df_fines['Day of Week'] = pd.Categorical(df_fines['Day of Week'], categories=day_order, ordered=True)
# Calculate frequencies for 'Fine Amount' directly from the dataset
if not df_fines.empty:
FINE_DATA = df_fines['Fine Amount'].value_counts().sort_index().to_dict()
TOTAL_TICKETS = df_fines.shape[0]
FINE_PROBABILITIES = {amount: count / TOTAL_TICKETS for amount, count in FINE_DATA.items()}
NYC_AVERAGE_FINE = df_fines['Fine Amount'].mean()
else:
# Fallback data if DataFrame is empty
FINE_DATA = {50: 8688, 250: 6057, 100: 2982, 115: 2612, 65: 2224, 150: 1553, 200: 1036, 180: 815, 650: 91}
TOTAL_TICKETS = sum(FINE_DATA.values())
FINE_PROBABILITIES = {amount: count/TOTAL_TICKETS for amount, count in FINE_DATA.items()}
NYC_AVERAGE_FINE = 127 # Fallback average
# Calculate weights for violation types based on the dataset
if not df_fines.empty:
VIOLATION_COUNTS = df_fines['Violation'].value_counts().to_dict()
VIOLATIONS = {
"MOBILE BUS LANE VIOLATION": VIOLATION_COUNTS.get("MOBILE BUS LANE VIOLATION", 0),
"NO STAND TAXI/FHV RELIEF STAND": VIOLATION_COUNTS.get("NO STAND TAXI/FHV RELIEF STAND", 0),
"MISUSE PARKING PERMIT": VIOLATION_COUNTS.get("MISUSE PARKING PERMIT", 0),
"NO PARKING-EXC. DSBLTY PERMIT": VIOLATION_COUNTS.get("NO PARKING-EXC. DSBLTY PERMIT", 0),
"FRAUDULENT USE PARKING PERMIT": VIOLATION_COUNTS.get("FRAUDULENT USE PARKING PERMIT", 0),
"NO STANDING-FOR HIRE VEH STND": VIOLATION_COUNTS.get("NO STANDING-FOR HIRE VEH STND", 0),
"PCKP DSCHRGE IN PRHBTD ZONE": VIOLATION_COUNTS.get("PCKP DSCHRGE IN PRHBTD ZONE", 0),
"WEIGH IN MOTION VIOLATION": VIOLATION_COUNTS.get("WEIGH IN MOTION VIOLATION", 0)
}
for k, v in VIOLATIONS.items():
if v == 0: VIOLATIONS[k] = 1 # Assign a small value if no occurrences to avoid errors
else:
VIOLATIONS = { # Fallback values
"MOBILE BUS LANE VIOLATION": 20316, "NO STAND TAXI/FHV RELIEF STAND": 1863, "MISUSE PARKING PERMIT": 1758,
"NO PARKING-EXC. DSBLTY PERMIT": 815, "FRAUDULENT USE PARKING PERMIT": 463, "NO STANDING-FOR HIRE VEH STND": 459,
"PCKP DSCHRGE IN PRHBTD ZONE": 293, "WEIGH IN MOTION VIOLATION": 91
}
FUN_FACTS = [
"π Bus lane violations fund NYC's public transit improvements and safety programs.",
"π° The most expensive fine ($650) equals 43 hours of minimum wage work in NYC.",
"π― These fines represent about 2.3% of average NYC household monthly income.",
"π Fine revenue helps fund traffic safety programs and infrastructure citywide.",
"π NYC processes over 10 million parking violations annually across all types.",
"π Average NYC driver pays $300-400 in fines per year - you're testing your luck!",
"β° Most tickets issued between 12-6 PM when traffic enforcement is highest.",
"π½ Revenue from these specific violations helps maintain city services.",
f"π Analysis based on {TOTAL_TICKETS:,} real violations from NYC open data.",
"π« Understanding fine patterns helps drivers make better parking decisions."
]
def get_random_violation():
"""Usar numpy en lugar de random para seleccionar violaciΓ³n"""
violations_names = list(VIOLATIONS.keys())
violations_weights = list(VIOLATIONS.values())
if sum(violations_weights) == 0:
if not df_fines.empty:
# Usar numpy para selecciΓ³n aleatoria en lugar de random.choice
return np.random.choice(df_fines['Violation'].unique())
else:
return np.random.choice(list(VIOLATIONS.keys()))
# Convertir pesos a probabilidades normalizadas
violations_weights = np.array(violations_weights)
probabilities = violations_weights / violations_weights.sum()
# Usar numpy.random.choice con probabilidades
return np.random.choice(violations_names, p=probabilities)
def spin_lottery():
"""Usar numpy en lugar de random.choices para la loterΓa"""
amounts = list(FINE_PROBABILITIES.keys())
probabilities = list(FINE_PROBABILITIES.values())
# Normalizar probabilidades para asegurar que suman 1
probabilities = np.array(probabilities)
probabilities = probabilities / probabilities.sum()
# Usar numpy para selecciΓ³n con probabilidades
return np.random.choice(amounts, p=probabilities)
def get_financial_analysis(fine_amount, df):
"""
Analyze the financial behavior of a specific fine
"""
if df.empty:
# Fallback data if no dataset
return {
'escalation_path': f"${fine_amount} β ${fine_amount + 50} β ${fine_amount + 100}",
'negotiation_success': "18% achieve discounts",
'avg_reduction': "$35",
'payment_behavior': "72% pay in full",
'interest_risk': f"${fine_amount * 0.2:.0f} in interest if delayed",
'risk_level': ("π‘ MEDIUM", "Standard fine - pay early to avoid surcharges"),
'financial_score': "45/100"
}
# Filter data by specific fine amount
fine_data = df[df['Fine Amount'] == fine_amount].copy()
if fine_data.empty:
fine_data = df.copy() # Use entire df to compute general stats
analysis = {}
avg_penalty = fine_data['Penalty Amount'].fillna(0).mean() if not fine_data.empty else 0
avg_interest = fine_data['Interest Amount'].fillna(0).mean() if not fine_data.empty else 0
step1 = fine_amount
step2 = fine_amount + avg_penalty
step3 = fine_amount + avg_penalty + avg_interest
analysis['escalation_path'] = f"${step1:.0f} β ${step2:.0f} β ${step3:.0f}"
reductions = fine_data['Reduction Amount'].fillna(0)
negotiation_rate = (reductions > 0).mean() * 100 if not fine_data.empty else 0
avg_reduction = reductions[reductions > 0].mean() if not reductions[reductions > 0].empty else 0
analysis['negotiation_success'] = f"{negotiation_rate:.0f}% achieve discounts"
analysis['avg_reduction'] = f"${avg_reduction:.0f}" if not pd.isna(avg_reduction) else "$0"
payment_amounts = fine_data['Payment Amount'].fillna(0)
full_payment_rate = (payment_amounts >= fine_amount * 0.95).mean() * 100 if not fine_data.empty else 0
analysis['payment_behavior'] = f"{full_payment_rate:.0f}% pay in full"
amount_due = fine_data['Amount Due'].fillna(0)
still_owe_rate = (amount_due > 0).mean() * 100 if not fine_data.empty else 0
max_interest = fine_data['Interest Amount'].fillna(0).max() if not fine_data.empty else 0
analysis['interest_risk'] = f"Up to ${max_interest:.0f} in possible interest"
financial_score = 0
if fine_amount > 0:
penalty_rate = avg_penalty / fine_amount
interest_rate = avg_interest / fine_amount
collection_difficulty = still_owe_rate / 100
financial_score = (penalty_rate + interest_rate + collection_difficulty) * 100
if financial_score < 20:
risk_level = ("π’ LOW", "This fine is relatively 'financially friendly'")
elif financial_score < 50:
risk_level = ("π‘ MEDIUM", "Standard fine - pay early to avoid surcharges")
else:
risk_level = ("π΄ HIGH", "Warning! This fine gets expensive quickly")
analysis['risk_level'] = risk_level
analysis['financial_score'] = f"{financial_score:.0f}/100"
return analysis
def create_financial_breakdown_display(fine_amount, df):
"""
Create the HTML display for financial analysis
"""
financial_data = get_financial_analysis(fine_amount, df)
risk_color_class = "success" if "π’" in financial_data['risk_level'][0] else "warning" if "π‘" in financial_data['risk_level'][0] else "danger"
return html.Div([
html.H4("π° DEEP FINANCIAL ANALYSIS", className="section-title"),
html.Div([
html.Div([
html.Span("π", style={'fontSize': '1.5rem', 'marginRight': '0.5rem'}),
html.Strong("Escalation Path:", style={'color': '#e74c3c'}),
html.Br(),
html.Span(financial_data['escalation_path'],
style={'fontSize': '1.1rem', 'fontWeight': 'bold', 'color': '#2c3e50'})
], style={'marginBottom': '1rem'})
]),
html.Div([
html.Div([
html.Span("βοΈ ", style={'fontSize': '1.2rem'}),
financial_data['negotiation_success']
], className="financial-stat"),
html.Div([
html.Span("π‘ ", style={'fontSize': '1.2rem'}),
f"Average discount: {financial_data['avg_reduction']}"
], className="financial-stat"),
html.Div([
html.Span("π ", style={'fontSize': '1.2rem'}),
financial_data['payment_behavior']
], className="financial-stat")
], className="financial-stats-grid"),
html.Div([
html.Div([
html.Span("π― Risk Level: ", style={'fontWeight': 'bold'}),
dbc.Badge(financial_data['risk_level'][0],
color=risk_color_class,
className="ms-1")
], style={'marginBottom': '0.5rem'}),
html.P(financial_data['risk_level'][1],
style={'fontStyle': 'italic', 'color': '#7f8c8d', 'margin': '0'})
], className="risk-level-container")
], className="card section-card")
def create_wheel_figure():
amounts = list(FINE_DATA.keys())
values = list(FINE_DATA.values())
colors = px.colors.sequential.Plasma
if len(amounts) > len(colors):
colors = px.colors.qualitative.Plotly + px.colors.sequential.Plasma + px.colors.qualitative.Alphabet
n_colors = len(amounts)
selected_colors = [colors[i % len(colors)] for i in range(n_colors)]
fig = go.Figure(data=[go.Pie(
labels=[f'${amt}' for amt in amounts], values=values, hole=0.4,
marker=dict(colors=selected_colors, line=dict(color='white', width=3)),
textinfo='label+percent', textposition='inside',
hovertemplate='<b>$%{label}</b><br>Probability: %{percent}<br>Count: %{value}<extra></extra>',
rotation=90
)])
fig.update_layout(
title=dict(text="π― NYC TRAFFIC FINE WHEEL π―", x=0.5, font=dict(size=24, color='#2c3e50')),
font=dict(size=14, color='white'),
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
height=400, showlegend=False, margin=dict(t=80, b=20, l=20, r=20),
annotations=[dict(text="SPIN!", x=0.5, y=0.5, font_size=20, showarrow=False, font_color='#2c3e50')]
)
return fig
# --- App Layout ---
app.layout = dbc.Container([
# Header Section
dbc.Row(dbc.Col(html.Div([
html.H1("π° NYC TRAFFIC FINE LOTTERY π°", className="main-title"),
html.P("Test Your Luck Against Real NYC Parking Violation Data", className="subtitle"),
html.P("Based on actual NYC Open Data from thousands of real traffic violations", className="context-text"),
html.Div([
html.Span("π", style={'fontSize': '1.5rem'}),
html.Span("REAL DATA 2023", className="badge")
], className="header-badge")
], className="header-section"), width=12)),
dbc.Row([
# Sidebar - Columna para Spin y New Fact
dbc.Col([
dbc.Card([
dbc.CardHeader(html.H2("π― SPIN & FACTS", className="section-title text-center")),
dbc.CardBody([
html.H3("Spin the Wheel!", className="card-subtitle mb-3 text-center"),
dcc.Graph(id='wheel-chart', figure=create_wheel_figure(), config={'displayModeBar': False}),
html.Div([
dbc.Button("π² SPIN THE WHEEL!", id="spin-button", className="spin-btn w-100 mb-3"),
], className="d-grid gap-2"),
html.Div(id="spin-result", className="result-display text-center mb-4", children=[
html.Span("π―", className="result-emoji"),
html.Span("$???", className="result-amount waiting"),
html.P("Click the wheel to find out your fine!", className="result-message")
]),
html.Hr(),
html.H4("NYC Traffic Fine Facts", className="card-subtitle mb-2 text-center"),
html.Div(id="fun-fact-content", className="fun-fact-content mb-3 text-center"),
dbc.Button("π New Fact", id="fact-button", className="fact-btn w-100"),
])
], className="h-100 sidebar-card"),
], width=4, className="sidebar-column mb-4"),
# Contenido Principal
dbc.Col([
dbc.Row([
# Columna Izquierda del Contenido Principal
dbc.Col([
# "You Violation Details"
dbc.Card([
dbc.CardHeader(html.H4("π¨ YOUR VIOLATION DETAILS", className="section-title")),
dbc.CardBody([
html.Div(id="violation-details", className="violation-details mb-4"),
])
], className="mb-4"),
# "Fine Comparison"
dbc.Card([
dbc.CardHeader(html.H4("βοΈ FINE COMPARISON", className="section-title")),
dbc.CardBody([
dcc.Dropdown(
id='compare-dropdown',
options=[
{'label': 'β Starbucks Coffee ($5)', 'value': 5},
{'label': 'π Pizza Slice ($3)', 'value': 3},
{'label': 'π¬ Movie Ticket ($15)', 'value': 15},
{'label': 'π Fast Food Meal ($12)', 'value': 12},
{'label': 'β½ Gallon of Gas ($4)', 'value': 4},
{'label': 'π± iPhone ($1000)', 'value': 1000}
],
placeholder="Compare your fine to...",
className="dbc-dropdown"
),
html.Div(id="comparison-result", className="comparison-result-container mt-3")
])
], className="mb-4"),
], width=6),
# Columna Derecha del Contenido Principal
dbc.Col([
# "Quick Stats"
dbc.Card([
dbc.CardHeader(html.H4("π QUICK STATS", className="section-title")),
dbc.CardBody([
html.Div(id="quick-stats-content"),
])
], className="mb-4"),
# "Interactive Tools"
dbc.Card([
dbc.CardHeader(html.H4("π° Annual Fine Calculator", className="section-title")),
dbc.CardBody([
html.Label("How many tickets per year?", className="slider-label"),
dcc.Slider(
id='tickets-slider',
min=1, max=52, step=1, value=4,
marks={i: str(i) for i in range(0, 53, 10)},
tooltip={"placement": "bottom", "always_visible": True},
className="mb-3"
),
dbc.Button("Calculate Annual Cost", id="calc-button", className="game-btn w-100"),
html.Div(id="annual-result", className="game-result mt-3"),
])
], className="mb-4"),
], width=6)
])
], width=8)
], className="main-content-row"),
html.Div(id="current-fine", style={'display': 'none'}),
# Footer Section
dbc.Row(dbc.Col(html.Footer([
html.P("Web App developed using Plotly Dash 2025", className="footer-text")
], className="app-footer"), width=12))
], fluid=True, className="app-container")
# --- Callbacks ---
@app.callback(
[Output('spin-result', 'children'),
Output('violation-details', 'children'),
Output('current-fine', 'children'),
Output('quick-stats-content', 'children')],
[Input('spin-button', 'n_clicks')]
)
def spin_wheel(n_clicks):
if n_clicks is None:
result = [
html.Span("π―", className="result-emoji"),
html.Span("$???", className="result-amount waiting"),
html.P("Click the wheel to find out your fine!", className="result-message")
]
initial_violation_details = html.Div(html.P("Spin the wheel to see your violation details and financial analysis.", className="text-center text-muted mt-3"))
return result, initial_violation_details, None, create_quick_stats()
fine_amount = spin_lottery()
violation_type = get_random_violation()
if fine_amount <= 65:
emoji = "π
"
amount_class = "result-amount cheap"
message = "Lucky you! That's a relatively small fine."
elif fine_amount <= 150:
emoji = "π"
amount_class = "result-amount medium"
message = "Ouch! That's a moderate fine."
elif fine_amount <= 250:
emoji = "π°"
amount_class = "result-amount expensive"
message = "Expensive! That's going to hurt the wallet."
else:
emoji = "πΈ"
amount_class = "result-amount very-expensive"
message = "OUCH! That's a major fine!"
result = [
html.Span(emoji, className="result-emoji"),
html.Span(f"${fine_amount}", className=amount_class),
html.P(message, className="result-message")
]
# --- Start of new logic for Violation Details - Day/Hour Counts ---
violation_df = df_fines[df_fines['Violation'] == violation_type]
# Get top 3 days
if not violation_df.empty:
day_counts = violation_df['Day of Week'].value_counts(normalize=True).head(3) * 100
day_details = [
html.Li(f"{day}: {count:.1f}%", className="violation-stat-item")
for day, count in day_counts.items()
]
day_details_html = html.Ul(day_details, className="violation-stats-list")
else:
day_details_html = html.P("No specific day pattern data available for this violation type.", className="text-muted small")
# Get top 3 hours
if not violation_df.empty:
hour_counts = violation_df['Violation Hour'].value_counts(normalize=True).head(3) * 100
hour_details = [
html.Li(f"{hour:02d}:00 - {count:.1f}%", className="violation-stat-item")
for hour, count in hour_counts.items()
]
hour_details_html = html.Ul(hour_details, className="violation-stats-list")
else:
hour_details_html = html.P("No specific hour pattern data available for this violation type.", className="text-muted small")
# Get total count for this violation type
total_violation_count = violation_df.shape[0] if not violation_df.empty else 0
total_violation_percentage = (total_violation_count / TOTAL_TICKETS) * 100 if TOTAL_TICKETS > 0 else 0
# --- End of new logic ---
violation_details_content = html.Div([
html.Div([
html.Div([
html.Span("π", className="violation-icon"),
html.Span(violation_type, className="violation-text")
], className="violation-info"),
html.Div([
html.Span("π° Fine Amount: ", className="detail-label"),
html.Span(f"${fine_amount}", className="detail-value")
], className="violation-detail"),
html.Div([
html.Span("π Probability: ", className="detail-label"),
html.Span(f"{FINE_PROBABILITIES.get(fine_amount, 0):.1%}", className="detail-value")
], className="violation-detail"),
html.Div([
html.Span("π Rank: ", className="detail-label"),
html.Span(f"#{sorted(FINE_DATA.keys(), reverse=True).index(fine_amount) + 1 if fine_amount in FINE_DATA else 'N/A'} most expensive", className="detail-value")
], className="violation-detail"),
# --- New violation details added here ---
html.Hr(className="my-3"),
html.H5("π Violation Patterns:", className="detail-subheader"),
html.Div([
html.Span("Total Occurrences of this Type: ", className="detail-label"),
html.Span(f"{total_violation_count:,} ({total_violation_percentage:.1f}% of all tickets)", className="detail-value")
], className="violation-detail mb-2"),
html.Div([
html.H6("Top Days:", className="detail-subheader-small"),
day_details_html
], className="violation-pattern-section"),
html.Div([
html.H6("Top Hours (24h):", className="detail-subheader-small"),
hour_details_html
], className="violation-pattern-section")
# --- End of new violation details ---
], className="violation-card-content"),
create_financial_breakdown_display(fine_amount, df_fines)
])
quick_stats = create_quick_stats(fine_amount)
return result, violation_details_content, fine_amount, quick_stats
@app.callback(
Output('annual-result', 'children'),
[Input('calc-button', 'n_clicks')],
[State('tickets-slider', 'value')]
)
def calculate_annual_cost(n_clicks, tickets_per_year):
if n_clicks is None:
return ""
expected_fine = NYC_AVERAGE_FINE
annual_cost = expected_fine * tickets_per_year
best_case = min(FINE_DATA.keys()) * tickets_per_year
worst_case = max(FINE_DATA.keys()) * tickets_per_year
if annual_cost < 1000:
risk_level = "π’ LOW RISK"
risk_message = "Manageable annual parking costs"
elif annual_cost < 1500:
risk_level = "π‘ MEDIUM RISK"
risk_message = "Moderate impact on budget"
else:
risk_level = "π΄ HIGH RISK"
risk_message = "Significant financial impact!"
return html.Div([
html.Div([
html.Div([
html.Span("π°", className="calc-icon"),
html.Span(f"Expected Annual Cost: ${annual_cost:.0f}", className="calc-stat")
], className="calc-item"),
html.Div([
html.Span("π", className="calc-icon"),
html.Span(f"Range: ${best_case:.0f} - ${worst_case:.0f}", className="calc-stat")
], className="calc-item"),
html.Div([
html.Span("π―", className="calc-icon"),
html.Span(f"Risk Level: {risk_level}", className="calc-stat")
], className="calc-item"),
html.Div([
html.Span("π‘", className="calc-icon"),
html.Span(risk_message, className="calc-message")
], className="calc-item")
], className="calc-results")
])
@app.callback(
Output('fun-fact-content', 'children'),
[Input('fact-button', 'n_clicks')]
)
def update_fun_fact(n_clicks):
if n_clicks is None:
return html.P("Click 'New Fact' to learn something interesting about NYC traffic fines!",
className="fun-fact-text")
# Usar numpy en lugar de random para seleccionar el fact
fact = np.random.choice(FUN_FACTS)
return html.P(fact, className="fun-fact-text")
@app.callback(
Output('comparison-result', 'children'),
[Input('compare-dropdown', 'value')],
[State('current-fine', 'children')]
)
def update_comparison(comparison_value, current_fine):
if not comparison_value or not current_fine:
return ""
fine_amount = current_fine
ratio = fine_amount / comparison_value
comparison_items = {
5: "β Starbucks Coffee",
3: "π Pizza Slice",
15: "π¬ Movie Ticket",
12: "π Fast Food Meal",
4: "β½ Gallon of Gas",
1000: "π± iPhone"
}
item_name = comparison_items.get(comparison_value, "Selected Item")
if ratio < 1:
message = f"Your ${fine_amount} fine is less than 1 {item_name.lower()}"
emoji = "π"
color_class = "comparison-good"
elif ratio < 2:
message = f"Your ${fine_amount} fine = {ratio:.1f} {item_name.lower()}s"
emoji = "π"
color_class = "comparison-medium"
elif ratio < 10:
message = f"Your ${fine_amount} fine = {ratio:.1f} {item_name.lower()}s"
emoji = "π°"
color_class = "comparison-bad"
else:
message = f"Your ${fine_amount} fine = {ratio:.0f} {item_name.lower()}s!"
emoji = "πΈ"
color_class = "comparison-terrible"
return html.Div([
html.Span(emoji, className="comparison-emoji"),
html.P(message, className=f"comparison-text {color_class}")
], className="comparison-display")
def create_quick_stats(current_fine=None):
"""Create quick stats display"""
cheapest = min(FINE_DATA.keys())
most_expensive = max(FINE_DATA.keys())
if current_fine:
percentile = sum(1 for amount in FINE_DATA.keys() if amount <= current_fine) / len(FINE_DATA.keys()) * 100
percentile_text = f"Your ${current_fine} fine is more expensive than {percentile:.0f}% of all fines"
if percentile < 25:
percentile_emoji = "π"
percentile_class = "stat-good"
elif percentile < 75:
percentile_emoji = "βοΈ"
percentile_class = "stat-medium"
else:
percentile_emoji = "πΈ"
percentile_class = "stat-bad"
else:
percentile_text = "Spin to see where your fine ranks"
percentile_emoji = "π―"
percentile_class = "stat-waiting"
return html.Div([
html.Div([
html.Span("π", className="stat-icon"),
html.Div([
html.Span("Most Expensive Fine", className="stat-label"),
html.Span(f"${most_expensive}", className="stat-value")
])
], className="stat-item"),
html.Div([
html.Span("π", className="stat-icon"),
html.Div([
html.Span("Cheapest Fine", className="stat-label"),
html.Span(f"${cheapest}", className="stat-value")
])
], className="stat-item"),
html.Div([
html.Span("π", className="stat-icon"),
html.Div([
html.Span("Average Fine", className="stat-label"),
html.Span(f"${NYC_AVERAGE_FINE:.0f}", className="stat-value")
])
], className="stat-item"),
html.Div([
html.Span("π", className="stat-icon"),
html.Div([
html.Span("Total Tickets Analyzed", className="stat-label"),
html.Span(f"{TOTAL_TICKETS:,}", className="stat-value")
])
], className="stat-item"),
html.Div([
html.Span(percentile_emoji, className="stat-icon"),
html.Div([
html.Span(percentile_text, className=f"stat-label {percentile_class}")
])
], className="stat-item percentile-stat")
], className="quick-stats-grid")