diff --git a/plots/quiver-basic/implementations/javascript/d3.js b/plots/quiver-basic/implementations/javascript/d3.js new file mode 100644 index 0000000000..871b026364 --- /dev/null +++ b/plots/quiver-basic/implementations/javascript/d3.js @@ -0,0 +1,214 @@ +// anyplot.ai +// quiver-basic: Basic Quiver Plot +// Library: d3 7.9.0 | JavaScript 22.23.1 +// Quality: 92/100 | Created: 2026-07-24 +//# anyplot-orientation: square + +const t = window.ANYPLOT_TOKENS; +const { width, height } = window.ANYPLOT_SIZE; + +// --- Data: ocean-current vortex sampled on a uniform buoy grid ------------- +// A rotating eddy (u = -y, v = x) with Gaussian decay from the eddy core, +// a classic pattern for coastal/oceanographic current surveys. +const gridSize = 15; +const domainExtent = 3; // km from the eddy core +const points = []; +for (let i = 0; i < gridSize; i++) { + for (let j = 0; j < gridSize; j++) { + const x = -domainExtent + (2 * domainExtent * i) / (gridSize - 1); + const y = -domainExtent + (2 * domainExtent * j) / (gridSize - 1); + const decay = Math.exp(-(x * x + y * y) / 6); + const u = -y * decay; // eastward component (cm/s) + const v = x * decay; // northward component (cm/s) + points.push({ x, y, u, v, mag: Math.sqrt(u * u + v * v) }); + } +} +const maxMag = d3.max(points, (d) => d.mag); + +// --- Layout: force a square plot area so vector angles render undistorted - +const margin = { top: 140, right: 90, bottom: 110, left: 110 }; +const availW = width - margin.left - margin.right; +const availH = height - margin.top - margin.bottom; +const side = Math.min(availW, availH); +const offsetX = margin.left + (availW - side) / 2; +const offsetY = margin.top + (availH - side) / 2; + +// --- Scales ----------------------------------------------------------------- +const xScale = d3.scaleLinear().domain([-domainExtent, domainExtent]).range([0, side]); +const yScale = d3.scaleLinear().domain([-domainExtent, domainExtent]).range([side, 0]); +const cellSize = side / (gridSize - 1); +const pxPerUnit = (cellSize * 0.85) / maxMag; // arrow length scaled to fit the cell +const color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([0, maxMag]); + +// --- SVG mount ---------------------------------------------------------------- +const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height); +const g = svg.append("g").attr("transform", `translate(${offsetX},${offsetY})`); + +// --- Axes --------------------------------------------------------------------- +const xAxis = g + .append("g") + .attr("transform", `translate(0,${side})`) + .call(d3.axisBottom(xScale).ticks(7).tickFormat((d) => `${d}`)); +const yAxis = g.append("g").call(d3.axisLeft(yScale).ticks(7).tickFormat((d) => `${d}`)); +for (const ax of [xAxis, yAxis]) { + ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px"); + ax.selectAll("line").attr("stroke", t.grid); + ax.select(".domain").attr("stroke", t.inkSoft).attr("stroke-width", 0.75).attr("stroke-opacity", 0.5); +} + +// faint reference grid so arrow positions read against the coordinate frame +g.selectAll("line.gridx") + .data(xScale.ticks(7)) + .join("line") + .attr("class", "gridx") + .attr("x1", (d) => xScale(d)) + .attr("x2", (d) => xScale(d)) + .attr("y1", 0) + .attr("y2", side) + .attr("stroke", t.grid) + .attr("stroke-width", 1); +g.selectAll("line.gridy") + .data(yScale.ticks(7)) + .join("line") + .attr("class", "gridy") + .attr("x1", 0) + .attr("x2", side) + .attr("y1", (d) => yScale(d)) + .attr("y2", (d) => yScale(d)) + .attr("stroke", t.grid) + .attr("stroke-width", 1); + +// subtle radial guide marking the eddy core's characteristic decay radius +const coreRadiusPx = xScale(Math.sqrt(6)) - xScale(0); +g.append("circle") + .attr("cx", xScale(0)) + .attr("cy", yScale(0)) + .attr("r", coreRadiusPx) + .attr("fill", "none") + .attr("stroke", t.inkSoft) + .attr("stroke-width", 1) + .attr("stroke-dasharray", "4,4") + .attr("stroke-opacity", 0.35); + +// --- Arrows (line + solid triangular head, colored by current speed) -------- +const headAngle = Math.PI / 7; +const headLen = Math.max(7, cellSize * 0.2); +const minShaftLen = Math.max(headLen * 1.5, cellSize * 0.3); // keep near-zero vectors legible + +const arrows = g.selectAll("g.arrow").data(points).join("g").attr("class", "arrow"); +arrows.each(function (d) { + const x0 = xScale(d.x); + const y0 = yScale(d.y); + const fill = color(d.mag); + + if (d.mag < 1e-9) { + // exact stagnation point at the eddy core: direction undefined, mark with a dot + d3.select(this).append("circle").attr("cx", x0).attr("cy", y0).attr("r", 3).attr("fill", fill); + return; + } + + const lenPx = Math.max(d.mag * pxPerUnit, minShaftLen); + const dxPx = (d.u / d.mag) * lenPx; + const dyPx = (-d.v / d.mag) * lenPx; // screen y grows downward, data y grows upward + const x1 = x0 + dxPx; + const y1 = y0 + dyPx; + const angle = Math.atan2(dyPx, dxPx); + + const hx1 = x1 - headLen * Math.cos(angle - headAngle); + const hy1 = y1 - headLen * Math.sin(angle - headAngle); + const hx2 = x1 - headLen * Math.cos(angle + headAngle); + const hy2 = y1 - headLen * Math.sin(angle + headAngle); + + d3.select(this) + .append("line") + .attr("x1", x0) + .attr("y1", y0) + .attr("x2", x1) + .attr("y2", y1) + .attr("stroke", fill) + .attr("stroke-width", 2.5) + .attr("stroke-linecap", "round"); + + d3.select(this) + .append("path") + .attr("d", `M${x1},${y1} L${hx1},${hy1} L${hx2},${hy2} Z`) + .attr("fill", fill); +}); + +// --- Axis labels -------------------------------------------------------------- +g.append("text") + .attr("x", side / 2) + .attr("y", side + 70) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", "18px") + .text("Distance east of eddy core (km)"); + +g.append("text") + .attr("transform", `translate(${-78},${side / 2}) rotate(-90)`) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", "18px") + .text("Distance north of eddy core (km)"); + +// --- Speed legend (color encodes current magnitude) -------------------------- +const legendW = 180; +const legendH = 14; +const legendX = offsetX + side - legendW; +const legendY = offsetY - 60; + +const gradientId = "quiver-speed-gradient"; +const defs = svg.append("defs"); +const gradient = defs + .append("linearGradient") + .attr("id", gradientId) + .attr("x1", "0%") + .attr("x2", "100%"); +gradient.append("stop").attr("offset", "0%").attr("stop-color", t.seq[0]); +gradient.append("stop").attr("offset", "100%").attr("stop-color", t.seq[1]); + +svg + .append("rect") + .attr("x", legendX) + .attr("y", legendY) + .attr("width", legendW) + .attr("height", legendH) + .attr("fill", `url(#${gradientId})`); + +svg + .append("text") + .attr("x", legendX) + .attr("y", legendY - 10) + .attr("text-anchor", "start") + .attr("fill", t.inkSoft) + .style("font-size", "13px") + .text("Current speed"); + +svg + .append("text") + .attr("x", legendX) + .attr("y", legendY + legendH + 20) + .attr("text-anchor", "start") + .attr("fill", t.inkSoft) + .style("font-size", "13px") + .text("0 cm/s"); + +svg + .append("text") + .attr("x", legendX + legendW) + .attr("y", legendY + legendH + 20) + .attr("text-anchor", "end") + .attr("fill", t.inkSoft) + .style("font-size", "13px") + .text(`${maxMag.toFixed(1)} cm/s`); + +// --- Title ---------------------------------------------------------------- +svg + .append("text") + .attr("x", width / 2) + .attr("y", 60) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", "26px") + .style("font-weight", "600") + .text("quiver-basic · javascript · d3 · anyplot.ai"); diff --git a/plots/quiver-basic/metadata/javascript/d3.yaml b/plots/quiver-basic/metadata/javascript/d3.yaml new file mode 100644 index 0000000000..09069abe23 --- /dev/null +++ b/plots/quiver-basic/metadata/javascript/d3.yaml @@ -0,0 +1,247 @@ +library: d3 +language: javascript +specification_id: quiver-basic +created: '2026-07-24T18:27:10Z' +updated: '2026-07-24T18:44:03Z' +generated_by: claude-sonnet +workflow_run: 30116648727 +issue: 1014 +language_version: 22.23.1 +library_version: 7.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/quiver-basic/javascript/d3/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/quiver-basic/javascript/d3/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/quiver-basic/javascript/d3/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/quiver-basic/javascript/d3/plot-dark.html +quality_score: 92 +review: + strengths: + - Physically motivated rotating-eddy vortex (u = -y·decay, v = x·decay with Gaussian + decay from the core) reads clearly as counterclockwise rotation with speed peaking + mid-radius and decaying toward both the core and the far field + - 'Attempt-1 weaknesses fully addressed: near-core arrows now get a minimum shaft + length (minShaftLen) so low-magnitude vectors stay legible, a dashed radial guide + circle marks the eddy core''s decay radius for storytelling, and axis domain lines + were lightened (stroke-width 0.75, opacity 0.5) for visual refinement' + - Arrow color correctly encodes current magnitude with the Imprint sequential palette + (imprint_seq, green→blue), identical between light and dark renders; legend gradient + stops exactly match the interpolateRgbBasis scale used on the data + - Square orientation correctly forced via equal x/y pixel scale (side = min(availW, + availH)) so vector angles render undistorted + - 225-arrow 15x15 grid with no overlap or crowding at any density level, confirmed + on close inspection of the eddy core, canvas edges, and corners + - Clean, deterministic, KISS-structured code with no fake interactivity, correct + mount-node contract, and no animation + weaknesses: + - Magnitude is non-monotonic with radius (peaks around r≈1.73km, decays both toward + the core and toward the domain edge), so the outer ring reads as 'weak' the same + way the core does — a subtle nuance a casual viewer could misread without extra + context; not a required fix but a nice-to-have for a future pass + - Title occupies roughly 40-45% of the square canvas width — legible and unclipped, + but there is headroom to grow it slightly for a stronger visual anchor if revisited + image_description: |- + Light render (plot-light.png): + Background: Warm off-white, matches #FAF8F1 — not pure white. + Chrome: Bold dark-ink title "quiver-basic · javascript · d3 · anyplot.ai" centered at top. Dark axis labels "Distance east of eddy core (km)" / "Distance north of eddy core (km)" with legible tick labels -3..3 on both axes. Subtle light-gray reference grid at each integer tick. A dashed light-gray circle marks the eddy core's characteristic decay radius. All chrome text confirmed fully readable against the light background, no clipping at any edge (verified via close crops of top, bottom, left, and center regions). + Data: 15x15 grid (225 points) of arrows forming a clear counterclockwise vortex pattern around the origin. Arrows are colored from Imprint brand green (#009E73, low magnitude, near core and far field) to Imprint blue (#4467A3, higher magnitude, mid-radius) via the imprint_seq sequential scale. A gradient legend "Current speed" (0 cm/s – 1.1 cm/s) sits top-right with matching gradient stops. The exact stagnation point at the eddy core is marked with a small green dot; near-zero vectors elsewhere get a minimum shaft length so direction stays readable. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black, matches #1A1A17 — not pure black. + Chrome: Same title, axis labels, and tick labels now rendered in light ink — clearly legible, no dark-on-dark issues anywhere (title, axis labels, tick labels, and legend text all confirmed light-colored on the dark surface). Gridlines and the dashed core-radius guide remain subtly visible without competing with the data. + Data: Arrow colors, positions, lengths, and the gradient legend are pixel-identical to the light render — only chrome (background, text, grid) flips between themes, confirming correct theme-adaptive implementation. The stagnation dot at the core is still the same brand green. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 30 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 8 + max: 8 + passed: true + comment: All font sizes explicitly set (26px title, 18px axis labels, 14px + ticks, 13px legend); readable at full size in both themes; title fits without + clipping + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No overlap confirmed via close crops of core, edges, and corners + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Minimum shaft length fix resolved the prior attempt's faint near-core + arrows; 225 arrows well spaced with no crowding + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Green-to-blue sequential scale, not red-green, good contrast against + both backgrounds + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Plot area fills a well-balanced majority of the square canvas with + even margins + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Descriptive axis labels with units (km, cm/s) + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'imprint_seq correctly used for single-polarity magnitude data, first + stop is #009E73, identical data colors across themes, theme-correct backgrounds' + design_excellence: + score: 15 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: false + comment: Physically motivated color-coded vortex shows real thought, but overall + chrome/legend treatment remains fairly standard + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Axis domain lines lightened per prior feedback (stroke-width 0.75, + opacity 0.5), subtle grid, generous margins + - id: DE-03 + name: Data Storytelling + score: 5 + max: 6 + passed: true + comment: New dashed radial guide circle at the core decay radius plus magnitude + coloring gives a clear, immediate read of the vortex structure + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct quiver plot with arrows at grid points + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: x, y, u, v grid; uniform spacing; scaled arrow length; magnitude + color encoding all present + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X/Y correctly mapped, full domain shown + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title matches mandated format exactly; legend labels correct + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Shows full magnitude range including the near-zero stagnation point + and the peak mid-radius band + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Neutral oceanographic eddy/current-survey scenario + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Plausible values and proportions for a coastal current survey + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Clean top-to-bottom flow: data, layout, scales, draw' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Fully deterministic formula, no randomness + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only the d3 global used, no unused code + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: No fake UI or over-engineering + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Correct mount-node contract, no animation, single svg appended + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Correct use of d3 scales, axis generators, and data joins; per-arrow + compound marks via .each is a common but not the most vectorized D3 pattern + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Manual arrowhead trigonometry and cell-density-aware length scaling + are D3-specific low-level SVG techniques not easily replicated in higher-level + chart libraries + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - colorbar + - custom-legend + patterns: + - data-generation + dataprep: [] + styling: + - alpha-blending + - gradient-fill