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/[email protected]/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.)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|