Spaces:
Running
Running
thibaud frere
commited on
Commit
·
7914ed2
1
Parent(s):
2da6ea7
update
Browse files- README.md +8 -7
- app/src/components/ColorPicker.astro +21 -1
- app/src/content/chapters/vibe-coding-charts.mdx +6 -1
- app/src/content/embeds/d3-pie-quad.html +347 -0
- app/src/content/embeds/d3-pie.html +109 -300
- app/src/content/embeds/vibe-code-d3-embeds-directives.md +46 -0
- tools/duplicated-spaces/README.md +32 -0
- tools/duplicated-spaces/duplicated_spaces/__init__.py +5 -0
- tools/duplicated-spaces/duplicated_spaces/cli.py +69 -0
- app/scripts/find_duplicated_spaces.py → tools/duplicated-spaces/duplicated_spaces/finder.py +7 -104
- tools/duplicated-spaces/pyproject.toml +23 -0
README.md
CHANGED
|
@@ -12,18 +12,19 @@ thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jp
|
|
| 12 |
|
| 13 |
## Find recent duplicated Spaces
|
| 14 |
|
| 15 |
-
This repository includes a small
|
| 16 |
|
| 17 |
-
|
| 18 |
|
| 19 |
```bash
|
| 20 |
-
|
|
|
|
| 21 |
```
|
| 22 |
|
| 23 |
Usage:
|
| 24 |
|
| 25 |
```bash
|
| 26 |
-
|
| 27 |
```
|
| 28 |
|
| 29 |
Options:
|
|
@@ -40,11 +41,11 @@ Examples:
|
|
| 40 |
export HF_TOKEN=hf_xxx
|
| 41 |
|
| 42 |
# Find Spaces duplicated from tfrere/my-space in the last 14 days
|
| 43 |
-
|
| 44 |
|
| 45 |
# Use a 7-day window and explicit token
|
| 46 |
-
|
| 47 |
```
|
| 48 |
|
| 49 |
-
The
|
| 50 |
|
|
|
|
| 12 |
|
| 13 |
## Find recent duplicated Spaces
|
| 14 |
|
| 15 |
+
This repository includes a small Poetry tool under `tools/duplicated-spaces` to list public Spaces created in the last N days that were duplicated from a given source Space.
|
| 16 |
|
| 17 |
+
Setup:
|
| 18 |
|
| 19 |
```bash
|
| 20 |
+
cd tools/duplicated-spaces
|
| 21 |
+
poetry install --no-root
|
| 22 |
```
|
| 23 |
|
| 24 |
Usage:
|
| 25 |
|
| 26 |
```bash
|
| 27 |
+
poetry run find-duplicated-spaces --source owner/space-name --days 14
|
| 28 |
```
|
| 29 |
|
| 30 |
Options:
|
|
|
|
| 41 |
export HF_TOKEN=hf_xxx
|
| 42 |
|
| 43 |
# Find Spaces duplicated from tfrere/my-space in the last 14 days
|
| 44 |
+
poetry run find-duplicated-spaces --source tfrere/my-space
|
| 45 |
|
| 46 |
# Use a 7-day window and explicit token
|
| 47 |
+
poetry run find-duplicated-spaces --source tfrere/my-space --days 7 --token $HF_TOKEN
|
| 48 |
```
|
| 49 |
|
| 50 |
+
The tool first checks card metadata (e.g., `duplicated_from`) and optionally falls back to parsing the README frontmatter for robustness.
|
| 51 |
|
app/src/components/ColorPicker.astro
CHANGED
|
@@ -82,7 +82,27 @@
|
|
| 82 |
const toHex = x => Math.round(255 * x).toString(16).padStart(2, '0');
|
| 83 |
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`.toUpperCase();
|
| 84 |
};
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
const updateUI = (h, adjusting) => { const rect = slider.getBoundingClientRect(); const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const t=Math.max(0, Math.min(1, (h/360))); const leftPx = r + t * Math.max(0, (rect.width - 2*r)); if (knob) knob.style.left = (leftPx/rect.width*100) + '%'; if (hueValue) hueValue.textContent=`${Math.round(h)}°`; if (slider) slider.setAttribute('aria-valuenow', String(Math.round(h))); const L=62, S=72; const baseHex=hslToHex(h,S,L); if (currentSwatch) currentSwatch.style.background=baseHex; if (currentName) currentName.textContent=getName(baseHex); if (currentHex) currentHex.textContent=baseHex; if (currentLch) currentLch.textContent = `HSL ${L}, ${S}, ${Math.round(h)}°`; if (currentRgb){ const hex=baseHex.replace('#',''); const R=parseInt(hex.slice(0,2),16), G=parseInt(hex.slice(2,4),16), B=parseInt(hex.slice(4,6),16); currentRgb.textContent=`RGB ${R}, ${G}, ${B}`; } const hoverHex=hslToHex(h, Math.max(0,S-10), Math.max(0, L-8)); const rootEl=document.documentElement; rootEl.style.setProperty('--primary-color', baseHex); rootEl.style.setProperty('--primary-color-hover', hoverHex); };
|
| 87 |
const getHueFromEvent = (ev) => { const rect=slider.getBoundingClientRect(); const clientX=ev.touches ? ev.touches[0].clientX : ev.clientX; const x = clientX - rect.left; const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const effX=Math.max(r, Math.min(rect.width - r, x)); const denom=Math.max(1, rect.width - 2*r); const t=(effX - r) / denom; return t*360; };
|
| 88 |
const unsubscribe = bus.subscribe(({ sourceId, hue, adjusting }) => { if (sourceId === instanceId) return; updateUI(hue, adjusting); });
|
|
|
|
| 82 |
const toHex = x => Math.round(255 * x).toString(16).padStart(2, '0');
|
| 83 |
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`.toUpperCase();
|
| 84 |
};
|
| 85 |
+
// Precompute hues for the provided color-name list
|
| 86 |
+
const NAME_HUES = COLOR_NAMES.map((c) => {
|
| 87 |
+
const hh = hexToHsl(c.hex).h || 0;
|
| 88 |
+
return { name: c.name, hue: hh };
|
| 89 |
+
});
|
| 90 |
+
// Pick closest name by circular hue distance; fallback to coarse labels
|
| 91 |
+
const getName = (hex) => {
|
| 92 |
+
const h = hexToHsl(hex).h || 0;
|
| 93 |
+
let bestName = '—';
|
| 94 |
+
let best = 361;
|
| 95 |
+
for (let i = 0; i < NAME_HUES.length; i++) {
|
| 96 |
+
const hh = NAME_HUES[i].hue;
|
| 97 |
+
const d = Math.abs(hh - h);
|
| 98 |
+
const dist = Math.min(d, 360 - d);
|
| 99 |
+
if (dist < best) { best = dist; bestName = NAME_HUES[i].name; }
|
| 100 |
+
}
|
| 101 |
+
if (bestName !== '—') return bestName;
|
| 102 |
+
const labels=['Red','Orange','Yellow','Lime','Green','Cyan','Blue','Indigo','Violet','Magenta'];
|
| 103 |
+
const idx=Math.round(((h%360)/360)*(labels.length-1));
|
| 104 |
+
return labels[idx];
|
| 105 |
+
};
|
| 106 |
const updateUI = (h, adjusting) => { const rect = slider.getBoundingClientRect(); const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const t=Math.max(0, Math.min(1, (h/360))); const leftPx = r + t * Math.max(0, (rect.width - 2*r)); if (knob) knob.style.left = (leftPx/rect.width*100) + '%'; if (hueValue) hueValue.textContent=`${Math.round(h)}°`; if (slider) slider.setAttribute('aria-valuenow', String(Math.round(h))); const L=62, S=72; const baseHex=hslToHex(h,S,L); if (currentSwatch) currentSwatch.style.background=baseHex; if (currentName) currentName.textContent=getName(baseHex); if (currentHex) currentHex.textContent=baseHex; if (currentLch) currentLch.textContent = `HSL ${L}, ${S}, ${Math.round(h)}°`; if (currentRgb){ const hex=baseHex.replace('#',''); const R=parseInt(hex.slice(0,2),16), G=parseInt(hex.slice(2,4),16), B=parseInt(hex.slice(4,6),16); currentRgb.textContent=`RGB ${R}, ${G}, ${B}`; } const hoverHex=hslToHex(h, Math.max(0,S-10), Math.max(0, L-8)); const rootEl=document.documentElement; rootEl.style.setProperty('--primary-color', baseHex); rootEl.style.setProperty('--primary-color-hover', hoverHex); };
|
| 107 |
const getHueFromEvent = (ev) => { const rect=slider.getBoundingClientRect(); const clientX=ev.touches ? ev.touches[0].clientX : ev.clientX; const x = clientX - rect.left; const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const effX=Math.max(r, Math.min(rect.width - r, x)); const denom=Math.max(1, rect.width - 2*r); const t=(effX - r) / denom; return t*360; };
|
| 108 |
const unsubscribe = bus.subscribe(({ sourceId, hue, adjusting }) => { if (sourceId === instanceId) return; updateUI(hue, adjusting); });
|
app/src/content/chapters/vibe-coding-charts.mdx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
| 1 |
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
|
|
|
| 2 |
|
| 3 |
## Vibe coding charts
|
| 4 |
|
|
|
|
|
|
|
| 5 |
### Prompt
|
| 6 |
|
| 7 |
This page explains how to use the directives to author D3 charts as self‑contained HTML fragments.
|
|
@@ -62,7 +65,9 @@ They can be found in the `app/src/content/embeds` folder and you can also use th
|
|
| 62 |
---
|
| 63 |
<HtmlEmbed src="d3-bar.html" title="d3-bar: Memory usage with recomputation" desc={`Figure 6: Memory usage with recomputation.<br/>Credits: <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">Ultrascale playbook</a>`}/>
|
| 64 |
---
|
| 65 |
-
<HtmlEmbed src="d3-pie.html" title="d3-pie: Pie charts by category"
|
|
|
|
|
|
|
| 66 |
---
|
| 67 |
<HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
|
| 68 |
---
|
|
|
|
| 1 |
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
| 2 |
+
import Note from '../../components/Note.astro';
|
| 3 |
|
| 4 |
## Vibe coding charts
|
| 5 |
|
| 6 |
+
<Note emoji="⚠️" variant="danger">This is a work in progress. It may change quickly.</Note>
|
| 7 |
+
|
| 8 |
### Prompt
|
| 9 |
|
| 10 |
This page explains how to use the directives to author D3 charts as self‑contained HTML fragments.
|
|
|
|
| 65 |
---
|
| 66 |
<HtmlEmbed src="d3-bar.html" title="d3-bar: Memory usage with recomputation" desc={`Figure 6: Memory usage with recomputation.<br/>Credits: <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">Ultrascale playbook</a>`}/>
|
| 67 |
---
|
| 68 |
+
<HtmlEmbed src="d3-pie.html" title="d3-pie: Pie charts by category" desc='Figure 7: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>' />
|
| 69 |
+
---
|
| 70 |
+
<HtmlEmbed src="d3-pie-quad.html" title="d3-pie-quad: Quad donuts by metric" align="center" frameless desc={'Quad view: Answer Tokens, Number of Samples, Number of Turns, Number of Images.'} />
|
| 71 |
---
|
| 72 |
<HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
|
| 73 |
---
|
app/src/content/embeds/d3-pie-quad.html
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-pie-quad"></div>
|
| 2 |
+
<style>
|
| 3 |
+
/* Layout piloté par container queries (par rapport au parent) */
|
| 4 |
+
.d3-pie-quad { container-type: inline-size; }
|
| 5 |
+
.d3-pie-quad .legend { width: 80%;margin: 0 auto; font-size: 12px; line-height: 1.35; color: var(--text-color); }
|
| 6 |
+
.d3-pie-quad .legend { margin-bottom: 32px; }
|
| 7 |
+
.d3-pie-quad .legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; align-items:center; justify-content:center; }
|
| 8 |
+
.d3-pie-quad .legend .item { display:flex; align-items:center; gap:8px; white-space:nowrap; }
|
| 9 |
+
.d3-pie-quad .legend .swatch { width:14px; height:14px; border-radius:3px; display:inline-block; border: 1px solid var(--border-color); }
|
| 10 |
+
.d3-pie-quad .legend .title { display:block; text-align:center; font-weight:800; margin-bottom:6px; }
|
| 11 |
+
.d3-pie-quad .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
|
| 12 |
+
.d3-pie-quad .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }
|
| 13 |
+
.d3-pie-quad .nodata { font-size: 12px; fill: var(--muted-color); }
|
| 14 |
+
/* Ghost legend items when hovering slices */
|
| 15 |
+
.d3-pie-quad.hovering .legend .item.ghost { opacity: .35; }
|
| 16 |
+
.d3-pie-quad .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: var(--transparent-page-contrast); stroke-width: 3px; }
|
| 17 |
+
/* Effet ghost synchronisé */
|
| 18 |
+
.d3-pie-quad .slice {
|
| 19 |
+
transition: opacity .15s ease;
|
| 20 |
+
}
|
| 21 |
+
.d3-pie-quad.hovering .slice.ghost {
|
| 22 |
+
opacity: .25;
|
| 23 |
+
}
|
| 24 |
+
/* Layout HTML (pas JS) pour la grille et les cellules */
|
| 25 |
+
.d3-pie-quad .plots-grid {
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-wrap: wrap;
|
| 28 |
+
justify-content: center;
|
| 29 |
+
align-items: flex-start;
|
| 30 |
+
gap: 12px 20px;
|
| 31 |
+
margin-top: 4px;
|
| 32 |
+
margin-left: auto;
|
| 33 |
+
margin-right: auto;
|
| 34 |
+
width: 100%;
|
| 35 |
+
}
|
| 36 |
+
/* Par défaut (flux ~1280): 2 colonnes centrées */
|
| 37 |
+
.content-grid .d3-pie-quad .plots-grid { width: 100%; }
|
| 38 |
+
.content-grid .d3-pie-quad .pie-cell { flex: 0 0 calc((100% - 20px)/2); }
|
| 39 |
+
/* En wrappers larges: viser 4 colonnes si l'espace le permet */
|
| 40 |
+
.wide .d3-pie-quad .plots-grid,
|
| 41 |
+
.full-width .d3-pie-quad .plots-grid { width: 100%; }
|
| 42 |
+
.wide .d3-pie-quad .pie-cell,
|
| 43 |
+
.full-width .d3-pie-quad .pie-cell { flex: 0 0 calc((100% - 60px)/4); }
|
| 44 |
+
/* Forcer 2 colonnes dans le flux lorsque le parent ~1280px */
|
| 45 |
+
.content-grid .d3-pie-quad .plots-grid { width: min(740px, 100%); }
|
| 46 |
+
.d3-pie-quad .pie-cell {
|
| 47 |
+
display: flex;
|
| 48 |
+
flex-direction: column;
|
| 49 |
+
align-items: center;
|
| 50 |
+
flex: 0 0 360px; /* 2 colonnes fixes dans le flux à 1280px */
|
| 51 |
+
}
|
| 52 |
+
/* 4/2/1 colonnes en fonction de la largeur du parent */
|
| 53 |
+
/* @container (min-width: 740px) {
|
| 54 |
+
.d3-pie-quad .plots-grid { width: 740px; }
|
| 55 |
+
}
|
| 56 |
+
@container (max-width: 739.98px) {
|
| 57 |
+
.d3-pie-quad .plots-grid { width: 100%; }
|
| 58 |
+
} */
|
| 59 |
+
@media (max-width: 500px) {
|
| 60 |
+
.d3-pie-quad .pie-cell { flex: 0 0 100%; }
|
| 61 |
+
}
|
| 62 |
+
/* Tooltip styling aligned with filters-quad */
|
| 63 |
+
.d3-pie-quad .d3-tooltip {
|
| 64 |
+
z-index: var(--z-elevated);
|
| 65 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 66 |
+
}
|
| 67 |
+
.d3-pie-quad .d3-tooltip__inner {
|
| 68 |
+
display: flex;
|
| 69 |
+
flex-direction: column;
|
| 70 |
+
gap: 6px;
|
| 71 |
+
min-width: 220px;
|
| 72 |
+
}
|
| 73 |
+
.d3-pie-quad .d3-tooltip__inner > div:first-child {
|
| 74 |
+
font-weight: 800;
|
| 75 |
+
letter-spacing: 0.1px;
|
| 76 |
+
margin-bottom: 0;
|
| 77 |
+
}
|
| 78 |
+
.d3-pie-quad .d3-tooltip__inner > div:nth-child(2) {
|
| 79 |
+
font-size: 11px;
|
| 80 |
+
color: var(--muted-color);
|
| 81 |
+
display: block;
|
| 82 |
+
margin-top: -4px;
|
| 83 |
+
margin-bottom: 2px;
|
| 84 |
+
letter-spacing: 0.1px;
|
| 85 |
+
}
|
| 86 |
+
.d3-pie-quad .d3-tooltip__inner > div:nth-child(n+3) {
|
| 87 |
+
padding-top: 6px;
|
| 88 |
+
border-top: 1px solid var(--border-color);
|
| 89 |
+
}
|
| 90 |
+
.d3-pie-quad .d3-tooltip__color-dot {
|
| 91 |
+
display: inline-block;
|
| 92 |
+
width: 12px;
|
| 93 |
+
height: 12px;
|
| 94 |
+
border-radius: 3px;
|
| 95 |
+
border: 1px solid var(--border-color);
|
| 96 |
+
}
|
| 97 |
+
</style>
|
| 98 |
+
<script>
|
| 99 |
+
(() => {
|
| 100 |
+
const THIS_SCRIPT = document.currentScript;
|
| 101 |
+
const ensureD3 = (cb) => {
|
| 102 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 103 |
+
let s = document.getElementById('d3-cdn-script');
|
| 104 |
+
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); }
|
| 105 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 106 |
+
s.addEventListener('load', onReady, { once: true });
|
| 107 |
+
if (window.d3) onReady();
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const bootstrap = () => {
|
| 111 |
+
const scriptEl = THIS_SCRIPT;
|
| 112 |
+
const host = scriptEl && scriptEl.parentElement;
|
| 113 |
+
let container = null;
|
| 114 |
+
if (host && host.querySelector) {
|
| 115 |
+
container = host.querySelector('.d3-pie-quad');
|
| 116 |
+
}
|
| 117 |
+
if (!container) {
|
| 118 |
+
let sib = scriptEl && scriptEl.previousElementSibling;
|
| 119 |
+
while (sib && !(sib.classList && sib.classList.contains('d3-pie-quad'))) {
|
| 120 |
+
sib = sib.previousElementSibling;
|
| 121 |
+
}
|
| 122 |
+
container = sib || document.querySelector('.d3-pie-quad');
|
| 123 |
+
}
|
| 124 |
+
if (!container) return;
|
| 125 |
+
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 126 |
+
|
| 127 |
+
// Tooltip
|
| 128 |
+
container.style.position = container.style.position || 'relative';
|
| 129 |
+
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 130 |
+
if (!tip) {
|
| 131 |
+
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
| 132 |
+
Object.assign(tip.style, {
|
| 133 |
+
position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
|
| 134 |
+
padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
|
| 135 |
+
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',
|
| 136 |
+
zIndex: 'var(--z-elevated)', backdropFilter: 'saturate(1.12) blur(8px)'
|
| 137 |
+
});
|
| 138 |
+
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
| 139 |
+
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 140 |
+
|
| 141 |
+
// HTML scaffolding: legend and plots grid as HTML; only pies are SVG
|
| 142 |
+
const legendHost = document.createElement('div'); legendHost.className = 'legend'; container.appendChild(legendHost);
|
| 143 |
+
const plotsHost = document.createElement('div'); plotsHost.className = 'plots-grid'; container.appendChild(plotsHost);
|
| 144 |
+
|
| 145 |
+
// Metrics (order and labels as in the Python script)
|
| 146 |
+
const METRICS = [
|
| 147 |
+
{ key:'answer_total_tokens', name:'Answer Tokens', title:'Weighted by ', letter:'a' },
|
| 148 |
+
{ key:'total_samples', name:'Number of Samples', title:'Weighted by ', letter:'b' },
|
| 149 |
+
{ key:'total_turns', name:'Number of Turns', title:'Weighted by ', letter:'c' },
|
| 150 |
+
{ key:'total_images', name:'Number of Images', title:'Weighted by ', letter:'d' }
|
| 151 |
+
];
|
| 152 |
+
|
| 153 |
+
// CSV: load from public path
|
| 154 |
+
const CSV_PATHS = [
|
| 155 |
+
'/data/vision.csv'
|
| 156 |
+
];
|
| 157 |
+
|
| 158 |
+
const fetchFirstAvailable = async (paths) => {
|
| 159 |
+
for (const p of paths) {
|
| 160 |
+
try {
|
| 161 |
+
const res = await fetch(p, { cache: 'no-cache' });
|
| 162 |
+
if (res.ok) { return await res.text(); }
|
| 163 |
+
} catch (_) { /* try next */ }
|
| 164 |
+
}
|
| 165 |
+
throw new Error('CSV not found: vision.csv');
|
| 166 |
+
};
|
| 167 |
+
|
| 168 |
+
const parseCsv = (text) => d3.csvParse(text, (d) => ({
|
| 169 |
+
subset_name: (d['subset_name']||'').trim(),
|
| 170 |
+
eagle_cathegory: (d['eagle_cathegory']||'').trim(),
|
| 171 |
+
answer_total_tokens: +((d['answer_total_tokens']||'0').toString().trim()) || 0,
|
| 172 |
+
total_samples: +((d['total_samples']||'0').toString().trim()) || 0,
|
| 173 |
+
total_turns: +((d['total_turns']||'0').toString().trim()) || 0,
|
| 174 |
+
total_images: +((d['total_images']||'0').toString().trim()) || 0
|
| 175 |
+
}));
|
| 176 |
+
|
| 177 |
+
// Layout
|
| 178 |
+
let width=800; const margin = { top: 8, right: 24, bottom: 0, left: 24 };
|
| 179 |
+
const CAPTION_GAP = 36; // espace entre titre et donut
|
| 180 |
+
const GAP_X = 20; // espace entre colonnes
|
| 181 |
+
const GAP_Y = 12; // espace entre lignes
|
| 182 |
+
const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
|
| 183 |
+
const DONUT_INNER_RATIO = 0.58; // ratio du trou central (0 = pie plein, 0.5 = moitié)
|
| 184 |
+
// LEGEND_GAP supprimé: l'espacement est désormais géré en CSS via .d3-pie-quad .legend { margin-bottom }
|
| 185 |
+
const SVG_VPAD = 16; // padding vertical supplémentaire à l'intérieur des SVG pour éviter la coupe
|
| 186 |
+
|
| 187 |
+
const updateSize = () => {
|
| 188 |
+
width = container.clientWidth || 800;
|
| 189 |
+
return { innerWidth: width - margin.left - margin.right };
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
function renderLegend(categories, colorOf){
|
| 193 |
+
legendHost.style.display = 'flex';
|
| 194 |
+
legendHost.style.alignItems = 'center';
|
| 195 |
+
legendHost.style.justifyContent = 'center';
|
| 196 |
+
legendHost.innerHTML = `<div class="items">${categories.map(c => `<div class="item" data-category="${c}"><span class=\"swatch\" style=\"background:${colorOf(c)}\"></span><span style=\"font-weight:500\">${c}</span></div>`).join('')}</div>`;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function drawPies(rows){
|
| 200 |
+
const { innerWidth } = updateSize();
|
| 201 |
+
|
| 202 |
+
// Catégories (triées) + échelle de couleurs harmonisée avec banner.html
|
| 203 |
+
const categories = Array.from(new Set(rows.map(r => r.eagle_cathegory || 'Unknown'))).sort();
|
| 204 |
+
const getCatColors = (n) => {
|
| 205 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(_) {}
|
| 206 |
+
return (d3.schemeTableau10 ? d3.schemeTableau10.slice(0, n) : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'].slice(0, n));
|
| 207 |
+
};
|
| 208 |
+
const color = d3.scaleOrdinal().domain(categories).range(getCatColors(categories.length));
|
| 209 |
+
const colorOf = (cat) => color(cat || 'Unknown');
|
| 210 |
+
|
| 211 |
+
// Clear plots grid
|
| 212 |
+
plotsHost.innerHTML = '';
|
| 213 |
+
|
| 214 |
+
// Légende au-dessus, centrée
|
| 215 |
+
renderLegend(categories, colorOf);
|
| 216 |
+
|
| 217 |
+
// Rayon fixé selon la largeur cible d'une cellule (gérée par CSS)
|
| 218 |
+
const CELL_BASIS = 360; // doit correspondre à .pie-cell { flex-basis }
|
| 219 |
+
const radius = Math.max(80, Math.min(120, Math.floor(CELL_BASIS * 0.42)));
|
| 220 |
+
const innerR = Math.round(radius * DONUT_INNER_RATIO);
|
| 221 |
+
// Placement géré par CSS; ici on ne fait que l'espacement vertical minimal
|
| 222 |
+
plotsHost.style.position = 'relative';
|
| 223 |
+
plotsHost.style.marginTop = (TOP_OFFSET) + 'px';
|
| 224 |
+
|
| 225 |
+
const pie = d3.pie().sort(null).value(d => d.value).padAngle(0.02);
|
| 226 |
+
const arc = d3.arc().innerRadius(innerR).outerRadius(radius).cornerRadius(3);
|
| 227 |
+
const arcLabel = d3.arc().innerRadius((innerR + radius) / 2).outerRadius((innerR + radius) / 2);
|
| 228 |
+
|
| 229 |
+
// Légende déjà rendue au-dessus
|
| 230 |
+
|
| 231 |
+
METRICS.forEach((metric, idx) => {
|
| 232 |
+
// Aggregate by category
|
| 233 |
+
const totals = new Map(); categories.forEach(c => totals.set(c, 0));
|
| 234 |
+
rows.forEach(r => { totals.set(r.eagle_cathegory, totals.get(r.eagle_cathegory) + (r[metric.key] || 0)); });
|
| 235 |
+
const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
|
| 236 |
+
const nonZeroValues = values.filter(v => (v.value || 0) > 0);
|
| 237 |
+
const totalSum = d3.sum(nonZeroValues, d => d.value);
|
| 238 |
+
|
| 239 |
+
// Create HTML cell container
|
| 240 |
+
const cell = document.createElement('div');
|
| 241 |
+
cell.className = 'pie-cell';
|
| 242 |
+
cell.style.width = (radius * 2) + 'px';
|
| 243 |
+
cell.style.height = (radius * 2 + SVG_VPAD * 2 + CAPTION_GAP + 24) + 'px';
|
| 244 |
+
cell.style.display = 'flex';
|
| 245 |
+
cell.style.flexDirection = 'column';
|
| 246 |
+
cell.style.alignItems = 'center';
|
| 247 |
+
cell.style.justifyContent = 'flex-start';
|
| 248 |
+
plotsHost.appendChild(cell);
|
| 249 |
+
|
| 250 |
+
// SVG pie inside cell
|
| 251 |
+
const svg = d3.select(cell).append('svg').attr('width', radius * 2).attr('height', radius * 2 + SVG_VPAD * 2).style('display','block');
|
| 252 |
+
const gCell = svg.append('g').attr('transform', `translate(${radius},${radius + SVG_VPAD})`);
|
| 253 |
+
|
| 254 |
+
if (!totalSum || totalSum <= 0 || nonZeroValues.length === 0) {
|
| 255 |
+
gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');
|
| 256 |
+
} else {
|
| 257 |
+
const data = pie(nonZeroValues);
|
| 258 |
+
const percent = (v) => (v / totalSum) * 100;
|
| 259 |
+
|
| 260 |
+
// Slices
|
| 261 |
+
const slices = gCell.selectAll('path.slice').data(data).enter().append('path').attr('class','slice')
|
| 262 |
+
.attr('d', arc)
|
| 263 |
+
.attr('fill', d => colorOf(d.data.category))
|
| 264 |
+
.attr('stroke', 'var(--surface-bg)')
|
| 265 |
+
.attr('stroke-width', 1.2)
|
| 266 |
+
.attr('data-category', d => d.data.category)
|
| 267 |
+
.on('mouseenter', function(ev, d){
|
| 268 |
+
const hoveredCategory = d.data.category;
|
| 269 |
+
d3.select(container).classed('hovering', true);
|
| 270 |
+
d3.select(container).selectAll('path.slice').classed('ghost', s => (s.data && s.data.category) !== hoveredCategory);
|
| 271 |
+
// Ghost legend items that are not hovered
|
| 272 |
+
d3.select(legendHost).selectAll('.item').classed('ghost', function(){ return this.dataset && this.dataset.category !== hoveredCategory; });
|
| 273 |
+
d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
|
| 274 |
+
const p = percent(d.data.value);
|
| 275 |
+
const catColor = colorOf(d.data.category);
|
| 276 |
+
let html = `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${catColor}\"></span><strong>${d.data.category}</strong></div>`;
|
| 277 |
+
html += `<div>${metric.name}</div>`;
|
| 278 |
+
html += `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>Value</strong><span style="margin-left:auto;text-align:right;">${d.data.value.toLocaleString()}</span></div>`;
|
| 279 |
+
/* Share row removed per request */
|
| 280 |
+
tipInner.innerHTML = html;
|
| 281 |
+
tip.style.opacity = '1';
|
| 282 |
+
})
|
| 283 |
+
.on('mousemove', function(ev){
|
| 284 |
+
const [mx, my] = d3.pointer(ev, container); const offsetX = 12, offsetY = 12; tip.style.transform = `translate(${Math.round(mx+offsetX)}px, ${Math.round(my+offsetY)}px)`;
|
| 285 |
+
})
|
| 286 |
+
.on('mouseleave', function(){
|
| 287 |
+
tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)';
|
| 288 |
+
d3.select(container).classed('hovering', false);
|
| 289 |
+
d3.select(container).selectAll('path.slice').classed('ghost', false);
|
| 290 |
+
d3.select(legendHost).selectAll('.item').classed('ghost', false);
|
| 291 |
+
d3.select(this).attr('stroke','var(--surface-bg)');
|
| 292 |
+
});
|
| 293 |
+
|
| 294 |
+
// Percentage labels (>= 3%)
|
| 295 |
+
gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()
|
| 296 |
+
.append('text').attr('class','slice-label').style('pointer-events','none')
|
| 297 |
+
.attr('transform', d => `translate(${arcLabel.centroid(d)})`)
|
| 298 |
+
.attr('text-anchor','middle')
|
| 299 |
+
.text(d => `${percent(d.data.value).toFixed(1)}%`);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
// HTML captions under the SVG (keep design)
|
| 303 |
+
const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center'; cell.appendChild(subtitleEl);
|
| 304 |
+
const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
+
// Container height flows naturally with HTML; nothing to do
|
| 308 |
+
|
| 309 |
+
// Reset global hover/ghost when leaving the plots area
|
| 310 |
+
plotsHost.onmouseleave = () => {
|
| 311 |
+
tip.style.opacity='0';
|
| 312 |
+
tip.style.transform='translate(-9999px, -9999px)';
|
| 313 |
+
d3.select(container).classed('hovering', false);
|
| 314 |
+
d3.select(container).selectAll('path.slice').classed('ghost', false);
|
| 315 |
+
d3.select(legendHost).selectAll('.item').classed('ghost', false);
|
| 316 |
+
};
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
async function init(){
|
| 320 |
+
try {
|
| 321 |
+
const text = await fetchFirstAvailable(CSV_PATHS);
|
| 322 |
+
const rows = parseCsv(text);
|
| 323 |
+
drawPies(rows);
|
| 324 |
+
|
| 325 |
+
// Resize handling
|
| 326 |
+
const rerender = () => drawPies(rows);
|
| 327 |
+
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); }
|
| 328 |
+
else { window.addEventListener('resize', rerender); }
|
| 329 |
+
} catch (err) {
|
| 330 |
+
const pre = document.createElement('pre'); pre.textContent = (err && err.message) ? err.message : String(err);
|
| 331 |
+
pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';
|
| 332 |
+
container.appendChild(pre);
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
init();
|
| 337 |
+
};
|
| 338 |
+
|
| 339 |
+
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
|
| 340 |
+
})();
|
| 341 |
+
</script>
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
|
app/src/content/embeds/d3-pie.html
CHANGED
|
@@ -1,345 +1,154 @@
|
|
| 1 |
<div class="d3-pie"></div>
|
| 2 |
<style>
|
| 3 |
-
|
| 4 |
-
.d3-pie {
|
| 5 |
-
.d3-pie .legend {
|
| 6 |
-
.d3-pie .legend {
|
| 7 |
-
.d3-pie .legend .
|
| 8 |
-
.d3-pie .legend .
|
| 9 |
-
.d3-pie .legend .swatch { width:14px; height:14px; border-radius:3px; display:inline-block; border: 1px solid var(--border-color); }
|
| 10 |
-
.d3-pie .legend .title { display:block; text-align:center; font-weight:800; margin-bottom:6px; }
|
| 11 |
-
.d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
|
| 12 |
-
.d3-pie .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }
|
| 13 |
-
.d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
|
| 14 |
/* Ghost legend items when hovering slices */
|
| 15 |
.d3-pie.hovering .legend .item.ghost { opacity: .35; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
.d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: var(--transparent-page-contrast); stroke-width: 3px; }
|
| 17 |
-
|
| 18 |
-
.d3-pie .
|
| 19 |
-
|
| 20 |
-
}
|
| 21 |
-
.d3-pie.
|
| 22 |
-
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
.d3-pie .plots-grid {
|
| 26 |
-
display: flex;
|
| 27 |
-
flex-wrap: wrap;
|
| 28 |
-
justify-content: center;
|
| 29 |
-
align-items: flex-start;
|
| 30 |
-
gap: 12px 20px;
|
| 31 |
-
margin-top: 4px;
|
| 32 |
-
margin-left: auto;
|
| 33 |
-
margin-right: auto;
|
| 34 |
-
width: 100%;
|
| 35 |
-
}
|
| 36 |
-
/* Par défaut (flux ~1280): 2 colonnes centrées */
|
| 37 |
-
.content-grid .d3-pie .plots-grid { width: 100%; }
|
| 38 |
-
.content-grid .d3-pie .pie-cell { flex: 0 0 calc((100% - 20px)/2); }
|
| 39 |
-
/* En wrappers larges: viser 4 colonnes si l'espace le permet */
|
| 40 |
-
.wide .d3-pie .plots-grid,
|
| 41 |
-
.full-width .d3-pie .plots-grid { width: 100%; }
|
| 42 |
-
.wide .d3-pie .pie-cell,
|
| 43 |
-
.full-width .d3-pie .pie-cell { flex: 0 0 calc((100% - 60px)/4); }
|
| 44 |
-
/* Forcer 2 colonnes dans le flux lorsque le parent ~1280px */
|
| 45 |
-
.content-grid .d3-pie .plots-grid { width: min(740px, 100%); }
|
| 46 |
-
.d3-pie .pie-cell {
|
| 47 |
-
display: flex;
|
| 48 |
-
flex-direction: column;
|
| 49 |
-
align-items: center;
|
| 50 |
-
flex: 0 0 360px; /* 2 colonnes fixes dans le flux à 1280px */
|
| 51 |
-
}
|
| 52 |
-
/* 4/2/1 colonnes en fonction de la largeur du parent */
|
| 53 |
-
/* @container (min-width: 740px) {
|
| 54 |
-
.d3-pie .plots-grid { width: 740px; }
|
| 55 |
-
}
|
| 56 |
-
@container (max-width: 739.98px) {
|
| 57 |
-
.d3-pie .plots-grid { width: 100%; }
|
| 58 |
-
} */
|
| 59 |
-
@media (max-width: 500px) {
|
| 60 |
-
.d3-pie .pie-cell { flex: 0 0 100%; }
|
| 61 |
-
}
|
| 62 |
-
/* Tooltip styling aligned with filters-quad */
|
| 63 |
-
.d3-pie .d3-tooltip {
|
| 64 |
-
z-index: var(--z-elevated);
|
| 65 |
-
backdrop-filter: saturate(1.12) blur(8px);
|
| 66 |
-
}
|
| 67 |
-
.d3-pie .d3-tooltip__inner {
|
| 68 |
-
display: flex;
|
| 69 |
-
flex-direction: column;
|
| 70 |
-
gap: 6px;
|
| 71 |
-
min-width: 220px;
|
| 72 |
-
}
|
| 73 |
-
.d3-pie .d3-tooltip__inner > div:first-child {
|
| 74 |
-
font-weight: 800;
|
| 75 |
-
letter-spacing: 0.1px;
|
| 76 |
-
margin-bottom: 0;
|
| 77 |
-
}
|
| 78 |
-
.d3-pie .d3-tooltip__inner > div:nth-child(2) {
|
| 79 |
-
font-size: 11px;
|
| 80 |
-
color: var(--muted-color);
|
| 81 |
-
display: block;
|
| 82 |
-
margin-top: -4px;
|
| 83 |
-
margin-bottom: 2px;
|
| 84 |
-
letter-spacing: 0.1px;
|
| 85 |
-
}
|
| 86 |
-
.d3-pie .d3-tooltip__inner > div:nth-child(n+3) {
|
| 87 |
-
padding-top: 6px;
|
| 88 |
-
border-top: 1px solid var(--border-color);
|
| 89 |
-
}
|
| 90 |
-
.d3-pie .d3-tooltip__color-dot {
|
| 91 |
-
display: inline-block;
|
| 92 |
-
width: 12px;
|
| 93 |
-
height: 12px;
|
| 94 |
-
border-radius: 3px;
|
| 95 |
-
border: 1px solid var(--border-color);
|
| 96 |
-
}
|
| 97 |
</style>
|
| 98 |
<script>
|
| 99 |
(() => {
|
| 100 |
-
const THIS_SCRIPT = document.currentScript;
|
| 101 |
const ensureD3 = (cb) => {
|
| 102 |
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 103 |
let s = document.getElementById('d3-cdn-script');
|
| 104 |
-
if (!s) { s = document.createElement('script'); s.id
|
| 105 |
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 106 |
-
s.addEventListener('load', onReady, { once:
|
| 107 |
-
if (window.d3) onReady();
|
| 108 |
};
|
| 109 |
|
| 110 |
const bootstrap = () => {
|
| 111 |
-
const scriptEl =
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
container =
|
| 116 |
-
}
|
| 117 |
-
if (!container) {
|
| 118 |
-
let sib = scriptEl && scriptEl.previousElementSibling;
|
| 119 |
-
while (sib && !(sib.classList && sib.classList.contains('d3-pie'))) {
|
| 120 |
-
sib = sib.previousElementSibling;
|
| 121 |
-
}
|
| 122 |
-
container = sib || document.querySelector('.d3-pie');
|
| 123 |
}
|
| 124 |
if (!container) return;
|
| 125 |
-
if (container.dataset) { if (container.dataset.mounted
|
| 126 |
|
| 127 |
-
// Tooltip
|
| 128 |
container.style.position = container.style.position || 'relative';
|
| 129 |
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 130 |
-
if (!tip) {
|
| 131 |
-
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
| 132 |
-
Object.assign(tip.style, {
|
| 133 |
-
position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
|
| 134 |
-
padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
|
| 135 |
-
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',
|
| 136 |
-
zIndex: 'var(--z-elevated)', backdropFilter: 'saturate(1.12) blur(8px)'
|
| 137 |
-
});
|
| 138 |
-
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
| 139 |
-
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 140 |
|
| 141 |
-
|
| 142 |
-
const
|
| 143 |
-
const plotsHost = document.createElement('div'); plotsHost.className = 'plots-grid'; container.appendChild(plotsHost);
|
| 144 |
|
| 145 |
-
|
| 146 |
-
const
|
| 147 |
-
{ key:'answer_total_tokens', name:'Answer Tokens', title:'Weighted by ', letter:'a' },
|
| 148 |
-
{ key:'total_samples', name:'Number of Samples', title:'Weighted by ', letter:'b' },
|
| 149 |
-
{ key:'total_turns', name:'Number of Turns', title:'Weighted by ', letter:'c' },
|
| 150 |
-
{ key:'total_images', name:'Number of Images', title:'Weighted by ', letter:'d' }
|
| 151 |
-
];
|
| 152 |
-
|
| 153 |
-
// CSV: load from public path
|
| 154 |
-
const CSV_PATHS = [
|
| 155 |
-
'/data/vision.csv'
|
| 156 |
-
];
|
| 157 |
|
|
|
|
| 158 |
const fetchFirstAvailable = async (paths) => {
|
| 159 |
-
for (const p of paths) {
|
| 160 |
-
try {
|
| 161 |
-
const res = await fetch(p, { cache: 'no-cache' });
|
| 162 |
-
if (res.ok) { return await res.text(); }
|
| 163 |
-
} catch (_) { /* try next */ }
|
| 164 |
-
}
|
| 165 |
throw new Error('CSV not found: vision.csv');
|
| 166 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
total_images: +((d['total_images']||'0').toString().trim()) || 0
|
| 175 |
-
}));
|
| 176 |
-
|
| 177 |
-
// Layout
|
| 178 |
-
let width=800; const margin = { top: 8, right: 24, bottom: 0, left: 24 };
|
| 179 |
-
const CAPTION_GAP = 36; // espace entre titre et donut
|
| 180 |
-
const GAP_X = 20; // espace entre colonnes
|
| 181 |
-
const GAP_Y = 12; // espace entre lignes
|
| 182 |
-
const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
|
| 183 |
-
const DONUT_INNER_RATIO = 0.58; // ratio du trou central (0 = pie plein, 0.5 = moitié)
|
| 184 |
-
// LEGEND_GAP supprimé: l'espacement est désormais géré en CSS via .d3-pie .legend { margin-bottom }
|
| 185 |
-
const SVG_VPAD = 16; // padding vertical supplémentaire à l'intérieur des SVG pour éviter la coupe
|
| 186 |
-
|
| 187 |
-
const updateSize = () => {
|
| 188 |
-
width = container.clientWidth || 800;
|
| 189 |
-
return { innerWidth: width - margin.left - margin.right };
|
| 190 |
-
};
|
| 191 |
-
|
| 192 |
-
function renderLegend(categories, colorOf){
|
| 193 |
-
legendHost.style.display = 'flex';
|
| 194 |
-
legendHost.style.alignItems = 'center';
|
| 195 |
-
legendHost.style.justifyContent = 'center';
|
| 196 |
-
legendHost.innerHTML = `<div class="items">${categories.map(c => `<div class="item" data-category="${c}"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:500">${c}</span></div>`).join('')}</div>`;
|
| 197 |
}
|
| 198 |
|
| 199 |
-
function
|
| 200 |
-
const
|
|
|
|
|
|
|
| 201 |
|
| 202 |
-
|
| 203 |
-
const
|
| 204 |
-
const
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
const
|
| 209 |
-
const colorOf = (cat) => color(cat || 'Unknown');
|
| 210 |
|
| 211 |
-
|
| 212 |
-
plotsHost.innerHTML = '';
|
| 213 |
|
| 214 |
-
|
| 215 |
-
|
|
|
|
| 216 |
|
| 217 |
-
|
| 218 |
-
const CELL_BASIS = 360; // doit correspondre à .pie-cell { flex-basis }
|
| 219 |
-
const radius = Math.max(80, Math.min(120, Math.floor(CELL_BASIS * 0.42)));
|
| 220 |
const innerR = Math.round(radius * DONUT_INNER_RATIO);
|
| 221 |
-
|
| 222 |
-
plotsHost.style.position = 'relative';
|
| 223 |
-
plotsHost.style.marginTop = (TOP_OFFSET) + 'px';
|
| 224 |
-
|
| 225 |
-
const pie = d3.pie().sort(null).value(d => d.value).padAngle(0.02);
|
| 226 |
const arc = d3.arc().innerRadius(innerR).outerRadius(radius).cornerRadius(3);
|
| 227 |
-
const arcLabel = d3.arc().innerRadius((innerR + radius)
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
.attr('stroke-width', 1.2)
|
| 266 |
-
.attr('data-category', d => d.data.category)
|
| 267 |
-
.on('mouseenter', function(ev, d){
|
| 268 |
-
const hoveredCategory = d.data.category;
|
| 269 |
-
d3.select(container).classed('hovering', true);
|
| 270 |
-
d3.select(container).selectAll('path.slice').classed('ghost', s => (s.data && s.data.category) !== hoveredCategory);
|
| 271 |
-
// Ghost legend items that are not hovered
|
| 272 |
-
d3.select(legendHost).selectAll('.item').classed('ghost', function(){ return this.dataset && this.dataset.category !== hoveredCategory; });
|
| 273 |
-
d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
|
| 274 |
-
const p = percent(d.data.value);
|
| 275 |
-
const catColor = colorOf(d.data.category);
|
| 276 |
-
let html = `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${catColor}\"></span><strong>${d.data.category}</strong></div>`;
|
| 277 |
-
html += `<div>${metric.name}</div>`;
|
| 278 |
-
html += `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>Value</strong><span style="margin-left:auto;text-align:right;">${d.data.value.toLocaleString()}</span></div>`;
|
| 279 |
-
/* Share row removed per request */
|
| 280 |
-
tipInner.innerHTML = html;
|
| 281 |
-
tip.style.opacity = '1';
|
| 282 |
-
})
|
| 283 |
-
.on('mousemove', function(ev){
|
| 284 |
-
const [mx, my] = d3.pointer(ev, container); const offsetX = 12, offsetY = 12; tip.style.transform = `translate(${Math.round(mx+offsetX)}px, ${Math.round(my+offsetY)}px)`;
|
| 285 |
-
})
|
| 286 |
-
.on('mouseleave', function(){
|
| 287 |
-
tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)';
|
| 288 |
-
d3.select(container).classed('hovering', false);
|
| 289 |
-
d3.select(container).selectAll('path.slice').classed('ghost', false);
|
| 290 |
-
d3.select(legendHost).selectAll('.item').classed('ghost', false);
|
| 291 |
-
d3.select(this).attr('stroke','var(--surface-bg)');
|
| 292 |
-
});
|
| 293 |
-
|
| 294 |
-
// Percentage labels (>= 3%)
|
| 295 |
-
gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()
|
| 296 |
-
.append('text').attr('class','slice-label').style('pointer-events','none')
|
| 297 |
-
.attr('transform', d => `translate(${arcLabel.centroid(d)})`)
|
| 298 |
-
.attr('text-anchor','middle')
|
| 299 |
-
.text(d => `${percent(d.data.value).toFixed(1)}%`);
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
// HTML captions under the SVG (keep design)
|
| 303 |
-
const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center'; cell.appendChild(subtitleEl);
|
| 304 |
-
const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);
|
| 305 |
-
});
|
| 306 |
-
|
| 307 |
-
// Container height flows naturally with HTML; nothing to do
|
| 308 |
-
|
| 309 |
-
// Reset global hover/ghost when leaving the plots area
|
| 310 |
-
plotsHost.onmouseleave = () => {
|
| 311 |
-
tip.style.opacity='0';
|
| 312 |
-
tip.style.transform='translate(-9999px, -9999px)';
|
| 313 |
-
d3.select(container).classed('hovering', false);
|
| 314 |
-
d3.select(container).selectAll('path.slice').classed('ghost', false);
|
| 315 |
-
d3.select(legendHost).selectAll('.item').classed('ghost', false);
|
| 316 |
-
};
|
| 317 |
}
|
| 318 |
|
| 319 |
-
async
|
| 320 |
try {
|
| 321 |
-
const text = await fetchFirstAvailable(
|
| 322 |
const rows = parseCsv(text);
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
else { window.addEventListener('resize', rerender); }
|
| 329 |
-
} catch (err) {
|
| 330 |
-
const pre = document.createElement('pre'); pre.textContent = (err && err.message) ? err.message : String(err);
|
| 331 |
-
pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';
|
| 332 |
-
container.appendChild(pre);
|
| 333 |
}
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
init();
|
| 337 |
};
|
| 338 |
|
| 339 |
-
if (document.readyState
|
| 340 |
})();
|
| 341 |
</script>
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
| 1 |
<div class="d3-pie"></div>
|
| 2 |
<style>
|
| 3 |
+
.d3-pie { position: relative; }
|
| 4 |
+
.d3-pie .legend { display:flex; flex-direction:column; align-items:flex-start; gap:6px; margin: 8px 0 0 0; font-size:12px; color: var(--text-color); }
|
| 5 |
+
.d3-pie .legend .legend-title { font-size:12px; font-weight:700; color: var(--text-color); }
|
| 6 |
+
.d3-pie .legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; }
|
| 7 |
+
.d3-pie .legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
|
| 8 |
+
.d3-pie .legend .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
/* Ghost legend items when hovering slices */
|
| 10 |
.d3-pie.hovering .legend .item.ghost { opacity: .35; }
|
| 11 |
+
/* Ghost effect on slices */
|
| 12 |
+
.d3-pie .slice { transition: opacity .15s ease; }
|
| 13 |
+
.d3-pie.hovering .slice.ghost { opacity: .25; }
|
| 14 |
+
/* Labels with contrast liseret */
|
| 15 |
.d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: var(--transparent-page-contrast); stroke-width: 3px; }
|
| 16 |
+
.d3-pie .d3-tooltip { position:absolute; top:0; left:0; transform:translate(-9999px,-9999px); pointer-events:none; padding:8px 10px; border-radius:8px; font-size:12px; line-height:1.35; border:1px solid var(--border-color); background:var(--surface-bg); color:var(--text-color); box-shadow:0 4px 24px rgba(0,0,0,.18); opacity:0; transition:opacity .12s ease; }
|
| 17 |
+
.d3-pie .d3-tooltip { z-index: var(--z-elevated); backdrop-filter: saturate(1.12) blur(8px); }
|
| 18 |
+
.d3-pie .d3-tooltip__inner { display:flex; flex-direction:column; gap:6px; min-width: 220px; text-align: left; }
|
| 19 |
+
.d3-pie .d3-tooltip__inner > div:first-child { font-weight: 800; letter-spacing: 0.1px; margin-bottom: 0; }
|
| 20 |
+
.d3-pie .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--muted-color); display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.1px; }
|
| 21 |
+
.d3-pie .d3-tooltip__inner > div:nth-child(n+3) { padding-top: 6px; border-top: 1px solid var(--border-color); }
|
| 22 |
+
.d3-pie .d3-tooltip .swatch { width:12px; height:12px; border-radius:3px; border:1px solid var(--border-color); display:inline-block; margin-right:6px; }
|
| 23 |
+
.d3-pie .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
</style>
|
| 25 |
<script>
|
| 26 |
(() => {
|
|
|
|
| 27 |
const ensureD3 = (cb) => {
|
| 28 |
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 29 |
let s = document.getElementById('d3-cdn-script');
|
| 30 |
+
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); }
|
| 31 |
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 32 |
+
s.addEventListener('load', onReady, { once:true }); if (window.d3) onReady();
|
|
|
|
| 33 |
};
|
| 34 |
|
| 35 |
const bootstrap = () => {
|
| 36 |
+
const scriptEl = document.currentScript;
|
| 37 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 38 |
+
if (!(container && container.classList && container.classList.contains('d3-pie'))){
|
| 39 |
+
const cs = Array.from(document.querySelectorAll('.d3-pie')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
|
| 40 |
+
container = cs[cs.length-1] || null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
if (!container) return;
|
| 43 |
+
if (container.dataset) { if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
|
| 44 |
|
|
|
|
| 45 |
container.style.position = container.style.position || 'relative';
|
| 46 |
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 47 |
+
if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; tipInner = document.createElement('div'); tipInner.className='d3-tooltip__inner'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
|
| 50 |
+
const legend = document.createElement('div'); legend.className = 'legend'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>'; container.appendChild(legend);
|
|
|
|
| 51 |
|
| 52 |
+
const svg = d3.select(card).append('svg').attr('width','100%').style('display','block');
|
| 53 |
+
const gRoot = svg.append('g');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
const DEFAULT_CSV = '/data/vision.csv';
|
| 56 |
const fetchFirstAvailable = async (paths) => {
|
| 57 |
+
for (const p of paths) { try { const r = await fetch(p, { cache:'no-cache' }); if (r.ok) return await r.text(); } catch(_){} }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
throw new Error('CSV not found: vision.csv');
|
| 59 |
};
|
| 60 |
+
function parseCsv(text){
|
| 61 |
+
return d3.csvParse(text, d => ({
|
| 62 |
+
category: (d['eagle_cathegory']||d['category']||'').trim(),
|
| 63 |
+
value: +((d['total_samples']||d['value']||'0').toString().trim()) || 0
|
| 64 |
+
}));
|
| 65 |
+
}
|
| 66 |
|
| 67 |
+
let width=800, height=340; const DONUT_INNER_RATIO = 0.6;
|
| 68 |
+
function updateSize(){
|
| 69 |
+
width = container.clientWidth || 800; height = Math.max(240, Math.round(width/3));
|
| 70 |
+
svg.attr('width', width).attr('height', height);
|
| 71 |
+
gRoot.attr('transform', `translate(${width/2},${height/2})`);
|
| 72 |
+
return { inner: Math.min(width, height) * 0.42 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
|
| 75 |
+
function makeLegend(categories, colorOf){
|
| 76 |
+
const items = legend.querySelector('.items'); items.innerHTML = '';
|
| 77 |
+
categories.forEach(name => { const el = document.createElement('span'); el.className='item'; el.dataset.category=name; const sw=document.createElement('span'); sw.className='swatch'; sw.style.background=colorOf(name); const txt=document.createElement('span'); txt.textContent=name; el.appendChild(sw); el.appendChild(txt); items.appendChild(el); });
|
| 78 |
+
}
|
| 79 |
|
| 80 |
+
function render(rows){
|
| 81 |
+
const { inner } = updateSize();
|
| 82 |
+
const categories = Array.from(new Set(rows.map(r => r.category || 'Unknown'))).sort();
|
| 83 |
+
const getColors = (n) => { try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') return window.ColorPalettes.getColors('categorical', n); } catch(_){} return (window.d3 && d3.schemeTableau10) ? d3.schemeTableau10.slice(0, n) : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'].slice(0,n); };
|
| 84 |
+
const palette = getColors(categories.length);
|
| 85 |
+
const color = d3.scaleOrdinal().domain(categories).range(palette);
|
| 86 |
+
const colorOf = (c) => color(c || 'Unknown');
|
|
|
|
| 87 |
|
| 88 |
+
makeLegend(categories, colorOf);
|
|
|
|
| 89 |
|
| 90 |
+
const totals = new Map(); categories.forEach(c => totals.set(c, 0)); rows.forEach(r => totals.set(r.category, (totals.get(r.category)||0) + (r.value||0)));
|
| 91 |
+
const values = categories.map(c => ({ category:c, value: totals.get(c)||0 })).filter(d => d.value > 0);
|
| 92 |
+
const sum = d3.sum(values, d=>d.value) || 1;
|
| 93 |
|
| 94 |
+
const radius = Math.max(60, Math.min(inner, 120));
|
|
|
|
|
|
|
| 95 |
const innerR = Math.round(radius * DONUT_INNER_RATIO);
|
| 96 |
+
const pie = d3.pie().sort(null).value(d=>d.value).padAngle(0.02);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
const arc = d3.arc().innerRadius(innerR).outerRadius(radius).cornerRadius(3);
|
| 98 |
+
const arcLabel = d3.arc().innerRadius((innerR + radius)/2).outerRadius((innerR + radius)/2);
|
| 99 |
+
|
| 100 |
+
const data = pie(values);
|
| 101 |
+
const slices = gRoot.selectAll('path.slice').data(data, d=>d.data.category);
|
| 102 |
+
slices.enter().append('path').attr('class','slice')
|
| 103 |
+
.attr('fill', d=>colorOf(d.data.category))
|
| 104 |
+
.attr('stroke','var(--surface-bg)')
|
| 105 |
+
.attr('stroke-width',1)
|
| 106 |
+
.attr('data-category', d => d.data.category)
|
| 107 |
+
.on('mouseenter', (ev, d) => {
|
| 108 |
+
const pct = (d.data.value / sum) * 100;
|
| 109 |
+
container.classList.add('hovering');
|
| 110 |
+
gRoot.selectAll('path.slice').classed('ghost', s => (s && s.data && s.data.category) !== d.data.category);
|
| 111 |
+
try { const items = legend.querySelectorAll('.item'); items.forEach(it => it.classList.toggle('ghost', it.dataset.category !== d.data.category)); } catch(_) {}
|
| 112 |
+
const colorSw = colorOf(d.data.category);
|
| 113 |
+
tipInner.innerHTML = `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"swatch\" style=\"background:${colorSw}\"></span><strong>${d.data.category}</strong></div>` +
|
| 114 |
+
`<div>Value</div>` +
|
| 115 |
+
`<div style=\"display:flex;align-items:center;gap:6px;white-space:nowrap;\"><strong>Total</strong><span style=\"margin-left:auto;text-align:right;\">${d.data.value.toLocaleString()} (${pct.toFixed(1)}%)</span></div>`;
|
| 116 |
+
tip.style.opacity='1';
|
| 117 |
+
})
|
| 118 |
+
.on('mousemove', (ev) => { const [mx,my] = d3.pointer(ev, container); tip.style.transform = `translate(${Math.round(mx+12)}px, ${Math.round(my+12)}px)`; })
|
| 119 |
+
.on('mouseleave', () => {
|
| 120 |
+
tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)';
|
| 121 |
+
container.classList.remove('hovering');
|
| 122 |
+
gRoot.selectAll('path.slice').classed('ghost', false);
|
| 123 |
+
try { const items = legend.querySelectorAll('.item'); items.forEach(it => it.classList.remove('ghost')); } catch(_) {}
|
| 124 |
+
})
|
| 125 |
+
.merge(slices)
|
| 126 |
+
.attr('d', arc)
|
| 127 |
+
.attr('fill', d=>colorOf(d.data.category));
|
| 128 |
+
slices.exit().remove();
|
| 129 |
+
|
| 130 |
+
const labels = gRoot.selectAll('text.slice-label').data(data.filter(d => (d.data.value/sum) >= 0.03), d=>d.data.category);
|
| 131 |
+
labels.enter().append('text').attr('class','slice-label').attr('text-anchor','middle')
|
| 132 |
+
.merge(labels)
|
| 133 |
+
.attr('transform', d => `translate(${arcLabel.centroid(d)})`)
|
| 134 |
+
.text(d => `${((d.data.value/sum)*100).toFixed(1)}%`);
|
| 135 |
+
labels.exit().remove();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
}
|
| 137 |
|
| 138 |
+
(async () => {
|
| 139 |
try {
|
| 140 |
+
const text = await fetchFirstAvailable([DEFAULT_CSV, './assets/data/vision.csv', '../assets/data/vision.csv']);
|
| 141 |
const rows = parseCsv(text);
|
| 142 |
+
render(rows);
|
| 143 |
+
const rerender = () => render(rows);
|
| 144 |
+
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
|
| 145 |
+
} catch (e) {
|
| 146 |
+
const pre = document.createElement('pre'); pre.textContent = (e && e.message) ? e.message : String(e); pre.style.color='var(--danger, #b00020)'; pre.style.fontSize='12px'; pre.style.whiteSpace='pre-wrap'; container.appendChild(pre);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
}
|
| 148 |
+
})();
|
|
|
|
|
|
|
| 149 |
};
|
| 150 |
|
| 151 |
+
if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once:true }); } else { ensureD3(bootstrap); }
|
| 152 |
})();
|
| 153 |
</script>
|
| 154 |
|
|
|
|
|
|
|
|
|
app/src/content/embeds/vibe-code-d3-embeds-directives.md
CHANGED
|
@@ -405,6 +405,52 @@ function makeLegend(seriesNames, colorFor) {
|
|
| 405 |
- Axes/labels are legible at small widths.
|
| 406 |
- Code is easy to skim: clear naming, early returns, short functions.
|
| 407 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
### 15) Example: small bar chart (structure only)
|
| 409 |
```html
|
| 410 |
<div class="d3-mini-bar"></div>
|
|
|
|
| 405 |
- Axes/labels are legible at small widths.
|
| 406 |
- Code is easy to skim: clear naming, early returns, short functions.
|
| 407 |
|
| 408 |
+
### 14.1) Agent Checklist (operational)
|
| 409 |
+
- Ensure root: one `<div .d3-xyz>` + scoped `<style>` + IIFE `<script>`
|
| 410 |
+
- Gate mount with `data-mounted` and select closest previous sibling instance
|
| 411 |
+
- Load D3 once via `#d3-cdn-script`; verify `window.d3.select`
|
| 412 |
+
- Colors from `window.ColorPalettes` with CSS variable fallbacks
|
| 413 |
+
- Legend present with visible title “Legend”; HTML-based, not SVG
|
| 414 |
+
- Controls in HTML only; if metric select exists, label text must be “Metric”
|
| 415 |
+
- Tooltip is a single absolute `.d3-tooltip` within the container
|
| 416 |
+
- Data load public-first; implement `fetchFirstAvailable([...])` with `cache:'no-cache'`
|
| 417 |
+
- Read optional HtmlEmbed `data-datafiles` and `data-config` per section 6.1
|
| 418 |
+
- Responsiveness: width from container; `ResizeObserver` fallback to `window.resize`
|
| 419 |
+
- Axis/tick/grid use CSS variables (`--axis-color`, `--tick-color`, `--grid-color`)
|
| 420 |
+
- SVG for marks only; UI/legend/controls in HTML
|
| 421 |
+
- No globals leaked; no external runtime deps besides D3/TFJS when necessary
|
| 422 |
+
- Error path: append small red `<pre>` with a readable message inside container
|
| 423 |
+
- Print-friendly: `svg` width 100%, height responsive, avoid heavy bitmaps
|
| 424 |
+
|
| 425 |
+
### 14.2) Definition of Done (DoD)
|
| 426 |
+
- Implements root structure and mounting guard
|
| 427 |
+
- Uses `ColorPalettes` (with safe fallback) and CSS variables for theming
|
| 428 |
+
- Legend with title “Legend” and consistent swatch style (14×14, r=3, 1px border)
|
| 429 |
+
- Metric select labelled “Metric” when present; accessible markup (`<label for>`)
|
| 430 |
+
- Tooltip works (show on hover, hide on leave, positioned via `d3.pointer`)
|
| 431 |
+
- Public-first data loading + HtmlEmbed prop support when applicable
|
| 432 |
+
- Responsive: resizes smoothly; axes and grid legible at small widths
|
| 433 |
+
- No console errors; graceful error message on load failures
|
| 434 |
+
- File is self‑contained; no globals; lints pass
|
| 435 |
+
|
| 436 |
+
### 14.3) Prompt modèle (for the agent)
|
| 437 |
+
```markdown
|
| 438 |
+
You are implementing a self-contained D3 embed fragment.
|
| 439 |
+
Name: d3-<type>.html (root class .d3-<type>)
|
| 440 |
+
Requirements:
|
| 441 |
+
- One root div + scoped style + IIFE script; no globals
|
| 442 |
+
- UI in HTML (legend+controls), chart primitives in SVG
|
| 443 |
+
- Legend title text exactly “Legend”; swatch 14×14, r=3, 1px border
|
| 444 |
+
- If a select toggles metrics, visible label text exactly “Metric”
|
| 445 |
+
- Colors via window.ColorPalettes (categorical/sequential/diverging), fallback to CSS variables or Tableau10
|
| 446 |
+
- Tooltip: single .d3-tooltip inside container, HTML, positioned via d3.pointer
|
| 447 |
+
- Data loading: try `/data/<file>` first, then `./assets/data/<file>`, `../assets/data/<file>`; implement fetchFirstAvailable(paths)
|
| 448 |
+
- Read optional HtmlEmbed attributes `data-datafiles` and `data-config` if present (see section 6.1)
|
| 449 |
+
- Responsiveness: compute width from container, use ResizeObserver; axis/tick/grid via CSS vars
|
| 450 |
+
- Error handling: append small red <pre> inside container on failure
|
| 451 |
+
Deliver one .html file with only the required elements.
|
| 452 |
+
```
|
| 453 |
+
|
| 454 |
### 15) Example: small bar chart (structure only)
|
| 455 |
```html
|
| 456 |
<div class="d3-mini-bar"></div>
|
tools/duplicated-spaces/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# duplicated-spaces
|
| 2 |
+
|
| 3 |
+
Small Poetry project to list public Spaces created in the last N days that were duplicated from a given source Space.
|
| 4 |
+
|
| 5 |
+
## Setup
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
cd tools/duplicated-spaces
|
| 9 |
+
poetry install --no-root
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
Optionally export your token:
|
| 13 |
+
|
| 14 |
+
```bash
|
| 15 |
+
export HF_TOKEN=hf_xxx
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
## Usage
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
poetry run find-duplicated-spaces --source owner/space-name --days 14
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
Options:
|
| 25 |
+
- `--source`: required. The source Space in the form `owner/space-name`.
|
| 26 |
+
- `--days`: optional. Time window in days (default: 14).
|
| 27 |
+
- `--token`: optional. Your HF token. Defaults to `HF_TOKEN` env var if set.
|
| 28 |
+
- `--no-deep`: optional. Disable README/frontmatter fallback detection.
|
| 29 |
+
|
| 30 |
+
The tool checks card metadata and may fallback to README frontmatter parsing for robustness.
|
| 31 |
+
|
| 32 |
+
|
tools/duplicated-spaces/duplicated_spaces/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .finder import find_duplicated_spaces
|
| 2 |
+
|
| 3 |
+
__all__ = ["find_duplicated_spaces"]
|
| 4 |
+
|
| 5 |
+
|
tools/duplicated-spaces/duplicated_spaces/cli.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
from huggingface_hub import HfApi
|
| 8 |
+
|
| 9 |
+
from .finder import find_duplicated_spaces
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 13 |
+
parser = argparse.ArgumentParser(
|
| 14 |
+
description="List recent Spaces duplicated from a given Space"
|
| 15 |
+
)
|
| 16 |
+
parser.add_argument(
|
| 17 |
+
"--source",
|
| 18 |
+
required=True,
|
| 19 |
+
help="Source Space in the form 'owner/space-name'",
|
| 20 |
+
)
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--days",
|
| 23 |
+
type=int,
|
| 24 |
+
default=14,
|
| 25 |
+
help="Time window in days (default: 14)",
|
| 26 |
+
)
|
| 27 |
+
parser.add_argument(
|
| 28 |
+
"--token",
|
| 29 |
+
default=os.environ.get("HF_TOKEN"),
|
| 30 |
+
help="Hugging Face token (optional). Defaults to HF_TOKEN env var if set.",
|
| 31 |
+
)
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--no-deep",
|
| 34 |
+
action="store_true",
|
| 35 |
+
help=(
|
| 36 |
+
"Disable deep detection (README/frontmatter fetch) when card metadata is missing."
|
| 37 |
+
),
|
| 38 |
+
)
|
| 39 |
+
return parser
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main(argv: Optional[list[str]] = None) -> None:
|
| 43 |
+
parser = build_parser()
|
| 44 |
+
args = parser.parse_args(argv)
|
| 45 |
+
|
| 46 |
+
api = HfApi(token=args.token)
|
| 47 |
+
duplicated = find_duplicated_spaces(
|
| 48 |
+
api=api,
|
| 49 |
+
source=args.source,
|
| 50 |
+
days=args.days,
|
| 51 |
+
deep_detection=not args.no_deep,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
if duplicated:
|
| 55 |
+
print(
|
| 56 |
+
f"Found {len(duplicated)} Space(s) duplicated from {args.source} in the last {args.days} days:\n"
|
| 57 |
+
)
|
| 58 |
+
for sid in duplicated:
|
| 59 |
+
print(f"https://huggingface.co/spaces/{sid}")
|
| 60 |
+
else:
|
| 61 |
+
print(
|
| 62 |
+
f"No public Spaces duplicated from {args.source} in the last {args.days} days."
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|
| 68 |
+
|
| 69 |
+
|
app/scripts/find_duplicated_spaces.py → tools/duplicated-spaces/duplicated_spaces/finder.py
RENAMED
|
@@ -1,22 +1,10 @@
|
|
| 1 |
-
|
| 2 |
-
"""
|
| 3 |
-
Find Spaces created in the last N days that were duplicated from a given source Space.
|
| 4 |
-
|
| 5 |
-
This script uses the public Hugging Face Hub APIs via `huggingface_hub` and optionally
|
| 6 |
-
falls back to reading README frontmatter for robustness.
|
| 7 |
-
|
| 8 |
-
Usage:
|
| 9 |
-
python app/scripts/find_duplicated_spaces.py --source owner/space-name [--days 14] [--token <hf_token>] [--no-deep]
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
"""
|
| 15 |
|
| 16 |
-
from __future__ import annotations
|
| 17 |
-
|
| 18 |
-
import argparse
|
| 19 |
-
import os
|
| 20 |
from datetime import datetime, timedelta, timezone
|
| 21 |
from typing import Iterable, List, Optional
|
| 22 |
|
|
@@ -24,39 +12,8 @@ import requests
|
|
| 24 |
from huggingface_hub import HfApi
|
| 25 |
|
| 26 |
|
| 27 |
-
def parse_args() -> argparse.Namespace:
|
| 28 |
-
parser = argparse.ArgumentParser(
|
| 29 |
-
description="List recent Spaces duplicated from a given Space"
|
| 30 |
-
)
|
| 31 |
-
parser.add_argument(
|
| 32 |
-
"--source",
|
| 33 |
-
required=True,
|
| 34 |
-
help="Source Space in the form 'owner/space-name'",
|
| 35 |
-
)
|
| 36 |
-
parser.add_argument(
|
| 37 |
-
"--days",
|
| 38 |
-
type=int,
|
| 39 |
-
default=14,
|
| 40 |
-
help="Time window in days (default: 14)",
|
| 41 |
-
)
|
| 42 |
-
parser.add_argument(
|
| 43 |
-
"--token",
|
| 44 |
-
default=os.environ.get("HF_TOKEN"),
|
| 45 |
-
help="Hugging Face token (optional). Defaults to HF_TOKEN env var if set.",
|
| 46 |
-
)
|
| 47 |
-
parser.add_argument(
|
| 48 |
-
"--no-deep",
|
| 49 |
-
action="store_true",
|
| 50 |
-
help=(
|
| 51 |
-
"Disable deep detection (README/frontmatter fetch) when card metadata is missing."
|
| 52 |
-
),
|
| 53 |
-
)
|
| 54 |
-
return parser.parse_args()
|
| 55 |
-
|
| 56 |
-
|
| 57 |
def iso_to_datetime(value: str) -> datetime:
|
| 58 |
"""Parse ISO 8601 timestamps returned by the Hub to aware datetime in UTC."""
|
| 59 |
-
# Examples: "2024-09-01T12:34:56.789Z" or without microseconds
|
| 60 |
try:
|
| 61 |
dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")
|
| 62 |
except ValueError:
|
|
@@ -66,7 +23,6 @@ def iso_to_datetime(value: str) -> datetime:
|
|
| 66 |
|
| 67 |
def readme_frontmatter_duplicated_from(space_id: str) -> Optional[str]:
|
| 68 |
"""Fetch README raw and try to extract duplicated_from from YAML frontmatter."""
|
| 69 |
-
# Raw README for Spaces is accessible at /spaces/{id}/raw/README.md
|
| 70 |
url = f"https://huggingface.co/spaces/{space_id}/raw/README.md"
|
| 71 |
try:
|
| 72 |
resp = requests.get(url, timeout=10)
|
|
@@ -76,43 +32,31 @@ def readme_frontmatter_duplicated_from(space_id: str) -> Optional[str]:
|
|
| 76 |
except requests.RequestException:
|
| 77 |
return None
|
| 78 |
|
| 79 |
-
# Very light-weight frontmatter scan to find a line like: duplicated_from: owner/space
|
| 80 |
-
# Do not parse full YAML to avoid extra deps.
|
| 81 |
lines = text.splitlines()
|
| 82 |
in_frontmatter = False
|
| 83 |
for line in lines:
|
| 84 |
if line.strip() == "---":
|
| 85 |
in_frontmatter = not in_frontmatter
|
| 86 |
-
# Stop if we closed the frontmatter without finding the key.
|
| 87 |
if not in_frontmatter:
|
| 88 |
break
|
| 89 |
continue
|
| 90 |
if in_frontmatter and line.strip().startswith("duplicated_from:"):
|
| 91 |
-
# Extract value after colon, trim quotes/spaces
|
| 92 |
value = line.split(":", 1)[1].strip().strip("'\"")
|
| 93 |
return value or None
|
| 94 |
return None
|
| 95 |
|
| 96 |
|
| 97 |
def get_recent_spaces(api: HfApi, days: int) -> Iterable:
|
| 98 |
-
"""Yield Spaces created within the last `days` days, iterating newest first.
|
| 99 |
-
|
| 100 |
-
Tries to sort by creation date descending; falls back gracefully if not supported.
|
| 101 |
-
"""
|
| 102 |
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
| 103 |
-
|
| 104 |
-
# Primary attempt: request spaces sorted by creation date (newest first)
|
| 105 |
try:
|
| 106 |
spaces_iter = api.list_spaces(full=True, sort="created", direction=-1)
|
| 107 |
except TypeError:
|
| 108 |
-
# Fallback: no sort support in current huggingface_hub; get a reasonably large list
|
| 109 |
-
# Note: This may include items older than the cutoff; we'll filter below.
|
| 110 |
spaces_iter = api.list_spaces(full=True)
|
| 111 |
|
| 112 |
for space in spaces_iter:
|
| 113 |
created_at_raw = getattr(space, "created_at", None) or getattr(space, "createdAt", None)
|
| 114 |
if not created_at_raw:
|
| 115 |
-
# If missing, include conservatively
|
| 116 |
yield space
|
| 117 |
continue
|
| 118 |
created_at = (
|
|
@@ -121,23 +65,11 @@ def get_recent_spaces(api: HfApi, days: int) -> Iterable:
|
|
| 121 |
if created_at >= cutoff:
|
| 122 |
yield space
|
| 123 |
else:
|
| 124 |
-
#
|
| 125 |
-
# Only do that when we explicitly asked for sort="created"
|
| 126 |
-
if "spaces_iter" in locals():
|
| 127 |
-
try:
|
| 128 |
-
# If we reached here under the sorted branch, short-circuit
|
| 129 |
-
# by checking if the generator came from the sorted call
|
| 130 |
-
_ = api # keep linter calm
|
| 131 |
-
except Exception:
|
| 132 |
-
pass
|
| 133 |
-
# We can't be certain the iterator is sorted in fallback; just continue
|
| 134 |
-
# without breaking to avoid missing any items.
|
| 135 |
continue
|
| 136 |
|
| 137 |
|
| 138 |
-
def find_duplicated_spaces(
|
| 139 |
-
api: HfApi, source: str, days: int, deep_detection: bool
|
| 140 |
-
) -> List[str]:
|
| 141 |
"""Return list of Space IDs that were duplicated from `source` within `days`."""
|
| 142 |
source = source.strip().strip("/ ")
|
| 143 |
results: List[str] = []
|
|
@@ -146,7 +78,6 @@ def find_duplicated_spaces(
|
|
| 146 |
if not space_id:
|
| 147 |
continue
|
| 148 |
|
| 149 |
-
# Check card metadata first
|
| 150 |
card = getattr(space, "cardData", None) or getattr(space, "card_data", None)
|
| 151 |
duplicated_from_value: Optional[str] = None
|
| 152 |
if isinstance(card, dict):
|
|
@@ -155,7 +86,6 @@ def find_duplicated_spaces(
|
|
| 155 |
duplicated_from_value = card[key].strip().strip("/ ")
|
| 156 |
break
|
| 157 |
|
| 158 |
-
# Optional deep detection via README frontmatter
|
| 159 |
if not duplicated_from_value and deep_detection:
|
| 160 |
duplicated_from_value = readme_frontmatter_duplicated_from(space_id)
|
| 161 |
if duplicated_from_value:
|
|
@@ -167,30 +97,3 @@ def find_duplicated_spaces(
|
|
| 167 |
return results
|
| 168 |
|
| 169 |
|
| 170 |
-
def main() -> None:
|
| 171 |
-
args = parse_args()
|
| 172 |
-
api = HfApi(token=args.token)
|
| 173 |
-
|
| 174 |
-
duplicated = find_duplicated_spaces(
|
| 175 |
-
api=api,
|
| 176 |
-
source=args.source,
|
| 177 |
-
days=args.days,
|
| 178 |
-
deep_detection=not args.no_deep,
|
| 179 |
-
)
|
| 180 |
-
|
| 181 |
-
if duplicated:
|
| 182 |
-
print(
|
| 183 |
-
f"Found {len(duplicated)} Space(s) duplicated from {args.source} in the last {args.days} days:\n"
|
| 184 |
-
)
|
| 185 |
-
for sid in duplicated:
|
| 186 |
-
print(f"https://huggingface.co/spaces/{sid}")
|
| 187 |
-
else:
|
| 188 |
-
print(
|
| 189 |
-
f"No public Spaces duplicated from {args.source} in the last {args.days} days."
|
| 190 |
-
)
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
if __name__ == "__main__":
|
| 194 |
-
main()
|
| 195 |
-
|
| 196 |
-
|
|
|
|
| 1 |
+
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
"""
|
| 4 |
+
Core logic to find Spaces duplicated from a given source within a time window.
|
| 5 |
+
Comments are in English (per user preference for code comments).
|
| 6 |
"""
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from datetime import datetime, timedelta, timezone
|
| 9 |
from typing import Iterable, List, Optional
|
| 10 |
|
|
|
|
| 12 |
from huggingface_hub import HfApi
|
| 13 |
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def iso_to_datetime(value: str) -> datetime:
|
| 16 |
"""Parse ISO 8601 timestamps returned by the Hub to aware datetime in UTC."""
|
|
|
|
| 17 |
try:
|
| 18 |
dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")
|
| 19 |
except ValueError:
|
|
|
|
| 23 |
|
| 24 |
def readme_frontmatter_duplicated_from(space_id: str) -> Optional[str]:
|
| 25 |
"""Fetch README raw and try to extract duplicated_from from YAML frontmatter."""
|
|
|
|
| 26 |
url = f"https://huggingface.co/spaces/{space_id}/raw/README.md"
|
| 27 |
try:
|
| 28 |
resp = requests.get(url, timeout=10)
|
|
|
|
| 32 |
except requests.RequestException:
|
| 33 |
return None
|
| 34 |
|
|
|
|
|
|
|
| 35 |
lines = text.splitlines()
|
| 36 |
in_frontmatter = False
|
| 37 |
for line in lines:
|
| 38 |
if line.strip() == "---":
|
| 39 |
in_frontmatter = not in_frontmatter
|
|
|
|
| 40 |
if not in_frontmatter:
|
| 41 |
break
|
| 42 |
continue
|
| 43 |
if in_frontmatter and line.strip().startswith("duplicated_from:"):
|
|
|
|
| 44 |
value = line.split(":", 1)[1].strip().strip("'\"")
|
| 45 |
return value or None
|
| 46 |
return None
|
| 47 |
|
| 48 |
|
| 49 |
def get_recent_spaces(api: HfApi, days: int) -> Iterable:
|
| 50 |
+
"""Yield Spaces created within the last `days` days, iterating newest first if possible."""
|
|
|
|
|
|
|
|
|
|
| 51 |
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
|
|
|
|
|
|
| 52 |
try:
|
| 53 |
spaces_iter = api.list_spaces(full=True, sort="created", direction=-1)
|
| 54 |
except TypeError:
|
|
|
|
|
|
|
| 55 |
spaces_iter = api.list_spaces(full=True)
|
| 56 |
|
| 57 |
for space in spaces_iter:
|
| 58 |
created_at_raw = getattr(space, "created_at", None) or getattr(space, "createdAt", None)
|
| 59 |
if not created_at_raw:
|
|
|
|
| 60 |
yield space
|
| 61 |
continue
|
| 62 |
created_at = (
|
|
|
|
| 65 |
if created_at >= cutoff:
|
| 66 |
yield space
|
| 67 |
else:
|
| 68 |
+
# We cannot guarantee sort order when falling back; continue to be safe.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
continue
|
| 70 |
|
| 71 |
|
| 72 |
+
def find_duplicated_spaces(api: HfApi, source: str, days: int, deep_detection: bool) -> List[str]:
|
|
|
|
|
|
|
| 73 |
"""Return list of Space IDs that were duplicated from `source` within `days`."""
|
| 74 |
source = source.strip().strip("/ ")
|
| 75 |
results: List[str] = []
|
|
|
|
| 78 |
if not space_id:
|
| 79 |
continue
|
| 80 |
|
|
|
|
| 81 |
card = getattr(space, "cardData", None) or getattr(space, "card_data", None)
|
| 82 |
duplicated_from_value: Optional[str] = None
|
| 83 |
if isinstance(card, dict):
|
|
|
|
| 86 |
duplicated_from_value = card[key].strip().strip("/ ")
|
| 87 |
break
|
| 88 |
|
|
|
|
| 89 |
if not duplicated_from_value and deep_detection:
|
| 90 |
duplicated_from_value = readme_frontmatter_duplicated_from(space_id)
|
| 91 |
if duplicated_from_value:
|
|
|
|
| 97 |
return results
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tools/duplicated-spaces/pyproject.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[tool.poetry]
|
| 2 |
+
name = "duplicated-spaces"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Find recent Hugging Face Spaces duplicated from a given Space"
|
| 5 |
+
authors = ["thibaud frere <>"]
|
| 6 |
+
readme = "README.md"
|
| 7 |
+
packages = [{ include = "duplicated_spaces" }]
|
| 8 |
+
|
| 9 |
+
[tool.poetry.dependencies]
|
| 10 |
+
python = ">=3.9,<4.0"
|
| 11 |
+
huggingface_hub = "^0.24.0"
|
| 12 |
+
requests = "^2.31.0"
|
| 13 |
+
|
| 14 |
+
[tool.poetry.group.dev.dependencies]
|
| 15 |
+
|
| 16 |
+
[tool.poetry.scripts]
|
| 17 |
+
find-duplicated-spaces = "duplicated_spaces.cli:main"
|
| 18 |
+
|
| 19 |
+
[build-system]
|
| 20 |
+
requires = ["poetry-core>=1.7.0"]
|
| 21 |
+
build-backend = "poetry.core.masonry.api"
|
| 22 |
+
|
| 23 |
+
|