{"spec_id":"probability-weibull","library":"muix","language":"javascript","code":"// anyplot.ai\n// probability-weibull: Weibull Probability Plot for Reliability Analysis\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-24\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useDrawingArea, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME === \"dark\" ? \"dark\" : \"light\";\n// ANYPLOT_TOKENS has no \"muted\" anchor — derive it from default-style-guide.md\n// \"Theme-adaptive Chrome\" (tertiary text token).\nconst INK_MUTED = THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data: fixed-seed LCG (the browser has no seeded RNG) -------------------\nfunction makeLcg(seed: number) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\n// Turbine blade fatigue-life test: cycles to failure, in thousands (kcycles).\nconst BETA_TRUE = 2.4;\nconst ETA_TRUE = 420;\nconst N = 24;\nconst CENSORED_EVERY = 6; // every 6th-ranked blade pulled for inspection, unfailed\n\nconst sortedTimes = Array.from({ length: N }, () => {\n  const u = rand();\n  return (\n    Math.round(ETA_TRUE * Math.pow(-Math.log(1 - u), 1 / BETA_TRUE) * 10) / 10\n  );\n}).sort((a, b) => a - b);\n\n// Never censor the very first (smallest) time — a suspension needs at least\n// one preceding failure to inherit a plotting position from.\nconst observations = sortedTimes.map((time, idx) => ({\n  time,\n  isCensored: idx > 0 && (idx + 1) % CENSORED_EVERY === 0,\n}));\n\n// Median-rank plotting positions via Johnson's rank-adjustment method: each\n// failure's adjusted rank absorbs the \"credit\" left behind by suspensions\n// that preceded it, then Benard's approximation turns rank into probability.\nlet adjustedRank = 0;\nlet lastProbability = 0;\nconst n = observations.length;\nconst points = observations.map((obs, idx) => {\n  const reverseRank = n - idx;\n  if (!obs.isCensored) {\n    adjustedRank += (n + 1 - adjustedRank) / (1 + reverseRank);\n    lastProbability = (adjustedRank - 0.3) / (n + 0.4);\n  }\n  return { ...obs, probability: lastProbability };\n});\n\n// y = ln(-ln(1-F)) linearizes the Weibull CDF so Weibull data plots straight.\nconst weibullY = (p: number) => Math.log(-Math.log(1 - p));\n\nconst failurePoints = points.filter((p) => !p.isCensored);\nconst censoredPoints = points.filter((p) => p.isCensored);\n\nconst failureData = failurePoints.map((p, i) => ({\n  x: p.time,\n  y: weibullY(p.probability),\n  id: `failure-${i}`,\n}));\nconst censoredData = censoredPoints.map((p, i) => ({\n  x: p.time,\n  y: weibullY(p.probability),\n  id: `censored-${i}`,\n}));\n\n// Least-squares fit of ln(t) vs y over failures only: y = beta*ln(t) + b, so\n// slope = shape parameter (beta) and eta = exp(-intercept / beta).\nconst xs = failurePoints.map((p) => Math.log(p.time));\nconst ys = failureData.map((d) => d.y);\nconst nF = xs.length;\nconst meanX = xs.reduce((a, b) => a + b, 0) / nF;\nconst meanY = ys.reduce((a, b) => a + b, 0) / nF;\nlet sxy = 0;\nlet sxx = 0;\nfor (let i = 0; i < nF; i += 1) {\n  sxy += (xs[i] - meanX) * (ys[i] - meanY);\n  sxx += (xs[i] - meanX) ** 2;\n}\nconst beta = sxy / sxx;\nconst intercept = meanY - beta * meanX;\nconst eta = Math.exp(-intercept / beta);\n\nconst X_MIN = 80;\nconst X_MAX = 1000;\nconst fitLineX = [X_MIN, X_MAX];\nconst fitLineY = fitLineX.map((x) => beta * Math.log(x) + intercept);\n\nconst yAt632 = weibullY(0.632);\n\n// Probability-paper y-axis: fixed percentage gridlines, deliberately NOT\n// evenly spaced in y — that nonlinearity is the whole point of the transform.\nconst PROB_TICKS = [1, 2, 5, 10, 20, 30, 50, 63.2, 80, 90, 95, 99];\nconst yTickValues = PROB_TICKS.map((p) => weibullY(p / 100));\nconst yTickLabels = new Map(\n  PROB_TICKS.map((p, i) => [yTickValues[i], `${p}%`]),\n);\nconst Y_MIN = weibullY(0.01);\nconst Y_MAX = weibullY(0.99);\n\n// Custom scatter marker: filled circle for observed failures, hollow ring for\n// right-censored (suspended) units — same hue, status told by fill alone.\nfunction StatusMarker({ series, xScale, yScale, color, markerSize }: any) {\n  const isCensored = series.id === \"censored\";\n  return (\n    <g>\n      {series.data.map((point: any) => (\n        <circle\n          key={point.id}\n          cx={xScale(point.x)}\n          cy={yScale(point.y)}\n          r={markerSize}\n          fill={isCensored ? \"none\" : color}\n          stroke={color}\n          strokeWidth={isCensored ? 2.5 : 0}\n        />\n      ))}\n    </g>\n  );\n}\n\nconst TITLE = \"probability-weibull · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_SIZE =\n  TITLE.length > 67 ? Math.max(15, Math.round((22 * 67) / TITLE.length)) : 22;\n\n// Chart margins — the y-axis label below is a hand-placed <text>, not the\n// built-in `label` prop, because ChartsYAxis offsets it by a fixed\n// `tickFontSize + tickSize + 10` regardless of the actual (custom-formatted)\n// tick label width, which collided with \"63.2%\"-style ticks.\n// `top` is tall enough for the hand-rolled 3-row legend below the title.\nconst MARGIN = { top: 132, right: 64, bottom: 84, left: 140 };\n\n// Hand-rolled legend: the built-in ChartsLegend only draws solid-color square\n// swatches, which can't reflect the filled-vs-hollow marker encoding used on\n// the chart. Swatches here mirror the on-chart marks exactly (dash for the\n// fit line, filled circle for failures, hollow ring for suspensions).\nconst LEGEND_FONT_SIZE = 13;\nconst LEGEND_ROW_HEIGHT = 22;\nconst LEGEND_TOP = 60;\n\nfunction Legend({ right }: { right: number }) {\n  const legendItems = [\n    {\n      kind: \"dash\" as const,\n      label: `Fit: β≈${beta.toFixed(2)}, η≈${Math.round(eta)} kcycles`,\n      swatchColor: t.ink,\n    },\n    { kind: \"filled\" as const, label: \"Failure\", swatchColor: t.palette[0] },\n    {\n      kind: \"hollow\" as const,\n      label: \"Suspended (censored)\",\n      swatchColor: t.palette[0],\n    },\n  ];\n  const legendWidth = Math.max(\n    ...legendItems.map(\n      (item) => 22 + item.label.length * LEGEND_FONT_SIZE * 0.58,\n    ),\n  );\n  const left = right - legendWidth;\n  return (\n    <g>\n      {legendItems.map((item, i) => {\n        const y = LEGEND_TOP + i * LEGEND_ROW_HEIGHT;\n        return (\n          <g key={item.kind}>\n            {item.kind === \"dash\" && (\n              <line\n                x1={left}\n                x2={left + 14}\n                y1={y}\n                y2={y}\n                stroke={item.swatchColor}\n                strokeWidth={2.5}\n                strokeDasharray=\"6 3\"\n              />\n            )}\n            {item.kind === \"filled\" && (\n              <circle cx={left + 7} cy={y} r={6} fill={item.swatchColor} />\n            )}\n            {item.kind === \"hollow\" && (\n              <circle\n                cx={left + 7}\n                cy={y}\n                r={6}\n                fill=\"none\"\n                stroke={item.swatchColor}\n                strokeWidth={2.5}\n              />\n            )}\n            <text\n              x={left + 22}\n              y={y}\n              dominantBaseline=\"central\"\n              fontSize={LEGEND_FONT_SIZE}\n              fill={t.inkSoft}\n            >\n              {item.label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// Small elevated callout for the eta/63.2% crossing — a filled rounded box\n// behind the label sharpens the focal point beyond a thin dashed line + text.\nfunction ReferenceCallout({ y, label }: { y: number; label: string }) {\n  const { left, width: drawWidth } = useDrawingArea();\n  const yScale = useYScale();\n  const yPixel = yScale(y);\n  const paddingX = 8;\n  const boxWidth = label.length * 6.8 + paddingX * 2;\n  const boxHeight = 22;\n  const boxX = left + drawWidth - boxWidth;\n  const boxY = yPixel - boxHeight - 8;\n  return (\n    <g>\n      <rect\n        x={boxX}\n        y={boxY}\n        width={boxWidth}\n        height={boxHeight}\n        rx={4}\n        fill={t.elevatedBg}\n        stroke={INK_MUTED}\n        strokeWidth={1}\n      />\n      <text\n        x={boxX + boxWidth / 2}\n        y={boxY + boxHeight / 2}\n        textAnchor=\"middle\"\n        dominantBaseline=\"central\"\n        fontSize={LEGEND_FONT_SIZE}\n        fill={INK_MUTED}\n      >\n        {label}\n      </text>\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const plotMidY = MARGIN.top + (height - MARGIN.top - MARGIN.bottom) / 2;\n\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      margin={MARGIN}\n      series={[\n        {\n          type: \"line\",\n          id: \"fit\",\n          data: fitLineY,\n          curve: \"linear\",\n          color: t.ink,\n          showMark: false,\n          disableHighlight: true,\n        },\n        {\n          type: \"scatter\",\n          id: \"failure\",\n          data: failureData,\n          markerSize: 12,\n          color: t.palette[0],\n        },\n        {\n          type: \"scatter\",\n          id: \"censored\",\n          data: censoredData,\n          markerSize: 12,\n          color: t.palette[0],\n        },\n      ]}\n      xAxis={[\n        {\n          data: fitLineX,\n          scaleType: \"log\",\n          min: X_MIN,\n          max: X_MAX,\n          tickInterval: [100, 200, 300, 500, 700, 1000],\n          valueFormatter: (v: number) => `${v}`,\n          label: \"Cycles to Failure (thousands)\",\n          tickLabelStyle: { fontSize: 14 },\n          labelStyle: { fontSize: 16 },\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: Y_MIN,\n          max: Y_MAX,\n          tickInterval: yTickValues,\n          valueFormatter: (v: number) => yTickLabels.get(v) ?? \"\",\n          tickLabelStyle: { fontSize: 14 },\n        },\n      ]}\n      skipAnimation\n    >\n      <ChartsGrid horizontal vertical />\n      <LinePlot\n        skipAnimation\n        slotProps={{ line: { strokeDasharray: \"12 7\", strokeWidth: 2.5 } }}\n      />\n      <ScatterPlot slots={{ scatter: StatusMarker }} />\n      <ChartsXAxis />\n      <ChartsYAxis />\n      <ChartsReferenceLine\n        y={yAt632}\n        lineStyle={{\n          stroke: INK_MUTED,\n          strokeDasharray: \"4 4\",\n          strokeWidth: 1.5,\n        }}\n      />\n      <ReferenceCallout\n        y={yAt632}\n        label={`63.2% · η ≈ ${Math.round(eta)} kcycles`}\n      />\n      <Legend right={width - MARGIN.right} />\n      <text\n        x={width / 2}\n        y={44}\n        textAnchor=\"middle\"\n        fontSize={TITLE_FONT_SIZE}\n        fontWeight={600}\n        fill={t.ink}\n      >\n        {TITLE}\n      </text>\n      <text\n        x={0}\n        y={0}\n        textAnchor=\"middle\"\n        fontSize={16}\n        fill={t.ink}\n        transform={`translate(36, ${plotMidY}) rotate(-90)`}\n      >\n        Cumulative Failure Probability\n      </text>\n    </ChartContainer>\n  );\n}\n"}