diff --git a/plots/choropleth-basic/implementations/javascript/chartjs.js b/plots/choropleth-basic/implementations/javascript/chartjs.js new file mode 100644 index 0000000000..9dee29a8d3 --- /dev/null +++ b/plots/choropleth-basic/implementations/javascript/chartjs.js @@ -0,0 +1,258 @@ +// anyplot.ai +// choropleth-basic: Choropleth Map with Regional Coloring +// Library: chartjs 4.4.7 | JavaScript 22.23.2 +// Quality: 85/100 | Created: 2026-09-02 + +// Chart.js has no built-in geographic/shape geometry (that lives only in the +// chartjs-chart-geo plugin, which is not installed in this runtime — see +// prompts/library/chartjs.md "No Workarounds"). This renders the same +// region -> value story as a tile map: one square per region, positioned by +// its real-world longitude/latitude, colored on the Imprint sequential scale. +// Region "boundaries" are the square's stroke; missing data renders as a +// muted gray tile, per the specification's data-handling note. +const t = window.ANYPLOT_TOKENS; +const MUTED = t.theme === "dark" ? "#A8A79F" : "#6B6A63"; + +// --- Data (in-memory, deterministic) ---------------------------------------- +// Renewable share of electricity generation (%) by country, with approximate +// centroid coordinates. Two countries carry no reading to demonstrate the +// missing-data handling the spec calls for. +const countries = [ + { code: "CA", lon: -106.3, lat: 56.1, value: 68 }, + { code: "US", lon: -95.7, lat: 37.1, value: 21 }, + { code: "MX", lon: -102.6, lat: 23.6, value: 24 }, + { code: "BR", lon: -51.9, lat: -14.2, value: 84 }, + { code: "AR", lon: -63.6, lat: -38.4, value: 33 }, + { code: "CO", lon: -74.1, lat: 4.6, value: 70 }, + { code: "PE", lon: -75.2, lat: -9.2, value: 60 }, + { code: "CL", lon: -71.5, lat: -35.7, value: 46 }, + { code: "GB", lon: -3.4, lat: 55.4, value: 43 }, + { code: "FR", lon: 2.2, lat: 46.6, value: 27 }, + { code: "DE", lon: 10.5, lat: 51.2, value: 46 }, + { code: "ES", lon: -3.7, lat: 40.5, value: 50 }, + { code: "IT", lon: 12.6, lat: 41.9, value: 41 }, + { code: "NO", lon: 8.5, lat: 60.5, value: 98 }, + { code: "SE", lon: 18.6, lat: 60.1, value: 68 }, + { code: "PL", lon: 19.1, lat: 51.9, value: 17 }, + { code: "RU", lon: 105.3, lat: 61.5, value: 20 }, + { code: "TR", lon: 35.2, lat: 38.9, value: 44 }, + { code: "EG", lon: 30.8, lat: 26.8, value: 12 }, + { code: "NG", lon: 8.7, lat: 9.1, value: null }, + { code: "ZA", lon: 22.9, lat: -30.6, value: 9 }, + { code: "KE", lon: 37.9, lat: -0.0, value: 90 }, + { code: "MA", lon: -7.1, lat: 31.8, value: 20 }, + { code: "SA", lon: 45.1, lat: 23.9, value: 1 }, + { code: "IN", lon: 78.9, lat: 20.6, value: 23 }, + { code: "CN", lon: 104.1, lat: 35.9, value: 31 }, + { code: "JP", lon: 138.3, lat: 36.2, value: 22 }, + { code: "KR", lon: 127.8, lat: 35.9, value: 9 }, + { code: "ID", lon: 113.9, lat: -0.8, value: null }, + { code: "TH", lon: 100.9, lat: 15.9, value: 18 }, + { code: "VN", lon: 108.8, lat: 14.1, value: 40 }, + { code: "AU", lon: 133.8, lat: -25.3, value: 32 }, + { code: "NZ", lon: 174.9, lat: -40.9, value: 87 }, +]; + +// --- Imprint sequential scale (green -> blue), binned for a readable legend +const hexToRgb = (hex) => ({ + r: parseInt(hex.slice(1, 3), 16), + g: parseInt(hex.slice(3, 5), 16), + b: parseInt(hex.slice(5, 7), 16), +}); +const lerp = (a, b, f) => Math.round(a + (b - a) * f); +const seqRgb = (frac) => { + const c0 = hexToRgb(t.seq[0]); + const c1 = hexToRgb(t.seq[1]); + return { r: lerp(c0.r, c1.r, frac), g: lerp(c0.g, c1.g, frac), b: lerp(c0.b, c1.b, frac) }; +}; +const toCss = ({ r, g, b }) => `rgb(${r}, ${g}, ${b})`; +// WCAG-ish relative luminance so a code label always reads against its own tile, +// regardless of theme (tile fill colors are identical across themes). +const luminance = ({ r, g, b }) => 0.299 * r + 0.587 * g + 0.114 * b; +const LABEL_LIGHT = "#FFFDF6"; +const LABEL_DARK = "#1A1A17"; + +const bins = [ + { label: "< 10%", max: 10 }, + { label: "10–25%", max: 25 }, + { label: "25–40%", max: 40 }, + { label: "40–65%", max: 65 }, + { label: "≥ 65%", max: Infinity }, +].map((bin, i, arr) => { + // Highest-renewable bins read as brand green, lowest as blue, so the color + // story matches the metric (more green = more renewable). + const rgb = seqRgb(1 - i / (arr.length - 1)); + return { ...bin, color: toCss(rgb), textColor: luminance(rgb) > 140 ? LABEL_DARK : LABEL_LIGHT }; +}); + +const binIndex = (value) => bins.findIndex((bin) => value <= bin.max); +const mutedTextColor = luminance(hexToRgb(MUTED)) > 140 ? LABEL_DARK : LABEL_LIGHT; + +// --- Basemap ----------------------------------------------------------------- +// Chart.js core has no polygon-fill geometry, so this draws a lightweight +// continent-outline graticule (stroked line datasets, no fill) behind the +// tiles purely for geographic orientation — it is not a projected basemap. +const CONTINENTS = [ + [ + [-125, 49], [-95, 78], [-75, 68], [-60, 50], [-52, 47], [-65, 44], + [-75, 35], [-80, 26], [-97, 26], [-90, 15], [-105, 20], [-115, 29], [-125, 49], + ], + [ + [-77, 10], [-60, 10], [-50, 0], [-35, -5], [-40, -20], [-48, -25], + [-58, -34], [-68, -47], [-72, -40], [-70, -20], [-79, -5], [-77, 10], + ], + [ + [-17, 15], [0, 5], [10, 4], [15, -5], [13, -18], [18, -34], [30, -30], + [40, -15], [42, 0], [45, 10], [38, 15], [33, 31], [10, 37], [-17, 15], + ], + [ + [-9, 43], [0, 50], [10, 54], [20, 55], [30, 60], [40, 65], [30, 45], + [20, 40], [10, 38], [-5, 36], [-9, 43], + ], + [ + [26, 40], [40, 45], [60, 55], [80, 55], [100, 50], [120, 50], [140, 55], + [145, 45], [130, 35], [120, 25], [105, 10], [95, 5], [80, 10], [70, 20], + [60, 25], [50, 30], [35, 30], [26, 40], + ], + [ + [113, -22], [122, -18], [135, -12], [145, -16], [153, -28], [150, -38], + [140, -38], [130, -32], [115, -34], [113, -22], + ], +]; +const basemapDatasets = CONTINENTS.map((outline, i) => ({ + type: "line", + label: `basemap-${i}`, + skipLegend: true, + data: outline.map(([lon, lat]) => ({ x: lon, y: lat })), + borderColor: t.grid, + backgroundColor: "transparent", + borderWidth: 1.5, + pointRadius: 0, + pointHoverRadius: 0, + fill: false, + tension: 0, +})); + +// --- Mount ------------------------------------------------------------------- +const canvas = document.createElement("canvas"); +document.getElementById("container").appendChild(canvas); + +// A tile-map has no room for chartjs-chart-geo's boundary shapes (not +// installed — see prompts/library/chartjs.md), so region codes are drawn +// directly onto each square with a plain Chart.js plugin (native canvas +// access, no external package) instead. +const regionLabelPlugin = { + id: "regionLabels", + afterDatasetsDraw(chart) { + const { ctx } = chart; + chart.data.datasets.forEach((dataset, dsIndex) => { + if (dataset.skipLegend) return; // basemap outline, not a data tile + const meta = chart.getDatasetMeta(dsIndex); + meta.data.forEach((point, i) => { + const raw = dataset.data[i]; + ctx.save(); + ctx.font = "600 12px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"; + ctx.fillStyle = dataset.textColor; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(raw.code, point.x, point.y); + ctx.restore(); + }); + }); + }, +}; + +// --- Chart --------------------------------------------------------------- +const datasets = bins.map((bin, i) => ({ + label: `${bin.label} renewable`, + data: countries.filter((c) => c.value !== null && binIndex(c.value) === i), + backgroundColor: bin.color, + textColor: bin.textColor, + borderColor: t.ink, + borderWidth: 1.5, + pointStyle: "rect", + pointRadius: 20, + pointHoverRadius: 22, +})); +datasets.push({ + label: "No data", + data: countries.filter((c) => c.value === null), + backgroundColor: MUTED, + textColor: mutedTextColor, + borderColor: t.ink, + borderWidth: 1.5, + pointStyle: "rect", + pointRadius: 20, + pointHoverRadius: 22, +}); + +const title = "Renewable Electricity Share · choropleth-basic · javascript · chartjs · anyplot.ai"; +const titleFontSize = Math.round(22 * Math.min(1, 67 / title.length)); + +new Chart(canvas, { + type: "scatter", + data: { + // Basemap outlines first so tile squares draw on top of them. + datasets: [ + ...basemapDatasets, + ...datasets.map((d) => ({ + ...d, + data: d.data.map((c) => ({ x: c.lon, y: c.lat, code: c.code, value: c.value })), + })), + ], + }, + plugins: [regionLabelPlugin], + options: { + responsive: true, + maintainAspectRatio: false, + animation: false, + layout: { padding: { top: 8, bottom: 8, left: 16, right: 16 } }, + plugins: { + title: { display: true, text: title, color: t.ink, font: { size: titleFontSize, weight: "500" } }, + legend: { + position: "bottom", + labels: { + color: t.inkSoft, + font: { size: 16 }, + boxWidth: 22, + boxHeight: 22, + padding: 18, + filter: (item, data) => !data.datasets[item.datasetIndex].skipLegend, + }, + }, + tooltip: { + filter: (item) => !item.dataset.skipLegend, + callbacks: { + title: () => "", + label: (ctx) => + ctx.raw.value === null || ctx.raw.value === undefined + ? `${ctx.raw.code}: no data` + : `${ctx.raw.code}: ${ctx.raw.value}% renewable`, + }, + }, + }, + scales: { + // Tightened to the real longitude/latitude extent of the 32 countries + // (plus a small margin) rather than the full -180..180/-90..90 globe, + // so the canvas isn't dominated by empty ocean. + x: { + type: "linear", + min: -118, + max: 182, + display: true, + border: { display: false }, + ticks: { display: false }, + grid: { display: false }, + }, + y: { + type: "linear", + min: -48, + max: 68, + display: true, + border: { display: false }, + ticks: { display: false }, + grid: { display: false }, + }, + }, + }, +}); diff --git a/plots/choropleth-basic/metadata/javascript/chartjs.yaml b/plots/choropleth-basic/metadata/javascript/chartjs.yaml new file mode 100644 index 0000000000..95f558a7d6 --- /dev/null +++ b/plots/choropleth-basic/metadata/javascript/chartjs.yaml @@ -0,0 +1,251 @@ +library: chartjs +language: javascript +specification_id: choropleth-basic +created: '2026-09-02T15:42:50Z' +updated: '2026-09-02T16:01:26Z' +generated_by: claude-sonnet +workflow_run: 33647965470 +issue: 3069 +language_version: 22.23.2 +library_version: 4.4.7 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/chartjs/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/chartjs/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/chartjs/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/choropleth-basic/javascript/chartjs/plot-dark.html +quality_score: 85 +review: + strengths: + - 'All three attempt-1 weaknesses were addressed directly: continent-outline basemap + adds geographic context, sequential color direction now runs green-for-high, and + axis bounds are tightened to the real data extent' + - 'Excellent theme-adaptive rendering: data tile colors are pixel-identical between + light and dark renders, while chrome correctly flips with no dark-on-dark or light-on-light + failures' + - Highly realistic, factually accurate renewable-electricity-share data for 32 countries + spanning every continent + - Honest, well-documented handling of Chart.js's lack of native geo/polygon support + - no chartjs-chart-geo plugin, no fake interactivity + - 'Missing-data handling implemented exactly per spec: two countries render as muted + gray No data tiles with a dedicated legend entry' + - Per-tile text color chosen via luminance check so every country code stays legible + regardless of the underlying fill color + weaknesses: + - 'SC-01/SC-02: Still not a literal choropleth - no filled region polygons or map + projection, though this is a genuine Chart.js core limitation and the basemap + is the right compensating move' + - 'CQ-01: The half-dozen small helper functions (hexToRgb, lerp, seqRgb, toCss, + luminance, binIndex) push slightly past pure KISS, though each is justified by + the binned-continuous-scale requirement' + - Continent outlines are stylized/approximate rather than accurate coastlines - + acceptable given the constraints, but worth noting as a simplification + image_description: |- + Light render (plot-light.png): + Background: Warm off-white, matches #FAF8F1, not pure white. + Chrome: Title "Renewable Electricity Share · choropleth-basic · javascript · chartjs · anyplot.ai" in dark ink, centered top, clearly legible. Bottom legend with six horizontal swatches and dark-ink labels, all readable. A lightweight continent-outline graticule (thin gray strokes, no fill) sits behind the tiles for geographic context. + Data: 32 country tiles (square markers) positioned by longitude/latitude, filled with the Imprint sequential scale (imprint_seq) binned into 5 buckets. Direction is correct: high-renewable countries (Norway, Brazil, Colombia, Kenya, New Zealand) render brand green #009E73; low-renewable countries (Saudi Arabia, South Africa, South Korea) render blue. 2 muted-gray "No data" tiles (NG, ID) with a dedicated legend entry. Each tile carries a legible 2-letter country code. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black, matches #1A1A17, not pure black. + Chrome: Title and legend render in light ink, clearly visible. Continent outlines remain visible (stroked in the theme's grid color) against the dark background. + Data: Tile fill colors are pixel-identical to the light render - confirmed, only chrome flipped. In-tile country-code text colors remain legible per-tile via luminance-based logic; no dark-on-dark failures on any tile, including the muted-gray "No data" tiles. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 27 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 6 + max: 8 + passed: true + comment: Title scales with length and is clearly legible; legend at 16px; + in-tile 12px country codes are small but legible in both themes + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No overlapping tiles, labels, legend entries, or basemap strokes + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Uniform 20px-radius tiles clearly visible for 32 points; size doesn't + vary with value + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Luminance-based per-tile text color plus redundant country-code labels + avoid hue-only signaling + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Axis bounds now tightened to the real data extent, removing excess + margin flagged in attempt 1 + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Cartesian axes intentionally unlabeled for map style; legend carries + descriptive labels + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: Built from imprint_seq, direction now correct (green = high); identical + data colors across themes + design_excellence: + score: 15 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Continent-outline basemap meaningfully improves polish; still a positioned-tile + map, not a filled-region choropleth + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Basemap strokes read as a map graticule; tiles remain crisply edge-highlighted + - id: DE-03 + name: Data Storytelling + score: 5 + max: 6 + passed: true + comment: Green-for-high/blue-for-low now matches reader intuition; geographic + layout creates clear narrative + spec_compliance: + score: 12 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 3 + max: 5 + passed: true + comment: Not a true choropleth (no filled polygons), but the continent-outline + basemap closes the gap given Chart.js core's limitations + - id: SC-02 + name: Required Features + score: 3 + max: 4 + passed: true + comment: Legend, missing-data handling, and geographic basemap context present; + no real map projection or region boundaries + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X=longitude, Y=latitude, color=value; all 32 points correctly visible + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format correct; legend labels correctly describe bins + no-data + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Full 1%-98% range, all continents represented, includes missing data + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Renewable electricity share is a real, neutral, comprehensible scenario + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Values closely track real-world figures (Norway 98%, Brazil 84%, + Saudi Arabia 1%, South Korea 9%) + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: true + comment: Mostly linear flow, but several small helper functions for the binned + continuous scale and basemap + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Fully deterministic hardcoded data + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: No unused imports + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Appropriately complex, no fake functionality + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Follows the mount-node contract correctly + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Per-bin datasets for automatic legend, tooltip callbacks, documented + plugin lifecycle hook + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: false + comment: Custom plugin for label drawing plus line-dataset basemap is Chart.js-specific + composition, though tile-map substitution itself isn't unique to the library + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - colorbar + - annotations + - custom-legend + patterns: + - data-generation + - iteration-over-groups + dataprep: + - binning + styling: + - custom-colormap + - edge-highlighting + - publication-ready