{"spec_id":"line-3d-trajectory","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nline-3d-trajectory: 3D Line Plot for Trajectory Visualization\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-05-16\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColorBar, ColumnDataSource, HoverTool, Label, LinearColorMapper, Range1d\nfrom bokeh.palettes import Viridis256\nfrom bokeh.plotting import figure\nfrom bokeh.resources import CDN\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (read from environment)\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\"\n\n# Data - Lorenz attractor trajectory (classic chaotic system)\nnp.random.seed(42)\n\n# Lorenz system parameters\nsigma = 10.0\nrho = 28.0\nbeta = 8.0 / 3.0\n\n# Generate trajectory with 1500 points for smooth visualization (inline Euler integration)\nn_points = 1500\ndt = 0.01\nx, y, z = np.zeros(n_points), np.zeros(n_points), np.zeros(n_points)\nx[0], y[0], z[0] = 0.1, 0.0, 0.0  # Initial conditions\n\nfor i in range(n_points - 1):\n    dx = sigma * (y[i] - x[i]) * dt\n    dy = (x[i] * (rho - z[i]) - y[i]) * dt\n    dz = (x[i] * y[i] - beta * z[i]) * dt\n    x[i + 1] = x[i] + dx\n    y[i + 1] = y[i] + dy\n    z[i + 1] = z[i] + dz\n\n# Normalize coordinates for better visualization\nx = (x - x.mean()) / x.std() * 2\ny = (y - y.mean()) / y.std() * 2\nz = (z - z.mean()) / z.std() * 2\n\n# 3D to 2D isometric projection (elevation=20°, azimuth=55°)\nelev_rad = np.radians(20)\nazim_rad = np.radians(55)\n\n# Rotation around z-axis (azimuth)\nx_rot = x * np.cos(azim_rad) - y * np.sin(azim_rad)\ny_rot = x * np.sin(azim_rad) + y * np.cos(azim_rad)\n\n# Rotation around x-axis (elevation) and project to 2D\nx_proj = x_rot\nz_proj = y_rot * np.sin(elev_rad) + z * np.cos(elev_rad)\n\n# Time/progression for color gradient (shows trajectory evolution)\ntime_seconds = np.linspace(0, n_points * dt, n_points)\nmax_time = n_points * dt  # 15 seconds\n\n# Color mapping using actual simulation time values\ncolor_mapper = LinearColorMapper(palette=Viridis256, low=0, high=max_time)\n\n# Create segments for multi-colored line with gradient\nn_segments = n_points - 1\nxs = [[x_proj[i], x_proj[i + 1]] for i in range(n_segments)]\nys = [[z_proj[i], z_proj[i + 1]] for i in range(n_segments)]\n\n# Vectorized color mapping for line segments\nsegment_times = time_seconds[:-1]\ncolor_indices = np.clip((segment_times / max_time * 255).astype(int), 0, 255)\ncolors = [Viridis256[idx] for idx in color_indices]\n\n# Create ColumnDataSource for line segments\nsource = ColumnDataSource(data={\"xs\": xs, \"ys\": ys, \"color\": colors})\n\n# Create scatter points for hover functionality\nhover_source = ColumnDataSource(\n    data={\n        \"x\": x_proj[::10],\n        \"y\": z_proj[::10],\n        \"time\": time_seconds[::10],\n        \"x_coord\": x[::10],\n        \"y_coord\": y[::10],\n        \"z_coord\": z[::10],\n    }\n)\n\n# Create Bokeh figure with interactive tools\np = figure(\n    width=4800,\n    height=2700,\n    title=\"Lorenz Attractor · line-3d-trajectory · bokeh · pyplots.ai\",\n    toolbar_location=\"right\",\n    tools=\"pan,wheel_zoom,box_zoom,reset,save\",\n)\n\n# Draw trajectory as multi-line with color gradient showing time progression\np.multi_line(xs=\"xs\", ys=\"ys\", line_color=\"color\", line_width=3, line_alpha=0.85, source=source)\n\n# Add subtle scatter points for hover interaction\nscatter = p.scatter(x=\"x\", y=\"y\", source=hover_source, size=18, alpha=0.15, hover_alpha=0.9, hover_color=\"orange\")\n\n# Add HoverTool for interactivity (Bokeh distinctive feature)\nhover = HoverTool(\n    renderers=[scatter],\n    tooltips=[(\"Time\", \"@time{0.2f} s\"), (\"X\", \"@x_coord{0.2f}\"), (\"Y\", \"@y_coord{0.2f}\"), (\"Z\", \"@z_coord{0.2f}\")],\n    mode=\"mouse\",\n)\np.add_tools(hover)\n\n# Set appropriate ranges with padding\nx_min, x_max = x_proj.min(), x_proj.max()\ny_min, y_max = z_proj.min(), z_proj.max()\nx_pad = (x_max - x_min) * 0.15\ny_pad = (y_max - y_min) * 0.15\n\n# Center the plot with balanced padding\np.x_range = Range1d(x_min - x_pad * 0.8, x_max + x_pad * 1.0)\np.y_range = Range1d(y_min - y_pad * 1.0, y_max + y_pad * 1.0)\n\n# Hide default axes for cleaner 3D projection look\np.xaxis.visible = False\np.yaxis.visible = False\n\n# Custom 3D axis lines positioned at the projected origin\norigin_3d_x, origin_3d_y, origin_3d_z = 0, 0, 0\norigin_x_rot = origin_3d_x * np.cos(azim_rad) - origin_3d_y * np.sin(azim_rad)\norigin_y_rot = origin_3d_x * np.sin(azim_rad) + origin_3d_y * np.cos(azim_rad)\norigin_x = origin_x_rot\norigin_y = origin_y_rot * np.sin(elev_rad) + origin_3d_z * np.cos(elev_rad)\n\n# Axis styling with theme-adaptive colors\naxis_color = INK_SOFT\naxis_width = 4\naxis_length = 2.5\n\n# Project 3D axis endpoints to 2D\n# X-axis: point (axis_length, 0, 0)\nx_end_x_rot = axis_length * np.cos(azim_rad)\nx_end_y_rot = axis_length * np.sin(azim_rad)\nx_axis_end_x = x_end_x_rot\nx_axis_end_y = x_end_y_rot * np.sin(elev_rad)\n\n# Y-axis: point (0, axis_length, 0)\ny_end_x_rot = -axis_length * np.sin(azim_rad)\ny_end_y_rot = axis_length * np.cos(azim_rad)\ny_axis_end_x = y_end_x_rot\ny_axis_end_y = y_end_y_rot * np.sin(elev_rad)\n\n# Z-axis: point (0, 0, axis_length)\nz_axis_end_x = origin_x\nz_axis_end_y = origin_y + axis_length * np.cos(elev_rad)\n\n# Draw axis lines from projected origin\np.line(x=[origin_x, x_axis_end_x], y=[origin_y, x_axis_end_y], line_color=axis_color, line_width=axis_width)\np.line(x=[origin_x, y_axis_end_x], y=[origin_y, y_axis_end_y], line_color=axis_color, line_width=axis_width)\np.line(x=[origin_x, z_axis_end_x], y=[origin_y, z_axis_end_y], line_color=axis_color, line_width=axis_width)\n\n# Add axis arrows (small triangles at the end of each axis)\narrow_size = 0.18\n\n# X-axis arrow\nx_dir = np.array([x_axis_end_x - origin_x, x_axis_end_y - origin_y])\nx_dir = x_dir / np.linalg.norm(x_dir)\nx_perp = np.array([-x_dir[1], x_dir[0]])\np.patch(\n    x=[\n        x_axis_end_x,\n        x_axis_end_x - arrow_size * x_dir[0] + arrow_size * 0.5 * x_perp[0],\n        x_axis_end_x - arrow_size * x_dir[0] - arrow_size * 0.5 * x_perp[0],\n    ],\n    y=[\n        x_axis_end_y,\n        x_axis_end_y - arrow_size * x_dir[1] + arrow_size * 0.5 * x_perp[1],\n        x_axis_end_y - arrow_size * x_dir[1] - arrow_size * 0.5 * x_perp[1],\n    ],\n    fill_color=axis_color,\n    line_color=axis_color,\n)\n\n# Y-axis arrow\ny_dir = np.array([y_axis_end_x - origin_x, y_axis_end_y - origin_y])\ny_dir = y_dir / np.linalg.norm(y_dir)\ny_perp = np.array([-y_dir[1], y_dir[0]])\np.patch(\n    x=[\n        y_axis_end_x,\n        y_axis_end_x - arrow_size * y_dir[0] + arrow_size * 0.5 * y_perp[0],\n        y_axis_end_x - arrow_size * y_dir[0] - arrow_size * 0.5 * y_perp[0],\n    ],\n    y=[\n        y_axis_end_y,\n        y_axis_end_y - arrow_size * y_dir[1] + arrow_size * 0.5 * y_perp[1],\n        y_axis_end_y - arrow_size * y_dir[1] - arrow_size * 0.5 * y_perp[1],\n    ],\n    fill_color=axis_color,\n    line_color=axis_color,\n)\n\n# Z-axis arrow\nz_dir = np.array([z_axis_end_x - origin_x, z_axis_end_y - origin_y])\nz_dir = z_dir / np.linalg.norm(z_dir)\nz_perp = np.array([-z_dir[1], z_dir[0]])\np.patch(\n    x=[\n        z_axis_end_x,\n        z_axis_end_x - arrow_size * z_dir[0] + arrow_size * 0.5 * z_perp[0],\n        z_axis_end_x - arrow_size * z_dir[0] - arrow_size * 0.5 * z_perp[0],\n    ],\n    y=[\n        z_axis_end_y,\n        z_axis_end_y - arrow_size * z_dir[1] + arrow_size * 0.5 * z_perp[1],\n        z_axis_end_y - arrow_size * z_dir[1] - arrow_size * 0.5 * z_perp[1],\n    ],\n    fill_color=axis_color,\n    line_color=axis_color,\n)\n\n# Add descriptive axis labels with theme-adaptive colors\nx_label = Label(\n    x=x_axis_end_x + 0.15,\n    y=x_axis_end_y - 0.2,\n    text=\"X (state)\",\n    text_font_size=\"36pt\",\n    text_color=INK,\n    text_font_style=\"bold\",\n)\np.add_layout(x_label)\n\ny_label = Label(\n    x=y_axis_end_x - 0.6,\n    y=y_axis_end_y - 0.4,\n    text=\"Y (state)\",\n    text_font_size=\"36pt\",\n    text_color=INK,\n    text_font_style=\"bold\",\n)\np.add_layout(y_label)\n\nz_label = Label(\n    x=z_axis_end_x + 0.15,\n    y=z_axis_end_y + 0.1,\n    text=\"Z (state)\",\n    text_font_size=\"36pt\",\n    text_color=INK,\n    text_font_style=\"bold\",\n)\np.add_layout(z_label)\n\n# Add color bar for time progression with correct time range (0-15 seconds)\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    width=60,\n    location=(0, 0),\n    title=\"Time (s)\",\n    title_text_font_size=\"32pt\",\n    major_label_text_font_size=\"24pt\",\n    title_standoff=20,\n    margin=40,\n    padding=20,\n)\np.add_layout(color_bar, \"right\")\n\n# Title styling for large canvas\np.title.text_font_size = \"44pt\"\np.title.text_font_style = \"bold\"\np.title.text_color = INK\n\n# Grid styling - subtle with theme-adaptive colors\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\np.xgrid.grid_line_dash = [6, 4]\np.ygrid.grid_line_dash = [6, 4]\n\n# Background styling with theme-adaptive colors\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\np.min_border_right = 220\n\n# Save HTML for interactive version\noutput_file(f\"plot-{THEME}.html\")\nsave(p, resources=CDN, title=\"line-3d-trajectory · bokeh · pyplots.ai\")\n\n# Screenshot with headless Chrome via Selenium\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}