109 lines
4.4 KiB
TypeScript
109 lines
4.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
const RationalExplorer: React.FC = () => {
|
|
const [cancelFactor, setCancelFactor] = useState(false); // If true, (x-2) is in numerator
|
|
|
|
// Base function f(x) = (x+1) / [(x-2)(x+1)] ? No simple case.
|
|
// Let's do: f(x) = (x+1) * [ (x-2) if cancel ] / [ (x-2) * (x-3) ]
|
|
// If cancel: f(x) = (x+1)/(x-3) with Hole at 2.
|
|
// If not cancel: f(x) = (x+1) / [(x-2)(x-3)] ... complex.
|
|
|
|
// Better example: f(x) = [numerator] / (x-2)
|
|
// Numerator options: (x-2) -> Hole. 1 -> VA.
|
|
|
|
const width = 300;
|
|
const height = 200;
|
|
const range = 6;
|
|
const scale = width / (range * 2);
|
|
const center = width / 2;
|
|
const toPx = (v: number, isY = false) => isY ? height/2 - v * scale : center + v * scale;
|
|
|
|
const generatePath = () => {
|
|
let d = "";
|
|
for (let x = -range; x <= range; x += 0.05) {
|
|
if (Math.abs(x - 2) < 0.1) continue; // Skip near discontinuity
|
|
|
|
let y = 0;
|
|
if (cancelFactor) {
|
|
// f(x) = (x-2) / (x-2) = 1
|
|
y = 1;
|
|
} else {
|
|
// f(x) = 1 / (x-2)
|
|
y = 1 / (x - 2);
|
|
}
|
|
|
|
if (Math.abs(y) > range) {
|
|
d += ` M `; // Break path
|
|
continue;
|
|
}
|
|
const px = toPx(x);
|
|
const py = toPx(y, true);
|
|
d += d.endsWith('M ') ? `${px} ${py}` : ` L ${px} ${py}`;
|
|
}
|
|
return d;
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
|
|
<div className="mb-6 flex flex-col items-center">
|
|
<div className="text-xl font-mono font-bold bg-slate-50 px-6 py-3 rounded-lg border border-slate-200 mb-4">
|
|
f(x) = <div className="inline-block align-middle text-center mx-2">
|
|
<div className="border-b border-slate-800 pb-1 mb-1">{cancelFactor ? <span className="text-rose-600">(x-2)</span> : "1"}</div>
|
|
<div className="text-indigo-600">(x-2)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex bg-slate-100 p-1 rounded-lg">
|
|
<button
|
|
onClick={() => setCancelFactor(false)}
|
|
className={`px-4 py-2 text-sm font-bold rounded-md transition-all ${!cancelFactor ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
|
>
|
|
Different Factor (1)
|
|
</button>
|
|
<button
|
|
onClick={() => setCancelFactor(true)}
|
|
className={`px-4 py-2 text-sm font-bold rounded-md transition-all ${cancelFactor ? 'bg-white shadow text-slate-800' : 'text-slate-500'}`}
|
|
>
|
|
Common Factor (x-2)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative h-[200px] w-full bg-white border border-slate-200 rounded-xl overflow-hidden">
|
|
<svg width="100%" height="100%" viewBox={`0 0 ${width} ${height}`}>
|
|
{/* Axes */}
|
|
<line x1="0" y1={height/2} x2={width} y2={height/2} stroke="#cbd5e1" strokeWidth="2" />
|
|
<line x1={center} y1="0" x2={center} y2={height} stroke="#cbd5e1" strokeWidth="2" />
|
|
|
|
{/* Discontinuity at x=2 */}
|
|
<line x1={toPx(2)} y1={0} x2={toPx(2)} y2={height} stroke="#cbd5e1" strokeDasharray="4,4" />
|
|
|
|
{/* Graph */}
|
|
<path d={generatePath()} stroke="#8b5cf6" strokeWidth="3" fill="none" />
|
|
|
|
{/* Hole Visualization */}
|
|
{cancelFactor && (
|
|
<circle cx={toPx(2)} cy={toPx(1, true)} r="4" fill="white" stroke="#8b5cf6" strokeWidth="2" />
|
|
)}
|
|
</svg>
|
|
|
|
{/* Labels */}
|
|
<div className="absolute top-2 right-2 text-xs font-bold text-slate-400">x=2</div>
|
|
</div>
|
|
|
|
<div className="mt-4 p-4 rounded-lg bg-violet-50 border border-violet-100 text-sm text-violet-900 text-center">
|
|
{cancelFactor ? (
|
|
<span>
|
|
<strong>Hole:</strong> The factor (x-2) cancels out. The graph looks like y=1, but x=2 is undefined.
|
|
</span>
|
|
) : (
|
|
<span>
|
|
<strong>Vertical Asymptote:</strong> The factor (x-2) stays in the denominator. y approaches infinity near x=2.
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RationalExplorer; |