@dnax/ui 0.0.67 → 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 +35 -17
- 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/history.test.ts +78 -0
- package/lib/history.ts +72 -0
- package/package.json +1 -1
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>
|
|
@@ -198,6 +198,7 @@ import { checkValidation, type ArkCheck } from "../lib/spreadsheetValidation"
|
|
|
198
198
|
import { normalizeCellOptions } from "../lib/spreadsheetOptions"
|
|
199
199
|
import { inLockRect, lockRects, prefillForRow } from "../lib/spreadsheetZones"
|
|
200
200
|
import { canAddRows, canRemoveRows, clampRows } from "../lib/spreadsheetRows"
|
|
201
|
+
import { createHistory } from "../lib/history"
|
|
201
202
|
import {
|
|
202
203
|
DEFAULT_ROW_KEY,
|
|
203
204
|
diffDocuments,
|
|
@@ -1148,12 +1149,14 @@ const clearCell = (row: number, column: string) => {
|
|
|
1148
1149
|
if (isCellLocked(row, column)) return
|
|
1149
1150
|
const old = state.value[row]?.[column]
|
|
1150
1151
|
if (isBlankValue(old) || (Array.isArray(old) && old.length === 0)) return
|
|
1152
|
+
pushHistory()
|
|
1151
1153
|
setCellValue(row, column, old, null)
|
|
1152
1154
|
}
|
|
1153
1155
|
|
|
1154
1156
|
const toggleBoolean = (row: number, column: string) => {
|
|
1155
1157
|
if (isCellLocked(row, column)) return
|
|
1156
1158
|
const old = !!state.value[row]?.[column]
|
|
1159
|
+
pushHistory()
|
|
1157
1160
|
setCellValue(row, column, old, !old)
|
|
1158
1161
|
}
|
|
1159
1162
|
|
|
@@ -1258,9 +1261,7 @@ interface Snapshot {
|
|
|
1258
1261
|
merges: MergeRange[]
|
|
1259
1262
|
rules: CondRule[]
|
|
1260
1263
|
}
|
|
1261
|
-
const
|
|
1262
|
-
const redoStack: Snapshot[] = []
|
|
1263
|
-
|
|
1264
|
+
const history = createHistory<Snapshot>(100)
|
|
1264
1265
|
const takeSnapshot = (): Snapshot => ({
|
|
1265
1266
|
rows: state.value.map((r) => ({ ...r })),
|
|
1266
1267
|
cols: cols.value.map((c) => ({ ...c })),
|
|
@@ -1273,13 +1274,9 @@ const takeSnapshot = (): Snapshot => ({
|
|
|
1273
1274
|
merges: merges.value.map((m) => ({ ...m })),
|
|
1274
1275
|
rules: condRules.value.map((r) => ({ ...r })),
|
|
1275
1276
|
})
|
|
1276
|
-
const pushHistory = () =>
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
redoStack.length = 0
|
|
1280
|
-
}
|
|
1281
|
-
const canUndo = computed(() => undoStack.length > 0 && !props.readonly && !props.disable)
|
|
1282
|
-
const canRedo = computed(() => redoStack.length > 0 && !props.readonly && !props.disable)
|
|
1277
|
+
const pushHistory = () => history.push(takeSnapshot())
|
|
1278
|
+
const canUndo = computed(() => history.canUndo.value && !props.readonly && !props.disable)
|
|
1279
|
+
const canRedo = computed(() => history.canRedo.value && !props.readonly && !props.disable)
|
|
1283
1280
|
|
|
1284
1281
|
const restoreSnapshot = (s: Snapshot) => {
|
|
1285
1282
|
cols.value = s.cols.map((c) => ({ ...c }))
|
|
@@ -1294,17 +1291,35 @@ const restoreSnapshot = (s: Snapshot) => {
|
|
|
1294
1291
|
condRules.value = s.rules.map((r) => ({ ...r }))
|
|
1295
1292
|
emit("update:columns", cols.value)
|
|
1296
1293
|
pushRows(state.value)
|
|
1294
|
+
// La sélection peut pointer sur une ligne/colonne qui n'existe plus (redo d'une
|
|
1295
|
+
// suppression) : on la ramène sur la première cellule plutôt que de laisser un
|
|
1296
|
+
// curseur fantôme hors grille.
|
|
1297
|
+
const current = sel.value
|
|
1298
|
+
if (current) {
|
|
1299
|
+
const maxRow = state.value.length - 1
|
|
1300
|
+
const bad =
|
|
1301
|
+
colIndex(current.column) === -1 ||
|
|
1302
|
+
(current.endColumn !== undefined && colIndex(current.endColumn) === -1) ||
|
|
1303
|
+
current.row > maxRow ||
|
|
1304
|
+
(current.endRow !== undefined && current.endRow > maxRow)
|
|
1305
|
+
if (bad) {
|
|
1306
|
+
if (state.value.length && cols.value.length) select(0, cols.value[0]!.name)
|
|
1307
|
+
else setSel(null)
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1297
1310
|
}
|
|
1298
1311
|
const undo = () => {
|
|
1299
1312
|
if (!canUndo.value) return
|
|
1300
|
-
|
|
1301
|
-
|
|
1313
|
+
const snapshot = history.undo(takeSnapshot())
|
|
1314
|
+
if (!snapshot) return
|
|
1315
|
+
restoreSnapshot(snapshot)
|
|
1302
1316
|
emit("structure-change", { rows: state.value, columns: cols.value, reason: "undo" })
|
|
1303
1317
|
}
|
|
1304
1318
|
const redo = () => {
|
|
1305
1319
|
if (!canRedo.value) return
|
|
1306
|
-
|
|
1307
|
-
|
|
1320
|
+
const snapshot = history.redo(takeSnapshot())
|
|
1321
|
+
if (!snapshot) return
|
|
1322
|
+
restoreSnapshot(snapshot)
|
|
1308
1323
|
emit("structure-change", { rows: state.value, columns: cols.value, reason: "redo" })
|
|
1309
1324
|
}
|
|
1310
1325
|
|
|
@@ -3353,8 +3368,7 @@ const loadSheetIntoEngine = (idx: number) => {
|
|
|
3353
3368
|
colWidths.value = { ...ext.widths }
|
|
3354
3369
|
setSel(null)
|
|
3355
3370
|
editing.value = null
|
|
3356
|
-
|
|
3357
|
-
redoStack.length = 0
|
|
3371
|
+
history.clear()
|
|
3358
3372
|
if (scrollEl.value) scrollEl.value.scrollTop = 0
|
|
3359
3373
|
}
|
|
3360
3374
|
/**
|
|
@@ -3766,7 +3780,10 @@ const findReplaceCurrent = () => {
|
|
|
3766
3780
|
const next = findCase.value
|
|
3767
3781
|
? raw.split(findQuery.value).join(findReplaceText.value)
|
|
3768
3782
|
: raw.replace(new RegExp(escapeRegExp(findQuery.value), "gi"), findReplaceText.value)
|
|
3769
|
-
if (next !== raw)
|
|
3783
|
+
if (next !== raw) {
|
|
3784
|
+
pushHistory()
|
|
3785
|
+
setCellValue(m.r, m.name, raw, next)
|
|
3786
|
+
}
|
|
3770
3787
|
findScan()
|
|
3771
3788
|
findNext(1)
|
|
3772
3789
|
}
|
|
@@ -4306,6 +4323,7 @@ const validateAndSet = async (r: number, name: string, old: any, next: any): Pro
|
|
|
4306
4323
|
return false
|
|
4307
4324
|
}
|
|
4308
4325
|
clearValError(r, name)
|
|
4326
|
+
pushHistory()
|
|
4309
4327
|
setCellValue(r, name, old, next)
|
|
4310
4328
|
return true
|
|
4311
4329
|
}
|
package/index.ts
CHANGED
|
@@ -203,19 +203,29 @@ export { DEFAULT_LAYOUT_VIEW, parseView, qLayoutKey } from "./lib/layout"
|
|
|
203
203
|
export type { LayoutView, LayoutZone, LayoutZonePlacement, QLayoutContext } from "./lib/layout"
|
|
204
204
|
export { vRipple } from "./lib/ripple"
|
|
205
205
|
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
206
|
-
export { chartToECharts, channelValue, pickFromEvent, tableModel, textValue } from "./lib/chart"
|
|
206
|
+
export { chartToECharts, channelValue, pickFromEvent, tableCsv, tableModel, textValue } from "./lib/chart"
|
|
207
207
|
export type {
|
|
208
208
|
QChartAxis,
|
|
209
209
|
QChartChannel,
|
|
210
210
|
QChartColumn,
|
|
211
211
|
QChartConfig,
|
|
212
|
+
QChartFormat,
|
|
213
|
+
QChartFormatKind,
|
|
214
|
+
QChartFormatOptions,
|
|
215
|
+
QChartGaugeBand,
|
|
212
216
|
QChartLegend,
|
|
213
217
|
QChartMark,
|
|
214
218
|
QChartMarkType,
|
|
215
219
|
QChartOrientation,
|
|
216
220
|
QChartTable,
|
|
221
|
+
QChartTableTotals,
|
|
222
|
+
QChartTableView,
|
|
223
|
+
QChartThreshold,
|
|
224
|
+
QChartTooltip,
|
|
217
225
|
QChartPick,
|
|
218
226
|
} from "./lib/chart"
|
|
227
|
+
export { DEFAULT_CURRENCY, DEFAULT_LOCALE, formatValue } from "./lib/chartFormat"
|
|
228
|
+
export { boxStats, histogram, linearFit, waterfallSteps } from "./lib/chartStats"
|
|
219
229
|
export { vTouchPan } from "./lib/touchPan"
|
|
220
230
|
export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
|
|
221
231
|
export { vTouchHold } from "./lib/touchHold"
|
package/lib/chart.axis.test.ts
CHANGED
|
@@ -28,7 +28,7 @@ function renderSvg(config: Record<string, any>, width: number, height: number):
|
|
|
28
28
|
width,
|
|
29
29
|
height,
|
|
30
30
|
})
|
|
31
|
-
chart.setOption(chartToECharts({ theme: THEME, ...config }))
|
|
31
|
+
chart.setOption(chartToECharts({ marks: [], theme: THEME, ...config }))
|
|
32
32
|
const svg = chart.renderToSVGString()
|
|
33
33
|
chart.dispose()
|
|
34
34
|
return svg
|