thibaud frere commited on
Commit
941cf22
·
1 Parent(s): 6105bcd
app/src/components/ResponsiveImage.astro ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ // @ts-ignore - types provided by Astro at runtime
3
+ import { Image } from 'astro:assets';
4
+
5
+ interface Props {
6
+ /** Source image imported via astro:assets */
7
+ src: any;
8
+ /** Alt text for accessibility */
9
+ alt: string;
10
+ /** Optional HTML string caption (use slot caption for rich content) */
11
+ caption?: string;
12
+ /** Optional class to apply on the <figure> wrapper when caption is used */
13
+ figureClass?: string;
14
+ /** Any additional attributes should be forwarded to the underlying <Image> */
15
+ [key: string]: any;
16
+ }
17
+
18
+ const { caption, figureClass, ...imgProps } = Astro.props as Props;
19
+ const hasCaptionSlot = Astro.slots.has('caption');
20
+ const hasCaption = hasCaptionSlot || (typeof caption === 'string' && caption.length > 0);
21
+ ---
22
+
23
+ {hasCaption ? (
24
+ <figure class={figureClass}>
25
+ <Image {...imgProps} />
26
+ <figcaption>
27
+ {hasCaptionSlot ? (
28
+ <slot name="caption" />
29
+ ) : (
30
+ caption && <span set:html={caption} />
31
+ )}
32
+ </figcaption>
33
+ </figure>
34
+ ) : (
35
+ <Image {...imgProps} />
36
+ )}
37
+
38
+
app/src/content/article.mdx CHANGED
@@ -28,7 +28,7 @@ import AvailableBlocks from "./chapters/available-blocks.mdx";
28
  import GettingStarted from "./chapters/getting-started.mdx";
29
 
30
  <Sidenote>
31
- Welcome to this single‑page research article template. It helps you publish **clear**, **modern**, and **interactive** technical writing with **minimal setup**. Grounded in **web‑native scholarship**, it favors **interactive explanations**, careful notation, and **inspectable examples** over static snapshots.
32
 
33
  It offers a **ready‑to‑publish, all‑in‑one workflow** so you can **focus on ideas** rather than infrastructure.
34
  <Fragment slot="aside">
 
28
  import GettingStarted from "./chapters/getting-started.mdx";
29
 
30
  <Sidenote>
31
+ Welcome to this single‑page **research article template**. It helps you publish **clear**, **modern**, and **interactive technical writing** with **minimal setup**. Grounded in **web‑native scholarship**, it favors **interactive explanations**, careful notation, and **inspectable examples** over static snapshots.
32
 
33
  It offers a **ready‑to‑publish, all‑in‑one workflow** so you can **focus on ideas** rather than infrastructure.
34
  <Fragment slot="aside">
app/src/content/assets/data/comparison/id_1_rank_1_sim_1.000.png ADDED

Git LFS Details

  • SHA256: e8e932d36f10201ebeca11747a61547d0f752c27fc1f908c3a22eef113975e22
  • Pointer size: 131 Bytes
  • Size of remote file: 313 kB
app/src/content/assets/data/comparison/id_1_rank_2_sim_0.165.png ADDED

Git LFS Details

  • SHA256: 71165c6b2d6d255e190e1ef5a3581402cf7fff55bc977f6a892e6f5fcc782884
  • Pointer size: 131 Bytes
  • Size of remote file: 426 kB
app/src/content/assets/data/comparison/id_1_rank_3_sim_0.143.png ADDED

Git LFS Details

  • SHA256: f4f4422d8fdb249acc6fd6e25307d88c759769e302f8a28726a7751162763c99
  • Pointer size: 131 Bytes
  • Size of remote file: 198 kB
app/src/content/assets/data/comparison/id_2_rank_1_sim_1.000.png ADDED

Git LFS Details

  • SHA256: e91e4eaddf3bf08798880367000cfb070e0efbce39c3305d712834e2217b45b4
  • Pointer size: 130 Bytes
  • Size of remote file: 31.8 kB
app/src/content/assets/data/comparison/id_2_rank_2_sim_0.978.png ADDED

Git LFS Details

  • SHA256: ada0f0d0f2e65a3c83db32af1fcfb02c8f69c5a38f63d59a9cc2a0f0bbf1a3d7
  • Pointer size: 130 Bytes
  • Size of remote file: 33 kB
app/src/content/assets/data/comparison/id_2_rank_3_sim_0.975.png ADDED

Git LFS Details

  • SHA256: 9b68b4348567c954f1bf9e70aa3f5f867027ae63a1aa8e45651de62632f54677
  • Pointer size: 130 Bytes
  • Size of remote file: 32.1 kB
app/src/content/assets/data/comparison/id_3_rank_1_sim_0.936.png ADDED

Git LFS Details

  • SHA256: ae9f919d75c7342fcc11870938c37a29cede69ba2b906e3e494c114421c0e0b5
  • Pointer size: 131 Bytes
  • Size of remote file: 210 kB
app/src/content/assets/data/comparison/id_3_rank_2_sim_0.686.png ADDED

Git LFS Details

  • SHA256: 5ef2e71424cab95f0b178d47ef45ef61e8ef6ae7f9bff733066f5c1fd868dbcf
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
app/src/content/assets/data/comparison/id_3_rank_3_sim_0.676.png ADDED

Git LFS Details

  • SHA256: 7ffd4f00b092add67877e19d1ba5d62e5f6748a2eaac6c8d5251c849b9d3e438
  • Pointer size: 131 Bytes
  • Size of remote file: 212 kB
