@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 CHANGED
@@ -1,6 +1,8 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
1
3
  interface ModuleOptions {
2
4
  }
3
- declare const _default: any;
5
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
4
6
 
5
7
  export { _default as default };
6
8
  export type { ModuleOptions };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.1-alpha.87",
7
+ "version": "0.1.1-alpha.89",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.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"
@@ -30,7 +29,6 @@
30
29
  :height="axes.plotHeight.value"
31
30
  )
32
31
  g.k-chart-hits
33
- // No more rect v-for here.
34
32
 
35
33
  // Dynamic Interaction Layer
36
34
  g.k-chart-interaction(v-if="hoveredIndex !== -1" clip-path="url(#plot-clip)")
@@ -41,7 +39,6 @@
41
39
  :cy="pt.y"
42
40
  :r="10"
43
41
  fill="transparent"
44
- style="cursor: pointer"
45
42
  @click="emit('click-point', pt.item, pt.index)"
46
43
  )
47
44
  circle(
@@ -57,7 +54,7 @@
57
54
  )
58
55
  .k-chart-tooltip(
59
56
  v-if="tooltipState.visible && !isLoading"
60
- :style="{ left: tooltipState.x + 'px', top: tooltipState.y + 'px' }"
57
+ :style="tooltipStyle"
61
58
  )
62
59
  slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
63
60
  span {{ tooltipState.content }}
@@ -73,6 +70,7 @@ import { useChartViewport } from "../../composables/useChartViewport";
73
70
  import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
74
71
  import { useChartExport } from "../../composables/useChartExport";
75
72
  import { useChartHitTest } from "../../composables/useChartHitTest";
