{"spec_id":"andrews-curves","library":"muix","language":"javascript","code":"// anyplot.ai\n// andrews-curves: Andrews Curves for Multivariate Data\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\nimport { LineChart } from \"@mui/x-charts/LineChart\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst TITLE = \"andrews-curves · 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// Iris-like flower measurements (cm): sepal length, sepal width, petal\n// length, petal width. Means/std-devs approximate the classic Iris species\n// so the curves show the same natural clustering the dataset is famous for.\nconst OBS_PER_SPECIES = 20;\nconst SPECIES = [\n  { name: \"Setosa\", seed: 11, means: [5.0, 3.4, 1.5, 0.25], stdDevs: [0.35, 0.38, 0.17, 0.11] },\n  { name: \"Versicolor\", seed: 23, means: [5.9, 2.8, 4.3, 1.33], stdDevs: [0.52, 0.31, 0.47, 0.2] },\n  { name: \"Virginica\", seed: 37, means: [6.6, 3.0, 5.55, 2.03], stdDevs: [0.64, 0.32, 0.55, 0.27] },\n];\n\nconst observations = SPECIES.flatMap((species) => {\n  const rand = lcg(species.seed);\n  return Array.from({ length: OBS_PER_SPECIES }, () => ({\n    species: species.name,\n    values: species.means.map((mean, i) => randomNormal(rand, mean, species.stdDevs[i])),\n  }));\n});\n\n// Normalize each variable to a z-score across the whole dataset so no single\n// measurement (petal length has the widest raw range) dominates the curve.\nconst DIM_COUNT = 4;\nconst dimMeans = Array.from(\n  { length: DIM_COUNT },\n  (_, d) => observations.reduce((sum, obs) => sum + obs.values[d], 0) / observations.length,\n);\nconst dimStdDevs = Array.from({ length: DIM_COUNT }, (_, d) => {\n  const variance =\n    observations.reduce((sum, obs) => sum + (obs.values[d] - dimMeans[d]) ** 2, 0) /\n    (observations.length - 1);\n  return Math.sqrt(variance);\n});\nobservations.forEach((obs) => {\n  obs.normalized = obs.values.map((v, d) => (v - dimMeans[d]) / dimStdDevs[d]);\n});\n\n// Andrews curve Fourier expansion for 4 variables, t in [-π, π]:\n// f(t) = x1/√2 + x2·sin(t) + x3·cos(t) + x4·sin(2t)\nconst T_COUNT = 121;\nconst tGrid = Array.from({ length: T_COUNT }, (_, k) => -Math.PI + (k * 2 * Math.PI) / (T_COUNT - 1));\nfunction andrewsCurve([x1, x2, x3, x4], tValue) {\n  return x1 / Math.SQRT2 + x2 * Math.sin(tValue) + x3 * Math.cos(tValue) + x4 * Math.sin(2 * tValue);\n}\nobservations.forEach((obs) => {\n  obs.curve = tGrid.map((tValue) => andrewsCurve(obs.normalized, tValue));\n});\n\nfunction hexToRgba(hex, alpha) {\n  const value = parseInt(hex.slice(1), 16);\n  const r = (value >> 16) & 255;\n  const g = (value >> 8) & 255;\n  const b = value & 255;\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// Individual curves stay unlabeled (60 legend entries would be unreadable);\n// each species instead gets one bold, fully-opaque mean curve below that\n// carries the legend label and doubles as a representative summary line.\nconst series = observations.map((obs, i) => {\n  const speciesIndex = SPECIES.findIndex((species) => species.name === obs.species);\n  return {\n    id: `${obs.species}-${i}`,\n    data: obs.curve,\n    color: hexToRgba(t.palette[speciesIndex], 0.4),\n    curve: \"natural\",\n    showMark: false,\n  };\n});\n\n// Per-species mean curve: the pointwise average of that species' 20 curves,\n// rendered bold and solid so the cluster's overall shape reads at a glance\n// through the alpha-blended cloud of individual observations.\nconst meanSeries = SPECIES.map((species, speciesIndex) => {\n  const curves = observations.filter((obs) => obs.species === species.name).map((obs) => obs.curve);\n  const meanCurve = tGrid.map(\n    (_, k) => curves.reduce((sum, curve) => sum + curve[k], 0) / curves.length,\n  );\n  return {\n    id: `${species.name}-mean`,\n    data: meanCurve,\n    color: t.palette[speciesIndex],\n    curve: \"natural\",\n    showMark: false,\n    label: species.name,\n  };\n});\n\n// CSS hook selecting only the three bold mean-curve lines, so they render\n// heavier than the alpha-blended individual observations behind them.\nconst meanLineSelector = SPECIES.map(\n  (species) => `& .MuiLineElement-series-${species.name}-mean`,\n).join(\", \");\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 sx={{ height: TITLE_HEIGHT, display: \"flex\", alignItems: \"center\", px: \"40px\" }}>\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        grid={{ horizontal: true }}\n        axisHighlight={{ x: \"line\", y: \"none\" }}\n        xAxis={[\n          {\n            data: tGrid,\n            scaleType: \"linear\",\n            label: \"t (Fourier Parameter)\",\n            tickMinStep: 0.5,\n            valueFormatter: (v) => v.toFixed(2),\n          },\n        ]}\n        yAxis={[\n          {\n            label: \"f(t)\",\n            valueFormatter: (v, context) =>\n              context.location === \"tick\" ? v.toFixed(2) : `f(t) = ${v.toFixed(2)}`,\n          },\n        ]}\n        series={[...series, ...meanSeries]}\n        margin={{ top: 24, bottom: 110, left: 90, right: 40 }}\n        sx={{\n          \"& .MuiLineElement-root\": { strokeWidth: 1.75 },\n          [meanLineSelector]: { strokeWidth: 3 },\n          \"& .MuiChartsAxisHighlight-root\": { stroke: t.inkSoft, strokeDasharray: \"4 3\" },\n          \"& .MuiChartsAxis-tickLabel\": { fontSize: \"14px\" },\n          \"& .MuiChartsAxis-label\": { fontSize: \"16px\" },\n          \"& .MuiChartsAxis-line\": { stroke: t.grid },\n          \"& .MuiChartsAxis-tick\": { stroke: t.grid },\n          \"& .MuiChartsLegend-label\": { fontSize: \"15px\" },\n          \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeWidth: 0.75 },\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    </Box>\n  );\n}\n"}