{"spec_id":"depth-order-book","library":"d3","language":"javascript","code":"// anyplot.ai\n// depth-order-book: Order Book Depth Chart\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-15\n\nconst t = window.ANYPLOT_TOKENS;\nconst theme = window.ANYPLOT_THEME;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 60, bottom: 90, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Imprint palette: bid = green (palette[0]), ask = red (palette[4], semantic sell/loss)\nconst BID_COLOR = t.palette[0];\nconst ASK_COLOR = t.palette[4];\n// Ask fill-opacity raised in dark mode: #AE3030 at 18% on #1A1A17 is nearly invisible\nconst ASK_FILL_OPACITY = theme === \"dark\" ? 0.28 : 0.18;\n\n// Park-Miller LCG — deterministic, no Date or Math.random\nlet _lcg = 42;\nconst rnd = () => { _lcg = (_lcg * 16807) % 2147483647; return _lcg / 2147483647; };\n\n// BTC/USD order book snapshot parameters\nconst MID_PRICE = 60000;\nconst BEST_BID  = 59990;\nconst BEST_ASK  = 60010;\nconst N         = 50;\nconst TICK      = 5;\n\n// Bid levels: best bid outward (price descends, cumulative quantity grows)\nconst bidLevels = [];\nconst bidWalls  = [];\nlet cumBid = 0;\nfor (let i = 0; i < N; i++) {\n  const price = BEST_BID - i * TICK;\n  let qty = 0.4 + rnd() * 1.6;\n  const isWall = i === 9 || i === 21 || i === 38;\n  if (isWall) qty *= 7;  // support walls\n  cumBid += qty;\n  bidLevels.push({ price, cumQty: cumBid });\n  if (isWall) bidWalls.push({ price, cumQty: cumBid });\n}\n\n// Ask levels: best ask outward (price rises, cumulative quantity grows)\nconst askLevels = [];\nconst askWalls  = [];\nlet cumAsk = 0;\nfor (let i = 0; i < N; i++) {\n  const price = BEST_ASK + i * TICK;\n  let qty = 0.4 + rnd() * 1.6;\n  const isWall = i === 7 || i === 18 || i === 34;\n  if (isWall) qty *= 7;  // resistance walls\n  cumAsk += qty;\n  askLevels.push({ price, cumQty: cumAsk });\n  if (isWall) askWalls.push({ price, cumQty: cumAsk });\n}\n\nconst maxCumQty = d3.max([cumBid, cumAsk]);\n\n// Area data: bids sorted ascending price (worst→best), asks ascending (best→worst)\n// No sentinel at mid price — gap between best_bid and best_ask is naturally visible\nconst bidAreaData = bidLevels.slice().reverse();\nconst askAreaData = askLevels;\n\n// Scales — symmetric around mid price for balanced visual\nconst halfRange = d3.max([\n  MID_PRICE - bidLevels[N - 1].price,\n  askLevels[N - 1].price - MID_PRICE\n]) * 1.04;\nconst x = d3.scaleLinear()\n  .domain([MID_PRICE - halfRange, MID_PRICE + halfRange])\n  .range([0, iw]);\nconst y = d3.scaleLinear()\n  .domain([0, maxCumQty * 1.08])\n  .range([ih, 0]);\n\n// SVG root\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// Y-axis grid lines (rendered first so they sit behind data)\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call(gg => gg.select(\".domain\").remove())\n  .call(gg => gg.selectAll(\".tick line\").attr(\"stroke\", t.grid));\n\n// Bid-ask spread gap highlight — narrow shaded rect between best bid and best ask\ng.append(\"rect\")\n  .attr(\"x\", x(BEST_BID))\n  .attr(\"y\", 0)\n  .attr(\"width\", x(BEST_ASK) - x(BEST_BID))\n  .attr(\"height\", ih)\n  .attr(\"fill\", t.grid)\n  .attr(\"fill-opacity\", 0.4);\n\n// Step-area and step-line generators (curveStepAfter gives left-continuous staircase)\nconst areaGen = d3.area().curve(d3.curveStepAfter)\n  .x(d => x(d.price)).y0(ih).y1(d => y(d.cumQty));\nconst lineGen = d3.line().curve(d3.curveStepAfter)\n  .x(d => x(d.price)).y(d => y(d.cumQty));\n\n// Bid semi-transparent fill\ng.append(\"path\").datum(bidAreaData)\n  .attr(\"fill\", BID_COLOR).attr(\"fill-opacity\", 0.18)\n  .attr(\"d\", areaGen);\n// Bid outline stroke\ng.append(\"path\").datum(bidAreaData)\n  .attr(\"fill\", \"none\").attr(\"stroke\", BID_COLOR).attr(\"stroke-width\", 2)\n  .attr(\"d\", lineGen);\n\n// Ask semi-transparent fill (raised opacity in dark mode for visibility on #1A1A17)\ng.append(\"path\").datum(askAreaData)\n  .attr(\"fill\", ASK_COLOR).attr(\"fill-opacity\", ASK_FILL_OPACITY)\n  .attr(\"d\", areaGen);\n// Ask outline stroke\ng.append(\"path\").datum(askAreaData)\n  .attr(\"fill\", \"none\").attr(\"stroke\", ASK_COLOR).attr(\"stroke-width\", 2)\n  .attr(\"d\", lineGen);\n\n// Wall annotations — D3 data join renders callout lines + price labels at support/resistance\nconst priceLabel = d3.format(\"$,\");\nconst WALL_TICK_LEN = 18;\n\ng.selectAll(\".bid-wall\").data(bidWalls).join(\"g\")\n  .attr(\"class\", \"bid-wall\")\n  .call(sel => sel.append(\"line\")\n    .attr(\"x1\", d => x(d.price)).attr(\"y1\", d => y(d.cumQty) - 4)\n    .attr(\"x2\", d => x(d.price)).attr(\"y2\", d => y(d.cumQty) - WALL_TICK_LEN)\n    .attr(\"stroke\", BID_COLOR).attr(\"stroke-width\", 1.5).attr(\"opacity\", 0.7))\n  .call(sel => sel.append(\"text\")\n    .attr(\"x\", d => x(d.price)).attr(\"y\", d => y(d.cumQty) - WALL_TICK_LEN - 5)\n    .attr(\"text-anchor\", \"middle\").attr(\"fill\", BID_COLOR)\n    .style(\"font-size\", \"11px\").style(\"font-weight\", \"500\")\n    .text(d => priceLabel(d.price)));\n\ng.selectAll(\".ask-wall\").data(askWalls).join(\"g\")\n  .attr(\"class\", \"ask-wall\")\n  .call(sel => sel.append(\"line\")\n    .attr(\"x1\", d => x(d.price)).attr(\"y1\", d => y(d.cumQty) - 4)\n    .attr(\"x2\", d => x(d.price)).attr(\"y2\", d => y(d.cumQty) - WALL_TICK_LEN)\n    .attr(\"stroke\", ASK_COLOR).attr(\"stroke-width\", 1.5).attr(\"opacity\", 0.7))\n  .call(sel => sel.append(\"text\")\n    .attr(\"x\", d => x(d.price)).attr(\"y\", d => y(d.cumQty) - WALL_TICK_LEN - 5)\n    .attr(\"text-anchor\", \"middle\").attr(\"fill\", ASK_COLOR)\n    .style(\"font-size\", \"11px\").style(\"font-weight\", \"500\")\n    .text(d => priceLabel(d.price)));\n\n// Mid-price dashed vertical line\nconst midX = x(MID_PRICE);\ng.append(\"line\")\n  .attr(\"x1\", midX).attr(\"y1\", 0)\n  .attr(\"x2\", midX).attr(\"y2\", ih)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"8,5\").attr(\"opacity\", 0.8);\n\n// Mid-price and spread annotation (left of the dashed line, near top)\ng.append(\"text\")\n  .attr(\"x\", midX - 12).attr(\"y\", 22)\n  .attr(\"text-anchor\", \"end\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(`Mid: $${d3.format(\",\")(MID_PRICE)}`);\ng.append(\"text\")\n  .attr(\"x\", midX - 12).attr(\"y\", 44)\n  .attr(\"text-anchor\", \"end\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(`Spread: $${BEST_ASK - BEST_BID}`);\n\n// X-axis\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickFormat(d3.format(\"$,\")));\n\n// Y-axis\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickFormat(d => d.toFixed(0)));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\".tick 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).attr(\"y\", ih + 58)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Price (USD)\");\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(-65,${ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Cumulative Volume (BTC)\");\n\n// Legend (top-right of plot area)\nconst legendX = iw - 210;\nconst legendY = 20;\n[\n  { color: BID_COLOR, label: \"Bids (Buy Orders)\" },\n  { color: ASK_COLOR, label: \"Asks (Sell Orders)\" },\n].forEach(({ color, label }, i) => {\n  const ly = legendY + i * 32;\n  g.append(\"rect\")\n    .attr(\"x\", legendX).attr(\"y\", ly - 11)\n    .attr(\"width\", 24).attr(\"height\", 14).attr(\"rx\", 2)\n    .attr(\"fill\", color).attr(\"fill-opacity\", 0.2);\n  g.append(\"line\")\n    .attr(\"x1\", legendX).attr(\"y1\", ly - 4)\n    .attr(\"x2\", legendX + 24).attr(\"y2\", ly - 4)\n    .attr(\"stroke\", color).attr(\"stroke-width\", 2);\n  g.append(\"text\")\n    .attr(\"x\", legendX + 32).attr(\"y\", ly)\n    .attr(\"fill\", t.ink).style(\"font-size\", \"14px\")\n    .text(label);\n});\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"depth-order-book · javascript · d3 · anyplot.ai\");\n"}