{"spec_id":"titration-curve","library":"d3","language":"javascript","code":"// anyplot.ai\n// titration-curve: Acid-Base Titration Curve\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-24\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 125, bottom: 85, left: 85 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: 25 mL of 0.1 M HCl titrated with 0.1 M NaOH (strong acid / strong base) ---\n// 501 points: 0.0, 0.1, 0.2, … 50.0 mL\nconst curveData = Array.from({ length: 501 }, (_, i) => {\n  const v = i * 0.1;\n  const mHCl = 0.0025, mNaOH = 0.1 * v * 0.001, totalVol = (25 + v) * 0.001;\n  const excess = mNaOH - mHCl;\n  const ph = Math.abs(excess) < 1e-12 ? 7.0 : excess < 0 ? -Math.log10(-excess / totalVol) : 14 + Math.log10(excess / totalVol);\n  return { v, ph };\n});\n\n// Central-difference numerical derivative dpH/dV (floor at 0 to suppress float noise at edges)\nconst derivData = curveData.slice(1, -1).map((_, i) => ({\n  v: curveData[i + 1].v,\n  dphdv: Math.max(0, (curveData[i + 2].ph - curveData[i].ph) / (curveData[i + 2].v - curveData[i].v)),\n}));\n\n// --- SVG ---\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 xSc = d3.scaleLinear().domain([0, 50]).range([0, iw]);\nconst ySc = d3.scaleLinear().domain([0, 14]).range([ih, 0]);\nconst maxD = d3.max(derivData, (d) => d.dphdv);\nconst dSc = d3.scaleLinear().domain([0, Math.ceil(maxD * 1.2)]).range([ih, 0]);\n\n// --- Gridlines (pH axis) ---\ng.selectAll(\".ygrid\")\n  .data(ySc.ticks(7))\n  .join(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", (d) => ySc(d)).attr(\"y2\", (d) => ySc(d))\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n\n// --- pH 7 reference line ---\ng.append(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", ySc(7)).attr(\"y2\", ySc(7))\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1)\n  .attr(\"stroke-dasharray\", \"4,4\").attr(\"opacity\", 0.4);\n\n// --- Steep transition zone highlight (equivalence region) ---\nconst shadeX = xSc(23), shadeW = xSc(27) - xSc(23);\ng.append(\"rect\")\n  .attr(\"x\", shadeX).attr(\"y\", 0)\n  .attr(\"width\", shadeW).attr(\"height\", ih)\n  .attr(\"fill\", t.palette[0]).attr(\"opacity\", 0.07);\ng.append(\"text\")\n  .attr(\"x\", shadeX + shadeW / 2).attr(\"y\", ih - 10)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\").attr(\"opacity\", 0.75)\n  .text(\"Steep transition zone\");\n\n// --- Derivative curve (dpH/dV) on secondary right axis ---\ng.append(\"path\")\n  .datum(derivData)\n  .attr(\"d\", d3.line().x((d) => xSc(d.v)).y((d) => dSc(d.dphdv)).curve(d3.curveCatmullRom.alpha(0.5)))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"8,4\")\n  .attr(\"stroke-linecap\", \"round\");\n\n// --- Titration curve (pH) ---\ng.append(\"path\")\n  .datum(curveData)\n  .attr(\"d\", d3.line().x((d) => xSc(d.v)).y((d) => ySc(d.ph)).curve(d3.curveMonotoneX))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-linecap\", \"round\")\n  .attr(\"stroke-linejoin\", \"round\");\n\n// --- Equivalence point: vertical dashed line ---\ng.append(\"line\")\n  .attr(\"x1\", xSc(25)).attr(\"x2\", xSc(25))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.ink).attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"10,5\").attr(\"opacity\", 0.55);\n\n// --- Equivalence point marker dot ---\ng.append(\"circle\")\n  .attr(\"cx\", xSc(25)).attr(\"cy\", ySc(7))\n  .attr(\"r\", 7).attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2.5);\n\n// --- Equivalence point annotation ---\nconst eqX = xSc(25) + 14;\nconst eqY = ySc(7) - 14;\ng.append(\"rect\")\n  .attr(\"x\", eqX - 5).attr(\"y\", eqY - 17)\n  .attr(\"width\", 165).attr(\"height\", 44)\n  .attr(\"fill\", t.elevatedBg).attr(\"rx\", 5).attr(\"opacity\", 0.93);\ng.append(\"text\")\n  .attr(\"x\", eqX + 2).attr(\"y\", eqY).attr(\"fill\", t.ink)\n  .style(\"font-size\", \"13px\").style(\"font-weight\", \"600\")\n  .text(\"Equivalence point\");\ng.append(\"text\")\n  .attr(\"x\", eqX + 2).attr(\"y\", eqY + 19).attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .text(\"V = 25.0 mL,  pH = 7.0\");\n\n// --- X axis ---\nconst xAx = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(xSc).ticks(10).tickSize(6));\nxAx.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAx.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAx.select(\".domain\").attr(\"stroke\", \"none\");\n\n// --- Left y-axis (pH) ---\nconst yAxL = g.append(\"g\").call(d3.axisLeft(ySc).ticks(7).tickSize(6));\nyAxL.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAxL.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAxL.select(\".domain\").attr(\"stroke\", \"none\");\n\n// --- Right y-axis (dpH/dV) — styled in derivative color ---\nconst yAxR = g.append(\"g\")\n  .attr(\"transform\", `translate(${iw},0)`)\n  .call(d3.axisRight(dSc).ticks(5).tickSize(6));\nyAxR.selectAll(\"text\").attr(\"fill\", t.palette[1]).style(\"font-size\", \"13px\");\nyAxR.selectAll(\"line\").attr(\"stroke\", t.palette[1]).attr(\"opacity\", 0.6);\nyAxR.select(\".domain\").attr(\"stroke\", t.palette[1]).attr(\"opacity\", 0.6);\n\n// --- Axis labels ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2).attr(\"y\", height - 16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Volume of NaOH Added (mL)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(22,${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"pH\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(${width - 20},${margin.top + ih / 2}) rotate(90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.palette[1]).style(\"font-size\", \"14px\")\n  .text(\"dpH/dV (pH per mL)\");\n\n// --- Legend ---\nconst lx = 30, ly = 20;\ng.append(\"line\")\n  .attr(\"x1\", lx).attr(\"x2\", lx + 30).attr(\"y1\", ly + 8).attr(\"y2\", ly + 8)\n  .attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 3.5);\ng.append(\"text\")\n  .attr(\"x\", lx + 38).attr(\"y\", ly + 13).attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n  .text(\"Titration curve (pH)\");\n\ng.append(\"line\")\n  .attr(\"x1\", lx).attr(\"x2\", lx + 30).attr(\"y1\", ly + 34).attr(\"y2\", ly + 34)\n  .attr(\"stroke\", t.palette[1]).attr(\"stroke-width\", 2.5).attr(\"stroke-dasharray\", \"8,4\");\ng.append(\"text\")\n  .attr(\"x\", lx + 38).attr(\"y\", ly + 39).attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n  .text(\"Derivative (dpH/dV)\");\n\n// --- Title ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 50).attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"titration-curve · javascript · d3 · anyplot.ai\");\n"}