@katlux/block-charts 0.1.1-alpha.93 → 0.1.1-alpha.95

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 (24) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/KAreaChart/KAreaChart.d.vue.ts +2 -0
  3. package/dist/runtime/components/KAreaChart/KAreaChart.vue +53 -25
  4. package/dist/runtime/components/KAreaChart/KAreaChart.vue.d.ts +2 -0
  5. package/dist/runtime/components/KBarChart/KBarChart.d.vue.ts +2 -0
  6. package/dist/runtime/components/KBarChart/KBarChart.vue +89 -36
  7. package/dist/runtime/components/KBarChart/KBarChart.vue.d.ts +2 -0
  8. package/dist/runtime/components/KHeatMap/KHeatMap.vue +7 -3
  9. package/dist/runtime/components/KLineChart/KLineChart.d.vue.ts +2 -0
  10. package/dist/runtime/components/KLineChart/KLineChart.vue +45 -23
  11. package/dist/runtime/components/KLineChart/KLineChart.vue.d.ts +2 -0
  12. package/dist/runtime/components/KPieChart/KPieChart.vue +7 -3
  13. package/dist/runtime/components/KScatterChart/KScatterChart.d.vue.ts +3 -1
  14. package/dist/runtime/components/KScatterChart/KScatterChart.vue +59 -13
  15. package/dist/runtime/components/KScatterChart/KScatterChart.vue.d.ts +3 -1
  16. package/dist/runtime/composables/useChartAnimation.js +7 -1
  17. package/dist/runtime/composables/useChartData.d.ts +1 -0
  18. package/dist/runtime/composables/useChartData.js +53 -22
  19. package/dist/runtime/composables/useChartExport.js +10 -3
  20. package/dist/runtime/composables/useChartHitTest.d.ts +1 -1
  21. package/dist/runtime/composables/useChartHitTest.js +3 -2
  22. package/dist/runtime/composables/useChartViewport.d.ts +3 -0
  23. package/dist/runtime/composables/useChartViewport.js +29 -7
  24. package/package.json +3 -3
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.1-alpha.93",
7
+ "version": "0.1.1-alpha.95",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -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[];
@@ -1,7 +1,7 @@
1
1
  <template lang="pug">
2
2
  .k-chart-wrapper(
3
3
  :class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
4
- @wheel.prevent="onWheel"
4
+ @wheel="onWheel"
5
5
  @mousedown="onMouseDown"
6
6
  @mousemove="onMouseMove"
7
7
  @mouseup="onMouseUp"
@@ -21,7 +21,7 @@
21
21
  :height="height"
22
22
  )
23
23
  defs
