|
| 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