feat(lessons): add lessons from client db
This commit is contained in:
125
src/components/lessons/ScatterplotInteractiveWidget.tsx
Normal file
125
src/components/lessons/ScatterplotInteractiveWidget.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface DataPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
isOutlier?: boolean;
|
||||
}
|
||||
|
||||
const ScatterplotInteractiveWidget: React.FC = () => {
|
||||
const [showLine, setShowLine] = useState(false);
|
||||
const [showResiduals, setShowResiduals] = useState(false);
|
||||
const [hasOutlier, setHasOutlier] = useState(false);
|
||||
|
||||
// Base Data (approx linear y = 1.5x + 10)
|
||||
const basePoints: DataPoint[] = [
|
||||
{x: 1, y: 12}, {x: 2, y: 14}, {x: 3, y: 13}, {x: 4, y: 17},
|
||||
{x: 5, y: 18}, {x: 6, y: 19}, {x: 7, y: 22}, {x: 8, y: 21}
|
||||
];
|
||||
|
||||
const points: DataPoint[] = hasOutlier
|
||||
? [...basePoints, {x: 7, y: 5, isOutlier: true}]
|
||||
: basePoints;
|
||||
|
||||
// Simple Linear Regression Calculation
|
||||
const n = points.length;
|
||||
const sumX = points.reduce((a, p) => a + p.x, 0);
|
||||
const sumY = points.reduce((a, p) => a + p.y, 0);
|
||||
const sumXY = points.reduce((a, p) => a + p.x * p.y, 0);
|
||||
const sumXX = points.reduce((a, p) => a + p.x * p.x, 0);
|
||||
|
||||
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
||||
const intercept = (sumY - slope * sumX) / n;
|
||||
|
||||
const predict = (x: number) => slope * x + intercept;
|
||||
|
||||
// Scales
|
||||
const width = 400;
|
||||
const height = 250;
|
||||
const maxX = 9;
|
||||
const maxY = 25;
|
||||
|
||||
const toX = (val: number) => (val / maxX) * width;
|
||||
const toY = (val: number) => height - (val / maxY) * height;
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
|
||||
<div className="flex flex-wrap gap-4 mb-6 justify-center">
|
||||
<button
|
||||
onClick={() => setShowLine(!showLine)}
|
||||
className={`px-4 py-2 rounded-full font-bold text-sm transition-all ${showLine ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600'}`}
|
||||
>
|
||||
{showLine ? 'Hide Line' : 'Show Line of Best Fit'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowResiduals(!showResiduals)}
|
||||
className={`px-4 py-2 rounded-full font-bold text-sm transition-all ${showResiduals ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600'}`}
|
||||
>
|
||||
{showResiduals ? 'Hide Residuals' : 'Show Residuals'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setHasOutlier(!hasOutlier)}
|
||||
className={`px-4 py-2 rounded-full font-bold text-sm transition-all border ${hasOutlier ? 'bg-rose-100 text-rose-700 border-rose-300' : 'bg-white text-slate-600 border-slate-300'}`}
|
||||
>
|
||||
{hasOutlier ? 'Remove Outlier' : 'Add Outlier'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative border-b border-l border-slate-300 bg-slate-50 rounded-tr-lg mb-4 h-[250px]">
|
||||
<svg width="100%" height="100%" viewBox={`0 0 ${width} ${height}`} className="overflow-visible">
|
||||
{/* Line of Best Fit */}
|
||||
{showLine && (
|
||||
<line
|
||||
x1={toX(0)} y1={toY(predict(0))}
|
||||
x2={toX(maxX)} y2={toY(predict(maxX))}
|
||||
stroke="#4f46e5" strokeWidth="2" strokeDasharray={hasOutlier ? "5,5" : ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Residuals */}
|
||||
{showLine && showResiduals && points.map((p, i) => (
|
||||
<line
|
||||
key={`res-${i}`}
|
||||
x1={toX(p.x)} y1={toY(p.y)}
|
||||
x2={toX(p.x)} y2={toY(predict(p.x))}
|
||||
stroke={p.y > predict(p.x) ? "#10b981" : "#f43f5e"} strokeWidth="1.5" opacity="0.6"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Points */}
|
||||
{points.map((p, i) => (
|
||||
<g key={i}>
|
||||
<circle
|
||||
cx={toX(p.x)} cy={toY(p.y)}
|
||||
r={p.isOutlier ? 6 : 4}
|
||||
fill={p.isOutlier ? "#f43f5e" : "#475569"}
|
||||
stroke="white" strokeWidth="2"
|
||||
className="transition-all duration-300"
|
||||
/>
|
||||
{p.isOutlier && (
|
||||
<text x={toX(p.x)+10} y={toY(p.y)} className="text-xs font-bold fill-rose-600">Outlier</text>
|
||||
)}
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
{/* Axes Labels */}
|
||||
<div className="absolute -bottom-6 w-full text-center text-xs font-bold text-slate-400">Variable X</div>
|
||||
<div className="absolute -left-8 top-1/2 -rotate-90 text-xs font-bold text-slate-400">Variable Y</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200 flex justify-between items-center text-sm">
|
||||
<div>
|
||||
<span className="font-bold text-slate-500 block text-xs uppercase">Slope (m)</span>
|
||||
<span className="font-mono font-bold text-indigo-700 text-lg">{slope.toFixed(2)}</span>
|
||||
</div>
|
||||
{hasOutlier && (
|
||||
<div className="text-rose-600 font-bold bg-rose-50 px-3 py-1 rounded border border-rose-200">
|
||||
Outlier pulls the line down!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScatterplotInteractiveWidget;
|
||||
Reference in New Issue
Block a user