Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	
		thibaud frere
		
	commited on
		
		
					Commit 
							
							·
						
						d61f156
	
1
								Parent(s):
							
							b6281fd
								
update
Browse files- app/.astro/astro/content.d.ts +0 -204
- app/package.json +0 -0
- app/public/data/against_baselines.csv +0 -1
- app/public/data/finevision.csv +0 -1
- app/src/content/assets/data/mnist-variant-model.json +3 -0
- app/src/content/chapters/available-blocks.mdx +22 -12
- app/src/content/embeds/d3-line.html +10 -12
- app/src/content/embeds/d3-neural.html +571 -0
- app/src/content/embeds/d3-scatter.html +160 -0
- app/src/content/embeds/palettes.html +78 -35
- app/src/env.d.ts +0 -9
    	
        app/.astro/astro/content.d.ts
    CHANGED
    
    | @@ -1,204 +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 | 
            -
            "available-blocks.mdx": {
         | 
| 156 | 
            -
            	id: "available-blocks.mdx";
         | 
| 157 | 
            -
              slug: "available-blocks";
         | 
| 158 | 
            -
              body: string;
         | 
| 159 | 
            -
              collection: "chapters";
         | 
| 160 | 
            -
              data: any
         | 
| 161 | 
            -
            } & { render(): Render[".mdx"] };
         | 
| 162 | 
            -
            "best-pratices.mdx": {
         | 
| 163 | 
            -
            	id: "best-pratices.mdx";
         | 
| 164 | 
            -
              slug: "best-pratices";
         | 
| 165 | 
            -
              body: string;
         | 
| 166 | 
            -
              collection: "chapters";
         | 
| 167 | 
            -
              data: any
         | 
| 168 | 
            -
            } & { render(): Render[".mdx"] };
         | 
| 169 | 
            -
            "getting-started.mdx": {
         | 
| 170 | 
            -
            	id: "getting-started.mdx";
         | 
| 171 | 
            -
              slug: "getting-started";
         | 
| 172 | 
            -
              body: string;
         | 
| 173 | 
            -
              collection: "chapters";
         | 
| 174 | 
            -
              data: any
         | 
| 175 | 
            -
            } & { render(): Render[".mdx"] };
         | 
| 176 | 
            -
            "writing-your-content.mdx": {
         | 
| 177 | 
            -
            	id: "writing-your-content.mdx";
         | 
| 178 | 
            -
              slug: "writing-your-content";
         | 
| 179 | 
            -
              body: string;
         | 
| 180 | 
            -
              collection: "chapters";
         | 
| 181 | 
            -
              data: any
         | 
| 182 | 
            -
            } & { render(): Render[".mdx"] };
         | 
| 183 | 
            -
            };
         | 
| 184 | 
            -
             | 
| 185 | 
            -
            	};
         | 
| 186 | 
            -
             | 
| 187 | 
            -
            	type DataEntryMap = {
         | 
| 188 | 
            -
            		"assets": Record<string, {
         | 
| 189 | 
            -
              id: string;
         | 
| 190 | 
            -
              collection: "assets";
         | 
| 191 | 
            -
              data: any;
         | 
| 192 | 
            -
            }>;
         | 
| 193 | 
            -
            "embeds": Record<string, {
         | 
| 194 | 
            -
              id: string;
         | 
| 195 | 
            -
              collection: "embeds";
         | 
| 196 | 
            -
              data: any;
         | 
| 197 | 
            -
            }>;
         | 
| 198 | 
            -
             | 
| 199 | 
            -
            	};
         | 
| 200 | 
            -
             | 
| 201 | 
            -
            	type AnyEntryMap = ContentEntryMap & DataEntryMap;
         | 
| 202 | 
            -
             | 
| 203 | 
            -
            	export type ContentConfig = never;
         | 
| 204 | 
            -
            }
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
    	
        app/package.json
    CHANGED
    
    | Binary files a/app/package.json and b/app/package.json differ | 
|  | 
    	
        app/public/data/against_baselines.csv
    DELETED
    
    | @@ -1 +0,0 @@ | |
| 1 | 
            -
            ../../src/content/assets/data/against_baselines.csv
         | 
|  | |
|  | 
    	
        app/public/data/finevision.csv
    DELETED
    
    | @@ -1 +0,0 @@ | |
| 1 | 
            -
            ../../src/content/assets/data/finevision.csv
         | 
|  | |
|  | 
    	
        app/src/content/assets/data/mnist-variant-model.json
    ADDED
    
    | @@ -0,0 +1,3 @@ | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            version https://git-lfs.github.com/spec/v1
         | 
| 2 | 
            +
            oid sha256:7dca86e85be46c1fca6a4e2503786e88e3f8d4609fb7284c8a1479620a5827da
         | 
| 3 | 
            +
            size 4315
         | 
    	
        app/src/content/chapters/available-blocks.mdx
    CHANGED
    
    | @@ -328,35 +328,45 @@ Props (optional) | |
| 328 | 
             
            - `frameless`: removes the card background and border for seamless embeds.
         | 
| 329 | 
             
            - `align`: aligns the title/description text. One of `left` (default), `center`, `right`.
         | 
| 330 |  | 
|  | |
| 331 | 
             
            <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
         | 
| 332 | 
             
            ---
         | 
| 333 | 
            -
            <HtmlEmbed
         | 
| 334 | 
            -
             | 
| 335 | 
            -
             | 
| 336 | 
            -
             | 
| 337 | 
            -
             | 
| 338 | 
             
            ---
         | 
| 339 | 
             
            <FullWidth>
         | 
| 340 | 
            -
            <HtmlEmbed
         | 
| 341 | 
            -
            src="d3-pie.html"
         | 
| 342 | 
            -
            title="Category distribution (4 metrics)"
         | 
| 343 | 
            -
            desc="Pie charts by category"
         | 
| 344 | 
            -
            align="center"
         | 
| 345 | 
            -
            />
         | 
| 346 | 
             
            </FullWidth>
         | 
| 347 | 
             
            ---
         | 
| 348 | 
             
            <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
         | 
| 349 |  | 
|  | |
|  | |
|  | |
| 350 | 
             
            <small className="muted">Example</small>
         | 
| 351 | 
             
            ```mdx
         | 
| 352 | 
             
            import HtmlEmbed from '../components/HtmlEmbed.astro'
         | 
| 353 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 354 | 
             
            <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
         | 
| 355 | 
             
            ```
         | 
| 356 |  | 
| 357 | 
             
            ### Iframes
         | 
| 358 |  | 
| 359 | 
            -
            You can embed external content in your article using **iframes**. For example, **TrackIO or  | 
| 360 |  | 
| 361 | 
             
            <small className="muted">Github code embed</small>
         | 
| 362 | 
             
            <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
         | 
|  | |
| 328 | 
             
            - `frameless`: removes the card background and border for seamless embeds.
         | 
| 329 | 
             
            - `align`: aligns the title/description text. One of `left` (default), `center`, `right`.
         | 
| 330 |  | 
| 331 | 
            +
            {/* <HtmlEmbed src="d3-scatter.html" frameless title="" desc="" /> */}
         | 
| 332 | 
             
            <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
         | 
| 333 | 
             
            ---
         | 
| 334 | 
            +
            <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>`}/>
         | 
| 335 | 
            +
            ---
         | 
| 336 | 
            +
            <Wide>
         | 
| 337 | 
            +
              <HtmlEmbed src="d3-neural.html" title="D3 Interactive neural network (MNIST-like)" desc="Draw a digit and visualize activations and class probabilities (0–9)." align="center" />
         | 
| 338 | 
            +
            </Wide>
         | 
| 339 | 
             
            ---
         | 
| 340 | 
             
            <FullWidth>
         | 
| 341 | 
            +
              <HtmlEmbed src="d3-pie.html" desc="D3 Pie charts by category" align="center" />
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 342 | 
             
            </FullWidth>
         | 
| 343 | 
             
            ---
         | 
| 344 | 
             
            <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
         | 
| 345 |  | 
| 346 | 
            +
            <br/><br/>
         | 
| 347 | 
            +
            Here are some examples of the two **libraries** in the template
         | 
| 348 | 
            +
             | 
| 349 | 
             
            <small className="muted">Example</small>
         | 
| 350 | 
             
            ```mdx
         | 
| 351 | 
             
            import HtmlEmbed from '../components/HtmlEmbed.astro'
         | 
| 352 |  | 
| 353 | 
            +
            <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
         | 
| 354 | 
            +
            <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>`}/>
         | 
| 355 | 
            +
             | 
| 356 | 
            +
            <Wide>
         | 
| 357 | 
            +
              <HtmlEmbed src="d3-neural.html" title="D3 Interactive neural network (MNIST-like)" desc="Draw a digit and visualize activations and class probabilities (0–9)." align="center" />
         | 
| 358 | 
            +
            </Wide>
         | 
| 359 | 
            +
             | 
| 360 | 
            +
            <FullWidth>
         | 
| 361 | 
            +
              <HtmlEmbed src="d3-pie.html" desc="D3 Pie charts by category" align="center" />
         | 
| 362 | 
            +
            </FullWidth>
         | 
| 363 | 
            +
             | 
| 364 | 
             
            <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
         | 
| 365 | 
             
            ```
         | 
| 366 |  | 
| 367 | 
             
            ### Iframes
         | 
| 368 |  | 
| 369 | 
            +
            You can embed external content in your article using **iframes**. For example, **TrackIO**, **Gradio** or even **Github code embeds** can be used this way.
         | 
| 370 |  | 
| 371 | 
             
            <small className="muted">Github code embed</small>
         | 
| 372 | 
             
            <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
         | 
    	
        app/src/content/embeds/d3-line.html
    CHANGED
    
    | @@ -232,25 +232,20 @@ | |
| 232 | 
             
                    xScale.range([0, innerWidth]);
         | 
| 233 | 
             
                    yScale.range([innerHeight, 0]);
         | 
| 234 |  | 
| 235 | 
            -
                    // Compute  | 
| 236 | 
            -
                    let  | 
| 237 | 
             
                    if (isRankStrictFlag) {
         | 
| 238 | 
             
                      const maxR = Math.max(1, Math.round(rankTickMax));
         | 
| 239 | 
            -
                      for (let v = 1; v <= maxR; v += 1)  | 
| 240 | 
             
                    } else {
         | 
| 241 | 
            -
                       | 
| 242 | 
            -
                       | 
| 243 | 
            -
                      const yMax = Math.max(yDomain[0], yDomain[1]);
         | 
| 244 | 
            -
                      let yStep = Math.max(1, Math.round((yMax - yMin) / 6));
         | 
| 245 | 
            -
                      if (!isFinite(yStep) || yStep <= 0) yStep = 1;
         | 
| 246 | 
            -
                      for (let v = Math.ceil(yMin); v <= Math.floor(yMax); v += yStep) { yIntTicks.push(v); }
         | 
| 247 | 
            -
                      if (yIntTicks.length === 0) { yIntTicks = [Math.round(yMin), Math.round(yMax)]; }
         | 
| 248 | 
             
                    }
         | 
| 249 |  | 
| 250 | 
             
                    // Grid (horizontal)
         | 
| 251 | 
             
                    gGrid.selectAll('*').remove();
         | 
| 252 | 
             
                    gGrid.selectAll('line')
         | 
| 253 | 
            -
                      .data( | 
| 254 | 
             
                      .join('line')
         | 
| 255 | 
             
                      .attr('x1', 0)
         | 
| 256 | 
             
                      .attr('x2', innerWidth)
         | 
| @@ -274,7 +269,10 @@ | |
| 274 | 
             
                    } else {
         | 
| 275 | 
             
                      xAxis = xAxis.ticks(8);
         | 
| 276 | 
             
                    }
         | 
| 277 | 
            -
                    const yAxis = d3.axisLeft(yScale) | 
|  | |
|  | |
|  | |
| 278 | 
             
                    gAxes.append('g')
         | 
| 279 | 
             
                      .attr('transform', `translate(0,${innerHeight})`)
         | 
| 280 | 
             
                      .call(xAxis)
         | 
|  | |
| 232 | 
             
                    xScale.range([0, innerWidth]);
         | 
| 233 | 
             
                    yScale.range([innerHeight, 0]);
         | 
| 234 |  | 
| 235 | 
            +
                    // Compute Y ticks
         | 
| 236 | 
            +
                    let yTicks = [];
         | 
| 237 | 
             
                    if (isRankStrictFlag) {
         | 
| 238 | 
             
                      const maxR = Math.max(1, Math.round(rankTickMax));
         | 
| 239 | 
            +
                      for (let v = 1; v <= maxR; v += 1) yTicks.push(v);
         | 
| 240 | 
             
                    } else {
         | 
| 241 | 
            +
                      // Use D3's tick generator to produce nice floating-point ticks
         | 
| 242 | 
            +
                      yTicks = yScale.ticks(6);
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 243 | 
             
                    }
         | 
