# Figure Friday 2025 - week 43

**URL:** <https://community.plotly.com/t/figure-friday-2025-week-43/95242>\
**Category:** Dash Python\
**Tags:** announcements, figure-friday\
**Created:** [October 25, 2025, 3:35am UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242 "2025-10-25T03:35:30Z")\
**Posts on this page:** 17\
**Page:** 1

<div class="post-metadata">

**Author:** ![adamschroeder](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/adamschroeder/32/17280_2.png) [@adamschroeder](https://community.plotly.com/u/adamschroeder)\
**Post date:** [October 25, 2025, 3:35am UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/1 "2025-10-25T03:35:30Z")

</div>

> ~~join the [Figure Friday session](https://streamyard.com/mbmx2vjpcm) on October 31, at noon Eastern Time, to showcase your creation and receive feedback from the community.~~ Figure Friday session cancelled this week.

What time of day sees the highest number of trick-or-treaters?

Answer this question and a few others by using Plotly on the [Halloween dataset](https://github.com/plotly/Figure-Friday/tree/main/2025/week-43).

## Things to consider:

- what can you improve in the app or sample figure below ([line chart](https://plotly.com/python/line-charts/))?
- would you like to tell a different data story using a Dash app?
- how can you explore the data with Plotly Studio?

Sample figure:  
🙏 Thank you to @Mike_Purtell for the code and sample figure

 ![Halloween](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/2/8/28dac69b060db5b2c9d33f1b4cd279fd79f21204.png)

> **Code for sample figure:**
>
> ```python
> import pandas as pd
> import plotly.express as px
>  
> # dataset: https://github.com/plotly/Figure-Friday/blob/main/2025/week-43/HalloweenTableau2024.csv
> df = pd.read_csv('https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-43/HalloweenTableau2024.csv')
> df = df.assign(Year = df['Date'].str.slice(-4))
>  
> # print(df)
> fig=px.line(
> df,
> x='Year', y='Count',
> color='Time',
> title='Haloween Visitor trick or treater counts by time and year'
> )
> fig.show()
> 
> ```

Below is a screenshot of the app that [Plotly Studio](https://plotly.com/downloads/) created on top of this dataset:

Prompt for the bar chart:

```auto
Average attendance by day of week as a bar chart with dropdown to filter by specific years and toggle to show median vs mean values.

```

 ![image](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/2/f/2f39ba8091ada47a145b13c159bb3ff5bd96acd0.png)

 ![image](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/a/4/a4566aac82a10817025c78f250c76c524fa62b81.png)

## Participation Instructions:

- **Create** - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
- **Submit** - post your creation to LinkedIn or Twitter with the hashtags `#FigureFriday` and `#plotly` by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
- **Celebrate** - join the [Figure Friday sessions](https://streamyard.com/mbmx2vjpcm) to showcase your creation and receive feedback from the community.

👉 If you prefer to collaborate with others on Discord, join the [Plotly Discord channel](https://discord.gg/Pa8scsgepB).

## Data Source:

Thank you to [Data Plus Science](https://www.dataplusscience.com/HalloweenData.html) for the data.

---

<div class="post-metadata">

**Author:** ![Avacsiglo21](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/avacsiglo21/32/29861_2.png) [@Avacsiglo21](https://community.plotly.com/u/Avacsiglo21)\
**Post date:** [October 29, 2025, 1:22pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/3 "2025-10-29T13:22:02Z")

</div>

Hi FFF Community,

For Week 43, Halloween Week, I built a narrative-driven dashboard that transforms Halloween visitor counts into actionable patterns.

By the way, back in Week 31, we worked with a dataset of Halloween Candies data by **FiveThirtyEight** (at that time, I created a gamified dashboard, ‘Candy Emperor’).

This current data, on the other hand, comes from someone who diligently counted ‘trick-or-treaters’ in 30-minute intervals from 6:00 to 8:15 PM for 17 years. What started as simple tallies became a small dataset of 102 samples —a treasure trove of patterns waiting to be discovered.

This time, I engineered/created six features to capture the “Personality” or “Essence” of each Halloween Night:

- **Momentum** : Did the night end stronger than it began? ( **The Grand Finale** factor)
- **Sharpness** : How explosive was the peak moment versus the lull?
- **Resilience** : The ability to sustain activity through the measurement window.
- **Consistency** : How predictable was the flow? ( **The Clockwork** metric)
- **Trend** : Was the night ascending or descending overall? (Linear slope)
- **Concentration** : How clustered was the activity?"

From these metrics emerged 4 distinct Halloween archetypes:

- **Rocket** (1 year): Builds momentum until the very end
- **Spike** (5 years): Explosive peak, quick fade
- **Volatile** (7 years): Unpredictable chaos—the Halloween norm
- **Retreat** (4 years): Strong start, steady decline

Three Ways to Explore

- **Radar Chart** : Compare the 6-metric DNA profile of any year vs. historical average
- **Timeline** : See the flow with an IQR band showing historical variability
- **Chaos Map** : Scatter plot of Momentum vs. Resilience—patterns cluster beautifully

Technical Highlights

- **Pure NumPy slope calculation** (no sklearn for 6 data points—keeping it lean)
- **Feature normalization** with domain-specific thresholds
- **Dynamic narrative generation** that adapts to pattern type

The Insight That Surprised Me

2009 was the only “Rocket”—but not for the reason you’d think.

- Total attendance: **-8% below historical average**
- But the slope from 6:00→7:00: **+244% growth**
- Pattern: Late bloomer that found its rhythm JUST in time

This revealed something profound: **Pattern ≠ Volume**. The shape of the curve tells a different story than total count. Some years are quiet but perfectly timed; others are chaotic yet successful.

:  
The dashboard is live:

> **[🎃 Halloween Energy Dashboard](https://8d15bb1c-9672-41c9-adf3-4fb83bbe8dbe.plotly.app)**

As usual questions more than welcome,this dashboard is not perfect a lot of thing to improve

 ![Captura de tela_29-10-2025_85026_127.0.0.1](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/d/4/d47d220ee389200e5a0bf8dd38f5e5503e9956ce.jpeg)  
 ![Captura de tela_29-10-2025_8513_127.0.0.1](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/4/9/49f678559477eb97d55ecac3e4ab10ca47508834.jpeg)  
 ![Captura de tela_29-10-2025_85134_127.0.0.1](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/8/1/812f044001605959107e8cd37a41fb5db7dc5434.jpeg)

---

<div class="post-metadata">

**Author:** ![Avacsiglo21](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/avacsiglo21/32/29861_2.png) [@Avacsiglo21](https://community.plotly.com/u/Avacsiglo21)\
**Post date:** [October 29, 2025, 1:26pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/4 "2025-10-29T13:26:41Z")

</div>

Here goes the code

> **The code**
>
> import dash  
> from dash import dcc, html, Input, Output  
> import dash\_bootstrap\_components as dbc  
> import plotly.graph\_objects as go  
> import pandas as pd  
> import numpy as np
> 
> # ============================================
> 
> # GLOBAL CONSTANTS AND METADATA (English UI Text)
> 
> # ============================================
> 
> MOMENTUM\_MIN = -1.5  
> MOMENTUM\_MAX = 2.5  
> SHARPNESS\_MAX = 1.2  
> SLOPE\_MAX = 0.5  
> RADAR\_CATEGORIES = [‘Momentum’, ‘Sharpness’, ‘Resilience’, ‘Consistency’, ‘Trend’, ‘Concentration’]
> 
> # Narrative descriptions for the Radar Tooltip
> 
> RADAR\_DESCRIPTIONS = {  
> ‘Momentum’: “The night’s pace. Indicates if the flow of candy ended stronger than it began (Was there a Grand Finale?).”,  
> ‘Sharpness’: “The Peak Roar. Measures how explosive the night’s peak moment was compared to the lull.”,  
> ‘Resilience’: “Night’s Resilience. Ability to sustain activity within the 6:00pm to 8:15pm window (Warning: does not measure the full night).”,  
> ‘Consistency’: ‘The Clockwork. Predictability of the flow. A high value means activity was steady, with no unexpected scares.’,  
> ‘Trend’: ‘The Night’s Trend. Measures the general slope of the flow: did the night feel consistently ascending or descending?’,  
> ‘Concentration’: ‘The Hour Bond. How clustered the activity is. Low dispersion = activity concentrated in a few key hours.’  
> }
> 
> # Sidebar style
> 
> SIDEBAR\_STYLE = {  
> “width”: “400px”,  
> “padding”: “2rem 1rem”,  
> “background”: “linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)”,  
> “borderRight”: “3px solid #ff6b35”,  
> “height”: “100vh”,  
> “overflow”: “hidden”,  
> }
> 
> # Main content style
> 
> CONTENT\_STYLE = {  
> “padding”: “1.5rem”,  
> “background”: “#0f0f1e”,  
> “minHeight”: “100vh”,  
> “flexGrow”: 1,  
> “overflowY”: “auto”  
> }
> 
> DAY\_MAPPING = {  
> ‘Monday’: ‘Monday 🌙’, ‘Tuesday’: ‘Tuesday 🔥’, ‘Wednesday’: ‘Wednesday 🧙’,  
> ‘Thursday’: ‘Thursday 👻’, ‘Friday’: ‘Friday 🎉’, ‘Saturday’: ‘Saturday 🥳’,  
> ‘Sunday’: ‘Sunday 😴’  
> }
> 
> PATTERN\_COLORS = {  
> “🚀 Rocket”: ‘#ff6b35’,  
> “⚡ Spike”: ‘#00d9ff’,  
> “🎢 Volatile”: ‘#ff00ff’,  
> “😴 Retreat”: ‘#808080’  
> }
> 
> # Base Layout definition for plots
> 
> BASE\_LAYOUT\_CONFIG = {  
> ‘paper\_bgcolor’: ‘#16213e’,  
> ‘plot\_bgcolor’: ‘#16213e’,  
> ‘font’: {‘color’: ‘#adb5bd’, ‘size’: 12},  
> ‘showlegend’: True,  
> ‘legend’: {  
> ‘orientation’: “h”,  
> ‘yanchor’: “top”,  
> ‘y’: -0.2,  
> ‘xanchor’: “center”,  
> ‘x’: 0.5,  
> ‘font’: {‘size’: 13}  
> }  
> }
> 
> # ============================================
> 
> # AUXILIARY ANALYSIS FUNCTIONS
> 
> # ============================================
> 
> def safe\_float(val, default=0.0):  
> “”“Converts to a float and ensures finiteness, otherwise returns the default.”“”  
> if isinstance(val, (np.ndarray, pd.Series, list)) and len(val) \> 0:  
> val = val[0]  
> try:  
> f\_val = float(val)  
> return f\_val if np.isfinite(f\_val) else default  
> except (TypeError, ValueError):  
> return default
> 
> def calculate\_features(counts, year\_data):  
> “”“Calculates the 6 key pattern metrics and normalizes them.”“”
> 
> ```
> counts_sum = counts.sum()
> counts_mean = counts.mean()
> peak_value = counts.max()
> 
> if len(counts) < 2 or counts_sum == 0 or counts_mean == 0 or peak_value == 0:
> return None 
>     
> # 1. MOMENTUM SCORE
> early_avg = counts[:2].mean() if len(counts) >= 2 else 0
> late_avg = counts[-2:].mean() if len(counts) >= 2 else 0
> momentum = (late_avg - early_avg) / early_avg if early_avg > 0 else 0
> momentum_scaled = (momentum - MOMENTUM_MIN) / (MOMENTUM_MAX - MOMENTUM_MIN)
> momentum_normalized = np.clip(momentum_scaled, 0, 1) 
> 
> # 2. PEAK SHARPNESS
> sharpness = (peak_value - counts_mean) / counts_mean
> sharpness_normalized = np.clip(sharpness / SHARPNESS_MAX, 0, 1) 
> 
> # 3. RESILIENCE 
> final_count = counts[-1]
> resilience_normalized = np.clip(final_count / peak_value, 0, 1)
> 
> # 4. CONSISTENCY 
> pct_changes = np.diff(counts) / counts[:-1]
> pct_changes = pct_changes[~np.isinf(pct_changes) & ~np.isnan(pct_changes) & (counts[:-1] != 0)]
> volatility = np.std(pct_changes) if len(pct_changes) > 0 else 0
> consistency = 1 / (1 + volatility * 2) 
> 
> # 5. TREND (SLOPE) - PURE NUMPY IMPLEMENTATION
> x_indices = np.arange(len(counts))
> 
> if np.std(counts) == 0:
> slope = 0.0
> else:
> # Calculate slope (m) using Cov(x,y) / Var(x)
> slope, intercept = np.polyfit(x_indices, counts, 1)
> 
> trend_normalized = np.clip(abs(slope) / SLOPE_MAX, 0, 1)
> 
> # 6. CONCENTRATION 
> cv = np.std(counts) / counts_mean
> concentration = 1 / (1 + cv) 
> 
> features = {
> 'Year': year_data['Year'].iloc[0],
> 'Day_of_Week': year_data['Day of Week'].iloc[0],
> 'Total_Count': counts_sum,
> 'Peak_Time': year_data.loc[year_data['Count'].idxmax(), 'Time'],
> 'Momentum': safe_float(momentum_normalized),
> 'Sharpness': safe_float(sharpness_normalized),
> 'Resilience': safe_float(resilience_normalized), 
> 'Consistency': safe_float(consistency),
> 'Trend': safe_float(trend_normalized),           
> 'Slope': safe_float(slope),                      
> 'Concentration': safe_float(concentration),
> 'Counts': counts
> }
> 
> # Pattern Classification (Uses the new Trend/Slope value)
> if features['Slope'] > 0.1 and features['Momentum'] > 0.7:
> pattern = "🚀 Rocket" 
> elif features['Slope'] < -0.1:
> pattern = "😴 Retreat" 
> elif features['Sharpness'] > 0.6 and features['Resilience'] < 0.3:
> pattern = "⚡ Spike" 
> else:
> pattern = "🎢 Volatile" 
>     
> features['Pattern'] = pattern
> return features
> 
> ```
> 
> def create\_info\_card(title, id\_name, text\_class, gradient\_style):  
> “”“Generates a reusable information card component (English titles).”“”  
> narrative\_titles = {  
> ‘total-count’: ‘Souls Counted’,  
> ‘peak-time’: ‘The Ritual Moment’,  
> ‘day-of-week’: ‘The Harvest Day’,  
> ‘vs-avg’: ‘vs. History’,  
> }  
> width\_col = 3 if id\_name not in [‘vs-avg’] else 3  
> return dbc.Col([  
> dbc.Card([  
> dbc.CardBody([  
> html.Div([  
> html.H6(narrative\_titles.get(id\_name, title), className=“text-muted mb-1”),  
> html.H5(id=id\_name, className=f"{text\_class} fw-bold mb-0")  
> ], className=“text-center”)  
> ])  
> ], style={‘background’: gradient\_style, ‘borderRadius’: ‘0.5rem’})  
> ], width={‘size’: width\_col, ‘md’: width\_col, ‘sm’: 6})
> 
> def generate\_narrative(data):  
> “”“Generates the narrative interpretation (English).”“”  
> pattern = data[‘Pattern’]
> 
> ```
> # 1. Pattern Summary (MAIN INSIGHT)
> if pattern == "🚀 Rocket":
> desc = "INSIGHT: The 'Rocket' took off with a flow that not only grew but sustained itself, culminating in a powerful finish. Requires candy planning!"
> elif pattern == "⚡ Spike":
> desc = "INSIGHT: The 'Spike' was a seismic event: a moment of brutal intensity that faded quickly. Calm arrived dramatically early."
> elif pattern == "😴 Retreat":
> desc = "INSIGHT: 'Retreat'. The night had a general downward trend. The peak occurred very early, and activity decreased until the end."
> else: # Volatile
> desc = "INSIGHT: 'Volatile' like a bubbling potion. The night was full of false peaks and unexpected drops. Impossible to predict!"
> 
> # 2. Key interpreted features
> momentum_text = "Epic Final Strength. The end was stronger than the beginning." if data['Momentum'] > 0.6 else "Steady Pace. Similar start and end." if data['Momentum'] > 0.4 else "Slow Start. Took effort to get going."
> resilience_text = "Mythical Resilience (up to 8:15pm). Final activity was almost equal to the peak." if data['Resilience'] > 0.7 else "Decent Resilience. Halved, but sustained." if data['Resilience'] > 0.4 else "Drastic Collapse. The night died quickly."
> consistency_text = "Predictable. Stable flow without surprises." if data['Consistency'] > 0.7 else "Normal. Expected variations." if data['Consistency'] < 0.5 else "Chaotic. Totally unpredictable."
> trend_text = "Strong Upward Trend. Linear regression was positive." if data['Slope'] > 0.1 else "Strong Downward Trend. Linear regression was negative." if data['Slope'] < -0.1 else "Flat Trend. The flow remained relatively level."
> 
> # 3. Open Question (Planning Narrative)
> if pattern == "⚡ Spike":
> q = "Was there an event or weather that caused the terror not to last (low Resilience)? Next time, when should we close the doors?"
> elif pattern == "💪 Endurance":
> q = "What factors (day of the week, weather) made the flow so constant and how can we replicate that marathon activity?"
> elif pattern == "🚀 Rocket":
> q = "What spell was used to ensure this explosive and sustained growth? Is it a repeatable pattern or a one-time event?"
> elif pattern == "😴 Retreat":
> q = "The trend is downwards. How can we delay the peak and use early advertising to increase arrival momentum?"
> else:
> q = "How can we minimize volatility and find the 'sweet spot' for restocking?"
>     
> narrative_elements = [
> html.H5(f"💀 The Legacy of Year {int(data['Year'])}", className="mb-2 fw-bold text-warning"),
> html.H6(f"Identified Pattern: {pattern}", className="mb-2 fw-bold text-info"),
> html.P(desc, className="small text-light mb-3"),
> html.Div("The DNA of Fear (Historical Six Metrics):", className="fw-bold text-warning mb-1"),
> html.P([html.B("Pace: "), momentum_text], className="mb-1 small text-light"),
> html.P([html.B("Resilience: "), resilience_text], className="mb-1 small text-light"),
> html.P([html.B("Trend: "), trend_text], className="mb-1 small text-light"), 
> html.P([html.B("Reliability: "), consistency_text], className="mb-1 small text-light"),
> html.Div("🤔 The Next Prediction:", className="fw-bold text-danger mt-3 mb-1"),
> html.P(q, className="small text-light")
> ]
> return dbc.Alert(narrative_elements, color="secondary", className="border border-warning")
> 
> ```
> 
> # ============================================
> 
> # LOAD AND PREPARE DATA
> 
> # ============================================
> 
> try:  
> # Ensure ‘HalloweenTableau2024.csv’ is available in the environment  
> halloween\_data = pd.read\_csv(‘HalloweenTableau2024.csv’)
> 
> ```
> # Preprocessing
> halloween_data['Date'] = pd.to_datetime(halloween_data['Date'], format='%m/%d/%y')
> halloween_data['Year'] = halloween_data['Date'].dt.year
> halloween_data = halloween_data.sort_values(['Date', 'Time']).reset_index(drop=True)
> 
> # Feature Engineering
> year_features = []
> for year in halloween_data['Year'].unique():
> year_data = halloween_data[halloween_data['Year'] == year].sort_values('Time')
> features = calculate_features(year_data['Count'].values, year_data)
> if features:
> year_features.append(features)
> 
> features_df = pd.DataFrame(year_features)
> 
> # Calculate historical averages and percentiles for the new IQR band feature
> counts_list = [pd.Series(f['Counts']) for f in year_features]
> aligned_counts = pd.concat(counts_list, axis=1).fillna(0) if counts_list else pd.DataFrame() 
> 
> avg_counts = aligned_counts.mean(axis=1).values
> # Calculate 25th and 75th percentiles for IQR band
> q25_counts = aligned_counts.quantile(0.25, axis=1).values
> q75_counts = aligned_counts.quantile(0.75, axis=1).values
> 
> ```
> 
> except Exception as e:  
> # Print error in English for debugging  
> print(f"FATAL ERROR: Could not load ‘HalloweenTableau2024.csv’. Details: {e}")  
> features\_df = pd.DataFrame()  
> avg\_counts =   
> q25\_counts =   
> q75\_counts =
> 
> # ============================================
> 
> # DASHBOARD LAYOUT
> 
> # ============================================
> 
> app = dash.Dash( **name** , external\_stylesheets=[dbc.themes.DARKLY])
> 
> app.title = “🎃 Halloween Energy Dashboard”
> 
> # Definition of information cards (English Titles)
> 
> card\_definitions = [  
> (“Total Attendance”, ‘total-count’, “text-warning”, ‘linear-gradient(135deg, #4e3a1b 0%, #1a1a2e 100%)’),  
> (“Peak Time”, ‘peak-time’, “text-danger”, ‘linear-gradient(135deg, #4e1b1b 0%, #1a1a2e 100%)’),  
> (“Day of Week”, ‘day-of-week’, “text-info”, ‘linear-gradient(135deg, #1b3a4e 0%, #1a1a2e 100%)’),  
> (“vs History”, ‘vs-avg’, “text-light”, ‘linear-gradient(135deg, #2d1b4e 0%, #1a1a2e 100%)’),  
> ]
> 
> app.layout = html.Div(style={‘display’: ‘flex’, ‘height’: ‘100vh’, ‘width’: ‘100vw’, ‘overflow’: ‘hidden’}, children=[
> 
> ```
> # SIDEBAR
> html.Div([
> html.Div([
> html.Div("🎃", style={'fontSize': '4rem', 'textAlign': 'center'}),
> html.H2("The Candy Oracle", className="text-warning text-center mb-1", 
> style={'fontWeight': 'bold'}),
> html.P("Mystical Energy Pattern Analysis", className="text-muted text-center mb-4"),
>         
> html.Hr(style={'borderColor': '#ff6b35', 'opacity': '0.3'}),
>         
> # Selectors
> *[html.Div([
> html.Label(label, className="text-warning mb-2 fw-bold"),
> dcc.Dropdown(
> id=id_val,
> options=[{'label': opt_label, 'value': opt_value} 
> for opt_label, opt_value in options] if not features_df.empty else [],
> value=default_value,
> clearable=False,
> style={'marginBottom': '1.5rem', 'color': '#0f0f1e'} 
> )
> ]) for label, id_val, options, default_value in [
> ("📅 The Ritual Year", 'year-selector', 
> [(f'{int(year)}', int(year)) for year in features_df['Year'].sort_values()], 
> int(features_df['Year'].max()) if not features_df.empty else None),
> ("⚖️ The Magic Contrast", 'compare-year', 
> [('The Historical Average', 'avg')] + [(f'{int(year)}', int(year)) 
> for year in features_df['Year'].sort_values()], 
> 'avg')
> ]],
>         
> html.Hr(style={'borderColor': '#ff6b35', 'opacity': '0.3'}),
>         
> # Radio Items (English View Selector)
> html.Label("🔮 Which Form of Fear Will You See?", className="text-warning mb-2 fw-bold"),
> dbc.RadioItems(
> id='view-selector',
> options=[
> {'label': '🧬 DNA Profile (Metrics)', 'value': 'radar'},
> {'label': '📈 The Timeline (Hourly Flow)', 'value': 'timeline'},
> {'label': '🎯 Chaos Map (Momentum vs. Resilience)', 'value': 'scatter'}, # NEW VIEW
> ],
> value='radar',
> className="text-light",
> labelStyle={'marginBottom': '0.8rem'}
> ),
>         
> html.Hr(style={'borderColor': '#ff6b35', 'opacity': '0.3'}),
>         
> html.Div(id='year-info', className="mt-3")
>         
> ], style={'maxHeight': '100%', 'overflowY': 'auto', 'paddingRight': '10px'}) 
>     
> ], style=SIDEBAR_STYLE),
> 
> # MAIN CONTENT
> html.Div([
> html.Div([
> html.H4("🕯️ Unveiling the Night's Secret", className="text-info fw-bold mb-1"),
> html.P("This digital Grimoire analyzes the 'trick-or-treat' energy by 30-minute intervals. Our objective: predict the exact moment to restock the candy arsenal and understand the Legacy of each Halloween.",
> className="text-muted small mb-4 border-bottom border-secondary pb-3")
> , # Cards generated programmatically
> dbc.Row([create_info_card(*card) for card in card_definitions], className="mb-3 g-3"),
>         
> # Main Graph
> dbc.Row([
> dbc.Col([
> dbc.Card([
> dbc.CardBody([
> dbc.Spinner(
> dcc.Graph(id='main-graph', style={'height': '70vh'}),
> color="warning"
> )
> ])
> ], style={'background': 'linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)', 'borderRadius': '0.5rem'})
> ], width=12)
> ])
>         
> ], className="flex-grow-1"), 
> 
> # FOOTER
> html.Footer([
> html.P([
> "Dashboard powered by the ",
> html.Span("Mystery of Plotly | Dash", className="text-warning fw-bold"),
> " | by Avacsiglo21 | Data courtesy of ",
> html.A("Data Plus Science", href="https://www.dataplusscience.com/", target="_blank", className="text-info fw-bold"),
> "."
> ], className="text-center text-muted small mb-0")
> ], style={
> 'padding': '1rem 0', 
> 'borderTop': '1px solid #2d3b55', 
> 'marginTop': '1rem',
> 'flexShrink': 0 
> })
>     
> ], style=CONTENT_STYLE)
> 
> ```
> 
> ])
> 
> # ============================================
> 
> # CALLBACK (CENTRAL PLOTTING LOGIC)
> 
> # ============================================
> 
> @app.callback(  
> [Output(‘main-graph’, ‘figure’),  
> Output(‘total-count’, ‘children’),  
> Output(‘peak-time’, ‘children’),  
> Output(‘day-of-week’, ‘children’),  
> Output(‘vs-avg’, ‘children’),  
> Output(‘year-info’, ‘children’)],  
> [Input(‘year-selector’, ‘value’),  
> Input(‘compare-year’, ‘value’),  
> Input(‘view-selector’, ‘value’)]  
> )  
> def update\_dashboard(selected\_year, compare\_year, view):  
> if features\_df.empty or selected\_year is None:  
> return go.Figure(), “N/A”, “N/A”, “N/A”, “N/A”, dbc.Alert(  
> “ERROR: The Data Grimoire could not be loaded. Check for the presence and validity of ‘HalloweenTableau2024.csv’.”,  
> color=“danger”)
> 
> ```
> year_data = features_df[features_df['Year'] == selected_year].iloc[0]
> 
> # --- Shared Metadata Calculation ---
> avg_total = features_df['Total_Count'].mean()
> diff_pct = ((year_data['Total_Count'] - avg_total) / avg_total) * 100
> 
> if diff_pct > 0:
> vs_avg_text, vs_avg_color = f"↑ +{diff_pct:.0f}% Better", "success"
> elif diff_pct < 0:
> vs_avg_text, vs_avg_color = f"↓ {diff_pct:.0f}% Less", "danger"
> else:
> vs_avg_text, vs_avg_color = "— Equal", "info"
>     
> year_info = generate_narrative(year_data)
> display_day = DAY_MAPPING.get(year_data['Day_of_Week'], year_data['Day_of_Week'])
> 
> fig = go.Figure()
> layout = BASE_LAYOUT_CONFIG.copy() 
> 
> # --- GRAPH GENERATION ---
> 
> if view == 'radar':
> # Generate hovertext that includes the category description (English).
> hover_descriptions = [RADAR_DESCRIPTIONS[cat] for cat in RADAR_CATEGORIES]
>     
> # Radar Trace (Selected Year)
> values = [float(year_data[cat]) for cat in ['Momentum', 'Sharpness', 'Resilience', 'Consistency', 'Trend', 'Concentration']]
> 
> fig.add_trace(go.Scatterpolar(
> r=values, 
> theta=RADAR_CATEGORIES, 
> fill='toself', 
> name=f'{selected_year} (Pattern: {year_data["Pattern"]})',
> line=dict(color='#ff6b35', width=3), 
> fillcolor='rgba(255, 107, 53, 0.4)',
>         
> # ADVANCED TOOLTIP IMPLEMENTATION:
> customdata=np.array([hover_descriptions, values]).T, 
> hovertemplate=(
> "<b>Metric:</b> %{theta}<br>"
> "<b>Value (%{selected_year}):</b> %{r:.2f}<br>"
> "---<br>"
> "<b>Definition:</b> %{customdata[0]}" 
> "<extra></extra>" 
> )
> ))
>     
> # Comparison Logic
> comp_name_label = ""
> if compare_year == 'avg':
> comp_r = [float(features_df[cat].mean()) for cat in ['Momentum', 'Sharpness', 'Resilience', 'Consistency', 'Trend', 'Concentration']]
> comp_name, comp_line = 'The Grand Historical Average', dict(color='#00d9ff', width=2, dash='dash')
> comp_name_label = 'The Grand Historical Average'
> else:
> compare_data = features_df[features_df['Year'] == compare_year].iloc[0]
> comp_r = [float(compare_data[cat]) for cat in ['Momentum', 'Sharpness', 'Resilience', 'Consistency', 'Trend', 'Concentration']]
> comp_name, comp_line = f'{compare_year} (Pattern: {compare_data["Pattern"]})', dict(color='#bb86fc', width=2, dash='dot')
> comp_name_label = f'{compare_year}'
> 
> # Radar Trace (Comparison Year/Average)
> fig.add_trace(go.Scatterpolar(
> r=comp_r, 
> theta=RADAR_CATEGORIES, 
> name=comp_name, 
> line=comp_line, 
> fillcolor='rgba(0, 217, 255, 0.1)',
> customdata=np.array([hover_descriptions, comp_r]).T,
> hovertemplate=(
> "<b>Metric:</b> %{theta}<br>"
> f"<b>Value ({comp_name_label}):</b> %{{r:.2f}}<br>"
> "---<br>"
> "<b>Definition:</b> %{customdata[0]}"
> "<extra></extra>"
> )
> ))
>     
> # Specific Radar Layout Configuration
> layout.update(polar=dict(
> radialaxis=dict(visible=True, range=[0, 1.05], gridcolor='#2d3b55', color='#adb5bd', tickfont=dict(size=10)),
> angularaxis=dict(gridcolor='#2d3b55', color='#ff6b35'),
> bgcolor='#16213e'
> ), title={'text': f"🧬 The Sweet Death Profile: {selected_year} vs. {comp_name_label}", 'font': {'size': 20, 'color': '#ff6b35'}, 'x': 0.5, 'xanchor': 'center'})
>     
> elif view == 'timeline':
> times = halloween_data['Time'].unique().tolist()
>     
> comp_y = None
> comp_name_label = ""
>     
> # Historical IQR Band Visualization (when comparing to average)
> if compare_year == 'avg' and len(avg_counts) == len(times) and len(q25_counts) == len(times):
>         
> comp_name_label = 'The Grand Historical Average'
>         
> # 1. Trace for 75th Percentile (Upper Boundary of the IQR Band)
> fig.add_trace(go.Scatter(
> x=times, y=q75_counts, mode='lines', 
> line=dict(width=0), 
> showlegend=False, 
> hoverinfo='none',
> name='75th Percentile'
> ))
>         
> # 2. Trace for Average (The center line, which fills the space to the next trace)
> fig.add_trace(go.Scatter(
> x=times, y=avg_counts, mode='lines',
> name='The Grand Historical Average (Mean)',
> line=dict(color='#00d9ff', width=3, dash='dash'),
> fill='tonexty', 
> fillcolor='rgba(0, 217, 255, 0.1)', 
> hoverlabel={'namelength': -1}
> ))
> 
> # 3. Trace for 25th Percentile (Lower Boundary of the IQR Band)
> fig.add_trace(go.Scatter(
> x=times, y=q25_counts, mode='lines', 
> line=dict(width=0, color='#00d9ff'), 
> showlegend=False, 
> hoverinfo='none',
> name='25th Percentile'
> ))
> 
> elif compare_year != 'avg':
> compare_data = features_df[features_df['Year'] == compare_year].iloc[0]
> comp_y, comp_name, comp_line = compare_data['Counts'], f'{compare_year} ({compare_data["Pattern"]})', dict(color='#bb86fc', width=3, dash='dot')
> comp_name_label = f'{compare_year}'
>         
> # Add the selected comparison year trace
> fig.add_trace(go.Scatter(
> x=times, y=comp_y, mode='lines+markers', name=comp_name, 
> line=comp_line, marker=dict(size=10)
> ))
>         
> # Trace for the currently selected year (always on top)
> fig.add_trace(go.Scatter(
> x=times, y=year_data['Counts'], mode='lines+markers',
> name=f'{selected_year} (Pattern: {year_data["Pattern"]})',
> line=dict(color='#ff6b35', width=4),
> marker=dict(size=12, line=dict(color='#16213e', width=2))
> ))
>         
> # Specific Timeline Layout Configuration 
> layout.update(
> xaxis={'title': 'The Night Clock (Time)', 'gridcolor': '#2d3b55', 'color': '#ff6b35'},
> yaxis={'title': 'The Tide of Visitors (Count)', 'gridcolor': '#2d3b55', 'color': '#ff6b35'},
> title={'text': f"📈 The Tide of Fear: Flow of {selected_year} vs. {comp_name_label}", 'font': {'size': 20, 'color': '#ff6b35'}, 'x': 0.5, 'xanchor': 'center'}
> )
> 
> elif view == 'scatter':
> # SCATTER PLOT (CHAOS MAP) - Momentum vs. Resilience
>     
> # # --- BACKGROUND TRACES (All Years) 
> 
> for pattern_name, color in PATTERN_COLORS.items():
> pattern_data = features_df[features_df['Pattern'] == pattern_name]
>         
> # Count BEFORE filtering (this is the true historical count)
> total_count = len(pattern_data) # 🔧 LÍNEA NUEVA
>         
> # Filter out the currently selected year from the background
> if not pattern_data.empty and selected_year in pattern_data['Year'].values:
> pattern_data = pattern_data[pattern_data['Year'] != selected_year]
>             
> if total_count > 0: # 🔧 CAMBIADO: de "not pattern_data.empty" a "total_count > 0"
> fig.add_trace(go.Scatter(
> x=pattern_data['Momentum'], y=pattern_data['Resilience'], mode='markers',
> marker=dict(size=10, opacity=0.6, color=color, line=dict(color='white', width=1)),
> name=f'{pattern_name} ({total_count})' # 🔧 CAMBIADO: de "len(pattern_data)" a "total_count"
> ))
> 
> # --- FOCUSED TRACE (Selected Year) ---
> selected_size = 15 # Base size for the highlighted dot
>     
> # Determine the size based on how much the year deviates from the historical average in total count (Narrative only)
> total_count_avg = features_df['Total_Count'].mean()
> size_scale_factor = np.clip(np.abs(year_data['Total_Count'] - total_count_avg) / total_count_avg, 0, 1) * 10
>     
> fig.add_trace(go.Scatter(
> x=[year_data['Momentum']], y=[year_data['Resilience']], mode='markers',
> marker=dict(size=selected_size + size_scale_factor, color=PATTERN_COLORS.get(year_data['Pattern'], 'white'), line=dict(color='white', width=4)),
> name=f'The Chosen One: {selected_year}', showlegend=False,
> hovertemplate=(
> f"<b>Year:</b> {selected_year}<br>"
> f"<b>Pattern:</b> {year_data['Pattern']}<br>"
> "<b>Momentum:</b> %{x:.2f}<br>"
> "<b>Resilience:</b> %{y:.2f}"
> "<extra></extra>"
> )
> ))
>     
> # --- SCATTER PLOT LEGEND CORRECTION (Uses BASE_LAYOUT but confirms title) ---
> layout['legend'].update({
> 'title': "Fear Energy Patterns",
> 'y': -0.2, # Confirms position below the plot area
> 'yanchor': 'top'
> })
>     
> # Specific Scatter Layout Configuration
> layout.update(
> xaxis={'title': 'Pace → (Momentum/Strong Finish)', 'gridcolor': '#2d3b55', 'color': '#ff6b35', 'range': [-0.1, 1.1]},
> yaxis={'title': 'Stamina → (Resilience/Sustainability)', 'gridcolor': '#2d3b55', 'color': '#ff6b35', 'range': [-0.1, 1.1]},
> title={'text': f"🎯 The Map of Chaos: Historical Patterns vs. {selected_year}", 'font': {'size': 20, 'color': '#ff6b35'}, 'x': 0.5, 'xanchor': 'center'}
> )
> 
> fig.update_layout(layout)
> 
> # --- Outputs ---
> return (fig, 
> f"{int(year_data['Total_Count']):,}", 
> year_data['Peak_Time'],
> display_day, 
> html.Span(vs_avg_text, className=f"text-{vs_avg_color} fw-bold"),
> year_info)
> 
> ```
> 
> server = app.server

---

<div class="post-metadata">

**Author:** ![adamschroeder](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/adamschroeder/32/17280_2.png) [@adamschroeder](https://community.plotly.com/u/adamschroeder)\
**Post date:** [October 29, 2025, 2:47pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/5 "2025-10-29T14:47:16Z")

</div>

> The Sweet Death Profile.

What an awesome title, @Avacsiglo21 .

Looks like the ‘consistency’ of trick-or-treating in 2021 was much below average.

 ![image](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/6/c/6ca93d2c3b972302e2fa18f9ab52b33d85c54a89.png)

I wonder if there was a storm that came in around 7:30. It’s like it started raining, which caused a big unexpected drop.

 ![image](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/1/6/165ded8ce823248c1b3aa41de9e6c9dd058653f5.png)

The summary on the left is also impressive. Is this AI generated?

 ![image](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/2/a/2af471df60f3f59fb9e513709822e9622fe9d81e.png)

---

<div class="post-metadata">

**Author:** ![Avacsiglo21](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/avacsiglo21/32/29861_2.png) [@Avacsiglo21](https://community.plotly.com/u/Avacsiglo21)\
**Post date:** [October 29, 2025, 3:01pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/6 "2025-10-29T15:01:33Z")

</div>

Yes, AI helped write the Halloween narrative shown in the left summary.

---

<div class="post-metadata">

**Author:** ![Mike\_Purtell](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/mike_purtell/32/21990_2.png) [@Mike\_Purtell](https://community.plotly.com/u/Mike_Purtell)\
**Post date:** [October 30, 2025, 3:52pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/7 "2025-10-30T15:52:21Z")

</div>

Hi @Avacsiglo21, great job on this week’s dashboard. Especially love the Halloween themed colors.

---

<div class="post-metadata">

**Author:** ![Avacsiglo21](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/avacsiglo21/32/29861_2.png) [@Avacsiglo21](https://community.plotly.com/u/Avacsiglo21)\
**Post date:** [October 30, 2025, 3:53pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/8 "2025-10-30T15:53:27Z")

</div>

Thanks a lot Mike

---

<div class="post-metadata">

**Author:** ![Mike\_Purtell](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/mike_purtell/32/21990_2.png) [@Mike\_Purtell](https://community.plotly.com/u/Mike_Purtell)\
**Post date:** [October 31, 2025, 12:39am UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/9 "2025-10-31T00:39:56Z")

</div>

For week 43, I wanted the convenience of Power Point for easily stepping from slide to slide, with the power of plotly for interacting with data using hover info, zoom, and other tools. So I made this dashboard using Dash Mantine carousel, with buttons on the left and right to move forward or backward by one slide. Please look through each slide and read the comments to follow the progression from a crowded and noisy slide to a clean one with call to action.

I look forward to using this approach in a professional situation. Will have to refine this a bit but I think it is doable.

I appreciate any comments, questions or suggestions. Happy Halloween to all of you.

Here is the app link to Plotly Cloud:

> **[Dash](https://0a925f2a-38c8-4c8c-ad7b-e733e6d51453.plotly.app)**

Here is a screenshot (Notice the arrows along the right and left side to advance or go back:

 ![image](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/9/7/9766c0b698cf675898157a00ce08e745e23ed0ea.png)

Here is the code:

````auto
import polars as pl
import polars.selectors as cs
import os
import plotly.express as px
import plotly.graph_objects as go
import dash
from dash import Dash, dcc, html, Input, Output
import dash_mantine_components as dmc
dash._dash_renderer._set_react_version('18.2.0')

#----- GLOBALS -----------------------------------------------------------------
style_horizontal_thick_line = {'border': 'none', 'height': '4px', 
    'background': 'linear-gradient(to right, #007bff, #ff7b00)', 
    'margin': '10px,', 'fontsize': 32}

style_h2 = {'text-align': 'center', 'font-size': '40px', 
            'fontFamily': 'Arial','font-weight': 'bold', 'color': 'gray'}

template_list = ['ggplot2', 'seaborn', 'simple_white', 'plotly','plotly_white',
    'plotly_dark', 'presentation', 'xgridoff', 'ygridoff', 'gridon', 'none']

dmc_text_red = {
    'fontSize':'16px', 
    'color':'red', 
    'textAlign':'left',
    'marginLeft':'100px'
}
# use dictionary dmc_text_red for and modify the color
dmc_text_gray = dmc_text_red.copy()
dmc_text_gray['color'] = 'gray'

{'fontSize':'16px', 'color':'gray', 'textAlign':'left','marginLeft':'100px'},
#----- LOAD AND CLEAN THE DATASET ----------------------------------------------
df = (
    pl.read_csv('Halloween.csv')
    .select(
        YEAR = pl.col('Date')
            .str.split('/')
            .list.get(2)
            # .cast(pl.Categorical),
            .cast(pl.UInt16()),
        DAY = pl.col('Day of Week').str.slice(0,3),
        TIME = pl.col('Time').str.replace('pm',' PM'),
        COUNT = pl.col('Count').cast(pl.UInt8()),
    )
)

df_yearly = (
        df
        .group_by(['YEAR'])
        .agg(pl.sum('COUNT').alias('TOTAL_COUNT'))
        .sort('YEAR')
    )
count_2022 = df_yearly.filter(pl.col('YEAR') == 2022).item(0, 'TOTAL_COUNT')
count_2024 = df_yearly.filter(pl.col('YEAR') == 2024).item(0, 'TOTAL_COUNT')
count_2025 = int(count_2024 + 0.5*(count_2024 - count_2022))

df_future = pl.DataFrame({ # predict 2025 by extrapolating from 2022, 2024
        'YEAR': [2024, 2025],
        'TOTAL_COUNT' : [count_2024, count_2025]
    })

time_list = sorted(df.unique('TIME')['TIME'].to_list())

time_color_dict = {}
for i, time in enumerate(time_list):
    # time_color_dict[time] = px.colors.qualitative.Light24[i]
    time_color_dict[time] = px.colors.qualitative.Alphabet[i]
    time_color_dict[time] = px.colors.qualitative.D3[i]
    
time_color_dict

#----- DASH COMPONENTS------ ---------------------------------------------------
select_template = (
    dmc.Select(
        label='Pick your favorite Plotly template',
        id='template',
        data= template_list,
        value=template_list[4],
        searchable=False, # Enables search functionality
        clearable=True, # Allows clearing the selection
        size='sm',
    ),
)

def get_fig(template):
    fig=px.line(
        df,
        x='YEAR',
        y='COUNT',
        color='TIME',
        color_discrete_map=time_color_dict,
        template = template,
        markers=True,
        title='Halloween TTT<br><sup>Slide 1</sup>',
        height=400, width=800
    )
    fig.update_layout(
        xaxis=dict(
            showline=True, 
            linewidth=1, 
            linecolor='gray', 
            mirror=True,
            showgrid=True, # show vertical grid lines
            tickmode='linear', # ensure ticks are evenly spaced
            dtick=1 # one tick/gridline per category
            ),
        yaxis=dict(
            showline=True, 
            linewidth=1, 
            linecolor='gray', 
            mirror=True,
            showgrid=True # show horizontal grid lines
            ),
        title_y=0.97,
    )
    fig.update_traces(line=dict( width=1))

    # Customize gridlines
    fig.update_xaxes(
        showgrid=True, # Ensure gridlines are visible
        gridwidth=1, # Thickness of vertical gridlines
        gridcolor='gray', # Color of vertical gridlines
    )

    fig.update_yaxes(
        showgrid=True, # Ensure gridlines are visible
        gridwidth=1, # Thickness of horizontal gridlines
        gridcolor='gray', # Color of horizontal gridlines
    )
    fig.update_layout(margin=dict(l=0, r=0, t=50, b=0))

    return fig

def get_carousel_slide(reviewer_text, id):
    slide = dmc.CarouselSlide(
        dmc.Stack([
            dmc.Center(dcc.Graph(figure=go.Figure(), id=id)),
            dmc.Text('Comment:', style=dmc_text_red),
            dmc.Text(reviewer_text, style=dmc_text_gray)
        ])
    )
    return slide

def update_fig(fig, slide_number, template):
    updated_fig = go.Figure(fig)
    if slide_number == 2: # change the plot title
        updated_fig.update_layout(
            title=dict(
                text=(
                    'Halloween Trick-or-Treaters by Time (TTT)<br>' + 
                    f'<sup>Slide {slide_number}</sup>'
                )
            )
        )
    if slide_number == 3: # change line shape to spline
        updated_fig.update_traces(line=dict(shape='spline'))
        updated_fig.update_layout(title=dict(
            text=(
                'Halloween Trick-or-Treaters by Time (TTT)<br>' +
                f'<sup>Slide {slide_number}</sup>'
            ))
        )
    
    if slide_number == 4: # remove x-axis and y-axis gridlines
        updated_fig.update_xaxes(showgrid=False, zeroline=False)
        updated_fig.update_yaxes(showgrid=False, zeroline=False)
        updated_fig.update_layout(title=dict(
            text=(
                'Halloween Trick-or-Treaters by Time (TTT)<br>' +           
                f'<sup>Slide {slide_number}</sup>'
            ))
        )

    if slide_number == 5: # remove top and right borders
        updated_fig.update_layout(
            xaxis=dict(
                showline=True, showgrid=False, zeroline=False, mirror=False),
            yaxis=dict(
                showline=True, showgrid=False, zeroline=False, mirror=False),
            title=dict(
                text=(
                    'Halloween Trick-or-Treaters by Time (TTT)<br>'+ 
                    f'<sup>Slide {slide_number}</sup>'
                ))
        )

    if slide_number == 6: # remove x-axis label
        updated_fig.update_xaxes(title='')
        updated_fig.update_layout(
            title=dict(
                text= ( 
                    'Halloween Trick-or-Treaters by Time (TTT)<br>' + 
                    f'<sup>Slide {slide_number}</sup>'
                ))
        )

    if slide_number == 7: # remove legend
        updated_fig.update_layout(
            showlegend=False,
            title=dict(
                text=(
                    'Halloween Trick-or-Treaters by Time (TTT)<br>'
                    f'<sup>Slide {slide_number}</sup>'
                )
            )
        )

    if slide_number == 8: # add color coded annotations
        count_list = df.filter(pl.col('YEAR') == 2024)['COUNT'].to_list()
        updated_fig.update_xaxes(range=[2008, 2025.75])
        y_shift_list = [15, 10, 10, 10, -15, -15]
        for i, shift in enumerate(y_shift_list):
            updated_fig.add_annotation(
                x=1,xref='paper', 
                y=count_list[i], yref='y', 
                text=time_list[i], showarrow=False, 
                font=dict(color=time_color_dict[time_list[i]], size=14), 
                yshift=y_shift_list[i])
        updated_fig.update_layout(
            showlegend=False,
            title=dict(
                text=(
                    'Halloween Trick-or-Treaters by Time (TTT)<br>' + 
                    f'<sup>Slide {slide_number}</sup>'
                )
            )
        )

    if slide_number == 9: # add vertical line and annotation for covid
        updated_fig.add_vline(
            x=2020, 
            line_width=2, 
            line_dash='dash',
            line_color='gray',
            annotation_text='Covid-19 Pandemic',
        )
        updated_fig.update_layout(
            showlegend=False,
            title=dict(
                text=(
                    'Halloween Trick-or-Treaters by Time (TTT)<br>' +
                    f'<sup>Slide {slide_number}</sup>'
                ))
        )
    if slide_number == 10: # add vertical line for 2013 (heavy rain)
        updated_fig.add_vline(
            x=2013, 
            line_width=2, 
            line_dash='dash',
            line_color='gray',
            annotation_text='1 inch of rain',
        )
        updated_fig.update_layout(
            showlegend=False,
            title=dict(
                text= (
                'Halloween Trick-or-Treaters by Time (TTT)<br>' + 
                f'<sup>Slide {slide_number}</sup>'
                ))
        )

    if slide_number == 11: # aggregate all time points, single trace by year
        updated_fig=px.line(
            df_yearly,
            x='YEAR',
            y='TOTAL_COUNT',
            template = template,
            markers=True,
            title='Total Halloween Trick-or-Treaters by Year',
            height=400, width=800,
            line_shape='spline',
        )
        updated_fig.update_traces(line=dict( width=1))
        updated_fig.update_layout(margin=dict(l=0, r=0, t=50, b=0))
        updated_fig.add_vline(
            x=2020, 
            line_width=2, 
            line_dash='dash',
            line_color='gray',
            annotation_text='Covid-19 Pandemic',
        )
        updated_fig.add_vline(
            x=2013, 
            line_width=2, 
            line_dash='dash',
            line_color='gray',
            annotation_text='1 inch of rain',
        )
        for i, year in enumerate([2022, 2023, 2024]):
            year_count = (
                df_yearly
                .filter(pl.col('YEAR') == year)
                .item(0, 'TOTAL_COUNT')
            )
            updated_fig.add_annotation(
                x=year,xref='x', 
                y=year_count, yref='y', 
                text=f'{year_count}', showarrow=False, 
                font=dict(color='gray', size=14), 
                yshift=20
                )
        updated_fig.update_xaxes(title='')
        updated_fig.update_layout(
            xaxis=dict(
                showline=True, 
                linewidth=1, 
                linecolor='gray',
                mirror=False, 
                ),
            yaxis=dict(
                showline=True, 
                linewidth=1, 
                linecolor='gray', 
                mirror=False     
                ),
            title_y=0.97,
        )
        updated_fig.update_layout(
            showlegend=False,
            title=dict(
                text=(
                    'Halloween Trick-or-Treaters Aggregated by Year (TTT)<br>'
                    f'<sup>Slide {slide_number}</sup>'
                ))
        )
        updated_fig.update_xaxes(showgrid=False, zeroline=False)
        updated_fig.update_yaxes(showgrid=False, zeroline=False)

    if slide_number == 12: # aggregate time points, single trace by year
        updated_fig.add_trace(go.Scatter(
            x=df_future['YEAR'],
            y=df_future['TOTAL_COUNT'],
            mode='lines+markers',
            line=dict(
                color='gray',
                dash='dot'
            ),
            marker=dict(size=8),
            name='Gray Dashed Line',
            showlegend=False
        ))
        updated_fig.add_annotation(
            x=2025,xref='x', 
            y=count_2025, yref='y', 
            text=f'{count_2025}', showarrow=False, 
            font=dict(color='red', size=14), 
            yshift=20
            )
        updated_fig.update_layout(
            showlegend=False,
            title=dict(
                text=(
                'Halloween Trick-or-Treaters Aggregated by Year<br>' +
                f'<sup>Slide {slide_number}</sup>'
                ))
        )

    if slide_number == 13: # aggregate all time points, single trace by year
        df_ordered_days = (
            pl.DataFrame({
                'DAY' : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
                'DAY_NUM' : [1, 2, 3, 4, 5, 6, 7],
                'DAY_COLOR' : ['#d3d3d3']*5 + ['orange', '#d3d3d3'],
            })
        )
        dict_day_color = dict(zip(
            df_ordered_days['DAY'], 
            df_ordered_days['DAY_COLOR']
        ))

        df_avg_by_day = (
            df
            .with_columns(DAY_COUNT = (pl.col('DAY').len().over('DAY')/6))
            .with_columns(
                NORM_DAY_COUNT = (pl.col('COUNT')/(pl.col('DAY_COUNT'))))
            .with_columns(
                DAY_TOTAL = (pl.col('NORM_DAY_COUNT').sum().over('DAY')))
            .unique(['DAY', 'DAY_TOTAL'])
            .select(['DAY', 'DAY_TOTAL'])
            .join(
                df_ordered_days,
                on='DAY',
                how='left'
            )
            .sort('DAY_NUM')
        )
        updated_fig=px.histogram(
            df_avg_by_day,
            x='DAY',
            y='DAY_TOTAL',
            color='DAY',
            color_discrete_map=dict_day_color,
            template = template,
            title=(
                '<b>Call to Action:</b>' +
                'Halloween 2025 is on a Friday (tonight) -- time to shop<br>' + 
                f'<sup>Slide {slide_number}</sup>'
            ),
            height=400, width=800,
        )
        updated_fig.update_layout(
            xaxis=dict(
                showline=True, 
                linewidth=1, 
                linecolor='gray', 
                mirror=False,
                showgrid=False, # show vertical grid lines
                tickmode='linear', # ensure ticks are evenly spaced
                dtick=1 # one tick/gridline per category
                ),
            yaxis=dict(
                showline=True, 
                linewidth=1, 
                linecolor='gray', 
                mirror=False,
                showgrid=False # show horizontal grid lines
                ),
            title_y=0.97,
        )
        updated_fig.update_xaxes(title='')
        updated_fig.update_yaxes(title='NORMALIZED TRICK OR TREATER COJNT PER DAY')
        updated_fig.update_layout(
            margin=dict(l=0, r=0, t=50, b=0),showlegend=False)
                    
    return updated_fig

# #----- DASH APPLICATION STRUCTURE---------------------------------------------
fig=go.Figure()
app = Dash()
server = app.server
app.layout = dmc.MantineProvider([
    html.Hr(style=style_horizontal_thick_line),
    dmc.Text('Trick or Treating', ta='center', style=style_h2), 
    html.Hr(style=style_horizontal_thick_line), 
    dmc.Grid(children = [dmc.GridCol(select_template, span=3, offset = 1),]),  
    dmc.Space(h=50),
    dmc.Carousel(
        withIndicators=False,
        height=500,
        slideGap='md',
        controlsOffset='sm',
        controlSize=60,
        withControls=True,
        children = [   
            get_carousel_slide(
                'Graph has trick-or-treater counts by time of day and year, ' +
                'but what the heck is TTT?',
                'fig_01'
            ),
            get_carousel_slide(
                'These curves so pointy, can they be smoothed out?',
                'fig_02'
            ),
            get_carousel_slide(
                (
                "Get rid of the gridlines. This is not PowerPoint, " +
                ' use plotly hover tips to interact with the data and ' +
                'show the exact value of any point'
                ),
                'fig_03'
            ),
            get_carousel_slide(
                "Looking better. Top and right borders are not needed, " + 
                "so get rid of them too",
                'fig_04'
            ),
            get_carousel_slide(
                'Remove the x-axis label, "YEAR", it is obvious', 
                'fig_05'
            ),
            get_carousel_slide(
                "Legend is hard to follow with spaghetti-like traces, " +
                "let's remove it",
                'fig_06'
            ),
            get_carousel_slide(
                "Now I am more confused, hard to tell what is what, " +
                    "can you put the legend back?",
                'fig_07'
            ),
            get_carousel_slide(
                'I could, but this approach with color coded ' +
                'annotations placed near the traces they represent is better. ' +
                'But what happened in 2020?',
                'fig_08'
            ),
            get_carousel_slide(
                'If 2020 was the start of the pandemic, then what happened in 2013?',
                'fig_09'
            ),
            get_carousel_slide(
                'Cincinatti Ohio had heavy rainfall in 2013, affecting turnout. ' + 
                "I don't care about the half-hourly data, " +
                "I want to know much candy to buy for this year.",
                'fig_10'
            ),
            get_carousel_slide(
                "Thanks for combining the data to show yearly totals, and " + 
                "labeling values from 2022 to 2024. Can we extrapolate to get a value for 2025?",
                'fig_11'
            ),
            get_carousel_slide(
                "Extrapolated value for 2025 is at the end of the dashed line, " +
                "which distinguishes future projections from past data." +
                'Halloween is on a Friday (tonight). Do more people show up ' +
                'on Fridays than on weeknights?',
                'fig_12'
            ),
            get_carousel_slide(
                "I thought Friday would be the busiest day for Halloween. This " +
                "data shows that Monday is busiest, Friday is the least busiest. " +
                'Glad I checked',
                'fig_13'
            )
    ])
])

@app.callback(
    Output('fig_01', 'figure'),
    Output('fig_02', 'figure'),
    Output('fig_03', 'figure'),
    Output('fig_04', 'figure'),
    Output('fig_05', 'figure'),
    Output('fig_06', 'figure'),
    Output('fig_07', 'figure'),
    Output('fig_08', 'figure'),
    Output('fig_09', 'figure'),
    Output('fig_10', 'figure'),
    Output('fig_11', 'figure'),
    Output('fig_12', 'figure'),
    Output('fig_13', 'figure'),
    Input('template', 'value'),
)
def callback(template):
    fig_01 = get_fig(template)
    fig_02 = update_fig(fig_01, 2, template)
    fig_03 = update_fig(fig_02, 3, template)
    fig_04 = update_fig(fig_03, 4, template)
    fig_05 = update_fig(fig_04, 5, template)
    fig_06 = update_fig(fig_05, 6, template)
    fig_07 = update_fig(fig_06, 7, template)
    fig_08 = update_fig(fig_07, 8, template)
    fig_09 = update_fig(fig_08, 9, template)
    fig_10 = update_fig(fig_09, 10, template)
    fig_11 = update_fig(fig_10, 11, template)
    fig_12 = update_fig(fig_11, 12, template)
    fig_13 = update_fig(fig_12, 13, template)
    
    return (
        fig_01, fig_02, fig_03, fig_04, fig_05, fig_06, fig_07, fig_08,
        fig_09, fig_10, fig_11, fig_12, fig_13
    )

if __name__ == ' __main__':
    app.run(debug=True)```
````

---

<div class="post-metadata">

**Author:** ![Avacsiglo21](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/avacsiglo21/32/29861_2.png) [@Avacsiglo21](https://community.plotly.com/u/Avacsiglo21)\
**Post date:** [October 31, 2025, 12:36pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/10 "2025-10-31T12:36:31Z")

</div>

Hi Mike,

Nice job! I have a quick question regarding some of the templates.

When I select the templates presentation, xgridoff, ygridoff, or gridon, the x and y scales (or axes labels/tick marks) seem to hide or disappear.

I’ve noticed that these templates also seem to be “bigger” or have a different size/layout compared to the others (like plotly\_white, ggplot2, or seaborn).

Is there a way to fix this issue with the scales?

---

<div class="post-metadata">

**Author:** ![adamschroeder](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/adamschroeder/32/17280_2.png) [@adamschroeder](https://community.plotly.com/u/adamschroeder)\
**Post date:** [October 31, 2025, 2:51pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/11 "2025-10-31T14:51:19Z")

</div>

> [@Mike\_Purtell](#):
>
> [https://0a925f2a-38c8-4c8c-ad7b-e733e6d51453.plotly.app](https://0a925f2a-38c8-4c8c-ad7b-e733e6d51453.plotly.app)

What a magnificent way to use the Dash Mantine carousel 💪 @AnnMarieW you should take a look if you have a minute. This is a great way to do presentations. Thank you for the idea, @Mike_Purtell .

Also, those comments at the bottom of the page are great, because they create the whole story.  
Similar to what @Avacsiglo21 has said, do you know why the graph title gets cut off sometimes? Does this have to do with the carousel?

 ![image](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/d/7/d7f7a263d1e47d7221c080fd6066d13895d84528.png)

---

<div class="post-metadata">

**Author:** ![Mike\_Purtell](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/mike_purtell/32/21990_2.png) [@Mike\_Purtell](https://community.plotly.com/u/Mike_Purtell)\
**Post date:** [October 31, 2025, 5:22pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/12 "2025-10-31T17:22:41Z")

</div>

Thank you @adamschroeder. I don’t know why the graph gets cut off but I noticed I could avoid that by zooming out a little bit. I am intrigued by the idea of plotly interaction on a presentation slide and will pursue this further. Collaborative exploration and experimentation like this are great benefit of this group. Happy Halloween to my Freaky, Frightening, and Fun Figure Friday Friends.

---

<div class="post-metadata">

**Author:** ![Mike\_Purtell](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/mike_purtell/32/21990_2.png) [@Mike\_Purtell](https://community.plotly.com/u/Mike_Purtell)\
**Post date:** [October 31, 2025, 5:33pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/13 "2025-10-31T17:33:04Z")

</div>

Hi Alex, what you are seeing is the default behavior of various templates. In my view the templates are all very good and offer much convenience, but sometimes they only fit well in a specific context. For example, the transition from having gridlines to not having them doesn’t really make sense for templates that don’t enable gridlines by default. Regarding the issue with the scales, my approach is to either use a different template or just override the template’s attributes with update\_layout or update\_traces. Thank you.

---

<div class="post-metadata">

**Author:** ![Ester](https://avatars.discourse-cdn.com/v4/letter/e/f04885/32.png) [@Ester](https://community.plotly.com/u/Ester)\
**Post date:** [October 31, 2025, 5:41pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/14 "2025-10-31T17:41:41Z")

</div>

🎃 **Halloween Night Dashboard — Cursor Powered** 👻

• An **interactive dashboard** revealing Halloween night trick-or-treat trends across multiple years.  
• **Built with Cursor** , blending intelligent coding with fast, precise iteration.  
• A **dark, cinematic interface** wrapped in deep black, orange, and red Halloween tones.  
• **Smart filters** for year, day of week, and time range enable focused exploration.  
• Eye-catching **KPI cards** summarize total, average, and peak visitor counts.  
• A glowing **Bubble Heatmap** shows when and where activity reached its height.  
• A sleek **Bar Chart** highlights yearly changes and peak Halloween seasons.  
• **Instant, real-time updates** keep the data experience fluid and engaging.  
• Designed with **Plotly, Dash, and Bootstrap Darkly** for modern responsiveness.

 ![Screenshot 2025-10-29 154530](https://us1.discourse-cdn.com/flex024/uploads/plot/original/3X/1/9/1969253b9485debb283361858778114683287a2c.png)

> **Code**
>
> ````import
> import plotly.express as px
> import plotly.graph_objects as go
> from dash import Dash, dcc, html, Input, Output
> import dash_bootstrap_components as dbc
> 
> # Load data
> df = pd.read_csv("Halloween.csv")
> 
> # Parse and enrich fields
> df["Date and Time"] = pd.to_datetime(df["Date and Time"], format="%m/%d/%y %H:%M")
> df["Date"] = pd.to_datetime(df["Date"], format="%m/%d/%y")
> df["Year"] = df["Date"].dt.year
> 
> # Ensure Time ordering
> time_order = [
> "6:00pm",
> "6:30pm",
> "7:00pm",
> "7:30pm",
> "8:00pm",
> "8:15pm",
> ]
> df["Time"] = pd.Categorical(df["Time"], categories=time_order, ordered=True)
> 
> # Define day order
> day_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
> df['Day of Week'] = pd.Categorical(df['Day of Week'], categories=day_order, ordered=True)
> 
> # App setup with Bootstrap dark theme
> app = Dash( __name__ , external_stylesheets=[dbc.themes.DARKLY])
> server = app.server
> 
> # Add responsive meta tag and custom CSS
> app.index_string = '''
> <!DOCTYPE html>
> <html>
> <head>
> {%metas%}
> <title>{%title%}</title>
> {%favicon%}
> {%css%}
> <meta name="viewport" content="width=device-width, initial-scale=1.0">
> <style>
> /* Custom dark theme overrides */
> .card {
> background-color: #141418 !important;
> border: 1px solid #2a2a33 !important;
> }
> .card-header {
> background-color: #1a1a1d !important;
> border-bottom: 1px solid #2a2a33 !important;
> color: #ececf1 !important;
> }
>             
> /* Summary number highlight */
> .summary-number {
> color: #ffcc33; /* warm halloween yellow */
> font-weight: 700;
> font-size: 1.15rem;
> }
>             
> /* Multi-select dropdown dark background */
> .Select-control {
> background-color: #000000 !important;
> border-color: #2a2a33 !important;
> }
> .Select-menu-outer {
> background-color: #000000 !important;
> border-color: #2a2a33 !important;
> }
> .Select-option {
> background-color: #000000 !important;
> color: #ececf1 !important;
> }
> .Select-option:hover {
> background-color: #141418 !important;
> }
> .Select-option.is-selected {
> background-color: #2a2a33 !important;
> }
> .Select-value-label {
> color: #ececf1 !important;
> }
> .Select-placeholder {
> color: #666 !important;
> }
> .Select-input > input {
> color: #ececf1 !important;
> }
> /* Year dropdown X close buttons - red */
> .Select-value-icon {
> color: #ff0000 !important;
> }
> .Select-value-icon:hover {
> color: #ff3333 !important;
> }
> /* RangeSlider styling - selected range is red */
> .rc-slider-rail {
> background-color: #444 !important;
> }
> .rc-slider-track {
> background-color: #ff0000 !important;
> }
> .rc-slider-handle {
> border-color: red !important;
> }
> .rc-slider-handle:hover {
> border-color: #ff3333 !important;
> }
> /* No auto animations; animations only via chart Play button */
> </style>
> </head>
> <body>
> {%app_entry%}
> <footer>
> {%config%}
> {%scripts%}
> {%renderer%}
> </footer>
> </body>
> </html>
> '''
> 
> background_color = "#0b0b0d" # near-black
> panel_color = "#141418"
> text_color = "#ececf1"
> orange = "#ff7a00"
> purple = "#6a0dad"
> green = "#3cff9a"
> 
> app.layout = dbc.Container(
> fluid=True,
> style={"backgroundColor": background_color, "minHeight": "100vh", "padding": "20px"},
> children=[
> # Header
> dbc.Row([
> dbc.Col([
> html.H1("Halloween Night Dashboard", className="mb-3", style={"color": orange, "margin": 0}),
> html.P("Explore trick-or-treat counts by year, day, and time.", 
> className="mb-4", style={"opacity": 0.8}),
> ])
> ]),
> 
> # KPI Stats Cards
> dbc.Row([
> dbc.Col([
> dbc.Card([
> dbc.CardBody([
> html.H4(id="kpi-total", className="mb-1", style={"color": "#ffffff", "fontWeight": "bold"}),
> html.P("Total Count", className="mb-0", style={"opacity": 0.8, "fontSize": "0.9rem"})
> ])
> ], className="text-center")
> ], width=3),
> dbc.Col([
> dbc.Card([
> dbc.CardBody([
> html.H4(id="kpi-avg", className="mb-1", style={"color": "#ffffff", "fontWeight": "bold"}),
> html.P("Average Count", className="mb-0", style={"opacity": 0.8, "fontSize": "0.9rem"})
> ])
> ], className="text-center")
> ], width=3),
> dbc.Col([
> dbc.Card([
> dbc.CardBody([
> html.H4(id="kpi-max", className="mb-1", style={"color": "#ffffff", "fontWeight": "bold"}),
> html.P("Peak Count", className="mb-0", style={"opacity": 0.8, "fontSize": "0.9rem"})
> ])
> ], className="text-center")
> ], width=3),
> dbc.Col([
> dbc.Card([
> dbc.CardBody([
> html.H4(id="kpi-best-time", className="mb-1", style={"color": "#ffffff", "fontWeight": "bold"}),
> html.P("Peak Time", className="mb-0", style={"opacity": 0.8, "fontSize": "0.9rem"})
> ])
> ], className="text-center")
> ], width=3),
> ], className="mb-4"),
> 
> # Filters at the top
> dbc.Row([
> dbc.Col([
> dbc.Card([
> dbc.CardHeader("Filters", style={"backgroundColor": "#1a1a1d"}),
> dbc.CardBody([
> dbc.Row([
> dbc.Col([
> dbc.Label("Year", className="fw-bold mb-2"),
> dcc.Dropdown(
> id="filter-year",
> options=[{"label": "All", "value": "all"}] + [{"label": str(y), "value": int(y)} for y in sorted(df["Year"].unique())],
> multi=True,
> value=["all"], # Default to "all"
> placeholder="Select year(s)",
> ),
> ], width=4),
> dbc.Col([
> dbc.Label("Day of Week", className="fw-bold mb-2"),
> dcc.Dropdown(
> id="filter-dow",
> options=[{"label": "All", "value": "all"}] + [{"label": d, "value": d} for d in df["Day of Week"].unique()],
> multi=True,
> value=["all"], # Default to "all"
> placeholder="Select days",
> ),
> ], width=4),
> dbc.Col([
> dbc.Label("Time Range", className="fw-bold mb-2"),
> dcc.RangeSlider(
> id="filter-time",
> min=0,
> max=len(time_order) - 1,
> step=1,
> value=[0, len(time_order) - 1],
> marks={i: t for i, t in enumerate(time_order)},
> allowCross=False,
> ),
> ], width=4),
> ])
> ])
> ], className="mb-4")
> ])
> ]),
> 
> # Store for cross-filtering state
> dcc.Store(id="selected-data-store", data={"selected_year": None, "selected_day_time": None}),
> 
> # Two Charts Side by Side
> dbc.Row([
> dbc.Col([
> dbc.Card([
> dbc.CardHeader("Bar Chart (with Trend)"),
> dbc.CardBody([
> dcc.Graph(id="graph-scatter", config={"displaylogo": False, "staticPlot": False})
> ])
> ])
> ], width=6),
> dbc.Col([
> dbc.Card([
> dbc.CardHeader("Bubble Heatmap"),
> dbc.CardBody([
> dcc.Graph(id="graph-heatmap", config={"displaylogo": False, "staticPlot": False})
> ])
> ])
> ], width=6),
> ], className="mb-4"),
> 
> # Summary Section
> dbc.Row([
> dbc.Col([
> dbc.Card([
> dbc.CardHeader("Summary", style={"backgroundColor": "#1a1a1d"}),
> dbc.CardBody([
> html.Div(id="summary-content", style={"fontSize": "0.95rem", "lineHeight": "1.6"})
> ])
> ])
> ])
> ]),
> ]
> )
> 
> def dark_layout(fig):
> fig.update_layout(
> template="plotly_dark",
> paper_bgcolor=panel_color,
> plot_bgcolor=panel_color,
> font_color=text_color,
> margin=dict(l=40, r=20, t=50, b=40),
> )
> return fig
> 
> @app.callback(
> Output("graph-heatmap", "figure"),
> Output("graph-scatter", "figure"),
> Output("kpi-total", "children"),
> Output("kpi-avg", "children"),
> Output("kpi-max", "children"),
> Output("kpi-best-time", "children"),
> Output("summary-content", "children"),
> Input("filter-year", "value"),
> Input("filter-dow", "value"),
> Input("filter-time", "value"),
> )
> def update_figures(years, dows, time_idx_range):
> filtered = df.copy()
>     
> # Handle "all" option for years
> if years:
> if "all" in years:
> # If "all" is selected, show all years (don't filter)
> pass
> else:
> # Filter out "all" from years list and filter by actual year values
> year_values = [y for y in years if y != "all" and isinstance(y, int)]
> if year_values:
> filtered = filtered[filtered["Year"].isin(year_values)]
>     
> # Handle "all" option for days of week
> if dows:
> if "all" in dows:
> # If "all" is selected, show all days (don't filter)
> pass
> else:
> # Filter out "all" from dows list and filter by actual day values
> dow_values = [d for d in dows if d != "all" and isinstance(d, str)]
> if dow_values:
> filtered = filtered[filtered["Day of Week"].isin(dow_values)]
>     
> start_i, end_i = time_idx_range
> valid_times = time_order[start_i : end_i + 1]
> filtered = filtered[filtered["Time"].isin(valid_times)]
> 
> # Calculate KPIs
> total_count = filtered["Count"].sum()
> avg_count = filtered["Count"].mean() if len(filtered) > 0 else 0
> max_count = filtered["Count"].max() if len(filtered) > 0 else 0
> best_time_row = filtered.loc[filtered["Count"].idxmax()] if len(filtered) > 0 else None
> best_time = best_time_row["Time"] if best_time_row is not None else "N/A"
> 
> # Bubble heatmap - Time vs Day of Week with circles
> # Get actual days present in data
> days_in_data = filtered["Day of Week"].unique()
> # Filter day_order to only include days present in data
> day_order_filtered = [day for day in day_order if day in days_in_data]
>     
> pivot = (
> filtered.groupby(["Day of Week", "Time"], as_index=False)["Count"].sum()
> .pivot(index="Time", columns="Day of Week", values="Count")
> .reindex(index=time_order, columns=day_order_filtered)
> )
>     
> # Prepare data for bubble chart (heatmap with circles)
> bubble_heatmap = go.Figure()
>     
> # Colors for heatmap intensity
> max_value = pivot.values.max() if not pivot.empty else 250
> threshold_ratio = min(50 / max_value, 0.4) if max_value > 0 else 0.2
> max_val = max_value # Maximum value for red highlighting
>     
> # Create bubbles for each cell - only max is red, others with opacity
> for col_idx, col in enumerate(pivot.columns):
> for time_idx, time in enumerate(pivot.index):
> value = pivot.loc[time, col]
> if pd.notna(value) and value > 0:
> # Max value: red color
> if value == max_val:
> color = "#ff0000" # Red for maximum
> opacity = 0.9 # More opaque
> border_width = 1.5
> border_color = 'rgba(255,255,255,0.6)'
> else:
> # Other values: original colors with opacity
> opacity = 0.6 # Opacity for other bubbles
> border_width = 1.5
> border_color = 'rgba(255,255,255,0.6)'
> if value <= max_value * 0.25:
> color = "white"
> elif value <= max_value * 0.5:
> color = "#e74c3c" # Rich red
> elif value <= max_value * 0.75:
> color = "#f39c12" # Warm orange
> else:
> color = "#ffcc33" # Bright yellow-orange
>                 
> bubble_heatmap.add_trace(go.Scatter(
> x=[time_idx],
> y=[col_idx], # Use column index instead of column name
> mode='markers',
> marker=dict(
> size=value / max_value * 50 + 10, # Scale bubble size
> color=color,
> line=dict(width=border_width, color=border_color),
> opacity=opacity
> ),
> name=f"{col} - {time}",
> hovertemplate=f"Day: {col}<br>Time: {time}<br>Count: {value:.0f}<extra></extra>",
> showlegend=False
> ))
>     
> # No trendline on bubble heatmap as requested
>     
> bubble_heatmap.update_layout(
> title=dict(text="Bubble Heatmap: Activity by Day and Time", x=0.5, xanchor="center"),
> xaxis=dict(
> title="Time",
> tickmode='array',
> tickvals=list(range(len(time_order))),
> ticktext=time_order,
> ),
> yaxis=dict(
> title="Day of Week",
> tickmode='array',
> tickvals=list(range(len(day_order_filtered))),
> ticktext=day_order_filtered,
> autorange='reversed' # Reverse y-axis so Monday is at top
> ),
> hovermode='closest',
> uirevision='constant'
> )
> bubble_heatmap = dark_layout(bubble_heatmap)
> 
> # Bar chart with annotations - Year vs Total Count
> yearly_totals = filtered.groupby("Year")["Count"].sum().reset_index()
> yearly_totals = yearly_totals.sort_values("Year")
>     
> bar_chart = go.Figure()
>     
> # Find maximum value
> max_year_value = yearly_totals["Count"].max()
>     
> # Create bar chart with vertical gradient (each bar has gradient from bottom to top)
> # Use colorscale for vertical gradient effect
> # Separate bars: non-max bars with normal border, max bar with thick border
> max_year = yearly_totals.loc[yearly_totals["Count"].idxmax(), "Year"]
>     
> # Non-max bars - darker colors
> non_max_data = yearly_totals[yearly_totals["Year"] != max_year]
> if len(non_max_data) > 0:
> bar_chart.add_trace(go.Bar(
> x=non_max_data["Year"],
> y=non_max_data["Count"],
> name='Total Count by Year',
> marker=dict(
> color=non_max_data["Count"],
> colorscale=[[0, '#ffb366'], [0.5, '#ff9900'], [1, '#ff7700']], # Orange gradient
> showscale=False,
> opacity=0.75, # Darker opacity
> line=dict(width=0) # No border for non-max bars
> ),
> text=[f"{int(count):,}" for count in non_max_data["Count"]],
> textposition='outside',
> textfont=dict(size=12),
> hovertemplate="Year: %{x}<br>Total Count: %{y:,}<extra></extra>"
> ))
>     
> # Max bar - red color
> max_data = yearly_totals[yearly_totals["Year"] == max_year]
> if len(max_data) > 0:
> bar_chart.add_trace(go.Bar(
> x=max_data["Year"],
> y=max_data["Count"],
> name='Max Count',
> marker=dict(
> color='#ff0000', # Red color for max
> opacity=0.9, # More opaque
> line=dict(width=5, color='white') # Thick white border
> ),
> text=[f"{int(count):,}" for count in max_data["Count"]],
> textposition='outside',
> textfont=dict(size=18, color='#ffffff'), # Larger white text
> hovertemplate="Year: %{x}<br>Total Count: %{y:,}<extra></extra>"
> ))
>     
> bar_chart.update_layout(
> title=dict(text="Total Count by Year", x=0.5, xanchor="center"),
> xaxis_title="Year",
> yaxis_title="Total Count",
> hovermode='closest',
> uirevision='constant',
> bargap=0
> )
> bar_chart = dark_layout(bar_chart)
> 
> # Generate summary content in English
> # Handle "all" option when counting
> years_count = None
> if years:
> if "all" in years:
> years_count = "all"
> else:
> years_count = len([y for y in years if y != "all"])
>     
> dows_count = None
> if dows:
> if "all" in dows:
> dows_count = "all"
> else:
> dows_count = len([d for d in dows if d != "all"])
>     
> time_range_text = f"{time_order[start_i]} to {time_order[end_i]}" if time_idx_range else "all times"
>     
> # Find trends
> year_trend = ""
> if len(filtered["Year"].unique()) > 1:
> yearly_totals = filtered.groupby("Year")["Count"].sum().sort_index()
> if len(yearly_totals) >= 2:
> first_year = yearly_totals.iloc[0]
> last_year = yearly_totals.iloc[-1]
> if last_year > first_year:
> year_trend = f"📈 {((last_year - first_year) / first_year * 100):.1f}% increase from {yearly_totals.index[0]} to {yearly_totals.index[-1]}"
> elif last_year < first_year:
> year_trend = f"📉 {((first_year - last_year) / first_year * 100):.1f}% decrease from {yearly_totals.index[0]} to {yearly_totals.index[-1]}"
> else:
> year_trend = f"➡️ Stable counts from {yearly_totals.index[0]} to {yearly_totals.index[-1]}"
>     
> # Find peak day
> peak_day = filtered.groupby("Day of Week")["Count"].sum().idxmax() if len(filtered) > 0 else "N/A"
>     
> # Find most active year
> most_active_year = filtered.groupby("Year")["Count"].sum().idxmax() if len(filtered) > 0 else "N/A"
>     
> summary_content = [
> html.P([
> html.Strong("Data Overview: "), 
> "Showing data for ",
> (html.Span(f"{years_count}", className="summary-number") if years_count is not None else "all"),
> " year(s), ",
> (html.Span(f"{dows_count}", className="summary-number") if dows_count is not None else "all"),
> " day(s), from ",
> html.Span(time_range_text, className="summary-number"),
> "."
> ], className="mb-2"),
>         
> html.P([
> html.Strong("Key Insights: "),
> "Peak activity occurs at ", html.Span(str(best_time), className="summary-number"),
> " with ", html.Span(f"{max_count:,}", className="summary-number"), " visitors. ",
> "The most active day is ", html.Span(str(peak_day), className="summary-number"),
> " and the busiest year was ", html.Span(str(most_active_year), className="summary-number"), "."
> ], className="mb-2"),
>         
> html.P([
> html.Strong("Trends: "),
> year_trend if year_trend else "Single year data - no trend analysis available."
> ], className="mb-2"),
>         
> html.P([
> html.Strong("Data Quality: "),
> "Total of ", html.Span(f"{len(filtered):,}", className="summary-number"), " data points with an average of ",
> html.Span(f"{avg_count:.1f}", className="summary-number"), " visitors per time slot."
> ], className="mb-0")
> ]
> 
> return (
> bubble_heatmap, 
> bar_chart,
> html.Span(f"{total_count:,.0f}", style={"fontWeight": "bold"}),
> html.Span(f"{avg_count:.1f}", style={"fontWeight": "bold"}),
> html.Span(f"{max_count:,.0f}", style={"fontWeight": "bold"}),
> html.Span(str(best_time), style={"fontWeight": "bold"}),
> summary_content
> )
> 
> if __name__ == " __main__":
> app.run(debug=True)```
> 
> ````

---

<div class="post-metadata">

**Author:** ![adamschroeder](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/adamschroeder/32/17280_2.png) [@adamschroeder](https://community.plotly.com/u/adamschroeder)\
**Post date:** [November 3, 2025, 2:53pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/15 "2025-11-03T14:53:25Z")

</div>

very interesting app, @Ester .  
Is this your first time building a Dash app with Cursor?  
How long did it take you?

---

<div class="post-metadata">

**Author:** ![Ester](https://avatars.discourse-cdn.com/v4/letter/e/f04885/32.png) [@Ester](https://community.plotly.com/u/Ester)\
**Post date:** [November 3, 2025, 3:05pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/16 "2025-11-03T15:05:01Z")

</div>

@adamschroeder Well, I didn’t watch the time, I just tested it, trying to convert the charts to Halloween style, but there were many errors and I had to undo the steps several times.  
The chart on the right wants to look like candy. 😀🎃

---

<div class="post-metadata">

**Author:** ![adamschroeder](https://sea2.discourse-cdn.com/flex024/user_avatar/community.plotly.com/adamschroeder/32/17280_2.png) [@adamschroeder](https://community.plotly.com/u/adamschroeder)\
**Post date:** [November 3, 2025, 3:28pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/17 "2025-11-03T15:28:36Z")

</div>

That’s what I thought as well when I looked at the bubble chart. It’s looks like candy pieces 🙂  
Do you prefer Cursor over other Python IDEs or you’re just testing it out?

---

<div class="post-metadata">

**Author:** ![Ester](https://avatars.discourse-cdn.com/v4/letter/e/f04885/32.png) [@Ester](https://community.plotly.com/u/Ester)\
**Post date:** [November 3, 2025, 3:34pm UTC](https://community.plotly.com/t/figure-friday-2025-week-43/95242/18 "2025-11-03T15:34:09Z")

</div>

I prefer to stick with VSCode, I just tried this too. Sometimes it’s good, but I switch tools in between.
