{"spec_id":"parallel-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nparallel-basic: Basic Parallel Coordinates Plot\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 82/100 | Updated: 2026-07-24\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path so local pygal.py doesn't shadow the installed package\nsys.path.pop(0)\n\nimport pygal\nfrom pygal.style import Style\n\n\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\"\nRULE = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint palette (canonical order) — Setosa=brand green, Versicolor=lavender, Virginica=blue\nSPECIES_COLORS = {\"Setosa\": \"#009E73\", \"Versicolor\": \"#C475FD\", \"Virginica\": \"#4467A3\"}\n\n# Data - Iris dataset, 15 samples per species across 4 dimensions\niris_data = {\n    \"Setosa\": [\n        [5.1, 3.5, 1.4, 0.2],\n        [4.9, 3.0, 1.4, 0.2],\n        [4.7, 3.2, 1.3, 0.2],\n        [4.6, 3.1, 1.5, 0.2],\n        [5.0, 3.6, 1.4, 0.2],\n        [5.4, 3.9, 1.7, 0.4],\n        [4.6, 3.4, 1.4, 0.3],\n        [5.0, 3.4, 1.5, 0.2],\n        [4.4, 2.9, 1.4, 0.2],\n        [4.9, 3.1, 1.5, 0.1],\n        [5.4, 3.7, 1.5, 0.2],\n        [4.8, 3.4, 1.6, 0.2],\n        [4.8, 3.0, 1.4, 0.1],\n        [4.3, 3.0, 1.1, 0.1],\n        [5.8, 4.0, 1.2, 0.2],\n    ],\n    \"Versicolor\": [\n        [7.0, 3.2, 4.7, 1.4],\n        [6.4, 3.2, 4.5, 1.5],\n        [6.9, 3.1, 4.9, 1.5],\n        [5.5, 2.3, 4.0, 1.3],\n        [6.5, 2.8, 4.6, 1.5],\n        [5.7, 2.8, 4.5, 1.3],\n        [6.3, 3.3, 4.7, 1.6],\n        [4.9, 2.4, 3.3, 1.0],\n        [6.6, 2.9, 4.6, 1.3],\n        [5.2, 2.7, 3.9, 1.4],\n        [5.0, 2.0, 3.5, 1.0],\n        [5.9, 3.0, 4.2, 1.5],\n        [6.0, 2.2, 4.0, 1.0],\n        [6.1, 2.9, 4.7, 1.4],\n        [5.6, 2.9, 3.6, 1.3],\n    ],\n    \"Virginica\": [\n        [6.3, 3.3, 6.0, 2.5],\n        [5.8, 2.7, 5.1, 1.9],\n        [7.1, 3.0, 5.9, 2.1],\n        [6.3, 2.9, 5.6, 1.8],\n        [6.5, 3.0, 5.8, 2.2],\n        [7.6, 3.0, 6.6, 2.1],\n        [4.9, 2.5, 4.5, 1.7],\n        [7.3, 2.9, 6.3, 1.8],\n        [6.7, 2.5, 5.8, 1.8],\n        [7.2, 3.6, 6.1, 2.5],\n        [6.5, 3.2, 5.1, 2.0],\n        [6.4, 2.7, 5.3, 1.9],\n        [6.8, 3.0, 5.5, 2.1],\n        [5.7, 2.5, 5.0, 2.0],\n        [5.8, 2.8, 5.1, 2.4],\n    ],\n}\n\nspecies_list = list(iris_data.keys())\ndimension_labels = [\"Sepal Length (cm)\", \"Sepal Width (cm)\", \"Petal Length (cm)\", \"Petal Width (cm)\"]\n\n# Per-dimension min/max for normalization\nall_values = [[row[i] for species in iris_data.values() for row in species] for i in range(4)]\nmins = [min(col) for col in all_values]\nmaxs = [max(col) for col in all_values]\n\n# Pygal cycles through `colors` sequentially per series added.\n# Order: 3 mean lines, then 3 combined per-species individual-observation lines\n# (one interrupted series per species, not one per observation).\ncolor_list = [SPECIES_COLORS[s] for s in species_list] * 2\n\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=tuple(color_list),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=48,\n    legend_font_size=44,\n    value_font_size=36,\n    opacity=0.50,\n    opacity_hover=1.0,\n    stroke_width=2.5,\n    guide_stroke_color=RULE,\n    major_guide_stroke_color=RULE,\n)\n\n# Plot — pygal.XY (not Line) so each species' 15 observations can share one series via\n# allow_interruptions: pygal reserves legend-at-bottom margin proportional to the total\n# series count (ceil(sqrt(N)) rows), so 45 separate untitled per-observation Line series\n# blew up the reservation to ~10 legend rows though only 1 ever renders. Collapsing the\n# observations into one interrupted XY series per species drops the series count from 48\n# to 6, and legend_at_bottom_columns=3 (matching the 3 titled series) keeps the margin\n# formula's row estimate at the true value of 1.\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"parallel-basic · python · pygal · anyplot.ai\",\n    x_title=\"Dimensions\",\n    y_title=\"Normalized Value (0–1)\",\n    show_dots=False,\n    show_y_guides=True,\n    show_x_guides=True,  # faint vertical lines at each dimension — the \"parallel axes\" identity\n    x_label_rotation=30,  # start-anchors each label at its tick instead of centering it on top;\n    # steeper than before so the leftmost label clears the y-axis \"0\" tick label sooner\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    legend_box_size=40,\n    truncate_legend=-1,\n    range=(0, 1),\n    xrange=(0, 3),\n    margin=100,\n    spacing=34,\n    margin_right=260,\n    show_legend=True,\n)\n\nchart.x_labels = [{\"value\": i, \"label\": label} for i, label in enumerate(dimension_labels)]\n\n# Mean lines per species — thicker stroke, appear in legend, tooltip shows the actual mean measurement\nfor species_name in species_list:\n    rows = iris_data[species_name]\n    mean_row = [sum(row[i] for row in rows) / len(rows) for i in range(4)]\n    normalized_mean = [(mean_row[i] - mins[i]) / (maxs[i] - mins[i]) for i in range(4)]\n    mean_points = [\n        {\"value\": (i, normalized_mean[i]), \"tooltip\": f\"{species_name} mean · {dimension_labels[i]}: {mean_row[i]:.2f}\"}\n        for i in range(4)\n    ]\n    chart.add(species_name, mean_points, stroke_style={\"width\": 7})\n\n# Individual observation lines — thinner, no legend entry, tooltip shows the actual measurement.\n# All 15 observations per species share one series, separated by None to break the line\n# between observations (allow_interruptions), instead of one series per observation.\nfor species_name in species_list:\n    combined_points = []\n    rows = iris_data[species_name]\n    for row_index, row in enumerate(rows):\n        normalized = [(row[i] - mins[i]) / (maxs[i] - mins[i]) for i in range(4)]\n        combined_points.extend(\n            {\"value\": (i, normalized[i]), \"tooltip\": f\"{species_name} · {dimension_labels[i]}: {row[i]:.1f}\"}\n            for i in range(4)\n        )\n        if row_index != len(rows) - 1:\n            combined_points.append(None)\n    chart.add(None, combined_points, stroke_style={\"width\": 3}, allow_interruptions=True)\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"}