{"spec_id":"scatter-connected-temporal","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nscatter-connected-temporal: Connected Scatter Plot with Temporal Path\nLibrary: pygal 3.1.3 | Python 3.13.15\nQuality: 78/100 | Updated: 2026-10-08\n\"\"\"\n\nimport os\nimport re\nimport sys\n\n\n# Remove script's own directory from sys.path so the real pygal package is found first\n_here = os.path.dirname(os.path.abspath(__file__))\nif _here in sys.path:\n    sys.path.remove(_here)\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\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# Imprint palette — amber semantic anchor for key events\nANYPLOT_AMBER = \"#DDCC77\"\n\n# Data — fertility rate vs urbanization for an emerging economy (1980–2023)\nnp.random.seed(42)\nyears = list(range(1980, 2024))\nn_years = len(years)\n\nurban_step = np.random.normal(0.95, 0.3, n_years)\nurban_step[20:24] = np.random.normal(-0.1, 0.2, 4)  # 2000–2003 stalled migration\nurbanization = 28 + np.cumsum(urban_step)\n\nfert_step = np.random.normal(-0.075, 0.03, n_years)\nfert_step[35:38] += 0.17  # 2015–2017 pronatalist rebound\nfertility = np.clip(5.6 + np.cumsum(fert_step), 1.2, 7.0)\n\n\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    return \"#{:02X}{:02X}{:02X}\".format(*(round(a + (b - a) * t) for a, b in ((r0, r1), (g0, g1), (b0, b1))))\n\n\n# Imprint imprint_seq gradient (#009E73 -> #4467A3) for temporal progression\nera_bounds = [(0, 10), (10, 20), (20, 30), (30, 37), (37, 43)]\nera_colors = [_lerp_hex(\"#009E73\", \"#4467A3\", i / (len(era_bounds) - 1)) for i in range(len(era_bounds))]\neras = [(f\"{years[s]}–{years[e]}\", s, e, c) for (s, e), c in zip(era_bounds, era_colors, strict=True)]\n\nannotate_years = {1990, 2000, 2010, 2020}\n\n# Title scaled for 81-char length: round(66 × 67/81) = 55\ntitle = \"Fertility vs Urbanization · scatter-connected-temporal · python · pygal · anyplot.ai\"\ntitle_font_size = max(44, round(66 * 67 / len(title)))\n\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\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    guide_stroke_color=INK_MUTED,\n    guide_stroke_dasharray=\"3,5\",\n    colors=(*era_colors, ANYPLOT_AMBER, \"#AE3030\", \"#4467A3\"),\n    font_family=font,\n    title_font_family=font,\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    value_label_font_size=44,\n    tooltip_font_size=36,\n    tooltip_font_family=font,\n    opacity=0.92,\n    opacity_hover=1.0,\n    stroke_width=5,\n    stroke_opacity=0.9,\n    stroke_opacity_hover=1.0,\n)\n\nx_min = float(np.floor(urbanization.min() / 5) * 5)\nx_max = float(np.ceil(urbanization.max() / 5) * 5)\ny_min = float(np.floor(fertility.min() * 2) / 2)\ny_max = float(np.ceil(fertility.max() * 2) / 2)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Urban Population (% of total)\",\n    y_title=\"Fertility Rate (births per woman)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    legend_box_size=28,\n    stroke=True,\n    dots_size=12,\n    show_x_guides=True,\n    show_y_guides=True,\n    x_value_formatter=lambda x: f\"{x:.0f}%\",\n    value_formatter=lambda y: f\"{y:.1f}\",\n    print_labels=True,\n    print_values=False,\n    margin_bottom=130,\n    margin_left=80,\n    margin_right=140,\n    margin_top=90,\n    range=(y_min, y_max),\n    xrange=(x_min, x_max),\n    x_labels_major_count=8,\n    y_labels_major_count=8,\n    js=[],\n    show_x_labels=True,\n    show_y_labels=True,\n)\n\n# Temporal path as Imprint imprint_seq gradient era segments\nfor era_name, start, end, color in eras:\n    end_idx = min(end + 1, n_years)\n    segment_points = [\n        {\"value\": (float(urbanization[i]), float(fertility[i])), \"color\": color} for i in range(start, end_idx)\n    ]\n    chart.add(\n        era_name,\n        segment_points,\n        stroke=True,\n        show_dots=True,\n        dots_size=12,\n        stroke_style={\"width\": 5, \"linecap\": \"round\", \"linejoin\": \"round\"},\n    )\n\n# Key years highlighted with amber dots and year labels\nannotated_points = []\nfor yr in sorted(annotate_years):\n    i = yr - years[0]\n    annotated_points.append(\n        {\"value\": (float(urbanization[i]), float(fertility[i])), \"label\": str(yr), \"color\": ANYPLOT_AMBER}\n    )\nchart.add(\"Key years\", annotated_points, stroke=False, dots_size=20)\n\n# Start and end markers\nchart.add(\n    f\"Start ({years[0]})\",\n    [{\"value\": (float(urbanization[0]), float(fertility[0])), \"label\": \"▶ 1980\", \"color\": \"#AE3030\"}],\n    stroke=False,\n    dots_size=26,\n)\nchart.add(\n    f\"End ({years[-1]})\",\n    [{\"value\": (float(urbanization[-1]), float(fertility[-1])), \"label\": \"● 2023\", \"color\": \"#4467A3\"}],\n    stroke=False,\n    dots_size=26,\n)\n\n# Patch label text colors for dark-theme legibility before PNG conversion\n# pygal's print_labels text color does not adapt to the dark background via the foreground Style token\n_label_texts = {str(yr) for yr in sorted(annotate_years)} | {\"▶ 1980\", \"● 2023\"}\n\n\ndef _patch_label_colors(svg_str, labels, fill_color):\n    def _fix(m):\n        tag_attrs, content = m.group(1), m.group(2)\n        if content.strip() not in labels:\n            return m.group(0)\n        # inline style outranks pygal's `.label` CSS class fill\n        return f'<text{tag_attrs} style=\"fill:{fill_color}\">{content}</text>'\n\n    return re.sub(r\"<text([^>]*)>(.*?)</text>\", _fix, svg_str, flags=re.DOTALL)\n\n\nsvg_data = chart.render()\nsvg_str = _patch_label_colors(svg_data.decode(\"utf-8\"), _label_texts, INK)\ncairosvg.svg2png(bytestring=svg_str.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\nchart.render_to_file(f\"plot-{THEME}.html\")\n"}