diff --git a/plots/span-basic/implementations/javascript/d3.js b/plots/span-basic/implementations/javascript/d3.js new file mode 100644 index 0000000000..9481a31673 --- /dev/null +++ b/plots/span-basic/implementations/javascript/d3.js @@ -0,0 +1,184 @@ +// anyplot.ai +// span-basic: Basic Span Plot (Highlighted Region) +// Library: d3 7.9.0 | JavaScript 22.23.1 +// Quality: 91/100 | Created: 2026-07-25 + +const t = window.ANYPLOT_TOKENS; +const { width, height } = window.ANYPLOT_SIZE; +const margin = { top: 110, right: 60, bottom: 90, left: 110 }; +const iw = width - margin.left - margin.right; +const ih = height - margin.top - margin.bottom; + +// --- Data (in-memory, deterministic) ---------------------------------------- +// Quarterly stock index level, 2006–2011, with two recession periods marked. +function lcg(seed) { + let s = seed; + return () => { + s = (s * 1103515245 + 12345) & 0x7fffffff; + return s / 0x7fffffff; + }; +} +const rand = lcg(42); + +const quarters = []; +for (let year = 2006; year <= 2011; year++) { + for (let q = 0; q < 4; q++) quarters.push(new Date(year, q * 3, 1)); +} + +let level = 100; +const series = quarters.map((date, i) => { + const inRecession = + (date >= new Date(2007, 9, 1) && date < new Date(2009, 3, 1)) || + (date >= new Date(2011, 6, 1) && date < new Date(2011, 12, 1)); + const drift = inRecession ? -3.2 : 1.8; + level += drift + (rand() - 0.5) * 4; + level = Math.max(level, 55); + return { date, index: level }; +}); + +const verticalSpans = [ + { start: new Date(2007, 9, 1), end: new Date(2009, 3, 1), label: "2007–09 recession" }, + { start: new Date(2011, 6, 1), end: new Date(2011, 12, 1), label: "2011 slowdown" }, +]; + +// A horizontal span demonstrates the spec's other direction: a fixed value-threshold +// "correction zone" band, independent of x, spanning the full plot width. +const horizontalSpans = [{ start: 60, end: 97, label: "Correction zone (index < 97)" }]; + +// --- SVG mount --------------------------------------------------------------- +const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height); +const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`); + +// --- Scales -------------------------------------------------------------------- +// Domain covers both the series dates and every vertical span's start/end so a span +// that extends past the last data point never extrapolates past the plotted x-axis. +const x = d3 + .scaleTime() + .domain(d3.extent([...series.map((d) => d.date), ...verticalSpans.flatMap((s) => [s.start, s.end])])) + .range([0, iw]); +const y = d3 + .scaleLinear() + .domain([d3.min(series, (d) => d.index) * 0.95, d3.max(series, (d) => d.index) * 1.05]) + .nice() + .range([ih, 0]); + +// --- Gridlines (y-axis only) --------------------------------------------------- +g.append("g") + .selectAll("line") + .data(y.ticks(6)) + .join("line") + .attr("x1", 0) + .attr("x2", iw) + .attr("y1", (d) => y(d)) + .attr("y2", (d) => y(d)) + .attr("stroke", t.grid) + .attr("stroke-width", 1); + +// --- Horizontal span (drawn beneath everything) — value-threshold band ---------- +const hSpanColor = t.amber; // amber — semantic anchor for warning/caution thresholds +g.selectAll(".hspan") + .data(horizontalSpans) + .join("rect") + .attr("class", "hspan") + .attr("x", 0) + .attr("y", (d) => Math.max(y(d.end), 0)) + .attr("width", iw) + .attr("height", (d) => Math.min(y(d.start), ih) - Math.max(y(d.end), 0)) + .attr("fill", hSpanColor) + .attr("opacity", 0.22); + +g.selectAll(".hspan-label") + .data(horizontalSpans) + .join("text") + .attr("class", "hspan-label") + .attr("x", 12) + .attr("y", (d) => Math.max(y(d.end), 0) + 20) + .attr("text-anchor", "start") + .attr("fill", t.inkSoft) + .style("font-size", "13px") + .style("font-weight", "500") + .text((d) => d.label); + +// --- Vertical spans (drawn beneath the line) ------------------------------------ +const spanColor = t.palette[4]; // matte red — semantic anchor for downturn periods +g.selectAll(".span") + .data(verticalSpans) + .join("rect") + .attr("class", "span") + .attr("x", (d) => x(d.start)) + .attr("y", 0) + .attr("width", (d) => x(d.end) - x(d.start)) + .attr("height", ih) + .attr("fill", spanColor) + .attr("opacity", 0.22); + +g.selectAll(".span-label") + .data(verticalSpans) + .join("text") + .attr("class", "span-label") + .attr("x", (d) => x(d.start) + (x(d.end) - x(d.start)) / 2) + .attr("y", 22) + .attr("text-anchor", "middle") + .attr("fill", t.inkSoft) + .style("font-size", "13px") + .style("font-weight", "500") + .text((d) => d.label); + +// --- Line (drawn above the spans) ----------------------------------------------- +const line = d3 + .line() + .x((d) => x(d.date)) + .y((d) => y(d.index)); + +g.append("path") + .datum(series) + .attr("fill", "none") + .attr("stroke", t.palette[0]) + .attr("stroke-width", 3) + .attr("stroke-linejoin", "round") + .attr("stroke-linecap", "round") + .attr("d", line); + +// --- Axes ------------------------------------------------------------------- +const xAxis = g + .append("g") + .attr("transform", `translate(0,${ih})`) + .call(d3.axisBottom(x).ticks(d3.timeYear.every(1)).tickFormat(d3.timeFormat("%Y")).tickSizeOuter(0)); +const yAxis = g.append("g").call(d3.axisLeft(y).ticks(6).tickSizeOuter(0)); + +for (const ax of [xAxis, yAxis]) { + ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "15px"); + ax.selectAll("line").attr("stroke", t.inkSoft); + ax.select(".domain").attr("stroke", t.inkSoft); +} +xAxis.selectAll("line").remove(); +yAxis.selectAll("line").remove(); + +// --- Axis titles -------------------------------------------------------------- +g.append("text") + .attr("x", iw / 2) + .attr("y", ih + 56) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", "17px") + .text("Year"); + +g.append("text") + .attr("transform", "rotate(-90)") + .attr("x", -ih / 2) + .attr("y", -80) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", "17px") + .text("Market Index"); + +// --- Title ------------------------------------------------------------------ +svg + .append("text") + .attr("x", width / 2) + .attr("y", 50) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", "26px") + .style("font-weight", "600") + .text("span-basic · javascript · d3 · anyplot.ai"); diff --git a/plots/span-basic/metadata/javascript/d3.yaml b/plots/span-basic/metadata/javascript/d3.yaml new file mode 100644 index 0000000000..dd23f10852 --- /dev/null +++ b/plots/span-basic/metadata/javascript/d3.yaml @@ -0,0 +1,256 @@ +library: d3 +language: javascript +specification_id: span-basic +created: '2026-07-25T14:36:23Z' +updated: '2026-07-25T14:54:48Z' +generated_by: claude-sonnet +workflow_run: 30161831345 +issue: 980 +language_version: 22.23.1 +library_version: 7.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/span-basic/javascript/d3/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/span-basic/javascript/d3/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/span-basic/javascript/d3/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/span-basic/javascript/d3/plot-dark.html +quality_score: 91 +review: + strengths: + - 'Both attempt-1 weaknesses are fully fixed: a horizontal "Correction zone (index + < 97)" span now demonstrates the spec''s second direction, and the x-domain now + uses d3.extent() across both the series dates and every span''s start/end so the + ''2011 slowdown'' span no longer bleeds through the right margin — confirmed by + pixel probe: the span fill and axis domain line both stop exactly at x=3080px + (= 3200 canvas width − 120px right margin), with clean off-white margin beyond + that point.' + - 'Correct Imprint palette usage: brand green (#009E73) for the line, matte-red + semantic anchor for the recession/slowdown spans, and amber semantic anchor for + the value-threshold band — identical data colors across both themes, only chrome + flips.' + - Direct in-span text labels avoid the need for a legend on this single-series chart, + a clean minimal design choice, and the technique of unioning series + span dates + into one scale domain is a thoughtful, non-obvious D3 pattern. + - Deterministic seeded (LCG) data generation, explicit font sizing on every text + element, and theme-correct chrome in both renders with no dark-on-dark or light-on-light + legibility failures. + weaknesses: + - Title occupies only ~30% of canvas width (measured via pixel scan) — well under + the 50-70% comfortable range; a slightly larger title fontsize would strengthen + the typographic hierarchy without risking overflow. + - The 24-point quarterly line series has no per-point markers; per the data-density + heuristic, sparse series (< 50 points) should get prominent markers in addition + to the line to make individual quarters distinguishable. + - Where the vertical recession span and the horizontal correction-zone span overlap + (e.g. 2008-2009 inside the < 97 band), the two semi-transparent fills blend into + an unlabeled third color — consider a slightly different compositing approach + or a note so the blended region doesn't read as an unexplained third category. + - Design polish is still fairly minimal beyond the span highlighting itself (no + additional refinement like an end-point marker on the line, subtle drop-shadow/separation + for the span labels, etc.) — DE-01 stays mid-range. + image_description: |- + Light render (plot-light.png): + Background: warm off-white, matches #FAF8F1 target (verified pixel value 250,248,241). + Chrome: bold centered title "span-basic · javascript · d3 · anyplot.ai" in dark ink; axis titles "Year" / "Market Index" in dark ink; tick labels in softer ink; y-axis-only subtle gridlines. All clearly readable, no overflow or clipping at any edge (pixel-probed right edge: span fill and axis line both terminate at x=3080px, leaving a clean 120px off-white margin to the canvas edge). + Data: brand-green (#009E73) line tracing a quarterly market index 2006-2011 (peak ~117, trough ~96); two matte-red (#AE3030) semi-transparent vertical bands ("2007-09 recession", "2011 slowdown") with centered in-band labels; one amber semi-transparent horizontal band ("Correction zone (index < 97)") spanning the full plot width near the bottom. Alpha ~0.22 throughout, underlying line stays visible through all spans. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: warm near-black, matches #1A1A17 target. + Chrome: title and axis titles switch to light ink, tick labels to softer light ink, gridlines to a low-opacity light color. No dark-on-dark failures observed anywhere — all text (including the "Correction zone" label sitting on the olive-toned amber band) remains clearly legible. + Data: identical brand-green line and identical span fill hues (rendered as darker translucent maroon/olive against the dark surface, as expected — only chrome flips, not data color). + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 29 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All font sizes explicitly set, fully readable in both themes; title + could be slightly larger for stronger hierarchy but nothing is illegible + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No text/element collisions in either render + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: 3px line weight and 0.22 span alpha keep both the line and the underlying + data visible through the highlighted regions + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Green line vs red/amber spans differ by hue and role, not hue alone; + spans are background context not competing data series + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Attempt-1 margin-bleed bug fixed — pixel probe confirms span fill + and axis line both stop exactly at x=3080px, clean 120px right margin restored; + correct 3200×1800 canvas, nothing cut off + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Descriptive Year / Market Index labels + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series #009E73; spans use semantic-anchor red and amber; theme-correct + chrome in both renders' + design_excellence: + score: 15 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: false + comment: Intentional semantic color choices lift it above generic default, + but overall polish beyond the span highlighting is still modest + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: L-shaped spine, y-only gridlines, generous margins; no longer held + back by the fixed margin-bleed issue + - id: DE-03 + name: Data Storytelling + score: 5 + max: 6 + passed: true + comment: Both recession bands and the threshold band create a clear, labeled + narrative guiding the viewer through the market's downturns + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct span-over-line-chart pattern + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Both vertical and horizontal span directions now demonstrated, semi-transparent + fill in spec's 0.2-0.3 range, optional labels included + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: x-domain now unions series and span dates, so no span end extrapolates + past the axis + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format matches exactly; legend appropriately omitted in favor + of direct labels + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Both direction/use-cases from the spec (recession periods, threshold + zone) are now covered + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Plausible, neutral finance scenario matching real 2007-09 / 2011 + downturns + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Plausible, internally consistent index values + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: No classes; single tiny LCG helper + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Seeded LCG + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only the d3 global, per library contract + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean, no fake functionality + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Correct mount-node contract, no animations + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Idiomatic scales/axes and .data().join() patterns throughout + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: false + comment: Union-domain-across-spans technique and in-place span labeling are + distinctive; nothing beyond that + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - layer-composition + patterns: + - data-generation + dataprep: + - time-series + styling: + - alpha-blending