@tekkare/romulus 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { RM_EXPORT_EXTENSIONS } from "../utils/export.js";
2
2
  const BRAND_TITLE = "OIP Healthcare";
3
- const BRAND_LINE = "par Tekkare";
3
+ const BRAND_LINE = "By Tekkare \u{1F49C}";
4
+ const COVER_SHEET = "Tekkare";
4
5
  function longDate(lang) {
5
6
  return (/* @__PURE__ */ new Date()).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
6
7
  year: "numeric",
@@ -16,6 +17,19 @@ function columnsOf(sheet, keep) {
16
17
  const kept = sheet.schema.filter((column) => keep.includes(column.column));
17
18
  return kept.length ? kept : sheet.schema;
18
19
  }
20
+ const XLSX_CELL_MAX = 32767;
21
+ function xlsxSafeText(text) {
22
+ return text.length > XLSX_CELL_MAX ? `${text.slice(0, XLSX_CELL_MAX - 1)}\u2026` : text;
23
+ }
24
+ function xlsxColumnsOf(sheet, keep) {
25
+ return columnsOf(sheet, keep).map((column) => ({
26
+ ...column,
27
+ value: (row) => {
28
+ const value = column.value(row);
29
+ return typeof value === "string" ? xlsxSafeText(value) : value;
30
+ }
31
+ }));
32
+ }
19
33
  function cellText(value, lang) {
20
34
  if (value === null || value === void 0) return "";
21
35
  if (value instanceof Date) {
@@ -76,63 +90,101 @@ async function raster(markup, width, height, scale = 2) {
76
90
  function printable(text) {
77
91
  return text.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
78
92
  }
93
+ function cardsOf(payload, config, lang) {
94
+ if (payload.cards?.length) return payload.cards.slice(0, payload.cardLimit ?? 100);
95
+ const sheet = payload.sheets[0];
96
+ if (!sheet) return [];
97
+ const pick = (name) => name ? sheet.schema.find((column) => column.column === name) : void 0;
98
+ const title = pick(sheet.titleColumn);
99
+ const subtitle = pick(sheet.subtitleColumn);
100
+ const columns = columnsOf(sheet, config.columns);
101
+ const head = title ?? columns[0];
102
+ if (!head) return [];
103
+ const fields = columns.filter((column) => column !== head && column !== subtitle);
104
+ return rowsOf(sheet, payload.cardLimit ?? 100).map((row) => ({
105
+ title: cellText(head.value(row), lang) || "\u2014",
106
+ subtitle: subtitle ? cellText(subtitle.value(row), lang) : void 0,
107
+ fields: fields.map((column) => ({ label: column.column, value: cellText(column.value(row), lang) })).filter((field) => field.value !== "")
108
+ }));
109
+ }
79
110
  export function useRmExport() {
80
111
  function coverSheet(payload, config, lang) {
81
- const center = { align: "left", alignVertical: "center", fontFamily: "Figtree" };
112
+ const COLUMNS = 40;
113
+ const TEXT_COLUMN = 7;
114
+ const KPI_COLUMNS = [6, 8];
115
+ const base = {
116
+ borderColor: "#FFFFFF",
117
+ align: "center",
118
+ alignVertical: "center",
119
+ fontFamily: "Figtree"
120
+ };
121
+ const empty = { value: "", borderColor: "#FFFFFF" };
122
+ const filler = () => Array.from({ length: COLUMNS }, () => ({ value: "", borderColor: "#FFFFFF", width: 20, height: 30 }));
123
+ const valueStyle = { ...base, fontSize: 18, fontWeight: "bold" };
124
+ const labelStyle = { ...base, fontSize: 18, color: "#b2b2b2", fontStyle: "italic" };
82
125
  const rows = [];
83
- const line = (value, style = {}) => rows.push([{ ...center, value, ...style }]);
126
+ const spread = (cells) => rows.push(Array.from({ length: COLUMNS }, (_, index) => cells[index] ?? empty));
127
+ const line = (value, style = {}) => spread({ [TEXT_COLUMN]: { ...base, value: xlsxSafeText(String(value ?? "")), ...style } });
128
+ rows.push(filler(), filler(), filler());
84
129
  if (config.options.includes("logo")) {
85
- line(BRAND_TITLE, { fontSize: 36, fontWeight: "bold", height: 46 });
86
- line(BRAND_LINE, { fontSize: 14, color: "#6B3FFF" });
87
- rows.push([]);
88
- }
89
- line(payload.title, { fontSize: 20, fontWeight: "bold" });
90
- if (payload.subtitle) line(payload.subtitle, { fontSize: 13, color: "#6C6C8A" });
91
- line(
92
- lang === "en" ? `Generated on ${longDate(lang)}` : `G\xE9n\xE9r\xE9 le ${longDate(lang)}`,
93
- { fontSize: 11, color: "#6C6C8A" }
94
- );
95
- rows.push([]);
96
- if (payload.kpi?.length && config.sections.includes("kpi")) {
97
- line(lang === "en" ? "Key figures" : "Chiffres cl\xE9s", { fontSize: 13, fontWeight: "bold" });
98
- payload.kpi.forEach(
99
- (kpi) => rows.push([
100
- { ...center, value: kpi.label, fontSize: 11, color: "#6C6C8A", width: 34 },
101
- { ...center, value: String(kpi.value), fontSize: 11, fontWeight: "bold", width: 24 }
102
- ])
130
+ line(
131
+ lang === "en" ? `This document was generated on ${longDate(lang)} by` : `Ce document a \xE9t\xE9 g\xE9n\xE9r\xE9 le ${longDate(lang)} par`,
132
+ { fontSize: 16 }
103
133
  );
104
- rows.push([]);
134
+ line(BRAND_TITLE, { fontSize: 72, fontWeight: "bold", width: 60 });
135
+ line(BRAND_LINE, { fontSize: 24, width: 60 });
136
+ } else {
137
+ line(payload.title, { fontSize: 24, fontWeight: "bold" });
105
138
  }
106
- if (payload.filters?.length && config.sections.includes("filters")) {
107
- line(lang === "en" ? "Active filters" : "Filtres actifs", { fontSize: 13, fontWeight: "bold" });
108
- payload.filters.forEach(
109
- (filter) => rows.push([
110
- { ...center, value: filter.label, fontSize: 11, color: "#6C6C8A" },
111
- { ...center, value: filter.value, fontSize: 11 }
112
- ])
139
+ rows.push(filler());
140
+ const kpi = config.sections.includes("kpi") ? payload.kpi ?? [] : [];
141
+ if (kpi.length) {
142
+ line(lang === "en" ? "Your export currently contains :" : "Votre export contient :", { fontSize: 18 });
143
+ rows.push(filler());
144
+ for (let index = 0; index < kpi.length; index += 2) {
145
+ const pair = [kpi[index], kpi[index + 1]];
146
+ spread(
147
+ Object.fromEntries(
148
+ pair.map((item, side) => [
149
+ KPI_COLUMNS[side],
150
+ item ? { ...valueStyle, value: String(item.value) } : empty
151
+ ])
152
+ )
153
+ );
154
+ spread(
155
+ Object.fromEntries(
156
+ pair.map((item, side) => [
157
+ KPI_COLUMNS[side],
158
+ item ? { ...labelStyle, value: String(item.label) } : empty
159
+ ])
160
+ )
161
+ );
162
+ rows.push(filler());
163
+ }
164
+ }
165
+ if (config.options.includes("logo")) line(payload.title, { fontSize: 20, fontWeight: "bold" });
166
+ if (payload.subtitle) line(payload.subtitle, { fontSize: 16 });
167
+ const filters = config.sections.includes("filters") ? payload.filters ?? [] : [];
168
+ if (filters.length) {
169
+ rows.push(filler());
170
+ filters.forEach(
171
+ (filter) => line(`${filter.label} : ${filter.value}`, { fontSize: 16, color: "#b2b2b2" })
113
172
  );
114
- rows.push([]);
115
173
  }
116
174
  const withSources = config.sections.includes("sources") || config.options.includes("sources");
117
175
  if (payload.sources?.length && withSources) {
118
- line(lang === "en" ? "Sources" : "Sources", { fontSize: 13, fontWeight: "bold" });
176
+ rows.push(filler());
119
177
  payload.sources.forEach((source) => {
120
178
  const date = source.date ? new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", {
121
179
  year: "numeric",
122
180
  month: "long",
123
181
  day: "numeric"
124
182
  }) : null;
125
- rows.push([
126
- { ...center, value: `${source.emoji ? `${source.emoji} ` : ""}${source.name}`, fontSize: 11 },
127
- {
128
- ...center,
129
- value: date ? lang === "en" ? `Last update: ${date}` : `Derni\xE8re mise \xE0 jour : ${date}` : "",
130
- fontSize: 11,
131
- color: "#6C6C8A"
132
- }
133
- ]);
183
+ const update = date ? lang === "en" ? ` - Last update: ${date}` : ` - Derni\xE8re mise \xE0 jour : ${date}` : "";
184
+ line(`${source.emoji ? `${source.emoji} ` : ""}Source : ${source.name}${update}`, { fontSize: 18 });
134
185
  });
135
186
  }
187
+ for (let index = 0; index < 15; index += 1) rows.push(filler());
136
188
  return rows;
137
189
  }
138
190
  async function toXlsx(config, payload, lang) {
@@ -141,13 +193,13 @@ export function useRmExport() {
141
193
  const sheets = payload.sheets;
142
194
  const data = [coverSheet(payload, config, lang)];
143
195
  const schema = [null];
144
- const names = [BRAND_TITLE];
196
+ const names = [COVER_SHEET];
145
197
  let rowCount = 0;
146
198
  sheets.forEach((sheet) => {
147
199
  const rows = rowsOf(sheet, payload.limit);
148
200
  rowCount += rows.length;
149
201
  data.push(rows);
150
- schema.push(columnsOf(sheet, config.columns));
202
+ schema.push(xlsxColumnsOf(sheet, config.columns));
151
203
  names.push(sheet.name.replace(/[[\]:*?/\\]/g, " ").slice(0, 31));
152
204
  });
153
205
  await writeXlsxFile(data, { schema, sheets: names, fileName, stickyRowsCount: 1 });
@@ -234,7 +286,12 @@ export function useRmExport() {
234
286
  const { default: JsPdf } = await import("jspdf");
235
287
  const { default: autoTable } = await import("jspdf-autotable");
236
288
  const landscape = config.layout === "a4-landscape";
237
- const doc = new JsPdf({ orientation: landscape ? "landscape" : "portrait", unit: "mm", format: "a4" });
289
+ const doc = new JsPdf({
290
+ orientation: landscape ? "landscape" : "portrait",
291
+ unit: "mm",
292
+ format: "a4",
293
+ compress: true
294
+ });
238
295
  const pageWidth = doc.internal.pageSize.getWidth();
239
296
  const margin = 14;
240
297
  const inner = pageWidth - margin * 2;
@@ -248,10 +305,10 @@ export function useRmExport() {
248
305
  doc.setTextColor(255, 255, 255);
249
306
  doc.setFont("helvetica", "bold");
250
307
  doc.setFontSize(22);
251
- doc.text(BRAND_TITLE, margin, 20);
308
+ doc.text(printable(BRAND_TITLE), margin, 20);
252
309
  doc.setFont("helvetica", "normal");
253
310
  doc.setFontSize(11);
254
- doc.text(BRAND_LINE, margin, 28);
311
+ doc.text(printable(BRAND_LINE), margin, 28);
255
312
  doc.setFontSize(9);
256
313
  doc.text(longDate(lang), pageWidth - margin, 28, { align: "right" });
257
314
  if (payload.logoDataUrl) {
@@ -332,47 +389,134 @@ export function useRmExport() {
332
389
  doc.addImage(canvas.toDataURL("image/png"), "PNG", margin, y, drawWidth, drawHeight, void 0, "FAST");
333
390
  y += drawHeight + 6;
334
391
  }
392
+ let rowCount = 0;
393
+ if (config.sections.includes("cards")) {
394
+ const cards = cardsOf(payload, config, lang);
395
+ rowCount = cards.length;
396
+ const labelWidth = Math.min(46, inner * 0.34);
397
+ const valueWidth = inner - labelWidth - 8;
398
+ const pageHeight = doc.internal.pageSize.getHeight();
399
+ cards.forEach((card) => {
400
+ const titleLines = doc.splitTextToSize(printable(card.title), inner - 8);
401
+ const subtitleLines = card.subtitle ? doc.splitTextToSize(printable(card.subtitle), inner - 8) : [];
402
+ const fields = card.fields.map((field) => ({
403
+ label: doc.splitTextToSize(printable(field.label), labelWidth),
404
+ value: doc.splitTextToSize(printable(field.value), valueWidth)
405
+ }));
406
+ const height = 6 + titleLines.length * 5 + (subtitleLines.length ? subtitleLines.length * 4 + 1 : 0) + (card.badges?.length ? 6 : 0) + fields.reduce((total2, field) => total2 + Math.max(field.label.length, field.value.length) * 4.2, 0) + 5;
407
+ if (y + height > pageHeight - margin) {
408
+ doc.addPage();
409
+ y = margin;
410
+ }
411
+ doc.setDrawColor(232, 232, 240);
412
+ doc.setFillColor(252, 252, 254);
413
+ doc.roundedRect(margin, y, inner, height, 2, 2, "FD");
414
+ let cursor = y + 6;
415
+ doc.setTextColor(...ink);
416
+ doc.setFont("helvetica", "bold");
417
+ doc.setFontSize(11);
418
+ doc.text(titleLines, margin + 4, cursor);
419
+ cursor += titleLines.length * 5;
420
+ if (subtitleLines.length) {
421
+ doc.setTextColor(...grey);
422
+ doc.setFont("helvetica", "normal");
423
+ doc.setFontSize(9);
424
+ doc.text(subtitleLines, margin + 4, cursor + 1);
425
+ cursor += subtitleLines.length * 4 + 1;
426
+ }
427
+ if (card.badges?.length) {
428
+ let badgeX = margin + 4;
429
+ doc.setFontSize(7);
430
+ card.badges.forEach((badge) => {
431
+ const text = printable(badge);
432
+ const width = doc.getTextWidth(text) + 4;
433
+ doc.setFillColor(240, 238, 255);
434
+ doc.roundedRect(badgeX, cursor - 2, width, 5, 1.2, 1.2, "F");
435
+ doc.setTextColor(...violet);
436
+ doc.text(text, badgeX + 2, cursor + 1.6);
437
+ badgeX += width + 2;
438
+ });
439
+ cursor += 6;
440
+ }
441
+ fields.forEach((field) => {
442
+ const lines = Math.max(field.label.length, field.value.length);
443
+ doc.setTextColor(...grey);
444
+ doc.setFont("helvetica", "bold");
445
+ doc.setFontSize(7);
446
+ doc.text(field.label, margin + 4, cursor + 1);
447
+ doc.setTextColor(...ink);
448
+ doc.setFont("helvetica", "normal");
449
+ doc.setFontSize(9);
450
+ doc.text(field.value, margin + 4 + labelWidth, cursor + 1);
451
+ cursor += lines * 4.2;
452
+ });
453
+ y += height + 4;
454
+ });
455
+ const total = payload.sheets[0]?.data.length ?? 0;
456
+ if (total > cards.length) {
457
+ doc.setTextColor(...grey);
458
+ doc.setFont("helvetica", "italic");
459
+ doc.setFontSize(8);
460
+ doc.text(
461
+ printable(
462
+ lang === "en" ? `${cards.length} of ${total} records shown. Export to Excel for the full set.` : `${cards.length} fiches sur ${total}. Passez par l'export Excel pour la totalit\xE9.`
463
+ ),
464
+ margin,
465
+ y + 2
466
+ );
467
+ y += 6;
468
+ }
469
+ }
335
470
  if (config.sections.includes("table")) {
336
- let rowCount = 0;
471
+ const MAX_COLUMNS = 8;
337
472
  payload.sheets.forEach((sheet) => {
338
- const columns = columnsOf(sheet, config.columns);
473
+ const all = columnsOf(sheet, config.columns);
474
+ const columns = all.slice(0, MAX_COLUMNS);
339
475
  const rows = rowsOf(sheet, payload.limit);
340
- rowCount += rows.length;
476
+ if (!config.sections.includes("cards")) rowCount += rows.length;
477
+ doc.addPage("a4", "landscape");
478
+ const wide = doc.internal.pageSize.getWidth() - margin * 2;
479
+ doc.setTextColor(...ink);
480
+ doc.setFont("helvetica", "bold");
481
+ doc.setFontSize(11);
482
+ doc.text(printable(sheet.name), margin, margin + 2);
483
+ if (all.length > columns.length) {
484
+ doc.setFont("helvetica", "italic");
485
+ doc.setFontSize(8);
486
+ doc.setTextColor(...grey);
487
+ doc.text(
488
+ printable(
489
+ lang === "en" ? `${columns.length} of ${all.length} columns. The Excel export carries them all.` : `${columns.length} colonnes sur ${all.length}. L'export Excel les porte toutes.`
490
+ ),
491
+ margin,
492
+ margin + 7
493
+ );
494
+ }
341
495
  autoTable(doc, {
342
- startY: y + 2,
496
+ startY: margin + 10,
343
497
  head: [columns.map((column) => printable(column.column))],
344
498
  body: rows.map((row) => columns.map((column) => printable(cellText(column.value(row), lang)))),
345
- styles: { fontSize: 7, cellPadding: 1.6, overflow: "linebreak", textColor: ink },
346
- headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold" },
499
+ styles: { fontSize: 6.5, cellPadding: 1.4, overflow: "linebreak", textColor: ink, minCellWidth: wide / columns.length / 2 },
500
+ headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold", valign: "middle" },
347
501
  alternateRowStyles: { fillColor: [250, 250, 253] },
348
- margin: { left: margin, right: margin },
349
- didDrawPage: () => {
350
- if (payload.sheets.length > 1) {
351
- doc.setTextColor(...grey);
352
- doc.setFont("helvetica", "bold");
353
- doc.setFontSize(8);
354
- doc.text(printable(sheet.name), margin, margin - 4);
355
- }
356
- }
502
+ margin: { left: margin, right: margin, top: margin, bottom: margin },
503
+ tableWidth: wide
357
504
  });
358
- y = doc.lastAutoTable?.finalY ?? y;
505
+ y = doc.lastAutoTable?.finalY ?? margin;
359
506
  });
360
- const withSources = config.sections.includes("sources") || config.options.includes("sources");
361
- if (payload.sources?.length && withSources) {
362
- doc.setTextColor(...grey);
363
- doc.setFont("helvetica", "normal");
364
- doc.setFontSize(8);
365
- const names = payload.sources.map((source) => `${source.name}${source.date ? ` (${new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", { year: "numeric", month: "long", day: "numeric" })})` : ""}`).join(" \xB7 ");
366
- const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
367
- doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
368
- }
369
- const fileName2 = `${config.fileName}.pdf`;
370
- const blob2 = doc.output("blob");
371
- return { fileName: fileName2, rows: rowCount, bytes: download(blob2, fileName2) };
507
+ }
508
+ const withSources = config.sections.includes("sources") || config.options.includes("sources");
509
+ if (payload.sources?.length && withSources) {
510
+ doc.setTextColor(...grey);
511
+ doc.setFont("helvetica", "normal");
512
+ doc.setFontSize(8);
513
+ const names = payload.sources.map((source) => `${source.name}${source.date ? ` (${new Date(source.date).toLocaleDateString(lang === "en" ? "en-US" : "fr-FR", { year: "numeric", month: "long", day: "numeric" })})` : ""}`).join(" \xB7 ");
514
+ const lines = doc.splitTextToSize(printable(`Sources : ${names}`), inner);
515
+ doc.text(lines, margin, Math.min(y + 8, doc.internal.pageSize.getHeight() - margin));
372
516
  }
373
517
  const fileName = `${config.fileName}.pdf`;
374
518
  const blob = doc.output("blob");
375
- return { fileName, rows: 0, bytes: download(blob, fileName) };
519
+ return { fileName, rows: rowCount, bytes: download(blob, fileName) };
376
520
  }
377
521
  async function toPermalink(payload) {
378
522
  const url = payload.permalink || (typeof window !== "undefined" ? window.location.href : "");
@@ -94,6 +94,31 @@ export declare function rmBaseOption(theme: RmChartTheme, decal?: boolean): {
94
94
  borderWidth: number;
95
95
  };
96
96
  };
97
+ /**
98
+ * Axe de valeurs, avec son libelle d'unite au-dessus quand il est fourni.
99
+ *
100
+ * `hidden` masque la graduation mais GARDE les lignes de fond : c'est le seul
101
+ * axe qu'on a le droit de cacher, et uniquement quand chaque point porte deja
102
+ * sa valeur en etiquette. Retirer aussi les lignes de fond ferait perdre toute
103
+ * echelle de comparaison entre deux courbes.
104
+ */
105
+ /**
106
+ * `formatter` s'applique AUSSI a la graduation, pas seulement a l'infobulle :
107
+ * un axe qui affiche 400 000 000 la ou l'infobulle dit 400 M donne deux
108
+ * lectures du meme nombre sur le meme graphe.
109
+ */
110
+ /**
111
+ * Couleur d'une serie a l'indice `index`.
112
+ *
113
+ * La palette categorielle compte six teintes ; au-dela, un simple modulo
114
+ * redonne EXACTEMENT la meme couleur a deux series et rend la legende
115
+ * inutilisable. Chaque tour de palette eclaircit donc la teinte d'un cran :
116
+ * la septieme serie reprend la premiere couleur, en plus clair.
117
+ *
118
+ * Ce n'est pas un remplacement du double encodage : au-dela d'une douzaine de
119
+ * series, un graphe de lignes ne se lit plus, quelle que soit la palette.
120
+ */
121
+ export declare function rmSeriesColor(palette: string[], index: number): string;
97
122
  export interface RmSeriesInput {
98
123
  name?: string;
99
124
  data: (number | null)[];
@@ -389,10 +414,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
389
414
  };
390
415
  axisLabel: {
391
416
  show: boolean;
392
- fontSize?: undefined;
393
- color?: undefined;
394
- fontFamily?: undefined;
395
417
  } | {
418
+ formatter?: ((value: number) => string) | undefined;
396
419
  fontSize: number;
397
420
  color: string;
398
421
  fontFamily: string;
@@ -432,10 +455,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
432
455
  };
433
456
  axisLabel: {
434
457
  show: boolean;
435
- fontSize?: undefined;
436
- color?: undefined;
437
- fontFamily?: undefined;
438
458
  } | {
459
+ formatter?: ((value: number) => string) | undefined;
439
460
  fontSize: number;
440
461
  color: string;
441
462
  fontFamily: string;
@@ -467,10 +488,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
467
488
  };
468
489
  axisLabel: {
469
490
  show: boolean;
470
- fontSize?: undefined;
471
- color?: undefined;
472
- fontFamily?: undefined;
473
491
  } | {
492
+ formatter?: ((value: number) => string) | undefined;
474
493
  fontSize: number;
475
494
  color: string;
476
495
  fontFamily: string;
@@ -90,13 +90,24 @@ export function rmBaseOption(theme, decal = false) {
90
90
  }
91
91
  };
92
92
  }
93
- const valueAxis = (theme, label, hidden = false) => ({
93
+ export function rmSeriesColor(palette, index) {
94
+ const base = palette[index % palette.length];
95
+ const cycle = Math.floor(index / palette.length);
96
+ if (cycle === 0 || !base.startsWith("#")) return base;
97
+ const hex = base.slice(1);
98
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
99
+ const channels = [0, 2, 4].map((offset) => parseInt(full.slice(offset, offset + 2), 16));
100
+ const ratio = Math.min(cycle * 0.28, 0.7);
101
+ const mixed = channels.map((value) => Math.round(value + (255 - value) * ratio));
102
+ return "#" + mixed.map((value) => value.toString(16).padStart(2, "0")).join("");
103
+ }
104
+ const valueAxis = (theme, label, hidden = false, formatter) => ({
94
105
  type: "value",
95
106
  name: hidden ? void 0 : label,
96
107
  nameLocation: "end",
97
108
  nameGap: 12,
98
109
  nameTextStyle: { fontSize: 10, color: theme.dimmedColor, fontFamily: FONT, align: "left" },
99
- axisLabel: hidden ? { show: false } : { fontSize: 11, color: theme.mutedColor, fontFamily: FONT },
110
+ axisLabel: hidden ? { show: false } : { fontSize: 11, color: theme.mutedColor, fontFamily: FONT, ...formatter ? { formatter } : {} },
100
111
  axisLine: hidden ? { show: false } : { lineStyle: { color: theme.axisLine } },
101
112
  axisTick: hidden ? { show: false } : void 0,
102
113
  splitLine: { lineStyle: { color: theme.splitLine } }
@@ -117,7 +128,7 @@ export function rmBarVerticalOption(categories, series, theme, palette, decal =
117
128
  name: s.name,
118
129
  stack: s.stack,
119
130
  data: s.data,
120
- itemStyle: { color: s.color ?? palette[i % palette.length], borderRadius: [3, 3, 0, 0] }
131
+ itemStyle: { color: s.color ?? rmSeriesColor(palette, i), borderRadius: [3, 3, 0, 0] }
121
132
  }))
122
133
  };
123
134
  }
@@ -131,7 +142,7 @@ export function rmBarHorizontalOption(labels, values, theme, palette, decal = fa
131
142
  type: "bar",
132
143
  data: values,
133
144
  itemStyle: {
134
- color: (params) => palette[params.dataIndex % palette.length],
145
+ color: (params) => rmSeriesColor(palette, params.dataIndex),
135
146
  borderRadius: [0, 3, 3, 0]
136
147
  },
137
148
  label: {
@@ -192,13 +203,13 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
192
203
  },
193
204
  xAxis: { type: "category", data: categories, ...axisCommon(theme), splitLine: { show: false }, boundaryGap: false },
194
205
  yAxis: dual ? [
195
- { ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
206
+ { ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
196
207
  // Le second axe ne rejoue pas la grille : deux grilles superposees
197
208
  // rendent le fond illisible.
198
- { ...valueAxis(theme, axisLabels?.[1], hideValueAxis), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
199
- ] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
209
+ { ...valueAxis(theme, axisLabels?.[1], hideValueAxis, valueFormatter), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
210
+ ] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis, valueFormatter), min: floorAtZero ? 0 : void 0 },
200
211
  series: series.map((s, i) => {
201
- const color = s.color ?? palette[i % palette.length];
212
+ const color = s.color ?? rmSeriesColor(palette, i);
202
213
  return {
203
214
  type: "line",
204
215
  name: s.name,
@@ -256,7 +267,7 @@ export function rmDonutOption(data, theme, palette, decal = false) {
256
267
  data: data.map((d, i) => ({
257
268
  value: d.value,
258
269
  name: d.name,
259
- itemStyle: { color: palette[i % palette.length] }
270
+ itemStyle: { color: rmSeriesColor(palette, i) }
260
271
  })),
261
272
  label: { fontSize: 11, color: theme.mutedColor, fontFamily: FONT }
262
273
  }]
@@ -27,6 +27,15 @@ export interface RmExportSheet {
27
27
  name: string;
28
28
  schema: RmExportColumn[];
29
29
  data: any[];
30
+ /**
31
+ * Colonne qui identifie une ligne, par son intitule. Elle sert de titre aux
32
+ * fiches du rapport PDF. Sans elle, le titre retombe sur la premiere colonne
33
+ * du schema, qui est souvent la date : une fiche titree « 12 mars 2025 » ne
34
+ * dit pas de quel produit elle parle.
35
+ */
36
+ titleColumn?: string;
37
+ /** Colonne posee sous le titre de la fiche (le laboratoire, la date...). */
38
+ subtitleColumn?: string;
30
39
  }
31
40
  /** Chiffre cle repris sur la page de garde. */
32
41
  export interface RmExportKpi {
@@ -44,6 +53,21 @@ export interface RmExportFilter {
44
53
  label: string;
45
54
  value: string;
46
55
  }
56
+ /**
57
+ * Une fiche : ce que la page montre sous forme de carte, rendu tel quel dans
58
+ * le rapport PDF. Une page qui n'en declare pas en obtient quand meme, les
59
+ * fiches etant alors derivees des lignes du tableau (cf. useRmExport).
60
+ */
61
+ export interface RmExportCard {
62
+ title: string;
63
+ subtitle?: string;
64
+ /** Etiquettes courtes posees sous le titre (SMR, statut, pays...). */
65
+ badges?: string[];
66
+ fields: {
67
+ label: string;
68
+ value: string;
69
+ }[];
70
+ }
47
71
  /**
48
72
  * Un format d'export.
49
73
  *
@@ -64,15 +88,25 @@ export interface RmExportFormat {
64
88
  tabular?: boolean;
65
89
  /** Le format rend les graphiques. */
66
90
  visual?: boolean;
67
- /** Le format est mis en page : le choix A4 s'applique. */
91
+ /** Le format est mis en page : choix A4 et fiches detaillees. */
68
92
  paged?: boolean;
93
+ /**
94
+ * Le format porte un en-tete de document : page de garde, chiffres cles,
95
+ * filtres actifs, sources. Vrai pour le PDF et le classeur, faux pour un
96
+ * CSV ou une image, qui ne savent porter que leur contenu.
97
+ */
98
+ document?: boolean;
69
99
  /** Format annonce mais pas encore livre : la carte reste visible, grisee. */
70
100
  disabled?: boolean;
71
101
  /** Raison affichee sur une carte grisee. */
72
102
  disabledHint?: string;
73
103
  }
74
- /** Nature d'une section : elle decide de quels formats la section releve. */
75
- export type RmExportSectionKind = 'table' | 'visual' | 'meta';
104
+ /**
105
+ * Nature d'une section : elle decide de quels formats la section releve.
106
+ * `cards` ne vaut que pour un format mis en page, `meta` que pour un format
107
+ * qui porte un en-tete de document.
108
+ */
109
+ export type RmExportSectionKind = 'table' | 'visual' | 'meta' | 'cards';
76
110
  /** Section proposee a l'etape « Donnees ». */
77
111
  export interface RmExportSection {
78
112
  key: string;
@@ -142,6 +176,17 @@ export interface RmExportPayload {
142
176
  sources?: RmExportSource[];
143
177
  filters?: RmExportFilter[];
144
178
  sheets: RmExportSheet[];
179
+ /**
180
+ * Fiches du rapport. Absentes, elles sont derivees des lignes du premier
181
+ * onglet : le titre vient de la premiere colonne, les champs des suivantes.
182
+ */
183
+ cards?: RmExportCard[];
184
+ /**
185
+ * Plafond de fiches dans un rapport. Une fiche fait une dizaine de lignes :
186
+ * mille resultats donneraient un PDF de plusieurs centaines de pages, que
187
+ * personne ne lit et qu'aucune messagerie n'accepte.
188
+ */
189
+ cardLimit?: number;
145
190
  /**
146
191
  * Graphiques a rasteriser ou vectoriser. Les charts Romulus sont rendus par
147
192
  * ECharts en SVG : on recupere le noeud, pas une capture d'ecran.
@@ -12,10 +12,11 @@ export const RM_EXPORT_FORMATS = [
12
12
  label: "PDF",
13
13
  icon: "FilePdf",
14
14
  color: "var(--color-danger)",
15
- desc: "Rapport mis en page, pr\xEAt \xE0 partager. KPI, graphiques, table, en-t\xEAte OIP Healthcare.",
15
+ desc: "Rapport mis en page, pr\xEAt \xE0 partager. Chiffres cl\xE9s, fiches, graphiques, en-t\xEAte OIP Healthcare.",
16
16
  tabular: true,
17
17
  visual: true,
18
- paged: true
18
+ paged: true,
19
+ document: true
19
20
  },
20
21
  {
21
22
  key: "xlsx",
@@ -24,7 +25,7 @@ export const RM_EXPORT_FORMATS = [
24
25
  color: "var(--color-success)",
25
26
  desc: "Donn\xE9es brutes avec page de garde. Pour analyse en local ou consolidation.",
26
27
  tabular: true,
27
- paged: true
28
+ document: true
28
29
  },
29
30
  {
30
31
  key: "csv",
@@ -61,7 +62,11 @@ export const RM_EXPORT_FORMATS = [
61
62
  export const RM_EXPORT_SECTIONS = [
62
63
  { key: "kpi", label: "Chiffres cl\xE9s", kind: "meta", on: true },
63
64
  { key: "charts", label: "Graphiques", kind: "visual", on: true },
64
- { key: "table", label: "Tableau", kind: "table", on: true },
65
+ { key: "cards", label: "Fiches d\xE9taill\xE9es", kind: "cards", on: true },
66
+ // Decochee : dans un rapport, les fiches disent la meme chose en lisible.
67
+ // La case reste offerte a qui veut le tableau en annexe. Elle ne concerne
68
+ // que le PDF — les onglets du classeur ne dependent pas d'elle.
69
+ { key: "table", label: "Tableau r\xE9capitulatif", kind: "table" },
65
70
  { key: "filters", label: "Filtres actifs", kind: "meta", on: true },
66
71
  { key: "sources", label: "Sources et mises \xE0 jour", kind: "meta", on: true }
67
72
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",