@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"
@@ -17,7 +16,7 @@
17
16
  canvas(ref="canvasRef" v-show="!isLoading")
18
17
  svg.k-chart-svg(v-show="!isLoading" ref="svgRef" :width="width" :height="height")
19
18
  defs
20
- clipPath(id="plot-clip")
19
+ clipPath(:id="'plot-clip-' + clipId")
21
20
  rect(
22
21
  :x="plotLeft"
23
22
  :y="plotTop"
@@ -25,17 +24,16 @@
25
24
  :height="plotHeight.value"
26
25
  )
27
26
  g.k-chart-hits
28
- // Hit detection is now handled by calculation in onMouseMove
27
+ // Hit detection is handled by calculation in onMouseMove
29
28
 
30
29
  // Dynamic Interaction Layer
31
- 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 + ')'")
32
31
  rect.k-chart-hit-proxy(
33
32
  :x="hoveredBar.x"
34
33
  :y="hoveredBar.y"
35
34
  :width="hoveredBar.w"
36
35
  :height="hoveredBar.h"
37
36
  fill="transparent"
38
- style="cursor: pointer"
39
37
  @click="emit('click-point', hoveredBar.item, hoveredIndex)"
40
38
  )
41
39
  rect(
@@ -46,22 +44,24 @@
46
44
  fill="transparent"
47
45
  stroke="#fff"
48
46
  stroke-width="2"
49
- rx="2"
47
+ stroke-dasharray="4,2"
50
48
  pointer-events="none"
51
49
  )
52
- .k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="{ left: tooltipState.x + 'px', top: tooltipState.y + 'px' }")
50
+ .k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
53
51
  slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
54
52
  span {{ tooltipState.content }}
55
53
  </template>
56
54
 
57
55
  <script setup>
58
- import { ref, watch, computed, onMounted, nextTick } from "vue";
56
+ import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
59
57
  import { useChartCanvas } from "../../composables/useChartCanvas";
60
58
  import { useChartSvg } from "../../composables/useChartSvg";
61
59
  import { useChartData } from "../../composables/useChartData";
62
60
  import { useChartViewport } from "../../composables/useChartViewport";
63
61
  import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
64
62
  import { useChartExport } from "../../composables/useChartExport";
