Skip to content

Commit f578efd

Browse files
feat(highcharts): implement swarm-basic (#9941)
## Implementation: `swarm-basic` - javascript/highcharts Implements the **javascript/highcharts** version of `swarm-basic`. **File:** `plots/swarm-basic/implementations/javascript/highcharts.js` **Parent Issue:** #974 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/30192249117)* --------- 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 b4dc4ea commit f578efd

2 files changed

Lines changed: 414 additions & 0 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// anyplot.ai
2+
// swarm-basic: Basic Swarm Plot
3+
// Library: highcharts 12.6.0 | JavaScript 22.23.1
4+
// Quality: 91/100 | Created: 2026-07-26
5+
6+
const t = window.ANYPLOT_TOKENS;
7+
8+
// --- Deterministic PRNG (mulberry32) + Box-Muller normal -------------------
9+
function mulberry32(seed) {
10+
return function () {
11+
seed |= 0;
12+
seed = (seed + 0x6d2b79f5) | 0;
13+
let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
14+
x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
15+
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
16+
};
17+
}
18+
19+
const rand = mulberry32(42);
20+
function randNormal(mean, sd) {
21+
const u1 = Math.max(rand(), 1e-9);
22+
const u2 = rand();
23+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
24+
return mean + z * sd;
25+
}
26+
27+
// --- Data: CRP biomarker levels (mg/L) across treatment groups -------------
28+
const groups = [
29+
{ name: "Placebo", mean: 8.2, sd: 2.4, n: 42 },
30+
{ name: "Low Dose", mean: 6.5, sd: 2.1, n: 40 },
31+
{ name: "Med Dose", mean: 4.8, sd: 1.9, n: 40 },
32+
{ name: "High Dose", mean: 3.1, sd: 1.6, n: 38 },
33+
];
34+
const categories = groups.map((g) => g.name);
35+
36+
groups.forEach((g) => {
37+
g.values = Array.from({ length: g.n }, () => Math.max(0.2, randNormal(g.mean, g.sd)));
38+
g.sampleMean = g.values.reduce((a, b) => a + b, 0) / g.values.length;
39+
});
40+
41+
// --- Beeswarm layout: bin points by value, fan them out from center-out ----
42+
// Also reports the densest bin size so denser groups can render more
43+
// transparent, easing within-bin overlap where points stack up the most.
44+
function beeswarmOffsets(values, binWidth, gap) {
45+
const order = values.map((v, i) => i).sort((a, b) => values[a] - values[b]);
46+
const offsets = new Array(values.length).fill(0);
47+
let bin = [];
48+
let binStart = null;
49+
let maxBinSize = 0;
50+
51+
const flushBin = () => {
52+
maxBinSize = Math.max(maxBinSize, bin.length);
53+
bin.forEach((idx, k) => {
54+
const side = k % 2 === 0 ? 1 : -1;
55+
const rank = Math.ceil((k + 1) / 2);
56+
offsets[idx] = side * rank * gap;
57+
});
58+
bin = [];
59+
};
60+
61+
order.forEach((i) => {
62+
const v = values[i];
63+
if (binStart === null || v - binStart > binWidth) {
64+
flushBin();
65+
binStart = v;
66+
}
67+
bin.push(i);
68+
});
69+
flushBin();
70+
71+
return { offsets, maxBinSize };
72+
}
73+
74+
const BIN_WIDTH = 0.5; // mg/L — points within this range share a swarm bin
75+
const SWARM_GAP = 0.045; // category-axis units between adjacent swarm points
76+
77+
groups.forEach((g, i) => {
78+
const { offsets, maxBinSize } = beeswarmOffsets(g.values, BIN_WIDTH, SWARM_GAP);
79+
g.points = g.values.map((v, k) => ({ x: i + offsets[k], y: Number(v.toFixed(2)) }));
80+
g.maxBinSize = maxBinSize;
81+
});
82+
83+
// Denser groups (bigger densest-bin stacks) render more transparent so
84+
// overlapping points stay individually readable; sparser groups stay opaque.
85+
const densestBin = Math.max(...groups.map((g) => g.maxBinSize));
86+
const sparsestBin = Math.min(...groups.map((g) => g.maxBinSize));
87+
const binRange = densestBin - sparsestBin || 1;
88+
groups.forEach((g) => {
89+
const density = (g.maxBinSize - sparsestBin) / binRange;
90+
g.opacity = 0.88 - density * 0.28; // 0.88 (sparsest) down to 0.60 (densest)
91+
});
92+
93+
// --- Chart -------------------------------------------------------------------
94+
Highcharts.chart("container", {
95+
chart: {
96+
type: "scatter",
97+
backgroundColor: "transparent",
98+
animation: false,
99+
style: { fontFamily: "inherit" },
100+
},
101+
credits: { enabled: false },
102+
colors: t.palette,
103+
title: {
104+
text: "swarm-basic · javascript · highcharts · anyplot.ai",
105+
style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
106+
},
107+
xAxis: {
108+
categories,
109+
min: -0.6,
110+
max: categories.length - 1 + 0.6,
111+
tickPositions: categories.map((_, i) => i),
112+
lineWidth: 0,
113+
tickColor: t.inkSoft,
114+
gridLineWidth: 0,
115+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
116+
title: { text: "Treatment Group", style: { color: t.inkSoft, fontSize: "16px" } },
117+
},
118+
yAxis: {
119+
title: { text: "CRP Level (mg/L)", style: { color: t.inkSoft, fontSize: "16px" } },
120+
lineWidth: 0,
121+
gridLineColor: t.grid,
122+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
123+
},
124+
legend: {
125+
itemStyle: { color: t.inkSoft, fontSize: "14px" },
126+
itemHoverStyle: { color: t.ink },
127+
},
128+
plotOptions: {
129+
series: { animation: false },
130+
scatter: { marker: { radius: 5, lineWidth: 0 } },
131+
},
132+
series: [
133+
...groups.map((g, i) => ({
134+
name: g.name,
135+
type: "scatter",
136+
color: t.palette[i],
137+
data: g.points,
138+
opacity: g.opacity,
139+
showInLegend: false,
140+
})),
141+
{
142+
name: "Group mean",
143+
type: "scatter",
144+
color: t.ink,
145+
zIndex: 5,
146+
data: groups.map((g, i) => ({ x: i, y: Number(g.sampleMean.toFixed(2)) })),
147+
marker: { symbol: "diamond", radius: 9, lineColor: t.pageBg, lineWidth: 3 },
148+
dataLabels: {
149+
enabled: true,
150+
format: "{point.y:.1f}",
151+
y: -16,
152+
style: {
153+
color: t.ink,
154+
fontSize: "13px",
155+
fontWeight: "600",
156+
textOutline: "2px " + t.pageBg,
157+
},
158+
},
159+
showInLegend: true,
160+
},
161+
],
162+
});

0 commit comments

Comments
 (0)