Loading Module…

πŸ“ˆ Plotly

24 topics • Click any card to expand

1. Plotly Express Basics

plotly.express (px) creates interactive charts in one line. Charts are HTML/JavaScript β€” hover, zoom, and pan by default.

First chart: px.scatter
import plotly.express as px

# Built-in dataset
df = px.data.gapminder().query("year == 2007")

fig = px.scatter(
    df, x='gdpPercap', y='lifeExp',
    size='pop', color='continent',
    hover_name='country',
    log_x=True,
    title='GDP per Capita vs Life Expectancy (2007)',
    labels={'gdpPercap': 'GDP per Capita (log)', 'lifeExp': 'Life Expectancy'}
)
fig.show()
px.line β€” time series
import plotly.express as px

df = px.data.gapminder().query("continent == 'Europe'")

fig = px.line(
    df, x='year', y='lifeExp',
    color='country',
    hover_name='country',
    title='Life Expectancy in Europe Over Time',
    labels={'lifeExp': 'Life Expectancy', 'year': 'Year'}
)
fig.update_layout(showlegend=False)   # too many countries for legend
fig.show()
px.line with hover_data and markers
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
months = pd.date_range('2024-01', periods=12, freq='MS')
df = pd.DataFrame({
    'month':   list(months) * 3,
    'revenue': np.concatenate([
        100 + np.cumsum(np.random.randn(12) * 5),
        80  + np.cumsum(np.random.randn(12) * 4),
        60  + np.cumsum(np.random.randn(12) * 3),
    ]),
    'region':  ['North']*12 + ['South']*12 + ['West']*12,
    'target':  np.concatenate([np.full(12, 110), np.full(12, 85), np.full(12, 65)]),
})
df['vs_target'] = (df['revenue'] - df['target']).round(1)

fig = px.line(
    df, x='month', y='revenue',
    color='region', markers=True,
    hover_data=['target', 'vs_target'],
    title='Monthly Revenue by Region with Target Context',
    labels={'revenue': 'Revenue ($K)', 'month': 'Month'},
    color_discrete_sequence=px.colors.qualitative.Bold,
)
fig.update_traces(marker=dict(size=8))
fig.update_layout(height=420, hovermode='x unified')
fig.show()
Range selector buttons and slider
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(7)
dates = pd.date_range('2020-01-01', periods=365*4, freq='D')
price = 100 + np.cumsum(np.random.randn(len(dates)) * 1.2)

df = pd.DataFrame({'date': dates, 'price': price.round(2)})

fig = px.line(
    df, x='date', y='price',
    title='Stock Price β€” Interactive Range Selector',
    labels={'price': 'Price ($)', 'date': 'Date'},
)
fig.update_traces(line=dict(color='#636EFA', width=1.5))

# Range selector buttons (1M, 3M, 6M, 1Y, All)
fig.update_xaxes(
    rangeslider_visible=True,
    rangeselector=dict(
        buttons=[
            dict(count=1,  label='1M', step='month', stepmode='backward'),
            dict(count=3,  label='3M', step='month', stepmode='backward'),
            dict(count=6,  label='6M', step='month', stepmode='backward'),
            dict(count=1,  label='1Y', step='year',  stepmode='backward'),
            dict(step='all', label='All'),
        ]
    )
)
fig.update_layout(height=460, xaxis_rangeslider_thickness=0.05)
fig.show()
print("Trace type:", fig.data[0].type)
print("Date range:", df['date'].min().date(), "to", df['date'].max().date())
Faceted Box Plot with Outlier Annotations
import plotly.express as px
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
n   = 300
df  = pd.DataFrame({
    'department': rng.choice(['Engineering','Sales','Marketing','Finance'], n),
    'level':      rng.choice(['Junior','Mid','Senior'], n),
    'salary':     rng.normal(85000, 20000, n).clip(40000, 180000),
})

fig = px.box(
    df, x='level', y='salary',
    facet_col='department', facet_col_wrap=2,
    color='level',
    color_discrete_sequence=px.colors.qualitative.Set2,
    points='outliers',
    title='Salary Distribution by Department & Level',
    labels={'salary': 'Annual Salary ($)', 'level': 'Level'},
    width=900, height=600,
    category_orders={'level': ['Junior', 'Mid', 'Senior']},
)
fig.update_traces(marker=dict(size=4, opacity=0.6))
fig.update_layout(showlegend=False, title_font_size=15)
fig.show()
💼 Real-World: Sales KPI Interactive Dashboard
A product manager builds a quick interactive scatter to explore the relationship between marketing spend and revenue by market.
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
markets = ['US','UK','DE','FR','JP','BR','IN','AU','CA','MX']
df = pd.DataFrame({
    'market':       markets,
    'ad_spend':     np.random.uniform(50, 500, 10).round(1),
    'revenue':      np.random.uniform(200, 2000, 10).round(1),
    'customers':    np.random.randint(500, 10000, 10),
    'region':       ['Americas','Europe','Europe','Europe',
                     'Asia','Americas','Asia','Pacific','Americas','Americas'],
})
df['roi'] = (df['revenue'] / df['ad_spend']).round(2)

fig = px.scatter(
    df, x='ad_spend', y='revenue',
    size='customers', color='region',
    text='market',
    hover_data=['roi', 'customers'],
    title='Marketing Spend vs Revenue by Market',
    labels={'ad_spend': 'Ad Spend ($K)', 'revenue': 'Revenue ($K)'},
    size_max=50
)
fig.update_traces(textposition='top center', textfont_size=10)
fig.update_layout(height=450)
fig.show()
🏋️ Practice: Multi-Line Chart with Hover Data
Using px.data.gapminder(), create a multi-line chart of life expectancy over time for exactly 5 countries of your choice. Add pop and gdpPercap as hover_data. Style the markers and use hovermode='x unified' so all lines show on hover.
Candlestick Chart with Volume
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
n   = 60
dates = pd.date_range('2024-01-01', periods=n, freq='B')
close = 100 + np.cumsum(rng.normal(0, 1.5, n))
high  = close + rng.uniform(0.5, 3, n)
low   = close - rng.uniform(0.5, 3, n)
open_ = close - rng.uniform(-1.5, 1.5, n)
vol   = rng.integers(500000, 2000000, n)

fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
                    row_heights=[0.7, 0.3],
                    vertical_spacing=0.03)

fig.add_trace(go.Candlestick(
    x=dates, open=open_, high=high, low=low, close=close,
    name='OHLC', increasing_line_color='#26a69a',
    decreasing_line_color='#ef5350'), row=1, col=1)

colors = ['#26a69a' if c >= o else '#ef5350'
          for c, o in zip(close, open_)]
fig.add_trace(go.Bar(x=dates, y=vol, marker_color=colors,
                     name='Volume', opacity=0.7), row=2, col=1)

# 10-day moving average
ma10 = pd.Series(close).rolling(10).mean()
fig.add_trace(go.Scatter(x=dates, y=ma10, mode='lines',
                         line=dict(color='orange', width=2, dash='dot'),
                         name='MA10'), row=1, col=1)

fig.update_layout(title='Stock OHLC + Volume', xaxis_rangeslider_visible=False,
                  height=550, template='plotly_dark',
                  hovermode='x unified')
fig.show()
Waterfall Chart for Revenue Analysis
import plotly.graph_objects as go

items  = ['Starting Revenue','Product A',  'Product B', 'Churn',
          'Upsell',          'Discounts',  'Net Revenue']
values = [1000, 250, 180, -120, 90, -60, 0]
values[-1] = sum(values[:-1])

measure = ['absolute'] + ['relative']*(len(items)-2) + ['total']

fig = go.Figure(go.Waterfall(
    name='Revenue',
    orientation='v',
    measure=measure,
    x=items,
    y=values,
    text=[f'${v:+,}' if v != 0 else f'${abs(values[-1]):,}' for v in values],
    textposition='outside',
    connector={'line': {'color': 'rgb(63, 63, 63)'}},
    increasing={'marker': {'color': '#26a69a'}},
    decreasing={'marker': {'color': '#ef5350'}},
    totals={'marker': {'color': '#7e57c2'}},
))

fig.update_layout(
    title='Annual Revenue Waterfall Analysis',
    title_font_size=16,
    yaxis_title='Revenue ($K)',
    waterfallgap=0.3,
    height=500,
    template='plotly_white',
    showlegend=False,
)
fig.show()
Starter Code
import plotly.express as px

df = px.data.gapminder()

# Pick 5 countries
countries = ['United States', 'China', 'India', 'Brazil', 'Germany']
# TODO: filtered = df[df['country'].isin(countries)]

# TODO: fig = px.line(
#     filtered, x='year', y='lifeExp',
#     color='country', markers=True,
#     hover_data=['pop', 'gdpPercap'],
#     title='Life Expectancy Trends β€” 5 Countries',
# )
# TODO: fig.update_traces(marker=dict(size=8))
# TODO: fig.update_layout(hovermode='x unified', height=450)
# TODO: fig.show()
✅ Practice Checklist
2. Bar & Pie Charts

px.bar for categorical comparisons; px.pie and px.sunburst for part-to-whole. All support hover, faceting, and animation.

px.bar with facets
import plotly.express as px

df = px.data.tips()

fig = px.bar(
    df, x='day', y='total_bill',
    color='sex', barmode='group',
    facet_col='time',
    title='Total Bill by Day, Gender, and Meal Time',
    labels={'total_bill': 'Total Bill ($)', 'day': 'Day'},
    color_discrete_sequence=px.colors.qualitative.Set2
)
fig.update_layout(height=400)
fig.show()
px.pie and px.sunburst
import plotly.express as px
import pandas as pd

# Pie chart
df_pie = pd.DataFrame({
    'channel':  ['Organic', 'Paid Search', 'Social', 'Email', 'Direct'],
    'sessions': [35, 25, 20, 12, 8],
})
fig1 = px.pie(df_pie, names='channel', values='sessions',
              title='Traffic by Channel', hole=0.4)
fig1.show()

# Sunburst β€” hierarchical
df_sun = px.data.gapminder().query("year==2007 and continent in ['Europe','Americas']")
fig2 = px.sunburst(df_sun, path=['continent','country'],
                   values='pop', color='lifeExp',
                   color_continuous_scale='RdYlGn',
                   title='Population & Life Expectancy')
fig2.show()
Grouped and stacked bar comparison
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(7)
quarters = ['Q1', 'Q2', 'Q3', 'Q4']
products = ['Widget', 'Gadget', 'Gizmo']

rows = []
for q in quarters:
    for p in products:
        rows.append({'quarter': q, 'product': p,
                     'revenue': round(np.random.uniform(40, 120), 1)})
df = pd.DataFrame(rows)

# Grouped bars
fig1 = px.bar(df, x='quarter', y='revenue', color='product',
              barmode='group',
              title='Quarterly Revenue β€” Grouped',
              color_discrete_sequence=px.colors.qualitative.Pastel)
fig1.update_layout(height=380)
fig1.show()

# Stacked bars (same data)
fig2 = px.bar(df, x='quarter', y='revenue', color='product',
              barmode='stack',
              title='Quarterly Revenue β€” Stacked',
              color_discrete_sequence=px.colors.qualitative.Pastel)
fig2.update_layout(height=380)
fig2.show()
Waterfall chart with go.Waterfall
import plotly.graph_objects as go

# P&L waterfall: starting revenue, additions, deductions, net
labels   = ['Gross Revenue', 'COGS', 'Gross Profit',
            'Operating Exp', 'EBITDA', 'D&A', 'Tax', 'Net Income']
measures = ['absolute', 'relative', 'total',
            'relative',  'total',   'relative', 'relative', 'total']
values   = [500, -180, None, -120, None, -30, -42, None]

fig = go.Figure(go.Waterfall(
    name='P&L 2024',
    orientation='v',
    measure=measures,
    x=labels,
    y=[500, -180, 0, -120, 0, -30, -42, 0],
    text=['+$500', '-$180', '$320', '-$120', '$200', '-$30', '-$42', '$128'],
    textposition='outside',
    connector=dict(line=dict(color='#444', width=1, dash='dot')),
    increasing=dict(marker=dict(color='#00CC96')),
    decreasing=dict(marker=dict(color='#EF553B')),
    totals=dict(marker=dict(color='#636EFA')),
))

fig.update_layout(
    title='2024 P&L Waterfall Chart',
    yaxis_title='Amount ($K)',
    height=450,
    showlegend=False,
    plot_bgcolor='#1a1a2e',
    paper_bgcolor='#16213e',
    font=dict(color='#e0e0e0'),
    yaxis=dict(gridcolor='#333'),
)
fig.show()
print("Waterfall traces:", len(fig.data))
print("Net Income: $128K")
💼 Real-World: Revenue Breakdown by Product & Region
A CFO uses an interactive sunburst chart to drill down from region β†’ product category β†’ SKU in the quarterly review.
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(5)
regions    = ['North America', 'Europe', 'Asia Pacific']
categories = ['Electronics', 'Clothing', 'Home', 'Food']
skus_per   = 3

rows = []
for region in regions:
    for cat in categories:
        for sku_i in range(1, skus_per + 1):
            rows.append({
                'region':   region,
                'category': cat,
                'sku':      f'{cat[:3]}-{sku_i:03d}',
                'revenue':  round(np.random.uniform(50, 500), 1),
            })
df = pd.DataFrame(rows)

fig = px.sunburst(
    df, path=['region', 'category', 'sku'],
    values='revenue',
    color='revenue',
    color_continuous_scale='Blues',
    title='Q2 2024 Revenue Drill-Down (Region β†’ Category β†’ SKU)',
)
fig.update_layout(height=550, coloraxis_showscale=False)
fig.show()
🏋️ Practice: Sunburst Drill-Down Chart
Build a px.sunburst chart using a DataFrame with three levels: continent -> country -> city (invent 2-3 cities per country). Use population as the values column and color by a numeric metric (e.g. GDP). Add a meaningful title and set height=520.
Starter Code
import plotly.express as px
import pandas as pd

# Build a simple hierarchical dataset
rows = [
    # continent, country, city, population, gdp_index
    ('Americas', 'USA', 'New York',    8_000_000, 95),
    ('Americas', 'USA', 'Los Angeles', 4_000_000, 88),
    ('Americas', 'Brazil', 'Sao Paulo',12_000_000, 55),
    ('Americas', 'Brazil', 'Rio',       6_500_000, 50),
    ('Europe',   'Germany', 'Berlin',   3_600_000, 82),
    ('Europe',   'Germany', 'Munich',   1_500_000, 90),
    ('Europe',   'France',  'Paris',    2_100_000, 78),
    ('Europe',   'France',  'Lyon',       500_000, 70),
    ('Asia',     'Japan',   'Tokyo',   13_900_000, 87),
    ('Asia',     'Japan',   'Osaka',    2_700_000, 80),
    ('Asia',     'India',   'Mumbai',  20_000_000, 42),
    ('Asia',     'India',   'Delhi',   30_000_000, 38),
]
df = pd.DataFrame(rows, columns=['continent','country','city','population','gdp_index'])

# TODO: fig = px.sunburst(
#     df, path=['continent', 'country', 'city'],
#     values='population',
#     color='gdp_index',
#     color_continuous_scale='Blues',
#     title='Population Drill-Down: Continent -> Country -> City',
# )
# TODO: fig.update_layout(height=520)
# TODO: fig.show()
✅ Practice Checklist
3. Histogram & Box Plot

px.histogram and px.box create interactive distribution charts. Hover shows exact statistics; click legend to toggle groups.

px.histogram with marginal
import plotly.express as px

df = px.data.tips()

fig = px.histogram(
    df, x='total_bill',
    color='time', barmode='overlay',
    marginal='box',       # adds mini box plot on top
    opacity=0.7,
    nbins=25,
    title='Total Bill Distribution by Meal Time',
    labels={'total_bill': 'Total Bill ($)'},
    color_discrete_sequence=['#636EFA','#EF553B']
)
fig.show()
px.box and px.violin
import plotly.express as px
import pandas as pd

df = px.data.tips()

fig1 = px.box(df, x='day', y='tip', color='smoker',
              notched=True,
              title='Tip Distribution β€” Box Plot (notched)',
              labels={'tip': 'Tip ($)'})
fig1.show()

fig2 = px.violin(df, x='day', y='total_bill', color='sex',
                 box=True, points='outliers',
                 title='Total Bill β€” Violin + Box',
                 labels={'total_bill': 'Total Bill ($)'})
fig2.show()
Faceted histogram across categories
import plotly.express as px
import numpy as np
import pandas as pd

np.random.seed(42)
groups = ['Group A', 'Group B', 'Group C']
rows = []
for g in groups:
    mean = {'Group A': 50, 'Group B': 65, 'Group C': 80}[g]
    vals = np.random.normal(mean, 12, 200)
    for v in vals:
        rows.append({'group': g, 'score': round(v, 1)})
df = pd.DataFrame(rows)

fig = px.histogram(
    df, x='score', facet_col='group',
    color='group', nbins=30,
    opacity=0.75,
    marginal='violin',
    title='Score Distribution Faceted by Group',
    labels={'score': 'Score'},
    color_discrete_sequence=px.colors.qualitative.Set1,
)
fig.update_layout(height=420, showlegend=False)
fig.show()
Notched box plot with strip overlay
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np

np.random.seed(42)
departments = ['Engineering', 'Sales', 'Marketing', 'Support']
rows = []
for dept in departments:
    base = {'Engineering': 95, 'Sales': 72, 'Marketing': 68, 'Support': 60}[dept]
    scores = np.random.normal(base, 10, 60).clip(0, 100)
    for s in scores:
        rows.append({'department': dept, 'score': round(s, 1)})
df = pd.DataFrame(rows)

# Notched violin with embedded box and all points
fig = px.violin(
    df, x='department', y='score',
    color='department',
    box=True,
    points='all',
    title='Performance Score Distribution by Department',
    labels={'score': 'Score', 'department': 'Department'},
    color_discrete_sequence=px.colors.qualitative.Set2,
)
fig.update_traces(
    meanline_visible=True,
    jitter=0.3,
    pointpos=-1.5,
    marker=dict(size=3, opacity=0.5),
)
fig.update_layout(height=480, showlegend=False)
fig.show()
for dept in departments:
    vals = df[df['department']==dept]['score']
    print(f"{dept}: median={vals.median():.1f}, std={vals.std():.1f}")
💼 Real-World: A/B Test Distribution Explorer
A data scientist uses interactive histograms to explore the full distribution of test results across experiment variants.
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from scipy import stats

np.random.seed(42)
control = np.random.normal(45.0, 12, 500)
variant = np.random.normal(49.5, 11, 500)

df = pd.DataFrame({
    'revenue':  np.concatenate([control, variant]),
    'group':    ['Control']*500 + ['Variant B']*500,
})

t, p = stats.ttest_ind(control, variant)

fig = px.histogram(
    df, x='revenue', color='group',
    barmode='overlay', opacity=0.65, nbins=35,
    marginal='violin',
    color_discrete_map={'Control':'#636EFA','Variant B':'#EF553B'},
    title=f'A/B Test Revenue Distribution  |  p-value={p:.4f} {"βœ“ Significant" if p<0.05 else "βœ— Not significant"}',
    labels={'revenue': 'Revenue ($)'},
)
fig.add_vline(x=control.mean(), line_dash='dash', line_color='#636EFA',
              annotation_text=f'Control ΞΌ={control.mean():.1f}')
fig.add_vline(x=variant.mean(), line_dash='dash', line_color='#EF553B',
              annotation_text=f'Variant ΞΌ={variant.mean():.1f}')
