@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
package/dist/module.json
CHANGED
|
@@ -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[];
|
|
@@ -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"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
:height="height"
|
|
22
22
|
)
|
|
23
23
|
defs
|
|
24
|
-
clipPath(id="plot-clip")
|
|
24
|
+
clipPath(:id="'plot-clip-' + clipId")
|
|
25
25
|
rect(
|
|
26
26
|
:x="axes.plotLeft"
|
|
27
27
|
:y="axes.plotTop"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
g.k-chart-hits
|
|
32
32
|
|
|
33
33
|
// Dynamic Interaction Layer
|
|
34
|
-
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 + ')'")
|
|
35
35
|
circle.k-chart-hit-proxy(
|
|
36
36
|
v-for="(pt, i) in hoveredPoints"
|
|
37
37
|
:key="i"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
</template>
|
|
62
62
|
|
|
63
63
|
<script setup>
|
|
64
|
-
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
64
|
+
import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
|
|
65
65
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
66
66
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
67
67
|
import { useChartData } from "../../composables/useChartData";
|
|
@@ -71,6 +71,7 @@ import { useChartAnimation, easeOut } from "../../composables/useChartAnimation"
|
|
|
71
71
|
import { useChartExport } from "../../composables/useChartExport";
|
|
72
72
|
import { useChartHitTest } from "../../composables/useChartHitTest";
|
|
73
73
|
import { cleanNumber, safeMax } from "../../composables/chartUtils";
|
|
74
|
+
const clipId = useId();
|
|
74
75
|
const props = defineProps({
|
|
75
76
|
dataProvider: { type: null, required: false },
|
|
76
77
|
xField: { type: String, required: false, default: "x" },
|
|
@@ -78,6 +79,7 @@ const props = defineProps({
|
|
|
78
79
|
seriesField: { type: String, required: false, default: "series" },
|
|
79
80
|
animated: { type: Boolean, required: false, default: true },
|
|
80
81
|
zoomable: { type: Boolean, required: false, default: true },
|
|
82
|
+
wheelNavigation: { type: Boolean, required: false, default: true },
|
|
81
83
|
maxZoom: { type: Number, required: false, default: 10 },
|
|
82
84
|
stacked: { type: Boolean, required: false, default: false },
|
|
83
85
|
colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7"] },
|
|
@@ -93,11 +95,15 @@ const svgRef = ref(null);
|
|
|
93
95
|
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
94
96
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
95
97
|
const dataRef = computed(() => props.dataProvider);
|
|
96
|
-
const { items } = useChartData(dataRef);
|
|
98
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
97
99
|
const { progress, animate } = useChartAnimation();
|
|
98
100
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
99
101
|
const isLoading = computed(() => {
|
|
100
|
-
|
|
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);
|
|
101
107
|
});
|
|
102
108
|
const tooltipStyle = computed(() => ({
|
|
103
109
|
left: `${tooltipState.value.x}px`,
|
|
@@ -106,7 +112,15 @@ const tooltipStyle = computed(() => ({
|
|
|
106
112
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
107
113
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
108
114
|
const plotLeftRef = computed(() => 55);
|
|
109
|
-
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
|
+
});
|
|
110
124
|
const { findNearestX } = useChartHitTest();
|
|
111
125
|
const {
|
|
112
126
|
scale,
|
|
@@ -129,14 +143,24 @@ const xAxisType = computed(() => {
|
|
|
129
143
|
const firstVal = items.value[0]?.[props.xField];
|
|
130
144
|
if (typeof firstVal === "number") return "linear";
|
|
131
145
|
if (typeof firstVal === "string") {
|
|
146
|
+
const cleaned = cleanNumber(firstVal, NaN);
|
|
147
|
+
if (Number.isFinite(cleaned)) return "linear";
|
|
132
148
|
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";
|
|
149
|
+
if (!isNaN(parsed) && !/^\d+$/.test(firstVal.trim()) && /[-/T:]|[A-Za-z]/.test(firstVal)) return "datetime";
|
|
136
150
|
return "category";
|
|
137
151
|
}
|
|
138
152
|
return "linear";
|
|
139
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
|
+
});
|
|
140
164
|
const categoryList = computed(() => {
|
|
141
165
|
if (xAxisType.value !== "category") return [];
|
|
142
166
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -215,7 +239,7 @@ const effectiveYMax = computed(() => {
|
|
|
215
239
|
return props.stacked ? Math.max(stackedMaxY.value, axes.yMax.value) : axes.yMax.value;
|
|
216
240
|
});
|
|
217
241
|
const toStackedY = (val) => {
|
|
218
|
-
const yMin = axes.yMin.value;
|
|
242
|
+
const yMin = props.stacked ? 0 : axes.yMin.value;
|
|
219
243
|
const yMax = effectiveYMax.value;
|
|
220
244
|
const range = yMax - yMin;
|
|
221
245
|
if (range === 0) return axes.plotTop + axes.plotHeight.value;
|
|
@@ -271,12 +295,7 @@ const draw = () => {
|
|
|
271
295
|
const x = axes.toX(val);
|
|
272
296
|
let label;
|
|
273
297
|
if (xAxisType.value === "datetime") {
|
|
274
|
-
|
|
275
|
-
const ts = parseValue(r[props.xField]);
|
|
276
|
-
const d = new Date(ts);
|
|
277
|
-
return d.getHours() === 0 && d.getMinutes() === 0;
|
|
278
|
-
});
|
|
279
|
-
label = allMidnight ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
298
|
+
label = isAllMidnight.value ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
280
299
|
} else {
|
|
281
300
|
label = val.toFixed(1);
|
|
282
301
|
}
|
|
@@ -325,7 +344,8 @@ const draw = () => {
|
|
|
325
344
|
for (const pt of pts) {
|
|
326
345
|
const x = axes.toX(pt.xVal);
|
|
327
346
|
const base = currentBaseline.get(pt.xVal) || 0;
|
|
328
|
-
const
|
|
347
|
+
const safeVal = Math.max(0, pt.yVal);
|
|
348
|
+
const top = base + safeVal;
|
|
329
349
|
const yTop = toStackedY(top);
|
|
330
350
|
const yBottom = toStackedY(base);
|
|
331
351
|
topPoints.push({ x, y: yTop });
|
|
@@ -413,28 +433,36 @@ watch(items, () => {
|
|
|
413
433
|
else draw();
|
|
414
434
|
});
|
|
415
435
|
watch(scale, (v) => emit("zoom-change", v));
|
|
416
|
-
watch(panOffset, (
|
|
417
|
-
|
|
418
|
-
if (hoveredIndex.value
|
|
419
|
-
if (hoveredIndex.value >= items.value.length) {
|
|
436
|
+
watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
|
|
437
|
+
watch(() => items.value.length, (newLen) => {
|
|
438
|
+
if (hoveredIndex.value >= newLen) {
|
|
420
439
|
setHovered(-1);
|
|
421
|
-
return [];
|
|
422
440
|
}
|
|
441
|
+
});
|
|
442
|
+
const hoveredPoints = computed(() => {
|
|
443
|
+
if (hoveredIndex.value === -1 || hoveredIndex.value >= items.value.length) return [];
|
|
423
444
|
const item = items.value[hoveredIndex.value];
|
|
445
|
+
if (!item) return [];
|
|
424
446
|
const xVal = resolveX(item);
|
|
425
447
|
const xPx = axes.toX(xVal);
|
|
426
448
|
const pts = [];
|
|
427
449
|
let seriesIdx = 0;
|
|
450
|
+
let cumulativeBase = 0;
|
|
428
451
|
for (const [, seriesList] of sortedSeriesMap.value) {
|
|
429
452
|
const found = seriesList.find((pt) => pt.xVal === xVal);
|
|
430
453
|
if (found) {
|
|
454
|
+
const safeVal = Math.max(0, found.yVal);
|
|
455
|
+
const top = props.stacked ? cumulativeBase + safeVal : found.yVal;
|
|
431
456
|
pts.push({
|
|
432
457
|
x: xPx,
|
|
433
|
-
y: toStackedY(
|
|
458
|
+
y: toStackedY(top),
|
|
434
459
|
color: props.colors[seriesIdx % props.colors.length],
|
|
435
460
|
item: found.item,
|
|
436
461
|
index: found.index
|
|
437
462
|
});
|
|
463
|
+
if (props.stacked) {
|
|
464
|
+
cumulativeBase += safeVal;
|
|
465
|
+
}
|
|
438
466
|
}
|
|
439
467
|
seriesIdx++;
|
|
440
468
|
}
|
|
@@ -464,7 +492,7 @@ const onMouseMove = (e) => {
|
|
|
464
492
|
hideTooltip();
|
|
465
493
|
return;
|
|
466
494
|
}
|
|
467
|
-
const nearest = findNearestX(mouseX,
|
|
495
|
+
const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
|
|
468
496
|
if (nearest) {
|
|
469
497
|
setHovered(nearest.index);
|
|
470
498
|
emit("hover-point", nearest.item, nearest.index);
|
|
@@ -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;
|
|
@@ -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="plotLeft"
|
|
22
22
|
:y="plotTop"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// Hit detection is handled by calculation in onMouseMove
|
|
28
28
|
|
|
29
29
|
// Dynamic Interaction Layer
|
|
30
|
-
g.k-chart-interaction(v-if="hoveredIndex !== -1 && hoveredBar" clip-path="url(#plot-clip)")
|
|
30
|
+
g.k-chart-interaction(v-if="hoveredIndex !== -1 && hoveredBar" :clip-path="'url(#plot-clip-' + clipId + ')'")
|
|
31
31
|
rect.k-chart-hit-proxy(
|
|
32
32
|
:x="hoveredBar.x"
|
|
33
33
|
:y="hoveredBar.y"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
fill="transparent"
|
|
45
45
|
stroke="#fff"
|
|
46
46
|
stroke-width="2"
|
|
47
|
-
|
|
47
|
+
stroke-dasharray="4,2"
|
|
48
48
|
pointer-events="none"
|
|
49
49
|
)
|
|
50
50
|
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
</template>
|
|
54
54
|
|
|
55
55
|
<script setup>
|
|
56
|
-
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
56
|
+
import { ref, watch, computed, onMounted, nextTick, useId, isRef } from "vue";
|
|
57
57
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
58
58
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
59
59
|
import { useChartData } from "../../composables/useChartData";
|
|
@@ -61,6 +61,7 @@ import { useChartViewport } from "../../composables/useChartViewport";
|
|
|
61
61
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
62
62
|
import { useChartExport } from "../../composables/useChartExport";
|
|
63
63
|
import { cleanNumber, safeMax } from "../../composables/chartUtils";
|
|
64
|
+
const clipId = useId();
|
|
64
65
|
const props = defineProps({
|
|
65
66
|
dataProvider: { type: null, required: false },
|
|
66
67
|
categoryField: { type: String, required: false, default: "category" },
|
|
@@ -70,6 +71,7 @@ const props = defineProps({
|
|
|
70
71
|
mode: { type: String, required: false, default: "clustered" },
|
|
71
72
|
animated: { type: Boolean, required: false, default: true },
|
|
72
73
|
zoomable: { type: Boolean, required: false, default: true },
|
|
74
|
+
wheelNavigation: { type: Boolean, required: false, default: true },
|
|
73
75
|
maxZoom: { type: Number, required: false, default: 8 },
|
|
74
76
|
colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7"] },
|
|
75
77
|
backgroundColor: { type: String, required: false, default: "#ffffff" },
|
|
@@ -84,11 +86,15 @@ const svgRef = ref(null);
|
|
|
84
86
|
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
85
87
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
86
88
|
const dataRef = computed(() => props.dataProvider);
|
|
87
|
-
const { items } = useChartData(dataRef);
|
|
89
|
+
const { items, loading: dataLoading } = useChartData(dataRef);
|
|
88
90
|
const { progress, animate } = useChartAnimation();
|
|
89
91
|
const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
90
92
|
const isLoading = computed(() => {
|
|
91
|
-
|
|
93
|
+
const dp = props.dataProvider;
|
|
94
|
+
if (!dp) return dataLoading.value;
|
|
95
|
+
const rawLoading = dp.loading !== void 0 ? isRef(dp.loading) ? dp.loading.value : dp.loading?.value ?? dp.loading : false;
|
|
96
|
+
const rawInitial = dp.initialLoad !== void 0 ? isRef(dp.initialLoad) ? dp.initialLoad.value : dp.initialLoad?.value ?? dp.initialLoad : false;
|
|
97
|
+
return Boolean(dataLoading.value || rawLoading || rawInitial);
|
|
92
98
|
});
|
|
93
99
|
const tooltipStyle = computed(() => ({
|
|
94
100
|
left: `${tooltipState.value.x}px`,
|
|
@@ -97,7 +103,15 @@ const tooltipStyle = computed(() => ({
|
|
|
97
103
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
98
104
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
99
105
|
const plotLeftRef = computed(() => 55);
|
|
100
|
-
const
|
|
106
|
+
const zoomableRef = computed(() => props.zoomable);
|
|
107
|
+
const wheelNavRef = computed(() => props.wheelNavigation);
|
|
108
|
+
const viewport = useChartViewport({
|
|
109
|
+
maxZoom: maxZoomRef,
|
|
110
|
+
plotWidth: plotWidthRef,
|
|
111
|
+
plotLeft: plotLeftRef,
|
|
112
|
+
zoomable: zoomableRef,
|
|
113
|
+
wheelNavigation: wheelNavRef
|
|
114
|
+
});
|
|
101
115
|
const {
|
|
102
116
|
scale,
|
|
103
117
|
panOffset,
|
|
@@ -119,6 +133,11 @@ const plotWidth = computed(() => Math.max(0, width.value - PAD.left - PAD.right)
|
|
|
119
133
|
const plotHeight = computed(() => Math.max(0, height.value - PAD.top - PAD.bottom));
|
|
120
134
|
const categories = computed(() => [...new Set(items.value.map((r) => String(r[props.categoryField])))]);
|
|
121
135
|
const groups = computed(() => [...new Set(items.value.map((r) => String(r[props.groupField] ?? "default")))]);
|
|
136
|
+
const minValue = computed(() => {
|
|
137
|
+
const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
|
|
138
|
+
const min = Math.min(0, ...vals);
|
|
139
|
+
return isFinite(min) ? min : 0;
|
|
140
|
+
});
|
|
122
141
|
const maxValue = computed(() => {
|
|
123
142
|
if (props.mode === "stacked") {
|
|
124
143
|
const sums = categories.value.map(
|
|
@@ -127,7 +146,11 @@ const maxValue = computed(() => {
|
|
|
127
146
|
return safeMax(sums, 10);
|
|
128
147
|
}
|
|
129
148
|
const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
|
|
130
|
-
return safeMax(vals, 10);
|
|
149
|
+
return Math.max(0, safeMax(vals, 10));
|
|
150
|
+
});
|
|
151
|
+
const valueRange = computed(() => {
|
|
152
|
+
const span = maxValue.value - minValue.value;
|
|
153
|
+
return span > 0 ? span : 10;
|
|
131
154
|
});
|
|
132
155
|
const visibleCatCount = computed(() => Math.max(1, Math.ceil(categories.value.length / scale.value)));
|
|
133
156
|
const panCatOffset = computed(() => {
|
|
@@ -158,6 +181,7 @@ const draw = () => {
|
|
|
158
181
|
if (props.mode === "clustered" && gCount > 1) {
|
|
159
182
|
barHeight = catHeight * 0.8 / gCount;
|
|
160
183
|
}
|
|
184
|
+
const xZero = plotLeft + (0 - minValue.value) / valueRange.value * plotWidth.value;
|
|
161
185
|
c.save();
|
|
162
186
|
c.strokeStyle = "rgba(100,100,120,0.1)";
|
|
163
187
|
c.lineWidth = 1;
|
|
@@ -165,14 +189,21 @@ const draw = () => {
|
|
|
165
189
|
c.font = "11px system-ui, sans-serif";
|
|
166
190
|
c.textAlign = "center";
|
|
167
191
|
for (let i = 0; i <= 5; i++) {
|
|
168
|
-
const val =
|
|
169
|
-
const x = plotLeft + val /
|
|
192
|
+
const val = minValue.value + valueRange.value / 5 * i;
|
|
193
|
+
const x = plotLeft + (val - minValue.value) / valueRange.value * plotWidth.value;
|
|
170
194
|
c.beginPath();
|
|
171
195
|
c.moveTo(x, plotTop);
|
|
172
196
|
c.lineTo(x, plotTop + plotHeight.value);
|
|
173
197
|
c.stroke();
|
|
174
198
|
c.fillText(val.toFixed(0), x, plotTop + plotHeight.value + 16);
|
|
175
199
|
}
|
|
200
|
+
if (minValue.value < 0) {
|
|
201
|
+
c.strokeStyle = "rgba(150,150,170,0.4)";
|
|
202
|
+
c.beginPath();
|
|
203
|
+
c.moveTo(xZero, plotTop);
|
|
204
|
+
c.lineTo(xZero, plotTop + plotHeight.value);
|
|
205
|
+
c.stroke();
|
|
206
|
+
}
|
|
176
207
|
c.strokeStyle = "rgba(100,100,120,0.05)";
|
|
177
208
|
for (let i = 0; i <= catCount; i++) {
|
|
178
209
|
const y = plotTop + i * catHeight;
|
|
@@ -199,13 +230,21 @@ const draw = () => {
|
|
|
199
230
|
const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
|
|
200
231
|
const catY = plotTop + catIdx * catHeight;
|
|
201
232
|
if (props.mode === "stacked") {
|
|
202
|
-
let
|
|
233
|
+
let xOffsetPos = 0;
|
|
234
|
+
let xOffsetNeg = 0;
|
|
203
235
|
groups.value.forEach((group, gIdx) => {
|
|
204
236
|
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
205
237
|
if (!item) return;
|
|
206
|
-
const
|
|
207
|
-
const bw =
|
|
208
|
-
|
|
238
|
+
const rawVal = cleanNumber(item[props.valueField]);
|
|
239
|
+
const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
|
|
240
|
+
let bx = xZero;
|
|
241
|
+
if (rawVal >= 0) {
|
|
242
|
+
bx = xZero + xOffsetPos * p / valueRange.value * plotWidth.value;
|
|
243
|
+
xOffsetPos += rawVal;
|
|
244
|
+
} else {
|
|
245
|
+
xOffsetNeg += Math.abs(rawVal);
|
|
246
|
+
bx = xZero - xOffsetNeg * p / valueRange.value * plotWidth.value;
|
|
247
|
+
}
|
|
209
248
|
const by = catY + (catHeight - barHeight) / 2;
|
|
210
249
|
const isHovered = hoveredIndex.value === bars.length;
|
|
211
250
|
const color = props.colors[gIdx % props.colors.length];
|
|
@@ -215,15 +254,14 @@ const draw = () => {
|
|
|
215
254
|
c.lineWidth = 1;
|
|
216
255
|
c.strokeRect(bx, by, bw, barHeight);
|
|
217
256
|
bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat} (${group}): ${item[props.valueField]}` });
|
|
218
|
-
xOffset += cleanNumber(item[props.valueField]);
|
|
219
257
|
});
|
|
220
258
|
} else if (props.mode === "simple") {
|
|
221
259
|
const item = catItems[0];
|
|
222
260
|
if (item) {
|
|
223
261
|
const isHovered = hoveredIndex.value === bars.length;
|
|
224
|
-
const
|
|
225
|
-
const bw =
|
|
226
|
-
const bx =
|
|
262
|
+
const rawVal = cleanNumber(item[props.valueField]);
|
|
263
|
+
const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
|
|
264
|
+
const bx = rawVal >= 0 ? xZero : xZero - bw;
|
|
227
265
|
const by = catY + (catHeight - barHeight) / 2;
|
|
228
266
|
const color = props.colors[0] || "#6366f1";
|
|
229
267
|
c.fillStyle = isHovered ? color : color + "dd";
|
|
@@ -235,13 +273,13 @@ const draw = () => {
|
|
|
235
273
|
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
236
274
|
if (!item) return;
|
|
237
275
|
const isHovered = hoveredIndex.value === bars.length;
|
|
238
|
-
const
|
|
239
|
-
const bw =
|
|
276
|
+
const rawVal = cleanNumber(item[props.valueField]);
|
|
277
|
+
const bw = Math.abs(rawVal) * p / valueRange.value * plotWidth.value;
|
|
278
|
+
const bx = rawVal >= 0 ? xZero : xZero - bw;
|
|
240
279
|
const gCountTotal = groups.value.length;
|
|
241
280
|
const totalBarsH = gCountTotal * barHeight + (gCountTotal - 1) * 2;
|
|
242
281
|
const startY = catY + (catHeight - totalBarsH) / 2;
|
|
243
282
|
const by = startY + gIdx * (barHeight + 2);
|
|
244
|
-
const bx = plotLeft;
|
|
245
283
|
const color = props.colors[gIdx % props.colors.length];
|
|
246
284
|
c.fillStyle = isHovered ? color : color + "dd";
|
|
247
285
|
c.fillRect(bx, by, bw, barHeight);
|
|
@@ -266,6 +304,7 @@ const draw = () => {
|
|
|
266
304
|
if (props.mode === "clustered" && gCount > 1) {
|
|
267
305
|
barWidth = catWidth * 0.8 / gCount;
|
|
268
306
|
}
|
|
307
|
+
const yZero = plotTop + plotHeight.value - (0 - minValue.value) / valueRange.value * plotHeight.value;
|
|
269
308
|
c.save();
|
|
270
309
|
c.strokeStyle = "rgba(100,100,120,0.1)";
|
|
271
310
|
c.lineWidth = 1;
|
|
@@ -273,14 +312,21 @@ const draw = () => {
|
|
|
273
312
|
c.font = "11px system-ui, sans-serif";
|
|
274
313
|
c.textAlign = "right";
|
|
275
314
|
for (let i = 0; i <= 5; i++) {
|
|
276
|
-
const val =
|
|
277
|
-
const y = plotTop + plotHeight.value - val /
|
|
315
|
+
const val = minValue.value + valueRange.value / 5 * i;
|
|
316
|
+
const y = plotTop + plotHeight.value - (val - minValue.value) / valueRange.value * plotHeight.value;
|
|
278
317
|
c.beginPath();
|
|
279
318
|
c.moveTo(plotLeft, y);
|
|
280
319
|
c.lineTo(plotLeft + plotWidth.value, y);
|
|
281
320
|
c.stroke();
|
|
282
321
|
c.fillText(val.toFixed(0), plotLeft - 8, y + 4);
|
|
283
322
|
}
|
|
323
|
+
if (minValue.value < 0) {
|
|
324
|
+
c.strokeStyle = "rgba(150,150,170,0.4)";
|
|
325
|
+
c.beginPath();
|
|
326
|
+
c.moveTo(plotLeft, yZero);
|
|
327
|
+
c.lineTo(plotLeft + plotWidth.value, yZero);
|
|
328
|
+
c.stroke();
|
|
329
|
+
}
|
|
284
330
|
c.strokeStyle = "rgba(100,100,120,0.05)";
|
|
285
331
|
for (let i = 0; i <= catCount; i++) {
|
|
286
332
|
const x = plotLeft + i * catWidth;
|
|
@@ -307,14 +353,22 @@ const draw = () => {
|
|
|
307
353
|
const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
|
|
308
354
|
const catX = plotLeft + catIdx * catWidth;
|
|
309
355
|
if (props.mode === "stacked") {
|
|
310
|
-
let
|
|
356
|
+
let yOffsetPos = 0;
|
|
357
|
+
let yOffsetNeg = 0;
|
|
311
358
|
groups.value.forEach((group, gIdx) => {
|
|
312
359
|
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
313
360
|
if (!item) return;
|
|
314
|
-
const
|
|
315
|
-
const bh =
|
|
361
|
+
const rawVal = cleanNumber(item[props.valueField]);
|
|
362
|
+
const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
|
|
316
363
|
const bx = catX + (catWidth - barWidth) / 2;
|
|
317
|
-
|
|
364
|
+
let by = yZero;
|
|
365
|
+
if (rawVal >= 0) {
|
|
366
|
+
yOffsetPos += rawVal;
|
|
367
|
+
by = yZero - yOffsetPos * p / valueRange.value * plotHeight.value;
|
|
368
|
+
} else {
|
|
369
|
+
by = yZero + yOffsetNeg * p / valueRange.value * plotHeight.value;
|
|
370
|
+
yOffsetNeg += Math.abs(rawVal);
|
|
371
|
+
}
|
|
318
372
|
const isHovered = hoveredIndex.value === bars.length;
|
|
319
373
|
const color = props.colors[gIdx % props.colors.length];
|
|
320
374
|
c.fillStyle = isHovered ? color : color + "dd";
|
|
@@ -323,16 +377,15 @@ const draw = () => {
|
|
|
323
377
|
c.lineWidth = 1;
|
|
324
378
|
c.strokeRect(bx, by, barWidth, bh);
|
|
325
379
|
bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
|
|
326
|
-
yOffset += cleanNumber(item[props.valueField]);
|
|
327
380
|
});
|
|
328
381
|
} else if (props.mode === "simple") {
|
|
329
382
|
const item = catItems[0];
|
|
330
383
|
if (item) {
|
|
331
384
|
const isHovered = hoveredIndex.value === bars.length;
|
|
332
|
-
const
|
|
333
|
-
const bh =
|
|
385
|
+
const rawVal = cleanNumber(item[props.valueField]);
|
|
386
|
+
const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
|
|
334
387
|
const bx = catX + (catWidth - barWidth) / 2;
|
|
335
|
-
const by =
|
|
388
|
+
const by = rawVal >= 0 ? yZero - bh : yZero;
|
|
336
389
|
const color = props.colors[0] || "#6366f1";
|
|
337
390
|
c.fillStyle = isHovered ? color : color + "dd";
|
|
338
391
|
c.fillRect(bx, by, barWidth, bh);
|
|
@@ -343,13 +396,13 @@ const draw = () => {
|
|
|
343
396
|
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
344
397
|
if (!item) return;
|
|
345
398
|
const isHovered = hoveredIndex.value === bars.length;
|
|
346
|
-
const
|
|
347
|
-
const bh =
|
|
399
|
+
const rawVal = cleanNumber(item[props.valueField]);
|
|
400
|
+
const bh = Math.abs(rawVal) * p / valueRange.value * plotHeight.value;
|
|
348
401
|
const gCountTotal = groups.value.length;
|
|
349
402
|
const totalBarsW = gCountTotal * barWidth + (gCountTotal - 1) * 2;
|
|
350
403
|
const startX = catX + (catWidth - totalBarsW) / 2;
|
|
351
404
|
const bx = startX + gIdx * (barWidth + 2);
|
|
352
|
-
const by =
|
|
405
|
+
const by = rawVal >= 0 ? yZero - bh : yZero;
|
|
353
406
|
const color = props.colors[gIdx % props.colors.length];
|
|
354
407
|
c.fillStyle = isHovered ? color : color + "dd";
|
|
355
408
|
c.fillRect(bx, by, barWidth, bh);
|
|
@@ -412,7 +465,7 @@ watch(items, () => {
|
|
|
412
465
|
else draw();
|
|
413
466
|
});
|
|
414
467
|
watch(scale, (v) => emit("zoom-change", v));
|
|
415
|
-
watch(panOffset, (
|
|
468
|
+
watch(() => [panOffset.value.x, panOffset.value.y], ([x, y]) => emit("pan-change", { x, y }));
|
|
416
469
|
onMounted(async () => {
|
|
417
470
|
await nextTick();
|
|
418
471
|
if (containerRef.value && canvasRef.value) {
|
|
@@ -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;
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
</template>
|
|
43
43
|
|
|
44
44
|
<script setup>
|
|
45
|
-
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
45
|
+
import { ref, watch, computed, onMounted, nextTick, isRef } from "vue";
|
|
46
46
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
47
47
|
import { useChartData } from "../../composables/useChartData";
|
|
48
48
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
@@ -68,10 +68,14 @@ const svgRef = ref(null);
|
|
|
68
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
|
});
|
|
76
80
|
const tooltipStyle = computed(() => ({
|
|
77
81
|
left: `${tooltipState.value.x}px`,
|
|
@@ -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;
|