{"spec_id":"swarm-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nswarm-basic: Basic Swarm Plot\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 88/100 | Updated: 2026-07-26\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\")\n\n# Data - employee performance scores by department (clamped to a plausible 0-100 scale)\nnp.random.seed(42)\ncategories = [\"Engineering\", \"Marketing\", \"Sales\", \"Operations\"]\ndata = {\n    \"Engineering\": np.clip(np.random.normal(82, 7, 45), 0, 100),\n    \"Marketing\": np.clip(np.random.normal(75, 9, 50), 0, 100),\n    \"Sales\": np.clip(np.random.normal(78, 10, 40), 0, 100),\n    \"Operations\": np.clip(np.random.normal(70, 8, 55), 0, 100),\n}\n\nall_values = np.concatenate(list(data.values()))\nY_MIN = 10 * np.floor(all_values.min() / 10)\nY_MAX = 10 * np.ceil(all_values.max() / 10)\n\n# Style - source-pixel sizes for a 3200x1800 canvas (see prompts/library/pygal.md)\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE + (INK,),  # last color reserved for the Group Mean marker\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    opacity=0.75,\n    opacity_hover=1.0,\n    stroke_width=2.5,\n)\n\n# Plot\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"swarm-basic · python · pygal · anyplot.ai\",\n    x_title=\"Department\",\n    y_title=\"Performance Score\",\n    show_legend=True,\n    legend_at_bottom=True,\n    stroke=False,\n    dots_size=10,\n    show_x_guides=False,\n    show_y_guides=True,\n    xrange=(0, 5),\n    range=(Y_MIN, Y_MAX),\n    margin=40,\n    margin_right=20,\n)\n\n# Beeswarm algorithm - spreads points horizontally to avoid overlap.\n# Collision thresholds are derived per-axis from the actual rendered dot\n# footprint (dots_size in px) against each axis's own data-unit-per-pixel\n# scale, so a 10px dot compares correctly whether it's 0.03 x-units wide\n# (category axis spans 5 units over ~2900 plot px) or ~0.6 y-units tall\n# (value axis spans Y_MAX-Y_MIN over ~1270 plot px) - not one flat number\n# for both axes.\nPLOT_WIDTH_PX = 2900\nPLOT_HEIGHT_PX = 1270\nDOT_RADIUS_PX = 10\nSPACING_PX = 4\n\nx_unit_per_px = 5 / PLOT_WIDTH_PX\ny_unit_per_px = (Y_MAX - Y_MIN) / PLOT_HEIGHT_PX\nmin_dist_x = 2 * DOT_RADIUS_PX * x_unit_per_px + SPACING_PX * x_unit_per_px\nmin_dist_y = 2 * DOT_RADIUS_PX * y_unit_per_px + SPACING_PX * y_unit_per_px\nstep_x = DOT_RADIUS_PX * x_unit_per_px + SPACING_PX * x_unit_per_px / 2\n\nfor cat_idx, (category, values) in enumerate(data.items()):\n    center_x = cat_idx + 1\n\n    sorted_indices = np.argsort(values)\n    placed = []\n    swarm_points = []\n\n    for idx in sorted_indices:\n        y = float(values[idx])\n        x = center_x\n        offset = 0\n        direction = 1\n\n        while True:\n            test_x = center_x + offset * direction\n            overlap = False\n            for px, py in placed:\n                dist_y = abs(y - py)\n                dist_x = abs(test_x - px)\n                if dist_y < min_dist_y and dist_x < min_dist_x:\n                    overlap = True\n                    break\n            if not overlap:\n                x = test_x\n                break\n            if direction == 1:\n                direction = -1\n            else:\n                direction = 1\n                offset += step_x\n\n        placed.append((x, y))\n        swarm_points.append((x, y))\n\n    chart.add(category, swarm_points)\n\n# Group mean markers - subtle reference points per category (neutral anchor color)\nmean_points = [(cat_idx + 1, float(np.mean(values))) for cat_idx, (_, values) in enumerate(data.items())]\nchart.add(\"Group Mean\", mean_points, dots_size=20)\n\n# x-axis category labels\nchart.x_labels = [\"\", \"Engineering\", \"Marketing\", \"Sales\", \"Operations\", \"\"]\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}