{"spec_id":"gauge-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ngauge-basic: Basic Gauge Chart\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-06-30\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\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# Imprint palette zone colors (semantic: bad → warning → good)\nZONE_BAD = \"#AE3030\"  # Imprint matte red — bad / below threshold\nZONE_WARN = \"#DDCC77\"  # Imprint amber — caution\nZONE_GOOD = \"#009E73\"  # Imprint brand green — target achieved\n\n# Data — Sales performance gauge\nvalue = 72\nmin_value = 0\nmax_value = 100\nthresholds = [30, 70]\n\n# Geometry: semi-circle from left (-π/2) to right (+π/2)\nboundaries = np.array([min_value] + thresholds + [max_value], dtype=float)\nboundary_angles = -np.pi / 2 + (boundaries - min_value) / (max_value - min_value) * np.pi\nneedle_angle = -np.pi / 2 + (value - min_value) / (max_value - min_value) * np.pi\n\n# Zone arcs — tooltip makes the HTML export genuinely informative\nzones_df = pd.DataFrame(\n    {\n        \"startAngle\": boundary_angles[:-1],\n        \"endAngle\": boundary_angles[1:],\n        \"color\": [ZONE_BAD, ZONE_WARN, ZONE_GOOD],\n        \"zone\": [\"Low (0–30)\", \"Medium (30–70)\", \"High (70–100)\"],\n    }\n)\n\ngauge_arcs = (\n    alt.Chart(zones_df)\n    .mark_arc(innerRadius=175, outerRadius=285, cornerRadius=6, stroke=PAGE_BG, strokeWidth=4)\n    .encode(\n        theta=alt.Theta(\"startAngle:Q\", scale=None),\n        theta2=\"endAngle:Q\",\n        color=alt.Color(\"color:N\", scale=None, legend=None),\n        tooltip=[alt.Tooltip(\"zone:N\", title=\"Zone\")],\n    )\n)\n\n# Needle\nneedle_length = 230\nneedle_df = pd.DataFrame(\n    [{\"x\": 0.0, \"y\": 0.0, \"x2\": needle_length * np.sin(needle_angle), \"y2\": needle_length * np.cos(needle_angle)}]\n)\nneedle = (\n    alt.Chart(needle_df)\n    .mark_rule(color=INK, strokeWidth=8, strokeCap=\"round\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=[-340, 340]), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=[-220, 370]), axis=None),\n        x2=\"x2:Q\",\n        y2=\"y2:Q\",\n    )\n)\n\n# Center hub (two-tone for definition)\nhub_df = pd.DataFrame([{\"x\": 0, \"y\": 0}])\nhub_outer = alt.Chart(hub_df).mark_circle(size=1800, color=INK).encode(x=\"x:Q\", y=\"y:Q\")\nhub_inner = alt.Chart(hub_df).mark_circle(size=350, color=PAGE_BG).encode(x=\"x:Q\", y=\"y:Q\")\n\n# Prominent value label — colored to match the zone it falls in\nvalue_label_df = pd.DataFrame([{\"x\": 0, \"y\": -100, \"text\": f\"{value}\"}])\nvalue_label = (\n    alt.Chart(value_label_df)\n    .mark_text(fontSize=60, fontWeight=\"bold\", color=ZONE_GOOD, baseline=\"middle\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Context label\ncontext_label_df = pd.DataFrame([{\"x\": 0, \"y\": -168, \"text\": \"Current Sales\"}])\ncontext_label = (\n    alt.Chart(context_label_df)\n    .mark_text(fontSize=16, color=INK_MUTED, baseline=\"middle\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Min/max range labels at arc ends\nrange_label_radius = 235\nrange_labels_df = pd.DataFrame(\n    [\n        {\"x\": range_label_radius * np.sin(boundary_angles[0]), \"y\": -32, \"text\": str(min_value)},\n        {\"x\": range_label_radius * np.sin(boundary_angles[-1]), \"y\": -32, \"text\": str(max_value)},\n    ]\n)\nrange_labels = (\n    alt.Chart(range_labels_df)\n    .mark_text(fontSize=16, color=INK_SOFT, fontWeight=\"bold\")\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Threshold labels just above the arc boundary edges\nthreshold_angles = -np.pi / 2 + (np.array(thresholds, dtype=float) - min_value) / (max_value - min_value) * np.pi\nthreshold_label_radius = 318\nthreshold_labels_df = pd.DataFrame(\n    {\n        \"x\": threshold_label_radius * np.sin(threshold_angles),\n        \"y\": threshold_label_radius * np.cos(threshold_angles),\n        \"text\": [str(t) for t in thresholds],\n    }\n)\nthreshold_labels = (\n    alt.Chart(threshold_labels_df)\n    .mark_text(fontSize=16, color=INK_SOFT, fontWeight=\"bold\", dy=-8)\n    .encode(x=\"x:Q\", y=\"y:Q\", text=\"text:N\")\n)\n\n# Compose all layers — square canvas suits a semi-circular gauge\nchart = (\n    alt.layer(gauge_arcs, needle, hub_outer, hub_inner, threshold_labels, range_labels, value_label, context_label)\n    .properties(\n        width=500,\n        height=460,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"gauge-basic · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\", color=INK, fontWeight=\"normal\"\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n)\n\n# Save PNG, then pad to exact 2400×2400 target\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 2400, 2400\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}