| 244 |  | 
| 245 | 
             
                    // Grid (horizontal)
         | 
| 246 | 
             
                    gGrid.selectAll('*').remove();
         | 
| 247 | 
             
                    gGrid.selectAll('line')
         | 
| 248 | 
            +
                      .data(yTicks)
         | 
| 249 | 
             
                      .join('line')
         | 
| 250 | 
             
                      .attr('x1', 0)
         | 
| 251 | 
             
                      .attr('x2', innerWidth)
         | 
|  | |
| 269 | 
             
                    } else {
         | 
| 270 | 
             
                      xAxis = xAxis.ticks(8);
         | 
| 271 | 
             
                    }
         | 
| 272 | 
            +
                    const yAxis = d3.axisLeft(yScale)
         | 
| 273 | 
            +
                      .tickValues(yTicks)
         | 
| 274 | 
            +
                      .tickSizeOuter(0)
         | 
| 275 | 
            +
                      .tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
         | 
| 276 | 
             
                    gAxes.append('g')
         | 
| 277 | 
             
                      .attr('transform', `translate(0,${innerHeight})`)
         | 
| 278 | 
             
                      .call(xAxis)
         | 
    	
        app/src/content/embeds/d3-neural.html
    ADDED
    
    | @@ -0,0 +1,571 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            <div class="d3-neural" style="width:100%;margin:10px 0;"></div>
         | 
| 2 | 
            +
            <style>
         | 
| 3 | 
            +
              .d3-neural .controls { margin-top: 12px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
         | 
| 4 | 
            +
              .d3-neural .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
         | 
| 5 | 
            +
              .d3-neural .controls input[type="range"]{ width: 160px; }
         | 
| 6 | 
            +
              .d3-neural .panel { display:flex; gap:16px; align-items:flex-start; }
         | 
| 7 | 
            +
              .d3-neural .left { flex: 0 0 320px; display:flex; flex-direction:column; gap:8px; }
         | 
| 8 | 
            +
              .d3-neural .right { flex: 1 1 auto; min-width: 0; }
         | 
| 9 | 
            +
              .d3-neural canvas { width: 100%; height: auto; border-radius: 8px; border: 1px solid var(--border-color); background: var(--surface-bg); display:block; }
         | 
| 10 | 
            +
              .d3-neural .preview28 { display:grid; grid-template-columns: repeat(28, 1fr); gap: 1px; width: 100%; }
         | 
| 11 | 
            +
              .d3-neural .preview28 span { display:block; aspect-ratio:1/1; border-radius:2px; }
         | 
| 12 | 
            +
              .d3-neural .legend { font-size: 12px; color: var(--text-color); line-height:1.35; }
         | 
| 13 | 
            +
              .d3-neural .probs { display:flex; gap:6px; align-items:flex-end; height: 64px; }
         | 
| 14 | 
            +
              .d3-neural .probs .bar { width: 10px; border-radius:2px 2px 0 0; background: var(--border-color); transition: height .15s ease, background-color .15s ease; }
         | 
| 15 | 
            +
              .d3-neural .probs .bar.active { background: var(--primary-color); }
         | 
| 16 | 
            +
              .d3-neural .probs .tick { font-size: 10px; color: var(--muted-color); text-align:center; margin-top: 2px; }
         | 
| 17 | 
            +
              .d3-neural .canvas-wrap { position: relative; }
         | 
| 18 | 
            +
              .d3-neural .erase-btn { position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-color);  }
         | 
| 19 | 
            +
              .d3-neural .canvas-hint { position: absolute; top: 8px; left: 8px; font-size: 11px; color: var(--muted-color); pointer-events: none; }
         | 
| 20 | 
            +
            </style>
         | 
| 21 | 
            +
            <script>
         | 
