{"spec_id":"ridgeline-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nridgeline-basic: Basic Ridgeline Plot\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-07-25\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\n\n\n# Pop script directory so local pygal.py doesn't shadow the installed package\n_script_dir = sys.path.pop(0)\nimport pygal\nfrom pygal.style import Style\n\n\nsys.path.insert(0, _script_dir)\n\n# Theme tokens\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\n# Data - Monthly temperature distributions for a temperate city\nnp.random.seed(42)\n\nmonths = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n# Seasonal temperature baselines (°C)\nbase_temps = [2, 4, 8, 13, 18, 22, 25, 24, 19, 13, 7, 3]\nmonth_data = []\nfor base in base_temps:\n    temps = np.random.normal(base, 3, 100)\n    month_data.append(temps)\n\n# Common x range for all distributions\nx_range = np.linspace(-10, 36, 150)\n\n# Compute KDE for each month (inline, no functions)\nkde_data = []\nfor temps in month_data:\n    n = len(temps)\n    bandwidth = n ** (-1 / 5) * np.std(temps)\n    density = np.zeros_like(x_range)\n    for xi in temps:\n        density += np.exp(-0.5 * ((x_range - xi) / bandwidth) ** 2)\n    density /= n * bandwidth * np.sqrt(2 * np.pi)\n    kde_data.append(density)\n\n# Normalize all densities\nmax_density = max(d.max() for d in kde_data)\nkde_data = [d / max_density for d in kde_data]\n\n\n# Imprint sequential colormap (single-polarity continuous data): brand green\n# -> blue. Each ridge's mean temperature drives its position on the\n# gradient, so color encodes the underlying continuous variable directly.\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    r, g, b = (int(round(a + (b - a) * t)) for a, b in ((r0, r1), (g0, g1), (b0, b1)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\ntemp_min, temp_max = min(base_temps), max(base_temps)\ncolors = tuple(_lerp_hex(\"#009E73\", \"#4467A3\", (t - temp_min) / (temp_max - temp_min)) for t in base_temps)\n\n# Title fontsize scales linearly off the 67-char baseline (see plot-generator.md)\ntitle = \"Monthly Temperature Distributions · ridgeline-basic · python · pygal · anyplot.ai\"\ntitle_font_size = max(44, round(66 * min(1.0, 67 / len(title))))\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=colors,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    # Fully opaque fills (painter's algorithm, back-to-front) instead of a\n    # translucent stack — semi-transparent overlaps compound at each ridge's\n    # flat baseline where they cross a neighbor's fill, producing a visible\n    # seam line; opaque fills simply occlude what's behind with no blending.\n    opacity=1,\n    opacity_hover=1,\n    # A pure `stroke-width: 0` CSS override is dropped by pygal's\n    # get_strokes() (falsy check), and even a near-zero width still\n    # rasterizes as a visible hairline in cairosvg — stroke_opacity=0 kills\n    # the outline unambiguously so each ridge's flat polygon-bottom edge\n    # doesn't read as a stray horizontal line.\n    stroke_opacity=0,\n)\n\n# Ridge parameters — height/spacing ratio tuned for ~60% vertical overlap\nridge_height = 3.0\nridge_spacing = 1.2\n\n# Y-axis labels positioned just above each ridge's own baseline (offset is a\n# small fraction of ridge_spacing, not ridge_height, so the label hugs its\n# own baseline instead of drifting toward the ridge above)\ny_label_values = []\nfor i, month in enumerate(reversed(months)):\n    y_label_values.append((i * ridge_spacing + ridge_spacing * 0.15, month))\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Temperature (°C)\",\n    y_title=\"\",\n    show_legend=False,\n    stroke=True,\n    fill=True,\n    dots_size=0,\n    show_x_guides=False,\n    show_y_guides=False,\n    range=(-0.5, len(months) * ridge_spacing + ridge_height * 1.15),\n    xrange=(-10, 36),\n    margin_bottom=40,\n)\n\nchart.y_labels = [{\"value\": v, \"label\": lbl} for v, lbl in y_label_values]\n\n# Add ridges from back (Dec) to front (Jan) for correct visual layering;\n# month names as series labels appear in HTML hover tooltips\nfor i, (month, density) in enumerate(reversed(list(zip(months, kde_data, strict=True)))):\n    baseline = i * ridge_spacing\n    scaled_density = density * ridge_height\n\n    bottom_edge = [(float(x), float(baseline)) for x in x_range]\n    top_edge = [(float(x), float(baseline + d)) for x, d in zip(x_range[::-1], scaled_density[::-1], strict=True)]\n    polygon = bottom_edge + top_edge + [bottom_edge[0]]\n\n    chart.add(month, polygon)\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}