From c45541db6047830b1862a49875b0f070d0a2b7fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jun 2026 20:53:04 +0000 Subject: [PATCH 1/5] feat(chartjs): implement flamegraph-basic --- .../implementations/javascript/chartjs.js | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 plots/flamegraph-basic/implementations/javascript/chartjs.js diff --git a/plots/flamegraph-basic/implementations/javascript/chartjs.js b/plots/flamegraph-basic/implementations/javascript/chartjs.js new file mode 100644 index 0000000000..e41755c893 --- /dev/null +++ b/plots/flamegraph-basic/implementations/javascript/chartjs.js @@ -0,0 +1,211 @@ +//# anyplot-orientation: landscape +// anyplot.ai +// flamegraph-basic: Flame Graph for Performance Profiling +// Library: chartjs 4.4.7 | JavaScript 22 +// Quality: pending | Created: 2026-06-08 + +const t = window.ANYPLOT_TOKENS; + +// --- Data: simulated CPU profile of a web API request handler -------------- +// Tree of call stacks. `self` = samples spent in the function itself (not in +// its children). A node's *total* = self + sum(children.total) and becomes the +// width of its flame-graph bar. Children sit above the parent and fill it +// left-to-right; any leftover width at depth+1 is the parent's `self` time. +const profile = { + name: "main", self: 2, children: [ + { name: "runServer", self: 6, children: [ + { name: "handleRequest", self: 8, children: [ + { name: "parseHeaders", self: 32, children: [] }, + { name: "routeRequest", self: 4, children: [ + { name: "authMiddleware", self: 18, children: [ + { name: "verifyToken", self: 14, children: [] }, + { name: "loadUser", self: 22, children: [] }, + ] }, + { name: "dispatchHandler", self: 6, children: [ + { name: "queryDB", self: 8, children: [ + { name: "openConn", self: 12, children: [] }, + { name: "executeQuery", self: 78, children: [] }, + { name: "parseRows", self: 22, children: [] }, + ] }, + { name: "renderTemplate", self: 14, children: [ + { name: "loadTemplate", self: 10, children: [] }, + { name: "compileTemplate", self: 32, children: [] }, + { name: "renderHTML", self: 24, children: [] }, + ] }, + { name: "serialize", self: 28, children: [] }, + ] }, + ] }, + { name: "writeResponse", self: 18, children: [] }, + ] }, + { name: "gcMinor", self: 20, children: [] }, + ] }, + ], +}; + +// Flatten the tree into (depth, start, end, total) frames via DFS. +const frames = []; +let maxDepth = 0; +function visit(node, depth, x) { + if (depth > maxDepth) maxDepth = depth; + let childX = x; + let kidsTotal = 0; + for (const child of node.children) { + const ct = visit(child, depth + 1, childX); + childX += ct; + kidsTotal += ct; + } + const total = node.self + kidsTotal; + frames.push({ name: node.name, depth, start: x, end: x + total, total }); + return total; +} +const totalSamples = visit(profile, 0, 0); + +// --- Warm palette (Imprint semantic exception for flame graphs) ------------ +// The spec explicitly calls for the conventional yellows-oranges-reds aesthetic +// invented by Brendan Gregg. Pick from Imprint's three warm anchors (amber, +// ochre, matte red) by hashing the function name + depth, so the same call +// site always reads the same hue across renders. +const warmAnchors = ["#DDCC77", "#BD8233", "#AE3030"]; +function warmFor(seed) { + let h = 2166136261; + for (let i = 0; i < seed.length; i++) { + h ^= seed.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return warmAnchors[(h >>> 0) % warmAnchors.length]; +} +function textOn(hex) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return lum > 0.5 ? "#1A1A17" : "#FFFDF6"; +} + +// --- Mount ----------------------------------------------------------------- +const canvas = document.createElement("canvas"); +document.getElementById("container").appendChild(canvas); + +// --- Custom flame-graph renderer ------------------------------------------ +// Chart.js core has no flame-graph type, so we run a `type: 'bar'` chart for +// its axes / title chrome and draw the per-frame rectangles ourselves in an +// `afterDatasetsDraw` plugin. y-pixel positions are computed from chartArea +// rather than the category scale so reverse / band alignment stay exact. +const flamePlugin = { + id: "flamegraph", + afterDatasetsDraw(chart) { + const { ctx, chartArea, scales: { x } } = chart; + const rows = maxDepth + 1; + const bandPx = (chartArea.bottom - chartArea.top) / rows; + const gap = 2; + const barH = Math.max(2, bandPx - gap); + + ctx.save(); + ctx.font = "600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"; + ctx.textBaseline = "middle"; + + for (const f of frames) { + const xLeft = x.getPixelForValue(f.start); + const xRight = x.getPixelForValue(f.end); + // depth 0 at the bottom band; depth N at the top. + const yCenter = chartArea.bottom - (f.depth + 0.5) * bandPx; + const top = yCenter - barH / 2; + const w = Math.max(1, xRight - xLeft - 1); + + const fill = warmFor(f.name + "·" + f.depth); + ctx.fillStyle = fill; + ctx.fillRect(xLeft, top, w, barH); + // pageBg-coloured hairline separates adjacent siblings on both themes. + ctx.strokeStyle = t.pageBg; + ctx.lineWidth = 1; + ctx.strokeRect(xLeft + 0.5, top + 0.5, w - 1, barH - 1); + + if (w >= 60) { + ctx.fillStyle = textOn(fill); + let text = f.name; + const maxText = w - 12; + if (ctx.measureText(text).width > maxText) { + while (text.length > 2 && ctx.measureText(text + "…").width > maxText) { + text = text.slice(0, -1); + } + text = text + "…"; + } + ctx.fillText(text, xLeft + 6, yCenter); + } + } + ctx.restore(); + }, +}; + +// --- Chart ----------------------------------------------------------------- +const depthLabels = Array.from({ length: maxDepth + 1 }, (_, i) => String(i)); + +new Chart(canvas, { + type: "bar", + data: { + labels: depthLabels, + datasets: [{ + label: "Stack frames", + data: depthLabels.map(() => 0), // placeholder; real bars are drawn by the plugin + backgroundColor: "rgba(0,0,0,0)", + borderColor: "rgba(0,0,0,0)", + }], + }, + options: { + indexAxis: "y", + responsive: true, + maintainAspectRatio: false, + animation: false, + layout: { padding: { left: 8, right: 18, top: 4, bottom: 8 } }, + plugins: { + title: { + display: true, + text: "flamegraph-basic · javascript · chartjs · anyplot.ai", + color: t.ink, + font: { size: 22, weight: "600" }, + padding: { top: 4, bottom: 6 }, + }, + subtitle: { + display: true, + text: "Simulated CPU profile · bar width = samples · horizontal order is arbitrary, not temporal", + color: t.inkSoft, + font: { size: 14, style: "italic" }, + padding: { bottom: 16 }, + }, + legend: { display: false }, + tooltip: { enabled: false }, + }, + scales: { + x: { + type: "linear", + min: 0, + max: totalSamples, + title: { + display: true, + text: "Samples", + color: t.ink, + font: { size: 14 }, + padding: { top: 8 }, + }, + ticks: { color: t.inkSoft, font: { size: 12 } }, + grid: { color: t.grid, drawTicks: false }, + border: { color: t.grid }, + }, + y: { + type: "category", + labels: depthLabels, + reverse: true, // depth 0 (root) at the bottom + title: { + display: true, + text: "Call stack depth (root → leaf)", + color: t.ink, + font: { size: 14 }, + }, + ticks: { color: t.inkSoft, font: { size: 12 } }, + grid: { display: false, drawTicks: false }, + border: { color: t.grid }, + }, + }, + }, + plugins: [flamePlugin], +}); From f33aea1c55c6d2be58f3106509e26a557036b54e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jun 2026 20:53:22 +0000 Subject: [PATCH 2/5] chore(chartjs): add metadata for flamegraph-basic --- .../metadata/javascript/chartjs.yaml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 plots/flamegraph-basic/metadata/javascript/chartjs.yaml diff --git a/plots/flamegraph-basic/metadata/javascript/chartjs.yaml b/plots/flamegraph-basic/metadata/javascript/chartjs.yaml new file mode 100644 index 0000000000..6cca918cee --- /dev/null +++ b/plots/flamegraph-basic/metadata/javascript/chartjs.yaml @@ -0,0 +1,21 @@ +# Per-library metadata for chartjs implementation of flamegraph-basic +# Auto-generated by impl-generate.yml + +library: chartjs +language: javascript +specification_id: flamegraph-basic +created: '2026-06-08T20:53:22Z' +updated: '2026-06-08T20:53:22Z' +generated_by: claude-opus +workflow_run: 27165720177 +issue: 4665 +language_version: 22.22.3 +library_version: 4.4.7 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-dark.html +quality_score: null +review: + strengths: [] + weaknesses: [] From 6e944fdef18afd5e708a4b8a00d1e72768bfc2b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jun 2026 20:58:32 +0000 Subject: [PATCH 3/5] chore(chartjs): update quality score 85 and review feedback for flamegraph-basic --- .../implementations/javascript/chartjs.js | 4 + .../metadata/javascript/chartjs.yaml | 276 +++++++++++++++++- 2 files changed, 273 insertions(+), 7 deletions(-) diff --git a/plots/flamegraph-basic/implementations/javascript/chartjs.js b/plots/flamegraph-basic/implementations/javascript/chartjs.js index e41755c893..8d75e267db 100644 --- a/plots/flamegraph-basic/implementations/javascript/chartjs.js +++ b/plots/flamegraph-basic/implementations/javascript/chartjs.js @@ -1,3 +1,7 @@ +// anyplot.ai +// flamegraph-basic: Flame Graph for Performance Profiling +// Library: chartjs 4.4.7 | JavaScript 22.22.3 +// Quality: 85/100 | Created: 2026-06-08 //# anyplot-orientation: landscape // anyplot.ai // flamegraph-basic: Flame Graph for Performance Profiling diff --git a/plots/flamegraph-basic/metadata/javascript/chartjs.yaml b/plots/flamegraph-basic/metadata/javascript/chartjs.yaml index 6cca918cee..fc88c086eb 100644 --- a/plots/flamegraph-basic/metadata/javascript/chartjs.yaml +++ b/plots/flamegraph-basic/metadata/javascript/chartjs.yaml @@ -1,11 +1,8 @@ -# Per-library metadata for chartjs implementation of flamegraph-basic -# Auto-generated by impl-generate.yml - library: chartjs language: javascript specification_id: flamegraph-basic created: '2026-06-08T20:53:22Z' -updated: '2026-06-08T20:53:22Z' +updated: '2026-06-08T20:58:31Z' generated_by: claude-opus workflow_run: 27165720177 issue: 4665 @@ -15,7 +12,272 @@ preview_url_light: https://storage.googleapis.com/anyplot-images/plots/flamegrap preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-dark.html -quality_score: null +quality_score: 85 review: - strengths: [] - weaknesses: [] + strengths: + - Custom Chart.js `afterDatasetsDraw` plugin cleanly renders the flame graph on + top of an invisible bar chart, reusing the library's title/scales/grid chrome + instead of building everything from scratch — genuinely idiomatic plugin pattern. + - 'Warm palette (#DDCC77, #BD8233, #AE3030) draws from Imprint''s three warm anchors + rather than inventing arbitrary yellows/oranges/reds, satisfying both the spec''s + Brendan-Gregg aesthetic and the style guide''s semantic-exception rule.' + - Data colors are identical between light and dark renders; only chrome (background, + title, ticks, labels, grid) flips — theme adaptation is correct. + - Per-frame text contrast is computed from per-bar luminance (`textOn`), so dark + labels land on amber bars and light labels land on the matte red — readable in + both themes. + - Realistic web-API request handler tree with auth middleware, DB query, and template + render stages; neutral domain, plausible sample counts (4–78 per frame). + - Bar labels truncate with an ellipsis when the bar is too narrow, and the 60 px + threshold suppresses labels on tiny frames so nothing overlaps. + - Subtitle explicitly states `bar width = samples · horizontal order is arbitrary, + not temporal`, which prevents the most common misreading of flame graphs. + - Canvas dimensions are exactly 3200×1800 landscape; layout is balanced with clean + inner padding and no clipping. + weaknesses: + - 'DQ-01: The call-stack tree contains only ~20 unique frames, well below the spec''s + stated size of 50–500 unique stack traces. Broaden the simulated profile (more + sibling branches, deeper leaf subtrees, a second top-level child of `main`) so + the flame graph demonstrates the density it is designed for.' + - 'DE-03: Frame colors are picked by hashing `name + depth`, so hue carries no signal + about which frames are hot. Convention is that a flame graph''s color is decorative, + but storytelling would improve if the warm palette was mapped to a meaningful + axis — e.g. depth, or a `self`-time threshold — so `executeQuery` (the actual + hotspot at 78 samples) reads as the focal point rather than being one red box + among several.' + - 'DE-02: The y-axis still shows numeric depth tick labels (0–6) on the left, which + duplicate information already encoded by the visible bar stacking. Consider hiding + y-tick labels (`ticks: { display: false }`) and relying on the rotated axis title + alone — would reduce chrome noise and let the bars breathe.' + - 'VQ-01 minor: Bar-internal text uses a hard-coded system font stack and the `textOn` + luminance threshold of 0.5 puts dark `#1A1A17` text on the amber `#DDCC77` bars. + Contrast is acceptable but not great; raising the threshold (e.g. 0.6) or switching + amber-bar text to a slightly darker ink would tighten legibility at the 400 px + mobile size.' + - 'CQ-01: Helper functions `visit`, `warmFor`, and `textOn` are reasonable for the + tree-flattening and color-mapping work, but `textOn` (3 lines of luminance math) + and `warmFor` (FNV-style hash) could be inlined or replaced with simpler logic + to keep the snippet flatter — the spec implementation style favors KISS over extensibility + hooks.' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white around #FAF8F1 — the correct Imprint light surface, not pure white. + Chrome: Title "flamegraph-basic · javascript · chartjs · anyplot.ai" is dark and bold at the top, clearly readable. + Subtitle "Simulated CPU profile · bar width = samples · horizontal order is arbitrary, not temporal" is in muted ink-soft italics, readable. + Y-axis title "Call stack depth (root → leaf)" rotated 90° on the left, dark ink, readable. + Y-axis tick labels (numeric depths 0–6) and X-axis tick labels (0–378 in increments of 50) are dark, readable. + X-axis title "Samples" sits centered below the plot, readable. + Grid lines are subtle vertical hairlines in the Imprint grid token color — visible but not dominant. + Data: Stacked horizontal bars layered bottom-up from "main" (depth 0, full red bar at 378 samples) up through "runServer", "handleRequest" (amber), down to leaves like "executeQuery" (ochre, 78 samples) and "renderHTML" (red, 24 samples). Colors drawn from the warm Imprint anchors #DDCC77 (amber), #BD8233 (ochre), #AE3030 (matte red). Function names appear inside each wide-enough bar; "writeR…" truncates with an ellipsis. No #009E73 brand green — this is the documented warm-palette semantic exception for flame graphs. + Legibility verdict: PASS — all chrome and bar-internal labels are clearly visible against the warm off-white background; no light-on-light failures. + + Dark render (plot-dark.png): + Background: Warm near-black around #1A1A17 — the correct Imprint dark surface, not pure black. + Chrome: Title is rendered in light Imprint ink against the dark background, clearly readable. + Subtitle is in muted light ink-soft italics, readable. + Y-axis title and tick labels in light ink, readable. X-axis title and tick labels in light ink, readable. + Grid hairlines are subtle but visible against the warm dark surface. + Data: Bar colors are IDENTICAL to the light render — same warm anchors at the same positions (main = red, runServer = ochre, handleRequest = amber, parseHeaders = red, …). Only the chrome flipped. Inside-bar text contrast is preserved: dark text on amber/ochre bars, white text on the matte-red bars (e.g. white "main", white "parseRows"). + Legibility verdict: PASS — no dark-on-dark failures; title, axis labels, ticks, and bar-internal labels are all readable; the warm bar colors remain clearly distinguishable from the warm near-black background. + criteria_checklist: + visual_quality: + score: 29 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: 'All chrome and bar-internal text readable in both themes. Minor: + dark text on amber bars is slightly thin at the 0.5 luminance threshold.' + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: Hairlines separate sibling bars cleanly. Labels truncate with ellipsis + when bars are narrow. No text/data collisions. + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Bars and labels clearly visible. Tiny frames are wisely left unlabeled. + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Warm CVD-safe palette (amber, ochre, matte red). No red-green encoding. + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Canvas exactly 3200×1800. Title ~70% width (expected for mandated + title). Balanced axis labels. No clipping. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: 'Descriptive: ''Call stack depth (root → leaf)'' and ''Samples''.' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Uses Imprint warm anchors (#DDCC77/#BD8233/#AE3030) per the documented + semantic exception for flame graphs. Backgrounds #FAF8F1 / #1A1A17 correct.' + design_excellence: + score: 12 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Warm Imprint-anchored palette and explanatory subtitle lift it above + defaults. Could go further with intentional hue-encodes-hotness. + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: true + comment: Subtle grid, hairline separators, no top/right spines. Numeric y-tick + labels are still present and redundant with the stacked bars. + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Vertical root-at-bottom hierarchy reads clearly; subtitle prevents + misreading. Color is hashed rather than encoding hotness, so the real hotspot + (executeQuery, 78 samples) is not the focal point. + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: 'Correct flame graph: horizontal bars per stack frame, bottom-up + depth layering, width ∝ samples.' + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Stack depth on y, sample width on x, function-name labels inside + wide bars, adjacent siblings with hairline separators. + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Depth → y category, total samples → x linear extent. Both axes span + the full data range (0–378). + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title exactly 'flamegraph-basic · javascript · chartjs · anyplot.ai'. + Legend hidden (single-series), labels are in-bar. + data_quality: + score: 12 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 3 + max: 6 + passed: false + comment: Tree has only ~20 unique frames; spec requires 50–500 unique stack + traces. Demonstrates the chart type but does not exercise its density. + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Web API request handler with auth, DB query, template render stages + — realistic and neutral. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Per-frame self samples 2–78, total 378 — plausible for a sampled + CPU profile. + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: true + comment: Three helper functions (visit, warmFor, textOn). The recursive visit + is justified; textOn/warmFor could be inlined. + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Hard-coded profile tree; FNV-style deterministic hash for color. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: No imports — Chart is the global per the Chart.js mount-node contract. + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Custom afterDatasetsDraw plugin is the right Chart.js escape hatch + for a non-native chart type. No fake interactivity. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: No savefig — harness owns plot-light.png/plot-dark.png and the HTML + twin. + library_features: + score: 8 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Uses Chart.js scales, options, plugin registration, and the afterDatasetsDraw + hook idiomatically. Mounts via canvas + new Chart(...) as the contract requires. + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Leverages scale.getPixelForValue and chart.chartArea to position + custom rectangles in scale space — distinctive Chart.js plugin-author technique. + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - html-export + - custom-plugin + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: + - edge-highlighting From 849bbc30ce4baf7ab69537376a9ed5727e1c5794 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jun 2026 21:04:53 +0000 Subject: [PATCH 4/5] fix(chartjs): address review feedback for flamegraph-basic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attempt 1/3 - fixes based on AI review - Expand profile tree to 54 unique frames (DQ-01: was ~20, spec requires 50-500) - Encode hotness via warm-palette tiers on self-samples (DE-03): amber <=10, ochre <=24, matte red >24 — executeQuery (92) becomes focal point - Hide numeric y-tick labels; rely on axis title alone (DE-02) - Replace FNV hash + textOn luminance helper with single tier lookup (CQ-01) --- .../implementations/javascript/chartjs.js | 142 +++++++++++------- 1 file changed, 88 insertions(+), 54 deletions(-) diff --git a/plots/flamegraph-basic/implementations/javascript/chartjs.js b/plots/flamegraph-basic/implementations/javascript/chartjs.js index 8d75e267db..e657630f11 100644 --- a/plots/flamegraph-basic/implementations/javascript/chartjs.js +++ b/plots/flamegraph-basic/implementations/javascript/chartjs.js @@ -3,10 +3,6 @@ // Library: chartjs 4.4.7 | JavaScript 22.22.3 // Quality: 85/100 | Created: 2026-06-08 //# anyplot-orientation: landscape -// anyplot.ai -// flamegraph-basic: Flame Graph for Performance Profiling -// Library: chartjs 4.4.7 | JavaScript 22 -// Quality: pending | Created: 2026-06-08 const t = window.ANYPLOT_TOKENS; @@ -16,37 +12,84 @@ const t = window.ANYPLOT_TOKENS; // width of its flame-graph bar. Children sit above the parent and fill it // left-to-right; any leftover width at depth+1 is the parent's `self` time. const profile = { - name: "main", self: 2, children: [ - { name: "runServer", self: 6, children: [ - { name: "handleRequest", self: 8, children: [ - { name: "parseHeaders", self: 32, children: [] }, - { name: "routeRequest", self: 4, children: [ - { name: "authMiddleware", self: 18, children: [ - { name: "verifyToken", self: 14, children: [] }, - { name: "loadUser", self: 22, children: [] }, + name: "main", self: 1, children: [ + { name: "runServer", self: 2, children: [ + { name: "handleRequest", self: 4, children: [ + { name: "parseHeaders", self: 3, children: [ + { name: "decodeUtf8", self: 12, children: [] }, + { name: "lowercaseKeys", self: 8, children: [] }, + { name: "splitCookies", self: 6, children: [] }, + ] }, + { name: "routeRequest", self: 2, children: [ + { name: "authMiddleware", self: 3, children: [ + { name: "verifyToken", self: 4, children: [ + { name: "parseJwt", self: 8, children: [] }, + { name: "validateSig", self: 14, children: [] }, + ] }, + { name: "loadUser", self: 3, children: [ + { name: "cacheGet", self: 4, children: [] }, + { name: "dbFetch", self: 22, children: [] }, + ] }, ] }, - { name: "dispatchHandler", self: 6, children: [ - { name: "queryDB", self: 8, children: [ - { name: "openConn", self: 12, children: [] }, - { name: "executeQuery", self: 78, children: [] }, - { name: "parseRows", self: 22, children: [] }, + { name: "rateLimitCheck", self: 2, children: [ + { name: "bucketLookup", self: 6, children: [] }, + { name: "bucketUpdate", self: 8, children: [] }, + ] }, + { name: "dispatchHandler", self: 4, children: [ + { name: "queryDB", self: 5, children: [ + { name: "openConn", self: 10, children: [] }, + { name: "executeQuery", self: 92, children: [] }, + { name: "parseRows", self: 18, children: [] }, + { name: "closeConn", self: 6, children: [] }, ] }, - { name: "renderTemplate", self: 14, children: [ + { name: "renderTemplate", self: 3, children: [ { name: "loadTemplate", self: 10, children: [] }, - { name: "compileTemplate", self: 32, children: [] }, + { name: "compileTemplate", self: 28, children: [] }, { name: "renderHTML", self: 24, children: [] }, + { name: "escapeHTML", self: 12, children: [] }, + ] }, + { name: "serialize", self: 4, children: [ + { name: "jsonStringify", self: 16, children: [] }, + { name: "gzipCompress", self: 22, children: [] }, ] }, - { name: "serialize", self: 28, children: [] }, ] }, ] }, - { name: "writeResponse", self: 18, children: [] }, + { name: "writeResponse", self: 3, children: [ + { name: "setHeaders", self: 6, children: [] }, + { name: "flushBuffer", self: 14, children: [] }, + ] }, + ] }, + { name: "gcMinor", self: 6, children: [ + { name: "markRefs", self: 10, children: [] }, + { name: "sweepHeap", self: 14, children: [] }, + ] }, + { name: "logRequest", self: 2, children: [ + { name: "formatLog", self: 4, children: [] }, + { name: "writeLog", self: 8, children: [] }, + ] }, + ] }, + { name: "backgroundJobs", self: 2, children: [ + { name: "cronTick", self: 3, children: [ + { name: "scanJobs", self: 6, children: [] }, + { name: "claimJob", self: 4, children: [] }, + ] }, + { name: "workerLoop", self: 3, children: [ + { name: "fetchJob", self: 8, children: [] }, + { name: "runJob", self: 4, children: [ + { name: "emailSend", self: 18, children: [] }, + { name: "imageResize", self: 3, children: [ + { name: "decodeImg", self: 12, children: [] }, + { name: "resampleImg", self: 26, children: [] }, + { name: "encodeImg", self: 14, children: [] }, + ] }, + { name: "dataExport", self: 18, children: [] }, + ] }, ] }, - { name: "gcMinor", self: 20, children: [] }, ] }, ], }; -// Flatten the tree into (depth, start, end, total) frames via DFS. +// Flatten the tree into (depth, start, end, total, self) frames via DFS. const frames = []; let maxDepth = 0; function visit(node, depth, x) { @@ -59,31 +102,20 @@ function visit(node, depth, x) { kidsTotal += ct; } const total = node.self + kidsTotal; - frames.push({ name: node.name, depth, start: x, end: x + total, total }); + frames.push({ name: node.name, depth, start: x, end: x + total, total, self: node.self }); return total; } const totalSamples = visit(profile, 0, 0); -// --- Warm palette (Imprint semantic exception for flame graphs) ------------ -// The spec explicitly calls for the conventional yellows-oranges-reds aesthetic -// invented by Brendan Gregg. Pick from Imprint's three warm anchors (amber, -// ochre, matte red) by hashing the function name + depth, so the same call -// site always reads the same hue across renders. -const warmAnchors = ["#DDCC77", "#BD8233", "#AE3030"]; -function warmFor(seed) { - let h = 2166136261; - for (let i = 0; i < seed.length; i++) { - h ^= seed.charCodeAt(i); - h = Math.imul(h, 16777619); - } - return warmAnchors[(h >>> 0) % warmAnchors.length]; -} -function textOn(hex) { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - return lum > 0.5 ? "#1A1A17" : "#FFFDF6"; +// --- Warm palette tiered by self-samples (hotness encoding) ---------------- +// Imprint's three warm anchors map to a self-time tier so the eye lands on +// the actual hotspot (the matte-red frame) instead of color being decorative. +// Dark text rides on the lighter amber/ochre tiers; light text on matte red. +// Thresholds: low ≤10 (cool amber), medium ≤24 (ochre), high >24 (matte red). +function colorFor(self) { + if (self > 24) return { fill: "#AE3030", text: "#FFFDF6" }; + if (self > 10) return { fill: "#BD8233", text: "#1A1A17" }; + return { fill: "#DDCC77", text: "#1A1A17" }; } // --- Mount ----------------------------------------------------------------- @@ -116,7 +148,7 @@ const flamePlugin = { const top = yCenter - barH / 2; const w = Math.max(1, xRight - xLeft - 1); - const fill = warmFor(f.name + "·" + f.depth); + const { fill, text } = colorFor(f.self); ctx.fillStyle = fill; ctx.fillRect(xLeft, top, w, barH); // pageBg-coloured hairline separates adjacent siblings on both themes. @@ -125,16 +157,16 @@ const flamePlugin = { ctx.strokeRect(xLeft + 0.5, top + 0.5, w - 1, barH - 1); if (w >= 60) { - ctx.fillStyle = textOn(fill); - let text = f.name; + ctx.fillStyle = text; + let label = f.name; const maxText = w - 12; - if (ctx.measureText(text).width > maxText) { - while (text.length > 2 && ctx.measureText(text + "…").width > maxText) { - text = text.slice(0, -1); + if (ctx.measureText(label).width > maxText) { + while (label.length > 2 && ctx.measureText(label + "…").width > maxText) { + label = label.slice(0, -1); } - text = text + "…"; + label = label + "…"; } - ctx.fillText(text, xLeft + 6, yCenter); + ctx.fillText(label, xLeft + 6, yCenter); } } ctx.restore(); @@ -171,7 +203,7 @@ new Chart(canvas, { }, subtitle: { display: true, - text: "Simulated CPU profile · bar width = samples · horizontal order is arbitrary, not temporal", + text: "Simulated CPU profile · bar width = samples · color = self-time tier · horizontal order is arbitrary, not temporal", color: t.inkSoft, font: { size: 14, style: "italic" }, padding: { bottom: 16 }, @@ -205,7 +237,9 @@ new Chart(canvas, { color: t.ink, font: { size: 14 }, }, - ticks: { color: t.inkSoft, font: { size: 12 } }, + // Hide numeric tick labels — visible bar stacking + axis title already + // convey depth; numeric ticks would just add chrome noise. + ticks: { display: false }, grid: { display: false, drawTicks: false }, border: { color: t.grid }, }, From f4e25426839a433e55feec681a59ce0af17ea0c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Jun 2026 21:11:24 +0000 Subject: [PATCH 5/5] chore(chartjs): update quality score 93 and review feedback for flamegraph-basic --- .../implementations/javascript/chartjs.js | 2 +- .../metadata/javascript/chartjs.yaml | 210 +++++++++--------- 2 files changed, 111 insertions(+), 101 deletions(-) diff --git a/plots/flamegraph-basic/implementations/javascript/chartjs.js b/plots/flamegraph-basic/implementations/javascript/chartjs.js index e657630f11..d78424888b 100644 --- a/plots/flamegraph-basic/implementations/javascript/chartjs.js +++ b/plots/flamegraph-basic/implementations/javascript/chartjs.js @@ -1,7 +1,7 @@ // anyplot.ai // flamegraph-basic: Flame Graph for Performance Profiling // Library: chartjs 4.4.7 | JavaScript 22.22.3 -// Quality: 85/100 | Created: 2026-06-08 +// Quality: 93/100 | Created: 2026-06-08 //# anyplot-orientation: landscape const t = window.ANYPLOT_TOKENS; diff --git a/plots/flamegraph-basic/metadata/javascript/chartjs.yaml b/plots/flamegraph-basic/metadata/javascript/chartjs.yaml index fc88c086eb..a662c1c185 100644 --- a/plots/flamegraph-basic/metadata/javascript/chartjs.yaml +++ b/plots/flamegraph-basic/metadata/javascript/chartjs.yaml @@ -2,7 +2,7 @@ library: chartjs language: javascript specification_id: flamegraph-basic created: '2026-06-08T20:53:22Z' -updated: '2026-06-08T20:58:31Z' +updated: '2026-06-08T21:11:24Z' generated_by: claude-opus workflow_run: 27165720177 issue: 4665 @@ -12,98 +12,99 @@ preview_url_light: https://storage.googleapis.com/anyplot-images/plots/flamegrap preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/flamegraph-basic/javascript/chartjs/plot-dark.html -quality_score: 85 +quality_score: 93 review: strengths: - - Custom Chart.js `afterDatasetsDraw` plugin cleanly renders the flame graph on - top of an invisible bar chart, reusing the library's title/scales/grid chrome - instead of building everything from scratch — genuinely idiomatic plugin pattern. - - 'Warm palette (#DDCC77, #BD8233, #AE3030) draws from Imprint''s three warm anchors - rather than inventing arbitrary yellows/oranges/reds, satisfying both the spec''s - Brendan-Gregg aesthetic and the style guide''s semantic-exception rule.' + - 'Color now meaningfully encodes self-time hotness via three Imprint warm anchors + (#DDCC77 amber ≤10, #BD8233 ochre ≤24, #AE3030 matte red >24), so the actual CPU + hotspots — executeQuery (92), compileTemplate (28), resampleImg (26), renderHTML + (24) — pop out as red focal points instead of being hashed at random.' + - 'Y-axis numeric tick labels are now hidden (`ticks: { display: false }`); the + rotated y-title ''Call stack depth (root → leaf)'' carries the meaning and the + bar stacking shows depth visually, eliminating the redundant chrome flagged in + attempt 1.' + - Call-stack tree now contains 54 unique frames (main → backgroundJobs/runServer + with auth, DB, render, gc, image-resize subtrees), comfortably inside the spec's + 50–500 range and demonstrating the density a flame graph is built for. + - 'Helper-function surface is reduced to just two: `visit` (necessary recursive + DFS for tree → frames) and `colorFor` (3-line tier threshold) — the FNV hash and + luminance threshold from attempt 1 are gone, keeping the snippet KISS.' + - 'Per-tier text contrast is explicit and correct: `#1A1A17` ink on the amber/ochre + bars, `#FFFDF6` on the matte-red bars — readable in both light and dark themes + without relying on a luminance heuristic.' + - Custom `afterDatasetsDraw` plugin idiomatically extends Chart.js with `scale.getPixelForValue` + and `chartArea` for coordinate transforms — the canonical escape hatch when Chart.js + has no native chart type for the spec. - Data colors are identical between light and dark renders; only chrome (background, - title, ticks, labels, grid) flips — theme adaptation is correct. - - Per-frame text contrast is computed from per-bar luminance (`textOn`), so dark - labels land on amber bars and light labels land on the matte red — readable in - both themes. - - Realistic web-API request handler tree with auth middleware, DB query, and template - render stages; neutral domain, plausible sample counts (4–78 per frame). - - Bar labels truncate with an ellipsis when the bar is too narrow, and the 60 px - threshold suppresses labels on tiny frames so nothing overlaps. - - Subtitle explicitly states `bar width = samples · horizontal order is arbitrary, - not temporal`, which prevents the most common misreading of flame graphs. - - Canvas dimensions are exactly 3200×1800 landscape; layout is balanced with clean - inner padding and no clipping. + title, ticks, grid, separator hairlines via `t.pageBg`) flips — theme adaptation + correct. + - Subtitle explicitly states `bar width = samples · color = self-time tier · horizontal + order is arbitrary, not temporal`, preventing the most common misreading of flame + graphs and now also documenting the new color encoding. + - Canvas is exactly 3200×1800 landscape; title spans ~70% width as expected for + the mandated long format; no clipping or overflow on either render. weaknesses: - - 'DQ-01: The call-stack tree contains only ~20 unique frames, well below the spec''s - stated size of 50–500 unique stack traces. Broaden the simulated profile (more - sibling branches, deeper leaf subtrees, a second top-level child of `main`) so - the flame graph demonstrates the density it is designed for.' - - 'DE-03: Frame colors are picked by hashing `name + depth`, so hue carries no signal - about which frames are hot. Convention is that a flame graph''s color is decorative, - but storytelling would improve if the warm palette was mapped to a meaningful - axis — e.g. depth, or a `self`-time threshold — so `executeQuery` (the actual - hotspot at 78 samples) reads as the focal point rather than being one red box - among several.' - - 'DE-02: The y-axis still shows numeric depth tick labels (0–6) on the left, which - duplicate information already encoded by the visible bar stacking. Consider hiding - y-tick labels (`ticks: { display: false }`) and relying on the rotated axis title - alone — would reduce chrome noise and let the bars breathe.' - - 'VQ-01 minor: Bar-internal text uses a hard-coded system font stack and the `textOn` - luminance threshold of 0.5 puts dark `#1A1A17` text on the amber `#DDCC77` bars. - Contrast is acceptable but not great; raising the threshold (e.g. 0.6) or switching - amber-bar text to a slightly darker ink would tighten legibility at the 400 px - mobile size.' - - 'CQ-01: Helper functions `visit`, `warmFor`, and `textOn` are reasonable for the - tree-flattening and color-mapping work, but `textOn` (3 lines of luminance math) - and `warmFor` (FNV-style hash) could be inlined or replaced with simpler logic - to keep the snippet flatter — the spec implementation style favors KISS over extensibility - hooks.' + - 'DE-01 minor: The three-tier color encoding is a clear upgrade over hashing, but + the boundaries (≤10, ≤24, >24) are coarse — frames at 22 samples (gzipCompress) + and 28 (compileTemplate) land in different tiers despite being similarly hot. + A continuous warm gradient (e.g. `t.seq` or interpolated across the three anchors) + would let the eye rank hotness more precisely instead of bucketing.' + - 'LM-01 minor: Most actual flame-graph geometry is drawn in raw `ctx.fillRect` + / `ctx.fillText` inside the plugin; Chart.js really only contributes title/subtitle/axes/grid + chrome. This is the right approach for a non-native chart type, but it means the + implementation does not exercise Chart.js''s higher-level dataset/scale features + deeply — closer to a canvas-on-Chart.js scaffold than full Chart.js mastery.' + - 'DE-02 minor: The single dataset has a placeholder `data: depthLabels.map(() => + 0)` plus invisible fill/border colors purely to keep Chart.js''s bar pipeline + happy. Functional and well-commented, but a slight hack — a non-deduction since + there''s no cleaner option in Chart.js core for this case.' image_description: |- Light render (plot-light.png): Background: Warm off-white around #FAF8F1 — the correct Imprint light surface, not pure white. - Chrome: Title "flamegraph-basic · javascript · chartjs · anyplot.ai" is dark and bold at the top, clearly readable. - Subtitle "Simulated CPU profile · bar width = samples · horizontal order is arbitrary, not temporal" is in muted ink-soft italics, readable. - Y-axis title "Call stack depth (root → leaf)" rotated 90° on the left, dark ink, readable. - Y-axis tick labels (numeric depths 0–6) and X-axis tick labels (0–378 in increments of 50) are dark, readable. - X-axis title "Samples" sits centered below the plot, readable. - Grid lines are subtle vertical hairlines in the Imprint grid token color — visible but not dominant. - Data: Stacked horizontal bars layered bottom-up from "main" (depth 0, full red bar at 378 samples) up through "runServer", "handleRequest" (amber), down to leaves like "executeQuery" (ochre, 78 samples) and "renderHTML" (red, 24 samples). Colors drawn from the warm Imprint anchors #DDCC77 (amber), #BD8233 (ochre), #AE3030 (matte red). Function names appear inside each wide-enough bar; "writeR…" truncates with an ellipsis. No #009E73 brand green — this is the documented warm-palette semantic exception for flame graphs. - Legibility verdict: PASS — all chrome and bar-internal labels are clearly visible against the warm off-white background; no light-on-light failures. + Chrome: Title "flamegraph-basic · javascript · chartjs · anyplot.ai" is bold dark ink, clearly readable. + Subtitle "Simulated CPU profile · bar width = samples · color = self-time tier · horizontal order is arbitrary, not temporal" is in muted ink-soft italic, fully readable. + Y-axis title "Call stack depth (root → leaf)" rotated 90° on the left in dark ink, readable. Numeric y-tick labels are correctly hidden (the attempt-1 weakness was fixed). + X-axis title "Samples" centered below the plot in dark ink. X-axis tick labels (0, 100, 200, 300, 400, 500, 554) are dark and readable. + Subtle vertical grid hairlines in the Imprint grid token color — visible but not dominant. + Data: Stacked horizontal bars layered bottom-up — depth 0 "main" full width (554 samples, amber), depth 1 splits "runServer" + "backgroundJobs", up through "handleRequest", "parseHeaders/routeRequest/writeResponse" etc. to leaves at depth 6/7. Colors map to self-time tiers: amber #DDCC77 for low (≤10), ochre #BD8233 for medium (≤24), matte red #AE3030 for the actual hotspots (executeQuery at 92, compileTemplate at 28, resampleImg at 26). Function names sit inside any bar wide enough (≥60 px); narrower bars truncate with an ellipsis ("dbFe…", "compil…", "rende…", "gzip…", "resam…", "parseH…", "write…") — readable, no overlap. No #009E73 brand green — documented warm-palette semantic exception for the conventional flame-graph aesthetic. + Legibility verdict: PASS — all chrome and bar-internal text is clearly visible against the warm off-white background; dark ink on amber/ochre bars is readable, light ink on matte-red bars is readable, no light-on-light failures. Dark render (plot-dark.png): Background: Warm near-black around #1A1A17 — the correct Imprint dark surface, not pure black. Chrome: Title is rendered in light Imprint ink against the dark background, clearly readable. - Subtitle is in muted light ink-soft italics, readable. - Y-axis title and tick labels in light ink, readable. X-axis title and tick labels in light ink, readable. - Grid hairlines are subtle but visible against the warm dark surface. - Data: Bar colors are IDENTICAL to the light render — same warm anchors at the same positions (main = red, runServer = ochre, handleRequest = amber, parseHeaders = red, …). Only the chrome flipped. Inside-bar text contrast is preserved: dark text on amber/ochre bars, white text on the matte-red bars (e.g. white "main", white "parseRows"). - Legibility verdict: PASS — no dark-on-dark failures; title, axis labels, ticks, and bar-internal labels are all readable; the warm bar colors remain clearly distinguishable from the warm near-black background. + Subtitle is light ink-soft italic, readable. + Y-axis title "Call stack depth (root → leaf)" in light ink, readable. Numeric y-ticks hidden (as in light). X-axis title "Samples" and X-axis tick labels in light ink, readable. + Subtle grid hairlines visible against the warm dark surface; pageBg-coloured hairlines between sibling bars are dark and tidy. + Data: Bar fill colors are IDENTICAL to the light render — same warm Imprint anchors at the same positions (main = amber, executeQuery = matte red, compileTemplate = matte red, resampleImg = matte red, etc.). Only the chrome flipped. Inside-bar text contrast preserved: dark #1A1A17 on amber/ochre bars (still readable because amber is light enough), white #FFFDF6 on the matte-red bars (e.g. white "executeQuery", "compil…", "rende…", "resam…"). + Legibility verdict: PASS — no dark-on-dark failures; title, axis title, ticks, and every visible bar-internal label are readable; the warm bar colors remain clearly distinguishable from the warm near-black background. criteria_checklist: visual_quality: - score: 29 + score: 30 max: 30 items: - id: VQ-01 name: Text Legibility - score: 7 + score: 8 max: 8 passed: true - comment: 'All chrome and bar-internal text readable in both themes. Minor: - dark text on amber bars is slightly thin at the 0.5 luminance threshold.' + comment: All chrome and bar-internal text readable in both themes. Explicit + dark-on-amber and light-on-red contrast assignments replace the attempt-1 + luminance heuristic, removing the previous minor contrast concern. - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: Hairlines separate sibling bars cleanly. Labels truncate with ellipsis - when bars are narrow. No text/data collisions. + comment: 1 px pageBg hairlines separate sibling bars cleanly. Labels truncate + with ellipsis when bars are narrow and are suppressed entirely below 60 + px width. No text/data collisions. - id: VQ-03 name: Element Visibility score: 6 max: 6 passed: true - comment: Bars and labels clearly visible. Tiny frames are wisely left unlabeled. + comment: Bars and labels clearly visible. Tiny frames (depth 6/7) are wisely + left unlabeled rather than being squashed into illegible text. - id: VQ-04 name: Color Accessibility score: 2 @@ -115,8 +116,8 @@ review: score: 4 max: 4 passed: true - comment: Canvas exactly 3200×1800. Title ~70% width (expected for mandated - title). Balanced axis labels. No clipping. + comment: Canvas exactly 3200×1800 landscape. Title ~70% width (expected for + mandated long title). Balanced axis labels, no clipping or overflow. - id: VQ-06 name: Axis Labels & Title score: 2 @@ -129,33 +130,35 @@ review: max: 2 passed: true comment: 'Uses Imprint warm anchors (#DDCC77/#BD8233/#AE3030) per the documented - semantic exception for flame graphs. Backgrounds #FAF8F1 / #1A1A17 correct.' + semantic exception for conventional flame-graph aesthetic. Backgrounds #FAF8F1 + / #1A1A17 correct on both renders.' design_excellence: - score: 12 + score: 17 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 5 + score: 7 max: 8 passed: true - comment: Warm Imprint-anchored palette and explanatory subtitle lift it above - defaults. Could go further with intentional hue-encodes-hotness. + comment: Color now meaningfully encodes self-time hotness (3 tiers) rather + than being decorative, drawing the eye to executeQuery and other real hotspots. + Slight room for a continuous gradient over discrete tiers. - id: DE-02 name: Visual Refinement - score: 3 + score: 5 max: 6 passed: true - comment: Subtle grid, hairline separators, no top/right spines. Numeric y-tick - labels are still present and redundant with the stacked bars. + comment: Y-tick labels hidden (attempt-1 fix), subtle x-grid, hairline separators, + no top/right spines, generous padding. - id: DE-03 name: Data Storytelling - score: 4 + score: 5 max: 6 passed: true - comment: Vertical root-at-bottom hierarchy reads clearly; subtitle prevents - misreading. Color is hashed rather than encoding hotness, so the real hotspot - (executeQuery, 78 samples) is not the focal point. + comment: Vertical root-at-bottom hierarchy reads clearly; subtitle calls out + 'color = self-time tier'; matte-red executeQuery sits as the visual focal + point — the actual hottest frame. spec_compliance: score: 15 max: 15 @@ -166,83 +169,88 @@ review: max: 5 passed: true comment: 'Correct flame graph: horizontal bars per stack frame, bottom-up - depth layering, width ∝ samples.' + depth layering, width proportional to samples.' - id: SC-02 name: Required Features score: 4 max: 4 passed: true comment: Stack depth on y, sample width on x, function-name labels inside - wide bars, adjacent siblings with hairline separators. + wide bars (with ellipsis truncation), adjacent siblings with minimal hairline + gaps. - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: Depth → y category, total samples → x linear extent. Both axes span - the full data range (0–378). + comment: Depth → y category, total samples → x linear extent. X axis spans + full data range (0–554). - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true comment: Title exactly 'flamegraph-basic · javascript · chartjs · anyplot.ai'. - Legend hidden (single-series), labels are in-bar. + Legend correctly hidden (labels are in-bar). data_quality: - score: 12 + score: 15 max: 15 items: - id: DQ-01 name: Feature Coverage - score: 3 + score: 6 max: 6 - passed: false - comment: Tree has only ~20 unique frames; spec requires 50–500 unique stack - traces. Demonstrates the chart type but does not exercise its density. + passed: true + comment: 'Tree now has 54 unique stack frames (was ~20 in attempt 1) — comfortably + inside spec''s 50–500 range. Multiple deep branches: auth, DB query, template + render, image resize, GC, logging, background workers.' - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Web API request handler with auth, DB query, template render stages - — realistic and neutral. + comment: Web API request handler with auth middleware, rate limit, DB query/template + render/serialize stages, plus background jobs (cron, worker loop, image + resize) — realistic and neutral. - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Per-frame self samples 2–78, total 378 — plausible for a sampled + comment: Per-frame self samples 1–92, total 554 — plausible for a sampled CPU profile. code_quality: - score: 9 + score: 10 max: 10 items: - id: CQ-01 name: KISS Structure - score: 2 + score: 3 max: 3 passed: true - comment: Three helper functions (visit, warmFor, textOn). The recursive visit - is justified; textOn/warmFor could be inlined. + comment: Only two helpers (visit DFS + 3-line colorFor) plus the flamegraph + plugin object. The FNV hash and luminance threshold from attempt 1 are gone. - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Hard-coded profile tree; FNV-style deterministic hash for color. + comment: Hard-coded profile tree; no RNG needed. - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: No imports — Chart is the global per the Chart.js mount-node contract. + comment: No imports — Chart is the harness-provided global per the Chart.js + mount-node contract. - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true comment: Custom afterDatasetsDraw plugin is the right Chart.js escape hatch - for a non-native chart type. No fake interactivity. + for a non-native chart type. Comments document why (no fake interactivity, + no plugin imports). - id: CQ-05 name: Output & API score: 1 @@ -259,8 +267,10 @@ review: score: 4 max: 5 passed: true - comment: Uses Chart.js scales, options, plugin registration, and the afterDatasetsDraw - hook idiomatically. Mounts via canvas + new Chart(...) as the contract requires. + comment: Uses Chart.js scales, options, registered title/subtitle plugins, + and the afterDatasetsDraw hook idiomatically. Most rendering is in raw ctx + calls inside the plugin (necessary for a non-native type), so slightly less + reliance on Chart.js's higher-level dataset/scale plumbing. - id: LM-02 name: Distinctive Features score: 4