Skip to content

Commit c118014

Browse files
feat(muix): implement residual-plot (#11601)
## Implementation: `residual-plot` - javascript/muix Implements the **javascript/muix** version of `residual-plot`. **File:** `plots/residual-plot/implementations/javascript/muix.tsx` **Parent Issue:** #2332 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/33966466905)* --------- 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 b289fad commit c118014

2 files changed

Lines changed: 420 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// anyplot.ai
2+
// residual-plot: Residual Plot
3+
// Library: muix 7.29.1 | JavaScript 22.23.2
4+
// Quality: 87/100 | Created: 2026-09-05
5+
import { ScatterChart } from "@mui/x-charts/ScatterChart";
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+
12+
// --- Data (in-memory, deterministic LCG) ------------------------------------
13+
// A simple linear regression predicting house price from square footage, with
14+
// noise that widens for larger homes — a classic heteroscedastic pattern a
15+
// residual plot is designed to surface.
16+
let seed = 42;
17+
function nextRandom() {
18+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
19+
return seed / 0x7fffffff;
20+
}
21+
function gaussian() {
22+
const u1 = Math.max(nextRandom(), 1e-9);
23+
const u2 = nextRandom();
24+
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
25+
}
26+
27+
const HOME_COUNT = 220;
28+
const squareFootage = Array.from(
29+
{ length: HOME_COUNT },
30+
() => 600 + nextRandom() * 2900,
31+
);
32+
const housePrices = squareFootage.map((sqft) => {
33+
const noise = gaussian() * (8000 + sqft * 18);
34+
return 42000 + sqft * 118 + noise;
35+
});
36+
37+
// Ordinary least squares fit: price = intercept + slope * sqft
38+
const meanSqft = squareFootage.reduce((sum, v) => sum + v, 0) / HOME_COUNT;
39+
const meanPrice = housePrices.reduce((sum, v) => sum + v, 0) / HOME_COUNT;
40+
let covariance = 0;
41+
let variance = 0;
42+
for (let i = 0; i < HOME_COUNT; i++) {
43+
covariance += (squareFootage[i] - meanSqft) * (housePrices[i] - meanPrice);
44+
variance += (squareFootage[i] - meanSqft) ** 2;
45+
}
46+
const slope = covariance / variance;
47+
const intercept = meanPrice - slope * meanSqft;
48+
49+
const fittedValues = squareFootage.map((sqft) => intercept + slope * sqft);
50+
const residuals = housePrices.map((price, i) => price - fittedValues[i]);
51+
52+
const residualMean = residuals.reduce((sum, r) => sum + r, 0) / HOME_COUNT;
53+
const residualStd = Math.sqrt(
54+
residuals.reduce((sum, r) => sum + (r - residualMean) ** 2, 0) /
55+
(HOME_COUNT - 1),
56+
);
57+
const upperBand = 2 * residualStd;
58+
const lowerBand = -2 * residualStd;
59+
60+
// Points beyond ±2 standard deviations get a semantic-red accent — they are
61+
// the leverage points / outliers a reviewer checks first.
62+
const withinBand = [];
63+
const outliers = [];
64+
fittedValues.forEach((fitted, i) => {
65+
const residual = residuals[i];
66+
const point = { x: fitted, y: residual, id: i };
67+
if (residual > upperBand || residual < lowerBand) {
68+
outliers.push(point);
69+
} else {
70+
withinBand.push(point);
71+
}
72+
});
73+
74+
const TITLE_HEIGHT = 66;
75+
76+
// --- Chart (default-exported component — the harness mounts it) ------------
77+
export default function Chart() {
78+
const { width, height } = window.ANYPLOT_SIZE;
79+
80+
return (
81+
<Box
82+
sx={{
83+
width,
84+
height,
85+
display: "flex",
86+
flexDirection: "column",
87+
paddingTop: "20px",
88+
}}
89+
>
90+
<Typography
91+
sx={{
92+
color: t.ink,
93+
fontSize: 26,
94+
fontWeight: 600,
95+
textAlign: "center",
96+
lineHeight: 1.2,
97+
}}
98+
>
99+
residual-plot · javascript · muix · anyplot.ai
100+
</Typography>
101+
<ScatterChart
102+
width={width}
103+
height={height - TITLE_HEIGHT}
104+
skipAnimation
105+
series={[
106+
{
107+
id: "residuals",
108+
data: withinBand,
109+
label: "Residuals",
110+
markerSize: 7,
111+
color: "rgba(0, 158, 115, 0.55)",
112+
},
113+
{
114+
id: "outliers",
115+
data: outliers,
116+
label: "Outliers (|residual| > 2σ)",
117+
markerSize: 11,
118+
color: "rgba(174, 48, 48, 0.85)",
119+
},
120+
]}
121+
xAxis={[
122+
{
123+
label: "Fitted Price ($)",
124+
labelStyle: { fontSize: 16, fill: t.ink },
125+
tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
126+
},
127+
]}
128+
yAxis={[
129+
{
130+
label: "Residual ($)",
131+
labelStyle: { fontSize: 16, fill: t.ink },
132+
tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
133+
},
134+
]}
135+
margin={{ left: 110, right: 40, top: 20, bottom: 90 }}
136+
grid={{ horizontal: true, vertical: true }}
137+
slotProps={{
138+
legend: {
139+
position: { vertical: "top", horizontal: "middle" },
140+
direction: "row",
141+
labelStyle: { fontSize: 13, fill: t.inkSoft },
142+
},
143+
}}
144+
sx={{
145+
"& .MuiChartsGrid-line": { stroke: t.grid, strokeWidth: 1 },
146+
"& circle": { stroke: t.pageBg, strokeWidth: 1 },
147+
}}
148+
>
149+
<ChartsReferenceLine
150+
y={0}
151+
label="Perfect fit: residual = 0"
152+
labelAlign="end"
153+
lineStyle={{ stroke: t.ink, strokeWidth: 2.5 }}
154+
labelStyle={{ fill: t.ink, fontSize: 14, fontWeight: 600 }}
155+
/>
156+
<ChartsReferenceLine
157+
y={upperBand}
158+
label={`+2σ: ${Math.round(upperBand).toLocaleString()}`}
159+
labelAlign="end"
160+
lineStyle={{
161+
stroke: t.inkSoft,
162+
strokeDasharray: "8 6",
163+
strokeWidth: 1.75,
164+
}}
165+
labelStyle={{ fill: t.inkSoft, fontSize: 14 }}
166+
/>
167+
<ChartsReferenceLine
168+
y={lowerBand}
169+
label={`−2σ: ${Math.round(lowerBand).toLocaleString()}`}
170+
labelAlign="end"
171+
lineStyle={{
172+
stroke: t.inkSoft,
173+
strokeDasharray: "8 6",
174+
strokeWidth: 1.75,
175+
}}
176+
labelStyle={{ fill: t.inkSoft, fontSize: 14 }}
177+
/>
178+
</ScatterChart>
179+
</Box>
180+
);
181+
}

0 commit comments

Comments
 (0)