{"spec_id":"sn-curve-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// sn-curve-basic: S-N Curve (Wöhler Curve)\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fixed-seed LCG — the browser has no seeded RNG.\nfunction lcg(seed) {\n  let state = seed;\n  return function () {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\n// Basquin's law for a steel alloy: stress = Sf' * (2N)^b\nconst FATIGUE_STRENGTH_COEFF = 1250; // Sf', MPa\nconst FATIGUE_STRENGTH_EXP = -0.095; // b\nconst ULTIMATE_STRENGTH = 620; // MPa\nconst YIELD_STRENGTH = 415; // MPa\nconst ENDURANCE_LIMIT = 180; // MPa — asymptote for infinite life\nconst RUNOUT_CYCLES = 2e7; // test termination — specimens beyond this are \"runouts\"\n\nfunction cyclesForStress(stress) {\n  return 0.5 * Math.pow(stress / FATIGUE_STRENGTH_COEFF, 1 / FATIGUE_STRENGTH_EXP);\n}\nfunction stressForCycles(cycles) {\n  return Math.max(FATIGUE_STRENGTH_COEFF * Math.pow(2 * cycles, FATIGUE_STRENGTH_EXP), ENDURANCE_LIMIT);\n}\n\n// Test specimens at 11 stress levels, 5 specimens each (steel coupon fatigue tests).\n// The top two levels (480, 450 MPa) sit above yield strength, giving the\n// low-cycle/plastic region alongside the high-cycle/elastic and infinite-life ones.\nconst stressLevels = [480, 450, 380, 340, 305, 275, 250, 225, 205, 190, 180];\nconst specimensPerLevel = 5;\nconst testData = [];\nstressLevels.forEach((stress) => {\n  const meanCycles = cyclesForStress(stress);\n  for (let i = 0; i < specimensPerLevel; i++) {\n    const scatterFactor = 0.6 + rand() * 0.8; // specimen-to-specimen scatter\n    const cycles = Math.min(Math.round(meanCycles * scatterFactor), RUNOUT_CYCLES);\n    testData.push([cycles, stress]);\n  }\n});\n\n// Basquin fit curve, floored at the endurance limit\nconst fitCurve = [];\nconst fitPoints = 60;\nfor (let i = 0; i <= fitPoints; i++) {\n  const logN = 3 + (i / fitPoints) * (Math.log10(RUNOUT_CYCLES) - 3);\n  const cycles = Math.pow(10, logN);\n  fitCurve.push([cycles, stressForCycles(cycles)]);\n}\n\nfunction formatCycles(value) {\n  if (value >= 1e6) return `${value / 1e6}M`;\n  if (value >= 1e3) return `${value / 1e3}k`;\n  return `${value}`;\n}\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"sn-curve-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    type: \"logarithmic\",\n    title: { text: \"Cycles to Failure (N)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    gridLineWidth: 1,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter: function () {\n        return formatCycles(this.value);\n      },\n    },\n    min: 1e3,\n    max: RUNOUT_CYCLES,\n  },\n  yAxis: {\n    type: \"logarithmic\",\n    title: { text: \"Stress Amplitude (MPa)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    min: 150,\n    max: 700,\n    startOnTick: false,\n    endOnTick: false,\n    plotLines: [\n      {\n        value: ULTIMATE_STRENGTH,\n        color: t.ink,\n        dashStyle: \"Solid\",\n        width: 1.5,\n        zIndex: 5,\n        label: {\n          text: `Ultimate Strength (${ULTIMATE_STRENGTH} MPa)`,\n          align: \"right\",\n          x: -10,\n          y: -6,\n          style: { color: t.inkSoft, fontSize: \"13px\" },\n        },\n      },\n      {\n        value: YIELD_STRENGTH,\n        color: t.ink,\n        dashStyle: \"Dash\",\n        width: 1.5,\n        zIndex: 5,\n        label: {\n          text: `Yield Strength (${YIELD_STRENGTH} MPa)`,\n          align: \"right\",\n          x: -10,\n          y: -6,\n          style: { color: t.inkSoft, fontSize: \"13px\" },\n        },\n      },\n      {\n        value: ENDURANCE_LIMIT,\n        color: t.ink,\n        dashStyle: \"ShortDot\",\n        width: 1.5,\n        zIndex: 5,\n        label: {\n          text: `Endurance Limit (${ENDURANCE_LIMIT} MPa)`,\n          align: \"right\",\n          x: -10,\n          y: -6,\n          style: { color: t.inkSoft, fontSize: \"13px\" },\n        },\n      },\n    ],\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false },\n    scatter: { marker: { radius: 5, fillColor: t.palette[0], lineColor: t.pageBg, lineWidth: 1.5 } },\n    line: { marker: { enabled: false }, lineWidth: 2.5 },\n  },\n  series: [\n    {\n      name: \"Fatigue Test Data\",\n      type: \"scatter\",\n      data: testData,\n      color: t.palette[0],\n    },\n    {\n      name: \"Basquin Fit (S = Sf'·(2N)^b)\",\n      type: \"line\",\n      data: fitCurve,\n      color: t.palette[1],\n    },\n  ],\n});\n"}