{"spec_id":"scatter-3d","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' scatter-3d: 3D Scatter Plot\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-09-10\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tibble)\nlibrary(scales)\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\"\nIMPRINT_SEQ_LOW  <- \"#009E73\"\nIMPRINT_SEQ_HIGH <- \"#4467A3\"\n\n# --- Data: CT-scanned porosity defects inside a 3D-printed titanium bracket -\n# Three weak layer-adhesion zones plus scattered background porosity, in\n# build-plate coordinates (mm). Defect size (micrometers) is the 4th variable.\nn_zone      <- 55\nzone_x      <- c(-9, 7, 1)\nzone_y      <- c(6, -7, 9)\nzone_z      <- c(5, 7, -6)\nzone_spread <- c(2.6, 3.1, 2.3)\n\nx <- c(\n  rnorm(n_zone, zone_x[1], zone_spread[1]),\n  rnorm(n_zone, zone_x[2], zone_spread[2]),\n  rnorm(n_zone, zone_x[3], zone_spread[3]),\n  runif(20, -15, 15)\n)\ny <- c(\n  rnorm(n_zone, zone_y[1], zone_spread[1]),\n  rnorm(n_zone, zone_y[2], zone_spread[2]),\n  rnorm(n_zone, zone_y[3], zone_spread[3]),\n  runif(20, -15, 15)\n)\nz <- c(\n  rnorm(n_zone, zone_z[1], zone_spread[1]),\n  rnorm(n_zone, zone_z[2], zone_spread[2]),\n  rnorm(n_zone, zone_z[3], zone_spread[3]),\n  runif(20, -15, 15)\n)\ndefect_size_um <- c(\n  rgamma(n_zone, shape = 6, scale = 8) + 15,\n  rgamma(n_zone, shape = 5, scale = 7) + 15,\n  rgamma(n_zone, shape = 7, scale = 6) + 15,\n  rgamma(20, shape = 2, scale = 6) + 10\n)\n\n# --- Orthographic projection (azimuth 35°, elevation 20°) --------------------\n# ggplot2 has no native 3D geom, so the third axis is folded into 2D screen\n# space via a standard rotate-then-project transform (the same math a 3D\n# chart library applies internally), rendered with geom_point/geom_segment.\naz <- 35 * pi / 180\nel <- 20 * pi / 180\n\nproject <- function(px, py, pz) {\n  x_rot <- px * cos(az) + py * sin(az)\n  y_rot <- -px * sin(az) + py * cos(az)\n  list(proj_x = x_rot, proj_y = y_rot * cos(el) - pz * sin(el))\n}\n\nproj  <- project(x, y, z)\ndepth <- (-x * sin(az) + y * cos(az)) * sin(el) + z * cos(el) # larger = closer\n\npoints_df <- tibble(\n  proj_x         = proj$proj_x,\n  proj_y         = proj$proj_y,\n  depth          = depth,\n  defect_size_um = defect_size_um\n) %>%\n  mutate(\n    depth_norm  = rescale(depth, to = c(0, 1)),\n    # Power curve (not linear) spreads size/alpha further apart across depth,\n    # so overlapping points in the densest cluster cores separate more clearly.\n    point_size  = 1.8 + depth_norm^1.3 * 3.0,\n    point_alpha = 0.32 + depth_norm^1.3 * 0.55\n  ) %>%\n  arrange(depth)\n\n# --- Bounding-box wireframe (static 3D reference frame) ----------------------\npad <- 1.15\nxr  <- range(x) * pad\nyr  <- range(y) * pad\nzr  <- range(z) * pad\n\ncorners       <- expand.grid(cx = xr, cy = yr, cz = zr)\ncorner_proj   <- project(corners$cx, corners$cy, corners$cz)\ncorners$proj_x <- corner_proj$proj_x\ncorners$proj_y <- corner_proj$proj_y\n\nedge_from <- c(1, 3, 5, 7, 1, 2, 5, 6, 1, 2, 3, 4)\nedge_to   <- c(2, 4, 6, 8, 3, 4, 7, 8, 5, 6, 7, 8)\nedges_df <- tibble(\n  x    = corners$proj_x[edge_from],\n  y    = corners$proj_y[edge_from],\n  xend = corners$proj_x[edge_to],\n  yend = corners$proj_y[edge_to]\n)\n\n# --- Axis labels, extended past the box tips along their own direction -------\ntip_idx  <- c(2, 3, 5) # X, Y, Z edges all start at corner 1\ndir_x    <- corners$proj_x[tip_idx] - corners$proj_x[1]\ndir_y    <- corners$proj_y[tip_idx] - corners$proj_y[1]\ndir_len  <- sqrt(dir_x^2 + dir_y^2)\nunit_x   <- dir_x / dir_len\nunit_y   <- dir_y / dir_len\nlabel_gap <- 8.2 # generous clearance so axis titles clear nearby data points\n\naxis_labels <- tibble(\n  label = c(\"X (mm)\", \"Y (mm)\", \"Z (mm)\"),\n  x     = corners$proj_x[tip_idx] + unit_x * label_gap,\n  y     = corners$proj_y[tip_idx] + unit_y * label_gap\n)\n\n# --- Numeric tick references along each axis edge (mid/high mm values) -------\n# Reviewers noted the box had no coordinate scale; nudge each tick label\n# perpendicular to its edge so it reads as a ruler mark, not edge clutter.\n# The low end (-15) sits at the shared tri-axis corner for all three axes, so\n# it is skipped here — labeling it three times over would just overlap.\ntick_vals   <- c(0, 15)\ntick_nudge  <- 1.4\ntick_x_proj <- project(tick_vals, rep(yr[1], 2), rep(zr[1], 2))\ntick_y_proj <- project(rep(xr[1], 2), tick_vals, rep(zr[1], 2))\ntick_z_proj <- project(rep(xr[1], 2), rep(yr[1], 2), tick_vals)\n\ntick_labels <- tibble(\n  label = as.character(rep(tick_vals, 3)),\n  x = c(tick_x_proj$proj_x, tick_y_proj$proj_x, tick_z_proj$proj_x) -\n    rep(unit_y, each = 2) * tick_nudge,\n  y = c(tick_x_proj$proj_y, tick_y_proj$proj_y, tick_z_proj$proj_y) +\n    rep(unit_x, each = 2) * tick_nudge\n)\n\n# --- Zone callouts, linking the projected scatter back to the CT-scan narrative\n# Each callout sits well outside its dense point cluster, with a thin leader\n# segment back to the cluster centroid, so the label never lands on top of\n# 55+ overlapping alpha-blended points. Offsets are hand-tuned per zone (not\n# a single radial push-out) because Zone B's cluster sits almost exactly\n# along the same sightline as the X-axis tip/title, so pushing it further\n# out along that line would just collide with \"X (mm)\" instead.\nzone_proj    <- project(zone_x, zone_y, zone_z)\nzone_off_x   <- c(-6.5, -8.5, 6.5)\nzone_off_y   <- c(7.5, -6.5, 7.5)\n\nzone_labels <- tibble(\n  label = c(\"Zone A\", \"Zone B\", \"Zone C\"),\n  x     = zone_proj$proj_x + zone_off_x,\n  y     = zone_proj$proj_y + zone_off_y\n)\n\nzone_leaders <- tibble(\n  x    = zone_proj$proj_x,\n  y    = zone_proj$proj_y,\n  xend = zone_proj$proj_x + zone_off_x * 0.82,\n  yend = zone_proj$proj_y + zone_off_y * 0.82\n)\n\n# --- Plot ---------------------------------------------------------------------\np <- ggplot() +\n  geom_segment(\n    data = edges_df,\n    aes(x = x, y = y, xend = xend, yend = yend),\n    color = INK_SOFT, linewidth = 0.35, alpha = 0.5\n  ) +\n  geom_point(\n    data = points_df,\n    aes(x = proj_x, y = proj_y, color = defect_size_um,\n        size = point_size, alpha = point_alpha)\n  ) +\n  geom_text(\n    data = tick_labels,\n    aes(x = x, y = y, label = label),\n    color = INK_SOFT, size = 2.6, alpha = 0.9\n  ) +\n  geom_text(\n    data = axis_labels,\n    aes(x = x, y = y, label = label),\n    color = INK_SOFT, size = 3.2, fontface = \"bold\"\n  ) +\n  geom_segment(\n    data = zone_leaders,\n    aes(x = x, y = y, xend = xend, yend = yend),\n    color = INK_SOFT, linewidth = 0.3, alpha = 0.7\n  ) +\n  geom_text(\n    data = zone_labels,\n    aes(x = x, y = y, label = label),\n    color = INK, size = 3.1, fontface = \"italic\"\n  ) +\n  scale_color_gradient(low = IMPRINT_SEQ_LOW, high = IMPRINT_SEQ_HIGH,\n                        name = \"Defect size (µm)\") +\n  scale_size_identity() +\n  scale_alpha_identity() +\n  coord_fixed(ratio = 1, clip = \"off\") +\n  labs(\n    title   = \"scatter-3d · r · ggplot2 · anyplot.ai\",\n    caption = \"CT-scanned porosity defects in a 3D-printed titanium bracket · build-plate coordinates, ±15 mm per axis\"\n  ) +\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,\n                                      margin = margin(b = 10)),\n    plot.caption      = element_text(color = INK_SOFT, size = 8, hjust = 0.5,\n                                      margin = margin(t = 10)),\n    legend.title      = element_text(color = INK, size = 10),\n    legend.text       = element_text(color = INK_SOFT, size = 8),\n    legend.position   = \"right\",\n    plot.margin       = margin(15, 25, 15, 15)\n  )\n\n# --- Save (PNG, both themes) ---------------------------------------------------\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"}