thibaud frere commited on
Commit
8d0d788
·
1 Parent(s): 651ae3a

update charts and vibe coding section

Browse files
app/src/components/HtmlEmbed.astro CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right'; id?: string }
3
- const { src, title, desc, frameless = false, align = 'left', id } = 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>;
@@ -18,12 +18,14 @@ function resolveFragment(requested: string): string | null {
18
 
19
  const html = resolveFragment(src);
20
  const mountId = `frag-${Math.random().toString(36).slice(2)}`;
 
 
21
  ---
22
  { html ? (
23
  <figure class="html-embed" id={id}>
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>
@@ -80,7 +82,8 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
80
  font-size: 0.95rem;
81
  color: var(--text-color);
82
  margin: 0;
83
- padding: var(--spacing-1);
 
84
  position: relative;
85
  display: block;
86
  width: 100%;
@@ -90,7 +93,8 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
90
  border: 1px solid var(--border-color);
91
  border-radius: 10px;
92
  padding: 8px;
93
- z-index: var(--z-elevated);
 
94
  }
95
  .html-embed__card.is-frameless {
96
  background: transparent;
@@ -102,16 +106,17 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
102
  font-size: 0.9rem;
103
  color: var(--muted-color);
104
  margin: 0;
105
- padding: var(--spacing-1);
 
106
  position: relative;
107
  z-index: var(--z-elevated);
108
  display: block;
109
  width: 100%;
110
  }
111
  /* Plotly – fragments & controls */
112
- .html-embed__card svg text { fill: var(--text-color) !important; }
113
- .html-embed__card label { color: var(--text-color) !important; }
114
- .plotly-graph-div { width: 100% !important; min-height: 320px; }
115
  @media (max-width: 768px) { .plotly-graph-div { min-height: 260px; } }
116
  [id^="plot-"] { display: flex; flex-direction: column; align-items: center; gap: 15px; }
117
  .plotly_caption { font-style: italic; margin-top: 10px; }
 
1
  ---
2
+ interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right'; id?: string, data?: string | string[], config?: any }
3
+ const { src, title, desc, frameless = false, align = 'left', id, data, config } = 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>;
 
18
 
19
  const html = resolveFragment(src);
20
  const mountId = `frag-${Math.random().toString(36).slice(2)}`;
21
+ const dataAttr = Array.isArray(data) ? JSON.stringify(data) : (typeof data === 'string' ? data : undefined);
22
+ const configAttr = typeof config === 'string' ? config : (config != null ? JSON.stringify(config) : undefined);
23
  ---
24
  { html ? (
25
  <figure class="html-embed" id={id}>
26
  {title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
27
  <div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
28
+ <div id={mountId} data-datafiles={dataAttr} data-config={configAttr} set:html={html} />
29
  </div>
30
  {desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
31
  </figure>
 
82
  font-size: 0.95rem;
83
  color: var(--text-color);
84
  margin: 0;
85
+ padding: 0;
86
+ padding-bottom: var(--spacing-1);
87
  position: relative;
88
  display: block;
89
  width: 100%;
 
93
  border: 1px solid var(--border-color);
94
  border-radius: 10px;
95
  padding: 8px;
96
+ z-index: calc(var(--z-elevated) + 1);
97
+ position: relative;
98
  }
99
  .html-embed__card.is-frameless {
100
  background: transparent;
 
106
  font-size: 0.9rem;
107
  color: var(--muted-color);
108
  margin: 0;
109
+ padding: 0;
110
+ padding-top: var(--spacing-1);
111
  position: relative;
112
  z-index: var(--z-elevated);
113
  display: block;
114
  width: 100%;
115
  }
116
  /* Plotly – fragments & controls */
117
+ .html-embed__card svg text { fill: var(--text-color); }
118
+ .html-embed__card label { color: var(--text-color); }
119
+ .plotly-graph-div { width: 100%; min-height: 320px; }
120
  @media (max-width: 768px) { .plotly-graph-div { min-height: 260px; } }
121
  [id^="plot-"] { display: flex; flex-direction: column; align-items: center; gap: 15px; }
122
  .plotly_caption { font-style: italic; margin-top: 10px; }
app/src/content/article.mdx CHANGED
@@ -29,6 +29,7 @@ import GettingStarted from "./chapters/getting-started.mdx";
29
  import Markdown from "./chapters/markdown.mdx";
30
  import Components from "./chapters/components.mdx";
31
  import Greetings from "./chapters/greetings.mdx";
 
32
 
33
  <Introduction />
34
 
@@ -40,6 +41,8 @@ import Greetings from "./chapters/greetings.mdx";
40
 
41
  <Components />
42
 
 
 
43
  <BestPractices />
44
 
45
  <Greetings />
 
29
  import Markdown from "./chapters/markdown.mdx";
30
  import Components from "./chapters/components.mdx";
31
  import Greetings from "./chapters/greetings.mdx";
32
+ import VibeCodingCharts from "./chapters/vibe-coding-charts.mdx";
33
 
34
  <Introduction />
35
 
 
41
 
42
  <Components />
43
 
44
+ <VibeCodingCharts />
45
+
46
  <BestPractices />
47
 
48
  <Greetings />
app/src/content/chapters/components.mdx CHANGED
@@ -28,14 +28,6 @@ You have to import them in the **.mdx** file you want to use them in.
28
 
29
  **Responsive images** automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
30
 
31
- | Prop | Required | Description
32
- |------------------------|----------|-------------------------------------------------------
33
- | `zoomable` | No | Adds a zoomable lightbox (Medium-like).
34
- | `downloadable` | No | Adds a download button to fetch the image file.
35
- | `loading="lazy"` | No | Lazy loads the image.
36
- | `caption` | No | Adds a caption and credit.
37
- | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
38
-
39
 
40
  <ResponsiveImage
41
  src={placeholder}
@@ -44,9 +36,18 @@ You have to import them in the **.mdx** file you want to use them in.
44
  id="placeholder-image"
45
  layout="fixed"
46
  alt="A placeholder image alt text"
47
- caption={'A placeholder image description <span class="image-credit">From the <a target="_blank" href="https://commons.wikimedia.org/wiki/File:RCA_Indian_Head_Test_Pattern.svg">RCA Indian Head Test Pattern</a></span>'}
48
  />
49
 
 
 
 
 
 
 
 
 
 
50
  <Accordion title="Code example">
51
  ```mdx
52
  import ResponsiveImage from '../components/ResponsiveImage.astro'
@@ -229,6 +230,24 @@ import Note from '../../components/Note.astro'
229
  </Accordion>
230
 
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  ### HtmlEmbed
233
 
234
  The main purpose of the ```HtmlEmbed``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
@@ -238,7 +257,7 @@ They exist in the `app/src/content/embeds` folder.
238
  For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
239
 
240
 
241
- <HtmlEmbed src="d3-line-example.html" title="This is a chart title" desc="Some chart description" />
242
 
243
  | Prop | Required | Description
244
  |-------------|----------|----------------------------------------------------------------------------------
@@ -248,17 +267,25 @@ For researchers who want to stay in **Python** while targeting **D3**, the [d3bl
248
  | `frameless` | No | Removes the card background and border for seamless embeds.
249
  | `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
250
  | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
 
 
251
 
252
  <Accordion title="Code example">
253
  ```mdx
254
  import HtmlEmbed from '../components/HtmlEmbed.astro'
255
 
256
- <HtmlEmbed src="plotly-line.html" title="Plotly Line" desc="Some chart description" />
257
- <HtmlEmbed src="d3-line-example.html" title="D3 Line" desc="Some chart description" />
258
- <HtmlEmbed src="d3-line-example.html" id="real-world-examples" title="D3 Line (with id)" desc="Linkable example via #real-world-examples" />
 
 
 
 
 
259
  ```
260
  </Accordion>
261
 
 
262
  #### Data
263
 
264
  If you need to link your **HTML embeds** to **data files**, there is an **`assets/data`** folder for this.
@@ -266,62 +293,3 @@ As long as your files are there, they will be served from the **`public/data`**
266
  You can fetch them with this address: **`[domain]/data/your-data.ext`**
267
 
268
  <Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
269
-
270
- #### Vibe coding charts
271
-
272
- If you are from the research field, it can be difficult to use **D3.js** charts instead of **Plotly**.
273
- Happily, **LLM's** are here to help you! In the `app/src/content/embeds` folder, there is a `vibe-code-d3-embeds-directives.md` file that you can use to vibe code **D3.js** charts.
274
-
275
- Inside this file, you will find every directives you need to code your own **HtmlEmbed** proof **D3.js** chart.
276
-
277
- #### Real world examples
278
-
279
- Here are some examples that were vibe coded to inspire you.
280
-
281
-
282
- ---
283
- <HtmlEmbed src="d3-line.html" title="Training curves by metric" desc="Interactive time series across runs. Choose a metric; hover for values." />
284
- ---
285
- <HtmlEmbed src="d3-bar.html" title="D3 Memory usage with recomputation" desc={`Memory usage with recomputation — <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">from the ultrascale playbook</a>`}/>
286
- ---
287
- <HtmlEmbed src="d3-neural.html" id="neural-network-mnist-like" title="D3 Interactive neural network (MNIST-like)" desc="Visualize activations and class probabilities (0–9)." align="center" />
288
- ---
289
- <HtmlEmbed src="d3-pie.html" title="D3 Pie charts by category" align="center" frameless />
290
- ---
291
- <HtmlEmbed src="filters-quad.html" frameless desc={"Figure 7: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence <br/> Credit: "+'<a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>'} align="center" />
292
-
293
- {/* ---
294
- <HtmlEmbed src="d3-comparison.html" title="Image similarity: query vs top-k" desc="Compare a query image to top-k matches. Shows rank and similarity." />
295
- ---
296
- <HtmlEmbed src="d3-area-stacked.html" title="Relative metric shares over steps (by run)" desc="Stacked area of normalized metric contributions across training steps. Data: /data/all_ratings_luis.csv" />
297
- ---
298
- <HtmlEmbed src="d3-boxplot.html" title="Metric distribution across runs (box plots)" desc="Per-run distribution for a selected metric (median, quartiles, whiskers, outliers). Data: /data/all_ratings_luis.csv" />
299
- */}
300
- ---
301
- <HtmlEmbed src="d3-scatter.html" title="2D projection by category" desc={`Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
302
- ---
303
-
304
- ### Iframes
305
-
306
- You can embed external content in your article using **iframes**. For example, **TrackIO**, **Gradio** or even **Github code embeds** can be used this way.
307
-
308
- <small className="muted">Github code embed</small>
309
- <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
310
-
311
- <small className="muted">TrackIO embed</small>
312
- <div className="">
313
- <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="660" frameborder="0"></iframe>
314
- </div>
315
-
316
- <small className="muted">Gradio embed</small>
317
- <div className="">
318
- <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
319
- </div>
320
-
321
- <Accordion title="Code example">
322
- ```mdx
323
- <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
324
- <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
325
- <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
326
- ```
327
- </Accordion>
 
28
 
29
  **Responsive images** automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
30
 
 
 
 
 
 
 
 
 
31
 
32
  <ResponsiveImage
33
  src={placeholder}
 
36
  id="placeholder-image"
37
  layout="fixed"
38
  alt="A placeholder image alt text"
39
+ caption={'A placeholder image description<br/> Credit: <a target="_blank" href="https://commons.wikimedia.org/wiki/File:RCA_Indian_Head_Test_Pattern.svg">RCA Indian Head Test Pattern</a>'}
40
  />
41
 
42
+ | Prop | Required | Description
43
+ |------------------------|----------|-------------------------------------------------------
44
+ | `zoomable` | No | Adds a zoomable lightbox (Medium-like).
45
+ | `downloadable` | No | Adds a download button to fetch the image file.
46
+ | `loading="lazy"` | No | Lazy loads the image.
47
+ | `caption` | No | Adds a caption and credit.
48
+ | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
49
+
50
+
51
  <Accordion title="Code example">
52
  ```mdx
53
  import ResponsiveImage from '../components/ResponsiveImage.astro'
 
230
  </Accordion>
231
 
232
 
233
+ ### Iframes
234
+
235
+ You can embed external content in your article using **iframes**. For example, **TrackIO**, **Gradio** or even **Github code embeds** can be used this way.
236
+
237
+ <small className="muted">Gradio embed example</small>
238
+ <div className="card">
239
+ <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
240
+ </div>
241
+
242
+ <Accordion title="Code example">
243
+ ```mdx
244
+ <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
245
+ <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
246
+ <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
247
+ ```
248
+ </Accordion>
249
+
250
+
251
  ### HtmlEmbed
252
 
253
  The main purpose of the ```HtmlEmbed``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
 
257
  For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
258
 
259
 
260
+ <HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Figure X: Some chart description<br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
261
 
262
  | Prop | Required | Description
263
  |-------------|----------|----------------------------------------------------------------------------------
 
267
  | `frameless` | No | Removes the card background and border for seamless embeds.
268
  | `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
269
  | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
270
+ | `data` | No | Path (string) or array of paths (string[]) to data file(s) consumed by the embed.
271
+ | `config` | No | Optional object for embed options (e.g., `{ defaultMetric: 'average_rank' }`).
272
 
273
  <Accordion title="Code example">
274
  ```mdx
275
  import HtmlEmbed from '../components/HtmlEmbed.astro'
276
 
277
+ <HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Some chart description <br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
278
+
279
+ <HtmlEmbed
280
+ src="d3-line.html"
281
+ title="Comparison A vs B"
282
+ data={[ 'formatting_filters.csv', 'relevance_filters.csv' ]}
283
+ config={{ defaultMetric: 'average_rank' }}
284
+ />
285
  ```
286
  </Accordion>
287
 
288
+
289
  #### Data
290
 
291
  If you need to link your **HTML embeds** to **data files**, there is an **`assets/data`** folder for this.
 
293
  You can fetch them with this address: **`[domain]/data/your-data.ext`**
294
 
295
  <Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/chapters/vibe-coding-charts.mdx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import HtmlEmbed from '../../components/HtmlEmbed.astro';
2
+
3
+ ## Vibe coding charts
4
+
5
+ ### Prompt
6
+
7
+ This page explains how to use the directives to author D3 charts as self‑contained HTML fragments.
8
+ Using claude code works better.
9
+
10
+ The goal is to make **responsive**, **accessible**, **interactive** and **dark mode ready** charts.
11
+
12
+ 1. Use this ref a a baseprompt: `app/src/content/embeds/vibe-code-d3-embeds-directives.md`.
13
+ 2. Opt: use an already existing chart as a starting point.
14
+ 3. Ask claude to code the chart. Here's a typical prompt:
15
+
16
+ ```markdown
17
+ I want you to code a new d3 chart named `yourchart`.
18
+ I have one CSV file called `yourdata.csv` in the data folder.
19
+ The csv has the following columns: `x`, `y`, `z`.
20
+ I want you to code a d3 chart that visualizes the data.
21
+ ```
22
+
23
+ 4. Once the chart created, iterate with littles adjustments to make it better.
24
+ 5. And that's it! 🎉
25
+
26
+ ### Real‑world examples
27
+
28
+ They can be found in the `app/src/content/embeds` folder and you can also use them as a starting point or examples to vibe code with.
29
+
30
+ <HtmlEmbed
31
+ title="d3-benchmark: LLM Benchmark"
32
+ src="d3-benchmark.html" desc={`Figure 1: Grouped bar chart comparing model scores across benchmarks (MMLU, GSM8K, HellaSwag, TruthfulQA, ARC‑C). Each group is a benchmark; colors encode models; values are accuracy/score (higher is better).`} />
33
+ ---
34
+ <HtmlEmbed
35
+ src="d3-line.html"
36
+ title="d3-line: Average Ranking of Models"
37
+ desc='Figure 2: Average Ranking of Models trained with internally deduplicated / merged samples. No clear benefit in merging can be seen with respect to model performance.<br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>'
38
+ data="internal_deduplication.csv"
39
+ config={{
40
+ defaultMetric: ['average_rank']
41
+ }}
42
+ />
43
+ ---
44
+ <HtmlEmbed
45
+ src="d3-neural-network.html"
46
+ id="neural-network-mnist-like"
47
+ title="d3-neural-network: MNIST-like Neural Network"
48
+ desc={`Figure 3: Interactive MNIST-like neural network. Draw a digit on the left canvas; activations propagate through hidden layers (node size/opacity reflect activation). The right side displays class probabilities (0–9) with the top class emphasized.`}
49
+ />
50
+ ---
51
+ <HtmlEmbed
52
+ src="d3-matrix.html"
53
+ title="d3-matrix: Baseline and Δ (Improved − Baseline)"
54
+ frameless
55
+ desc={`<p>
56
+ Figure 4: Left: baseline matrix (row-normalized, sequential palette).
57
+ Right: Δ (Improved − Baseline) in percentage points, using a diverging palette centered at 0 to highlight improvements vs degradations.
58
+ </p>`}
59
+ />
60
+ ---
61
+ <HtmlEmbed title="d3-line-quad: Comparison across thresholds" src="d3-line-quad.html" desc={"Figure 5: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence <br/> Credit: "+'<a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>'} />
62
+ ---
63
+ <HtmlEmbed src="d3-bar.html" title="d3-bar: Memory usage with recomputation" desc={`Figure 6: Memory usage with recomputation.<br/>Credits: <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">Ultrascale playbook</a>`}/>
64
+ ---
65
+ <HtmlEmbed src="d3-pie.html" title="d3-pie: Pie charts by category" align="center" frameless desc='Figure 7: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>' />
66
+ ---
67
+ <HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
68
+ ---
69
+
70
+
app/src/content/embeds/banner.html CHANGED
@@ -91,7 +91,8 @@
91
  // Create SVG
92
  const svg = d3.select(container).append('svg')
93
  .attr('width', '100%')
94
- .style('display', 'block');
 
95
 
96
  const render = () => {
97
  const width = container.clientWidth || 800;
@@ -104,6 +105,7 @@
104
  // Subtle stroke color depending on theme
105
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
106
  const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
 
107
 
108
 
109
  // Group for points (no blend mode for better print/PDF visibility)
@@ -122,20 +124,28 @@
122
  left: '0px',
123
  transform: 'translate(-9999px, -9999px)',
124
  pointerEvents: 'none',
125
- padding: '8px 10px',
126
- borderRadius: '8px',
127
  fontSize: '12px',
128
  lineHeight: '1.35',
129
  border: '1px solid var(--border-color)',
130
  background: 'var(--surface-bg)',
131
  color: 'var(--text-color)',
132
- boxShadow: '0 4px 24px rgba(0,0,0,.18)',
133
  opacity: '0',
134
- transition: 'opacity .12s ease'
 
 
135
  });
136
  tipInner = document.createElement('div');
137
  tipInner.className = 'd3-tooltip__inner';
138
- tipInner.style.textAlign = 'left';
 
 
 
 
 
 
139
  tip.appendChild(tipInner);
140
  container.appendChild(tip);
141
  } else {
@@ -160,16 +170,21 @@
160
  .attr('stroke-width', 0.4)
161
  .on('mouseenter', function(ev, i) {
162
  d3.select(this).raise()
 
 
163
  .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
164
- .attr('stroke-width', 1.2);
 
 
165
  const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
166
  const type = i < lenSpiral ? 'spiral' : 'bulge';
167
  const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
168
- tipInner.innerHTML = `<div><strong>${labelOf(i)}</strong></div>` +
169
- `<div><strong>Type</strong> ${type}${arm ? ` (arm ${arm})` : ''}</div>` +
170
- `<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>` +
171
- `<div><strong>X</strong> ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
172
- `<div><strong>r</strong> ${r.toFixed(3)} · <strong>z</strong> ${Zraw[i].toFixed(3)}</div>`;
 
173
  tip.style.opacity = '1';
174
  })
175
  .on('mousemove', (ev, i) => {
@@ -180,7 +195,13 @@
180
  .on('mouseleave', function() {
181
  tip.style.opacity = '0';
182
  tip.style.transform = 'translate(-9999px, -9999px)';
183
- d3.select(this).attr('stroke', strokeColor).attr('stroke-width', 0.4);
 
 
 
 
 
 
184
  }),
185
  (update) => update
186
  .attr('cx', (i) => xScale(X[i]))
@@ -192,16 +213,21 @@
192
  .attr('stroke-width', 0.4)
193
  .on('mouseenter', function(ev, i) {
194
  d3.select(this).raise()
 
 
195
  .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
196
- .attr('stroke-width', 1.2);
 
 
197
  const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
198
  const type = i < lenSpiral ? 'spiral' : 'bulge';
199
  const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
200
- tipInner.innerHTML = `<div><strong>${labelOf(i)}</strong></div>` +
201
- `<div><strong>Type</strong> ${type}${arm ? ` (arm ${arm})` : ''}</div>` +
202
- `<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>` +
203
- `<div><strong>X</strong> ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
204
- `<div><strong>r</strong> ${r.toFixed(3)} · <strong>z</strong> ${Zraw[i].toFixed(3)}</div>`;
 
205
  tip.style.opacity = '1';
206
  })
207
  .on('mousemove', (ev, i) => {
@@ -212,7 +238,13 @@
212
  .on('mouseleave', function() {
213
  tip.style.opacity = '0';
214
  tip.style.transform = 'translate(-9999px, -9999px)';
215
- d3.select(this).attr('stroke', strokeColor).attr('stroke-width', 0.4);
 
 
 
 
 
 
216
  })
217
  );
218
  };
 
91
  // Create SVG
92
  const svg = d3.select(container).append('svg')
93
  .attr('width', '100%')
94
+ .style('display', 'block')
95
+ .style('cursor', 'crosshair');
96
 
97
  const render = () => {
98
  const width = container.clientWidth || 800;
 
105
  // Subtle stroke color depending on theme
106
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
107
  const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
108
+ const glowColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.25)';
109
 
110
 
111
  // Group for points (no blend mode for better print/PDF visibility)
 
124
  left: '0px',
125
  transform: 'translate(-9999px, -9999px)',
126
  pointerEvents: 'none',
127
+ padding: '10px 12px',
128
+ borderRadius: '12px',
129
  fontSize: '12px',
130
  lineHeight: '1.35',
131
  border: '1px solid var(--border-color)',
132
  background: 'var(--surface-bg)',
133
  color: 'var(--text-color)',
134
+ boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)',
135
  opacity: '0',
136
+ transition: 'opacity .12s ease',
137
+ backdropFilter: 'saturate(1.12) blur(8px)',
138
+ zIndex: '20'
139
  });
140
  tipInner = document.createElement('div');
141
  tipInner.className = 'd3-tooltip__inner';
142
+ Object.assign(tipInner.style, {
143
+ textAlign: 'left',
144
+ display: 'flex',
145
+ flexDirection: 'column',
146
+ gap: '6px',
147
+ minWidth: '220px'
148
+ });
149
  tip.appendChild(tipInner);
150
  container.appendChild(tip);
151
  } else {
 
170
  .attr('stroke-width', 0.4)
171
  .on('mouseenter', function(ev, i) {
172
  d3.select(this).raise()
173
+ .style('filter', `drop-shadow(0 0 8px ${glowColor})`)
174
+ .transition().duration(120).ease(d3.easeCubicOut)
175
  .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
176
+ .attr('stroke-width', 1.4)
177
+ .attr('r', (sizesPx[i] / 2) * 1.25)
178
+ .attr('fill-opacity', 1);
179
  const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
180
  const type = i < lenSpiral ? 'spiral' : 'bulge';
181
  const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
182
+ tipInner.innerHTML =
183
+ `<div style="font-weight:800;letter-spacing:.1px;"><strong>${labelOf(i)}</strong></div>` +
184
+ `<div style="font-size:11px;color:var(--muted-color);margin-top:-4px;margin-bottom:2px;letter-spacing:.1px;"><strong>Type</strong> ${type}${arm ? ` (Arm ${arm})` : ''}</div>` +
185
+ `<div style="padding-top:6px;border-top:1px solid var(--border-color);"><strong>Position</strong> X ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
186
+ `<div><strong>Distance</strong> Radius ${r.toFixed(3)} · <strong>Z</strong> ${Zraw[i].toFixed(3)}</div>` +
187
+ `<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>`;
188
  tip.style.opacity = '1';
189
  })
190
  .on('mousemove', (ev, i) => {
 
195
  .on('mouseleave', function() {
196
  tip.style.opacity = '0';
197
  tip.style.transform = 'translate(-9999px, -9999px)';
198
+ d3.select(this)
199
+ .style('filter', null)
200
+ .transition().duration(120).ease(d3.easeCubicOut)
201
+ .attr('stroke', strokeColor)
202
+ .attr('stroke-width', 0.4)
203
+ .attr('r', (i2) => sizesPx[i2] / 2)
204
+ .attr('fill-opacity', 0.9);
205
  }),
206
  (update) => update
207
  .attr('cx', (i) => xScale(X[i]))
 
213
  .attr('stroke-width', 0.4)
214
  .on('mouseenter', function(ev, i) {
215
  d3.select(this).raise()
216
+ .style('filter', `drop-shadow(0 0 8px ${glowColor})`)
217
+ .transition().duration(120).ease(d3.easeCubicOut)
218
  .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
219
+ .attr('stroke-width', 1.4)
220
+ .attr('r', (sizesPx[i] / 2) * 1.25)
221
+ .attr('fill-opacity', 1);
222
  const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
223
  const type = i < lenSpiral ? 'spiral' : 'bulge';
224
  const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
225
+ tipInner.innerHTML =
226
+ `<div style="font-weight:800;letter-spacing:.1px;"><strong>${labelOf(i)}</strong></div>` +
227
+ `<div style="font-size:11px;color:var(--muted-color);margin-top:-4px;margin-bottom:2px;letter-spacing:.1px;"><strong>Type</strong> ${type}${arm ? ` (Arm ${arm})` : ''}</div>` +
228
+ `<div style="padding-top:6px;border-top:1px solid var(--border-color);"><strong>Position</strong> X ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
229
+ `<div><strong>Distance</strong> Radius ${r.toFixed(3)} · <strong>Z</strong> ${Zraw[i].toFixed(3)}</div>` +
230
+ `<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>`;
231
  tip.style.opacity = '1';
232
  })
233
  .on('mousemove', (ev, i) => {
 
238
  .on('mouseleave', function() {
239
  tip.style.opacity = '0';
240
  tip.style.transform = 'translate(-9999px, -9999px)';
241
+ d3.select(this)
242
+ .style('filter', null)
243
+ .transition().duration(120).ease(d3.easeCubicOut)
244
+ .attr('stroke', strokeColor)
245
+ .attr('stroke-width', 0.4)
246
+ .attr('r', (i2) => sizesPx[i2] / 2)
247
+ .attr('fill-opacity', 0.9);
248
  })
249
  );
250
  };
app/src/content/embeds/bar.html DELETED
@@ -1 +0,0 @@
1
- <div> <div id="3e4ed4fe-23b4-4ec1-810e-20d6fce0209b" class="plotly-graph-div" style="height:100%; width:100%;"></div> <script type="text/javascript"> window.PLOTLYENV=window.PLOTLYENV || {}; if (document.getElementById("3e4ed4fe-23b4-4ec1-810e-20d6fce0209b")) { Plotly.newPlot( "3e4ed4fe-23b4-4ec1-810e-20d6fce0209b", [{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[4.0,4.0,4.0,4.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[4.0,4.0,4.0,4.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[8.0,8.0,8.0,8.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[3.6,14.4,57.6,230.4],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[13.3,13.3,13.3,13.3],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[13.3,13.3,13.3,13.3],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[26.6,26.6,26.6,26.6],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[9.3,37.2,148.8,595.2],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[26.0,26.0,26.0,26.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[26.0,26.0,26.0,26.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[52.0,52.0,52.0,52.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[46.2,184.8,739.2,2956.8],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[244.0,244.0,244.0,244.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[244.0,244.0,244.0,244.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[488.0,488.0,488.0,488.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[145.7,582.8,2331.2,9324.8],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[1520.0,1520.0,1520.0,1520.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[1520.0,1520.0,1520.0,1520.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[3040.0,3040.0,3040.0,3040.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[1519.9,6079.6,24318.4,97273.6],"type":"bar"}], {"template":{"data":{"histogram2dcontour":[{"type":"histogram2dcontour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"choropleth":[{"type":"choropleth","colorbar":{"outlinewidth":0,"ticks":""}}],"histogram2d":[{"type":"histogram2d","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"heatmap":[{"type":"heatmap","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"heatmapgl":[{"type":"heatmapgl","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"contourcarpet":[{"type":"contourcarpet","colorbar":{"outlinewidth":0,"ticks":""}}],"contour":[{"type":"contour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"surface":[{"type":"surface","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"mesh3d":[{"type":"mesh3d","colorbar":{"outlinewidth":0,"ticks":""}}],"scatter":[{"fillpattern":{"fillmode":"overlay","size":10,"solidity":0.2},"type":"scatter"}],"parcoords":[{"type":"parcoords","line":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolargl":[{"type":"scatterpolargl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"bar":[{"error_x":{"color":"#2a3f5f"},"error_y":{"color":"#2a3f5f"},"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"bar"}],"scattergeo":[{"type":"scattergeo","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolar":[{"type":"scatterpolar","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"histogram":[{"marker":{"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"histogram"}],"scattergl":[{"type":"scattergl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatter3d":[{"type":"scatter3d","line":{"colorbar":{"outlinewidth":0,"ticks":""}},"marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattermapbox":[{"type":"scattermapbox","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterternary":[{"type":"scatterternary","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattercarpet":[{"type":"scattercarpet","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"carpet":[{"aaxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"baxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"type":"carpet"}],"table":[{"cells":{"fill":{"color":"#EBF0F8"},"line":{"color":"white"}},"header":{"fill":{"color":"#C8D4E3"},"line":{"color":"white"}},"type":"table"}],"barpolar":[{"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"barpolar"}],"pie":[{"automargin":true,"type":"pie"}]},"layout":{"autotypenumbers":"strict","colorway":["#636efa","#EF553B","#00cc96","#ab63fa","#FFA15A","#19d3f3","#FF6692","#B6E880","#FF97FF","#FECB52"],"font":{"color":"#2a3f5f"},"hovermode":"closest","hoverlabel":{"align":"left"},"paper_bgcolor":"white","plot_bgcolor":"#E5ECF6","polar":{"bgcolor":"#E5ECF6","angularaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"radialaxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"ternary":{"bgcolor":"#E5ECF6","aaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"baxis":{"gridcolor":"white","linecolor":"white","ticks":""},"caxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"coloraxis":{"colorbar":{"outlinewidth":0,"ticks":""}},"colorscale":{"sequential":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"sequentialminus":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"diverging":[[0,"#8e0152"],[0.1,"#c51b7d"],[0.2,"#de77ae"],[0.3,"#f1b6da"],[0.4,"#fde0ef"],[0.5,"#f7f7f7"],[0.6,"#e6f5d0"],[0.7,"#b8e186"],[0.8,"#7fbc41"],[0.9,"#4d9221"],[1,"#276419"]]},"xaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"yaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"scene":{"xaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"yaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"zaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2}},"shapedefaults":{"line":{"color":"#2a3f5f"}},"annotationdefaults":{"arrowcolor":"#2a3f5f","arrowhead":0,"arrowwidth":1},"geo":{"bgcolor":"white","landcolor":"#E5ECF6","subunitcolor":"white","showland":true,"showlakes":true,"lakecolor":"white"},"title":{"x":0.05},"mapbox":{"style":"light"}}},"margin":{"l":40,"r":28,"t":20,"b":40},"legend":{"orientation":"h","yanchor":"bottom","y":1.02,"xanchor":"left","x":0},"xaxis":{"title":{"text":"Sequence Length"},"fixedrange":true},"yaxis":{"title":{"text":"Memory (GB)"},"fixedrange":true},"barmode":"stack","autosize":true,"paper_bgcolor":"rgba(0,0,0,0)","plot_bgcolor":"rgba(0,0,0,0)","hovermode":"x unified","updatemenus":[{"active":0,"buttons":[{"args":[{"visible":[true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]},{"yaxis":{"range":[0,258.72]}}],"label":"1B","method":"update"},{"args":[{"visible":[false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false]},{"yaxis":{"range":[0,680.8200000000002]}}],"label":"3B","method":"update"},{"args":[{"visible":[false,false,false,false,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false]},{"yaxis":{"range":[0,3213.84]}}],"label":"8B","method":"update"},{"args":[{"visible":[false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,false,false,false,false]},{"yaxis":{"range":[0,10815.84]}}],"label":"70B","method":"update"},{"args":[{"visible":[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true]},{"yaxis":{"range":[0,108521.28000000001]}}],"label":"405B","method":"update"}],"showactive":true,"type":"dropdown","x":1.03,"xanchor":"left","y":0.6,"yanchor":"top"},{"active":0,"buttons":[{"args":[{"y":[[4.0,4.0,4.0,4.0],[4.0,4.0,4.0,4.0],[8.0,8.0,8.0,8.0],[3.6,14.4,57.6,230.4],[13.3,13.3,13.3,13.3],[13.3,13.3,13.3,13.3],[26.6,26.6,26.6,26.6],[9.3,37.2,148.8,595.2],[26.0,26.0,26.0,26.0],[26.0,26.0,26.0,26.0],[52.0,52.0,52.0,52.0],[46.2,184.8,739.2,2956.8],[244.0,244.0,244.0,244.0],[244.0,244.0,244.0,244.0],[488.0,488.0,488.0,488.0],[145.7,582.8,2331.2,9324.8],[1520.0,1520.0,1520.0,1520.0],[1520.0,1520.0,1520.0,1520.0],[3040.0,3040.0,3040.0,3040.0],[1519.9,6079.6,24318.4,97273.6]]},{"yaxis":{"range":[0,108521.28000000001]}}],"label":"None","method":"update"},{"args":[{"y":[[4.0,4.0,4.0,4.0],[4.0,4.0,4.0,4.0],[8.0,8.0,8.0,8.0],[0.9,3.6,14.4,57.6],[13.3,13.3,13.3,13.3],[13.3,13.3,13.3,13.3],[26.6,26.6,26.6,26.6],[2.325,9.3,37.2,148.8],[26.0,26.0,26.0,26.0],[26.0,26.0,26.0,26.0],[52.0,52.0,52.0,52.0],[11.55,46.2,184.8,739.2],[244.0,244.0,244.0,244.0],[244.0,244.0,244.0,244.0],[488.0,488.0,488.0,488.0],[36.425,145.7,582.8,2331.2],[1520.0,1520.0,1520.0,1520.0],[1520.0,1520.0,1520.0,1520.0],[3040.0,3040.0,3040.0,3040.0],[379.975,1519.9,6079.6,24318.4]]},{"yaxis":{"range":[0,31918.320000000003]}}],"label":"selective","method":"update"},{"args":[{"y":[[4.0,4.0,4.0,4.0],[4.0,4.0,4.0,4.0],[8.0,8.0,8.0,8.0],[0.225,0.9,3.6,14.4],[13.3,13.3,13.3,13.3],[13.3,13.3,13.3,13.3],[26.6,26.6,26.6,26.6],[0.58125,2.325,9.3,37.2],[26.0,26.0,26.0,26.0],[26.0,26.0,26.0,26.0],[52.0,52.0,52.0,52.0],[2.8875,11.55,46.2,184.8],[244.0,244.0,244.0,244.0],[244.0,244.0,244.0,244.0],[488.0,488.0,488.0,488.0],[9.10625,36.425,145.7,582.8],[1520.0,1520.0,1520.0,1520.0],[1520.0,1520.0,1520.0,1520.0],[3040.0,3040.0,3040.0,3040.0],[94.99375,379.975,1519.9,6079.6]]},{"yaxis":{"range":[0,12767.580000000002]}}],"label":"full","method":"update"}],"showactive":true,"type":"dropdown","x":1.03,"xanchor":"left","y":0.4,"yanchor":"top"}],"annotations":[{"showarrow":false,"text":"Model Size:","x":1.03,"xanchor":"left","xref":"paper","y":0.6,"yanchor":"bottom","yref":"paper"},{"showarrow":false,"text":"Recomputation:","x":1.03,"xanchor":"left","xref":"paper","y":0.4,"yanchor":"bottom","yref":"paper"}]}, {"displayModeBar": false, "responsive": true, "scrollZoom": false} ) }; </script> </div>
 
 
app/src/content/embeds/d3-area-stacked.html DELETED
@@ -1,220 +0,0 @@
1
- <div class="d3-area-stacked" style="width:100%;margin:10px 0;"></div>
2
- <style>
3
- .d3-area-stacked .controls {
4
- margin-top: 12px;
5
- display: flex;
6
- gap: 16px;
7
- align-items: center;
8
- flex-wrap: wrap;
9
- }
10
- .d3-area-stacked .controls label {
11
- font-size: 12px;
12
- color: var(--muted-color);
13
- display: flex;
14
- align-items: center;
15
- gap: 8px;
16
- white-space: nowrap;
17
- padding: 6px 10px;
18
- }
19
- .d3-area-stacked .controls select {
20
- font-size: 12px;
21
- padding: 8px 28px 8px 10px;
22
- border: 1px solid var(--border-color);
23
- border-radius: 8px;
24
- background-color: var(--surface-bg);
25
- color: var(--text-color);
26
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
27
- background-repeat: no-repeat;
28
- background-position: right 8px center;
29
- background-size: 12px;
30
- -webkit-appearance: none;
31
- -moz-appearance: none;
32
- appearance: none;
33
- cursor: pointer;
34
- transition: border-color .15s ease, box-shadow .15s ease;
35
- }
36
- [data-theme="dark"] .d3-area-stacked .controls select {
37
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
38
- }
39
- .d3-area-stacked .controls select:hover { border-color: var(--primary-color); }
40
- .d3-area-stacked .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
41
- .d3-area-stacked .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
42
- </style>
43
- <script>
44
- (() => {
45
- const ensureD3 = (cb) => {
46
- if (window.d3 && typeof window.d3.select === 'function') return cb();
47
- let s = document.getElementById('d3-cdn-script');
48
- 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); }
49
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
50
- s.addEventListener('load', onReady, { once: true });
51
- if (window.d3) onReady();
52
- };
53
-
54
- const bootstrap = () => {
55
- const scriptEl = document.currentScript;
56
- let container = scriptEl ? scriptEl.previousElementSibling : null;
57
- if (!(container && container.classList && container.classList.contains('d3-area-stacked'))){
58
- const cs = Array.from(document.querySelectorAll('.d3-area-stacked')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
59
- container = cs[cs.length-1] || null;
60
- }
61
- if (!container) return;
62
- if (container.dataset){ if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
63
-
64
- // Tooltip
65
- container.style.position = container.style.position || 'relative';
66
- let tip = container.querySelector('.d3-tooltip'); let tipInner;
67
- if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style,{ position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
68
-
69
- // Controls
70
- const controls = document.createElement('div'); controls.className='controls';
71
- const labelRun = document.createElement('label'); labelRun.textContent='Run';
72
- const selRun = document.createElement('select');
73
- labelRun.appendChild(selRun);
74
- const labelSmooth = document.createElement('label'); labelSmooth.textContent='Smoothing';
75
- const selSmooth = document.createElement('select'); ['none','monotone','basis'].forEach((s)=>{ const o=document.createElement('option'); o.value=s; o.textContent=s; selSmooth.appendChild(o); });
76
- labelSmooth.appendChild(selSmooth);
77
-
78
- // SVG
79
- const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
80
- const gRoot = svg.append('g');
81
- const gGrid = gRoot.append('g').attr('class','grid');
82
- const gAxes = gRoot.append('g').attr('class','axes');
83
- const gAreas = gRoot.append('g').attr('class','areas');
84
- const gLegend = gRoot.append('foreignObject').attr('class','legend');
85
-
86
- // State & scales
87
- let width=800,height=360; const margin={top:16,right:28,bottom:56,left:64};
88
- const x=d3.scaleLinear();
89
- const y=d3.scaleLinear();
90
- const color=d3.scaleOrdinal().range(['var(--primary-color)','rgb(78, 165, 183)','rgb(227, 138, 66)','rgb(206, 192, 250)']);
91
-
92
- // Data (real): relative contributions of selected metrics over steps for a run
93
- const metricsToUse = ['ai2d_exact_match','docvqa_val_anls','textvqa_val_exact_match','chartqa_relaxed_overall'];
94
- let categories = metricsToUse.slice();
95
- let allRows = [];
96
- let currentRun = null;
97
- let stacked = [];
98
-
99
- async function fetchFirstAvailable(paths){
100
- for (const p of paths){
101
- try { const res = await fetch(p, { cache:'no-cache' }); if (res.ok) return await res.text(); } catch(e){}
102
- }
103
- throw new Error('Failed to load area data');
104
- }
105
-
106
- function computeStackForRun(runName){
107
- const rows = allRows.filter(r=>r.run===runName && metricsToUse.includes(r.metric));
108
- const byStep = d3.rollup(rows, v=>{
109
- const m = new Map(); v.forEach(r=>{ m.set(r.metric, +r.value); });
110
- const obj = {}; metricsToUse.forEach(k=>{ obj[k] = m.get(k) ?? 0; });
111
- // Normalize to shares
112
- const sum = metricsToUse.reduce((acc,k)=>acc + Math.max(0, obj[k]), 0) || 1;
113
- metricsToUse.forEach(k=>{ obj[k] = Math.max(0, obj[k]) / sum; });
114
- return obj;
115
- }, r=>+r.step);
116
- const steps = Array.from(byStep.keys()).sort((a,b)=>a-b);
117
- const series = categories.map(cat => steps.map(step => ({ x: step, y: byStep.get(step)[cat] })));
118
- stacked = d3.stack().keys(d3.range(categories.length)).value((d, key)=>d[key].y)(d3.transpose(series));
119
- return steps;
120
- }
121
-
122
- function renderLegend(innerWidth){
123
- const legendWidth=160, legendHeight=84;
124
- gLegend.attr('x', innerWidth-legendWidth).attr('y', 0).attr('width', legendWidth).attr('height', legendHeight);
125
- const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
126
- root.html(`
127
- <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end;">
128
- ${categories.map((c,i)=>`<div style=\"display:flex;align-items:center;gap:8px;\"><span style=\"width:18px;height:10px;background:${color(c)};border-radius:2px;display:inline-block\"></span><span>${c}</span></div>`).join('')}
129
- </div>
130
- `);
131
- }
132
-
133
- function updateScales(){
134
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
135
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
136
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
137
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
138
-
139
- width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
140
- const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
141
-
142
- const yMax = stacked.length ? d3.max(stacked[stacked.length-1], (d) => d[1]) : 1;
143
- const xMin = stacked.length ? d3.min(stacked[0], d=>d.data[0].x) : 0;
144
- const xMax = stacked.length ? d3.max(stacked[0], d=>d.data[d.data.length-1].x) : 1;
145
- x.domain([xMin, xMax]).range([0, innerWidth]).nice();
146
- y.domain([0, yMax]).range([innerHeight, 0]).nice();
147
-
148
- // Grid
149
- gGrid.selectAll('*').remove();
150
- gGrid.selectAll('line.grid-y').data(y.ticks(5)).join('line')
151
- .attr('class','grid-y').attr('x1',0).attr('x2',innerWidth).attr('y1',d=>y(d)).attr('y2',d=>y(d))
152
- .attr('stroke', gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
153
-
154
- // Axes
155
- gAxes.selectAll('*').remove();
156
- gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x).ticks(6)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
157
- gAxes.append('g').call(d3.axisLeft(y).ticks(5)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
158
- gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').style('font-size','12px').style('fill', tickColor).text('Step');
159
- gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-48},${innerHeight/2}) rotate(-90)`).style('font-size','12px').style('fill', tickColor).text('Value');
160
-
161
- renderLegend(innerWidth);
162
- return { innerWidth, innerHeight };
163
- }
164
-
165
- function draw(){
166
- const { innerWidth, innerHeight } = updateScales();
167
-
168
- const curve = selSmooth.value==='monotone' ? d3.curveMonotoneX : selSmooth.value==='basis' ? d3.curveBasis : d3.curveLinear;
169
- const area = d3.area().curve(curve).x((d,i)=>x(d.data[i].x)).y0(d=>y(d[0])).y1(d=>y(d[1]));
170
-
171
- const layers = gAreas.selectAll('path.layer').data(stacked);
172
- layers.enter().append('path').attr('class','layer')
173
- .attr('fill', (d,i)=>color(categories[i]))
174
- .attr('opacity', 0.9)
175
- .on('mouseenter', function(ev, d){ const i = stacked.indexOf(d); d3.select(this).attr('opacity', 1); tipInner.innerHTML = `<div><strong>${categories[i]}</strong></div>`; tip.style.opacity = '1'; })
176
- .on('mousemove', function(ev){ const [mx,my]=d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
177
- .on('mouseleave', function(){ d3.select(this).attr('opacity', 0.9); tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; })
178
- .merge(layers)
179
- .transition().duration(180)
180
- .attr('d', d=>area(d));
181
- layers.exit().remove();
182
- }
183
-
184
- container.appendChild(controls);
185
- controls.appendChild(labelRun);
186
- controls.appendChild(labelSmooth);
187
- selSmooth.addEventListener('change', ()=>draw());
188
-
189
- (async () => {
190
- try {
191
- const csvText = await fetchFirstAvailable([
192
- '/data/all_ratings_luis.csv',
193
- '/data/against_baselines.csv',
194
- '/data/ss_vs_s1.csv',
195
- './assets/data/all_ratings_luis.csv',
196
- '../assets/data/all_ratings_luis.csv',
197
- '/data/all_ratings_luis.csv'
198
- ]);
199
- allRows = d3.csvParse(csvText, d=>({ run: d.run, step: +d.step, metric: d.metric, value: +d.value }));
200
- const runs = Array.from(new Set(allRows.map(r=>r.run))).filter(Boolean);
201
- runs.forEach(r=>{ const o=document.createElement('option'); o.value=r; o.textContent=r; selRun.appendChild(o); });
202
- currentRun = runs[0]; selRun.value = currentRun;
203
- selRun.addEventListener('change', (e)=>{ currentRun = e.target.value; draw(); });
204
- color.domain(categories);
205
- computeStackForRun(currentRun);
206
- draw();
207
- } catch (e) {
208
- const pre = document.createElement('pre'); pre.style.color = 'crimson'; pre.textContent = 'Failed to load area data.'; container.appendChild(pre);
209
- }
210
- })();
211
-
212
- const rerender = () => { draw(); };
213
- if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
214
- };
215
-
216
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
217
- })();
218
- </script>
219
-
220
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-bar.html CHANGED
@@ -1,12 +1,66 @@
1
- <div class="d3-bar" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
- .d3-bar .controls { margin-top: 12px; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
4
- .d3-bar .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
5
  .d3-bar .controls select { font-size: 12px; padding: 8px 28px 8px 10px; border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--surface-bg); color: var(--text-color); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
6
  [data-theme="dark"] .d3-bar .controls select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); }
7
  .d3-bar .controls select:hover { border-color: var(--primary-color); }
8
  .d3-bar .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
9
- .d3-bar .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </style>
11
  <script>
12
  (() => {
@@ -28,12 +82,7 @@
28
  // Data, matching bar.py
29
  const seqLabels = ["1024","2048","4096","8192"];
30
  const seqScale = [1,2,4,8];
31
- const components = [
32
- { key: 'parameters', color: 'rgb(78, 165, 183)' },
33
- { key: 'gradients', color: 'rgb(227, 138, 66)' },
34
- { key: 'optimizer', color: 'var(--primary-color)' },
35
- { key: 'activations', color: 'rgb(206, 192, 250)' },
36
- ];
37
  const modelSizes = ["1B","3B","8B","70B","405B"];
38
  const paramsMem = { "1B":4.0, "3B":13.3, "8B":26.0, "70B":244.0, "405B":1520.0 };
39
  const actCoeff = { "1B":3.6, "3B":9.3, "8B":46.2, "70B":145.7, "405B":1519.9 };
@@ -62,20 +111,32 @@
62
  // Controls
63
  const controls = document.createElement('div');
64
  controls.className = 'controls';
 
65
  const labelSize = document.createElement('label'); labelSize.textContent = 'Model Size';
66
  const selSize = document.createElement('select'); modelSizes.forEach((s) => { const o = document.createElement('option'); o.value = s; o.textContent = s; selSize.appendChild(o); });
67
- labelSize.appendChild(selSize);
 
68
  const labelRecomp = document.createElement('label'); labelRecomp.textContent = 'Recomputation';
69
  const selRecomp = document.createElement('select'); recomputeModes.forEach((m) => { const o = document.createElement('option'); o.value = m; o.textContent = m; selRecomp.appendChild(o); });
70
- labelRecomp.appendChild(selRecomp);
71
 
72
- // SVG scaffolding
73
- const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
 
 
 
 
 
 
 
 
 
 
 
74
  const gRoot = svg.append('g');
75
  const gGrid = gRoot.append('g').attr('class','grid');
76
  const gAxes = gRoot.append('g').attr('class','axes');
77
  const gBars = gRoot.append('g').attr('class','bars');
78
- const gLegend = gRoot.append('foreignObject').attr('class','legend');
79
 
80
  // Tooltip
81
  container.style.position = container.style.position || 'relative';
@@ -91,7 +152,21 @@
91
  let width=800, height=360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
92
  const x0 = d3.scaleBand().paddingInner(0.25).paddingOuter(0.1); // groups (seq)
93
  const y = d3.scaleLinear();
94
- const colorOf = (key) => components.find((c)=>c.key===key).color;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  function yMax(sizeKey, mode){
97
  const s = Y[mode][sizeKey];
@@ -99,23 +174,27 @@
99
  return max*1.05;
100
  }
101
 
102
- function renderLegend(innerWidth, innerHeight){
103
- const legendWidth = 160, legendHeight = 84;
104
- gLegend.attr('x', 15).attr('y', -3).attr('width', legendWidth).attr('height', legendHeight);
105
- const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
106
- root.html(`
107
- <div style="display:flex;flex-direction:column;gap:6px;">
108
- ${components.map(c => `<div style="display:flex;align-items:center;gap:8px;"><span style="width:18px;height:10px;background:${c.color};border-radius:2px;display:inline-block"></span><span>${c.key}</span></div>`).join('')}
109
- </div>
110
- `);
 
 
 
 
 
 
 
 
 
111
  }
112
 
113
  function updateScales(){
114
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
115
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
116
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
117
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
118
-
119
  width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
120
  const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
121
 
@@ -126,25 +205,25 @@
126
  gGrid.selectAll('*').remove();
127
  gGrid.selectAll('line').data(y.ticks(6)).join('line')
128
  .attr('x1', 0).attr('x2', innerWidth).attr('y1', (d)=>y(d)).attr('y2', (d)=>y(d))
129
- .attr('stroke', gridColor).attr('stroke-width', 1).attr('shape-rendering', 'crispEdges');
130
 
131
  // Axes
132
  gAxes.selectAll('*').remove();
133
- gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
134
- gAxes.append('g').call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('~f'))).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
135
 
136
  // Axis labels
137
- gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').style('font-size','12px').style('fill', tickColor).text('Sequence Length');
138
- gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`).style('font-size','12px').style('fill', tickColor).text('Memory (GB)');
139
 
140
- renderLegend(innerWidth, innerHeight);
141
 
142
  return { innerWidth, innerHeight };
143
  }
144
 
145
  function drawBars(){
146
  const stacks = Y[currentMode][currentSize];
147
- const series = components.map((c)=>({ key: c.key, color: c.color, values: stacks[c.key] }));
148
  // Stack values
149
  const stacked = seqLabels.map((label, i) => {
150
  let acc = 0; const items = [];
@@ -153,6 +232,8 @@
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
 
@@ -187,14 +268,39 @@
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);
191
- tipInner.innerHTML = `<div><strong>${d.key}</strong></div><div><strong>Seq</strong> ${d.xLabel}</div><div><strong>Mem</strong> ${d.value.toFixed(1)} GB</div>`;
 
 
 
 
 
 
 
 
 
 
192
  tip.style.opacity = '1';
 
 
 
193
  })
194
  .on('mousemove', function(ev, d){
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))
@@ -206,8 +312,8 @@
206
 
207
  // Boot
208
  update();
209
- container.appendChild(controls);
210
- controls.appendChild(labelSize); controls.appendChild(labelRecomp);
211
  selSize.addEventListener('change', (e)=>{ currentSize = e.target.value; update(); });
212
  selRecomp.addEventListener('change', (e)=>{ currentMode = e.target.value; update(); });
213
 
 
1
+ <div class="d3-bar" ></div>
2
  <style>
3
+ /* Theme-driven axis/tick/grid variables */
4
+ .d3-bar {
5
+ --axis-color: rgba(0,0,0,0.25);
6
+ --tick-color: rgba(0,0,0,0.55);
7
+ --grid-color: rgba(0,0,0,0.05);
8
+ }
9
+ [data-theme="dark"] .d3-bar {
10
+ --axis-color: rgba(255,255,255,0.25);
11
+ --tick-color: rgba(255,255,255,0.70);
12
+ --grid-color: rgba(255,255,255,0.08);
13
+ }
14
+ .d3-bar .controls { margin-top: 0; display: flex; gap: 16px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
15
+ .d3-bar .controls .control-group { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
16
+ .d3-bar .controls label { font-size: 12px; color: var(--text-color); font-weight: 700; }
17
  .d3-bar .controls select { font-size: 12px; padding: 8px 28px 8px 10px; border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--surface-bg); color: var(--text-color); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
18
  [data-theme="dark"] .d3-bar .controls select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); }
19
  .d3-bar .controls select:hover { border-color: var(--primary-color); }
20
  .d3-bar .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
21
+ /* Header (legend + controls) placed after chart */
22
+ .d3-bar .chart-header { display: flex; align-items: flex-start; justify-content: flex-start; gap: 12px; margin: 8px 0 0 0; flex-wrap: wrap; }
23
+ .d3-bar .legend-bottom { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; font-size: 12px; color: var(--text-color); }
24
+ .d3-bar .legend-bottom .legend-title { font-size: 12px; font-weight: 700; color: var(--text-color); }
25
+ .d3-bar .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
26
+ .d3-bar .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
27
+ .d3-bar .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border-color); display: inline-block; }
28
+ .d3-bar.hovering .legend-bottom .item.ghost { opacity: .35; }
29
+ .d3-bar.hovering .bars path.ghost { opacity: .35; }
30
+ .d3-bar .axis-label { fill: var(--text-color); font-size: 12px; font-weight: 700; }
31
+ /* Apply axis/tick/grid purely via CSS */
32
+ .d3-bar .axes path,
33
+ .d3-bar .axes line { stroke: var(--axis-color); }
34
+ .d3-bar .axes text { fill: var(--tick-color); }
35
+ .d3-bar .grid line { stroke: var(--grid-color); }
36
+ /* Tooltip improvements */
37
+ .d3-bar .d3-tooltip { z-index: var(--z-tooltip); backdrop-filter: saturate(1.12) blur(8px); }
38
+ /* Hover/transition styling for bars and legend */
39
+ .d3-bar .bars path.bar { transition: opacity .12s ease, stroke .12s ease, stroke-width .12s ease; }
40
+ .d3-bar .bars path.bar.highlight { stroke: none; stroke-width: 0; }
41
+ .d3-bar.hovering .bars path.ghost { opacity: .25; }
42
+ .d3-bar .legend-bottom .item.hovered { color: inherit; }
43
+ .d3-bar .legend-bottom .item.hovered .swatch { border-color: var(--border-color); }
44
+ .d3-bar .d3-tooltip .swatch { width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-color); display: inline-block; margin-right: 6px; vertical-align: -2px; }
45
+ /* Chart card wrapper */
46
+ .d3-bar .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
47
+ /* Layout adjustments to give controls more space */
48
+ .d3-bar .chart-header {
49
+ padding-left: 8px;
50
+ padding-right: 8px;
51
+ gap: 20px;
52
+ }
53
+ .d3-bar .controls {
54
+ justify-content: flex-start;
55
+ min-width: 320px;
56
+ }
57
+ .d3-bar .controls .control-group {
58
+ min-width: 150px;
59
+ }
60
+ .d3-bar .controls select {
61
+ font-size: 13px;
62
+ min-width: 160px;
63
+ }
64
  </style>
65
  <script>
66
  (() => {
 
82
  // Data, matching bar.py
83
  const seqLabels = ["1024","2048","4096","8192"];
84
  const seqScale = [1,2,4,8];
85
+ const componentKeys = ['parameters','gradients','optimizer','activations'];
 
 
 
 
 
86
  const modelSizes = ["1B","3B","8B","70B","405B"];
87
  const paramsMem = { "1B":4.0, "3B":13.3, "8B":26.0, "70B":244.0, "405B":1520.0 };
88
  const actCoeff = { "1B":3.6, "3B":9.3, "8B":46.2, "70B":145.7, "405B":1519.9 };
 
111
  // Controls
112
  const controls = document.createElement('div');
113
  controls.className = 'controls';
114
+ const groupSize = document.createElement('div'); groupSize.className = 'control-group';
115
  const labelSize = document.createElement('label'); labelSize.textContent = 'Model Size';
116
  const selSize = document.createElement('select'); modelSizes.forEach((s) => { const o = document.createElement('option'); o.value = s; o.textContent = s; selSize.appendChild(o); });
117
+ groupSize.appendChild(labelSize); groupSize.appendChild(selSize);
118
+ const groupRecomp = document.createElement('div'); groupRecomp.className = 'control-group';
119
  const labelRecomp = document.createElement('label'); labelRecomp.textContent = 'Recomputation';
120
  const selRecomp = document.createElement('select'); recomputeModes.forEach((m) => { const o = document.createElement('option'); o.value = m; o.textContent = m; selRecomp.appendChild(o); });
121
+ groupRecomp.appendChild(labelRecomp); groupRecomp.appendChild(selRecomp);
122
 
123
+ // Header (legend + controls) to be placed after chart
124
+ const header = document.createElement('div'); header.className = 'chart-header';
125
+ const legendBottom = document.createElement('div'); legendBottom.className = 'legend-bottom';
126
+ const legendTitle = document.createElement('div'); legendTitle.className = 'legend-title'; legendTitle.textContent = 'Legend';
127
+ const legendItems = document.createElement('div'); legendItems.className = 'items';
128
+ legendBottom.appendChild(legendTitle); legendBottom.appendChild(legendItems);
129
+ header.appendChild(legendBottom);
130
+ header.appendChild(controls);
131
+ // SVG scaffolding inside a card wrapper
132
+ const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
133
+ // Place header after the chart card
134
+ container.appendChild(header);
135
+ const svg = d3.select(card).append('svg').attr('width','100%').style('display','block');
136
  const gRoot = svg.append('g');
137
  const gGrid = gRoot.append('g').attr('class','grid');
138
  const gAxes = gRoot.append('g').attr('class','axes');
139
  const gBars = gRoot.append('g').attr('class','bars');
 
140
 
141
  // Tooltip
142
  container.style.position = container.style.position || 'relative';
 
152
  let width=800, height=360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
153
  const x0 = d3.scaleBand().paddingInner(0.25).paddingOuter(0.1); // groups (seq)
154
  const y = d3.scaleLinear();
155
+ function getCategoricalColors(count){
156
+ try {
157
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
158
+ return window.ColorPalettes.getColors('categorical', count);
159
+ }
160
+ } catch(_) {}
161
+ const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
162
+ const tableau = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'];
163
+ const pool = [primary, ...tableau];
164
+ const arr = []; for (let i=0;i<count;i++){ arr.push(pool[i % pool.length]); }
165
+ return arr;
166
+ }
167
+ const palette = getCategoricalColors(componentKeys.length);
168
+ const colorMap = new Map(componentKeys.map((k,i)=>[k, palette[i]]));
169
+ const colorOf = (key) => colorMap.get(key) || 'var(--primary-color)';
170
 
171
  function yMax(sizeKey, mode){
172
  const s = Y[mode][sizeKey];
 
174
  return max*1.05;
175
  }
176
 
177
+ function renderLegend(){
178
+ legendItems.innerHTML = componentKeys.map((key, i) => {
179
+ const color = palette[i];
180
+ return `<span class="item" data-key="${key}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${key}</span></span>`;
181
+ }).join('');
182
+ legendItems.querySelectorAll('.item').forEach((el) => {
183
+ el.addEventListener('mouseenter', () => {
184
+ const k = el.getAttribute('data-key'); if (!k) return;
185
+ container.classList.add('hovering');
186
+ gBars.selectAll('path.bar').classed('ghost', d => d && d.key !== k);
187
+ legendItems.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-key') !== k));
188
+ });
189
+ el.addEventListener('mouseleave', () => {
190
+ container.classList.remove('hovering');
191
+ gBars.selectAll('path.bar').classed('ghost', false);
192
+ legendItems.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
193
+ });
194
+ });
195
  }
196
 
197
  function updateScales(){
 
 
 
 
 
198
  width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
199
  const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
200
 
 
205
  gGrid.selectAll('*').remove();
206
  gGrid.selectAll('line').data(y.ticks(6)).join('line')
207
  .attr('x1', 0).attr('x2', innerWidth).attr('y1', (d)=>y(d)).attr('y2', (d)=>y(d))
208
+ .attr('stroke', 'var(--grid-color)').attr('stroke-width', 1).attr('shape-rendering', 'crispEdges');
209
 
210
  // Axes
211
  gAxes.selectAll('*').remove();
212
+ gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0)).call((g)=>{ g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px'); });
213
+ gAxes.append('g').call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('~f'))).call((g)=>{ g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px'); });
214
 
215
  // Axis labels
216
+ gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').text('Sequence Length');
217
+ gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`).text('Memory (GB)');
218
 
219
+ renderLegend();
220
 
221
  return { innerWidth, innerHeight };
222
  }
223
 
224
  function drawBars(){
225
  const stacks = Y[currentMode][currentSize];
226
+ const series = componentKeys.map((key, i)=>({ key, color: palette[i], values: stacks[key] }));
227
  // Stack values
228
  const stacked = seqLabels.map((label, i) => {
229
  let acc = 0; const items = [];
 
232
  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 });
233
  acc = y1;
234
  });
235
+ const total = acc;
236
+ items.forEach(it => { it.total = total; });
237
  return { label, items };
238
  });
239
 
 
268
  .attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
269
  .attr('fill', (d)=>d.color)
270
  .on('mouseenter', function(ev, d){
271
+ container.classList.add('hovering');
272
+ gBars.selectAll('path.bar').classed('ghost', (dd) => !(dd && dd.key === d.key));
273
+ const pct = d.total > 0 ? (d.value / d.total * 100) : 0;
274
+ tipInner.innerHTML = `
275
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
276
+ <span class="swatch" style="background:${d.color}"></span>
277
+ <strong>${d.key}</strong>
278
+ </div>
279
+ <div><strong>Seq</strong> ${d.xLabel}</div>
280
+ <div><strong>Mem</strong> ${d.value.toFixed(1)} GB <span style="opacity:.7">(${pct.toFixed(0)}%)</span></div>
281
+ <div style="opacity:.7"><strong>Total</strong> ${d.total.toFixed(1)} GB</div>
282
+ `;
283
  tip.style.opacity = '1';
284
+ const li = legendItems.querySelector(`.item[data-key="${d.key}"]`);
285
+ if (li) li.classList.add('hovered');
286
+ legendItems.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-key') !== d.key));
287
  })
288
  .on('mousemove', function(ev, d){
289
+ const [mx, my] = d3.pointer(ev, container);
290
+ const offsetX = 12, offsetY = 12;
291
+ const maxX = (container.clientWidth || 0) - (tip.offsetWidth + 6);
292
+ const maxY = (container.clientHeight || 0) - (tip.offsetHeight + 6);
293
+ const tx = Math.max(0, Math.min(mx + offsetX, maxX));
294
+ const ty = Math.max(0, Math.min(my + offsetY, maxY));
295
+ tip.style.transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`;
296
+ })
297
+ .on('mouseleave', function(){
298
+ tip.style.opacity='0';
299
+ tip.style.transform='translate(-9999px, -9999px)';
300
+ container.classList.remove('hovering');
301
+ gBars.selectAll('path.bar').classed('ghost', false).classed('highlight', false);
302
+ legendItems.querySelectorAll('.item').forEach(it => { it.classList.remove('hovered'); it.classList.remove('ghost'); });
303
  })
 
304
  .merge(bars)
305
  .transition().duration(200)
306
  .attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
 
312
 
313
  // Boot
314
  update();
315
+ // controls already appended to footer; populate control groups
316
+ controls.appendChild(groupSize); controls.appendChild(groupRecomp);
317
  selSize.addEventListener('change', (e)=>{ currentSize = e.target.value; update(); });
318
  selRecomp.addEventListener('change', (e)=>{ currentMode = e.target.value; update(); });
319
 
app/src/content/embeds/d3-benchmark.html ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="d3-benchmark"></div>
2
+ <style>
3
+ .d3-benchmark {
4
+ position: relative;
5
+ --axis-color: rgba(0,0,0,0.25);
6
+ --tick-color: rgba(0,0,0,0.55);
7
+ --grid-color: rgba(0,0,0,0.05);
8
+ }
9
+ [data-theme="dark"] .d3-benchmark {
10
+ --axis-color: rgba(255,255,255,0.25);
11
+ --tick-color: rgba(255,255,255,0.70);
12
+ --grid-color: rgba(255,255,255,0.08);
13
+ }
14
+ .d3-benchmark .controls {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: 12px;
18
+ margin-bottom: 10px;
19
+ }
20
+ .d3-benchmark .controls label {
21
+ font-size: 12px;
22
+ color: var(--muted-color);
23
+ }
24
+ .d3-benchmark .controls select {
25
+ appearance: none;
26
+ -webkit-appearance: none;
27
+ -moz-appearance: none;
28
+ border: 1px solid var(--border-color);
29
+ border-radius: 8px;
30
+ padding: 6px 28px 6px 10px;
31
+ background-color: var(--surface-bg);
32
+ color: var(--text-color);
33
+ font-size: 13px;
34
+ line-height: 1.2;
35
+ background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.41 1.59L6 6.17l4.59-4.58L12 3 6 9 0 3z' fill='%23999'/%3E%3C/svg%3E");
36
+ background-repeat: no-repeat;
37
+ background-position: right 8px center;
38
+ }
39
+ .d3-benchmark .controls select:focus-visible {
40
+ outline: 2px solid var(--primary-color);
41
+ outline-offset: 2px;
42
+ }
43
+ .d3-benchmark .legend {
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: flex-start;
47
+ gap: 6px;
48
+ margin: 8px 0 0 0;
49
+ }
50
+ .d3-benchmark .legend .legend-title {
51
+ font-size: 12px;
52
+ font-weight: 700;
53
+ color: var(--text-color);
54
+ }
55
+ .d3-benchmark .legend .items {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ gap: 8px 14px;
59
+ }
60
+ .d3-benchmark .legend .item {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 8px;
64
+ font-size: 12px;
65
+ color: var(--muted-color);
66
+ cursor: pointer;
67
+ }
68
+ .d3-benchmark .legend .swatch {
69
+ width: 14px;
70
+ height: 14px;
71
+ border-radius: 3px;
72
+ border: 1px solid var(--border-color);
73
+ }
74
+ .d3-benchmark .ghost { opacity: .25; }
75
+ .d3-benchmark .d3-tooltip {
76
+ position: absolute;
77
+ top: 0px;
78
+ left: 0px;
79
+ transform: translate(-9999px, -9999px);
80
+ pointer-events: none;
81
+ padding: 8px 10px;
82
+ border-radius: 8px;
83
+ font-size: 12px;
84
+ line-height: 1.35;
85
+ border: 1px solid var(--border-color);
86
+ background: var(--surface-bg);
87
+ color: var(--text-color);
88
+ box-shadow: 0 4px 24px rgba(0,0,0,.18);
89
+ opacity: 0;
90
+ transition: opacity .12s ease;
91
+ text-align: left;
92
+ }
93
+ .d3-benchmark .chart-card {
94
+ background: var(--surface-bg);
95
+ border: 1px solid var(--border-color);
96
+ border-radius: 10px;
97
+ padding: 8px;
98
+ }
99
+ </style>
100
+ <script>
101
+ (() => {
102
+ const ensureD3 = (cb) => {
103
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
104
+ let s = document.getElementById('d3-cdn-script');
105
+ if (!s) {
106
+ s = document.createElement('script');
107
+ s.id = 'd3-cdn-script';
108
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
109
+ document.head.appendChild(s);
110
+ }
111
+ const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
112
+ s.addEventListener('load', onReady, { once: true });
113
+ if (window.d3) onReady();
114
+ };
115
+
116
+ const bootstrap = () => {
117
+ const scriptEl = document.currentScript;
118
+ let container = scriptEl ? scriptEl.previousElementSibling : null;
119
+ if (!(container && container.classList && container.classList.contains('d3-benchmark'))){
120
+ const cs = Array.from(document.querySelectorAll('.d3-benchmark')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
121
+ container = cs[cs.length-1] || null;
122
+ }
123
+ if (!container) return;
124
+ if (container.dataset) { if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
125
+
126
+ container.style.position = container.style.position || 'relative';
127
+ let tip = container.querySelector('.d3-tooltip'); let tipInner;
128
+ if (!tip) {
129
+ tip = document.createElement('div'); tip.className = 'd3-tooltip';
130
+ tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tip.appendChild(tipInner);
131
+ container.appendChild(tip);
132
+ } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
133
+
134
+ // header below chart
135
+ const header = document.createElement('div'); header.className = 'chart-header';
136
+
137
+ const makeLegend = (series, colorBySeries) => {
138
+ let legend = header.querySelector('.legend');
139
+ if (!legend) { legend = document.createElement('div'); legend.className = 'legend'; header.appendChild(legend); }
140
+ // Ensure title
141
+ let title = legend.querySelector('.legend-title');
142
+ if (!title) { title = document.createElement('div'); title.className = 'legend-title'; title.textContent = 'Legend'; legend.appendChild(title); }
143
+ // Ensure items container
144
+ let items = legend.querySelector('.items');
145
+ if (!items) { items = document.createElement('div'); items.className = 'items'; legend.appendChild(items); }
146
+ items.innerHTML = '';
147
+ series.forEach(name => {
148
+ const item = document.createElement('div'); item.className = 'item';
149
+ const sw = document.createElement('span'); sw.className = 'swatch'; sw.style.background = colorBySeries(name);
150
+ const txt = document.createElement('span'); txt.textContent = name;
151
+ item.appendChild(sw); item.appendChild(txt); items.appendChild(item);
152
+ item.addEventListener('mouseenter', () => { state.highlightModel = name; updateHighlight(); });
153
+ item.addEventListener('mouseleave', () => { state.highlightModel = null; updateHighlight(); });
154
+ });
155
+ };
156
+
157
+ // SVG scaffolding inside a card wrapper, then header appended after
158
+ const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
159
+ container.appendChild(header);
160
+ const svg = d3.select(card).append('svg').attr('width','100%').style('display','block');
161
+ const gRoot = svg.append('g');
162
+
163
+ // No controls (fixed scale mode)
164
+
165
+ // Public-first data loading with inline fallback
166
+ const fetchFirstAvailable = async (paths) => {
167
+ for (const p of paths) {
168
+ try {
169
+ const res = await fetch(p, { cache:'no-cache' });
170
+ if (!res.ok) throw new Error('HTTP '+res.status);
171
+ const text = await res.text();
172
+ // Try JSON first; if CSV, parse with d3.csvParse
173
+ try { return JSON.parse(text); } catch(e) {}
174
+ if (window.d3 && d3.csvParse) { return d3.csvParse(text); }
175
+ } catch (e) { /* keep trying */ }
176
+ }
177
+ return null;
178
+ };
179
+
180
+ // Inline fallback dataset (scores in % where applicable)
181
+ const inlineData = [
182
+ { benchmark:'MMLU', model:'GPT-4o', score: 88 },
183
+ { benchmark:'MMLU', model:'Llama 3 70B', score: 80 },
184
+ { benchmark:'MMLU', model:'Mixtral 8x7B',score: 73 },
185
+ { benchmark:'MMLU', model:'Gemma 2 27B', score: 76 },
186
+ { benchmark:'GSM8K', model:'GPT-4o', score: 94 },
187
+ { benchmark:'GSM8K', model:'Llama 3 70B', score: 83 },
188
+ { benchmark:'GSM8K', model:'Mixtral 8x7B',score: 79 },
189
+ { benchmark:'GSM8K', model:'Gemma 2 27B', score: 81 },
190
+ { benchmark:'HellaSwag', model:'GPT-4o', score: 95 },
191
+ { benchmark:'HellaSwag', model:'Llama 3 70B', score: 89 },
192
+ { benchmark:'HellaSwag', model:'Mixtral 8x7B',score: 86 },
193
+ { benchmark:'HellaSwag', model:'Gemma 2 27B', score: 87 },
194
+ { benchmark:'TruthfulQA', model:'GPT-4o', score: 64 },
195
+ { benchmark:'TruthfulQA', model:'Llama 3 70B', score: 56 },
196
+ { benchmark:'TruthfulQA', model:'Mixtral 8x7B',score: 51 },
197
+ { benchmark:'TruthfulQA', model:'Gemma 2 27B', score: 53 },
198
+ { benchmark:'ARC-C', model:'GPT-4o', score: 79 },
199
+ { benchmark:'ARC-C', model:'Llama 3 70B', score: 72 },
200
+ { benchmark:'ARC-C', model:'Mixtral 8x7B',score: 68 },
201
+ { benchmark:'ARC-C', model:'Gemma 2 27B', score: 70 }
202
+ ];
203
+
204
+ const state = {
205
+ data: inlineData,
206
+ colorsByModel: null,
207
+ highlightModel: null,
208
+ };
209
+
210
+ const margin = { top: 12, right: 28, bottom: 24, left: 56 };
211
+ let width = 800, height = 360;
212
+ const x0 = d3.scaleBand().paddingInner(0.2).paddingOuter(0.05); // group: benchmark
213
+ const x1 = d3.scaleBand().padding(0.12); // series: model per benchmark
214
+ const y = d3.scaleLinear();
215
+ const xAxis = d3.axisBottom(x0).tickSizeOuter(0);
216
+ const yAxis = d3.axisLeft(y).ticks(6).tickSizeOuter(0);
217
+ const yTopPadding = 2; // avoid bars touching top at max
218
+
219
+ function getPrimaryColor(){
220
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getPrimary === 'function') return window.ColorPalettes.getPrimary(); } catch(e) {}
221
+ return getComputedStyle(document.documentElement).getPropertyValue('--primary-color') || '#6D4AFF';
222
+ }
223
+ function getCategoricalColors(n){
224
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(e) {}
225
+ // Fallback: generate hues around the primary color (simple fallback)
226
+ const base = getPrimaryColor();
227
+ const colors = [];
228
+ for (let i=0;i<n;i++) {
229
+ const hue = Math.round((360/n)*i);
230
+ colors.push(`hsl(${hue}, 60%, 55%)`);
231
+ }
232
+ return colors;
233
+ }
234
+
235
+ function computeSeriesColors(models){
236
+ const palette = getCategoricalColors(models.length);
237
+ const map = new Map(models.map((m, i) => [m, palette[i % palette.length]]));
238
+ return (model) => map.get(model) || getPrimaryColor();
239
+ }
240
+
241
+ function getModels(data){
242
+ return Array.from(new Set(data.map(d => d.model)));
243
+ }
244
+ function getBenchmarks(data){
245
+ return Array.from(new Set(data.map(d => d.benchmark)));
246
+ }
247
+
248
+ function updateSize(){
249
+ width = container.clientWidth || 800;
250
+ height = Math.max(240, Math.round(width / 3.4));
251
+ svg.attr('width', width).attr('height', height);
252
+ gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
253
+ return { innerWidth: width - margin.left - margin.right, innerHeight: height - margin.top - margin.bottom };
254
+ }
255
+
256
+ function showTip(html, x, y){
257
+ tip.style.transform = `translate(${x + 12}px, ${y + 12}px)`;
258
+ tip.style.opacity = '1';
259
+ const inner = tip.querySelector('.d3-tooltip__inner') || tip;
260
+ inner.innerHTML = html;
261
+ }
262
+ function hideTip(){
263
+ tip.style.opacity = '0';
264
+ tip.style.transform = 'translate(-9999px, -9999px)';
265
+ }
266
+
267
+ function updateHighlight(){
268
+ const model = state.highlightModel;
269
+ const bars = gRoot.selectAll('rect.bar');
270
+ const labels = gRoot.selectAll('text.value');
271
+ if (model) {
272
+ bars.classed('ghost', d => d.model !== model);
273
+ labels.classed('ghost', d => d.model !== model);
274
+ const items = container.querySelectorAll('.legend .item');
275
+ items.forEach((el) => {
276
+ const name = el.textContent.trim();
277
+ if (name !== model) el.classList.add('ghost'); else el.classList.remove('ghost');
278
+ });
279
+ } else {
280
+ bars.classed('ghost', false);
281
+ labels.classed('ghost', false);
282
+ container.querySelectorAll('.legend .item').forEach(el => el.classList.remove('ghost'));
283
+ }
284
+ }
285
+
286
+ function render(){
287
+ const { innerWidth, innerHeight } = updateSize();
288
+ const models = getModels(state.data);
289
+ if (!state.colorsByModel) state.colorsByModel = computeSeriesColors(models);
290
+ makeLegend(models, state.colorsByModel);
291
+
292
+ x0.domain(getBenchmarks(state.data)).range([0, innerWidth]);
293
+ x1.domain(models).range([0, x0.bandwidth()]);
294
+
295
+ const yMaxRaw = 100;
296
+ const yMax = yMaxRaw + yTopPadding;
297
+ y.domain([0, yMax]).range([innerHeight, 0]).nice();
298
+
299
+ // Axes (standardized colors)
300
+ gRoot
301
+ .selectAll('.axis-x')
302
+ .data([0])
303
+ .join('g')
304
+ .attr('class','axis-x')
305
+ .attr('transform',`translate(0,${innerHeight})`)
306
+ .call(xAxis)
307
+ .call(g => {
308
+ g.selectAll('path, line').attr('stroke', 'var(--axis-color)');
309
+ g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px');
310
+ });
311
+ gRoot
312
+ .selectAll('.axis-y')
313
+ .data([0])
314
+ .join('g')
315
+ .attr('class','axis-y')
316
+ .call(yAxis)
317
+ .call(g => {
318
+ g.selectAll('path, line').attr('stroke', 'var(--axis-color)');
319
+ g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px');
320
+ });
321
+
322
+ // Gridlines (y) standardized color
323
+ gRoot
324
+ .selectAll('.grid-y')
325
+ .data([0])
326
+ .join('g')
327
+ .attr('class','grid-y')
328
+ .call(d3.axisLeft(y).ticks(6).tickSize(-innerWidth).tickFormat(''))
329
+ .call(g => g.select('.domain').remove())
330
+ .call(g => g.selectAll('.tick line').attr('stroke','var(--grid-color)').attr('stroke-opacity',1))
331
+ .call(g => g.selectAll('.tick').filter((d, i, nodes) => i === nodes.length - 1).select('line').attr('stroke-opacity', 0));
332
+
333
+ // Groups per benchmark
334
+ const groups = gRoot.selectAll('.group').data(getBenchmarks(state.data), d => d);
335
+ const groupsEnter = groups.enter().append('g').attr('class','group');
336
+ groupsEnter.merge(groups).attr('transform', d => `translate(${x0(d)},0)`);
337
+ groups.exit().remove();
338
+
339
+ // Bars per model
340
+ const nested = d3.group(state.data, d => d.benchmark);
341
+ groupsEnter.each(function(bench){ d3.select(this).selectAll('rect.bar').data([]).join('rect'); });
342
+ const allGroups = gRoot.selectAll('.group');
343
+ allGroups.each(function(bench){
344
+ const dataForBench = nested.get(bench) || [];
345
+ const bars = d3.select(this).selectAll('rect.bar').data(models.map(m => ({ bench, model:m, score:(dataForBench.find(dd=>dd.model===m)||{score:0}).score })) , d => d.model);
346
+ bars.join(
347
+ enter => enter.append('rect')
348
+ .attr('class','bar')
349
+ .attr('x', d => x1(d.model))
350
+ .attr('y', innerHeight)
351
+ .attr('width', x1.bandwidth())
352
+ .attr('height', 0)
353
+ .attr('fill', d => state.colorsByModel(d.model))
354
+ .on('mouseenter', (event, d) => { state.highlightModel = d.model; updateHighlight(); })
355
+ .on('mousemove', (event, d) => {
356
+ const [mx, my] = d3.pointer(event, container);
357
+ showTip(`<strong>${d.model}</strong><br/>${d.bench}: <strong>${d.score}</strong>`, mx, my);
358
+ })
359
+ .on('mouseleave', () => { hideTip(); state.highlightModel = null; updateHighlight(); })
360
+ .transition().duration(160)
361
+ .attr('y', d => y(d.score))
362
+ .attr('height', d => Math.max(0, innerHeight - y(d.score))),
363
+ update => update
364
+ .on('mouseenter', (event, d) => { state.highlightModel = d.model; updateHighlight(); })
365
+ .on('mousemove', (event, d) => {
366
+ const [mx, my] = d3.pointer(event, container);
367
+ showTip(`<strong>${d.model}</strong><br/>${d.bench}: <strong>${d.score}</strong>`, mx, my);
368
+ })
369
+ .on('mouseleave', () => { hideTip(); state.highlightModel = null; updateHighlight(); })
370
+ .transition().duration(160)
371
+ .attr('x', d => x1(d.model))
372
+ .attr('y', d => y(d.score))
373
+ .attr('width', x1.bandwidth())
374
+ .attr('height', d => Math.max(0, innerHeight - y(d.score)))
375
+ .attr('fill', d => state.colorsByModel(d.model)),
376
+ exit => exit.transition().duration(120).attr('y', innerHeight).attr('height', 0).remove()
377
+ );
378
+
379
+ // Value labels centered above bars (small, darker)
380
+ const labels = d3.select(this).selectAll('text.value').data(models.map(m => ({ bench, model:m, score:(dataForBench.find(dd=>dd.model===m)||{score:0}).score })) , d => d.model);
381
+ labels.join(
382
+ enter => enter.append('text')
383
+ .attr('class','value')
384
+ .attr('x', d => x1(d.model) + x1.bandwidth()/2)
385
+ .attr('y', d => y(d.score) - 4)
386
+ .attr('text-anchor','middle')
387
+ .attr('fill','var(--text-color)')
388
+ .attr('opacity',0.9)
389
+ .attr('font-size',10)
390
+ .text(d => d.score),
391
+ update => update
392
+ .transition().duration(160)
393
+ .attr('x', d => x1(d.model) + x1.bandwidth()/2)
394
+ .attr('y', d => y(d.score) - 4)
395
+ .text(d => d.score),
396
+ exit => exit.remove()
397
+ );
398
+ });
399
+
400
+ // Axis labels
401
+ gRoot.selectAll('.y-label').data([0]).join('text').attr('class','y-label')
402
+ .attr('transform', `rotate(-90)`)
403
+ .attr('x', -innerHeight / 2)
404
+ .attr('y', -margin.left + 24)
405
+ .attr('text-anchor','middle')
406
+ .attr('fill','var(--text-color)')
407
+ .attr('font-size',12)
408
+ .attr('font-weight',700)
409
+ .text('score');
410
+ }
411
+
412
+ // Initial render + resize handling
413
+ render();
414
+ const rerender = () => render();
415
+ if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); }
416
+ else { window.addEventListener('resize', rerender); }
417
+
418
+ // Attempt to load external data (public-first). Expect either JSON array with {benchmark, model, score}
419
+ (async () => {
420
+ const maybe = await fetchFirstAvailable([
421
+ '/data/llm_benchmarks.json',
422
+ './assets/data/llm_benchmarks.json',
423
+ '../assets/data/llm_benchmarks.json'
424
+ ]);
425
+ if (Array.isArray(maybe) && maybe.length && maybe[0].benchmark && maybe[0].model && (typeof maybe[0].score === 'number')) {
426
+ state.data = maybe;
427
+ state.colorsByModel = null; // recompute in case of different model set
428
+ render();
429
+ } else if (maybe && maybe.columns) {
430
+ // CSV parsed via d3.csvParse -> convert fields
431
+ const parsed = maybe.map(r => ({ benchmark: r.benchmark, model: r.model, score: +r.score }));
432
+ if (parsed.length) { state.data = parsed; state.colorsByModel = null; render(); }
433
+ }
434
+ })().catch(() => {
435
+ // Graceful failure: inline fallback already rendered
436
+ });
437
+ };
438
+
439
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); }
440
+ else { ensureD3(bootstrap); }
441
+ })();
442
+ </script>
443
+
444
+
app/src/content/embeds/d3-boxplot.html DELETED
@@ -1,251 +0,0 @@
1
- <div class="d3-boxplot" style="width:100%;margin:10px 0;"></div>
2
- <style>
3
- .d3-boxplot .controls {
4
- margin-top: 12px;
5
- display: flex;
6
- gap: 16px;
7
- align-items: center;
8
- flex-wrap: wrap;
9
- }
10
- .d3-boxplot .controls label {
11
- font-size: 12px;
12
- color: var(--muted-color);
13
- display: flex;
14
- align-items: center;
15
- gap: 8px;
16
- white-space: nowrap;
17
- padding: 6px 10px;
18
- }
19
- .d3-boxplot .controls select {
20
- font-size: 12px;
21
- padding: 8px 28px 8px 10px;
22
- border: 1px solid var(--border-color);
23
- border-radius: 8px;
24
- background-color: var(--surface-bg);
25
- color: var(--text-color);
26
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
27
- background-repeat: no-repeat;
28
- background-position: right 8px center;
29
- background-size: 12px;
30
- -webkit-appearance: none;
31
- -moz-appearance: none;
32
- appearance: none;
33
- cursor: pointer;
34
- transition: border-color .15s ease, box-shadow .15s ease;
35
- }
36
- [data-theme="dark"] .d3-boxplot .controls select {
37
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
38
- }
39
- .d3-boxplot .controls select:hover { border-color: var(--primary-color); }
40
- .d3-boxplot .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
41
- .d3-boxplot .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
42
- </style>
43
- <script>
44
- (() => {
45
- const ensureD3 = (cb) => {
46
- if (window.d3 && typeof window.d3.select === 'function') return cb();
47
- let s = document.getElementById('d3-cdn-script');
48
- 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); }
49
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
50
- s.addEventListener('load', onReady, { once: true });
51
- if (window.d3) onReady();
52
- };
53
-
54
- const bootstrap = () => {
55
- const scriptEl = document.currentScript;
56
- let container = scriptEl ? scriptEl.previousElementSibling : null;
57
- if (!(container && container.classList && container.classList.contains('d3-boxplot'))){
58
- const cs = Array.from(document.querySelectorAll('.d3-boxplot')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
59
- container = cs[cs.length-1] || null;
60
- }
61
- if (!container) return;
62
- if (container.dataset){ if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
63
-
64
- // Tooltip
65
- container.style.position = container.style.position || 'relative';
66
- let tip = container.querySelector('.d3-tooltip'); let tipInner;
67
- if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style,{ position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
68
-
69
- // Controls
70
- const controls = document.createElement('div'); controls.className='controls';
71
- const labelMetric = document.createElement('label'); labelMetric.textContent='Metric';
72
- const selMetric = document.createElement('select');
73
- labelMetric.appendChild(selMetric);
74
-
75
- // SVG
76
- const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
77
- const gRoot = svg.append('g');
78
- const gGrid = gRoot.append('g').attr('class','grid');
79
- const gAxes = gRoot.append('g').attr('class','axes');
80
- const gBoxes = gRoot.append('g').attr('class','boxes');
81
-
82
- // State & scales
83
- let width=800,height=360; const margin={top:16,right:28,bottom:56,left:64};
84
- const x=d3.scaleBand().padding(0.4);
85
- const y=d3.scaleLinear();
86
- const color=d3.scaleOrdinal().range(['var(--primary-color)','rgb(78, 165, 183)','rgb(227, 138, 66)','rgb(206, 192, 250)']);
87
-
88
- // Data (real): distribution of a chosen metric across runs
89
- let allRows = [];
90
- let groups = [];
91
- let currentMetric = null;
92
- let stats = [];
93
-
94
- async function fetchFirstAvailable(paths){
95
- for (const p of paths){
96
- try { const res = await fetch(p, { cache:'no-cache' }); if (res.ok) return await res.text(); } catch(e){}
97
- }
98
- throw new Error('Failed to load boxplot data');
99
- }
100
-
101
- function computeStatsForMetric(metric){
102
- const byRun = d3.rollup(allRows.filter(r=>r.metric===metric), v=> v.map(r=>+r.value).filter(Number.isFinite), r=>r.run);
103
- groups = Array.from(byRun.keys());
104
- const result = groups.map((g)=>{
105
- const data = (byRun.get(g) || []).slice().sort((a,b)=>a-b);
106
- if (!data.length) return { key:g, q1:NaN, med:NaN, q3:NaN, min:NaN, max:NaN, outliers:[] };
107
- const q1 = d3.quantile(data, 0.25);
108
- const med = d3.quantile(data, 0.5);
109
- const q3 = d3.quantile(data, 0.75);
110
- const iqr = q3 - q1;
111
- const lo = q1 - 1.5*iqr;
112
- const hi = q3 + 1.5*iqr;
113
- const min = d3.min(data.filter(v=>v>=lo));
114
- const max = d3.max(data.filter(v=>v<=hi));
115
- const outliers = data.filter(v=>v<lo || v>hi);
116
- return { key:g, q1, med, q3, iqr, min, max, outliers };
117
- });
118
- return result;
119
- }
120
-
121
- function updateScales(){
122
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
123
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
124
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
125
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
126
-
127
- width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
128
- const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
129
-
130
- x.domain(groups).range([0, innerWidth]);
131
- const yMin = Math.floor(d3.min(stats, d=>d.min ?? 0))-0; const yMax = Math.ceil(d3.max(stats, d=>d.max ?? 1))+0;
132
- y.domain([yMin, yMax]).range([innerHeight,0]).nice();
133
-
134
- // Grid
135
- gGrid.selectAll('*').remove();
136
- gGrid.selectAll('line.grid-y').data(y.ticks(6)).join('line')
137
- .attr('class','grid-y').attr('x1',0).attr('x2',innerWidth).attr('y1',d=>y(d)).attr('y2',d=>y(d))
138
- .attr('stroke', gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
139
-
140
- // Axes
141
- gAxes.selectAll('*').remove();
142
- gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
143
- gAxes.append('g').call(d3.axisLeft(y).ticks(6)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
144
- gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').style('font-size','12px').style('fill', tickColor).text('Group');
145
- gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-48},${innerHeight/2}) rotate(-90)`).style('font-size','12px').style('fill', tickColor).text('Value');
146
-
147
- return { innerWidth, innerHeight };
148
- }
149
-
150
- function draw(){
151
- const { innerWidth, innerHeight } = updateScales();
152
- const bw = Math.max(10, x.bandwidth());
153
-
154
- // Boxes
155
- const groupsSel = gBoxes.selectAll('g.bp').data(stats, d=>d.key);
156
- const gEnter = groupsSel.enter().append('g').attr('class','bp').attr('transform', d=>`translate(${x(d.key)},0)`);
157
- const merged = gEnter.merge(groupsSel).attr('transform', d=>`translate(${x(d.key)},0)`);
158
- groupsSel.exit().remove();
159
-
160
- // Box rect (Q1-Q3)
161
- const box = merged.selectAll('rect.box').data(d=>[d]);
162
- box.enter().append('rect').attr('class','box')
163
- .attr('x', 0).attr('width', bw)
164
- .attr('y', d=>y(d.q3)).attr('height', d=>Math.max(0.5, y(d.q1) - y(d.q3)))
165
- .attr('fill','var(--surface-bg)')
166
- .attr('stroke', 'var(--primary-color)')
167
- .attr('rx', 4).attr('ry', 4)
168
- .merge(box)
169
- .transition().duration(180)
170
- .attr('y', d=>y(d.q3)).attr('height', d=>Math.max(0.5, y(d.q1) - y(d.q3)));
171
- box.exit().remove();
172
-
173
- // Median line
174
- const med = merged.selectAll('line.med').data(d=>[d]);
175
- med.enter().append('line').attr('class','med')
176
- .attr('x1', 0).attr('x2', bw)
177
- .attr('y1', d=>y(d.med)).attr('y2', d=>y(d.med))
178
- .attr('stroke','var(--primary-color)').attr('stroke-width',2)
179
- .merge(med)
180
- .transition().duration(180)
181
- .attr('y1', d=>y(d.med)).attr('y2', d=>y(d.med));
182
- med.exit().remove();
183
-
184
- // Whiskers
185
- const whisker = merged.selectAll('line.whisk').data(d=>[d]);
186
- whisker.enter().append('line').attr('class','whisk')
187
- .attr('x1', bw/2).attr('x2', bw/2)
188
- .attr('y1', d=>y(d.min)).attr('y2', d=>y(d.max))
189
- .attr('stroke','var(--border-color)')
190
- .merge(whisker)
191
- .transition().duration(180)
192
- .attr('y1', d=>y(d.min)).attr('y2', d=>y(d.max));
193
- whisker.exit().remove();
194
-
195
- // Whisker caps
196
- const caps = merged.selectAll('g.caps').data(d=>[d]);
197
- const capsEnter = caps.enter().append('g').attr('class','caps');
198
- capsEnter.append('line').attr('class','cap cap--min').attr('x1',0).attr('x2',bw).attr('y1',d=>y(d.min)).attr('y2',d=>y(d.min)).attr('stroke','var(--border-color)');
199
- capsEnter.append('line').attr('class','cap cap--max').attr('x1',0).attr('x2',bw).attr('y1',d=>y(d.max)).attr('y2',d=>y(d.max)).attr('stroke','var(--border-color)');
200
- caps.merge(capsEnter).select('.cap--min').transition().duration(180).attr('y1',d=>y(d.min)).attr('y2',d=>y(d.min));
201
- caps.merge(capsEnter).select('.cap--max').transition().duration(180).attr('y1',d=>y(d.max)).attr('y2',d=>y(d.max));
202
- caps.exit().remove();
203
-
204
- // Outliers
205
- const outs = merged.selectAll('circle.out').data(d=>d.outliers.map(v=>({key:d.key, v})));
206
- outs.enter().append('circle').attr('class','out')
207
- .attr('cx', bw/2).attr('cy', d=>y(d.v)).attr('r', 2.5)
208
- .attr('fill', 'var(--primary-color)')
209
- .on('mouseenter', function(ev, d){ tipInner.innerHTML = `<div><strong>${d.key}</strong> outlier: ${d.v.toFixed(2)}</div>`; tip.style.opacity='1'; d3.select(this).attr('r',3.2); })
210
- .on('mousemove', function(ev){ const [mx,my]=d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
211
- .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('r',2.5); })
212
- .merge(outs)
213
- .transition().duration(180)
214
- .attr('cy', d=>y(d.v));
215
- outs.exit().remove();
216
- }
217
-
218
- container.appendChild(controls);
219
- controls.appendChild(labelMetric);
220
-
221
- (async () => {
222
- try {
223
- const csvText = await fetchFirstAvailable([
224
- '/data/all_ratings_luis.csv',
225
- '/data/against_baselines.csv',
226
- '/data/ss_vs_s1.csv',
227
- './assets/data/all_ratings_luis.csv',
228
- '../assets/data/all_ratings_luis.csv'
229
- ]);
230
- allRows = d3.csvParse(csvText, d=>({ run: d.run, step: +d.step, metric: d.metric, value: +d.value }));
231
- const metrics = Array.from(new Set(allRows.map(r=>r.metric))).filter(Boolean).slice(0, 20);
232
- metrics.forEach(m=>{ const o=document.createElement('option'); o.value=m; o.textContent=m; selMetric.appendChild(o); });
233
- currentMetric = metrics.includes('textvqa_val_exact_match') ? 'textvqa_val_exact_match' : metrics[0];
234
- selMetric.value = currentMetric;
235
- selMetric.addEventListener('change', (e)=>{ currentMetric = e.target.value; stats = computeStatsForMetric(currentMetric); draw(); });
236
- stats = computeStatsForMetric(currentMetric);
237
- draw();
238
- } catch (e) {
239
- const pre = document.createElement('pre'); pre.style.color = 'crimson'; pre.textContent = 'Failed to load boxplot data.'; container.appendChild(pre);
240
- }
241
- })();
242
-
243
- const rerender = () => { draw(); };
244
- if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
245
- };
246
-
247
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
248
- })();
249
- </script>
250
-
251
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-comparison.html DELETED
@@ -1,149 +0,0 @@
1
- <div class="image-comparison" style="width:100%;margin:10px 0;"></div>
2
- <style>
3
- .image-comparison { position: relative; }
4
- .image-comparison .controls { display:flex; align-items:center; gap:16px; justify-content:center; flex-wrap:wrap; margin:14px 0; }
5
- .image-comparison .controls label { font-size:14px; color: var(--text-color); display:flex; align-items:center; justify-content:center; gap:10px; font-weight:600; }
6
- .image-comparison .controls select {
7
- font-size: 14px;
8
- padding: 8px 32px 8px 12px;
9
- border: 1px solid var(--border-color);
10
- border-radius: 10px;
11
- background-color: var(--surface-bg);
12
- color: var(--text-color);
13
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
14
- background-repeat: no-repeat;
15
- background-position: right 8px center;
16
- background-size: 12px;
17
- -webkit-appearance: none; appearance: none; cursor: pointer;
18
- transition: border-color .15s ease, box-shadow .15s ease;
19
- box-shadow: 0 1px 2px rgba(0,0,0,.04);
20
- }
21
- [data-theme="dark"] .image-comparison .controls select {
22
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
23
- }
24
- .image-comparison .controls select:hover { border-color: var(--primary-color); }
25
- .image-comparison .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
26
-
27
- /* Responsive: empiler label et select proprement sur petits écrans */
28
- @media (max-width: 520px) {
29
- .image-comparison .controls label { flex-direction: column; gap: 6px; }
30
- }
31
-
32
- .image-comparison .grid { display:grid; grid-template-columns: repeat(4, 1fr); gap: 12px; width:100%; align-items: start; }
33
- /* Large → 4 colonnes; Medium → 2 colonnes; Mobile → 1 colonne */
34
- @media (max-width: 1100px) { .image-comparison .grid { grid-template-columns: repeat(2, 1fr); } }
35
- @media (max-width: 680px) { .image-comparison .grid { grid-template-columns: 1fr; } }
36
-
37
- .image-comparison .card { position: relative; border:1px solid var(--border-color); border-radius:10px; overflow:hidden; background: var(--surface-bg); display:flex; flex-direction:column; }
38
- .image-comparison .card .media { position: relative; width:100%; height: 200px; background: var(--surface-2, var(--surface-bg)); display:block; }
39
- .image-comparison .card .media img { width:100%; height:100%; object-fit: contain; display:block; }
40
- .image-comparison .badge { position:absolute; top:8px; left:8px; font-size:11px; padding:3px 6px; border-radius:6px; background: var(--surface-bg); color: var(--text-color); border:1px solid var(--border-color); }
41
- .image-comparison .meta { padding:8px 10px; border-top:1px solid var(--border-color); font-size:12px; display:flex; height: 55px; align-items:start; justify-content:space-between; gap:8px; }
42
- .image-comparison .meta .label { color: var(--muted-color); }
43
- .image-comparison .meta .value { font-weight:600; }
44
- </style>
45
- <script>
46
- (() => {
47
- const THIS_SCRIPT = document.currentScript;
48
- const bootstrap = () => {
49
- const scriptEl = THIS_SCRIPT;
50
- const host = scriptEl && scriptEl.parentElement;
51
- let container = null;
52
- if (host && host.querySelector) {
53
- container = host.querySelector('.image-comparison');
54
- }
55
- if (!container) {
56
- let sib = scriptEl && scriptEl.previousElementSibling;
57
- while (sib && !(sib.classList && sib.classList.contains('image-comparison'))) {
58
- sib = sib.previousElementSibling;
59
- }
60
- container = sib || document.querySelector('.image-comparison');
61
- }
62
- if (!container) return;
63
- if (container.dataset && container.dataset.mounted === 'true') return; if (container.dataset) container.dataset.mounted = 'true';
64
-
65
- // Known filenames in /public/data/comparison
66
- const FILES = {
67
- '1': { query: 'id_1_query.png', 1: 'id_1_rank_1_sim_1.000.png', 2: 'id_1_rank_2_sim_0.165.png', 3: 'id_1_rank_3_sim_0.143.png' },
68
- '2': { query: 'id_2_query.png', 1: 'id_2_rank_1_sim_1.000.png', 2: 'id_2_rank_2_sim_0.978.png', 3: 'id_2_rank_3_sim_0.975.png' },
69
- '3': { query: 'id_3_query.png', 1: 'id_3_rank_1_sim_0.936.png', 2: 'id_3_rank_2_sim_0.686.png', 3: 'id_3_rank_3_sim_0.676.png' },
70
- };
71
-
72
- // Images served from [domain]/public/data/comparison/*.png → path is /data/comparison/
73
- const CANDIDATE_BASES = [ '/data/comparison/' ];
74
-
75
- const resolveBase = (candidates, filename) => new Promise((resolve) => {
76
- let idx = 0; const tryNext = () => {
77
- if (idx >= candidates.length) return resolve(candidates[0]);
78
- const img = new Image();
79
- img.onload = () => resolve(candidates[idx]);
80
- img.onerror = () => { idx += 1; tryNext(); };
81
- img.src = candidates[idx] + filename;
82
- }; tryNext();
83
- });
84
-
85
- // Controls
86
- const controls = document.createElement('div'); controls.className = 'controls';
87
- const label = document.createElement('label'); label.textContent = 'Example';
88
- const select = document.createElement('select');
89
- const EXAMPLE_LABELS = { '1': 'Photo', '2': 'Chart', '3': 'Drawing' };
90
- ['1','2','3'].forEach((id)=>{ const o=document.createElement('option'); o.value=id; o.textContent=EXAMPLE_LABELS[id]; select.appendChild(o); });
91
- label.appendChild(select); controls.appendChild(label); container.appendChild(controls);
92
-
93
- // Grid
94
- const grid = document.createElement('div'); grid.className = 'grid'; container.appendChild(grid);
95
-
96
- let basePath = CANDIDATE_BASES[0];
97
-
98
- const parseInfo = (filename) => {
99
- const rankMatch = filename.match(/rank_(\d+)/i); const rank = rankMatch ? rankMatch[1] : '';
100
- const simMatch = filename.match(/sim_([0-9.]+)/i); const sim = simMatch ? simMatch[1] : '';
101
- return { rank, sim };
102
- };
103
-
104
- const formatSim = (val) => {
105
- if (val == null || val === '') return '—';
106
- return String(val).replace(/\.$/, '');
107
- };
108
-
109
- const render = (id) => {
110
- const files = FILES[id]; if (!files) return;
111
- const ordered = [files.query, files[1], files[2], files[3]]; // query, then matches 1, 2, 3
112
- grid.innerHTML = '';
113
- ordered.forEach((fname, idx) => {
114
- const { sim } = parseInfo(fname);
115
- const isQuery = idx === 0;
116
- const card = document.createElement('div'); card.className = 'card';
117
- const media = document.createElement('div'); media.className = 'media';
118
- const img = document.createElement('img'); img.alt = `example ${id} ${isQuery ? 'query' : `match ${idx}`}`; img.loading = 'lazy'; img.src = basePath + fname; media.appendChild(img);
119
- const meta = document.createElement('div'); meta.className = 'meta';
120
-
121
- if (isQuery) {
122
- const label = document.createElement('span'); label.className = 'value'; label.textContent = 'Query';
123
- meta.appendChild(label);
124
- } else {
125
- const content = document.createElement('span');
126
- content.innerHTML = `<span class="value">Match ${idx}</span><br><span class="label">Similarity: ${formatSim(sim)}</span>`;
127
- meta.appendChild(content);
128
- }
129
-
130
- card.appendChild(media); card.appendChild(meta); grid.appendChild(card);
131
- });
132
- };
133
-
134
- (async () => {
135
- // Resolve a working base then initial render
136
- basePath = await resolveBase(CANDIDATE_BASES, FILES['1'].query);
137
- render('1');
138
- })();
139
-
140
- select.addEventListener('change', () => render(select.value));
141
- };
142
-
143
- if (document.readyState === 'loading') {
144
- document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
145
- } else { bootstrap(); }
146
- })();
147
- </script>
148
-
149
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/d3-confusion-matrix.html ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="d3-confusion-matrix" ></div>
2
+ <style>
3
+ .d3-confusion-matrix {
4
+ position: relative;
5
+ }
6
+ .d3-confusion-matrix .panels {
7
+ display: flex;
8
+ flex-wrap: wrap;
9
+ gap: 16px;
10
+ margin-bottom: 4px;
11
+ }
12
+ .d3-confusion-matrix .panel {
13
+ flex: 1 1 320px;
14
+ min-width: 280px;
15
+ }
16
+ .d3-confusion-matrix .panel__title {
17
+ color: var(--text-color);
18
+ font-size: 12px;
19
+ line-height: 1.35;
20
+ margin: 0 0 6px 0;
21
+ font-weight: 600;
22
+ }
23
+ .d3-confusion-matrix .axis-label {
24
+ fill: var(--text-color);
25
+ font-size: 11px;
26
+ font-weight: 700;
27
+ }
28
+ .d3-confusion-matrix .cell-border {
29
+ stroke: var(--border-color);
30
+ stroke-width: 1px;
31
+ fill: none;
32
+ }
33
+ .d3-confusion-matrix .cell-text {
34
+ fill: var(--muted-color);
35
+ font-size: 11px;
36
+ pointer-events: none;
37
+ }
38
+ .d3-confusion-matrix .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
39
+ </style>
40
+ <script>
41
+ (() => {
42
+ // Load D3 from CDN once
43
+ const ensureD3 = (cb) => {
44
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
45
+ let s = document.getElementById('d3-cdn-script');
46
+ if (!s) {
47
+ s = document.createElement('script');
48
+ s.id = 'd3-cdn-script';
49
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
50
+ document.head.appendChild(s);
51
+ }
52
+ const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
53
+ s.addEventListener('load', onReady, { once: true });
54
+ if (window.d3) onReady();
55
+ };
56
+
57
+ const bootstrap = () => {
58
+ const scriptEl = document.currentScript;
59
+ let container = scriptEl ? scriptEl.previousElementSibling : null;
60
+ if (!(container && container.classList && container.classList.contains('d3-confusion-matrix'))){
61
+ const cs = Array.from(document.querySelectorAll('.d3-confusion-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
62
+ container = cs[cs.length - 1] || null;
63
+ }
64
+ if (!container) return;
65
+ if (container.dataset) {
66
+ if (container.dataset.mounted === 'true') return;
67
+ container.dataset.mounted = 'true';
68
+ }
69
+
70
+ // Tooltip (HTML, single instance inside container)
71
+ container.style.position = container.style.position || 'relative';
72
+ let tip = container.querySelector('.d3-tooltip');
73
+ let tipInner;
74
+ if (!tip) {
75
+ tip = document.createElement('div');
76
+ tip.className = 'd3-tooltip';
77
+ Object.assign(tip.style, {
78
+ position: 'absolute',
79
+ top: '0px',
80
+ left: '0px',
81
+ transform: 'translate(-9999px, -9999px)',
82
+ pointerEvents: 'none',
83
+ padding: '8px 10px',
84
+ borderRadius: '8px',
85
+ fontSize: '12px',
86
+ lineHeight: '1.35',
87
+ border: '1px solid var(--border-color)',
88
+ background: 'var(--surface-bg)',
89
+ color: 'var(--text-color)',
90
+ boxShadow: '0 4px 24px rgba(0,0,0,.18)',
91
+ opacity: '0',
92
+ transition: 'opacity .12s ease'
93
+ });
94
+ tipInner = document.createElement('div');
95
+ tipInner.className = 'd3-tooltip__inner';
96
+ tipInner.style.textAlign = 'left';
97
+ tip.appendChild(tipInner);
98
+ container.appendChild(tip);
99
+ } else {
100
+ tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
101
+ }
102
+
103
+ // Panels container (two side-by-side matrices)
104
+ const panels = document.createElement('div');
105
+ panels.className = 'panels';
106
+ const panelA = document.createElement('div');
107
+ panelA.className = 'panel';
108
+ const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';
109
+ panelA.appendChild(titleA);
110
+ const mountA = document.createElement('div'); panelA.appendChild(mountA);
111
+ const panelB = document.createElement('div');
112
+ panelB.className = 'panel';
113
+ const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved − Baseline, pp)';
114
+ panelB.appendChild(titleB);
115
+ const mountB = document.createElement('div'); panelB.appendChild(mountB);
116
+ panels.appendChild(panelA);
117
+ panels.appendChild(panelB);
118
+ container.appendChild(panels);
119
+
120
+ // SVG scaffolding
121
+ const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);
122
+ const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');
123
+ const gRootA = svgA.append('g');
124
+ const gCellsA = gRootA.append('g');
125
+ const gAxesA = gRootA.append('g');
126
+ const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);
127
+ const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');
128
+ const gRootB = svgB.append('g');
129
+ const gCellsB = gRootB.append('g');
130
+ const gAxesB = gRootB.append('g');
131
+
132
+ // Demo data (two distinct 10x10 confusion matrices: Baseline vs Improved)
133
+ // Rows: actual, Columns: predicted
134
+ const classes = ['0','1','2','3','4','5','6','7','8','9'];
135
+ const matrixA = [
136
+ [90, 2, 1, 0, 0, 0, 1, 0, 5, 1],
137
+ [ 3, 85, 5, 1, 0, 1, 2, 1, 1, 1],
138
+ [ 1, 6, 70, 10, 4, 4, 1, 1, 1, 2],
139
+ [ 0, 1, 8, 65, 10, 10, 2, 1, 1, 2],
140
+ [ 0, 0, 2, 6, 83, 3, 1, 1, 3, 1],
141
+ [ 0, 1, 2, 12, 4, 70, 5, 2, 2, 2],
142
+ [ 1, 2, 1, 0, 1, 2, 88, 1, 3, 1],
143
+ [ 0, 1, 1, 1, 1, 1, 2, 90, 1, 2],
144
+ [ 6, 2, 2, 4, 6, 3, 3, 2, 70, 2],
145
+ [ 1, 1, 1, 1, 2, 1, 1, 2, 1, 89]
146
+ ];
147
+ const matrixB = [
148
+ [94, 1, 0, 0, 0, 0, 1, 0, 3, 1],
149
+ [ 2, 90, 3, 1, 0, 0, 1, 1, 1, 1],
150
+ [ 1, 4, 78, 7, 3, 3, 1, 1, 1, 1],
151
+ [ 0, 1, 5, 74, 7, 8, 1, 1, 1, 2],
152
+ [ 0, 0, 1, 4, 88, 2, 1, 1, 2, 1],
153
+ [ 0, 1, 1, 9, 3, 78, 3, 1, 2, 2],
154
+ [ 1, 1, 1, 0, 1, 1, 91, 1, 2, 1],
155
+ [ 0, 1, 1, 1, 1, 1, 1, 92, 1, 1],
156
+ [ 4, 1, 1, 3, 4, 2, 2, 2, 79, 2],
157
+ [ 1, 1, 1, 1, 2, 1, 1, 1, 1, 90]
158
+ ];
159
+
160
+ // Colors: sequential palette via window.ColorPalettes with graceful fallback
161
+ const getSequentialColors = (count) => {
162
+ try {
163
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
164
+ return window.ColorPalettes.getColors('sequential', count);
165
+ }
166
+ } catch (_) {}
167
+ // Fallback: generate a monochrome scale using the primary color with varying opacity
168
+ const arr = [];
169
+ for (let i = 0; i < count; i++) arr.push('var(--primary-color)');
170
+ return arr;
171
+ };
172
+
173
+ const palette = getSequentialColors(13);
174
+ const getDivergingColors = (count) => {
175
+ try {
176
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
177
+ return window.ColorPalettes.getColors('diverging', count);
178
+ }
179
+ } catch (_) {}
180
+ const steps = Math.max(3, count|0);
181
+ const arr = [];
182
+ for (let i = 0; i < steps; i++) {
183
+ const t = i / (steps - 1);
184
+ const pct = Math.round(t * 100);
185
+ arr.push(`color-mix(in srgb, #D64545 ${100-pct}%, #3A7BD5 ${pct}%)`);
186
+ }
187
+ return arr;
188
+ };
189
+
190
+ let width = 800;
191
+ let height = 480;
192
+ const margin = { top: 36, right: 24, bottom: 26, left: 56 };
193
+
194
+ function updateSize() {
195
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
196
+ width = container.clientWidth || 800;
197
+ const gap = 16; // matches CSS .panels gap
198
+ const minPanel = 320;
199
+ const nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;
200
+ const panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);
201
+ const base = Math.max(minPanel, Math.round(panelWidth * 0.92));
202
+ height = base;
203
+ // Responsive SVG: width 100%, height auto, preserve aspect via viewBox
204
+ svgA
205
+ .attr('viewBox', `0 0 ${panelWidth} ${height}`)
206
+ .attr('preserveAspectRatio', 'xMidYMid meet')
207
+ .style('width', '100%')
208
+ .style('height', 'auto');
209
+ svgB
210
+ .attr('viewBox', `0 0 ${panelWidth} ${height}`)
211
+ .attr('preserveAspectRatio', 'xMidYMid meet')
212
+ .style('width', '100%')
213
+ .style('height', 'auto');
214
+ gRootA.attr('transform', `translate(${margin.left},${margin.top})`);
215
+ gRootB.attr('transform', `translate(${margin.left},${margin.top})`);
216
+ const innerWidth = panelWidth - margin.left - margin.right;
217
+ const innerHeight = height - margin.top - margin.bottom;
218
+ return { innerWidth, innerHeight, isDark };
219
+ }
220
+
221
+ function computeValues(normalization, matrix) {
222
+ const n = classes.length;
223
+ const totalsByRow = matrix.map(row => row.reduce((a, b) => a + b, 0));
224
+ const flat = [];
225
+ let minV = Infinity, maxV = -Infinity;
226
+ for (let r = 0; r < n; r++) {
227
+ for (let c = 0; c < n; c++) {
228
+ const count = matrix[r][c];
229
+ const value = normalization === 'row' ? (totalsByRow[r] ? count / totalsByRow[r] : 0) : count;
230
+ if (value < minV) minV = value;
231
+ if (value > maxV) maxV = value;
232
+ flat.push({ r, c, count, value });
233
+ }
234
+ }
235
+ return { data: flat, minV, maxV };
236
+ }
237
+
238
+ function getColorScale(values, minV, maxV) {
239
+ // If ColorPalettes is available, use quantiles to enhance visual variation across the distribution
240
+ const hasPalette = !(palette.length === 0);
241
+ if (hasPalette && (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')) {
242
+ const scale = d3.scaleQuantile().domain(values).range(palette);
243
+ return (v) => scale(v);
244
+ }
245
+ // Fallback: primary color with opacity mapped to normalized value
246
+ const norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);
247
+ return (v) => `color-mix(in oklab, var(--primary-color) ${Math.round(norm(v) * 100)}%, var(--surface-bg))`;
248
+ }
249
+
250
+ // (no local contrast function — use ColorPalettes.getTextStyleForBackground)
251
+
252
+ // Compute a fixed readable text color from a CSS rgb()/rgba() string
253
+ function chooseFixedReadableTextOnBg(bgCss){
254
+ try {
255
+ const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/);
256
+ if (!m) return '#0e1116';
257
+ const parts = m[1].split(',').map(s => parseFloat(s.trim()));
258
+ const [r, g, b] = parts;
259
+ // sRGB → relative luminance
260
+ const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);
261
+ const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4)));
262
+ const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2];
263
+ // Threshold ~ 0.5 for readability; darker BG → white text, else near-black
264
+ return L < 0.5 ? '#ffffff' : '#0e1116';
265
+ } catch(_) { return '#0e1116'; }
266
+ }
267
+
268
+ function render() {
269
+ const { innerWidth, innerHeight } = updateSize();
270
+ const n = classes.length;
271
+ const gridSize = Math.min(innerWidth, innerHeight);
272
+ const cellSize = gridSize / n;
273
+
274
+ const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
275
+ const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
276
+
277
+ // Panel A: Baseline (row-normalized)
278
+ const dataA = computeValues('row', matrixA);
279
+ const colorA = getColorScale(dataA.data.map(d => d.value), dataA.minV, dataA.maxV);
280
+
281
+ gCellsA.selectAll('rect.cell-bg')
282
+ .data([0])
283
+ .join('rect')
284
+ .attr('class', 'cell-bg')
285
+ .attr('x', 0)
286
+ .attr('y', 0)
287
+ .attr('width', gridSize)
288
+ .attr('height', gridSize)
289
+ .attr('fill', 'none')
290
+ .attr('stroke', 'var(--border-color)')
291
+ .attr('stroke-width', 1);
292
+
293
+ const cellsA = gCellsA.selectAll('g.cell')
294
+ .data(dataA.data, d => `${d.r}-${d.c}-A`);
295
+
296
+ const cellsEnterA = cellsA.enter()
297
+ .append('g')
298
+ .attr('class', 'cell');
299
+
300
+ cellsEnterA.append('rect')
301
+ .attr('rx', 2)
302
+ .attr('ry', 2)
303
+ .on('mousemove', (event, d) => {
304
+ const [px, py] = d3.pointer(event, container);
305
+ tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong><br/>${(d.value * 100).toFixed(1)}% (${d.count})`;
306
+ tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
307
+ tip.style.opacity = '1';
308
+ })
309
+ .on('mouseleave', () => {
310
+ tip.style.opacity = '0';
311
+ });
312
+
313
+ cellsEnterA.append('text')
314
+ .attr('class', 'cell-text')
315
+ .attr('text-anchor', 'middle')
316
+ .attr('dominant-baseline', 'middle');
317
+
318
+ const cellsMergedA = cellsEnterA.merge(cellsA);
319
+
320
+ cellsMergedA.select('rect')
321
+ .attr('x', d => x(d.c))
322
+ .attr('y', d => y(d.r))
323
+ .attr('width', Math.max(1, x.bandwidth()))
324
+ .attr('height', Math.max(1, y.bandwidth()))
325
+ .attr('fill', d => colorA(d.value));
326
+
327
+ cellsMergedA.select('text')
328
+ .attr('x', d => x(d.c) + x.bandwidth() / 2)
329
+ .attr('y', d => y(d.r) + y.bandwidth() / 2)
330
+ .text(d => `${Math.round(d.value * 100)}`)
331
+ .style('fill', function(d){
332
+ try {
333
+ const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
334
+ const bg = rect ? getComputedStyle(rect).fill : colorA(d.value);
335
+ return chooseFixedReadableTextOnBg(bg);
336
+ } catch (_) {
337
+ return '#0e1116';
338
+ }
339
+ });
340
+
341
+ cellsA.exit().remove();
342
+
343
+ gAxesA.selectAll('*').remove();
344
+
345
+ gAxesA.append('g')
346
+ .selectAll('text')
347
+ .data(classes)
348
+ .join('text')
349
+ .attr('class', 'axis-label')
350
+ .attr('text-anchor', 'middle')
351
+ .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
352
+ .attr('y', -8)
353
+ .text(d => d);
354
+
355
+ gAxesA.append('g')
356
+ .selectAll('text')
357
+ .data(classes)
358
+ .join('text')
359
+ .attr('class', 'axis-label')
360
+ .attr('text-anchor', 'end')
361
+ .attr('x', -8)
362
+ .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
363
+ .attr('dominant-baseline', 'middle')
364
+ .text(d => d);
365
+
366
+ gAxesA.append('text')
367
+ .attr('class', 'axis-label')
368
+ .attr('text-anchor', 'middle')
369
+ .attr('x', gridSize / 2)
370
+ .attr('y', innerHeight + 20)
371
+ .text('Predicted');
372
+
373
+ gAxesA.append('text')
374
+ .attr('class', 'axis-label')
375
+ .attr('text-anchor', 'middle')
376
+ .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
377
+ .text('Actual');
378
+
379
+ // Panel B: Delta (Improved − Baseline), row-normalized differences in percentage points
380
+ const dataB = computeValues('row', matrixB);
381
+ const diverging = getDivergingColors(13);
382
+ // Build delta values aligned to A's ordering
383
+ const mapA = new Map(dataA.data.map(d => [d.r + '-' + d.c, d.value]));
384
+ const delta = dataB.data.map(d => ({ r: d.r, c: d.c, count: d.count, value: (d.value - (mapA.get(d.r + '-' + d.c) || 0)) }));
385
+ // Symmetric domain around 0 (in proportions), express later as pp in labels
386
+ const maxAbsDelta = Math.max(0.01, d3.max(delta, d => Math.abs(d.value)) || 0.01);
387
+ const colorB = d3.scaleQuantize().domain([-maxAbsDelta, maxAbsDelta]).range(diverging);
388
+
389
+ gCellsB.selectAll('rect.cell-bg')
390
+ .data([0])
391
+ .join('rect')
392
+ .attr('class', 'cell-bg')
393
+ .attr('x', 0)
394
+ .attr('y', 0)
395
+ .attr('width', gridSize)
396
+ .attr('height', gridSize)
397
+ .attr('fill', 'none')
398
+ .attr('stroke', 'var(--border-color)')
399
+ .attr('stroke-width', 1);
400
+
401
+ const cellsB = gCellsB.selectAll('g.cell')
402
+ .data(dataB.data, d => `${d.r}-${d.c}-B`);
403
+
404
+ const cellsEnterB = cellsB.enter()
405
+ .append('g')
406
+ .attr('class', 'cell');
407
+
408
+ cellsEnterB.append('rect')
409
+ .attr('rx', 2)
410
+ .attr('ry', 2)
411
+ .on('mousemove', (event, d) => {
412
+ const [px, py] = d3.pointer(event, container);
413
+ const a = dataA.data.find(x => x.r===d.r && x.c===d.c);
414
+ const b = dataB.data.find(x => x.r===d.r && x.c===d.c);
415
+ const dv = ((b ? b.value : 0) - (a ? a.value : 0)) * 100;
416
+ tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong>` +
417
+ `<br/>baseline ${(a ? a.value*100 : 0).toFixed(1)}%` +
418
+ `<br/>improved ${(b ? b.value*100 : 0).toFixed(1)}%` +
419
+ `<br/>delta ${dv.toFixed(1)} pp`;
420
+ tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
421
+ tip.style.opacity = '1';
422
+ })
423
+ .on('mouseleave', () => {
424
+ tip.style.opacity = '0';
425
+ });
426
+
427
+ cellsEnterB.append('text')
428
+ .attr('class', 'cell-text')
429
+ .attr('text-anchor', 'middle')
430
+ .attr('dominant-baseline', 'middle');
431
+
432
+ const cellsMergedB = cellsEnterB.merge(cellsB);
433
+
434
+ cellsMergedB.select('rect')
435
+ .attr('x', d => x(d.c))
436
+ .attr('y', d => y(d.r))
437
+ .attr('width', Math.max(1, x.bandwidth()))
438
+ .attr('height', Math.max(1, y.bandwidth()))
439
+ .attr('fill', d => colorB(delta.find(x => x.r===d.r && x.c===d.c).value));
440
+
441
+ cellsMergedB.select('text')
442
+ .attr('x', d => x(d.c) + x.bandwidth() / 2)
443
+ .attr('y', d => y(d.r) + y.bandwidth() / 2)
444
+ .text(d => {
445
+ const dv = delta.find(x => x.r===d.r && x.c===d.c).value; return `${Math.round(dv * 100)}`;
446
+ })
447
+ .style('fill', function(d){
448
+ try {
449
+ const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
450
+ const dv = delta.find(x => x.r===d.r && x.c===d.c).value;
451
+ const bg = rect ? getComputedStyle(rect).fill : colorB(dv);
452
+ return chooseFixedReadableTextOnBg(bg);
453
+ } catch (_) {
454
+ return '#0e1116';
455
+ }
456
+ });
457
+
458
+ cellsB.exit().remove();
459
+
460
+ gAxesB.selectAll('*').remove();
461
+
462
+ gAxesB.append('g')
463
+ .selectAll('text')
464
+ .data(classes)
465
+ .join('text')
466
+ .attr('class', 'axis-label')
467
+ .attr('text-anchor', 'middle')
468
+ .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
469
+ .attr('y', -8)
470
+ .text(d => d);
471
+
472
+ gAxesB.append('g')
473
+ .selectAll('text')
474
+ .data(classes)
475
+ .join('text')
476
+ .attr('class', 'axis-label')
477
+ .attr('text-anchor', 'end')
478
+ .attr('x', -8)
479
+ .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
480
+ .attr('dominant-baseline', 'middle')
481
+ .text(d => d);
482
+
483
+ gAxesB.append('text')
484
+ .attr('class', 'axis-label')
485
+ .attr('text-anchor', 'middle')
486
+ .attr('x', gridSize / 2)
487
+ .attr('y', innerHeight + 20)
488
+ .text('Predicted');
489
+
490
+ gAxesB.append('text')
491
+ .attr('class', 'axis-label')
492
+ .attr('text-anchor', 'middle')
493
+ .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
494
+ .text('Actual');
495
+ }
496
+
497
+ // Initial render + resize handling
498
+ render();
499
+ const rerender = () => render();
500
+ if (window.ResizeObserver) {
501
+ const ro = new ResizeObserver(() => rerender());
502
+ ro.observe(container);
503
+ } else {
504
+ window.addEventListener('resize', rerender);
505
+ }
506
+ };
507
+
508
+ if (document.readyState === 'loading') {
509
+ document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
510
+ } else {
511
+ ensureD3(bootstrap);
512
+ }
513
+ })();
514
+ </script>
515
+
516
+
app/src/content/embeds/d3-line-example.html DELETED
@@ -1,456 +0,0 @@
1
- <div class="d3-line-example" style="width:100%;margin:10px 0;"></div>
2
- <style>
3
- .d3-line-example .d3-line__controls select {
4
- font-size: 12px;
5
- padding: 8px 28px 8px 10px;
6
- border: 1px solid var(--border-color);
7
- border-radius: 8px;
8
- background-color: var(--surface-bg);
9
- color: var(--text-color);
10
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
11
- background-repeat: no-repeat;
12
- background-position: right 8px center;
13
- background-size: 12px;
14
- -webkit-appearance: none;
15
- -moz-appearance: none;
16
- appearance: none;
17
- cursor: pointer;
18
- transition: border-color .15s ease, box-shadow .15s ease;
19
- }
20
- [data-theme="dark"] .d3-line-example .d3-line__controls select {
21
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
22
- }
23
- .d3-line-example .d3-line__controls select:hover {
24
- border-color: var(--primary-color);
25
- }
26
- .d3-line-example .d3-line__controls select:focus {
27
- border-color: var(--primary-color);
28
- box-shadow: 0 0 0 3px rgba(232,137,171,.25);
29
- outline: none;
30
- }
31
- .d3-line-example .d3-line__controls label { gap: 8px; }
32
-
33
- /* Range slider themed with --primary-color */
34
- .d3-line-example .d3-line__controls input[type="range"] {
35
- -webkit-appearance: none;
36
- appearance: none;
37
- width: 100%;
38
- height: 6px;
39
- border-radius: 999px;
40
- background: var(--border-color);
41
- outline: none;
42
- }
43
- .d3-line-example .d3-line__controls input[type="range"]::-webkit-slider-runnable-track {
44
- height: 6px;
45
- background: transparent;
46
- border-radius: 999px;
47
- }
48
- .d3-line-example .d3-line__controls input[type="range"]::-webkit-slider-thumb {
49
- -webkit-appearance: none;
50
- appearance: none;
51
- width: 16px;
52
- height: 16px;
53
- border-radius: 50%;
54
- background: var(--primary-color);
55
- border: 2px solid var(--on-primary);
56
- margin-top: -5px;
57
- cursor: pointer;
58
- }
59
- .d3-line-example .d3-line__controls input[type="range"]::-moz-range-track {
60
- height: 6px;
61
- background: transparent;
62
- border-radius: 999px;
63
- }
64
- .d3-line-example .d3-line__controls input[type="range"]::-moz-range-thumb {
65
- width: 16px;
66
- height: 16px;
67
- border-radius: 50%;
68
- background: var(--primary-color);
69
- border: 2px solid var(--on-primary);
70
- cursor: pointer;
71
- }
72
- /* Improved line color via CSS */
73
- .d3-line-example .lines path.improved { stroke: var(--primary-color); }
74
-
75
- /* Responsive: stack controls on small screens */
76
- @media (max-width: 640px) {
77
- .d3-line-example .d3-line__controls {
78
- flex-wrap: wrap;
79
- }
80
- .d3-line-example .d3-line__controls label {
81
- flex: 1 1 100% !important;
82
- width: 100%;
83
- }
84
- }
85
- </style>
86
- <script>
87
- (() => {
88
- const ensureD3 = (cb) => {
89
- if (window.d3 && typeof window.d3.select === 'function') return cb();
90
- let s = document.getElementById('d3-cdn-script');
91
- if (!s) {
92
- s = document.createElement('script');
93
- s.id = 'd3-cdn-script';
94
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
95
- document.head.appendChild(s);
96
- }
97
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
98
- s.addEventListener('load', onReady, { once: true });
99
- if (window.d3) onReady();
100
- };
101
-
102
- const bootstrap = () => {
103
- const scriptEl = document.currentScript;
104
-
105
- const getLocalPrev = () => {
106
- if (!scriptEl) return null;
107
- let el = scriptEl.previousElementSibling;
108
- while (el && !(el.classList && el.classList.contains('d3-line-example'))) {
109
- el = el.previousElementSibling;
110
- }
111
- return el || null;
112
- };
113
-
114
- const localTarget = getLocalPrev();
115
- const targets = localTarget
116
- ? [localTarget]
117
- : Array.from(document.querySelectorAll('.d3-line-example'))
118
- .filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
119
-
120
- targets.forEach((container) => {
121
- if (!container) return;
122
- if (container.dataset) {
123
- if (container.dataset.mounted === 'true') return;
124
- container.dataset.mounted = 'true';
125
- }
126
-
127
- // Dataset params matching the Plotly version
128
- const datasets = [
129
- { 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 },
130
- { 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 },
131
- { 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 },
132
- ];
133
-
134
- // Controls UI
135
- const controls = document.createElement('div');
136
- controls.className = 'd3-line__controls';
137
- Object.assign(controls.style, {
138
- marginTop: '12px',
139
- display: 'flex',
140
- gap: '16px',
141
- alignItems: 'center'
142
- });
143
-
144
- const labelDs = document.createElement('label');
145
- Object.assign(labelDs.style, {
146
- fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px'
147
- });
148
- labelDs.textContent = 'Dataset';
149
- const selectDs = document.createElement('select');
150
- Object.assign(selectDs.style, { fontSize: '12px' });
151
- datasets.forEach((d, i) => {
152
- const o = document.createElement('option');
153
- o.value = String(i);
154
- o.textContent = d.name;
155
- selectDs.appendChild(o);
156
- });
157
- labelDs.appendChild(selectDs);
158
-
159
- const labelAlpha = document.createElement('label');
160
- Object.assign(labelAlpha.style, {
161
- fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '10px', flex: '1', padding: '6px 10px'
162
- });
163
- labelAlpha.appendChild(document.createTextNode('Augmentation α'));
164
- const slider = document.createElement('input');
165
- slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.01'; slider.value = '0.70';
166
- Object.assign(slider.style, { flex: '1' });
167
- const alphaVal = document.createElement('span'); alphaVal.className = 'alpha-value'; alphaVal.textContent = slider.value;
168
- labelAlpha.appendChild(slider);
169
- labelAlpha.appendChild(alphaVal);
170
-
171
- controls.appendChild(labelDs);
172
- controls.appendChild(labelAlpha);
173
-
174
- // Create SVG
175
- const svg = d3.select(container).append('svg')
176
- .attr('width', '100%')
177
- .style('display', 'block');
178
-
179
- // Groups
180
- const gRoot = svg.append('g');
181
- const gGrid = gRoot.append('g').attr('class', 'grid');
182
- const gAxes = gRoot.append('g').attr('class', 'axes');
183
- const gLines = gRoot.append('g').attr('class', 'lines');
184
- const gHover = gRoot.append('g').attr('class', 'hover');
185
- const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
186
-
187
- // Tooltip
188
- container.style.position = container.style.position || 'relative';
189
- let tip = container.querySelector('.d3-tooltip');
190
- let tipInner;
191
- if (!tip) {
192
- tip = document.createElement('div');
193
- tip.className = 'd3-tooltip';
194
- Object.assign(tip.style, {
195
- position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
196
- padding: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
197
- background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0',
198
- transition: 'opacity .12s ease'
199
- });
200
- tipInner = document.createElement('div');
201
- tipInner.className = 'd3-tooltip__inner';
202
- tipInner.style.textAlign = 'left';
203
- tip.appendChild(tipInner);
204
- container.appendChild(tip);
205
- } else {
206
- tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
207
- }
208
-
209
- // Colors
210
- const colorBase = '#64748b'; // slate-500
211
- const colorImproved = 'var(--primary-color)';
212
- const colorTarget = '#4b5563'; // gray-600
213
- const legendBgLight = 'rgba(255,255,255,0.85)';
214
- const legendBgDark = 'rgba(17,17,23,0.85)';
215
-
216
- // Data and helpers
217
- const N = 240;
218
- const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
219
- const logistic = (x, { ymin, ymax, k, x0 }) => ymin + (ymax - ymin) / (1 + Math.exp(-k * (x - x0)));
220
- const blend = (l, e, a) => (1 - a) * l + a * e;
221
-
222
- let datasetIndex = 0;
223
- let alpha = parseFloat(slider.value) || 0.7;
224
-
225
- let yBase = [];
226
- let yAug = [];
227
- let yImp = [];
228
- let yTgt = [];
229
-
230
- function computeCurves() {
231
- const d = datasets[datasetIndex];
232
- yBase = xs.map((x) => logistic(x, d.base));
233
- yAug = xs.map((x) => logistic(x, d.aug));
234
- yTgt = xs.map(() => d.target);
235
- yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
236
- }
237
-
238
- // Scales and layout
239
- let width = 800, height = 360;
240
- let margin = { top: 16, right: 28, bottom: 56, left: 64 };
241
- let xScale = d3.scaleLinear();
242
- let yScale = d3.scaleLinear();
243
-
244
- // Paths
245
- const lineGen = d3.line()
246
- .curve(d3.curveCatmullRom.alpha(0.6))
247
- .x((d, i) => xScale(xs[i]))
248
- .y((d) => yScale(d));
249
-
250
- const pathBase = gLines.append('path').attr('fill', 'none').attr('stroke', colorBase).attr('stroke-width', 2);
251
- const pathImp = gLines.append('path').attr('class', 'improved').attr('fill', 'none').style('stroke', 'var(--primary-color)').attr('stroke-width', 2);
252
- const pathTgt = gLines.append('path').attr('fill', 'none').attr('stroke', colorTarget).attr('stroke-width', 2).attr('stroke-dasharray', '6,6');
253
-
254
- // Hover elements
255
- const hoverLine = gHover.append('line').attr('stroke-width', 1);
256
- const hoverDotB = gHover.append('circle').attr('r', 3.5).attr('fill', colorBase).attr('stroke', '#fff').attr('stroke-width', 1);
257
- const hoverDotI = gHover.append('circle').attr('class', 'improved').attr('r', 3.5).style('fill', 'var(--primary-color)').attr('stroke', '#fff').attr('stroke-width', 1);
258
- const hoverDotT = gHover.append('circle').attr('r', 3.5).attr('fill', colorTarget).attr('stroke', '#fff').attr('stroke-width', 1);
259
-
260
- const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
261
-
262
- function updateScales() {
263
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
264
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
265
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
266
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
267
-
268
- width = container.clientWidth || 800;
269
- height = Math.max(260, Math.round(width / 3));
270
- svg.attr('width', width).attr('height', height);
271
-
272
- const innerWidth = width - margin.left - margin.right;
273
- const innerHeight = height - margin.top - margin.bottom;
274
- gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
275
-
276
- xScale.domain([0, 1]).range([0, innerWidth]);
277
- yScale.domain([0, 1]).range([innerHeight, 0]);
278
-
279
- // Grid (horizontal)
280
- gGrid.selectAll('*').remove();
281
- const yTicks = yScale.ticks(6);
282
- gGrid.selectAll('line')
283
- .data(yTicks)
284
- .join('line')
285
- .attr('x1', 0)
286
- .attr('x2', innerWidth)
287
- .attr('y1', (d) => yScale(d))
288
- .attr('y2', (d) => yScale(d))
289
- .attr('stroke', gridColor)
290
- .attr('stroke-width', 1)
291
- .attr('shape-rendering', 'crispEdges');
292
-
293
- // Axes
294
- gAxes.selectAll('*').remove();
295
- const xAxis = d3.axisBottom(xScale).ticks(8).tickSizeOuter(0);
296
- const yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0).tickFormat(d3.format('.2f'));
297
- gAxes.append('g')
298
- .attr('transform', `translate(0,${innerHeight})`)
299
- .call(xAxis)
300
- .call((g) => {
301
- g.selectAll('path, line').attr('stroke', axisColor);
302
- g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
303
- });
304
- gAxes.append('g')
305
- .call(yAxis)
306
- .call((g) => {
307
- g.selectAll('path, line').attr('stroke', axisColor);
308
- g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
309
- });
310
-
311
- // Axis labels (X and Y)
312
- gAxes.append('text')
313
- .attr('class', 'axis-label axis-label--x')
314
- .attr('x', innerWidth / 2)
315
- .attr('y', innerHeight + 44)
316
- .attr('text-anchor', 'middle')
317
- .style('font-size', '12px')
318
- .style('fill', tickColor)
319
- .text('Epoch');
320
- gAxes.append('text')
321
- .attr('class', 'axis-label axis-label--y')
322
- .attr('text-anchor', 'middle')
323
- .attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`)
324
- .style('font-size', '12px')
325
- .style('fill', tickColor)
326
- .text('Accuracy');
327
-
328
- overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
329
- hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
330
-
331
- // Legend inside plot (bottom-right), no background/border/shadow
332
- const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
333
- const legendHeight = 64;
334
- gLegend
335
- .attr('x', innerWidth - legendWidth + 42)
336
- .attr('y', innerHeight - legendHeight - 12)
337
- .attr('width', legendWidth)
338
- .attr('height', legendHeight);
339
- const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
340
- Object.assign(legendRoot.node().style, {
341
- background: 'transparent',
342
- border: 'none',
343
- borderRadius: '0',
344
- padding: '0',
345
- fontSize: '12px',
346
- lineHeight: '1.35',
347
- color: 'var(--text-color)'
348
- });
349
- legendRoot.html(`
350
- <div style="display:flex;flex-direction:column;gap:6px;">
351
- <div style="display:flex;align-items:center;gap:8px;">
352
- <span style="width:18px;height:3px;background:${colorBase};border-radius:2px;display:inline-block"></span>
353
- <span>Baseline</span>
354
- </div>
355
- <div style="display:flex;align-items:center;gap:8px;">
356
- <span style="width:18px;height:3px;background:${colorImproved};border-radius:2px;display:inline-block"></span>
357
- <span>Improved</span>
358
- </div>
359
- <div style="display:flex;align-items:center;gap:8px;">
360
- <span style="width:18px;height:0;border-top:2px dashed ${colorTarget};display:inline-block"></span>
361
- <span>Target</span>
362
- </div>
363
- </div>
364
- `);
365
- }
366
-
367
- function updatePaths() {
368
- pathBase.transition().duration(200).attr('d', lineGen(yBase));
369
- pathImp.transition().duration(200).attr('d', lineGen(yImp));
370
- pathTgt.transition().duration(200).attr('d', lineGen(yTgt));
371
- }
372
-
373
- function updateAlpha(a) {
374
- alpha = a;
375
- alphaVal.textContent = a.toFixed(2);
376
- yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
377
- pathImp.transition().duration(80).attr('d', lineGen(yImp));
378
- }
379
-
380
- function applyDataset() {
381
- computeCurves();
382
- updatePaths();
383
- }
384
-
385
- // Hover interactions
386
- function onMove(event) {
387
- const [mx, my] = d3.pointer(event, overlay.node());
388
- const xi = Math.max(0, Math.min(N - 1, Math.round(xScale.invert(mx) * (N - 1))));
389
- const xpx = xScale(xs[xi]);
390
- const yb = yBase[xi], yi = yImp[xi], yt = yTgt[xi];
391
- hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
392
- hoverDotB.attr('cx', xpx).attr('cy', yScale(yb)).style('display', null);
393
- hoverDotI.attr('cx', xpx).attr('cy', yScale(yi)).style('display', null);
394
- hoverDotT.attr('cx', xpx).attr('cy', yScale(yt)).style('display', null);
395
-
396
- // Tooltip content
397
- const ds = datasets[datasetIndex].name;
398
- tipInner.innerHTML = `<div><strong>${ds}</strong></div>` +
399
- `<div><strong>x</strong> ${xs[xi].toFixed(2)}</div>` +
400
- `<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>` +
401
- `<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>` +
402
- `<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>`;
403
- const offsetX = 12, offsetY = 12;
404
- tip.style.opacity = '1';
405
- tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
406
- }
407
-
408
- function onLeave() {
409
- tip.style.opacity = '0';
410
- tip.style.transform = 'translate(-9999px, -9999px)';
411
- hoverLine.style('display', 'none');
412
- hoverDotB.style('display', 'none');
413
- hoverDotI.style('display', 'none');
414
- hoverDotT.style('display', 'none');
415
- }
416
-
417
- overlay.on('mousemove', onMove).on('mouseleave', onLeave);
418
-
419
- // Init + controls wiring
420
- computeCurves();
421
- updateScales();
422
- updatePaths();
423
-
424
- // Attach controls after SVG for consistency with Plotly fragment
425
- container.appendChild(controls);
426
-
427
- selectDs.addEventListener('change', (e) => {
428
- datasetIndex = parseInt(e.target.value) || 0;
429
- applyDataset();
430
- });
431
- slider.addEventListener('input', (e) => {
432
- const a = parseFloat(e.target.value) || 0;
433
- updateAlpha(a);
434
- });
435
-
436
- // Resize handling
437
- const render = () => {
438
- updateScales();
439
- updatePaths();
440
- };
441
- if (window.ResizeObserver) {
442
- const ro = new ResizeObserver(() => render());
443
- ro.observe(container);
444
- } else {
445
- window.addEventListener('resize', render);
446
- }
447
- render();
448
- });
449
- };
450
-
451
- if (document.readyState === 'loading') {
452
- document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
453
- } else { ensureD3(bootstrap); }
454
- })();
455
- </script>
456
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/{filters-quad.html → d3-line-quad.html} RENAMED
@@ -1,43 +1,33 @@
1
- <div class="filters-quad" style="width:100%;margin:10px 0;">
2
- <div class="filters-quad__controls" aria-label="Global chart controls"></div>
3
- <div class="filters-quad__legend" aria-label="Global legend"></div>
4
- <div class="filters-quad__grid">
5
  <div class="quad-cell" data-title="Formatting Filter" data-csv="/data/formatting_filters.csv"></div>
6
  <div class="quad-cell" data-title="Relevance Filter" data-csv="/data/relevance_filters.csv"></div>
7
  <div class="quad-cell" data-title="Visual Dependency Filter" data-csv="/data/visual_dependency_filters.csv"></div>
8
  <div class="quad-cell" data-title="Image Correspondence Filter" data-csv="/data/image_correspondence_filters.csv"></div>
9
  </div>
10
  <noscript>JavaScript is required to render these charts.</noscript>
 
11
  </div>
12
  <style>
13
- .filters-quad { position: relative; }
14
- .filters-quad__grid { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
15
- @media (max-width: 980px) { .filters-quad__grid { grid-template-columns: 1fr; } }
16
-
17
- .filters-quad__controls { display:flex; align-items:center; justify-content:center; gap:12px; margin: 6px 0 12px 0; flex-wrap:wrap; }
18
- .filters-quad__controls label { font-size:14px; color: var(--text-color); font-weight:600; display:flex; align-items:center; gap:8px; }
19
- .filters-quad__controls select {
20
- font-size: 14px; padding: 8px 32px 8px 12px; border: 1px solid var(--border-color); border-radius: 10px;
21
- background-color: var(--surface-bg); color: var(--text-color);
22
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
23
- background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;
24
- }
25
- [data-theme="dark"] .filters-quad__controls select {
26
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
27
- }
28
- .filters-quad__controls select:hover { border-color: var(--primary-color); }
29
- .filters-quad__controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
30
-
31
- .filters-quad__legend { display:flex; align-items:center; justify-content:center; gap:12px; flex-wrap:wrap; font-size:12px; color: var(--text-color); margin: 2px 0 10px 0; }
32
- .filters-quad__legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
33
- .filters-quad__legend .swatch { width:10px; height:10px; border-radius:50%; display:inline-block; }
34
 
35
  .quad-cell { border:1px solid var(--border-color); border-radius:10px; background: var(--surface-bg); display:flex; flex-direction:column; position: relative; }
36
  /* Stacking order to ensure hover/tooltip overlays are not hidden by neighbors */
37
- .filters-quad__grid .quad-cell:nth-child(1) { z-index: 4; } /* top-left */
38
- .filters-quad__grid .quad-cell:nth-child(3) { z-index: 3; } /* bottom-left */
39
- .filters-quad__grid .quad-cell:nth-child(2) { z-index: 2; } /* top-right */
40
- .filters-quad__grid .quad-cell:nth-child(4) { z-index: 1; } /* bottom-right */
41
  .quad-cell .cell-header { padding:8px 10px; border-bottom:1px solid var(--border-color); display:flex; align-items:center; justify-content:space-between; gap:8px; }
42
  .quad-cell .cell-title { font-size: 13px; font-weight: 700; color: var(--text-color); }
43
  .quad-cell .cell-controls { display:flex; align-items:center; gap:12px; }
@@ -45,39 +35,40 @@
45
  .quad-cell select {
46
  font-size: 12px; padding: 6px 28px 6px 10px; border: 1px solid var(--border-color); border-radius: 8px;
47
  background-color: var(--surface-bg); color: var(--text-color);
48
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
49
  background-repeat: no-repeat; background-position: right 8px center; background-size: 12px;
50
  -webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;
51
  }
52
  [data-theme="dark"] .quad-cell select {
53
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
54
  }
55
  .quad-cell select:hover { border-color: var(--primary-color); }
56
  .quad-cell select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
57
  .quad-cell .cell-body { position: relative; }
58
  .quad-cell .cell-body { width:100%; overflow:hidden; }
59
  .quad-cell .cell-body svg { max-width:100%; height:auto; }
60
- .quad-cell .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
61
- .quad-cell .legend .items { display:flex; flex-wrap:wrap; gap:8px 12px; align-items:center; justify-content:flex-end; }
62
- .quad-cell .legend .item { display:flex; align-items:center; gap:6px; white-space:nowrap; }
63
- .quad-cell .legend .swatch { width:10px; height:10px; border-radius:50%; display:inline-block; }
 
64
  /* Tooltip refined styling */
65
- .filters-quad .d3-tooltip {
66
  z-index: 20;
67
  backdrop-filter: saturate(1.12) blur(8px);
68
  }
69
- .filters-quad .d3-tooltip__inner {
70
  display: flex;
71
  flex-direction: column;
72
  gap: 6px;
73
  min-width: 220px;
74
  }
75
- .filters-quad .d3-tooltip__inner > div:first-child {
76
  font-weight: 800;
77
  letter-spacing: 0.1px;
78
  margin-bottom: 0;
79
  }
80
- .filters-quad .d3-tooltip__inner > div:nth-child(2) {
81
  font-size: 11px;
82
  color: var(--muted-color);
83
  display: block;
@@ -85,18 +76,110 @@
85
  margin-bottom: 2px;
86
  letter-spacing: 0.1px;
87
  }
88
- .filters-quad .d3-tooltip__inner > div:nth-child(n+3) {
89
  padding-top: 6px;
90
  border-top: 1px solid var(--border-color);
91
  }
92
- .filters-quad .d3-tooltip__inner svg {
93
  display: inline-block;
94
  vertical-align: middle;
95
  margin-right: 2px;
96
  }
97
- .filters-quad .d3-tooltip__inner strong {
98
  margin-right: 6px;
99
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  </style>
101
  <script>
102
  (() => {
@@ -144,7 +227,7 @@
144
  const gLines = gRoot.append('g').attr('class','lines');
145
  const gPoints = gRoot.append('g').attr('class','points');
146
  const gHover = gRoot.append('g').attr('class','hover');
147
- const gLegend = gRoot.append('foreignObject').attr('class','legend').style('display','none');
148
 
149
  // Tooltip
150
  cell.style.position = cell.style.position || 'relative';
@@ -189,44 +272,27 @@
189
  // Colors and markers (match original embeds)
190
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
191
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
192
- const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
193
- const markerSize = 6;
194
- function drawMarker(selection, shape, size) {
195
- const s = size / 2;
196
- switch (shape) {
197
- case 'circle':
198
- return selection.append('circle').attr('r', s);
199
- case 'square':
200
- return selection.append('rect').attr('x', -s).attr('y', -s).attr('width', size).attr('height', size);
201
- case 'triangle':
202
- return selection.append('path').attr('d', `M0,${-s * 1.2} L${s * 1.1},${s * 0.6} L${-s * 1.1},${s * 0.6} Z`);
203
- case 'diamond':
204
- return selection.append('path').attr('d', `M0,${-s * 1.2} L${s * 1.1},0 L0,${s * 1.2} L${-s * 1.1},0 Z`);
205
- case 'inverted-triangle':
206
- return selection.append('path').attr('d', `M0,${s * 1.2} L${s * 1.1},${-s * 0.6} L${-s * 1.1},${-s * 0.6} Z`);
207
- default:
208
- return selection.append('circle').attr('r', s);
209
- }
210
- }
211
- // Inline SVG for tooltip shapes
212
- function markerSVG(shape, color) {
213
- const size = 12; const s = size / 2; const stroke = color;
214
- if (shape === 'circle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
215
- if (shape === 'square') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><rect x="-5" y="-5" width="10" height="10" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
216
- if (shape === 'triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L5,3 L-5,3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
217
- if (shape === 'diamond') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L6,0 L0,6 L-6,0 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
218
- if (shape === 'inverted-triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,6 L5,-3 L-5,-3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
219
- return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
220
- }
221
  // Ready signal for async load completion
222
  let readyResolve = null;
223
  const ready = new Promise((res)=> { readyResolve = res; });
224
 
 
 
 
 
 
 
 
 
 
 
 
225
  function updateScales(){
226
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
227
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
228
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
229
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
230
 
231
  const rect = cell.getBoundingClientRect();
232
  width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
@@ -245,31 +311,22 @@
245
  gGrid.selectAll('*').remove();
246
  gGrid.selectAll('line').data(yTicks).join('line')
247
  .attr('x1',0).attr('x2',innerWidth).attr('y1',d=>yScale(d)).attr('y2',d=>yScale(d))
248
- .attr('stroke',gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
249
 
250
  // Axes
251
  gAxes.selectAll('*').remove();
252
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
253
- const fmtK = (v) => {
254
- const abs = Math.abs(v);
255
- if (abs >= 1000) {
256
- const n = v / 1000;
257
- const s = d3.format('.1f')(n);
258
- return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
259
- }
260
- return d3.format('d')(v);
261
- };
262
- xAxis = xAxis.tickFormat(fmtK);
263
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
264
- gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
265
- gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
266
 
267
  // Axis labels
268
  gAxes.append('text')
269
  .attr('class', 'x-axis-label')
270
  .attr('x', innerWidth / 2)
271
  .attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))
272
- .attr('fill', tickColor)
273
  .attr('text-anchor', 'middle')
274
  .style('font-size', '12px')
275
  .style('font-weight', '700')
@@ -280,18 +337,12 @@
280
  .attr('transform', 'rotate(-90)')
281
  .attr('x', -innerHeight / 2)
282
  .attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
283
- .attr('fill', tickColor)
284
  .attr('text-anchor', 'middle')
285
  .style('font-size', '12px')
286
  .style('font-weight', '700')
287
  .text(axisLabelY);
288
 
289
- // Legend box (top-right)
290
- // Per-cell legend hidden; global legend is used
291
- const legendWidth = 0, legendHeight = 0;
292
- gLegend.attr('x', 0).attr('y', 0).attr('width', legendWidth).attr('height', legendHeight);
293
- const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
294
- Object.assign(legendRoot.node().style, { width:'0px', height:'0px', display:'none' });
295
  return { innerWidth, innerHeight, tickColor };
296
  }
297
 
@@ -322,7 +373,7 @@
322
  axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
323
  const { innerWidth, innerHeight } = updateScales();
324
 
325
- const series = runs.map((r, i) => ({ run:r, color: pool[i % pool.length], marker: markerShapes[i % markerShapes.length], values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
326
 
327
  // zones ± stderr (métriques non rank)
328
  gAreas.selectAll('*').remove();
@@ -334,43 +385,70 @@
334
  const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);
335
  const coords = upper.concat(lower);
336
  const pathData = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed)(coords);
337
- gAreas.append('path').attr('d', pathData).attr('fill', s.color).attr('opacity', 0.15).attr('stroke', 'none');
 
 
 
 
 
 
 
 
338
  });
339
  }
340
 
341
  const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
342
- paths.enter().append('path').attr('class','run-line').attr('fill','none').attr('stroke-width',2).attr('opacity',0.9)
343
- .attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values))
344
- .merge(paths).attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  paths.exit().remove();
346
 
347
- gPoints.selectAll('*').remove();
348
- series.forEach((s, idx) => {
349
- const pointGroup = gPoints.selectAll(`.points-${idx}`)
350
- .data(s.values)
351
- .join('g')
352
- .attr('class', `points-${idx}`)
353
- .attr('transform', d => `translate(${xScale(d.step)},${yScale(d.value)})`);
354
- drawMarker(pointGroup, s.marker, markerSize)
355
- .attr('fill', s.color)
356
- .attr('stroke', s.color)
357
- .attr('stroke-width', 1.5)
358
- .style('cursor', 'crosshair');
359
- });
 
 
 
360
 
361
  // No per-cell legend content (handled globally)
362
 
363
  // Hover
364
  gHover.selectAll('*').remove();
365
  const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
366
- const hoverLine = gHover.append('line').attr('stroke','rgba(0,0,0,0.25)').attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
367
  const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);
368
  function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
369
- let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${nearest}</div>`;
370
- const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, marker:s.marker, pt }; }).filter(e => e.pt && e.pt.value!=null);
371
  entries.sort((a,b)=> (a.pt.value - b.pt.value));
372
  const fmt = (vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4));
373
- entries.forEach(e => { const err = (e.pt.stderr!=null && isFinite(e.pt.stderr) && e.pt.stderr>0) ? ` ± ${fmt(e.pt.stderr)}` : ''; html += `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;">${markerSVG(e.marker, e.color)}<strong>${e.run}</strong> ${fmt(e.pt.value)}${err}</div>`; });
 
 
 
374
  tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`; }
375
  function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }, 100); }
376
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
@@ -418,7 +496,7 @@
418
  const isRank = /rank/i.test(key); const isAverage = /average/i.test(key); const isRankStrict = isRank && !isAverage;
419
  runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });
420
  const rankMax = isRank ? Math.max(1, Math.round(maxVal)) : null;
421
- return { isRank, isRankStrict, min: minVal, max: maxVal, rankMax };
422
  },
423
  setSharedY: (cfg) => { sharedYConfig = cfg || null; if (metricList && metricList.length) { /* re-render last metric if possible */ const current = cfg && cfg.key ? cfg.key : null; const m = current || metricList[0]; renderMetric(m); } }
424
  };
@@ -427,23 +505,30 @@
427
  const bootstrap = () => {
428
  const scriptEl = THIS_SCRIPT;
429
  let host = null;
 
 
 
 
 
 
430
  // Try finding within parent (fragment mount is inside parent)
431
  if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {
432
- host = scriptEl.parentElement.querySelector('.filters-quad');
433
  }
434
  // Fallback: scan previous siblings
435
  if (!host) {
436
  let sib = scriptEl && scriptEl.previousElementSibling;
437
- while (sib && !(sib.classList && sib.classList.contains('filters-quad'))) {
438
  sib = sib.previousElementSibling;
439
  }
440
  host = sib || null;
441
  }
442
  // Last resort: global query
443
- if (!host) { host = document.querySelector('.filters-quad'); }
444
  if (!host) return;
445
  if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
446
  const cells = host.querySelectorAll('.quad-cell'); if (!cells.length) return;
 
447
  const instances = Array.from(cells).map(cell => initRunLine(cell));
448
 
449
  (async () => {
@@ -455,14 +540,15 @@
455
  if (!metrics.length) { metrics = lists[0] || []; }
456
  const def = (metrics.includes('ai2d_exact_match') ? 'ai2d_exact_match' : (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || ''));
457
 
458
- let ctrl = host.querySelector('.filters-quad__controls');
459
- if (!ctrl) { ctrl = document.createElement('div'); ctrl.className = 'filters-quad__controls'; host.insertBefore(ctrl, host.firstChild); }
460
- ctrl.innerHTML = '';
461
- const label = document.createElement('label'); label.textContent = 'Metric';
462
- const select = document.createElement('select');
463
- metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=prettyMetricLabel(m); select.appendChild(o); });
464
- if (def) select.value = def;
465
- label.appendChild(select); ctrl.appendChild(label);
 
466
 
467
  const computeAndApplySharedY = (metric) => {
468
  try {
@@ -482,11 +568,11 @@
482
 
483
  const applyAll = (v) => { computeAndApplySharedY(v); instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); };
484
  if (def) applyAll(def);
485
- select.addEventListener('change', () => applyAll(select.value));
486
 
487
- // Légende globale (couleurs + formes, alignée avec les marqueurs des séries)
488
- const legendHost = host.querySelector('.filters-quad__legend');
489
- if (legendHost) {
490
  try {
491
  const f = '/data/formatting_filters.csv';
492
  const r = await fetch(f, { cache:'no-cache' });
@@ -496,21 +582,32 @@
496
  const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
497
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
498
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])];
499
- const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle'];
500
- const shapeSVG = (shape, color) => {
501
- const size = 12; const s = size/2; const stroke = color;
502
- if (shape === 'circle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
503
- if (shape === 'square') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><rect x="-5" y="-5" width="10" height="10" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
504
- if (shape === 'triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L5,3 L-5,3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
505
- if (shape === 'diamond') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L6,0 L0,6 L-6,0 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
506
- if (shape === 'inverted-triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,6 L5,-3 L-5,-3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
507
- return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
508
- };
509
- legendHost.innerHTML = runList.map((name, i)=> {
510
  const color = pool[i % pool.length];
511
- const shape = markerShapes[i % markerShapes.length];
512
- return `<span class="item">${shapeSVG(shape, color)}<span>${name}</span></span>`;
513
  }).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  }
515
  } catch {}
516
  }
@@ -521,3 +618,5 @@
521
  })();
522
  </script>
523
 
 
 
 
1
+ <div class="line-quad">
2
+
3
+ <div class="line-quad__grid">
 
4
  <div class="quad-cell" data-title="Formatting Filter" data-csv="/data/formatting_filters.csv"></div>
5
  <div class="quad-cell" data-title="Relevance Filter" data-csv="/data/relevance_filters.csv"></div>
6
  <div class="quad-cell" data-title="Visual Dependency Filter" data-csv="/data/visual_dependency_filters.csv"></div>
7
  <div class="quad-cell" data-title="Image Correspondence Filter" data-csv="/data/image_correspondence_filters.csv"></div>
8
  </div>
9
  <noscript>JavaScript is required to render these charts.</noscript>
10
+
11
  </div>
12
  <style>
13
+ .line-quad { position: relative; }
14
+ /* Axis/tick/grid use global variables from _variables.css */
15
+ /* Apply axis/tick/grid purely via CSS */
16
+ .line-quad .axes path,
17
+ .line-quad .axes line { stroke: var(--axis-color); }
18
+ .line-quad .axes text { fill: var(--tick-color); }
19
+ .line-quad .grid line { stroke: var(--grid-color); }
20
+ .line-quad__grid { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
21
+ @media (max-width: 980px) { .line-quad__grid { grid-template-columns: 1fr; } }
22
+
23
+
 
 
 
 
 
 
 
 
 
 
24
 
25
  .quad-cell { border:1px solid var(--border-color); border-radius:10px; background: var(--surface-bg); display:flex; flex-direction:column; position: relative; }
26
  /* Stacking order to ensure hover/tooltip overlays are not hidden by neighbors */
27
+ .line-quad__grid .quad-cell:nth-child(1) { z-index: 4; } /* top-left */
28
+ .line-quad__grid .quad-cell:nth-child(3) { z-index: 3; } /* bottom-left */
29
+ .line-quad__grid .quad-cell:nth-child(2) { z-index: 2; } /* top-right */
30
+ .line-quad__grid .quad-cell:nth-child(4) { z-index: 1; } /* bottom-right */
31
  .quad-cell .cell-header { padding:8px 10px; border-bottom:1px solid var(--border-color); display:flex; align-items:center; justify-content:space-between; gap:8px; }
32
  .quad-cell .cell-title { font-size: 13px; font-weight: 700; color: var(--text-color); }
33
  .quad-cell .cell-controls { display:flex; align-items:center; gap:12px; }
 
35
  .quad-cell select {
36
  font-size: 12px; padding: 6px 28px 6px 10px; border: 1px solid var(--border-color); border-radius: 8px;
37
  background-color: var(--surface-bg); color: var(--text-color);
38
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
39
  background-repeat: no-repeat; background-position: right 8px center; background-size: 12px;
40
  -webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;
41
  }
42
  [data-theme="dark"] .quad-cell select {
43
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
44
  }
45
  .quad-cell select:hover { border-color: var(--primary-color); }
46
  .quad-cell select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
47
  .quad-cell .cell-body { position: relative; }
48
  .quad-cell .cell-body { width:100%; overflow:hidden; }
49
  .quad-cell .cell-body svg { max-width:100%; height:auto; }
50
+
51
+ .line-quad.hovering .lines path.ghost { opacity: .25; }
52
+ .line-quad.hovering .points circle.ghost { opacity: .25; }
53
+ .line-quad.hovering .areas path.ghost { opacity: .08; }
54
+ .line-quad.hovering .legend-bottom .item.ghost { opacity: .35; }
55
  /* Tooltip refined styling */
56
+ .line-quad .d3-tooltip {
57
  z-index: 20;
58
  backdrop-filter: saturate(1.12) blur(8px);
59
  }
60
+ .line-quad .d3-tooltip__inner {
61
  display: flex;
62
  flex-direction: column;
63
  gap: 6px;
64
  min-width: 220px;
65
  }
66
+ .line-quad .d3-tooltip__inner > div:first-child {
67
  font-weight: 800;
68
  letter-spacing: 0.1px;
69
  margin-bottom: 0;
70
  }
71
+ .line-quad .d3-tooltip__inner > div:nth-child(2) {
72
  font-size: 11px;
73
  color: var(--muted-color);
74
  display: block;
 
76
  margin-bottom: 2px;
77
  letter-spacing: 0.1px;
78
  }
79
+ .line-quad .d3-tooltip__inner > div:nth-child(n+3) {
80
  padding-top: 6px;
81
  border-top: 1px solid var(--border-color);
82
  }
83
+ .line-quad .d3-tooltip__inner svg {
84
  display: inline-block;
85
  vertical-align: middle;
86
  margin-right: 2px;
87
  }
88
+ .line-quad .d3-tooltip__inner strong {
89
  margin-right: 6px;
90
  }
91
+ .line-quad .d3-tooltip__color-dot {
92
+ display: inline-block;
93
+ width: 12px;
94
+ height: 12px;
95
+ border-radius: 3px;
96
+ border: 1px solid var(--border-color);
97
+ }
98
+ /* Header layout (like d3-line-simple) */
99
+ .line-quad__header {
100
+ display: flex;
101
+ align-items: flex-start;
102
+ justify-content: flex-start;
103
+ gap: 12px;
104
+ margin: 8px 0 0 0;
105
+ flex-wrap: wrap;
106
+ }
107
+ .line-quad__header .legend-bottom {
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: flex-start;
111
+ gap: 6px;
112
+ font-size: 12px;
113
+ color: var(--text-color);
114
+ }
115
+ .line-quad__header .legend-bottom .legend-title {
116
+ font-size: 12px;
117
+ font-weight: 700;
118
+ color: var(--text-color);
119
+ }
120
+ .line-quad__header .legend-bottom .items {
121
+ display: flex;
122
+ flex-wrap: wrap;
123
+ gap: 8px 14px;
124
+ }
125
+ .line-quad__header .legend-bottom .item {
126
+ display: inline-flex;
127
+ align-items: center;
128
+ gap: 6px;
129
+ white-space: nowrap;
130
+ }
131
+ .line-quad__header .legend-bottom .swatch {
132
+ width: 14px;
133
+ height: 14px;
134
+ border-radius: 3px;
135
+ border: 1px solid var(--border-color);
136
+ display: inline-block;
137
+ }
138
+ .line-quad .controls {
139
+ margin-top: 0;
140
+ display: flex;
141
+ gap: 16px;
142
+ align-items: center;
143
+ justify-content: flex-end;
144
+ width: auto;
145
+ flex-wrap: wrap;
146
+ }
147
+ .line-quad .controls .control-group {
148
+ display: flex;
149
+ flex-direction: column;
150
+ align-items: flex-start;
151
+ gap: 6px;
152
+ }
153
+ .line-quad .controls label {
154
+ font-size: 12px;
155
+ color: var(--text-color);
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 6px;
159
+ white-space: nowrap;
160
+ font-weight: 700;
161
+ }
162
+ .line-quad .controls select {
163
+ font-size: 12px;
164
+ padding: 8px 28px 8px 10px;
165
+ border: 1px solid var(--border-color);
166
+ border-radius: 8px;
167
+ background-color: var(--surface-bg);
168
+ color: var(--text-color);
169
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
170
+ background-repeat: no-repeat;
171
+ background-position: right 8px center;
172
+ background-size: 12px;
173
+ -webkit-appearance: none;
174
+ appearance: none;
175
+ cursor: pointer;
176
+ transition: border-color .15s ease, box-shadow .15s ease;
177
+ }
178
+ [data-theme="dark"] .line-quad .controls select {
179
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
180
+ }
181
+ .line-quad .controls select:hover { border-color: var(--primary-color); }
182
+ .line-quad .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
183
  </style>
184
  <script>
185
  (() => {
 
227
  const gLines = gRoot.append('g').attr('class','lines');
228
  const gPoints = gRoot.append('g').attr('class','points');
229
  const gHover = gRoot.append('g').attr('class','hover');
230
+ // Removed per-cell legend; using global footer legend
231
 
232
  // Tooltip
233
  cell.style.position = cell.style.position || 'relative';
 
272
  // Colors and markers (match original embeds)
273
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
274
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
275
+ // Shapes supprimés: on n'utilise que la couleur
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  // Ready signal for async load completion
277
  let readyResolve = null;
278
  const ready = new Promise((res)=> { readyResolve = res; });
279
 
280
+ // Shared formatter for thousands: 5000 -> 5k, 1500 -> 1.5k (trim .0)
281
+ const formatK = (v) => {
282
+ const abs = Math.abs(v);
283
+ if (abs >= 1000) {
284
+ const n = v / 1000;
285
+ const s = d3.format('.1f')(n);
286
+ return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
287
+ }
288
+ return d3.format('d')(v);
289
+ };
290
+
291
  function updateScales(){
292
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
293
+ const axisColor = 'var(--axis-color)';
294
+ const tickColor = 'var(--tick-color)';
295
+ const gridColor = 'var(--grid-color)';
296
 
297
  const rect = cell.getBoundingClientRect();
298
  width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
 
311
  gGrid.selectAll('*').remove();
312
  gGrid.selectAll('line').data(yTicks).join('line')
313
  .attr('x1',0).attr('x2',innerWidth).attr('y1',d=>yScale(d)).attr('y2',d=>yScale(d))
314
+ .attr('stroke', gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
315
 
316
  // Axes
317
  gAxes.selectAll('*').remove();
318
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
319
+ xAxis = xAxis.tickFormat(formatK);
 
 
 
 
 
 
 
 
 
320
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
321
+ gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','11px'); });
322
+ gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','11px'); });
323
 
324
  // Axis labels
325
  gAxes.append('text')
326
  .attr('class', 'x-axis-label')
327
  .attr('x', innerWidth / 2)
328
  .attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))
329
+ .attr('fill', 'var(--text-color)')
330
  .attr('text-anchor', 'middle')
331
  .style('font-size', '12px')
332
  .style('font-weight', '700')
 
337
  .attr('transform', 'rotate(-90)')
338
  .attr('x', -innerHeight / 2)
339
  .attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
340
+ .attr('fill', 'var(--text-color)')
341
  .attr('text-anchor', 'middle')
342
  .style('font-size', '12px')
343
  .style('font-weight', '700')
344
  .text(axisLabelY);
345
 
 
 
 
 
 
 
346
  return { innerWidth, innerHeight, tickColor };
347
  }
348
 
 
373
  axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
374
  const { innerWidth, innerHeight } = updateScales();
375
 
376
+ const series = runs.map((r, i) => ({ run:r, color: pool[i % pool.length], values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
377
 
378
  // zones ± stderr (métriques non rank)
379
  gAreas.selectAll('*').remove();
 
385
  const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);
386
  const coords = upper.concat(lower);
387
  const pathData = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed)(coords);
388
+ gAreas.append('path')
389
+ .attr('class','area')
390
+ .attr('data-run', s.run)
391
+ .attr('d', pathData)
392
+ .attr('fill', s.color)
393
+ .attr('opacity', 0)
394
+ .attr('stroke', 'none')
395
+ .transition().duration(450).ease(d3.easeCubicOut)
396
+ .attr('opacity', 0.15);
397
  });
398
  }
399
 
400
  const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
401
+ paths.enter()
402
+ .append('path')
403
+ .attr('class','run-line')
404
+ .attr('data-run', d=>d.run)
405
+ .attr('fill','none')
406
+ .attr('stroke-width', 1)
407
+ .attr('opacity',0)
408
+ .attr('stroke', d=>d.color)
409
+ .attr('d', d=>lineGen(d.values))
410
+ .transition().duration(450).ease(d3.easeCubicOut)
411
+ .attr('opacity',0.9);
412
+ paths
413
+ .transition().duration(450).ease(d3.easeCubicOut)
414
+ .attr('stroke', d=>d.color)
415
+ .attr('opacity',0.9)
416
+ .attr('d', d=>lineGen(d.values));
417
  paths.exit().remove();
418
 
419
+ // Draw light point markers at each data sample (subtle)
420
+ const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
421
+ const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
422
+ ptsSel.enter().append('circle').attr('class','pt')
423
+ .attr('data-run', d=>d.run)
424
+ .attr('r', 2)
425
+ .attr('fill', d=>d.color)
426
+ .attr('fill-opacity', 0.6)
427
+ .attr('stroke', 'none')
428
+ .attr('cx', d=>xScale(d.step))
429
+ .attr('cy', d=>yScale(d.value))
430
+ .merge(ptsSel)
431
+ .transition().duration(180)
432
+ .attr('cx', d=>xScale(d.step))
433
+ .attr('cy', d=>yScale(d.value));
434
+ ptsSel.exit().remove();
435
 
436
  // No per-cell legend content (handled globally)
437
 
438
  // Hover
439
  gHover.selectAll('*').remove();
440
  const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
441
+ const hoverLine = gHover.append('line').style('stroke','var(--text-color)').attr('stroke-opacity', 0.25).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
442
  const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);
443
  function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
444
+ let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${formatK(nearest)}</div>`;
445
+ const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null);
446
  entries.sort((a,b)=> (a.pt.value - b.pt.value));
447
  const fmt = (vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4));
448
+ entries.forEach(e => {
449
+ const err = (e.pt.stderr!=null && isFinite(e.pt.stderr) && e.pt.stderr>0) ? ` ± ${fmt(e.pt.stderr)}` : '';
450
+ html += `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}${err}</span></div>`;
451
+ });
452
  tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`; }
453
  function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }, 100); }
454
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
 
496
  const isRank = /rank/i.test(key); const isAverage = /average/i.test(key); const isRankStrict = isRank && !isAverage;
497
  runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });
498
  const rankMax = isRank ? Math.max(1, Math.round(maxVal)) : null;
499
+ return { isRank, isRankStrict, min: maxVal === 0 && minVal === Infinity ? null : minVal, max: maxVal, rankMax };
500
  },
501
  setSharedY: (cfg) => { sharedYConfig = cfg || null; if (metricList && metricList.length) { /* re-render last metric if possible */ const current = cfg && cfg.key ? cfg.key : null; const m = current || metricList[0]; renderMetric(m); } }
502
  };
 
505
  const bootstrap = () => {
506
  const scriptEl = THIS_SCRIPT;
507
  let host = null;
508
+ // Build header (legend + controls) and append after grid
509
+ const header = document.createElement('div'); header.className = 'line-quad__header';
510
+ const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>';
511
+ const controls = document.createElement('div'); controls.className = 'controls'; controls.innerHTML = '<div class="control-group"><label>Metric</label><select></select></div>';
512
+ header.appendChild(legend);
513
+ header.appendChild(controls);
514
  // Try finding within parent (fragment mount is inside parent)
515
  if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {
516
+ host = scriptEl.parentElement.querySelector('.line-quad');
517
  }
518
  // Fallback: scan previous siblings
519
  if (!host) {
520
  let sib = scriptEl && scriptEl.previousElementSibling;
521
+ while (sib && !(sib.classList && sib.classList.contains('line-quad'))) {
522
  sib = sib.previousElementSibling;
523
  }
524
  host = sib || null;
525
  }
526
  // Last resort: global query
527
+ if (!host) { host = document.querySelector('.line-quad'); }
528
  if (!host) return;
529
  if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
530
  const cells = host.querySelectorAll('.quad-cell'); if (!cells.length) return;
531
+ host.appendChild(header);
532
  const instances = Array.from(cells).map(cell => initRunLine(cell));
533
 
534
  (async () => {
 
540
  if (!metrics.length) { metrics = lists[0] || []; }
541
  const def = (metrics.includes('ai2d_exact_match') ? 'ai2d_exact_match' : (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || ''));
542
 
543
+ // Wire header controls (select under "Metric" label)
544
+ const headerEl = host.querySelector('.line-quad__header');
545
+ if (headerEl && !headerEl.isConnected) host.appendChild(header);
546
+ const select = (headerEl || header).querySelector('.controls select');
547
+ if (select) {
548
+ select.innerHTML = '';
549
+ metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=prettyMetricLabel(m); select.appendChild(o); });
550
+ if (def) select.value = def;
551
+ }
552
 
553
  const computeAndApplySharedY = (metric) => {
554
  try {
 
568
 
569
  const applyAll = (v) => { computeAndApplySharedY(v); instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); };
570
  if (def) applyAll(def);
571
+ if (select) select.addEventListener('change', () => applyAll(select.value));
572
 
573
+ // Global legend (in header, colors only)
574
+ const legendItemsHost = (headerEl || header).querySelector('.legend-bottom .items');
575
+ if (legendItemsHost) {
576
  try {
577
  const f = '/data/formatting_filters.csv';
578
  const r = await fetch(f, { cache:'no-cache' });
 
582
  const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
583
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
584
  const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])];
585
+ legendItemsHost.innerHTML = runList.map((name, i)=> {
 
 
 
 
 
 
 
 
 
 
586
  const color = pool[i % pool.length];
587
+ return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
 
588
  }).join('');
589
+ // Legend hover ghosting across all cells
590
+ legendItemsHost.querySelectorAll('.item').forEach(el => {
591
+ el.addEventListener('mouseenter', () => {
592
+ const run = el.getAttribute('data-run'); if (!run) return;
593
+ host.classList.add('hovering');
594
+ host.querySelectorAll('.quad-cell').forEach(cell => {
595
+ cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
596
+ cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
597
+ cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run));
598
+ });
599
+ legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
600
+ });
601
+ el.addEventListener('mouseleave', () => {
602
+ host.classList.remove('hovering');
603
+ host.querySelectorAll('.quad-cell').forEach(cell => {
604
+ cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost'));
605
+ cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost'));
606
+ cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost'));
607
+ });
608
+ legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
609
+ });
610
+ });
611
  }
612
  } catch {}
613
  }
 
618
  })();
619
  </script>
620
 
621
+
622
+
app/src/content/embeds/d3-line.html CHANGED
@@ -1,6 +1,36 @@
1
- <div class="d3-line" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
- .d3-line .d3-line__controls select {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  font-size: 12px;
5
  padding: 8px 28px 8px 10px;
6
  border: 1px solid var(--border-color);
@@ -8,505 +38,431 @@
8
  background-color: var(--surface-bg);
9
  color: var(--text-color);
10
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
11
- background-repeat: no-repeat;
12
- background-position: right 8px center;
13
- background-size: 12px;
14
- -webkit-appearance: none;
15
- -moz-appearance: none;
16
- appearance: none;
17
- cursor: pointer;
18
- transition: border-color .15s ease, box-shadow .15s ease;
19
  }
20
- [data-theme="dark"] .d3-line .d3-line__controls select {
21
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
22
  }
23
- .d3-line .d3-line__controls select:hover {
24
- border-color: var(--primary-color);
25
- }
26
- .d3-line .d3-line__controls select:focus {
27
- border-color: var(--primary-color);
28
- box-shadow: 0 0 0 3px rgba(232,137,171,.25);
29
- outline: none;
30
- }
31
- .d3-line .d3-line__controls label { gap: 8px; }
32
-
33
- /* Range slider themed with --primary-color */
34
- .d3-line .d3-line__controls input[type="range"] {
35
- -webkit-appearance: none;
36
- appearance: none;
37
- width: 100%;
38
- height: 6px;
39
- border-radius: 999px;
40
- background: var(--border-color);
41
- outline: none;
42
- }
43
- .d3-line .d3-line__controls input[type="range"]::-webkit-slider-runnable-track {
44
- height: 6px;
45
- background: transparent;
46
- border-radius: 999px;
47
- }
48
- .d3-line .d3-line__controls input[type="range"]::-webkit-slider-thumb {
49
- -webkit-appearance: none;
50
- appearance: none;
51
- width: 16px;
52
- height: 16px;
53
- border-radius: 50%;
54
- background: var(--primary-color);
55
- border: 2px solid var(--on-primary);
56
- margin-top: -5px;
57
- cursor: pointer;
58
  }
59
- .d3-line .d3-line__controls input[type="range"]::-moz-range-track {
60
- height: 6px;
61
- background: transparent;
62
- border-radius: 999px;
63
  }
64
- .d3-line .d3-line__controls input[type="range"]::-moz-range-thumb {
65
- width: 16px;
66
- height: 16px;
67
- border-radius: 50%;
68
- background: var(--primary-color);
69
- border: 2px solid var(--on-primary);
70
- cursor: pointer;
71
  }
72
- /* Improved line color via CSS */
73
- .d3-line .lines path.improved { stroke: var(--primary-color); }
 
 
 
 
 
 
 
 
 
74
  </style>
75
  <script>
76
  (() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  const ensureD3 = (cb) => {
78
  if (window.d3 && typeof window.d3.select === 'function') return cb();
79
  let s = document.getElementById('d3-cdn-script');
80
- if (!s) {
81
- s = document.createElement('script');
82
- s.id = 'd3-cdn-script';
83
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
84
- document.head.appendChild(s);
85
- }
86
  const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
87
- s.addEventListener('load', onReady, { once: true });
88
- if (window.d3) onReady();
89
  };
90
 
91
  const bootstrap = () => {
92
  const scriptEl = document.currentScript;
93
- let container = null;
94
-
95
- // Prefer the closest previous sibling with class .d3-line (local instance)
96
- if (scriptEl) {
97
- let el = scriptEl.previousElementSibling;
98
- while (el && !(el.classList && el.classList.contains('d3-line'))) {
99
- el = el.previousElementSibling;
100
- }
101
- if (el) container = el;
102
  }
103
-
104
- // Fallback: pick the last unmounted .d3-line in the document
105
- if (!container) {
106
- const candidates = Array.from(document.querySelectorAll('.d3-line'))
107
- .filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
108
- container = candidates[candidates.length - 1] || null;
109
- }
110
-
111
  if (!container) return;
112
- if (container.dataset) {
113
- if (container.dataset.mounted === 'true') return;
114
- container.dataset.mounted = 'true';
115
- }
116
-
117
- // CSV: prefer public path, fallback to relative
118
- const CSV_PATHS = [
119
- '/data/against_baselines.csv',
120
- './assets/data/against_baselines.csv',
121
- '../assets/data/against_baselines.csv',
122
- '../../assets/data/against_baselines.csv'
123
- ];
124
- const fetchFirstAvailable = async (paths) => {
125
- for (const p of paths) {
126
- try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(e) {}
127
- }
128
- throw new Error('CSV not found: against_baselines.csv');
129
- };
130
 
131
- // Controls UI
132
  const controls = document.createElement('div');
133
- controls.className = 'd3-line__controls';
134
- Object.assign(controls.style, {
135
- marginTop: '12px',
136
- display: 'flex',
137
- gap: '16px',
138
- alignItems: 'center',
139
- justifyContent: 'space-between',
140
- width: '100%'
141
- });
142
-
143
  const labelMetric = document.createElement('label');
144
- Object.assign(labelMetric.style, {
145
- fontSize: '12px', color: 'var(--muted-color)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px', marginLeft: 'auto'
146
- });
147
  labelMetric.textContent = 'Metric';
148
  const selectMetric = document.createElement('select');
149
- Object.assign(selectMetric.style, { fontSize: '12px' });
150
- labelMetric.appendChild(selectMetric);
151
-
152
- // Inline legend on the right of the select
153
- const legendInline = document.createElement('div');
154
- legendInline.className = 'controls__legend';
155
- Object.assign(legendInline.style, {
156
- display: 'flex',
157
- gap: '8px',
158
- alignItems: 'center',
159
- flexWrap: 'nowrap',
160
- fontSize: '11px',
161
- marginLeft: '8px'
162
- });
163
- controls.appendChild(legendInline);
164
- controls.appendChild(labelMetric);
165
-
166
- // Create SVG
167
- const svg = d3.select(container).append('svg')
168
- .attr('width', '100%')
169
- .style('display', 'block');
170
-
171
- // Groups
172
- const gRoot = svg.append('g');
173
- const gGrid = gRoot.append('g').attr('class', 'grid');
174
- const gAxes = gRoot.append('g').attr('class', 'axes');
175
- const gLines = gRoot.append('g').attr('class', 'lines');
176
- const gPoints = gRoot.append('g').attr('class', 'points');
177
- const gHover = gRoot.append('g').attr('class', 'hover');
178
- const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
179
 
180
  // Tooltip
181
  container.style.position = container.style.position || 'relative';
182
- let tip = container.querySelector('.d3-tooltip');
183
- let tipInner;
184
  if (!tip) {
185
- tip = document.createElement('div');
186
- tip.className = 'd3-tooltip';
187
  Object.assign(tip.style, {
188
- position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
189
- padding: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
190
- background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0',
191
- transition: 'opacity .12s ease'
192
  });
193
- tipInner = document.createElement('div');
194
- tipInner.className = 'd3-tooltip__inner';
195
- tipInner.style.textAlign = 'left';
196
- tip.appendChild(tipInner);
197
- container.appendChild(tip);
198
- } else {
199
- tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
200
- }
201
-
202
- // Colors per run
203
- const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
204
- const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
205
-
206
- // State and data
207
- let metricList = [];
208
- let runList = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  let runOrder = [];
210
- const dataByMetric = new Map(); // metric => { run => [{step,value}] }
211
- let isRankStrictFlag = false;
212
- let rankTickMax = 1;
213
-
214
- // Scales and layout
215
- let width = 800, height = 360;
216
- let margin = { top: 16, right: 28, bottom: 56, left: 64 };
217
- let xScale = d3.scaleLinear();
218
- let yScale = d3.scaleLinear();
219
-
220
- // Line generators
221
- const lineGenSmooth = d3.line()
222
- .curve(d3.curveCatmullRom.alpha(0.05))
223
- .x((d) => xScale(d.step))
224
- .y((d) => yScale(d.value));
225
- const lineGenStep = d3.line()
226
- .curve(d3.curveStepAfter)
227
- .x((d) => xScale(d.step))
228
- .y((d) => yScale(d.value));
229
 
230
- // Hover elements
231
- const hoverLine = gHover.append('line').attr('stroke-width', 1);
232
-
233
- const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
234
-
235
- // Responsive layout for controls and inline legend
236
- function applyResponsiveLayout(containerWidth) {
237
- const isNarrow = containerWidth <= 600;
238
- if (isNarrow) {
239
- controls.style.flexWrap = 'wrap';
240
- controls.style.alignItems = 'flex-start';
241
- controls.style.justifyContent = 'flex-start';
242
- legendInline.style.flexWrap = 'wrap';
243
- legendInline.style.rowGap = '4px';
244
- labelMetric.style.marginLeft = '0';
245
- labelMetric.style.flexBasis = '100%';
246
- labelMetric.style.width = '100%';
247
- } else {
248
- controls.style.flexWrap = 'nowrap';
249
- controls.style.alignItems = 'center';
250
- controls.style.justifyContent = 'space-between';
251
- legendInline.style.flexWrap = 'nowrap';
252
- labelMetric.style.marginLeft = 'auto';
253
- labelMetric.style.flexBasis = 'auto';
254
- labelMetric.style.width = 'auto';
255
  }
 
256
  }
257
 
258
- function updateScales() {
259
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
260
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
261
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
262
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
 
 
264
  width = container.clientWidth || 800;
265
- height = Math.max(260, Math.round(width / 3));
266
  svg.attr('width', width).attr('height', height);
267
-
268
- // Update controls layout for current width
269
- applyResponsiveLayout(width);
270
-
271
  const innerWidth = width - margin.left - margin.right;
272
  const innerHeight = height - margin.top - margin.bottom;
273
- gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
274
-
275
- xScale.range([0, innerWidth]);
276
- yScale.range([innerHeight, 0]);
277
-
278
- // Compute Y ticks
279
- let yTicks = [];
280
- if (isRankStrictFlag) {
281
- const maxR = Math.max(1, Math.round(rankTickMax));
282
- for (let v = 1; v <= maxR; v += 1) yTicks.push(v);
283
- } else {
284
- // Use D3's tick generator to produce nice floating-point ticks
285
- yTicks = yScale.ticks(6);
286
- }
287
 
288
- // Grid (horizontal)
 
 
 
 
 
 
 
 
 
 
 
289
  gGrid.selectAll('*').remove();
290
- gGrid.selectAll('line')
291
- .data(yTicks)
292
- .join('line')
293
- .attr('x1', 0)
294
- .attr('x2', innerWidth)
295
- .attr('y1', (d) => yScale(d))
296
- .attr('y2', (d) => yScale(d))
297
- .attr('stroke', gridColor)
298
- .attr('stroke-width', 1)
299
- .attr('shape-rendering', 'crispEdges');
300
 
301
- // Axes
302
  gAxes.selectAll('*').remove();
303
- let xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
304
- const isNarrow = width <= 600;
305
- const formatK = (v) => {
306
- const vv = Math.round(v);
307
- if (isNarrow && Math.abs(vv) >= 1000) return `${Math.round(vv / 1000)}K`;
308
- return d3.format('d')(vv);
309
- };
310
- if (isRankStrictFlag) {
311
- const [dx0, dx1] = xScale.domain();
312
- const start = Math.ceil(dx0 / 1000) * 1000;
313
- const end = Math.floor(dx1 / 1000) * 1000;
314
- const xTicks = [];
315
- for (let v = start; v <= end; v += 1000) xTicks.push(v);
316
- if (xTicks.length === 0) xTicks.push(Math.round(dx0));
317
- xAxis = xAxis.tickValues(xTicks).tickFormat(formatK);
318
- } else {
319
- xAxis = xAxis.ticks(8);
320
- if (isNarrow) {
321
- xAxis = xAxis.tickFormat((v) => (Math.abs(v) >= 1000 ? `${Math.round(v / 1000)}K` : d3.format('d')(Math.round(v))));
 
 
 
322
  }
323
- }
324
- const yAxis = d3.axisLeft(yScale)
325
- .tickValues(yTicks)
326
- .tickSizeOuter(0)
327
- .tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
328
- gAxes.append('g')
329
- .attr('transform', `translate(0,${innerHeight})`)
330
- .call(xAxis)
331
- .call((g) => {
332
- g.selectAll('path, line').attr('stroke', axisColor);
333
- g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
334
- });
335
- gAxes.append('g')
336
- .call(yAxis)
337
- .call((g) => {
338
- g.selectAll('path, line').attr('stroke', axisColor);
339
- g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
340
- });
341
-
342
- // Axis labels (X and Y)
343
- gAxes.append('text')
344
- .attr('class', 'axis-label axis-label--x')
345
- .attr('x', innerWidth / 2)
346
- .attr('y', innerHeight + 44)
347
- .attr('text-anchor', 'middle')
348
- .style('font-size', '12px')
349
- .style('fill', tickColor)
350
- .text('Step');
351
- gAxes.append('text')
352
- .attr('class', 'axis-label axis-label--y')
353
- .attr('text-anchor', 'middle')
354
- .attr('transform', `translate(${-44},${innerHeight/2}) rotate(-90)`)
355
- .style('font-size', '12px')
356
- .style('fill', tickColor)
357
- .text('Value');
358
-
359
- overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
360
- hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
361
-
362
- // Legend placeholder; actual content set in renderMetric
363
- const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
364
- const legendHeight = 64;
365
- gLegend
366
- .attr('x', innerWidth - legendWidth + 42)
367
- .attr('y', innerHeight - legendHeight - 12)
368
- .attr('width', legendWidth)
369
- .attr('height', legendHeight);
370
- const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
371
- Object.assign(legendRoot.node().style, {
372
- background: 'transparent',
373
- border: 'none',
374
- borderRadius: '0',
375
- padding: '0',
376
- fontSize: '12px',
377
- lineHeight: '1.35',
378
- color: 'var(--text-color)'
379
  });
380
-
381
- return { innerWidth, innerHeight };
382
- }
383
-
384
- function renderMetric(metricKey){
385
- const map = dataByMetric.get(metricKey) || {};
386
- const runs = runOrder;
387
- // Domain
388
- let minStep = Infinity, maxStep = -Infinity, maxVal = 0, minVal = Infinity;
389
- const isRank = /rank/i.test(metricKey);
390
- const isAverage = /average/i.test(metricKey);
391
- const isRankStrict = isRank && !isAverage;
392
- runs.forEach(r => {
393
- const arr = map[r] || [];
394
- arr.forEach(pt => {
395
- const val = isRankStrict ? Math.round(pt.value) : pt.value;
396
- minStep = Math.min(minStep, pt.step);
397
- maxStep = Math.max(maxStep, pt.step);
398
- maxVal = Math.max(maxVal, val);
399
- minVal = Math.min(minVal, val);
400
- });
401
  });
402
- if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
403
- xScale.domain([minStep, maxStep]);
404
- if (isRank) {
405
- rankTickMax = Math.max(1, Math.round(maxVal));
406
- yScale.domain([rankTickMax, 1]);
407
- } else {
408
- yScale.domain([0, Math.max(1, maxVal)]).nice();
409
- }
410
- isRankStrictFlag = isRankStrict;
411
-
412
- const { innerWidth, innerHeight } = updateScales();
413
 
414
- // Bind lines
415
- const series = runs.map((r, i) => ({
416
- run: r,
417
- color: pool[i % pool.length],
418
- values: (map[r]||[])
419
- .slice()
420
- .sort((a,b)=>a.step-b.step)
421
- .map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value) } : pt)
422
- .filter(pt => !isRankStrict || (pt.step % 1000 === 0))
423
- }));
424
- const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
425
- const gen = isRank ? lineGenStep : lineGenSmooth;
426
- paths.enter().append('path').attr('class','run-line').attr('fill','none').attr('stroke-width',2)
427
- .attr('stroke', d=>d.color).attr('opacity',0.9)
428
- .attr('d', d=>gen(d.values))
429
- .merge(paths)
430
- .transition().duration(200)
431
- .attr('stroke', d=>d.color)
432
- .attr('d', d=>gen(d.values));
433
  paths.exit().remove();
434
 
435
- // No point markers for rank metrics (and none for others)
436
- gPoints.selectAll('*').remove();
437
-
438
- // Inline legend content (row, right side) compact
439
- 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('');
440
-
441
- // Hover
442
- const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step)));
443
- const steps = Array.from(stepSet).sort((a,b)=>a-b);
444
- function onMove(event){
445
- const [mx, my] = d3.pointer(event, overlay.node());
446
- const sx = Math.max(steps[0], Math.min(steps[steps.length-1], Math.round(xScale.invert(mx)/1)*1));
447
- const nearest = steps.reduce((best, s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  const xpx = xScale(nearest);
449
- hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null).attr('stroke', 'rgba(0,0,0,0.25)');
450
- // Tooltip content
451
- let html = `<div><strong>${metricKey}</strong></div><div><strong>step</strong> ${nearest}</div>`;
452
- series.forEach(s=>{
453
  const m = new Map(s.values.map(v=>[v.step, v.value]));
454
- const val = m.has(nearest) ? m.get(nearest) : null;
455
- if (val != null) {
456
- const formatVal = (vv) => (isRankStrict ? d3.format('d')(vv) : (+vv).toFixed(4));
457
- 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> ${formatVal(val)}</div>`;
458
- }
 
459
  });
460
- tipInner.innerHTML = html;
461
- const offsetX = 12, offsetY = 12;
462
- tip.style.opacity = '1'; tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
463
  }
464
  function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
465
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
466
  }
467
 
468
- // (old hover removed; hover is attached in renderMetric)
469
-
470
- // Load CSV and wire controls
471
  (async () => {
472
  try {
473
  const text = await fetchFirstAvailable(CSV_PATHS);
474
- const rows = d3.csvParse(text, d => ({ run: (d.run||'').trim(), step: +d.step, metric: (d.metric||'').trim(), value: +d.value }));
475
- metricList = Array.from(new Set(rows.map(r=>r.metric))).sort();
476
- runList = Array.from(new Set(rows.map(r=>r.run))).sort();
477
- runOrder = runList;
478
- // Build dataByMetric
479
- metricList.forEach(m => {
480
- const map = {};
481
- runList.forEach(r => { map[r] = []; });
482
- 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 }); });
 
 
 
 
 
 
 
 
483
  dataByMetric.set(m, map);
484
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
- // Populate metric select (default to average_rank if present)
487
- metricList.forEach((m)=>{ const o=document.createElement('option'); o.value=m; o.textContent=m; selectMetric.appendChild(o); });
488
- const def = metricList.find(m => /average_rank/i.test(m)) || metricList[0];
489
- if (def) selectMetric.value = def;
490
-
491
- container.appendChild(controls);
492
- updateScales();
493
- renderMetric(selectMetric.value);
494
-
495
- selectMetric.addEventListener('change', ()=>{ renderMetric(selectMetric.value); });
496
-
497
- const rerender = () => { renderMetric(selectMetric.value); };
498
- if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
499
  } catch (e) {
500
  const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
501
- pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';
502
- container.appendChild(pre);
503
  }
504
  })();
505
  };
506
 
507
- if (document.readyState === 'loading') {
508
- document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
509
- } else { ensureD3(bootstrap); }
510
  })();
511
  </script>
512
 
 
1
+ <div class="d3-line-simple"></div>
2
  <style>
3
+ .d3-line-simple { position: relative; }
4
+ /* Theme-driven axis/tick/grid variables */
5
+ .d3-line-simple {
6
+ --axis-color: rgba(0,0,0,0.25);
7
+ --tick-color: rgba(0,0,0,0.55);
8
+ --grid-color: rgba(0,0,0,0.05);
9
+ }
10
+ [data-theme="dark"] .d3-line-simple {
11
+ --axis-color: rgba(255,255,255,0.25);
12
+ --tick-color: rgba(255,255,255,0.70);
13
+ --grid-color: rgba(255,255,255,0.08);
14
+ }
15
+ .d3-line-simple .controls {
16
+ margin-top: 0;
17
+ display: flex;
18
+ gap: 16px;
19
+ align-items: center;
20
+ justify-content: flex-end;
21
+ width: auto;
22
+ flex-wrap: wrap;
23
+ }
24
+ .d3-line-simple .controls label {
25
+ font-size: 12px;
26
+ color: var(--text-color);
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 6px;
30
+ white-space: nowrap;
31
+ font-weight: 700;
32
+ }
33
+ .d3-line-simple .controls select {
34
  font-size: 12px;
35
  padding: 8px 28px 8px 10px;
36
  border: 1px solid var(--border-color);
 
38
  background-color: var(--surface-bg);
39
  color: var(--text-color);
40
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
41
+ background-repeat: no-repeat; background-position: right 8px center; background-size: 12px;
42
+ -webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;
 
 
 
 
 
 
43
  }
44
+ [data-theme="dark"] .d3-line-simple .controls select {
45
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
46
  }
47
+ .d3-line-simple .controls select:hover { border-color: var(--primary-color); }
48
+ .d3-line-simple .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
49
+ .d3-line-simple .axis-label { fill: var(--text-color); font-size: 12px; font-weight: 700; }
50
+ .d3-line-simple .axes path, .d3-line-simple .axes line { stroke: var(--axis-color); }
51
+ .d3-line-simple .axes text { fill: var(--tick-color); }
52
+ .d3-line-simple .grid line { stroke: var(--grid-color); }
53
+ .d3-line-simple .legend { font-size: 12px; color: var(--text-color);padding-left: 6px; }
54
+ .d3-line-simple .legend .items { display:flex; flex-wrap:wrap; gap:8px 12px; align-items:center; }
55
+ .d3-line-simple .legend .item { display:flex; align-items:center; gap:6px; white-space:nowrap; }
56
+ .d3-line-simple .legend .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); display:inline-block; }
57
+ /* Ghosting on hover */
58
+ .d3-line-simple.hovering .legend-bottom .item.ghost { opacity: .35; }
59
+ .d3-line-simple.hovering .lines path.ghost { opacity: .25; }
60
+ .d3-line-simple.hovering .points circle.ghost { opacity: .25; }
61
+ .d3-line-simple.hovering .areas path.ghost { opacity: .08; }
62
+ .d3-line-simple .chart-header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin: 0 0 8px 0; flex-wrap: wrap; }
63
+ .d3-line-simple .legend-bottom { display:flex; align-items:center; justify-content:flex-start; font-size:12px; color: var(--text-color); }
64
+ .d3-line-simple .legend-bottom .items { display:flex; flex-wrap:wrap; gap:8px 14px; }
65
+ .d3-line-simple .legend-bottom .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
66
+ .d3-line-simple .legend-bottom .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); display:inline-block; }
67
+ .d3-line-simple .lines path.active { stroke-width: 3; }
68
+ /* Layout tweaks: stack label above select, add legend title above items */
69
+ .d3-line-simple .controls .control-group {
70
+ display: flex;
71
+ flex-direction: column;
72
+ align-items: flex-start;
73
+ gap: 6px;
 
 
 
 
 
 
 
 
74
  }
75
+ .d3-line-simple .legend-bottom {
76
+ flex-direction: column;
77
+ align-items: flex-start;
78
+ gap: 6px;
79
  }
80
+ .d3-line-simple .legend-bottom .legend-title {
81
+ font-size: 12px;
82
+ font-weight: 700;
83
+ color: var(--text-color);
 
 
 
84
  }
85
+ /* Tooltip styling aligned with filters-quad */
86
+ .d3-line-simple .d3-tooltip { z-index: var(--z-elevated); backdrop-filter: saturate(1.12) blur(8px); }
87
+ .d3-line-simple .d3-tooltip__inner { display:flex; flex-direction:column; gap:6px; min-width: 220px; }
88
+ .d3-line-simple .d3-tooltip__inner > div:first-child { font-weight: 800; letter-spacing: 0.1px; margin-bottom: 0; }
89
+ .d3-line-simple .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--muted-color); display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.1px; }
90
+ .d3-line-simple .d3-tooltip__inner > div:nth-child(n+3) { padding-top: 6px; border-top: 1px solid var(--border-color); }
91
+ .d3-line-simple .d3-tooltip__color-dot { display:inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-color); }
92
+ /* Chart card only around the SVG */
93
+ .d3-line-simple .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
94
+ /* Place header below chart and align start */
95
+ .d3-line-simple .chart-header { display:flex; align-items:flex-start; justify-content:flex-start; gap:12px; margin: 8px 0 0 0; flex-wrap: wrap; }
96
  </style>
97
  <script>
98
  (() => {
99
+ // Pretty label mapping for metric keys (aligned with filters-quad)
100
+ const prettyMetricLabel = (key) => {
101
+ if (!key) return '';
102
+ const table = {
103
+ 'ai2d_exact_match': 'AI2D Exact Match',
104
+ 'average_rank': 'Average Rank',
105
+ };
106
+ if (table[key]) return table[key];
107
+ const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
108
+ return cleaned.split(/\s+/).map(w => {
109
+ if (/^(ai2d|umap|id|auc|f1)$/i.test(w)) return w.toUpperCase();
110
+ return w.charAt(0).toUpperCase() + w.slice(1);
111
+ }).join(' ');
112
+ };
113
+
114
  const ensureD3 = (cb) => {
115
  if (window.d3 && typeof window.d3.select === 'function') return cb();
116
  let s = document.getElementById('d3-cdn-script');
117
+ 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); }
 
 
 
 
 
118
  const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
119
+ s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
 
120
  };
121
 
122
  const bootstrap = () => {
123
  const scriptEl = document.currentScript;
124
+ let container = scriptEl ? scriptEl.previousElementSibling : null;
125
+ if (!(container && container.classList && container.classList.contains('d3-line-simple'))){
126
+ const cs = Array.from(document.querySelectorAll('.d3-line-simple')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
127
+ container = cs[cs.length - 1] || null;
 
 
 
 
 
128
  }
 
 
 
 
 
 
 
 
129
  if (!container) return;
130
+ if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
+ // Controls (we will place them in a footer below the chart)
133
  const controls = document.createElement('div');
134
+ controls.className = 'controls';
135
+ const controlGroup = document.createElement('div');
136
+ controlGroup.className = 'control-group';
 
 
 
 
 
 
 
137
  const labelMetric = document.createElement('label');
 
 
 
138
  labelMetric.textContent = 'Metric';
139
  const selectMetric = document.createElement('select');
140
+ // Associate label and select with a unique id
141
+ const uniqueId = Math.random().toString(36).slice(2, 9);
142
+ selectMetric.id = `metric-select-${uniqueId}`;
143
+ labelMetric.setAttribute('for', selectMetric.id);
144
+ controlGroup.appendChild(labelMetric);
145
+ controlGroup.appendChild(selectMetric);
146
+ controls.appendChild(controlGroup);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  // Tooltip
149
  container.style.position = container.style.position || 'relative';
150
+ let tip = container.querySelector('.d3-tooltip'); let tipInner;
 
151
  if (!tip) {
152
+ tip = document.createElement('div'); tip.className = 'd3-tooltip';
 
153
  Object.assign(tip.style, {
154
+ position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
155
+ padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
156
+ background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease'
 
157
  });
158
+ tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
159
+ } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
160
+
161
+ // Header (legend + controls) placed after the chart
162
+ const header = document.createElement('div'); header.className = 'chart-header';
163
+ const legendBottom = document.createElement('div'); legendBottom.className = 'legend-bottom'; header.appendChild(legendBottom);
164
+ header.appendChild(controls);
165
+
166
+ // Chart card (SVG)
167
+ const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
168
+ container.appendChild(header);
169
+ // SVG
170
+ const svg = d3.select(card).append('svg').attr('width','100%').style('display','block');
171
+ const gRoot = svg.append('g');
172
+ const gGrid = gRoot.append('g').attr('class','grid');
173
+ const gAxes = gRoot.append('g').attr('class','axes');
174
+ const gAreas = gRoot.append('g').attr('class','areas');
175
+ const gLines = gRoot.append('g').attr('class','lines');
176
+ const gPoints = gRoot.append('g').attr('class','points');
177
+ // (legend removed from inside SVG)
178
+ const overlay = gRoot.append('rect').attr('fill','transparent').style('cursor','crosshair');
179
+ const hoverLine = gRoot.append('line').attr('stroke-width',1).style('display','none');
180
+
181
+ // State/data
182
+ let width = 800, height = 480; const margin = { top: 16, right: 32, bottom: 44, left: 56 };
183
+ const xScale = d3.scaleLinear();
184
+ const yScale = d3.scaleLinear();
185
+ const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
186
+ const dataByMetric = new Map();
187
  let runOrder = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ // Colors
190
+ function getRunColors(count){
191
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', count); } catch(_){}
192
+ return d3.schemeTableau10 ? d3.schemeTableau10.slice(0, count) : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'].slice(0, count);
193
+ }
194
+ // Format helper for thousands (5000 -> 5k, 1500 -> 1.5k)
195
+ function formatK(v){
196
+ const abs = Math.abs(v);
197
+ if (abs >= 1000) {
198
+ const n = v / 1000;
199
+ const s = d3.format('.1f')(n);
200
+ return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
202
+ return d3.format('d')(v);
203
  }
204
 
205
+ // CSV: prefer public path, fallback to relative
206
+ // Data file(s) from HtmlEmbed attribute (string or JSON array), else default cascade
207
+ // Find the HtmlEmbed wrapper that carries data-datafiles
208
+ let mountEl = container;
209
+ while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
210
+ mountEl = mountEl.parentElement;
211
+ }
212
+ let providedData = null;
213
+ let providedConfig = null;
214
+ try {
215
+ const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
216
+ if (attr && attr.trim()) {
217
+ providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
218
+ }
219
+ } catch(_) {}
220
+ try {
221
+ const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
222
+ if (cfg && cfg.trim()) {
223
+ providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
224
+ }
225
+ } catch(_) {}
226
+ const DEFAULT_CSV = '/data/formatting_filters.csv';
227
+ // Normalize: if a provided path has no slash, assume it lives under /data/
228
+ const ensureDataPrefix = (p) => {
229
+ if (typeof p !== 'string' || !p) return p;
230
+ return p.includes('/') ? p : `/data/${p}`;
231
+ };
232
+ const normalizeInput = (inp) => Array.isArray(inp) ? inp.map(ensureDataPrefix) : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
233
+ const CSV_PATHS = Array.isArray(providedData)
234
+ ? normalizeInput(providedData)
235
+ : (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [
236
+ DEFAULT_CSV,
237
+ './assets/data/formatting_filters.csv',
238
+ '../assets/data/formatting_filters.csv',
239
+ '../../assets/data/formatting_filters.csv'
240
+ ]);
241
+ const fetchFirstAvailable = async (paths) => {
242
+ for (const p of paths) { try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(_){} }
243
+ throw new Error('CSV not found: formatting_filters.csv');
244
+ };
245
 
246
+ function updateLayout(){
247
+ const axisColor = getComputedStyle(container).getPropertyValue('--axis-color').trim() || 'rgba(0,0,0,0.25)';
248
  width = container.clientWidth || 800;
249
+ height = Math.max(280, Math.round(width / 3));
250
  svg.attr('width', width).attr('height', height);
251
+ gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
 
 
 
252
  const innerWidth = width - margin.left - margin.right;
253
  const innerHeight = height - margin.top - margin.bottom;
254
+ overlay.attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
255
+ hoverLine.attr('y1',0).attr('y2', innerHeight).attr('stroke', axisColor);
256
+ return { innerWidth, innerHeight };
257
+ }
 
 
 
 
 
 
 
 
 
 
258
 
259
+ function render(metricKey){
260
+ const { innerWidth, innerHeight } = updateLayout();
261
+ const map = dataByMetric.get(metricKey) || {};
262
+ const runs = runOrder;
263
+ // domains
264
+ let minStep = Infinity, maxStep = -Infinity, minV = Infinity, maxV = -Infinity;
265
+ runs.forEach(r => { (map[r]||[]).forEach(pt => { minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); minV = Math.min(minV, pt.value); maxV = Math.max(maxV, pt.value); }); });
266
+ if (!isFinite(minStep) || !isFinite(maxStep)) return;
267
+ xScale.domain([minStep, maxStep]).range([0, innerWidth]);
268
+ yScale.domain([minV, maxV]).nice().range([innerHeight, 0]);
269
+
270
+ // grid
271
  gGrid.selectAll('*').remove();
272
+ gGrid.selectAll('line').data(yScale.ticks(6)).join('line')
273
+ .attr('x1',0).attr('x2', innerWidth).attr('y1', d=>yScale(d)).attr('y2', d=>yScale(d))
274
+ .attr('stroke','var(--grid-color)').attr('stroke-width',1).attr('shape-rendering','crispEdges');
 
 
 
 
 
 
 
275
 
276
+ // axes
277
  gAxes.selectAll('*').remove();
278
+ gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(xScale).ticks(8).tickFormat(formatK)).call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','12px'); });
279
+ gAxes.append('g').call(d3.axisLeft(yScale).ticks(6)).call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','12px'); });
280
+ gAxes.append('text').attr('class','axis-label').attr('text-anchor','middle').attr('x', innerWidth/2).attr('y', innerHeight + 38).text('Step');
281
+ gAxes.append('text').attr('class','axis-label').attr('text-anchor','middle').attr('transform', `translate(${-44}, ${innerHeight/2}) rotate(-90)`).text('Value');
282
+
283
+ // shaded areas (stderr or min/max if provided)
284
+ gAreas.selectAll('*').remove();
285
+ const areaGenClosed = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed);
286
+ // Colors (needed for both areas and lines)
287
+ const colors = getRunColors(runs.length);
288
+ const seriesForAreas = [];
289
+ runs.forEach((r, i) => {
290
+ const vals = (map[r]||[]).slice().sort((a,b)=>a.step-b.step);
291
+ const withBounds = vals.map(v => {
292
+ let lo = null, hi = null;
293
+ if (v.stderr != null && isFinite(v.stderr) && v.stderr > 0) { lo = v.value - v.stderr; hi = v.value + v.stderr; }
294
+ if (v.min != null && isFinite(v.min)) lo = (lo==null) ? v.min : lo;
295
+ if (v.max != null && isFinite(v.max)) hi = (hi==null) ? v.max : hi;
296
+ return { step: v.step, lo, hi };
297
+ }).filter(v => v.lo != null && v.hi != null && isFinite(v.lo) && isFinite(v.hi));
298
+ if (withBounds.length > 0) {
299
+ seriesForAreas.push({ run: r, color: colors[i % colors.length], bounds: withBounds });
300
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  });
302
+ seriesForAreas.forEach(s => {
303
+ const upper = s.bounds.map(d => [ xScale(d.step), yScale(d.hi) ]);
304
+ const lower = s.bounds.slice().reverse().map(d => [ xScale(d.step), yScale(d.lo) ]);
305
+ const coords = upper.concat(lower);
306
+ gAreas.append('path')
307
+ .attr('class','area')
308
+ .attr('data-run', s.run)
309
+ .attr('d', areaGenClosed(coords))
310
+ .attr('fill', s.color)
311
+ .attr('opacity', 0.15)
312
+ .attr('stroke','none');
 
 
 
 
 
 
 
 
 
 
313
  });
 
 
 
 
 
 
 
 
 
 
 
314
 
315
+ // lines
316
+ const series = runs.map((r, i) => ({ run:r, color: colors[i % colors.length], values: (map[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
317
+ const paths = gLines.selectAll('path.run').data(series, d=>d.run);
318
+ const pathsEnter = paths.enter().append('path').attr('class','run').attr('fill','none').attr('stroke-width',2).attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
319
+ pathsEnter.merge(paths).transition().duration(200).attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  paths.exit().remove();
321
 
322
+ // Hover capture paths (wider invisible stroke for easy hover)
323
+ const captures = gLines.selectAll('path.run-hover').data(series, d=>`cap-${d.run}`);
324
+ captures.enter().append('path').attr('class','run-hover').attr('fill','none').attr('stroke','transparent').attr('stroke-width', 12).style('pointer-events','stroke')
325
+ .attr('d', d=>lineGen(d.values))
326
+ .merge(captures)
327
+ .attr('d', d=>lineGen(d.values))
328
+ .on('mouseenter', function(ev, d){
329
+ container.classList.add('hovering');
330
+ // ghost non hovered lines and points
331
+ gLines.selectAll('path.run').classed('ghost', s => s.run !== d.run);
332
+ gPoints.selectAll('circle.pt').classed('ghost', p => p.run !== d.run);
333
+ gAreas.selectAll('path.area').classed('ghost', a => a && a.getAttribute && a.getAttribute('data-run') !== d.run);
334
+ // ghost legend items
335
+ try {
336
+ const legendNode = legendBottom;
337
+ if (legendNode) {
338
+ legendNode.querySelectorAll('.item').forEach(el => {
339
+ const name = el.getAttribute('data-run');
340
+ el.classList.toggle('ghost', name !== d.run);
341
+ });
342
+ }
343
+ } catch {}
344
+ })
345
+ .on('mouseleave', function(){
346
+ container.classList.remove('hovering');
347
+ gLines.selectAll('path.run').classed('ghost', false);
348
+ gPoints.selectAll('circle.pt').classed('ghost', false);
349
+ gAreas.selectAll('path.area').classed('ghost', false);
350
+ try { const legendNode = legendBottom; if (legendNode) legendNode.querySelectorAll('.item').forEach(el => el.classList.remove('ghost')); } catch {}
351
+ });
352
+ captures.exit().remove();
353
+
354
+ // point markers (subtle)
355
+ const allPts = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
356
+ const ptsSel = gPoints.selectAll('circle.pt').data(allPts, d=>`${d.run}-${d.step}`);
357
+ ptsSel.enter().append('circle').attr('class','pt').attr('r', 2).attr('fill', d=>d.color).attr('fill-opacity', 0.6)
358
+ .attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value))
359
+ .merge(ptsSel).transition().duration(150).attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value));
360
+ ptsSel.exit().remove();
361
+
362
+ // legend (HTML below, left) with title above items
363
+ legendBottom.innerHTML = `<div class="legend-title">Legend</div><div class="items">${series.map(s => `<span class="item" data-run="${s.run}"><span class="swatch" style="background:${s.color}"></span><span>${s.run}</span></span>`).join('')}</div>`;
364
+ // Legend hover → ghost lines/points
365
+ try {
366
+ const legendNode = legendBottom;
367
+ legendNode.querySelectorAll('.item').forEach(el => {
368
+ el.addEventListener('mouseenter', () => {
369
+ const run = el.getAttribute('data-run'); if (!run) return;
370
+ container.classList.add('hovering');
371
+ gLines.selectAll('path.run').classed('ghost', s => s.run !== run);
372
+ gPoints.selectAll('circle.pt').classed('ghost', p => p.run !== run);
373
+ gAreas.selectAll('path.area').classed('ghost', a => a && a.getAttribute && a.getAttribute('data-run') !== run);
374
+ legendNode.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
375
+ });
376
+ el.addEventListener('mouseleave', () => {
377
+ container.classList.remove('hovering');
378
+ gLines.selectAll('path.run').classed('ghost', false);
379
+ gPoints.selectAll('circle.pt').classed('ghost', false);
380
+ gAreas.selectAll('path.area').classed('ghost', false);
381
+ legendNode.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
382
+ });
383
+ });
384
+ } catch {}
385
+
386
+ // hover
387
+ function onMove(ev){
388
+ const [mx, my] = d3.pointer(ev, overlay.node());
389
+ const sx = xScale.invert(mx);
390
+ // nearest integer step if steps are integers; else nearest by distance
391
+ const steps = Array.from(new Set(allPts.map(p=>p.step))).sort((a,b)=>a-b);
392
+ const nearest = steps.reduce((best, s) => Math.abs(s - sx) < Math.abs(best - sx) ? s : best, steps[0]);
393
  const xpx = xScale(nearest);
394
+ hoverLine.style('display', null).attr('x1', xpx).attr('x2', xpx);
395
+ // tooltip content (styled)
396
+ let html = `<div style=\"font-weight:800;letter-spacing:.1px;\">${metricKey}</div><div style=\"font-size:11px;color:var(--muted-color);margin-top:-4px;margin-bottom:2px;\">Step ${formatK(nearest)}</div>`;
397
+ const entries = series.map(s => {
398
  const m = new Map(s.values.map(v=>[v.step, v.value]));
399
+ const val = m.get(nearest);
400
+ return { run: s.run, color: s.color, val };
401
+ }).filter(e => e.val != null);
402
+ entries.sort((a, b) => a.val - b.val);
403
+ entries.forEach(e => {
404
+ html += `<div style=\"display:flex;align-items:center;gap:6px;white-space:nowrap;\"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;\">${(+e.val).toFixed(4)}</span></div>`;
405
  });
406
+ tipInner.innerHTML = html; tip.style.opacity = '1'; tip.style.transform = `translate(${Math.round(mx + margin.left + 12)}px, ${Math.round(my + margin.top + 12)}px)`;
 
 
407
  }
408
  function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
409
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
410
  }
411
 
412
+ // load CSV and init
 
 
413
  (async () => {
414
  try {
415
  const text = await fetchFirstAvailable(CSV_PATHS);
416
+ const rows = d3.csvParse(text, d => ({
417
+ run:(d.run||'').trim(),
418
+ step:+d.step,
419
+ metric:(d.metric||'').trim(),
420
+ value:+d.value,
421
+ // Optional bounds: stderr or [min,max] or [lower,upper]
422
+ stderr: (d.stderr!=null && d.stderr!=='') ? +d.stderr : null,
423
+ min: (d.min!=null && d.min!=='') ? +d.min : (d.lower!=null && d.lower!=='') ? +d.lower : null,
424
+ max: (d.max!=null && d.max!=='') ? +d.max : (d.upper!=null && d.upper!=='') ? +d.upper : null
425
+ }));
426
+ const metrics = Array.from(new Set(rows.map(r=>r.metric))).sort();
427
+ runOrder = Array.from(new Set(rows.map(r=>r.run))).sort();
428
+ metrics.forEach(m => {
429
+ const map = {}; runOrder.forEach(r => map[r] = []);
430
+ rows.filter(r=>r.metric===m).forEach(r => {
431
+ if (!isNaN(r.step) && !isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value, stderr: r.stderr, min: r.min, max: r.max });
432
+ });
433
  dataByMetric.set(m, map);
434
  });
435
+ // populate metric select (pretty labels) or hide if single-file with single metric
436
+ const isSingleFile = !Array.isArray(providedData) && typeof providedData === 'string';
437
+ metrics.forEach(m => { const o = document.createElement('option'); o.value=m; o.textContent=prettyMetricLabel(m); selectMetric.appendChild(o); });
438
+ // default metric selection via config.defaultMetric (match raw key or pretty label, case-insensitive)
439
+ if (metrics.length) {
440
+ let initial = metrics[0];
441
+ const desired = providedConfig && typeof providedConfig === 'object' && providedConfig.defaultMetric ? String(providedConfig.defaultMetric) : null;
442
+ if (desired) {
443
+ const lcDesired = desired.toLowerCase();
444
+ const byKey = metrics.find(m => m.toLowerCase() === lcDesired);
445
+ const byPretty = metrics.find(m => prettyMetricLabel(m).toLowerCase() === lcDesired);
446
+ initial = byKey || byPretty || initial;
447
+ }
448
+ selectMetric.value = initial;
449
+ }
450
+ if (isSingleFile && metrics.length <= 1) {
451
+ controls.style.display = 'none';
452
+ }
453
 
454
+ render(selectMetric.value);
455
+ selectMetric.addEventListener('change', () => render(selectMetric.value));
456
+ const rerender = () => render(selectMetric.value);
457
+ if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
 
 
 
 
 
 
 
 
 
458
  } catch (e) {
459
  const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
460
+ pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; container.appendChild(pre);
 
461
  }
462
  })();
463
  };
464
 
465
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
 
 
466
  })();
467
  </script>
468
 
app/src/content/embeds/d3-matrix.html ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="d3-matrix" ></div>
2
+ <style>
3
+ .d3-matrix {
4
+ position: relative;
5
+ }
6
+ .d3-matrix .panels {
7
+ display: flex;
8
+ flex-wrap: wrap;
9
+ gap: 16px;
10
+ margin-bottom: 4px;
11
+ }
12
+ .d3-matrix .panel {
13
+ flex: 1 1 320px;
14
+ min-width: 280px;
15
+ }
16
+ .d3-matrix .panel__title {
17
+ color: var(--text-color);
18
+ font-size: 12px;
19
+ line-height: 1.35;
20
+ margin: 0 0 6px 0;
21
+ font-weight: 600;
22
+ }
23
+ .d3-matrix .axis-label {
24
+ fill: var(--text-color);
25
+ font-size: 11px;
26
+ font-weight: 700;
27
+ }
28
+ .d3-matrix .cell-border {
29
+ stroke: var(--border-color);
30
+ stroke-width: 1px;
31
+ fill: none;
32
+ }
33
+ .d3-matrix .cell-text {
34
+ fill: var(--muted-color);
35
+ font-size: 11px;
36
+ pointer-events: none;
37
+ }
38
+ .d3-matrix .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
39
+ </style>
40
+ <script>
41
+ (() => {
42
+ // Load D3 from CDN once
43
+ const ensureD3 = (cb) => {
44
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
45
+ let s = document.getElementById('d3-cdn-script');
46
+ if (!s) {
47
+ s = document.createElement('script');
48
+ s.id = 'd3-cdn-script';
49
+ s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
50
+ document.head.appendChild(s);
51
+ }
52
+ const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
53
+ s.addEventListener('load', onReady, { once: true });
54
+ if (window.d3) onReady();
55
+ };
56
+
57
+ const bootstrap = () => {
58
+ const scriptEl = document.currentScript;
59
+ let container = scriptEl ? scriptEl.previousElementSibling : null;
60
+ if (!(container && container.classList && container.classList.contains('d3-matrix'))){
61
+ const cs = Array.from(document.querySelectorAll('.d3-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
62
+ container = cs[cs.length - 1] || null;
63
+ }
64
+ if (!container) return;
65
+ if (container.dataset) {
66
+ if (container.dataset.mounted === 'true') return;
67
+ container.dataset.mounted = 'true';
68
+ }
69
+
70
+ // Tooltip (HTML, single instance inside container)
71
+ container.style.position = container.style.position || 'relative';
72
+ let tip = container.querySelector('.d3-tooltip');
73
+ let tipInner;
74
+ if (!tip) {
75
+ tip = document.createElement('div');
76
+ tip.className = 'd3-tooltip';
77
+ Object.assign(tip.style, {
78
+ position: 'absolute',
79
+ top: '0px',
80
+ left: '0px',
81
+ transform: 'translate(-9999px, -9999px)',
82
+ pointerEvents: 'none',
83
+ padding: '8px 10px',
84
+ borderRadius: '8px',
85
+ fontSize: '12px',
86
+ lineHeight: '1.35',
87
+ border: '1px solid var(--border-color)',
88
+ background: 'var(--surface-bg)',
89
+ color: 'var(--text-color)',
90
+ boxShadow: '0 4px 24px rgba(0,0,0,.18)',
91
+ opacity: '0',
92
+ transition: 'opacity .12s ease'
93
+ });
94
+ tipInner = document.createElement('div');
95
+ tipInner.className = 'd3-tooltip__inner';
96
+ tipInner.style.textAlign = 'left';
97
+ tip.appendChild(tipInner);
98
+ container.appendChild(tip);
99
+ } else {
100
+ tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
101
+ }
102
+
103
+ // Panels container (two side-by-side matrices)
104
+ const panels = document.createElement('div');
105
+ panels.className = 'panels';
106
+ const panelA = document.createElement('div');
107
+ panelA.className = 'panel';
108
+ const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';
109
+ panelA.appendChild(titleA);
110
+ const mountA = document.createElement('div'); panelA.appendChild(mountA);
111
+ const panelB = document.createElement('div');
112
+ panelB.className = 'panel';
113
+ const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved − Baseline, pp)';
114
+ panelB.appendChild(titleB);
115
+ const mountB = document.createElement('div'); panelB.appendChild(mountB);
116
+ panels.appendChild(panelA);
117
+ panels.appendChild(panelB);
118
+ container.appendChild(panels);
119
+
120
+ // SVG scaffolding
121
+ const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);
122
+ const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');
123
+ const gRootA = svgA.append('g');
124
+ const gCellsA = gRootA.append('g');
125
+ const gAxesA = gRootA.append('g');
126
+ const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);
127
+ const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');
128
+ const gRootB = svgB.append('g');
129
+ const gCellsB = gRootB.append('g');
130
+ const gAxesB = gRootB.append('g');
131
+
132
+ // Demo data (two distinct 10x10 matrices: Baseline vs Improved)
133
+ // Rows / Columns are generic class labels
134
+ const classes = ['0','1','2','3','4','5','6','7','8','9'];
135
+ const matrixA = [
136
+ [90, 2, 1, 0, 0, 0, 1, 0, 5, 1],
137
+ [ 3, 85, 5, 1, 0, 1, 2, 1, 1, 1],
138
+ [ 1, 6, 70, 10, 4, 4, 1, 1, 1, 2],
139
+ [ 0, 1, 8, 65, 10, 10, 2, 1, 1, 2],
140
+ [ 0, 0, 2, 6, 83, 3, 1, 1, 3, 1],
141
+ [ 0, 1, 2, 12, 4, 70, 5, 2, 2, 2],
142
+ [ 1, 2, 1, 0, 1, 2, 88, 1, 3, 1],
143
+ [ 0, 1, 1, 1, 1, 1, 2, 90, 1, 2],
144
+ [ 6, 2, 2, 4, 6, 3, 3, 2, 70, 2],
145
+ [ 1, 1, 1, 1, 2, 1, 1, 2, 1, 89]
146
+ ];
147
+ const matrixB = [
148
+ [94, 1, 0, 0, 0, 0, 1, 0, 3, 1],
149
+ [ 2, 90, 3, 1, 0, 0, 1, 1, 1, 1],
150
+ [ 1, 4, 78, 7, 3, 3, 1, 1, 1, 1],
151
+ [ 0, 1, 5, 74, 7, 8, 1, 1, 1, 2],
152
+ [ 0, 0, 1, 4, 88, 2, 1, 1, 2, 1],
153
+ [ 0, 1, 1, 9, 3, 78, 3, 1, 2, 2],
154
+ [ 1, 1, 1, 0, 1, 1, 91, 1, 2, 1],
155
+ [ 0, 1, 1, 1, 1, 1, 1, 92, 1, 1],
156
+ [ 4, 1, 1, 3, 4, 2, 2, 2, 79, 2],
157
+ [ 1, 1, 1, 1, 2, 1, 1, 1, 1, 90]
158
+ ];
159
+
160
+ // Colors: sequential palette via window.ColorPalettes with graceful fallback
161
+ const getSequentialColors = (count) => {
162
+ try {
163
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
164
+ return window.ColorPalettes.getColors('sequential', count);
165
+ }
166
+ } catch (_) {}
167
+ // Fallback: generate a monochrome scale using the primary color with varying opacity
168
+ const arr = [];
169
+ for (let i = 0; i < count; i++) arr.push('var(--primary-color)');
170
+ return arr;
171
+ };
172
+
173
+ const palette = getSequentialColors(13);
174
+ const getDivergingColors = (count) => {
175
+ try {
176
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
177
+ return window.ColorPalettes.getColors('diverging', count);
178
+ }
179
+ } catch (_) {}
180
+ const steps = Math.max(3, count|0);
181
+ const arr = [];
182
+ for (let i = 0; i < steps; i++) {
183
+ const t = i / (steps - 1);
184
+ const pct = Math.round(t * 100);
185
+ arr.push(`color-mix(in srgb, #D64545 ${100-pct}%, #3A7BD5 ${pct}%)`);
186
+ }
187
+ return arr;
188
+ };
189
+
190
+ let width = 800;
191
+ let height = 480;
192
+ const margin = { top: 36, right: 24, bottom: 26, left: 56 };
193
+
194
+ function updateSize() {
195
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
196
+ width = container.clientWidth || 800;
197
+ const gap = 16; // matches CSS .panels gap
198
+ const minPanel = 320;
199
+ const nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;
200
+ const panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);
201
+ const base = Math.max(minPanel, Math.round(panelWidth * 0.92));
202
+ height = base;
203
+ // Responsive SVG: width 100%, height auto, preserve aspect via viewBox
204
+ svgA
205
+ .attr('viewBox', `0 0 ${panelWidth} ${height}`)
206
+ .attr('preserveAspectRatio', 'xMidYMid meet')
207
+ .style('width', '100%')
208
+ .style('height', 'auto');
209
+ svgB
210
+ .attr('viewBox', `0 0 ${panelWidth} ${height}`)
211
+ .attr('preserveAspectRatio', 'xMidYMid meet')
212
+ .style('width', '100%')
213
+ .style('height', 'auto');
214
+ gRootA.attr('transform', `translate(${margin.left},${margin.top})`);
215
+ gRootB.attr('transform', `translate(${margin.left},${margin.top})`);
216
+ const innerWidth = panelWidth - margin.left - margin.right;
217
+ const innerHeight = height - margin.top - margin.bottom;
218
+ return { innerWidth, innerHeight, isDark };
219
+ }
220
+
221
+ function computeValues(normalization, matrix) {
222
+ const n = classes.length;
223
+ const totalsByRow = matrix.map(row => row.reduce((a, b) => a + b, 0));
224
+ const flat = [];
225
+ let minV = Infinity, maxV = -Infinity;
226
+ for (let r = 0; r < n; r++) {
227
+ for (let c = 0; c < n; c++) {
228
+ const count = matrix[r][c];
229
+ const value = normalization === 'row' ? (totalsByRow[r] ? count / totalsByRow[r] : 0) : count;
230
+ if (value < minV) minV = value;
231
+ if (value > maxV) maxV = value;
232
+ flat.push({ r, c, count, value });
233
+ }
234
+ }
235
+ return { data: flat, minV, maxV };
236
+ }
237
+
238
+ function getColorScale(values, minV, maxV) {
239
+ // If ColorPalettes is available, use quantiles to enhance visual variation across the distribution
240
+ const hasPalette = !(palette.length === 0);
241
+ if (hasPalette && (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')) {
242
+ const scale = d3.scaleQuantile().domain(values).range(palette);
243
+ return (v) => scale(v);
244
+ }
245
+ // Fallback: primary color with opacity mapped to normalized value
246
+ const norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);
247
+ return (v) => `color-mix(in oklab, var(--primary-color) ${Math.round(norm(v) * 100)}%, var(--surface-bg))`;
248
+ }
249
+
250
+ // Compute a fixed readable text color from a CSS rgb()/rgba() string
251
+ function chooseFixedReadableTextOnBg(bgCss){
252
+ try {
253
+ const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/);
254
+ if (!m) return '#0e1116';
255
+ const parts = m[1].split(',').map(s => parseFloat(s.trim()));
256
+ const [r, g, b] = parts;
257
+ // sRGB → relative luminance
258
+ const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);
259
+ const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4)));
260
+ const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2];
261
+ // Threshold ~ 0.5 for readability; darker BG → white text, else near-black
262
+ return L < 0.5 ? '#ffffff' : '#0e1116';
263
+ } catch(_) { return '#0e1116'; }
264
+ }
265
+
266
+ function render() {
267
+ const { innerWidth, innerHeight } = updateSize();
268
+ const n = classes.length;
269
+ const gridSize = Math.min(innerWidth, innerHeight);
270
+ const cellSize = gridSize / n;
271
+
272
+ const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
273
+ const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
274
+
275
+ // Panel A: Baseline (row-normalized)
276
+ const dataA = computeValues('row', matrixA);
277
+ const colorA = getColorScale(dataA.data.map(d => d.value), dataA.minV, dataA.maxV);
278
+
279
+ gCellsA.selectAll('rect.cell-bg')
280
+ .data([0])
281
+ .join('rect')
282
+ .attr('class', 'cell-bg')
283
+ .attr('x', 0)
284
+ .attr('y', 0)
285
+ .attr('width', gridSize)
286
+ .attr('height', gridSize)
287
+ .attr('fill', 'none')
288
+ .attr('stroke', 'var(--border-color)')
289
+ .attr('stroke-width', 1);
290
+
291
+ const cellsA = gCellsA.selectAll('g.cell')
292
+ .data(dataA.data, d => `${d.r}-${d.c}-A`);
293
+
294
+ const cellsEnterA = cellsA.enter()
295
+ .append('g')
296
+ .attr('class', 'cell');
297
+
298
+ cellsEnterA.append('rect')
299
+ .attr('rx', 2)
300
+ .attr('ry', 2)
301
+ .on('mousemove', (event, d) => {
302
+ const [px, py] = d3.pointer(event, container);
303
+ tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong><br/>${(d.value * 100).toFixed(1)}% (${d.count})`;
304
+ tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
305
+ tip.style.opacity = '1';
306
+ })
307
+ .on('mouseleave', () => {
308
+ tip.style.opacity = '0';
309
+ });
310
+
311
+ cellsEnterA.append('text')
312
+ .attr('class', 'cell-text')
313
+ .attr('text-anchor', 'middle')
314
+ .attr('dominant-baseline', 'middle');
315
+
316
+ const cellsMergedA = cellsEnterA.merge(cellsA);
317
+
318
+ cellsMergedA.select('text')
319
+ .attr('x', d => x(d.c) + x.bandwidth() / 2)
320
+ .attr('y', d => y(d.r) + y.bandwidth() / 2)
321
+ .text(d => `${Math.round(d.value * 100)}`)
322
+ .style('fill', function(d){
323
+ try {
324
+ const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
325
+ const bg = rect ? getComputedStyle(rect).fill : colorA(d.value);
326
+ return chooseFixedReadableTextOnBg(bg);
327
+ } catch (_) {
328
+ return '#0e1116';
329
+ }
330
+ });
331
+
332
+ cellsMergedA.select('rect')
333
+ .attr('x', d => x(d.c))
334
+ .attr('y', d => y(d.r))
335
+ .attr('width', Math.max(1, x.bandwidth()))
336
+ .attr('height', Math.max(1, y.bandwidth()))
337
+ .attr('fill', d => colorA(d.value));
338
+
339
+ cellsA.exit().remove();
340
+
341
+ gAxesA.selectAll('*').remove();
342
+
343
+ gAxesA.append('g')
344
+ .selectAll('text')
345
+ .data(classes)
346
+ .join('text')
347
+ .attr('class', 'axis-label')
348
+ .attr('text-anchor', 'middle')
349
+ .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
350
+ .attr('y', -8)
351
+ .text(d => d);
352
+
353
+ gAxesA.append('g')
354
+ .selectAll('text')
355
+ .data(classes)
356
+ .join('text')
357
+ .attr('class', 'axis-label')
358
+ .attr('text-anchor', 'end')
359
+ .attr('x', -8)
360
+ .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
361
+ .attr('dominant-baseline', 'middle')
362
+ .text(d => d);
363
+
364
+ gAxesA.append('text')
365
+ .attr('class', 'axis-label')
366
+ .attr('text-anchor', 'middle')
367
+ .attr('x', gridSize / 2)
368
+ .attr('y', innerHeight + 20)
369
+ .text('Columns');
370
+
371
+ gAxesA.append('text')
372
+ .attr('class', 'axis-label')
373
+ .attr('text-anchor', 'middle')
374
+ .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
375
+ .text('Rows');
376
+
377
+ // Panel B: Delta (Improved �� Baseline), row-normalized differences in percentage points
378
+ const dataB = computeValues('row', matrixB);
379
+ const diverging = getDivergingColors(13);
380
+ // Build delta values aligned to A's ordering
381
+ const mapA = new Map(dataA.data.map(d => [d.r + '-' + d.c, d.value]));
382
+ const delta = dataB.data.map(d => ({ r: d.r, c: d.c, count: d.count, value: (d.value - (mapA.get(d.r + '-' + d.c) || 0)) }));
383
+ // Symmetric domain around 0 (in proportions), express later as pp in labels
384
+ const maxAbsDelta = Math.max(0.01, d3.max(delta, d => Math.abs(d.value)) || 0.01);
385
+ const colorB = d3.scaleQuantize().domain([-maxAbsDelta, maxAbsDelta]).range(diverging);
386
+
387
+ gCellsB.selectAll('rect.cell-bg')
388
+ .data([0])
389
+ .join('rect')
390
+ .attr('class', 'cell-bg')
391
+ .attr('x', 0)
392
+ .attr('y', 0)
393
+ .attr('width', gridSize)
394
+ .attr('height', gridSize)
395
+ .attr('fill', 'none')
396
+ .attr('stroke', 'var(--border-color)')
397
+ .attr('stroke-width', 1);
398
+
399
+ const cellsB = gCellsB.selectAll('g.cell')
400
+ .data(dataB.data, d => `${d.r}-${d.c}-B`);
401
+
402
+ const cellsEnterB = cellsB.enter()
403
+ .append('g')
404
+ .attr('class', 'cell');
405
+
406
+ cellsEnterB.append('rect')
407
+ .attr('rx', 2)
408
+ .attr('ry', 2)
409
+ .on('mousemove', (event, d) => {
410
+ const [px, py] = d3.pointer(event, container);
411
+ const a = dataA.data.find(x => x.r===d.r && x.c===d.c);
412
+ const b = dataB.data.find(x => x.r===d.r && x.c===d.c);
413
+ const dv = ((b ? b.value : 0) - (a ? a.value : 0)) * 100;
414
+ tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong>` +
415
+ `<br/>baseline ${(a ? a.value*100 : 0).toFixed(1)}%` +
416
+ `<br/>improved ${(b ? b.value*100 : 0).toFixed(1)}%` +
417
+ `<br/>delta ${dv.toFixed(1)} pp`;
418
+ tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
419
+ tip.style.opacity = '1';
420
+ })
421
+ .on('mouseleave', () => {
422
+ tip.style.opacity = '0';
423
+ });
424
+
425
+ cellsEnterB.append('text')
426
+ .attr('class', 'cell-text')
427
+ .attr('text-anchor', 'middle')
428
+ .attr('dominant-baseline', 'middle');
429
+
430
+ const cellsMergedB = cellsEnterB.merge(cellsB);
431
+
432
+ cellsMergedB.select('rect')
433
+ .attr('x', d => x(d.c))
434
+ .attr('y', d => y(d.r))
435
+ .attr('width', Math.max(1, x.bandwidth()))
436
+ .attr('height', Math.max(1, y.bandwidth()))
437
+ .attr('fill', d => colorB(delta.find(x => x.r===d.r && x.c===d.c).value));
438
+
439
+ cellsMergedB.select('text')
440
+ .attr('x', d => x(d.c) + x.bandwidth() / 2)
441
+ .attr('y', d => y(d.r) + y.bandwidth() / 2)
442
+ .text(d => {
443
+ const dv = delta.find(x => x.r===d.r && x.c===d.c).value; return `${Math.round(dv * 100)}`;
444
+ })
445
+ .style('fill', function(d){
446
+ try {
447
+ const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
448
+ const dv = delta.find(x => x.r===d.r && x.c===d.c).value;
449
+ const bg = rect ? getComputedStyle(rect).fill : colorB(dv);
450
+ return chooseFixedReadableTextOnBg(bg);
451
+ } catch (_) {
452
+ return '#0e1116';
453
+ }
454
+ });
455
+
456
+ cellsB.exit().remove();
457
+
458
+ gAxesB.selectAll('*').remove();
459
+
460
+ gAxesB.append('g')
461
+ .selectAll('text')
462
+ .data(classes)
463
+ .join('text')
464
+ .attr('class', 'axis-label')
465
+ .attr('text-anchor', 'middle')
466
+ .attr('x', (_, i) => x(i) + x.bandwidth() / 2)
467
+ .attr('y', -8)
468
+ .text(d => d);
469
+
470
+ gAxesB.append('g')
471
+ .selectAll('text')
472
+ .data(classes)
473
+ .join('text')
474
+ .attr('class', 'axis-label')
475
+ .attr('text-anchor', 'end')
476
+ .attr('x', -8)
477
+ .attr('y', (_, i) => y(i) + y.bandwidth() / 2)
478
+ .attr('dominant-baseline', 'middle')
479
+ .text(d => d);
480
+
481
+ gAxesB.append('text')
482
+ .attr('class', 'axis-label')
483
+ .attr('text-anchor', 'middle')
484
+ .attr('x', gridSize / 2)
485
+ .attr('y', innerHeight + 20)
486
+ .text('Columns');
487
+
488
+ gAxesB.append('text')
489
+ .attr('class', 'axis-label')
490
+ .attr('text-anchor', 'middle')
491
+ .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
492
+ .text('Rows');
493
+ }
494
+
495
+ // Initial render + resize handling
496
+ render();
497
+ const rerender = () => render();
498
+ if (window.ResizeObserver) {
499
+ const ro = new ResizeObserver(() => rerender());
500
+ ro.observe(container);
501
+ } else {
502
+ window.addEventListener('resize', rerender);
503
+ }
504
+ };
505
+
506
+ if (document.readyState === 'loading') {
507
+ document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
508
+ } else {
509
+ ensureD3(bootstrap);
510
+ }
511
+ })();
512
+ </script>
513
+
514
+
515
+
app/src/content/embeds/{d3-neural.html → d3-neural-network.html} RENAMED
@@ -1,11 +1,21 @@
1
- <div class="d3-neural" style="width:100%;margin:10px 0;"></div>
2
  <style>
 
3
  .d3-neural .controls { margin-top: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
4
  .d3-neural .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
5
  .d3-neural .controls input[type="range"]{ width: 160px; }
6
- .d3-neural .panel { display:flex; gap:16px; align-items:center; }
7
- .d3-neural .left { flex: 0 0 320px; display:flex; flex-direction:column; gap:8px; }
8
- .d3-neural .right { flex: 1 1 auto; min-width: 0; }
 
 
 
 
 
 
 
 
 
9
  .d3-neural canvas { width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border-color); background: var(--surface-bg); display:block; }
10
  .d3-neural .preview28 { display:grid; grid-template-columns: repeat(28, 1fr); gap: 1px; width: 100%; }
11
  .d3-neural .preview28 span { display:block; aspect-ratio:1/1; border-radius:2px; }
@@ -16,7 +26,8 @@
16
  .d3-neural .probs .tick { font-size: 10px; color: var(--muted-color); text-align:center; margin-top: 2px; }
17
  .d3-neural .canvas-wrap { position: relative; }
18
  .d3-neural .erase-btn { position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-color); }
19
- .d3-neural .canvas-hint { position: absolute; top: 8px; left: 12px; font-size: 12px; font-weight: 700; color: rgb(156, 156, 156); pointer-events: none; }
 
20
  </style>
21
  <script>
22
  (() => {
@@ -44,12 +55,16 @@
44
  if (!container) return;
45
  if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
46
 
 
 
47
  // Layout: left (canvas + preview + controls), right (svg network)
48
  const panel = document.createElement('div');
49
  panel.className = 'panel';
50
  const left = document.createElement('div'); left.className = 'left';
 
 
51
  const right = document.createElement('div'); right.className = 'right';
52
- panel.appendChild(left); panel.appendChild(right);
53
  container.appendChild(panel);
54
 
55
  // Canvas for drawing
@@ -62,6 +77,8 @@
62
  canvasWrap.appendChild(canvas);
63
  // Erase icon button (top-right)
64
  const eraseBtn = document.createElement('button'); eraseBtn.className='erase-btn button--ghost'; eraseBtn.type='button'; eraseBtn.setAttribute('aria-label','Clear');
 
 
65
  eraseBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg>';
66
  eraseBtn.addEventListener('click', () => clearCanvas());
67
  canvasWrap.appendChild(eraseBtn);
@@ -78,6 +95,7 @@
78
 
79
  // SVG network on right
80
  const svg = d3.select(right).append('svg').attr('width','100%').style('display','block');
 
81
  const gRoot = svg.append('g');
82
  const gInput = gRoot.append('g').attr('class','input');
83
  const gInputLinks = gRoot.append('g').attr('class','input-links');
@@ -87,7 +105,7 @@
87
  const gOutText = gRoot.append('g').attr('class','out-probs');
88
 
89
  // Network structure (compact: 8 -> 8 -> 10)
90
- const layerSizes = [8, 8, 10];
91
  const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
92
  // Links only between hidden->hidden and hidden->output
93
  const links = [];
@@ -239,13 +257,15 @@
239
  function dot(a,b){ let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; return s; }
240
 
241
  // Resize handling and node layout
242
- let width=800, height=360; const margin = { top: 16, right: 24, bottom: 24, left: 24 };
243
  let inputGrid = { cell: 0, x: 0, y: 0, width: 0, height: 0 };
244
  function layoutNodes(){
245
  // Right panel width, and a non-square aspect ratio for clarity
246
- width = Math.max(300, Math.round(right.clientWidth || 800));
247
- height = Math.max(260, Math.round(width * 0.45));
248
  svg.attr('width', width).attr('height', height);
 
 
249
  const innerW = width - margin.left - margin.right; const innerH = height - margin.top - margin.bottom;
250
  gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
251
  // Input grid layout (28x28) at left — cap width to a fraction of innerW
@@ -255,31 +275,46 @@
255
  let cell = Math.max(3, Math.min(cellByHeight, cellByWidth));
256
  let gridH = cell * 28; let gridY = Math.floor((innerH - gridH)/2);
257
  inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
258
- // Ensure there is always space for layers to the right
259
- const minRightPad = 40; // minimal free space at right
260
- let startX = inputGrid.width + 24;
261
- if (startX > innerW - minRightPad) {
262
- // Shrink grid to free horizontal room
263
- cell = Math.max(3, Math.floor((innerW - minRightPad - 24) / 28));
 
 
264
  gridH = cell * 28; gridY = Math.floor((innerH - gridH)/2);
265
  inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
266
- startX = inputGrid.width + 24;
267
  }
268
- const nLayers = layerSizes.length;
269
- // Reserve space at right for output labels/bars so they don't get cut off
270
- const rightLabelPad = 100; // px reserved for digit label + bar
271
- const availableW = Math.max(100, innerW - startX - rightLabelPad);
272
- // Reduce inter-layer spacing slightly; keep a sane min/max
273
- const stepX = nLayers > 1 ? Math.min(200, Math.max(28, availableW / (nLayers - 1))) : 0;
274
- const xs = Array.from({ length: nLayers }, (_, li) => startX + stepX * li);
275
  // Y positions evenly spaced per layer
276
  layers.forEach((nodes, li)=>{
277
- const n = nodes.length; const spacing = innerH/(n+1);
278
- nodes.forEach((nd, i)=>{ nd.x = xs[li]; nd.y = spacing*(i+1); });
 
 
 
 
 
 
 
 
279
  });
280
  }
281
 
282
  let lastX28 = new Float32Array(28*28);
 
 
 
 
 
 
 
 
 
283
  function renderInputGrid(){
284
  if (!inputGrid || inputGrid.cell <= 0) return;
285
  const data = Array.from({ length: 28*28 }, (_, i) => ({ i, v: lastX28[i] || 0 }));
@@ -292,34 +327,112 @@
292
  .merge(sel)
293
  .attr('x', d => inputGrid.x + (d.i % 28) * inputGrid.cell + offset)
294
  .attr('y', d => inputGrid.y + Math.floor(d.i / 28) * inputGrid.cell + offset)
295
- .attr('fill', d => { const g = 255 - Math.round(d.v * 255); return `rgb(${g},${g},${g})`; })
 
 
 
 
 
296
  .attr('stroke', 'none');
297
  sel.exit().remove();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
 
300
  function renderInputLinks(){
301
  // Draw bundle-like links from input grid right edge to first layer nodes (features)
302
  const firstLayer = layers[0];
303
  if (!firstLayer || !inputGrid || inputGrid.cell <= 0) { gInputLinks.selectAll('path').remove(); return; }
304
- const innerH = height - margin.top - margin.bottom;
305
  const x0 = inputGrid.x + inputGrid.width;
 
 
 
 
 
 
306
  const paths = firstLayer.map((n, idx) => {
307
- const yTarget = n.y;
308
- // source y roughly aligned to node y, clamped within the grid
309
- const y0 = Math.max(inputGrid.y, Math.min(inputGrid.y + inputGrid.height, yTarget));
310
- const dx = (n.x - x0) * 0.35;
311
- return { x0, y0, x1: n.x - 12, y1: yTarget, c1x: x0 + dx, c1y: y0, c2x: n.x - dx, c2y: yTarget };
 
 
 
312
  });
313
  const sel = gInputLinks.selectAll('path.input-link').data(paths);
314
  sel.enter().append('path').attr('class','input-link')
315
  .attr('fill','none')
316
- .attr('stroke','rgba(0,0,0,0.25)')
 
317
  .attr('stroke-width', 1)
 
318
  .merge(sel)
319
- .attr('d', d => `M${d.x0},${d.y0} C${d.c1x},${d.c1y} ${d.c2x},${d.c2y} ${d.x1},${d.y1}`);
 
320
  sel.exit().remove();
321
  }
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  function renderGraph(showEdges){
324
  layoutNodes();
325
  renderInputGrid();
@@ -330,8 +443,11 @@
330
  nodeSel.enter().append('circle').attr('class','node')
331
  .attr('r', 10)
332
  .attr('cx', d=>d.x).attr('cy', d=>d.y)
333
- .attr('fill', d=> d.layer===2 ? 'var(--primary-color)' : 'var(--surface-bg)')
334
- .attr('stroke','var(--border-color)').attr('stroke-width',1)
 
 
 
335
  .merge(nodeSel)
336
  .attr('cx', d=>d.x).attr('cy', d=>d.y)
337
  .attr('opacity', 1);
@@ -339,30 +455,38 @@
339
 
340
  // Labels for first hidden layer only (avoid stacking with output probs)
341
  const labels = [];
342
- layers[0].forEach((n,i)=> labels.push({ x:n.x, y:n.y-16, txt:`f${i+1}` }));
343
  const labSel = gLabels.selectAll('text').data(labels);
344
- labSel.enter().append('text').style('font-size','12px').style('fill','var(--muted-color)')
345
- .attr('x', d=>d.x).attr('y', d=>d.y)
 
 
 
 
 
 
346
  .text(d=>d.txt)
347
  .merge(labSel)
348
- .attr('x', d=>d.x).attr('y', d=>d.y).text(d=>d.txt);
 
 
 
 
 
349
  labSel.exit().remove();
350
 
351
  // Links as smooth curves
352
- const pathFor = (d) => {
353
- const x1 = layers[d.s.l][d.s.i].x, y1 = layers[d.s.l][d.s.i].y;
354
- const x2 = layers[d.t.l][d.t.j].x, y2 = layers[d.t.l][d.t.j].y;
355
- const dx = (x2 - x1) * 0.45;
356
- return `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`;
357
- };
358
  const linkSel = gLinks.selectAll('path.link').data(links, d=> `${d.s.l}-${d.s.i}-${d.t.l}-${d.t.j}`);
359
  linkSel.enter().append('path').attr('class','link')
360
- .attr('d', pathFor)
361
  .attr('fill','none')
362
- .attr('stroke','rgba(0,0,0,0.25)')
 
363
  .attr('stroke-width', d=> 0.5 + d.w*1.2)
 
364
  .merge(linkSel)
365
- .attr('d', pathFor)
 
366
  .attr('stroke-width', d=> 0.5 + d.w*1.2);
367
  linkSel.exit().remove();
368
 
@@ -372,8 +496,17 @@
372
  if (!d || typeof d.digit !== 'number') return d3.select(this).attr('transform');
373
  const n = layers[2][d.digit];
374
  if (!n) return d3.select(this).attr('transform');
375
- return `translate(${n.x+18},${n.y})`;
 
376
  });
 
 
 
 
 
 
 
 
377
  }
378
 
379
  function setNodeActivations(h1, h2, out){
@@ -385,50 +518,86 @@
385
  if (Array.isArray(out)) {
386
  for (let i=0;i<out.length;i++){ if (out[i] > bestProb){ bestProb = out[i]; argmaxIdx = i; } }
387
  }
388
- // Color/opacity by activation
389
  gNodes.selectAll('circle.node')
390
- .attr('fill', d=> d.layer===2 ? 'var(--primary-color)' : `rgba(0,0,0,${0.06 + 0.44*d.a})`)
391
- .attr('stroke', d=> d.layer===2 ? 'var(--primary-color)' : 'var(--border-color)')
392
- .attr('opacity', d=> 0.25 + 0.75*Math.min(1, d.a))
393
- .attr('r', d=> 8 + 6*Math.min(1, d.a));
 
 
 
394
  // Link opacity by activation flow
395
  gLinks.selectAll('path.link')
396
- .attr('stroke', d=>{
 
 
 
 
 
 
 
397
  const aS = layers[d.s.l][d.s.i].a || 0; const aT = layers[d.t.l][d.t.j].a || 0;
398
- const alpha = Math.min(1, 0.08 + 0.85 * (aS * aT));
399
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
400
- const base = isDark ? 255 : 0;
401
- return `rgba(${base},${base},${base},${alpha})`;
402
  });
403
- // Output labels: bold digit + small horizontal bar for probability
404
- const outs = layers[2].map((n,i)=>({ x:n.x+18, y:n.y, digit: i, prob: (out[i]||0), isTop: i===argmaxIdx }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  const gSel = gOutText.selectAll('g.out-label').data(outs, d=>d.digit);
406
  const gEnter = gSel.enter().append('g').attr('class','out-label');
407
  gEnter.append('text').attr('class','out-digit')
408
- .style('font-size','12px').style('font-weight','700').style('fill','var(--text-color)');
409
- gEnter.append('rect').attr('class','out-bar-bg').attr('rx',2).attr('ry',2)
410
- .attr('height', 4).attr('fill', 'var(--border-color)');
411
- gEnter.append('rect').attr('class','out-bar').attr('rx',2).attr('ry',2)
412
- .attr('height', 4);
413
- const BAR_MAX = 64;
414
- gEnter.merge(gSel)
415
  .attr('transform', d=>`translate(${d.x},${d.y})`)
416
  .each(function(d){
417
  const sel = d3.select(this);
418
  sel.select('text.out-digit')
419
- .attr('x', 0).attr('y', -2)
420
  .text(String(d.digit));
421
- sel.select('rect.out-bar-bg')
422
- .attr('x', 0).attr('y', 6)
423
- .attr('width', BAR_MAX);
424
- sel.select('rect.out-bar')
425
- .attr('x', 0).attr('y', 6)
426
- .attr('width', Math.max(1, Math.round(d.prob * BAR_MAX)))
427
- .attr('fill', d.isTop ? 'var(--primary-color)' : 'var(--border-color)');
428
  // Ghost non-top predictions
429
  sel.style('opacity', d.isTop ? 1 : 0.35);
430
  });
 
 
 
431
  gSel.exit().remove();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  }
433
 
434
  // (no separate updateBars; bars are rendered next to nodes)
@@ -441,6 +610,8 @@
441
  renderInputGrid();
442
  const feats = computeFeatures(x28); // 8D in [0,1]
443
  const inkMass = feats[0];
 
 
444
  // Hidden 1 = raw features
445
  const h1 = feats;
446
  // Hidden 2 = simple non-linear mix for visualization only
@@ -497,6 +668,7 @@
497
 
498
  // Drawing interactions
499
  let drawing=false; let last=null;
 
500
  const getPos = (ev) => {
501
  const rect = canvas.getBoundingClientRect();
502
  const sx = CANVAS_PX/rect.width; const sy = CANVAS_PX/rect.height;
@@ -511,7 +683,11 @@
511
  ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke();
512
  last = p; runPipeline();
513
  }
514
- function onDown(ev){ drawing=true; last=null; drawTo(getPos(ev)); ev.preventDefault(); }
 
 
 
 
515
  function onMove(ev){ if (!drawing) return; drawTo(getPos(ev)); ev.preventDefault(); }
516
  function onUp(){ drawing=false; last=null; }
517
  canvas.addEventListener('mousedown', onDown); canvas.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp);
 
1
+ <div class="d3-neural"></div>
2
  <style>
3
+ .d3-neural { position: relative; width:100%;margin:0;}
4
  .d3-neural .controls { margin-top: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
5
  .d3-neural .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
6
  .d3-neural .controls input[type="range"]{ width: 160px; }
7
+ .d3-neural .panel { display:flex; gap:8px; align-items:stretch; flex-wrap: nowrap; }
8
+ .d3-neural .left { flex: 0 0 33.333%; max-width: 33.333%; min-width: 160px; display:flex; flex-direction:column; gap:8px; }
9
+ .d3-neural .right { flex: 1 1 66.666%; max-width: 66.666%; min-width: 280px; display:flex; }
10
+ .d3-neural .right > svg { flex: 1 1 auto; height: 100%; }
11
+ .d3-neural .arrow-sep { flex: 0 0 18px; max-width: 18px; display:flex; align-items:center; justify-content:center; color: var(--muted-color); }
12
+ .d3-neural .arrow-sep svg { display:block; width: 16px; height: 16px; }
13
+ @media (max-width: 800px) {
14
+ .d3-neural .panel { flex-direction: column; }
15
+ .d3-neural .left,
16
+ .d3-neural .right { flex: 0 0 100%; max-width: 100%; min-width: 0; }
17
+ .d3-neural .arrow-sep { display: none; }
18
+ }
19
  .d3-neural canvas { width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border-color); background: var(--surface-bg); display:block; }
20
  .d3-neural .preview28 { display:grid; grid-template-columns: repeat(28, 1fr); gap: 1px; width: 100%; }
21
  .d3-neural .preview28 span { display:block; aspect-ratio:1/1; border-radius:2px; }
 
26
  .d3-neural .probs .tick { font-size: 10px; color: var(--muted-color); text-align:center; margin-top: 2px; }
27
  .d3-neural .canvas-wrap { position: relative; }
28
  .d3-neural .erase-btn { position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-color); }
29
+ .d3-neural .canvas-hint { position: absolute; top: 8px; left: 12px; font-size: 12px; font-weight: 700; color: rgba(0,0,0,.9); pointer-events: none; transition: opacity .12s ease; }
30
+
31
  </style>
32
  <script>
33
  (() => {
 
55
  if (!container) return;
56
  if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
57
 
58
+ // (tooltip removed)
59
+
60
  // Layout: left (canvas + preview + controls), right (svg network)
61
  const panel = document.createElement('div');
62
  panel.className = 'panel';
63
  const left = document.createElement('div'); left.className = 'left';
64
+ const arrowSep = document.createElement('div'); arrowSep.className = 'arrow-sep';
65
+ arrowSep.innerHTML = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><line x1="3" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="17,7 22,12 17,17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
66
  const right = document.createElement('div'); right.className = 'right';
67
+ panel.appendChild(left); panel.appendChild(arrowSep); panel.appendChild(right);
68
  container.appendChild(panel);
69
 
70
  // Canvas for drawing
 
77
  canvasWrap.appendChild(canvas);
78
  // Erase icon button (top-right)
79
  const eraseBtn = document.createElement('button'); eraseBtn.className='erase-btn button--ghost'; eraseBtn.type='button'; eraseBtn.setAttribute('aria-label','Clear');
80
+ // Hidden until the user interacts with the canvas
81
+ eraseBtn.style.display = 'none';
82
  eraseBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg>';
83
  eraseBtn.addEventListener('click', () => clearCanvas());
84
  canvasWrap.appendChild(eraseBtn);
 
95
 
96
  // SVG network on right
97
  const svg = d3.select(right).append('svg').attr('width','100%').style('display','block');
98
+ const defs = svg.append('defs');
99
  const gRoot = svg.append('g');
100
  const gInput = gRoot.append('g').attr('class','input');
101
  const gInputLinks = gRoot.append('g').attr('class','input-links');
 
105
  const gOutText = gRoot.append('g').attr('class','out-probs');
106
 
107
  // Network structure (compact: 8 -> 8 -> 10)
108
+ const layerSizes = [8, 8, 8];
109
  const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
110
  // Links only between hidden->hidden and hidden->output
111
  const links = [];
 
257
  function dot(a,b){ let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; return s; }
258
 
259
  // Resize handling and node layout
260
+ let width=640, height=360; const margin = { top: 16, right: 8, bottom: 24, left: 8 };
261
  let inputGrid = { cell: 0, x: 0, y: 0, width: 0, height: 0 };
262
  function layoutNodes(){
263
  // Right panel width, and a non-square aspect ratio for clarity
264
+ width = Math.max(280, Math.round(right.clientWidth || 640));
265
+ height = Math.max(260, Math.round(width * 0.56));
266
  svg.attr('width', width).attr('height', height);
267
+ // Match canvas height to SVG height so both columns align vertically
268
+ try { canvas.style.height = '100%'; canvasWrap.style.height = height + 'px'; } catch(_) {}
269
  const innerW = width - margin.left - margin.right; const innerH = height - margin.top - margin.bottom;
270
  gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
271
  // Input grid layout (28x28) at left — cap width to a fraction of innerW
 
275
  let cell = Math.max(3, Math.min(cellByHeight, cellByWidth));
276
  let gridH = cell * 28; let gridY = Math.floor((innerH - gridH)/2);
277
  inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
278
+ // Equal horizontal gaps: grid -> L0 -> L1 -> L2
279
+ const nLayers = layerSizes.length; // 3
280
+ const rightLabelPad = 36; // smaller pad; use more width for spreading layers
281
+ const minGap = 28; const maxGap = 260;
282
+ // Ensure enough free space; shrink grid if needed
283
+ const desiredMinFree = rightLabelPad + nLayers * minGap; // 3 equal gaps
284
+ if (inputGrid.width + desiredMinFree > innerW) {
285
+ cell = Math.max(3, Math.floor((innerW - desiredMinFree) / 28));
286
  gridH = cell * 28; gridY = Math.floor((innerH - gridH)/2);
287
  inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
 
288
  }
289
+ const gridRight = inputGrid.x + inputGrid.width;
290
+ const freeW = Math.max(nLayers * minGap, innerW - gridRight - rightLabelPad);
291
+ const gapX = Math.min(maxGap, Math.max(minGap, Math.floor(freeW / nLayers)));
292
+ const xs = Array.from({ length: nLayers }, (_, li) => gridRight + gapX * (li + 1));
 
 
 
293
  // Y positions evenly spaced per layer
294
  layers.forEach((nodes, li)=>{
295
+ const n = nodes.length;
296
+ if (n <= 1) {
297
+ nodes.forEach((nd)=>{ nd.x = xs[li]; nd.y = innerH/2; });
298
+ } else {
299
+ const occupancy = 0.9; // use 90% of vertical space
300
+ const span = innerH * occupancy;
301
+ const topPad = (innerH - span) / 2;
302
+ const spacing = span / (n - 1);
303
+ nodes.forEach((nd, i)=>{ nd.x = xs[li]; nd.y = topPad + i*spacing; });
304
+ }
305
  });
306
  }
307
 
308
  let lastX28 = new Float32Array(28*28);
309
+ function nodeRadiusForNode(n){
310
+ const a = Math.max(0, Math.min(1, (n && typeof n.a === 'number') ? n.a : 0));
311
+ if (n && n.layer === 2) {
312
+ // Output nodes: variable radius based on activation
313
+ return 8 + 10 * a; // ~8–18
314
+ }
315
+ // Hidden/feature nodes: variable radius based on activation
316
+ return 5 + 5 * a; // ~5–10
317
+ }
318
  function renderInputGrid(){
319
  if (!inputGrid || inputGrid.cell <= 0) return;
320
  const data = Array.from({ length: 28*28 }, (_, i) => ({ i, v: lastX28[i] || 0 }));
 
327
  .merge(sel)
328
  .attr('x', d => inputGrid.x + (d.i % 28) * inputGrid.cell + offset)
329
  .attr('y', d => inputGrid.y + Math.floor(d.i / 28) * inputGrid.cell + offset)
330
+ .attr('fill', d => {
331
+ // Increase perceived contrast of the input grid by applying a gamma curve
332
+ const k = Math.pow(Math.max(0, Math.min(1, d.v)), 0.6); // gamma < 1 → darker darks
333
+ const g = 255 - Math.round(k * 255);
334
+ return `rgb(${g},${g},${g})`;
335
+ })
336
  .attr('stroke', 'none');
337
  sel.exit().remove();
338
+
339
+ // Border around the input grid area
340
+ const borderSel = gInput.selectAll('rect.input-border').data([0]);
341
+ borderSel.enter().append('rect').attr('class','input-border')
342
+ .attr('fill','none')
343
+ .attr('rx', 0).attr('ry', 0)
344
+ .attr('stroke','var(--text-color)')
345
+ .attr('stroke-opacity', 0.25)
346
+ .attr('stroke-width', 1)
347
+ .lower()
348
+ .merge(borderSel)
349
+ .attr('x', inputGrid.x-1)
350
+ .attr('y', inputGrid.y-1)
351
+ .attr('width', inputGrid.width+1)
352
+ .attr('height', inputGrid.height+1);
353
+
354
+ // Centered label above the input grid
355
+ const labelSel = gInput.selectAll('text.input-label').data([0]);
356
+ labelSel.enter().append('text').attr('class','input-label')
357
+ .attr('text-anchor','middle')
358
+ .style('font-size','12px')
359
+ .style('font-weight','700')
360
+ .style('fill','var(--muted-color)')
361
+ .merge(labelSel)
362
+ .attr('x', inputGrid.x + inputGrid.width / 2)
363
+ .attr('y', Math.max(12, inputGrid.y - 10))
364
+ .text('Input 28×28');
365
+ }
366
+
367
+ // Compute link path between two layered nodes using their current radii
368
+ function computeLinkD(d){
369
+ const s = layers[d.s.l][d.s.i];
370
+ const t = layers[d.t.l][d.t.j];
371
+ if (!s || !t) return '';
372
+ const rs = nodeRadiusForNode(s);
373
+ const rt = nodeRadiusForNode(t);
374
+ // Use fixed anchors on circle edges for all inter-layer links (except grid->L0 handled elsewhere)
375
+ const x1 = s.x + rs, y1 = s.y; // right edge of source circle
376
+ const x2 = t.x - rt, y2 = t.y; // left edge of target circle
377
+ const dx = (x2 - x1) * 0.45;
378
+ return `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`;
379
  }
380
 
381
  function renderInputLinks(){
382
  // Draw bundle-like links from input grid right edge to first layer nodes (features)
383
  const firstLayer = layers[0];
384
  if (!firstLayer || !inputGrid || inputGrid.cell <= 0) { gInputLinks.selectAll('path').remove(); return; }
 
385
  const x0 = inputGrid.x + inputGrid.width;
386
+ // Define a centered vertical band (half grid height) and distribute sources evenly
387
+ const k = firstLayer.length;
388
+ const band = inputGrid.height * 0.5;
389
+ const centerY = inputGrid.y + inputGrid.height / 2;
390
+ const yStart = centerY - band / 2;
391
+ const spacing = k > 1 ? band / (k - 1) : 0;
392
  const paths = firstLayer.map((n, idx) => {
393
+ // source y from centered band, equidistant
394
+ const y0 = k > 1 ? (yStart + idx * spacing) : centerY;
395
+ // Target anchor: center of left edge of the node circle
396
+ const r = nodeRadiusForNode(n);
397
+ const x1 = n.x - r;
398
+ const y1 = n.y;
399
+ const dx = (x1 - x0) * 0.35;
400
+ return { x0, y0, x1, y1, c1x: x0 + dx, c1y: y0, c2x: x1 - dx, c2y: y1, idx };
401
  });
402
  const sel = gInputLinks.selectAll('path.input-link').data(paths);
403
  sel.enter().append('path').attr('class','input-link')
404
  .attr('fill','none')
405
+ .attr('stroke','var(--text-color)')
406
+ .attr('stroke-opacity', 0.25)
407
  .attr('stroke-width', 1)
408
+ .attr('stroke-linecap','round')
409
  .merge(sel)
410
+ .attr('d', d => `M${d.x0},${d.y0} C${d.c1x},${d.c1y} ${d.c2x},${d.c2y} ${d.x1},${d.y1}`)
411
+ .attr('stroke','var(--text-color)');
412
  sel.exit().remove();
413
  }
414
 
415
+ // Recompute input link path on the fly (used when node radii change)
416
+ function computeInputLinkD(idx){
417
+ const firstLayer = layers[0];
418
+ const n = firstLayer[idx]; if (!n) return '';
419
+ const x0 = inputGrid.x + inputGrid.width;
420
+ const k = firstLayer.length;
421
+ const band = inputGrid.height * 0.5;
422
+ const centerY = inputGrid.y + inputGrid.height / 2;
423
+ const yStart = centerY - band / 2;
424
+ const spacing = k > 1 ? band / (k - 1) : 0;
425
+ const y0 = k > 1 ? (yStart + idx * spacing) : centerY;
426
+ const yTarget = n.y;
427
+ const vx = n.x - x0; const vy = yTarget - y0; const L = Math.hypot(vx, vy) || 1;
428
+ const r = nodeRadiusForNode(n);
429
+ const x1 = n.x - (vx / L) * r;
430
+ const y1 = yTarget - (vy / L) * r;
431
+ const dx = (x1 - x0) * 0.35;
432
+ const c1x = x0 + dx, c1y = y0, c2x = x1 - dx, c2y = y1;
433
+ return `M${x0},${y0} C${c1x},${c1y} ${c2x},${c2y} ${x1},${y1}`;
434
+ }
435
+
436
  function renderGraph(showEdges){
437
  layoutNodes();
438
  renderInputGrid();
 
443
  nodeSel.enter().append('circle').attr('class','node')
444
  .attr('r', 10)
445
  .attr('cx', d=>d.x).attr('cy', d=>d.y)
446
+ .attr('fill', d=> d.layer===2 ? 'var(--page-bg)' : 'var(--primary-color)')
447
+ .attr('fill-opacity', d=> d.layer===2 ? 1 : 0.12)
448
+ .attr('stroke', d=> d.layer===2 ? 'var(--border-color)' : 'var(--border-color)')
449
+ .attr('stroke-width',1)
450
+ .attr('stroke-linejoin','round')
451
  .merge(nodeSel)
452
  .attr('cx', d=>d.x).attr('cy', d=>d.y)
453
  .attr('opacity', 1);
 
455
 
456
  // Labels for first hidden layer only (avoid stacking with output probs)
457
  const labels = [];
458
+ layers[0].forEach((n,i)=> labels.push({ x:n.x-30, y:n.y+4, txt:`f${i+1}` }));
459
  const labSel = gLabels.selectAll('text').data(labels);
460
+ labSel.enter().append('text')
461
+ .style('font-size','10px')
462
+ .style('fill','var(--muted-color)')
463
+ .style('paint-order','stroke')
464
+ .style('stroke','var(--page-bg)')
465
+ .style('stroke-width','3px')
466
+ .attr('x', d=>d.x)
467
+ .attr('y', d=>d.y)
468
  .text(d=>d.txt)
469
  .merge(labSel)
470
+ .style('paint-order','stroke')
471
+ .style('stroke','var(--page-bg)')
472
+ .style('stroke-width','5px')
473
+ .attr('x', d=>d.x)
474
+ .attr('y', d=>d.y)
475
+ .text(d=>d.txt);
476
  labSel.exit().remove();
477
 
478
  // Links as smooth curves
 
 
 
 
 
 
479
  const linkSel = gLinks.selectAll('path.link').data(links, d=> `${d.s.l}-${d.s.i}-${d.t.l}-${d.t.j}`);
480
  linkSel.enter().append('path').attr('class','link')
481
+ .attr('d', computeLinkD)
482
  .attr('fill','none')
483
+ .attr('stroke','var(--text-color)')
484
+ .attr('stroke-opacity', 0.25)
485
  .attr('stroke-width', d=> 0.5 + d.w*1.2)
486
+ .attr('stroke-linecap','round')
487
  .merge(linkSel)
488
+ .attr('d', computeLinkD)
489
+ .attr('stroke','var(--text-color)')
490
  .attr('stroke-width', d=> 0.5 + d.w*1.2);
491
  linkSel.exit().remove();
492
 
 
496
  if (!d || typeof d.digit !== 'number') return d3.select(this).attr('transform');
497
  const n = layers[2][d.digit];
498
  if (!n) return d3.select(this).attr('transform');
499
+ const offset = nodeRadiusForNode(n) + 8;
500
+ return `translate(${n.x + offset},${n.y})`;
501
  });
502
+ // Ensure clip-path circles are updated on resize
503
+ if (defs) {
504
+ const clips = defs.selectAll('clipPath.clip-node').data(layers[2], d=>d.id);
505
+ const ce = clips.enter().append('clipPath').attr('class','clip-node').attr('clipPathUnits','userSpaceOnUse').attr('id', d=>`clip-${d.id}`);
506
+ ce.append('circle');
507
+ clips.merge(ce).select('circle').attr('cx', d=>d.x).attr('cy', d=>d.y).attr('r', d=>nodeRadiusForNode(d));
508
+ clips.exit().remove();
509
+ }
510
  }
511
 
512
  function setNodeActivations(h1, h2, out){
 
518
  if (Array.isArray(out)) {
519
  for (let i=0;i<out.length;i++){ if (out[i] > bestProb){ bestProb = out[i]; argmaxIdx = i; } }
520
  }
521
+ // Color/size by activation with smooth transitions
522
  gNodes.selectAll('circle.node')
523
+ .transition().duration(180).ease(d3.easeCubicOut)
524
+ .attr('fill', d=> d.layer===2 ? 'var(--page-bg)' : 'var(--primary-color)')
525
+ .attr('fill-opacity', d=> d.layer===2 ? 1 : (0.12 + 0.58*Math.min(1, d.a||0)))
526
+ .attr('stroke', 'var(--primary-color)')
527
+ .attr('stroke-opacity', d=> (d.layer===2 ? 0.9 : (0.45 + 0.45*Math.min(1, d.a||0))))
528
+ .attr('opacity', d=> 0.55 + 0.45*Math.min(1, d.a||0))
529
+ .attr('r', d=> nodeRadiusForNode(d));
530
  // Link opacity by activation flow
531
  gLinks.selectAll('path.link')
532
+ .transition().duration(180).ease(d3.easeCubicOut)
533
+ .attr('d', computeLinkD)
534
+ .attr('stroke','var(--text-color)')
535
+ .attr('stroke-opacity', d=>{
536
+ const aS = layers[d.s.l][d.s.i].a || 0; const aT = layers[d.t.l][d.t.j].a || 0;
537
+ return Math.min(1, 0.15 + 0.85 * (aS * aT));
538
+ })
539
+ .attr('stroke-width', d=>{
540
  const aS = layers[d.s.l][d.s.i].a || 0; const aT = layers[d.t.l][d.t.j].a || 0;
541
+ return 0.6 + 2.2*(aS*aT);
 
 
 
542
  });
543
+ // Theme-aware and activation-aware input links
544
+ gInputLinks.selectAll('path.input-link')
545
+ .transition().duration(180).ease(d3.easeCubicOut)
546
+ .attr('d', (d)=> computeInputLinkD(d.idx))
547
+ .attr('stroke','var(--text-color)')
548
+ .attr('stroke-opacity', 0.25)
549
+ .attr('stroke-width', d=> 0.6 + 2.0*(layers[0][d.idx] ? (layers[0][d.idx].a||0) : 0));
550
+ // Update clip-path circles to match new radii/positions of output nodes
551
+ if (defs) {
552
+ const clips = defs.selectAll('clipPath.clip-node').data(layers[2], d=>d.id);
553
+ clips.select('circle')
554
+ .transition().duration(180).ease(d3.easeCubicOut)
555
+ .attr('cx', d=>d.x)
556
+ .attr('cy', d=>d.y)
557
+ .attr('r', d=> nodeRadiusForNode(d));
558
+ }
559
+ // Theme-aware input links on updates handled above via transition
560
+ // Output labels: digit placed to the right of the node
561
+ const outs = layers[2].map((n,i)=>({ x:n.x + nodeRadiusForNode(n) + 8, y:n.y, digit: i, prob: (out[i]||0), isTop: i===argmaxIdx }));
562
  const gSel = gOutText.selectAll('g.out-label').data(outs, d=>d.digit);
563
  const gEnter = gSel.enter().append('g').attr('class','out-label');
564
  gEnter.append('text').attr('class','out-digit')
565
+ .style('font-size','12px').style('font-weight','800').style('fill','var(--text-color)')
566
+ .attr('text-anchor','start').attr('dominant-baseline','middle')
567
+ .style('paint-order','stroke').style('stroke','var(--transparent-page-contrast)').style('stroke-width','3px');
568
+ const merged = gEnter.merge(gSel)
 
 
 
569
  .attr('transform', d=>`translate(${d.x},${d.y})`)
570
  .each(function(d){
571
  const sel = d3.select(this);
572
  sel.select('text.out-digit')
573
+ .attr('x', 0).attr('y', 0)
574
  .text(String(d.digit));
 
 
 
 
 
 
 
575
  // Ghost non-top predictions
576
  sel.style('opacity', d.isTop ? 1 : 0.35);
577
  });
578
+ // Remove any previous decorative rings (no highlight ring desired)
579
+ gRoot.selectAll('circle.top-ring').remove();
580
+ // (tooltip interactions removed)
581
  gSel.exit().remove();
582
+
583
+ // Output liquid fill using clipPath + rect from bottom
584
+ const rects = gNodes.selectAll('rect.out-liquid').data(layers[2], d=>d.id);
585
+ const rectEnter = rects.enter().append('rect').attr('class','out-liquid')
586
+ .attr('fill','var(--primary-color)')
587
+ .attr('fill-opacity', 0.55)
588
+ .attr('clip-path', d => `url(#clip-${d.id})`);
589
+ rectEnter.merge(rects)
590
+ .transition().duration(180).ease(d3.easeCubicOut)
591
+ .attr('x', d=> d.x - nodeRadiusForNode(d))
592
+ .attr('width', d=> 2 * nodeRadiusForNode(d))
593
+ .attr('y', d=> {
594
+ const r = nodeRadiusForNode(d);
595
+ const h = 2 * r * Math.max(0, Math.min(1, d.a||0));
596
+ return d.y + r - h;
597
+ })
598
+ .attr('height', d=> 2 * nodeRadiusForNode(d) * Math.max(0, Math.min(1, d.a||0)))
599
+ .attr('fill-opacity', 0.55);
600
+ rects.exit().remove();
601
  }
602
 
603
  // (no separate updateBars; bars are rendered next to nodes)
 
610
  renderInputGrid();
611
  const feats = computeFeatures(x28); // 8D in [0,1]
612
  const inkMass = feats[0];
613
+ // Hide hint when user has drawn something
614
+ if (hint) { hint.style.opacity = inkMass < 0.01 ? 1 : 0; }
615
  // Hidden 1 = raw features
616
  const h1 = feats;
617
  // Hidden 2 = simple non-linear mix for visualization only
 
668
 
669
  // Drawing interactions
670
  let drawing=false; let last=null;
671
+ let hasInteracted=false;
672
  const getPos = (ev) => {
673
  const rect = canvas.getBoundingClientRect();
674
  const sx = CANVAS_PX/rect.width; const sy = CANVAS_PX/rect.height;
 
683
  ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke();
684
  last = p; runPipeline();
685
  }
686
+ function onDown(ev){
687
+ drawing=true; last=null;
688
+ if (!hasInteracted){ hasInteracted=true; try { eraseBtn.style.display = 'flex'; } catch(_) {} }
689
+ drawTo(getPos(ev)); ev.preventDefault();
690
+ }
691
  function onMove(ev){ if (!drawing) return; drawTo(getPos(ev)); ev.preventDefault(); }
692
  function onUp(){ drawing=false; last=null; }
693
  canvas.addEventListener('mousedown', onDown); canvas.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp);
app/src/content/embeds/d3-pie.html CHANGED
@@ -1,4 +1,4 @@
1
- <div class="d3-pie" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
  /* Layout piloté par container queries (par rapport au parent) */
4
  .d3-pie { container-type: inline-size; }
@@ -11,7 +11,16 @@
11
  .d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
12
  .d3-pie .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }
13
  .d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
14
- .d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: rgba(255,255,255,0.2); stroke-width: 3px; }
 
 
 
 
 
 
 
 
 
15
  /* Layout HTML (pas JS) pour la grille et les cellules */
16
  .d3-pie .plots-grid {
17
  display: flex;
@@ -50,6 +59,41 @@
50
  @media (max-width: 500px) {
51
  .d3-pie .pie-cell { flex: 0 0 100%; }
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </style>
54
  <script>
55
  (() => {
@@ -87,8 +131,9 @@
87
  tip = document.createElement('div'); tip.className = 'd3-tooltip';
88
  Object.assign(tip.style, {
89
  position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
90
- padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
91
- background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease'
 
92
  });
93
  tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
94
  } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
@@ -148,7 +193,7 @@
148
  legendHost.style.display = 'flex';
149
  legendHost.style.alignItems = 'center';
150
  legendHost.style.justifyContent = 'center';
151
- legendHost.innerHTML = `<div class="items">${categories.map(c => `<div class="item"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:500">${c}</span></div>`).join('')}</div>`;
152
  }
153
 
154
  function drawPies(rows){
@@ -184,7 +229,8 @@
184
  const totals = new Map(); categories.forEach(c => totals.set(c, 0));
185
  rows.forEach(r => { totals.set(r.eagle_cathegory, totals.get(r.eagle_cathegory) + (r[metric.key] || 0)); });
186
  const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
187
- const totalSum = d3.sum(values, d => d.value);
 
188
 
189
  // Create HTML cell container
190
  const cell = document.createElement('div');
@@ -201,10 +247,10 @@
201
  const svg = d3.select(cell).append('svg').attr('width', radius * 2).attr('height', radius * 2 + SVG_VPAD * 2).style('display','block');
202
  const gCell = svg.append('g').attr('transform', `translate(${radius},${radius + SVG_VPAD})`);
203
 
204
- if (!totalSum || totalSum <= 0) {
205
  gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');
206
  } else {
207
- const data = pie(values);
208
  const percent = (v) => (v / totalSum) * 100;
209
 
210
  // Slices
@@ -213,16 +259,33 @@
213
  .attr('fill', d => colorOf(d.data.category))
214
  .attr('stroke', 'var(--surface-bg)')
215
  .attr('stroke-width', 1.2)
 
216
  .on('mouseenter', function(ev, d){
 
 
 
 
 
217
  d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
218
  const p = percent(d.data.value);
219
- 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>`;
 
 
 
 
 
220
  tip.style.opacity = '1';
221
  })
222
  .on('mousemove', function(ev){
223
  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)`;
224
  })
225
- .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('stroke','var(--surface-bg)'); });
 
 
 
 
 
 
226
 
227
  // Percentage labels (>= 3%)
228
  gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()
@@ -233,11 +296,20 @@
233
  }
234
 
235
  // HTML captions under the SVG (keep design)
236
- const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center'; subtitleEl.style.marginTop = (CAPTION_GAP - 18) + 'px'; cell.appendChild(subtitleEl);
237
  const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);
238
  });
239
 
240
  // Container height flows naturally with HTML; nothing to do
 
 
 
 
 
 
 
 
 
241
  }
242
 
243
  async function init(){
 
1
+ <div class="d3-pie"></div>
2
  <style>
3
  /* Layout piloté par container queries (par rapport au parent) */
4
  .d3-pie { container-type: inline-size; }
 
11
  .d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
12
  .d3-pie .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }
13
  .d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
14
+ /* Ghost legend items when hovering slices */
15
+ .d3-pie.hovering .legend .item.ghost { opacity: .35; }
16
+ .d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: var(--transparent-page-contrast); stroke-width: 3px; }
17
+ /* Effet ghost synchronisé */
18
+ .d3-pie .slice {
19
+ transition: opacity .15s ease;
20
+ }
21
+ .d3-pie.hovering .slice.ghost {
22
+ opacity: .25;
23
+ }
24
  /* Layout HTML (pas JS) pour la grille et les cellules */
25
  .d3-pie .plots-grid {
26
  display: flex;
 
59
  @media (max-width: 500px) {
60
  .d3-pie .pie-cell { flex: 0 0 100%; }
61
  }
62
+ /* Tooltip styling aligned with filters-quad */
63
+ .d3-pie .d3-tooltip {
64
+ z-index: var(--z-elevated);
65
+ backdrop-filter: saturate(1.12) blur(8px);
66
+ }
67
+ .d3-pie .d3-tooltip__inner {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 6px;
71
+ min-width: 220px;
72
+ }
73
+ .d3-pie .d3-tooltip__inner > div:first-child {
74
+ font-weight: 800;
75
+ letter-spacing: 0.1px;
76
+ margin-bottom: 0;
77
+ }
78
+ .d3-pie .d3-tooltip__inner > div:nth-child(2) {
79
+ font-size: 11px;
80
+ color: var(--muted-color);
81
+ display: block;
82
+ margin-top: -4px;
83
+ margin-bottom: 2px;
84
+ letter-spacing: 0.1px;
85
+ }
86
+ .d3-pie .d3-tooltip__inner > div:nth-child(n+3) {
87
+ padding-top: 6px;
88
+ border-top: 1px solid var(--border-color);
89
+ }
90
+ .d3-pie .d3-tooltip__color-dot {
91
+ display: inline-block;
92
+ width: 12px;
93
+ height: 12px;
94
+ border-radius: 3px;
95
+ border: 1px solid var(--border-color);
96
+ }
97
  </style>
98
  <script>
99
  (() => {
 
131
  tip = document.createElement('div'); tip.className = 'd3-tooltip';
132
  Object.assign(tip.style, {
133
  position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
134
+ padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
135
+ background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity:'0', transition:'opacity .12s ease',
136
+ zIndex: 'var(--z-elevated)', backdropFilter: 'saturate(1.12) blur(8px)'
137
  });
138
  tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
139
  } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
 
193
  legendHost.style.display = 'flex';
194
  legendHost.style.alignItems = 'center';
195
  legendHost.style.justifyContent = 'center';
196
+ legendHost.innerHTML = `<div class="items">${categories.map(c => `<div class="item" data-category="${c}"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:500">${c}</span></div>`).join('')}</div>`;
197
  }
198
 
199
  function drawPies(rows){
 
229
  const totals = new Map(); categories.forEach(c => totals.set(c, 0));
230
  rows.forEach(r => { totals.set(r.eagle_cathegory, totals.get(r.eagle_cathegory) + (r[metric.key] || 0)); });
231
  const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
232
+ const nonZeroValues = values.filter(v => (v.value || 0) > 0);
233
+ const totalSum = d3.sum(nonZeroValues, d => d.value);
234
 
235
  // Create HTML cell container
236
  const cell = document.createElement('div');
 
247
  const svg = d3.select(cell).append('svg').attr('width', radius * 2).attr('height', radius * 2 + SVG_VPAD * 2).style('display','block');
248
  const gCell = svg.append('g').attr('transform', `translate(${radius},${radius + SVG_VPAD})`);
249
 
250
+ if (!totalSum || totalSum <= 0 || nonZeroValues.length === 0) {
251
  gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');
252
  } else {
253
+ const data = pie(nonZeroValues);
254
  const percent = (v) => (v / totalSum) * 100;
255
 
256
  // Slices
 
259
  .attr('fill', d => colorOf(d.data.category))
260
  .attr('stroke', 'var(--surface-bg)')
261
  .attr('stroke-width', 1.2)
262
+ .attr('data-category', d => d.data.category)
263
  .on('mouseenter', function(ev, d){
264
+ const hoveredCategory = d.data.category;
265
+ d3.select(container).classed('hovering', true);
266
+ d3.select(container).selectAll('path.slice').classed('ghost', s => (s.data && s.data.category) !== hoveredCategory);
267
+ // Ghost legend items that are not hovered
268
+ d3.select(legendHost).selectAll('.item').classed('ghost', function(){ return this.dataset && this.dataset.category !== hoveredCategory; });
269
  d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
270
  const p = percent(d.data.value);
271
+ const catColor = colorOf(d.data.category);
272
+ let html = `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${catColor}\"></span><strong>${d.data.category}</strong></div>`;
273
+ html += `<div>${metric.name}</div>`;
274
+ html += `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>Value</strong><span style="margin-left:auto;text-align:right;">${d.data.value.toLocaleString()}</span></div>`;
275
+ /* Share row removed per request */
276
+ tipInner.innerHTML = html;
277
  tip.style.opacity = '1';
278
  })
279
  .on('mousemove', function(ev){
280
  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)`;
281
  })
282
+ .on('mouseleave', function(){
283
+ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)';
284
+ d3.select(container).classed('hovering', false);
285
+ d3.select(container).selectAll('path.slice').classed('ghost', false);
286
+ d3.select(legendHost).selectAll('.item').classed('ghost', false);
287
+ d3.select(this).attr('stroke','var(--surface-bg)');
288
+ });
289
 
290
  // Percentage labels (>= 3%)
291
  gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()
 
296
  }
297
 
298
  // HTML captions under the SVG (keep design)
299
+ const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center'; cell.appendChild(subtitleEl);
300
  const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);
301
  });
302
 
303
  // Container height flows naturally with HTML; nothing to do
304
+
305
+ // Reset global hover/ghost when leaving the plots area
306
+ plotsHost.onmouseleave = () => {
307
+ tip.style.opacity='0';
308
+ tip.style.transform='translate(-9999px, -9999px)';
309
+ d3.select(container).classed('hovering', false);
310
+ d3.select(container).selectAll('path.slice').classed('ghost', false);
311
+ d3.select(legendHost).selectAll('.item').classed('ghost', false);
312
+ };
313
  }
314
 
315
  async function init(){
app/src/content/embeds/d3-scatter.html CHANGED
@@ -1,7 +1,35 @@
1
- <div class="d3-scatter" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
  /* Frameless: no controls, no axes, only dots */
4
  .d3-scatter svg { display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  </style>
6
  <script>
7
  (() => {
@@ -29,7 +57,7 @@
29
  let tip = container.querySelector('.d3-tooltip'); let tipInner;
30
  if (!tip) {
31
  tip = document.createElement('div'); tip.className = 'd3-tooltip';
32
- Object.assign(tip.style, { position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' });
33
  tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
34
  } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
35
 
@@ -48,6 +76,8 @@
48
  const y = d3.scaleLinear();
49
  const color = d3.scaleOrdinal();
50
  const radius = () => 4;
 
 
51
 
52
  // Data loading (real): banner visualization positions by category
53
  async function fetchFirstAvailable(paths){
@@ -68,9 +98,10 @@
68
 
69
  function updateScales(data){
70
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
71
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
72
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
73
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
 
74
 
75
  width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
76
  const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
@@ -111,17 +142,24 @@
111
  dots.enter().append('circle').attr('class','dot')
112
  .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
113
  .attr('fill', fillFor).attr('fill-opacity', 0.85)
 
114
  .on('mouseenter', function(ev, d){
115
- d3.select(this).attr('stroke','rgba(0,0,0,0.9)').attr('stroke-width', 1);
116
- tipInner.innerHTML = `<div><strong>${d.label || 'Item'}</strong></div><div>(x: ${d.x.toFixed(2)}, y: ${d.y.toFixed(2)})</div><div>Category: ${d.group}</div>`;
 
 
 
 
 
117
  tip.style.opacity = '1';
118
  })
119
  .on('mousemove', function(ev){ const [mx, my] = d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
120
- .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('stroke','none'); })
121
  .merge(dots)
122
  .transition().duration(180)
123
  .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
124
- .attr('fill', fillFor).attr('fill-opacity', 0.85);
 
125
  dots.exit().remove();
126
 
127
  // Compute centroids per category
@@ -173,14 +211,14 @@
173
  .attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)
174
  .each(function(d){
175
  const base = color(d.category || 'Unknown') || 'var(--text-color)';
176
- const bg = getComputedStyle(document.documentElement).getPropertyValue('--surface-bg').trim() || '#fff';
177
  const bgNode = this.querySelector('text.label-bg');
178
  const fgNode = this.querySelector('text.label-fg');
179
  if (bgNode) {
180
  bgNode.textContent = d.category;
181
- bgNode.style.setProperty('fill', bg, 'important');
182
- bgNode.style.setProperty('stroke', bg);
183
- bgNode.style.setProperty('stroke-width', '10px');
184
  bgNode.style.setProperty('paint-order', 'stroke fill');
185
  bgNode.style.setProperty('font-weight','800');
186
  bgNode.style.setProperty('font-size','16px');
 
1
+ <div class="d3-scatter" ></div>
2
  <style>
3
  /* Frameless: no controls, no axes, only dots */
4
  .d3-scatter svg { display: block; }
5
+ /* Tooltip refined styling (align with filters-quad) */
6
+ .d3-scatter .d3-tooltip {
7
+ z-index: 20;
8
+ backdrop-filter: saturate(1.12) blur(8px);
9
+ }
10
+ .d3-scatter .d3-tooltip__inner {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: 6px;
14
+ min-width: 200px;
15
+ }
16
+ .d3-scatter .d3-tooltip__inner > div:first-child {
17
+ font-weight: 800;
18
+ letter-spacing: 0.1px;
19
+ margin-bottom: 0;
20
+ }
21
+ .d3-scatter .d3-tooltip__inner > div:nth-child(2) {
22
+ font-size: 11px;
23
+ color: var(--muted-color);
24
+ display: block;
25
+ margin-top: -4px;
26
+ margin-bottom: 2px;
27
+ letter-spacing: 0.1px;
28
+ }
29
+ .d3-scatter .d3-tooltip__inner > div:nth-child(n+3) {
30
+ padding-top: 6px;
31
+ border-top: 1px solid var(--border-color);
32
+ }
33
  </style>
34
  <script>
35
  (() => {
 
57
  let tip = container.querySelector('.d3-tooltip'); let tipInner;
58
  if (!tip) {
59
  tip = document.createElement('div'); tip.className = 'd3-tooltip';
60
+ Object.assign(tip.style, { position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity:'0', transition:'opacity .12s ease', backdropFilter:'saturate(1.12) blur(8px)' });
61
  tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
62
  } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
63
 
 
76
  const y = d3.scaleLinear();
77
  const color = d3.scaleOrdinal();
78
  const radius = () => 4;
79
+ let isDarkMode = false;
80
+ function getDotStrokeColor(){ return 'var(--muted-color)'; }
81
 
82
  // Data loading (real): banner visualization positions by category
83
  async function fetchFirstAvailable(paths){
 
98
 
99
  function updateScales(data){
100
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
101
+ isDarkMode = !!isDark;
102
+ const axisColor = "var(--page-bg)";
103
+ const tickColor = "var(--page-bg)";
104
+ const gridColor = "var(--page-bg)";
105
 
106
  width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
107
  const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
 
142
  dots.enter().append('circle').attr('class','dot')
143
  .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
144
  .attr('fill', fillFor).attr('fill-opacity', 0.85)
145
+ .attr('stroke', getDotStrokeColor()).attr('stroke-width', '0.75px')
146
  .on('mouseenter', function(ev, d){
147
+ d3.select(this).style('stroke','var(--text-color)').style('stroke-width','1.5px').attr('fill-opacity', 1);
148
+ const swatch = `<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="5" fill="${fillFor(d)}" /></svg>`;
149
+ tipInner.innerHTML = `
150
+ <div><strong>${d.label || 'Item'}</strong></div>
151
+ <div style="display:flex;align-items:center;gap:6px;">${swatch}<span>${d.group}</span></div>
152
+ <div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>x</strong><span style="margin-left:auto;text-align:right;">${d.x.toFixed(2)}</span></div>
153
+ <div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>y</strong><span style="margin-left:auto;text-align:right;">${d.y.toFixed(2)}</span></div>`;
154
  tip.style.opacity = '1';
155
  })
156
  .on('mousemove', function(ev){ const [mx, my] = d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
157
+ .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).style('stroke', getDotStrokeColor()).style('stroke-width','0.75px').attr('fill-opacity', 0.85); })
158
  .merge(dots)
159
  .transition().duration(180)
160
  .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
161
+ .attr('fill', fillFor).attr('fill-opacity', 0.85)
162
+ .attr('stroke', getDotStrokeColor()).attr('stroke-width','0.75px');
163
  dots.exit().remove();
164
 
165
  // Compute centroids per category
 
211
  .attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)
212
  .each(function(d){
213
  const base = color(d.category || 'Unknown') || 'var(--text-color)';
214
+ const bg = getComputedStyle(document.documentElement).getPropertyValue('--page-bg').trim() || '#fff';
215
  const bgNode = this.querySelector('text.label-bg');
216
  const fgNode = this.querySelector('text.label-fg');
217
  if (bgNode) {
218
  bgNode.textContent = d.category;
219
+ bgNode.style.setProperty('fill', "var(--page-bg)", 'important');
220
+ bgNode.style.setProperty('stroke', "var(--page-bg)");
221
+ bgNode.style.setProperty('stroke-width', '8px');
222
  bgNode.style.setProperty('paint-order', 'stroke fill');
223
  bgNode.style.setProperty('font-weight','800');
224
  bgNode.style.setProperty('font-size','16px');
app/src/content/embeds/heatmap.html DELETED
@@ -1,159 +0,0 @@
1
- <div class="d3-train-diagram" style="width:100%;margin:10px 0;"></div>
2
- <div class="caption">Hover blocks to show an explanation.</div>
3
- <style>
4
- .d3-train-diagram + .caption { margin-top: 8px; font-size: 14px; color: var(--muted-color); }
5
- </style>
6
- <script>
7
- (() => {
8
- const ensureD3 = (cb) => {
9
- if (window.d3 && typeof window.d3.select === 'function') return cb();
10
- let s = document.getElementById('d3-cdn-script');
11
- 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); }
12
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
13
- s.addEventListener('load', onReady, { once: true });
14
- if (window.d3) onReady();
15
- };
16
-
17
- const bootstrap = () => {
18
- const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
19
- const container = (mount && mount.querySelector && mount.querySelector('.d3-train-diagram')) || document.querySelector('.d3-train-diagram');
20
- if (!container) return;
21
- if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
22
-
23
- // Diagram spec
24
- const numBlocks = 7;
25
- const rows = [
26
- { key: 'model', label: 'Model', color: '#a78bfa' },
27
- { key: 'forward', label: 'Forward', color: '#14b8a6' },
28
- { key: 'backward', label: 'Backward', color: '#f59e0b' },
29
- { key: 'gradients', label: 'Gradients', color: 'var(--primary-color)' },
30
- { key: 'optimization', label: 'Optimization', color: '#10b981' },
31
- { key: 'updated', label: 'Updated', color: '#7c3aed' },
32
- ];
33
- const hoverText = {
34
- model: 'Each block represents a submodule of the model.',
35
- forward: 'Forward pass: compute activations layer by layer.',
36
- backward: 'Backpropagation: compute gradients through the chain.',
37
- gradients: 'Gradient accumulators for each layer.',
38
- optimization: 'Optimization step: update the weights.',
39
- updated: 'Parameters updated, ready for the next iteration.'
40
- };
41
-
42
- // SVG
43
- const svg = d3.select(container).append('svg').attr('width', '100%').style('display','block');
44
- const gRoot = svg.append('g');
45
- const gLegend = gRoot.append('foreignObject').attr('class','legend');
46
- const gArrows = gRoot.append('g').attr('class','arrows');
47
- const gBlocks = gRoot.append('g').attr('class','blocks');
48
- const gLabels = gRoot.append('g').attr('class','row-labels');
49
-
50
- // Tooltip (reuse style from others)
51
- container.style.position = container.style.position || 'relative';
52
- let tip = container.querySelector('.d3-tooltip'); let tipInner;
53
- if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style,{ position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
54
-
55
- // Layout
56
- let width=800, height=360; const margin = { top: 24, right: 180, bottom: 40, left: 32 };
57
- const x = d3.scaleBand().domain(d3.range(numBlocks)).paddingInner(0.2).paddingOuter(0.05);
58
- const y = d3.scaleBand().domain(d3.range(rows.length)).paddingInner(0.35);
59
-
60
- function updateScales(){
61
- width = container.clientWidth || 800;
62
- const rowH = Math.max(54, Math.min(80, Math.round(width / 12)));
63
- const innerHeight = rows.length * rowH;
64
- height = innerHeight + margin.top + margin.bottom;
65
- svg.attr('width', width).attr('height', height);
66
- const innerWidth = width - margin.left - margin.right;
67
- gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
68
-
69
- x.range([0, innerWidth]);
70
- y.range([0, innerHeight]);
71
-
72
- return { innerWidth, innerHeight };
73
- }
74
-
75
- function render(){
76
- const { innerWidth, innerHeight } = updateScales();
77
-
78
- // Legend right side
79
- const legendWidth = 160, legendHeight = rows.length * 20;
80
- gLegend.attr('x', innerWidth + 16).attr('y', 0).attr('width', legendWidth).attr('height', legendHeight);
81
- const lroot = gLegend.selectAll('div').data([0]).join('xhtml:div');
82
- lroot.html(`
83
- <div style="display:flex;flex-direction:column;gap:8px;">
84
- ${rows.map(r => `<div style=\"display:flex;align-items:center;gap:8px;\"><span style=\"width:14px;height:14px;background:${r.color};border-radius:4px;display:inline-block\"></span><span>${r.label}</span></div>`).join('')}
85
- </div>
86
- `);
87
-
88
- // Row labels on the right side aligned to centers
89
- gLabels.selectAll('*').remove();
90
- gLabels.selectAll('text').data(rows).join('text')
91
- .attr('x', innerWidth + 16)
92
- .attr('y', (_,i)=> y(i) + y.bandwidth()/2)
93
- .attr('dy','0.35em')
94
- .style('font-size','14px')
95
- .style('fill','var(--text-color)')
96
- .text(d=>d.label);
97
-
98
- // Blocks per row
99
- const blockW = Math.min(84, x.bandwidth());
100
- const blockH = Math.min(52, Math.round(y.bandwidth() * 0.8));
101
- const blocks = [];
102
- rows.forEach((row, ri) => {
103
- for (let i=0;i<numBlocks;i++) blocks.push({ row, ri, i });
104
- });
105
- const sel = gBlocks.selectAll('rect.block').data(blocks, d=>`${d.row.key}-${d.i}`);
106
- sel.join(
107
- enter => enter.append('rect').attr('class','block')
108
- .attr('x', d=>x(d.i))
109
- .attr('y', d=>y(d.ri) + (y.bandwidth()-blockH)/2)
110
- .attr('rx', 12).attr('ry', 12)
111
- .attr('width', blockW)
112
- .attr('height', blockH)
113
- .attr('fill', d=>d.row.color)
114
- .attr('opacity', 0.95)
115
- .attr('stroke', 'rgba(0,0,0,0.18)')
116
- .attr('filter', 'url(#shadow)')
117
- .on('mouseenter', function(ev, d){
118
- d3.select(this).attr('opacity', 1.0).attr('stroke-width', 1.2);
119
- tipInner.innerHTML = `<div><strong>${d.row.label}</strong></div><div>${hoverText[d.row.key]}</div>`;
120
- tip.style.opacity = '1';
121
- })
122
- .on('mousemove', function(ev){ const [mx,my] = d3.pointer(ev, container); tip.style.transform = `translate(${mx+12}px, ${my+12}px)`; })
123
- .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px,-9999px)'; d3.select(this).attr('opacity', 0.95).attr('stroke-width', 1); })
124
- );
125
-
126
- // Arrows forward/backward
127
- gArrows.selectAll('*').remove();
128
- const arrowY = (ri) => y(ri) + y.bandwidth()/2;
129
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
130
- const arrowColor = isDark ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.55)';
131
- const defs = svg.select('defs').empty() ? svg.append('defs') : svg.select('defs');
132
- const marker = defs.append('marker').attr('id','arrow').attr('viewBox','0 0 10 10').attr('refX', 10).attr('refY', 5).attr('markerWidth', 6).attr('markerHeight', 6).attr('orient','auto-start-reverse');
133
- marker.append('path').attr('d','M 0 0 L 10 5 L 0 10 z').attr('fill', arrowColor);
134
- // drop shadow filter
135
- const flt = defs.append('filter').attr('id','shadow').attr('x','-20%').attr('y','-20%').attr('width','140%').attr('height','140%');
136
- flt.append('feDropShadow').attr('dx','0').attr('dy','1').attr('stdDeviation','1.5').attr('flood-color','rgba(0,0,0,0.18)');
137
- // Forward arrow (top orientation)
138
- gArrows.append('line').attr('x1', x(0)).attr('y1', arrowY(1)-28).attr('x2', x(numBlocks-1)+blockW).attr('y2', arrowY(1)-28)
139
- .attr('stroke', rows[1].color).attr('stroke-width', 4).attr('marker-end','url(#arrow)');
140
- // Backward arrow (orange, reversed)
141
- gArrows.append('line').attr('x1', x(numBlocks-1)+blockW).attr('y1', arrowY(2)-20).attr('x2', x(0)).attr('y2', arrowY(2)-20)
142
- .attr('stroke', rows[2].color).attr('stroke-width', 4).attr('marker-end','url(#arrow)');
143
- // Vertical arrows (gradients down, updated up)
144
- const midX = x(3) + blockW/2;
145
- gArrows.append('line').attr('x1', midX).attr('y1', arrowY(2)+blockH/2+4).attr('x2', midX).attr('y2', arrowY(3)-blockH/2-6)
146
- .attr('stroke', rows[3].color).attr('stroke-width', 3).attr('marker-end','url(#arrow)');
147
- gArrows.append('line').attr('x1', midX).attr('y1', arrowY(4)+blockH/2+6).attr('x2', midX).attr('y2', arrowY(5)-blockH/2-6)
148
- .attr('stroke', rows[5].color).attr('stroke-width', 3).attr('marker-end','url(#arrow)');
149
- }
150
-
151
- render();
152
- if (window.ResizeObserver) { const ro = new ResizeObserver(()=>render()); ro.observe(container); } else { window.addEventListener('resize', render); }
153
- };
154
-
155
- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
156
- })();
157
- </script>
158
-
159
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/vibe-code-d3-embeds-directives.md CHANGED
@@ -1,7 +1,69 @@
1
  ## Embed Chart Authoring Guidelines
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  Authoring rules for creating a new interactive chart as a single self-contained `.html` file under `src/content/embeds/`. These conventions are derived from `d3-bar.html`, `d3-comparison.html`, `d3-neural.html`, `d3-line.html`, and `d3-pie.html`.
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  ### 1) File, naming, and structure
6
  - Name files with a clear prefix and purpose: `d3-<type>.html` (e.g., `d3-scatter.html`).
7
  - Wrap everything in a single `<div class="<root-class>">`, a `<style>` block scoped to that root class, and a `<script>` IIFE.
@@ -10,7 +72,7 @@ Authoring rules for creating a new interactive chart as a single self-contained
10
 
11
  Minimal skeleton:
12
  ```html
13
- <div class="d3-yourchart" style="width:100%;margin:10px 0;"></div>
14
  <style>
15
  .d3-yourchart {/* all styles scoped to the root */}
16
  </style>
@@ -92,16 +154,77 @@ Minimal skeleton:
92
  ### 3) Styling and theming
93
  - Scope all rules under the root class; do not style `body`, `svg` globally.
94
  - Use CSS variables for theme alignment: `--primary-color`, `--text-color`, `--muted-color`, `--surface-bg`, `--border-color`.
 
95
  - For dark mode–aware strokes/ticks, either:
96
  - Read `document.documentElement.getAttribute('data-theme') === 'dark'`, or
97
  - Prefer CSS-only where possible.
98
  - Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  ### 4) Controls (labels, selects, sliders)
101
- - Compose controls as plain HTML elements appended inside the root container.
102
  - Style selects like in `d3-line.html`/`d3-bar.html` for consistency (rounded 8px, custom caret via data-URI, focus ring).
103
  - Use `<label>` wrapping the input for accessibility; set concise text (e.g., "Metric", "Model Size").
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  ### 5) Tooltip pattern
106
  - Create a single `.d3-tooltip` absolutely positioned inside the container.
107
  - Show on hover, hide on leave; position using `d3.pointer(event, container)` plus a small offset.
@@ -113,6 +236,89 @@ Minimal skeleton:
113
  - Implement `fetchFirstAvailable(paths)`; try in order with `cache:'no-cache'`; handle errors gracefully with a red `<pre>` message.
114
  - For images or JSON models, mirror the same approach (see `d3-comparison.html`, `d3-neural.html`).
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  ### 7) Responsiveness and layout
117
  - Compute `width = container.clientWidth`, and a height derived from width (e.g., `width / 3`), with a sensible minimum height.
118
  - Maintain a `margin` object and derive `innerWidth/innerHeight` for plots.
@@ -120,9 +326,50 @@ Minimal skeleton:
120
  - Recompute scales/axes/grid on every render.
121
 
122
  ### 8) Legends and labels
123
- - Use `foreignObject` + inline HTML to render compact legends that wrap nicely (see `d3-line.html`, `d3-pie.html`).
124
- - For axes, remove and re-append groups each render (simple and predictable), or update in place if needed.
125
  - Always add axis labels when applicable (e.g., `Step`, `Value`).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  ### 9) Accessibility
128
  - Provide `alt` attributes on `<img>` (see `d3-comparison.html`).
@@ -150,18 +397,19 @@ Minimal skeleton:
150
  ### 14) Conventions checklist (before committing)
151
  - Root class is unique and matches file name (`d3-<type>`).
152
  - No globals added; script wrapped in an IIFE.
153
- - `data-mounded` guard is present to avoid double-mount.
 
154
  - Uses CSS variables for colors; dark-mode friendly.
155
  - Responsive: recomputes layout on resize; uses `ResizeObserver`.
156
- - Controls are accessible and consistently styled.
157
- - Tooltip is present (if hover/inspect is required).
158
  - Data loading includes public-path-first strategy and graceful error.
159
- - Axes/labels/legends are legible at small widths.
160
  - Code is easy to skim: clear naming, early returns, short functions.
161
 
162
  ### 15) Example: small bar chart (structure only)
163
  ```html
164
- <div class="d3-mini-bar" style="width:100%;margin:10px 0;"></div>
165
  <style>
166
  .d3-mini-bar .bar { stroke: none; }
167
  </style>
 
1
  ## Embed Chart Authoring Guidelines
2
 
3
+ ### Quickstart (TL;DR)
4
+ - Create a single self-contained HTML fragment: root div + scoped style + IIFE script.
5
+ - Draw marks/axes in SVG; render UI (legend and controls) in HTML.
6
+ - Place legend and controls ABOVE the chart. Include a legend title "Legend" and a select labeled "Metric" when relevant.
7
+ - Load data from public `/data` first, then fall back to `assets/data`.
8
+ - Use `window.ColorPalettes` for colors; stick to CSS variables for theming.
9
+
10
+ Minimal header markup:
11
+ ```html
12
+ <div class="legend">
13
+ <div class="legend-title">Legend</div>
14
+ <div class="items"></div>
15
+ <!-- items populated by JS: <span class="item"><span class="swatch"></span><span>Name</span></span> -->
16
+ </div>
17
+ <div class="controls">
18
+ <div class="control-group">
19
+ <label for="metric-select-<id>">Metric</label>
20
+ <select id="metric-select-<id>"></select>
21
+ </div>
22
+ <!-- optional: other controls -->
23
+ </div>
24
+ ```
25
+
26
+ See also: `d3-line-simple.html`, `d3-line-quad.html`, `d3-benchmark.html`.
27
+
28
  Authoring rules for creating a new interactive chart as a single self-contained `.html` file under `src/content/embeds/`. These conventions are derived from `d3-bar.html`, `d3-comparison.html`, `d3-neural.html`, `d3-line.html`, and `d3-pie.html`.
29
 
30
+ ### A) Colors & palettes (MANDATORY)
31
+ - Always obtain color arrays from `window.ColorPalettes`; do not hardcode palettes.
32
+ - Use the categorical/sequential/diverging helpers and the current primary color.
33
+ - If you change `--primary-color` dynamically, call `window.ColorPalettes.refresh()` so listeners update.
34
+
35
+ Usage:
36
+ ```js
37
+ // Usage (with explicit counts)
38
+ const cat = window.ColorPalettes.getColors('categorical', 8);
39
+ const seq = window.ColorPalettes.getColors('sequential', 8);
40
+ const div = window.ColorPalettes.getColors('diverging', 7);
41
+
42
+ // For current primary color string
43
+ const primaryHex = window.ColorPalettes.getPrimary();
44
+
45
+ // If you change --primary-color dynamically, call refresh to notify listeners
46
+ document.documentElement.style.setProperty('--primary-color', '#6D4AFF');
47
+ window.ColorPalettes.refresh();
48
+ ```
49
+
50
+ Notes:
51
+ - Keep chart accents (lines, markers, selection) aligned with `--primary-color`.
52
+ - Prefer CSS variables for fills/strokes when possible; derive series colors via `ColorPalettes`.
53
+ - Provide a graceful fallback to CSS variables if `window.ColorPalettes` is unavailable.
54
+
55
+ ### B) Layout & form elements (HTML-only)
56
+ - All UI controls (labels, selects, sliders, buttons, toggles) must be plain HTML inside the root container.
57
+ - Do not draw controls with SVG; style them consistently (rounded 8px, custom caret, focus ring).
58
+ - Use `<label>` wrapping inputs for accessibility and concise text (e.g., "Metric", "Model Size").
59
+ - Manage layout with CSS inside the scoped `<style>` for the root class; avoid global rules.
60
+
61
+ ### C) SVG scope: charts only; UI in HTML
62
+ - SVG is for chart primitives (marks, axes, gridlines) only.
63
+ - Put legends and controls in HTML (adjacent DOM is preferred; `foreignObject` only if necessary).
64
+ - Tooltips are HTML positioned absolutely inside the root container.
65
+ - Details: see sections 4 (controls), 5 (tooltips), 8 (legends).
66
+
67
  ### 1) File, naming, and structure
68
  - Name files with a clear prefix and purpose: `d3-<type>.html` (e.g., `d3-scatter.html`).
69
  - Wrap everything in a single `<div class="<root-class>">`, a `<style>` block scoped to that root class, and a `<script>` IIFE.
 
72
 
73
  Minimal skeleton:
74
  ```html
75
+ <div class="d3-yourchart"></div>
76
  <style>
77
  .d3-yourchart {/* all styles scoped to the root */}
78
  </style>
 
154
  ### 3) Styling and theming
155
  - Scope all rules under the root class; do not style `body`, `svg` globally.
156
  - Use CSS variables for theme alignment: `--primary-color`, `--text-color`, `--muted-color`, `--surface-bg`, `--border-color`.
157
+ - Derive palette colors from `window.ColorPalettes` (categorical, sequential, diverging); do not hardcode arrays.
158
  - For dark mode–aware strokes/ticks, either:
159
  - Read `document.documentElement.getAttribute('data-theme') === 'dark'`, or
160
  - Prefer CSS-only where possible.
161
  - Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
162
 
163
+ Standard axis/tick/grid colors (reuse across charts):
164
+
165
+ ```css
166
+ .your-root-class {
167
+ --axis-color: rgba(0,0,0,0.25);
168
+ --tick-color: rgba(0,0,0,0.55);
169
+ --grid-color: rgba(0,0,0,0.05);
170
+ }
171
+ [data-theme="dark"] .your-root-class {
172
+ --axis-color: rgba(255,255,255,0.25);
173
+ --tick-color: rgba(255,255,255,0.70);
174
+ --grid-color: rgba(255,255,255,0.08);
175
+ }
176
+ /* Example axis application (D3) */
177
+ g.axis-x, g.axis-y { }
178
+ /* In JS after calling axis: */
179
+ // g.selectAll('path, line').attr('stroke', 'var(--axis-color)');
180
+ // g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px');
181
+ /* Gridlines: */
182
+ // grid.call(d3.axisLeft(y).ticks(6).tickSize(-innerWidth).tickFormat(''))
183
+ // .call(g => g.selectAll('.tick line').attr('stroke','var(--grid-color)'));
184
+ ```
185
+
186
+ #### 3.1) Text on fixed-colored backgrounds
187
+
188
+ - When rendering text over cells/areas with fixed background colors (that do not change with theme), compute a readable text style once from the actual background color.
189
+ - Use `window.ColorPalettes.getTextStyleForBackground(bgCss, { blend: 0.6 })` when available; avoid tying text color to dark mode toggles since the background is constant.
190
+ - Do not re-evaluate on theme toggle unless the background color itself changes.
191
+
192
+ Example:
193
+ ```js
194
+ const bg = getComputedStyle(cellRect).fill; // e.g., 'rgb(12, 34, 56)'
195
+ const style = window.ColorPalettes?.getTextStyleForBackground
196
+ ? window.ColorPalettes.getTextStyleForBackground(bg, { blend: 0.6 })
197
+ : { fill: 'var(--text-color)' };
198
+ textSel.style('fill', style.fill);
199
+ ```
200
+
201
  ### 4) Controls (labels, selects, sliders)
202
+ - Compose controls as plain HTML elements appended inside the root container (no SVG UI).
203
  - Style selects like in `d3-line.html`/`d3-bar.html` for consistency (rounded 8px, custom caret via data-URI, focus ring).
204
  - Use `<label>` wrapping the input for accessibility; set concise text (e.g., "Metric", "Model Size").
205
 
206
+ #### 4.1) Required select label: "Metric"
207
+ - When a select is used to switch metrics, include a visible label above the select with the exact text "Metric".
208
+ - Preferred markup (grouped for easy vertical stacking):
209
+
210
+ ```html
211
+ <div class="controls">
212
+ <div class="control-group">
213
+ <label for="metric-select-<unique>">Metric</label>
214
+ <select id="metric-select-<unique>"></select>
215
+ </div>
216
+ </div>
217
+ ```
218
+
219
+ Minimal CSS (match project styles):
220
+
221
+ ```css
222
+ .controls { display:flex; gap:16px; align-items:center; justify-content:flex-end; flex-wrap:wrap; }
223
+ .controls .control-group { display:flex; flex-direction:column; align-items:flex-start; gap:6px; }
224
+ .controls label { font-size:12px; font-weight:700; color: var(--text-color); }
225
+ .controls select { font-size:12px; padding:8px 28px 8px 10px; border:1px solid var(--border-color); border-radius:8px; background: var(--surface-bg); color: var(--text-color); }
226
+ ```
227
+
228
  ### 5) Tooltip pattern
229
  - Create a single `.d3-tooltip` absolutely positioned inside the container.
230
  - Show on hover, hide on leave; position using `d3.pointer(event, container)` plus a small offset.
 
236
  - Implement `fetchFirstAvailable(paths)`; try in order with `cache:'no-cache'`; handle errors gracefully with a red `<pre>` message.
237
  - For images or JSON models, mirror the same approach (see `d3-comparison.html`, `d3-neural.html`).
238
 
239
+ #### 6.1) Data props (HtmlEmbed → embed)
240
+
241
+ - HtmlEmbed accepts an optional `data` prop that can be a string (single file) or an array of strings (multiple files).
242
+ - This prop is passed to the fragment via the `data-datafiles` HTML attribute.
243
+ - In the embed script, read this attribute from the closest ancestor that carries it (the `HtmlEmbed` wrapper), not necessarily the chart’s direct container.
244
+ - Recommended normalization: if a value contains no slash, automatically prefix it with `/data/` to target the public data folder.
245
+ - If `data` is not provided, keep the usual fallback (public, then `assets/data`).
246
+
247
+ Optional configuration (e.g., default metric):
248
+
249
+ ```js
250
+ // In HtmlEmbed usage (MDX):
251
+ <HtmlEmbed src="d3-line-simple.html" data="internal_deduplication.csv" config={{ defaultMetric: 'average_rank' }} />
252
+
253
+ // In the embed script (read from closest ancestor):
254
+ let mountEl = container;
255
+ while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
256
+ mountEl = mountEl.parentElement;
257
+ }
258
+ let providedConfig = null;
259
+ try {
260
+ const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
261
+ if (cfg && cfg.trim()) providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
262
+ } catch(_) {}
263
+ // Example: selecting initial metric if present
264
+ const desired = providedConfig && providedConfig.defaultMetric ? String(providedConfig.defaultMetric) : null;
265
+ ```
266
+
267
+ Examples (MDX):
268
+
269
+ ```mdx
270
+ <HtmlEmbed src="d3-line-simple.html" title="Run A" data="formatting_filters.csv" />
271
+ <HtmlEmbed src="d3-line-simple.html" title="Run B" data="relevance_filters.csv" />
272
+
273
+ <HtmlEmbed
274
+ src="d3-line-simple.html"
275
+ title="Comparison A vs B"
276
+ data={[ 'formatting_filters.csv', 'relevance_filters.csv' ]}
277
+ />
278
+ ```
279
+
280
+ Reading on the embed side (JS):
281
+
282
+ ```js
283
+ // Find the closest ancestor that carries the attribute
284
+ let mountEl = container;
285
+ while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
286
+ mountEl = mountEl.parentElement;
287
+ }
288
+ let providedData = null;
289
+ try {
290
+ const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
291
+ if (attr && attr.trim()) {
292
+ providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
293
+ }
294
+ } catch(_) {}
295
+
296
+ const DEFAULT_CSV = '/data/formatting_filters.csv';
297
+ const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
298
+ const normalizeInput = (inp) => Array.isArray(inp)
299
+ ? inp.map(ensureDataPrefix)
300
+ : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
301
+
302
+ const CSV_PATHS = Array.isArray(providedData)
303
+ ? normalizeInput(providedData)
304
+ : (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [
305
+ DEFAULT_CSV,
306
+ './assets/data/formatting_filters.csv',
307
+ '../assets/data/formatting_filters.csv',
308
+ '../../assets/data/formatting_filters.csv'
309
+ ]);
310
+
311
+ const fetchFirstAvailable = async (paths) => {
312
+ for (const p of paths) {
313
+ try {
314
+ const r = await fetch(p, { cache: 'no-cache' });
315
+ if (r.ok) return await r.text();
316
+ } catch(_){}
317
+ }
318
+ throw new Error('CSV not found');
319
+ };
320
+ ```
321
+
322
  ### 7) Responsiveness and layout
323
  - Compute `width = container.clientWidth`, and a height derived from width (e.g., `width / 3`), with a sensible minimum height.
324
  - Maintain a `margin` object and derive `innerWidth/innerHeight` for plots.
 
326
  - Recompute scales/axes/grid on every render.
327
 
328
  ### 8) Legends and labels
329
+ - Prefer HTML for legends for wrapping and accessibility; avoid SVG-based legends.
 
330
  - Always add axis labels when applicable (e.g., `Step`, `Value`).
331
+ - Standardize legend swatch size: 14×14px, border-radius 3px, 1px border `var(--border-color)`.
332
+
333
+ #### 8.1) Required legend title: "Legend"
334
+ - Always render a visible title above legend items with the exact text "Legend".
335
+ - Canonical markup:
336
+
337
+ ```html
338
+ <div class="legend">
339
+ <div class="legend-title">Legend</div>
340
+ <div class="items">
341
+ <!-- <span class="item"><span class="swatch"></span><span>Series A</span></span> ... -->
342
+ </div>
343
+ </div>
344
+ ```
345
+
346
+ Minimal CSS (match project styles):
347
+
348
+ ```css
349
+ .legend { display:flex; flex-direction:column; align-items:flex-start; gap:6px; }
350
+ .legend-title { font-size:12px; font-weight:700; color: var(--text-color); }
351
+ .legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; }
352
+ .legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; font-size:12px; color: var(--text-color); }
353
+ .legend .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); }
354
+ ```
355
+
356
+ Recommended JS pattern to (re)build the legend:
357
+
358
+ ```js
359
+ function makeLegend(seriesNames, colorFor) {
360
+ let legend = container.querySelector('.legend');
361
+ if (!legend) { legend = document.createElement('div'); legend.className = 'legend'; container.appendChild(legend); }
362
+ let title = legend.querySelector('.legend-title'); if (!title) { title = document.createElement('div'); title.className = 'legend-title'; title.textContent = 'Legend'; legend.appendChild(title); }
363
+ let items = legend.querySelector('.items'); if (!items) { items = document.createElement('div'); items.className = 'items'; legend.appendChild(items); }
364
+ items.innerHTML = '';
365
+ seriesNames.forEach(name => {
366
+ const el = document.createElement('span'); el.className = 'item';
367
+ const sw = document.createElement('span'); sw.className = 'swatch'; sw.style.background = colorFor(name);
368
+ const txt = document.createElement('span'); txt.textContent = name;
369
+ el.appendChild(sw); el.appendChild(txt); items.appendChild(el);
370
+ });
371
+ }
372
+ ```
373
 
374
  ### 9) Accessibility
375
  - Provide `alt` attributes on `<img>` (see `d3-comparison.html`).
 
397
  ### 14) Conventions checklist (before committing)
398
  - Root class is unique and matches file name (`d3-<type>`).
399
  - No globals added; script wrapped in an IIFE.
400
+ - `data-mounted` guard is present to avoid double-mount.
401
+ - Colors come from `window.ColorPalettes` (no hardcoded arrays); `--primary-color` respected.
402
  - Uses CSS variables for colors; dark-mode friendly.
403
  - Responsive: recomputes layout on resize; uses `ResizeObserver`.
404
+ - Controls are HTML-only, accessible, and consistently styled.
405
+ - Legends and tooltips are HTML, not SVG.
406
  - Data loading includes public-path-first strategy and graceful error.
407
+ - Axes/labels are legible at small widths.
408
  - Code is easy to skim: clear naming, early returns, short functions.
409
 
410
  ### 15) Example: small bar chart (structure only)
411
  ```html
412
+ <div class="d3-mini-bar"></div>
413
  <style>
414
  .d3-mini-bar .bar { stroke: none; }
415
  </style>
app/src/scripts/color-palettes.js CHANGED
@@ -164,6 +164,54 @@
164
  const mo = new MutationObserver(() => updatePalettes());
165
  mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
166
  setInterval(updatePalettes, 400);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  window.ColorPalettes = {
168
  refresh: updatePalettes,
169
  getPrimary: () => getPrimaryHex(),
@@ -174,7 +222,9 @@
174
  if (key === 'sequential') return generators.sequential(primary, total);
175
  if (key === 'diverging') return generators.diverging(primary, total);
176
  return [];
177
- }
 
 
178
  };
179
  };
180
 
 
164
  const mo = new MutationObserver(() => updatePalettes());
165
  mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
166
  setInterval(updatePalettes, 400);
167
+ // Utility: choose high-contrast (or softened) text style against an arbitrary background color
168
+ const pickTextStyleForBackground = (bgCss, opts = {}) => {
169
+ const cssRoot = document.documentElement;
170
+ const getCssVar = (name) => {
171
+ try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
172
+ };
173
+ const resolveCssToRgb01 = (css) => {
174
+ const rgb = parseCssColorToRgb(css);
175
+ if (!rgb) return null;
176
+ return rgb; // already 0..1
177
+ };
178
+ const mixRgb01 = (a, b, t) => ({ r: a.r*(1-t)+b.r*t, g: a.g*(1-t)+b.g*t, b: a.b*(1-t)+b.b*t });
179
+ const relLum = (rgb) => {
180
+ const f = (u) => srgbToLinear(u);
181
+ return 0.2126*f(rgb.r) + 0.7152*f(rgb.g) + 0.0722*f(rgb.b);
182
+ };
183
+ const contrast = (fg, bg) => {
184
+ const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1,L2), b = Math.min(L1,L2);
185
+ return (a + 0.05) / (b + 0.05);
186
+ };
187
+ try {
188
+ const bg = resolveCssToRgb01(bgCss);
189
+ if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
190
+ const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
191
+ const candidates = candidatesCss
192
+ .map(css => ({ css, rgb: resolveCssToRgb01(css) }))
193
+ .filter(x => !!x.rgb);
194
+ // Pick the max contrast
195
+ let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
196
+ for (let i=1;i<candidates.length;i++){
197
+ const cr = contrast(candidates[i].rgb, bg);
198
+ if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
199
+ }
200
+ // Optional softening via blend factor (0..1), blending towards muted color
201
+ const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
202
+ let finalRgb = best.rgb;
203
+ if (blend > 0) {
204
+ const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
205
+ const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
206
+ finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
207
+ }
208
+ const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
209
+ const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40*haloStrength})` : `rgba(0,0,0,${0.30 + 0.30*haloStrength})`;
210
+ return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
211
+ } catch {
212
+ return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
213
+ }
214
+ };
215
  window.ColorPalettes = {
216
  refresh: updatePalettes,
217
  getPrimary: () => getPrimaryHex(),
 
222
  if (key === 'sequential') return generators.sequential(primary, total);
223
  if (key === 'diverging') return generators.diverging(primary, total);
224
  return [];
225
+ },
226
+ getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
227
+ chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
228
  };
229
  };
230
 
app/src/styles/_variables.css CHANGED
@@ -20,6 +20,7 @@
20
  /* Text & Surfaces */
21
  --page-bg: #ffffff;
22
  --text-color: rgba(0,0,0,.85);
 
23
  --muted-color: rgba(0,0,0,.6);
24
  --border-color: rgba(0,0,0,.1);
25
  --surface-bg: #fafafa;
@@ -81,6 +82,11 @@
81
  --z-overlay: 1000;
82
  --z-modal: 1100;
83
  --z-tooltip: 1200;
 
 
 
 
 
84
  }
85
 
86
  /* ============================================================================ */
@@ -93,6 +99,12 @@
93
  --border-color: rgba(255,255,255,.15);
94
  --surface-bg: #12151b;
95
  --code-bg: #12151b;
 
 
 
 
 
 
96
 
97
  /* Primary (lower L in dark) */
98
  --primary-color: oklch(from var(--primary-base) calc(l - 0.08) c h);
 
20
  /* Text & Surfaces */
21
  --page-bg: #ffffff;
22
  --text-color: rgba(0,0,0,.85);
23
+ --transparent-page-contrast: rgba(255,255,255,.85);
24
  --muted-color: rgba(0,0,0,.6);
25
  --border-color: rgba(0,0,0,.1);
26
  --surface-bg: #fafafa;
 
82
  --z-overlay: 1000;
83
  --z-modal: 1100;
84
  --z-tooltip: 1200;
85
+
86
+ /* Charts (global) */
87
+ --axis-color: var(--text-color);
88
+ --tick-color: var(--muted-color);
89
+ --grid-color: rgba(0,0,0,.08);
90
  }
91
 
92
  /* ============================================================================ */
 
99
  --border-color: rgba(255,255,255,.15);
100
  --surface-bg: #12151b;
101
  --code-bg: #12151b;
102
+ --transparent-page-contrast: rgba(0,0,0,.85);
103
+
104
+ /* Charts (global) */
105
+ --axis-color: var(--text-color);
106
+ --tick-color: var(--muted-color);
107
+ --grid-color: rgba(255,255,255,.10);
108
 
109
  /* Primary (lower L in dark) */
110
  --primary-color: oklch(from var(--primary-base) calc(l - 0.08) c h);
app/src/styles/components/_card.css ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ .card {
2
+ background: var(--surface-bg);
3
+ border: 1px solid var(--border-color);
4
+ border-radius: 10px;
5
+ padding: var(--spacing-2);
6
+ z-index: calc(var(--z-elevated) + 1);
7
+ position: relative;
8
+ margin-bottom: var(--block-spacing-y);
9
+ }
app/src/styles/global.css CHANGED
@@ -7,6 +7,7 @@
7
  @import './components/_button.css';
8
  @import './components/_table.css';
9
  @import './components/_tag.css';
 
10
 
11
  .demo-wide,
12
  .demo-full-width {
 
7
  @import './components/_button.css';
8
  @import './components/_table.css';
9
  @import './components/_tag.css';
10
+ @import './components/_card.css';
11
 
12
  .demo-wide,
13
  .demo-full-width {