@dnax/ui 0.0.59 → 0.0.60

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.
@@ -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
- const key = s.key ?? "sheet-" + n
3226
- n++
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(n),
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 ?? "Sheet " + (sheetIdx.value + 1)
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: "Sheet 1",
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 ? 16 : 0
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 xAxis = hasHeatmap
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 yAxis = hasHeatmap
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.59",
3
+ "version": "0.0.60",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",