{"spec_id":"stem-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// stem-basic: Basic Stem Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-07-25\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 60, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Discrete-time impulse response of a damped oscillator: h[n] = e^(-0.15n) * cos(0.5n)\nconst sampleCount = 40;\nconst envelope = (n) => Math.exp(-0.15 * n);\nconst data = Array.from({ length: sampleCount }, (_, n) => ({\n  n,\n  amplitude: envelope(n) * Math.cos(0.5 * 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\n  .scaleLinear()\n  .domain([d3.min(data, (d) => d.n) - 1, d3.max(data, (d) => d.n) + 1])\n  .range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain(d3.extent(data, (d) => d.amplitude))\n  .nice()\n  .range([ih, 0]);\n// Marker radius tied to |amplitude|: the decaying envelope reads directly\n// through marker prominence, not just vertical position.\nconst r = d3.scaleLinear().domain([0, 1]).range([5.5, 11]).clamp(true);\n\n// --- Decay-envelope guide (d3-shape line generator) -----------------------------\n// A thin dashed guide along +/- e^(-0.15n) gives the reader an immediate read\n// on the envelope the stems are riding, rather than leaving it implicit.\nconst envelopeLine = d3\n  .line()\n  .curve(d3.curveMonotoneX)\n  .x((d) => x(d.n))\n  .y((d) => y(d.value));\nconst upperEnvelope = data.map((d) => ({ n: d.n, value: envelope(d.n) }));\nconst lowerEnvelope = data.map((d) => ({ n: d.n, value: -envelope(d.n) }));\nfor (const env of [upperEnvelope, lowerEnvelope]) {\n  g.append(\"path\")\n    .datum(env)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1.25)\n    .attr(\"stroke-dasharray\", \"5,4\")\n    .attr(\"stroke-opacity\", 0.35)\n    .attr(\"d\", envelopeLine);\n}\n\n// --- Baseline (y = 0) ----------------------------------------------------------\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\n// --- Stems ----------------------------------------------------------------------\n// Positive samples render at full opacity; negative samples are slightly\n// dimmed, so the sign transition of the oscillation is visible at a glance.\ng.selectAll(\".stem\")\n  .data(data)\n  .join(\"line\")\n  .attr(\"class\", \"stem\")\n  .attr(\"x1\", (d) => x(d.n))\n  .attr(\"x2\", (d) => x(d.n))\n  .attr(\"y1\", y(0))\n  .attr(\"y2\", (d) => y(d.amplitude))\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-opacity\", (d) => (d.amplitude >= 0 ? 1 : 0.55));\n\n// --- Markers ----------------------------------------------------------------------\ng.selectAll(\".marker\")\n  .data(data)\n  .join(\"circle\")\n  .attr(\"class\", \"marker\")\n  .attr(\"cx\", (d) => x(d.n))\n  .attr(\"cy\", (d) => y(d.amplitude))\n  .attr(\"r\", (d) => r(Math.abs(d.amplitude)))\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", (d) => (d.amplitude >= 0 ? 1 : 0.55))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(10).tickFormat(d3.format(\"d\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(8));\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 + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Sample Index (n)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Amplitude\");\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(\"stem-basic · javascript · d3 · anyplot.ai\");\n"}