Spaces:
Running
Running
thibaud frere
commited on
Commit
·
f6bb916
1
Parent(s):
7914ed2
update palette and add trackio example
Browse files- app/.astro/astro/content.d.ts +245 -0
- app/src/components/Palettes.astro +1 -1
- app/src/content/assets/data/trackio_demo.csv +3 -0
- app/src/content/assets/data/trackio_wandb_demo.csv +3 -0
- app/src/content/chapters/components.mdx +1 -0
- app/src/content/chapters/vibe-coding-charts.mdx +5 -1
- app/src/content/embeds/d3-trackio.html +495 -0
- app/src/pages/index.astro +1 -1
- app/src/scripts/color-palettes.js +0 -236
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 '/
|
| 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="/
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|