app/src/content/chapters/available-blocks.mdx CHANGED
@@ -7,6 +7,7 @@ import Wide from '../../components/Wide.astro';
7
  import Note from '../../components/Note.astro';
8
  import FullWidth from '../../components/FullWidth.astro';
9
  import Accordion from '../../components/Accordion.astro';
 
10
 
11
  ## Available components
12
 
@@ -15,7 +16,7 @@ import Accordion from '../../components/Accordion.astro';
15
  <br/>
16
  <div className="button-group">
17
  <a className="button" href="#math">Math</a>
18
- <a className="button" href="#images">Images</a>
19
  <a className="button" href="#code-blocks">Code</a>
20
  <a className="button" href="#mermaid-diagrams">Mermaid</a>
21
  <a className="button" href="#citations-and-notes">Citations & notes</a>
@@ -54,54 +55,43 @@ $$
54
  $$
55
  ```
56
 
57
- ### Image
58
 
59
  **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**.
60
 
61
- Props (optional)
62
- - `data-zoomable`: adds a zoomable lightbox.
63
- - `data-downloadable`: adds a small download button to fetch the image file.
64
- - `loading="lazy"`: lazy loads the image.
65
- - `figcaption`: adds a caption and credit.
66
-
67
- **Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
68
-
69
- **Optional:** Download button: add `data-downloadable` to render a small button in the bottom-right corner that downloads the image. By default it tries to use the original filename; you can override via `data-download-name="myfile.png"` and `data-download-src="/path/to/original.png"`.
70
-
71
- **Optional:** Lazy loading: add `loading="lazy"` to opt-in.
72
-
73
- **Optional:** Figcaption and credits: add a `figcaption` element with a `span` containing the credit.
74
 
75
 
76
-
77
- <figure>
78
- <Image
79
- src={placeholder}
80
- data-zoomable
81
- data-downloadable
82
- layout="fixed"
83
- alt="Tensor parallelism in a transformer block"
84
- />
85
- <figcaption>
86
- Tensor parallelism in a transformer block
87
- <span className="image-credit">Original work on <a target="_blank" href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=tensor_parallelism_in_a_transformer_block">Ultrascale Playbook</a></span>
88
- </figcaption>
89
- </figure>
90
 
91
  <small className="muted">Example</small>
92
  ```mdx
93
- import { Image } from 'astro:assets'
94
  import myImage from './assets/images/placeholder.jpg'
95
 
96
- <Image src={myImage} alt="Responsive, optimized example image" />
97
-
98
- <figure>
99
- <Image src={myImage} layout="fixed" data-zoomable data-downloadable alt="Example with caption and credit" loading="lazy" />
100
- <figcaption>
101
- Optimized image with a descriptive caption.
102
- <span className="image-credit">Credit: Photo by <a href="https://example.com">Author</a></span>
103
- </figcaption>
104
- </figure>
 
 
105
  ```
106
 
107
 
@@ -349,53 +339,58 @@ import audioDemo from './assets/audio/audio-example.wav'
349
 
350
  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.
351
 
352
- They exist in the `app/src/content/fragments` folder.
353
 
354
  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.
355
 
356
- Here are some examples of the two **libraries** in the template
 
 
 
 
 
 
357
 
358
- Props (optional)
359
- - `title`: short title displayed above the card.
360
- - `desc`: short description displayed below the card. Supports inline HTML (e.g., links).
361
- - `frameless`: removes the card background and border for seamless embeds.
362
- - `align`: aligns the title/description text. One of `left` (default), `center`, `right`.
363
-
364
- {/* <HtmlEmbed src="d3-scatter.html" frameless title="" desc="" /> */}
365
- <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
366
  ---
367
- <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>`}/>
368
- ---
369
- <Wide>
370
- <HtmlEmbed src="d3-neural.html" title="D3 Interactive neural network (MNIST-like)" desc="Draw a digit and visualize activations and class probabilities (0–9)." align="center" />
371
- </Wide>
372
- ---
373
- <FullWidth>
374
- <HtmlEmbed src="d3-pie.html" desc="D3 Pie charts by category" align="center" />
375
- </FullWidth>
376
- ---
377
- <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
378
 
379
- <br/><br/>
380
- Here are some examples of the two **libraries** in the template
381
 
382
- <small className="muted">Example</small>
 
383
  ```mdx
384
  import HtmlEmbed from '../components/HtmlEmbed.astro'
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
 
387
  <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>`}/>
388
-
389
  <Wide>
390
  <HtmlEmbed src="d3-neural.html" title="D3 Interactive neural network (MNIST-like)" desc="Draw a digit and visualize activations and class probabilities (0–9)." align="center" />
391
  </Wide>
392
-
393
  <FullWidth>
394
  <HtmlEmbed src="d3-pie.html" desc="D3 Pie charts by category" align="center" />
395
  </FullWidth>
396
 
397
- <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
398
- ```
399
 
400
  ### Iframes
401
 
@@ -411,12 +406,7 @@ You can embed external content in your article using **iframes**. For example, *
411
 
412
  <small className="muted">Gradio embed</small>
413
  <div className="">
414
- <iframe
415
- src="https://gradio-hello-world.hf.space"
416
- width="100%"
417
- height="380"
418
- frameborder="0"
419
- ></iframe>
420
  </div>
421
 
422
  <small className="muted">Example</small>
 
7
  import Note from '../../components/Note.astro';
8
  import FullWidth from '../../components/FullWidth.astro';
9
  import Accordion from '../../components/Accordion.astro';
10
+ import ResponsiveImage from '../../components/ResponsiveImage.astro';
11
 
12
  ## Available components
13
 
 
16
  <br/>
17
  <div className="button-group">
18
  <a className="button" href="#math">Math</a>
19
+ <a className="button" href="#responsiveimage">ResponsiveImage</a>
20
  <a className="button" href="#code-blocks">Code</a>
21
  <a className="button" href="#mermaid-diagrams">Mermaid</a>
22
  <a className="button" href="#citations-and-notes">Citations & notes</a>
 
55
  $$
56
  ```
