@katlux/block-charts 0.1.0-beta.8 → 0.1.1-alpha.101

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 (34) hide show
  1. package/LICENSE +200 -0
  2. package/dist/module.d.mts +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +2 -0
  5. package/dist/runtime/components/KAreaChart/KAreaChart.d.vue.ts +2 -0
  6. package/dist/runtime/components/KAreaChart/KAreaChart.vue +271 -69
  7. package/dist/runtime/components/KAreaChart/KAreaChart.vue.d.ts +2 -0
  8. package/dist/runtime/components/KBarChart/KBarChart.d.vue.ts +2 -0
  9. package/dist/runtime/components/KBarChart/KBarChart.vue +323 -125
  10. package/dist/runtime/components/KBarChart/KBarChart.vue.d.ts +2 -0
  11. package/dist/runtime/components/KHeatMap/KHeatMap.vue +56 -21
  12. package/dist/runtime/components/KLineChart/KLineChart.d.vue.ts +2 -0
  13. package/dist/runtime/components/KLineChart/KLineChart.vue +179 -48
  14. package/dist/runtime/components/KLineChart/KLineChart.vue.d.ts +2 -0
  15. package/dist/runtime/components/KPieChart/KPieChart.vue +32 -29
  16. package/dist/runtime/components/KScatterChart/KScatterChart.d.vue.ts +3 -1
  17. package/dist/runtime/components/KScatterChart/KScatterChart.vue +161 -49
  18. package/dist/runtime/components/KScatterChart/KScatterChart.vue.d.ts +3 -1
  19. package/dist/runtime/composables/chartUtils.d.ts +8 -0
  20. package/dist/runtime/composables/chartUtils.js +58 -0
  21. package/dist/runtime/composables/useChartAnimation.js +7 -1
  22. package/dist/runtime/composables/useChartAxes.d.ts +4 -2
  23. package/dist/runtime/composables/useChartAxes.js +32 -13
  24. package/dist/runtime/composables/useChartCanvas.d.ts +1 -0
  25. package/dist/runtime/composables/useChartCanvas.js +51 -18
  26. package/dist/runtime/composables/useChartData.d.ts +13 -7
  27. package/dist/runtime/composables/useChartData.js +99 -30
  28. package/dist/runtime/composables/useChartExport.d.ts +1 -1
  29. package/dist/runtime/composables/useChartExport.js +26 -3
  30. package/dist/runtime/composables/useChartHitTest.d.ts +2 -2
  31. package/dist/runtime/composables/useChartHitTest.js +7 -5
  32. package/dist/runtime/composables/useChartViewport.d.ts +3 -0
  33. package/dist/runtime/composables/useChartViewport.js +30 -7
  34. package/package.json +12 -7
@@ -1,8 +1,7 @@
1
1
  <template lang="pug">
2
2
  .k-chart-wrapper(
3
3
  :class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
4
- :style="{ cursor: isDragging ? 'grabbing' : canPan ? 'grab' : 'default' }"
5
- @wheel.prevent="onWheel"
4
+ @wheel="onWheel"
6
5
  @mousedown="onMouseDown"
7
6
  @mousemove="onMouseMove"
8
7
  @mouseup="onMouseUp"
@@ -22,7 +21,7 @@
22
21
  :height="height"
23
22
  )
24
23
  defs
25
- clipPath(id="plot-clip")
24
+ clipPath(:id="'plot-clip-' + clipId")
26
25
  rect(
27
26
  :x="axes.plotLeft"
28
27
  :y="axes.plotTop"
@@ -30,10 +29,9 @@
30
29
  :height="axes.plotHeight.value"
31
30
  )
32
31
  g.k-chart-hits
33
- // No more rect v-for here.
34
32
 
35
33
  // Dynamic Interaction Layer
