Spaces:
Running
Running
thibaud frere
commited on
Commit
·
8d0d788
1
Parent(s):
651ae3a
update charts and vibe coding section
Browse files- app/src/components/HtmlEmbed.astro +14 -9
- app/src/content/article.mdx +3 -0
- app/src/content/chapters/components.mdx +40 -72
- app/src/content/chapters/vibe-coding-charts.mdx +70 -0
- app/src/content/embeds/banner.html +52 -20
- app/src/content/embeds/bar.html +0 -1
- app/src/content/embeds/d3-area-stacked.html +0 -220
- app/src/content/embeds/d3-bar.html +149 -43
- app/src/content/embeds/d3-benchmark.html +444 -0
- app/src/content/embeds/d3-boxplot.html +0 -251
- app/src/content/embeds/d3-comparison.html +0 -149
- app/src/content/embeds/d3-confusion-matrix.html +516 -0
- app/src/content/embeds/d3-line-example.html +0 -456
- app/src/content/embeds/{filters-quad.html → d3-line-quad.html} +246 -147
- app/src/content/embeds/d3-line.html +387 -431
- app/src/content/embeds/d3-matrix.html +515 -0
- app/src/content/embeds/{d3-neural.html → d3-neural-network.html} +255 -79
- app/src/content/embeds/d3-pie.html +83 -11
- app/src/content/embeds/d3-scatter.html +51 -13
- app/src/content/embeds/heatmap.html +0 -159
- app/src/content/embeds/vibe-code-d3-embeds-directives.md +257 -9
- app/src/scripts/color-palettes.js +51 -1
- app/src/styles/_variables.css +12 -0
- app/src/styles/components/_card.css +9 -0
- app/src/styles/global.css +1 -0
app/src/components/HtmlEmbed.astro
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right'; id?: string }
|
| 3 |
-
const { src, title, desc, frameless = false, align = 'left', id } = Astro.props as Props;
|
| 4 |
|
| 5 |
// Load all .html embeds under src/content/embeds/** as strings (dev & build)
|
| 6 |
const embeds = (import.meta as any).glob('../content/embeds/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
|
|
@@ -18,12 +18,14 @@ function resolveFragment(requested: string): string | null {
|
|
| 18 |
|
| 19 |
const html = resolveFragment(src);
|
| 20 |
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
|
|
|
|
|
|
| 21 |
---
|
| 22 |
{ html ? (
|
| 23 |
<figure class="html-embed" id={id}>
|
| 24 |
{title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
|
| 25 |
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
|
| 26 |
-
<div id={mountId} set:html={html} />
|
| 27 |
</div>
|
| 28 |
{desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
|
| 29 |
</figure>
|
|
@@ -80,7 +82,8 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
|
| 80 |
font-size: 0.95rem;
|
| 81 |
color: var(--text-color);
|
| 82 |
margin: 0;
|
| 83 |
-
padding:
|
|
|
|
| 84 |
position: relative;
|
| 85 |
display: block;
|
| 86 |
width: 100%;
|
|
@@ -90,7 +93,8 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
|
| 90 |
border: 1px solid var(--border-color);
|
| 91 |
border-radius: 10px;
|
| 92 |
padding: 8px;
|
| 93 |
-
z-index: var(--z-elevated);
|
|
|
|
| 94 |
}
|
| 95 |
.html-embed__card.is-frameless {
|
| 96 |
background: transparent;
|
|
@@ -102,16 +106,17 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
|
| 102 |
font-size: 0.9rem;
|
| 103 |
color: var(--muted-color);
|
| 104 |
margin: 0;
|
| 105 |
-
padding:
|
|
|
|
| 106 |
position: relative;
|
| 107 |
z-index: var(--z-elevated);
|
| 108 |
display: block;
|
| 109 |
width: 100%;
|
| 110 |
}
|
| 111 |
/* Plotly – fragments & controls */
|
| 112 |
-
.html-embed__card svg text { fill: var(--text-color)
|
| 113 |
-
.html-embed__card label { color: var(--text-color)
|
| 114 |
-
.plotly-graph-div { width: 100
|
| 115 |
@media (max-width: 768px) { .plotly-graph-div { min-height: 260px; } }
|
| 116 |
[id^="plot-"] { display: flex; flex-direction: column; align-items: center; gap: 15px; }
|
| 117 |
.plotly_caption { font-style: italic; margin-top: 10px; }
|
|
|
|
| 1 |
---
|
| 2 |
+
interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right'; id?: string, data?: string | string[], config?: any }
|
| 3 |
+
const { src, title, desc, frameless = false, align = 'left', id, data, config } = Astro.props as Props;
|
| 4 |
|
| 5 |
// Load all .html embeds under src/content/embeds/** as strings (dev & build)
|
| 6 |
const embeds = (import.meta as any).glob('../content/embeds/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
|
|
|
|
| 18 |
|
| 19 |
const html = resolveFragment(src);
|
| 20 |
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
| 21 |
+
const dataAttr = Array.isArray(data) ? JSON.stringify(data) : (typeof data === 'string' ? data : undefined);
|
| 22 |
+
const configAttr = typeof config === 'string' ? config : (config != null ? JSON.stringify(config) : undefined);
|
| 23 |
---
|
| 24 |
{ html ? (
|
| 25 |
<figure class="html-embed" id={id}>
|
| 26 |
{title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
|
| 27 |
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
|
| 28 |
+
<div id={mountId} data-datafiles={dataAttr} data-config={configAttr} set:html={html} />
|
| 29 |
</div>
|
| 30 |
{desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
|
| 31 |
</figure>
|
|
|
|
| 82 |
font-size: 0.95rem;
|
| 83 |
color: var(--text-color);
|
| 84 |
margin: 0;
|
| 85 |
+
padding: 0;
|
| 86 |
+
padding-bottom: var(--spacing-1);
|
| 87 |
position: relative;
|
| 88 |
display: block;
|
| 89 |
width: 100%;
|
|
|
|
| 93 |
border: 1px solid var(--border-color);
|
| 94 |
border-radius: 10px;
|
| 95 |
padding: 8px;
|
| 96 |
+
z-index: calc(var(--z-elevated) + 1);
|
| 97 |
+
position: relative;
|
| 98 |
}
|
| 99 |
.html-embed__card.is-frameless {
|
| 100 |
background: transparent;
|
|
|
|
| 106 |
font-size: 0.9rem;
|
| 107 |
color: var(--muted-color);
|
| 108 |
margin: 0;
|
| 109 |
+
padding: 0;
|
| 110 |
+
padding-top: var(--spacing-1);
|
| 111 |
position: relative;
|
| 112 |
z-index: var(--z-elevated);
|
| 113 |
display: block;
|
| 114 |
width: 100%;
|
| 115 |
}
|
| 116 |
/* Plotly – fragments & controls */
|
| 117 |
+
.html-embed__card svg text { fill: var(--text-color); }
|
| 118 |
+
.html-embed__card label { color: var(--text-color); }
|
| 119 |
+
.plotly-graph-div { width: 100%; min-height: 320px; }
|
| 120 |
@media (max-width: 768px) { .plotly-graph-div { min-height: 260px; } }
|
| 121 |
[id^="plot-"] { display: flex; flex-direction: column; align-items: center; gap: 15px; }
|
| 122 |
.plotly_caption { font-style: italic; margin-top: 10px; }
|
app/src/content/article.mdx
CHANGED
|
@@ -29,6 +29,7 @@ import GettingStarted from "./chapters/getting-started.mdx";
|
|
| 29 |
import Markdown from "./chapters/markdown.mdx";
|
| 30 |
import Components from "./chapters/components.mdx";
|
| 31 |
import Greetings from "./chapters/greetings.mdx";
|
|
|
|
| 32 |
|
| 33 |
<Introduction />
|
| 34 |
|
|
@@ -40,6 +41,8 @@ import Greetings from "./chapters/greetings.mdx";
|
|
| 40 |
|
| 41 |
<Components />
|
| 42 |
|
|
|
|
|
|
|
| 43 |
<BestPractices />
|
| 44 |
|
| 45 |
<Greetings />
|
|
|
|
| 29 |
import Markdown from "./chapters/markdown.mdx";
|
| 30 |
import Components from "./chapters/components.mdx";
|
| 31 |
import Greetings from "./chapters/greetings.mdx";
|
| 32 |
+
import VibeCodingCharts from "./chapters/vibe-coding-charts.mdx";
|
| 33 |
|
| 34 |
<Introduction />
|
| 35 |
|
|
|
|
| 41 |
|
| 42 |
<Components />
|
| 43 |
|
| 44 |
+
<VibeCodingCharts />
|
| 45 |
+
|
| 46 |
<BestPractices />
|
| 47 |
|
| 48 |
<Greetings />
|
app/src/content/chapters/components.mdx
CHANGED
|
@@ -28,14 +28,6 @@ You have to import them in the **.mdx** file you want to use them in.
|
|
| 28 |
|
| 29 |
**Responsive images** automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
|
| 30 |
|
| 31 |
-
| Prop | Required | Description
|
| 32 |
-
|------------------------|----------|-------------------------------------------------------
|
| 33 |
-
| `zoomable` | No | Adds a zoomable lightbox (Medium-like).
|
| 34 |
-
| `downloadable` | No | Adds a download button to fetch the image file.
|
| 35 |
-
| `loading="lazy"` | No | Lazy loads the image.
|
| 36 |
-
| `caption` | No | Adds a caption and credit.
|
| 37 |
-
| `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
|
| 38 |
-
|
| 39 |
|
| 40 |
<ResponsiveImage
|
| 41 |
src={placeholder}
|
|
@@ -44,9 +36,18 @@ You have to import them in the **.mdx** file you want to use them in.
|
|
| 44 |
id="placeholder-image"
|
| 45 |
layout="fixed"
|
| 46 |
alt="A placeholder image alt text"
|
| 47 |
-
caption={'A placeholder image description
|
| 48 |
/>
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
<Accordion title="Code example">
|
| 51 |
```mdx
|
| 52 |
import ResponsiveImage from '../components/ResponsiveImage.astro'
|
|
@@ -229,6 +230,24 @@ import Note from '../../components/Note.astro'
|
|
| 229 |
</Accordion>
|
| 230 |
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
### HtmlEmbed
|
| 233 |
|
| 234 |
The main purpose of the ```HtmlEmbed``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
|
|
@@ -238,7 +257,7 @@ They exist in the `app/src/content/embeds` folder.
|
|
| 238 |
For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
|
| 239 |
|
| 240 |
|
| 241 |
-
<HtmlEmbed src="d3-line
|
| 242 |
|
| 243 |
| Prop | Required | Description
|
| 244 |
|-------------|----------|----------------------------------------------------------------------------------
|
|
@@ -248,17 +267,25 @@ For researchers who want to stay in **Python** while targeting **D3**, the [d3bl
|
|
| 248 |
| `frameless` | No | Removes the card background and border for seamless embeds.
|
| 249 |
| `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
|
| 250 |
| `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
|
|
|
|
|
|
|
| 251 |
|
| 252 |
<Accordion title="Code example">
|
| 253 |
```mdx
|
| 254 |
import HtmlEmbed from '../components/HtmlEmbed.astro'
|
| 255 |
|
| 256 |
-
<HtmlEmbed src="
|
| 257 |
-
|
| 258 |
-
<HtmlEmbed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
```
|
| 260 |
</Accordion>
|
| 261 |
|
|
|
|
| 262 |
#### Data
|
| 263 |
|
| 264 |
If you need to link your **HTML embeds** to **data files**, there is an **`assets/data`** folder for this.
|
|
@@ -266,62 +293,3 @@ As long as your files are there, they will be served from the **`public/data`**
|
|
| 266 |
You can fetch them with this address: **`[domain]/data/your-data.ext`**
|
| 267 |
|
| 268 |
<Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
|
| 269 |
-
|
| 270 |
-
#### Vibe coding charts
|
| 271 |
-
|
| 272 |
-
If you are from the research field, it can be difficult to use **D3.js** charts instead of **Plotly**.
|
| 273 |
-
Happily, **LLM's** are here to help you! In the `app/src/content/embeds` folder, there is a `vibe-code-d3-embeds-directives.md` file that you can use to vibe code **D3.js** charts.
|
| 274 |
-
|
| 275 |
-
Inside this file, you will find every directives you need to code your own **HtmlEmbed** proof **D3.js** chart.
|
| 276 |
-
|
| 277 |
-
#### Real world examples
|
| 278 |
-
|
| 279 |
-
Here are some examples that were vibe coded to inspire you.
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
---
|
| 283 |
-
<HtmlEmbed src="d3-line.html" title="Training curves by metric" desc="Interactive time series across runs. Choose a metric; hover for values." />
|
| 284 |
-
---
|
| 285 |
-
<HtmlEmbed src="d3-bar.html" title="D3 Memory usage with recomputation" desc={`Memory usage with recomputation — <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">from the ultrascale playbook</a>`}/>
|
| 286 |
-
---
|
| 287 |
-
<HtmlEmbed src="d3-neural.html" id="neural-network-mnist-like" title="D3 Interactive neural network (MNIST-like)" desc="Visualize activations and class probabilities (0–9)." align="center" />
|
| 288 |
-
---
|
| 289 |
-
<HtmlEmbed src="d3-pie.html" title="D3 Pie charts by category" align="center" frameless />
|
| 290 |
-
---
|
| 291 |
-
<HtmlEmbed src="filters-quad.html" frameless desc={"Figure 7: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence <br/> Credit: "+'<a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>'} align="center" />
|
| 292 |
-
|
| 293 |
-
{/* ---
|
| 294 |
-
<HtmlEmbed src="d3-comparison.html" title="Image similarity: query vs top-k" desc="Compare a query image to top-k matches. Shows rank and similarity." />
|
| 295 |
-
---
|
| 296 |
-
<HtmlEmbed src="d3-area-stacked.html" title="Relative metric shares over steps (by run)" desc="Stacked area of normalized metric contributions across training steps. Data: /data/all_ratings_luis.csv" />
|
| 297 |
-
---
|
| 298 |
-
<HtmlEmbed src="d3-boxplot.html" title="Metric distribution across runs (box plots)" desc="Per-run distribution for a selected metric (median, quartiles, whiskers, outliers). Data: /data/all_ratings_luis.csv" />
|
| 299 |
-
*/}
|
| 300 |
-
---
|
| 301 |
-
<HtmlEmbed src="d3-scatter.html" title="2D projection by category" desc={`Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
|
| 302 |
-
---
|
| 303 |
-
|
| 304 |
-
### Iframes
|
| 305 |
-
|
| 306 |
-
You can embed external content in your article using **iframes**. For example, **TrackIO**, **Gradio** or even **Github code embeds** can be used this way.
|
| 307 |
-
|
| 308 |
-
<small className="muted">Github code embed</small>
|
| 309 |
-
<iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
|
| 310 |
-
|
| 311 |
-
<small className="muted">TrackIO embed</small>
|
| 312 |
-
<div className="">
|
| 313 |
-
<iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="660" frameborder="0"></iframe>
|
| 314 |
-
</div>
|
| 315 |
-
|
| 316 |
-
<small className="muted">Gradio embed</small>
|
| 317 |
-
<div className="">
|
| 318 |
-
<iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
|
| 319 |
-
</div>
|
| 320 |
-
|
| 321 |
-
<Accordion title="Code example">
|
| 322 |
-
```mdx
|
| 323 |
-
<iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
|
| 324 |
-
<iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
|
| 325 |
-
<iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
|
| 326 |
-
```
|
| 327 |
-
</Accordion>
|
|
|
|
| 28 |
|
| 29 |
**Responsive images** automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
<ResponsiveImage
|
| 33 |
src={placeholder}
|
|
|
|
| 36 |
id="placeholder-image"
|
| 37 |
layout="fixed"
|
| 38 |
alt="A placeholder image alt text"
|
| 39 |
+
caption={'A placeholder image description<br/> Credit: <a target="_blank" href="https://commons.wikimedia.org/wiki/File:RCA_Indian_Head_Test_Pattern.svg">RCA Indian Head Test Pattern</a>'}
|
| 40 |
/>
|
| 41 |
|
| 42 |
+
| Prop | Required | Description
|
| 43 |
+
|------------------------|----------|-------------------------------------------------------
|
| 44 |
+
| `zoomable` | No | Adds a zoomable lightbox (Medium-like).
|
| 45 |
+
| `downloadable` | No | Adds a download button to fetch the image file.
|
| 46 |
+
| `loading="lazy"` | No | Lazy loads the image.
|
| 47 |
+
| `caption` | No | Adds a caption and credit.
|
| 48 |
+
| `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
|
| 49 |
+
|
| 50 |
+
|
| 51 |
<Accordion title="Code example">
|
| 52 |
```mdx
|
| 53 |
import ResponsiveImage from '../components/ResponsiveImage.astro'
|
|
|
|
| 230 |
</Accordion>
|
| 231 |
|
| 232 |
|
| 233 |
+
### Iframes
|
| 234 |
+
|
| 235 |
+
You can embed external content in your article using **iframes**. For example, **TrackIO**, **Gradio** or even **Github code embeds** can be used this way.
|
| 236 |
+
|
| 237 |
+
<small className="muted">Gradio embed example</small>
|
| 238 |
+
<div className="card">
|
| 239 |
+
<iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<Accordion title="Code example">
|
| 243 |
+
```mdx
|
| 244 |
+
<iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
|
| 245 |
+
<iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
|
| 246 |
+
<iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
|
| 247 |
+
```
|
| 248 |
+
</Accordion>
|
| 249 |
+
|
| 250 |
+
|
| 251 |
### HtmlEmbed
|
| 252 |
|
| 253 |
The main purpose of the ```HtmlEmbed``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
|
|
|
|
| 257 |
For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
|
| 258 |
|
| 259 |
|
| 260 |
+
<HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Figure X: Some chart description<br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
|
| 261 |
|
| 262 |
| Prop | Required | Description
|
| 263 |
|-------------|----------|----------------------------------------------------------------------------------
|
|
|
|
| 267 |
| `frameless` | No | Removes the card background and border for seamless embeds.
|
| 268 |
| `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
|
| 269 |
| `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
|
| 270 |
+
| `data` | No | Path (string) or array of paths (string[]) to data file(s) consumed by the embed.
|
| 271 |
+
| `config` | No | Optional object for embed options (e.g., `{ defaultMetric: 'average_rank' }`).
|
| 272 |
|
| 273 |
<Accordion title="Code example">
|
| 274 |
```mdx
|
| 275 |
import HtmlEmbed from '../components/HtmlEmbed.astro'
|
| 276 |
|
| 277 |
+
<HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Some chart description <br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
|
| 278 |
+
|
| 279 |
+
<HtmlEmbed
|
| 280 |
+
src="d3-line.html"
|
| 281 |
+
title="Comparison A vs B"
|
| 282 |
+
data={[ 'formatting_filters.csv', 'relevance_filters.csv' ]}
|
| 283 |
+
config={{ defaultMetric: 'average_rank' }}
|
| 284 |
+
/>
|
| 285 |
```
|
| 286 |
</Accordion>
|
| 287 |
|
| 288 |
+
|
| 289 |
#### Data
|
| 290 |
|
| 291 |
If you need to link your **HTML embeds** to **data files**, there is an **`assets/data`** folder for this.
|
|
|
|
| 293 |
You can fetch them with this address: **`[domain]/data/your-data.ext`**
|
| 294 |
|
| 295 |
<Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/chapters/vibe-coding-charts.mdx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
| 2 |
+
|
| 3 |
+
## Vibe coding charts
|
| 4 |
+
|
| 5 |
+
### Prompt
|
| 6 |
+
|
| 7 |
+
This page explains how to use the directives to author D3 charts as self‑contained HTML fragments.
|
| 8 |
+
Using claude code works better.
|
| 9 |
+
|
| 10 |
+
The goal is to make **responsive**, **accessible**, **interactive** and **dark mode ready** charts.
|
| 11 |
+
|
| 12 |
+
1. Use this ref a a baseprompt: `app/src/content/embeds/vibe-code-d3-embeds-directives.md`.
|
| 13 |
+
2. Opt: use an already existing chart as a starting point.
|
| 14 |
+
3. Ask claude to code the chart. Here's a typical prompt:
|
| 15 |
+
|
| 16 |
+
```markdown
|
| 17 |
+
I want you to code a new d3 chart named `yourchart`.
|
| 18 |
+
I have one CSV file called `yourdata.csv` in the data folder.
|
| 19 |
+
The csv has the following columns: `x`, `y`, `z`.
|
| 20 |
+
I want you to code a d3 chart that visualizes the data.
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
4. Once the chart created, iterate with littles adjustments to make it better.
|
| 24 |
+
5. And that's it! 🎉
|
| 25 |
+
|
| 26 |
+
### Real‑world examples
|
| 27 |
+
|
| 28 |
+
They can be found in the `app/src/content/embeds` folder and you can also use them as a starting point or examples to vibe code with.
|
| 29 |
+
|
| 30 |
+
<HtmlEmbed
|
| 31 |
+
title="d3-benchmark: LLM Benchmark"
|
| 32 |
+
src="d3-benchmark.html" desc={`Figure 1: Grouped bar chart comparing model scores across benchmarks (MMLU, GSM8K, HellaSwag, TruthfulQA, ARC‑C). Each group is a benchmark; colors encode models; values are accuracy/score (higher is better).`} />
|
| 33 |
+
---
|
| 34 |
+
<HtmlEmbed
|
| 35 |
+
src="d3-line.html"
|
| 36 |
+
title="d3-line: Average Ranking of Models"
|
| 37 |
+
desc='Figure 2: Average Ranking of Models trained with internally deduplicated / merged samples. No clear benefit in merging can be seen with respect to model performance.<br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>'
|
| 38 |
+
data="internal_deduplication.csv"
|
| 39 |
+
config={{
|
| 40 |
+
defaultMetric: ['average_rank']
|
| 41 |
+
}}
|
| 42 |
+
/>
|
| 43 |
+
---
|
| 44 |
+
<HtmlEmbed
|
| 45 |
+
src="d3-neural-network.html"
|
| 46 |
+
id="neural-network-mnist-like"
|
| 47 |
+
title="d3-neural-network: MNIST-like Neural Network"
|
| 48 |
+
desc={`Figure 3: Interactive MNIST-like neural network. Draw a digit on the left canvas; activations propagate through hidden layers (node size/opacity reflect activation). The right side displays class probabilities (0–9) with the top class emphasized.`}
|
| 49 |
+
/>
|
| 50 |
+
---
|
| 51 |
+
<HtmlEmbed
|
| 52 |
+
src="d3-matrix.html"
|
| 53 |
+
title="d3-matrix: Baseline and Δ (Improved − Baseline)"
|
| 54 |
+
frameless
|
| 55 |
+
desc={`<p>
|
| 56 |
+
Figure 4: Left: baseline matrix (row-normalized, sequential palette).
|
| 57 |
+
Right: Δ (Improved − Baseline) in percentage points, using a diverging palette centered at 0 to highlight improvements vs degradations.
|
| 58 |
+
</p>`}
|
| 59 |
+
/>
|
| 60 |
+
---
|
| 61 |
+
<HtmlEmbed title="d3-line-quad: Comparison across thresholds" src="d3-line-quad.html" desc={"Figure 5: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence <br/> Credit: "+'<a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>'} />
|
| 62 |
+
---
|
| 63 |
+
<HtmlEmbed src="d3-bar.html" title="d3-bar: Memory usage with recomputation" desc={`Figure 6: Memory usage with recomputation.<br/>Credits: <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">Ultrascale playbook</a>`}/>
|
| 64 |
+
---
|
| 65 |
+
<HtmlEmbed src="d3-pie.html" title="d3-pie: Pie charts by category" align="center" frameless desc='Figure 7: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>' />
|
| 66 |
+
---
|
| 67 |
+
<HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
|
app/src/content/embeds/banner.html
CHANGED
|
@@ -91,7 +91,8 @@
|
|
| 91 |
// Create SVG
|
| 92 |
const svg = d3.select(container).append('svg')
|
| 93 |
.attr('width', '100%')
|
| 94 |
-
.style('display', 'block')
|
|
|
|
| 95 |
|
| 96 |
const render = () => {
|
| 97 |
const width = container.clientWidth || 800;
|
|
@@ -104,6 +105,7 @@
|
|
| 104 |
// Subtle stroke color depending on theme
|
| 105 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 106 |
const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
// Group for points (no blend mode for better print/PDF visibility)
|
|
@@ -122,20 +124,28 @@
|
|
| 122 |
left: '0px',
|
| 123 |
transform: 'translate(-9999px, -9999px)',
|
| 124 |
pointerEvents: 'none',
|
| 125 |
-
padding: '
|
| 126 |
-
borderRadius: '
|
| 127 |
fontSize: '12px',
|
| 128 |
lineHeight: '1.35',
|
| 129 |
border: '1px solid var(--border-color)',
|
| 130 |
background: 'var(--surface-bg)',
|
| 131 |
color: 'var(--text-color)',
|
| 132 |
-
boxShadow: '0
|
| 133 |
opacity: '0',
|
| 134 |
-
transition: 'opacity .12s ease'
|
|
|
|
|
|
|
| 135 |
});
|
| 136 |
tipInner = document.createElement('div');
|
| 137 |
tipInner.className = 'd3-tooltip__inner';
|
| 138 |
-
tipInner.style
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
tip.appendChild(tipInner);
|
| 140 |
container.appendChild(tip);
|
| 141 |
} else {
|
|
@@ -160,16 +170,21 @@
|
|
| 160 |
.attr('stroke-width', 0.4)
|
| 161 |
.on('mouseenter', function(ev, i) {
|
| 162 |
d3.select(this).raise()
|
|
|
|
|
|
|
| 163 |
.attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
|
| 164 |
-
.attr('stroke-width', 1.
|
|
|
|
|
|
|
| 165 |
const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
|
| 166 |
const type = i < lenSpiral ? 'spiral' : 'bulge';
|
| 167 |
const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
|
| 168 |
-
tipInner.innerHTML =
|
| 169 |
-
`<div><strong
|
| 170 |
-
`<div><strong>
|
| 171 |
-
`<div><strong>
|
| 172 |
-
`<div><strong>
|
|
|
|
| 173 |
tip.style.opacity = '1';
|
| 174 |
})
|
| 175 |
.on('mousemove', (ev, i) => {
|
|
@@ -180,7 +195,13 @@
|
|
| 180 |
.on('mouseleave', function() {
|
| 181 |
tip.style.opacity = '0';
|
| 182 |
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 183 |
-
d3.select(this)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
}),
|
| 185 |
(update) => update
|
| 186 |
.attr('cx', (i) => xScale(X[i]))
|
|
@@ -192,16 +213,21 @@
|
|
| 192 |
.attr('stroke-width', 0.4)
|
| 193 |
.on('mouseenter', function(ev, i) {
|
| 194 |
d3.select(this).raise()
|
|
|
|
|
|
|
| 195 |
.attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
|
| 196 |
-
.attr('stroke-width', 1.
|
|
|
|
|
|
|
| 197 |
const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
|
| 198 |
const type = i < lenSpiral ? 'spiral' : 'bulge';
|
| 199 |
const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
|
| 200 |
-
tipInner.innerHTML =
|
| 201 |
-
`<div><strong
|
| 202 |
-
`<div><strong>
|
| 203 |
-
`<div><strong>
|
| 204 |
-
`<div><strong>
|
|
|
|
| 205 |
tip.style.opacity = '1';
|
| 206 |
})
|
| 207 |
.on('mousemove', (ev, i) => {
|
|
@@ -212,7 +238,13 @@
|
|
| 212 |
.on('mouseleave', function() {
|
| 213 |
tip.style.opacity = '0';
|
| 214 |
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 215 |
-
d3.select(this)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
})
|
| 217 |
);
|
| 218 |
};
|
|
|
|
| 91 |
// Create SVG
|
| 92 |
const svg = d3.select(container).append('svg')
|
| 93 |
.attr('width', '100%')
|
| 94 |
+
.style('display', 'block')
|
| 95 |
+
.style('cursor', 'crosshair');
|
| 96 |
|
| 97 |
const render = () => {
|
| 98 |
const width = container.clientWidth || 800;
|
|
|
|
| 105 |
// Subtle stroke color depending on theme
|
| 106 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 107 |
const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
|
| 108 |
+
const glowColor = isDark ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.25)';
|
| 109 |
|
| 110 |
|
| 111 |
// Group for points (no blend mode for better print/PDF visibility)
|
|
|
|
| 124 |
left: '0px',
|
| 125 |
transform: 'translate(-9999px, -9999px)',
|
| 126 |
pointerEvents: 'none',
|
| 127 |
+
padding: '10px 12px',
|
| 128 |
+
borderRadius: '12px',
|
| 129 |
fontSize: '12px',
|
| 130 |
lineHeight: '1.35',
|
| 131 |
border: '1px solid var(--border-color)',
|
| 132 |
background: 'var(--surface-bg)',
|
| 133 |
color: 'var(--text-color)',
|
| 134 |
+
boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)',
|
| 135 |
opacity: '0',
|
| 136 |
+
transition: 'opacity .12s ease',
|
| 137 |
+
backdropFilter: 'saturate(1.12) blur(8px)',
|
| 138 |
+
zIndex: '20'
|
| 139 |
});
|
| 140 |
tipInner = document.createElement('div');
|
| 141 |
tipInner.className = 'd3-tooltip__inner';
|
| 142 |
+
Object.assign(tipInner.style, {
|
| 143 |
+
textAlign: 'left',
|
| 144 |
+
display: 'flex',
|
| 145 |
+
flexDirection: 'column',
|
| 146 |
+
gap: '6px',
|
| 147 |
+
minWidth: '220px'
|
| 148 |
+
});
|
| 149 |
tip.appendChild(tipInner);
|
| 150 |
container.appendChild(tip);
|
| 151 |
} else {
|
|
|
|
| 170 |
.attr('stroke-width', 0.4)
|
| 171 |
.on('mouseenter', function(ev, i) {
|
| 172 |
d3.select(this).raise()
|
| 173 |
+
.style('filter', `drop-shadow(0 0 8px ${glowColor})`)
|
| 174 |
+
.transition().duration(120).ease(d3.easeCubicOut)
|
| 175 |
.attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
|
| 176 |
+
.attr('stroke-width', 1.4)
|
| 177 |
+
.attr('r', (sizesPx[i] / 2) * 1.25)
|
| 178 |
+
.attr('fill-opacity', 1);
|
| 179 |
const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
|
| 180 |
const type = i < lenSpiral ? 'spiral' : 'bulge';
|
| 181 |
const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
|
| 182 |
+
tipInner.innerHTML =
|
| 183 |
+
`<div style="font-weight:800;letter-spacing:.1px;"><strong>${labelOf(i)}</strong></div>` +
|
| 184 |
+
`<div style="font-size:11px;color:var(--muted-color);margin-top:-4px;margin-bottom:2px;letter-spacing:.1px;"><strong>Type</strong> ${type}${arm ? ` (Arm ${arm})` : ''}</div>` +
|
| 185 |
+
`<div style="padding-top:6px;border-top:1px solid var(--border-color);"><strong>Position</strong> X ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
|
| 186 |
+
`<div><strong>Distance</strong> Radius ${r.toFixed(3)} · <strong>Z</strong> ${Zraw[i].toFixed(3)}</div>` +
|
| 187 |
+
`<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>`;
|
| 188 |
tip.style.opacity = '1';
|
| 189 |
})
|
| 190 |
.on('mousemove', (ev, i) => {
|
|
|
|
| 195 |
.on('mouseleave', function() {
|
| 196 |
tip.style.opacity = '0';
|
| 197 |
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 198 |
+
d3.select(this)
|
| 199 |
+
.style('filter', null)
|
| 200 |
+
.transition().duration(120).ease(d3.easeCubicOut)
|
| 201 |
+
.attr('stroke', strokeColor)
|
| 202 |
+
.attr('stroke-width', 0.4)
|
| 203 |
+
.attr('r', (i2) => sizesPx[i2] / 2)
|
| 204 |
+
.attr('fill-opacity', 0.9);
|
| 205 |
}),
|
| 206 |
(update) => update
|
| 207 |
.attr('cx', (i) => xScale(X[i]))
|
|
|
|
| 213 |
.attr('stroke-width', 0.4)
|
| 214 |
.on('mouseenter', function(ev, i) {
|
| 215 |
d3.select(this).raise()
|
| 216 |
+
.style('filter', `drop-shadow(0 0 8px ${glowColor})`)
|
| 217 |
+
.transition().duration(120).ease(d3.easeCubicOut)
|
| 218 |
.attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
|
| 219 |
+
.attr('stroke-width', 1.4)
|
| 220 |
+
.attr('r', (sizesPx[i] / 2) * 1.25)
|
| 221 |
+
.attr('fill-opacity', 1);
|
| 222 |
const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
|
| 223 |
const type = i < lenSpiral ? 'spiral' : 'bulge';
|
| 224 |
const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
|
| 225 |
+
tipInner.innerHTML =
|
| 226 |
+
`<div style="font-weight:800;letter-spacing:.1px;"><strong>${labelOf(i)}</strong></div>` +
|
| 227 |
+
`<div style="font-size:11px;color:var(--muted-color);margin-top:-4px;margin-bottom:2px;letter-spacing:.1px;"><strong>Type</strong> ${type}${arm ? ` (Arm ${arm})` : ''}</div>` +
|
| 228 |
+
`<div style="padding-top:6px;border-top:1px solid var(--border-color);"><strong>Position</strong> X ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
|
| 229 |
+
`<div><strong>Distance</strong> Radius ${r.toFixed(3)} · <strong>Z</strong> ${Zraw[i].toFixed(3)}</div>` +
|
| 230 |
+
`<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>`;
|
| 231 |
tip.style.opacity = '1';
|
| 232 |
})
|
| 233 |
.on('mousemove', (ev, i) => {
|
|
|
|
| 238 |
.on('mouseleave', function() {
|
| 239 |
tip.style.opacity = '0';
|
| 240 |
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 241 |
+
d3.select(this)
|
| 242 |
+
.style('filter', null)
|
| 243 |
+
.transition().duration(120).ease(d3.easeCubicOut)
|
| 244 |
+
.attr('stroke', strokeColor)
|
| 245 |
+
.attr('stroke-width', 0.4)
|
| 246 |
+
.attr('r', (i2) => sizesPx[i2] / 2)
|
| 247 |
+
.attr('fill-opacity', 0.9);
|
| 248 |
})
|
| 249 |
);
|
| 250 |
};
|
app/src/content/embeds/bar.html
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
<div> <div id="3e4ed4fe-23b4-4ec1-810e-20d6fce0209b" class="plotly-graph-div" style="height:100%; width:100%;"></div> <script type="text/javascript"> window.PLOTLYENV=window.PLOTLYENV || {}; if (document.getElementById("3e4ed4fe-23b4-4ec1-810e-20d6fce0209b")) { Plotly.newPlot( "3e4ed4fe-23b4-4ec1-810e-20d6fce0209b", [{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[4.0,4.0,4.0,4.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[4.0,4.0,4.0,4.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[8.0,8.0,8.0,8.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":true,"x":["1024","2048","4096","8192"],"y":[3.6,14.4,57.6,230.4],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[13.3,13.3,13.3,13.3],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[13.3,13.3,13.3,13.3],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[26.6,26.6,26.6,26.6],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[9.3,37.2,148.8,595.2],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[26.0,26.0,26.0,26.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[26.0,26.0,26.0,26.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[52.0,52.0,52.0,52.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[46.2,184.8,739.2,2956.8],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[244.0,244.0,244.0,244.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[244.0,244.0,244.0,244.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[488.0,488.0,488.0,488.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[145.7,582.8,2331.2,9324.8],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(78, 165, 183)"},"name":"parameters","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[1520.0,1520.0,1520.0,1520.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(227, 138, 66)"},"name":"gradients","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[1520.0,1520.0,1520.0,1520.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(232, 137, 171)"},"name":"optimizer","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[3040.0,3040.0,3040.0,3040.0],"type":"bar"},{"hovertemplate":"Seq len=%{x}\u003cbr\u003eMem=%{y:.1f}GB\u003cbr\u003e%{data.name}\u003cextra\u003e\u003c\u002fextra\u003e","marker":{"color":"rgb(206, 192, 250)"},"name":"activations","showlegend":true,"visible":false,"x":["1024","2048","4096","8192"],"y":[1519.9,6079.6,24318.4,97273.6],"type":"bar"}], {"template":{"data":{"histogram2dcontour":[{"type":"histogram2dcontour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"choropleth":[{"type":"choropleth","colorbar":{"outlinewidth":0,"ticks":""}}],"histogram2d":[{"type":"histogram2d","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"heatmap":[{"type":"heatmap","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"heatmapgl":[{"type":"heatmapgl","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"contourcarpet":[{"type":"contourcarpet","colorbar":{"outlinewidth":0,"ticks":""}}],"contour":[{"type":"contour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"surface":[{"type":"surface","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"mesh3d":[{"type":"mesh3d","colorbar":{"outlinewidth":0,"ticks":""}}],"scatter":[{"fillpattern":{"fillmode":"overlay","size":10,"solidity":0.2},"type":"scatter"}],"parcoords":[{"type":"parcoords","line":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolargl":[{"type":"scatterpolargl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"bar":[{"error_x":{"color":"#2a3f5f"},"error_y":{"color":"#2a3f5f"},"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"bar"}],"scattergeo":[{"type":"scattergeo","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolar":[{"type":"scatterpolar","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"histogram":[{"marker":{"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"histogram"}],"scattergl":[{"type":"scattergl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatter3d":[{"type":"scatter3d","line":{"colorbar":{"outlinewidth":0,"ticks":""}},"marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattermapbox":[{"type":"scattermapbox","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterternary":[{"type":"scatterternary","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattercarpet":[{"type":"scattercarpet","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"carpet":[{"aaxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"baxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"type":"carpet"}],"table":[{"cells":{"fill":{"color":"#EBF0F8"},"line":{"color":"white"}},"header":{"fill":{"color":"#C8D4E3"},"line":{"color":"white"}},"type":"table"}],"barpolar":[{"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"barpolar"}],"pie":[{"automargin":true,"type":"pie"}]},"layout":{"autotypenumbers":"strict","colorway":["#636efa","#EF553B","#00cc96","#ab63fa","#FFA15A","#19d3f3","#FF6692","#B6E880","#FF97FF","#FECB52"],"font":{"color":"#2a3f5f"},"hovermode":"closest","hoverlabel":{"align":"left"},"paper_bgcolor":"white","plot_bgcolor":"#E5ECF6","polar":{"bgcolor":"#E5ECF6","angularaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"radialaxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"ternary":{"bgcolor":"#E5ECF6","aaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"baxis":{"gridcolor":"white","linecolor":"white","ticks":""},"caxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"coloraxis":{"colorbar":{"outlinewidth":0,"ticks":""}},"colorscale":{"sequential":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"sequentialminus":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"diverging":[[0,"#8e0152"],[0.1,"#c51b7d"],[0.2,"#de77ae"],[0.3,"#f1b6da"],[0.4,"#fde0ef"],[0.5,"#f7f7f7"],[0.6,"#e6f5d0"],[0.7,"#b8e186"],[0.8,"#7fbc41"],[0.9,"#4d9221"],[1,"#276419"]]},"xaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"yaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"scene":{"xaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"yaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"zaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2}},"shapedefaults":{"line":{"color":"#2a3f5f"}},"annotationdefaults":{"arrowcolor":"#2a3f5f","arrowhead":0,"arrowwidth":1},"geo":{"bgcolor":"white","landcolor":"#E5ECF6","subunitcolor":"white","showland":true,"showlakes":true,"lakecolor":"white"},"title":{"x":0.05},"mapbox":{"style":"light"}}},"margin":{"l":40,"r":28,"t":20,"b":40},"legend":{"orientation":"h","yanchor":"bottom","y":1.02,"xanchor":"left","x":0},"xaxis":{"title":{"text":"Sequence Length"},"fixedrange":true},"yaxis":{"title":{"text":"Memory (GB)"},"fixedrange":true},"barmode":"stack","autosize":true,"paper_bgcolor":"rgba(0,0,0,0)","plot_bgcolor":"rgba(0,0,0,0)","hovermode":"x unified","updatemenus":[{"active":0,"buttons":[{"args":[{"visible":[true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]},{"yaxis":{"range":[0,258.72]}}],"label":"1B","method":"update"},{"args":[{"visible":[false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false,false,false,false,false]},{"yaxis":{"range":[0,680.8200000000002]}}],"label":"3B","method":"update"},{"args":[{"visible":[false,false,false,false,false,false,false,false,true,true,true,true,false,false,false,false,false,false,false,false]},{"yaxis":{"range":[0,3213.84]}}],"label":"8B","method":"update"},{"args":[{"visible":[false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true,false,false,false,false]},{"yaxis":{"range":[0,10815.84]}}],"label":"70B","method":"update"},{"args":[{"visible":[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,true,true,true,true]},{"yaxis":{"range":[0,108521.28000000001]}}],"label":"405B","method":"update"}],"showactive":true,"type":"dropdown","x":1.03,"xanchor":"left","y":0.6,"yanchor":"top"},{"active":0,"buttons":[{"args":[{"y":[[4.0,4.0,4.0,4.0],[4.0,4.0,4.0,4.0],[8.0,8.0,8.0,8.0],[3.6,14.4,57.6,230.4],[13.3,13.3,13.3,13.3],[13.3,13.3,13.3,13.3],[26.6,26.6,26.6,26.6],[9.3,37.2,148.8,595.2],[26.0,26.0,26.0,26.0],[26.0,26.0,26.0,26.0],[52.0,52.0,52.0,52.0],[46.2,184.8,739.2,2956.8],[244.0,244.0,244.0,244.0],[244.0,244.0,244.0,244.0],[488.0,488.0,488.0,488.0],[145.7,582.8,2331.2,9324.8],[1520.0,1520.0,1520.0,1520.0],[1520.0,1520.0,1520.0,1520.0],[3040.0,3040.0,3040.0,3040.0],[1519.9,6079.6,24318.4,97273.6]]},{"yaxis":{"range":[0,108521.28000000001]}}],"label":"None","method":"update"},{"args":[{"y":[[4.0,4.0,4.0,4.0],[4.0,4.0,4.0,4.0],[8.0,8.0,8.0,8.0],[0.9,3.6,14.4,57.6],[13.3,13.3,13.3,13.3],[13.3,13.3,13.3,13.3],[26.6,26.6,26.6,26.6],[2.325,9.3,37.2,148.8],[26.0,26.0,26.0,26.0],[26.0,26.0,26.0,26.0],[52.0,52.0,52.0,52.0],[11.55,46.2,184.8,739.2],[244.0,244.0,244.0,244.0],[244.0,244.0,244.0,244.0],[488.0,488.0,488.0,488.0],[36.425,145.7,582.8,2331.2],[1520.0,1520.0,1520.0,1520.0],[1520.0,1520.0,1520.0,1520.0],[3040.0,3040.0,3040.0,3040.0],[379.975,1519.9,6079.6,24318.4]]},{"yaxis":{"range":[0,31918.320000000003]}}],"label":"selective","method":"update"},{"args":[{"y":[[4.0,4.0,4.0,4.0],[4.0,4.0,4.0,4.0],[8.0,8.0,8.0,8.0],[0.225,0.9,3.6,14.4],[13.3,13.3,13.3,13.3],[13.3,13.3,13.3,13.3],[26.6,26.6,26.6,26.6],[0.58125,2.325,9.3,37.2],[26.0,26.0,26.0,26.0],[26.0,26.0,26.0,26.0],[52.0,52.0,52.0,52.0],[2.8875,11.55,46.2,184.8],[244.0,244.0,244.0,244.0],[244.0,244.0,244.0,244.0],[488.0,488.0,488.0,488.0],[9.10625,36.425,145.7,582.8],[1520.0,1520.0,1520.0,1520.0],[1520.0,1520.0,1520.0,1520.0],[3040.0,3040.0,3040.0,3040.0],[94.99375,379.975,1519.9,6079.6]]},{"yaxis":{"range":[0,12767.580000000002]}}],"label":"full","method":"update"}],"showactive":true,"type":"dropdown","x":1.03,"xanchor":"left","y":0.4,"yanchor":"top"}],"annotations":[{"showarrow":false,"text":"Model Size:","x":1.03,"xanchor":"left","xref":"paper","y":0.6,"yanchor":"bottom","yref":"paper"},{"showarrow":false,"text":"Recomputation:","x":1.03,"xanchor":"left","xref":"paper","y":0.4,"yanchor":"bottom","yref":"paper"}]}, {"displayModeBar": false, "responsive": true, "scrollZoom": false} ) }; </script> </div>
|
|
|
|
|
|
app/src/content/embeds/d3-area-stacked.html
DELETED
|
@@ -1,220 +0,0 @@
|
|
| 1 |
-
<div class="d3-area-stacked" style="width:100%;margin:10px 0;"></div>
|
| 2 |
-
<style>
|
| 3 |
-
.d3-area-stacked .controls {
|
| 4 |
-
margin-top: 12px;
|
| 5 |
-
display: flex;
|
| 6 |
-
gap: 16px;
|
| 7 |
-
align-items: center;
|
| 8 |
-
flex-wrap: wrap;
|
| 9 |
-
}
|
| 10 |
-
.d3-area-stacked .controls label {
|
| 11 |
-
font-size: 12px;
|
| 12 |
-
color: var(--muted-color);
|
| 13 |
-
display: flex;
|
| 14 |
-
align-items: center;
|
| 15 |
-
gap: 8px;
|
| 16 |
-
white-space: nowrap;
|
| 17 |
-
padding: 6px 10px;
|
| 18 |
-
}
|
| 19 |
-
.d3-area-stacked .controls select {
|
| 20 |
-
font-size: 12px;
|
| 21 |
-
padding: 8px 28px 8px 10px;
|
| 22 |
-
border: 1px solid var(--border-color);
|
| 23 |
-
border-radius: 8px;
|
| 24 |
-
background-color: var(--surface-bg);
|
| 25 |
-
color: var(--text-color);
|
| 26 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 27 |
-
background-repeat: no-repeat;
|
| 28 |
-
background-position: right 8px center;
|
| 29 |
-
background-size: 12px;
|
| 30 |
-
-webkit-appearance: none;
|
| 31 |
-
-moz-appearance: none;
|
| 32 |
-
appearance: none;
|
| 33 |
-
cursor: pointer;
|
| 34 |
-
transition: border-color .15s ease, box-shadow .15s ease;
|
| 35 |
-
}
|
| 36 |
-
[data-theme="dark"] .d3-area-stacked .controls select {
|
| 37 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 38 |
-
}
|
| 39 |
-
.d3-area-stacked .controls select:hover { border-color: var(--primary-color); }
|
| 40 |
-
.d3-area-stacked .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 41 |
-
.d3-area-stacked .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
|
| 42 |
-
</style>
|
| 43 |
-
<script>
|
| 44 |
-
(() => {
|
| 45 |
-
const ensureD3 = (cb) => {
|
| 46 |
-
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 47 |
-
let s = document.getElementById('d3-cdn-script');
|
| 48 |
-
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
| 49 |
-
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 50 |
-
s.addEventListener('load', onReady, { once: true });
|
| 51 |
-
if (window.d3) onReady();
|
| 52 |
-
};
|
| 53 |
-
|
| 54 |
-
const bootstrap = () => {
|
| 55 |
-
const scriptEl = document.currentScript;
|
| 56 |
-
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 57 |
-
if (!(container && container.classList && container.classList.contains('d3-area-stacked'))){
|
| 58 |
-
const cs = Array.from(document.querySelectorAll('.d3-area-stacked')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
|
| 59 |
-
container = cs[cs.length-1] || null;
|
| 60 |
-
}
|
| 61 |
-
if (!container) return;
|
| 62 |
-
if (container.dataset){ if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
|
| 63 |
-
|
| 64 |
-
// Tooltip
|
| 65 |
-
container.style.position = container.style.position || 'relative';
|
| 66 |
-
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 67 |
-
if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style,{ position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 68 |
-
|
| 69 |
-
// Controls
|
| 70 |
-
const controls = document.createElement('div'); controls.className='controls';
|
| 71 |
-
const labelRun = document.createElement('label'); labelRun.textContent='Run';
|
| 72 |
-
const selRun = document.createElement('select');
|
| 73 |
-
labelRun.appendChild(selRun);
|
| 74 |
-
const labelSmooth = document.createElement('label'); labelSmooth.textContent='Smoothing';
|
| 75 |
-
const selSmooth = document.createElement('select'); ['none','monotone','basis'].forEach((s)=>{ const o=document.createElement('option'); o.value=s; o.textContent=s; selSmooth.appendChild(o); });
|
| 76 |
-
labelSmooth.appendChild(selSmooth);
|
| 77 |
-
|
| 78 |
-
// SVG
|
| 79 |
-
const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
|
| 80 |
-
const gRoot = svg.append('g');
|
| 81 |
-
const gGrid = gRoot.append('g').attr('class','grid');
|
| 82 |
-
const gAxes = gRoot.append('g').attr('class','axes');
|
| 83 |
-
const gAreas = gRoot.append('g').attr('class','areas');
|
| 84 |
-
const gLegend = gRoot.append('foreignObject').attr('class','legend');
|
| 85 |
-
|
| 86 |
-
// State & scales
|
| 87 |
-
let width=800,height=360; const margin={top:16,right:28,bottom:56,left:64};
|
| 88 |
-
const x=d3.scaleLinear();
|
| 89 |
-
const y=d3.scaleLinear();
|
| 90 |
-
const color=d3.scaleOrdinal().range(['var(--primary-color)','rgb(78, 165, 183)','rgb(227, 138, 66)','rgb(206, 192, 250)']);
|
| 91 |
-
|
| 92 |
-
// Data (real): relative contributions of selected metrics over steps for a run
|
| 93 |
-
const metricsToUse = ['ai2d_exact_match','docvqa_val_anls','textvqa_val_exact_match','chartqa_relaxed_overall'];
|
| 94 |
-
let categories = metricsToUse.slice();
|
| 95 |
-
let allRows = [];
|
| 96 |
-
let currentRun = null;
|
| 97 |
-
let stacked = [];
|
| 98 |
-
|
| 99 |
-
async function fetchFirstAvailable(paths){
|
| 100 |
-
for (const p of paths){
|
| 101 |
-
try { const res = await fetch(p, { cache:'no-cache' }); if (res.ok) return await res.text(); } catch(e){}
|
| 102 |
-
}
|
| 103 |
-
throw new Error('Failed to load area data');
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
function computeStackForRun(runName){
|
| 107 |
-
const rows = allRows.filter(r=>r.run===runName && metricsToUse.includes(r.metric));
|
| 108 |
-
const byStep = d3.rollup(rows, v=>{
|
| 109 |
-
const m = new Map(); v.forEach(r=>{ m.set(r.metric, +r.value); });
|
| 110 |
-
const obj = {}; metricsToUse.forEach(k=>{ obj[k] = m.get(k) ?? 0; });
|
| 111 |
-
// Normalize to shares
|
| 112 |
-
const sum = metricsToUse.reduce((acc,k)=>acc + Math.max(0, obj[k]), 0) || 1;
|
| 113 |
-
metricsToUse.forEach(k=>{ obj[k] = Math.max(0, obj[k]) / sum; });
|
| 114 |
-
return obj;
|
| 115 |
-
}, r=>+r.step);
|
| 116 |
-
const steps = Array.from(byStep.keys()).sort((a,b)=>a-b);
|
| 117 |
-
const series = categories.map(cat => steps.map(step => ({ x: step, y: byStep.get(step)[cat] })));
|
| 118 |
-
stacked = d3.stack().keys(d3.range(categories.length)).value((d, key)=>d[key].y)(d3.transpose(series));
|
| 119 |
-
return steps;
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
function renderLegend(innerWidth){
|
| 123 |
-
const legendWidth=160, legendHeight=84;
|
| 124 |
-
gLegend.attr('x', innerWidth-legendWidth).attr('y', 0).attr('width', legendWidth).attr('height', legendHeight);
|
| 125 |
-
const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
|
| 126 |
-
root.html(`
|
| 127 |
-
<div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end;">
|
| 128 |
-
${categories.map((c,i)=>`<div style=\"display:flex;align-items:center;gap:8px;\"><span style=\"width:18px;height:10px;background:${color(c)};border-radius:2px;display:inline-block\"></span><span>${c}</span></div>`).join('')}
|
| 129 |
-
</div>
|
| 130 |
-
`);
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
function updateScales(){
|
| 134 |
-
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 135 |
-
const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
|
| 136 |
-
const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
|
| 137 |
-
const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
|
| 138 |
-
|
| 139 |
-
width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
|
| 140 |
-
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 141 |
-
|
| 142 |
-
const yMax = stacked.length ? d3.max(stacked[stacked.length-1], (d) => d[1]) : 1;
|
| 143 |
-
const xMin = stacked.length ? d3.min(stacked[0], d=>d.data[0].x) : 0;
|
| 144 |
-
const xMax = stacked.length ? d3.max(stacked[0], d=>d.data[d.data.length-1].x) : 1;
|
| 145 |
-
x.domain([xMin, xMax]).range([0, innerWidth]).nice();
|
| 146 |
-
y.domain([0, yMax]).range([innerHeight, 0]).nice();
|
| 147 |
-
|
| 148 |
-
// Grid
|
| 149 |
-
gGrid.selectAll('*').remove();
|
| 150 |
-
gGrid.selectAll('line.grid-y').data(y.ticks(5)).join('line')
|
| 151 |
-
.attr('class','grid-y').attr('x1',0).attr('x2',innerWidth).attr('y1',d=>y(d)).attr('y2',d=>y(d))
|
| 152 |
-
.attr('stroke', gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
|
| 153 |
-
|
| 154 |
-
// Axes
|
| 155 |
-
gAxes.selectAll('*').remove();
|
| 156 |
-
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x).ticks(6)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
|
| 157 |
-
gAxes.append('g').call(d3.axisLeft(y).ticks(5)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
|
| 158 |
-
gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').style('font-size','12px').style('fill', tickColor).text('Step');
|
| 159 |
-
gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-48},${innerHeight/2}) rotate(-90)`).style('font-size','12px').style('fill', tickColor).text('Value');
|
| 160 |
-
|
| 161 |
-
renderLegend(innerWidth);
|
| 162 |
-
return { innerWidth, innerHeight };
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
function draw(){
|
| 166 |
-
const { innerWidth, innerHeight } = updateScales();
|
| 167 |
-
|
| 168 |
-
const curve = selSmooth.value==='monotone' ? d3.curveMonotoneX : selSmooth.value==='basis' ? d3.curveBasis : d3.curveLinear;
|
| 169 |
-
const area = d3.area().curve(curve).x((d,i)=>x(d.data[i].x)).y0(d=>y(d[0])).y1(d=>y(d[1]));
|
| 170 |
-
|
| 171 |
-
const layers = gAreas.selectAll('path.layer').data(stacked);
|
| 172 |
-
layers.enter().append('path').attr('class','layer')
|
| 173 |
-
.attr('fill', (d,i)=>color(categories[i]))
|
| 174 |
-
.attr('opacity', 0.9)
|
| 175 |
-
.on('mouseenter', function(ev, d){ const i = stacked.indexOf(d); d3.select(this).attr('opacity', 1); tipInner.innerHTML = `<div><strong>${categories[i]}</strong></div>`; tip.style.opacity = '1'; })
|
| 176 |
-
.on('mousemove', function(ev){ const [mx,my]=d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
|
| 177 |
-
.on('mouseleave', function(){ d3.select(this).attr('opacity', 0.9); tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; })
|
| 178 |
-
.merge(layers)
|
| 179 |
-
.transition().duration(180)
|
| 180 |
-
.attr('d', d=>area(d));
|
| 181 |
-
layers.exit().remove();
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
container.appendChild(controls);
|
| 185 |
-
controls.appendChild(labelRun);
|
| 186 |
-
controls.appendChild(labelSmooth);
|
| 187 |
-
selSmooth.addEventListener('change', ()=>draw());
|
| 188 |
-
|
| 189 |
-
(async () => {
|
| 190 |
-
try {
|
| 191 |
-
const csvText = await fetchFirstAvailable([
|
| 192 |
-
'/data/all_ratings_luis.csv',
|
| 193 |
-
'/data/against_baselines.csv',
|
| 194 |
-
'/data/ss_vs_s1.csv',
|
| 195 |
-
'./assets/data/all_ratings_luis.csv',
|
| 196 |
-
'../assets/data/all_ratings_luis.csv',
|
| 197 |
-
'/data/all_ratings_luis.csv'
|
| 198 |
-
]);
|
| 199 |
-
allRows = d3.csvParse(csvText, d=>({ run: d.run, step: +d.step, metric: d.metric, value: +d.value }));
|
| 200 |
-
const runs = Array.from(new Set(allRows.map(r=>r.run))).filter(Boolean);
|
| 201 |
-
runs.forEach(r=>{ const o=document.createElement('option'); o.value=r; o.textContent=r; selRun.appendChild(o); });
|
| 202 |
-
currentRun = runs[0]; selRun.value = currentRun;
|
| 203 |
-
selRun.addEventListener('change', (e)=>{ currentRun = e.target.value; draw(); });
|
| 204 |
-
color.domain(categories);
|
| 205 |
-
computeStackForRun(currentRun);
|
| 206 |
-
draw();
|
| 207 |
-
} catch (e) {
|
| 208 |
-
const pre = document.createElement('pre'); pre.style.color = 'crimson'; pre.textContent = 'Failed to load area data.'; container.appendChild(pre);
|
| 209 |
-
}
|
| 210 |
-
})();
|
| 211 |
-
|
| 212 |
-
const rerender = () => { draw(); };
|
| 213 |
-
if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
|
| 214 |
-
};
|
| 215 |
-
|
| 216 |
-
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
|
| 217 |
-
})();
|
| 218 |
-
</script>
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/d3-bar.html
CHANGED
|
@@ -1,12 +1,66 @@
|
|
| 1 |
-
<div class="d3-bar"
|
| 2 |
<style>
|
| 3 |
-
|
| 4 |
-
.d3-bar
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
.d3-bar .controls select { font-size: 12px; padding: 8px 28px 8px 10px; border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--surface-bg); color: var(--text-color); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
|
| 6 |
[data-theme="dark"] .d3-bar .controls select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); }
|
| 7 |
.d3-bar .controls select:hover { border-color: var(--primary-color); }
|
| 8 |
.d3-bar .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</style>
|
| 11 |
<script>
|
| 12 |
(() => {
|
|
@@ -28,12 +82,7 @@
|
|
| 28 |
// Data, matching bar.py
|
| 29 |
const seqLabels = ["1024","2048","4096","8192"];
|
| 30 |
const seqScale = [1,2,4,8];
|
| 31 |
-
const
|
| 32 |
-
{ key: 'parameters', color: 'rgb(78, 165, 183)' },
|
| 33 |
-
{ key: 'gradients', color: 'rgb(227, 138, 66)' },
|
| 34 |
-
{ key: 'optimizer', color: 'var(--primary-color)' },
|
| 35 |
-
{ key: 'activations', color: 'rgb(206, 192, 250)' },
|
| 36 |
-
];
|
| 37 |
const modelSizes = ["1B","3B","8B","70B","405B"];
|
| 38 |
const paramsMem = { "1B":4.0, "3B":13.3, "8B":26.0, "70B":244.0, "405B":1520.0 };
|
| 39 |
const actCoeff = { "1B":3.6, "3B":9.3, "8B":46.2, "70B":145.7, "405B":1519.9 };
|
|
@@ -62,20 +111,32 @@
|
|
| 62 |
// Controls
|
| 63 |
const controls = document.createElement('div');
|
| 64 |
controls.className = 'controls';
|
|
|
|
| 65 |
const labelSize = document.createElement('label'); labelSize.textContent = 'Model Size';
|
| 66 |
const selSize = document.createElement('select'); modelSizes.forEach((s) => { const o = document.createElement('option'); o.value = s; o.textContent = s; selSize.appendChild(o); });
|
| 67 |
-
labelSize.appendChild(selSize);
|
|
|
|
| 68 |
const labelRecomp = document.createElement('label'); labelRecomp.textContent = 'Recomputation';
|
| 69 |
const selRecomp = document.createElement('select'); recomputeModes.forEach((m) => { const o = document.createElement('option'); o.value = m; o.textContent = m; selRecomp.appendChild(o); });
|
| 70 |
-
labelRecomp.appendChild(selRecomp);
|
| 71 |
|
| 72 |
-
//
|
| 73 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
const gRoot = svg.append('g');
|
| 75 |
const gGrid = gRoot.append('g').attr('class','grid');
|
| 76 |
const gAxes = gRoot.append('g').attr('class','axes');
|
| 77 |
const gBars = gRoot.append('g').attr('class','bars');
|
| 78 |
-
const gLegend = gRoot.append('foreignObject').attr('class','legend');
|
| 79 |
|
| 80 |
// Tooltip
|
| 81 |
container.style.position = container.style.position || 'relative';
|
|
@@ -91,7 +152,21 @@
|
|
| 91 |
let width=800, height=360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
|
| 92 |
const x0 = d3.scaleBand().paddingInner(0.25).paddingOuter(0.1); // groups (seq)
|
| 93 |
const y = d3.scaleLinear();
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
function yMax(sizeKey, mode){
|
| 97 |
const s = Y[mode][sizeKey];
|
|
@@ -99,23 +174,27 @@
|
|
| 99 |
return max*1.05;
|
| 100 |
}
|
| 101 |
|
| 102 |
-
function renderLegend(
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
function updateScales(){
|
| 114 |
-
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 115 |
-
const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
|
| 116 |
-
const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
|
| 117 |
-
const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
|
| 118 |
-
|
| 119 |
width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
|
| 120 |
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 121 |
|
|
@@ -126,25 +205,25 @@
|
|
| 126 |
gGrid.selectAll('*').remove();
|
| 127 |
gGrid.selectAll('line').data(y.ticks(6)).join('line')
|
| 128 |
.attr('x1', 0).attr('x2', innerWidth).attr('y1', (d)=>y(d)).attr('y2', (d)=>y(d))
|
| 129 |
-
.attr('stroke',
|
| 130 |
|
| 131 |
// Axes
|
| 132 |
gAxes.selectAll('*').remove();
|
| 133 |
-
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0)).call((g)=>{ g.selectAll('path, line').attr('stroke',
|
| 134 |
-
gAxes.append('g').call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('~f'))).call((g)=>{ g.selectAll('path, line').attr('stroke',
|
| 135 |
|
| 136 |
// Axis labels
|
| 137 |
-
gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').
|
| 138 |
-
gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`).
|
| 139 |
|
| 140 |
-
renderLegend(
|
| 141 |
|
| 142 |
return { innerWidth, innerHeight };
|
| 143 |
}
|
| 144 |
|
| 145 |
function drawBars(){
|
| 146 |
const stacks = Y[currentMode][currentSize];
|
| 147 |
-
const series =
|
| 148 |
// Stack values
|
| 149 |
const stacked = seqLabels.map((label, i) => {
|
| 150 |
let acc = 0; const items = [];
|
|
@@ -153,6 +232,8 @@
|
|
| 153 |
items.push({ key: s.key, color: s.color, i, y0, y1, xLabel: label, value: s.values[i], isBottom: idx === 0, isTop: idx === series.length - 1 });
|
| 154 |
acc = y1;
|
| 155 |
});
|
|
|
|
|
|
|
| 156 |
return { label, items };
|
| 157 |
});
|
| 158 |
|
|
@@ -187,14 +268,39 @@
|
|
| 187 |
.attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
|
| 188 |
.attr('fill', (d)=>d.color)
|
| 189 |
.on('mouseenter', function(ev, d){
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
tip.style.opacity = '1';
|
|
|
|
|
|
|
|
|
|
| 193 |
})
|
| 194 |
.on('mousemove', function(ev, d){
|
| 195 |
-
const [mx, my] = d3.pointer(ev, container);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
})
|
| 197 |
-
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('stroke','none'); })
|
| 198 |
.merge(bars)
|
| 199 |
.transition().duration(200)
|
| 200 |
.attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
|
|
@@ -206,8 +312,8 @@
|
|
| 206 |
|
| 207 |
// Boot
|
| 208 |
update();
|
| 209 |
-
|
| 210 |
-
controls.appendChild(
|
| 211 |
selSize.addEventListener('change', (e)=>{ currentSize = e.target.value; update(); });
|
| 212 |
selRecomp.addEventListener('change', (e)=>{ currentMode = e.target.value; update(); });
|
| 213 |
|
|
|
|
| 1 |
+
<div class="d3-bar" ></div>
|
| 2 |
<style>
|
| 3 |
+
/* Theme-driven axis/tick/grid variables */
|
| 4 |
+
.d3-bar {
|
| 5 |
+
--axis-color: rgba(0,0,0,0.25);
|
| 6 |
+
--tick-color: rgba(0,0,0,0.55);
|
| 7 |
+
--grid-color: rgba(0,0,0,0.05);
|
| 8 |
+
}
|
| 9 |
+
[data-theme="dark"] .d3-bar {
|
| 10 |
+
--axis-color: rgba(255,255,255,0.25);
|
| 11 |
+
--tick-color: rgba(255,255,255,0.70);
|
| 12 |
+
--grid-color: rgba(255,255,255,0.08);
|
| 13 |
+
}
|
| 14 |
+
.d3-bar .controls { margin-top: 0; display: flex; gap: 16px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
|
| 15 |
+
.d3-bar .controls .control-group { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
|
| 16 |
+
.d3-bar .controls label { font-size: 12px; color: var(--text-color); font-weight: 700; }
|
| 17 |
.d3-bar .controls select { font-size: 12px; padding: 8px 28px 8px 10px; border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--surface-bg); color: var(--text-color); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
|
| 18 |
[data-theme="dark"] .d3-bar .controls select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); }
|
| 19 |
.d3-bar .controls select:hover { border-color: var(--primary-color); }
|
| 20 |
.d3-bar .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 21 |
+
/* Header (legend + controls) placed after chart */
|
| 22 |
+
.d3-bar .chart-header { display: flex; align-items: flex-start; justify-content: flex-start; gap: 12px; margin: 8px 0 0 0; flex-wrap: wrap; }
|
| 23 |
+
.d3-bar .legend-bottom { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; font-size: 12px; color: var(--text-color); }
|
| 24 |
+
.d3-bar .legend-bottom .legend-title { font-size: 12px; font-weight: 700; color: var(--text-color); }
|
| 25 |
+
.d3-bar .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
|
| 26 |
+
.d3-bar .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
| 27 |
+
.d3-bar .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border-color); display: inline-block; }
|
| 28 |
+
.d3-bar.hovering .legend-bottom .item.ghost { opacity: .35; }
|
| 29 |
+
.d3-bar.hovering .bars path.ghost { opacity: .35; }
|
| 30 |
+
.d3-bar .axis-label { fill: var(--text-color); font-size: 12px; font-weight: 700; }
|
| 31 |
+
/* Apply axis/tick/grid purely via CSS */
|
| 32 |
+
.d3-bar .axes path,
|
| 33 |
+
.d3-bar .axes line { stroke: var(--axis-color); }
|
| 34 |
+
.d3-bar .axes text { fill: var(--tick-color); }
|
| 35 |
+
.d3-bar .grid line { stroke: var(--grid-color); }
|
| 36 |
+
/* Tooltip improvements */
|
| 37 |
+
.d3-bar .d3-tooltip { z-index: var(--z-tooltip); backdrop-filter: saturate(1.12) blur(8px); }
|
| 38 |
+
/* Hover/transition styling for bars and legend */
|
| 39 |
+
.d3-bar .bars path.bar { transition: opacity .12s ease, stroke .12s ease, stroke-width .12s ease; }
|
| 40 |
+
.d3-bar .bars path.bar.highlight { stroke: none; stroke-width: 0; }
|
| 41 |
+
.d3-bar.hovering .bars path.ghost { opacity: .25; }
|
| 42 |
+
.d3-bar .legend-bottom .item.hovered { color: inherit; }
|
| 43 |
+
.d3-bar .legend-bottom .item.hovered .swatch { border-color: var(--border-color); }
|
| 44 |
+
.d3-bar .d3-tooltip .swatch { width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-color); display: inline-block; margin-right: 6px; vertical-align: -2px; }
|
| 45 |
+
/* Chart card wrapper */
|
| 46 |
+
.d3-bar .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
|
| 47 |
+
/* Layout adjustments to give controls more space */
|
| 48 |
+
.d3-bar .chart-header {
|
| 49 |
+
padding-left: 8px;
|
| 50 |
+
padding-right: 8px;
|
| 51 |
+
gap: 20px;
|
| 52 |
+
}
|
| 53 |
+
.d3-bar .controls {
|
| 54 |
+
justify-content: flex-start;
|
| 55 |
+
min-width: 320px;
|
| 56 |
+
}
|
| 57 |
+
.d3-bar .controls .control-group {
|
| 58 |
+
min-width: 150px;
|
| 59 |
+
}
|
| 60 |
+
.d3-bar .controls select {
|
| 61 |
+
font-size: 13px;
|
| 62 |
+
min-width: 160px;
|
| 63 |
+
}
|
| 64 |
</style>
|
| 65 |
<script>
|
| 66 |
(() => {
|
|
|
|
| 82 |
// Data, matching bar.py
|
| 83 |
const seqLabels = ["1024","2048","4096","8192"];
|
| 84 |
const seqScale = [1,2,4,8];
|
| 85 |
+
const componentKeys = ['parameters','gradients','optimizer','activations'];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
const modelSizes = ["1B","3B","8B","70B","405B"];
|
| 87 |
const paramsMem = { "1B":4.0, "3B":13.3, "8B":26.0, "70B":244.0, "405B":1520.0 };
|
| 88 |
const actCoeff = { "1B":3.6, "3B":9.3, "8B":46.2, "70B":145.7, "405B":1519.9 };
|
|
|
|
| 111 |
// Controls
|
| 112 |
const controls = document.createElement('div');
|
| 113 |
controls.className = 'controls';
|
| 114 |
+
const groupSize = document.createElement('div'); groupSize.className = 'control-group';
|
| 115 |
const labelSize = document.createElement('label'); labelSize.textContent = 'Model Size';
|
| 116 |
const selSize = document.createElement('select'); modelSizes.forEach((s) => { const o = document.createElement('option'); o.value = s; o.textContent = s; selSize.appendChild(o); });
|
| 117 |
+
groupSize.appendChild(labelSize); groupSize.appendChild(selSize);
|
| 118 |
+
const groupRecomp = document.createElement('div'); groupRecomp.className = 'control-group';
|
| 119 |
const labelRecomp = document.createElement('label'); labelRecomp.textContent = 'Recomputation';
|
| 120 |
const selRecomp = document.createElement('select'); recomputeModes.forEach((m) => { const o = document.createElement('option'); o.value = m; o.textContent = m; selRecomp.appendChild(o); });
|
| 121 |
+
groupRecomp.appendChild(labelRecomp); groupRecomp.appendChild(selRecomp);
|
| 122 |
|
| 123 |
+
// Header (legend + controls) to be placed after chart
|
| 124 |
+
const header = document.createElement('div'); header.className = 'chart-header';
|
| 125 |
+
const legendBottom = document.createElement('div'); legendBottom.className = 'legend-bottom';
|
| 126 |
+
const legendTitle = document.createElement('div'); legendTitle.className = 'legend-title'; legendTitle.textContent = 'Legend';
|
| 127 |
+
const legendItems = document.createElement('div'); legendItems.className = 'items';
|
| 128 |
+
legendBottom.appendChild(legendTitle); legendBottom.appendChild(legendItems);
|
| 129 |
+
header.appendChild(legendBottom);
|
| 130 |
+
header.appendChild(controls);
|
| 131 |
+
// SVG scaffolding inside a card wrapper
|
| 132 |
+
const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
|
| 133 |
+
// Place header after the chart card
|
| 134 |
+
container.appendChild(header);
|
| 135 |
+
const svg = d3.select(card).append('svg').attr('width','100%').style('display','block');
|
| 136 |
const gRoot = svg.append('g');
|
| 137 |
const gGrid = gRoot.append('g').attr('class','grid');
|
| 138 |
const gAxes = gRoot.append('g').attr('class','axes');
|
| 139 |
const gBars = gRoot.append('g').attr('class','bars');
|
|
|
|
| 140 |
|
| 141 |
// Tooltip
|
| 142 |
container.style.position = container.style.position || 'relative';
|
|
|
|
| 152 |
let width=800, height=360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
|
| 153 |
const x0 = d3.scaleBand().paddingInner(0.25).paddingOuter(0.1); // groups (seq)
|
| 154 |
const y = d3.scaleLinear();
|
| 155 |
+
function getCategoricalColors(count){
|
| 156 |
+
try {
|
| 157 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 158 |
+
return window.ColorPalettes.getColors('categorical', count);
|
| 159 |
+
}
|
| 160 |
+
} catch(_) {}
|
| 161 |
+
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 162 |
+
const tableau = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'];
|
| 163 |
+
const pool = [primary, ...tableau];
|
| 164 |
+
const arr = []; for (let i=0;i<count;i++){ arr.push(pool[i % pool.length]); }
|
| 165 |
+
return arr;
|
| 166 |
+
}
|
| 167 |
+
const palette = getCategoricalColors(componentKeys.length);
|
| 168 |
+
const colorMap = new Map(componentKeys.map((k,i)=>[k, palette[i]]));
|
| 169 |
+
const colorOf = (key) => colorMap.get(key) || 'var(--primary-color)';
|
| 170 |
|
| 171 |
function yMax(sizeKey, mode){
|
| 172 |
const s = Y[mode][sizeKey];
|
|
|
|
| 174 |
return max*1.05;
|
| 175 |
}
|
| 176 |
|
| 177 |
+
function renderLegend(){
|
| 178 |
+
legendItems.innerHTML = componentKeys.map((key, i) => {
|
| 179 |
+
const color = palette[i];
|
| 180 |
+
return `<span class="item" data-key="${key}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${key}</span></span>`;
|
| 181 |
+
}).join('');
|
| 182 |
+
legendItems.querySelectorAll('.item').forEach((el) => {
|
| 183 |
+
el.addEventListener('mouseenter', () => {
|
| 184 |
+
const k = el.getAttribute('data-key'); if (!k) return;
|
| 185 |
+
container.classList.add('hovering');
|
| 186 |
+
gBars.selectAll('path.bar').classed('ghost', d => d && d.key !== k);
|
| 187 |
+
legendItems.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-key') !== k));
|
| 188 |
+
});
|
| 189 |
+
el.addEventListener('mouseleave', () => {
|
| 190 |
+
container.classList.remove('hovering');
|
| 191 |
+
gBars.selectAll('path.bar').classed('ghost', false);
|
| 192 |
+
legendItems.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
|
| 193 |
+
});
|
| 194 |
+
});
|
| 195 |
}
|
| 196 |
|
| 197 |
function updateScales(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
|
| 199 |
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 200 |
|
|
|
|
| 205 |
gGrid.selectAll('*').remove();
|
| 206 |
gGrid.selectAll('line').data(y.ticks(6)).join('line')
|
| 207 |
.attr('x1', 0).attr('x2', innerWidth).attr('y1', (d)=>y(d)).attr('y2', (d)=>y(d))
|
| 208 |
+
.attr('stroke', 'var(--grid-color)').attr('stroke-width', 1).attr('shape-rendering', 'crispEdges');
|
| 209 |
|
| 210 |
// Axes
|
| 211 |
gAxes.selectAll('*').remove();
|
| 212 |
+
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x0)).call((g)=>{ g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px'); });
|
| 213 |
+
gAxes.append('g').call(d3.axisLeft(y).ticks(6).tickFormat(d3.format('~f'))).call((g)=>{ g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px'); });
|
| 214 |
|
| 215 |
// Axis labels
|
| 216 |
+
gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').text('Sequence Length');
|
| 217 |
+
gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`).text('Memory (GB)');
|
| 218 |
|
| 219 |
+
renderLegend();
|
| 220 |
|
| 221 |
return { innerWidth, innerHeight };
|
| 222 |
}
|
| 223 |
|
| 224 |
function drawBars(){
|
| 225 |
const stacks = Y[currentMode][currentSize];
|
| 226 |
+
const series = componentKeys.map((key, i)=>({ key, color: palette[i], values: stacks[key] }));
|
| 227 |
// Stack values
|
| 228 |
const stacked = seqLabels.map((label, i) => {
|
| 229 |
let acc = 0; const items = [];
|
|
|
|
| 232 |
items.push({ key: s.key, color: s.color, i, y0, y1, xLabel: label, value: s.values[i], isBottom: idx === 0, isTop: idx === series.length - 1 });
|
| 233 |
acc = y1;
|
| 234 |
});
|
| 235 |
+
const total = acc;
|
| 236 |
+
items.forEach(it => { it.total = total; });
|
| 237 |
return { label, items };
|
| 238 |
});
|
| 239 |
|
|
|
|
| 268 |
.attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
|
| 269 |
.attr('fill', (d)=>d.color)
|
| 270 |
.on('mouseenter', function(ev, d){
|
| 271 |
+
container.classList.add('hovering');
|
| 272 |
+
gBars.selectAll('path.bar').classed('ghost', (dd) => !(dd && dd.key === d.key));
|
| 273 |
+
const pct = d.total > 0 ? (d.value / d.total * 100) : 0;
|
| 274 |
+
tipInner.innerHTML = `
|
| 275 |
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
|
| 276 |
+
<span class="swatch" style="background:${d.color}"></span>
|
| 277 |
+
<strong>${d.key}</strong>
|
| 278 |
+
</div>
|
| 279 |
+
<div><strong>Seq</strong> ${d.xLabel}</div>
|
| 280 |
+
<div><strong>Mem</strong> ${d.value.toFixed(1)} GB <span style="opacity:.7">(${pct.toFixed(0)}%)</span></div>
|
| 281 |
+
<div style="opacity:.7"><strong>Total</strong> ${d.total.toFixed(1)} GB</div>
|
| 282 |
+
`;
|
| 283 |
tip.style.opacity = '1';
|
| 284 |
+
const li = legendItems.querySelector(`.item[data-key="${d.key}"]`);
|
| 285 |
+
if (li) li.classList.add('hovered');
|
| 286 |
+
legendItems.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-key') !== d.key));
|
| 287 |
})
|
| 288 |
.on('mousemove', function(ev, d){
|
| 289 |
+
const [mx, my] = d3.pointer(ev, container);
|
| 290 |
+
const offsetX = 12, offsetY = 12;
|
| 291 |
+
const maxX = (container.clientWidth || 0) - (tip.offsetWidth + 6);
|
| 292 |
+
const maxY = (container.clientHeight || 0) - (tip.offsetHeight + 6);
|
| 293 |
+
const tx = Math.max(0, Math.min(mx + offsetX, maxX));
|
| 294 |
+
const ty = Math.max(0, Math.min(my + offsetY, maxY));
|
| 295 |
+
tip.style.transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`;
|
| 296 |
+
})
|
| 297 |
+
.on('mouseleave', function(){
|
| 298 |
+
tip.style.opacity='0';
|
| 299 |
+
tip.style.transform='translate(-9999px, -9999px)';
|
| 300 |
+
container.classList.remove('hovering');
|
| 301 |
+
gBars.selectAll('path.bar').classed('ghost', false).classed('highlight', false);
|
| 302 |
+
legendItems.querySelectorAll('.item').forEach(it => { it.classList.remove('hovered'); it.classList.remove('ghost'); });
|
| 303 |
})
|
|
|
|
| 304 |
.merge(bars)
|
| 305 |
.transition().duration(200)
|
| 306 |
.attr('d', (d)=> roundedPath(0, y(d.y1), bandWidth, Math.max(0.5, y(d.y0) - y(d.y1)), d.isTop, d.isBottom))
|
|
|
|
| 312 |
|
| 313 |
// Boot
|
| 314 |
update();
|
| 315 |
+
// controls already appended to footer; populate control groups
|
| 316 |
+
controls.appendChild(groupSize); controls.appendChild(groupRecomp);
|
| 317 |
selSize.addEventListener('change', (e)=>{ currentSize = e.target.value; update(); });
|
| 318 |
selRecomp.addEventListener('change', (e)=>{ currentMode = e.target.value; update(); });
|
| 319 |
|
app/src/content/embeds/d3-benchmark.html
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-benchmark"></div>
|
| 2 |
+
<style>
|
| 3 |
+
.d3-benchmark {
|
| 4 |
+
position: relative;
|
| 5 |
+
--axis-color: rgba(0,0,0,0.25);
|
| 6 |
+
--tick-color: rgba(0,0,0,0.55);
|
| 7 |
+
--grid-color: rgba(0,0,0,0.05);
|
| 8 |
+
}
|
| 9 |
+
[data-theme="dark"] .d3-benchmark {
|
| 10 |
+
--axis-color: rgba(255,255,255,0.25);
|
| 11 |
+
--tick-color: rgba(255,255,255,0.70);
|
| 12 |
+
--grid-color: rgba(255,255,255,0.08);
|
| 13 |
+
}
|
| 14 |
+
.d3-benchmark .controls {
|
| 15 |
+
display: flex;
|
| 16 |
+
align-items: center;
|
| 17 |
+
gap: 12px;
|
| 18 |
+
margin-bottom: 10px;
|
| 19 |
+
}
|
| 20 |
+
.d3-benchmark .controls label {
|
| 21 |
+
font-size: 12px;
|
| 22 |
+
color: var(--muted-color);
|
| 23 |
+
}
|
| 24 |
+
.d3-benchmark .controls select {
|
| 25 |
+
appearance: none;
|
| 26 |
+
-webkit-appearance: none;
|
| 27 |
+
-moz-appearance: none;
|
| 28 |
+
border: 1px solid var(--border-color);
|
| 29 |
+
border-radius: 8px;
|
| 30 |
+
padding: 6px 28px 6px 10px;
|
| 31 |
+
background-color: var(--surface-bg);
|
| 32 |
+
color: var(--text-color);
|
| 33 |
+
font-size: 13px;
|
| 34 |
+
line-height: 1.2;
|
| 35 |
+
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.41 1.59L6 6.17l4.59-4.58L12 3 6 9 0 3z' fill='%23999'/%3E%3C/svg%3E");
|
| 36 |
+
background-repeat: no-repeat;
|
| 37 |
+
background-position: right 8px center;
|
| 38 |
+
}
|
| 39 |
+
.d3-benchmark .controls select:focus-visible {
|
| 40 |
+
outline: 2px solid var(--primary-color);
|
| 41 |
+
outline-offset: 2px;
|
| 42 |
+
}
|
| 43 |
+
.d3-benchmark .legend {
|
| 44 |
+
display: flex;
|
| 45 |
+
flex-direction: column;
|
| 46 |
+
align-items: flex-start;
|
| 47 |
+
gap: 6px;
|
| 48 |
+
margin: 8px 0 0 0;
|
| 49 |
+
}
|
| 50 |
+
.d3-benchmark .legend .legend-title {
|
| 51 |
+
font-size: 12px;
|
| 52 |
+
font-weight: 700;
|
| 53 |
+
color: var(--text-color);
|
| 54 |
+
}
|
| 55 |
+
.d3-benchmark .legend .items {
|
| 56 |
+
display: flex;
|
| 57 |
+
flex-wrap: wrap;
|
| 58 |
+
gap: 8px 14px;
|
| 59 |
+
}
|
| 60 |
+
.d3-benchmark .legend .item {
|
| 61 |
+
display: inline-flex;
|
| 62 |
+
align-items: center;
|
| 63 |
+
gap: 8px;
|
| 64 |
+
font-size: 12px;
|
| 65 |
+
color: var(--muted-color);
|
| 66 |
+
cursor: pointer;
|
| 67 |
+
}
|
| 68 |
+
.d3-benchmark .legend .swatch {
|
| 69 |
+
width: 14px;
|
| 70 |
+
height: 14px;
|
| 71 |
+
border-radius: 3px;
|
| 72 |
+
border: 1px solid var(--border-color);
|
| 73 |
+
}
|
| 74 |
+
.d3-benchmark .ghost { opacity: .25; }
|
| 75 |
+
.d3-benchmark .d3-tooltip {
|
| 76 |
+
position: absolute;
|
| 77 |
+
top: 0px;
|
| 78 |
+
left: 0px;
|
| 79 |
+
transform: translate(-9999px, -9999px);
|
| 80 |
+
pointer-events: none;
|
| 81 |
+
padding: 8px 10px;
|
| 82 |
+
border-radius: 8px;
|
| 83 |
+
font-size: 12px;
|
| 84 |
+
line-height: 1.35;
|
| 85 |
+
border: 1px solid var(--border-color);
|
| 86 |
+
background: var(--surface-bg);
|
| 87 |
+
color: var(--text-color);
|
| 88 |
+
box-shadow: 0 4px 24px rgba(0,0,0,.18);
|
| 89 |
+
opacity: 0;
|
| 90 |
+
transition: opacity .12s ease;
|
| 91 |
+
text-align: left;
|
| 92 |
+
}
|
| 93 |
+
.d3-benchmark .chart-card {
|
| 94 |
+
background: var(--surface-bg);
|
| 95 |
+
border: 1px solid var(--border-color);
|
| 96 |
+
border-radius: 10px;
|
| 97 |
+
padding: 8px;
|
| 98 |
+
}
|
| 99 |
+
</style>
|
| 100 |
+
<script>
|
| 101 |
+
(() => {
|
| 102 |
+
const ensureD3 = (cb) => {
|
| 103 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 104 |
+
let s = document.getElementById('d3-cdn-script');
|
| 105 |
+
if (!s) {
|
| 106 |
+
s = document.createElement('script');
|
| 107 |
+
s.id = 'd3-cdn-script';
|
| 108 |
+
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 109 |
+
document.head.appendChild(s);
|
| 110 |
+
}
|
| 111 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 112 |
+
s.addEventListener('load', onReady, { once: true });
|
| 113 |
+
if (window.d3) onReady();
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
const bootstrap = () => {
|
| 117 |
+
const scriptEl = document.currentScript;
|
| 118 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 119 |
+
if (!(container && container.classList && container.classList.contains('d3-benchmark'))){
|
| 120 |
+
const cs = Array.from(document.querySelectorAll('.d3-benchmark')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
|
| 121 |
+
container = cs[cs.length-1] || null;
|
| 122 |
+
}
|
| 123 |
+
if (!container) return;
|
| 124 |
+
if (container.dataset) { if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
|
| 125 |
+
|
| 126 |
+
container.style.position = container.style.position || 'relative';
|
| 127 |
+
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 128 |
+
if (!tip) {
|
| 129 |
+
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
| 130 |
+
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tip.appendChild(tipInner);
|
| 131 |
+
container.appendChild(tip);
|
| 132 |
+
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 133 |
+
|
| 134 |
+
// header below chart
|
| 135 |
+
const header = document.createElement('div'); header.className = 'chart-header';
|
| 136 |
+
|
| 137 |
+
const makeLegend = (series, colorBySeries) => {
|
| 138 |
+
let legend = header.querySelector('.legend');
|
| 139 |
+
if (!legend) { legend = document.createElement('div'); legend.className = 'legend'; header.appendChild(legend); }
|
| 140 |
+
// Ensure title
|
| 141 |
+
let title = legend.querySelector('.legend-title');
|
| 142 |
+
if (!title) { title = document.createElement('div'); title.className = 'legend-title'; title.textContent = 'Legend'; legend.appendChild(title); }
|
| 143 |
+
// Ensure items container
|
| 144 |
+
let items = legend.querySelector('.items');
|
| 145 |
+
if (!items) { items = document.createElement('div'); items.className = 'items'; legend.appendChild(items); }
|
| 146 |
+
items.innerHTML = '';
|
| 147 |
+
series.forEach(name => {
|
| 148 |
+
const item = document.createElement('div'); item.className = 'item';
|
| 149 |
+
const sw = document.createElement('span'); sw.className = 'swatch'; sw.style.background = colorBySeries(name);
|
| 150 |
+
const txt = document.createElement('span'); txt.textContent = name;
|
| 151 |
+
item.appendChild(sw); item.appendChild(txt); items.appendChild(item);
|
| 152 |
+
item.addEventListener('mouseenter', () => { state.highlightModel = name; updateHighlight(); });
|
| 153 |
+
item.addEventListener('mouseleave', () => { state.highlightModel = null; updateHighlight(); });
|
| 154 |
+
});
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
// SVG scaffolding inside a card wrapper, then header appended after
|
| 158 |
+
const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
|
| 159 |
+
container.appendChild(header);
|
| 160 |
+
const svg = d3.select(card).append('svg').attr('width','100%').style('display','block');
|
| 161 |
+
const gRoot = svg.append('g');
|
| 162 |
+
|
| 163 |
+
// No controls (fixed scale mode)
|
| 164 |
+
|
| 165 |
+
// Public-first data loading with inline fallback
|
| 166 |
+
const fetchFirstAvailable = async (paths) => {
|
| 167 |
+
for (const p of paths) {
|
| 168 |
+
try {
|
| 169 |
+
const res = await fetch(p, { cache:'no-cache' });
|
| 170 |
+
if (!res.ok) throw new Error('HTTP '+res.status);
|
| 171 |
+
const text = await res.text();
|
| 172 |
+
// Try JSON first; if CSV, parse with d3.csvParse
|
| 173 |
+
try { return JSON.parse(text); } catch(e) {}
|
| 174 |
+
if (window.d3 && d3.csvParse) { return d3.csvParse(text); }
|
| 175 |
+
} catch (e) { /* keep trying */ }
|
| 176 |
+
}
|
| 177 |
+
return null;
|
| 178 |
+
};
|
| 179 |
+
|
| 180 |
+
// Inline fallback dataset (scores in % where applicable)
|
| 181 |
+
const inlineData = [
|
| 182 |
+
{ benchmark:'MMLU', model:'GPT-4o', score: 88 },
|
| 183 |
+
{ benchmark:'MMLU', model:'Llama 3 70B', score: 80 },
|
| 184 |
+
{ benchmark:'MMLU', model:'Mixtral 8x7B',score: 73 },
|
| 185 |
+
{ benchmark:'MMLU', model:'Gemma 2 27B', score: 76 },
|
| 186 |
+
{ benchmark:'GSM8K', model:'GPT-4o', score: 94 },
|
| 187 |
+
{ benchmark:'GSM8K', model:'Llama 3 70B', score: 83 },
|
| 188 |
+
{ benchmark:'GSM8K', model:'Mixtral 8x7B',score: 79 },
|
| 189 |
+
{ benchmark:'GSM8K', model:'Gemma 2 27B', score: 81 },
|
| 190 |
+
{ benchmark:'HellaSwag', model:'GPT-4o', score: 95 },
|
| 191 |
+
{ benchmark:'HellaSwag', model:'Llama 3 70B', score: 89 },
|
| 192 |
+
{ benchmark:'HellaSwag', model:'Mixtral 8x7B',score: 86 },
|
| 193 |
+
{ benchmark:'HellaSwag', model:'Gemma 2 27B', score: 87 },
|
| 194 |
+
{ benchmark:'TruthfulQA', model:'GPT-4o', score: 64 },
|
| 195 |
+
{ benchmark:'TruthfulQA', model:'Llama 3 70B', score: 56 },
|
| 196 |
+
{ benchmark:'TruthfulQA', model:'Mixtral 8x7B',score: 51 },
|
| 197 |
+
{ benchmark:'TruthfulQA', model:'Gemma 2 27B', score: 53 },
|
| 198 |
+
{ benchmark:'ARC-C', model:'GPT-4o', score: 79 },
|
| 199 |
+
{ benchmark:'ARC-C', model:'Llama 3 70B', score: 72 },
|
| 200 |
+
{ benchmark:'ARC-C', model:'Mixtral 8x7B',score: 68 },
|
| 201 |
+
{ benchmark:'ARC-C', model:'Gemma 2 27B', score: 70 }
|
| 202 |
+
];
|
| 203 |
+
|
| 204 |
+
const state = {
|
| 205 |
+
data: inlineData,
|
| 206 |
+
colorsByModel: null,
|
| 207 |
+
highlightModel: null,
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
const margin = { top: 12, right: 28, bottom: 24, left: 56 };
|
| 211 |
+
let width = 800, height = 360;
|
| 212 |
+
const x0 = d3.scaleBand().paddingInner(0.2).paddingOuter(0.05); // group: benchmark
|
| 213 |
+
const x1 = d3.scaleBand().padding(0.12); // series: model per benchmark
|
| 214 |
+
const y = d3.scaleLinear();
|
| 215 |
+
const xAxis = d3.axisBottom(x0).tickSizeOuter(0);
|
| 216 |
+
const yAxis = d3.axisLeft(y).ticks(6).tickSizeOuter(0);
|
| 217 |
+
const yTopPadding = 2; // avoid bars touching top at max
|
| 218 |
+
|
| 219 |
+
function getPrimaryColor(){
|
| 220 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getPrimary === 'function') return window.ColorPalettes.getPrimary(); } catch(e) {}
|
| 221 |
+
return getComputedStyle(document.documentElement).getPropertyValue('--primary-color') || '#6D4AFF';
|
| 222 |
+
}
|
| 223 |
+
function getCategoricalColors(n){
|
| 224 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(e) {}
|
| 225 |
+
// Fallback: generate hues around the primary color (simple fallback)
|
| 226 |
+
const base = getPrimaryColor();
|
| 227 |
+
const colors = [];
|
| 228 |
+
for (let i=0;i<n;i++) {
|
| 229 |
+
const hue = Math.round((360/n)*i);
|
| 230 |
+
colors.push(`hsl(${hue}, 60%, 55%)`);
|
| 231 |
+
}
|
| 232 |
+
return colors;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
function computeSeriesColors(models){
|
| 236 |
+
const palette = getCategoricalColors(models.length);
|
| 237 |
+
const map = new Map(models.map((m, i) => [m, palette[i % palette.length]]));
|
| 238 |
+
return (model) => map.get(model) || getPrimaryColor();
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
function getModels(data){
|
| 242 |
+
return Array.from(new Set(data.map(d => d.model)));
|
| 243 |
+
}
|
| 244 |
+
function getBenchmarks(data){
|
| 245 |
+
return Array.from(new Set(data.map(d => d.benchmark)));
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function updateSize(){
|
| 249 |
+
width = container.clientWidth || 800;
|
| 250 |
+
height = Math.max(240, Math.round(width / 3.4));
|
| 251 |
+
svg.attr('width', width).attr('height', height);
|
| 252 |
+
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 253 |
+
return { innerWidth: width - margin.left - margin.right, innerHeight: height - margin.top - margin.bottom };
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function showTip(html, x, y){
|
| 257 |
+
tip.style.transform = `translate(${x + 12}px, ${y + 12}px)`;
|
| 258 |
+
tip.style.opacity = '1';
|
| 259 |
+
const inner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 260 |
+
inner.innerHTML = html;
|
| 261 |
+
}
|
| 262 |
+
function hideTip(){
|
| 263 |
+
tip.style.opacity = '0';
|
| 264 |
+
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
function updateHighlight(){
|
| 268 |
+
const model = state.highlightModel;
|
| 269 |
+
const bars = gRoot.selectAll('rect.bar');
|
| 270 |
+
const labels = gRoot.selectAll('text.value');
|
| 271 |
+
if (model) {
|
| 272 |
+
bars.classed('ghost', d => d.model !== model);
|
| 273 |
+
labels.classed('ghost', d => d.model !== model);
|
| 274 |
+
const items = container.querySelectorAll('.legend .item');
|
| 275 |
+
items.forEach((el) => {
|
| 276 |
+
const name = el.textContent.trim();
|
| 277 |
+
if (name !== model) el.classList.add('ghost'); else el.classList.remove('ghost');
|
| 278 |
+
});
|
| 279 |
+
} else {
|
| 280 |
+
bars.classed('ghost', false);
|
| 281 |
+
labels.classed('ghost', false);
|
| 282 |
+
container.querySelectorAll('.legend .item').forEach(el => el.classList.remove('ghost'));
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
function render(){
|
| 287 |
+
const { innerWidth, innerHeight } = updateSize();
|
| 288 |
+
const models = getModels(state.data);
|
| 289 |
+
if (!state.colorsByModel) state.colorsByModel = computeSeriesColors(models);
|
| 290 |
+
makeLegend(models, state.colorsByModel);
|
| 291 |
+
|
| 292 |
+
x0.domain(getBenchmarks(state.data)).range([0, innerWidth]);
|
| 293 |
+
x1.domain(models).range([0, x0.bandwidth()]);
|
| 294 |
+
|
| 295 |
+
const yMaxRaw = 100;
|
| 296 |
+
const yMax = yMaxRaw + yTopPadding;
|
| 297 |
+
y.domain([0, yMax]).range([innerHeight, 0]).nice();
|
| 298 |
+
|
| 299 |
+
// Axes (standardized colors)
|
| 300 |
+
gRoot
|
| 301 |
+
.selectAll('.axis-x')
|
| 302 |
+
.data([0])
|
| 303 |
+
.join('g')
|
| 304 |
+
.attr('class','axis-x')
|
| 305 |
+
.attr('transform',`translate(0,${innerHeight})`)
|
| 306 |
+
.call(xAxis)
|
| 307 |
+
.call(g => {
|
| 308 |
+
g.selectAll('path, line').attr('stroke', 'var(--axis-color)');
|
| 309 |
+
g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px');
|
| 310 |
+
});
|
| 311 |
+
gRoot
|
| 312 |
+
.selectAll('.axis-y')
|
| 313 |
+
.data([0])
|
| 314 |
+
.join('g')
|
| 315 |
+
.attr('class','axis-y')
|
| 316 |
+
.call(yAxis)
|
| 317 |
+
.call(g => {
|
| 318 |
+
g.selectAll('path, line').attr('stroke', 'var(--axis-color)');
|
| 319 |
+
g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px');
|
| 320 |
+
});
|
| 321 |
+
|
| 322 |
+
// Gridlines (y) standardized color
|
| 323 |
+
gRoot
|
| 324 |
+
.selectAll('.grid-y')
|
| 325 |
+
.data([0])
|
| 326 |
+
.join('g')
|
| 327 |
+
.attr('class','grid-y')
|
| 328 |
+
.call(d3.axisLeft(y).ticks(6).tickSize(-innerWidth).tickFormat(''))
|
| 329 |
+
.call(g => g.select('.domain').remove())
|
| 330 |
+
.call(g => g.selectAll('.tick line').attr('stroke','var(--grid-color)').attr('stroke-opacity',1))
|
| 331 |
+
.call(g => g.selectAll('.tick').filter((d, i, nodes) => i === nodes.length - 1).select('line').attr('stroke-opacity', 0));
|
| 332 |
+
|
| 333 |
+
// Groups per benchmark
|
| 334 |
+
const groups = gRoot.selectAll('.group').data(getBenchmarks(state.data), d => d);
|
| 335 |
+
const groupsEnter = groups.enter().append('g').attr('class','group');
|
| 336 |
+
groupsEnter.merge(groups).attr('transform', d => `translate(${x0(d)},0)`);
|
| 337 |
+
groups.exit().remove();
|
| 338 |
+
|
| 339 |
+
// Bars per model
|
| 340 |
+
const nested = d3.group(state.data, d => d.benchmark);
|
| 341 |
+
groupsEnter.each(function(bench){ d3.select(this).selectAll('rect.bar').data([]).join('rect'); });
|
| 342 |
+
const allGroups = gRoot.selectAll('.group');
|
| 343 |
+
allGroups.each(function(bench){
|
| 344 |
+
const dataForBench = nested.get(bench) || [];
|
| 345 |
+
const bars = d3.select(this).selectAll('rect.bar').data(models.map(m => ({ bench, model:m, score:(dataForBench.find(dd=>dd.model===m)||{score:0}).score })) , d => d.model);
|
| 346 |
+
bars.join(
|
| 347 |
+
enter => enter.append('rect')
|
| 348 |
+
.attr('class','bar')
|
| 349 |
+
.attr('x', d => x1(d.model))
|
| 350 |
+
.attr('y', innerHeight)
|
| 351 |
+
.attr('width', x1.bandwidth())
|
| 352 |
+
.attr('height', 0)
|
| 353 |
+
.attr('fill', d => state.colorsByModel(d.model))
|
| 354 |
+
.on('mouseenter', (event, d) => { state.highlightModel = d.model; updateHighlight(); })
|
| 355 |
+
.on('mousemove', (event, d) => {
|
| 356 |
+
const [mx, my] = d3.pointer(event, container);
|
| 357 |
+
showTip(`<strong>${d.model}</strong><br/>${d.bench}: <strong>${d.score}</strong>`, mx, my);
|
| 358 |
+
})
|
| 359 |
+
.on('mouseleave', () => { hideTip(); state.highlightModel = null; updateHighlight(); })
|
| 360 |
+
.transition().duration(160)
|
| 361 |
+
.attr('y', d => y(d.score))
|
| 362 |
+
.attr('height', d => Math.max(0, innerHeight - y(d.score))),
|
| 363 |
+
update => update
|
| 364 |
+
.on('mouseenter', (event, d) => { state.highlightModel = d.model; updateHighlight(); })
|
| 365 |
+
.on('mousemove', (event, d) => {
|
| 366 |
+
const [mx, my] = d3.pointer(event, container);
|
| 367 |
+
showTip(`<strong>${d.model}</strong><br/>${d.bench}: <strong>${d.score}</strong>`, mx, my);
|
| 368 |
+
})
|
| 369 |
+
.on('mouseleave', () => { hideTip(); state.highlightModel = null; updateHighlight(); })
|
| 370 |
+
.transition().duration(160)
|
| 371 |
+
.attr('x', d => x1(d.model))
|
| 372 |
+
.attr('y', d => y(d.score))
|
| 373 |
+
.attr('width', x1.bandwidth())
|
| 374 |
+
.attr('height', d => Math.max(0, innerHeight - y(d.score)))
|
| 375 |
+
.attr('fill', d => state.colorsByModel(d.model)),
|
| 376 |
+
exit => exit.transition().duration(120).attr('y', innerHeight).attr('height', 0).remove()
|
| 377 |
+
);
|
| 378 |
+
|
| 379 |
+
// Value labels centered above bars (small, darker)
|
| 380 |
+
const labels = d3.select(this).selectAll('text.value').data(models.map(m => ({ bench, model:m, score:(dataForBench.find(dd=>dd.model===m)||{score:0}).score })) , d => d.model);
|
| 381 |
+
labels.join(
|
| 382 |
+
enter => enter.append('text')
|
| 383 |
+
.attr('class','value')
|
| 384 |
+
.attr('x', d => x1(d.model) + x1.bandwidth()/2)
|
| 385 |
+
.attr('y', d => y(d.score) - 4)
|
| 386 |
+
.attr('text-anchor','middle')
|
| 387 |
+
.attr('fill','var(--text-color)')
|
| 388 |
+
.attr('opacity',0.9)
|
| 389 |
+
.attr('font-size',10)
|
| 390 |
+
.text(d => d.score),
|
| 391 |
+
update => update
|
| 392 |
+
.transition().duration(160)
|
| 393 |
+
.attr('x', d => x1(d.model) + x1.bandwidth()/2)
|
| 394 |
+
.attr('y', d => y(d.score) - 4)
|
| 395 |
+
.text(d => d.score),
|
| 396 |
+
exit => exit.remove()
|
| 397 |
+
);
|
| 398 |
+
});
|
| 399 |
+
|
| 400 |
+
// Axis labels
|
| 401 |
+
gRoot.selectAll('.y-label').data([0]).join('text').attr('class','y-label')
|
| 402 |
+
.attr('transform', `rotate(-90)`)
|
| 403 |
+
.attr('x', -innerHeight / 2)
|
| 404 |
+
.attr('y', -margin.left + 24)
|
| 405 |
+
.attr('text-anchor','middle')
|
| 406 |
+
.attr('fill','var(--text-color)')
|
| 407 |
+
.attr('font-size',12)
|
| 408 |
+
.attr('font-weight',700)
|
| 409 |
+
.text('score');
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
// Initial render + resize handling
|
| 413 |
+
render();
|
| 414 |
+
const rerender = () => render();
|
| 415 |
+
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); }
|
| 416 |
+
else { window.addEventListener('resize', rerender); }
|
| 417 |
+
|
| 418 |
+
// Attempt to load external data (public-first). Expect either JSON array with {benchmark, model, score}
|
| 419 |
+
(async () => {
|
| 420 |
+
const maybe = await fetchFirstAvailable([
|
| 421 |
+
'/data/llm_benchmarks.json',
|
| 422 |
+
'./assets/data/llm_benchmarks.json',
|
| 423 |
+
'../assets/data/llm_benchmarks.json'
|
| 424 |
+
]);
|
| 425 |
+
if (Array.isArray(maybe) && maybe.length && maybe[0].benchmark && maybe[0].model && (typeof maybe[0].score === 'number')) {
|
| 426 |
+
state.data = maybe;
|
| 427 |
+
state.colorsByModel = null; // recompute in case of different model set
|
| 428 |
+
render();
|
| 429 |
+
} else if (maybe && maybe.columns) {
|
| 430 |
+
// CSV parsed via d3.csvParse -> convert fields
|
| 431 |
+
const parsed = maybe.map(r => ({ benchmark: r.benchmark, model: r.model, score: +r.score }));
|
| 432 |
+
if (parsed.length) { state.data = parsed; state.colorsByModel = null; render(); }
|
| 433 |
+
}
|
| 434 |
+
})().catch(() => {
|
| 435 |
+
// Graceful failure: inline fallback already rendered
|
| 436 |
+
});
|
| 437 |
+
};
|
| 438 |
+
|
| 439 |
+
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); }
|
| 440 |
+
else { ensureD3(bootstrap); }
|
| 441 |
+
})();
|
| 442 |
+
</script>
|
| 443 |
+
|
| 444 |
+
|
app/src/content/embeds/d3-boxplot.html
DELETED
|
@@ -1,251 +0,0 @@
|
|
| 1 |
-
<div class="d3-boxplot" style="width:100%;margin:10px 0;"></div>
|
| 2 |
-
<style>
|
| 3 |
-
.d3-boxplot .controls {
|
| 4 |
-
margin-top: 12px;
|
| 5 |
-
display: flex;
|
| 6 |
-
gap: 16px;
|
| 7 |
-
align-items: center;
|
| 8 |
-
flex-wrap: wrap;
|
| 9 |
-
}
|
| 10 |
-
.d3-boxplot .controls label {
|
| 11 |
-
font-size: 12px;
|
| 12 |
-
color: var(--muted-color);
|
| 13 |
-
display: flex;
|
| 14 |
-
align-items: center;
|
| 15 |
-
gap: 8px;
|
| 16 |
-
white-space: nowrap;
|
| 17 |
-
padding: 6px 10px;
|
| 18 |
-
}
|
| 19 |
-
.d3-boxplot .controls select {
|
| 20 |
-
font-size: 12px;
|
| 21 |
-
padding: 8px 28px 8px 10px;
|
| 22 |
-
border: 1px solid var(--border-color);
|
| 23 |
-
border-radius: 8px;
|
| 24 |
-
background-color: var(--surface-bg);
|
| 25 |
-
color: var(--text-color);
|
| 26 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 27 |
-
background-repeat: no-repeat;
|
| 28 |
-
background-position: right 8px center;
|
| 29 |
-
background-size: 12px;
|
| 30 |
-
-webkit-appearance: none;
|
| 31 |
-
-moz-appearance: none;
|
| 32 |
-
appearance: none;
|
| 33 |
-
cursor: pointer;
|
| 34 |
-
transition: border-color .15s ease, box-shadow .15s ease;
|
| 35 |
-
}
|
| 36 |
-
[data-theme="dark"] .d3-boxplot .controls select {
|
| 37 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 38 |
-
}
|
| 39 |
-
.d3-boxplot .controls select:hover { border-color: var(--primary-color); }
|
| 40 |
-
.d3-boxplot .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 41 |
-
.d3-boxplot .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
|
| 42 |
-
</style>
|
| 43 |
-
<script>
|
| 44 |
-
(() => {
|
| 45 |
-
const ensureD3 = (cb) => {
|
| 46 |
-
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 47 |
-
let s = document.getElementById('d3-cdn-script');
|
| 48 |
-
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
| 49 |
-
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 50 |
-
s.addEventListener('load', onReady, { once: true });
|
| 51 |
-
if (window.d3) onReady();
|
| 52 |
-
};
|
| 53 |
-
|
| 54 |
-
const bootstrap = () => {
|
| 55 |
-
const scriptEl = document.currentScript;
|
| 56 |
-
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 57 |
-
if (!(container && container.classList && container.classList.contains('d3-boxplot'))){
|
| 58 |
-
const cs = Array.from(document.querySelectorAll('.d3-boxplot')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
|
| 59 |
-
container = cs[cs.length-1] || null;
|
| 60 |
-
}
|
| 61 |
-
if (!container) return;
|
| 62 |
-
if (container.dataset){ if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
|
| 63 |
-
|
| 64 |
-
// Tooltip
|
| 65 |
-
container.style.position = container.style.position || 'relative';
|
| 66 |
-
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 67 |
-
if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style,{ position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 68 |
-
|
| 69 |
-
// Controls
|
| 70 |
-
const controls = document.createElement('div'); controls.className='controls';
|
| 71 |
-
const labelMetric = document.createElement('label'); labelMetric.textContent='Metric';
|
| 72 |
-
const selMetric = document.createElement('select');
|
| 73 |
-
labelMetric.appendChild(selMetric);
|
| 74 |
-
|
| 75 |
-
// SVG
|
| 76 |
-
const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
|
| 77 |
-
const gRoot = svg.append('g');
|
| 78 |
-
const gGrid = gRoot.append('g').attr('class','grid');
|
| 79 |
-
const gAxes = gRoot.append('g').attr('class','axes');
|
| 80 |
-
const gBoxes = gRoot.append('g').attr('class','boxes');
|
| 81 |
-
|
| 82 |
-
// State & scales
|
| 83 |
-
let width=800,height=360; const margin={top:16,right:28,bottom:56,left:64};
|
| 84 |
-
const x=d3.scaleBand().padding(0.4);
|
| 85 |
-
const y=d3.scaleLinear();
|
| 86 |
-
const color=d3.scaleOrdinal().range(['var(--primary-color)','rgb(78, 165, 183)','rgb(227, 138, 66)','rgb(206, 192, 250)']);
|
| 87 |
-
|
| 88 |
-
// Data (real): distribution of a chosen metric across runs
|
| 89 |
-
let allRows = [];
|
| 90 |
-
let groups = [];
|
| 91 |
-
let currentMetric = null;
|
| 92 |
-
let stats = [];
|
| 93 |
-
|
| 94 |
-
async function fetchFirstAvailable(paths){
|
| 95 |
-
for (const p of paths){
|
| 96 |
-
try { const res = await fetch(p, { cache:'no-cache' }); if (res.ok) return await res.text(); } catch(e){}
|
| 97 |
-
}
|
| 98 |
-
throw new Error('Failed to load boxplot data');
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
function computeStatsForMetric(metric){
|
| 102 |
-
const byRun = d3.rollup(allRows.filter(r=>r.metric===metric), v=> v.map(r=>+r.value).filter(Number.isFinite), r=>r.run);
|
| 103 |
-
groups = Array.from(byRun.keys());
|
| 104 |
-
const result = groups.map((g)=>{
|
| 105 |
-
const data = (byRun.get(g) || []).slice().sort((a,b)=>a-b);
|
| 106 |
-
if (!data.length) return { key:g, q1:NaN, med:NaN, q3:NaN, min:NaN, max:NaN, outliers:[] };
|
| 107 |
-
const q1 = d3.quantile(data, 0.25);
|
| 108 |
-
const med = d3.quantile(data, 0.5);
|
| 109 |
-
const q3 = d3.quantile(data, 0.75);
|
| 110 |
-
const iqr = q3 - q1;
|
| 111 |
-
const lo = q1 - 1.5*iqr;
|
| 112 |
-
const hi = q3 + 1.5*iqr;
|
| 113 |
-
const min = d3.min(data.filter(v=>v>=lo));
|
| 114 |
-
const max = d3.max(data.filter(v=>v<=hi));
|
| 115 |
-
const outliers = data.filter(v=>v<lo || v>hi);
|
| 116 |
-
return { key:g, q1, med, q3, iqr, min, max, outliers };
|
| 117 |
-
});
|
| 118 |
-
return result;
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
function updateScales(){
|
| 122 |
-
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 123 |
-
const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
|
| 124 |
-
const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
|
| 125 |
-
const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
|
| 126 |
-
|
| 127 |
-
width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
|
| 128 |
-
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 129 |
-
|
| 130 |
-
x.domain(groups).range([0, innerWidth]);
|
| 131 |
-
const yMin = Math.floor(d3.min(stats, d=>d.min ?? 0))-0; const yMax = Math.ceil(d3.max(stats, d=>d.max ?? 1))+0;
|
| 132 |
-
y.domain([yMin, yMax]).range([innerHeight,0]).nice();
|
| 133 |
-
|
| 134 |
-
// Grid
|
| 135 |
-
gGrid.selectAll('*').remove();
|
| 136 |
-
gGrid.selectAll('line.grid-y').data(y.ticks(6)).join('line')
|
| 137 |
-
.attr('class','grid-y').attr('x1',0).attr('x2',innerWidth).attr('y1',d=>y(d)).attr('y2',d=>y(d))
|
| 138 |
-
.attr('stroke', gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
|
| 139 |
-
|
| 140 |
-
// Axes
|
| 141 |
-
gAxes.selectAll('*').remove();
|
| 142 |
-
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
|
| 143 |
-
gAxes.append('g').call(d3.axisLeft(y).ticks(6)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
|
| 144 |
-
gAxes.append('text').attr('class','axis-label axis-label--x').attr('x', innerWidth/2).attr('y', innerHeight + 44).attr('text-anchor','middle').style('font-size','12px').style('fill', tickColor).text('Group');
|
| 145 |
-
gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-48},${innerHeight/2}) rotate(-90)`).style('font-size','12px').style('fill', tickColor).text('Value');
|
| 146 |
-
|
| 147 |
-
return { innerWidth, innerHeight };
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
function draw(){
|
| 151 |
-
const { innerWidth, innerHeight } = updateScales();
|
| 152 |
-
const bw = Math.max(10, x.bandwidth());
|
| 153 |
-
|
| 154 |
-
// Boxes
|
| 155 |
-
const groupsSel = gBoxes.selectAll('g.bp').data(stats, d=>d.key);
|
| 156 |
-
const gEnter = groupsSel.enter().append('g').attr('class','bp').attr('transform', d=>`translate(${x(d.key)},0)`);
|
| 157 |
-
const merged = gEnter.merge(groupsSel).attr('transform', d=>`translate(${x(d.key)},0)`);
|
| 158 |
-
groupsSel.exit().remove();
|
| 159 |
-
|
| 160 |
-
// Box rect (Q1-Q3)
|
| 161 |
-
const box = merged.selectAll('rect.box').data(d=>[d]);
|
| 162 |
-
box.enter().append('rect').attr('class','box')
|
| 163 |
-
.attr('x', 0).attr('width', bw)
|
| 164 |
-
.attr('y', d=>y(d.q3)).attr('height', d=>Math.max(0.5, y(d.q1) - y(d.q3)))
|
| 165 |
-
.attr('fill','var(--surface-bg)')
|
| 166 |
-
.attr('stroke', 'var(--primary-color)')
|
| 167 |
-
.attr('rx', 4).attr('ry', 4)
|
| 168 |
-
.merge(box)
|
| 169 |
-
.transition().duration(180)
|
| 170 |
-
.attr('y', d=>y(d.q3)).attr('height', d=>Math.max(0.5, y(d.q1) - y(d.q3)));
|
| 171 |
-
box.exit().remove();
|
| 172 |
-
|
| 173 |
-
// Median line
|
| 174 |
-
const med = merged.selectAll('line.med').data(d=>[d]);
|
| 175 |
-
med.enter().append('line').attr('class','med')
|
| 176 |
-
.attr('x1', 0).attr('x2', bw)
|
| 177 |
-
.attr('y1', d=>y(d.med)).attr('y2', d=>y(d.med))
|
| 178 |
-
.attr('stroke','var(--primary-color)').attr('stroke-width',2)
|
| 179 |
-
.merge(med)
|
| 180 |
-
.transition().duration(180)
|
| 181 |
-
.attr('y1', d=>y(d.med)).attr('y2', d=>y(d.med));
|
| 182 |
-
med.exit().remove();
|
| 183 |
-
|
| 184 |
-
// Whiskers
|
| 185 |
-
const whisker = merged.selectAll('line.whisk').data(d=>[d]);
|
| 186 |
-
whisker.enter().append('line').attr('class','whisk')
|
| 187 |
-
.attr('x1', bw/2).attr('x2', bw/2)
|
| 188 |
-
.attr('y1', d=>y(d.min)).attr('y2', d=>y(d.max))
|
| 189 |
-
.attr('stroke','var(--border-color)')
|
| 190 |
-
.merge(whisker)
|
| 191 |
-
.transition().duration(180)
|
| 192 |
-
.attr('y1', d=>y(d.min)).attr('y2', d=>y(d.max));
|
| 193 |
-
whisker.exit().remove();
|
| 194 |
-
|
| 195 |
-
// Whisker caps
|
| 196 |
-
const caps = merged.selectAll('g.caps').data(d=>[d]);
|
| 197 |
-
const capsEnter = caps.enter().append('g').attr('class','caps');
|
| 198 |
-
capsEnter.append('line').attr('class','cap cap--min').attr('x1',0).attr('x2',bw).attr('y1',d=>y(d.min)).attr('y2',d=>y(d.min)).attr('stroke','var(--border-color)');
|
| 199 |
-
capsEnter.append('line').attr('class','cap cap--max').attr('x1',0).attr('x2',bw).attr('y1',d=>y(d.max)).attr('y2',d=>y(d.max)).attr('stroke','var(--border-color)');
|
| 200 |
-
caps.merge(capsEnter).select('.cap--min').transition().duration(180).attr('y1',d=>y(d.min)).attr('y2',d=>y(d.min));
|
| 201 |
-
caps.merge(capsEnter).select('.cap--max').transition().duration(180).attr('y1',d=>y(d.max)).attr('y2',d=>y(d.max));
|
| 202 |
-
caps.exit().remove();
|
| 203 |
-
|
| 204 |
-
// Outliers
|
| 205 |
-
const outs = merged.selectAll('circle.out').data(d=>d.outliers.map(v=>({key:d.key, v})));
|
| 206 |
-
outs.enter().append('circle').attr('class','out')
|
| 207 |
-
.attr('cx', bw/2).attr('cy', d=>y(d.v)).attr('r', 2.5)
|
| 208 |
-
.attr('fill', 'var(--primary-color)')
|
| 209 |
-
.on('mouseenter', function(ev, d){ tipInner.innerHTML = `<div><strong>${d.key}</strong> outlier: ${d.v.toFixed(2)}</div>`; tip.style.opacity='1'; d3.select(this).attr('r',3.2); })
|
| 210 |
-
.on('mousemove', function(ev){ const [mx,my]=d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
|
| 211 |
-
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('r',2.5); })
|
| 212 |
-
.merge(outs)
|
| 213 |
-
.transition().duration(180)
|
| 214 |
-
.attr('cy', d=>y(d.v));
|
| 215 |
-
outs.exit().remove();
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
container.appendChild(controls);
|
| 219 |
-
controls.appendChild(labelMetric);
|
| 220 |
-
|
| 221 |
-
(async () => {
|
| 222 |
-
try {
|
| 223 |
-
const csvText = await fetchFirstAvailable([
|
| 224 |
-
'/data/all_ratings_luis.csv',
|
| 225 |
-
'/data/against_baselines.csv',
|
| 226 |
-
'/data/ss_vs_s1.csv',
|
| 227 |
-
'./assets/data/all_ratings_luis.csv',
|
| 228 |
-
'../assets/data/all_ratings_luis.csv'
|
| 229 |
-
]);
|
| 230 |
-
allRows = d3.csvParse(csvText, d=>({ run: d.run, step: +d.step, metric: d.metric, value: +d.value }));
|
| 231 |
-
const metrics = Array.from(new Set(allRows.map(r=>r.metric))).filter(Boolean).slice(0, 20);
|
| 232 |
-
metrics.forEach(m=>{ const o=document.createElement('option'); o.value=m; o.textContent=m; selMetric.appendChild(o); });
|
| 233 |
-
currentMetric = metrics.includes('textvqa_val_exact_match') ? 'textvqa_val_exact_match' : metrics[0];
|
| 234 |
-
selMetric.value = currentMetric;
|
| 235 |
-
selMetric.addEventListener('change', (e)=>{ currentMetric = e.target.value; stats = computeStatsForMetric(currentMetric); draw(); });
|
| 236 |
-
stats = computeStatsForMetric(currentMetric);
|
| 237 |
-
draw();
|
| 238 |
-
} catch (e) {
|
| 239 |
-
const pre = document.createElement('pre'); pre.style.color = 'crimson'; pre.textContent = 'Failed to load boxplot data.'; container.appendChild(pre);
|
| 240 |
-
}
|
| 241 |
-
})();
|
| 242 |
-
|
| 243 |
-
const rerender = () => { draw(); };
|
| 244 |
-
if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
|
| 245 |
-
};
|
| 246 |
-
|
| 247 |
-
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
|
| 248 |
-
})();
|
| 249 |
-
</script>
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/d3-comparison.html
DELETED
|
@@ -1,149 +0,0 @@
|
|
| 1 |
-
<div class="image-comparison" style="width:100%;margin:10px 0;"></div>
|
| 2 |
-
<style>
|
| 3 |
-
.image-comparison { position: relative; }
|
| 4 |
-
.image-comparison .controls { display:flex; align-items:center; gap:16px; justify-content:center; flex-wrap:wrap; margin:14px 0; }
|
| 5 |
-
.image-comparison .controls label { font-size:14px; color: var(--text-color); display:flex; align-items:center; justify-content:center; gap:10px; font-weight:600; }
|
| 6 |
-
.image-comparison .controls select {
|
| 7 |
-
font-size: 14px;
|
| 8 |
-
padding: 8px 32px 8px 12px;
|
| 9 |
-
border: 1px solid var(--border-color);
|
| 10 |
-
border-radius: 10px;
|
| 11 |
-
background-color: var(--surface-bg);
|
| 12 |
-
color: var(--text-color);
|
| 13 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 14 |
-
background-repeat: no-repeat;
|
| 15 |
-
background-position: right 8px center;
|
| 16 |
-
background-size: 12px;
|
| 17 |
-
-webkit-appearance: none; appearance: none; cursor: pointer;
|
| 18 |
-
transition: border-color .15s ease, box-shadow .15s ease;
|
| 19 |
-
box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
| 20 |
-
}
|
| 21 |
-
[data-theme="dark"] .image-comparison .controls select {
|
| 22 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 23 |
-
}
|
| 24 |
-
.image-comparison .controls select:hover { border-color: var(--primary-color); }
|
| 25 |
-
.image-comparison .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 26 |
-
|
| 27 |
-
/* Responsive: empiler label et select proprement sur petits écrans */
|
| 28 |
-
@media (max-width: 520px) {
|
| 29 |
-
.image-comparison .controls label { flex-direction: column; gap: 6px; }
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
.image-comparison .grid { display:grid; grid-template-columns: repeat(4, 1fr); gap: 12px; width:100%; align-items: start; }
|
| 33 |
-
/* Large → 4 colonnes; Medium → 2 colonnes; Mobile → 1 colonne */
|
| 34 |
-
@media (max-width: 1100px) { .image-comparison .grid { grid-template-columns: repeat(2, 1fr); } }
|
| 35 |
-
@media (max-width: 680px) { .image-comparison .grid { grid-template-columns: 1fr; } }
|
| 36 |
-
|
| 37 |
-
.image-comparison .card { position: relative; border:1px solid var(--border-color); border-radius:10px; overflow:hidden; background: var(--surface-bg); display:flex; flex-direction:column; }
|
| 38 |
-
.image-comparison .card .media { position: relative; width:100%; height: 200px; background: var(--surface-2, var(--surface-bg)); display:block; }
|
| 39 |
-
.image-comparison .card .media img { width:100%; height:100%; object-fit: contain; display:block; }
|
| 40 |
-
.image-comparison .badge { position:absolute; top:8px; left:8px; font-size:11px; padding:3px 6px; border-radius:6px; background: var(--surface-bg); color: var(--text-color); border:1px solid var(--border-color); }
|
| 41 |
-
.image-comparison .meta { padding:8px 10px; border-top:1px solid var(--border-color); font-size:12px; display:flex; height: 55px; align-items:start; justify-content:space-between; gap:8px; }
|
| 42 |
-
.image-comparison .meta .label { color: var(--muted-color); }
|
| 43 |
-
.image-comparison .meta .value { font-weight:600; }
|
| 44 |
-
</style>
|
| 45 |
-
<script>
|
| 46 |
-
(() => {
|
| 47 |
-
const THIS_SCRIPT = document.currentScript;
|
| 48 |
-
const bootstrap = () => {
|
| 49 |
-
const scriptEl = THIS_SCRIPT;
|
| 50 |
-
const host = scriptEl && scriptEl.parentElement;
|
| 51 |
-
let container = null;
|
| 52 |
-
if (host && host.querySelector) {
|
| 53 |
-
container = host.querySelector('.image-comparison');
|
| 54 |
-
}
|
| 55 |
-
if (!container) {
|
| 56 |
-
let sib = scriptEl && scriptEl.previousElementSibling;
|
| 57 |
-
while (sib && !(sib.classList && sib.classList.contains('image-comparison'))) {
|
| 58 |
-
sib = sib.previousElementSibling;
|
| 59 |
-
}
|
| 60 |
-
container = sib || document.querySelector('.image-comparison');
|
| 61 |
-
}
|
| 62 |
-
if (!container) return;
|
| 63 |
-
if (container.dataset && container.dataset.mounted === 'true') return; if (container.dataset) container.dataset.mounted = 'true';
|
| 64 |
-
|
| 65 |
-
// Known filenames in /public/data/comparison
|
| 66 |
-
const FILES = {
|
| 67 |
-
'1': { query: 'id_1_query.png', 1: 'id_1_rank_1_sim_1.000.png', 2: 'id_1_rank_2_sim_0.165.png', 3: 'id_1_rank_3_sim_0.143.png' },
|
| 68 |
-
'2': { query: 'id_2_query.png', 1: 'id_2_rank_1_sim_1.000.png', 2: 'id_2_rank_2_sim_0.978.png', 3: 'id_2_rank_3_sim_0.975.png' },
|
| 69 |
-
'3': { query: 'id_3_query.png', 1: 'id_3_rank_1_sim_0.936.png', 2: 'id_3_rank_2_sim_0.686.png', 3: 'id_3_rank_3_sim_0.676.png' },
|
| 70 |
-
};
|
| 71 |
-
|
| 72 |
-
// Images served from [domain]/public/data/comparison/*.png → path is /data/comparison/
|
| 73 |
-
const CANDIDATE_BASES = [ '/data/comparison/' ];
|
| 74 |
-
|
| 75 |
-
const resolveBase = (candidates, filename) => new Promise((resolve) => {
|
| 76 |
-
let idx = 0; const tryNext = () => {
|
| 77 |
-
if (idx >= candidates.length) return resolve(candidates[0]);
|
| 78 |
-
const img = new Image();
|
| 79 |
-
img.onload = () => resolve(candidates[idx]);
|
| 80 |
-
img.onerror = () => { idx += 1; tryNext(); };
|
| 81 |
-
img.src = candidates[idx] + filename;
|
| 82 |
-
}; tryNext();
|
| 83 |
-
});
|
| 84 |
-
|
| 85 |
-
// Controls
|
| 86 |
-
const controls = document.createElement('div'); controls.className = 'controls';
|
| 87 |
-
const label = document.createElement('label'); label.textContent = 'Example';
|
| 88 |
-
const select = document.createElement('select');
|
| 89 |
-
const EXAMPLE_LABELS = { '1': 'Photo', '2': 'Chart', '3': 'Drawing' };
|
| 90 |
-
['1','2','3'].forEach((id)=>{ const o=document.createElement('option'); o.value=id; o.textContent=EXAMPLE_LABELS[id]; select.appendChild(o); });
|
| 91 |
-
label.appendChild(select); controls.appendChild(label); container.appendChild(controls);
|
| 92 |
-
|
| 93 |
-
// Grid
|
| 94 |
-
const grid = document.createElement('div'); grid.className = 'grid'; container.appendChild(grid);
|
| 95 |
-
|
| 96 |
-
let basePath = CANDIDATE_BASES[0];
|
| 97 |
-
|
| 98 |
-
const parseInfo = (filename) => {
|
| 99 |
-
const rankMatch = filename.match(/rank_(\d+)/i); const rank = rankMatch ? rankMatch[1] : '';
|
| 100 |
-
const simMatch = filename.match(/sim_([0-9.]+)/i); const sim = simMatch ? simMatch[1] : '';
|
| 101 |
-
return { rank, sim };
|
| 102 |
-
};
|
| 103 |
-
|
| 104 |
-
const formatSim = (val) => {
|
| 105 |
-
if (val == null || val === '') return '—';
|
| 106 |
-
return String(val).replace(/\.$/, '');
|
| 107 |
-
};
|
| 108 |
-
|
| 109 |
-
const render = (id) => {
|
| 110 |
-
const files = FILES[id]; if (!files) return;
|
| 111 |
-
const ordered = [files.query, files[1], files[2], files[3]]; // query, then matches 1, 2, 3
|
| 112 |
-
grid.innerHTML = '';
|
| 113 |
-
ordered.forEach((fname, idx) => {
|
| 114 |
-
const { sim } = parseInfo(fname);
|
| 115 |
-
const isQuery = idx === 0;
|
| 116 |
-
const card = document.createElement('div'); card.className = 'card';
|
| 117 |
-
const media = document.createElement('div'); media.className = 'media';
|
| 118 |
-
const img = document.createElement('img'); img.alt = `example ${id} ${isQuery ? 'query' : `match ${idx}`}`; img.loading = 'lazy'; img.src = basePath + fname; media.appendChild(img);
|
| 119 |
-
const meta = document.createElement('div'); meta.className = 'meta';
|
| 120 |
-
|
| 121 |
-
if (isQuery) {
|
| 122 |
-
const label = document.createElement('span'); label.className = 'value'; label.textContent = 'Query';
|
| 123 |
-
meta.appendChild(label);
|
| 124 |
-
} else {
|
| 125 |
-
const content = document.createElement('span');
|
| 126 |
-
content.innerHTML = `<span class="value">Match ${idx}</span><br><span class="label">Similarity: ${formatSim(sim)}</span>`;
|
| 127 |
-
meta.appendChild(content);
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
card.appendChild(media); card.appendChild(meta); grid.appendChild(card);
|
| 131 |
-
});
|
| 132 |
-
};
|
| 133 |
-
|
| 134 |
-
(async () => {
|
| 135 |
-
// Resolve a working base then initial render
|
| 136 |
-
basePath = await resolveBase(CANDIDATE_BASES, FILES['1'].query);
|
| 137 |
-
render('1');
|
| 138 |
-
})();
|
| 139 |
-
|
| 140 |
-
select.addEventListener('change', () => render(select.value));
|
| 141 |
-
};
|
| 142 |
-
|
| 143 |
-
if (document.readyState === 'loading') {
|
| 144 |
-
document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
|
| 145 |
-
} else { bootstrap(); }
|
| 146 |
-
})();
|
| 147 |
-
</script>
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/d3-confusion-matrix.html
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-confusion-matrix" ></div>
|
| 2 |
+
<style>
|
| 3 |
+
.d3-confusion-matrix {
|
| 4 |
+
position: relative;
|
| 5 |
+
}
|
| 6 |
+
.d3-confusion-matrix .panels {
|
| 7 |
+
display: flex;
|
| 8 |
+
flex-wrap: wrap;
|
| 9 |
+
gap: 16px;
|
| 10 |
+
margin-bottom: 4px;
|
| 11 |
+
}
|
| 12 |
+
.d3-confusion-matrix .panel {
|
| 13 |
+
flex: 1 1 320px;
|
| 14 |
+
min-width: 280px;
|
| 15 |
+
}
|
| 16 |
+
.d3-confusion-matrix .panel__title {
|
| 17 |
+
color: var(--text-color);
|
| 18 |
+
font-size: 12px;
|
| 19 |
+
line-height: 1.35;
|
| 20 |
+
margin: 0 0 6px 0;
|
| 21 |
+
font-weight: 600;
|
| 22 |
+
}
|
| 23 |
+
.d3-confusion-matrix .axis-label {
|
| 24 |
+
fill: var(--text-color);
|
| 25 |
+
font-size: 11px;
|
| 26 |
+
font-weight: 700;
|
| 27 |
+
}
|
| 28 |
+
.d3-confusion-matrix .cell-border {
|
| 29 |
+
stroke: var(--border-color);
|
| 30 |
+
stroke-width: 1px;
|
| 31 |
+
fill: none;
|
| 32 |
+
}
|
| 33 |
+
.d3-confusion-matrix .cell-text {
|
| 34 |
+
fill: var(--muted-color);
|
| 35 |
+
font-size: 11px;
|
| 36 |
+
pointer-events: none;
|
| 37 |
+
}
|
| 38 |
+
.d3-confusion-matrix .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
|
| 39 |
+
</style>
|
| 40 |
+
<script>
|
| 41 |
+
(() => {
|
| 42 |
+
// Load D3 from CDN once
|
| 43 |
+
const ensureD3 = (cb) => {
|
| 44 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 45 |
+
let s = document.getElementById('d3-cdn-script');
|
| 46 |
+
if (!s) {
|
| 47 |
+
s = document.createElement('script');
|
| 48 |
+
s.id = 'd3-cdn-script';
|
| 49 |
+
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 50 |
+
document.head.appendChild(s);
|
| 51 |
+
}
|
| 52 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 53 |
+
s.addEventListener('load', onReady, { once: true });
|
| 54 |
+
if (window.d3) onReady();
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
const bootstrap = () => {
|
| 58 |
+
const scriptEl = document.currentScript;
|
| 59 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 60 |
+
if (!(container && container.classList && container.classList.contains('d3-confusion-matrix'))){
|
| 61 |
+
const cs = Array.from(document.querySelectorAll('.d3-confusion-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 62 |
+
container = cs[cs.length - 1] || null;
|
| 63 |
+
}
|
| 64 |
+
if (!container) return;
|
| 65 |
+
if (container.dataset) {
|
| 66 |
+
if (container.dataset.mounted === 'true') return;
|
| 67 |
+
container.dataset.mounted = 'true';
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Tooltip (HTML, single instance inside container)
|
| 71 |
+
container.style.position = container.style.position || 'relative';
|
| 72 |
+
let tip = container.querySelector('.d3-tooltip');
|
| 73 |
+
let tipInner;
|
| 74 |
+
if (!tip) {
|
| 75 |
+
tip = document.createElement('div');
|
| 76 |
+
tip.className = 'd3-tooltip';
|
| 77 |
+
Object.assign(tip.style, {
|
| 78 |
+
position: 'absolute',
|
| 79 |
+
top: '0px',
|
| 80 |
+
left: '0px',
|
| 81 |
+
transform: 'translate(-9999px, -9999px)',
|
| 82 |
+
pointerEvents: 'none',
|
| 83 |
+
padding: '8px 10px',
|
| 84 |
+
borderRadius: '8px',
|
| 85 |
+
fontSize: '12px',
|
| 86 |
+
lineHeight: '1.35',
|
| 87 |
+
border: '1px solid var(--border-color)',
|
| 88 |
+
background: 'var(--surface-bg)',
|
| 89 |
+
color: 'var(--text-color)',
|
| 90 |
+
boxShadow: '0 4px 24px rgba(0,0,0,.18)',
|
| 91 |
+
opacity: '0',
|
| 92 |
+
transition: 'opacity .12s ease'
|
| 93 |
+
});
|
| 94 |
+
tipInner = document.createElement('div');
|
| 95 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 96 |
+
tipInner.style.textAlign = 'left';
|
| 97 |
+
tip.appendChild(tipInner);
|
| 98 |
+
container.appendChild(tip);
|
| 99 |
+
} else {
|
| 100 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Panels container (two side-by-side matrices)
|
| 104 |
+
const panels = document.createElement('div');
|
| 105 |
+
panels.className = 'panels';
|
| 106 |
+
const panelA = document.createElement('div');
|
| 107 |
+
panelA.className = 'panel';
|
| 108 |
+
const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';
|
| 109 |
+
panelA.appendChild(titleA);
|
| 110 |
+
const mountA = document.createElement('div'); panelA.appendChild(mountA);
|
| 111 |
+
const panelB = document.createElement('div');
|
| 112 |
+
panelB.className = 'panel';
|
| 113 |
+
const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved − Baseline, pp)';
|
| 114 |
+
panelB.appendChild(titleB);
|
| 115 |
+
const mountB = document.createElement('div'); panelB.appendChild(mountB);
|
| 116 |
+
panels.appendChild(panelA);
|
| 117 |
+
panels.appendChild(panelB);
|
| 118 |
+
container.appendChild(panels);
|
| 119 |
+
|
| 120 |
+
// SVG scaffolding
|
| 121 |
+
const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);
|
| 122 |
+
const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');
|
| 123 |
+
const gRootA = svgA.append('g');
|
| 124 |
+
const gCellsA = gRootA.append('g');
|
| 125 |
+
const gAxesA = gRootA.append('g');
|
| 126 |
+
const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);
|
| 127 |
+
const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');
|
| 128 |
+
const gRootB = svgB.append('g');
|
| 129 |
+
const gCellsB = gRootB.append('g');
|
| 130 |
+
const gAxesB = gRootB.append('g');
|
| 131 |
+
|
| 132 |
+
// Demo data (two distinct 10x10 confusion matrices: Baseline vs Improved)
|
| 133 |
+
// Rows: actual, Columns: predicted
|
| 134 |
+
const classes = ['0','1','2','3','4','5','6','7','8','9'];
|
| 135 |
+
const matrixA = [
|
| 136 |
+
[90, 2, 1, 0, 0, 0, 1, 0, 5, 1],
|
| 137 |
+
[ 3, 85, 5, 1, 0, 1, 2, 1, 1, 1],
|
| 138 |
+
[ 1, 6, 70, 10, 4, 4, 1, 1, 1, 2],
|
| 139 |
+
[ 0, 1, 8, 65, 10, 10, 2, 1, 1, 2],
|
| 140 |
+
[ 0, 0, 2, 6, 83, 3, 1, 1, 3, 1],
|
| 141 |
+
[ 0, 1, 2, 12, 4, 70, 5, 2, 2, 2],
|
| 142 |
+
[ 1, 2, 1, 0, 1, 2, 88, 1, 3, 1],
|
| 143 |
+
[ 0, 1, 1, 1, 1, 1, 2, 90, 1, 2],
|
| 144 |
+
[ 6, 2, 2, 4, 6, 3, 3, 2, 70, 2],
|
| 145 |
+
[ 1, 1, 1, 1, 2, 1, 1, 2, 1, 89]
|
| 146 |
+
];
|
| 147 |
+
const matrixB = [
|
| 148 |
+
[94, 1, 0, 0, 0, 0, 1, 0, 3, 1],
|
| 149 |
+
[ 2, 90, 3, 1, 0, 0, 1, 1, 1, 1],
|
| 150 |
+
[ 1, 4, 78, 7, 3, 3, 1, 1, 1, 1],
|
| 151 |
+
[ 0, 1, 5, 74, 7, 8, 1, 1, 1, 2],
|
| 152 |
+
[ 0, 0, 1, 4, 88, 2, 1, 1, 2, 1],
|
| 153 |
+
[ 0, 1, 1, 9, 3, 78, 3, 1, 2, 2],
|
| 154 |
+
[ 1, 1, 1, 0, 1, 1, 91, 1, 2, 1],
|
| 155 |
+
[ 0, 1, 1, 1, 1, 1, 1, 92, 1, 1],
|
| 156 |
+
[ 4, 1, 1, 3, 4, 2, 2, 2, 79, 2],
|
| 157 |
+
[ 1, 1, 1, 1, 2, 1, 1, 1, 1, 90]
|
| 158 |
+
];
|
| 159 |
+
|
| 160 |
+
// Colors: sequential palette via window.ColorPalettes with graceful fallback
|
| 161 |
+
const getSequentialColors = (count) => {
|
| 162 |
+
try {
|
| 163 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 164 |
+
return window.ColorPalettes.getColors('sequential', count);
|
| 165 |
+
}
|
| 166 |
+
} catch (_) {}
|
| 167 |
+
// Fallback: generate a monochrome scale using the primary color with varying opacity
|
| 168 |
+
const arr = [];
|
| 169 |
+
for (let i = 0; i < count; i++) arr.push('var(--primary-color)');
|
| 170 |
+
return arr;
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
const palette = getSequentialColors(13);
|
| 174 |
+
const getDivergingColors = (count) => {
|
| 175 |
+
try {
|
| 176 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 177 |
+
return window.ColorPalettes.getColors('diverging', count);
|
| 178 |
+
}
|
| 179 |
+
} catch (_) {}
|
| 180 |
+
const steps = Math.max(3, count|0);
|
| 181 |
+
const arr = [];
|
| 182 |
+
for (let i = 0; i < steps; i++) {
|
| 183 |
+
const t = i / (steps - 1);
|
| 184 |
+
const pct = Math.round(t * 100);
|
| 185 |
+
arr.push(`color-mix(in srgb, #D64545 ${100-pct}%, #3A7BD5 ${pct}%)`);
|
| 186 |
+
}
|
| 187 |
+
return arr;
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
let width = 800;
|
| 191 |
+
let height = 480;
|
| 192 |
+
const margin = { top: 36, right: 24, bottom: 26, left: 56 };
|
| 193 |
+
|
| 194 |
+
function updateSize() {
|
| 195 |
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 196 |
+
width = container.clientWidth || 800;
|
| 197 |
+
const gap = 16; // matches CSS .panels gap
|
| 198 |
+
const minPanel = 320;
|
| 199 |
+
const nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;
|
| 200 |
+
const panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);
|
| 201 |
+
const base = Math.max(minPanel, Math.round(panelWidth * 0.92));
|
| 202 |
+
height = base;
|
| 203 |
+
// Responsive SVG: width 100%, height auto, preserve aspect via viewBox
|
| 204 |
+
svgA
|
| 205 |
+
.attr('viewBox', `0 0 ${panelWidth} ${height}`)
|
| 206 |
+
.attr('preserveAspectRatio', 'xMidYMid meet')
|
| 207 |
+
.style('width', '100%')
|
| 208 |
+
.style('height', 'auto');
|
| 209 |
+
svgB
|
| 210 |
+
.attr('viewBox', `0 0 ${panelWidth} ${height}`)
|
| 211 |
+
.attr('preserveAspectRatio', 'xMidYMid meet')
|
| 212 |
+
.style('width', '100%')
|
| 213 |
+
.style('height', 'auto');
|
| 214 |
+
gRootA.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 215 |
+
gRootB.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 216 |
+
const innerWidth = panelWidth - margin.left - margin.right;
|
| 217 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 218 |
+
return { innerWidth, innerHeight, isDark };
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
function computeValues(normalization, matrix) {
|
| 222 |
+
const n = classes.length;
|
| 223 |
+
const totalsByRow = matrix.map(row => row.reduce((a, b) => a + b, 0));
|
| 224 |
+
const flat = [];
|
| 225 |
+
let minV = Infinity, maxV = -Infinity;
|
| 226 |
+
for (let r = 0; r < n; r++) {
|
| 227 |
+
for (let c = 0; c < n; c++) {
|
| 228 |
+
const count = matrix[r][c];
|
| 229 |
+
const value = normalization === 'row' ? (totalsByRow[r] ? count / totalsByRow[r] : 0) : count;
|
| 230 |
+
if (value < minV) minV = value;
|
| 231 |
+
if (value > maxV) maxV = value;
|
| 232 |
+
flat.push({ r, c, count, value });
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
return { data: flat, minV, maxV };
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
function getColorScale(values, minV, maxV) {
|
| 239 |
+
// If ColorPalettes is available, use quantiles to enhance visual variation across the distribution
|
| 240 |
+
const hasPalette = !(palette.length === 0);
|
| 241 |
+
if (hasPalette && (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')) {
|
| 242 |
+
const scale = d3.scaleQuantile().domain(values).range(palette);
|
| 243 |
+
return (v) => scale(v);
|
| 244 |
+
}
|
| 245 |
+
// Fallback: primary color with opacity mapped to normalized value
|
| 246 |
+
const norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);
|
| 247 |
+
return (v) => `color-mix(in oklab, var(--primary-color) ${Math.round(norm(v) * 100)}%, var(--surface-bg))`;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// (no local contrast function — use ColorPalettes.getTextStyleForBackground)
|
| 251 |
+
|
| 252 |
+
// Compute a fixed readable text color from a CSS rgb()/rgba() string
|
| 253 |
+
function chooseFixedReadableTextOnBg(bgCss){
|
| 254 |
+
try {
|
| 255 |
+
const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/);
|
| 256 |
+
if (!m) return '#0e1116';
|
| 257 |
+
const parts = m[1].split(',').map(s => parseFloat(s.trim()));
|
| 258 |
+
const [r, g, b] = parts;
|
| 259 |
+
// sRGB → relative luminance
|
| 260 |
+
const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);
|
| 261 |
+
const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4)));
|
| 262 |
+
const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2];
|
| 263 |
+
// Threshold ~ 0.5 for readability; darker BG → white text, else near-black
|
| 264 |
+
return L < 0.5 ? '#ffffff' : '#0e1116';
|
| 265 |
+
} catch(_) { return '#0e1116'; }
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function render() {
|
| 269 |
+
const { innerWidth, innerHeight } = updateSize();
|
| 270 |
+
const n = classes.length;
|
| 271 |
+
const gridSize = Math.min(innerWidth, innerHeight);
|
| 272 |
+
const cellSize = gridSize / n;
|
| 273 |
+
|
| 274 |
+
const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
|
| 275 |
+
const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
|
| 276 |
+
|
| 277 |
+
// Panel A: Baseline (row-normalized)
|
| 278 |
+
const dataA = computeValues('row', matrixA);
|
| 279 |
+
const colorA = getColorScale(dataA.data.map(d => d.value), dataA.minV, dataA.maxV);
|
| 280 |
+
|
| 281 |
+
gCellsA.selectAll('rect.cell-bg')
|
| 282 |
+
.data([0])
|
| 283 |
+
.join('rect')
|
| 284 |
+
.attr('class', 'cell-bg')
|
| 285 |
+
.attr('x', 0)
|
| 286 |
+
.attr('y', 0)
|
| 287 |
+
.attr('width', gridSize)
|
| 288 |
+
.attr('height', gridSize)
|
| 289 |
+
.attr('fill', 'none')
|
| 290 |
+
.attr('stroke', 'var(--border-color)')
|
| 291 |
+
.attr('stroke-width', 1);
|
| 292 |
+
|
| 293 |
+
const cellsA = gCellsA.selectAll('g.cell')
|
| 294 |
+
.data(dataA.data, d => `${d.r}-${d.c}-A`);
|
| 295 |
+
|
| 296 |
+
const cellsEnterA = cellsA.enter()
|
| 297 |
+
.append('g')
|
| 298 |
+
.attr('class', 'cell');
|
| 299 |
+
|
| 300 |
+
cellsEnterA.append('rect')
|
| 301 |
+
.attr('rx', 2)
|
| 302 |
+
.attr('ry', 2)
|
| 303 |
+
.on('mousemove', (event, d) => {
|
| 304 |
+
const [px, py] = d3.pointer(event, container);
|
| 305 |
+
tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong><br/>${(d.value * 100).toFixed(1)}% (${d.count})`;
|
| 306 |
+
tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
|
| 307 |
+
tip.style.opacity = '1';
|
| 308 |
+
})
|
| 309 |
+
.on('mouseleave', () => {
|
| 310 |
+
tip.style.opacity = '0';
|
| 311 |
+
});
|
| 312 |
+
|
| 313 |
+
cellsEnterA.append('text')
|
| 314 |
+
.attr('class', 'cell-text')
|
| 315 |
+
.attr('text-anchor', 'middle')
|
| 316 |
+
.attr('dominant-baseline', 'middle');
|
| 317 |
+
|
| 318 |
+
const cellsMergedA = cellsEnterA.merge(cellsA);
|
| 319 |
+
|
| 320 |
+
cellsMergedA.select('rect')
|
| 321 |
+
.attr('x', d => x(d.c))
|
| 322 |
+
.attr('y', d => y(d.r))
|
| 323 |
+
.attr('width', Math.max(1, x.bandwidth()))
|
| 324 |
+
.attr('height', Math.max(1, y.bandwidth()))
|
| 325 |
+
.attr('fill', d => colorA(d.value));
|
| 326 |
+
|
| 327 |
+
cellsMergedA.select('text')
|
| 328 |
+
.attr('x', d => x(d.c) + x.bandwidth() / 2)
|
| 329 |
+
.attr('y', d => y(d.r) + y.bandwidth() / 2)
|
| 330 |
+
.text(d => `${Math.round(d.value * 100)}`)
|
| 331 |
+
.style('fill', function(d){
|
| 332 |
+
try {
|
| 333 |
+
const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
|
| 334 |
+
const bg = rect ? getComputedStyle(rect).fill : colorA(d.value);
|
| 335 |
+
return chooseFixedReadableTextOnBg(bg);
|
| 336 |
+
} catch (_) {
|
| 337 |
+
return '#0e1116';
|
| 338 |
+
}
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
cellsA.exit().remove();
|
| 342 |
+
|
| 343 |
+
gAxesA.selectAll('*').remove();
|
| 344 |
+
|
| 345 |
+
gAxesA.append('g')
|
| 346 |
+
.selectAll('text')
|
| 347 |
+
.data(classes)
|
| 348 |
+
.join('text')
|
| 349 |
+
.attr('class', 'axis-label')
|
| 350 |
+
.attr('text-anchor', 'middle')
|
| 351 |
+
.attr('x', (_, i) => x(i) + x.bandwidth() / 2)
|
| 352 |
+
.attr('y', -8)
|
| 353 |
+
.text(d => d);
|
| 354 |
+
|
| 355 |
+
gAxesA.append('g')
|
| 356 |
+
.selectAll('text')
|
| 357 |
+
.data(classes)
|
| 358 |
+
.join('text')
|
| 359 |
+
.attr('class', 'axis-label')
|
| 360 |
+
.attr('text-anchor', 'end')
|
| 361 |
+
.attr('x', -8)
|
| 362 |
+
.attr('y', (_, i) => y(i) + y.bandwidth() / 2)
|
| 363 |
+
.attr('dominant-baseline', 'middle')
|
| 364 |
+
.text(d => d);
|
| 365 |
+
|
| 366 |
+
gAxesA.append('text')
|
| 367 |
+
.attr('class', 'axis-label')
|
| 368 |
+
.attr('text-anchor', 'middle')
|
| 369 |
+
.attr('x', gridSize / 2)
|
| 370 |
+
.attr('y', innerHeight + 20)
|
| 371 |
+
.text('Predicted');
|
| 372 |
+
|
| 373 |
+
gAxesA.append('text')
|
| 374 |
+
.attr('class', 'axis-label')
|
| 375 |
+
.attr('text-anchor', 'middle')
|
| 376 |
+
.attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
|
| 377 |
+
.text('Actual');
|
| 378 |
+
|
| 379 |
+
// Panel B: Delta (Improved − Baseline), row-normalized differences in percentage points
|
| 380 |
+
const dataB = computeValues('row', matrixB);
|
| 381 |
+
const diverging = getDivergingColors(13);
|
| 382 |
+
// Build delta values aligned to A's ordering
|
| 383 |
+
const mapA = new Map(dataA.data.map(d => [d.r + '-' + d.c, d.value]));
|
| 384 |
+
const delta = dataB.data.map(d => ({ r: d.r, c: d.c, count: d.count, value: (d.value - (mapA.get(d.r + '-' + d.c) || 0)) }));
|
| 385 |
+
// Symmetric domain around 0 (in proportions), express later as pp in labels
|
| 386 |
+
const maxAbsDelta = Math.max(0.01, d3.max(delta, d => Math.abs(d.value)) || 0.01);
|
| 387 |
+
const colorB = d3.scaleQuantize().domain([-maxAbsDelta, maxAbsDelta]).range(diverging);
|
| 388 |
+
|
| 389 |
+
gCellsB.selectAll('rect.cell-bg')
|
| 390 |
+
.data([0])
|
| 391 |
+
.join('rect')
|
| 392 |
+
.attr('class', 'cell-bg')
|
| 393 |
+
.attr('x', 0)
|
| 394 |
+
.attr('y', 0)
|
| 395 |
+
.attr('width', gridSize)
|
| 396 |
+
.attr('height', gridSize)
|
| 397 |
+
.attr('fill', 'none')
|
| 398 |
+
.attr('stroke', 'var(--border-color)')
|
| 399 |
+
.attr('stroke-width', 1);
|
| 400 |
+
|
| 401 |
+
const cellsB = gCellsB.selectAll('g.cell')
|
| 402 |
+
.data(dataB.data, d => `${d.r}-${d.c}-B`);
|
| 403 |
+
|
| 404 |
+
const cellsEnterB = cellsB.enter()
|
| 405 |
+
.append('g')
|
| 406 |
+
.attr('class', 'cell');
|
| 407 |
+
|
| 408 |
+
cellsEnterB.append('rect')
|
| 409 |
+
.attr('rx', 2)
|
| 410 |
+
.attr('ry', 2)
|
| 411 |
+
.on('mousemove', (event, d) => {
|
| 412 |
+
const [px, py] = d3.pointer(event, container);
|
| 413 |
+
const a = dataA.data.find(x => x.r===d.r && x.c===d.c);
|
| 414 |
+
const b = dataB.data.find(x => x.r===d.r && x.c===d.c);
|
| 415 |
+
const dv = ((b ? b.value : 0) - (a ? a.value : 0)) * 100;
|
| 416 |
+
tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong>` +
|
| 417 |
+
`<br/>baseline ${(a ? a.value*100 : 0).toFixed(1)}%` +
|
| 418 |
+
`<br/>improved ${(b ? b.value*100 : 0).toFixed(1)}%` +
|
| 419 |
+
`<br/>delta ${dv.toFixed(1)} pp`;
|
| 420 |
+
tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
|
| 421 |
+
tip.style.opacity = '1';
|
| 422 |
+
})
|
| 423 |
+
.on('mouseleave', () => {
|
| 424 |
+
tip.style.opacity = '0';
|
| 425 |
+
});
|
| 426 |
+
|
| 427 |
+
cellsEnterB.append('text')
|
| 428 |
+
.attr('class', 'cell-text')
|
| 429 |
+
.attr('text-anchor', 'middle')
|
| 430 |
+
.attr('dominant-baseline', 'middle');
|
| 431 |
+
|
| 432 |
+
const cellsMergedB = cellsEnterB.merge(cellsB);
|
| 433 |
+
|
| 434 |
+
cellsMergedB.select('rect')
|
| 435 |
+
.attr('x', d => x(d.c))
|
| 436 |
+
.attr('y', d => y(d.r))
|
| 437 |
+
.attr('width', Math.max(1, x.bandwidth()))
|
| 438 |
+
.attr('height', Math.max(1, y.bandwidth()))
|
| 439 |
+
.attr('fill', d => colorB(delta.find(x => x.r===d.r && x.c===d.c).value));
|
| 440 |
+
|
| 441 |
+
cellsMergedB.select('text')
|
| 442 |
+
.attr('x', d => x(d.c) + x.bandwidth() / 2)
|
| 443 |
+
.attr('y', d => y(d.r) + y.bandwidth() / 2)
|
| 444 |
+
.text(d => {
|
| 445 |
+
const dv = delta.find(x => x.r===d.r && x.c===d.c).value; return `${Math.round(dv * 100)}`;
|
| 446 |
+
})
|
| 447 |
+
.style('fill', function(d){
|
| 448 |
+
try {
|
| 449 |
+
const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
|
| 450 |
+
const dv = delta.find(x => x.r===d.r && x.c===d.c).value;
|
| 451 |
+
const bg = rect ? getComputedStyle(rect).fill : colorB(dv);
|
| 452 |
+
return chooseFixedReadableTextOnBg(bg);
|
| 453 |
+
} catch (_) {
|
| 454 |
+
return '#0e1116';
|
| 455 |
+
}
|
| 456 |
+
});
|
| 457 |
+
|
| 458 |
+
cellsB.exit().remove();
|
| 459 |
+
|
| 460 |
+
gAxesB.selectAll('*').remove();
|
| 461 |
+
|
| 462 |
+
gAxesB.append('g')
|
| 463 |
+
.selectAll('text')
|
| 464 |
+
.data(classes)
|
| 465 |
+
.join('text')
|
| 466 |
+
.attr('class', 'axis-label')
|
| 467 |
+
.attr('text-anchor', 'middle')
|
| 468 |
+
.attr('x', (_, i) => x(i) + x.bandwidth() / 2)
|
| 469 |
+
.attr('y', -8)
|
| 470 |
+
.text(d => d);
|
| 471 |
+
|
| 472 |
+
gAxesB.append('g')
|
| 473 |
+
.selectAll('text')
|
| 474 |
+
.data(classes)
|
| 475 |
+
.join('text')
|
| 476 |
+
.attr('class', 'axis-label')
|
| 477 |
+
.attr('text-anchor', 'end')
|
| 478 |
+
.attr('x', -8)
|
| 479 |
+
.attr('y', (_, i) => y(i) + y.bandwidth() / 2)
|
| 480 |
+
.attr('dominant-baseline', 'middle')
|
| 481 |
+
.text(d => d);
|
| 482 |
+
|
| 483 |
+
gAxesB.append('text')
|
| 484 |
+
.attr('class', 'axis-label')
|
| 485 |
+
.attr('text-anchor', 'middle')
|
| 486 |
+
.attr('x', gridSize / 2)
|
| 487 |
+
.attr('y', innerHeight + 20)
|
| 488 |
+
.text('Predicted');
|
| 489 |
+
|
| 490 |
+
gAxesB.append('text')
|
| 491 |
+
.attr('class', 'axis-label')
|
| 492 |
+
.attr('text-anchor', 'middle')
|
| 493 |
+
.attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
|
| 494 |
+
.text('Actual');
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// Initial render + resize handling
|
| 498 |
+
render();
|
| 499 |
+
const rerender = () => render();
|
| 500 |
+
if (window.ResizeObserver) {
|
| 501 |
+
const ro = new ResizeObserver(() => rerender());
|
| 502 |
+
ro.observe(container);
|
| 503 |
+
} else {
|
| 504 |
+
window.addEventListener('resize', rerender);
|
| 505 |
+
}
|
| 506 |
+
};
|
| 507 |
+
|
| 508 |
+
if (document.readyState === 'loading') {
|
| 509 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 510 |
+
} else {
|
| 511 |
+
ensureD3(bootstrap);
|
| 512 |
+
}
|
| 513 |
+
})();
|
| 514 |
+
</script>
|
| 515 |
+
|
| 516 |
+
|
app/src/content/embeds/d3-line-example.html
DELETED
|
@@ -1,456 +0,0 @@
|
|
| 1 |
-
<div class="d3-line-example" style="width:100%;margin:10px 0;"></div>
|
| 2 |
-
<style>
|
| 3 |
-
.d3-line-example .d3-line__controls select {
|
| 4 |
-
font-size: 12px;
|
| 5 |
-
padding: 8px 28px 8px 10px;
|
| 6 |
-
border: 1px solid var(--border-color);
|
| 7 |
-
border-radius: 8px;
|
| 8 |
-
background-color: var(--surface-bg);
|
| 9 |
-
color: var(--text-color);
|
| 10 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 11 |
-
background-repeat: no-repeat;
|
| 12 |
-
background-position: right 8px center;
|
| 13 |
-
background-size: 12px;
|
| 14 |
-
-webkit-appearance: none;
|
| 15 |
-
-moz-appearance: none;
|
| 16 |
-
appearance: none;
|
| 17 |
-
cursor: pointer;
|
| 18 |
-
transition: border-color .15s ease, box-shadow .15s ease;
|
| 19 |
-
}
|
| 20 |
-
[data-theme="dark"] .d3-line-example .d3-line__controls select {
|
| 21 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 22 |
-
}
|
| 23 |
-
.d3-line-example .d3-line__controls select:hover {
|
| 24 |
-
border-color: var(--primary-color);
|
| 25 |
-
}
|
| 26 |
-
.d3-line-example .d3-line__controls select:focus {
|
| 27 |
-
border-color: var(--primary-color);
|
| 28 |
-
box-shadow: 0 0 0 3px rgba(232,137,171,.25);
|
| 29 |
-
outline: none;
|
| 30 |
-
}
|
| 31 |
-
.d3-line-example .d3-line__controls label { gap: 8px; }
|
| 32 |
-
|
| 33 |
-
/* Range slider themed with --primary-color */
|
| 34 |
-
.d3-line-example .d3-line__controls input[type="range"] {
|
| 35 |
-
-webkit-appearance: none;
|
| 36 |
-
appearance: none;
|
| 37 |
-
width: 100%;
|
| 38 |
-
height: 6px;
|
| 39 |
-
border-radius: 999px;
|
| 40 |
-
background: var(--border-color);
|
| 41 |
-
outline: none;
|
| 42 |
-
}
|
| 43 |
-
.d3-line-example .d3-line__controls input[type="range"]::-webkit-slider-runnable-track {
|
| 44 |
-
height: 6px;
|
| 45 |
-
background: transparent;
|
| 46 |
-
border-radius: 999px;
|
| 47 |
-
}
|
| 48 |
-
.d3-line-example .d3-line__controls input[type="range"]::-webkit-slider-thumb {
|
| 49 |
-
-webkit-appearance: none;
|
| 50 |
-
appearance: none;
|
| 51 |
-
width: 16px;
|
| 52 |
-
height: 16px;
|
| 53 |
-
border-radius: 50%;
|
| 54 |
-
background: var(--primary-color);
|
| 55 |
-
border: 2px solid var(--on-primary);
|
| 56 |
-
margin-top: -5px;
|
| 57 |
-
cursor: pointer;
|
| 58 |
-
}
|
| 59 |
-
.d3-line-example .d3-line__controls input[type="range"]::-moz-range-track {
|
| 60 |
-
height: 6px;
|
| 61 |
-
background: transparent;
|
| 62 |
-
border-radius: 999px;
|
| 63 |
-
}
|
| 64 |
-
.d3-line-example .d3-line__controls input[type="range"]::-moz-range-thumb {
|
| 65 |
-
width: 16px;
|
| 66 |
-
height: 16px;
|
| 67 |
-
border-radius: 50%;
|
| 68 |
-
background: var(--primary-color);
|
| 69 |
-
border: 2px solid var(--on-primary);
|
| 70 |
-
cursor: pointer;
|
| 71 |
-
}
|
| 72 |
-
/* Improved line color via CSS */
|
| 73 |
-
.d3-line-example .lines path.improved { stroke: var(--primary-color); }
|
| 74 |
-
|
| 75 |
-
/* Responsive: stack controls on small screens */
|
| 76 |
-
@media (max-width: 640px) {
|
| 77 |
-
.d3-line-example .d3-line__controls {
|
| 78 |
-
flex-wrap: wrap;
|
| 79 |
-
}
|
| 80 |
-
.d3-line-example .d3-line__controls label {
|
| 81 |
-
flex: 1 1 100% !important;
|
| 82 |
-
width: 100%;
|
| 83 |
-
}
|
| 84 |
-
}
|
| 85 |
-
</style>
|
| 86 |
-
<script>
|
| 87 |
-
(() => {
|
| 88 |
-
const ensureD3 = (cb) => {
|
| 89 |
-
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 90 |
-
let s = document.getElementById('d3-cdn-script');
|
| 91 |
-
if (!s) {
|
| 92 |
-
s = document.createElement('script');
|
| 93 |
-
s.id = 'd3-cdn-script';
|
| 94 |
-
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 95 |
-
document.head.appendChild(s);
|
| 96 |
-
}
|
| 97 |
-
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 98 |
-
s.addEventListener('load', onReady, { once: true });
|
| 99 |
-
if (window.d3) onReady();
|
| 100 |
-
};
|
| 101 |
-
|
| 102 |
-
const bootstrap = () => {
|
| 103 |
-
const scriptEl = document.currentScript;
|
| 104 |
-
|
| 105 |
-
const getLocalPrev = () => {
|
| 106 |
-
if (!scriptEl) return null;
|
| 107 |
-
let el = scriptEl.previousElementSibling;
|
| 108 |
-
while (el && !(el.classList && el.classList.contains('d3-line-example'))) {
|
| 109 |
-
el = el.previousElementSibling;
|
| 110 |
-
}
|
| 111 |
-
return el || null;
|
| 112 |
-
};
|
| 113 |
-
|
| 114 |
-
const localTarget = getLocalPrev();
|
| 115 |
-
const targets = localTarget
|
| 116 |
-
? [localTarget]
|
| 117 |
-
: Array.from(document.querySelectorAll('.d3-line-example'))
|
| 118 |
-
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 119 |
-
|
| 120 |
-
targets.forEach((container) => {
|
| 121 |
-
if (!container) return;
|
| 122 |
-
if (container.dataset) {
|
| 123 |
-
if (container.dataset.mounted === 'true') return;
|
| 124 |
-
container.dataset.mounted = 'true';
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
// Dataset params matching the Plotly version
|
| 128 |
-
const datasets = [
|
| 129 |
-
{ name: 'CIFAR-10', base: { ymin:0.10, ymax:0.90, k:10.0, x0:0.55 }, aug: { ymin:0.15, ymax:0.96, k:12.0, x0:0.40 }, target: 0.97 },
|
| 130 |
-
{ name: 'CIFAR-100', base: { ymin:0.05, ymax:0.70, k: 9.5, x0:0.60 }, aug: { ymin:0.08, ymax:0.80, k:11.0, x0:0.45 }, target: 0.85 },
|
| 131 |
-
{ name: 'ImageNet-1K', base: { ymin:0.02, ymax:0.68, k: 8.5, x0:0.65 }, aug: { ymin:0.04, ymax:0.75, k: 9.5, x0:0.50 }, target: 0.82 },
|
| 132 |
-
];
|
| 133 |
-
|
| 134 |
-
// Controls UI
|
| 135 |
-
const controls = document.createElement('div');
|
| 136 |
-
controls.className = 'd3-line__controls';
|
| 137 |
-
Object.assign(controls.style, {
|
| 138 |
-
marginTop: '12px',
|
| 139 |
-
display: 'flex',
|
| 140 |
-
gap: '16px',
|
| 141 |
-
alignItems: 'center'
|
| 142 |
-
});
|
| 143 |
-
|
| 144 |
-
const labelDs = document.createElement('label');
|
| 145 |
-
Object.assign(labelDs.style, {
|
| 146 |
-
fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px'
|
| 147 |
-
});
|
| 148 |
-
labelDs.textContent = 'Dataset';
|
| 149 |
-
const selectDs = document.createElement('select');
|
| 150 |
-
Object.assign(selectDs.style, { fontSize: '12px' });
|
| 151 |
-
datasets.forEach((d, i) => {
|
| 152 |
-
const o = document.createElement('option');
|
| 153 |
-
o.value = String(i);
|
| 154 |
-
o.textContent = d.name;
|
| 155 |
-
selectDs.appendChild(o);
|
| 156 |
-
});
|
| 157 |
-
labelDs.appendChild(selectDs);
|
| 158 |
-
|
| 159 |
-
const labelAlpha = document.createElement('label');
|
| 160 |
-
Object.assign(labelAlpha.style, {
|
| 161 |
-
fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '10px', flex: '1', padding: '6px 10px'
|
| 162 |
-
});
|
| 163 |
-
labelAlpha.appendChild(document.createTextNode('Augmentation α'));
|
| 164 |
-
const slider = document.createElement('input');
|
| 165 |
-
slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.01'; slider.value = '0.70';
|
| 166 |
-
Object.assign(slider.style, { flex: '1' });
|
| 167 |
-
const alphaVal = document.createElement('span'); alphaVal.className = 'alpha-value'; alphaVal.textContent = slider.value;
|
| 168 |
-
labelAlpha.appendChild(slider);
|
| 169 |
-
labelAlpha.appendChild(alphaVal);
|
| 170 |
-
|
| 171 |
-
controls.appendChild(labelDs);
|
| 172 |
-
controls.appendChild(labelAlpha);
|
| 173 |
-
|
| 174 |
-
// Create SVG
|
| 175 |
-
const svg = d3.select(container).append('svg')
|
| 176 |
-
.attr('width', '100%')
|
| 177 |
-
.style('display', 'block');
|
| 178 |
-
|
| 179 |
-
// Groups
|
| 180 |
-
const gRoot = svg.append('g');
|
| 181 |
-
const gGrid = gRoot.append('g').attr('class', 'grid');
|
| 182 |
-
const gAxes = gRoot.append('g').attr('class', 'axes');
|
| 183 |
-
const gLines = gRoot.append('g').attr('class', 'lines');
|
| 184 |
-
const gHover = gRoot.append('g').attr('class', 'hover');
|
| 185 |
-
const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
|
| 186 |
-
|
| 187 |
-
// Tooltip
|
| 188 |
-
container.style.position = container.style.position || 'relative';
|
| 189 |
-
let tip = container.querySelector('.d3-tooltip');
|
| 190 |
-
let tipInner;
|
| 191 |
-
if (!tip) {
|
| 192 |
-
tip = document.createElement('div');
|
| 193 |
-
tip.className = 'd3-tooltip';
|
| 194 |
-
Object.assign(tip.style, {
|
| 195 |
-
position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
|
| 196 |
-
padding: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
|
| 197 |
-
background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0',
|
| 198 |
-
transition: 'opacity .12s ease'
|
| 199 |
-
});
|
| 200 |
-
tipInner = document.createElement('div');
|
| 201 |
-
tipInner.className = 'd3-tooltip__inner';
|
| 202 |
-
tipInner.style.textAlign = 'left';
|
| 203 |
-
tip.appendChild(tipInner);
|
| 204 |
-
container.appendChild(tip);
|
| 205 |
-
} else {
|
| 206 |
-
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 207 |
-
}
|
| 208 |
-
|
| 209 |
-
// Colors
|
| 210 |
-
const colorBase = '#64748b'; // slate-500
|
| 211 |
-
const colorImproved = 'var(--primary-color)';
|
| 212 |
-
const colorTarget = '#4b5563'; // gray-600
|
| 213 |
-
const legendBgLight = 'rgba(255,255,255,0.85)';
|
| 214 |
-
const legendBgDark = 'rgba(17,17,23,0.85)';
|
| 215 |
-
|
| 216 |
-
// Data and helpers
|
| 217 |
-
const N = 240;
|
| 218 |
-
const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
|
| 219 |
-
const logistic = (x, { ymin, ymax, k, x0 }) => ymin + (ymax - ymin) / (1 + Math.exp(-k * (x - x0)));
|
| 220 |
-
const blend = (l, e, a) => (1 - a) * l + a * e;
|
| 221 |
-
|
| 222 |
-
let datasetIndex = 0;
|
| 223 |
-
let alpha = parseFloat(slider.value) || 0.7;
|
| 224 |
-
|
| 225 |
-
let yBase = [];
|
| 226 |
-
let yAug = [];
|
| 227 |
-
let yImp = [];
|
| 228 |
-
let yTgt = [];
|
| 229 |
-
|
| 230 |
-
function computeCurves() {
|
| 231 |
-
const d = datasets[datasetIndex];
|
| 232 |
-
yBase = xs.map((x) => logistic(x, d.base));
|
| 233 |
-
yAug = xs.map((x) => logistic(x, d.aug));
|
| 234 |
-
yTgt = xs.map(() => d.target);
|
| 235 |
-
yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
// Scales and layout
|
| 239 |
-
let width = 800, height = 360;
|
| 240 |
-
let margin = { top: 16, right: 28, bottom: 56, left: 64 };
|
| 241 |
-
let xScale = d3.scaleLinear();
|
| 242 |
-
let yScale = d3.scaleLinear();
|
| 243 |
-
|
| 244 |
-
// Paths
|
| 245 |
-
const lineGen = d3.line()
|
| 246 |
-
.curve(d3.curveCatmullRom.alpha(0.6))
|
| 247 |
-
.x((d, i) => xScale(xs[i]))
|
| 248 |
-
.y((d) => yScale(d));
|
| 249 |
-
|
| 250 |
-
const pathBase = gLines.append('path').attr('fill', 'none').attr('stroke', colorBase).attr('stroke-width', 2);
|
| 251 |
-
const pathImp = gLines.append('path').attr('class', 'improved').attr('fill', 'none').style('stroke', 'var(--primary-color)').attr('stroke-width', 2);
|
| 252 |
-
const pathTgt = gLines.append('path').attr('fill', 'none').attr('stroke', colorTarget).attr('stroke-width', 2).attr('stroke-dasharray', '6,6');
|
| 253 |
-
|
| 254 |
-
// Hover elements
|
| 255 |
-
const hoverLine = gHover.append('line').attr('stroke-width', 1);
|
| 256 |
-
const hoverDotB = gHover.append('circle').attr('r', 3.5).attr('fill', colorBase).attr('stroke', '#fff').attr('stroke-width', 1);
|
| 257 |
-
const hoverDotI = gHover.append('circle').attr('class', 'improved').attr('r', 3.5).style('fill', 'var(--primary-color)').attr('stroke', '#fff').attr('stroke-width', 1);
|
| 258 |
-
const hoverDotT = gHover.append('circle').attr('r', 3.5).attr('fill', colorTarget).attr('stroke', '#fff').attr('stroke-width', 1);
|
| 259 |
-
|
| 260 |
-
const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
|
| 261 |
-
|
| 262 |
-
function updateScales() {
|
| 263 |
-
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 264 |
-
const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
|
| 265 |
-
const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
|
| 266 |
-
const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
|
| 267 |
-
|
| 268 |
-
width = container.clientWidth || 800;
|
| 269 |
-
height = Math.max(260, Math.round(width / 3));
|
| 270 |
-
svg.attr('width', width).attr('height', height);
|
| 271 |
-
|
| 272 |
-
const innerWidth = width - margin.left - margin.right;
|
| 273 |
-
const innerHeight = height - margin.top - margin.bottom;
|
| 274 |
-
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 275 |
-
|
| 276 |
-
xScale.domain([0, 1]).range([0, innerWidth]);
|
| 277 |
-
yScale.domain([0, 1]).range([innerHeight, 0]);
|
| 278 |
-
|
| 279 |
-
// Grid (horizontal)
|
| 280 |
-
gGrid.selectAll('*').remove();
|
| 281 |
-
const yTicks = yScale.ticks(6);
|
| 282 |
-
gGrid.selectAll('line')
|
| 283 |
-
.data(yTicks)
|
| 284 |
-
.join('line')
|
| 285 |
-
.attr('x1', 0)
|
| 286 |
-
.attr('x2', innerWidth)
|
| 287 |
-
.attr('y1', (d) => yScale(d))
|
| 288 |
-
.attr('y2', (d) => yScale(d))
|
| 289 |
-
.attr('stroke', gridColor)
|
| 290 |
-
.attr('stroke-width', 1)
|
| 291 |
-
.attr('shape-rendering', 'crispEdges');
|
| 292 |
-
|
| 293 |
-
// Axes
|
| 294 |
-
gAxes.selectAll('*').remove();
|
| 295 |
-
const xAxis = d3.axisBottom(xScale).ticks(8).tickSizeOuter(0);
|
| 296 |
-
const yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0).tickFormat(d3.format('.2f'));
|
| 297 |
-
gAxes.append('g')
|
| 298 |
-
.attr('transform', `translate(0,${innerHeight})`)
|
| 299 |
-
.call(xAxis)
|
| 300 |
-
.call((g) => {
|
| 301 |
-
g.selectAll('path, line').attr('stroke', axisColor);
|
| 302 |
-
g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
|
| 303 |
-
});
|
| 304 |
-
gAxes.append('g')
|
| 305 |
-
.call(yAxis)
|
| 306 |
-
.call((g) => {
|
| 307 |
-
g.selectAll('path, line').attr('stroke', axisColor);
|
| 308 |
-
g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
|
| 309 |
-
});
|
| 310 |
-
|
| 311 |
-
// Axis labels (X and Y)
|
| 312 |
-
gAxes.append('text')
|
| 313 |
-
.attr('class', 'axis-label axis-label--x')
|
| 314 |
-
.attr('x', innerWidth / 2)
|
| 315 |
-
.attr('y', innerHeight + 44)
|
| 316 |
-
.attr('text-anchor', 'middle')
|
| 317 |
-
.style('font-size', '12px')
|
| 318 |
-
.style('fill', tickColor)
|
| 319 |
-
.text('Epoch');
|
| 320 |
-
gAxes.append('text')
|
| 321 |
-
.attr('class', 'axis-label axis-label--y')
|
| 322 |
-
.attr('text-anchor', 'middle')
|
| 323 |
-
.attr('transform', `translate(${-52},${innerHeight/2}) rotate(-90)`)
|
| 324 |
-
.style('font-size', '12px')
|
| 325 |
-
.style('fill', tickColor)
|
| 326 |
-
.text('Accuracy');
|
| 327 |
-
|
| 328 |
-
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 329 |
-
hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
|
| 330 |
-
|
| 331 |
-
// Legend inside plot (bottom-right), no background/border/shadow
|
| 332 |
-
const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
|
| 333 |
-
const legendHeight = 64;
|
| 334 |
-
gLegend
|
| 335 |
-
.attr('x', innerWidth - legendWidth + 42)
|
| 336 |
-
.attr('y', innerHeight - legendHeight - 12)
|
| 337 |
-
.attr('width', legendWidth)
|
| 338 |
-
.attr('height', legendHeight);
|
| 339 |
-
const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
|
| 340 |
-
Object.assign(legendRoot.node().style, {
|
| 341 |
-
background: 'transparent',
|
| 342 |
-
border: 'none',
|
| 343 |
-
borderRadius: '0',
|
| 344 |
-
padding: '0',
|
| 345 |
-
fontSize: '12px',
|
| 346 |
-
lineHeight: '1.35',
|
| 347 |
-
color: 'var(--text-color)'
|
| 348 |
-
});
|
| 349 |
-
legendRoot.html(`
|
| 350 |
-
<div style="display:flex;flex-direction:column;gap:6px;">
|
| 351 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
| 352 |
-
<span style="width:18px;height:3px;background:${colorBase};border-radius:2px;display:inline-block"></span>
|
| 353 |
-
<span>Baseline</span>
|
| 354 |
-
</div>
|
| 355 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
| 356 |
-
<span style="width:18px;height:3px;background:${colorImproved};border-radius:2px;display:inline-block"></span>
|
| 357 |
-
<span>Improved</span>
|
| 358 |
-
</div>
|
| 359 |
-
<div style="display:flex;align-items:center;gap:8px;">
|
| 360 |
-
<span style="width:18px;height:0;border-top:2px dashed ${colorTarget};display:inline-block"></span>
|
| 361 |
-
<span>Target</span>
|
| 362 |
-
</div>
|
| 363 |
-
</div>
|
| 364 |
-
`);
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
function updatePaths() {
|
| 368 |
-
pathBase.transition().duration(200).attr('d', lineGen(yBase));
|
| 369 |
-
pathImp.transition().duration(200).attr('d', lineGen(yImp));
|
| 370 |
-
pathTgt.transition().duration(200).attr('d', lineGen(yTgt));
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
function updateAlpha(a) {
|
| 374 |
-
alpha = a;
|
| 375 |
-
alphaVal.textContent = a.toFixed(2);
|
| 376 |
-
yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
|
| 377 |
-
pathImp.transition().duration(80).attr('d', lineGen(yImp));
|
| 378 |
-
}
|
| 379 |
-
|
| 380 |
-
function applyDataset() {
|
| 381 |
-
computeCurves();
|
| 382 |
-
updatePaths();
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
// Hover interactions
|
| 386 |
-
function onMove(event) {
|
| 387 |
-
const [mx, my] = d3.pointer(event, overlay.node());
|
| 388 |
-
const xi = Math.max(0, Math.min(N - 1, Math.round(xScale.invert(mx) * (N - 1))));
|
| 389 |
-
const xpx = xScale(xs[xi]);
|
| 390 |
-
const yb = yBase[xi], yi = yImp[xi], yt = yTgt[xi];
|
| 391 |
-
hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
|
| 392 |
-
hoverDotB.attr('cx', xpx).attr('cy', yScale(yb)).style('display', null);
|
| 393 |
-
hoverDotI.attr('cx', xpx).attr('cy', yScale(yi)).style('display', null);
|
| 394 |
-
hoverDotT.attr('cx', xpx).attr('cy', yScale(yt)).style('display', null);
|
| 395 |
-
|
| 396 |
-
// Tooltip content
|
| 397 |
-
const ds = datasets[datasetIndex].name;
|
| 398 |
-
tipInner.innerHTML = `<div><strong>${ds}</strong></div>` +
|
| 399 |
-
`<div><strong>x</strong> ${xs[xi].toFixed(2)}</div>` +
|
| 400 |
-
`<div><span style="display:inline-block;width:10px;height:10px;background:${colorBase};border-radius:50%;margin-right:6px;"></span><strong>Baseline</strong> ${yb.toFixed(3)}</div>` +
|
| 401 |
-
`<div><span style="display:inline-block;width:10px;height:10px;background:${colorImproved};border-radius:50%;margin-right:6px;"></span><strong>Improved</strong> ${yi.toFixed(3)}</div>` +
|
| 402 |
-
`<div><span style="display:inline-block;width:10px;height:10px;background:${colorTarget};border-radius:50%;margin-right:6px;"></span><strong>Target</strong> ${yt.toFixed(3)}</div>`;
|
| 403 |
-
const offsetX = 12, offsetY = 12;
|
| 404 |
-
tip.style.opacity = '1';
|
| 405 |
-
tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
|
| 406 |
-
}
|
| 407 |
-
|
| 408 |
-
function onLeave() {
|
| 409 |
-
tip.style.opacity = '0';
|
| 410 |
-
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 411 |
-
hoverLine.style('display', 'none');
|
| 412 |
-
hoverDotB.style('display', 'none');
|
| 413 |
-
hoverDotI.style('display', 'none');
|
| 414 |
-
hoverDotT.style('display', 'none');
|
| 415 |
-
}
|
| 416 |
-
|
| 417 |
-
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
| 418 |
-
|
| 419 |
-
// Init + controls wiring
|
| 420 |
-
computeCurves();
|
| 421 |
-
updateScales();
|
| 422 |
-
updatePaths();
|
| 423 |
-
|
| 424 |
-
// Attach controls after SVG for consistency with Plotly fragment
|
| 425 |
-
container.appendChild(controls);
|
| 426 |
-
|
| 427 |
-
selectDs.addEventListener('change', (e) => {
|
| 428 |
-
datasetIndex = parseInt(e.target.value) || 0;
|
| 429 |
-
applyDataset();
|
| 430 |
-
});
|
| 431 |
-
slider.addEventListener('input', (e) => {
|
| 432 |
-
const a = parseFloat(e.target.value) || 0;
|
| 433 |
-
updateAlpha(a);
|
| 434 |
-
});
|
| 435 |
-
|
| 436 |
-
// Resize handling
|
| 437 |
-
const render = () => {
|
| 438 |
-
updateScales();
|
| 439 |
-
updatePaths();
|
| 440 |
-
};
|
| 441 |
-
if (window.ResizeObserver) {
|
| 442 |
-
const ro = new ResizeObserver(() => render());
|
| 443 |
-
ro.observe(container);
|
| 444 |
-
} else {
|
| 445 |
-
window.addEventListener('resize', render);
|
| 446 |
-
}
|
| 447 |
-
render();
|
| 448 |
-
});
|
| 449 |
-
};
|
| 450 |
-
|
| 451 |
-
if (document.readyState === 'loading') {
|
| 452 |
-
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 453 |
-
} else { ensureD3(bootstrap); }
|
| 454 |
-
})();
|
| 455 |
-
</script>
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/{filters-quad.html → d3-line-quad.html}
RENAMED
|
@@ -1,43 +1,33 @@
|
|
| 1 |
-
<div class="
|
| 2 |
-
|
| 3 |
-
<div class="
|
| 4 |
-
<div class="filters-quad__grid">
|
| 5 |
<div class="quad-cell" data-title="Formatting Filter" data-csv="/data/formatting_filters.csv"></div>
|
| 6 |
<div class="quad-cell" data-title="Relevance Filter" data-csv="/data/relevance_filters.csv"></div>
|
| 7 |
<div class="quad-cell" data-title="Visual Dependency Filter" data-csv="/data/visual_dependency_filters.csv"></div>
|
| 8 |
<div class="quad-cell" data-title="Image Correspondence Filter" data-csv="/data/image_correspondence_filters.csv"></div>
|
| 9 |
</div>
|
| 10 |
<noscript>JavaScript is required to render these charts.</noscript>
|
|
|
|
| 11 |
</div>
|
| 12 |
<style>
|
| 13 |
-
.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
.
|
| 18 |
-
.
|
| 19 |
-
.
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
}
|
| 25 |
-
[data-theme="dark"] .filters-quad__controls select {
|
| 26 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 27 |
-
}
|
| 28 |
-
.filters-quad__controls select:hover { border-color: var(--primary-color); }
|
| 29 |
-
.filters-quad__controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 30 |
-
|
| 31 |
-
.filters-quad__legend { display:flex; align-items:center; justify-content:center; gap:12px; flex-wrap:wrap; font-size:12px; color: var(--text-color); margin: 2px 0 10px 0; }
|
| 32 |
-
.filters-quad__legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
|
| 33 |
-
.filters-quad__legend .swatch { width:10px; height:10px; border-radius:50%; display:inline-block; }
|
| 34 |
|
| 35 |
.quad-cell { border:1px solid var(--border-color); border-radius:10px; background: var(--surface-bg); display:flex; flex-direction:column; position: relative; }
|
| 36 |
/* Stacking order to ensure hover/tooltip overlays are not hidden by neighbors */
|
| 37 |
-
.
|
| 38 |
-
.
|
| 39 |
-
.
|
| 40 |
-
.
|
| 41 |
.quad-cell .cell-header { padding:8px 10px; border-bottom:1px solid var(--border-color); display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
| 42 |
.quad-cell .cell-title { font-size: 13px; font-weight: 700; color: var(--text-color); }
|
| 43 |
.quad-cell .cell-controls { display:flex; align-items:center; gap:12px; }
|
|
@@ -45,39 +35,40 @@
|
|
| 45 |
.quad-cell select {
|
| 46 |
font-size: 12px; padding: 6px 28px 6px 10px; border: 1px solid var(--border-color); border-radius: 8px;
|
| 47 |
background-color: var(--surface-bg); color: var(--text-color);
|
| 48 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='
|
| 49 |
background-repeat: no-repeat; background-position: right 8px center; background-size: 12px;
|
| 50 |
-webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;
|
| 51 |
}
|
| 52 |
[data-theme="dark"] .quad-cell select {
|
| 53 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='
|
| 54 |
}
|
| 55 |
.quad-cell select:hover { border-color: var(--primary-color); }
|
| 56 |
.quad-cell select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 57 |
.quad-cell .cell-body { position: relative; }
|
| 58 |
.quad-cell .cell-body { width:100%; overflow:hidden; }
|
| 59 |
.quad-cell .cell-body svg { max-width:100%; height:auto; }
|
| 60 |
-
|
| 61 |
-
.quad
|
| 62 |
-
.quad
|
| 63 |
-
.quad
|
|
|
|
| 64 |
/* Tooltip refined styling */
|
| 65 |
-
.
|
| 66 |
z-index: 20;
|
| 67 |
backdrop-filter: saturate(1.12) blur(8px);
|
| 68 |
}
|
| 69 |
-
.
|
| 70 |
display: flex;
|
| 71 |
flex-direction: column;
|
| 72 |
gap: 6px;
|
| 73 |
min-width: 220px;
|
| 74 |
}
|
| 75 |
-
.
|
| 76 |
font-weight: 800;
|
| 77 |
letter-spacing: 0.1px;
|
| 78 |
margin-bottom: 0;
|
| 79 |
}
|
| 80 |
-
.
|
| 81 |
font-size: 11px;
|
| 82 |
color: var(--muted-color);
|
| 83 |
display: block;
|
|
@@ -85,18 +76,110 @@
|
|
| 85 |
margin-bottom: 2px;
|
| 86 |
letter-spacing: 0.1px;
|
| 87 |
}
|
| 88 |
-
.
|
| 89 |
padding-top: 6px;
|
| 90 |
border-top: 1px solid var(--border-color);
|
| 91 |
}
|
| 92 |
-
.
|
| 93 |
display: inline-block;
|
| 94 |
vertical-align: middle;
|
| 95 |
margin-right: 2px;
|
| 96 |
}
|
| 97 |
-
.
|
| 98 |
margin-right: 6px;
|
| 99 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
</style>
|
| 101 |
<script>
|
| 102 |
(() => {
|
|
@@ -144,7 +227,7 @@
|
|
| 144 |
const gLines = gRoot.append('g').attr('class','lines');
|
| 145 |
const gPoints = gRoot.append('g').attr('class','points');
|
| 146 |
const gHover = gRoot.append('g').attr('class','hover');
|
| 147 |
-
|
| 148 |
|
| 149 |
// Tooltip
|
| 150 |
cell.style.position = cell.style.position || 'relative';
|
|
@@ -189,44 +272,27 @@
|
|
| 189 |
// Colors and markers (match original embeds)
|
| 190 |
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 191 |
const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
|
| 192 |
-
|
| 193 |
-
const markerSize = 6;
|
| 194 |
-
function drawMarker(selection, shape, size) {
|
| 195 |
-
const s = size / 2;
|
| 196 |
-
switch (shape) {
|
| 197 |
-
case 'circle':
|
| 198 |
-
return selection.append('circle').attr('r', s);
|
| 199 |
-
case 'square':
|
| 200 |
-
return selection.append('rect').attr('x', -s).attr('y', -s).attr('width', size).attr('height', size);
|
| 201 |
-
case 'triangle':
|
| 202 |
-
return selection.append('path').attr('d', `M0,${-s * 1.2} L${s * 1.1},${s * 0.6} L${-s * 1.1},${s * 0.6} Z`);
|
| 203 |
-
case 'diamond':
|
| 204 |
-
return selection.append('path').attr('d', `M0,${-s * 1.2} L${s * 1.1},0 L0,${s * 1.2} L${-s * 1.1},0 Z`);
|
| 205 |
-
case 'inverted-triangle':
|
| 206 |
-
return selection.append('path').attr('d', `M0,${s * 1.2} L${s * 1.1},${-s * 0.6} L${-s * 1.1},${-s * 0.6} Z`);
|
| 207 |
-
default:
|
| 208 |
-
return selection.append('circle').attr('r', s);
|
| 209 |
-
}
|
| 210 |
-
}
|
| 211 |
-
// Inline SVG for tooltip shapes
|
| 212 |
-
function markerSVG(shape, color) {
|
| 213 |
-
const size = 12; const s = size / 2; const stroke = color;
|
| 214 |
-
if (shape === 'circle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 215 |
-
if (shape === 'square') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><rect x="-5" y="-5" width="10" height="10" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 216 |
-
if (shape === 'triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L5,3 L-5,3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 217 |
-
if (shape === 'diamond') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L6,0 L0,6 L-6,0 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 218 |
-
if (shape === 'inverted-triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,6 L5,-3 L-5,-3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 219 |
-
return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 220 |
-
}
|
| 221 |
// Ready signal for async load completion
|
| 222 |
let readyResolve = null;
|
| 223 |
const ready = new Promise((res)=> { readyResolve = res; });
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
function updateScales(){
|
| 226 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 227 |
-
const axisColor =
|
| 228 |
-
const tickColor =
|
| 229 |
-
const gridColor =
|
| 230 |
|
| 231 |
const rect = cell.getBoundingClientRect();
|
| 232 |
width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
|
|
@@ -245,31 +311,22 @@
|
|
| 245 |
gGrid.selectAll('*').remove();
|
| 246 |
gGrid.selectAll('line').data(yTicks).join('line')
|
| 247 |
.attr('x1',0).attr('x2',innerWidth).attr('y1',d=>yScale(d)).attr('y2',d=>yScale(d))
|
| 248 |
-
.attr('stroke',gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
|
| 249 |
|
| 250 |
// Axes
|
| 251 |
gAxes.selectAll('*').remove();
|
| 252 |
let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
|
| 253 |
-
|
| 254 |
-
const abs = Math.abs(v);
|
| 255 |
-
if (abs >= 1000) {
|
| 256 |
-
const n = v / 1000;
|
| 257 |
-
const s = d3.format('.1f')(n);
|
| 258 |
-
return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
|
| 259 |
-
}
|
| 260 |
-
return d3.format('d')(v);
|
| 261 |
-
};
|
| 262 |
-
xAxis = xAxis.tickFormat(fmtK);
|
| 263 |
const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
|
| 264 |
-
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
|
| 265 |
-
gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
|
| 266 |
|
| 267 |
// Axis labels
|
| 268 |
gAxes.append('text')
|
| 269 |
.attr('class', 'x-axis-label')
|
| 270 |
.attr('x', innerWidth / 2)
|
| 271 |
.attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))
|
| 272 |
-
.attr('fill',
|
| 273 |
.attr('text-anchor', 'middle')
|
| 274 |
.style('font-size', '12px')
|
| 275 |
.style('font-weight', '700')
|
|
@@ -280,18 +337,12 @@
|
|
| 280 |
.attr('transform', 'rotate(-90)')
|
| 281 |
.attr('x', -innerHeight / 2)
|
| 282 |
.attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
|
| 283 |
-
.attr('fill',
|
| 284 |
.attr('text-anchor', 'middle')
|
| 285 |
.style('font-size', '12px')
|
| 286 |
.style('font-weight', '700')
|
| 287 |
.text(axisLabelY);
|
| 288 |
|
| 289 |
-
// Legend box (top-right)
|
| 290 |
-
// Per-cell legend hidden; global legend is used
|
| 291 |
-
const legendWidth = 0, legendHeight = 0;
|
| 292 |
-
gLegend.attr('x', 0).attr('y', 0).attr('width', legendWidth).attr('height', legendHeight);
|
| 293 |
-
const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
|
| 294 |
-
Object.assign(legendRoot.node().style, { width:'0px', height:'0px', display:'none' });
|
| 295 |
return { innerWidth, innerHeight, tickColor };
|
| 296 |
}
|
| 297 |
|
|
@@ -322,7 +373,7 @@
|
|
| 322 |
axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
|
| 323 |
const { innerWidth, innerHeight } = updateScales();
|
| 324 |
|
| 325 |
-
const series = runs.map((r, i) => ({ run:r, color: pool[i % pool.length],
|
| 326 |
|
| 327 |
// zones ± stderr (métriques non rank)
|
| 328 |
gAreas.selectAll('*').remove();
|
|
@@ -334,43 +385,70 @@
|
|
| 334 |
const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);
|
| 335 |
const coords = upper.concat(lower);
|
| 336 |
const pathData = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed)(coords);
|
| 337 |
-
gAreas.append('path')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
});
|
| 339 |
}
|
| 340 |
|
| 341 |
const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
|
| 342 |
-
paths.enter()
|
| 343 |
-
.
|
| 344 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
paths.exit().remove();
|
| 346 |
|
| 347 |
-
|
| 348 |
-
series.
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
// No per-cell legend content (handled globally)
|
| 362 |
|
| 363 |
// Hover
|
| 364 |
gHover.selectAll('*').remove();
|
| 365 |
const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
|
| 366 |
-
const hoverLine = gHover.append('line').
|
| 367 |
const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);
|
| 368 |
function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
|
| 369 |
-
let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${nearest}</div>`;
|
| 370 |
-
const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color,
|
| 371 |
entries.sort((a,b)=> (a.pt.value - b.pt.value));
|
| 372 |
const fmt = (vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4));
|
| 373 |
-
entries.forEach(e => {
|
|
|
|
|
|
|
|
|
|
| 374 |
tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`; }
|
| 375 |
function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }, 100); }
|
| 376 |
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
|
@@ -418,7 +496,7 @@
|
|
| 418 |
const isRank = /rank/i.test(key); const isAverage = /average/i.test(key); const isRankStrict = isRank && !isAverage;
|
| 419 |
runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });
|
| 420 |
const rankMax = isRank ? Math.max(1, Math.round(maxVal)) : null;
|
| 421 |
-
return { isRank, isRankStrict, min: minVal, max: maxVal, rankMax };
|
| 422 |
},
|
| 423 |
setSharedY: (cfg) => { sharedYConfig = cfg || null; if (metricList && metricList.length) { /* re-render last metric if possible */ const current = cfg && cfg.key ? cfg.key : null; const m = current || metricList[0]; renderMetric(m); } }
|
| 424 |
};
|
|
@@ -427,23 +505,30 @@
|
|
| 427 |
const bootstrap = () => {
|
| 428 |
const scriptEl = THIS_SCRIPT;
|
| 429 |
let host = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
// Try finding within parent (fragment mount is inside parent)
|
| 431 |
if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {
|
| 432 |
-
host = scriptEl.parentElement.querySelector('.
|
| 433 |
}
|
| 434 |
// Fallback: scan previous siblings
|
| 435 |
if (!host) {
|
| 436 |
let sib = scriptEl && scriptEl.previousElementSibling;
|
| 437 |
-
while (sib && !(sib.classList && sib.classList.contains('
|
| 438 |
sib = sib.previousElementSibling;
|
| 439 |
}
|
| 440 |
host = sib || null;
|
| 441 |
}
|
| 442 |
// Last resort: global query
|
| 443 |
-
if (!host) { host = document.querySelector('.
|
| 444 |
if (!host) return;
|
| 445 |
if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
|
| 446 |
const cells = host.querySelectorAll('.quad-cell'); if (!cells.length) return;
|
|
|
|
| 447 |
const instances = Array.from(cells).map(cell => initRunLine(cell));
|
| 448 |
|
| 449 |
(async () => {
|
|
@@ -455,14 +540,15 @@
|
|
| 455 |
if (!metrics.length) { metrics = lists[0] || []; }
|
| 456 |
const def = (metrics.includes('ai2d_exact_match') ? 'ai2d_exact_match' : (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || ''));
|
| 457 |
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
const
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
| 466 |
|
| 467 |
const computeAndApplySharedY = (metric) => {
|
| 468 |
try {
|
|
@@ -482,11 +568,11 @@
|
|
| 482 |
|
| 483 |
const applyAll = (v) => { computeAndApplySharedY(v); instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); };
|
| 484 |
if (def) applyAll(def);
|
| 485 |
-
select.addEventListener('change', () => applyAll(select.value));
|
| 486 |
|
| 487 |
-
//
|
| 488 |
-
const
|
| 489 |
-
if (
|
| 490 |
try {
|
| 491 |
const f = '/data/formatting_filters.csv';
|
| 492 |
const r = await fetch(f, { cache:'no-cache' });
|
|
@@ -496,21 +582,32 @@
|
|
| 496 |
const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
|
| 497 |
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 498 |
const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])];
|
| 499 |
-
|
| 500 |
-
const shapeSVG = (shape, color) => {
|
| 501 |
-
const size = 12; const s = size/2; const stroke = color;
|
| 502 |
-
if (shape === 'circle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 503 |
-
if (shape === 'square') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><rect x="-5" y="-5" width="10" height="10" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 504 |
-
if (shape === 'triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L5,3 L-5,3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 505 |
-
if (shape === 'diamond') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,-6 L6,0 L0,6 L-6,0 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 506 |
-
if (shape === 'inverted-triangle') return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><path d="M0,6 L5,-3 L-5,-3 Z" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 507 |
-
return `<svg width="12" height="12" viewBox="-6 -6 12 12" aria-hidden="true"><circle r="5" fill="${color}" stroke="${stroke}" stroke-width="1" /></svg>`;
|
| 508 |
-
};
|
| 509 |
-
legendHost.innerHTML = runList.map((name, i)=> {
|
| 510 |
const color = pool[i % pool.length];
|
| 511 |
-
|
| 512 |
-
return `<span class="item">${shapeSVG(shape, color)}<span>${name}</span></span>`;
|
| 513 |
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
}
|
| 515 |
} catch {}
|
| 516 |
}
|
|
@@ -521,3 +618,5 @@
|
|
| 521 |
})();
|
| 522 |
</script>
|
| 523 |
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="line-quad">
|
| 2 |
+
|
| 3 |
+
<div class="line-quad__grid">
|
|
|
|
| 4 |
<div class="quad-cell" data-title="Formatting Filter" data-csv="/data/formatting_filters.csv"></div>
|
| 5 |
<div class="quad-cell" data-title="Relevance Filter" data-csv="/data/relevance_filters.csv"></div>
|
| 6 |
<div class="quad-cell" data-title="Visual Dependency Filter" data-csv="/data/visual_dependency_filters.csv"></div>
|
| 7 |
<div class="quad-cell" data-title="Image Correspondence Filter" data-csv="/data/image_correspondence_filters.csv"></div>
|
| 8 |
</div>
|
| 9 |
<noscript>JavaScript is required to render these charts.</noscript>
|
| 10 |
+
|
| 11 |
</div>
|
| 12 |
<style>
|
| 13 |
+
.line-quad { position: relative; }
|
| 14 |
+
/* Axis/tick/grid use global variables from _variables.css */
|
| 15 |
+
/* Apply axis/tick/grid purely via CSS */
|
| 16 |
+
.line-quad .axes path,
|
| 17 |
+
.line-quad .axes line { stroke: var(--axis-color); }
|
| 18 |
+
.line-quad .axes text { fill: var(--tick-color); }
|
| 19 |
+
.line-quad .grid line { stroke: var(--grid-color); }
|
| 20 |
+
.line-quad__grid { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
| 21 |
+
@media (max-width: 980px) { .line-quad__grid { grid-template-columns: 1fr; } }
|
| 22 |
+
|
| 23 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
.quad-cell { border:1px solid var(--border-color); border-radius:10px; background: var(--surface-bg); display:flex; flex-direction:column; position: relative; }
|
| 26 |
/* Stacking order to ensure hover/tooltip overlays are not hidden by neighbors */
|
| 27 |
+
.line-quad__grid .quad-cell:nth-child(1) { z-index: 4; } /* top-left */
|
| 28 |
+
.line-quad__grid .quad-cell:nth-child(3) { z-index: 3; } /* bottom-left */
|
| 29 |
+
.line-quad__grid .quad-cell:nth-child(2) { z-index: 2; } /* top-right */
|
| 30 |
+
.line-quad__grid .quad-cell:nth-child(4) { z-index: 1; } /* bottom-right */
|
| 31 |
.quad-cell .cell-header { padding:8px 10px; border-bottom:1px solid var(--border-color); display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
| 32 |
.quad-cell .cell-title { font-size: 13px; font-weight: 700; color: var(--text-color); }
|
| 33 |
.quad-cell .cell-controls { display:flex; align-items:center; gap:12px; }
|
|
|
|
| 35 |
.quad-cell select {
|
| 36 |
font-size: 12px; padding: 6px 28px 6px 10px; border: 1px solid var(--border-color); border-radius: 8px;
|
| 37 |
background-color: var(--surface-bg); color: var(--text-color);
|
| 38 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 39 |
background-repeat: no-repeat; background-position: right 8px center; background-size: 12px;
|
| 40 |
-webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;
|
| 41 |
}
|
| 42 |
[data-theme="dark"] .quad-cell select {
|
| 43 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 44 |
}
|
| 45 |
.quad-cell select:hover { border-color: var(--primary-color); }
|
| 46 |
.quad-cell select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 47 |
.quad-cell .cell-body { position: relative; }
|
| 48 |
.quad-cell .cell-body { width:100%; overflow:hidden; }
|
| 49 |
.quad-cell .cell-body svg { max-width:100%; height:auto; }
|
| 50 |
+
|
| 51 |
+
.line-quad.hovering .lines path.ghost { opacity: .25; }
|
| 52 |
+
.line-quad.hovering .points circle.ghost { opacity: .25; }
|
| 53 |
+
.line-quad.hovering .areas path.ghost { opacity: .08; }
|
| 54 |
+
.line-quad.hovering .legend-bottom .item.ghost { opacity: .35; }
|
| 55 |
/* Tooltip refined styling */
|
| 56 |
+
.line-quad .d3-tooltip {
|
| 57 |
z-index: 20;
|
| 58 |
backdrop-filter: saturate(1.12) blur(8px);
|
| 59 |
}
|
| 60 |
+
.line-quad .d3-tooltip__inner {
|
| 61 |
display: flex;
|
| 62 |
flex-direction: column;
|
| 63 |
gap: 6px;
|
| 64 |
min-width: 220px;
|
| 65 |
}
|
| 66 |
+
.line-quad .d3-tooltip__inner > div:first-child {
|
| 67 |
font-weight: 800;
|
| 68 |
letter-spacing: 0.1px;
|
| 69 |
margin-bottom: 0;
|
| 70 |
}
|
| 71 |
+
.line-quad .d3-tooltip__inner > div:nth-child(2) {
|
| 72 |
font-size: 11px;
|
| 73 |
color: var(--muted-color);
|
| 74 |
display: block;
|
|
|
|
| 76 |
margin-bottom: 2px;
|
| 77 |
letter-spacing: 0.1px;
|
| 78 |
}
|
| 79 |
+
.line-quad .d3-tooltip__inner > div:nth-child(n+3) {
|
| 80 |
padding-top: 6px;
|
| 81 |
border-top: 1px solid var(--border-color);
|
| 82 |
}
|
| 83 |
+
.line-quad .d3-tooltip__inner svg {
|
| 84 |
display: inline-block;
|
| 85 |
vertical-align: middle;
|
| 86 |
margin-right: 2px;
|
| 87 |
}
|
| 88 |
+
.line-quad .d3-tooltip__inner strong {
|
| 89 |
margin-right: 6px;
|
| 90 |
}
|
| 91 |
+
.line-quad .d3-tooltip__color-dot {
|
| 92 |
+
display: inline-block;
|
| 93 |
+
width: 12px;
|
| 94 |
+
height: 12px;
|
| 95 |
+
border-radius: 3px;
|
| 96 |
+
border: 1px solid var(--border-color);
|
| 97 |
+
}
|
| 98 |
+
/* Header layout (like d3-line-simple) */
|
| 99 |
+
.line-quad__header {
|
| 100 |
+
display: flex;
|
| 101 |
+
align-items: flex-start;
|
| 102 |
+
justify-content: flex-start;
|
| 103 |
+
gap: 12px;
|
| 104 |
+
margin: 8px 0 0 0;
|
| 105 |
+
flex-wrap: wrap;
|
| 106 |
+
}
|
| 107 |
+
.line-quad__header .legend-bottom {
|
| 108 |
+
display: flex;
|
| 109 |
+
flex-direction: column;
|
| 110 |
+
align-items: flex-start;
|
| 111 |
+
gap: 6px;
|
| 112 |
+
font-size: 12px;
|
| 113 |
+
color: var(--text-color);
|
| 114 |
+
}
|
| 115 |
+
.line-quad__header .legend-bottom .legend-title {
|
| 116 |
+
font-size: 12px;
|
| 117 |
+
font-weight: 700;
|
| 118 |
+
color: var(--text-color);
|
| 119 |
+
}
|
| 120 |
+
.line-quad__header .legend-bottom .items {
|
| 121 |
+
display: flex;
|
| 122 |
+
flex-wrap: wrap;
|
| 123 |
+
gap: 8px 14px;
|
| 124 |
+
}
|
| 125 |
+
.line-quad__header .legend-bottom .item {
|
| 126 |
+
display: inline-flex;
|
| 127 |
+
align-items: center;
|
| 128 |
+
gap: 6px;
|
| 129 |
+
white-space: nowrap;
|
| 130 |
+
}
|
| 131 |
+
.line-quad__header .legend-bottom .swatch {
|
| 132 |
+
width: 14px;
|
| 133 |
+
height: 14px;
|
| 134 |
+
border-radius: 3px;
|
| 135 |
+
border: 1px solid var(--border-color);
|
| 136 |
+
display: inline-block;
|
| 137 |
+
}
|
| 138 |
+
.line-quad .controls {
|
| 139 |
+
margin-top: 0;
|
| 140 |
+
display: flex;
|
| 141 |
+
gap: 16px;
|
| 142 |
+
align-items: center;
|
| 143 |
+
justify-content: flex-end;
|
| 144 |
+
width: auto;
|
| 145 |
+
flex-wrap: wrap;
|
| 146 |
+
}
|
| 147 |
+
.line-quad .controls .control-group {
|
| 148 |
+
display: flex;
|
| 149 |
+
flex-direction: column;
|
| 150 |
+
align-items: flex-start;
|
| 151 |
+
gap: 6px;
|
| 152 |
+
}
|
| 153 |
+
.line-quad .controls label {
|
| 154 |
+
font-size: 12px;
|
| 155 |
+
color: var(--text-color);
|
| 156 |
+
display: flex;
|
| 157 |
+
align-items: center;
|
| 158 |
+
gap: 6px;
|
| 159 |
+
white-space: nowrap;
|
| 160 |
+
font-weight: 700;
|
| 161 |
+
}
|
| 162 |
+
.line-quad .controls select {
|
| 163 |
+
font-size: 12px;
|
| 164 |
+
padding: 8px 28px 8px 10px;
|
| 165 |
+
border: 1px solid var(--border-color);
|
| 166 |
+
border-radius: 8px;
|
| 167 |
+
background-color: var(--surface-bg);
|
| 168 |
+
color: var(--text-color);
|
| 169 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 170 |
+
background-repeat: no-repeat;
|
| 171 |
+
background-position: right 8px center;
|
| 172 |
+
background-size: 12px;
|
| 173 |
+
-webkit-appearance: none;
|
| 174 |
+
appearance: none;
|
| 175 |
+
cursor: pointer;
|
| 176 |
+
transition: border-color .15s ease, box-shadow .15s ease;
|
| 177 |
+
}
|
| 178 |
+
[data-theme="dark"] .line-quad .controls select {
|
| 179 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 180 |
+
}
|
| 181 |
+
.line-quad .controls select:hover { border-color: var(--primary-color); }
|
| 182 |
+
.line-quad .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 183 |
</style>
|
| 184 |
<script>
|
| 185 |
(() => {
|
|
|
|
| 227 |
const gLines = gRoot.append('g').attr('class','lines');
|
| 228 |
const gPoints = gRoot.append('g').attr('class','points');
|
| 229 |
const gHover = gRoot.append('g').attr('class','hover');
|
| 230 |
+
// Removed per-cell legend; using global footer legend
|
| 231 |
|
| 232 |
// Tooltip
|
| 233 |
cell.style.position = cell.style.position || 'relative';
|
|
|
|
| 272 |
// Colors and markers (match original embeds)
|
| 273 |
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 274 |
const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
|
| 275 |
+
// Shapes supprimés: on n'utilise que la couleur
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
// Ready signal for async load completion
|
| 277 |
let readyResolve = null;
|
| 278 |
const ready = new Promise((res)=> { readyResolve = res; });
|
| 279 |
|
| 280 |
+
// Shared formatter for thousands: 5000 -> 5k, 1500 -> 1.5k (trim .0)
|
| 281 |
+
const formatK = (v) => {
|
| 282 |
+
const abs = Math.abs(v);
|
| 283 |
+
if (abs >= 1000) {
|
| 284 |
+
const n = v / 1000;
|
| 285 |
+
const s = d3.format('.1f')(n);
|
| 286 |
+
return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
|
| 287 |
+
}
|
| 288 |
+
return d3.format('d')(v);
|
| 289 |
+
};
|
| 290 |
+
|
| 291 |
function updateScales(){
|
| 292 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 293 |
+
const axisColor = 'var(--axis-color)';
|
| 294 |
+
const tickColor = 'var(--tick-color)';
|
| 295 |
+
const gridColor = 'var(--grid-color)';
|
| 296 |
|
| 297 |
const rect = cell.getBoundingClientRect();
|
| 298 |
width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
|
|
|
|
| 311 |
gGrid.selectAll('*').remove();
|
| 312 |
gGrid.selectAll('line').data(yTicks).join('line')
|
| 313 |
.attr('x1',0).attr('x2',innerWidth).attr('y1',d=>yScale(d)).attr('y2',d=>yScale(d))
|
| 314 |
+
.attr('stroke', gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');
|
| 315 |
|
| 316 |
// Axes
|
| 317 |
gAxes.selectAll('*').remove();
|
| 318 |
let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
|
| 319 |
+
xAxis = xAxis.tickFormat(formatK);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
|
| 321 |
+
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','11px'); });
|
| 322 |
+
gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','11px'); });
|
| 323 |
|
| 324 |
// Axis labels
|
| 325 |
gAxes.append('text')
|
| 326 |
.attr('class', 'x-axis-label')
|
| 327 |
.attr('x', innerWidth / 2)
|
| 328 |
.attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))
|
| 329 |
+
.attr('fill', 'var(--text-color)')
|
| 330 |
.attr('text-anchor', 'middle')
|
| 331 |
.style('font-size', '12px')
|
| 332 |
.style('font-weight', '700')
|
|
|
|
| 337 |
.attr('transform', 'rotate(-90)')
|
| 338 |
.attr('x', -innerHeight / 2)
|
| 339 |
.attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
|
| 340 |
+
.attr('fill', 'var(--text-color)')
|
| 341 |
.attr('text-anchor', 'middle')
|
| 342 |
.style('font-size', '12px')
|
| 343 |
.style('font-weight', '700')
|
| 344 |
.text(axisLabelY);
|
| 345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
return { innerWidth, innerHeight, tickColor };
|
| 347 |
}
|
| 348 |
|
|
|
|
| 373 |
axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
|
| 374 |
const { innerWidth, innerHeight } = updateScales();
|
| 375 |
|
| 376 |
+
const series = runs.map((r, i) => ({ run:r, color: pool[i % pool.length], values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
|
| 377 |
|
| 378 |
// zones ± stderr (métriques non rank)
|
| 379 |
gAreas.selectAll('*').remove();
|
|
|
|
| 385 |
const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);
|
| 386 |
const coords = upper.concat(lower);
|
| 387 |
const pathData = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed)(coords);
|
| 388 |
+
gAreas.append('path')
|
| 389 |
+
.attr('class','area')
|
| 390 |
+
.attr('data-run', s.run)
|
| 391 |
+
.attr('d', pathData)
|
| 392 |
+
.attr('fill', s.color)
|
| 393 |
+
.attr('opacity', 0)
|
| 394 |
+
.attr('stroke', 'none')
|
| 395 |
+
.transition().duration(450).ease(d3.easeCubicOut)
|
| 396 |
+
.attr('opacity', 0.15);
|
| 397 |
});
|
| 398 |
}
|
| 399 |
|
| 400 |
const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
|
| 401 |
+
paths.enter()
|
| 402 |
+
.append('path')
|
| 403 |
+
.attr('class','run-line')
|
| 404 |
+
.attr('data-run', d=>d.run)
|
| 405 |
+
.attr('fill','none')
|
| 406 |
+
.attr('stroke-width', 1)
|
| 407 |
+
.attr('opacity',0)
|
| 408 |
+
.attr('stroke', d=>d.color)
|
| 409 |
+
.attr('d', d=>lineGen(d.values))
|
| 410 |
+
.transition().duration(450).ease(d3.easeCubicOut)
|
| 411 |
+
.attr('opacity',0.9);
|
| 412 |
+
paths
|
| 413 |
+
.transition().duration(450).ease(d3.easeCubicOut)
|
| 414 |
+
.attr('stroke', d=>d.color)
|
| 415 |
+
.attr('opacity',0.9)
|
| 416 |
+
.attr('d', d=>lineGen(d.values));
|
| 417 |
paths.exit().remove();
|
| 418 |
|
| 419 |
+
// Draw light point markers at each data sample (subtle)
|
| 420 |
+
const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
|
| 421 |
+
const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
|
| 422 |
+
ptsSel.enter().append('circle').attr('class','pt')
|
| 423 |
+
.attr('data-run', d=>d.run)
|
| 424 |
+
.attr('r', 2)
|
| 425 |
+
.attr('fill', d=>d.color)
|
| 426 |
+
.attr('fill-opacity', 0.6)
|
| 427 |
+
.attr('stroke', 'none')
|
| 428 |
+
.attr('cx', d=>xScale(d.step))
|
| 429 |
+
.attr('cy', d=>yScale(d.value))
|
| 430 |
+
.merge(ptsSel)
|
| 431 |
+
.transition().duration(180)
|
| 432 |
+
.attr('cx', d=>xScale(d.step))
|
| 433 |
+
.attr('cy', d=>yScale(d.value));
|
| 434 |
+
ptsSel.exit().remove();
|
| 435 |
|
| 436 |
// No per-cell legend content (handled globally)
|
| 437 |
|
| 438 |
// Hover
|
| 439 |
gHover.selectAll('*').remove();
|
| 440 |
const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
|
| 441 |
+
const hoverLine = gHover.append('line').style('stroke','var(--text-color)').attr('stroke-opacity', 0.25).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
|
| 442 |
const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);
|
| 443 |
function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
|
| 444 |
+
let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${formatK(nearest)}</div>`;
|
| 445 |
+
const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null);
|
| 446 |
entries.sort((a,b)=> (a.pt.value - b.pt.value));
|
| 447 |
const fmt = (vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4));
|
| 448 |
+
entries.forEach(e => {
|
| 449 |
+
const err = (e.pt.stderr!=null && isFinite(e.pt.stderr) && e.pt.stderr>0) ? ` ± ${fmt(e.pt.stderr)}` : '';
|
| 450 |
+
html += `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}${err}</span></div>`;
|
| 451 |
+
});
|
| 452 |
tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`; }
|
| 453 |
function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }, 100); }
|
| 454 |
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
|
|
|
| 496 |
const isRank = /rank/i.test(key); const isAverage = /average/i.test(key); const isRankStrict = isRank && !isAverage;
|
| 497 |
runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });
|
| 498 |
const rankMax = isRank ? Math.max(1, Math.round(maxVal)) : null;
|
| 499 |
+
return { isRank, isRankStrict, min: maxVal === 0 && minVal === Infinity ? null : minVal, max: maxVal, rankMax };
|
| 500 |
},
|
| 501 |
setSharedY: (cfg) => { sharedYConfig = cfg || null; if (metricList && metricList.length) { /* re-render last metric if possible */ const current = cfg && cfg.key ? cfg.key : null; const m = current || metricList[0]; renderMetric(m); } }
|
| 502 |
};
|
|
|
|
| 505 |
const bootstrap = () => {
|
| 506 |
const scriptEl = THIS_SCRIPT;
|
| 507 |
let host = null;
|
| 508 |
+
// Build header (legend + controls) and append after grid
|
| 509 |
+
const header = document.createElement('div'); header.className = 'line-quad__header';
|
| 510 |
+
const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>';
|
| 511 |
+
const controls = document.createElement('div'); controls.className = 'controls'; controls.innerHTML = '<div class="control-group"><label>Metric</label><select></select></div>';
|
| 512 |
+
header.appendChild(legend);
|
| 513 |
+
header.appendChild(controls);
|
| 514 |
// Try finding within parent (fragment mount is inside parent)
|
| 515 |
if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {
|
| 516 |
+
host = scriptEl.parentElement.querySelector('.line-quad');
|
| 517 |
}
|
| 518 |
// Fallback: scan previous siblings
|
| 519 |
if (!host) {
|
| 520 |
let sib = scriptEl && scriptEl.previousElementSibling;
|
| 521 |
+
while (sib && !(sib.classList && sib.classList.contains('line-quad'))) {
|
| 522 |
sib = sib.previousElementSibling;
|
| 523 |
}
|
| 524 |
host = sib || null;
|
| 525 |
}
|
| 526 |
// Last resort: global query
|
| 527 |
+
if (!host) { host = document.querySelector('.line-quad'); }
|
| 528 |
if (!host) return;
|
| 529 |
if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
|
| 530 |
const cells = host.querySelectorAll('.quad-cell'); if (!cells.length) return;
|
| 531 |
+
host.appendChild(header);
|
| 532 |
const instances = Array.from(cells).map(cell => initRunLine(cell));
|
| 533 |
|
| 534 |
(async () => {
|
|
|
|
| 540 |
if (!metrics.length) { metrics = lists[0] || []; }
|
| 541 |
const def = (metrics.includes('ai2d_exact_match') ? 'ai2d_exact_match' : (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || ''));
|
| 542 |
|
| 543 |
+
// Wire header controls (select under "Metric" label)
|
| 544 |
+
const headerEl = host.querySelector('.line-quad__header');
|
| 545 |
+
if (headerEl && !headerEl.isConnected) host.appendChild(header);
|
| 546 |
+
const select = (headerEl || header).querySelector('.controls select');
|
| 547 |
+
if (select) {
|
| 548 |
+
select.innerHTML = '';
|
| 549 |
+
metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=prettyMetricLabel(m); select.appendChild(o); });
|
| 550 |
+
if (def) select.value = def;
|
| 551 |
+
}
|
| 552 |
|
| 553 |
const computeAndApplySharedY = (metric) => {
|
| 554 |
try {
|
|
|
|
| 568 |
|
| 569 |
const applyAll = (v) => { computeAndApplySharedY(v); instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); };
|
| 570 |
if (def) applyAll(def);
|
| 571 |
+
if (select) select.addEventListener('change', () => applyAll(select.value));
|
| 572 |
|
| 573 |
+
// Global legend (in header, colors only)
|
| 574 |
+
const legendItemsHost = (headerEl || header).querySelector('.legend-bottom .items');
|
| 575 |
+
if (legendItemsHost) {
|
| 576 |
try {
|
| 577 |
const f = '/data/formatting_filters.csv';
|
| 578 |
const r = await fetch(f, { cache:'no-cache' });
|
|
|
|
| 582 |
const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
|
| 583 |
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 584 |
const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])];
|
| 585 |
+
legendItemsHost.innerHTML = runList.map((name, i)=> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
const color = pool[i % pool.length];
|
| 587 |
+
return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
|
|
|
|
| 588 |
}).join('');
|
| 589 |
+
// Legend hover ghosting across all cells
|
| 590 |
+
legendItemsHost.querySelectorAll('.item').forEach(el => {
|
| 591 |
+
el.addEventListener('mouseenter', () => {
|
| 592 |
+
const run = el.getAttribute('data-run'); if (!run) return;
|
| 593 |
+
host.classList.add('hovering');
|
| 594 |
+
host.querySelectorAll('.quad-cell').forEach(cell => {
|
| 595 |
+
cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
|
| 596 |
+
cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
|
| 597 |
+
cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run));
|
| 598 |
+
});
|
| 599 |
+
legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
|
| 600 |
+
});
|
| 601 |
+
el.addEventListener('mouseleave', () => {
|
| 602 |
+
host.classList.remove('hovering');
|
| 603 |
+
host.querySelectorAll('.quad-cell').forEach(cell => {
|
| 604 |
+
cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost'));
|
| 605 |
+
cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost'));
|
| 606 |
+
cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost'));
|
| 607 |
+
});
|
| 608 |
+
legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
|
| 609 |
+
});
|
| 610 |
+
});
|
| 611 |
}
|
| 612 |
} catch {}
|
| 613 |
}
|
|
|
|
| 618 |
})();
|
| 619 |
</script>
|
| 620 |
|
| 621 |
+
|
| 622 |
+
|
app/src/content/embeds/d3-line.html
CHANGED
|
@@ -1,6 +1,36 @@
|
|
| 1 |
-
<div class="d3-line"
|
| 2 |
<style>
|
| 3 |
-
.d3-line
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
font-size: 12px;
|
| 5 |
padding: 8px 28px 8px 10px;
|
| 6 |
border: 1px solid var(--border-color);
|
|
@@ -8,505 +38,431 @@
|
|
| 8 |
background-color: var(--surface-bg);
|
| 9 |
color: var(--text-color);
|
| 10 |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 11 |
-
background-repeat: no-repeat;
|
| 12 |
-
|
| 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 .
|
| 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 .
|
| 24 |
-
|
| 25 |
-
}
|
| 26 |
-
.d3-line .d3-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
}
|
| 31 |
-
.d3-line .
|
| 32 |
-
|
| 33 |
-
/*
|
| 34 |
-
.d3-line .
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
}
|
| 43 |
-
.d3-line .
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 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 .
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
}
|
| 64 |
-
.d3-line .
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
background: var(--primary-color);
|
| 69 |
-
border: 2px solid var(--on-primary);
|
| 70 |
-
cursor: pointer;
|
| 71 |
}
|
| 72 |
-
/*
|
| 73 |
-
.d3-line .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
</style>
|
| 75 |
<script>
|
| 76 |
(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
const ensureD3 = (cb) => {
|
| 78 |
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 79 |
let s = document.getElementById('d3-cdn-script');
|
| 80 |
-
if (!s) {
|
| 81 |
-
s = document.createElement('script');
|
| 82 |
-
s.id = 'd3-cdn-script';
|
| 83 |
-
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 84 |
-
document.head.appendChild(s);
|
| 85 |
-
}
|
| 86 |
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 87 |
-
s.addEventListener('load', onReady, { once: true });
|
| 88 |
-
if (window.d3) onReady();
|
| 89 |
};
|
| 90 |
|
| 91 |
const bootstrap = () => {
|
| 92 |
const scriptEl = document.currentScript;
|
| 93 |
-
let container = null;
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
let el = scriptEl.previousElementSibling;
|
| 98 |
-
while (el && !(el.classList && el.classList.contains('d3-line'))) {
|
| 99 |
-
el = el.previousElementSibling;
|
| 100 |
-
}
|
| 101 |
-
if (el) container = el;
|
| 102 |
}
|
| 103 |
-
|
| 104 |
-
// Fallback: pick the last unmounted .d3-line in the document
|
| 105 |
-
if (!container) {
|
| 106 |
-
const candidates = Array.from(document.querySelectorAll('.d3-line'))
|
| 107 |
-
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 108 |
-
container = candidates[candidates.length - 1] || null;
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
if (!container) return;
|
| 112 |
-
if (container.dataset) {
|
| 113 |
-
if (container.dataset.mounted === 'true') return;
|
| 114 |
-
container.dataset.mounted = 'true';
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
// CSV: prefer public path, fallback to relative
|
| 118 |
-
const CSV_PATHS = [
|
| 119 |
-
'/data/against_baselines.csv',
|
| 120 |
-
'./assets/data/against_baselines.csv',
|
| 121 |
-
'../assets/data/against_baselines.csv',
|
| 122 |
-
'../../assets/data/against_baselines.csv'
|
| 123 |
-
];
|
| 124 |
-
const fetchFirstAvailable = async (paths) => {
|
| 125 |
-
for (const p of paths) {
|
| 126 |
-
try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(e) {}
|
| 127 |
-
}
|
| 128 |
-
throw new Error('CSV not found: against_baselines.csv');
|
| 129 |
-
};
|
| 130 |
|
| 131 |
-
// Controls
|
| 132 |
const controls = document.createElement('div');
|
| 133 |
-
controls.className = '
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
display: 'flex',
|
| 137 |
-
gap: '16px',
|
| 138 |
-
alignItems: 'center',
|
| 139 |
-
justifyContent: 'space-between',
|
| 140 |
-
width: '100%'
|
| 141 |
-
});
|
| 142 |
-
|
| 143 |
const labelMetric = document.createElement('label');
|
| 144 |
-
Object.assign(labelMetric.style, {
|
| 145 |
-
fontSize: '12px', color: 'var(--muted-color)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px', marginLeft: 'auto'
|
| 146 |
-
});
|
| 147 |
labelMetric.textContent = 'Metric';
|
| 148 |
const selectMetric = document.createElement('select');
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
display: 'flex',
|
| 157 |
-
gap: '8px',
|
| 158 |
-
alignItems: 'center',
|
| 159 |
-
flexWrap: 'nowrap',
|
| 160 |
-
fontSize: '11px',
|
| 161 |
-
marginLeft: '8px'
|
| 162 |
-
});
|
| 163 |
-
controls.appendChild(legendInline);
|
| 164 |
-
controls.appendChild(labelMetric);
|
| 165 |
-
|
| 166 |
-
// Create SVG
|
| 167 |
-
const svg = d3.select(container).append('svg')
|
| 168 |
-
.attr('width', '100%')
|
| 169 |
-
.style('display', 'block');
|
| 170 |
-
|
| 171 |
-
// Groups
|
| 172 |
-
const gRoot = svg.append('g');
|
| 173 |
-
const gGrid = gRoot.append('g').attr('class', 'grid');
|
| 174 |
-
const gAxes = gRoot.append('g').attr('class', 'axes');
|
| 175 |
-
const gLines = gRoot.append('g').attr('class', 'lines');
|
| 176 |
-
const gPoints = gRoot.append('g').attr('class', 'points');
|
| 177 |
-
const gHover = gRoot.append('g').attr('class', 'hover');
|
| 178 |
-
const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
|
| 179 |
|
| 180 |
// Tooltip
|
| 181 |
container.style.position = container.style.position || 'relative';
|
| 182 |
-
let tip = container.querySelector('.d3-tooltip');
|
| 183 |
-
let tipInner;
|
| 184 |
if (!tip) {
|
| 185 |
-
tip = document.createElement('div');
|
| 186 |
-
tip.className = 'd3-tooltip';
|
| 187 |
Object.assign(tip.style, {
|
| 188 |
-
position:
|
| 189 |
-
padding:
|
| 190 |
-
background:
|
| 191 |
-
transition: 'opacity .12s ease'
|
| 192 |
});
|
| 193 |
-
tipInner = document.createElement('div');
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
let runOrder = [];
|
| 210 |
-
const dataByMetric = new Map(); // metric => { run => [{step,value}] }
|
| 211 |
-
let isRankStrictFlag = false;
|
| 212 |
-
let rankTickMax = 1;
|
| 213 |
-
|
| 214 |
-
// Scales and layout
|
| 215 |
-
let width = 800, height = 360;
|
| 216 |
-
let margin = { top: 16, right: 28, bottom: 56, left: 64 };
|
| 217 |
-
let xScale = d3.scaleLinear();
|
| 218 |
-
let yScale = d3.scaleLinear();
|
| 219 |
-
|
| 220 |
-
// Line generators
|
| 221 |
-
const lineGenSmooth = d3.line()
|
| 222 |
-
.curve(d3.curveCatmullRom.alpha(0.05))
|
| 223 |
-
.x((d) => xScale(d.step))
|
| 224 |
-
.y((d) => yScale(d.value));
|
| 225 |
-
const lineGenStep = d3.line()
|
| 226 |
-
.curve(d3.curveStepAfter)
|
| 227 |
-
.x((d) => xScale(d.step))
|
| 228 |
-
.y((d) => yScale(d.value));
|
| 229 |
|
| 230 |
-
//
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
//
|
| 236 |
-
function
|
| 237 |
-
const
|
| 238 |
-
if (
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
legendInline.style.flexWrap = 'wrap';
|
| 243 |
-
legendInline.style.rowGap = '4px';
|
| 244 |
-
labelMetric.style.marginLeft = '0';
|
| 245 |
-
labelMetric.style.flexBasis = '100%';
|
| 246 |
-
labelMetric.style.width = '100%';
|
| 247 |
-
} else {
|
| 248 |
-
controls.style.flexWrap = 'nowrap';
|
| 249 |
-
controls.style.alignItems = 'center';
|
| 250 |
-
controls.style.justifyContent = 'space-between';
|
| 251 |
-
legendInline.style.flexWrap = 'nowrap';
|
| 252 |
-
labelMetric.style.marginLeft = 'auto';
|
| 253 |
-
labelMetric.style.flexBasis = 'auto';
|
| 254 |
-
labelMetric.style.width = 'auto';
|
| 255 |
}
|
|
|
|
| 256 |
}
|
| 257 |
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
|
|
|
|
|
|
| 264 |
width = container.clientWidth || 800;
|
| 265 |
-
height = Math.max(
|
| 266 |
svg.attr('width', width).attr('height', height);
|
| 267 |
-
|
| 268 |
-
// Update controls layout for current width
|
| 269 |
-
applyResponsiveLayout(width);
|
| 270 |
-
|
| 271 |
const innerWidth = width - margin.left - margin.right;
|
| 272 |
const innerHeight = height - margin.top - margin.bottom;
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
// Compute Y ticks
|
| 279 |
-
let yTicks = [];
|
| 280 |
-
if (isRankStrictFlag) {
|
| 281 |
-
const maxR = Math.max(1, Math.round(rankTickMax));
|
| 282 |
-
for (let v = 1; v <= maxR; v += 1) yTicks.push(v);
|
| 283 |
-
} else {
|
| 284 |
-
// Use D3's tick generator to produce nice floating-point ticks
|
| 285 |
-
yTicks = yScale.ticks(6);
|
| 286 |
-
}
|
| 287 |
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
gGrid.selectAll('*').remove();
|
| 290 |
-
gGrid.selectAll('line')
|
| 291 |
-
.
|
| 292 |
-
.
|
| 293 |
-
.attr('x1', 0)
|
| 294 |
-
.attr('x2', innerWidth)
|
| 295 |
-
.attr('y1', (d) => yScale(d))
|
| 296 |
-
.attr('y2', (d) => yScale(d))
|
| 297 |
-
.attr('stroke', gridColor)
|
| 298 |
-
.attr('stroke-width', 1)
|
| 299 |
-
.attr('shape-rendering', 'crispEdges');
|
| 300 |
|
| 301 |
-
//
|
| 302 |
gAxes.selectAll('*').remove();
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
| 322 |
}
|
| 323 |
-
}
|
| 324 |
-
const yAxis = d3.axisLeft(yScale)
|
| 325 |
-
.tickValues(yTicks)
|
| 326 |
-
.tickSizeOuter(0)
|
| 327 |
-
.tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
|
| 328 |
-
gAxes.append('g')
|
| 329 |
-
.attr('transform', `translate(0,${innerHeight})`)
|
| 330 |
-
.call(xAxis)
|
| 331 |
-
.call((g) => {
|
| 332 |
-
g.selectAll('path, line').attr('stroke', axisColor);
|
| 333 |
-
g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
|
| 334 |
-
});
|
| 335 |
-
gAxes.append('g')
|
| 336 |
-
.call(yAxis)
|
| 337 |
-
.call((g) => {
|
| 338 |
-
g.selectAll('path, line').attr('stroke', axisColor);
|
| 339 |
-
g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
|
| 340 |
-
});
|
| 341 |
-
|
| 342 |
-
// Axis labels (X and Y)
|
| 343 |
-
gAxes.append('text')
|
| 344 |
-
.attr('class', 'axis-label axis-label--x')
|
| 345 |
-
.attr('x', innerWidth / 2)
|
| 346 |
-
.attr('y', innerHeight + 44)
|
| 347 |
-
.attr('text-anchor', 'middle')
|
| 348 |
-
.style('font-size', '12px')
|
| 349 |
-
.style('fill', tickColor)
|
| 350 |
-
.text('Step');
|
| 351 |
-
gAxes.append('text')
|
| 352 |
-
.attr('class', 'axis-label axis-label--y')
|
| 353 |
-
.attr('text-anchor', 'middle')
|
| 354 |
-
.attr('transform', `translate(${-44},${innerHeight/2}) rotate(-90)`)
|
| 355 |
-
.style('font-size', '12px')
|
| 356 |
-
.style('fill', tickColor)
|
| 357 |
-
.text('Value');
|
| 358 |
-
|
| 359 |
-
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 360 |
-
hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
|
| 361 |
-
|
| 362 |
-
// Legend placeholder; actual content set in renderMetric
|
| 363 |
-
const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
|
| 364 |
-
const legendHeight = 64;
|
| 365 |
-
gLegend
|
| 366 |
-
.attr('x', innerWidth - legendWidth + 42)
|
| 367 |
-
.attr('y', innerHeight - legendHeight - 12)
|
| 368 |
-
.attr('width', legendWidth)
|
| 369 |
-
.attr('height', legendHeight);
|
| 370 |
-
const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
|
| 371 |
-
Object.assign(legendRoot.node().style, {
|
| 372 |
-
background: 'transparent',
|
| 373 |
-
border: 'none',
|
| 374 |
-
borderRadius: '0',
|
| 375 |
-
padding: '0',
|
| 376 |
-
fontSize: '12px',
|
| 377 |
-
lineHeight: '1.35',
|
| 378 |
-
color: 'var(--text-color)'
|
| 379 |
});
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
const isRankStrict = isRank && !isAverage;
|
| 392 |
-
runs.forEach(r => {
|
| 393 |
-
const arr = map[r] || [];
|
| 394 |
-
arr.forEach(pt => {
|
| 395 |
-
const val = isRankStrict ? Math.round(pt.value) : pt.value;
|
| 396 |
-
minStep = Math.min(minStep, pt.step);
|
| 397 |
-
maxStep = Math.max(maxStep, pt.step);
|
| 398 |
-
maxVal = Math.max(maxVal, val);
|
| 399 |
-
minVal = Math.min(minVal, val);
|
| 400 |
-
});
|
| 401 |
});
|
| 402 |
-
if (!isFinite(minStep) || !isFinite(maxStep)) { return; }
|
| 403 |
-
xScale.domain([minStep, maxStep]);
|
| 404 |
-
if (isRank) {
|
| 405 |
-
rankTickMax = Math.max(1, Math.round(maxVal));
|
| 406 |
-
yScale.domain([rankTickMax, 1]);
|
| 407 |
-
} else {
|
| 408 |
-
yScale.domain([0, Math.max(1, maxVal)]).nice();
|
| 409 |
-
}
|
| 410 |
-
isRankStrictFlag = isRankStrict;
|
| 411 |
-
|
| 412 |
-
const { innerWidth, innerHeight } = updateScales();
|
| 413 |
|
| 414 |
-
//
|
| 415 |
-
const series = runs.map((r, i) => ({
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
.slice()
|
| 420 |
-
.sort((a,b)=>a.step-b.step)
|
| 421 |
-
.map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value) } : pt)
|
| 422 |
-
.filter(pt => !isRankStrict || (pt.step % 1000 === 0))
|
| 423 |
-
}));
|
| 424 |
-
const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
|
| 425 |
-
const gen = isRank ? lineGenStep : lineGenSmooth;
|
| 426 |
-
paths.enter().append('path').attr('class','run-line').attr('fill','none').attr('stroke-width',2)
|
| 427 |
-
.attr('stroke', d=>d.color).attr('opacity',0.9)
|
| 428 |
-
.attr('d', d=>gen(d.values))
|
| 429 |
-
.merge(paths)
|
| 430 |
-
.transition().duration(200)
|
| 431 |
-
.attr('stroke', d=>d.color)
|
| 432 |
-
.attr('d', d=>gen(d.values));
|
| 433 |
paths.exit().remove();
|
| 434 |
|
| 435 |
-
//
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
const xpx = xScale(nearest);
|
| 449 |
-
hoverLine.
|
| 450 |
-
//
|
| 451 |
-
let html = `<div
|
| 452 |
-
series.
|
| 453 |
const m = new Map(s.values.map(v=>[v.step, v.value]));
|
| 454 |
-
const val = m.
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
|
|
|
| 459 |
});
|
| 460 |
-
tipInner.innerHTML = html;
|
| 461 |
-
const offsetX = 12, offsetY = 12;
|
| 462 |
-
tip.style.opacity = '1'; tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
|
| 463 |
}
|
| 464 |
function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
|
| 465 |
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
| 466 |
}
|
| 467 |
|
| 468 |
-
//
|
| 469 |
-
|
| 470 |
-
// Load CSV and wire controls
|
| 471 |
(async () => {
|
| 472 |
try {
|
| 473 |
const text = await fetchFirstAvailable(CSV_PATHS);
|
| 474 |
-
const rows = d3.csvParse(text, d => ({
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
dataByMetric.set(m, map);
|
| 484 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
const
|
| 489 |
-
if (
|
| 490 |
-
|
| 491 |
-
container.appendChild(controls);
|
| 492 |
-
updateScales();
|
| 493 |
-
renderMetric(selectMetric.value);
|
| 494 |
-
|
| 495 |
-
selectMetric.addEventListener('change', ()=>{ renderMetric(selectMetric.value); });
|
| 496 |
-
|
| 497 |
-
const rerender = () => { renderMetric(selectMetric.value); };
|
| 498 |
-
if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
|
| 499 |
} catch (e) {
|
| 500 |
const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
|
| 501 |
-
pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';
|
| 502 |
-
container.appendChild(pre);
|
| 503 |
}
|
| 504 |
})();
|
| 505 |
};
|
| 506 |
|
| 507 |
-
if (document.readyState === 'loading') {
|
| 508 |
-
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 509 |
-
} else { ensureD3(bootstrap); }
|
| 510 |
})();
|
| 511 |
</script>
|
| 512 |
|
|
|
|
| 1 |
+
<div class="d3-line-simple"></div>
|
| 2 |
<style>
|
| 3 |
+
.d3-line-simple { position: relative; }
|
| 4 |
+
/* Theme-driven axis/tick/grid variables */
|
| 5 |
+
.d3-line-simple {
|
| 6 |
+
--axis-color: rgba(0,0,0,0.25);
|
| 7 |
+
--tick-color: rgba(0,0,0,0.55);
|
| 8 |
+
--grid-color: rgba(0,0,0,0.05);
|
| 9 |
+
}
|
| 10 |
+
[data-theme="dark"] .d3-line-simple {
|
| 11 |
+
--axis-color: rgba(255,255,255,0.25);
|
| 12 |
+
--tick-color: rgba(255,255,255,0.70);
|
| 13 |
+
--grid-color: rgba(255,255,255,0.08);
|
| 14 |
+
}
|
| 15 |
+
.d3-line-simple .controls {
|
| 16 |
+
margin-top: 0;
|
| 17 |
+
display: flex;
|
| 18 |
+
gap: 16px;
|
| 19 |
+
align-items: center;
|
| 20 |
+
justify-content: flex-end;
|
| 21 |
+
width: auto;
|
| 22 |
+
flex-wrap: wrap;
|
| 23 |
+
}
|
| 24 |
+
.d3-line-simple .controls label {
|
| 25 |
+
font-size: 12px;
|
| 26 |
+
color: var(--text-color);
|
| 27 |
+
display: flex;
|
| 28 |
+
align-items: center;
|
| 29 |
+
gap: 6px;
|
| 30 |
+
white-space: nowrap;
|
| 31 |
+
font-weight: 700;
|
| 32 |
+
}
|
| 33 |
+
.d3-line-simple .controls select {
|
| 34 |
font-size: 12px;
|
| 35 |
padding: 8px 28px 8px 10px;
|
| 36 |
border: 1px solid var(--border-color);
|
|
|
|
| 38 |
background-color: var(--surface-bg);
|
| 39 |
color: var(--text-color);
|
| 40 |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 41 |
+
background-repeat: no-repeat; background-position: right 8px center; background-size: 12px;
|
| 42 |
+
-webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
}
|
| 44 |
+
[data-theme="dark"] .d3-line-simple .controls select {
|
| 45 |
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
| 46 |
}
|
| 47 |
+
.d3-line-simple .controls select:hover { border-color: var(--primary-color); }
|
| 48 |
+
.d3-line-simple .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }
|
| 49 |
+
.d3-line-simple .axis-label { fill: var(--text-color); font-size: 12px; font-weight: 700; }
|
| 50 |
+
.d3-line-simple .axes path, .d3-line-simple .axes line { stroke: var(--axis-color); }
|
| 51 |
+
.d3-line-simple .axes text { fill: var(--tick-color); }
|
| 52 |
+
.d3-line-simple .grid line { stroke: var(--grid-color); }
|
| 53 |
+
.d3-line-simple .legend { font-size: 12px; color: var(--text-color);padding-left: 6px; }
|
| 54 |
+
.d3-line-simple .legend .items { display:flex; flex-wrap:wrap; gap:8px 12px; align-items:center; }
|
| 55 |
+
.d3-line-simple .legend .item { display:flex; align-items:center; gap:6px; white-space:nowrap; }
|
| 56 |
+
.d3-line-simple .legend .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); display:inline-block; }
|
| 57 |
+
/* Ghosting on hover */
|
| 58 |
+
.d3-line-simple.hovering .legend-bottom .item.ghost { opacity: .35; }
|
| 59 |
+
.d3-line-simple.hovering .lines path.ghost { opacity: .25; }
|
| 60 |
+
.d3-line-simple.hovering .points circle.ghost { opacity: .25; }
|
| 61 |
+
.d3-line-simple.hovering .areas path.ghost { opacity: .08; }
|
| 62 |
+
.d3-line-simple .chart-header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin: 0 0 8px 0; flex-wrap: wrap; }
|
| 63 |
+
.d3-line-simple .legend-bottom { display:flex; align-items:center; justify-content:flex-start; font-size:12px; color: var(--text-color); }
|
| 64 |
+
.d3-line-simple .legend-bottom .items { display:flex; flex-wrap:wrap; gap:8px 14px; }
|
| 65 |
+
.d3-line-simple .legend-bottom .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; }
|
| 66 |
+
.d3-line-simple .legend-bottom .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); display:inline-block; }
|
| 67 |
+
.d3-line-simple .lines path.active { stroke-width: 3; }
|
| 68 |
+
/* Layout tweaks: stack label above select, add legend title above items */
|
| 69 |
+
.d3-line-simple .controls .control-group {
|
| 70 |
+
display: flex;
|
| 71 |
+
flex-direction: column;
|
| 72 |
+
align-items: flex-start;
|
| 73 |
+
gap: 6px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
+
.d3-line-simple .legend-bottom {
|
| 76 |
+
flex-direction: column;
|
| 77 |
+
align-items: flex-start;
|
| 78 |
+
gap: 6px;
|
| 79 |
}
|
| 80 |
+
.d3-line-simple .legend-bottom .legend-title {
|
| 81 |
+
font-size: 12px;
|
| 82 |
+
font-weight: 700;
|
| 83 |
+
color: var(--text-color);
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
+
/* Tooltip styling aligned with filters-quad */
|
| 86 |
+
.d3-line-simple .d3-tooltip { z-index: var(--z-elevated); backdrop-filter: saturate(1.12) blur(8px); }
|
| 87 |
+
.d3-line-simple .d3-tooltip__inner { display:flex; flex-direction:column; gap:6px; min-width: 220px; }
|
| 88 |
+
.d3-line-simple .d3-tooltip__inner > div:first-child { font-weight: 800; letter-spacing: 0.1px; margin-bottom: 0; }
|
| 89 |
+
.d3-line-simple .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--muted-color); display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.1px; }
|
| 90 |
+
.d3-line-simple .d3-tooltip__inner > div:nth-child(n+3) { padding-top: 6px; border-top: 1px solid var(--border-color); }
|
| 91 |
+
.d3-line-simple .d3-tooltip__color-dot { display:inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-color); }
|
| 92 |
+
/* Chart card only around the SVG */
|
| 93 |
+
.d3-line-simple .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
|
| 94 |
+
/* Place header below chart and align start */
|
| 95 |
+
.d3-line-simple .chart-header { display:flex; align-items:flex-start; justify-content:flex-start; gap:12px; margin: 8px 0 0 0; flex-wrap: wrap; }
|
| 96 |
</style>
|
| 97 |
<script>
|
| 98 |
(() => {
|
| 99 |
+
// Pretty label mapping for metric keys (aligned with filters-quad)
|
| 100 |
+
const prettyMetricLabel = (key) => {
|
| 101 |
+
if (!key) return '';
|
| 102 |
+
const table = {
|
| 103 |
+
'ai2d_exact_match': 'AI2D Exact Match',
|
| 104 |
+
'average_rank': 'Average Rank',
|
| 105 |
+
};
|
| 106 |
+
if (table[key]) return table[key];
|
| 107 |
+
const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
|
| 108 |
+
return cleaned.split(/\s+/).map(w => {
|
| 109 |
+
if (/^(ai2d|umap|id|auc|f1)$/i.test(w)) return w.toUpperCase();
|
| 110 |
+
return w.charAt(0).toUpperCase() + w.slice(1);
|
| 111 |
+
}).join(' ');
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
const ensureD3 = (cb) => {
|
| 115 |
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 116 |
let s = document.getElementById('d3-cdn-script');
|
| 117 |
+
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 119 |
+
s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
|
|
|
|
| 120 |
};
|
| 121 |
|
| 122 |
const bootstrap = () => {
|
| 123 |
const scriptEl = document.currentScript;
|
| 124 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 125 |
+
if (!(container && container.classList && container.classList.contains('d3-line-simple'))){
|
| 126 |
+
const cs = Array.from(document.querySelectorAll('.d3-line-simple')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 127 |
+
container = cs[cs.length - 1] || null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
if (!container) return;
|
| 130 |
+
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
+
// Controls (we will place them in a footer below the chart)
|
| 133 |
const controls = document.createElement('div');
|
| 134 |
+
controls.className = 'controls';
|
| 135 |
+
const controlGroup = document.createElement('div');
|
| 136 |
+
controlGroup.className = 'control-group';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
const labelMetric = document.createElement('label');
|
|
|
|
|
|
|
|
|
|
| 138 |
labelMetric.textContent = 'Metric';
|
| 139 |
const selectMetric = document.createElement('select');
|
| 140 |
+
// Associate label and select with a unique id
|
| 141 |
+
const uniqueId = Math.random().toString(36).slice(2, 9);
|
| 142 |
+
selectMetric.id = `metric-select-${uniqueId}`;
|
| 143 |
+
labelMetric.setAttribute('for', selectMetric.id);
|
| 144 |
+
controlGroup.appendChild(labelMetric);
|
| 145 |
+
controlGroup.appendChild(selectMetric);
|
| 146 |
+
controls.appendChild(controlGroup);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
// Tooltip
|
| 149 |
container.style.position = container.style.position || 'relative';
|
| 150 |
+
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
|
|
|
| 151 |
if (!tip) {
|
| 152 |
+
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
|
|
|
| 153 |
Object.assign(tip.style, {
|
| 154 |
+
position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
|
| 155 |
+
padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
|
| 156 |
+
background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease'
|
|
|
|
| 157 |
});
|
| 158 |
+
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
| 159 |
+
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 160 |
+
|
| 161 |
+
// Header (legend + controls) placed after the chart
|
| 162 |
+
const header = document.createElement('div'); header.className = 'chart-header';
|
| 163 |
+
const legendBottom = document.createElement('div'); legendBottom.className = 'legend-bottom'; header.appendChild(legendBottom);
|
| 164 |
+
header.appendChild(controls);
|
| 165 |
+
|
| 166 |
+
// Chart card (SVG)
|
| 167 |
+
const card = document.createElement('div'); card.className = 'chart-card'; container.appendChild(card);
|
| 168 |
+
container.appendChild(header);
|
| 169 |
+
// SVG
|
| 170 |
+
const svg = d3.select(card).append('svg').attr('width','100%').style('display','block');
|
| 171 |
+
const gRoot = svg.append('g');
|
| 172 |
+
const gGrid = gRoot.append('g').attr('class','grid');
|
| 173 |
+
const gAxes = gRoot.append('g').attr('class','axes');
|
| 174 |
+
const gAreas = gRoot.append('g').attr('class','areas');
|
| 175 |
+
const gLines = gRoot.append('g').attr('class','lines');
|
| 176 |
+
const gPoints = gRoot.append('g').attr('class','points');
|
| 177 |
+
// (legend removed from inside SVG)
|
| 178 |
+
const overlay = gRoot.append('rect').attr('fill','transparent').style('cursor','crosshair');
|
| 179 |
+
const hoverLine = gRoot.append('line').attr('stroke-width',1).style('display','none');
|
| 180 |
+
|
| 181 |
+
// State/data
|
| 182 |
+
let width = 800, height = 480; const margin = { top: 16, right: 32, bottom: 44, left: 56 };
|
| 183 |
+
const xScale = d3.scaleLinear();
|
| 184 |
+
const yScale = d3.scaleLinear();
|
| 185 |
+
const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
|
| 186 |
+
const dataByMetric = new Map();
|
| 187 |
let runOrder = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
+
// Colors
|
| 190 |
+
function getRunColors(count){
|
| 191 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', count); } catch(_){}
|
| 192 |
+
return d3.schemeTableau10 ? d3.schemeTableau10.slice(0, count) : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'].slice(0, count);
|
| 193 |
+
}
|
| 194 |
+
// Format helper for thousands (5000 -> 5k, 1500 -> 1.5k)
|
| 195 |
+
function formatK(v){
|
| 196 |
+
const abs = Math.abs(v);
|
| 197 |
+
if (abs >= 1000) {
|
| 198 |
+
const n = v / 1000;
|
| 199 |
+
const s = d3.format('.1f')(n);
|
| 200 |
+
return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
}
|
| 202 |
+
return d3.format('d')(v);
|
| 203 |
}
|
| 204 |
|
| 205 |
+
// CSV: prefer public path, fallback to relative
|
| 206 |
+
// Data file(s) from HtmlEmbed attribute (string or JSON array), else default cascade
|
| 207 |
+
// Find the HtmlEmbed wrapper that carries data-datafiles
|
| 208 |
+
let mountEl = container;
|
| 209 |
+
while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
|
| 210 |
+
mountEl = mountEl.parentElement;
|
| 211 |
+
}
|
| 212 |
+
let providedData = null;
|
| 213 |
+
let providedConfig = null;
|
| 214 |
+
try {
|
| 215 |
+
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
|
| 216 |
+
if (attr && attr.trim()) {
|
| 217 |
+
providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
|
| 218 |
+
}
|
| 219 |
+
} catch(_) {}
|
| 220 |
+
try {
|
| 221 |
+
const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
|
| 222 |
+
if (cfg && cfg.trim()) {
|
| 223 |
+
providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
|
| 224 |
+
}
|
| 225 |
+
} catch(_) {}
|
| 226 |
+
const DEFAULT_CSV = '/data/formatting_filters.csv';
|
| 227 |
+
// Normalize: if a provided path has no slash, assume it lives under /data/
|
| 228 |
+
const ensureDataPrefix = (p) => {
|
| 229 |
+
if (typeof p !== 'string' || !p) return p;
|
| 230 |
+
return p.includes('/') ? p : `/data/${p}`;
|
| 231 |
+
};
|
| 232 |
+
const normalizeInput = (inp) => Array.isArray(inp) ? inp.map(ensureDataPrefix) : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
|
| 233 |
+
const CSV_PATHS = Array.isArray(providedData)
|
| 234 |
+
? normalizeInput(providedData)
|
| 235 |
+
: (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [
|
| 236 |
+
DEFAULT_CSV,
|
| 237 |
+
'./assets/data/formatting_filters.csv',
|
| 238 |
+
'../assets/data/formatting_filters.csv',
|
| 239 |
+
'../../assets/data/formatting_filters.csv'
|
| 240 |
+
]);
|
| 241 |
+
const fetchFirstAvailable = async (paths) => {
|
| 242 |
+
for (const p of paths) { try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(_){} }
|
| 243 |
+
throw new Error('CSV not found: formatting_filters.csv');
|
| 244 |
+
};
|
| 245 |
|
| 246 |
+
function updateLayout(){
|
| 247 |
+
const axisColor = getComputedStyle(container).getPropertyValue('--axis-color').trim() || 'rgba(0,0,0,0.25)';
|
| 248 |
width = container.clientWidth || 800;
|
| 249 |
+
height = Math.max(280, Math.round(width / 3));
|
| 250 |
svg.attr('width', width).attr('height', height);
|
| 251 |
+
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
|
|
|
|
|
|
| 252 |
const innerWidth = width - margin.left - margin.right;
|
| 253 |
const innerHeight = height - margin.top - margin.bottom;
|
| 254 |
+
overlay.attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
|
| 255 |
+
hoverLine.attr('y1',0).attr('y2', innerHeight).attr('stroke', axisColor);
|
| 256 |
+
return { innerWidth, innerHeight };
|
| 257 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
+
function render(metricKey){
|
| 260 |
+
const { innerWidth, innerHeight } = updateLayout();
|
| 261 |
+
const map = dataByMetric.get(metricKey) || {};
|
| 262 |
+
const runs = runOrder;
|
| 263 |
+
// domains
|
| 264 |
+
let minStep = Infinity, maxStep = -Infinity, minV = Infinity, maxV = -Infinity;
|
| 265 |
+
runs.forEach(r => { (map[r]||[]).forEach(pt => { minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); minV = Math.min(minV, pt.value); maxV = Math.max(maxV, pt.value); }); });
|
| 266 |
+
if (!isFinite(minStep) || !isFinite(maxStep)) return;
|
| 267 |
+
xScale.domain([minStep, maxStep]).range([0, innerWidth]);
|
| 268 |
+
yScale.domain([minV, maxV]).nice().range([innerHeight, 0]);
|
| 269 |
+
|
| 270 |
+
// grid
|
| 271 |
gGrid.selectAll('*').remove();
|
| 272 |
+
gGrid.selectAll('line').data(yScale.ticks(6)).join('line')
|
| 273 |
+
.attr('x1',0).attr('x2', innerWidth).attr('y1', d=>yScale(d)).attr('y2', d=>yScale(d))
|
| 274 |
+
.attr('stroke','var(--grid-color)').attr('stroke-width',1).attr('shape-rendering','crispEdges');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
+
// axes
|
| 277 |
gAxes.selectAll('*').remove();
|
| 278 |
+
gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(xScale).ticks(8).tickFormat(formatK)).call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','12px'); });
|
| 279 |
+
gAxes.append('g').call(d3.axisLeft(yScale).ticks(6)).call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','12px'); });
|
| 280 |
+
gAxes.append('text').attr('class','axis-label').attr('text-anchor','middle').attr('x', innerWidth/2).attr('y', innerHeight + 38).text('Step');
|
| 281 |
+
gAxes.append('text').attr('class','axis-label').attr('text-anchor','middle').attr('transform', `translate(${-44}, ${innerHeight/2}) rotate(-90)`).text('Value');
|
| 282 |
+
|
| 283 |
+
// shaded areas (stderr or min/max if provided)
|
| 284 |
+
gAreas.selectAll('*').remove();
|
| 285 |
+
const areaGenClosed = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed);
|
| 286 |
+
// Colors (needed for both areas and lines)
|
| 287 |
+
const colors = getRunColors(runs.length);
|
| 288 |
+
const seriesForAreas = [];
|
| 289 |
+
runs.forEach((r, i) => {
|
| 290 |
+
const vals = (map[r]||[]).slice().sort((a,b)=>a.step-b.step);
|
| 291 |
+
const withBounds = vals.map(v => {
|
| 292 |
+
let lo = null, hi = null;
|
| 293 |
+
if (v.stderr != null && isFinite(v.stderr) && v.stderr > 0) { lo = v.value - v.stderr; hi = v.value + v.stderr; }
|
| 294 |
+
if (v.min != null && isFinite(v.min)) lo = (lo==null) ? v.min : lo;
|
| 295 |
+
if (v.max != null && isFinite(v.max)) hi = (hi==null) ? v.max : hi;
|
| 296 |
+
return { step: v.step, lo, hi };
|
| 297 |
+
}).filter(v => v.lo != null && v.hi != null && isFinite(v.lo) && isFinite(v.hi));
|
| 298 |
+
if (withBounds.length > 0) {
|
| 299 |
+
seriesForAreas.push({ run: r, color: colors[i % colors.length], bounds: withBounds });
|
| 300 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
});
|
| 302 |
+
seriesForAreas.forEach(s => {
|
| 303 |
+
const upper = s.bounds.map(d => [ xScale(d.step), yScale(d.hi) ]);
|
| 304 |
+
const lower = s.bounds.slice().reverse().map(d => [ xScale(d.step), yScale(d.lo) ]);
|
| 305 |
+
const coords = upper.concat(lower);
|
| 306 |
+
gAreas.append('path')
|
| 307 |
+
.attr('class','area')
|
| 308 |
+
.attr('data-run', s.run)
|
| 309 |
+
.attr('d', areaGenClosed(coords))
|
| 310 |
+
.attr('fill', s.color)
|
| 311 |
+
.attr('opacity', 0.15)
|
| 312 |
+
.attr('stroke','none');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
+
// lines
|
| 316 |
+
const series = runs.map((r, i) => ({ run:r, color: colors[i % colors.length], values: (map[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
|
| 317 |
+
const paths = gLines.selectAll('path.run').data(series, d=>d.run);
|
| 318 |
+
const pathsEnter = paths.enter().append('path').attr('class','run').attr('fill','none').attr('stroke-width',2).attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
|
| 319 |
+
pathsEnter.merge(paths).transition().duration(200).attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
paths.exit().remove();
|
| 321 |
|
| 322 |
+
// Hover capture paths (wider invisible stroke for easy hover)
|
| 323 |
+
const captures = gLines.selectAll('path.run-hover').data(series, d=>`cap-${d.run}`);
|
| 324 |
+
captures.enter().append('path').attr('class','run-hover').attr('fill','none').attr('stroke','transparent').attr('stroke-width', 12).style('pointer-events','stroke')
|
| 325 |
+
.attr('d', d=>lineGen(d.values))
|
| 326 |
+
.merge(captures)
|
| 327 |
+
.attr('d', d=>lineGen(d.values))
|
| 328 |
+
.on('mouseenter', function(ev, d){
|
| 329 |
+
container.classList.add('hovering');
|
| 330 |
+
// ghost non hovered lines and points
|
| 331 |
+
gLines.selectAll('path.run').classed('ghost', s => s.run !== d.run);
|
| 332 |
+
gPoints.selectAll('circle.pt').classed('ghost', p => p.run !== d.run);
|
| 333 |
+
gAreas.selectAll('path.area').classed('ghost', a => a && a.getAttribute && a.getAttribute('data-run') !== d.run);
|
| 334 |
+
// ghost legend items
|
| 335 |
+
try {
|
| 336 |
+
const legendNode = legendBottom;
|
| 337 |
+
if (legendNode) {
|
| 338 |
+
legendNode.querySelectorAll('.item').forEach(el => {
|
| 339 |
+
const name = el.getAttribute('data-run');
|
| 340 |
+
el.classList.toggle('ghost', name !== d.run);
|
| 341 |
+
});
|
| 342 |
+
}
|
| 343 |
+
} catch {}
|
| 344 |
+
})
|
| 345 |
+
.on('mouseleave', function(){
|
| 346 |
+
container.classList.remove('hovering');
|
| 347 |
+
gLines.selectAll('path.run').classed('ghost', false);
|
| 348 |
+
gPoints.selectAll('circle.pt').classed('ghost', false);
|
| 349 |
+
gAreas.selectAll('path.area').classed('ghost', false);
|
| 350 |
+
try { const legendNode = legendBottom; if (legendNode) legendNode.querySelectorAll('.item').forEach(el => el.classList.remove('ghost')); } catch {}
|
| 351 |
+
});
|
| 352 |
+
captures.exit().remove();
|
| 353 |
+
|
| 354 |
+
// point markers (subtle)
|
| 355 |
+
const allPts = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
|
| 356 |
+
const ptsSel = gPoints.selectAll('circle.pt').data(allPts, d=>`${d.run}-${d.step}`);
|
| 357 |
+
ptsSel.enter().append('circle').attr('class','pt').attr('r', 2).attr('fill', d=>d.color).attr('fill-opacity', 0.6)
|
| 358 |
+
.attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value))
|
| 359 |
+
.merge(ptsSel).transition().duration(150).attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value));
|
| 360 |
+
ptsSel.exit().remove();
|
| 361 |
+
|
| 362 |
+
// legend (HTML below, left) with title above items
|
| 363 |
+
legendBottom.innerHTML = `<div class="legend-title">Legend</div><div class="items">${series.map(s => `<span class="item" data-run="${s.run}"><span class="swatch" style="background:${s.color}"></span><span>${s.run}</span></span>`).join('')}</div>`;
|
| 364 |
+
// Legend hover → ghost lines/points
|
| 365 |
+
try {
|
| 366 |
+
const legendNode = legendBottom;
|
| 367 |
+
legendNode.querySelectorAll('.item').forEach(el => {
|
| 368 |
+
el.addEventListener('mouseenter', () => {
|
| 369 |
+
const run = el.getAttribute('data-run'); if (!run) return;
|
| 370 |
+
container.classList.add('hovering');
|
| 371 |
+
gLines.selectAll('path.run').classed('ghost', s => s.run !== run);
|
| 372 |
+
gPoints.selectAll('circle.pt').classed('ghost', p => p.run !== run);
|
| 373 |
+
gAreas.selectAll('path.area').classed('ghost', a => a && a.getAttribute && a.getAttribute('data-run') !== run);
|
| 374 |
+
legendNode.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
|
| 375 |
+
});
|
| 376 |
+
el.addEventListener('mouseleave', () => {
|
| 377 |
+
container.classList.remove('hovering');
|
| 378 |
+
gLines.selectAll('path.run').classed('ghost', false);
|
| 379 |
+
gPoints.selectAll('circle.pt').classed('ghost', false);
|
| 380 |
+
gAreas.selectAll('path.area').classed('ghost', false);
|
| 381 |
+
legendNode.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
|
| 382 |
+
});
|
| 383 |
+
});
|
| 384 |
+
} catch {}
|
| 385 |
+
|
| 386 |
+
// hover
|
| 387 |
+
function onMove(ev){
|
| 388 |
+
const [mx, my] = d3.pointer(ev, overlay.node());
|
| 389 |
+
const sx = xScale.invert(mx);
|
| 390 |
+
// nearest integer step if steps are integers; else nearest by distance
|
| 391 |
+
const steps = Array.from(new Set(allPts.map(p=>p.step))).sort((a,b)=>a-b);
|
| 392 |
+
const nearest = steps.reduce((best, s) => Math.abs(s - sx) < Math.abs(best - sx) ? s : best, steps[0]);
|
| 393 |
const xpx = xScale(nearest);
|
| 394 |
+
hoverLine.style('display', null).attr('x1', xpx).attr('x2', xpx);
|
| 395 |
+
// tooltip content (styled)
|
| 396 |
+
let html = `<div style=\"font-weight:800;letter-spacing:.1px;\">${metricKey}</div><div style=\"font-size:11px;color:var(--muted-color);margin-top:-4px;margin-bottom:2px;\">Step ${formatK(nearest)}</div>`;
|
| 397 |
+
const entries = series.map(s => {
|
| 398 |
const m = new Map(s.values.map(v=>[v.step, v.value]));
|
| 399 |
+
const val = m.get(nearest);
|
| 400 |
+
return { run: s.run, color: s.color, val };
|
| 401 |
+
}).filter(e => e.val != null);
|
| 402 |
+
entries.sort((a, b) => a.val - b.val);
|
| 403 |
+
entries.forEach(e => {
|
| 404 |
+
html += `<div style=\"display:flex;align-items:center;gap:6px;white-space:nowrap;\"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;\">${(+e.val).toFixed(4)}</span></div>`;
|
| 405 |
});
|
| 406 |
+
tipInner.innerHTML = html; tip.style.opacity = '1'; tip.style.transform = `translate(${Math.round(mx + margin.left + 12)}px, ${Math.round(my + margin.top + 12)}px)`;
|
|
|
|
|
|
|
| 407 |
}
|
| 408 |
function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
|
| 409 |
overlay.on('mousemove', onMove).on('mouseleave', onLeave);
|
| 410 |
}
|
| 411 |
|
| 412 |
+
// load CSV and init
|
|
|
|
|
|
|
| 413 |
(async () => {
|
| 414 |
try {
|
| 415 |
const text = await fetchFirstAvailable(CSV_PATHS);
|
| 416 |
+
const rows = d3.csvParse(text, d => ({
|
| 417 |
+
run:(d.run||'').trim(),
|
| 418 |
+
step:+d.step,
|
| 419 |
+
metric:(d.metric||'').trim(),
|
| 420 |
+
value:+d.value,
|
| 421 |
+
// Optional bounds: stderr or [min,max] or [lower,upper]
|
| 422 |
+
stderr: (d.stderr!=null && d.stderr!=='') ? +d.stderr : null,
|
| 423 |
+
min: (d.min!=null && d.min!=='') ? +d.min : (d.lower!=null && d.lower!=='') ? +d.lower : null,
|
| 424 |
+
max: (d.max!=null && d.max!=='') ? +d.max : (d.upper!=null && d.upper!=='') ? +d.upper : null
|
| 425 |
+
}));
|
| 426 |
+
const metrics = Array.from(new Set(rows.map(r=>r.metric))).sort();
|
| 427 |
+
runOrder = Array.from(new Set(rows.map(r=>r.run))).sort();
|
| 428 |
+
metrics.forEach(m => {
|
| 429 |
+
const map = {}; runOrder.forEach(r => map[r] = []);
|
| 430 |
+
rows.filter(r=>r.metric===m).forEach(r => {
|
| 431 |
+
if (!isNaN(r.step) && !isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value, stderr: r.stderr, min: r.min, max: r.max });
|
| 432 |
+
});
|
| 433 |
dataByMetric.set(m, map);
|
| 434 |
});
|
| 435 |
+
// populate metric select (pretty labels) or hide if single-file with single metric
|
| 436 |
+
const isSingleFile = !Array.isArray(providedData) && typeof providedData === 'string';
|
| 437 |
+
metrics.forEach(m => { const o = document.createElement('option'); o.value=m; o.textContent=prettyMetricLabel(m); selectMetric.appendChild(o); });
|
| 438 |
+
// default metric selection via config.defaultMetric (match raw key or pretty label, case-insensitive)
|
| 439 |
+
if (metrics.length) {
|
| 440 |
+
let initial = metrics[0];
|
| 441 |
+
const desired = providedConfig && typeof providedConfig === 'object' && providedConfig.defaultMetric ? String(providedConfig.defaultMetric) : null;
|
| 442 |
+
if (desired) {
|
| 443 |
+
const lcDesired = desired.toLowerCase();
|
| 444 |
+
const byKey = metrics.find(m => m.toLowerCase() === lcDesired);
|
| 445 |
+
const byPretty = metrics.find(m => prettyMetricLabel(m).toLowerCase() === lcDesired);
|
| 446 |
+
initial = byKey || byPretty || initial;
|
| 447 |
+
}
|
| 448 |
+
selectMetric.value = initial;
|
| 449 |
+
}
|
| 450 |
+
if (isSingleFile && metrics.length <= 1) {
|
| 451 |
+
controls.style.display = 'none';
|
| 452 |
+
}
|
| 453 |
|
| 454 |
+
render(selectMetric.value);
|
| 455 |
+
selectMetric.addEventListener('change', () => render(selectMetric.value));
|
| 456 |
+
const rerender = () => render(selectMetric.value);
|
| 457 |
+
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
} catch (e) {
|
| 459 |
const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
|
| 460 |
+
pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; container.appendChild(pre);
|
|
|
|
| 461 |
}
|
| 462 |
})();
|
| 463 |
};
|
| 464 |
|
| 465 |
+
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
|
|
|
|
|
|
|
| 466 |
})();
|
| 467 |
</script>
|
| 468 |
|
app/src/content/embeds/d3-matrix.html
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-matrix" ></div>
|
| 2 |
+
<style>
|
| 3 |
+
.d3-matrix {
|
| 4 |
+
position: relative;
|
| 5 |
+
}
|
| 6 |
+
.d3-matrix .panels {
|
| 7 |
+
display: flex;
|
| 8 |
+
flex-wrap: wrap;
|
| 9 |
+
gap: 16px;
|
| 10 |
+
margin-bottom: 4px;
|
| 11 |
+
}
|
| 12 |
+
.d3-matrix .panel {
|
| 13 |
+
flex: 1 1 320px;
|
| 14 |
+
min-width: 280px;
|
| 15 |
+
}
|
| 16 |
+
.d3-matrix .panel__title {
|
| 17 |
+
color: var(--text-color);
|
| 18 |
+
font-size: 12px;
|
| 19 |
+
line-height: 1.35;
|
| 20 |
+
margin: 0 0 6px 0;
|
| 21 |
+
font-weight: 600;
|
| 22 |
+
}
|
| 23 |
+
.d3-matrix .axis-label {
|
| 24 |
+
fill: var(--text-color);
|
| 25 |
+
font-size: 11px;
|
| 26 |
+
font-weight: 700;
|
| 27 |
+
}
|
| 28 |
+
.d3-matrix .cell-border {
|
| 29 |
+
stroke: var(--border-color);
|
| 30 |
+
stroke-width: 1px;
|
| 31 |
+
fill: none;
|
| 32 |
+
}
|
| 33 |
+
.d3-matrix .cell-text {
|
| 34 |
+
fill: var(--muted-color);
|
| 35 |
+
font-size: 11px;
|
| 36 |
+
pointer-events: none;
|
| 37 |
+
}
|
| 38 |
+
.d3-matrix .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }
|
| 39 |
+
</style>
|
| 40 |
+
<script>
|
| 41 |
+
(() => {
|
| 42 |
+
// Load D3 from CDN once
|
| 43 |
+
const ensureD3 = (cb) => {
|
| 44 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 45 |
+
let s = document.getElementById('d3-cdn-script');
|
| 46 |
+
if (!s) {
|
| 47 |
+
s = document.createElement('script');
|
| 48 |
+
s.id = 'd3-cdn-script';
|
| 49 |
+
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 50 |
+
document.head.appendChild(s);
|
| 51 |
+
}
|
| 52 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 53 |
+
s.addEventListener('load', onReady, { once: true });
|
| 54 |
+
if (window.d3) onReady();
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
const bootstrap = () => {
|
| 58 |
+
const scriptEl = document.currentScript;
|
| 59 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 60 |
+
if (!(container && container.classList && container.classList.contains('d3-matrix'))){
|
| 61 |
+
const cs = Array.from(document.querySelectorAll('.d3-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 62 |
+
container = cs[cs.length - 1] || null;
|
| 63 |
+
}
|
| 64 |
+
if (!container) return;
|
| 65 |
+
if (container.dataset) {
|
| 66 |
+
if (container.dataset.mounted === 'true') return;
|
| 67 |
+
container.dataset.mounted = 'true';
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Tooltip (HTML, single instance inside container)
|
| 71 |
+
container.style.position = container.style.position || 'relative';
|
| 72 |
+
let tip = container.querySelector('.d3-tooltip');
|
| 73 |
+
let tipInner;
|
| 74 |
+
if (!tip) {
|
| 75 |
+
tip = document.createElement('div');
|
| 76 |
+
tip.className = 'd3-tooltip';
|
| 77 |
+
Object.assign(tip.style, {
|
| 78 |
+
position: 'absolute',
|
| 79 |
+
top: '0px',
|
| 80 |
+
left: '0px',
|
| 81 |
+
transform: 'translate(-9999px, -9999px)',
|
| 82 |
+
pointerEvents: 'none',
|
| 83 |
+
padding: '8px 10px',
|
| 84 |
+
borderRadius: '8px',
|
| 85 |
+
fontSize: '12px',
|
| 86 |
+
lineHeight: '1.35',
|
| 87 |
+
border: '1px solid var(--border-color)',
|
| 88 |
+
background: 'var(--surface-bg)',
|
| 89 |
+
color: 'var(--text-color)',
|
| 90 |
+
boxShadow: '0 4px 24px rgba(0,0,0,.18)',
|
| 91 |
+
opacity: '0',
|
| 92 |
+
transition: 'opacity .12s ease'
|
| 93 |
+
});
|
| 94 |
+
tipInner = document.createElement('div');
|
| 95 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 96 |
+
tipInner.style.textAlign = 'left';
|
| 97 |
+
tip.appendChild(tipInner);
|
| 98 |
+
container.appendChild(tip);
|
| 99 |
+
} else {
|
| 100 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Panels container (two side-by-side matrices)
|
| 104 |
+
const panels = document.createElement('div');
|
| 105 |
+
panels.className = 'panels';
|
| 106 |
+
const panelA = document.createElement('div');
|
| 107 |
+
panelA.className = 'panel';
|
| 108 |
+
const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';
|
| 109 |
+
panelA.appendChild(titleA);
|
| 110 |
+
const mountA = document.createElement('div'); panelA.appendChild(mountA);
|
| 111 |
+
const panelB = document.createElement('div');
|
| 112 |
+
panelB.className = 'panel';
|
| 113 |
+
const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved − Baseline, pp)';
|
| 114 |
+
panelB.appendChild(titleB);
|
| 115 |
+
const mountB = document.createElement('div'); panelB.appendChild(mountB);
|
| 116 |
+
panels.appendChild(panelA);
|
| 117 |
+
panels.appendChild(panelB);
|
| 118 |
+
container.appendChild(panels);
|
| 119 |
+
|
| 120 |
+
// SVG scaffolding
|
| 121 |
+
const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);
|
| 122 |
+
const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');
|
| 123 |
+
const gRootA = svgA.append('g');
|
| 124 |
+
const gCellsA = gRootA.append('g');
|
| 125 |
+
const gAxesA = gRootA.append('g');
|
| 126 |
+
const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);
|
| 127 |
+
const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');
|
| 128 |
+
const gRootB = svgB.append('g');
|
| 129 |
+
const gCellsB = gRootB.append('g');
|
| 130 |
+
const gAxesB = gRootB.append('g');
|
| 131 |
+
|
| 132 |
+
// Demo data (two distinct 10x10 matrices: Baseline vs Improved)
|
| 133 |
+
// Rows / Columns are generic class labels
|
| 134 |
+
const classes = ['0','1','2','3','4','5','6','7','8','9'];
|
| 135 |
+
const matrixA = [
|
| 136 |
+
[90, 2, 1, 0, 0, 0, 1, 0, 5, 1],
|
| 137 |
+
[ 3, 85, 5, 1, 0, 1, 2, 1, 1, 1],
|
| 138 |
+
[ 1, 6, 70, 10, 4, 4, 1, 1, 1, 2],
|
| 139 |
+
[ 0, 1, 8, 65, 10, 10, 2, 1, 1, 2],
|
| 140 |
+
[ 0, 0, 2, 6, 83, 3, 1, 1, 3, 1],
|
| 141 |
+
[ 0, 1, 2, 12, 4, 70, 5, 2, 2, 2],
|
| 142 |
+
[ 1, 2, 1, 0, 1, 2, 88, 1, 3, 1],
|
| 143 |
+
[ 0, 1, 1, 1, 1, 1, 2, 90, 1, 2],
|
| 144 |
+
[ 6, 2, 2, 4, 6, 3, 3, 2, 70, 2],
|
| 145 |
+
[ 1, 1, 1, 1, 2, 1, 1, 2, 1, 89]
|
| 146 |
+
];
|
| 147 |
+
const matrixB = [
|
| 148 |
+
[94, 1, 0, 0, 0, 0, 1, 0, 3, 1],
|
| 149 |
+
[ 2, 90, 3, 1, 0, 0, 1, 1, 1, 1],
|
| 150 |
+
[ 1, 4, 78, 7, 3, 3, 1, 1, 1, 1],
|
| 151 |
+
[ 0, 1, 5, 74, 7, 8, 1, 1, 1, 2],
|
| 152 |
+
[ 0, 0, 1, 4, 88, 2, 1, 1, 2, 1],
|
| 153 |
+
[ 0, 1, 1, 9, 3, 78, 3, 1, 2, 2],
|
| 154 |
+
[ 1, 1, 1, 0, 1, 1, 91, 1, 2, 1],
|
| 155 |
+
[ 0, 1, 1, 1, 1, 1, 1, 92, 1, 1],
|
| 156 |
+
[ 4, 1, 1, 3, 4, 2, 2, 2, 79, 2],
|
| 157 |
+
[ 1, 1, 1, 1, 2, 1, 1, 1, 1, 90]
|
| 158 |
+
];
|
| 159 |
+
|
| 160 |
+
// Colors: sequential palette via window.ColorPalettes with graceful fallback
|
| 161 |
+
const getSequentialColors = (count) => {
|
| 162 |
+
try {
|
| 163 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 164 |
+
return window.ColorPalettes.getColors('sequential', count);
|
| 165 |
+
}
|
| 166 |
+
} catch (_) {}
|
| 167 |
+
// Fallback: generate a monochrome scale using the primary color with varying opacity
|
| 168 |
+
const arr = [];
|
| 169 |
+
for (let i = 0; i < count; i++) arr.push('var(--primary-color)');
|
| 170 |
+
return arr;
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
const palette = getSequentialColors(13);
|
| 174 |
+
const getDivergingColors = (count) => {
|
| 175 |
+
try {
|
| 176 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 177 |
+
return window.ColorPalettes.getColors('diverging', count);
|
| 178 |
+
}
|
| 179 |
+
} catch (_) {}
|
| 180 |
+
const steps = Math.max(3, count|0);
|
| 181 |
+
const arr = [];
|
| 182 |
+
for (let i = 0; i < steps; i++) {
|
| 183 |
+
const t = i / (steps - 1);
|
| 184 |
+
const pct = Math.round(t * 100);
|
| 185 |
+
arr.push(`color-mix(in srgb, #D64545 ${100-pct}%, #3A7BD5 ${pct}%)`);
|
| 186 |
+
}
|
| 187 |
+
return arr;
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
let width = 800;
|
| 191 |
+
let height = 480;
|
| 192 |
+
const margin = { top: 36, right: 24, bottom: 26, left: 56 };
|
| 193 |
+
|
| 194 |
+
function updateSize() {
|
| 195 |
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 196 |
+
width = container.clientWidth || 800;
|
| 197 |
+
const gap = 16; // matches CSS .panels gap
|
| 198 |
+
const minPanel = 320;
|
| 199 |
+
const nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;
|
| 200 |
+
const panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);
|
| 201 |
+
const base = Math.max(minPanel, Math.round(panelWidth * 0.92));
|
| 202 |
+
height = base;
|
| 203 |
+
// Responsive SVG: width 100%, height auto, preserve aspect via viewBox
|
| 204 |
+
svgA
|
| 205 |
+
.attr('viewBox', `0 0 ${panelWidth} ${height}`)
|
| 206 |
+
.attr('preserveAspectRatio', 'xMidYMid meet')
|
| 207 |
+
.style('width', '100%')
|
| 208 |
+
.style('height', 'auto');
|
| 209 |
+
svgB
|
| 210 |
+
.attr('viewBox', `0 0 ${panelWidth} ${height}`)
|
| 211 |
+
.attr('preserveAspectRatio', 'xMidYMid meet')
|
| 212 |
+
.style('width', '100%')
|
| 213 |
+
.style('height', 'auto');
|
| 214 |
+
gRootA.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 215 |
+
gRootB.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 216 |
+
const innerWidth = panelWidth - margin.left - margin.right;
|
| 217 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 218 |
+
return { innerWidth, innerHeight, isDark };
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
function computeValues(normalization, matrix) {
|
| 222 |
+
const n = classes.length;
|
| 223 |
+
const totalsByRow = matrix.map(row => row.reduce((a, b) => a + b, 0));
|
| 224 |
+
const flat = [];
|
| 225 |
+
let minV = Infinity, maxV = -Infinity;
|
| 226 |
+
for (let r = 0; r < n; r++) {
|
| 227 |
+
for (let c = 0; c < n; c++) {
|
| 228 |
+
const count = matrix[r][c];
|
| 229 |
+
const value = normalization === 'row' ? (totalsByRow[r] ? count / totalsByRow[r] : 0) : count;
|
| 230 |
+
if (value < minV) minV = value;
|
| 231 |
+
if (value > maxV) maxV = value;
|
| 232 |
+
flat.push({ r, c, count, value });
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
return { data: flat, minV, maxV };
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
function getColorScale(values, minV, maxV) {
|
| 239 |
+
// If ColorPalettes is available, use quantiles to enhance visual variation across the distribution
|
| 240 |
+
const hasPalette = !(palette.length === 0);
|
| 241 |
+
if (hasPalette && (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')) {
|
| 242 |
+
const scale = d3.scaleQuantile().domain(values).range(palette);
|
| 243 |
+
return (v) => scale(v);
|
| 244 |
+
}
|
| 245 |
+
// Fallback: primary color with opacity mapped to normalized value
|
| 246 |
+
const norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);
|
| 247 |
+
return (v) => `color-mix(in oklab, var(--primary-color) ${Math.round(norm(v) * 100)}%, var(--surface-bg))`;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// Compute a fixed readable text color from a CSS rgb()/rgba() string
|
| 251 |
+
function chooseFixedReadableTextOnBg(bgCss){
|
| 252 |
+
try {
|
| 253 |
+
const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/);
|
| 254 |
+
if (!m) return '#0e1116';
|
| 255 |
+
const parts = m[1].split(',').map(s => parseFloat(s.trim()));
|
| 256 |
+
const [r, g, b] = parts;
|
| 257 |
+
// sRGB → relative luminance
|
| 258 |
+
const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);
|
| 259 |
+
const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4)));
|
| 260 |
+
const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2];
|
| 261 |
+
// Threshold ~ 0.5 for readability; darker BG → white text, else near-black
|
| 262 |
+
return L < 0.5 ? '#ffffff' : '#0e1116';
|
| 263 |
+
} catch(_) { return '#0e1116'; }
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
function render() {
|
| 267 |
+
const { innerWidth, innerHeight } = updateSize();
|
| 268 |
+
const n = classes.length;
|
| 269 |
+
const gridSize = Math.min(innerWidth, innerHeight);
|
| 270 |
+
const cellSize = gridSize / n;
|
| 271 |
+
|
| 272 |
+
const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
|
| 273 |
+
const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);
|
| 274 |
+
|
| 275 |
+
// Panel A: Baseline (row-normalized)
|
| 276 |
+
const dataA = computeValues('row', matrixA);
|
| 277 |
+
const colorA = getColorScale(dataA.data.map(d => d.value), dataA.minV, dataA.maxV);
|
| 278 |
+
|
| 279 |
+
gCellsA.selectAll('rect.cell-bg')
|
| 280 |
+
.data([0])
|
| 281 |
+
.join('rect')
|
| 282 |
+
.attr('class', 'cell-bg')
|
| 283 |
+
.attr('x', 0)
|
| 284 |
+
.attr('y', 0)
|
| 285 |
+
.attr('width', gridSize)
|
| 286 |
+
.attr('height', gridSize)
|
| 287 |
+
.attr('fill', 'none')
|
| 288 |
+
.attr('stroke', 'var(--border-color)')
|
| 289 |
+
.attr('stroke-width', 1);
|
| 290 |
+
|
| 291 |
+
const cellsA = gCellsA.selectAll('g.cell')
|
| 292 |
+
.data(dataA.data, d => `${d.r}-${d.c}-A`);
|
| 293 |
+
|
| 294 |
+
const cellsEnterA = cellsA.enter()
|
| 295 |
+
.append('g')
|
| 296 |
+
.attr('class', 'cell');
|
| 297 |
+
|
| 298 |
+
cellsEnterA.append('rect')
|
| 299 |
+
.attr('rx', 2)
|
| 300 |
+
.attr('ry', 2)
|
| 301 |
+
.on('mousemove', (event, d) => {
|
| 302 |
+
const [px, py] = d3.pointer(event, container);
|
| 303 |
+
tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong><br/>${(d.value * 100).toFixed(1)}% (${d.count})`;
|
| 304 |
+
tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
|
| 305 |
+
tip.style.opacity = '1';
|
| 306 |
+
})
|
| 307 |
+
.on('mouseleave', () => {
|
| 308 |
+
tip.style.opacity = '0';
|
| 309 |
+
});
|
| 310 |
+
|
| 311 |
+
cellsEnterA.append('text')
|
| 312 |
+
.attr('class', 'cell-text')
|
| 313 |
+
.attr('text-anchor', 'middle')
|
| 314 |
+
.attr('dominant-baseline', 'middle');
|
| 315 |
+
|
| 316 |
+
const cellsMergedA = cellsEnterA.merge(cellsA);
|
| 317 |
+
|
| 318 |
+
cellsMergedA.select('text')
|
| 319 |
+
.attr('x', d => x(d.c) + x.bandwidth() / 2)
|
| 320 |
+
.attr('y', d => y(d.r) + y.bandwidth() / 2)
|
| 321 |
+
.text(d => `${Math.round(d.value * 100)}`)
|
| 322 |
+
.style('fill', function(d){
|
| 323 |
+
try {
|
| 324 |
+
const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
|
| 325 |
+
const bg = rect ? getComputedStyle(rect).fill : colorA(d.value);
|
| 326 |
+
return chooseFixedReadableTextOnBg(bg);
|
| 327 |
+
} catch (_) {
|
| 328 |
+
return '#0e1116';
|
| 329 |
+
}
|
| 330 |
+
});
|
| 331 |
+
|
| 332 |
+
cellsMergedA.select('rect')
|
| 333 |
+
.attr('x', d => x(d.c))
|
| 334 |
+
.attr('y', d => y(d.r))
|
| 335 |
+
.attr('width', Math.max(1, x.bandwidth()))
|
| 336 |
+
.attr('height', Math.max(1, y.bandwidth()))
|
| 337 |
+
.attr('fill', d => colorA(d.value));
|
| 338 |
+
|
| 339 |
+
cellsA.exit().remove();
|
| 340 |
+
|
| 341 |
+
gAxesA.selectAll('*').remove();
|
| 342 |
+
|
| 343 |
+
gAxesA.append('g')
|
| 344 |
+
.selectAll('text')
|
| 345 |
+
.data(classes)
|
| 346 |
+
.join('text')
|
| 347 |
+
.attr('class', 'axis-label')
|
| 348 |
+
.attr('text-anchor', 'middle')
|
| 349 |
+
.attr('x', (_, i) => x(i) + x.bandwidth() / 2)
|
| 350 |
+
.attr('y', -8)
|
| 351 |
+
.text(d => d);
|
| 352 |
+
|
| 353 |
+
gAxesA.append('g')
|
| 354 |
+
.selectAll('text')
|
| 355 |
+
.data(classes)
|
| 356 |
+
.join('text')
|
| 357 |
+
.attr('class', 'axis-label')
|
| 358 |
+
.attr('text-anchor', 'end')
|
| 359 |
+
.attr('x', -8)
|
| 360 |
+
.attr('y', (_, i) => y(i) + y.bandwidth() / 2)
|
| 361 |
+
.attr('dominant-baseline', 'middle')
|
| 362 |
+
.text(d => d);
|
| 363 |
+
|
| 364 |
+
gAxesA.append('text')
|
| 365 |
+
.attr('class', 'axis-label')
|
| 366 |
+
.attr('text-anchor', 'middle')
|
| 367 |
+
.attr('x', gridSize / 2)
|
| 368 |
+
.attr('y', innerHeight + 20)
|
| 369 |
+
.text('Columns');
|
| 370 |
+
|
| 371 |
+
gAxesA.append('text')
|
| 372 |
+
.attr('class', 'axis-label')
|
| 373 |
+
.attr('text-anchor', 'middle')
|
| 374 |
+
.attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
|
| 375 |
+
.text('Rows');
|
| 376 |
+
|
| 377 |
+
// Panel B: Delta (Improved �� Baseline), row-normalized differences in percentage points
|
| 378 |
+
const dataB = computeValues('row', matrixB);
|
| 379 |
+
const diverging = getDivergingColors(13);
|
| 380 |
+
// Build delta values aligned to A's ordering
|
| 381 |
+
const mapA = new Map(dataA.data.map(d => [d.r + '-' + d.c, d.value]));
|
| 382 |
+
const delta = dataB.data.map(d => ({ r: d.r, c: d.c, count: d.count, value: (d.value - (mapA.get(d.r + '-' + d.c) || 0)) }));
|
| 383 |
+
// Symmetric domain around 0 (in proportions), express later as pp in labels
|
| 384 |
+
const maxAbsDelta = Math.max(0.01, d3.max(delta, d => Math.abs(d.value)) || 0.01);
|
| 385 |
+
const colorB = d3.scaleQuantize().domain([-maxAbsDelta, maxAbsDelta]).range(diverging);
|
| 386 |
+
|
| 387 |
+
gCellsB.selectAll('rect.cell-bg')
|
| 388 |
+
.data([0])
|
| 389 |
+
.join('rect')
|
| 390 |
+
.attr('class', 'cell-bg')
|
| 391 |
+
.attr('x', 0)
|
| 392 |
+
.attr('y', 0)
|
| 393 |
+
.attr('width', gridSize)
|
| 394 |
+
.attr('height', gridSize)
|
| 395 |
+
.attr('fill', 'none')
|
| 396 |
+
.attr('stroke', 'var(--border-color)')
|
| 397 |
+
.attr('stroke-width', 1);
|
| 398 |
+
|
| 399 |
+
const cellsB = gCellsB.selectAll('g.cell')
|
| 400 |
+
.data(dataB.data, d => `${d.r}-${d.c}-B`);
|
| 401 |
+
|
| 402 |
+
const cellsEnterB = cellsB.enter()
|
| 403 |
+
.append('g')
|
| 404 |
+
.attr('class', 'cell');
|
| 405 |
+
|
| 406 |
+
cellsEnterB.append('rect')
|
| 407 |
+
.attr('rx', 2)
|
| 408 |
+
.attr('ry', 2)
|
| 409 |
+
.on('mousemove', (event, d) => {
|
| 410 |
+
const [px, py] = d3.pointer(event, container);
|
| 411 |
+
const a = dataA.data.find(x => x.r===d.r && x.c===d.c);
|
| 412 |
+
const b = dataB.data.find(x => x.r===d.r && x.c===d.c);
|
| 413 |
+
const dv = ((b ? b.value : 0) - (a ? a.value : 0)) * 100;
|
| 414 |
+
tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong>` +
|
| 415 |
+
`<br/>baseline ${(a ? a.value*100 : 0).toFixed(1)}%` +
|
| 416 |
+
`<br/>improved ${(b ? b.value*100 : 0).toFixed(1)}%` +
|
| 417 |
+
`<br/>delta ${dv.toFixed(1)} pp`;
|
| 418 |
+
tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
|
| 419 |
+
tip.style.opacity = '1';
|
| 420 |
+
})
|
| 421 |
+
.on('mouseleave', () => {
|
| 422 |
+
tip.style.opacity = '0';
|
| 423 |
+
});
|
| 424 |
+
|
| 425 |
+
cellsEnterB.append('text')
|
| 426 |
+
.attr('class', 'cell-text')
|
| 427 |
+
.attr('text-anchor', 'middle')
|
| 428 |
+
.attr('dominant-baseline', 'middle');
|
| 429 |
+
|
| 430 |
+
const cellsMergedB = cellsEnterB.merge(cellsB);
|
| 431 |
+
|
| 432 |
+
cellsMergedB.select('rect')
|
| 433 |
+
.attr('x', d => x(d.c))
|
| 434 |
+
.attr('y', d => y(d.r))
|
| 435 |
+
.attr('width', Math.max(1, x.bandwidth()))
|
| 436 |
+
.attr('height', Math.max(1, y.bandwidth()))
|
| 437 |
+
.attr('fill', d => colorB(delta.find(x => x.r===d.r && x.c===d.c).value));
|
| 438 |
+
|
| 439 |
+
cellsMergedB.select('text')
|
| 440 |
+
.attr('x', d => x(d.c) + x.bandwidth() / 2)
|
| 441 |
+
.attr('y', d => y(d.r) + y.bandwidth() / 2)
|
| 442 |
+
.text(d => {
|
| 443 |
+
const dv = delta.find(x => x.r===d.r && x.c===d.c).value; return `${Math.round(dv * 100)}`;
|
| 444 |
+
})
|
| 445 |
+
.style('fill', function(d){
|
| 446 |
+
try {
|
| 447 |
+
const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
|
| 448 |
+
const dv = delta.find(x => x.r===d.r && x.c===d.c).value;
|
| 449 |
+
const bg = rect ? getComputedStyle(rect).fill : colorB(dv);
|
| 450 |
+
return chooseFixedReadableTextOnBg(bg);
|
| 451 |
+
} catch (_) {
|
| 452 |
+
return '#0e1116';
|
| 453 |
+
}
|
| 454 |
+
});
|
| 455 |
+
|
| 456 |
+
cellsB.exit().remove();
|
| 457 |
+
|
| 458 |
+
gAxesB.selectAll('*').remove();
|
| 459 |
+
|
| 460 |
+
gAxesB.append('g')
|
| 461 |
+
.selectAll('text')
|
| 462 |
+
.data(classes)
|
| 463 |
+
.join('text')
|
| 464 |
+
.attr('class', 'axis-label')
|
| 465 |
+
.attr('text-anchor', 'middle')
|
| 466 |
+
.attr('x', (_, i) => x(i) + x.bandwidth() / 2)
|
| 467 |
+
.attr('y', -8)
|
| 468 |
+
.text(d => d);
|
| 469 |
+
|
| 470 |
+
gAxesB.append('g')
|
| 471 |
+
.selectAll('text')
|
| 472 |
+
.data(classes)
|
| 473 |
+
.join('text')
|
| 474 |
+
.attr('class', 'axis-label')
|
| 475 |
+
.attr('text-anchor', 'end')
|
| 476 |
+
.attr('x', -8)
|
| 477 |
+
.attr('y', (_, i) => y(i) + y.bandwidth() / 2)
|
| 478 |
+
.attr('dominant-baseline', 'middle')
|
| 479 |
+
.text(d => d);
|
| 480 |
+
|
| 481 |
+
gAxesB.append('text')
|
| 482 |
+
.attr('class', 'axis-label')
|
| 483 |
+
.attr('text-anchor', 'middle')
|
| 484 |
+
.attr('x', gridSize / 2)
|
| 485 |
+
.attr('y', innerHeight + 20)
|
| 486 |
+
.text('Columns');
|
| 487 |
+
|
| 488 |
+
gAxesB.append('text')
|
| 489 |
+
.attr('class', 'axis-label')
|
| 490 |
+
.attr('text-anchor', 'middle')
|
| 491 |
+
.attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)
|
| 492 |
+
.text('Rows');
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
// Initial render + resize handling
|
| 496 |
+
render();
|
| 497 |
+
const rerender = () => render();
|
| 498 |
+
if (window.ResizeObserver) {
|
| 499 |
+
const ro = new ResizeObserver(() => rerender());
|
| 500 |
+
ro.observe(container);
|
| 501 |
+
} else {
|
| 502 |
+
window.addEventListener('resize', rerender);
|
| 503 |
+
}
|
| 504 |
+
};
|
| 505 |
+
|
| 506 |
+
if (document.readyState === 'loading') {
|
| 507 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 508 |
+
} else {
|
| 509 |
+
ensureD3(bootstrap);
|
| 510 |
+
}
|
| 511 |
+
})();
|
| 512 |
+
</script>
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
|
app/src/content/embeds/{d3-neural.html → d3-neural-network.html}
RENAMED
|
@@ -1,11 +1,21 @@
|
|
| 1 |
-
<div class="d3-neural"
|
| 2 |
<style>
|
|
|
|
| 3 |
.d3-neural .controls { margin-top: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
| 4 |
.d3-neural .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
|
| 5 |
.d3-neural .controls input[type="range"]{ width: 160px; }
|
| 6 |
-
.d3-neural .panel { display:flex; gap:
|
| 7 |
-
.d3-neural .left { flex: 0 0
|
| 8 |
-
.d3-neural .right { flex: 1 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
.d3-neural canvas { width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border-color); background: var(--surface-bg); display:block; }
|
| 10 |
.d3-neural .preview28 { display:grid; grid-template-columns: repeat(28, 1fr); gap: 1px; width: 100%; }
|
| 11 |
.d3-neural .preview28 span { display:block; aspect-ratio:1/1; border-radius:2px; }
|
|
@@ -16,7 +26,8 @@
|
|
| 16 |
.d3-neural .probs .tick { font-size: 10px; color: var(--muted-color); text-align:center; margin-top: 2px; }
|
| 17 |
.d3-neural .canvas-wrap { position: relative; }
|
| 18 |
.d3-neural .erase-btn { position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-color); }
|
| 19 |
-
.d3-neural .canvas-hint { position: absolute; top: 8px; left: 12px; font-size: 12px; font-weight: 700; color:
|
|
|
|
| 20 |
</style>
|
| 21 |
<script>
|
| 22 |
(() => {
|
|
@@ -44,12 +55,16 @@
|
|
| 44 |
if (!container) return;
|
| 45 |
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 46 |
|
|
|
|
|
|
|
| 47 |
// Layout: left (canvas + preview + controls), right (svg network)
|
| 48 |
const panel = document.createElement('div');
|
| 49 |
panel.className = 'panel';
|
| 50 |
const left = document.createElement('div'); left.className = 'left';
|
|
|
|
|
|
|
| 51 |
const right = document.createElement('div'); right.className = 'right';
|
| 52 |
-
panel.appendChild(left); panel.appendChild(right);
|
| 53 |
container.appendChild(panel);
|
| 54 |
|
| 55 |
// Canvas for drawing
|
|
@@ -62,6 +77,8 @@
|
|
| 62 |
canvasWrap.appendChild(canvas);
|
| 63 |
// Erase icon button (top-right)
|
| 64 |
const eraseBtn = document.createElement('button'); eraseBtn.className='erase-btn button--ghost'; eraseBtn.type='button'; eraseBtn.setAttribute('aria-label','Clear');
|
|
|
|
|
|
|
| 65 |
eraseBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg>';
|
| 66 |
eraseBtn.addEventListener('click', () => clearCanvas());
|
| 67 |
canvasWrap.appendChild(eraseBtn);
|
|
@@ -78,6 +95,7 @@
|
|
| 78 |
|
| 79 |
// SVG network on right
|
| 80 |
const svg = d3.select(right).append('svg').attr('width','100%').style('display','block');
|
|
|
|
| 81 |
const gRoot = svg.append('g');
|
| 82 |
const gInput = gRoot.append('g').attr('class','input');
|
| 83 |
const gInputLinks = gRoot.append('g').attr('class','input-links');
|
|
@@ -87,7 +105,7 @@
|
|
| 87 |
const gOutText = gRoot.append('g').attr('class','out-probs');
|
| 88 |
|
| 89 |
// Network structure (compact: 8 -> 8 -> 10)
|
| 90 |
-
const layerSizes = [8, 8,
|
| 91 |
const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
|
| 92 |
// Links only between hidden->hidden and hidden->output
|
| 93 |
const links = [];
|
|
@@ -239,13 +257,15 @@
|
|
| 239 |
function dot(a,b){ let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; return s; }
|
| 240 |
|
| 241 |
// Resize handling and node layout
|
| 242 |
-
let width=
|
| 243 |
let inputGrid = { cell: 0, x: 0, y: 0, width: 0, height: 0 };
|
| 244 |
function layoutNodes(){
|
| 245 |
// Right panel width, and a non-square aspect ratio for clarity
|
| 246 |
-
width = Math.max(
|
| 247 |
-
height = Math.max(260, Math.round(width * 0.
|
| 248 |
svg.attr('width', width).attr('height', height);
|
|
|
|
|
|
|
| 249 |
const innerW = width - margin.left - margin.right; const innerH = height - margin.top - margin.bottom;
|
| 250 |
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 251 |
// Input grid layout (28x28) at left — cap width to a fraction of innerW
|
|
@@ -255,31 +275,46 @@
|
|
| 255 |
let cell = Math.max(3, Math.min(cellByHeight, cellByWidth));
|
| 256 |
let gridH = cell * 28; let gridY = Math.floor((innerH - gridH)/2);
|
| 257 |
inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
|
| 258 |
-
//
|
| 259 |
-
const
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
|
|
|
|
|
|
| 264 |
gridH = cell * 28; gridY = Math.floor((innerH - gridH)/2);
|
| 265 |
inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
|
| 266 |
-
startX = inputGrid.width + 24;
|
| 267 |
}
|
| 268 |
-
const
|
| 269 |
-
|
| 270 |
-
const
|
| 271 |
-
const
|
| 272 |
-
// Reduce inter-layer spacing slightly; keep a sane min/max
|
| 273 |
-
const stepX = nLayers > 1 ? Math.min(200, Math.max(28, availableW / (nLayers - 1))) : 0;
|
| 274 |
-
const xs = Array.from({ length: nLayers }, (_, li) => startX + stepX * li);
|
| 275 |
// Y positions evenly spaced per layer
|
| 276 |
layers.forEach((nodes, li)=>{
|
| 277 |
-
const n = nodes.length;
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
});
|
| 280 |
}
|
| 281 |
|
| 282 |
let lastX28 = new Float32Array(28*28);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
function renderInputGrid(){
|
| 284 |
if (!inputGrid || inputGrid.cell <= 0) return;
|
| 285 |
const data = Array.from({ length: 28*28 }, (_, i) => ({ i, v: lastX28[i] || 0 }));
|
|
@@ -292,34 +327,112 @@
|
|
| 292 |
.merge(sel)
|
| 293 |
.attr('x', d => inputGrid.x + (d.i % 28) * inputGrid.cell + offset)
|
| 294 |
.attr('y', d => inputGrid.y + Math.floor(d.i / 28) * inputGrid.cell + offset)
|
| 295 |
-
.attr('fill', d => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
.attr('stroke', 'none');
|
| 297 |
sel.exit().remove();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
}
|
| 299 |
|
| 300 |
function renderInputLinks(){
|
| 301 |
// Draw bundle-like links from input grid right edge to first layer nodes (features)
|
| 302 |
const firstLayer = layers[0];
|
| 303 |
if (!firstLayer || !inputGrid || inputGrid.cell <= 0) { gInputLinks.selectAll('path').remove(); return; }
|
| 304 |
-
const innerH = height - margin.top - margin.bottom;
|
| 305 |
const x0 = inputGrid.x + inputGrid.width;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
const paths = firstLayer.map((n, idx) => {
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
const
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
| 312 |
});
|
| 313 |
const sel = gInputLinks.selectAll('path.input-link').data(paths);
|
| 314 |
sel.enter().append('path').attr('class','input-link')
|
| 315 |
.attr('fill','none')
|
| 316 |
-
.attr('stroke','
|
|
|
|
| 317 |
.attr('stroke-width', 1)
|
|
|
|
| 318 |
.merge(sel)
|
| 319 |
-
.attr('d', d => `M${d.x0},${d.y0} C${d.c1x},${d.c1y} ${d.c2x},${d.c2y} ${d.x1},${d.y1}`)
|
|
|
|
| 320 |
sel.exit().remove();
|
| 321 |
}
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
function renderGraph(showEdges){
|
| 324 |
layoutNodes();
|
| 325 |
renderInputGrid();
|
|
@@ -330,8 +443,11 @@
|
|
| 330 |
nodeSel.enter().append('circle').attr('class','node')
|
| 331 |
.attr('r', 10)
|
| 332 |
.attr('cx', d=>d.x).attr('cy', d=>d.y)
|
| 333 |
-
.attr('fill', d=> d.layer===2 ? 'var(--
|
| 334 |
-
.attr('
|
|
|
|
|
|
|
|
|
|
| 335 |
.merge(nodeSel)
|
| 336 |
.attr('cx', d=>d.x).attr('cy', d=>d.y)
|
| 337 |
.attr('opacity', 1);
|
|
@@ -339,30 +455,38 @@
|
|
| 339 |
|
| 340 |
// Labels for first hidden layer only (avoid stacking with output probs)
|
| 341 |
const labels = [];
|
| 342 |
-
layers[0].forEach((n,i)=> labels.push({ x:n.x, y:n.y
|
| 343 |
const labSel = gLabels.selectAll('text').data(labels);
|
| 344 |
-
labSel.enter().append('text')
|
| 345 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
.text(d=>d.txt)
|
| 347 |
.merge(labSel)
|
| 348 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
labSel.exit().remove();
|
| 350 |
|
| 351 |
// Links as smooth curves
|
| 352 |
-
const pathFor = (d) => {
|
| 353 |
-
const x1 = layers[d.s.l][d.s.i].x, y1 = layers[d.s.l][d.s.i].y;
|
| 354 |
-
const x2 = layers[d.t.l][d.t.j].x, y2 = layers[d.t.l][d.t.j].y;
|
| 355 |
-
const dx = (x2 - x1) * 0.45;
|
| 356 |
-
return `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`;
|
| 357 |
-
};
|
| 358 |
const linkSel = gLinks.selectAll('path.link').data(links, d=> `${d.s.l}-${d.s.i}-${d.t.l}-${d.t.j}`);
|
| 359 |
linkSel.enter().append('path').attr('class','link')
|
| 360 |
-
.attr('d',
|
| 361 |
.attr('fill','none')
|
| 362 |
-
.attr('stroke','
|
|
|
|
| 363 |
.attr('stroke-width', d=> 0.5 + d.w*1.2)
|
|
|
|
| 364 |
.merge(linkSel)
|
| 365 |
-
.attr('d',
|
|
|
|
| 366 |
.attr('stroke-width', d=> 0.5 + d.w*1.2);
|
| 367 |
linkSel.exit().remove();
|
| 368 |
|
|
@@ -372,8 +496,17 @@
|
|
| 372 |
if (!d || typeof d.digit !== 'number') return d3.select(this).attr('transform');
|
| 373 |
const n = layers[2][d.digit];
|
| 374 |
if (!n) return d3.select(this).attr('transform');
|
| 375 |
-
|
|
|
|
| 376 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
}
|
| 378 |
|
| 379 |
function setNodeActivations(h1, h2, out){
|
|
@@ -385,50 +518,86 @@
|
|
| 385 |
if (Array.isArray(out)) {
|
| 386 |
for (let i=0;i<out.length;i++){ if (out[i] > bestProb){ bestProb = out[i]; argmaxIdx = i; } }
|
| 387 |
}
|
| 388 |
-
// Color/
|
| 389 |
gNodes.selectAll('circle.node')
|
| 390 |
-
.
|
| 391 |
-
.attr('
|
| 392 |
-
.attr('opacity', d=> 0.
|
| 393 |
-
.attr('
|
|
|
|
|
|
|
|
|
|
| 394 |
// Link opacity by activation flow
|
| 395 |
gLinks.selectAll('path.link')
|
| 396 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
const aS = layers[d.s.l][d.s.i].a || 0; const aT = layers[d.t.l][d.t.j].a || 0;
|
| 398 |
-
|
| 399 |
-
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 400 |
-
const base = isDark ? 255 : 0;
|
| 401 |
-
return `rgba(${base},${base},${base},${alpha})`;
|
| 402 |
});
|
| 403 |
-
//
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
const gSel = gOutText.selectAll('g.out-label').data(outs, d=>d.digit);
|
| 406 |
const gEnter = gSel.enter().append('g').attr('class','out-label');
|
| 407 |
gEnter.append('text').attr('class','out-digit')
|
| 408 |
-
.style('font-size','12px').style('font-weight','
|
| 409 |
-
|
| 410 |
-
.
|
| 411 |
-
gEnter.
|
| 412 |
-
.attr('height', 4);
|
| 413 |
-
const BAR_MAX = 64;
|
| 414 |
-
gEnter.merge(gSel)
|
| 415 |
.attr('transform', d=>`translate(${d.x},${d.y})`)
|
| 416 |
.each(function(d){
|
| 417 |
const sel = d3.select(this);
|
| 418 |
sel.select('text.out-digit')
|
| 419 |
-
.attr('x', 0).attr('y',
|
| 420 |
.text(String(d.digit));
|
| 421 |
-
sel.select('rect.out-bar-bg')
|
| 422 |
-
.attr('x', 0).attr('y', 6)
|
| 423 |
-
.attr('width', BAR_MAX);
|
| 424 |
-
sel.select('rect.out-bar')
|
| 425 |
-
.attr('x', 0).attr('y', 6)
|
| 426 |
-
.attr('width', Math.max(1, Math.round(d.prob * BAR_MAX)))
|
| 427 |
-
.attr('fill', d.isTop ? 'var(--primary-color)' : 'var(--border-color)');
|
| 428 |
// Ghost non-top predictions
|
| 429 |
sel.style('opacity', d.isTop ? 1 : 0.35);
|
| 430 |
});
|
|
|
|
|
|
|
|
|
|
| 431 |
gSel.exit().remove();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
}
|
| 433 |
|
| 434 |
// (no separate updateBars; bars are rendered next to nodes)
|
|
@@ -441,6 +610,8 @@
|
|
| 441 |
renderInputGrid();
|
| 442 |
const feats = computeFeatures(x28); // 8D in [0,1]
|
| 443 |
const inkMass = feats[0];
|
|
|
|
|
|
|
| 444 |
// Hidden 1 = raw features
|
| 445 |
const h1 = feats;
|
| 446 |
// Hidden 2 = simple non-linear mix for visualization only
|
|
@@ -497,6 +668,7 @@
|
|
| 497 |
|
| 498 |
// Drawing interactions
|
| 499 |
let drawing=false; let last=null;
|
|
|
|
| 500 |
const getPos = (ev) => {
|
| 501 |
const rect = canvas.getBoundingClientRect();
|
| 502 |
const sx = CANVAS_PX/rect.width; const sy = CANVAS_PX/rect.height;
|
|
@@ -511,7 +683,11 @@
|
|
| 511 |
ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke();
|
| 512 |
last = p; runPipeline();
|
| 513 |
}
|
| 514 |
-
function onDown(ev){
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
function onMove(ev){ if (!drawing) return; drawTo(getPos(ev)); ev.preventDefault(); }
|
| 516 |
function onUp(){ drawing=false; last=null; }
|
| 517 |
canvas.addEventListener('mousedown', onDown); canvas.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp);
|
|
|
|
| 1 |
+
<div class="d3-neural"></div>
|
| 2 |
<style>
|
| 3 |
+
.d3-neural { position: relative; width:100%;margin:0;}
|
| 4 |
.d3-neural .controls { margin-top: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
| 5 |
.d3-neural .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
|
| 6 |
.d3-neural .controls input[type="range"]{ width: 160px; }
|
| 7 |
+
.d3-neural .panel { display:flex; gap:8px; align-items:stretch; flex-wrap: nowrap; }
|
| 8 |
+
.d3-neural .left { flex: 0 0 33.333%; max-width: 33.333%; min-width: 160px; display:flex; flex-direction:column; gap:8px; }
|
| 9 |
+
.d3-neural .right { flex: 1 1 66.666%; max-width: 66.666%; min-width: 280px; display:flex; }
|
| 10 |
+
.d3-neural .right > svg { flex: 1 1 auto; height: 100%; }
|
| 11 |
+
.d3-neural .arrow-sep { flex: 0 0 18px; max-width: 18px; display:flex; align-items:center; justify-content:center; color: var(--muted-color); }
|
| 12 |
+
.d3-neural .arrow-sep svg { display:block; width: 16px; height: 16px; }
|
| 13 |
+
@media (max-width: 800px) {
|
| 14 |
+
.d3-neural .panel { flex-direction: column; }
|
| 15 |
+
.d3-neural .left,
|
| 16 |
+
.d3-neural .right { flex: 0 0 100%; max-width: 100%; min-width: 0; }
|
| 17 |
+
.d3-neural .arrow-sep { display: none; }
|
| 18 |
+
}
|
| 19 |
.d3-neural canvas { width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border-color); background: var(--surface-bg); display:block; }
|
| 20 |
.d3-neural .preview28 { display:grid; grid-template-columns: repeat(28, 1fr); gap: 1px; width: 100%; }
|
| 21 |
.d3-neural .preview28 span { display:block; aspect-ratio:1/1; border-radius:2px; }
|
|
|
|
| 26 |
.d3-neural .probs .tick { font-size: 10px; color: var(--muted-color); text-align:center; margin-top: 2px; }
|
| 27 |
.d3-neural .canvas-wrap { position: relative; }
|
| 28 |
.d3-neural .erase-btn { position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-color); }
|
| 29 |
+
.d3-neural .canvas-hint { position: absolute; top: 8px; left: 12px; font-size: 12px; font-weight: 700; color: rgba(0,0,0,.9); pointer-events: none; transition: opacity .12s ease; }
|
| 30 |
+
|
| 31 |
</style>
|
| 32 |
<script>
|
| 33 |
(() => {
|
|
|
|
| 55 |
if (!container) return;
|
| 56 |
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 57 |
|
| 58 |
+
// (tooltip removed)
|
| 59 |
+
|
| 60 |
// Layout: left (canvas + preview + controls), right (svg network)
|
| 61 |
const panel = document.createElement('div');
|
| 62 |
panel.className = 'panel';
|
| 63 |
const left = document.createElement('div'); left.className = 'left';
|
| 64 |
+
const arrowSep = document.createElement('div'); arrowSep.className = 'arrow-sep';
|
| 65 |
+
arrowSep.innerHTML = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><line x1="3" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><polyline points="17,7 22,12 17,17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
| 66 |
const right = document.createElement('div'); right.className = 'right';
|
| 67 |
+
panel.appendChild(left); panel.appendChild(arrowSep); panel.appendChild(right);
|
| 68 |
container.appendChild(panel);
|
| 69 |
|
| 70 |
// Canvas for drawing
|
|
|
|
| 77 |
canvasWrap.appendChild(canvas);
|
| 78 |
// Erase icon button (top-right)
|
| 79 |
const eraseBtn = document.createElement('button'); eraseBtn.className='erase-btn button--ghost'; eraseBtn.type='button'; eraseBtn.setAttribute('aria-label','Clear');
|
| 80 |
+
// Hidden until the user interacts with the canvas
|
| 81 |
+
eraseBtn.style.display = 'none';
|
| 82 |
eraseBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg>';
|
| 83 |
eraseBtn.addEventListener('click', () => clearCanvas());
|
| 84 |
canvasWrap.appendChild(eraseBtn);
|
|
|
|
| 95 |
|
| 96 |
// SVG network on right
|
| 97 |
const svg = d3.select(right).append('svg').attr('width','100%').style('display','block');
|
| 98 |
+
const defs = svg.append('defs');
|
| 99 |
const gRoot = svg.append('g');
|
| 100 |
const gInput = gRoot.append('g').attr('class','input');
|
| 101 |
const gInputLinks = gRoot.append('g').attr('class','input-links');
|
|
|
|
| 105 |
const gOutText = gRoot.append('g').attr('class','out-probs');
|
| 106 |
|
| 107 |
// Network structure (compact: 8 -> 8 -> 10)
|
| 108 |
+
const layerSizes = [8, 8, 8];
|
| 109 |
const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
|
| 110 |
// Links only between hidden->hidden and hidden->output
|
| 111 |
const links = [];
|
|
|
|
| 257 |
function dot(a,b){ let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; return s; }
|
| 258 |
|
| 259 |
// Resize handling and node layout
|
| 260 |
+
let width=640, height=360; const margin = { top: 16, right: 8, bottom: 24, left: 8 };
|
| 261 |
let inputGrid = { cell: 0, x: 0, y: 0, width: 0, height: 0 };
|
| 262 |
function layoutNodes(){
|
| 263 |
// Right panel width, and a non-square aspect ratio for clarity
|
| 264 |
+
width = Math.max(280, Math.round(right.clientWidth || 640));
|
| 265 |
+
height = Math.max(260, Math.round(width * 0.56));
|
| 266 |
svg.attr('width', width).attr('height', height);
|
| 267 |
+
// Match canvas height to SVG height so both columns align vertically
|
| 268 |
+
try { canvas.style.height = '100%'; canvasWrap.style.height = height + 'px'; } catch(_) {}
|
| 269 |
const innerW = width - margin.left - margin.right; const innerH = height - margin.top - margin.bottom;
|
| 270 |
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 271 |
// Input grid layout (28x28) at left — cap width to a fraction of innerW
|
|
|
|
| 275 |
let cell = Math.max(3, Math.min(cellByHeight, cellByWidth));
|
| 276 |
let gridH = cell * 28; let gridY = Math.floor((innerH - gridH)/2);
|
| 277 |
inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
|
| 278 |
+
// Equal horizontal gaps: grid -> L0 -> L1 -> L2
|
| 279 |
+
const nLayers = layerSizes.length; // 3
|
| 280 |
+
const rightLabelPad = 36; // smaller pad; use more width for spreading layers
|
| 281 |
+
const minGap = 28; const maxGap = 260;
|
| 282 |
+
// Ensure enough free space; shrink grid if needed
|
| 283 |
+
const desiredMinFree = rightLabelPad + nLayers * minGap; // 3 equal gaps
|
| 284 |
+
if (inputGrid.width + desiredMinFree > innerW) {
|
| 285 |
+
cell = Math.max(3, Math.floor((innerW - desiredMinFree) / 28));
|
| 286 |
gridH = cell * 28; gridY = Math.floor((innerH - gridH)/2);
|
| 287 |
inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
|
|
|
|
| 288 |
}
|
| 289 |
+
const gridRight = inputGrid.x + inputGrid.width;
|
| 290 |
+
const freeW = Math.max(nLayers * minGap, innerW - gridRight - rightLabelPad);
|
| 291 |
+
const gapX = Math.min(maxGap, Math.max(minGap, Math.floor(freeW / nLayers)));
|
| 292 |
+
const xs = Array.from({ length: nLayers }, (_, li) => gridRight + gapX * (li + 1));
|
|
|
|
|
|
|
|
|
|
| 293 |
// Y positions evenly spaced per layer
|
| 294 |
layers.forEach((nodes, li)=>{
|
| 295 |
+
const n = nodes.length;
|
| 296 |
+
if (n <= 1) {
|
| 297 |
+
nodes.forEach((nd)=>{ nd.x = xs[li]; nd.y = innerH/2; });
|
| 298 |
+
} else {
|
| 299 |
+
const occupancy = 0.9; // use 90% of vertical space
|
| 300 |
+
const span = innerH * occupancy;
|
| 301 |
+
const topPad = (innerH - span) / 2;
|
| 302 |
+
const spacing = span / (n - 1);
|
| 303 |
+
nodes.forEach((nd, i)=>{ nd.x = xs[li]; nd.y = topPad + i*spacing; });
|
| 304 |
+
}
|
| 305 |
});
|
| 306 |
}
|
| 307 |
|
| 308 |
let lastX28 = new Float32Array(28*28);
|
| 309 |
+
function nodeRadiusForNode(n){
|
| 310 |
+
const a = Math.max(0, Math.min(1, (n && typeof n.a === 'number') ? n.a : 0));
|
| 311 |
+
if (n && n.layer === 2) {
|
| 312 |
+
// Output nodes: variable radius based on activation
|
| 313 |
+
return 8 + 10 * a; // ~8–18
|
| 314 |
+
}
|
| 315 |
+
// Hidden/feature nodes: variable radius based on activation
|
| 316 |
+
return 5 + 5 * a; // ~5–10
|
| 317 |
+
}
|
| 318 |
function renderInputGrid(){
|
| 319 |
if (!inputGrid || inputGrid.cell <= 0) return;
|
| 320 |
const data = Array.from({ length: 28*28 }, (_, i) => ({ i, v: lastX28[i] || 0 }));
|
|
|
|
| 327 |
.merge(sel)
|
| 328 |
.attr('x', d => inputGrid.x + (d.i % 28) * inputGrid.cell + offset)
|
| 329 |
.attr('y', d => inputGrid.y + Math.floor(d.i / 28) * inputGrid.cell + offset)
|
| 330 |
+
.attr('fill', d => {
|
| 331 |
+
// Increase perceived contrast of the input grid by applying a gamma curve
|
| 332 |
+
const k = Math.pow(Math.max(0, Math.min(1, d.v)), 0.6); // gamma < 1 → darker darks
|
| 333 |
+
const g = 255 - Math.round(k * 255);
|
| 334 |
+
return `rgb(${g},${g},${g})`;
|
| 335 |
+
})
|
| 336 |
.attr('stroke', 'none');
|
| 337 |
sel.exit().remove();
|
| 338 |
+
|
| 339 |
+
// Border around the input grid area
|
| 340 |
+
const borderSel = gInput.selectAll('rect.input-border').data([0]);
|
| 341 |
+
borderSel.enter().append('rect').attr('class','input-border')
|
| 342 |
+
.attr('fill','none')
|
| 343 |
+
.attr('rx', 0).attr('ry', 0)
|
| 344 |
+
.attr('stroke','var(--text-color)')
|
| 345 |
+
.attr('stroke-opacity', 0.25)
|
| 346 |
+
.attr('stroke-width', 1)
|
| 347 |
+
.lower()
|
| 348 |
+
.merge(borderSel)
|
| 349 |
+
.attr('x', inputGrid.x-1)
|
| 350 |
+
.attr('y', inputGrid.y-1)
|
| 351 |
+
.attr('width', inputGrid.width+1)
|
| 352 |
+
.attr('height', inputGrid.height+1);
|
| 353 |
+
|
| 354 |
+
// Centered label above the input grid
|
| 355 |
+
const labelSel = gInput.selectAll('text.input-label').data([0]);
|
| 356 |
+
labelSel.enter().append('text').attr('class','input-label')
|
| 357 |
+
.attr('text-anchor','middle')
|
| 358 |
+
.style('font-size','12px')
|
| 359 |
+
.style('font-weight','700')
|
| 360 |
+
.style('fill','var(--muted-color)')
|
| 361 |
+
.merge(labelSel)
|
| 362 |
+
.attr('x', inputGrid.x + inputGrid.width / 2)
|
| 363 |
+
.attr('y', Math.max(12, inputGrid.y - 10))
|
| 364 |
+
.text('Input 28×28');
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
// Compute link path between two layered nodes using their current radii
|
| 368 |
+
function computeLinkD(d){
|
| 369 |
+
const s = layers[d.s.l][d.s.i];
|
| 370 |
+
const t = layers[d.t.l][d.t.j];
|
| 371 |
+
if (!s || !t) return '';
|
| 372 |
+
const rs = nodeRadiusForNode(s);
|
| 373 |
+
const rt = nodeRadiusForNode(t);
|
| 374 |
+
// Use fixed anchors on circle edges for all inter-layer links (except grid->L0 handled elsewhere)
|
| 375 |
+
const x1 = s.x + rs, y1 = s.y; // right edge of source circle
|
| 376 |
+
const x2 = t.x - rt, y2 = t.y; // left edge of target circle
|
| 377 |
+
const dx = (x2 - x1) * 0.45;
|
| 378 |
+
return `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`;
|
| 379 |
}
|
| 380 |
|
| 381 |
function renderInputLinks(){
|
| 382 |
// Draw bundle-like links from input grid right edge to first layer nodes (features)
|
| 383 |
const firstLayer = layers[0];
|
| 384 |
if (!firstLayer || !inputGrid || inputGrid.cell <= 0) { gInputLinks.selectAll('path').remove(); return; }
|
|
|
|
| 385 |
const x0 = inputGrid.x + inputGrid.width;
|
| 386 |
+
// Define a centered vertical band (half grid height) and distribute sources evenly
|
| 387 |
+
const k = firstLayer.length;
|
| 388 |
+
const band = inputGrid.height * 0.5;
|
| 389 |
+
const centerY = inputGrid.y + inputGrid.height / 2;
|
| 390 |
+
const yStart = centerY - band / 2;
|
| 391 |
+
const spacing = k > 1 ? band / (k - 1) : 0;
|
| 392 |
const paths = firstLayer.map((n, idx) => {
|
| 393 |
+
// source y from centered band, equidistant
|
| 394 |
+
const y0 = k > 1 ? (yStart + idx * spacing) : centerY;
|
| 395 |
+
// Target anchor: center of left edge of the node circle
|
| 396 |
+
const r = nodeRadiusForNode(n);
|
| 397 |
+
const x1 = n.x - r;
|
| 398 |
+
const y1 = n.y;
|
| 399 |
+
const dx = (x1 - x0) * 0.35;
|
| 400 |
+
return { x0, y0, x1, y1, c1x: x0 + dx, c1y: y0, c2x: x1 - dx, c2y: y1, idx };
|
| 401 |
});
|
| 402 |
const sel = gInputLinks.selectAll('path.input-link').data(paths);
|
| 403 |
sel.enter().append('path').attr('class','input-link')
|
| 404 |
.attr('fill','none')
|
| 405 |
+
.attr('stroke','var(--text-color)')
|
| 406 |
+
.attr('stroke-opacity', 0.25)
|
| 407 |
.attr('stroke-width', 1)
|
| 408 |
+
.attr('stroke-linecap','round')
|
| 409 |
.merge(sel)
|
| 410 |
+
.attr('d', d => `M${d.x0},${d.y0} C${d.c1x},${d.c1y} ${d.c2x},${d.c2y} ${d.x1},${d.y1}`)
|
| 411 |
+
.attr('stroke','var(--text-color)');
|
| 412 |
sel.exit().remove();
|
| 413 |
}
|
| 414 |
|
| 415 |
+
// Recompute input link path on the fly (used when node radii change)
|
| 416 |
+
function computeInputLinkD(idx){
|
| 417 |
+
const firstLayer = layers[0];
|
| 418 |
+
const n = firstLayer[idx]; if (!n) return '';
|
| 419 |
+
const x0 = inputGrid.x + inputGrid.width;
|
| 420 |
+
const k = firstLayer.length;
|
| 421 |
+
const band = inputGrid.height * 0.5;
|
| 422 |
+
const centerY = inputGrid.y + inputGrid.height / 2;
|
| 423 |
+
const yStart = centerY - band / 2;
|
| 424 |
+
const spacing = k > 1 ? band / (k - 1) : 0;
|
| 425 |
+
const y0 = k > 1 ? (yStart + idx * spacing) : centerY;
|
| 426 |
+
const yTarget = n.y;
|
| 427 |
+
const vx = n.x - x0; const vy = yTarget - y0; const L = Math.hypot(vx, vy) || 1;
|
| 428 |
+
const r = nodeRadiusForNode(n);
|
| 429 |
+
const x1 = n.x - (vx / L) * r;
|
| 430 |
+
const y1 = yTarget - (vy / L) * r;
|
| 431 |
+
const dx = (x1 - x0) * 0.35;
|
| 432 |
+
const c1x = x0 + dx, c1y = y0, c2x = x1 - dx, c2y = y1;
|
| 433 |
+
return `M${x0},${y0} C${c1x},${c1y} ${c2x},${c2y} ${x1},${y1}`;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
function renderGraph(showEdges){
|
| 437 |
layoutNodes();
|
| 438 |
renderInputGrid();
|
|
|
|
| 443 |
nodeSel.enter().append('circle').attr('class','node')
|
| 444 |
.attr('r', 10)
|
| 445 |
.attr('cx', d=>d.x).attr('cy', d=>d.y)
|
| 446 |
+
.attr('fill', d=> d.layer===2 ? 'var(--page-bg)' : 'var(--primary-color)')
|
| 447 |
+
.attr('fill-opacity', d=> d.layer===2 ? 1 : 0.12)
|
| 448 |
+
.attr('stroke', d=> d.layer===2 ? 'var(--border-color)' : 'var(--border-color)')
|
| 449 |
+
.attr('stroke-width',1)
|
| 450 |
+
.attr('stroke-linejoin','round')
|
| 451 |
.merge(nodeSel)
|
| 452 |
.attr('cx', d=>d.x).attr('cy', d=>d.y)
|
| 453 |
.attr('opacity', 1);
|
|
|
|
| 455 |
|
| 456 |
// Labels for first hidden layer only (avoid stacking with output probs)
|
| 457 |
const labels = [];
|
| 458 |
+
layers[0].forEach((n,i)=> labels.push({ x:n.x-30, y:n.y+4, txt:`f${i+1}` }));
|
| 459 |
const labSel = gLabels.selectAll('text').data(labels);
|
| 460 |
+
labSel.enter().append('text')
|
| 461 |
+
.style('font-size','10px')
|
| 462 |
+
.style('fill','var(--muted-color)')
|
| 463 |
+
.style('paint-order','stroke')
|
| 464 |
+
.style('stroke','var(--page-bg)')
|
| 465 |
+
.style('stroke-width','3px')
|
| 466 |
+
.attr('x', d=>d.x)
|
| 467 |
+
.attr('y', d=>d.y)
|
| 468 |
.text(d=>d.txt)
|
| 469 |
.merge(labSel)
|
| 470 |
+
.style('paint-order','stroke')
|
| 471 |
+
.style('stroke','var(--page-bg)')
|
| 472 |
+
.style('stroke-width','5px')
|
| 473 |
+
.attr('x', d=>d.x)
|
| 474 |
+
.attr('y', d=>d.y)
|
| 475 |
+
.text(d=>d.txt);
|
| 476 |
labSel.exit().remove();
|
| 477 |
|
| 478 |
// Links as smooth curves
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
const linkSel = gLinks.selectAll('path.link').data(links, d=> `${d.s.l}-${d.s.i}-${d.t.l}-${d.t.j}`);
|
| 480 |
linkSel.enter().append('path').attr('class','link')
|
| 481 |
+
.attr('d', computeLinkD)
|
| 482 |
.attr('fill','none')
|
| 483 |
+
.attr('stroke','var(--text-color)')
|
| 484 |
+
.attr('stroke-opacity', 0.25)
|
| 485 |
.attr('stroke-width', d=> 0.5 + d.w*1.2)
|
| 486 |
+
.attr('stroke-linecap','round')
|
| 487 |
.merge(linkSel)
|
| 488 |
+
.attr('d', computeLinkD)
|
| 489 |
+
.attr('stroke','var(--text-color)')
|
| 490 |
.attr('stroke-width', d=> 0.5 + d.w*1.2);
|
| 491 |
linkSel.exit().remove();
|
| 492 |
|
|
|
|
| 496 |
if (!d || typeof d.digit !== 'number') return d3.select(this).attr('transform');
|
| 497 |
const n = layers[2][d.digit];
|
| 498 |
if (!n) return d3.select(this).attr('transform');
|
| 499 |
+
const offset = nodeRadiusForNode(n) + 8;
|
| 500 |
+
return `translate(${n.x + offset},${n.y})`;
|
| 501 |
});
|
| 502 |
+
// Ensure clip-path circles are updated on resize
|
| 503 |
+
if (defs) {
|
| 504 |
+
const clips = defs.selectAll('clipPath.clip-node').data(layers[2], d=>d.id);
|
| 505 |
+
const ce = clips.enter().append('clipPath').attr('class','clip-node').attr('clipPathUnits','userSpaceOnUse').attr('id', d=>`clip-${d.id}`);
|
| 506 |
+
ce.append('circle');
|
| 507 |
+
clips.merge(ce).select('circle').attr('cx', d=>d.x).attr('cy', d=>d.y).attr('r', d=>nodeRadiusForNode(d));
|
| 508 |
+
clips.exit().remove();
|
| 509 |
+
}
|
| 510 |
}
|
| 511 |
|
| 512 |
function setNodeActivations(h1, h2, out){
|
|
|
|
| 518 |
if (Array.isArray(out)) {
|
| 519 |
for (let i=0;i<out.length;i++){ if (out[i] > bestProb){ bestProb = out[i]; argmaxIdx = i; } }
|
| 520 |
}
|
| 521 |
+
// Color/size by activation with smooth transitions
|
| 522 |
gNodes.selectAll('circle.node')
|
| 523 |
+
.transition().duration(180).ease(d3.easeCubicOut)
|
| 524 |
+
.attr('fill', d=> d.layer===2 ? 'var(--page-bg)' : 'var(--primary-color)')
|
| 525 |
+
.attr('fill-opacity', d=> d.layer===2 ? 1 : (0.12 + 0.58*Math.min(1, d.a||0)))
|
| 526 |
+
.attr('stroke', 'var(--primary-color)')
|
| 527 |
+
.attr('stroke-opacity', d=> (d.layer===2 ? 0.9 : (0.45 + 0.45*Math.min(1, d.a||0))))
|
| 528 |
+
.attr('opacity', d=> 0.55 + 0.45*Math.min(1, d.a||0))
|
| 529 |
+
.attr('r', d=> nodeRadiusForNode(d));
|
| 530 |
// Link opacity by activation flow
|
| 531 |
gLinks.selectAll('path.link')
|
| 532 |
+
.transition().duration(180).ease(d3.easeCubicOut)
|
| 533 |
+
.attr('d', computeLinkD)
|
| 534 |
+
.attr('stroke','var(--text-color)')
|
| 535 |
+
.attr('stroke-opacity', d=>{
|
| 536 |
+
const aS = layers[d.s.l][d.s.i].a || 0; const aT = layers[d.t.l][d.t.j].a || 0;
|
| 537 |
+
return Math.min(1, 0.15 + 0.85 * (aS * aT));
|
| 538 |
+
})
|
| 539 |
+
.attr('stroke-width', d=>{
|
| 540 |
const aS = layers[d.s.l][d.s.i].a || 0; const aT = layers[d.t.l][d.t.j].a || 0;
|
| 541 |
+
return 0.6 + 2.2*(aS*aT);
|
|
|
|
|
|
|
|
|
|
| 542 |
});
|
| 543 |
+
// Theme-aware and activation-aware input links
|
| 544 |
+
gInputLinks.selectAll('path.input-link')
|
| 545 |
+
.transition().duration(180).ease(d3.easeCubicOut)
|
| 546 |
+
.attr('d', (d)=> computeInputLinkD(d.idx))
|
| 547 |
+
.attr('stroke','var(--text-color)')
|
| 548 |
+
.attr('stroke-opacity', 0.25)
|
| 549 |
+
.attr('stroke-width', d=> 0.6 + 2.0*(layers[0][d.idx] ? (layers[0][d.idx].a||0) : 0));
|
| 550 |
+
// Update clip-path circles to match new radii/positions of output nodes
|
| 551 |
+
if (defs) {
|
| 552 |
+
const clips = defs.selectAll('clipPath.clip-node').data(layers[2], d=>d.id);
|
| 553 |
+
clips.select('circle')
|
| 554 |
+
.transition().duration(180).ease(d3.easeCubicOut)
|
| 555 |
+
.attr('cx', d=>d.x)
|
| 556 |
+
.attr('cy', d=>d.y)
|
| 557 |
+
.attr('r', d=> nodeRadiusForNode(d));
|
| 558 |
+
}
|
| 559 |
+
// Theme-aware input links on updates handled above via transition
|
| 560 |
+
// Output labels: digit placed to the right of the node
|
| 561 |
+
const outs = layers[2].map((n,i)=>({ x:n.x + nodeRadiusForNode(n) + 8, y:n.y, digit: i, prob: (out[i]||0), isTop: i===argmaxIdx }));
|
| 562 |
const gSel = gOutText.selectAll('g.out-label').data(outs, d=>d.digit);
|
| 563 |
const gEnter = gSel.enter().append('g').attr('class','out-label');
|
| 564 |
gEnter.append('text').attr('class','out-digit')
|
| 565 |
+
.style('font-size','12px').style('font-weight','800').style('fill','var(--text-color)')
|
| 566 |
+
.attr('text-anchor','start').attr('dominant-baseline','middle')
|
| 567 |
+
.style('paint-order','stroke').style('stroke','var(--transparent-page-contrast)').style('stroke-width','3px');
|
| 568 |
+
const merged = gEnter.merge(gSel)
|
|
|
|
|
|
|
|
|
|
| 569 |
.attr('transform', d=>`translate(${d.x},${d.y})`)
|
| 570 |
.each(function(d){
|
| 571 |
const sel = d3.select(this);
|
| 572 |
sel.select('text.out-digit')
|
| 573 |
+
.attr('x', 0).attr('y', 0)
|
| 574 |
.text(String(d.digit));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
// Ghost non-top predictions
|
| 576 |
sel.style('opacity', d.isTop ? 1 : 0.35);
|
| 577 |
});
|
| 578 |
+
// Remove any previous decorative rings (no highlight ring desired)
|
| 579 |
+
gRoot.selectAll('circle.top-ring').remove();
|
| 580 |
+
// (tooltip interactions removed)
|
| 581 |
gSel.exit().remove();
|
| 582 |
+
|
| 583 |
+
// Output liquid fill using clipPath + rect from bottom
|
| 584 |
+
const rects = gNodes.selectAll('rect.out-liquid').data(layers[2], d=>d.id);
|
| 585 |
+
const rectEnter = rects.enter().append('rect').attr('class','out-liquid')
|
| 586 |
+
.attr('fill','var(--primary-color)')
|
| 587 |
+
.attr('fill-opacity', 0.55)
|
| 588 |
+
.attr('clip-path', d => `url(#clip-${d.id})`);
|
| 589 |
+
rectEnter.merge(rects)
|
| 590 |
+
.transition().duration(180).ease(d3.easeCubicOut)
|
| 591 |
+
.attr('x', d=> d.x - nodeRadiusForNode(d))
|
| 592 |
+
.attr('width', d=> 2 * nodeRadiusForNode(d))
|
| 593 |
+
.attr('y', d=> {
|
| 594 |
+
const r = nodeRadiusForNode(d);
|
| 595 |
+
const h = 2 * r * Math.max(0, Math.min(1, d.a||0));
|
| 596 |
+
return d.y + r - h;
|
| 597 |
+
})
|
| 598 |
+
.attr('height', d=> 2 * nodeRadiusForNode(d) * Math.max(0, Math.min(1, d.a||0)))
|
| 599 |
+
.attr('fill-opacity', 0.55);
|
| 600 |
+
rects.exit().remove();
|
| 601 |
}
|
| 602 |
|
| 603 |
// (no separate updateBars; bars are rendered next to nodes)
|
|
|
|
| 610 |
renderInputGrid();
|
| 611 |
const feats = computeFeatures(x28); // 8D in [0,1]
|
| 612 |
const inkMass = feats[0];
|
| 613 |
+
// Hide hint when user has drawn something
|
| 614 |
+
if (hint) { hint.style.opacity = inkMass < 0.01 ? 1 : 0; }
|
| 615 |
// Hidden 1 = raw features
|
| 616 |
const h1 = feats;
|
| 617 |
// Hidden 2 = simple non-linear mix for visualization only
|
|
|
|
| 668 |
|
| 669 |
// Drawing interactions
|
| 670 |
let drawing=false; let last=null;
|
| 671 |
+
let hasInteracted=false;
|
| 672 |
const getPos = (ev) => {
|
| 673 |
const rect = canvas.getBoundingClientRect();
|
| 674 |
const sx = CANVAS_PX/rect.width; const sy = CANVAS_PX/rect.height;
|
|
|
|
| 683 |
ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke();
|
| 684 |
last = p; runPipeline();
|
| 685 |
}
|
| 686 |
+
function onDown(ev){
|
| 687 |
+
drawing=true; last=null;
|
| 688 |
+
if (!hasInteracted){ hasInteracted=true; try { eraseBtn.style.display = 'flex'; } catch(_) {} }
|
| 689 |
+
drawTo(getPos(ev)); ev.preventDefault();
|
| 690 |
+
}
|
| 691 |
function onMove(ev){ if (!drawing) return; drawTo(getPos(ev)); ev.preventDefault(); }
|
| 692 |
function onUp(){ drawing=false; last=null; }
|
| 693 |
canvas.addEventListener('mousedown', onDown); canvas.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp);
|
app/src/content/embeds/d3-pie.html
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
<div class="d3-pie"
|
| 2 |
<style>
|
| 3 |
/* Layout piloté par container queries (par rapport au parent) */
|
| 4 |
.d3-pie { container-type: inline-size; }
|
|
@@ -11,7 +11,16 @@
|
|
| 11 |
.d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
|
| 12 |
.d3-pie .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }
|
| 13 |
.d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
/* Layout HTML (pas JS) pour la grille et les cellules */
|
| 16 |
.d3-pie .plots-grid {
|
| 17 |
display: flex;
|
|
@@ -50,6 +59,41 @@
|
|
| 50 |
@media (max-width: 500px) {
|
| 51 |
.d3-pie .pie-cell { flex: 0 0 100%; }
|
| 52 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
</style>
|
| 54 |
<script>
|
| 55 |
(() => {
|
|
@@ -87,8 +131,9 @@
|
|
| 87 |
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
| 88 |
Object.assign(tip.style, {
|
| 89 |
position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
|
| 90 |
-
padding:'
|
| 91 |
-
background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0
|
|
|
|
| 92 |
});
|
| 93 |
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
| 94 |
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
|
@@ -148,7 +193,7 @@
|
|
| 148 |
legendHost.style.display = 'flex';
|
| 149 |
legendHost.style.alignItems = 'center';
|
| 150 |
legendHost.style.justifyContent = 'center';
|
| 151 |
-
legendHost.innerHTML = `<div class="items">${categories.map(c => `<div class="item"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:500">${c}</span></div>`).join('')}</div>`;
|
| 152 |
}
|
| 153 |
|
| 154 |
function drawPies(rows){
|
|
@@ -184,7 +229,8 @@
|
|
| 184 |
const totals = new Map(); categories.forEach(c => totals.set(c, 0));
|
| 185 |
rows.forEach(r => { totals.set(r.eagle_cathegory, totals.get(r.eagle_cathegory) + (r[metric.key] || 0)); });
|
| 186 |
const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
|
| 187 |
-
const
|
|
|
|
| 188 |
|
| 189 |
// Create HTML cell container
|
| 190 |
const cell = document.createElement('div');
|
|
@@ -201,10 +247,10 @@
|
|
| 201 |
const svg = d3.select(cell).append('svg').attr('width', radius * 2).attr('height', radius * 2 + SVG_VPAD * 2).style('display','block');
|
| 202 |
const gCell = svg.append('g').attr('transform', `translate(${radius},${radius + SVG_VPAD})`);
|
| 203 |
|
| 204 |
-
if (!totalSum || totalSum <= 0) {
|
| 205 |
gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');
|
| 206 |
} else {
|
| 207 |
-
const data = pie(
|
| 208 |
const percent = (v) => (v / totalSum) * 100;
|
| 209 |
|
| 210 |
// Slices
|
|
@@ -213,16 +259,33 @@
|
|
| 213 |
.attr('fill', d => colorOf(d.data.category))
|
| 214 |
.attr('stroke', 'var(--surface-bg)')
|
| 215 |
.attr('stroke-width', 1.2)
|
|
|
|
| 216 |
.on('mouseenter', function(ev, d){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
|
| 218 |
const p = percent(d.data.value);
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
tip.style.opacity = '1';
|
| 221 |
})
|
| 222 |
.on('mousemove', function(ev){
|
| 223 |
const [mx, my] = d3.pointer(ev, container); const offsetX = 12, offsetY = 12; tip.style.transform = `translate(${Math.round(mx+offsetX)}px, ${Math.round(my+offsetY)}px)`;
|
| 224 |
})
|
| 225 |
-
.on('mouseleave', function(){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
// Percentage labels (>= 3%)
|
| 228 |
gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()
|
|
@@ -233,11 +296,20 @@
|
|
| 233 |
}
|
| 234 |
|
| 235 |
// HTML captions under the SVG (keep design)
|
| 236 |
-
const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center';
|
| 237 |
const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);
|
| 238 |
});
|
| 239 |
|
| 240 |
// Container height flows naturally with HTML; nothing to do
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
}
|
| 242 |
|
| 243 |
async function init(){
|
|
|
|
| 1 |
+
<div class="d3-pie"></div>
|
| 2 |
<style>
|
| 3 |
/* Layout piloté par container queries (par rapport au parent) */
|
| 4 |
.d3-pie { container-type: inline-size; }
|
|
|
|
| 11 |
.d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
|
| 12 |
.d3-pie .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }
|
| 13 |
.d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
|
| 14 |
+
/* Ghost legend items when hovering slices */
|
| 15 |
+
.d3-pie.hovering .legend .item.ghost { opacity: .35; }
|
| 16 |
+
.d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: var(--transparent-page-contrast); stroke-width: 3px; }
|
| 17 |
+
/* Effet ghost synchronisé */
|
| 18 |
+
.d3-pie .slice {
|
| 19 |
+
transition: opacity .15s ease;
|
| 20 |
+
}
|
| 21 |
+
.d3-pie.hovering .slice.ghost {
|
| 22 |
+
opacity: .25;
|
| 23 |
+
}
|
| 24 |
/* Layout HTML (pas JS) pour la grille et les cellules */
|
| 25 |
.d3-pie .plots-grid {
|
| 26 |
display: flex;
|
|
|
|
| 59 |
@media (max-width: 500px) {
|
| 60 |
.d3-pie .pie-cell { flex: 0 0 100%; }
|
| 61 |
}
|
| 62 |
+
/* Tooltip styling aligned with filters-quad */
|
| 63 |
+
.d3-pie .d3-tooltip {
|
| 64 |
+
z-index: var(--z-elevated);
|
| 65 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 66 |
+
}
|
| 67 |
+
.d3-pie .d3-tooltip__inner {
|
| 68 |
+
display: flex;
|
| 69 |
+
flex-direction: column;
|
| 70 |
+
gap: 6px;
|
| 71 |
+
min-width: 220px;
|
| 72 |
+
}
|
| 73 |
+
.d3-pie .d3-tooltip__inner > div:first-child {
|
| 74 |
+
font-weight: 800;
|
| 75 |
+
letter-spacing: 0.1px;
|
| 76 |
+
margin-bottom: 0;
|
| 77 |
+
}
|
| 78 |
+
.d3-pie .d3-tooltip__inner > div:nth-child(2) {
|
| 79 |
+
font-size: 11px;
|
| 80 |
+
color: var(--muted-color);
|
| 81 |
+
display: block;
|
| 82 |
+
margin-top: -4px;
|
| 83 |
+
margin-bottom: 2px;
|
| 84 |
+
letter-spacing: 0.1px;
|
| 85 |
+
}
|
| 86 |
+
.d3-pie .d3-tooltip__inner > div:nth-child(n+3) {
|
| 87 |
+
padding-top: 6px;
|
| 88 |
+
border-top: 1px solid var(--border-color);
|
| 89 |
+
}
|
| 90 |
+
.d3-pie .d3-tooltip__color-dot {
|
| 91 |
+
display: inline-block;
|
| 92 |
+
width: 12px;
|
| 93 |
+
height: 12px;
|
| 94 |
+
border-radius: 3px;
|
| 95 |
+
border: 1px solid var(--border-color);
|
| 96 |
+
}
|
| 97 |
</style>
|
| 98 |
<script>
|
| 99 |
(() => {
|
|
|
|
| 131 |
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
| 132 |
Object.assign(tip.style, {
|
| 133 |
position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',
|
| 134 |
+
padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
|
| 135 |
+
background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity:'0', transition:'opacity .12s ease',
|
| 136 |
+
zIndex: 'var(--z-elevated)', backdropFilter: 'saturate(1.12) blur(8px)'
|
| 137 |
});
|
| 138 |
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
| 139 |
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
|
|
|
| 193 |
legendHost.style.display = 'flex';
|
| 194 |
legendHost.style.alignItems = 'center';
|
| 195 |
legendHost.style.justifyContent = 'center';
|
| 196 |
+
legendHost.innerHTML = `<div class="items">${categories.map(c => `<div class="item" data-category="${c}"><span class="swatch" style="background:${colorOf(c)}"></span><span style="font-weight:500">${c}</span></div>`).join('')}</div>`;
|
| 197 |
}
|
| 198 |
|
| 199 |
function drawPies(rows){
|
|
|
|
| 229 |
const totals = new Map(); categories.forEach(c => totals.set(c, 0));
|
| 230 |
rows.forEach(r => { totals.set(r.eagle_cathegory, totals.get(r.eagle_cathegory) + (r[metric.key] || 0)); });
|
| 231 |
const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
|
| 232 |
+
const nonZeroValues = values.filter(v => (v.value || 0) > 0);
|
| 233 |
+
const totalSum = d3.sum(nonZeroValues, d => d.value);
|
| 234 |
|
| 235 |
// Create HTML cell container
|
| 236 |
const cell = document.createElement('div');
|
|
|
|
| 247 |
const svg = d3.select(cell).append('svg').attr('width', radius * 2).attr('height', radius * 2 + SVG_VPAD * 2).style('display','block');
|
| 248 |
const gCell = svg.append('g').attr('transform', `translate(${radius},${radius + SVG_VPAD})`);
|
| 249 |
|
| 250 |
+
if (!totalSum || totalSum <= 0 || nonZeroValues.length === 0) {
|
| 251 |
gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');
|
| 252 |
} else {
|
| 253 |
+
const data = pie(nonZeroValues);
|
| 254 |
const percent = (v) => (v / totalSum) * 100;
|
| 255 |
|
| 256 |
// Slices
|
|
|
|
| 259 |
.attr('fill', d => colorOf(d.data.category))
|
| 260 |
.attr('stroke', 'var(--surface-bg)')
|
| 261 |
.attr('stroke-width', 1.2)
|
| 262 |
+
.attr('data-category', d => d.data.category)
|
| 263 |
.on('mouseenter', function(ev, d){
|
| 264 |
+
const hoveredCategory = d.data.category;
|
| 265 |
+
d3.select(container).classed('hovering', true);
|
| 266 |
+
d3.select(container).selectAll('path.slice').classed('ghost', s => (s.data && s.data.category) !== hoveredCategory);
|
| 267 |
+
// Ghost legend items that are not hovered
|
| 268 |
+
d3.select(legendHost).selectAll('.item').classed('ghost', function(){ return this.dataset && this.dataset.category !== hoveredCategory; });
|
| 269 |
d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);
|
| 270 |
const p = percent(d.data.value);
|
| 271 |
+
const catColor = colorOf(d.data.category);
|
| 272 |
+
let html = `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${catColor}\"></span><strong>${d.data.category}</strong></div>`;
|
| 273 |
+
html += `<div>${metric.name}</div>`;
|
| 274 |
+
html += `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>Value</strong><span style="margin-left:auto;text-align:right;">${d.data.value.toLocaleString()}</span></div>`;
|
| 275 |
+
/* Share row removed per request */
|
| 276 |
+
tipInner.innerHTML = html;
|
| 277 |
tip.style.opacity = '1';
|
| 278 |
})
|
| 279 |
.on('mousemove', function(ev){
|
| 280 |
const [mx, my] = d3.pointer(ev, container); const offsetX = 12, offsetY = 12; tip.style.transform = `translate(${Math.round(mx+offsetX)}px, ${Math.round(my+offsetY)}px)`;
|
| 281 |
})
|
| 282 |
+
.on('mouseleave', function(){
|
| 283 |
+
tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)';
|
| 284 |
+
d3.select(container).classed('hovering', false);
|
| 285 |
+
d3.select(container).selectAll('path.slice').classed('ghost', false);
|
| 286 |
+
d3.select(legendHost).selectAll('.item').classed('ghost', false);
|
| 287 |
+
d3.select(this).attr('stroke','var(--surface-bg)');
|
| 288 |
+
});
|
| 289 |
|
| 290 |
// Percentage labels (>= 3%)
|
| 291 |
gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()
|
|
|
|
| 296 |
}
|
| 297 |
|
| 298 |
// HTML captions under the SVG (keep design)
|
| 299 |
+
const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center'; cell.appendChild(subtitleEl);
|
| 300 |
const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);
|
| 301 |
});
|
| 302 |
|
| 303 |
// Container height flows naturally with HTML; nothing to do
|
| 304 |
+
|
| 305 |
+
// Reset global hover/ghost when leaving the plots area
|
| 306 |
+
plotsHost.onmouseleave = () => {
|
| 307 |
+
tip.style.opacity='0';
|
| 308 |
+
tip.style.transform='translate(-9999px, -9999px)';
|
| 309 |
+
d3.select(container).classed('hovering', false);
|
| 310 |
+
d3.select(container).selectAll('path.slice').classed('ghost', false);
|
| 311 |
+
d3.select(legendHost).selectAll('.item').classed('ghost', false);
|
| 312 |
+
};
|
| 313 |
}
|
| 314 |
|
| 315 |
async function init(){
|
app/src/content/embeds/d3-scatter.html
CHANGED
|
@@ -1,7 +1,35 @@
|
|
| 1 |
-
<div class="d3-scatter"
|
| 2 |
<style>
|
| 3 |
/* Frameless: no controls, no axes, only dots */
|
| 4 |
.d3-scatter svg { display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
</style>
|
| 6 |
<script>
|
| 7 |
(() => {
|
|
@@ -29,7 +57,7 @@
|
|
| 29 |
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 30 |
if (!tip) {
|
| 31 |
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
| 32 |
-
Object.assign(tip.style, { position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'
|
| 33 |
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
| 34 |
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 35 |
|
|
@@ -48,6 +76,8 @@
|
|
| 48 |
const y = d3.scaleLinear();
|
| 49 |
const color = d3.scaleOrdinal();
|
| 50 |
const radius = () => 4;
|
|
|
|
|
|
|
| 51 |
|
| 52 |
// Data loading (real): banner visualization positions by category
|
| 53 |
async function fetchFirstAvailable(paths){
|
|
@@ -68,9 +98,10 @@
|
|
| 68 |
|
| 69 |
function updateScales(data){
|
| 70 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 71 |
-
|
| 72 |
-
const
|
| 73 |
-
const
|
|
|
|
| 74 |
|
| 75 |
width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
|
| 76 |
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
@@ -111,17 +142,24 @@
|
|
| 111 |
dots.enter().append('circle').attr('class','dot')
|
| 112 |
.attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
|
| 113 |
.attr('fill', fillFor).attr('fill-opacity', 0.85)
|
|
|
|
| 114 |
.on('mouseenter', function(ev, d){
|
| 115 |
-
d3.select(this).
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
tip.style.opacity = '1';
|
| 118 |
})
|
| 119 |
.on('mousemove', function(ev){ const [mx, my] = d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
|
| 120 |
-
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).
|
| 121 |
.merge(dots)
|
| 122 |
.transition().duration(180)
|
| 123 |
.attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
|
| 124 |
-
.attr('fill', fillFor).attr('fill-opacity', 0.85)
|
|
|
|
| 125 |
dots.exit().remove();
|
| 126 |
|
| 127 |
// Compute centroids per category
|
|
@@ -173,14 +211,14 @@
|
|
| 173 |
.attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)
|
| 174 |
.each(function(d){
|
| 175 |
const base = color(d.category || 'Unknown') || 'var(--text-color)';
|
| 176 |
-
const bg = getComputedStyle(document.documentElement).getPropertyValue('--
|
| 177 |
const bgNode = this.querySelector('text.label-bg');
|
| 178 |
const fgNode = this.querySelector('text.label-fg');
|
| 179 |
if (bgNode) {
|
| 180 |
bgNode.textContent = d.category;
|
| 181 |
-
bgNode.style.setProperty('fill', bg, 'important');
|
| 182 |
-
bgNode.style.setProperty('stroke', bg);
|
| 183 |
-
bgNode.style.setProperty('stroke-width', '
|
| 184 |
bgNode.style.setProperty('paint-order', 'stroke fill');
|
| 185 |
bgNode.style.setProperty('font-weight','800');
|
| 186 |
bgNode.style.setProperty('font-size','16px');
|
|
|
|
| 1 |
+
<div class="d3-scatter" ></div>
|
| 2 |
<style>
|
| 3 |
/* Frameless: no controls, no axes, only dots */
|
| 4 |
.d3-scatter svg { display: block; }
|
| 5 |
+
/* Tooltip refined styling (align with filters-quad) */
|
| 6 |
+
.d3-scatter .d3-tooltip {
|
| 7 |
+
z-index: 20;
|
| 8 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 9 |
+
}
|
| 10 |
+
.d3-scatter .d3-tooltip__inner {
|
| 11 |
+
display: flex;
|
| 12 |
+
flex-direction: column;
|
| 13 |
+
gap: 6px;
|
| 14 |
+
min-width: 200px;
|
| 15 |
+
}
|
| 16 |
+
.d3-scatter .d3-tooltip__inner > div:first-child {
|
| 17 |
+
font-weight: 800;
|
| 18 |
+
letter-spacing: 0.1px;
|
| 19 |
+
margin-bottom: 0;
|
| 20 |
+
}
|
| 21 |
+
.d3-scatter .d3-tooltip__inner > div:nth-child(2) {
|
| 22 |
+
font-size: 11px;
|
| 23 |
+
color: var(--muted-color);
|
| 24 |
+
display: block;
|
| 25 |
+
margin-top: -4px;
|
| 26 |
+
margin-bottom: 2px;
|
| 27 |
+
letter-spacing: 0.1px;
|
| 28 |
+
}
|
| 29 |
+
.d3-scatter .d3-tooltip__inner > div:nth-child(n+3) {
|
| 30 |
+
padding-top: 6px;
|
| 31 |
+
border-top: 1px solid var(--border-color);
|
| 32 |
+
}
|
| 33 |
</style>
|
| 34 |
<script>
|
| 35 |
(() => {
|
|
|
|
| 57 |
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 58 |
if (!tip) {
|
| 59 |
tip = document.createElement('div'); tip.className = 'd3-tooltip';
|
| 60 |
+
Object.assign(tip.style, { position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity:'0', transition:'opacity .12s ease', backdropFilter:'saturate(1.12) blur(8px)' });
|
| 61 |
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
|
| 62 |
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 63 |
|
|
|
|
| 76 |
const y = d3.scaleLinear();
|
| 77 |
const color = d3.scaleOrdinal();
|
| 78 |
const radius = () => 4;
|
| 79 |
+
let isDarkMode = false;
|
| 80 |
+
function getDotStrokeColor(){ return 'var(--muted-color)'; }
|
| 81 |
|
| 82 |
// Data loading (real): banner visualization positions by category
|
| 83 |
async function fetchFirstAvailable(paths){
|
|
|
|
| 98 |
|
| 99 |
function updateScales(data){
|
| 100 |
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 101 |
+
isDarkMode = !!isDark;
|
| 102 |
+
const axisColor = "var(--page-bg)";
|
| 103 |
+
const tickColor = "var(--page-bg)";
|
| 104 |
+
const gridColor = "var(--page-bg)";
|
| 105 |
|
| 106 |
width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
|
| 107 |
const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
|
|
| 142 |
dots.enter().append('circle').attr('class','dot')
|
| 143 |
.attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
|
| 144 |
.attr('fill', fillFor).attr('fill-opacity', 0.85)
|
| 145 |
+
.attr('stroke', getDotStrokeColor()).attr('stroke-width', '0.75px')
|
| 146 |
.on('mouseenter', function(ev, d){
|
| 147 |
+
d3.select(this).style('stroke','var(--text-color)').style('stroke-width','1.5px').attr('fill-opacity', 1);
|
| 148 |
+
const swatch = `<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="5" fill="${fillFor(d)}" /></svg>`;
|
| 149 |
+
tipInner.innerHTML = `
|
| 150 |
+
<div><strong>${d.label || 'Item'}</strong></div>
|
| 151 |
+
<div style="display:flex;align-items:center;gap:6px;">${swatch}<span>${d.group}</span></div>
|
| 152 |
+
<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>x</strong><span style="margin-left:auto;text-align:right;">${d.x.toFixed(2)}</span></div>
|
| 153 |
+
<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>y</strong><span style="margin-left:auto;text-align:right;">${d.y.toFixed(2)}</span></div>`;
|
| 154 |
tip.style.opacity = '1';
|
| 155 |
})
|
| 156 |
.on('mousemove', function(ev){ const [mx, my] = d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
|
| 157 |
+
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).style('stroke', getDotStrokeColor()).style('stroke-width','0.75px').attr('fill-opacity', 0.85); })
|
| 158 |
.merge(dots)
|
| 159 |
.transition().duration(180)
|
| 160 |
.attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
|
| 161 |
+
.attr('fill', fillFor).attr('fill-opacity', 0.85)
|
| 162 |
+
.attr('stroke', getDotStrokeColor()).attr('stroke-width','0.75px');
|
| 163 |
dots.exit().remove();
|
| 164 |
|
| 165 |
// Compute centroids per category
|
|
|
|
| 211 |
.attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)
|
| 212 |
.each(function(d){
|
| 213 |
const base = color(d.category || 'Unknown') || 'var(--text-color)';
|
| 214 |
+
const bg = getComputedStyle(document.documentElement).getPropertyValue('--page-bg').trim() || '#fff';
|
| 215 |
const bgNode = this.querySelector('text.label-bg');
|
| 216 |
const fgNode = this.querySelector('text.label-fg');
|
| 217 |
if (bgNode) {
|
| 218 |
bgNode.textContent = d.category;
|
| 219 |
+
bgNode.style.setProperty('fill', "var(--page-bg)", 'important');
|
| 220 |
+
bgNode.style.setProperty('stroke', "var(--page-bg)");
|
| 221 |
+
bgNode.style.setProperty('stroke-width', '8px');
|
| 222 |
bgNode.style.setProperty('paint-order', 'stroke fill');
|
| 223 |
bgNode.style.setProperty('font-weight','800');
|
| 224 |
bgNode.style.setProperty('font-size','16px');
|
app/src/content/embeds/heatmap.html
DELETED
|
@@ -1,159 +0,0 @@
|
|
| 1 |
-
<div class="d3-train-diagram" style="width:100%;margin:10px 0;"></div>
|
| 2 |
-
<div class="caption">Hover blocks to show an explanation.</div>
|
| 3 |
-
<style>
|
| 4 |
-
.d3-train-diagram + .caption { margin-top: 8px; font-size: 14px; color: var(--muted-color); }
|
| 5 |
-
</style>
|
| 6 |
-
<script>
|
| 7 |
-
(() => {
|
| 8 |
-
const ensureD3 = (cb) => {
|
| 9 |
-
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 10 |
-
let s = document.getElementById('d3-cdn-script');
|
| 11 |
-
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
| 12 |
-
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 13 |
-
s.addEventListener('load', onReady, { once: true });
|
| 14 |
-
if (window.d3) onReady();
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
const bootstrap = () => {
|
| 18 |
-
const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
|
| 19 |
-
const container = (mount && mount.querySelector && mount.querySelector('.d3-train-diagram')) || document.querySelector('.d3-train-diagram');
|
| 20 |
-
if (!container) return;
|
| 21 |
-
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 22 |
-
|
| 23 |
-
// Diagram spec
|
| 24 |
-
const numBlocks = 7;
|
| 25 |
-
const rows = [
|
| 26 |
-
{ key: 'model', label: 'Model', color: '#a78bfa' },
|
| 27 |
-
{ key: 'forward', label: 'Forward', color: '#14b8a6' },
|
| 28 |
-
{ key: 'backward', label: 'Backward', color: '#f59e0b' },
|
| 29 |
-
{ key: 'gradients', label: 'Gradients', color: 'var(--primary-color)' },
|
| 30 |
-
{ key: 'optimization', label: 'Optimization', color: '#10b981' },
|
| 31 |
-
{ key: 'updated', label: 'Updated', color: '#7c3aed' },
|
| 32 |
-
];
|
| 33 |
-
const hoverText = {
|
| 34 |
-
model: 'Each block represents a submodule of the model.',
|
| 35 |
-
forward: 'Forward pass: compute activations layer by layer.',
|
| 36 |
-
backward: 'Backpropagation: compute gradients through the chain.',
|
| 37 |
-
gradients: 'Gradient accumulators for each layer.',
|
| 38 |
-
optimization: 'Optimization step: update the weights.',
|
| 39 |
-
updated: 'Parameters updated, ready for the next iteration.'
|
| 40 |
-
};
|
| 41 |
-
|
| 42 |
-
// SVG
|
| 43 |
-
const svg = d3.select(container).append('svg').attr('width', '100%').style('display','block');
|
| 44 |
-
const gRoot = svg.append('g');
|
| 45 |
-
const gLegend = gRoot.append('foreignObject').attr('class','legend');
|
| 46 |
-
const gArrows = gRoot.append('g').attr('class','arrows');
|
| 47 |
-
const gBlocks = gRoot.append('g').attr('class','blocks');
|
| 48 |
-
const gLabels = gRoot.append('g').attr('class','row-labels');
|
| 49 |
-
|
| 50 |
-
// Tooltip (reuse style from others)
|
| 51 |
-
container.style.position = container.style.position || 'relative';
|
| 52 |
-
let tip = container.querySelector('.d3-tooltip'); let tipInner;
|
| 53 |
-
if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style,{ position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
|
| 54 |
-
|
| 55 |
-
// Layout
|
| 56 |
-
let width=800, height=360; const margin = { top: 24, right: 180, bottom: 40, left: 32 };
|
| 57 |
-
const x = d3.scaleBand().domain(d3.range(numBlocks)).paddingInner(0.2).paddingOuter(0.05);
|
| 58 |
-
const y = d3.scaleBand().domain(d3.range(rows.length)).paddingInner(0.35);
|
| 59 |
-
|
| 60 |
-
function updateScales(){
|
| 61 |
-
width = container.clientWidth || 800;
|
| 62 |
-
const rowH = Math.max(54, Math.min(80, Math.round(width / 12)));
|
| 63 |
-
const innerHeight = rows.length * rowH;
|
| 64 |
-
height = innerHeight + margin.top + margin.bottom;
|
| 65 |
-
svg.attr('width', width).attr('height', height);
|
| 66 |
-
const innerWidth = width - margin.left - margin.right;
|
| 67 |
-
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 68 |
-
|
| 69 |
-
x.range([0, innerWidth]);
|
| 70 |
-
y.range([0, innerHeight]);
|
| 71 |
-
|
| 72 |
-
return { innerWidth, innerHeight };
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
function render(){
|
| 76 |
-
const { innerWidth, innerHeight } = updateScales();
|
| 77 |
-
|
| 78 |
-
// Legend right side
|
| 79 |
-
const legendWidth = 160, legendHeight = rows.length * 20;
|
| 80 |
-
gLegend.attr('x', innerWidth + 16).attr('y', 0).attr('width', legendWidth).attr('height', legendHeight);
|
| 81 |
-
const lroot = gLegend.selectAll('div').data([0]).join('xhtml:div');
|
| 82 |
-
lroot.html(`
|
| 83 |
-
<div style="display:flex;flex-direction:column;gap:8px;">
|
| 84 |
-
${rows.map(r => `<div style=\"display:flex;align-items:center;gap:8px;\"><span style=\"width:14px;height:14px;background:${r.color};border-radius:4px;display:inline-block\"></span><span>${r.label}</span></div>`).join('')}
|
| 85 |
-
</div>
|
| 86 |
-
`);
|
| 87 |
-
|
| 88 |
-
// Row labels on the right side aligned to centers
|
| 89 |
-
gLabels.selectAll('*').remove();
|
| 90 |
-
gLabels.selectAll('text').data(rows).join('text')
|
| 91 |
-
.attr('x', innerWidth + 16)
|
| 92 |
-
.attr('y', (_,i)=> y(i) + y.bandwidth()/2)
|
| 93 |
-
.attr('dy','0.35em')
|
| 94 |
-
.style('font-size','14px')
|
| 95 |
-
.style('fill','var(--text-color)')
|
| 96 |
-
.text(d=>d.label);
|
| 97 |
-
|
| 98 |
-
// Blocks per row
|
| 99 |
-
const blockW = Math.min(84, x.bandwidth());
|
| 100 |
-
const blockH = Math.min(52, Math.round(y.bandwidth() * 0.8));
|
| 101 |
-
const blocks = [];
|
| 102 |
-
rows.forEach((row, ri) => {
|
| 103 |
-
for (let i=0;i<numBlocks;i++) blocks.push({ row, ri, i });
|
| 104 |
-
});
|
| 105 |
-
const sel = gBlocks.selectAll('rect.block').data(blocks, d=>`${d.row.key}-${d.i}`);
|
| 106 |
-
sel.join(
|
| 107 |
-
enter => enter.append('rect').attr('class','block')
|
| 108 |
-
.attr('x', d=>x(d.i))
|
| 109 |
-
.attr('y', d=>y(d.ri) + (y.bandwidth()-blockH)/2)
|
| 110 |
-
.attr('rx', 12).attr('ry', 12)
|
| 111 |
-
.attr('width', blockW)
|
| 112 |
-
.attr('height', blockH)
|
| 113 |
-
.attr('fill', d=>d.row.color)
|
| 114 |
-
.attr('opacity', 0.95)
|
| 115 |
-
.attr('stroke', 'rgba(0,0,0,0.18)')
|
| 116 |
-
.attr('filter', 'url(#shadow)')
|
| 117 |
-
.on('mouseenter', function(ev, d){
|
| 118 |
-
d3.select(this).attr('opacity', 1.0).attr('stroke-width', 1.2);
|
| 119 |
-
tipInner.innerHTML = `<div><strong>${d.row.label}</strong></div><div>${hoverText[d.row.key]}</div>`;
|
| 120 |
-
tip.style.opacity = '1';
|
| 121 |
-
})
|
| 122 |
-
.on('mousemove', function(ev){ const [mx,my] = d3.pointer(ev, container); tip.style.transform = `translate(${mx+12}px, ${my+12}px)`; })
|
| 123 |
-
.on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px,-9999px)'; d3.select(this).attr('opacity', 0.95).attr('stroke-width', 1); })
|
| 124 |
-
);
|
| 125 |
-
|
| 126 |
-
// Arrows forward/backward
|
| 127 |
-
gArrows.selectAll('*').remove();
|
| 128 |
-
const arrowY = (ri) => y(ri) + y.bandwidth()/2;
|
| 129 |
-
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 130 |
-
const arrowColor = isDark ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.55)';
|
| 131 |
-
const defs = svg.select('defs').empty() ? svg.append('defs') : svg.select('defs');
|
| 132 |
-
const marker = defs.append('marker').attr('id','arrow').attr('viewBox','0 0 10 10').attr('refX', 10).attr('refY', 5).attr('markerWidth', 6).attr('markerHeight', 6).attr('orient','auto-start-reverse');
|
| 133 |
-
marker.append('path').attr('d','M 0 0 L 10 5 L 0 10 z').attr('fill', arrowColor);
|
| 134 |
-
// drop shadow filter
|
| 135 |
-
const flt = defs.append('filter').attr('id','shadow').attr('x','-20%').attr('y','-20%').attr('width','140%').attr('height','140%');
|
| 136 |
-
flt.append('feDropShadow').attr('dx','0').attr('dy','1').attr('stdDeviation','1.5').attr('flood-color','rgba(0,0,0,0.18)');
|
| 137 |
-
// Forward arrow (top orientation)
|
| 138 |
-
gArrows.append('line').attr('x1', x(0)).attr('y1', arrowY(1)-28).attr('x2', x(numBlocks-1)+blockW).attr('y2', arrowY(1)-28)
|
| 139 |
-
.attr('stroke', rows[1].color).attr('stroke-width', 4).attr('marker-end','url(#arrow)');
|
| 140 |
-
// Backward arrow (orange, reversed)
|
| 141 |
-
gArrows.append('line').attr('x1', x(numBlocks-1)+blockW).attr('y1', arrowY(2)-20).attr('x2', x(0)).attr('y2', arrowY(2)-20)
|
| 142 |
-
.attr('stroke', rows[2].color).attr('stroke-width', 4).attr('marker-end','url(#arrow)');
|
| 143 |
-
// Vertical arrows (gradients down, updated up)
|
| 144 |
-
const midX = x(3) + blockW/2;
|
| 145 |
-
gArrows.append('line').attr('x1', midX).attr('y1', arrowY(2)+blockH/2+4).attr('x2', midX).attr('y2', arrowY(3)-blockH/2-6)
|
| 146 |
-
.attr('stroke', rows[3].color).attr('stroke-width', 3).attr('marker-end','url(#arrow)');
|
| 147 |
-
gArrows.append('line').attr('x1', midX).attr('y1', arrowY(4)+blockH/2+6).attr('x2', midX).attr('y2', arrowY(5)-blockH/2-6)
|
| 148 |
-
.attr('stroke', rows[5].color).attr('stroke-width', 3).attr('marker-end','url(#arrow)');
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
render();
|
| 152 |
-
if (window.ResizeObserver) { const ro = new ResizeObserver(()=>render()); ro.observe(container); } else { window.addEventListener('resize', render); }
|
| 153 |
-
};
|
| 154 |
-
|
| 155 |
-
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
|
| 156 |
-
})();
|
| 157 |
-
</script>
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/vibe-code-d3-embeds-directives.md
CHANGED
|
@@ -1,7 +1,69 @@
|
|
| 1 |
## Embed Chart Authoring Guidelines
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
Authoring rules for creating a new interactive chart as a single self-contained `.html` file under `src/content/embeds/`. These conventions are derived from `d3-bar.html`, `d3-comparison.html`, `d3-neural.html`, `d3-line.html`, and `d3-pie.html`.
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
### 1) File, naming, and structure
|
| 6 |
- Name files with a clear prefix and purpose: `d3-<type>.html` (e.g., `d3-scatter.html`).
|
| 7 |
- Wrap everything in a single `<div class="<root-class>">`, a `<style>` block scoped to that root class, and a `<script>` IIFE.
|
|
@@ -10,7 +72,7 @@ Authoring rules for creating a new interactive chart as a single self-contained
|
|
| 10 |
|
| 11 |
Minimal skeleton:
|
| 12 |
```html
|
| 13 |
-
<div class="d3-yourchart"
|
| 14 |
<style>
|
| 15 |
.d3-yourchart {/* all styles scoped to the root */}
|
| 16 |
</style>
|
|
@@ -92,16 +154,77 @@ Minimal skeleton:
|
|
| 92 |
### 3) Styling and theming
|
| 93 |
- Scope all rules under the root class; do not style `body`, `svg` globally.
|
| 94 |
- Use CSS variables for theme alignment: `--primary-color`, `--text-color`, `--muted-color`, `--surface-bg`, `--border-color`.
|
|
|
|
| 95 |
- For dark mode–aware strokes/ticks, either:
|
| 96 |
- Read `document.documentElement.getAttribute('data-theme') === 'dark'`, or
|
| 97 |
- Prefer CSS-only where possible.
|
| 98 |
- Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
### 4) Controls (labels, selects, sliders)
|
| 101 |
-
- Compose controls as plain HTML elements appended inside the root container.
|
| 102 |
- Style selects like in `d3-line.html`/`d3-bar.html` for consistency (rounded 8px, custom caret via data-URI, focus ring).
|
| 103 |
- Use `<label>` wrapping the input for accessibility; set concise text (e.g., "Metric", "Model Size").
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
### 5) Tooltip pattern
|
| 106 |
- Create a single `.d3-tooltip` absolutely positioned inside the container.
|
| 107 |
- Show on hover, hide on leave; position using `d3.pointer(event, container)` plus a small offset.
|
|
@@ -113,6 +236,89 @@ Minimal skeleton:
|
|
| 113 |
- Implement `fetchFirstAvailable(paths)`; try in order with `cache:'no-cache'`; handle errors gracefully with a red `<pre>` message.
|
| 114 |
- For images or JSON models, mirror the same approach (see `d3-comparison.html`, `d3-neural.html`).
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
### 7) Responsiveness and layout
|
| 117 |
- Compute `width = container.clientWidth`, and a height derived from width (e.g., `width / 3`), with a sensible minimum height.
|
| 118 |
- Maintain a `margin` object and derive `innerWidth/innerHeight` for plots.
|
|
@@ -120,9 +326,50 @@ Minimal skeleton:
|
|
| 120 |
- Recompute scales/axes/grid on every render.
|
| 121 |
|
| 122 |
### 8) Legends and labels
|
| 123 |
-
-
|
| 124 |
-
- For axes, remove and re-append groups each render (simple and predictable), or update in place if needed.
|
| 125 |
- Always add axis labels when applicable (e.g., `Step`, `Value`).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
### 9) Accessibility
|
| 128 |
- Provide `alt` attributes on `<img>` (see `d3-comparison.html`).
|
|
@@ -150,18 +397,19 @@ Minimal skeleton:
|
|
| 150 |
### 14) Conventions checklist (before committing)
|
| 151 |
- Root class is unique and matches file name (`d3-<type>`).
|
| 152 |
- No globals added; script wrapped in an IIFE.
|
| 153 |
-
- `data-
|
|
|
|
| 154 |
- Uses CSS variables for colors; dark-mode friendly.
|
| 155 |
- Responsive: recomputes layout on resize; uses `ResizeObserver`.
|
| 156 |
-
- Controls are accessible and consistently styled.
|
| 157 |
-
-
|
| 158 |
- Data loading includes public-path-first strategy and graceful error.
|
| 159 |
-
- Axes/labels
|
| 160 |
- Code is easy to skim: clear naming, early returns, short functions.
|
| 161 |
|
| 162 |
### 15) Example: small bar chart (structure only)
|
| 163 |
```html
|
| 164 |
-
<div class="d3-mini-bar"
|
| 165 |
<style>
|
| 166 |
.d3-mini-bar .bar { stroke: none; }
|
| 167 |
</style>
|
|
|
|
| 1 |
## Embed Chart Authoring Guidelines
|
| 2 |
|
| 3 |
+
### Quickstart (TL;DR)
|
| 4 |
+
- Create a single self-contained HTML fragment: root div + scoped style + IIFE script.
|
| 5 |
+
- Draw marks/axes in SVG; render UI (legend and controls) in HTML.
|
| 6 |
+
- Place legend and controls ABOVE the chart. Include a legend title "Legend" and a select labeled "Metric" when relevant.
|
| 7 |
+
- Load data from public `/data` first, then fall back to `assets/data`.
|
| 8 |
+
- Use `window.ColorPalettes` for colors; stick to CSS variables for theming.
|
| 9 |
+
|
| 10 |
+
Minimal header markup:
|
| 11 |
+
```html
|
| 12 |
+
<div class="legend">
|
| 13 |
+
<div class="legend-title">Legend</div>
|
| 14 |
+
<div class="items"></div>
|
| 15 |
+
<!-- items populated by JS: <span class="item"><span class="swatch"></span><span>Name</span></span> -->
|
| 16 |
+
</div>
|
| 17 |
+
<div class="controls">
|
| 18 |
+
<div class="control-group">
|
| 19 |
+
<label for="metric-select-<id>">Metric</label>
|
| 20 |
+
<select id="metric-select-<id>"></select>
|
| 21 |
+
</div>
|
| 22 |
+
<!-- optional: other controls -->
|
| 23 |
+
</div>
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
See also: `d3-line-simple.html`, `d3-line-quad.html`, `d3-benchmark.html`.
|
| 27 |
+
|
| 28 |
Authoring rules for creating a new interactive chart as a single self-contained `.html` file under `src/content/embeds/`. These conventions are derived from `d3-bar.html`, `d3-comparison.html`, `d3-neural.html`, `d3-line.html`, and `d3-pie.html`.
|
| 29 |
|
| 30 |
+
### A) Colors & palettes (MANDATORY)
|
| 31 |
+
- Always obtain color arrays from `window.ColorPalettes`; do not hardcode palettes.
|
| 32 |
+
- Use the categorical/sequential/diverging helpers and the current primary color.
|
| 33 |
+
- If you change `--primary-color` dynamically, call `window.ColorPalettes.refresh()` so listeners update.
|
| 34 |
+
|
| 35 |
+
Usage:
|
| 36 |
+
```js
|
| 37 |
+
// Usage (with explicit counts)
|
| 38 |
+
const cat = window.ColorPalettes.getColors('categorical', 8);
|
| 39 |
+
const seq = window.ColorPalettes.getColors('sequential', 8);
|
| 40 |
+
const div = window.ColorPalettes.getColors('diverging', 7);
|
| 41 |
+
|
| 42 |
+
// For current primary color string
|
| 43 |
+
const primaryHex = window.ColorPalettes.getPrimary();
|
| 44 |
+
|
| 45 |
+
// If you change --primary-color dynamically, call refresh to notify listeners
|
| 46 |
+
document.documentElement.style.setProperty('--primary-color', '#6D4AFF');
|
| 47 |
+
window.ColorPalettes.refresh();
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Notes:
|
| 51 |
+
- Keep chart accents (lines, markers, selection) aligned with `--primary-color`.
|
| 52 |
+
- Prefer CSS variables for fills/strokes when possible; derive series colors via `ColorPalettes`.
|
| 53 |
+
- Provide a graceful fallback to CSS variables if `window.ColorPalettes` is unavailable.
|
| 54 |
+
|
| 55 |
+
### B) Layout & form elements (HTML-only)
|
| 56 |
+
- All UI controls (labels, selects, sliders, buttons, toggles) must be plain HTML inside the root container.
|
| 57 |
+
- Do not draw controls with SVG; style them consistently (rounded 8px, custom caret, focus ring).
|
| 58 |
+
- Use `<label>` wrapping inputs for accessibility and concise text (e.g., "Metric", "Model Size").
|
| 59 |
+
- Manage layout with CSS inside the scoped `<style>` for the root class; avoid global rules.
|
| 60 |
+
|
| 61 |
+
### C) SVG scope: charts only; UI in HTML
|
| 62 |
+
- SVG is for chart primitives (marks, axes, gridlines) only.
|
| 63 |
+
- Put legends and controls in HTML (adjacent DOM is preferred; `foreignObject` only if necessary).
|
| 64 |
+
- Tooltips are HTML positioned absolutely inside the root container.
|
| 65 |
+
- Details: see sections 4 (controls), 5 (tooltips), 8 (legends).
|
| 66 |
+
|
| 67 |
### 1) File, naming, and structure
|
| 68 |
- Name files with a clear prefix and purpose: `d3-<type>.html` (e.g., `d3-scatter.html`).
|
| 69 |
- Wrap everything in a single `<div class="<root-class>">`, a `<style>` block scoped to that root class, and a `<script>` IIFE.
|
|
|
|
| 72 |
|
| 73 |
Minimal skeleton:
|
| 74 |
```html
|
| 75 |
+
<div class="d3-yourchart"></div>
|
| 76 |
<style>
|
| 77 |
.d3-yourchart {/* all styles scoped to the root */}
|
| 78 |
</style>
|
|
|
|
| 154 |
### 3) Styling and theming
|
| 155 |
- Scope all rules under the root class; do not style `body`, `svg` globally.
|
| 156 |
- Use CSS variables for theme alignment: `--primary-color`, `--text-color`, `--muted-color`, `--surface-bg`, `--border-color`.
|
| 157 |
+
- Derive palette colors from `window.ColorPalettes` (categorical, sequential, diverging); do not hardcode arrays.
|
| 158 |
- For dark mode–aware strokes/ticks, either:
|
| 159 |
- Read `document.documentElement.getAttribute('data-theme') === 'dark'`, or
|
| 160 |
- Prefer CSS-only where possible.
|
| 161 |
- Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
|
| 162 |
|
| 163 |
+
Standard axis/tick/grid colors (reuse across charts):
|
| 164 |
+
|
| 165 |
+
```css
|
| 166 |
+
.your-root-class {
|
| 167 |
+
--axis-color: rgba(0,0,0,0.25);
|
| 168 |
+
--tick-color: rgba(0,0,0,0.55);
|
| 169 |
+
--grid-color: rgba(0,0,0,0.05);
|
| 170 |
+
}
|
| 171 |
+
[data-theme="dark"] .your-root-class {
|
| 172 |
+
--axis-color: rgba(255,255,255,0.25);
|
| 173 |
+
--tick-color: rgba(255,255,255,0.70);
|
| 174 |
+
--grid-color: rgba(255,255,255,0.08);
|
| 175 |
+
}
|
| 176 |
+
/* Example axis application (D3) */
|
| 177 |
+
g.axis-x, g.axis-y { }
|
| 178 |
+
/* In JS after calling axis: */
|
| 179 |
+
// g.selectAll('path, line').attr('stroke', 'var(--axis-color)');
|
| 180 |
+
// g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px');
|
| 181 |
+
/* Gridlines: */
|
| 182 |
+
// grid.call(d3.axisLeft(y).ticks(6).tickSize(-innerWidth).tickFormat(''))
|
| 183 |
+
// .call(g => g.selectAll('.tick line').attr('stroke','var(--grid-color)'));
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
#### 3.1) Text on fixed-colored backgrounds
|
| 187 |
+
|
| 188 |
+
- When rendering text over cells/areas with fixed background colors (that do not change with theme), compute a readable text style once from the actual background color.
|
| 189 |
+
- Use `window.ColorPalettes.getTextStyleForBackground(bgCss, { blend: 0.6 })` when available; avoid tying text color to dark mode toggles since the background is constant.
|
| 190 |
+
- Do not re-evaluate on theme toggle unless the background color itself changes.
|
| 191 |
+
|
| 192 |
+
Example:
|
| 193 |
+
```js
|
| 194 |
+
const bg = getComputedStyle(cellRect).fill; // e.g., 'rgb(12, 34, 56)'
|
| 195 |
+
const style = window.ColorPalettes?.getTextStyleForBackground
|
| 196 |
+
? window.ColorPalettes.getTextStyleForBackground(bg, { blend: 0.6 })
|
| 197 |
+
: { fill: 'var(--text-color)' };
|
| 198 |
+
textSel.style('fill', style.fill);
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
### 4) Controls (labels, selects, sliders)
|
| 202 |
+
- Compose controls as plain HTML elements appended inside the root container (no SVG UI).
|
| 203 |
- Style selects like in `d3-line.html`/`d3-bar.html` for consistency (rounded 8px, custom caret via data-URI, focus ring).
|
| 204 |
- Use `<label>` wrapping the input for accessibility; set concise text (e.g., "Metric", "Model Size").
|
| 205 |
|
| 206 |
+
#### 4.1) Required select label: "Metric"
|
| 207 |
+
- When a select is used to switch metrics, include a visible label above the select with the exact text "Metric".
|
| 208 |
+
- Preferred markup (grouped for easy vertical stacking):
|
| 209 |
+
|
| 210 |
+
```html
|
| 211 |
+
<div class="controls">
|
| 212 |
+
<div class="control-group">
|
| 213 |
+
<label for="metric-select-<unique>">Metric</label>
|
| 214 |
+
<select id="metric-select-<unique>"></select>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
Minimal CSS (match project styles):
|
| 220 |
+
|
| 221 |
+
```css
|
| 222 |
+
.controls { display:flex; gap:16px; align-items:center; justify-content:flex-end; flex-wrap:wrap; }
|
| 223 |
+
.controls .control-group { display:flex; flex-direction:column; align-items:flex-start; gap:6px; }
|
| 224 |
+
.controls label { font-size:12px; font-weight:700; color: var(--text-color); }
|
| 225 |
+
.controls select { font-size:12px; padding:8px 28px 8px 10px; border:1px solid var(--border-color); border-radius:8px; background: var(--surface-bg); color: var(--text-color); }
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
### 5) Tooltip pattern
|
| 229 |
- Create a single `.d3-tooltip` absolutely positioned inside the container.
|
| 230 |
- Show on hover, hide on leave; position using `d3.pointer(event, container)` plus a small offset.
|
|
|
|
| 236 |
- Implement `fetchFirstAvailable(paths)`; try in order with `cache:'no-cache'`; handle errors gracefully with a red `<pre>` message.
|
| 237 |
- For images or JSON models, mirror the same approach (see `d3-comparison.html`, `d3-neural.html`).
|
| 238 |
|
| 239 |
+
#### 6.1) Data props (HtmlEmbed → embed)
|
| 240 |
+
|
| 241 |
+
- HtmlEmbed accepts an optional `data` prop that can be a string (single file) or an array of strings (multiple files).
|
| 242 |
+
- This prop is passed to the fragment via the `data-datafiles` HTML attribute.
|
| 243 |
+
- In the embed script, read this attribute from the closest ancestor that carries it (the `HtmlEmbed` wrapper), not necessarily the chart’s direct container.
|
| 244 |
+
- Recommended normalization: if a value contains no slash, automatically prefix it with `/data/` to target the public data folder.
|
| 245 |
+
- If `data` is not provided, keep the usual fallback (public, then `assets/data`).
|
| 246 |
+
|
| 247 |
+
Optional configuration (e.g., default metric):
|
| 248 |
+
|
| 249 |
+
```js
|
| 250 |
+
// In HtmlEmbed usage (MDX):
|
| 251 |
+
<HtmlEmbed src="d3-line-simple.html" data="internal_deduplication.csv" config={{ defaultMetric: 'average_rank' }} />
|
| 252 |
+
|
| 253 |
+
// In the embed script (read from closest ancestor):
|
| 254 |
+
let mountEl = container;
|
| 255 |
+
while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
|
| 256 |
+
mountEl = mountEl.parentElement;
|
| 257 |
+
}
|
| 258 |
+
let providedConfig = null;
|
| 259 |
+
try {
|
| 260 |
+
const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
|
| 261 |
+
if (cfg && cfg.trim()) providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
|
| 262 |
+
} catch(_) {}
|
| 263 |
+
// Example: selecting initial metric if present
|
| 264 |
+
const desired = providedConfig && providedConfig.defaultMetric ? String(providedConfig.defaultMetric) : null;
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
Examples (MDX):
|
| 268 |
+
|
| 269 |
+
```mdx
|
| 270 |
+
<HtmlEmbed src="d3-line-simple.html" title="Run A" data="formatting_filters.csv" />
|
| 271 |
+
<HtmlEmbed src="d3-line-simple.html" title="Run B" data="relevance_filters.csv" />
|
| 272 |
+
|
| 273 |
+
<HtmlEmbed
|
| 274 |
+
src="d3-line-simple.html"
|
| 275 |
+
title="Comparison A vs B"
|
| 276 |
+
data={[ 'formatting_filters.csv', 'relevance_filters.csv' ]}
|
| 277 |
+
/>
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
Reading on the embed side (JS):
|
| 281 |
+
|
| 282 |
+
```js
|
| 283 |
+
// Find the closest ancestor that carries the attribute
|
| 284 |
+
let mountEl = container;
|
| 285 |
+
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 286 |
+
mountEl = mountEl.parentElement;
|
| 287 |
+
}
|
| 288 |
+
let providedData = null;
|
| 289 |
+
try {
|
| 290 |
+
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
|
| 291 |
+
if (attr && attr.trim()) {
|
| 292 |
+
providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
|
| 293 |
+
}
|
| 294 |
+
} catch(_) {}
|
| 295 |
+
|
| 296 |
+
const DEFAULT_CSV = '/data/formatting_filters.csv';
|
| 297 |
+
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 298 |
+
const normalizeInput = (inp) => Array.isArray(inp)
|
| 299 |
+
? inp.map(ensureDataPrefix)
|
| 300 |
+
: (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
|
| 301 |
+
|
| 302 |
+
const CSV_PATHS = Array.isArray(providedData)
|
| 303 |
+
? normalizeInput(providedData)
|
| 304 |
+
: (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [
|
| 305 |
+
DEFAULT_CSV,
|
| 306 |
+
'./assets/data/formatting_filters.csv',
|
| 307 |
+
'../assets/data/formatting_filters.csv',
|
| 308 |
+
'../../assets/data/formatting_filters.csv'
|
| 309 |
+
]);
|
| 310 |
+
|
| 311 |
+
const fetchFirstAvailable = async (paths) => {
|
| 312 |
+
for (const p of paths) {
|
| 313 |
+
try {
|
| 314 |
+
const r = await fetch(p, { cache: 'no-cache' });
|
| 315 |
+
if (r.ok) return await r.text();
|
| 316 |
+
} catch(_){}
|
| 317 |
+
}
|
| 318 |
+
throw new Error('CSV not found');
|
| 319 |
+
};
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
### 7) Responsiveness and layout
|
| 323 |
- Compute `width = container.clientWidth`, and a height derived from width (e.g., `width / 3`), with a sensible minimum height.
|
| 324 |
- Maintain a `margin` object and derive `innerWidth/innerHeight` for plots.
|
|
|
|
| 326 |
- Recompute scales/axes/grid on every render.
|
| 327 |
|
| 328 |
### 8) Legends and labels
|
| 329 |
+
- Prefer HTML for legends for wrapping and accessibility; avoid SVG-based legends.
|
|
|
|
| 330 |
- Always add axis labels when applicable (e.g., `Step`, `Value`).
|
| 331 |
+
- Standardize legend swatch size: 14×14px, border-radius 3px, 1px border `var(--border-color)`.
|
| 332 |
+
|
| 333 |
+
#### 8.1) Required legend title: "Legend"
|
| 334 |
+
- Always render a visible title above legend items with the exact text "Legend".
|
| 335 |
+
- Canonical markup:
|
| 336 |
+
|
| 337 |
+
```html
|
| 338 |
+
<div class="legend">
|
| 339 |
+
<div class="legend-title">Legend</div>
|
| 340 |
+
<div class="items">
|
| 341 |
+
<!-- <span class="item"><span class="swatch"></span><span>Series A</span></span> ... -->
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
Minimal CSS (match project styles):
|
| 347 |
+
|
| 348 |
+
```css
|
| 349 |
+
.legend { display:flex; flex-direction:column; align-items:flex-start; gap:6px; }
|
| 350 |
+
.legend-title { font-size:12px; font-weight:700; color: var(--text-color); }
|
| 351 |
+
.legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; }
|
| 352 |
+
.legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; font-size:12px; color: var(--text-color); }
|
| 353 |
+
.legend .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); }
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
Recommended JS pattern to (re)build the legend:
|
| 357 |
+
|
| 358 |
+
```js
|
| 359 |
+
function makeLegend(seriesNames, colorFor) {
|
| 360 |
+
let legend = container.querySelector('.legend');
|
| 361 |
+
if (!legend) { legend = document.createElement('div'); legend.className = 'legend'; container.appendChild(legend); }
|
| 362 |
+
let title = legend.querySelector('.legend-title'); if (!title) { title = document.createElement('div'); title.className = 'legend-title'; title.textContent = 'Legend'; legend.appendChild(title); }
|
| 363 |
+
let items = legend.querySelector('.items'); if (!items) { items = document.createElement('div'); items.className = 'items'; legend.appendChild(items); }
|
| 364 |
+
items.innerHTML = '';
|
| 365 |
+
seriesNames.forEach(name => {
|
| 366 |
+
const el = document.createElement('span'); el.className = 'item';
|
| 367 |
+
const sw = document.createElement('span'); sw.className = 'swatch'; sw.style.background = colorFor(name);
|
| 368 |
+
const txt = document.createElement('span'); txt.textContent = name;
|
| 369 |
+
el.appendChild(sw); el.appendChild(txt); items.appendChild(el);
|
| 370 |
+
});
|
| 371 |
+
}
|
| 372 |
+
```
|
| 373 |
|
| 374 |
### 9) Accessibility
|
| 375 |
- Provide `alt` attributes on `<img>` (see `d3-comparison.html`).
|
|
|
|
| 397 |
### 14) Conventions checklist (before committing)
|
| 398 |
- Root class is unique and matches file name (`d3-<type>`).
|
| 399 |
- No globals added; script wrapped in an IIFE.
|
| 400 |
+
- `data-mounted` guard is present to avoid double-mount.
|
| 401 |
+
- Colors come from `window.ColorPalettes` (no hardcoded arrays); `--primary-color` respected.
|
| 402 |
- Uses CSS variables for colors; dark-mode friendly.
|
| 403 |
- Responsive: recomputes layout on resize; uses `ResizeObserver`.
|
| 404 |
+
- Controls are HTML-only, accessible, and consistently styled.
|
| 405 |
+
- Legends and tooltips are HTML, not SVG.
|
| 406 |
- Data loading includes public-path-first strategy and graceful error.
|
| 407 |
+
- Axes/labels are legible at small widths.
|
| 408 |
- Code is easy to skim: clear naming, early returns, short functions.
|
| 409 |
|
| 410 |
### 15) Example: small bar chart (structure only)
|
| 411 |
```html
|
| 412 |
+
<div class="d3-mini-bar"></div>
|
| 413 |
<style>
|
| 414 |
.d3-mini-bar .bar { stroke: none; }
|
| 415 |
</style>
|
app/src/scripts/color-palettes.js
CHANGED
|
@@ -164,6 +164,54 @@
|
|
| 164 |
const mo = new MutationObserver(() => updatePalettes());
|
| 165 |
mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
|
| 166 |
setInterval(updatePalettes, 400);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
window.ColorPalettes = {
|
| 168 |
refresh: updatePalettes,
|
| 169 |
getPrimary: () => getPrimaryHex(),
|
|
@@ -174,7 +222,9 @@
|
|
| 174 |
if (key === 'sequential') return generators.sequential(primary, total);
|
| 175 |
if (key === 'diverging') return generators.diverging(primary, total);
|
| 176 |
return [];
|
| 177 |
-
}
|
|
|
|
|
|
|
| 178 |
};
|
| 179 |
};
|
| 180 |
|
|
|
|
| 164 |
const mo = new MutationObserver(() => updatePalettes());
|
| 165 |
mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
|
| 166 |
setInterval(updatePalettes, 400);
|
| 167 |
+
// Utility: choose high-contrast (or softened) text style against an arbitrary background color
|
| 168 |
+
const pickTextStyleForBackground = (bgCss, opts = {}) => {
|
| 169 |
+
const cssRoot = document.documentElement;
|
| 170 |
+
const getCssVar = (name) => {
|
| 171 |
+
try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
|
| 172 |
+
};
|
| 173 |
+
const resolveCssToRgb01 = (css) => {
|
| 174 |
+
const rgb = parseCssColorToRgb(css);
|
| 175 |
+
if (!rgb) return null;
|
| 176 |
+
return rgb; // already 0..1
|
| 177 |
+
};
|
| 178 |
+
const mixRgb01 = (a, b, t) => ({ r: a.r*(1-t)+b.r*t, g: a.g*(1-t)+b.g*t, b: a.b*(1-t)+b.b*t });
|
| 179 |
+
const relLum = (rgb) => {
|
| 180 |
+
const f = (u) => srgbToLinear(u);
|
| 181 |
+
return 0.2126*f(rgb.r) + 0.7152*f(rgb.g) + 0.0722*f(rgb.b);
|
| 182 |
+
};
|
| 183 |
+
const contrast = (fg, bg) => {
|
| 184 |
+
const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1,L2), b = Math.min(L1,L2);
|
| 185 |
+
return (a + 0.05) / (b + 0.05);
|
| 186 |
+
};
|
| 187 |
+
try {
|
| 188 |
+
const bg = resolveCssToRgb01(bgCss);
|
| 189 |
+
if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
|
| 190 |
+
const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
|
| 191 |
+
const candidates = candidatesCss
|
| 192 |
+
.map(css => ({ css, rgb: resolveCssToRgb01(css) }))
|
| 193 |
+
.filter(x => !!x.rgb);
|
| 194 |
+
// Pick the max contrast
|
| 195 |
+
let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
|
| 196 |
+
for (let i=1;i<candidates.length;i++){
|
| 197 |
+
const cr = contrast(candidates[i].rgb, bg);
|
| 198 |
+
if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
|
| 199 |
+
}
|
| 200 |
+
// Optional softening via blend factor (0..1), blending towards muted color
|
| 201 |
+
const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
|
| 202 |
+
let finalRgb = best.rgb;
|
| 203 |
+
if (blend > 0) {
|
| 204 |
+
const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
|
| 205 |
+
const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
|
| 206 |
+
finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
|
| 207 |
+
}
|
| 208 |
+
const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
|
| 209 |
+
const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40*haloStrength})` : `rgba(0,0,0,${0.30 + 0.30*haloStrength})`;
|
| 210 |
+
return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
|
| 211 |
+
} catch {
|
| 212 |
+
return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
|
| 213 |
+
}
|
| 214 |
+
};
|
| 215 |
window.ColorPalettes = {
|
| 216 |
refresh: updatePalettes,
|
| 217 |
getPrimary: () => getPrimaryHex(),
|
|
|
|
| 222 |
if (key === 'sequential') return generators.sequential(primary, total);
|
| 223 |
if (key === 'diverging') return generators.diverging(primary, total);
|
| 224 |
return [];
|
| 225 |
+
},
|
| 226 |
+
getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
|
| 227 |
+
chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
|
| 228 |
};
|
| 229 |
};
|
| 230 |
|
app/src/styles/_variables.css
CHANGED
|
@@ -20,6 +20,7 @@
|
|
| 20 |
/* Text & Surfaces */
|
| 21 |
--page-bg: #ffffff;
|
| 22 |
--text-color: rgba(0,0,0,.85);
|
|
|
|
| 23 |
--muted-color: rgba(0,0,0,.6);
|
| 24 |
--border-color: rgba(0,0,0,.1);
|
| 25 |
--surface-bg: #fafafa;
|
|
@@ -81,6 +82,11 @@
|
|
| 81 |
--z-overlay: 1000;
|
| 82 |
--z-modal: 1100;
|
| 83 |
--z-tooltip: 1200;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
/* ============================================================================ */
|
|
@@ -93,6 +99,12 @@
|
|
| 93 |
--border-color: rgba(255,255,255,.15);
|
| 94 |
--surface-bg: #12151b;
|
| 95 |
--code-bg: #12151b;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
/* Primary (lower L in dark) */
|
| 98 |
--primary-color: oklch(from var(--primary-base) calc(l - 0.08) c h);
|
|
|
|
| 20 |
/* Text & Surfaces */
|
| 21 |
--page-bg: #ffffff;
|
| 22 |
--text-color: rgba(0,0,0,.85);
|
| 23 |
+
--transparent-page-contrast: rgba(255,255,255,.85);
|
| 24 |
--muted-color: rgba(0,0,0,.6);
|
| 25 |
--border-color: rgba(0,0,0,.1);
|
| 26 |
--surface-bg: #fafafa;
|
|
|
|
| 82 |
--z-overlay: 1000;
|
| 83 |
--z-modal: 1100;
|
| 84 |
--z-tooltip: 1200;
|
| 85 |
+
|
| 86 |
+
/* Charts (global) */
|
| 87 |
+
--axis-color: var(--text-color);
|
| 88 |
+
--tick-color: var(--muted-color);
|
| 89 |
+
--grid-color: rgba(0,0,0,.08);
|
| 90 |
}
|
| 91 |
|
| 92 |
/* ============================================================================ */
|
|
|
|
| 99 |
--border-color: rgba(255,255,255,.15);
|
| 100 |
--surface-bg: #12151b;
|
| 101 |
--code-bg: #12151b;
|
| 102 |
+
--transparent-page-contrast: rgba(0,0,0,.85);
|
| 103 |
+
|
| 104 |
+
/* Charts (global) */
|
| 105 |
+
--axis-color: var(--text-color);
|
| 106 |
+
--tick-color: var(--muted-color);
|
| 107 |
+
--grid-color: rgba(255,255,255,.10);
|
| 108 |
|
| 109 |
/* Primary (lower L in dark) */
|
| 110 |
--primary-color: oklch(from var(--primary-base) calc(l - 0.08) c h);
|
app/src/styles/components/_card.css
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.card {
|
| 2 |
+
background: var(--surface-bg);
|
| 3 |
+
border: 1px solid var(--border-color);
|
| 4 |
+
border-radius: 10px;
|
| 5 |
+
padding: var(--spacing-2);
|
| 6 |
+
z-index: calc(var(--z-elevated) + 1);
|
| 7 |
+
position: relative;
|
| 8 |
+
margin-bottom: var(--block-spacing-y);
|
| 9 |
+
}
|
app/src/styles/global.css
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
@import './components/_button.css';
|
| 8 |
@import './components/_table.css';
|
| 9 |
@import './components/_tag.css';
|
|
|
|
| 10 |
|
| 11 |
.demo-wide,
|
| 12 |
.demo-full-width {
|
|
|
|
| 7 |
@import './components/_button.css';
|
| 8 |
@import './components/_table.css';
|
| 9 |
@import './components/_tag.css';
|
| 10 |
+
@import './components/_card.css';
|
| 11 |
|
| 12 |
.demo-wide,
|
| 13 |
.demo-full-width {
|