thibaud frere commited on
Commit
e0e09d3
·
1 Parent(s): 4ec15bc
app/.astro/astro/content.d.ts CHANGED
@@ -1,238 +0,0 @@
1
- declare module 'astro:content' {
2
- interface Render {
3
- '.mdx': Promise<{
4
- Content: import('astro').MarkdownInstance<{}>['Content'];
5
- headings: import('astro').MarkdownHeading[];
6
- remarkPluginFrontmatter: Record<string, any>;
7
- components: import('astro').MDXInstance<{}>['components'];
8
- }>;
9
- }
10
- }
11
-
12
- declare module 'astro:content' {
13
- interface RenderResult {
14
- Content: import('astro/runtime/server/index.js').AstroComponentFactory;
15
- headings: import('astro').MarkdownHeading[];
16
- remarkPluginFrontmatter: Record<string, any>;
17
- }
18
- interface Render {
19
- '.md': Promise<RenderResult>;
20
- }
21
-
22
- export interface RenderedContent {
23
- html: string;
24
- metadata?: {
25
- imagePaths: Array<string>;
26
- [key: string]: unknown;
27
- };
28
- }
29
- }
30
-
31
- declare module 'astro:content' {
32
- type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
33
-
34
- export type CollectionKey = keyof AnyEntryMap;
35
- export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
36
-
37
- export type ContentCollectionKey = keyof ContentEntryMap;
38
- export type DataCollectionKey = keyof DataEntryMap;
39
-
40
- type AllValuesOf<T> = T extends any ? T[keyof T] : never;
41
- type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
42
- ContentEntryMap[C]
43
- >['slug'];
44
-
45
- /** @deprecated Use `getEntry` instead. */
46
- export function getEntryBySlug<
47
- C extends keyof ContentEntryMap,
48
- E extends ValidContentEntrySlug<C> | (string & {}),
49
- >(
50
- collection: C,
51
- // Note that this has to accept a regular string too, for SSR
52
- entrySlug: E,
53
- ): E extends ValidContentEntrySlug<C>
54
- ? Promise<CollectionEntry<C>>
55
- : Promise<CollectionEntry<C> | undefined>;
56
-
57
- /** @deprecated Use `getEntry` instead. */
58
- export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
59
- collection: C,
60
- entryId: E,
61
- ): Promise<CollectionEntry<C>>;
62
-
63
- export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
64
- collection: C,
65
- filter?: (entry: CollectionEntry<C>) => entry is E,
66
- ): Promise<E[]>;
67
- export function getCollection<C extends keyof AnyEntryMap>(
68
- collection: C,
69
- filter?: (entry: CollectionEntry<C>) => unknown,
70
- ): Promise<CollectionEntry<C>[]>;
71
-
72
- export function getEntry<
73
- C extends keyof ContentEntryMap,
74
- E extends ValidContentEntrySlug<C> | (string & {}),
75
- >(entry: {
76
- collection: C;
77
- slug: E;
78
- }): E extends ValidContentEntrySlug<C>
79
- ? Promise<CollectionEntry<C>>
80
- : Promise<CollectionEntry<C> | undefined>;
81
- export function getEntry<
82
- C extends keyof DataEntryMap,
83
- E extends keyof DataEntryMap[C] | (string & {}),
84
- >(entry: {
85
- collection: C;
86
- id: E;
87
- }): E extends keyof DataEntryMap[C]
88
- ? Promise<DataEntryMap[C][E]>
89
- : Promise<CollectionEntry<C> | undefined>;
90
- export function getEntry<
91
- C extends keyof ContentEntryMap,
92
- E extends ValidContentEntrySlug<C> | (string & {}),
93
- >(
94
- collection: C,
95
- slug: E,
96
- ): E extends ValidContentEntrySlug<C>
97
- ? Promise<CollectionEntry<C>>
98
- : Promise<CollectionEntry<C> | undefined>;
99
- export function getEntry<
100
- C extends keyof DataEntryMap,
101
- E extends keyof DataEntryMap[C] | (string & {}),
102
- >(
103
- collection: C,
104
- id: E,
105
- ): E extends keyof DataEntryMap[C]
106
- ? Promise<DataEntryMap[C][E]>
107
- : Promise<CollectionEntry<C> | undefined>;
108
-
109
- /** Resolve an array of entry references from the same collection */
110
- export function getEntries<C extends keyof ContentEntryMap>(
111
- entries: {
112
- collection: C;
113
- slug: ValidContentEntrySlug<C>;
114
- }[],
115
- ): Promise<CollectionEntry<C>[]>;
116
- export function getEntries<C extends keyof DataEntryMap>(
117
- entries: {
118
- collection: C;
119
- id: keyof DataEntryMap[C];
120
- }[],
121
- ): Promise<CollectionEntry<C>[]>;
122
-
123
- export function render<C extends keyof AnyEntryMap>(
124
- entry: AnyEntryMap[C][string],
125
- ): Promise<RenderResult>;
126
-
127
- export function reference<C extends keyof AnyEntryMap>(
128
- collection: C,
129
- ): import('astro/zod').ZodEffects<
130
- import('astro/zod').ZodString,
131
- C extends keyof ContentEntryMap
132
- ? {
133
- collection: C;
134
- slug: ValidContentEntrySlug<C>;
135
- }
136
- : {
137
- collection: C;
138
- id: keyof DataEntryMap[C];
139
- }
140
- >;
141
- // Allow generic `string` to avoid excessive type errors in the config
142
- // if `dev` is not running to update as you edit.
143
- // Invalid collection names will be caught at build time.
144
- export function reference<C extends string>(
145
- collection: C,
146
- ): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
147
-
148
- type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
149
- type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
150
- ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
151
- >;
152
-
153
- type ContentEntryMap = {
154
- "chapters": {
155
- "best-pratices.mdx": {
156
- id: "best-pratices.mdx";
157
- slug: "best-pratices";
158
- body: string;
159
- collection: "chapters";
160
- data: any
161
- } & { render(): Render[".mdx"] };
162
- "components.mdx": {
163
- id: "components.mdx";
164
- slug: "components";
165
- body: string;
166
- collection: "chapters";
167
- data: any
168
- } & { render(): Render[".mdx"] };
169
- "debug-components.mdx": {
170
- id: "debug-components.mdx";
171
- slug: "debug-components";
172
- body: string;
173
- collection: "chapters";
174
- data: any
175
- } & { render(): Render[".mdx"] };
176
- "getting-started.mdx": {
177
- id: "getting-started.mdx";
178
- slug: "getting-started";
179
- body: string;
180
- collection: "chapters";
181
- data: any
182
- } & { render(): Render[".mdx"] };
183
- "greetings.mdx": {
184
- id: "greetings.mdx";
185
- slug: "greetings";
186
- body: string;
187
- collection: "chapters";
188
- data: any
189
- } & { render(): Render[".mdx"] };
190
- "introduction.mdx": {
191
- id: "introduction.mdx";
192
- slug: "introduction";
193
- body: string;
194
- collection: "chapters";
195
- data: any
196
- } & { render(): Render[".mdx"] };
197
- "markdown.mdx": {
198
- id: "markdown.mdx";
199
- slug: "markdown";
200
- body: string;
201
- collection: "chapters";
202
- data: any
203
- } & { render(): Render[".mdx"] };
204
- "writing-your-content.mdx": {
205
- id: "writing-your-content.mdx";
206
- slug: "writing-your-content";
207
- body: string;
208
- collection: "chapters";
209
- data: any
210
- } & { render(): Render[".mdx"] };
211
- };
212
- "embeds": {
213
- "vibe-code-d3-embeds-directives.md": {
214
- id: "vibe-code-d3-embeds-directives.md";
215
- slug: "vibe-code-d3-embeds-directives";
216
- body: string;
217
- collection: "embeds";
218
- data: any
219
- } & { render(): Render[".md"] };
220
- };
221
-
222
- };
223
-
224
- type DataEntryMap = {
225
- "assets": {
226
- "data/mnist-variant-model": {
227
- id: "data/mnist-variant-model";
228
- collection: "assets";
229
- data: any
230
- };
231
- };
232
-
233
- };
234
-
235
- type AnyEntryMap = ContentEntryMap & DataEntryMap;
236
-
237
- export type ContentConfig = never;
238
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/.astro/settings.json CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:585e9065073b0b2528747f99b78ccc382984e713ac7e6a5c9f99a47956e3f42e
3
  size 58
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed6d28be38b13c36af0d93f09ca03071e80381d49463aa549a5ee625ef9a8b56
3
  size 58