| 22 | 
            +
              (() => {
         | 
| 23 | 
            +
                const ensureD3 = (cb) => {
         | 
| 24 | 
            +
                  if (window.d3 && typeof window.d3.select === 'function') return cb();
         | 
| 25 | 
            +
                  let s = document.getElementById('d3-cdn-script');
         | 
| 26 | 
            +
                  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); }
         | 
| 27 | 
            +
                  const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
         | 
| 28 | 
            +
                  s.addEventListener('load', onReady, { once: true });
         | 
| 29 | 
            +
                  if (window.d3) onReady();
         | 
| 30 | 
            +
                };
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                const ensureTF = (cb) => {
         | 
| 33 | 
            +
                  if (window.tf && typeof window.tf.tensor === 'function') return cb();
         | 
| 34 | 
            +
                  let s = document.getElementById('tfjs-cdn-script');
         | 
| 35 | 
            +
                  if (!s) { s = document.createElement('script'); s.id = 'tfjs-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.20.0/dist/tf.min.js'; document.head.appendChild(s); }
         | 
| 36 | 
            +
                  const onReady = () => { if (window.tf && typeof window.tf.tensor === 'function') cb(); };
         | 
| 37 | 
            +
                  s.addEventListener('load', onReady, { once: true });
         | 
| 38 | 
            +
                  if (window.tf) onReady();
         | 
| 39 | 
            +
                };
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                const bootstrap = () => {
         | 
| 42 | 
            +
                  const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
         | 
| 43 | 
            +
                  const container = (mount && mount.querySelector && mount.querySelector('.d3-neural')) || document.querySelector('.d3-neural');
         | 
| 44 | 
            +
                  if (!container) return;
         | 
| 45 | 
            +
                  if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  // Layout: left (canvas + preview + controls), right (svg network)
         | 
| 48 | 
            +
                  const panel = document.createElement('div');
         | 
| 49 | 
            +
                  panel.className = 'panel';
         | 
| 50 | 
            +
                  const left = document.createElement('div'); left.className = 'left';
         | 
| 51 | 
            +
                  const right = document.createElement('div'); right.className = 'right';
         | 
| 52 | 
            +
                  panel.appendChild(left); panel.appendChild(right);
         | 
| 53 | 
            +
                  container.appendChild(panel);
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  // Canvas for drawing
         | 
| 56 | 
            +
                  const CANVAS_PX = 224; // canvas pixels (square)
         | 
| 57 | 
            +
                  const canvas = document.createElement('canvas'); canvas.width = CANVAS_PX; canvas.height = CANVAS_PX;
         | 
| 58 | 
            +
                  const ctx = canvas.getContext('2d');
         | 
| 59 | 
            +
                  // init white bg
         | 
| 60 | 
            +
                  ctx.fillStyle = '#ffffff'; ctx.fillRect(0,0,CANVAS_PX,CANVAS_PX);
         | 
| 61 | 
            +
                  const canvasWrap = document.createElement('div'); canvasWrap.className = 'canvas-wrap';
         | 
| 62 | 
            +
                  canvasWrap.appendChild(canvas);
         | 
| 63 | 
            +
                  // Erase icon button (top-right)
         | 
| 64 | 
            +
                  const eraseBtn = document.createElement('button'); eraseBtn.className='erase-btn button--ghost'; eraseBtn.type='button'; eraseBtn.setAttribute('aria-label','Clear');
         | 
| 65 | 
            +
                  eraseBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg>';
         | 
| 66 | 
            +
                  eraseBtn.addEventListener('click', () => clearCanvas());
         | 
| 67 | 
            +
                  canvasWrap.appendChild(eraseBtn);
         | 
| 68 | 
            +
                  // Hint (top-left)
         | 
| 69 | 
            +
                  const hint = document.createElement('div'); hint.className='canvas-hint'; hint.textContent='Draw a digit';
         | 
| 70 | 
            +
                  canvasWrap.appendChild(hint);
         | 
| 71 | 
            +
                  left.appendChild(canvasWrap);
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  // (preview grid removed)
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  // (controls removed; erase button is overlayed on canvas)
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  // (prediction panel removed; predictions rendered next to output nodes)
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  // SVG network on right
         | 
| 80 | 
            +
                  const svg = d3.select(right).append('svg').attr('width','100%').style('display','block');
         | 
| 81 | 
            +
                  const gRoot = svg.append('g');
         | 
| 82 | 
            +
                  const gInput = gRoot.append('g').attr('class','input');
         | 
| 83 | 
            +
                  const gInputLinks = gRoot.append('g').attr('class','input-links');
         | 
| 84 | 
            +
                  const gLinks = gRoot.append('g').attr('class','links');
         | 
| 85 | 
            +
                  const gNodes = gRoot.append('g').attr('class','nodes');
         | 
| 86 | 
            +
                  const gLabels = gRoot.append('g').attr('class','labels');
         | 
| 87 | 
            +
                  const gOutText = gRoot.append('g').attr('class','out-probs');
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  // Network structure (compact: 8 -> 8 -> 10)
         | 
| 90 | 
            +
                  const layerSizes = [8, 8, 10];
         | 
| 91 | 
            +
                  const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
         | 
| 92 | 
            +
                  // Links only between hidden->hidden and hidden->output
         | 
| 93 | 
            +
                  const links = [];
         | 
| 94 | 
            +
                  for (let i=0;i<layerSizes[0];i++){
         | 
| 95 | 
            +
                    for (let j=0;j<layerSizes[1];j++) links.push({ s:{l:0,i}, t:{l:1,j}, w: (Math.sin(i*17+j*31)+1)/2 });
         | 
| 96 | 
            +
                  }
         | 
| 97 | 
            +
                  for (let i=0;i<layerSizes[1];i++){
         | 
| 98 | 
            +
                    for (let j=0;j<layerSizes[2];j++) links.push({ s:{l:1,i}, t:{l:2,j}, w: (Math.cos(i*7+j*13)+1)/2 });
         | 
| 99 | 
            +
                  }
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  // Linear classifier: logits = W * feats + b, feats in [0,1]
         | 
| 102 | 
            +
                  // features: [total, cx, cy, lr, tb, htrans, vtrans, loopiness]
         | 
| 103 | 
            +
                  const W = [
         | 
| 104 | 
            +
                    // 0  1    2    3    4    5      6      7
         | 
| 105 | 
            +
                    [ 0.3, 0.0, 0.0, 0.0, 0.0, -0.8, -0.6,  1.2], // 0
         | 
| 106 | 
            +
                    [-0.2, 0.9, 0.2, 0.8, 0.1, -0.2,  0.2, -1.1], // 1
         | 
| 107 | 
            +
                    [ 0.1, 0.4, 0.2, 0.5, 0.2,  0.9,  0.1, -0.6], // 2
         | 
| 108 | 
            +
                    [ 0.2, 0.3, 0.2, 0.2, 0.2,  0.9,  0.0, -0.2], // 3
         | 
| 109 | 
            +
                    [ 0.0,-0.3, 0.2,-0.6, 0.4,  0.2,  0.8, -0.6], // 4
         | 
| 110 | 
            +
                    [ 0.1,-0.4, 0.2,-0.5, 0.5,  0.9,  0.1, -0.6], // 5
         | 
| 111 | 
            +
                    [ 0.2,-0.2, 0.6,-0.2, 0.8, -0.3,  0.2,  0.6], // 6
         | 
| 112 | 
            +
                    [ 0.0, 0.6,-0.2, 0.6,-0.8,  0.6,  0.0, -0.8], // 7
         | 
| 113 | 
            +
                    [ 0.4, 0.0, 0.0, 0.1, 0.1,  0.6,  0.6,  1.0], // 8
         | 
| 114 | 
            +
                    [ 0.2, 0.2,-0.6, 0.2,-0.8,  0.2,  0.6,  0.5], // 9
         | 
| 115 | 
            +
                  ];
         | 
| 116 | 
            +
                  const b = [-0.2, -0.1, -0.05, -0.05, -0.05, -0.05, -0.05, -0.1, -0.15, -0.1];
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                  function computeFeatures(x28){
         | 
| 119 | 
            +
                    // x28: Float32Array length 784, values in [0,1] (1 = black/ink)
         | 
| 120 | 
            +
                    let sum=0, cx=0, cy=0; const w=28, h=28;
         | 
| 121 | 
            +
                    const rowSum = new Array(h).fill(0); const colSum = new Array(w).fill(0);
         | 
| 122 | 
            +
                    let hTransitions=0, vTransitions=0;
         | 
| 123 | 
            +
                    for (let y=0;y<h;y++){
         | 
| 124 | 
            +
                      for (let x=0;x<w;x++){
         | 
| 125 | 
            +
                        const v = x28[y*w+x]; sum += v; cx += x*v; cy += y*v; rowSum[y]+=v; colSum[x]+=v;
         | 
| 126 | 
            +
                        if (x>0){ const v0=x28[y*w+(x-1)], v1=v; if ((v0>0.25)!==(v1>0.25)) hTransitions+=1; }
         | 
| 127 | 
            +
                        if (y>0){ const v0=x28[(y-1)*w+x], v1=v; if ((v0>0.25)!==(v1>0.25)) vTransitions+=1; }
         | 
| 128 | 
            +
                      }
         | 
| 129 | 
            +
                    }
         | 
| 130 | 
            +
                    const total = sum/(w*h); // [0,1]
         | 
| 131 | 
            +
                    const cxn = sum>1e-6 ? (cx/sum)/(w-1) : 0.5; // [0,1]
         | 
| 132 | 
            +
                    const cyn = sum>1e-6 ? (cy/sum)/(h-1) : 0.5; // [0,1]
         | 
| 133 | 
            +
                    let left=0,right=0,top=0,bottom=0;
         | 
| 134 | 
            +
                    for (let y=0;y<h;y++){ for (let x=0;x<w;x++){ const v=x28[y*w+x]; if (x<w/2) left+=v; else right+=v; if (y<h/2) top+=v; else bottom+=v; }}
         | 
| 135 | 
            +
                    const lr = (right/(right+left+1e-6));
         | 
| 136 | 
            +
                    const tb = (bottom/(bottom+top+1e-6));
         | 
| 137 | 
            +
                    const htn = Math.min(1, hTransitions/(w*h*0.35));
         | 
| 138 | 
            +
                    const vtn = Math.min(1, vTransitions/(w*h*0.35));
         | 
| 139 | 
            +
                    // Loopiness proxy: ink near perimeter low vs center high
         | 
| 140 | 
            +
                    let perimeter=0, center=0; const m=5;
         | 
| 141 | 
            +
                    for (let y=0;y<h;y++){
         | 
| 142 | 
            +
                      for (let x=0;x<w;x++){
         | 
| 143 | 
            +
                        const v=x28[y*w+x];
         | 
| 144 | 
            +
                        const isBorder = (x<m||x>=w-m||y<m||y>=h-m);
         | 
| 145 | 
            +
                        if (isBorder) perimeter+=v; else center+=v;
         | 
| 146 | 
            +
                      }
         | 
| 147 | 
            +
                    }
         | 
| 148 | 
            +
                    const loopiness = Math.min(1, center/(perimeter+center+1e-6)*1.8);
         | 
| 149 | 
            +
                    return [total, cxn, cyn, lr, tb, htn, vtn, loopiness];
         | 
| 150 | 
            +
                  }
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                  function softmax(arr){ const m=Math.max(...arr); const ex=arr.map(v=>Math.exp(v-m)); const s=ex.reduce((a,b)=>a+b,0)+1e-12; return ex.map(v=>v/s); }
         | 
| 153 | 
            +
                  function l2norm(a){ return Math.hypot(...a) || 0; }
         | 
| 154 | 
            +
                  function normalize(a){ const n=l2norm(a); return n>0 ? a.map(v=>v/n) : a.slice(); }
         | 
| 155 | 
            +
                  function cosine(a,b){ let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; const na=l2norm(a), nb=l2norm(b)||1; return na>0 ? s/(na*nb) : 0; }
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                  // MNIST-like normalization: crop to tight bbox, scale into 20x20, center in 28x28
         | 
| 158 | 
            +
                  function normalize28(x28){
         | 
| 159 | 
            +
                    const w=28,h=28, thr=0.2;
         | 
| 160 | 
            +
                    let minX=29,minY=29,maxX=-1,maxY=-1, sum=0, cx=0, cy=0;
         | 
| 161 | 
            +
                    for (let y=0;y<h;y++){
         | 
| 162 | 
            +
                      for (let x=0;x<w;x++){
         | 
| 163 | 
            +
                        const v = x28[y*w+x];
         | 
| 164 | 
            +
                        if (v>thr){ if (x<minX) minX=x; if (x>maxX) maxX=x; if (y<minY) minY=y; if (y>maxY) maxY=y; }
         | 
| 165 | 
            +
                        sum += v; cx += x*v; cy += y*v;
         | 
| 166 | 
            +
                      }
         | 
| 167 | 
            +
                    }
         | 
| 168 | 
            +
                    if (sum < 1e-3 || maxX<0){ return x28; }
         | 
| 169 | 
            +
                    const comX = cx/sum, comY = cy/sum;
         | 
| 170 | 
            +
                    const bw = Math.max(1, maxX-minX+1), bh = Math.max(1, maxY-minY+1);
         | 
| 171 | 
            +
                    const scale = 20/Math.max(bw, bh);
         | 
| 172 | 
            +
                    const out = new Float32Array(w*h);
         | 
| 173 | 
            +
                    // center of canvas
         | 
| 174 | 
            +
                    const cxOut = (w-1)/2, cyOut = (h-1)/2;
         | 
| 175 | 
            +
                    for (let y=0;y<h;y++){
         | 
| 176 | 
            +
                      for (let x=0;x<w;x++){
         | 
| 177 | 
            +
                        // map output pixel to source space around COM
         | 
| 178 | 
            +
                        const sx = (x - cxOut)/scale + comX;
         | 
| 179 | 
            +
                        const sy = (y - cyOut)/scale + comY;
         | 
| 180 | 
            +
                        out[y*w+x] = bilinearSample(x28, w, h, sx, sy);
         | 
| 181 | 
            +
                      }
         | 
| 182 | 
            +
                    }
         | 
| 183 | 
            +
                    return out;
         | 
| 184 | 
            +
                  }
         | 
| 185 | 
            +
                  function bilinearSample(img, w, h, x, y){
         | 
| 186 | 
            +
                    const x0 = Math.floor(x), y0 = Math.floor(y);
         | 
| 187 | 
            +
                    const x1 = x0+1, y1 = y0+1;
         | 
| 188 | 
            +
                    const tx = x - x0, ty = y - y0;
         | 
| 189 | 
            +
                    function at(ix,iy){ if (ix<0||iy<0||ix>=w||iy>=h) return 0; return img[iy*w+ix]; }
         | 
| 190 | 
            +
                    const v00 = at(x0,y0), v10 = at(x1,y0), v01 = at(x0,y1), v11 = at(x1,y1);
         | 
| 191 | 
            +
                    const a = v00*(1-tx)+v10*tx; const b = v01*(1-tx)+v11*tx; return a*(1-ty)+b*ty;
         | 
| 192 | 
            +
                  }
         | 
| 193 | 
            +
                  // Simple dilation (max-pooling 3x3) to thicken strokes
         | 
| 194 | 
            +
                  function dilate28(x){
         | 
| 195 | 
            +
                    const w=28,h=28; const out=new Float32Array(w*h);
         | 
| 196 | 
            +
                    for (let y=0;y<h;y++){
         | 
| 197 | 
            +
                      for (let x0=0;x0<w;x0++){
         | 
| 198 | 
            +
                        let m=0;
         | 
| 199 | 
            +
                        for (let dy=-1;dy<=1;dy++){
         | 
| 200 | 
            +
                          for (let dx=-1;dx<=1;dx++){
         | 
| 201 | 
            +
                            const xx=x0+dx, yy=y+dy; if (xx<0||yy<0||xx>=w||yy>=h) continue;
         | 
| 202 | 
            +
                            const v = x[yy*w+xx]; if (v>m) m=v;
         | 
| 203 | 
            +
                          }
         | 
| 204 | 
            +
                        }
         | 
| 205 | 
            +
                        out[y*w+x0]=m;
         | 
| 206 | 
            +
                      }
         | 
| 207 | 
            +
                    }
         | 
| 208 | 
            +
                    return out;
         | 
| 209 | 
            +
                  }
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                  // Glyph-based 28x28 prototypes for digits 0-9 (normalized)
         | 
| 212 | 
            +
                  const protoGlyphs28 = [];
         | 
| 213 | 
            +
                  (function buildGlyphProtos(){
         | 
| 214 | 
            +
                    const off = document.createElement('canvas'); off.width = CANVAS_PX; off.height = CANVAS_PX;
         | 
| 215 | 
            +
                    const c = off.getContext('2d');
         | 
| 216 | 
            +
                    for (let d=0; d<10; d++){
         | 
| 217 | 
            +
                      c.fillStyle = '#ffffff'; c.fillRect(0,0,off.width,off.height);
         | 
| 218 | 
            +
                      c.fillStyle = '#000000'; c.textAlign='center'; c.textBaseline='middle';
         | 
| 219 | 
            +
                      c.font = 'bold 180px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif';
         | 
| 220 | 
            +
                      c.fillText(String(d), off.width/2, off.height*0.56);
         | 
| 221 | 
            +
                      const src = c.getImageData(0,0,off.width,off.height).data; const block = off.width/28;
         | 
| 222 | 
            +
                      const vec = new Float32Array(28*28);
         | 
| 223 | 
            +
                      for (let gy=0; gy<28; gy++){
         | 
| 224 | 
            +
                        for (let gx=0; gx<28; gx++){
         | 
| 225 | 
            +
                          let acc=0, cnt=0; const x0=Math.floor(gx*block), y0=Math.floor(gy*block);
         | 
| 226 | 
            +
                          for (let yy=y0; yy<y0+block; yy++){
         | 
| 227 | 
            +
                            for (let xx=x0; xx<x0+block; xx++){
         | 
| 228 | 
            +
                              const idx=(yy*off.width+xx)*4; const r=src[idx], g=src[idx+1], b=src[idx+2];
         | 
| 229 | 
            +
                              const gray=(r+g+b)/3/255; acc += (1-gray); cnt++;
         | 
| 230 | 
            +
                            }
         | 
| 231 | 
            +
                          }
         | 
| 232 | 
            +
                          vec[gy*28+gx] = acc/(cnt||1);
         | 
| 233 | 
            +
                        }
         | 
| 234 | 
            +
                      }
         | 
| 235 | 
            +
                      const normed = normalize28(vec);
         | 
| 236 | 
            +
                      const n = l2norm(normed)||1; protoGlyphs28.push(normed.map(v=>v/n));
         | 
| 237 | 
            +
                    }
         | 
| 238 | 
            +
                  })();
         | 
| 239 | 
            +
                  function dot(a,b){ let s=0; for (let i=0;i<a.length;i++) s+=a[i]*b[i]; return s; }
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                  // Resize handling and node layout
         | 
| 242 | 
            +
                  let width=800, height=360; const margin = { top: 16, right: 24, bottom: 24, left: 24 };
         | 
| 243 | 
            +
                  let inputGrid = { cell: 0, x: 0, y: 0, width: 0, height: 0 };
         | 
| 244 | 
            +
                  function layoutNodes(){
         | 
| 245 | 
            +
                    // Right panel width, and a non-square aspect ratio for clarity
         | 
| 246 | 
            +
                    width = Math.max(300, Math.round(right.clientWidth || 800));
         | 
| 247 | 
            +
                    height = Math.max(260, Math.round(width * 0.45));
         | 
| 248 | 
            +
                    svg.attr('width', width).attr('height', height);
         | 
| 249 | 
            +
                    const innerW = width - margin.left - margin.right; const innerH = height - margin.top - margin.bottom;
         | 
| 250 | 
            +
                    gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
         | 
| 251 | 
            +
                    // Input grid layout (28x28) at left — cap width to a fraction of innerW
         | 
| 252 | 
            +
                    const maxGridFrac = 0.28; // at most 28% of available width
         | 
| 253 | 
            +
                    const cellByHeight = Math.floor(innerH / 28);
         | 
| 254 | 
            +
                    const cellByWidth = Math.floor((innerW * maxGridFrac) / 28);
         | 
| 255 | 
            +
                    let cell = Math.max(3, Math.min(cellByHeight, cellByWidth));
         | 
| 256 | 
            +
                    let gridH = cell * 28; let gridY = Math.floor((innerH - gridH)/2);
         | 
| 257 | 
            +
                    inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
         | 
| 258 | 
            +
                    // Ensure there is always space for layers to the right
         | 
| 259 | 
            +
                    const minRightPad = 40; // minimal free space at right
         | 
| 260 | 
            +
                    let startX = inputGrid.width + 24;
         | 
| 261 | 
            +
                    if (startX > innerW - minRightPad) {
         | 
| 262 | 
            +
                      // Shrink grid to free horizontal room
         | 
| 263 | 
            +
                      cell = Math.max(3, Math.floor((innerW - minRightPad - 24) / 28));
         | 
| 264 | 
            +
                      gridH = cell * 28; gridY = Math.floor((innerH - gridH)/2);
         | 
| 265 | 
            +
                      inputGrid = { cell, x: 0, y: gridY, width: cell*28, height: gridH };
         | 
| 266 | 
            +
                      startX = inputGrid.width + 24;
         | 
| 267 | 
            +
                    }
         | 
| 268 | 
            +
                    const nLayers = layerSizes.length;
         | 
| 269 | 
            +
                    // Reserve space at right for output labels/bars so they don't get cut off
         | 
| 270 | 
            +
                    const rightLabelPad = 100; // px reserved for digit label + bar
         | 
| 271 | 
            +
                    const availableW = Math.max(100, innerW - startX - rightLabelPad);
         | 
| 272 | 
            +
                    // Reduce inter-layer spacing slightly; keep a sane min/max
         | 
| 273 | 
            +
                    const stepX = nLayers > 1 ? Math.min(200, Math.max(28, availableW / (nLayers - 1))) : 0;
         | 
| 274 | 
            +
                    const xs = Array.from({ length: nLayers }, (_, li) => startX + stepX * li);
         | 
| 275 | 
            +
                    // Y positions evenly spaced per layer
         | 
| 276 | 
            +
                    layers.forEach((nodes, li)=>{
         | 
| 277 | 
            +
                      const n = nodes.length; const spacing = innerH/(n+1);
         | 
| 278 | 
            +
                      nodes.forEach((nd, i)=>{ nd.x = xs[li]; nd.y = spacing*(i+1); });
         | 
| 279 | 
            +
                    });
         | 
| 280 | 
            +
                  }
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                  let lastX28 = new Float32Array(28*28);
         | 
| 283 | 
            +
                  function renderInputGrid(){
         | 
| 284 | 
            +
                    if (!inputGrid || inputGrid.cell <= 0) return;
         | 
| 285 | 
            +
                    const data = Array.from({ length: 28*28 }, (_, i) => ({ i, v: lastX28[i] || 0 }));
         | 
| 286 | 
            +
                    const sel = gInput.selectAll('rect.input-px').data(data, d=>d.i);
         | 
| 287 | 
            +
                    const gap = Math.max(1, Math.floor(inputGrid.cell * 0.10));
         | 
| 288 | 
            +
                    const inner = Math.max(1, inputGrid.cell - gap);
         | 
| 289 | 
            +
                    const offset = Math.floor(gap / 2);
         | 
| 290 | 
            +
                    sel.enter().append('rect').attr('class','input-px')
         | 
| 291 | 
            +
                      .attr('width', inner).attr('height', inner)
         | 
| 292 | 
            +
                      .merge(sel)
         | 
| 293 | 
            +
                      .attr('x', d => inputGrid.x + (d.i % 28) * inputGrid.cell + offset)
         | 
| 294 | 
            +
                      .attr('y', d => inputGrid.y + Math.floor(d.i / 28) * inputGrid.cell + offset)
         | 
| 295 | 
            +
                      .attr('fill', d => { const g = 255 - Math.round(d.v * 255); return `rgb(${g},${g},${g})`; })
         | 
| 296 | 
            +
                      .attr('stroke', 'none');
         | 
| 297 | 
            +
                    sel.exit().remove();
         | 
| 298 | 
            +
                  }
         | 
| 299 | 
            +
             | 
| 300 | 
            +
                  function renderInputLinks(){
         | 
| 301 | 
            +
                    // Draw bundle-like links from input grid right edge to first layer nodes (features)
         | 
| 302 | 
            +
                    const firstLayer = layers[0];
         | 
| 303 | 
            +
                    if (!firstLayer || !inputGrid || inputGrid.cell <= 0) { gInputLinks.selectAll('path').remove(); return; }
         | 
| 304 | 
            +
                    const innerH = height - margin.top - margin.bottom;
         | 
| 305 | 
            +
                    const x0 = inputGrid.x + inputGrid.width;
         | 
| 306 | 
            +
                    const paths = firstLayer.map((n, idx) => {
         | 
| 307 | 
            +
                      const yTarget = n.y;
         | 
| 308 | 
            +
                      // source y roughly aligned to node y, clamped within the grid
         | 
| 309 | 
            +
                      const y0 = Math.max(inputGrid.y, Math.min(inputGrid.y + inputGrid.height, yTarget));
         | 
| 310 | 
            +
                      const dx = (n.x - x0) * 0.35;
         | 
| 311 | 
            +
                      return { x0, y0, x1: n.x - 12, y1: yTarget, c1x: x0 + dx, c1y: y0, c2x: n.x - dx, c2y: yTarget };
         | 
| 312 | 
            +
                    });
         | 
| 313 | 
            +
                    const sel = gInputLinks.selectAll('path.input-link').data(paths);
         | 
| 314 | 
            +
                    sel.enter().append('path').attr('class','input-link')
         | 
| 315 | 
            +
                      .attr('fill','none')
         | 
| 316 | 
            +
                      .attr('stroke','rgba(0,0,0,0.25)')
         | 
| 317 | 
            +
                      .attr('stroke-width', 1)
         | 
| 318 | 
            +
                      .merge(sel)
         | 
| 319 | 
            +
                      .attr('d', d => `M${d.x0},${d.y0} C${d.c1x},${d.c1y} ${d.c2x},${d.c2y} ${d.x1},${d.y1}`);
         | 
| 320 | 
            +
                    sel.exit().remove();
         | 
| 321 | 
            +
                  }
         | 
| 322 | 
            +
             | 
| 323 | 
            +
                  function renderGraph(showEdges){
         | 
| 324 | 
            +
                    layoutNodes();
         | 
| 325 | 
            +
                    renderInputGrid();
         | 
| 326 | 
            +
                    renderInputLinks();
         | 
| 327 | 
            +
                    // Nodes
         | 
| 328 | 
            +
                    const allNodes = layers.flat();
         | 
| 329 | 
            +
                    const nodeSel = gNodes.selectAll('circle.node').data(allNodes, d=>d.id);
         | 
| 330 | 
            +
                    nodeSel.enter().append('circle').attr('class','node')
         | 
| 331 | 
            +
                      .attr('r', 10)
         | 
| 332 | 
            +
                      .attr('cx', d=>d.x).attr('cy', d=>d.y)
         | 
| 333 | 
            +
                      .attr('fill', d=> d.layer===2 ? 'var(--primary-color)' : 'var(--surface-bg)')
         | 
| 334 | 
            +
                      .attr('stroke','var(--border-color)').attr('stroke-width',1)
         | 
| 335 | 
            +
                      .merge(nodeSel)
         | 
| 336 | 
            +
                      .attr('cx', d=>d.x).attr('cy', d=>d.y)
         | 
| 337 | 
            +
                      .attr('opacity', 1);
         | 
| 338 | 
            +
                    nodeSel.exit().remove();
         | 
| 339 | 
            +
             | 
| 340 | 
            +
                    // Labels for first hidden layer only (avoid stacking with output probs)
         | 
| 341 | 
            +
                    const labels = [];
         | 
| 342 | 
            +
                    layers[0].forEach((n,i)=> labels.push({ x:n.x, y:n.y-16, txt:`f${i+1}` }));
         | 
| 343 | 
            +
                    const labSel = gLabels.selectAll('text').data(labels);
         | 
| 344 | 
            +
                    labSel.enter().append('text').style('font-size','12px').style('fill','var(--muted-color)')
         | 
| 345 | 
            +
                      .attr('x', d=>d.x).attr('y', d=>d.y)
         | 
| 346 | 
            +
                      .text(d=>d.txt)
         | 
| 347 | 
            +
                      .merge(labSel)
         | 
| 348 | 
            +
                      .attr('x', d=>d.x).attr('y', d=>d.y).text(d=>d.txt);
         | 
| 349 | 
            +
                    labSel.exit().remove();
         | 
| 350 | 
            +
             | 
| 351 | 
            +
                    // Links as smooth curves
         | 
| 352 | 
            +
                    const pathFor = (d) => {
         | 
| 353 | 
            +
                      const x1 = layers[d.s.l][d.s.i].x, y1 = layers[d.s.l][d.s.i].y;
         | 
| 354 | 
            +
                      const x2 = layers[d.t.l][d.t.j].x, y2 = layers[d.t.l][d.t.j].y;
         | 
| 355 | 
            +
                      const dx = (x2 - x1) * 0.45;
         | 
| 356 | 
            +
                      return `M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`;
         | 
| 357 | 
            +
                    };
         | 
| 358 | 
            +
                    const linkSel = gLinks.selectAll('path.link').data(links, d=> `${d.s.l}-${d.s.i}-${d.t.l}-${d.t.j}`);
         | 
| 359 | 
            +
                    linkSel.enter().append('path').attr('class','link')
         | 
| 360 | 
            +
                      .attr('d', pathFor)
         | 
| 361 | 
            +
                      .attr('fill','none')
         | 
| 362 | 
            +
                      .attr('stroke','rgba(0,0,0,0.25)')
         | 
| 363 | 
            +
                      .attr('stroke-width', d=> 0.5 + d.w*1.2)
         | 
| 364 | 
            +
                      .merge(linkSel)
         | 
| 365 | 
            +
                      .attr('d', pathFor)
         | 
| 366 | 
            +
                      .attr('stroke-width', d=> 0.5 + d.w*1.2);
         | 
| 367 | 
            +
                    linkSel.exit().remove();
         | 
| 368 | 
            +
                  }
         | 
| 369 | 
            +
             | 
| 370 | 
            +
                  function setNodeActivations(h1, h2, out){
         | 
| 371 | 
            +
                    layers[0].forEach((n,i)=> n.a = h1[i] || 0);
         | 
| 372 | 
            +
                    layers[1].forEach((n,i)=> n.a = h2[i] || 0);
         | 
| 373 | 
            +
                    layers[2].forEach((n,i)=> n.a = out[i] || 0);
         | 
| 374 | 
            +
                    // Determine top prediction (for ghosting others)
         | 
| 375 | 
            +
                    let argmaxIdx = 0; let bestProb = -1;
         | 
| 376 | 
            +
                    if (Array.isArray(out)) {
         | 
| 377 | 
            +
                      for (let i=0;i<out.length;i++){ if (out[i] > bestProb){ bestProb = out[i]; argmaxIdx = i; } }
         | 
| 378 | 
            +
                    }
         | 
| 379 | 
            +
                    // Color/opacity by activation
         | 
| 380 | 
            +
                    gNodes.selectAll('circle.node')
         | 
| 381 | 
            +
                      .attr('fill', d=> d.layer===2 ? 'var(--primary-color)' : `rgba(0,0,0,${0.06 + 0.44*d.a})`)
         | 
| 382 | 
            +
                      .attr('stroke', d=> d.layer===2 ? 'var(--primary-color)' : 'var(--border-color)')
         | 
| 383 | 
            +
                      .attr('opacity', d=> 0.25 + 0.75*Math.min(1, d.a))
         | 
| 384 | 
            +
                      .attr('r', d=> 8 + 6*Math.min(1, d.a));
         | 
| 385 | 
            +
                    // Link opacity by activation flow
         | 
| 386 | 
            +
                    gLinks.selectAll('path.link')
         | 
| 387 | 
            +
                      .attr('stroke', d=>{
         | 
| 388 | 
            +
                        const aS = layers[d.s.l][d.s.i].a || 0; const aT = layers[d.t.l][d.t.j].a || 0;
         | 
| 389 | 
            +
                        const alpha = Math.min(1, 0.08 + 0.85 * (aS * aT));
         | 
| 390 | 
            +
                        const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
         | 
| 391 | 
            +
                        const base = isDark ? 255 : 0;
         | 
| 392 | 
            +
                        return `rgba(${base},${base},${base},${alpha})`;
         | 
| 393 | 
            +
                      });
         | 
| 394 | 
            +
                    // Output labels: bold digit + small horizontal bar for probability
         | 
| 395 | 
            +
                    const outs = layers[2].map((n,i)=>({ x:n.x+18, y:n.y, digit: i, prob: (out[i]||0), isTop: i===argmaxIdx }));
         | 
| 396 | 
            +
                    const gSel = gOutText.selectAll('g.out-label').data(outs, d=>d.digit);
         | 
| 397 | 
            +
                    const gEnter = gSel.enter().append('g').attr('class','out-label');
         | 
| 398 | 
            +
                    gEnter.append('text').attr('class','out-digit')
         | 
| 399 | 
            +
                      .style('font-size','12px').style('font-weight','700').style('fill','var(--text-color)');
         | 
| 400 | 
            +
                    gEnter.append('rect').attr('class','out-bar-bg').attr('rx',2).attr('ry',2)
         | 
| 401 | 
            +
                      .attr('height', 4).attr('fill', 'var(--border-color)');
         | 
| 402 | 
            +
                    gEnter.append('rect').attr('class','out-bar').attr('rx',2).attr('ry',2)
         | 
| 403 | 
            +
                      .attr('height', 4);
         | 
| 404 | 
            +
                    const BAR_MAX = 64;
         | 
| 405 | 
            +
                    gEnter.merge(gSel)
         | 
| 406 | 
            +
                      .attr('transform', d=>`translate(${d.x},${d.y})`)
         | 
| 407 | 
            +
                      .each(function(d){
         | 
| 408 | 
            +
                        const sel = d3.select(this);
         | 
| 409 | 
            +
                        sel.select('text.out-digit')
         | 
| 410 | 
            +
                          .attr('x', 0).attr('y', -2)
         | 
| 411 | 
            +
                          .text(String(d.digit));
         | 
| 412 | 
            +
                        sel.select('rect.out-bar-bg')
         | 
| 413 | 
            +
                          .attr('x', 0).attr('y', 6)
         | 
| 414 | 
            +
                          .attr('width', BAR_MAX);
         | 
| 415 | 
            +
                        sel.select('rect.out-bar')
         | 
| 416 | 
            +
                          .attr('x', 0).attr('y', 6)
         | 
| 417 | 
            +
                          .attr('width', Math.max(1, Math.round(d.prob * BAR_MAX)))
         | 
| 418 | 
            +
                          .attr('fill', d.isTop ? 'var(--primary-color)' : 'var(--border-color)');
         | 
| 419 | 
            +
                        // Ghost non-top predictions
         | 
| 420 | 
            +
                        sel.style('opacity', d.isTop ? 1 : 0.35);
         | 
| 421 | 
            +
                      });
         | 
| 422 | 
            +
                    gSel.exit().remove();
         | 
| 423 | 
            +
                  }
         | 
| 424 | 
            +
             | 
| 425 | 
            +
                  // (no separate updateBars; bars are rendered next to nodes)
         | 
| 426 | 
            +
             | 
| 427 | 
            +
                  function runPipeline(){
         | 
| 428 | 
            +
                    const x28raw = downsample28();
         | 
| 429 | 
            +
                    const x28 = dilate28(normalize28(x28raw));
         | 
| 430 | 
            +
                    // Update input grid data
         | 
| 431 | 
            +
                    lastX28 = x28;
         | 
| 432 | 
            +
                    renderInputGrid();
         | 
| 433 | 
            +
                    const feats = computeFeatures(x28); // 8D in [0,1]
         | 
| 434 | 
            +
                    const inkMass = feats[0];
         | 
| 435 | 
            +
                    // Hidden 1 = raw features
         | 
| 436 | 
            +
                    const h1 = feats;
         | 
| 437 | 
            +
                    // Hidden 2 = simple non-linear mix for visualization only
         | 
| 438 | 
            +
                    const h2 = layers[1].map((_, j)=>{
         | 
| 439 | 
            +
                      let s=0; for (let i=0;i<layers[0].length;i++){ const w = (Math.sin(i*17+j*31)+1)/2 * 0.8 + 0.1; s += w*h1[i]; }
         | 
| 440 | 
            +
                      return Math.tanh(s*0.8);
         | 
| 441 | 
            +
                    });
         | 
| 442 | 
            +
                    let prob;
         | 
| 443 | 
            +
                    if (inkMass < 0.03){
         | 
| 444 | 
            +
                      // Too little ink: return near-uniform distribution
         | 
| 445 | 
            +
                      prob = Array.from({length:10}, ()=> 1/10);
         | 
| 446 | 
            +
                    } else {
         | 
| 447 | 
            +
                      // Prefer TFJS model if available
         | 
| 448 | 
            +
                      const tfProbs = predictTfjs(x28);
         | 
| 449 | 
            +
                      if (tfProbs && tfProbs.length === 10) {
         | 
| 450 | 
            +
                        prob = tfProbs;
         | 
| 451 | 
            +
                      } else {
         | 
| 452 | 
            +
                        // Fallback: rely mostly on glyph similarity
         | 
| 453 | 
            +
                        const x28n = normalize(x28);
         | 
| 454 | 
            +
                        const logitsGlyph = protoGlyphs28.map(p => 8.0 * cosine(x28n, p));
         | 
| 455 | 
            +
                        const logitsLinear = W.map((row, k)=> dot(row, h1) + b[k]);
         | 
| 456 | 
            +
                        const logits = logitsGlyph.map((v,k)=> v + 0.2*logitsLinear[k]);
         | 
| 457 | 
            +
                        prob = softmax(logits);
         | 
| 458 | 
            +
                      }
         | 
| 459 | 
            +
                    }
         | 
| 460 | 
            +
                    setNodeActivations(h1, h2.map(v => (v+1)/2), prob);
         | 
| 461 | 
            +
                  }
         | 
| 462 | 
            +
             | 
| 463 | 
            +
                  function downsample28(){
         | 
| 464 | 
            +
                    // From canvas (224x224) to 28x28 by average pooling in 8x8 blocks
         | 
| 465 | 
            +
                    const block = CANVAS_PX/28; // 8
         | 
| 466 | 
            +
                    const src = ctx.getImageData(0,0,CANVAS_PX,CANVAS_PX).data;
         | 
| 467 | 
            +
                    const out = new Float32Array(28*28);
         | 
| 468 | 
            +
                    for (let gy=0; gy<28; gy++){
         | 
| 469 | 
            +
                      for (let gx=0; gx<28; gx++){
         | 
| 470 | 
            +
                        let acc=0; let cnt=0;
         | 
| 471 | 
            +
                        const x0 = Math.floor(gx*block), y0 = Math.floor(gy*block);
         | 
| 472 | 
            +
                        for (let y=y0; y<y0+block; y++){
         | 
| 473 | 
            +
                          for (let x=x0; x<x0+block; x++){
         | 
| 474 | 
            +
                            const idx = (y*CANVAS_PX + x)*4; // RGBA
         | 
| 475 | 
            +
                            const r=src[idx], g=src[idx+1], b=src[idx+2];
         | 
| 476 | 
            +
                            const gray = (r+g+b)/3/255; // 1: white, 0: black
         | 
| 477 | 
            +
                            const ink = 1-gray; // 1: ink/black
         | 
| 478 | 
            +
                            acc += ink; cnt++;
         | 
| 479 | 
            +
                          }
         | 
| 480 | 
            +
                        }
         | 
| 481 | 
            +
                        out[gy*28+gx] = acc/(cnt||1);
         | 
| 482 | 
            +
                      }
         | 
| 483 | 
            +
                    }
         | 
| 484 | 
            +
                    return out;
         | 
| 485 | 
            +
                  }
         | 
| 486 | 
            +
             | 
| 487 | 
            +
                  function clearCanvas(){ ctx.fillStyle = '#ffffff'; ctx.fillRect(0,0,CANVAS_PX,CANVAS_PX); runPipeline(); }
         | 
| 488 | 
            +
             | 
| 489 | 
            +
                  // Drawing interactions
         | 
| 490 | 
            +
                  let drawing=false; let last=null;
         | 
| 491 | 
            +
                  const getPos = (ev) => {
         | 
| 492 | 
            +
                    const rect = canvas.getBoundingClientRect();
         | 
| 493 | 
            +
                    const sx = CANVAS_PX/rect.width; const sy = CANVAS_PX/rect.height;
         | 
| 494 | 
            +
                    const x = (('touches' in ev)? ev.touches[0].clientX : ev.clientX) - rect.left;
         | 
| 495 | 
            +
                    const y = (('touches' in ev)? ev.touches[0].clientY : ev.clientY) - rect.top;
         | 
| 496 | 
            +
                    return { x: x*sx, y: y*sy };
         | 
| 497 | 
            +
                  };
         | 
| 498 | 
            +
                  function drawTo(p){
         | 
| 499 | 
            +
                    const size = 24;
         | 
| 500 | 
            +
                    ctx.lineCap='round'; ctx.lineJoin='round'; ctx.strokeStyle='#000000'; ctx.lineWidth=size;
         | 
| 501 | 
            +
                    if (!last) last = p;
         | 
| 502 | 
            +
                    ctx.beginPath(); ctx.moveTo(last.x, last.y); ctx.lineTo(p.x, p.y); ctx.stroke();
         | 
| 503 | 
            +
                    last = p; runPipeline();
         | 
| 504 | 
            +
                  }
         | 
| 505 | 
            +
                  function onDown(ev){ drawing=true; last=null; drawTo(getPos(ev)); ev.preventDefault(); }
         | 
| 506 | 
            +
                  function onMove(ev){ if (!drawing) return; drawTo(getPos(ev)); ev.preventDefault(); }
         | 
| 507 | 
            +
                  function onUp(){ drawing=false; last=null; }
         | 
| 508 | 
            +
                  canvas.addEventListener('mousedown', onDown); canvas.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp);
         | 
