| <!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> |
|
|