{"spec_id":"sn-curve-basic","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' sn-curve-basic: S-N Curve (Wöhler Curve)\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 89/100 | Created: 2026-05-20\n\nlibrary(ggplot2)\nlibrary(dplyr)\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\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\nIMPRINT   <- c(\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n                 \"#AE3030\", \"#2ABCCD\", \"#954477\")\nGRID        <- adjustcolor(INK_SOFT, alpha.f = 0.25)\n\n# AISI 1045 Steel material properties (MPa)\nsigma_u <- 750   # Ultimate tensile strength\nsigma_y <- 530   # Yield strength\nsigma_e <- 375   # Endurance limit (~50% × UTS, engineering standard for steel)\n\n# Basquin equation: log10(N) = C0 - k * log10(stress)\n# C0 chosen so sigma_e maps to N ≈ 10^6 (conventional steel endurance life)\nk  <- 10\nC0 <- 31.74\n\n# N at endurance limit — left boundary of infinite-life zone\nN_endurance <- 10^(C0 - k * log10(sigma_e))\n\n# Fatigue test specimens: multiple replicates per stress level (all above endurance limit)\nstress_levels <- c(620, 580, 540, 500, 460, 430, 410, 390)\nreps          <- c(2, 3, 2, 3, 2, 3, 2, 3)\n\ndf <- data.frame(stress = rep(stress_levels, reps)) |>\n    mutate(cycles = 10^(C0 - k * log10(stress) + rnorm(n(), 0, 0.12)))\n\n# Basquin fit line — log-spaced in cycle direction for accurate log-scale rendering\nfit_df <- data.frame(\n    cycles = 10^seq(3, log10(N_endurance * 0.998), length.out = 300)\n) |> mutate(stress = 10^((C0 - log10(cycles)) / k))\n\n# Plot\np <- ggplot() +\n    # Infinite-life zone: stronger fill to emphasize key engineering insight\n    annotate(\"rect\",\n        xmin = N_endurance, xmax = 10^8,\n        ymin = 310, ymax = sigma_e,\n        fill = IMPRINT[5], alpha = 0.14\n    ) +\n    # Reference lines for key material properties\n    geom_hline(yintercept = sigma_u, linetype = \"dashed\",\n               color = IMPRINT[2], linewidth = 0.65, alpha = 0.85) +\n    geom_hline(yintercept = sigma_y, linetype = \"dashed\",\n               color = IMPRINT[3], linewidth = 0.65, alpha = 0.85) +\n    # Endurance limit thicker — the key design threshold for infinite life\n    geom_hline(yintercept = sigma_e, linetype = \"longdash\",\n               color = IMPRINT[5], linewidth = 1.1, alpha = 0.95) +\n    # Basquin fit curve\n    geom_line(data = fit_df, aes(x = cycles, y = stress),\n              color = INK_MUTED, linewidth = 1.0) +\n    # Test data scatter (multiple replicates per stress level)\n    geom_point(data = df, aes(x = cycles, y = stress),\n               shape = 21, size = 3.0, stroke = 0.5,\n               color = IMPRINT[1], fill = IMPRINT[1], alpha = 0.85) +\n    # Reference line labels — right-aligned at x = 10^7.5\n    annotate(\"text\", x = 10^7.5, y = sigma_u * 1.05,\n             label = \"Ultimate Strength (750 MPa)\",\n             color = IMPRINT[2], hjust = 1, vjust = 0, size = 4.0) +\n    annotate(\"text\", x = 10^7.5, y = sigma_y * 1.05,\n             label = \"Yield Strength (530 MPa)\",\n             color = IMPRINT[3], hjust = 1, vjust = 0, size = 4.0) +\n    annotate(\"text\", x = 10^7.5, y = sigma_e * 1.06,\n             label = \"Endurance Limit (375 MPa)\",\n             color = IMPRINT[5], hjust = 1, vjust = 0, size = 4.0,\n             fontface = \"bold\") +\n    # Infinite-life region label inside shaded zone\n    annotate(\"text\", x = 10^7.2, y = 340,\n             label = \"Infinite Life\\nRegion\",\n             color = INK_MUTED, hjust = 0.5, size = 3.2, fontface = \"italic\") +\n    # Log-log axes\n    scale_x_log10(\n        limits = c(1e3, 10^8),\n        breaks = 10^(3:8),\n        labels = scales::trans_format(\"log10\", scales::math_format(10^.x))\n    ) +\n    scale_y_log10(\n        limits = c(310, 900),\n        breaks = c(350, 400, 500, 600, 700, 800),\n        labels = scales::comma_format(accuracy = 1)\n    ) +\n    labs(\n        title   = \"sn-curve-basic · r · ggplot2 · anyplot.ai\",\n        x       = \"Cycles to Failure\",\n        y       = \"Stress Amplitude (MPa)\",\n        caption = \"Basquin model: log N = 31.7 − 10 · log σ  |  AISI 1045 steel\"\n    ) +\n    theme_minimal(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        panel.grid.major.x = element_blank(),\n        panel.grid.major.y = element_line(color = GRID, linewidth = 0.3),\n        panel.grid.minor = element_blank(),\n        panel.border     = element_blank(),\n        axis.line        = element_line(color = INK_SOFT, linewidth = 0.5),\n        axis.ticks       = element_blank(),\n        axis.title       = element_text(color = INK, size = 10),\n        axis.text        = element_text(color = INK_SOFT, size = 8),\n        plot.title       = element_text(color = INK, size = 13, face = \"bold\"),\n        plot.caption     = element_text(color = INK_MUTED, size = 7, hjust = 1),\n        plot.margin      = margin(10, 20, 10, 10, \"pt\")\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"}