@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.
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +2 -2
- package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
- package/dist/components/charts/BarChart.vue.d.ts.map +1 -1
- package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -1
- package/dist/components/charts/Donut.vue.d.ts.map +1 -1
- package/dist/components/charts/Funnel.vue.d.ts.map +1 -1
- package/dist/components/charts/Gauge.vue.d.ts.map +1 -1
- package/dist/components/charts/LineChart.vue.d.ts.map +1 -1
- package/dist/components/charts/MapChart.vue.d.ts +25 -0
- package/dist/components/charts/MapChart.vue.d.ts.map +1 -0
- package/dist/components/charts/RadialBars.vue.d.ts.map +1 -1
- package/dist/components/charts/RankBars.vue.d.ts.map +1 -1
- package/dist/components/charts/StatCard.vue.d.ts.map +1 -1
- package/dist/components/charts/core/arc.d.ts +6 -0
- package/dist/components/charts/core/arc.d.ts.map +1 -0
- package/dist/components/charts/core/useChartTip.d.ts +13 -0
- package/dist/components/charts/core/useChartTip.d.ts.map +1 -0
- package/dist/components/charts/core/world-paths.d.ts +13 -0
- package/dist/components/charts/core/world-paths.d.ts.map +1 -0
- package/dist/components/charts/index.d.ts +2 -0
- package/dist/components/charts/index.d.ts.map +1 -1
- package/dist/components/charts/mapTypes.d.ts +7 -0
- package/dist/components/charts/mapTypes.d.ts.map +1 -0
- package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/DateInput.vue.d.ts +3 -0
- package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/NumberInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/index.d.ts +0 -1
- package/dist/components/form/inputs/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/form-flow/FormFlowPlayer.vue.d.ts +18 -0
- package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
- package/dist/form-flow/structure.d.ts +12 -0
- package/dist/form-flow/structure.d.ts.map +1 -1
- package/dist/index.cjs +177 -154
- package/dist/index.mjs +37661 -43116
- package/dist/style.css +1 -2
- package/package.json +1 -1
- package/src/components/charts/BarChart.vue +15 -15
- package/src/components/charts/ChartTooltip.vue +7 -6
- package/src/components/charts/Donut.vue +25 -40
- package/src/components/charts/Funnel.vue +10 -12
- package/src/components/charts/Gauge.vue +4 -2
- package/src/components/charts/LineChart.vue +15 -17
- package/src/components/charts/MapChart.vue +198 -0
- package/src/components/charts/RadialBars.vue +3 -1
- package/src/components/charts/RankBars.vue +2 -0
- package/src/components/charts/Sparkline.vue +1 -1
- package/src/components/charts/StatCard.vue +2 -0
- package/src/components/charts/core/arc.ts +53 -0
- package/src/components/charts/core/useChartTip.ts +35 -0
- package/src/components/charts/core/world-paths.ts +351 -0
- package/src/components/charts/index.ts +3 -0
- package/src/components/charts/mapTypes.ts +6 -0
- package/src/components/dataTable/useTableVirtualization.ts +5 -5
- package/src/components/form/inputs/DateInput.vue +9 -1
- package/src/components/form/inputs/NumberInput.vue +17 -6
- package/src/components/form/inputs/index.ts +0 -1
- package/src/components/index.ts +2 -2
- package/src/form-flow/FormFlowPlayer.vue +49 -0
- package/src/form-flow/structure.ts +12 -0
- package/dist/components/dataTable/useTableVirtualization.d.ts +0 -25
- package/dist/components/dataTable/useTableVirtualization.d.ts.map +0 -1
- package/dist/components/form/inputs/JSONInput.vue.d.ts +0 -21
- package/dist/components/form/inputs/JSONInput.vue.d.ts.map +0 -1
- package/src/components/form/inputs/JSONInput.vue +0 -60
package/package.json
CHANGED
|
@@ -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
|
|
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 {
|
|
16
|
-
import {
|
|
14
|
+
import { resolveColor } from './core/palette'
|
|
15
|
+
import { chartUid } from './core/uid'
|
|
17
16
|
import { useChartAnim } from './core/useChartAnim'
|
|
18
|
-
import
|
|
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 (
|
|
110
|
+
// ── Shared hover tooltip (prefer="top", pinned) ──────────────────────────────
|
|
108
111
|
const hoverIdx = ref<number | null>(null)
|
|
109
|
-
const
|
|
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
|
|
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 =
|
|
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="{ '
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `prefer
|
|
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 {
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
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" :
|
|
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 (
|
|
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 =
|
|
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="{ '
|
|
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
|
-
:
|
|
298
|
-
|
|
299
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 {
|
|
17
|
-
import {
|
|
15
|
+
import { alpha, resolveColor } from './core/palette'
|
|
16
|
+
import { chartUid } from './core/uid'
|
|
18
17
|
import { useChartAnim } from './core/useChartAnim'
|
|
19
|
-
import
|
|
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:
|
|
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 (
|
|
118
|
+
// ── Hover crosshair + shared tooltip (prefer="top", pinned) ───────────────────
|
|
116
119
|
const hoverIdx = ref<number | null>(null)
|
|
117
|
-
const
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|