@katlux/block-charts 0.1.1-alpha.87 → 0.1.1-alpha.89
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/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/KAreaChart/KAreaChart.vue +233 -60
- package/dist/runtime/components/KBarChart/KBarChart.vue +237 -114
- package/dist/runtime/components/KHeatMap/KHeatMap.vue +28 -15
- package/dist/runtime/components/KLineChart/KLineChart.vue +149 -41
- package/dist/runtime/components/KPieChart/KPieChart.vue +25 -26
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +84 -32
- package/dist/runtime/composables/chartUtils.d.ts +8 -0
- package/dist/runtime/composables/chartUtils.js +58 -0
- package/dist/runtime/composables/useChartAxes.d.ts +3 -1
- package/dist/runtime/composables/useChartAxes.js +26 -9
- package/dist/runtime/composables/useChartData.d.ts +4 -3
- package/dist/runtime/composables/useChartData.js +19 -18
- package/dist/runtime/composables/useChartExport.d.ts +1 -1
- package/dist/runtime/composables/useChartExport.js +18 -2
- package/dist/runtime/composables/useChartHitTest.d.ts +1 -1
- package/dist/runtime/composables/useChartHitTest.js +4 -3
- package/package.json +1 -1
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
.k-chart-wrapper(
|
|
3
3
|
ref="wrapperRef"
|
|
4
4
|
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
5
|
-
:style="{ cursor: isDragging ? 'grabbing' : canPan ? 'grab' : 'default' }"
|
|
6
5
|
@wheel.prevent="onWheel"
|
|
7
6
|
@mousedown="onMouseDown"
|
|
8
7
|
@mousemove="onMouseMove"
|
|
@@ -39,7 +38,6 @@
|
|
|
39
38
|
:cy="pt.y"
|
|
40
39
|
:r="10"
|
|
41
40
|
fill="transparent"
|
|
42
|
-
style="cursor: pointer"
|
|
43
41
|
@click="emit('click-point', pt.item, pt.index)"
|
|
44
42
|
)
|
|
45
43
|
// Visible indicators
|
|
@@ -57,7 +55,7 @@
|
|
|
57
55
|
// Tooltip
|
|
58
56
|
.k-chart-tooltip(
|
|
59
57
|
v-if="tooltipState.visible && !isLoading"
|
|
60
|
-
:style="
|
|
58
|
+
:style="tooltipStyle"
|
|
61
59
|
)
|
|
62
60
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
63
61
|
span {{ tooltipState.content }}
|
|
@@ -92,7 +90,7 @@ const wrapperRef = ref(null);
|
|
|
92
90
|
const containerRef = ref(null);
|
|
93
91
|
const canvasRef = ref(null);
|
|
94
92
|
const svgRef = ref(null);
|
|
95
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
93
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
96
94
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
97
95
|
const dataRef = computed(() => props.dataProvider);
|
|
98
96
|
const { items, loading } = useChartData(dataRef);
|
|
@@ -101,6 +99,10 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
|
101
99
|
const isLoading = computed(() => {
|
|
102
100
|
return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
|
|
103
101
|
});
|
|
102
|
+
const tooltipStyle = computed(() => ({
|
|
103
|
+
left: `${tooltipState.value.x}px`,
|
|
104
|
+
top: `${tooltipState.value.y}px`
|
|
105
|
+
}));
|
|
104
106
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
105
107
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
106
108
|
const plotLeftRef = computed(() => 55);
|
|
@@ -120,8 +122,45 @@ const {
|
|
|
120
122
|
onMouseUp,
|
|
121
123
|
onMouseLeave: onViewportMouseLeave
|
|
122
124
|
} = viewport;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
+
const xAxisType = computed(() => {
|
|
126
|
+
if (!items.value.length) return "linear";
|
|
127
|
+
const firstVal = items.value[0]?.[props.xField];
|
|
128
|
+
if (typeof firstVal === "number") return "linear";
|
|
129
|
+
if (typeof firstVal === "string") {
|
|
130
|
+
const parsed = Date.parse(firstVal);
|
|
131
|
+
if (!isNaN(parsed) && !/^\d+$/.test(firstVal.trim())) return "datetime";
|
|
132
|
+
const num = Number(firstVal);
|
|
133
|
+
if (!isNaN(num)) return "linear";
|
|
134
|
+
return "category";
|
|
135
|
+
}
|
|
136
|
+
return "linear";
|
|
137
|
+
});
|
|
138
|
+
const categoryList = computed(() => {
|
|
139
|
+
if (xAxisType.value !== "category") return [];
|
|
140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
141
|
+
const list = [];
|
|
142
|
+
for (const item of items.value) {
|
|
143
|
+
const val = String(item[props.xField] ?? "");
|
|
144
|
+
if (!seen.has(val)) {
|
|
145
|
+
seen.add(val);
|
|
146
|
+
list.push(val);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return list;
|
|
150
|
+
});
|
|
151
|
+
const categoryIndexMap = computed(() => {
|
|
152
|
+
const map = {};
|
|
153
|
+
categoryList.value.forEach((cat, i) => {
|
|
154
|
+
map[cat] = i;
|
|
155
|
+
});
|
|
156
|
+
return map;
|
|
157
|
+
});
|
|
158
|
+
const resolveX = (item, index = 0) => {
|
|
159
|
+
if (!item) return index;
|
|
160
|
+
const raw = item[props.xField];
|
|
161
|
+
if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
|
|
162
|
+
return parseValue(raw, index);
|
|
163
|
+
};
|
|
125
164
|
const seriesMap = computed(() => {
|
|
126
165
|
const map = /* @__PURE__ */ new Map();
|
|
127
166
|
for (const item of items.value) {
|
|
@@ -131,6 +170,24 @@ const seriesMap = computed(() => {
|
|
|
131
170
|
}
|
|
132
171
|
return map;
|
|
133
172
|
});
|
|
173
|
+
const sortedSeriesMap = computed(() => {
|
|
174
|
+
const map = /* @__PURE__ */ new Map();
|
|
175
|
+
const itemIndexMap = /* @__PURE__ */ new Map();
|
|
176
|
+
for (let i = 0; i < items.value.length; i++) {
|
|
177
|
+
itemIndexMap.set(items.value[i], i);
|
|
178
|
+
}
|
|
179
|
+
for (const [key, seriesItems] of seriesMap.value) {
|
|
180
|
+
const enriched = seriesItems.map((item, idx) => ({
|
|
181
|
+
item,
|
|
182
|
+
index: itemIndexMap.get(item) ?? idx,
|
|
183
|
+
xVal: resolveX(item, idx)
|
|
184
|
+
})).sort((a, b) => a.xVal - b.xVal);
|
|
185
|
+
map.set(key, enriched);
|
|
186
|
+
}
|
|
187
|
+
return map;
|
|
188
|
+
});
|
|
189
|
+
const xFieldRef = computed(() => props.xField);
|
|
190
|
+
const yFieldRef = computed(() => props.yField);
|
|
134
191
|
const axes = useChartAxes({
|
|
135
192
|
items,
|
|
136
193
|
xField: xFieldRef,
|
|
@@ -139,13 +196,14 @@ const axes = useChartAxes({
|
|
|
139
196
|
height,
|
|
140
197
|
scale,
|
|
141
198
|
panOffset,
|
|
142
|
-
padding: { top: 20, right: 20, bottom: 40, left: 55 }
|
|
199
|
+
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
200
|
+
xValueGetter: (r, i) => resolveX(r, i)
|
|
143
201
|
});
|
|
144
202
|
const draw = () => {
|
|
145
203
|
if (!ctx.value) return;
|
|
146
204
|
clear(props.backgroundColor);
|
|
147
205
|
const c = ctx.value;
|
|
148
|
-
const p = easeOut(progress.value);
|
|
206
|
+
const p = props.animated ? easeOut(progress.value) : 1;
|
|
149
207
|
drawGrid(c);
|
|
150
208
|
drawAxes(c);
|
|
151
209
|
c.save();
|
|
@@ -153,9 +211,9 @@ const draw = () => {
|
|
|
153
211
|
c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
|
|
154
212
|
c.clip();
|
|
155
213
|
let seriesIdx = 0;
|
|
156
|
-
for (const [,
|
|
214
|
+
for (const [, seriesData] of sortedSeriesMap.value) {
|
|
157
215
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
158
|
-
drawLine(c,
|
|
216
|
+
drawLine(c, seriesData, color, p);
|
|
159
217
|
seriesIdx++;
|
|
160
218
|
}
|
|
161
219
|
c.restore();
|
|
@@ -220,13 +278,36 @@ const drawAxes = (c) => {
|
|
|
220
278
|
c.fillText(val.toFixed(1), axes.plotLeft - 8, y + 4);
|
|
221
279
|
}
|
|
222
280
|
c.textAlign = "center";
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
const xSteps = 5;
|
|
295
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
296
|
+
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / xSteps);
|
|
297
|
+
const x = axes.toX(val);
|
|
298
|
+
let label;
|
|
299
|
+
if (xAxisType.value === "datetime") {
|
|
300
|
+
const allMidnight = items.value.every((r) => {
|
|
301
|
+
const ts = parseValue(r[props.xField]);
|
|
302
|
+
const d = new Date(ts);
|
|
303
|
+
return d.getHours() === 0 && d.getMinutes() === 0;
|
|
304
|
+
});
|
|
305
|
+
label = allMidnight ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
306
|
+
} else {
|
|
307
|
+
label = val.toFixed(1);
|
|
308
|
+
}
|
|
309
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
310
|
+
}
|
|
230
311
|
}
|
|
231
312
|
c.beginPath();
|
|
232
313
|
c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
@@ -234,7 +315,7 @@ const drawAxes = (c) => {
|
|
|
234
315
|
c.stroke();
|
|
235
316
|
c.beginPath();
|
|
236
317
|
c.moveTo(axes.plotLeft, axes.plotTop);
|
|
237
|
-
c.lineTo(axes.plotLeft
|
|
318
|
+
c.lineTo(axes.plotLeft + axes.plotHeight.value);
|
|
238
319
|
c.stroke();
|
|
239
320
|
c.fillStyle = "rgba(120,120,140,0.8)";
|
|
240
321
|
if (props.xAxisTitle) {
|
|
@@ -253,8 +334,7 @@ const drawAxes = (c) => {
|
|
|
253
334
|
};
|
|
254
335
|
const drawLine = (c, data, color, progress2) => {
|
|
255
336
|
if (!data.length) return;
|
|
256
|
-
const
|
|
257
|
-
const totalPts = sorted.length;
|
|
337
|
+
const totalPts = data.length;
|
|
258
338
|
const drawCount = Math.max(1, Math.floor(totalPts * progress2));
|
|
259
339
|
c.save();
|
|
260
340
|
c.strokeStyle = color;
|
|
@@ -262,18 +342,20 @@ const drawLine = (c, data, color, progress2) => {
|
|
|
262
342
|
c.lineJoin = "round";
|
|
263
343
|
c.lineCap = "round";
|
|
264
344
|
c.beginPath();
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const
|
|
345
|
+
for (let i = 0; i < drawCount; i++) {
|
|
346
|
+
const pt = data[i];
|
|
347
|
+
const x = axes.toX(pt.xVal);
|
|
348
|
+
const y = axes.toY(parseValue(pt.item[props.yField]));
|
|
268
349
|
if (i === 0) c.moveTo(x, y);
|
|
269
350
|
else c.lineTo(x, y);
|
|
270
|
-
}
|
|
351
|
+
}
|
|
271
352
|
c.stroke();
|
|
353
|
+
const currentHoveredIdx = hoveredIndex.value;
|
|
272
354
|
for (let i = 0; i < drawCount; i++) {
|
|
273
|
-
const
|
|
274
|
-
const x = axes.toX(
|
|
275
|
-
const y = axes.toY(parseValue(item[props.yField]));
|
|
276
|
-
const isHovered =
|
|
355
|
+
const pt = data[i];
|
|
356
|
+
const x = axes.toX(pt.xVal);
|
|
357
|
+
const y = axes.toY(parseValue(pt.item[props.yField]));
|
|
358
|
+
const isHovered = currentHoveredIdx !== -1 && pt.index === currentHoveredIdx;
|
|
277
359
|
c.beginPath();
|
|
278
360
|
c.arc(x, y, isHovered ? 5 : 3, 0, Math.PI * 2);
|
|
279
361
|
c.fillStyle = isHovered ? "#fff" : color;
|
|
@@ -284,10 +366,12 @@ const drawLine = (c, data, color, progress2) => {
|
|
|
284
366
|
}
|
|
285
367
|
c.restore();
|
|
286
368
|
};
|
|
287
|
-
watch(
|
|
369
|
+
watch(progress, draw);
|
|
370
|
+
watch([scale, panOffset, hoveredIndex, resizeTick, width, height], draw);
|
|
288
371
|
watch(items, () => {
|
|
289
372
|
if (props.animated) animate();
|
|
290
|
-
|
|
373
|
+
else draw();
|
|
374
|
+
});
|
|
291
375
|
watch(scale, (v) => emit("zoom-change", v));
|
|
292
376
|
watch(panOffset, (v) => emit("pan-change", v), { deep: true });
|
|
293
377
|
onMounted(async () => {
|
|
@@ -300,20 +384,24 @@ onMounted(async () => {
|
|
|
300
384
|
});
|
|
301
385
|
const hoveredPoints = computed(() => {
|
|
302
386
|
if (hoveredIndex.value === -1) return [];
|
|
387
|
+
if (hoveredIndex.value >= items.value.length) {
|
|
388
|
+
setHovered(-1);
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
303
391
|
const item = items.value[hoveredIndex.value];
|
|
304
|
-
const xVal =
|
|
392
|
+
const xVal = resolveX(item);
|
|
305
393
|
const xPx = axes.toX(xVal);
|
|
306
394
|
const pts = [];
|
|
307
395
|
let seriesIdx = 0;
|
|
308
|
-
for (const [
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
396
|
+
for (const [, seriesList] of sortedSeriesMap.value) {
|
|
397
|
+
const found = seriesList.find((pt) => pt.xVal === xVal);
|
|
398
|
+
if (found) {
|
|
311
399
|
pts.push({
|
|
312
400
|
x: xPx,
|
|
313
|
-
y: axes.toY(parseValue(
|
|
401
|
+
y: axes.toY(parseValue(found.item[props.yField])),
|
|
314
402
|
color: props.colors[seriesIdx % props.colors.length],
|
|
315
|
-
item:
|
|
316
|
-
index:
|
|
403
|
+
item: found.item,
|
|
404
|
+
index: found.index
|
|
317
405
|
});
|
|
318
406
|
}
|
|
319
407
|
seriesIdx++;
|
|
@@ -331,18 +419,31 @@ const onMouseMove = (e) => {
|
|
|
331
419
|
if (!rect) return;
|
|
332
420
|
const mouseX = e.clientX - rect.left;
|
|
333
421
|
const mouseY = e.clientY - rect.top;
|
|
334
|
-
|
|
422
|
+
if (mouseX < axes.plotLeft || mouseX > axes.plotLeft + axes.plotWidth.value || mouseY < axes.plotTop || mouseY > axes.plotTop + axes.plotHeight.value) {
|
|
423
|
+
setHovered(-1);
|
|
424
|
+
hideTooltip();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const nearest = findNearestX(mouseX, items.value, axes.toX, (r, i) => resolveX(r, i));
|
|
335
428
|
if (nearest) {
|
|
336
429
|
setHovered(nearest.index);
|
|
337
|
-
showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
|
|
338
430
|
emit("hover-point", nearest.item, nearest.index);
|
|
431
|
+
const xPx = axes.toX(resolveX(nearest.item, nearest.index));
|
|
432
|
+
const yPx = axes.toY(parseValue(nearest.item[props.yField]));
|
|
433
|
+
showTooltip(
|
|
434
|
+
xPx,
|
|
435
|
+
yPx - 10,
|
|
436
|
+
`${props.xField}: ${nearest.item[props.xField]} | ${props.yField}: ${nearest.item[props.yField]}`,
|
|
437
|
+
nearest.item,
|
|
438
|
+
nearest.index
|
|
439
|
+
);
|
|
339
440
|
} else {
|
|
340
441
|
setHovered(-1);
|
|
341
442
|
hideTooltip();
|
|
342
443
|
}
|
|
343
444
|
};
|
|
344
445
|
const onMouseLeave = (e) => {
|
|
345
|
-
onViewportMouseLeave(
|
|
446
|
+
onViewportMouseLeave();
|
|
346
447
|
setHovered(-1);
|
|
347
448
|
hideTooltip();
|
|
348
449
|
};
|
|
@@ -351,7 +452,7 @@ defineExpose({
|
|
|
351
452
|
zoomOut: viewport.zoomOut,
|
|
352
453
|
resetZoom: viewport.resetZoom,
|
|
353
454
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "line-chart.png"),
|
|
354
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "line-chart.svg")
|
|
455
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "line-chart.svg", canvasRef.value)
|
|
355
456
|
});
|
|
356
457
|
</script>
|
|
357
458
|
|
|
@@ -359,6 +460,13 @@ defineExpose({
|
|
|
359
460
|
.k-chart-wrapper {
|
|
360
461
|
position: relative;
|
|
361
462
|
user-select: none;
|
|
463
|
+
cursor: default;
|
|
464
|
+
}
|
|
465
|
+
.k-chart-wrapper.k-chart--pannable {
|
|
466
|
+
cursor: grab;
|
|
467
|
+
}
|
|
468
|
+
.k-chart-wrapper.k-chart--grabbing {
|
|
469
|
+
cursor: grabbing;
|
|
362
470
|
}
|
|
363
471
|
.k-chart-wrapper .k-chart-controls {
|
|
364
472
|
position: absolute;
|
|
@@ -9,14 +9,13 @@
|
|
|
9
9
|
canvas(ref="canvasRef" v-show="!isLoading")
|
|
10
10
|
svg.k-chart-svg(v-show="!isLoading" ref="svgRef" :width="width" :height="height")
|
|
11
11
|
g.k-chart-hits
|
|
12
|
-
// Hit detection
|
|
12
|
+
// Hit detection handled via angle calculation in onMouseMove
|
|
13
13
|
|
|
14
14
|
// Dynamic Interaction Layer
|
|
15
15
|
g.k-chart-interaction(v-if="hoveredIndex !== -1 && hoveredSeg")
|
|
16
16
|
path.k-chart-hit-proxy(
|
|
17
17
|
:d="hoveredSeg.d"
|
|
18
18
|
fill="transparent"
|
|
19
|
-
style="cursor: pointer"
|
|
20
19
|
@click="emit('click-point', hoveredSeg.item, hoveredIndex)"
|
|
21
20
|
)
|
|
22
21
|
path(
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
stroke-width="3"
|
|
27
26
|
pointer-events="none"
|
|
28
27
|
)
|
|
29
|
-
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="
|
|
28
|
+
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
|
|
30
29
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
31
30
|
span {{ tooltipState.content }}
|
|
32
31
|
</template>
|
|
@@ -38,6 +37,7 @@ import { useChartSvg } from "../../composables/useChartSvg";
|
|
|
38
37
|
import { useChartData } from "../../composables/useChartData";
|
|
39
38
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
40
39
|
import { useChartExport } from "../../composables/useChartExport";
|
|
40
|
+
import { cleanNumber } from "../../composables/chartUtils";
|
|
41
41
|
const props = defineProps({
|
|
42
42
|
dataProvider: { type: null, required: false },
|
|
43
43
|
labelField: { type: String, required: false, default: "label" },
|
|
@@ -53,7 +53,7 @@ const emit = defineEmits(["click-point", "hover-point"]);
|
|
|
53
53
|
const containerRef = ref(null);
|
|
54
54
|
const canvasRef = ref(null);
|
|
55
55
|
const svgRef = ref(null);
|
|
56
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
56
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
57
57
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
58
58
|
const dataRef = computed(() => props.dataProvider);
|
|
59
59
|
const { items } = useChartData(dataRef);
|
|
@@ -62,8 +62,12 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
|
62
62
|
const isLoading = computed(() => {
|
|
63
63
|
return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
|
|
64
64
|
});
|
|
65
|
+
const tooltipStyle = computed(() => ({
|
|
66
|
+
left: `${tooltipState.value.x}px`,
|
|
67
|
+
top: `${tooltipState.value.y}px`
|
|
68
|
+
}));
|
|
65
69
|
const hitSegments = ref([]);
|
|
66
|
-
const total = computed(() => items.value.reduce((s, r) => s +
|
|
70
|
+
const total = computed(() => items.value.reduce((s, r) => s + Math.max(0, cleanNumber(r[props.valueField])), 0));
|
|
67
71
|
const hoveredSeg = computed(() => {
|
|
68
72
|
if (hoveredIndex.value === -1) return null;
|
|
69
73
|
return hitSegments.value[hoveredIndex.value];
|
|
@@ -88,7 +92,7 @@ const draw = () => {
|
|
|
88
92
|
if (!ctx.value) return;
|
|
89
93
|
clear(props.backgroundColor);
|
|
90
94
|
const c = ctx.value;
|
|
91
|
-
const p = easeOut(progress.value);
|
|
95
|
+
const p = props.animated ? easeOut(progress.value) : 1;
|
|
92
96
|
const segs = [];
|
|
93
97
|
const cx = width.value / 2;
|
|
94
98
|
const cy = props.showLegend ? (height.value - 50) / 2 : height.value / 2;
|
|
@@ -96,7 +100,8 @@ const draw = () => {
|
|
|
96
100
|
let startAngle = -Math.PI / 2;
|
|
97
101
|
const tot = total.value || 1;
|
|
98
102
|
items.value.forEach((item, i) => {
|
|
99
|
-
const
|
|
103
|
+
const val = Math.max(0, cleanNumber(item[props.valueField]));
|
|
104
|
+
const slice = val / tot * Math.PI * 2 * p;
|
|
100
105
|
const endAngle = startAngle + slice;
|
|
101
106
|
const color = props.colors[i % props.colors.length];
|
|
102
107
|
const isHovered = hoveredIndex.value === i;
|
|
@@ -113,13 +118,13 @@ const draw = () => {
|
|
|
113
118
|
const lr = r * 0.7;
|
|
114
119
|
const lx = cx + lr * Math.cos(midAngle);
|
|
115
120
|
const ly = cy + lr * Math.sin(midAngle);
|
|
116
|
-
if (p > 0.5) {
|
|
121
|
+
if (p > 0.5 && val > 0) {
|
|
117
122
|
c.save();
|
|
118
123
|
c.fillStyle = "#fff";
|
|
119
124
|
c.font = "bold 11px system-ui, sans-serif";
|
|
120
125
|
c.textAlign = "center";
|
|
121
126
|
c.textBaseline = "middle";
|
|
122
|
-
const pct = (
|
|
127
|
+
const pct = (val / tot * 100).toFixed(0) + "%";
|
|
123
128
|
c.fillText(pct, lx, ly);
|
|
124
129
|
c.restore();
|
|
125
130
|
}
|
|
@@ -128,41 +133,32 @@ const draw = () => {
|
|
|
128
133
|
cx: cx + r * 0.75 * Math.cos(startAngle + slice / 2),
|
|
129
134
|
cy: cy + r * 0.75 * Math.sin(startAngle + slice / 2),
|
|
130
135
|
item,
|
|
131
|
-
label: `${item[props.labelField]}: ${
|
|
136
|
+
label: `${item[props.labelField]}: ${val}`,
|
|
132
137
|
startAngle,
|
|
133
138
|
endAngle
|
|
134
139
|
});
|
|
135
140
|
startAngle = endAngle;
|
|
136
141
|
});
|
|
137
142
|
if (props.donut) {
|
|
143
|
+
const ri = r * 0.5;
|
|
138
144
|
c.save();
|
|
139
145
|
c.beginPath();
|
|
140
|
-
c.arc(cx, cy,
|
|
141
|
-
c.fillStyle = "#
|
|
142
|
-
c.globalCompositeOperation = "destination-out";
|
|
146
|
+
c.arc(cx, cy, ri, 0, Math.PI * 2);
|
|
147
|
+
c.fillStyle = props.backgroundColor || "#ffffff";
|
|
143
148
|
c.fill();
|
|
144
|
-
c.globalCompositeOperation = "source-over";
|
|
145
149
|
c.restore();
|
|
146
150
|
}
|
|
147
151
|
hitSegments.value = segs;
|
|
148
152
|
if (props.showLegend) {
|
|
149
153
|
drawLegend(c);
|
|
150
154
|
}
|
|
151
|
-
if (props.title) {
|
|
152
|
-
c.save();
|
|
153
|
-
c.fillStyle = "rgba(120,120,140,0.9)";
|
|
154
|
-
c.font = "bold 16px system-ui, sans-serif";
|
|
155
|
-
c.textAlign = "center";
|
|
156
|
-
c.fillText(props.title, width.value / 2, 30);
|
|
157
|
-
c.restore();
|
|
158
|
-
}
|
|
159
155
|
};
|
|
160
156
|
const drawLegend = (c) => {
|
|
161
157
|
c.save();
|
|
162
158
|
c.font = "11px system-ui, sans-serif";
|
|
163
159
|
c.textAlign = "left";
|
|
164
160
|
c.textBaseline = "middle";
|
|
165
|
-
const keys = items.value.map((
|
|
161
|
+
const keys = items.value.map((r) => String(r[props.labelField]));
|
|
166
162
|
let xOffset = 20;
|
|
167
163
|
let yOffset = height.value - 25;
|
|
168
164
|
keys.forEach((key, i) => {
|
|
@@ -180,10 +176,12 @@ const drawLegend = (c) => {
|
|
|
180
176
|
});
|
|
181
177
|
c.restore();
|
|
182
178
|
};
|
|
183
|
-
watch(
|
|
179
|
+
watch(progress, draw);
|
|
180
|
+
watch([hoveredIndex, resizeTick, width, height, () => props.donut], draw);
|
|
184
181
|
watch(items, () => {
|
|
185
182
|
if (props.animated) animate();
|
|
186
|
-
|
|
183
|
+
else draw();
|
|
184
|
+
});
|
|
187
185
|
onMounted(async () => {
|
|
188
186
|
await nextTick();
|
|
189
187
|
if (containerRef.value && canvasRef.value) {
|
|
@@ -225,7 +223,7 @@ const onMouseLeave = () => {
|
|
|
225
223
|
};
|
|
226
224
|
defineExpose({
|
|
227
225
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "pie-chart.png"),
|
|
228
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "pie-chart.svg")
|
|
226
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "pie-chart.svg", canvasRef.value)
|
|
229
227
|
});
|
|
230
228
|
</script>
|
|
231
229
|
|
|
@@ -233,6 +231,7 @@ defineExpose({
|
|
|
233
231
|
.k-chart-wrapper {
|
|
234
232
|
position: relative;
|
|
235
233
|
user-select: none;
|
|
234
|
+
cursor: default;
|
|
236
235
|
}
|
|
237
236
|
.k-chart-wrapper .k-chart-inner {
|
|
238
237
|
width: 100%;
|