fig.update_layout(height=450)
fig.show()
🏋️ Practice: Grouped Violin Plot
Using px.data.tips(), create a violin plot of total_bill grouped by day on the x-axis, colored by smoker (yes/no). Enable box=True and points='all'. Then add a second figure: a faceted histogram of tip amounts, faceted by time (Lunch/Dinner), with a 'rug' marginal.
Parallel Coordinates Plot for Multivariate Exploration
import plotly.express as px
import pandas as pd
import numpy as np

rng = np.random.default_rng(0)
n   = 300
df  = pd.DataFrame({
    'tenure':       rng.exponential(24, n).clip(1, 120),
    'usage_rate':   rng.beta(5, 2, n) * 100,
    'support_calls':rng.poisson(3, n),
    'billing_score':rng.normal(75, 15, n).clip(0, 100),
    'monthly_spend':rng.lognormal(4, 0.5, n),
    'churn':        rng.choice([0, 1], n, p=[0.75, 0.25]),
})

fig = px.parallel_coordinates(
    df,
    color='churn',
    color_continuous_scale=px.colors.diverging.Tealrose,
    color_continuous_midpoint=0.5,
    dimensions=['tenure','usage_rate','support_calls','billing_score','monthly_spend'],
    labels={
        'tenure':        'Tenure (mo)',
        'usage_rate':    'Usage %',
        'support_calls': 'Support Calls',
        'billing_score': 'Billing Score',
        'monthly_spend': 'Monthly Spend',
    },
    title='Parallel Coordinates: Customer Churn Features',
)
fig.update_layout(height=450, title_font_size=15,
                  coloraxis_colorbar=dict(title='Churn Risk', tickvals=[0,1],
                                          ticktext=['Low','High']))
fig.show()
Radar / Spider Chart for Multi-Metric Comparison
import plotly.graph_objects as go
import numpy as np

categories = ['Speed', 'Accuracy', 'Recall', 'Precision', 'F1-Score', 'AUC-ROC']
models = {
    'Logistic Regression': [65, 72, 68, 75, 71, 74],
    'Random Forest':       [80, 85, 83, 87, 85, 88],
    'XGBoost':             [88, 90, 89, 91, 90, 92],
    'Neural Network':      [75, 88, 86, 89, 87, 90],
}
colors = ['#636EFA','#EF553B','#00CC96','#AB63FA']

fig = go.Figure()
for (model, values), color in zip(models.items(), colors):
    vals = values + [values[0]]  # close the polygon
    cats = categories + [categories[0]]
    fig.add_trace(go.Scatterpolar(
        r=vals, theta=cats, fill='toself',
        name=model, line_color=color,
        fillcolor=color, opacity=0.25,
        hovertemplate='%{theta}: %{r}<extra>' + model + '</extra>',
    ))

fig.update_layout(
    polar=dict(radialaxis=dict(visible=True, range=[50, 100],
                                tickfont=dict(size=9))),
    title='Model Performance Radar Chart',
    title_font_size=15,
    legend=dict(x=1.05, y=0.5),
    height=500,
    template='plotly_white',
)
fig.show()
Starter Code
import plotly.express as px

df = px.data.tips()

# 1. Grouped violin: total_bill by day, colored by smoker
# TODO: fig1 = px.violin(
#     df, x='day', y='total_bill', color='smoker',
#     box=True, points='all',
#     title='Total Bill Distribution by Day and Smoker Status',
# )
# TODO: fig1.update_layout(height=450)
# TODO: fig1.show()

# 2. Faceted histogram: tip by time with rug marginal
# TODO: fig2 = px.histogram(
#     df, x='tip', facet_col='time',
#     color='time', marginal='rug',
#     nbins=20, opacity=0.8,
#     title='Tip Distribution: Lunch vs Dinner',
# )
# TODO: fig2.update_layout(height=400)
# TODO: fig2.show()
✅ Practice Checklist
4. Heatmap & Scatter Matrix

px.imshow renders 2D matrices as interactive heatmaps. px.scatter_matrix creates an interactive pairplot grid.

px.imshow β€” correlation heatmap
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
features = ['Revenue', 'Cost', 'Margin', 'Customers', 'NPS']
n = 200
data = np.random.randn(n, 5)
# Add correlations
data[:, 2] = 0.8*data[:,0] - 0.6*data[:,1] + np.random.randn(n)*0.3
data[:, 4] = 0.5*data[:,2] + np.random.randn(n)*0.7

df   = pd.DataFrame(data, columns=features)
corr = df.corr().round(2)

fig = px.imshow(
    corr,
    text_auto=True,
    color_continuous_scale='RdBu_r',
    zmin=-1, zmax=1,
    title='Feature Correlation Matrix'
)
fig.update_layout(height=450)
fig.show()
px.scatter_matrix
import plotly.express as px

df = px.data.iris()

fig = px.scatter_matrix(
    df,
    dimensions=['sepal_length','sepal_width','petal_length','petal_width'],
    color='species',
    symbol='species',
    title='Iris Dataset β€” Interactive Scatter Matrix',
    labels={col: col.replace('_',' ') for col in df.columns}
)
fig.update_traces(diagonal_visible=False, showupperhalf=False)
fig.update_layout(height=600)
fig.show()
Annotated heatmap with go.Heatmap
import plotly.graph_objects as go
import numpy as np

np.random.seed(3)
days    = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
hours   = [f'{h:02d}:00' for h in range(9, 18)]
z_data  = np.random.poisson(5, (len(days), len(hours))).astype(float)
# Simulate peak hours mid-day
z_data[:, 2:6] += np.random.poisson(8, (len(days), 4))

# Build annotation text
annotations = []
for i, day in enumerate(days):
    for j, hr in enumerate(hours):
        annotations.append(dict(
            x=hr, y=day,
            text=str(int(z_data[i, j])),
            font=dict(color='white' if z_data[i, j] > 10 else 'black', size=11),
            showarrow=False,
        ))

fig = go.Figure(data=go.Heatmap(
    z=z_data, x=hours, y=days,
    colorscale='YlOrRd',
    hoverongaps=False,
))
fig.update_layout(
    title='Support Tickets: Day Γ— Hour (with annotations)',
    annotations=annotations,
    height=380,
    xaxis_title='Hour', yaxis_title='Day',
)
fig.show()
Custom colorscale heatmap β€” monthly sales grid
import plotly.graph_objects as go
import numpy as np

np.random.seed(8)
products = ['Widget', 'Gadget', 'Gizmo', 'Doohickey', 'Thingamajig']
months   = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
            'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

# Simulate seasonal sales data
z = np.random.uniform(20, 100, (len(products), len(months)))
# Add seasonality: Q4 boost
z[:, 9:] *= 1.4
# Add product-specific trends
z[0, :] += np.linspace(0, 20, 12)
z = z.round(1)

# Custom discrete colorscale: red -> yellow -> green
custom_scale = [
    [0.0,  '#d73027'],
    [0.25, '#f46d43'],
    [0.5,  '#ffffbf'],
    [0.75, '#74add1'],
    [1.0,  '#313695'],
]

# Build cell annotations
annotations = []
for i, prod in enumerate(products):
    for j, mon in enumerate(months):
        annotations.append(dict(
            x=mon, y=prod,
            text=str(int(z[i, j])),
            showarrow=False,
            font=dict(size=9,
                      color='white' if z[i, j] > 75 or z[i, j] < 35 else 'black'),
        ))

fig = go.Figure(data=go.Heatmap(
    z=z, x=months, y=products,
    colorscale=custom_scale,
    colorbar=dict(title='Units Sold'),
    hovertemplate='Product: %{y}<br>Month: %{x}<br>Sales: %{z}<extra></extra>',
))
fig.update_layout(
    title='Monthly Sales Heatmap β€” Custom Colorscale',
    annotations=annotations,
    height=380,
    xaxis_title='Month',
    yaxis_title='Product',
)
fig.show()
print("Peak month:", months[int(z.sum(axis=0).argmax())])
print("Top product:", products[int(z.sum(axis=1).argmax())])
💼 Real-World: Operations Metrics Heatmap
An operations manager visualizes a week Γ— hour activity heatmap to identify peak load periods for staffing.
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(7)
hours = list(range(24))
days  = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']

# Simulate ticket volumes: high weekday business hours
data = np.random.poisson(3, (7, 24)).astype(float)
data[0:5, 9:18] += np.random.poisson(12, (5, 9))   # weekday 9-18
data[0:5, 0:6]  *= 0.3                              # overnight low
data[5:7, :]    *= 0.5                              # weekend lower

df = pd.DataFrame(data.round(0).astype(int),
                  index=days, columns=[f'{h:02d}:00' for h in hours])

fig = px.imshow(
    df,
    color_continuous_scale='YlOrRd',
    aspect='auto',
    title='Support Ticket Volume β€” Week Γ— Hour Heatmap',
    labels=dict(x='Hour', y='Day', color='Tickets')
)
fig.update_xaxes(tickangle=45, tickmode='array',
                 tickvals=list(range(0,24,2)),
                 ticktext=[f'{h:02d}:00' for h in range(0,24,2)])
fig.update_layout(height=380)
fig.show()
🏋️ Practice: Annotated Correlation Heatmap
Load px.data.iris() and compute the correlation matrix of the four numeric columns. Use px.imshow with text_auto='.2f', the RdBu_r colorscale, and zmin=-1/zmax=1. Then build a second heatmap using go.Heatmap directly, adding manual annotation text showing each correlation value. Compare the two approaches.
Interactive Treemap with Drill-Down
import plotly.express as px
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
regions   = ['North America','Europe','Asia Pacific','Latin America']
countries = {
    'North America': ['USA','Canada','Mexico'],
    'Europe':        ['Germany','France','UK','Spain'],
    'Asia Pacific':  ['China','Japan','India','Australia'],
    'Latin America': ['Brazil','Argentina','Colombia'],
}
rows = []
for region, clist in countries.items():
    for country in clist:
        for product in ['Software','Hardware','Services']:
            rows.append({
                'region': region, 'country': country, 'product': product,
                'revenue': rng.uniform(50, 500),
                'growth':  rng.uniform(-10, 40),
            })

df = pd.DataFrame(rows)

fig = px.treemap(
    df,
    path=[px.Constant('Global'), 'region', 'country', 'product'],
    values='revenue',
    color='growth',
    color_continuous_scale='RdYlGn',
    color_continuous_midpoint=15,
    title='Global Revenue Treemap β€” Click to Drill Down',
    hover_data={'revenue': ':.1f', 'growth': ':.1f'},
)
fig.update_traces(textinfo='label+value+percent parent',
                  hovertemplate='<b>%{label}</b><br>Revenue: $%{value:.1f}M<br>Growth: %{color:.1f}%')
fig.update_layout(height=600, title_font_size=15,
                  coloraxis_colorbar=dict(title='Growth %'))
fig.show()
Sankey Diagram for Flow Analysis
import plotly.graph_objects as go

# Marketing funnel flow
labels = [
    'Website Visitors',  # 0
    'Ad Campaign',       # 1
    'Organic Search',    # 2
    'Social Media',      # 3
    'Product Page',      # 4
    'Add to Cart',       # 5
    'Checkout',          # 6
    'Purchased',         # 7
    'Abandoned',         # 8
]

source = [0, 0, 0, 1, 2, 3, 4, 5, 6]
target = [1, 2, 3, 4, 4, 4, 5, 6, 7]
value  = [3000, 5000, 2000, 2800, 4200, 1500, 4500, 2000, 2500]

link_colors = ['rgba(100,149,237,0.4)'] * len(source)
for i, t in enumerate(target):
    if t == 8:
        link_colors[i] = 'rgba(255,99,71,0.4)'

fig = go.Figure(go.Sankey(
    arrangement='snap',
    node=dict(
        pad=15, thickness=20,
        line=dict(color='white', width=0.5),
        label=labels,
        color=['#4a90d9','#5ab4ac','#d4b483','#8fc97e',
               '#feb24c','#f03b20','#43a2ca','#2ca25f','#ef5350'],
    ),
    link=dict(
        source=source, target=target, value=value,
        color=link_colors,
        hovertemplate='%{source.label} β†’ %{target.label}: %{value:,}<extra></extra>',
    ),
))
fig.update_layout(title_text='Marketing Funnel β€” Sankey Diagram',
                  title_font_size=16, height=500, font_size=12,
                  template='plotly_white')
fig.show()
Bubble Chart with Time Slider
import plotly.express as px
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
countries = ['USA','China','India','Germany','UK','France','Japan','Brazil','Canada','Australia']
years = range(2018, 2025)

rows = []
for country in countries:
    gdp   = rng.uniform(1000, 25000)
    pop   = rng.uniform(50, 1400)
    for year in years:
        rows.append({
            'country':    country,
            'year':       year,
            'gdp_growth': rng.normal(2.5, 1.5),
            'gdp_pc':     gdp * (1 + rng.normal(0.025, 0.01))**(year-2018),
            'population': pop + rng.normal(0, 2),
            'continent':  'Americas' if country in ['USA','Brazil','Canada'] else
                          'Europe'   if country in ['Germany','UK','France'] else
                          'Asia'     if country in ['China','India','Japan'] else 'Oceania',
        })

df = pd.DataFrame(rows)

fig = px.scatter(
    df, x='gdp_pc', y='gdp_growth',
    size='population', color='continent',
    animation_frame='year', animation_group='country',
    hover_name='country',
    log_x=True,
    size_max=55,
    color_discrete_sequence=px.colors.qualitative.Bold,
    title='GDP per Capita vs Growth Rate (2018–2024)',
    labels={'gdp_pc': 'GDP per Capita (USD, log)', 'gdp_growth': 'GDP Growth (%)'},
    range_x=[500, 40000], range_y=[-3, 8],
)
fig.update_layout(height=550, title_font_size=15)
fig.show()
Starter Code
import plotly.express as px
import plotly.graph_objects as go
import numpy as np

df = px.data.iris()
cols = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
corr = df[cols].corr().round(2)

# 1. px.imshow version (easy)
# TODO: fig1 = px.imshow(
#     corr, text_auto='.2f',
#     color_continuous_scale='RdBu_r', zmin=-1, zmax=1,
#     title='Iris Correlation Matrix (px.imshow)',
# )
# TODO: fig1.update_layout(height=420)
# TODO: fig1.show()

# 2. go.Heatmap version with manual annotations
z = corr.values
labels = corr.columns.tolist()
# TODO: annotations = []
# TODO: for i in range(len(labels)):
#     for j in range(len(labels)):
#         annotations.append(dict(
#             x=labels[j], y=labels[i],
#             text=str(z[i, j]),
#             showarrow=False,
#             font=dict(color='white' if abs(z[i,j]) > 0.5 else 'black'),
#         ))
# TODO: fig2 = go.Figure(data=go.Heatmap(
#     z=z, x=labels, y=labels,
#     colorscale='RdBu_r', zmin=-1, zmax=1,
# ))
# TODO: fig2.update_layout(title='Iris Correlation (go.Heatmap + annotations)',
#                          annotations=annotations, height=420)
# TODO: fig2.show()
✅ Practice Checklist
5. 3D Charts

Plotly renders true 3D scatter and surface plots in the browser. Drag to rotate, scroll to zoom.

3D scatter plot
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({
    'x':       np.random.randn(300),
    'y':       np.random.randn(300),
    'z':       np.random.randn(300),
    'cluster': np.random.choice(['A','B','C'], 300),
    'size':    np.random.uniform(5, 20, 300),
})
# Separate clusters
df.loc[df.cluster=='A','x'] += 2
df.loc[df.cluster=='B','y'] += 2
df.loc[df.cluster=='C','z'] += 2

fig = px.scatter_3d(df, x='x', y='y', z='z',
                    color='cluster', size='size',
                    opacity=0.7,
                    title='3D Cluster Visualization')
fig.update_layout(height=500)
fig.show()
3D surface plot
import plotly.graph_objects as go
import numpy as np

x = np.linspace(-3, 3, 60)
y = np.linspace(-3, 3, 60)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2)) * np.exp(-0.1*(X**2+Y**2))

fig = go.Figure(data=[
    go.Surface(z=Z, x=X, y=Y,
               colorscale='Viridis',
               contours=dict(z=dict(show=True, usecolormap=True,
                                    highlightcolor='white', project_z=True)))
])
fig.update_layout(
    title='3D Surface: Damped Sinc Function',
    scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Z'),
    height=500
)
fig.show()
3D scatter with color scale and hover
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(99)
n = 500
# Spiral helix dataset
t = np.linspace(0, 4 * np.pi, n)
df = pd.DataFrame({
    'x':        np.cos(t) + np.random.randn(n) * 0.15,
    'y':        np.sin(t) + np.random.randn(n) * 0.15,
    'z':        t / (2 * np.pi),   # height increases with angle
    'intensity': np.sin(t) ** 2,
    'label':    [f'Point {i}' for i in range(n)],
})

fig = px.scatter_3d(
    df, x='x', y='y', z='z',
    color='intensity',
    color_continuous_scale='Plasma',
    hover_name='label',
    hover_data={'x': ':.2f', 'y': ':.2f', 'z': ':.2f'},
    opacity=0.75,
    title='3D Helix β€” Color by Intensity',
    size_max=6,
)
fig.update_traces(marker=dict(size=3))
fig.update_layout(height=520)
fig.show()
3D line plot and surface from meshgrid
import plotly.graph_objects as go
import numpy as np

# -- 3D line: double helix DNA-like structure --
t = np.linspace(0, 6 * np.pi, 300)
r = 1.5

fig = go.Figure()
# Strand 1
fig.add_trace(go.Scatter3d(
    x=r * np.cos(t), y=r * np.sin(t), z=t / np.pi,
    mode='lines',
    line=dict(color='#636EFA', width=5),
    name='Strand 1',
))
# Strand 2 (180 degrees offset)
fig.add_trace(go.Scatter3d(
    x=r * np.cos(t + np.pi), y=r * np.sin(t + np.pi), z=t / np.pi,
    mode='lines',
    line=dict(color='#EF553B', width=5),
    name='Strand 2',
))
# Rungs (every 15th point)
step = 15
for i in range(0, len(t), step):
    fig.add_trace(go.Scatter3d(
        x=[r*np.cos(t[i]), r*np.cos(t[i]+np.pi)],
        y=[r*np.sin(t[i]), r*np.sin(t[i]+np.pi)],
        z=[t[i]/np.pi, t[i]/np.pi],
        mode='lines',
        line=dict(color='#aaa', width=2),
        showlegend=False,
    ))

# -- 3D surface: saddle function z = x^2 - y^2 --
xs = np.linspace(-2, 2, 40)
ys = np.linspace(-2, 2, 40)
Xs, Ys = np.meshgrid(xs, ys)
Zs = Xs**2 - Ys**2

fig2 = go.Figure(data=[go.Surface(
    x=Xs, y=Ys, z=Zs,
    colorscale='RdBu',
    colorbar=dict(title='z = xΒ²-yΒ²'),
)])
fig2.update_layout(
    title='3D Saddle Surface (z = xΒ² - yΒ²)',
    scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Z'),
    height=480,
)

fig.update_layout(
    title='3D Double Helix Line Plot',
    scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Turn'),
    height=520,
    showlegend=True,
)
fig.show()
fig2.show()
print("Double helix: 2 strands, rungs every 15 points")
print("Saddle surface grid:", Xs.shape)
💼 Real-World: 3D Risk Surface for Options Pricing
A quant visualizes how an option's price changes with underlying price and time-to-expiry using a 3D surface.
import plotly.graph_objects as go
import numpy as np
from scipy.stats import norm

def black_scholes_call(S, K, T, r, sigma):
    # Avoid division by zero
    T = np.where(T < 1e-6, 1e-6, T)
    d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    return S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)