| 509 | 
            +
                  canvas.addEventListener('touchstart', onDown, { passive:false }); canvas.addEventListener('touchmove', onMove, { passive:false }); window.addEventListener('touchend', onUp);
         | 
| 510 | 
            +
             | 
| 511 | 
            +
                  // (erase button handled as overlay)
         | 
| 512 | 
            +
                  const rerender = () => { renderGraph(true); };
         | 
| 513 | 
            +
                  if (window.ResizeObserver) {
         | 
| 514 | 
            +
                    const ro = new ResizeObserver(()=>rerender());
         | 
| 515 | 
            +
                    ro.observe(right);
         | 
| 516 | 
            +
                    ro.observe(canvas);
         | 
| 517 | 
            +
                  } else { window.addEventListener('resize', rerender); }
         | 
| 518 | 
            +
             | 
| 519 | 
            +
                  // TFJS model (optional)
         | 
| 520 | 
            +
                  let tfModel = null;
         | 
| 521 | 
            +
                  const tryLoadModel = async () => {
         | 
| 522 | 
            +
                    await new Promise((res)=> ensureTF(res));
         | 
| 523 | 
            +
                    const candidates = [
         | 
| 524 | 
            +
                      // Prefer local variant in assets (weights shards must be colocated)
         | 
| 525 | 
            +
                      './assets/data/mnist-variant-model.json',
         | 
| 526 | 
            +
                      '../assets/data/mnist-variant-model.json',
         | 
| 527 | 
            +
                      '/assets/data/mnist-variant-model.json',
         | 
| 528 | 
            +
                      // Fallback to public TFJS MNIST
         | 
| 529 | 
            +
                      'https://storage.googleapis.com/tfjs-models/tfjs/mnist/model.json'
         | 
| 530 | 
            +
                    ];
         | 
| 531 | 
            +
                    for (const u of candidates){
         | 
| 532 | 
            +
                      try { tfModel = await tf.loadLayersModel(u); return; } catch(_) { /* try next */ }
         | 
| 533 | 
            +
                    }
         | 
| 534 | 
            +
                    tfModel = null;
         | 
| 535 | 
            +
                  };
         | 
