@katlux/block-charts 0.1.0-beta.8 → 0.1.1-alpha.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +200 -0
- package/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/components/KAreaChart/KAreaChart.d.vue.ts +2 -0
- package/dist/runtime/components/KAreaChart/KAreaChart.vue +271 -69
- package/dist/runtime/components/KAreaChart/KAreaChart.vue.d.ts +2 -0
- package/dist/runtime/components/KBarChart/KBarChart.d.vue.ts +2 -0
- package/dist/runtime/components/KBarChart/KBarChart.vue +323 -125
- package/dist/runtime/components/KBarChart/KBarChart.vue.d.ts +2 -0
- package/dist/runtime/components/KHeatMap/KHeatMap.vue +56 -21
- package/dist/runtime/components/KLineChart/KLineChart.d.vue.ts +2 -0
- package/dist/runtime/components/KLineChart/KLineChart.vue +179 -48
- package/dist/runtime/components/KLineChart/KLineChart.vue.d.ts +2 -0
- package/dist/runtime/components/KPieChart/KPieChart.vue +32 -29
- package/dist/runtime/components/KScatterChart/KScatterChart.d.vue.ts +3 -1
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +161 -49
- package/dist/runtime/components/KScatterChart/KScatterChart.vue.d.ts +3 -1
- package/dist/runtime/composables/chartUtils.d.ts +8 -0
- package/dist/runtime/composables/chartUtils.js +58 -0
- package/dist/runtime/composables/useChartAnimation.js +7 -1
- package/dist/runtime/composables/useChartAxes.d.ts +4 -2
- package/dist/runtime/composables/useChartAxes.js +32 -13
- package/dist/runtime/composables/useChartCanvas.d.ts +1 -0
- package/dist/runtime/composables/useChartCanvas.js +51 -18
- package/dist/runtime/composables/useChartData.d.ts +13 -7
- package/dist/runtime/composables/useChartData.js +99 -30
- package/dist/runtime/composables/useChartExport.d.ts +1 -1
- package/dist/runtime/composables/useChartExport.js +26 -3
- package/dist/runtime/composables/useChartHitTest.d.ts +2 -2
- package/dist/runtime/composables/useChartHitTest.js +7 -5
- package/dist/runtime/composables/useChartViewport.d.ts +3 -0
- package/dist/runtime/composables/useChartViewport.js +30 -7
- package/package.json +12 -7
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
.k-chart-wrapper(
|
|
3
3
|
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
4
|
-
|
|
5
|
-
@wheel.prevent="onWheel"
|
|
4
|
+
@wheel="onWheel"
|
|
6
5
|
@mousedown="onMouseDown"
|
|
7
6
|
@mousemove="onMouseMove"
|
|
8
7
|
@mouseup="onMouseUp"
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
:height="height"
|
|
23
22
|
)
|
|
24
23
|
defs
|
|
25
|
-
clipPath(id="plot-clip")
|
|
24
|
+
clipPath(:id="'plot-clip-' + clipId")
|
|
26
25
|
rect(
|
|
27
26
|
:x="axes.plotLeft"
|
|
28
27
|
:y="axes.plotTop"
|
|
@@ -30,10 +29,9 @@
|
|
|
30
29
|
:height="axes.plotHeight.value"
|
|
31
30
|
)
|
|
32
31
|
g.k-chart-hits
|
|
33
|
-
// No more rect v-for here.
|
|
34
32
|
|
|
35
33
|
// Dynamic Interaction Layer
|
|
36
|
-
g.k-chart-interaction(v-if="hoveredIndex !== -1" clip-path="url(#plot-clip)")
|
|
34
|
+
g.k-chart-interaction(v-if="hoveredIndex !== -1" :clip-path="'url(#plot-clip-' + clipId + ')'")
|
|
37
35
|
circle.k-chart-hit-proxy(
|
|
38
36
|
v-for="(pt, i) in hoveredPoints"
|
|
39
37
|
:key="i"
|
|
@@ -41,7 +39,6 @@
|
|
|
41
39
|
:cy="pt.y"
|
|
42
40
|
:r="10"
|
|
43
41
|
fill="transparent"
|
|
44
|
-
style="cursor: pointer"
|
|
45
42
|
@click="emit('click-point', pt.item, pt.index)"
|
|
46
43
|
)
|
|
47
44
|
circle(
|
|
@@ -57,14 +54,14 @@
|
|
|
57
54
|
)
|
|
58
55
|
.k-chart-tooltip(
|
|
59
56
|
v-if="tooltipState.visible && !isLoading"
|
|
60
|
-
:style="
|
|
57
|
+
:style="tooltipStyle"
|
|
61
58
|
)
|
|
62
59
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
63
60
|
span {{ tooltipState.content }}
|
|
64
61
|
</template>
|
|
65
62
|
|
|
66
63
|
<script setup>
|
|
67
|
-
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
64
|
+
import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
|
|
68
65
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
69
66
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
70
67
|
import { useChartData } from "../../composables/useChartData";
|
|
@@ -73,6 +70,8 @@ import { useChartViewport } from "../../composables/useChartViewport";
|
|
|
73
70
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
74
71
|
import { useChartExport } from "../../composables/useChartExport";
|
|
75
72
|
import { useChartHitTest } from "../../composables/useChartHitTest";
|
|
73
|
+
import { cleanNumber, safeMax } from "../../composables/chartUtils";
|
|
74
|
+
const clipId = useId();
|
|
76
75
|
const props = defineProps({
|
|
77
76
|
dataProvider: { type: null, required: false },
|
|
78
77
|
xField: { type: String, required: false, default: "x" },
|
|
@@ -80,6 +79,7 @@ const props = defineProps({
|
|
|
80
79
|
seriesField: { type: String, required: false, default: "series" },
|
|
81
80
|
animated: { type: Boolean, required: false, default: true },
|
|
82
81
|
zoomable: { type: Boolean, required: false, default: true },
|
|
82
|
+
wheelNavigation: { type: Boolean, required: false, default: true },
|
|
83
83
|
maxZoom: { type: Number, required: false, default: 10 },
|
|
84
84
|
stacked: { type: Boolean, required: false, default: false },
|
|
85
85
|
colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7"] },
|
|
@@ -92,19 +92,35 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
|
|
|
92
92
|
const containerRef = ref(null);
|
|
93
93
|
const canvasRef = ref(null);
|
|
94
94
|
const svgRef = ref(null);
|
|
95
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
95
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
96
96
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
97
97
|
const dataRef = computed(() => props.dataProvider);
|
|
98
|
-
const { items } = useChartData(dataRef);
|
|
98
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
99
99
|
const { progress, animate } = useChartAnimation();
|
|
100
100
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
101
101
|
const isLoading = computed(() => {
|
|
102
|
-
|
|
102
|
+
const dp = props.dataProvider;
|
|
103
|
+
if (!dp) return dataLoading.value;
|
|
104
|
+
const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
|
|
105
|
+
const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
|
|
106
|
+
return Boolean(dataLoading.value || rawLoading || rawInitial);
|
|
103
107
|
});
|
|
108
|
+
const tooltipStyle = computed(() => ({
|
|
109
|
+
left: `${tooltipState.value.x}px`,
|
|
110
|
+
top: `${tooltipState.value.y}px`
|
|
111
|
+
}));
|
|
104
112
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
105
113
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
106
114
|
const plotLeftRef = computed(() => 55);
|
|
107
|
-
const
|
|
115
|
+
const zoomableRef = computed(() => props.zoomable);
|
|
116
|
+
const wheelNavRef = computed(() => props.wheelNavigation);
|
|
117
|
+
const viewport = useChartViewport({
|
|
118
|
+
maxZoom: maxZoomRef,
|
|
119
|
+
plotWidth: plotWidthRef,
|
|
120
|
+
plotLeft: plotLeftRef,
|
|
121
|
+
zoomable: zoomableRef,
|
|
122
|
+
wheelNavigation: wheelNavRef
|
|
123
|
+
});
|
|
108
124
|
const { findNearestX } = useChartHitTest();
|
|
109
125
|
const {
|
|
110
126
|
scale,
|
|
@@ -122,6 +138,56 @@ const {
|
|
|
122
138
|
} = viewport;
|
|
123
139
|
const xFieldRef = computed(() => props.xField);
|
|
124
140
|
const yFieldRef = computed(() => props.yField);
|
|
141
|
+
const xAxisType = computed(() => {
|
|
142
|
+
if (!items.value.length) return "linear";
|
|
143
|
+
const sample = items.value.find((r) => r?.[props.xField] !== void 0 && r?.[props.xField] !== null)?.[props.xField];
|
|
144
|
+
if (sample === void 0 || sample === null) return "linear";
|
|
145
|
+
if (typeof sample === "number") return "linear";
|
|
146
|
+
if (typeof sample === "string") {
|
|
147
|
+
const cleaned = cleanNumber(sample, NaN);
|
|
148
|
+
if (Number.isFinite(cleaned)) return "linear";
|
|
149
|
+
const parsed = Date.parse(sample);
|
|
150
|
+
if (!isNaN(parsed) && !/^\d+$/.test(sample.trim()) && /[-/T:]|[A-Za-z]/.test(sample)) return "datetime";
|
|
151
|
+
return "category";
|
|
152
|
+
}
|
|
153
|
+
return "linear";
|
|
154
|
+
});
|
|
155
|
+
const isAllMidnight = computed(() => {
|
|
156
|
+
if (xAxisType.value !== "datetime" || !items.value.length) return false;
|
|
157
|
+
const checkCount = Math.min(items.value.length, 100);
|
|
158
|
+
for (let i = 0; i < checkCount; i++) {
|
|
159
|
+
const ts = parseValue(items.value[i][props.xField]);
|
|
160
|
+
const d = new Date(ts);
|
|
161
|
+
if (d.getHours() !== 0 || d.getMinutes() !== 0) return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
});
|
|
165
|
+
const categoryList = computed(() => {
|
|
166
|
+
if (xAxisType.value !== "category") return [];
|
|
167
|
+
const seen = /* @__PURE__ */ new Set();
|
|
168
|
+
const list = [];
|
|
169
|
+
for (const item of items.value) {
|
|
170
|
+
const val = String(item[props.xField] ?? "");
|
|
171
|
+
if (!seen.has(val)) {
|
|
172
|
+
seen.add(val);
|
|
173
|
+
list.push(val);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return list;
|
|
177
|
+
});
|
|
178
|
+
const categoryIndexMap = computed(() => {
|
|
179
|
+
const map = {};
|
|
180
|
+
categoryList.value.forEach((cat, i) => {
|
|
181
|
+
map[cat] = i;
|
|
182
|
+
});
|
|
183
|
+
return map;
|
|
184
|
+
});
|
|
185
|
+
const resolveX = (item, index = 0) => {
|
|
186
|
+
if (!item) return index;
|
|
187
|
+
const raw = item[props.xField];
|
|
188
|
+
if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
|
|
189
|
+
return parseValue(raw, index);
|
|
190
|
+
};
|
|
125
191
|
const seriesMap = computed(() => {
|
|
126
192
|
const map = /* @__PURE__ */ new Map();
|
|
127
193
|
for (const item of items.value) {
|
|
@@ -131,12 +197,60 @@ const seriesMap = computed(() => {
|
|
|
131
197
|
}
|
|
132
198
|
return map;
|
|
133
199
|
});
|
|
134
|
-
const
|
|
200
|
+
const sortedSeriesMap = computed(() => {
|
|
201
|
+
const map = /* @__PURE__ */ new Map();
|
|
202
|
+
const itemIndexMap = /* @__PURE__ */ new Map();
|
|
203
|
+
for (let i = 0; i < items.value.length; i++) {
|
|
204
|
+
itemIndexMap.set(items.value[i], i);
|
|
205
|
+
}
|
|
206
|
+
for (const [key, seriesItems] of seriesMap.value) {
|
|
207
|
+
const enriched = seriesItems.map((item, idx) => ({
|
|
208
|
+
item,
|
|
209
|
+
index: itemIndexMap.get(item) ?? idx,
|
|
210
|
+
xVal: resolveX(item, idx),
|
|
211
|
+
yVal: cleanNumber(item[props.yField])
|
|
212
|
+
})).sort((a, b) => a.xVal - b.xVal);
|
|
213
|
+
map.set(key, enriched);
|
|
214
|
+
}
|
|
215
|
+
return map;
|
|
216
|
+
});
|
|
217
|
+
const stackedMaxY = computed(() => {
|
|
218
|
+
if (!props.stacked) return 0;
|
|
219
|
+
const xTotals = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const [, seriesItems] of sortedSeriesMap.value) {
|
|
221
|
+
for (const pt of seriesItems) {
|
|
222
|
+
const current = xTotals.get(pt.xVal) || 0;
|
|
223
|
+
xTotals.set(pt.xVal, current + Math.max(0, pt.yVal));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return safeMax(Array.from(xTotals.values()), 10);
|
|
227
|
+
});
|
|
228
|
+
const axes = useChartAxes({
|
|
229
|
+
items,
|
|
230
|
+
xField: xFieldRef,
|
|
231
|
+
yField: yFieldRef,
|
|
232
|
+
width,
|
|
233
|
+
height,
|
|
234
|
+
scale,
|
|
235
|
+
panOffset,
|
|
236
|
+
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
237
|
+
xValueGetter: (r, i) => resolveX(r, i)
|
|
238
|
+
});
|
|
239
|
+
const effectiveYMax = computed(() => {
|
|
240
|
+
return props.stacked ? Math.max(stackedMaxY.value, axes.yMax.value) : axes.yMax.value;
|
|
241
|
+
});
|
|
242
|
+
const toStackedY = (val) => {
|
|
243
|
+
const yMin = props.stacked ? 0 : axes.yMin.value;
|
|
244
|
+
const yMax = effectiveYMax.value;
|
|
245
|
+
const range = yMax - yMin;
|
|
246
|
+
if (range === 0) return axes.plotTop + axes.plotHeight.value;
|
|
247
|
+
return axes.plotTop + axes.plotHeight.value - (val - yMin) / range * axes.plotHeight.value;
|
|
248
|
+
};
|
|
135
249
|
const draw = () => {
|
|
136
250
|
if (!ctx.value) return;
|
|
137
251
|
clear(props.backgroundColor);
|
|
138
252
|
const c = ctx.value;
|
|
139
|
-
const p = easeOut(progress.value);
|
|
253
|
+
const p = props.animated ? easeOut(progress.value) : 1;
|
|
140
254
|
c.save();
|
|
141
255
|
c.strokeStyle = "rgba(100,100,120,0.1)";
|
|
142
256
|
c.lineWidth = 1;
|
|
@@ -160,17 +274,40 @@ const draw = () => {
|
|
|
160
274
|
c.font = "11px system-ui, sans-serif";
|
|
161
275
|
c.textAlign = "right";
|
|
162
276
|
for (let i = 0; i <= 5; i++) {
|
|
163
|
-
const val = axes.yMin.value + (
|
|
164
|
-
c.fillText(val.toFixed(1), axes.plotLeft - 8,
|
|
277
|
+
const val = axes.yMin.value + (effectiveYMax.value - axes.yMin.value) * (i / 5);
|
|
278
|
+
c.fillText(val.toFixed(1), axes.plotLeft - 8, toStackedY(val) + 4);
|
|
165
279
|
}
|
|
166
280
|
c.textAlign = "center";
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
for (let i = 0; i <= 5; i++) {
|
|
295
|
+
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
|
|
296
|
+
const x = axes.toX(val);
|
|
297
|
+
let label;
|
|
298
|
+
if (xAxisType.value === "datetime") {
|
|
299
|
+
label = isAllMidnight.value ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
300
|
+
} else {
|
|
301
|
+
label = val.toFixed(1);
|
|
302
|
+
}
|
|
303
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
304
|
+
}
|
|
173
305
|
}
|
|
306
|
+
c.strokeStyle = "rgba(150,150,170,0.5)";
|
|
307
|
+
c.beginPath();
|
|
308
|
+
c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
309
|
+
c.lineTo(axes.plotLeft + axes.plotWidth.value, axes.plotTop + axes.plotHeight.value);
|
|
310
|
+
c.stroke();
|
|
174
311
|
c.beginPath();
|
|
175
312
|
c.moveTo(axes.plotLeft, axes.plotTop);
|
|
176
313
|
c.lineTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
@@ -194,41 +331,75 @@ const draw = () => {
|
|
|
194
331
|
c.beginPath();
|
|
195
332
|
c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
|
|
196
333
|
c.clip();
|
|
334
|
+
const currentBaseline = /* @__PURE__ */ new Map();
|
|
197
335
|
let seriesIdx = 0;
|
|
198
|
-
for (const [
|
|
336
|
+
for (const [, seriesItems] of sortedSeriesMap.value) {
|
|
199
337
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const pts = sorted.slice(0, drawCount);
|
|
338
|
+
const drawCount = Math.max(1, Math.floor(seriesItems.length * p));
|
|
339
|
+
const pts = seriesItems.slice(0, drawCount);
|
|
203
340
|
c.save();
|
|
204
341
|
c.beginPath();
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
342
|
+
if (props.stacked) {
|
|
343
|
+
const topPoints = [];
|
|
344
|
+
const bottomPoints = [];
|
|
345
|
+
for (const pt of pts) {
|
|
346
|
+
const x = axes.toX(pt.xVal);
|
|
347
|
+
const base = currentBaseline.get(pt.xVal) || 0;
|
|
348
|
+
const safeVal = Math.max(0, pt.yVal);
|
|
349
|
+
const top = base + safeVal;
|
|
350
|
+
const yTop = toStackedY(top);
|
|
351
|
+
const yBottom = toStackedY(base);
|
|
352
|
+
topPoints.push({ x, y: yTop });
|
|
353
|
+
bottomPoints.push({ x, y: yBottom });
|
|
354
|
+
currentBaseline.set(pt.xVal, top);
|
|
355
|
+
}
|
|
356
|
+
topPoints.forEach((pt, i) => {
|
|
357
|
+
if (i === 0) c.moveTo(pt.x, pt.y);
|
|
358
|
+
else c.lineTo(pt.x, pt.y);
|
|
359
|
+
});
|
|
360
|
+
for (let i = bottomPoints.length - 1; i >= 0; i--) {
|
|
361
|
+
c.lineTo(bottomPoints[i].x, bottomPoints[i].y);
|
|
362
|
+
}
|
|
217
363
|
c.closePath();
|
|
364
|
+
c.fillStyle = color + "44";
|
|
365
|
+
c.fill();
|
|
366
|
+
c.beginPath();
|
|
367
|
+
topPoints.forEach((pt, i) => {
|
|
368
|
+
if (i === 0) c.moveTo(pt.x, pt.y);
|
|
369
|
+
else c.lineTo(pt.x, pt.y);
|
|
370
|
+
});
|
|
371
|
+
c.strokeStyle = color;
|
|
372
|
+
c.lineWidth = 2;
|
|
373
|
+
c.stroke();
|
|
374
|
+
} else {
|
|
375
|
+
pts.forEach((pt, i) => {
|
|
376
|
+
const x = axes.toX(pt.xVal);
|
|
377
|
+
const y = toStackedY(pt.yVal);
|
|
378
|
+
if (i === 0) c.moveTo(x, y);
|
|
379
|
+
else c.lineTo(x, y);
|
|
380
|
+
});
|
|
381
|
+
if (pts.length > 0) {
|
|
382
|
+
const lastX = axes.toX(pts[pts.length - 1].xVal);
|
|
383
|
+
const firstX = axes.toX(pts[0].xVal);
|
|
384
|
+
const baselineY = axes.plotTop + axes.plotHeight.value;
|
|
385
|
+
c.lineTo(lastX, baselineY);
|
|
386
|
+
c.lineTo(firstX, baselineY);
|
|
387
|
+
c.closePath();
|
|
388
|
+
}
|
|
389
|
+
c.fillStyle = color + "33";
|
|
390
|
+
c.fill();
|
|
391
|
+
c.beginPath();
|
|
392
|
+
pts.forEach((pt, i) => {
|
|
393
|
+
const x = axes.toX(pt.xVal);
|
|
394
|
+
const y = toStackedY(pt.yVal);
|
|
395
|
+
if (i === 0) c.moveTo(x, y);
|
|
396
|
+
else c.lineTo(x, y);
|
|
397
|
+
});
|
|
398
|
+
c.strokeStyle = color;
|
|
399
|
+
c.lineWidth = 2;
|
|
400
|
+
c.lineJoin = "round";
|
|
401
|
+
c.stroke();
|
|
218
402
|
}
|
|
219
|
-
c.fillStyle = color + "33";
|
|
220
|
-
c.fill();
|
|
221
|
-
c.beginPath();
|
|
222
|
-
pts.forEach((item, i) => {
|
|
223
|
-
const x = axes.toX(parseValue(item[props.xField]));
|
|
224
|
-
const y = axes.toY(parseValue(item[props.yField]));
|
|
225
|
-
if (i === 0) c.moveTo(x, y);
|
|
226
|
-
else c.lineTo(x, y);
|
|
227
|
-
});
|
|
228
|
-
c.strokeStyle = color;
|
|
229
|
-
c.lineWidth = 2;
|
|
230
|
-
c.lineJoin = "round";
|
|
231
|
-
c.stroke();
|
|
232
403
|
c.restore();
|
|
233
404
|
seriesIdx++;
|
|
234
405
|
}
|
|
@@ -256,29 +427,43 @@ const drawLegend = (c) => {
|
|
|
256
427
|
});
|
|
257
428
|
c.restore();
|
|
258
429
|
};
|
|
259
|
-
watch(
|
|
430
|
+
watch(progress, draw);
|
|
431
|
+
watch([scale, panOffset, hoveredIndex, resizeTick, width, height, () => props.stacked], draw);
|
|
260
432
|
watch(items, () => {
|
|
261
433
|
if (props.animated) animate();
|
|
262
|
-
|
|
434
|
+
else draw();
|
|
435
|
+
});
|
|
263
436
|
watch(scale, (v) => emit("zoom-change", v));
|
|
264
|
-
watch(panOffset, (
|
|
437
|
+
watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
|
|
438
|
+
watch(() => items.value.length, (newLen) => {
|
|
439
|
+
if (hoveredIndex.value >= newLen) {
|
|
440
|
+
setHovered(-1);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
265
443
|
const hoveredPoints = computed(() => {
|
|
266
|
-
if (hoveredIndex.value === -1) return [];
|
|
444
|
+
if (hoveredIndex.value === -1 || hoveredIndex.value >= items.value.length) return [];
|
|
267
445
|
const item = items.value[hoveredIndex.value];
|
|
268
|
-
|
|
446
|
+
if (!item) return [];
|
|
447
|
+
const xVal = resolveX(item);
|
|
269
448
|
const xPx = axes.toX(xVal);
|
|
270
449
|
const pts = [];
|
|
271
450
|
let seriesIdx = 0;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
451
|
+
let cumulativeBase = 0;
|
|
452
|
+
for (const [, seriesList] of sortedSeriesMap.value) {
|
|
453
|
+
const found = seriesList.find((pt) => pt.xVal === xVal);
|
|
454
|
+
if (found) {
|
|
455
|
+
const safeVal = Math.max(0, found.yVal);
|
|
456
|
+
const top = props.stacked ? cumulativeBase + safeVal : found.yVal;
|
|
275
457
|
pts.push({
|
|
276
458
|
x: xPx,
|
|
277
|
-
y:
|
|
459
|
+
y: toStackedY(top),
|
|
278
460
|
color: props.colors[seriesIdx % props.colors.length],
|
|
279
|
-
item:
|
|
280
|
-
index:
|
|
461
|
+
item: found.item,
|
|
462
|
+
index: found.index
|
|
281
463
|
});
|
|
464
|
+
if (props.stacked) {
|
|
465
|
+
cumulativeBase += safeVal;
|
|
466
|
+
}
|
|
282
467
|
}
|
|
283
468
|
seriesIdx++;
|
|
284
469
|
}
|
|
@@ -293,7 +478,7 @@ onMounted(async () => {
|
|
|
293
478
|
}
|
|
294
479
|
});
|
|
295
480
|
const onMouseMove = (e) => {
|
|
296
|
-
|
|
481
|
+
onViewportMouseMove(e);
|
|
297
482
|
if (isDragging.value) {
|
|
298
483
|
setHovered(-1);
|
|
299
484
|
hideTooltip();
|
|
@@ -303,11 +488,24 @@ const onMouseMove = (e) => {
|
|
|
303
488
|
if (!rect) return;
|
|
304
489
|
const mouseX = e.clientX - rect.left;
|
|
305
490
|
const mouseY = e.clientY - rect.top;
|
|
306
|
-
|
|
491
|
+
if (mouseX < axes.plotLeft || mouseX > axes.plotLeft + axes.plotWidth.value || mouseY < axes.plotTop || mouseY > axes.plotTop + axes.plotHeight.value) {
|
|
492
|
+
setHovered(-1);
|
|
493
|
+
hideTooltip();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
|
|
307
497
|
if (nearest) {
|
|
308
498
|
setHovered(nearest.index);
|
|
309
|
-
showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
|
|
310
499
|
emit("hover-point", nearest.item, nearest.index);
|
|
500
|
+
const xPx = axes.toX(resolveX(nearest.item, nearest.index));
|
|
501
|
+
const yPx = toStackedY(cleanNumber(nearest.item[props.yField]));
|
|
502
|
+
showTooltip(
|
|
503
|
+
xPx,
|
|
504
|
+
yPx - 10,
|
|
505
|
+
`${props.xField}: ${nearest.item[props.xField]} | ${props.yField}: ${nearest.item[props.yField]}`,
|
|
506
|
+
nearest.item,
|
|
507
|
+
nearest.index
|
|
508
|
+
);
|
|
311
509
|
} else {
|
|
312
510
|
setHovered(-1);
|
|
313
511
|
hideTooltip();
|
|
@@ -323,7 +521,7 @@ defineExpose({
|
|
|
323
521
|
zoomOut: viewport.zoomOut,
|
|
324
522
|
resetZoom: viewport.resetZoom,
|
|
325
523
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "area-chart.png"),
|
|
326
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg")
|
|
524
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg", canvasRef.value)
|
|
327
525
|
});
|
|
328
526
|
</script>
|
|
329
527
|
|
|
@@ -331,6 +529,13 @@ defineExpose({
|
|
|
331
529
|
.k-chart-wrapper {
|
|
332
530
|
position: relative;
|
|
333
531
|
user-select: none;
|
|
532
|
+
cursor: default;
|
|
533
|
+
}
|
|
534
|
+
.k-chart-wrapper.k-chart--pannable {
|
|
535
|
+
cursor: grab;
|
|
536
|
+
}
|
|
537
|
+
.k-chart-wrapper.k-chart--grabbing {
|
|
538
|
+
cursor: grabbing;
|
|
334
539
|
}
|
|
335
540
|
.k-chart-wrapper .k-chart-controls {
|
|
336
541
|
position: absolute;
|
|
@@ -340,9 +545,6 @@ defineExpose({
|
|
|
340
545
|
display: flex;
|
|
341
546
|
gap: 4px;
|
|
342
547
|
}
|
|
343
|
-
.k-chart-wrapper .k-chart-btn {
|
|
344
|
-
display: none;
|
|
345
|
-
}
|
|
346
548
|
.k-chart-wrapper .k-chart-inner {
|
|
347
549
|
width: 100%;
|
|
348
550
|
height: 400px;
|
|
@@ -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
|
stacked?: boolean;
|
|
10
11
|
colors?: string[];
|
|
@@ -41,6 +42,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
41
42
|
seriesField: string;
|
|
42
43
|
animated: boolean;
|
|
43
44
|
zoomable: boolean;
|
|
45
|
+
wheelNavigation: boolean;
|
|
44
46
|
maxZoom: number;
|
|
45
47
|
stacked: boolean;
|
|
46
48
|
colors: string[];
|
|
@@ -7,6 +7,7 @@ type __VLS_Props = {
|
|
|
7
7
|
mode?: 'simple' | 'clustered' | 'stacked';
|
|
8
8
|
animated?: boolean;
|
|
9
9
|
zoomable?: boolean;
|
|
10
|
+
wheelNavigation?: boolean;
|
|
10
11
|
maxZoom?: number;
|
|
11
12
|
colors?: string[];
|
|
12
13
|
backgroundColor?: string;
|
|
@@ -39,6 +40,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
39
40
|
}>, {
|
|
40
41
|
animated: boolean;
|
|
41
42
|
zoomable: boolean;
|
|
43
|
+
wheelNavigation: boolean;
|
|
42
44
|
maxZoom: number;
|
|
43
45
|
colors: string[];
|
|
44
46
|
backgroundColor: string;
|