36
- g.k-chart-interaction(v-if="hoveredIndex !== -1" clip-path="url(#plot-clip)")
34
+ g.k-chart-interaction(v-if="hoveredIndex !== -1" :clip-path="'url(#plot-clip-' + clipId + ')'")
37
35
  circle.k-chart-hit-proxy(
38
36
  v-for="(pt, i) in hoveredPoints"
39
37
  :key="i"
@@ -41,7 +39,6 @@
41
39
  :cy="pt.y"
42
40
  :r="10"
43
41
  fill="transparent"
44
- style="cursor: pointer"
45
42
  @click="emit('click-point', pt.item, pt.index)"
46
43
  )
47
44
  circle(
@@ -57,14 +54,14 @@
57
54
  )
58
55
  .k-chart-tooltip(
59
56
  v-if="tooltipState.visible && !isLoading"
60
- :style="{ left: tooltipState.x + 'px', top: tooltipState.y + 'px' }"
57
+ :style="tooltipStyle"
61
58
  )
62
59
  slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
63
60
  span {{ tooltipState.content }}
64
61
  </template>
65
62
 
66
63
  <script setup>
67
- import { ref, watch, computed, onMounted, nextTick } from "vue";
64
+ import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
68
65
  import { useChartCanvas } from "../../composables/useChartCanvas";
69
66
  import { useChartSvg } from "../../composables/useChartSvg";
70
67
  import { useChartData } from "../../composables/useChartData";
@@ -73,6 +70,8 @@ import { useChartViewport } from "../../composables/useChartViewport";
73
70
  import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
74
71
  import { useChartExport } from "../../composables/useChartExport";
75
72
  import { useChartHitTest } from "../../composables/useChartHitTest";
