thibaud frere commited on
Commit
c59bbe1
·
1 Parent(s): b8e1b6c
.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
- <img class="icon light" src={sunIconUrl} alt="light" width="20" height="20" />
7
- <img class="icon dark" src={moonIconUrl} alt="dark" width="20" height="20" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 two places
15
 
16
- - `app/src/content/` — where you can find the `article.mdx`, `bibliography.bib` and html fragments.
17
- - `app/src/content/assets/` — images, audio, and other static assets. (handled by git lfs)
 
 
18
 
19
- The `article.mdx` file is the main file that contains your article.
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) => { const y0 = acc; const y1 = acc + s.values[i]; items.push({ key: s.key, color: s.color, i, y0, y1, xLabel: label, value: s.values[i] }); acc = y1; });
 
 
 
 
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
- const rects = groupsEnter.merge(groups).selectAll('rect.bar').data(d=>d.items, d=>d.key);
164
- rects.enter().append('rect').attr('class','bar').attr('x', 0).attr('width', bandWidth)
165
- .attr('y', (d)=>y(d.y1)).attr('height', (d)=>Math.max(0.5, y(d.y0) - y(d.y1)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(rects)
177
  .transition().duration(200)
178
- .attr('width', bandWidth)
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
- rects.exit().remove();
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
- // Dataset params matching the Plotly version
101
- const datasets = [
102
- { name: 'CIFAR-10', base: { ymin:0.10, ymax:0.90, k:10.0, x0:0.55 }, aug: { ymin:0.15, ymax:0.96, k:12.0, x0:0.40 }, target: 0.97 },
103
- { name: 'CIFAR-100', base: { ymin:0.05, ymax:0.70, k: 9.5, x0:0.60 }, aug: { ymin:0.08, ymax:0.80, k:11.0, x0:0.45 }, target: 0.85 },
104
- { name: 'ImageNet-1K', base: { ymin:0.02, ymax:0.68, k: 8.5, x0:0.65 }, aug: { ymin:0.04, ymax:0.75, k: 9.5, x0:0.50 }, target: 0.82 },
 
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 labelDs = document.createElement('label');
118
- Object.assign(labelDs.style, {
119
- fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px'
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
- labelDs.appendChild(selectDs);
131
-
132
- const labelAlpha = document.createElement('label');
133
- Object.assign(labelAlpha.style, {
134
- fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '10px', flex: '1', padding: '6px 10px'
 
 
 
 
 
 
 
 
 
 
135
  });
136
- labelAlpha.appendChild(document.createTextNode('Augmentation α'));
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 colorBase = '#64748b'; // slate-500
184
- const colorImproved = 'var(--primary-color)';
185
- const colorTarget = '#4b5563'; // gray-600
186
- const legendBgLight = 'rgba(255,255,255,0.85)';
187
- const legendBgDark = 'rgba(17,17,23,0.85)';
188
-
189
- // Data and helpers
190
- const N = 240;
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
- // Paths
218
  const lineGen = d3.line()
219
- .curve(d3.curveCatmullRom.alpha(0.6))
220
- .x((d, i) => xScale(xs[i]))
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.domain([0, 1]).range([0, innerWidth]);
250
- yScale.domain([0, 1]).range([innerHeight, 0]);
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('Epoch');
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('Accuracy');
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 inside plot (bottom-right), no background/border/shadow
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
- function applyDataset() {
354
- computeCurves();
355
- updatePaths();
356
  }
357
 
358
- // Hover interactions
359
- function onMove(event) {
360
- const [mx, my] = d3.pointer(event, overlay.node());
361
- const xi = Math.max(0, Math.min(N - 1, Math.round(xScale.invert(mx) * (N - 1))));
362
- const xpx = xScale(xs[xi]);
363
- const yb = yBase[xi], yi = yImp[xi], yt = yTgt[xi];
364
- hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
365
- hoverDotB.attr('cx', xpx).attr('cy', yScale(yb)).style('display', null);
366
- hoverDotI.attr('cx', xpx).attr('cy', yScale(yi)).style('display', null);
367
- hoverDotT.attr('cx', xpx).attr('cy', yScale(yt)).style('display', null);
368
-
369
- // Tooltip content
370
- const ds = datasets[datasetIndex].name;
371
- tipInner.innerHTML = `<div><strong>${ds}</strong></div>` +
372
- `<div><strong>x</strong> ${xs[xi].toFixed(2)}</div>` +
373
- `<div><span style="display:inline-block;width:10px;height:10px;background:${colorBase};border-radius:50%;margin-right:6px;"></span><strong>Baseline</strong> ${yb.toFixed(3)}</div>` +
374
- `<div><span style="display:inline-block;width:10px;height:10px;background:${colorImproved};border-radius:50%;margin-right:6px;"></span><strong>Improved</strong> ${yi.toFixed(3)}</div>` +
375
- `<div><span style="display:inline-block;width:10px;height:10px;background:${colorTarget};border-radius:50%;margin-right:6px;"></span><strong>Target</strong> ${yt.toFixed(3)}</div>`;
376
- const offsetX = 12, offsetY = 12;
377
- tip.style.opacity = '1';
378
- tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
379
- }
380
-
381
- function onLeave() {
382
- tip.style.opacity = '0';
383
- tip.style.transform = 'translate(-9999px, -9999px)';
384
- hoverLine.style('display', 'none');
385
- hoverDotB.style('display', 'none');
386
- hoverDotI.style('display', 'none');
387
- hoverDotT.style('display', 'none');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
389
 
390
- overlay.on('mousemove', onMove).on('mouseleave', onLeave);
391
-
392
- // Init + controls wiring
393
- computeCurves();
394
- updateScales();
395
- updatePaths();
396
-
397
- // Attach controls after SVG for consistency with Plotly fragment
398
- container.appendChild(controls);
399
-
400
- selectDs.addEventListener('change', (e) => {
401
- datasetIndex = parseInt(e.target.value) || 0;
402
- applyDataset();
403
- });
404
- slider.addEventListener('input', (e) => {
405
- const a = parseFloat(e.target.value) || 0;
406
- updateAlpha(a);
407
- });
408
 
409
- // Resize handling
410
- const render = () => {
411
- updateScales();
412
- updatePaths();
413
- };
414
- if (window.ResizeObserver) {
415
- const ro = new ResizeObserver(() => render());
416
- ro.observe(container);
417
- } else {
418
- window.addEventListener('resize', render);
419
- }
420
- render();
 
 
 
 
 
 
 
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: 260px 1fr auto; 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-left: 12px; border-left: 1px solid var(--border-color); }
9
- .palettes .palette-card__actions { display: flex; align-items: center; justify-content: flex-end; justify-self: end; }
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-left: none; padding-left: 0; }
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>light to dark</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> (e.g., negatives/positives) around a <strong>baseline</strong>.', 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,7 +180,7 @@
180
 
181
  content.appendChild(title); content.appendChild(desc);
182
  actions.appendChild(btn);
183
- card.appendChild(sw); card.appendChild(content); card.appendChild(actions);
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
- [data-theme="dark"] #theme-toggle .icon { filter: invert(1) brightness(1.2); }
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