Skip to content

Commit c248257

Browse files
feat(muix): implement scatter-categorical (#11623)
## Implementation: `scatter-categorical` - javascript/muix Implements the **javascript/muix** version of `scatter-categorical`. **File:** `plots/scatter-categorical/implementations/javascript/muix.tsx` **Parent Issue:** #2597 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/33969250408)* --------- 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 8697a8f commit c248257

2 files changed

Lines changed: 419 additions & 0 deletions

File tree

  • plots/scatter-categorical
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// anyplot.ai
2+
// scatter-categorical: Categorical Scatter Plot
3+
// Library: muix 7.29.1 | JavaScript 22.23.2
4+
// Quality: 86/100 | Created: 2026-09-05
5+
//# anyplot-orientation: landscape
6+
// anyplot.ai
7+
// scatter-categorical: Categorical Scatter Plot
8+
// Library: MUI X Charts | React | Node 22
9+
// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.
10+
// Quality: pending | Created: 2026-09-05
11+
import { ScatterChart } from "@mui/x-charts/ScatterChart";
12+
13+
const t = window.ANYPLOT_TOKENS;
14+
const { width, height } = window.ANYPLOT_SIZE;
15+
16+
// Deterministic LCG (seed 42) — no Math.random() in the browser harness
17+
let seed = 42;
18+
function rng() {
19+
seed = (1664525 * seed + 1013904223) >>> 0;
20+
return seed / 4294967296;
21+
}
22+
function randn() {
23+
const u = Math.max(rng(), 1e-9);
24+
const v = rng();
25+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
26+
}
27+
28+
// --- Data: penguin body measurements by species — flipper length vs body
29+
// mass, three overlapping-but-separable clusters (a classic categorical
30+
// scatter scenario). ---------------------------------------------------------
31+
const SPECIES = [
32+
{
33+
name: "Adelie",
34+
flipperMean: 190,
35+
flipperSd: 6.5,
36+
massMean: 3700,
37+
massSd: 460,
38+
n: 40,
39+
},
40+
{
41+
name: "Chinstrap",
42+
flipperMean: 196,
43+
flipperSd: 7.2,
44+
massMean: 3730,
45+
massSd: 380,
46+
n: 34,
47+
},
48+
{
49+
name: "Gentoo",
50+
flipperMean: 217,
51+
flipperSd: 6.5,
52+
massMean: 5090,
53+
massSd: 500,
54+
n: 38,
55+
},
56+
];
57+
58+
let pointId = 0;
59+
const series = SPECIES.map((sp, i) => ({
60+
id: sp.name,
61+
label: sp.name,
62+
color: t.palette[i],
63+
markerSize: 7,
64+
// Fade the other two species when one is hovered, showcasing MUI X's
65+
// built-in cross-series highlight/fade interaction (purely additive —
66+
// has no effect on the static screenshot).
67+
highlightScope: { highlight: "series", fade: "global" },
68+
data: Array.from({ length: sp.n }, () => ({
69+
x: Math.round(sp.flipperMean + randn() * sp.flipperSd),
70+
// Body mass in kg (not g) — keeps y tick labels short ("3.7", not
71+
// "3,700"), which avoids the label colliding with the axis title (MUI X
72+
// offsets the y-axis title from a fixed tick-fontsize heuristic, not the
73+
// tick label's actual rendered width).
74+
y: Math.round(sp.massMean + randn() * sp.massSd) / 1000,
75+
id: pointId++,
76+
})),
77+
}));
78+
79+
const allPoints = series.flatMap((s) => s.data);
80+
const xs = allPoints.map((p) => p.x);
81+
const ys = allPoints.map((p) => p.y);
82+
const xPad = (Math.max(...xs) - Math.min(...xs)) * 0.1;
83+
const yPad = (Math.max(...ys) - Math.min(...ys)) * 0.1;
84+
const X_MIN = Math.min(...xs) - xPad;
85+
const X_MAX = Math.max(...xs) + xPad;
86+
const Y_MIN = Math.min(...ys) - yPad;
87+
const Y_MAX = Math.max(...ys) + yPad;
88+
89+
const TITLE = "scatter-categorical · javascript · muix · anyplot.ai";
90+
const MARGIN = { top: 90, right: 210, bottom: 90, left: 120 };
91+
const Y_LABEL_X = 36;
92+
const Y_LABEL_Y = MARGIN.top + (height - MARGIN.top - MARGIN.bottom) / 2;
93+
94+
// --- Chart (default-exported component — the harness mounts it) -------------
95+
export default function Chart() {
96+
return (
97+
<ScatterChart
98+
width={width}
99+
height={height}
100+
margin={MARGIN}
101+
series={series}
102+
skipAnimation
103+
grid={{ vertical: true, horizontal: true }}
104+
// Scatter markers are the only <circle> elements this chart renders
105+
// (legend swatches are <rect>s) — a page-background-colored edge
106+
// stroke plus slight fill transparency keeps overlapping Adelie/
107+
// Chinstrap points individually distinguishable instead of merging
108+
// into solid blobs, in both themes.
109+
sx={{
110+
"& circle": {
111+
stroke: t.pageBg,
112+
strokeWidth: 1.5,
113+
fillOpacity: 0.82,
114+
},
115+
}}
116+
xAxis={[
117+
{
118+
min: X_MIN,
119+
max: X_MAX,
120+
label: "Flipper Length (mm)",
121+
labelStyle: { fontSize: 16, fill: t.ink, fontWeight: 500 },
122+
tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
123+
stroke: t.inkSoft,
124+
},
125+
]}
126+
yAxis={[
127+
{
128+
min: Y_MIN,
129+
max: Y_MAX,
130+
valueFormatter: (v) => v.toFixed(1),
131+
tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
132+
stroke: t.inkSoft,
133+
},
134+
]}
135+
legend={{
136+
position: { vertical: "middle", horizontal: "right" },
137+
direction: "column",
138+
labelStyle: { fontSize: 15, fill: t.ink },
139+
itemMarkWidth: 14,
140+
itemMarkHeight: 14,
141+
markGap: 8,
142+
itemGap: 16,
143+
}}
144+
>
145+
<text
146+
x={width / 2}
147+
y={48}
148+
textAnchor="middle"
149+
fontSize={26}
150+
fontWeight={600}
151+
fill={t.ink}
152+
>
153+
{TITLE}
154+
</text>
155+
{/* Manually placed y-axis title (independent of MUI X's built-in
156+
label offset, which is sized off tickFontSize rather than the tick
157+
labels' actual rendered width) — guarantees no collision with the
158+
tick numbers regardless of their digit count. */}
159+
<text
160+
x={Y_LABEL_X}
161+
y={Y_LABEL_Y}
162+
textAnchor="middle"
163+
dominantBaseline="central"
164+
fontSize={16}
165+
fontWeight={500}
166+
fill={t.ink}
167+
transform={`rotate(-90, ${Y_LABEL_X}, ${Y_LABEL_Y})`}
168+
>
169+
Body Mass (kg)
170+
</text>
171+
</ScatterChart>
172+
);
173+
}

0 commit comments

Comments
 (0)