app/astro.config.mjs CHANGED
@@ -57,7 +57,8 @@ export default defineConfig({
57
  rehypeKatex,
58
  [rehypeCitation, {
59
  bibliography: 'src/content/bibliography.bib',
60
- linkCitations: true
 
61
  }],
62
  rehypeReferencesAndFootnotes,
63
  rehypeRestoreAtInCode,
 
57
  rehypeKatex,
58
  [rehypeCitation, {
59
  bibliography: 'src/content/bibliography.bib',
60
+ linkCitations: true,
61
+ csl: "apa"
62
  }],
63
  rehypeReferencesAndFootnotes,
64
  rehypeRestoreAtInCode,
app/src/components/HtmlEmbed.astro CHANGED
@@ -69,7 +69,11 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
69
  </script>
70
 
71
  <style is:global>
72
- .html-embed { margin: 0 0 var(--block-spacing-y); }
 
 
 
 
73
  .html-embed__title {
74
  text-align: left;
75
  font-weight: 600;
@@ -77,9 +81,7 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
77
  color: var(--text-color);
78
  margin: 0;
79
  padding: var(--spacing-1);
80
- background: var(--page-bg);
81
  position: relative;
82
- z-index: var(--z-elevated);
83
  display: block;
84
  width: 100%;
85
  }
@@ -88,6 +90,7 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
88
  border: 1px solid var(--border-color);
89
  border-radius: 10px;
90
  padding: 8px;
 
91
  }
92
  .html-embed__card.is-frameless {
93
  background: transparent;
@@ -100,7 +103,6 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
100
  color: var(--muted-color);
101
  margin: 0;
102
  padding: var(--spacing-1);
103
- background: var(--page-bg);
104
  position: relative;
105
  z-index: var(--z-elevated);
106
  display: block;
 
69
  </script>
70
 
71
  <style is:global>
72
+ .html-embed { margin: 0 0 var(--block-spacing-y);
73
+ z-index: var(--z-elevated);
74
+ position: relative;
75
+
76
+ }
77
  .html-embed__title {
78
  text-align: left;
79
  font-weight: 600;
 
81
  color: var(--text-color);
82
  margin: 0;
83
  padding: var(--spacing-1);
 
84
  position: relative;
 
85
  display: block;
86
  width: 100%;
87
  }
 
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;
 
103
  color: var(--muted-color);
104
  margin: 0;
105
  padding: var(--spacing-1);
 
106
  position: relative;
107
  z-index: var(--z-elevated);
108
  display: block;
app/src/components/ThemeToggle.astro CHANGED
@@ -40,12 +40,44 @@
40
  apply(next);
41
  });
42
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </script>
44
  </button>
45
 
46
 
47
  <style>
48
- #theme-toggle { display: inline-flex; align-items: center; gap: 8px; border: none; background: transparent; padding: 6px 10px; border-radius: 8px; cursor: pointer; margin: 12px 16px; color: var(--text-color) !important; }
49
  #theme-toggle .icon.dark { display: none; }
50
  [data-theme="dark"] #theme-toggle .icon.light { display: none; }
51
  [data-theme="dark"] #theme-toggle .icon.dark { display: inline; }
 
40
  apply(next);
41
  });
42
  }
43
+
44
+ // Adjust offset for Hugging Face Spaces tiny header (production)
45
+ // Sets CSS var --hf-spaces-topbar used in _layout.css for sticky placement
46
+ const computeSpacesTopbarOffset = () => {
47
+ try {
48
+ const host = window.location.hostname || '';
49
+ const isSpaces = /\.hf\.space$/.test(host) || host.endsWith('huggingface.co');
50
+ let topbar = 0;
51
+ if (isSpaces) {
52
+ // Scan fixed elements anchored to top to estimate tiny header height
53
+ const nodes = Array.from(document.querySelectorAll('*'));
54
+ for (const el of nodes) {
55
+ const cs = window.getComputedStyle(el);
56
+ if (cs.position === 'fixed' && cs.top === '0px' && el.offsetHeight > 0) {
57
+ const rect = el.getBoundingClientRect();
58
+ if (rect.top <= 1 && rect.height <= 80) {
59
+ topbar = Math.max(topbar, Math.ceil(rect.bottom));
60
+ }
61
+ }
62
+ }
63
+ // Fallback reasonable tiny header height
64
+ if (!topbar) topbar = 40;
65
+ }
66
+ document.documentElement.style.setProperty('--hf-spaces-topbar', `${topbar}px`);
67
+ } catch (_) {
68
+ // No-op
69
+ }
70
+ };
71
+ window.addEventListener('load', computeSpacesTopbarOffset);
72
+ window.addEventListener('resize', computeSpacesTopbarOffset);
73
+ // Re-run shortly after load to catch late-mounted headers
74
+ setTimeout(computeSpacesTopbarOffset, 800);
75
  </script>
76
  </button>
77
 
78
 
79
  <style>
80
+ #theme-toggle { display: inline-flex; align-items: center; gap: 8px; border: none; background: transparent; padding: 6px 10px; border-radius: 8px; cursor: pointer; color: var(--text-color) !important; }
81
  #theme-toggle .icon.dark { display: none; }
82
  [data-theme="dark"] #theme-toggle .icon.light { display: none; }
83
  [data-theme="dark"] #theme-toggle .icon.dark { display: inline; }
app/src/content/assets/data/all_ratings_luis.csv CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1a47d8de2edf309fd39eb7e2ef5790d7f9c3ec4d5cc0f0c8680c12112f0d63e3
3
- size 63287
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:104433529e7d9c8a3bd297be1138e9e87677a666953d1362c517ec389c6c9172
3
+ size 64966
app/src/content/assets/data/banner_visualisation_data.csv CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b19a66e4f5999c3dcf60140f5eeab57016ebeb3d277db08c16dd0b5bede35495
3
- size 81529
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b155d8c319b1788befe716017fecca580768157feee6221f3af44b7bb9f9c7e5
3
+ size 81995
app/src/content/chapters/components.mdx CHANGED
@@ -284,21 +284,22 @@ Here are some examples that were vibe coded to inspire you.
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
- <Wide>
288
  <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" />
289
- </Wide>
290
  ---
291
- <Wide>
292
- <HtmlEmbed src="d3-pie.html" desc="D3 Pie charts by category" align="center" />
293
- </Wide>
294
  ---
295
- <Wide>
296
- <HtmlEmbed src="filters-quad.html" desc="Figure 7: Comparison across thresholds for all four filters individually: Formatting, Relevance, Visual Dependency, and Image-Question Correspondence." align="center" />
297
- </Wide>
298
 
299
- ---
300
  <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." />
301
  ---
 
 
 
 
 
 
 
302
 
303
  ### Iframes
304
 
 
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
 
app/src/content/chapters/markdown.mdx CHANGED
@@ -90,9 +90,9 @@ Hello i'm a standalone output block.
90
  ```python
91
  print("This script prints a very very long line to check overflow behavior.")
92
  ```
93
- ::::output
94
  This script prints a very very long line to check overflow behavior.
95
- ::::
96
  </Accordion>
97
 
98
 
@@ -122,8 +122,6 @@ The **citation keys** come from `app/src/content/bibliography.bib`.
122
 
123
  **Citation** use the `@` syntax (e.g., `[@vaswani2017attention]` or `@vaswani2017attention` in narrative form) and are **automatically** collected to render the **bibliography** at the end of the article.
124
 
125
- <Note variant="info" emoji="💡">Multiple citations to the same citation key are automatically merged into a single citation.</Note>
126
-
127
  1) In-text citation with brackets: [@vaswani2017attention].
128
 
129
  2) Narrative citation: As shown by @kingma2015adam, stochastic optimization is widely used.
@@ -144,6 +142,8 @@ The **citation keys** come from `app/src/content/bibliography.bib`.
144
  ```
145
  </Accordion>
146
 
 
 
147
  ### Footnote
148
 
149
  **Footnote** use an identifier like `[^f1]` and a definition anywhere in the document, e.g., `[^f1]: Your explanation`. They are **numbered** and **listed automatically** at the end of the article.
 
90
  ```python
91
  print("This script prints a very very long line to check overflow behavior.")
92
  ```
93
+ :::output
94
  This script prints a very very long line to check overflow behavior.
95
+ :::
96
  </Accordion>
97
 
98
 
 
122
 
123
  **Citation** use the `@` syntax (e.g., `[@vaswani2017attention]` or `@vaswani2017attention` in narrative form) and are **automatically** collected to render the **bibliography** at the end of the article.
124
 
 
 
125
  1) In-text citation with brackets: [@vaswani2017attention].
126
 
127
  2) Narrative citation: As shown by @kingma2015adam, stochastic optimization is widely used.
 
142
  ```
143
  </Accordion>
144
 
145
+ <Note variant="info" emoji="💡">You can change the citation style in the `astro.config.mjs` file. There are several styles available: `apa`, `vancouver`, `harvard1`, `chicago`, `mla`. Default is `apa`.</Note>
146
+
147
  ### Footnote
148
 
149
  **Footnote** use an identifier like `[^f1]` and a definition anywhere in the document, e.g., `[^f1]: Your explanation`. They are **numbered** and **listed automatically** at the end of the article.
app/src/content/chapters/writing-your-content.mdx CHANGED
@@ -151,40 +151,20 @@ Always pair it with text, icons, shape or position. The simulation helps you spo
151
 
152
  #### Using the palettes
153
 
154
- ##### CSS
155
- Use the generated **CSS variables** to style elements (e.g., `color: var(--palette-categorical-1)`).
156
 
157
  <Accordion title="Code example">
158
- ```css
159
- /* Usage */
160
- .series-a { color: var(--palette-categorical-1); }
161
- .series-b { color: var(--palette-categorical-2); }
162
- .heatmap-low { background: var(--palette-sequential-1); }
163
- .heatmap-high { background: var(--palette-sequential-6); }
164
- .neg { color: var(--palette-diverging-1); } /* left = primary */
165
- .pos { color: var(--palette-diverging-6); } /* right = complement */
166
-
167
- /* Override size */
168
- :root { --palette-count: 8; } /* global */
169
- :root { --palette-diverging-count: 7; } /* per palette */
170
- ```
171
- </Accordion>
172
-
173
- <Note variant="info" emoji='💡'>**CSS overrides** automatically trigger a **palette regeneration** via JS observers; no manual call is needed.</Note>
174
 