| 536 | 
            +
             | 
| 537 | 
            +
                  function predictTfjs(x28){
         | 
| 538 | 
            +
                    if (!tfModel || !window.tf) return null;
         | 
| 539 | 
            +
                    const run = (arr) => {
         | 
| 540 | 
            +
                      const t = tf.tidy(()=> tf.tensor(arr, [28,28,1]).expandDims(0));
         | 
| 541 | 
            +
                      try { const y = tfModel.predict(t); const p = y.softmax(); const out = Array.from(p.dataSync()); tf.dispose([y,p,t]); return out; } catch(e){ tf.dispose(t); return null; }
         | 
| 542 | 
            +
                    };
         | 
| 543 | 
            +
                    // Try both orientations and keep the one with higher confidence
         | 
| 544 | 
            +
                    const p1 = run(x28);
         | 
| 545 | 
            +
                    const inv = x28.map(v=>1-v);
         | 
| 546 | 
            +
                    const p2 = run(inv);
         | 
| 547 | 
            +
                    let probs = p1 || p2;
         | 
| 548 | 
            +
                    if (p1 && p2){
         | 
| 549 | 
            +
                      const m1 = Math.max(...p1), m2 = Math.max(...p2);
         | 
| 550 | 
            +
                      probs = m2>m1 ? p2 : p1;
         | 
| 551 | 
            +
                    }
         | 
| 552 | 
            +
                    if (!probs) return null;
         | 
| 553 | 
            +
                    // Normalize output size to 10 classes (pad or slice)
         | 
| 554 | 
            +
                    if (probs.length < 10){ probs = probs.concat(Array(10 - probs.length).fill(0)); }
         | 
| 555 | 
            +
                    if (probs.length > 10){ probs = probs.slice(0,10); }
         | 
| 556 | 
            +
                    return probs;
         | 
| 557 | 
            +
                  }
         | 
| 558 | 
            +
             | 
| 559 | 
            +
                  // Initial render
         | 
| 560 | 
            +
                  renderGraph(true);
         | 
| 561 | 
            +
                  clearCanvas();
         | 
| 562 | 
            +
                  tryLoadModel();
         | 
| 563 | 
            +
                };
         | 
| 564 | 
            +
             | 
| 565 | 
            +
                if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
         | 
| 566 | 
            +
              })();
         | 
| 567 | 
            +
            </script>
         | 
| 568 | 
            +
             | 
| 569 | 
            +
             | 
