{"spec_id":"frequency-polygon-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// frequency-polygon-basic: Frequency Polygon for Distribution Comparison\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-02\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"frequency-polygon-basic · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 56;\n\n// --- Data (in-memory, deterministic LCG — no seeded RNG in the browser) -----\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\n\nfunction randomNormal(rand, mean, stdDev) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * stdDev;\n}\n\n// Draw from a mixture of normals: each call picks a component by weight,\n// then samples from it. Used for the sleep-deprived condition to model\n// occasional attentional lapses (microsleeps) on top of normal responses,\n// producing a right-skewed / mildly bimodal shape.\nfunction randomMixture(rand, components) {\n  const pick = rand();\n  let cumulative = 0;\n  for (const component of components) {\n    cumulative += component.weight;\n    if (pick <= cumulative) {\n      return randomNormal(rand, component.mean, component.stdDev);\n    }\n  }\n  const last = components[components.length - 1];\n  return randomNormal(rand, last.mean, last.stdDev);\n}\n\n// Simple visual-stimulus reaction times (ms) across three experimental\n// conditions in a psychology lab study.\nconst SAMPLE_SIZE = 300;\nconst CONDITIONS = [\n  { label: \"Control\", seed: 11, components: [{ weight: 1, mean: 320, stdDev: 40 }] },\n  { label: \"Caffeine\", seed: 23, components: [{ weight: 1, mean: 280, stdDev: 35 }] },\n  {\n    // Mostly normal responses, plus a lapse-trial subgroup (microsleeps)\n    // that pulls the tail out to the right and creates a second mode.\n    label: \"Sleep-deprived\",\n    seed: 37,\n    components: [\n      { weight: 0.68, mean: 350, stdDev: 35 },\n      { weight: 0.32, mean: 475, stdDev: 45 },\n    ],\n  },\n];\nconst samplesByCondition = CONDITIONS.map((condition) => {\n  const rand = lcg(condition.seed);\n  return Array.from({ length: SAMPLE_SIZE }, () =>\n    Math.max(150, randomMixture(rand, condition.components)),\n  );\n});\nconst meanByCondition = samplesByCondition.map(\n  (samples) => samples.reduce((sum, value) => sum + value, 0) / samples.length,\n);\n\n// Shared bin edges across all conditions so the polygons stay comparable.\nconst allTimes = samplesByCondition.flat();\nconst dataMin = Math.min(...allTimes);\nconst dataMax = Math.max(...allTimes);\nconst BIN_COUNT = 18;\nconst binWidth = (dataMax - dataMin) / BIN_COUNT;\nconst binEdges = Array.from({ length: BIN_COUNT + 1 }, (_, i) => dataMin + i * binWidth);\nconst binMidpoints = Array.from(\n  { length: BIN_COUNT },\n  (_, i) => (binEdges[i] + binEdges[i + 1]) / 2,\n);\n\nfunction histogram(samples) {\n  const counts = new Array(BIN_COUNT).fill(0);\n  samples.forEach((value) => {\n    const index = Math.min(BIN_COUNT - 1, Math.floor((value - dataMin) / binWidth));\n    counts[Math.max(0, index)] += 1;\n  });\n  return counts;\n}\nconst countsByCondition = samplesByCondition.map(histogram);\n\n// Extend each polygon one bin-width past the outer midpoints at zero so the\n// outline closes at the baseline instead of hanging in mid-air.\nconst reactionTimes = [\n  binMidpoints[0] - binWidth,\n  ...binMidpoints,\n  binMidpoints[BIN_COUNT - 1] + binWidth,\n];\nconst frequenciesByCondition = countsByCondition.map((counts) => [0, ...counts, 0]);\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const chartHeight = height - TITLE_HEIGHT;\n\n  return (\n    <Box sx={{ width, height, bgcolor: t.pageBg }}>\n      <Box\n        sx={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          px: \"40px\",\n        }}\n      >\n        <Typography sx={{ color: t.ink, fontSize: \"22px\", fontWeight: 600, lineHeight: 1 }}>\n          {TITLE}\n        </Typography>\n      </Box>\n      <LineChart\n        width={width}\n        height={chartHeight}\n        skipAnimation\n        colors={t.palette.slice(0, CONDITIONS.length)}\n        grid={{ horizontal: true }}\n        xAxis={[\n          {\n            data: reactionTimes,\n            scaleType: \"linear\",\n            label: \"Reaction Time (ms)\",\n            valueFormatter: (v) => Math.round(v).toString(),\n            disableTicks: true,\n          },\n        ]}\n        yAxis={[\n          {\n            min: 0,\n            label: \"Frequency\",\n            disableTicks: true,\n          },\n        ]}\n        series={CONDITIONS.map((condition, index) => ({\n          id: condition.label,\n          data: frequenciesByCondition[index],\n          label: condition.label,\n          curve: \"linear\",\n          area: true,\n          showMark: true,\n        }))}\n        margin={{ top: 24, bottom: 100, left: 90, right: 40 }}\n        sx={{\n          \"& .MuiAreaElement-root\": { fillOpacity: 0.14 },\n          \"& .MuiLineElement-root\": { strokeWidth: 3 },\n          \"& .MuiMarkElement-root\": { strokeWidth: 2, r: 3.5 },\n          \"& .MuiChartsAxis-tickLabel\": { fontSize: \"14px\" },\n          \"& .MuiChartsAxis-label\": { fontSize: \"16px\" },\n          \"& .MuiChartsAxis-line\": { stroke: t.grid },\n          \"& .MuiChartsLegend-label\": { fontSize: \"15px\" },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeWidth: 0.75 },\n          \"& .MuiChartsReferenceLine-line\": { strokeDasharray: \"4 4\", strokeWidth: 1.5 },\n          \"& .MuiChartsReferenceLine-label\": { fontSize: \"11px\" },\n        }}\n        slotProps={{\n          legend: {\n            position: { vertical: \"bottom\", horizontal: \"middle\" },\n            itemMarkWidth: 20,\n            itemMarkHeight: 4,\n            padding: { top: 20 },\n          },\n        }}\n      >\n        {CONDITIONS.map((condition, index) => (\n          <ChartsReferenceLine\n            key={condition.label}\n            x={meanByCondition[index]}\n            lineStyle={{ stroke: t.palette[index] }}\n            labelStyle={{ fill: t.palette[index] }}\n            label={`${condition.label} mean`}\n            labelAlign=\"start\"\n          />\n        ))}\n      </LineChart>\n    </Box>\n  );\n}\n"}