63
+ import { cleanNumber, safeMax, safeMin } from "../../composables/chartUtils";
64
+ const clipId = useId();
65
65
  const props = defineProps({
66
66
  dataProvider: { type: null, required: false },
67
67
  categoryField: { type: String, required: false, default: "category" },
@@ -71,6 +71,7 @@ const props = defineProps({
71
71
  mode: { type: String, required: false, default: "clustered" },
72
72
  animated: { type: Boolean, required: false, default: true },
73
73
  zoomable: { type: Boolean, required: false, default: true },
74
+ wheelNavigation: { type: Boolean, required: false, default: true },
74
75
  maxZoom: { type: Number, required: false, default: 8 },
75
76
  colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7"] },
76
77
  backgroundColor: { type: String, required: false, default: "#ffffff" },
@@ -82,19 +83,35 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
82
83
  const containerRef = ref(null);
83
84
  const canvasRef = ref(null);
84
85
  const svgRef = ref(null);
85
- const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
86
+ const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
86
87
  const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
87
88
  const dataRef = computed(() => props.dataProvider);
88
- const { items } = useChartData(dataRef);
89
+ const { items, loading: dataLoading } = useChartData(dataRef);
89
90
  const { progress, animate } = useChartAnimation();
90
91
  const { exportPng, exportSvg: exportSvgFile } = useChartExport();
91
92
  const isLoading = computed(() => {
92
- 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);
93
98
  });
99
+ const tooltipStyle = computed(() => ({
100
+ left: `${tooltipState.value.x}px`,
101
+ top: `${tooltipState.value.y}px`
102
+ }));
94
103
  const maxZoomRef = computed(() => props.maxZoom);
95
104
  const plotWidthRef = computed(() => Math.max(0, width.value - 75));
96
105
  const plotLeftRef = computed(() => 55);
97
- 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
+ });
98
115
  const {
99
116
  scale,
100
117
  panOffset,
@@ -116,15 +133,46 @@ const plotWidth = computed(() => Math.max(0, width.value - PAD.left - PAD.right)
116
133
  const plotHeight = computed(() => Math.max(0, height.value - PAD.top - PAD.bottom));
117
134
  const categories = computed(() => [...new Set(items.value.map((r) => String(r[props.categoryField])))]);
118
135
  const groups = computed(() => [...new Set(items.value.map((r) => String(r[props.groupField] ?? "default")))]);
136
+ const categoryMap = computed(() => {
137
+ const map = /* @__PURE__ */ new Map();
138
+ for (const item of items.value) {
139
+ const cat = String(item[props.categoryField]);
140
+ let list = map.get(cat);
141
+ if (!list) {
142
+ list = [];
143
+ map.set(cat, list);
144
+ }
145
+ list.push(item);
146
+ }
147
+ return map;
148
+ });
149
+ const minValue = computed(() => {
150
+ if (props.mode === "stacked") {
151
+ const negSums = categories.value.map((cat) => {
152
+ const catItems = categoryMap.value.get(cat) || [];
153
+ return catItems.reduce((acc, r) => acc + Math.min(0, cleanNumber(r[props.valueField])), 0);
154
+ });
155
+ const min2 = Math.min(0, safeMin(negSums, 0));
156
+ return isFinite(min2) ? min2 : 0;
157
+ }
158
+ const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
159
+ const min = Math.min(0, safeMin(vals, 0));
160
+ return isFinite(min) ? min : 0;
161
+ });
119
162
  const maxValue = computed(() => {
120
163
  if (props.mode === "stacked") {
121
- const sums = categories.value.map(
122
- (cat) => items.value.filter((r) => String(r[props.categoryField]) === cat).reduce((acc, r) => acc + Math.max(0, Number(r[props.valueField])), 0)
123
- );
124
- return Math.max(...sums, 10);
164
+ const sums = categories.value.map((cat) => {
165
+ const catItems = categoryMap.value.get(cat) || [];
166
+ return catItems.reduce((acc, r) => acc + Math.max(0, cleanNumber(r[props.valueField])), 0);
167
+ });
168
+ return safeMax(sums, 10);
125
169
  }
126
- const vals = items.value.map((r) => Number(r[props.valueField]));
127
- return vals.length ? Math.max(...vals, 10) : 10;
170
+ const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
171
+ return Math.max(0, safeMax(vals, 10));
172
+ });
173
+ const valueRange = computed(() => {
174
+ const span = maxValue.value - minValue.value;
175
+ return span > 0 ? span : 10;
128
176
  });
129
177
  const visibleCatCount = computed(() => Math.max(1, Math.ceil(categories.value.length / scale.value)));
130
178
  const panCatOffset = computed(() => {
@@ -144,117 +192,258 @@ const draw = () => {
144
192
  if (!ctx.value) return;
145
193
  clear(props.backgroundColor);
146
194
  const c = ctx.value;
147
- const p = easeOut(progress.value);
195
+ const p = props.animated ? easeOut(progress.value) : 1;
148
196
  const bars = [];
197
+ const isHorizontal = props.orientation === "horizontal";
149
198
  const catCount = visibleCategories.value.length;
150
199
  const gCount = groups.value.length || 1;
151
- const catWidth = plotWidth.value / (catCount || 1);
152
- let barWidth = catWidth * 0.7;
153
- if (props.mode === "clustered" && gCount > 1) {
154
- barWidth = catWidth * 0.8 / gCount;
155
- }
156
- c.save();
157
- c.strokeStyle = "rgba(100,100,120,0.1)";
158
- c.lineWidth = 1;
159
- c.fillStyle = "rgba(150,150,170,0.8)";
160
- c.font = "11px system-ui, sans-serif";
161
- c.textAlign = "right";
162
- for (let i = 0; i <= 5; i++) {
163
- const val = maxValue.value / 5 * i;
164
- const y = plotTop + plotHeight.value - val / maxValue.value * plotHeight.value;
200
+ if (isHorizontal) {
201
+ const catHeight = plotHeight.value / (catCount || 1);
202
+ let barHeight = catHeight * 0.7;
203
+ if (props.mode === "clustered" && gCount > 1) {
204
+ barHeight = catHeight * 0.8 / gCount;
205
+ }
206
+ const xZero = plotLeft + (0 - minValue.value) / valueRange.value * plotWidth.value;
207
+ c.save();
208
+ c.strokeStyle = "rgba(100,100,120,0.1)";
209
+ c.lineWidth = 1;
210
+ c.fillStyle = "rgba(150,150,170,0.8)";
211
+ c.font = "11px system-ui, sans-serif";
212
+ c.textAlign = "center";
213
+ for (let i = 0; i <= 5; i++) {
214
+ const val = minValue.value + valueRange.value / 5 * i;
215
+ const x = plotLeft + (val - minValue.value) / valueRange.value * plotWidth.value;
216
+ c.beginPath();
217
+ c.moveTo(x, plotTop);
218
+ c.lineTo(x, plotTop + plotHeight.value);
219
+ c.stroke();
220
+ c.fillText(val.toFixed(0), x, plotTop + plotHeight.value + 16);
221
+ }
222
+ if (minValue.value < 0) {
223
+ c.strokeStyle = "rgba(150,150,170,0.4)";
224
+ c.beginPath();
225
+ c.moveTo(xZero, plotTop);
226
+ c.lineTo(xZero, plotTop + plotHeight.value);
227
+ c.stroke();
228
+ }
229
+ c.strokeStyle = "rgba(100,100,120,0.05)";
230
+ for (let i = 0; i <= catCount; i++) {
231
+ const y = plotTop + i * catHeight;
232
+ c.beginPath();
233
+ c.moveTo(plotLeft, y);
234
+ c.lineTo(plotLeft + plotWidth.value, y);
235
+ c.stroke();
236
+ }
237
+ c.strokeStyle = "rgba(150,150,170,0.5)";
165
238
  c.beginPath();
166
- c.moveTo(plotLeft, y);
167
- c.lineTo(plotLeft + plotWidth.value, y);
239
+ c.moveTo(plotLeft, plotTop + plotHeight.value);
240
+ c.lineTo(plotLeft + plotWidth.value, plotTop + plotHeight.value);
168
241
  c.stroke();
169
- c.fillText(val.toFixed(0), plotLeft - 8, y + 4);
170
- }
171
- c.strokeStyle = "rgba(100,100,120,0.05)";
172
- for (let i = 0; i <= catCount; i++) {
173
- const x = plotLeft + i * catWidth;
174
242
  c.beginPath();
175
- c.moveTo(x, plotTop);
176
- c.lineTo(x, plotTop + plotHeight.value);
243
+ c.moveTo(plotLeft, plotTop);
244
+ c.lineTo(plotLeft, plotTop + plotHeight.value);
177
245
  c.stroke();
178
- }
179
- c.strokeStyle = "rgba(150,150,170,0.5)";
180
- c.beginPath();
181
- c.moveTo(plotLeft, plotTop + plotHeight.value);
182
- c.lineTo(plotLeft + plotWidth.value, plotTop + plotHeight.value);
183
- c.stroke();
184
- c.beginPath();
185
- c.moveTo(plotLeft, plotTop);
186
- c.lineTo(plotLeft, plotTop + plotHeight.value);
187
- c.stroke();
188
- c.restore();
189
- c.save();
190
- c.beginPath();
191
- c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value);
192
- c.clip();
193
- visibleCategories.value.forEach((cat, catIdx) => {
194
- const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
195
- const catX = plotLeft + catIdx * catWidth;
196
- if (props.mode === "stacked") {
197
- let yOffset = 0;
198
- groups.value.forEach((group, gIdx) => {
199
- const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
200
- if (!item) return;
201
- const val = Number(item[props.valueField]) * p;
202
- const bh = val / maxValue.value * plotHeight.value;
203
- const bx = catX + (catWidth - barWidth) / 2;
204
- const by = plotTop + plotHeight.value - (yOffset + val) / maxValue.value * plotHeight.value;
205
- const isHovered = hoveredIndex.value === bars.length;
206
- const color = props.colors[gIdx % props.colors.length];
207
- c.fillStyle = isHovered ? color : color + "dd";
208
- c.fillRect(bx, by, barWidth, bh);
209
- c.strokeStyle = "rgba(255,255,255,0.3)";
210
- c.lineWidth = 1;
211
- c.strokeRect(bx, by, barWidth, bh);
212
- bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
213
- yOffset += Number(item[props.valueField]);
214
- });
215
- } else if (props.mode === "simple") {
216
- const item = catItems[0];
217
- if (item) {
218
- const isHovered = hoveredIndex.value === bars.length;
219
- const val = Number(item[props.valueField]) * p;
220
- const bh = val / maxValue.value * plotHeight.value;
221
- const bx = catX + (catWidth - barWidth) / 2;
222
- const by = plotTop + plotHeight.value - bh;
223
- const color = props.colors[0] || "#6366f1";
224
- c.fillStyle = isHovered ? color : color + "dd";
225
- c.fillRect(bx, by, barWidth, bh);
226
- bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat}: ${item[props.valueField]}` });
246
+ c.restore();
247
+ c.save();
248
+ c.beginPath();
249
+ c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value);
250
+ c.clip();
251
+ visibleCategories.value.forEach((cat, catIdx) => {
252
+ const catItems = categoryMap.value.get(cat) || [];
253
+ const catY = plotTop + catIdx * catHeight;
254
+ if (props.mode === "stacked") {
255
+ let xOffsetPos = 0;
256
+ let xOffsetNeg = 0;
257
+ groups.value.forEach((group, gIdx) => {
258
+ const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
259
+ if (!item) return;
260
+ const rawVal = cleanNumber(item[props.valueField]);
261
+ const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
262
+ let bx = xZero;
263
+ if (rawVal >= 0) {
264
+ bx = xZero + xOffsetPos * p / valueRange.value * plotWidth.value;
265
+ xOffsetPos += rawVal;
266
+ } else {
267
+ xOffsetNeg += Math.abs(rawVal);
268
+ bx = xZero - xOffsetNeg * p / valueRange.value * plotWidth.value;
269
+ }
270
+ const by = catY + (catHeight - barHeight) / 2;
271
+ const isHovered = hoveredIndex.value === bars.length;
272
+ const color = props.colors[gIdx % props.colors.length];
273
+ c.fillStyle = isHovered ? color : color + "dd";
274
+ c.fillRect(bx, by, bw, barHeight);
275
+ c.strokeStyle = "rgba(255,255,255,0.3)";
276
+ c.lineWidth = 1;
277
+ c.strokeRect(bx, by, bw, barHeight);
278
+ bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat} (${group}): ${item[props.valueField]}` });
279
+ });
280
+ } else if (props.mode === "simple") {
281
+ const item = catItems[0];
282
+ if (item) {
283
+ const isHovered = hoveredIndex.value === bars.length;
284
+ const rawVal = cleanNumber(item[props.valueField]);
285
+ const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
286
+ const bx = rawVal >= 0 ? xZero : xZero - bw;
287
+ const by = catY + (catHeight - barHeight) / 2;
288
+ const color = props.colors[0] || "#6366f1";
289
+ c.fillStyle = isHovered ? color : color + "dd";
290
+ c.fillRect(bx, by, bw, barHeight);
291
+ bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat}: ${item[props.valueField]}` });
292
+ }
293
+ } else {
294
+ groups.value.forEach((group, gIdx) => {
295
+ const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
296
+ if (!item) return;
297
+ const isHovered = hoveredIndex.value === bars.length;
298
+ const rawVal = cleanNumber(item[props.valueField]);
299
+ const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
300
+ const bx = rawVal >= 0 ? xZero : xZero - bw;
301
+ const gCountTotal = groups.value.length;
302
+ const totalBarsH = gCountTotal * barHeight + (gCountTotal - 1) * 2;
303
+ const startY = catY + (catHeight - totalBarsH) / 2;
304
+ const by = startY + gIdx * (barHeight + 2);
305
+ const color = props.colors[gIdx % props.colors.length];
306
+ c.fillStyle = isHovered ? color : color + "dd";
307
+ c.fillRect(bx, by, bw, barHeight);
308
+ bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat} (${group}): ${item[props.valueField]}` });
309
+ });
227
310
  }