175
- ##### JS
176
- Fetch arrays of colors via `window.ColorPalettes.getColors('categorical')`. To change sizes **at runtime**, set CSS vars (e.g., `document.documentElement.style.setProperty('--palette-count','8')`) and call `window.ColorPalettes.refresh()`.
177
 
178
- <Accordion title="Code example">
179
- ```js
180
- // Usage
181
- const cat = window.ColorPalettes.getColors('categorical');
182
- const seq = window.ColorPalettes.getColors('sequential');
183
- const div = window.ColorPalettes.getColors('diverging');
184
-
185
- // Override size (runtime)
186
- document.documentElement.style.setProperty('--palette-count', '8');
187
- document.documentElement.style.setProperty('--palette-diverging-count', '7');
188
  window.ColorPalettes.refresh();
189
  ```
190
  </Accordion>
 
151
 
152
  #### Using the palettes
153
 
154
+ You can copy them manually from the palette viewer just above, or fetch colors via `window.ColorPalettes.getColors(key, count)` where `key` is one of `'categorical'`, `'sequential'`, `'diverging'`, and `count` is the desired number of colors (defaults to 6).
 
155
 
156
  <Accordion title="Code example">
157
+ ```js
158
+ // Usage (with explicit counts)
159
+ const cat = window.ColorPalettes.getColors('categorical', 8);
160
+ const seq = window.ColorPalettes.getColors('sequential', 8);
161
+ const div = window.ColorPalettes.getColors('diverging', 7);
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ // For current primary color string
164
+ const primaryHex = window.ColorPalettes.getPrimary();
165
 
166
+ // If you change --primary-color dynamically, call refresh to notify listeners
167
+ document.documentElement.style.setProperty('--primary-color', '#6D4AFF');
 
 
 
 
 
 
 
 
168
  window.ColorPalettes.refresh();
169
  ```
170
  </Accordion>
app/src/content/embeds/d3-area-stacked.html ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-boxplot.html ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-pie.html CHANGED
@@ -1,12 +1,55 @@
1
  <div class="d3-pie" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
- .d3-pie .legend { font-size: 12px; line-height: 1.35; color: var(--text-color); }
 
 
 
4
  .d3-pie .legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; align-items:center; justify-content:center; }
5
  .d3-pie .legend .item { display:flex; align-items:center; gap:8px; white-space:nowrap; }
6
  .d3-pie .legend .swatch { width:14px; height:14px; border-radius:3px; display:inline-block; border: 1px solid var(--border-color); }
 
7
  .d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
 
8
  .d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
9
  .d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: rgba(255,255,255,0.2); stroke-width: 3px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </style>
11
  <script>
12
  (() => {
@@ -50,18 +93,16 @@
50
  tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
51
  } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
52
 
53
- // SVG scaffolding
54
- const svg = d3.select(container).append('svg').attr('width','100%').style('display','block').attr('preserveAspectRatio','xMidYMin meet');
55
- const gRoot = svg.append('g');
56
- const gLegend = gRoot.append('foreignObject').attr('class','legend');
57
- const gPlots = gRoot.append('g').attr('class','plots');
58
 
59
  // Metrics (order and labels as in the Python script)
60
  const METRICS = [
61
- { key:'answer_total_tokens', name:'Answer Tokens', title:'Weighted by Answer Tokens', letter:'a' },
62
- { key:'total_samples', name:'Number of Samples', title:'Weighted by Number of Samples', letter:'b' },
63
- { key:'total_turns', name:'Number of Turns', title:'Weighted by Number of Turns', letter:'c' },
64
- { key:'total_images', name:'Number of Images', title:'Weighted by Number of Images', letter:'d' }
65
  ];
66
 
67
  // CSV: load from public path
@@ -90,28 +131,24 @@
90
 
91
  // Layout
92
  let width=800; const margin = { top: 8, right: 24, bottom: 0, left: 24 };
93
- const CAPTION_GAP = 24; // espace entre titre et donut
94
  const GAP_X = 20; // espace entre colonnes
95
  const GAP_Y = 12; // espace entre lignes
96
- const LEGEND_HEIGHT_DESKTOP = 62; // hauteur de la légende sur desktop
97
- const LEGEND_HEIGHT_MOBILE = 84; // hauteur de la légende sur mobile
98
  const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
 
 
 
 
99
  const updateSize = () => {
100
  width = container.clientWidth || 800;
101
- svg.attr('width', width);
102
- gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
103
  return { innerWidth: width - margin.left - margin.right };
104
  };
105
 
106
- function renderLegend(categories, colorOf, width, x, legendY, legendHeight){
107
- gLegend.attr('x', x).attr('y', legendY).attr('width', width).attr('height', legendHeight);
108
- const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
109
- root
110
- .style('height', legendHeight + 'px')
111
- .style('display', 'flex')
112
- .style('align-items', 'center')
113
- .style('justify-content', 'center');
114
- root.html(`<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>`);
115
  }
116
 
117
  function drawPies(rows){
@@ -122,60 +159,25 @@
122
  const color = d3.scaleOrdinal().domain(categories).range(d3.schemeTableau10);
123
  const colorOf = (cat) => color(cat || 'Unknown');
124
 
125
- // Clear plots
126
- gPlots.selectAll('*').remove();
127
-
128
- // Colonnes responsives: tenter 4 colonnes si possible, sinon descendre à 3/2/1
129
- const selectCols = () => {
130
- const MIN_RADIUS = 80; // garantir lisibilité
131
- const allowed = [4, 2, 1]; // seulement 4 / 2 / 1 colonnes
132
- // 1) essayer avec contrainte de rayon minimal
133
- for (const c of allowed) {
134
- const cw = (innerWidth - GAP_X * (c - 1)) / c;
135
- const r = Math.max(30, Math.min(cw * 0.42, 120));
136
- const gw = c * (r * 2) + (c - 1) * GAP_X;
137
- if (gw <= innerWidth && r >= MIN_RADIUS) {
138
- return { c, r };
139
- }
140
- }
141
- // 2) sinon, première config qui tient (même si plus petit rayon)
142
- for (const c of allowed) {
143
- const cw = (innerWidth - GAP_X * (c - 1)) / c;
144
- const r = Math.max(30, Math.min(cw * 0.42, 120));
145
- const gw = c * (r * 2) + (c - 1) * GAP_X;
146
- if (gw <= innerWidth) {
147
- return { c, r };
148
- }
149
- }
150
- // 3) fallback très petit écran
151
- const r1 = Math.max(30, Math.min(innerWidth * 0.42, 120));
152
- return { c: 1, r: r1 };
153
- };
154
- const { c: cols, r: radius } = selectCols();
155
- const rowsCount = Math.ceil(METRICS.length / cols);
156
- const innerR = Math.round(radius * 0.28);
157
- // Calculer un espacement effectif pour occuper toute la largeur disponible
158
- const baseGap = GAP_X;
159
- const effectiveGapX = cols > 1
160
- ? Math.max(baseGap, Math.floor((innerWidth - cols * (radius * 2)) / (cols - 1)))
161
- : 0;
162
- // largeur réelle de la grille avec l'espacement effectif
163
- const gridWidth = cols * (radius * 2) + (cols - 1) * effectiveGapX;
164
- const xOffset = Math.max(0, Math.floor((innerWidth - gridWidth) / 2));
165
- gPlots.attr('transform', `translate(${xOffset},${TOP_OFFSET})`);
166
- const perRowHeight = Math.ceil(radius * 2 + CAPTION_GAP + 20); // donut + caption + marge
167
- const plotsHeight = rowsCount * perRowHeight + (rowsCount - 1) * GAP_Y;
168
 
169
  const pie = d3.pie().sort(null).value(d => d.value).padAngle(0.02);
170
  const arc = d3.arc().innerRadius(innerR).outerRadius(radius).cornerRadius(3);
171
  const arcLabel = d3.arc().innerRadius((innerR + radius) / 2).outerRadius((innerR + radius) / 2);
172
 
173
- // Positionner la légende sous les graphiques, calée sur la grille centrée
174
- const legendY = TOP_OFFSET + plotsHeight + 4;
175
- const legendHeight = innerWidth <= 600 ? LEGEND_HEIGHT_MOBILE : LEGEND_HEIGHT_DESKTOP;
176
- renderLegend(categories, colorOf, gridWidth, xOffset, legendY, legendHeight);
177
-
178
- const captions = new Map(METRICS.map(m => [m.key, `${m.title}`]));
179
 
180
  METRICS.forEach((metric, idx) => {
181
  // Aggregate by category
@@ -184,12 +186,20 @@
184
  const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
185
  const totalSum = d3.sum(values, d => d.value);
186
 
187
- const rowIdx = Math.floor(idx / cols);
188
- const colIdx = idx % cols;
189
- const cx = colIdx * ((radius * 2) + effectiveGapX) + radius;
190
- const cy = TOP_OFFSET + rowIdx * (perRowHeight + GAP_Y) + CAPTION_GAP + radius;
191
-
192
- const gCell = gPlots.append('g').attr('transform', `translate(${cx},${cy})`);
 
 
 
 
 
 
 
 
193
 
194
  if (!totalSum || totalSum <= 0) {
195
  gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');
@@ -222,17 +232,12 @@
222
  .text(d => `${percent(d.data.value).toFixed(1)}%`);
223
  }
224
 
225
- // Caption above donut
226
- gCell.append('text')
227
- .attr('class','caption')
228
- .attr('text-anchor','middle')
229
- .attr('y', -(radius + (CAPTION_GAP - 6)))
230
- .text(captions.get(metric.key));
231
  });
232
 
233
- // Définir la hauteur totale du SVG après avoir placé les éléments
234
- const totalHeight = Math.ceil(margin.top + TOP_OFFSET + plotsHeight + 4 + legendHeight + margin.bottom);
235
- svg.attr('height', totalHeight);
236
  }
237
 
238
  async function init(){
 
1
  <div class="d3-pie" style="width:100%;margin:10px 0;"></div>
2
  <style>
3
+ /* Layout piloté par container queries (par rapport au parent) */
4
+ .d3-pie { container-type: inline-size; }
5
+ .d3-pie .legend { width: 80%;margin: 0 auto; font-size: 12px; line-height: 1.35; color: var(--text-color); }
6
+ .d3-pie .legend { margin-bottom: 32px; }
7
  .d3-pie .legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; align-items:center; justify-content:center; }
8
  .d3-pie .legend .item { display:flex; align-items:center; gap:8px; white-space:nowrap; }
9
  .d3-pie .legend .swatch { width:14px; height:14px; border-radius:3px; display:inline-block; border: 1px solid var(--border-color); }
10
+ .d3-pie .legend .title { display:block; text-align:center; font-weight:800; margin-bottom:6px; }
11
  .d3-pie .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }
