thibaud frere commited on
Commit
52307d3
·
1 Parent(s): db576f5
Dockerfile CHANGED
@@ -14,6 +14,10 @@ RUN npm install
14
  # Copy the rest of the application code
15
  COPY app/ .
16
 
 
 
 
 
17
  # Build the application
18
  RUN npm run build
19
 
 
14
  # Copy the rest of the application code
15
  COPY app/ .
16
 
17
+ # Ensure public/data exists inside the container (symlinks won't resolve at build time)
18
+ RUN mkdir -p public/data && \
19
+ cp -a src/content/assets/data/. public/data/
20
+
21
  # Build the application
22
  RUN npm run build
23
 
app/.astro/astro/content.d.ts CHANGED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": {
189
+ "data/mnist-variant-model": {
190
+ id: "data/mnist-variant-model";
191
+ collection: "assets";
192
+ data: any
193
+ };
194
+ };
195
+ "embeds": Record<string, {
196
+ id: string;
197
+ collection: "embeds";
198
+ data: any;
199
+ }>;
200
+
201
+ };
202
+
203
+ type AnyEntryMap = ContentEntryMap & DataEntryMap;
204
+
205
+ export type ContentConfig = never;
206
+ }
app/scripts/export-pdf.mjs CHANGED
@@ -165,7 +165,7 @@ async function main() {
165
  const margin = parseMargin(args.margin);
166
  const wait = (args.wait || 'full'); // 'networkidle' | 'images' | 'plotly' | 'full'
167
 
168
- // filename can be provided, else computed from page title later
169
  let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || 'article';
170
 
171
  console.log('> Building Astro site…');
@@ -204,14 +204,24 @@ async function main() {
204
  // Give time for CDN scripts (Plotly/D3) to attach and for our fragment hooks to run
205
  try { await page.waitForFunction(() => !!window.Plotly, { timeout: 8000 }); } catch {}
206
  try { await page.waitForFunction(() => !!window.d3, { timeout: 8000 }); } catch {}
207
- // Compute slug from title if needed
208
  if (!args.filename) {
209
- const title = await page.evaluate(() => {
210
- const h1 = document.querySelector('h1.hero-title');
211
- const t = h1 ? h1.textContent : document.title;
212
- return (t || '').replace(/\s+/g, ' ').trim();
213
  });
214
- outFileBase = slugify(title);
 
 
 
 
 
 
 
 
 
 
215
  }
216
 
217
  // Wait for render readiness
 
165
  const margin = parseMargin(args.margin);
166
  const wait = (args.wait || 'full'); // 'networkidle' | 'images' | 'plotly' | 'full'
167
 
168
+ // filename can be provided, else computed from DOM (button) or page title later
169
  let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || 'article';
170
 
171
  console.log('> Building Astro site…');
 
204
  // Give time for CDN scripts (Plotly/D3) to attach and for our fragment hooks to run
205
  try { await page.waitForFunction(() => !!window.Plotly, { timeout: 8000 }); } catch {}
206
  try { await page.waitForFunction(() => !!window.d3, { timeout: 8000 }); } catch {}
207
+ // Prefer explicit filename from the download button if present
208
  if (!args.filename) {
209
+ const fromBtn = await page.evaluate(() => {
210
+ const btn = document.getElementById('download-pdf-btn');
211
+ const f = btn ? btn.getAttribute('data-pdf-filename') : null;
212
+ return f || '';
213
  });
214
+ if (fromBtn) {
215
+ outFileBase = String(fromBtn).replace(/\.pdf$/i, '');
216
+ } else {
217
+ // Fallback: compute slug from hero title or document.title
218
+ const title = await page.evaluate(() => {
219
+ const h1 = document.querySelector('h1.hero-title');
220
+ const t = h1 ? h1.textContent : document.title;
221
+ return (t || '').replace(/\s+/g, ' ').trim();
222
+ });
223
+ outFileBase = slugify(title);
224
+ }
225
  }
226
 
227
  // Wait for render readiness
app/src/components/HtmlEmbed.astro CHANGED
@@ -92,6 +92,15 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
92
  @media (prefers-color-scheme: dark) {
93
  [data-theme="dark"] .html-embed__card:not(.is-frameless) { background: #12151b; border-color: rgba(255,255,255,.15); }
94
  }
 
 
 
 
 
 
 
 
 
95
  @media print {
96
  .html-embed, .html-embed__card { break-inside: avoid; page-break-inside: avoid; }
97
  }
 
92
  @media (prefers-color-scheme: dark) {
93
  [data-theme="dark"] .html-embed__card:not(.is-frameless) { background: #12151b; border-color: rgba(255,255,255,.15); }
94
  }
95
+ @media print {
96
+ .html-embed, .html-embed__card { max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; }
97
+ .html-embed__card { padding: 6px; }
98
+ .html-embed__card.is-frameless { padding: 0; }
99
+ .html-embed__card svg,
100
+ .html-embed__card canvas,
101
+ .html-embed__card img { max-width: 100% !important; height: auto !important; }
102
+ .html-embed__card > div[id^="frag-"] { width: 100% !important; }
103
+ }
104
  @media print {
105
  .html-embed, .html-embed__card { break-inside: avoid; page-break-inside: avoid; }
106
  }
app/src/content/article.mdx CHANGED
@@ -30,15 +30,13 @@ import AvailableBlocks from "./chapters/available-blocks.mdx";
30
  import GettingStarted from "./chapters/getting-started.mdx";
31
 
32
  <Sidenote>
33
- Welcome to this single-page research article template.
34
- It’s designed to help you write clear, modern, and **interactive** technical articles with **minimal setup**.
35
- Whether you cover machine learning, data science, physics, or software topics, this template keeps the authoring flow simple while offering robust features out of the box.
36
  <Fragment slot="aside">
37
  Reading time: 20–25 minutes.
38
  </Fragment>
39
- In this guide, you’ll learn how to install the template,
40
- write content (math, citations, images, code, sidenotes, interactive fragments),
41
- customize styles and behavior, and follow a few **best practices** for publishing.
42
 
43
  </Sidenote>
44
 
@@ -72,15 +70,13 @@ import GettingStarted from "./chapters/getting-started.mdx";
72
  </Sidenote>
73
 
74
  ## Introduction
75
- The web enables forms of explanation that static PDFs cannot: **reactive diagrams**, progressive notation, and **exploratory views** that reveal how ideas behave. Use **interactive fragments** to let readers hover, scrub, and inspectbuilding **intuition**, not just reading results.
76
-
77
- Careful notation, **well‑chosen visual encodings**, and **small interactive experiments** deepen understanding. By making these artifacts **first‑class**—alongside text, math, and code—this template helps your audience grasp mechanisms, limits, and trade‑offs.
78
 
79
- Clear, **inspectable examples** make methods safer and more comprehensible. Embed live widgets, reveal **intermediate states**, and link to sources so readers can verify claims and **reproduce results**.
80
 
81
- Not every contribution fits a PDF. Treat demos, visualizations, and interactive write‑ups as **real scholarship**: cite them, version them, and ship them together.
82
 
83
- This project is heavely inspired by [**Distill**](https://distill.pub) (2016–2021), which championed clear, web‑native scholarship.
84
 
85
 
86
  {/* ### Notable examples of excellent scientific articles
@@ -103,8 +99,3 @@ A short, curated list of well‑designed and often interactive work:
103
 
104
  <BestPractices />
105
 
106
-
107
- ## Conclusion
108
-
109
- This template provides a **practical baseline** for writing and sharing technical articles with **math**, **citations**, and **interactive figures**. **Start simple**, **iterate** on structure and style, and keep content **maintainable** for future readers and collaborators.
110
-
 
30
  import GettingStarted from "./chapters/getting-started.mdx";
31
 
32
  <Sidenote>
33
+ Welcome to this singlepage research article template. It helps you publish **clear**, **modern**, and **interactive** technical writing with **minimal setup**. Grounded in **web‑native scholarship**, it favors **interactive explanations**, careful notation, and **inspectable examples** over static snapshots.
34
+
35
+ It offers a **ready‑to‑publish, all‑in‑one workflow** so you can **focus on ideas** rather than infrastructure.
36
  <Fragment slot="aside">
37
  Reading time: 20–25 minutes.
38
  </Fragment>
39
+ Use it as a **practical baseline**: **start simple**, **iterate** on structure and style, and keep content **maintainable** for future readers and collaborators.
 
 
40
 
41
  </Sidenote>
42
 
 
70
  </Sidenote>
71
 
72
  ## Introduction
73
+ The web enables explanations that static PDFs cannot: **reactive diagrams**, **progressive notation**, and **exploratory views** that show how ideas behave. Use **interactive fragments** so readers can **hover**, **scrub**, and **inspect**—building **intuition**, not just reading results.
 
 
74
 
75
+ Careful notation, **thoughtful visual encodings**, and **small, manipulable experiments** deepen understanding. By making these artifacts **first‑class** alongside **text, math, and code**, this template helps readers grasp **mechanisms, limits, and trade‑offs**.
76
 
77
+ Not every contribution fits a PDF. Treat demos, visualizations, and interactive write‑ups as **scholarship**: **cite** them, **version** them, and **ship** them with clear, **inspectable examples** that expose **intermediate states** and link to sources so readers can **verify** claims and **reproduce** results.
78
 
79
+ This project is heavily inspired by [**Distill**](https://distill.pub) (2016–2021), which championed clear, web‑native scholarship.
80
 
81
 
82
  {/* ### Notable examples of excellent scientific articles
 
99
 
100
  <BestPractices />
101
 
 
 
 
 
 
app/src/content/assets/data/{finevision.csv → vision.csv} RENAMED
File without changes
app/src/content/assets/images/placeholder-wide.png ADDED

Git LFS Details

  • SHA256: 200daf8ade0c7f035d883fefa9a12d6ba7cca504b1d5571774748c3c90639103
  • Pointer size: 130 Bytes
  • Size of remote file: 34.6 kB
app/src/content/chapters/getting-started.mdx CHANGED
@@ -12,7 +12,7 @@ cd app
12
  npm install
13
  ```
14
  <Fragment slot="aside">
15
- You can also use yarn.
16
  </Fragment>
17
  </Sidenote>
18
 
@@ -28,9 +28,8 @@ npm run dev
28
  ```bash
29
  npm run build
30
  ```
31
-
32
- Serving the `dist/` directory on any static host is enough to deliver the site.
33
-
34
  A [slug-title].pdf and thumb.jpg are also generated at build time. You can find them in the public folder.
35
 
36
  ### Deploy
 
12
  npm install
13
  ```
14
  <Fragment slot="aside">
15
+ Alternatively, you can use **Yarn** as your package manager.
16
  </Fragment>
17
  </Sidenote>
18
 
 
28
  ```bash
29
  npm run build
30
  ```
31
+ <small className="muted">Serving the `dist/` directory on any static host is enough to deliver the site.</small>
32
+ <br/><br/>
 
33
  A [slug-title].pdf and thumb.jpg are also generated at build time. You can find them in the public folder.
34
 
35
  ### Deploy
app/src/content/chapters/writing-your-content.mdx CHANGED
@@ -36,7 +36,8 @@ published: "Feb 19, 2025"
36
  tags:
37
  - research
38
  - template
39
- ogImage: "https://example.com/your-og-image.png"
 
40
  ---
41
 
42
  {/* IMPORTS */}
@@ -121,19 +122,10 @@ Use the **color picker** below to see how the primary color affects the theme.
121
  Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
122
 
123
  <HtmlEmbed frameless src="palettes.html" />
124
-
125
-
126
- ### Embeds
127
-
128
- Use HTML fragments to embed interactive charts and widgets. Place your fragments under `app/src/content/embeds/` and reference them via the `HtmlEmbed` component.
129
-
130
- <small className="muted">Example</small>
131
- ```mdx
132
- import HtmlEmbed from '../components/HtmlEmbed.astro'
133
-
134
- <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
135
- ```
136
-
137
 
138
  ### Placement
139
 
 
36
  tags:
37
  - research
38
  - template
39
+ {/* Optional override of the default Open Graph image */}
40
+ ogImage: "https://override-example.com/your-og-image.png"
41
  ---
42
 
43
  {/* IMPORTS */}
 
122
  Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
123
 
124
  <HtmlEmbed frameless src="palettes.html" />
125
+ <br/>
126
+ **Use color with care.**
127
+ Color should rarely be the only channel of meaning.
128
+ Always pair it with text, icons, shape or position. The simulation helps you spot palettes and states that become indistinguishable for people with color‑vision deficiencies.
 
 
 
 
 
 
 
 
 
129
 
130
  ### Placement
131
 
app/src/content/embeds/d3-line-old.html DELETED
File without changes
app/src/content/embeds/d3-pie.html CHANGED
@@ -54,7 +54,7 @@
54
 
55
  // CSV: load from public path
56
  const CSV_PATHS = [
57
- '/data/finevision.csv'
58
  ];
59
 
60
  const fetchFirstAvailable = async (paths) => {
@@ -64,7 +64,7 @@
64
  if (res.ok) { return await res.text(); }
65
  } catch (_) { /* try next */ }
66
  }
67
- throw new Error('CSV not found: finevision.csv');
68
  };
69
 
70
  const parseCsv = (text) => d3.csvParse(text, (d) => ({
 
54
 
55
  // CSV: load from public path
56
  const CSV_PATHS = [
57
+ '/data/vision.csv'
58
  ];
59
 
60
  const fetchFirstAvailable = async (paths) => {
 
64
  if (res.ok) { return await res.text(); }
65
  } catch (_) { /* try next */ }
66
  }
67
+ throw new Error('CSV not found: vision.csv');
68
  };
69
 
70
  const parseCsv = (text) => d3.csvParse(text, (d) => ({
app/src/content/embeds/d3-scatter.html DELETED
@@ -1,160 +0,0 @@
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/original_embeds/d3js/banner.html DELETED
@@ -1,244 +0,0 @@
1
- <div class="d3-galaxy" style="width:100%;margin:10px 0;"></div>
2
- <script>
3
- (() => {
4
- const ensureD3 = (cb) => {
5
- if (window.d3 && typeof window.d3.select === 'function') return cb();
6
- let s = document.getElementById('d3-cdn-script');
7
- if (!s) {
8
- s = document.createElement('script');
9
- s.id = 'd3-cdn-script';
10
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
11
- document.head.appendChild(s);
12
- }
13
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
14
- s.addEventListener('load', onReady, { once: true });
15
- if (window.d3) onReady();
16
- };
17
-
18
- const bootstrap = () => {
19
- const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
20
- const container = (mount && mount.querySelector && mount.querySelector('.d3-galaxy')) || document.querySelector('.d3-galaxy');
21
- if (!container) return;
22
- if (container.dataset) {
23
- if (container.dataset.mounted === 'true') return;
24
- container.dataset.mounted = 'true';
25
- }
26
- // Scene params (match previous Plotly ranges)
27
- const cx = 1.5, cy = 0.5;
28
- const a = 1.3, b = 0.45;
29
- const numPoints = 3000;
30
- const numArms = 3;
31
- const numTurns = 2.1;
32
- const angleJitter = 0.12;
33
- const posNoise = 0.015;
34
-
35
- // Generate spiral + bulge
36
- const twoPi = Math.PI * 2;
37
- const t = Float64Array.from({ length: numPoints }, () => Math.random() * (twoPi * numTurns));
38
- const armIndices = Int16Array.from({ length: numPoints }, () => Math.floor(Math.random() * numArms));
39
- const armOffsets = Float64Array.from(armIndices, (k) => k * (twoPi / numArms));
40
- const theta = Float64Array.from(t, (tv, i) => tv + armOffsets[i] + d3.randomNormal.source(Math.random)(0, angleJitter)());
41
- const rNorm = Float64Array.from(t, (tv) => Math.pow(tv / (twoPi * numTurns), 0.9));
42
- const noiseScale = (rn) => posNoise * (0.8 + 0.6 * rn);
43
- const noiseX = Float64Array.from(rNorm, (rn) => d3.randomNormal.source(Math.random)(0, noiseScale(rn))());
44
- const noiseY = Float64Array.from(rNorm, (rn) => d3.randomNormal.source(Math.random)(0, noiseScale(rn))());
45
-
46
- const xSpiral = Float64Array.from(theta, (th, i) => cx + a * rNorm[i] * Math.cos(th) + noiseX[i]);
47
- const ySpiral = Float64Array.from(theta, (th, i) => cy + b * rNorm[i] * Math.sin(th) + noiseY[i]);
48
-
49
- const bulgePoints = Math.floor(0.18 * numPoints);
50
- const phiB = Float64Array.from({ length: bulgePoints }, () => twoPi * Math.random());
51
- const rB = Float64Array.from({ length: bulgePoints }, () => Math.pow(Math.random(), 2.2) * 0.22);
52
- const noiseXB = Float64Array.from({ length: bulgePoints }, () => d3.randomNormal.source(Math.random)(0, posNoise * 0.6)());
53
- const noiseYB = Float64Array.from({ length: bulgePoints }, () => d3.randomNormal.source(Math.random)(0, posNoise * 0.6)());
54
- const xBulge = Float64Array.from(phiB, (ph, i) => cx + a * rB[i] * Math.cos(ph) + noiseXB[i]);
55
- const yBulge = Float64Array.from(phiB, (ph, i) => cy + b * rB[i] * Math.sin(ph) + noiseYB[i]);
56
-
57
- // Concatenate
58
- const X = Array.from(xSpiral).concat(Array.from(xBulge));
59
- const Y = Array.from(ySpiral).concat(Array.from(yBulge));
60
- const lenSpiral = xSpiral.length;
61
-
62
- const zSpiral = Array.from(rNorm, (rn) => 1 - rn);
63
- const maxRB = rB && rB.length ? (window.d3 && d3.max ? d3.max(rB) : Math.max.apply(null, Array.from(rB))) : 1;
64
- const zBulge = Array.from(rB, (rb) => 1 - (maxRB ? rb / maxRB : 0));
65
- const Zraw = zSpiral.concat(zBulge);
66
- const sizesPx = Zraw.map((z) => (z + 1) * 5); // 5..10 px (diameter)
67
-
68
- // Labels (same categories as Python version)
69
- const labelOf = (i) => {
70
- const z = Zraw[i];
71
- if (z < 0.25) return 'smol dot';
72
- if (z < 0.5) return 'ok-ish dot';
73
- if (z < 0.75) return 'a dot';
74
- return 'biiig dot';
75
- };
76
-
77
- // Sort by size ascending for z-index: small first, big last
78
- const idx = d3.range(X.length).sort((i, j) => sizesPx[i] - sizesPx[j]);
79
-
80
- // Colors: piecewise gradient [0 -> 0.5 -> 1]
81
- const c0 = d3.rgb(78, 165, 183); // rgb(78, 165, 183)
82
- const c1 = d3.rgb(206, 192, 250); // rgb(206, 192, 250)
83
- const c2 = d3.rgb(232, 137, 171); // rgb(232, 137, 171)
84
- const interp01 = d3.interpolateRgb(c0, c1);
85
- const interp12 = d3.interpolateRgb(c1, c2);
86
- const colorFor = (v) => {
87
- const t = Math.max(0, Math.min(1, v));
88
- return t <= 0.5 ? interp01(t / 0.5) : interp12((t - 0.5) / 0.5);
89
- };
90
-
91
- // Create SVG
92
- const svg = d3.select(container).append('svg')
93
- .attr('width', '100%')
94
- .style('display', 'block');
95
-
96
- const render = () => {
97
- const width = container.clientWidth || 800;
98
- const height = Math.max(260, Math.round(width / 3)); // keep ~3:1, min height
99
- svg.attr('width', width).attr('height', height);
100
-
101
- const xScale = d3.scaleLinear().domain([0, 3]).range([0, width]);
102
- const yScale = d3.scaleLinear().domain([0, 1]).range([height, 0]);
103
-
104
- // Subtle stroke color depending on theme
105
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
106
- const strokeColor = isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)';
107
-
108
- // Background rect using gradient
109
- const bg = svg.selectAll('rect.d3-bg').data([0]);
110
- bg.join('rect')
111
- .attr('class', 'd3-bg')
112
- .attr('x', 0)
113
- .attr('y', 0)
114
- .attr('width', width)
115
- .attr('height', height)
116
- .attr('fill', 'url(#spaceBg)');
117
-
118
- // Group with blend mode so points softly accumulate light
119
- const g = svg.selectAll('g.points').data([0]).join('g').attr('class', 'points').style('mix-blend-mode', 'screen');
120
-
121
- // Ensure container can host an absolute tooltip
122
- container.style.position = container.style.position || 'relative';
123
- let tip = container.querySelector('.d3-tooltip');
124
- let tipInner;
125
- if (!tip) {
126
- tip = document.createElement('div');
127
- tip.className = 'd3-tooltip';
128
- Object.assign(tip.style, {
129
- position: 'absolute',
130
- top: '0px',
131
- left: '0px',
132
- transform: 'translate(-9999px, -9999px)',
133
- pointerEvents: 'none',
134
- padding: '8px 10px',
135
- borderRadius: '8px',
136
- fontSize: '12px',
137
- lineHeight: '1.35',
138
- border: '1px solid var(--border-color)',
139
- background: 'var(--surface-bg)',
140
- color: 'var(--text-color)',
141
- boxShadow: '0 4px 24px rgba(0,0,0,.18)',
142
- opacity: '0',
143
- transition: 'opacity .12s ease'
144
- });
145
- tipInner = document.createElement('div');
146
- tipInner.className = 'd3-tooltip__inner';
147
- tipInner.style.textAlign = 'left';
148
- tip.appendChild(tipInner);
149
- container.appendChild(tip);
150
- } else {
151
- tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
152
- }
153
-
154
- // Final filter: remove small dots very close to the galaxy center (after placement)
155
- const centerHoleRadius = 0.48; // elliptical radius threshold
156
- const smallSizeThreshold = 7.5; // same notion as Python size cut
157
- const rTotal = idx.map((i) => Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2));
158
- const idxFiltered = idx.filter((i, k) => !(rTotal[k] <= centerHoleRadius && sizesPx[i] < smallSizeThreshold));
159
-
160
- const sel = g.selectAll('circle').data(idxFiltered, (i) => i);
161
- sel.join(
162
- (enter) => enter.append('circle')
163
- .attr('cx', (i) => xScale(X[i]))
164
- .attr('cy', (i) => yScale(Y[i]))
165
- .attr('r', (i) => sizesPx[i] / 2)
166
- .attr('fill', (i) => colorFor(Zraw[i]))
167
- .attr('fill-opacity', 0.9)
168
- .attr('stroke', strokeColor)
169
- .attr('stroke-width', 0.4)
170
- .on('mouseenter', function(ev, i) {
171
- d3.select(this).raise()
172
- .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
173
- .attr('stroke-width', 1.2);
174
- const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
175
- const type = i < lenSpiral ? 'spiral' : 'bulge';
176
- const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
177
- tipInner.innerHTML = `<div><strong>${labelOf(i)}</strong></div>` +
178
- `<div><strong>Type</strong> ${type}${arm ? ` (arm ${arm})` : ''}</div>` +
179
- `<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>` +
180
- `<div><strong>X</strong> ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
181
- `<div><strong>r</strong> ${r.toFixed(3)} · <strong>z</strong> ${Zraw[i].toFixed(3)}</div>`;
182
- tip.style.opacity = '1';
183
- })
184
- .on('mousemove', (ev, i) => {
185
- const [mx, my] = d3.pointer(ev, container);
186
- const offsetX = 10, offsetY = 12;
187
- tip.style.transform = `translate(${Math.round(mx + offsetX)}px, ${Math.round(my + offsetY)}px)`;
188
- })
189
- .on('mouseleave', function() {
190
- tip.style.opacity = '0';
191
- tip.style.transform = 'translate(-9999px, -9999px)';
192
- d3.select(this).attr('stroke', strokeColor).attr('stroke-width', 0.4);
193
- }),
194
- (update) => update
195
- .attr('cx', (i) => xScale(X[i]))
196
- .attr('cy', (i) => yScale(Y[i]))
197
- .attr('r', (i) => sizesPx[i] / 2)
198
- .attr('fill', (i) => colorFor(Zraw[i]))
199
- .attr('fill-opacity', 0.9)
200
- .attr('stroke', strokeColor)
201
- .attr('stroke-width', 0.4)
202
- .on('mouseenter', function(ev, i) {
203
- d3.select(this).raise()
204
- .attr('stroke', isDark ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)')
205
- .attr('stroke-width', 1.2);
206
- const r = Math.sqrt(((X[i] - cx) / a) ** 2 + ((Y[i] - cy) / b) ** 2);
207
- const type = i < lenSpiral ? 'spiral' : 'bulge';
208
- const arm = i < lenSpiral ? (armIndices[i] + 1) : null;
209
- tipInner.innerHTML = `<div><strong>${labelOf(i)}</strong></div>` +
210
- `<div><strong>Type</strong> ${type}${arm ? ` (arm ${arm})` : ''}</div>` +
211
- `<div><strong>Size</strong> ${sizesPx[i].toFixed(1)} px</div>` +
212
- `<div><strong>X</strong> ${X[i].toFixed(2)} · <strong>Y</strong> ${Y[i].toFixed(2)}</div>` +
213
- `<div><strong>r</strong> ${r.toFixed(3)} · <strong>z</strong> ${Zraw[i].toFixed(3)}</div>`;
214
- tip.style.opacity = '1';
215
- })
216
- .on('mousemove', (ev, i) => {
217
- const [mx, my] = d3.pointer(ev, container);
218
- const offsetX = 10, offsetY = 12;
219
- tip.style.transform = `translate(${Math.round(mx + offsetX)}px, ${Math.round(my + offsetY)}px)`;
220
- })
221
- .on('mouseleave', function() {
222
- tip.style.opacity = '0';
223
- tip.style.transform = 'translate(-9999px, -9999px)';
224
- d3.select(this).attr('stroke', strokeColor).attr('stroke-width', 0.4);
225
- })
226
- );
227
- };
228
-
229
- // First render + resize
230
- if (window.ResizeObserver) {
231
- const ro = new ResizeObserver(() => render());
232
- ro.observe(container);
233
- } else {
234
- window.addEventListener('resize', render);
235
- }
236
- render();
237
- };
238
-
239
- if (document.readyState === 'loading') {
240
- document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
241
- } else { ensureD3(bootstrap); }
242
- })();
243
- </script>
244
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/original_embeds/d3js/line.html DELETED
@@ -1,356 +0,0 @@
1
- <div class="d3-line" style="width:100%;margin:10px 0;"></div>
2
- <script>
3
- (() => {
4
- const ensureD3 = (cb) => {
5
- if (window.d3 && typeof window.d3.select === 'function') return cb();
6
- let s = document.getElementById('d3-cdn-script');
7
- if (!s) {
8
- s = document.createElement('script');
9
- s.id = 'd3-cdn-script';
10
- s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
11
- document.head.appendChild(s);
12
- }
13
- const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
14
- s.addEventListener('load', onReady, { once: true });
15
- if (window.d3) onReady();
16
- };
17
-
18
- const bootstrap = () => {
19
- const mount = document.currentScript ? document.currentScript.previousElementSibling : null;
20
- const container = (mount && mount.querySelector && mount.querySelector('.d3-line')) || document.querySelector('.d3-line');
21
- if (!container) return;
22
- if (container.dataset) {
23
- if (container.dataset.mounted === 'true') return;
24
- container.dataset.mounted = 'true';
25
- }
26
-
27
- // Dataset params matching the Plotly version
28
- const datasets = [
29
- { name: 'CIFAR-10', base: { ymin:0.10, ymax:0.90, k:10.0, x0:0.55 }, aug: { ymin:0.15, ymax:0.96, k:12.0, x0:0.40 }, target: 0.97 },
30
- { name: 'CIFAR-100', base: { ymin:0.05, ymax:0.70, k: 9.5, x0:0.60 }, aug: { ymin:0.08, ymax:0.80, k:11.0, x0:0.45 }, target: 0.85 },
31
- { name: 'ImageNet-1K', base: { ymin:0.02, ymax:0.68, k: 8.5, x0:0.65 }, aug: { ymin:0.04, ymax:0.75, k: 9.5, x0:0.50 }, target: 0.82 },
32
- ];
33
-
34
- // Controls UI
35
- const controls = document.createElement('div');
36
- controls.className = 'd3-line__controls';
37
- Object.assign(controls.style, {
38
- marginTop: '12px',
39
- display: 'flex',
40
- gap: '16px',
41
- alignItems: 'center'
42
- });
43
-
44
- const labelDs = document.createElement('label');
45
- Object.assign(labelDs.style, {
46
- fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px'
47
- });
48
- labelDs.textContent = 'Dataset';
49
- const selectDs = document.createElement('select');
50
- Object.assign(selectDs.style, { fontSize: '12px', padding: '2px 6px' });
51
- datasets.forEach((d, i) => {
52
- const o = document.createElement('option');
53
- o.value = String(i);
54
- o.textContent = d.name;
55
- selectDs.appendChild(o);
56
- });
57
- labelDs.appendChild(selectDs);
58
-
59
- const labelAlpha = document.createElement('label');
60
- Object.assign(labelAlpha.style, {
61
- fontSize: '12px', color: 'rgba(0,0,0,.65)', display: 'flex', alignItems: 'center', gap: '10px', flex: '1', padding: '6px 10px'
62
- });
63
- labelAlpha.appendChild(document.createTextNode('Augmentation α'));
64
- const slider = document.createElement('input');
65
- slider.type = 'range'; slider.min = '0'; slider.max = '1'; slider.step = '0.01'; slider.value = '0.70';
66
- Object.assign(slider.style, { flex: '1' });
67
- const alphaVal = document.createElement('span'); alphaVal.className = 'alpha-value'; alphaVal.textContent = slider.value;
68
- labelAlpha.appendChild(slider);
69
- labelAlpha.appendChild(alphaVal);
70
-
71
- controls.appendChild(labelDs);
72
- controls.appendChild(labelAlpha);
73
-
74
- // Create SVG
75
- const svg = d3.select(container).append('svg')
76
- .attr('width', '100%')
77
- .style('display', 'block');
78
-
79
- // Groups
80
- const gRoot = svg.append('g');
81
- const gGrid = gRoot.append('g').attr('class', 'grid');
82
- const gAxes = gRoot.append('g').attr('class', 'axes');
83
- const gLines = gRoot.append('g').attr('class', 'lines');
84
- const gHover = gRoot.append('g').attr('class', 'hover');
85
- const gLegend = gRoot.append('foreignObject').attr('class', 'legend');
86
-
87
- // Tooltip
88
- container.style.position = container.style.position || 'relative';
89
- let tip = container.querySelector('.d3-tooltip');
90
- let tipInner;
91
- if (!tip) {
92
- tip = document.createElement('div');
93
- tip.className = 'd3-tooltip';
94
- Object.assign(tip.style, {
95
- position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
96
- padding: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
97
- background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0',
98
- transition: 'opacity .12s ease'
99
- });
100
- tipInner = document.createElement('div');
101
- tipInner.className = 'd3-tooltip__inner';
102
- tipInner.style.textAlign = 'left';
103
- tip.appendChild(tipInner);
104
- container.appendChild(tip);
105
- } else {
106
- tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
107
- }
108
-
109
- // Colors
110
- const colorBase = '#64748b'; // slate-500
111
- const colorImproved = '#2563eb'; // blue-600
112
- const colorTarget = '#4b5563'; // gray-600
113
- const legendBgLight = 'rgba(255,255,255,0.85)';
114
- const legendBgDark = 'rgba(17,17,23,0.85)';
115
-
116
- // Data and helpers
117
- const N = 240;
118
- const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
119
- const logistic = (x, { ymin, ymax, k, x0 }) => ymin + (ymax - ymin) / (1 + Math.exp(-k * (x - x0)));
120
- const blend = (l, e, a) => (1 - a) * l + a * e;
121
-
122
- let datasetIndex = 0;
123
- let alpha = parseFloat(slider.value) || 0.7;
124
-
125
- let yBase = [];
126
- let yAug = [];
127
- let yImp = [];
128
- let yTgt = [];
129
-
130
- function computeCurves() {
131
- const d = datasets[datasetIndex];
132
- yBase = xs.map((x) => logistic(x, d.base));
133
- yAug = xs.map((x) => logistic(x, d.aug));
134
- yTgt = xs.map(() => d.target);
135
- yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
136
- }
137
-
138
- // Scales and layout
139
- let width = 800, height = 360;
140
- let margin = { top: 16, right: 28, bottom: 40, left: 44 };
141
- let xScale = d3.scaleLinear();
142
- let yScale = d3.scaleLinear();
143
-
144
- // Paths
145
- const lineGen = d3.line()
146
- .curve(d3.curveCatmullRom.alpha(0.6))
147
- .x((d, i) => xScale(xs[i]))
148
- .y((d) => yScale(d));
149
-
150
- const pathBase = gLines.append('path').attr('fill', 'none').attr('stroke', colorBase).attr('stroke-width', 2);
151
- const pathImp = gLines.append('path').attr('fill', 'none').attr('stroke', colorImproved).attr('stroke-width', 2);
152
- const pathTgt = gLines.append('path').attr('fill', 'none').attr('stroke', colorTarget).attr('stroke-width', 2).attr('stroke-dasharray', '6,6');
153
-
154
- // Hover elements
155
- const hoverLine = gHover.append('line').attr('stroke-width', 1);
156
- const hoverDotB = gHover.append('circle').attr('r', 3.5).attr('fill', colorBase).attr('stroke', '#fff').attr('stroke-width', 1);
157
- const hoverDotI = gHover.append('circle').attr('r', 3.5).attr('fill', colorImproved).attr('stroke', '#fff').attr('stroke-width', 1);
158
- const hoverDotT = gHover.append('circle').attr('r', 3.5).attr('fill', colorTarget).attr('stroke', '#fff').attr('stroke-width', 1);
159
-
160
- const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
161
-
162
- function updateScales() {
163
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
164
- const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
165
- const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)';
166
- const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)';
167
-
168
- width = container.clientWidth || 800;
169
- height = Math.max(260, Math.round(width / 3));
170
- svg.attr('width', width).attr('height', height);
171
-
172
- const innerWidth = width - margin.left - margin.right;
173
- const innerHeight = height - margin.top - margin.bottom;
174
- gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
175
-
176
- xScale.domain([0, 1]).range([0, innerWidth]);
177
- yScale.domain([0, 1]).range([innerHeight, 0]);
178
-
179
- // Grid (horizontal)
180
- gGrid.selectAll('*').remove();
181
- const yTicks = yScale.ticks(6);
182
- gGrid.selectAll('line')
183
- .data(yTicks)
184
- .join('line')
185
- .attr('x1', 0)
186
- .attr('x2', innerWidth)
187
- .attr('y1', (d) => yScale(d))
188
- .attr('y2', (d) => yScale(d))
189
- .attr('stroke', gridColor)
190
- .attr('stroke-width', 1)
191
- .attr('shape-rendering', 'crispEdges');
192
-
193
- // Axes
194
- gAxes.selectAll('*').remove();
195
- const xAxis = d3.axisBottom(xScale).ticks(8).tickSizeOuter(0);
196
- const yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0).tickFormat(d3.format('.2f'));
197
- gAxes.append('g')
198
- .attr('transform', `translate(0,${innerHeight})`)
199
- .call(xAxis)
200
- .call((g) => {
201
- g.selectAll('path, line').attr('stroke', axisColor);
202
- g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
203
- });
204
- gAxes.append('g')
205
- .call(yAxis)
206
- .call((g) => {
207
- g.selectAll('path, line').attr('stroke', axisColor);
208
- g.selectAll('text').attr('fill', tickColor).style('font-size', '12px');
209
- });
210
-
211
- // Axis labels (X and Y)
212
- gAxes.append('text')
213
- .attr('class', 'axis-label axis-label--x')
214
- .attr('x', innerWidth)
215
- .attr('y', innerHeight + 32)
216
- .attr('text-anchor', 'end')
217
- .style('font-size', '12px')
218
- .style('fill', tickColor)
219
- .text('x');
220
- gAxes.append('text')
221
- .attr('class', 'axis-label axis-label--y')
222
- .attr('text-anchor', 'middle')
223
- .attr('transform', `translate(${-36},${innerHeight/2}) rotate(-90)`)
224
- .style('font-size', '12px')
225
- .style('fill', tickColor)
226
- .text('y');
227
-
228
- overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
229
- hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor);
230
-
231
- // Legend inside plot (bottom-right), no background/border/shadow
232
- const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22)));
233
- const legendHeight = 64;
234
- gLegend
235
- .attr('x', innerWidth - legendWidth)
236
- .attr('y', innerHeight - legendHeight)
237
- .attr('width', legendWidth)
238
- .attr('height', legendHeight);
239
- const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div');
240
- Object.assign(legendRoot.node().style, {
241
- background: 'transparent',
242
- border: 'none',
243
- borderRadius: '0',
244
- padding: '0',
245
- fontSize: '12px',
246
- lineHeight: '1.35',
247
- color: 'var(--text-color)'
248
- });
249
- legendRoot.html(`
250
- <div style="display:flex;flex-direction:column;gap:6px;">
251
- <div style="display:flex;align-items:center;gap:8px;">
252
- <span style="width:18px;height:3px;background:${colorBase};border-radius:2px;display:inline-block"></span>
253
- <span>Baseline</span>
254
- </div>
255
- <div style="display:flex;align-items:center;gap:8px;">
256
- <span style="width:18px;height:3px;background:${colorImproved};border-radius:2px;display:inline-block"></span>
257
- <span>Improved</span>
258
- </div>
259
- <div style="display:flex;align-items:center;gap:8px;">
260
- <span style="width:18px;height:0;border-top:2px dashed ${colorTarget};display:inline-block"></span>
261
- <span>Target</span>
262
- </div>
263
- </div>
264
- `);
265
- }
266
-
267
- function updatePaths() {
268
- pathBase.transition().duration(200).attr('d', lineGen(yBase));
269
- pathImp.transition().duration(200).attr('d', lineGen(yImp));
270
- pathTgt.transition().duration(200).attr('d', lineGen(yTgt));
271
- }
272
-
273
- function updateAlpha(a) {
274
- alpha = a;
275
- alphaVal.textContent = a.toFixed(2);
276
- yImp = yBase.map((v, i) => blend(v, yAug[i], alpha));
277
- pathImp.transition().duration(80).attr('d', lineGen(yImp));
278
- }
279
-
280
- function applyDataset() {
281
- computeCurves();
282
- updatePaths();
283
- }
284
-
285
- // Hover interactions
286
- function onMove(event) {
287
- const [mx, my] = d3.pointer(event, overlay.node());
288
- const xi = Math.max(0, Math.min(N - 1, Math.round(xScale.invert(mx) * (N - 1))));
289
- const xpx = xScale(xs[xi]);
290
- const yb = yBase[xi], yi = yImp[xi], yt = yTgt[xi];
291
- hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
292
- hoverDotB.attr('cx', xpx).attr('cy', yScale(yb)).style('display', null);
293
- hoverDotI.attr('cx', xpx).attr('cy', yScale(yi)).style('display', null);
294
- hoverDotT.attr('cx', xpx).attr('cy', yScale(yt)).style('display', null);
295
-
296
- // Tooltip content
297
- const ds = datasets[datasetIndex].name;
298
- tipInner.innerHTML = `<div><strong>${ds}</strong></div>` +
299
- `<div><strong>x</strong> ${xs[xi].toFixed(2)}</div>` +
300
- `<div><span style="display:inline-block;width:10px;height:10px;background:${colorBase};border-radius:50%;margin-right:6px;"></span><strong>Baseline</strong> ${yb.toFixed(3)}</div>` +
301
- `<div><span style="display:inline-block;width:10px;height:10px;background:${colorImproved};border-radius:50%;margin-right:6px;"></span><strong>Improved</strong> ${yi.toFixed(3)}</div>` +
302
- `<div><span style="display:inline-block;width:10px;height:10px;background:${colorTarget};border-radius:50%;margin-right:6px;"></span><strong>Target</strong> ${yt.toFixed(3)}</div>`;
303
- const offsetX = 12, offsetY = 12;
304
- tip.style.opacity = '1';
305
- tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`;
306
- }
307
-
308
- function onLeave() {
309
- tip.style.opacity = '0';
310
- tip.style.transform = 'translate(-9999px, -9999px)';
311
- hoverLine.style('display', 'none');
312
- hoverDotB.style('display', 'none');
313
- hoverDotI.style('display', 'none');
314
- hoverDotT.style('display', 'none');
315
- }
316
-
317
- overlay.on('mousemove', onMove).on('mouseleave', onLeave);
318
-
319
- // Init + controls wiring
320
- computeCurves();
321
- updateScales();
322
- updatePaths();
323
-
324
- // Attach controls after SVG for consistency with Plotly fragment
325
- container.appendChild(controls);
326
-
327
- selectDs.addEventListener('change', (e) => {
328
- datasetIndex = parseInt(e.target.value) || 0;
329
- applyDataset();
330
- });
331
- slider.addEventListener('input', (e) => {
332
- const a = parseFloat(e.target.value) || 0;
333
- updateAlpha(a);
334
- });
335
-
336
- // Resize handling
337
- const render = () => {
338
- updateScales();
339
- updatePaths();
340
- };
341
- if (window.ResizeObserver) {
342
- const ro = new ResizeObserver(() => render());
343
- ro.observe(container);
344
- } else {
345
- window.addEventListener('resize', render);
346
- }
347
- render();
348
- };
349
-
350
- if (document.readyState === 'loading') {
351
- document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
352
- } else { ensureD3(bootstrap); }
353
- })();
354
- </script>
355
-
356
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/content/embeds/palettes.html CHANGED
@@ -68,8 +68,6 @@
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>
 
68
  </div>
69
  <div class="palettes__grid"></div>
70
  <div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
 
 
71
  <!-- Hidden SVG filters used by the page-wide simulation classes -->
72
  <svg aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute; left:-9999px; overflow:hidden;">
73
  <defs>
app/src/env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference path="../.astro/types.d.ts" />
app/src/pages/index.astro CHANGED
@@ -4,8 +4,8 @@ import Hero from '../components/Hero.astro';
4
  import Footer from '../components/Footer.astro';
5
  import ThemeToggle from '../components/ThemeToggle.astro';
6
  import Seo from '../components/Seo.astro';
7
- // @ts-ignore Astro asset import typed via env.d.ts
8
- import ogDefaultUrl from '../content/assets/images/visual-vocabulary-poster.png?url';
9
  import 'katex/dist/katex.min.css';
10
  import '../styles/global.css';
11
  const articleFM = (ArticleMod as any).frontmatter ?? {};
 
4
  import Footer from '../components/Footer.astro';
5
  import ThemeToggle from '../components/ThemeToggle.astro';
6
  import Seo from '../components/Seo.astro';
7
+ // Default OG image served from public/
8
+ const ogDefaultUrl = '/thumb.jpg';
9
  import 'katex/dist/katex.min.css';
10
  import '../styles/global.css';
11
  const articleFM = (ArticleMod as any).frontmatter ?? {};