@dnax/ui 0.0.66 → 0.0.69
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/components/QChart.vue +329 -19
- package/components/QSpreadsheet.vue +194 -37
- package/index.ts +11 -1
- package/lib/chart.axis.test.ts +1 -1
- package/lib/chart.bi.test.ts +839 -0
- package/lib/chart.modules.test.ts +232 -0
- package/lib/chart.test.ts +13 -4
- package/lib/chart.ts +1400 -80
- package/lib/chartFormat.ts +222 -0
- package/lib/chartStats.ts +178 -0
- package/lib/formula.test.ts +8 -0
- package/lib/formula.ts +4 -2
- package/lib/history.test.ts +78 -0
- package/lib/history.ts +72 -0
- package/lib/spreadsheet.test.ts +110 -0
- package/lib/spreadsheet.ts +204 -5
- package/package.json +1 -1
- package/styles/main.css +65 -0
package/components/QChart.vue
CHANGED
|
@@ -13,16 +13,31 @@
|
|
|
13
13
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue"
|
|
14
14
|
import { chartToECharts, COLOR_TOKENS } from "../lib/chart"
|
|
15
15
|
import { normalizeCssColor } from "../lib/color"
|
|
16
|
-
import { legendPickOf, nextLegendPick, pickFromEvent, sameSelection, tableModel } from "../lib/chart"
|
|
17
|
-
import
|
|
16
|
+
import { legendPickOf, nextLegendPick, pickFromEvent, sameSelection, tableCsv, tableModel } from "../lib/chart"
|
|
17
|
+
import { useConfigLang } from "../lib/useComponentProps"
|
|
18
|
+
import type {
|
|
19
|
+
QChartAxis,
|
|
20
|
+
QChartColumn,
|
|
21
|
+
QChartLegend,
|
|
22
|
+
QChartMark,
|
|
23
|
+
QChartPick,
|
|
24
|
+
QChartTooltip,
|
|
25
|
+
} from "../lib/chart"
|
|
18
26
|
|
|
19
27
|
interface Props {
|
|
20
28
|
/** Marques à rendre : `{ type: 'line', data, x, y }`, `{ type: 'bar', … }`… */
|
|
21
29
|
marks?: QChartMark[]
|
|
22
|
-
/** Axe X (type, libellé, bornes, grille) */
|
|
30
|
+
/** Axe X (type, libellé, bornes, grille, format, granularité temporelle) */
|
|
23
31
|
x?: QChartAxis
|
|
24
|
-
/** Axe Y (type, libellé, bornes, grille) */
|
|
32
|
+
/** Axe Y (type, libellé, bornes, grille, format) */
|
|
25
33
|
y?: QChartAxis
|
|
34
|
+
/** Axe Y **secondaire**, à droite — pour un combo volume (barres) + taux (ligne) :
|
|
35
|
+
* la marque visée déclare `yAxis: 'right'`. */
|
|
36
|
+
yRight?: QChartAxis
|
|
37
|
+
/** Locale des nombres / dates (`fr-FR` par défaut, sinon celle du provider) */
|
|
38
|
+
locale?: string
|
|
39
|
+
/** Mode **compact** (mini-widget) : marges réduites, axes et légende masqués par défaut */
|
|
40
|
+
compact?: boolean
|
|
26
41
|
/** Hauteur du conteneur (nombre = px, sinon valeur CSS) */
|
|
27
42
|
height?: number | string
|
|
28
43
|
/** Titre du graphique */
|
|
@@ -32,8 +47,9 @@ interface Props {
|
|
|
32
47
|
/** Légende : `false` pour la masquer, `true` pour la forcer (défaut : dès qu'une série est
|
|
33
48
|
* nommée), ou `{ position: 'top' | 'bottom' | 'left' | 'right', offset, align }` */
|
|
34
49
|
legend?: boolean | QChartLegend
|
|
35
|
-
/** Info-bulle au survol (défaut : true)
|
|
36
|
-
|
|
50
|
+
/** Info-bulle au survol (défaut : true) — `false` l'éteint, un objet la personnalise
|
|
51
|
+
* (`formatter`, `valueFormatter`, `sortDesc`) */
|
|
52
|
+
tooltip?: boolean | QChartTooltip
|
|
37
53
|
/** Réaction des marques liées à la sélection : `filter` (défaut) **retire** les données non
|
|
38
54
|
* liées, `dim` les **estompe** (`dimOpacity`) en gardant tout le contexte visible. */
|
|
39
55
|
linkMode?: "filter" | "dim"
|
|
@@ -47,6 +63,18 @@ interface Props {
|
|
|
47
63
|
* les éléments qui portent ce `name` sont mis en évidence (`highlight`) dans toutes les
|
|
48
64
|
* séries. `null` efface la sélection. */
|
|
49
65
|
selected?: QChartPick | null
|
|
66
|
+
/** État **chargement** : superpose un message (slot `loading`) */
|
|
67
|
+
loading?: boolean
|
|
68
|
+
/** État **erreur** : `true` (message localisé) ou un texte à afficher (slot `error`) */
|
|
69
|
+
error?: string | boolean
|
|
70
|
+
/** État **vide** forcé (sinon détecté : aucune donnée) — slot `empty` */
|
|
71
|
+
empty?: boolean
|
|
72
|
+
/** Textes des états et de la table (prioritaires sur l'i18n du provider) */
|
|
73
|
+
texts?: Partial<
|
|
74
|
+
Record<"loading" | "empty" | "error" | "exportCsv" | "total" | "page" | "sortAscending" | "sortDescending", string>
|
|
75
|
+
>
|
|
76
|
+
/** Marque `table` : bouton d'**export CSV** */
|
|
77
|
+
exportable?: boolean
|
|
50
78
|
/** Options ECharts brutes, fusionnées en dernier (échappatoire) */
|
|
51
79
|
options?: Record<string, any>
|
|
52
80
|
}
|
|
@@ -76,6 +104,37 @@ const emit = defineEmits<{
|
|
|
76
104
|
unpick: [pick: QChartPick]
|
|
77
105
|
}>()
|
|
78
106
|
|
|
107
|
+
// ─── i18n des états et de la table ────────────────────────────────────────────
|
|
108
|
+
const I18N = {
|
|
109
|
+
en: {
|
|
110
|
+
loading: "Loading…",
|
|
111
|
+
empty: "No data",
|
|
112
|
+
error: "Unable to display the chart",
|
|
113
|
+
exportCsv: "Export CSV",
|
|
114
|
+
total: "Total",
|
|
115
|
+
page: "Page {n} / {m}",
|
|
116
|
+
sortAscending: "Sort ascending",
|
|
117
|
+
sortDescending: "Sort descending",
|
|
118
|
+
},
|
|
119
|
+
fr: {
|
|
120
|
+
loading: "Chargement…",
|
|
121
|
+
empty: "Aucune donnée",
|
|
122
|
+
error: "Impossible d'afficher le graphique",
|
|
123
|
+
exportCsv: "Exporter en CSV",
|
|
124
|
+
total: "Total",
|
|
125
|
+
page: "Page {n} / {m}",
|
|
126
|
+
sortAscending: "Tri croissant",
|
|
127
|
+
sortDescending: "Tri décroissant",
|
|
128
|
+
},
|
|
129
|
+
} as const
|
|
130
|
+
type UiText = keyof (typeof I18N)["en"]
|
|
131
|
+
const configLang = useConfigLang()
|
|
132
|
+
const t = (key: UiText): string => props.texts?.[key] ?? I18N[configLang.value === "fr" ? "fr" : "en"][key]
|
|
133
|
+
const fmtPage = (n: number, total: number) =>
|
|
134
|
+
t("page").replace("{n}", String(n)).replace("{m}", String(total))
|
|
135
|
+
/** Locale des nombres / dates : prop, sinon celle du provider (`fr` → `fr-FR`) */
|
|
136
|
+
const locale = computed(() => props.locale ?? (configLang.value === "fr" ? "fr-FR" : "en-US"))
|
|
137
|
+
|
|
79
138
|
const el = ref<HTMLElement | null>(null)
|
|
80
139
|
const chart = shallowRef<any>(null)
|
|
81
140
|
let modulePromise: Promise<any> | null = null
|
|
@@ -85,11 +144,12 @@ let themeObserver: MutationObserver | undefined
|
|
|
85
144
|
/** Charge ECharts (modules enregistrés une seule fois, arbre secoué). */
|
|
86
145
|
const loadECharts = () => {
|
|
87
146
|
modulePromise ??= (async () => {
|
|
88
|
-
const [core, charts, components, renderers] = await Promise.all([
|
|
147
|
+
const [core, charts, components, renderers, features] = await Promise.all([
|
|
89
148
|
import("echarts/core"),
|
|
90
149
|
import("echarts/charts"),
|
|
91
150
|
import("echarts/components"),
|
|
92
151
|
import("echarts/renderers"),
|
|
152
|
+
import("echarts/features"),
|
|
93
153
|
])
|
|
94
154
|
core.use([
|
|
95
155
|
charts.BarChart,
|
|
@@ -97,13 +157,25 @@ const loadECharts = () => {
|
|
|
97
157
|
charts.ScatterChart,
|
|
98
158
|
charts.PieChart, // marque `pie` (camembert / anneau)
|
|
99
159
|
charts.HeatmapChart, // marque `heatmap` (carte de chaleur)
|
|
160
|
+
charts.BoxplotChart, // marque `boxplot` (boîte à moustaches)
|
|
161
|
+
charts.GaugeChart, // marque `gauge` (jauge)
|
|
162
|
+
charts.FunnelChart, // marque `funnel` (entonnoir)
|
|
163
|
+
charts.RadarChart, // marque `radar` (multicritères)
|
|
164
|
+
charts.TreemapChart, // marque `treemap`
|
|
165
|
+
charts.SunburstChart, // marque `sunburst`
|
|
166
|
+
charts.SankeyChart, // marque `sankey` (flux)
|
|
100
167
|
components.GridComponent,
|
|
168
|
+
components.RadarComponent, // coordonnée polaire de la marque `radar`
|
|
101
169
|
components.TooltipComponent,
|
|
102
170
|
components.LegendComponent,
|
|
103
171
|
components.TitleComponent, // titre (`title` prop)
|
|
104
172
|
components.MarkLineComponent,
|
|
105
|
-
components.
|
|
173
|
+
components.MarkAreaComponent, // marque `band` (zones de seuil)
|
|
174
|
+
components.VisualMapComponent, // échelles de couleurs (heatmap, `fill` numérique d'un `dot`)
|
|
106
175
|
renderers.CanvasRenderer,
|
|
176
|
+
// ECharts 6 : `grid.containLabel` vit désormais dans un module de compatibilité —
|
|
177
|
+
// sans lui l'option est ignorée **silencieusement** (les libellés débordent du cadre).
|
|
178
|
+
features.LegacyGridContainLabel,
|
|
107
179
|
])
|
|
108
180
|
return core
|
|
109
181
|
})()
|
|
@@ -170,6 +242,9 @@ const build = () => {
|
|
|
170
242
|
marks: props.marks,
|
|
171
243
|
x: props.x,
|
|
172
244
|
y: props.y,
|
|
245
|
+
yRight: props.yRight,
|
|
246
|
+
locale: locale.value,
|
|
247
|
+
compact: props.compact,
|
|
173
248
|
colors: props.colors,
|
|
174
249
|
title: props.title,
|
|
175
250
|
legend: props.legend,
|
|
@@ -197,12 +272,88 @@ const linkChart = (group: string, instance: any, api: any) => {
|
|
|
197
272
|
}
|
|
198
273
|
|
|
199
274
|
const tableMark = computed(() => (props.marks ?? []).find((mark) => mark.type === "table"))
|
|
275
|
+
|
|
276
|
+
/** État de vue **interactif** de la table (tri, page) — prioritaire sur les options de la marque */
|
|
277
|
+
const sortState = ref<{ field: string; desc: boolean } | null>(null)
|
|
278
|
+
const pageState = ref(0)
|
|
279
|
+
|
|
200
280
|
const table = computed(() =>
|
|
201
281
|
tableMark.value
|
|
202
|
-
? tableModel(tableMark.value, props.selected ?? null, (props.linkMode as "filter" | "dim") ?? "filter"
|
|
282
|
+
? tableModel(tableMark.value, props.selected ?? null, (props.linkMode as "filter" | "dim") ?? "filter", {
|
|
283
|
+
...(sortState.value ? { sortBy: sortState.value.field, sortDesc: sortState.value.desc } : {}),
|
|
284
|
+
...(pageState.value ? { page: pageState.value } : {}),
|
|
285
|
+
})
|
|
203
286
|
: null,
|
|
204
287
|
)
|
|
205
288
|
|
|
289
|
+
/** Style d'une colonne (largeur fixe, alignement) */
|
|
290
|
+
const columnStyle = (column: QChartColumn) => ({
|
|
291
|
+
textAlign: column.align,
|
|
292
|
+
...(column.width !== undefined
|
|
293
|
+
? { width: typeof column.width === "number" ? `${column.width}px` : column.width }
|
|
294
|
+
: {}),
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
/** Tri suivant : `asc` → `desc` → aucun (l'ordre d'origine revient) */
|
|
298
|
+
const toggleSort = (column: QChartColumn) => {
|
|
299
|
+
if (column.sortable === false) return
|
|
300
|
+
const base =
|
|
301
|
+
sortState.value ??
|
|
302
|
+
(tableMark.value?.sortBy
|
|
303
|
+
? { field: tableMark.value.sortBy, desc: !!tableMark.value.sortDesc }
|
|
304
|
+
: null)
|
|
305
|
+
if (!base || base.field !== column.field) sortState.value = { field: column.field, desc: false }
|
|
306
|
+
else if (!base.desc) sortState.value = { field: column.field, desc: true }
|
|
307
|
+
else sortState.value = null
|
|
308
|
+
pageState.value = 0
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const sortIndicator = (column: QChartColumn) => {
|
|
312
|
+
const current = sortState.value ?? (tableMark.value?.sortBy ? { field: tableMark.value.sortBy, desc: !!tableMark.value.sortDesc } : null)
|
|
313
|
+
if (!current || current.field !== column.field) return ""
|
|
314
|
+
return current.desc ? "\u2193" : "\u2191"
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const sortTitle = (column: QChartColumn) => {
|
|
318
|
+
if (column.sortable === false) return ""
|
|
319
|
+
return sortIndicator(column) === "\u2191" ? t("sortDescending") : t("sortAscending")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const goPage = (delta: number) => {
|
|
323
|
+
const model = table.value
|
|
324
|
+
if (!model?.pages) return
|
|
325
|
+
pageState.value = Math.max(0, Math.min(model.pages - 1, (model.page ?? 0) + delta))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Export CSV de **toutes** les lignes visibles (pas seulement la page) */
|
|
329
|
+
const exportCsv = () => {
|
|
330
|
+
const model = table.value
|
|
331
|
+
if (!model || typeof window === "undefined" || typeof document === "undefined") return
|
|
332
|
+
const csv = tableCsv(model, { label: t("total") })
|
|
333
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" })
|
|
334
|
+
const url = URL.createObjectURL(blob)
|
|
335
|
+
const slug =
|
|
336
|
+
(props.title ?? "chart")
|
|
337
|
+
.toLowerCase()
|
|
338
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
339
|
+
.replace(/^-|-$/g, "") || "chart"
|
|
340
|
+
const anchor = document.createElement("a")
|
|
341
|
+
anchor.href = url
|
|
342
|
+
anchor.download = `${slug}.csv`
|
|
343
|
+
anchor.click()
|
|
344
|
+
URL.revokeObjectURL(url)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Les données changent → le tri et la page repartent de zéro (sinon une page vide peut rester)
|
|
348
|
+
watch(
|
|
349
|
+
() => props.marks,
|
|
350
|
+
() => {
|
|
351
|
+
sortState.value = null
|
|
352
|
+
pageState.value = 0
|
|
353
|
+
},
|
|
354
|
+
{ deep: true },
|
|
355
|
+
)
|
|
356
|
+
|
|
206
357
|
/** Champ qui porte la clé partagée d'une ligne : le `localField` de `link`, sinon le premier
|
|
207
358
|
* champ de la première colonne. C'est ce qui alimente `@pick`, la sélection et la mise en
|
|
208
359
|
* évidence — une ligne se désigne par la même clé que les autres marques. */
|
|
@@ -247,16 +398,37 @@ const emitPick = (pick: QChartPick) => {
|
|
|
247
398
|
const pickRow = (row: any, dataIndex: number) => {
|
|
248
399
|
const markIndex = (props.marks ?? []).findIndex((mark) => mark.type === "table")
|
|
249
400
|
const name = rowKey(row)
|
|
401
|
+
// Index **absolu** (toutes les lignes visibles), pas celui de la page courante
|
|
402
|
+
const all = table.value?.allRows
|
|
403
|
+
const absolute = all ? all.indexOf(row) : -1
|
|
250
404
|
emitPick({
|
|
251
405
|
...(name !== undefined ? { name } : {}),
|
|
252
406
|
markIndex,
|
|
253
407
|
markType: "table",
|
|
254
|
-
dataIndex,
|
|
408
|
+
dataIndex: absolute >= 0 ? absolute : dataIndex,
|
|
255
409
|
origin: "mark",
|
|
256
410
|
data: row,
|
|
257
411
|
})
|
|
258
412
|
}
|
|
259
413
|
|
|
414
|
+
/** Une donnée est-elle présente ? (marque `table` : lignes visibles ; sinon : données déclarées) */
|
|
415
|
+
const hasData = computed(() => {
|
|
416
|
+
const marks = props.marks ?? []
|
|
417
|
+
return marks.some((mark) => (mark.data?.length ?? 0) > 0)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
/** État affiché en surimpression (`loading` > `error` > `empty`) */
|
|
421
|
+
const stateKind = computed<"loading" | "error" | "empty" | null>(() => {
|
|
422
|
+
if (props.loading) return "loading"
|
|
423
|
+
if (props.error) return "error"
|
|
424
|
+
if (props.empty ?? !hasData.value) return "empty"
|
|
425
|
+
return null
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
const errorMessage = computed(() =>
|
|
429
|
+
typeof props.error === "string" ? props.error : t("error"),
|
|
430
|
+
)
|
|
431
|
+
|
|
260
432
|
const render = async () => {
|
|
261
433
|
// Une marque `table` remplace le tracé : on libère l'instance si elle existait
|
|
262
434
|
if (tableMark.value) {
|
|
@@ -348,6 +520,9 @@ watch(
|
|
|
348
520
|
props.selected,
|
|
349
521
|
props.x,
|
|
350
522
|
props.y,
|
|
523
|
+
props.yRight,
|
|
524
|
+
props.locale,
|
|
525
|
+
props.compact,
|
|
351
526
|
props.title,
|
|
352
527
|
props.colors,
|
|
353
528
|
props.legend,
|
|
@@ -414,7 +589,7 @@ defineExpose({
|
|
|
414
589
|
</script>
|
|
415
590
|
|
|
416
591
|
<template>
|
|
417
|
-
<!-- Marque `table` : les données en HTML (sélection, jointure et `@pick` comme le reste) -->
|
|
592
|
+
<!-- Marque `table` : les données en HTML (sélection, jointure, tri, pagination et `@pick` comme le reste) -->
|
|
418
593
|
<div
|
|
419
594
|
v-if="table"
|
|
420
595
|
class="q-chart q-chart--table"
|
|
@@ -422,11 +597,25 @@ defineExpose({
|
|
|
422
597
|
role="table"
|
|
423
598
|
:aria-label="title || 'Table'"
|
|
424
599
|
>
|
|
600
|
+
<div v-if="exportable" class="q-chart__table-bar">
|
|
601
|
+
<button type="button" class="q-chart__table-action" @click="exportCsv">
|
|
602
|
+
{{ t("exportCsv") }}
|
|
603
|
+
</button>
|
|
604
|
+
</div>
|
|
425
605
|
<table :class="`is-${table.separator}`">
|
|
426
606
|
<thead>
|
|
427
607
|
<tr>
|
|
428
|
-
<th
|
|
608
|
+
<th
|
|
609
|
+
v-for="column in table.columns"
|
|
610
|
+
:key="column.field"
|
|
611
|
+
:style="columnStyle(column)"
|
|
612
|
+
:title="sortTitle(column)"
|
|
613
|
+
:class="{ 'is-sortable': column.sortable !== false }"
|
|
614
|
+
:aria-sort="sortIndicator(column) === '↑' ? 'ascending' : sortIndicator(column) === '↓' ? 'descending' : 'none'"
|
|
615
|
+
@click="toggleSort(column)"
|
|
616
|
+
>
|
|
429
617
|
{{ column.label }}
|
|
618
|
+
<span v-if="sortIndicator(column)" class="q-chart__sort">{{ sortIndicator(column) }}</span>
|
|
430
619
|
</th>
|
|
431
620
|
</tr>
|
|
432
621
|
</thead>
|
|
@@ -438,23 +627,59 @@ defineExpose({
|
|
|
438
627
|
:style="isDimmed(row) ? { opacity: dimOpacity } : undefined"
|
|
439
628
|
@click="pickRow(row, index)"
|
|
440
629
|
>
|
|
441
|
-
<td v-for="column in table.columns" :key="column.field" :style="
|
|
630
|
+
<td v-for="column in table.columns" :key="column.field" :style="columnStyle(column)">
|
|
442
631
|
{{ cellValue(column, row) }}
|
|
443
632
|
</td>
|
|
444
633
|
</tr>
|
|
445
634
|
</tbody>
|
|
635
|
+
<tfoot v-if="table.totals">
|
|
636
|
+
<tr>
|
|
637
|
+
<td v-for="column in table.columns" :key="column.field" :style="columnStyle(column)">
|
|
638
|
+
{{ table.totals[column.field] ?? "" }}
|
|
639
|
+
</td>
|
|
640
|
+
</tr>
|
|
641
|
+
</tfoot>
|
|
446
642
|
</table>
|
|
447
|
-
<p v-if="table.rows.length === 0" class="q-chart__empty">
|
|
643
|
+
<p v-if="table.rows.length === 0" class="q-chart__empty">{{ t("empty") }}</p>
|
|
644
|
+
<div v-if="table.pages && table.pages > 1" class="q-chart__pager">
|
|
645
|
+
<button
|
|
646
|
+
type="button"
|
|
647
|
+
class="q-chart__table-action"
|
|
648
|
+
:disabled="(table.page ?? 0) <= 0"
|
|
649
|
+
@click="goPage(-1)"
|
|
650
|
+
>
|
|
651
|
+
‹
|
|
652
|
+
</button>
|
|
653
|
+
<span class="q-chart__pager-label">{{ fmtPage((table.page ?? 0) + 1, table.pages) }}</span>
|
|
654
|
+
<button
|
|
655
|
+
type="button"
|
|
656
|
+
class="q-chart__table-action"
|
|
657
|
+
:disabled="(table.page ?? 0) >= table.pages - 1"
|
|
658
|
+
@click="goPage(1)"
|
|
659
|
+
>
|
|
660
|
+
›
|
|
661
|
+
</button>
|
|
662
|
+
</div>
|
|
448
663
|
</div>
|
|
449
664
|
|
|
450
665
|
<div
|
|
451
666
|
v-else
|
|
452
|
-
|
|
453
|
-
class="q-chart"
|
|
667
|
+
class="q-chart q-chart--plot"
|
|
454
668
|
:style="{ height: typeof height === 'number' ? `${height}px` : height }"
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
669
|
+
>
|
|
670
|
+
<div
|
|
671
|
+
ref="el"
|
|
672
|
+
class="q-chart__canvas"
|
|
673
|
+
role="img"
|
|
674
|
+
:aria-label="title || 'Chart'"
|
|
675
|
+
:aria-busy="stateKind === 'loading' || undefined"
|
|
676
|
+
></div>
|
|
677
|
+
<div v-if="stateKind" class="q-chart__state" :class="`q-chart__state--${stateKind}`" role="status">
|
|
678
|
+
<slot v-if="stateKind === 'loading'" name="loading">{{ t("loading") }}</slot>
|
|
679
|
+
<slot v-else-if="stateKind === 'error'" name="error">{{ errorMessage }}</slot>
|
|
680
|
+
<slot v-else name="empty">{{ t("empty") }}</slot>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
458
683
|
</template>
|
|
459
684
|
|
|
460
685
|
<style scoped>
|
|
@@ -531,4 +756,89 @@ defineExpose({
|
|
|
531
756
|
.q-chart {
|
|
532
757
|
width: 100%;
|
|
533
758
|
}
|
|
759
|
+
|
|
760
|
+
/* ─── Tracé (canvas + surimpression d'état) ─── */
|
|
761
|
+
.q-chart--plot {
|
|
762
|
+
position: relative;
|
|
763
|
+
width: 100%;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.q-chart__canvas {
|
|
767
|
+
width: 100%;
|
|
768
|
+
height: 100%;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/* États `loading` / `empty` / `error` : une surimpression lisible sur le thème hôte */
|
|
772
|
+
.q-chart__state {
|
|
773
|
+
position: absolute;
|
|
774
|
+
inset: 0;
|
|
775
|
+
display: flex;
|
|
776
|
+
align-items: center;
|
|
777
|
+
justify-content: center;
|
|
778
|
+
padding: 12px;
|
|
779
|
+
font-size: 13px;
|
|
780
|
+
text-align: center;
|
|
781
|
+
color: var(--muted-foreground, #71717a);
|
|
782
|
+
background: color-mix(in srgb, var(--card, #ffffff) 76%, transparent);
|
|
783
|
+
backdrop-filter: blur(2px);
|
|
784
|
+
-webkit-backdrop-filter: blur(2px);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.q-chart__state--error {
|
|
788
|
+
color: var(--negative, #c10015);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/* ─── Table BI (barre d'outils, tri, totaux, pagination) ─── */
|
|
792
|
+
.q-chart__table-bar {
|
|
793
|
+
display: flex;
|
|
794
|
+
justify-content: flex-end;
|
|
795
|
+
padding: 4px 10px;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.q-chart__table-action {
|
|
799
|
+
padding: 2px 8px;
|
|
800
|
+
font: inherit;
|
|
801
|
+
font-size: 12px;
|
|
802
|
+
color: var(--muted-foreground, #71717a);
|
|
803
|
+
cursor: pointer;
|
|
804
|
+
background: transparent;
|
|
805
|
+
border: 1px solid var(--border, #e4e4e7);
|
|
806
|
+
border-radius: 4px;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.q-chart__table-action:hover:not(:disabled) {
|
|
810
|
+
color: var(--foreground, #18181b);
|
|
811
|
+
background: var(--accent, #f4f4f5);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.q-chart__table-action:disabled {
|
|
815
|
+
cursor: default;
|
|
816
|
+
opacity: 0.45;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.q-chart--table th.is-sortable {
|
|
820
|
+
cursor: pointer;
|
|
821
|
+
user-select: none;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.q-chart__sort {
|
|
825
|
+
margin-left: 4px;
|
|
826
|
+
font-size: 10px;
|
|
827
|
+
opacity: 0.75;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.q-chart--table tfoot td {
|
|
831
|
+
font-weight: 600;
|
|
832
|
+
border-top: 1px solid var(--border, #e4e4e7);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.q-chart__pager {
|
|
836
|
+
display: flex;
|
|
837
|
+
align-items: center;
|
|
838
|
+
justify-content: flex-end;
|
|
839
|
+
gap: 8px;
|
|
840
|
+
padding: 6px 10px;
|
|
841
|
+
font-size: 12px;
|
|
842
|
+
color: var(--muted-foreground, #71717a);
|
|
843
|
+
}
|
|
534
844
|
</style>
|