Skip to content

Commit 104084a

Browse files
Merge branch 'main' into implementation/point-basic/makie
2 parents d224801 + 0382424 commit 104084a

22 files changed

Lines changed: 4541 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// anyplot.ai
2+
// pdp-basic: Partial Dependence Plot
3+
// Library: muix 7.29.1 | JavaScript 22.23.2
4+
// Quality: 97/100 | Created: 2026-09-05
5+
import { LineChart } from "@mui/x-charts/LineChart";
6+
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
7+
import Box from "@mui/material/Box";
8+
import Typography from "@mui/material/Typography";
9+
10+
const t = window.ANYPLOT_TOKENS;
11+
const muted = window.ANYPLOT_THEME === "dark" ? "#A8A79F" : "#6B6A63";
12+
13+
// --- Data (in-memory, deterministic) ---------------------------------------
14+
// Tiny fixed-seed LCG — the browser has no seeded RNG.
15+
function makeLcg(seed: number) {
16+
let state = seed >>> 0;
17+
return function next() {
18+
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
19+
return state / 4294967296;
20+
};
21+
}
22+
23+
const rng = makeLcg(20260905);
24+
const GRID_POINTS = 61;
25+
const SPEND_MIN = 5;
26+
const SPEND_MAX = 65;
27+
28+
// Simulated PartialDependenceDisplay output for a GradientBoostingRegressor
29+
// predicting weekly units sold from weekly marketing spend, averaging over
30+
// every other feature in the model.
31+
const spend = Array.from(
32+
{ length: GRID_POINTS },
33+
(_, i) => SPEND_MIN + (i * (SPEND_MAX - SPEND_MIN)) / (GRID_POINTS - 1),
34+
);
35+
36+
const rawPrediction = spend.map((x) => {
37+
const saturating = 620 / (1 + Math.exp(-(x - 32) / 7));
38+
const modelWiggle = (rng() - 0.5) * 16;
39+
return saturating + modelWiggle;
40+
});
41+
42+
// Center at zero so the curve reads as "effect relative to the average
43+
// prediction" rather than an absolute (and arbitrary-looking) sales count.
44+
const meanPrediction =
45+
rawPrediction.reduce((sum, v) => sum + v, 0) / rawPrediction.length;
46+
const partialDependence = rawPrediction.map((v) => v - meanPrediction);
47+
48+
// Confidence band widens toward both ends of the spend range, where training
49+
// samples are sparser and the model's average prediction is less certain.
50+
const ciHalfWidth = spend.map((x) => 9 + 0.5 * Math.abs(x - 32));
51+
const ciLowerBound = partialDependence.map((v, i) => v - ciHalfWidth[i]);
52+
const ciBandWidth = ciHalfWidth.map((halfWidth) => 2 * halfWidth);
53+
54+
// A handful of individual conditional expectation (ICE) curves — the
55+
// per-instance predictions the PDP curve is the average of. Each sample
56+
// varies the saturation midpoint/amplitude and carries its own model noise,
57+
// then is shifted by the same meanPrediction constant as the PDP so it reads
58+
// in the same "effect relative to average" units.
59+
const ICE_SAMPLE_COUNT = 8;
60+
const iceCurves = Array.from({ length: ICE_SAMPLE_COUNT }, () => {
61+
const midpointShift = (rng() - 0.5) * 16;
62+
const amplitudeScale = 0.82 + rng() * 0.36;
63+
return spend.map((x) => {
64+
const saturating =
65+
(620 * amplitudeScale) /
66+
(1 + Math.exp(-(x - (32 + midpointShift)) / 7));
67+
const modelWiggle = (rng() - 0.5) * 12;
68+
return saturating + modelWiggle - meanPrediction;
69+
});
70+
});
71+
72+
// --- Chart (default-exported component — the harness mounts it) -------------
73+
export default function Chart() {
74+
const W = window.ANYPLOT_SIZE.width;
75+
const H = window.ANYPLOT_SIZE.height;
76+
const CHART_TOP = 64;
77+
78+
const title = "pdp-basic · javascript · muix · anyplot.ai";
79+
const titleSize =
80+
title.length > 67 ? Math.round((22 * 67) / title.length) : 22;
81+
82+
// MUI X's built-in y-axis title sits at a fixed, small offset from the
83+
// axis line — too small to clear wide 4-digit tick numbers, so it renders
84+
// the axis label as its own rotated element in a reserved strip instead.
85+
const Y_LABEL_W = 44;
86+
const yAxisLabel = "Partial dependence (Δ units sold/week)";
87+
88+
return (
89+
<Box sx={{ position: "relative", width: W, height: H, bgcolor: t.pageBg }}>
90+
<Box sx={{ position: "absolute", top: 20, left: 56, right: 56 }}>
91+
<Typography sx={{ color: t.ink, fontSize: titleSize, fontWeight: 500 }}>
92+
{title}
93+
</Typography>
94+
</Box>
95+
<Box
96+
sx={{
97+
position: "absolute",
98+
top: CHART_TOP,
99+
left: 0,
100+
width: Y_LABEL_W,
101+
bottom: 0,
102+
display: "flex",
103+
alignItems: "center",
104+
justifyContent: "center",
105+
}}
106+
>
107+
<Typography
108+
sx={{
109+
color: t.ink,
110+
fontSize: 16,
111+
whiteSpace: "nowrap",
112+
transform: "rotate(-90deg)",
113+
}}
114+
>
115+
{yAxisLabel}
116+
</Typography>
117+
</Box>
118+
<Box
119+
sx={{
120+
position: "absolute",
121+
top: CHART_TOP,
122+
left: Y_LABEL_W,
123+
right: 0,
124+
bottom: 0,
125+
}}
126+
>
127+
<LineChart
128+
width={W - Y_LABEL_W}
129+
height={H - CHART_TOP}
130+
skipAnimation
131+
series={[
132+
...iceCurves.map((curve, i) => ({
133+
id: `ice-${i}`,
134+
data: curve,
135+
color: t.palette[0],
136+
curve: "monotoneX" as const,
137+
area: false,
138+
showMark: false,
139+
valueFormatter: () => null,
140+
})),
141+
{
142+
id: "pdp",
143+
data: partialDependence,
144+
label: "Partial dependence",
145+
color: t.palette[0],
146+
curve: "monotoneX",
147+
area: false,
148+
showMark: ({ index }: { index: number }) => index % 6 === 0,
149+
valueFormatter: (value: number | null) =>
150+
value == null
151+
? null
152+
: `${value >= 0 ? "+" : ""}${value.toFixed(0)} units/week`,
153+
},
154+
{
155+
id: "ci-lower",
156+
data: ciLowerBound,
157+
color: muted,
158+
curve: "monotoneX",
159+
area: true,
160+
stack: "ci",
161+
showMark: false,
162+
valueFormatter: () => null,
163+
},
164+
{
165+
id: "ci-band",
166+
data: ciBandWidth,
167+
label: "95% confidence interval",
168+
color: muted,
169+
curve: "monotoneX",
170+
area: true,
171+
stack: "ci",
172+
showMark: false,
173+
valueFormatter: (value: number | null) =>
174+
value == null ? null : ${(value / 2).toFixed(0)} units/week`,
175+
},
176+
]}
177+
xAxis={[
178+
{
179+
data: spend,
180+
scaleType: "linear",
181+
label: "Weekly marketing spend ($1,000s)",
182+
labelStyle: { fontSize: 16 },
183+
tickLabelStyle: { fontSize: 14 },
184+
valueFormatter: (value: number) => `$${value.toFixed(0)}k`,
185+
},
186+
]}
187+
yAxis={[
188+
{
189+
tickLabelStyle: { fontSize: 14 },
190+
},
191+
]}
192+
grid={{ horizontal: true }}
193+
slotProps={{ legend: { labelStyle: { fontSize: 14 } } }}
194+
sx={{
195+
"& .MuiLineElement-series-pdp": { strokeWidth: 3.5 },
196+
"& .MuiLineElement-series-ci-band": { strokeWidth: 0 },
197+
"& .MuiLineElement-series-ci-lower": { strokeWidth: 0 },
198+
"& .MuiAreaElement-series-ci-lower": { fill: "none" },
199+
"& .MuiAreaElement-series-ci-band": { fillOpacity: 0.22 },
200+
...Object.fromEntries(
201+
iceCurves.map((_, i) => [
202+
`& .MuiLineElement-series-ice-${i}`,
203+
{ strokeWidth: 1.1, strokeOpacity: 0.22 },
204+
]),
205+
),
206+
}}
207+
>
208+
<ChartsReferenceLine
209+
y={0}
210+
label="average prediction"
211+
labelAlign="end"
212+
labelStyle={{ fontSize: 13, fill: muted }}
213+
lineStyle={{ stroke: muted, strokeDasharray: "6 4" }}
214+
/>
215+
</LineChart>
216+
</Box>
217+
</Box>
218+
);
219+
}

0 commit comments

Comments
 (0)