@tekkare/romulus 0.1.45 → 0.1.47
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/FilterComplex.vue +62 -13
- package/dist/runtime/components/LineChart.vue +4 -2
- package/dist/runtime/components/TableCard.vue +1 -1
- package/dist/runtime/utils/chart.d.ts +38 -3
- package/dist/runtime/utils/chart.js +41 -10
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -18,17 +18,58 @@ const props = withDefaults(defineProps<{
|
|
|
18
18
|
icon?: Component
|
|
19
19
|
label: string
|
|
20
20
|
options: FilterComplexOption[]
|
|
21
|
+
/** Langue du chrome du panneau : titres, recherches, actions. */
|
|
22
|
+
lang?: 'fr' | 'en'
|
|
23
|
+
/**
|
|
24
|
+
* La recherche est servie par l'appelant : la liste des options arrive
|
|
25
|
+
* deja filtree et le filtre local est retire. Indispensable des que le
|
|
26
|
+
* referentiel ne tient pas dans `options` (une recherche locale sur les
|
|
27
|
+
* cent premieres valeurs laisse croire que le reste n'existe pas).
|
|
28
|
+
*/
|
|
29
|
+
remote?: boolean
|
|
21
30
|
}>(), {
|
|
22
31
|
icon: undefined,
|
|
32
|
+
lang: 'en',
|
|
33
|
+
remote: false,
|
|
23
34
|
})
|
|
24
35
|
|
|
25
36
|
const selected = defineModel<string[]>({ default: () => [] })
|
|
37
|
+
/**
|
|
38
|
+
* Texte saisi dans la colonne « ajouter ». Expose pour que l'appelant puisse
|
|
39
|
+
* aller chercher les options correspondantes, cf. `remote`.
|
|
40
|
+
*/
|
|
41
|
+
const addSearch = defineModel<string>('search', { default: '' })
|
|
26
42
|
|
|
27
43
|
const isOpen = ref(false)
|
|
28
44
|
const wrapperRef = ref<HTMLElement>()
|
|
29
|
-
const addSearch = ref('')
|
|
30
45
|
const delSearch = ref('')
|
|
31
46
|
|
|
47
|
+
/** Le chrome du panneau, dans les deux langues. */
|
|
48
|
+
const WORDS = {
|
|
49
|
+
fr: {
|
|
50
|
+
custom: 'Sélection libre',
|
|
51
|
+
searchAdd: 'Rechercher pour ajouter...',
|
|
52
|
+
addAll: 'Tout ajouter',
|
|
53
|
+
current: 'Sélection en cours',
|
|
54
|
+
clear: 'Effacer',
|
|
55
|
+
searchSelection: 'Rechercher dans la sélection...',
|
|
56
|
+
noResult: 'Aucun résultat',
|
|
57
|
+
noSelection: 'Aucune sélection',
|
|
58
|
+
},
|
|
59
|
+
en: {
|
|
60
|
+
custom: 'Custom selection',
|
|
61
|
+
searchAdd: 'Search to add...',
|
|
62
|
+
addAll: 'Add all',
|
|
63
|
+
current: 'Current selection',
|
|
64
|
+
clear: 'Clear',
|
|
65
|
+
searchSelection: 'Search selection...',
|
|
66
|
+
noResult: 'No results',
|
|
67
|
+
noSelection: 'No selection',
|
|
68
|
+
},
|
|
69
|
+
} as const
|
|
70
|
+
|
|
71
|
+
const t = computed(() => WORDS[props.lang])
|
|
72
|
+
|
|
32
73
|
const isActive = computed(() => selected.value.length > 0)
|
|
33
74
|
|
|
34
75
|
const availableOptions = computed(() => {
|
|
@@ -36,14 +77,22 @@ const availableOptions = computed(() => {
|
|
|
36
77
|
})
|
|
37
78
|
|
|
38
79
|
const filteredAvailable = computed(() => {
|
|
39
|
-
if (!addSearch.value) return availableOptions.value
|
|
80
|
+
if (props.remote || !addSearch.value) return availableOptions.value
|
|
40
81
|
const q = addSearch.value.toLowerCase()
|
|
41
82
|
return availableOptions.value.filter(o => o.label.toLowerCase().includes(q))
|
|
42
83
|
})
|
|
43
84
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
/**
|
|
86
|
+
* La selection se lit sur `selected`, pas sur `options` : une valeur choisie
|
|
87
|
+
* puis sortie de la liste (recherche suivante, referentiel pagine) doit rester
|
|
88
|
+
* visible et retirable, sinon elle filtre l'analyse sans plus apparaitre
|
|
89
|
+
* nulle part.
|
|
90
|
+
*/
|
|
91
|
+
const selectedOptions = computed(() =>
|
|
92
|
+
selected.value.map(
|
|
93
|
+
value => props.options.find(o => o.value === value) ?? { label: value, value },
|
|
94
|
+
),
|
|
95
|
+
)
|
|
47
96
|
|
|
48
97
|
const filteredSelected = computed(() => {
|
|
49
98
|
if (!delSearch.value) return selectedOptions.value
|
|
@@ -110,14 +159,14 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
110
159
|
<div v-if="isOpen" class="filter-complex__panel" @click.stop>
|
|
111
160
|
<!-- Left: Add -->
|
|
112
161
|
<div class="filter-complex__left">
|
|
113
|
-
<h3 class="filter-complex__title">
|
|
162
|
+
<h3 class="filter-complex__title">{{ t.custom }}</h3>
|
|
114
163
|
|
|
115
164
|
<div class="filter-complex__search">
|
|
116
165
|
<PhMagnifyingGlass :size="14" color="var(--text-muted)" />
|
|
117
166
|
<input
|
|
118
167
|
v-model="addSearch"
|
|
119
168
|
type="text"
|
|
120
|
-
placeholder="
|
|
169
|
+
:placeholder="t.searchAdd"
|
|
121
170
|
class="filter-complex__search-input"
|
|
122
171
|
>
|
|
123
172
|
<PhX
|
|
@@ -133,7 +182,7 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
133
182
|
class="filter-complex__action filter-complex__action--add"
|
|
134
183
|
@click="addAll"
|
|
135
184
|
>
|
|
136
|
-
|
|
185
|
+
{{ t.addAll }} ({{ filteredAvailable.length }})
|
|
137
186
|
</button>
|
|
138
187
|
|
|
139
188
|
<div class="filter-complex__list">
|
|
@@ -147,7 +196,7 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
147
196
|
<PhPlusCircle :size="16" class="filter-complex__item-icon filter-complex__item-icon--add" />
|
|
148
197
|
</button>
|
|
149
198
|
<p v-if="filteredAvailable.length === 0" class="filter-complex__empty">
|
|
150
|
-
|
|
199
|
+
{{ t.noResult }}
|
|
151
200
|
</p>
|
|
152
201
|
</div>
|
|
153
202
|
</div>
|
|
@@ -155,13 +204,13 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
155
204
|
<!-- Right: Selection -->
|
|
156
205
|
<div class="filter-complex__right">
|
|
157
206
|
<div class="filter-complex__right-header">
|
|
158
|
-
<h3 class="filter-complex__title">
|
|
207
|
+
<h3 class="filter-complex__title">{{ t.current }}</h3>
|
|
159
208
|
<button
|
|
160
209
|
v-if="selected.length > 0"
|
|
161
210
|
class="filter-complex__action filter-complex__action--clear"
|
|
162
211
|
@click="clearAll"
|
|
163
212
|
>
|
|
164
|
-
|
|
213
|
+
{{ t.clear }}
|
|
165
214
|
</button>
|
|
166
215
|
</div>
|
|
167
216
|
|
|
@@ -170,7 +219,7 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
170
219
|
<input
|
|
171
220
|
v-model="delSearch"
|
|
172
221
|
type="text"
|
|
173
|
-
placeholder="
|
|
222
|
+
:placeholder="t.searchSelection"
|
|
174
223
|
class="filter-complex__search-input"
|
|
175
224
|
>
|
|
176
225
|
<PhX
|
|
@@ -192,7 +241,7 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
192
241
|
<PhMinusCircle :size="16" class="filter-complex__item-icon filter-complex__item-icon--remove" />
|
|
193
242
|
</button>
|
|
194
243
|
<p v-if="selected.length === 0" class="filter-complex__empty">
|
|
195
|
-
|
|
244
|
+
{{ t.noSelection }}
|
|
196
245
|
</p>
|
|
197
246
|
</div>
|
|
198
247
|
</div>
|
|
@@ -27,7 +27,9 @@ const props = withDefaults(defineProps<{
|
|
|
27
27
|
axisLabels?: [string, string]
|
|
28
28
|
/**
|
|
29
29
|
* Etiquette chaque point de sa valeur, en pastille de la couleur de la
|
|
30
|
-
* serie.
|
|
30
|
+
* serie. Laisse libre, elle se pose tant que le graphe reste aere (peu de
|
|
31
|
+
* courbes ET peu de points) et se retire au-dela, ou les pastilles se
|
|
32
|
+
* chevauchent. `true` ou `false` tranche a la place de la regle.
|
|
31
33
|
*/
|
|
32
34
|
valueLabels?: boolean
|
|
33
35
|
/**
|
|
@@ -55,7 +57,7 @@ const props = withDefaults(defineProps<{
|
|
|
55
57
|
step: false,
|
|
56
58
|
smooth: true,
|
|
57
59
|
axisLabels: undefined,
|
|
58
|
-
valueLabels:
|
|
60
|
+
valueLabels: undefined,
|
|
59
61
|
hideValueAxis: false,
|
|
60
62
|
legendPosition: 'bottom',
|
|
61
63
|
startAtZero: false,
|
|
@@ -18,5 +18,5 @@
|
|
|
18
18
|
</template>
|
|
19
19
|
|
|
20
20
|
<style scoped>
|
|
21
|
-
.rm-table-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-xs);box-sizing:border-box;color:var(--text-primary);display:flex;flex-direction:column;font-family:var(--font-sans);
|
|
21
|
+
.rm-table-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-xs);box-sizing:border-box;color:var(--text-primary);display:flex;flex-direction:column;font-family:var(--font-sans);overflow:hidden;padding:0;width:100%}.rm-table-card__header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between;padding:24px}.rm-table-card__header :deep(.rm-table-card__title){font-size:var(--font-18);font-weight:800;margin:0}.rm-table-card__header :deep(.rm-table-card__caption){color:var(--text-secondary);font-size:var(--font-12);margin:0;white-space:nowrap}.rm-table-card :deep(.rm-table-wrapper){background:transparent;border:none;border-radius:0}.rm-table-card__header+:deep(.rm-table-wrapper){border-top:1px solid var(--border-default)}.rm-table-card>:deep(:not(.rm-table-wrapper):not(.rm-table-card__header)){padding:16px 24px}
|
|
22
22
|
</style>
|
|
@@ -320,8 +320,8 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
320
320
|
axisLabels?: [string, string];
|
|
321
321
|
/**
|
|
322
322
|
* Etiquette la valeur de chaque point, en pastille de la couleur de sa
|
|
323
|
-
* serie.
|
|
324
|
-
*
|
|
323
|
+
* serie. Non precise : automatique tant que le graphe reste aere (voir
|
|
324
|
+
* LABEL_MAX_SERIES et LABEL_MAX_POINTS).
|
|
325
325
|
*/
|
|
326
326
|
valueLabels?: boolean;
|
|
327
327
|
/**
|
|
@@ -527,7 +527,12 @@ export declare function rmLineOption(categories: (string | number)[], series: Rm
|
|
|
527
527
|
type: string;
|
|
528
528
|
name: string | undefined;
|
|
529
529
|
yAxisIndex: 0 | 1 | undefined;
|
|
530
|
-
data: (number |
|
|
530
|
+
data: (number | {
|
|
531
|
+
value: number;
|
|
532
|
+
label: {
|
|
533
|
+
offset: number[];
|
|
534
|
+
};
|
|
535
|
+
} | null)[];
|
|
531
536
|
smooth: boolean;
|
|
532
537
|
step: string | undefined;
|
|
533
538
|
symbol: string;
|
|
@@ -669,3 +674,33 @@ export declare function rmSparklineOption(values: (number | null)[], color: stri
|
|
|
669
674
|
};
|
|
670
675
|
}[];
|
|
671
676
|
};
|
|
677
|
+
/**
|
|
678
|
+
* Met un graphe de series en tableau : une ligne par serie, une colonne par
|
|
679
|
+
* categorie.
|
|
680
|
+
*
|
|
681
|
+
* Un graphe donne la forme, pas les chiffres : on y lit une tendance, on n'y
|
|
682
|
+
* releve pas une valeur. Le tableau qui l'accompagne rend la donnee lisible au
|
|
683
|
+
* chiffre pres, copiable, et accessible au lecteur d'ecran, que le SVG du
|
|
684
|
+
* graphe ne porte pas.
|
|
685
|
+
*
|
|
686
|
+
* Les valeurs restent des NOMBRES : le tri de RmTable compare alors des
|
|
687
|
+
* nombres, et la page formate a l'affichage par un slot de colonne. Formater
|
|
688
|
+
* ici donnerait un tri alphabetique sur des chaines a espaces insecables.
|
|
689
|
+
*/
|
|
690
|
+
export declare function rmSeriesTable(categories: (string | number)[], series: RmSeriesInput[], options?: {
|
|
691
|
+
label?: string;
|
|
692
|
+
key?: string;
|
|
693
|
+
}): {
|
|
694
|
+
columns: ({
|
|
695
|
+
key: string;
|
|
696
|
+
label: string;
|
|
697
|
+
sortable: boolean;
|
|
698
|
+
align: "right";
|
|
699
|
+
} | {
|
|
700
|
+
key: string;
|
|
701
|
+
label: string;
|
|
702
|
+
sortable: boolean;
|
|
703
|
+
})[];
|
|
704
|
+
rows: Record<string, unknown>[];
|
|
705
|
+
valueKeys: string[];
|
|
706
|
+
};
|
|
@@ -166,12 +166,15 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
166
166
|
smooth = true,
|
|
167
167
|
decal = false,
|
|
168
168
|
axisLabels,
|
|
169
|
-
valueLabels
|
|
169
|
+
valueLabels,
|
|
170
170
|
hideValueAxis = false,
|
|
171
171
|
legendPosition = "bottom",
|
|
172
172
|
startAtZero = false,
|
|
173
173
|
valueFormatter
|
|
174
174
|
} = options;
|
|
175
|
+
const LABEL_MAX_SERIES = 5;
|
|
176
|
+
const LABEL_MAX_POINTS = 8;
|
|
177
|
+
const labelled = valueLabels ?? (series.length <= LABEL_MAX_SERIES && categories.length <= LABEL_MAX_POINTS);
|
|
175
178
|
const AREA_MAX_SERIES = 5;
|
|
176
179
|
const filled = area ?? series.length <= AREA_MAX_SERIES;
|
|
177
180
|
const dual = series.some((s) => s.axis === 1);
|
|
@@ -202,7 +205,7 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
202
205
|
right: dual ? 56 : 32,
|
|
203
206
|
// Les pastilles du point le plus haut depassent la courbe : sans marge
|
|
204
207
|
// supplementaire en haut, ECharts les rogne.
|
|
205
|
-
top: dual ? 40 :
|
|
208
|
+
top: dual ? 40 : labelled ? 32 : 24,
|
|
206
209
|
bottom: bottomLegend ? 56 : 36,
|
|
207
210
|
containLabel: true
|
|
208
211
|
},
|
|
@@ -219,18 +222,25 @@ export function rmLineOption(categories, series, theme, palette, options = {}) {
|
|
|
219
222
|
type: "line",
|
|
220
223
|
name: s.name,
|
|
221
224
|
yAxisIndex: dual ? s.axis ?? 0 : void 0,
|
|
222
|
-
|
|
225
|
+
/*
|
|
226
|
+
Les points des deux bords sont poses SUR le cadre : leur pastille,
|
|
227
|
+
centree, mord sur la graduation a gauche et sort de la carte a
|
|
228
|
+
droite. On la decale vers l'interieur, elle reste rattachee a son
|
|
229
|
+
point par la proximite.
|
|
230
|
+
*/
|
|
231
|
+
data: labelled ? s.data.map((value, position) => {
|
|
232
|
+
const edge = position === 0 ? 1 : position === s.data.length - 1 ? -1 : 0;
|
|
233
|
+
if (!edge || value === null || value === void 0) return value;
|
|
234
|
+
return { value, label: { offset: [edge * 16, 0] } };
|
|
235
|
+
}) : s.data,
|
|
223
236
|
smooth: step ? false : smooth,
|
|
224
237
|
step: step ? "end" : void 0,
|
|
225
238
|
symbol: "circle",
|
|
226
|
-
// La pastille de valeur marque deja le point, le symbole ferait un
|
|
227
|
-
// second marqueur au meme endroit. On l'annule par sa taille et non
|
|
228
|
-
// par `symbol: 'none'` : ECharts accroche les etiquettes aux
|
|
229
|
-
// symboles, les supprimer supprime aussi les etiquettes.
|
|
230
239
|
// Le point est la cible de survol : trop petit, l'infobulle se derobe
|
|
231
|
-
// et les series courtes n'ont plus de reperes lisibles.
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
// et les series courtes n'ont plus de reperes lisibles. Il reste
|
|
241
|
+
// visible sous la pastille de valeur, qui se pose au-dessus de lui.
|
|
242
|
+
symbolSize: 8,
|
|
243
|
+
label: labelled ? {
|
|
234
244
|
show: true,
|
|
235
245
|
position: "top",
|
|
236
246
|
distance: 6,
|
|
@@ -298,3 +308,24 @@ export function rmSparklineOption(values, color) {
|
|
|
298
308
|
}]
|
|
299
309
|
};
|
|
300
310
|
}
|
|
311
|
+
export function rmSeriesTable(categories, series, options = {}) {
|
|
312
|
+
const { label = "S\xE9rie", key = "series" } = options;
|
|
313
|
+
const valueKeys = categories.map(String);
|
|
314
|
+
const columns = [
|
|
315
|
+
{ key, label, sortable: true },
|
|
316
|
+
...valueKeys.map((category) => ({
|
|
317
|
+
key: category,
|
|
318
|
+
label: category,
|
|
319
|
+
sortable: true,
|
|
320
|
+
align: "right"
|
|
321
|
+
}))
|
|
322
|
+
];
|
|
323
|
+
const rows = series.map((entry, index) => {
|
|
324
|
+
const row = { [key]: entry.name ?? `#${index + 1}` };
|
|
325
|
+
valueKeys.forEach((category, position) => {
|
|
326
|
+
row[category] = entry.data[position] ?? null;
|
|
327
|
+
});
|
|
328
|
+
return row;
|
|
329
|
+
});
|
|
330
|
+
return { columns, rows, valueKeys };
|
|
331
|
+
}
|