{"spec_id":"indicator-rsi","library":"d3","language":"javascript","code":"// anyplot.ai\n// indicator-rsi: RSI Technical Indicator Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 70, left: 80 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: 120-day RSI(14) series derived from a synthetic price walk ------\n// Deterministic LCG (browser has no seeded Math.random)\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 numDays = 120;\nconst prices = [180];\nfor (let i = 1; i < numDays + 1; i++) {\n  const drift = Math.sin(i / 9) * 0.9;\n  const noise = (rand() - 0.5) * 3.2;\n  prices.push(prices[i - 1] + drift + noise);\n}\n\nconst lookback = 14;\nfunction computeRsi(series, period) {\n  const rsi = new Array(series.length).fill(null);\n  let gainSum = 0;\n  let lossSum = 0;\n  for (let i = 1; i <= period; i++) {\n    const change = series[i] - series[i - 1];\n    if (change > 0) gainSum += change;\n    else lossSum -= change;\n  }\n  let avgGain = gainSum / period;\n  let avgLoss = lossSum / period;\n  rsi[period] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);\n  for (let i = period + 1; i < series.length; i++) {\n    const change = series[i] - series[i - 1];\n    const gain = change > 0 ? change : 0;\n    const loss = change < 0 ? -change : 0;\n    avgGain = (avgGain * (period - 1) + gain) / period;\n    avgLoss = (avgLoss * (period - 1) + loss) / period;\n    rsi[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);\n  }\n  return rsi;\n}\n\nconst startDate = new Date(2025, 5, 2);\nconst rsiSeries = computeRsi(prices, lookback);\nconst data = rsiSeries\n  .map((value, i) => ({ date: new Date(startDate.getTime() + i * 86400000), value }))\n  .filter((d) => d.value !== null);\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]);\nconst y = d3.scaleLinear().domain([0, 100]).range([ih, 0]);\n\n// --- Zone shading (overbought 70-100, oversold 0-30) --------------------------\ng.append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", y(100))\n  .attr(\"width\", iw)\n  .attr(\"height\", y(70) - y(100))\n  .attr(\"fill\", t.palette[4])\n  .attr(\"opacity\", 0.1);\n\ng.append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", y(30))\n  .attr(\"width\", iw)\n  .attr(\"height\", y(0) - y(30))\n  .attr(\"fill\", t.palette[2])\n  .attr(\"opacity\", 0.1);\n\n// --- Threshold + centerline lines ---------------------------------------------\n// Labels sit at the left edge (day 14+) where the series has more headroom\n// before its terminal value, avoiding the right-edge collision with the line.\nconst thresholds = [\n  { level: 70, label: \"Overbought (70)\", lineColor: t.palette[4], labelColor: t.palette[4] },\n  { level: 50, label: \"Centerline (50)\", lineColor: t.grid, labelColor: t.inkSoft },\n  { level: 30, label: \"Oversold (30)\", lineColor: t.palette[2], labelColor: t.palette[2] },\n];\n\nfor (const th of thresholds) {\n  g.append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", iw)\n    .attr(\"y1\", y(th.level))\n    .attr(\"y2\", y(th.level))\n    .attr(\"stroke\", th.lineColor)\n    .attr(\"stroke-width\", th.level === 50 ? 1.5 : 2)\n    .attr(\"stroke-dasharray\", th.level === 50 ? \"2,4\" : \"6,4\")\n    .attr(\"opacity\", th.level === 50 ? 0.6 : 0.85);\n\n  g.append(\"text\")\n    .attr(\"x\", 4)\n    .attr(\"y\", y(th.level) - 8)\n    .attr(\"text-anchor\", \"start\")\n    .attr(\"fill\", th.labelColor)\n    .style(\"font-size\", \"13px\")\n    .style(\"font-weight\", \"600\")\n    .text(th.label);\n}\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(7).tickFormat(d3.timeFormat(\"%b %d\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickValues([0, 30, 50, 70, 100]));\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}\nxAxis.selectAll(\".tick line\").remove();\nyAxis.selectAll(\".tick line\").remove();\n\n// --- RSI line ---------------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.value))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\").datum(data).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 3).attr(\"d\", line);\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\", -56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"RSI\");\n\n// --- Title + subtitle -----------------------------------------------------\nconst title = \"indicator-rsi · javascript · d3 · anyplot.ai\";\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(title);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`14-period RSI on a synthetic 120-day daily close series`);\n"}