Mosaic Plot for Categorical Association Analysis — Plotly

A mosaic plot visualizes contingency tables by dividing a rectangular area into smaller rectangles whose areas are proportional to cell frequencies. This statistical visualization technique effectively shows relationships and associations between two or more categorical variables, making it easy to identify patterns, dependencies, and deviations from expected frequencies in cross-tabulated data.

Mosaic Plot for Categorical Association Analysis rendered with Plotly

Python source (Plotly)

""" anyplot.ai
mosaic-categorical: Mosaic Plot for Categorical Association Analysis
Library: plotly 6.7.0 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-19
"""

import os

import pandas as pd
import plotly.graph_objects as go


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Data: Titanic survival data by passenger class
data = {
    "Class": ["First", "First", "Second", "Second", "Third", "Third"],
    "Survival": ["Survived", "Did Not Survive", "Survived", "Did Not Survive", "Survived", "Did Not Survive"],
    "Count": [203, 122, 118, 167, 178, 528],
}
df = pd.DataFrame(data)

# Pivot to create contingency table
contingency = df.pivot(index="Survival", columns="Class", values="Count")
contingency = contingency[["First", "Second", "Third"]]

# "Did Not Survive" at bottom, "Survived" at top
survival_order = ["Did Not Survive", "Survived"]
contingency = contingency.reindex(survival_order)

# Calculate proportions for mosaic layout
class_totals = contingency.sum(axis=0)
total = class_totals.sum()
class_widths = class_totals / total

survival_categories = contingency.index.tolist()
class_categories = contingency.columns.tolist()

# Okabe-Ito palette: Survived = brand green (first series), Did Not Survive = vermillion
colors = {"Survived": "#009E73", "Did Not Survive": "#C475FD"}

# Gap between rectangles
gap = 0.015

# Build shapes, annotations, and hover trace data
shapes = []
annotations = []
hover_xs, hover_ys, hover_texts, hover_colors = [], [], [], []

x_start = 0
for class_name in class_categories:
    width = class_widths[class_name] - gap
    class_total = contingency[class_name].sum()

    y_start = 0
    for survival in survival_categories:
        count = contingency.loc[survival, class_name]
        height = count / class_total
        pct = count / class_total * 100

        # Rectangle shape
        shapes.append(
            {
                "type": "rect",
                "x0": x_start,
                "y0": y_start,
                "x1": x_start + width,
                "y1": y_start + height,
                "fillcolor": colors[survival],
                "line": {"color": PAGE_BG, "width": 2},
                "layer": "below",
            }
        )

        # Count annotation inside rectangle
        if height > 0.08:
            annotations.append(
                {
                    "x": x_start + width / 2,
                    "y": y_start + height / 2,
                    "text": f"<b>{count}</b>",
                    "showarrow": False,
                    "font": {"size": 20, "color": "white"},
                    "xanchor": "center",
                    "yanchor": "middle",
                }
            )

        # Collect hover point data (per cell)
        hover_xs.append(x_start + width / 2)
        hover_ys.append(y_start + height / 2)
        hover_texts.append(
            f"<b>{class_name} Class — {survival}</b><br>"
            f"Count: {count}<br>"
            f"Within-class proportion: {pct:.1f}%<br>"
            f"Share of all passengers: {count / total * 100:.1f}%"
        )
        hover_colors.append(colors[survival])

        y_start += height

    # Class label below column
    annotations.append(
        {
            "x": x_start + width / 2,
            "y": -0.08,
            "text": f"<b>{class_name}</b>",
            "showarrow": False,
            "font": {"size": 22, "color": INK},
            "xanchor": "center",
            "yanchor": "top",
        }
    )

    x_start += width + gap

# Create figure
fig = go.Figure()

# Legend traces (invisible markers shown only in legend)
for survival_name in ["Survived", "Did Not Survive"]:
    fig.add_trace(
        go.Scatter(
            x=[None],
            y=[None],
            mode="markers",
            marker={"size": 20, "color": colors[survival_name]},
            name=survival_name,
            showlegend=True,
        )
    )

# Per-cell invisible scatter for interactive hover tooltips
fig.add_trace(
    go.Scatter(
        x=hover_xs,
        y=hover_ys,
        mode="markers",
        marker={"size": 1, "color": hover_colors, "opacity": 0},
        hovertemplate="%{text}<extra></extra>",
        text=hover_texts,
        showlegend=False,
    )
)

fig.update_layout(
    title={
        "text": "mosaic-categorical · python · plotly · anyplot.ai",
        "font": {"size": 28, "color": INK},
        "x": 0.5,
        "xanchor": "center",
    },
    shapes=shapes,
    annotations=annotations,
    xaxis={
        "title": {"text": "Passenger Class", "font": {"size": 22, "color": INK}},
        "tickfont": {"size": 18, "color": INK_SOFT},
        "showgrid": False,
        "zeroline": False,
        "showticklabels": False,
        "range": [-0.15, 1.02],
        "linecolor": INK_SOFT,
    },
    yaxis={
        "title": {"text": "Proportion", "font": {"size": 22, "color": INK}},
        "tickfont": {"size": 18, "color": INK_SOFT},
        "showgrid": False,
        "zeroline": False,
        "range": [-0.12, 1.02],
        "linecolor": INK_SOFT,
    },
    legend={
        "font": {"size": 18, "color": INK_SOFT},
        "x": 1.01,
        "y": 0.75,
        "xanchor": "left",
        "yanchor": "middle",
        "bgcolor": ELEVATED_BG,
        "bordercolor": INK_SOFT,
        "borderwidth": 1,
    },
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK},
    margin={"l": 120, "r": 160, "t": 100, "b": 100},
)

# Save
fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Part of Mosaic Plot for Categorical Association Analysis on anyplot.ai.

Other implementations