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