@katlux/block-charts 0.1.1-alpha.88 → 0.1.1-alpha.90
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 +172 -59
- 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 +96 -40
- package/dist/runtime/components/KPieChart/KPieChart.vue +25 -26
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +42 -54
- package/dist/runtime/composables/chartUtils.d.ts +8 -0
- package/dist/runtime/composables/chartUtils.js +58 -0
- package/dist/runtime/composables/useChartAxes.d.ts +2 -1
- package/dist/runtime/composables/useChartAxes.js +20 -7
- 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/package.json +1 -1
package/dist/module.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
1
3
|
interface ModuleOptions {
|
|
2
4
|
}
|
|
3
|
-
declare const _default:
|
|
5
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
4
6
|
|
|
5
7
|
export { _default as default };
|
|
6
8
|
export type { ModuleOptions };
|
package/dist/module.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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
4
|
@wheel.prevent="onWheel"
|
|
6
5
|
@mousedown="onMouseDown"
|
|
7
6
|
@mousemove="onMouseMove"
|
|
@@ -40,7 +39,6 @@
|
|
|
40
39
|
:cy="pt.y"
|
|
41
40
|
:r="10"
|
|
42
41
|
fill="transparent"
|
|
43
|
-
style="cursor: pointer"
|
|
44
42
|
@click="emit('click-point', pt.item, pt.index)"
|
|
45
43
|
)
|
|
46
44
|
circle(
|
|
@@ -56,7 +54,7 @@
|
|
|
56
54
|
)
|
|
57
55
|
.k-chart-tooltip(
|
|
58
56
|
v-if="tooltipState.visible && !isLoading"
|
|
59
|
-
:style="
|
|
57
|
+
:style="tooltipStyle"
|
|
60
58
|
)
|
|
61
59
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
62
60
|
span {{ tooltipState.content }}
|
|
@@ -72,6 +70,7 @@ import { useChartViewport } from "../../composables/useChartViewport";
|
|
|
72
70
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
73
71
|
import { useChartExport } from "../../composables/useChartExport";
|
|
74
72
|
import { useChartHitTest } from "../../composables/useChartHitTest";
|
|
73
|
+
import { cleanNumber, safeMax } from "../../composables/chartUtils";
|
|
75
74
|
const props = defineProps({
|
|
76
75
|
dataProvider: { type: null, required: false },
|
|
77
76
|
xField: { type: String, required: false, default: "x" },
|
|
@@ -91,7 +90,7 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
|
|
|
91
90
|
const containerRef = ref(null);
|
|
92
91
|
const canvasRef = ref(null);
|
|
93
92
|
const svgRef = ref(null);
|
|
94
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
93
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
95
94
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
96
95
|
const dataRef = computed(() => props.dataProvider);
|
|
97
96
|
const { items } = useChartData(dataRef);
|
|
@@ -100,6 +99,10 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
|
100
99
|
const isLoading = computed(() => {
|
|
101
100
|
return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
|
|
102
101
|
});
|
|
102
|
+
const tooltipStyle = computed(() => ({
|
|
103
|
+
left: `${tooltipState.value.x}px`,
|
|
104
|
+
top: `${tooltipState.value.y}px`
|
|
105
|
+
}));
|
|
103
106
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
104
107
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
105
108
|
const plotLeftRef = computed(() => 55);
|
|
@@ -122,19 +125,30 @@ const {
|
|
|
122
125
|
const xFieldRef = computed(() => props.xField);
|
|
123
126
|
const yFieldRef = computed(() => props.yField);
|
|
124
127
|
const xAxisType = computed(() => {
|
|
125
|
-
if (!items.value.length) return "
|
|
126
|
-
const
|
|
127
|
-
if (typeof
|
|
128
|
-
if (typeof
|
|
129
|
-
const
|
|
130
|
-
if (!isNaN(
|
|
128
|
+
if (!items.value.length) return "linear";
|
|
129
|
+
const firstVal = items.value[0]?.[props.xField];
|
|
130
|
+
if (typeof firstVal === "number") return "linear";
|
|
131
|
+
if (typeof firstVal === "string") {
|
|
132
|
+
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";
|
|
131
136
|
return "category";
|
|
132
137
|
}
|
|
133
|
-
return "
|
|
138
|
+
return "linear";
|
|
134
139
|
});
|
|
135
140
|
const categoryList = computed(() => {
|
|
136
141
|
if (xAxisType.value !== "category") return [];
|
|
137
|
-
|
|
142
|
+
const seen = /* @__PURE__ */ new Set();
|
|
143
|
+
const list = [];
|
|
144
|
+
for (const item of items.value) {
|
|
145
|
+
const val = String(item[props.xField] ?? "");
|
|
146
|
+
if (!seen.has(val)) {
|
|
147
|
+
seen.add(val);
|
|
148
|
+
list.push(val);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return list;
|
|
138
152
|
});
|
|
139
153
|
const categoryIndexMap = computed(() => {
|
|
140
154
|
const map = {};
|
|
@@ -158,6 +172,34 @@ const seriesMap = computed(() => {
|
|
|
158
172
|
}
|
|
159
173
|
return map;
|
|
160
174
|
});
|
|
175
|
+
const sortedSeriesMap = computed(() => {
|
|
176
|
+
const map = /* @__PURE__ */ new Map();
|
|
177
|
+
const itemIndexMap = /* @__PURE__ */ new Map();
|
|
178
|
+
for (let i = 0; i < items.value.length; i++) {
|
|
179
|
+
itemIndexMap.set(items.value[i], i);
|
|
180
|
+
}
|
|
181
|
+
for (const [key, seriesItems] of seriesMap.value) {
|
|
182
|
+
const enriched = seriesItems.map((item, idx) => ({
|
|
183
|
+
item,
|
|
184
|
+
index: itemIndexMap.get(item) ?? idx,
|
|
185
|
+
xVal: resolveX(item, idx),
|
|
186
|
+
yVal: cleanNumber(item[props.yField])
|
|
187
|
+
})).sort((a, b) => a.xVal - b.xVal);
|
|
188
|
+
map.set(key, enriched);
|
|
189
|
+
}
|
|
190
|
+
return map;
|
|
191
|
+
});
|
|
192
|
+
const stackedMaxY = computed(() => {
|
|
193
|
+
if (!props.stacked) return 0;
|
|
194
|
+
const xTotals = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const [, seriesItems] of sortedSeriesMap.value) {
|
|
196
|
+
for (const pt of seriesItems) {
|
|
197
|
+
const current = xTotals.get(pt.xVal) || 0;
|
|
198
|
+
xTotals.set(pt.xVal, current + Math.max(0, pt.yVal));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return safeMax(Array.from(xTotals.values()), 10);
|
|
202
|
+
});
|
|
161
203
|
const axes = useChartAxes({
|
|
162
204
|
items,
|
|
163
205
|
xField: xFieldRef,
|
|
@@ -169,11 +211,21 @@ const axes = useChartAxes({
|
|
|
169
211
|
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
170
212
|
xValueGetter: (r, i) => resolveX(r, i)
|
|
171
213
|
});
|
|
214
|
+
const effectiveYMax = computed(() => {
|
|
215
|
+
return props.stacked ? Math.max(stackedMaxY.value, axes.yMax.value) : axes.yMax.value;
|
|
216
|
+
});
|
|
217
|
+
const toStackedY = (val) => {
|
|
218
|
+
const yMin = axes.yMin.value;
|
|
219
|
+
const yMax = effectiveYMax.value;
|
|
220
|
+
const range = yMax - yMin;
|
|
221
|
+
if (range === 0) return axes.plotTop + axes.plotHeight.value;
|
|
222
|
+
return axes.plotTop + axes.plotHeight.value - (val - yMin) / range * axes.plotHeight.value;
|
|
223
|
+
};
|
|
172
224
|
const draw = () => {
|
|
173
225
|
if (!ctx.value) return;
|
|
174
226
|
clear(props.backgroundColor);
|
|
175
227
|
const c = ctx.value;
|
|
176
|
-
const p = easeOut(progress.value);
|
|
228
|
+
const p = props.animated ? easeOut(progress.value) : 1;
|
|
177
229
|
c.save();
|
|
178
230
|
c.strokeStyle = "rgba(100,100,120,0.1)";
|
|
179
231
|
c.lineWidth = 1;
|
|
@@ -197,8 +249,8 @@ const draw = () => {
|
|
|
197
249
|
c.font = "11px system-ui, sans-serif";
|
|
198
250
|
c.textAlign = "right";
|
|
199
251
|
for (let i = 0; i <= 5; i++) {
|
|
200
|
-
const val = axes.yMin.value + (
|
|
201
|
-
c.fillText(val.toFixed(1), axes.plotLeft - 8,
|
|
252
|
+
const val = axes.yMin.value + (effectiveYMax.value - axes.yMin.value) * (i / 5);
|
|
253
|
+
c.fillText(val.toFixed(1), axes.plotLeft - 8, toStackedY(val) + 4);
|
|
202
254
|
}
|
|
203
255
|
c.textAlign = "center";
|
|
204
256
|
if (xAxisType.value === "category") {
|
|
@@ -231,6 +283,11 @@ const draw = () => {
|
|
|
231
283
|
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
232
284
|
}
|
|
233
285
|
}
|
|
286
|
+
c.strokeStyle = "rgba(150,150,170,0.5)";
|
|
287
|
+
c.beginPath();
|
|
288
|
+
c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
289
|
+
c.lineTo(axes.plotLeft + axes.plotWidth.value, axes.plotTop + axes.plotHeight.value);
|
|
290
|
+
c.stroke();
|
|
234
291
|
c.beginPath();
|
|
235
292
|
c.moveTo(axes.plotLeft, axes.plotTop);
|
|
236
293
|
c.lineTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
@@ -254,41 +311,74 @@ const draw = () => {
|
|
|
254
311
|
c.beginPath();
|
|
255
312
|
c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
|
|
256
313
|
c.clip();
|
|
314
|
+
const currentBaseline = /* @__PURE__ */ new Map();
|
|
257
315
|
let seriesIdx = 0;
|
|
258
|
-
for (const [, seriesItems] of
|
|
316
|
+
for (const [, seriesItems] of sortedSeriesMap.value) {
|
|
259
317
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
const pts = sorted.slice(0, drawCount);
|
|
318
|
+
const drawCount = Math.max(1, Math.floor(seriesItems.length * p));
|
|
319
|
+
const pts = seriesItems.slice(0, drawCount);
|
|
263
320
|
c.save();
|
|
264
321
|
c.beginPath();
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
322
|
+
if (props.stacked) {
|
|
323
|
+
const topPoints = [];
|
|
324
|
+
const bottomPoints = [];
|
|
325
|
+
for (const pt of pts) {
|
|
326
|
+
const x = axes.toX(pt.xVal);
|
|
327
|
+
const base = currentBaseline.get(pt.xVal) || 0;
|
|
328
|
+
const top = base + pt.yVal;
|
|
329
|
+
const yTop = toStackedY(top);
|
|
330
|
+
const yBottom = toStackedY(base);
|
|
331
|
+
topPoints.push({ x, y: yTop });
|
|
332
|
+
bottomPoints.push({ x, y: yBottom });
|
|
333
|
+
currentBaseline.set(pt.xVal, top);
|
|
334
|
+
}
|
|
335
|
+
topPoints.forEach((pt, i) => {
|
|
336
|
+
if (i === 0) c.moveTo(pt.x, pt.y);
|
|
337
|
+
else c.lineTo(pt.x, pt.y);
|
|
338
|
+
});
|
|
339
|
+
for (let i = bottomPoints.length - 1; i >= 0; i--) {
|
|
340
|
+
c.lineTo(bottomPoints[i].x, bottomPoints[i].y);
|
|
341
|
+
}
|
|
277
342
|
c.closePath();
|
|
343
|
+
c.fillStyle = color + "44";
|
|
344
|
+
c.fill();
|
|
345
|
+
c.beginPath();
|
|
346
|
+
topPoints.forEach((pt, i) => {
|
|
347
|
+
if (i === 0) c.moveTo(pt.x, pt.y);
|
|
348
|
+
else c.lineTo(pt.x, pt.y);
|
|
349
|
+
});
|
|
350
|
+
c.strokeStyle = color;
|
|
351
|
+
c.lineWidth = 2;
|
|
352
|
+
c.stroke();
|
|
353
|
+
} else {
|
|
354
|
+
pts.forEach((pt, i) => {
|
|
355
|
+
const x = axes.toX(pt.xVal);
|
|
356
|
+
const y = toStackedY(pt.yVal);
|
|
357
|
+
if (i === 0) c.moveTo(x, y);
|
|
358
|
+
else c.lineTo(x, y);
|
|
359
|
+
});
|
|
360
|
+
if (pts.length > 0) {
|
|
361
|
+
const lastX = axes.toX(pts[pts.length - 1].xVal);
|
|
362
|
+
const firstX = axes.toX(pts[0].xVal);
|
|
363
|
+
const baselineY = axes.plotTop + axes.plotHeight.value;
|
|
364
|
+
c.lineTo(lastX, baselineY);
|
|
365
|
+
c.lineTo(firstX, baselineY);
|
|
366
|
+
c.closePath();
|
|
367
|
+
}
|
|
368
|
+
c.fillStyle = color + "33";
|
|
369
|
+
c.fill();
|
|
370
|
+
c.beginPath();
|
|
371
|
+
pts.forEach((pt, i) => {
|
|
372
|
+
const x = axes.toX(pt.xVal);
|
|
373
|
+
const y = toStackedY(pt.yVal);
|
|
374
|
+
if (i === 0) c.moveTo(x, y);
|
|
375
|
+
else c.lineTo(x, y);
|
|
376
|
+
});
|
|
377
|
+
c.strokeStyle = color;
|
|
378
|
+
c.lineWidth = 2;
|
|
379
|
+
c.lineJoin = "round";
|
|
380
|
+
c.stroke();
|
|
278
381
|
}
|
|
279
|
-
c.fillStyle = color + "33";
|
|
280
|
-
c.fill();
|
|
281
|
-
c.beginPath();
|
|
282
|
-
pts.forEach((item, i) => {
|
|
283
|
-
const x = axes.toX(resolveX(item, i));
|
|
284
|
-
const y = axes.toY(parseValue(item[props.yField]));
|
|
285
|
-
if (i === 0) c.moveTo(x, y);
|
|
286
|
-
else c.lineTo(x, y);
|
|
287
|
-
});
|
|
288
|
-
c.strokeStyle = color;
|
|
289
|
-
c.lineWidth = 2;
|
|
290
|
-
c.lineJoin = "round";
|
|
291
|
-
c.stroke();
|
|
292
382
|
c.restore();
|
|
293
383
|
seriesIdx++;
|
|
294
384
|
}
|
|
@@ -316,28 +406,34 @@ const drawLegend = (c) => {
|
|
|
316
406
|
});
|
|
317
407
|
c.restore();
|
|
318
408
|
};
|
|
319
|
-
watch(
|
|
409
|
+
watch(progress, draw);
|
|
410
|
+
watch([scale, panOffset, hoveredIndex, resizeTick, width, height, () => props.stacked], draw);
|
|
320
411
|
watch(items, () => {
|
|
321
412
|
if (props.animated) animate();
|
|
322
|
-
|
|
413
|
+
else draw();
|
|
414
|
+
});
|
|
323
415
|
watch(scale, (v) => emit("zoom-change", v));
|
|
324
416
|
watch(panOffset, (v) => emit("pan-change", v), { deep: true });
|
|
325
417
|
const hoveredPoints = computed(() => {
|
|
326
418
|
if (hoveredIndex.value === -1) return [];
|
|
419
|
+
if (hoveredIndex.value >= items.value.length) {
|
|
420
|
+
setHovered(-1);
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
327
423
|
const item = items.value[hoveredIndex.value];
|
|
328
424
|
const xVal = resolveX(item);
|
|
329
425
|
const xPx = axes.toX(xVal);
|
|
330
426
|
const pts = [];
|
|
331
427
|
let seriesIdx = 0;
|
|
332
|
-
for (const [,
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
428
|
+
for (const [, seriesList] of sortedSeriesMap.value) {
|
|
429
|
+
const found = seriesList.find((pt) => pt.xVal === xVal);
|
|
430
|
+
if (found) {
|
|
335
431
|
pts.push({
|
|
336
432
|
x: xPx,
|
|
337
|
-
y:
|
|
433
|
+
y: toStackedY(found.yVal),
|
|
338
434
|
color: props.colors[seriesIdx % props.colors.length],
|
|
339
|
-
item:
|
|
340
|
-
index:
|
|
435
|
+
item: found.item,
|
|
436
|
+
index: found.index
|
|
341
437
|
});
|
|
342
438
|
}
|
|
343
439
|
seriesIdx++;
|
|
@@ -353,7 +449,7 @@ onMounted(async () => {
|
|
|
353
449
|
}
|
|
354
450
|
});
|
|
355
451
|
const onMouseMove = (e) => {
|
|
356
|
-
|
|
452
|
+
onViewportMouseMove(e);
|
|
357
453
|
if (isDragging.value) {
|
|
358
454
|
setHovered(-1);
|
|
359
455
|
hideTooltip();
|
|
@@ -363,11 +459,24 @@ const onMouseMove = (e) => {
|
|
|
363
459
|
if (!rect) return;
|
|
364
460
|
const mouseX = e.clientX - rect.left;
|
|
365
461
|
const mouseY = e.clientY - rect.top;
|
|
366
|
-
|
|
462
|
+
if (mouseX < axes.plotLeft || mouseX > axes.plotLeft + axes.plotWidth.value || mouseY < axes.plotTop || mouseY > axes.plotTop + axes.plotHeight.value) {
|
|
463
|
+
setHovered(-1);
|
|
464
|
+
hideTooltip();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const nearest = findNearestX(mouseX, items.value, axes.toX, (r, i) => resolveX(r, i));
|
|
367
468
|
if (nearest) {
|
|
368
469
|
setHovered(nearest.index);
|
|
369
|
-
showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
|
|
370
470
|
emit("hover-point", nearest.item, nearest.index);
|
|
471
|
+
const xPx = axes.toX(resolveX(nearest.item, nearest.index));
|
|
472
|
+
const yPx = toStackedY(cleanNumber(nearest.item[props.yField]));
|
|
473
|
+
showTooltip(
|
|
474
|
+
xPx,
|
|
475
|
+
yPx - 10,
|
|
476
|
+
`${props.xField}: ${nearest.item[props.xField]} | ${props.yField}: ${nearest.item[props.yField]}`,
|
|
477
|
+
nearest.item,
|
|
478
|
+
nearest.index
|
|
479
|
+
);
|
|
371
480
|
} else {
|
|
372
481
|
setHovered(-1);
|
|
373
482
|
hideTooltip();
|
|
@@ -383,7 +492,7 @@ defineExpose({
|
|
|
383
492
|
zoomOut: viewport.zoomOut,
|
|
384
493
|
resetZoom: viewport.resetZoom,
|
|
385
494
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "area-chart.png"),
|
|
386
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg")
|
|
495
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg", canvasRef.value)
|
|
387
496
|
});
|
|
388
497
|
</script>
|
|
389
498
|
|
|
@@ -391,6 +500,13 @@ defineExpose({
|
|
|
391
500
|
.k-chart-wrapper {
|
|
392
501
|
position: relative;
|
|
393
502
|
user-select: none;
|
|
503
|
+
cursor: default;
|
|
504
|
+
}
|
|
505
|
+
.k-chart-wrapper.k-chart--pannable {
|
|
506
|
+
cursor: grab;
|
|
507
|
+
}
|
|
508
|
+
.k-chart-wrapper.k-chart--grabbing {
|
|
509
|
+
cursor: grabbing;
|
|
394
510
|
}
|
|
395
511
|
.k-chart-wrapper .k-chart-controls {
|
|
396
512
|
position: absolute;
|
|
@@ -400,9 +516,6 @@ defineExpose({
|
|
|
400
516
|
display: flex;
|
|
401
517
|
gap: 4px;
|
|
402
518
|
}
|
|
403
|
-
.k-chart-wrapper .k-chart-btn {
|
|
404
|
-
display: none;
|
|
405
|
-
}
|
|
406
519
|
.k-chart-wrapper .k-chart-inner {
|
|
407
520
|
width: 100%;
|
|
408
521
|
height: 400px;
|