|
|
<div class="arxiv-umap"></div> |
|
|
|
|
|
<style> |
|
|
.arxiv-umap { |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.arxiv-umap svg { |
|
|
display: block; |
|
|
} |
|
|
|
|
|
|
|
|
.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'; } |
|
|
|
|
|
|
|
|
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; } |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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)'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 = []; |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
const groups = d3.group(data, d => { |
|
|
const category = d.primary_category; |
|
|
const fullDomain = category.split('.')[0]; |
|
|
return domainToFamily[fullDomain] || domainToFamily[fullDomain.split('-')[0]] || 'Other Sciences'; |
|
|
}); |
|
|
|
|
|
centroids = Array.from(groups.entries()).map(([family, points]) => { |
|
|
|
|
|
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; |
|
|
}); |
|
|
return neighbors.length; |
|
|
}); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const maxDensityIndex = d3.maxIndex(densities); |
|
|
const densityCenter = points[maxDensityIndex]; |
|
|
|
|
|
return { |
|
|
category: family, |
|
|
x, |
|
|
y, |
|
|
count: points.length, |
|
|
density: totalWeight / points.length, |
|
|
maxDensityPoint: densityCenter |
|
|
}; |
|
|
}).filter(centroid => centroid.count >= 100); |
|
|
} |
|
|
|
|
|
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 }; |
|
|
} |
|
|
|
|
|
|
|
|
function shuffleArray(array, seed = 5) { |
|
|
const shuffled = [...array]; |
|
|
|
|
|
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) { |
|
|
|
|
|
const shuffledColors = shuffleArray(arr); |
|
|
color.range(shuffledColors); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
const mainCategory = category.split('.')[0]; |
|
|
return color(mainCategory); |
|
|
}; |
|
|
|
|
|
|
|
|
calculateCentroids(data); |
|
|
|
|
|
|
|
|
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>`; |
|
|
|
|
|
const title = d.title || 'Untitled Paper'; |
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
const abstract = d.abstract || 'No abstract 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(); |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
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 => { |
|
|
|
|
|
data = rawData.filter((_, index) => index % 1 === 0); |
|
|
console.log(`📊 Affichage de ${data.length} points (1 sur 1) sur ${rawData.length} total`); |
|
|
|
|
|
categories = Array.from(new Set(data.map(d => { |
|
|
const category = d.primary_category; |
|
|
|
|
|
if (category.includes('-')) { |
|
|
return category.split('-')[0]; |
|
|
} |
|
|
|
|
|
if (category.includes('.')) { |
|
|
return category.split('.')[0]; |
|
|
} |
|
|
|
|
|
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'); |
|
|
}); |
|
|
|
|
|
|
|
|
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> |