diff --git a/plots/circlepacking-basic/implementations/javascript/highcharts.js b/plots/circlepacking-basic/implementations/javascript/highcharts.js new file mode 100644 index 0000000000..63cf640e5b --- /dev/null +++ b/plots/circlepacking-basic/implementations/javascript/highcharts.js @@ -0,0 +1,336 @@ +// anyplot.ai +// circlepacking-basic: Circle Packing Chart +// Library: highcharts 12.6.0 | JavaScript 22.23.2 +// Quality: 89/100 | Created: 2026-09-02 +//# anyplot-orientation: square + +// Highcharts' packed-bubble / circle-packing series lives in the +// highcharts-more add-on module, which is not vendored here — only the core +// bundle (with its SVGRenderer) is loaded. So the hierarchy below is packed +// with a small deterministic relaxation algorithm (index-seeded, no RNG) and +// drawn natively with `chart.renderer`: a root ring, one ring per directory, +// and (for one directory) a nested sub-ring for its files, three levels deep. +// The same recursive layout/render pair handles every level. No other +// charting library is used. + +const t = window.ANYPLOT_TOKENS; + +// --- Data: a small repository's directory sizes (KB), up to 3 levels deep ---- +// Most directories hold files directly (2 levels below root); "components/" +// is further split into named files to exercise a 3rd nesting level. +const CATEGORIES = [ + { + id: "src", + label: "src/", + children: [ + { + id: "src-components", + label: "components/", + children: [ + { id: "src-components-table", label: "Table.jsx", value: 150 }, + { id: "src-components-modal", label: "Modal.jsx", value: 130 }, + { id: "src-components-form", label: "Form.jsx", value: 120 }, + { id: "src-components-button", label: "Button.jsx", value: 80 }, + ], + }, + { id: "src-api", label: "api.js", value: 340 }, + { id: "src-store", label: "store.js", value: 210 }, + { id: "src-utils", label: "utils.js", value: 120 }, + { id: "src-hooks", label: "hooks.js", value: 95 }, + { id: "src-styles", label: "styles.css", value: 70 }, + { id: "src-types", label: "types.d.ts", value: 55 }, + ], + }, + { + id: "tests", + label: "tests/", + children: [ + { id: "tests-unit", label: "unit/", value: 200 }, + { id: "tests-integration", label: "integration/", value: 150 }, + { id: "tests-e2e", label: "e2e/", value: 90 }, + { id: "tests-fixtures", label: "fixtures/", value: 60 }, + { id: "tests-mocks", label: "mocks/", value: 45 }, + ], + }, + { + id: "docs", + label: "docs/", + children: [ + { id: "docs-api", label: "api-reference.md", value: 130 }, + { id: "docs-guide", label: "guide.md", value: 85 }, + { id: "docs-changelog", label: "changelog.md", value: 50 }, + { id: "docs-readme", label: "readme.md", value: 45 }, + ], + }, + { + id: "assets", + label: "assets/", + children: [ + { id: "assets-images", label: "images/", value: 380 }, + { id: "assets-fonts", label: "fonts/", value: 190 }, + { id: "assets-videos", label: "videos/", value: 140 }, + { id: "assets-icons", label: "icons/", value: 95 }, + { id: "assets-logo", label: "logo.svg", value: 40 }, + ], + }, + { + id: "build", + label: "build/", + children: [ + { id: "build-bundle", label: "bundle.js", value: 600 }, + { id: "build-vendor", label: "vendor.js", value: 420 }, + { id: "build-maps", label: "sourcemaps/", value: 210 }, + { id: "build-manifest", label: "manifest.json", value: 45 }, + ], + }, +]; + +// Directories are abstract categories, so the Imprint palette is used in +// canonical order — src = brand green (palette[0]). +const categoryColor = (i) => t.palette[i % t.palette.length]; + +const formatSize = (kb) => (kb >= 1000 ? `${(kb / 1000).toFixed(1)} MB` : `${kb} KB`); + +function hexToRgba(hex, alpha) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +// Pick readable label ink by the fill's own luminance rather than the active +// theme — a leaf circle's colour is the same in light and dark mode, so the +// text riding on top needs the ink value with contrast to THAT colour, not to +// the page. #1A1A17 / #F0EFE8 are exactly the light/dark INK tokens reused +// for this purpose, never a new custom hex. +function contrastInk(hex) { + const r = parseInt(hex.slice(1, 3), 16) / 255; + const g = parseInt(hex.slice(3, 5), 16) / 255; + const b = parseInt(hex.slice(5, 7), 16) / 255; + const lin = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4); + const luminance = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b); + return luminance > 0.42 ? "#1A1A17" : "#F0EFE8"; +} + +// --- Circle packing: a small deterministic relaxation, applied per level ----- +// Seed circles on a ring (index-based angle, no RNG), then repeatedly resolve +// overlaps and pull toward the centroid until the group settles into a tight, +// non-overlapping cluster. The same routine packs siblings at every depth — +// files within a directory, a directory alongside files within its parent, +// and top-level directories within the repository. +function packCircles(items, gap) { + if (items.length === 0) return { placed: [], boundingRadius: 0 }; + if (items.length === 1) { + return { placed: [{ id: items[0].id, x: 0, y: 0, r: items[0].r }], boundingRadius: items[0].r }; + } + const n = items.length; + const nodes = items.map((it, i) => { + const theta = (i / n) * Math.PI * 2; + const seedR = it.r * 1.6 + i * 4; + return { id: it.id, r: it.r, x: seedR * Math.cos(theta), y: seedR * Math.sin(theta) }; + }); + for (let iter = 0; iter < 260; iter += 1) { + nodes.forEach((a) => { + a.x -= a.x * 0.02; + a.y -= a.y * 0.02; + }); + for (let i = 0; i < n; i += 1) { + for (let j = i + 1; j < n; j += 1) { + const a = nodes[i]; + const b = nodes[j]; + let dx = b.x - a.x; + let dy = b.y - a.y; + let dist = Math.sqrt(dx * dx + dy * dy); + const minDist = a.r + b.r + gap; + if (dist < 1e-6) { + dx = 0.01 * (i + 1); + dy = 0.01 * (j + 1); + dist = Math.sqrt(dx * dx + dy * dy); + } + if (dist < minDist) { + const overlap = (minDist - dist) / 2; + const ux = dx / dist; + const uy = dy / dist; + a.x -= ux * overlap; + a.y -= uy * overlap; + b.x += ux * overlap; + b.y += uy * overlap; + } + } + } + } + const minX = Math.min(...nodes.map((nd) => nd.x - nd.r)); + const maxX = Math.max(...nodes.map((nd) => nd.x + nd.r)); + const minY = Math.min(...nodes.map((nd) => nd.y - nd.r)); + const maxY = Math.max(...nodes.map((nd) => nd.y + nd.r)); + const ox = (minX + maxX) / 2; + const oy = (minY + maxY) / 2; + nodes.forEach((nd) => { + nd.x -= ox; + nd.y -= oy; + }); + let boundingRadius = 0; + nodes.forEach((nd) => { + const d = Math.sqrt(nd.x * nd.x + nd.y * nd.y) + nd.r; + if (d > boundingRadius) boundingRadius = d; + }); + return { placed: nodes, boundingRadius }; +} + +// --- Layout: a recursive pack, depth 0 = root, 1 = category, 2+ = nested ---- +// Circle area, not radius, encodes size: radius = sqrt(value) in abstract +// units; everything is rescaled to pixels once the final plot area is known. +// Every node below the root inherits its top-level category's colour index, +// so a nested sub-directory (e.g. "components/") reads as part of "src/". +const LEAF_GAP = 1.4; +const RING_PADDING = 6.5; +const CATEGORY_GAP = 7; +const ROOT_PADDING = 7; + +const leafRadius = (value) => Math.sqrt(value); + +function layoutNode(node, depth, categoryIndex) { + if (!node.children) { + return { id: node.id, label: node.label, value: node.value, r: leafRadius(node.value), depth, categoryIndex, leaf: true }; + } + const childLayouts = node.children.map((child, i) => layoutNode(child, depth + 1, depth === 0 ? i : categoryIndex)); + const gap = depth === 0 ? CATEGORY_GAP : LEAF_GAP; + const { placed, boundingRadius } = packCircles( + childLayouts.map((cl) => ({ id: cl.id, r: cl.r })), + gap, + ); + const totalValue = childLayouts.reduce((s, cl) => s + cl.value, 0); + const children = childLayouts.map((cl) => { + const p = placed.find((pp) => pp.id === cl.id); + return { ...cl, x: p.x, y: p.y }; + }); + // Padding shrinks with depth so nested rings hug their contents a little + // tighter than the outer category rings do. + const padding = depth === 0 ? ROOT_PADDING : depth === 1 ? RING_PADDING : RING_PADDING * 0.7; + return { + id: node.id, + label: node.label, + value: totalValue, + r: boundingRadius + padding, + depth, + categoryIndex, + leaf: false, + children, + }; +} + +const tree = layoutNode({ id: "root", label: "repository", children: CATEGORIES }, 0, null); + +// --- Chart shell (no series — every circle is drawn with the renderer) ------- +const chart = Highcharts.chart("container", { + chart: { + backgroundColor: "transparent", + animation: false, + style: { fontFamily: "inherit" }, + marginTop: 100, + marginBottom: 40, + marginLeft: 40, + marginRight: 40, + }, + credits: { enabled: false }, + colors: t.palette, + title: { + text: "circlepacking-basic · javascript · highcharts · anyplot.ai", + style: { color: t.ink, fontSize: "22px", fontWeight: "600" }, + }, + subtitle: { + text: "Repository directory sizes — circle area = file/folder size (KB), colour = directory", + style: { color: t.inkSoft, fontSize: "14px" }, + }, + xAxis: { visible: false }, + yAxis: { visible: false }, + legend: { enabled: false }, + plotOptions: { series: { animation: false } }, + series: [], +}); + +const cx = chart.plotLeft + chart.plotWidth / 2; +const cy = chart.plotTop + chart.plotHeight / 2; +const radiusMax = Math.min(chart.plotWidth, chart.plotHeight) / 2; +const finalScale = (radiusMax - 12) / tree.r; +const px = (x) => cx + x * finalScale; +const py = (y) => cy + y * finalScale; +const pr = (r) => r * finalScale; + +function addTitle(el, text) { + const title = document.createElementNS("http://www.w3.org/2000/svg", "title"); + title.textContent = text; + el.element.appendChild(title); +} + +const g = chart.renderer.g("circle-packing").add(); + +// --- Render: recursive, depth drives stroke weight / fill alpha / labels ---- +// depth 0 = root outline, depth 1 = category ring, depth 2+ = nested rings +// (subtly thinner stroke, higher fill alpha) or solid leaf circles. +function renderNode(node, parentX, parentY, pathPrefix) { + const x = px(parentX + (node.x ?? 0)); + const y = py(parentY + (node.y ?? 0)); + const r = pr(node.r); + + if (node.leaf) { + const color = categoryColor(node.categoryIndex); + const leaf = chart.renderer + .circle(x, y, r) + .attr({ fill: color, stroke: t.pageBg, "stroke-width": 1.5 }) + .add(g); + addTitle(leaf, `${pathPrefix}${node.label} — ${formatSize(node.value)}`); + + if (r >= 28) { + const fontSize = Math.max(10, Math.min(13, Math.round(r * 0.26))); + const maxChars = Math.max(3, Math.floor((r * 1.7) / (fontSize * 0.58))); + const text = node.label.length > maxChars ? `${node.label.slice(0, maxChars - 1)}…` : node.label; + chart.renderer + .text(text, x, y + fontSize * 0.35) + .attr({ align: "center" }) + .css({ color: contrastInk(color), fontSize: `${fontSize}px`, fontWeight: "500" }) + .add(g); + } + return; + } + + if (node.depth === 0) { + const root = chart.renderer + .circle(x, y, r) + .attr({ fill: "transparent", stroke: t.grid, "stroke-width": 1.5 }) + .add(g); + addTitle(root, `repository — ${formatSize(node.value)} total`); + } else { + const isCategory = node.depth === 1; + const color = categoryColor(node.categoryIndex); + const ring = chart.renderer + .circle(x, y, r) + .attr({ + fill: hexToRgba(color, isCategory ? 0.1 : 0.17), + stroke: color, + "stroke-width": isCategory ? 2.5 : 1.8, + }) + .add(g); + addTitle(ring, `${pathPrefix}${node.label} — ${formatSize(node.value)} total`); + + const labelThreshold = isCategory ? 55 : 40; + if (r >= labelThreshold) { + const fontSize = isCategory ? Math.max(12, Math.min(16, Math.round(r * 0.1))) : Math.max(11, Math.min(13, Math.round(r * 0.12))); + chart.renderer + .text(`${node.label} · ${formatSize(node.value)}`, x, y - r + fontSize + 6) + .attr({ align: "center" }) + .css({ color: t.ink, fontSize: `${fontSize}px`, fontWeight: "600" }) + .add(g); + } + } + + const childPathPrefix = node.depth === 0 ? "" : `${pathPrefix}${node.label}`; + const originX = parentX + (node.x ?? 0); + const originY = parentY + (node.y ?? 0); + node.children.forEach((child) => renderNode(child, originX, originY, childPathPrefix)); +} + +renderNode(tree, 0, 0, ""); + +// Static-frame timing signal for the harness. +window.__anyplotReady = true; diff --git a/plots/circlepacking-basic/metadata/javascript/highcharts.yaml b/plots/circlepacking-basic/metadata/javascript/highcharts.yaml new file mode 100644 index 0000000000..fb9b18e63c --- /dev/null +++ b/plots/circlepacking-basic/metadata/javascript/highcharts.yaml @@ -0,0 +1,266 @@ +library: highcharts +language: javascript +specification_id: circlepacking-basic +created: '2026-09-02T15:52:32Z' +updated: '2026-09-02T16:08:57Z' +generated_by: claude-sonnet +workflow_run: 33649853396 +issue: 2498 +language_version: 22.23.2 +library_version: 12.6.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/highcharts/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/highcharts/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/highcharts/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/highcharts/plot-dark.html +quality_score: 89 +review: + strengths: + - 'Fixed the attempt-1 hierarchy-depth weakness: components/ is now split into 4 + named files (Table.jsx, Modal.jsx, Form.jsx, Button.jsx), giving the chart a genuine + 3-level hierarchy (root -> category -> subdirectory -> file) instead of 2, which + better demonstrates the packing algorithm and improves feature coverage' + - 'Addressed the attempt-1 uniformity critique: ring stroke width (2.5 for category + rings vs 1.8 for nested sub-rings), fill alpha (0.1 vs 0.17), and padding now + vary by depth, giving the deeper components/ ring a visually distinct treatment + from the top-level category rings' + - 'Per-leaf label ink is computed from each fill''s own luminance rather than the + page theme, so text stays readable on every one of the 8 Imprint colors in both + light and dark renders -- first series correctly uses brand green #009E73' + - Data colors are pixel-identical between the light and dark renders; only chrome + (title/subtitle ink, ring outline colors) flips, and SVG hover tooltips + preserve full names/totals on every circle including truncated small labels + weaknesses: + - 'New nested-ring label overlap introduced by the added 3rd hierarchy level: the + ''components/ · 480 KB'' ring-title text is crossed by the components ring''s + own stroke line (the descender of ''p'' in ''components'' is cut by the outline) + -- the label formula `y - r + fontSize + 6` doesn''t clear the stroke for depth>=2 + rings, which use a smaller RING_PADDING * 0.7. Increase the offset (or the padding) + for nested rings so their label sits fully above the stroke.' + - Smallest leaf labels (font floor is 10px, e.g. 'e2e/', 'icons/', 'guide.md', truncated + names like 'types.d...') are legible at full 2400px resolution but wash out at + the ~400px mobile thumbnail scale -- consider raising the font floor slightly + or hiding labels on circles below ~30px radius instead of the current 28px threshold. + - 'Implementation is still 100% chart.renderer with series: [] -- a well-justified + workaround given highcharts-more isn''t vendored, but it means Highcharts'' higher-level + series/plotOptions API is never exercised, which continues to cap Library Mastery.' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white, consistent with #FAF8F1 -- not pure white. + Chrome: Title "circlepacking-basic · javascript · highcharts · anyplot.ai" in bold dark ink at top; subtitle "Repository directory sizes — circle area = file/folder size (KB), colour = directory" in softer gray ink below. Each of the 5 top-level directory rings has a dark-ink label above it (e.g. "build/ · 1.3 MB", "src/ · 1.4 MB"), and the new nested "components/ · 480 KB" sub-ring inside src/ has its own label too. All chrome text is clearly readable against the light background, EXCEPT the "components/ · 480 KB" label whose descender is visibly crossed by the components ring's own stroke line (a minor overlap defect, not a legibility failure -- every character is still fully readable). + Data: Root circle outlined in light gray encompasses 5 tinted category rings (src=green #009E73, tests=lavender, docs=blue, assets=ochre, build=matte red -- canonical Imprint order for these abstract directory categories). Inside src/, a nested components/ ring shows 4 named files (Table.jsx, Modal.jsx, Form.jsx, Button...) three levels deep. Each ring has a low-alpha fill in its category color and contains solid leaf circles in the same color, area-scaled to file/folder size. Leaf labels use ink chosen per-leaf from the fill's own luminance (white text on green/red/blue/lavender leaves, dark text on ochre leaves) -- first series is confirmed #009E73. + Legibility verdict: PASS (one minor label/stroke overlap noted above, not a blocking failure) + + Dark render (plot-dark.png): + Background: Warm near-black, consistent with #1A1A17 -- not pure black. + Chrome: Same title and subtitle now rendered in light ink, clearly readable against the dark background. Ring category labels (e.g. "src/ · 1.4 MB", "components/ · 480 KB") flip to light ink for contrast against the page; the same stroke-overlap on "components/" is present here too (chrome-independent, geometry-driven). + Data: All 5 category ring colors and all leaf fill colors are pixel-identical to the light render (green/lavender/blue/ochre/red) -- only the chrome flipped. Leaf label ink is unchanged from the light render (e.g. "api.js" still white-on-green, "images/" still dark-on-ochre) since it's computed from each leaf's fixed fill color rather than the page theme -- intended design, reads correctly in both themes. No dark-on-dark or light-on-light failures observed. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 26 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 6 + max: 8 + passed: true + comment: Explicit font sizes throughout, readable at full resolution in both + themes; smallest leaf labels (font floor 10px) still likely to fade at ~400px + mobile scale + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: The new 'components/ · 480 KB' nested-ring label is crossed by that + ring's own stroke line -- a real but minor overlap, text remains fully readable + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Circles well-sized and packed with visible gaps; a couple of the + smallest leaf circles are tiny dots by nature of the data + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Per-leaf luminance-based ink keeps contrast high on every palette + color; CVD-safe Imprint palette + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Square 2400x2400 canvas confirmed (measured); root circle fills a + good majority of the canvas with balanced margins, no overflow or clipping + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: No traditional axes needed for circle packing; subtitle explicitly + states the unit (KB) and encoding + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series is #009E73; remaining 4 categories use canonical Imprint + order; identical data colors across themes; backgrounds theme-correct' + design_excellence: + score: 17 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 7 + max: 8 + passed: true + comment: Depth-based stroke width and fill alpha differentiation (new this + attempt) plus the deeper hierarchy push this clearly past 'configured default' + toward polished + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: No axes/spines, generous whitespace, subtle tinted ring fills; the + components/ label crowding keeps it just short of perfect + - id: DE-03 + name: Data Storytelling + score: 5 + max: 6 + passed: true + comment: Size-by-area plus category color plus the new 3rd nesting level makes + the largest directories/files immediately obvious while showing genuine + hierarchical depth + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Genuine circle packing chart + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Efficient non-overlapping packing, color-by-category, labels on larger + circles with hover for smaller, area-scaled sizing, padded root circle -- + all present + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Circle area correctly encodes value (radius = sqrt(value)); all nodes + represented + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title matches the mandated format exactly; categories self-label + on the chart in lieu of a separate legend, which is appropriate here + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Now uses 3 hierarchy levels (root -> category -> components subdirectory + -> file), up from 2 in the previous attempt, with good size variation (40 + KB to 600 KB) across 5 categories and ~30 leaves + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Plausible, neutral software-repository directory-size scenario + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: File/folder sizes and MB/KB formatting are realistic and internally + consistent + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: true + comment: Several helper functions (packCircles, layoutNode, contrastInk, hexToRgba, + formatSize) are used, justified by the custom recursive geometry algorithm, + but it departs from a flat imports->data->plot script + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: 'Fully deterministic: hardcoded data, index-seeded relaxation, no + RNG' + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: No unused imports; only browser globals used + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Appropriate complexity for a from-scratch recursive packing algorithm; + no fake UI/interactivity + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Correct mount-node contract, animation disabled, harness handles + PNG/HTML export + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 3 + max: 5 + passed: true + comment: Correct, well-integrated use of chart.renderer given highcharts-more + isn't vendored, but still bypasses Highcharts' higher-level series/plotOptions + API entirely + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Leverages chart.renderer.g/circle/text tied into the chart's own + plotLeft/plotWidth/margins lifecycle -- a genuinely Highcharts-specific + technique, not a generic SVG draw + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - patches + - hover-tooltips + patterns: + - iteration-over-groups + dataprep: [] + styling: + - alpha-blending