Spaces:
Running
Running
| <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> |