@bagelink/vue 1.15.63 → 1.15.65
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/AccordionItem.vue.d.ts.map +1 -1
- package/dist/components/Avatar.vue.d.ts +6 -1
- package/dist/components/Avatar.vue.d.ts.map +1 -1
- package/dist/components/Badge.vue.d.ts.map +1 -1
- package/dist/components/Card.vue.d.ts +7 -0
- package/dist/components/Card.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/EmptyState.vue.d.ts +43 -0
- package/dist/components/EmptyState.vue.d.ts.map +1 -0
- package/dist/components/Icon/Icon.vue.d.ts +13 -0
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Image.vue.d.ts +26 -1
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/ListItem.vue.d.ts +9 -9
- package/dist/components/ListItem.vue.d.ts.map +1 -1
- package/dist/components/Menu.vue.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +3 -3
- package/dist/components/calendar/CalendarPopover.vue.d.ts +10 -0
- package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
- package/dist/components/charts/BarChart.vue.d.ts +34 -0
- package/dist/components/charts/BarChart.vue.d.ts.map +1 -0
- package/dist/components/charts/ChartTooltip.vue.d.ts +33 -0
- package/dist/components/charts/ChartTooltip.vue.d.ts.map +1 -0
- package/dist/components/charts/Donut.vue.d.ts +53 -0
- package/dist/components/charts/Donut.vue.d.ts.map +1 -0
- package/dist/components/charts/Funnel.vue.d.ts +53 -0
- package/dist/components/charts/Funnel.vue.d.ts.map +1 -0
- package/dist/components/charts/Gauge.vue.d.ts +28 -0
- package/dist/components/charts/Gauge.vue.d.ts.map +1 -0
- package/dist/components/charts/LineChart.vue.d.ts +37 -0
- package/dist/components/charts/LineChart.vue.d.ts.map +1 -0
- package/dist/components/charts/RadialBars.vue.d.ts +34 -0
- package/dist/components/charts/RadialBars.vue.d.ts.map +1 -0
- package/dist/components/charts/RankBars.vue.d.ts +27 -0
- package/dist/components/charts/RankBars.vue.d.ts.map +1 -0
- package/dist/components/charts/Sparkline.vue.d.ts +25 -0
- package/dist/components/charts/Sparkline.vue.d.ts.map +1 -0
- package/dist/components/charts/StatCard.vue.d.ts +28 -0
- package/dist/components/charts/StatCard.vue.d.ts.map +1 -0
- package/dist/components/charts/core/data.d.ts +46 -0
- package/dist/components/charts/core/data.d.ts.map +1 -0
- package/dist/components/charts/core/format.d.ts +13 -0
- package/dist/components/charts/core/format.d.ts.map +1 -0
- package/dist/components/charts/core/palette.d.ts +19 -0
- package/dist/components/charts/core/palette.d.ts.map +1 -0
- package/dist/components/charts/core/uid.d.ts +2 -0
- package/dist/components/charts/core/uid.d.ts.map +1 -0
- package/dist/components/charts/core/useChartAnim.d.ts +11 -0
- package/dist/components/charts/core/useChartAnim.d.ts.map +1 -0
- package/dist/components/charts/core/useChartFrame.d.ts +21 -0
- package/dist/components/charts/core/useChartFrame.d.ts.map +1 -0
- package/dist/components/charts/core/useScale.d.ts +16 -0
- package/dist/components/charts/core/useScale.d.ts.map +1 -0
- package/dist/components/charts/index.d.ts +12 -0
- package/dist/components/charts/index.d.ts.map +1 -0
- package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -0
- package/dist/components/form/inputs/RadioGroup.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RangeInput.vue.d.ts +13 -4
- package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/Layout.vue.d.ts +1 -1
- package/dist/components/layout/Layout.vue.d.ts.map +1 -1
- package/dist/components/layout/Panel.vue.d.ts +1 -1
- package/dist/components/layout/Panel.vue.d.ts.map +1 -1
- package/dist/components/layout/Timeline.types.d.ts +9 -0
- package/dist/components/layout/Timeline.types.d.ts.map +1 -0
- package/dist/components/layout/Timeline.vue.d.ts +42 -0
- package/dist/components/layout/Timeline.vue.d.ts.map +1 -0
- package/dist/components/layout/TimelineItem.vue.d.ts +37 -0
- package/dist/components/layout/TimelineItem.vue.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/dialog/Dialog.vue.d.ts +4 -0
- package/dist/dialog/Dialog.vue.d.ts.map +1 -1
- package/dist/index.cjs +110 -116
- package/dist/index.mjs +38059 -37009
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/AccordionItem.vue +24 -22
- package/src/components/Avatar.vue +49 -11
- package/src/components/Badge.vue +4 -7
- package/src/components/Card.vue +32 -2
- package/src/components/Dropdown.vue +14 -3
- package/src/components/EmptyState.vue +91 -0
- package/src/components/Icon/Icon.vue +118 -25
- package/src/components/Image.vue +70 -3
- package/src/components/ListItem.vue +43 -22
- package/src/components/Menu.vue +10 -2
- package/src/components/charts/BarChart.vue +197 -0
- package/src/components/charts/ChartTooltip.vue +74 -0
- package/src/components/charts/Donut.vue +219 -0
- package/src/components/charts/Funnel.vue +377 -0
- package/src/components/charts/Gauge.vue +90 -0
- package/src/components/charts/LineChart.vue +255 -0
- package/src/components/charts/RadialBars.vue +99 -0
- package/src/components/charts/RankBars.vue +72 -0
- package/src/components/charts/Sparkline.vue +90 -0
- package/src/components/charts/StatCard.vue +84 -0
- package/src/components/charts/core/data.ts +95 -0
- package/src/components/charts/core/format.ts +64 -0
- package/src/components/charts/core/palette.ts +52 -0
- package/src/components/charts/core/uid.ts +6 -0
- package/src/components/charts/core/useChartAnim.ts +60 -0
- package/src/components/charts/core/useChartFrame.ts +49 -0
- package/src/components/charts/core/useScale.ts +39 -0
- package/src/components/charts/index.ts +12 -0
- package/src/components/form/inputs/RadioGroup.vue +2 -1
- package/src/components/form/inputs/RangeInput.vue +43 -15
- package/src/components/form/inputs/SelectInput.vue +1 -19
- package/src/components/index.ts +3 -1
- package/src/components/layout/Timeline.types.ts +9 -0
- package/src/components/layout/Timeline.vue +54 -0
- package/src/components/layout/TimelineItem.vue +93 -0
- package/src/components/layout/index.ts +3 -0
- package/src/dialog/Dialog.vue +29 -1
- package/src/styles/bagel.css +1 -0
- package/src/styles/gradients.css +181 -0
- package/src/styles/layout.css +9 -0
- package/src/styles/theme.css +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +0 -47
- package/dist/components/analytics/BarChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/KpiCard.vue.d.ts +0 -24
- package/dist/components/analytics/KpiCard.vue.d.ts.map +0 -1
- package/dist/components/analytics/LineChart.vue.d.ts +0 -35
- package/dist/components/analytics/LineChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/PieChart.vue.d.ts +0 -53
- package/dist/components/analytics/PieChart.vue.d.ts.map +0 -1
- package/dist/components/analytics/index.d.ts +0 -5
- package/dist/components/analytics/index.d.ts.map +0 -1
- package/src/components/analytics/BarChart.vue +0 -262
- package/src/components/analytics/KpiCard.vue +0 -84
- package/src/components/analytics/LineChart.vue +0 -357
- package/src/components/analytics/PieChart.vue +0 -544
- package/src/components/analytics/index.ts +0 -4
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* LineChart — line + optional area fill, single or multi-series, theme-driven.
|
|
4
|
+
* Renders JUST the plot (no title/card chrome) so you can drop it in any Card.
|
|
5
|
+
*
|
|
6
|
+
* <LineChart :data="[5,8,6,12]" />
|
|
7
|
+
* <LineChart :data="revenue" color="green" area :currency="'USD'" />
|
|
8
|
+
* <LineChart :series="[{ name:'2024', data:[…] }, { name:'2025', data:[…] }]" />
|
|
9
|
+
*/
|
|
10
|
+
import { computed, ref } from 'vue'
|
|
11
|
+
import { chartUid } from './core/uid'
|
|
12
|
+
import type { RawPoint, RawSeries } from './core/data'
|
|
13
|
+
import { allValues, normalizeSeries, sharedLabels } from './core/data'
|
|
14
|
+
import { alpha, resolveColor } from './core/palette'
|
|
15
|
+
import { formatLabel, formatValue, type ValueFormat } from './core/format'
|
|
16
|
+
import { linear, niceMax, ticks } from './core/useScale'
|
|
17
|
+
import { useChartFrame } from './core/useChartFrame'
|
|
18
|
+
import { useChartAnim } from './core/useChartAnim'
|
|
19
|
+
import ChartTooltip from './ChartTooltip.vue'
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<{
|
|
22
|
+
data?: RawPoint[]
|
|
23
|
+
series?: RawSeries[]
|
|
24
|
+
labels?: (string | number)[]
|
|
25
|
+
color?: string
|
|
26
|
+
/** Fill area under each line. */
|
|
27
|
+
area?: boolean
|
|
28
|
+
/** Use a soft top→bottom gradient for area fills (premium look).
|
|
29
|
+
Implies `area`. On by default when `area`/`stacked` is set. */
|
|
30
|
+
gradient?: boolean
|
|
31
|
+
/** Stack multi-series areas (implies `area`). Composition over time. */
|
|
32
|
+
stacked?: boolean
|
|
33
|
+
height?: number
|
|
34
|
+
/** Show horizontal grid lines + y ticks. */
|
|
35
|
+
grid?: boolean
|
|
36
|
+
/** Show data-point dots. */
|
|
37
|
+
points?: boolean
|
|
38
|
+
/** Currency code → formats values as currency. */
|
|
39
|
+
currency?: string
|
|
40
|
+
prefix?: string
|
|
41
|
+
suffix?: string
|
|
42
|
+
/** Max x-axis labels to show (avoids crowding). */
|
|
43
|
+
maxLabels?: number
|
|
44
|
+
animated?: boolean
|
|
45
|
+
}>(), {
|
|
46
|
+
height: 220,
|
|
47
|
+
grid: true,
|
|
48
|
+
points: false,
|
|
49
|
+
maxLabels: 7,
|
|
50
|
+
animated: true,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const el = ref<HTMLElement>()
|
|
54
|
+
const heightRef = computed(() => props.height)
|
|
55
|
+
const { width, height: plotH, pad, innerWidth, innerHeight, flipX, isRTL } = useChartFrame(el, { height: heightRef })
|
|
56
|
+
const { progress } = useChartAnim({ el, enabled: props.animated, duration: 800 })
|
|
57
|
+
|
|
58
|
+
const series = computed(() => normalizeSeries(props.data, props.series, { labels: props.labels, defaultColor: props.color }))
|
|
59
|
+
const cats = computed(() => sharedLabels(series.value))
|
|
60
|
+
|
|
61
|
+
const valueFmt = computed<ValueFormat>(() => ({ currency: props.currency, prefix: props.prefix, suffix: props.suffix }))
|
|
62
|
+
const axisFmt = computed<ValueFormat>(() => ({ ...valueFmt.value, compact: true }))
|
|
63
|
+
|
|
64
|
+
const isStacked = computed(() => props.stacked && series.value.length > 1)
|
|
65
|
+
|
|
66
|
+
// Per-label cumulative totals for the stacked domain.
|
|
67
|
+
const stackTotals = computed(() => cats.value.map((_, i) => series.value.reduce((sum, s) => sum + (s.points[i]?.y ?? 0), 0)))
|
|
68
|
+
|
|
69
|
+
const yMax = computed(() => niceMax(isStacked.value ? Math.max(0, ...stackTotals.value) : Math.max(0, ...allValues(series.value))))
|
|
70
|
+
const yMin = computed(() => (isStacked.value ? 0 : Math.min(0, ...allValues(series.value))))
|
|
71
|
+
|
|
72
|
+
const sx = computed(() => linear(0, Math.max(1, cats.value.length - 1), 0, innerWidth.value))
|
|
73
|
+
const sy = computed(() => linear(yMin.value, yMax.value, innerHeight.value, 0))
|
|
74
|
+
|
|
75
|
+
const yTicks = computed(() => (props.grid ? ticks(yMin.value, yMax.value, 4) : []))
|
|
76
|
+
|
|
77
|
+
const lines = computed(() => {
|
|
78
|
+
const base = innerHeight.value
|
|
79
|
+
// Running cumulative per label for stacking.
|
|
80
|
+
const cum = cats.value.map(() => 0)
|
|
81
|
+
return series.value.map((s, i) => {
|
|
82
|
+
const color = resolveColor(s.color, i)
|
|
83
|
+
if (isStacked.value) {
|
|
84
|
+
const lower = cats.value.map((_, idx) => cum[idx])
|
|
85
|
+
const upper = cats.value.map((_, idx) => cum[idx] + (s.points[idx]?.y ?? 0))
|
|
86
|
+
upper.forEach((v, idx) => { cum[idx] = v })
|
|
87
|
+
const topCoords = upper.map((v, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(v), p: s.points[idx] ?? { label: cats.value[idx], y: v } }))
|
|
88
|
+
const line = topCoords.map((c, idx) => `${idx === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
|
|
89
|
+
const lowerPts = lower.map((v, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(v) }))
|
|
90
|
+
const down = [...lowerPts].reverse().map(c => `L ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
|
|
91
|
+
const areaPath = topCoords.length ? `${line} ${down} Z` : ''
|
|
92
|
+
return { name: s.name, color, coords: topCoords, line, areaPath }
|
|
93
|
+
}
|
|
94
|
+
const coords = s.points.map((p, idx) => ({ x: flipX(sx.value(idx)), y: sy.value(p.y), p }))
|
|
95
|
+
const line = coords.map((c, idx) => `${idx === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
|
|
96
|
+
const areaPath = coords.length
|
|
97
|
+
? `${line} L ${coords[coords.length - 1].x.toFixed(1)} ${base} L ${coords[0].x.toFixed(1)} ${base} Z`
|
|
98
|
+
: ''
|
|
99
|
+
return { name: s.name, color, coords, line, areaPath }
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// In stacked mode the area is always shown (it's the whole point).
|
|
104
|
+
const showArea = computed(() => props.area || isStacked.value)
|
|
105
|
+
// Gradient fills default ON whenever an area is shown, unless explicitly off.
|
|
106
|
+
const useGradient = computed(() => showArea.value && props.gradient !== false)
|
|
107
|
+
|
|
108
|
+
const uid = chartUid()
|
|
109
|
+
const gradId = (i: number) => `${uid}-grad-${i}`
|
|
110
|
+
|
|
111
|
+
const labelStride = computed(() => Math.max(1, Math.ceil(cats.value.length / props.maxLabels)))
|
|
112
|
+
const dashLength = computed(() => (innerWidth.value + innerHeight.value) * 2)
|
|
113
|
+
const dashOffset = computed(() => dashLength.value * (1 - progress.value))
|
|
114
|
+
|
|
115
|
+
// ── Hover crosshair + shared tooltip (follows the mouse across the plot) ──────
|
|
116
|
+
const hoverIdx = ref<number | null>(null)
|
|
117
|
+
const mouseX = ref(0)
|
|
118
|
+
|
|
119
|
+
function onMove(e: MouseEvent) {
|
|
120
|
+
const host = el.value
|
|
121
|
+
if (!host || !cats.value.length) return
|
|
122
|
+
const rect = host.getBoundingClientRect()
|
|
123
|
+
// Mouse x within the plot area (account for left padding + RTL flip).
|
|
124
|
+
let local = e.clientX - rect.left - pad.value.left
|
|
125
|
+
if (isRTL.value) local = innerWidth.value - local
|
|
126
|
+
const idx = Math.round((local / Math.max(1, innerWidth.value)) * (cats.value.length - 1))
|
|
127
|
+
hoverIdx.value = Math.min(cats.value.length - 1, Math.max(0, idx))
|
|
128
|
+
mouseX.value = e.clientX - rect.left
|
|
129
|
+
}
|
|
130
|
+
function onLeave() { hoverIdx.value = null }
|
|
131
|
+
|
|
132
|
+
const hoverX = computed(() => (hoverIdx.value == null ? 0 : flipX(sx.value(hoverIdx.value))))
|
|
133
|
+
|
|
134
|
+
// Markers (one per series) at the hovered index.
|
|
135
|
+
const hoverMarkers = computed(() => {
|
|
136
|
+
if (hoverIdx.value == null) return []
|
|
137
|
+
return lines.value
|
|
138
|
+
.map(l => l.coords[hoverIdx.value as number])
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.map((c, i) => ({ x: c.x, y: c.y, color: lines.value[i].color }))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Tooltip rows: every series' value at the hovered label.
|
|
144
|
+
const hoverRows = computed(() => {
|
|
145
|
+
if (hoverIdx.value == null) return []
|
|
146
|
+
return series.value.map((s, i) => ({
|
|
147
|
+
name: s.name,
|
|
148
|
+
color: resolveColor(s.color, i),
|
|
149
|
+
value: formatValue(s.points[hoverIdx.value as number]?.y ?? 0, valueFmt.value),
|
|
150
|
+
}))
|
|
151
|
+
})
|
|
152
|
+
const hoverLabel = computed(() => (hoverIdx.value == null ? '' : formatLabel(cats.value[hoverIdx.value])))
|
|
153
|
+
|
|
154
|
+
// Total row for stacked charts.
|
|
155
|
+
const hoverTotal = computed(() => {
|
|
156
|
+
if (hoverIdx.value == null || !isStacked.value) return null
|
|
157
|
+
const sum = series.value.reduce((s, srs) => s + (srs.points[hoverIdx.value as number]?.y ?? 0), 0)
|
|
158
|
+
return formatValue(sum, valueFmt.value)
|
|
159
|
+
})
|
|
160
|
+
|
|
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
|
+
})
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<template>
|
|
170
|
+
<div ref="el" class="bgl-chart w-100p relative" :style="{ height: `${plotH}px` }" @mousemove="onMove" @mouseleave="onLeave">
|
|
171
|
+
<svg v-if="width" :width="width" :height="plotH" class="display-block overflow-hidden">
|
|
172
|
+
<defs v-if="useGradient">
|
|
173
|
+
<linearGradient
|
|
174
|
+
v-for="(l, i) in lines" :id="gradId(i)" :key="i"
|
|
175
|
+
gradientUnits="userSpaceOnUse" :x1="0" :y1="0" :x2="0" :y2="innerHeight"
|
|
176
|
+
>
|
|
177
|
+
<stop offset="0%" :stop-color="l.color" :stop-opacity="isStacked ? 0.9 : 0.32" />
|
|
178
|
+
<stop offset="100%" :stop-color="l.color" :stop-opacity="isStacked ? 0.15 : 0.02" />
|
|
179
|
+
</linearGradient>
|
|
180
|
+
</defs>
|
|
181
|
+
<g :transform="`translate(${pad.left}, ${pad.top})`">
|
|
182
|
+
<!-- grid + y ticks -->
|
|
183
|
+
<g v-if="grid" class="bgl-chart__grid">
|
|
184
|
+
<g v-for="(t, i) in yTicks" :key="i">
|
|
185
|
+
<line :x1="0" :x2="innerWidth" :y1="sy(t)" :y2="sy(t)" />
|
|
186
|
+
<text class="bgl-chart__ytick" :x="-8" :y="sy(t) + 3">{{ formatValue(t, axisFmt) }}</text>
|
|
187
|
+
</g>
|
|
188
|
+
</g>
|
|
189
|
+
|
|
190
|
+
<!-- area fills (gradient by default; stacked layers read as bands) -->
|
|
191
|
+
<template v-if="showArea">
|
|
192
|
+
<path
|
|
193
|
+
v-for="(l, i) in lines" :key="`a${i}`" :d="l.areaPath"
|
|
194
|
+
:fill="useGradient ? `url(#${gradId(i)})` : alpha(l.color, isStacked ? 78 : 14)"
|
|
195
|
+
stroke="none" :opacity="progress"
|
|
196
|
+
/>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<!-- lines -->
|
|
200
|
+
<path
|
|
201
|
+
v-for="(l, i) in lines" :key="`l${i}`" :d="l.line" :stroke="l.color" stroke-width="2.5"
|
|
202
|
+
fill="none" stroke-linecap="round" stroke-linejoin="round"
|
|
203
|
+
:stroke-dasharray="dashLength" :stroke-dashoffset="dashOffset"
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
<!-- points (values shown via the shared hover tooltip) -->
|
|
207
|
+
<template v-if="points">
|
|
208
|
+
<g v-for="(l, i) in lines" :key="`p${i}`">
|
|
209
|
+
<circle
|
|
210
|
+
v-for="(c, idx) in l.coords" :key="idx"
|
|
211
|
+
:cx="c.x" :cy="c.y" r="3.5" :fill="l.color" class="bgl-chart__pt" :opacity="progress"
|
|
212
|
+
/>
|
|
213
|
+
</g>
|
|
214
|
+
</template>
|
|
215
|
+
|
|
216
|
+
<!-- x labels -->
|
|
217
|
+
<g class="bgl-chart__xlabels">
|
|
218
|
+
<text
|
|
219
|
+
v-for="(lbl, i) in cats" v-show="i % labelStride === 0" :key="i"
|
|
220
|
+
:x="flipX(sx(i))" :y="innerHeight + 16" text-anchor="middle"
|
|
221
|
+
>{{ formatLabel(lbl) }}</text>
|
|
222
|
+
</g>
|
|
223
|
+
|
|
224
|
+
<!-- hover crosshair + per-series markers -->
|
|
225
|
+
<g v-if="hoverIdx != null" class="bgl-chart__hover" pointer-events="none">
|
|
226
|
+
<line :x1="hoverX" :x2="hoverX" :y1="0" :y2="innerHeight" class="bgl-chart__crosshair" />
|
|
227
|
+
<circle v-for="(m, i) in hoverMarkers" :key="i" :cx="m.x" :cy="m.y" r="4" :fill="m.color" class="bgl-chart__hoverpt" />
|
|
228
|
+
</g>
|
|
229
|
+
</g>
|
|
230
|
+
</svg>
|
|
231
|
+
|
|
232
|
+
<!-- shared tooltip following the mouse -->
|
|
233
|
+
<ChartTooltip
|
|
234
|
+
v-if="hoverIdx != null" :style="tooltipStyle"
|
|
235
|
+
:label="hoverLabel" :rows="hoverRows" :total="hoverTotal"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
</template>
|
|
239
|
+
|
|
240
|
+
<style scoped>
|
|
241
|
+
.bgl-chart { direction: inherit; }
|
|
242
|
+
.bgl-chart__grid line { stroke: var(--bgl-border-color); stroke-width: 1; }
|
|
243
|
+
.bgl-chart__ytick,
|
|
244
|
+
.bgl-chart__xlabels text { fill: var(--bgl-gray); font-size: 11px; }
|
|
245
|
+
.bgl-chart__ytick { text-anchor: end; }
|
|
246
|
+
.bgl-chart__pt { cursor: pointer; transition: r 0.15s ease; outline: none; }
|
|
247
|
+
.bgl-chart__pt:hover { r: 5; }
|
|
248
|
+
.bgl-chart__pt:focus,
|
|
249
|
+
.bgl-chart__pt:focus-visible { outline: none; }
|
|
250
|
+
[dir="rtl"] .bgl-chart__ytick { text-anchor: start; }
|
|
251
|
+
|
|
252
|
+
/* hover crosshair + markers */
|
|
253
|
+
.bgl-chart__crosshair { stroke: var(--bgl-border-color); stroke-width: 1; stroke-dasharray: 3 3; }
|
|
254
|
+
.bgl-chart__hoverpt { stroke: var(--bgl-box-bg); stroke-width: 2; }
|
|
255
|
+
</style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* RadialBars — concentric progress rings (the "activity rings" look). Each item
|
|
4
|
+
* is a value 0–max rendered as a ring; great for multi-KPI completion tiles.
|
|
5
|
+
*
|
|
6
|
+
* <RadialBars :data="[{label:'Sales',value:82},{label:'Leads',value:64},{label:'Churn',value:23}]" />
|
|
7
|
+
* <RadialBars :data="rings" :max="100" :legend="false" />
|
|
8
|
+
*/
|
|
9
|
+
import { computed, ref } from 'vue'
|
|
10
|
+
import { alpha, resolveColor } from './core/palette'
|
|
11
|
+
import { useChartAnim } from './core/useChartAnim'
|
|
12
|
+
import type { ThemeType } from '../../types'
|
|
13
|
+
|
|
14
|
+
interface Ring { label: string; value: number; color?: ThemeType | string; max?: number }
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
data: Ring[]
|
|
18
|
+
/** Shared max when a ring doesn't set its own. */
|
|
19
|
+
max?: number
|
|
20
|
+
size?: number
|
|
21
|
+
/** Ring thickness in px. */
|
|
22
|
+
thickness?: number
|
|
23
|
+
/** Gap between rings in px. */
|
|
24
|
+
gap?: number
|
|
25
|
+
legend?: boolean
|
|
26
|
+
/** Show the value of each ring in the legend. Default %. */
|
|
27
|
+
suffix?: string
|
|
28
|
+
animated?: boolean
|
|
29
|
+
}>(), {
|
|
30
|
+
max: 100,
|
|
31
|
+
size: 180,
|
|
32
|
+
thickness: 14,
|
|
33
|
+
gap: 6,
|
|
34
|
+
legend: true,
|
|
35
|
+
suffix: '%',
|
|
36
|
+
animated: true,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const el = ref<HTMLElement>()
|
|
40
|
+
const { progress } = useChartAnim({ el, enabled: props.animated, duration: 900 })
|
|
41
|
+
|
|
42
|
+
const cx = computed(() => props.size / 2)
|
|
43
|
+
|
|
44
|
+
function ringPath(r: number): string {
|
|
45
|
+
const c = cx.value
|
|
46
|
+
// Full-circle path (two arcs) starting at 12 o'clock.
|
|
47
|
+
return `M ${c} ${c - r} A ${r} ${r} 0 1 1 ${c - 0.001} ${c - r} Z`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Arc { path: string; color: string; label: string; display: string; circ: number; filled: number }
|
|
51
|
+
const arcs = computed<Arc[]>(() =>
|
|
52
|
+
props.data.map((d, i) => {
|
|
53
|
+
const r = cx.value - props.thickness / 2 - i * (props.thickness + props.gap)
|
|
54
|
+
const max = d.max ?? props.max
|
|
55
|
+
const frac = Math.min(1, Math.max(0, d.value / (max || 1)))
|
|
56
|
+
const circ = 2 * Math.PI * r
|
|
57
|
+
return {
|
|
58
|
+
path: ringPath(r),
|
|
59
|
+
color: resolveColor(d.color, i),
|
|
60
|
+
label: d.label,
|
|
61
|
+
display: `${d.value}${props.suffix}`,
|
|
62
|
+
circ,
|
|
63
|
+
filled: circ * frac * progress.value,
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div
|
|
71
|
+
ref="el" class="bgl-radial inline-flex gap-1"
|
|
72
|
+
:class="legend ? 'row' : 'column'"
|
|
73
|
+
>
|
|
74
|
+
<svg :width="size" :height="size" :viewBox="`0 0 ${size} ${size}`" class="bgl-radial__svg flex-shrink-0">
|
|
75
|
+
<g v-for="(a, i) in arcs" :key="i">
|
|
76
|
+
<path :d="a.path" :stroke="alpha(a.color, 14)" :stroke-width="thickness" fill="none" stroke-linecap="round" />
|
|
77
|
+
<path
|
|
78
|
+
:d="a.path" :stroke="a.color" :stroke-width="thickness" fill="none" stroke-linecap="round"
|
|
79
|
+
:stroke-dasharray="`${a.filled} ${a.circ}`"
|
|
80
|
+
:transform="`rotate(-90 ${cx} ${cx})`"
|
|
81
|
+
/>
|
|
82
|
+
</g>
|
|
83
|
+
</svg>
|
|
84
|
+
|
|
85
|
+
<ul v-if="legend" class="bgl-radial__legend grid gap-05 m-0 min-w-0">
|
|
86
|
+
<li v-for="(a, i) in arcs" :key="i" class="flex align-items-center gap-05 txt13">
|
|
87
|
+
<span class="bgl-radial__dot flex-shrink-0" :style="{ background: a.color }" />
|
|
88
|
+
<span>{{ a.label }}</span>
|
|
89
|
+
<span class="bgl-radial__val semibold tabular-nums ms-auto">{{ a.display }}</span>
|
|
90
|
+
</li>
|
|
91
|
+
</ul>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.bgl-radial__svg path { transition: stroke-dasharray 0.3s ease; }
|
|
97
|
+
.bgl-radial__legend { list-style: none; padding: 0; }
|
|
98
|
+
.bgl-radial__dot { width: 10px; height: 10px; border-radius: 3px; }
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* RankBars — horizontal ranked bars for top-N lists (leaderboards, sources by
|
|
4
|
+
* revenue, most-viewed). Pure CSS bars: label + bar + value per row.
|
|
5
|
+
*
|
|
6
|
+
* <RankBars :data="[{label:'Google',value:4200},{label:'Direct',value:3100}]" />
|
|
7
|
+
* <RankBars :data="rows" color="purple" currency="USD" :max-rows="5" />
|
|
8
|
+
*/
|
|
9
|
+
import { computed } from 'vue'
|
|
10
|
+
import { ramp, resolveColor } from './core/palette'
|
|
11
|
+
import { formatValue, type ValueFormat } from './core/format'
|
|
12
|
+
import type { ThemeType } from '../../types'
|
|
13
|
+
|
|
14
|
+
interface Row { label: string; value: number; color?: ThemeType | string }
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
data: Row[]
|
|
18
|
+
/** Base tone for bars (monochrome shade ramp by rank). */
|
|
19
|
+
color?: ThemeType | string
|
|
20
|
+
/** One tone per row instead of shades. Per-row `color` always wins. */
|
|
21
|
+
mode?: 'shades' | 'multi'
|
|
22
|
+
/** Cap rows shown (already-sorted input recommended). */
|
|
23
|
+
maxRows?: number
|
|
24
|
+
/** Sort descending by value before rendering. */
|
|
25
|
+
sort?: boolean
|
|
26
|
+
currency?: string
|
|
27
|
+
prefix?: string
|
|
28
|
+
suffix?: string
|
|
29
|
+
}>(), {
|
|
30
|
+
color: 'primary',
|
|
31
|
+
mode: 'shades',
|
|
32
|
+
sort: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const fmt = computed<ValueFormat>(() => ({ currency: props.currency, prefix: props.prefix, suffix: props.suffix }))
|
|
36
|
+
|
|
37
|
+
const rows = computed(() => {
|
|
38
|
+
let r = [...props.data]
|
|
39
|
+
if (props.sort) r.sort((a, b) => b.value - a.value)
|
|
40
|
+
if (props.maxRows) r = r.slice(0, props.maxRows)
|
|
41
|
+
const max = Math.max(1, ...r.map(d => d.value))
|
|
42
|
+
return r.map((d, i) => ({
|
|
43
|
+
label: d.label,
|
|
44
|
+
value: d.value,
|
|
45
|
+
display: formatValue(d.value, fmt.value),
|
|
46
|
+
pct: (d.value / max) * 100,
|
|
47
|
+
color: d.color
|
|
48
|
+
? resolveColor(d.color)
|
|
49
|
+
: props.mode === 'multi'
|
|
50
|
+
? resolveColor(undefined, i)
|
|
51
|
+
: ramp(props.color, r.length, i),
|
|
52
|
+
}))
|
|
53
|
+
})
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div class="bgl-rank grid w-100p gap-05">
|
|
58
|
+
<div v-for="(r, i) in rows" :key="i" class="bgl-rank__row align-items-center grid gap-075">
|
|
59
|
+
<span class="bgl-rank__label ellipsis-1 txt14" :title="r.label">{{ r.label }}</span>
|
|
60
|
+
<span class="bgl-rank__track overflow-hidden round">
|
|
61
|
+
<span class="bgl-rank__bar display-block round" :style="{ width: `${r.pct}%`, background: r.color }" />
|
|
62
|
+
</span>
|
|
63
|
+
<span class="txt13 tabular-nums semibold">{{ r.display }}</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.bgl-rank__row { grid-template-columns: minmax(60px, 28%) 1fr auto; }
|
|
70
|
+
.bgl-rank__track { height: 10px; background: var(--bgl-border-color); }
|
|
71
|
+
.bgl-rank__bar { height: 100%; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
|
|
72
|
+
</style>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Sparkline — a tiny inline line/area chart with no axes or labels. Perfect for
|
|
4
|
+
* cards, table cells, and stat tiles.
|
|
5
|
+
*
|
|
6
|
+
* <Sparkline :data="[5,8,6,12,9,14]" />
|
|
7
|
+
* <Sparkline :data="trend" color="green" area />
|
|
8
|
+
*/
|
|
9
|
+
import { computed, ref } from 'vue'
|
|
10
|
+
import type { RawPoint } from './core/data'
|
|
11
|
+
import { normalizeSeries } from './core/data'
|
|
12
|
+
import { alpha, resolveColor } from './core/palette'
|
|
13
|
+
import { linear } from './core/useScale'
|
|
14
|
+
import { useChartAnim } from './core/useChartAnim'
|
|
15
|
+
import { chartUid } from './core/uid'
|
|
16
|
+
|
|
17
|
+
defineOptions({ name: 'BglSparkline' })
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
data: RawPoint[]
|
|
21
|
+
color?: string
|
|
22
|
+
/** Fill the area under the line. */
|
|
23
|
+
area?: boolean
|
|
24
|
+
/** Use a soft gradient for the area fill (default on when `area`). */
|
|
25
|
+
gradient?: boolean
|
|
26
|
+
width?: number
|
|
27
|
+
height?: number
|
|
28
|
+
strokeWidth?: number
|
|
29
|
+
/** Draw a dot on the last point. */
|
|
30
|
+
marker?: boolean
|
|
31
|
+
animated?: boolean
|
|
32
|
+
}>(), {
|
|
33
|
+
width: 100,
|
|
34
|
+
height: 28,
|
|
35
|
+
strokeWidth: 2,
|
|
36
|
+
animated: true,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const el = ref<HTMLElement>()
|
|
40
|
+
const { progress } = useChartAnim({ el, enabled: props.animated, duration: 600 })
|
|
41
|
+
|
|
42
|
+
const stroke = computed(() => resolveColor(props.color))
|
|
43
|
+
const points = computed(() => normalizeSeries(props.data, undefined)[0]?.points ?? [])
|
|
44
|
+
const useGradient = computed(() => props.area && props.gradient !== false)
|
|
45
|
+
const gradId = chartUid('spark') + '-grad'
|
|
46
|
+
|
|
47
|
+
const pad = computed(() => props.strokeWidth + (props.marker ? 2 : 0))
|
|
48
|
+
const geom = computed(() => {
|
|
49
|
+
const pts = points.value
|
|
50
|
+
if (pts.length < 2) return { line: '', area: '', last: null as null | { x: number; y: number } }
|
|
51
|
+
const ys = pts.map(p => p.y)
|
|
52
|
+
const min = Math.min(...ys)
|
|
53
|
+
const max = Math.max(...ys)
|
|
54
|
+
const sx = linear(0, pts.length - 1, pad.value, props.width - pad.value)
|
|
55
|
+
const sy = linear(min, max, props.height - pad.value, pad.value)
|
|
56
|
+
const coords = pts.map((p, i) => ({ x: sx(i), y: sy(p.y) }))
|
|
57
|
+
const line = coords.map((c, i) => `${i === 0 ? 'M' : 'L'} ${c.x.toFixed(1)} ${c.y.toFixed(1)}`).join(' ')
|
|
58
|
+
const area = `${line} L ${coords[coords.length - 1].x.toFixed(1)} ${props.height - pad.value} L ${coords[0].x.toFixed(1)} ${props.height - pad.value} Z`
|
|
59
|
+
return { line, area, last: coords[coords.length - 1] }
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Animate by revealing the stroke via dash offset.
|
|
63
|
+
const dashLength = computed(() => props.width * 2)
|
|
64
|
+
const dashOffset = computed(() => dashLength.value * (1 - progress.value))
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<template>
|
|
68
|
+
<span ref="el" class="bgl-sparkline inline-block line-height-0 vertical-align-middle" :style="{ width: `${width}px`, height: `${height}px` }">
|
|
69
|
+
<svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" preserveAspectRatio="none" class="bgl-sparkline__svg display-block">
|
|
70
|
+
<defs v-if="useGradient">
|
|
71
|
+
<linearGradient :id="gradId" x1="0" y1="0" x2="0" y2="1">
|
|
72
|
+
<stop offset="0%" :stop-color="stroke" stop-opacity="0.35" />
|
|
73
|
+
<stop offset="100%" :stop-color="stroke" stop-opacity="0.02" />
|
|
74
|
+
</linearGradient>
|
|
75
|
+
</defs>
|
|
76
|
+
<path v-if="area && geom.area" :d="geom.area" :fill="useGradient ? `url(#${gradId})` : alpha(stroke, 14)" stroke="none" :opacity="progress" />
|
|
77
|
+
<path
|
|
78
|
+
:d="geom.line" :stroke="stroke" :stroke-width="strokeWidth" fill="none"
|
|
79
|
+
stroke-linecap="round" stroke-linejoin="round"
|
|
80
|
+
:stroke-dasharray="dashLength" :stroke-dashoffset="dashOffset"
|
|
81
|
+
/>
|
|
82
|
+
<circle v-if="marker && geom.last" :cx="geom.last.x" :cy="geom.last.y" :r="strokeWidth + 0.5" :fill="stroke" :opacity="progress" />
|
|
83
|
+
</svg>
|
|
84
|
+
</span>
|
|
85
|
+
</template>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
/* Only the SVG overflow has no utility equivalent. */
|
|
89
|
+
.bgl-sparkline__svg { overflow: visible; }
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* StatCard — a KPI tile: label, big value, delta chip, and an optional trend
|
|
4
|
+
* sparkline. Composes Card + Sparkline; all formatting is locale-aware.
|
|
5
|
+
*
|
|
6
|
+
* <StatCard label="Revenue" :value="48200" currency="USD" :change="12.4" :trend="[…]" />
|
|
7
|
+
* <StatCard label="Active users" :value="1280" :change="-3.1" icon="group" color="purple" />
|
|
8
|
+
*/
|
|
9
|
+
import { computed } from 'vue'
|
|
10
|
+
import Card from '../Card.vue'
|
|
11
|
+
import Icon from '../Icon/Icon.vue'
|
|
12
|
+
import Sparkline from './Sparkline.vue'
|
|
13
|
+
import type { RawPoint } from './core/data'
|
|
14
|
+
import { formatValue } from './core/format'
|
|
15
|
+
import { resolveColor } from './core/palette'
|
|
16
|
+
import type { ThemeType } from '../../types'
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<{
|
|
19
|
+
label: string
|
|
20
|
+
value: number | string
|
|
21
|
+
icon?: string
|
|
22
|
+
color?: ThemeType | string
|
|
23
|
+
/** Percentage change; sign drives the up/down chip color. */
|
|
24
|
+
change?: number
|
|
25
|
+
/** Invert chip colors when down is good (e.g. churn, cost). */
|
|
26
|
+
invertChange?: boolean
|
|
27
|
+
/** Sparkline data for a trend line. */
|
|
28
|
+
trend?: RawPoint[]
|
|
29
|
+
currency?: string
|
|
30
|
+
prefix?: string
|
|
31
|
+
suffix?: string
|
|
32
|
+
subtitle?: string
|
|
33
|
+
loading?: boolean
|
|
34
|
+
/** Outlined card (transparent bg + border), like `<Card outline>`. */
|
|
35
|
+
outline?: boolean
|
|
36
|
+
/** Hairline framed card, like `<Card frame>`. */
|
|
37
|
+
frame?: boolean
|
|
38
|
+
}>(), {
|
|
39
|
+
color: 'primary',
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const accent = computed(() => resolveColor(props.color))
|
|
43
|
+
const formatted = computed(() =>
|
|
44
|
+
typeof props.value === 'number'
|
|
45
|
+
? formatValue(props.value, { currency: props.currency, prefix: props.prefix, suffix: props.suffix })
|
|
46
|
+
: `${props.prefix ?? ''}${props.value}${props.suffix ?? ''}`,
|
|
47
|
+
)
|
|
48
|
+
const hasChange = computed(() => props.change !== undefined && props.change !== 0)
|
|
49
|
+
const up = computed(() => (props.change ?? 0) > 0)
|
|
50
|
+
const good = computed(() => (props.invertChange ? !up.value : up.value))
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<Card thin :outline="outline" :frame="frame" class="bgl-stat flex column gap-1">
|
|
55
|
+
<div class="flex space-between w-100p align-items-start gap-05">
|
|
56
|
+
<div class="flex gap-05 align-items-center min-w-0">
|
|
57
|
+
<span v-if="icon" class="bgl-stat__icon grid place-items-center flex-shrink-0" :style="{ color: accent, background: `color-mix(in srgb, ${accent} 12%, transparent)` }">
|
|
58
|
+
<Icon :name="icon" :size="1.1" />
|
|
59
|
+
</span>
|
|
60
|
+
<div class="min-w-0">
|
|
61
|
+
<p class="txt13 opacity-6 m-0 ellipsis-1">{{ label }}</p>
|
|
62
|
+
<p v-if="subtitle" class="txt12 color-gray m-0 ellipsis-1">{{ subtitle }}</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<span v-if="hasChange" class="bgl-stat__delta inline-flex align-items-center flex-shrink-0 semibold" :class="good ? 'bgl-stat__delta--up' : 'bgl-stat__delta--down'">
|
|
66
|
+
<Icon :name="up ? 'trending_up' : 'trending_down'" :size="0.9" />
|
|
67
|
+
{{ Math.abs(change!).toFixed(1) }}%
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="flex space-between w-100p align-items-end gap-1">
|
|
72
|
+
<span class="txt28 m_txt24 semibold line-height-1 tabular-nums" :class="{ 'opacity-4': loading }">{{ loading ? '…' : formatted }}</span>
|
|
73
|
+
<Sparkline v-if="trend?.length" :data="trend" :color="color" area class="flex-shrink-0 opacity-9" :width="96" :height="32" />
|
|
74
|
+
</div>
|
|
75
|
+
</Card>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<style scoped>
|
|
79
|
+
.bgl-stat__icon { width: 34px; height: 34px; border-radius: 9px; }
|
|
80
|
+
/* Delta chip: layout via utilities; pill shape + sizing stay here. */
|
|
81
|
+
.bgl-stat__delta { gap: 0.15rem; font-size: 0.78rem; padding: 0.1rem 0.4rem; border-radius: 999px; }
|
|
82
|
+
.bgl-stat__delta--up { color: var(--bgl-green); background: color-mix(in srgb, var(--bgl-green) 12%, transparent); }
|
|
83
|
+
.bgl-stat__delta--down { color: var(--bgl-red); background: color-mix(in srgb, var(--bgl-red) 12%, transparent); }
|
|
84
|
+
</style>
|