{"spec_id":"line-3d-trajectory","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-3d-trajectory: 3D Line Plot for Trajectory Visualization\nLibrary: pygal 3.1.3 | Python 3.13.15\nQuality: 86/100 | Updated: 2026-08-25\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Background\" + \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette (see prompts/default-style-guide.md \"Categorical Palette\")\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data — Lorenz attractor trajectory (deterministic chaotic ODE, no randomness)\nsigma, rho, beta, dt, num_steps = 10.0, 28.0, 8.0 / 3.0, 0.01, 3000\nx, y, z = 1.0, 1.0, 1.0\ntrajectory = np.zeros((num_steps, 3))\nfor i in range(num_steps):\n    dx, dy, dz = sigma * (y - x), x * (rho - z) - y, x * y - beta * z\n    x, y, z = x + dt * dx, y + dt * dy, z + dt * dz\n    trajectory[i] = [x, y, z]\n\n# pygal has no native 3D chart type, so the 3rd spatial axis is preserved via\n# a true isometric projection (standard 45 deg azimuth / 35.264 deg elevation\n# rotation matrices) instead of simply plotting x vs y and discarding z — the\n# rotated x/y below are each a mix of all three original coordinates, so the\n# projected 2D view actually encodes the 3D shape of the trajectory\nxc = trajectory[:, 0] - trajectory[:, 0].mean()\nyc = trajectory[:, 1] - trajectory[:, 1].mean()\nzc = trajectory[:, 2] - trajectory[:, 2].mean()\n\nazimuth, elevation = np.radians(45.0), np.radians(35.264)\nx_rot = xc * np.cos(azimuth) - yc * np.sin(azimuth)\ny_rot = xc * np.sin(azimuth) + yc * np.cos(azimuth)\ny_iso = y_rot * np.cos(elevation) - zc * np.sin(elevation)\n\nx_norm = x_rot / (x_rot.std() + 1e-8)\ny_norm = y_iso / (y_iso.std() + 1e-8)\n\n# pygal has no per-vertex gradient stroke, so the trajectory is split into\n# equal-time segments; pygal cycles a serie's color from `Style.colors` by\n# series index (a per-add `color=` kwarg is silently dropped — undocumented\n# SerieConfig limitation), so the gradient itself must be pre-built here and\n# handed to the Style as one color per segment. Alpha is baked into each\n# stop (rgba) to thin overplotting in the tightly-wound coil regions —\n# >500 points per the data-density heuristic (3000 points / 30 series here).\nnum_segments = 30\nsegment_length = len(x_norm) // num_segments\n\n# imprint_seq: brand green -> blue (single-polarity continuous encoding)\nseq_start, seq_end = IMPRINT_PALETTE[0], IMPRINT_PALETTE[2]\ngradient_colors = []\nfor i in range(num_segments):\n    t = i / (num_segments - 1)\n    r = round(int(seq_start[1:3], 16) + (int(seq_end[1:3], 16) - int(seq_start[1:3], 16)) * t)\n    g = round(int(seq_start[3:5], 16) + (int(seq_end[3:5], 16) - int(seq_start[3:5], 16)) * t)\n    b = round(int(seq_start[5:7], 16) + (int(seq_end[5:7], 16) - int(seq_start[5:7], 16)) * t)\n    gradient_colors.append(f\"rgba({r}, {g}, {b}, 0.75)\")\n\n# Plot — see prompts/library/pygal.md \"Sizing + Theme\" for the canonical values\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=tuple(gradient_colors),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=1.5,\n)\n\nchart = pygal.XY(\n    title=\"line-3d-trajectory · python · pygal · anyplot.ai\",\n    x_title=\"X — isometric projection of (x, y, z)\",\n    y_title=\"Y — isometric projection; color encodes elapsed time\",\n    width=3200,\n    height=1800,\n    style=custom_style,\n    show_legend=False,\n    show_dots=False,\n)\n\nfor i in range(num_segments):\n    start_idx = i * segment_length\n    end_idx = start_idx + segment_length if i < num_segments - 1 else len(x_norm)\n    xy_pairs = list(zip(x_norm[start_idx:end_idx].tolist(), y_norm[start_idx:end_idx].tolist(), strict=True))\n    chart.add(f\"Time step {start_idx}–{end_idx - 1}\", xy_pairs, stroke_style={\"width\": 1.5})\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}