{"spec_id":"andrews-curves","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// andrews-curves: Andrews Curves for Multivariate Data\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fixed-seed LCG — the browser has no seeded RNG.\nlet lcgState = 42;\nfunction lcgRandom() {\n  lcgState = (lcgState * 1664525 + 1013904223) % 4294967296;\n  return lcgState / 4294967296;\n}\nfunction randomNormal(mean, std) {\n  const u1 = Math.max(lcgRandom(), 1e-9);\n  const u2 = lcgRandom();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\n// Three flower-measurement clusters (sepal length/width, petal length/width),\n// modeled on the classic iris relationships: petals separate the clusters\n// far more cleanly than sepals.\nconst groups = [\n  { name: \"Cluster A\", n: 20, means: [5.0, 3.4, 1.5, 0.25], stds: [0.35, 0.38, 0.17, 0.1] },\n  { name: \"Cluster B\", n: 20, means: [5.9, 2.8, 4.3, 1.3], stds: [0.51, 0.31, 0.47, 0.2] },\n  { name: \"Cluster C\", n: 20, means: [6.6, 3.0, 5.6, 2.0], stds: [0.64, 0.32, 0.55, 0.27] },\n];\n\nconst observations = [];\ngroups.forEach((group) => {\n  for (let i = 0; i < group.n; i++) {\n    const row = group.means.map((mean, j) => randomNormal(mean, group.stds[j]));\n    observations.push({ group: group.name, row });\n  }\n});\n\n// Standardize each variable (z-score) across the full pool so no single\n// measurement dominates the Fourier expansion.\nconst numVars = groups[0].means.length;\nconst columnStats = [];\nfor (let j = 0; j < numVars; j++) {\n  const values = observations.map((o) => o.row[j]);\n  const mean = values.reduce((a, b) => a + b, 0) / values.length;\n  const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;\n  columnStats.push({ mean, std: Math.sqrt(variance) });\n}\nobservations.forEach((o) => {\n  o.z = o.row.map((v, j) => (v - columnStats[j].mean) / columnStats[j].std);\n});\n\n// --- Andrews curve transform -------------------------------------------------\n// f(t) = x1/sqrt(2) + x2*sin(t) + x3*cos(t) + x4*sin(2t)\nfunction andrewsCurve(z, tt) {\n  return (\n    z[0] / Math.sqrt(2) +\n    z[1] * Math.sin(tt) +\n    z[2] * Math.cos(tt) +\n    z[3] * Math.sin(2 * tt)\n  );\n}\n\nconst numSamples = 100;\nconst tStep = (2 * Math.PI) / (numSamples - 1);\n\nfunction curvePoints(z) {\n  const data = [];\n  for (let k = 0; k < numSamples; k++) {\n    const tt = -Math.PI + k * tStep;\n    data.push([tt, andrewsCurve(z, tt)]);\n  }\n  return data;\n}\n\n// Faint individual curves establish the density texture; they carry no\n// legend entry since the bold centroid curve below speaks for the group.\nconst individualSeries = observations.map((o) => ({\n  type: \"line\",\n  name: o.group,\n  data: curvePoints(o.z),\n  color: t.palette[groups.findIndex((g) => g.name === o.group)],\n  opacity: 0.3,\n  lineWidth: 1,\n  showInLegend: false,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n}));\n\n// Bold per-cluster centroid curve — a deliberate visual anchor that keeps\n// each group legible (and carries the legend) even where individual curves\n// braid together in the densest overlap band.\nconst centroidSeries = groups.map((group, groupIndex) => {\n  const members = observations.filter((o) => o.group === group.name);\n  const centroidZ = columnStats.map(\n    (_, j) => members.reduce((sum, o) => sum + o.z[j], 0) / members.length\n  );\n  return {\n    type: \"line\",\n    name: group.name,\n    data: curvePoints(centroidZ),\n    color: t.palette[groupIndex],\n    lineWidth: 3,\n    zIndex: 5,\n    showInLegend: true,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n  };\n});\n\nconst series = [...individualSeries, ...centroidSeries];\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    zoomType: \"x\",\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"andrews-curves · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Standardized flower measurements as Fourier curves — similar observations trace similar shapes\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    title: {\n      text: \"t (Fourier parameter, -π to π)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    min: -Math.PI,\n    max: Math.PI,\n    tickPositions: [-Math.PI, -Math.PI / 2, 0, Math.PI / 2, Math.PI],\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter() {\n        const labels = { [-Math.PI]: \"-π\", [-Math.PI / 2]: \"-π/2\", 0: \"0\", [Math.PI / 2]: \"π/2\", [Math.PI]: \"π\" };\n        return labels[this.value] ?? this.value.toFixed(2);\n      },\n    },\n    lineWidth: 0,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n  },\n  yAxis: {\n    title: {\n      text: \"f(t) (curve value)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    lineWidth: 0,\n    gridLineColor: t.grid,\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false, states: { hover: { enabled: false } } },\n  },\n  series,\n});\n"}