|
|
<!DOCTYPE html>
|
|
|
<html lang="en" class="dark">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Eclat Algorithm Simulator</title>
|
|
|
|
|
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
<script>
|
|
|
tailwind.config = {
|
|
|
darkMode: 'class',
|
|
|
theme: {
|
|
|
extend: {
|
|
|
colors: {
|
|
|
border: "hsl(var(--border))",
|
|
|
input: "hsl(var(--input))",
|
|
|
ring: "hsl(var(--ring))",
|
|
|
background: "hsl(var(--background))",
|
|
|
foreground: "hsl(var(--foreground))",
|
|
|
primary: {
|
|
|
DEFAULT: "hsl(var(--primary))",
|
|
|
foreground: "hsl(var(--primary-foreground))",
|
|
|
},
|
|
|
secondary: {
|
|
|
DEFAULT: "hsl(var(--secondary))",
|
|
|
foreground: "hsl(var(--secondary-foreground))",
|
|
|
},
|
|
|
destructive: {
|
|
|
DEFAULT: "hsl(var(--destructive))",
|
|
|
foreground: "hsl(var(--destructive-foreground))",
|
|
|
},
|
|
|
muted: {
|
|
|
DEFAULT: "hsl(var(--muted))",
|
|
|
foreground: "hsl(var(--muted-foreground))",
|
|
|
},
|
|
|
accent: {
|
|
|
DEFAULT: "hsl(var(--accent))",
|
|
|
foreground: "hsl(var(--accent-foreground))",
|
|
|
},
|
|
|
popover: {
|
|
|
DEFAULT: "hsl(var(--popover))",
|
|
|
foreground: "hsl(var(--popover-foreground))",
|
|
|
},
|
|
|
card: {
|
|
|
DEFAULT: "hsl(var(--card))",
|
|
|
foreground: "hsl(var(--card-foreground))",
|
|
|
},
|
|
|
success: {
|
|
|
DEFAULT: "#10b981",
|
|
|
foreground: "#ffffff"
|
|
|
}
|
|
|
},
|
|
|
borderRadius: {
|
|
|
lg: "var(--radius)",
|
|
|
md: "calc(var(--radius) - 2px)",
|
|
|
sm: "calc(var(--radius) - 4px)",
|
|
|
},
|
|
|
animation: {
|
|
|
'pulse-glow': 'pulseGlow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
|
},
|
|
|
keyframes: {
|
|
|
pulseGlow: {
|
|
|
'0%, 100%': { opacity: 1, boxShadow: '0 0 10px hsl(var(--primary) / 0.5)' },
|
|
|
'50%': { opacity: .8, boxShadow: '0 0 20px hsl(var(--primary) / 0.8)' },
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</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/framer-motion@10.16.4/dist/framer-motion.js"></script>
|
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
|
|
|
|
<style>
|
|
|
:root {
|
|
|
--background: 224 71% 4%;
|
|
|
--foreground: 213 31% 91%;
|
|
|
|
|
|
--card: 224 71% 4%;
|
|
|
--card-foreground: 213 31% 91%;
|
|
|
|
|
|
--popover: 224 71% 4%;
|
|
|
--popover-foreground: 215 20.2% 65.1%;
|
|
|
|
|
|
--primary: 263.4 70% 50.4%;
|
|
|
--primary-foreground: 210 40% 98%;
|
|
|
|
|
|
--secondary: 222.2 47.4% 11.2%;
|
|
|
--secondary-foreground: 210 40% 98%;
|
|
|
|
|
|
--muted: 217.2 32.6% 17.5%;
|
|
|
--muted-foreground: 215 20.2% 65.1%;
|
|
|
|
|
|
--accent: 45 93% 47%;
|
|
|
--accent-foreground: 210 40% 98%;
|
|
|
|
|
|
--destructive: 0 62.8% 30.6%;
|
|
|
--destructive-foreground: 210 40% 98%;
|
|
|
|
|
|
--border: 217.2 32.6% 17.5%;
|
|
|
--input: 217.2 32.6% 17.5%;
|
|
|
--ring: 224.3 76.3% 48%;
|
|
|
|
|
|
--radius: 0.5rem;
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
background-color: hsl(var(--background));
|
|
|
color: hsl(var(--foreground));
|
|
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
|
}
|
|
|
|
|
|
.circuit-pattern {
|
|
|
background-image: radial-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
|
|
background-size: 24px 24px;
|
|
|
}
|
|
|
|
|
|
.text-glow {
|
|
|
text-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
|
|
|
}
|
|
|
|
|
|
.glow-primary {
|
|
|
box-shadow: 0 0 15px -3px hsl(var(--primary) / 0.6);
|
|
|
}
|
|
|
|
|
|
.glow-accent {
|
|
|
box-shadow: 0 0 15px -3px hsl(var(--accent) / 0.4);
|
|
|
}
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar {
|
|
|
width: 8px;
|
|
|
}
|
|
|
::-webkit-scrollbar-track {
|
|
|
background: hsl(var(--secondary));
|
|
|
}
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
background: hsl(var(--muted));
|
|
|
border-radius: 4px;
|
|
|
}
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
|
background: hsl(var(--primary));
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div id="root"></div>
|
|
|
|
|
|
|
|
|
{% raw %}
|
|
|
<script type="text/babel">
|
|
|
const { useState, useEffect, useRef } = React;
|
|
|
const { motion, AnimatePresence } = window.Motion;
|
|
|
|
|
|
|
|
|
function cn(...classes) {
|
|
|
return classes.filter(Boolean).join(' ');
|
|
|
}
|
|
|
|
|
|
|
|
|
const Icon = ({ d, className }) => (
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
|
<path d={d} />
|
|
|
</svg>
|
|
|
);
|
|
|
|
|
|
const Icons = {
|
|
|
Play: (props) => <Icon d="M5 3l14 9-14 9V3z" {...props} />,
|
|
|
Pause: (props) => <Icon d="M6 4h4v16H6zm8 0h4v16h-4z" {...props} />,
|
|
|
RotateCcw: (props) => <Icon d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8 M3 3v5h5" {...props} />,
|
|
|
ChevronRight: (props) => <Icon d="M9 18l6-6-6-6" {...props} />,
|
|
|
Database: (props) => <Icon d="M3 5c0-1.1 4.03-2 9-2s9 .9 9 2c0 1.1-4.03 2-9 2s-9-.9-9-2z M3 5v14c0 1.1 4.03 2 9 2s9-.9 9-2V5 M3 12c0 1.1 4.03 2 9 2s9-.9 9-2" {...props} />,
|
|
|
Info: (props) => <Icon d="M12 16v-4 M12 8h.01 M22 12A10 10 0 1 1 12 2a10 10 0 0 1 10 10z" {...props} />,
|
|
|
RotateCw: (props) => <Icon d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8 M21 3v5h-5" {...props} />,
|
|
|
Lightbulb: (props) => <Icon d="M9 18h6 M10 22h4 M15.09 14c.18-.9.93-1.54 1.86-1.54.96 0 1.63.74 1.93 1.54M9 14c.18-.9.93-1.54 1.86-1.54.96 0 1.63.74 1.93 1.54" {...props} />,
|
|
|
ChevronDown: (props) => <Icon d="M6 9l6 6 6-6" {...props} />,
|
|
|
ChevronUp: (props) => <Icon d="M18 15l-6-6-6 6" {...props} />,
|
|
|
ShoppingCart: (props) => <Icon d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" {...props} />,
|
|
|
Sparkles: (props) => <Icon d="M12 3l1.912 5.813a2 2 0 0 1 1.275 1.275L21 12l-5.813 1.912a2 2 0 0 1-1.275 1.275L12 21l-1.912-5.813a2 2 0 0 1-1.275-1.275L3 12l5.813-1.912a2 2 0 0 1 1.275-1.275z" {...props} />,
|
|
|
ArrowRight: (props) => <Icon d="M5 12h14M12 5l7 7-7 7" {...props} />,
|
|
|
BookOpen: (props) => <Icon d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" {...props} />,
|
|
|
Zap: (props) => <Icon d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" {...props} />,
|
|
|
CheckCircle: (props) => <Icon d="M22 11.08V12a10 10 0 1 1-5.93-9.14 M22 4L12 14.01l-3-3" {...props} />,
|
|
|
XCircle: (props) => <Icon d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" {...props} />,
|
|
|
Trophy: (props) => <Icon d="M8 21h8M12 17v4M7 4h10c.66 0 1.33.2 2 .59V9a8 8 0 0 1-8 8 8 8 0 0 1-8-8V4.59c.67-.39 1.34-.59 2-.59zM19 10a5 5 0 0 0 0 10M5 10a5 5 0 0 1 0 10" {...props} />,
|
|
|
TrendingUp: (props) => <Icon d="M23 6l-9.5 9.5-5-5L1 18" {...props} />,
|
|
|
HelpCircle: (props) => <Icon d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3 M12 17h.01 M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z" {...props} />,
|
|
|
X: (props) => <Icon d="M18 6L6 18M6 6l12 12" {...props} />,
|
|
|
Search: (props) => <Icon d="M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0z" {...props} />,
|
|
|
};
|
|
|
|
|
|
|
|
|
const Button = ({ children, variant = 'default', size = 'default', className, onClick, disabled }) => {
|
|
|
const variants = {
|
|
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
|
};
|
|
|
const sizes = {
|
|
|
default: "h-10 px-4 py-2",
|
|
|
sm: "h-9 rounded-md px-3",
|
|
|
icon: "h-10 w-10",
|
|
|
lg: "h-11 rounded-md px-8",
|
|
|
};
|
|
|
return (
|
|
|
<button
|
|
|
onClick={onClick}
|
|
|
disabled={disabled}
|
|
|
className={cn(
|
|
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
|
variants[variant],
|
|
|
sizes[size],
|
|
|
className
|
|
|
)}
|
|
|
>
|
|
|
{children}
|
|
|
</button>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const Slider = ({ value, onValueChange, min, max, step, className }) => {
|
|
|
return (
|
|
|
<input
|
|
|
type="range"
|
|
|
min={min}
|
|
|
max={max}
|
|
|
step={step}
|
|
|
value={value[0]}
|
|
|
onChange={(e) => onValueChange([parseFloat(e.target.value)])}
|
|
|
className={cn("w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary", className)}
|
|
|
/>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const Input = ({ className, ...props }) => (
|
|
|
<input
|
|
|
className={cn("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className)}
|
|
|
{...props}
|
|
|
/>
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const BeginnerTip = ({ title, children, defaultOpen = false }) => {
|
|
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
|
return (
|
|
|
<motion.div layout className="rounded-lg border border-accent/30 bg-accent/5 overflow-hidden">
|
|
|
<button onClick={() => setIsOpen(!isOpen)} className="w-full p-3 flex items-center gap-2 text-left hover:bg-accent/10 transition-colors">
|
|
|
<Icons.Lightbulb className="w-4 h-4 text-accent shrink-0" />
|
|
|
<span className="text-sm font-medium text-foreground flex-1">{title}</span>
|
|
|
{isOpen ? <Icons.ChevronUp className="w-4 h-4 text-muted-foreground" /> : <Icons.ChevronDown className="w-4 h-4 text-muted-foreground" />}
|
|
|
</button>
|
|
|
{isOpen && (
|
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="px-3 pb-3 text-sm text-foreground/80">
|
|
|
{children}
|
|
|
</motion.div>
|
|
|
)}
|
|
|
</motion.div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const TransactionPanel = ({ transactions, highlightedTids, step }) => {
|
|
|
const itemEmojis = { 'Bread': '๐', 'Milk': '๐ฅ', 'Eggs': '๐ฅ', 'Butter': '๐ง' };
|
|
|
return (
|
|
|
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
|
|
|
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
|
|
<div className="w-8 h-8 rounded-lg bg-secondary/20 flex items-center justify-center">
|
|
|
<Icons.ShoppingCart className="w-4 h-4 text-secondary" />
|
|
|
</div>
|
|
|
<div>
|
|
|
<h2 className="font-semibold text-foreground">Transaction Database</h2>
|
|
|
<p className="text-xs text-muted-foreground">Horizontal Format (Traditional)</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="p-4 space-y-3">
|
|
|
{transactions.map((transaction, index) => (
|
|
|
<motion.div
|
|
|
key={transaction.id}
|
|
|
initial={{ opacity: 0, x: -20 }}
|
|
|
animate={{ opacity: step >= 0 ? 1 : 0.3, x: 0, scale: highlightedTids.includes(transaction.id) ? 1.02 : 1 }}
|
|
|
transition={{ delay: index * 0.1 }}
|
|
|
className={`p-3 rounded-lg border transition-all duration-300 ${highlightedTids.includes(transaction.id) ? 'bg-primary/10 border-primary/50 glow-primary' : 'bg-muted/30 border-border/30 hover:border-border/60'}`}
|
|
|
>
|
|
|
<div className="flex items-center gap-3">
|
|
|
<span className={`font-mono text-sm font-bold w-8 h-8 rounded-full flex items-center justify-center ${highlightedTids.includes(transaction.id) ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'}`}>
|
|
|
T{transaction.id}
|
|
|
</span>
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
{transaction.items.map((item, i) => (
|
|
|
<motion.span key={i} initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: index * 0.1 + i * 0.05 }} className="px-2 py-1 rounded-md bg-background/50 border border-border/30 text-sm flex items-center gap-1">
|
|
|
<span>{itemEmojis[item]}</span><span className="text-foreground/80">{item}</span>
|
|
|
</motion.span>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const TidSetPanel = ({ tidSets, step, minSupportCount, onItemHover, highlightedItem }) => {
|
|
|
const itemEmojis = { 'Bread': '๐', 'Milk': '๐ฅ', 'Eggs': '๐ฅ', 'Butter': '๐ง' };
|
|
|
const itemColors = {
|
|
|
'Bread': 'from-amber-500/20 to-orange-500/20 border-amber-500/50',
|
|
|
'Milk': 'from-blue-500/20 to-cyan-500/20 border-blue-500/50',
|
|
|
'Eggs': 'from-yellow-500/20 to-amber-500/20 border-yellow-500/50',
|
|
|
'Butter': 'from-yellow-600/20 to-orange-400/20 border-yellow-600/50',
|
|
|
};
|
|
|
|
|
|
if (step < 1) {
|
|
|
return (
|
|
|
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
|
|
|
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
|
|
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center"><Icons.Database className="w-4 h-4 text-primary" /></div>
|
|
|
<div><h2 className="font-semibold text-foreground">TID Sets</h2><p className="text-xs text-muted-foreground">Vertical Format (Eclat)</p></div>
|
|
|
</div>
|
|
|
<div className="p-8 flex items-center justify-center">
|
|
|
<div className="text-center">
|
|
|
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mx-auto mb-4"><Icons.Database className="w-8 h-8 text-muted-foreground/50" /></div>
|
|
|
<p className="text-muted-foreground text-sm">Press <span className="text-primary font-semibold">Play</span> or <span className="text-primary font-semibold">Next Step</span> to begin</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden h-full">
|
|
|
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
|
|
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center animate-pulse-glow"><Icons.Database className="w-4 h-4 text-primary" /></div>
|
|
|
<div><h2 className="font-semibold text-foreground">TID Sets</h2><p className="text-xs text-muted-foreground">Vertical Format (Item โ Transaction IDs)</p></div>
|
|
|
</div>
|
|
|
<div className="p-4 space-y-3">
|
|
|
{tidSets.map((tidSet, index) => {
|
|
|
const isFrequent = tidSet.tids.length >= minSupportCount;
|
|
|
const isHighlighted = highlightedItem === tidSet.item;
|
|
|
return (
|
|
|
<motion.div
|
|
|
key={tidSet.item}
|
|
|
initial={{ opacity: 0, x: 20 }}
|
|
|
animate={{ opacity: 1, x: 0, scale: isHighlighted ? 1.02 : 1 }}
|
|
|
transition={{ delay: index * 0.15 }}
|
|
|
onMouseEnter={() => onItemHover(tidSet.item, tidSet.tids)}
|
|
|
onMouseLeave={() => onItemHover(null)}
|
|
|
className={`p-3 rounded-lg border bg-gradient-to-r transition-all duration-300 cursor-pointer ${itemColors[tidSet.item]} ${isHighlighted ? 'glow-primary' : ''}`}
|
|
|
>
|
|
|
<div className="flex items-center justify-between mb-2">
|
|
|
<div className="flex items-center gap-2">
|
|
|
<span className="text-lg">{itemEmojis[tidSet.item]}</span>
|
|
|
<span className="font-semibold text-foreground">{tidSet.item}</span>
|
|
|
</div>
|
|
|
{step >= 2 && (
|
|
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} className={`flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${isFrequent ? 'bg-success/20 text-success' : 'bg-destructive/20 text-destructive'}`}>
|
|
|
{isFrequent ? <><Icons.CheckCircle className="w-3 h-3" /> Frequent</> : <><Icons.XCircle className="w-3 h-3" /> Pruned</>}
|
|
|
</motion.div>
|
|
|
)}
|
|
|
</div>
|
|
|
<div className="flex items-center gap-2">
|
|
|
<span className="text-xs text-muted-foreground font-mono">TID:</span>
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
{tidSet.tids.map((tid, i) => (
|
|
|
<motion.span key={tid} initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ delay: index * 0.15 + i * 0.05 }} className="px-2 py-0.5 rounded bg-background/50 text-xs font-mono text-foreground/80 border border-border/30">
|
|
|
{tid}
|
|
|
</motion.span>
|
|
|
))}
|
|
|
</div>
|
|
|
<span className="ml-auto text-xs font-mono text-accent font-bold">|{tidSet.tids.length}|</span>
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
{step >= 1 && (
|
|
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="p-4 border-t border-border/30 bg-primary/5">
|
|
|
<p className="text-xs text-muted-foreground"><span className="text-primary font-semibold">๐ Transformed:</span> Now each item maps to the transactions containing it.</p>
|
|
|
</motion.div>
|
|
|
)}
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const IntersectionVisualizer = ({ tidSets, minSupportCount, step }) => {
|
|
|
const itemEmojis = { 'Bread': '๐', 'Milk': '๐ฅ', 'Eggs': '๐ฅ', 'Butter': '๐ง' };
|
|
|
const frequentItems = tidSets.filter(t => t.tids.length >= minSupportCount);
|
|
|
const intersections = [];
|
|
|
for (let i = 0; i < frequentItems.length; i++) {
|
|
|
for (let j = i + 1; j < frequentItems.length; j++) {
|
|
|
const result = frequentItems[i].tids.filter(tid => frequentItems[j].tids.includes(tid));
|
|
|
intersections.push({
|
|
|
item1: frequentItems[i].item,
|
|
|
item2: frequentItems[j].item,
|
|
|
tids1: frequentItems[i].tids,
|
|
|
tids2: frequentItems[j].tids,
|
|
|
result,
|
|
|
isFrequent: result.length >= minSupportCount
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden">
|
|
|
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
|
|
<div className="w-8 h-8 rounded-lg bg-accent/20 flex items-center justify-center"><Icons.Zap className="w-4 h-4 text-accent" /></div>
|
|
|
<div><h2 className="font-semibold text-foreground">Set Intersections</h2><p className="text-xs text-muted-foreground">Finding 2-item patterns through TID overlap</p></div>
|
|
|
</div>
|
|
|
<div className="p-4">
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
{intersections.map((intersection, index) => (
|
|
|
<motion.div
|
|
|
key={`${intersection.item1}-${intersection.item2}`}
|
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
|
transition={{ delay: index * 0.1 }}
|
|
|
className={`p-4 rounded-lg border transition-all ${intersection.isFrequent ? 'bg-success/5 border-success/30' : 'bg-muted/20 border-border/30 opacity-60'}`}
|
|
|
>
|
|
|
<div className="flex items-center justify-center gap-2 mb-3">
|
|
|
<span className="px-2 py-1 rounded bg-background/50 text-sm font-medium flex items-center gap-1">{itemEmojis[intersection.item1]} {intersection.item1}</span>
|
|
|
<span className="text-primary font-mono">โฉ</span>
|
|
|
<span className="px-2 py-1 rounded bg-background/50 text-sm font-medium flex items-center gap-1">{itemEmojis[intersection.item2]} {intersection.item2}</span>
|
|
|
</div>
|
|
|
<div className="flex items-center justify-center gap-2 mb-3 text-xs">
|
|
|
<div className="flex gap-0.5">
|
|
|
{intersection.tids1.map(tid => (
|
|
|
<span key={tid} className={`w-5 h-5 rounded flex items-center justify-center font-mono ${intersection.result.includes(tid) ? 'bg-success/30 text-success' : 'bg-muted/50 text-muted-foreground'}`}>{tid}</span>
|
|
|
))}
|
|
|
</div>
|
|
|
<Icons.ArrowRight className="w-3 h-3 text-muted-foreground" />
|
|
|
<div className="flex gap-0.5">
|
|
|
{intersection.result.length > 0 ? intersection.result.map(tid => (
|
|
|
<span key={tid} className="w-5 h-5 rounded flex items-center justify-center font-mono bg-success/30 text-success font-bold">{tid}</span>
|
|
|
)) : <span className="text-muted-foreground">โ
</span>}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className={`text-center text-xs font-medium px-2 py-1 rounded ${intersection.isFrequent ? 'bg-success/20 text-success' : 'bg-destructive/10 text-destructive/70'}`}>
|
|
|
Support: {intersection.result.length} {intersection.isFrequent ? ' โ Frequent' : ' โ Pruned'}
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const FrequentItemsetsPanel = ({ itemsets, step, onItemsetHover }) => {
|
|
|
const itemEmojis = { 'Bread': '๐', 'Milk': '๐ฅ', 'Eggs': '๐ฅ', 'Butter': '๐ง' };
|
|
|
const oneItemsets = itemsets.filter(i => i.items.length === 1);
|
|
|
const twoItemsets = itemsets.filter(i => i.items.length === 2);
|
|
|
const threeItemsets = itemsets.filter(i => i.items.length === 3);
|
|
|
|
|
|
const ItemsetCard = ({ items, support, tids, delay }) => (
|
|
|
<motion.div
|
|
|
initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: delay }}
|
|
|
onMouseEnter={() => onItemsetHover(items.join(','), tids)} onMouseLeave={() => onItemsetHover(null)}
|
|
|
className="p-2 rounded-lg bg-muted/30 border border-border/30 hover:border-primary/50 transition-all cursor-pointer"
|
|
|
>
|
|
|
<div className="flex items-center justify-between">
|
|
|
<span className="flex items-center gap-1">
|
|
|
{items.map(item => <span key={item} className="flex items-center gap-1">{itemEmojis[item]} {item}</span>)}
|
|
|
</span>
|
|
|
<span className="text-xs font-mono text-accent">{support.toFixed(0)}%</span>
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
);
|
|
|
|
|
|
return (
|
|
|
<div className="rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm overflow-hidden">
|
|
|
<div className="p-4 border-b border-border/50 flex items-center gap-3">
|
|
|
<div className="w-8 h-8 rounded-lg bg-success/20 flex items-center justify-center"><Icons.Trophy className="w-4 h-4 text-success" /></div>
|
|
|
<div><h2 className="font-semibold text-foreground">Frequent Itemsets Discovered</h2><p className="text-xs text-muted-foreground">Patterns that meet minimum support</p></div>
|
|
|
</div>
|
|
|
<div className="p-4">
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
<div>
|
|
|
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-xs font-mono text-primary">1</span> Single Items</h3>
|
|
|
<div className="space-y-2">{oneItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />)}</div>
|
|
|
</div>
|
|
|
<div>
|
|
|
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-secondary/20 flex items-center justify-center text-xs font-mono text-secondary">2</span> Item Pairs {step < 3 && <span className="text-xs text-muted-foreground">(next)</span>}</h3>
|
|
|
<div className="space-y-2">
|
|
|
{step >= 3 ? twoItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />) : <div className="p-4 rounded-lg border border-dashed border-border/30 text-center"><Icons.TrendingUp className="w-6 h-6 text-muted-foreground/50 mx-auto mb-2" /><p className="text-xs text-muted-foreground">Continue to find pairs</p></div>}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div>
|
|
|
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2"><span className="w-6 h-6 rounded-full bg-accent/20 flex items-center justify-center text-xs font-mono text-accent">3</span> Triplets {step < 4 && <span className="text-xs text-muted-foreground">(final)</span>}</h3>
|
|
|
<div className="space-y-2">
|
|
|
{step >= 4 ? (threeItemsets.length > 0 ? threeItemsets.map((is, i) => <ItemsetCard key={i} {...is} delay={i * 0.05} />) : <div className="p-4 rounded-lg border border-border/30 text-center"><p className="text-xs text-muted-foreground">No 3-itemsets found</p></div>) : <div className="p-4 rounded-lg border border-dashed border-border/30 text-center"><Icons.Trophy className="w-6 h-6 text-muted-foreground/50 mx-auto mb-2" /><p className="text-xs text-muted-foreground">Discover triplets last</p></div>}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const StepExplainer = ({ step, minSupport, minSupportCount }) => {
|
|
|
const stepContent = [
|
|
|
{ title: "Step 1: Look at Receipts ๐งพ", simple: "Transactions", description: "Each row is one customer's shopping basket. Goal: find patterns.", color: "primary" },
|
|
|
{ title: "Step 2: Flip the View ๐", simple: "Vertical Format", description: "Convert 'Receipt โ Items' to 'Item โ Receipt Numbers'. This is key to Eclat!", color: "primary" },
|
|
|
{ title: "Step 3: Check Popularity โญ", simple: "Filter by Support", description: `Remove rare items. Must appear in at least ${minSupportCount} receipts (${minSupport}%).`, color: "secondary" },
|
|
|
{ title: "Step 4: Find Pairs ๐", simple: "Intersections", description: "Find overlapping receipt numbers. Overlap = Bought together.", color: "accent" },
|
|
|
{ title: "Step 5: Bigger Patterns ๐", simple: "3+ Items", description: "Intersect pairs to find triplets. Keep going until no overlaps remain.", color: "success" }
|
|
|
];
|
|
|
const content = stepContent[step] || stepContent[0];
|
|
|
|
|
|
return (
|
|
|
<AnimatePresence mode="wait">
|
|
|
<motion.div
|
|
|
key={step}
|
|
|
initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 10 }} transition={{ duration: 0.3 }}
|
|
|
className={`p-5 rounded-xl border backdrop-blur-sm bg-${content.color}/5 border-${content.color}/30`}
|
|
|
>
|
|
|
<div className="flex items-center gap-4">
|
|
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center text-xl font-bold bg-${content.color}/20 text-${content.color}`}>{step + 1}</div>
|
|
|
<div>
|
|
|
<h3 className="font-bold text-foreground text-lg">{content.title}</h3>
|
|
|
<p className="text-sm text-foreground/80">{content.description}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
</AnimatePresence>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const IntroTutorial = ({ onStart }) => {
|
|
|
const [page, setPage] = useState(0);
|
|
|
const pages = [
|
|
|
{ title: "Welcome to Eclat! ๐", content: <div className="space-y-4"><p>Imagine owning a store. You want to know: "If someone buys Bread, do they also buy Milk?" Eclat finds these patterns automatically using math!</p></div> },
|
|
|
{ title: "How Eclat Works ๐", content: <div className="space-y-4"><p>Most algorithms scan receipts one by one (slow). Eclat uses a trick: it lists <strong>Transaction IDs (TIDs)</strong> for each item. Then it just compares lists of numbers!</p></div> },
|
|
|
{ title: "Let's Simluate! ๐", content: <div className="space-y-4"><p>We have 6 receipts. You can control the speed, hover over items to see connections, and adjust the 'Support' threshold. Ready?</p></div> }
|
|
|
];
|
|
|
|
|
|
return (
|
|
|
<div className="fixed inset-0 bg-background/90 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
|
|
<motion.div initial={{ scale: 0.9 }} animate={{ scale: 1 }} className="max-w-md w-full bg-card border border-border rounded-xl p-6 shadow-2xl">
|
|
|
<div className="text-center mb-6">
|
|
|
<h2 className="text-2xl font-bold text-primary mb-2">{pages[page].title}</h2>
|
|
|
<div className="text-muted-foreground">{pages[page].content}</div>
|
|
|
</div>
|
|
|
<div className="flex justify-between items-center">
|
|
|
<div className="flex gap-1">{pages.map((_, i) => <div key={i} className={`w-2 h-2 rounded-full ${i===page?'bg-primary':'bg-muted'}`} />)}</div>
|
|
|
{page < pages.length - 1 ? <Button onClick={() => setPage(p => p+1)}>Next <Icons.ChevronRight className="w-4 h-4 ml-1"/></Button> : <Button onClick={onStart}>Start <Icons.Play className="w-4 h-4 ml-1"/></Button>}
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const GlossaryPanel = ({ isOpen, onClose }) => {
|
|
|
const terms = [
|
|
|
{ t: "Transaction", d: "A single shopping receipt (e.g., T1)" },
|
|
|
{ t: "TID", d: "Transaction ID (the number of the receipt)" },
|
|
|
{ t: "Support", d: "How frequently an item appears (%)" },
|
|
|
{ t: "Itemset", d: "A collection of one or more items" },
|
|
|
{ t: "Intersection", d: "Finding common numbers in two lists" }
|
|
|
];
|
|
|
|
|
|
return (
|
|
|
<AnimatePresence>
|
|
|
{isOpen && (
|
|
|
<>
|
|
|
<div className="fixed inset-0 bg-background/50 z-40" onClick={onClose} />
|
|
|
<motion.div initial={{ x: '100%' }} animate={{ x: 0 }} exit={{ x: '100%' }} className="fixed right-0 top-0 h-full w-80 bg-card border-l border-border z-50 p-6 shadow-xl">
|
|
|
<div className="flex justify-between items-center mb-6">
|
|
|
<h2 className="font-bold text-lg">Glossary</h2>
|
|
|
<button onClick={onClose}><Icons.X className="w-5 h-5" /></button>
|
|
|
</div>
|
|
|
<div className="space-y-4">
|
|
|
{terms.map((item, i) => (
|
|
|
<div key={i} className="p-3 bg-muted/20 rounded-lg">
|
|
|
<div className="font-bold text-primary text-sm">{item.t}</div>
|
|
|
<div className="text-sm text-muted-foreground">{item.d}</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
</>
|
|
|
)}
|
|
|
</AnimatePresence>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const TheoryPanel = () => {
|
|
|
const playSound = () => {
|
|
|
const audio = document.getElementById('clickSound');
|
|
|
if (audio) {
|
|
|
audio.currentTime = 0;
|
|
|
audio.play();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<section className="mt-8 p-6 rounded-xl bg-card/80 border border-border/50 backdrop-blur-sm relative">
|
|
|
<div className="flex items-center gap-3 mb-6">
|
|
|
<div className="w-10 h-10 rounded-lg bg-indigo-500/20 flex items-center justify-center">
|
|
|
<Icons.BookOpen className="w-5 h-5 text-indigo-400" />
|
|
|
</div>
|
|
|
<div>
|
|
|
<h2 className="text-xl font-bold text-foreground">Theoretical Background</h2>
|
|
|
<p className="text-sm text-muted-foreground">Understanding the core concepts behind Eclat</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div className="grid md:grid-cols-2 gap-8 mb-12">
|
|
|
<div className="space-y-4">
|
|
|
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
|
|
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
|
|
1. Vertical Data Format
|
|
|
</h3>
|
|
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
|
Traditional algorithms like Apriori use a <strong>Horizontal</strong> layout (Transaction ID โ List of Items).
|
|
|
Eclat flips this to a <strong>Vertical</strong> layout (Item โ List of Transaction IDs).
|
|
|
This transformation is the key to its speed, as it avoids repeatedly scanning the entire database.
|
|
|
</p>
|
|
|
</div>
|
|
|
|
|
|
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
|
|
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
|
|
2. Set Intersection
|
|
|
</h3>
|
|
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
|
To calculate the support of a new itemset (e.g., <span className="font-mono text-accent">{"{A, B}"}</span>), Eclat simply calculates the intersection of their TID sets:
|
|
|
<br/>
|
|
|
<code className="bg-background/50 px-1 rounded text-xs mt-1 block w-fit">TID(A) โฉ TID(B) = TID(A,B)</code>
|
|
|
The size of this resulting set is the support count.
|
|
|
</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
|
|
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
|
|
3. Depth-First Search (DFS)
|
|
|
</h3>
|
|
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
|
Unlike Apriori which uses Breadth-First Search (finding all pairs, then all triplets), Eclat uses <strong>Depth-First Search</strong>.
|
|
|
It creates long patterns quickly by extending a specific prefix (e.g., A -> AB -> ABC) before backtracking.
|
|
|
</p>
|
|
|
</div>
|
|
|
|
|
|
<div className="p-4 rounded-lg bg-muted/20 border border-border/30">
|
|
|
<h3 className="font-semibold text-primary mb-2 flex items-center gap-2">
|
|
|
4. Advantages
|
|
|
</h3>
|
|
|
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
|
|
|
<li><strong>Speed:</strong> Generally faster than Apriori for dense datasets.</li>
|
|
|
<li><strong>Memory:</strong> Does not need to generate candidate sets explicitly.</li>
|
|
|
<li><strong>Efficiency:</strong> Intersection operations are computationally cheap on modern CPUs.</li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* Centered Button with Fixes */}
|
|
|
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center">
|
|
|
<audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio>
|
|
|
<a
|
|
|
href="/eclat"
|
|
|
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-2 px-6 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"
|
|
|
>
|
|
|
Back to Core
|
|
|
</a>
|
|
|
</div>
|
|
|
</section>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const SAMPLE_TRANSACTIONS = [
|
|
|
{ id: 1, items: ['Bread', 'Milk', 'Eggs'] },
|
|
|
{ id: 2, items: ['Bread', 'Butter'] },
|
|
|
{ id: 3, items: ['Milk', 'Butter', 'Eggs'] },
|
|
|
{ id: 4, items: ['Bread', 'Milk', 'Butter'] },
|
|
|
{ id: 5, items: ['Bread', 'Milk', 'Eggs', 'Butter'] },
|
|
|
{ id: 6, items: ['Milk', 'Eggs'] },
|
|
|
];
|
|
|
const ITEMS = ['Bread', 'Milk', 'Eggs', 'Butter'];
|
|
|
|
|
|
const EclatSimulation = () => {
|
|
|
const [showTutorial, setShowTutorial] = useState(true);
|
|
|
const [showGlossary, setShowGlossary] = useState(false);
|
|
|
const [step, setStep] = useState(0);
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
const [minSupport, setMinSupport] = useState(50);
|
|
|
const [tidSets, setTidSets] = useState([]);
|
|
|
const [frequentItemsets, setFrequentItemsets] = useState([]);
|
|
|
const [highlightedTids, setHighlightedTids] = useState([]);
|
|
|
const [highlightedItem, setHighlightedItem] = useState(null);
|
|
|
|
|
|
const totalTransactions = SAMPLE_TRANSACTIONS.length;
|
|
|
const minSupportCount = Math.ceil((minSupport / 100) * totalTransactions);
|
|
|
|
|
|
|
|
|
const buildTidSets = () => ITEMS.map(item => ({
|
|
|
item, tids: SAMPLE_TRANSACTIONS.filter(t => t.items.includes(item)).map(t => t.id)
|
|
|
}));
|
|
|
|
|
|
const intersect = (t1, t2) => t1.filter(x => t2.includes(x));
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (step >= 1) setTidSets(buildTidSets());
|
|
|
}, [step]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (!isPlaying) return;
|
|
|
const timer = setTimeout(() => {
|
|
|
if (step < 4) setStep(s => s + 1);
|
|
|
else setIsPlaying(false);
|
|
|
}, 3000);
|
|
|
return () => clearTimeout(timer);
|
|
|
}, [isPlaying, step]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (step >= 2) {
|
|
|
const currentTidSets = buildTidSets();
|
|
|
const frequent = [];
|
|
|
|
|
|
|
|
|
currentTidSets.forEach(ts => {
|
|
|
if (ts.tids.length >= minSupportCount) frequent.push({ items: [ts.item], tids: ts.tids, support: (ts.tids.length / totalTransactions) * 100 });
|
|
|
});
|
|
|
|
|
|
|
|
|
if (step >= 3) {
|
|
|
const freqItems = frequent.map(f => f.items[0]);
|
|
|
for(let i=0; i<freqItems.length; i++) {
|
|
|
for(let j=i+1; j<freqItems.length; j++) {
|
|
|
const t1 = currentTidSets.find(t=>t.item===freqItems[i]).tids;
|
|
|
const t2 = currentTidSets.find(t=>t.item===freqItems[j]).tids;
|
|
|
const inter = intersect(t1, t2);
|
|
|
if(inter.length >= minSupportCount) frequent.push({ items: [freqItems[i], freqItems[j]], tids: inter, support: (inter.length/totalTransactions)*100 });
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (step >= 4) {
|
|
|
const pairs = frequent.filter(f => f.items.length === 2);
|
|
|
for(let i=0; i<pairs.length; i++) {
|
|
|
for(let j=i+1; j<pairs.length; j++) {
|
|
|
const combined = [...new Set([...pairs[i].items, ...pairs[j].items])];
|
|
|
if(combined.length === 3) {
|
|
|
const inter = intersect(pairs[i].tids, pairs[j].tids);
|
|
|
if(inter.length >= minSupportCount && !frequent.some(f=>f.items.length===3 && f.items.every(it=>combined.includes(it)))) {
|
|
|
frequent.push({ items: combined, tids: inter, support: (inter.length/totalTransactions)*100 });
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
setFrequentItemsets(frequent);
|
|
|
}
|
|
|
}, [step, minSupport, minSupportCount]);
|
|
|
|
|
|
const handleHover = (item, tids = []) => {
|
|
|
setHighlightedItem(item);
|
|
|
setHighlightedTids(tids);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<div className="min-h-screen bg-background grid-bg circuit-pattern pb-20">
|
|
|
{showTutorial && <IntroTutorial onStart={() => setShowTutorial(false)} />}
|
|
|
<GlossaryPanel isOpen={showGlossary} onClose={() => setShowGlossary(false)} />
|
|
|
|
|
|
<header className="border-b border-border/50 backdrop-blur-sm bg-background/80 sticky top-0 z-30">
|
|
|
<div className="container mx-auto px-4 py-4 flex flex-wrap items-center justify-between gap-4">
|
|
|
<div className="flex items-center gap-3">
|
|
|
<div className="w-10 h-10 rounded-lg bg-primary/20 flex items-center justify-center glow-primary"><Icons.Database className="w-5 h-5 text-primary"/></div>
|
|
|
<div><h1 className="text-xl font-bold text-foreground text-glow">Eclat Algorithm</h1><p className="text-xs text-muted-foreground">Interactive Learning Simulation</p></div>
|
|
|
</div>
|
|
|
<div className="flex items-center gap-3">
|
|
|
<Button variant="outline" size="sm" onClick={() => setShowGlossary(true)} className="gap-2"><Icons.HelpCircle className="w-4 h-4"/> Glossary</Button>
|
|
|
<Button variant="ghost" size="sm" onClick={() => setShowTutorial(true)} className="gap-2"><Icons.RotateCw className="w-4 h-4"/> Tutorial</Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<main className="container mx-auto px-4 py-6">
|
|
|
<motion.div initial={{opacity:0, y:20}} animate={{opacity:1, y:0}} className="mb-6 p-4 rounded-xl bg-card/50 border border-border/50 backdrop-blur-sm">
|
|
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
|
<Button onClick={() => setIsPlaying(!isPlaying)} className="gap-2" variant={isPlaying ? "secondary" : "default"}>
|
|
|
{isPlaying ? <Icons.Pause className="w-4 h-4"/> : <Icons.Play className="w-4 h-4"/>} {isPlaying ? 'Pause' : 'Play'}
|
|
|
</Button>
|
|
|
<Button onClick={() => step < 4 && setStep(s => s+1)} variant="outline" disabled={step >= 4} className="gap-2">
|
|
|
<Icons.ChevronRight className="w-4 h-4"/> Next
|
|
|
</Button>
|
|
|
<Button onClick={() => { setStep(0); setIsPlaying(false); }} variant="ghost" className="gap-2"><Icons.RotateCcw className="w-4 h-4"/> Reset</Button>
|
|
|
</div>
|
|
|
<div className="flex items-center gap-4 w-full md:w-auto">
|
|
|
<span className="text-sm text-muted-foreground whitespace-nowrap">Support: {minSupport}%</span>
|
|
|
<Slider value={[minSupport]} onValueChange={([v]) => setMinSupport(v)} min={20} max={80} step={10} className="w-full md:w-32"/>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
|
{['Transactions', 'Build TID Sets', 'Frequent 1-Items', '2-Item Intersections', '3-Item Patterns'].map((label, i) => (
|
|
|
<div key={i} className="flex items-center gap-2">
|
|
|
<motion.div animate={step === i ? { scale: [1, 1.1, 1] } : {}} className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all ${step >= i ? 'bg-primary text-primary-foreground glow-primary' : 'bg-muted text-muted-foreground'}`}>{i + 1}</motion.div>
|
|
|
<span className={`text-xs hidden sm:block ${step >= i ? 'text-foreground' : 'text-muted-foreground'}`}>{label}</span>
|
|
|
{i < 4 && <Icons.ChevronRight className="w-4 h-4 text-muted-foreground hidden sm:block"/>}
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
|
|
|
<StepExplainer step={step} minSupport={minSupport} minSupportCount={minSupportCount} />
|
|
|
|
|
|
{step === 0 && <div className="mt-4"><BeginnerTip title="๐ New to this?" defaultOpen={true}><p>Think of this like looking at <strong>grocery store receipts</strong>. Each row (T1, T2...) is one customer's shopping basket.</p></BeginnerTip></div>}
|
|
|
{step === 1 && <div className="mt-4"><BeginnerTip title="๐ Why did we flip the data?"><p><strong>Before:</strong> "Receipt 1 has Bread" (horizontal). <strong>After:</strong> "Bread is in Receipt 1" (vertical). This makes finding pairs faster!</p></BeginnerTip></div>}
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
|
|
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
|
|
|
<TransactionPanel transactions={SAMPLE_TRANSACTIONS} highlightedTids={highlightedTids} step={step} />
|
|
|
</motion.div>
|
|
|
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }}>
|
|
|
<TidSetPanel tidSets={tidSets} step={step} minSupportCount={minSupportCount} onItemHover={handleHover} highlightedItem={highlightedItem} />
|
|
|
</motion.div>
|
|
|
</div>
|
|
|
|
|
|
<AnimatePresence>
|
|
|
{step >= 3 && (
|
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mt-6">
|
|
|
<IntersectionVisualizer tidSets={tidSets} minSupportCount={minSupportCount} step={step} />
|
|
|
</motion.div>
|
|
|
)}
|
|
|
</AnimatePresence>
|
|
|
|
|
|
<AnimatePresence>
|
|
|
{step >= 2 && (
|
|
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mt-6">
|
|
|
<FrequentItemsetsPanel itemsets={frequentItemsets} step={step} onItemsetHover={handleHover} />
|
|
|
</motion.div>
|
|
|
)}
|
|
|
</AnimatePresence>
|
|
|
|
|
|
<TheoryPanel />
|
|
|
</main>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
|
root.render(<EclatSimulation />);
|
|
|
</script>
|
|
|
{% endraw %}
|
|
|
</body>
|
|
|
</html> |