Spaces:
Running
Running
thibaud frere
commited on
Commit
·
52307d3
1
Parent(s):
db576f5
update
Browse files- Dockerfile +4 -0
- app/.astro/astro/content.d.ts +206 -0
- app/scripts/export-pdf.mjs +17 -7
- app/src/components/HtmlEmbed.astro +9 -0
- app/src/content/article.mdx +8 -17
- app/src/content/assets/data/{finevision.csv → vision.csv} +0 -0
- app/src/content/assets/images/placeholder-wide.png +3 -0
- app/src/content/chapters/getting-started.mdx +3 -4
- app/src/content/chapters/writing-your-content.mdx +6 -14
- app/src/content/embeds/d3-line-old.html +0 -0
- app/src/content/embeds/d3-pie.html +2 -2
- app/src/content/embeds/d3-scatter.html +0 -160
- app/src/content/embeds/original_embeds/d3js/banner.html +0 -244
- app/src/content/embeds/original_embeds/d3js/line.html +0 -356
- app/src/content/embeds/palettes.html +0 -2
- app/src/env.d.ts +1 -0
- app/src/pages/index.astro +2 -2
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 |
-
//
|
208 |
if (!args.filename) {
|
209 |
-
const
|
210 |
-
const
|
211 |
-
const
|
212 |
-
return
|
213 |
});
|
214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
34 |
-
|
35 |
-
|
36 |
<Fragment slot="aside">
|
37 |
Reading time: 20–25 minutes.
|
38 |
</Fragment>
|
39 |
-
|
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
|
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 |
-
|
80 |
|
81 |
-
Not every contribution fits a PDF. Treat demos, visualizations, and interactive write‑ups as **
|
82 |
|
83 |
-
This project is
|
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 single‑page 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
|
app/src/content/chapters/getting-started.mdx
CHANGED
@@ -12,7 +12,7 @@ cd app
|
|
12 |
npm install
|
13 |
```
|
14 |
<Fragment slot="aside">
|
15 |
-
|
16 |
</Fragment>
|
17 |
</Sidenote>
|
18 |
|
@@ -28,9 +28,8 @@ npm run dev
|
|
28 |
```bash
|
29 |
npm run build
|
30 |
```
|
31 |
-
|
32 |
-
|
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 |
-
|
|
|
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 |
-
|
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/
|
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:
|
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 |
-
//
|
8 |
-
|
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 ?? {};
|