Spaces:
Running
Running
thibaud frere
commited on
Commit
·
c59bbe1
1
Parent(s):
b8e1b6c
update
Browse files- .gitattributes +2 -0
- app/public/data/against_baselines.csv +1 -0
- app/public/data/finevision.csv +1 -0
- app/src/components/HtmlEmbed.astro +4 -4
- app/src/components/ThemeToggle.astro +16 -6
- app/src/content/assets/data/against_baselines.csv +3 -0
- app/src/content/assets/data/finevision.csv +3 -0
- app/src/content/assets/icones/moon.svg +0 -4
- app/src/content/assets/icones/sun.svg +0 -12
- app/src/content/chapters/available-blocks.mdx +10 -0
- app/src/content/chapters/writing-your-content.mdx +20 -7
- app/src/content/embeds/d3-bar.html +29 -8
- app/src/content/embeds/d3-line-old.html +0 -0
- app/src/content/embeds/d3-line.html +142 -170
- app/src/content/embeds/d3-pie.html +227 -0
- app/src/content/embeds/palettes.html +7 -7
- app/src/styles/global.css +2 -2
.gitattributes
CHANGED
@@ -1,3 +1,5 @@
|
|
1 |
*.png filter=lfs diff=lfs merge=lfs -text
|
2 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
3 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
1 |
*.png filter=lfs diff=lfs merge=lfs -text
|
2 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
3 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
4 |
+
*.csv filter=lfs diff=lfs merge=lfs -text
|
5 |
+
*.json filter=lfs diff=lfs merge=lfs -text
|
app/public/data/against_baselines.csv
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
../../src/content/assets/data/against_baselines.csv
|
app/public/data/finevision.csv
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
../../src/content/assets/data/finevision.csv
|
app/src/components/HtmlEmbed.astro
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
---
|
2 |
-
interface Props { src: string; title?: string; desc?: string; frameless?: boolean }
|
3 |
-
const { src, title, desc, frameless = false } = Astro.props as Props;
|
4 |
|
5 |
// Load all .html embeds under src/content/embeds/** as strings (dev & build)
|
6 |
const embeds = (import.meta as any).glob('../content/embeds/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
|
@@ -21,11 +21,11 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
|
21 |
---
|
22 |
{ html ? (
|
23 |
<figure class="html-embed">
|
24 |
-
{title && <figcaption class="html-embed__title">{title}</figcaption>}
|
25 |
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
|
26 |
<div id={mountId} set:html={html} />
|
27 |
</div>
|
28 |
-
{desc && <figcaption class="html-embed__desc" set:html={desc}></figcaption>}
|
29 |
</figure>
|
30 |
) : (
|
31 |
<div><!-- Fragment not found: {src} --></div>
|
|
|
1 |
---
|
2 |
+
interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right' }
|
3 |
+
const { src, title, desc, frameless = false, align = 'left' } = Astro.props as Props;
|
4 |
|
5 |
// Load all .html embeds under src/content/embeds/** as strings (dev & build)
|
6 |
const embeds = (import.meta as any).glob('../content/embeds/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
|
|
|
21 |
---
|
22 |
{ html ? (
|
23 |
<figure class="html-embed">
|
24 |
+
{title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
|
25 |
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
|
26 |
<div id={mountId} set:html={html} />
|
27 |
</div>
|
28 |
+
{desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
|
29 |
</figure>
|
30 |
) : (
|
31 |
<div><!-- Fragment not found: {src} --></div>
|
app/src/components/ThemeToggle.astro
CHANGED
@@ -1,10 +1,20 @@
|
|
1 |
-
---
|
2 |
-
import sunIconUrl from "../content/assets/icones/sun.svg?url";
|
3 |
-
import moonIconUrl from "../content/assets/icones/moon.svg?url";
|
4 |
-
---
|
5 |
<button id="theme-toggle" aria-label="Toggle color theme">
|
6 |
-
<
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
<script>
|
9 |
const btn = document.getElementById('theme-toggle');
|
10 |
const media = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
|
|
|
|
|
|
|
1 |
<button id="theme-toggle" aria-label="Toggle color theme">
|
2 |
+
<svg class="icon light" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">
|
3 |
+
<circle cx="12" cy="12" r="5"/>
|
4 |
+
<g stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
5 |
+
<line x1="12" y1="1" x2="12" y2="4"/>
|
6 |
+
<line x1="12" y1="20" x2="12" y2="23"/>
|
7 |
+
<line x1="1" y1="12" x2="4" y2="12"/>
|
8 |
+
<line x1="20" y1="12" x2="23" y2="12"/>
|
9 |
+
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/>
|
10 |
+
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/>
|
11 |
+
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/>
|
12 |
+
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
|
13 |
+
</g>
|
14 |
+
</svg>
|
15 |
+
<svg class="icon dark" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="currentColor">
|
16 |
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
17 |
+
</svg>
|
18 |
<script>
|
19 |
const btn = document.getElementById('theme-toggle');
|
20 |
const media = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
|
app/src/content/assets/data/against_baselines.csv
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:d5db6c112739ccf6b5f98dfdd480f9748c5b78c66452b02e0168eeed6acea875
|
3 |
+
size 50100
|
app/src/content/assets/data/finevision.csv
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:d28bd13dc3a9ff100c82e8c9dc59270563b865383d09cf28c5aba5812bfa75ee
|
3 |
+
size 10913
|
app/src/content/assets/icones/moon.svg
DELETED
app/src/content/assets/icones/sun.svg
DELETED
app/src/content/chapters/available-blocks.mdx
CHANGED
@@ -326,6 +326,7 @@ Props (optional)
|
|
326 |
- `title`: short title displayed above the card.
|
327 |
- `desc`: short description displayed below the card. Supports inline HTML (e.g., links).
|
328 |
- `frameless`: removes the card background and border for seamless embeds.
|
|
|
329 |
|
330 |
<HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
|
331 |
---
|
@@ -335,6 +336,15 @@ title="Memory usage with recomputation"
|
|
335 |
desc={`Memory usage with recomputation — <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">from the ultrascale playbook</a>`}
|
336 |
/>
|
337 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
<HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
|
339 |
|
340 |
<small className="muted">Example</small>
|
|
|
326 |
- `title`: short title displayed above the card.
|
327 |
- `desc`: short description displayed below the card. Supports inline HTML (e.g., links).
|
328 |
- `frameless`: removes the card background and border for seamless embeds.
|
329 |
+
- `align`: aligns the title/description text. One of `left` (default), `center`, `right`.
|
330 |
|
331 |
<HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
|
332 |
---
|
|
|
336 |
desc={`Memory usage with recomputation — <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">from the ultrascale playbook</a>`}
|
337 |
/>
|
338 |
---
|
339 |
+
<FullWidth>
|
340 |
+
<HtmlEmbed
|
341 |
+
src="d3-pie.html"
|
342 |
+
title="Category distribution (4 metrics)"
|
343 |
+
desc="Pie charts by category from finevision.csv"
|
344 |
+
align="center"
|
345 |
+
/>
|
346 |
+
</FullWidth>
|
347 |
+
---
|
348 |
<HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
|
349 |
|
350 |
<small className="muted">Example</small>
|
app/src/content/chapters/writing-your-content.mdx
CHANGED
@@ -11,12 +11,14 @@ import audioDemo from '../assets/audio/audio-example.wav';
|
|
11 |
|
12 |
### Introduction
|
13 |
|
14 |
-
Your article lives in
|
15 |
|
16 |
-
- `app/src/content
|
17 |
-
- `
|
|
|
|
|
18 |
|
19 |
-
The `article.mdx` file is the main
|
20 |
|
21 |
<small className="muted">Example</small>
|
22 |
```mdx
|
@@ -79,7 +81,7 @@ Below is an image imported via Astro and optimized at build time:
|
|
79 |
**If** your article becomes **too long** for one file, you can **organize** it into **separate chapters**.
|
80 |
|
81 |
Simply **create a new file** in the `app/src/content/chapters` **directory**.
|
82 |
-
Then, **include** your new chapter in the main article.
|
83 |
|
84 |
|
85 |
<small className="muted">Example</small>
|
@@ -88,8 +90,7 @@ import MyChapter from './chapters/my-chapter.mdx';
|
|
88 |
|
89 |
<MyChapter />
|
90 |
```
|
91 |
-
|
92 |
-
You can see an example of this in the <a target="_blank" href="https://huggingface.co/spaces/tfrere/research-article-template/blob/main/app/src/content/chapters/best-pratices.mdx">app/src/content/chapters/best-pratices.mdx</a> file.
|
93 |
|
94 |
### Table of contents
|
95 |
|
@@ -122,6 +123,18 @@ Here is a suggestion of **color palettes** for your **data visualizations** that
|
|
122 |
<HtmlEmbed frameless src="palettes.html" />
|
123 |
|
124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
### Placement
|
126 |
|
127 |
Use these helpers when you need to step outside the main content flow: **Sidenotes** for contextual side notes, **Wide** to extend beyond the main column, and **Full-width** for full-width, immersive sections.
|
|
|
11 |
|
12 |
### Introduction
|
13 |
|
14 |
+
Your article lives in one place
|
15 |
|
16 |
+
Everything is self-contained under `app/src/content/`:
|
17 |
+
- MDX: `article.mdx` and [optional chapters](#chapters) in `chapters/`
|
18 |
+
- Assets: `assets/` (images, audio; tracked via Git LFS)
|
19 |
+
- Embeds: `embed/` (HTMLEmbed for Plotly/D3, etc.) — see [HtmlEmbed](#htmlembed)
|
20 |
|
21 |
+
The `article.mdx` file is the main entry point of your article.
|
22 |
|
23 |
<small className="muted">Example</small>
|
24 |
```mdx
|
|
|
81 |
**If** your article becomes **too long** for one file, you can **organize** it into **separate chapters**.
|
82 |
|
83 |
Simply **create a new file** in the `app/src/content/chapters` **directory**.
|
84 |
+
Then, **include** your new chapter in the main `article.mdx` like below.
|
85 |
|
86 |
|
87 |
<small className="muted">Example</small>
|
|
|
90 |
|
91 |
<MyChapter />
|
92 |
```
|
93 |
+
<small className="muted">You can see a living example here <a target="_blank" href="https://huggingface.co/spaces/tfrere/research-article-template/blob/main/app/src/content/chapters/best-pratices.mdx">app/src/content/chapters/best-pratices.mdx</a>.</small>
|
|
|
94 |
|
95 |
### Table of contents
|
96 |
|
|
|
123 |
<HtmlEmbed frameless src="palettes.html" />
|
124 |
|
125 |
|
126 |
+
### Embeds
|
127 |
+
|
128 |
+
Use HTML fragments to embed interactive charts and widgets. Place your fragments under `app/src/content/embeds/` and reference them via the `HtmlEmbed` component.
|
129 |
+
|
130 |
+
<small className="muted">Example</small>
|
131 |
+
```mdx
|
132 |
+
import HtmlEmbed from '../components/HtmlEmbed.astro'
|
133 |
+
|
134 |
+
<HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
|
135 |
+
```
|
136 |
+
|
137 |
+
|
138 |
### Placement
|
139 |
|
140 |
Use these helpers when you need to step outside the main content flow: **Sidenotes** for contextual side notes, **Wide** to extend beyond the main column, and **Full-width** for full-width, immersive sections.
|
app/src/content/embeds/d3-bar.html
CHANGED
@@ -148,7 +148,11 @@
|
|
148 |
// Stack values
|
149 |
const stacked = seqLabels.map((label, i) => {
|
150 |
let acc = 0; const items = [];
|
151 |
-
series.forEach((s) => {
|
|
|
|
|
|
|
|
|
152 |
return { label, items };
|
153 |
});
|
154 |
|
@@ -160,9 +164,27 @@
|
|
160 |
groupsEnter.merge(groups).attr('transform', (d)=>`translate(${x0(d.label)},0)`);
|
161 |
groups.exit().remove();
|
162 |
|
163 |
-
|
164 |
-
|
165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
.attr('fill', (d)=>d.color)
|
167 |
.on('mouseenter', function(ev, d){
|
168 |
d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
|
@@ -173,12 +195,11 @@
|
|
173 |
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)`;
|
174 |
})
|
175 |
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('stroke','none'); })
|
176 |
-
.merge(
|
177 |
.transition().duration(200)
|
178 |
-
.attr('
|
179 |
-
.attr('y', (d)=>y(d.y1)).attr('height', (d)=>Math.max(0.5, y(d.y0) - y(d.y1)))
|
180 |
.attr('fill', (d)=>d.color);
|
181 |
-
|
182 |
}
|
183 |
|
184 |
function update(){ drawBars(); }
|
|
|
148 |
// Stack values
|
149 |
const stacked = seqLabels.map((label, i) => {
|
150 |
let acc = 0; const items = [];
|
151 |
+
series.forEach((s, idx) => {
|
152 |
+
const y0 = acc; const y1 = acc + s.values[i];
|
153 |
+
items.push({ key: s.key, color: s.color, i, y0, y1, xLabel: label, value: s.values[i], isBottom: idx === 0, isTop: idx === series.length - 1 });
|
154 |
+
acc = y1;
|
155 |
+
});
|
156 |
return { label, items };
|
157 |
});
|
158 |
|
|
|
164 |
groupsEnter.merge(groups).attr('transform', (d)=>`translate(${x0(d.label)},0)`);
|
165 |
groups.exit().remove();
|
166 |
|
167 |
+
// Helper to draw per-corner rounded rectangle path
|
168 |
+
const rCorner = 4;
|
169 |
+
const roundedPath = (x, yTop, w, h, isTop, isBottom) => {
|
170 |
+
const r = Math.min(rCorner, Math.max(0, Math.min(w, h) / 2));
|
171 |
+
const rTL = isTop ? r : 0, rTR = isTop ? r : 0, rBR = isBottom ? r : 0, rBL = isBottom ? r : 0;
|
172 |
+
const x0 = x, y0 = yTop, x1 = x + w, y1 = yTop + h;
|
173 |
+
return `M${x0 + rTL},${y0}`
|
174 |
+
+ `H${x1 - rTR}`
|
175 |
+
+ (rTR ? `Q${x1},${y0} ${x1},${y0 + rTR}` : `V${y0}`)
|
176 |
+
+ `V${y1 - rBR}`
|
177 |
+
+ (rBR ? `Q${x1},${y1} ${x1 - rBR},${y1}` : `H${x1}`)
|
178 |
+
+ `H${x0 + rBL}`
|
179 |
+
+ (rBL ? `Q${x0},${y1} ${x0},${y1 - rBL}` : `V${y1}`)
|
180 |
+
+ `V${y0 + rTL}`
|
181 |
+
+ (rTL ? `Q${x0},${y0} ${x0 + rTL},${y0}` : `H${x0}`)
|
182 |
+
+ 'Z';
|
183 |
+
};
|
184 |
+
|
185 |
+
const bars = groupsEnter.merge(groups).selectAll('path.bar').data(d=>d.items, d=>d.key);
|
186 |
+
bars.enter().append('path').attr('class','bar')
|
187 |
+
.attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
|
188 |
.attr('fill', (d)=>d.color)
|
189 |
.on('mouseenter', function(ev, d){
|
190 |
d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
|
|
|
195 |
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)`;
|
196 |
})
|
197 |
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('stroke','none'); })
|
198 |
+
.merge(bars)
|
199 |
.transition().duration(200)
|
200 |
+
.attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
|
|
|
201 |
.attr('fill', (d)=>d.color);
|
202 |
+
bars.exit().remove();
|
203 |
}
|
204 |
|
205 |
function update(){ drawBars(); }
|
app/src/content/embeds/d3-line-old.html
ADDED
File without changes
|
app/src/content/embeds/d3-line.html
CHANGED
@@ -97,12 +97,19 @@
|
|
97 |
container.dataset.mounted = 'true';
|
98 |
}
|
99 |
|
100 |
-
//
|
101 |
-
const
|
102 |
-
|
103 |
-
|
104 |
-
|
|
|
105 |
];
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
|
107 |
// Controls UI
|
108 |
const controls = document.createElement('div');
|
@@ -111,38 +118,32 @@
|
|
111 |
marginTop: '12px',
|
112 |
display: 'flex',
|
113 |
gap: '16px',
|
114 |
-
alignItems: 'center'
|
|
|
|
|
115 |
});
|
116 |
|
117 |
-
const
|
118 |
-
Object.assign(
|
119 |
-
fontSize: '12px', color: '
|
120 |
-
});
|
121 |
-
labelDs.textContent = 'Dataset';
|
122 |
-
const selectDs = document.createElement('select');
|
123 |
-
Object.assign(selectDs.style, { fontSize: '12px' });
|
124 |
-
datasets.forEach((d, i) => {
|
125 |
-
const o = document.createElement('option');
|
126 |
-
o.value = String(i);
|
127 |
-
o.textContent = d.name;
|
128 |
-
selectDs.appendChild(o);
|
129 |
});
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
});
|
136 |
-
|
137 |
-
const slider = document.createElement('input');
|
138 |
-
slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.01'; slider.value = '0.70';
|
139 |
-
Object.assign(slider.style, { flex: '1' });
|
140 |
-
const alphaVal = document.createElement('span'); alphaVal.className = 'alpha-value'; alphaVal.textContent = slider.value;
|
141 |
-
labelAlpha.appendChild(slider);
|
142 |
-
labelAlpha.appendChild(alphaVal);
|
143 |
-
|
144 |
-
controls.appendChild(labelDs);
|
145 |
-
controls.appendChild(labelAlpha);
|
146 |
|
147 |
// Create SVG
|
148 |
const svg = d3.select(container).append('svg')
|
@@ -179,34 +180,15 @@
|
|
179 |
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
180 |
}
|
181 |
|
182 |
-
// Colors
|
183 |
-
const
|
184 |
-
const
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
const
|
191 |
-
const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
|
192 |
-
const logistic = (x, { ymin, ymax, k, x0 }) => ymin + (ymax - ymin) / (1 + Math.exp(-k * (x - x0)));
|
193 |
-
const blend = (l, e, a) => (1 - a) * l + a * e;
|
194 |
-
|
195 |
-
let datasetIndex = 0;
|
196 |
-
let alpha = parseFloat(slider.value) || 0.7;
|
197 |
-
|
198 |
-
let yBase = [];
|
199 |
-
let yAug = [];
|
200 |
-
let yImp = [];
|
201 |
-
let yTgt = [];
|
202 |
-
|
203 |
-
function computeCurves() {
|
204 |
-
const d = datasets[datasetIndex];
|
205 |
-
yBase = xs.map((x) => logistic(x, d.base));
|
206 |
-
yAug = xs.map((x) => logistic(x, d.aug));
|
207 |
-
yTgt = xs.map(() => d.target);
|
208 |
-
yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
|
209 |
-
}
|
210 |
|
211 |
// Scales and layout
|
212 |
let width = 800, height = 360;
|
@@ -214,21 +196,14 @@
|
|
214 |
let xScale = d3.scaleLinear();
|
215 |
let yScale = d3.scaleLinear();
|
216 |
|
217 |
-
//
|
218 |
const lineGen = d3.line()
|
219 |
-
.curve(d3.curveCatmullRom.alpha(0.
|
220 |
-
.x((d
|
221 |
-
.y((d) => yScale(d));
|
222 |
-
|
223 |
-
const pathBase = gLines.append('path').attr('fill', 'none').attr('stroke', colorBase).attr('stroke-width', 2);
|
224 |
-
const pathImp = gLines.append('path').attr('class', 'improved').attr('fill', 'none').style('stroke', 'var(--primary-color)').attr('stroke-width', 2);
|
225 |
-
const pathTgt = gLines.append('path').attr('fill', 'none').attr('stroke', colorTarget).attr('stroke-width', 2).attr('stroke-dasharray', '6,6');
|
226 |
|
227 |
// Hover elements
|
228 |
const hoverLine = gHover.append('line').attr('stroke-width', 1);
|
229 |
-
const hoverDotB = gHover.append('circle').attr('r', 3.5).attr('fill', colorBase).attr('stroke', '#fff').attr('stroke-width', 1);
|
230 |
-
const hoverDotI = gHover.append('circle').attr('class', 'improved').attr('r', 3.5).style('fill', 'var(--primary-color)').attr('stroke', '#fff').attr('stroke-width', 1);
|
231 |
-
const hoverDotT = gHover.append('circle').attr('r', 3.5).attr('fill', colorTarget).attr('stroke', '#fff').attr('stroke-width', 1);
|
232 |
|
233 |
const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
|
234 |
|
@@ -246,8 +221,8 @@
|
|
246 |
const innerHeight = height - margin.top - margin.bottom;
|
247 |
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
248 |
|
249 |
-
xScale.
|
250 |
-
yScale.
|
251 |
|
252 |
// Grid (horizontal)
|
253 |
gGrid.selectAll('*').remove();
|
@@ -289,19 +264,19 @@
|
|
289 |
.attr('text-anchor', 'middle')
|
290 |
.style('font-size', '12px')
|
291 |
.style('fill', tickColor)
|
292 |
-
.text('
|
293 |
gAxes.append('text')
|
294 |
.attr('class', 'axis-label axis-label--y')
|
295 |
.attr('text-anchor', 'middle')
|
296 |
.attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`)
|
297 |
.style('font-size', '12px')
|
298 |
.style('fill', tickColor)
|
299 |
-
.text('
|
300 |
|
301 |
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
302 |
hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
|
303 |
|
304 |
-
// Legend
|
305 |
const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
|
306 |
const legendHeight = 64;
|
307 |
gLegend
|
@@ -319,105 +294,102 @@
|
|
319 |
lineHeight: '1.35',
|
320 |
color: 'var(--text-color)'
|
321 |
});
|
322 |
-
legendRoot.html(`
|
323 |
-
<div style="display:flex;flex-direction:column;gap:6px;">
|
324 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
325 |
-
<span style="width:18px;height:3px;background:${colorBase};border-radius:2px;display:inline-block"></span>
|
326 |
-
<span>Baseline</span>
|
327 |
-
</div>
|
328 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
329 |
-
<span style="width:18px;height:3px;background:${colorImproved};border-radius:2px;display:inline-block"></span>
|
330 |
-
<span>Improved</span>
|
331 |
-
</div>
|
332 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
333 |
-
<span style="width:18px;height:0;border-top:2px dashed ${colorTarget};display:inline-block"></span>
|
334 |
-
<span>Target</span>
|
335 |
-
</div>
|
336 |
-
</div>
|
337 |
-
`);
|
338 |
-
}
|
339 |
-
|
340 |
-
function updatePaths() {
|
341 |
-
pathBase.transition().duration(200).attr('d', lineGen(yBase));
|
342 |
-
pathImp.transition().duration(200).attr('d', lineGen(yImp));
|
343 |
-
pathTgt.transition().duration(200).attr('d', lineGen(yTgt));
|
344 |
-
}
|
345 |
-
|
346 |
-
function updateAlpha(a) {
|
347 |
-
alpha = a;
|
348 |
-
alphaVal.textContent = a.toFixed(2);
|
349 |
-
yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
|
350 |
-
pathImp.transition().duration(80).attr('d', lineGen(yImp));
|
351 |
-
}
|
352 |
|
353 |
-
|
354 |
-
computeCurves();
|
355 |
-
updatePaths();
|
356 |
}
|
357 |
|
358 |
-
|
359 |
-
|
360 |
-
const
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
const
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
}
|
389 |
|
390 |
-
|
391 |
-
|
392 |
-
//
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
});
|
408 |
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
421 |
};
|
422 |
|
423 |
if (document.readyState === 'loading') {
|
|
|
97 |
container.dataset.mounted = 'true';
|
98 |
}
|
99 |
|
100 |
+
// CSV: prefer public path, fallback to relative
|
101 |
+
const CSV_PATHS = [
|
102 |
+
'/data/against_baselines.csv',
|
103 |
+
'./assets/data/against_baselines.csv',
|
104 |
+
'../assets/data/against_baselines.csv',
|
105 |
+
'../../assets/data/against_baselines.csv'
|
106 |
];
|
107 |
+
const fetchFirstAvailable = async (paths) => {
|
108 |
+
for (const p of paths) {
|
109 |
+
try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(e) {}
|
110 |
+
}
|
111 |
+
throw new Error('CSV not found: against_baselines.csv');
|
112 |
+
};
|
113 |
|
114 |
// Controls UI
|
115 |
const controls = document.createElement('div');
|
|
|
118 |
marginTop: '12px',
|
119 |
display: 'flex',
|
120 |
gap: '16px',
|
121 |
+
alignItems: 'center',
|
122 |
+
justifyContent: 'flex-start',
|
123 |
+
width: '100%'
|
124 |
});
|
125 |
|
126 |
+
const labelMetric = document.createElement('label');
|
127 |
+
Object.assign(labelMetric.style, {
|
128 |
+
fontSize: '12px', color: 'var(--muted-color)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
});
|
130 |
+
labelMetric.textContent = 'Metric';
|
131 |
+
const selectMetric = document.createElement('select');
|
132 |
+
Object.assign(selectMetric.style, { fontSize: '12px' });
|
133 |
+
labelMetric.appendChild(selectMetric);
|
134 |
+
controls.appendChild(labelMetric);
|
135 |
+
|
136 |
+
// Inline legend on the right of the select
|
137 |
+
const legendInline = document.createElement('div');
|
138 |
+
legendInline.className = 'controls__legend';
|
139 |
+
Object.assign(legendInline.style, {
|
140 |
+
display: 'flex',
|
141 |
+
gap: '8px',
|
142 |
+
alignItems: 'center',
|
143 |
+
flexWrap: 'nowrap',
|
144 |
+
fontSize: '11px'
|
145 |
});
|
146 |
+
controls.appendChild(legendInline);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
|
148 |
// Create SVG
|
149 |
const svg = d3.select(container).append('svg')
|
|
|
180 |
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
181 |
}
|
182 |
|
183 |
+
// Colors per run
|
184 |
+
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
185 |
+
const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
|
186 |
+
|
187 |
+
// State and data
|
188 |
+
let metricList = [];
|
189 |
+
let runList = [];
|
190 |
+
let runOrder = [];
|
191 |
+
const dataByMetric = new Map(); // metric => { run => [{step,value}] }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
|
193 |
// Scales and layout
|
194 |
let width = 800, height = 360;
|
|
|
196 |
let xScale = d3.scaleLinear();
|
197 |
let yScale = d3.scaleLinear();
|
198 |
|
199 |
+
// Line generator
|
200 |
const lineGen = d3.line()
|
201 |
+
.curve(d3.curveCatmullRom.alpha(0.05))
|
202 |
+
.x((d) => xScale(d.step))
|
203 |
+
.y((d) => yScale(d.value));
|
|
|
|
|
|
|
|
|
204 |
|
205 |
// Hover elements
|
206 |
const hoverLine = gHover.append('line').attr('stroke-width', 1);
|
|
|
|
|
|
|
207 |
|
208 |
const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
|
209 |
|
|
|
221 |
const innerHeight = height - margin.top - margin.bottom;
|
222 |
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
223 |
|
224 |
+
xScale.range([0, innerWidth]);
|
225 |
+
yScale.range([innerHeight, 0]);
|
226 |
|
227 |
// Grid (horizontal)
|
228 |
gGrid.selectAll('*').remove();
|
|
|
264 |
.attr('text-anchor', 'middle')
|
265 |
.style('font-size', '12px')
|
266 |
.style('fill', tickColor)
|
267 |
+
.text('Step');
|
268 |
gAxes.append('text')
|
269 |
.attr('class', 'axis-label axis-label--y')
|
270 |
.attr('text-anchor', 'middle')
|
271 |
.attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`)
|
272 |
.style('font-size', '12px')
|
273 |
.style('fill', tickColor)
|
274 |
+
.text('Value');
|
275 |
|
276 |
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
277 |
hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
|
278 |
|
279 |
+
// Legend placeholder; actual content set in renderMetric
|
280 |
const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
|
281 |
const legendHeight = 64;
|
282 |
gLegend
|
|
|
294 |
lineHeight: '1.35',
|
295 |
color: 'var(--text-color)'
|
296 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
297 |
|
298 |
+
return { innerWidth, innerHeight };
|
|
|
|
|
299 |
}
|
300 |
|
301 |
+
function renderMetric(metricKey){
|
302 |
+
const map = dataByMetric.get(metricKey) || {};
|
303 |
+
const runs = runOrder;
|
304 |
+
// Domain
|
305 |
+
let minStep = Infinity, maxStep = -Infinity, maxVal = 0, minVal = Infinity;
|
306 |
+
runs.forEach(r => {
|
307 |
+
const arr = map[r] || [];
|
308 |
+
arr.forEach(pt => { minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); maxVal = Math.max(maxVal, pt.value); minVal = Math.min(minVal, pt.value); });
|
309 |
+
});
|
310 |
+
if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
|
311 |
+
xScale.domain([minStep, maxStep]);
|
312 |
+
const isRank = /rank/i.test(metricKey);
|
313 |
+
yScale.domain(isRank ? [Math.max(maxVal, 1), Math.min(minVal, 0)] : [0, Math.max(1, maxVal)]).nice();
|
314 |
+
|
315 |
+
const { innerWidth, innerHeight } = updateScales();
|
316 |
+
|
317 |
+
// Bind lines
|
318 |
+
const series = runs.map((r, i) => ({ run: r, color: pool[i % pool.length], values: (map[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
|
319 |
+
const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
|
320 |
+
paths.enter().append('path').attr('class','run-line').attr('fill','none').attr('stroke-width',2)
|
321 |
+
.attr('stroke', d=>d.color).attr('opacity',0.9)
|
322 |
+
.attr('d', d=>lineGen(d.values))
|
323 |
+
.merge(paths)
|
324 |
+
.transition().duration(200)
|
325 |
+
.attr('stroke', d=>d.color)
|
326 |
+
.attr('d', d=>lineGen(d.values));
|
327 |
+
paths.exit().remove();
|
328 |
+
|
329 |
+
// Inline legend content (row, right side) compact
|
330 |
+
legendInline.innerHTML = series.map(s => `<span style="display:inline-flex;align-items:center;gap:6px;white-space:nowrap;"><span style="width:18px;height:10px;background:${s.color};border-radius:3px;display:inline-block"></span><span>${s.run}</span></span>`).join('');
|
331 |
+
|
332 |
+
// Hover
|
333 |
+
const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step)));
|
334 |
+
const steps = Array.from(stepSet).sort((a,b)=>a-b);
|
335 |
+
function onMove(event){
|
336 |
+
const [mx, my] = d3.pointer(event, overlay.node());
|
337 |
+
const sx = Math.max(steps[0], Math.min(steps[steps.length-1], Math.round(xScale.invert(mx)/1)*1));
|
338 |
+
const nearest = steps.reduce((best, s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]);
|
339 |
+
const xpx = xScale(nearest);
|
340 |
+
hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null).attr('stroke', 'rgba(0,0,0,0.25)');
|
341 |
+
// Tooltip content
|
342 |
+
let html = `<div><strong>${metricKey}</strong></div><div><strong>step</strong> ${nearest}</div>`;
|
343 |
+
series.forEach(s=>{
|
344 |
+
const m = new Map(s.values.map(v=>[v.step, v.value]));
|
345 |
+
const val = m.has(nearest) ? m.get(nearest) : null;
|
346 |
+
if (val != null) html += `<div><span style="display:inline-block;width:10px;height:10px;background:${s.color};border-radius:50%;margin-right:6px;"></span><strong>${s.run}</strong> ${(+val).toFixed(4)}</div>`;
|
347 |
+
});
|
348 |
+
tipInner.innerHTML = html;
|
349 |
+
const offsetX = 12, offsetY = 12;
|
350 |
+
tip.style.opacity = '1'; tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
|
351 |
+
}
|
352 |
+
function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
|
353 |
+
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
354 |
}
|
355 |
|
356 |
+
// (old hover removed; hover is attached in renderMetric)
|
357 |
+
|
358 |
+
// Load CSV and wire controls
|
359 |
+
(async () => {
|
360 |
+
try {
|
361 |
+
const text = await fetchFirstAvailable(CSV_PATHS);
|
362 |
+
const rows = d3.csvParse(text, d => ({ run: (d.run||'').trim(), step: +d.step, metric: (d.metric||'').trim(), value: +d.value }));
|
363 |
+
metricList = Array.from(new Set(rows.map(r=>r.metric))).sort();
|
364 |
+
runList = Array.from(new Set(rows.map(r=>r.run))).sort();
|
365 |
+
runOrder = ['FineVision', ...runList.filter(r=>r!=='FineVision')];
|
366 |
+
// Build dataByMetric
|
367 |
+
metricList.forEach(m => {
|
368 |
+
const map = {};
|
369 |
+
runList.forEach(r => { map[r] = []; });
|
370 |
+
rows.filter(r=>r.metric===m).forEach(r => { if (!isNaN(r.step) && !isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value }); });
|
371 |
+
dataByMetric.set(m, map);
|
372 |
+
});
|
|
|
373 |
|
374 |
+
// Populate metric select (default to average_rank if present)
|
375 |
+
metricList.forEach((m)=>{ const o=document.createElement('option'); o.value=m; o.textContent=m; selectMetric.appendChild(o); });
|
376 |
+
const def = metricList.find(m => /average_rank/i.test(m)) || metricList[0];
|
377 |
+
if (def) selectMetric.value = def;
|
378 |
+
|
379 |
+
container.appendChild(controls);
|
380 |
+
updateScales();
|
381 |
+
renderMetric(selectMetric.value);
|
382 |
+
|
383 |
+
selectMetric.addEventListener('change', ()=>{ renderMetric(selectMetric.value); });
|
384 |
+
|
385 |
+
const rerender = () => { renderMetric(selectMetric.value); };
|
386 |
+
if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
|
387 |
+
} catch (e) {
|
388 |
+
const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
|
389 |
+
pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';
|
390 |
+
container.appendChild(pre);
|
391 |
+
}
|
392 |
+
})();
|
393 |
};
|
394 |
|
395 |
if (document.readyState === 'loading') {
|
app/src/content/embeds/d3-pie.html
ADDED
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div class="d3-pie" style="width:100%;margin:10px 0;"></div>
|
2 |
+
<style>
|
3 |
+
.d3-pie .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
|
4 |
+
.d3-pie .legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; align-items:center; justify-content:center; }
|
5 |
+
.d3-pie .legend .item { display:flex; align-items:center; gap:8px; white-space:nowrap; }
|
6 |
+
.d3-pie .legend .swatch { width:14px; height:14px; border-radius:3px; display:inline-block; border: 1px solid var(--border-color); }
|
7 |
+
.d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
|
8 |
+
.d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
|
9 |
+
.d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: rgba(255,255,255,0.6); stroke-width: 3px; }
|
10 |
+
</style>
|
11 |
+
<script>
|
12 |
+
(() => {
|
13 |
+
const ensureD3 = (cb) => {
|
14 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
15 |
+
let s = document.getElementById('d3-cdn-script');
|
16 |
+
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); }
|
17 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
18 |
+
s.addEventListener('load', onReady, { once: true });
|
19 |
+
if (window.d3) onReady();
|
20 |
+
};
|
21 |
+
|
22 |
+
const bootstrap = () => {
|
23 |
+
const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
|
24 |
+
const container = (mount && mount.querySelector && mount.querySelector('.d3-pie')) || document.querySelector('.d3-pie');
|
25 |
+
if (!container) return;
|
26 |
+
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
27 |
+
|
28 |
+
// Tooltip
|
29 |
+
container.style.position = container.style.position || 'relative';
|
30 |
+
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
31 |
+
if (!tip) {
|
32 |
+
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
33 |
+
Object.assign(tip.style, {
|
34 |
+
position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
|
35 |
+
padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
|
36 |
+
background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease'
|
37 |
+
});
|
38 |
+
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
39 |
+
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
40 |
+
|
41 |
+
// SVG scaffolding
|
42 |
+
const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
|
43 |
+
const gRoot = svg.append('g');
|
44 |
+
const gLegend = gRoot.append('foreignObject').attr('class','legend');
|
45 |
+
const gPlots = gRoot.append('g').attr('class','plots');
|
46 |
+
|
47 |
+
// Metrics (order and labels as in the Python script)
|
48 |
+
const METRICS = [
|
49 |
+
{ key:'answer_total_tokens', name:'Answer Tokens', title:'Weighted by Answer Tokens', letter:'a' },
|
50 |
+
{ key:'total_samples', name:'Number of Samples', title:'Weighted by Number of Samples', letter:'b' },
|
51 |
+
{ key:'total_turns', name:'Number of Turns', title:'Weighted by Number of Turns', letter:'c' },
|
52 |
+
{ key:'total_images', name:'Number of Images', title:'Weighted by Number of Images', letter:'d' }
|
53 |
+
];
|
54 |
+
|
55 |
+
// CSV: prefer inline <script type="text/csv" class="d3-pie__csv">, otherwise fallback to fetch
|
56 |
+
const CSV_PATHS = [
|
57 |
+
'/data/finevision.csv', // public path priority
|
58 |
+
'/assets/data/finevision.csv',
|
59 |
+
'./assets/data/finevision.csv',
|
60 |
+
'../assets/data/finevision.csv',
|
61 |
+
'../../assets/data/finevision.csv'
|
62 |
+
];
|
63 |
+
|
64 |
+
const getInlineCsv = () => {
|
65 |
+
const el = container.querySelector('script.d3-pie__csv[type="text/csv"]');
|
66 |
+
return el ? el.textContent.trim() : '';
|
67 |
+
};
|
68 |
+
|
69 |
+
const fetchFirstAvailable = async (paths) => {
|
70 |
+
for (const p of paths) {
|
71 |
+
try {
|
72 |
+
const res = await fetch(p, { cache: 'no-cache' });
|
73 |
+
if (res.ok) { return await res.text(); }
|
74 |
+
} catch (_) { /* try next */ }
|
75 |
+
}
|
76 |
+
throw new Error('CSV introuvable: finevision.csv');
|
77 |
+
};
|
78 |
+
|
79 |
+
const parseCsv = (text) => d3.csvParse(text, (d) => ({
|
80 |
+
subset_name: (d['subset_name']||'').trim(),
|
81 |
+
eagle_cathegory: (d['eagle_cathegory']||'').trim(),
|
82 |
+
answer_total_tokens: +((d['answer_total_tokens']||'0').toString().trim()) || 0,
|
83 |
+
total_samples: +((d['total_samples']||'0').toString().trim()) || 0,
|
84 |
+
total_turns: +((d['total_turns']||'0').toString().trim()) || 0,
|
85 |
+
total_images: +((d['total_images']||'0').toString().trim()) || 0
|
86 |
+
}));
|
87 |
+
|
88 |
+
// Layout
|
89 |
+
let width=800, height=460; const margin = { top: 8, right: 24, bottom: 60, left: 24 };
|
90 |
+
const CAPTION_GAP = 28; // espace entre titre et donut (augmenté)
|
91 |
+
const LEGEND_GAP = 8; // espace entre donut et légende (réduit)
|
92 |
+
const updateSize = () => {
|
93 |
+
width = container.clientWidth || 800;
|
94 |
+
height = Math.max(320, Math.round(width/2.5));
|
95 |
+
svg.attr('width', width).attr('height', height);
|
96 |
+
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
97 |
+
return { innerWidth: width - margin.left - margin.right, innerHeight: height - margin.top - margin.bottom };
|
98 |
+
};
|
99 |
+
|
100 |
+
function renderLegend(categories, colorOf, innerWidth, innerHeight){
|
101 |
+
const legendHeight = 60;
|
102 |
+
gLegend.attr('x', 0).attr('y', innerHeight + LEGEND_GAP).attr('width', innerWidth).attr('height', legendHeight);
|
103 |
+
const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
|
104 |
+
root.html(`<div class="items">${categories.map(c => `<div class="item"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:800">${c}</span></div>`).join('')}</div>`);
|
105 |
+
}
|
106 |
+
|
107 |
+
function drawPies(rows){
|
108 |
+
const { innerWidth, innerHeight } = updateSize();
|
109 |
+
|
110 |
+
// Categories
|
111 |
+
const categories = Array.from(new Set(rows.map(r => r.eagle_cathegory))).sort();
|
112 |
+
|
113 |
+
// Build a categorical palette anchored on bar chart vibes + theme primary
|
114 |
+
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
115 |
+
const base = [primary, '#4EA5B7', '#E38A42', '#CEC0FA'];
|
116 |
+
const pool = Array.from(new Set([
|
117 |
+
...base,
|
118 |
+
...(d3.schemeTableau10 || []),
|
119 |
+
...(d3.schemeSet3 || []),
|
120 |
+
...(d3.schemePastel1 || []),
|
121 |
+
].flat().filter(Boolean)));
|
122 |
+
|
123 |
+
const colorOf = (cat) => {
|
124 |
+
const idx = categories.indexOf(cat);
|
125 |
+
return pool[idx % pool.length] || primary;
|
126 |
+
};
|
127 |
+
|
128 |
+
renderLegend(categories, colorOf, innerWidth, innerHeight);
|
129 |
+
|
130 |
+
// Clear plots
|
131 |
+
gPlots.selectAll('*').remove();
|
132 |
+
|
133 |
+
const gapX = 20;
|
134 |
+
const cols = 4;
|
135 |
+
const pieAreaWidth = innerWidth;
|
136 |
+
const cellWidth = (pieAreaWidth - gapX * (cols - 1)) / cols;
|
137 |
+
const cellHeight = innerHeight - 6; // room for captions
|
138 |
+
const radius = Math.max(30, Math.min(cellWidth, cellHeight) * 0.42);
|
139 |
+
const innerR = Math.round(radius * 0.58); // donut, trou modéré
|
140 |
+
|
141 |
+
const pie = d3.pie().sort(null).value(d => d.value).padAngle(0.02);
|
142 |
+
const arc = d3.arc().innerRadius(innerR).outerRadius(radius).cornerRadius(3);
|
143 |
+
const arcLabel = d3.arc().innerRadius((innerR + radius) / 2).outerRadius((innerR + radius) / 2);
|
144 |
+
|
145 |
+
const captions = new Map(METRICS.map(m => [m.key, `${m.title}`]));
|
146 |
+
|
147 |
+
METRICS.forEach((metric, idx) => {
|
148 |
+
// Aggregate by category
|
149 |
+
const totals = new Map(); categories.forEach(c => totals.set(c, 0));
|
150 |
+
rows.forEach(r => { totals.set(r.eagle_cathegory, totals.get(r.eagle_cathegory) + (r[metric.key] || 0)); });
|
151 |
+
const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
|
152 |
+
const totalSum = d3.sum(values, d => d.value);
|
153 |
+
|
154 |
+
const col = idx; // 0..3
|
155 |
+
const cx = col * (cellWidth + gapX) + cellWidth / 2;
|
156 |
+
const cy = innerHeight / 2;
|
157 |
+
|
158 |
+
const gCell = gPlots.append('g').attr('transform', `translate(${cx},${cy})`);
|
159 |
+
|
160 |
+
if (!totalSum || totalSum <= 0) {
|
161 |
+
gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');
|
162 |
+
} else {
|
163 |
+
const data = pie(values);
|
164 |
+
const percent = (v) => (v / totalSum) * 100;
|
165 |
+
|
166 |
+
// Slices
|
167 |
+
const slices = gCell.selectAll('path.slice').data(data).enter().append('path').attr('class','slice')
|
168 |
+
.attr('d', arc)
|
169 |
+
.attr('fill', d => colorOf(d.data.category))
|
170 |
+
.attr('stroke', 'var(--surface-bg)')
|
171 |
+
.attr('stroke-width', 1.2)
|
172 |
+
.on('mouseenter', function(ev, d){
|
173 |
+
d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
|
174 |
+
const p = percent(d.data.value);
|
175 |
+
tipInner.innerHTML = `<div><strong>${d.data.category}</strong></div><div><strong>${metric.name}</strong> ${d.data.value.toLocaleString()}</div><div><strong>Share</strong> ${p.toFixed(1)}%</div>`;
|
176 |
+
tip.style.opacity = '1';
|
177 |
+
})
|
178 |
+
.on('mousemove', function(ev){
|
179 |
+
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)`;
|
180 |
+
})
|
181 |
+
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('stroke','var(--surface-bg)'); });
|
182 |
+
|
183 |
+
// Percentage labels (>= 3%)
|
184 |
+
gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()
|
185 |
+
.append('text').attr('class','slice-label').style('pointer-events','none')
|
186 |
+
.attr('transform', d => `translate(${arcLabel.centroid(d)})`)
|
187 |
+
.attr('text-anchor','middle')
|
188 |
+
.text(d => `${percent(d.data.value).toFixed(1)}%`);
|
189 |
+
}
|
190 |
+
|
191 |
+
// Caption above donut
|
192 |
+
gCell.append('text')
|
193 |
+
.attr('class','caption')
|
194 |
+
.attr('text-anchor','middle')
|
195 |
+
.attr('y', -(radius + CAPTION_GAP))
|
196 |
+
.text(captions.get(metric.key));
|
197 |
+
});
|
198 |
+
}
|
199 |
+
|
200 |
+
async function init(){
|
201 |
+
try {
|
202 |
+
let text = getInlineCsv();
|
203 |
+
if (!text) { text = await fetchFirstAvailable(CSV_PATHS); }
|
204 |
+
const rows = parseCsv(text);
|
205 |
+
drawPies(rows);
|
206 |
+
|
207 |
+
// Resize handling
|
208 |
+
const rerender = () => drawPies(rows);
|
209 |
+
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); }
|
210 |
+
else { window.addEventListener('resize', rerender); }
|
211 |
+
} catch (err) {
|
212 |
+
const pre = document.createElement('pre'); pre.textContent = (err && err.message) ? err.message : String(err);
|
213 |
+
pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';
|
214 |
+
container.appendChild(pre);
|
215 |
+
}
|
216 |
+
}
|
217 |
+
|
218 |
+
init();
|
219 |
+
};
|
220 |
+
|
221 |
+
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
|
222 |
+
})();
|
223 |
+
</script>
|
224 |
+
|
225 |
+
|
226 |
+
|
227 |
+
|
app/src/content/embeds/palettes.html
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
<div class="palettes" style="width:100%; margin: 10px 0;">
|
2 |
<style>
|
3 |
.palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
4 |
-
.palettes .palette-card { position: relative; display: grid; grid-template-columns:
|
5 |
/* removed circular badge */
|
6 |
.palettes .palette-card__swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 8px; margin: 0; }
|
7 |
.palettes .palette-card__swatches .sw { width: 100%; min-width: 0; min-height: 0; border-radius: 8px; border: 1px solid var(--border-color); }
|
8 |
-
.palettes .palette-card__content { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; justify-content: center; min-width: 0; padding-
|
9 |
-
.palettes .palette-card__actions { display: flex; align-items: center; justify-content: flex-
|
10 |
.palettes .palette-card__actions { align-self: stretch; }
|
11 |
/* .palettes .copy-btn { margin: 0; padding: 0 10px; height: 100%; border-radius: 8px; } */
|
12 |
/* .palettes .copy-btn:hover { background: var(--primary-color); color: var(--on-primary)!important; border-color: transparent; }
|
@@ -15,7 +15,7 @@
|
|
15 |
@media (max-width: 640px) {
|
16 |
.palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; }
|
17 |
.palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
18 |
-
.palettes .palette-card__content { border-
|
19 |
.palettes .palette-card__actions { justify-self: start; }
|
20 |
}
|
21 |
</style>
|
@@ -124,11 +124,11 @@
|
|
124 |
|
125 |
return results.slice(0, 6);
|
126 |
}},
|
127 |
-
{ key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>
|
128 |
const c = chroma(baseHex).saturate(0.3);
|
129 |
return chroma.scale([c.darken(2), c, c.brighten(2)]).mode('lab').correctLightness(true).colors(6);
|
130 |
}},
|
131 |
-
{ key: 'diverging', title: 'Diverging', desc: 'For <strong>centered ranges</strong> with <strong>two extremes</strong>
|
132 |
const baseH = chroma(baseHex).get('hsl.h');
|
133 |
const compH = (baseH + 180) % 360;
|
134 |
const left = chroma.hsl(baseH, 0.75, 0.55);
|
@@ -180,7 +180,7 @@
|
|
180 |
|
181 |
content.appendChild(title); content.appendChild(desc);
|
182 |
actions.appendChild(btn);
|
183 |
-
card.appendChild(
|
184 |
grid.appendChild(card);
|
185 |
});
|
186 |
};
|
|
|
1 |
<div class="palettes" style="width:100%; margin: 10px 0;">
|
2 |
<style>
|
3 |
.palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
4 |
+
.palettes .palette-card { position: relative; display: grid; grid-template-columns: auto 1fr 260px; align-items: stretch; gap: 14px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--surface-bg); padding: 12px; transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease; }
|
5 |
/* removed circular badge */
|
6 |
.palettes .palette-card__swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 8px; margin: 0; }
|
7 |
.palettes .palette-card__swatches .sw { width: 100%; min-width: 0; min-height: 0; border-radius: 8px; border: 1px solid var(--border-color); }
|
8 |
+
.palettes .palette-card__content { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; justify-content: center; min-width: 0; padding-right: 12px; border-right: 1px solid var(--border-color); }
|
9 |
+
.palettes .palette-card__actions { display: flex; align-items: center; justify-content: flex-start; justify-self: start; }
|
10 |
.palettes .palette-card__actions { align-self: stretch; }
|
11 |
/* .palettes .copy-btn { margin: 0; padding: 0 10px; height: 100%; border-radius: 8px; } */
|
12 |
/* .palettes .copy-btn:hover { background: var(--primary-color); color: var(--on-primary)!important; border-color: transparent; }
|
|
|
15 |
@media (max-width: 640px) {
|
16 |
.palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; }
|
17 |
.palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
18 |
+
.palettes .palette-card__content { border-right: none; padding-right: 0; }
|
19 |
.palettes .palette-card__actions { justify-self: start; }
|
20 |
}
|
21 |
</style>
|
|
|
124 |
|
125 |
return results.slice(0, 6);
|
126 |
}},
|
127 |
+
{ key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.', generator: (baseHex) => {
|
128 |
const c = chroma(baseHex).saturate(0.3);
|
129 |
return chroma.scale([c.darken(2), c, c.brighten(2)]).mode('lab').correctLightness(true).colors(6);
|
130 |
}},
|
131 |
+
{ key: 'diverging', title: 'Diverging', desc: 'For <strong>centered ranges</strong> with <strong>two extremes</strong> around a <strong>baseline</strong>. (e.g., negatives/positives)', generator: (baseHex) => {
|
132 |
const baseH = chroma(baseHex).get('hsl.h');
|
133 |
const compH = (baseH + 180) % 360;
|
134 |
const left = chroma.hsl(baseH, 0.75, 0.55);
|
|
|
180 |
|
181 |
content.appendChild(title); content.appendChild(desc);
|
182 |
actions.appendChild(btn);
|
183 |
+
card.appendChild(actions); card.appendChild(content); card.appendChild(sw);
|
184 |
grid.appendChild(card);
|
185 |
});
|
186 |
};
|
app/src/styles/global.css
CHANGED
@@ -47,10 +47,10 @@ figure.has-dl-btn { position: relative; }
|
|
47 |
/* ============================================================================ */
|
48 |
/* Theme Toggle button (moved from component) */
|
49 |
/* ============================================================================ */
|
50 |
-
#theme-toggle { display: inline-flex; align-items: center; gap: 8px; border: none; background: transparent; padding: 6px 10px; border-radius: 8px; cursor: pointer; margin: 12px 16px; }
|
51 |
#theme-toggle .icon.dark { display: none; }
|
52 |
[data-theme="dark"] #theme-toggle .icon.light { display: none; }
|
53 |
[data-theme="dark"] #theme-toggle .icon.dark { display: inline; }
|
54 |
-
|
55 |
|
56 |
|
|
|
47 |
/* ============================================================================ */
|
48 |
/* Theme Toggle button (moved from component) */
|
49 |
/* ============================================================================ */
|
50 |
+
#theme-toggle { display: inline-flex; align-items: center; gap: 8px; border: none; background: transparent; padding: 6px 10px; border-radius: 8px; cursor: pointer; margin: 12px 16px; color: var(--text-color) !important; }
|
51 |
#theme-toggle .icon.dark { display: none; }
|
52 |
[data-theme="dark"] #theme-toggle .icon.light { display: none; }
|
53 |
[data-theme="dark"] #theme-toggle .icon.dark { display: inline; }
|
54 |
+
#theme-toggle .icon { filter: none !important; }
|
55 |
|
56 |
|