{"spec_id":"line-retention-cohort","library":"muix","language":"javascript","code":"// anyplot.ai\n// line-retention-cohort: User Retention Curve by Cohort\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-20\n//# anyplot-orientation: landscape\n// anyplot.ai\n// line-retention-cohort: User Retention Curve by Cohort\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-20\n\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Deterministic LCG — reproducible cohort noise without a seeded global RNG\nfunction makeLcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\n// SaaS product: 5 monthly signup cohorts tracked weekly for 12 weeks\nconst COHORTS = [\n  { label: \"Jan 2025\", size: 1245 },\n  { label: \"Feb 2025\", size: 1089 },\n  { label: \"Mar 2025\", size: 1312 },\n  { label: \"Apr 2025\", size:  987 },\n  { label: \"May 2025\", size: 1567 },\n];\n\nconst WEEKS = Array.from({ length: 12 }, (_, i) => i);\n\n// Exponential decay toward a long-term retention floor, with small realistic jitter\nfunction buildCurve(rng, floor) {\n  return WEEKS.map((w) => {\n    if (w === 0) return 100;\n    const smooth = floor + (100 - floor) * Math.exp(-0.45 * w);\n    const jitter = (rng() - 0.5) * 1.6;\n    return Math.round(Math.max(floor - 2, smooth + jitter) * 10) / 10;\n  });\n}\n\nconst rng = makeLcg(42);\n// Newer cohorts retain better — product improving month over month\nconst retentionData = COHORTS.map((_, i) => buildCurve(rng, 17 + i * 3.5));\n\n// Visual hierarchy: older cohorts fade back, newer stand out\nconst LINE_WIDTHS   = [1.5, 1.75, 2.0, 2.25, 2.75];\nconst LINE_OPACITIES = [0.45, 0.6, 0.75, 0.875, 1.0];\n\n// Imprint palette: first series (#009E73) through position 5 via t.palette\nconst SERIES = COHORTS.map((c, i) => ({\n  id: `cohort-${i}`,\n  data: retentionData[i],\n  label: `${c.label}  (n = ${c.size.toLocaleString()})`,\n  color: t.palette[i],\n  showMark: false,\n  curve: \"monotoneX\",\n}));\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const titleHeight = 52;\n  return (\n    <Box sx={{ width, height, display: \"flex\", flexDirection: \"column\", pt: \"16px\" }}>\n      <Typography\n        sx={{\n          color: t.ink,\n          fontSize: 22,\n          fontWeight: 500,\n          textAlign: \"center\",\n          lineHeight: 1.2,\n          mb: \"4px\",\n          fontFamily: \"inherit\",\n        }}\n      >\n        line-retention-cohort · javascript · muix · anyplot.ai\n      </Typography>\n      <LineChart\n        width={width}\n        height={height - titleHeight}\n        skipAnimation\n        series={SERIES}\n        xAxis={[{\n          data: WEEKS,\n          label: \"Weeks since signup\",\n          tickMinStep: 1,\n          labelStyle: { fontSize: 16, fill: t.inkSoft },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          disableLine: true,\n          disableTicks: true,\n        }]}\n        yAxis={[{\n          min: 0,\n          max: 100,\n          label: \"Retained users (%)\",\n          labelStyle: { fontSize: 16, fill: t.inkSoft },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n          tickMinStep: 10,\n          disableLine: true,\n          disableTicks: true,\n        }]}\n        grid={{ horizontal: true }}\n        margin={{ top: 60, right: 240, bottom: 80, left: 90 }}\n        sx={{\n          \"& .MuiLineElement-series-cohort-0\": { strokeWidth: LINE_WIDTHS[0], opacity: LINE_OPACITIES[0] },\n          \"& .MuiLineElement-series-cohort-1\": { strokeWidth: LINE_WIDTHS[1], opacity: LINE_OPACITIES[1] },\n          \"& .MuiLineElement-series-cohort-2\": { strokeWidth: LINE_WIDTHS[2], opacity: LINE_OPACITIES[2] },\n          \"& .MuiLineElement-series-cohort-3\": { strokeWidth: LINE_WIDTHS[3], opacity: LINE_OPACITIES[3] },\n          \"& .MuiLineElement-series-cohort-4\": { strokeWidth: LINE_WIDTHS[4], opacity: LINE_OPACITIES[4] },\n          \"& .MuiChartsGrid-line\": { opacity: 0.22, strokeDasharray: \"3 3\" },\n        }}\n        slotProps={{\n          legend: {\n            position: { vertical: \"middle\", horizontal: \"right\" },\n            direction: \"column\",\n            padding: { left: 16, top: 0 },\n            itemMarkWidth: 20,\n            itemMarkHeight: 3,\n          },\n        }}\n      >\n        <ChartsReferenceLine\n          y={20}\n          label=\"20% retention target\"\n          lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 4\", strokeWidth: 1.5 }}\n          labelStyle={{ fill: t.inkSoft, fontSize: 14 }}\n          labelAlign=\"end\"\n        />\n      </LineChart>\n    </Box>\n  );\n}\n"}