@tekkare/romulus 0.1.60 → 0.1.62
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 +1 -1
- package/dist/runtime/components/Button.vue +113 -21
- package/dist/runtime/components/Chart.vue +24 -4
- package/dist/runtime/components/Table.vue +289 -89
- package/dist/runtime/composables/useRmFilterBarView.js +2 -2
- package/dist/runtime/utils/chart.d.ts +135 -0
- package/dist/runtime/utils/chart.js +24 -6
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -1,41 +1,126 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmButton - bouton du design system.
|
|
3
|
+
|
|
4
|
+
Six niveaux d'emphase, du plus fort au plus discret : `primary`, `secondary`,
|
|
5
|
+
`tertiary`, `text`, `text-danger`, `danger`. Une seule action `primary` par
|
|
6
|
+
ecran ou par section, sinon plus rien ne ressort.
|
|
7
|
+
|
|
8
|
+
Le bouton texte est TOUJOURS souligne : dans un tableau dense, un mot violet
|
|
9
|
+
sans soulignement ne se distingue pas d'une valeur mise en avant, et le
|
|
10
|
+
daltonien ne le voit pas du tout.
|
|
11
|
+
|
|
12
|
+
`danger` est le seul a inverser son emphase au survol (contour rouge, puis
|
|
13
|
+
fond rouge plein) : l'inversion est le signal, elle marque le moment ou
|
|
14
|
+
l'action devient possible.
|
|
15
|
+
|
|
16
|
+
Les anciens noms (`primary-fill`, `primary-stroke`, `outline`, `ghost`,
|
|
17
|
+
`discovery-fill`...) restent acceptes et retombent sur les six variantes. Ils
|
|
18
|
+
ne sont plus a utiliser dans du code neuf.
|
|
19
|
+
-->
|
|
1
20
|
<script setup lang="ts">
|
|
21
|
+
import { computed } from 'vue'
|
|
22
|
+
|
|
23
|
+
/** Les six niveaux d'emphase de la reference. */
|
|
24
|
+
export type RmButtonVariant =
|
|
25
|
+
| 'primary'
|
|
26
|
+
| 'secondary'
|
|
27
|
+
| 'tertiary'
|
|
28
|
+
| 'text'
|
|
29
|
+
| 'text-danger'
|
|
30
|
+
| 'danger'
|
|
31
|
+
|
|
32
|
+
/** Noms d'avant l'unification, gardes pour ne pas casser les appels en place. */
|
|
33
|
+
export type RmButtonLegacyVariant =
|
|
34
|
+
| 'primary-fill'
|
|
35
|
+
| 'primary-stroke'
|
|
36
|
+
| 'secondary-fill'
|
|
37
|
+
| 'secondary-stroke'
|
|
38
|
+
| 'secondary-transparent'
|
|
39
|
+
| 'outline'
|
|
40
|
+
| 'ghost'
|
|
41
|
+
| 'discovery-fill'
|
|
42
|
+
| 'discovery-stroke'
|
|
43
|
+
| 'databank-fill'
|
|
44
|
+
| 'databank-stroke'
|
|
45
|
+
| 'pharma-fill'
|
|
46
|
+
| 'pharma-stroke'
|
|
47
|
+
|
|
2
48
|
export interface RmButtonProps {
|
|
3
|
-
|
|
4
|
-
variant?: 'primary' | 'primary-fill' | 'primary-stroke' | 'discovery-fill' | 'discovery-stroke' | 'databank-fill' | 'databank-stroke' | 'pharma-fill' | 'pharma-stroke' | 'secondary' | 'outline' | 'ghost' | 'danger'
|
|
5
|
-
/** Size of the button */
|
|
49
|
+
variant?: RmButtonVariant | RmButtonLegacyVariant
|
|
6
50
|
size?: 'sm' | 'md' | 'lg'
|
|
7
|
-
/** Whether the button is disabled */
|
|
8
51
|
disabled?: boolean
|
|
9
|
-
/**
|
|
52
|
+
/** Pleine largeur, pour un tiroir ou un formulaire dense. */
|
|
10
53
|
block?: boolean
|
|
11
|
-
/**
|
|
54
|
+
/** Forme pilule, pour les controles d'une barre de filtres. */
|
|
55
|
+
pill?: boolean
|
|
56
|
+
/**
|
|
57
|
+
* Carre a une seule icone, 32x32 (28 en `sm`, 40 en `lg`). Demande un
|
|
58
|
+
* `aria-label` : un bouton sans texte n'est annonce que par son intitule.
|
|
59
|
+
*/
|
|
60
|
+
iconOnly?: boolean
|
|
61
|
+
/** Fond et bordure transparents. Pour une action discrete posee sur une carte. */
|
|
62
|
+
ghost?: boolean
|
|
12
63
|
type?: 'button' | 'submit' | 'reset'
|
|
13
|
-
/** Phosphor
|
|
64
|
+
/** Icone Phosphor avant le libelle. */
|
|
14
65
|
prefixIcon?: string
|
|
15
|
-
/** Phosphor
|
|
66
|
+
/** Icone Phosphor apres le libelle. */
|
|
16
67
|
suffixIcon?: string
|
|
17
68
|
/**
|
|
18
69
|
* Action en cours : le bouton tourne et n'accepte plus de clic. Sans cet
|
|
19
|
-
* etat, un export long laisse croire que le clic n'a pas pris et
|
|
20
|
-
* relance la meme requete.
|
|
70
|
+
* etat, un export long laisse croire que le clic n'a pas pris et
|
|
71
|
+
* l'utilisateur relance la meme requete.
|
|
21
72
|
*/
|
|
22
73
|
loading?: boolean
|
|
74
|
+
/** Teinte de marque d'une action `primary` : la couleur du produit courant. */
|
|
75
|
+
accent?: 'healthcare' | 'discovery' | 'databank' | 'pharma'
|
|
23
76
|
}
|
|
24
77
|
|
|
25
|
-
withDefaults(defineProps<RmButtonProps>(), {
|
|
26
|
-
variant: 'primary
|
|
78
|
+
const props = withDefaults(defineProps<RmButtonProps>(), {
|
|
79
|
+
variant: 'primary',
|
|
27
80
|
size: 'md',
|
|
28
81
|
disabled: false,
|
|
29
82
|
block: false,
|
|
83
|
+
pill: false,
|
|
84
|
+
iconOnly: false,
|
|
85
|
+
ghost: false,
|
|
30
86
|
type: 'button',
|
|
31
87
|
prefixIcon: undefined,
|
|
32
88
|
suffixIcon: undefined,
|
|
33
89
|
loading: false,
|
|
90
|
+
accent: undefined,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
defineEmits<{ click: [event: MouseEvent] }>()
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Ou retombe chaque ancien nom. Le fond plein devient l'emphase forte, le
|
|
97
|
+
* contour l'emphase moyenne, le transparent l'emphase basse.
|
|
98
|
+
*/
|
|
99
|
+
const LEGACY: Record<string, { variant: RmButtonVariant, ghost?: boolean, accent?: RmButtonProps['accent'] }> = {
|
|
100
|
+
'primary-fill': { variant: 'primary' },
|
|
101
|
+
'primary-stroke': { variant: 'secondary' },
|
|
102
|
+
'secondary-fill': { variant: 'tertiary' },
|
|
103
|
+
'secondary-stroke': { variant: 'secondary' },
|
|
104
|
+
'secondary-transparent': { variant: 'tertiary', ghost: true },
|
|
105
|
+
'outline': { variant: 'secondary' },
|
|
106
|
+
'ghost': { variant: 'tertiary', ghost: true },
|
|
107
|
+
'discovery-fill': { variant: 'primary', accent: 'discovery' },
|
|
108
|
+
'discovery-stroke': { variant: 'secondary' },
|
|
109
|
+
'databank-fill': { variant: 'primary', accent: 'databank' },
|
|
110
|
+
'databank-stroke': { variant: 'secondary' },
|
|
111
|
+
'pharma-fill': { variant: 'primary', accent: 'pharma' },
|
|
112
|
+
'pharma-stroke': { variant: 'secondary' },
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resolved = computed(() => LEGACY[props.variant] ?? { variant: props.variant as RmButtonVariant })
|
|
116
|
+
const variantClass = computed(() => `rm-button--${resolved.value.variant}`)
|
|
117
|
+
const isGhost = computed(() => props.ghost || resolved.value.ghost === true)
|
|
118
|
+
const accentClass = computed(() => {
|
|
119
|
+
const accent = props.accent ?? resolved.value.accent
|
|
120
|
+
return accent ? `rm-button--accent-${accent}` : undefined
|
|
34
121
|
})
|
|
35
122
|
|
|
36
|
-
|
|
37
|
-
click: [event: MouseEvent]
|
|
38
|
-
}>()
|
|
123
|
+
const iconSize = computed(() => (props.size === 'sm' ? 14 : props.size === 'lg' ? 20 : 16))
|
|
39
124
|
</script>
|
|
40
125
|
|
|
41
126
|
<template>
|
|
@@ -45,19 +130,26 @@ defineEmits<{
|
|
|
45
130
|
:aria-busy="loading ? 'true' : undefined"
|
|
46
131
|
:class="[
|
|
47
132
|
'rm-button',
|
|
48
|
-
|
|
133
|
+
variantClass,
|
|
134
|
+
accentClass,
|
|
49
135
|
`rm-button--${size}`,
|
|
50
|
-
{
|
|
136
|
+
{
|
|
137
|
+
'rm-button--block': block,
|
|
138
|
+
'rm-button--pill': pill,
|
|
139
|
+
'rm-button--icon': iconOnly,
|
|
140
|
+
'rm-button--ghost': isGhost,
|
|
141
|
+
'rm-button--loading': loading,
|
|
142
|
+
},
|
|
51
143
|
]"
|
|
52
144
|
@click="$emit('click', $event)"
|
|
53
145
|
>
|
|
54
|
-
<RmIcon v-if="loading" name="CircleNotch" :size="
|
|
55
|
-
<RmIcon v-else-if="prefixIcon" :name="prefixIcon" :size="
|
|
146
|
+
<RmIcon v-if="loading" name="CircleNotch" :size="iconSize" class="rm-button__spinner" />
|
|
147
|
+
<RmIcon v-else-if="prefixIcon" :name="prefixIcon" :size="iconSize" />
|
|
56
148
|
<slot />
|
|
57
|
-
<RmIcon v-if="suffixIcon" :name="suffixIcon" :size="
|
|
149
|
+
<RmIcon v-if="suffixIcon && !loading" :name="suffixIcon" :size="iconSize" />
|
|
58
150
|
</button>
|
|
59
151
|
</template>
|
|
60
152
|
|
|
61
153
|
<style scoped>
|
|
62
|
-
.rm-button
|
|
154
|
+
.rm-button{align-items:center;border:1px solid transparent;border-radius:8px;cursor:pointer;display:inline-flex;font-family:inherit;font-size:13px;font-weight:600;gap:6px;justify-content:center;padding:9px 16px;transition:background var(--duration-fast,.15s),color var(--duration-fast,.15s),border-color var(--duration-fast,.15s);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.rm-button:focus-visible{outline:3px solid var(--color-secondary);outline-offset:2px}.rm-button:disabled{cursor:not-allowed;opacity:.5;pointer-events:none}.rm-button--loading{cursor:progress}.rm-button__spinner{animation:rm-button-spin .9s linear infinite}@keyframes rm-button-spin{to{transform:rotate(1turn)}}.rm-button--primary{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-button--primary:hover:not(:disabled){background:var(--color-primary-hover);border-color:var(--color-primary-hover)}.rm-button--secondary{background:var(--bg-surface);border-color:var(--border-default);color:var(--text-secondary)}.rm-button--secondary:hover:not(:disabled){background:var(--bg-surface-3);border-color:var(--color-violet-pale);color:var(--text-primary)}.rm-button--tertiary{background:var(--bg-surface-3);color:var(--text-secondary)}.rm-button--tertiary:hover:not(:disabled){background:var(--bg-hover);color:var(--color-secondary)}.rm-button--text,.rm-button--text-danger{background:transparent;border:none;padding:4px 0;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px}.rm-button--text{color:var(--color-primary)}.rm-button--text:hover:not(:disabled){color:var(--color-primary-hover);text-decoration-thickness:2px}.rm-button--text-danger{color:var(--color-danger)}.rm-button--text-danger:hover:not(:disabled){color:var(--color-danger);opacity:.8;text-decoration-thickness:2px}.rm-button--danger{background:var(--bg-surface);border-color:var(--color-danger);color:var(--color-danger)}.rm-button--danger:hover:not(:disabled){background:var(--color-danger);color:var(--color-text-inverse)}.rm-button--accent-healthcare{background:var(--color-healthcare);border-color:var(--color-healthcare)}.rm-button--accent-discovery{background:var(--color-discovery);border-color:var(--color-discovery)}.rm-button--accent-databank{background:var(--color-databank);border-color:var(--color-databank)}.rm-button--accent-pharma{background:var(--color-pharma);border-color:var(--color-pharma)}.rm-button--sm{font-size:12px;padding:6px 12px}.rm-button--lg{font-size:14px;padding:12px 22px}.rm-button--text-danger.rm-button--lg,.rm-button--text-danger.rm-button--sm,.rm-button--text.rm-button--lg,.rm-button--text.rm-button--sm{padding:4px 0}.rm-button--pill{border-radius:var(--radius-pill,999px)}.rm-button--block{display:flex;width:100%}.rm-button--icon{background:var(--bg-surface);border-color:var(--border-default);color:var(--text-secondary);height:32px;padding:0;width:32px}.rm-button--icon:hover:not(:disabled){background:var(--bg-hover);border-color:var(--color-violet-pale);color:var(--color-secondary)}.rm-button--icon.rm-button--sm{height:28px;width:28px}.rm-button--icon.rm-button--lg{height:40px;width:40px}.rm-button--ghost{background:transparent;border-color:transparent}.rm-button--ghost:hover:not(:disabled){background:var(--bg-hover);border-color:transparent}
|
|
63
155
|
</style>
|
|
@@ -29,7 +29,21 @@ const props = withDefaults(defineProps<{
|
|
|
29
29
|
description: undefined,
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* ECharts arrive en import dynamique : le premier graphe d'une session attend
|
|
34
|
+
* le telechargement du morceau, plusieurs centaines de kilooctets, pendant
|
|
35
|
+
* lesquelles la carte n'est qu'un vide de la hauteur du graphe. Un squelette
|
|
36
|
+
* tient la place le temps du premier trace.
|
|
37
|
+
*
|
|
38
|
+
* Le module est garde ici, hors du composant : une fois charge, les graphes
|
|
39
|
+
* suivants s'initialisent sans attente et ne doivent PAS faire clignoter un
|
|
40
|
+
* squelette d'une frame. `pending` part donc de l'etat du cache, pas de
|
|
41
|
+
* `true`.
|
|
42
|
+
*/
|
|
43
|
+
let echartsModule: typeof import('echarts') | null = null
|
|
44
|
+
|
|
32
45
|
const el = ref<HTMLElement | null>(null)
|
|
46
|
+
const pending = ref(echartsModule === null)
|
|
33
47
|
const chart = shallowRef<{ setOption: (o: unknown, n?: boolean) => void, resize: () => void, dispose: () => void } | null>(null)
|
|
34
48
|
let observer: ResizeObserver | null = null
|
|
35
49
|
|
|
@@ -38,9 +52,12 @@ const heightPx = () =>
|
|
|
38
52
|
|
|
39
53
|
onMounted(async () => {
|
|
40
54
|
if (!el.value) return
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
echartsModule = echartsModule ?? await import('echarts')
|
|
56
|
+
// Le composant peut avoir ete demonte pendant l'attente du morceau.
|
|
57
|
+
if (!el.value) return
|
|
58
|
+
chart.value = echartsModule.init(el.value, undefined, { renderer: 'svg' })
|
|
43
59
|
chart.value.setOption(props.option)
|
|
60
|
+
pending.value = false
|
|
44
61
|
|
|
45
62
|
// ResizeObserver plutot qu'un listener global sur window : un graphe dans un
|
|
46
63
|
// panneau repliable ou une grille change de taille sans que la fenetre bouge.
|
|
@@ -65,11 +82,14 @@ defineExpose({ instance: chart })
|
|
|
65
82
|
|
|
66
83
|
<template>
|
|
67
84
|
<figure class="rm-chart">
|
|
68
|
-
<div
|
|
85
|
+
<div class="rm-chart__frame" :style="{ height: `${heightPx()}px` }">
|
|
86
|
+
<div ref="el" class="echart" :style="{ height: `${heightPx()}px` }" role="img" :aria-label="description" />
|
|
87
|
+
<RmSkeleton v-if="pending" class="rm-chart__skeleton" width="100%" :height="heightPx()" radius="8px" />
|
|
88
|
+
</div>
|
|
69
89
|
<figcaption v-if="description" class="sr-only">{{ description }}</figcaption>
|
|
70
90
|
</figure>
|
|
71
91
|
</template>
|
|
72
92
|
|
|
73
93
|
<style scoped>
|
|
74
|
-
.rm-chart{margin:0}.echart{width:100%}
|
|
94
|
+
.rm-chart{margin:0}.rm-chart__frame{position:relative}.echart{width:100%}.rm-chart__skeleton{inset:0;position:absolute}
|
|
75
95
|
</style>
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmTable - la table du design system.
|
|
3
|
+
|
|
4
|
+
Des qu'il faut comparer plusieurs entites sur plusieurs dimensions. Pour une
|
|
5
|
+
seule metrique, c'est un KPI ; pour une liste sans structure de colonnes,
|
|
6
|
+
c'est une `<ul>`.
|
|
7
|
+
|
|
8
|
+
Ce que la reference impose et que le composant tient a votre place :
|
|
9
|
+
|
|
10
|
+
- Un nom accessible. `caption` est pose en `<caption>`, masque a l'oeil par
|
|
11
|
+
defaut parce que le titre de la carte le porte deja a l'ecran. Sans lui,
|
|
12
|
+
un lecteur d'ecran annonce « tableau » et rien d'autre.
|
|
13
|
+
- `scope="col"` sur chaque en-tete, et la colonne d'identite rendue en
|
|
14
|
+
`<th scope="row">` : la cellule lue en premier nomme la ligne au lieu de
|
|
15
|
+
reciter la premiere valeur.
|
|
16
|
+
- `aria-sort` sur les colonnes triables, et le tri declenche par un vrai
|
|
17
|
+
bouton : un `<th>` avec un `@click` ne s'atteint pas au clavier.
|
|
18
|
+
- Pas de bordure verticale. La zebre suffit a suivre une ligne, les traits
|
|
19
|
+
verticaux ajoutent du bruit a chaque colonne.
|
|
20
|
+
- Les nombres a droite en chiffres a chasse fixe : sans cela les unites ne
|
|
21
|
+
tombent pas les unes sous les autres et la colonne ne se compare plus.
|
|
22
|
+
|
|
23
|
+
Les variantes de la reference sont des drapeaux : `compact`, `sticky-header`,
|
|
24
|
+
`selectable`, `expandable`, plus `groupBy` pour les sections et `heat` par
|
|
25
|
+
colonne pour la carte de chaleur.
|
|
26
|
+
-->
|
|
1
27
|
<script setup lang="ts">
|
|
2
28
|
import { computed, ref } from 'vue'
|
|
3
|
-
import { PhCaretUp, PhCaretDown } from '@phosphor-icons/vue'
|
|
4
29
|
|
|
5
30
|
export interface RmTableColumn {
|
|
6
31
|
key: string
|
|
@@ -8,30 +33,70 @@ export interface RmTableColumn {
|
|
|
8
33
|
sortable?: boolean
|
|
9
34
|
align?: 'left' | 'center' | 'right'
|
|
10
35
|
width?: string
|
|
36
|
+
/**
|
|
37
|
+
* Colonne de nombres : alignee a droite, chiffres a chasse fixe. C'est ce
|
|
38
|
+
* qui aligne les unites entre elles et rend la colonne comparable d'un coup
|
|
39
|
+
* d'oeil.
|
|
40
|
+
*/
|
|
41
|
+
numeric?: boolean
|
|
42
|
+
/**
|
|
43
|
+
* Colonne d'identite : la cellule sort en `<th scope="row">`. Une seule
|
|
44
|
+
* colonne par table, la premiere en general (le pays, le produit).
|
|
45
|
+
*/
|
|
46
|
+
rowHeader?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* Carte de chaleur : renvoie le niveau de 1 (bas) a 5 (haut), ou `null`
|
|
49
|
+
* pour laisser la cellule nue. La valeur reste ecrite dans la cellule, la
|
|
50
|
+
* couleur ne la remplace jamais.
|
|
51
|
+
*/
|
|
52
|
+
heat?: (value: any, row: Record<string, any>) => 1 | 2 | 3 | 4 | 5 | null
|
|
11
53
|
}
|
|
12
54
|
|
|
13
55
|
export interface RmTableProps {
|
|
14
56
|
columns: RmTableColumn[]
|
|
15
57
|
rows: Record<string, any>[]
|
|
58
|
+
/** Nom accessible de la table. Masque a l'oeil sauf `caption-visible`. */
|
|
59
|
+
caption?: string
|
|
60
|
+
captionVisible?: boolean
|
|
16
61
|
sortField?: string
|
|
17
62
|
sortOrder?: 'asc' | 'desc'
|
|
18
63
|
striped?: boolean
|
|
19
64
|
hoverable?: boolean
|
|
65
|
+
/** Rythme dense : 12px de texte, 6/12 de gouttiere. Pour les longues tables. */
|
|
66
|
+
compact?: boolean
|
|
20
67
|
stickyHeader?: boolean
|
|
21
68
|
maxHeight?: string
|
|
22
69
|
loading?: boolean
|
|
23
70
|
emptyText?: string
|
|
71
|
+
/** Case a cocher par ligne, plus une barre d'actions groupees. */
|
|
72
|
+
selectable?: boolean
|
|
73
|
+
/** Champ qui identifie une ligne. Sans lui, c'est l'index. */
|
|
74
|
+
rowKey?: string
|
|
75
|
+
/** Regroupe les lignes par la valeur de ce champ, sous un intitule de section. */
|
|
76
|
+
groupBy?: string
|
|
77
|
+
/** Intitule d'une section, a partir de la valeur groupee. */
|
|
78
|
+
groupLabel?: (value: any) => string
|
|
79
|
+
/** Chaque ligne se deplie sur un detail, rendu par le slot `expanded`. */
|
|
80
|
+
expandable?: boolean
|
|
24
81
|
}
|
|
25
82
|
|
|
26
83
|
const props = withDefaults(defineProps<RmTableProps>(), {
|
|
84
|
+
caption: '',
|
|
85
|
+
captionVisible: false,
|
|
27
86
|
sortField: '',
|
|
28
87
|
sortOrder: 'asc',
|
|
29
88
|
striped: true,
|
|
30
89
|
hoverable: true,
|
|
90
|
+
compact: false,
|
|
31
91
|
stickyHeader: true,
|
|
32
92
|
maxHeight: '500px',
|
|
33
93
|
loading: false,
|
|
34
|
-
emptyText: '
|
|
94
|
+
emptyText: 'Aucune donnée',
|
|
95
|
+
selectable: false,
|
|
96
|
+
rowKey: '',
|
|
97
|
+
groupBy: '',
|
|
98
|
+
groupLabel: undefined,
|
|
99
|
+
expandable: false,
|
|
35
100
|
})
|
|
36
101
|
|
|
37
102
|
const emit = defineEmits<{
|
|
@@ -39,124 +104,259 @@ const emit = defineEmits<{
|
|
|
39
104
|
rowClick: [row: Record<string, any>, index: number]
|
|
40
105
|
}>()
|
|
41
106
|
|
|
107
|
+
/** Les lignes retenues, par leur cle. */
|
|
108
|
+
const selected = defineModel<(string | number)[]>('selected', { default: () => [] })
|
|
109
|
+
|
|
42
110
|
const currentSort = ref(props.sortField)
|
|
43
111
|
const currentOrder = ref<'asc' | 'desc'>(props.sortOrder)
|
|
112
|
+
const expanded = ref<(string | number)[]>([])
|
|
44
113
|
|
|
45
114
|
function toggleSort(col: RmTableColumn) {
|
|
46
115
|
if (!col.sortable) return
|
|
47
|
-
|
|
48
116
|
if (currentSort.value === col.key) {
|
|
49
117
|
currentOrder.value = currentOrder.value === 'asc' ? 'desc' : 'asc'
|
|
50
|
-
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
51
120
|
currentSort.value = col.key
|
|
52
121
|
currentOrder.value = 'asc'
|
|
53
122
|
}
|
|
54
|
-
|
|
55
123
|
emit('sort', currentSort.value, currentOrder.value)
|
|
56
124
|
}
|
|
57
125
|
|
|
58
|
-
|
|
126
|
+
/** `aria-sort` ne se pose que sur la colonne triee : ailleurs c'est `none`. */
|
|
127
|
+
function ariaSort(col: RmTableColumn) {
|
|
128
|
+
if (!col.sortable) return undefined
|
|
129
|
+
if (currentSort.value !== col.key) return 'none'
|
|
130
|
+
return currentOrder.value === 'asc' ? 'ascending' : 'descending'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function cellValue(row: Record<string, any>, key: string): any {
|
|
59
134
|
return key.split('.').reduce((obj, k) => obj?.[k], row)
|
|
60
135
|
}
|
|
61
136
|
|
|
137
|
+
function keyOf(row: Record<string, any>, index: number) {
|
|
138
|
+
return props.rowKey ? cellValue(row, props.rowKey) : index
|
|
139
|
+
}
|
|
140
|
+
|
|
62
141
|
const sortedRows = computed(() => {
|
|
63
142
|
if (!currentSort.value) return props.rows
|
|
64
|
-
|
|
65
143
|
return [...props.rows].sort((a, b) => {
|
|
66
|
-
const aVal =
|
|
67
|
-
const bVal =
|
|
68
|
-
|
|
144
|
+
const aVal = cellValue(a, currentSort.value) ?? ''
|
|
145
|
+
const bVal = cellValue(b, currentSort.value) ?? ''
|
|
69
146
|
const aStr = Array.isArray(aVal) ? aVal.join(', ') : String(aVal)
|
|
70
147
|
const bStr = Array.isArray(bVal) ? bVal.join(', ') : String(bVal)
|
|
71
|
-
|
|
72
148
|
const aNum = Number(aStr)
|
|
73
149
|
const bNum = Number(bStr)
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const cmp = isNumeric ? aNum - bNum : aStr.localeCompare(bStr)
|
|
150
|
+
const numeric = !Number.isNaN(aNum) && !Number.isNaN(bNum) && aStr !== '' && bStr !== ''
|
|
151
|
+
const cmp = numeric ? aNum - bNum : aStr.localeCompare(bStr)
|
|
77
152
|
return currentOrder.value === 'asc' ? cmp : -cmp
|
|
78
153
|
})
|
|
79
154
|
})
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Les lignes decoupees en sections. Sans `groupBy`, une seule section sans
|
|
158
|
+
* intitule : le rendu reste une suite de `<tr>` dans un seul `<tbody>`.
|
|
159
|
+
*/
|
|
160
|
+
const sections = computed(() => {
|
|
161
|
+
const rows = sortedRows.value.map((row, index) => ({ row, index }))
|
|
162
|
+
if (!props.groupBy) return [{ label: '', rows }]
|
|
163
|
+
|
|
164
|
+
const buckets = new Map<any, typeof rows>()
|
|
165
|
+
for (const entry of rows) {
|
|
166
|
+
const value = cellValue(entry.row, props.groupBy)
|
|
167
|
+
if (!buckets.has(value)) buckets.set(value, [])
|
|
168
|
+
buckets.get(value)!.push(entry)
|
|
169
|
+
}
|
|
170
|
+
return [...buckets.entries()].map(([value, group]) => ({
|
|
171
|
+
label: props.groupLabel ? props.groupLabel(value) : String(value ?? ''),
|
|
172
|
+
rows: group,
|
|
173
|
+
}))
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const colspan = computed(
|
|
177
|
+
() => props.columns.length + (props.selectable ? 1 : 0) + (props.expandable ? 1 : 0),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
const allKeys = computed(() => sortedRows.value.map((row, index) => keyOf(row, index)))
|
|
181
|
+
const allSelected = computed(
|
|
182
|
+
() => allKeys.value.length > 0 && allKeys.value.every(key => selected.value.includes(key)),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
function toggleAll() {
|
|
186
|
+
selected.value = allSelected.value ? [] : [...allKeys.value]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toggleRow(key: string | number) {
|
|
190
|
+
selected.value = selected.value.includes(key)
|
|
191
|
+
? selected.value.filter(k => k !== key)
|
|
192
|
+
: [...selected.value, key]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function toggleExpand(key: string | number) {
|
|
196
|
+
expanded.value = expanded.value.includes(key)
|
|
197
|
+
? expanded.value.filter(k => k !== key)
|
|
198
|
+
: [...expanded.value, key]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function heatClass(col: RmTableColumn, row: Record<string, any>) {
|
|
202
|
+
if (!col.heat) return undefined
|
|
203
|
+
const level = col.heat(cellValue(row, col.key), row)
|
|
204
|
+
return level ? `rm-table__td--heat-${level}` : undefined
|
|
205
|
+
}
|
|
80
206
|
</script>
|
|
81
207
|
|
|
82
208
|
<template>
|
|
83
|
-
<div class="rm-table-wrapper"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
>
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
</
|
|
153
|
-
</
|
|
154
|
-
</
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
<div class="rm-table-wrapper">
|
|
210
|
+
<!-- La barre d'actions groupees ne sort qu'a partir d'une ligne retenue :
|
|
211
|
+
posee en permanence, elle occuperait une bande vide en haut de table. -->
|
|
212
|
+
<div v-if="selectable && selected.length > 0" class="rm-table__bulk">
|
|
213
|
+
<span class="rm-table__bulk-count">
|
|
214
|
+
<b>{{ selected.length }}</b> {{ selected.length > 1 ? 'lignes retenues' : 'ligne retenue' }}
|
|
215
|
+
</span>
|
|
216
|
+
<span class="rm-table__bulk-actions">
|
|
217
|
+
<slot name="bulk" :selected="selected" />
|
|
218
|
+
</span>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div
|
|
222
|
+
class="rm-table__scroll"
|
|
223
|
+
:style="stickyHeader && maxHeight !== 'auto' ? { maxHeight } : undefined"
|
|
224
|
+
>
|
|
225
|
+
<table
|
|
226
|
+
class="rm-table"
|
|
227
|
+
:class="{
|
|
228
|
+
'rm-table--compact': compact,
|
|
229
|
+
'rm-table--sticky': stickyHeader,
|
|
230
|
+
'rm-table--hoverable': hoverable,
|
|
231
|
+
}"
|
|
232
|
+
>
|
|
233
|
+
<caption v-if="caption" :class="{ 'rm-sr-only': !captionVisible }">{{ caption }}</caption>
|
|
234
|
+
|
|
235
|
+
<thead class="rm-table__head">
|
|
236
|
+
<tr>
|
|
237
|
+
<th v-if="selectable" scope="col" class="rm-table__th rm-table__th--check">
|
|
238
|
+
<input
|
|
239
|
+
type="checkbox"
|
|
240
|
+
:checked="allSelected"
|
|
241
|
+
aria-label="Tout sélectionner"
|
|
242
|
+
@change="toggleAll"
|
|
243
|
+
>
|
|
244
|
+
</th>
|
|
245
|
+
<th v-if="expandable" scope="col" class="rm-table__th rm-table__th--expand">
|
|
246
|
+
<span class="rm-sr-only">Détail</span>
|
|
247
|
+
</th>
|
|
248
|
+
<th
|
|
249
|
+
v-for="col in columns"
|
|
250
|
+
:key="col.key"
|
|
251
|
+
scope="col"
|
|
252
|
+
class="rm-table__th"
|
|
253
|
+
:class="[
|
|
254
|
+
(col.numeric ? 'rm-table__th--num' : col.align && `rm-table__th--${col.align}`),
|
|
255
|
+
]"
|
|
256
|
+
:style="col.width ? { width: col.width } : undefined"
|
|
257
|
+
:aria-sort="ariaSort(col)"
|
|
258
|
+
>
|
|
259
|
+
<!-- Le tri passe par un bouton : un `<th>` cliquable n'est ni
|
|
260
|
+
atteignable au clavier ni annonce comme actionnable. -->
|
|
261
|
+
<button
|
|
262
|
+
v-if="col.sortable"
|
|
263
|
+
type="button"
|
|
264
|
+
class="rm-table__sort"
|
|
265
|
+
@click="toggleSort(col)"
|
|
266
|
+
>
|
|
267
|
+
{{ col.label }}
|
|
268
|
+
</button>
|
|
269
|
+
<template v-else>{{ col.label }}</template>
|
|
270
|
+
</th>
|
|
271
|
+
</tr>
|
|
272
|
+
</thead>
|
|
273
|
+
|
|
274
|
+
<tbody v-if="loading">
|
|
275
|
+
<tr v-for="i in 5" :key="i">
|
|
276
|
+
<td v-for="n in colspan" :key="n" class="rm-table__td">
|
|
277
|
+
<div class="rm-skeleton rm-table__skeleton" />
|
|
278
|
+
</td>
|
|
279
|
+
</tr>
|
|
280
|
+
</tbody>
|
|
281
|
+
|
|
282
|
+
<tbody v-else-if="sortedRows.length === 0">
|
|
283
|
+
<tr>
|
|
284
|
+
<td :colspan="colspan" class="rm-table__empty">{{ emptyText }}</td>
|
|
285
|
+
</tr>
|
|
286
|
+
</tbody>
|
|
287
|
+
|
|
288
|
+
<tbody v-for="(section, s) in sections" v-else :key="s">
|
|
289
|
+
<tr v-if="section.label" class="rm-table__group">
|
|
290
|
+
<th :colspan="colspan" scope="colgroup">{{ section.label }}</th>
|
|
291
|
+
</tr>
|
|
292
|
+
|
|
293
|
+
<template v-for="(entry, i) in section.rows" :key="keyOf(entry.row, entry.index)">
|
|
294
|
+
<!-- La zebre suit le rang dans la section, pas la position dans le
|
|
295
|
+
DOM : les lignes de detail depliees decaleraient la parite. -->
|
|
296
|
+
<tr
|
|
297
|
+
class="rm-table__row"
|
|
298
|
+
:class="{
|
|
299
|
+
'rm-table__row--striped': striped && i % 2 === 1,
|
|
300
|
+
'rm-table__row--selected': selectable && selected.includes(keyOf(entry.row, entry.index)),
|
|
301
|
+
'rm-table__row--expandable': expandable,
|
|
302
|
+
}"
|
|
303
|
+
@click="expandable ? toggleExpand(keyOf(entry.row, entry.index)) : emit('rowClick', entry.row, entry.index)"
|
|
304
|
+
>
|
|
305
|
+
<td v-if="selectable" class="rm-table__td rm-table__td--check" @click.stop>
|
|
306
|
+
<input
|
|
307
|
+
type="checkbox"
|
|
308
|
+
:checked="selected.includes(keyOf(entry.row, entry.index))"
|
|
309
|
+
:aria-label="`Retenir la ligne ${i + 1}`"
|
|
310
|
+
@change="toggleRow(keyOf(entry.row, entry.index))"
|
|
311
|
+
>
|
|
312
|
+
</td>
|
|
313
|
+
|
|
314
|
+
<td v-if="expandable" class="rm-table__td rm-table__td--expand">
|
|
315
|
+
<span
|
|
316
|
+
class="rm-table__caret"
|
|
317
|
+
:class="{ 'is-open': expanded.includes(keyOf(entry.row, entry.index)) }"
|
|
318
|
+
>
|
|
319
|
+
<RmIcon name="CaretRight" :size="14" />
|
|
320
|
+
</span>
|
|
321
|
+
</td>
|
|
322
|
+
|
|
323
|
+
<component
|
|
324
|
+
:is="col.rowHeader ? 'th' : 'td'"
|
|
325
|
+
v-for="col in columns"
|
|
326
|
+
:key="col.key"
|
|
327
|
+
:scope="col.rowHeader ? 'row' : undefined"
|
|
328
|
+
class="rm-table__td"
|
|
329
|
+
:class="[
|
|
330
|
+
(col.numeric ? 'rm-table__td--num' : col.align && `rm-table__td--${col.align}`),
|
|
331
|
+
heatClass(col, entry.row),
|
|
332
|
+
]"
|
|
333
|
+
>
|
|
334
|
+
<slot
|
|
335
|
+
:name="col.key"
|
|
336
|
+
:value="cellValue(entry.row, col.key)"
|
|
337
|
+
:row="entry.row"
|
|
338
|
+
:index="entry.index"
|
|
339
|
+
>{{ cellValue(entry.row, col.key) ?? '-' }}</slot>
|
|
340
|
+
</component>
|
|
341
|
+
</tr>
|
|
342
|
+
|
|
343
|
+
<tr
|
|
344
|
+
v-if="expandable && expanded.includes(keyOf(entry.row, entry.index))"
|
|
345
|
+
class="rm-table__detail"
|
|
346
|
+
>
|
|
347
|
+
<td :colspan="colspan">
|
|
348
|
+
<div class="rm-table__detail-body">
|
|
349
|
+
<slot name="expanded" :row="entry.row" :index="entry.index" />
|
|
350
|
+
</div>
|
|
351
|
+
</td>
|
|
352
|
+
</tr>
|
|
353
|
+
</template>
|
|
354
|
+
</tbody>
|
|
355
|
+
</table>
|
|
356
|
+
</div>
|
|
157
357
|
</div>
|
|
158
358
|
</template>
|
|
159
359
|
|
|
160
360
|
<style scoped>
|
|
161
|
-
.rm-table-wrapper{background
|
|
361
|
+
.rm-table-wrapper{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);flex:1;overflow:hidden}.rm-table__scroll{overflow:auto}.rm-table{border-collapse:collapse;font-size:var(--font-13,13px);width:100%}.rm-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-table caption{caption-side:top;color:var(--text-primary);font-size:var(--font-13,13px);font-weight:700;padding:0 0 var(--space-8,8px);text-align:left}.rm-table__th{background:var(--bg-surface-2);border-bottom:1px solid var(--border-default);color:var(--text-secondary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:12px 14px;text-align:left;text-transform:uppercase;white-space:nowrap}.rm-table--sticky .rm-table__th{border-bottom:none;box-shadow:inset 0 -1px 0 var(--border-default);position:sticky;top:0;z-index:2}.rm-table__th--center{text-align:center}.rm-table__th--num,.rm-table__th--right{text-align:right}.rm-table__th--check,.rm-table__th--expand{padding-right:0;width:36px}.rm-table__sort{align-items:center;background:none;border:none;color:inherit;cursor:pointer;display:inline-flex;font:inherit;gap:var(--space-6,6px);letter-spacing:inherit;padding:0;text-transform:inherit}.rm-table__sort:hover{color:var(--text-primary)}.rm-table__sort:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--text-muted);content:"";display:inline-block;height:0;opacity:.5;vertical-align:middle;width:0}.rm-table__th[aria-sort=ascending] .rm-table__sort:after{border-bottom:5px solid var(--color-primary);border-top:none;opacity:1}.rm-table__th[aria-sort=descending] .rm-table__sort:after{border-top:5px solid var(--color-primary);opacity:1}.rm-table a:focus-visible,.rm-table button:focus-visible{border-radius:var(--radius-sm,4px);outline:2px solid var(--color-primary);outline-offset:2px}.rm-table__td{border-bottom:1px solid var(--border-soft);color:var(--text-primary);padding:12px 14px;text-align:left;vertical-align:top}th.rm-table__td{background:transparent;font-weight:700;letter-spacing:0;text-transform:none}.rm-table__td--center{text-align:center}.rm-table__td--right{text-align:right}.rm-table__td--num{font-variant-numeric:tabular-nums;text-align:right}.rm-table__td--check,.rm-table__td--expand{color:var(--text-muted);padding-right:0;width:36px}.rm-table__row--striped>.rm-table__td{background:var(--bg-surface-2)}.rm-table--hoverable .rm-table__row:hover>.rm-table__td,.rm-table__row--selected>.rm-table__td{background:var(--bg-hover)}.rm-table__row--expandable{cursor:pointer}.rm-table__caret{display:inline-flex;transition:transform .16s var(--ease-out,ease)}.rm-table__caret.is-open{color:var(--color-primary);transform:rotate(90deg)}.rm-table__group>th{background:var(--bg-hover);color:var(--color-primary);font-size:var(--font-11,11px);font-weight:700;letter-spacing:.08em;padding:var(--space-8,8px) var(--space-12,12px);text-align:left;text-transform:uppercase}.rm-table__detail>td{background:var(--bg-surface-2);border-bottom:1px solid var(--border-soft);padding:0}.rm-table__detail-body{color:var(--text-secondary);font-size:var(--font-12,12px);line-height:1.6;padding:var(--space-12,12px) var(--space-20,20px)}.rm-table__td--heat-1{background:#fee5e5!important;color:#911616}.rm-table__td--heat-2{background:#ffe7d2!important;color:#8a4309}.rm-table__td--heat-3{background:#fff6cc!important;color:#6b5400}.rm-table__td--heat-4{background:#d8f3dc!important;color:#1b5e25}.rm-table__td--heat-5{background:#b7e4c7!important;color:#0e4b19}:root[data-theme=dark] .rm-table__td--heat-1{background:#3f1414!important;color:#ffb4b4}:root[data-theme=dark] .rm-table__td--heat-2{background:#3d2410!important;color:#ffc58a}:root[data-theme=dark] .rm-table__td--heat-3{background:#3a3210!important;color:#ffe07a}:root[data-theme=dark] .rm-table__td--heat-4{background:#18351f!important;color:#8ae0a4}:root[data-theme=dark] .rm-table__td--heat-5{background:#0f4019!important;color:#b4f0c2}.rm-table--compact .rm-table__td,.rm-table--compact .rm-table__th{font-size:var(--font-12,12px);padding:var(--space-6,6px) var(--space-12,12px)}.rm-table--compact .rm-table__th{font-size:var(--font-10,10px)}.rm-table__bulk{align-items:center;background:var(--bg-hover);border-bottom:1px solid var(--color-violet-pale);display:flex;font-size:var(--font-12,12px);justify-content:space-between;padding:var(--space-8,8px) var(--space-16,16px)}.rm-table__bulk-count b{color:var(--color-primary)}.rm-table__bulk-actions{display:flex;gap:var(--space-4,4px)}.rm-table__empty{color:var(--text-muted);padding:32px 16px;text-align:center}.rm-table__skeleton{height:14px;width:80%}
|
|
162
362
|
</style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref } from "vue";
|
|
1
|
+
import { ref, toRaw } from "vue";
|
|
2
2
|
const view = ref(null);
|
|
3
3
|
export function useRmFilterBarView() {
|
|
4
4
|
let mine = null;
|
|
@@ -9,7 +9,7 @@ export function useRmFilterBarView() {
|
|
|
9
9
|
view.value = next;
|
|
10
10
|
},
|
|
11
11
|
clear: () => {
|
|
12
|
-
if (view.value === mine) view.value = null;
|
|
12
|
+
if (view.value && toRaw(view.value) === mine) view.value = null;
|
|
13
13
|
mine = null;
|
|
14
14
|
}
|
|
15
15
|
};
|
|
@@ -94,6 +94,48 @@ export declare function rmBaseOption(theme: RmChartTheme, decal?: boolean): {
|
|
|
94
94
|
borderWidth: number;
|
|
95
95
|
};
|
|
96
96
|
};
|
|
97
|
+
export declare function rmLegend(theme: RmChartTheme, count: number, position?: 'top' | 'bottom'): {
|
|
98
|
+
pageIconSize?: number | undefined;
|
|
99
|
+
pageIconColor?: string | undefined;
|
|
100
|
+
pageIconInactiveColor?: string | undefined;
|
|
101
|
+
pageTextStyle?: {
|
|
102
|
+
color: string;
|
|
103
|
+
fontSize: number;
|
|
104
|
+
fontFamily: string;
|
|
105
|
+
} | undefined;
|
|
106
|
+
type: string;
|
|
107
|
+
tooltip: {
|
|
108
|
+
show: boolean;
|
|
109
|
+
};
|
|
110
|
+
formatter: (name: string) => string;
|
|
111
|
+
textStyle: {
|
|
112
|
+
color: string;
|
|
113
|
+
fontSize: number;
|
|
114
|
+
fontFamily: string;
|
|
115
|
+
};
|
|
116
|
+
bottom: number;
|
|
117
|
+
left: string;
|
|
118
|
+
} | {
|
|
119
|
+
pageIconSize?: number | undefined;
|
|
120
|
+
pageIconColor?: string | undefined;
|
|
121
|
+
pageIconInactiveColor?: string | undefined;
|
|
122
|
+
pageTextStyle?: {
|
|
123
|
+
color: string;
|
|
124
|
+
fontSize: number;
|
|
125
|
+
fontFamily: string;
|
|
126
|
+
} | undefined;
|
|
127
|
+
type: string;
|
|
128
|
+
tooltip: {
|
|
129
|
+
show: boolean;
|
|
130
|
+
};
|
|
131
|
+
formatter: (name: string) => string;
|
|
132
|
+
textStyle: {
|
|
133
|
+
color: string;
|
|
134
|
+
fontSize: number;
|
|
135
|
+
fontFamily: string;
|
|
136
|
+
};
|
|
137
|
+
top: number;
|
|
138
|
+
};
|
|
97
139
|
/**
|
|
98
140
|
* Axe de valeurs, avec son libelle d'unite au-dessus quand il est fourni.
|
|
99
141
|
*
|
|
@@ -136,13 +178,46 @@ export interface RmSeriesInput {
|
|
|
136
178
|
/** Bar chart vertical, une ou plusieurs series, empilable. */
|
|
137
179
|
export declare function rmBarVerticalOption(categories: (string | number)[], series: RmSeriesInput[], theme: RmChartTheme, palette: string[], decal?: boolean): {
|
|
138
180
|
legend: {
|
|
181
|
+
pageIconSize?: number | undefined;
|
|
182
|
+
pageIconColor?: string | undefined;
|
|
183
|
+
pageIconInactiveColor?: string | undefined;
|
|
184
|
+
pageTextStyle?: {
|
|
185
|
+
color: string;
|
|
186
|
+
fontSize: number;
|
|
187
|
+
fontFamily: string;
|
|
188
|
+
} | undefined;
|
|
189
|
+
type: string;
|
|
190
|
+
tooltip: {
|
|
191
|
+
show: boolean;
|
|
192
|
+
};
|
|
193
|
+
formatter: (name: string) => string;
|
|
194
|
+
textStyle: {
|
|
195
|
+
color: string;
|
|
196
|
+
fontSize: number;
|
|
197
|
+
fontFamily: string;
|
|
198
|
+
};
|
|
139
199
|
bottom: number;
|
|
140
200
|
left: string;
|
|
201
|
+
} | {
|
|
202
|
+
pageIconSize?: number | undefined;
|
|
203
|
+
pageIconColor?: string | undefined;
|
|
204
|
+
pageIconInactiveColor?: string | undefined;
|
|
205
|
+
pageTextStyle?: {
|
|
206
|
+
color: string;
|
|
207
|
+
fontSize: number;
|
|
208
|
+
fontFamily: string;
|
|
209
|
+
} | undefined;
|
|
210
|
+
type: string;
|
|
211
|
+
tooltip: {
|
|
212
|
+
show: boolean;
|
|
213
|
+
};
|
|
214
|
+
formatter: (name: string) => string;
|
|
141
215
|
textStyle: {
|
|
142
216
|
color: string;
|
|
143
217
|
fontSize: number;
|
|
144
218
|
fontFamily: string;
|
|
145
219
|
};
|
|
220
|
+
top: number;
|
|
146
221
|
} | undefined;
|
|
147
222
|
grid: {
|
|
148
223
|
bottom: number;
|
|
@@ -369,6 +444,19 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
369
444
|
borderWidth: number;
|
|
370
445
|
};
|
|
371
446
|
legend: {
|
|
447
|
+
pageIconSize?: number | undefined;
|
|
448
|
+
pageIconColor?: string | undefined;
|
|
449
|
+
pageIconInactiveColor?: string | undefined;
|
|
450
|
+
pageTextStyle?: {
|
|
451
|
+
color: string;
|
|
452
|
+
fontSize: number;
|
|
453
|
+
fontFamily: string;
|
|
454
|
+
} | undefined;
|
|
455
|
+
type: string;
|
|
456
|
+
tooltip: {
|
|
457
|
+
show: boolean;
|
|
458
|
+
};
|
|
459
|
+
formatter: (name: string) => string;
|
|
372
460
|
textStyle: {
|
|
373
461
|
color: string;
|
|
374
462
|
fontSize: number;
|
|
@@ -377,6 +465,19 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
377
465
|
bottom: number;
|
|
378
466
|
left: string;
|
|
379
467
|
} | {
|
|
468
|
+
pageIconSize?: number | undefined;
|
|
469
|
+
pageIconColor?: string | undefined;
|
|
470
|
+
pageIconInactiveColor?: string | undefined;
|
|
471
|
+
pageTextStyle?: {
|
|
472
|
+
color: string;
|
|
473
|
+
fontSize: number;
|
|
474
|
+
fontFamily: string;
|
|
475
|
+
} | undefined;
|
|
476
|
+
type: string;
|
|
477
|
+
tooltip: {
|
|
478
|
+
show: boolean;
|
|
479
|
+
};
|
|
480
|
+
formatter: (name: string) => string;
|
|
380
481
|
textStyle: {
|
|
381
482
|
color: string;
|
|
382
483
|
fontSize: number;
|
|
@@ -601,12 +702,46 @@ export declare function rmDonutOption(data: {
|
|
|
601
702
|
borderWidth: number;
|
|
602
703
|
};
|
|
603
704
|
legend: {
|
|
705
|
+
pageIconSize?: number | undefined;
|
|
706
|
+
pageIconColor?: string | undefined;
|
|
707
|
+
pageIconInactiveColor?: string | undefined;
|
|
708
|
+
pageTextStyle?: {
|
|
709
|
+
color: string;
|
|
710
|
+
fontSize: number;
|
|
711
|
+
fontFamily: string;
|
|
712
|
+
} | undefined;
|
|
713
|
+
type: string;
|
|
714
|
+
tooltip: {
|
|
715
|
+
show: boolean;
|
|
716
|
+
};
|
|
717
|
+
formatter: (name: string) => string;
|
|
718
|
+
textStyle: {
|
|
719
|
+
color: string;
|
|
720
|
+
fontSize: number;
|
|
721
|
+
fontFamily: string;
|
|
722
|
+
};
|
|
604
723
|
bottom: number;
|
|
724
|
+
left: string;
|
|
725
|
+
} | {
|
|
726
|
+
pageIconSize?: number | undefined;
|
|
727
|
+
pageIconColor?: string | undefined;
|
|
728
|
+
pageIconInactiveColor?: string | undefined;
|
|
729
|
+
pageTextStyle?: {
|
|
730
|
+
color: string;
|
|
731
|
+
fontSize: number;
|
|
732
|
+
fontFamily: string;
|
|
733
|
+
} | undefined;
|
|
734
|
+
type: string;
|
|
735
|
+
tooltip: {
|
|
736
|
+
show: boolean;
|
|
737
|
+
};
|
|
738
|
+
formatter: (name: string) => string;
|
|
605
739
|
textStyle: {
|
|
606
740
|
color: string;
|
|
607
741
|
fontSize: number;
|
|
608
742
|
fontFamily: string;
|
|
609
743
|
};
|
|
744
|
+
top: number;
|
|
610
745
|
};
|
|
611
746
|
series: {
|
|
612
747
|
type: string;
|
|
@@ -90,6 +90,27 @@ export function rmBaseOption(theme, decal = false) {
|
|
|
90
90
|
}
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
+
const LEGEND_MAX_INLINE = 6;
|
|
94
|
+
const LEGEND_NAME_MAX = 38;
|
|
95
|
+
const LEGEND_NAME_TAIL = 12;
|
|
96
|
+
const rmLegendName = (name) => name.length > LEGEND_NAME_MAX ? `${name.slice(0, LEGEND_NAME_MAX - LEGEND_NAME_TAIL - 1)}\u2026${name.slice(-LEGEND_NAME_TAIL)}` : name;
|
|
97
|
+
export function rmLegend(theme, count, position = "bottom") {
|
|
98
|
+
const scroll = count > LEGEND_MAX_INLINE;
|
|
99
|
+
return {
|
|
100
|
+
...position === "bottom" ? { bottom: 0, left: "center" } : { top: 0 },
|
|
101
|
+
type: scroll ? "scroll" : "plain",
|
|
102
|
+
// Le nom complet, non ecrete, au survol de l'entree.
|
|
103
|
+
tooltip: { show: true },
|
|
104
|
+
formatter: rmLegendName,
|
|
105
|
+
textStyle: { color: theme.mutedColor, fontSize: 11, fontFamily: FONT },
|
|
106
|
+
...scroll ? {
|
|
107
|
+
pageIconSize: 10,
|
|
108
|
+
pageIconColor: theme.mutedColor,
|
|
109
|
+
pageIconInactiveColor: theme.axisLine,
|
|
110
|
+
pageTextStyle: { color: theme.mutedColor, fontSize: 11, fontFamily: FONT }
|
|
111
|
+
} : {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
93
114
|
export function rmSeriesColor(palette, index) {
|
|
94
115
|
const base = palette[index % palette.length];
|
|
95
116
|
const cycle = Math.floor(index / palette.length);
|
|
@@ -120,7 +141,7 @@ const axisCommon = (theme) => ({
|
|
|
120
141
|
export function rmBarVerticalOption(categories, series, theme, palette, decal = false) {
|
|
121
142
|
return {
|
|
122
143
|
...rmBaseOption(theme, decal),
|
|
123
|
-
legend: series.length > 1 ?
|
|
144
|
+
legend: series.length > 1 ? rmLegend(theme, series.length) : void 0,
|
|
124
145
|
// La legende passee sous le graphe prend sa place dans la gouttiere basse,
|
|
125
146
|
// sinon elle recouvre les libelles de l'axe des abscisses.
|
|
126
147
|
grid: { ...rmBaseOption(theme, decal).grid, bottom: series.length > 1 ? 56 : 36 },
|
|
@@ -190,10 +211,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
190
211
|
} : base.tooltip,
|
|
191
212
|
// Legende obligatoire en double axe : sans elle, rien ne dit quelle courbe
|
|
192
213
|
// se lit sur quel axe.
|
|
193
|
-
legend: series.length > 1 || dual ?
|
|
194
|
-
...bottomLegend ? { bottom: 0, left: "center" } : { top: 0 },
|
|
195
|
-
textStyle: { color: theme.mutedColor, fontSize: 11, fontFamily: FONT }
|
|
196
|
-
} : void 0,
|
|
214
|
+
legend: series.length > 1 || dual ? rmLegend(theme, series.length, legendPosition) : void 0,
|
|
197
215
|
grid: {
|
|
198
216
|
// Sans graduation a gauche, la gouttiere de 48 px ne sert plus qu'a
|
|
199
217
|
// laisser depasser la premiere pastille de valeur.
|
|
@@ -275,7 +293,7 @@ export function rmDonutOption(data, theme, palette, decal = false) {
|
|
|
275
293
|
...rmBaseOption(theme, decal),
|
|
276
294
|
grid: void 0,
|
|
277
295
|
tooltip: { ...rmBaseOption(theme, decal).tooltip, trigger: "item" },
|
|
278
|
-
legend:
|
|
296
|
+
legend: rmLegend(theme, data.length),
|
|
279
297
|
series: [{
|
|
280
298
|
type: "pie",
|
|
281
299
|
radius: ["45%", "70%"],
|