{"spec_id":"sn-curve-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// sn-curve-basic: S-N Curve (Wöhler Curve)\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fatigue test data for a quenched-and-tempered 4340 steel coupon, fit with a\n// Basquin power law: stress = fitA * cycles^fitB.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1103515245 + 12345) % 2147483648;\n    return state / 2147483648;\n  };\n}\nconst rand = lcg(42);\n\nconst ultimateStrength = 745; // MPa\nconst yieldStrength = 650; // MPa — ~0.87 of ultimate, typical for Q&T 4340 steel\nconst enduranceLimit = 260; // MPa\nconst fitA = 1450; // MPa — Basquin intercept\nconst fitB = -0.11; // Basquin exponent\n\n// Nominal stress levels tested, multiple specimens per level (scatter in\n// cycles-to-failure at fixed stress is the realistic fatigue-test outcome).\nconst stressLevels = [620, 560, 500, 450, 410, 375, 345, 320, 300, 280, 265];\nconst specimensPerLevel = 4;\n\nconst testPoints = [];\nstressLevels.forEach((stress) => {\n  const meanCycles = Math.pow(stress / fitA, 1 / fitB);\n  for (let i = 0; i < specimensPerLevel; i++) {\n    // Spread specimens deterministically around the mean so same-stress\n    // clusters stay visually distinct instead of overlapping into one blob.\n    const jitter = (i - (specimensPerLevel - 1) / 2) * 0.24;\n    const scatterFactor = 1 + jitter + (rand() - 0.5) * 0.08;\n    testPoints.push([meanCycles * scatterFactor, stress]);\n  }\n});\n\n// Basquin fit curve, drawn from the low-cycle data down to the endurance limit.\nconst cyclesAtEndurance = Math.pow(enduranceLimit / fitA, 1 / fitB);\nconst logNStart = 3;\nconst logNEnd = Math.log10(cyclesAtEndurance);\nconst fitSteps = 60;\nconst fitCurve = [];\nfor (let i = 0; i <= fitSteps; i++) {\n  const logN = logNStart + (i / fitSteps) * (logNEnd - logNStart);\n  const cycles = Math.pow(10, logN);\n  fitCurve.push([cycles, fitA * Math.pow(cycles, fitB)]);\n}\n\n// --- Helpers -----------------------------------------------------------------\nconst SUPERSCRIPTS = { 0: \"⁰\", 1: \"¹\", 2: \"²\", 3: \"³\", 4: \"⁴\", 5: \"⁵\", 6: \"⁶\", 7: \"⁷\", 8: \"⁸\", 9: \"⁹\" };\nfunction formatDecadeTick(value) {\n  const exponent = Math.log10(value);\n  if (Math.abs(exponent - Math.round(exponent)) > 1e-6) return \"\";\n  const digits = String(Math.round(exponent)).split(\"\");\n  return \"10\" + digits.map((d) => SUPERSCRIPTS[d] ?? d).join(\"\");\n}\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"sn-curve-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 36,\n    textStyle: { color: t.ink, fontSize: 24, fontWeight: 600 },\n  },\n  legend: {\n    data: [\"Test specimens\", \"Basquin fit\"],\n    top: 96,\n    left: \"center\",\n    itemWidth: 22,\n    itemHeight: 12,\n    textStyle: { color: t.inkSoft, fontSize: 16 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n    valueFormatter: (value) => Math.round(value).toLocaleString(),\n  },\n  grid: { left: 140, right: 90, top: 180, bottom: 120 },\n  xAxis: {\n    type: \"log\",\n    min: 1e3,\n    max: 1e7,\n    name: \"Cycles to Failure (N)\",\n    nameLocation: \"middle\",\n    nameGap: 50,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: formatDecadeTick },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"log\",\n    min: 200,\n    max: 900,\n    name: \"Stress Amplitude (MPa)\",\n    nameLocation: \"middle\",\n    nameGap: 80,\n    nameRotate: 90,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Test specimens\",\n      type: \"scatter\",\n      data: testPoints,\n      symbolSize: 13,\n      itemStyle: { color: t.palette[0], opacity: 0.85 },\n    },\n    {\n      name: \"Basquin fit\",\n      type: \"line\",\n      data: fitCurve,\n      showSymbol: false,\n      lineStyle: { color: t.palette[1], width: 3 },\n      itemStyle: { color: t.palette[1] },\n      markArea: {\n        silent: true,\n        itemStyle: { color: t.ink, opacity: 0.06 },\n        label: { show: true, position: \"insideTopLeft\", color: t.inkSoft, fontSize: 13, fontStyle: \"italic\" },\n        data: [[{ yAxis: 200, name: \"Infinite life region\" }, { yAxis: enduranceLimit }]],\n      },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        label: { color: t.ink, fontSize: 14, position: \"insideEndTop\" },\n        lineStyle: { color: t.ink, width: 1.5 },\n        data: [\n          {\n            yAxis: ultimateStrength,\n            lineStyle: { type: \"solid\" },\n            label: { formatter: \"Ultimate Strength · {c} MPa\" },\n          },\n          {\n            yAxis: yieldStrength,\n            lineStyle: { type: \"dashed\" },\n            label: { formatter: \"Yield Strength · {c} MPa\" },\n          },\n          {\n            yAxis: enduranceLimit,\n            lineStyle: { type: \"dotted\" },\n            label: { formatter: \"Endurance Limit · {c} MPa\", position: \"insideEndBottom\" },\n          },\n        ],\n      },\n    },\n  ],\n});\n"}