{"spec_id":"andrews-curves","library":"d3","language":"javascript","code":"// anyplot.ai\n// andrews-curves: Andrews Curves for Multivariate Data\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 210, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: synthetic iris-like measurements (6 variables, 3 species) -------\n// Deterministic LCG (the browser has no seeded Math.random) drives a\n// Box-Muller transform so each species clusters around realistic means.\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\nfunction gaussian(mean, std) {\n  const u1 = lcg() || 1e-9;\n  const u2 = lcg();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\n// 6 variables per observation: the 4 classic iris measurements plus 2 derived\n// area measurements (sepal_area, petal_area), giving the Andrews expansion\n// fuller use of its 4-8 recommended dimensionality.\nconst species = [\n  {\n    name: \"setosa\",\n    n: 30,\n    means: [5.0, 3.4, 1.5, 0.2, 17.0, 0.3],\n    stds: [0.35, 0.38, 0.17, 0.11, 1.8, 0.15],\n  },\n  {\n    name: \"versicolor\",\n    n: 30,\n    means: [5.9, 2.8, 4.3, 1.3, 16.5, 5.6],\n    stds: [0.52, 0.31, 0.47, 0.2, 2.2, 1.0],\n  },\n  {\n    name: \"virginica\",\n    n: 30,\n    means: [6.6, 3.0, 5.6, 2.0, 19.8, 11.2],\n    stds: [0.64, 0.32, 0.55, 0.27, 2.6, 1.8],\n  },\n];\n\nconst observations = [];\nfor (const sp of species) {\n  for (let i = 0; i < sp.n; i++) {\n    observations.push({\n      species: sp.name,\n      values: sp.means.map((m, j) => gaussian(m, sp.stds[j])),\n    });\n  }\n}\n\n// Standardize each variable (z-score) so no single measurement dominates\nconst dims = observations[0].values.length;\nfor (let j = 0; j < dims; j++) {\n  const col = observations.map((o) => o.values[j]);\n  const mean = d3.mean(col);\n  const std = d3.deviation(col);\n  observations.forEach((o) => (o.values[j] = (o.values[j] - mean) / std));\n}\n\n// --- Andrews curve: x1/sqrt(2) + x2 sin(t) + x3 cos(t) + x4 sin(2t) + ... --\n// General Fourier expansion so any number of standardized variables (here 6)\n// contributes alternating sin/cos terms at increasing frequency.\nfunction andrews(tt, v) {\n  let f = v[0] / Math.SQRT2;\n  for (let k = 1; k < v.length; k++) {\n    const freq = Math.ceil(k / 2);\n    f += k % 2 === 1 ? v[k] * Math.sin(freq * tt) : v[k] * Math.cos(freq * tt);\n  }\n  return f;\n}\n\nconst N_SAMPLES = 120;\nconst tSamples = d3.range(N_SAMPLES + 1).map((i) => -Math.PI + (2 * Math.PI * i) / N_SAMPLES);\n\nconst curves = observations.map((o) => ({\n  species: o.species,\n  points: tSamples.map((tt) => ({ t: tt, value: andrews(tt, o.values) })),\n}));\n\n// Per-species mean curve (Andrews is linear in v, so this is the curve of the\n// mean vector) drawn bolder on top, giving the dense central overlap a clear\n// visual-hierarchy anchor beyond color alone.\nconst meanCurves = species.map((sp) => {\n  const spValues = observations.filter((o) => o.species === sp.name).map((o) => o.values);\n  const meanValues = d3.range(dims).map((j) => d3.mean(spValues, (v) => v[j]));\n  return {\n    species: sp.name,\n    points: tSamples.map((tt) => ({ t: tt, value: andrews(tt, meanValues) })),\n  };\n});\n\nconst yExtent = d3.extent(curves.flatMap((c) => c.points.map((p) => p.value)));\nconst yPad = (yExtent[1] - yExtent[0]) * 0.08;\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([-Math.PI, Math.PI]).range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([yExtent[0] - yPad, yExtent[1] + yPad])\n  .nice()\n  .range([ih, 0]);\nconst color = d3\n  .scaleOrdinal()\n  .domain(species.map((s) => s.name))\n  .range(t.palette);\n\n// --- Gridlines (y-axis only) -------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Axes -----------------------------------------------------------------\nconst piTicks = [-Math.PI, -Math.PI / 2, 0, Math.PI / 2, Math.PI];\nconst piLabels = [\"-π\", \"-π/2\", \"0\", \"π/2\", \"π\"];\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .tickValues(piTicks)\n      .tickFormat((d, i) => piLabels[i])\n      .tickSizeOuter(0)\n  );\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickSizeOuter(0));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\".tick line\").remove();\nyAxis.selectAll(\".tick line\").remove();\n\n// --- Axis labels -------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 58)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"t (Fourier parameter)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -72)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"f(t)\");\n\n// --- Curves --------------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.t))\n  .y((d) => y(d.value));\n\ng.selectAll(\"path.curve\")\n  .data(curves)\n  .join(\"path\")\n  .attr(\"class\", \"curve\")\n  .attr(\"d\", (d) => line(d.points))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (d) => color(d.species))\n  .attr(\"stroke-width\", 1.1)\n  .attr(\"stroke-opacity\", 0.35);\n\n// Bolder mean curves on top, one per species, for visual hierarchy.\ng.selectAll(\"path.mean-curve\")\n  .data(meanCurves)\n  .join(\"path\")\n  .attr(\"class\", \"mean-curve\")\n  .attr(\"d\", (d) => line(d.points))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (d) => color(d.species))\n  .attr(\"stroke-width\", 3)\n  .attr(\"stroke-opacity\", 0.9);\n\n// --- Legend ------------------------------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 40},${margin.top + 20})`);\nspecies.forEach((sp, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 34})`);\n  row\n    .append(\"rect\")\n    .attr(\"width\", 18)\n    .attr(\"height\", 18)\n    .attr(\"rx\", 3)\n    .attr(\"fill\", color(sp.name));\n  row\n    .append(\"text\")\n    .attr(\"x\", 26)\n    .attr(\"y\", 14)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(sp.name[0].toUpperCase() + sp.name.slice(1));\n});\n\n// --- Title ---------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"andrews-curves · javascript · d3 · anyplot.ai\");\n"}