thibaud frere commited on
Commit
f6bb916
·
1 Parent(s): 7914ed2

update palette and add trackio example

Browse files
app/.astro/astro/content.d.ts CHANGED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "vibe-coding-charts.mdx": {
205
+ id: "vibe-coding-charts.mdx";
206
+ slug: "vibe-coding-charts";
207
+ body: string;
208
+ collection: "chapters";
209
+ data: any
210
+ } & { render(): Render[".mdx"] };
211
+ "writing-your-content.mdx": {
212
+ id: "writing-your-content.mdx";
213
+ slug: "writing-your-content";
214
+ body: string;
215
+ collection: "chapters";
216
+ data: any
217
+ } & { render(): Render[".mdx"] };
218
+ };
219
+ "embeds": {
220
+ "vibe-code-d3-embeds-directives.md": {
221
+ id: "vibe-code-d3-embeds-directives.md";
222
+ slug: "vibe-code-d3-embeds-directives";
223
+ body: string;
224
+ collection: "embeds";
225
+ data: any
226
+ } & { render(): Render[".md"] };
227
+ };
228
+
229
+ };
230
+
231
+ type DataEntryMap = {
232
+ "assets": {
233
+ "data/mnist-variant-model": {
234
+ id: "data/mnist-variant-model";
235
+ collection: "assets";
236
+ data: any
237
+ };
238
+ };
239
+
240
+ };
241
+
242
+ type AnyEntryMap = ContentEntryMap & DataEntryMap;
243
+
244
+ export type ContentConfig = never;
245
+ }
app/src/components/Palettes.astro CHANGED
@@ -80,7 +80,7 @@ const rootId = `palettes-${Math.random().toString(36).slice(2)}`;
80
  </div>
81
  </div>
82
  <script type="module" is:inline>
83
- import '/src/scripts/color-palettes.js';
84
  const ROOT_ID = "{rootId}";