| 570 | 
            +
             | 
| 571 | 
            +
             | 
    	
        app/src/content/embeds/d3-scatter.html
    ADDED
    
    | @@ -0,0 +1,160 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            <div class="d3-scatter" style="width:100%;margin:10px 0;"></div>
         | 
| 2 | 
            +
            <style>
         | 
| 3 | 
            +
              .d3-scatter .controls { margin-top: 12px; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
         | 
| 4 | 
            +
              .d3-scatter .controls label { font-size: 12px; color: var(--muted-color); display: flex; align-items: center; gap: 8px; white-space: nowrap; padding: 6px 10px; }
         | 
| 5 | 
            +
              .d3-scatter .controls select { font-size: 12px; padding: 8px 28px 8px 10px; border: 1px solid var(--border-color); border-radius: 8px; background-color: var(--surface-bg); color: var(--text-color); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; background-size: 12px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
         | 
| 6 | 
            +
              [data-theme="dark"] .d3-scatter .controls select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); }
         | 
| 7 | 
            +
              .d3-scatter .point { opacity: .9; }
         | 
| 8 | 
            +
              .d3-scatter .point:hover { opacity: 1; }
         | 
| 9 | 
            +
            </style>
         | 
| 10 | 
            +
            <script>
         | 
| 11 | 
            +
              (() => {
         | 
| 12 | 
            +
                const ensureD3 = (cb) => {
         | 
| 13 | 
            +
                  if (window.d3 && typeof window.d3.select === 'function') return cb();
         | 
| 14 | 
            +
                  let s = document.getElementById('d3-cdn-script');
         | 
| 15 | 
            +
                  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); }
         | 
| 16 | 
            +
                  const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
         | 
| 17 | 
            +
                  s.addEventListener('load', onReady, { once: true });
         | 
| 18 | 
            +
                  if (window.d3) onReady();
         | 
| 19 | 
            +
                };
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                const bootstrap = () => {
         | 
| 22 | 
            +
                  const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
         | 
| 23 | 
            +
                  const container = (mount && mount.querySelector && mount.querySelector('.d3-scatter')) || document.querySelector('.d3-scatter');
         | 
| 24 | 
            +
                  if (!container) return;
         | 
| 25 | 
            +
                  if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  // Try multiple paths: prefer public path then relative copies under content assets
         | 
| 28 | 
            +
                  const JSON_PATHS = [
         | 
| 29 | 
            +
                    '/data/data.json',
         | 
| 30 | 
            +
                    './assets/data/data.json',
         | 
| 31 | 
            +
                    '../assets/data/data.json',
         | 
| 32 | 
            +
                    '../../assets/data/data.json'
         | 
| 33 | 
            +
                  ];
         | 
| 34 | 
            +
                  const fetchFirstAvailable = async (paths) => {
         | 
| 35 | 
            +
                    for (const p of paths) {
         | 
| 36 | 
            +
                      try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.json(); } catch(e) {}
         | 
| 37 | 
            +
                    }
         | 
| 38 | 
            +
                    throw new Error('JSON not found: data.json');
         | 
| 39 | 
            +
                  };
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  // SVG scaffolding
         | 
| 42 | 
            +
                  const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
         | 
| 43 | 
            +
                  const gRoot = svg.append('g');
         | 
| 44 | 
            +
                  const gGrid = gRoot.append('g').attr('class','grid');
         | 
| 45 | 
            +
                  const gAxes = gRoot.append('g').attr('class','axes');
         | 
| 46 | 
            +
                  const gPoints = gRoot.append('g').attr('class','points');
         | 
| 47 | 
            +
                  const gHover = gRoot.append('g').attr('class','hover');
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  // Tooltip
         | 
| 50 | 
            +
                  container.style.position = container.style.position || 'relative';
         | 
| 51 | 
            +
                  let tip = container.querySelector('.d3-tooltip'); let tipInner;
         | 
| 52 | 
            +
                  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; }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  // Layout & scales
         | 
| 55 | 
            +
                  let width=800, height=360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
         | 
| 56 | 
            +
                  const x = d3.scaleLinear();
         | 
| 57 | 
            +
                  const y = d3.scaleLinear();
         | 
| 58 | 
            +
                  const color = d3.scaleOrdinal(d3.schemeTableau10);
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  function updateScales(domainX, domainY){
         | 
| 63 | 
            +
                    const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
         | 
| 64 | 
            +
                    const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
         | 
| 65 | 
            +
                    const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
         | 
| 66 | 
            +
                    const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);
         | 
| 69 | 
            +
                    const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    x.domain(domainX).range([0, innerWidth]).nice();
         | 
| 72 | 
            +
                    y.domain(domainY).range([innerHeight, 0]).nice();
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    // Grid
         | 
| 75 | 
            +
                    gGrid.selectAll('*').remove();
         | 
| 76 | 
            +
                    gGrid.selectAll('line').data(y.ticks(6)).join('line')
         | 
| 77 | 
            +
                      .attr('x1', 0).attr('x2', innerWidth).attr('y1', (d)=>y(d)).attr('y2', (d)=>y(d))
         | 
| 78 | 
            +
                      .attr('stroke', gridColor).attr('stroke-width', 1).attr('shape-rendering', 'crispEdges');
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    // Axes
         | 
| 81 | 
            +
                    gAxes.selectAll('*').remove();
         | 
| 82 | 
            +
                    gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(d3.axisBottom(x).ticks(8)).call((g)=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','12px'); });
         | 
| 83 | 
            +
                    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'); });
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    // Axis labels
         | 
| 86 | 
            +
                    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('x');
         | 
| 87 | 
            +
                    gAxes.append('text').attr('class','axis-label axis-label--y').attr('text-anchor','middle').attr('transform', `translate(${-36},${innerHeight/2}) rotate(-90)`).style('font-size','12px').style('fill', tickColor).text('y');
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    return { innerWidth, innerHeight };
         | 
| 92 | 
            +
                  }
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  function normalizeData(raw){
         | 
| 95 | 
            +
                    // Accepts: [{x,y,label?}] or [[x,y,label?]] or objects with other keys (will try first two numeric fields)
         | 
| 96 | 
            +
                    const out = [];
         | 
| 97 | 
            +
                    if (Array.isArray(raw)) {
         | 
| 98 | 
            +
                      raw.forEach((row) => {
         | 
| 99 | 
            +
                        if (row == null) return;
         | 
| 100 | 
            +
                        if (Array.isArray(row)) {
         | 
| 101 | 
            +
                          const x = +row[0]; const y = +row[1]; const label = row.length > 2 ? row[2] : undefined; if (Number.isFinite(x) && Number.isFinite(y)) out.push({ x, y, label });
         | 
| 102 | 
            +
                        } else if (typeof row === 'object') {
         | 
| 103 | 
            +
                          if ('x' in row && 'y' in row) { const x = +row.x; const y = +row.y; const label = row.label; if (Number.isFinite(x) && Number.isFinite(y)) out.push({ x, y, label }); }
         | 
| 104 | 
            +
                          else {
         | 
| 105 | 
            +
                            const vals = Object.values(row).filter(v => typeof v === 'number' || (typeof v === 'string' && v.trim() !== ''));
         | 
| 106 | 
            +
                            const nums = vals.map(v => +v).filter(Number.isFinite);
         | 
| 107 | 
            +
                            if (nums.length >= 2) { out.push({ x: nums[0], y: nums[1], label: vals.length > 2 ? vals[2] : undefined }); }
         | 
| 108 | 
            +
                          }
         | 
| 109 | 
            +
                        }
         | 
| 110 | 
            +
                      });
         | 
| 111 | 
            +
                    }
         | 
| 112 | 
            +
                    return out;
         | 
| 113 | 
            +
                  }
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  function render(points){
         | 
| 116 | 
            +
                    if (!points || points.length === 0) { gPoints.selectAll('*').remove(); return; }
         | 
| 117 | 
            +
                    const xExtent = d3.extent(points, d=>d.x);
         | 
| 118 | 
            +
                    const yExtent = d3.extent(points, d=>d.y);
         | 
| 119 | 
            +
                    updateScales(xExtent, yExtent);
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                    const sel = gPoints.selectAll('circle.point').data(points);
         | 
| 122 | 
            +
                    sel.enter().append('circle').attr('class','point')
         | 
| 123 | 
            +
                      .attr('r', 3)
         | 
| 124 | 
            +
                      .attr('cx', d=>x(d.x))
         | 
| 125 | 
            +
                      .attr('cy', d=>y(d.y))
         | 
| 126 | 
            +
                      .attr('fill', d=> d.label != null ? color(d.label) : 'var(--primary-color)')
         | 
| 127 | 
            +
                      .on('mouseenter', function(ev, d){
         | 
| 128 | 
            +
                        tipInner.innerHTML = `<div><strong>x</strong> ${(+d.x).toFixed(4)}</div><div><strong>y</strong> ${(+d.y).toFixed(4)}</div>${d.label!=null?`<div><strong>label</strong> ${d.label}</div>`:''}`;
         | 
| 129 | 
            +
                        tip.style.opacity = '1';
         | 
| 130 | 
            +
                      })
         | 
| 131 | 
            +
                      .on('mousemove', function(ev){ const [mx,my] = d3.pointer(ev, container); const offsetX=12, offsetY=12; tip.style.transform = `translate(${Math.round(mx+offsetX)}px, ${Math.round(my+offsetY)}px)`; })
         | 
| 132 | 
            +
                      .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; })
         | 
| 133 | 
            +
                      .merge(sel)
         | 
| 134 | 
            +
                      .attr('cx', d=>x(d.x))
         | 
| 135 | 
            +
                      .attr('cy', d=>y(d.y))
         | 
| 136 | 
            +
                      .attr('fill', d=> d.label != null ? color(d.label) : 'var(--primary-color)');
         | 
| 137 | 
            +
                    sel.exit().remove();
         | 
| 138 | 
            +
                  }
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  (async () => {
         | 
| 141 | 
            +
                    try {
         | 
| 142 | 
            +
                      const raw = await fetchFirstAvailable(JSON_PATHS);
         | 
| 143 | 
            +
                      const points = normalizeData(raw);
         | 
| 144 | 
            +
                      render(points);
         | 
| 145 | 
            +
                      const rerender = () => { render(points); };
         | 
| 146 | 
            +
                      if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }
         | 
| 147 | 
            +
                    } catch (e) {
         | 
| 148 | 
            +
                      const pre = document.createElement('pre'); pre.textContent = 'JSON load error: ' + (e && e.message ? e.message : e);
         | 
| 149 | 
            +
                      pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';
         | 
| 150 | 
            +
                      container.appendChild(pre);
         | 
| 151 | 
            +
                    }
         | 
| 152 | 
            +
                  })();
         | 
| 153 | 
            +
                };
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }
         | 
| 156 | 
            +
              })();
         | 
| 157 | 
            +
            </script>
         | 
| 158 | 
            +
             | 
| 159 | 
            +
             | 
| 160 | 
            +
             | 
    	
        app/src/content/embeds/palettes.html
    CHANGED
    
    | @@ -15,8 +15,24 @@ | |
| 15 | 
             
                .palettes .copy-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } */
         | 
| 16 | 
             
                .palettes .copy-btn svg { width: 18px; height: 18px; fill: currentColor; display: block; }
         | 
| 17 | 
             
                /* Simulation UI */
         | 
| 18 | 
            -
                .palettes .palettes__select { width: 100%; max-width:  | 
| 19 | 
             
                .palettes .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 1px, 1px); white-space: nowrap; border: 0; }
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 20 | 
             
                /* Page-wide color vision simulation classes */
         | 
| 21 | 
             
                html.cb-grayscale, body.cb-grayscale { filter: grayscale(1) !important; }
         | 
| 22 | 
             
                html.cb-protanopia, body.cb-protanopia { filter: url(#cb-protanopia) !important; }
         | 
| @@ -28,21 +44,32 @@ | |
| 28 | 
             
                  .palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr));  }
         | 
| 29 | 
             
                  .palettes .palette-card__content { border-right: none; padding-right: 0; }
         | 
| 30 | 
             
                  .palettes .palette-card__actions { justify-self: start; }
         | 
|  | |
| 31 | 
             
                }
         | 
| 32 | 
             
              </style>
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 33 | 
             
              <div class="palettes__grid"></div>
         | 
| 34 | 
             
              <div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
         | 
| 35 | 
             
                <br/>
         | 
| 36 | 
             
                <p ><strong>Use color with care.</strong> Color should rarely be the only channel of meaning. Always pair it with text, icons, shape or position. The simulation below helps you spot palettes and states that become indistinguishable for people with color‑vision deficiencies. Toggle modes while checking charts, legends and interactions to ensure sufficient contrast and redundant cues.</p>
         | 
| 37 | 
            -
                <label for="cb-select">Color vision simulation</label>
         | 