12
+ .d3-pie .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }
13
  .d3-pie .nodata { font-size: 12px; fill: var(--muted-color); }
14
  .d3-pie .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: rgba(255,255,255,0.2); stroke-width: 3px; }
15
+ /* Layout HTML (pas JS) pour la grille et les cellules */
16
+ .d3-pie .plots-grid {
17
+ display: flex;
18
+ flex-wrap: wrap;
19
+ justify-content: center;
20
+ align-items: flex-start;
21
+ gap: 12px 20px;
22
+ margin-top: 4px;
23
+ margin-left: auto;
24
+ margin-right: auto;
25
+ width: 100%;
26
+ }
27
+ /* Par défaut (flux ~1280): 2 colonnes centrées */
28
+ .content-grid .d3-pie .plots-grid { width: 100%; }
29
+ .content-grid .d3-pie .pie-cell { flex: 0 0 calc((100% - 20px)/2); }
30
+ /* En wrappers larges: viser 4 colonnes si l'espace le permet */
31
+ .wide .d3-pie .plots-grid,
32
+ .full-width .d3-pie .plots-grid { width: 100%; }
33
+ .wide .d3-pie .pie-cell,
34
+ .full-width .d3-pie .pie-cell { flex: 0 0 calc((100% - 60px)/4); }
35
+ /* Forcer 2 colonnes dans le flux lorsque le parent ~1280px */
36
+ .content-grid .d3-pie .plots-grid { width: min(740px, 100%); }
37
+ .d3-pie .pie-cell {
38
+ display: flex;
39
+ flex-direction: column;
40
+ align-items: center;
41
+ flex: 0 0 360px; /* 2 colonnes fixes dans le flux à 1280px */
42
+ }
43
+ /* 4/2/1 colonnes en fonction de la largeur du parent */
44
+ /* @container (min-width: 740px) {
45
+ .d3-pie .plots-grid { width: 740px; }
46
+ }
47
+ @container (max-width: 739.98px) {
48
+ .d3-pie .plots-grid { width: 100%; }
49
+ } */
50
+ @media (max-width: 500px) {
51
+ .d3-pie .pie-cell { flex: 0 0 100%; }
52
+ }
53
  </style>
54
  <script>
55
  (() => {
 
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; }
95
 
96
+ // HTML scaffolding: legend and plots grid as HTML; only pies are SVG
97
+ const legendHost = document.createElement('div'); legendHost.className = 'legend'; container.appendChild(legendHost);
98
+ const plotsHost = document.createElement('div'); plotsHost.className = 'plots-grid'; container.appendChild(plotsHost);
 
 
99
 
100
  // Metrics (order and labels as in the Python script)
101
  const METRICS = [
102
+ { key:'answer_total_tokens', name:'Answer Tokens', title:'Weighted by ', letter:'a' },
103
+ { key:'total_samples', name:'Number of Samples', title:'Weighted by ', letter:'b' },
104
+ { key:'total_turns', name:'Number of Turns', title:'Weighted by ', letter:'c' },
105
+ { key:'total_images', name:'Number of Images', title:'Weighted by ', letter:'d' }
106
  ];
107
 
108
  // CSV: load from public path
 
131
 
132
  // Layout
133
  let width=800; const margin = { top: 8, right: 24, bottom: 0, left: 24 };
134
+ const CAPTION_GAP = 36; // espace entre titre et donut
135
  const GAP_X = 20; // espace entre colonnes
136
  const GAP_Y = 12; // espace entre lignes
 
 
137
  const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
138
+ const DONUT_INNER_RATIO = 0.58; // ratio du trou central (0 = pie plein, 0.5 = moitié)
139
+ // LEGEND_GAP supprimé: l'espacement est désormais géré en CSS via .d3-pie .legend { margin-bottom }
140
+ const SVG_VPAD = 16; // padding vertical supplémentaire à l'intérieur des SVG pour éviter la coupe
141
+
142
  const updateSize = () => {
143
  width = container.clientWidth || 800;
 
 
144
  return { innerWidth: width - margin.left - margin.right };
145
  };
146
 
147
+ function renderLegend(categories, colorOf){
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){
 
159
  const color = d3.scaleOrdinal().domain(categories).range(d3.schemeTableau10);
160
  const colorOf = (cat) => color(cat || 'Unknown');
161
 
162
+ // Clear plots grid
163
+ plotsHost.innerHTML = '';
164
+
165
+ // Légende au-dessus, centrée
166
+ renderLegend(categories, colorOf);
167
+
168
+ // Rayon fixé selon la largeur cible d'une cellule (gérée par CSS)
169
+ const CELL_BASIS = 360; // doit correspondre à .pie-cell { flex-basis }
170
+ const radius = Math.max(80, Math.min(120, Math.floor(CELL_BASIS * 0.42)));
171
+ const innerR = Math.round(radius * DONUT_INNER_RATIO);
172
+ // Placement géré par CSS; ici on ne fait que l'espacement vertical minimal
173
+ plotsHost.style.position = 'relative';
174
+ plotsHost.style.marginTop = (TOP_OFFSET) + 'px';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  const pie = d3.pie().sort(null).value(d => d.value).padAngle(0.02);
177
  const arc = d3.arc().innerRadius(innerR).outerRadius(radius).cornerRadius(3);
178
  const arcLabel = d3.arc().innerRadius((innerR + radius) / 2).outerRadius((innerR + radius) / 2);
179
 
180
+ // Légende déjà rendue au-dessus
 
 
 
 
 
181
 
182
  METRICS.forEach((metric, idx) => {
183
  // Aggregate by category
 
186
  const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));
187
  const totalSum = d3.sum(values, d => d.value);
188
 
189
+ // Create HTML cell container
190
+ const cell = document.createElement('div');
191
+ cell.className = 'pie-cell';
192
+ cell.style.width = (radius * 2) + 'px';
193
+ cell.style.height = (radius * 2 + SVG_VPAD * 2 + CAPTION_GAP + 24) + 'px';
194
+ cell.style.display = 'flex';
195
+ cell.style.flexDirection = 'column';
196
+ cell.style.alignItems = 'center';
197
+ cell.style.justifyContent = 'flex-start';
198
+ plotsHost.appendChild(cell);
199
+
200
+ // SVG pie inside cell
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');
 
232
  .text(d => `${percent(d.data.value).toFixed(1)}%`);
233
  }
234
 
235
+ // HTML captions under the SVG (keep design)
236
+ const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center'; subtitleEl.style.marginTop = (CAPTION_GAP - 18) + 'px'; cell.appendChild(subtitleEl);
237
+ const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);
 
 
 
238
  });
239
 
240
+ // Container height flows naturally with HTML; nothing to do
 
 
241
  }
242
 
