{"spec_id":"line-3d-trajectory","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' line-3d-trajectory: 3D Line Plot for Trajectory Visualization\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 87/100 | Created: 2026-09-10\n\nlibrary(ggplot2)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -------------------------------------------------------------\nTHEME    <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG  <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nINK      <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\n\n# --- Camera: orthographic projection (elevation 22, azimuth -55) --------------\n# ggplot2 has no 3D grammar, so the trajectory is projected to 2D screen\n# coordinates ourselves (the same technique any static 3D renderer uses under\n# the hood), then drawn with plain geom_path/geom_segment/geom_text.\nelev <- 22 * pi / 180\nazim <- -55 * pi / 180\n\nview_dir   <- c(cos(elev) * cos(azim), cos(elev) * sin(azim), sin(elev))\nworld_up   <- c(0, 0, 1)\nright_axis <- c(\n  view_dir[2] * world_up[3] - view_dir[3] * world_up[2],\n  view_dir[3] * world_up[1] - view_dir[1] * world_up[3],\n  view_dir[1] * world_up[2] - view_dir[2] * world_up[1]\n)\nright_axis <- right_axis / sqrt(sum(right_axis^2))\nup_axis <- c(\n  right_axis[2] * view_dir[3] - right_axis[3] * view_dir[2],\n  right_axis[3] * view_dir[1] - right_axis[1] * view_dir[3],\n  right_axis[1] * view_dir[2] - right_axis[2] * view_dir[1]\n)\n\nproject_x <- function(x, y, z) x * right_axis[1] + y * right_axis[2] + z * right_axis[3]\nproject_y <- function(x, y, z) x * up_axis[1]    + y * up_axis[2]    + z * up_axis[3]\ndepth_of  <- function(x, y, z) x * view_dir[1]   + y * view_dir[2]   + z * view_dir[3]\n\n# --- Data: Lorenz attractor, integrated with classic RK4 -----------------------\nsigma <- 10\nrho   <- 28\nbeta  <- 8 / 3\ndt    <- 0.008\nn_burn  <- 1000   # discard the transient before the state settles onto the attractor\nn_steps <- 2000\n\nlorenz_rhs <- function(s) {\n  c(\n    sigma * (s[2] - s[1]),\n    s[1] * (rho - s[3]) - s[2],\n    s[1] * s[2] - beta * s[3]\n  )\n}\n\nrk4_step <- function(s, dt) {\n  k1 <- lorenz_rhs(s)\n  k2 <- lorenz_rhs(s + dt / 2 * k1)\n  k3 <- lorenz_rhs(s + dt / 2 * k2)\n  k4 <- lorenz_rhs(s + dt * k3)\n  s + dt / 6 * (k1 + 2 * k2 + 2 * k3 + k4)\n}\n\nstate <- c(x = 0.1, y = 0, z = 0)\nfor (i in seq_len(n_burn)) state <- rk4_step(state, dt)\n\ntraj <- matrix(NA_real_, nrow = n_steps, ncol = 3, dimnames = list(NULL, c(\"x\", \"y\", \"z\")))\nfor (i in seq_len(n_steps)) {\n  state <- rk4_step(state, dt)\n  traj[i, ] <- state\n}\ntraj <- as.data.frame(traj)\ntraj$t <- (seq_len(n_steps) - 1) * dt\n\ntraj$px         <- project_x(traj$x, traj$y, traj$z)\ntraj$py         <- project_y(traj$x, traj$y, traj$z)\ntraj$depth      <- depth_of(traj$x, traj$y, traj$z)\ndepth_rng       <- range(traj$depth)\ntraj$depth_norm <- (traj$depth - depth_rng[1]) / diff(depth_rng)\n\n# --- Axis box: three edges meeting at the near-bottom corner -------------------\nxr <- range(traj$x)\nyr <- range(traj$y)\nzr <- range(traj$z)\nx_pad <- diff(xr) * 0.06\ny_pad <- diff(yr) * 0.06\nz_pad <- diff(zr) * 0.06\n\ncorner <- c(xr[1] - x_pad, yr[1] - y_pad, zr[1] - z_pad)\n\naxis_lines <- data.frame(\n  x    = rep(corner[1], 3),\n  y    = rep(corner[2], 3),\n  z    = rep(corner[3], 3),\n  xend = c(xr[2] + x_pad, corner[1], corner[1]),\n  yend = c(corner[2], yr[2] + y_pad, corner[2]),\n  zend = c(corner[3], corner[3], zr[2] + z_pad)\n)\naxis_lines$px    <- project_x(axis_lines$x, axis_lines$y, axis_lines$z)\naxis_lines$py    <- project_y(axis_lines$x, axis_lines$y, axis_lines$z)\naxis_lines$pxend <- project_x(axis_lines$xend, axis_lines$yend, axis_lines$zend)\naxis_lines$pyend <- project_y(axis_lines$xend, axis_lines$yend, axis_lines$zend)\n\nx_breaks <- pretty(xr, n = 4)\nx_breaks <- x_breaks[x_breaks >= xr[1] & x_breaks <= xr[2]]\ny_breaks <- pretty(yr, n = 4)\ny_breaks <- y_breaks[y_breaks >= yr[1] & y_breaks <= yr[2]]\nz_breaks <- pretty(zr, n = 4)\nz_breaks <- z_breaks[z_breaks >= zr[1] & z_breaks <= zr[2]]\n\nx_ticks <- data.frame(x = x_breaks, y = corner[2] - y_pad * 1.4, z = corner[3], label = x_breaks)\ny_ticks <- data.frame(x = corner[1] - x_pad * 1.4, y = y_breaks, z = corner[3], label = y_breaks)\nx_ticks$px <- project_x(x_ticks$x, x_ticks$y, x_ticks$z)\nx_ticks$py <- project_y(x_ticks$x, x_ticks$y, x_ticks$z)\ny_ticks$px <- project_x(y_ticks$x, y_ticks$y, y_ticks$z)\ny_ticks$py <- project_y(y_ticks$x, y_ticks$y, y_ticks$z)\n\n# Z ticks sit on the vertical axis line; nudge the label sideways in pixel\n# space so it doesn't merge with the axis line itself.\nz_ticks <- data.frame(x = corner[1], y = corner[2], z = z_breaks, label = z_breaks)\nz_ticks$px <- project_x(z_ticks$x, z_ticks$y, z_ticks$z) - diff(xr) * 0.05\nz_ticks$py <- project_y(z_ticks$x, z_ticks$y, z_ticks$z)\n\naxis_labels <- data.frame(\n  x     = c(xr[2] + x_pad * 3, corner[1], corner[1]),\n  y     = c(corner[2], yr[2] + y_pad * 3, corner[2]),\n  z     = c(corner[3], corner[3], zr[2] + z_pad * 3),\n  label = c(\"X\", \"Y\", \"Z\")\n)\naxis_labels$px <- project_x(axis_labels$x, axis_labels$y, axis_labels$z)\naxis_labels$py <- project_y(axis_labels$x, axis_labels$y, axis_labels$z)\n\n# --- Plot -----------------------------------------------------------------\np <- ggplot() +\n  geom_segment(data = axis_lines, aes(x = px, y = py, xend = pxend, yend = pyend),\n               color = INK_SOFT, linewidth = 0.6) +\n  geom_text(data = x_ticks, aes(px, py, label = label), color = INK_SOFT, size = 3.2) +\n  geom_text(data = y_ticks, aes(px, py, label = label), color = INK_SOFT, size = 3.2) +\n  geom_text(data = z_ticks, aes(px, py, label = label), color = INK_SOFT, size = 3.2) +\n  geom_path(data = traj, aes(px, py, color = t, alpha = depth_norm),\n            linewidth = 0.7, lineend = \"round\") +\n  geom_text(data = axis_labels, aes(px, py, label = label),\n            color = INK, size = 3.8, fontface = \"bold\") +\n  scale_color_gradient(low = \"#009E73\", high = \"#4467A3\", name = \"Time (s)\") +\n  scale_alpha_continuous(range = c(0.45, 1), guide = \"none\") +\n  labs(title = \"Lorenz Attractor · line-3d-trajectory · r · ggplot2 · anyplot.ai\") +\n  coord_fixed(ratio = 1, clip = \"off\") +\n  theme_void(base_size = 8) +\n  theme(\n    plot.background  = element_rect(fill = PAGE_BG, color = PAGE_BG),\n    panel.background = element_rect(fill = PAGE_BG, color = NA),\n    plot.title       = element_text(color = INK, size = 12, hjust = 0.5, margin = margin(b = 14)),\n    legend.position  = \"right\",\n    legend.title     = element_text(color = INK, size = 10),\n    legend.text      = element_text(color = INK_SOFT, size = 8),\n    legend.key       = element_rect(fill = PAGE_BG, color = NA),\n    plot.margin      = margin(t = 20, r = 20, b = 10, l = 20)\n  )\n\n# --- Save -----------------------------------------------------------------\nggsave(\n  filename = sprintf(\"plot-%s.png\", THEME),\n  plot     = p,\n  device   = ragg::agg_png,\n  width    = 8,\n  height   = 4.5,\n  units    = \"in\",\n  dpi      = 400\n)\n"}