@katlux/block-charts 0.1.1-alpha.93 → 0.1.1-alpha.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/KAreaChart/KAreaChart.d.vue.ts +2 -0
- package/dist/runtime/components/KAreaChart/KAreaChart.vue +53 -25
- 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 +89 -36
- package/dist/runtime/components/KBarChart/KBarChart.vue.d.ts +2 -0
- package/dist/runtime/components/KHeatMap/KHeatMap.vue +7 -3
- package/dist/runtime/components/KLineChart/KLineChart.d.vue.ts +2 -0
- package/dist/runtime/components/KLineChart/KLineChart.vue +45 -23
- package/dist/runtime/components/KLineChart/KLineChart.vue.d.ts +2 -0
- package/dist/runtime/components/KPieChart/KPieChart.vue +7 -3
- package/dist/runtime/components/KScatterChart/KScatterChart.d.vue.ts +3 -1
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +59 -13
- package/dist/runtime/components/KScatterChart/KScatterChart.vue.d.ts +3 -1
- package/dist/runtime/composables/useChartAnimation.js +7 -1
- package/dist/runtime/composables/useChartData.d.ts +1 -0
- package/dist/runtime/composables/useChartData.js +53 -22
- package/dist/runtime/composables/useChartExport.js +10 -3
- package/dist/runtime/composables/useChartHitTest.d.ts +1 -1
- package/dist/runtime/composables/useChartHitTest.js +3 -2
- package/dist/runtime/composables/useChartViewport.d.ts +3 -0
- package/dist/runtime/composables/useChartViewport.js +29 -7
- package/package.json +3 -3
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.k-chart-wrapper(
|
|
3
3
|
ref="wrapperRef"
|
|
4
4
|
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
5
|
-
@wheel
|
|
5
|
+
@wheel="onWheel"
|
|
6
6
|
@mousedown="onMouseDown"
|
|
7
7
|
@mousemove="onMouseMove"
|
|
8
8
|
@mouseup="onMouseUp"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
:height="height"
|
|
23
23
|
)
|
|
24
24
|
defs
|
|
25
|
-
clipPath(id="plot-clip")
|
|
25
|
+
clipPath(:id="'plot-clip-' + clipId")
|
|
26
26
|
rect(
|
|
27
27
|
:x="axes.plotLeft"
|
|
28
28
|
:y="axes.plotTop"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
:height="axes.plotHeight.value"
|
|
31
31
|
)
|
|
32
32
|
// Dynamic Interaction Layer: Repositions one set of circles to the nearest point
|
|
33
|
-
g.k-chart-interaction(v-if="hoveredIndex !== -1" clip-path="url(#plot-clip)")
|
|
33
|
+
g.k-chart-interaction(v-if="hoveredIndex !== -1" :clip-path="'url(#plot-clip-' + clipId + ')'")
|
|
34
34
|
circle.k-chart-hit-proxy(
|
|
35
35
|
v-for="(pt, i) in hoveredPoints"
|
|
36
36
|
:key="i"
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
</template>
|
|
63
63
|
|
|
64
64
|
<script setup>
|
|
65
|
-
import { ref, watch, computed, onMounted, onUnmounted, nextTick } from "vue";
|
|
65
|
+
import { ref, watch, computed, onMounted, onUnmounted, nextTick, useId, isRef } from "vue";
|
|
66
66
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
67
67
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
68
68
|
import { useChartData } from "../../composables/useChartData";
|
|
@@ -71,6 +71,8 @@ import { useChartViewport } from "../../composables/useChartViewport";
|
|
|
71
71
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
72
72
|
import { useChartExport } from "../../composables/useChartExport";
|
|
73
73
|
import { useChartHitTest } from "../../composables/useChartHitTest";
|
|
74
|
+
import { cleanNumber } from "../../composables/chartUtils";
|
|
75
|
+
const clipId = useId();
|
|
74
76
|
const props = defineProps({
|
|
75
77
|
dataProvider: { type: null, required: false },
|
|
76
78
|
xField: { type: String, required: false, default: "x" },
|
|
@@ -78,6 +80,7 @@ const props = defineProps({
|
|
|
78
80
|
seriesField: { type: String, required: false, default: "series" },
|
|
79
81
|
animated: { type: Boolean, required: false, default: true },
|
|
80
82
|
zoomable: { type: Boolean, required: false, default: true },
|
|
83
|
+
wheelNavigation: { type: Boolean, required: false, default: true },
|
|
81
84
|
maxZoom: { type: Number, required: false, default: 10 },
|
|
82
85
|
colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7", "#14b8a6", "#fb923c"] },
|
|
83
86
|
backgroundColor: { type: String, required: false, default: "#ffffff" },
|
|
@@ -93,11 +96,15 @@ const svgRef = ref(null);
|
|
|
93
96
|
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
94
97
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
95
98
|
const dataRef = computed(() => props.dataProvider);
|
|
96
|
-
const { items, loading } = useChartData(dataRef);
|
|
99
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
97
100
|
const { progress, animate } = useChartAnimation();
|
|
98
101
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
99
102
|
const isLoading = computed(() => {
|
|
100
|
-
|
|
103
|
+
const dp = props.dataProvider;
|
|
104
|
+
if (!dp) return dataLoading.value;
|
|
105
|
+
const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
|
|
106
|
+
const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
|
|
107
|
+
return Boolean(dataLoading.value || rawLoading || rawInitial);
|
|
101
108
|
});
|
|
102
109
|
const tooltipStyle = computed(() => ({
|
|
103
110
|
left: `${tooltipState.value.x}px`,
|
|
@@ -106,7 +113,15 @@ const tooltipStyle = computed(() => ({
|
|
|
106
113
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
107
114
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
108
115
|
const plotLeftRef = computed(() => 55);
|
|
109
|
-
const
|
|
116
|
+
const zoomableRef = computed(() => props.zoomable);
|
|
117
|
+
const wheelNavRef = computed(() => props.wheelNavigation);
|
|
118
|
+
const viewport = useChartViewport({
|
|
119
|
+
maxZoom: maxZoomRef,
|
|
120
|
+
plotWidth: plotWidthRef,
|
|
121
|
+
plotLeft: plotLeftRef,
|
|
122
|
+
zoomable: zoomableRef,
|
|
123
|
+
wheelNavigation: wheelNavRef
|
|
124
|
+
});
|
|
110
125
|
const { findNearestX } = useChartHitTest();
|
|
111
126
|
const {
|
|
112
127
|
scale,
|
|
@@ -127,14 +142,24 @@ const xAxisType = computed(() => {
|
|
|
127
142
|
const firstVal = items.value[0]?.[props.xField];
|
|
128
143
|
if (typeof firstVal === "number") return "linear";
|
|
129
144
|
if (typeof firstVal === "string") {
|
|
145
|
+
const cleaned = cleanNumber(firstVal, NaN);
|
|
146
|
+
if (Number.isFinite(cleaned)) return "linear";
|
|
130
147
|
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";
|
|
148
|
+
if (!isNaN(parsed) && !/^\d+$/.test(firstVal.trim()) && /[-/T:]|[A-Za-z]/.test(firstVal)) return "datetime";
|
|
134
149
|
return "category";
|
|
135
150
|
}
|
|
136
151
|
return "linear";
|
|
137
152
|
});
|
|
153
|
+
const isAllMidnight = computed(() => {
|
|
154
|
+
if (xAxisType.value !== "datetime" || !items.value.length) return false;
|
|
155
|
+
const checkCount = Math.min(items.value.length, 100);
|
|
156
|
+
for (let i = 0; i < checkCount; i++) {
|
|
157
|
+
const ts = parseValue(items.value[i][props.xField]);
|
|
158
|
+
const d = new Date(ts);
|
|
159
|
+
if (d.getHours() !== 0 || d.getMinutes() !== 0) return false;
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
});
|
|
138
163
|
const categoryList = computed(() => {
|
|
139
164
|
if (xAxisType.value !== "category") return [];
|
|
140
165
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -297,12 +322,7 @@ const drawAxes = (c) => {
|
|
|
297
322
|
const x = axes.toX(val);
|
|
298
323
|
let label;
|
|
299
324
|
if (xAxisType.value === "datetime") {
|
|
300
|
-
|
|
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" });
|
|
325
|
+
label = isAllMidnight.value ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
306
326
|
} else {
|
|
307
327
|
label = val.toFixed(1);
|
|
308
328
|
}
|
|
@@ -373,7 +393,7 @@ watch(items, () => {
|
|
|
373
393
|
else draw();
|
|
374
394
|
});
|
|
375
395
|
watch(scale, (v) => emit("zoom-change", v));
|
|
376
|
-
watch(panOffset, (
|
|
396
|
+
watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
|
|
377
397
|
onMounted(async () => {
|
|
378
398
|
await nextTick();
|
|
379
399
|
if (containerRef.value && canvasRef.value) {
|
|
@@ -382,13 +402,15 @@ onMounted(async () => {
|
|
|
382
402
|
else draw();
|
|
383
403
|
}
|
|
384
404
|
});
|
|
385
|
-
|
|
386
|
-
if (hoveredIndex.value
|
|
387
|
-
if (hoveredIndex.value >= items.value.length) {
|
|
405
|
+
watch(() => items.value.length, (newLen) => {
|
|
406
|
+
if (hoveredIndex.value >= newLen) {
|
|
388
407
|
setHovered(-1);
|
|
389
|
-
return [];
|
|
390
408
|
}
|
|
409
|
+
});
|
|
410
|
+
const hoveredPoints = computed(() => {
|
|
411
|
+
if (hoveredIndex.value === -1 || hoveredIndex.value >= items.value.length) return [];
|
|
391
412
|
const item = items.value[hoveredIndex.value];
|
|
413
|
+
if (!item) return [];
|
|
392
414
|
const xVal = resolveX(item);
|
|
393
415
|
const xPx = axes.toX(xVal);
|
|
394
416
|
const pts = [];
|
|
@@ -424,7 +446,7 @@ const onMouseMove = (e) => {
|
|
|
424
446
|
hideTooltip();
|
|
425
447
|
return;
|
|
426
448
|
}
|
|
427
|
-
const nearest = findNearestX(mouseX,
|
|
449
|
+
const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
|
|
428
450
|
if (nearest) {
|
|
429
451
|
setHovered(nearest.index);
|
|
430
452
|
emit("hover-point", nearest.item, nearest.index);
|
|
@@ -443,7 +465,7 @@ const onMouseMove = (e) => {
|
|
|
443
465
|
}
|
|
444
466
|
};
|
|
445
467
|
const onMouseLeave = (e) => {
|
|
446
|
-
onViewportMouseLeave();
|
|
468
|
+
onViewportMouseLeave(e);
|
|
447
469
|
setHovered(-1);
|
|
448
470
|
hideTooltip();
|
|
449
471
|
};
|
|
@@ -5,6 +5,7 @@ type __VLS_Props = {
|
|
|
5
5
|
seriesField?: string;
|
|
6
6
|
animated?: boolean;
|
|
7
7
|
zoomable?: boolean;
|
|
8
|
+
wheelNavigation?: boolean;
|
|
8
9
|
maxZoom?: number;
|
|
9
10
|
colors?: string[];
|
|
10
11
|
backgroundColor?: string;
|
|
@@ -40,6 +41,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
40
41
|
seriesField: string;
|
|
41
42
|
animated: boolean;
|
|
42
43
|
zoomable: boolean;
|
|
44
|
+
wheelNavigation: boolean;
|
|
43
45
|
maxZoom: number;
|
|
44
46
|
colors: string[];
|
|
45
47
|
backgroundColor: string;
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
</template>
|
|
32
32
|
|
|
33
33
|
<script setup>
|
|
34
|
-
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
34
|
+
import { ref, watch, computed, onMounted, nextTick, isRef } from "vue";
|
|
35
35
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
36
36
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
37
37
|
import { useChartData } from "../../composables/useChartData";
|
|
@@ -56,11 +56,15 @@ const svgRef = ref(null);
|
|
|
56
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
|
-
const { items } = useChartData(dataRef);
|
|
59
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
60
60
|
const { progress, animate } = useChartAnimation();
|
|
61
61
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
62
62
|
const isLoading = computed(() => {
|
|
63
|
-
|
|
63
|
+
const dp = props.dataProvider;
|
|
64
|
+
if (!dp) return dataLoading.value;
|
|
65
|
+
const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
|
|
66
|
+
const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
|
|
67
|
+
return Boolean(dataLoading.value || rawLoading || rawInitial);
|
|
64
68
|
});
|
|
65
69
|
const tooltipStyle = computed(() => ({
|
|
66
70
|
left: `${tooltipState.value.x}px`,
|
|
@@ -7,6 +7,7 @@ type __VLS_Props = {
|
|
|
7
7
|
labelField?: string;
|
|
8
8
|
animated?: boolean;
|
|
9
9
|
zoomable?: boolean;
|
|
10
|
+
wheelNavigation?: boolean;
|
|
10
11
|
maxZoom?: number;
|
|
11
12
|
colors?: string[];
|
|
12
13
|
backgroundColor?: string;
|
|
@@ -42,14 +43,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
42
43
|
seriesField: string;
|
|
43
44
|
animated: boolean;
|
|
44
45
|
zoomable: boolean;
|
|
46
|
+
wheelNavigation: boolean;
|
|
45
47
|
maxZoom: number;
|
|
46
48
|
colors: string[];
|
|
47
49
|
backgroundColor: string;
|
|
48
50
|
showLegend: boolean;
|
|
49
51
|
xAxisTitle: string;
|
|
50
52
|
yAxisTitle: string;
|
|
51
|
-
labelField: string;
|
|
52
53
|
sizeField: string;
|
|
54
|
+
labelField: string;
|
|
53
55
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
54
56
|
declare const _default: typeof __VLS_export;
|
|
55
57
|
export default _default;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
.k-chart-wrapper(
|
|
3
3
|
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
4
|
-
@wheel
|
|
4
|
+
@wheel="onWheel"
|
|
5
5
|
@mousedown="onMouseDown"
|
|
6
6
|
@mousemove="onMouseMove"
|
|
7
7
|
@mouseup="onMouseUp"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
canvas(ref="canvasRef" v-show="!isLoading")
|
|
17
17
|
svg.k-chart-svg(v-show="!isLoading" ref="svgRef" :width="width" :height="height")
|
|
18
18
|
defs
|
|
19
|
-
clipPath(id="plot-clip")
|
|
19
|
+
clipPath(:id="'plot-clip-' + clipId")
|
|
20
20
|
rect(
|
|
21
21
|
:x="axes.plotLeft"
|
|
22
22
|
:y="axes.plotTop"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
g.k-chart-hits
|
|
27
27
|
|
|
28
28
|
// Dynamic Interaction Layer
|
|
29
|
-
g.k-chart-interaction(v-if="hoveredIndex !== -1 && !isLoading && hoveredPoint" clip-path="url(#plot-clip)")
|
|
29
|
+
g.k-chart-interaction(v-if="hoveredIndex !== -1 && !isLoading && hoveredPoint" :clip-path="'url(#plot-clip-' + clipId + ')'")
|
|
30
30
|
circle.k-chart-hit-proxy(
|
|
31
31
|
:cx="hoveredPoint.x"
|
|
32
32
|
:cy="hoveredPoint.y"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
</template>
|
|
50
50
|
|
|
51
51
|
<script setup>
|
|
52
|
-
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
52
|
+
import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
|
|
53
53
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
54
54
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
55
55
|
import { useChartData } from "../../composables/useChartData";
|
|
@@ -59,6 +59,7 @@ import { useChartAnimation, easeOut } from "../../composables/useChartAnimation"
|
|
|
59
59
|
import { useChartExport } from "../../composables/useChartExport";
|
|
60
60
|
import { useChartHitTest } from "../../composables/useChartHitTest";
|
|
61
61
|
import { cleanNumber, safeMax } from "../../composables/chartUtils";
|
|
62
|
+
const clipId = useId();
|
|
62
63
|
const props = defineProps({
|
|
63
64
|
dataProvider: { type: null, required: false },
|
|
64
65
|
xField: { type: String, required: false, default: "x" },
|
|
@@ -68,6 +69,7 @@ const props = defineProps({
|
|
|
68
69
|
labelField: { type: String, required: false, default: "label" },
|
|
69
70
|
animated: { type: Boolean, required: false, default: true },
|
|
70
71
|
zoomable: { type: Boolean, required: false, default: true },
|
|
72
|
+
wheelNavigation: { type: Boolean, required: false, default: true },
|
|
71
73
|
maxZoom: { type: Number, required: false, default: 20 },
|
|
72
74
|
colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7", "#14b8a6", "#fb923c"] },
|
|
73
75
|
backgroundColor: { type: String, required: false, default: "#ffffff" },
|
|
@@ -82,11 +84,15 @@ const svgRef = ref(null);
|
|
|
82
84
|
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
83
85
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
84
86
|
const dataRef = computed(() => props.dataProvider);
|
|
85
|
-
const { items } = useChartData(dataRef);
|
|
87
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
86
88
|
const { progress, animate } = useChartAnimation();
|
|
87
89
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
88
90
|
const isLoading = computed(() => {
|
|
89
|
-
|
|
91
|
+
const dp = props.dataProvider;
|
|
92
|
+
if (!dp) return dataLoading.value;
|
|
93
|
+
const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
|
|
94
|
+
const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
|
|
95
|
+
return Boolean(dataLoading.value || rawLoading || rawInitial);
|
|
90
96
|
});
|
|
91
97
|
const tooltipStyle = computed(() => ({
|
|
92
98
|
left: `${tooltipState.value.x}px`,
|
|
@@ -95,7 +101,15 @@ const tooltipStyle = computed(() => ({
|
|
|
95
101
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
96
102
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
97
103
|
const plotLeftRef = computed(() => 55);
|
|
98
|
-
const
|
|
104
|
+
const zoomableRef = computed(() => props.zoomable);
|
|
105
|
+
const wheelNavRef = computed(() => props.wheelNavigation);
|
|
106
|
+
const viewport = useChartViewport({
|
|
107
|
+
maxZoom: maxZoomRef,
|
|
108
|
+
plotWidth: plotWidthRef,
|
|
109
|
+
plotLeft: plotLeftRef,
|
|
110
|
+
zoomable: zoomableRef,
|
|
111
|
+
wheelNavigation: wheelNavRef
|
|
112
|
+
});
|
|
99
113
|
const { findNearestEuclidean } = useChartHitTest();
|
|
100
114
|
const {
|
|
101
115
|
scale,
|
|
@@ -118,12 +132,24 @@ const xAxisType = computed(() => {
|
|
|
118
132
|
const sample = items.value[0][props.xField];
|
|
119
133
|
if (typeof sample === "number") return "numeric";
|
|
120
134
|
if (typeof sample === "string") {
|
|
135
|
+
const cleaned = cleanNumber(sample, NaN);
|
|
136
|
+
if (Number.isFinite(cleaned)) return "numeric";
|
|
121
137
|
const ts = Date.parse(sample);
|
|
122
|
-
if (!isNaN(ts) &&
|
|
138
|
+
if (!isNaN(ts) && !/^\d+$/.test(sample.trim()) && /[-/T:]|[A-Za-z]/.test(sample)) return "datetime";
|
|
123
139
|
return "category";
|
|
124
140
|
}
|
|
125
141
|
return "numeric";
|
|
126
142
|
});
|
|
143
|
+
const isAllMidnight = computed(() => {
|
|
144
|
+
if (xAxisType.value !== "datetime" || !items.value.length) return false;
|
|
145
|
+
const checkCount = Math.min(items.value.length, 100);
|
|
146
|
+
for (let i = 0; i < checkCount; i++) {
|
|
147
|
+
const ts = parseValue(items.value[i][props.xField]);
|
|
148
|
+
const d = new Date(ts);
|
|
149
|
+
if (d.getHours() !== 0 || d.getMinutes() !== 0) return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
127
153
|
const categoryList = computed(() => {
|
|
128
154
|
if (xAxisType.value !== "category") return [];
|
|
129
155
|
return [...new Set(items.value.map((r) => String(r[props.xField])))];
|
|
@@ -213,9 +239,29 @@ const draw = () => {
|
|
|
213
239
|
c.fillText(val.toFixed(1), axes.plotLeft - 8, axes.toY(val) + 4);
|
|
214
240
|
}
|
|
215
241
|
c.textAlign = "center";
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
242
|
+
if (xAxisType.value === "category") {
|
|
243
|
+
const count = categoryList.value.length;
|
|
244
|
+
if (count > 0) {
|
|
245
|
+
const xSteps = Math.min(10, count - 1);
|
|
246
|
+
const stepSize = xSteps > 0 ? (count - 1) / xSteps : 1;
|
|
247
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
248
|
+
const idx = Math.round(i * stepSize);
|
|
249
|
+
const x = axes.toX(idx);
|
|
250
|
+
const label = categoryList.value[idx] ?? "";
|
|
251
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
for (let i = 0; i <= 5; i++) {
|
|
256
|
+
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
|
|
257
|
+
let label;
|
|
258
|
+
if (xAxisType.value === "datetime") {
|
|
259
|
+
label = isAllMidnight.value ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
260
|
+
} else {
|
|
261
|
+
label = val.toFixed(1);
|
|
262
|
+
}
|
|
263
|
+
c.fillText(label, axes.toX(val), axes.plotTop + axes.plotHeight.value + 16);
|
|
264
|
+
}
|
|
219
265
|
}
|
|
220
266
|
c.strokeStyle = "rgba(150,150,170,0.5)";
|
|
221
267
|
c.beginPath();
|
|
@@ -302,7 +348,7 @@ watch(items, () => {
|
|
|
302
348
|
else draw();
|
|
303
349
|
});
|
|
304
350
|
watch(scale, (v) => emit("zoom-change", v));
|
|
305
|
-
watch(panOffset, (
|
|
351
|
+
watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
|
|
306
352
|
onMounted(async () => {
|
|
307
353
|
await nextTick();
|
|
308
354
|
if (containerRef.value && canvasRef.value) {
|
|
@@ -322,7 +368,7 @@ const onMouseMove = (e) => {
|
|
|
322
368
|
if (!rect) return;
|
|
323
369
|
const mx = e.clientX - rect.left;
|
|
324
370
|
const my = e.clientY - rect.top;
|
|
325
|
-
const nearest = findNearestEuclidean(items.value, mx, my, axes, props.xField, props.yField, 20);
|
|
371
|
+
const nearest = findNearestEuclidean(items.value, mx, my, axes, props.xField, props.yField, 20, (item, i) => resolveX(item, i));
|
|
326
372
|
if (nearest) {
|
|
327
373
|
setHovered(nearest.index);
|
|
328
374
|
showTooltip(mx + 10, my - 10, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
|
|
@@ -7,6 +7,7 @@ type __VLS_Props = {
|
|
|
7
7
|
labelField?: string;
|
|
8
8
|
animated?: boolean;
|
|
9
9
|
zoomable?: boolean;
|
|
10
|
+
wheelNavigation?: boolean;
|
|
10
11
|
maxZoom?: number;
|
|
11
12
|
colors?: string[];
|
|
12
13
|
backgroundColor?: string;
|
|
@@ -42,14 +43,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
42
43
|
seriesField: string;
|
|
43
44
|
animated: boolean;
|
|
44
45
|
zoomable: boolean;
|
|
46
|
+
wheelNavigation: boolean;
|
|
45
47
|
maxZoom: number;
|
|
46
48
|
colors: string[];
|
|
47
49
|
backgroundColor: string;
|
|
48
50
|
showLegend: boolean;
|
|
49
51
|
xAxisTitle: string;
|
|
50
52
|
yAxisTitle: string;
|
|
51
|
-
labelField: string;
|
|
52
53
|
sizeField: string;
|
|
54
|
+
labelField: string;
|
|
53
55
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
54
56
|
declare const _default: typeof __VLS_export;
|
|
55
57
|
export default _default;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref } from "vue";
|
|
1
|
+
import { ref, onScopeDispose } from "vue";
|
|
2
2
|
export function useChartAnimation(duration = 600) {
|
|
3
3
|
const progress = ref(0);
|
|
4
4
|
let rafId = null;
|
|
@@ -25,6 +25,12 @@ export function useChartAnimation(duration = 600) {
|
|
|
25
25
|
rafId = null;
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
try {
|
|
29
|
+
onScopeDispose(() => {
|
|
30
|
+
stop();
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
28
34
|
return { progress, animate, stop };
|
|
29
35
|
}
|
|
30
36
|
export function easeOut(t) {
|
|
@@ -15,5 +15,6 @@ export declare function extractProviderItems<T = any>(provider?: ADataProvider |
|
|
|
15
15
|
/**
|
|
16
16
|
* Provider-agnostic data watcher for Katlux chart components.
|
|
17
17
|
* Consumes data through Katlux DataProvider instances (Server-side or Client-side).
|
|
18
|
+
* Uses provider.subscribe() instead of Vue watch() so it works with __v_skip providers.
|
|
18
19
|
*/
|
|
19
20
|
export declare function useChartData<T = any>(dataProvider: Ref<ADataProvider | null | undefined> | ADataProvider | null | undefined): IChartDataContext<T>;
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { ref, watch, onMounted, getCurrentInstance, isRef } from "vue";
|
|
1
|
+
import { ref, watch, onMounted, onUnmounted, onScopeDispose, getCurrentScope, getCurrentInstance, isRef } from "vue";
|
|
2
2
|
export function extractProviderItems(provider) {
|
|
3
3
|
if (!provider) return [];
|
|
4
4
|
const instance = isRef(provider) ? provider.value : provider;
|
|
5
5
|
if (!instance) return [];
|
|
6
|
+
const hasActiveFilter = Boolean(
|
|
7
|
+
instance.filter && (typeof instance.filter === "object" && Object.keys(instance.filter).length > 0 || typeof instance.hasFilter === "boolean" && instance.hasFilter || typeof instance.hasFilter === "function" && instance.hasFilter())
|
|
8
|
+
);
|
|
6
9
|
const filtered = isRef(instance.filteredData) ? instance.filteredData.value : instance.filteredData;
|
|
7
|
-
if (Array.isArray(filtered)
|
|
8
|
-
|
|
10
|
+
if (Array.isArray(filtered)) {
|
|
11
|
+
if (hasActiveFilter) {
|
|
12
|
+
return filtered;
|
|
13
|
+
}
|
|
14
|
+
if (filtered.length > 0) {
|
|
15
|
+
return filtered;
|
|
16
|
+
}
|
|
9
17
|
}
|
|
10
18
|
const all = isRef(instance.allData) ? instance.allData.value : instance.allData;
|
|
11
19
|
if (Array.isArray(all) && all.length > 0) {
|
|
20
|
+
if (hasActiveFilter && Array.isArray(filtered)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
12
23
|
return all;
|
|
13
24
|
}
|
|
14
25
|
let raw = isRef(instance.pageData) ? instance.pageData.value : instance.pageData;
|
|
@@ -29,24 +40,49 @@ export function useChartData(dataProvider) {
|
|
|
29
40
|
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
30
41
|
if (provider) {
|
|
31
42
|
items.value = extractProviderItems(provider);
|
|
43
|
+
const rawLoading = provider.loading !== void 0 ? isRef(provider.loading) ? provider.loading.value : provider.loading?.value ?? provider.loading : false;
|
|
44
|
+
const rawInitial = provider.initialLoad !== void 0 ? isRef(provider.initialLoad) ? provider.initialLoad.value : provider.initialLoad?.value ?? provider.initialLoad : false;
|
|
45
|
+
loading.value = Boolean(rawLoading || rawInitial);
|
|
32
46
|
} else {
|
|
33
47
|
items.value = [];
|
|
48
|
+
loading.value = false;
|
|
34
49
|
}
|
|
35
50
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
51
|
+
let unsubscribe = null;
|
|
52
|
+
const setupSubscription = () => {
|
|
53
|
+
if (unsubscribe) {
|
|
54
|
+
unsubscribe();
|
|
55
|
+
unsubscribe = null;
|
|
56
|
+
}
|
|
57
|
+
const isServer = typeof window === "undefined" || typeof import.meta !== "undefined" && Boolean(import.meta.server);
|
|
58
|
+
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
59
|
+
if (!isServer && provider?.subscribe) {
|
|
60
|
+
unsubscribe = provider.subscribe(() => updateItems());
|
|
61
|
+
}
|
|
62
|
+
updateItems();
|
|
63
|
+
};
|
|
64
|
+
if (isRef(dataProvider)) {
|
|
65
|
+
watch(dataProvider, setupSubscription, { immediate: true });
|
|
66
|
+
} else {
|
|
67
|
+
setupSubscription();
|
|
68
|
+
}
|
|
69
|
+
if (getCurrentInstance()) {
|
|
70
|
+
onMounted(updateItems);
|
|
71
|
+
onUnmounted(() => {
|
|
72
|
+
if (unsubscribe) {
|
|
73
|
+
unsubscribe();
|
|
74
|
+
unsubscribe = null;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (getCurrentScope()) {
|
|
79
|
+
onScopeDispose(() => {
|
|
80
|
+
if (unsubscribe) {
|
|
81
|
+
unsubscribe();
|
|
82
|
+
unsubscribe = null;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
50
86
|
const reload = async () => {
|
|
51
87
|
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
52
88
|
if (!provider) return;
|
|
@@ -65,10 +101,5 @@ export function useChartData(dataProvider) {
|
|
|
65
101
|
loading.value = false;
|
|
66
102
|
}
|
|
67
103
|
};
|
|
68
|
-
if (getCurrentInstance()) {
|
|
69
|
-
onMounted(updateItems);
|
|
70
|
-
} else {
|
|
71
|
-
updateItems();
|
|
72
|
-
}
|
|
73
104
|
return { items, loading, error, reload };
|
|
74
105
|
}
|
|
@@ -28,8 +28,10 @@ export function useChartExport() {
|
|
|
28
28
|
imageEl.setAttribute("href", dataUrl);
|
|
29
29
|
imageEl.setAttribute("x", "0");
|
|
30
30
|
imageEl.setAttribute("y", "0");
|
|
31
|
-
const
|
|
32
|
-
const
|
|
31
|
+
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
32
|
+
const svgBox = svgEl.viewBox?.baseVal;
|
|
33
|
+
const w = svgBox && svgBox.width > 0 ? svgBox.width : canvas.style.width ? parseFloat(canvas.style.width) : svgEl.clientWidth || canvas.width / dpr || 400;
|
|
34
|
+
const h = svgBox && svgBox.height > 0 ? svgBox.height : canvas.style.height ? parseFloat(canvas.style.height) : svgEl.clientHeight || canvas.height / dpr || 400;
|
|
33
35
|
imageEl.setAttribute("width", String(w));
|
|
34
36
|
imageEl.setAttribute("height", String(h));
|
|
35
37
|
clone.insertBefore(imageEl, clone.firstChild);
|
|
@@ -39,7 +41,12 @@ export function useChartExport() {
|
|
|
39
41
|
const blob = new Blob([svgStr], { type: "image/svg+xml;charset=utf-8" });
|
|
40
42
|
const url = URL.createObjectURL(blob);
|
|
41
43
|
download(url, filename);
|
|
42
|
-
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
try {
|
|
46
|
+
URL.revokeObjectURL(url);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}, 1e3);
|
|
43
50
|
} catch (e) {
|
|
44
51
|
console.error("[KChart] SVG export failed:", e);
|
|
45
52
|
}
|
|
@@ -4,7 +4,7 @@ export declare function useChartHitTest(): {
|
|
|
4
4
|
item: any;
|
|
5
5
|
index: number;
|
|
6
6
|
} | null;
|
|
7
|
-
findNearestEuclidean: (items: any[], mouseX: number, mouseY: number, axes: IChartAxesContext, xField: string, yField: string, threshold?: number) => {
|
|
7
|
+
findNearestEuclidean: (items: any[], mouseX: number, mouseY: number, axes: IChartAxesContext, xField: string, yField: string, threshold?: number, xValueGetter?: (item: any, index: number) => number) => {
|
|
8
8
|
item: any;
|
|
9
9
|
index: number;
|
|
10
10
|
} | null;
|
|
@@ -16,12 +16,13 @@ export function useChartHitTest() {
|
|
|
16
16
|
}
|
|
17
17
|
return { item: items[nearestIdx], index: nearestIdx };
|
|
18
18
|
};
|
|
19
|
-
const findNearestEuclidean = (items, mouseX, mouseY, axes, xField, yField, threshold = 20) => {
|
|
19
|
+
const findNearestEuclidean = (items, mouseX, mouseY, axes, xField, yField, threshold = 20, xValueGetter) => {
|
|
20
20
|
if (!items.length) return null;
|
|
21
21
|
let nearestIdx = -1;
|
|
22
22
|
let minDistanceSq = threshold * threshold;
|
|
23
23
|
items.forEach((item, i) => {
|
|
24
|
-
const
|
|
24
|
+
const xVal = xValueGetter ? xValueGetter(item, i) : parseValue(item[xField], i);
|
|
25
|
+
const px = axes.toX(xVal);
|
|
25
26
|
const py = axes.toY(parseValue(item[yField]));
|
|
26
27
|
const dx = mouseX - px;
|
|
27
28
|
const dy = mouseY - py;
|
|
@@ -9,6 +9,7 @@ export interface IViewportState {
|
|
|
9
9
|
canPan: Ref<boolean>;
|
|
10
10
|
}
|
|
11
11
|
export interface IChartViewportContext extends IViewportState {
|
|
12
|
+
zoomTo: (newScale: number, anchorX?: number) => void;
|
|
12
13
|
zoomIn: () => void;
|
|
13
14
|
zoomOut: () => void;
|
|
14
15
|
resetZoom: () => void;
|
|
@@ -22,4 +23,6 @@ export declare function useChartViewport(options: {
|
|
|
22
23
|
maxZoom?: Ref<number>;
|
|
23
24
|
plotWidth: Ref<number>;
|
|
24
25
|
plotLeft?: Ref<number>;
|
|
26
|
+
zoomable?: Ref<boolean> | boolean;
|
|
27
|
+
wheelNavigation?: Ref<boolean> | boolean;
|
|
25
28
|
}): IChartViewportContext;
|