@katlux/block-charts 0.1.1-alpha.87 → 0.1.1-alpha.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/KAreaChart/KAreaChart.vue +233 -60
- package/dist/runtime/components/KBarChart/KBarChart.vue +237 -114
- package/dist/runtime/components/KHeatMap/KHeatMap.vue +28 -15
- package/dist/runtime/components/KLineChart/KLineChart.vue +149 -41
- package/dist/runtime/components/KPieChart/KPieChart.vue +25 -26
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +84 -32
- package/dist/runtime/composables/chartUtils.d.ts +8 -0
- package/dist/runtime/composables/chartUtils.js +58 -0
- package/dist/runtime/composables/useChartAxes.d.ts +3 -1
- package/dist/runtime/composables/useChartAxes.js +26 -9
- package/dist/runtime/composables/useChartData.d.ts +4 -3
- package/dist/runtime/composables/useChartData.js +19 -18
- package/dist/runtime/composables/useChartExport.d.ts +1 -1
- package/dist/runtime/composables/useChartExport.js +18 -2
- package/dist/runtime/composables/useChartHitTest.d.ts +1 -1
- package/dist/runtime/composables/useChartHitTest.js +4 -3
- package/package.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
.k-chart-wrapper(
|
|
3
3
|
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
4
|
-
:style="{ cursor: isDragging ? 'grabbing' : canPan ? 'grab' : 'default' }"
|
|
5
4
|
@wheel.prevent="onWheel"
|
|
6
5
|
@mousedown="onMouseDown"
|
|
7
6
|
@mousemove="onMouseMove"
|
|
@@ -25,16 +24,14 @@
|
|
|
25
24
|
:height="axes.plotHeight.value"
|
|
26
25
|
)
|
|
27
26
|
g.k-chart-hits
|
|
28
|
-
// No circles here! Distance-based hit testing now.
|
|
29
27
|
|
|
30
28
|
// Dynamic Interaction Layer
|
|
31
|
-
g.k-chart-interaction(v-if="hoveredIndex !== -1 && !isLoading" clip-path="url(#plot-clip)")
|
|
29
|
+
g.k-chart-interaction(v-if="hoveredIndex !== -1 && !isLoading && hoveredPoint" clip-path="url(#plot-clip)")
|
|
32
30
|
circle.k-chart-hit-proxy(
|
|
33
31
|
:cx="hoveredPoint.x"
|
|
34
32
|
:cy="hoveredPoint.y"
|
|
35
33
|
:r="20"
|
|
36
34
|
fill="transparent"
|
|
37
|
-
style="cursor: pointer"
|
|
38
35
|
@click="emit('click-point', hoveredPoint.item, hoveredPoint.index)"
|
|
39
36
|
)
|
|
40
37
|
circle(
|
|
@@ -46,7 +43,7 @@
|
|
|
46
43
|
stroke-width="2"
|
|
47
44
|
pointer-events="none"
|
|
48
45
|
)
|
|
49
|
-
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="
|
|
46
|
+
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
|
|
50
47
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
51
48
|
span {{ tooltipState.content }}
|
|
52
49
|
</template>
|
|
@@ -56,11 +53,12 @@ import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
|
56
53
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
57
54
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
58
55
|
import { useChartData } from "../../composables/useChartData";
|
|
59
|
-
import { useChartAxes } from "../../composables/useChartAxes";
|
|
56
|
+
import { useChartAxes, parseValue } from "../../composables/useChartAxes";
|
|
60
57
|
import { useChartViewport } from "../../composables/useChartViewport";
|
|
61
58
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
62
59
|
import { useChartExport } from "../../composables/useChartExport";
|
|
63
60
|
import { useChartHitTest } from "../../composables/useChartHitTest";
|
|
61
|
+
import { cleanNumber, safeMax } from "../../composables/chartUtils";
|
|
64
62
|
const props = defineProps({
|
|
65
63
|
dataProvider: { type: null, required: false },
|
|
66
64
|
xField: { type: String, required: false, default: "x" },
|
|
@@ -81,7 +79,7 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
|
|
|
81
79
|
const containerRef = ref(null);
|
|
82
80
|
const canvasRef = ref(null);
|
|
83
81
|
const svgRef = ref(null);
|
|
84
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
82
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
85
83
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
86
84
|
const dataRef = computed(() => props.dataProvider);
|
|
87
85
|
const { items } = useChartData(dataRef);
|
|
@@ -90,6 +88,10 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
|
90
88
|
const isLoading = computed(() => {
|
|
91
89
|
return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
|
|
92
90
|
});
|
|
91
|
+
const tooltipStyle = computed(() => ({
|
|
92
|
+
left: `${tooltipState.value.x}px`,
|
|
93
|
+
top: `${tooltipState.value.y}px`
|
|
94
|
+
}));
|
|
93
95
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
94
96
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
95
97
|
const plotLeftRef = computed(() => 55);
|
|
@@ -111,8 +113,46 @@ const {
|
|
|
111
113
|
} = viewport;
|
|
112
114
|
const xFieldRef = computed(() => props.xField);
|
|
113
115
|
const yFieldRef = computed(() => props.yField);
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
+
const xAxisType = computed(() => {
|
|
117
|
+
if (!items.value.length) return "numeric";
|
|
118
|
+
const sample = items.value[0][props.xField];
|
|
119
|
+
if (typeof sample === "number") return "numeric";
|
|
120
|
+
if (typeof sample === "string") {
|
|
121
|
+
const ts = Date.parse(sample);
|
|
122
|
+
if (!isNaN(ts) && isNaN(Number(sample))) return "datetime";
|
|
123
|
+
return "category";
|
|
124
|
+
}
|
|
125
|
+
return "numeric";
|
|
126
|
+
});
|
|
127
|
+
const categoryList = computed(() => {
|
|
128
|
+
if (xAxisType.value !== "category") return [];
|
|
129
|
+
return [...new Set(items.value.map((r) => String(r[props.xField])))];
|
|
130
|
+
});
|
|
131
|
+
const categoryIndexMap = computed(() => {
|
|
132
|
+
const map = {};
|
|
133
|
+
categoryList.value.forEach((cat, i) => {
|
|
134
|
+
map[cat] = i;
|
|
135
|
+
});
|
|
136
|
+
return map;
|
|
137
|
+
});
|
|
138
|
+
const resolveX = (item, index = 0) => {
|
|
139
|
+
if (!item) return index;
|
|
140
|
+
const raw = item[props.xField];
|
|
141
|
+
if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
|
|
142
|
+
return parseValue(raw, index);
|
|
143
|
+
};
|
|
144
|
+
const axes = useChartAxes({
|
|
145
|
+
items,
|
|
146
|
+
xField: xFieldRef,
|
|
147
|
+
yField: yFieldRef,
|
|
148
|
+
width,
|
|
149
|
+
height,
|
|
150
|
+
scale,
|
|
151
|
+
panOffset,
|
|
152
|
+
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
153
|
+
xValueGetter: (r, i) => resolveX(r, i)
|
|
154
|
+
});
|
|
155
|
+
const maxSize = computed(() => safeMax(items.value.map((r) => cleanNumber(r[props.sizeField] ?? 1, 1)), 1));
|
|
116
156
|
const seriesMap = computed(() => {
|
|
117
157
|
const map = /* @__PURE__ */ new Map();
|
|
118
158
|
for (const item of items.value) {
|
|
@@ -125,15 +165,16 @@ const seriesMap = computed(() => {
|
|
|
125
165
|
const hoveredPoint = computed(() => {
|
|
126
166
|
if (hoveredIndex.value === -1) return null;
|
|
127
167
|
const item = items.value[hoveredIndex.value];
|
|
128
|
-
|
|
129
|
-
const
|
|
168
|
+
if (!item) return null;
|
|
169
|
+
const sizeRaw = cleanNumber(item[props.sizeField] ?? 1, 1);
|
|
170
|
+
const r = 4 + sizeRaw / (maxSize.value || 1) * 16;
|
|
130
171
|
const seriesKey = String(item[props.seriesField] ?? "default");
|
|
131
172
|
const seriesKeys = Array.from(seriesMap.value.keys());
|
|
132
173
|
const seriesIdx = seriesKeys.indexOf(seriesKey);
|
|
133
174
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
134
175
|
return {
|
|
135
|
-
x: axes.toX(
|
|
136
|
-
y: axes.toY(
|
|
176
|
+
x: axes.toX(resolveX(item, hoveredIndex.value)),
|
|
177
|
+
y: axes.toY(parseValue(item[props.yField])),
|
|
137
178
|
r,
|
|
138
179
|
color,
|
|
139
180
|
item,
|
|
@@ -144,7 +185,7 @@ const draw = () => {
|
|
|
144
185
|
if (!ctx.value) return;
|
|
145
186
|
clear(props.backgroundColor);
|
|
146
187
|
const c = ctx.value;
|
|
147
|
-
const p = easeOut(progress.value);
|
|
188
|
+
const p = props.animated ? easeOut(progress.value) : 1;
|
|
148
189
|
c.save();
|
|
149
190
|
c.strokeStyle = "rgba(100,100,120,0.1)";
|
|
150
191
|
c.lineWidth = 1;
|
|
@@ -162,15 +203,8 @@ const draw = () => {
|
|
|
162
203
|
c.lineTo(x, axes.plotTop + axes.plotHeight.value);
|
|
163
204
|
c.stroke();
|
|
164
205
|
}
|
|
165
|
-
c.
|
|
166
|
-
c.
|
|
167
|
-
c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
168
|
-
c.lineTo(axes.plotLeft + axes.plotWidth.value, axes.plotTop + axes.plotHeight.value);
|
|
169
|
-
c.stroke();
|
|
170
|
-
c.beginPath();
|
|
171
|
-
c.moveTo(axes.plotLeft, axes.plotTop);
|
|
172
|
-
c.lineTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
173
|
-
c.stroke();
|
|
206
|
+
c.restore();
|
|
207
|
+
c.save();
|
|
174
208
|
c.fillStyle = "rgba(150,150,170,0.8)";
|
|
175
209
|
c.font = "11px system-ui, sans-serif";
|
|
176
210
|
c.textAlign = "right";
|
|
@@ -183,8 +217,18 @@ const draw = () => {
|
|
|
183
217
|
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
|
|
184
218
|
c.fillText(val.toFixed(1), axes.toX(val), axes.plotTop + axes.plotHeight.value + 16);
|
|
185
219
|
}
|
|
220
|
+
c.strokeStyle = "rgba(150,150,170,0.5)";
|
|
221
|
+
c.beginPath();
|
|
222
|
+
c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
223
|
+
c.lineTo(axes.plotLeft + axes.plotWidth.value, axes.plotTop + axes.plotHeight.value);
|
|
224
|
+
c.stroke();
|
|
225
|
+
c.beginPath();
|
|
226
|
+
c.moveTo(axes.plotLeft, axes.plotTop);
|
|
227
|
+
c.lineTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
228
|
+
c.stroke();
|
|
186
229
|
c.fillStyle = "rgba(120,120,140,0.8)";
|
|
187
230
|
if (props.xAxisTitle) {
|
|
231
|
+
c.textAlign = "center";
|
|
188
232
|
c.fillText(props.xAxisTitle, axes.plotLeft + axes.plotWidth.value / 2, axes.plotTop + axes.plotHeight.value + 34);
|
|
189
233
|
}
|
|
190
234
|
if (props.yAxisTitle) {
|
|
@@ -203,11 +247,11 @@ const draw = () => {
|
|
|
203
247
|
let seriesIdx = 0;
|
|
204
248
|
for (const [, seriesItems] of seriesMap.value) {
|
|
205
249
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
206
|
-
seriesItems.forEach((item) => {
|
|
207
|
-
const x = axes.toX(
|
|
208
|
-
const y = axes.toY(
|
|
209
|
-
const sizeRaw =
|
|
210
|
-
const r = 4 + sizeRaw / maxSize.value * 16;
|
|
250
|
+
seriesItems.forEach((item, idx) => {
|
|
251
|
+
const x = axes.toX(resolveX(item, idx));
|
|
252
|
+
const y = axes.toY(parseValue(item[props.yField]));
|
|
253
|
+
const sizeRaw = cleanNumber(item[props.sizeField] ?? 1, 1);
|
|
254
|
+
const r = 4 + sizeRaw / (maxSize.value || 1) * 16;
|
|
211
255
|
const isHovered = items.value[hoveredIndex.value] === item;
|
|
212
256
|
const alpha = p;
|
|
213
257
|
c.save();
|
|
@@ -251,16 +295,17 @@ const drawLegend = (c) => {
|
|
|
251
295
|
}
|
|
252
296
|
c.restore();
|
|
253
297
|
};
|
|
254
|
-
watch(
|
|
298
|
+
watch(progress, draw);
|
|
299
|
+
watch([scale, panOffset, hoveredIndex, resizeTick, width, height], draw);
|
|
255
300
|
watch(items, () => {
|
|
256
301
|
if (props.animated) animate();
|
|
257
|
-
|
|
302
|
+
else draw();
|
|
303
|
+
});
|
|
258
304
|
watch(scale, (v) => emit("zoom-change", v));
|
|
259
305
|
watch(panOffset, (v) => emit("pan-change", v), { deep: true });
|
|
260
306
|
onMounted(async () => {
|
|
261
307
|
await nextTick();
|
|
262
308
|
if (containerRef.value && canvasRef.value) {
|
|
263
|
-
containerRef.value.style.position = "relative";
|
|
264
309
|
setupCanvas(canvasRef.value);
|
|
265
310
|
if (props.animated) animate();
|
|
266
311
|
else draw();
|
|
@@ -297,7 +342,7 @@ defineExpose({
|
|
|
297
342
|
zoomOut: viewport.zoomOut,
|
|
298
343
|
resetZoom: viewport.resetZoom,
|
|
299
344
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "scatter-chart.png"),
|
|
300
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "scatter-chart.svg")
|
|
345
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "scatter-chart.svg", canvasRef.value)
|
|
301
346
|
});
|
|
302
347
|
</script>
|
|
303
348
|
|
|
@@ -305,6 +350,13 @@ defineExpose({
|
|
|
305
350
|
.k-chart-wrapper {
|
|
306
351
|
position: relative;
|
|
307
352
|
user-select: none;
|
|
353
|
+
cursor: default;
|
|
354
|
+
}
|
|
355
|
+
.k-chart-wrapper.k-chart--pannable {
|
|
356
|
+
cursor: grab;
|
|
357
|
+
}
|
|
358
|
+
.k-chart-wrapper.k-chart--grabbing {
|
|
359
|
+
cursor: grabbing;
|
|
308
360
|
}
|
|
309
361
|
.k-chart-wrapper .k-chart-controls {
|
|
310
362
|
position: absolute;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust numerical utilities for Katlux chart engine.
|
|
3
|
+
* Prevents call stack overflow on large datasets (100k+ points)
|
|
4
|
+
* and safely normalizes unparsed or localized number strings.
|
|
5
|
+
*/
|
|
6
|
+
export declare function cleanNumber(val: any, fallback?: number): number;
|
|
7
|
+
export declare function safeMax(arr: number[], fallback?: number): number;
|
|
8
|
+
export declare function safeMin(arr: number[], fallback?: number): number;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function cleanNumber(val, fallback = 0) {
|
|
2
|
+
if (val === null || val === void 0) return fallback;
|
|
3
|
+
if (typeof val === "number") {
|
|
4
|
+
return Number.isFinite(val) ? val : fallback;
|
|
5
|
+
}
|
|
6
|
+
if (typeof val === "string") {
|
|
7
|
+
let s = val.trim();
|
|
8
|
+
if (!s) return fallback;
|
|
9
|
+
s = s.replace(/\s+/g, "");
|
|
10
|
+
if (s.includes(".") && s.includes(",")) {
|
|
11
|
+
if (s.lastIndexOf(",") > s.lastIndexOf(".")) {
|
|
12
|
+
s = s.replace(/\./g, "").replace(",", ".");
|
|
13
|
+
} else {
|
|
14
|
+
s = s.replace(/,/g, "");
|
|
15
|
+
}
|
|
16
|
+
} else if (s.includes(",")) {
|
|
17
|
+
const parts = s.split(",");
|
|
18
|
+
if (parts.length > 2 || parts.length === 2 && parts[1].length === 3) {
|
|
19
|
+
s = s.replace(/,/g, "");
|
|
20
|
+
} else {
|
|
21
|
+
s = s.replace(",", ".");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const num = Number(s);
|
|
25
|
+
return Number.isFinite(num) ? num : fallback;
|
|
26
|
+
}
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
export function safeMax(arr, fallback = 0) {
|
|
30
|
+
if (!arr || arr.length === 0) return fallback;
|
|
31
|
+
let max = -Infinity;
|
|
32
|
+
let found = false;
|
|
33
|
+
for (let i = 0; i < arr.length; i++) {
|
|
34
|
+
const v = arr[i];
|
|
35
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
36
|
+
if (!found || v > max) {
|
|
37
|
+
max = v;
|
|
38
|
+
found = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return found ? max : fallback;
|
|
43
|
+
}
|
|
44
|
+
export function safeMin(arr, fallback = 0) {
|
|
45
|
+
if (!arr || arr.length === 0) return fallback;
|
|
46
|
+
let min = Infinity;
|
|
47
|
+
let found = false;
|
|
48
|
+
for (let i = 0; i < arr.length; i++) {
|
|
49
|
+
const v = arr[i];
|
|
50
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
51
|
+
if (!found || v < min) {
|
|
52
|
+
min = v;
|
|
53
|
+
found = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return found ? min : fallback;
|
|
58
|
+
}
|
|
@@ -18,6 +18,7 @@ export interface IChartAxesOptions {
|
|
|
18
18
|
left: number;
|
|
19
19
|
};
|
|
20
20
|
scaleMode?: TScaleMode;
|
|
21
|
+
xValueGetter?: (item: any, index: number) => number;
|
|
21
22
|
}
|
|
22
23
|
export interface IChartAxesContext {
|
|
23
24
|
xMin: ComputedRef<number>;
|
|
@@ -34,7 +35,8 @@ export interface IChartAxesContext {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* Robustly parses a value into a number.
|
|
37
|
-
* Handles
|
|
38
|
+
* Handles numbers, strings with thousands/decimal formatting, ISO dates,
|
|
39
|
+
* and falls back to array index for unparseable category strings.
|
|
38
40
|
*/
|
|
39
41
|
export declare function parseValue(val: any, fallbackIndex?: number): number;
|
|
40
42
|
export declare function useChartAxes(opts: IChartAxesOptions): IChartAxesContext;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
+
import { cleanNumber, safeMax, safeMin } from "./chartUtils.js";
|
|
2
3
|
export function parseValue(val, fallbackIndex) {
|
|
3
4
|
if (val === null || val === void 0) return fallbackIndex ?? 0;
|
|
4
|
-
if (typeof val === "number") return
|
|
5
|
-
const n = Number(val);
|
|
6
|
-
if (!isNaN(n)) return n;
|
|
5
|
+
if (typeof val === "number") return Number.isFinite(val) ? val : fallbackIndex ?? 0;
|
|
7
6
|
if (typeof val === "string") {
|
|
7
|
+
const cleaned = cleanNumber(val, NaN);
|
|
8
|
+
if (Number.isFinite(cleaned)) return cleaned;
|
|
8
9
|
const d = Date.parse(val);
|
|
9
10
|
if (!isNaN(d)) return d;
|
|
10
11
|
}
|
|
@@ -16,21 +17,25 @@ export function useChartAxes(opts) {
|
|
|
16
17
|
const plotTop = pad.top;
|
|
17
18
|
const plotWidth = computed(() => Math.max(0, opts.width.value - pad.left - pad.right));
|
|
18
19
|
const plotHeight = computed(() => Math.max(0, opts.height.value - pad.top - pad.bottom));
|
|
20
|
+
const getXVal = (r, i) => {
|
|
21
|
+
if (opts.xValueGetter) return opts.xValueGetter(r, i);
|
|
22
|
+
return parseValue(r[opts.xField.value], i);
|
|
23
|
+
};
|
|
19
24
|
const rawXMin = computed(() => {
|
|
20
|
-
const vals = opts.items.value.map((r, i) =>
|
|
21
|
-
return
|
|
25
|
+
const vals = opts.items.value.map((r, i) => getXVal(r, i));
|
|
26
|
+
return safeMin(vals, 0);
|
|
22
27
|
});
|
|
23
28
|
const rawXMax = computed(() => {
|
|
24
|
-
const vals = opts.items.value.map((r, i) =>
|
|
25
|
-
return
|
|
29
|
+
const vals = opts.items.value.map((r, i) => getXVal(r, i));
|
|
30
|
+
return safeMax(vals, 1);
|
|
26
31
|
});
|
|
27
32
|
const yMin = computed(() => {
|
|
28
33
|
const vals = opts.items.value.map((r, i) => parseValue(r[opts.yField.value], i));
|
|
29
|
-
return
|
|
34
|
+
return safeMin(vals, 0);
|
|
30
35
|
});
|
|
31
36
|
const yMax = computed(() => {
|
|
32
37
|
const vals = opts.items.value.map((r, i) => parseValue(r[opts.yField.value], i));
|
|
33
|
-
return
|
|
38
|
+
return safeMax(vals, 1);
|
|
34
39
|
});
|
|
35
40
|
const xRange = computed(() => rawXMax.value - rawXMin.value);
|
|
36
41
|
const visibleXRange = computed(() => xRange.value / opts.scale.value);
|
|
@@ -47,6 +52,18 @@ export function useChartAxes(opts) {
|
|
|
47
52
|
return plotLeft + (value - xMin.value) / range * plotWidth.value;
|
|
48
53
|
};
|
|
49
54
|
const toY = (value) => {
|
|
55
|
+
if (opts.scaleMode === "logarithmic") {
|
|
56
|
+
const minVal = Math.max(yMin.value, 1e-6);
|
|
57
|
+
const maxVal = Math.max(yMax.value, minVal + 1e-6);
|
|
58
|
+
const logMin = Math.log10(minVal);
|
|
59
|
+
const logMax = Math.log10(maxVal);
|
|
60
|
+
const logRange = logMax - logMin;
|
|
61
|
+
if (logRange === 0) return plotTop + plotHeight.value;
|
|
62
|
+
const safeVal = Math.max(value, minVal);
|
|
63
|
+
const logVal = Math.log10(safeVal);
|
|
64
|
+
const ratio = (logVal - logMin) / logRange;
|
|
65
|
+
return plotTop + plotHeight.value - ratio * plotHeight.value;
|
|
66
|
+
}
|
|
50
67
|
const range = yMax.value - yMin.value;
|
|
51
68
|
if (range === 0) return plotTop + plotHeight.value;
|
|
52
69
|
return plotTop + plotHeight.value - (value - yMin.value) / range * plotHeight.value;
|
|
@@ -7,12 +7,13 @@ export interface IChartDataContext<T = any> {
|
|
|
7
7
|
reload: () => Promise<void>;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Extracts item rows
|
|
10
|
+
* Extracts item rows from a Katlux DataProvider instance.
|
|
11
|
+
* Prioritizes complete unpaginated datasets (filteredData or allData)
|
|
12
|
+
* so charts display all data points instead of being truncated to pageData (e.g. pageSize: 10).
|
|
11
13
|
*/
|
|
12
14
|
export declare function extractProviderItems<T = any>(provider?: ADataProvider | any): T[];
|
|
13
15
|
/**
|
|
14
16
|
* Provider-agnostic data watcher for Katlux chart components.
|
|
15
|
-
* Consumes data
|
|
16
|
-
* using strictly the `pageData` property.
|
|
17
|
+
* Consumes data through Katlux DataProvider instances (Server-side or Client-side).
|
|
17
18
|
*/
|
|
18
19
|
export declare function useChartData<T = any>(dataProvider: Ref<ADataProvider | null | undefined> | ADataProvider | null | undefined): IChartDataContext<T>;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { ref, watch, onMounted, isRef } from "vue";
|
|
1
|
+
import { ref, watch, onMounted, 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
|
-
|
|
7
|
-
if (
|
|
8
|
-
|
|
6
|
+
const filtered = isRef(instance.filteredData) ? instance.filteredData.value : instance.filteredData;
|
|
7
|
+
if (Array.isArray(filtered) && filtered.length > 0) {
|
|
8
|
+
return filtered;
|
|
9
9
|
}
|
|
10
|
+
const all = isRef(instance.allData) ? instance.allData.value : instance.allData;
|
|
11
|
+
if (Array.isArray(all) && all.length > 0) {
|
|
12
|
+
return all;
|
|
13
|
+
}
|
|
14
|
+
let raw = isRef(instance.pageData) ? instance.pageData.value : instance.pageData;
|
|
10
15
|
if (!raw) return [];
|
|
11
16
|
if (Array.isArray(raw)) {
|
|
12
17
|
return raw;
|
|
@@ -20,15 +25,9 @@ export function useChartData(dataProvider) {
|
|
|
20
25
|
const items = ref([]);
|
|
21
26
|
const loading = ref(false);
|
|
22
27
|
const error = ref(null);
|
|
23
|
-
const ensureUnpaginated = (provider) => {
|
|
24
|
-
if (provider && provider.pageSize && isRef(provider.pageSize) && provider.pageSize.value !== -1) {
|
|
25
|
-
provider.pageSize.value = -1;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
28
|
const updateItems = () => {
|
|
29
29
|
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
30
30
|
if (provider) {
|
|
31
|
-
ensureUnpaginated(provider);
|
|
32
31
|
items.value = extractProviderItems(provider);
|
|
33
32
|
} else {
|
|
34
33
|
items.value = [];
|
|
@@ -38,16 +37,15 @@ export function useChartData(dataProvider) {
|
|
|
38
37
|
() => {
|
|
39
38
|
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
40
39
|
if (!provider) return null;
|
|
40
|
+
const filtered = isRef(provider.filteredData) ? provider.filteredData.value : provider.filteredData;
|
|
41
|
+
const all = isRef(provider.allData) ? provider.allData.value : provider.allData;
|
|
41
42
|
const pd = isRef(provider.pageData) ? provider.pageData.value : provider.pageData;
|
|
42
|
-
return { provider, pd };
|
|
43
|
+
return { provider, filtered, all, pd };
|
|
43
44
|
},
|
|
44
|
-
(
|
|
45
|
-
if (newVal?.provider) {
|
|
46
|
-
ensureUnpaginated(newVal.provider);
|
|
47
|
-
}
|
|
45
|
+
() => {
|
|
48
46
|
updateItems();
|
|
49
47
|
},
|
|
50
|
-
{ immediate: true, deep:
|
|
48
|
+
{ immediate: true, deep: false }
|
|
51
49
|
);
|
|
52
50
|
const reload = async () => {
|
|
53
51
|
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
@@ -55,7 +53,6 @@ export function useChartData(dataProvider) {
|
|
|
55
53
|
loading.value = true;
|
|
56
54
|
error.value = null;
|
|
57
55
|
try {
|
|
58
|
-
ensureUnpaginated(provider);
|
|
59
56
|
if (provider.refresh) {
|
|
60
57
|
await provider.refresh();
|
|
61
58
|
} else if (provider.loadPageData) {
|
|
@@ -68,6 +65,10 @@ export function useChartData(dataProvider) {
|
|
|
68
65
|
loading.value = false;
|
|
69
66
|
}
|
|
70
67
|
};
|
|
71
|
-
|
|
68
|
+
if (getCurrentInstance()) {
|
|
69
|
+
onMounted(updateItems);
|
|
70
|
+
} else {
|
|
71
|
+
updateItems();
|
|
72
|
+
}
|
|
72
73
|
return { items, loading, error, reload };
|
|
73
74
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface IChartExportContext {
|
|
2
2
|
exportPng: (canvas: HTMLCanvasElement, filename?: string) => void;
|
|
3
|
-
exportSvg: (svgEl: SVGSVGElement, filename?: string) => void;
|
|
3
|
+
exportSvg: (svgEl: SVGSVGElement, filename?: string, canvas?: HTMLCanvasElement | null) => void;
|
|
4
4
|
}
|
|
5
5
|
export declare function useChartExport(): IChartExportContext;
|
|
@@ -16,10 +16,26 @@ export function useChartExport() {
|
|
|
16
16
|
console.error("[KChart] PNG export failed:", e);
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
-
const exportSvg = (svgEl, filename = "chart.svg") => {
|
|
19
|
+
const exportSvg = (svgEl, filename = "chart.svg", canvas) => {
|
|
20
20
|
try {
|
|
21
|
+
const clone = svgEl.cloneNode(true);
|
|
22
|
+
if (!clone.getAttribute("xmlns")) {
|
|
23
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
24
|
+
}
|
|
25
|
+
if (canvas) {
|
|
26
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
27
|
+
const imageEl = document.createElementNS("http://www.w3.org/2000/svg", "image");
|
|
28
|
+
imageEl.setAttribute("href", dataUrl);
|
|
29
|
+
imageEl.setAttribute("x", "0");
|
|
30
|
+
imageEl.setAttribute("y", "0");
|
|
31
|
+
const w = canvas.style.width ? parseFloat(canvas.style.width) : canvas.width || svgEl.clientWidth || 400;
|
|
32
|
+
const h = canvas.style.height ? parseFloat(canvas.style.height) : canvas.height || svgEl.clientHeight || 400;
|
|
33
|
+
imageEl.setAttribute("width", String(w));
|
|
34
|
+
imageEl.setAttribute("height", String(h));
|
|
35
|
+
clone.insertBefore(imageEl, clone.firstChild);
|
|
36
|
+
}
|
|
21
37
|
const serializer = new XMLSerializer();
|
|
22
|
-
const svgStr = serializer.serializeToString(
|
|
38
|
+
const svgStr = serializer.serializeToString(clone);
|
|
23
39
|
const blob = new Blob([svgStr], { type: "image/svg+xml;charset=utf-8" });
|
|
24
40
|
const url = URL.createObjectURL(blob);
|
|
25
41
|
download(url, filename);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type IChartAxesContext } from './useChartAxes.js';
|
|
2
2
|
export declare function useChartHitTest(): {
|
|
3
|
-
findNearestX: (items: any[], mouseX: number, xField: string, axes: IChartAxesContext) => {
|
|
3
|
+
findNearestX: (items: any[], mouseX: number, xField: string, axes: IChartAxesContext, xValueGetter?: (item: any, index?: number) => number) => {
|
|
4
4
|
item: any;
|
|
5
5
|
index: number;
|
|
6
6
|
} | null;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { parseValue } from "./useChartAxes.js";
|
|
2
2
|
export function useChartHitTest() {
|
|
3
|
-
const findNearestX = (items, mouseX, xField, axes) => {
|
|
3
|
+
const findNearestX = (items, mouseX, xField, axes, xValueGetter) => {
|
|
4
4
|
if (!items.length) return null;
|
|
5
5
|
const dataX = axes.fromX(mouseX);
|
|
6
|
+
const getX = xValueGetter ?? ((item, i) => parseValue(item[xField], i));
|
|
6
7
|
let nearestIdx = 0;
|
|
7
|
-
let minDiff = Math.abs(
|
|
8
|
+
let minDiff = Math.abs(getX(items[0], 0) - dataX);
|
|
8
9
|
for (let i = 1; i < items.length; i++) {
|
|
9
|
-
const val =
|
|
10
|
+
const val = getX(items[i], i);
|
|
10
11
|
const diff = Math.abs(val - dataX);
|
|
11
12
|
if (diff < minDiff) {
|
|
12
13
|
minDiff = diff;
|