{"spec_id":"renko-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// renko-basic: Basic Renko Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 70, bottom: 90, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: deterministic daily-close random walk (in-memory, LCG PRNG) ------\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\n// Four phases (uptrend, consolidation, downtrend, recovery) so the resulting\n// bricks trace a clear trend story, matching the spec's \"identify trend\n// directions\" application.\nconst N_OBSERVATIONS = 220;\nconst BRICK_SIZE = 3; // $ price move required to draw a new brick\nconst phases = [\n  { end: 60, drift: 0.18 },\n  { end: 100, drift: 0.0 },\n  { end: 165, drift: -0.16 },\n  { end: N_OBSERVATIONS, drift: 0.22 },\n];\nconst closes = [148];\nfor (let i = 1; i < N_OBSERVATIONS; i++) {\n  const drift = phases.find((p) => i < p.end).drift;\n  const noise = (rand() - 0.5) * 4.5;\n  closes.push(Math.max(60, closes[i - 1] + drift + noise));\n}\n\n// --- Renko brick construction ------------------------------------------------\n// A new brick is emitted every time the close crosses a full BRICK_SIZE step\n// away from the last brick boundary, in either direction — this is what lets\n// a run of up bricks reverse into a run of down bricks (trend reversal).\nconst bricks = [];\nlet base = Math.round(closes[0] / BRICK_SIZE) * BRICK_SIZE;\nfor (let i = 1; i < closes.length; i++) {\n  let diff = closes[i] - base;\n  while (diff >= BRICK_SIZE) {\n    const open = base;\n    base += BRICK_SIZE;\n    diff -= BRICK_SIZE;\n    bricks.push({ open, close: base, up: true });\n  }\n  while (diff <= -BRICK_SIZE) {\n    const open = base;\n    base -= BRICK_SIZE;\n    diff += BRICK_SIZE;\n    bricks.push({ open, close: base, up: false });\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// Diagonal hatch pattern: a redundant non-color cue for bearish bricks so\n// direction reads correctly for colorblind viewers, not solely via hue.\nsvg\n  .append(\"defs\")\n  .append(\"pattern\")\n  .attr(\"id\", \"bearish-hatch\")\n  .attr(\"width\", 7)\n  .attr(\"height\", 7)\n  .attr(\"patternUnits\", \"userSpaceOnUse\")\n  .attr(\"patternTransform\", \"rotate(45)\")\n  .append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"y1\", 0)\n  .attr(\"x2\", 0)\n  .attr(\"y2\", 7)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-opacity\", 0.4);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3\n  .scaleBand()\n  .domain(d3.range(bricks.length))\n  .range([0, iw])\n  .paddingInner(0.22)\n  .paddingOuter(0.05);\nconst levels = bricks.flatMap((b) => [b.open, b.close]);\nconst y = d3\n  .scaleLinear()\n  .domain([d3.min(levels) - BRICK_SIZE, d3.max(levels) + BRICK_SIZE])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y-axis only, subtle) -------------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Close-price trace (d3-shape line generator through brick midpoints) ----\n// A distinctive d3-shape touch beyond the manual rect join: a dashed line\n// tracing each brick's closing level, drawn beneath the bricks so it only\n// peeks through the small inter-brick gaps — reinforcing the underlying price\n// path without competing visually with the brick fills.\nconst closeTrace = d3\n  .line()\n  .x((d, i) => x(i) + x.bandwidth() / 2)\n  .y((d) => y(d.close))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\")\n  .datum(bricks)\n  .attr(\"d\", closeTrace)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-opacity\", 0.45)\n  .attr(\"stroke-dasharray\", \"2,3\");\n\n// --- Bricks ---------------------------------------------------------------\n// Bullish (up) -> Imprint brand green; Bearish (down) -> Imprint matte red —\n// the semantic finance exception (profit/up -> green, loss/down -> red),\n// labeled explicitly in the legend below.\ng.selectAll(\"rect.brick\")\n  .data(bricks)\n  .join(\"rect\")\n  .attr(\"class\", \"brick\")\n  .attr(\"x\", (d, i) => x(i))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"y\", (d) => y(Math.max(d.open, d.close)))\n  .attr(\"height\", (d) => Math.abs(y(d.close) - y(d.open)))\n  .attr(\"fill\", (d) => (d.up ? t.palette[0] : t.palette[4]))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// Hatch overlay on bearish bricks only — the redundant non-color direction cue.\ng.selectAll(\"rect.brick-hatch\")\n  .data(bricks.map((d, i) => ({ ...d, i })).filter((d) => !d.up))\n  .join(\"rect\")\n  .attr(\"class\", \"brick-hatch\")\n  .attr(\"x\", (d) => x(d.i))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"y\", (d) => y(Math.max(d.open, d.close)))\n  .attr(\"height\", (d) => Math.abs(y(d.close) - y(d.open)))\n  .attr(\"fill\", \"url(#bearish-hatch)\")\n  .attr(\"pointer-events\", \"none\");\n\n// --- Peak callout: mark the series' single highest brick (data storytelling) ---\nlet peakIdx = 0;\nlet peakPrice = -Infinity;\nbricks.forEach((d, i) => {\n  const top = Math.max(d.open, d.close);\n  if (top > peakPrice) {\n    peakPrice = top;\n    peakIdx = i;\n  }\n});\nconst peakX = x(peakIdx) + x.bandwidth() / 2;\nconst peakY = y(peakPrice);\ng.append(\"circle\")\n  .attr(\"cx\", peakX)\n  .attr(\"cy\", peakY)\n  .attr(\"r\", 4.5)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5);\nconst peakLabelAbove = peakY - 16 >= 10;\ng.append(\"text\")\n  .attr(\"x\", Math.min(Math.max(peakX, 70), iw - 70))\n  .attr(\"y\", peakLabelAbove ? peakY - 16 : peakY + 24)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(`Peak: $${peakPrice.toFixed(0)}`);\n\n// --- Axes -----------------------------------------------------------------\nconst tickEvery = Math.max(1, Math.ceil(bricks.length / 12));\nconst xTickValues = d3.range(bricks.length).filter((i) => i % tickEvery === 0);\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues(xTickValues).tickFormat((i) => i + 1));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).tickFormat(d3.format(\"$,.0f\")));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\").style(\"font-weight\", \"400\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels (bolder weight than tick labels for typographic hierarchy) --\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Brick Index\");\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -96)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Price ($)\");\n\n// --- Legend (semantic color mapping must be explicit) ------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top - 56})`);\nconst legendItems = [\n  { label: \"Bullish (Up)\", color: t.palette[0] },\n  { label: \"Bearish (Down)\", color: t.palette[4] },\n];\nlegendItems.forEach((item, i) => {\n  const item_g = legend.append(\"g\").attr(\"transform\", `translate(${i * 190},0)`);\n  item_g.append(\"rect\").attr(\"width\", 22).attr(\"height\", 22).attr(\"fill\", item.color);\n  item_g\n    .append(\"text\")\n    .attr(\"x\", 32)\n    .attr(\"y\", 17)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"16px\")\n    .text(item.label);\n});\n\n// --- Title --------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"renko-basic · javascript · d3 · anyplot.ai\");\n"}