K     = 100      # strike price
r     = 0.05     # risk-free rate
sigma = 0.20     # volatility

S_vals = np.linspace(70, 130, 50)   # underlying price
T_vals = np.linspace(0.02, 1.0, 50) # time to expiry (years)
S_grid, T_grid = np.meshgrid(S_vals, T_vals)
C_grid = black_scholes_call(S_grid, K, T_grid, r, sigma)

fig = go.Figure(data=[go.Surface(
    x=S_grid, y=T_grid, z=C_grid,
    colorscale='Plasma',
    colorbar=dict(title='Call Price ($)')
)])
fig.update_layout(
    title=f'Black-Scholes Call Option Price  (K={K}, Οƒ={sigma})',
    scene=dict(
        xaxis_title='Underlying Price (S)',
        yaxis_title='Time to Expiry (T)',
        zaxis_title='Call Price ($)',
    ),
    height=520
)
fig.show()
🏋️ Practice: 3D Scatter with Size, Color, and Hover
Create a 3D scatter plot of 400 random points in three clusters separated along all three axes. Color by cluster label, size the markers by a 'confidence' column (random 5-20), and add meaningful hover_data. Print a summary of how many points are in each cluster.
Starter Code
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
n_per = 133

# Build three clusters
def make_cluster(cx, cy, cz, label, n):
    return pd.DataFrame({
        'x': np.random.randn(n) + cx,
        'y': np.random.randn(n) + cy,
        'z': np.random.randn(n) + cz,
        'cluster': label,
        'confidence': np.random.uniform(5, 20, n).round(1),
    })

# TODO: df = pd.concat([
#     make_cluster(0, 0, 0, 'Alpha', n_per),
#     make_cluster(4, 0, 0, 'Beta',  n_per),
#     make_cluster(2, 4, 2, 'Gamma', n_per),
# ], ignore_index=True)

# Print cluster summary
# TODO: print(df.groupby('cluster')[['x','y','z']].mean().round(2))

# TODO: fig = px.scatter_3d(
#     df, x='x', y='y', z='z',
#     color='cluster', size='confidence',
#     hover_data={'confidence': True, 'x': ':.2f', 'y': ':.2f', 'z': ':.2f'},
#     opacity=0.75,
#     title='3D Cluster Scatter β€” Size by Confidence',
# )
# TODO: fig.update_layout(height=520)
# TODO: fig.show()
✅ Practice Checklist
6. Subplots with make_subplots

make_subplots creates multi-panel figures. Mix chart types, share axes, and control spacing β€” all in one interactive figure.

2Γ—2 subplot grid
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

np.random.seed(5)
x = np.linspace(0, 10, 100)

fig = make_subplots(rows=2, cols=2,
                    subplot_titles=['Line','Bar','Scatter','Histogram'])

fig.add_trace(go.Scatter(x=x, y=np.sin(x), name='sin'), row=1, col=1)
fig.add_trace(go.Bar(x=['A','B','C','D'], y=[4,7,3,8], name='bar'), row=1, col=2)
fig.add_trace(go.Scatter(x=np.random.randn(100), y=np.random.randn(100),
                         mode='markers', name='scatter',
                         marker=dict(opacity=0.5)), row=2, col=1)
fig.add_trace(go.Histogram(x=np.random.randn(500), name='hist'), row=2, col=2)

fig.update_layout(title='Multi-Type Dashboard', height=500, showlegend=False)
fig.show()
Shared x-axis β€” price + volume
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd

np.random.seed(9)
dates  = pd.date_range('2024-01-01', periods=60, freq='B')
price  = 100 + np.cumsum(np.random.randn(60) * 1.5)
volume = np.random.randint(1000, 8000, 60)
colors = ['green' if price[i] >= price[i-1] else 'red' for i in range(len(price))]

fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
                    row_heights=[0.7, 0.3],
                    vertical_spacing=0.03)

fig.add_trace(go.Scatter(x=dates, y=price, name='Price',
                         line=dict(color='royalblue', width=2)), row=1, col=1)
fig.add_trace(go.Bar(x=dates, y=volume, name='Volume',
                     marker_color=colors, opacity=0.7), row=2, col=1)

fig.update_layout(title='Stock Price & Volume', height=500, showlegend=False)
fig.update_xaxes(rangeslider_visible=False)
fig.show()
2Γ—2 grid with mixed chart types and shared axes
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd

np.random.seed(11)
x = np.linspace(0, 2 * np.pi, 80)
dates = pd.date_range('2024-01', periods=12, freq='MS')
revenue = 50 + np.cumsum(np.random.randn(12) * 4)
categories = ['Q1', 'Q2', 'Q3', 'Q4']
vals_a = [22, 35, 28, 41]
vals_b = [18, 29, 33, 37]

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=['Sine & Cosine Waves', 'Monthly Revenue',
                    'Grouped Bar by Quarter', 'Normal Distribution'],
    vertical_spacing=0.12, horizontal_spacing=0.1,
)

# Row 1, Col 1: Two line traces
fig.add_trace(go.Scatter(x=x, y=np.sin(x), name='sin',
                         line=dict(color='#636EFA')), row=1, col=1)
fig.add_trace(go.Scatter(x=x, y=np.cos(x), name='cos',
                         line=dict(color='#EF553B', dash='dash')), row=1, col=1)

# Row 1, Col 2: Area chart
fig.add_trace(go.Scatter(x=dates, y=revenue.round(1), fill='tozeroy',
                         line=dict(color='#00CC96'), name='Revenue'), row=1, col=2)

# Row 2, Col 1: Grouped bars
fig.add_trace(go.Bar(x=categories, y=vals_a, name='Product A',
                     marker_color='#AB63FA'), row=2, col=1)
fig.add_trace(go.Bar(x=categories, y=vals_b, name='Product B',
                     marker_color='#FFA15A'), row=2, col=1)

# Row 2, Col 2: Histogram
fig.add_trace(go.Histogram(x=np.random.randn(400), nbinsx=25,
                            marker_color='#19D3F3', name='Normal'), row=2, col=2)

fig.update_layout(title='Multi-Panel Dashboard', height=580,
                  barmode='group', showlegend=True,
                  legend=dict(orientation='h', y=-0.08))
fig.show()
Bar + Line + Scatter in shared-axis subplots
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd

np.random.seed(17)
months = pd.date_range('2024-01', periods=12, freq='MS')
sales    = np.random.randint(30, 100, 12)
target   = np.full(12, 70)
cum_sales = sales.cumsum()
margin_pct = np.random.uniform(0.15, 0.45, 12).round(3)

# 3 rows: bar chart, line overlay, scatter β€” all share x-axis
fig = make_subplots(
    rows=3, cols=1,
    shared_xaxes=True,
    row_heights=[0.5, 0.25, 0.25],
    vertical_spacing=0.05,
    subplot_titles=['Monthly Sales vs Target',
                    'Cumulative Sales',
                    'Margin %'],
)

# Row 1: Bar (actual) + Line (target)
bar_colors = ['#00CC96' if s >= 70 else '#EF553B' for s in sales]
fig.add_trace(go.Bar(x=months, y=sales, name='Actual',
                     marker_color=bar_colors), row=1, col=1)
fig.add_trace(go.Scatter(x=months, y=target, name='Target',
                         line=dict(color='white', dash='dash', width=2),
                         mode='lines'), row=1, col=1)

# Row 2: Cumulative area line
fig.add_trace(go.Scatter(x=months, y=cum_sales, fill='tozeroy',
                         name='Cumulative', line=dict(color='#636EFA')), row=2, col=1)

# Row 3: Scatter dots for margin
fig.add_trace(go.Scatter(x=months, y=(margin_pct * 100).round(1),
                         mode='markers+lines', name='Margin %',
                         marker=dict(color='#AB63FA', size=8),
                         line=dict(color='#AB63FA', width=1.5)), row=3, col=1)
fig.add_hline(y=30, line_dash='dot', line_color='gray',
              annotation_text='30% target', row=3, col=1)

fig.update_layout(
    title='Sales Performance β€” Bar + Line + Scatter Subplots',
    height=600, showlegend=True,
    legend=dict(orientation='h', y=-0.06),
    template='plotly_dark',
)
fig.update_xaxes(rangeslider_visible=False)
fig.show()
print("Traces:", [t.type for t in fig.data])
print("Total sales:", int(cum_sales[-1]))
💼 Real-World: Marketing Analytics Dashboard
A growth team builds a 4-panel Plotly dashboard showing funnel, revenue trend, channel mix, and conversion by cohort.
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd

np.random.seed(3)
months  = pd.date_range('2024-01', periods=12, freq='MS')
revenue = 100 + np.cumsum(np.random.randn(12)*8) + np.arange(12)*5
sessions= np.random.randint(8000, 20000, 12)
cvr     = revenue / sessions * 100

channels = ['Organic','Paid','Social','Email','Direct']
ch_vals  = [35, 25, 18, 12, 10]

funnel_stages  = ['Visit','Sign Up','Trial','Paid']
funnel_vals    = [10000, 3200, 1100, 420]

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=['Revenue Trend ($K)', 'Traffic by Channel',
                    'Conversion Funnel', 'CVR vs Sessions'],
    specs=[[{},{}],[{},{}]]
)

# Revenue
fig.add_trace(go.Scatter(x=months, y=revenue.round(1), fill='tozeroy',
                         line=dict(color='#636EFA'), name='Revenue'), row=1, col=1)

# Channel pie β€” use domain trace
fig.add_trace(go.Pie(labels=channels, values=ch_vals, hole=0.35,
                     showlegend=False, textinfo='label+percent'), row=1, col=2)

# Funnel
fig.add_trace(go.Funnel(y=funnel_stages, x=funnel_vals,
                        marker_color=['#636EFA','#EF553B','#00CC96','#AB63FA'],
                        textinfo='value+percent initial'), row=2, col=1)

# CVR vs Sessions scatter
fig.add_trace(go.Scatter(x=sessions, y=cvr.round(3), mode='markers+text',
                         text=[m.strftime('%b') for m in months],
                         textposition='top center',
                         marker=dict(size=10, color='#FFA15A')), row=2, col=2)

fig.update_layout(title='Growth Dashboard β€” 2024', height=650)
fig.show()
🏋️ Practice: 2Γ—2 Subplot Grid
Build a 2x2 subplot figure using make_subplots. Panel (1,1): a scatter of random points colored by a third variable. Panel (1,2): a bar chart of 5 category totals. Panel (2,1): a line with fill='tozeroy' for a time series. Panel (2,2): a histogram of 300 normal values. Give each panel a subtitle and set overall height=550.
Starter Code
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import pandas as pd

np.random.seed(42)

# TODO: fig = make_subplots(
#     rows=2, cols=2,
#     subplot_titles=['Random Scatter', 'Category Totals',
#                     'Time Series', 'Distribution'],
# )

# Panel (1,1): scatter with color (use marker colorscale)
x1 = np.random.randn(100)
y1 = np.random.randn(100)
c1 = np.random.randn(100)
# TODO: fig.add_trace(go.Scatter(x=x1, y=y1, mode='markers',
#     marker=dict(color=c1, colorscale='Viridis', showscale=False),
#     name='scatter'), row=1, col=1)

# Panel (1,2): bar chart
cats = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon']
vals = np.random.randint(10, 80, 5)
# TODO: fig.add_trace(go.Bar(x=cats, y=vals, name='totals',
#     marker_color='#636EFA'), row=1, col=2)

# Panel (2,1): area line
dates = pd.date_range('2024-01', periods=24, freq='MS')
ts = 100 + np.cumsum(np.random.randn(24) * 3)
# TODO: fig.add_trace(go.Scatter(x=dates, y=ts.round(1), fill='tozeroy',
#     line=dict(color='#00CC96'), name='series'), row=2, col=1)

# Panel (2,2): histogram
# TODO: fig.add_trace(go.Histogram(x=np.random.randn(300), nbinsx=20,
#     marker_color='#EF553B', name='hist'), row=2, col=2)

# TODO: fig.update_layout(title='My 2x2 Dashboard', height=550, showlegend=False)
# TODO: fig.show()
✅ Practice Checklist
7. Customizing Layout & Traces

update_layout() controls the figure-level design. update_traces() modifies all traces at once. Both accept selector filtering.

update_layout β€” theme and fonts
import plotly.express as px
import plotly.graph_objects as go

df = px.data.gapminder().query("year == 2007 and continent == 'Europe'")

fig = px.scatter(df, x='gdpPercap', y='lifeExp',
                 size='pop', color='country',
                 hover_name='country', log_x=True)

fig.update_layout(
    title=dict(text='Europe 2007: GDP vs Life Expectancy',
               font=dict(size=16, color='white'), x=0.5),
    plot_bgcolor='#1a1a2e',
    paper_bgcolor='#16213e',
    font=dict(color='#e0e0e0'),
    xaxis=dict(gridcolor='#333', title='GDP per Capita (log)'),
    yaxis=dict(gridcolor='#333', title='Life Expectancy'),
    showlegend=False,
    height=450,
)
fig.show()
update_traces and add_shape
import plotly.express as px
import plotly.graph_objects as go
import numpy as np

np.random.seed(1)
x = np.arange(1, 13)
y = 80 + np.cumsum(np.random.randn(12) * 5)

fig = px.line(x=x, y=y, markers=True,
              title='Monthly Revenue with Target Zone')

# Customise the line trace
fig.update_traces(
    line=dict(color='#00CC96', width=3),
    marker=dict(size=9, color='white',
                line=dict(color='#00CC96', width=2))
)

# Add target band (rectangle shape)
fig.add_hrect(y0=90, y1=110, fillcolor='rgba(100,200,100,0.1)',
              line_width=0, annotation_text='Target range')
fig.add_hline(y=100, line_dash='dot', line_color='gray',
              annotation_text='Target')

fig.update_layout(xaxis_title='Month', yaxis_title='Revenue ($K)')
fig.show()
Custom color palette, annotations, and template
import plotly.graph_objects as go
import plotly.express as px
import numpy as np

np.random.seed(5)
categories = ['Product A', 'Product B', 'Product C', 'Product D', 'Product E']
q1 = np.random.randint(30, 90, 5)
q2 = np.random.randint(30, 90, 5)

fig = go.Figure()
fig.add_trace(go.Bar(x=categories, y=q1, name='Q1',
                     marker_color='#636EFA'))
fig.add_trace(go.Bar(x=categories, y=q2, name='Q2',
                     marker_color='#EF553B'))

# Annotate the tallest Q2 bar
peak_idx = int(np.argmax(q2))
fig.add_annotation(
    x=categories[peak_idx], y=q2[peak_idx] + 4,
    text=f'Peak Q2: {q2[peak_idx]}',
    showarrow=True, arrowhead=2,
    font=dict(color='#EF553B', size=12),
)

# Reference line for target
fig.add_hline(y=70, line_dash='dash', line_color='gray',
              annotation_text='Target 70', annotation_position='right')

fig.update_layout(
    title='Q1 vs Q2 Sales by Product',
    barmode='group',
    template='plotly_dark',
    height=420,
    legend=dict(orientation='h', y=1.05),
    yaxis_title='Sales ($K)',
)
fig.show()
Interactive updatemenus β€” dropdown to switch chart view
import plotly.graph_objects as go
import numpy as np

np.random.seed(13)
categories = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta']
q1 = np.random.randint(20, 100, 6)
q2 = np.random.randint(20, 100, 6)
q3 = np.random.randint(20, 100, 6)
q4 = np.random.randint(20, 100, 6)

# All four quarter traces β€” only Q1 visible by default
quarters_data = [
    go.Bar(x=categories, y=q1, name='Q1', marker_color='#636EFA', visible=True),
    go.Bar(x=categories, y=q2, name='Q2', marker_color='#EF553B', visible=False),
    go.Bar(x=categories, y=q3, name='Q3', marker_color='#00CC96', visible=False),
    go.Bar(x=categories, y=q4, name='Q4', marker_color='#AB63FA', visible=False),
]
fig = go.Figure(data=quarters_data)

# Dropdown buttons β€” each shows only one quarter
buttons = []
for i, label in enumerate(['Q1', 'Q2', 'Q3', 'Q4']):
    vis = [j == i for j in range(4)]
    buttons.append(dict(
        label=label,
        method='update',
        args=[{'visible': vis},
              {'title': f'{label} Sales by Product',
               'yaxis': {'title': f'{label} Revenue ($K)'}}],
    ))

fig.update_layout(
    title='Q1 Sales by Product',
    yaxis_title='Q1 Revenue ($K)',
    height=420,
    template='plotly_dark',
    updatemenus=[dict(
        type='dropdown',
        direction='down',
        x=0.01, y=1.12, xanchor='left',
        showactive=True,
        buttons=buttons,
        bgcolor='#1e293b',
        bordercolor='#475569',
        font=dict(color='white'),
    )],
    annotations=[dict(text='Select Quarter:', x=0, y=1.16,
                      xref='paper', yref='paper',
                      showarrow=False, font=dict(color='#94a3b8'))],
)
fig.show()
print("Dropdown buttons:", len(buttons))
print("Available quarters: Q1, Q2, Q3, Q4")
💼 Real-World: Branded Executive KPI Chart
A BI developer creates a dark-themed, branded interactive chart for the C-suite weekly review email.
import plotly.graph_objects as go
import pandas as pd
import numpy as np

np.random.seed(42)
months  = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
revenue = np.array([4.2,3.8,5.1,5.8,6.2,7.0,6.5,7.8,8.1,7.5,9.2,10.4])
target  = np.full(12, 6.0)
prev_yr = revenue * np.random.uniform(0.75, 0.95, 12)

fig = go.Figure()

# Previous year (subtle background)
fig.add_trace(go.Bar(x=months, y=prev_yr, name='2023',
                     marker_color='rgba(100,100,180,0.3)', showlegend=True))

# Current year bars
fig.add_trace(go.Bar(x=months, y=revenue, name='2024',
                     marker_color=['#00CC96' if r>=t else '#EF553B'
                                   for r,t in zip(revenue, target)]))

# Target line
fig.add_trace(go.Scatter(x=months, y=target, name='Target',
                         line=dict(color='white', dash='dash', width=2),
                         mode='lines'))

# YoY growth annotations
for i, (m, r, p) in enumerate(zip(months, revenue, prev_yr)):
    yoy = (r-p)/p*100
    fig.add_annotation(x=m, y=r+0.15, text=f'+{yoy:.0f}%',
                       font=dict(size=8, color='#aaa'), showarrow=False)

fig.update_layout(
    title=dict(text='2024 Monthly Revenue ($M) vs Target & Prior Year',
               font=dict(size=15, color='white'), x=0.5),
    barmode='overlay',
    plot_bgcolor='#111827', paper_bgcolor='#0f172a',
    font=dict(color='#cbd5e1'),
    xaxis=dict(gridcolor='#1e293b'),
    yaxis=dict(gridcolor='#1e293b', title='Revenue ($M)'),
    legend=dict(orientation='h', y=1.08),
    height=460,
)
fig.show()
🏋️ Practice: Branded Dark-Theme KPI Chart
Create a go.Figure with two traces: a Bar for actual monthly revenue (12 months, random 50-120) and a Scatter line for the monthly target (fixed at 80). Apply a full dark theme using update_layout (plot_bgcolor, paper_bgcolor, font color, grid color). Add an annotation at the month with the highest revenue. Add an add_hline for the target. Set a centered title.
Starter Code
import plotly.graph_objects as go
import numpy as np

np.random.seed(7)
months  = ['Jan','Feb','Mar','Apr','May','Jun',
           'Jul','Aug','Sep','Oct','Nov','Dec']
revenue = np.random.randint(50, 121, 12)
target  = np.full(12, 80)

fig = go.Figure()