| 38 | 
            -
                <select id="cb-select" class="palettes__select">
         | 
| 39 | 
            -
                  <option value="none">None — full color</option>
         | 
| 40 | 
            -
                  <option value="grayscale">Grayscale — no hue (luminance only)</option>
         | 
| 41 | 
            -
                  <option value="protanopia">Protanopia — reduced/absent reds</option>
         | 
| 42 | 
            -
                  <option value="deuteranopia">Deuteranopia — reduced/absent greens</option>
         | 
| 43 | 
            -
                  <option value="tritanopia">Tritanopia — reduced/absent blues</option>
         | 
| 44 | 
            -
                  <option value="achromatopsia">Achromatopsia — no color at all</option>
         | 
| 45 | 
            -
                </select>
         | 
| 46 | 
             
                <!-- Hidden SVG filters used by the page-wide simulation classes -->
         | 
| 47 | 
             
                <svg aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute; left:-9999px; overflow:hidden;">
         | 
| 48 | 
             
                  <defs>
         | 
| @@ -84,7 +111,7 @@ | |
| 84 | 
             
                  };
         | 
| 85 |  | 
| 86 | 
             
                const cards = [
         | 
| 87 | 
            -
                  { key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors. | 
| 88 | 
             
                      const base = chroma(baseHex);
         | 
| 89 | 
             
                      const lc = base.lch();
         | 
| 90 | 
             
                      const baseH = base.get('hsl.h') || 0;
         | 
| @@ -116,17 +143,18 @@ | |
| 116 | 
             
                      // Base en premier
         | 
| 117 | 
             
                      pushHex(base);
         | 
| 118 |  | 
| 119 | 
            -
                       | 
| 120 | 
            -
                      const  | 
| 121 | 
            -
                      const hueOffsets = [0,  | 
| 122 | 
             
                      const lVariants = [L0, Math.max(40, L0 - 6), Math.min(85, L0 + 6)];
         | 
| 123 |  | 
| 124 | 
            -
                       | 
|  | |
| 125 | 
             
                        let accepted = false;
         | 
| 126 | 
             
                        for (let li = 0; li < lVariants.length && !accepted; li++) {
         | 
| 127 | 
             
                          for (let oi = 0; oi < hueOffsets.length && !accepted; oi++) {
         | 
| 128 | 
            -
                             | 
| 129 | 
            -
                             | 
| 130 | 
             
                            const hex = col.hex();
         | 
| 131 | 
             
                            if (!seen.has(hex.toLowerCase()) && isFarEnough(hex)) {
         | 
| 132 | 
             
                              pushHex(col);
         | 
| @@ -135,12 +163,11 @@ | |
| 135 | 
             
                          }
         | 
| 136 | 
             
                        }
         | 
| 137 | 
             
                        if (!accepted) {
         | 
| 138 | 
            -
                          // Réduction de  | 
| 139 | 
             
                          let cTry = C0 - 10;
         | 
| 140 | 
            -
                          let h = (baseH + step + 360) % 360;
         | 
| 141 | 
             
                          let trials = 0;
         | 
| 142 | 
             
                          while (!accepted && cTry >= 30 && trials < 6) {
         | 
| 143 | 
            -
                            const col = makeSafe( | 
| 144 | 
             
                            const hex = col.hex();
         | 
| 145 | 
             
                            if (!seen.has(hex.toLowerCase()) && isFarEnough(hex)) {
         | 
| 146 | 
             
                              pushHex(col);
         | 
| @@ -150,11 +177,10 @@ | |
| 150 | 
             
                            cTry -= 5;
         | 
| 151 | 
             
                            trials++;
         | 
| 152 | 
             
                          }
         | 
| 153 | 
            -
                          // Dernier recours: choisir la teinte la plus éloignée possible même si < seuil
         | 
| 154 | 
             
                          if (!accepted) {
         | 
| 155 | 
             
                            let bestHex = null; let bestMin = -1;
         | 
| 156 | 
             
                            hueOffsets.forEach(off => {
         | 
| 157 | 
            -
                              const hh = ( | 
| 158 | 
             
                              const cand = makeSafe(hh, L0, C0).hex();
         | 
| 159 | 
             
                              const minD = results.reduce((m, prev) => Math.min(m, chroma.distance(cand, prev, 'lab')), Infinity);
         | 
| 160 | 
             
                              if (minD > bestMin && !seen.has(cand.toLowerCase())) { bestMin = minD; bestHex = cand; }
         | 
| @@ -162,23 +188,22 @@ | |
| 162 | 
             
                            if (bestHex) { seen.add(bestHex.toLowerCase()); results.push(bestHex); }
         | 
| 163 | 
             
                          }
         | 
| 164 | 
             
                        }
         | 
| 165 | 
            -
                      } | 
| 166 |  | 
| 167 | 
            -
                      return results.slice(0,  | 
| 168 | 
             
                  }},
         | 
| 169 | 
            -
                  { key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.', generator: (baseHex) => {
         | 
|  | |
| 170 | 
             
                      const c = chroma(baseHex).saturate(0.3);
         | 
| 171 | 
            -
                      return chroma.scale([c.darken(2), c, c.brighten(2)]).mode('lab').correctLightness(true).colors( | 
| 172 | 
             
                  }},
         | 
| 173 | 
            -
                  { key: 'diverging', title: 'Diverging', desc: 'For <strong>centered ranges</strong> with <strong>two extremes</strong> around a <strong>baseline</strong>. (e.g., negatives/positives)', generator: (baseHex) => {
         | 
|  | |
| 174 | 
             
                      const baseH = chroma(baseHex).get('hsl.h');
         | 
| 175 | 
             
                      const compH = (baseH + 180) % 360;
         | 
| 176 | 
             
                      const left = chroma.hsl(baseH, 0.75, 0.55);
         | 
| 177 | 
             
                      const right = chroma.hsl(compH, 0.75, 0.55);
         | 
| 178 | 
            -
                       | 
| 179 | 
            -
                      const leftRamp = chroma.scale([left, center]).mode('lch').correctLightness(true).colors(4);
         | 
| 180 | 
            -
                      const rightRamp = chroma.scale([center, right]).mode('lch').correctLightness(true).colors(4);
         | 
| 181 | 
            -
                      return [leftRamp[0], leftRamp[1], leftRamp[2], rightRamp[1], rightRamp[2], rightRamp[3]];
         | 
| 182 | 
             
                  }}
         | 
| 183 | 
             
                ];
         | 
| 184 |  | 
| @@ -186,6 +211,13 @@ | |
| 186 | 
             
                  try { return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); } catch { return ''; }
         | 
| 187 | 
             
                };
         | 
| 188 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 189 | 
             
                const render = () => {
         | 
| 190 | 
             
                  const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
         | 
| 191 | 
             
                  const root = mount && mount.closest('.palettes') ? mount.closest('.palettes') : document.querySelector('.palettes');
         | 
| @@ -195,9 +227,10 @@ | |
| 195 | 
             
                  grid.innerHTML = '';
         | 
| 196 | 
             
                  const css = getCssPrimary();
         | 
| 197 | 
             
                  const baseHex = css && /^#|rgb|hsl/i.test(css) ? chroma(css).hex() : '#E889AB';
         | 
|  | |
| 198 |  | 
| 199 | 
             
                  const html = cards.map((c) => {
         | 
| 200 | 
            -
                    const colors = c.generator(baseHex).slice(0,  | 
| 201 | 
             
                    const swatches = colors.map(col => `<div class="sw" style="background:${col}"></div>`).join('');
         | 
| 202 | 
             
                    return `
         | 
| 203 | 
             
                      <div class="palette-card" data-colors="${colors.join(',')}">
         | 
| @@ -211,14 +244,14 @@ | |
| 211 | 
             
                          </button>
         | 
| 212 | 
             
                        </div>
         | 
| 213 | 
             
                        <div class="palette-card__actions"></div>
         | 
| 214 | 
            -
                        <div class="palette-card__swatches">${swatches}</div>
         | 
| 215 | 
             
                      </div>
         | 
| 216 | 
             
                    `;
         | 
| 217 | 
             
                  }).join('');
         | 
| 218 | 
             
                  grid.innerHTML = html;
         | 
| 219 | 
             
                  };
         | 
| 220 |  | 
| 221 | 
            -
                const MODE_TO_CLASS = {  | 
| 222 | 
             
                const CLEAR_CLASSES = Object.values(MODE_TO_CLASS);
         | 
| 223 | 
             
                const clearCbClasses = () => {
         | 
| 224 | 
             
                  const rootEl = document.documentElement;
         | 
| @@ -241,6 +274,15 @@ | |
| 241 | 
             
                  select.addEventListener('change', () => applyCbClass(select.value));
         | 
| 242 | 
             
                };
         | 
| 243 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 244 | 
             
                let copyDelegationSetup = false;
         | 
| 245 | 
             
                const setupCopyDelegation = () => {
         | 
| 246 | 
             
                  if (copyDelegationSetup) return;
         | 
| @@ -268,8 +310,9 @@ | |
| 268 | 
             
                };
         | 
| 269 |  | 
| 270 | 
             
                const bootstrap = () => {
         | 
| 271 | 
            -
                  render();
         | 
| 272 | 
             
                  setupCbSim();
         | 
|  | |
|  | |
| 273 | 
             
                  setupCopyDelegation();
         | 
| 274 | 
             
                  const mo = new MutationObserver(() => render());
         | 
| 275 | 
             
                  mo.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
         | 
|  | |
| 15 | 
             
                .palettes .copy-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } */
         | 
| 16 | 
             
                .palettes .copy-btn svg { width: 18px; height: 18px; fill: currentColor; display: block; }
         | 
| 17 | 
             
                /* Simulation UI */
         | 
| 18 | 
            +
                .palettes .palettes__select { width: 100%; max-width: 100%; border: 1px solid var(--border-color); background: var(--surface-bg); color: var(--text-color); padding: 8px 10px; border-radius: 8px; }
         | 
| 19 | 
             
                .palettes .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 1px, 1px); white-space: nowrap; border: 0; }
         | 
| 20 | 
            +
                .palettes .palettes__controls { display: flex; flex-wrap: nowrap; gap: 16px; align-items: center; margin: 8px 0 14px; }
         | 
| 21 | 
            +
                .palettes .palettes__field { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 0 0 50%; max-width: 50%; }
         | 
| 22 | 
            +
                .palettes .palettes__label { font-size: 12px; color: var(--muted-color); font-weight: 800; }
         | 
| 23 | 
            +
                .palettes .palettes__count { display: flex; align-items: center; gap: 8px; max-width: 100%; }
         | 
| 24 | 
            +
                .palettes .palettes__count input[type="range"] { width: 100%; }
         | 
| 25 | 
            +
                .palettes .palettes__count output { min-width: 28px; text-align: center; font-variant-numeric: tabular-nums; font-size: 12px; color: var(--muted-color); }
         | 
| 26 | 
            +
                /* Slider styling */
         | 
| 27 | 
            +
                .palettes input[type="range"] { -webkit-appearance: none; appearance: none; height: 24px; background: transparent; cursor: pointer; accent-color: var(--primary-color); }
         | 
| 28 | 
            +
                .palettes input[type="range"]:focus { outline: none; }
         | 
| 29 | 
            +
                /* WebKit */
         | 
| 30 | 
            +
                .palettes input[type="range"]::-webkit-slider-runnable-track { height: 6px; background: var(--border-color); border-radius: 999px; }
         | 
| 31 | 
            +
                .palettes input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -6px; width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,.15); }
         | 
| 32 | 
            +
                /* Firefox */
         | 
| 33 | 
            +
                .palettes input[type="range"]::-moz-range-track { height: 6px; background: var(--border-color); border: none; border-radius: 999px; }
         | 
| 34 | 
            +
                .palettes input[type="range"]::-moz-range-progress { height: 6px; background: var(--primary-color); border-radius: 999px; }
         | 
| 35 | 
            +
                .palettes input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,.15); }
         | 
| 36 | 
             
                /* Page-wide color vision simulation classes */
         | 
| 37 | 
             
                html.cb-grayscale, body.cb-grayscale { filter: grayscale(1) !important; }
         | 
| 38 | 
             
                html.cb-protanopia, body.cb-protanopia { filter: url(#cb-protanopia) !important; }
         | 
|  | |
| 44 | 
             
                  .palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr));  }
         | 
| 45 | 
             
                  .palettes .palette-card__content { border-right: none; padding-right: 0; }
         | 
| 46 | 
             
                  .palettes .palette-card__actions { justify-self: start; }
         | 
| 47 | 
            +
                  
         | 
| 48 | 
             
                }
         | 
| 49 | 
             
              </style>
         | 
| 50 | 
            +
              <div class="palettes__controls">
         | 
