{"spec_id":"quiver-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// quiver-basic: Basic Quiver Plot\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 92/100 | Created: 2026-07-24\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: ocean-current vortex sampled on a uniform buoy grid -------------\n// A rotating eddy (u = -y, v = x) with Gaussian decay from the eddy core,\n// a classic pattern for coastal/oceanographic current surveys.\nconst gridSize = 15;\nconst domainExtent = 3; // km from the eddy core\nconst points = [];\nfor (let i = 0; i < gridSize; i++) {\n  for (let j = 0; j < gridSize; j++) {\n    const x = -domainExtent + (2 * domainExtent * i) / (gridSize - 1);\n    const y = -domainExtent + (2 * domainExtent * j) / (gridSize - 1);\n    const decay = Math.exp(-(x * x + y * y) / 6);\n    const u = -y * decay; // eastward component (cm/s)\n    const v = x * decay; // northward component (cm/s)\n    points.push({ x, y, u, v, mag: Math.sqrt(u * u + v * v) });\n  }\n}\nconst maxMag = d3.max(points, (d) => d.mag);\n\n// --- Layout: force a square plot area so vector angles render undistorted -\nconst margin = { top: 140, right: 90, bottom: 110, left: 110 };\nconst availW = width - margin.left - margin.right;\nconst availH = height - margin.top - margin.bottom;\nconst side = Math.min(availW, availH);\nconst offsetX = margin.left + (availW - side) / 2;\nconst offsetY = margin.top + (availH - side) / 2;\n\n// --- Scales -----------------------------------------------------------------\nconst xScale = d3.scaleLinear().domain([-domainExtent, domainExtent]).range([0, side]);\nconst yScale = d3.scaleLinear().domain([-domainExtent, domainExtent]).range([side, 0]);\nconst cellSize = side / (gridSize - 1);\nconst pxPerUnit = (cellSize * 0.85) / maxMag; // arrow length scaled to fit the cell\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, maxMag]);\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(${offsetX},${offsetY})`);\n\n// --- Axes ---------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${side})`)\n  .call(d3.axisBottom(xScale).ticks(7).tickFormat((d) => `${d}`));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(yScale).ticks(7).tickFormat((d) => `${d}`));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 0.75).attr(\"stroke-opacity\", 0.5);\n}\n\n// faint reference grid so arrow positions read against the coordinate frame\ng.selectAll(\"line.gridx\")\n  .data(xScale.ticks(7))\n  .join(\"line\")\n  .attr(\"class\", \"gridx\")\n  .attr(\"x1\", (d) => xScale(d))\n  .attr(\"x2\", (d) => xScale(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", side)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\ng.selectAll(\"line.gridy\")\n  .data(yScale.ticks(7))\n  .join(\"line\")\n  .attr(\"class\", \"gridy\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", side)\n  .attr(\"y1\", (d) => yScale(d))\n  .attr(\"y2\", (d) => yScale(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// subtle radial guide marking the eddy core's characteristic decay radius\nconst coreRadiusPx = xScale(Math.sqrt(6)) - xScale(0);\ng.append(\"circle\")\n  .attr(\"cx\", xScale(0))\n  .attr(\"cy\", yScale(0))\n  .attr(\"r\", coreRadiusPx)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1)\n  .attr(\"stroke-dasharray\", \"4,4\")\n  .attr(\"stroke-opacity\", 0.35);\n\n// --- Arrows (line + solid triangular head, colored by current speed) --------\nconst headAngle = Math.PI / 7;\nconst headLen = Math.max(7, cellSize * 0.2);\nconst minShaftLen = Math.max(headLen * 1.5, cellSize * 0.3); // keep near-zero vectors legible\n\nconst arrows = g.selectAll(\"g.arrow\").data(points).join(\"g\").attr(\"class\", \"arrow\");\narrows.each(function (d) {\n  const x0 = xScale(d.x);\n  const y0 = yScale(d.y);\n  const fill = color(d.mag);\n\n  if (d.mag < 1e-9) {\n    // exact stagnation point at the eddy core: direction undefined, mark with a dot\n    d3.select(this).append(\"circle\").attr(\"cx\", x0).attr(\"cy\", y0).attr(\"r\", 3).attr(\"fill\", fill);\n    return;\n  }\n\n  const lenPx = Math.max(d.mag * pxPerUnit, minShaftLen);\n  const dxPx = (d.u / d.mag) * lenPx;\n  const dyPx = (-d.v / d.mag) * lenPx; // screen y grows downward, data y grows upward\n  const x1 = x0 + dxPx;\n  const y1 = y0 + dyPx;\n  const angle = Math.atan2(dyPx, dxPx);\n\n  const hx1 = x1 - headLen * Math.cos(angle - headAngle);\n  const hy1 = y1 - headLen * Math.sin(angle - headAngle);\n  const hx2 = x1 - headLen * Math.cos(angle + headAngle);\n  const hy2 = y1 - headLen * Math.sin(angle + headAngle);\n\n  d3.select(this)\n    .append(\"line\")\n    .attr(\"x1\", x0)\n    .attr(\"y1\", y0)\n    .attr(\"x2\", x1)\n    .attr(\"y2\", y1)\n    .attr(\"stroke\", fill)\n    .attr(\"stroke-width\", 2.5)\n    .attr(\"stroke-linecap\", \"round\");\n\n  d3.select(this)\n    .append(\"path\")\n    .attr(\"d\", `M${x1},${y1} L${hx1},${hy1} L${hx2},${hy2} Z`)\n    .attr(\"fill\", fill);\n});\n\n// --- Axis labels --------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", side / 2)\n  .attr(\"y\", side + 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Distance east of eddy core (km)\");\n\ng.append(\"text\")\n  .attr(\"transform\", `translate(${-78},${side / 2}) rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Distance north of eddy core (km)\");\n\n// --- Speed legend (color encodes current magnitude) --------------------------\nconst legendW = 180;\nconst legendH = 14;\nconst legendX = offsetX + side - legendW;\nconst legendY = offsetY - 60;\n\nconst gradientId = \"quiver-speed-gradient\";\nconst defs = svg.append(\"defs\");\nconst gradient = defs\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0%\")\n  .attr(\"x2\", \"100%\");\ngradient.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.seq[0]);\ngradient.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.seq[1]);\n\nsvg\n  .append(\"rect\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY)\n  .attr(\"width\", legendW)\n  .attr(\"height\", legendH)\n  .attr(\"fill\", `url(#${gradientId})`);\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY - 10)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"Current speed\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX)\n  .attr(\"y\", legendY + legendH + 20)\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"0 cm/s\");\n\nsvg\n  .append(\"text\")\n  .attr(\"x\", legendX + legendW)\n  .attr(\"y\", legendY + legendH + 20)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(`${maxMag.toFixed(1)} cm/s`);\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(\"quiver-basic · javascript · d3 · anyplot.ai\");\n"}