thibaud frere
commited on
Commit
·
941cf22
1
Parent(s):
6105bcd
update
Browse files- app/src/components/ResponsiveImage.astro +38 -0
- app/src/content/article.mdx +1 -1
- app/src/content/assets/data/comparison/id_1_rank_1_sim_1.000.png +3 -0
- app/src/content/assets/data/comparison/id_1_rank_2_sim_0.165.png +3 -0
- app/src/content/assets/data/comparison/id_1_rank_3_sim_0.143.png +3 -0
- app/src/content/assets/data/comparison/id_2_rank_1_sim_1.000.png +3 -0
- app/src/content/assets/data/comparison/id_2_rank_2_sim_0.978.png +3 -0
- app/src/content/assets/data/comparison/id_2_rank_3_sim_0.975.png +3 -0
- app/src/content/assets/data/comparison/id_3_rank_1_sim_0.936.png +3 -0
- app/src/content/assets/data/comparison/id_3_rank_2_sim_0.686.png +3 -0
- app/src/content/assets/data/comparison/id_3_rank_3_sim_0.676.png +3 -0
- app/src/content/chapters/available-blocks.mdx +63 -73
- app/src/content/chapters/getting-started.mdx +17 -17
- app/src/content/chapters/writing-your-content.mdx +1 -1
- app/src/content/embeds/d3-comparison.html +126 -0
- app/src/content/embeds/d3-line-example.html +428 -0
- app/src/content/embeds/d3-line.html +19 -2
- app/src/content/embeds/{line.html → plotly-line.html} +0 -0
- app/src/env.d.ts +6 -1
- app/src/styles/_base.css +6 -2
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
|
| 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
|
app/src/content/assets/data/comparison/id_1_rank_2_sim_0.165.png
ADDED
|
Git LFS Details
|
app/src/content/assets/data/comparison/id_1_rank_3_sim_0.143.png
ADDED
|
Git LFS Details
|
app/src/content/assets/data/comparison/id_2_rank_1_sim_1.000.png
ADDED
|
Git LFS Details
|
app/src/content/assets/data/comparison/id_2_rank_2_sim_0.978.png
ADDED
|
Git LFS Details
|
app/src/content/assets/data/comparison/id_2_rank_3_sim_0.975.png
ADDED
|
Git LFS Details
|
app/src/content/assets/data/comparison/id_3_rank_1_sim_0.936.png
ADDED
|
Git LFS Details
|
app/src/content/assets/data/comparison/id_3_rank_2_sim_0.686.png
ADDED
|
Git LFS Details
|
app/src/content/assets/data/comparison/id_3_rank_3_sim_0.676.png
ADDED
|
Git LFS Details
|
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="#
|
| 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 |
-
###
|
| 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 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 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 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 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
|
| 94 |
import myImage from './assets/images/placeholder.jpg'
|
| 95 |
|
| 96 |
-
<
|
| 97 |
-
|
| 98 |
-
<
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
| 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/
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
-
|
| 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 |
-
<
|
| 380 |
-
Here are some examples of the two **libraries** in the template
|
| 381 |
|
| 382 |
-
<
|
|
|
|
| 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 |
-
|
| 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**, **
|
| 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
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 52 |
.content-grid main h2 a:hover,
|
| 53 |
.content-grid main h3 a:hover,
|
| 54 |
-
.content-grid main h4 a:hover
|
|
|
|
|
|
|
| 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); }
|