| | <div class="d3-model-popularity"></div> |
| | <style> |
| | .d3-model-popularity .controls { |
| | margin-top: 0; |
| | display: flex; |
| | gap: 16px; |
| | align-items: center; |
| | justify-content: flex-end; |
| | flex-wrap: wrap; |
| | } |
| | |
| | .d3-model-popularity .controls .control-group { |
| | display: flex; |
| | flex-direction: column; |
| | align-items: flex-start; |
| | gap: 6px; |
| | } |
| | |
| | .d3-model-popularity .controls label { |
| | font-size: 12px; |
| | color: var(--text-color); |
| | font-weight: 700; |
| | } |
| | |
| | .d3-model-popularity .controls select { |
| | font-size: 12px; |
| | padding: 8px 28px 8px 10px; |
| | border: 1px solid var(--border-color); |
| | border-radius: 8px; |
| | background-color: var(--surface-bg); |
| | color: var(--text-color); |
| | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); |
| | background-repeat: no-repeat; |
| | background-position: right 8px center; |
| | background-size: 12px; |
| | -webkit-appearance: none; |
| | -moz-appearance: none; |
| | appearance: none; |
| | cursor: pointer; |
| | transition: border-color .15s ease, box-shadow .15s ease; |
| | } |
| | |
| | [data-theme="dark"] .d3-model-popularity .controls select { |
| | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); |
| | } |
| | |
| | .d3-model-popularity .controls select:hover { |
| | border-color: var(--primary-color); |
| | } |
| | |
| | .d3-model-popularity .controls select:focus { |
| | border-color: var(--primary-color); |
| | box-shadow: 0 0 0 3px rgba(232, 137, 171, .25); |
| | outline: none; |
| | } |
| | |
| | |
| | .d3-model-popularity .chart-header { |
| | display: flex; |
| | align-items: flex-start; |
| | justify-content: flex-start; |
| | gap: 12px; |
| | margin: 8px 0 0 0; |
| | flex-wrap: wrap; |
| | } |
| | |
| | .d3-model-popularity .legend-bottom { |
| | display: flex; |
| | flex-direction: column; |
| | align-items: flex-start; |
| | gap: 6px; |
| | font-size: 12px; |
| | color: var(--text-color); |
| | } |
| | |
| | .d3-model-popularity .legend-bottom .legend-title { |
| | font-size: 12px; |
| | font-weight: 700; |
| | color: var(--text-color); |
| | } |
| | |
| | .d3-model-popularity .legend-bottom .items { |
| | display: flex; |
| | flex-wrap: wrap; |
| | gap: 8px 14px; |
| | } |
| | |
| | .d3-model-popularity .legend-bottom .item { |
| | display: inline-flex; |
| | align-items: center; |
| | gap: 6px; |
| | white-space: nowrap; |
| | } |
| | |
| | .d3-model-popularity .legend-bottom .swatch { |
| | width: 14px; |
| | height: 14px; |
| | border-radius: 3px; |
| | border: 1px solid var(--border-color); |
| | display: inline-block; |
| | } |
| | |
| | .d3-model-popularity .axis-label { |
| | fill: var(--text-color); |
| | font-size: 12px; |
| | font-weight: 700; |
| | } |
| | |
| | |
| | .d3-model-popularity .axes path, |
| | .d3-model-popularity .axes line { |
| | stroke: var(--axis-color); |
| | } |
| | |
| | .d3-model-popularity .axes text { |
| | fill: var(--tick-color); |
| | } |
| | |
| | .d3-model-popularity .grid line { |
| | stroke: var(--grid-color); |
| | } |
| | |
| | |
| | .d3-model-popularity .d3-tooltip { |
| | z-index: var(--z-tooltip); |
| | backdrop-filter: saturate(1.12) blur(8px); |
| | } |
| | |
| | |
| | .d3-model-popularity .bars rect { |
| | transition: opacity .12s ease; |
| | } |
| | |
| | .d3-model-popularity .bars rect:hover { |
| | opacity: 0.8; |
| | } |
| | |
| | |
| | .d3-model-popularity .chart-card { |
| | background: var(--surface-bg); |
| | border: 1px solid var(--border-color); |
| | border-radius: 10px; |
| | padding: 8px; |
| | } |
| | |
| | |
| | .d3-model-popularity .chart-header { |
| | display: flex; |
| | justify-content: center; |
| | align-items: center; |
| | margin-bottom: 20px; |
| | padding: 0; |
| | } |
| | |
| | .d3-model-popularity .controls { |
| | justify-content: center; |
| | min-width: 200px; |
| | } |
| | |
| | .d3-model-popularity .controls .control-group { |
| | min-width: 150px; |
| | } |
| | |
| | .d3-model-popularity .controls select { |
| | font-size: 13px; |
| | min-width: 160px; |
| | } |
| | |
| | |
| | .d3-model-popularity .model-name { |
| | font-size: 11px; |
| | fill: var(--tick-color); |
| | text-anchor: end; |
| | } |
| | |
| | |
| | .d3-model-popularity .bars rect { |
| | transition: opacity .15s ease, stroke .15s ease; |
| | } |
| | |
| | .d3-model-popularity .bars rect:hover { |
| | stroke: var(--text-color); |
| | stroke-width: 1px; |
| | } |
| | </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('d3-model-popularity'))) { |
| | const candidates = Array.from(document.querySelectorAll('.d3-model-popularity')) |
| | .filter((el) => !(el.dataset && el.dataset.mounted === 'true')); |
| | container = candidates[candidates.length - 1] || null; |
| | } |
| | if (!container) return; |
| | if (container.dataset) { |
| | if (container.dataset.mounted === 'true') return; |
| | container.dataset.mounted = 'true'; |
| | } |
| | |
| | |
| | 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().startsWith('[') ? JSON.parse(attr) : attr.trim(); |
| | } |
| | } catch(_) {} |
| | |
| | const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p; |
| | const normalizeInput = (inp) => Array.isArray(inp) |
| | ? inp.map(ensureDataPrefix) |
| | : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null); |
| | |
| | const CSV_PATHS = Array.isArray(providedData) |
| | ? normalizeInput(providedData) |
| | : (typeof providedData === 'string' ? normalizeInput(providedData) || ['/data/model_popularity_by_downloads.csv'] : [ |
| | '/data/model_popularity_by_downloads.csv', |
| | '/data/model_popularity_by_last_modified.csv', |
| | './assets/data/model_popularity_by_downloads.csv', |
| | './assets/data/model_popularity_by_last_modified.csv', |
| | '../assets/data/model_popularity_by_downloads.csv', |
| | '../assets/data/model_popularity_by_last_modified.csv', |
| | '../../assets/data/model_popularity_by_downloads.csv', |
| | '../../assets/data/model_popularity_by_last_modified.csv' |
| | ]); |
| | |
| | const fetchFirstAvailable = async (paths) => { |
| | for (const p of paths) { |
| | try { |
| | const r = await fetch(p, { cache: 'no-cache' }); |
| | if (r.ok) return await r.text(); |
| | } catch(_){} |
| | } |
| | throw new Error('CSV not found'); |
| | }; |
| | |
| | |
| | const controls = document.createElement('div'); |
| | controls.className = 'controls'; |
| | const groupMetric = document.createElement('div'); |
| | groupMetric.className = 'control-group'; |
| | const labelMetric = document.createElement('label'); |
| | labelMetric.textContent = 'Metric'; |
| | const selMetric = document.createElement('select'); |
| | |
| | |
| | const metrics = [ |
| | { value: 'downloads', text: 'Sort by Downloads' }, |
| | { value: 'last_modified', text: 'Sort by Last Modified' } |
| | ]; |
| | metrics.forEach((m) => { |
| | const o = document.createElement('option'); |
| | o.value = m.value; |
| | o.textContent = m.text; |
| | selMetric.appendChild(o); |
| | }); |
| | |
| | groupMetric.appendChild(labelMetric); |
| | groupMetric.appendChild(selMetric); |
| | |
| | |
| | const header = document.createElement('div'); |
| | header.className = 'chart-header'; |
| | header.appendChild(controls); |
| | |
| | |
| | container.appendChild(header); |
| | |
| | |
| | const card = document.createElement('div'); |
| | card.className = 'chart-card'; |
| | container.appendChild(card); |
| | const svg = d3.select(card).append('svg').attr('width', '100%').style('display', 'block'); |
| | const gRoot = svg.append('g'); |
| | const gGrid = gRoot.append('g').attr('class', 'grid'); |
| | const gAxes = gRoot.append('g').attr('class', 'axes'); |
| | const gBars = gRoot.append('g').attr('class', 'bars'); |
| | |
| | |
| | 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: '8px 10px', |
| | borderRadius: '8px', |
| | fontSize: '12px', |
| | lineHeight: '1.35', |
| | border: '1px solid var(--border-color)', |
| | background: 'var(--surface-bg)', |
| | color: 'var(--text-color)', |
| | boxShadow: '0 4px 24px rgba(0,0,0,.18)', |
| | opacity: '0', |
| | transition: 'opacity .12s ease' |
| | }); |
| | 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; |
| | } |
| | |
| | |
| | let currentMetric = 'downloads'; |
| | let data = []; |
| | selMetric.value = currentMetric; |
| | |
| | |
| | let width = 800, height = 1000; |
| | const margin = { top: 16, right: 30, bottom: 80, left: 290 }; |
| | const y = d3.scaleBand().padding(0.2); |
| | const x = d3.scaleLinear(); |
| | |
| | function getCategoricalColors(count) { |
| | try { |
| | if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') { |
| | return window.ColorPalettes.getColors('categorical', count); |
| | } |
| | } catch (_) { } |
| | |
| | return ['#636efa', '#EF553B', '#00cc96', '#ab63fa', '#FFA15A', '#19d3f3', '#FF6692', '#B6E880', '#FF97FF', '#FECB52']; |
| | } |
| | |
| | function formatNumber(num) { |
| | if (num >= 1000000) { |
| | return (num / 1000000).toFixed(1) + 'M'; |
| | } else if (num >= 1000) { |
| | return (num / 1000).toFixed(1) + 'K'; |
| | } |
| | return num.toString(); |
| | } |
| | |
| | function getModelType(name) { |
| | const lower = name.toLowerCase(); |
| | if (lower.includes('clip')) return 'clip'; |
| | if (lower.includes('vit')) return 'vit'; |
| | if (lower.includes('resnet')) return 'resnet'; |
| | if (lower.includes('whisper')) return 'whisper'; |
| | if (lower.includes('blip')) return 'blip'; |
| | if (lower.includes('qwen')) return 'qwen2_vl'; |
| | if (lower.includes('gemma')) return 'gemma3'; |
| | if (lower.includes('dinov2')) return 'dinov2'; |
| | if (lower.includes('siglip')) return 'siglip'; |
| | if (lower.includes('table')) return 'table_transformer'; |
| | if (lower.includes('layout')) return 'layoutlmv3'; |
| | if (lower.includes('music')) return 'musicgen'; |
| | if (lower.includes('internvl')) return 'internvl'; |
| | if (lower.includes('clipseg')) return 'clipseg'; |
| | if (lower.includes('vitmatte')) return 'vitmatte'; |
| | if (lower.includes('mobilevit')) return 'mobilevit'; |
| | if (lower.includes('unidepth')) return 'vit'; |
| | if (lower.includes('align')) return 'align'; |
| | if (lower.includes('bit')) return 'bit'; |
| | if (lower.includes('bert')) return 'd_fine'; |
| | if (lower.includes('fashion')) return 'clip'; |
| | if (lower.includes('age')) return 'vit'; |
| | if (lower.includes('nsfw')) return 'vit'; |
| | return 'other'; |
| | } |
| | |
| | async function loadData() { |
| | try { |
| | const csvText = await fetchFirstAvailable(CSV_PATHS); |
| | const parsed = d3.csvParse(csvText); |
| | |
| | |
| | data = parsed.map(d => ({ |
| | name: d['Model Name'].replace(/"/g, ''), |
| | downloads: parseInt(d.Downloads.replace(/"/g, '')) || 0 |
| | })).filter(d => d.downloads > 0); |
| | |
| | |
| | const uniqueData = new Map(); |
| | data.forEach(d => { |
| | if (!uniqueData.has(d.name) || uniqueData.get(d.name).downloads < d.downloads) { |
| | uniqueData.set(d.name, d); |
| | } |
| | }); |
| | |
| | data = Array.from(uniqueData.values()); |
| | |
| | |
| | if (currentMetric === 'downloads') { |
| | data.sort((a, b) => b.downloads - a.downloads); |
| | } else { |
| | |
| | |
| | |
| | } |
| | |
| | |
| | data.forEach(d => { |
| | d.type = getModelType(d.name); |
| | }); |
| | |
| | update(); |
| | } catch (error) { |
| | console.error('Error loading data:', error); |
| | const errorDiv = document.createElement('pre'); |
| | errorDiv.style.color = 'red'; |
| | errorDiv.style.padding = '16px'; |
| | errorDiv.textContent = `Error loading data: ${error.message}`; |
| | container.appendChild(errorDiv); |
| | } |
| | } |
| | |
| | function updateScales() { |
| | width = container.clientWidth || 800; |
| | |
| | const minHeightPerModel = 25; |
| | const calculatedHeight = Math.max(400, data.length * minHeightPerModel + margin.top + margin.bottom); |
| | height = Math.min(1200, calculatedHeight); |
| | 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})`); |
| | |
| | |
| | y.domain(data.map(d => d.name)).range([0, innerHeight]); |
| | x.domain([0, d3.max(data, d => d.downloads) || 1]).range([0, innerWidth]).nice(); |
| | |
| | |
| | gGrid.selectAll('*').remove(); |
| | gGrid.selectAll('line').data(x.ticks(6)).join('line') |
| | .attr('x1', (d) => x(d)).attr('x2', (d) => x(d)).attr('y1', 0).attr('y2', innerHeight) |
| | .attr('stroke', 'var(--grid-color)').attr('stroke-width', 1).attr('shape-rendering', 'crispEdges'); |
| | |
| | |
| | gAxes.selectAll('*').remove(); |
| | |
| | |
| | gAxes.append('g').attr('transform', `translate(0,${innerHeight})`) |
| | .call(d3.axisBottom(x).ticks(6).tickFormat(d3.format('~s'))) |
| | .call((g) => { |
| | g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); |
| | g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size', '12px'); |
| | }); |
| | |
| | |
| | gAxes.append('g') |
| | .call(d3.axisLeft(y)) |
| | .call((g) => { |
| | g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); |
| | g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size', '11px'); |
| | }); |
| | |
| | |
| | gAxes.append('text').attr('class', 'axis-label axis-label--x') |
| | .attr('x', innerWidth / 2).attr('y', innerHeight + 64) |
| | .attr('text-anchor', 'middle').text('Downloads'); |
| | |
| | |
| | return { innerWidth, innerHeight }; |
| | } |
| | |
| | function renderLegend() { |
| | |
| | } |
| | |
| | function drawBars() { |
| | if (!data || data.length === 0) return; |
| | |
| | const { innerWidth, innerHeight } = updateScales(); |
| | |
| | |
| | const types = [...new Set(data.map(d => d.type))]; |
| | const colors = getCategoricalColors(types.length); |
| | |
| | |
| | const shuffledColors = [...colors].sort(() => Math.random() - 0.5); |
| | const colorMap = new Map(types.map((type, i) => [type, shuffledColors[i]])); |
| | |
| | const bars = gBars.selectAll('rect').data(data, d => d.name); |
| | |
| | bars.enter().append('rect') |
| | .attr('y', d => y(d.name)) |
| | .attr('x', 1) |
| | .attr('width', 0) |
| | .attr('height', y.bandwidth()) |
| | .attr('fill', d => colorMap.get(d.type)) |
| | .on('mouseenter', function (ev, d) { |
| | tipInner.innerHTML = ` |
| | <div><strong>${d.name}</strong></div> |
| | <div><strong>Type:</strong> ${d.type}</div> |
| | <div><strong>Downloads:</strong> ${d.downloads.toLocaleString()}</div> |
| | `; |
| | tip.style.opacity = '1'; |
| | }) |
| | .on('mousemove', function (ev, d) { |
| | const [mx, my] = d3.pointer(ev, container); |
| | const offsetX = 12, offsetY = 12; |
| | const maxX = (container.clientWidth || 0) - (tip.offsetWidth + 6); |
| | const maxY = (container.clientHeight || 0) - (tip.offsetHeight + 6); |
| | const tx = Math.max(0, Math.min(mx + offsetX, maxX)); |
| | const ty = Math.max(0, Math.min(my + offsetY, maxY)); |
| | tip.style.transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`; |
| | }) |
| | .on('mouseleave', function () { |
| | tip.style.opacity = '0'; |
| | tip.style.transform = 'translate(-9999px, -9999px)'; |
| | }) |
| | .merge(bars) |
| | .transition().duration(600) |
| | .attr('y', d => y(d.name)) |
| | .attr('x', 1) |
| | .attr('width', d => x(d.downloads)) |
| | .attr('height', y.bandwidth()) |
| | .attr('fill', d => colorMap.get(d.type)); |
| | |
| | bars.exit().remove(); |
| | |
| | renderLegend(); |
| | } |
| | |
| | function update() { |
| | drawBars(); |
| | } |
| | |
| | |
| | selMetric.addEventListener('change', async (e) => { |
| | currentMetric = e.target.value; |
| | |
| | const newPaths = currentMetric === 'downloads' |
| | ? ['/data/model_popularity_by_downloads.csv', './assets/data/model_popularity_by_downloads.csv', '../assets/data/model_popularity_by_downloads.csv'] |
| | : ['/data/model_popularity_by_last_modified.csv', './assets/data/model_popularity_by_last_modified.csv', '../assets/data/model_popularity_by_last_modified.csv']; |
| | |
| | CSV_PATHS.splice(0, CSV_PATHS.length, ...newPaths); |
| | await loadData(); |
| | }); |
| | |
| | |
| | controls.appendChild(groupMetric); |
| | loadData(); |
| | |
| | const rerender = () => { update(); }; |
| | if (window.ResizeObserver) { |
| | const ro = new ResizeObserver(() => rerender()); |
| | ro.observe(container); |
| | } else { |
| | window.addEventListener('resize', rerender); |
| | } |
| | }; |
| | |
| | if (document.readyState === 'loading') { |
| | document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); |
| | } else { |
| | ensureD3(bootstrap); |
| | } |
| | })(); |
| | </script> |
| |
|