# Bar trace for actual revenue
# TODO: fig.add_trace(go.Bar(
#     x=months, y=revenue, name='Actual',
#     marker_color=['#00CC96' if r >= 80 else '#EF553B' for r in revenue],
# ))

# Line trace for target
# TODO: fig.add_trace(go.Scatter(
#     x=months, y=target, name='Target',
#     line=dict(color='white', dash='dash', width=2), mode='lines',
# ))

# Annotation at peak month
# TODO: peak = int(np.argmax(revenue))
# TODO: fig.add_annotation(
#     x=months[peak], y=revenue[peak] + 3,
#     text=f'Peak: {revenue[peak]}',
#     showarrow=True, arrowhead=2,
#     font=dict(color='#00CC96', size=12),
# )

# Dark theme layout
# TODO: fig.update_layout(
#     title=dict(text='Monthly Revenue vs Target', x=0.5,
#                font=dict(size=15, color='white')),
#     plot_bgcolor='#111827', paper_bgcolor='#0f172a',
#     font=dict(color='#cbd5e1'),
#     xaxis=dict(gridcolor='#1e293b'),
#     yaxis=dict(gridcolor='#1e293b', title='Revenue ($K)'),
#     barmode='overlay', height=430,
# )
# TODO: fig.show()
✅ Practice Checklist
8. Animated Charts

animation_frame adds a play button to animate over a variable (e.g. year, month). Great for showing trends over time.

Animated bubble chart
import plotly.express as px

# Classic animated gapminder chart
df = px.data.gapminder()

fig = px.scatter(
    df, x='gdpPercap', y='lifeExp',
    animation_frame='year',
    animation_group='country',
    size='pop', color='continent',
    hover_name='country',
    log_x=True, size_max=55,
    range_x=[100, 100000], range_y=[25, 90],
    title='World Development 1952–2007',
    labels={'gdpPercap': 'GDP per Capita', 'lifeExp': 'Life Expectancy'},
)
fig.update_layout(height=520)
fig.show()
Animated bar chart race
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
products = ['Widget', 'Gadget', 'Doohickey', 'Gizmo', 'Thingamajig']
quarters = ['Q1','Q2','Q3','Q4']

rows = []
sales = {p: np.random.uniform(50, 200) for p in products}
for q in quarters:
    for p in products:
        sales[p] += np.random.uniform(-20, 40)
        rows.append({'quarter': q, 'product': p, 'sales': max(sales[p], 10)})
df = pd.DataFrame(rows)

fig = px.bar(df, x='sales', y='product',
             animation_frame='quarter',
             orientation='h',
             color='product',
             range_x=[0, 400],
             title='Product Sales Race by Quarter',
             labels={'sales': 'Cumulative Sales ($K)'},
             color_discrete_sequence=px.colors.qualitative.Bold)
fig.update_layout(showlegend=False, height=380)
fig.show()
Animated scatter with custom transition speed
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(3)
years = list(range(2015, 2026))
regions = ['North', 'South', 'East', 'West']

rows = []
for region in regions:
    revenue = np.random.uniform(80, 120)
    customers = np.random.randint(500, 2000)
    for yr in years:
        revenue    += np.random.uniform(-5, 12)
        customers  += np.random.randint(-50, 150)
        rows.append({
            'year':      yr,
            'region':    region,
            'revenue':   round(revenue, 1),
            'customers': max(customers, 100),
            'margin':    round(np.random.uniform(0.1, 0.4), 2),
        })
df = pd.DataFrame(rows)

fig = px.scatter(
    df, x='customers', y='revenue',
    animation_frame='year', animation_group='region',
    color='region', size='margin', size_max=40,
    hover_name='region', hover_data=['margin'],
    range_x=[0, 4000], range_y=[50, 250],
    title='Revenue vs Customers by Region (Animated 2015-2025)',
    labels={'revenue': 'Revenue ($K)', 'customers': 'Active Customers'},
)
fig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 800
fig.layout.updatemenus[0].buttons[0].args[1]['transition']['duration'] = 400
fig.update_layout(height=480)
fig.show()
Animated line chart with frames and sliders
import plotly.graph_objects as go
import numpy as np

np.random.seed(22)
n_frames = 20
x = np.linspace(0, 4 * np.pi, 200)

# Each frame reveals more of the wave, with noise that changes each step
frames = []
for k in range(1, n_frames + 1):
    end = int(len(x) * k / n_frames)
    noise = np.random.randn(end) * 0.15
    y = np.sin(x[:end]) * np.exp(-x[:end] * 0.08) + noise
    frames.append(go.Frame(
        data=[go.Scatter(x=x[:end], y=y,
                         mode='lines',
                         line=dict(color='#636EFA', width=2))],
        name=str(k),
        layout=go.Layout(title_text=f'Damped Wave β€” Step {k}/{n_frames}'),
    ))

# Initial trace (first frame)
y0 = np.sin(x[:1]) * np.exp(-x[:1] * 0.08)
fig = go.Figure(
    data=[go.Scatter(x=x[:1], y=y0, mode='lines',
                     line=dict(color='#636EFA', width=2))],
    frames=frames,
)

# Play/Pause buttons + slider
fig.update_layout(
    title='Animated Damped Wave β€” Frames & Slider',
    xaxis=dict(range=[0, 4*np.pi], title='x'),
    yaxis=dict(range=[-1.3, 1.3], title='Amplitude'),
    height=460,
    updatemenus=[dict(
        type='buttons', showactive=False,
        y=1.05, x=0, xanchor='left',
        buttons=[
            dict(label='Play',
                 method='animate',
                 args=[None, {'frame': {'duration': 120, 'redraw': True},
                              'fromcurrent': True}]),
            dict(label='Pause',
                 method='animate',
                 args=[[None], {'frame': {'duration': 0},
                                'mode': 'immediate'}]),
        ],
    )],
    sliders=[dict(
        steps=[dict(method='animate', args=[[str(k)],
               {'mode': 'immediate', 'frame': {'duration': 120}}],
               label=str(k)) for k in range(1, n_frames+1)],
        transition=dict(duration=80),
        x=0, y=0, len=1.0,
        currentvalue=dict(prefix='Step: ', visible=True),
    )],
)
fig.show()
print(f"Total frames: {len(fig.frames)}")
💼 Real-World: Global CO2 Emissions Animation
An environmental analyst animates per-capita CO2 emissions vs GDP across countries and decades for a policy presentation.
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(10)
countries  = ['US','China','India','Germany','Brazil','UK','Japan','France','Canada','Australia']
continents = ['Americas','Asia','Asia','Europe','Americas','Europe','Asia','Europe','Americas','Oceania']
years      = list(range(1990, 2025, 5))

rows = []
for i, (c, cont) in enumerate(zip(countries, continents)):
    gdp  = np.random.uniform(5000, 60000)
    co2  = np.random.uniform(2, 16)
    pop  = np.random.uniform(20, 1400)
    for yr in years:
        gdp  *= np.random.uniform(1.01, 1.05)
        co2  *= np.random.uniform(0.97, 1.02)
        rows.append({'country':c,'continent':cont,'year':yr,
                     'gdp_pc':round(gdp,0),'co2_pc':round(co2,2),'pop':round(pop,1)})

df = pd.DataFrame(rows)

fig = px.scatter(
    df, x='gdp_pc', y='co2_pc',
    animation_frame='year', animation_group='country',
    size='pop', color='continent',
    hover_name='country',
    log_x=True, size_max=45,
    range_x=[3000, 120000], range_y=[0, 20],
    title='CO2 Emissions vs GDP per Capita (1990–2024)',
    labels={'gdp_pc': 'GDP per Capita ($, log)', 'co2_pc': 'CO2 per Capita (tonnes)'},
)
fig.update_layout(height=520)
fig.show()
🏋️ Practice: Animated Geographic Scatter
Using px.data.gapminder(), create an animated choropleth map (px.choropleth) with animation_frame='year', coloring countries by lifeExp. Set range_color=[30, 85], use the RdYlGn colorscale, and add hover_data for gdpPercap and pop. Print the number of unique years in the animation.
Starter Code
import plotly.express as px

df = px.data.gapminder()

# Print unique years that will be animation frames
# TODO: years = sorted(df['year'].unique())
# TODO: print(f"Animation frames: {len(years)} years from {years[0]} to {years[-1]}")

# Build animated choropleth
# TODO: fig = px.choropleth(
#     df,
#     locations='iso_alpha',
#     color='lifeExp',
#     hover_name='country',
#     hover_data=['gdpPercap', 'pop'],
#     animation_frame='year',
#     color_continuous_scale='RdYlGn',
#     range_color=[30, 85],
#     title='World Life Expectancy 1952-2007 (Animated)',
#     labels={'lifeExp': 'Life Expectancy'},
# )
# TODO: fig.update_layout(height=500,
#     coloraxis_colorbar=dict(title='Life Exp (years)'))
# TODO: fig.show()
✅ Practice Checklist
9. Maps & Geographic Charts

px.choropleth and px.scatter_geo create world or country-level maps. px.scatter_mapbox uses tile maps for city-level data.

Choropleth world map
import plotly.express as px

df = px.data.gapminder().query("year == 2007")

fig = px.choropleth(
    df, locations='iso_alpha',
    color='lifeExp',
    hover_name='country',
    hover_data=['gdpPercap', 'pop'],
    color_continuous_scale='RdYlGn',
    range_color=[40, 85],
    title='Life Expectancy by Country (2007)',
    labels={'lifeExp': 'Life Expectancy'}
)
fig.update_layout(height=480, coloraxis_colorbar=dict(title='Years'))
fig.show()
Scatter map β€” US cities
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(7)
cities = pd.DataFrame({
    'city':    ['New York','Los Angeles','Chicago','Houston','Phoenix',
                'Philadelphia','San Antonio','San Diego','Dallas','San Jose'],
    'lat':     [40.71,34.05,41.88,29.76,33.45,39.95,29.42,32.72,32.78,37.34],
    'lon':     [-74.01,-118.24,-87.63,-95.37,-112.07,-75.16,-98.49,-117.16,-96.80,-121.89],
    'revenue': np.random.uniform(50, 500, 10).round(1),
    'customers':np.random.randint(1000, 50000, 10),
})

fig = px.scatter_geo(cities, lat='lat', lon='lon',
                     size='revenue', color='revenue',
                     hover_name='city',
                     hover_data=['customers'],
                     color_continuous_scale='Reds',
                     scope='usa',
                     title='US Market Revenue by City')
fig.update_layout(height=430)
fig.show()
Bubble map on a natural earth projection
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(21)
# Major world cities with lat/lon
cities = pd.DataFrame({
    'city':    ['New York','London','Tokyo','Sydney','Sao Paulo',
                'Mumbai','Cairo','Lagos','Moscow','Beijing'],
    'country': ['USA','UK','Japan','Australia','Brazil',
                'India','Egypt','Nigeria','Russia','China'],
    'lat':     [40.71, 51.51, 35.68, -33.87, -23.55,
                19.08, 30.04,  6.52,  55.75,  39.91],
    'lon':     [-74.01, -0.13, 139.69, 151.21, -46.63,
                 72.88,  31.24,  3.40,  37.62, 116.41],
    'gdp_bn':  np.random.uniform(100, 800, 10).round(1),
    'pop_m':   np.random.uniform(5, 35, 10).round(1),
})

fig = px.scatter_geo(
    cities, lat='lat', lon='lon',
    size='gdp_bn', color='gdp_bn',
    hover_name='city', hover_data=['country', 'pop_m'],
    color_continuous_scale='Plasma',
    projection='natural earth',
    title='World City GDP β€” Bubble Map',
    labels={'gdp_bn': 'GDP ($B)'},
    size_max=40,
)
fig.update_layout(height=450)
fig.show()
px.scatter_mapbox β€” open-street tile map
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(33)
# Sample delivery locations across a city (centred on Chicago)
n = 60
lat_center, lon_center = 41.88, -87.63
deliveries = pd.DataFrame({
    'id':       [f'DEL-{i:03d}' for i in range(n)],
    'lat':      lat_center + np.random.randn(n) * 0.08,
    'lon':      lon_center + np.random.randn(n) * 0.12,
    'status':   np.random.choice(['Delivered', 'In Transit', 'Failed'], n,
                                  p=[0.65, 0.25, 0.10]),
    'packages': np.random.randint(1, 20, n),
    'duration': np.random.uniform(5, 60, n).round(1),
})

color_map = {'Delivered': '#00CC96', 'In Transit': '#636EFA', 'Failed': '#EF553B'}

fig = px.scatter_mapbox(
    deliveries,
    lat='lat', lon='lon',
    color='status',
    size='packages',
    size_max=20,
    hover_name='id',
    hover_data={'packages': True, 'duration': ':.1f', 'lat': False, 'lon': False},
    color_discrete_map=color_map,
    zoom=11,
    center={'lat': lat_center, 'lon': lon_center},
    title='Delivery Status Map β€” Chicago (Open-Street Tiles)',
    mapbox_style='open-street-map',
)
fig.update_layout(height=520, legend_title_text='Status')
fig.show()
status_counts = deliveries['status'].value_counts()
print("Delivery summary:")
for status, count in status_counts.items():
    print(f"  {status}: {count}")
💼 Real-World: Global Sales Heatmap by Country
A VP of Sales uses a choropleth to present YTD revenue performance by country and flag underperforming markets.
import plotly.express as px
import pandas as pd
import numpy as np

# ISO codes and country names
data = {
    'iso_alpha': ['USA','GBR','DEU','FRA','JPN','BRA','IND','AUS','CAN','MEX',
                  'CHN','KOR','SGP','ZAF','NLD','SWE','NOR','CHE','ITA','ESP'],
    'country':   ['US','UK','Germany','France','Japan','Brazil','India','Australia',
                  'Canada','Mexico','China','S.Korea','Singapore','S.Africa',
                  'Netherlands','Sweden','Norway','Switzerland','Italy','Spain'],
    'target':    [1000,300,280,220,350,150,200,180,260,120,
                  500,180,90,80,130,110,100,120,160,140],
}
np.random.seed(42)
df = pd.DataFrame(data)
df['actual']  = (df['target'] * np.random.uniform(0.6, 1.3, len(df))).round(0)
df['vs_target'] = ((df['actual'] - df['target']) / df['target'] * 100).round(1)

fig = px.choropleth(
    df, locations='iso_alpha',
    color='vs_target',
    hover_name='country',
    hover_data={'actual': True, 'target': True, 'vs_target': True},
    color_continuous_scale='RdYlGn',
    range_color=[-40, 40],
    title='YTD Revenue vs Target by Country (%)',
    labels={'vs_target': 'vs Target (%)'}
)
fig.update_layout(
    height=480,
    coloraxis_colorbar=dict(title='vs Target %', ticksuffix='%')
)
fig.show()
🏋️ Practice: Geographic Scatter Plot
Build a px.scatter_geo bubble map of at least 8 world cities. Each bubble should represent a metric of your choice (e.g., revenue, population). Color by a second numeric column, add hover_name and hover_data, and use projection='natural earth'. Print the city with the highest metric value.
Starter Code
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(55)

cities = pd.DataFrame({
    'city':    ['New York', 'London', 'Tokyo', 'Sydney',
                'Dubai', 'Singapore', 'Toronto', 'Paris'],
    'lat':     [40.71, 51.51, 35.68, -33.87, 25.20,  1.35, 43.65, 48.85],
    'lon':     [-74.01, -0.13, 139.69, 151.21, 55.27, 103.82, -79.38,  2.35],
    'revenue': np.random.uniform(100, 900, 8).round(1),
    'customers': np.random.randint(5000, 50000, 8),
})

# Print highest revenue city
# TODO: top = cities.loc[cities['revenue'].idxmax(), 'city']
# TODO: print(f"Highest revenue city: {top} (${cities['revenue'].max():.1f}M)")

# TODO: fig = px.scatter_geo(
#     cities, lat='lat', lon='lon',
#     size='revenue', color='customers',
#     hover_name='city',
#     hover_data=['revenue', 'customers'],
#     color_continuous_scale='Viridis',
#     projection='natural earth',
#     title='Global City Revenue & Customer Base',
#     size_max=40,
# )
# TODO: fig.update_layout(height=460)
# TODO: fig.show()
✅ Practice Checklist
10. Exporting & Sharing

Save charts as interactive HTML, static PNG/PDF/SVG (requires kaleido), or embed in web apps via fig.to_json().

Export to HTML and JSON
import plotly.express as px
import os

df  = px.data.iris()
fig = px.scatter(df, x='sepal_length', y='sepal_width',
                 color='species', title='Iris β€” Export Demo')

# ── Standalone HTML (fully self-contained, shareable) ──
fig.write_html('iris_chart.html', include_plotlyjs='cdn')
print(f"HTML: {os.path.getsize('iris_chart.html') / 1024:.1f} KB")

# ── JSON (for embedding in web apps) ──
json_str = fig.to_json()
print(f"JSON length: {len(json_str):,} chars")

# ── Show in browser / Jupyter ──
fig.show()

# Cleanup
os.remove('iris_chart.html')
Export as PNG/PDF with kaleido
import plotly.express as px
import os

# Note: requires  pip install kaleido
df  = px.data.tips()
fig = px.box(df, x='day', y='total_bill', color='time',
             title='Total Bill by Day')

try:
    fig.write_image('chart.png', width=800, height=500, scale=2)
    fig.write_image('chart.pdf')
    fig.write_image('chart.svg')
    print("Saved PNG, PDF, SVG")
    for f in ['chart.png','chart.pdf','chart.svg']:
        if os.path.exists(f): os.remove(f)
except Exception as e:
    print(f"kaleido not installed: {e}")
    print("Install with:  pip install kaleido")

fig.show()
Embed multiple charts in a single HTML report
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import os

np.random.seed(42)

# Build two figures
df_iris = px.data.iris()
fig1 = px.scatter(df_iris, x='sepal_length', y='petal_length',
                  color='species', title='Iris Scatter')

months = list(range(1, 13))
revenue = 100 + np.cumsum(np.random.randn(12) * 5)
fig2 = px.line(x=months, y=revenue, markers=True,
               title='Monthly Revenue',
               labels={'x': 'Month', 'y': 'Revenue ($K)'})

# Combine into one HTML file manually
html_parts = [
    '<html><head><meta charset="UTF-8"><title>Combined Report</title></head><body>',
    '<h1 style="font-family:sans-serif;padding:20px">Multi-Chart Report</h1>',
    fig1.to_html(full_html=False, include_plotlyjs='cdn'),
    fig2.to_html(full_html=False, include_plotlyjs=False),
    '</body></html>',
]
combined_html = '\n'.join(html_parts)

out = 'combined_report.html'
with open(out, 'w', encoding='utf-8') as f:
    f.write(combined_html)

print(f"Combined report: {out} ({os.path.getsize(out)/1024:.1f} KB)")
print("Contains 2 fully interactive charts in one file.")
os.remove(out)
Export to PDF and static image with kaleido
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
import os

rng = np.random.default_rng(42)
df  = pd.DataFrame({
    'month':   pd.date_range('2024-01-01', periods=12, freq='ME').strftime('%b'),
    'revenue': np.cumsum(rng.uniform(50, 200, 12)) + 500,
    'target':  np.linspace(600, 1800, 12),
})

fig = go.Figure()
fig.add_trace(go.Bar(x=df['month'], y=df['revenue'], name='Revenue',
                     marker_color='steelblue', opacity=0.85))
fig.add_trace(go.Scatter(x=df['month'], y=df['target'], name='Target',
                         line=dict(color='tomato', width=2, dash='dash'),
                         mode='lines+markers', marker_size=7))
