@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.
- package/LICENSE +200 -0
- package/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/components/KAreaChart/KAreaChart.d.vue.ts +2 -0
- package/dist/runtime/components/KAreaChart/KAreaChart.vue +271 -69
- package/dist/runtime/components/KAreaChart/KAreaChart.vue.d.ts +2 -0
- package/dist/runtime/components/KBarChart/KBarChart.d.vue.ts +2 -0
- package/dist/runtime/components/KBarChart/KBarChart.vue +323 -125
- package/dist/runtime/components/KBarChart/KBarChart.vue.d.ts +2 -0
- package/dist/runtime/components/KHeatMap/KHeatMap.vue +56 -21
- package/dist/runtime/components/KLineChart/KLineChart.d.vue.ts +2 -0
- package/dist/runtime/components/KLineChart/KLineChart.vue +179 -48
- package/dist/runtime/components/KLineChart/KLineChart.vue.d.ts +2 -0
- package/dist/runtime/components/KPieChart/KPieChart.vue +32 -29
- package/dist/runtime/components/KScatterChart/KScatterChart.d.vue.ts +3 -1
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +161 -49
- package/dist/runtime/components/KScatterChart/KScatterChart.vue.d.ts +3 -1
- package/dist/runtime/composables/chartUtils.d.ts +8 -0
- package/dist/runtime/composables/chartUtils.js +58 -0
- package/dist/runtime/composables/useChartAnimation.js +7 -1
- package/dist/runtime/composables/useChartAxes.d.ts +4 -2
- package/dist/runtime/composables/useChartAxes.js +32 -13
- package/dist/runtime/composables/useChartCanvas.d.ts +1 -0
- package/dist/runtime/composables/useChartCanvas.js +51 -18
- package/dist/runtime/composables/useChartData.d.ts +13 -7
- package/dist/runtime/composables/useChartData.js +99 -30
- package/dist/runtime/composables/useChartExport.d.ts +1 -1
- package/dist/runtime/composables/useChartExport.js +26 -3
- package/dist/runtime/composables/useChartHitTest.d.ts +2 -2
- package/dist/runtime/composables/useChartHitTest.js +7 -5
- package/dist/runtime/composables/useChartViewport.d.ts +3 -0
- package/dist/runtime/composables/useChartViewport.js +30 -7
- 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
|
-
|
|
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
|
|
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
|
-
|
|
47
|
+
stroke-dasharray="4,2"
|
|
50
48
|
pointer-events="none"
|
|
51
49
|
)
|
|
52
|
-
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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) =>
|
|
127
|
-
return
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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,
|
|
167
|
-
c.lineTo(plotLeft + plotWidth.value,
|
|
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(
|
|
176
|
-
c.lineTo(
|
|
243
|
+
c.moveTo(plotLeft, plotTop);
|
|
244
|
+
c.lineTo(plotLeft, plotTop + plotHeight.value);
|
|
177
245
|
c.stroke();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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(
|
|
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
|
-
|
|
487
|
+
else draw();
|
|
488
|
+
});
|
|
298
489
|
watch(scale, (v) => emit("zoom-change", v));
|
|
299
|
-
watch(panOffset, (
|
|
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;
|