{"spec_id":"line-parametric","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-parametric: Parametric Curve Plot\nLibrary: altair 6.2.1 | Python 3.13.14\nQuality: 86/100 | Updated: 2026-06-20\n\"\"\"\n\nimport sys\n\n\nsys.path = sys.path[1:]  # prevent local altair.py from shadowing the altair package\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 (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\"\nMARKER_START = \"#BD8233\"  # Imprint ochre — start of traversal\nMARKER_END = \"#AE3030\"  # Imprint matte red — end of traversal\n\n# Data\nt_liss = np.linspace(0, 2 * np.pi, 1200)\nt_spir = np.linspace(0, 6 * np.pi, 1200)\n\ndf_liss = pd.DataFrame({\"x\": np.sin(3 * t_liss), \"y\": np.sin(2 * t_liss), \"t\": t_liss})\ndf_spir = pd.DataFrame(\n    {\"x\": t_spir * np.cos(t_spir) / (6 * np.pi), \"y\": t_spir * np.sin(t_spir) / (6 * np.pi), \"t\": t_spir}\n)\n\n# Discrete gradient: 100 colors interpolated from Imprint sequential (#009E73 → #4467A3)\n# mark_trail/mark_line with quantitative color encoding don't render path geometry in\n# Vega-Lite v6.4 — workaround: N layered mark_line segments with constant colors each.\n# 100 segments closes the visible gap artifacts that 30 segments produced.\nN_SEG = 100\n_c0 = np.array([0, 158, 115])  # #009E73\n_c1 = np.array([68, 103, 163])  # #4467A3\nseg_colors = [\"#{:02x}{:02x}{:02x}\".format(*(_c0 + (_c1 - _c0) * i / (N_SEG - 1)).astype(int)) for i in range(N_SEG)]\n\n# Shared axis / scale config\nscale_xy = alt.Scale(domain=[-1.15, 1.15])\naxis_cfg = alt.Axis(labelFontSize=10, titleFontSize=12, domain=False, ticks=False)\ncolor_scale = alt.Scale(range=[\"#009E73\", \"#4467A3\"])\ngrad_legend = alt.Legend(gradientLength=120, gradientThickness=10, orient=\"right\")\n\n# Panel configs\npanel_configs = [\n    {\n        \"df\": df_liss,\n        \"title\": \"Lissajous · x = sin(3t), y = sin(2t)\",\n        \"xs\": 0.0,\n        \"ys\": 0.0,  # t=0 and t=2π both map to (0, 0) for this curve\n        \"xe\": 0.0,\n        \"ye\": 0.0,\n        \"ls\": \"t = 0\",\n        \"le\": \"t = 2π\",\n        \"dys\": -18,\n        \"dxs\": -5,\n        \"dye\": 18,\n        \"dxe\": 5,\n        \"legend\": grad_legend,\n    },\n    {\n        \"df\": df_spir,\n        \"title\": \"Archimedean Spiral · x = t·cos(t), y = t·sin(t)\",\n        \"xs\": float(df_spir[\"x\"].iloc[0]),\n        \"ys\": float(df_spir[\"y\"].iloc[0]),\n        \"xe\": float(df_spir[\"x\"].iloc[-1]),\n        \"ye\": float(df_spir[\"y\"].iloc[-1]),\n        \"ls\": \"t = 0\",\n        \"le\": \"t = 6π\",\n        \"dys\": -18,\n        \"dxs\": 10,\n        \"dye\": -18,\n        \"dxe\": -15,\n        \"legend\": None,\n    },\n]\n\n\ndef build_gradient_segments(df, n_seg, colors, scale_xy, axis_cfg):\n    t_vals = df[\"t\"].values\n    t_breaks = np.linspace(t_vals[0], t_vals[-1], n_seg + 1)\n    layers = []\n    for i in range(n_seg):\n        mask = (t_vals >= t_breaks[i]) & (t_vals <= t_breaks[i + 1])\n        df_seg = df[mask]\n        if len(df_seg) < 2:\n            continue\n        layers.append(\n            alt.Chart(df_seg)\n            .mark_line(strokeWidth=3, color=colors[i], clip=True, strokeCap=\"square\")\n            .encode(\n                x=alt.X(\"x:Q\", title=\"x(t)\", scale=scale_xy, axis=axis_cfg),\n                y=alt.Y(\"y:Q\", title=\"y(t)\", scale=scale_xy, axis=axis_cfg),\n                order=alt.Order(\"t:Q\"),\n            )\n        )\n    return layers\n\n\npanels = []\nfor c in panel_configs:\n    df = c[\"df\"]\n    seg_layers = build_gradient_segments(df, N_SEG, seg_colors, scale_xy, axis_cfg)\n\n    # Invisible dummy to carry the gradient legend and set the color scale domain\n    df_ends = df.iloc[[0, -1]][[\"x\", \"y\", \"t\"]].copy()\n    dummy = (\n        alt.Chart(df_ends)\n        .mark_point(opacity=0, size=0)\n        .encode(\n            x=alt.X(\"x:Q\", scale=scale_xy),\n            y=alt.Y(\"y:Q\", scale=scale_xy),\n            color=alt.Color(\"t:Q\", title=\"Parameter t\", scale=color_scale, legend=c[\"legend\"]),\n        )\n    )\n\n    curve = alt.layer(*seg_layers, dummy)\n\n    df_s = pd.DataFrame({\"x\": [c[\"xs\"]], \"y\": [c[\"ys\"]]})\n    df_e = pd.DataFrame({\"x\": [c[\"xe\"]], \"y\": [c[\"ye\"]]})\n\n    s_dot = (\n        alt.Chart(df_s)\n        .mark_point(size=160, filled=True, color=MARKER_START, stroke=PAGE_BG, strokeWidth=1.5)\n        .encode(x=\"x:Q\", y=\"y:Q\")\n    )\n    e_dot = (\n        alt.Chart(df_e)\n        .mark_point(size=160, shape=\"triangle-up\", filled=True, color=MARKER_END, stroke=PAGE_BG, strokeWidth=1.5)\n        .encode(x=\"x:Q\", y=\"y:Q\")\n    )\n    s_lbl = (\n        alt.Chart(df_s)\n        .mark_text(fontSize=10, fontWeight=\"bold\", dy=c[\"dys\"], dx=c[\"dxs\"], color=MARKER_START)\n        .encode(x=\"x:Q\", y=\"y:Q\", text=alt.value(c[\"ls\"]))\n    )\n    e_lbl = (\n        alt.Chart(df_e)\n        .mark_text(fontSize=10, fontWeight=\"bold\", dy=c[\"dye\"], dx=c[\"dxe\"], color=MARKER_END)\n        .encode(x=\"x:Q\", y=\"y:Q\", text=alt.value(c[\"le\"]))\n    )\n\n    panels.append(\n        (curve + s_dot + e_dot + s_lbl + e_lbl).properties(\n            width=270, height=300, title=alt.Title(c[\"title\"], fontSize=12, color=INK_SOFT)\n        )\n    )\n\n# Combine panels\nchart = (\n    alt.hconcat(*panels, spacing=25)\n    .resolve_scale(color=\"independent\")\n    .properties(\n        background=PAGE_BG,\n        title=alt.Title(\n            \"line-parametric · python · altair · anyplot.ai\",\n            fontSize=16,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"Color gradient encodes traversal direction: start (green) → end (blue)\",\n            subtitleFontSize=10,\n            subtitleColor=INK_MUTED,\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_axis(\n        gridColor=INK, gridOpacity=0.10, domainColor=INK_SOFT, tickColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save — canvas gate: 3200 × 1800 (landscape 16:9)\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\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"}