@katlux/block-charts 0.1.1-alpha.86 → 0.1.1-alpha.88
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.vue +78 -18
- package/dist/runtime/components/KHeatMap/KHeatMap.vue +12 -2
- package/dist/runtime/components/KLineChart/KLineChart.vue +66 -14
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +75 -11
- package/dist/runtime/composables/useChartAxes.d.ts +3 -2
- package/dist/runtime/composables/useChartAxes.js +12 -7
- package/dist/runtime/composables/useChartCanvas.d.ts +1 -0
- package/dist/runtime/composables/useChartCanvas.js +33 -18
- package/dist/runtime/composables/useChartData.d.ts +9 -5
- package/dist/runtime/composables/useChartData.js +56 -28
- package/dist/runtime/composables/useChartHitTest.d.ts +1 -1
- package/dist/runtime/composables/useChartHitTest.js +4 -3
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
:height="axes.plotHeight.value"
|
|
31
31
|
)
|
|
32
32
|
g.k-chart-hits
|
|
33
|
-
// No more rect v-for here.
|
|
34
33
|
|
|
35
34
|
// Dynamic Interaction Layer
|
|
36
35
|
g.k-chart-interaction(v-if="hoveredIndex !== -1" clip-path="url(#plot-clip)")
|
|
@@ -122,6 +121,34 @@ const {
|
|
|
122
121
|
} = viewport;
|
|
123
122
|
const xFieldRef = computed(() => props.xField);
|
|
124
123
|
const yFieldRef = computed(() => props.yField);
|
|
124
|
+
const xAxisType = computed(() => {
|
|
125
|
+
if (!items.value.length) return "numeric";
|
|
126
|
+
const sample = items.value[0][props.xField];
|
|
127
|
+
if (typeof sample === "number") return "numeric";
|
|
128
|
+
if (typeof sample === "string") {
|
|
129
|
+
const ts = Date.parse(sample);
|
|
130
|
+
if (!isNaN(ts) && isNaN(Number(sample))) return "datetime";
|
|
131
|
+
return "category";
|
|
132
|
+
}
|
|
133
|
+
return "numeric";
|
|
134
|
+
});
|
|
135
|
+
const categoryList = computed(() => {
|
|
136
|
+
if (xAxisType.value !== "category") return [];
|
|
137
|
+
return [...new Set(items.value.map((r) => String(r[props.xField])))];
|
|
138
|
+
});
|
|
139
|
+
const categoryIndexMap = computed(() => {
|
|
140
|
+
const map = {};
|
|
141
|
+
categoryList.value.forEach((cat, i) => {
|
|
142
|
+
map[cat] = i;
|
|
143
|
+
});
|
|
144
|
+
return map;
|
|
145
|
+
});
|
|
146
|
+
const resolveX = (item, index = 0) => {
|
|
147
|
+
if (!item) return index;
|
|
148
|
+
const raw = item[props.xField];
|
|
149
|
+
if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
|
|
150
|
+
return parseValue(raw, index);
|
|
151
|
+
};
|
|
125
152
|
const seriesMap = computed(() => {
|
|
126
153
|
const map = /* @__PURE__ */ new Map();
|
|
127
154
|
for (const item of items.value) {
|
|
@@ -131,7 +158,17 @@ const seriesMap = computed(() => {
|
|
|
131
158
|
}
|
|
132
159
|
return map;
|
|
133
160
|
});
|
|
134
|
-
const axes = useChartAxes({
|
|
161
|
+
const axes = useChartAxes({
|
|
162
|
+
items,
|
|
163
|
+
xField: xFieldRef,
|
|
164
|
+
yField: yFieldRef,
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
scale,
|
|
168
|
+
panOffset,
|
|
169
|
+
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
170
|
+
xValueGetter: (r, i) => resolveX(r, i)
|
|
171
|
+
});
|
|
135
172
|
const draw = () => {
|
|
136
173
|
if (!ctx.value) return;
|
|
137
174
|
clear(props.backgroundColor);
|
|
@@ -164,12 +201,35 @@ const draw = () => {
|
|
|
164
201
|
c.fillText(val.toFixed(1), axes.plotLeft - 8, axes.toY(val) + 4);
|
|
165
202
|
}
|
|
166
203
|
c.textAlign = "center";
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
204
|
+
if (xAxisType.value === "category") {
|
|
205
|
+
const count = categoryList.value.length;
|
|
206
|
+
if (count > 0) {
|
|
207
|
+
const xSteps = Math.min(10, count - 1);
|
|
208
|
+
const stepSize = xSteps > 0 ? (count - 1) / xSteps : 1;
|
|
209
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
210
|
+
const idx = Math.round(i * stepSize);
|
|
211
|
+
const x = axes.toX(idx);
|
|
212
|
+
const label = categoryList.value[idx] ?? "";
|
|
213
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
for (let i = 0; i <= 5; i++) {
|
|
218
|
+
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
|
|
219
|
+
const x = axes.toX(val);
|
|
220
|
+
let label;
|
|
221
|
+
if (xAxisType.value === "datetime") {
|
|
222
|
+
const allMidnight = items.value.every((r) => {
|
|
223
|
+
const ts = parseValue(r[props.xField]);
|
|
224
|
+
const d = new Date(ts);
|
|
225
|
+
return d.getHours() === 0 && d.getMinutes() === 0;
|
|
226
|
+
});
|
|
227
|
+
label = allMidnight ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
228
|
+
} else {
|
|
229
|
+
label = val.toFixed(1);
|
|
230
|
+
}
|
|
231
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
232
|
+
}
|
|
173
233
|
}
|
|
174
234
|
c.beginPath();
|
|
175
235
|
c.moveTo(axes.plotLeft, axes.plotTop);
|
|
@@ -195,22 +255,22 @@ const draw = () => {
|
|
|
195
255
|
c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
|
|
196
256
|
c.clip();
|
|
197
257
|
let seriesIdx = 0;
|
|
198
|
-
for (const [
|
|
258
|
+
for (const [, seriesItems] of seriesMap.value) {
|
|
199
259
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
200
|
-
const sorted = [...seriesItems].sort((a, b) =>
|
|
260
|
+
const sorted = [...seriesItems].sort((a, b) => resolveX(a) - resolveX(b));
|
|
201
261
|
const drawCount = Math.max(1, Math.floor(sorted.length * p));
|
|
202
262
|
const pts = sorted.slice(0, drawCount);
|
|
203
263
|
c.save();
|
|
204
264
|
c.beginPath();
|
|
205
265
|
pts.forEach((item, i) => {
|
|
206
|
-
const x = axes.toX(
|
|
266
|
+
const x = axes.toX(resolveX(item, i));
|
|
207
267
|
const y = axes.toY(parseValue(item[props.yField]));
|
|
208
268
|
if (i === 0) c.moveTo(x, y);
|
|
209
269
|
else c.lineTo(x, y);
|
|
210
270
|
});
|
|
211
271
|
if (pts.length > 0) {
|
|
212
|
-
const lastX = axes.toX(
|
|
213
|
-
const firstX = axes.toX(
|
|
272
|
+
const lastX = axes.toX(resolveX(pts[pts.length - 1], pts.length - 1));
|
|
273
|
+
const firstX = axes.toX(resolveX(pts[0], 0));
|
|
214
274
|
const baselineY = axes.plotTop + axes.plotHeight.value;
|
|
215
275
|
c.lineTo(lastX, baselineY);
|
|
216
276
|
c.lineTo(firstX, baselineY);
|
|
@@ -220,7 +280,7 @@ const draw = () => {
|
|
|
220
280
|
c.fill();
|
|
221
281
|
c.beginPath();
|
|
222
282
|
pts.forEach((item, i) => {
|
|
223
|
-
const x = axes.toX(
|
|
283
|
+
const x = axes.toX(resolveX(item, i));
|
|
224
284
|
const y = axes.toY(parseValue(item[props.yField]));
|
|
225
285
|
if (i === 0) c.moveTo(x, y);
|
|
226
286
|
else c.lineTo(x, y);
|
|
@@ -265,12 +325,12 @@ watch(panOffset, (v) => emit("pan-change", v), { deep: true });
|
|
|
265
325
|
const hoveredPoints = computed(() => {
|
|
266
326
|
if (hoveredIndex.value === -1) return [];
|
|
267
327
|
const item = items.value[hoveredIndex.value];
|
|
268
|
-
const xVal =
|
|
328
|
+
const xVal = resolveX(item);
|
|
269
329
|
const xPx = axes.toX(xVal);
|
|
270
330
|
const pts = [];
|
|
271
331
|
let seriesIdx = 0;
|
|
272
|
-
for (const [
|
|
273
|
-
const seriesItem = seriesItems.find((it) =>
|
|
332
|
+
for (const [, seriesItems] of seriesMap.value) {
|
|
333
|
+
const seriesItem = seriesItems.find((it) => resolveX(it) === xVal);
|
|
274
334
|
if (seriesItem) {
|
|
275
335
|
pts.push({
|
|
276
336
|
x: xPx,
|
|
@@ -303,7 +363,7 @@ const onMouseMove = (e) => {
|
|
|
303
363
|
if (!rect) return;
|
|
304
364
|
const mouseX = e.clientX - rect.left;
|
|
305
365
|
const mouseY = e.clientY - rect.top;
|
|
306
|
-
const nearest = findNearestX(items.value, mouseX, props.xField, axes);
|
|
366
|
+
const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
|
|
307
367
|
if (nearest) {
|
|
308
368
|
setHovered(nearest.index);
|
|
309
369
|
showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
|
|
@@ -65,7 +65,7 @@ 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
71
|
const { items } = useChartData(dataRef);
|
|
@@ -137,6 +137,7 @@ const draw = () => {
|
|
|
137
137
|
});
|
|
138
138
|
if (props.showLegend) drawLegend(c);
|
|
139
139
|
};
|
|
140
|
+
watch([items, renderedCells, hoveredIndex, width, height, resizeTick], draw, { deep: true });
|
|
140
141
|
const drawAxes = (c) => {
|
|
141
142
|
c.save();
|
|
142
143
|
c.fillStyle = "rgba(120,120,140,0.8)";
|
|
@@ -185,7 +186,16 @@ const drawLegend = (c) => {
|
|
|
185
186
|
c.fillText(maxVal.value.toString(), x + gradW, y + gradH + 10);
|
|
186
187
|
c.restore();
|
|
187
188
|
};
|
|
188
|
-
watch(
|
|
189
|
+
watch(isLoading, (val) => {
|
|
190
|
+
if (!val) {
|
|
191
|
+
nextTick(() => {
|
|
192
|
+
if (canvasRef.value) {
|
|
193
|
+
setupCanvas(canvasRef.value);
|
|
194
|
+
draw();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
189
199
|
onMounted(async () => {
|
|
190
200
|
await nextTick();
|
|
191
201
|
if (containerRef.value && canvasRef.value) {
|
|
@@ -122,6 +122,34 @@ const {
|
|
|
122
122
|
} = viewport;
|
|
123
123
|
const xFieldRef = computed(() => props.xField);
|
|
124
124
|
const yFieldRef = computed(() => props.yField);
|
|
125
|
+
const xAxisType = computed(() => {
|
|
126
|
+
if (!items.value.length) return "numeric";
|
|
127
|
+
const sample = items.value[0][props.xField];
|
|
128
|
+
if (typeof sample === "number") return "numeric";
|
|
129
|
+
if (typeof sample === "string") {
|
|
130
|
+
const ts = Date.parse(sample);
|
|
131
|
+
if (!isNaN(ts) && isNaN(Number(sample))) return "datetime";
|
|
132
|
+
return "category";
|
|
133
|
+
}
|
|
134
|
+
return "numeric";
|
|
135
|
+
});
|
|
136
|
+
const categoryList = computed(() => {
|
|
137
|
+
if (xAxisType.value !== "category") return [];
|
|
138
|
+
return [...new Set(items.value.map((r) => String(r[props.xField])))];
|
|
139
|
+
});
|
|
140
|
+
const categoryIndexMap = computed(() => {
|
|
141
|
+
const map = {};
|
|
142
|
+
categoryList.value.forEach((cat, i) => {
|
|
143
|
+
map[cat] = i;
|
|
144
|
+
});
|
|
145
|
+
return map;
|
|
146
|
+
});
|
|
147
|
+
const resolveX = (item, index = 0) => {
|
|
148
|
+
if (!item) return index;
|
|
149
|
+
const raw = item[props.xField];
|
|
150
|
+
if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
|
|
151
|
+
return parseValue(raw, index);
|
|
152
|
+
};
|
|
125
153
|
const seriesMap = computed(() => {
|
|
126
154
|
const map = /* @__PURE__ */ new Map();
|
|
127
155
|
for (const item of items.value) {
|
|
@@ -139,7 +167,8 @@ const axes = useChartAxes({
|
|
|
139
167
|
height,
|
|
140
168
|
scale,
|
|
141
169
|
panOffset,
|
|
142
|
-
padding: { top: 20, right: 20, bottom: 40, left: 55 }
|
|
170
|
+
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
171
|
+
xValueGetter: (r, i) => resolveX(r, i)
|
|
143
172
|
});
|
|
144
173
|
const draw = () => {
|
|
145
174
|
if (!ctx.value) return;
|
|
@@ -220,13 +249,36 @@ const drawAxes = (c) => {
|
|
|
220
249
|
c.fillText(val.toFixed(1), axes.plotLeft - 8, y + 4);
|
|
221
250
|
}
|
|
222
251
|
c.textAlign = "center";
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
if (xAxisType.value === "category") {
|
|
253
|
+
const count = categoryList.value.length;
|
|
254
|
+
if (count > 0) {
|
|
255
|
+
const xSteps = Math.min(10, count - 1);
|
|
256
|
+
const stepSize = xSteps > 0 ? (count - 1) / xSteps : 1;
|
|
257
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
258
|
+
const idx = Math.round(i * stepSize);
|
|
259
|
+
const x = axes.toX(idx);
|
|
260
|
+
const label = categoryList.value[idx] ?? "";
|
|
261
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
const xSteps = 5;
|
|
266
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
267
|
+
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / xSteps);
|
|
268
|
+
const x = axes.toX(val);
|
|
269
|
+
let label;
|
|
270
|
+
if (xAxisType.value === "datetime") {
|
|
271
|
+
const allMidnight = items.value.every((r) => {
|
|
272
|
+
const ts = parseValue(r[props.xField]);
|
|
273
|
+
const d = new Date(ts);
|
|
274
|
+
return d.getHours() === 0 && d.getMinutes() === 0;
|
|
275
|
+
});
|
|
276
|
+
label = allMidnight ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
277
|
+
} else {
|
|
278
|
+
label = val.toFixed(1);
|
|
279
|
+
}
|
|
280
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
281
|
+
}
|
|
230
282
|
}
|
|
231
283
|
c.beginPath();
|
|
232
284
|
c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
|
|
@@ -253,7 +305,7 @@ const drawAxes = (c) => {
|
|
|
253
305
|
};
|
|
254
306
|
const drawLine = (c, data, color, progress2) => {
|
|
255
307
|
if (!data.length) return;
|
|
256
|
-
const sorted = [...data].sort((a, b) =>
|
|
308
|
+
const sorted = [...data].sort((a, b) => resolveX(a) - resolveX(b));
|
|
257
309
|
const totalPts = sorted.length;
|
|
258
310
|
const drawCount = Math.max(1, Math.floor(totalPts * progress2));
|
|
259
311
|
c.save();
|
|
@@ -263,7 +315,7 @@ const drawLine = (c, data, color, progress2) => {
|
|
|
263
315
|
c.lineCap = "round";
|
|
264
316
|
c.beginPath();
|
|
265
317
|
sorted.slice(0, drawCount).forEach((item, i) => {
|
|
266
|
-
const x = axes.toX(
|
|
318
|
+
const x = axes.toX(resolveX(item, i));
|
|
267
319
|
const y = axes.toY(parseValue(item[props.yField]));
|
|
268
320
|
if (i === 0) c.moveTo(x, y);
|
|
269
321
|
else c.lineTo(x, y);
|
|
@@ -271,7 +323,7 @@ const drawLine = (c, data, color, progress2) => {
|
|
|
271
323
|
c.stroke();
|
|
272
324
|
for (let i = 0; i < drawCount; i++) {
|
|
273
325
|
const item = sorted[i];
|
|
274
|
-
const x = axes.toX(
|
|
326
|
+
const x = axes.toX(resolveX(item, i));
|
|
275
327
|
const y = axes.toY(parseValue(item[props.yField]));
|
|
276
328
|
const isHovered = hoveredIndex.value === items.value.indexOf(item);
|
|
277
329
|
c.beginPath();
|
|
@@ -301,12 +353,12 @@ onMounted(async () => {
|
|
|
301
353
|
const hoveredPoints = computed(() => {
|
|
302
354
|
if (hoveredIndex.value === -1) return [];
|
|
303
355
|
const item = items.value[hoveredIndex.value];
|
|
304
|
-
const xVal =
|
|
356
|
+
const xVal = resolveX(item);
|
|
305
357
|
const xPx = axes.toX(xVal);
|
|
306
358
|
const pts = [];
|
|
307
359
|
let seriesIdx = 0;
|
|
308
360
|
for (const [key, seriesItems] of seriesMap.value) {
|
|
309
|
-
const seriesItem = seriesItems.find((it) =>
|
|
361
|
+
const seriesItem = seriesItems.find((it) => resolveX(it) === xVal);
|
|
310
362
|
if (seriesItem) {
|
|
311
363
|
pts.push({
|
|
312
364
|
x: xPx,
|
|
@@ -331,7 +383,7 @@ const onMouseMove = (e) => {
|
|
|
331
383
|
if (!rect) return;
|
|
332
384
|
const mouseX = e.clientX - rect.left;
|
|
333
385
|
const mouseY = e.clientY - rect.top;
|
|
334
|
-
const nearest = findNearestX(items.value, mouseX, props.xField, axes);
|
|
386
|
+
const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
|
|
335
387
|
if (nearest) {
|
|
336
388
|
setHovered(nearest.index);
|
|
337
389
|
showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
|
|
@@ -25,10 +25,9 @@
|
|
|
25
25
|
:height="axes.plotHeight.value"
|
|
26
26
|
)
|
|
27
27
|
g.k-chart-hits
|
|
28
|
-
// No circles here! Distance-based hit testing now.
|
|
29
28
|
|
|
30
29
|
// Dynamic Interaction Layer
|
|
31
|
-
g.k-chart-interaction(v-if="hoveredIndex !== -1 && !isLoading" clip-path="url(#plot-clip)")
|
|
30
|
+
g.k-chart-interaction(v-if="hoveredIndex !== -1 && !isLoading && hoveredPoint" clip-path="url(#plot-clip)")
|
|
32
31
|
circle.k-chart-hit-proxy(
|
|
33
32
|
:cx="hoveredPoint.x"
|
|
34
33
|
:cy="hoveredPoint.y"
|
|
@@ -56,7 +55,7 @@ import { ref, watch, computed, onMounted, nextTick } from "vue";
|
|
|
56
55
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
57
56
|
import { useChartSvg } from "../../composables/useChartSvg";
|
|
58
57
|
import { useChartData } from "../../composables/useChartData";
|
|
59
|
-
import { useChartAxes } from "../../composables/useChartAxes";
|
|
58
|
+
import { useChartAxes, parseValue } from "../../composables/useChartAxes";
|
|
60
59
|
import { useChartViewport } from "../../composables/useChartViewport";
|
|
61
60
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
62
61
|
import { useChartExport } from "../../composables/useChartExport";
|
|
@@ -111,7 +110,45 @@ const {
|
|
|
111
110
|
} = viewport;
|
|
112
111
|
const xFieldRef = computed(() => props.xField);
|
|
113
112
|
const yFieldRef = computed(() => props.yField);
|
|
114
|
-
const
|
|
113
|
+
const xAxisType = computed(() => {
|
|
114
|
+
if (!items.value.length) return "numeric";
|
|
115
|
+
const sample = items.value[0][props.xField];
|
|
116
|
+
if (typeof sample === "number") return "numeric";
|
|
117
|
+
if (typeof sample === "string") {
|
|
118
|
+
const ts = Date.parse(sample);
|
|
119
|
+
if (!isNaN(ts) && isNaN(Number(sample))) return "datetime";
|
|
120
|
+
return "category";
|
|
121
|
+
}
|
|
122
|
+
return "numeric";
|
|
123
|
+
});
|
|
124
|
+
const categoryList = computed(() => {
|
|
125
|
+
if (xAxisType.value !== "category") return [];
|
|
126
|
+
return [...new Set(items.value.map((r) => String(r[props.xField])))];
|
|
127
|
+
});
|
|
128
|
+
const categoryIndexMap = computed(() => {
|
|
129
|
+
const map = {};
|
|
130
|
+
categoryList.value.forEach((cat, i) => {
|
|
131
|
+
map[cat] = i;
|
|
132
|
+
});
|
|
133
|
+
return map;
|
|
134
|
+
});
|
|
135
|
+
const resolveX = (item, index = 0) => {
|
|
136
|
+
if (!item) return index;
|
|
137
|
+
const raw = item[props.xField];
|
|
138
|
+
if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
|
|
139
|
+
return parseValue(raw, index);
|
|
140
|
+
};
|
|
141
|
+
const axes = useChartAxes({
|
|
142
|
+
items,
|
|
143
|
+
xField: xFieldRef,
|
|
144
|
+
yField: yFieldRef,
|
|
145
|
+
width,
|
|
146
|
+
height,
|
|
147
|
+
scale,
|
|
148
|
+
panOffset,
|
|
149
|
+
padding: { top: 20, right: 20, bottom: 40, left: 55 },
|
|
150
|
+
xValueGetter: (r, i) => resolveX(r, i)
|
|
151
|
+
});
|
|
115
152
|
const maxSize = computed(() => Math.max(...items.value.map((r) => Number(r[props.sizeField] ?? 1)), 1));
|
|
116
153
|
const seriesMap = computed(() => {
|
|
117
154
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -125,6 +162,7 @@ const seriesMap = computed(() => {
|
|
|
125
162
|
const hoveredPoint = computed(() => {
|
|
126
163
|
if (hoveredIndex.value === -1) return null;
|
|
127
164
|
const item = items.value[hoveredIndex.value];
|
|
165
|
+
if (!item) return null;
|
|
128
166
|
const sizeRaw = Number(item[props.sizeField] ?? 1);
|
|
129
167
|
const r = 4 + sizeRaw / maxSize.value * 16;
|
|
130
168
|
const seriesKey = String(item[props.seriesField] ?? "default");
|
|
@@ -132,8 +170,8 @@ const hoveredPoint = computed(() => {
|
|
|
132
170
|
const seriesIdx = seriesKeys.indexOf(seriesKey);
|
|
133
171
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
134
172
|
return {
|
|
135
|
-
x: axes.toX(
|
|
136
|
-
y: axes.toY(
|
|
173
|
+
x: axes.toX(resolveX(item, hoveredIndex.value)),
|
|
174
|
+
y: axes.toY(parseValue(item[props.yField])),
|
|
137
175
|
r,
|
|
138
176
|
color,
|
|
139
177
|
item,
|
|
@@ -179,9 +217,35 @@ const draw = () => {
|
|
|
179
217
|
c.fillText(val.toFixed(1), axes.plotLeft - 8, axes.toY(val) + 4);
|
|
180
218
|
}
|
|
181
219
|
c.textAlign = "center";
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
220
|
+
if (xAxisType.value === "category") {
|
|
221
|
+
const count = categoryList.value.length;
|
|
222
|
+
if (count > 0) {
|
|
223
|
+
const xSteps = Math.min(10, count - 1);
|
|
224
|
+
const stepSize = xSteps > 0 ? (count - 1) / xSteps : 1;
|
|
225
|
+
for (let i = 0; i <= xSteps; i++) {
|
|
226
|
+
const idx = Math.round(i * stepSize);
|
|
227
|
+
const x = axes.toX(idx);
|
|
228
|
+
const label = categoryList.value[idx] ?? "";
|
|
229
|
+
c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
for (let i = 0; i <= 5; i++) {
|
|
234
|
+
const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
|
|
235
|
+
const x = axes.toX(val);
|
|
236
|
+
let label;
|
|
237
|
+
if (xAxisType.value === "datetime") {
|
|
238
|
+
const allMidnight = items.value.every((r) => {
|
|
239
|
+
const ts = parseValue(r[props.xField]);
|
|
240
|
+
const d = new Date(ts);
|
|
241
|
+
return d.getHours() === 0 && d.getMinutes() === 0;
|
|
242
|
+
});
|
|
243
|
+
label = allMidnight ? new Date(val).toLocaleDateString([], { month: "short", day: "numeric" }) : new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
244
|
+
} else {
|
|
245
|
+
label = val.toFixed(1);
|
|
246
|
+
}
|
|
247
|
+
c.fillText(label, x, axes.toX(val), axes.plotTop + axes.plotHeight.value + 16);
|
|
248
|
+
}
|
|
185
249
|
}
|
|
186
250
|
c.fillStyle = "rgba(120,120,140,0.8)";
|
|
187
251
|
if (props.xAxisTitle) {
|
|
@@ -204,8 +268,8 @@ const draw = () => {
|
|
|
204
268
|
for (const [, seriesItems] of seriesMap.value) {
|
|
205
269
|
const color = props.colors[seriesIdx % props.colors.length];
|
|
206
270
|
seriesItems.forEach((item) => {
|
|
207
|
-
const x = axes.toX(
|
|
208
|
-
const y = axes.toY(
|
|
271
|
+
const x = axes.toX(resolveX(item, items.value.indexOf(item)));
|
|
272
|
+
const y = axes.toY(parseValue(item[props.yField]));
|
|
209
273
|
const sizeRaw = Number(item[props.sizeField] ?? 1);
|
|
210
274
|
const r = 4 + sizeRaw / maxSize.value * 16;
|
|
211
275
|
const isHovered = items.value[hoveredIndex.value] === item;
|
|
@@ -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,7 @@ export interface IChartAxesContext {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* Robustly parses a value into a number.
|
|
37
|
-
* Handles: numbers, numeric strings,
|
|
38
|
+
* Handles: numbers, numeric strings, ISO date strings, and falls back to array index for unparseable category strings.
|
|
38
39
|
*/
|
|
39
|
-
export declare function parseValue(val: any): number;
|
|
40
|
+
export declare function parseValue(val: any, fallbackIndex?: number): number;
|
|
40
41
|
export declare function useChartAxes(opts: IChartAxesOptions): IChartAxesContext;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
-
export function parseValue(val) {
|
|
3
|
-
if (val === null || val === void 0) return 0;
|
|
2
|
+
export function parseValue(val, fallbackIndex) {
|
|
3
|
+
if (val === null || val === void 0) return fallbackIndex ?? 0;
|
|
4
|
+
if (typeof val === "number") return isNaN(val) ? fallbackIndex ?? 0 : val;
|
|
4
5
|
const n = Number(val);
|
|
5
6
|
if (!isNaN(n)) return n;
|
|
6
7
|
if (typeof val === "string") {
|
|
7
8
|
const d = Date.parse(val);
|
|
8
9
|
if (!isNaN(d)) return d;
|
|
9
10
|
}
|
|
10
|
-
return 0;
|
|
11
|
+
return fallbackIndex ?? 0;
|
|
11
12
|
}
|
|
12
13
|
export function useChartAxes(opts) {
|
|
13
14
|
const pad = opts.padding ?? { top: 20, right: 20, bottom: 40, left: 55 };
|
|
@@ -15,20 +16,24 @@ export function useChartAxes(opts) {
|
|
|
15
16
|
const plotTop = pad.top;
|
|
16
17
|
const plotWidth = computed(() => Math.max(0, opts.width.value - pad.left - pad.right));
|
|
17
18
|
const plotHeight = computed(() => Math.max(0, opts.height.value - pad.top - pad.bottom));
|
|
19
|
+
const getXVal = (r, i) => {
|
|
20
|
+
if (opts.xValueGetter) return opts.xValueGetter(r, i);
|
|
21
|
+
return parseValue(r[opts.xField.value], i);
|
|
22
|
+
};
|
|
18
23
|
const rawXMin = computed(() => {
|
|
19
|
-
const vals = opts.items.value.map((r) =>
|
|
24
|
+
const vals = opts.items.value.map((r, i) => getXVal(r, i));
|
|
20
25
|
return vals.length ? Math.min(...vals) : 0;
|
|
21
26
|
});
|
|
22
27
|
const rawXMax = computed(() => {
|
|
23
|
-
const vals = opts.items.value.map((r) =>
|
|
28
|
+
const vals = opts.items.value.map((r, i) => getXVal(r, i));
|
|
24
29
|
return vals.length ? Math.max(...vals) : 1;
|
|
25
30
|
});
|
|
26
31
|
const yMin = computed(() => {
|
|
27
|
-
const vals = opts.items.value.map((r) => parseValue(r[opts.yField.value]));
|
|
32
|
+
const vals = opts.items.value.map((r, i) => parseValue(r[opts.yField.value], i));
|
|
28
33
|
return vals.length ? Math.min(...vals) : 0;
|
|
29
34
|
});
|
|
30
35
|
const yMax = computed(() => {
|
|
31
|
-
const vals = opts.items.value.map((r) => parseValue(r[opts.yField.value]));
|
|
36
|
+
const vals = opts.items.value.map((r, i) => parseValue(r[opts.yField.value], i));
|
|
32
37
|
return vals.length ? Math.max(...vals) : 1;
|
|
33
38
|
});
|
|
34
39
|
const xRange = computed(() => rawXMax.value - rawXMin.value);
|
|
@@ -4,32 +4,47 @@ export function useChartCanvas() {
|
|
|
4
4
|
const ctx = ref(null);
|
|
5
5
|
const width = ref(0);
|
|
6
6
|
const height = ref(0);
|
|
7
|
-
const dpr = ref(window
|
|
7
|
+
const dpr = ref(typeof window !== "undefined" ? window.devicePixelRatio ?? 1 : 1);
|
|
8
|
+
const resizeTick = ref(0);
|
|
8
9
|
let resizeObserver = null;
|
|
9
10
|
const applySize = (el, w, h) => {
|
|
10
|
-
const ratio = window.devicePixelRatio ?? 1;
|
|
11
|
+
const ratio = typeof window !== "undefined" ? window.devicePixelRatio ?? 1 : 1;
|
|
12
|
+
const newPixelW = Math.round(w * ratio);
|
|
13
|
+
const newPixelH = Math.round(h * ratio);
|
|
11
14
|
dpr.value = ratio;
|
|
12
|
-
|
|
13
|
-
el.height
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
let sizeChanged = false;
|
|
16
|
+
if (el.width !== newPixelW || el.height !== newPixelH) {
|
|
17
|
+
el.width = newPixelW;
|
|
18
|
+
el.height = newPixelH;
|
|
19
|
+
ctx.value = el.getContext("2d");
|
|
20
|
+
ctx.value?.scale(ratio, ratio);
|
|
21
|
+
sizeChanged = true;
|
|
22
|
+
}
|
|
23
|
+
if (width.value !== w || height.value !== h || sizeChanged) {
|
|
24
|
+
width.value = w;
|
|
25
|
+
height.value = h;
|
|
26
|
+
resizeTick.value++;
|
|
27
|
+
}
|
|
17
28
|
};
|
|
18
29
|
const setupCanvas = (el) => {
|
|
19
30
|
canvas.value = el;
|
|
20
31
|
ctx.value = el.getContext("2d");
|
|
21
32
|
const rect = el.getBoundingClientRect();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
const initialW = rect.width || el.clientWidth || 400;
|
|
34
|
+
const initialH = rect.height || el.clientHeight || 400;
|
|
35
|
+
applySize(el, initialW, initialH);
|
|
36
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
37
|
+
resizeObserver?.disconnect();
|
|
38
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const { width: w, height: h } = entry.contentRect;
|
|
41
|
+
if (canvas.value && w > 0 && h > 0) {
|
|
42
|
+
applySize(canvas.value, w, h);
|
|
43
|
+
}
|
|
29
44
|
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
});
|
|
46
|
+
resizeObserver.observe(el);
|
|
47
|
+
}
|
|
33
48
|
};
|
|
34
49
|
const clear = (color) => {
|
|
35
50
|
if (ctx.value && canvas.value) {
|
|
@@ -43,5 +58,5 @@ export function useChartCanvas() {
|
|
|
43
58
|
onUnmounted(() => {
|
|
44
59
|
resizeObserver?.disconnect();
|
|
45
60
|
});
|
|
46
|
-
return { canvas, ctx, width, height, dpr, clear, setupCanvas };
|
|
61
|
+
return { canvas, ctx, width, height, dpr, resizeTick, clear, setupCanvas };
|
|
47
62
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Ref } from 'vue';
|
|
2
|
+
import type { ADataProvider } from '@katlux/providers';
|
|
2
3
|
export interface IChartDataContext<T = any> {
|
|
3
4
|
items: Ref<T[]>;
|
|
4
5
|
loading: Ref<boolean>;
|
|
@@ -6,9 +7,12 @@ export interface IChartDataContext<T = any> {
|
|
|
6
7
|
reload: () => Promise<void>;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
* 1. Plain arrays
|
|
11
|
-
* 2. Katlux DataProviders (with .pageData ref)
|
|
12
|
-
* 3. Legacy fetchable providers (with .fetch() method)
|
|
10
|
+
* Extracts item rows strictly from a Katlux DataProvider instance's `pageData` property.
|
|
13
11
|
*/
|
|
14
|
-
export declare function
|
|
12
|
+
export declare function extractProviderItems<T = any>(provider?: ADataProvider | any): T[];
|
|
13
|
+
/**
|
|
14
|
+
* Provider-agnostic data watcher for Katlux chart components.
|
|
15
|
+
* Consumes data exclusively through Katlux DataProvider instances (Server-side or Client-side),
|
|
16
|
+
* using strictly the `pageData` property.
|
|
17
|
+
*/
|
|
18
|
+
export declare function useChartData<T = any>(dataProvider: Ref<ADataProvider | null | undefined> | ADataProvider | null | undefined): IChartDataContext<T>;
|
|
@@ -1,45 +1,73 @@
|
|
|
1
1
|
import { ref, watch, onMounted, isRef } from "vue";
|
|
2
|
-
export function
|
|
2
|
+
export function extractProviderItems(provider) {
|
|
3
|
+
if (!provider) return [];
|
|
4
|
+
const instance = isRef(provider) ? provider.value : provider;
|
|
5
|
+
if (!instance) return [];
|
|
6
|
+
let raw = instance.pageData;
|
|
7
|
+
if (isRef(raw)) {
|
|
8
|
+
raw = raw.value;
|
|
9
|
+
}
|
|
10
|
+
if (!raw) return [];
|
|
11
|
+
if (Array.isArray(raw)) {
|
|
12
|
+
return raw;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(raw.rows)) {
|
|
15
|
+
return raw.rows;
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
export function useChartData(dataProvider) {
|
|
3
20
|
const items = ref([]);
|
|
4
21
|
const loading = ref(false);
|
|
5
22
|
const error = ref(null);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
23
|
+
const ensureUnpaginated = (provider) => {
|
|
24
|
+
if (provider && provider.pageSize && isRef(provider.pageSize) && provider.pageSize.value !== -1) {
|
|
25
|
+
provider.pageSize.value = -1;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const updateItems = () => {
|
|
29
|
+
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
30
|
+
if (provider) {
|
|
31
|
+
ensureUnpaginated(provider);
|
|
32
|
+
items.value = extractProviderItems(provider);
|
|
33
|
+
} else {
|
|
9
34
|
items.value = [];
|
|
10
|
-
return;
|
|
11
35
|
}
|
|
36
|
+
};
|
|
37
|
+
watch(
|
|
38
|
+
() => {
|
|
39
|
+
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
40
|
+
if (!provider) return null;
|
|
41
|
+
const pd = isRef(provider.pageData) ? provider.pageData.value : provider.pageData;
|
|
42
|
+
return { provider, pd };
|
|
43
|
+
},
|
|
44
|
+
(newVal) => {
|
|
45
|
+
if (newVal?.provider) {
|
|
46
|
+
ensureUnpaginated(newVal.provider);
|
|
47
|
+
}
|
|
48
|
+
updateItems();
|
|
49
|
+
},
|
|
50
|
+
{ immediate: true, deep: true }
|
|
51
|
+
);
|
|
52
|
+
const reload = async () => {
|
|
53
|
+
const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
|
|
54
|
+
if (!provider) return;
|
|
12
55
|
loading.value = true;
|
|
13
56
|
error.value = null;
|
|
14
57
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
items.value = isRef(source.pageData) ? source.pageData.value : source.pageData;
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (typeof source.fetch === "function") {
|
|
24
|
-
const result = await source.fetch();
|
|
25
|
-
items.value = result?.rows ?? result ?? [];
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (source.rows) {
|
|
29
|
-
items.value = source.rows;
|
|
30
|
-
return;
|
|
58
|
+
ensureUnpaginated(provider);
|
|
59
|
+
if (provider.refresh) {
|
|
60
|
+
await provider.refresh();
|
|
61
|
+
} else if (provider.loadPageData) {
|
|
62
|
+
await provider.loadPageData();
|
|
31
63
|
}
|
|
64
|
+
updateItems();
|
|
32
65
|
} catch (e) {
|
|
33
|
-
error.value = e?.message ?? "Failed to
|
|
66
|
+
error.value = e?.message ?? "Failed to reload chart provider";
|
|
34
67
|
} finally {
|
|
35
68
|
loading.value = false;
|
|
36
69
|
}
|
|
37
70
|
};
|
|
38
|
-
|
|
39
|
-
onMounted(load);
|
|
40
|
-
watch(providerOrData, load);
|
|
41
|
-
watch(() => providerOrData.value?.pageData?.value, (newRows) => {
|
|
42
|
-
if (newRows) items.value = newRows;
|
|
43
|
-
}, { deep: true });
|
|
71
|
+
onMounted(updateItems);
|
|
44
72
|
return { items, loading, error, reload };
|
|
45
73
|
}
|
|
@@ -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;
|