CSNET's picture
รู้สึกว่าเส้นลาก ซ้าย ขวา บน node มันจะสลับกันนะ ช่วยแก้ให้หน่อย
39f2917 verified
Raw
History Blame Contribute Delete
16.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Binary Bloom Tree Explorer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--secondary: #3b82f6;
--secondary-dark: #1d4ed8;
--bg: #f3f4f6;
--surface: #ffffff;
--text: #1f2937;
--text-muted: #6b7280;
--line: #d1d5db;
--danger: #ef4444;
--success: #22c55e;
--warning: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg);
color: var(--text);
overflow-x: hidden;
}
header {
background: var(--surface);
box-shadow: 0 2px 8px rgba(0,0,0,.05);
padding: 1rem 2rem;
display: flex;
align-items: center;
gap: 1rem;
position: sticky;
top: 0;
z-index: 10;
}
header h1 {
font-size: 1.5rem;
font-weight: 600;
color: var(--primary-dark);
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
margin-left: auto;
}
.controls button {
border: none;
background: var(--primary);
color: #fff;
padding: .5rem 1rem;
border-radius: .5rem;
font-size: .875rem;
cursor: pointer;
transition: .2s;
}
.controls button:hover {
background: var(--primary-dark);
}
.controls .zoom {
display: flex;
align-items: center;
gap: .5rem;
font-size: .875rem;
}
.controls .zoom span {
width: 2rem;
text-align: center;
font-weight: 600;
}
.tree-wrapper {
margin: 2rem;
overflow-x: auto;
padding-bottom: 2rem;
}
.tree {
display: inline-block;
min-width: 100%;
padding: 1rem;
transform-origin: top left;
transition: transform .3s;
}
.node {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding: 1rem 0;
}
.node-content {
position: relative;
background: var(--surface);
padding: .75rem;
border-radius: 1rem;
box-shadow: 0 4px 12px rgba(0,0,0,.08);
width: 240px;
display: flex;
flex-direction: column;
align-items: center;
gap: .5rem;
transition: .2s;
z-index: 2;
}
.node-content:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,.12);
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--primary);
}
.info {
text-align: center;
}
.info .name {
font-weight: 600;
font-size: 1rem;
}
.info .rank {
font-size: .75rem;
color: var(--text-muted);
margin-bottom: .25rem;
}
.scores {
display: flex;
justify-content: space-between;
width: 100%;
gap: .5rem;
}
.score {
flex: 1;
background: var(--bg);
border-radius: .5rem;
padding: .5rem;
text-align: center;
font-size: .75rem;
}
.score span {
font-weight: 700;
color: var(--primary-dark);
}
.controls-node {
position: absolute;
top: -.5rem;
right: -.5rem;
display: flex;
gap: .25rem;
}
.controls-node button {
background: var(--primary);
color: #fff;
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
font-size: .75rem;
display: flex;
align-items: center;
justify-content: center;
transition: .2s;
}
.controls-node button:hover {
background: var(--primary-dark);
}
.children {
display: flex;
justify-content: center;
position: relative;
margin-top: 2rem;
}
.children::before {
content: "";
position: absolute;
top: -1rem;
left: 50%;
width: 2px;
height: 1rem;
background: var(--line);
}
.children .node {
position: relative;
margin: 0 1rem;
}
.children .node::before {
content: "";
position: absolute;
top: -1rem;
left: 50%;
width: 2px;
height: 1rem;
background: var(--line);
}
.children .node:first-child::after,
.children .node:last-child::after {
content: "";
position: absolute;
top: -1rem;
width: 50%;
height: 2px;
background: var(--line);
}
.children .node:first-child::after {
right: 50%;
transform-origin: right;
}
.children .node:last-child::after {
left: 50%;
transform-origin: left;
}
.hiddenSubtree > .children {
display: none;
}
.tooltip {
position: absolute;
top: 110%;
background: var(--surface);
font-size: .75rem;
padding: .5rem .75rem;
border-radius: .5rem;
box-shadow: 0 4px 12px rgba(0,0,0,.15);
pointer-events: none;
opacity: 0;
transform: translateY(-4px);
transition: .2s;
z-index: 3;
white-space: nowrap;
}
.node-content:hover .tooltip {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 600px) {
.node-content {
width: 160px;
}
.avatar {
width: 48px;
height: 48px;
}
.scores {
flex-direction: column;
gap: .25rem;
}
}
</style>
</head>
<body>
<header>
<h1>Binary Bloom Tree Explorer</h1>
<div class="controls">
<button id="expandAll">Expand All</button>
<button id="collapseAll">Collapse All</button>
<div class="zoom">
<button id="zoomOut">-</button>
<span id="zoomLevel">100%</span>
<button id="zoomIn">+</button>
</div>
</div>
</header>
<div class="tree-wrapper">
<div id="tree" class="tree"></div>
</div>
<script>
const mockData = [
{ id: 1, parentId: null, side: null, name: "Natasha Romanoff", position: "Crown Diamond", avatarUrl: "https://i.pravatar.cc/150?img=47", scoreLeft: 125000, scoreRight: 132000, isExpanded: true, details: "Joined: 2020-03-15, Country: USA" },
{ id: 2, parentId: 1, side: "L", name: "Steve Rogers", position: "Diamond", avatarUrl: "https://i.pravatar.cc/150?img=32", scoreLeft: 48000, scoreRight: 55000, isExpanded: true, details: "Joined: 2021-01-05, Country: USA" },
{ id: 3, parentId: 1, side: "R", name: "Tony Stark", position: "Ruby", avatarUrl: "https://i.pravatar.cc/150?img=11", scoreLeft: 39000, scoreRight: 42000, isExpanded: true, details: "Joined: 2021-02-18, Country: USA" },
{ id: 4, parentId: 2, side: "L", name: "Bruce Banner", position: "Emerald", avatarUrl: "https://i.pravatar.cc/150?img=28", scoreLeft: 21000, scoreRight: 19500, isExpanded: true, details: "Joined: 2021-09-10, Country: USA" },
{ id: 5, parentId: 2, side: "R", name: "Clint Barton", position: "Sapphire", avatarUrl: "https://i.pravatar.cc/150?img=15", scoreLeft: 18000, scoreRight: 22000, isExpanded: false, details: "Joined: 2022-03-22, Country: USA" },
{ id: 6, parentId: 3, side: "L", name: "Peter Parker", position: "Platinum", avatarUrl: "https://i.pravatar.cc/150?img=54", scoreLeft: 15000, scoreRight: 16500, isExpanded: true, details: "Joined: 2022-05-14, Country: USA" },
{ id: 7, parentId: 3, side: "R", name: "Carol Danvers", position: "Gold", avatarUrl: "https://i.pravatar.cc/150?img=44", scoreLeft: 12000, scoreRight: 13500, isExpanded: false, details: "Joined: 2022-07-09, Country: USA" },
{ id: 8, parentId: 4, side: "L", name: "T'Challa", position: "Silver", avatarUrl: "https://i.pravatar.cc/150?img=33", scoreLeft: 8000, scoreRight: 7500, isExpanded: true, details: "Joined: 2023-01-30, Country: Wakanda" },
{ id: 9, parentId: 4, side: "R", name: "Wanda Maximoff", position: "Silver", avatarUrl: "https://i.pravatar.cc/150?img=41", scoreLeft: 7000, scoreRight: 8500, isExpanded: false, details: "Joined: 2023-03-11, Country: Sokovia" },
{ id: 10, parentId: 5, side: "L", name: "Sam Wilson", position: "Bronze", avatarUrl: "https://i.pravatar.cc/150?img=52", scoreLeft: 4200, scoreRight: 3800, isExpanded: false, details: "Joined: 2023-06-25, Country: USA" },
{ id: 11, parentId: 5, side: "R", name: "Scott Lang", position: "Bronze", avatarUrl: "https://i.pravatar.cc/150?img=29", scoreLeft: 3600, scoreRight: 4100, isExpanded: false, details: "Joined: 2023-08-03, Country: USA" },
{ id: 12, parentId: 6, side: "L", name: "Hope van Dyne", position: "Bronze", avatarUrl: "https://i.pravatar.cc/150?img=26", scoreLeft: 2200, scoreRight: 1900, isExpanded: false, details: "Joined: 2023-09-14, Country: USA" },
{ id: 13, parentId: 6, side: "R", name: "Shuri", position: "Bronze", avatarUrl: "https://i.pravatar.cc/150?img=48", scoreLeft: 1800, scoreRight: 2100, isExpanded: false, details: "Joined: 2023-10-05, Country: Wakanda" },
{ id: 14, parentId: 7, side: "L", name: "Okoye", position: "Bronze", avatarUrl: "https://i.pravatar.cc/150?img=24", scoreLeft: 1100, scoreRight: 1300, isExpanded: false, details: "Joined: 2023-11-01, Country: Wakanda" },
{ id: 15, parentId: 7, side: "R", name: "M'Baku", position: "Bronze", avatarUrl: "https://i.pravatar.cc/150?img=12", scoreLeft: 900, scoreRight: 1200, isExpanded: false, details: "Joined: 2023-12-12, Country: Wakanda" },
{ id: 16, parentId: 8, side: "L", name: "Nakia", position: "Member", avatarUrl: "https://i.pravatar.cc/150?img=9", scoreLeft: 400, scoreRight: 350, isExpanded: false, details: "Joined: 2024-02-10, Country: Wakanda" },
{ id: 17, parentId: 8, side: "R", name: "Zuri", position: "Member", avatarUrl: "https://i.pravatar.cc/150?img=5", scoreLeft: 300, scoreRight: 400, isExpanded: false, details: "Joined: 2024-03-01, Country: Wakanda" },
{ id: 18, parentId: 9, side: "L", name: "Vision", position: "Member", avatarUrl: "https://i.pravatar.cc/150?img=60", scoreLeft: 250, scoreRight: 200, isExpanded: false, details: "Joined: 2024-03-15, Country: Earth" },
{ id: 19, parentId: 9, side: "R", name: "Pietro Maximoff", position: "Member", avatarUrl: "https://i.pravatar.cc/150?img=22", scoreLeft: 200, scoreRight: 250, isExpanded: false, details: "Joined: 2024-04-01, Country: Sokovia" },
{ id: 20, parentId: 10, side: "L", name: "Maria Hill", position: "Member", avatarUrl: "https://i.pravatar.cc/150?img=17", scoreLeft: 100, scoreRight: 150, isExpanded: false, details: "Joined: 2024-05-01, Country: USA" }
];
const treeContainer = document.getElementById('tree');
let scale = 1;
function buildTree(parentId = null) {
const nodes = mockData.filter(n => n.parentId === parentId);
if (!nodes.length) return null;
const fragment = document.createDocumentFragment();
nodes.forEach(node => {
const div = document.createElement('div');
div.className = `node ${!node.isExpanded ? 'hiddenSubtree' : ''}`;
div.dataset.id = node.id;
div.innerHTML = `
<div class="node-content">
<img class="avatar" src="${node.avatarUrl}" alt="${node.name}">
<div class="info">
<div class="name">${node.name}</div>
<div class="rank">${node.position}</div>
</div>
<div class="scores">
<div class="score">Left<br><span>${node.scoreLeft.toLocaleString()}</span></div>
<div class="score">Right<br><span>${node.scoreRight.toLocaleString()}</span></div>
</div>
<div class="controls-node">
<button class="toggle" title="${node.isExpanded ? 'Collapse' : 'Expand'}">
${node.isExpanded ? '−' : '+'}
</button>
</div>
<div class="tooltip">${node.details}</div>
</div>
<div class="children"></div>
`;
const childrenContainer = div.querySelector('.children');
const children = buildTree(node.id);
if (children) {
childrenContainer.appendChild(children);
} else {
div.querySelector('.toggle').style.display = 'none';
}
fragment.appendChild(div);
});
return fragment;
}
function attachToggleEvents() {
treeContainer.addEventListener('click', e => {
if (e.target.classList.contains('toggle')) {
const nodeEl = e.target.closest('.node');
const isHidden = nodeEl.classList.toggle('hiddenSubtree');
e.target.textContent = isHidden ? '+' : '−';
e.target.title = isHidden ? 'Expand' : 'Collapse';
}
});
}
function expandAll() {
document.querySelectorAll('.node').forEach(n => n.classList.remove('hiddenSubtree'));
document.querySelectorAll('.toggle').forEach(b => {
b.textContent = '−';
b.title = 'Collapse';
});
}
function collapseAll() {
document.querySelectorAll('.node').forEach(n => {
if (n.querySelector('.children').childElementCount) {
n.classList.add('hiddenSubtree');
const btn = n.querySelector('.toggle');
btn.textContent = '+';
btn.title = 'Expand';
}
});
}
function setScale(newScale) {
scale = newScale;
treeContainer.style.transform = `scale(${scale})`;
document.getElementById('zoomLevel').textContent = Math.round(scale * 100) + '%';
}
// Render tree and hook up events
treeContainer.appendChild(buildTree());
attachToggleEvents();
// Zoom controls
document.getElementById('zoomIn').addEventListener('click', () => {
if (scale < 2) setScale(scale + 0.1);
});
document.getElementById('zoomOut').addEventListener('click', () => {
if (scale > 0.5) setScale(scale - 0.1);
});
// Global expand / collapse
document.getElementById('expandAll').addEventListener('click', expandAll);
document.getElementById('collapseAll').addEventListener('click', collapseAll);
// Center the tree on load
setScale(scale);
</script>
</body>
</html>