Skip to content

Commit 69716d8

Browse files
feat(highcharts): implement cat-box-strip (#11176)
## Implementation: `cat-box-strip` - javascript/highcharts Implements the **javascript/highcharts** version of `cat-box-strip`. **File:** `plots/cat-box-strip/implementations/javascript/highcharts.js` **Parent Issue:** #2695 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/33644704672)* --------- 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 f2c639a commit 69716d8

2 files changed

Lines changed: 448 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// anyplot.ai
2+
// cat-box-strip: Box Plot with Strip Overlay
3+
// Library: highcharts 12.6.0 | JavaScript 22.23.2
4+
// Quality: 91/100 | Created: 2026-09-02
5+
6+
//# anyplot-orientation: landscape
7+
const t = window.ANYPLOT_TOKENS;
8+
9+
// --- Data (in-memory, deterministic) ----------------------------------------
10+
// Commute time (minutes) by transportation mode. Sample sizes differ across
11+
// groups on purpose (comparing groups with sample size awareness).
12+
function lcg(seed) {
13+
let state = seed;
14+
return function () {
15+
state = (state * 1664525 + 1013904223) % 4294967296;
16+
return state / 4294967296;
17+
};
18+
}
19+
const rand = lcg(42);
20+
21+
function randNormal() {
22+
const u1 = Math.max(rand(), 1e-9);
23+
const u2 = rand();
24+
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
25+
}
26+
27+
function quantile(sorted, q) {
28+
const pos = (sorted.length - 1) * q;
29+
const base = Math.floor(pos);
30+
const rest = pos - base;
31+
return sorted[base + 1] !== undefined
32+
? sorted[base] + rest * (sorted[base + 1] - sorted[base])
33+
: sorted[base];
34+
}
35+
36+
const groups = [
37+
{ name: "Car", n: 80, mean: 28, std: 6 },
38+
{ name: "Bus", n: 65, mean: 42, std: 10 },
39+
{ name: "Bike", n: 50, mean: 22, std: 5 },
40+
{ name: "Walk", n: 35, mean: 48, std: 8 },
41+
];
42+
const categories = groups.map((g) => g.name);
43+
44+
const samples = groups.map((g) => {
45+
const values = [];
46+
for (let i = 0; i < g.n; i++) {
47+
values.push(Math.max(4, Math.round(g.mean + g.std * randNormal())));
48+
}
49+
return values;
50+
});
51+
// A couple of delayed-bus outliers, kept alongside the full box context.
52+
samples[1].push(95, 91);
53+
54+
const stats = samples.map((values) => {
55+
const sorted = [...values].sort((a, b) => a - b);
56+
const q1 = quantile(sorted, 0.25);
57+
const median = quantile(sorted, 0.5);
58+
const q3 = quantile(sorted, 0.75);
59+
const iqr = q3 - q1;
60+
const lowerFence = q1 - 1.5 * iqr;
61+
const upperFence = q3 + 1.5 * iqr;
62+
const inFence = sorted.filter((v) => v >= lowerFence && v <= upperFence);
63+
return {
64+
q1,
65+
median,
66+
q3,
67+
whiskerLow: inFence[0],
68+
whiskerHigh: inFence[inFence.length - 1],
69+
};
70+
});
71+
72+
// --- Box + whiskers, drawn with the core SVG renderer -----------------------
73+
// highcharts-more (boxplot/errorbar series) isn't loaded in this bundle, so
74+
// the box and whiskers are built from primitive shapes anchored to real axis
75+
// coordinates, using the actual quartile/whisker statistics computed above.
76+
function drawBoxes(chart) {
77+
const xAxis = chart.xAxis[0];
78+
const yAxis = chart.yAxis[0];
79+
const categoryPx = xAxis.toPixels(1) - xAxis.toPixels(0);
80+
const halfWidth = categoryPx * 0.22;
81+
82+
stats.forEach((s, i) => {
83+
const color = t.palette[i % t.palette.length];
84+
const cx = xAxis.toPixels(i);
85+
const q1Y = yAxis.toPixels(s.q1);
86+
const q3Y = yAxis.toPixels(s.q3);
87+
const medianY = yAxis.toPixels(s.median);
88+
const lowY = yAxis.toPixels(s.whiskerLow);
89+
const highY = yAxis.toPixels(s.whiskerHigh);
90+
91+
chart.renderer
92+
.path(["M", cx, highY, "L", cx, q3Y])
93+
.attr({ "stroke-width": 2, stroke: t.inkSoft, zIndex: 2 })
94+
.add();
95+
chart.renderer
96+
.path(["M", cx, q1Y, "L", cx, lowY])
97+
.attr({ "stroke-width": 2, stroke: t.inkSoft, zIndex: 2 })
98+
.add();
99+
chart.renderer
100+
.path(["M", cx - halfWidth, highY, "L", cx + halfWidth, highY])
101+
.attr({ "stroke-width": 2, stroke: t.inkSoft, zIndex: 2 })
102+
.add();
103+
chart.renderer
104+
.path(["M", cx - halfWidth, lowY, "L", cx + halfWidth, lowY])
105+
.attr({ "stroke-width": 2, stroke: t.inkSoft, zIndex: 2 })
106+
.add();
107+
chart.renderer
108+
.rect(cx - halfWidth, Math.min(q1Y, q3Y), halfWidth * 2, Math.abs(q1Y - q3Y))
109+
.attr({
110+
fill: color,
111+
"fill-opacity": 0.16,
112+
stroke: color,
113+
"stroke-width": 2.5,
114+
zIndex: 3,
115+
})
116+
.add();
117+
chart.renderer
118+
.path(["M", cx - halfWidth, medianY, "L", cx + halfWidth, medianY])
119+
.attr({ "stroke-width": 3, stroke: color, zIndex: 4 })
120+
.add();
121+
});
122+
}
123+
124+
// --- Strip overlay (jittered scatter, drawn above the box) ------------------
125+
const stripSeries = samples.map((values, i) => {
126+
const color = t.palette[i % t.palette.length];
127+
return {
128+
type: "scatter",
129+
name: categories[i],
130+
data: values.map((v) => [i + (rand() - 0.5) * 0.64, v]),
131+
color: color,
132+
marker: {
133+
radius: 4,
134+
lineWidth: 1,
135+
lineColor: t.pageBg,
136+
fillColor: Highcharts.color(color).setOpacity(0.55).get("rgba"),
137+
},
138+
zIndex: 5,
139+
showInLegend: false,
140+
states: { hover: { enabled: false } },
141+
};
142+
});
143+
144+
// --- Chart --------------------------------------------------------------------
145+
Highcharts.chart("container", {
146+
chart: {
147+
type: "scatter",
148+
backgroundColor: "transparent",
149+
animation: false,
150+
style: { fontFamily: "inherit" },
151+
events: {
152+
load: function () {
153+
drawBoxes(this);
154+
},
155+
},
156+
},
157+
credits: { enabled: false },
158+
colors: t.palette,
159+
title: {
160+
text: "cat-box-strip · javascript · highcharts · anyplot.ai",
161+
style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
162+
},
163+
xAxis: {
164+
categories: categories,
165+
min: -0.5,
166+
max: categories.length - 0.5,
167+
tickPositions: categories.map((_, i) => i),
168+
lineColor: t.inkSoft,
169+
tickColor: t.inkSoft,
170+
gridLineColor: t.grid,
171+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
172+
title: {
173+
text: "Transportation Mode",
174+
style: { color: t.inkSoft, fontSize: "16px" },
175+
},
176+
},
177+
yAxis: {
178+
title: {
179+
text: "Commute Time (minutes)",
180+
style: { color: t.inkSoft, fontSize: "16px" },
181+
},
182+
gridLineColor: t.grid,
183+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
184+
},
185+
legend: { enabled: false },
186+
tooltip: {
187+
pointFormat: "Commute: <b>{point.y:.0f} min</b>",
188+
},
189+
plotOptions: {
190+
series: { animation: false },
191+
},
192+
series: stripSeries,
193+
});

0 commit comments

Comments
 (0)