73
+ import { cleanNumber, safeMax } from "../../composables/chartUtils";
74
+ const clipId = useId();
76
75
  const props = defineProps({
77
76
  dataProvider: { type: null, required: false },
78
77
  xField: { type: String, required: false, default: "x" },
@@ -80,6 +79,7 @@ const props = defineProps({
80
79
  seriesField: { type: String, required: false, default: "series" },
81
80
  animated: { type: Boolean, required: false, default: true },
82
81
  zoomable: { type: Boolean, required: false, default: true },
82
+ wheelNavigation: { type: Boolean, required: false, default: true },
83
83
  maxZoom: { type: Number, required: false, default: 10 },
84
84
  stacked: { type: Boolean, required: false, default: false },
85
85
  colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7"] },
@@ -92,19 +92,35 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
92
92
  const containerRef = ref(null);
93
93
  const canvasRef = ref(null);
94
94
  const svgRef = ref(null);
95
- const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
95
+ const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
96
96
  const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
97
97
  const dataRef = computed(() => props.dataProvider);
98
- const { items } = useChartData(dataRef);
98
+ const { items, loading: dataLoading } = useChartData(dataRef);
99
99
  const { progress, animate } = useChartAnimation();
100
100
  const { exportPng, exportSvg: exportSvgFile } = useChartExport();
101
101
  const isLoading = computed(() => {
102
- return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
102
+ const dp = props.dataProvider;
103
+ if (!dp) return dataLoading.value;
104
+ const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
105
+ const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
106
+ return Boolean(dataLoading.value || rawLoading || rawInitial);
103
107
  });
108
+ const tooltipStyle = computed(() => ({
109
+ left: `${tooltipState.value.x}px`,
110
+ top: `${tooltipState.value.y}px`
111
+ }));
104
112
  const maxZoomRef = computed(() => props.maxZoom);
105
113
  const plotWidthRef = computed(() => Math.max(0, width.value - 75));
106
114
  const plotLeftRef = computed(() => 55);
107
- const viewport = useChartViewport({ maxZoom: maxZoomRef, plotWidth: plotWidthRef, plotLeft: plotLeftRef });
115
+ const zoomableRef = computed(() => props.zoomable);
116
+ const wheelNavRef = computed(() => props.wheelNavigation);
117
+ const viewport = useChartViewport({
118
+ maxZoom: maxZoomRef,
119
+ plotWidth: plotWidthRef,
120
+ plotLeft: plotLeftRef,
121
+ zoomable: zoomableRef,
122
+ wheelNavigation: wheelNavRef
123
+ });
108
124
  const { findNearestX } = useChartHitTest();
109
125
  const {
110
126
  scale,
@@ -122,6 +138,56 @@ const {
122
138
  } = viewport;
123
139
  const xFieldRef = computed(() => props.xField);
124
140
  const yFieldRef = computed(() => props.yField);
141
+ const xAxisType = computed(() => {
142
+ if (!items.value.length) return "linear";
143
+ const sample = items.value.find((r) => r?.[props.xField] !== void 0 && r?.[props.xField] !== null)?.[props.xField];
144
+ if (sample === void 0 || sample === null) return "linear";
145
+ if (typeof sample === "number") return "linear";
146
+ if (typeof sample === "string") {
147
+ const cleaned = cleanNumber(sample, NaN);
148
+ if (Number.isFinite(cleaned)) return "linear";
149
+ const parsed = Date.parse(sample);
150
+ if (!isNaN(parsed) && !/^\d+$/.test(sample.trim()) && /[-/T:]|[A-Za-z]/.test(sample)) return "datetime";
151
+ return "category";
152
+ }
153
+ return "linear";
154
+ });
155
+ const isAllMidnight = computed(() => {
156
+ if (xAxisType.value !== "datetime" || !items.value.length) return false;
157
+ const checkCount = Math.min(items.value.length, 100);
158
+ for (let i = 0; i < checkCount; i++) {
159
+ const ts = parseValue(items.value[i][props.xField]);
160
+ const d = new Date(ts);
161
+ if (d.getHours() !== 0 || d.getMinutes() !== 0) return false;
162
+ }
163
+ return true;
164
+ });
165
+ const categoryList = computed(() => {
166
+ if (xAxisType.value !== "category") return [];
167
+ const seen = /* @__PURE__ */ new Set();
168
+ const list = [];
169
+ for (const item of items.value) {
170
+ const val = String(item[props.xField] ?? "");
171
+ if (!seen.has(val)) {
172
+ seen.add(val);
173
+ list.push(val);
174
+ }
175
+ }
176
+ return list;
177
+ });
178
+ const categoryIndexMap = computed(() => {
179
+ const map = {};
180
+ categoryList.value.forEach((cat, i) => {
181
+ map[cat] = i;
182
+ });
183
+ return map;
184
+ });
185
+ const resolveX = (item, index = 0) => {
186
+ if (!item) return index;
187
+ const raw = item[props.xField];
188
+ if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
189
+ return parseValue(raw, index);
190
+ };
125
191
  const seriesMap = computed(() => {
126
192
  const map = /* @__PURE__ */ new Map();
127
193
  for (const item of items.value) {
@@ -131,12 +197,60 @@ const seriesMap = computed(() => {
131
197
  }
132
198
  return map;
133
199
  });
134
- const axes = useChartAxes({ items, xField: xFieldRef, yField: yFieldRef, width, height, scale, panOffset, padding: { top: 20, right: 20, bottom: 40, left: 55 } });
200
+ const sortedSeriesMap = computed(() => {
201
+ const map = /* @__PURE__ */ new Map();
202
+ const itemIndexMap = /* @__PURE__ */ new Map();
203
+ for (let i = 0; i < items.value.length; i++) {
204
+ itemIndexMap.set(items.value[i], i);
205
+ }
206
+ for (const [key, seriesItems] of seriesMap.value) {
207
+ const enriched = seriesItems.map((item, idx) => ({
208
+ item,
209
+ index: itemIndexMap.get(item) ?? idx,
210
+ xVal: resolveX(item, idx),
211
+ yVal: cleanNumber(item[props.yField])
212
+ })).sort((a, b) => a.xVal - b.xVal);
213
+ map.set(key, enriched);
214
+ }
215
+ return map;
216
+ });
217
+ const stackedMaxY = computed(() => {
218
+ if (!props.stacked) return 0;
219
+ const xTotals = /* @__PURE__ */ new Map();
220
+ for (const [, seriesItems] of sortedSeriesMap.value) {
221
+ for (const pt of seriesItems) {
222
+ const current = xTotals.get(pt.xVal) || 0;
223
+ xTotals.set(pt.xVal, current + Math.max(0, pt.yVal));
224
+ }
225
+ }
226
+ return safeMax(Array.from(xTotals.values()), 10);
227
+ });
228
+ const axes = useChartAxes({
229
+ items,
230
+ xField: xFieldRef,
231
+ yField: yFieldRef,
232
+ width,
233
+ height,
234
+ scale,
235
+ panOffset,
236
+ padding: { top: 20, right: 20, bottom: 40, left: 55 },
237
+ xValueGetter: (r, i) => resolveX(r, i)
238
+ });
239
+ const effectiveYMax = computed(() => {
240
+ return props.stacked ? Math.max(stackedMaxY.value, axes.yMax.value) : axes.yMax.value;
241
+ });
242
+ const toStackedY = (val) => {
243
+ const yMin = props.stacked ? 0 : axes.yMin.value;
244
+ const yMax = effectiveYMax.value;
245
+ const range = yMax - yMin;
246
+ if (range === 0) return axes.plotTop + axes.plotHeight.value;
247
+ return axes.plotTop + axes.plotHeight.value - (val - yMin) / range * axes.plotHeight.value;
248
+ };
135
249
  const draw = () => {
136
250
  if (!ctx.value) return;
137
251
  clear(props.backgroundColor);
138
252
  const c = ctx.value;
139
- const p = easeOut(progress.value);
253
+ const p = props.animated ? easeOut(progress.value) : 1;
140
254
  c.save();
141
255
  c.strokeStyle = "rgba(100,100,120,0.1)";
142
256
  c.lineWidth = 1;
@@ -160,17 +274,40 @@ const draw = () => {
160
274
  c.font = "11px system-ui, sans-serif";
161
275
  c.textAlign = "right";
162
276
  for (let i = 0; i <= 5; i++) {
163
- const val = axes.yMin.value + (axes.yMax.value - axes.yMin.value) * (i / 5);
164
- c.fillText(val.toFixed(1), axes.plotLeft - 8, axes.toY(val) + 4);
277
+ const val = axes.yMin.value + (effectiveYMax.value - axes.yMin.value) * (i / 5);
278
+ c.fillText(val.toFixed(1), axes.plotLeft - 8, toStackedY(val) + 4);
165
279
  }
166
280
  c.textAlign = "center";
167
- for (let i = 0; i <= 5; i++) {
168
- const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
169
- const x = axes.toX(val);
170
- let label = val.toFixed(1);
171
- if (val > 1e12) label = new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
172
- c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
281
+ if (xAxisType.value === "category") {
282
+ const count = categoryList.value.length;
283
+ if (count > 0) {
284
+ const xSteps = Math.min(10, count - 1);
285
+ const stepSize = xSteps > 0 ? (count - 1) / xSteps : 1;
286
+ for (let i = 0; i <= xSteps; i++) {
287
+ const idx = Math.round(i * stepSize);
288
+ const x = axes.toX(idx);
289
+ const label = categoryList.value[idx] ?? "";
290
+ c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
291
+ }
292
+ }
293
+ } else {
294
+ for (let i = 0; i <= 5; i++) {
295
+ const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
296
+ const x = axes.toX(val);
297
+ let label;
298
+ if (xAxisType.value === "datetime") {
299
+ label = isAllMidnight.value ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
300
+ } else {
301
+ label = val.toFixed(1);
302
+ }
303
+ c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
304
+ }
173
305
  }
306
+ c.strokeStyle = "rgba(150,150,170,0.5)";
307
+ c.beginPath();
308
+ c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
309
+ c.lineTo(axes.plotLeft + axes.plotWidth.value, axes.plotTop + axes.plotHeight.value);
310
+ c.stroke();
174
311
  c.beginPath();
175
312
  c.moveTo(axes.plotLeft, axes.plotTop);
176
313
  c.lineTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
@@ -194,41 +331,75 @@ const draw = () => {
194
331
  c.beginPath();
195
332
  c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
196
333
  c.clip();
334
+ const currentBaseline = /* @__PURE__ */ new Map();
197
335
  let seriesIdx = 0;
198
- for (const [key, seriesItems] of seriesMap.value) {
336
+ for (const [, seriesItems] of sortedSeriesMap.value) {
199
337
  const color = props.colors[seriesIdx % props.colors.length];
200
- const sorted = [...seriesItems].sort((a, b) => parseValue(a[props.xField]) - parseValue(b[props.xField]));
201
- const drawCount = Math.max(1, Math.floor(sorted.length * p));
202
- const pts = sorted.slice(0, drawCount);
338
+ const drawCount = Math.max(1, Math.floor(seriesItems.length * p));
339
+ const pts = seriesItems.slice(0, drawCount);
203
340
  c.save();
204
341
  c.beginPath();
205
- pts.forEach((item, i) => {
206
- const x = axes.toX(parseValue(item[props.xField]));
207
- const y = axes.toY(parseValue(item[props.yField]));
208
- if (i === 0) c.moveTo(x, y);
209
- else c.lineTo(x, y);
210
- });
211
- if (pts.length > 0) {
212
- const lastX = axes.toX(parseValue(pts[pts.length - 1][props.xField]));
213
- const firstX = axes.toX(parseValue(pts[0][props.xField]));
214
- const baselineY = axes.plotTop + axes.plotHeight.value;
215
- c.lineTo(lastX, baselineY);
216
- c.lineTo(firstX, baselineY);
342
+ if (props.stacked) {
343
+ const topPoints = [];
344
+ const bottomPoints = [];
345
+ for (const pt of pts) {
346
+ const x = axes.toX(pt.xVal);
347
+ const base = currentBaseline.get(pt.xVal) || 0;
348
+ const safeVal = Math.max(0, pt.yVal);
349
+ const top = base + safeVal;
350
+ const yTop = toStackedY(top);
351
+ const yBottom = toStackedY(base);
352
+ topPoints.push({ x, y: yTop });
353
+ bottomPoints.push({ x, y: yBottom });
354
+ currentBaseline.set(pt.xVal, top);
355
+ }
356
+ topPoints.forEach((pt, i) => {
357
+ if (i === 0) c.moveTo(pt.x, pt.y);
358
+ else c.lineTo(pt.x, pt.y);
359
+ });
360
+ for (let i = bottomPoints.length - 1; i >= 0; i--) {
361
+ c.lineTo(bottomPoints[i].x, bottomPoints[i].y);
362
+ }
217
363
  c.closePath();
364
+ c.fillStyle = color + "44";
365
+ c.fill();
366
+ c.beginPath();
367
+ topPoints.forEach((pt, i) => {
368
+ if (i === 0) c.moveTo(pt.x, pt.y);
369
+ else c.lineTo(pt.x, pt.y);
370
+ });
371
+ c.strokeStyle = color;
372
+ c.lineWidth = 2;
373
+ c.stroke();
374
+ } else {
375
+ pts.forEach((pt, i) => {
376
+ const x = axes.toX(pt.xVal);
377
+ const y = toStackedY(pt.yVal);
378
+ if (i === 0) c.moveTo(x, y);
379
+ else c.lineTo(x, y);
380
+ });
381
+ if (pts.length > 0) {
382
+ const lastX = axes.toX(pts[pts.length - 1].xVal);
383
+ const firstX = axes.toX(pts[0].xVal);
384
+ const baselineY = axes.plotTop + axes.plotHeight.value;
385
+ c.lineTo(lastX, baselineY);
386
+ c.lineTo(firstX, baselineY);
387
+ c.closePath();
388
+ }
389
+ c.fillStyle = color + "33";
390
+ c.fill();
391
+ c.beginPath();
392
+ pts.forEach((pt, i) => {
393
+ const x = axes.toX(pt.xVal);
394
+ const y = toStackedY(pt.yVal);
395
+ if (i === 0) c.moveTo(x, y);
396
+ else c.lineTo(x, y);
397
+ });
398
+ c.strokeStyle = color;
399
+ c.lineWidth = 2;
400
+ c.lineJoin = "round";
401
+ c.stroke();
218
402
  }
219
- c.fillStyle = color + "33";
220
- c.fill();
221
- c.beginPath();
222
- pts.forEach((item, i) => {
223
- const x = axes.toX(parseValue(item[props.xField]));
224
- const y = axes.toY(parseValue(item[props.yField]));
225
- if (i === 0) c.moveTo(x, y);
226
- else c.lineTo(x, y);
227
- });
228
- c.strokeStyle = color;
229
- c.lineWidth = 2;
230
- c.lineJoin = "round";
231
- c.stroke();
232
403
  c.restore();
233
404
  seriesIdx++;
234
405
  }
@@ -256,29 +427,43 @@ const drawLegend = (c) => {
256
427
  });
257
428
  c.restore();
258
429
  };
259
- watch([items, scale, panOffset, hoveredIndex, progress], draw, { deep: true });
430
+ watch(progress, draw);
431
+ watch([scale, panOffset, hoveredIndex, resizeTick, width, height, () => props.stacked], draw);
260
432
  watch(items, () => {
261
433
  if (props.animated) animate();
262
- }, { deep: true });
434
+ else draw();
435
+ });
263
436
  watch(scale, (v) => emit("zoom-change", v));
264
- watch(panOffset, (v) => emit("pan-change", v), { deep: true });
437
+ watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
438
+ watch(() => items.value.length, (newLen) => {
439
+ if (hoveredIndex.value >= newLen) {
440
+ setHovered(-1);
441
+ }
442
+ });
265
443
  const hoveredPoints = computed(() => {
266
- if (hoveredIndex.value === -1) return [];
444
+ if (hoveredIndex.value === -1 || hoveredIndex.value >= items.value.length) return [];
267
445
  const item = items.value[hoveredIndex.value];
268
- const xVal = parseValue(item[props.xField]);
446
+ if (!item) return [];
447
+ const xVal = resolveX(item);
269
448
  const xPx = axes.toX(xVal);
270
449
  const pts = [];
271
450
  let seriesIdx = 0;
272
- for (const [key, seriesItems] of seriesMap.value) {
273
- const seriesItem = seriesItems.find((it) => parseValue(it[props.xField]) === xVal);
274
- if (seriesItem) {
451
+ let cumulativeBase = 0;
452
+ for (const [, seriesList] of sortedSeriesMap.value) {
453
+ const found = seriesList.find((pt) => pt.xVal === xVal);
454
+ if (found) {
455
+ const safeVal = Math.max(0, found.yVal);
456
+ const top = props.stacked ? cumulativeBase + safeVal : found.yVal;
275
457
  pts.push({
276
458
  x: xPx,
277
- y: axes.toY(parseValue(seriesItem[props.yField])),
459
+ y: toStackedY(top),
278
460
  color: props.colors[seriesIdx % props.colors.length],
279
- item: seriesItem,
280
- index: items.value.indexOf(seriesItem)
461
+ item: found.item,
462
+ index: found.index
281
463
  });
464
+ if (props.stacked) {
465
+ cumulativeBase += safeVal;
466
+ }
282
467
  }
283
468
  seriesIdx++;
284
469
  }
@@ -293,7 +478,7 @@ onMounted(async () => {
293
478
  }
294
479
  });
295
480
  const onMouseMove = (e) => {
296
- viewport.onMouseMove(e);
481
+ onViewportMouseMove(e);
297
482
  if (isDragging.value) {
298
483
  setHovered(-1);
299
484
  hideTooltip();
@@ -303,11 +488,24 @@ const onMouseMove = (e) => {
303
488
  if (!rect) return;
304
489
  const mouseX = e.clientX - rect.left;
305
490
  const mouseY = e.clientY - rect.top;
306
- const nearest = findNearestX(items.value, mouseX, props.xField, axes);
491
+ if (mouseX < axes.plotLeft || mouseX > axes.plotLeft + axes.plotWidth.value || mouseY < axes.plotTop || mouseY > axes.plotTop + axes.plotHeight.value) {
492
+ setHovered(-1);
493
+ hideTooltip();
494
+ return;
495
+ }
496
+ const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
307
497
  if (nearest) {
308
498
  setHovered(nearest.index);
309
- showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
310
499
  emit("hover-point", nearest.item, nearest.index);
500
+ const xPx = axes.toX(resolveX(nearest.item, nearest.index));
501
+ const yPx = toStackedY(cleanNumber(nearest.item[props.yField]));
502
+ showTooltip(
503
+ xPx,
504
+ yPx - 10,
505
+ `${props.xField}: ${nearest.item[props.xField]} | ${props.yField}: ${nearest.item[props.yField]}`,
506
+ nearest.item,
507
+ nearest.index
508
+ );
311
509
  } else {
312
510
  setHovered(-1);
313
511
  hideTooltip();
@@ -323,7 +521,7 @@ defineExpose({
323
521
  zoomOut: viewport.zoomOut,
324
522
  resetZoom: viewport.resetZoom,
325
523
  exportPng: () => canvasRef.value && exportPng(canvasRef.value, "area-chart.png"),
326
- exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg")
524
+ exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg", canvasRef.value)
327
525
  });
328
526
  </script>
329
527
 
@@ -331,6 +529,13 @@ defineExpose({
331
529
  .k-chart-wrapper {
332
530
  position: relative;
333
531
  user-select: none;
532
+ cursor: default;
533
+ }
534
+ .k-chart-wrapper.k-chart--pannable {
535
+ cursor: grab;
536
+ }
537
+ .k-chart-wrapper.k-chart--grabbing {
538
+ cursor: grabbing;
334
539
  }
335
540
  .k-chart-wrapper .k-chart-controls {
336
541
  position: absolute;
@@ -340,9 +545,6 @@ defineExpose({
340
545
  display: flex;
341
546
  gap: 4px;
342
547
  }
343
- .k-chart-wrapper .k-chart-btn {
344
- display: none;
345
- }
346
548
  .k-chart-wrapper .k-chart-inner {
347
549
  width: 100%;
348
550
  height: 400px;
@@ -5,6 +5,7 @@ type __VLS_Props = {
5
5
  seriesField?: string;
6
6
  animated?: boolean;
7
7
  zoomable?: boolean;
8
+ wheelNavigation?: boolean;
8
9
  maxZoom?: number;
9
10
  stacked?: boolean;
10
11
  colors?: string[];
@@ -41,6 +42,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
41
42
  seriesField: string;
42
43
  animated: boolean;
43
44
  zoomable: boolean;
45
+ wheelNavigation: boolean;
44
46
  maxZoom: number;
45
47
  stacked: boolean;
46
48
  colors: string[];
@@ -7,6 +7,7 @@ type __VLS_Props = {
7
7
  mode?: 'simple' | 'clustered' | 'stacked';
8
8
  animated?: boolean;
9
9
  zoomable?: boolean;
10
+ wheelNavigation?: boolean;
10
11
  maxZoom?: number;
11
12
  colors?: string[];
12
13
  backgroundColor?: string;
@@ -39,6 +40,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
39
40
  }>, {
40
41
  animated: boolean;
41
42
  zoomable: boolean;
43
+ wheelNavigation: boolean;
42
44
  maxZoom: number;
43
45
  colors: string[];
44
46
  backgroundColor: string;