{"spec_id":"gauge-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ngauge-basic: Basic Gauge Chart\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-30\n\"\"\"\n\nimport math\nimport os\nimport re\nimport sys\nfrom pathlib import Path\n\n\n# Remove script directory from path to avoid name collision with the pygal package\n_script_dir = str(Path(__file__).parent)\nsys.path = [p for p in sys.path if p != _script_dir]\n\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_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — quarterly sales performance against an annual target (Q3 2024)\nvalue = 72\nmin_value = 0\nmax_value = 100\nthresholds = [30, 70]\n\n# Semantic zone colors: bad → warn → good (Imprint traffic-light anchors)\nZONE_COLORS = (\"#AE3030\", \"#DDCC77\", \"#009E73\")\n\nzone_label = \"Poor\" if value < thresholds[0] else \"Fair\" if value < thresholds[1] else \"Good\"\nzone_color = \"#AE3030\" if value < thresholds[0] else \"#DDCC77\" if value < thresholds[1] else \"#009E73\"\n\n# Stable descriptive title (does not embed runtime zone/value strings)\n_title = \"Quarterly Sales Performance · gauge-basic · pygal · anyplot.ai\"\n# Scale font size down only when title exceeds the 67-char reference length\n_title_fs = max(44, round(66 * 67 / max(len(_title), 67)))\n\n# Zone widths for the half-pie arcs (sum = max_value)\n_poor_w = thresholds[0] - min_value  # 30\n_fair_w = thresholds[1] - thresholds[0]  # 40\n_good_w = max_value - thresholds[1]  # 30\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=ZONE_COLORS,\n    title_font_size=_title_fs,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\nchart = pygal.Pie(\n    width=3200,\n    height=1800,\n    title=_title,\n    style=custom_style,\n    half_pie=True,\n    inner_radius=0.60,\n    show_legend=True,\n    legend_at_bottom=True,\n    print_values=False,\n    margin=60,\n    # Rich tooltip labels for the interactive HTML export (LM-02)\n    value_formatter=lambda x: f\"Span: {int(x)} pts of {max_value}\",\n)\n\nchart.add(f\"Poor  (0–{thresholds[0]})\", _poor_w)\nchart.add(f\"Fair  ({thresholds[0]}–{thresholds[1]})\", _fair_w)\nchart.add(f\"Good  ({thresholds[1]}–{max_value})\", _good_w)\n\n# Render to SVG, then inject needle + prominent value label (fixes SC-01, SC-02)\nsvg_str = chart.render().decode(\"utf-8\")\n\n# Extract the half-pie center (CX, CY) and outer radius from the SVG path data.\n# pygal pie slices start from the center: d=\"M cx cy L ...\"\n# The arc command carries the radius: A rx ry ...\n_path_match = re.search(r'd=\"M\\s+([\\d.]+)\\s+([\\d.]+)\\s+L[\\s\\d.,\\-]+A\\s+([\\d.]+)\\s+\\3', svg_str)\nif _path_match:\n    CX = float(_path_match.group(1))\n    CY = float(_path_match.group(2))\n    R_OUTER = float(_path_match.group(3))\nelse:\n    # Fallback geometry for 3200×1800, margin=60, legend_at_bottom\n    CX, CY, R_OUTER = 1600.0, 1440.0, 1100.0\n\nR_INNER = R_OUTER * 0.60  # matches inner_radius=0.60\n\n# Needle: value 0 → left (180°), value 100 → right (0°)\nangle_rad = math.radians(180.0 - (value / max_value) * 180.0)\ntip_x = CX + R_OUTER * 0.87 * math.cos(angle_rad)\ntip_y = CY - R_OUTER * 0.87 * math.sin(angle_rad)\n\n# Prominent value label centered in the inner donut area\nval_y = CY - R_INNER * 0.52\nlbl_y = val_y + 0.24 * R_INNER\n\noverlay = (\n    f'<g id=\"value-overlay\">'\n    f'<line x1=\"{CX:.1f}\" y1=\"{CY:.1f}\" x2=\"{tip_x:.1f}\" y2=\"{tip_y:.1f}\"'\n    f' stroke=\"{INK}\" stroke-width=\"22\" stroke-linecap=\"round\"/>'\n    f'<circle cx=\"{CX:.1f}\" cy=\"{CY:.1f}\" r=\"{R_OUTER * 0.038:.1f}\" fill=\"{INK}\"/>'\n    f'<text x=\"{CX:.1f}\" y=\"{val_y:.1f}\" text-anchor=\"middle\"'\n    f' dominant-baseline=\"middle\" font-size=\"{R_INNER * 0.40:.0f}\"'\n    f' font-weight=\"bold\" fill=\"{zone_color}\" font-family=\"sans-serif\">{value}</text>'\n    f'<text x=\"{CX:.1f}\" y=\"{lbl_y:.1f}\" text-anchor=\"middle\"'\n    f' dominant-baseline=\"middle\" font-size=\"{R_INNER * 0.15:.0f}\"'\n    f' fill=\"{INK_SOFT}\" font-family=\"sans-serif\">{zone_label}</text>'\n    f\"</g>\"\n)\n\nclose_idx = svg_str.rfind(\"</svg>\")\nmodified_svg = svg_str[:close_idx] + overlay + svg_str[close_idx:]\n\nimport cairosvg\n\n\ncairosvg.svg2png(bytestring=modified_svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n\n# HTML: modified SVG preserves both JS interactivity and the value overlay\nwith open(f\"plot-{THEME}.html\", \"w\", encoding=\"utf-8\") as f:\n    f.write(modified_svg)\n"}