{"spec_id":"andrews-curves","library":"echarts","language":"javascript","code":"// anyplot.ai\n// andrews-curves: Andrews Curves for Multivariate Data\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Synthetic iris-like measurements: sepal length/width, petal length/width (cm),\n// plus derived sepal/petal area, per species cluster, generated with a\n// fixed-seed PRNG for reproducibility.\nfunction mulberry32(seed) {\n  let a = seed;\n  return function () {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let z = Math.imul(a ^ (a >>> 15), 1 | a);\n    z = (z + Math.imul(z ^ (z >>> 7), 61 | z)) ^ z;\n    return ((z ^ (z >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(42);\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst SPECIES = [\n  { name: \"setosa\", means: [5.0, 3.4, 1.5, 0.25], stds: [0.35, 0.38, 0.17, 0.11] },\n  { name: \"versicolor\", means: [5.9, 2.8, 4.3, 1.3], stds: [0.52, 0.31, 0.47, 0.2] },\n  { name: \"virginica\", means: [6.6, 3.0, 5.6, 2.0], stds: [0.64, 0.32, 0.55, 0.27] },\n];\nconst SAMPLES_PER_SPECIES = 15;\n\nconst observations = [];\nSPECIES.forEach((species) => {\n  for (let i = 0; i < SAMPLES_PER_SPECIES; i++) {\n    const [sepalLength, sepalWidth, petalLength, petalWidth] = species.means.map(\n      (mean, j) => mean + randNormal() * species.stds[j]\n    );\n    const features = [\n      sepalLength,\n      sepalWidth,\n      petalLength,\n      petalWidth,\n      sepalLength * sepalWidth,\n      petalLength * petalWidth,\n    ];\n    observations.push({ category: species.name, features });\n  }\n});\n\n// Standardize each dimension (z-score) so no single variable dominates the curve\nconst numDims = observations[0].features.length;\nconst dimMeans = [];\nconst dimStds = [];\nfor (let j = 0; j < numDims; j++) {\n  const values = observations.map((o) => o.features[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  dimMeans.push(mean);\n  dimStds.push(Math.sqrt(variance));\n}\nobservations.forEach((o) => {\n  o.z = o.features.map((v, j) => (v - dimMeans[j]) / dimStds[j]);\n});\n\n// Andrews curve: f(t) = z1/sqrt(2) + z2 sin(t) + z3 cos(t) + z4 sin(2t) + ...\nfunction andrewsCurve(z, t) {\n  let value = z[0] / Math.SQRT2;\n  for (let k = 1; k < z.length; k++) {\n    const harmonic = Math.ceil(k / 2);\n    value += k % 2 === 1 ? z[k] * Math.sin(harmonic * t) : z[k] * Math.cos(harmonic * t);\n  }\n  return value;\n}\n\nconst T_STEPS = 120;\nconst tValues = Array.from({ length: T_STEPS + 1 }, (_, i) => -Math.PI + (2 * Math.PI * i) / T_STEPS);\n\nconst categoryColors = { setosa: t.palette[0], versicolor: t.palette[1], virginica: t.palette[2] };\n\n// Mark, per species, the curve closest to its cluster centroid (in z-space) as\n// the representative curve — drawn bolder and more opaque to sharpen the\n// cluster storytelling amid the 45 overplotted curves.\nSPECIES.forEach((species) => {\n  const members = observations.filter((o) => o.category === species.name);\n  const centroid = members[0].z.map((_, j) => members.reduce((sum, o) => sum + o.z[j], 0) / members.length);\n  let closest = members[0];\n  let closestDist = Infinity;\n  members.forEach((o) => {\n    const dist = Math.sqrt(o.z.reduce((sum, v, j) => sum + (v - centroid[j]) ** 2, 0));\n    if (dist < closestDist) {\n      closestDist = dist;\n      closest = o;\n    }\n  });\n  closest.isRepresentative = true;\n});\n\n// Draw representative curves last so they sit on top of the dense overplot.\nconst orderedObservations = [...observations].sort((a, b) => (a.isRepresentative ? 1 : 0) - (b.isRepresentative ? 1 : 0));\n\nconst series = orderedObservations.map((o) => ({\n  name: o.category,\n  type: \"line\",\n  data: tValues.map((tv) => [tv, andrewsCurve(o.z, tv)]),\n  showSymbol: false,\n  lineStyle: o.isRepresentative\n    ? { color: categoryColors[o.category], width: 3.2, opacity: 0.95 }\n    : { color: categoryColors[o.category], width: 1.6, opacity: 0.42 },\n  itemStyle: { color: categoryColors[o.category] },\n  z: o.isRepresentative ? 3 : 1,\n  emphasis: { disabled: true },\n}));\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"Iris Species Clustering · andrews-curves · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 30,\n    textStyle: { color: t.ink, fontSize: 19, fontWeight: 500 },\n  },\n  legend: {\n    data: SPECIES.map((s) => s.name),\n    top: 90,\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 16 },\n    itemWidth: 24,\n    itemHeight: 4,\n  },\n  grid: { left: 100, right: 70, top: 170, bottom: 100 },\n  xAxis: {\n    type: \"value\",\n    name: \"t (Fourier parameter)\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: -Math.PI,\n    max: Math.PI,\n    interval: Math.PI / 2,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: (val) => {\n        const k = val / Math.PI;\n        if (Math.abs(k) < 0.01) return \"0\";\n        if (Math.abs(Math.abs(k) - 1) < 0.01) return k < 0 ? \"-π\" : \"π\";\n        if (Math.abs(Math.abs(k) - 0.5) < 0.01) return k < 0 ? \"-π/2\" : \"π/2\";\n        return val.toFixed(2);\n      },\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"f(t)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series,\n});\n"}