93 lines
4.3 KiB
TypeScript
93 lines
4.3 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
const SlopeInterceptWidget: React.FC = () => {
|
|
const [m, setM] = useState(2);
|
|
const [b, setB] = useState(1);
|
|
|
|
// Visualization config
|
|
const range = 10;
|
|
const scale = 25; // px per unit
|
|
const center = 150;
|
|
|
|
const toPx = (val: number, isY = false) => isY ? center - val * scale : center + val * scale;
|
|
|
|
// Points for triangle
|
|
const p1 = { x: 0, y: b };
|
|
const p2 = { x: 1, y: m * 1 + b };
|
|
// Triangle vertex (1, b)
|
|
const p3 = { x: 1, y: b };
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
|
|
<div className="flex flex-col md:flex-row gap-8">
|
|
<div className="w-full md:w-1/3 space-y-6">
|
|
<div className="p-4 bg-slate-50 rounded-xl border border-slate-200 text-center">
|
|
<div className="text-sm text-slate-500 font-bold uppercase mb-1">Equation</div>
|
|
<div className="text-2xl font-mono font-bold text-slate-800">
|
|
y = <span className="text-blue-600">{m}</span>x + <span className="text-rose-600">{b}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-bold text-blue-600 uppercase">Slope (m) = {m}</label>
|
|
<input
|
|
type="range" min="-5" max="5" step="0.5"
|
|
value={m} onChange={e => setM(parseFloat(e.target.value))}
|
|
className="w-full h-2 bg-blue-100 rounded-lg appearance-none cursor-pointer accent-blue-600 mt-2"
|
|
/>
|
|
<p className="text-xs text-slate-400 mt-1">Rate of Change (Rise / Run)</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-bold text-rose-600 uppercase">Y-Intercept (b) = {b}</label>
|
|
<input
|
|
type="range" min="-5" max="5" step="1"
|
|
value={b} onChange={e => setB(parseFloat(e.target.value))}
|
|
className="w-full h-2 bg-rose-100 rounded-lg appearance-none cursor-pointer accent-rose-600 mt-2"
|
|
/>
|
|
<p className="text-xs text-slate-400 mt-1">Starting Value (when x=0)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="w-full md:flex-1 h-[300px] bg-white border border-slate-200 rounded-xl relative overflow-hidden">
|
|
<svg width="100%" height="100%" viewBox="0 0 300 300" className="absolute top-0 left-0">
|
|
<defs>
|
|
<pattern id="si-grid" width={scale} height={scale} patternUnits="userSpaceOnUse">
|
|
<path d={`M ${scale} 0 L 0 0 0 ${scale}`} fill="none" stroke="#f1f5f9" strokeWidth="1"/>
|
|
</pattern>
|
|
</defs>
|
|
<rect width="100%" height="100%" fill="url(#si-grid)" />
|
|
|
|
{/* Axes */}
|
|
<line x1="0" y1={center} x2="300" y2={center} stroke="#cbd5e1" strokeWidth="2" />
|
|
<line x1={center} y1="0" x2={center} y2="300" stroke="#cbd5e1" strokeWidth="2" />
|
|
|
|
{/* The Line */}
|
|
<line
|
|
x1={toPx(-range)} y1={toPx(m * -range + b, true)}
|
|
x2={toPx(range)} y2={toPx(m * range + b, true)}
|
|
stroke="#1e293b" strokeWidth="3"
|
|
/>
|
|
|
|
{/* Slope Triangle (between x=0 and x=1) */}
|
|
<path
|
|
d={`M ${toPx(p1.x)} ${toPx(p1.y, true)} L ${toPx(p3.x)} ${toPx(p3.y, true)} L ${toPx(p2.x)} ${toPx(p2.y, true)} Z`}
|
|
fill="rgba(37, 99, 235, 0.1)" stroke="#2563eb" strokeWidth="1" strokeDasharray="4,2"
|
|
/>
|
|
|
|
{/* Intercept Point */}
|
|
<circle cx={toPx(0)} cy={toPx(b, true)} r="5" fill="#e11d48" stroke="white" strokeWidth="2" />
|
|
<text x={toPx(0) + 10} y={toPx(b, true)} className="text-xs font-bold fill-rose-600">b={b}</text>
|
|
|
|
{/* Rise/Run Labels */}
|
|
<text x={toPx(0.5)} y={toPx(b, true) + (m>0 ? 15 : -10)} textAnchor="middle" className="text-[10px] font-bold fill-blue-400">Run: 1</text>
|
|
<text x={toPx(1) + 5} y={toPx(b + m/2, true)} className="text-[10px] font-bold fill-blue-600">Rise: {m}</text>
|
|
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SlopeInterceptWidget; |