73
+ import { cleanNumber, safeMax } from "../../composables/chartUtils";
76
74
  const props = defineProps({
77
75
  dataProvider: { type: null, required: false },
78
76
  xField: { type: String, required: false, default: "x" },
@@ -92,7 +90,7 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
92
90
  const containerRef = ref(null);
93
91
  const canvasRef = ref(null);
94
92
  const svgRef = ref(null);
95
- const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
93
+ const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
96
94
  const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
97
95
  const dataRef = computed(() => props.dataProvider);
98
96
  const { items } = useChartData(dataRef);
@@ -101,6 +99,10 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
101
99
  const isLoading = computed(() => {
102
100
  return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
103
101
  });
102
+ const tooltipStyle = computed(() => ({
103
+ left: `${tooltipState.value.x}px`,
104
+ top: `${tooltipState.value.y}px`
105
+ }));
104
106
  const maxZoomRef = computed(() => props.maxZoom);
105
107
  const plotWidthRef = computed(() => Math.max(0, width.value - 75));
106
108
  const plotLeftRef = computed(() => 55);
@@ -122,6 +124,45 @@ const {
122
124
  } = viewport;
123
125
  const xFieldRef = computed(() => props.xField);
124
126
  const yFieldRef = computed(() => props.yField);
127
+ const xAxisType = computed(() => {
128
+ if (!items.value.length) return "linear";
129
+ const firstVal = items.value[0]?.[props.xField];
130
+ if (typeof firstVal === "number") return "linear";
131
+ if (typeof firstVal === "string") {
132
+ 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";
136
+ return "category";
137
+ }
138
+ return "linear";
139
+ });
140
+ const categoryList = computed(() => {
141
+ if (xAxisType.value !== "category") return [];
142
+ const seen = /* @__PURE__ */ new Set();
143
+ const list = [];
144
+ for (const item of items.value) {
145
+ const val = String(item[props.xField] ?? "");
146
+ if (!seen.has(val)) {
147
+ seen.add(val);
148
+ list.push(val);
149
+ }
150
+ }
151
+ return list;
152
+ });
153
+ const categoryIndexMap = computed(() => {
154
+ const map = {};
155
+ categoryList.value.forEach((cat, i) => {
156
+ map[cat] = i;
157
+ });
158
+ return map;
159
+ });
160
+ const resolveX = (item, index = 0) => {
161
+ if (!item) return index;
162
+ const raw = item[props.xField];
163
+ if (xAxisType.value === "category") return categoryIndexMap.value[String(raw)] ?? index;
164
+ return parseValue(raw, index);
165
+ };
125
166
  const seriesMap = computed(() => {
126
167
  const map = /* @__PURE__ */ new Map();
127
168
  for (const item of items.value) {
@@ -131,12 +172,60 @@ const seriesMap = computed(() => {
131
172
  }
132
173
  return map;
133
174
  });
134
- const axes = useChartAxes({ items, xField: xFieldRef, yField: yFieldRef, width, height, scale, panOffset, padding: { top: 20, right: 20, bottom: 40, left: 55 } });
175
+ const sortedSeriesMap = computed(() => {
176
+ const map = /* @__PURE__ */ new Map();
177
+ const itemIndexMap = /* @__PURE__ */ new Map();
178
+ for (let i = 0; i < items.value.length; i++) {
179
+ itemIndexMap.set(items.value[i], i);
180
+ }
181
+ for (const [key, seriesItems] of seriesMap.value) {
182
+ const enriched = seriesItems.map((item, idx) => ({
183
+ item,
184
+ index: itemIndexMap.get(item) ?? idx,
185
+ xVal: resolveX(item, idx),
186
+ yVal: cleanNumber(item[props.yField])
187
+ })).sort((a, b) => a.xVal - b.xVal);
188
+ map.set(key, enriched);
189
+ }
190
+ return map;
191
+ });
192
+ const stackedMaxY = computed(() => {
193
+ if (!props.stacked) return 0;
194
+ const xTotals = /* @__PURE__ */ new Map();
195
+ for (const [, seriesItems] of sortedSeriesMap.value) {
196
+ for (const pt of seriesItems) {
197
+ const current = xTotals.get(pt.xVal) || 0;
198
+ xTotals.set(pt.xVal, current + Math.max(0, pt.yVal));
199
+ }
200
+ }
201
+ return safeMax(Array.from(xTotals.values()), 10);
202
+ });
203
+ const axes = useChartAxes({
204
+ items,
205
+ xField: xFieldRef,
206
+ yField: yFieldRef,
207
+ width,
208
+ height,
209
+ scale,
210
+ panOffset,
211
+ padding: { top: 20, right: 20, bottom: 40, left: 55 },
212
+ xValueGetter: (r, i) => resolveX(r, i)
213
+ });
214
+ const effectiveYMax = computed(() => {
215
+ return props.stacked ? Math.max(stackedMaxY.value, axes.yMax.value) : axes.yMax.value;
216
+ });
217
+ const toStackedY = (val) => {
218
+ const yMin = axes.yMin.value;
219
+ const yMax = effectiveYMax.value;
220
+ const range = yMax - yMin;
221
+ if (range === 0) return axes.plotTop + axes.plotHeight.value;
222
+ return axes.plotTop + axes.plotHeight.value - (val - yMin) / range * axes.plotHeight.value;
223
+ };
135
224
  const draw = () => {
136
225
  if (!ctx.value) return;
137
226
  clear(props.backgroundColor);
138
227
  const c = ctx.value;
139
- const p = easeOut(progress.value);
228
+ const p = props.animated ? easeOut(progress.value) : 1;
140
229
  c.save();
141
230
  c.strokeStyle = "rgba(100,100,120,0.1)";
142
231
  c.lineWidth = 1;
@@ -160,17 +249,45 @@ const draw = () => {
160
249
  c.font = "11px system-ui, sans-serif";
161
250
  c.textAlign = "right";
162
251
  for (let i = 0; i <= 5; i++) {
163
- const val = axes.yMin.value + (axes.yMax.value - axes.yMin.value) * (i / 5);
164
- c.fillText(val.toFixed(1), axes.plotLeft - 8, axes.toY(val) + 4);
252
+ const val = axes.yMin.value + (effectiveYMax.value - axes.yMin.value) * (i / 5);
253
+ c.fillText(val.toFixed(1), axes.plotLeft - 8, toStackedY(val) + 4);
165
254
  }
166
255
  c.textAlign = "center";
167
- for (let i = 0; i <= 5; i++) {
168
- const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
169
- const x = axes.toX(val);
170
- let label = val.toFixed(1);
171
- if (val > 1e12) label = new Date(val).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
172
- c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
256
+ if (xAxisType.value === "category") {
257
+ const count = categoryList.value.length;
258
+ if (count > 0) {
259
+ const xSteps = Math.min(10, count - 1);
260
+ const stepSize = xSteps > 0 ? (count - 1) / xSteps : 1;
261
+ for (let i = 0; i <= xSteps; i++) {
262
+ const idx = Math.round(i * stepSize);
263
+ const x = axes.toX(idx);
264
+ const label = categoryList.value[idx] ?? "";
265
+ c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
266
+ }
267
+ }
268
+ } else {
269
+ for (let i = 0; i <= 5; i++) {
270
+ const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
271
+ const x = axes.toX(val);
272
+ let label;
273
+ if (xAxisType.value === "datetime") {
274
+ const allMidnight = items.value.every((r) => {
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" });
280
+ } else {
281
+ label = val.toFixed(1);
282
+ }
283
+ c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
284
+ }
173
285
  }
286
+ c.strokeStyle = "rgba(150,150,170,0.5)";
287
+ c.beginPath();
288
+ c.moveTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
289
+ c.lineTo(axes.plotLeft + axes.plotWidth.value, axes.plotTop + axes.plotHeight.value);
290
+ c.stroke();
174
291
  c.beginPath();
175
292
  c.moveTo(axes.plotLeft, axes.plotTop);
176
293
  c.lineTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
@@ -194,41 +311,74 @@ const draw = () => {
194
311
  c.beginPath();
195
312
  c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
196
313
  c.clip();
314
+ const currentBaseline = /* @__PURE__ */ new Map();
197
315
  let seriesIdx = 0;
198
- for (const [key, seriesItems] of seriesMap.value) {
316
+ for (const [, seriesItems] of sortedSeriesMap.value) {
199
317
  const color = props.colors[seriesIdx % props.colors.length];
200
- const sorted = [...seriesItems].sort((a, b) => parseValue(a[props.xField]) - parseValue(b[props.xField]));
201
- const drawCount = Math.max(1, Math.floor(sorted.length * p));
202
- const pts = sorted.slice(0, drawCount);
318
+ const drawCount = Math.max(1, Math.floor(seriesItems.length * p));
319
+ const pts = seriesItems.slice(0, drawCount);
203
320
  c.save();
204
321
  c.beginPath();
205
- pts.forEach((item, i) => {
206
- const x = axes.toX(parseValue(item[props.xField]));
207
- const y = axes.toY(parseValue(item[props.yField]));
208
- if (i === 0) c.moveTo(x, y);
209
- else c.lineTo(x, y);
210
- });
211
- if (pts.length > 0) {
212
- const lastX = axes.toX(parseValue(pts[pts.length - 1][props.xField]));
213
- const firstX = axes.toX(parseValue(pts[0][props.xField]));
214
- const baselineY = axes.plotTop + axes.plotHeight.value;
215
- c.lineTo(lastX, baselineY);
216
- c.lineTo(firstX, baselineY);
322
+ if (props.stacked) {
323
+ const topPoints = [];
324
+ const bottomPoints = [];
325
+ for (const pt of pts) {
326
+ const x = axes.toX(pt.xVal);
327
+ const base = currentBaseline.get(pt.xVal) || 0;
328
+ const top = base + pt.yVal;
329
+ const yTop = toStackedY(top);
330
+ const yBottom = toStackedY(base);
331
+ topPoints.push({ x, y: yTop });
332
+ bottomPoints.push({ x, y: yBottom });
333
+ currentBaseline.set(pt.xVal, top);
334
+ }
335
+ topPoints.forEach((pt, i) => {
336
+ if (i === 0) c.moveTo(pt.x, pt.y);
337
+ else c.lineTo(pt.x, pt.y);
338
+ });
339
+ for (let i = bottomPoints.length - 1; i >= 0; i--) {
340
+ c.lineTo(bottomPoints[i].x, bottomPoints[i].y);
341
+ }
217
342
  c.closePath();
343
+ c.fillStyle = color + "44";
344
+ c.fill();
345
+ c.beginPath();
346
+ topPoints.forEach((pt, i) => {
347
+ if (i === 0) c.moveTo(pt.x, pt.y);
348
+ else c.lineTo(pt.x, pt.y);
349
+ });
350
+ c.strokeStyle = color;
351
+ c.lineWidth = 2;
352
+ c.stroke();
353
+ } else {
354
+ pts.forEach((pt, i) => {
355
+ const x = axes.toX(pt.xVal);
356
+ const y = toStackedY(pt.yVal);
357
+ if (i === 0) c.moveTo(x, y);
358
+ else c.lineTo(x, y);
359
+ });
360
+ if (pts.length > 0) {
361
+ const lastX = axes.toX(pts[pts.length - 1].xVal);
362
+ const firstX = axes.toX(pts[0].xVal);
363
+ const baselineY = axes.plotTop + axes.plotHeight.value;
364
+ c.lineTo(lastX, baselineY);
365
+ c.lineTo(firstX, baselineY);
366
+ c.closePath();
367
+ }
368
+ c.fillStyle = color + "33";
369
+ c.fill();
370
+ c.beginPath();
371
+ pts.forEach((pt, i) => {
372
+ const x = axes.toX(pt.xVal);
373
+ const y = toStackedY(pt.yVal);
374
+ if (i === 0) c.moveTo(x, y);
375
+ else c.lineTo(x, y);
376
+ });
377
+ c.strokeStyle = color;
378
+ c.lineWidth = 2;
379
+ c.lineJoin = "round";
380
+ c.stroke();
218
381
  }
219
- c.fillStyle = color + "33";
220
- c.fill();
221
- c.beginPath();
222
- pts.forEach((item, i) => {
223
- const x = axes.toX(parseValue(item[props.xField]));
224
- const y = axes.toY(parseValue(item[props.yField]));
225
- if (i === 0) c.moveTo(x, y);
226
- else c.lineTo(x, y);
227
- });
228
- c.strokeStyle = color;
229
- c.lineWidth = 2;
230
- c.lineJoin = "round";
231
- c.stroke();
232
382
  c.restore();
233
383
  seriesIdx++;
234
384
  }
@@ -256,28 +406,34 @@ const drawLegend = (c) => {
256
406
  });
257
407
  c.restore();
258
408
  };
259
- watch([items, scale, panOffset, hoveredIndex, progress], draw, { deep: true });
409
+ watch(progress, draw);
410
+ watch([scale, panOffset, hoveredIndex, resizeTick, width, height, () => props.stacked], draw);
260
411
  watch(items, () => {
261
412
  if (props.animated) animate();
262
- }, { deep: true });
413
+ else draw();
414
+ });
263
415
  watch(scale, (v) => emit("zoom-change", v));
264
416
  watch(panOffset, (v) => emit("pan-change", v), { deep: true });
265
417
  const hoveredPoints = computed(() => {
266
418
  if (hoveredIndex.value === -1) return [];
419
+ if (hoveredIndex.value >= items.value.length) {
420
+ setHovered(-1);
421
+ return [];
422
+ }
267
423
  const item = items.value[hoveredIndex.value];
268
- const xVal = parseValue(item[props.xField]);
424
+ const xVal = resolveX(item);
269
425
  const xPx = axes.toX(xVal);
270
426
  const pts = [];
271
427
  let seriesIdx = 0;
272
- for (const [key, seriesItems] of seriesMap.value) {
273
- const seriesItem = seriesItems.find((it) => parseValue(it[props.xField]) === xVal);
274
- if (seriesItem) {
428
+ for (const [, seriesList] of sortedSeriesMap.value) {
429
+ const found = seriesList.find((pt) => pt.xVal === xVal);
430
+ if (found) {
275
431
  pts.push({
276
432
  x: xPx,
277
- y: axes.toY(parseValue(seriesItem[props.yField])),
433
+ y: toStackedY(found.yVal),
278
434
  color: props.colors[seriesIdx % props.colors.length],
279
- item: seriesItem,
280
- index: items.value.indexOf(seriesItem)
435
+ item: found.item,
436
+ index: found.index
281
437
  });
282
438
  }
283
439
  seriesIdx++;
@@ -293,7 +449,7 @@ onMounted(async () => {
293
449
  }
294
450
  });
295
451
  const onMouseMove = (e) => {
296
- viewport.onMouseMove(e);
452
+ onViewportMouseMove(e);
297
453
  if (isDragging.value) {
298
454
  setHovered(-1);
299
455
  hideTooltip();
@@ -303,11 +459,24 @@ const onMouseMove = (e) => {
303
459
  if (!rect) return;
304
460
  const mouseX = e.clientX - rect.left;
305
461
  const mouseY = e.clientY - rect.top;
306
- const nearest = findNearestX(items.value, mouseX, props.xField, axes);
462
+ if (mouseX < axes.plotLeft || mouseX > axes.plotLeft + axes.plotWidth.value || mouseY < axes.plotTop || mouseY > axes.plotTop + axes.plotHeight.value) {
463
+ setHovered(-1);
464
+ hideTooltip();
465
+ return;
466
+ }
467
+ const nearest = findNearestX(mouseX, items.value, axes.toX, (r, i) => resolveX(r, i));
307
468
  if (nearest) {
308
469
  setHovered(nearest.index);
309
- showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
310
470
  emit("hover-point", nearest.item, nearest.index);
471
+ const xPx = axes.toX(resolveX(nearest.item, nearest.index));
472
+ const yPx = toStackedY(cleanNumber(nearest.item[props.yField]));
473
+ showTooltip(
474
+ xPx,
475
+ yPx - 10,
476
+ `${props.xField}: ${nearest.item[props.xField]} | ${props.yField}: ${nearest.item[props.yField]}`,
477
+ nearest.item,
478
+ nearest.index
479
+ );
311
480
  } else {
312
481
  setHovered(-1);
313
482
  hideTooltip();
@@ -323,7 +492,7 @@ defineExpose({
323
492
  zoomOut: viewport.zoomOut,
324
493
  resetZoom: viewport.resetZoom,
325
494
  exportPng: () => canvasRef.value && exportPng(canvasRef.value, "area-chart.png"),
326
- exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg")
495
+ exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg", canvasRef.value)
327
496
  });
328
497
  </script>
329
498
 
@@ -331,6 +500,13 @@ defineExpose({
331
500
  .k-chart-wrapper {
332
501
  position: relative;
333
502
  user-select: none;
503
+ cursor: default;
504
+ }
505
+ .k-chart-wrapper.k-chart--pannable {
506
+ cursor: grab;
507
+ }
508
+ .k-chart-wrapper.k-chart--grabbing {
509
+ cursor: grabbing;
334
510
  }
335
511
  .k-chart-wrapper .k-chart-controls {
336
512
  position: absolute;
@@ -340,9 +516,6 @@ defineExpose({
340
516
  display: flex;
341
517
  gap: 4px;
342
518
  }
343
- .k-chart-wrapper .k-chart-btn {
344
- display: none;
345
- }
346
519
  .k-chart-wrapper .k-chart-inner {
347
520
  width: 100%;
348
521
  height: 400px;