thibaud frere commited on
Commit
a51dcbe
·
1 Parent(s): 3988a64

fix reponsiveness on chart

Browse files
app/.astro/astro/content.d.ts CHANGED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare module 'astro:content' {
2
+ interface Render {
3
+ '.mdx': Promise<{
4
+ Content: import('astro').MarkdownInstance<{}>['Content'];
5
+ headings: import('astro').MarkdownHeading[];
6
+ remarkPluginFrontmatter: Record<string, any>;
7
+ components: import('astro').MDXInstance<{}>['components'];
8
+ }>;
9
+ }
10
+ }
11
+
12
+ declare module 'astro:content' {
13
+ interface RenderResult {
14
+ Content: import('astro/runtime/server/index.js').AstroComponentFactory;
15
+ headings: import('astro').MarkdownHeading[];
16
+ remarkPluginFrontmatter: Record<string, any>;
17
+ }
18
+ interface Render {
19
+ '.md': Promise<RenderResult>;
20
+ }
21
+
22
+ export interface RenderedContent {
23
+ html: string;
24
+ metadata?: {
25
+ imagePaths: Array<string>;
26
+ [key: string]: unknown;
27
+ };
28
+ }
29
+ }
30
+
31
+ declare module 'astro:content' {
32
+ type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
33
+
34
+ export type CollectionKey = keyof AnyEntryMap;
35
+ export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
36
+
37
+ export type ContentCollectionKey = keyof ContentEntryMap;
38
+ export type DataCollectionKey = keyof DataEntryMap;
39
+
40
+ type AllValuesOf<T> = T extends any ? T[keyof T] : never;
41
+ type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
42
+ ContentEntryMap[C]
43
+ >['slug'];
44
+
45
+ /** @deprecated Use `getEntry` instead. */
46
+ export function getEntryBySlug<
47
+ C extends keyof ContentEntryMap,
48
+ E extends ValidContentEntrySlug<C> | (string & {}),
49
+ >(
50
+ collection: C,
51
+ // Note that this has to accept a regular string too, for SSR
52
+ entrySlug: E,
53
+ ): E extends ValidContentEntrySlug<C>
54
+ ? Promise<CollectionEntry<C>>
55
+ : Promise<CollectionEntry<C> | undefined>;
56
+
57
+ /** @deprecated Use `getEntry` instead. */
58
+ export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
59
+ collection: C,
60
+ entryId: E,
61
+ ): Promise<CollectionEntry<C>>;
62
+
63
+ export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
64
+ collection: C,
65
+ filter?: (entry: CollectionEntry<C>) => entry is E,
66
+ ): Promise<E[]>;
67
+ export function getCollection<C extends keyof AnyEntryMap>(
68
+ collection: C,
69
+ filter?: (entry: CollectionEntry<C>) => unknown,
70
+ ): Promise<CollectionEntry<C>[]>;
71
+
72
+ export function getEntry<
73
+ C extends keyof ContentEntryMap,
74
+ E extends ValidContentEntrySlug<C> | (string & {}),
75
+ >(entry: {
76
+ collection: C;
77
+ slug: E;
78
+ }): E extends ValidContentEntrySlug<C>
79
+ ? Promise<CollectionEntry<C>>
80
+ : Promise<CollectionEntry<C> | undefined>;
81
+ export function getEntry<
82
+ C extends keyof DataEntryMap,
83
+ E extends keyof DataEntryMap[C] | (string & {}),
84
+ >(entry: {
85
+ collection: C;
86
+ id: E;
87
+ }): E extends keyof DataEntryMap[C]
88
+ ? Promise<DataEntryMap[C][E]>
89
+ : Promise<CollectionEntry<C> | undefined>;
90
+ export function getEntry<
91
+ C extends keyof ContentEntryMap,
92
+ E extends ValidContentEntrySlug<C> | (string & {}),
93
+ >(
94
+ collection: C,
95
+ slug: E,
96
+ ): E extends ValidContentEntrySlug<C>
97
+ ? Promise<CollectionEntry<C>>
98
+ : Promise<CollectionEntry<C> | undefined>;
99
+ export function getEntry<
100
+ C extends keyof DataEntryMap,
101
+ E extends keyof DataEntryMap[C] | (string & {}),
102
+ >(
103
+ collection: C,
104
+ id: E,
105
+ ): E extends keyof DataEntryMap[C]
106
+ ? Promise<DataEntryMap[C][E]>
107
+ : Promise<CollectionEntry<C> | undefined>;
108
+
109
+ /** Resolve an array of entry references from the same collection */
110
+ export function getEntries<C extends keyof ContentEntryMap>(
111
+ entries: {
112
+ collection: C;
113
+ slug: ValidContentEntrySlug<C>;
114
+ }[],
115
+ ): Promise<CollectionEntry<C>[]>;
116
+ export function getEntries<C extends keyof DataEntryMap>(
117
+ entries: {
118
+ collection: C;
119
+ id: keyof DataEntryMap[C];
120
+ }[],
121
+ ): Promise<CollectionEntry<C>[]>;
122
+
123
+ export function render<C extends keyof AnyEntryMap>(
124
+ entry: AnyEntryMap[C][string],
125
+ ): Promise<RenderResult>;
126
+
127
+ export function reference<C extends keyof AnyEntryMap>(
128
+ collection: C,
129
+ ): import('astro/zod').ZodEffects<
130
+ import('astro/zod').ZodString,
131
+ C extends keyof ContentEntryMap
132
+ ? {
133
+ collection: C;
134
+ slug: ValidContentEntrySlug<C>;
135
+ }
136
+ : {
137
+ collection: C;
138
+ id: keyof DataEntryMap[C];
139
+ }
140
+ >;
141
+ // Allow generic `string` to avoid excessive type errors in the config
142
+ // if `dev` is not running to update as you edit.
143
+ // Invalid collection names will be caught at build time.
144
+ export function reference<C extends string>(
145
+ collection: C,
146
+ ): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
147
+
148
+ type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
149
+ type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
150
+ ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
151
+ >;
152
+
153
+ type ContentEntryMap = {
154
+ "chapters": {
155
+ "best-pratices.mdx": {
156
+ id: "best-pratices.mdx";
157
+ slug: "best-pratices";
158
+ body: string;
159
+ collection: "chapters";
160
+ data: any
161
+ } & { render(): Render[".mdx"] };
162
+ "components.mdx": {
163
+ id: "components.mdx";
164
+ slug: "components";
165
+ body: string;
166
+ collection: "chapters";
167
+ data: any
168
+ } & { render(): Render[".mdx"] };
169
+ "debug-components.mdx": {
170
+ id: "debug-components.mdx";
171
+ slug: "debug-components";
172
+ body: string;
173
+ collection: "chapters";
174
+ data: any
175
+ } & { render(): Render[".mdx"] };
176
+ "getting-started.mdx": {
177
+ id: "getting-started.mdx";
178
+ slug: "getting-started";
179
+ body: string;
180
+ collection: "chapters";
181
+ data: any
182
+ } & { render(): Render[".mdx"] };
183
+ "introduction.mdx": {
184
+ id: "introduction.mdx";
185
+ slug: "introduction";
186
+ body: string;
187
+ collection: "chapters";
188
+ data: any
189
+ } & { render(): Render[".mdx"] };
190
+ "markdown.mdx": {
191
+ id: "markdown.mdx";
192
+ slug: "markdown";
193
+ body: string;
194
+ collection: "chapters";
195
+ data: any
196
+ } & { render(): Render[".mdx"] };
197
+ "writing-your-content.mdx": {
198
+ id: "writing-your-content.mdx";
199
+ slug: "writing-your-content";
200
+ body: string;
201
+ collection: "chapters";
202
+ data: any
203
+ } & { render(): Render[".mdx"] };
204
+ };
205
+ "embeds": {
206
+ "vibe-code-d3-embeds-directives.md": {
207
+ id: "vibe-code-d3-embeds-directives.md";
208
+ slug: "vibe-code-d3-embeds-directives";
209
+ body: string;
210
+ collection: "embeds";
211
+ data: any
212
+ } & { render(): Render[".md"] };
213
+ };
214
+
215
+ };
216
+
217
+ type DataEntryMap = {
218
+ "assets": {
219
+ "data/mnist-variant-model": {
220
+ id: "data/mnist-variant-model";
221
+ collection: "assets";
222
+ data: any
223
+ };
224
+ };
225
+
226
+ };
227
+
228
+ type AnyEntryMap = ContentEntryMap & DataEntryMap;
229
+
230
+ export type ContentConfig = never;
231
+ }
app/scripts/export-pdf.mjs CHANGED
@@ -124,8 +124,8 @@ async function waitForD3(page, timeoutMs = 20000) {
124
  await page.evaluate(async (timeout) => {
125
  const start = Date.now();
126
  const isReady = () => {
127
- // Prioritize hero banner if present
128
- const hero = document.querySelector('.hero .d3-galaxy') || document.querySelector('.d3-galaxy');
129
  if (hero) {
130
  return !!hero.querySelector('svg circle, svg path, svg rect, svg g');
131
  }
@@ -274,6 +274,8 @@ async function main() {
274
  }
275
  function fixSvg(svg){
276
  if (!svg) return;
 
 
277
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
278
  try { svg.removeAttribute('width'); } catch {}
279
  try { svg.removeAttribute('height'); } catch {}
@@ -320,15 +322,14 @@ async function main() {
320
  // - Ensure an SVG background (CSS background on svg element)
321
  const cssHandle = await page.addStyleTag({ content: `
322
  .hero .points { mix-blend-mode: normal !important; }
323
- .d3-galaxy svg { background: var(--surface-bg); }
324
  ` });
325
- const thumbPath = resolve(cwd, 'dist', 'thumb.jpg');
326
  await page.screenshot({ path: thumbPath, type: 'jpeg', quality: 85, fullPage: false });
327
  // Also emit PNG for compatibility if needed
328
- const thumbPngPath = resolve(cwd, 'dist', 'thumb.png');
329
  await page.screenshot({ path: thumbPngPath, type: 'png', fullPage: false });
330
- const publicThumb = resolve(cwd, 'public', 'thumb.jpg');
331
- const publicThumbPng = resolve(cwd, 'public', 'thumb.png');
332
  try { await fs.copyFile(thumbPath, publicThumb); } catch {}
333
  try { await fs.copyFile(thumbPngPath, publicThumbPng); } catch {}
334
  // Remove temporary style so PDF is unaffected
@@ -371,6 +372,8 @@ async function main() {
371
  }
372
  function fixSvg(svg){
373
  if (!svg) return;
 
 
374
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
375
  try { svg.removeAttribute('width'); } catch {}
376
  try { svg.removeAttribute('height'); } catch {}
@@ -423,8 +426,9 @@ async function main() {
423
 
424
  /* Banner centering & visibility */
425
  .hero .points { mix-blend-mode: normal !important; }
426
- .d3-galaxy { width: 100% !important; height: 300px; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; }
427
- .d3-galaxy svg { width: 100% !important; height: auto !important; }
 
428
  ` });
429
  } catch {}
430
  await page.pdf({
 
124
  await page.evaluate(async (timeout) => {
125
  const start = Date.now();
126
  const isReady = () => {
127
+ // Prioritize hero banner if present (generic container)
128
+ const hero = document.querySelector('.hero-banner');
129
  if (hero) {
130
  return !!hero.querySelector('svg circle, svg path, svg rect, svg g');
131
  }
 
274
  }
275
  function fixSvg(svg){
276
  if (!svg) return;
277
+ // Do not alter hero banner SVG sizing; it may rely on explicit width/height
278
+ try { if (svg.closest && svg.closest('.hero-banner')) return; } catch {}
279
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
280
  try { svg.removeAttribute('width'); } catch {}
281
  try { svg.removeAttribute('height'); } catch {}
 
322
  // - Ensure an SVG background (CSS background on svg element)
323
  const cssHandle = await page.addStyleTag({ content: `
324
  .hero .points { mix-blend-mode: normal !important; }
 
325
  ` });
326
+ const thumbPath = resolve(cwd, 'dist', 'thumb.auto.jpg');
327
  await page.screenshot({ path: thumbPath, type: 'jpeg', quality: 85, fullPage: false });
328
  // Also emit PNG for compatibility if needed
329
+ const thumbPngPath = resolve(cwd, 'dist', 'thumb.auto.png');
330
  await page.screenshot({ path: thumbPngPath, type: 'png', fullPage: false });
331
+ const publicThumb = resolve(cwd, 'public', 'thumb.auto.jpg');
332
+ const publicThumbPng = resolve(cwd, 'public', 'thumb.auto.png');
333
  try { await fs.copyFile(thumbPath, publicThumb); } catch {}
334
  try { await fs.copyFile(thumbPngPath, publicThumbPng); } catch {}
335
  // Remove temporary style so PDF is unaffected
 
372
  }
373
  function fixSvg(svg){
374
  if (!svg) return;
375
+ // Do not alter hero banner SVG sizing; it may rely on explicit width/height
376
+ try { if (svg.closest && svg.closest('.hero-banner')) return; } catch {}
377
  if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
378
  try { svg.removeAttribute('width'); } catch {}
379
  try { svg.removeAttribute('height'); } catch {}
 
426
 
427
  /* Banner centering & visibility */
428
  .hero .points { mix-blend-mode: normal !important; }
429
+ /* Do NOT force a fixed height to avoid clipping in PDF */
430
+ .hero-banner { width: 100% !important; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; }
431
+ .hero-banner svg { width: 100% !important; height: auto !important; }
432
  ` });
433
  } catch {}
434
  await page.pdf({
app/src/content/chapters/components.mdx CHANGED
@@ -249,13 +249,6 @@ For researchers who want to stay in **Python** while targeting **D3**, the [d3bl
249
  | `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
250
  | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
251
 
252
- <Note emoji="💡" variant="info">
253
- You can refer to a chart by giving it a name and pointing to it with a link.
254
- -> `<a href="#neural-network-mnist-like">like his</a>`
255
- -> `<HtmlEmbed id="neural-network-mnist-like"/>`
256
- Live example: <a href="#neural-network-mnist-like">Fig 1</a>
257
- </Note>
258
-
259
  <Accordion title="Code example">
260
  ```mdx
261
  import HtmlEmbed from '../components/HtmlEmbed.astro'
 
249
  | `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
250
  | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
251
 
 
 
 
 
 
 
 
252
  <Accordion title="Code example">
253
  ```mdx
254
  import HtmlEmbed from '../components/HtmlEmbed.astro'
app/src/content/embeds/d3-line-example.html CHANGED
@@ -71,6 +71,17 @@
71
  }
72
  /* Improved line color via CSS */
73
  .d3-line-example .lines path.improved { stroke: var(--primary-color); }
 
 
 
 
 
 
 
 
 
 
 
74
  </style>
75
  <script>
76
  (() => {
 
71
  }
72
  /* Improved line color via CSS */
73
  .d3-line-example .lines path.improved { stroke: var(--primary-color); }
74
+
75
+ /* Responsive: stack controls on small screens */
76
+ @media (max-width: 640px) {
77
+ .d3-line-example .d3-line__controls {
78
+ flex-wrap: wrap;
79
+ }
80
+ .d3-line-example .d3-line__controls label {
81
+ flex: 1 1 100% !important;
82
+ width: 100%;
83
+ }
84
+ }
85
  </style>
86
  <script>
87
  (() => {
app/src/content/embeds/d3-line.html CHANGED
@@ -232,6 +232,29 @@
232
 
233
  const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  function updateScales() {
236
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
237
  const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
@@ -242,6 +265,9 @@
242
  height = Math.max(260, Math.round(width / 3));
243
  svg.attr('width', width).attr('height', height);
244
 
 
 
 
245
  const innerWidth = width - margin.left - margin.right;
246
  const innerHeight = height - margin.top - margin.bottom;
247
  gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
@@ -275,6 +301,12 @@
275
  // Axes
276
  gAxes.selectAll('*').remove();
277
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
 
 
 
 
 
 
278
  if (isRankStrictFlag) {
279
  const [dx0, dx1] = xScale.domain();
280
  const start = Math.ceil(dx0 / 1000) * 1000;
@@ -282,9 +314,12 @@
282
  const xTicks = [];
283
  for (let v = start; v <= end; v += 1000) xTicks.push(v);
284
  if (xTicks.length === 0) xTicks.push(Math.round(dx0));
285
- xAxis = xAxis.tickValues(xTicks).tickFormat(d3.format('d'));
286
  } else {
287
  xAxis = xAxis.ticks(8);
 
 
 
288
  }
289
  const yAxis = d3.axisLeft(yScale)
290
  .tickValues(yTicks)
 
232
 
233
  const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair');
234
 
235
+ // Responsive layout for controls and inline legend
236
+ function applyResponsiveLayout(containerWidth) {
237
+ const isNarrow = containerWidth <= 600;
238
+ if (isNarrow) {
239
+ controls.style.flexWrap = 'wrap';
240
+ controls.style.alignItems = 'flex-start';
241
+ controls.style.justifyContent = 'flex-start';
242
+ legendInline.style.flexWrap = 'wrap';
243
+ legendInline.style.rowGap = '4px';
244
+ labelMetric.style.marginLeft = '0';
245
+ labelMetric.style.flexBasis = '100%';
246
+ labelMetric.style.width = '100%';
247
+ } else {
248
+ controls.style.flexWrap = 'nowrap';
249
+ controls.style.alignItems = 'center';
250
+ controls.style.justifyContent = 'space-between';
251
+ legendInline.style.flexWrap = 'nowrap';
252
+ labelMetric.style.marginLeft = 'auto';
253
+ labelMetric.style.flexBasis = 'auto';
254
+ labelMetric.style.width = 'auto';
255
+ }
256
+ }
257
+
258
  function updateScales() {
259
  const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
260
  const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)';
 
265
  height = Math.max(260, Math.round(width / 3));
266
  svg.attr('width', width).attr('height', height);
267
 
268
+ // Update controls layout for current width
269
+ applyResponsiveLayout(width);
270
+
271
  const innerWidth = width - margin.left - margin.right;
272
  const innerHeight = height - margin.top - margin.bottom;
273
  gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
 
301
  // Axes
302
  gAxes.selectAll('*').remove();
303
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
304
+ const isNarrow = width <= 600;
305
+ const formatK = (v) => {
306
+ const vv = Math.round(v);
307
+ if (isNarrow && Math.abs(vv) >= 1000) return `${Math.round(vv / 1000)}K`;
308
+ return d3.format('d')(vv);
309
+ };
310
  if (isRankStrictFlag) {
311
  const [dx0, dx1] = xScale.domain();
312
  const start = Math.ceil(dx0 / 1000) * 1000;
 
314
  const xTicks = [];
315
  for (let v = start; v <= end; v += 1000) xTicks.push(v);
316
  if (xTicks.length === 0) xTicks.push(Math.round(dx0));
317
+ xAxis = xAxis.tickValues(xTicks).tickFormat(formatK);
318
  } else {
319
  xAxis = xAxis.ticks(8);
320
+ if (isNarrow) {
321
+ xAxis = xAxis.tickFormat((v) => (Math.abs(v) >= 1000 ? `${Math.round(v / 1000)}K` : d3.format('d')(Math.round(v))));
322
+ }
323
  }
324
  const yAxis = d3.axisLeft(yScale)
325
  .tickValues(yTicks)
app/src/content/embeds/d3-pie.html CHANGED
@@ -93,7 +93,8 @@
93
  const CAPTION_GAP = 24; // espace entre titre et donut
94
  const GAP_X = 20; // espace entre colonnes
95
  const GAP_Y = 12; // espace entre lignes
96
- const LEGEND_HEIGHT = 62; // hauteur de la légende
 
97
  const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
98
  const updateSize = () => {
99
  width = container.clientWidth || 800;
@@ -102,8 +103,7 @@
102
  return { innerWidth: width - margin.left - margin.right };
103
  };
104
 
105
- function renderLegend(categories, colorOf, width, x, legendY){
106
- const legendHeight = LEGEND_HEIGHT;
107
  gLegend.attr('x', x).attr('y', legendY).attr('width', width).attr('height', legendHeight);
108
  const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
109
  root
@@ -172,7 +172,8 @@
172
 
173
  // Positionner la légende sous les graphiques, calée sur la grille centrée
174
  const legendY = TOP_OFFSET + plotsHeight + 4;
175
- renderLegend(categories, colorOf, gridWidth, xOffset, legendY);
 
176
 
177
  const captions = new Map(METRICS.map(m => [m.key, `${m.title}`]));
178
 
@@ -230,7 +231,7 @@
230
  });
231
 
232
  // Définir la hauteur totale du SVG après avoir placé les éléments
233
- const totalHeight = Math.ceil(margin.top + TOP_OFFSET + plotsHeight + 4 + LEGEND_HEIGHT + margin.bottom);
234
  svg.attr('height', totalHeight);
235
  }
236
 
 
93
  const CAPTION_GAP = 24; // espace entre titre et donut
94
  const GAP_X = 20; // espace entre colonnes
95
  const GAP_Y = 12; // espace entre lignes
96
+ const LEGEND_HEIGHT_DESKTOP = 62; // hauteur de la légende sur desktop
97
+ const LEGEND_HEIGHT_MOBILE = 84; // hauteur de la légende sur mobile
98
  const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut
99
  const updateSize = () => {
100
  width = container.clientWidth || 800;
 
103
  return { innerWidth: width - margin.left - margin.right };
104
  };
105
 
106
+ function renderLegend(categories, colorOf, width, x, legendY, legendHeight){
 
107
  gLegend.attr('x', x).attr('y', legendY).attr('width', width).attr('height', legendHeight);
108
  const root = gLegend.selectAll('div').data([0]).join('xhtml:div');
109
  root
 
172
 
173
  // Positionner la légende sous les graphiques, calée sur la grille centrée
174
  const legendY = TOP_OFFSET + plotsHeight + 4;
175
+ const legendHeight = innerWidth <= 600 ? LEGEND_HEIGHT_MOBILE : LEGEND_HEIGHT_DESKTOP;
176
+ renderLegend(categories, colorOf, gridWidth, xOffset, legendY, legendHeight);
177
 
178
  const captions = new Map(METRICS.map(m => [m.key, `${m.title}`]));
179
 
 
231
  });
232
 
233
  // Définir la hauteur totale du SVG après avoir placé les éléments
234
+ const totalHeight = Math.ceil(margin.top + TOP_OFFSET + plotsHeight + 4 + legendHeight + margin.bottom);
235
  svg.attr('height', totalHeight);
236
  }
237
 
app/src/content/embeds/filters-quad.html CHANGED
@@ -162,6 +162,18 @@
162
  // Axes
163
  gAxes.selectAll('*').remove();
164
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
 
 
 
 
 
 
 
 
 
 
 
 
165
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
166
  gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
167
  gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
 
162
  // Axes
163
  gAxes.selectAll('*').remove();
164
  let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);
165
+ const isMobile = (typeof window !== 'undefined' && window.matchMedia) ? window.matchMedia('(max-width: 980px)').matches : false;
166
+ if (isMobile) {
167
+ const fmtK = (v) => {
168
+ const abs = Math.abs(v);
169
+ if (abs >= 1000) {
170
+ const n = v / 1000;
171
+ return (Math.round(n) === n ? n : d3.format('.1f')(n)) + 'K';
172
+ }
173
+ return d3.format('d')(v);
174
+ };
175
+ xAxis = xAxis.tickFormat(fmtK);
176
+ }
177
  const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));
178
  gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });
179
  gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke',axisColor); g.selectAll('text').attr('fill',tickColor).style('font-size','11px'); });