{"spec_id":"probability-weibull","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// probability-weibull: Weibull Probability Plot for Reliability Analysis\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-07\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Turbine blade fatigue-life data (hours) — fixed, deterministic\n// Format: [time_hours, is_censored]\nconst rawData = [\n  [1580, false], [2100, false], [2540, true],\n  [2870, false], [3200, false], [3450, true],\n  [3720, false], [3980, false], [4150, false],\n  [4380, false], [4600, true],  [4820, false],\n  [5050, false], [5280, false], [5500, false],\n  [5750, true],  [6020, false], [6340, false],\n  [6700, false], [7100, false], [7550, true],\n  [8200, false], [9100, false],\n];\n\n// Sort by time, separate failures from suspensions\nconst sorted = rawData.slice().sort((a, b) => a[0] - b[0]);\nconst failures = sorted.filter(d => !d[1]);\nconst suspensions = sorted.filter(d => d[1]);\nconst nFail = failures.length;\n\n// Median rank plotting positions: F_j = (j+1 - 0.3) / (nFail + 0.4)\nconst failureData = failures.map(([time], j) => {\n  const F = (j + 1 - 0.3) / (nFail + 0.4);\n  return { x: time, y: Math.log(-Math.log(1 - F)), F };\n});\n\n// OLS Weibull fit: ln(-ln(1-F)) = beta * ln(t) + c\nconst lnT = failureData.map(p => Math.log(p.x));\nconst yWei = failureData.map(p => p.y);\nconst m = lnT.length;\nconst xBar = lnT.reduce((s, v) => s + v, 0) / m;\nconst yBar = yWei.reduce((s, v) => s + v, 0) / m;\nconst ssxy = lnT.reduce((s, v, i) => s + (v - xBar) * (yWei[i] - yBar), 0);\nconst ssxx = lnT.reduce((s, v) => s + (v - xBar) ** 2, 0);\nconst betaHat = ssxy / ssxx;\nconst c0 = yBar - betaHat * xBar;\nconst etaHat = Math.exp(-c0 / betaHat);\n\n// Suspension markers: project onto fitted line at their censoring times\nconst suspData = suspensions.map(([time]) => ({\n  x: time,\n  y: betaHat * Math.log(time) + c0,\n}));\n\n// Fitted Weibull line across x range\nconst xLogMin = Math.log(1200);\nconst xLogMax = Math.log(11000);\nconst fitData = [];\nfor (let i = 0; i <= 60; i++) {\n  const lnx = xLogMin + (i / 60) * (xLogMax - xLogMin);\n  const y = betaHat * lnx + c0;\n  if (y >= -4.8 && y <= 2.5) fitData.push({ x: Math.exp(lnx), y });\n}\n\n// 63.2% reference line (y = 0 on Weibull scale, crossing at x = eta)\nconst refData = [\n  { x: Math.exp(xLogMin), y: 0 },\n  { x: Math.exp(xLogMax), y: 0 },\n];\n\n// Y-axis Weibull probability ticks\nconst probLevels = [0.01, 0.05, 0.10, 0.20, 0.30, 0.50, 0.6321, 0.80, 0.90, 0.95, 0.99];\nconst yTickVals = probLevels.map(p => Math.log(-Math.log(1 - p)));\nconst yTickLabels = ['1%', '5%', '10%', '20%', '30%', '50%', '63.2%', '80%', '90%', '95%', '99%'];\n\n// L-shaped spine: draw only bottom + left borders, removing the default chart box\nconst spinePlugin = {\n  id: 'spines',\n  afterDatasetsDraw({ ctx, chartArea: { top, right, bottom, left } }) {\n    ctx.save();\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.beginPath();\n    ctx.moveTo(left, top);\n    ctx.lineTo(left, bottom);\n    ctx.moveTo(left, bottom);\n    ctx.lineTo(right, bottom);\n    ctx.stroke();\n    ctx.restore();\n  },\n};\n\n// Mount canvas\nconst canvas = document.createElement('canvas');\ndocument.getElementById('container').appendChild(canvas);\n\n// Title — scale font size for long string (baseline: 22px at 67 chars)\nconst titleText = 'Turbine Blade Fatigue · probability-weibull · javascript · chartjs · anyplot.ai';\nconst titleSize = Math.max(14, Math.round(22 * 67 / titleText.length));\n\nnew Chart(canvas, {\n  plugins: [spinePlugin],\n  data: {\n    datasets: [\n      {\n        type: 'line',\n        label: `Weibull Fit  β = ${betaHat.toFixed(2)},  η = ${Math.round(etaHat).toLocaleString()} h`,\n        data: fitData,\n        borderColor: t.palette[2],\n        borderWidth: 3,\n        pointRadius: 0,\n        fill: false,\n        tension: 0,\n        order: 3,\n      },\n      {\n        type: 'line',\n        label: `63.2% Reference  (η = ${Math.round(etaHat).toLocaleString()} h)`,\n        data: refData,\n        borderColor: t.inkSoft,\n        borderWidth: 2,\n        borderDash: [10, 6],\n        pointRadius: 0,\n        fill: false,\n        order: 4,\n      },\n      {\n        type: 'scatter',\n        label: `Failures  (n = ${nFail})`,\n        data: failureData,\n        backgroundColor: t.palette[0],\n        borderColor: t.palette[0],\n        pointRadius: 7,\n        pointHoverRadius: 9,\n        order: 1,\n      },\n      {\n        type: 'scatter',\n        label: `Suspensions  (n = ${suspensions.length})`,\n        data: suspData,\n        backgroundColor: 'transparent',\n        borderColor: t.palette[1],\n        borderWidth: 2,\n        pointStyle: 'crossRot',\n        pointRadius: 7,\n        pointHoverRadius: 9,\n        order: 2,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: titleText,\n        color: t.ink,\n        font: { size: titleSize, weight: '500' },\n        padding: { top: 8, bottom: 16 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 14 },\n          boxWidth: 22,\n          padding: 14,\n        },\n      },\n      tooltip: {\n        callbacks: {\n          label: (ctx) => {\n            if (ctx.datasetIndex === 2) {\n              const p = failureData[ctx.dataIndex];\n              return `t = ${p.x.toLocaleString()} h,  F = ${(p.F * 100).toFixed(1)}%`;\n            }\n            if (ctx.datasetIndex === 3) {\n              return `Suspended at ${suspensions[ctx.dataIndex][0].toLocaleString()} h`;\n            }\n            return '';\n          },\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: 'logarithmic',\n        min: Math.exp(xLogMin),\n        max: Math.exp(xLogMax),\n        title: {\n          display: true,\n          text: 'Time to Failure (hours)',\n          color: t.ink,\n          font: { size: 16 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          callback: (val) => {\n            const niceVals = [1000, 2000, 3000, 5000, 7000, 10000];\n            return niceVals.includes(val) ? val.toLocaleString() : '';\n          },\n        },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n      y: {\n        type: 'linear',\n        min: -4.8,\n        max: 2.5,\n        title: {\n          display: true,\n          text: 'Cumulative Failure Probability F(t)',\n          color: t.ink,\n          font: { size: 16 },\n        },\n        afterBuildTicks: (scale) => {\n          scale.ticks = yTickVals.map(v => ({ value: v }));\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 12 },\n          callback: (val) => {\n            const idx = yTickVals.findIndex(v => Math.abs(v - val) < 0.05);\n            return idx >= 0 ? yTickLabels[idx] : '';\n          },\n        },\n        grid: { color: t.grid },\n        border: { display: false },\n      },\n    },\n  },\n});\n"}