| <!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>
|
|
|
| <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;
|
|
|
|
|
| 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;
|
|
|
|
|
| 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: "π¨βπ¦±" },
|
| ];
|
|
|
|
|
|
|
| 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>
|
| );
|
| };
|
|
|
|
|
| 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);
|
|
|
|
|
| const playSound = (e) => {
|
| e.preventDefault();
|
| const audio = document.getElementById('clickSound');
|
| if (audio) {
|
| audio.currentTime = 0;
|
| audio.play().catch(err => console.log("Audio play prevented:", err));
|
| }
|
|
|
|
|
| setTimeout(() => {
|
| window.location.href = "/xgboost-regression";
|
| }, 150);
|
| };
|
|
|
|
|
| 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]);
|
|
|
|
|
| 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> |