{"spec_id":"indicator-macd","library":"d3","language":"javascript","code":"// anyplot.ai\n// indicator-macd: MACD Technical Indicator Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 140, right: 60, bottom: 70, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: deterministic LCG-driven synthetic MACD series (12/26/9) ---------\nlet seed = 42;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst numPeriods = 120;\nconst startDate = new Date(\"2025-04-01T00:00:00Z\");\n\nfunction ema(values, period) {\n  const k = 2 / (period + 1);\n  const out = new Array(values.length);\n  out[0] = values[0];\n  for (let i = 1; i < values.length; i += 1) {\n    out[i] = values[i] * k + out[i - 1] * (1 - k);\n  }\n  return out;\n}\n\nconst closes = [];\nlet price = 182;\nlet trendPhase = 0;\nfor (let i = 0; i < numPeriods; i += 1) {\n  trendPhase = i / 14;\n  const drift = Math.sin(trendPhase) * 0.9;\n  const noise = (lcg() - 0.5) * 3.2;\n  price += drift + noise;\n  closes.push(price);\n}\n\nconst ema12 = ema(closes, 12);\nconst ema26 = ema(closes, 26);\nconst macdLine = ema12.map((v, i) => v - ema26[i]);\nconst signalLine = ema(macdLine, 9);\n\nconst data = closes.map((_, i) => ({\n  date: new Date(startDate.getTime() + i * 86400000),\n  macd: macdLine[i],\n  signal: signalLine[i],\n  histogram: macdLine[i] - signalLine[i],\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\n  .scaleTime()\n  .domain(d3.extent(data, (d) => d.date))\n  .range([0, iw]);\n\nconst yExtent = d3.extent(data.flatMap((d) => [d.macd, d.signal, d.histogram]));\nconst yPad = (yExtent[1] - yExtent[0]) * 0.12;\nconst y = d3\n  .scaleLinear()\n  .domain([yExtent[0] - yPad, yExtent[1] + yPad])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y only) --------------------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .call((sel) => sel.selectAll(\"line\").attr(\"stroke\", t.grid));\n\n// --- Histogram bars: green above zero, red below ------------------------------\nconst barWidth = Math.max(1.5, (iw / numPeriods) * 0.6);\nconst posColor = t.palette[0]; // brand green — gain\nconst negColor = t.palette[4]; // matte red — loss (semantic exception: finance up/down)\n\ng.selectAll(\"rect.hist\")\n  .data(data)\n  .join(\"rect\")\n  .attr(\"class\", \"hist\")\n  .attr(\"x\", (d) => x(d.date) - barWidth / 2)\n  .attr(\"y\", (d) => y(Math.max(0, d.histogram)))\n  .attr(\"width\", barWidth)\n  .attr(\"height\", (d) => Math.abs(y(d.histogram) - y(0)))\n  .attr(\"fill\", (d) => (d.histogram >= 0 ? posColor : negColor))\n  .attr(\"opacity\", 0.75);\n\n// --- Zero reference line (dashed to read as a reference, not another series) ----\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(0))\n  .attr(\"y2\", y(0))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,4\");\n\n// --- MACD + signal lines ---------------------------------------------------------\nconst macdColor = t.palette[2]; // blue\nconst signalColor = t.palette[3]; // ochre — distinct from blue, warm/cool contrast\n\nconst macdPath = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.macd))\n  .curve(d3.curveMonotoneX);\n\nconst signalPath = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.signal))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(data).attr(\"fill\", \"none\").attr(\"stroke\", macdColor).attr(\"stroke-width\", 3).attr(\"d\", macdPath);\n\ng.append(\"path\")\n  .datum(data)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", signalColor)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"d\", signalPath);\n\n// --- Crossover callouts: detect sign flips of (macd - signal) past the EMA -----\n// warm-up window and label the first bullish and first later bearish crossing\n// with a leader-line + auto-sized backdrop (sized via getBBox(), not hard-coded).\nfunction findCrossovers(rows, minIndex) {\n  const found = [];\n  for (let i = Math.max(1, minIndex); i < rows.length; i += 1) {\n    const prevDiff = rows[i - 1].macd - rows[i - 1].signal;\n    const currDiff = rows[i].macd - rows[i].signal;\n    if (prevDiff <= 0 && currDiff > 0) found.push({ index: i, type: \"bullish\" });\n    else if (prevDiff >= 0 && currDiff < 0) found.push({ index: i, type: \"bearish\" });\n  }\n  return found;\n}\n\nconst crossings = findCrossovers(data, 35);\nconst bullishCross = crossings.find((c) => c.type === \"bullish\");\nconst bearishCross = crossings.find((c) => c.type === \"bearish\" && (!bullishCross || c.index > bullishCross.index));\nconst callouts = [bullishCross, bearishCross].filter(Boolean);\n\nconst calloutGroup = g.append(\"g\").attr(\"class\", \"callouts\");\ncallouts.forEach((c) => {\n  const d = data[c.index];\n  const isBullish = c.type === \"bullish\";\n  const cx = x(d.date);\n  const cy = y(d.macd);\n  const dy = isBullish ? -34 : 34;\n  const labelY = Math.max(14, Math.min(ih - 14, cy + dy));\n  const calloutColor = isBullish ? posColor : negColor;\n\n  calloutGroup\n    .append(\"circle\")\n    .attr(\"cx\", cx)\n    .attr(\"cy\", cy)\n    .attr(\"r\", 5.5)\n    .attr(\"fill\", calloutColor)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 2);\n\n  calloutGroup\n    .append(\"line\")\n    .attr(\"x1\", cx)\n    .attr(\"y1\", cy + (labelY > cy ? 8 : -8))\n    .attr(\"x2\", cx)\n    .attr(\"y2\", labelY + (labelY > cy ? -8 : 8))\n    .attr(\"stroke\", calloutColor)\n    .attr(\"stroke-width\", 1.25)\n    .attr(\"stroke-dasharray\", \"2,2\");\n\n  const label = calloutGroup\n    .append(\"text\")\n    .attr(\"x\", cx)\n    .attr(\"y\", labelY)\n    .attr(\"text-anchor\", \"middle\")\n    .style(\"font-size\", \"13px\")\n    .style(\"font-weight\", \"600\")\n    .attr(\"fill\", calloutColor)\n    .text(isBullish ? \"Bullish crossover\" : \"Bearish crossover\");\n\n  const bbox = label.node().getBBox();\n  calloutGroup\n    .insert(\"rect\", () => label.node())\n    .attr(\"x\", bbox.x - 6)\n    .attr(\"y\", bbox.y - 3)\n    .attr(\"width\", bbox.width + 12)\n    .attr(\"height\", bbox.height + 6)\n    .attr(\"rx\", 3)\n    .attr(\"fill\", t.pageBg)\n    .attr(\"opacity\", 0.85);\n});\n\n// --- Axes --------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(6).tickFormat(d3.timeFormat(\"%b %d\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels ---------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\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\", -66)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"MACD Value\");\n\n// --- Legend --------------------------------------------------------------------\nconst legend = [\n  { label: \"MACD (12,26)\", color: macdColor, type: \"line\" },\n  { label: \"Signal (9)\", color: signalColor, type: \"line\" },\n  { label: \"Histogram +\", color: posColor, type: \"swatch\" },\n  { label: \"Histogram −\", color: negColor, type: \"swatch\" },\n];\n\nconst legendGroup = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + 10}, ${margin.top - 45})`);\n\nlet lx = 0;\nlegend.forEach((item) => {\n  const entry = legendGroup.append(\"g\").attr(\"transform\", `translate(${lx},0)`);\n  if (item.type === \"line\") {\n    entry\n      .append(\"line\")\n      .attr(\"x1\", 0)\n      .attr(\"x2\", 26)\n      .attr(\"y1\", 0)\n      .attr(\"y2\", 0)\n      .attr(\"stroke\", item.color)\n      .attr(\"stroke-width\", 3);\n  } else {\n    entry.append(\"rect\").attr(\"x\", 0).attr(\"y\", -7).attr(\"width\", 20).attr(\"height\", 14).attr(\"fill\", item.color).attr(\"opacity\", 0.75);\n  }\n  const textEl = entry\n    .append(\"text\")\n    .attr(\"x\", 34)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n  lx += 34 + textEl.node().getBBox().width + 30;\n});\n\n// --- Title -----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"indicator-macd · javascript · d3 · anyplot.ai\");\n"}