24
- clipPath(id="plot-clip")
24
+ clipPath(:id="'plot-clip-' + clipId")
25
25
  rect(
26
26
  :x="axes.plotLeft"
27
27
  :y="axes.plotTop"
@@ -31,7 +31,7 @@
31
31
  g.k-chart-hits
32
32
 
33
33
  // Dynamic Interaction Layer
34
- 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 + ')'")
35
35
  circle.k-chart-hit-proxy(
36
36
  v-for="(pt, i) in hoveredPoints"
37
37
  :key="i"
@@ -61,7 +61,7 @@
61
61
  </template>
62
62
 
63
63
  <script setup>
64
- import { ref, watch, computed, onMounted, nextTick } from "vue";
64
+ import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
65
65
  import { useChartCanvas } from "../../composables/useChartCanvas";
66
66
  import { useChartSvg } from "../../composables/useChartSvg";
67
67
  import { useChartData } from "../../composables/useChartData";
@@ -71,6 +71,7 @@ import { useChartAnimation, easeOut } from "../../composables/useChartAnimation"
71
71
  import { useChartExport } from "../../composables/useChartExport";
72
72
  import { useChartHitTest } from "../../composables/useChartHitTest";
73
73
  import { cleanNumber, safeMax } from "../../composables/chartUtils";
74
+ const clipId = useId();
74
75
  const props = defineProps({
75
76
  dataProvider: { type: null, required: false },
76
77
  xField: { type: String, required: false, default: "x" },
@@ -78,6 +79,7 @@ const props = defineProps({
78
79
  seriesField: { type: String, required: false, default: "series" },
79
80
  animated: { type: Boolean, required: false, default: true },
80
81
  zoomable: { type: Boolean, required: false, default: true },
82
+ wheelNavigation: { type: Boolean, required: false, default: true },
81
83
  maxZoom: { type: Number, required: false, default: 10 },
82
84
  stacked: { type: Boolean, required: false, default: false },
83
85
  colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7"] },
@@ -93,11 +95,15 @@ const svgRef = ref(null);
93
95
  const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
94
96
  const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
95
97
  const dataRef = computed(() => props.dataProvider);
96
- const { items } = useChartData(dataRef);
98
+ const { items, loading: dataLoading } = useChartData(dataRef);
97
99
  const { progress, animate } = useChartAnimation();
98
100
  const { exportPng, exportSvg: exportSvgFile } = useChartExport();
99
101
  const isLoading = computed(() => {
100
- 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);
101
107
  });
102
108
  const tooltipStyle = computed(() => ({
103
109
  left: `${tooltipState.value.x}px`,
@@ -106,7 +112,15 @@ const tooltipStyle = computed(() => ({
106
112
  const maxZoomRef = computed(() => props.maxZoom);
107
113
  const plotWidthRef = computed(() => Math.max(0, width.value - 75));
108
114
  const plotLeftRef = computed(() => 55);
109
- 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
+ });
110
124
  const { findNearestX } = useChartHitTest();
111
125
  const {
112
126
  scale,
@@ -129,14 +143,24 @@ const xAxisType = computed(() => {
129
143
  const firstVal = items.value[0]?.[props.xField];
130
144
  if (typeof firstVal === "number") return "linear";
131
145
  if (typeof firstVal === "string") {
146
+ const cleaned = cleanNumber(firstVal, NaN);
147
+ if (Number.isFinite(cleaned)) return "linear";
132
148
  const parsed = Date.parse(firstVal);
133
- if (!isNaN(parsed) && !/^\d+$/.test(firstVal.trim())) return "datetime";
134
- const num = Number(firstVal);
135
- if (!isNaN(num)) return "linear";
149
+ if (!isNaN(parsed) && !/^\d+$/.test(firstVal.trim()) && /[-/T:]|[A-Za-z]/.test(firstVal)) return "datetime";
136
150
  return "category";
137
151
  }
138
152
  return "linear";
139
153
  });
154
+ const isAllMidnight = computed(() => {
155
+ if (xAxisType.value !== "datetime" || !items.value.length) return false;
156
+ const checkCount = Math.min(items.value.length, 100);
157
+ for (let i = 0; i < checkCount; i++) {
158
+ const ts = parseValue(items.value[i][props.xField]);
159
+ const d = new Date(ts);
160
+ if (d.getHours() !== 0 || d.getMinutes() !== 0) return false;
161
+ }
162
+ return true;
163
+ });
140
164
  const categoryList = computed(() => {
141
165
  if (xAxisType.value !== "category") return [];
142
166
  const seen = /* @__PURE__ */ new Set();
@@ -215,7 +239,7 @@ const effectiveYMax = computed(() => {
215
239
  return props.stacked ? Math.max(stackedMaxY.value, axes.yMax.value) : axes.yMax.value;
216
240
  });
217
241
  const toStackedY = (val) => {
218
- const yMin = axes.yMin.value;
242
+ const yMin = props.stacked ? 0 : axes.yMin.value;
219
243
  const yMax = effectiveYMax.value;
220
244
  const range = yMax - yMin;
221
245
  if (range === 0) return axes.plotTop + axes.plotHeight.value;
@@ -271,12 +295,7 @@ const draw = () => {
271
295
  const x = axes.toX(val);
272
296
  let label;
273
297
  if (xAxisType.value === "datetime") {
274
- const allMidnight = items.value.every((r) => {
275
- const ts = parseValue(r[props.xField]);
276
- const d = new Date(ts);
277
- return d.getHours() === 0 && d.getMinutes() === 0;
278
- });
279
- label = allMidnight ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
298
+ label = isAllMidnight.value ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
280
299
  } else {
281
300
  label = val.toFixed(1);
282
301
  }
@@ -325,7 +344,8 @@ const draw = () => {
325
344
  for (const pt of pts) {
326
345
  const x = axes.toX(pt.xVal);
327
346
  const base = currentBaseline.get(pt.xVal) || 0;
328
- const top = base + pt.yVal;
347
+ const safeVal = Math.max(0, pt.yVal);
348
+ const top = base + safeVal;
329
349
  const yTop = toStackedY(top);
330
350
  const yBottom = toStackedY(base);
331
351
  topPoints.push({ x, y: yTop });
@@ -413,28 +433,36 @@ watch(items, () => {
413
433
  else draw();
414
434
  });
415
435
  watch(scale, (v) => emit("zoom-change", v));
416
- watch(panOffset, (v) => emit("pan-change", v), { deep: true });
417
- const hoveredPoints = computed(() => {
418
- if (hoveredIndex.value === -1) return [];
419
- if (hoveredIndex.value >= items.value.length) {
436
+ watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
437
+ watch(() => items.value.length, (newLen) => {
438
+ if (hoveredIndex.value >= newLen) {
420
439
  setHovered(-1);
421
- return [];
422
440
  }
441
+ });
442
+ const hoveredPoints = computed(() => {
443
+ if (hoveredIndex.value === -1 || hoveredIndex.value >= items.value.length) return [];
423
444
  const item = items.value[hoveredIndex.value];
445
+ if (!item) return [];
424
446
  const xVal = resolveX(item);
425
447
  const xPx = axes.toX(xVal);
426
448
  const pts = [];
427
449
  let seriesIdx = 0;
450
+ let cumulativeBase = 0;
428
451
  for (const [, seriesList] of sortedSeriesMap.value) {
429
452
  const found = seriesList.find((pt) => pt.xVal === xVal);
430
453
  if (found) {
454
+ const safeVal = Math.max(0, found.yVal);
455
+ const top = props.stacked ? cumulativeBase + safeVal : found.yVal;
431
456
  pts.push({
432
457
  x: xPx,
433
- y: toStackedY(found.yVal),
458
+ y: toStackedY(top),
434
459
  color: props.colors[seriesIdx % props.colors.length],
435
460
  item: found.item,
436
461
  index: found.index
437
462
  });
463
+ if (props.stacked) {
464
+ cumulativeBase += safeVal;
465
+ }
438
466
  }
439
467
  seriesIdx++;
440
468
  }
@@ -464,7 +492,7 @@ const onMouseMove = (e) => {
464
492
  hideTooltip();
465
493
  return;
466
494
  }
467
- const nearest = findNearestX(mouseX, items.value, axes.toX, (r, i) => resolveX(r, i));
495
+ const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
468
496
  if (nearest) {
469
497
  setHovered(nearest.index);
470
498
  emit("hover-point", nearest.item, nearest.index);
@@ -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;
@@ -1,7 +1,7 @@
1
1
  <template lang="pug">
2
2
  .k-chart-wrapper(
3
3
  :class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
4
- @wheel.prevent="onWheel"
4
+ @wheel="onWheel"
5
5
  @mousedown="onMouseDown"
6
6
  @mousemove="onMouseMove"
7
7
  @mouseup="onMouseUp"
@@ -16,7 +16,7 @@
16
16
  canvas(ref="canvasRef" v-show="!isLoading")
17
17
  svg.k-chart-svg(v-show="!isLoading" ref="svgRef" :width="width" :height="height")
18
18
  defs
19
- clipPath(id="plot-clip")
19
+ clipPath(:id="'plot-clip-' + clipId")
20
20
  rect(
21
21
  :x="plotLeft"
22
22
  :y="plotTop"
@@ -27,7 +27,7 @@
27
27
  // Hit detection is handled by calculation in onMouseMove
28
28
 
29
29
  // Dynamic Interaction Layer
30
- g.k-chart-interaction(v-if="hoveredIndex !== -1 && hoveredBar" clip-path="url(#plot-clip)")
30
+ g.k-chart-interaction(v-if="hoveredIndex !== -1 && hoveredBar" :clip-path="'url(#plot-clip-' + clipId + ')'")
31
31
  rect.k-chart-hit-proxy(
32
32
  :x="hoveredBar.x"
33
33
  :y="hoveredBar.y"
@@ -44,7 +44,7 @@
44
44
  fill="transparent"
45
45
  stroke="#fff"
46
46
  stroke-width="2"
47
- rx="2"
47
+ stroke-dasharray="4,2"
48
48
  pointer-events="none"
49
49
  )
50
50
  .k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
@@ -53,7 +53,7 @@
53
53
  </template>
54
54
 
55
55
  <script setup>
56
- import { ref, watch, computed, onMounted, nextTick } from "vue";
56
+ import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
57
57
  import { useChartCanvas } from "../../composables/useChartCanvas";
58
58
  import { useChartSvg } from "../../composables/useChartSvg";
59
59
  import { useChartData } from "../../composables/useChartData";
@@ -61,6 +61,7 @@ import { useChartViewport } from "../../composables/useChartViewport";
61
61
  import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
62
62
  import { useChartExport } from "../../composables/useChartExport";
63
63
  import { cleanNumber, safeMax } from "../../composables/chartUtils";
64
+ const clipId = useId();
64
65
  const props = defineProps({
65
66
  dataProvider: { type: null, required: false },
66
67
  categoryField: { type: String, required: false, default: "category" },
@@ -70,6 +71,7 @@ const props = defineProps({
70
71
  mode: { type: String, required: false, default: "clustered" },
71
72
  animated: { type: Boolean, required: false, default: true },
72
73
  zoomable: { type: Boolean, required: false, default: true },
74
+ wheelNavigation: { type: Boolean, required: false, default: true },
73
75
  maxZoom: { type: Number, required: false, default: 8 },
74
76
  colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7"] },
75
77
  backgroundColor: { type: String, required: false, default: "#ffffff" },
@@ -84,11 +86,15 @@ const svgRef = ref(null);
84
86
  const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
85
87
  const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
86
88
  const dataRef = computed(() => props.dataProvider);
87
- const { items } = useChartData(dataRef);
89
+ const { items, loading: dataLoading } = useChartData(dataRef);
88
90
  const { progress, animate } = useChartAnimation();
89
91
  const { exportPng, exportSvg: exportSvgFile } = useChartExport();
90
92
  const isLoading = computed(() => {
91
- return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
93
+ const dp = props.dataProvider;
94
+ if (!dp) return dataLoading.value;
95
+ const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
96
+ const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
97
+ return Boolean(dataLoading.value || rawLoading || rawInitial);
92
98
  });
93
99
  const tooltipStyle = computed(() => ({
94
100
  left: `${tooltipState.value.x}px`,
@@ -97,7 +103,15 @@ const tooltipStyle = computed(() => ({
97
103
  const maxZoomRef = computed(() => props.maxZoom);
98
104
  const plotWidthRef = computed(() => Math.max(0, width.value - 75));
99
105
  const plotLeftRef = computed(() => 55);
100
- const viewport = useChartViewport({ maxZoom: maxZoomRef, plotWidth: plotWidthRef, plotLeft: plotLeftRef });
106
+ const zoomableRef = computed(() => props.zoomable);
107
+ const wheelNavRef = computed(() => props.wheelNavigation);
108
+ const viewport = useChartViewport({
109
+ maxZoom: maxZoomRef,
110
+ plotWidth: plotWidthRef,
111
+ plotLeft: plotLeftRef,
112
+ zoomable: zoomableRef,
113
+ wheelNavigation: wheelNavRef
114
+ });
101
115
  const {
102
116
  scale,
103
117
  panOffset,
@@ -119,6 +133,11 @@ const plotWidth = computed(() => Math.max(0, width.value - PAD.left - PAD.right)
119
133
  const plotHeight = computed(() => Math.max(0, height.value - PAD.top - PAD.bottom));
120
134
  const categories = computed(() => [...new Set(items.value.map((r) => String(r[props.categoryField])))]);
121
135
  const groups = computed(() => [...new Set(items.value.map((r) => String(r[props.groupField] ?? "default")))]);
136
+ const minValue = computed(() => {
137
+ const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
138
+ const min = Math.min(0, ...vals);
139
+ return isFinite(min) ? min : 0;
140
+ });
122
141
  const maxValue = computed(() => {
123
142
  if (props.mode === "stacked") {
124
143
  const sums = categories.value.map(
@@ -127,7 +146,11 @@ const maxValue = computed(() => {
127
146
  return safeMax(sums, 10);
128
147
  }
129
148
  const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
130
- return safeMax(vals, 10);
149
+ return Math.max(0, safeMax(vals, 10));
150
+ });
151
+ const valueRange = computed(() => {
152
+ const span = maxValue.value - minValue.value;
153
+ return span > 0 ? span : 10;
131
154
  });
132
155
  const visibleCatCount = computed(() => Math.max(1, Math.ceil(categories.value.length / scale.value)));
133
156
  const panCatOffset = computed(() => {
@@ -158,6 +181,7 @@ const draw = () => {
158
181
  if (props.mode === "clustered" && gCount > 1) {
159
182
  barHeight = catHeight * 0.8 / gCount;
160
183
  }
184
+ const xZero = plotLeft + (0 - minValue.value) / valueRange.value * plotWidth.value;
161
185
  c.save();
162
186
  c.strokeStyle = "rgba(100,100,120,0.1)";
163
187
  c.lineWidth = 1;
@@ -165,14 +189,21 @@ const draw = () => {
165
189
  c.font = "11px system-ui, sans-serif";
166
190
  c.textAlign = "center";
167
191
  for (let i = 0; i <= 5; i++) {
168
- const val = maxValue.value / 5 * i;
169
- const x = plotLeft + val / maxValue.value * plotWidth.value;
192
+ const val = minValue.value + valueRange.value / 5 * i;
193
+ const x = plotLeft + (val - minValue.value) / valueRange.value * plotWidth.value;
170
194
  c.beginPath();
171
195
  c.moveTo(x, plotTop);
172
196
  c.lineTo(x, plotTop + plotHeight.value);
173
197
  c.stroke();
174
198
  c.fillText(val.toFixed(0), x, plotTop + plotHeight.value + 16);
175
199
  }
200
+ if (minValue.value < 0) {
201
+ c.strokeStyle = "rgba(150,150,170,0.4)";
202
+ c.beginPath();
203
+ c.moveTo(xZero, plotTop);
204
+ c.lineTo(xZero, plotTop + plotHeight.value);
205
+ c.stroke();
206
+ }
176
207
  c.strokeStyle = "rgba(100,100,120,0.05)";
177
208
  for (let i = 0; i <= catCount; i++) {
178
209
  const y = plotTop + i * catHeight;
@@ -199,13 +230,21 @@ const draw = () => {
199
230
  const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
200
231
  const catY = plotTop + catIdx * catHeight;
201
232
  if (props.mode === "stacked") {
202
- let xOffset = 0;
233
+ let xOffsetPos = 0;
234
+ let xOffsetNeg = 0;
203
235
  groups.value.forEach((group, gIdx) => {
204
236
  const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
205
237
  if (!item) return;
206
- const val = cleanNumber(item[props.valueField]) * p;
207
- const bw = val / maxValue.value * plotWidth.value;
208
- const bx = plotLeft + xOffset / maxValue.value * plotWidth.value;
238
+ const rawVal = cleanNumber(item[props.valueField]);
239
+ const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
240
+ let bx = xZero;
241
+ if (rawVal >= 0) {
242
+ bx = xZero + xOffsetPos * p / valueRange.value * plotWidth.value;
243
+ xOffsetPos += rawVal;
244
+ } else {
245
+ xOffsetNeg += Math.abs(rawVal);
246
+ bx = xZero - xOffsetNeg * p / valueRange.value * plotWidth.value;
247
+ }
209
248
  const by = catY + (catHeight - barHeight) / 2;
210
249
  const isHovered = hoveredIndex.value === bars.length;
211
250
  const color = props.colors[gIdx % props.colors.length];
@@ -215,15 +254,14 @@ const draw = () => {
215
254
  c.lineWidth = 1;
216
255
  c.strokeRect(bx, by, bw, barHeight);
217
256
  bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat} (${group}): ${item[props.valueField]}` });
218
- xOffset += cleanNumber(item[props.valueField]);
219
257
  });
220
258
  } else if (props.mode === "simple") {
221
259
  const item = catItems[0];
222
260
  if (item) {
223
261
  const isHovered = hoveredIndex.value === bars.length;
224
- const val = cleanNumber(item[props.valueField]) * p;
225
- const bw = val / maxValue.value * plotWidth.value;
226
- const bx = plotLeft;
262
+ const rawVal = cleanNumber(item[props.valueField]);
263
+ const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
264
+ const bx = rawVal >= 0 ? xZero : xZero - bw;
227
265
  const by = catY + (catHeight - barHeight) / 2;
228
266
  const color = props.colors[0] || "#6366f1";
229
267
  c.fillStyle = isHovered ? color : color + "dd";
@@ -235,13 +273,13 @@ const draw = () => {
235
273
  const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
236
274
  if (!item) return;
237
275
  const isHovered = hoveredIndex.value === bars.length;
238
- const val = cleanNumber(item[props.valueField]) * p;
239
- const bw = val / maxValue.value * plotWidth.value;
276
+ const rawVal = cleanNumber(item[props.valueField]);
277
+ const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
278
+ const bx = rawVal >= 0 ? xZero : xZero - bw;
240
279
  const gCountTotal = groups.value.length;
241
280
  const totalBarsH = gCountTotal * barHeight + (gCountTotal - 1) * 2;
242
281
  const startY = catY + (catHeight - totalBarsH) / 2;
243
282
  const by = startY + gIdx * (barHeight + 2);
244
- const bx = plotLeft;
245
283
  const color = props.colors[gIdx % props.colors.length];
246
284
  c.fillStyle = isHovered ? color : color + "dd";
247
285
  c.fillRect(bx, by, bw, barHeight);
@@ -266,6 +304,7 @@ const draw = () => {
266
304
  if (props.mode === "clustered" && gCount > 1) {
267
305
  barWidth = catWidth * 0.8 / gCount;
268
306
  }
307
+ const yZero = plotTop + plotHeight.value - (0 - minValue.value) / valueRange.value * plotHeight.value;
269
308
  c.save();
270
309
  c.strokeStyle = "rgba(100,100,120,0.1)";
271
310
  c.lineWidth = 1;
@@ -273,14 +312,21 @@ const draw = () => {
273
312
  c.font = "11px system-ui, sans-serif";
274
313
  c.textAlign = "right";
275
314
  for (let i = 0; i <= 5; i++) {
276
- const val = maxValue.value / 5 * i;
277
- const y = plotTop + plotHeight.value - val / maxValue.value * plotHeight.value;
315
+ const val = minValue.value + valueRange.value / 5 * i;
316
+ const y = plotTop + plotHeight.value - (val - minValue.value) / valueRange.value * plotHeight.value;
278
317
  c.beginPath();
279
318
  c.moveTo(plotLeft, y);
280
319
  c.lineTo(plotLeft + plotWidth.value, y);
281
320
  c.stroke();
282
321
  c.fillText(val.toFixed(0), plotLeft - 8, y + 4);
283
322
  }
323
+ if (minValue.value < 0) {
324
+ c.strokeStyle = "rgba(150,150,170,0.4)";
325
+ c.beginPath();
326
+ c.moveTo(plotLeft, yZero);
327
+ c.lineTo(plotLeft + plotWidth.value, yZero);
328
+ c.stroke();
329
+ }
284
330
  c.strokeStyle = "rgba(100,100,120,0.05)";
285
331
  for (let i = 0; i <= catCount; i++) {
286
332
  const x = plotLeft + i * catWidth;
@@ -307,14 +353,22 @@ const draw = () => {
307
353
  const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
308
354
  const catX = plotLeft + catIdx * catWidth;
309
355
  if (props.mode === "stacked") {
310
- let yOffset = 0;
356
+ let yOffsetPos = 0;
357
+ let yOffsetNeg = 0;
311
358
  groups.value.forEach((group, gIdx) => {
312
359
  const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
313
360
  if (!item) return;
314
- const val = cleanNumber(item[props.valueField]) * p;
315
- const bh = val / maxValue.value * plotHeight.value;
361
+ const rawVal = cleanNumber(item[props.valueField]);
362
+ const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
316
363
  const bx = catX + (catWidth - barWidth) / 2;
317
- const by = plotTop + plotHeight.value - (yOffset + val) / maxValue.value * plotHeight.value;
364
+ let by = yZero;
365
+ if (rawVal >= 0) {
366
+ yOffsetPos += rawVal;
367
+ by = yZero - yOffsetPos * p / valueRange.value * plotHeight.value;
368
+ } else {
369
+ by = yZero + yOffsetNeg * p / valueRange.value * plotHeight.value;
370
+ yOffsetNeg += Math.abs(rawVal);
371
+ }
318
372
  const isHovered = hoveredIndex.value === bars.length;
319
373
  const color = props.colors[gIdx % props.colors.length];
320
374
  c.fillStyle = isHovered ? color : color + "dd";
@@ -323,16 +377,15 @@ const draw = () => {
323
377
  c.lineWidth = 1;
324
378
  c.strokeRect(bx, by, barWidth, bh);
325
379
  bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
326
- yOffset += cleanNumber(item[props.valueField]);
327
380
  });
328
381
  } else if (props.mode === "simple") {
329
382
  const item = catItems[0];
330
383
  if (item) {
331
384
  const isHovered = hoveredIndex.value === bars.length;
332
- const val = cleanNumber(item[props.valueField]) * p;
333
- const bh = val / maxValue.value * plotHeight.value;
385
+ const rawVal = cleanNumber(item[props.valueField]);
386
+ const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
334
387
  const bx = catX + (catWidth - barWidth) / 2;
335
- const by = plotTop + plotHeight.value - bh;
388
+ const by = rawVal >= 0 ? yZero - bh : yZero;
336
389
  const color = props.colors[0] || "#6366f1";
337
390
  c.fillStyle = isHovered ? color : color + "dd";
338
391
  c.fillRect(bx, by, barWidth, bh);
@@ -343,13 +396,13 @@ const draw = () => {
343
396
  const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
344
397
  if (!item) return;
345
398
  const isHovered = hoveredIndex.value === bars.length;
346
- const val = cleanNumber(item[props.valueField]) * p;
347
- const bh = val / maxValue.value * plotHeight.value;
399
+ const rawVal = cleanNumber(item[props.valueField]);
400
+ const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
348
401
  const gCountTotal = groups.value.length;
349
402
  const totalBarsW = gCountTotal * barWidth + (gCountTotal - 1) * 2;
350
403
  const startX = catX + (catWidth - totalBarsW) / 2;
351
404
  const bx = startX + gIdx * (barWidth + 2);
352
- const by = plotTop + plotHeight.value - bh;
405
+ const by = rawVal >= 0 ? yZero - bh : yZero;
353
406
  const color = props.colors[gIdx % props.colors.length];
354
407
  c.fillStyle = isHovered ? color : color + "dd";
355
408
  c.fillRect(bx, by, barWidth, bh);
@@ -412,7 +465,7 @@ watch(items, () => {
412
465
  else draw();
413
466
  });
414
467
  watch(scale, (v) => emit("zoom-change", v));
415
- watch(panOffset, (v) => emit("pan-change", v), { deep: true });
468
+ watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
416
469
  onMounted(async () => {
417
470
  await nextTick();
418
471
  if (containerRef.value && canvasRef.value) {
@@ -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;
@@ -42,7 +42,7 @@
42
42
  </template>
43
43
 
44
44
  <script setup>
45
- import { ref, watch, computed, onMounted, nextTick } from "vue";
45
+ import { ref, watch, computed, onMounted, nextTick, isRef } from "vue";
46
46
  import { useChartSvg } from "../../composables/useChartSvg";
47
47
  import { useChartData } from "../../composables/useChartData";
48
48
  import { useChartCanvas } from "../../composables/useChartCanvas";
@@ -68,10 +68,14 @@ const svgRef = ref(null);
68
68
  const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
69
69
  const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
70
70
  const dataRef = computed(() => props.dataProvider);
71
- const { items } = useChartData(dataRef);
71
+ const { items, loading: dataLoading } = useChartData(dataRef);
72
72
  const { exportPng, exportSvg: exportSvgFile } = useChartExport();
73
73
  const isLoading = computed(() => {
74
- return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
74
+ const dp = props.dataProvider;
75
+ if (!dp) return dataLoading.value;
76
+ const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
77
+ const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
78
+ return Boolean(dataLoading.value || rawLoading || rawInitial);
75
79
  });
76
80
  const tooltipStyle = computed(() => ({
77
81
  left: `${tooltipState.value.x}px`,
@@ -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
  colors?: string[];
10
11
  backgroundColor?: string;
@@ -40,6 +41,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
40
41
  seriesField: string;
41
42
  animated: boolean;
42
43
  zoomable: boolean;
44
+ wheelNavigation: boolean;
43
45
  maxZoom: number;
44
46
  colors: string[];
45
47
  backgroundColor: string;