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