@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
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
:width="hoveredCell.w"
|
|
22
22
|
:height="hoveredCell.h"
|
|
23
23
|
fill="transparent"
|
|
24
|
-
style="cursor: pointer"
|
|
25
24
|
@click="emit('click-point', hoveredCell.item, hoveredIndex)"
|
|
26
25
|
)
|
|
27
26
|
rect(
|
|
@@ -37,17 +36,18 @@
|
|
|
37
36
|
)
|
|
38
37
|
|
|
39
38
|
// Labels remain in SVG for sharpness and easy styling
|
|
40
|
-
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="
|
|
39
|
+
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
|
|
41
40
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
42
41
|
span {{ tooltipState.content }}
|
|
43
42
|
</template>
|
|
44
43
|
|
|
45
44
|
<script setup>
|
|
46
|
-
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
45
|
+
import { ref, watch, computed, onMounted, nextTick, isRef } from "vue";
|
|
47
46
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
48
47
|
import { useChartData } from "../../composables/useChartData";
|
|
49
48
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
50
49
|
import { useChartExport } from "../../composables/useChartExport";
|
|
50
|
+
import { cleanNumber, safeMax, safeMin } from "../../composables/chartUtils";
|
|
51
51
|
const props = defineProps({
|
|
52
52
|
dataProvider: { type: null, required: false },
|
|
53
53
|
rowField: { type: String, required: false, default: "row" },
|
|
@@ -65,31 +65,57 @@ const emit = defineEmits(["click-point", "hover-point"]);
|
|
|
65
65
|
const containerRef = ref(null);
|
|
66
66
|
const canvasRef = ref(null);
|
|
67
67
|
const svgRef = ref(null);
|
|
68
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
68
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
69
69
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
70
70
|
const dataRef = computed(() => props.dataProvider);
|
|
71
|
-
const { items } = useChartData(dataRef);
|
|
71
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
72
72
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
73
73
|
const isLoading = computed(() => {
|
|
74
|
-
|
|
74
|
+
const dp = props.dataProvider;
|
|
75
|
+
if (!dp) return dataLoading.value;
|
|
76
|
+
const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
|
|
77
|
+
const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
|
|
78
|
+
return Boolean(dataLoading.value || rawLoading || rawInitial);
|
|
75
79
|
});
|
|
80
|
+
const tooltipStyle = computed(() => ({
|
|
81
|
+
left: `${tooltipState.value.x}px`,
|
|
82
|
+
top: `${tooltipState.value.y}px`
|
|
83
|
+
}));
|
|
76
84
|
const padLeft = 80;
|
|
77
85
|
const padTop = 30;
|
|
78
86
|
const padRight = 20;
|
|
79
87
|
const padBottom = 20;
|
|
80
88
|
const rows = computed(() => [...new Set(items.value.map((r) => String(r[props.rowField])))]);
|
|
81
89
|
const cols = computed(() => [...new Set(items.value.map((r) => String(r[props.columnField])))]);
|
|
90
|
+
const rowIndexMap = computed(() => {
|
|
91
|
+
const map = /* @__PURE__ */ new Map();
|
|
92
|
+
rows.value.forEach((r, i) => map.set(r, i));
|
|
93
|
+
return map;
|
|
94
|
+
});
|
|
95
|
+
const colIndexMap = computed(() => {
|
|
96
|
+
const map = /* @__PURE__ */ new Map();
|
|
97
|
+
cols.value.forEach((c, i) => map.set(c, i));
|
|
98
|
+
return map;
|
|
99
|
+
});
|
|
82
100
|
const cellW = computed(() => Math.max(8, (width.value - padLeft - padRight) / (cols.value.length || 1)));
|
|
83
101
|
const cellH = computed(() => Math.max(8, (height.value - padTop - padBottom) / (rows.value.length || 1)));
|
|
84
|
-
const maxVal = computed(() =>
|
|
102
|
+
const maxVal = computed(() => safeMax(items.value.map((r) => cleanNumber(r[props.valueField])), 1));
|
|
103
|
+
const minVal = computed(() => safeMin(items.value.map((r) => cleanNumber(r[props.valueField])), 0));
|
|
85
104
|
const hoveredCell = computed(() => {
|
|
86
105
|
if (hoveredIndex.value === -1) return null;
|
|
87
106
|
return renderedCells.value[hoveredIndex.value];
|
|
88
107
|
});
|
|
89
|
-
const
|
|
108
|
+
const parseHexColor = (colorStr, fallback) => {
|
|
109
|
+
let hex = (colorStr || fallback).replace("#", "").trim();
|
|
110
|
+
if (hex.length === 3) {
|
|
111
|
+
hex = hex.split("").map((c) => c + c).join("");
|
|
112
|
+
}
|
|
113
|
+
const num = parseInt(hex.padEnd(6, "0").slice(0, 6), 16);
|
|
114
|
+
return isNaN(num) ? parseInt(fallback.replace("#", ""), 16) : num;
|
|
115
|
+
};
|
|
90
116
|
const lerp = (t) => {
|
|
91
|
-
const low =
|
|
92
|
-
const high =
|
|
117
|
+
const low = parseHexColor(props.colorLow, "1e1e2e");
|
|
118
|
+
const high = parseHexColor(props.colorHigh, "6366f1");
|
|
93
119
|
const lr = low >> 16 & 255;
|
|
94
120
|
const lg = low >> 8 & 255;
|
|
95
121
|
const lb = low & 255;
|
|
@@ -104,9 +130,10 @@ const lerp = (t) => {
|
|
|
104
130
|
const renderedCells = computed(() => {
|
|
105
131
|
const range = maxVal.value - minVal.value || 1;
|
|
106
132
|
return items.value.map((item) => {
|
|
107
|
-
const ri =
|
|
108
|
-
const ci =
|
|
109
|
-
const
|
|
133
|
+
const ri = rowIndexMap.value.get(String(item[props.rowField])) ?? 0;
|
|
134
|
+
const ci = colIndexMap.value.get(String(item[props.columnField])) ?? 0;
|
|
135
|
+
const val = cleanNumber(item[props.valueField]);
|
|
136
|
+
const t = Math.max(0, Math.min(1, (val - minVal.value) / range));
|
|
110
137
|
return {
|
|
111
138
|
x: padLeft + ci * cellW.value,
|
|
112
139
|
y: padTop + ri * cellH.value,
|
|
@@ -114,7 +141,7 @@ const renderedCells = computed(() => {
|
|
|
114
141
|
h: cellH.value - 1,
|
|
115
142
|
color: lerp(t),
|
|
116
143
|
item,
|
|
117
|
-
label: `${item[props.rowField]} \xD7 ${item[props.columnField]}: ${
|
|
144
|
+
label: `${item[props.rowField]} \xD7 ${item[props.columnField]}: ${val}`
|
|
118
145
|
};
|
|
119
146
|
});
|
|
120
147
|
});
|
|
@@ -124,12 +151,9 @@ const draw = () => {
|
|
|
124
151
|
const c = ctx.value;
|
|
125
152
|
drawAxes(c);
|
|
126
153
|
renderedCells.value.forEach((cell, i) => {
|
|
127
|
-
const isHovered = hoveredIndex.value === i;
|
|
128
154
|
c.fillStyle = cell.color;
|
|
129
|
-
c.
|
|
130
|
-
|
|
131
|
-
c.fill();
|
|
132
|
-
if (isHovered) {
|
|
155
|
+
c.fillRect(cell.x, cell.y, cell.w, cell.h);
|
|
156
|
+
if (hoveredIndex.value === i) {
|
|
133
157
|
c.strokeStyle = "#fff";
|
|
134
158
|
c.lineWidth = 2;
|
|
135
159
|
c.strokeRect(cell.x + 1, cell.y + 1, cell.w - 2, cell.h - 2);
|
|
@@ -185,7 +209,18 @@ const drawLegend = (c) => {
|
|
|
185
209
|
c.fillText(maxVal.value.toString(), x + gradW, y + gradH + 10);
|
|
186
210
|
c.restore();
|
|
187
211
|
};
|
|
188
|
-
watch([renderedCells, hoveredIndex, width, height], draw);
|
|
212
|
+
watch([renderedCells, hoveredIndex, width, height, resizeTick], draw);
|
|
213
|
+
watch(items, draw);
|
|
214
|
+
watch(isLoading, (val) => {
|
|
215
|
+
if (!val) {
|
|
216
|
+
nextTick(() => {
|
|
217
|
+
if (canvasRef.value) {
|
|
218
|
+
setupCanvas(canvasRef.value);
|
|
219
|
+
draw();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
189
224
|
onMounted(async () => {
|
|
190
225
|
await nextTick();
|
|
191
226
|
if (containerRef.value && canvasRef.value) {
|
|
@@ -217,7 +252,7 @@ const onMouseLeave = () => {
|
|
|
217
252
|
};
|
|
218
253
|
defineExpose({
|
|
219
254
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "heatmap.png"),
|
|
220
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "heatmap.svg")
|
|
255
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "heatmap.svg", canvasRef.value)
|
|
221
256
|
});
|
|
222
257
|
</script>
|
|
223
258
|
|
|
@@ -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;
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
.k-chart-wrapper(
|
|
3
3
|
ref="wrapperRef"
|
|
4
4
|
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
5
|
-
|
|
6
|
-
@wheel.prevent="onWheel"
|
|
5
|
+
@wheel="onWheel"
|
|
7
6
|
@mousedown="onMouseDown"
|
|
8
7
|
@mousemove="onMouseMove"
|
|
9
8
|
@mouseup="onMouseUp"
|
|
@@ -23,7 +22,7 @@
|
|
|
23
22
|
:height="height"
|
|
24
23
|
)
|
|
25
24
|
defs
|
|
26
|
-
clipPath(id="plot-clip")
|
|
25
|
+
clipPath(:id="'plot-clip-' + clipId")
|
|
27
26
|
rect(
|
|
28
27
|
:x="axes.plotLeft"
|
|
29
28
|
:y="axes.plotTop"
|
|
@@ -31,7 +30,7 @@
|
|
|
31
30
|
:height="axes.plotHeight.value"
|
|
32
31
|
)
|
|
33
32
|
// Dynamic Interaction Layer: Repositions one set of circles to the nearest point
|
|
34
|
-
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 + ')'")
|
|
35
34
|
circle.k-chart-hit-proxy(
|
|
36
35
|
v-for="(pt, i) in hoveredPoints"
|
|
37
36
|
:key="i"
|
|
@@ -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,14 +55,14 @@
|
|
|
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 }}
|
|
64
62
|
</template>
|
|
65
63
|
|
|
66
64
|
<script setup>
|
|
67
|
-
import { ref, watch, computed, onMounted, onUnmounted, nextTick } from "vue";
|
|
65
|
+
import { ref, watch, computed, onMounted, onUnmounted, nextTick, useId, isRef } from "vue";
|
|
68
66
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
69
67
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
70
68
|
import { useChartData } from "../../composables/useChartData";
|
|
@@ -73,6 +71,8 @@ import { useChartViewport } from "../../composables/useChartViewport";
|
|
|
73
71
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
74
72
|
import { useChartExport } from "../../composables/useChartExport";
|
|
75
73
|
import { useChartHitTest } from "../../composables/useChartHitTest";
|
|
74
|
+
import { cleanNumber } from "../../composables/chartUtils";
|
|
75
|
+
const clipId = useId();
|
|
76
76
|
const props = defineProps({
|
|
77
77
|
dataProvider: { type: null, required: false },
|
|
78
78
|
xField: { type: String, required: false, default: "x" },
|
|
@@ -80,6 +80,7 @@ const props = defineProps({
|
|
|
80
80
|
seriesField: { type: String, required: false, default: "series" },
|
|
81
81
|
animated: { type: Boolean, required: false, default: true },
|
|
82
82
|
zoomable: { type: Boolean, required: false, default: true },
|
|
83
|
+
wheelNavigation: { type: Boolean, required: false, default: true },
|
|
83
84
|
maxZoom: { type: Number, required: false, default: 10 },
|
|
84
85
|
colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7", "#14b8a6", "#fb923c"] },
|
|
85
86
|
backgroundColor: { type: String, required: false, default: "#ffffff" },
|
|
@@ -92,19 +93,35 @@ const wrapperRef = ref(null);
|
|
|
92
93
|
const containerRef = ref(null);
|
|
93
94
|
const canvasRef = ref(null);
|
|
94
95
|
const svgRef = ref(null);
|
|
95
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
96
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
96
97
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
97
98
|
const dataRef = computed(() => props.dataProvider);
|
|
98
|
-
const { items, loading } = useChartData(dataRef);
|
|
99
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
99
100
|
const { progress, animate } = useChartAnimation();
|
|
100
101
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
101
102
|
const isLoading = computed(() => {
|
|
102
|
-
|
|
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);
|
|
103
108
|
});
|
|
109
|
+
const tooltipStyle = computed(() => ({
|
|
110
|
+
left: `${tooltipState.value.x}px`,
|
|
111
|
+
top: `${tooltipState.value.y}px`
|
|
112
|
+
}));
|
|
104
113
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
105
114
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
106
115
|
const plotLeftRef = computed(() => 55);
|
|
107
|
-
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
|
+
});
|
|
108
125
|
const { findNearestX } = useChartHitTest();
|
|
109
126
|
const {
|
|
110
127
|
scale,
|
|
@@ -120,8 +137,56 @@ const {
|
|
|
120
137
|
onMouseUp,
|
|
121
138
|
onMouseLeave: onViewportMouseLeave
|
|
122
139
|
} = viewport;
|
|
123
|
-
const
|
|
124
|
-
|
|
140
|
+
const xAxisType = computed(() => {
|
|
141
|
+
if (!items.value.length) return "linear";
|
|
142
|
+
const sample = items.value.find((r) => r?.[props.xField] !== void 0 && r?.[props.xField] !== null)?.[props.xField];
|
|
143
|
+
if (sample === void 0 || sample === null) return "linear";
|
|
144
|
+
if (typeof sample === "number") return "linear";
|
|
145
|
+
if (typeof sample === "string") {
|
|
146
|
+
const cleaned = cleanNumber(sample, NaN);
|
|
147
|
+
if (Number.isFinite(cleaned)) return "linear";
|
|
148
|
+
const parsed = Date.parse(sample);
|
|
149
|
+
if (!isNaN(parsed) && !/^\d+$/.test(sample.trim()) && /[-/T:]|[A-Za-z]/.test(sample)) return "datetime";
|
|
150
|
+
return "category";
|
|
151
|
+
}
|
|
152
|
+
return "linear";
|
|
153
|
+
});
|
|
154
|
+
const isAllMidnight = computed(() => {
|
|
155
|
+
if (xAxisType.value !== "datetime" || !items.value.length) return false;
|
|
156
|
+
const checkCount = Math.min(items.value.length, 100);
|
|
157
|
+
for (let i = 0; i < checkCount; i++) {
|
|
158
|
+
const ts = parseValue(items.value[i][props.xField]);
|
|
159
|
+
const d = new Date(ts);
|
|
160
|
+
if (d.getHours() !== 0 || d.getMinutes() !== 0) return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
const categoryList = computed(() => {
|
|
165
|
+
if (xAxisType.value !== "category") return [];
|
|
166
|
+
const seen = /* @__PURE__ */ new Set();
|
|
167
|
+
const list = [];
|
|
168
|
+
for (const item of items.value) {
|
|
169
|
+
const val = String(item[props.xField] ?? "");
|
|
170
|
+
if (!seen.has(val)) {
|
|
171
|
+
seen.add(val);
|
|
172
|
+
list.push(val);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return list;
|
|
176
|
+
});
|
|
177
|
+
const categoryIndexMap = computed(() => {
|
|
178
|
+
const map = {};
|
|
179
|
+
categoryList.value.forEach((cat, i) => {
|
|
180
|
+
map[cat] = i;
|
|
181
|
+
});
|
|
182
|
+
return map;
|
|
183
|
+
});
|
|
184
|
+
const resolveX = (item, index = 0) => {
|
|
185
|
+
if (!item) return index;
|
|
186
|
+
const raw = item[props.xField];
|
|
187
|
+
if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
|
|
188
|
+
return parseValue(raw, index);
|
|
189
|
+
};
|
|
125
190
|
const seriesMap = computed(() => {
|
|
126
191
|
const map = /* @__PURE__ */ new Map();
|
|
127
192
|
for (const item of items.value) {
|
|
@@ -131,6 +196,24 @@ const seriesMap = computed(() => {
|
|
|
131
196
|
}
|
|
132
197
|
return map;
|
|
133
198
|
});
|
|
199
|
+
const sortedSeriesMap = computed(() => {
|
|
200
|
+
const map = /* @__PURE__ */ new Map();
|
|
201
|
+
const itemIndexMap = /* @__PURE__ */ new Map();
|
|
202
|
+
for (let i = 0; i < items.value.length; i++) {
|
|
203
|
+
itemIndexMap.set(items.value[i], i);
|
|
204
|
+
}
|
|
205
|
+
for (const [key, seriesItems] of seriesMap.value) {
|
|
206
|
+
const enriched = seriesItems.map((item, idx) => ({
|
|
207
|
+
item,
|
|
208
|
+
index: itemIndexMap.get(item) ?? idx,
|
|
209
|
+
xVal: resolveX(item, idx)
|
|
210
|
+
})).sort((a, b) => a.xVal - b.xVal);
|
|
211
|
+
map.set(key, enriched);
|
|
212
|
+
}
|
|
213
|
+
return map;
|
|
214
|
+
});
|
|
215
|
+
const xFieldRef = computed(() => props.xField);
|
|
216
|
+
const yFieldRef = computed(() => props.yField);
|
|
134
217
|
const axes = useChartAxes({
|
|
135
218
|
items,
|
|
136
219
|
xField: xFieldRef,
|
|
@@ -139,13 +222,14 @@ const axes = useChartAxes({
|
|
|
139
222
|
height,
|
|
140
223
|
scale,
|
|
141
224
|
panOffset,
|
|
142
|
-
padding: { top: 20, right: 20, bottom: 40, left: 55 }
|
|
225
|
+
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
226
|
+
xValueGetter: (r, i) => resolveX(r, i)
|
|
143
227
|
});
|
|
144
228
|
const draw = () => {
|
|
145
229
|
if (!ctx.value) return;
|
|
146
230
|
clear(props.backgroundColor);
|
|
147
231
|
const c = ctx.value;
|
|
148
|
-
const p = easeOut(progress.value);
|
|
232
|
+
const p = props.animated ? easeOut(progress.value) : 1;
|
|
149
233
|
drawGrid(c);
|
|
150
234
|
drawAxes(c);
|
|
151
235
|
c.save();
|
|
@@ -153,9 +237,9 @@ const draw = () => {
|
|
|
153
237
|
c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
|
|
154
238
|
c.clip();
|
|
155
239
|
let seriesIdx = 0;
|
|
156
|
-
for (const [,
|
|
240
|
+
for (const [, seriesData] of sortedSeriesMap.value) {
|
|
157
241
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
158
|
-
drawLine(c,
|
|
242
|
+
drawLine(c, seriesData, color, p);
|
|
159
243
|
seriesIdx++;
|
|
160
244
|
}
|
|
161
245
|
c.restore();
|
|
@@ -220,13 +304,31 @@ const drawAxes = (c) => {
|
|
|
220
304
|
c.fillText(val.toFixed(1), axes.plotLeft - 8, y + 4);
|
|
221
305
|
}
|
|
222
306
|
c.textAlign = "center";
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
307
|
+
if (xAxisType.value === "category") {
|
|
308
|
+
const count = categoryList.value.length;
|
|
309
|
+
if (count > 0) {
|
|
310
|
+
const xSteps = Math.min(10, count - 1);
|
|
311
|
+
const stepSize = xSteps > 0 ? (count - 1) / xSteps : 1;
|
|
312
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
313
|
+
const idx = Math.round(i * stepSize);
|
|
314
|
+
const x = axes.toX(idx);
|
|
315
|
+
const label = categoryList.value[idx] ?? "";
|
|
316
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
const xSteps = 5;
|
|
321
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
322
|
+
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / xSteps);
|
|
323
|
+
const x = axes.toX(val);
|
|
324
|
+
let label;
|
|
325
|
+
if (xAxisType.value === "datetime") {
|
|
326
|
+
label = isAllMidnight.value ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
327
|
+
} else {
|
|
328
|
+
label = val.toFixed(1);
|
|
329
|
+
}
|
|
330
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
331
|
+
}
|
|
230
332
|
}
|
|
231
333
|
c.beginPath();
|
|
232
334
|
c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
@@ -253,8 +355,7 @@ const drawAxes = (c) => {
|
|
|
253
355
|
};
|
|
254
356
|
const drawLine = (c, data, color, progress2) => {
|
|
255
357
|
if (!data.length) return;
|
|
256
|
-
const
|
|
257
|
-
const totalPts = sorted.length;
|
|
358
|
+
const totalPts = data.length;
|
|
258
359
|
const drawCount = Math.max(1, Math.floor(totalPts * progress2));
|
|
259
360
|
c.save();
|
|
260
361
|
c.strokeStyle = color;
|
|
@@ -262,18 +363,20 @@ const drawLine = (c, data, color, progress2) => {
|
|
|
262
363
|
c.lineJoin = "round";
|
|
263
364
|
c.lineCap = "round";
|
|
264
365
|
c.beginPath();
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const
|
|
366
|
+
for (let i = 0; i < drawCount; i++) {
|
|
367
|
+
const pt = data[i];
|
|
368
|
+
const x = axes.toX(pt.xVal);
|
|
369
|
+
const y = axes.toY(parseValue(pt.item[props.yField]));
|
|
268
370
|
if (i === 0) c.moveTo(x, y);
|
|
269
371
|
else c.lineTo(x, y);
|
|
270
|
-
}
|
|
372
|
+
}
|
|
271
373
|
c.stroke();
|
|
374
|
+
const currentHoveredIdx = hoveredIndex.value;
|
|
272
375
|
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 =
|
|
376
|
+
const pt = data[i];
|
|
377
|
+
const x = axes.toX(pt.xVal);
|
|
378
|
+
const y = axes.toY(parseValue(pt.item[props.yField]));
|
|
379
|
+
const isHovered = currentHoveredIdx !== -1 && pt.index === currentHoveredIdx;
|
|
277
380
|
c.beginPath();
|
|
278
381
|
c.arc(x, y, isHovered ? 5 : 3, 0, Math.PI * 2);
|
|
279
382
|
c.fillStyle = isHovered ? "#fff" : color;
|
|
@@ -284,12 +387,14 @@ const drawLine = (c, data, color, progress2) => {
|
|
|
284
387
|
}
|
|
285
388
|
c.restore();
|
|
286
389
|
};
|
|
287
|
-
watch(
|
|
390
|
+
watch(progress, draw);
|
|
391
|
+
watch([scale, panOffset, resizeTick, width, height, hoveredIndex], draw);
|
|
288
392
|
watch(items, () => {
|
|
289
393
|
if (props.animated) animate();
|
|
290
|
-
|
|
394
|
+
else draw();
|
|
395
|
+
});
|
|
291
396
|
watch(scale, (v) => emit("zoom-change", v));
|
|
292
|
-
watch(panOffset, (
|
|
397
|
+
watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
|
|
293
398
|
onMounted(async () => {
|
|
294
399
|
await nextTick();
|
|
295
400
|
if (containerRef.value && canvasRef.value) {
|
|
@@ -298,22 +403,28 @@ onMounted(async () => {
|
|
|
298
403
|
else draw();
|
|
299
404
|
}
|
|
300
405
|
});
|
|
406
|
+
watch(() => items.value.length, (newLen) => {
|
|
407
|
+
if (hoveredIndex.value >= newLen) {
|
|
408
|
+
setHovered(-1);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
301
411
|
const hoveredPoints = computed(() => {
|
|
302
|
-
if (hoveredIndex.value === -1) return [];
|
|
412
|
+
if (hoveredIndex.value === -1 || hoveredIndex.value >= items.value.length) return [];
|
|
303
413
|
const item = items.value[hoveredIndex.value];
|
|
304
|
-
|
|
414
|
+
if (!item) return [];
|
|
415
|
+
const xVal = resolveX(item);
|
|
305
416
|
const xPx = axes.toX(xVal);
|
|
306
417
|
const pts = [];
|
|
307
418
|
let seriesIdx = 0;
|
|
308
|
-
for (const [
|
|
309
|
-
const
|
|
310
|
-
if (
|
|
419
|
+
for (const [, seriesList] of sortedSeriesMap.value) {
|
|
420
|
+
const found = seriesList.find((pt) => pt.xVal === xVal);
|
|
421
|
+
if (found) {
|
|
311
422
|
pts.push({
|
|
312
423
|
x: xPx,
|
|
313
|
-
y: axes.toY(parseValue(
|
|
424
|
+
y: axes.toY(parseValue(found.item[props.yField])),
|
|
314
425
|
color: props.colors[seriesIdx % props.colors.length],
|
|
315
|
-
item:
|
|
316
|
-
index:
|
|
426
|
+
item: found.item,
|
|
427
|
+
index: found.index
|
|
317
428
|
});
|
|
318
429
|
}
|
|
319
430
|
seriesIdx++;
|
|
@@ -331,11 +442,24 @@ const onMouseMove = (e) => {
|
|
|
331
442
|
if (!rect) return;
|
|
332
443
|
const mouseX = e.clientX - rect.left;
|
|
333
444
|
const mouseY = e.clientY - rect.top;
|
|
334
|
-
|
|
445
|
+
if (mouseX < axes.plotLeft || mouseX > axes.plotLeft + axes.plotWidth.value || mouseY < axes.plotTop || mouseY > axes.plotTop + axes.plotHeight.value) {
|
|
446
|
+
setHovered(-1);
|
|
447
|
+
hideTooltip();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
|
|
335
451
|
if (nearest) {
|
|
336
452
|
setHovered(nearest.index);
|
|
337
|
-
showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
|
|
338
453
|
emit("hover-point", nearest.item, nearest.index);
|
|
454
|
+
const xPx = axes.toX(resolveX(nearest.item, nearest.index));
|
|
455
|
+
const yPx = axes.toY(parseValue(nearest.item[props.yField]));
|
|
456
|
+
showTooltip(
|
|
457
|
+
xPx,
|
|
458
|
+
yPx - 10,
|
|
459
|
+
`${props.xField}: ${nearest.item[props.xField]} | ${props.yField}: ${nearest.item[props.yField]}`,
|
|
460
|
+
nearest.item,
|
|
461
|
+
nearest.index
|
|
462
|
+
);
|
|
339
463
|
} else {
|
|
340
464
|
setHovered(-1);
|
|
341
465
|
hideTooltip();
|
|
@@ -351,7 +475,7 @@ defineExpose({
|
|
|
351
475
|
zoomOut: viewport.zoomOut,
|
|
352
476
|
resetZoom: viewport.resetZoom,
|
|
353
477
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "line-chart.png"),
|
|
354
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "line-chart.svg")
|
|
478
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "line-chart.svg", canvasRef.value)
|
|
355
479
|
});
|
|
356
480
|
</script>
|
|
357
481
|
|
|
@@ -359,6 +483,13 @@ defineExpose({
|
|
|
359
483
|
.k-chart-wrapper {
|
|
360
484
|
position: relative;
|
|
361
485
|
user-select: none;
|
|
486
|
+
cursor: default;
|
|
487
|
+
}
|
|
488
|
+
.k-chart-wrapper.k-chart--pannable {
|
|
489
|
+
cursor: grab;
|
|
490
|
+
}
|
|
491
|
+
.k-chart-wrapper.k-chart--grabbing {
|
|
492
|
+
cursor: grabbing;
|
|
362
493
|
}
|
|
363
494
|
.k-chart-wrapper .k-chart-controls {
|
|
364
495
|
position: absolute;
|
|
@@ -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;
|