Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Embedding Distance Visualization</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #4361ee; | |
| --secondary-color: #3f37c9; | |
| --accent-color: #4895ef; | |
| --light-color: #f8f9fa; | |
| --dark-color: #212529; | |
| --success-color: #4cc9f0; | |
| --warning-color: #f8961e; | |
| --danger-color: #f94144; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background-color: var(--light-color); | |
| color: var(--dark-color); | |
| line-height: 1.6; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 10px; | |
| } | |
| .subtitle { | |
| font-size: 1.1rem; | |
| opacity: 0.9; | |
| } | |
| .upload-section { | |
| background-color: white; | |
| border-radius: 10px; | |
| padding: 30px; | |
| margin-bottom: 30px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| text-align: center; | |
| } | |
| .upload-area { | |
| border: 2px dashed #ccc; | |
| border-radius: 8px; | |
| padding: 30px; | |
| margin: 20px 0; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .upload-area:hover { | |
| border-color: var(--primary-color); | |
| background-color: rgba(67, 97, 238, 0.05); | |
| } | |
| .upload-area.active { | |
| border-color: var(--success-color); | |
| background-color: rgba(76, 201, 240, 0.1); | |
| } | |
| .upload-icon { | |
| font-size: 3rem; | |
| color: var(--primary-color); | |
| margin-bottom: 15px; | |
| } | |
| .btn { | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| display: inline-block; | |
| margin-top: 15px; | |
| } | |
| .btn:hover { | |
| background-color: var(--secondary-color); | |
| transform: translateY(-2px); | |
| } | |
| .btn:active { | |
| transform: translateY(0); | |
| } | |
| .btn-secondary { | |
| background-color: var(--light-color); | |
| color: var(--dark-color); | |
| border: 1px solid #ccc; | |
| } | |
| .btn-secondary:hover { | |
| background-color: #e9ecef; | |
| } | |
| .visualization-section { | |
| background-color: white; | |
| border-radius: 10px; | |
| padding: 30px; | |
| margin-bottom: 30px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| position: relative; | |
| min-height: 600px; | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 100%; | |
| margin-top: 20px; | |
| position: relative; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| z-index: 100; | |
| } | |
| .data-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 20px; | |
| } | |
| .data-table th, .data-table td { | |
| padding: 12px 15px; | |
| text-align: left; | |
| border-bottom: 1px solid #ddd; | |
| } | |
| .data-table th { | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .data-table tr:hover { | |
| background-color: #f5f5f5; | |
| } | |
| .controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .slider-container { | |
| flex-grow: 1; | |
| margin: 0 20px; | |
| } | |
| .slider-container label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-weight: 600; | |
| } | |
| .radio-group { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .radio-group label { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| } | |
| .loading { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(255, 255, 255, 0.8); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 10; | |
| flex-direction: column; | |
| border-radius: 10px; | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 5px solid rgba(67, 97, 238, 0.1); | |
| border-radius: 50%; | |
| border-top-color: var(--primary-color); | |
| animation: spin 1s ease-in-out infinite; | |
| margin-bottom: 15px; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .error-message { | |
| color: var(--danger-color); | |
| background-color: rgba(249, 65, 68, 0.1); | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin: 20px 0; | |
| border-left: 4px solid var(--danger-color); | |
| } | |
| .info-message { | |
| color: var(--dark-color); | |
| background-color: rgba(33, 37, 41, 0.05); | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin: 20px 0; | |
| border-left: 4px solid var(--dark-color); | |
| } | |
| .distance-legend { | |
| background-color: white; | |
| border-radius: 5px; | |
| padding: 10px; | |
| position: absolute; | |
| right: 50px; | |
| top: 50px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| font-size: 0.9rem; | |
| } | |
| .distance-legend h4 { | |
| margin-bottom: 8px; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 5px; | |
| } | |
| .distance-legend p { | |
| margin: 5px 0; | |
| } | |
| footer { | |
| text-align: center; | |
| margin-top: 40px; | |
| color: #6c757d; | |
| font-size: 0.9rem; | |
| } | |
| @media (max-width: 768px) { | |
| .controls { | |
| flex-direction: column; | |
| gap: 15px; | |
| } | |
| .slider-container { | |
| width: 100%; | |
| margin: 0; | |
| } | |
| .distance-legend { | |
| position: relative; | |
| right: auto; | |
| top: auto; | |
| margin: 20px 0; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Sentence Embedding Visualizer</h1> | |
| <p class="subtitle">Visualize sentences by cosine distance (x-axis) and Euclidean distance (y-axis)</p> | |
| </header> | |
| <div class="upload-section"> | |
| <h2><i class="fas fa-cloud-upload-alt"></i> Upload Your Data</h2> | |
| <p>Upload a JSON file containing an array of objects with 'sentence' and 'embeddings' properties</p> | |
| <div id="upload-area" class="upload-area"> | |
| <div class="upload-icon"> | |
| <i class="fas fa-file-upload"></i> | |
| </div> | |
| <h3>Drag & Drop your file here</h3> | |
| <p>or</p> | |
| <button id="browse-btn" class="btn btn-secondary"> | |
| <i class="fas fa-folder-open"></i> Browse Files | |
| </button> | |
| <input type="file" id="file-input", accept=".json" style="display: none;"> | |
| </div> | |
| <div id="sample-data" class="info-message" style="display: none;"> | |
| <h4>Sample Data Format:</h4> | |
| <pre>[ | |
| {"sentence": "This is a sample sentence", "embeddings": [0.1, 0.4, 0.2, 0.1, 0.2]}, | |
| {"sentence": "Another example sentence", "embeddings": [0.5, 0.2, 0.3, 0.2, 0.1]}, | |
| ... | |
| ]</pre> | |
| <button id="load-sample" class="btn">Load Sample Data</button> | |
| </div> | |
| </div> | |
| <div id="error-container" class="error-message" style="display: none;"></div> | |
| <div class="visualization-section"> | |
| <div id="loading" class="loading" style="display: none;"> | |
| <div class="spinner"></div> | |
| <p>Processing your embeddings...</p> | |
| </div> | |
| <h2><i class="fas fa-chart-line"></i> Distance Visualization</h2> | |
| <div id="controls" class="controls" style="display: none;"> | |
| <div class="radio-group"> | |
| <h4>Reference Point: </h4> | |
| <label><input type="radio" name="reference" value="first" checked> First Item</label> | |
| <label><input type="radio" name="reference" value="average"> Average Embedding</label> | |
| <label><input type="radio" name="reference" value="random"> Random Item</label> | |
| <label><input type="radio" name="reference" value="unit"> Unit Vector</label> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="point-size">Point Size</label> | |
| <input type="range" id="point-size" min="2" max="20" value="8"> | |
| </div> | |
| <div class="slider-container"> | |
| <label for="font-size">Font Size</label> | |
| <input type="range" id="font-size", min="8", max="24" value="12"> | |
| </div> | |
| </div> | |
| <div id="distance-legend", class="distance-legend", style="display: none;"> | |
| <h4>Distance Metrics:</h4> | |
| <p><strong>X-axis:</strong> Cosine distance from reference (0-1)</p> | |
| <p><strong>Y-axis:</strong> Euclidean distance from reference</p> | |
| <p>(Hover points for details)</p> | |
| </div> | |
| <div id="chart-container" class="chart-container"></div> | |
| </div> | |
| <div id="data-table-container" style="display: none;"> | |
| <h2><i class="fas fa-table"></i> Data Summary</h2> | |
| <table class="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Sentence</th> | |
| <th>Embedding Length</th> | |
| <th>Cosine Distance</th> | |
| <th>Euclidean Distance</th> | |
| </tr> | |
| </thead> | |
| <tbody id="data-table-body"></tbody> | |
| </table> | |
| </div> | |
| <footer> | |
| <p>Sentence Embedding Visualizer © 2023 | Uses cosine and Euclidean distances</p> | |
| </footer> | |
| </div> | |
| <div id="tooltip" class="tooltip"></div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM elements | |
| const uploadArea = document.getElementById('upload-area'); | |
| const fileInput = document.getElementById('file-input'); | |
| const browseBtn = document.getElementById('browse-btn'); | |
| const errorContainer = document.getElementById('error-container'); | |
| const loadingElement = document.getElementById('loading'); | |
| const chartContainer = document.getElementById('chart-container'); | |
| const controlsElement = document.getElementById('controls'); | |
| const dataTableContainer = document.getElementById('data-table-container'); | |
| const dataTableBody = document.getElementById('data-table-body'); | |
| const tooltip = document.getElementById('tooltip'); | |
| const sampleDataBtn = document.getElementById('load-sample'); | |
| const sampleDataElement = document.getElementById('sample-data'); | |
| const distanceLegend = document.getElementById('distance-legend'); | |
| let currentData = []; | |
| // Sample data toggle | |
| browseBtn.addEventListener('click', () => { | |
| fileInput.click(); | |
| sampleDataElement.style.display = 'none'; | |
| }); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| // Drag and drop functionality | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| uploadArea.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| uploadArea.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| uploadArea.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| uploadArea.classList.add('active'); | |
| sampleDataElement.style.display = 'none'; | |
| } | |
| function unhighlight() { | |
| uploadArea.classList.remove('active'); | |
| } | |
| uploadArea.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length) { | |
| fileInput.files = files; | |
| handleFileSelect(); | |
| } | |
| } | |
| // File handling | |
| function handleFileSelect() { | |
| const file = fileInput.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const content = e.target.result; | |
| const data = JSON.parse(content); | |
| currentData = data; | |
| processData(data); | |
| } catch (error) { | |
| showError("Error parsing JSON file: " + error.message); | |
| } | |
| }; | |
| reader.onerror = function() { | |
| showError="Error reading the file"; | |
| }; | |
| reader.readAsText(file); | |
| } | |
| // Sample data | |
| sampleDataBtn.addEventListener('click', function() { | |
| currentData = [ | |
| {"sentence": "The quick brown fox jumps over the lazy dog", "embeddings": [0.8, 0.1, 0.05, 0.05, 0.1, 0.9, 0.1]}, | |
| {"sentence": "Natural language processing is fascinating", "embeddings": [0.7, 0.2, 0.1, 0.1, 0.2, 0.8, 0.3]}, | |
| {"sentence": "Machine learning models can understand text", "embeddings": [0.6, 0.3, 0.2, 0.2, 0.3, 0.7, 0.5]}, | |
| {"sentence": "Word embeddings capture semantic meaning", "embeddings": [0.5, 0.4, 0.3, 0.3, 0.4, 0.6, 0.7]}, | |
| {"sentence": "Sentences can be converted to numerical vectors", "embeddings": [0.4, 0.5, 0.4, 0.4, 0.5, 0.5, 0.8]}, | |
| {"sentence": "Transformer models revolutionized NLP", "embeddings": [0.3, 0.6, 0.5, 0.5, 0.6, 0.4, 0.6]}, | |
| {"sentence": "Attention mechanisms improved model performance", "embeddings": [0.2, 0.7, 0.6, 0.6, 0.7, 0.3, 0.4]}, | |
| {"sentence": "BERT became a foundational model for NLP tasks", "embeddings": [0.1, 0.8, 0.7, 0.7, 0.8, 0.2, 0.2]}, | |
| {"sentence": "GPT models can generate human-like text", "embeddings": [0.2, 0.6, 0.8, 0.8, 0.7, 0.4, 0.3]}, | |
| {"sentence": "Vector space models represent meaning numerically", "embeddings": [0.9, 0.05, 0.01, 0.01, 0.05, 0.95, 0.05]} | |
| ]; | |
| processData(currentData); | |
| }); | |
| // Show sample data format when hovering upload area | |
| uploadArea.addEventListener('mouseenter', () => { | |
| sampleDataElement.style.display = 'block'; | |
| }); | |
| uploadArea.addEventListener('mouseleave', () => { | |
| if (!uploadArea.classList.contains('active')) { | |
| sampleDataElement.style.display = 'none'; | |
| } | |
| }); | |
| // Error handling | |
| function showError(message) { | |
| errorContainer.textContent = message; | |
| errorContainer.style.display = 'block'; | |
| setTimeout(() => { | |
| errorContainer.style.display = 'none'; | |
| }, 5000); | |
| } | |
| // Distance calculation functions | |
| function cosineDistance(a, b) { | |
| let dotProduct = 0; | |
| let magnitudeA = 0; | |
| let magnitudeB = 0; | |
| for (let i = 0; i < a.length; i++) { | |
| dotProduct += a[i] * b[i]; | |
| magnitudeA += a[i] * a[i]; | |
| magnitudeB += b[i] * b[i]; | |
| } | |
| magnitudeA = Math.sqrt(magnitudeA); | |
| magnitudeB = Math.sqrt(magnitudeB); | |
| if (magnitudeA === 0 || magnitudeB === 0) return 0; | |
| const similarity = dotProduct / (magnitudeA * magnitudeB); | |
| return 1 - similarity; // Convert similarity to distance | |
| } | |
| function euclideanDistance(a, b) { | |
| let distance = 0; | |
| for (let i = 0; i < a.length; i++) { | |
| distance += Math.pow(a[i] - b[i], 2); | |
| } | |
| return Math.sqrt(distance); | |
| } | |
| function averageEmbedding(embeddings) { | |
| if (embeddings.length === 0) return []; | |
| const avg = new Array(embeddings[0].length).fill(0); | |
| for (const emb of embeddings) { | |
| for (let i = 0; i < emb.length; i++) { | |
| avg[i] += emb[i]; | |
| } | |
| } | |
| return avg.map(val => val / embeddings.length); | |
| } | |
| // Create a unit vector of the same dimension as the embeddings | |
| function unitVector(dimension) { | |
| const divisor = Math.sqrt(dimension); | |
| return new Array(dimension).fill(1 / divisor); | |
| } | |
| // Data processing | |
| function processData(data) { | |
| if (!Array.isArray(data)) { | |
| showError("Data should be an array of objects"); | |
| return; | |
| } | |
| // Validate data structure | |
| const invalidItems = data.filter(item => | |
| !item.sentence || !Array.isArray(item.embeddings) || item.embeddings.length === 0 | |
| ); | |
| if (invalidItems.length > 0) { | |
| showError(`Some items are invalid (missing sentence or embeddings). First invalid item index: ${invalidItems[0]}`); | |
| return; | |
| } | |
| // Check that all embeddings have the same dimension | |
| const embeddingLengths = [...new Set(data.map(item => item.embeddings.length))]; | |
| if (embeddingLengths.length > 1) { | |
| showError="All embeddings must have the same dimension"; | |
| return; | |
| } | |
| loadingElement.style.display = 'flex'; | |
| // Use setTimeout to allow UI to update before heavy computation | |
| setTimeout(() => { | |
| try { | |
| // Determine reference point based on radio button selection | |
| const referencePoint = getReferencePoint(data); | |
| // Calculate distances for each embedding | |
| const points = []; | |
| const embeddings = data.map(item => item.embeddings); | |
| for (let i = 0; i < data.length; i++) { | |
| const cosDist = cosineDistance(embeddings[i], referencePoint); | |
| const eucDist = euclideanDistance(embeddings[i], referencePoint); | |
| points.push({ x: cosDist, y: eucDist }); | |
| } | |
| // Create visualization | |
| createScatterPlot(points, data, referencePoint); | |
| // Populate data table | |
| populateDataTable(points, data); | |
| controlsElement.style.display = 'flex'; | |
| dataTableContainer.style.display = 'block'; | |
| distanceLegend.style.display = 'block'; | |
| // Add event listeners for reference point changes | |
| document.querySelectorAll('input[name="reference"]').forEach(radio => { | |
| radio.addEventListener('change', function() { | |
| if (this.checked) { | |
| loadingElement.style.display = 'flex'; | |
| setTimeout(() => { | |
| try { | |
| const newReferencePoint = getReferencePoint(currentData); | |
| const newPoints = []; | |
| const embeddings = currentData.map(item => item.embeddings); | |
| for (let i = 0; i < currentData.length; i++) { | |
| const cosDist = cosineDistance(embeddings[i], newReferencePoint); | |
| const eucDist = euclideanDistance(embeddings[i], newReferencePoint); | |
| newPoints.push({ x: cosDist, y: eucDist }); | |
| } | |
| createScatterPlot(newPoints, currentData, newReferencePoint); | |
| populateDataTable(newPoints, currentData); | |
| } finally { | |
| loadingElement.style.display = 'none'; | |
| } | |
| }, 100); | |
| } | |
| }); | |
| }); | |
| } catch (error) { | |
| showError("Error processing data: " + error.message); | |
| console.error(error); | |
| } finally { | |
| loadingElement.style.display = 'none'; | |
| } | |
| }, 100); | |
| } | |
| function getReferencePoint(data) { | |
| const embeddings = data.map(item => item.embeddings); | |
| const embeddingDimension = embeddings.length > 0 ? embeddings[0].length : 0; | |
| const reference = document.querySelector('input[name="reference"]:checked').value; | |
| switch(reference) { | |
| case 'first': | |
| return embeddings[0]; | |
| case 'average': | |
| return averageEmbedding(embeddings); | |
| case 'random': | |
| return embeddings[Math.floor(Math.random() * embeddings.length)]; | |
| case 'unit': | |
| return unitVector(embeddingDimension); | |
| default: | |
| return embeddings[0]; | |
| } | |
| } | |
| // Visualization | |
| function createScatterPlot(points, originalData, referencePoint) { | |
| // Clear previous chart | |
| chartContainer.innerHTML = ''; | |
| // Get container dimensions | |
| const width = chartContainer.clientWidth; | |
| const height = 500; | |
| // Create SVG | |
| const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); | |
| svg.setAttribute("width", "100%"); | |
| svg.setAttribute("height", height); | |
| svg.setAttribute("viewBox", `0 0 ${width} ${height}`); | |
| chartContainer.appendChild(svg); | |
| // Calculate scales to fit all points with padding | |
| const xs = points.map(p => p.x); | |
| const ys = points.map(p => p.y); | |
| const xMin = Math.min(...xs); | |
| const xMax = Math.max(...xs); | |
| const yMin = Math.min(...ys); | |
| const yMax = Math.max(...ys); | |
| const xRange = xMax - xMin || 1; | |
| const yRange = yMax - yMin || 1; | |
| const padding = 0.1; | |
| const scaleX = value => { | |
| return ((value - (xMin - xRange * padding)) / (xRange * (1 + 2 * padding))) * width; | |
| }; | |
| const scaleY = value => { | |
| return height - ((value - (yMin - yRange * padding)) / (yRange * (1 + 2 * padding))) * height; | |
| }; | |
| // Create circles and labels | |
| const pointSize = document.getElementById('point-size').value; | |
| const fontSize = document.getElementById('font-size').value; | |
| // Add axes | |
| const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); | |
| xAxis.setAttribute("x1", scaleX(xMin - xRange * padding)); | |
| xAxis.setAttribute("y1", scaleY(0)); | |
| xAxis.setAttribute("x2", scaleX(xMax + xRange * padding)); | |
| xAxis.setAttribute("y2", scaleY(0)); | |
| xAxis.setAttribute("stroke", "#ccc"); | |
| xAxis.setAttribute("stroke-width", "1"); | |
| svg.appendChild(xAxis); | |
| const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); | |
| yAxis.setAttribute("x1", scaleX(0)); | |
| yAxis.setAttribute("y1", scaleY(yMin - yRange * padding)); | |
| yAxis.setAttribute("x2", scaleX(0)); | |
| yAxis.setAttribute("y2", scaleY(yMax + yRange * padding)); | |
| yAxis.setAttribute("stroke", "#ccc"); | |
| yAxis.setAttribute("stroke-width", "1"); | |
| svg.appendChild(yAxis); | |
| // Add axis labels | |
| const xAxisLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); | |
| xAxisLabel.setAttribute("x", scaleX(xMax + xRange * padding - 0.1 * xRange)); | |
| xAxisLabel.setAttribute("y", scaleY(0) - 10); | |
| xAxisLabel.setAttribute("text-anchor", "end"); | |
| xAxisLabel.setAttribute("font-size", "12"); | |
| xAxisLabel.textContent = "Cosine Distance"; | |
| svg.appendChild(xAxisLabel); | |
| const yAxisLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); | |
| yAxisLabel.setAttribute("x", scaleX(0) + 10); | |
| yAxisLabel.setAttribute("y", scaleY(yMax + yRange * padding - 0.1 * yRange) + 10); | |
| yAxisLabel.setAttribute("font-size", "12"); | |
| yAxisLabel.textContent = "Euclidean Distance"; | |
| svg.appendChild(yAxisLabel); | |
| // Highlight the reference point if it's one of the data points | |
| const referenceIndex = originalData.findIndex(item => { | |
| if (item.embeddings.length !== referencePoint.length) return false; | |
| return item.embeddings.every((val, i) => val === referencePoint[i]); | |
| }); | |
| const referenceLabel = document.querySelector('input[name="reference"]:checked').value; | |
| let refDescription = "Reference: " + | |
| (referenceLabel === 'first' ? "First Item" : | |
| referenceLabel === 'average' ? "Average Embedding" : | |
| referenceLabel === 'random' ? "Random Item" : | |
| "Unit Vector"); | |
| if (referenceIndex !== -1) { | |
| const refPoint = document.createElementNS("http://www.w3.org/2000/svg", "circle"); | |
| refPoint.setAttribute("cx", scaleX(0)); | |
| refPoint.setAttribute("cy", scaleY(0)); | |
| refPoint.setAttribute("r", pointSize + 4); | |
| refPoint.setAttribute("fill", "none"); | |
| refPoint.setAttribute("stroke", "#f94144"); | |
| refPoint.setAttribute("stroke-width", "3"); | |
| svg.appendChild(refPoint); | |
| const refLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); | |
| refLabel.setAttribute("x", scaleX(0) + pointSize + 5); | |
| refLabel.setAttribute("y", scaleY(0) + fontSize / 3); | |
| refLabel.setAttribute("font-size", fontSize); | |
| refLabel.setAttribute("font-weight", "bold"); | |
| refLabel.setAttribute("fill", "#f94144"); | |
| refLabel.textContent = refDescription; | |
| svg.appendChild(refLabel); | |
| } else { | |
| // For unit vector or other synthetic references, show a marker at (0,0) | |
| const refPoint = document.createElementNS("http://www.w3.org/2000/svg", "circle"); | |
| refPoint.setAttribute("cx", scaleX(0)); | |
| refPoint.setAttribute("cy", scaleY(0)); | |
| refPoint.setAttribute("r", pointSize + 4); | |
| refPoint.setAttribute("fill", "none"); | |
| refPoint.setAttribute("stroke", "#f94144"); | |
| refPoint.setAttribute("stroke-width", "3"); | |
| svg.appendChild(refPoint); | |
| const refLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); | |
| refLabel.setAttribute("x", scaleX(0) + pointSize + 5); | |
| refLabel.setAttribute("y", scaleY(0) + fontSize / 3); | |
| refLabel.setAttribute("font-size", fontSize); | |
| refLabel.setAttribute("font-weight", "bold"); | |
| refLabel.setAttribute("fill", "#f94144"); | |
| refLabel.textContent = refDescription; | |
| svg.appendChild(refLabel); | |
| } | |
| // Plot all points | |
| points.forEach((point, i) => { | |
| const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); | |
| circle.setAttribute("cx", scaleX(point.x)); | |
| circle.setAttribute("cy", scaleY(point.y)); | |
| circle.setAttribute("r", pointSize); | |
| circle.setAttribute("fill", `hsl(${(i * 360 / points.length)}, 70%, 50%)`); | |
| circle.setAttribute("data-index", i); | |
| circle.setAttribute("class", "data-point"); | |
| svg.appendChild(circle); | |
| const label = document.createElementNS("http://www.w3.org/2000/svg", "text"); | |
| label.setAttribute("x", scaleX(point.x) + pointSize + 5); | |
| label.setAttribute("y", scaleY(point.y) + fontSize / 3); | |
| label.setAttribute("font-size", fontSize); | |
| label.textContent = originalData[i].sentence.substring(0, 20) + | |
| (originalData[i].sentence.length > 20 ? "..." : ""); | |
| svg.appendChild(label); | |
| }); | |
| // Add interactivity | |
| const dataPoints = document.querySelectorAll('.data-point'); | |
| dataPoints.forEach(point => { | |
| point.addEventListener('mouseover', function(e) { | |
| const index = this.getAttribute('data-index'); | |
| const sentence = originalData[index].sentence; | |
| const embeddings = originalData[index].embeddings; | |
| const embeddingsStr = embeddings.map(v => v.toFixed(2)).join(', '); | |
| tooltip.innerHTML = ` | |
| <strong>${sentence}</strong><br> | |
| Cosine Distance: ${points[index].x.toFixed(4)}<br> | |
| Euclidean Distance: ${points[index].y.toFixed(4)}<br> | |
| Embeddings: [${embeddingsStr}]<br> | |
| ${refDescription} | |
| `; | |
| tooltip.style.left = `${e.clientX + 10}px`; | |
| tooltip.style.top = `${e.clientY + 10}px`; | |
| tooltip.style.opacity = 1; | |
| }); | |
| point.addEventListener('mouseout', function() { | |
| tooltip.style.opacity = 0; | |
| }); | |
| point.addEventListener('mousemove', function(e) { | |
| tooltip.style.left = `${e.clientX + 10}px`; | |
| tooltip.style.top = `${e.clientY + 10}px`; | |
| }); | |
| }); | |
| // Update visualization when controls change | |
| document.getElementById('point-size').addEventListener('input', function() { | |
| dataPoints.forEach(point => { | |
| point.setAttribute('r', this.value); | |
| }); | |
| // Update reference point circle if it exists | |
| const refCircle = svg.querySelector('circle[stroke="#f94144"]'); | |
| if (refCircle) { | |
| refCircle.setAttribute('r', parseInt(this.value) + 4); | |
| } | |
| }); | |
| document.getElementById('font-size').addEventListener('input', function() { | |
| const labels = svg.querySelectorAll('text:not([font-weight="bold"])'); | |
| labels.forEach(label => { | |
| label.setAttribute('font-size', this.value); | |
| }); | |
| // Update reference label if it exists | |
| const refLabel = svg.querySelector('text[font-weight="bold"]'); | |
| if (refLabel) { | |
| refLabel.setAttribute('font-size', this.value); | |
| } | |
| }); | |
| } | |
| // Data table population | |
| function populateDataTable(points, originalData) { | |
| dataTableBody.innerHTML = ''; | |
| originalData.forEach((item, i) => { | |
| const row = document.createElement('tr'); | |
| const sentenceCell = document.createElement('td'); | |
| sentenceCell.textContent = item.sentence; | |
| row.appendChild(sentenceCell); | |
| const dimCell = document.createElement('td'); | |
| dimCell.textContent = item.embeddings.length; | |
| row.appendChild(dimCell); | |
| const cosCell = document.createElement('td'); | |
| cosCell.textContent = points[i].x.toFixed(4); | |
| row.appendChild(cosCell); | |
| const eucCell = document.createElement('td'); | |
| eucCell.textContent = points[i].y.toFixed(4); | |
| row.appendChild(eucCell); | |
| dataTableBody.appendChild(row); | |
| }); | |
| } | |
| // Initially show sample data format | |
| sampleDataElement.style.display = 'block'; | |
| }); | |
| </script> | |
| </body> | |
| </html> |