|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>AE Content-Defined Chunking Visualization</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet"> |
|
<style> |
|
|
|
body { |
|
font-family: 'Inter', sans-serif; |
|
background-color: #f3f4f6; |
|
} |
|
canvas { |
|
background-color: #ffffff; |
|
border-radius: 0.5rem; |
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); |
|
} |
|
|
|
button { |
|
transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; |
|
} |
|
button:hover { |
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); |
|
} |
|
button:active { |
|
transform: translateY(1px); |
|
} |
|
|
|
input[type="range"] { |
|
-webkit-appearance: none; |
|
appearance: none; |
|
width: 100%; |
|
height: 8px; |
|
background: #d1d5db; |
|
border-radius: 9999px; |
|
outline: none; |
|
} |
|
input[type="range"]::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
appearance: none; |
|
width: 16px; |
|
height: 16px; |
|
background: #3b82f6; |
|
border-radius: 9999px; |
|
cursor: pointer; |
|
} |
|
input[type="range"]::-moz-range-thumb { |
|
width: 16px; |
|
height: 16px; |
|
background: #3b82f6; |
|
border-radius: 9999px; |
|
cursor: pointer; |
|
border: none; |
|
} |
|
|
|
#explanation { |
|
background-color: #e0f2fe; |
|
border-left: 4px solid #0ea5e9; |
|
color: #0369a1; |
|
} |
|
</style> |
|
</head> |
|
<body class="p-4 md:p-8 bg-gray-100"> |
|
|
|
<div class="max-w-6xl mx-auto bg-white p-6 rounded-lg shadow-lg"> |
|
<h1 class="text-2xl md:text-3xl font-bold mb-4 text-center text-gray-800">Asymmetric Extremum (AE) CDC Visualization</h1> |
|
<p class="text-sm text-gray-600 mb-6 text-center">Based on the paper by Zhang et al. (INFOCOM 2015). This visualization simplifies the 'value' to be the direct byte value (S=1).</p> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 items-center bg-gray-50 p-4 rounded-lg border border-gray-200"> |
|
<div class="flex flex-wrap justify-center md:justify-start gap-2"> |
|
<button id="startBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-md shadow">Start</button> |
|
<button id="pauseBtn" class="bg-yellow-500 hover:bg-yellow-600 text-white font-semibold py-2 px-4 rounded-md shadow" disabled>Pause</button> |
|
<button id="stepBtn" class="bg-green-500 hover:bg-green-600 text-white font-semibold py-2 px-4 rounded-md shadow">Step</button> |
|
<button id="resetBtn" class="bg-red-500 hover:bg-red-600 text-white font-semibold py-2 px-4 rounded-md shadow">Reset</button> |
|
</div> |
|
|
|
<div class="flex flex-col items-center gap-2"> |
|
<div class="w-full max-w-xs"> |
|
<label for="windowSize" class="block text-sm font-medium text-gray-700 text-center">Window Size (w): <span id="windowSizeValue">8</span></label> |
|
<input type="range" id="windowSize" min="2" max="20" value="8" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
</div> |
|
<div class="w-full max-w-xs"> |
|
<label for="dataSize" class="block text-sm font-medium text-gray-700 text-center">Data Size: <span id="dataSizeValue">100</span></label> |
|
<input type="range" id="dataSize" min="20" max="300" value="100" step="10" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
</div> |
|
</div> |
|
|
|
<div class="flex flex-col items-center md:items-end gap-2"> |
|
<div class="w-full max-w-xs"> |
|
<label for="speed" class="block text-sm font-medium text-gray-700 text-center md:text-right">Animation Speed: <span id="speedValue">500</span> ms</label> |
|
<input type="range" id="speed" min="50" max="2000" value="500" step="50" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<canvas id="cdcCanvas" width="1000" height="300" class="w-full"></canvas> |
|
</div> |
|
|
|
<div id="explanation" class="p-4 rounded-md text-sm mb-4 border-l-4 border-blue-500 bg-blue-50 text-blue-800"> |
|
Click 'Start' or 'Step' to begin the visualization. The algorithm scans the data stream (represented by bars) to find chunk boundaries (cut-points). |
|
</div> |
|
|
|
<div class="flex flex-wrap justify-center gap-4 text-xs text-gray-600"> |
|
<div class="flex items-center gap-1"><div class="w-3 h-3 rounded-sm bg-gray-400"></div><span>Data Point</span></div> |
|
<div class="flex items-center gap-1"><div class="w-3 h-3 rounded-sm bg-blue-500"></div><span>Current Scan Position (i)</span></div> |
|
<div class="flex items-center gap-1"><div class="w-3 h-3 rounded-sm bg-red-500"></div><span>Potential Max Point (max_position)</span></div> |
|
<div class="flex items-center gap-1"><div class="w-3 h-3 rounded-sm border border-red-500 bg-red-200"></div><span>Right Window (w)</span></div> |
|
<div class="flex items-center gap-1"><div class="w-1 h-4 bg-green-600"></div><span>Cut-Point</span></div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const canvas = document.getElementById('cdcCanvas'); |
|
const ctx = canvas.getContext('2d'); |
|
const dpr = window.devicePixelRatio || 1; |
|
|
|
function resizeCanvas() { |
|
const rect = canvas.getBoundingClientRect(); |
|
canvas.width = rect.width * dpr; |
|
canvas.height = rect.height * dpr; |
|
ctx.scale(dpr, dpr); |
|
draw(); |
|
} |
|
|
|
|
|
const startBtn = document.getElementById('startBtn'); |
|
const pauseBtn = document.getElementById('pauseBtn'); |
|
const stepBtn = document.getElementById('stepBtn'); |
|
const resetBtn = document.getElementById('resetBtn'); |
|
const windowSizeSlider = document.getElementById('windowSize'); |
|
const windowSizeValueSpan = document.getElementById('windowSizeValue'); |
|
const dataSizeSlider = document.getElementById('dataSize'); |
|
const dataSizeValueSpan = document.getElementById('dataSizeValue'); |
|
const speedSlider = document.getElementById('speed'); |
|
const speedValueSpan = document.getElementById('speedValue'); |
|
const explanationDiv = document.getElementById('explanation'); |
|
|
|
|
|
let dataStream = []; |
|
let windowSize = parseInt(windowSizeSlider.value); |
|
let dataSize = parseInt(dataSizeSlider.value); |
|
let animationSpeed = parseInt(speedSlider.value); |
|
let isRunning = false; |
|
let animationFrameId = null; |
|
let timeoutId = null; |
|
|
|
|
|
let currentIndex = 0; |
|
let maxPosition = 0; |
|
let maxValue = -1; |
|
let chunkStart = 0; |
|
let cutPoints = []; |
|
|
|
|
|
function init() { |
|
isRunning = false; |
|
if (animationFrameId) cancelAnimationFrame(animationFrameId); |
|
if (timeoutId) clearTimeout(timeoutId); |
|
animationFrameId = null; |
|
timeoutId = null; |
|
|
|
windowSize = parseInt(windowSizeSlider.value); |
|
dataSize = parseInt(dataSizeSlider.value); |
|
animationSpeed = parseInt(speedSlider.value); |
|
|
|
|
|
dataStream = Array.from({ length: dataSize }, () => Math.floor(Math.random() * 256)); |
|
|
|
|
|
currentIndex = 0; |
|
maxPosition = 0; |
|
maxValue = dataStream.length > 0 ? dataStream[0] : -1; |
|
chunkStart = 0; |
|
cutPoints = []; |
|
|
|
|
|
startBtn.disabled = false; |
|
pauseBtn.disabled = true; |
|
stepBtn.disabled = false; |
|
windowSizeSlider.disabled = false; |
|
dataSizeSlider.disabled = false; |
|
updateExplanation(`Ready. Data Size: ${dataSize}, Window Size (w): ${windowSize}. Click Start or Step.`); |
|
|
|
resizeCanvas(); |
|
} |
|
|
|
|
|
function draw() { |
|
if (!ctx) return; |
|
|
|
const width = canvas.width / dpr; |
|
const height = canvas.height / dpr; |
|
const barWidth = width / dataStream.length; |
|
const maxBarHeight = height * 0.9; |
|
|
|
|
|
ctx.clearRect(0, 0, width, height); |
|
|
|
|
|
dataStream.forEach((value, index) => { |
|
const barHeight = (value / 255) * maxBarHeight; |
|
const x = index * barWidth; |
|
const y = height - barHeight; |
|
|
|
|
|
ctx.fillStyle = '#9ca3af'; |
|
|
|
|
|
if (index === currentIndex && currentIndex < dataStream.length) { |
|
ctx.fillStyle = '#3b82f6'; |
|
} |
|
|
|
|
|
if (index === maxPosition && currentIndex < dataStream.length) { |
|
ctx.fillStyle = '#ef4444'; |
|
} |
|
|
|
|
|
if (index > maxPosition && index <= maxPosition + windowSize && currentIndex < dataStream.length) { |
|
|
|
if (index !== currentIndex && index !== maxPosition) { |
|
ctx.fillStyle = '#fecaca'; |
|
} |
|
|
|
ctx.strokeStyle = '#ef4444'; |
|
ctx.lineWidth = 1; |
|
ctx.strokeRect(x, y, barWidth, barHeight); |
|
} |
|
|
|
|
|
ctx.fillRect(x, y, barWidth * 0.9, barHeight); |
|
}); |
|
|
|
|
|
ctx.strokeStyle = '#16a34a'; |
|
ctx.lineWidth = 2; |
|
cutPoints.forEach(cutIndex => { |
|
const x = (cutIndex + 1) * barWidth; |
|
if (x <= width) { |
|
ctx.beginPath(); |
|
ctx.moveTo(x, height * 0.05); |
|
ctx.lineTo(x, height * 0.95); |
|
ctx.stroke(); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function algorithmStep() { |
|
if (currentIndex >= dataStream.length) { |
|
updateExplanation(`End of data stream reached. Found ${cutPoints.length} cut-points.`); |
|
stop(); |
|
return false; |
|
} |
|
|
|
const currentValue = dataStream[currentIndex]; |
|
let explanation = `Step: Scanning index ${currentIndex} (Value: ${currentValue}). Current Max: Value ${maxValue} at index ${maxPosition}.`; |
|
|
|
|
|
|
|
if (currentValue <= maxValue) { |
|
explanation += `\nValue ${currentValue} <= Max Value ${maxValue}. Checking window condition.`; |
|
|
|
if (currentIndex === maxPosition + windowSize) { |
|
explanation += `\nIndex ${currentIndex} == Max Position ${maxPosition} + Window Size ${windowSize}. Found Cut-Point!`; |
|
|
|
const cutPointIndex = currentIndex; |
|
|
|
if (cutPointIndex < dataStream.length && !cutPoints.includes(cutPointIndex)) { |
|
cutPoints.push(cutPointIndex); |
|
} |
|
|
|
|
|
chunkStart = cutPointIndex + 1; |
|
currentIndex = chunkStart; |
|
|
|
|
|
if (currentIndex < dataStream.length) { |
|
maxPosition = currentIndex; |
|
maxValue = dataStream[currentIndex]; |
|
explanation += `\nResetting search for next chunk starting at index ${chunkStart}. New Max: Value ${maxValue} at index ${maxPosition}.`; |
|
} else { |
|
explanation += `\nEnd of data stream reached after finding cut-point.`; |
|
updateExplanation(explanation); |
|
stop(); |
|
return false; |
|
} |
|
} else { |
|
explanation += `\nIndex ${currentIndex} != Max Position ${maxPosition} + Window Size ${windowSize}. Continue scanning.`; |
|
currentIndex++; |
|
} |
|
} |
|
|
|
else { |
|
explanation += `\nValue ${currentValue} > Max Value ${maxValue}. Updating Max Point.`; |
|
|
|
maxValue = currentValue; |
|
maxPosition = currentIndex; |
|
explanation += `\nNew Max: Value ${maxValue} at index ${maxPosition}. Continue scanning.`; |
|
currentIndex++; |
|
} |
|
|
|
updateExplanation(explanation); |
|
return true; |
|
} |
|
|
|
|
|
function loop() { |
|
if (!isRunning) return; |
|
|
|
const continueProcessing = algorithmStep(); |
|
draw(); |
|
|
|
if (continueProcessing) { |
|
|
|
timeoutId = setTimeout(() => { |
|
animationFrameId = requestAnimationFrame(loop); |
|
}, animationSpeed); |
|
} else { |
|
|
|
stop(); |
|
} |
|
} |
|
|
|
|
|
function start() { |
|
if (isRunning) return; |
|
isRunning = true; |
|
startBtn.disabled = true; |
|
pauseBtn.disabled = false; |
|
stepBtn.disabled = true; |
|
windowSizeSlider.disabled = true; |
|
dataSizeSlider.disabled = true; |
|
updateExplanation("Running..."); |
|
loop(); |
|
} |
|
|
|
function pause() { |
|
if (!isRunning) return; |
|
isRunning = false; |
|
if (animationFrameId) cancelAnimationFrame(animationFrameId); |
|
if (timeoutId) clearTimeout(timeoutId); |
|
animationFrameId = null; |
|
timeoutId = null; |
|
startBtn.disabled = false; |
|
pauseBtn.disabled = true; |
|
stepBtn.disabled = false; |
|
updateExplanation("Paused."); |
|
} |
|
|
|
function step() { |
|
if (isRunning) pause(); |
|
isRunning = false; |
|
startBtn.disabled = false; |
|
pauseBtn.disabled = true; |
|
stepBtn.disabled = false; |
|
windowSizeSlider.disabled = true; |
|
dataSizeSlider.disabled = true; |
|
|
|
algorithmStep(); |
|
draw(); |
|
if (currentIndex >= dataStream.length) { |
|
stepBtn.disabled = true; |
|
} |
|
} |
|
|
|
function reset() { |
|
pause(); |
|
init(); |
|
} |
|
|
|
function updateExplanation(text) { |
|
explanationDiv.innerHTML = text.replace(/\n/g, '<br>'); |
|
} |
|
|
|
|
|
startBtn.addEventListener('click', start); |
|
pauseBtn.addEventListener('click', pause); |
|
stepBtn.addEventListener('click', step); |
|
resetBtn.addEventListener('click', reset); |
|
|
|
windowSizeSlider.addEventListener('input', (e) => { |
|
windowSizeValueSpan.textContent = e.target.value; |
|
if (!isRunning) { |
|
windowSize = parseInt(e.target.value); |
|
init(); |
|
} |
|
}); |
|
dataSizeSlider.addEventListener('input', (e) => { |
|
dataSizeValueSpan.textContent = e.target.value; |
|
if (!isRunning) { |
|
dataSize = parseInt(e.target.value); |
|
init(); |
|
} |
|
}); |
|
speedSlider.addEventListener('input', (e) => { |
|
animationSpeed = parseInt(e.target.value); |
|
speedValueSpan.textContent = e.target.value; |
|
|
|
if (isRunning && timeoutId) { |
|
clearTimeout(timeoutId); |
|
timeoutId = setTimeout(() => { |
|
animationFrameId = requestAnimationFrame(loop); |
|
}, animationSpeed); |
|
} |
|
}); |
|
|
|
window.addEventListener('resize', resizeCanvas); |
|
|
|
|
|
init(); |
|
|
|
</script> |
|
</body> |
|
</html> |
|
|