Skip to content

Commit 3c72cef

Browse files
feat(chartjs): implement roc-curve (#11603)
## Implementation: `roc-curve` - javascript/chartjs Implements the **javascript/chartjs** version of `roc-curve`. **File:** `plots/roc-curve/implementations/javascript/chartjs.js` **Parent Issue:** #2273 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/33967019081)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent a75ef1f commit 3c72cef

2 files changed

Lines changed: 387 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// anyplot.ai
2+
// roc-curve: ROC Curve with AUC
3+
// Library: chartjs 4.4.7 | JavaScript 22.23.2
4+
// Quality: 86/100 | Created: 2026-09-05
5+
6+
//# anyplot-orientation: square
7+
const t = window.ANYPLOT_TOKENS;
8+
9+
// --- Reproducible PRNG (LCG) + Box-Muller normal sampler --------------------
10+
function makeLcg(seed) {
11+
let state = seed;
12+
return () => {
13+
state = (state * 1664525 + 1013904223) % 4294967296;
14+
return state / 4294967296;
15+
};
16+
}
17+
function sampleNormal(rand) {
18+
const u1 = Math.max(rand(), 1e-9);
19+
const u2 = rand();
20+
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
21+
}
22+
23+
// --- Data: synthetic classifier scores for a tumor-malignancy screen -------
24+
// 220 benign cases, 220 malignant cases; two candidate classifiers scored on
25+
// the same cohort with different separability between the score distributions.
26+
const N_PER_CLASS = 220;
27+
const rand = makeLcg(42);
28+
const labels = [];
29+
const deepModelScores = [];
30+
const baselineModelScores = [];
31+
for (let i = 0; i < N_PER_CLASS; i++) {
32+
labels.push(0);
33+
deepModelScores.push(sampleNormal(rand) * 1.0 + 0.0);
34+
baselineModelScores.push(sampleNormal(rand) * 1.0 + 0.0);
35+
}
36+
for (let i = 0; i < N_PER_CLASS; i++) {
37+
labels.push(1);
38+
deepModelScores.push(sampleNormal(rand) * 1.0 + 2.1);
39+
baselineModelScores.push(sampleNormal(rand) * 1.0 + 1.0);
40+
}
41+
42+
// --- ROC curve + AUC (trapezoidal rule) -------------------------------------
43+
function computeRoc(scores, classLabels) {
44+
const positives = classLabels.reduce((sum, l) => sum + l, 0);
45+
const negatives = classLabels.length - positives;
46+
const order = scores
47+
.map((score, i) => i)
48+
.sort((a, b) => scores[b] - scores[a]);
49+
50+
const points = [{ x: 0, y: 0 }];
51+
let truePositives = 0;
52+
let falsePositives = 0;
53+
for (const i of order) {
54+
if (classLabels[i] === 1) truePositives++;
55+
else falsePositives++;
56+
points.push({ x: falsePositives / negatives, y: truePositives / positives });
57+
}
58+
59+
let auc = 0;
60+
for (let i = 1; i < points.length; i++) {
61+
const dx = points[i].x - points[i - 1].x;
62+
auc += (dx * (points[i].y + points[i - 1].y)) / 2;
63+
}
64+
return { points, auc };
65+
}
66+
67+
const deepRoc = computeRoc(deepModelScores, labels);
68+
const baselineRoc = computeRoc(baselineModelScores, labels);
69+
70+
// --- Mount -------------------------------------------------------------------
71+
const canvas = document.createElement("canvas");
72+
document.getElementById("container").appendChild(canvas);
73+
74+
// --- Chart ---------------------------------------------------------------
75+
new Chart(canvas, {
76+
type: "line",
77+
data: {
78+
datasets: [
79+
{
80+
label: `Deep Ensemble (AUC = ${deepRoc.auc.toFixed(2)})`,
81+
data: deepRoc.points,
82+
borderColor: t.palette[0],
83+
backgroundColor: t.palette[0],
84+
borderWidth: 4,
85+
pointRadius: 0,
86+
fill: false,
87+
tension: 0,
88+
},
89+
{
90+
label: `Logistic Baseline (AUC = ${baselineRoc.auc.toFixed(2)})`,
91+
data: baselineRoc.points,
92+
borderColor: t.palette[1],
93+
backgroundColor: t.palette[1],
94+
borderWidth: 3,
95+
borderDash: [10, 6],
96+
pointRadius: 0,
97+
fill: false,
98+
tension: 0,
99+
},
100+
{
101+
label: "Random Classifier",
102+
data: [
103+
{ x: 0, y: 0 },
104+
{ x: 1, y: 1 },
105+
],
106+
borderColor: t.inkSoft,
107+
backgroundColor: t.inkSoft,
108+
borderWidth: 2,
109+
borderDash: [4, 4],
110+
pointRadius: 0,
111+
fill: false,
112+
tension: 0,
113+
},
114+
],
115+
},
116+
options: {
117+
responsive: true,
118+
maintainAspectRatio: false,
119+
animation: false,
120+
aspectRatio: 1,
121+
plugins: {
122+
title: {
123+
display: true,
124+
text: "roc-curve · javascript · chartjs · anyplot.ai",
125+
color: t.ink,
126+
font: { size: 26 },
127+
padding: { bottom: 24 },
128+
},
129+
legend: {
130+
position: "bottom",
131+
labels: { color: t.ink, font: { size: 18 }, boxWidth: 28, padding: 20 },
132+
},
133+
},
134+
scales: {
135+
x: {
136+
type: "linear",
137+
min: 0,
138+
max: 1,
139+
ticks: { color: t.inkSoft, font: { size: 15 }, stepSize: 0.2 },
140+
grid: { color: t.grid },
141+
title: { display: true, text: "False Positive Rate", color: t.ink, font: { size: 18 } },
142+
},
143+
y: {
144+
type: "linear",
145+
min: 0,
146+
max: 1,
147+
ticks: { color: t.inkSoft, font: { size: 15 }, stepSize: 0.2 },
148+
grid: { color: t.grid },
149+
title: { display: true, text: "True Positive Rate", color: t.ink, font: { size: 18 } },
150+
},
151+
},
152+
},
153+
});
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
library: chartjs
2+
language: javascript
3+
specification_id: roc-curve
4+
created: '2026-09-05T12:51:09Z'
5+
updated: '2026-09-05T13:04:05Z'
6+
generated_by: claude-sonnet
7+
workflow_run: 33967019081
8+
issue: 2273
9+
language_version: 22.23.2
10+
library_version: 4.4.7
11+
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/chartjs/plot-light.png
12+
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/chartjs/plot-dark.png
13+
preview_html_light: https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/chartjs/plot-light.html
14+
preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/chartjs/plot-dark.html
15+
quality_score: 86
16+
review:
17+
strengths:
18+
- 'Both Attempt-1 issues verifiably fixed: reference line now uses t.inkSoft (pixel-confirmed
19+
(74,74,68) light / (184,183,176) dark, both with strong contrast against background),
20+
and second series now uses canonical t.palette[1] (#C475FD) instead of skipping
21+
to palette[2]'
22+
- First series correctly uses brand green t.palette[0] (#009E73)
23+
- Distinct line styles (solid vs. dashed) give redundant encoding beyond color
24+
- AUC values embedded directly in the legend labels
25+
- Explicit font sizing throughout (title 26px, axis titles 18px, ticks 15px, legend
26+
18px), no overlap
27+
- 'Square 2400x2400 canvas is an apt choice given the spec''s ''equal aspect ratio
28+
preferred'' note, with aspectRatio: 1 enforced'
29+
- Deterministic seeded LCG + Box-Muller data generation gives a reproducible, plausible
30+
medical-screening scenario (AUC 0.92 vs 0.79)
31+
weaknesses:
32+
- 'Design Excellence remains fairly conventional: a correct, clean, but fairly standard
33+
two-line ROC plot. A subtle visual-hierarchy touch (e.g., light fill under the
34+
leading curve, or an annotated optimal-threshold point) would lift DE-01/DE-03.'
35+
- 'Library Mastery is generic: nothing distinctively Chart.js (e.g., no custom tooltip
36+
callback formatting FPR/TPR/threshold, no plugin-based annotation for the optimal
37+
operating point). A small Chart.js-specific touch would raise LM-02.'
38+
image_description: |-
39+
Light render (plot-light.png):
40+
Background: Warm off-white, pixel-confirmed (250,248,241) ~ #FAF8F1, matches spec.
41+
Chrome: Bold dark title "roc-curve · javascript · chartjs · anyplot.ai" at top, clearly readable. Axis titles "False Positive Rate" / "True Positive Rate" in medium-dark ink, tick labels (0.0-1.0, step 0.2) in softer gray, subtle gridlines on both axes. Legend below plot with three entries, all readable.
42+
Data: Thick solid brand-green curve (Deep Ensemble, AUC=0.92, palette[0] #009E73) bows sharply toward top-left. Dashed lavender curve (Logistic Baseline, AUC=0.79, palette[1] #C475FD) bows more gradually. Diagonal "Random Classifier" reference line now renders in dark gray (inkSoft #4A4A44, pixel-confirmed (74,74,68)), clearly distinguishable from background.
43+
Legibility verdict: PASS
44+
45+
Dark render (plot-dark.png):
46+
Background: Warm near-black, pixel-confirmed (26,26,23) ~ #1A1A17, matches spec.
47+
Chrome: Title, axis titles, tick labels, and legend text all correctly flip to light-colored ink. No dark-on-dark text failures.
48+
Data: Green and lavender curves pixel-identical in hue to light render (data-color consistency confirmed). Reference line now renders in light gray (inkSoft #B8B7B0, pixel-confirmed (184,183,176)) against (26,26,23) background - strong, easily perceptible contrast. This fixes the Attempt-1 failure where the line used the undefined t.muted token and was effectively invisible (~(58,58,54) vs (26,26,23)).
49+
Legibility verdict: PASS
50+
criteria_checklist:
51+
visual_quality:
52+
score: 29
53+
max: 30
54+
items:
55+
- id: VQ-01
56+
name: Text Legibility
57+
score: 7
58+
max: 8
59+
passed: true
60+
comment: Explicit font sizes throughout, well-proportioned, readable in both
61+
themes
62+
- id: VQ-02
63+
name: No Overlap
64+
score: 6
65+
max: 6
66+
passed: true
67+
comment: No overlap in either render
68+
- id: VQ-03
69+
name: Element Visibility
70+
score: 6
71+
max: 6
72+
passed: true
73+
comment: Reference line now renders at t.inkSoft with strong contrast in both
74+
themes (fixed from Attempt 1)
75+
- id: VQ-04
76+
name: Color Accessibility
77+
score: 2
78+
max: 2
79+
passed: true
80+
comment: Green vs lavender + solid/dashed gives redundant encoding
81+
- id: VQ-05
82+
name: Layout & Canvas
83+
score: 4
84+
max: 4
85+
passed: true
86+
comment: Well-balanced square canvas, good margins
87+
- id: VQ-06
88+
name: Axis Labels & Title
89+
score: 2
90+
max: 2
91+
passed: true
92+
comment: Descriptive axis titles
93+
- id: VQ-07
94+
name: Palette Compliance
95+
score: 2
96+
max: 2
97+
passed: true
98+
comment: 'First series #009E73, second series now correctly palette[1] (#C475FD);
99+
chrome theme-correct in both renders'
100+
design_excellence:
101+
score: 13
102+
max: 20
103+
items:
104+
- id: DE-01
105+
name: Aesthetic Sophistication
106+
score: 5
107+
max: 8
108+
passed: false
109+
comment: AUC-in-legend + distinct linestyles show design thought, still fairly
110+
conventional
111+
- id: DE-02
112+
name: Visual Refinement
113+
score: 4
114+
max: 6
115+
passed: false
116+
comment: Subtle grid/whitespace; the previously-broken reference line now
117+
renders correctly as an unobtrusive muted guide
118+
- id: DE-03
119+
name: Data Storytelling
120+
score: 4
121+
max: 6
122+
passed: true
123+
comment: Solid/thick vs dashed/thin creates clear visual hierarchy toward
124+
the better model
125+
spec_compliance:
126+
score: 15
127+
max: 15
128+
items:
129+
- id: SC-01
130+
name: Plot Type
131+
score: 5
132+
max: 5
133+
passed: true
134+
comment: Correct ROC-curve line chart
135+
- id: SC-02
136+
name: Required Features
137+
score: 4
138+
max: 4
139+
passed: true
140+
comment: Diagonal y=x reference line present and now reliably visible in both
141+
themes
142+
- id: SC-03
143+
name: Data Mapping
144+
score: 3
145+
max: 3
146+
passed: true
147+
comment: FPR on x, TPR on y, full 0-1 range
148+
- id: SC-04
149+
name: Title & Legend
150+
score: 3
151+
max: 3
152+
passed: true
153+
comment: Title format exact; legend labels correct with AUC values
154+
data_quality:
155+
score: 14
156+
max: 15
157+
items:
158+
- id: DQ-01
159+
name: Feature Coverage
160+
score: 5
161+
max: 6
162+
passed: true
163+
comment: Two classifiers with clearly different separability
164+
- id: DQ-02
165+
name: Realistic Context
166+
score: 5
167+
max: 5
168+
passed: true
169+
comment: Neutral medical-screening scenario
170+
- id: DQ-03
171+
name: Appropriate Scale
172+
score: 4
173+
max: 4
174+
passed: true
175+
comment: Plausible score distributions and resulting AUC values
176+
code_quality:
177+
score: 9
178+
max: 10
179+
items:
180+
- id: CQ-01
181+
name: KISS Structure
182+
score: 2
183+
max: 3
184+
passed: true
185+
comment: A few small helper functions (LCG, Box-Muller, ROC/AUC), reasonably
186+
necessary for JS
187+
- id: CQ-02
188+
name: Reproducibility
189+
score: 2
190+
max: 2
191+
passed: true
192+
comment: Deterministic seeded LCG (seed=42)
193+
- id: CQ-03
194+
name: Clean Imports
195+
score: 2
196+
max: 2
197+
passed: true
198+
comment: No unused imports
199+
- id: CQ-04
200+
name: Code Elegance
201+
score: 2
202+
max: 2
203+
passed: true
204+
comment: No fake functionality, appropriately complex
205+
- id: CQ-05
206+
name: Output & API
207+
score: 1
208+
max: 1
209+
passed: true
210+
comment: 'Correct mount-node contract, animation: false set'
211+
library_mastery:
212+
score: 6
213+
max: 10
214+
items:
215+
- id: LM-01
216+
name: Idiomatic Usage
217+
score: 4
218+
max: 5
219+
passed: true
220+
comment: Idiomatic Chart.js line-chart config with typed linear scales
221+
- id: LM-02
222+
name: Distinctive Features
223+
score: 2
224+
max: 5
225+
passed: false
226+
comment: Mostly generic line-chart usage; no Chart.js-specific feature leveraged
227+
verdict: APPROVED
228+
impl_tags:
229+
dependencies: []
230+
techniques: []
231+
patterns:
232+
- data-generation
233+
dataprep: []
234+
styling: []

0 commit comments

Comments
 (0)