fig.update_layout(
    title='Monthly Revenue vs Target', title_font_size=16,
    xaxis_title='Month', yaxis_title='Revenue ($K)',
    legend=dict(x=0.01, y=0.99), height=450,
    template='plotly_white',
)

# Export as PNG (requires kaleido: pip install kaleido)
try:
    fig.write_image('revenue_chart.png', width=900, height=450, scale=2)
    size = os.path.getsize('revenue_chart.png')
    print(f'PNG exported: revenue_chart.png ({size/1024:.1f} KB)')
    os.remove('revenue_chart.png')
except Exception as e:
    print(f'PNG export requires kaleido: pip install kaleido ({e})')

# Export as interactive HTML
html_str = fig.to_html(full_html=True, include_plotlyjs='cdn')
with open('revenue_chart.html', 'w') as f:
    f.write(html_str)
size = os.path.getsize('revenue_chart.html')
print(f'HTML exported: revenue_chart.html ({size/1024:.1f} KB)')
os.remove('revenue_chart.html')

# Export figure dict (JSON-serializable)
fig_dict = fig.to_dict()
print(f'Figure dict keys: {list(fig_dict.keys())}')
print(f'Number of traces: {len(fig_dict["data"])}')
💼 Real-World: Automated Interactive Report Generator
A data engineer generates a self-contained HTML report with multiple charts embedded as a single shareable file.
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px
import pandas as pd
import numpy as np
import os

np.random.seed(42)
months  = pd.date_range('2024-01', periods=12, freq='MS')
revenue = 100 + np.cumsum(np.random.randn(12) * 8) + np.arange(12) * 4
cac     = np.random.uniform(30, 80, 12)
churn   = np.random.uniform(0.02, 0.08, 12)

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=['Monthly Revenue ($K)', 'Customer Acquisition Cost ($)',
                    'Churn Rate (%)', 'Revenue vs CAC'],
    specs=[[{},{}],[{},{}]]
)

fig.add_trace(go.Scatter(x=months, y=revenue.round(1),
                         fill='tozeroy', line=dict(color='#636EFA')), row=1, col=1)
fig.add_trace(go.Bar(x=months, y=cac.round(1),
                     marker_color='#EF553B'), row=1, col=2)
fig.add_trace(go.Scatter(x=months, y=(churn*100).round(2),
                         mode='lines+markers', line=dict(color='#AB63FA')), row=2, col=1)
fig.add_trace(go.Scatter(x=cac, y=revenue,
                         mode='markers', text=[m.strftime('%b') for m in months],
                         textposition='top center',
                         marker=dict(color='#00CC96', size=9)), row=2, col=2)

fig.update_layout(title='SaaS KPI Report β€” 2024', height=600,
                  showlegend=False, template='plotly_dark')

# Export as standalone HTML
out = 'saas_report.html'
fig.write_html(out, include_plotlyjs='cdn', full_html=True)
size_kb = os.path.getsize(out) / 1024
print(f"Report saved: {out} ({size_kb:.0f} KB)")
print("Open in any browser β€” fully interactive, no server needed.")
fig.show()
os.remove(out)
🏋️ Practice: Multi-Chart HTML Report
Build three separate figures (a scatter, a bar chart, and a line chart) using any px datasets. Export each with fig.to_html(full_html=False, include_plotlyjs='cdn' for the first, include_plotlyjs=False for the rest). Stitch them together with a custom HTML wrapper and write to 'my_report.html'. Print the file size and then remove it.
Starter Code
import plotly.express as px
import os

# Figure 1: scatter
df_gap = px.data.gapminder().query("year == 2007")
# TODO: fig1 = px.scatter(df_gap, x='gdpPercap', y='lifeExp',
#     size='pop', color='continent', log_x=True,
#     title='GDP vs Life Expectancy 2007')

# Figure 2: bar β€” top 10 countries by population
# TODO: top10 = df_gap.nlargest(10, 'pop')
# TODO: fig2 = px.bar(top10, x='country', y='pop',
#     color='continent', title='Top 10 Countries by Population')

# Figure 3: line β€” Europe average life exp over time
# TODO: eur = px.data.gapminder().query("continent == 'Europe'")
# TODO: avg = eur.groupby('year')['lifeExp'].mean().reset_index()
# TODO: fig3 = px.line(avg, x='year', y='lifeExp', markers=True,
#     title='Europe Average Life Expectancy Over Time')

# Combine and save
# TODO: html_body = '\n'.join([
#     '<html><head><meta charset="UTF-8"><title>My Report</title></head><body>',
#     '<h1 style="font-family:sans-serif;padding:16px">My Plotly Report</h1>',
#     fig1.to_html(full_html=False, include_plotlyjs='cdn'),
#     fig2.to_html(full_html=False, include_plotlyjs=False),
#     fig3.to_html(full_html=False, include_plotlyjs=False),
#     '</body></html>',
# ])
# TODO: out = 'my_report.html'
# TODO: with open(out, 'w', encoding='utf-8') as f:
#     f.write(html_body)
# TODO: print(f"Report saved: {out} ({os.path.getsize(out)/1024:.1f} KB)")
# TODO: os.remove(out)
✅ Practice Checklist
11. 3D Charts

Create interactive 3D visualizations β€” scatter plots, surface plots, and line trajectories β€” that users can rotate and zoom in the browser.

3D scatter plot with color mapping
import plotly.graph_objects as go
import numpy as np

np.random.seed(42)
x, y, z = np.random.randn(3, 200)
fig = go.Figure(data=[go.Scatter3d(
    x=x, y=y, z=z, mode='markers',
    marker=dict(size=5, color=z, colorscale='Viridis', showscale=True,
                colorbar=dict(title='Z value'))
)])
fig.update_layout(title='3D Scatter Plot',
    scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Z'))
fig.write_html('scatter3d.html')
print('Saved scatter3d.html')
Interactive 3D surface plot
import plotly.graph_objects as go
import numpy as np

x = np.linspace(-3, 3, 50)
y = np.linspace(-3, 3, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
fig = go.Figure(data=[go.Surface(z=Z, x=X, y=Y, colorscale='RdBu')])
fig.update_layout(
    title='3D Surface: sin(sqrt(x^2+y^2))',
    scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='sin(r)'),
    width=700, height=600
)
fig.write_html('surface3d.html')
print('Saved surface3d.html')
3D line trajectory colored by progress
import plotly.graph_objects as go
import numpy as np

t = np.linspace(0, 10*np.pi, 500)
x = np.sin(t)
y = np.cos(t)
z = t / (10*np.pi)
fig = go.Figure(data=[go.Scatter3d(
    x=x, y=y, z=z, mode='lines',
    line=dict(color=t, colorscale='Plasma', width=4)
)])
fig.update_layout(
    title='3D Spiral Trajectory',
    scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Height')
)
fig.write_html('spiral3d.html')
print('Saved spiral3d.html')
Side-by-side 3D subplots
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

np.random.seed(42)
fig = make_subplots(rows=1, cols=2,
                    specs=[[{'type':'scatter3d'},{'type':'surface'}]],
                    subplot_titles=['Scatter3D', 'Surface'])

x, y, z = np.random.randn(3, 100)
fig.add_trace(go.Scatter3d(x=x, y=y, z=z, mode='markers',
              marker=dict(size=4, color=z, colorscale='Viridis')), row=1, col=1)

t = np.linspace(-2, 2, 30)
X, Y = np.meshgrid(t, t)
Z = np.sin(X) * np.cos(Y)
fig.add_trace(go.Surface(x=X, y=Y, z=Z, colorscale='Plasma', showscale=False), row=1, col=2)
fig.update_layout(title='3D Subplots', height=500)
fig.write_html('3d_subplots.html')
print('Saved 3d_subplots.html')
🏋️ Practice: 3D Sales Globe
Create a 3D scatter plot with x=region_id (1-5), y=quarter (1-4), z=revenue. Color points by product category and size them by profit margin. Save to globe_sales.html.
Starter Code
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(42)
n = 100
df = pd.DataFrame({
    'region':   np.random.randint(1, 6, n).astype(float),
    'quarter':  np.random.randint(1, 5, n).astype(float),
    'revenue':  np.random.exponential(50000, n),
    'margin':   np.random.uniform(0.05, 0.40, n),
    'category': np.random.choice(['Electronics','Clothing','Food'], n),
})
# TODO: 3D scatter with color by category, size by margin
# TODO: save to 'globe_sales.html'
✅ Practice Checklist
12. Geographic Maps

Visualize spatial data with choropleth maps, scatter geo, and mapbox β€” pinpoint patterns across countries, US states, and cities.

US choropleth map by state
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
states = ['CA','TX','NY','FL','IL','PA','OH','GA','NC','MI',
          'WA','AZ','MA','TN','IN','MO','MD','CO','WI','MN']
df = pd.DataFrame({'state': states, 'value': np.random.randint(100, 10000, len(states))})
fig = px.choropleth(df, locations='state', color='value',
                    locationmode='USA-states', scope='usa',
                    color_continuous_scale='Blues',
                    title='Simulated Metric by US State')
fig.write_html('us_choropleth.html')
print('Saved us_choropleth.html')
World choropleth with country data
import plotly.express as px
import pandas as pd
import numpy as np

countries = ['USA','CHN','IND','BRA','RUS','AUS','CAN','DEU','GBR','FRA',
             'JPN','KOR','MEX','IDN','NGA','EGY','ZAF','ARG','SAU','TUR']
np.random.seed(42)
df = pd.DataFrame({'country': countries, 'gdp_per_capita': np.random.uniform(5000, 65000, len(countries))})
fig = px.choropleth(df, locations='country', color='gdp_per_capita',
                    color_continuous_scale='Plasma',
                    title='Simulated GDP Per Capita by Country')
fig.write_html('world_choropleth.html')
print('Saved world_choropleth.html')
Scatter geo β€” city bubble map
import plotly.express as px
import pandas as pd

cities = pd.DataFrame({
    'city': ['New York','Los Angeles','Chicago','Houston','Phoenix',
             'Philadelphia','San Antonio','San Diego','Dallas','San Jose'],
    'lat':  [40.71, 34.05, 41.85, 29.76, 33.45, 39.95, 29.42, 32.72, 32.78, 37.34],
    'lon':  [-74.01,-118.24,-87.65,-95.37,-112.07,-75.17,-98.49,-117.16,-96.80,-121.89],
    'pop':  [8336817,3979576,2693976,2320268,1608139,1603797,1434625,1386932,1304379,1035317],
    'region': ['NE','West','MW','South','West','NE','South','West','South','West'],
})
fig = px.scatter_geo(cities, lat='lat', lon='lon', size='pop', color='region',
                     hover_name='city', scope='usa', size_max=40,
                     title='Top 10 US Cities by Population')
fig.write_html('city_bubble.html')
print('Saved city_bubble.html')
Mapbox scatter map with open tiles
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({
    'lat':   np.random.uniform(25, 49, 50),
    'lon':   np.random.uniform(-125, -67, 50),
    'value': np.random.uniform(10, 100, 50),
    'label': [f'Site {i}' for i in range(50)],
})
fig = px.scatter_mapbox(df, lat='lat', lon='lon', size='value',
                        color='value', color_continuous_scale='Reds',
                        hover_name='label', zoom=3,
                        mapbox_style='open-street-map',
                        title='Random Sites across the US')
fig.write_html('mapbox_scatter.html')
print('Saved mapbox_scatter.html')
🏋️ Practice: Global Temperature Map
Create a world choropleth showing average temperature by country (simulated). Use a RdBu_r diverging colorscale centered at 15 degrees. Save to world_temp.html.
Starter Code
import plotly.express as px
import pandas as pd
import numpy as np

countries = ['USA','CHN','IND','BRA','RUS','AUS','CAN','DEU','GBR','FRA',
             'JPN','KOR','MEX','IDN','NGA','EGY','ZAF','ARG','SAU','TUR']
np.random.seed(42)
df = pd.DataFrame({'country': countries, 'avg_temp': np.random.uniform(5, 30, len(countries))})
# TODO: choropleth with locations='country', color='avg_temp'
# TODO: colorscale='RdBu_r', color midpoint=15
# TODO: save to 'world_temp.html'
✅ Practice Checklist
13. Animations

Bring data to life with Plotly animations β€” animated scatter plots, bar chart races, and frame-by-frame time-lapse visualizations.

Animated scatter with animation_frame
import plotly.express as px

try:
    gapminder = px.data.gapminder()
    fig = px.scatter(gapminder, x='gdpPercap', y='lifeExp',
                     animation_frame='year', animation_group='country',
                     size='pop', color='continent', hover_name='country',
                     log_x=True, size_max=55,
                     range_x=[100, 100000], range_y=[25, 90],
                     title='Gapminder: GDP vs Life Expectancy over Time')
    fig.write_html('gapminder_animation.html')
    print('Saved gapminder_animation.html')
except Exception as e:
    print(f'Note: {e}')
Animated bar chart race
import plotly.graph_objects as go
import numpy as np

np.random.seed(42)
categories = ['A','B','C','D','E']
years = list(range(2018, 2024))
data = {cat: np.cumsum(np.random.randint(5,20,len(years))) for cat in categories}

frames = []
for i, year in enumerate(years):
    vals = [data[c][i] for c in categories]
    order = sorted(range(len(vals)), key=lambda x: vals[x])
    frames.append(go.Frame(
        data=[go.Bar(x=[vals[j] for j in order], y=[categories[j] for j in order],
                     orientation='h', marker_color='steelblue')],
        name=str(year)
    ))

fig = go.Figure(frames=frames, data=frames[0].data)
fig.update_layout(
    title='Bar Chart Race', xaxis_title='Cumulative Value',
    updatemenus=[dict(type='buttons', showactive=False,
                      buttons=[dict(label='Play', method='animate',
                                   args=[None, dict(frame=dict(duration=800))])])]
)
fig.write_html('bar_race.html')
print('Saved bar_race.html')
Animated line chart over time
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
months = pd.date_range('2022-01', periods=24, freq='ME')
categories = ['Electronics','Clothing','Food']
rows = []
for cat in categories:
    base = {'Electronics':5000,'Clothing':3000,'Food':7000}[cat]
    for m in months:
        rows.append({'month': m.strftime('%Y-%m'), 'category': cat,
                     'revenue': base + np.random.randint(-500, 1500)})
df = pd.DataFrame(rows)
df = df.sort_values('month')

fig = px.line(df, x='month', y='revenue', color='category',
              animation_frame='month',
              range_y=[df.revenue.min()-200, df.revenue.max()+200],
              title='Monthly Revenue Animation')
fig.write_html('animated_line.html')
print('Saved animated_line.html')
Slider-controlled visualization
import plotly.graph_objects as go
import numpy as np

x = np.linspace(0, 2*np.pi, 200)
steps = []
figs_data = []
for freq in np.linspace(1, 5, 20):
    figs_data.append(go.Scatter(x=x, y=np.sin(freq*x), mode='lines',
                                name=f'f={freq:.1f}'))

steps = [dict(method='update', args=[{'y': [np.sin(f*x)], 'name': [f'f={f:.1f}']}],
              label=f'{f:.1f}') for f in np.linspace(1, 5, 20)]

fig = go.Figure(data=[figs_data[0]])
fig.update_layout(
    title='Interactive Slider: sin(f*x)',
    sliders=[dict(active=0, steps=steps, currentvalue=dict(prefix='Frequency: '))],
    xaxis_title='x', yaxis_title='sin(f*x)', yaxis_range=[-1.2, 1.2]
)
fig.write_html('slider_sine.html')
print('Saved slider_sine.html')
🏋️ Practice: Population Growth Animation
Create an animated bar chart showing population by continent (in billions) from 2000 to 2023. Each frame = one year. Add a play button. Save to population_animation.html.
Starter Code
import plotly.graph_objects as go
import numpy as np

continents = ['Africa','Asia','Europe','Americas','Oceania']
years = list(range(2000, 2024))
np.random.seed(42)
base = [0.8, 3.7, 0.7, 0.9, 0.03]
growth = [0.03, 0.01, 0.002, 0.01, 0.015]

# TODO: build frames list (one per year)
# TODO: each frame: go.Frame with go.Bar showing populations
# TODO: fig with play button in updatemenus
# TODO: save to 'population_animation.html'
✅ Practice Checklist
14. Plotly Dash Fundamentals

Dash app structure (code pattern)
# Dash apps run as web servers - this shows the code pattern
print('Dash app structure:')
print('''
from dash import Dash, dcc, html
from dash.dependencies import Input, Output
import plotly.express as px

app = Dash(__name__)
app.layout = html.Div([
    html.H1("Dashboard"),
    dcc.Dropdown(id="metric", options=["Revenue","Users"], value="Revenue"),
    dcc.Graph(id="chart"),
])

@app.callback(Output("chart","figure"), Input("metric","value"))
def update(metric):
    import numpy as np, pandas as pd
    df = pd.DataFrame({"date": pd.date_range("2024-01-01", periods=30),
                        metric: 100 + np.cumsum(np.random.randn(30))})
    return px.line(df, x="date", y=metric, title=f"{metric} over Time")

if __name__ == "__main__":
    app.run_server(debug=True)
''')
print("Run with: python app.py")
Plotly figure for Dash callback
import plotly.graph_objects as go
import plotly.express as px
import numpy as np
import pandas as pd

def make_dashboard_fig(category='All'):
    np.random.seed(42)
    dates = pd.date_range('2024-01-01', periods=90, freq='D')
    cats = ['Electronics','Apparel','Food']
    dfs = [pd.DataFrame({'date':dates,'revenue':np.random.exponential(1000,90),'cat':c}) for c in cats]
    df = pd.concat(dfs)
    if category != 'All':
        df = df[df.cat == category]
    fig = px.area(df.groupby(['date','cat'])['revenue'].sum().reset_index(),
                  x='date', y='revenue', color='cat',
                  title=f'Revenue: {category}', template='plotly_white')
    fig.update_layout(hovermode='x unified', height=400)
    return fig

fig = make_dashboard_fig('Electronics')
fig.write_html('dash_callback_fig.html')
print(f'Dashboard figure saved - traces: {len(fig.data)}')
DataTable interactive grid pattern
# Dash DataTable pattern
print('DataTable pattern:')
print('''
from dash import dash_table
import pandas as pd

df = pd.read_csv("data.csv")
table = dash_table.DataTable(
    data=df.to_dict("records"),
    columns=[{"name": c, "id": c} for c in df.columns],
    filter_action="native",
    sort_action="native",
    page_size=10,
    export_format="csv",
    style_data_conditional=[{
        "if": {"filter_query": "{revenue} > 1000"},
        "backgroundColor": "#d4edda",
    }],
)
''')
print('Key features: filter_action, sort_action, export_format, conditional styling')
Multi-page Dash routing
# Multi-page Dash 2.x pattern
print('Multi-page Dash pattern:')
print('''
# pages/home.py
import dash
dash.register_page(__name__, path="/")
layout = html.Div([html.H2("Home")])

# pages/analytics.py
dash.register_page(__name__, path="/analytics")
layout = html.Div([dcc.Graph(figure=create_fig())])

# app.py
app = Dash(__name__, use_pages=True)
app.layout = html.Div([
    html.Nav([
        dcc.Link("Home", href="/"),
        dcc.Link("Analytics", href="/analytics"),
    ]),
    dash.page_container
])
''')
print('Each page: dash.register_page(__name__, path="/route")')
🏋️ Practice: Filter Callback
Write a function make_fig(year, continent) that filters gapminder data and returns a scatter of GDP vs life expectancy. Call it for (1952,'Asia'), (2007,'Europe'), (2007,'All'). Save each as HTML.
Starter Code
import plotly.express as px

gap = px.data.gapminder()

def make_fig(year, continent):
    # TODO: filter by year and continent ('All' = no continent filter)
    # TODO: return px.scatter size='pop', color='country', log_x=True
    pass

for year, cont in [(1952,'Asia'), (2007,'Europe'), (2007,'All')]:
    fig = make_fig(year, cont)
    if fig:
        fig.write_html(f'gap_{year}_{cont}.html')
✅ Practice Checklist
15. Map Visualizations

Choropleth world map
import plotly.express as px

df = px.data.gapminder().query("year == 2007")
fig = px.choropleth(df, locations='iso_alpha', color='gdpPercap',
                    hover_name='country', color_continuous_scale='Viridis',
                    range_color=[0, 50000], title='GDP per Capita (2007)',
                    labels={'gdpPercap': 'GDP per Capita'})
fig.update_layout(geo=dict(showframe=False, showcoastlines=True))
fig.write_html('choropleth_world.html')
print(f'Choropleth saved - {len(df)} countries')
Scatter geo map (USA)
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({'lat':np.random.uniform(25,50,80),'lon':np.random.uniform(-125,-65,80),
                   'city':[f'City_{i}' for i in range(80)],
                   'sales':np.random.exponential(500,80),
                   'category':np.random.choice(['A','B','C'],80)})
fig = px.scatter_geo(df, lat='lat', lon='lon', size='sales', color='category',
                     hover_name='city', scope='usa', title='Sales Distribution USA', size_max=20)
fig.write_html('scatter_map.html')
print(f'Scatter geo saved - {len(df)} points')
Animated choropleth over time
import plotly.express as px

df = px.data.gapminder()
fig = px.choropleth(df, locations='iso_alpha', color='lifeExp',
                    hover_name='country', animation_frame='year',
                    color_continuous_scale='RdYlGn', range_color=[30, 90],
                    title='Life Expectancy Over Time')
fig.update_layout(geo=dict(showframe=False))
fig.write_html('choropleth_animated.html')
print(f'Animated choropleth - {df.year.nunique()} frames')
Density mapbox heatmap
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({'lat':np.random.normal(40.7128,0.05,400),
                   'lon':np.random.normal(-74.006,0.05,400),
                   'intensity':np.random.exponential(1,400)})
fig = px.density_mapbox(df, lat='lat', lon='lon', z='intensity', radius=15,
                         center=dict(lat=40.7128,lon=-74.006), zoom=10,
                         mapbox_style='open-street-map',
                         title='Event Density (NYC Area)', color_continuous_scale='Inferno')
fig.write_html('density_map.html')
print(f'Density map saved - {len(df)} events')
🏋️ Practice: US State Choropleth
Create a choropleth of US states using locationmode='USA-states'. Assign random performance_score (0-100) to each state abbreviation. Color with RdYlGn. Save as HTML.
Starter Code
import plotly.express as px
import pandas as pd
import numpy as np

states = ['AL','AK','AZ','AR','CA','CO','CT','DE','FL','GA','HI','ID','IL','IN','IA',
          'KS','KY','LA','ME','MD','MA','MI','MN','MS','MO','MT','NE','NV','NH','NJ',
          'NM','NY','NC','ND','OH','OK','OR','PA','RI','SC','SD','TN','TX','UT','VT',
          'VA','WA','WV','WI','WY']
np.random.seed(42)
df = pd.DataFrame({'state':states,'score':np.random.uniform(40,100,len(states))})
# TODO: px.choropleth locationmode='USA-states', color='score', scope='usa'
# TODO: color_continuous_scale='RdYlGn'
# TODO: save 'us_choropleth.html'
✅ Practice Checklist
16. 3D Plots & Surface Visualizations

3D surface with contour projections
import plotly.graph_objects as go
import numpy as np

x = y = np.linspace(-3, 3, 60)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
fig = go.Figure(go.Surface(x=X, y=Y, z=Z, colorscale='Viridis',
                             contours={'z':{'show':True,'start':-1,'end':1,'size':0.25}}))
fig.update_layout(title='3D Surface: sin(sqrt(x2+y2))',
                  scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Z',
                             camera=dict(eye=dict(x=1.5, y=-1.5, z=1.2))),
                  width=700, height=500)
