{"spec_id":"streamline-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// streamline-basic: Basic Streamline Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Vector field: potential flow past a circular cylinder ------------------\n// Uniform stream (speed U) plus a doublet of strength kappa placed at the\n// origin. Their superposition is the classic textbook CFD demo: streamlines\n// approach from the left, split around a circular obstacle of radius\n// R = sqrt(kappa / U), and reconverge downstream.\nconst freeStreamSpeed = 1;\nconst doubletStrength = 1;\nconst cylinderRadius = Math.sqrt(doubletStrength / freeStreamSpeed);\n\nconst fieldAt = (x, y) => {\n  const r2 = Math.max(x * x + y * y, 1e-6);\n  const u = freeStreamSpeed + (doubletStrength * (y * y - x * x)) / (r2 * r2);\n  const v = (-2 * doubletStrength * x * y) / (r2 * r2);\n  return { u, v, speed: Math.sqrt(u * u + v * v) };\n};\n\nconst rk4Step = (x, y, dt) => {\n  const k1 = fieldAt(x, y);\n  const k2 = fieldAt(x + (dt / 2) * k1.u, y + (dt / 2) * k1.v);\n  const k3 = fieldAt(x + (dt / 2) * k2.u, y + (dt / 2) * k2.v);\n  const k4 = fieldAt(x + dt * k3.u, y + dt * k3.v);\n  return {\n    x: x + (dt / 6) * (k1.u + 2 * k2.u + 2 * k3.u + k4.u),\n    y: y + (dt / 6) * (k1.v + 2 * k2.v + 2 * k3.v + k4.v),\n  };\n};\n\n// --- Trace streamlines from seeds along the left inflow edge ----------------\nconst domainX = [-4, 4];\nconst domainY = [-2.5, 2.5];\nconst stepSize = 0.08;\nconst maxSteps = 160;\n\nconst seedYs = d3.range(-2.375, 2.4, 0.25);\nconst streamlines = seedYs.map((y0) => {\n  const points = [];\n  let x = domainX[0];\n  let y = y0;\n  for (let i = 0; i < maxSteps; i += 1) {\n    const r = Math.sqrt(x * x + y * y);\n    if (r < cylinderRadius * 0.98) break;\n    points.push({ x, y, speed: fieldAt(x, y).speed });\n    if (x < domainX[0] - 0.1 || x > domainX[1] + 0.1 || y < domainY[0] - 0.1 || y > domainY[1] + 0.1) break;\n    const next = rk4Step(x, y, stepSize);\n    x = next.x;\n    y = next.y;\n  }\n  return points;\n});\n\nconst allSpeeds = streamlines.flat().map((p) => p.speed);\nconst speedExtent = d3.extent(allSpeeds);\n\n// --- Scales -------------------------------------------------------------\nconst margin = { top: 100, right: 170, bottom: 80, left: 80 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Equal x/y pixel-per-unit so the cylinder renders as a true circle.\nconst pxPerUnit = ih / (domainY[1] - domainY[0]);\nconst plottedWidth = pxPerUnit * (domainX[1] - domainX[0]);\nconst offsetX = margin.left + (iw - plottedWidth) / 2;\n\nconst xScale = d3.scaleLinear().domain(domainX).range([offsetX, offsetX + plottedWidth]);\nconst yScale = d3.scaleLinear().domain(domainY).range([margin.top + ih, margin.top]);\nconst speedColor = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain(speedExtent);\nconst widthScale = d3.scaleLinear().domain(speedExtent).range([1.4, 3.6]).clamp(true);\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Axes -------------------------------------------------------------------\nconst xAxis = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${margin.top + ih})`)\n  .call(d3.axisBottom(xScale).ticks(8));\nconst yAxis = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${offsetX},0)`)\n  .call(d3.axisLeft(yScale).ticks(6));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.grid);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", offsetX + plottedWidth / 2)\n  .attr(\"y\", margin.top + ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"X Position (R)\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", `translate(${margin.left - 48}, ${margin.top + ih / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Y Position (R)\");\n\n// --- Cylinder obstacle --------------------------------------------------\nsvg\n  .append(\"circle\")\n  .attr(\"cx\", xScale(0))\n  .attr(\"cy\", yScale(0))\n  .attr(\"r\", pxPerUnit * cylinderRadius)\n  .attr(\"fill\", t.inkSoft)\n  .attr(\"fill-opacity\", 0.4)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2);\n\n// --- Streamlines, colored + widened by local velocity magnitude -------------\nconst segments = streamlines.flatMap((points) =>\n  points.slice(1).map((p1, i) => {\n    const p0 = points[i];\n    return { x1: p0.x, y1: p0.y, x2: p1.x, y2: p1.y, speed: (p0.speed + p1.speed) / 2 };\n  }),\n);\n\nsvg\n  .append(\"defs\")\n  .append(\"clipPath\")\n  .attr(\"id\", \"plotClip\")\n  .append(\"rect\")\n  .attr(\"x\", offsetX)\n  .attr(\"y\", margin.top)\n  .attr(\"width\", plottedWidth)\n  .attr(\"height\", ih);\n\nsvg\n  .append(\"g\")\n  .attr(\"clip-path\", \"url(#plotClip)\")\n  .selectAll(\"line\")\n  .data(segments)\n  .join(\"line\")\n  .attr(\"x1\", (d) => xScale(d.x1))\n  .attr(\"y1\", (d) => yScale(d.y1))\n  .attr(\"x2\", (d) => xScale(d.x2))\n  .attr(\"y2\", (d) => yScale(d.y2))\n  .attr(\"stroke\", (d) => speedColor(d.speed))\n  .attr(\"stroke-width\", (d) => widthScale(d.speed))\n  .attr(\"stroke-linecap\", \"round\");\n\n// --- Colorbar legend (velocity magnitude) ------------------------------\nconst legendWidth = 24;\nconst legendHeight = 400;\nconst legendX = width - margin.right + 55;\nconst legendY = margin.top + (ih - legendHeight) / 2;\n\nconst gradient = svg\n  .append(\"defs\")\n  .append(\"linearGradient\")\n  .attr(\"id\", \"speedGradient\")\n  .attr(\"x1\", \"0\")\n  .attr(\"x2\", \"0\")\n  .attr(\"y1\", \"1\")\n  .attr(\"y2\", \"0\");\nd3.range(0, 1.001, 0.1).forEach((frac) => {\n  gradient\n    .append(\"stop\")\n    .attr(\"offset\", `${frac * 100}%`)\n    .attr(\"stop-color\", speedColor(speedExtent[0] + frac * (speedExtent[1] - speedExtent[0])));\n});\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY)\n  .attr(\"width\", legendWidth)\n  .attr(\"height\", legendHeight)\n  .attr(\"rx\", 3)\n  .attr(\"ry\", 3)\n  .attr(\"fill\", \"url(#speedGradient)\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendWidth / 2)\n  .attr(\"y\", legendY - 20)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text(\"|v|\");\n\nconst legendTicks = [\n  { y: legendY, value: speedExtent[1] },\n  { y: legendY + legendHeight, value: speedExtent[0] },\n];\nfor (const tick of legendTicks) {\n  svg\n    .append(\"line\")\n    .attr(\"x1\", legendX + legendWidth)\n    .attr(\"x2\", legendX + legendWidth + 6)\n    .attr(\"y1\", tick.y)\n    .attr(\"y2\", tick.y)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1);\n  svg\n    .append(\"text\")\n    .attr(\"x\", legendX + legendWidth + 10)\n    .attr(\"y\", tick.y)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(tick.value.toFixed(2));\n}\n\n// --- Annotation: mark the peak-speed point at the cylinder's shoulder -------\nconst flatPoints = streamlines.flat();\nconst maxSpeedPoint = flatPoints.reduce((best, p) => (p.speed > best.speed ? p : best));\nconst maxPx = xScale(maxSpeedPoint.x);\nconst maxPy = yScale(maxSpeedPoint.y);\nconst labelDx = 46;\nconst labelDy = maxSpeedPoint.y >= 0 ? -34 : 34;\n\nsvg\n  .append(\"line\")\n  .attr(\"x1\", maxPx)\n  .attr(\"y1\", maxPy)\n  .attr(\"x2\", maxPx + labelDx)\n  .attr(\"y2\", maxPy + labelDy)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.2);\n\nsvg\n  .append(\"circle\")\n  .attr(\"cx\", maxPx)\n  .attr(\"cy\", maxPy)\n  .attr(\"r\", 5)\n  .attr(\"fill\", speedColor(maxSpeedPoint.speed))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", maxPx + labelDx + 6)\n  .attr(\"y\", maxPy + labelDy)\n  .attr(\"dominant-baseline\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(`Peak speed |v| ≈ ${maxSpeedPoint.speed.toFixed(2)}`);\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(\"streamline-basic · javascript · d3 · anyplot.ai\");\n"}