243
  async function init(){
app/src/content/embeds/d3-scatter.html ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="d3-scatter" style="width:100%;margin:10px 0;"></div>
2
+ <style>
3
+ /* Frameless: no controls, no axes, only dots */
4
+ .d3-scatter svg { display: block; }
5
+ </style>
6
+ <script>
7
+ (() => {
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 scriptEl = document.currentScript;
19
+ let container = scriptEl ? scriptEl.previousElementSibling : null;
20
+ if (!(container && container.classList && container.classList.contains('d3-scatter'))){
21
+ const cs = Array.from(document.querySelectorAll('.d3-scatter')).filter(el => !(el.dataset && el.dataset.mounted==='true'));
22
+ container = cs[cs.length-1] || null;
23
+ }
24
+ if (!container) return;
25
+ if (container.dataset){ if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }
26
+
27
+ // Tooltip
28
+ container.style.position = container.style.position || 'relative';
29
+ let tip = container.querySelector('.d3-tooltip'); let tipInner;
30
+ if (!tip) {
31
+ tip = document.createElement('div'); tip.className = 'd3-tooltip';
32
+ Object.assign(tip.style, { position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' });
33
+ tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
34
+ } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
35
+
36
+ // SVG
37
+ const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
38
+ const gRoot = svg.append('g');
39
+ const gGrid = gRoot.append('g').attr('class','grid');
40
+ const gAxes = gRoot.append('g').attr('class','axes');
41
+ const gDots = gRoot.append('g').attr('class','dots');
42
+ const gCentroids = gRoot.append('g').attr('class','centroids');
43
+ const gLegend = gRoot.append('foreignObject').attr('class','legend');
44
+
45
+ // State & scales
46
+ let width=800, height=360; const margin = { top: 8, right: 12, bottom: 8, left: 12 };
47
+ const x = d3.scaleLinear();
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){
54
+ for (const p of paths){
55
+ try {
56
+ const res = await fetch(p, { cache: 'no-cache' });
57
+ if (res.ok){ return await res.text(); }
58
+ } catch (e) {}
59
+ }
60
+ throw new Error('Failed to load data from provided paths');
61
+ }
62
+
63
+ let data = [];
64
+ let categories = [];
65
+ let colorMode = 'group';
66
+
67
+ function renderLegend(innerWidth){ gLegend.remove(); }
68
+
69
+ function updateScales(data){
70
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
71
+ const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
72
+ const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
73
+ const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
74
+
75
+ width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
76
+ const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
77
+
78
+ const xExtent = d3.extent(data, d=>d.x);
79
+ const yExtent = d3.extent(data, d=>d.y);
80
+ x.domain([xExtent[0], xExtent[1]]).range([0, innerWidth]).nice();
81
+ y.domain([yExtent[0], yExtent[1]]).range([innerHeight, 0]).nice();
82
+
83
+ // Frameless: no grid, no axes
84
+ gGrid.selectAll('*').remove();
85
+ gAxes.selectAll('*').remove();
86
+
87
+ renderLegend(innerWidth);
88
+
89
+ return { innerWidth, innerHeight };
90
+ }
91
+
92
+ function refreshPalette(){
93
+ try {
94
+ const raw = getComputedStyle(document.documentElement).getPropertyValue('--palette-categorical-json').trim();
95
+ const arr = raw ? JSON.parse(raw) : [];
96
+ if (arr && arr.length) color.range(arr);
97
+ else color.range(['var(--primary-color)','#4EA5B7','#E38A42','#CEC0FA','#98C97C','#F6BD60']);
98
+ } catch {
99
+ color.range(['var(--primary-color)','#4EA5B7','#E38A42','#CEC0FA','#98C97C','#F6BD60']);
100
+ }
101
+ // Recolor existing marks/labels after palette changes
102
+ try { if (data && data.length) draw(); } catch {}
103
+ }
104
+
105
+ function draw(){
106
+ if (!data || !data.length) return;
107
+ const { innerWidth, innerHeight } = updateScales(data);
108
+ const fillFor = d => colorMode === 'group' ? color(d.group) : 'var(--primary-color)';
109
+
110
+ const dots = gDots.selectAll('circle.dot').data(data, (d,i)=>d.id || i);
111
+ dots.enter().append('circle').attr('class','dot')
112
+ .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
113
+ .attr('fill', fillFor).attr('fill-opacity', 0.85)
114
+ .on('mouseenter', function(ev, d){
115
+ d3.select(this).attr('stroke','rgba(0,0,0,0.9)').attr('stroke-width', 1);
116
+ tipInner.innerHTML = `<div><strong>${d.label || 'Item'}</strong></div><div>(x: ${d.x.toFixed(2)}, y: ${d.y.toFixed(2)})</div><div>Category: ${d.group}</div>`;
117
+ tip.style.opacity = '1';
118
+ })
119
+ .on('mousemove', function(ev){ const [mx, my] = d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })
120
+ .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).attr('stroke','none'); })
121
+ .merge(dots)
122
+ .transition().duration(180)
123
+ .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())
124
+ .attr('fill', fillFor).attr('fill-opacity', 0.85);
125
+ dots.exit().remove();
126
+
127
+ // Compute centroids per category
128
+ const centroids = Array.from(
129
+ d3.rollup(
130
+ data,
131
+ (v) => ({
132
+ category: v[0] ? v[0].group : 'Unknown',
133
+ x: d3.mean(v, (d) => d.x),
134
+ y: d3.mean(v, (d) => d.y),
135
+ count: v.length
136
+ }),
137
+ (d) => d.group
138
+ ).values()
139
+ );
140
+
141
+ // Map to pixel space nodes for collision-avoiding label placement
142
+ const nodes = centroids.map((c) => ({
143
+ category: c.category,
144
+ count: c.count,
145
+ targetX: x(c.x),
146
+ targetY: y(c.y),
147
+ x: x(c.x),
148
+ y: y(c.y),
149
+ width: Math.max(18, (String(c.category || '').length || 6) * 11),
150
+ height: 16
151
+ }));
152
+
153
+ if (nodes.length > 1) {
154
+ const sim = d3.forceSimulation(nodes)
155
+ .force('x', d3.forceX((d) => d.targetX).strength(0.9))
156
+ .force('y', d3.forceY((d) => d.targetY).strength(0.9))
157
+ .force('collide', d3.forceCollide((d) => Math.hypot(d.width/2, d.height/2) + 15))
158
+ .stop();
159
+ for (let i = 0; i < 650; i++) sim.tick();
160
+ const maxOffset = 45;
161
+ nodes.forEach((n) => {
162
+ const dx = n.x - n.targetX, dy = n.y - n.targetY; const dist = Math.hypot(dx, dy);
163
+ if (dist > maxOffset && dist > 0) { const s = maxOffset / dist; n.x = n.targetX + dx * s; n.y = n.targetY + dy * s; }
164
+ });
165
+ }
166
+
167
+ const labels = gCentroids.selectAll('g.centroid').data(nodes, d => d.category || 'Unknown');
168
+ const enter = labels.enter().append('g').attr('class','centroid').attr('pointer-events','none');
169
+ enter.append('text').attr('class','label-bg').attr('text-anchor','middle').attr('dominant-baseline','middle');
170
+ enter.append('text').attr('class','label-fg').attr('text-anchor','middle').attr('dominant-baseline','middle');
171
+ const merged = enter.merge(labels);
172
+ merged
173
+ .attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)
174
+ .each(function(d){
175
+ const base = color(d.category || 'Unknown') || 'var(--text-color)';
176
+ const bg = getComputedStyle(document.documentElement).getPropertyValue('--surface-bg').trim() || '#fff';
177
+ const bgNode = this.querySelector('text.label-bg');
178
+ const fgNode = this.querySelector('text.label-fg');
179
+ if (bgNode) {
180
+ bgNode.textContent = d.category;
181
+ bgNode.style.setProperty('fill', bg, 'important');
182
+ bgNode.style.setProperty('stroke', bg);
183
+ bgNode.style.setProperty('stroke-width', '10px');
184
+ bgNode.style.setProperty('paint-order', 'stroke fill');
185
+ bgNode.style.setProperty('font-weight','800');
186
+ bgNode.style.setProperty('font-size','16px');
187
+ }
188
+ if (fgNode) {
189
+ fgNode.textContent = d.category;
190
+ fgNode.style.setProperty('fill', base, 'important');
191
+ fgNode.style.setProperty('font-weight','800');
192
+ fgNode.style.setProperty('font-size','16px');
193
+ }
194
+ });
195
+ labels.exit().remove();
196
+ }
197
+
198
+ // Initial load
199
+ refreshPalette();
200
+ document.addEventListener('palettes:updated', refreshPalette);
201
+
202
+ (async () => {
203
+ try {
204
+ const csvText = await fetchFirstAvailable([
205
+ '/data/banner_visualisation_data.csv',
206
+ './assets/data/banner_visualisation_data.csv',
207
+ '../assets/data/banner_visualisation_data.csv',
208
+ '/data/banner_visualisation_data.csv'
209
+ ]);
210
+ const rows = d3.csvParse(csvText);
211
+ data = rows.map((r, i) => ({
212
+ id: +r.original_id ?? i,
213
+ x: +r.x_position,
214
+ y: +r.y_position,
215
+ group: r.category || 'Unknown',
216
+ label: r.subset || r.category || `Item ${i+1}`
217
+ })).filter(d => Number.isFinite(d.x) && Number.isFinite(d.y));
218
+ categories = Array.from(new Set(data.map(d=>d.group)));
219
+ color.domain(categories);
220
+ draw();
221
+ } catch (e) {
222
+ const pre = document.createElement('pre'); pre.style.color = 'crimson'; pre.textContent = 'Failed to load scatter data.'; container.appendChild(pre);
223
+ }
224
+ })();
225
+
226
+ const rerender = () => { draw(); };
227
+ if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
228
+ };
229
+
230
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
231
+ })();
232
+ </script>
233
+
234
+
app/src/content/embeds/demo/palettes.html CHANGED
@@ -66,10 +66,10 @@
66
  <div class="palettes__field">
67
  <div class="palettes__label-row">
68
  <label class="palettes__label" for="color-count">Number of colors</label>
69
- <output id="color-count-out" for="color-count" class="ghost-badge">6</output>
70
  </div>
71
  <div class="palettes__count">
72
- <input id="color-count" type="range" min="6" max="10" step="1" value="6" aria-label="Number of colors" />
73
  </div>
74
  </div>
75
  </div>
@@ -103,18 +103,12 @@
103
  { key: 'diverging', title: 'Diverging', desc: 'Opposing extremes via <strong>base → white → complement</strong>; smooth contrast around a neutral midpoint.' }
104
  ];
105
 
106
- const getCssVar = (name) => {
107
- try { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } catch { return ''; }
108
- };
109
-
110
- const getPaletteColors = (key) => {
111
- const count = Number(getCssVar(`--palette-${key}-count-current`)) || Number(getCssVar('--palette-count')) || 6;
112
- const colors = [];
113
- for (let i = 1; i <= count; i++) {
114
- const v = getCssVar(`--palette-${key}-${i}`);
115
- if (v) colors.push(v);
116
  }
117
- return colors;
118
  };
