Skip to content

Commit f837dc1

Browse files
feat(highcharts): implement roc-curve (#11609)
## Implementation: `roc-curve` - javascript/highcharts Implements the **javascript/highcharts** version of `roc-curve`. **File:** `plots/roc-curve/implementations/javascript/highcharts.js` **Parent Issue:** #2273 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/33967490382)* --------- 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 7f8d0c9 commit f837dc1

2 files changed

Lines changed: 429 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// anyplot.ai
2+
// roc-curve: ROC Curve with AUC
3+
// Library: highcharts 12.6.0 | JavaScript 22.23.2
4+
// Quality: 91/100 | Created: 2026-09-05
5+
6+
//# anyplot-orientation: square
7+
const t = window.ANYPLOT_TOKENS;
8+
9+
// --- Data (in-memory, deterministic) ----------------------------------------
10+
// Two diagnostic tests for detecting a disease, evaluated across classifier
11+
// thresholds. tpr = fpr^(1/power) is concave and bows toward the top-left
12+
// corner for power > 1, mimicking a real ROC curve shape.
13+
const numPoints = 101;
14+
const falsePositiveRates = Array.from({ length: numPoints }, (_, i) => i / (numPoints - 1));
15+
16+
const rocCurve = (power) => falsePositiveRates.map((fpr) => Math.pow(fpr, 1 / power));
17+
18+
const areaUnderCurve = (tprValues) => {
19+
let area = 0;
20+
for (let i = 1; i < falsePositiveRates.length; i += 1) {
21+
const dx = falsePositiveRates[i] - falsePositiveRates[i - 1];
22+
area += (dx * (tprValues[i] + tprValues[i - 1])) / 2;
23+
}
24+
return area;
25+
};
26+
27+
const antibodyTestTpr = rocCurve(9);
28+
const enzymeTestTpr = rocCurve(4);
29+
const antibodyTestAuc = areaUnderCurve(antibodyTestTpr);
30+
const enzymeTestAuc = areaUnderCurve(enzymeTestTpr);
31+
32+
const toPoints = (tprValues) =>
33+
falsePositiveRates.map((fpr, i) => [fpr, tprValues[i]]);
34+
35+
// Example decision threshold called out on the best-performing curve, so the
36+
// chart tells a concrete "if you accept 10% false positives, you get this
37+
// sensitivity" story instead of stopping at the raw curve shapes.
38+
const thresholdIndex = 10; // falsePositiveRates[10] === 0.10 exactly
39+
const thresholdFpr = falsePositiveRates[thresholdIndex];
40+
const thresholdTpr = antibodyTestTpr[thresholdIndex];
41+
42+
// --- Chart -------------------------------------------------------------------
43+
Highcharts.chart("container", {
44+
chart: {
45+
type: "line",
46+
backgroundColor: "transparent",
47+
animation: false,
48+
style: { fontFamily: "inherit" },
49+
},
50+
credits: { enabled: false },
51+
colors: t.palette,
52+
title: {
53+
text: "roc-curve · javascript · highcharts · anyplot.ai",
54+
style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
55+
},
56+
xAxis: {
57+
title: { text: "False Positive Rate", style: { color: t.inkSoft, fontSize: "16px" } },
58+
min: 0,
59+
max: 1,
60+
tickInterval: 0.2,
61+
minorTickInterval: 0.1,
62+
lineColor: t.inkSoft,
63+
tickColor: t.inkSoft,
64+
gridLineColor: t.grid,
65+
gridLineWidth: 1,
66+
minorGridLineColor: t.grid,
67+
minorGridLineWidth: 1,
68+
minorGridLineDashStyle: "Dot",
69+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
70+
plotLines: [
71+
{
72+
value: thresholdFpr,
73+
color: t.inkSoft,
74+
dashStyle: "ShortDash",
75+
width: 1.5,
76+
zIndex: 5,
77+
label: {
78+
text: `FPR = ${thresholdFpr.toFixed(2)}`,
79+
style: { color: t.inkSoft, fontSize: "12px" },
80+
align: "left",
81+
x: 6,
82+
y: 14,
83+
},
84+
},
85+
],
86+
},
87+
yAxis: {
88+
title: { text: "True Positive Rate", style: { color: t.inkSoft, fontSize: "16px" } },
89+
min: 0,
90+
max: 1,
91+
tickInterval: 0.2,
92+
minorTickInterval: 0.1,
93+
gridLineColor: t.grid,
94+
minorGridLineColor: t.grid,
95+
minorGridLineWidth: 1,
96+
minorGridLineDashStyle: "Dot",
97+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
98+
},
99+
legend: {
100+
itemStyle: { color: t.inkSoft, fontSize: "14px" },
101+
itemHoverStyle: { color: t.ink },
102+
verticalAlign: "bottom",
103+
},
104+
tooltip: {
105+
valueDecimals: 2,
106+
},
107+
plotOptions: {
108+
series: { animation: false, marker: { enabled: false } },
109+
},
110+
series: [
111+
{
112+
// Area fill turns "AUC" from a legend number into a visible shape: the
113+
// filled region under the top curve literally is the area it names.
114+
type: "area",
115+
name: `Antibody test (AUC = ${antibodyTestAuc.toFixed(2)})`,
116+
data: toPoints(antibodyTestTpr),
117+
color: t.palette[0],
118+
fillOpacity: 0.08,
119+
threshold: 0,
120+
lineWidth: 3,
121+
},
122+
{
123+
name: `Enzyme test (AUC = ${enzymeTestAuc.toFixed(2)})`,
124+
data: toPoints(enzymeTestTpr),
125+
color: t.palette[1],
126+
lineWidth: 3,
127+
},
128+
{
129+
name: "Random classifier (AUC = 0.50)",
130+
data: [
131+
[0, 0],
132+
[1, 1],
133+
],
134+
color: t.ink,
135+
dashStyle: "Dash",
136+
lineWidth: 2,
137+
enableMouseTracking: false,
138+
},
139+
{
140+
// Callout marker for the example operating threshold above.
141+
type: "scatter",
142+
name: "Example threshold",
143+
data: [[thresholdFpr, thresholdTpr]],
144+
color: t.palette[0],
145+
marker: { enabled: true, radius: 6, lineWidth: 2, lineColor: t.ink },
146+
dataLabels: {
147+
enabled: true,
148+
format: `TPR = ${thresholdTpr.toFixed(2)}`,
149+
style: { color: t.ink, fontSize: "12px", fontWeight: "600", textOutline: "none" },
150+
y: -14,
151+
},
152+
showInLegend: false,
153+
enableMouseTracking: false,
154+
},
155+
],
156+
});

0 commit comments

Comments
 (0)