228
- } else {
229
- groups.value.forEach((group, gIdx) => {
230
- const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
231
- if (!item) return;
232
- const isHovered = hoveredIndex.value === bars.length;
233
- const val = Number(item[props.valueField]) * p;
234
- const bh = val / maxValue.value * plotHeight.value;
235
- const gCountTotal = groups.value.length;
236
- const totalBarsW = gCountTotal * barWidth + (gCountTotal - 1) * 2;
237
- const startX = catX + (catWidth - totalBarsW) / 2;
238
- const bx = startX + gIdx * (barWidth + 2);
239
- const by = plotTop + plotHeight.value - bh;
240
- const color = props.colors[gIdx % props.colors.length];
241
- c.fillStyle = isHovered ? color : color + "dd";
242
- c.fillRect(bx, by, barWidth, bh);
243
- bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
244
- });
311
+ });
312
+ c.restore();
313
+ c.save();
314
+ c.fillStyle = "rgba(150,150,170,0.9)";
315
+ c.font = "11px system-ui, sans-serif";
316
+ c.textAlign = "right";
317
+ visibleCategories.value.forEach((cat, catIdx) => {
318
+ const catY = plotTop + catIdx * catHeight;
319
+ const label = cat.length > 10 ? cat.slice(0, 10) + "\u2026" : cat;
320
+ c.fillText(label, plotLeft - 8, catY + catHeight / 2 + 4);
321
+ });
322
+ c.restore();
323
+ } else {
324
+ const catWidth = plotWidth.value / (catCount || 1);
325
+ let barWidth = catWidth * 0.7;
326
+ if (props.mode === "clustered" && gCount > 1) {
327
+ barWidth = catWidth * 0.8 / gCount;
245
328
  }
246
- });
247
- c.restore();
248
- c.save();
249
- c.fillStyle = "rgba(150,150,170,0.9)";
250
- c.font = "11px system-ui, sans-serif";
251
- c.textAlign = "center";
252
- visibleCategories.value.forEach((cat, catIdx) => {
253
- const catX = plotLeft + catIdx * catWidth;
254
- const label = cat.length > 10 ? cat.slice(0, 10) + "\u2026" : cat;
255
- c.fillText(label, catX + catWidth / 2, plotTop + plotHeight.value + 18);
256
- });
257
- c.restore();
329
+ const yZero = plotTop + plotHeight.value - (0 - minValue.value) / valueRange.value * plotHeight.value;
330
+ c.save();
331
+ c.strokeStyle = "rgba(100,100,120,0.1)";
332
+ c.lineWidth = 1;
333
+ c.fillStyle = "rgba(150,150,170,0.8)";
334
+ c.font = "11px system-ui, sans-serif";
335
+ c.textAlign = "right";
336
+ for (let i = 0; i <= 5; i++) {
337
+ const val = minValue.value + valueRange.value / 5 * i;
338
+ const y = plotTop + plotHeight.value - (val - minValue.value) / valueRange.value * plotHeight.value;
339
+ c.beginPath();
340
+ c.moveTo(plotLeft, y);
341
+ c.lineTo(plotLeft + plotWidth.value, y);
342
+ c.stroke();
343
+ c.fillText(val.toFixed(0), plotLeft - 8, y + 4);
344
+ }
345
+ if (minValue.value < 0) {
346
+ c.strokeStyle = "rgba(150,150,170,0.4)";
347
+ c.beginPath();
348
+ c.moveTo(plotLeft, yZero);
349
+ c.lineTo(plotLeft + plotWidth.value, yZero);
350
+ c.stroke();
351
+ }
352
+ c.strokeStyle = "rgba(100,100,120,0.05)";
353
+ for (let i = 0; i <= catCount; i++) {
354
+ const x = plotLeft + i * catWidth;
355
+ c.beginPath();
356
+ c.moveTo(x, plotTop);
357
+ c.lineTo(x, plotTop + plotHeight.value);
358
+ c.stroke();
359
+ }
360
+ c.strokeStyle = "rgba(150,150,170,0.5)";
361
+ c.beginPath();
362
+ c.moveTo(plotLeft, plotTop + plotHeight.value);
363
+ c.lineTo(plotLeft + plotWidth.value, plotTop + plotHeight.value);
364
+ c.stroke();
365
+ c.beginPath();
366
+ c.moveTo(plotLeft, plotTop);
367
+ c.lineTo(plotLeft, plotTop + plotHeight.value);
368
+ c.stroke();
369
+ c.restore();
370
+ c.save();
371
+ c.beginPath();
372
+ c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value);
373
+ c.clip();
374
+ visibleCategories.value.forEach((cat, catIdx) => {
375
+ const catItems = categoryMap.value.get(cat) || [];
376
+ const catX = plotLeft + catIdx * catWidth;
377
+ if (props.mode === "stacked") {
378
+ let yOffsetPos = 0;
379
+ let yOffsetNeg = 0;
380
+ groups.value.forEach((group, gIdx) => {
381
+ const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
382
+ if (!item) return;
383
+ const rawVal = cleanNumber(item[props.valueField]);
384
+ const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
385
+ const bx = catX + (catWidth - barWidth) / 2;
386
+ let by = yZero;
387
+ if (rawVal >= 0) {
388
+ yOffsetPos += rawVal;
389
+ by = yZero - yOffsetPos * p / valueRange.value * plotHeight.value;
390
+ } else {
391
+ by = yZero + yOffsetNeg * p / valueRange.value * plotHeight.value;
392
+ yOffsetNeg += Math.abs(rawVal);
393
+ }
394
+ const isHovered = hoveredIndex.value === bars.length;
395
+ const color = props.colors[gIdx % props.colors.length];
396
+ c.fillStyle = isHovered ? color : color + "dd";
397
+ c.fillRect(bx, by, barWidth, bh);
398
+ c.strokeStyle = "rgba(255,255,255,0.3)";
399
+ c.lineWidth = 1;
400
+ c.strokeRect(bx, by, barWidth, bh);
401
+ bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
402
+ });
403
+ } else if (props.mode === "simple") {
404
+ const item = catItems[0];
405
+ if (item) {
406
+ const isHovered = hoveredIndex.value === bars.length;
407
+ const rawVal = cleanNumber(item[props.valueField]);
408
+ const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
409
+ const bx = catX + (catWidth - barWidth) / 2;
410
+ const by = rawVal >= 0 ? yZero - bh : yZero;
411
+ const color = props.colors[0] || "#6366f1";
412
+ c.fillStyle = isHovered ? color : color + "dd";
413
+ c.fillRect(bx, by, barWidth, bh);
414
+ bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat}: ${item[props.valueField]}` });
415
+ }
416
+ } else {
417
+ groups.value.forEach((group, gIdx) => {
418
+ const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
419
+ if (!item) return;
420
+ const isHovered = hoveredIndex.value === bars.length;
421
+ const rawVal = cleanNumber(item[props.valueField]);
422
+ const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
423
+ const gCountTotal = groups.value.length;
424
+ const totalBarsW = gCountTotal * barWidth + (gCountTotal - 1) * 2;
425
+ const startX = catX + (catWidth - totalBarsW) / 2;
426
+ const bx = startX + gIdx * (barWidth + 2);
427
+ const by = rawVal >= 0 ? yZero - bh : yZero;
428
+ const color = props.colors[gIdx % props.colors.length];
429
+ c.fillStyle = isHovered ? color : color + "dd";
430
+ c.fillRect(bx, by, barWidth, bh);
431
+ bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
432
+ });
433
+ }
434
+ });
435
+ c.restore();
436
+ c.save();
437
+ c.fillStyle = "rgba(150,150,170,0.9)";
438
+ c.font = "11px system-ui, sans-serif";
439
+ c.textAlign = "center";
440
+ visibleCategories.value.forEach((cat, catIdx) => {
441
+ const catX = plotLeft + catIdx * catWidth;
442
+ const label = cat.length > 10 ? cat.slice(0, 10) + "\u2026" : cat;
443
+ c.fillText(label, catX + catWidth / 2, plotTop + plotHeight.value + 18);
444
+ });
445
+ c.restore();
446
+ }
258
447
  c.save();
259
448
  c.fillStyle = "rgba(120,120,140,0.8)";
260
449
  if (props.xAxisTitle) {
@@ -291,12 +480,14 @@ const drawLegend = (c) => {
291
480
  });
292
481
  c.restore();
293
482
  };
294
- watch([items, scale, panOffset, hoveredIndex, progress, () => props.mode, () => props.orientation], draw, { deep: true });
483
+ watch(progress, draw);
484
+ watch([scale, panOffset, hoveredIndex, resizeTick, width, height, () => props.mode, () => props.orientation], draw);
295
485
  watch(items, () => {
296
486
  if (props.animated) animate();
297
- }, { deep: true });
487
+ else draw();
488
+ });
298
489
  watch(scale, (v) => emit("zoom-change", v));
299
- watch(panOffset, (v) => emit("pan-change", v), { deep: true });
490
+ watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
300
491
  onMounted(async () => {
301
492
  await nextTick();
302
493
  if (containerRef.value && canvasRef.value) {
@@ -339,7 +530,7 @@ defineExpose({
339
530
  zoomOut: viewport.zoomOut,
340
531
  resetZoom: viewport.resetZoom,
341
532
  exportPng: () => canvasRef.value && exportPng(canvasRef.value, "bar-chart.png"),
342
- exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "bar-chart.svg")
533
+ exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "bar-chart.svg", canvasRef.value)
343
534
  });
344
535
  </script>
345
536
 
@@ -347,6 +538,13 @@ defineExpose({
347
538
  .k-chart-wrapper {
348
539
  position: relative;
349
540
  user-select: none;
541
+ cursor: default;
542
+ }
543
+ .k-chart-wrapper.k-chart--pannable {
544
+ cursor: grab;
545
+ }
546
+ .k-chart-wrapper.k-chart--grabbing {
547
+ cursor: grabbing;
350
548
  }
351
549
  .k-chart-wrapper .k-chart-controls {
352
550
  position: absolute;
@@ -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;