119
 
120
  const render = () => {
@@ -123,8 +117,10 @@
123
  if (!root) return;
124
  const grid = root.querySelector('.palettes__grid');
125
  if (!grid) return;
 
 
126
  const html = cards.map((c) => {
127
- const colors = getPaletteColors(c.key);
128
  const swatches = colors.map(col => `<div class="sw" style="background:${col}"></div>`).join('');
129
  return `
130
  <div class="palette-card" data-colors="${colors.join(',')}">
@@ -172,19 +168,13 @@
172
  const input = document.getElementById('color-count');
173
  const out = document.getElementById('color-count-out');
174
  if (!input) return;
175
- const read = () => {
176
- const v = Number(getCssVar('--palette-count')) || 6;
177
- return Math.max(6, Math.min(10, v));
178
- };
179
- const write = (v) => {
180
- document.documentElement.style.setProperty('--palette-count', String(v));
181
- if (window.ColorPalettes && typeof window.ColorPalettes.refresh === 'function') window.ColorPalettes.refresh();
182
- };
183
  const syncOut = () => { if (out) out.textContent = String(read()); };
184
- const syncInput = () => { try { input.value = String(read()); } catch {} };
185
- syncInput(); syncOut();
186
- input.addEventListener('input', () => { write(input.value); syncOut(); });
187
- document.addEventListener('palettes:updated', () => { syncInput(); syncOut(); render(); });
188
  };
189
 
190
  let copyDelegationSetup = false;
@@ -218,17 +208,8 @@
218
  setupCountControl();
219
  render();
220
  setupCopyDelegation();
221
- // Re-render when global palettes update and when :root style changes
222
  document.addEventListener('palettes:updated', render);
223
- const mo = new MutationObserver(() => render());
224
- mo.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
225
-
226
- // Fallback: if global script loads after this fragment and we missed the event
227
- const waitForPalettes = () => {
228
- if (getCssVar('--palette-categorical-1')) { render(); return; }
229
- setTimeout(waitForPalettes, 50);
230
- };
231
- waitForPalettes();
232
  };
233
 
234
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
 
66
  <div class="palettes__field">
67
  <div class="palettes__label-row">
68
  <label class="palettes__label" for="color-count">Number of colors</label>
69
+ <output id="color-count-out" for="color-count" class="ghost-badge">8</output>
70
  </div>
71
  <div class="palettes__count">
72
+ <input id="color-count" type="range" min="6" max="10" step="1" value="8" aria-label="Number of colors" />
73
  </div>
74
  </div>
75
  </div>
 
103
  { key: 'diverging', title: 'Diverging', desc: 'Opposing extremes via <strong>base → white → complement</strong>; smooth contrast around a neutral midpoint.' }
104
  ];
105
 
106
+ const getPaletteColors = (key, count) => {
107
+ const total = Number(count) || 6;
108
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
109
+ return window.ColorPalettes.getColors(key, total) || [];
 
 
 
 
 
 
110
  }
111
+ return [];
112
  };
113
 
114
  const render = () => {
 
117
  if (!root) return;
118
  const grid = root.querySelector('.palettes__grid');
119
  if (!grid) return;
120
+ const input = document.getElementById('color-count');
121
+ const total = input ? Number(input.value) || 6 : 6;
122
  const html = cards.map((c) => {
123
+ const colors = getPaletteColors(c.key, total);
124
  const swatches = colors.map(col => `<div class="sw" style="background:${col}"></div>`).join('');
125
  return `
126
  <div class="palette-card" data-colors="${colors.join(',')}">
 
168
  const input = document.getElementById('color-count');
169
  const out = document.getElementById('color-count-out');
170
  if (!input) return;
171
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
172
+ const read = () => clamp(Number(input.value) || 6, 6, 10);
 
 
 
 
 
 
173
  const syncOut = () => { if (out) out.textContent = String(read()); };
174
+ const onChange = () => { syncOut(); render(); };
175
+ syncOut();
176
+ input.addEventListener('input', onChange);
177
+ document.addEventListener('palettes:updated', () => { syncOut(); render(); });
178
  };
179
 
180
  let copyDelegationSetup = false;
 
208
  setupCountControl();
209
  render();
210
  setupCopyDelegation();
211
+ // Re-render when primary color changes
212
  document.addEventListener('palettes:updated', render);
 
 
 
 
 
 
 
 
 
213
  };
214
 
215
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
app/src/content/embeds/filters-quad.html CHANGED
@@ -61,10 +61,60 @@
61
  .quad-cell .legend .items { display:flex; flex-wrap:wrap; gap:8px 12px; align-items:center; justify-content:flex-end; }
62
  .quad-cell .legend .item { display:flex; align-items:center; gap:6px; white-space:nowrap; }
63
  .quad-cell .legend .swatch { width:10px; height:10px; border-radius:50%; display:inline-block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  </style>
65
  <script>
66
  (() => {
67
  const THIS_SCRIPT = document.currentScript;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  const ensureD3 = (cb) => {
69
  if (window.d3 && typeof window.d3.select === 'function') return cb();
70
  let s = document.getElementById('d3-cdn-script');
@@ -98,15 +148,43 @@
98
 
99
  // Tooltip
100
  cell.style.position = cell.style.position || 'relative';
101
- let tip = cell.querySelector('.d3-tooltip'); let tipInner;
102
- if (!tip) { tip = document.createElement('div'); tip.className = 'd3-tooltip'; Object.assign(tip.style, { position:'absolute', top:'0', left:'0', 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); cell.appendChild(tip);} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  // State
105
  let metricList = []; let runList = []; let runOrder = []; const dataByMetric = new Map();
106
- let width = 800, height = 340; const margin = { top: 12, right: 20, bottom: 46, left: 56 };
107
  const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
108
  const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
109
- let isRankStrictFlag = false; let rankTickMax = 1;
 
 
110
 
111
  // Colors and markers (match original embeds)
112
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
@@ -130,6 +208,16 @@
130
  return selection.append('circle').attr('r', s);
131
  }
132
  }
 
 
 
 
 
 
 
 
 
 
133
  // Ready signal for async load completion
134
  let readyResolve = null;
135
  const ready = new Promise((res)=> { readyResolve = res; });
@@ -162,22 +250,42 @@
162
  // Axes
163
  gAxes.selectAll('*').remove();
164
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
165
- const isMobile = (typeof window !== 'undefined' && window.matchMedia) ? window.matchMedia('(max-width: 980px)').matches : false;
166
- if (isMobile) {
167
- const fmtK = (v) => {
168
- const abs = Math.abs(v);
169
- if (abs >= 1000) {
170
- const n = v / 1000;
171
- return (Math.round(n) === n ? n : d3.format('.1f')(n)) + 'K';
172
- }
173
- return d3.format('d')(v);
174
- };
175
- xAxis = xAxis.tickFormat(fmtK);
176
- }
177
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
178
  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'); });
179
  gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  // Legend box (top-right)
182
  // Per-cell legend hidden; global legend is used
183
  const legendWidth = 0, legendHeight = 0;
@@ -194,9 +302,24 @@
194
  const isRank = /rank/i.test(metricKey); const isAverage = /average/i.test(metricKey); const isRankStrict = isRank && !isAverage;
195
  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); }); });
196
  if (!isFinite(minStep) || !isFinite(maxStep)) return;
197
- xScale.domain([minStep, maxStep]); if (isRank) { rankTickMax = Math.max(1, Math.round(maxVal)); yScale.domain([rankTickMax, 1]); } else { yScale.domain([minVal, maxVal]).nice(); }
198
- isRankStrictFlag = isRankStrict;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
 
200
  const { innerWidth, innerHeight } = updateScales();
201
 
