@katlux/block-charts 0.1.1-alpha.87 → 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/KLineChart/KLineChart.vue +66 -14
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +75 -11
- package/dist/runtime/composables/useChartAxes.d.ts +1 -0
- package/dist/runtime/composables/useChartAxes.js +6 -2
- 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);
|
|
@@ -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;
|
|
@@ -16,12 +16,16 @@ export function useChartAxes(opts) {
|
|
|
16
16
|
const plotTop = pad.top;
|
|
17
17
|
const plotWidth = computed(() => Math.max(0, opts.width.value - pad.left - pad.right));
|
|
18
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
|
+
};
|
|
19
23
|
const rawXMin = computed(() => {
|
|
20
|
-
const vals = opts.items.value.map((r, i) =>
|
|
24
|
+
const vals = opts.items.value.map((r, i) => getXVal(r, i));
|
|
21
25
|
return vals.length ? Math.min(...vals) : 0;
|
|
22
26
|
});
|
|
23
27
|
const rawXMax = computed(() => {
|
|
24
|
-
const vals = opts.items.value.map((r, i) =>
|
|
28
|
+
const vals = opts.items.value.map((r, i) => getXVal(r, i));
|
|
25
29
|
return vals.length ? Math.max(...vals) : 1;
|
|
26
30
|
});
|
|
27
31
|
const yMin = computed(() => {
|
|
@@ -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;
|