57
 
58
+ ### ResponsiveImage
59
 
60
  **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**.
61
 
62
+ | Prop | Required | Description | Option/Note |
63
+ |------------------------|----------|---------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
64
+ | `data-zoomable` | No | Adds a zoomable lightbox (Medium-like). | Only images with this attribute will open full-screen on click. |
65
+ | `data-downloadable` | No | Adds a small download button to fetch the image file. | By default uses the original filename. Can be customized with `data-download-name` and `data-download-src`. |
66
+ | `loading="lazy"` | No | Lazy loads the image. | Defers image loading until it is visible on screen. |
67
+ | `caption` | No | Adds a caption and credit. | Pass HTML as the `caption` prop, or use `<slot name="caption">`. Keep `<span className="image-credit">` for credits. |
 
 
 
 
 
 
 
68
 
69
 
70
+ <ResponsiveImage
71
+ src={placeholder}
72
+ data-zoomable
73
+ data-downloadable
74
+ layout="fixed"
75
+ alt="Tensor parallelism in a transformer block"
76
+ caption={'Tensor parallelism in a transformer block <span class="image-credit">Original work on <a target="_blank" href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=tensor_parallelism_in_a_transformer_block">Ultrascale Playbook</a></span>'}
77
+ />
 
 
 
 
 
 
78
 
79
  <small className="muted">Example</small>
80
  ```mdx
81
+ import ResponsiveImage from '../components/ResponsiveImage.astro'
82
  import myImage from './assets/images/placeholder.jpg'
83
 
84
+ <ResponsiveImage src={myImage} alt="Responsive, optimized example image" />
85
+
86
+ <ResponsiveImage
87
+ src={myImage}
88
+ layout="fixed"
89
+ data-zoomable
90
+ data-downloadable
91
+ loading="lazy"
92
+ alt="Example with caption and credit"
93
+ caption={'Optimized image with a descriptive caption. <span class="image-credit">Credit: Photo by <a href="https://example.com">Author</a></span>'}
94
+ />
95
  ```
96
 
97
 
 
339
 
340
  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.
341
 
342
+ They exist in the `app/src/content/embeds` folder.
343
 
344
  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.
345
 
346
+ | Prop | Required | Description
347
+ |-------------|----------|--------------------------------------------------------------------------------------------------
348
+ | `src` | Yes | Path to the embed file in the `embeds` folder.
349
+ | `title` | No | Short title displayed above the card.
350
+ | `desc` | No | Short description displayed below the card. Supports inline HTML (e.g., links).
351
+ | `frameless` | No | Removes the card background and border for seamless embeds.
352
+ | `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
353
 
354
+ <HtmlEmbed src="plotly-line.html" title="Plotly Line" desc="Interactive time series" />
 
 
 
 
 
 
 
355
  ---
 
 
 
 
 
 
 
 
 
 
 
356
 
357
+ <HtmlEmbed src="d3-line-example.html" title="D3 Line" desc="Interactive time series" />
 
358
 
359
+ <br/>
360
+ <small className="muted">Examples</small>
361
  ```mdx
362
  import HtmlEmbed from '../components/HtmlEmbed.astro'
363
 
364
+ <HtmlEmbed src="plotly-line.html" title="Plotly Line" desc="Interactive time series" />
365
+ <HtmlEmbed src="d3-line-example.html" title="D3 Line" desc="Interactive time series" />
366
+ ```
367
+
368
+ #### Data
369
+
370
+ If you need to link your HTML embeds to data files, there is an **`assets/data`** folder for this.
371
+ As long as your files are there, they will be served from the **`public/data`** folder.
372
+ You can fetch them with this address: **`[domain]/data/your-data.ext`**
373
+
374
+ <Note>⚠️ <b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
375
+
376
+ #### Real world examples
377
+
378
+ Here are some real world examples to inspire you.
379
+
380
+ <HtmlEmbed src="d3-comparison.html" title="Image comparison" desc="" />
381
+ ---
382
  <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
383
+ ---
384
  <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>`}/>
385
+ ---
386
  <Wide>
387
  <HtmlEmbed src="d3-neural.html" title="D3 Interactive neural network (MNIST-like)" desc="Draw a digit and visualize activations and class probabilities (0–9)." align="center" />
388
  </Wide>
389
+ ---
390
  <FullWidth>
391
  <HtmlEmbed src="d3-pie.html" desc="D3 Pie charts by category" align="center" />
392
  </FullWidth>
393
 
 
 
394
 
395
  ### Iframes
396
 
 
406
 
407
  <small className="muted">Gradio embed</small>
408
  <div className="">
409
+ <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
 
 
 
 
 
410
  </div>
411
 
412
  <small className="muted">Example</small>
app/src/content/chapters/getting-started.mdx CHANGED
@@ -5,6 +5,16 @@ import Note from '../../components/Note.astro';
5
 
6
  ### Installation
7
 
 
 
 
 
 
 
 
 
 
 
