diff --git a/plots/ternary-basic/implementations/javascript/d3.js b/plots/ternary-basic/implementations/javascript/d3.js new file mode 100644 index 0000000000..ecca7659bd --- /dev/null +++ b/plots/ternary-basic/implementations/javascript/d3.js @@ -0,0 +1,194 @@ +// anyplot.ai +// ternary-basic: Basic Ternary Plot +// Library: d3 7.9.0 | JavaScript 22.23.1 +// Quality: 92/100 | Updated: 2026-08-05 + +//# anyplot-orientation: square +const t = window.ANYPLOT_TOKENS; +const { width, height } = window.ANYPLOT_SIZE; +const margin = { top: 210, right: 110, bottom: 110, left: 110 }; +const iw = width - margin.left - margin.right; +const ih = height - margin.top - margin.bottom; + +// --- Data (in-memory, deterministic) ---------------------------------------- +// Soil samples: clay / sand / silt proportions (%), each triplet sums to 100. +function lcg(seed) { + let s = seed; + return () => { + s = (s * 1103515245 + 12345) & 0x7fffffff; + return s / 0x7fffffff; + }; +} +const rand = lcg(42123); +const samples = []; +for (let i = 0; i < 28; i++) { + const wClay = 0.02 + rand() * 0.98; + const wSand = 0.02 + rand() * 0.98; + const wSilt = 0.02 + rand() * 0.98; + const total = wClay + wSand + wSilt; + samples.push({ + clay: (wClay / total) * 100, + sand: (wSand / total) * 100, + silt: (wSilt / total) * 100, + }); +} + +// --- Triangle geometry -------------------------------------------------------- +// Apex = clay, bottom-left = sand, bottom-right = silt (classic soil-texture layout). +const side = iw; +const triHeight = (side * Math.sqrt(3)) / 2; +const apex = { x: margin.left + side / 2, y: margin.top }; +const left = { x: margin.left, y: margin.top + triHeight }; +const right = { x: margin.left + side, y: margin.top + triHeight }; +const centroid = { + x: (apex.x + left.x + right.x) / 3, + y: (apex.y + left.y + right.y) / 3, +}; + +// Barycentric (a=clay, b=sand, c=silt, each 0-1, a+b+c=1) -> pixel coordinates. +function toXY(a, b, c) { + return { + x: a * apex.x + b * left.x + c * right.x, + y: a * apex.y + b * left.y + c * right.y, + }; +} +function outward(p, dist) { + const dx = p.x - centroid.x; + const dy = p.y - centroid.y; + const len = Math.hypot(dx, dy) || 1; + return { x: p.x + (dx / len) * dist, y: p.y + (dy / len) * dist }; +} + +// --- SVG mount ---------------------------------------------------------------- +const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height); + +// --- Grid lines (20% intervals, three families parallel to each edge) -------- +const levels = [0.2, 0.4, 0.6, 0.8]; +const gridSegments = []; +for (const lv of levels) { + gridSegments.push([toXY(lv, 1 - lv, 0), toXY(lv, 0, 1 - lv)]); // parallel to base + gridSegments.push([toXY(1 - lv, lv, 0), toXY(0, lv, 1 - lv)]); // parallel to apex-right + gridSegments.push([toXY(1 - lv, 0, lv), toXY(0, 1 - lv, lv)]); // parallel to apex-left +} +svg + .selectAll(".grid-line") + .data(gridSegments) + .join("line") + .attr("class", "grid-line") + .attr("x1", (d) => d[0].x) + .attr("y1", (d) => d[0].y) + .attr("x2", (d) => d[1].x) + .attr("y2", (d) => d[1].y) + .attr("stroke", t.grid) + .attr("stroke-width", 1.5); + +// --- Triangle border ------------------------------------------------------------ +svg + .append("path") + .attr("d", `M${apex.x},${apex.y} L${left.x},${left.y} L${right.x},${right.y} Z`) + .attr("fill", "none") + .attr("stroke", t.inkSoft) + .attr("stroke-width", 2.5); + +// --- Tick marks + labels (one axis per edge, 0-100 by 20%) ------------------- +const tickLevels = [0, 0.2, 0.4, 0.6, 0.8, 1]; +const ticks = []; +for (const lv of tickLevels) { + ticks.push(toXY(lv, 1 - lv, 0)); // clay axis, edge apex-left + ticks.push(toXY(1 - lv, 0, lv)); // silt axis, edge apex-right + ticks.push(toXY(0, lv, 1 - lv)); // sand axis, edge base +} +for (const p of ticks) { + const tickEnd = outward(p, 14); + const labelPos = outward(p, 34); + svg + .append("line") + .attr("x1", p.x) + .attr("y1", p.y) + .attr("x2", tickEnd.x) + .attr("y2", tickEnd.y) + .attr("stroke", t.inkSoft) + .attr("stroke-width", 1.5); +} +// Skip lv=0: each axis's "0" tick lands exactly on the vertex where a +// different axis's "100" tick already sits (e.g. the silt axis's 0 coincides +// with the clay axis's 100 at the apex) - drawing both stacks two labels on +// one pixel. The "100" from the owning axis is kept and is unambiguous. +const tickLabels = []; +for (const lv of tickLevels) { + if (lv === 0) continue; + tickLabels.push({ p: outward(toXY(lv, 1 - lv, 0), 34), text: Math.round(lv * 100) }); + tickLabels.push({ p: outward(toXY(1 - lv, 0, lv), 34), text: Math.round(lv * 100) }); + tickLabels.push({ p: outward(toXY(0, lv, 1 - lv), 34), text: Math.round(lv * 100) }); +} +svg + .selectAll(".tick-label") + .data(tickLabels) + .join("text") + .attr("class", "tick-label") + .attr("x", (d) => d.p.x) + .attr("y", (d) => d.p.y) + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .attr("fill", t.inkSoft) + .style("font-size", "14px") + .style("font-family", "sans-serif") + .text((d) => d.text); + +// --- Vertex labels -------------------------------------------------------------- +// The apex label stays centered (ample horizontal room either side), but the +// left/right vertex labels grow inward from their vertex ("start"/"end" +// anchors) instead of centering past the canvas edge - "Sand (%)" is wide +// enough that a centered anchor pushed its leading "S" off the left border. +const vertexLabels = [ + { p: outward(apex, 78), text: "Clay (%)", anchor: "middle" }, + { p: outward(left, 78), text: "Sand (%)", anchor: "start" }, + { p: outward(right, 78), text: "Silt (%)", anchor: "end" }, +]; +svg + .selectAll(".vertex-label") + .data(vertexLabels) + .join("text") + .attr("class", "vertex-label") + .attr("x", (d) => d.p.x) + .attr("y", (d) => d.p.y) + .attr("text-anchor", (d) => d.anchor) + .attr("dominant-baseline", "middle") + .attr("fill", t.ink) + .style("font-size", "20px") + .style("font-weight", "600") + .style("font-family", "sans-serif") + .text((d) => d.text); + +// --- Data points -------------------------------------------------------------- +// Marker radius scales with compositional purity (the dominant component's +// share) so the rarer near-vertex, high-purity samples read as a focal point +// against the more common balanced mixtures clustered mid-triangle. +const points = samples.map((s) => { + const purity = Math.max(s.clay, s.sand, s.silt) / 100; + return { ...toXY(s.clay / 100, s.sand / 100, s.silt / 100), r: 7 + purity * 7 }; +}); +svg + .selectAll(".sample") + .data(points) + .join("circle") + .attr("class", "sample") + .attr("cx", (d) => d.x) + .attr("cy", (d) => d.y) + .attr("r", (d) => d.r) + .attr("fill", t.palette[0]) + .attr("fill-opacity", 0.85) + .attr("stroke", t.pageBg) + .attr("stroke-width", 1.5); + +// --- Title ------------------------------------------------------------------ +svg + .append("text") + .attr("x", width / 2) + .attr("y", 56) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", "22px") + .style("font-weight", "600") + .style("font-family", "sans-serif") + .text("ternary-basic · javascript · d3 · anyplot.ai"); diff --git a/plots/ternary-basic/metadata/javascript/d3.yaml b/plots/ternary-basic/metadata/javascript/d3.yaml new file mode 100644 index 0000000000..91f3164ea6 --- /dev/null +++ b/plots/ternary-basic/metadata/javascript/d3.yaml @@ -0,0 +1,240 @@ +library: d3 +language: javascript +specification_id: ternary-basic +created: '2026-08-04T15:21:12Z' +updated: '2026-08-05T21:47:22Z' +generated_by: claude-sonnet +workflow_run: 30922911394 +issue: 1001 +language_version: 22.23.1 +library_version: 7.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/javascript/d3/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/javascript/d3/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/javascript/d3/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/javascript/d3/plot-dark.html +quality_score: 92 +review: + strengths: + - Correct barycentric-to-pixel transform with three grid-line families and edge + tick marks at 20% intervals, matching the spec's 'regular interval gridlines' + and 'tick marks along each edge' requirements + - Marker radius encodes compositional purity (dominant component share), giving + the scatter a meaningful visual hierarchy without added chartjunk + - Thoughtful start/end text-anchor logic on the left/right vertex labels prevents + them from clipping at the canvas edges + - 'Fully theme-adaptive chrome — brand green #009E73 is identical and clearly legible + on both the light (#FAF8F1) and dark (#1A1A17) surfaces' + - Deterministic LCG-seeded data generation and idiomatic D3 `.data().join()` usage + throughout (grid lines, tick labels, vertex labels, sample points) + weaknesses: + - Single accent color and uniform marker shape keep the design at 'strong' rather + than showcase-level — a secondary cue (e.g. subtle alpha/size gradient tied to + purity, or a couple of direct sample-group labels) would push the data storytelling + further + - No sample lands above ~65% clay, so coverage leans toward sand/silt-dominant mixtures + — a sample or two closer to the clay apex would balance feature coverage across + all three vertices + image_description: |- + Light render (plot-light.png): + Background: Warm off-white, consistent with #FAF8F1 — not pure white. + Chrome: Title "ternary-basic · javascript · d3 · anyplot.ai" centered at top in dark bold text; vertex labels "Clay (%)" (top), "Sand (%)" (bottom-left), "Silt (%)" (bottom-right) in bold dark ink; tick labels (0/20/40/60/80/100) along each edge in a softer dark gray; triangular border and gridlines in subtle gray. All chrome text is clearly readable against the light background. + Data: 28 circular markers in brand green (#009E73), radius scaled by compositional purity, with a subtle pale stroke for edge definition against overlapping points. Points span a broad range of clay/sand/silt mixtures, concentrated toward sand- and silt-dominant compositions with a few near-vertex and mid-triangle samples. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black, consistent with #1A1A17 — not pure black. + Chrome: Same title and vertex labels now rendered in light/off-white text, tick labels in a lighter soft gray, triangular border and gridlines in a light, low-opacity gray — all fully legible against the dark surface, with no dark-on-dark text anywhere. + Data: Markers are identical #009E73 green to the light render — only the chrome (background, text, grid) flipped, confirming palette-token compliance. + 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 (14px ticks, 20px vertex labels, 22px + title); readable in both themes, no overflow + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No text overlap; lv=0 duplicate-label case deliberately skipped + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: 28 sparse points with prominent 7-14px radius markers, purity-scaled + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Single-series green with pale stroke for definition; no CVD risk + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Triangle occupies ~60-70% of canvas, balanced top/bottom margins, + nothing cut off + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: 'Vertex labels descriptive with units: Clay/Sand/Silt (%)' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series #009E73, identical across themes, backgrounds and chrome + theme-correct' + design_excellence: + score: 14 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Clean, intentional, but single accent color keeps it below showcase-level + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Subtle gridlines, generous whitespace, custom triangular frame in + place of spines + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Purity-scaled marker size creates a real focal point, though no explicit + narrative beyond that + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct ternary/barycentric plot + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Gridlines at regular intervals, labeled vertices, edge tick marks, + distinct markers all present + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Barycentric transform correctly maps clay/sand/silt to all three + axes + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format exact; single-series legend correctly omitted + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Samples span near-vertex to balanced mid-triangle mixtures across + sand/silt/clay + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Soil-texture clay/sand/silt scenario — neutral, comprehensible, real-world + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Each triplet normalized to sum to 100%, plausible soil-science proportions + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Flat top-to-bottom script; helper functions are minimal geometry + utilities, no classes + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Fixed-seed LCG (seed=42123) + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only the d3 global, no unused imports + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Appropriate complexity for ternary geometry, no fake functionality + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Single svg sized from ANYPLOT_SIZE, correct mount-node contract, + no animation + library_mastery: + score: 8 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Consistent .data().join() pattern throughout; correct token/mount + usage + - id: LM-02 + name: Distinctive Features + score: 3 + max: 3 + passed: true + comment: Hand-rolled barycentric transform, triangular grid families, and + radial outward-label placement — genuinely D3-specific low-level work + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - manual-ticks + patterns: + - data-generation + dataprep: [] + styling: + - alpha-blending + - edge-highlighting