{"spec_id":"sn-curve-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// sn-curve-basic: S-N Curve (Wöhler Curve)\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-02\n\nimport * as React from \"react\";\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport { useDrawingArea, useXScale, useYScale } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Basquin power-law fit, anchored between a low-cycle point and the knee\n// where the curve levels off at the endurance limit: S = C * N^b -----------\nconst N1 = 1e3;\nconst S1 = 700; // MPa, low-cycle anchor\nconst N_KNEE = 1e6; // cycles — transition from finite life to infinite life\nconst ENDURANCE_LIMIT = 380; // MPa — safe stress below which failure won't occur\nconst ULTIMATE_STRENGTH = 760; // MPa\nconst YIELD_STRENGTH = 620; // MPa\n\nconst B_EXP = Math.log(ENDURANCE_LIMIT / S1) / Math.log(N_KNEE / N1);\nconst C_COEF = S1 * Math.pow(N1, -B_EXP);\nconst basquinStress = (n) =>\n  n <= N_KNEE ? C_COEF * Math.pow(n, B_EXP) : ENDURANCE_LIMIT;\nconst basquinCycles = (s) => Math.pow(s / C_COEF, 1 / B_EXP);\n\n// --- Fitted curve: dense log-spaced grid from low-cycle fatigue through the\n// knee into the infinite-life region -----------------------------------------\nconst X_MIN = 500;\nconst X_MAX = 1e7;\nconst GRID_N = 60;\nconst logMin = Math.log10(X_MIN);\nconst logMax = Math.log10(X_MAX);\nconst fitCycles = Array.from({ length: GRID_N }, (_, i) =>\n  Math.pow(10, logMin + (i / (GRID_N - 1)) * (logMax - logMin)),\n);\nconst fitStress = fitCycles.map(basquinStress);\n\n// --- Test specimens: fixed stress per level, scatter in life (cycles) — the\n// standard convention for fatigue coupon testing ----------------------------\nlet seed = 42;\nfunction rng() {\n  seed = (Math.imul(1664525, seed) + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\n\nconst STRESS_LEVELS = [700, 650, 600, 550, 500, 450, 420, 400];\nconst SPECIMENS_PER_LEVEL = 3;\nconst specimens = [];\nSTRESS_LEVELS.forEach((stress, levelIdx) => {\n  const baseCycles = basquinCycles(stress);\n  for (let k = 0; k < SPECIMENS_PER_LEVEL; k++) {\n    const jitter = Math.pow(10, (rng() - 0.5) * 0.3);\n    specimens.push({\n      id: `L${levelIdx}-${k}`,\n      x: Math.round(baseCycles * jitter),\n      y: stress,\n    });\n  }\n});\n\n// --- Axis formatting ---------------------------------------------------------\nconst SUPERSCRIPT = {\n  \"0\": \"⁰\",\n  \"1\": \"¹\",\n  \"2\": \"²\",\n  \"3\": \"³\",\n  \"4\": \"⁴\",\n  \"5\": \"⁵\",\n  \"6\": \"⁶\",\n  \"7\": \"⁷\",\n  \"8\": \"⁸\",\n  \"9\": \"⁹\",\n};\nconst fmtCycles = (n) => {\n  const exp = Math.round(Math.log10(n));\n  return `10${String(exp)\n    .split(\"\")\n    .map((d) => SUPERSCRIPT[d])\n    .join(\"\")}`;\n};\nconst CYCLE_TICKS = [1e3, 1e4, 1e5, 1e6, 1e7];\nconst STRESS_TICKS = [400, 500, 600, 700, 800];\n\nconst TITLE = \"sn-curve-basic · javascript · muix · anyplot.ai\";\n\n// --- Fatigue life regions, called out in the spec notes: low-cycle (plastic),\n// high-cycle (elastic), and infinite-life (below the endurance limit) ---------\nconst LOW_HIGH_BOUNDARY = 1e4; // conventional low-cycle / high-cycle divide\nconst REGIONS = [\n  { label: \"LOW-CYCLE FATIGUE\", xMin: X_MIN, xMax: LOW_HIGH_BOUNDARY },\n  { label: \"HIGH-CYCLE FATIGUE\", xMin: LOW_HIGH_BOUNDARY, xMax: N_KNEE },\n  { label: \"INFINITE LIFE\", xMin: N_KNEE, xMax: X_MAX },\n];\n\n// Translucent band below the endurance limit — the \"safe for infinite life\"\n// stress range — plus a thicker, amber-colored endurance-limit stroke so it\n// stays visually distinct from the purple fit line where the two coincide.\nfunction SafeZoneBand() {\n  const { left, top, width, height } = useDrawingArea();\n  const yScale = useYScale(\"stress\") as ((v: number) => number) | undefined;\n  if (!yScale) return null;\n  const yTop = yScale(ENDURANCE_LIMIT);\n  const yBottom = top + height;\n  return (\n    <g>\n      <rect\n        x={left}\n        y={yTop}\n        width={width}\n        height={Math.max(yBottom - yTop, 1)}\n        fill={t.palette[0]}\n        opacity={0.07}\n      />\n      <text\n        x={left + width - 10}\n        y={yTop + (yBottom - yTop) / 2}\n        textAnchor=\"end\"\n        dominantBaseline=\"middle\"\n        fontSize={11}\n        fontStyle=\"italic\"\n        fill={t.inkSoft}\n        opacity={0.75}\n      >\n        Safe zone — infinite life below endurance limit\n      </text>\n    </g>\n  );\n}\n\n// Region labels + boundary ticks placed under the x-axis, calling out the\n// three fatigue regions described in the spec (low-cycle / high-cycle /\n// infinite-life) so the story reads without parsing the reference-line labels.\nfunction FatigueRegionLabels() {\n  const { left, top, width, height } = useDrawingArea();\n  const xScale = useXScale(\"cycles\") as ((v: number) => number) | undefined;\n  if (!xScale) return null;\n  const lineY = top + height + 62;\n  const textY = lineY + 15;\n  return (\n    <g>\n      {REGIONS.map((r) => {\n        const x0 = xScale(Math.max(r.xMin, X_MIN));\n        const x1 = xScale(Math.min(r.xMax, X_MAX));\n        const xMid = (x0 + x1) / 2;\n        return (\n          <React.Fragment key={r.label}>\n            <line\n              x1={x0 + 4}\n              y1={lineY}\n              x2={x1 - 4}\n              y2={lineY}\n              stroke={t.inkSoft}\n              strokeWidth={1}\n              opacity={0.35}\n            />\n            <line\n              x1={x0 + 4}\n              y1={lineY - 4}\n              x2={x0 + 4}\n              y2={lineY + 4}\n              stroke={t.inkSoft}\n              strokeWidth={1}\n              opacity={0.35}\n            />\n            <line\n              x1={x1 - 4}\n              y1={lineY - 4}\n              x2={x1 - 4}\n              y2={lineY + 4}\n              stroke={t.inkSoft}\n              strokeWidth={1}\n              opacity={0.35}\n            />\n            <text\n              x={xMid}\n              y={textY}\n              textAnchor=\"middle\"\n              fontSize={11}\n              letterSpacing={0.6}\n              fill={t.inkSoft}\n              opacity={0.85}\n            >\n              {r.label}\n            </text>\n          </React.Fragment>\n        );\n      })}\n    </g>\n  );\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const TITLE_H = 56;\n  const chartH = height - TITLE_H;\n\n  return (\n    <Box\n      sx={{\n        width,\n        height,\n        background: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography\n        sx={{\n          fontSize: \"22px\",\n          fontWeight: 500,\n          color: t.ink,\n          textAlign: \"center\",\n          pt: \"14px\",\n          pb: \"6px\",\n        }}\n      >\n        {TITLE}\n      </Typography>\n      <ChartContainer\n        width={width}\n        height={chartH}\n        margin={{ top: 28, right: 48, bottom: 132, left: 108 }}\n        sx={{\n          \".MuiLineElement-series-fit\": { strokeWidth: 3.5 },\n          \"& circle\": { stroke: t.pageBg, strokeWidth: 2 },\n        }}\n        series={[\n          {\n            type: \"scatter\",\n            id: \"specimens\",\n            xAxisId: \"cycles\",\n            yAxisId: \"stress\",\n            data: specimens,\n            color: t.palette[0],\n            markerSize: 11,\n            label: \"Test Specimens\",\n          },\n          {\n            type: \"line\",\n            id: \"fit\",\n            xAxisId: \"cycles\",\n            yAxisId: \"stress\",\n            data: fitStress,\n            color: t.palette[1],\n            showMark: false,\n            curve: \"monotoneX\",\n            label: \"Basquin Fit (Power-Law)\",\n          },\n        ]}\n        xAxis={[\n          {\n            id: \"cycles\",\n            scaleType: \"log\",\n            data: fitCycles,\n            min: X_MIN,\n            max: X_MAX,\n            label: \"Cycles to Failure, N\",\n            valueFormatter: fmtCycles,\n            tickInterval: CYCLE_TICKS,\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n            labelStyle: { fontSize: 16, fill: t.ink },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"stress\",\n            scaleType: \"log\",\n            min: 340,\n            max: 820,\n            label: \"Stress Amplitude (MPa)\",\n            tickInterval: STRESS_TICKS,\n            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n            labelStyle: { fontSize: 16, fill: t.ink },\n          },\n        ]}\n      >\n        <ChartsGrid horizontal />\n        <SafeZoneBand />\n        <LinePlot skipAnimation />\n        <ScatterPlot skipAnimation />\n        <ChartsReferenceLine\n          y={ULTIMATE_STRENGTH}\n          axisId=\"stress\"\n          label={`Ultimate Strength = ${ULTIMATE_STRENGTH} MPa`}\n          labelAlign=\"start\"\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n          lineStyle={{\n            stroke: t.amber,\n            strokeDasharray: \"2 4\",\n            strokeWidth: 1.5,\n            opacity: 0.85,\n          }}\n        />\n        <ChartsReferenceLine\n          y={YIELD_STRENGTH}\n          axisId=\"stress\"\n          label={`Yield Strength = ${YIELD_STRENGTH} MPa`}\n          labelAlign=\"start\"\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n          lineStyle={{\n            stroke: t.amber,\n            strokeDasharray: \"6 4\",\n            strokeWidth: 1.5,\n            opacity: 0.85,\n          }}\n        />\n        <ChartsReferenceLine\n          y={ENDURANCE_LIMIT}\n          axisId=\"stress\"\n          label={`Endurance Limit = ${ENDURANCE_LIMIT} MPa`}\n          labelAlign=\"start\"\n          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}\n          lineStyle={{\n            stroke: t.amber,\n            strokeDasharray: \"3 5\",\n            strokeWidth: 5,\n            opacity: 0.85,\n          }}\n        />\n        <ChartsXAxis axisId=\"cycles\" />\n        <ChartsYAxis axisId=\"stress\" />\n        <ChartsLegend\n          position={{ vertical: \"top\", horizontal: \"right\" }}\n          slotProps={{\n            legend: {\n              itemMarkWidth: 16,\n              itemMarkHeight: 16,\n              markGap: 8,\n              itemGap: 24,\n              labelStyle: { fontSize: 14, fill: t.ink },\n            },\n          }}\n        />\n        <ChartsTooltip trigger=\"item\" />\n        <FatigueRegionLabels />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}