@katlux/block-charts 0.1.0-beta.0
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/build.config.ts +4 -0
- package/package.json +31 -0
- package/src/module.ts +25 -0
- package/src/runtime/components/KAreaChart/KAreaChart.vue +410 -0
- package/src/runtime/components/KBarChart/KBarChart.vue +427 -0
- package/src/runtime/components/KHeatMap/KHeatMap.vue +301 -0
- package/src/runtime/components/KLineChart/KLineChart.vue +493 -0
- package/src/runtime/components/KPieChart/KPieChart.vue +307 -0
- package/src/runtime/components/KScatterChart/KScatterChart.vue +375 -0
- package/src/runtime/composables/useChartAnimation.ts +45 -0
- package/src/runtime/composables/useChartAxes.ts +105 -0
- package/src/runtime/composables/useChartCanvas.ts +67 -0
- package/src/runtime/composables/useChartData.ts +79 -0
- package/src/runtime/composables/useChartExport.ts +40 -0
- package/src/runtime/composables/useChartHitTest.ts +71 -0
- package/src/runtime/composables/useChartSvg.ts +45 -0
- package/src/runtime/composables/useChartViewport.ts +140 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.k-chart-wrapper(
|
|
3
|
+
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
4
|
+
:style="{ cursor: isDragging ? 'grabbing' : canPan ? 'grab' : 'default' }"
|
|
5
|
+
@wheel.prevent="onWheel"
|
|
6
|
+
@mousedown="onMouseDown"
|
|
7
|
+
@mousemove="onMouseMove"
|
|
8
|
+
@mouseup="onMouseUp"
|
|
9
|
+
@mouseleave="onMouseLeave"
|
|
10
|
+
)
|
|
11
|
+
.k-chart-controls(v-if="zoomable")
|
|
12
|
+
KButton(size="small" @click="zoomIn" title="Zoom In") +
|
|
13
|
+
KButton(size="small" @click="zoomOut" title="Zoom Out") −
|
|
14
|
+
KButton(size="small" @click="resetZoom" title="Reset") ↺
|
|
15
|
+
.k-chart-inner(ref="containerRef")
|
|
16
|
+
KLoader(:loading="isLoading" overlay)
|
|
17
|
+
canvas(ref="canvasRef" v-show="!isLoading")
|
|
18
|
+
svg.k-chart-svg(v-show="!isLoading" ref="svgRef" :width="width" :height="height")
|
|
19
|
+
defs
|
|
20
|
+
clipPath(id="plot-clip")
|
|
21
|
+
rect(
|
|
22
|
+
:x="plotLeft"
|
|
23
|
+
:y="plotTop"
|
|
24
|
+
:width="plotWidth.value"
|
|
25
|
+
:height="plotHeight.value"
|
|
26
|
+
)
|
|
27
|
+
g.k-chart-hits
|
|
28
|
+
// Hit detection is now handled by calculation in onMouseMove
|
|
29
|
+
|
|
30
|
+
// Dynamic Interaction Layer
|
|
31
|
+
g.k-chart-interaction(v-if="hoveredIndex !== -1 && hoveredBar" clip-path="url(#plot-clip)")
|
|
32
|
+
rect.k-chart-hit-proxy(
|
|
33
|
+
:x="hoveredBar.x"
|
|
34
|
+
:y="hoveredBar.y"
|
|
35
|
+
:width="hoveredBar.w"
|
|
36
|
+
:height="hoveredBar.h"
|
|
37
|
+
fill="transparent"
|
|
38
|
+
style="cursor: pointer"
|
|
39
|
+
@click="emit('click-point', hoveredBar.item, hoveredIndex)"
|
|
40
|
+
)
|
|
41
|
+
rect(
|
|
42
|
+
:x="hoveredBar.x - 2"
|
|
43
|
+
:y="hoveredBar.y - 2"
|
|
44
|
+
:width="hoveredBar.w + 4"
|
|
45
|
+
:height="hoveredBar.h + 4"
|
|
46
|
+
fill="transparent"
|
|
47
|
+
stroke="#fff"
|
|
48
|
+
stroke-width="2"
|
|
49
|
+
rx="2"
|
|
50
|
+
pointer-events="none"
|
|
51
|
+
)
|
|
52
|
+
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="{ left: tooltipState.x + 'px', top: tooltipState.y + 'px' }")
|
|
53
|
+
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
54
|
+
span {{ tooltipState.content }}
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<script lang="ts" setup>
|
|
58
|
+
import { ref, watch, computed, onMounted, nextTick } from 'vue'
|
|
59
|
+
import { useChartCanvas } from '../../composables/useChartCanvas'
|
|
60
|
+
import { useChartSvg } from '../../composables/useChartSvg'
|
|
61
|
+
import { useChartData } from '../../composables/useChartData'
|
|
62
|
+
import { useChartViewport } from '../../composables/useChartViewport'
|
|
63
|
+
import { useChartAnimation, easeOut } from '../../composables/useChartAnimation'
|
|
64
|
+
import { useChartExport } from '../../composables/useChartExport'
|
|
65
|
+
|
|
66
|
+
const props = withDefaults(defineProps<{
|
|
67
|
+
dataProvider?: any
|
|
68
|
+
categoryField?: string
|
|
69
|
+
valueField?: string
|
|
70
|
+
groupField?: string
|
|
71
|
+
orientation?: 'vertical' | 'horizontal'
|
|
72
|
+
mode?: 'simple' | 'clustered' | 'stacked'
|
|
73
|
+
animated?: boolean
|
|
74
|
+
zoomable?: boolean
|
|
75
|
+
maxZoom?: number
|
|
76
|
+
colors?: string[]
|
|
77
|
+
backgroundColor?: string
|
|
78
|
+
showLegend?: boolean
|
|
79
|
+
xAxisTitle?: string
|
|
80
|
+
yAxisTitle?: string
|
|
81
|
+
}>(), {
|
|
82
|
+
categoryField: 'category',
|
|
83
|
+
valueField: 'value',
|
|
84
|
+
groupField: 'group',
|
|
85
|
+
orientation: 'vertical',
|
|
86
|
+
mode: 'clustered',
|
|
87
|
+
animated: true,
|
|
88
|
+
zoomable: true,
|
|
89
|
+
maxZoom: 8,
|
|
90
|
+
colors: () => ['#6366f1', '#22d3ee', '#f59e0b', '#10b981', '#f43f5e', '#a855f7'],
|
|
91
|
+
backgroundColor: '#ffffff',
|
|
92
|
+
showLegend: true,
|
|
93
|
+
xAxisTitle: '',
|
|
94
|
+
yAxisTitle: ''
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const emit = defineEmits<{
|
|
98
|
+
'click-point': [item: any, index: number]
|
|
99
|
+
'hover-point': [item: any, index: number]
|
|
100
|
+
'zoom-change': [scale: number]
|
|
101
|
+
'pan-change': [offset: { x: number; y: number }]
|
|
102
|
+
}>()
|
|
103
|
+
|
|
104
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
105
|
+
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
106
|
+
const svgRef = ref<SVGSVGElement | null>(null)
|
|
107
|
+
|
|
108
|
+
const { ctx, width, height, clear, setupCanvas } = useChartCanvas()
|
|
109
|
+
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg()
|
|
110
|
+
const dataRef = computed(() => props.dataProvider)
|
|
111
|
+
const { items } = useChartData(dataRef)
|
|
112
|
+
const { progress, animate } = useChartAnimation()
|
|
113
|
+
const { exportPng, exportSvg: exportSvgFile } = useChartExport()
|
|
114
|
+
|
|
115
|
+
const isLoading = computed(() => {
|
|
116
|
+
return props.dataProvider?.loading?.value || (props.dataProvider as any)?.initialLoad?.value || false
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const maxZoomRef = computed(() => props.maxZoom)
|
|
120
|
+
const plotWidthRef = computed(() => Math.max(0, width.value - 75))
|
|
121
|
+
const plotLeftRef = computed(() => 55)
|
|
122
|
+
const viewport = useChartViewport({ maxZoom: maxZoomRef, plotWidth: plotWidthRef, plotLeft: plotLeftRef })
|
|
123
|
+
const {
|
|
124
|
+
scale, panOffset, isDragging, canPan,
|
|
125
|
+
zoomIn, zoomOut, resetZoom,
|
|
126
|
+
onWheel, onMouseDown, onMouseMove: onViewportMouseMove, onMouseUp, onMouseLeave: onViewportMouseLeave
|
|
127
|
+
} = viewport
|
|
128
|
+
|
|
129
|
+
const PAD = { top: 20, right: 20, bottom: 50, left: 55 }
|
|
130
|
+
|
|
131
|
+
const plotLeft = PAD.left
|
|
132
|
+
const plotTop = PAD.top
|
|
133
|
+
const plotWidth = computed(() => Math.max(0, width.value - PAD.left - PAD.right))
|
|
134
|
+
const plotHeight = computed(() => Math.max(0, height.value - PAD.top - PAD.bottom))
|
|
135
|
+
|
|
136
|
+
// Gather unique categories and groups
|
|
137
|
+
const categories = computed(() => [...new Set(items.value.map(r => String(r[props.categoryField])))])
|
|
138
|
+
const groups = computed(() => [...new Set(items.value.map(r => String(r[props.groupField] ?? 'default')))])
|
|
139
|
+
|
|
140
|
+
const maxValue = computed(() => {
|
|
141
|
+
if (props.mode === 'stacked') {
|
|
142
|
+
const sums = categories.value.map(cat =>
|
|
143
|
+
items.value.filter(r => String(r[props.categoryField]) === cat)
|
|
144
|
+
.reduce((acc, r) => acc + Math.max(0, Number(r[props.valueField])), 0)
|
|
145
|
+
)
|
|
146
|
+
return Math.max(...sums, 10)
|
|
147
|
+
}
|
|
148
|
+
const vals = items.value.map(r => Number(r[props.valueField]))
|
|
149
|
+
return vals.length ? Math.max(...vals, 10) : 10
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Apply zoom/pan to visible categories
|
|
153
|
+
const visibleCatCount = computed(() => Math.max(1, Math.ceil(categories.value.length / scale.value)))
|
|
154
|
+
const panCatOffset = computed(() => {
|
|
155
|
+
const maxPan = categories.value.length - visibleCatCount.value
|
|
156
|
+
const ratio = panOffset.value.x / (plotWidth.value || 1)
|
|
157
|
+
return Math.round(Math.min(Math.max(ratio * categories.value.length, 0), maxPan))
|
|
158
|
+
})
|
|
159
|
+
const visibleCategories = computed(() =>
|
|
160
|
+
categories.value.slice(panCatOffset.value, panCatOffset.value + visibleCatCount.value)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
interface IBarRect { x: number; y: number; w: number; h: number; item: any; label: string }
|
|
164
|
+
const hitBars = ref<IBarRect[]>([])
|
|
165
|
+
|
|
166
|
+
const hoveredBar = computed(() => {
|
|
167
|
+
if (hoveredIndex.value === -1) return null
|
|
168
|
+
return hitBars.value[hoveredIndex.value]
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const draw = () => {
|
|
172
|
+
if (!ctx.value) return
|
|
173
|
+
clear(props.backgroundColor)
|
|
174
|
+
const c = ctx.value
|
|
175
|
+
const p = easeOut(progress.value)
|
|
176
|
+
const bars: IBarRect[] = []
|
|
177
|
+
|
|
178
|
+
const catCount = visibleCategories.value.length
|
|
179
|
+
const gCount = groups.value.length || 1
|
|
180
|
+
const catWidth = plotWidth.value / (catCount || 1)
|
|
181
|
+
|
|
182
|
+
let barWidth = catWidth * 0.7
|
|
183
|
+
if (props.mode === 'clustered' && gCount > 1) {
|
|
184
|
+
barWidth = (catWidth * 0.8) / gCount
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Grid & Y labels
|
|
188
|
+
c.save()
|
|
189
|
+
c.strokeStyle = 'rgba(100,100,120,0.1)'
|
|
190
|
+
c.lineWidth = 1
|
|
191
|
+
c.fillStyle = 'rgba(150,150,170,0.8)'
|
|
192
|
+
c.font = '11px system-ui, sans-serif'
|
|
193
|
+
c.textAlign = 'right'
|
|
194
|
+
for (let i = 0; i <= 5; i++) {
|
|
195
|
+
const val = (maxValue.value / 5) * i
|
|
196
|
+
const y = plotTop + plotHeight.value - (val / maxValue.value) * plotHeight.value
|
|
197
|
+
c.beginPath(); c.moveTo(plotLeft, y); c.lineTo(plotLeft + plotWidth.value, y); c.stroke()
|
|
198
|
+
c.fillText(val.toFixed(0), plotLeft - 8, y + 4)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Vertical grid (optional for bar charts, but let's add thin lines between categories)
|
|
202
|
+
c.strokeStyle = 'rgba(100,100,120,0.05)'
|
|
203
|
+
for (let i = 0; i <= catCount; i++) {
|
|
204
|
+
const x = plotLeft + i * catWidth
|
|
205
|
+
c.beginPath(); c.moveTo(x, plotTop); c.lineTo(x, plotTop + plotHeight.value); c.stroke()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Lines
|
|
209
|
+
c.strokeStyle = 'rgba(150,150,170,0.5)'
|
|
210
|
+
c.beginPath(); c.moveTo(plotLeft, plotTop + plotHeight.value); c.lineTo(plotLeft + plotWidth.value, plotTop + plotHeight.value); c.stroke()
|
|
211
|
+
c.beginPath(); c.moveTo(plotLeft, plotTop); c.lineTo(plotLeft, plotTop + plotHeight.value); c.stroke()
|
|
212
|
+
c.restore()
|
|
213
|
+
|
|
214
|
+
// Clipping for plot area
|
|
215
|
+
c.save()
|
|
216
|
+
c.beginPath()
|
|
217
|
+
c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value)
|
|
218
|
+
c.clip()
|
|
219
|
+
|
|
220
|
+
// Draw bars per visible category
|
|
221
|
+
visibleCategories.value.forEach((cat, catIdx) => {
|
|
222
|
+
const catItems = items.value.filter(r => String(r[props.categoryField]) === cat)
|
|
223
|
+
const catX = plotLeft + catIdx * catWidth
|
|
224
|
+
|
|
225
|
+
if (props.mode === 'stacked') {
|
|
226
|
+
let yOffset = 0
|
|
227
|
+
groups.value.forEach((group, gIdx) => {
|
|
228
|
+
const item = catItems.find(r => String(r[props.groupField] ?? 'default') === group)
|
|
229
|
+
if (!item) return
|
|
230
|
+
const val = Number(item[props.valueField]) * p
|
|
231
|
+
const bh = (val / maxValue.value) * plotHeight.value
|
|
232
|
+
const bx = catX + (catWidth - barWidth) / 2
|
|
233
|
+
const by = plotTop + plotHeight.value - (yOffset + val) / maxValue.value * plotHeight.value
|
|
234
|
+
const isHovered = hoveredIndex.value === bars.length
|
|
235
|
+
const color = props.colors[gIdx % props.colors.length]
|
|
236
|
+
c.fillStyle = isHovered ? color : color + 'dd'
|
|
237
|
+
c.fillRect(bx, by, barWidth, bh)
|
|
238
|
+
|
|
239
|
+
// Stroke for separation
|
|
240
|
+
c.strokeStyle = 'rgba(255,255,255,0.3)'
|
|
241
|
+
c.lineWidth = 1
|
|
242
|
+
c.strokeRect(bx, by, barWidth, bh)
|
|
243
|
+
|
|
244
|
+
bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` })
|
|
245
|
+
yOffset += Number(item[props.valueField])
|
|
246
|
+
})
|
|
247
|
+
} else if (props.mode === 'simple') {
|
|
248
|
+
const item = catItems[0]
|
|
249
|
+
if (item) {
|
|
250
|
+
const isHovered = hoveredIndex.value === bars.length
|
|
251
|
+
const val = Number(item[props.valueField]) * p
|
|
252
|
+
const bh = (val / maxValue.value) * plotHeight.value
|
|
253
|
+
const bx = catX + (catWidth - barWidth) / 2
|
|
254
|
+
const by = plotTop + plotHeight.value - bh
|
|
255
|
+
const color = props.colors[0] || '#6366f1'
|
|
256
|
+
c.fillStyle = isHovered ? color : color + 'dd'
|
|
257
|
+
c.fillRect(bx, by, barWidth, bh)
|
|
258
|
+
bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat}: ${item[props.valueField]}` })
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// clustered
|
|
262
|
+
groups.value.forEach((group, gIdx) => {
|
|
263
|
+
const item = catItems.find(r => String(r[props.groupField] ?? 'default') === group)
|
|
264
|
+
if (!item) return
|
|
265
|
+
const isHovered = hoveredIndex.value === bars.length
|
|
266
|
+
const val = Number(item[props.valueField]) * p
|
|
267
|
+
const bh = (val / maxValue.value) * plotHeight.value
|
|
268
|
+
|
|
269
|
+
const gCountTotal = groups.value.length
|
|
270
|
+
const totalBarsW = gCountTotal * barWidth + (gCountTotal - 1) * 2
|
|
271
|
+
const startX = catX + (catWidth - totalBarsW) / 2
|
|
272
|
+
const bx = startX + gIdx * (barWidth + 2)
|
|
273
|
+
const by = plotTop + plotHeight.value - bh
|
|
274
|
+
|
|
275
|
+
const color = props.colors[gIdx % props.colors.length]
|
|
276
|
+
c.fillStyle = isHovered ? color : color + 'dd'
|
|
277
|
+
c.fillRect(bx, by, barWidth, bh)
|
|
278
|
+
bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` })
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
})
|
|
283
|
+
c.restore()
|
|
284
|
+
|
|
285
|
+
// X labels - Draw AFTER clipping so they are visible
|
|
286
|
+
c.save()
|
|
287
|
+
c.fillStyle = 'rgba(150,150,170,0.9)'
|
|
288
|
+
c.font = '11px system-ui, sans-serif'
|
|
289
|
+
c.textAlign = 'center'
|
|
290
|
+
visibleCategories.value.forEach((cat, catIdx) => {
|
|
291
|
+
const catX = plotLeft + catIdx * catWidth
|
|
292
|
+
const label = cat.length > 10 ? cat.slice(0, 10) + '…' : cat
|
|
293
|
+
c.fillText(label, catX + catWidth / 2, plotTop + plotHeight.value + 18)
|
|
294
|
+
})
|
|
295
|
+
c.restore()
|
|
296
|
+
|
|
297
|
+
// Axis Titles
|
|
298
|
+
c.save()
|
|
299
|
+
c.fillStyle = 'rgba(120,120,140,0.8)'
|
|
300
|
+
if (props.xAxisTitle) {
|
|
301
|
+
c.textAlign = 'center'
|
|
302
|
+
c.fillText(props.xAxisTitle, plotLeft + plotWidth.value / 2, plotTop + plotHeight.value + 38)
|
|
303
|
+
}
|
|
304
|
+
if (props.yAxisTitle) {
|
|
305
|
+
c.translate(plotLeft - 40, plotTop + plotHeight.value / 2)
|
|
306
|
+
c.rotate(-Math.PI / 2)
|
|
307
|
+
c.textAlign = 'center'
|
|
308
|
+
c.fillText(props.yAxisTitle, 0, 0)
|
|
309
|
+
}
|
|
310
|
+
c.restore()
|
|
311
|
+
|
|
312
|
+
hitBars.value = bars
|
|
313
|
+
if (props.showLegend) {
|
|
314
|
+
drawLegend(c)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const drawLegend = (c: CanvasRenderingContext2D) => {
|
|
319
|
+
c.save()
|
|
320
|
+
c.font = '11px system-ui, sans-serif'
|
|
321
|
+
c.textAlign = 'left'
|
|
322
|
+
c.textBaseline = 'middle'
|
|
323
|
+
|
|
324
|
+
let xOffset = plotLeft
|
|
325
|
+
const yPos = plotTop + plotHeight.value + 35
|
|
326
|
+
|
|
327
|
+
groups.value.forEach((group, i) => {
|
|
328
|
+
const color = props.colors[i % props.colors.length]
|
|
329
|
+
c.fillStyle = color
|
|
330
|
+
c.fillRect(xOffset, yPos - 4, 10, 8)
|
|
331
|
+
|
|
332
|
+
c.fillStyle = 'rgba(150,150,170,0.9)'
|
|
333
|
+
const label = group === 'default' ? 'Series' : group
|
|
334
|
+
c.fillText(label, xOffset + 15, yPos)
|
|
335
|
+
xOffset += c.measureText(label).width + 35
|
|
336
|
+
})
|
|
337
|
+
c.restore()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
watch([items, scale, panOffset, hoveredIndex, progress, () => props.mode, () => props.orientation], draw, { deep: true })
|
|
341
|
+
watch(items, () => { if (props.animated) animate() }, { deep: true })
|
|
342
|
+
watch(scale, v => emit('zoom-change', v))
|
|
343
|
+
watch(panOffset, v => emit('pan-change', v), { deep: true })
|
|
344
|
+
|
|
345
|
+
onMounted(async () => {
|
|
346
|
+
await nextTick()
|
|
347
|
+
if (containerRef.value && canvasRef.value) {
|
|
348
|
+
setupCanvas(canvasRef.value)
|
|
349
|
+
if (props.animated) animate()
|
|
350
|
+
else draw()
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
355
|
+
onViewportMouseMove(e)
|
|
356
|
+
if (isDragging.value) {
|
|
357
|
+
setHovered(-1)
|
|
358
|
+
hideTooltip()
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const rect = containerRef.value?.getBoundingClientRect()
|
|
363
|
+
if (!rect) return
|
|
364
|
+
const mouseX = e.clientX - rect.left
|
|
365
|
+
const mouseY = e.clientY - rect.top
|
|
366
|
+
|
|
367
|
+
// Find if mouse is within a bar
|
|
368
|
+
const hovered = hitBars.value.find(b =>
|
|
369
|
+
mouseX >= b.x && mouseX <= b.x + b.w &&
|
|
370
|
+
mouseY >= b.y && mouseY <= b.y + b.h
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if (hovered) {
|
|
374
|
+
const idx = hitBars.value.indexOf(hovered)
|
|
375
|
+
setHovered(idx)
|
|
376
|
+
showTooltip(mouseX + 10, mouseY - 10, hovered.label, hovered.item, idx)
|
|
377
|
+
emit('hover-point', hovered.item, idx)
|
|
378
|
+
} else {
|
|
379
|
+
setHovered(-1)
|
|
380
|
+
hideTooltip()
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const onMouseLeave = (e: MouseEvent) => {
|
|
385
|
+
onViewportMouseLeave(e)
|
|
386
|
+
setHovered(-1)
|
|
387
|
+
hideTooltip()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
defineExpose({
|
|
391
|
+
zoomIn: viewport.zoomIn, zoomOut: viewport.zoomOut, resetZoom: viewport.resetZoom,
|
|
392
|
+
exportPng: () => canvasRef.value && exportPng(canvasRef.value, 'bar-chart.png'),
|
|
393
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, 'bar-chart.svg')
|
|
394
|
+
})
|
|
395
|
+
</script>
|
|
396
|
+
|
|
397
|
+
<style lang="scss" scoped>
|
|
398
|
+
.k-chart-wrapper {
|
|
399
|
+
position: relative; user-select: none;
|
|
400
|
+
.k-chart-controls { position: absolute; top: 8px; right: 8px; z-index: 10; display: flex; gap: 4px; }
|
|
401
|
+
.k-chart-btn { display: none; }
|
|
402
|
+
.k-chart-inner {
|
|
403
|
+
width: 100%;
|
|
404
|
+
height: 400px;
|
|
405
|
+
position: relative;
|
|
406
|
+
canvas, .k-chart-svg {
|
|
407
|
+
position: absolute;
|
|
408
|
+
top: 0;
|
|
409
|
+
left: 0;
|
|
410
|
+
width: 100%;
|
|
411
|
+
height: 100%;
|
|
412
|
+
display: block;
|
|
413
|
+
}
|
|
414
|
+
.k-chart-svg {
|
|
415
|
+
overflow: visible;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
.k-chart-svg {
|
|
419
|
+
pointer-events: none;
|
|
420
|
+
.k-chart-hit-proxy {
|
|
421
|
+
pointer-events: all;
|
|
422
|
+
cursor: pointer;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
.k-chart-tooltip { position: absolute; background: var(--bg-color-elevated, #1e1e2e); color: var(--text-color-primary, #cdd6f4); border: 1px solid var(--border-color-medium, #45475a); border-radius: 8px; padding: 6px 10px; font-size: 12px; pointer-events: none; z-index: 20; white-space: nowrap; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
|
|
426
|
+
}
|
|
427
|
+
</style>
|