fig.write_html('surface_3d.html')
print('3D surface saved')
3D scatter with cluster colors
import plotly.express as px
import numpy as np

np.random.seed(42)
n = 300
x = np.random.randn(n); y = np.random.randn(n)
z = x**2 + y**2 + np.random.randn(n) * 0.5
cats = ['Inner' if v < 2 else 'Middle' if v < 5 else 'Outer' for v in z]
fig = px.scatter_3d(x=x, y=y, z=z, color=cats, symbol=cats,
                    color_discrete_sequence=px.colors.qualitative.Set1,
                    title='3D Scatter by Distance from Origin')
fig.update_traces(marker=dict(size=4, opacity=0.7))
fig.write_html('scatter_3d.html')
print(f'3D scatter saved - {n} points')
3D spiral trajectory
import plotly.graph_objects as go
import numpy as np

t = np.linspace(0, 8*np.pi, 500)
x = np.cos(t)*np.exp(-t/20); y = np.sin(t)*np.exp(-t/20); z = t/(4*np.pi)
fig = go.Figure()
fig.add_trace(go.Scatter3d(x=x, y=y, z=z, mode='lines',
                            line=dict(color=t, colorscale='Plasma', width=4), name='Trajectory'))
fig.add_trace(go.Scatter3d(x=[x[0]], y=[y[0]], z=[z[0]], mode='markers',
                            marker=dict(size=8, color='green'), name='Start'))
fig.add_trace(go.Scatter3d(x=[x[-1]], y=[y[-1]], z=[z[-1]], mode='markers',
                            marker=dict(size=8, color='red'), name='End'))
fig.update_layout(title='3D Spiral Trajectory',
                  scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Height'))
fig.write_html('trajectory_3d.html')
print('3D trajectory saved')
Loss landscape with gradient descent
import plotly.graph_objects as go
import numpy as np

x = y = np.linspace(-3, 3, 60)
X, Y = np.meshgrid(x, y)
Z = (np.sin(X*2)*np.cos(Y*2)*0.5 + (X**2+Y**2)*0.1 + np.exp(-((X-1)**2+(Y-1)**2))*(-1.5))
Z = (Z - Z.min()) / (Z.max() - Z.min()) * 3

px_path, py_path, pz_path = [2.5], [-2.5], [float(Z[0,-1])]
lr = 0.08
for _ in range(60):
    ix = int(np.argmin(np.abs(x - px_path[-1])))
    iy = int(np.argmin(np.abs(y - py_path[-1])))
    gx = (Z[iy, min(ix+1,59)] - Z[iy, max(ix-1,0)]) / 2
    gy = (Z[min(iy+1,59), ix] - Z[max(iy-1,0), ix]) / 2
    nx, ny = float(np.clip(px_path[-1]-lr*gx,-3,3)), float(np.clip(py_path[-1]-lr*gy,-3,3))
    px_path.append(nx); py_path.append(ny)
    pz_path.append(float(Z[int(np.argmin(np.abs(y-ny))), int(np.argmin(np.abs(x-nx)))]))

fig = go.Figure([go.Surface(x=X, y=Y, z=Z, colorscale='RdYlGn_r', opacity=0.85),
                  go.Scatter3d(x=px_path, y=py_path, z=pz_path, mode='lines+markers',
                               line=dict(color='blue', width=5), marker=dict(size=3),
                               name='GD Path')])
fig.update_layout(title='Loss Landscape + Gradient Descent', width=750, height=550)
fig.write_html('loss_landscape.html')
print(f'Loss landscape saved - final loss: {pz_path[-1]:.3f}')
🏋️ Practice: 3D Cluster Scatter
Generate 3 Gaussian clusters at (0,0,0), (3,3,0), (0,3,3) in 3D. Create a 3D scatter with each cluster in a different color. Add axis labels and save as 'clusters_3d.html'.
Starter Code
import plotly.graph_objects as go
import numpy as np

np.random.seed(42)
centers = [(0,0,0),(3,3,0),(0,3,3)]
colors = ['blue','red','green']
fig = go.Figure()
for i,(cx,cy,cz) in enumerate(centers):
    n = 50
    x = np.random.randn(n)+cx; y = np.random.randn(n)+cy; z = np.random.randn(n)+cz
    # TODO: add Scatter3d trace
    pass
# TODO: update_layout with title, axis labels
# TODO: fig.write_html('clusters_3d.html')
✅ Practice Checklist
17. Candlestick & Financial Charts

Plotly's go.Candlestick and go.Ohlc render OHLC price data with interactive zoom, range slectors, and volume overlays β€” essential for financial dashboards.

Basic candlestick with volume overlay
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np, pandas as pd

np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='B')
close = 100 + np.cumsum(np.random.randn(60) * 1.5)
open_ = close + np.random.randn(60) * 0.8
high  = np.maximum(open_, close) + np.abs(np.random.randn(60) * 0.5)
low   = np.minimum(open_, close) - np.abs(np.random.randn(60) * 0.5)
volume = np.random.randint(1_000_000, 5_000_000, 60)

fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
                    vertical_spacing=0.03, row_heights=[0.75, 0.25])

fig.add_trace(go.Candlestick(x=dates, open=open_, high=high,
                              low=low, close=close, name='OHLC',
                              increasing_line_color='#26a69a',
                              decreasing_line_color='#ef5350'), row=1, col=1)

colors = ['#26a69a' if c >= o else '#ef5350' for c, o in zip(close, open_)]
fig.add_trace(go.Bar(x=dates, y=volume, name='Volume',
                     marker_color=colors, opacity=0.7), row=2, col=1)

fig.update_layout(title='OHLC Price + Volume', xaxis_rangeslider_visible=False,
                  template='plotly_dark', height=500)
fig.update_yaxes(title_text='Price ($)', row=1)
fig.update_yaxes(title_text='Volume', row=2)
fig.write_html('candlestick.html')
print('Chart saved')
Moving averages on candlestick
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=120, freq='B')
close = 150 + np.cumsum(np.random.randn(120) * 2)
open_ = close + np.random.randn(120)
high  = np.maximum(open_, close) + np.abs(np.random.randn(120))
low   = np.minimum(open_, close) - np.abs(np.random.randn(120))

s = pd.Series(close, index=dates)
ma20 = s.rolling(20).mean()
ma50 = s.rolling(50).mean()

fig = go.Figure()
fig.add_trace(go.Candlestick(x=dates, open=open_, high=high,
                              low=low, close=close, name='OHLC',
                              increasing_fillcolor='#26a69a',
                              decreasing_fillcolor='#ef5350'))
fig.add_trace(go.Scatter(x=dates, y=ma20, name='MA 20',
                          line=dict(color='orange', width=1.5)))
fig.add_trace(go.Scatter(x=dates, y=ma50, name='MA 50',
                          line=dict(color='cyan', width=1.5)))

fig.update_layout(title='Candlestick with Moving Averages',
                  template='plotly_dark', xaxis_rangeslider_visible=False,
                  hovermode='x unified', height=450)
fig.write_html('candlestick_ma.html')
print('Saved with MA20/MA50')
Range selector buttons and OHLC bar chart
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(0)
dates = pd.date_range('2022-01-01', periods=250, freq='B')
close = 200 + np.cumsum(np.random.randn(250) * 2.5)
open_ = close + np.random.randn(250) * 1.2
high  = np.maximum(open_, close) + np.abs(np.random.randn(250))
low   = np.minimum(open_, close) - np.abs(np.random.randn(250))

fig = go.Figure(go.Ohlc(x=dates, open=open_, high=high,
                         low=low, close=close, name='OHLC Bars',
                         increasing_line_color='lime',
                         decreasing_line_color='red'))

fig.update_layout(
    title='OHLC with Range Selectors',
    template='plotly_dark',
    xaxis=dict(
        rangeselector=dict(
            buttons=[
                dict(count=1, label='1M', step='month', stepmode='backward'),
                dict(count=3, label='3M', step='month', stepmode='backward'),
                dict(count=6, label='6M', step='month', stepmode='backward'),
                dict(step='all', label='All'),
            ]
        ),
        rangeslider=dict(visible=True),
        type='date',
    ),
    height=450,
)
fig.write_html('ohlc_range.html')
print('OHLC chart with range selector saved')
💼 Real-World: Stock Screener Dashboard
A portfolio tracker shows daily OHLC for top 5 holdings, highlighting days where close > open in green and flagging high-volume days.
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np, pandas as pd

np.random.seed(7)
tickers = ['AAPL', 'MSFT', 'GOOG']
dates = pd.date_range('2024-01-01', periods=40, freq='B')

fig = make_subplots(rows=len(tickers), cols=1, shared_xaxes=True,
                    subplot_titles=tickers, vertical_spacing=0.06)

for row, ticker in enumerate(tickers, 1):
    c = 150 + row*20 + np.cumsum(np.random.randn(40)*2)
    o = c + np.random.randn(40)
    h = np.maximum(o,c) + np.abs(np.random.randn(40)*0.5)
    l = np.minimum(o,c) - np.abs(np.random.randn(40)*0.5)
    fig.add_trace(go.Candlestick(x=dates, open=o, high=h, low=l, close=c,
                                  name=ticker, showlegend=True), row=row, col=1)

fig.update_xaxes(rangeslider_visible=False)
fig.update_layout(title='Portfolio Overview', template='plotly_dark', height=700)
fig.write_html('portfolio.html')
print(f'Portfolio chart: {len(tickers)} tickers saved')
🏋️ Practice: Bollinger Bands
Generate 90 days of OHLC data (random walk starting at 100). Compute a 20-day rolling mean and Β±2 std Bollinger Bands from the close price. Plot as a candlestick with the upper, middle, and lower bands as overlaid lines. Color the bands blue. Save as 'bollinger.html'.
Starter Code
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=90, freq='B')
close = 100 + np.cumsum(np.random.randn(90) * 1.5)
open_ = close + np.random.randn(90)
high  = np.maximum(open_, close) + np.abs(np.random.randn(90)*0.5)
low   = np.minimum(open_, close) - np.abs(np.random.randn(90)*0.5)

s = pd.Series(close, index=dates)
# TODO: compute ma = s.rolling(20).mean()
# TODO: std = s.rolling(20).std()
# TODO: upper = ma + 2*std, lower = ma - 2*std

fig = go.Figure()
# TODO: add Candlestick trace
# TODO: add Scatter traces for upper, middle (ma), lower bands
fig.update_layout(title='Bollinger Bands', template='plotly_dark',
                  xaxis_rangeslider_visible=False)
fig.write_html('bollinger.html')
✅ Practice Checklist
18. Treemap & Sunburst Charts

Treemaps and sunbursts visualize hierarchical data where area/angle encodes value. Use them for budget breakdowns, file system sizes, market cap, and org charts.

Treemap from flat DataFrame
import plotly.express as px
import pandas as pd

df = pd.DataFrame({
    'category': ['Electronics','Electronics','Electronics',
                 'Clothing','Clothing','Food','Food','Food','Food'],
    'subcategory': ['Phones','Laptops','Tablets',
                    'Shirts','Pants','Dairy','Bakery','Produce','Frozen'],
    'sales': [4500, 3200, 1800, 2100, 1600, 900, 750, 1200, 680],
    'profit': [900, 480, 270, 420, 240, 135, 112, 180, 102],
})

fig = px.treemap(df,
    path=['category', 'subcategory'],
    values='sales',
    color='profit',
    color_continuous_scale='RdYlGn',
    title='Sales Treemap by Category β†’ Subcategory',
    hover_data={'profit': ':,.0f'},
)
fig.update_traces(textinfo='label+value+percent root')
fig.write_html('treemap_sales.html')
print(f'Treemap saved β€” {len(df)} rows')
Sunburst for org/portfolio drilldown
import plotly.express as px
import pandas as pd

df = pd.DataFrame({
    'continent': ['Americas','Americas','Americas','Europe','Europe','Asia','Asia','Asia'],
    'country':   ['USA','Brazil','Canada','Germany','France','China','Japan','India'],
    'sector':    ['Tech','Finance','Mining','Auto','Pharma','Tech','Auto','IT'],
    'market_cap':[2800, 400, 350, 680, 520, 1900, 750, 420],
})

fig = px.sunburst(df,
    path=['continent', 'country', 'sector'],
    values='market_cap',
    color='market_cap',
    color_continuous_scale='Blues',
    title='Market Cap: Continent β†’ Country β†’ Sector ($ Bn)',
)
fig.update_traces(
    textinfo='label+percent parent',
    insidetextorientation='radial',
)
fig.update_layout(height=550)
fig.write_html('sunburst_portfolio.html')
print('Sunburst saved')
Treemap with custom text and color
import plotly.graph_objects as go

labels  = ['Total','A Div','B Div','C Div','A1','A2','B1','B2','C1','C2','C3']
parents = ['',    'Total','Total','Total','A Div','A Div','B Div','B Div','C Div','C Div','C Div']
values  = [0,     0,      0,      0,      120,    80,     200,    150,    60,     90,     50]

fig = go.Figure(go.Treemap(
    labels=labels,
    parents=parents,
    values=values,
    branchvalues='total',
    marker=dict(
        colorscale='Viridis',
        showscale=True,
        colorbar=dict(title='Revenue'),
    ),
    texttemplate='<b>%{label}</b><br>$%{value}k',
    hovertemplate='<b>%{label}</b><br>Revenue: $%{value}k<br>Share: %{percentRoot:.1%}<extra></extra>',
))
fig.update_layout(title='Division Revenue Treemap', height=450)
fig.write_html('treemap_custom.html')
print('Custom treemap saved')
💼 Real-World: IT Infrastructure Cost Treemap
An IT manager visualizes cloud spending broken down by department > service > resource type to identify cost hotspots at a glance.
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(1)
depts = ['Engineering','Marketing','Operations']
services = ['Compute','Storage','Database','Network']
data = []
for d in depts:
    for svc in services:
        cost = np.random.randint(500, 8000)
        growth = np.random.uniform(-0.1, 0.3)
        data.append({'dept': d, 'service': svc, 'cost': cost, 'growth_pct': growth})

df = pd.DataFrame(data)
fig = px.treemap(df,
    path=['dept', 'service'],
    values='cost',
    color='growth_pct',
    color_continuous_scale='RdYlGn_r',
    color_continuous_midpoint=0,
    title='Cloud Cost by Dept β†’ Service (color = MoM growth)',
    custom_data=['growth_pct'],
)
fig.update_traces(
    texttemplate='<b>%{label}</b><br>$%{value:,.0f}',
    hovertemplate='%{label}<br>Cost: $%{value:,.0f}<br>Growth: %{customdata[0]:.1%}<extra></extra>',
)
fig.write_html('it_cost_treemap.html')
print(f'IT cost treemap saved β€” {df.cost.sum():,} total spend')
🏋️ Practice: File System Sunburst
Build a sunburst showing a simulated file system: root β†’ 3 folders (docs, src, data) β†’ 2-3 files each with sizes (KB). Use go.Sunburst with branchvalues='total'. Color by size. Show label+percent parent as text. Save as 'filesystem.html'.
Starter Code
import plotly.graph_objects as go

labels  = ['root','docs','src','data',
           'report.pdf','slides.pptx',
           'main.py','utils.py','tests.py',
           'train.csv','test.csv']
parents = ['','root','root','root',
           'docs','docs',
           'src','src','src',
           'data','data']
values  = [0, 0, 0, 0,
           2048, 5120,
           45, 30, 22,
           10240, 4096]

# TODO: create go.Sunburst with branchvalues='total'
# TODO: color by values, add textinfo='label+percent parent'
fig = go.Figure()
fig.update_layout(title='File System Sunburst')
fig.write_html('filesystem.html')
✅ Practice Checklist
19. Violin & Strip Charts