| 51 | 
            +
                <div class="palettes__field">
         | 
| 52 | 
            +
                  <label class="palettes__label" for="cb-select">Color vision simulation</label>
         | 
| 53 | 
            +
                  <select id="cb-select" class="palettes__select">
         | 
| 54 | 
            +
                    <option value="none">Normal color vision — typical for most people</option>
         | 
| 55 | 
            +
                    <option value="achromatopsia">Achromatopsia — no color at all</option>
         | 
| 56 | 
            +
                    <option value="protanopia">Protanopia — reduced/absent reds</option>
         | 
| 57 | 
            +
                    <option value="deuteranopia">Deuteranopia — reduced/absent greens</option>
         | 
| 58 | 
            +
                    <option value="tritanopia">Tritanopia — reduced/absent blues</option>
         | 
| 59 | 
            +
                  </select>
         | 
| 60 | 
            +
                </div>
         | 
| 61 | 
            +
                <div class="palettes__field">
         | 
| 62 | 
            +
                  <label class="palettes__label" for="color-count">Number of colors</label>
         | 
| 63 | 
            +
                  <div class="palettes__count">
         | 
| 64 | 
            +
                    <input id="color-count" type="range" min="6" max="10" step="1" value="6" aria-label="Number of colors" />
         | 
| 65 | 
            +
                    <output id="color-count-out" for="color-count">6</output>
         | 
| 66 | 
            +
                  </div>
         | 
| 67 | 
            +
                </div>
         | 
| 68 | 
            +
              </div>
         | 
| 69 | 
             
              <div class="palettes__grid"></div>
         | 
| 70 | 
             
              <div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
         | 
| 71 | 
             
                <br/>
         | 
| 72 | 
             
                <p ><strong>Use color with care.</strong> Color should rarely be the only channel of meaning. Always pair it with text, icons, shape or position. The simulation below helps you spot palettes and states that become indistinguishable for people with color‑vision deficiencies. Toggle modes while checking charts, legends and interactions to ensure sufficient contrast and redundant cues.</p>
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 73 | 
             
                <!-- Hidden SVG filters used by the page-wide simulation classes -->
         | 
| 74 | 
             
                <svg aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute; left:-9999px; overflow:hidden;">
         | 
| 75 | 
             
                  <defs>
         | 
|  | |
| 111 | 
             
                  };
         | 
| 112 |  | 
| 113 | 
             
                const cards = [
         | 
| 114 | 
            +
                  { key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors.', generator: (baseHex, count) => {
         | 
| 115 | 
             
                      const base = chroma(baseHex);
         | 
| 116 | 
             
                      const lc = base.lch();
         | 
| 117 | 
             
                      const baseH = base.get('hsl.h') || 0;
         | 
|  | |
| 143 | 
             
                      // Base en premier
         | 
| 144 | 
             
                      pushHex(base);
         | 
| 145 |  | 
| 146 | 
            +
                      const total = Math.max(6, Math.min(10, count || 6));
         | 
| 147 | 
            +
                      const hueStep = 360 / total;
         | 
| 148 | 
            +
                      const hueOffsets = [0, 18, -18, 36, -36, 54, -54, 72, -72];
         | 
| 149 | 
             
                      const lVariants = [L0, Math.max(40, L0 - 6), Math.min(85, L0 + 6)];
         | 
| 150 |  | 
| 151 | 
            +
                      for (let idx = 1; results.length < total && idx < total + 12; idx++) {
         | 
| 152 | 
            +
                        let stepHue = (baseH + idx * hueStep) % 360;
         | 
| 153 | 
             
                        let accepted = false;
         | 
| 154 | 
             
                        for (let li = 0; li < lVariants.length && !accepted; li++) {
         | 
| 155 | 
             
                          for (let oi = 0; oi < hueOffsets.length && !accepted; oi++) {
         | 
| 156 | 
            +
                            const h = (stepHue + hueOffsets[oi] + 360) % 360;
         | 
| 157 | 
            +
                            const col = makeSafe(h, lVariants[li], C0);
         | 
| 158 | 
             
                            const hex = col.hex();
         | 
| 159 | 
             
                            if (!seen.has(hex.toLowerCase()) && isFarEnough(hex)) {
         | 
| 160 | 
             
                              pushHex(col);
         | 
|  | |
| 163 | 
             
                          }
         | 
| 164 | 
             
                        }
         | 
| 165 | 
             
                        if (!accepted) {
         | 
| 166 | 
            +
                          // Réduction de chroma si nécessaire
         | 
| 167 | 
             
                          let cTry = C0 - 10;
         | 
|  | |
| 168 | 
             
                          let trials = 0;
         | 
| 169 | 
             
                          while (!accepted && cTry >= 30 && trials < 6) {
         | 
| 170 | 
            +
                            const col = makeSafe(stepHue, L0, cTry);
         | 
| 171 | 
             
                            const hex = col.hex();
         | 
| 172 | 
             
                            if (!seen.has(hex.toLowerCase()) && isFarEnough(hex)) {
         | 
| 173 | 
             
                              pushHex(col);
         | 
|  | |
| 177 | 
             
                            cTry -= 5;
         | 
| 178 | 
             
                            trials++;
         | 
| 179 | 
             
                          }
         | 
|  | |
| 180 | 
             
                          if (!accepted) {
         | 
| 181 | 
             
                            let bestHex = null; let bestMin = -1;
         | 
| 182 | 
             
                            hueOffsets.forEach(off => {
         | 
| 183 | 
            +
                              const hh = (stepHue + off + 360) % 360;
         | 
| 184 | 
             
                              const cand = makeSafe(hh, L0, C0).hex();
         | 
| 185 | 
             
                              const minD = results.reduce((m, prev) => Math.min(m, chroma.distance(cand, prev, 'lab')), Infinity);
         | 
| 186 | 
             
                              if (minD > bestMin && !seen.has(cand.toLowerCase())) { bestMin = minD; bestHex = cand; }
         | 
|  | |
| 188 | 
             
                            if (bestHex) { seen.add(bestHex.toLowerCase()); results.push(bestHex); }
         | 
| 189 | 
             
                          }
         | 
| 190 | 
             
                        }
         | 
| 191 | 
            +
                      }
         | 
| 192 |  | 
| 193 | 
            +
                      return results.slice(0, total);
         | 
| 194 | 
             
                  }},
         | 
| 195 | 
            +
                  { key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.', generator: (baseHex, count) => {
         | 
| 196 | 
            +
                      const total = Math.max(6, Math.min(10, count || 6));
         | 
| 197 | 
             
                      const c = chroma(baseHex).saturate(0.3);
         | 
| 198 | 
            +
                      return chroma.scale([c.darken(2), c, c.brighten(2)]).mode('lab').correctLightness(true).colors(total);
         | 
| 199 | 
             
                  }},
         | 
| 200 | 
            +
                  { key: 'diverging', title: 'Diverging', desc: 'For <strong>centered ranges</strong> with <strong>two extremes</strong> around a <strong>baseline</strong>. (e.g., negatives/positives)', generator: (baseHex, count) => {
         | 
| 201 | 
            +
                      const total = Math.max(6, Math.min(10, count || 6));
         | 
| 202 | 
             
                      const baseH = chroma(baseHex).get('hsl.h');
         | 
| 203 | 
             
                      const compH = (baseH + 180) % 360;
         | 
| 204 | 
             
                      const left = chroma.hsl(baseH, 0.75, 0.55);
         | 
| 205 | 
             
                      const right = chroma.hsl(compH, 0.75, 0.55);
         | 
| 206 | 
            +
                      return chroma.scale([left, '#ffffff', right]).mode('lch').correctLightness(true).colors(total);
         | 
|  | |
|  | |
|  | |
| 207 | 
             
                  }}
         | 
| 208 | 
             
                ];
         | 
| 209 |  | 
|  | |
| 211 | 
             
                  try { return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); } catch { return ''; }
         | 
| 212 | 
             
                };
         | 
| 213 |  | 
| 214 | 
            +
                const getDesiredCount = () => {
         | 
| 215 | 
            +
                  const input = document.getElementById('color-count');
         | 
| 216 | 
            +
                  let v = input ? parseInt(input.value, 10) : 6;
         | 
| 217 | 
            +
                  if (Number.isNaN(v)) v = 6;
         | 
| 218 | 
            +
                  return Math.max(6, Math.min(10, v));
         | 
| 219 | 
            +
                };
         | 
| 220 | 
            +
             | 
| 221 | 
             
                const render = () => {
         | 
| 222 | 
             
                  const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
         | 
| 223 | 
             
                  const root = mount && mount.closest('.palettes') ? mount.closest('.palettes') : document.querySelector('.palettes');
         | 
|  | |
| 227 | 
             
                  grid.innerHTML = '';
         | 
| 228 | 
             
                  const css = getCssPrimary();
         | 
| 229 | 
             
                  const baseHex = css && /^#|rgb|hsl/i.test(css) ? chroma(css).hex() : '#E889AB';
         | 
| 230 | 
            +
                  const count = getDesiredCount();
         | 
| 231 |  | 
| 232 | 
             
                  const html = cards.map((c) => {
         | 
| 233 | 
            +
                    const colors = c.generator(baseHex, count).slice(0, count);
         | 
| 234 | 
             
                    const swatches = colors.map(col => `<div class="sw" style="background:${col}"></div>`).join('');
         | 
| 235 | 
             
                    return `
         | 
| 236 | 
             
                      <div class="palette-card" data-colors="${colors.join(',')}">
         | 
|  | |
| 244 | 
             
                          </button>
         | 
| 245 | 
             
                        </div>
         | 
| 246 | 
             
                        <div class="palette-card__actions"></div>
         | 
| 247 | 
            +
                        <div class="palette-card__swatches" style="grid-template-columns: repeat(${colors.length}, minmax(0, 1fr));">${swatches}</div>
         | 
| 248 | 
             
                      </div>
         | 
| 249 | 
             
                    `;
         | 
| 250 | 
             
                  }).join('');
         | 
| 251 | 
             
                  grid.innerHTML = html;
         | 
| 252 | 
             
                  };
         | 
| 253 |  | 
| 254 | 
            +
                const MODE_TO_CLASS = { protanopia: 'cb-protanopia', deuteranopia: 'cb-deuteranopia', tritanopia: 'cb-tritanopia', achromatopsia: 'cb-achromatopsia' };
         | 
| 255 | 
             
                const CLEAR_CLASSES = Object.values(MODE_TO_CLASS);
         | 
| 256 | 
             
                const clearCbClasses = () => {
         | 
| 257 | 
             
                  const rootEl = document.documentElement;
         | 
|  | |
| 274 | 
             
                  select.addEventListener('change', () => applyCbClass(select.value));
         | 
| 275 | 
             
                };
         | 
| 276 |  | 
| 277 | 
            +
                const setupCountControl = () => {
         | 
| 278 | 
            +
                  const input = document.getElementById('color-count');
         | 
| 279 | 
            +
                  const out = document.getElementById('color-count-out');
         | 
| 280 | 
            +
                  if (!input) return;
         | 
| 281 | 
            +
                  const sync = () => { if (out) out.textContent = String(getDesiredCount()); render(); };
         | 
| 282 | 
            +
                  input.addEventListener('input', sync);
         | 
| 283 | 
            +
                  try { if (out) out.textContent = String(getDesiredCount()); } catch {}
         | 
| 284 | 
            +
                };
         | 
| 285 | 
            +
             | 
| 286 | 
             
                let copyDelegationSetup = false;
         | 
| 287 | 
             
                const setupCopyDelegation = () => {
         | 
| 288 | 
             
                  if (copyDelegationSetup) return;
         | 
|  | |
| 310 | 
             
                };
         | 
| 311 |  | 
| 312 | 
             
                const bootstrap = () => {
         | 
|  | |
| 313 | 
             
                  setupCbSim();
         | 
| 314 | 
            +
                  setupCountControl();
         | 
| 315 | 
            +
                  render();
         | 
| 316 | 
             
                  setupCopyDelegation();
         | 
| 317 | 
             
                  const mo = new MutationObserver(() => render());
         | 
| 318 | 
             
                  mo.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
         | 
    	
        app/src/env.d.ts
    DELETED
    
    | @@ -1,9 +0,0 @@ | |
| 1 | 
            -
            /// <reference path="../.astro/types.d.ts" />
         | 
| 2 | 
            -
            /// <reference types="vite/client" />
         | 
| 3 | 
            -
             | 
| 4 | 
            -
            declare module '*.png?url' {
         | 
| 5 | 
            -
              const src: string;
         | 
| 6 | 
            -
              export default src;
         | 
| 7 | 
            -
            }
         | 
| 8 | 
            -
             | 
| 9 | 
            -
            // (Global window typings for Plotly/D3 are intentionally omitted; components handle typing inline.)
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
 
			
