@tekkare/romulus 0.1.25 → 0.1.26

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.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.24",
7
+ "version": "0.1.25",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -6,6 +6,7 @@
6
6
  import { computed } from 'vue'
7
7
  import { useChartTheme } from '../composables/useChartTheme'
8
8
  import { rmLineOption, type RmSeriesInput, RM_CHART_HEIGHTS } from '../utils/chart'
9
+ import { rmCompact, rmNumber } from '../utils/format'
9
10
 
10
11
  const props = withDefaults(defineProps<{
11
12
  categories: (string | number)[]
@@ -20,6 +21,29 @@ const props = withDefaults(defineProps<{
20
21
  * `axis: 1` : deux unites sur un graphe sans libelle d'axe se lisent mal.
21
22
  */
22
23
  axisLabels?: [string, string]
24
+ /**
25
+ * Etiquette chaque point de sa valeur, en pastille de la couleur de la
26
+ * serie. A reserver aux series courtes, sinon les pastilles se chevauchent.
27
+ */
28
+ valueLabels?: boolean
29
+ /**
30
+ * Masque la graduation de l'axe des ordonnees, lignes de fond gardees. Ne
31
+ * s'utilise qu'avec `valueLabels`, sinon le graphe perd tout chiffre.
32
+ */
33
+ hideValueAxis?: boolean
34
+ /** Position de la legende. */
35
+ legendPosition?: 'top' | 'bottom'
36
+ /**
37
+ * Ancre l'axe a zero quand toutes les valeurs sont positives, pour ne pas
38
+ * exagerer visuellement de faibles variations.
39
+ */
40
+ startAtZero?: boolean
41
+ /**
42
+ * Format des valeurs, dans les etiquettes comme dans l'infobulle.
43
+ * `compact` abrege ("18,8 M") quand la place manque ; `plain` garde le
44
+ * nombre entier avec ses separateurs de milliers.
45
+ */
46
+ numberFormat?: 'compact' | 'plain'
23
47
  height?: keyof typeof RM_CHART_HEIGHTS | number
24
48
  description?: string
25
49
  }>(), {
@@ -27,12 +51,23 @@ const props = withDefaults(defineProps<{
27
51
  step: false,
28
52
  smooth: true,
29
53
  axisLabels: undefined,
54
+ valueLabels: false,
55
+ hideValueAxis: false,
56
+ legendPosition: 'top',
57
+ startAtZero: false,
58
+ numberFormat: undefined,
30
59
  height: 'standard',
31
60
  description: undefined,
32
61
  })
33
62
 
34
63
  const { theme, palette, decal } = useChartTheme()
35
64
 
65
+ const formatter = computed(() => {
66
+ if (props.numberFormat === 'compact') return rmCompact
67
+ if (props.numberFormat === 'plain') return rmNumber
68
+ return undefined
69
+ })
70
+
36
71
  const option = computed(() =>
37
72
  rmLineOption(props.categories, props.series, theme.value, palette.value.cat, {
38
73
  area: props.area,
@@ -40,6 +75,11 @@ const option = computed(() =>
40
75
  smooth: props.smooth,
41
76
  decal: decal.value,
42
77
  axisLabels: props.axisLabels,
78
+ valueLabels: props.valueLabels,
79
+ hideValueAxis: props.hideValueAxis,
80
+ legendPosition: props.legendPosition,
81
+ startAtZero: props.startAtZero,
82
+ valueFormatter: formatter.value,
43
83
  }),
44
84
  )
45
85
  </script>
@@ -288,14 +288,67 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
288
288
  decal?: boolean;
289
289
  /** Libelles des deux axes, requis des qu'une serie porte `axis: 1`. */
290
290
  axisLabels?: [string, string];
291
+ /**
292
+ * Etiquette la valeur de chaque point, en pastille de la couleur de sa
293
+ * serie. Reserve aux series courtes : au-dela d'une quinzaine de points
294
+ * les pastilles se chevauchent et deviennent illisibles.
295
+ */
296
+ valueLabels?: boolean;
297
+ /**
298
+ * Masque la graduation de l'axe des ordonnees, lignes de fond gardees.
299
+ * A n'activer qu'avec `valueLabels` : sans graduation NI etiquette, le
300
+ * graphe ne porte plus aucun chiffre.
301
+ */
302
+ hideValueAxis?: boolean;
303
+ /** Position de la legende. `bottom` degage le haut du graphe. */
304
+ legendPosition?: 'top' | 'bottom';
305
+ /**
306
+ * Ancre l'axe a zero quand toutes les valeurs sont positives. Sans ca
307
+ * ECharts cadre sur l'amplitude des donnees et exagere les variations :
308
+ * une serie qui oscille de 3 % occupe alors toute la hauteur.
309
+ * Ignore des qu'une valeur est negative, ou l'axe doit descendre plus bas.
310
+ */
311
+ startAtZero?: boolean;
312
+ /** Formate les valeurs, dans les etiquettes comme dans l'infobulle. */
313
+ valueFormatter?: (value: number) => string;
291
314
  }): {
315
+ tooltip: {
316
+ trigger: string;
317
+ backgroundColor: string;
318
+ textStyle: {
319
+ color: string;
320
+ fontFamily: string;
321
+ fontSize: number;
322
+ };
323
+ borderColor: string;
324
+ borderWidth: number;
325
+ } | {
326
+ valueFormatter: (value: unknown) => string;
327
+ trigger: string;
328
+ backgroundColor: string;
329
+ textStyle: {
330
+ color: string;
331
+ fontFamily: string;
332
+ fontSize: number;
333
+ };
334
+ borderColor: string;
335
+ borderWidth: number;
336
+ };
292
337
  legend: {
293
- top: number;
294
338
  textStyle: {
295
339
  color: string;
296
340
  fontSize: number;
297
341
  fontFamily: string;
298
342
  };
343
+ bottom: number;
344
+ left: string;
345
+ } | {
346
+ textStyle: {
347
+ color: string;
348
+ fontSize: number;
349
+ fontFamily: string;
350
+ };
351
+ top: number;
299
352
  } | undefined;
300
353
  grid: {
301
354
  left: number;
@@ -322,7 +375,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
322
375
  type: string;
323
376
  data: (string | number)[];
324
377
  };
325
- yAxis: {
378
+ yAxis: ({
379
+ min: number | undefined;
326
380
  type: string;
327
381
  name: string | undefined;
328
382
  nameLocation: string;
@@ -334,21 +388,38 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
334
388
  align: string;
335
389
  };
336
390
  axisLabel: {
391
+ show: boolean;
392
+ fontSize?: undefined;
393
+ color?: undefined;
394
+ fontFamily?: undefined;
395
+ } | {
337
396
  fontSize: number;
338
397
  color: string;
339
398
  fontFamily: string;
399
+ show?: undefined;
340
400
  };
341
401
  axisLine: {
402
+ show: boolean;
403
+ lineStyle?: undefined;
404
+ } | {
342
405
  lineStyle: {
343
406
  color: string;
344
407
  };
408
+ show?: undefined;
345
409
  };
410
+ axisTick: {
411
+ show: boolean;
412
+ } | undefined;
346
413
  splitLine: {
347
414
  lineStyle: {
348
415
  color: string;
349
416
  };
350
417
  };
351
- } | ({
418
+ } | {
419
+ splitLine: {
420
+ show: boolean;
421
+ };
422
+ min: number | undefined;
352
423
  type: string;
353
424
  name: string | undefined;
354
425
  nameLocation: string;
@@ -360,24 +431,30 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
360
431
  align: string;
361
432
  };
362
433
  axisLabel: {
434
+ show: boolean;
435
+ fontSize?: undefined;
436
+ color?: undefined;
437
+ fontFamily?: undefined;
438
+ } | {
363
439
  fontSize: number;
364
440
  color: string;
365
441
  fontFamily: string;
442
+ show?: undefined;
366
443
  };
367
444
  axisLine: {
445
+ show: boolean;
446
+ lineStyle?: undefined;
447
+ } | {
368
448
  lineStyle: {
369
449
  color: string;
370
450
  };
451
+ show?: undefined;
371
452
  };
372
- splitLine: {
373
- lineStyle: {
374
- color: string;
375
- };
376
- };
377
- } | {
378
- splitLine: {
453
+ axisTick: {
379
454
  show: boolean;
380
- };
455
+ } | undefined;
456
+ })[] | {
457
+ min: number | undefined;
381
458
  type: string;
382
459
  name: string | undefined;
383
460
  nameLocation: string;
@@ -389,16 +466,34 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
389
466
  align: string;
390
467
  };
391
468
  axisLabel: {
469
+ show: boolean;
470
+ fontSize?: undefined;
471
+ color?: undefined;
472
+ fontFamily?: undefined;
473
+ } | {
392
474
  fontSize: number;
393
475
  color: string;
394
476
  fontFamily: string;
477
+ show?: undefined;
395
478
  };
396
479
  axisLine: {
480
+ show: boolean;
481
+ lineStyle?: undefined;
482
+ } | {
483
+ lineStyle: {
484
+ color: string;
485
+ };
486
+ show?: undefined;
487
+ };
488
+ axisTick: {
489
+ show: boolean;
490
+ } | undefined;
491
+ splitLine: {
397
492
  lineStyle: {
398
493
  color: string;
399
494
  };
400
495
  };
401
- })[];
496
+ };
402
497
  series: {
403
498
  type: string;
404
499
  name: string | undefined;
@@ -408,6 +503,20 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
408
503
  step: string | undefined;
409
504
  symbol: string;
410
505
  symbolSize: number;
506
+ label: {
507
+ show: boolean;
508
+ position: string;
509
+ distance: number;
510
+ fontSize: number;
511
+ fontFamily: string;
512
+ color: string;
513
+ backgroundColor: string;
514
+ padding: number[];
515
+ borderRadius: number;
516
+ formatter: (p: {
517
+ value: number | null;
518
+ }) => string;
519
+ } | undefined;
411
520
  lineStyle: {
412
521
  color: string;
413
522
  width: number;
@@ -440,17 +549,6 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
440
549
  textStyle: {
441
550
  fontFamily: string;
442
551
  };
443
- tooltip: {
444
- trigger: string;
445
- backgroundColor: string;
446
- textStyle: {
447
- color: string;
448
- fontFamily: string;
449
- fontSize: number;
450
- };
451
- borderColor: string;
452
- borderWidth: number;
453
- };
454
552
  };
455
553
  /** Donut. Au-dela de 6 parts, regrouper le reste en "Autres". */
456
554
  export declare function rmDonutOption(data: {
@@ -90,14 +90,15 @@ export function rmBaseOption(theme, decal = false) {
90
90
  }
91
91
  };
92
92
  }
93
- const valueAxis = (theme, label) => ({
93
+ const valueAxis = (theme, label, hidden = false) => ({
94
94
  type: "value",
95
- name: label,
95
+ name: hidden ? void 0 : label,
96
96
  nameLocation: "end",
97
97
  nameGap: 12,
98
98
  nameTextStyle: { fontSize: 10, color: theme.dimmedColor, fontFamily: FONT, align: "left" },
99
- axisLabel: { fontSize: 11, color: theme.mutedColor, fontFamily: FONT },
100
- axisLine: { lineStyle: { color: theme.axisLine } },
99
+ axisLabel: hidden ? { show: false } : { fontSize: 11, color: theme.mutedColor, fontFamily: FONT },
100
+ axisLine: hidden ? { show: false } : { lineStyle: { color: theme.axisLine } },
101
+ axisTick: hidden ? { show: false } : void 0,
101
102
  splitLine: { lineStyle: { color: theme.splitLine } }
102
103
  });
103
104
  const axisCommon = (theme) => ({
@@ -145,21 +146,54 @@ export function rmBarHorizontalOption(labels, values, theme, palette, decal = fa
145
146
  };
146
147
  }
147
148
  export function rmLineOption(categories, series, theme, palette, options = {}) {
148
- const { area = false, step = false, smooth = true, decal = false, axisLabels } = options;
149
+ const {
150
+ area = false,
151
+ step = false,
152
+ smooth = true,
153
+ decal = false,
154
+ axisLabels,
155
+ valueLabels = false,
156
+ hideValueAxis = false,
157
+ legendPosition = "top",
158
+ startAtZero = false,
159
+ valueFormatter
160
+ } = options;
149
161
  const dual = series.some((s) => s.axis === 1);
162
+ const bottomLegend = legendPosition === "bottom";
163
+ const format = (value) => valueFormatter ? valueFormatter(value) : String(value);
164
+ const values = series.flatMap((s) => s.data).filter((v) => typeof v === "number");
165
+ const floorAtZero = startAtZero && values.length > 0 && Math.min(...values) >= 0;
166
+ const base = rmBaseOption(theme, decal);
150
167
  return {
151
- ...rmBaseOption(theme, decal),
168
+ ...base,
169
+ tooltip: valueFormatter ? {
170
+ ...base.tooltip,
171
+ valueFormatter: (value) => typeof value === "number" ? format(value) : ""
172
+ } : base.tooltip,
152
173
  // Legende obligatoire en double axe : sans elle, rien ne dit quelle courbe
153
174
  // se lit sur quel axe.
154
- legend: series.length > 1 || dual ? { top: 0, textStyle: { color: theme.mutedColor, fontSize: 11, fontFamily: FONT } } : void 0,
155
- grid: { left: 48, right: dual ? 56 : 16, top: dual ? 40 : 24, bottom: 36, containLabel: true },
175
+ legend: series.length > 1 || dual ? {
176
+ ...bottomLegend ? { bottom: 0, left: "center" } : { top: 0 },
177
+ textStyle: { color: theme.mutedColor, fontSize: 11, fontFamily: FONT }
178
+ } : void 0,
179
+ grid: {
180
+ // Sans graduation a gauche, la gouttiere de 48 px ne sert plus qu'a
181
+ // laisser depasser la premiere pastille de valeur.
182
+ left: hideValueAxis ? 24 : 48,
183
+ right: dual ? 56 : 16,
184
+ // Les pastilles du point le plus haut depassent la courbe : sans marge
185
+ // supplementaire en haut, ECharts les rogne.
186
+ top: dual ? 40 : valueLabels ? 32 : 24,
187
+ bottom: bottomLegend ? 56 : 36,
188
+ containLabel: true
189
+ },
156
190
  xAxis: { type: "category", data: categories, ...axisCommon(theme), splitLine: { show: false }, boundaryGap: false },
157
191
  yAxis: dual ? [
158
- valueAxis(theme, axisLabels?.[0]),
192
+ { ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
159
193
  // Le second axe ne rejoue pas la grille : deux grilles superposees
160
194
  // rendent le fond illisible.
161
- { ...valueAxis(theme, axisLabels?.[1]), splitLine: { show: false } }
162
- ] : valueAxis(theme, axisLabels?.[0]),
195
+ { ...valueAxis(theme, axisLabels?.[1], hideValueAxis), splitLine: { show: false }, min: floorAtZero ? 0 : void 0 }
196
+ ] : { ...valueAxis(theme, axisLabels?.[0], hideValueAxis), min: floorAtZero ? 0 : void 0 },
163
197
  series: series.map((s, i) => {
164
198
  const color = s.color ?? palette[i % palette.length];
165
199
  return {
@@ -169,8 +203,22 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
169
203
  data: s.data,
170
204
  smooth: step ? false : smooth,
171
205
  step: step ? "end" : void 0,
172
- symbol: "circle",
206
+ // La pastille de valeur marque deja le point : garder en plus le
207
+ // symbole empile deux marqueurs au meme endroit.
208
+ symbol: valueLabels ? "none" : "circle",
173
209
  symbolSize: 5,
210
+ label: valueLabels ? {
211
+ show: true,
212
+ position: "top",
213
+ distance: 6,
214
+ fontSize: 11,
215
+ fontFamily: FONT,
216
+ color: "#FFFFFF",
217
+ backgroundColor: color,
218
+ padding: [3, 5],
219
+ borderRadius: 2,
220
+ formatter: (p) => p.value === null || p.value === void 0 ? "" : format(p.value)
221
+ } : void 0,
174
222
  lineStyle: { color, width: 2.5 },
175
223
  itemStyle: { color },
176
224
  areaStyle: area ? {
@@ -25,3 +25,14 @@ export declare const rmBig: (n: number) => string;
25
25
  export declare const rmPct: (n: number, digits?: number) => string;
26
26
  /** "8,7" - decimale francaise sans unite (ratios : HR, OR, taux). */
27
27
  export declare const rmDecimal: (n: number, digits?: number) => string;
28
+ /**
29
+ * "18,8 M" · "345 k" · "1,2 Md" - grands nombres abreges SANS unite, pour
30
+ * les etiquettes de graphe ou l'espace manque. Distinct de `rmBig`, qui colle
31
+ * un € et ne vaut donc que pour des montants ; ici on compte des boites, des
32
+ * patients, des actes.
33
+ *
34
+ * Passe par `Intl` en `fr-FR` et non par un suffixe anglais code a la main :
35
+ * la virgule decimale et l'espace insecable avant le suffixe sont la regle
36
+ * francaise, y compris sur un graphe.
37
+ */
38
+ export declare const rmCompact: (n: number) => string;
@@ -5,3 +5,4 @@ export const rmPrice = (n) => n.toLocaleString("fr-FR") + " \u20AC";
5
5
  export const rmBig = (n) => n >= 1e9 ? (n / 1e9).toFixed(1).replace(".", ",") + " Md\u20AC" : n >= 1e6 ? (n / 1e6).toFixed(0) + " M\u20AC" : n >= 1e3 ? (n / 1e3).toFixed(0) + " k" : String(n);
6
6
  export const rmPct = (n, digits = 1) => (n > 0 ? "+" : "") + n.toFixed(digits).replace(".", ",") + " %";
7
7
  export const rmDecimal = (n, digits = 1) => n.toFixed(digits).replace(".", ",");
8
+ export const rmCompact = (n) => new Intl.NumberFormat("fr-FR", { notation: "compact", maximumFractionDigits: 1 }).format(n);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",