{"spec_id":"stock-event-flags","library":"d3","language":"javascript","code":"// anyplot.ai\n// stock-event-flags: Stock Chart with Event Flags\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst inkMuted = t.theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 60, bottom: 70, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: deterministic LCG (no seeded Math.random in the browser) --------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst TRADING_DAYS = 180;\nconst startDate = new Date(Date.UTC(2025, 0, 2));\nconst dates = [];\nfor (let i = 0; i < TRADING_DAYS; i++) {\n  const d = new Date(startDate);\n  d.setUTCDate(d.getUTCDate() + Math.floor((i / 5) * 7));\n  dates.push(d);\n}\n\nlet price = 142;\nconst closes = dates.map((_, i) => {\n  const drift = 0.06;\n  const shock = (rand() - 0.5) * 4.2;\n  const bump = i > 40 && i < 46 ? 6 : i > 110 && i < 116 ? -5 : 0;\n  price = Math.max(60, price + drift + shock + bump * 0.2);\n  return price;\n});\n\nconst priceData = dates.map((date, i) => ({ date, close: closes[i] }));\n\nconst events = [\n  { dayIndex: 12, type: \"earnings\", label: \"Q4 Earnings Beat\" },\n  { dayIndex: 30, type: \"dividend\", label: \"Dividend $0.24\" },\n  { dayIndex: 43, type: \"news\", label: \"Product Launch\" },\n  { dayIndex: 62, type: \"earnings\", label: \"Q1 Earnings Miss\" },\n  { dayIndex: 78, type: \"split\", label: \"3-for-1 Split\" },\n  { dayIndex: 96, type: \"dividend\", label: \"Dividend $0.26\" },\n  { dayIndex: 113, type: \"news\", label: \"Analyst Upgrade\" },\n  { dayIndex: 128, type: \"earnings\", label: \"Q2 Earnings Beat\" },\n  { dayIndex: 150, type: \"news\", label: \"Regulatory Filing\" },\n  { dayIndex: 166, type: \"dividend\", label: \"Dividend $0.28\" },\n].map((e, i) => ({\n  ...e,\n  date: dates[e.dayIndex],\n  close: closes[e.dayIndex],\n  side: i % 2 === 0 ? \"up\" : \"down\",\n}));\n\n// Event-type -> Imprint categorical color, canonical palette order (0-3)\nconst EVENT_COLOR = {\n  earnings: t.palette[0],\n  dividend: t.palette[1],\n  split: t.palette[2],\n  news: t.palette[3],\n};\nconst EVENT_SYMBOL = {\n  earnings: \"▲\", // triangle — reporting event\n  dividend: \"$\",\n  split: \"✂\", // scissors — split\n  news: \"★\", // star — news/analyst\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(dates)).range([0, iw]);\nconst yMin = d3.min(closes) - 8;\nconst yMax = d3.max(closes) + 8;\nconst y = d3.scaleLinear().domain([yMin, yMax]).nice().range([ih, 0]);\n\n// --- Gridlines (y only) -------------------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\ng.select(\".grid .domain\").remove();\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\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickFormat((v) => `$${v.toFixed(0)}`));\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.inkSoft);\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 (2025)\");\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -72)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Closing Price (USD)\");\n\n// --- Price line -----------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.date))\n  .y((d) => y(d.close))\n  .curve(d3.curveMonotoneX);\n\ng.append(\"path\")\n  .datum(priceData)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3)\n  .attr(\"d\", line);\n\n// --- Event flags: connector, marker at price, flag box above/below --------\n// Flags sit in two fixed lanes at the top/bottom of the plot area (alternating\n// by event index) so they never obscure the price line; a dashed connector\n// ties each flag to its exact date/price on the line.\nconst FLAG_W = 140;\nconst FLAG_H = 46;\nconst LANE_UP_TOP = 6;\nconst LANE_DOWN_TOP = ih - FLAG_H - 6;\nconst flagLayer = g.append(\"g\").attr(\"class\", \"event-flags\");\n\nconst flagGroups = flagLayer\n  .selectAll(\"g.flag\")\n  .data(events)\n  .join(\"g\")\n  .attr(\"class\", \"flag\");\n\n// Vertical dashed connector from flag to price point\nflagGroups\n  .append(\"line\")\n  .attr(\"x1\", (d) => x(d.date))\n  .attr(\"x2\", (d) => x(d.date))\n  .attr(\"y1\", (d) => (d.side === \"up\" ? LANE_UP_TOP + FLAG_H : LANE_DOWN_TOP))\n  .attr(\"y2\", (d) => y(d.close))\n  .attr(\"stroke\", (d) => EVENT_COLOR[d.type])\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"4,4\")\n  .attr(\"opacity\", 0.7);\n\n// Marker dot at the price/date intersection\nflagGroups\n  .append(\"circle\")\n  .attr(\"cx\", (d) => x(d.date))\n  .attr(\"cy\", (d) => y(d.close))\n  .attr(\"r\", 5)\n  .attr(\"fill\", (d) => EVENT_COLOR[d.type])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// Flag box (rounded rect + icon + label) in the top or bottom lane\nflagGroups.each(function (d) {\n  const fg = d3.select(this);\n  const boxY = d.side === \"up\" ? LANE_UP_TOP : LANE_DOWN_TOP;\n  fg\n    .append(\"rect\")\n    .attr(\"x\", -FLAG_W / 2)\n    .attr(\"y\", boxY)\n    .attr(\"width\", FLAG_W)\n    .attr(\"height\", FLAG_H)\n    .attr(\"rx\", 8)\n    .attr(\"fill\", t.elevatedBg)\n    .attr(\"stroke\", EVENT_COLOR[d.type])\n    .attr(\"stroke-width\", 1.5)\n    .attr(\"transform\", `translate(${x(d.date)},0)`);\n  fg\n    .append(\"text\")\n    .attr(\"x\", -FLAG_W / 2 + 12)\n    .attr(\"y\", boxY + 19)\n    .attr(\"fill\", EVENT_COLOR[d.type])\n    .style(\"font-size\", \"16px\")\n    .style(\"font-weight\", \"700\")\n    .text(EVENT_SYMBOL[d.type])\n    .attr(\"transform\", `translate(${x(d.date)},0)`);\n  fg\n    .append(\"text\")\n    .attr(\"x\", -FLAG_W / 2 + 30)\n    .attr(\"y\", boxY + 19)\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"14px\")\n    .style(\"font-weight\", \"600\")\n    .text(d.label)\n    .attr(\"transform\", `translate(${x(d.date)},0)`);\n  fg\n    .append(\"text\")\n    .attr(\"x\", -FLAG_W / 2 + 12)\n    .attr(\"y\", boxY + 36)\n    .attr(\"fill\", inkMuted)\n    .style(\"font-size\", \"13px\")\n    .text(d3.timeFormat(\"%b %-d\")(d.date))\n    .attr(\"transform\", `translate(${x(d.date)},0)`);\n});\n\n// --- Legend (event-type color key) -----------------------------------------\nconst legendItems = [\n  { type: \"earnings\", label: \"Earnings\" },\n  { type: \"dividend\", label: \"Dividend\" },\n  { type: \"split\", label: \"Split\" },\n  { type: \"news\", label: \"News\" },\n];\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left}, ${margin.top - 58})`);\nlet lx = 0;\nlegendItems.forEach((item) => {\n  const li = legend.append(\"g\").attr(\"transform\", `translate(${lx},0)`);\n  li.append(\"rect\")\n    .attr(\"width\", 16)\n    .attr(\"height\", 16)\n    .attr(\"rx\", 3)\n    .attr(\"fill\", EVENT_COLOR[item.type]);\n  li.append(\"text\")\n    .attr(\"x\", 24)\n    .attr(\"y\", 13)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n  lx += 24 + item.label.length * 8 + 36;\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(\"Tech Stock 2025 · stock-event-flags · javascript · d3 · anyplot.ai\");\n"}