202
  const series = runs.map((r, i) => ({ run:r, color: pool[i % pool.length], marker: markerShapes[i % markerShapes.length], values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
@@ -242,10 +365,14 @@
242
  const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
243
  const hoverLine = gHover.append('line').attr('stroke','rgba(0,0,0,0.25)').attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
244
  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);
245
- function onMove(ev){ 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);
246
- let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${nearest}</div>`; series.forEach(s=>{ const m = new Map(s.values.map(v=>[v.step, v])); const pt = m.get(nearest); if (pt && pt.value!=null){ const fmt = (vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4)); const err = (pt.stderr!=null && isFinite(pt.stderr) && pt.stderr>0) ? ` ± ${fmt(pt.stderr)}` : ''; html+=`<div><span style=\"display:inline-block;width:10px;height:10px;background:${s.color};border-radius:50%;margin-right:6px;\"></span><strong>${s.run}</strong> ${fmt(pt.value)}${err}</div>`; }});
 
 
 
 
247
  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)`; }
248
- function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }
249
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
250
  }
251
 
@@ -284,7 +411,16 @@
284
  return {
285
  ready,
286
  getMetrics: () => metricList.slice(),
287
- setMetric: (m) => { if (m) renderMetric(m); }
 
 
 
 
 
 
 
 
 
288
  };
289
  }
290
 
@@ -324,11 +460,27 @@
324
  ctrl.innerHTML = '';
325
  const label = document.createElement('label'); label.textContent = 'Metric';
326
  const select = document.createElement('select');
327
- metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=m; select.appendChild(o); });
328
  if (def) select.value = def;
329
  label.appendChild(select); ctrl.appendChild(label);
330
 
331
- const applyAll = (v) => instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  if (def) applyAll(def);
333
  select.addEventListener('change', () => applyAll(select.value));
334
 
 
61
  .quad-cell .legend .items { display:flex; flex-wrap:wrap; gap:8px 12px; align-items:center; justify-content:flex-end; }
62
  .quad-cell .legend .item { display:flex; align-items:center; gap:6px; white-space:nowrap; }
63
  .quad-cell .legend .swatch { width:10px; height:10px; border-radius:50%; display:inline-block; }
64
+ /* Tooltip refined styling */
65
+ .filters-quad .d3-tooltip {
66
+ z-index: 20;
67
+ backdrop-filter: saturate(1.12) blur(8px);
68
+ }
69
+ .filters-quad .d3-tooltip__inner {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: 6px;
73
+ min-width: 220px;
74
+ }
75
+ .filters-quad .d3-tooltip__inner > div:first-child {
76
+ font-weight: 800;
77
+ letter-spacing: 0.1px;
78
+ margin-bottom: 0;
79
+ }
80
+ .filters-quad .d3-tooltip__inner > div:nth-child(2) {
81
+ font-size: 11px;
82
+ color: var(--muted-color);
83
+ display: block;
84
+ margin-top: -4px;
85
+ margin-bottom: 2px;
86
+ letter-spacing: 0.1px;
87
+ }
88
+ .filters-quad .d3-tooltip__inner > div:nth-child(n+3) {
89
+ padding-top: 6px;
90
+ border-top: 1px solid var(--border-color);
91
+ }
92
+ .filters-quad .d3-tooltip__inner svg {
93
+ display: inline-block;
94
+ vertical-align: middle;
95
+ margin-right: 2px;
96
+ }
97
+ .filters-quad .d3-tooltip__inner strong {
98
+ margin-right: 6px;
99
+ }
100
  </style>
101
  <script>
102
  (() => {
103
  const THIS_SCRIPT = document.currentScript;
104
+ // Pretty label mapping for metric keys
105
+ const prettyMetricLabel = (key) => {
106
+ if (!key) return '';
107
+ const table = {
108
+ 'ai2d_exact_match': 'AI2D Exact Match',
109
+ 'average_rank': 'Average Rank',
110
+ };
111
+ if (table[key]) return table[key];
112
+ const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
113
+ return cleaned.split(/\s+/).map(w => {
114
+ if (/^(ai2d|umap|id|auc|f1)$/i.test(w)) return w.toUpperCase();
115
+ return w.charAt(0).toUpperCase() + w.slice(1);
116
+ }).join(' ');
117
+ };
118
  const ensureD3 = (cb) => {
119
  if (window.d3 && typeof window.d3.select === 'function') return cb();
120
  let s = document.getElementById('d3-cdn-script');
 
148
 
149
  // Tooltip
150
  cell.style.position = cell.style.position || 'relative';
151
+ let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;
152
+ if (!tip) {
153
+ tip = document.createElement('div');
154
+ tip.className = 'd3-tooltip';
155
+ Object.assign(tip.style, {
156
+ position:'absolute',
157
+ top:'0',
158
+ left:'0',
159
+ transform:'translate(-9999px,-9999px)',
160
+ pointerEvents:'none',
161
+ padding:'10px 12px',
162
+ borderRadius:'12px',
163
+ fontSize:'12px',
164
+ lineHeight:'1.35',
165
+ border:'1px solid var(--border-color)',
166
+ background:'var(--surface-bg)',
167
+ color:'var(--text-color)',
168
+ boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)',
169
+ opacity:'0',
170
+ transition:'opacity .12s ease',
171
+ backdropFilter:'saturate(1.12) blur(8px)'
172
+ });
173
+ tipInner = document.createElement('div');
174
+ tipInner.className = 'd3-tooltip__inner';
175
+ tipInner.style.textAlign='left';
176
+ tip.appendChild(tipInner);
177
+ cell.appendChild(tip);
178
+ } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
179
 
180
  // State
181
  let metricList = []; let runList = []; let runOrder = []; const dataByMetric = new Map();
182
+ let width = 800, height = 340; const margin = { top: 16, right: 20, bottom: 46, left: 56 };
183
  const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();
184
  const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
185
+ let isRankStrictFlag = false; let isRankMetricFlag = false; let rankTickMax = 1;
186
+ let sharedYConfig = null; // { type: 'rank_strict', maxRank } | { type: 'value', min, max }
187
+ let axisLabelY = 'Value';
188
 
189
  // Colors and markers (match original embeds)
190
  const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
 
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; });
 
250
  // Axes
251
  gAxes.selectAll('*').remove();
252
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
253
+ const fmtK = (v) => {
254
+ const abs = Math.abs(v);
255
+ if (abs >= 1000) {
256
+ const n = v / 1000;
257
+ const s = d3.format('.1f')(n);
258
+ return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';
259
+ }
260
+ return d3.format('d')(v);
261
+ };
262
+ xAxis = xAxis.tickFormat(fmtK);
 
 
263
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
264
  gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
265
  gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
266
 
267
+ // Axis labels
268
+ gAxes.append('text')
269
+ .attr('class', 'x-axis-label')
270
+ .attr('x', innerWidth / 2)
271
+ .attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))
272
+ .attr('fill', tickColor)
273
+ .attr('text-anchor', 'middle')
274
+ .style('font-size', '12px')
275
+ .style('font-weight', '700')
276
+ .text('Steps');
277
+
278
+ gAxes.append('text')
279
+ .attr('class', 'y-axis-label')
280
+ .attr('transform', 'rotate(-90)')
281
+ .attr('x', -innerHeight / 2)
282
+ .attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
283
+ .attr('fill', tickColor)
284
+ .attr('text-anchor', 'middle')
285
+ .style('font-size', '12px')
286
+ .style('font-weight', '700')
287
+ .text(axisLabelY);
288
+
289
  // Legend box (top-right)
290
  // Per-cell legend hidden; global legend is used
291
  const legendWidth = 0, legendHeight = 0;
 
302
  const isRank = /rank/i.test(metricKey); const isAverage = /average/i.test(metricKey); const isRankStrict = isRank && !isAverage;
303
  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); }); });
304
  if (!isFinite(minStep) || !isFinite(maxStep)) return;
305
+ xScale.domain([minStep, maxStep]);
306
+ if (sharedYConfig && sharedYConfig.type === 'rank_strict') {
307
+ rankTickMax = Math.max(1, Math.round(sharedYConfig.maxRank||1));
308
+ yScale.domain([rankTickMax, 1]);
309
+ isRankStrictFlag = true;
310
+ isRankMetricFlag = true;
311
+ } else if (sharedYConfig && sharedYConfig.type === 'value') {
312
+ yScale.domain([sharedYConfig.min, sharedYConfig.max]);
313
+ isRankStrictFlag = isRankStrict;
314
+ isRankMetricFlag = isRank;
315
+ } else {
316
+ if (isRank) { rankTickMax = Math.max(1, Math.round(maxVal)); yScale.domain([rankTickMax, 1]); }
317
+ else { yScale.domain([minVal, maxVal]).nice(); }
318
+ isRankStrictFlag = isRankStrict;
319
+ isRankMetricFlag = isRank;
320
+ }
321
 
322
+ axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
323
  const { innerWidth, innerHeight } = updateScales();
324
 
325
  const series = runs.map((r, i) => ({ run:r, color: pool[i % pool.length], marker: markerShapes[i % markerShapes.length], values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
 
365
  const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
366
  const hoverLine = gHover.append('line').attr('stroke','rgba(0,0,0,0.25)').attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
367
  const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);
368
+ function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);
369
+ let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${nearest}</div>`;
370
+ const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, marker:s.marker, pt }; }).filter(e => e.pt && e.pt.value!=null);
371
+ entries.sort((a,b)=> (a.pt.value - b.pt.value));
372
+ const fmt = (vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4));
373
+ entries.forEach(e => { const err = (e.pt.stderr!=null && isFinite(e.pt.stderr) && e.pt.stderr>0) ? ` ± ${fmt(e.pt.stderr)}` : ''; html += `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;">${markerSVG(e.marker, e.color)}<strong>${e.run}</strong> ${fmt(e.pt.value)}${err}</div>`; });
374
  tipInner.innerHTML = html; const offsetX=12, offsetY=12; tip.style.opacity='1'; tip.style.transform=`translate(${Math.round(mx+offsetX+margin.left)}px, ${Math.round(my+offsetY+margin.top)}px)`; }
375
+ function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }, 100); }
376
  overlay.on('mousemove', onMove).on('mouseleave', onLeave);
377
  }
378
 
 
411
  return {
412
  ready,
413
  getMetrics: () => metricList.slice(),
414
+ setMetric: (m) => { if (m) renderMetric(m); },
415
+ getYInfo: (m) => {
416
+ const key = m; const map = dataByMetric.get(key) || {}; const runs = runOrder;
417
+ let maxVal = 0, minVal = Infinity; let minStep = Infinity, maxStep = -Infinity;
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
  };
425
  }
426
 
 
460
  ctrl.innerHTML = '';
461
  const label = document.createElement('label'); label.textContent = 'Metric';
462
  const select = document.createElement('select');
463
+ metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=prettyMetricLabel(m); select.appendChild(o); });
464
  if (def) select.value = def;
465
  label.appendChild(select); ctrl.appendChild(label);
466
 