8
  <Sidenote>
9
  ```bash
10
  git lfs install
@@ -30,26 +40,10 @@ npm run dev
30
  npm run build
31
  ```
32
 
33
- <Note title="Note">
34
- Serving the `dist/` directory on any static host is enough to deliver the site.
35
- </Note>
36
- <Note title="Note">
37
- A [slug-title].pdf and thumb.jpg are also generated at build time.<br/>You can find them in the public folder.
38
- </Note>
39
-
40
 
41
  ### Deploy
42
 
43
- The recommended way is to **duplicate this Space** on **Hugging Face** rather than cloning it directly:
44
-
45
- 1. Open the template Space: **[🤗 science-blog-template](https://huggingface.co/spaces/tfrere/science-blog-template)** and click **"Duplicate this Space"**.
46
- 2. Give it a name, choose visibility, and **keep the free CPU instance**.
47
- 3. Clone your new Space repo.
48
- ```bash
49
- git clone git@hf.co:spaces/<your-username>/<your-space>
50
- cd <your-space>
51
- ```
52
- 3. Then push your changes to your new Space repo.<br/>**Every push automatically triggers a build and deploy** on Spaces.
53
  ```bash
54
  # Make edits locally, then:
55
  git add .
@@ -57,3 +51,9 @@ git commit -m "Update content"
57
  git push
58
  ```
59
 
 
 
 
 
 
 
 
5
 
6
  ### Installation
7
 
8
+ The recommended way is to **duplicate this Space** on **Hugging Face** rather than cloning it directly:
9
+
10
+ 1. Open the template Space: **[🤗 science-blog-template](https://huggingface.co/spaces/tfrere/science-blog-template)** and click **"Duplicate this Space"**.
11
+ 2. Give it a name, choose visibility, and **keep the free CPU instance**.
12
+ 3. Clone your new Space repo.
13
+ ```bash
14
+ git clone git@hf.co:spaces/<your-username>/<your-space>
15
+ cd <your-space>
16
+ ```
17
+
18
  <Sidenote>
19
  ```bash
20
  git lfs install
 
40
  npm run build
41
  ```
42
 
 
 
 
 
 
 
 
43
 
44
  ### Deploy
45
 
46
+ Push your changes to your new Space repo.<br/>**Every push automatically triggers a build and deploy** on Spaces.
 
 
 
 
 
 
 
 
 
47
  ```bash
48
  # Make edits locally, then:
49
  git add .
 
51
  git push
52
  ```
53
 
54
+ <Note title="Note">
55
+ Serving the `dist/` directory on any static host is enough to deliver the site.
56
+ </Note>
57
+ <Note title="Note">
58
+ A [slug-title].pdf and thumb.jpg are also generated at build time.<br/>You can find them in the public folder.
59
+ </Note>
app/src/content/chapters/writing-your-content.mdx CHANGED
@@ -15,7 +15,7 @@ Your **article** lives in **one place**
15
 
16
  Everything is **self-contained** under `app/src/content/`:
17
  - **MDX**: `article.mdx` and [**optional chapters**](#chapters) in `chapters/`
18
- - **Assets**: `assets/` (**images**, **audio**; tracked via **Git LFS**)
19
  - **Embeds**: `embed/` (**HTMLEmbed** for **Plotly/D3**, etc.) — see [**HtmlEmbed**](#htmlembed)
20
 
21
  The `article.mdx` file is the **main entry point** of your article.
 
15
 
16
  Everything is **self-contained** under `app/src/content/`:
17
  - **MDX**: `article.mdx` and [**optional chapters**](#chapters) in `chapters/`
18
+ - **Assets**: `assets/` (**images**, **audios**, **datas**, etc.) tracked via **Git LFS**)
19
  - **Embeds**: `embed/` (**HTMLEmbed** for **Plotly/D3**, etc.) — see [**HtmlEmbed**](#htmlembed)
20
 
21
  The `article.mdx` file is the **main entry point** of your article.
app/src/content/embeds/d3-comparison.html ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:flex-start; margin:12px 0; }
5
+ .image-comparison .controls label { font-size:12px; color: var(--muted-color); display:flex; align-items:center; gap:8px; }
6
+ .image-comparison .controls select {
7
+ font-size: 12px;
8
+ padding: 8px 28px 8px 10px;
9
+ border: 1px solid var(--border-color);
10
+ border-radius: 8px;
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
+ }
20
+ [data-theme="dark"] .image-comparison .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
+ .image-comparison .controls select:hover { border-color: var(--primary-color); }
24
+ .image-comparison .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
25
+
26
+ .image-comparison .grid { display:grid; grid-template-columns: repeat(4, 1fr); gap: 12px; width:100%; align-items: start; }
27
+ @media (max-width: 980px) { .image-comparison .grid { grid-template-columns: repeat(2, 1fr); } }
28
+
29
+ .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; }
30
+ .image-comparison .card .media { position: relative; width:100%; height: 200px; background: var(--surface-2, var(--surface-bg)); display:block; }
31
+ .image-comparison .card .media img { width:100%; height:100%; object-fit: contain; display:block; }
32
+ .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); }
33
+ .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; }
34
+ .image-comparison .meta .label { color: var(--muted-color); }
35
+ .image-comparison .meta .value { font-weight:600; }
36
+ </style>
37
+ <script>
38
+ (() => {
39
+ const bootstrap = () => {
40
+ const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
41
+ const container = (mount && mount.querySelector && mount.querySelector('.image-comparison')) || document.querySelector('.image-comparison');
42
+ if (!container) return;
43
+ if (container.dataset && container.dataset.mounted === 'true') return; if (container.dataset) container.dataset.mounted = 'true';
44
+
45
+ // Known filenames in /public/data/comparison
46
+ const FILES = {
47
+ '1': { 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' },
48
+ '2': { 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' },
49
+ '3': { 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' },
50
+ };
51
+
52
+ // Images served from [domain]/public/data/comparison/*.png → path is /data/comparison/
53
+ const CANDIDATE_BASES = [ '/data/comparison/' ];
54
+
55
+ const resolveBase = (candidates, filename) => new Promise((resolve) => {
56
+ let idx = 0; const tryNext = () => {
57
+ if (idx >= candidates.length) return resolve(candidates[0]);
58
+ const img = new Image();
59
+ img.onload = () => resolve(candidates[idx]);
60
+ img.onerror = () => { idx += 1; tryNext(); };
61
+ img.src = candidates[idx] + filename;
62
+ }; tryNext();
63
+ });
64
+
65
+ // Controls
66
+ const controls = document.createElement('div'); controls.className = 'controls';
67
+ const label = document.createElement('label'); label.textContent = 'Example';
68
+ const select = document.createElement('select');
69
+ const EXAMPLE_LABELS = { '1': 'photo', '2': 'chart', '3': 'drawing' };
70
+ ['1','2','3'].forEach((id)=>{ const o=document.createElement('option'); o.value=id; o.textContent=EXAMPLE_LABELS[id]; select.appendChild(o); });
71
+ label.appendChild(select); controls.appendChild(label); container.appendChild(controls);
72
+
73
+ // Grid
74
+ const grid = document.createElement('div'); grid.className = 'grid'; container.appendChild(grid);
75
+
76
+ let basePath = CANDIDATE_BASES[0];
77
+
78
+ const parseInfo = (filename) => {
79
+ const rankMatch = filename.match(/rank_(\d+)/i); const rank = rankMatch ? rankMatch[1] : '';
80
+ const simMatch = filename.match(/sim_([0-9.]+)/i); const sim = simMatch ? simMatch[1] : '';
81
+ return { rank, sim };
82
+ };
83
+
84
+ const formatSim = (val) => {
85
+ if (val == null || val === '') return '—';
86
+ return String(val).replace(/\.$/, '');
87
+ };
88
+
89
+ const render = (id) => {
90
+ const files = FILES[id]; if (!files) return;
91
+ const ordered = [files[1], files[1], files[2], files[3]]; // rank_1 twice then rank_2 and rank_3
92
+ grid.innerHTML = '';
93
+ ordered.forEach((fname, idx) => {
94
+ const { sim } = parseInfo(fname);
95
+ const isFirst = idx === 0;
96
+ const isDuplicateOfFirst = idx === 1;
97
+ const card = document.createElement('div'); card.className = 'card';
98
+ const media = document.createElement('div'); media.className = 'media';
99
+ const img = document.createElement('img'); img.alt = `example ${id} image ${idx+1}${isDuplicateOfFirst ? ' identical' : ''}`; img.loading = 'lazy'; img.src = basePath + fname; media.appendChild(img);
100
+ const meta = document.createElement('div'); meta.className = 'meta';
101
+ const left = document.createElement('span'); left.className = 'label'; left.textContent = isFirst ? 'Query' : 'Similarity';
102
+ meta.appendChild(left);
103
+ if (!isFirst) {
104
+ const right = document.createElement('span'); right.className = 'value'; right.textContent = isDuplicateOfFirst ? '1.000 identical' : formatSim(sim);
105
+ meta.appendChild(right);
106
+ }
107
+ card.appendChild(media); card.appendChild(meta); grid.appendChild(card);
108
+ });
109
+ };
110
+
111
+ (async () => {
112
+ // Resolve a working base then initial render
113
+ basePath = await resolveBase(CANDIDATE_BASES, FILES['1'][1]);
114
+ render('1');
115
+ })();
116
+
117
+ select.addEventListener('change', () => render(select.value));
118
+ };
119
+
120
+ if (document.readyState === 'loading') {
121
+ document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
122
+ } else { bootstrap(); }
123
+ })();
124
+ </script>
125
+
126
+
app/src/content/embeds/d3-line-example.html ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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);
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 .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 mount = document.currentScript ? document.currentScript.previousElementSibling : null;
93
+ const container = (mount && mount.querySelector && mount.querySelector('.d3-line')) || document.querySelector('.d3-line');
94
+ if (!container) return;
95
+ if (container.dataset) {
96
+ if (container.dataset.mounted === 'true') return;
97
+ container.dataset.mounted = 'true';
98
+ }
99
+
100
+ // Dataset params matching the Plotly version
101
+ const datasets = [
102
+ { name: 'CIFAR-10', base: { ymin:0.10, ymax:0.90, k:10.0, x0:0.55 }, aug: { ymin:0.15, ymax:0.96, k:12.0, x0:0.40 }, target: 0.97 },
103
+ { name: 'CIFAR-100', base: { ymin:0.05, ymax:0.70, k: 9.5, x0:0.60 }, aug: { ymin:0.08, ymax:0.80, k:11.0, x0:0.45 }, target: 0.85 },
104
+ { name: 'ImageNet-1K', base: { ymin:0.02, ymax:0.68, k: 8.5, x0:0.65 }, aug: { ymin:0.04, ymax:0.75, k: 9.5, x0:0.50 }, target: 0.82 },
105
+ ];
106
+
107
+ // Controls UI
108
+ const controls = document.createElement('div');
109
+ controls.className = 'd3-line__controls';
110
+ Object.assign(controls.style, {
111
+ marginTop: '12px',
112
+ display: 'flex',
113
+ gap: '16px',
114
+ alignItems: 'center'
115
+ });
116
+
117
+ const labelDs = document.createElement('label');
118
+ Object.assign(labelDs.style, {
119
+ fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px'
120
+ });
121
+ labelDs.textContent = 'Dataset';
122
+ const selectDs = document.createElement('select');
123
+ Object.assign(selectDs.style, { fontSize: '12px' });
124
+ datasets.forEach((d, i) => {
125
+ const o = document.createElement('option');
126
+ o.value = String(i);
127
+ o.textContent = d.name;
128
+ selectDs.appendChild(o);
129
+ });
130
+ labelDs.appendChild(selectDs);
131
+
132
+ const labelAlpha = document.createElement('label');
133
+ Object.assign(labelAlpha.style, {
134
+ fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '10px', flex: '1', padding: '6px 10px'
135
+ });
136
+ labelAlpha.appendChild(document.createTextNode('Augmentation α'));
137
+ const slider = document.createElement('input');
138
+ slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.01'; slider.value = '0.70';
139
+ Object.assign(slider.style, { flex: '1' });
140
+ const alphaVal = document.createElement('span'); alphaVal.className = 'alpha-value'; alphaVal.textContent = slider.value;
141
+ labelAlpha.appendChild(slider);
142
+ labelAlpha.appendChild(alphaVal);
143
+
144
+ controls.appendChild(labelDs);
145
+ controls.appendChild(labelAlpha);
146
+
147
+ // Create SVG
148
+ const svg = d3.select(container).append('svg')
149
+ .attr('width', '100%')
150
+ .style('display', 'block');
151
+
152
+ // Groups
153
+ const gRoot = svg.append('g');
154
+ const gGrid = gRoot.append('g').attr('class', 'grid');
155
+ const gAxes = gRoot.append('g').attr('class', 'axes');
156
+ const gLines = gRoot.append('g').attr('class', 'lines');
157
+ const gHover = gRoot.append('g').attr('class', 'hover');
158
+ const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
159
+
160
+ // Tooltip
161
+ container.style.position = container.style.position || 'relative';
162
+ let tip = container.querySelector('.d3-tooltip');
163
+ let tipInner;
164
+ if (!tip) {
165
+ tip = document.createElement('div');
166
+ tip.className = 'd3-tooltip';
167
+ Object.assign(tip.style, {
168
+ position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
169
+ padding: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
170
+ background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0',
171
+ transition: 'opacity .12s ease'
172
+ });
173
+ tipInner = document.createElement('div');
174
+ tipInner.className = 'd3-tooltip__inner';
175
+ tipInner.style.textAlign = 'left';
176
+ tip.appendChild(tipInner);
177
+ container.appendChild(tip);
178
+ } else {
179
+ tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
180
+ }
181
+
182
+ // Colors
183
+ const colorBase = '#64748b'; // slate-500
184
+ const colorImproved = 'var(--primary-color)';
185
+ const colorTarget = '#4b5563'; // gray-600
186
+ const legendBgLight = 'rgba(255,255,255,0.85)';
187
+ const legendBgDark = 'rgba(17,17,23,0.85)';
188
+
189
+ // Data and helpers
190
+ const N = 240;
191
+ const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
192
+ const logistic = (x, { ymin, ymax, k, x0 }) => ymin + (ymax - ymin) / (1 + Math.exp(-k * (x - x0)));
193
+ const blend = (l, e, a) => (1 - a) * l + a * e;
194
+
195
+ let datasetIndex = 0;
196
+ let alpha = parseFloat(slider.value) || 0.7;
197
+
198
+ let yBase = [];
199
+ let yAug = [];
200
+ let yImp = [];
201
+ let yTgt = [];
202
+
203
+ function computeCurves() {
204
+ const d = datasets[datasetIndex];
205
+ yBase = xs.map((x) => logistic(x, d.base));
206
+ yAug = xs.map((x) => logistic(x, d.aug));
207
+ yTgt = xs.map(() => d.target);
208
+ yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
209
+ }
210
+
211
+ // Scales and layout
212
+ let width = 800, height = 360;
213
+ let margin = { top: 16, right: 28, bottom: 56, left: 64 };
214
+ let xScale = d3.scaleLinear();
215
+ let yScale = d3.scaleLinear();
216
+
217
+ // Paths
218
+ const lineGen = d3.line()
219
+ .curve(d3.curveCatmullRom.alpha(0.6))
220
+ .x((d, i) => xScale(xs[i]))
221
+ .y((d) => yScale(d));
222
+
223
+ const pathBase = gLines.append('path').attr('fill', 'none').attr('stroke', colorBase).attr('stroke-width', 2);
224
+ const pathImp = gLines.append('path').attr('class', 'improved').attr('fill', 'none').style('stroke', 'var(--primary-color)').attr('stroke-width', 2);
225
+ const pathTgt = gLines.append('path').attr('fill', 'none').attr('stroke', colorTarget).attr('stroke-width', 2).attr('stroke-dasharray', '6,6');
226
+
227
+ // Hover elements
228
+ const hoverLine = gHover.append('line').attr('stroke-width', 1);
229
+ const hoverDotB = gHover.append('circle').attr('r', 3.5).attr('fill', colorBase).attr('stroke', '#fff').attr('stroke-width', 1);
230
+ const hoverDotI = gHover.append('circle').attr('class', 'improved').attr('r', 3.5).style('fill', 'var(--primary-color)').attr('stroke', '#fff').attr('stroke-width', 1);
231
+ const hoverDotT = gHover.append('circle').attr('r', 3.5).attr('fill', colorTarget).attr('stroke', '#fff').attr('stroke-width', 1);
232
+
233
+ const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
234
+
235
+ function updateScales() {
236
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
237
+ const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
238
+ const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
239
+ const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
240
+
241
+ width = container.clientWidth || 800;
242
+ height = Math.max(260, Math.round(width / 3));
243
+ svg.attr('width', width).attr('height', height);
244
+
245
+ const innerWidth = width - margin.left - margin.right;
246
+ const innerHeight = height - margin.top - margin.bottom;
247
+ gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
248
+
249
+ xScale.domain([0, 1]).range([0, innerWidth]);
250
+ yScale.domain([0, 1]).range([innerHeight, 0]);
251
+
252
+ // Grid (horizontal)
253
+ gGrid.selectAll('*').remove();
254
+ const yTicks = yScale.ticks(6);
255
+ gGrid.selectAll('line')
256
+ .data(yTicks)
257
+ .join('line')
258
+ .attr('x1', 0)
259
+ .attr('x2', innerWidth)
260
+ .attr('y1', (d) => yScale(d))
261
+ .attr('y2', (d) => yScale(d))
262
+ .attr('stroke', gridColor)
263
+ .attr('stroke-width', 1)
264
+ .attr('shape-rendering', 'crispEdges');
265
+
266
+ // Axes
267
+ gAxes.selectAll('*').remove();
268
+ const xAxis = d3.axisBottom(xScale).ticks(8).tickSizeOuter(0);
269
+ const yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0).tickFormat(d3.format('.2f'));
270
+ gAxes.append('g')
271
+ .attr('transform', `translate(0,${innerHeight})`)
272
+ .call(xAxis)
273
+ .call((g) => {
274
+ g.selectAll('path, line').attr('stroke', axisColor);
275
+ g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
276
+ });
277
+ gAxes.append('g')
278
+ .call(yAxis)
279
+ .call((g) => {
280
+ g.selectAll('path, line').attr('stroke', axisColor);
281
+ g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
282
+ });
283
+
284
+ // Axis labels (X and Y)
285
+ gAxes.append('text')
286
+ .attr('class', 'axis-label axis-label--x')
287
+ .attr('x', innerWidth / 2)
288
+ .attr('y', innerHeight + 44)
289
+ .attr('text-anchor', 'middle')
290
+ .style('font-size', '12px')
291
+ .style('fill', tickColor)
292
+ .text('Epoch');
293
+ gAxes.append('text')
294
+ .attr('class', 'axis-label axis-label--y')
295
+ .attr('text-anchor', 'middle')
296
+ .attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`)
297
+ .style('font-size', '12px')
298
+ .style('fill', tickColor)
299
+ .text('Accuracy');
300
+
301
+ overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
302
+ hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
303
+
304
+ // Legend inside plot (bottom-right), no background/border/shadow
305
+ const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
306
+ const legendHeight = 64;
307
+ gLegend
308
+ .attr('x', innerWidth - legendWidth + 42)
309
+ .attr('y', innerHeight - legendHeight - 12)
310
+ .attr('width', legendWidth)
311
+ .attr('height', legendHeight);
312
+ const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
313
+ Object.assign(legendRoot.node().style, {
314
+ background: 'transparent',
315
+ border: 'none',
316
+ borderRadius: '0',
317
+ padding: '0',
318
+ fontSize: '12px',
319
+ lineHeight: '1.35',
320
+ color: 'var(--text-color)'
321
+ });
322
+ legendRoot.html(`
323
+ <div style="display:flex;flex-direction:column;gap:6px;">
324
+ <div style="display:flex;align-items:center;gap:8px;">
325
+ <span style="width:18px;height:3px;background:${colorBase};border-radius:2px;display:inline-block"></span>
326
+ <span>Baseline</span>
327
+ </div>
328
+ <div style="display:flex;align-items:center;gap:8px;">
329
+ <span style="width:18px;height:3px;background:${colorImproved};border-radius:2px;display:inline-block"></span>
330
+ <span>Improved</span>
331
+ </div>
332
+ <div style="display:flex;align-items:center;gap:8px;">
333
+ <span style="width:18px;height:0;border-top:2px dashed ${colorTarget};display:inline-block"></span>
334
+ <span>Target</span>
335
+ </div>
336
+ </div>
337
+ `);
338
+ }
339
+
340
+ function updatePaths() {
341
+ pathBase.transition().duration(200).attr('d', lineGen(yBase));
342
+ pathImp.transition().duration(200).attr('d', lineGen(yImp));
343
+ pathTgt.transition().duration(200).attr('d', lineGen(yTgt));
344
+ }
345
+
346
+ function updateAlpha(a) {
347
+ alpha = a;
348
+ alphaVal.textContent = a.toFixed(2);
349
+ yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
350
+ pathImp.transition().duration(80).attr('d', lineGen(yImp));
351
+ }
352
+
353
+ function applyDataset() {
354
+ computeCurves();
355
+ updatePaths();
356
+ }
357
+
358
+ // Hover interactions
359
+ function onMove(event) {
360
+ const [mx, my] = d3.pointer(event, overlay.node());
361
+ const xi = Math.max(0, Math.min(N - 1, Math.round(xScale.invert(mx) * (N - 1))));
362
+ const xpx = xScale(xs[xi]);
363
+ const yb = yBase[xi], yi = yImp[xi], yt = yTgt[xi];
364
+ hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
365
+ hoverDotB.attr('cx', xpx).attr('cy', yScale(yb)).style('display', null);
366
+ hoverDotI.attr('cx', xpx).attr('cy', yScale(yi)).style('display', null);
367
+ hoverDotT.attr('cx', xpx).attr('cy', yScale(yt)).style('display', null);
368
+
369
+ // Tooltip content
370
+ const ds = datasets[datasetIndex].name;
371
+ tipInner.innerHTML = `<div><strong>${ds}</strong></div>` +
372
+ `<div><strong>x</strong> ${xs[xi].toFixed(2)}</div>` +
373
+ `<div><span style="display:inline-block;width:10px;height:10px;background:${colorBase};border-radius:50%;margin-right:6px;"></span><strong>Baseline</strong> ${yb.toFixed(3)}</div>` +
374
+ `<div><span style="display:inline-block;width:10px;height:10px;background:${colorImproved};border-radius:50%;margin-right:6px;"></span><strong>Improved</strong> ${yi.toFixed(3)}</div>` +
375
+ `<div><span style="display:inline-block;width:10px;height:10px;background:${colorTarget};border-radius:50%;margin-right:6px;"></span><strong>Target</strong> ${yt.toFixed(3)}</div>`;
376
+ const offsetX = 12, offsetY = 12;
377
+ tip.style.opacity = '1';
378
+ tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
379
+ }
380
+
381
+ function onLeave() {
382
+ tip.style.opacity = '0';
383
+ tip.style.transform = 'translate(-9999px, -9999px)';
384
+ hoverLine.style('display', 'none');
385
+ hoverDotB.style('display', 'none');
386
+ hoverDotI.style('display', 'none');
387
+ hoverDotT.style('display', 'none');
388
+ }
389
+
390
+ overlay.on('mousemove', onMove).on('mouseleave', onLeave);
391
+
392
+ // Init + controls wiring
393
+ computeCurves();
394
+ updateScales();
395
+ updatePaths();
396
+
397
+ // Attach controls after SVG for consistency with Plotly fragment
398
+ container.appendChild(controls);
399
+
400
+ selectDs.addEventListener('change', (e) => {
401
+ datasetIndex = parseInt(e.target.value) || 0;
402
+ applyDataset();
403
+ });
404
+ slider.addEventListener('input', (e) => {
405
+ const a = parseFloat(e.target.value) || 0;
406
+ updateAlpha(a);
407
+ });
408
+
409
+ // Resize handling
410
+ const render = () => {
411
+ updateScales();
412
+ updatePaths();
413
+ };
414
+ if (window.ResizeObserver) {
415
+ const ro = new ResizeObserver(() => render());
416
+ ro.observe(container);
417
+ } else {
418
+ window.addEventListener('resize', render);
419
+ }
420
+ render();
421
+ };
422
+
423
+ if (document.readyState === 'loading') {
424
+ document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
425
+ } else { ensureD3(bootstrap); }
426
+ })();
427
+ </script>
428
+
app/src/content/embeds/d3-line.html CHANGED
@@ -89,8 +89,25 @@
89
  };
90
 
91
  const bootstrap = () => {
92
- const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
93
- const container = (mount && mount.querySelector && mount.querySelector('.d3-line')) || document.querySelector('.d3-line');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  if (!container) return;
95
  if (container.dataset) {
96
  if (container.dataset.mounted === 'true') return;
 
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;
app/src/content/embeds/{line.html → plotly-line.html} RENAMED
File without changes
app/src/env.d.ts CHANGED
@@ -1 +1,6 @@
1
- /// <reference path="../.astro/types.d.ts" />
 
 
 
 
 
 
1
+ /// <reference path="../.astro/types.d.ts" />
2
+ /// <reference types="astro/client" />
3
+
4
+ declare module 'astro:assets' {
5
+ export const Image: any;
6
+ }
app/src/styles/_base.css CHANGED
@@ -48,10 +48,14 @@ html { font-size: 14px; line-height: 1.6; }
48
  /* Do not underline heading links inside the article (not the TOC) */
49
  .content-grid main h2 a,
50
  .content-grid main h3 a,
51
- .content-grid main h4 a { color: inherit; border-bottom: none; text-decoration: none; }
 
 
52
  .content-grid main h2 a:hover,
53
  .content-grid main h3 a:hover,
54
- .content-grid main h4 a:hover { color: inherit; border-bottom: none; text-decoration: none; }
 
 
55
 
56
  .content-grid main ul,
57
  .content-grid main ol { padding-left: 24px; margin: 0 0 var(--spacing-3); }
 
48
  /* Do not underline heading links inside the article (not the TOC) */
49
  .content-grid main h2 a,
50
  .content-grid main h3 a,
51
+ .content-grid main h4 a,
52
+ .content-grid main h5 a,
53
+ .content-grid main h6 a { color: inherit; border-bottom: none; text-decoration: none; }
54
  .content-grid main h2 a:hover,
55
  .content-grid main h3 a:hover,
56
+ .content-grid main h4 a:hover,
57
+ .content-grid main h5 a:hover,
58
+ .content-grid main h6 a:hover { color: inherit; border-bottom: none; text-decoration: none; }
59
 
60
  .content-grid main ul,
61
  .content-grid main ol { padding-left: 24px; margin: 0 0 var(--spacing-3); }