{"spec_id":"andrews-curves","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// andrews-curves: Andrews Curves for Multivariate Data\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (Box-Muller over a fixed-seed LCG) -----------------\nlet seed = 42;\nfunction uniform() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction normal(mean, std) {\n  const u1 = uniform() || 1e-9;\n  const u2 = uniform();\n  return mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// --- Data: wines from three regions, five chemistry measurements per wine --\nconst REGIONS = [\n  {\n    name: \"Bordeaux\",\n    acidity: [6.5, 0.5],\n    sugar: [2.2, 0.4],\n    alcohol: [12.8, 0.4],\n    pH: [3.3, 0.08],\n    tannin: [7.4, 0.5],\n  },\n  {\n    name: \"Rioja\",\n    acidity: [7.8, 0.6],\n    sugar: [3.0, 0.5],\n    alcohol: [13.5, 0.5],\n    pH: [3.5, 0.1],\n    tannin: [5.6, 0.5],\n  },\n  {\n    name: \"Chianti\",\n    acidity: [7.1, 0.5],\n    sugar: [4.4, 0.7],\n    alcohol: [12.2, 0.35],\n    pH: [3.4, 0.07],\n    tannin: [6.5, 0.45],\n  },\n];\nconst WINES_PER_REGION = 20;\n\nconst wines = [];\nREGIONS.forEach((region) => {\n  for (let i = 0; i < WINES_PER_REGION; i++) {\n    wines.push({\n      region: region.name,\n      acidity: normal(...region.acidity),\n      sugar: normal(...region.sugar),\n      alcohol: normal(...region.alcohol),\n      pH: normal(...region.pH),\n      tannin: normal(...region.tannin),\n    });\n  }\n});\n\n// Standardize each variable (z-score) so no dimension dominates the curve.\nconst VARS = [\"acidity\", \"sugar\", \"alcohol\", \"pH\", \"tannin\"];\nconst stats = {};\nVARS.forEach((v) => {\n  const values = wines.map((w) => w[v]);\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  stats[v] = { mean, std: Math.sqrt(variance) };\n});\nwines.forEach((w) => {\n  VARS.forEach((v) => {\n    w[`z_${v}`] = (w[v] - stats[v].mean) / stats[v].std;\n  });\n});\n\n// --- Andrews curve: f(t) = x1/√2 + x2·sin(t) + x3·cos(t) + x4·sin(2t) + x5·cos(2t)\nconst N_POINTS = 121;\nconst T_MIN = -Math.PI;\nconst T_MAX = Math.PI;\nfunction andrewsCurve(w) {\n  const points = [];\n  for (let i = 0; i < N_POINTS; i++) {\n    const tt = T_MIN + ((T_MAX - T_MIN) * i) / (N_POINTS - 1);\n    const f =\n      w.z_acidity / Math.SQRT2 +\n      w.z_sugar * Math.sin(tt) +\n      w.z_alcohol * Math.cos(tt) +\n      w.z_pH * Math.sin(2 * tt) +\n      w.z_tannin * Math.cos(2 * tt);\n    points.push({ x: tt, y: f });\n  }\n  return points;\n}\n\n// Per-region average curve (bold overlay) — the mean of each region's\n// standardized variables traces the curve a \"typical\" wine from that region\n// would produce, making the cluster separation an explicit visual claim\n// instead of something the reader has to infer from 60 overlapping lines.\nconst regionMeans = {};\nREGIONS.forEach((region) => {\n  const regionWines = wines.filter((w) => w.region === region.name);\n  const mean = {};\n  VARS.forEach((v) => {\n    mean[`z_${v}`] = regionWines.reduce((a, w) => a + w[`z_${v}`], 0) / regionWines.length;\n  });\n  regionMeans[region.name] = mean;\n});\n\nfunction withAlpha(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\nconst regionColor = {\n  Bordeaux: t.palette[0],\n  Rioja: t.palette[1],\n  Chianti: t.palette[2],\n};\n\nconst individualDatasets = wines.map((w) => ({\n  label: w.region,\n  data: andrewsCurve(w),\n  borderColor: withAlpha(regionColor[w.region], 0.32),\n  borderWidth: 1.1,\n  pointRadius: 0,\n  tension: 0,\n  fill: false,\n  order: 0,\n}));\n\n// Bold, fully-opaque region-mean curves drawn on top (higher `order`) of the\n// translucent individual curves, and placed first so the legend-dedup filter\n// below picks their solid swatch instead of a faint individual one.\nconst meanDatasets = REGIONS.map((region) => ({\n  label: region.name,\n  data: andrewsCurve(regionMeans[region.name]),\n  borderColor: regionColor[region.name],\n  borderWidth: 3.5,\n  pointRadius: 0,\n  tension: 0,\n  fill: false,\n  order: 1,\n}));\n\nconst datasets = [...meanDatasets, ...individualDatasets];\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"andrews-curves · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Bold lines trace each region's average wine — Rioja's lower tannin visibly separates its curve from Bordeaux and Chianti\",\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 12 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          filter: (item, data) =>\n            data.datasets.findIndex((d) => d.label === item.text) === item.datasetIndex,\n        },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: T_MIN,\n        max: T_MAX,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          stepSize: Math.PI / 2,\n          callback: (value) => {\n            const ratio = value / Math.PI;\n            if (Math.abs(ratio) < 0.01) return \"0\";\n            if (Math.abs(ratio - 1) < 0.01) return \"π\";\n            if (Math.abs(ratio + 1) < 0.01) return \"-π\";\n            if (Math.abs(ratio - 0.5) < 0.01) return \"π/2\";\n            if (Math.abs(ratio + 0.5) < 0.01) return \"-π/2\";\n            return \"\";\n          },\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"t (radians)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Andrews curve f(t)\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n});\n"}