diff --git a/plots/probability-weibull/implementations/javascript/muix.tsx b/plots/probability-weibull/implementations/javascript/muix.tsx index 6791f5d93af..bf171a13157 100644 --- a/plots/probability-weibull/implementations/javascript/muix.tsx +++ b/plots/probability-weibull/implementations/javascript/muix.tsx @@ -1,94 +1,93 @@ // anyplot.ai // probability-weibull: Weibull Probability Plot for Reliability Analysis -// Library: muix 7.29.1 | JavaScript 22.23.2 -// Quality: 92/100 | Created: 2026-08-24 -import { ChartContainer } from "@mui/x-charts/ChartContainer"; -import { ScatterPlot } from "@mui/x-charts/ScatterChart"; +// Library: muix 9.16.0 | JavaScript 22.23.3 +// Quality: 87/100 | Updated: 2026-10-24 + +import { ChartsContainer } from "@mui/x-charts/ChartsContainer"; import { LinePlot } from "@mui/x-charts/LineChart"; import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis"; import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis"; import { ChartsGrid } from "@mui/x-charts/ChartsGrid"; import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine"; -import { useDrawingArea, useYScale } from "@mui/x-charts/hooks"; +import { useDrawingArea, useXScale, useYScale } from "@mui/x-charts/hooks"; const t = window.ANYPLOT_TOKENS; -const THEME = window.ANYPLOT_THEME === "dark" ? "dark" : "light"; -// ANYPLOT_TOKENS has no "muted" anchor — derive it from default-style-guide.md -// "Theme-adaptive Chrome" (tertiary text token). -const INK_MUTED = THEME === "dark" ? "#A8A79F" : "#6B6A63"; -// --- Data: fixed-seed LCG (the browser has no seeded RNG) ------------------- -function makeLcg(seed: number) { - let state = seed >>> 0; - return () => { - state = (state * 1664525 + 1013904223) >>> 0; - return state / 4294967296; - }; -} -const rand = makeLcg(42); +// Constants +const TITLE = "probability-weibull · javascript · muix · anyplot.ai"; +const CHAR_LIFE_PCT = 63.2; +const PROB_TICKS = [0.1, 0.5, 1, 2, 5, 10, 20, 30, 50, 63.2, 80, 90, 95, 99, 99.9]; +const LOG_TICK_MANTISSAS = [1, 1.5, 2, 3, 5, 7]; +const DEFAULT_MIN_PROB_PCT = 1; +const DEFAULT_MAX_PROB_PCT = 99; +const Y_PAD_SHARE = 0.08; +const X_MIN_FACTOR = 0.72; +const X_MAX_FACTOR = 1.38; +const MARKER_RADIUS = 7; +const HEADER_HEIGHT = 76; +const PROB_AXIS_LABEL = "Cumulative Failure Probability (%)"; +const FAILURE_LABEL = "Failure"; +const CENSORED_LABEL = "Suspended (censored)"; + +// Columns: set these to your data's column names +const TIME = "Cycles to Failure (kcycles)"; // role: x +const IS_CENSORED = "Right Censored"; // role: group -// Turbine blade fatigue-life test: cycles to failure, in thousands (kcycles). +// Data: replace this block with your own records const BETA_TRUE = 2.4; const ETA_TRUE = 420; -const N = 24; -const CENSORED_EVERY = 6; // every 6th-ranked blade pulled for inspection, unfailed +const SAMPLE_SIZE = 24; +const CENSORED_EVERY = 6; -const sortedTimes = Array.from({ length: N }, () => { - const u = rand(); - return ( - Math.round(ETA_TRUE * Math.pow(-Math.log(1 - u), 1 / BETA_TRUE) * 10) / 10 - ); -}).sort((a, b) => a - b); +const rawSamples: number[] = []; +let lcgState = 42 >>> 0; +for (let i = 0; i < SAMPLE_SIZE; i += 1) { + lcgState = (Math.imul(lcgState, 1664525) + 1013904223) >>> 0; + const u = lcgState / 4294967296; + const sampleTime = Math.round(ETA_TRUE * Math.pow(-Math.log(1 - u), 1 / BETA_TRUE) * 10) / 10; + rawSamples.push(sampleTime); +} +rawSamples.sort((a, b) => a - b); -// Never censor the very first (smallest) time — a suspension needs at least -// one preceding failure to inherit a plotting position from. -const observations = sortedTimes.map((time, idx) => ({ - time, - isCensored: idx > 0 && (idx + 1) % CENSORED_EVERY === 0, +const data: Array> = rawSamples.map((timeVal, idx) => ({ + [TIME]: timeVal, + [IS_CENSORED]: idx > 0 && (idx + 1) % CENSORED_EVERY === 0, })); -// Median-rank plotting positions via Johnson's rank-adjustment method: each -// failure's adjusted rank absorbs the "credit" left behind by suspensions -// that preceded it, then Benard's approximation turns rank into probability. +// Plot: reads only data and the constants above +const sortedObs = [...data].sort((a, b) => Number(a[TIME]) - Number(b[TIME])); +const totalN = sortedObs.length; let adjustedRank = 0; let lastProbability = 0; -const n = observations.length; -const points = observations.map((obs, idx) => { - const reverseRank = n - idx; - if (!obs.isCensored) { - adjustedRank += (n + 1 - adjustedRank) / (1 + reverseRank); - lastProbability = (adjustedRank - 0.3) / (n + 0.4); +const plottedPoints = sortedObs.map((row, idx) => { + const time = Number(row[TIME]); + const isCensored = Boolean(row[IS_CENSORED]); + const reverseRank = totalN - idx; + if (!isCensored) { + adjustedRank += (totalN + 1 - adjustedRank) / (1 + reverseRank); + lastProbability = (adjustedRank - 0.3) / (totalN + 0.4); } - return { ...obs, probability: lastProbability }; + const weibullYVal = Math.log(-Math.log(1 - lastProbability)); + return { + id: `obs-${idx}`, + time, + isCensored, + probability: lastProbability, + y: weibullYVal, + }; }); -// y = ln(-ln(1-F)) linearizes the Weibull CDF so Weibull data plots straight. -const weibullY = (p: number) => Math.log(-Math.log(1 - p)); - -const failurePoints = points.filter((p) => !p.isCensored); -const censoredPoints = points.filter((p) => p.isCensored); +const failurePoints = plottedPoints.filter((p) => !p.isCensored); +const censoredPoints = plottedPoints.filter((p) => p.isCensored); -const failureData = failurePoints.map((p, i) => ({ - x: p.time, - y: weibullY(p.probability), - id: `failure-${i}`, -})); -const censoredData = censoredPoints.map((p, i) => ({ - x: p.time, - y: weibullY(p.probability), - id: `censored-${i}`, -})); - -// Least-squares fit of ln(t) vs y over failures only: y = beta*ln(t) + b, so -// slope = shape parameter (beta) and eta = exp(-intercept / beta). const xs = failurePoints.map((p) => Math.log(p.time)); -const ys = failureData.map((d) => d.y); -const nF = xs.length; -const meanX = xs.reduce((a, b) => a + b, 0) / nF; -const meanY = ys.reduce((a, b) => a + b, 0) / nF; +const ys = failurePoints.map((p) => p.y); +const nFailures = xs.length; +const meanX = xs.reduce((a, b) => a + b, 0) / nFailures; +const meanY = ys.reduce((a, b) => a + b, 0) / nFailures; let sxy = 0; let sxx = 0; -for (let i = 0; i < nF; i += 1) { +for (let i = 0; i < nFailures; i += 1) { sxy += (xs[i] - meanX) * (ys[i] - meanY); sxx += (xs[i] - meanX) ** 2; } @@ -96,142 +95,85 @@ const beta = sxy / sxx; const intercept = meanY - beta * meanX; const eta = Math.exp(-intercept / beta); -const X_MIN = 80; -const X_MAX = 1000; -const fitLineX = [X_MIN, X_MAX]; -const fitLineY = fitLineX.map((x) => beta * Math.log(x) + intercept); - -const yAt632 = weibullY(0.632); +const minObsTime = Math.min(...plottedPoints.map((p) => p.time)); +const maxObsTime = Math.max(...plottedPoints.map((p) => p.time)); +const xMin = minObsTime * X_MIN_FACTOR; +const xMax = maxObsTime * X_MAX_FACTOR; +const xTickValues: number[] = []; +for (let decade = Math.floor(Math.log10(xMin)); decade <= Math.ceil(Math.log10(xMax)); decade += 1) { + for (const m of LOG_TICK_MANTISSAS) { + const tick = m * 10 ** decade; + if (tick >= xMin && tick <= xMax) xTickValues.push(tick); + } +} +const yAtCharLife =Math.log(-Math.log(1 - CHAR_LIFE_PCT / 100)); -// Probability-paper y-axis: fixed percentage gridlines, deliberately NOT -// evenly spaced in y — that nonlinearity is the whole point of the transform. -const PROB_TICKS = [1, 2, 5, 10, 20, 30, 50, 63.2, 80, 90, 95, 99]; -const yTickValues = PROB_TICKS.map((p) => weibullY(p / 100)); -const yTickLabels = new Map( - PROB_TICKS.map((p, i) => [yTickValues[i], `${p}%`]), +const minPlotY = Math.min(...plottedPoints.map((p) => p.y)); +const maxPlotY = Math.max(...plottedPoints.map((p) => p.y)); +const plotYSpan = maxPlotY - minPlotY; +const yMin = Math.min( + minPlotY - plotYSpan * Y_PAD_SHARE, + Math.log(-Math.log(1 - DEFAULT_MIN_PROB_PCT / 100)), +); +const yMax = Math.max( + maxPlotY + plotYSpan * Y_PAD_SHARE, + Math.log(-Math.log(1 - DEFAULT_MAX_PROB_PCT / 100)), ); -const Y_MIN = weibullY(0.01); -const Y_MAX = weibullY(0.99); - -// Custom scatter marker: filled circle for observed failures, hollow ring for -// right-censored (suspended) units — same hue, status told by fill alone. -function StatusMarker({ series, xScale, yScale, color, markerSize }: any) { - const isCensored = series.id === "censored"; - return ( - - {series.data.map((point: any) => ( - - ))} - - ); -} -const TITLE = "probability-weibull · javascript · muix · anyplot.ai"; -const TITLE_FONT_SIZE = - TITLE.length > 67 ? Math.max(15, Math.round((22 * 67) / TITLE.length)) : 22; +const activeProbTicks = PROB_TICKS.filter((p) => { + const wy = Math.log(-Math.log(1 - p / 100)); + return wy >= yMin && wy <= yMax; +}); +const yTickValues = activeProbTicks.map((p) => Math.log(-Math.log(1 - p / 100))); +const yTickLabels = new Map(activeProbTicks.map((p, i) => [yTickValues[i], `${p}%`])); -// Chart margins — the y-axis label below is a hand-placed , not the -// built-in `label` prop, because ChartsYAxis offsets it by a fixed -// `tickFontSize + tickSize + 10` regardless of the actual (custom-formatted) -// tick label width, which collided with "63.2%"-style ticks. -// `top` is tall enough for the hand-rolled 3-row legend below the title. -const MARGIN = { top: 132, right: 64, bottom: 84, left: 140 }; +const fitXStart = Math.max(xMin, Math.exp((yMin - intercept) / beta)); +const fitXEnd = Math.min(xMax, Math.exp((yMax - intercept) / beta)); +const fitLineX = [fitXStart, fitXEnd]; +const fitLineY = fitLineX.map((x) => beta * Math.log(x) + intercept); -// Hand-rolled legend: the built-in ChartsLegend only draws solid-color square -// swatches, which can't reflect the filled-vs-hollow marker encoding used on -// the chart. Swatches here mirror the on-chart marks exactly (dash for the -// fit line, filled circle for failures, hollow ring for suspensions). -const LEGEND_FONT_SIZE = 13; -const LEGEND_ROW_HEIGHT = 22; -const LEGEND_TOP = 60; +const timeUnitMatch = /\(([^)]+)\)/.exec(TIME); +const timeUnit = timeUnitMatch ? timeUnitMatch[1] : ""; +const unitSuffix = timeUnit ? ` ${timeUnit}` : ""; +const fitLegendText = `Fit: β ≈ ${beta.toFixed(2)}, η ≈ ${Math.round(eta)}${unitSuffix}`; +const charLifeCalloutText = `${CHAR_LIFE_PCT}% · η ≈ ${Math.round(eta)}${unitSuffix}`; -function Legend({ right }: { right: number }) { - const legendItems = [ - { - kind: "dash" as const, - label: `Fit: β≈${beta.toFixed(2)}, η≈${Math.round(eta)} kcycles`, - swatchColor: t.ink, - }, - { kind: "filled" as const, label: "Failure", swatchColor: t.palette[0] }, - { - kind: "hollow" as const, - label: "Suspended (censored)", - swatchColor: t.palette[0], - }, - ]; - const legendWidth = Math.max( - ...legendItems.map( - (item) => 22 + item.label.length * LEGEND_FONT_SIZE * 0.58, - ), - ); - const left = right - legendWidth; - return ( - - {legendItems.map((item, i) => { - const y = LEGEND_TOP + i * LEGEND_ROW_HEIGHT; - return ( - - {item.kind === "dash" && ( - - )} - {item.kind === "filled" && ( - - )} - {item.kind === "hollow" && ( - - )} - - {item.label} - - - ); - })} - - ); -} - -// Small elevated callout for the eta/63.2% crossing — a filled rounded box -// behind the label sharpens the focal point beyond a thin dashed line + text. -function ReferenceCallout({ y, label }: { y: number; label: string }) { +function WeibullOverlay() { const { left, width: drawWidth } = useDrawingArea(); + const xScale = useXScale(); const yScale = useYScale(); - const yPixel = yScale(y); - const paddingX = 8; - const boxWidth = label.length * 6.8 + paddingX * 2; - const boxHeight = 22; - const boxX = left + drawWidth - boxWidth; - const boxY = yPixel - boxHeight - 8; + + const yPixelChar = Number(yScale(yAtCharLife)); + const paddingX = 10; + const boxWidth = charLifeCalloutText.length * 7.6 + paddingX * 2; + const boxHeight = 26; + const boxX = left + drawWidth - boxWidth - 10; + const boxY = yPixelChar - boxHeight - 8; + return ( + {failurePoints.map((pt) => ( + + ))} + {censoredPoints.map((pt) => ( + + ))} - {label} + {charLifeCalloutText} ); } -// --- Chart (default-exported component — the harness mounts it) ----------- export default function Chart() { const { width, height } = window.ANYPLOT_SIZE; - const plotMidY = MARGIN.top + (height - MARGIN.top - MARGIN.bottom) / 2; + const chartHeight = height - HEADER_HEIGHT; return ( - `${v}`, - label: "Cycles to Failure (thousands)", - tickLabelStyle: { fontSize: 14 }, - labelStyle: { fontSize: 16 }, - }, - ]} - yAxis={[ - { - scaleType: "linear", - min: Y_MIN, - max: Y_MAX, - tickInterval: yTickValues, - valueFormatter: (v: number) => yTickLabels.get(v) ?? "", - tickLabelStyle: { fontSize: 14 }, - }, - ]} - skipAnimation +
- - - - - - - - - - {TITLE} - - {TITLE}
+
+
+ + {fitLegendText} +
+
+ + {FAILURE_LABEL} +
+
+ + + {CENSORED_LABEL} + +
+
+ + `${Number(v.toPrecision(3))}`, + label: TIME, + tickLabelStyle: { fontSize: 14, fill: t.inkSoft }, + labelStyle: { fontSize: 15, fill: t.ink }, + }, + ]} + yAxis={[ + { + scaleType: "linear", + min: yMin, + max: yMax, + width: 96, + tickInterval: yTickValues, + valueFormatter: (v: number) => yTickLabels.get(v) ?? "", + label: PROB_AXIS_LABEL, + tickLabelStyle: { fontSize: 14, fill: t.inkSoft }, + labelStyle: { fontSize: 15, fill: t.ink }, + }, + ]} + skipAnimation > - Cumulative Failure Probability -
- + + + + + + + + ); } diff --git a/plots/probability-weibull/metadata/javascript/muix.yaml b/plots/probability-weibull/metadata/javascript/muix.yaml index 453af16cc59..288a20c3d77 100644 --- a/plots/probability-weibull/metadata/javascript/muix.yaml +++ b/plots/probability-weibull/metadata/javascript/muix.yaml @@ -2,138 +2,129 @@ library: muix language: javascript specification_id: probability-weibull created: '2026-08-24T08:12:54Z' -updated: '2026-08-24T08:29:17Z' -generated_by: claude-sonnet -workflow_run: 32703903746 +updated: '2026-10-24T12:05:19Z' +generated_by: gemini-4-argon +adopted_from: 5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69 (5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69) +workflow_run: 40091871386 issue: 4668 -language_version: 22.23.2 -library_version: 7.29.1 +language_version: 22.23.3 +library_version: 9.16.0 preview_url_light: https://storage.googleapis.com/anyplot-images/plots/probability-weibull/javascript/muix/plot-light.png preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/probability-weibull/javascript/muix/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/probability-weibull/javascript/muix/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/probability-weibull/javascript/muix/plot-dark.html -quality_score: 92 +quality_score: 87 review: strengths: - - Correctly implements median-rank plotting positions via Johnson's rank-adjustment - method for censored data (not a naive median-rank shortcut) — a genuinely reliability-engineering-accurate - technique for handling suspensions. - - Custom StatusMarker (filled circle for failures, hollow ring for suspensions) - encodes censoring by shape/fill, not color alone — CVD-safe and matches the hand-rolled - legend swatches exactly. - - Reference-line callout at 63.2% cleanly surfaces the fitted characteristic life - (eta) with a real least-squares fit computed in-code (beta and eta both annotated), - not hard-coded values. - - Correct log x-axis and Weibull-linearized y-axis (ln(-ln(1-F))) with percentage - tick labels — the nonlinear, non-evenly-spaced y-grid is exactly what real Weibull - probability paper looks like. - - 'Data colors (brand green #009E73) are identical across light and dark renders; - only chrome (background, grid, ink, callout box) adapts via ANYPLOT_TOKENS.' + - 'Correct Weibull probability paper: log-scaled time axis (scaleType ''log'') and + a linearized ln(-ln(1-F)) y axis with reliability-standard probability ticks (1 + %…99 %, plus 63.2 %) placed via tickInterval and a valueFormatter, so the fit + is a straight line.' + - Plotting positions follow the median rank (i-0.3)/(n+0.4) with Johnson's adjusted + rank for suspensions; the first failure sits at 2.87 % as computed, and the fitted + beta/eta (2.09 / 453 kcycles) agree with the line, which crosses the 63.2 % reference + line at about 450 kcycles. + - Censored observations are hollow brand-green rings against filled failures, clearly + distinct in both themes; all data colors are identical between renders and chrome + flips cleanly. + - 'Uses MUI X v9 composition idiomatically: ChartsContainer with a log x axis, LinePlot + for the fit, ChartsReferenceLine for the 63.2 % line, ChartsGrid, and the useDrawingArea/useXScale/useYScale + hooks to draw the point overlay in data coordinates; skipAnimation is set.' + - Compact header row puts the title on the left and the legend (with the fit's beta + and eta in the fit entry) on the right, leaving the full height to the plot. weaknesses: - - Grid lines (both axes) are on the more visible side rather than a subtle backdrop - — could be lightened/thinned slightly for a cleaner probability-paper look without - losing the reference gridlines' function. - - A few adjacent points (~250 kcycles and ~380-400 kcycles clusters) sit close enough - that their 12px markers nearly touch — not a real overlap, but a touch more separation - or slightly smaller markers in dense clusters would improve individual-point readability. - - 'LM-02 headroom: the implementation already goes beyond the composable ChartContainer - API with custom slots and hooks; a further distinctive touch (e.g. a subtle tooltip - customization visible only in the interactive HTML) would round it out, though - this is a nice-to-have, not a defect.' + - 'Suggestion: anchor the eta callout at the crossing of the fit line and the 63.2 + % reference line (about 453 kcycles) instead of the right edge, so the characteristic + life is read where it occurs.' + - 'Suggestion: the fit line and the reference line are both dashed; a solid fit + line would separate the model from the reference more clearly.' image_description: |- Light render (plot-light.png): - Background: Warm off-white, consistent with #FAF8F1 — not pure white. - Chrome: Bold dark title "probability-weibull · javascript · muix · anyplot.ai" centered at top; x-axis labeled "Cycles to Failure (thousands)" (log scale, ticks 100/200/300/500/700/1000); y-axis labeled "Cumulative Failure Probability" (rotated, hand-placed) with percentage ticks from 1% to 99% on the Weibull-linearized scale; three-row hand-rolled legend top-right (dashed fit line, filled "Failure" swatch, hollow "Suspended (censored)" swatch); horizontal dotted reference line at 63.2% with an elevated callout box reading "63.2% · η ≈ 453 kcycles". All chrome text renders in dark ink against the light background. - Data: Brand green (#009E73) filled circles for failures and hollow green rings for censored/suspended units, roughly following a straight dashed fit line across the log-x / linearized-y plot — exactly the expected "straight line on Weibull paper" signature. First/only categorical series correctly uses palette[0]. - Legibility verdict: PASS — title, axis labels, tick labels, legend text, and the reference callout are all clearly readable against the light background; no light-on-light issues. + Background: warm off-white #FAF8F1 across the whole canvas, no white panel. + Chrome: bold dark title "probability-weibull · javascript · muix · anyplot.ai" top-left (about a third of the width); legend top-right ("Fit: β ≈ 2.09, η ≈ 453 kcycles" dashed line, filled green "Failure", hollow green "Suspended (censored)"); y-axis "Cumulative Failure Probability (%)" with ticks 1 %–99 % plus 63.2 %; x-axis "Cycles to Failure (kcycles)" with log ticks 70–1000; subtle grey grid in both directions. All text dark and readable. + Data: 20 filled #009E73 failure dots and 4 hollow green censored rings along a dark dashed fit line from ~1.8 % at 65 kcycles to 99 % at ~950; thin dotted 63.2 % reference line with a boxed callout "63.2% · η ≈ 453 kcycles" at the right; fit crosses the reference line near 450 kcycles. Nothing clipped or overlapping. + Legibility verdict: PASS Dark render (plot-dark.png): - Background: Warm near-black, consistent with #1A1A17 — not pure black. - Chrome: Identical layout and text content to the light render, now rendered in light ink (title, axis labels, tick labels, legend text, callout text) against the dark background. Grid lines are dim gray, still visible but not dominant. - Data: Same brand green (#009E73) filled/hollow markers and dashed fit line as the light render — data colors are pixel-identical between themes, confirming only chrome flipped. - Legibility verdict: PASS — no dark-on-dark failures observed; every text element (including the small legend and callout labels) remains legible against the near-black surface. + Background: warm near-black #1A1A17. + Chrome: title, legend text, axis titles and tick labels all light ink and readable; grid subtle grey; callout box on elevated dark surface with light text. No dark-on-dark text. + Data: identical #009E73 filled dots and hollow rings at the same positions; fit line flips to light ink as chrome; brand green clearly visible. + Legibility verdict: PASS criteria_checklist: visual_quality: - score: 28 + score: 30 max: 30 items: - id: VQ-01 name: Text Legibility - score: 7 + score: 8 max: 8 passed: true - comment: All font sizes explicit (title 22px, axis labels 16px, tick labels - 14px, legend 13px); readable in both themes with no dark-on-dark or light-on-light - issues. + comment: Title 22 px, axis titles 15 px, ticks and legend 14 px (CSS), all + readable in both themes - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No text/legend/data collisions; legend and callout box sit clear - of the plotted data and each other. + comment: No text collisions; callout box clear of data - id: VQ-03 name: Element Visibility - score: 5 + score: 6 max: 6 passed: true - comment: Markers (12px) are prominent for the 24-point sparse dataset; a couple - of adjacent points in dense clusters (~250, ~380-400 kcycles) sit close - enough to nearly touch. + comment: 24 sparse points at r=7 with page-colored outline; hollow rings distinct; + legend glyphs match marks - id: VQ-04 name: Color Accessibility score: 2 max: 2 passed: true - comment: Failure vs censored distinguished by fill/shape (filled vs hollow), - not color alone — CVD-safe. + comment: Single hue; filled vs hollow carries the censoring distinction, CVD-safe - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: Good proportions, title well within bounds (~35% width at default - size), nothing clipped, generous margins. + comment: 3200x1800; balanced margins, nothing cut off - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: Descriptive axis labels with units (kcycles, cumulative failure probability - %). + comment: Descriptive labels with units (kcycles, %) - id: VQ-07 name: Palette Compliance score: 2 max: 2 passed: true - comment: 'First/only series uses Imprint palette[0] (#009E73); backgrounds - correct #FAF8F1/#1A1A17; both renders theme-correct.' + comment: 'Data in #009E73, fit and reference in ink tokens, correct backgrounds + in both renders' design_excellence: - score: 15 + score: 10 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 5 max: 8 - passed: true - comment: Custom marker slot, hand-rolled legend that mirrors the on-chart - encoding exactly, and an elevated reference callout — deliberate polish - beyond defaults. + passed: false + comment: Header row integrating title and a legend that carries the fit parameters; + clear ink-vs-green hierarchy between model and data - id: DE-02 name: Visual Refinement - score: 4 + score: 3 max: 6 - passed: true - comment: Clean chart with generous whitespace; grid is present on both axes - and is slightly more visible than a minimal-chrome ideal. + passed: false + comment: Probability ticks chosen at reliability-paper positions with the + 63.2 % tick, log ticks at 1-1.5-2-3-5-7 mantissas, generous plot area - id: DE-03 name: Data Storytelling - score: 5 + score: 2 max: 6 - passed: true - comment: The 63.2% reference line + callout creates a clear focal point (characteristic - life), and the fitted dashed line visually anchors the whole story. + passed: false + comment: Displays the spec's annotations; no focal point chosen beyond the + Notes spec_compliance: score: 15 max: 15 @@ -143,31 +134,27 @@ review: score: 5 max: 5 passed: true - comment: 'Correct Weibull probability plot: log-x, linearized Weibull y-axis, - fitted straight line.' + comment: Weibull probability plot on probability paper - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: Log x-axis, linearized y-axis, annotated beta/eta, median-rank plotting - positions (i-0.3)/(n+0.4) via Johnson's rank-adjustment, censored-vs-failure - marker distinction, 63.2% reference line — all present. + comment: Log x, linearized y, median ranks, fit with beta/eta, hollow censored + markers, 63.2 % reference line all present - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: x = time-to-failure, y = weibullY(probability); full data range shown - (80-1000 x-axis, 1%-99% y-axis). + comment: Points at their time and plotting position, no displacement; axes + cover all data - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title exactly matches 'probability-weibull · javascript · muix · - anyplot.ai'; legend labels (Fit, Failure, Suspended (censored)) match the - data. + comment: Mandated title; legend entries readable and correct data_quality: score: 15 max: 15 @@ -177,22 +164,21 @@ review: score: 6 max: 6 passed: true - comment: Shows failures, suspensions, fitted line, and the characteristic-life - reference — all plot-type aspects covered. + comment: Failures, suspensions mixed in, scatter around the fit at both ends - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Turbine blade fatigue-life data with periodic inspection-driven censoring - is plausible and neutral. + comment: Fatigue life in kcycles with suspensions, neutral engineering context - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Kcycles values (roughly 80-1000) are sensible for turbine blade fatigue - testing. + comment: 'Checked: first failure at (1-0.3)/24.4 = 2.87 % matches the render; + beta 2.09 and eta 453 are consistent with the generating Weibull(2.4, 420) + for n=24; fit crosses 63.2 % at about 450' code_quality: score: 10 max: 10 @@ -202,64 +188,64 @@ review: score: 3 max: 3 passed: true - comment: Functions present are React-required (component + small slot helpers) - or clearly justified (LCG, StatusMarker, Legend, ReferenceCallout) — no - gratuitous abstraction. + comment: Flat module-level code; the only extra function is the React overlay + component that needs the scale hooks - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Fixed-seed LCG (seed=42) makes data fully deterministic. + comment: Fixed-seed LCG - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Every import (ChartContainer, ScatterPlot, LinePlot, axes, grid, - reference line, hooks) is used. + comment: All imports used - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: No fake interactivity; comments explain non-obvious design tradeoffs - (custom legend, hand-placed y-label). + comment: Closed-form least squares and rank computation are short and appropriate; + no library call replaces them - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: 'Correct harness contract: default-exported component sized to ANYPLOT_SIZE, - skipAnimation set on ChartContainer and LinePlot.' + comment: Default-exported component, v9 API, skipAnimation set library_mastery: - score: 9 + score: 7 max: 10 items: - id: LM-01 name: Idiomatic Usage - score: 5 + score: 4 max: 5 - passed: true - comment: Uses the composable ChartContainer + individual plot/axis components - pattern (the recommended advanced MUI X API) rather than a monolithic chart - wrapper. + passed: false + comment: v9 composition (ChartsContainer, LinePlot, ChartsGrid, ChartsXAxis/YAxis) + with log scaleType, tickInterval and valueFormatter for probability-paper + ticks - id: LM-02 name: Distinctive Features - score: 4 + score: 3 max: 5 - passed: true - comment: Custom marker via `slots.scatter`, useDrawingArea/useYScale hooks - for the reference callout, ChartsReferenceLine — genuinely distinctive MUI - X usage, though the interactive HTML surface (tooltips) isn't leveraged - further. + passed: false + comment: useDrawingArea/useXScale/useYScale hooks to draw custom SVG marks + in data coordinates, ChartsReferenceLine for the 63.2 % line verdict: APPROVED + model: claude-opus-5-5 + criteria_version: qc-899b86ffde.aqr-869a70087c.sg-c73b4641dd.lib-f7283eb626 + rendered_at: '2026-10-24T12:02:14Z' impl_tags: dependencies: [] techniques: - - annotations + - layer-composition - custom-legend - manual-ticks + - annotations + - html-export patterns: - data-generation dataprep: