{"spec_id":"waterfall-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// waterfall-basic: Basic Waterfall Chart\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-08-04\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 140, right: 60, bottom: 160, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic): quarterly revenue bridge, $K --------\nconst changes = [\n  { label: \"Starting Revenue\", value: 850, type: \"total\" },\n  { label: \"Cost of Goods Sold\", value: -320, type: \"change\" },\n  { label: \"Operating Expenses\", value: -180, type: \"change\" },\n  { label: \"R&D Investment\", value: -95, type: \"change\" },\n  { label: \"Marketing Spend\", value: -60, type: \"change\" },\n  { label: \"Tax Adjustment\", value: -45, type: \"change\" },\n  { label: \"Other Income\", value: 25, type: \"change\" },\n  { label: \"Net Profit\", value: null, type: \"total\" },\n];\n\nlet cumulative = 0;\nconst steps = changes.map((d) => {\n  if (d.type === \"total\" && d.value !== null) {\n    cumulative = d.value;\n    return { ...d, start: 0, end: d.value };\n  }\n  if (d.type === \"change\") {\n    const start = cumulative;\n    cumulative += d.value;\n    return { ...d, start, end: cumulative };\n  }\n  // final total: value derived from the running cumulative\n  return { ...d, value: cumulative, start: 0, end: cumulative };\n});\n\nconst fmtTotal = (v) => `$${d3.format(\",\")(Math.round(v))}K`;\nconst fmtChange = (v) => `${v >= 0 ? \"+\" : \"\"}${d3.format(\",\")(Math.round(v))}K`;\n\n// --- Scales ------------------------------------------------------------------\nconst x = d3\n  .scaleBand()\n  .domain(steps.map((d) => d.label))\n  .range([0, iw])\n  .padding(0.35);\n\nconst maxLevel = d3.max(steps, (d) => Math.max(d.start, d.end));\nconst y = d3\n  .scaleLinear()\n  .domain([0, maxLevel * 1.12])\n  .nice()\n  .range([ih, 0]);\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// --- Y grid (horizontal only) --------------------------------------------------\ng.append(\"g\")\n  .call(d3.axisLeft(y).ticks(6).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Connecting lines (running-total flow) -------------------------------------\nfor (let i = 0; i < steps.length - 1; i++) {\n  const level = steps[i].end;\n  g.append(\"line\")\n    .attr(\"x1\", x(steps[i].label) + x.bandwidth())\n    .attr(\"x2\", x(steps[i + 1].label))\n    .attr(\"y1\", y(level))\n    .attr(\"y2\", y(level))\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1.5)\n    .attr(\"stroke-dasharray\", \"4,4\")\n    .attr(\"opacity\", 0.6);\n}\n\n// --- Bars -----------------------------------------------------------------------\nconst barColor = (d) => {\n  if (d.type === \"total\") return t.ink;\n  return d.value >= 0 ? t.palette[0] : t.palette[4];\n};\n\ng.selectAll(\"rect.bar\")\n  .data(steps)\n  .join(\"rect\")\n  .attr(\"class\", \"bar\")\n  .attr(\"x\", (d) => x(d.label))\n  .attr(\"y\", (d) => y(Math.max(d.start, d.end)))\n  .attr(\"width\", x.bandwidth())\n  .attr(\"height\", (d) => Math.max(1, y(Math.min(d.start, d.end)) - y(Math.max(d.start, d.end))))\n  .attr(\"fill\", barColor)\n  .attr(\"rx\", 3);\n\n// --- Value labels -----------------------------------------------------------------\ng.selectAll(\"text.value\")\n  .data(steps)\n  .join(\"text\")\n  .attr(\"class\", \"value\")\n  .attr(\"x\", (d) => x(d.label) + x.bandwidth() / 2)\n  .attr(\"y\", (d) => y(Math.max(d.start, d.end)) - 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", (d) => (d.type === \"total\" ? t.ink : barColor(d)))\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => (d.type === \"total\" ? fmtTotal(d.value) : fmtChange(d.value)));\n\n// --- X axis -----------------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x));\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxis\n  .selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .attr(\"transform\", \"rotate(-30)\")\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"dx\", \"-0.6em\")\n  .attr(\"dy\", \"0.3em\");\n\n// --- Y axis -----------------------------------------------------------------------\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat((v) => `$${d3.format(\",\")(v)}K`));\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\nyAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Amount ($K)\");\n\n// --- Legend ------------------------------------------------------------------------\nconst legendItems = [\n  { label: \"Increase\", color: t.palette[0] },\n  { label: \"Decrease\", color: t.palette[4] },\n  { label: \"Total\", color: t.ink },\n];\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${width - margin.right - 340},70)`);\nlegendItems.forEach((item, i) => {\n  const lg = legend.append(\"g\").attr(\"transform\", `translate(${i * 115},0)`);\n  lg.append(\"rect\").attr(\"width\", 16).attr(\"height\", 16).attr(\"rx\", 3).attr(\"fill\", item.color);\n  lg.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});\n\n// --- Title ---------------------------------------------------------------------------\nconst title = \"Quarterly Revenue Bridge · waterfall-basic · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / title.length)));\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\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}