Violin plots combine a KDE curve with a box plot to show distribution shape. Strip/jitter plots show individual data points β€” great for small-to-medium datasets.

Violin with box and points overlay
import plotly.express as px
import numpy as np, pandas as pd

np.random.seed(42)
groups = ['Control', 'Treatment A', 'Treatment B']
data = pd.concat([
    pd.DataFrame({'group': g,
                  'score': np.random.normal(loc, 12, 80)})
    for g, loc in zip(groups, [50, 62, 71])
])

fig = px.violin(data, x='group', y='score', color='group',
                box=True, points='all',
                title='Score Distribution by Treatment Group',
                labels={'score': 'Test Score', 'group': 'Group'},
                color_discrete_sequence=px.colors.qualitative.Pastel)

fig.update_traces(meanline_visible=True, jitter=0.3, pointpos=-1.8,
                  marker=dict(size=4, opacity=0.5))
fig.update_layout(showlegend=False, height=450)
fig.write_html('violin_groups.html')
print('Violin chart saved')
Side-by-side violin for before/after
import plotly.graph_objects as go
import numpy as np

np.random.seed(10)
before = np.random.normal(65, 15, 100)
after  = np.random.normal(72, 12, 100)

fig = go.Figure()
fig.add_trace(go.Violin(y=before, name='Before',
                         side='negative', line_color='#636EFA',
                         fillcolor='rgba(99,110,250,0.3)',
                         box_visible=True, meanline_visible=True))
fig.add_trace(go.Violin(y=after,  name='After',
                         side='positive', line_color='#EF553B',
                         fillcolor='rgba(239,85,59,0.3)',
                         box_visible=True, meanline_visible=True))

fig.update_layout(
    title='Before vs After Intervention (Split Violin)',
    violingap=0, violinmode='overlay',
    yaxis_title='Score',
    template='plotly_white',
    height=420,
)
fig.write_html('violin_split.html')
print(f'Before mean: {before.mean():.1f}  After mean: {after.mean():.1f}')
Strip plot (jitter) with mean markers
import plotly.express as px
import numpy as np, pandas as pd

np.random.seed(5)
categories = ['Category A', 'Category B', 'Category C', 'Category D']
df = pd.concat([
    pd.DataFrame({'category': cat, 'value': np.random.exponential(scale, 50)})
    for cat, scale in zip(categories, [10, 20, 15, 25])
])

fig = px.strip(df, x='category', y='value', color='category',
               title='Strip Plot with Individual Data Points',
               stripmode='overlay',
               color_discrete_sequence=px.colors.qualitative.Set2)

# Add mean markers
for cat in categories:
    mean_val = df[df.category == cat]['value'].mean()
    fig.add_scatter(x=[cat], y=[mean_val], mode='markers',
                    marker=dict(symbol='line-ew', size=20, color='black', line_width=2),
                    showlegend=False, hovertemplate=f'Mean: {mean_val:.1f}')

fig.update_traces(jitter=0.4, marker_size=5, marker_opacity=0.6,
                  selector=dict(type='strip'))
fig.update_layout(showlegend=False, height=420)
fig.write_html('strip_plot.html')
print('Strip plot saved')
💼 Real-World: A/B Test Distribution Comparison
A data scientist uses split violins to compare conversion rates and session durations between two website variants, showing both distribution shape and individual data points.
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

np.random.seed(99)
metrics = {
    'Conversion Rate (%)': (np.random.beta(2, 20, 200)*100,
                            np.random.beta(3, 20, 200)*100),
    'Session Duration (s)': (np.random.exponential(120, 200),
                             np.random.exponential(145, 200)),
}

fig = make_subplots(rows=1, cols=2, subplot_titles=list(metrics.keys()))
colors = ['#636EFA', '#EF553B']

for col, (metric, (ctrl, var)) in enumerate(metrics.items(), 1):
    for data, name, side, color in [
        (ctrl, 'Control',  'negative', colors[0]),
        (var,  'Variant',  'positive', colors[1]),
    ]:
        fig.add_trace(
            go.Violin(y=data, name=name, side=side,
                      line_color=color, box_visible=True,
                      meanline_visible=True, showlegend=(col==1)),
            row=1, col=col
        )

fig.update_layout(title='A/B Test: Control vs Variant',
                  violinmode='overlay', template='plotly_white',
                  height=420)
fig.write_html('ab_test_violin.html')
for m, (c, v) in metrics.items():
    print(f'{m}: ctrl={c.mean():.1f} | var={v.mean():.1f} | lift={((v.mean()-c.mean())/c.mean())*100:.1f}%')
🏋️ Practice: Multi-Group Violin
Generate exam scores for 4 subjects (Math, Science, English, History) with 60 students each (different means: 72, 68, 75, 65; std=12). Create a violin plot with box=True, points='outliers'. Color by subject. Add a horizontal dashed line at score=70 (passing threshold). Save as 'exam_scores.html'.
Starter Code
import plotly.express as px
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(42)
subjects = ['Math','Science','English','History']
means    = [72, 68, 75, 65]

data = pd.concat([
    pd.DataFrame({'subject': s, 'score': np.random.normal(m, 12, 60)})
    for s, m in zip(subjects, means)
])
data['score'] = data['score'].clip(0, 100)

# TODO: create px.violin with box=True, points='outliers'
# TODO: add go.Scatter horizontal line at y=70 (passing threshold)
fig = go.Figure()
fig.update_layout(title='Exam Score Distributions', height=430)
fig.write_html('exam_scores.html')
✅ Practice Checklist
20. Parallel Coordinates & Categories

Parallel coordinates show multi-dimensional continuous data on parallel axes β€” each line is one observation. Parallel categories (Sankey-style) show flows between categorical variables.

Parallel coordinates for ML feature analysis
import plotly.express as px
from sklearn.datasets import load_iris
import pandas as pd

iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target  # 0, 1, 2

fig = px.parallel_coordinates(
    df,
    color='species',
    dimensions=iris.feature_names,
    color_continuous_scale=px.colors.diverging.Tealrose,
    color_continuous_midpoint=1,
    title='Iris Dataset β€” Parallel Coordinates',
    labels={n: n.replace(' (cm)', '') for n in iris.feature_names},
)
fig.update_layout(height=430)
fig.write_html('parallel_coords_iris.html')
print('Drag axes to filter! Saved.')
Parallel coordinates with custom dimension ranges
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(42)
n = 200
df = pd.DataFrame({
    'age':     np.random.randint(22, 65, n),
    'income':  np.random.exponential(50000, n).clip(20000, 200000),
    'savings': np.random.exponential(30000, n).clip(0, 150000),
    'credit':  np.random.randint(300, 850, n),
    'loan':    np.random.choice([0, 1], n, p=[0.7, 0.3]),
})

fig = go.Figure(go.Parcoords(
    line=dict(color=df['loan'], colorscale='RdYlGn_r',
              showscale=True, colorbar=dict(title='Default')),
    dimensions=[
        dict(label='Age', values=df['age'], range=[22, 65]),
        dict(label='Income ($)', values=df['income'], range=[20000, 200000]),
        dict(label='Savings ($)', values=df['savings'], range=[0, 150000]),
        dict(label='Credit Score', values=df['credit'], range=[300, 850]),
    ],
))
fig.update_layout(title='Loan Default β€” Parallel Coordinates',
                  height=430, template='plotly_dark')
fig.write_html('parallel_loan.html')
print('Loan parallel coords saved')
Parallel categories (categorical Sankey flow)
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(3)
n = 500
df = pd.DataFrame({
    'region':    np.random.choice(['North','South','East','West'], n),
    'segment':   np.random.choice(['SMB','Enterprise','Consumer'], n),
    'channel':   np.random.choice(['Online','Retail','Partner'], n),
    'outcome':   np.random.choice(['Won','Lost','Pending'], n, p=[0.4,0.4,0.2]),
})

fig = px.parallel_categories(
    df,
    dimensions=['region','segment','channel','outcome'],
    color=df['outcome'].map({'Won': 0, 'Pending': 0.5, 'Lost': 1}),
    color_continuous_scale='RdYlGn_r',
    title='Sales Pipeline Flow: Region β†’ Segment β†’ Channel β†’ Outcome',
)
fig.update_layout(height=450)
fig.write_html('parallel_categories.html')
print('Parallel categories chart saved')
💼 Real-World: Model Feature Explorer
An ML team uses parallel coordinates on their validation set to interactively filter observations β€” dragging axes to isolate the feature ranges where the model underperforms.
import plotly.express as px
import numpy as np, pandas as pd
from sklearn.datasets import load_wine

wine = load_wine()
df = pd.DataFrame(wine.data, columns=wine.feature_names)
df['class'] = wine.target
df['error'] = np.random.choice([0, 1], len(df), p=[0.8, 0.2])

# Use only most informative features
top_features = ['alcohol', 'malic_acid', 'flavanoids',
                'color_intensity', 'proline']

fig = px.parallel_coordinates(
    df,
    color='class',
    dimensions=top_features + ['class'],
    color_continuous_scale=px.colors.sequential.Viridis,
    title='Wine Dataset β€” Feature Space by Class',
    labels={f: f.replace('_', ' ').title() for f in top_features},
)
fig.update_layout(height=430)
fig.write_html('wine_parallel.html')
print(f'Wine parallel coords: {len(df)} samples, {len(top_features)} features')
🏋️ Practice: Car Dataset Explorer
Using px.data.cars() (or generate synthetic data: mpg, cylinders, horsepower, weight, model_year), create a parallel coordinates chart colored by 'cylinders'. Add a second chart using px.parallel_categories with columns cylinders and origin. Save both as 'cars_parcoord.html' and 'cars_parcat.html'.
Starter Code
import plotly.express as px
import pandas as pd
import numpy as np

# Use built-in cars dataset
try:
    df = px.data.cars()
except Exception:
    np.random.seed(42)
    n = 200
    df = pd.DataFrame({
        'mpg': np.random.normal(23, 7, n).clip(8, 46),
        'cylinders': np.random.choice([4, 6, 8], n, p=[0.5, 0.3, 0.2]),
        'horsepower': np.random.normal(100, 40, n).clip(40, 230),
        'weight': np.random.normal(2800, 700, n).clip(1600, 5000),
        'model_year': np.random.randint(70, 83, n),
        'origin': np.random.choice(['USA','Europe','Japan'], n),
    })

# TODO: parallel_coordinates colored by cylinders
# TODO: parallel_categories with cylinders and origin
✅ Practice Checklist
21. Funnel & Waterfall Charts

Funnel charts show sequential stage drop-off (sales pipelines, conversion funnels). Waterfall charts show cumulative effects of positive and negative values β€” perfect for P&L statements.

Sales funnel with conversion rates
import plotly.graph_objects as go

stages = ['Website Visits', 'Product Views', 'Add to Cart',
          'Checkout Started', 'Purchase Completed']
counts = [10000, 5200, 2100, 980, 420]

conversion = [f'{counts[i]/counts[i-1]:.0%}' if i > 0 else '100%'
              for i in range(len(counts))]

fig = go.Figure(go.Funnel(
    y=stages,
    x=counts,
    textposition='inside',
    textinfo='value+percent previous',
    opacity=0.85,
    marker=dict(
        color=['#636EFA','#EF553B','#00CC96','#AB63FA','#FFA15A'],
        line=dict(width=2, color='white'),
    ),
    connector=dict(line=dict(color='#888', width=1)),
))
fig.update_layout(
    title='E-Commerce Conversion Funnel',
    template='plotly_white',
    height=420,
    margin=dict(l=200),
)
fig.write_html('sales_funnel.html')
print(f'Overall conversion: {counts[-1]/counts[0]:.1%}')
Waterfall chart for P&L statement
import plotly.graph_objects as go

items   = ['Revenue','COGS','Gross Profit','R&D',
           'S&M','G&A','EBITDA','D&A','EBIT']
values  = [1000, -380, None, -150, -120, -80, None, -45, None]
measure = ['absolute','relative','total','relative',
           'relative','relative','total','relative','total']
text    = [f'${abs(v):,}' if v else '' for v in values]

fig = go.Figure(go.Waterfall(
    orientation='v',
    measure=measure,
    x=items,
    y=values,
    text=text,
    textposition='outside',
    increasing=dict(marker_color='#26a69a'),
    decreasing=dict(marker_color='#ef5350'),
    totals=dict(marker_color='#636EFA'),
    connector=dict(line=dict(color='#999', width=1, dash='dot')),
))
fig.update_layout(
    title='P&L Waterfall β€” Q4 2024',
    yaxis_title='$ Thousands',
    template='plotly_white',
    height=450,
    showlegend=False,
)
fig.write_html('pnl_waterfall.html')
print('P&L waterfall saved')
Funnel area and horizontal funnel
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

stages = ['Awareness','Interest','Consideration','Intent','Purchase']
values = [50000, 22000, 8500, 3200, 980]

fig = make_subplots(rows=1, cols=2,
                    subplot_titles=['Standard Funnel', 'Funnel Area'],
                    specs=[[{'type':'funnel'}, {'type':'funnelarea'}]])

fig.add_trace(
    go.Funnel(y=stages, x=values, textinfo='value+percent initial',
              marker_color=px.colors.sequential.Blues_r[1:]),
    row=1, col=1)

fig.add_trace(
    go.Funnelarea(labels=stages, values=values,
                  textinfo='label+percent',
                  marker_colors=px.colors.sequential.Greens_r[1:]),
    row=1, col=2)

fig.update_layout(title='Marketing Funnel Comparison', height=420)
fig.write_html('funnel_comparison.html')
print(f'Top-of-funnel to purchase: {values[-1]/values[0]:.1%}')
💼 Real-World: SaaS Revenue Waterfall
A CFO presents monthly recurring revenue changes: starting ARR, new business, expansions, contractions, and churn β€” visualized as a waterfall to show net revenue change.
import plotly.graph_objects as go
import numpy as np

months = ['Jan','Feb','Mar','Apr','May','Jun']
starting_arr = 500

data_rows = []
for m in months:
    new_biz    = np.random.randint(15, 40)
    expansion  = np.random.randint(5, 20)
    contraction= -np.random.randint(2, 10)
    churn      = -np.random.randint(5, 20)
    data_rows.append((m, new_biz, expansion, contraction, churn))

items   = ['Start']
values  = [starting_arr]
measure = ['absolute']

for month, new, exp, con, churn in data_rows:
    items   += [f'{month} New', f'{month} Exp', f'{month} Con', f'{month} Churn']
    values  += [new, exp, con, churn]
    measure += ['relative','relative','relative','relative']

items.append('End ARR')
values.append(None)
measure.append('total')

fig = go.Figure(go.Waterfall(
    x=items, y=values, measure=measure,
    increasing=dict(marker_color='#26a69a'),
    decreasing=dict(marker_color='#ef5350'),
    totals=dict(marker_color='#636EFA'),
    textposition='outside', texttemplate='%{y:+.0f}',
))
fig.update_layout(title='SaaS ARR Waterfall (H1 2024)', height=450,
                  template='plotly_white', xaxis_tickangle=45)
fig.write_html('saas_waterfall.html')
print('SaaS waterfall saved')
🏋️ Practice: Budget Variance Waterfall
Create a waterfall chart showing budget vs actuals for 5 departments: start with 'Budget Total' at 1,000,000. Show each department as a relative bar (some over, some under). End with 'Actual Total' as a total bar. Color over-budget red, under-budget green. Save as 'budget_variance.html'.
Starter Code
import plotly.graph_objects as go

departments = ['Engineering','Marketing','Sales','Operations','HR']
budget_each = [200000, 150000, 180000, 120000, 100000]  # sums to 750k; rest is overhead
variances   = [+15000, -8000, +22000, -5000, +3000]      # actual - budget per dept

items   = ['Budget Total'] + departments + ['Overhead', 'Actual Total']
# TODO: set up values list (budget_total=750000, then variances, then overhead=250000, total=None)
# TODO: set up measure list
# TODO: create go.Waterfall with increasing green, decreasing red
fig = go.Figure()
fig.update_layout(title='Budget Variance Waterfall', height=430)
fig.write_html('budget_variance.html')
✅ Practice Checklist
22. Indicator & Gauge Charts

go.Indicator renders KPI numbers, delta comparisons, and gauge/speedometer charts. Combine them in subplots to build executive dashboards without any extra libraries.

KPI indicators with delta
import plotly.graph_objects as go
from plotly.subplots import make_subplots

fig = make_subplots(rows=1, cols=4,
                    specs=[[{'type':'indicator'}]*4])

kpis = [
    ('Revenue',  '1.24M',  1_240_000, 1_100_000),
    ('Users',    '48.3K',  48_300,    45_100),
    ('Churn %',  '2.1%',   2.1,       2.8),
    ('NPS',      '67',     67,        61),
]

for col, (name, num_str, val, ref) in enumerate(kpis, 1):
    better_if_lower = 'churn' in name.lower()
    fig.add_trace(go.Indicator(
        mode='number+delta',
        value=val,
        title=dict(text=name, font=dict(size=14)),
        delta=dict(
            reference=ref,
            increasing=dict(color='red' if better_if_lower else 'green'),
            decreasing=dict(color='green' if better_if_lower else 'red'),
            valueformat='.1%' if '%' in num_str else None,
        ),
        number=dict(
            prefix='$' if 'M' in num_str else '',
            suffix='%' if '%' in num_str else '',
        ),
    ), row=1, col=col)

fig.update_layout(title='Business KPI Dashboard', height=200,
                  template='plotly_dark', margin=dict(t=50, b=20))
fig.write_html('kpi_indicators.html')
print('KPI dashboard saved')
Gauge / speedometer chart
import plotly.graph_objects as go
from plotly.subplots import make_subplots

fig = make_subplots(rows=1, cols=3,
                    specs=[[{'type':'indicator'}]*3])

gauges = [
    ('CPU Usage', 73, '%', 'red', [(0,40,'green'),(40,70,'yellow'),(70,100,'red')]),
    ('Memory',    58, '%', 'orange', [(0,60,'green'),(60,80,'orange'),(80,100,'red')]),
    ('SLA Score', 96.5, '%', 'green', [(0,90,'red'),(90,95,'yellow'),(95,100,'green')]),
]

for col, (name, val, suffix, color, steps) in enumerate(gauges, 1):
    fig.add_trace(go.Indicator(
        mode='gauge+number+delta',
        value=val,
        title=dict(text=name, font=dict(size=14)),
        delta=dict(reference=80 if 'SLA' not in name else 95),
        number=dict(suffix=suffix),
        gauge=dict(
            axis=dict(range=[0, 100]),
            bar=dict(color=color, thickness=0.25),
            steps=[dict(range=[lo, hi], color=c) for lo, hi, c in steps],
            threshold=dict(line=dict(color='white', width=3),
                           thickness=0.75, value=val),
        ),
    ), row=1, col=col)

fig.update_layout(title='Infrastructure Gauges', height=280,
                  template='plotly_dark', margin=dict(t=60, b=10))
fig.write_html('gauges.html')
print('Gauge dashboard saved')
Bullet chart style indicator
import plotly.graph_objects as go
from plotly.subplots import make_subplots

metrics = [
    ('Q1 Revenue',   1.15, 1.25, 'M$'),
    ('Q1 Leads',     430,  500,  ''),
    ('Avg Deal ($)', 22500, 20000, '$'),
    ('Win Rate',     42,   40,   '%'),
]

fig = make_subplots(rows=len(metrics), cols=1,
                    specs=[[{'type':'indicator'}]] * len(metrics),
                    vertical_spacing=0.0)