85
  (() => {
86
  const cards = [
 
80
  </div>
81
  </div>
82
  <script type="module" is:inline>
83
+ import '/scripts/color-palettes.js';
84
  const ROOT_ID = "{rootId}";
85
  (() => {
86
  const cards = [
app/src/content/assets/data/trackio_demo.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c3da98a9f857932dd4b39344370c1ca41b9ccfbdd0fdd43bc9752f346219605
3
+ size 9635
app/src/content/assets/data/trackio_wandb_demo.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b63be98ff7b53f4fb9c1db279348ce1d78f901cb2dce51b1c3b10bf5b3faae4d
3
+ size 6033
app/src/content/chapters/components.mdx CHANGED
@@ -237,6 +237,7 @@ You can embed external content in your article using **iframes**. For example, *
237
  <small className="muted">Gradio embed example</small>
238
  <div className="card">
239
  <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
 
240
  </div>
241
 
242
  <Accordion title="Code example">
 
237
  <small className="muted">Gradio embed example</small>
238
  <div className="card">
239
  <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
240
+ <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
241
  </div>
242
 
243
  <Accordion title="Code example">
app/src/content/chapters/vibe-coding-charts.mdx CHANGED
@@ -71,5 +71,9 @@ They can be found in the `app/src/content/embeds` folder and you can also use th
71
  ---
72
  <HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
73
  ---
74
-
 
 
 
 
75
 
 
71
  ---
72
  <HtmlEmbed src="d3-scatter.html" title="d3-scatter: 2D projection by category" desc={`Figure 8: Dataset visualization via UMAP <br/> Credit: <a href="https://huggingface.co/spaces/HuggingFaceM4/FineVision" target="_blank">FineVision</a>`} frameless align="center" />
73
  ---
74
+ <HtmlEmbed
75
+ src="d3-trackio.html"
76
+ title="Résultats TrackIO"
77
+ />
78
+ ---
79
 
app/src/content/embeds/d3-trackio.html ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="d3-trackio">
2
+ <div class="d3-trackio__grid">
3
+ <div class="cell" data-metric="epoch" data-title="epoch"></div>
4
+ <div class="cell" data-metric="train_accuracy" data-title="train_accuracy"></div>
5
+ <div class="cell" data-metric="train_loss" data-title="train_loss"></div>
6
+ <div class="cell" data-metric="val_accuracy" data-title="val_accuracy"></div>
7
+ <div class="cell cell--wide" data-metric="val_loss" data-title="val_loss"></div>
8
+ </div>
9
+ <noscript>JavaScript is required to render this chart.</noscript>
10
+ </div>
11
+ <style>
12
+ .d3-trackio { position: relative; }
13
+ .d3-trackio__grid {
14
+ display: grid;
15
+ grid-template-columns: repeat(2, minmax(0, 1fr));
16
+ gap: 12px;
17
+ }
18
+ @media (max-width: 980px) {
19
+ .d3-trackio__grid { grid-template-columns: 1fr; }
20
+ }
21
+ .d3-trackio__grid .cell--wide { grid-column: 1 / -1; }
22
+
23
+ .d3-trackio .cell {
24
+ border: 1px solid var(--border-color);
25
+ border-radius: 10px;
26
+ background: var(--surface-bg);
27
+ display: flex;
28
+ flex-direction: column;
29
+ position: relative;
30
+ }
31
+ .d3-trackio .cell-header {
32
+ padding: 8px 10px;
33
+ border-bottom: 1px solid var(--border-color);
34
+ display: flex;
35
+ align-items: center;
36
+ justify-content: space-between;
37
+ gap: 8px;
38
+ }
39
+ .d3-trackio .cell-title {
40
+ font-size: 13px;
41
+ font-weight: 700;
42
+ color: var(--text-color);
43
+ text-transform: none;
44
+ }
45
+ .d3-trackio .cell-body { position: relative; width: 100%; overflow: hidden; }
46
+ .d3-trackio .cell-body svg { max-width: 100%; height: auto; display: block; }
47
+
48
+ /* Axes/grid colors via project CSS variables */
49
+ .d3-trackio .axes path,
50
+ .d3-trackio .axes line { stroke: var(--axis-color); }
51
+ .d3-trackio .axes text { fill: var(--tick-color); }
52
+ .d3-trackio .grid line { stroke: var(--grid-color); }
53
+
54
+ /* Global header (legend) below the grid for consistency */
55
+ .d3-trackio__header {
56
+ display: flex;
57
+ align-items: flex-start;
58
+ justify-content: flex-start;
59
+ gap: 12px;
60
+ margin: 8px 0 0 0;
61
+ flex-wrap: wrap;
62
+ }
63
+ .d3-trackio__header .legend-bottom {
64
+ display: flex;
65
+ flex-direction: column;
66
+ align-items: flex-start;
67
+ gap: 6px;
68
+ font-size: 12px;
69
+ color: var(--text-color);
70
+ }
71
+ .d3-trackio__header .legend-bottom .legend-title { font-size: 12px; font-weight: 700; color: var(--text-color); }
72
+ .d3-trackio__header .legend-bottom .items { display: flex; flex-wrap: wrap; gap: 8px 14px; }
73
+ .d3-trackio__header .legend-bottom .item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
74
+ .d3-trackio__header .legend-bottom .swatch { width: 14px; height: 14px; border-radius: 3px; border: 1px solid var(--border-color); display: inline-block; }
75
+
76
+ /* Hover ghosting */
77
+ .d3-trackio.hovering .lines path.ghost { opacity: .25; }
78
+ .d3-trackio.hovering .points circle.ghost { opacity: .25; }
79
+ .d3-trackio.hovering .areas path.ghost { opacity: .08; }
80
+ .d3-trackio.hovering .legend-bottom .item.ghost { opacity: .35; }
81
+
82
+ /* Tooltip styling aligned with other embeds */
83
+ .d3-trackio .d3-tooltip {
84
+ z-index: 20;
85
+ backdrop-filter: saturate(1.12) blur(8px);
86
+ }
87
+ .d3-trackio .d3-tooltip__inner { display: flex; flex-direction: column; gap: 6px; min-width: 220px; }
88
+ .d3-trackio .d3-tooltip__inner > div:first-child { font-weight: 800; letter-spacing: 0.1px; margin-bottom: 0; }
89
+ .d3-trackio .d3-tooltip__inner > div:nth-child(2) { font-size: 11px; color: var(--muted-color); display: block; margin-top: -4px; margin-bottom: 2px; letter-spacing: 0.1px; }
90
+ .d3-trackio .d3-tooltip__inner > div:nth-child(n+3) { padding-top: 6px; border-top: 1px solid var(--border-color); }
91
+ .d3-trackio .d3-tooltip__color-dot { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--border-color); }
92
+ </style>
93
+ <script>
94
+ (() => {
95
+ const THIS_SCRIPT = document.currentScript;
96
+
97
+ const TARGET_METRICS = [
98
+ 'epoch',
99
+ 'train_accuracy',
100
+ 'train_loss',
101
+ 'val_accuracy',
102
+ 'val_loss'
103
+ ];
104
+
105
+ const prettyMetricLabel = (key) => {
106
+ if (!key) return '';
107
+ const table = {
108
+ 'train_accuracy': 'Train Accuracy',
109
+ 'val_accuracy': 'Val Accuracy',
110
+ 'train_loss': 'Train Loss',
111
+ 'val_loss': 'Val Loss',
112
+ 'epoch': 'Epoch'
113
+ };
114
+ if (table[key]) return table[key];
115
+ const cleaned = String(key).replace(/[_-]+/g, ' ').trim();
116
+ return cleaned.split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
117
+ };
118
+
119
+ const ensureD3 = (cb) => {
120
+ if (window.d3 && typeof window.d3.select === 'function') return cb();
121
+ let s = document.getElementById('d3-cdn-script');
122
+ 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); }
123
+ const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
124
+ s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
125
+ };
126
+
127
+ // Build per-cell line chart
128
+ function initCell(cell) {
129
+ const d3 = window.d3;
130
+ const metricKey = cell.getAttribute('data-metric');
131
+ const titleText = cell.getAttribute('data-title') || metricKey;
132
+
133
+ // Header
134
+ const header = document.createElement('div'); header.className = 'cell-header';
135
+ const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = prettyMetricLabel(titleText); header.appendChild(title);
136
+ cell.appendChild(header);
137
+
138
+ // Body & SVG
139
+ const body = document.createElement('div'); body.className = 'cell-body'; cell.appendChild(body);
140
+ const svg = d3.select(body).append('svg').attr('width','100%').style('display','block');
141
+ const gRoot = svg.append('g');
142
+ const gGrid = gRoot.append('g').attr('class','grid');
143
+ const gAxes = gRoot.append('g').attr('class','axes');
144
+ const gAreas = gRoot.append('g').attr('class','areas');
145
+ const gLines = gRoot.append('g').attr('class','lines');
146
+ const gPoints = gRoot.append('g').attr('class','points');
147
+ const gHover = gRoot.append('g').attr('class','hover');
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'); tip.className = 'd3-tooltip';
154
+ Object.assign(tip.style, {
155
+ position:'absolute', top:'0', left:'0', transform:'translate(-9999px,-9999px)', pointerEvents:'none',
156
+ padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',
157
+ background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity:'0', transition:'opacity .12s ease'
158
+ });
159
+ tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); cell.appendChild(tip);
160
+ } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
161
+
162
+ // Layout & scales
163
+ let width = 800, height = 200; const margin = { top: 12, right: 20, bottom: 36, left: 56 };
164
+ const xScale = d3.scaleLinear();
165
+ const yScale = d3.scaleLinear();
166
+ const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));
167
+
168
+ function updateLayout(axisLabelY){
169
+ const rect = cell.getBoundingClientRect();
170
+ width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));
171
+ // Hauteur fixe (encore plus basse) pour uniformiser les 5 graphiques
172
+ height = 200;
173
+ svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');
174
+ const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom;
175
+ gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
176
+ xScale.range([0, innerWidth]); yScale.range([innerHeight, 0]);
177
+
178
+ // Grid
179
+ gGrid.selectAll('*').remove();
180
+ gGrid.selectAll('line').data(yScale.ticks(6)).join('line')
181
+ .attr('x1',0).attr('x2',innerWidth).attr('y1',d=>yScale(d)).attr('y2',d=>yScale(d))
182
+ .attr('stroke','var(--grid-color)').attr('stroke-width',1).attr('shape-rendering','crispEdges');
183
+
184
+ // Axes
185
+ gAxes.selectAll('*').remove();
186
+ let xAxis = d3.axisBottom(xScale).tickSizeOuter(0).ticks(8).tickFormat(formatK);
187
+ let yAxis = d3.axisLeft(yScale).ticks(6);
188
+ gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','11px'); });
189
+ gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke','var(--axis-color)'); g.selectAll('text').attr('fill','var(--tick-color)').style('font-size','11px'); });
190
+
191
+ // Axis labels
192
+ gAxes.append('text')
193
+ .attr('class', 'x-axis-label')
194
+ .attr('x', innerWidth / 2)
195
+ .attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))
196
+ .attr('fill', 'var(--text-color)')
197
+ .attr('text-anchor', 'middle')
198
+ .style('font-size', '12px')
199
+ .style('font-weight', '700')
200
+ .text('Steps');
201
+ gAxes.append('text')
202
+ .attr('class', 'y-axis-label')
203
+ .attr('transform', 'rotate(-90)')
204
+ .attr('x', -innerHeight / 2)
205
+ .attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))
206
+ .attr('fill', 'var(--text-color)')
207
+ .attr('text-anchor', 'middle')
208
+ .style('font-size', '12px')
209
+ .style('font-weight', '700')
210
+ .text(axisLabelY || 'Value');
211
+
212
+ return { innerWidth, innerHeight };
213
+ }
214
+
215
+ const formatK = (v) => {
216
+ const abs = Math.abs(v);
217
+ if (abs >= 1000) { const n = v / 1000; const s = d3.format('.1f')(n); return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k'; }
218
+ return d3.format('d')(v);
219
+ };
220
+
221
+ function render(metricData, colorForRun) {
222
+ const runs = Object.keys(metricData || {});
223
+ const hasAny = runs.some(r => (metricData[r]||[]).length > 0);
224
+ if (!hasAny) {
225
+ // Show a single empty message and hide the SVG group
226
+ gRoot.style('display', 'none');
227
+ let msg = body.querySelector('.empty-msg');
228
+ if (!msg) {
229
+ msg = document.createElement('div');
230
+ msg.className = 'empty-msg';
231
+ msg.textContent = 'Metric not found in data.';
232
+ Object.assign(msg.style, { padding:'10px', fontSize:'12px', color:'var(--muted-color)' });
233
+ body.appendChild(msg);
234
+ }
235
+ return;
236
+ }
237
+ // Ensure message is removed if previously shown
238
+ const msg = body.querySelector('.empty-msg'); if (msg) msg.remove();
239
+ gRoot.style('display', null);
240
+
241
+ let minStep = Infinity, maxStep = -Infinity, minVal = Infinity, maxVal = -Infinity;
242
+ runs.forEach(r => { (metricData[r]||[]).forEach(pt => { minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); minVal = Math.min(minVal, pt.value); maxVal = Math.max(maxVal, pt.value); }); });
243
+ const isAccuracy = /accuracy/i.test(metricKey);
244
+ const axisLabelY = prettyMetricLabel(metricKey);
245
+ xScale.domain([minStep, maxStep]);
246
+ if (isAccuracy) yScale.domain([0, 1]).nice(); else yScale.domain([minVal, maxVal]).nice();
247
+
248
+ const { innerWidth, innerHeight } = updateLayout(axisLabelY);
249
+
250
+ // Vertical grid lines at each available step (same visibility as horizontal)
251
+ const stepSetForGrid = new Set();
252
+ runs.forEach(r => { (metricData[r]||[]).forEach(pt => stepSetForGrid.add(pt.step)); });
253
+ const stepsForGrid = Array.from(stepSetForGrid).sort((a,b)=>a-b);
254
+ gGrid.selectAll('line.vstep')
255
+ .data(stepsForGrid)
256
+ .join(
257
+ enter => enter.append('line').attr('class','vstep')
258
+ .attr('y1', 0).attr('y2', innerHeight)
259
+ .attr('x1', d => xScale(d)).attr('x2', d => xScale(d))
260
+ .attr('stroke','var(--grid-color)').attr('stroke-width',1).attr('shape-rendering','crispEdges'),
261
+ update => update
262
+ .attr('y1', 0).attr('y2', innerHeight)
263
+ .attr('x1', d => xScale(d)).attr('x2', d => xScale(d)),
264
+ exit => exit.remove()
265
+ );
266
+
267
+ // Shaded areas from stderr if available
268
+ gAreas.selectAll('*').remove();
269
+ runs.forEach(r => {
270
+ const vals = (metricData[r]||[]).slice().sort((a,b)=>a.step-b.step);
271
+ const withErr = vals.filter(v => v && v.stderr != null && isFinite(v.stderr) && v.stderr > 0 && isFinite(v.value));
272
+ if (!withErr.length) return;
273
+ const upper = withErr.map(d => [xScale(d.step), yScale(d.value + d.stderr)]);
274
+ const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);
275
+ const coords = upper.concat(lower);
276
+ const pathData = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed)(coords);
277
+ gAreas.append('path')
278
+ .attr('class','area')
279
+ .attr('data-run', r)
280
+ .attr('d', pathData)
281
+ .attr('fill', colorForRun(r))
282
+ .attr('opacity', 0.15)
283
+ .attr('stroke', 'none');
284
+ });
285
+
286
+ // Lines
287
+ const series = runs.map(r => ({ run: r, color: colorForRun(r), values: (metricData[r]||[]).slice().sort((a,b)=>a.step-b.step) }));
288
+ const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);
289
+ paths.enter().append('path').attr('class','run-line').attr('data-run', d=>d.run).attr('fill','none').attr('stroke-width', 1.5).attr('opacity', 0.9)
290
+ .attr('stroke', d=>d.color).attr('d', d=>lineGen(d.values));
291
+ paths.transition().duration(260).attr('stroke', d=>d.color).attr('opacity',0.9).attr('d', d=>lineGen(d.values));
292
+ paths.exit().remove();
293
+
294
+ // Points
295
+ const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));
296
+ const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
297
+ ptsSel.enter().append('circle').attr('class','pt').attr('data-run', d=>d.run).attr('r', 2).attr('fill', d=>d.color).attr('fill-opacity', 0.6)
298
+ .attr('stroke', 'none').attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value))
299
+ .merge(ptsSel)
300
+ .transition().duration(150)
301
+ .attr('cx', d=>xScale(d.step)).attr('cy', d=>yScale(d.value));
302
+ ptsSel.exit().remove();
303
+
304
+ // Hover
305
+ gHover.selectAll('*').remove();
306
+ const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);
307
+ const hoverLine = gHover.append('line').style('stroke','var(--text-color)').attr('stroke-opacity', 0.25).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');
308
+ 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);
309
+ 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);
310
+ let html = `<div><strong>${prettyMetricLabel(metricKey)}</strong></div><div><strong>step</strong> ${formatK(nearest)}</div>`;
311
+ const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null);
312
+ entries.sort((a,b)=> (a.pt.value - b.pt.value));
313
+ const fmt = (vv)=> (isAccuracy? (+vv).toFixed(4) : (+vv).toFixed(4));
314
+ entries.forEach(e => {
315
+ const err = (e.pt.stderr!=null && isFinite(e.pt.stderr) && e.pt.stderr>0) ? ` ± ${fmt(e.pt.stderr)}` : '';
316
+ html += `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}${err}</span></div>`;
317
+ });
318
+ 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)`; }
319
+ function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }, 100); }
320
+ overlay.on('mousemove', onMove).on('mouseleave', onLeave);
321
+ }
322
+
323
+ return { metricKey, render };
324
+ }
325
+
326
+ const bootstrap = () => {
327
+ const scriptEl = THIS_SCRIPT;
328
+ // Locate host
329
+ let host = null;
330
+ if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {
331
+ host = scriptEl.parentElement.querySelector('.d3-trackio');
332
+ }
333
+ if (!host) {
334
+ let sib = scriptEl && scriptEl.previousElementSibling;
335
+ while (sib && !(sib.classList && sib.classList.contains('d3-trackio'))) { sib = sib.previousElementSibling; }
336
+ host = sib || null;
337
+ }
338
+ if (!host) { host = document.querySelector('.d3-trackio'); }
339
+ if (!host) return;
340
+ if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';
341
+
342
+ // Build global header (legend)
343
+ const header = document.createElement('div'); header.className = 'd3-trackio__header';
344
+ const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>';
345
+ header.appendChild(legend);
346
+ host.appendChild(header);
347
+
348
+ const cells = Array.from(host.querySelectorAll('.cell'));
349
+ if (!cells.length) return;
350
+
351
+ const instances = cells.map(cell => initCell(cell));
352
+
353
+ // Read HtmlEmbed attributes from closest ancestor carrying them
354
+ let mountEl = host;
355
+ while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
356
+ mountEl = mountEl.parentElement;
357
+ }
358
+ let providedData = null; let providedConfig = null;
359
+ try { const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null; if (attr && attr.trim()) { providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim(); } } catch(_) {}
360
+ try { const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; if (cfg && cfg.trim()) { providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; } } catch(_) {}
361
+
362
+ const DEFAULT_CSV = '/data/trackio_wandb_demo.csv';
363
+ const ensureDataPrefix = (p) => { if (typeof p !== 'string' || !p) return p; return p.includes('/') ? p : `/data/${p}`; };
364
+ const normalizeInput = (inp) => Array.isArray(inp) ? inp.map(ensureDataPrefix) : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
365
+ const CSV_PATHS = Array.isArray(providedData)
366
+ ? normalizeInput(providedData)
367
+ : (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [
368
+ DEFAULT_CSV,
369
+ './assets/data/formatting_filters.csv',
370
+ '../assets/data/formatting_filters.csv',
371
+ '../../assets/data/formatting_filters.csv'
372
+ ]);
373
+ const fetchFirstAvailable = async (paths) => { for (const p of paths) { try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(_){} } throw new Error('CSV not found'); };
374
+
375
+ const d3 = window.d3;
376
+ (async () => {
377
+ try {
378
+ // Load one or many CSVs and merge rows
379
+ let textAll = '';
380
+ if (Array.isArray(CSV_PATHS) && CSV_PATHS.length > 1) {
381
+ const texts = await Promise.all(CSV_PATHS.map(p => fetch(p, { cache:'no-cache' }).then(r => r.ok ? r.text() : '').catch(()=>'')));
382
+ textAll = texts.filter(Boolean).join('\n');
383
+ } else {
384
+ textAll = await fetchFirstAvailable(CSV_PATHS);
385
+ }
386
+ const rows = d3.csvParse(textAll, d => ({
387
+ run:(d.run||'').trim(),
388
+ step:+d.step,
389
+ metric:(d.metric||'').trim(),
390
+ value:+d.value,
391
+ stderr: (d.stderr!=null && d.stderr!=='') ? +d.stderr : null
392
+ }));
393
+
394
+ // Filter to target metrics if present in data, with synonym resolution
395
+ const metricsInData = Array.from(new Set(rows.map(r => r.metric)));
396
+ const lcSet = new Set(metricsInData.map(m => m.toLowerCase()));
397
+ const preferIfExists = (cand) => cand.find(c => lcSet.has(String(c).toLowerCase())) || null;
398
+ const resolveMetric = (target) => {
399
+ // Config override
400
+ const override = providedConfig && providedConfig.metricMap && providedConfig.metricMap[target];
401
+ if (override && lcSet.has(String(override).toLowerCase())) return metricsInData.find(m => m.toLowerCase() === String(override).toLowerCase());
402
+ // Exact
403
+ const exact = metricsInData.find(m => m.toLowerCase() === target.toLowerCase()); if (exact) return exact;
404
+ // Heuristics by target
405
+ const cands = (name) => metricsInData.filter(m => m.toLowerCase().includes(name));
406
+ if (target === 'epoch') return preferIfExists(['epoch']);
407
+ if (target === 'train_accuracy') return preferIfExists([
408
+ 'train_accuracy','training_accuracy','accuracy_train','train_acc','acc_train','train/accuracy','accuracy'
409
+ ]) || cands('acc').find(m => /train|trn/i.test(m));
410
+ if (target === 'val_accuracy') return preferIfExists([
411
+ 'val_accuracy','valid_accuracy','validation_accuracy','val_acc','acc_val','val/accuracy'
412
+ ]) || cands('acc').find(m => /val|valid/i.test(m));
413
+ if (target === 'train_loss') return preferIfExists([
414
+ 'train_loss','training_loss','loss_train','train/loss','loss'
415
+ ]) || cands('loss').find(m => /train|trn/i.test(m));
416
+ if (target === 'val_loss') return preferIfExists([
417
+ 'val_loss','validation_loss','valid_loss','loss_val','val/loss'
418
+ ]) || cands('loss').find(m => /val|valid/i.test(m));
419
+ return null;
420
+ };
421
+ const TARGET_TO_DATA = Object.fromEntries(TARGET_METRICS.map(t => [t, resolveMetric(t)]));
422
+ const metricsToDraw = TARGET_METRICS.filter(t => !!TARGET_TO_DATA[t]);
423
+
424
+ // Build run list and shared color mapping
425
+ const runList = Array.from(new Set(rows.map(r => String(r.run||'').trim()).filter(Boolean))).sort();
426
+ let palette = null;
427
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') palette = window.ColorPalettes.getColors('categorical', runList.length); } catch(_) {}
428
+ if (!palette) {
429
+ const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
430
+ palette = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
431
+ }
432
+ const colorForRun = (name) => palette[runList.indexOf(name) % palette.length];
433
+
434
+ // Populate legend
435
+ const legendItemsHost = legend.querySelector('.items');
436
+ legendItemsHost.innerHTML = runList.map((name) => {
437
+ const color = colorForRun(name);
438
+ return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
439
+ }).join('');
440
+
441
+ // Build per-metric data map (using resolved names)
442
+ const dataByMetric = new Map();
443
+ metricsToDraw.forEach(tgt => {
444
+ const m = TARGET_TO_DATA[tgt];
445
+ const map = {}; runList.forEach(r => map[r] = []);
446
+ rows.filter(r=>r.metric===m).forEach(r => {
447
+ if (!isNaN(r.step) && !isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr });
448
+ });
449
+ dataByMetric.set(tgt, map);
450
+ });
451
+
452
+ // Render each cell
453
+ instances.forEach(inst => {
454
+ const metricMap = dataByMetric.get(inst.metricKey) || {};
455
+ inst.render(metricMap, colorForRun);
456
+ });
457
+
458
+ // Resize handling
459
+ const rerender = () => { instances.forEach(inst => inst.render(dataByMetric.get(inst.metricKey)||{}, colorForRun)); };
460
+ if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(host); } else { window.addEventListener('resize', rerender); }
461
+
462
+ // Legend hover ghosting across all cells
463
+ legendItemsHost.querySelectorAll('.item').forEach(el => {
464
+ el.addEventListener('mouseenter', () => {
465
+ const run = el.getAttribute('data-run'); if (!run) return;
466
+ host.classList.add('hovering');
467
+ host.querySelectorAll('.cell').forEach(cell => {
468
+ cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));
469
+ cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));
470
+ cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run));
471
+ });
472
+ legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));
473
+ });
474
+ el.addEventListener('mouseleave', () => {
475
+ host.classList.remove('hovering');
476
+ host.querySelectorAll('.cell').forEach(cell => {
477
+ cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost'));
478
+ cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost'));
479
+ cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost'));
480
+ });
481
+ legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
482
+ });
483
+ });
484
+ } catch (e) {
485
+ const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);
486
+ pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; host.appendChild(pre);
487
+ }
488
+ })();
489
+ };
490
+
491
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
492
+ })();
493
+ </script>
494
+
495
+
app/src/pages/index.astro CHANGED
@@ -143,7 +143,7 @@ const licence = (articleFM as any)?.licence ?? (articleFM as any)?.license ?? (a
143
  } catch {}
144
  })();
145
  </script>
146
- <script type="module" src="/src/scripts/color-palettes.js"></script>
147
 
148
  <!-- TO MANAGE PROPERLY -->
149
  <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
 
143
  } catch {}
144
  })();
145
  </script>
146
+ <script type="module" src="/scripts/color-palettes.js"></script>
147
 
148
  <!-- TO MANAGE PROPERLY -->
149
  <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
app/src/scripts/color-palettes.js DELETED
@@ -1,236 +0,0 @@
1
- // Global color palettes generator and watcher
2
- // - Observes CSS variable --primary-color and theme changes
3
- // - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab)
4
- // - Exposes results as CSS variables on :root
5
- // - Supports variable color counts per palette via CSS vars
6
- // - Dispatches a 'palettes:updated' CustomEvent after each update
7
-
8
- (() => {
9
- const MODE = { cssRoot: document.documentElement };
10
-
11
- const getCssVar = (name) => {
12
- try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
13
- };
14
- const getIntFromCssVar = (name, fallback) => {
15
- const raw = getCssVar(name);
16
- if (!raw) return fallback;
17
- const v = parseInt(String(raw), 10);
18
- if (Number.isNaN(v)) return fallback;
19
- return v;
20
- };
21
- const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
22
-
23
- // Color math (OKLab/OKLCH)
24
- const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4));
25
- const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055);
26
- const rgbToOklab = (r, g, b) => {
27
- const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b);
28
- const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl);
29
- const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl);
30
- const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl);
31
- const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
32
- const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
33
- const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
34
- return { L, a, b: b2 };
35
- };
36
- const oklabToRgb = (L, a, b) => {
37
- const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
38
- const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
39
- const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
40
- const l = l_ * l_ * l_;
41
- const m = m_ * m_ * m_;
42
- const s = s_ * s_ * s_;
43
- const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
44
- const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
45
- const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
46
- return { r, g, b: b3 };
47
- };
48
- const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
49
- const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a*a + b*b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
50
- const clamp01 = (x) => Math.min(1, Math.max(0, x));
51
- const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
52
- const toHex = ({ r, g, b }) => { const R = Math.round(clamp01(r)*255), G = Math.round(clamp01(g)*255), B = Math.round(clamp01(b)*255); const h = (n) => n.toString(16).padStart(2,'0'); return `#${h(R)}${h(G)}${h(B)}`.toUpperCase(); };
53
- const oklchToHexSafe = (L, C, h) => { let c = C; for (let i=0;i<12;i++){ const { a, b } = oklchToOklab(L,c,h); const rgb = oklabToRgb(L,a,b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c-0.02);} return toHex(oklabToRgb(L,0,0)); };
54
- const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1])/255, g: Number(m[2])/255, b: Number(m[3])/255 }; } catch { return null; } };
55
-
56
- const getPrimaryHex = () => {
57
- const css = getCssVar('--primary-color');
58
- if (!css) return '#E889AB';
59
- if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(css)) return css.toUpperCase();
60
- const rgb = parseCssColorToRgb(css);
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) => {
68
- const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
69
- const { r, g, b } = parseHex(baseHex);
70
- const { L, a, b: bb } = rgbToOklab(r,g,b);
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)); }
78
- return results;
79
- },
80
- sequential: (baseHex, count) => {
81
- const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
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));
89
- const out = [];
90
- for (let i=0;i<total;i++) { const t = total===1 ? 0 : i/(total-1); const lNow = startL*(1-t)+endL*t; const cNow = cBase*(0.85 + 0.15*(1 - Math.abs(0.5 - t)*2)); out.push(oklchToHexSafe(lNow, cNow, h)); }
91
- return out;
92
- },
93
- diverging: (baseHex, count) => {
94
- const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; };
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;
102
- // Right endpoint: complement with same L and similar C (clamped safe)
103
- const compH = (baseLch.h + 180) % 360;
104
- const cSafe = Math.min(0.35, Math.max(0.08, baseLch.C));
105
- const rightLab = oklchToOklab(baseLab.L, cSafe, compH);
106
- const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
107
-
108
- const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
109
- const lerp = (a, b, t) => a + (b - a) * t;
110
- const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t));
111
-
112
- const out = [];
113
- if (total % 2 === 1) {
114
- const nSide = (total - 1) >> 1; // items on each side
115
- // Left side: include left endpoint exactly at index 0
116
- for (let i = 0; i < nSide; i++) {
117
- const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
118
- // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter
119
- const tt = t * 0.9; // keep some distance from pure white before center
120
- out.push(lerpOKLabHex(leftLab, whiteLab, tt));
121
- }
122
- // Center
123
- out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b));
124
- // Right side: start near white and end EXACTLY at rightLab
125
- for (let i = 0; i < nSide; i++) {
126
- const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
127
- const tt = Math.max(0.1, t); // avoid starting at pure white
128
- out.push(lerpOKLabHex(whiteLab, rightLab, tt));
129
- }
130
- // Ensure first and last are exact endpoints
131
- if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
132
- } else {
133
- const nSide = total >> 1;
134
- // Left half including left endpoint, approaching white but not reaching it
135
- for (let i = 0; i < nSide; i++) {
136
- const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
137
- const tt = t * 0.9;
138
- out.push(lerpOKLabHex(leftLab, whiteLab, tt));
139
- }
140
- // Right half: mirror from near white to exact right endpoint
141
- for (let i = 0; i < nSide; i++) {
142
- const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
143
- const tt = Math.max(0.1, t);
144
- out.push(lerpOKLabHex(whiteLab, rightLab, tt));
145
- }
146
- if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
147
- }
148
- return out;
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 = () => {
163
- updatePalettes();
164
- const mo = new MutationObserver(() => updatePalettes());
165
- mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
166
- setInterval(updatePalettes, 400);
167
- // Utility: choose high-contrast (or softened) text style against an arbitrary background color
168
- const pickTextStyleForBackground = (bgCss, opts = {}) => {
169
- const cssRoot = document.documentElement;
170
- const getCssVar = (name) => {
171
- try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
172
- };
173
- const resolveCssToRgb01 = (css) => {
174
- const rgb = parseCssColorToRgb(css);
175
- if (!rgb) return null;
176
- return rgb; // already 0..1
177
- };
178
- const mixRgb01 = (a, b, t) => ({ r: a.r*(1-t)+b.r*t, g: a.g*(1-t)+b.g*t, b: a.b*(1-t)+b.b*t });
179
- const relLum = (rgb) => {
180
- const f = (u) => srgbToLinear(u);
181
- return 0.2126*f(rgb.r) + 0.7152*f(rgb.g) + 0.0722*f(rgb.b);
182
- };
183
- const contrast = (fg, bg) => {
184
- const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1,L2), b = Math.min(L1,L2);
185
- return (a + 0.05) / (b + 0.05);
186
- };
187
- try {
188
- const bg = resolveCssToRgb01(bgCss);
189
- if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
190
- const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
191
- const candidates = candidatesCss
192
- .map(css => ({ css, rgb: resolveCssToRgb01(css) }))
193
- .filter(x => !!x.rgb);
194
- // Pick the max contrast
195
- let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
196
- for (let i=1;i<candidates.length;i++){
197
- const cr = contrast(candidates[i].rgb, bg);
198
- if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
199
- }
200
- // Optional softening via blend factor (0..1), blending towards muted color
201
- const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
202
- let finalRgb = best.rgb;
203
- if (blend > 0) {
204
- const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
205
- const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
206
- finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
207
- }
208
- const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
209
- const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40*haloStrength})` : `rgba(0,0,0,${0.30 + 0.30*haloStrength})`;
210
- return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
211
- } catch {
212
- return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
213
- }
214
- };
215
- window.ColorPalettes = {
216
- refresh: updatePalettes,
217
- notify: () => { try { const primary = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {} },
218
- getPrimary: () => getPrimaryHex(),
219
- getColors: (key, count = 6) => {
220
- const primary = getPrimaryHex();
221
- const total = Math.max(1, Math.min(12, Number(count) || 6));
222
- if (key === 'categorical') return generators.categorical(primary, total);
223
- if (key === 'sequential') return generators.sequential(primary, total);
224
- if (key === 'diverging') return generators.diverging(primary, total);
225
- return [];
226
- },
227
- getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
228
- chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
229
- };
230
- };
231
-
232
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
233
- else bootstrap();
234
- })();
235
-
236
-