thibaud frere
cleanup
1ee6ce7
<div class="arxiv-umap"></div>
<style>
.arxiv-umap {
position: relative;
}
.arxiv-umap svg {
display: block;
}
/* Tooltip styling comme d3-scatter */
.arxiv-umap .d3-tooltip {
z-index: 20;
backdrop-filter: saturate(1.12) blur(8px);
}
.arxiv-umap .d3-tooltip__inner {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 280px;
max-width: 400px;
max-height: 90vh;
overflow-y: auto;
word-wrap: break-word;
word-break: break-word;
}
.arxiv-umap .paper-header {
background: linear-gradient(135deg, var(--surface-bg), var(--code-bg));
padding: 12px;
margin: -10px -12px 8px -12px;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid var(--border-color);
}
.arxiv-umap .paper-title {
font-weight: 700;
font-size: 14px;
line-height: 1.4;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-color);
}
.arxiv-umap .paper-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.arxiv-umap .paper-category {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--muted-color);
}
.arxiv-umap .paper-badges {
display: flex;
gap: 6px;
align-items: center;
}
.arxiv-umap .paper-authors {
font-size: 11px;
color: var(--muted-color);
font-style: italic;
margin-bottom: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
opacity: 0.9;
}
.arxiv-umap .paper-abstract {
font-size: 11px;
line-height: 1.4;
color: var(--text-color);
padding-top: 0;
display: -webkit-box;
-webkit-line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
max-height: none;
}
.arxiv-umap .paper-year {
background: var(--primary-color);
color: white;
padding: 3px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
white-space: nowrap;
}
.arxiv-umap .paper-id {
background: var(--border-color);
color: var(--muted-color);
padding: 2px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 500;
font-family: monospace;
white-space: nowrap;
}
</style>
<script>
(() => {
const ensureD3 = (cb) => {
if (window.d3 && typeof window.d3.select === 'function') return cb();
let s = document.getElementById('d3-cdn-script');
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
s.addEventListener('load', onReady, { once: true });
if (window.d3) onReady();
};
const bootstrap = () => {
const scriptEl = document.currentScript;
let container = scriptEl ? scriptEl.previousElementSibling : null;
if (!(container && container.classList && container.classList.contains('arxiv-umap'))) {
const cs = Array.from(document.querySelectorAll('.arxiv-umap')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
container = cs[cs.length - 1] || null;
}
if (!container) return;
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
// Tooltip
container.style.position = container.style.position || 'relative';
let tip = container.querySelector('.d3-tooltip'); let tipInner;
if (!tip) {
tip = document.createElement('div'); tip.className = 'd3-tooltip';
Object.assign(tip.style, { position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none', padding: '10px 12px', borderRadius: '12px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)', background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity: '0', transition: 'opacity .12s ease', backdropFilter: 'saturate(1.12) blur(8px)' });
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign = 'left'; tip.appendChild(tipInner); container.appendChild(tip);
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
// SVG
const svg = d3.select(container).append('svg').attr('width', '100%').style('display', 'block');
const gRoot = svg.append('g');
const gDots = gRoot.append('g').attr('class', 'dots');
const gCentroids = gRoot.append('g').attr('class', 'centroids');
// State & scales
let width = 800, height = 360; const margin = { top: 8, right: 12, bottom: 8, left: 12 };
const x = d3.scaleLinear();
const y = d3.scaleLinear();
const color = d3.scaleOrdinal();
const radius = () => 3;
let isDarkMode = false;
// Beautiful category labels
const categoryLabels = {
'cs': 'Computer Science',
'math': 'Mathematics',
'physics': 'Physics',
'stat': 'Statistics',
'eess': 'Electrical Engineering',
'econ': 'Economics',
'q-bio': 'Quantitative Biology',
'q-fin': 'Quantitative Finance',
'astro-ph': 'Astrophysics',
'cond-mat': 'Condensed Matter',
'gr-qc': 'General Relativity',
'hep-ex': 'High Energy Physics - Experiment',
'hep-lat': 'High Energy Physics - Lattice',
'hep-ph': 'High Energy Physics - Phenomenology',
'hep-th': 'High Energy Physics - Theory',
'math-ph': 'Mathematical Physics',
'nlin': 'Nonlinear Sciences',
'nucl-ex': 'Nuclear Experiment',
'nucl-th': 'Nuclear Theory',
'quant-ph': 'Quantum Physics'
};
function getCategoryLabel(category) {
return categoryLabels[category] || category;
}
// Inverse mapping: from family names to domain codes for colors
const familyToDomainCode = {
'Computer Science': 'cs',
'Physics': 'physics',
'Astrophysics': 'astro-ph',
'Condensed Matter': 'cond-mat',
'Quantum Physics': 'quant-ph',
'Mathematics': 'math',
'Statistics': 'stat',
'Mathematical Physics': 'math-ph',
'Engineering': 'eess',
'Biology': 'q-bio',
'Economics': 'econ',
'Finance': 'q-fin',
'General Relativity': 'gr-qc',
'Particle Physics': 'hep-ph',
'Nonlinear Sciences': 'nlin',
'Nuclear Physics': 'nucl-ex'
};
function getFamilyColor(familyName) {
const domainCode = familyToDomainCode[familyName] || familyName;
return color(domainCode) || 'var(--text-color)';
}
function getDotStrokeColor(fillColor = null) {
if (!fillColor) return 'var(--muted-color)';
let resolvedColor = fillColor;
if (fillColor.startsWith('var(')) {
const tempEl = document.createElement('div');
tempEl.style.color = fillColor;
document.body.appendChild(tempEl);
resolvedColor = getComputedStyle(tempEl).color;
document.body.removeChild(tempEl);
}
try {
const colorObj = d3.color(resolvedColor);
if (!colorObj) return 'var(--muted-color)';
return isDarkMode ?
colorObj.darker(0.3).toString() :
colorObj.brighter(0.8).toString();
} catch {
return 'var(--muted-color)';
}
}
// Data loading
async function fetchFirstAvailable(paths) {
for (const p of paths) {
try {
const res = await fetch(p, { cache: 'no-cache' });
if (res.ok) { return await res.json(); }
} catch (e) { }
}
throw new Error('Failed to load data from provided paths');
}
let data = [];
let categories = [];
let centroids = [];
// Mapping des domaines vers les 9 grandes familles
const domainToFamily = {
'cs': 'Computer Science',
'physics': 'Physics',
'astro-ph': 'Astrophysics',
'cond-mat': 'Condensed Matter',
'quant-ph': 'Quantum Physics',
'math': 'Mathematics',
'stat': 'Statistics',
'math-ph': 'Mathematical Physics',
'eess': 'Engineering',
'q-bio': 'Biology',
'econ': 'Economics',
'q-fin': 'Finance',
'gr-qc': 'General Relativity',
'hep-ex': 'Particle Physics',
'hep-lat': 'Particle Physics',
'hep-ph': 'Particle Physics',
'hep-th': 'Particle Physics',
'nlin': 'Nonlinear Sciences',
'nucl-ex': 'Nuclear Physics',
'nucl-th': 'Nuclear Physics'
};
function calculateCentroids(data) {
// Group by the 9 major scientific families
const groups = d3.group(data, d => {
const category = d.primary_category;
const fullDomain = category.split('.')[0]; // Keep full domain like "astro-ph", "cond-mat"
return domainToFamily[fullDomain] || domainToFamily[fullDomain.split('-')[0]] || 'Other Sciences';
});
centroids = Array.from(groups.entries()).map(([family, points]) => {
// Calculate local density for each point
const densities = points.map(point => {
const neighbors = points.filter(p => {
const distance = Math.sqrt(
Math.pow(p.x - point.x, 2) + Math.pow(p.y - point.y, 2)
);
return distance < 0.1; // Rayon de voisinage
});
return neighbors.length; // DensitΓ© = nombre de voisins
});
// Centroid pondΓ©rΓ© par la densitΓ©
const totalWeight = d3.sum(densities);
const x = d3.sum(points, (d, i) => d.x * densities[i]) / totalWeight;
const y = d3.sum(points, (d, i) => d.y * densities[i]) / totalWeight;
// Maximum density point for information
const maxDensityIndex = d3.maxIndex(densities);
const densityCenter = points[maxDensityIndex];
return {
category: family,
x,
y,
count: points.length,
density: totalWeight / points.length, // DensitΓ© moyenne
maxDensityPoint: densityCenter
};
}).filter(centroid => centroid.count >= 100); // Only show top 9 families
}
function updateScales(data) {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
isDarkMode = !!isDark;
width = container.clientWidth || 800; height = Math.max(260, Math.round(width / 3)); svg.attr('width', width).attr('height', height);
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
const xExtent = d3.extent(data, d => d.x);
const yExtent = d3.extent(data, d => d.y);
x.domain([xExtent[0], xExtent[1]]).range([0, innerWidth]).nice();
y.domain([yExtent[0], yExtent[1]]).range([innerHeight, 0]).nice();
return { innerWidth, innerHeight };
}
// Helper function to shuffle array with fixed seed
function shuffleArray(array, seed = 5) {
const shuffled = [...array];
// Simple seeded random number generator
let rng = seed;
const seededRandom = () => {
rng = (rng * 9301 + 49297) % 233280;
return rng / 233280;
};
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(seededRandom() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
function refreshPalette() {
try {
const cats = categories && categories.length ? categories.length : 6;
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
const arr = window.ColorPalettes.getColors('categorical', cats) || [];
if (arr && arr.length) {
// Randomize color order
const shuffledColors = shuffleArray(arr);
color.range(shuffledColors);
return;
}
}
// fallback with randomization
const fallbackColors = (d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ab']).slice(0, cats);
const shuffledFallback = shuffleArray(fallbackColors);
color.range(shuffledFallback);
} catch {
const cats = categories && categories.length ? categories.length : 6;
const fallbackColors = (d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ab']).slice(0, cats);
const shuffledFallback = shuffleArray(fallbackColors);
color.range(shuffledFallback);
}
try { if (data && data.length) draw(); } catch { }
}
function draw() {
if (!data || !data.length) return;
const { innerWidth, innerHeight } = updateScales(data);
const fillFor = d => {
const category = d.primary_category;
// Extract main prefix - take everything before first dot
const mainCategory = category.split('.')[0];
return color(mainCategory);
};
// Calculate centroids
calculateCentroids(data);
// Points
const dots = gDots.selectAll('circle.dot').data(data, (d, i) => d.id || i);
dots.enter().append('circle').attr('class', 'dot')
.attr('cx', d => x(d.x)).attr('cy', d => y(d.y)).attr('r', radius())
.attr('fill', fillFor).attr('fill-opacity', 0.85)
.attr('stroke', d => getDotStrokeColor(fillFor(d))).attr('stroke-width', '0.75px')
.on('mouseenter', function (ev, d) {
d3.select(this).style('stroke', 'var(--text-color)').style('stroke-width', '1.5px').attr('fill-opacity', 1);
const swatch = `<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="5" fill="${fillFor(d)}" /></svg>`;
// Keep title as is, let CSS handle truncation
const title = d.title || 'Untitled Paper';
// Format authors nicely
const authorsText = d.authors && d.authors.length > 0 ?
(d.authors.length <= 3 ? d.authors.join(', ') : `${d.authors.slice(0, 2).join(', ')} et al. (${d.authors.length} authors)`) :
'Unknown authors';
// Get abstract if available, let CSS handle truncation with line-clamp
const abstract = d.abstract || 'No abstract available';
// Extract arXiv ID if available
const arxivId = d.url ? d.url.match(/abs\/([^\/]+)$/)?.[1] || '' : '';
tipInner.innerHTML = `
<div class="paper-header">
<div class="paper-title">${title}</div>
<div class="paper-meta">
<div class="paper-category">
${swatch}
<span>${d.primary_category}</span>
</div>
<div class="paper-badges">
${arxivId ? `<span class="paper-id">${arxivId}</span>` : ''}
</div>
</div>
<div class="paper-authors">${authorsText}</div>
</div>
<div class="paper-abstract">${abstract}</div>`;
tip.style.opacity = '1';
})
.on('mousemove', function (ev) { const [mx, my] = d3.pointer(ev, container); const ox = 12, oy = 12; tip.style.transform = `translate(${Math.round(mx + ox)}px, ${Math.round(my + oy)}px)`; })
.on('mouseleave', function (ev, d) { tip.style.opacity = '0'; tip.style.transform = 'translate(-9999px, -9999px)'; d3.select(this).style('stroke', getDotStrokeColor(fillFor(d))).style('stroke-width', '0.75px').attr('fill-opacity', 0.85); })
.on('click', function (ev, d) { if (d.url) window.open(d.url, '_blank'); })
.merge(dots)
.transition().duration(180)
.attr('cx', d => x(d.x)).attr('cy', d => y(d.y)).attr('r', radius())
.attr('fill', fillFor).attr('fill-opacity', 0.85)
.attr('stroke', d => getDotStrokeColor(fillFor(d))).attr('stroke-width', '0.75px');
dots.exit().remove();
// Centroids with labels (d3-scatter style)
const nodes = centroids.map((c) => ({
category: c.category,
count: c.count,
targetX: x(c.x),
targetY: y(c.y),
x: x(c.x),
y: y(c.y),
width: Math.max(18, (String(c.category || '').length || 6) * 11),
height: 16
}));
if (nodes.length > 1) {
const sim = d3.forceSimulation(nodes)
.force('x', d3.forceX((d) => d.targetX).strength(0.9))
.force('y', d3.forceY((d) => d.targetY).strength(0.9))
.force('collide', d3.forceCollide((d) => Math.hypot(d.width / 2, d.height / 2) + 15))
.stop();
for (let i = 0; i < 650; i++) sim.tick();
const maxOffset = 45;
nodes.forEach((n) => {
const dx = n.x - n.targetX, dy = n.y - n.targetY; const dist = Math.hypot(dx, dy);
if (dist > maxOffset && dist > 0) { const s = maxOffset / dist; n.x = n.targetX + dx * s; n.y = n.targetY + dy * s; }
});
}
const labels = gCentroids.selectAll('g.centroid').data(nodes, d => d.category || 'Unknown');
const enter = labels.enter().append('g').attr('class', 'centroid').attr('pointer-events', 'none');
enter.append('text').attr('class', 'label-bg').attr('text-anchor', 'middle').attr('dominant-baseline', 'middle');
enter.append('text').attr('class', 'label-fg').attr('text-anchor', 'middle').attr('dominant-baseline', 'middle');
const merged = enter.merge(labels);
merged
.transition().duration(180)
.attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)
.each(function (d) {
const base = getFamilyColor(d.category || 'Unknown');
const bgNode = this.querySelector('text.label-bg');
const fgNode = this.querySelector('text.label-fg');
if (bgNode) {
bgNode.textContent = getCategoryLabel(d.category);
bgNode.style.setProperty('fill', "var(--page-bg)", 'important');
bgNode.style.setProperty('stroke', "var(--page-bg)");
bgNode.style.setProperty('stroke-width', '8px');
bgNode.style.setProperty('paint-order', 'stroke fill');
bgNode.style.setProperty('font-weight', '800');
bgNode.style.setProperty('font-size', '16px');
}
if (fgNode) {
fgNode.textContent = getCategoryLabel(d.category);
fgNode.style.setProperty('fill', base, 'important');
fgNode.style.setProperty('font-weight', '800');
fgNode.style.setProperty('font-size', '16px');
}
});
labels.exit().remove();
}
// Load data
let mountEl = container;
while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
mountEl = mountEl.parentElement;
}
let providedData = null;
try {
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
if (attr && attr.trim()) {
providedData = attr.trim();
}
} catch (_) { }
const DEFAULT_JSON = '/data/data.json';
const ensureDataPrefix = (p) => {
if (typeof p !== 'string' || !p) return p;
return p.includes('/') ? p : `/data/${p}`;
};
const JSON_PATHS = providedData ? [ensureDataPrefix(providedData)] : [
DEFAULT_JSON,
'./assets/data/data.json',
'../assets/data/data.json',
'../../assets/data/data.json'
];
const fetchFirstAvailableJson = async (paths) => {
for (const p of paths) { try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.json(); } catch (_) { } }
throw new Error('JSON not found: data.json');
};
fetchFirstAvailableJson(JSON_PATHS).then(rawData => {
// Show only 1 point out of 5 for performance
data = rawData.filter((_, index) => index % 1 === 0);
console.log(`πŸ“Š Affichage de ${data.length} points (1 sur 1) sur ${rawData.length} total`);
// Extract main prefixes (math, cs, physics, etc.)
categories = Array.from(new Set(data.map(d => {
const category = d.primary_category;
// For categories like "math-ph", take only "math"
if (category.includes('-')) {
return category.split('-')[0];
}
// For categories like "cs.AI", take only "cs"
if (category.includes('.')) {
return category.split('.')[0];
}
// Otherwise, return the complete category
return category;
}).filter(Boolean)));
color.domain(categories);
refreshPalette();
draw();
}).catch(e => {
console.error('Failed to load data:', e);
gRoot.append('text').attr('x', width / 2).attr('y', height / 2).attr('text-anchor', 'middle').attr('fill', '#e74c3c').text('Failed to load data');
});
// Resize
if (window.ResizeObserver) {
const ro = new ResizeObserver(() => draw());
ro.observe(container);
} else {
window.addEventListener('resize', draw);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
} else {
ensureD3(bootstrap);
}
})();
</script>