467
+ const computeAndApplySharedY = (metric) => {
468
+ try {
469
+ const infos = instances.map(i => i && typeof i.getYInfo === 'function' ? i.getYInfo(metric) : null).filter(Boolean);
470
+ if (!infos.length) return;
471
+ const anyRank = infos.some(info => info.isRank);
472
+ if (anyRank) {
473
+ const maxRank = Math.max(1, ...infos.map(info => Math.round(info.rankMax || 1)));
474
+ instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'rank_strict', maxRank, key: metric }));
475
+ } else {
476
+ const min = Math.min(...infos.map(info => info.min));
477
+ const max = Math.max(...infos.map(info => info.max));
478
+ instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'value', min, max, key: metric }));
479
+ }
480
+ } catch (_) {}
481
+ };
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
 
app/src/scripts/color-palettes.js CHANGED
@@ -61,15 +61,7 @@
61
  if (rgb) return toHex(rgb);
62
  return '#E889AB';
63
  };
64
- const getCounts = () => {
65
- const Fallback = 6;
66
- const globalCount = clamp(getIntFromCssVar('--palette-count', Fallback), 1, 12);
67
- return {
68
- categorical: clamp(getIntFromCssVar('--palette-categorical-count', globalCount), 1, 12),
69
- sequential: clamp(getIntFromCssVar('--palette-sequential-count', globalCount), 1, 12),
70
- diverging: clamp(getIntFromCssVar('--palette-diverging-count', globalCount), 1, 12),
71
- };
72
- };
73
 
74
  const generators = {
75
  categorical: (baseHex, count) => {
@@ -79,7 +71,7 @@
79
  const { C, h } = oklabToOklch(L,a,bb);
80
  const L0 = Math.min(0.85, Math.max(0.4, L));
81
  const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
82
- const total = Math.max(1, Math.min(12, count || 6));
83
  const hueStep = 360 / total;
84
  const results = [];
85
  for (let i=0;i<total;i++) { const hDeg = (h + i*hueStep) % 360; const lVar = ((i % 3) - 1) * 0.04; results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg)); }
@@ -90,7 +82,7 @@
90
  const { r, g, b } = parseHex(baseHex);
91
  const { L, a, b: bb } = rgbToOklab(r,g,b);
92
  const { C, h } = oklabToOklch(L,a,bb);
93
- const total = Math.max(1, Math.min(12, count || 6));
94
  const startL = Math.max(0.25, L - 0.18);
95
  const endL = Math.min(0.92, L + 0.18);
96
  const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
@@ -103,7 +95,7 @@
103
  const { r, g, b } = parseHex(baseHex);
104
  const baseLab = rgbToOklab(r,g,b);
105
  const baseLch = oklabToOklch(baseLab.L, baseLab.a, baseLab.b);
106
- const total = Math.max(1, Math.min(12, count || 6));
107
 
108
  // Left endpoint: EXACT primary color (no darkening)
109
  const leftLab = baseLab;
@@ -157,41 +149,14 @@
157
  }
158
  };
159
 
160
- const setCssVar = (name, value) => { try { MODE.cssRoot.style.setProperty(name, value); } catch {} };
161
- const removeCssVar = (name) => { try { MODE.cssRoot.style.removeProperty(name); } catch {} };
162
-
163
  let lastSignature = '';
164
- let lastCounts = { categorical: 0, sequential: 0, diverging: 0 };
165
 
166
  const updatePalettes = () => {
167
  const primary = getPrimaryHex();
168
- const counts = getCounts();
169
- const signature = `${primary}|${counts.categorical}|${counts.sequential}|${counts.diverging}`;
170
  if (signature === lastSignature) return;
171
-
172
- const out = {};
173
- out.categorical = generators.categorical(primary, counts.categorical);
174
- out.sequential = generators.sequential(primary, counts.sequential);
175
- out.diverging = generators.diverging(primary, counts.diverging);
176
-
177
- setCssVar('--primary-hex', primary);
178
- setCssVar('--palette-categorical-count-current', String(out.categorical.length));
179
- setCssVar('--palette-sequential-count-current', String(out.sequential.length));
180
- setCssVar('--palette-diverging-count-current', String(out.diverging.length));
181
-
182
- const applyList = (key, list, prevCount) => {
183
- for (let i=0;i<list.length;i++) setCssVar(`--palette-${key}-${i+1}`, list[i]);
184
- for (let i=list.length;i<prevCount;i++) removeCssVar(`--palette-${key}-${i+1}`);
185
- setCssVar(`--palette-${key}-json`, JSON.stringify(list));
186
- };
187
- applyList('categorical', out.categorical, lastCounts.categorical);
188
- applyList('sequential', out.sequential, lastCounts.sequential);
189
- applyList('diverging', out.diverging, lastCounts.diverging);
190
-
191
- lastCounts = { categorical: out.categorical.length, sequential: out.sequential.length, diverging: out.diverging.length };
192
  lastSignature = signature;
193
-
194
- try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary, counts, palettes: out } })); } catch {}
195
  };
196
 
197
  const bootstrap = () => {
@@ -201,11 +166,14 @@
201
  setInterval(updatePalettes, 400);
202
  window.ColorPalettes = {
203
  refresh: updatePalettes,
204
- getColors: (key) => {
205
- const count = Number(getCssVar(`--palette-${key}-count-current`)) || 0;
206
- const arr = [];
207
- for (let i=0;i<count;i++) arr.push(getCssVar(`--palette-${key}-${i+1}`));
208
- return arr.filter(Boolean);
 
 
 
209
  }
210
  };
211
  };
 
61
  if (rgb) return toHex(rgb);
62
  return '#E889AB';
63
  };
64
+ // No count management via CSS anymore; counts are passed directly to the API
 
 
 
 
 
 
 
 
65
 
66
  const generators = {
67
  categorical: (baseHex, count) => {
 
71
  const { C, h } = oklabToOklch(L,a,bb);
72
  const L0 = Math.min(0.85, Math.max(0.4, L));
73
  const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
74
+ const total = Math.max(1, Math.min(12, count || 8));
75
  const hueStep = 360 / total;
76
  const results = [];
77
  for (let i=0;i<total;i++) { const hDeg = (h + i*hueStep) % 360; const lVar = ((i % 3) - 1) * 0.04; results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg)); }
 
82
  const { r, g, b } = parseHex(baseHex);
83
  const { L, a, b: bb } = rgbToOklab(r,g,b);
84
  const { C, h } = oklabToOklch(L,a,bb);
85
+ const total = Math.max(1, Math.min(12, count || 8));
86
  const startL = Math.max(0.25, L - 0.18);
87
  const endL = Math.min(0.92, L + 0.18);
88
  const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
 
95
  const { r, g, b } = parseHex(baseHex);
96
  const baseLab = rgbToOklab(r,g,b);
97
  const baseLch = oklabToOklch(baseLab.L, baseLab.a, baseLab.b);
98
+ const total = Math.max(1, Math.min(12, count || 8));
99
 
100
  // Left endpoint: EXACT primary color (no darkening)
101
  const leftLab = baseLab;
 
149
  }
150
  };
151
 
 
 
 
152
  let lastSignature = '';
 
153
 
154
  const updatePalettes = () => {
155
  const primary = getPrimaryHex();
156
+ const signature = `${primary}`;
 
157
  if (signature === lastSignature) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  lastSignature = signature;
159
+ try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {}
 
160
  };
161
 
162
  const bootstrap = () => {
 
166
  setInterval(updatePalettes, 400);
167
  window.ColorPalettes = {
168
  refresh: updatePalettes,
169
+ getPrimary: () => getPrimaryHex(),
170
+ getColors: (key, count = 6) => {
171
+ const primary = getPrimaryHex();
172
+ const total = Math.max(1, Math.min(12, Number(count) || 6));
173
+ if (key === 'categorical') return generators.categorical(primary, total);
174
+ if (key === 'sequential') return generators.sequential(primary, total);
175
+ if (key === 'diverging') return generators.diverging(primary, total);
176
+ return [];
177
  }
178
  };
179
  };
app/src/styles/_layout.css CHANGED
@@ -66,6 +66,7 @@
66
  box-sizing: border-box;
67
  position: relative;
68
  z-index: var(--z-elevated);
 
69
  }
70
 
71
  .wide {
@@ -92,6 +93,18 @@
92
  }
93
  }
94
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  /* ------------------------------------------------------------------------- */
96
  /* Hero meta bar responsiveness */
97
  /* Two columns at collapse breakpoint, then one column below small screens */
 
66
  box-sizing: border-box;
67
  position: relative;
68
  z-index: var(--z-elevated);
69
+ background-color: var(--background-color);
70
  }
71
 
72
  .wide {
 
93
  }
94
  }
95
 
96
+ /* ============================================================================ */
97
+ /* Theme toggle placement */
98
+ /* ============================================================================ */
99
+
100
+ #theme-toggle {
101
+ position: fixed;
102
+ top: calc(var(--spacing-2) + var(--hf-spaces-topbar, 0px));
103
+ right: var(--content-padding-x);
104
+ margin: 0;
105
+ z-index: var(--z-overlay);
106
+ }
107
+
108
  /* ------------------------------------------------------------------------- */
109
  /* Hero meta bar responsiveness */
110
  /* Two columns at collapse breakpoint, then one column below small screens */
app/src/styles/_variables.css CHANGED
@@ -55,7 +55,7 @@
55
  --block-spacing-y: var(--spacing-4); /* default vertical spacing between block components */
56
 
57
  /* Config */
58
- --palette-count: 6;
59
 
60
  /* Button tokens */
61
  --button-radius: 6px;
 
55
  --block-spacing-y: var(--spacing-4); /* default vertical spacing between block components */
56
 
57
  /* Config */
58
+ --palette-count: 8;
59
 
60
  /* Button tokens */
61
  --button-radius: 6px;