for row, (name, actual, target, unit) in enumerate(metrics, 1):
    color = 'green' if actual >= target else 'red'
    fig.add_trace(go.Indicator(
        mode='number+gauge+delta',
        value=actual,
        title=dict(text=name, font=dict(size=12)),
        delta=dict(reference=target,
                   relative=True,
                   increasing=dict(color='green'),
                   decreasing=dict(color='red')),
        number=dict(prefix=unit if unit=='$' else '',
                    suffix=unit if unit!='$' else ''),
        gauge=dict(
            shape='bullet',
            axis=dict(range=[0, target*1.5]),
            threshold=dict(value=target,
                           line=dict(color='black', width=2),
                           thickness=0.75),
            bar=dict(color=color),
        ),
    ), row=row, col=1)

fig.update_layout(title='Q1 Performance vs Target',
                  height=360, template='plotly_white',
                  margin=dict(l=160, t=60, b=20))
fig.write_html('bullet_chart.html')
print('Bullet chart saved')
💼 Real-World: Executive SaaS Dashboard
A SaaS company's weekly executive report shows ARR, MRR growth, churn rate, and NPS as KPI indicators with week-over-week deltas and color coding.
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

# Simulate current vs previous week metrics
np.random.seed(42)
metrics = {
    'ARR ($M)':    (4.82,  4.71,  False),
    'MRR Growth%': (3.2,   2.8,   False),
    'Churn %':     (1.8,   2.1,   True),   # lower is better
    'NPS':         (71,    68,    False),
    'CAC ($)':     (1250,  1380,  True),    # lower is better
    'LTV/CAC':     (4.8,   4.2,   False),
}

n = len(metrics)
fig = make_subplots(rows=2, cols=3,
                    specs=[[{'type':'indicator'}]*3]*2)

for idx, (name, (curr, prev, lower_better)) in enumerate(metrics.items()):
    row, col = divmod(idx, 3)
    fig.add_trace(go.Indicator(
        mode='number+delta',
        value=curr,
        title=dict(text=name, font=dict(size=13)),
        delta=dict(
            reference=prev,
            relative=True,
            increasing=dict(color='red' if lower_better else 'green'),
            decreasing=dict(color='green' if lower_better else 'red'),
        ),
    ), row=row+1, col=col+1)

fig.update_layout(title='SaaS Weekly KPI Report',
                  height=320, template='plotly_dark',
                  margin=dict(t=60, b=20))
fig.write_html('saas_kpi.html')
print('SaaS KPI report saved')
🏋️ Practice: OKR Progress Gauges
Create 4 gauge indicators in a 2x2 subplot grid, one per OKR: (1) Revenue Target: 85% achieved (target 100), (2) Customer Satisfaction: 4.2/5, (3) Bug Backlog: 23 (target <30, lower is better), (4) Feature Delivery: 7/10 shipped. Use colored gauge steps (red/yellow/green). Save as 'okr_gauges.html'.
Starter Code
import plotly.graph_objects as go
from plotly.subplots import make_subplots

fig = make_subplots(rows=2, cols=2,
                    specs=[[{'type':'indicator'}]*2]*2)

okrs = [
    ('Revenue Target',      85,   100,  '%',  [(0,60,'red'),(60,80,'yellow'),(80,100,'green')]),
    ('Cust. Satisfaction',  4.2,  5,    '/5', [(0,3,'red'),(3,4,'yellow'),(4,5,'green')]),
    ('Bug Backlog',         23,   30,   '',   [(0,15,'green'),(15,25,'yellow'),(25,30,'red')]),
    ('Feature Delivery',    7,    10,   '/10',[(0,5,'red'),(5,7,'yellow'),(7,10,'green')]),
]

for idx, (name, val, max_val, suffix, steps) in enumerate(okrs):
    row, col = divmod(idx, 2)
    # TODO: add go.Indicator with mode='gauge+number', gauge steps, threshold at target
    pass

fig.update_layout(title='OKR Progress Gauges', height=480, template='plotly_dark')
fig.write_html('okr_gauges.html')
✅ Practice Checklist
23. Animated Bar Chart Race & Timelines

Plotly animations use frames and sliders to animate data over time. Bar chart races, animated scatter plots, and timeline maps reveal how data evolves.

Bar chart race with animation frames
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(42)
companies = ['Alpha','Beta','Gamma','Delta','Epsilon','Zeta']
years = list(range(2015, 2025))

# Generate cumulative revenue data
revenues = {c: np.cumsum(np.random.exponential(10, len(years))) * (1 + 0.1 * i)
            for i, c in enumerate(companies)}
df = pd.DataFrame(revenues, index=years)

frames = []
for year in years:
    row = df.loc[year].sort_values(ascending=True)
    frames.append(go.Frame(
        data=[go.Bar(x=row.values, y=row.index,
                     orientation='h',
                     marker_color=px_colors := [
                         '#636EFA','#EF553B','#00CC96','#AB63FA',
                         '#FFA15A','#19D3F3'][:len(row)],
                     text=[f'${v:.0f}B' for v in row.values],
                     textposition='outside')],
        name=str(year),
        layout=go.Layout(title_text=f'Company Revenue Race β€” {year}')
    ))

first = df.loc[years[0]].sort_values(ascending=True)
fig = go.Figure(
    data=[go.Bar(x=first.values, y=first.index, orientation='h',
                 marker_color=['#636EFA','#EF553B','#00CC96',
                               '#AB63FA','#FFA15A','#19D3F3'][:len(first)],
                 text=[f'${v:.0f}B' for v in first.values],
                 textposition='outside')],
    frames=frames,
    layout=go.Layout(
        title=f'Company Revenue Race β€” {years[0]}',
        xaxis=dict(range=[0, df.values.max()*1.15], title='Revenue ($B)'),
        updatemenus=[dict(type='buttons', showactive=False,
                          buttons=[dict(label='Play',
                                       method='animate',
                                       args=[None, dict(frame=dict(duration=600, redraw=True),
                                                        fromcurrent=True)])])],
        sliders=[dict(steps=[dict(method='animate', args=[[str(y)]], label=str(y))
                             for y in years],
                      currentvalue=dict(prefix='Year: '))],
        height=450, template='plotly_dark',
    )
)
fig.write_html('bar_race.html')
print('Bar chart race saved')
Animated scatter over time
import plotly.express as px

df = px.data.gapminder()

fig = px.scatter(
    df, x='gdpPercap', y='lifeExp',
    animation_frame='year',
    animation_group='country',
    size='pop', color='continent',
    hover_name='country',
    log_x=True,
    size_max=55,
    range_x=[100, 100_000],
    range_y=[25, 90],
    title='Gapminder: GDP vs Life Expectancy (1952-2007)',
    labels={'gdpPercap': 'GDP per Capita', 'lifeExp': 'Life Expectancy'},
    template='plotly_white',
)
fig.update_layout(height=500)
fig.write_html('gapminder_animation.html')
print(f'Animated scatter: {df.year.nunique()} frames, {df.country.nunique()} countries')
Animated choropleth map
import plotly.express as px

df = px.data.gapminder()

fig = px.choropleth(
    df,
    locations='iso_alpha',
    color='lifeExp',
    hover_name='country',
    animation_frame='year',
    color_continuous_scale='RdYlGn',
    range_color=[25, 90],
    title='World Life Expectancy Over Time (1952-2007)',
    labels={'lifeExp': 'Life Expectancy'},
    projection='natural earth',
)
fig.update_layout(
    coloraxis_colorbar=dict(title='Life Exp.', x=1.0),
    height=480,
    geo=dict(showframe=False, showcoastlines=False),
)
fig.write_html('choropleth_animated.html')
print('Animated choropleth saved')
💼 Real-World: Market Share Race
A market analyst builds a bar chart race showing quarterly market share shifts between 5 smartphone brands over 3 years, helping executives spot disruption trends.
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(7)
brands  = ['AlphaPhone','BetaMobile','GammaDevice','DeltaTech','EpsilonX']
quarters= [f'Q{q} {y}' for y in range(2021,2025) for q in range(1,5)]

# Market share that sums to 100% each quarter
raw = np.random.dirichlet(np.ones(len(brands)), len(quarters)) * 100
df  = pd.DataFrame(raw, columns=brands, index=quarters)

frames = []
for q in quarters:
    row = df.loc[q].sort_values(ascending=True)
    frames.append(go.Frame(
        data=[go.Bar(x=row.values, y=row.index, orientation='h',
                     text=[f'{v:.1f}%' for v in row.values],
                     textposition='outside',
                     marker_color=['#636EFA','#EF553B','#00CC96','#AB63FA','#FFA15A'][:len(row)])],
        name=q,
        layout=go.Layout(title_text=f'Smartphone Market Share β€” {q}')
    ))

first = df.iloc[0].sort_values(ascending=True)
fig = go.Figure(
    data=[go.Bar(x=first.values, y=first.index, orientation='h',
                 text=[f'{v:.1f}%' for v in first.values], textposition='outside',
                 marker_color=['#636EFA','#EF553B','#00CC96','#AB63FA','#FFA15A'])],
    frames=frames,
    layout=go.Layout(
        title=f'Smartphone Market Share β€” {quarters[0]}',
        xaxis=dict(range=[0, 45], title='Market Share (%)'),
        updatemenus=[dict(type='buttons', showactive=False,
                          buttons=[dict(label='β–Ά Play', method='animate',
                                        args=[None, dict(frame=dict(duration=700))])])],
        sliders=[dict(steps=[dict(method='animate', args=[[q]], label=q) for q in quarters],
                      currentvalue=dict(prefix='Quarter: '))],
        template='plotly_dark', height=450,
    )
)
fig.write_html('market_share_race.html')
print(f'Race saved: {len(quarters)} quarters')
🏋️ Practice: Population Pyramid Animation
Generate synthetic population data for age groups (0-9, 10-19, ..., 70+) across 5 decades (1980-2020). Build an animated horizontal bar chart race where each frame is a decade. Use negative values for female population to create a butterfly/pyramid effect. Save as 'population_pyramid.html'.
Starter Code
import plotly.graph_objects as go
import numpy as np

age_groups = ['0-9','10-19','20-29','30-39','40-49','50-59','60-69','70+']
decades = [1980, 1990, 2000, 2010, 2020]

np.random.seed(42)
frames = []
for decade in decades:
    # Generate male/female populations (in millions)
    base = np.random.exponential(5, len(age_groups)) + 2
    male   =  base + np.random.randn(len(age_groups))
    female = -(base + np.random.randn(len(age_groups)))
    frames.append(go.Frame(
        data=[
            go.Bar(y=age_groups, x=male,   orientation='h', name='Male'),
            # TODO: add female Bar trace with negative values
        ],
        name=str(decade),
        layout=go.Layout(title_text=f'Population Pyramid β€” {decade}')
    ))

# TODO: create fig with first frame data, frames, updatemenus, sliders
fig = go.Figure()
fig.update_layout(title='Population Pyramid', barmode='overlay',
                  xaxis_title='Population (M)', height=450)
fig.write_html('population_pyramid.html')
✅ Practice Checklist
24. Custom Templates & Styling

Plotly templates define default colors, fonts, backgrounds, and axis styles. Create reusable brand templates with pio.templates and apply them globally or per-chart.

Creating and applying a custom template
import plotly.graph_objects as go
import plotly.io as pio
import plotly.express as px

# Define a brand template
brand_template = go.layout.Template(
    layout=go.Layout(
        font=dict(family='Arial', size=13, color='#2c2c2c'),
        paper_bgcolor='#f8f9fa',
        plot_bgcolor='#ffffff',
        colorway=['#005A9E','#E63946','#06D6A0','#FFB703','#8338EC'],
        title=dict(font=dict(size=18, color='#005A9E', family='Arial Bold')),
        xaxis=dict(gridcolor='#e9ecef', linecolor='#dee2e6', zeroline=False),
        yaxis=dict(gridcolor='#e9ecef', linecolor='#dee2e6', zeroline=False),
        legend=dict(bgcolor='rgba(255,255,255,0.8)',
                    bordercolor='#dee2e6', borderwidth=1),
        hoverlabel=dict(bgcolor='white', font_size=12,
                        bordercolor='#dee2e6'),
    )
)

# Register the template
pio.templates['brand'] = brand_template
pio.templates.default = 'brand'   # set as global default

import numpy as np, pandas as pd
np.random.seed(42)
df = pd.DataFrame({
    'month': pd.date_range('2024-01', periods=12, freq='MS'),
    'product_a': np.cumsum(np.random.randn(12)*5) + 100,
    'product_b': np.cumsum(np.random.randn(12)*4) + 80,
})

fig = px.line(df, x='month', y=['product_a','product_b'],
              title='Monthly Sales β€” Brand Template',
              labels={'value':'Revenue ($K)', 'variable':'Product'})
fig.write_html('brand_template.html')
print('Brand template chart saved')

# Reset to default
pio.templates.default = 'plotly'
Dark theme with custom accent colors
import plotly.graph_objects as go
import plotly.io as pio
import numpy as np, pandas as pd

dark_template = go.layout.Template(
    layout=dict(
        paper_bgcolor='#0d1117',
        plot_bgcolor='#0d1117',
        font=dict(color='#c9d1d9', family='JetBrains Mono, monospace'),
        colorway=['#79c0ff','#ff7b72','#56d364','#d2a8ff','#ffa657','#39c5cf'],
        xaxis=dict(gridcolor='#30363d', linecolor='#30363d',
                   tickcolor='#8b949e', title_font_color='#8b949e'),
        yaxis=dict(gridcolor='#30363d', linecolor='#30363d',
                   tickcolor='#8b949e', title_font_color='#8b949e'),
        title=dict(font=dict(color='#79c0ff', size=17)),
        legend=dict(bgcolor='#161b22', bordercolor='#30363d', borderwidth=1),
        hoverlabel=dict(bgcolor='#161b22', bordercolor='#30363d',
                        font=dict(color='#c9d1d9')),
    )
)
pio.templates['github_dark'] = dark_template

np.random.seed(42)
categories = ['Q1','Q2','Q3','Q4']
products   = ['Widget','Gadget','Gizmo']
fig = go.Figure()
for p in products:
    fig.add_trace(go.Bar(name=p, x=categories,
                          y=np.random.randint(50, 200, 4),
                          text=np.random.randint(50, 200, 4),
                          texttemplate='%{text}',
                          textposition='outside'))
fig.update_layout(title='Quarterly Sales β€” GitHub Dark Theme',
                  template='github_dark', barmode='group', height=430)
fig.write_html('dark_theme.html')
print('Dark theme chart saved')
Per-trace styling and annotations
import plotly.graph_objects as go
import numpy as np, pandas as pd

np.random.seed(42)
x = pd.date_range('2024-01', periods=52, freq='W')
actual   = 100 + np.cumsum(np.random.randn(52) * 3)
forecast = actual[-1] + np.cumsum(np.random.randn(12) * 2.5)
upper_ci = forecast + 1.96 * np.arange(1, 13) * 0.8
lower_ci = forecast - 1.96 * np.arange(1, 13) * 0.8
future_x = pd.date_range(x[-1], periods=13, freq='W')[1:]

fig = go.Figure()

# Actual data
fig.add_trace(go.Scatter(x=x, y=actual, name='Actual',
                          line=dict(color='#636EFA', width=2.5)))

# Forecast with confidence interval fill
fig.add_trace(go.Scatter(x=future_x, y=upper_ci, name='Upper CI 95%',
                          line=dict(color='rgba(255,127,14,0)', width=0),
                          showlegend=False))
fig.add_trace(go.Scatter(x=future_x, y=lower_ci, name='Lower CI 95%',
                          line=dict(color='rgba(255,127,14,0)', width=0),
                          fill='tonexty', fillcolor='rgba(255,127,14,0.2)',
                          showlegend=False))
fig.add_trace(go.Scatter(x=future_x, y=forecast, name='Forecast',
                          line=dict(color='#FF7F0E', width=2, dash='dash')))

# Annotation for forecast start
fig.add_vline(x=x[-1], line_dash='dot', line_color='gray', opacity=0.7)
fig.add_annotation(x=x[-1], y=actual[-1], text='Forecast begins',
                    showarrow=True, arrowhead=2, bgcolor='rgba(0,0,0,0.6)',
                    font=dict(color='white'))

fig.update_layout(title='Revenue Actual + 12-Week Forecast with CI',
                  xaxis_title='Date', yaxis_title='Revenue ($K)',
                  template='plotly_dark', height=430, hovermode='x unified')
fig.write_html('forecast_styled.html')
print('Styled forecast chart saved')
💼 Real-World: Branded Marketing Report
A marketing team builds a reusable company-branded Plotly template with corporate fonts and colors, then applies it to all quarterly charts for consistent reporting.
import plotly.graph_objects as go
import plotly.io as pio
import plotly.express as px
import numpy as np, pandas as pd

# Corporate brand template
pio.templates['corporate'] = go.layout.Template(
    layout=dict(
        font=dict(family='Helvetica Neue, sans-serif', size=13),
        paper_bgcolor='#FFFFFF',
        plot_bgcolor='#FAFAFA',
        colorway=['#1A237E','#283593','#3949AB','#5C6BC0','#7986CB'],
        title=dict(font=dict(size=16, color='#1A237E', family='Helvetica Neue Bold')),
        xaxis=dict(gridcolor='#EEEEEE', linecolor='#BDBDBD'),
        yaxis=dict(gridcolor='#EEEEEE', linecolor='#BDBDBD'),
        legend=dict(bgcolor='rgba(255,255,255,0.9)'),
    )
)

np.random.seed(5)
months = pd.date_range('2024-01', periods=12, freq='MS')
channels = ['Organic','Paid','Email','Social']
data = {c: np.random.randint(200, 800, 12) for c in channels}
df = pd.DataFrame(data, index=months).reset_index().rename(columns={'index':'month'})
df_melt = df.melt('month', var_name='channel', value_name='leads')

fig = px.area(df_melt, x='month', y='leads', color='channel',
              title='Marketing Leads by Channel β€” Corporate Theme',
              template='corporate')
fig.update_traces(opacity=0.8)
fig.write_html('corporate_report.html')
print(f'Corporate report saved β€” {df_melt.leads.sum():,} total leads')
🏋️ Practice: Dashboard Layout
Create a 2x2 subplot dashboard using make_subplots: (top-left) line chart of weekly sales, (top-right) bar chart of sales by region, (bottom-left) scatter of spend vs revenue, (bottom-right) pie chart of channel mix. Apply a consistent dark template. Add a main title. Save as 'dashboard.html'.
Starter Code
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np, pandas as pd

np.random.seed(42)
weeks   = pd.date_range('2024-01', periods=20, freq='W')
sales   = 100 + np.cumsum(np.random.randn(20)*5)
regions = ['North','South','East','West']
reg_sales = np.random.randint(200, 600, 4)
spend   = np.random.uniform(10, 100, 50)
revenue = spend * 3 + np.random.randn(50) * 20
channels = ['Organic','Paid','Email','Social']
channel_mix = [40, 30, 20, 10]

fig = make_subplots(rows=2, cols=2,
                    subplot_titles=['Weekly Sales','Sales by Region',
                                    'Spend vs Revenue','Channel Mix'],
                    specs=[[{},{}],[{},{'type':'pie'}]])

# TODO: add go.Scatter for weekly sales (row=1,col=1)
# TODO: add go.Bar for regions (row=1,col=2)
# TODO: add go.Scatter mode='markers' for spend vs revenue (row=2,col=1)
# TODO: add go.Pie for channels (row=2,col=2)

fig.update_layout(title='Sales Dashboard', template='plotly_dark',
                  height=600, showlegend=False)
fig.write_html('dashboard.html')
✅ Practice Checklist