{"spec_id":"indicator-sma","library":"d3","language":"javascript","code":"// anyplot.ai\n// indicator-sma: Simple Moving Average (SMA) Indicator Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 70, bottom: 70, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: one year of daily closes (fixed-seed LCG random walk) + SMA overlays\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\nfunction gaussian() {\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 PERIODS = 252;\nconst dates = [];\nconst cursor = new Date(2024, 0, 2);\nwhile (dates.length < PERIODS) {\n  const weekday = cursor.getDay();\n  if (weekday !== 0 && weekday !== 6) dates.push(new Date(cursor));\n  cursor.setDate(cursor.getDate() + 1);\n}\n\nconst closes = [];\nlet price = 148;\nfor (let i = 0; i < PERIODS; i++) {\n  price *= 1 + 0.0006 + 0.014 * gaussian();\n  closes.push(price);\n}\n\nfunction movingAverage(values, period) {\n  return values.map((_, i) => {\n    if (i < period - 1) return null;\n    let sum = 0;\n    for (let j = i - period + 1; j <= i; j++) sum += values[j];\n    return sum / period;\n  });\n}\n\nconst series = [\n  { key: \"close\", label: \"Close\", values: closes, color: t.palette[0], width: 3.5 },\n  { key: \"sma20\", label: \"SMA 20\", values: movingAverage(closes, 20), color: t.palette[1], width: 2.5 },\n  { key: \"sma50\", label: \"SMA 50\", values: movingAverage(closes, 50), color: t.palette[2], width: 2.5 },\n  { key: \"sma200\", label: \"SMA 200\", values: movingAverage(closes, 200), color: t.palette[3], width: 2.5 },\n];\n\n// --- Crossover detection (SMA 20 x SMA 50, D3-computed golden/death cross) --\nfunction findCrossover(a, b) {\n  for (let i = 1; i < a.length; i++) {\n    if (a[i - 1] === null || b[i - 1] === null || a[i] === null || b[i] === null) continue;\n    const prevDiff = a[i - 1] - b[i - 1];\n    const currDiff = a[i] - b[i];\n    if (prevDiff === 0) continue;\n    if ((prevDiff < 0 && currDiff >= 0) || (prevDiff > 0 && currDiff <= 0)) {\n      const frac = prevDiff / (prevDiff - currDiff);\n      return {\n        date: new Date(dates[i - 1].getTime() + frac * (dates[i].getTime() - dates[i - 1].getTime())),\n        value: a[i - 1] + frac * (a[i] - a[i - 1]),\n        bullish: prevDiff < 0,\n      };\n    }\n  }\n  return null;\n}\nconst crossover = findCrossover(series[1].values, series[2].values);\n\n// --- Scales -------------------------------------------------------------\nconst x = d3.scaleTime().domain(d3.extent(dates)).range([0, iw]);\nconst allValues = series.flatMap((s) => s.values).filter((v) => v !== null);\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min(allValues) * 0.97, d3.max(allValues) * 1.03])\n  .nice()\n  .range([ih, 0]);\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// --- Gridlines (y-axis only, per spec) -------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(d3.timeMonth.every(1)).tickFormat(d3.timeFormat(\"%b %Y\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(\"$,.0f\")));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\"text\").attr(\"transform\", \"rotate(-30)\").style(\"text-anchor\", \"end\");\n\ng.append(\"text\")\n  .attr(\"x\", -margin.left + 24)\n  .attr(\"y\", -30)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Price (USD)\");\n\n// --- Lines ------------------------------------------------------------------\nconst line = d3\n  .line()\n  .defined((d) => d.value !== null)\n  .x((d) => x(d.date))\n  .y((d) => y(d.value))\n  .curve(d3.curveMonotoneX);\n\nfor (const s of series) {\n  const points = dates.map((date, i) => ({ date, value: s.values[i] }));\n  g.append(\"path\")\n    .datum(points)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", s.width)\n    .attr(\"stroke-linejoin\", \"round\")\n    .attr(\"stroke-linecap\", \"round\")\n    .attr(\"opacity\", s.key === \"close\" ? 1 : 0.9)\n    .attr(\"d\", line);\n}\n\n// --- Crossover annotation (marks the golden/death cross with a dashed rule,\n// a ringed marker, and a leader-line label) --------------------------------\nif (crossover) {\n  const cx = x(crossover.date);\n  const cy = y(crossover.value);\n  const label = crossover.bullish ? \"Golden Cross\" : \"Death Cross\";\n  const labelBelow = cy < ih * 0.3;\n\n  g.append(\"line\")\n    .attr(\"x1\", cx)\n    .attr(\"x2\", cx)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", ih)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1)\n    .attr(\"stroke-dasharray\", \"4,4\")\n    .attr(\"opacity\", 0.5);\n\n  g.append(\"circle\")\n    .attr(\"cx\", cx)\n    .attr(\"cy\", cy)\n    .attr(\"r\", 8)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.ink)\n    .attr(\"stroke-width\", 2);\n\n  g.append(\"text\")\n    .attr(\"x\", cx)\n    .attr(\"y\", labelBelow ? cy + 28 : cy - 18)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"15px\")\n    .style(\"font-weight\", \"600\")\n    .text(label);\n}\n\n// --- Title --------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"indicator-sma · javascript · d3 · anyplot.ai\");\n\n// --- Legend (horizontal, below title) ---------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(0, 90)`);\nconst legendWidth = series.reduce((acc, s) => acc + s.label.length * 11 + 60, 0);\nlet cursorX = width / 2 - legendWidth / 2;\nfor (const s of series) {\n  const item = legend.append(\"g\").attr(\"transform\", `translate(${cursorX},0)`);\n  item\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 28)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", s.width)\n    .attr(\"stroke-linecap\", \"round\");\n  item\n    .append(\"text\")\n    .attr(\"x\", 38)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(s.label);\n  cursorX += s.label.length * 11 + 60;\n}\n"}