{"spec_id":"parallel-categories-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nparallel-categories-basic: Basic Parallel Categories Plot\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-13\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom matplotlib.path import Path\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Data: Product purchase flow (Channel -> Category -> Outcome)\nnp.random.seed(42)\n\nn_samples = 500\nchannels = np.random.choice([\"Online\", \"Store\", \"Mobile\"], size=n_samples, p=[0.4, 0.35, 0.25])\ncategories = np.random.choice([\"Electronics\", \"Clothing\", \"Home\", \"Sports\"], size=n_samples, p=[0.3, 0.25, 0.25, 0.2])\noutcomes = np.random.choice([\"Purchased\", \"Returned\", \"Abandoned\"], size=n_samples, p=[0.6, 0.15, 0.25])\n\ndf = pd.DataFrame({\"Channel\": channels, \"Category\": categories, \"Outcome\": outcomes})\n\n# Define dimensions and their categories\ndimensions = [\"Channel\", \"Category\", \"Outcome\"]\ndim_categories = {\n    \"Channel\": [\"Online\", \"Store\", \"Mobile\"],\n    \"Category\": [\"Electronics\", \"Clothing\", \"Home\", \"Sports\"],\n    \"Outcome\": [\"Purchased\", \"Returned\", \"Abandoned\"],\n}\n\n# Okabe-Ito palette for channel colors\ncolors = {\"Online\": \"#009E73\", \"Store\": \"#C475FD\", \"Mobile\": \"#4467A3\"}\n\n# Create figure\nfig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Calculate positions for each dimension\nn_dims = len(dimensions)\nx_positions = np.linspace(0, 1, n_dims)\ndim_width = 0.08\n\n# Calculate category positions within each dimension\ncategory_positions = {}\ncategory_heights = {}\n\nfor dim in dimensions:\n    cats = dim_categories[dim]\n    counts = df[dim].value_counts()\n    total = counts.sum()\n\n    heights = {cat: counts.get(cat, 0) / total for cat in cats}\n\n    y_start = 0.05\n    y_end = 0.95\n    available_height = y_end - y_start\n    gap = 0.02\n    total_gap = gap * (len(cats) - 1)\n    usable_height = available_height - total_gap\n\n    positions = {}\n    current_y = y_start\n    for cat in cats:\n        h = heights[cat] * usable_height\n        positions[cat] = (current_y, current_y + h)\n        current_y += h + gap\n\n    category_positions[dim] = positions\n    category_heights[dim] = heights\n\n# Draw ribbons between consecutive dimensions\nfor i in range(n_dims - 1):\n    dim1 = dimensions[i]\n    dim2 = dimensions[i + 1]\n    x1 = x_positions[i]\n    x2 = x_positions[i + 1]\n\n    flow_counts = df.groupby([dim1, dim2]).size().reset_index(name=\"count\")\n\n    current_y_left = {cat: category_positions[dim1][cat][0] for cat in dim_categories[dim1]}\n    current_y_right = {cat: category_positions[dim2][cat][0] for cat in dim_categories[dim2]}\n\n    total = len(df)\n\n    for _, row in flow_counts.iterrows():\n        cat1 = row[dim1]\n        cat2 = row[dim2]\n        count = row[\"count\"]\n\n        y1_top = current_y_left[cat1] + (count / df[dim1].value_counts()[cat1]) * (\n            category_positions[dim1][cat1][1] - category_positions[dim1][cat1][0]\n        )\n\n        y2_bottom = current_y_right[cat2]\n        y2_top = current_y_right[cat2] + (count / df[dim2].value_counts()[cat2]) * (\n            category_positions[dim2][cat2][1] - category_positions[dim2][cat2][0]\n        )\n\n        y1_bottom = current_y_left[cat1]\n\n        x_ctrl1 = x1 + dim_width + (x2 - x1 - 2 * dim_width) * 0.4\n        x_ctrl2 = x1 + dim_width + (x2 - x1 - 2 * dim_width) * 0.6\n\n        vertices = [\n            (x1 + dim_width, y1_bottom),\n            (x_ctrl1, y1_bottom),\n            (x_ctrl2, y2_bottom),\n            (x2 - dim_width, y2_bottom),\n            (x2 - dim_width, y2_top),\n            (x_ctrl2, y2_top),\n            (x_ctrl1, y1_top),\n            (x1 + dim_width, y1_top),\n            (x1 + dim_width, y1_bottom),\n        ]\n\n        codes = [\n            Path.MOVETO,\n            Path.CURVE4,\n            Path.CURVE4,\n            Path.CURVE4,\n            Path.LINETO,\n            Path.CURVE4,\n            Path.CURVE4,\n            Path.CURVE4,\n            Path.CLOSEPOLY,\n        ]\n\n        path = Path(vertices, codes)\n\n        if i == 0:\n            color = colors[cat1]\n        else:\n            orig_cat = df[df[dim1] == cat1][\"Channel\"].mode()\n            if len(orig_cat) > 0:\n                color = colors.get(orig_cat.iloc[0], INK_SOFT)\n            else:\n                color = INK_SOFT\n\n        patch = mpatches.PathPatch(path, facecolor=color, edgecolor=PAGE_BG, linewidth=0.5, alpha=0.6)\n        ax.add_patch(patch)\n\n        current_y_left[cat1] = y1_top\n        current_y_right[cat2] = y2_top\n\n# Draw category bars\nfor i, dim in enumerate(dimensions):\n    x = x_positions[i]\n    for cat in dim_categories[dim]:\n        y_start, y_end = category_positions[dim][cat]\n\n        rect = mpatches.Rectangle(\n            (x - dim_width, y_start),\n            dim_width * 2,\n            y_end - y_start,\n            facecolor=ELEVATED_BG,\n            edgecolor=INK_SOFT,\n            linewidth=2,\n        )\n        ax.add_patch(rect)\n\n        ax.text(x, (y_start + y_end) / 2, cat, ha=\"center\", va=\"center\", fontsize=14, fontweight=\"bold\", color=INK)\n\n# Add dimension labels\nfor i, dim in enumerate(dimensions):\n    ax.text(x_positions[i], 1.02, dim, ha=\"center\", va=\"bottom\", fontsize=20, fontweight=\"bold\", color=INK)\n\n# Styling\nax.set_xlim(-0.15, 1.15)\nax.set_ylim(-0.05, 1.15)\nax.set_aspect(\"equal\")\nax.axis(\"off\")\n\n# Title\nax.set_title(\"parallel-categories-basic · matplotlib · anyplot.ai\", fontsize=24, fontweight=\"medium\", pad=20, color=INK)\n\n# Legend\nlegend_patches = [mpatches.Patch(color=colors[ch], alpha=0.6, label=ch) for ch in [\"Online\", \"Store\", \"Mobile\"]]\nleg = ax.legend(\n    handles=legend_patches,\n    loc=\"lower right\",\n    fontsize=16,\n    title=\"Channel\",\n    title_fontsize=18,\n    bbox_to_anchor=(1.12, 0.0),\n)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}