machinelearningalgorithms / templates /Apriori-Simulator-three.html
deedrop1140's picture
Upload 182 files
d0a6b4f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apriori Algorithm Simulator | Learn Data Mining Interactively</title>
<!-- Dependencies -->
<script src="https://cdn.tailwindcss.com"></script>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f8fafc;
}
.gradient-hero {
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
}
.text-gradient {
background: linear-gradient(to right, #2563eb, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.shadow-card {
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}
.shadow-soft {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-slide-up {
animation: slideUp 0.5s ease-out forwards;
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.animate-scale-in {
animation: scaleIn 0.3s ease-out forwards;
}
.gradient-primary {
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useMemo } = React;
// --- Data & Helpers ---
const groceryItems = [
{ id: "bread", name: "Bread", emoji: "🍞", category: "bakery" },
{ id: "milk", name: "Milk", emoji: "πŸ₯›", category: "dairy" },
{ id: "butter", name: "Butter", emoji: "🧈", category: "dairy" },
{ id: "eggs", name: "Eggs", emoji: "πŸ₯š", category: "dairy" },
{ id: "cheese", name: "Cheese", emoji: "πŸ§€", category: "dairy" },
{ id: "apple", name: "Apple", emoji: "🍎", category: "fruits" },
{ id: "banana", name: "Banana", emoji: "🍌", category: "fruits" },
{ id: "coffee", name: "Coffee", emoji: "β˜•", category: "beverages" },
{ id: "cereal", name: "Cereal", emoji: "πŸ₯£", category: "breakfast" },
{ id: "yogurt", name: "Yogurt", emoji: "πŸ₯›", category: "dairy" },
];
const getItemEmoji = (id) => groceryItems.find(i => i.id === id)?.emoji || "πŸ“¦";
const getItemDisplayName = (id) => groceryItems.find(i => i.id === id)?.name || id;
// --- Logic Engine ---
const transactions = [
{ id: 1, items: ["bread", "milk", "butter"], customer: "πŸ‘©" },
{ id: 2, items: ["bread", "eggs", "milk"], customer: "πŸ‘¨" },
{ id: 3, items: ["milk", "butter", "cheese"], customer: "πŸ‘΅" },
{ id: 4, items: ["bread", "milk", "butter", "eggs"], customer: "πŸ‘¦" },
{ id: 5, items: ["bread", "butter"], customer: "πŸ‘§" },
{ id: 6, items: ["milk", "eggs", "cheese"], customer: "πŸ‘΄" },
{ id: 7, items: ["bread", "milk", "butter", "cheese"], customer: "πŸ‘©β€πŸ¦°" },
{ id: 8, items: ["bread", "milk"], customer: "πŸ‘¨β€πŸ¦±" },
];
// --- UI Components ---
const Icon = ({ name, size = 20, className = "" }) => {
const icons = {
"lightbulb": <><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .5 2.2 1.5 3.1.7.9 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></>,
"search": <><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></>,
"rotate-ccw": <><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></>,
"chevron-left": <path d="m15 18-6-6 6-6"/>,
"chevron-right": <path d="m9 18 6-6 6-6"/>,
"shopping-cart": <><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></>,
"sparkles": <><path d="m12 3 1.912 5.813a2 2 0 0 0 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 0-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 0-1.275-1.275L3 12l5.813-1.912a2 2 0 0 0 1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></>,
"arrow-right": <><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></>,
"trending-up": <><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></>
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
{icons[name] || null}
</svg>
);
};
const Button = ({ children, onClick, disabled, variant = "default", size = "md", className = "" }) => {
const variants = {
default: "bg-slate-900 text-white hover:bg-slate-800",
outline: "border border-slate-200 bg-white hover:bg-slate-50 text-slate-700",
hero: "gradient-primary text-white shadow-lg hover:opacity-90",
icon: "p-2 border border-slate-200 hover:bg-slate-50"
};
const sizes = {
md: "px-4 py-2",
lg: "px-8 py-3 text-lg font-bold"
};
return (
<button
onClick={onClick}
disabled={disabled}
className={`inline-flex items-center justify-center rounded-xl transition-all active:scale-95 disabled:opacity-50 disabled:active:scale-100 ${variants[variant]} ${sizes[size]} ${className}`}
>
{children}
</button>
);
};
// --- Main Simulator ---
const steps = [
{ id: 0, title: "Meet Sarah, the Store Owner πŸ‘©β€πŸ’Ό", subtitle: "She wants to understand her customers better" },
{ id: 1, title: "Sarah looks at shopping receipts 🧾", subtitle: "8 customers visited her store today" },
{ id: 2, title: "Let's count each item! πŸ”’", subtitle: "How many times did each item appear?" },
{ id: 3, title: "Find the popular items! ⭐", subtitle: "Items bought by at least 3 customers are 'frequent'" },
{ id: 4, title: "Which items are bought TOGETHER? 🀝", subtitle: "Let's check pairs of frequent items" },
{ id: 5, title: "Sarah discovers patterns! πŸ’‘", subtitle: "These are called 'Association Rules'" },
{ id: 6, title: "Sarah can use these insights! 🎯", subtitle: "Now she knows how to arrange her store" },
];
const AprioriSimulator = () => {
const [currentStep, setCurrentStep] = useState(0);
const nextStep = () => currentStep < steps.length - 1 && setCurrentStep(currentStep + 1);
const prevStep = () => currentStep > 0 && setCurrentStep(currentStep - 1);
const reset = () => setCurrentStep(0);
// Audio Handler
const playSound = (e) => {
e.preventDefault(); // Prevent default anchor behavior to allow sound to play
const audio = document.getElementById('clickSound');
if (audio) {
audio.currentTime = 0;
audio.play().catch(err => console.log("Audio play prevented:", err));
}
// Navigate after a short delay for the sound
setTimeout(() => {
window.location.href = "/xgboost-regression";
}, 150);
};
// Calculations
const itemCounts = useMemo(() => {
const counts = {};
transactions.forEach(t => t.items.forEach(item => counts[item] = (counts[item] || 0) + 1));
return counts;
}, []);
const frequentItems = Object.entries(itemCounts).filter(([_, c]) => c >= 3).map(([i]) => i);
const pairCounts = useMemo(() => {
const counts = {};
transactions.forEach(t => {
for (let i = 0; i < t.items.length; i++) {
for (let j = i + 1; j < t.items.length; j++) {
const pair = [t.items[i], t.items[j]].sort().join("+");
if (frequentItems.includes(t.items[i]) && frequentItems.includes(t.items[j])) {
counts[pair] = (counts[pair] || 0) + 1;
}
}
}
});
return counts;
}, [frequentItems]);
const frequentPairs = Object.entries(pairCounts).filter(([_, c]) => c >= 3).sort((a,b) => b[1]-a[1]);
// Define progress bar style outside of JSX to avoid double-curly-brace pattern
const progressStyle = { width: `${((currentStep + 1) / steps.length) * 100}%` };
const renderStepContent = () => {
switch (currentStep) {
case 0: return (
<div className="text-center animate-scale-in">
<div className="text-8xl mb-6">πŸ‘©β€πŸ’Ό</div>
<div className="bg-white rounded-3xl p-8 shadow-card max-w-md mx-auto border border-blue-50">
<p className="text-xl leading-relaxed text-slate-700">
"Hi! I'm <strong>Sarah</strong>. I own a small grocery store.
I noticed some customers buy certain items together..."
</p>
<p className="text-xl leading-relaxed mt-4 text-slate-700">
"I want to find these <strong>patterns</strong> so I can place
related items close together!"
</p>
</div>
<div className="mt-8 flex justify-center gap-6 text-5xl">
🍞 πŸ₯› 🧈 πŸ₯š πŸ§€
</div>
</div>
);
case 1: return (
<div className="animate-slide-up">
<div className="grid gap-3 max-w-2xl mx-auto">
{transactions.map((t, idx) => {
// Removed double-curly-brace style pattern for Flask compatibility
const animStyle = { animationDelay: `${idx * 100}ms` };
return (
<div key={t.id} className="flex items-center gap-4 bg-white rounded-2xl p-4 shadow-soft border border-slate-100 animate-slide-up" style={animStyle}>
<div className="text-3xl">{t.customer}</div>
<div className="text-2xl opacity-50">πŸ›’</div>
<div className="flex flex-wrap gap-2">
{t.items.map(item => (
<span key={item} className="bg-slate-100 text-slate-700 px-3 py-1 rounded-full text-sm font-medium flex items-center gap-1 border border-slate-200">
<span className="text-lg">{getItemEmoji(item)}</span>
{getItemDisplayName(item)}
</span>
))}
</div>
</div>
);
})}
</div>
</div>
);
case 2: return (
<div className="animate-scale-in">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 max-w-3xl mx-auto">
{Object.entries(itemCounts).sort((a,b) => b[1]-a[1]).map(([item, count], idx) => (
<div key={item} className="bg-white rounded-2xl p-6 shadow-soft text-center border border-slate-100">
<div className="text-5xl mb-2">{getItemEmoji(item)}</div>
<div className="font-bold text-lg text-slate-800">{getItemDisplayName(item)}</div>
<div className="mt-2 text-4xl font-extrabold text-blue-600">{count}</div>
<div className="text-sm text-slate-400 font-medium uppercase tracking-wider">Times Bought</div>
</div>
))}
</div>
</div>
);
case 3: return (
<div className="animate-scale-in">
<div className="text-center mb-8">
<div className="inline-block bg-blue-50 text-blue-700 px-6 py-3 rounded-2xl border border-blue-100">
<p className="text-lg font-medium">
<strong>Rule:</strong> Bought by <strong>at least 3</strong> customers
</p>
</div>
</div>
<div className="grid sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
{Object.entries(itemCounts).sort((a,b) => b[1]-a[1]).map(([item, count]) => (
<div key={item} className={`rounded-2xl p-5 flex items-center justify-between border-2 transition-all ${count >= 3 ? "bg-green-50 border-green-200 shadow-sm" : "bg-slate-50 border-slate-100 opacity-50"}`}>
<div className="flex items-center gap-4">
<span className="text-4xl">{getItemEmoji(item)}</span>
<div>
<div className="font-bold text-slate-800">{getItemDisplayName(item)}</div>
<div className="text-sm text-slate-500 font-medium">{count} / 8 customers</div>
</div>
</div>
<div className="text-2xl">{count >= 3 ? "βœ…" : "❌"}</div>
</div>
))}
</div>
</div>
);
case 4: return (
<div className="animate-scale-in">
<div className="grid sm:grid-cols-2 gap-4 max-w-2xl mx-auto">
{frequentPairs.map(([pair, count], idx) => {
const [i1, i2] = pair.split("+");
// Removed double-curly-brace style pattern for Flask compatibility
const animStyle = { animationDelay: `${idx * 150}ms` };
return (
<div key={pair} className="bg-white rounded-2xl p-6 shadow-card border border-blue-50 text-center animate-slide-up" style={animStyle}>
<div className="flex items-center justify-center gap-4 mb-3">
<span className="text-5xl">{getItemEmoji(i1)}</span>
<span className="text-3xl font-bold text-blue-400">+</span>
<span className="text-5xl">{getItemEmoji(i2)}</span>
</div>
<div className="font-bold text-slate-800 text-lg">{getItemDisplayName(i1)} & {getItemDisplayName(i2)}</div>
<div className="mt-3 text-blue-600 font-extrabold text-2xl">Together {count}x</div>
</div>
);
})}
</div>
</div>
);
case 5: return (
<div className="animate-scale-in max-w-2xl mx-auto space-y-4">
{[
{ from: "bread", to: "milk", p: 86, tip: "Most bread buyers also get milk!" },
{ from: "bread", to: "butter", p: 71, tip: "Bread and butter are best friends!" },
{ from: "milk", to: "butter", p: 57, tip: "Dairy items stick together!" }
].map((rule, idx) => (
<div key={idx} className="bg-white rounded-2xl p-6 shadow-card border border-blue-50 border-l-4 border-l-blue-600">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<span className="text-sm font-bold text-slate-400 uppercase">If buys</span>
<span className="bg-blue-600 text-white px-4 py-1 rounded-full font-bold flex items-center gap-2">
{getItemEmoji(rule.from)} {getItemDisplayName(rule.from)}
</span>
</div>
<div className="text-2xl text-slate-300">➜</div>
<div className="flex items-center gap-3">
<span className="text-sm font-bold text-slate-400 uppercase">Then likely buys</span>
<span className="bg-green-500 text-white px-4 py-1 rounded-full font-bold flex items-center gap-2">
{getItemEmoji(rule.to)} {getItemDisplayName(rule.to)}
</span>
</div>
<div className="ml-auto text-3xl font-black text-blue-600">{rule.p}%</div>
</div>
<div className="mt-4 flex items-center gap-2 text-slate-500 italic text-sm">
<Icon name="lightbulb" className="w-4 h-4 text-amber-500" />
{rule.tip}
</div>
</div>
))}
</div>
);
case 6: return (
<div className="text-center animate-scale-in max-w-2xl mx-auto">
<div className="text-8xl mb-6">πŸŽ‰</div>
<h2 className="text-3xl font-black text-slate-800 mb-8">Sarah has a plan!</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
{[
{ e: "πŸ›’", t: "Store Layout", d: "Put bread near milk and butter" },
{ e: "🏷️", t: "Bundle Deals", d: "Offer 'Bread + Butter' discount packs" },
{ e: "πŸ“±", t: "Smart Suggestions", d: "If someone has milk, suggest butter!" },
{ e: "πŸ“¦", t: "Inventory", d: "Stock more milk when bread is on sale" },
].map((tip, idx) => (
<div key={idx} className="bg-white rounded-2xl p-5 shadow-soft border border-slate-100">
<div className="text-4xl mb-2">{tip.e}</div>
<div className="font-bold text-lg text-slate-800">{tip.t}</div>
<div className="text-slate-500">{tip.d}</div>
</div>
))}
</div>
<div className="mt-10 bg-blue-600 text-white rounded-3xl p-8 shadow-xl">
<h3 className="text-2xl font-bold mb-3">🧠 You mastered Apriori!</h3>
<p className="text-blue-100 text-lg leading-relaxed">
You just learned how to find <strong>frequent itemsets</strong> and turn them into
<strong> association rules</strong> to drive real business decisions!
</p>
</div>
</div>
);
default: return null;
}
};
return (
<div className="min-h-screen gradient-hero py-12 px-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-10">
<h1 className="text-4xl md:text-5xl font-black mb-2 tracking-tight">
<span className="text-gradient">Apriori Algorithm</span>
</h1>
<p className="text-slate-500 text-lg font-medium">Learn how stores find shopping patterns! πŸ›οΈ</p>
</div>
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between items-center mb-3">
<span className="text-sm font-bold text-slate-400 uppercase tracking-widest">Step {currentStep + 1} of {steps.length}</span>
<span className="text-sm font-bold text-blue-600">{steps[currentStep].title}</span>
</div>
<div className="h-3 bg-slate-100 rounded-full overflow-hidden shadow-inner border border-slate-50">
{/* Using pre-defined variable for style to avoid double-curly-brace pattern */}
<div
className="h-full gradient-primary transition-all duration-700 ease-in-out"
style={progressStyle}
/>
</div>
</div>
{/* Card Content */}
<div className="min-h-[500px] mb-10">
<div className="bg-white rounded-[2rem] p-8 shadow-card border border-blue-50">
<div className="text-center mb-8">
<h2 className="text-2xl font-black text-slate-800 mb-1">{steps[currentStep].title}</h2>
<p className="text-slate-400 font-medium">{steps[currentStep].subtitle}</p>
</div>
{renderStepContent()}
</div>
</div>
{/* Navigation */}
<div className="flex items-center justify-center gap-4">
<Button variant="outline" size="lg" onClick={reset} disabled={currentStep === 0}>
<Icon name="rotate-ccw" className="w-5 h-5 mr-2" />
Reset
</Button>
<Button variant="outline" size="lg" onClick={prevStep} disabled={currentStep === 0}>
<Icon name="chevron-left" className="w-5 h-5 mr-1" />
Back
</Button>
<Button variant="hero" size="lg" onClick={nextStep} className="px-12">
{currentStep === steps.length - 1 ? "Start Over" : "Next Step"}
{currentStep < steps.length - 1 && <Icon name="chevron-right" className="ml-2 w-6 h-6" />}
</Button>
</div>
{/* Centered Button (FIXED) */}
<div className="mt-12 flex justify-center pb-8">
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
<a
href="/xgboost-regression"
onClick={playSound}
className="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider cursor-pointer"
>
Back to Core
</a>
</div>
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<AprioriSimulator />);
</script>
</body>
</html>