diff --git a/plots/area-elevation-profile/implementations/javascript/chartjs.js b/plots/area-elevation-profile/implementations/javascript/chartjs.js new file mode 100644 index 0000000000..d62ddaf196 --- /dev/null +++ b/plots/area-elevation-profile/implementations/javascript/chartjs.js @@ -0,0 +1,235 @@ +// anyplot.ai +// area-elevation-profile: Terrain Elevation Profile Along Transect +// Library: chartjs 4.4.7 | JavaScript 22.22.3 +// Quality: 90/100 | Created: 2026-06-10 + +//# anyplot-orientation: landscape +const t = window.ANYPLOT_TOKENS; + +// Alpine trail — control points (distance km, elevation m) +const ctrlPoints = [ + { d: 0, e: 940 }, + { d: 6, e: 1120 }, + { d: 14, e: 1620 }, + { d: 20, e: 2100 }, + { d: 27, e: 1820 }, + { d: 33, e: 2450 }, + { d: 40, e: 2200 }, + { d: 47, e: 1700 }, + { d: 56, e: 2300 }, + { d: 63, e: 1880 }, + { d: 70, e: 1280 }, + { d: 76, e: 1050 }, + { d: 80, e: 880 }, +]; + +// Landmarks to annotate (trailhead, passes, summit, hut, end) +const landmarks = [ + { d: 0, e: 940, name: "Trailhead", elev: "940 m" }, + { d: 20, e: 2100, name: "First Pass", elev: "2100 m" }, + { d: 33, e: 2450, name: "Summit Peak", elev: "2450 m" }, + { d: 47, e: 1700, name: "Mountain Hut", elev: "1700 m" }, + { d: 56, e: 2300, name: "Second Pass", elev: "2300 m" }, + { d: 80, e: 880, name: "Trail End", elev: "880 m" }, +]; + +// Cubic Hermite spline interpolation for smooth elevation profile +function hermite(x, pts) { + const n = pts.length; + if (x <= pts[0].d) return pts[0].e; + if (x >= pts[n - 1].d) return pts[n - 1].e; + let i = 1; + while (i < n - 1 && pts[i].d < x) i++; + i--; + const p0 = pts[Math.max(0, i - 1)]; + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = pts[Math.min(n - 1, i + 2)]; + const tl = (x - p1.d) / (p2.d - p1.d); + const dt = p2.d - p1.d; + const m1 = (p2.e - p0.e) / (p2.d - p0.d); + const m2 = (p3.e - p1.e) / (p3.d - p1.d); + const h00 = 2*tl*tl*tl - 3*tl*tl + 1; + const h10 = tl*tl*tl - 2*tl*tl + tl; + const h01 = -2*tl*tl*tl + 3*tl*tl; + const h11 = tl*tl*tl - tl*tl; + return h00*p1.e + h10*dt*m1 + h01*p2.e + h11*dt*m2; +} + +// Sample 200 points along the trail +const N = 200; +const distances = []; +const elevations = []; +for (let i = 0; i < N; i++) { + const d = (i / (N - 1)) * 80; + distances.push(parseFloat(d.toFixed(3))); + elevations.push(Math.round(hermite(d, ctrlPoints))); +} + +// Mount canvas +const canvas = document.createElement("canvas"); +document.getElementById("container").appendChild(canvas); + +// Custom plugin: draws vertical dashed guide lines + label boxes at each landmark +const landmarkPlugin = { + id: "landmarks", + afterDraw(chart) { + const { ctx: c, chartArea, scales } = chart; + const xSc = scales.x; + const ySc = scales.y; + c.save(); + + landmarks.forEach(lm => { + const xPx = xSc.getPixelForValue(lm.d); + const yPx = ySc.getPixelForValue(lm.e); + const lineH = 17; + const pad = 5; + const gap = 9; + + // Dashed vertical guide from terrain dot down to x-axis baseline + c.beginPath(); + c.setLineDash([5, 4]); + c.strokeStyle = t.inkSoft + "90"; + c.lineWidth = 1.5; + c.moveTo(xPx, yPx + 6); + c.lineTo(xPx, chartArea.bottom); + c.stroke(); + c.setLineDash([]); + + // Hollow dot at terrain elevation + c.beginPath(); + c.arc(xPx, yPx, 5, 0, 2 * Math.PI); + c.fillStyle = t.pageBg; + c.fill(); + c.beginPath(); + c.arc(xPx, yPx, 5, 0, 2 * Math.PI); + c.strokeStyle = t.ink; + c.lineWidth = 2; + c.stroke(); + + // Measure label box dimensions + c.font = "bold 15px sans-serif"; + const nameW = c.measureText(lm.name).width; + c.font = "14px sans-serif"; + const elevW = c.measureText(lm.elev).width; + const boxW = Math.max(nameW, elevW) + pad * 2; + const boxH = lineH * 2 + pad; + + // Place label above dot; flip below if too close to chart top + const flipBelow = (yPx - gap - boxH) < chartArea.top + 5; + const boxTop = flipBelow ? yPx + gap : yPx - gap - boxH; + + // Clamp box horizontally to stay within chart area (handles edge landmarks) + let boxLeft = xPx - boxW / 2; + boxLeft = Math.max(chartArea.left, Math.min(chartArea.right - boxW, boxLeft)); + const boxCenterX = boxLeft + boxW / 2; + + // Background box for readability + c.fillStyle = t.elevatedBg; + c.beginPath(); + c.roundRect(boxLeft, boxTop, boxW, boxH, 3); + c.fill(); + + // Landmark name (bold) + c.font = "bold 15px sans-serif"; + c.fillStyle = t.ink; + c.textAlign = "center"; + c.textBaseline = "top"; + c.fillText(lm.name, boxCenterX, boxTop + pad); + + // Elevation value (regular, secondary) + c.font = "14px sans-serif"; + c.fillStyle = t.inkSoft; + c.fillText(lm.elev, boxCenterX, boxTop + pad + lineH); + }); + + c.restore(); + }, +}; + +new Chart(canvas, { + type: "line", + data: { + labels: distances, + datasets: [{ + label: "Elevation", + data: elevations, + borderColor: t.palette[0], + borderWidth: 2.5, + fill: "start", + backgroundColor(ctx) { + const chart = ctx.chart; + const { ctx: c, chartArea } = chart; + if (!chartArea) return t.palette[0] + "60"; + const grad = c.createLinearGradient(0, chartArea.top, 0, chartArea.bottom); + grad.addColorStop(0, t.palette[0] + "BB"); + grad.addColorStop(1, t.palette[0] + "18"); + return grad; + }, + pointRadius: 0, + tension: 0.3, + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + animation: false, + plugins: { + title: { + display: true, + text: "area-elevation-profile · javascript · chartjs · anyplot.ai", + color: t.ink, + font: { size: 22, weight: "500" }, + padding: { top: 16, bottom: 4 }, + }, + subtitle: { + display: true, + text: "Vertical exaggeration ≈ 10×", + color: t.inkSoft, + font: { size: 13 }, + padding: { bottom: 10 }, + }, + legend: { display: false }, + }, + scales: { + x: { + type: "linear", + min: 0, + max: 80, + ticks: { + color: t.inkSoft, + font: { size: 14 }, + stepSize: 10, + callback: (v) => v + " km", + }, + grid: { display: false }, + title: { + display: true, + text: "Distance (km)", + color: t.ink, + font: { size: 16 }, + }, + border: { color: t.inkSoft }, + }, + y: { + min: 400, + max: 2800, + ticks: { + color: t.inkSoft, + font: { size: 14 }, + stepSize: 400, + callback: (v) => v + " m", + }, + grid: { color: t.grid }, + title: { + display: true, + text: "Elevation (m)", + color: t.ink, + font: { size: 16 }, + }, + border: { color: t.inkSoft }, + }, + }, + }, + plugins: [landmarkPlugin], +}); diff --git a/plots/area-elevation-profile/metadata/javascript/chartjs.yaml b/plots/area-elevation-profile/metadata/javascript/chartjs.yaml new file mode 100644 index 0000000000..fa84772dcd --- /dev/null +++ b/plots/area-elevation-profile/metadata/javascript/chartjs.yaml @@ -0,0 +1,272 @@ +library: chartjs +language: javascript +specification_id: area-elevation-profile +created: '2026-06-10T06:06:54Z' +updated: '2026-06-10T06:25:53Z' +generated_by: claude-sonnet +workflow_run: 27256385447 +issue: 4578 +language_version: 22.22.3 +library_version: 4.4.7 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/area-elevation-profile/javascript/chartjs/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/area-elevation-profile/javascript/chartjs/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/area-elevation-profile/javascript/chartjs/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/area-elevation-profile/javascript/chartjs/plot-dark.html +quality_score: 90 +review: + strengths: + - Beautiful gradient area fill using Chart.js backgroundColor function with chartArea + reference — idiomatic and visually polished terrain silhouette + - 'Custom landmarkPlugin using afterDraw with canvas 2D API: hollow dots, dashed + vertical guides, and themed rounded-corner label boxes demonstrate deep Chart.js + expertise' + - Cubic Hermite spline interpolation over 13 hard-coded control points produces + a smooth, realistic Alpine terrain profile without any external dependency + - 'Full theme adaptation throughout: pageBg, elevatedBg, ink, inkSoft, grid tokens + all applied correctly — both renders are visually correct and readable' + - Label clamping logic (Math.max/min on boxLeft) prevents landmark boxes from clipping + at chart edges; flip-below logic handles summit-level labels near the chart top + - Vertical exaggeration noted via subtitle; single-series legend correctly hidden; + x-axis gridlines hidden keeping the chart clean + weaknesses: + - 'DE-01: Design is professional and above defaults, but lacks a strong visual signature + — the optional slope-based gradient coloring (green flat → red steep) mentioned + in the spec would add a distinctive second data dimension to the fill and elevate + the chart to publication-ready level' + - 'DE-02: Axis border lines (border: { color: t.inkSoft }) on both x and y scales + create a subtle box frame around the data area; removing the top and right equivalent + borders (or using borderColor: ''transparent'' on the right/top sides) would align + with the style guide''s L-shaped spine preference and improve refinement' + - 'DE-03: Visual storytelling focuses on landmark labels but misses a climactic + emphasis on the Summit Peak — making the summit label box slightly larger, adding + a distinct color accent (e.g. palette[1] border on the box), or a summit annotation + arrow would guide the viewer''s eye to the narrative high point' + - 'VQ-02: Landmark label boxes for First Pass (20 km) and Summit Peak (33 km) are + horizontally close (~13 km apart); if landmark names were longer they would crowd + — adding a minimum horizontal separation check in the plugin would future-proof + this' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) — correct theme surface, clearly not pure white + Chrome: Title "area-elevation-profile · javascript · chartjs · anyplot.ai" in dark ink at top (~55% canvas width), subtitle "Vertical exaggeration ≈ 10×" in secondary ink below. X-axis label "Distance (km)" and Y-axis label "Elevation (m)" both in dark ink, visually balanced at 16px CSS. Tick labels in inkSoft (muted dark) at 14px CSS; Y-axis shows 400m–2800m at 400m intervals, X-axis shows 0–80km at 10km intervals. Subtle Y-axis grid lines; no X-axis grid — clean visual floor. + Data: Single terrain profile series in #009E73 (brand green) with gradient area fill from ~73% opacity at top to ~9% at the x-axis baseline — creates a rich terrain silhouette effect. Six landmark annotations (Trailhead, First Pass, Summit Peak, Mountain Hut, Second Pass, Trail End) with hollow circle dots on the terrain line, dashed vertical guides to the x-axis, and elevated-background rounded-corner label boxes showing name (bold) and elevation. + Legibility verdict: PASS — all title, axis, tick, and landmark label text is clearly readable against the warm off-white background; no light-on-light issues + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17) — correct dark theme surface, not pure black + Chrome: Title and subtitle in light ink (#F0EFE8 equivalent); axis labels and tick labels in light secondary ink (#B8B7B0 equivalent) — all text is light-on-dark and fully readable. Y-axis grid lines are subtly visible as faint light rules. Landmark label boxes use elevatedBg (#242420) creating a barely perceptible lift from the background, with ink text readable within. + Data: Terrain profile and gradient fill are identical to light render — #009E73 brand green line and fill with same gradient stops. Data color identity confirmed across both themes; only chrome (background, text, grid) flips. + Legibility verdict: PASS — no dark-on-dark failures detected; brand green #009E73 remains clearly visible on the dark surface; all text elements readable in both landmark boxes and axis chrome + criteria_checklist: + visual_quality: + score: 28 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All font sizes explicitly set (title 22px, axis labels 16px, ticks + 14px, landmark labels 14-15px); readable in both themes; landmark labels + at 15px CSS are appropriately sized for the complexity of this chart + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: Labels well-placed with flip-below and horizontal clamping; First + Pass and Summit Peak boxes (20km, 33km) are comfortably separated in the + render but close enough to warrant a minor deduction + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Terrain profile and gradient fill are prominently visible; pointRadius=0 + appropriate for 200-point dense profile line + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: 'Single series using #009E73 brand green — CVD-safe, adequate contrast + on both surfaces' + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Canvas gate passed; landscape 3200x1800 appropriate for elevation + profile; generous whitespace with good plot-to-canvas ratio + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Distance (km) and Elevation (m) with units; correct anyplot title + format; subtitle noting vertical exaggeration + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series t.palette[0] = #009E73; background #FAF8F1 (light) + / #1A1A17 (dark); all chrome tokens theme-adaptive; data colors identical + across both renders' + design_excellence: + score: 14 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Gradient area fill, custom landmark plugin with rounded boxes and + hollow dots — clearly above defaults; professional mountain-silhouette look; + not publication-ready (no slope coloring, no accent emphasis on summit) + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Y-axis grid only (x-axis gridless), legend hidden, generous whitespace; + axis border lines create slight box frame that could be further refined + toward L-shaped spines + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Landmark journey narrative with dashed guides and labeled boxes; + gradient fill conveys terrain depth; no climactic emphasis on Summit Peak + as the narrative high point + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct area/filled-line chart for elevation profile + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Area fill, start/end labels (Trailhead/Trail End), landmark annotations + with vertical guides, vertical exaggeration noted in subtitle + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Distance (km) on X, Elevation (m) on Y; 200 sample points within + spec's 50-500 range; 80 km trail with 6 landmarks + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title exactly 'area-elevation-profile · javascript · chartjs · anyplot.ai'; + no legend (appropriate for single series) + data_quality: + score: 14 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 5 + max: 6 + passed: true + comment: Area fill, smooth interpolation, landmark annotations, elevation + labels, start/end labels, vertical exaggeration — all main features. Optional + slope-based gradient coloring from spec not implemented + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Alpine trail scenario; realistic landmark names (passes, summit, + hut, trailhead); neutral outdoor-recreation context + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Elevation 880m–2450m realistic for European Alps; 80km multi-stage + trail; elevation range plausible for the described terrain + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Flat script: data definition → canvas creation → plugin object → + Chart instantiation; no wrapper classes' + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: All data hard-coded deterministically; no Math.random() + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: No imports; uses global Chart and window.ANYPLOT_TOKENS as per harness + contract + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Well-structured; hermite spline is appropriately complex for the + use case; no fake UI + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: 'Correct Chart.js API; harness handles file output; animation: false + set correctly' + library_mastery: + score: 9 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: 'Expert Chart.js patterns: gradient backgroundColor as function with + chartArea reference, fill: ''start'' for area, linear scale with tick callbacks, + responsive/maintainAspectRatio/animation settings all correct' + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Custom afterDraw plugin with canvas 2D API (roundRect, arc, setLineDash) + is distinctively Chart.js; chartArea-anchored gradient fill is a pattern + unique to this library's declarative API + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + patterns: + - data-generation + - iteration-over-groups + dataprep: + - interpolation + styling: + - gradient-fill + - alpha-blending