{"spec_id":"indicator-rsi","library":"muix","language":"javascript","code":"// anyplot.ai\n// indicator-rsi: RSI Technical Indicator Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst isDark = window.ANYPLOT_THEME === \"dark\";\nconst MUTED = isDark ? \"#A8A79F\" : \"#6B6A63\"; // theme-adaptive Imprint \"muted\" anchor (not in ANYPLOT_TOKENS)\n\nconst LOOKBACK = 14;\nconst RSI_PERIODS = 120;\n\n// Tiny fixed-seed LCG — the 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(7);\n\n// Business-day dates starting Jan 2, 2025.\nconst totalDays = RSI_PERIODS + LOOKBACK;\nconst dates = [];\nconst cursor = new Date(Date.UTC(2025, 0, 2));\nwhile (dates.length < totalDays + 1) {\n  const weekday = cursor.getUTCDay();\n  if (weekday !== 0 && weekday !== 6) {\n    dates.push(new Date(cursor));\n  }\n  cursor.setUTCDate(cursor.getUTCDate() + 1);\n}\nconst dateFormatter = new Intl.DateTimeFormat(\"en-US\", { month: \"short\", day: \"numeric\", timeZone: \"UTC\" });\n\n// Closing price: a slow oscillation (bull/bear swings) plus noise, so the\n// resulting RSI comfortably reaches both the oversold and overbought bands.\nconst closes = [];\nlet price = 62;\nfor (let i = 0; i <= totalDays; i++) {\n  const trend = 0.55 * Math.sin(i / 14) + 0.18 * Math.sin(i / 4.5);\n  const noise = (rand() - 0.5) * 1.9;\n  price += trend + noise;\n  closes.push(price);\n}\n\n// Wilder's RSI(14): average gain/loss smoothed with a (period-1)/period decay.\nfunction computeRSI(values, period) {\n  const gains = [];\n  const losses = [];\n  for (let i = 1; i < values.length; i++) {\n    const change = values[i] - values[i - 1];\n    gains.push(Math.max(change, 0));\n    losses.push(Math.max(-change, 0));\n  }\n  let avgGain = gains.slice(0, period).reduce((a, b) => a + b, 0) / period;\n  let avgLoss = losses.slice(0, period).reduce((a, b) => a + b, 0) / period;\n  const rsi = [avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss)];\n  for (let i = period; i < gains.length; i++) {\n    avgGain = (avgGain * (period - 1) + gains[i]) / period;\n    avgLoss = (avgLoss * (period - 1) + losses[i]) / period;\n    rsi.push(avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss));\n  }\n  return rsi;\n}\n\nconst rsiValues = computeRSI(closes, LOOKBACK).slice(0, RSI_PERIODS);\nconst dateLabels = dates.slice(dates.length - RSI_PERIODS).map((d) => dateFormatter.format(d));\n\nconst MARGIN = { top: 24, right: 32, bottom: 64, left: 76 };\n\n// The y-axis is a fixed 0-100 linear scale, so the pixel position of any RSI\n// value within the plot area is a plain linear map — used to draw the\n// oversold/overbought backdrop bands without a Pro-only reference-area component.\nfunction yToPixel(value, plotHeight) {\n  return MARGIN.top + ((100 - value) / 100) * plotHeight;\n}\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width; // 1600 CSS px (landscape)\n  const H = window.ANYPLOT_SIZE.height; // 900 CSS px\n  const TITLE_H = 72;\n  const LEGEND_H = 44;\n  const chartH = H - TITLE_H - LEGEND_H;\n  const plotHeight = chartH - MARGIN.top - MARGIN.bottom;\n  const overboughtBand = { top: yToPixel(100, plotHeight), height: yToPixel(70, plotHeight) - yToPixel(100, plotHeight) };\n  const oversoldBand = { top: yToPixel(30, plotHeight), height: yToPixel(0, plotHeight) - yToPixel(30, plotHeight) };\n\n  return (\n    <Box\n      sx={{\n        width: W,\n        height: H,\n        bgcolor: t.pageBg,\n        display: \"flex\",\n        flexDirection: \"column\",\n        fontFamily: \"'Roboto', 'Helvetica Neue', Arial, sans-serif\",\n        boxSizing: \"border-box\",\n      }}\n    >\n      {/* Title */}\n      <Box sx={{ height: TITLE_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <Typography sx={{ color: t.ink, fontSize: 22, fontWeight: 600 }}>\n          indicator-rsi · javascript · muix · anyplot.ai\n        </Typography>\n      </Box>\n\n      {/* Chart — wrapped in a relative box so the oversold/overbought bands can\n          sit as plain absolutely-positioned layers behind the (transparent)\n          MUI X chart, computed from the fixed 0-100 linear y-scale. */}\n      <Box sx={{ position: \"relative\", width: W, height: chartH }}>\n        <Box\n          sx={{\n            position: \"absolute\",\n            left: MARGIN.left,\n            right: MARGIN.right,\n            top: overboughtBand.top,\n            height: overboughtBand.height,\n            bgcolor: t.amber,\n            opacity: 0.14,\n            pointerEvents: \"none\",\n          }}\n        />\n        <Box\n          sx={{\n            position: \"absolute\",\n            left: MARGIN.left,\n            right: MARGIN.right,\n            top: oversoldBand.top,\n            height: oversoldBand.height,\n            bgcolor: MUTED,\n            opacity: 0.14,\n            pointerEvents: \"none\",\n          }}\n        />\n        <ChartContainer\n          width={W}\n          height={chartH}\n          series={[\n            {\n              type: \"line\",\n              id: \"rsi\",\n              data: rsiValues,\n              label: \"RSI (14)\",\n              color: t.palette[0],\n              showMark: false,\n              curve: \"linear\",\n            },\n          ]}\n          xAxis={[\n            {\n              id: \"dates\",\n              scaleType: \"band\",\n              data: dateLabels,\n              categoryGapRatio: 0.15,\n              tickLabelInterval: (_value, index) => index % 10 === 0,\n              tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n            },\n          ]}\n          yAxis={[\n            {\n              id: \"rsi-axis\",\n              min: 0,\n              max: 100,\n              tickNumber: 6,\n              tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n            },\n          ]}\n          margin={MARGIN}\n          skipAnimation\n          sx={{\n            \"& .MuiChartsGrid-line\": { stroke: t.grid },\n            \"& .MuiLineElement-root\": { strokeWidth: 2.75 },\n          }}\n        >\n          <ChartsGrid horizontal />\n          <LinePlot skipAnimation />\n          <ChartsReferenceLine\n            y={70}\n            axisId=\"rsi-axis\"\n            label=\"Overbought (70)\"\n            labelAlign=\"end\"\n            labelStyle={{ fontSize: 12, fill: t.amber }}\n            lineStyle={{ stroke: t.amber, strokeWidth: 1.5 }}\n          />\n          <ChartsReferenceLine\n            y={30}\n            axisId=\"rsi-axis\"\n            label=\"Oversold (30)\"\n            labelAlign=\"end\"\n            labelStyle={{ fontSize: 12, fill: MUTED }}\n            lineStyle={{ stroke: MUTED, strokeWidth: 1.5 }}\n          />\n          <ChartsReferenceLine\n            y={50}\n            axisId=\"rsi-axis\"\n            label=\"Neutral (50)\"\n            labelAlign=\"end\"\n            labelStyle={{ fontSize: 12, fill: t.inkSoft }}\n            lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"6 4\", strokeOpacity: 0.6 }}\n          />\n          <ChartsXAxis\n            axisId=\"dates\"\n            position=\"bottom\"\n            label=\"Trading Date\"\n            labelStyle={{ fontSize: 14, fill: t.ink }}\n            disableLine\n          />\n          <ChartsYAxis\n            axisId=\"rsi-axis\"\n            position=\"left\"\n            label=\"RSI\"\n            labelStyle={{ fontSize: 14, fill: t.ink }}\n            disableLine\n          />\n          <ChartsTooltip trigger=\"axis\" />\n        </ChartContainer>\n      </Box>\n\n      {/* Legend */}\n      <Box sx={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: \"32px\" }}>\n        <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n          <Box sx={{ width: 24, height: 3, bgcolor: t.palette[0], borderRadius: \"2px\", flexShrink: 0 }} />\n          <Typography sx={{ color: t.inkSoft, fontSize: 14 }}>RSI (14-period)</Typography>\n        </Box>\n        <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n          <Box sx={{ width: 14, height: 14, bgcolor: t.amber, opacity: 0.5, borderRadius: \"2px\", flexShrink: 0 }} />\n          <Typography sx={{ color: t.inkSoft, fontSize: 14 }}>Overbought zone (&gt; 70)</Typography>\n        </Box>\n        <Box sx={{ display: \"flex\", alignItems: \"center\", gap: \"8px\" }}>\n          <Box sx={{ width: 14, height: 14, bgcolor: MUTED, opacity: 0.5, borderRadius: \"2px\", flexShrink: 0 }} />\n          <Typography sx={{ color: t.inkSoft, fontSize: 14 }}>Oversold zone (&lt; 30)</Typography>\n        </Box>\n      </Box>\n    </Box>\n  );\n}\n"}