diff --git a/plots/horizon-basic/implementations/javascript/d3.js b/plots/horizon-basic/implementations/javascript/d3.js new file mode 100644 index 0000000000..aa3050966a --- /dev/null +++ b/plots/horizon-basic/implementations/javascript/d3.js @@ -0,0 +1,205 @@ +// anyplot.ai +// horizon-basic: Horizon Chart +// Library: d3 7.9.0 | JavaScript 22.23.2 +// Quality: 89/100 | Created: 2026-08-18 +//# anyplot-orientation: landscape +// anyplot.ai +// horizon-basic: Horizon Chart +// Library: d3 7.9.0 | JavaScript 22 +// Quality: pending | Created: 2026-08-18 + +const t = window.ANYPLOT_TOKENS; +const { width, height } = window.ANYPLOT_SIZE; + +// --- Reproducible PRNG (LCG, no seeded RNG exists in the browser) ---------- +function makeRng(seed) { + let s = seed >>> 0; + return () => { + s = (s * 1664525 + 1013904223) >>> 0; + return s / 4294967296; + }; +} + +// --- Data: hourly temperature deviation from baseline, 8 facility sensors -- +const STATIONS = [ + "Greenhouse North", + "Greenhouse South", + "Warehouse A", + "Warehouse B", + "Rooftop Array", + "Basement Vault", + "Loading Dock", + "Server Room", +]; +const HOURS = 200; +const startDate = new Date("2026-01-01T00:00:00Z"); + +const series = STATIONS.map((name, i) => { + const rng = makeRng(1000 + i * 37); + const phase = rng() * Math.PI * 2; + const amplitude = 1.8 + rng() * 1.6; + let walk = 0; + const values = []; + for (let h = 0; h < HOURS; h++) { + walk += (rng() - 0.5) * 0.5; + walk = Math.max(-2, Math.min(2, walk)); + const diurnal = amplitude * Math.sin((2 * Math.PI * h) / 24 + phase); + values.push({ date: new Date(startDate.getTime() + h * 3600 * 1000), value: diurnal + walk }); + } + return { name, values }; +}); + +const allValues = series.flatMap((s) => s.values.map((d) => d.value)); +const maxAbs = Math.ceil(d3.max(allValues, (d) => Math.abs(d))); +const NUM_BANDS = 3; +const bandSize = maxAbs / NUM_BANDS; + +// --- Layout ------------------------------------------------------------------ +const margin = { top: 150, right: 50, bottom: 60, left: 200 }; +const iw = width - margin.left - margin.right; +const ih = height - margin.top - margin.bottom; +const gap = 4; +const stripHeight = (ih - gap * (series.length - 1)) / series.length; + +// --- Scales -------------------------------------------------------------- +const x = d3 + .scaleTime() + .domain(d3.extent(series[0].values, (d) => d.date)) + .range([0, iw]); + +// Band color ramps — blue for above-baseline, red for below (Imprint div stops). +// The midpoint stop is the theme-adaptive page background, so shading from it +// toward the full hue reproduces the classic horizon "darker = larger" effect. +const midpoint = t.div[1]; +const shade = (i) => 0.35 + 0.65 * (i / (NUM_BANDS - 1)); +const posColors = d3.range(NUM_BANDS).map((i) => d3.interpolateRgb(midpoint, t.div[2])(shade(i))); +const negColors = d3.range(NUM_BANDS).map((i) => d3.interpolateRgb(midpoint, t.div[0])(shade(i))); + +function bandScale(i) { + const lower = i * bandSize; + const upper = (i + 1) * bandSize; + return d3.scaleLinear().domain([lower, upper]).range([stripHeight, 0]).clamp(true); +} + +function bandPath(values, yScale, sign) { + return d3 + .area() + .x((d) => x(d.date)) + .y0(stripHeight) + .y1((d) => yScale(Math.max(sign * d.value, 0))) + .curve(d3.curveMonotoneX)(values); +} + +// --- SVG mount --------------------------------------------------------------- +const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height); + +// --- Title (fontsize scales down for long titles, see plot-generator.md) ----- +const title = "Facility Temperature Deviation · horizon-basic · javascript · d3 · anyplot.ai"; +const titleDefault = 26; +const titleSize = title.length > 67 ? Math.round(titleDefault * (67 / title.length)) : titleDefault; +svg + .append("text") + .attr("x", width / 2) + .attr("y", 46) + .attr("text-anchor", "middle") + .attr("fill", t.ink) + .style("font-size", `${titleSize}px`) + .style("font-weight", "600") + .text(title); + +// --- Legend: band-color scale (value -> shade) -------------------------------- +const legendSwatchW = 42; +const legendSwatchH = 20; +const legendColors = [...negColors.slice().reverse(), ...posColors]; +const legendW = legendColors.length * legendSwatchW; +const legendX = width - margin.right - legendW; +const legendY = 76; + +svg + .append("text") + .attr("x", legendX + legendW) + .attr("y", legendY - 10) + .attr("text-anchor", "end") + .attr("fill", t.inkSoft) + .style("font-size", "14px") + .text("Deviation from baseline (°C)"); + +svg + .selectAll(".legend-swatch") + .data(legendColors) + .join("rect") + .attr("class", "legend-swatch") + .attr("x", (d, i) => legendX + i * legendSwatchW) + .attr("y", legendY) + .attr("width", legendSwatchW) + .attr("height", legendSwatchH) + .attr("fill", (d) => d); + +const legendTicks = [ + { x: legendX, label: `-${maxAbs}°C` }, + { x: legendX + legendW / 2, label: "0°C" }, + { x: legendX + legendW, label: `+${maxAbs}°C` }, +]; +svg + .selectAll(".legend-tick") + .data(legendTicks) + .join("text") + .attr("class", "legend-tick") + .attr("x", (d) => d.x) + .attr("y", legendY + legendSwatchH + 16) + .attr("text-anchor", (d, i) => (i === 0 ? "start" : i === 2 ? "end" : "middle")) + .attr("fill", t.inkSoft) + .style("font-size", "13px") + .text((d) => d.label); + +// --- Horizon strips ------------------------------------------------------------ +const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`); + +g.append("rect") + .attr("width", iw) + .attr("height", ih) + .attr("fill", "none") + .attr("stroke", t.grid) + .attr("stroke-width", 1); + +series.forEach((s, si) => { + const strip = g.append("g").attr("transform", `translate(0, ${si * (stripHeight + gap)})`); + + const clipId = `horizon-clip-${si}`; + strip.append("clipPath").attr("id", clipId).append("rect").attr("width", iw).attr("height", stripHeight); + + const layers = strip.append("g").attr("clip-path", `url(#${clipId})`); + for (let i = 0; i < NUM_BANDS; i++) { + const yScale = bandScale(i); + layers.append("path").attr("d", bandPath(s.values, yScale, 1)).attr("fill", posColors[i]); + layers.append("path").attr("d", bandPath(s.values, yScale, -1)).attr("fill", negColors[i]); + } + + strip + .append("line") + .attr("x1", 0) + .attr("x2", iw) + .attr("y1", stripHeight) + .attr("y2", stripHeight) + .attr("stroke", t.grid) + .attr("stroke-width", 1); + + strip + .append("text") + .attr("x", -12) + .attr("y", stripHeight / 2) + .attr("dy", "0.35em") + .attr("text-anchor", "end") + .attr("fill", t.inkSoft) + .style("font-size", "15px") + .text(s.name); +}); + +// --- Shared time axis (bottom only) ------------------------------------------- +const xAxis = g + .append("g") + .attr("transform", `translate(0, ${ih})`) + .call(d3.axisBottom(x).ticks(8).tickFormat(d3.timeFormat("%b %d"))); +xAxis.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px"); +xAxis.selectAll("line").attr("stroke", t.grid); +xAxis.select(".domain").attr("stroke", t.inkSoft); diff --git a/plots/horizon-basic/metadata/javascript/d3.yaml b/plots/horizon-basic/metadata/javascript/d3.yaml new file mode 100644 index 0000000000..513b613e30 --- /dev/null +++ b/plots/horizon-basic/metadata/javascript/d3.yaml @@ -0,0 +1,251 @@ +library: d3 +language: javascript +specification_id: horizon-basic +created: '2026-08-18T01:08:55Z' +updated: '2026-08-18T01:13:51Z' +generated_by: claude-sonnet +workflow_run: 32086655703 +issue: 1877 +language_version: 22.23.2 +library_version: 7.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/horizon-basic/javascript/d3/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/horizon-basic/javascript/d3/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/horizon-basic/javascript/d3/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/horizon-basic/javascript/d3/plot-dark.html +quality_score: 89 +review: + strengths: + - Correctly implements the horizon-chart fold algorithm (overlaid bands via clipPath, + one per band per sign) matching the classic horizon-chart definition rather + than a fake/simulated version. + - Color intensity increases with magnitude across 3 mirrored bands, using the spec's + own suggested blue-positive/red-negative convention with theme-adaptive Imprint + diverging stops (div[0]/midpoint/div[2]) — data colors identical between light + and dark renders. + - Deterministic, reproducible data via a seeded LCG, with a plausible, neutral facility-monitoring + context (8 named sensors, diurnal cycle + bounded random walk). + - Theme-correct chrome in both renders (backgrounds, ink/inkSoft text, grid strokes) + with no legibility failures. + - 'Clean, idiomatic D3: scaleTime, d3.area + curveMonotoneX, clipPath, axisBottom, + no extraneous imports, no fake interactivity, animations correctly omitted for + the static harness capture.' + weaknesses: + - The bounding rect drawn around the whole strip area plus a baseline rule under + every strip add a bit of extra chrome/frame that slightly undercuts visual refinement + — consider dropping the outer rect or lightening it further. + - With 8 near-parallel strips and no single-series emphasis, there's no visual hook + drawing the eye to a specific noteworthy pattern or outlier sensor — a subtle + highlight (e.g., bolder label or annotation on the most volatile station) would + strengthen data storytelling. + - Legend swatch/tick text (13px) and station row labels (15px) are the smallest + text elements on the canvas — legible at full size but tight once scaled down + to a ~400px thumbnail. + - Small helper functions (bandScale, bandPath, makeRng) add minor structural complexity + versus a fully flat script; justified here by the per-band/per-series repetition + but worth noting for KISS scoring. + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1-consistent), matches the required light-theme surface. + Chrome: Title "Facility Temperature Deviation · horizon-basic · javascript · d3 · anyplot.ai" is centered at top in bold dark ink, clearly legible. A right-aligned diverging legend ("Deviation from baseline (°C)", -6°C / 0°C / +6°C) sits above the chart. 8 horizontal strips are labeled on the left with station names (Greenhouse North/South, Warehouse A/B, Rooftop Array, Basement Vault, Loading Dock, Server Room) in soft gray-brown ink. A bottom time axis shows "Jan 01"–"Jan 09" tick labels. + Data: Each strip shows a folded horizon pattern — light-to-dark blue mounds for above-baseline (warmer) periods and light-to-dark red mounds for below-baseline (colder) periods, following a clear diurnal rhythm. Peaks vary in saturation with magnitude as intended. + Legibility verdict: PASS — all title, legend, row-label, and tick text is clearly readable against the light background; no light-on-light issues. + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17-consistent), matches the required dark-theme surface. + Chrome: Same title, legend, row labels, and axis ticks now rendered in light ink/inkSoft tones, fully legible against the dark background. Strip separator lines and the bounding rect remain visible but subtle. + Data: Same red/blue diverging bands as the light render, at identical hues and identical magnitude-to-saturation mapping — confirms only chrome (not data color) flips between themes. + Legibility verdict: PASS — no dark-on-dark failures observed; every text element (title, legend units, row labels, date ticks) reads clearly against the near-black surface. + criteria_checklist: + visual_quality: + score: 29 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All text explicitly sized and readable in both themes; legend/row-label + text (13-15px) is the tightest at thumbnail scale + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No collisions between title, legend, row labels, or clipped horizon + bands + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Band peaks and troughs clearly visible at this data density (200 + points/series) + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Red-blue diverging scale is CVD-friendlier than red-green; adequate + contrast on both surfaces + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Canvas gate passed; balanced proportions, nothing cut off + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Descriptive title, units shown in legend + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: Uses imprint_div (theme-adaptive midpoint) correctly for the diverging + magnitude encoding; backgrounds match theme tokens + design_excellence: + score: 12 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Genuine horizon-fold technique with magnitude-graded shading; raised + from default on evidence + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: true + comment: Clean overall but the outer bounding rect and per-strip baseline + rule add extra chrome + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: true + comment: No single-series emphasis or focal point across the 8 parallel strips + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct folded horizon-chart construction + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: 3 mirrored bands, meaningful zero baseline, magnitude-graded intensity + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X = time, value folded into color per horizon-chart convention + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format matches spec exactly; legend explains value-to-color + mapping + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: All spec-noted horizon-chart aspects present + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Plausible, neutral facility-monitoring scenario + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Deviation magnitudes (~±5-6°C) are believable for facility temperature + sensors + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: true + comment: A few small helper functions (bandScale, bandPath, makeRng) beyond + a flat script, justified by per-band/per-series repetition + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Seeded LCG, fully deterministic + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only the global d3 is used + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: No fake UI/interactivity, appropriate complexity for the technique + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: 'Correctly builds one into #container per the mount-node contract; + no animations' + library_mastery: + score: 9 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Idiomatic d3-scale/d3-shape/d3-axis usage throughout + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Hand-built horizon fold via clipPath + layered d3.area is exactly + the bespoke-chart use case D3 is meant for + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - custom-legend + - layer-composition + patterns: + - data-generation + - iteration-over-groups + dataprep: + - time-series + styling: + - custom-colormap