{"spec_id":"indicator-ema","library":"d3","language":"javascript","code":"// anyplot.ai\n// indicator-ema: Exponential Moving Average (EMA) Indicator Chart\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: 150, right: 70, bottom: 80, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\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);\n\nconst NUM_DAYS = 120;\nconst dates = [];\nconst cursor = new Date(2024, 0, 2);\nwhile (dates.length < NUM_DAYS) {\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 < NUM_DAYS; i++) {\n  const drift = 0.0006;\n  const shock = (rand() - 0.5) * 0.028;\n  price *= 1 + drift + shock;\n  closes.push(price);\n}\n\nfunction ema(values, period) {\n  const k = 2 / (period + 1);\n  const out = [values[0]];\n  for (let i = 1; i < values.length; i++) {\n    out.push(values[i] * k + out[i - 1] * (1 - k));\n  }\n  return out;\n}\n\nconst emaShortValues = ema(closes, 12);\nconst emaLongValues = ema(closes, 26);\n\nconst data = dates.map((date, i) => ({\n  date,\n  close: closes[i],\n  emaShort: emaShortValues[i],\n  emaLong: emaLongValues[i],\n}));\n\n// Crossover points: sign change of (emaShort - emaLong)\nconst crossovers = [];\nfor (let i = 1; i < data.length; i++) {\n  const prevDiff = data[i - 1].emaShort - data[i - 1].emaLong;\n  const currDiff = data[i].emaShort - data[i].emaLong;\n  if (prevDiff !== 0 && Math.sign(prevDiff) !== Math.sign(currDiff)) {\n    crossovers.push(data[i]);\n  }\n}\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.scaleTime().domain(d3.extent(data, (d) => d.date)).range([0, iw]);\nconst yMin = d3.min(data, (d) => Math.min(d.close, d.emaShort, d.emaLong));\nconst yMax = d3.max(data, (d) => Math.max(d.close, d.emaShort, d.emaLong));\nconst y = d3.scaleLinear().domain([yMin, yMax]).nice().range([ih, 0]);\n\n// --- Gridlines (y-axis only) -----------------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).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).tickFormat((d) => `$${d.toFixed(0)}`));\n\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}\n\n// --- Lines: price (neutral reference) + two EMA overlays --------------------\nconst lineClose = d3.line().x((d) => x(d.date)).y((d) => y(d.close)).curve(d3.curveMonotoneX);\nconst lineEmaLong = d3.line().x((d) => x(d.date)).y((d) => y(d.emaLong)).curve(d3.curveMonotoneX);\nconst lineEmaShort = d3.line().x((d) => x(d.date)).y((d) => y(d.emaShort)).curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(data)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-opacity\", 0.5)\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"d\", lineClose);\n\ng.append(\"path\")\n  .datum(data)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"d\", lineEmaLong);\n\ng.append(\"path\")\n  .datum(data)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"d\", lineEmaShort);\n\n// --- Crossover markers (short EMA crossing long EMA) -------------------------\n// The first golden cross (bullish) and first death cross (bearish) get a\n// distinctive triangle glyph + leader-line callout naming the signal; the\n// rest stay plain rings so the chart doesn't get cluttered.\nconst labeledCrossovers = [];\nfor (const c of crossovers) {\n  const type = c.emaShort > c.emaLong ? \"Golden cross\" : \"Death cross\";\n  if (!labeledCrossovers.some((l) => l.type === type)) {\n    labeledCrossovers.push({ ...c, type });\n  }\n  if (labeledCrossovers.length === 2) break;\n}\nconst labeledDates = new Set(labeledCrossovers.map((d) => +d.date));\n\ng.selectAll(\".crossover\")\n  .data(crossovers.filter((d) => !labeledDates.has(+d.date)))\n  .join(\"circle\")\n  .attr(\"class\", \"crossover\")\n  .attr(\"cx\", (d) => x(d.date))\n  .attr(\"cy\", (d) => y(d.emaShort))\n  .attr(\"r\", 7)\n  .attr(\"fill\", t.pageBg)\n  .attr(\"stroke\", t.amber)\n  .attr(\"stroke-width\", 2.5);\n\nconst triangle = d3.symbol().type(d3.symbolTriangle).size(190)();\nconst callouts = g.append(\"g\").attr(\"class\", \"crossover-callouts\");\nfor (const c of labeledCrossovers) {\n  const cx = x(c.date);\n  const cy = y(c.emaShort);\n  const isGolden = c.type === \"Golden cross\";\n  const labelBelow = cy < ih * 0.4;\n  const labelY = cy + (labelBelow ? 44 : -44);\n\n  callouts\n    .append(\"path\")\n    .attr(\"d\", triangle)\n    .attr(\"transform\", `translate(${cx},${cy}) rotate(${isGolden ? 0 : 180})`)\n    .attr(\"fill\", t.amber);\n\n  callouts\n    .append(\"line\")\n    .attr(\"x1\", cx)\n    .attr(\"y1\", cy + (labelBelow ? 13 : -13))\n    .attr(\"x2\", cx)\n    .attr(\"y2\", labelY + (labelBelow ? -9 : 9))\n    .attr(\"stroke\", t.amber)\n    .attr(\"stroke-width\", 1.5);\n\n  // Halo behind the label so it stays legible where the close-price line\n  // crosses underneath it.\n  const calloutLabel = callouts\n    .append(\"text\")\n    .attr(\"x\", cx)\n    .attr(\"y\", labelY + (labelBelow ? 4 : -4))\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"13px\")\n    .style(\"font-weight\", \"600\")\n    .text(c.type);\n  const labelBox = calloutLabel.node().getBBox();\n  callouts\n    .append(\"rect\")\n    .attr(\"x\", labelBox.x - 6)\n    .attr(\"y\", labelBox.y - 3)\n    .attr(\"width\", labelBox.width + 12)\n    .attr(\"height\", labelBox.height + 6)\n    .attr(\"rx\", 4)\n    .attr(\"fill\", t.pageBg)\n    .attr(\"fill-opacity\", 0.92)\n    .lower();\n}\n\n// --- Axis labels -------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Trading date\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Closing price (USD)\");\n\n// --- Legend ------------------------------------------------------------------\nconst legendItems = [\n  { label: \"Close price\", color: t.ink, opacity: 0.5 },\n  { label: \"EMA 26 (long)\", color: t.palette[1], opacity: 1 },\n  { label: \"EMA 12 (short)\", color: t.palette[0], opacity: 1 },\n];\n\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top - 60})`);\nlet legendX = 0;\nconst legendGap = 40;\nfor (const item of legendItems) {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(${legendX},0)`);\n  row\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 28)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", item.color)\n    .attr(\"stroke-opacity\", item.opacity)\n    .attr(\"stroke-width\", 3.5);\n  const label = row\n    .append(\"text\")\n    .attr(\"x\", 36)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n  const labelWidth = label.node().getBBox().width;\n  legendX += 36 + labelWidth + legendGap;\n}\n\n// --- Title (fontsize scales linearly off the 67-char baseline) ---------------\nconst title = \"NovaTech Inc. (NVTC) · indicator-ema · javascript · d3 · anyplot.ai\";\nconst baselineChars = 67;\nconst defaultTitleSize = 28;\nconst titleFloor = 15;\nconst ratio = title.length > baselineChars ? baselineChars / title.length : 1;\nconst titleSize = Math.max(titleFloor, Math.round(defaultTitleSize * ratio));\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}