{"spec_id":"gauge-activity-rings","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ngauge-activity-rings: Activity Rings Progress Chart\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 86/100 | Created: 2026-06-14\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\n\n# Drop script directory from sys.path so `altair` package resolves, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\nnp = importlib.import_module(\"numpy\")\npd = importlib.import_module(\"pandas\")\nImage = importlib.import_module(\"PIL.Image\")\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# Background track opacity — bumped for dark theme where it would otherwise vanish\nTRACK_OPACITY = 0.18 if THEME == \"light\" else 0.28\n\n# Imprint palette — positions 1-3 for three activity rings\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — daily fitness tracker goals\nmetrics = [\"Move\", \"Exercise\", \"Stand\"]\nvalues = [420, 25, 9]\ngoals = [600, 30, 12]\nunits = [\"kcal\", \"min\", \"hr\"]\ncolors = IMPRINT_PALETTE[:3]\n\nfractions = [v / g for v, g in zip(values, goals, strict=False)]\n# Ring geometry: outer ring = Move (primary), inner = Stand\nouter_radii = [185, 130, 75]\ninner_radii = [150, 95, 40]\n\nTWO_PI = 2 * np.pi\nVIEW_CX = 250  # view center x (width / 2)\nVIEW_CY = 230  # view center y (height / 2)\n\n# Title with adaptive fontsize\ntitle = \"gauge-activity-rings · python · altair · anyplot.ai\"\nn = len(title)\ntitle_size = max(11, round(16 * (67 / n if n > 67 else 1.0)))\n\n# Index of highest-completion ring for visual storytelling emphasis (DE-03)\nbest_idx = fractions.index(max(fractions))\n\n# Build arc layers inline — no helper function (KISS / CQ-01)\nlayers = []\nfor metric, val, goal, unit, color, r_out, r_in, frac in zip(\n    metrics, values, goals, units, colors, outer_radii, inner_radii, fractions, strict=False\n):\n    tip = f\"{val} / {goal} {unit}  ({round(frac * 100)}%)\"\n    full_df = pd.DataFrame([{\"s\": 0.0, \"e\": TWO_PI}])\n    arc_df = pd.DataFrame([{\"s\": 0.0, \"e\": min(frac, 1.0) * TWO_PI, \"metric\": metric, \"tip\": tip}])\n    track = (\n        alt.Chart(full_df)\n        .mark_arc(innerRadius=r_in, outerRadius=r_out, cornerRadius=22)\n        .encode(\n            theta=alt.Theta(\"s:Q\", scale=None), theta2=\"e:Q\", color=alt.value(color), opacity=alt.value(TRACK_OPACITY)\n        )\n    )\n    arc = (\n        alt.Chart(arc_df)\n        .mark_arc(innerRadius=r_in, outerRadius=r_out, cornerRadius=22)\n        .encode(\n            theta=alt.Theta(\"s:Q\", scale=None),\n            theta2=\"e:Q\",\n            color=alt.value(color),\n            opacity=alt.value(1.0),\n            tooltip=[alt.Tooltip(\"metric:N\", title=\"Activity\"), alt.Tooltip(\"tip:N\", title=\"Progress\")],\n        )\n    )\n    layers.append(track + arc)\n\n# Center labels: stacked metric lines inside the inner hole\n# Best-performing ring gets a ★ prefix and slightly larger font for storytelling (DE-03)\ncenter_y_positions = [VIEW_CY - 20, VIEW_CY, VIEW_CY + 20]\ncenter_labels = []\nfor i, (metric, frac, color, cy) in enumerate(zip(metrics, fractions, colors, center_y_positions, strict=False)):\n    is_best = i == best_idx\n    label_text = f\"{'★ ' if is_best else ''}{metric}  {round(frac * 100)}%\"\n    label_size = 14 if is_best else 12\n    lbl_df = pd.DataFrame([{\"txt\": label_text}])\n    center_labels.append(\n        alt.Chart(lbl_df)\n        .mark_text(fontSize=label_size, fontWeight=\"bold\", align=\"center\", baseline=\"middle\")\n        .encode(x=alt.value(VIEW_CX), y=alt.value(cy), text=\"txt:N\", color=alt.value(color))\n    )\n\nall_layers = layers + center_labels\nchart = (\n    alt.layer(*all_layers)\n    .properties(\n        width=500,\n        height=460,\n        background=PAGE_BG,\n        title=alt.TitleParams(text=title, fontSize=title_size, color=INK, anchor=\"middle\"),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_title(color=INK, fontSize=title_size)\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact square target (2400×2400)\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 {TW}×{TH}. Shrink .properties(width=, height=) 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\n# Save interactive HTML\nchart.save(f\"plot-{THEME}.html\")\n"}