Skip to content

Commit b75ee16

Browse files
feat(highcharts): implement ecdf-basic (#9490)
## Implementation: `ecdf-basic` - javascript/highcharts Implements the **javascript/highcharts** version of `ecdf-basic`. **File:** `plots/ecdf-basic/implementations/javascript/highcharts.js` **Parent Issue:** #976 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/28160912734)* --------- 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 8ced086 commit b75ee16

2 files changed

Lines changed: 454 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// anyplot.ai
2+
// ecdf-basic: Basic ECDF Plot
3+
// Library: highcharts 12.6.0 | JavaScript 22.23.0
4+
// Quality: 87/100 | Created: 2026-06-25
5+
6+
//# anyplot-orientation: landscape
7+
8+
const t = window.ANYPLOT_TOKENS;
9+
10+
// Deterministic LCG random number generator
11+
function lcg(seed) {
12+
let s = seed >>> 0;
13+
return () => {
14+
s = (Math.imul(1664525, s) + 1013904223) >>> 0;
15+
return s / 4294967296;
16+
};
17+
}
18+
19+
// Box-Muller transform — normally distributed samples
20+
function normalSamples(rng, mu, sigma, n) {
21+
const out = [];
22+
while (out.length < n) {
23+
const u1 = Math.max(rng(), 1e-10);
24+
const u2 = rng();
25+
const mag = sigma * Math.sqrt(-2 * Math.log(u1));
26+
out.push(mu + mag * Math.cos(2 * Math.PI * u2));
27+
if (out.length < n) out.push(mu + mag * Math.sin(2 * Math.PI * u2));
28+
}
29+
return out;
30+
}
31+
32+
// ECDF as [x, proportion] step-function points (right-continuous convention)
33+
// startX is a virtual point at proportion=0 before the first data value
34+
function ecdfSeries(data, startX) {
35+
const sorted = [...data].sort((a, b) => a - b);
36+
const n = sorted.length;
37+
const pts = [[startX, 0]];
38+
sorted.forEach((x, i) => pts.push([x, (i + 1) / n]));
39+
return pts;
40+
}
41+
42+
// Plant height (cm) under two growing conditions — 100 plants each
43+
const standard = normalSamples(lcg(42), 15.0, 3.0, 100);
44+
const enhanced = normalSamples(lcg(137), 18.5, 2.5, 100);
45+
46+
const allX = [...standard, ...enhanced];
47+
const xMin = Math.min(...allX);
48+
const xMax = Math.max(...allX);
49+
const startX = xMin - (xMax - xMin) * 0.04;
50+
51+
const seriesStandard = ecdfSeries(standard, startX);
52+
const seriesEnhanced = ecdfSeries(enhanced, startX);
53+
54+
// Actual sample medians for annotation (average of 50th and 51st values in 100-item sorted array)
55+
const sortedStd = [...standard].sort((a, b) => a - b);
56+
const sortedEnh = [...enhanced].sort((a, b) => a - b);
57+
const medStd = (sortedStd[49] + sortedStd[50]) / 2;
58+
const medEnh = (sortedEnh[49] + sortedEnh[50]) / 2;
59+
60+
// Title fits within 67 chars — use default 22px
61+
const titleText = 'Plant Heights · ecdf-basic · javascript · highcharts · anyplot.ai';
62+
63+
Highcharts.chart('container', {
64+
chart: {
65+
type: 'area',
66+
backgroundColor: 'transparent',
67+
animation: false,
68+
style: { fontFamily: 'inherit' },
69+
},
70+
credits: { enabled: false },
71+
colors: t.palette,
72+
title: {
73+
text: titleText,
74+
style: { color: t.ink, fontSize: '22px', fontWeight: '600' },
75+
},
76+
subtitle: {
77+
text: `Enhanced nutrients shift median height from ${medStd.toFixed(1)} cm to ${medEnh.toFixed(1)} cm`,
78+
style: { color: t.inkSoft, fontSize: '14px' },
79+
},
80+
xAxis: {
81+
title: {
82+
text: 'Plant Height (cm)',
83+
style: { color: t.inkSoft, fontSize: '16px' },
84+
},
85+
lineColor: t.inkSoft,
86+
tickColor: t.inkSoft,
87+
gridLineColor: t.grid,
88+
labels: { style: { color: t.inkSoft, fontSize: '14px' } },
89+
plotLines: [
90+
{
91+
value: medStd,
92+
color: t.palette[0],
93+
dashStyle: 'Dash',
94+
width: 1.5,
95+
zIndex: 3,
96+
label: {
97+
text: `median ${medStd.toFixed(1)} cm`,
98+
style: { color: t.palette[0], fontSize: '11px' },
99+
rotation: 0,
100+
y: 16,
101+
x: 4,
102+
},
103+
},
104+
{
105+
value: medEnh,
106+
color: t.palette[1],
107+
dashStyle: 'Dash',
108+
width: 1.5,
109+
zIndex: 3,
110+
label: {
111+
text: `median ${medEnh.toFixed(1)} cm`,
112+
style: { color: t.palette[1], fontSize: '11px' },
113+
rotation: 0,
114+
y: 16,
115+
x: 4,
116+
},
117+
},
118+
],
119+
},
120+
yAxis: {
121+
title: {
122+
text: 'Cumulative Proportion',
123+
style: { color: t.inkSoft, fontSize: '16px' },
124+
},
125+
min: 0,
126+
max: 1,
127+
tickInterval: 0.25,
128+
gridLineColor: t.grid,
129+
lineColor: t.inkSoft,
130+
tickColor: t.inkSoft,
131+
labels: {
132+
style: { color: t.inkSoft, fontSize: '14px' },
133+
formatter() { return `${Math.round(this.value * 100)}%`; },
134+
},
135+
plotLines: [
136+
{
137+
value: 0.5,
138+
color: t.inkSoft,
139+
dashStyle: 'ShortDash',
140+
width: 1,
141+
zIndex: 2,
142+
label: {
143+
text: '50th percentile',
144+
style: { color: t.inkSoft, fontSize: '11px' },
145+
align: 'right',
146+
x: -4,
147+
y: -4,
148+
},
149+
},
150+
],
151+
},
152+
legend: {
153+
enabled: true,
154+
itemStyle: { color: t.inkSoft, fontSize: '14px' },
155+
itemHoverStyle: { color: t.ink },
156+
},
157+
plotOptions: {
158+
series: {
159+
animation: false,
160+
step: 'right',
161+
marker: { enabled: false },
162+
},
163+
},
164+
series: [
165+
{
166+
name: 'Standard Nutrients',
167+
data: seriesStandard,
168+
color: t.palette[0],
169+
lineWidth: 2.5,
170+
fillOpacity: 0.08,
171+
},
172+
{
173+
name: 'Enhanced Nutrients',
174+
data: seriesEnhanced,
175+
color: t.palette[1],
176+
lineWidth: 3.0,
177+
fillOpacity: 0.08,
178+
},
179+
],
180+
});

0 commit comments

Comments
 (0)