@bagelink/vue 1.15.202 → 1.15.205

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.
Files changed (69) hide show
  1. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  2. package/dist/components/Swiper.vue.d.ts +2 -2
  3. package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
  4. package/dist/components/charts/BarChart.vue.d.ts.map +1 -1
  5. package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -1
  6. package/dist/components/charts/Donut.vue.d.ts.map +1 -1
  7. package/dist/components/charts/Funnel.vue.d.ts.map +1 -1
  8. package/dist/components/charts/Gauge.vue.d.ts.map +1 -1
  9. package/dist/components/charts/LineChart.vue.d.ts.map +1 -1
  10. package/dist/components/charts/MapChart.vue.d.ts +25 -0
  11. package/dist/components/charts/MapChart.vue.d.ts.map +1 -0
  12. package/dist/components/charts/RadialBars.vue.d.ts.map +1 -1
  13. package/dist/components/charts/RankBars.vue.d.ts.map +1 -1
  14. package/dist/components/charts/StatCard.vue.d.ts.map +1 -1
  15. package/dist/components/charts/core/arc.d.ts +6 -0
  16. package/dist/components/charts/core/arc.d.ts.map +1 -0
  17. package/dist/components/charts/core/useChartTip.d.ts +13 -0
  18. package/dist/components/charts/core/useChartTip.d.ts.map +1 -0
  19. package/dist/components/charts/core/world-paths.d.ts +13 -0
  20. package/dist/components/charts/core/world-paths.d.ts.map +1 -0
  21. package/dist/components/charts/index.d.ts +2 -0
  22. package/dist/components/charts/index.d.ts.map +1 -1
  23. package/dist/components/charts/mapTypes.d.ts +7 -0
  24. package/dist/components/charts/mapTypes.d.ts.map +1 -0
  25. package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
  26. package/dist/components/form/inputs/DateInput.vue.d.ts +3 -0
  27. package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
  28. package/dist/components/form/inputs/NumberInput.vue.d.ts.map +1 -1
  29. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  30. package/dist/components/form/inputs/index.d.ts +0 -1
  31. package/dist/components/form/inputs/index.d.ts.map +1 -1
  32. package/dist/components/index.d.ts +2 -2
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/form-flow/FormFlowPlayer.vue.d.ts +18 -0
  35. package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
  36. package/dist/form-flow/structure.d.ts +12 -0
  37. package/dist/form-flow/structure.d.ts.map +1 -1
  38. package/dist/index.cjs +177 -154
  39. package/dist/index.mjs +37661 -43116
  40. package/dist/style.css +1 -2
  41. package/package.json +1 -1
  42. package/src/components/charts/BarChart.vue +15 -15
  43. package/src/components/charts/ChartTooltip.vue +7 -6
  44. package/src/components/charts/Donut.vue +25 -40
  45. package/src/components/charts/Funnel.vue +10 -12
  46. package/src/components/charts/Gauge.vue +4 -2
  47. package/src/components/charts/LineChart.vue +15 -17
  48. package/src/components/charts/MapChart.vue +198 -0
  49. package/src/components/charts/RadialBars.vue +3 -1
  50. package/src/components/charts/RankBars.vue +2 -0
  51. package/src/components/charts/Sparkline.vue +1 -1
  52. package/src/components/charts/StatCard.vue +2 -0
  53. package/src/components/charts/core/arc.ts +53 -0
  54. package/src/components/charts/core/useChartTip.ts +35 -0
  55. package/src/components/charts/core/world-paths.ts +351 -0
  56. package/src/components/charts/index.ts +3 -0
  57. package/src/components/charts/mapTypes.ts +6 -0
  58. package/src/components/dataTable/useTableVirtualization.ts +5 -5
  59. package/src/components/form/inputs/DateInput.vue +9 -1
  60. package/src/components/form/inputs/NumberInput.vue +17 -6
  61. package/src/components/form/inputs/index.ts +0 -1
  62. package/src/components/index.ts +2 -2
  63. package/src/form-flow/FormFlowPlayer.vue +49 -0
  64. package/src/form-flow/structure.ts +12 -0
  65. package/dist/components/dataTable/useTableVirtualization.d.ts +0 -25
  66. package/dist/components/dataTable/useTableVirtualization.d.ts.map +0 -1
  67. package/dist/components/form/inputs/JSONInput.vue.d.ts +0 -21
  68. package/dist/components/form/inputs/JSONInput.vue.d.ts.map +0 -1
  69. package/src/components/form/inputs/JSONInput.vue +0 -60
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.202",
4
+ "version": "1.15.205",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -7,15 +7,18 @@
7
7
  * <BarChart :series="[{name:'A',data:[…]},{name:'B',data:[…]}]" stacked />
