{"spec_id":"gauge-activity-rings","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\ngauge-activity-rings: Activity Rings Progress Chart\nLibrary: plotly 6.8.0 | Python 3.13.13\nQuality: 90/100 | Created: 2026-06-14\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens (Imprint palette — see prompts/default-style-guide.md)\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 categorical palette — first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — daily fitness summary (outer ring = primary metric)\nmetrics = [\n    {\"name\": \"Move\", \"value\": 420, \"goal\": 600, \"unit\": \"kcal\"},\n    {\"name\": \"Exercise\", \"value\": 25, \"goal\": 30, \"unit\": \"min\"},\n    {\"name\": \"Stand\", \"value\": 9, \"goal\": 12, \"unit\": \"hr\"},\n]\nring_colors = IMPRINT_PALETTE[:3]  # green, lavender, blue (outer → inner)\n\n# Ring geometry — outer to inner\nradii = [0.83, 0.58, 0.33]\nline_width = 30  # px on the 600×600 logical canvas (→ 120 px in 2400×2400 output)\nTRACK_ALPHA = 0.14 if THEME == \"light\" else 0.22  # higher in dark mode so track reads on near-black bg\n\n\ndef arc_xy(radius, fraction):\n    fraction = min(fraction, 1.0)\n    n = max(400, int(600 / radius))  # more points for inner (smaller) rings to stay smooth\n    angles = np.linspace(90, 90 - fraction * 360, n)\n    return (radius * np.cos(np.radians(angles))).tolist(), (radius * np.sin(np.radians(angles))).tolist()\n\n\ndef circle_xy(radius):\n    n = max(400, int(600 / radius))\n    angles = np.linspace(0, 360, n + 1)\n    return (radius * np.cos(np.radians(angles))).tolist(), (radius * np.sin(np.radians(angles))).tolist()\n\n\ndef hex_to_rgba(h, alpha):\n    r, g, b = int(h[1:3], 16), int(h[3:5], 16), int(h[5:7], 16)\n    return f\"rgba({r},{g},{b},{alpha})\"\n\n\n# Title — 51 chars, below 67-char baseline → no shrink needed\ntitle = \"gauge-activity-rings · python · plotly · anyplot.ai\"\nn_chars = len(title)\ntitle_size = max(11, round(16 * (67 / n_chars if n_chars > 67 else 1.0)))\n\n# Figure\nfig = go.Figure()\n\nfor metric, radius, color in zip(metrics, radii, ring_colors, strict=False):\n    fraction = metric[\"value\"] / metric[\"goal\"]\n    track_color = hex_to_rgba(color, TRACK_ALPHA)\n\n    # Background track (full circle, faint)\n    cx, cy = circle_xy(radius)\n    fig.add_trace(\n        go.Scatter(\n            x=cx,\n            y=cy,\n            mode=\"lines\",\n            line={\"color\": track_color, \"width\": line_width},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n    # Progress arc (clockwise from 12 o'clock)\n    ax, ay = arc_xy(radius, fraction)\n    pct = fraction * 100\n    legend_label = f\"{metric['name']}  {metric['value']}/{metric['goal']} {metric['unit']}\"\n    fig.add_trace(\n        go.Scatter(\n            x=ax,\n            y=ay,\n            mode=\"lines\",\n            line={\"color\": color, \"width\": line_width},\n            name=legend_label,\n            hovertemplate=(\n                f\"<b>{metric['name']}</b><br>\"\n                f\"{metric['value']} / {metric['goal']} {metric['unit']}<br>\"\n                f\"{pct:.0f}% complete<extra></extra>\"\n            ),\n        )\n    )\n\n    # Rounded end caps — markers matching line width at arc endpoints\n    s_x = radius * np.cos(np.radians(90))\n    s_y = radius * np.sin(np.radians(90))\n    end_angle = 90 - fraction * 360\n    e_x = radius * np.cos(np.radians(end_angle))\n    e_y = radius * np.sin(np.radians(end_angle))\n\n    fig.add_trace(\n        go.Scatter(\n            x=[s_x, e_x],\n            y=[s_y, e_y],\n            mode=\"markers\",\n            marker={\"color\": color, \"size\": line_width, \"line\": {\"width\": 0}},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Center annotation — average completion across all three rings\navg_pct = np.mean([m[\"value\"] / m[\"goal\"] for m in metrics]) * 100\nfig.add_annotation(\n    x=0, y=0.09, text=f\"<b>{avg_pct:.0f}%</b>\", font={\"size\": 28, \"color\": INK}, showarrow=False, xanchor=\"center\"\n)\nfig.add_annotation(\n    x=0, y=-0.10, text=\"avg. complete\", font={\"size\": 11, \"color\": INK_MUTED}, showarrow=False, xanchor=\"center\"\n)\n\n# Layout — square canvas (2400×2400 via width=600 height=600 scale=4)\nfig.update_layout(\n    title={\n        \"text\": title,\n        \"font\": {\"size\": title_size, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.98,\n        \"yanchor\": \"top\",\n    },\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    showlegend=True,\n    legend={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"color\": INK_SOFT, \"size\": 11},\n        \"orientation\": \"h\",\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": -0.04,\n        \"yanchor\": \"top\",\n        \"traceorder\": \"normal\",\n        \"itemsizing\": \"constant\",\n    },\n    margin={\"l\": 75, \"r\": 75, \"t\": 70, \"b\": 95},\n    xaxis={\n        \"range\": [-1.08, 1.08],\n        \"showgrid\": False,\n        \"showticklabels\": False,\n        \"zeroline\": False,\n        \"showline\": False,\n        \"scaleanchor\": \"y\",\n        \"scaleratio\": 1,\n    },\n    yaxis={\"range\": [-1.08, 1.08], \"showgrid\": False, \"showticklabels\": False, \"zeroline\": False, \"showline\": False},\n)\n\n# Save — square: width=600, height=600, scale=4 → 2400×2400 px\nfig.write_image(f\"plot-{THEME}.png\", width=600, height=600, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}