<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Language Learning - Hierarchical View</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Monaco', 'Consolas', monospace;
background: #0a0a0f;
color: #e0e0e0;
overflow: hidden;
}
#container {
display: flex;
height: 100vh;
}
#canvas-container {
flex: 1;
position: relative;
}
#sidebar {
width: 280px;
background: #12121a;
padding: 20px;
overflow-y: auto;
border-left: 1px solid #333;
}
h1 { font-size: 14px; margin-bottom: 20px; color: #888; }
h2 { font-size: 12px; margin: 15px 0 10px; color: #666; text-transform: uppercase; }
.stat {
display: flex;
justify-content: space-between;
padding: 5px 0;
font-size: 12px;
}
.stat-value { color: #4ecdc4; }
.view-toggle {
display: flex;
gap: 5px;
margin-bottom: 15px;
}
.view-toggle a {
flex: 1;
padding: 8px;
text-align: center;
background: #1a1a25;
color: #888;
text-decoration: none;
border-radius: 4px;
font-size: 11px;
}
.view-toggle a.active {
background: #2a2a35;
color: #4ecdc4;
}
.view-toggle a:hover {
background: #252535;
}
.info-box {
background: #1a1a25;
padding: 10px;
border-radius: 4px;
font-size: 11px;
margin: 10px 0;
color: #888;
}
.info-box strong {
color: #4ecdc4;
}
#tooltip {
position: absolute;
background: rgba(0,0,0,0.9);
border: 1px solid #4ecdc4;
padding: 10px;
font-size: 11px;
pointer-events: none;
display: none;
max-width: 250px;
z-index: 100;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #4ecdc4;
}
canvas { display: block; }
.controls {
margin: 10px 0;
}
.controls button {
padding: 6px 12px;
margin: 2px;
background: #1a1a25;
color: #e0e0e0;
border: 1px solid #333;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
}
.controls button:hover {
background: #252535;
}
.slider-container {
margin: 10px 0;
}
.slider-container label {
display: block;
font-size: 11px;
color: #888;
margin-bottom: 5px;
}
.hint {
font-size: 9px;
color: #555;
margin-top: 4px;
}
</style>
</head>
<body>
<div id="container">
<div id="canvas-container">
<canvas id="canvas"></canvas>
<div id="tooltip"></div>
<div id="loading">Loading hierarchy data...</div>
</div>
<div id="sidebar">
<h1>HIERARCHICAL VISUALIZER</h1>
<div class="view-toggle">
<a href="index.html">3D Graph</a>
<a href="index-hierarchy.html" class="active">Hierarchy</a>
</div>
<h2>Statistics</h2>
<div class="stat"><span>Words:</span><span class="stat-value" id="stat-words">0</span></div>
<div class="stat"><span>Pairs:</span><span class="stat-value" id="stat-pairs">0</span></div>
<div class="stat"><span>Clusters:</span><span class="stat-value" id="stat-clusters">0</span></div>
<div class="stat"><span>Level:</span><span class="stat-value" id="stat-level">0</span></div>
<div class="stat"><span>Avg Surprise:</span><span class="stat-value" id="stat-surprise">0</span></div>
<h2>Navigation</h2>
<div class="controls">
<button onclick="zoomOut()">Zoom Out</button>
<button onclick="zoomTo(hierarchy)">Reset View</button>
</div>
<div class="info-box">
<strong>Click</strong> a circle to zoom in<br>
<strong>Right-click</strong> to zoom out<br>
<strong>Scroll</strong> to zoom in/out<br>
<strong>Hover</strong> for details
</div>
<h2>Recursive Structure</h2>
<div class="info-box">
The same learning process happens at each level:
<br><br>
<strong>Level 0:</strong> Words<br>
<strong>Level 1:</strong> Word Clusters<br>
<strong>Level 2:</strong> Super-Clusters<br>
<strong>Level N:</strong> Meta-Structures
<br><br>
Each level applies: COUNT → STRUCTURE → CLUSTER
</div>
<h2>Time Evolution</h2>
<div class="slider-container">
<label>Iteration: <span id="iteration-value">All</span></label>
<input type="range" id="iteration-slider" min="0" max="100" value="100" style="width: 100%;">
<div class="hint">Slide to see hierarchy at different learning stages</div>
</div>
<h2>Current View</h2>
<div id="current-view" class="info-box">
Viewing: <strong>All Structures</strong><br>
Level: <strong>-1</strong><br>
Children: <strong>0</strong>
</div>
</div>
</div>
<script src="viz-hierarchy.js"></script>
<script>
// Update current view display
const originalZoomTo = zoomTo;
zoomTo = function(node) {
originalZoomTo(node);
updateCurrentView(node);
};
function updateCurrentView(node) {
if (!node) return;
const div = document.getElementById('current-view');
let html = `Viewing: <strong>${node.name || node.id}</strong><br>`;
html += `Level: <strong>${node.level}</strong><br>`;
html += `Children: <strong>${node.children?.length || 0}</strong>`;
if (node.data?.iteration !== undefined) {
html += `<br>Formed at iteration: <strong>${node.data.iteration}</strong>`;
}
if (node.data?.similarity !== undefined && node.data.similarity > 0) {
html += `<br>Similarity: <strong>${node.data.similarity.toFixed(3)}</strong>`;
}
div.innerHTML = html;
}
// Iteration timeline slider
let maxIteration = 100;
// Override loadData to set up timeline
const originalLoadData = loadData;
loadData = async function() {
await originalLoadData();
// Set max iteration from data
if (hierarchy && hierarchy.data && hierarchy.data.totalIterations) {
maxIteration = hierarchy.data.totalIterations;
} else if (data && data.stats && data.stats.clusterIterations) {
maxIteration = data.stats.clusterIterations;
}
const slider = document.getElementById('iteration-slider');
slider.max = maxIteration;
slider.value = maxIteration;
document.getElementById('iteration-value').textContent = 'All';
};
document.getElementById('iteration-slider').addEventListener('input', (e) => {
const iter = parseInt(e.target.value);
if (iter >= maxIteration) {
document.getElementById('iteration-value').textContent = 'All';
// Show all clusters
filterByIteration(Infinity);
} else {
document.getElementById('iteration-value').textContent = iter;
// Filter to show only clusters formed by this iteration
filterByIteration(iter);
}
});
function filterByIteration(maxIter) {
// Rebuild hierarchy with only clusters formed by maxIter
if (!data) return;
// Rebuild and repack
hierarchy = buildHierarchyFiltered(data, maxIter);
setParentRefs(hierarchy, null);
const size = Math.min(canvas.width, canvas.height) * 0.45;
packCircles(hierarchy, canvas.width / 2, canvas.height / 2, size);
// Reset view
viewX = canvas.width / 2;
viewY = canvas.height / 2;
viewScale = 1;
targetX = viewX;
targetY = viewY;
targetScale = 1;
currentView = hierarchy;
}
function buildHierarchyFiltered(data, maxIter) {
// Similar to buildHierarchy but filters by iteration
const root = {
id: 'root',
name: `Structures (iter ≤ ${maxIter === Infinity ? 'all' : maxIter})`,
level: -1,
children: [],
data: { type: 'root' }
};
const words = data.words || {};
const hierarchy = data.hierarchy || {};
const merges = (hierarchy.merges || []).filter(m => m.iteration <= maxIter);
// Get clusters that exist at this iteration
const validClusterIds = new Set(merges.map(m => m.clusterId));
// Create word nodes
const wordNodes = new Map();
for (const [word, info] of Object.entries(words)) {
wordNodes.set(word, {
id: `word_${word}`,
name: word,
level: 0,
children: [],
data: {
type: 'word',
word: word,
surprise: info.surprise || 0,
count: info.count || 1,
degree: info.degree || 0,
totalMI: info.totalMI || 0
}
});
}
// Build clusters from filtered merges
const clusterNodes = new Map();
const wordToCluster = new Map();
for (const merge of merges) {
const clusterId = merge.clusterId;
if (!clusterNodes.has(clusterId)) {
clusterNodes.set(clusterId, {
id: `cluster_${clusterId}`,
name: `${merge.entity1} + ${merge.entity2}`,
level: 1,
children: [],
data: {
type: 'cluster',
clusterId: clusterId,
iteration: merge.iteration,
similarity: merge.similarity
}
});
}
// Add entities to cluster
for (const entity of [merge.entity1, merge.entity2]) {
if (wordNodes.has(entity) && !wordToCluster.has(entity)) {
clusterNodes.get(clusterId).children.push(wordNodes.get(entity));
wordToCluster.set(entity, clusterId);
}
}
}
// Add clusters
const clusters = Array.from(clusterNodes.values());
clusters.sort((a, b) => (a.data.iteration || 0) - (b.data.iteration || 0));
root.children.push(...clusters);
// Add unclustered words
const orphans = [];
for (const [word, node] of wordNodes) {
if (!wordToCluster.has(word)) {
orphans.push(node);
}
}
if (orphans.length > 0) {
root.children.push({
id: 'unclustered',
name: 'Unclustered',
level: 1,
children: orphans,
data: { type: 'unclustered', memberCount: orphans.length }
});
}
return root;
}
</script>
</body>
</html>