@dnax/ui 0.0.59 → 0.0.61
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/QSidebar.vue +12 -2
- package/components/QSpreadsheet.vue +7 -6
- package/lib/chart.axis.test.ts +127 -0
- package/lib/chart.ts +17 -3
- package/package.json +1 -1
package/components/QSidebar.vue
CHANGED
|
@@ -180,7 +180,10 @@ let isHorizontal = false
|
|
|
180
180
|
|
|
181
181
|
const onPointerDown = (e: PointerEvent) => {
|
|
182
182
|
if (isStatic.value || !open.value || props.disable) return
|
|
183
|
-
|
|
183
|
+
// ⚠️ Ne **pas** `setPointerCapture` ici : capturer au `pointerdown` fait retarger le
|
|
184
|
+
// `click` qui suit vers l'`<aside>` → **aucun** handler interne ne se déclenche (ni le ×
|
|
185
|
+
// de fermeture, ni les items de menu). La capture est prise uniquement si le geste est
|
|
186
|
+
// **confirmé horizontal** (cf. `onPointerMove`), donc un vrai clic n'est jamais capturé.
|
|
184
187
|
startX = e.clientX
|
|
185
188
|
startY = e.clientY
|
|
186
189
|
currentDx = 0
|
|
@@ -198,6 +201,10 @@ const onPointerMove = (e: PointerEvent) => {
|
|
|
198
201
|
if (!isHorizontal) {
|
|
199
202
|
if (Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy)) {
|
|
200
203
|
isHorizontal = true
|
|
204
|
+
// Capture **au moment du swipe confirmé** (et non au pointerdown) : c'est ce qui
|
|
205
|
+
// laisse le `click` d'un vrai clic atteindre les handlers internes, tout en gardant
|
|
206
|
+
// les événements du swipe même si le pointeur sort du panneau.
|
|
207
|
+
rootEl.value.setPointerCapture(e.pointerId)
|
|
201
208
|
rootEl.value.style.transition = "none"
|
|
202
209
|
}
|
|
203
210
|
else return
|
|
@@ -210,11 +217,14 @@ const onPointerMove = (e: PointerEvent) => {
|
|
|
210
217
|
rootEl.value.style.transform = `translateX(${dir === 1 ? travel : -travel}px)`
|
|
211
218
|
}
|
|
212
219
|
|
|
213
|
-
const onPointerUp = () => {
|
|
220
|
+
const onPointerUp = (e: PointerEvent) => {
|
|
214
221
|
if (!dragging.value) return
|
|
215
222
|
dragging.value = false
|
|
216
223
|
const el = rootEl.value
|
|
217
224
|
if (!el) return
|
|
225
|
+
// Relâche la capture prise au swipe confirmé (le navigateur le fait aussi en fin de geste ;
|
|
226
|
+
// on l'explicite pour rester cohérent).
|
|
227
|
+
if (el.hasPointerCapture?.(e.pointerId)) el.releasePointerCapture(e.pointerId)
|
|
218
228
|
el.style.transition = "" // restaure la transition CSS
|
|
219
229
|
const width = el.offsetWidth || parseInt(props.width, 10) || 260
|
|
220
230
|
if (currentDx > Math.min(80, width * 0.3)) {
|
|
@@ -3222,15 +3222,16 @@ watch(
|
|
|
3222
3222
|
const prevKey = localSheets.value[sheetIdx.value]?.key
|
|
3223
3223
|
let n = 1
|
|
3224
3224
|
localSheets.value = v.map((s) => {
|
|
3225
|
-
|
|
3226
|
-
|
|
3225
|
+
// `num` capturé AVANT l'incrément : clé **et** nom par défaut partagent le même numéro
|
|
3226
|
+
// (sinon le 1er onglet s'appelait « Sheet 2 »).
|
|
3227
|
+
const num = n++
|
|
3227
3228
|
// Clés injectées sur les objets du parent (comme pour `rows`) : chaque feuille est
|
|
3228
3229
|
// identifiable avant même d'être ouverte
|
|
3229
3230
|
ensureRowKeys(s.rows)
|
|
3230
3231
|
return {
|
|
3231
3232
|
...s,
|
|
3232
|
-
key,
|
|
3233
|
-
name: s.name ?? sheetName(
|
|
3233
|
+
key: s.key ?? "sheet-" + num,
|
|
3234
|
+
name: s.name ?? sheetName(num),
|
|
3234
3235
|
rows: (s.rows ?? []).map((r) => ({ ...r })),
|
|
3235
3236
|
columns: s.columns ? canonicalColumns(s.columns) : undefined,
|
|
3236
3237
|
}
|
|
@@ -3255,7 +3256,7 @@ watch([state, cols], () => {
|
|
|
3255
3256
|
|
|
3256
3257
|
const activeSheetName = computed(() => {
|
|
3257
3258
|
const s = localSheets.value[sheetIdx.value]
|
|
3258
|
-
if (s) return s.name ??
|
|
3259
|
+
if (s) return s.name ?? sheetName(sheetIdx.value + 1)
|
|
3259
3260
|
return props.sheets?.[0]?.name ?? sheetName(1)
|
|
3260
3261
|
})
|
|
3261
3262
|
const sheetRename = ref<{ idx: number; val: string } | null>(null)
|
|
@@ -3288,7 +3289,7 @@ const addSheet = () => {
|
|
|
3288
3289
|
// premier onglet (propriété sheets vide) — devient multi
|
|
3289
3290
|
const first: QSpreadsheetSheet = {
|
|
3290
3291
|
key: "sheet-1",
|
|
3291
|
-
name:
|
|
3292
|
+
name: sheetName(1),
|
|
3292
3293
|
columns: cols.value.map((c) => ({ ...c })),
|
|
3293
3294
|
rows: state.value.map((r) => ({ ...r })),
|
|
3294
3295
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Tests du **placement des noms d'axes** de `<q-chart>` : `chartToECharts()` produit bien des
|
|
2
|
+
// noms dans le cadre. Vérifié en rendant l'option en SVG via l'**SSR d'ECharts** — du JS pur,
|
|
3
|
+
// aucun navigateur (cf. règle projet).
|
|
4
|
+
//
|
|
5
|
+
// Rappel du bug corrigé : sans `nameLocation`, le moteur place le nom **à la fin** de l'axe —
|
|
6
|
+
// le nom X s'écrivait vers la droite (hors cadre) et le nom Y, centré sur la ligne d'axe,
|
|
7
|
+
// débordait à gauche (rogné) dès que le libellé était large.
|
|
8
|
+
import { describe, expect, it } from "bun:test"
|
|
9
|
+
import * as echarts from "echarts"
|
|
10
|
+
import { chartToECharts } from "./chart"
|
|
11
|
+
|
|
12
|
+
const THEME = { text: "#000000", muted: "#666666", grid: "#dddddd" }
|
|
13
|
+
/** Largeur approximative d'un caractère à 12px (mesure faite sur le rendu ECharts). */
|
|
14
|
+
const CHAR_W = 7
|
|
15
|
+
const FONT_H = 12
|
|
16
|
+
|
|
17
|
+
interface Box {
|
|
18
|
+
x0: number
|
|
19
|
+
x1: number
|
|
20
|
+
y0: number
|
|
21
|
+
y1: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderSvg(config: Record<string, any>, width: number, height: number): string {
|
|
25
|
+
const chart = echarts.init(null as any, null, {
|
|
26
|
+
renderer: "svg",
|
|
27
|
+
ssr: true,
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
})
|
|
31
|
+
chart.setOption(chartToECharts({ theme: THEME, ...config }))
|
|
32
|
+
const svg = chart.renderToSVGString()
|
|
33
|
+
chart.dispose()
|
|
34
|
+
return svg
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Boîte approximative du `<text>` d'un nom d'axe (via `translate` + `text-anchor`). */
|
|
38
|
+
function nameBox(svg: string, name: string): Box | undefined {
|
|
39
|
+
for (const line of svg.split("\n")) {
|
|
40
|
+
if (!line.includes("<text")) continue
|
|
41
|
+
const text = (line.match(/>([^<]*)<\/text>/) ?? [, ""])[1]
|
|
42
|
+
if (text !== name) continue
|
|
43
|
+
const m = line.match(/translate\(([-\d.]+) ([-\d.]+)\)/)
|
|
44
|
+
if (!m) continue
|
|
45
|
+
const x = Number(m[1])
|
|
46
|
+
const y = Number(m[2])
|
|
47
|
+
const anchor = (line.match(/text-anchor="([^"]+)"/) ?? [, "middle"])[1]
|
|
48
|
+
const w = text.length * CHAR_W
|
|
49
|
+
const x0 = anchor === "start" ? x : anchor === "end" ? x - w : x - w / 2
|
|
50
|
+
return { x0, x1: x0 + w, y0: y - FONT_H / 2, y1: y + FONT_H / 2 }
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const inside = (b: Box, w: number, h: number) =>
|
|
56
|
+
b.x0 >= 0 && b.x1 <= w && b.y0 >= 0 && b.y1 <= h
|
|
57
|
+
|
|
58
|
+
const verticalConfig = {
|
|
59
|
+
marks: [
|
|
60
|
+
{
|
|
61
|
+
type: "line",
|
|
62
|
+
data: [
|
|
63
|
+
{ month: "Jan", value: 1_200_000 },
|
|
64
|
+
{ month: "Feb", value: 3_400_000 },
|
|
65
|
+
{ month: "Mar", value: 2_600_000 },
|
|
66
|
+
],
|
|
67
|
+
x: "month",
|
|
68
|
+
y: "value",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
// libellés volontairement larges (le cas qui rognait le nom Y)
|
|
72
|
+
x: { label: "Month of the year" },
|
|
73
|
+
y: { label: "Revenue in thousands" },
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const horizontalConfig = {
|
|
77
|
+
marks: [
|
|
78
|
+
{
|
|
79
|
+
type: "bar",
|
|
80
|
+
orientation: "horizontal",
|
|
81
|
+
data: [
|
|
82
|
+
{ dept: "Engineering", value: 34 },
|
|
83
|
+
{ dept: "Sales", value: 26 },
|
|
84
|
+
],
|
|
85
|
+
x: "dept",
|
|
86
|
+
y: "value",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
x: { label: "Headcount" },
|
|
90
|
+
y: { label: "Department" },
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("noms d'axes — placement dans le cadre", () => {
|
|
94
|
+
it("X et Y restent dans le cadre (vertical, horizontal, petit canevas)", () => {
|
|
95
|
+
const cases: [Record<string, any>, number, number][] = [
|
|
96
|
+
[verticalConfig, 520, 280],
|
|
97
|
+
[horizontalConfig, 520, 280],
|
|
98
|
+
[verticalConfig, 300, 160],
|
|
99
|
+
]
|
|
100
|
+
for (const [config, w, h] of cases) {
|
|
101
|
+
const svg = renderSvg(config, w, h)
|
|
102
|
+
const bx = nameBox(svg, config.x.label)
|
|
103
|
+
const by = nameBox(svg, config.y.label)
|
|
104
|
+
expect(bx).toBeDefined()
|
|
105
|
+
expect(by).toBeDefined()
|
|
106
|
+
expect(inside(bx!, w, h)).toBe(true)
|
|
107
|
+
expect(inside(by!, w, h)).toBe(true)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("le nom X est centré sous l'axe, pas collé au bord droit", () => {
|
|
112
|
+
const svg = renderSvg(verticalConfig, 520, 280)
|
|
113
|
+
const bx = nameBox(svg, verticalConfig.x.label)!
|
|
114
|
+
// centré : commence bien après le bord gauche et finit bien avant le bord droit
|
|
115
|
+
expect(bx.x0).toBeGreaterThan(60)
|
|
116
|
+
expect(bx.x1).toBeLessThan(520 - 60)
|
|
117
|
+
// sous la zone de tracé
|
|
118
|
+
expect(bx.y0).toBeGreaterThan(200)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("le nom Y est ancré à gauche sur la ligne d'axe (jamais rogné), en haut", () => {
|
|
122
|
+
const svg = renderSvg(verticalConfig, 520, 280)
|
|
123
|
+
const by = nameBox(svg, verticalConfig.y.label)!
|
|
124
|
+
expect(by.x0).toBeGreaterThan(0)
|
|
125
|
+
expect(by.y1).toBeLessThan(160)
|
|
126
|
+
})
|
|
127
|
+
})
|
package/lib/chart.ts
CHANGED
|
@@ -1180,7 +1180,7 @@ export function chartToECharts(config: QChartConfig): Record<string, any> {
|
|
|
1180
1180
|
const scaleBand = visualMaps.some((vm) => vm.show !== false) ? 30 : 0
|
|
1181
1181
|
/** Bandeau du **nom d'axe** (nom de l'axe y au-dessus, nom de l'axe x en dessous) */
|
|
1182
1182
|
const axisNameTop = config.y?.label ? 16 : 0
|
|
1183
|
-
const axisNameBottom = config.x?.label ?
|
|
1183
|
+
const axisNameBottom = config.x?.label ? 22 : 0
|
|
1184
1184
|
const legendOption = !legendOn
|
|
1185
1185
|
? { show: false }
|
|
1186
1186
|
: {
|
|
@@ -1241,22 +1241,36 @@ export function chartToECharts(config: QChartConfig): Record<string, any> {
|
|
|
1241
1241
|
splitLine: { show: axis?.grid ?? true, lineStyle: { color: theme.grid } },
|
|
1242
1242
|
})
|
|
1243
1243
|
|
|
1244
|
+
// ─── Nom d'axe : placement **toujours dans le cadre** ───
|
|
1245
|
+
// Axe **horizontal** : centré sous l'axe (`middle`). À la fin de l'axe (`end`, le défaut
|
|
1246
|
+
// ECharts) le nom s'écrivait vers la droite, hors du cadre.
|
|
1247
|
+
const axisNameX = { nameLocation: "middle" as const, nameGap: 32 }
|
|
1248
|
+
// Axe **vertical** : ancré à **gauche** (`align: left`) sur la ligne d'axe, en haut.
|
|
1249
|
+
// Un `nameGap` fixe ne peut pas convenir : un nom centré sur l'axe déborde à gauche, un nom
|
|
1250
|
+
// rotaté au milieu recouvre/est rogné par les nombres (largeur variable : `1,000` vs
|
|
1251
|
+
// `1,000,000`). Ancré à gauche, il s'étend vers la droite → jamais rogné, jamais superposé.
|
|
1252
|
+
const axisNameY = {
|
|
1253
|
+
nameLocation: "end" as const,
|
|
1254
|
+
nameTextStyle: { ...axisLabel, align: "left" as const },
|
|
1255
|
+
}
|
|
1244
1256
|
// Orientation horizontale : catégories sur l'axe Y
|
|
1245
1257
|
const hasBar = series.some((s) => s.type === "bar")
|
|
1246
|
-
const
|
|
1258
|
+
const xAxisBase = hasHeatmap
|
|
1247
1259
|
? categoryAxis(config.x, categories, true)
|
|
1248
1260
|
: horizontal
|
|
1249
1261
|
? valueAxis(config.x, hasBar)
|
|
1250
1262
|
: axisType === "category"
|
|
1251
1263
|
? categoryAxis(config.x)
|
|
1252
1264
|
: valueAxis(config.x, false)
|
|
1253
|
-
const
|
|
1265
|
+
const yAxisBase = hasHeatmap
|
|
1254
1266
|
? categoryAxis(config.y, rowCategories, true)
|
|
1255
1267
|
: horizontal
|
|
1256
1268
|
? axisType === "category"
|
|
1257
1269
|
? categoryAxis(config.y)
|
|
1258
1270
|
: valueAxis(config.y, false)
|
|
1259
1271
|
: valueAxis(config.y, hasBar)
|
|
1272
|
+
const xAxis = config.x?.label ? { ...xAxisBase, ...axisNameX } : xAxisBase
|
|
1273
|
+
const yAxis = config.y?.label ? { ...yAxisBase, ...axisNameY } : yAxisBase
|
|
1260
1274
|
|
|
1261
1275
|
// Familles **hors axes** (`pie` seul) : ni grid ni xAxis/yAxis — sinon ECharts laisse
|
|
1262
1276
|
// un cadre fantôme et décale le titre.
|