8
8
  */
9
9
  import { computed, ref } from 'vue'
10
- import { chartUid } from './core/uid'
10
+ import ChartTooltip from './ChartTooltip.vue'
11
11
  import type { RawPoint, RawSeries } from './core/data'
12
12
  import { allValues, normalizeSeries, sharedLabels } from './core/data'
13
- import { resolveColor } from './core/palette'
14
13
  import { formatLabel, formatValue, type ValueFormat } from './core/format'
15
- import { band, linear, niceMax, ticks } from './core/useScale'
16
- import { useChartFrame } from './core/useChartFrame'
14
+ import { resolveColor } from './core/palette'
15
+ import { chartUid } from './core/uid'
17
16
  import { useChartAnim } from './core/useChartAnim'
18
- import ChartTooltip from './ChartTooltip.vue'
17
+ import { useChartFrame } from './core/useChartFrame'
18
+ import { tipPinnedStyle, trackLocalPointer } from './core/useChartTip'
19
+ import { band, linear, niceMax, ticks } from './core/useScale'
20
+
21
+ defineOptions({ name: 'BglBarChart' })
19
22
 
20
23
  const props = withDefaults(defineProps<{
21
24
  data?: RawPoint[]
@@ -104,18 +107,19 @@ const uid = chartUid('bar')
104
107
  const gradColors = computed(() => [...new Set(bars.value.map(b => b.color))])
105
108
  const gradIdFor = (color: string) => `${uid}-${gradColors.value.indexOf(color)}`
106
109
 
107
- // ── Shared hover tooltip (mouse-follow, per category) ────────────────────────
110
+ // ── Shared hover tooltip (prefer="top", pinned) ──────────────────────────────
108
111
  const hoverIdx = ref<number | null>(null)
109
- const mouseX = ref(0)
112
+ const tipX = ref(0)
110
113
  function onMove(e: MouseEvent) {
114
+ if (!cats.value.length) return
115
+ trackLocalPointer(e, tipX)
111
116
  const host = el.value
112
- if (!host || !cats.value.length) return
117
+ if (!host) return
113
118
  const rect = host.getBoundingClientRect()
114
119
  let local = e.clientX - rect.left - pad.value.left
115
120
  if (flipX(0) !== 0) local = innerWidth.value - local
116
121
  const i = Math.floor((local / Math.max(1, innerWidth.value)) * cats.value.length)
117
122
  hoverIdx.value = Math.min(cats.value.length - 1, Math.max(0, i))
118
- mouseX.value = e.clientX - rect.left
119
123
  }
120
124
  function onLeave() { hoverIdx.value = null }
121
125
 
@@ -132,11 +136,7 @@ const hoverTotal = computed(() => {
132
136
  if (hoverIdx.value == null || !props.stacked) return null
133
137
  return formatValue(stackTotals.value[hoverIdx.value] ?? 0, valueFmt.value)
134
138
  })
135
- const tooltipStyle = computed(() => {
136
- const half = 70
137
- const x = Math.min(Math.max(mouseX.value, half), Math.max(half, width.value - half))
138
- return { left: `${x}px` }
139
- })
139
+ const tooltipStyle = tipPinnedStyle(tipX, width)
140
140
  </script>
141
141
 
142
142
  <template>
@@ -164,7 +164,7 @@ const tooltipStyle = computed(() => {
164
164
  v-for="(b, i) in bars" :key="i"
165
165
  :x="b.x" :y="b.y" :width="Math.max(0, b.w)" :height="Math.max(0, b.h)"
166
166
  :rx="Math.min(radius, b.w / 2)" :fill="gradient ? `url(#${gradIdFor(b.color)})` : b.color"
167
- class="bgl-chart__bar" :class="{ 'is-dim opacity-5': hoverIdx != null && hoverIdx !== b.cat }"
167
+ class="bgl-chart__bar" :class="{ 'opacity-5': hoverIdx != null && hoverIdx !== b.cat }"
168
168
  />
169
169
 
170
170
  <g class="bgl-chart__xlabels">
@@ -1,16 +1,17 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
3
  * ChartTooltip — the single tooltip used by every chart (Line, Bar, Donut,
4
- * Funnel). Renders a label header, optional hero value, per-series rows
4
+ * Funnel, Map). Renders a label header, optional hero value, per-series rows
5
5
  * (dot · name · value), an optional total row, and an optional stats block.
6
6
  *
7
7
  * Positioned by the host via `style` (absolute left/top); the host owns
8
- * mouse tracking. Non-interactive (pointer-events: none).
8
+ * mouse tracking via `useChartTip` helpers. Non-interactive (pointer-events: none).
9
9
  *
10
- * Placement is dynamic by default: the tooltip measures itself and flips to
11
- * the opposite side of its anchor whenever the preferred side would clip past
12
- * the top/bottom of the viewport. Hosts only express a *preferred* side via
13
- * `prefer` (or the legacy `above`/`below` flags).
10
+ * Placement modes:
11
+ * - `prefer="above"|"below"`: cursor-anchored (Donut, Map, Funnel) — host sets
12
+ * `{ left, top }` from the pointer.
13
+ * - `prefer="top"`: pinned to the top of the chart (Line, Bar) — host sets
14
+ * `{ left }` only; no vertical flip.
14
15
  */
15
16
  import { nextTick, ref, watch } from 'vue'
16
17
  import { useI18n } from '@bagelink/vue'
@@ -11,11 +11,13 @@
11
11
  */
12
12
  import { computed, ref } from 'vue'
13
13
  import { useI18n } from '@bagelink/vue'
14
- import { ramp, resolveColor } from './core/palette'
14
+ import type { ThemeType } from '../../types'
15
+ import ChartTooltip from './ChartTooltip.vue'
16
+ import { slicePath } from './core/arc'
15
17
  import { formatValue, type ValueFormat } from './core/format'
18
+ import { ramp, resolveColor } from './core/palette'
16
19
  import { useChartAnim } from './core/useChartAnim'
17
- import ChartTooltip from './ChartTooltip.vue'
18
- import type { ThemeType } from '../../types'
20
+ import { tipFollowStyle, trackLocalPointer } from './core/useChartTip'
19
21
 
20
22
  defineOptions({ name: 'BglDonut' })
21
23
 
@@ -66,10 +68,9 @@ const props = withDefaults(defineProps<{
66
68
 
67
69
  const { $t } = useI18n()
68
70
  const el = ref<HTMLElement>()
69
- const { progress } = useChartAnim({ el, enabled: props.animated, duration: 800 })
71
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
70
72
 
71
73
  const fmt = computed<ValueFormat>(() => ({ currency: props.currency, compact: true }))
72
- /** Formatted amount for a slice (used on-chart, in the legend + tooltip). */
73
74
  function fmtValue(v: number): string {
74
75
  return formatValue(v, { currency: props.currency, compact: true })
75
76
  }
@@ -79,11 +80,8 @@ const inner = computed(() => (props.pie ? 0 : radius.value * (1 - props.thicknes
79
80
 
80
81
  interface Arc {
81
82
  d: string; color: string; label: string; value: number; pct: number
82
- /** Centroid for on-slice label placement. */
83
83
  lx: number; ly: number
84
- /** Whether this slice shows an on-chart label at all. */
85
84
  tagged: boolean
86
- /** Stacked on-slice label pieces (empty string = that line is hidden). */
87
85
  tagName: string; tagValue: string; tagPct: string
88
86
  }
89
87
  const arcs = computed<Arc[]>(() => {
@@ -91,32 +89,14 @@ const arcs = computed<Arc[]>(() => {
91
89
  const ri = inner.value
92
90
  const cx = r
93
91
  const cy = r
94
- // Radius where the label centroid sits — pushed toward the outer edge
95
- // (68% of the way from the inner edge to the rim) so labels sit near the rim.
96
92
  const rMid = ri + (r - ri) * 0.68
97
- let angle = -Math.PI / 2 // start at top
93
+ let angle = -Math.PI / 2
98
94
  return props.data.map((d, i) => {
99
95
  const frac = Math.max(0, d.value) / total.value
100
96
  const sweep = frac * Math.PI * 2 * progress.value
101
97
  const a0 = angle
102
- const a1 = angle + sweep
103
- angle = a1
104
- const aMid = (a0 + a1) / 2
105
- const large = sweep > Math.PI ? 1 : 0
106
- const x0 = cx + r * Math.cos(a0)
107
- const y0 = cy + r * Math.sin(a0)
108
- const x1 = cx + r * Math.cos(a1)
109
- const y1 = cy + r * Math.sin(a1)
110
- let d2: string
111
- if (ri > 0) {
112
- const xi1 = cx + ri * Math.cos(a1)
113
- const yi1 = cy + ri * Math.sin(a1)
114
- const xi0 = cx + ri * Math.cos(a0)
115
- const yi0 = cy + ri * Math.sin(a0)
116
- d2 = `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} L ${xi1} ${yi1} A ${ri} ${ri} 0 ${large} 0 ${xi0} ${yi0} Z`
117
- } else {
118
- d2 = `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} Z`
119
- }
98
+ angle += sweep
99
+ const aMid = a0 + sweep / 2
120
100
  const sliceColor = d.color
121
101
  ? resolveColor(d.color)
122
102
  : props.mode === 'multi'
@@ -125,7 +105,11 @@ const arcs = computed<Arc[]>(() => {
125
105
  const pct = frac * 100
126
106
  const tagged = !!props.labels && frac >= props.labelMinShare
127
107
  return {
128
- d: d2, color: sliceColor, label: d.label, value: d.value, pct,
108
+ d: slicePath(cx, cy, r, ri, a0, sweep),
109
+ color: sliceColor,
110
+ label: d.label,
111
+ value: d.value,
112
+ pct,
129
113
  lx: cx + rMid * Math.cos(aMid),
130
114
  ly: cy + rMid * Math.sin(aMid),
131
115
  tagged,
@@ -142,21 +126,22 @@ const centerText = computed(() => {
142
126
  return ''
143
127
  })
144
128
 
145
- // ── Shared hover tooltip (slice-based, mouse-follow) ─────────────────────────
129
+ // ── Shared hover tooltip (cursor-follow, prefer="above") ─────────────────────
146
130
  const hoverIdx = ref<number | null>(null)
147
131
  const tipX = ref(0)
148
132
  const tipY = ref(0)
149
133
  function onSliceEnter(i: number) { hoverIdx.value = i }
150
134
  function onLeave() { hoverIdx.value = null }
151
- function onMove(e: MouseEvent) {
152
- const host = (e.currentTarget as HTMLElement).getBoundingClientRect()
153
- tipX.value = e.clientX - host.left
154
- tipY.value = e.clientY - host.top
155
- }
135
+ function onMove(e: MouseEvent) { trackLocalPointer(e, tipX, tipY) }
136
+
156
137
  const hoverArc = computed(() => (hoverIdx.value == null ? null : arcs.value[hoverIdx.value]))
157
- const hoverValue = computed(() => (hoverArc.value ? fmtValue(hoverArc.value.value) : ''))
158
- const hoverStats = computed(() => (hoverArc.value ? [{ label: $t('charts.share'), value: `${hoverArc.value.pct.toFixed(0)}%` }] : []))
159
- const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.value}px` }))
138
+ const hoverRows = computed(() => hoverArc.value
139
+ ? [{ name: hoverArc.value.label, color: hoverArc.value.color, value: fmtValue(hoverArc.value.value) }]
140
+ : [])
141
+ const hoverStats = computed(() => (hoverArc.value
142
+ ? [{ label: $t('charts.share'), value: `${hoverArc.value.pct.toFixed(0)}%` }]
143
+ : []))
144
+ const tooltipStyle = tipFollowStyle(tipX, tipY)
160
145
  </script>
161
146
 
162
147
  <template>
@@ -191,7 +176,7 @@ const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.val
191
176
  </div>
192
177
  <ChartTooltip
193
178
  v-if="hoverArc" prefer="above" :style="tooltipStyle"
194
- :label="hoverArc.label" :hero="hoverValue" :stats="hoverStats"
179
+ :label="hoverArc.label" :rows="hoverRows" :stats="hoverStats"
195
180
  />
196
181
  </div>
197
182
 
@@ -17,6 +17,7 @@ import { formatValue, type ValueFormat } from './core/format'
17
17
  import { chartUid } from './core/uid'
18
18
  import ChartTooltip from './ChartTooltip.vue'
19
19
  import { useChartAnim } from './core/useChartAnim'
20
+ import { tipFollowStyle, trackLocalPointer } from './core/useChartTip'
20
21
  import type { ThemeType } from '../../types'
21
22
 
22
23
  defineOptions({ name: 'BglFunnel' })
@@ -221,18 +222,14 @@ const subSegments = computed(() => {
221
222
  return out
222
223
  })
223
224
 
224
- // ── Shared hover tooltip (matches LineChart) ─────────────────────────────────
225
+ // ── Shared hover tooltip (cursor-follow, prefer="above") ─────────────────────
225
226
  const hoverIdx = ref<number | null>(null)
226
227
  const tipX = ref(0)
227
228
  const tipY = ref(0)
228
229
 
229
230
  function onSegEnter(i: number) { hoverIdx.value = i }
230
231
  function onSegLeave() { hoverIdx.value = null }
231
- function onFlowMove(e: MouseEvent) {
232
- const host = (e.currentTarget as HTMLElement).getBoundingClientRect()
233
- tipX.value = e.clientX - host.left
234
- tipY.value = e.clientY - host.top
235
- }
232
+ function onFlowMove(e: MouseEvent) { trackLocalPointer(e, tipX, tipY) }
236
233
 
237
234
  const hoverStage = computed(() => (hoverIdx.value == null ? null : stages.value[hoverIdx.value]))
238
235
  const hoverStats = computed(() => {
@@ -247,7 +244,7 @@ const hoverTotal = computed(() => {
247
244
  const s = hoverStage.value
248
245
  return s && isSegmented.value ? s.display : null
249
246
  })
250
- const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.value}px` }))
247
+ const tooltipStyle = tipFollowStyle(tipX, tipY)
251
248
  </script>
252
249
 
253
250
  <template>
@@ -274,7 +271,7 @@ const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.val
274
271
  <path
275
272
  v-for="(s, i) in segments" :key="i"
276
273
  :d="s.path" :fill="gradient ? `url(#${s.gradId})` : s.color"
277
- class="bgl-funnel__seg" :class="{ 'is-dim opacity-5': hoverIdx != null && hoverIdx !== i }"
274
+ class="bgl-funnel__seg" :class="{ 'opacity-5': hoverIdx != null && hoverIdx !== i }"
278
275
  @mouseenter="onSegEnter(i)"
279
276
  />
280
277
  </template>
@@ -290,13 +287,14 @@ const tooltipStyle = computed(() => ({ left: `${tipX.value}px`, top: `${tipY.val
290
287
  </template>
291
288
  </svg>
292
289
 
293
- <!-- Shared tooltip -->
290
+ <!-- Shared tooltip (cursor-follow) -->
294
291
  <ChartTooltip
295
292
  v-if="hoverStage" prefer="above" :style="tooltipStyle"
296
293
  :label="hoverStage.label"
297
- :hero="isSegmented ? undefined : hoverStage.display"
298
- :rows="isSegmented ? hoverStage.rows : []"
299
- :total="isSegmented ? hoverTotal : null"
294
+ :rows="isSegmented
295
+ ? hoverStage.rows
296
+ : [{ name: hoverStage.label, color: hoverStage.color, value: hoverStage.display }]"
297
+ :total="hoverTotal"
300
298
  :stats="hoverStats"
301
299
  />
302
300
 
@@ -38,7 +38,7 @@ const props = withDefaults(defineProps<{
38
38
  })
39
39
 
40
40
  const el = ref<HTMLElement>()
41
- const { progress } = useChartAnim({ el, enabled: props.animated, duration: 900 })
41
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
42
42
 
43
43
  const stroke = computed(() => resolveColor(props.color))
44
44
  const r = computed(() => (props.size - props.thickness) / 2)
@@ -57,8 +57,10 @@ function polar(angleDeg: number): { x: number; y: number } {
57
57
  }
58
58
 
59
59
  function arcPath(fraction: number): string {
60
- const span = arcSpan.value * fraction
60
+ let span = arcSpan.value * fraction
61
61
  if (span <= 0) return ''
62
+ // SVG arcs collapse when start/end coincide — keep just under a full turn.
63
+ if (span >= 360) span = 359.999
62
64
  const a0 = startAngle.value
63
65
  const a1 = startAngle.value + span
64
66
  const p0 = polar(a0)
@@ -8,15 +8,18 @@
8
8
  * <LineChart :series="[{ name:'2024', data:[…] }, { name:'2025', data:[…] }]" />
9
9
  */
10
10
  import { computed, ref } from 'vue'
11
- import { chartUid } from './core/uid'
11
+ import ChartTooltip from './ChartTooltip.vue'
12
12
  import type { RawPoint, RawSeries } from './core/data'
13
13
  import { allValues, normalizeSeries, sharedLabels } from './core/data'
14
- import { alpha, resolveColor } from './core/palette'
15
14
  import { formatLabel, formatValue, type ValueFormat } from './core/format'
16
- import { linear, niceMax, ticks } from './core/useScale'
17
- import { useChartFrame } from './core/useChartFrame'
15
+ import { alpha, resolveColor } from './core/palette'
16
+ import { chartUid } from './core/uid'
18
17
  import { useChartAnim } from './core/useChartAnim'
19
- import ChartTooltip from './ChartTooltip.vue'
18
+ import { useChartFrame } from './core/useChartFrame'
19
+ import { tipPinnedStyle, trackLocalPointer } from './core/useChartTip'
20
+ import { linear, niceMax, ticks } from './core/useScale'
21
+
22
+ defineOptions({ name: 'BglLineChart' })
20
23
 
21
24
  const props = withDefaults(defineProps<{
22
25
  data?: RawPoint[]
@@ -53,7 +56,7 @@ const props = withDefaults(defineProps<{
53
56
  const el = ref<HTMLElement>()
54
57
  const heightRef = computed(() => props.height)
55
58
  const { width, height: plotH, pad, innerWidth, innerHeight, flipX, isRTL } = useChartFrame(el, { height: heightRef })
56
- const { progress } = useChartAnim({ el, enabled: props.animated, duration: 800 })
59
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
57
60
 
58
61
  const series = computed(() => normalizeSeries(props.data, props.series, { labels: props.labels, defaultColor: props.color }))
59
62
  const cats = computed(() => sharedLabels(series.value))
@@ -112,20 +115,20 @@ const labelStride = computed(() => Math.max(1, Math.ceil(cats.value.length / pro
112
115
  const dashLength = computed(() => (innerWidth.value + innerHeight.value) * 2)
113
116
  const dashOffset = computed(() => dashLength.value * (1 - progress.value))
114
117
 
115
- // ── Hover crosshair + shared tooltip (follows the mouse across the plot) ──────
118
+ // ── Hover crosshair + shared tooltip (prefer="top", pinned) ───────────────────
116
119
  const hoverIdx = ref<number | null>(null)
117
- const mouseX = ref(0)
120
+ const tipX = ref(0)
118
121
 
119
122
  function onMove(e: MouseEvent) {
123
+ if (!cats.value.length) return
124
+ trackLocalPointer(e, tipX)
120
125
  const host = el.value
121
- if (!host || !cats.value.length) return
126
+ if (!host) return
122
127
  const rect = host.getBoundingClientRect()
123
- // Mouse x within the plot area (account for left padding + RTL flip).
124
128
  let local = e.clientX - rect.left - pad.value.left
125
129
  if (isRTL.value) local = innerWidth.value - local
126
130
  const idx = Math.round((local / Math.max(1, innerWidth.value)) * (cats.value.length - 1))
127
131
  hoverIdx.value = Math.min(cats.value.length - 1, Math.max(0, idx))
128
- mouseX.value = e.clientX - rect.left
129
132
  }
130
133
  function onLeave() { hoverIdx.value = null }
131
134
 
@@ -158,12 +161,7 @@ const hoverTotal = computed(() => {
158
161
  return formatValue(sum, valueFmt.value)
159
162
  })
160
163
 
161
- // Keep the tooltip inside the chart horizontally.
162
- const tooltipStyle = computed(() => {
163
- const half = 70
164
- const x = Math.min(Math.max(mouseX.value, half), Math.max(half, width.value - half))
165
- return { left: `${x}px` }
166
- })
164
+ const tooltipStyle = tipPinnedStyle(tipX, width)
167
165
  </script>
168
166
 
169
167
  <template>
@@ -0,0 +1,198 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * MapChart — SVG choropleth of countries (ISO-3166-1 alpha-2). Pure SVG, no
4
+ * tiles. Same “plot only / wrap in Card” contract as BarChart / Donut.
5
+ *
6
+ * <MapChart :data="[{ id: 'US', value: 4200 }, { id: 'IL', label: 'Israel', value: 900 }]" />
7
+ * <MapChart :data="rows" color="green" :height="280" />
8
+ */
9
+ import { computed, ref } from 'vue'
10
+ import type { ThemeType } from '../../types'
11
+ import ChartTooltip from './ChartTooltip.vue'
12
+ import { formatValue, type ValueFormat } from './core/format'
13
+ import { resolveColor } from './core/palette'
14
+ import { useChartAnim } from './core/useChartAnim'
15
+ import { tipFollowStyle, trackLocalPointer } from './core/useChartTip'
16
+ import { WORLD_NAMES, WORLD_PATHS, WORLD_VIEWBOX } from './core/world-paths'
17
+ import type { MapDatum } from './mapTypes'
18
+
19
+ defineOptions({ name: 'BglMapChart' })
20
+
21
+ export type { MapDatum }
22
+
23
+ const props = withDefaults(defineProps<{
24
+ data: MapDatum[]
25
+ /** Base tone for the value ramp (low → high intensity). */
26
+ color?: ThemeType | string
27
+ height?: number
28
+ /** Show min→max legend strip under the map. */
29
+ legend?: boolean
30
+ currency?: string
31
+ prefix?: string
32
+ suffix?: string
33
+ animated?: boolean
34
+ }>(), {
35
+ color: 'primary',
36
+ height: 280,
37
+ legend: true,
38
+ animated: true,
39
+ })
40
+
41
+ const el = ref<HTMLElement>()
42
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
43
+
44
+ const fmt = computed<ValueFormat>(() => ({
45
+ currency: props.currency,
46
+ prefix: props.prefix,
47
+ suffix: props.suffix,
48
+ compact: true,
49
+ }))
50
+
51
+ const byId = computed(() => {
52
+ const map = new Map<string, MapDatum>()
53
+ for (const d of props.data) {
54
+ const id = d.id?.toUpperCase?.() ?? ''
55
+ if (!id) continue
56
+ const prev = map.get(id)
57
+ map.set(id, prev
58
+ ? { ...d, id, value: prev.value + d.value, label: d.label ?? prev.label }
59
+ : { ...d, id })
60
+ }
61
+ return map
62
+ })
63
+
64
+ const maxValue = computed(() => Math.max(0, ...[...byId.value.values()].map(d => d.value)))
65
+
66
+ const baseColor = computed(() => resolveColor(props.color))
67
+ const emptyFill = 'color-mix(in srgb, var(--bgl-gray) 12%, var(--bgl-box-bg))'
68
+ const strokeColor = 'color-mix(in srgb, var(--bgl-gray) 28%, var(--bgl-box-bg))'
69
+
70
+ /** Continuous ramp: empty → light tint → full base color by value share. */
71
+ function fillFor(value: number | undefined): string {
72
+ if (value == null || value <= 0 || maxValue.value <= 0) return emptyFill
73
+ const t = Math.sqrt(value / maxValue.value) // sqrt so mid values stay visible
74
+ const pct = 28 + t * 72 * progress.value
75
+ return `color-mix(in srgb, ${baseColor.value} ${pct}%, var(--bgl-box-bg))`
76
+ }
77
+
78
+ interface CountryPath {
79
+ id: string
80
+ d: string
81
+ label: string
82
+ value: number | undefined
83
+ fill: string
84
+ hasData: boolean
85
+ }
86
+
87
+ const countries = computed<CountryPath[]>(() =>
88
+ Object.entries(WORLD_PATHS).map(([id, d]) => {
89
+ const row = byId.value.get(id)
90
+ const value = row?.value
91
+ return {
92
+ id,
93
+ d,
94
+ label: row?.label || WORLD_NAMES[id] || id,
95
+ value,
96
+ fill: fillFor(value),
97
+ hasData: value != null && value > 0,
98
+ }
99
+ }),
100
+ )
101
+
102
+ const hoverId = ref<string | null>(null)
103
+ const tipX = ref(0)
104
+ const tipY = ref(0)
105
+
106
+ function onEnter(id: string) { hoverId.value = id }
107
+ function onLeave() { hoverId.value = null }
108
+ function onMove(e: MouseEvent) { trackLocalPointer(e, tipX, tipY) }
109
+
110
+ const hover = computed(() => {
111
+ if (!hoverId.value) return null
112
+ const c = countries.value.find(x => x.id === hoverId.value)
113
+ return c?.hasData ? c : null
114
+ })
115
+
116
+ const hoverRows = computed(() => hover.value
117
+ ? [{ name: hover.value.label, color: hover.value.fill, value: formatValue(hover.value.value!, fmt.value) }]
118
+ : [])
119
+
120
+ const tooltipStyle = tipFollowStyle(tipX, tipY)
121
+
122
+ const legendMin = computed(() => formatValue(0, fmt.value))
123
+ const legendMax = computed(() => formatValue(maxValue.value, fmt.value))
124
+ </script>
125
+
126
+ <template>
127
+ <div ref="el" class="bgl-map w-100p flex column gap-075">
128
+ <div
129
+ class="bgl-map__plot relative w-100p"
130
+ :style="{ height: `${height}px` }"
131
+ @mousemove="onMove"
132
+ @mouseleave="onLeave"
133
+ >
134
+ <svg
135
+ class="bgl-map__svg"
136
+ dir="ltr"
137
+ :viewBox="`0 0 ${WORLD_VIEWBOX.width} ${WORLD_VIEWBOX.height}`"
138
+ preserveAspectRatio="xMidYMid meet"
139
+ width="100%"
140
+ height="100%"
141
+ >
142
+ <path
143
+ v-for="c in countries"
144
+ :key="c.id"
145
+ :d="c.d"
146
+ :fill="c.fill"
147
+ :stroke="strokeColor"
148
+ stroke-width="0.6"
149
+ vector-effect="non-scaling-stroke"
150
+ class="bgl-map__country"
151
+ :class="{
152
+ 'opacity-5': hoverId != null && hoverId !== c.id,
153
+ 'bgl-map__country--hot': hoverId === c.id && c.hasData,
154
+ }"
155
+ @mouseenter="onEnter(c.id)"
156
+ />
157
+ </svg>
158
+
159
+ <ChartTooltip
160
+ v-if="hover"
161
+ prefer="above"
162
+ :style="tooltipStyle"
163
+ :label="hover.label"
164
+ :rows="hoverRows"
165
+ />
166
+ </div>
167
+
168
+ <div
169
+ v-if="legend && maxValue > 0"
170
+ class="bgl-map__legend flex align-items-center gap-05 txt12 color-gray"
171
+ >
172
+ <span class="tabular-nums">{{ legendMin }}</span>
173
+ <div
174
+ class="bgl-map__ramp flex-1"
175
+ :style="{
176
+ background: `linear-gradient(to right, color-mix(in srgb, ${baseColor} 28%, var(--bgl-box-bg)), ${baseColor})`,
177
+ }"
178
+ />
179
+ <span class="tabular-nums">{{ legendMax }}</span>
180
+ </div>
181
+ </div>
182
+ </template>
183
+
184
+ <style scoped>
185
+ .bgl-map__plot { min-height: 0; }
186
+ .bgl-map__svg { display: block; overflow: visible; }
187
+ .bgl-map__country {
188
+ cursor: default;
189
+ transition: opacity 0.15s ease, fill 0.15s ease;
190
+ outline: none;
191
+ }
192
+ .bgl-map__country--hot { stroke: var(--bgl-gray); stroke-width: 1.2; }
193
+ .bgl-map__ramp {
194
+ height: 0.5rem;
195
+ border-radius: 999px;
196
+ min-width: 4rem;
197
+ }
198
+ </style>
@@ -11,6 +11,8 @@ import { alpha, resolveColor } from './core/palette'
11
11
  import { useChartAnim } from './core/useChartAnim'
12
12
  import type { ThemeType } from '../../types'
13
13
 
14
+ defineOptions({ name: 'BglRadialBars' })
15
+
14
16
  interface Ring { label: string; value: number; color?: ThemeType | string; max?: number }
15
17
 
16
18
  const props = withDefaults(defineProps<{
@@ -37,7 +39,7 @@ const props = withDefaults(defineProps<{
37
39
  })
38
40
 
39
41
  const el = ref<HTMLElement>()
40
- const { progress } = useChartAnim({ el, enabled: props.animated, duration: 900 })
42
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
41
43
 
42
44
  const cx = computed(() => props.size / 2)
43
45
 
@@ -11,6 +11,8 @@ import { ramp, resolveColor } from './core/palette'
11
11
  import { formatValue, type ValueFormat } from './core/format'
12
12
  import type { ThemeType } from '../../types'
13
13
 
14
+ defineOptions({ name: 'BglRankBars' })
15
+
14
16
  interface Row { label: string; value: number; color?: ThemeType | string }
15
17
 
16
18
  const props = withDefaults(defineProps<{
@@ -37,7 +37,7 @@ const props = withDefaults(defineProps<{
37
37
  })
38
38
 
39
39
  const el = ref<HTMLElement>()
40
- const { progress } = useChartAnim({ el, enabled: props.animated, duration: 600 })
40
+ const { progress } = useChartAnim({ el, enabled: props.animated, duration: 700 })
41
41
 
42
42
  const stroke = computed(() => resolveColor(props.color))
43
43
  const points = computed(() => normalizeSeries(props.data, undefined)[0]?.points ?? [])
@@ -15,6 +15,8 @@ import { formatValue } from './core/format'
15
15
  import { resolveColor } from './core/palette'
16
16
  import type { ThemeType } from '../../types'
17
17
 
18
+ defineOptions({ name: 'BglStatCard' })
19
+
18
20
  const props = withDefaults(defineProps<{
19
21
  label: string
20
22
  value: number | string