@katlux/block-charts 0.1.1-alpha.88 → 0.1.1-alpha.90

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.88",
7
+ "version": "0.1.1-alpha.90",
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"
@@ -40,7 +39,6 @@
40
39
  :cy="pt.y"
41
40
  :r="10"
42
41
  fill="transparent"
43
- style="cursor: pointer"
44
42
  @click="emit('click-point', pt.item, pt.index)"
45
43
  )
46
44
  circle(
@@ -56,7 +54,7 @@
56
54
  )
57
55
  .k-chart-tooltip(
58
56
  v-if="tooltipState.visible && !isLoading"
59
- :style="{ left: tooltipState.x + 'px', top: tooltipState.y + 'px' }"
57
+ :style="tooltipStyle"
60
58
  )
61
59
  slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
62
60
  span {{ tooltipState.content }}
@@ -72,6 +70,7 @@ import { useChartViewport } from "../../composables/useChartViewport";
72
70
  import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
73
71
  import { useChartExport } from "../../composables/useChartExport";
74
72
  import { useChartHitTest } from "../../composables/useChartHitTest";
73
+ import { cleanNumber, safeMax } from "../../composables/chartUtils";
75
74
  const props = defineProps({
76
75
  dataProvider: { type: null, required: false },
77
76
  xField: { type: String, required: false, default: "x" },
@@ -91,7 +90,7 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
91
90
  const containerRef = ref(null);
92
91
  const canvasRef = ref(null);
93
92
  const svgRef = ref(null);
94
- const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
93
+ const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
95
94
  const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
96
95
  const dataRef = computed(() => props.dataProvider);
97
96
  const { items } = useChartData(dataRef);
@@ -100,6 +99,10 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
100
99
  const isLoading = computed(() => {
101
100
  return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
102
101
  });
102
+ const tooltipStyle = computed(() => ({
103
+ left: `${tooltipState.value.x}px`,
104
+ top: `${tooltipState.value.y}px`
105
+ }));
103
106
  const maxZoomRef = computed(() => props.maxZoom);
104
107
  const plotWidthRef = computed(() => Math.max(0, width.value - 75));
105
108
  const plotLeftRef = computed(() => 55);
@@ -122,19 +125,30 @@ const {
122
125
  const xFieldRef = computed(() => props.xField);
123
126
  const yFieldRef = computed(() => props.yField);
124
127
  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";
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";
131
136
  return "category";
132
137
  }
133
- return "numeric";
138
+ return "linear";
134
139
  });
135
140
  const categoryList = computed(() => {
136
141
  if (xAxisType.value !== "category") return [];
137
- return [...new Set(items.value.map((r) => String(r[props.xField])))];
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;
138
152
  });
139
153
  const categoryIndexMap = computed(() => {
140
154
  const map = {};
@@ -158,6 +172,34 @@ const seriesMap = computed(() => {
158
172
  }
159
173
  return map;
160
174
  });
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
+ });
161
203
  const axes = useChartAxes({
162
204
  items,
163
205
  xField: xFieldRef,
@@ -169,11 +211,21 @@ const axes = useChartAxes({
169
211
  padding: { top: 20, right: 20, bottom: 40, left: 55 },
170
212
  xValueGetter: (r, i) => resolveX(r, i)
171
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
+ };
172
224
  const draw = () => {
173
225
  if (!ctx.value) return;
174
226
  clear(props.backgroundColor);
175
227
  const c = ctx.value;
176
- const p = easeOut(progress.value);
228
+ const p = props.animated ? easeOut(progress.value) : 1;
177
229
  c.save();
178
230
  c.strokeStyle = "rgba(100,100,120,0.1)";
179
231
  c.lineWidth = 1;
@@ -197,8 +249,8 @@ const draw = () => {
197
249
  c.font = "11px system-ui, sans-serif";
198
250
  c.textAlign = "right";
199
251
  for (let i = 0; i <= 5; i++) {
200
- const val = axes.yMin.value + (axes.yMax.value - axes.yMin.value) * (i / 5);
201
- 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);
202
254
  }
203
255
  c.textAlign = "center";
204
256
  if (xAxisType.value === "category") {
@@ -231,6 +283,11 @@ const draw = () => {
231
283
  c.fillText(label, x, axes.plotTop + axes.plotHeight.value + 16);
232
284
  }
233
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();
234
291
  c.beginPath();
235
292
  c.moveTo(axes.plotLeft, axes.plotTop);
236
293
  c.lineTo(axes.plotLeft, axes.plotTop + axes.plotHeight.value);
@@ -254,41 +311,74 @@ const draw = () => {
254
311
  c.beginPath();
255
312
  c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
256
313
  c.clip();
314
+ const currentBaseline = /* @__PURE__ */ new Map();
257
315
  let seriesIdx = 0;
258
- for (const [, seriesItems] of seriesMap.value) {
316
+ for (const [, seriesItems] of sortedSeriesMap.value) {
259
317
  const color = props.colors[seriesIdx % props.colors.length];
260
- const sorted = [...seriesItems].sort((a, b) => resolveX(a) - resolveX(b));
261
- const drawCount = Math.max(1, Math.floor(sorted.length * p));
262
- const pts = sorted.slice(0, drawCount);
318
+ const drawCount = Math.max(1, Math.floor(seriesItems.length * p));
319
+ const pts = seriesItems.slice(0, drawCount);
263
320
  c.save();
264
321
  c.beginPath();
265
- pts.forEach((item, i) => {
266
- const x = axes.toX(resolveX(item, i));
267
- const y = axes.toY(parseValue(item[props.yField]));
268
- if (i === 0) c.moveTo(x, y);
269
- else c.lineTo(x, y);
270
- });
271
- if (pts.length > 0) {
272
- const lastX = axes.toX(resolveX(pts[pts.length - 1], pts.length - 1));
273
- const firstX = axes.toX(resolveX(pts[0], 0));
274
- const baselineY = axes.plotTop + axes.plotHeight.value;
275
- c.lineTo(lastX, baselineY);
276
- 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
+ }
277
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();
278
381
  }
279
- c.fillStyle = color + "33";
280
- c.fill();
281
- c.beginPath();
282
- pts.forEach((item, i) => {
283
- const x = axes.toX(resolveX(item, i));
284
- const y = axes.toY(parseValue(item[props.yField]));
285
- if (i === 0) c.moveTo(x, y);
286
- else c.lineTo(x, y);
287
- });
288
- c.strokeStyle = color;
289
- c.lineWidth = 2;
290
- c.lineJoin = "round";
291
- c.stroke();
292
382
  c.restore();
293
383
  seriesIdx++;
294
384
  }
@@ -316,28 +406,34 @@ const drawLegend = (c) => {
316
406
  });
317
407
  c.restore();
318
408
  };
319
- watch([items, scale, panOffset, hoveredIndex, progress], draw, { deep: true });
409
+ watch(progress, draw);
410
+ watch([scale, panOffset, hoveredIndex, resizeTick, width, height, () => props.stacked], draw);
320
411
  watch(items, () => {
321
412
  if (props.animated) animate();
322
- }, { deep: true });
413
+ else draw();
414
+ });
323
415
  watch(scale, (v) => emit("zoom-change", v));
324
416
  watch(panOffset, (v) => emit("pan-change", v), { deep: true });
325
417
  const hoveredPoints = computed(() => {
326
418
  if (hoveredIndex.value === -1) return [];
419
+ if (hoveredIndex.value >= items.value.length) {
420
+ setHovered(-1);
421
+ return [];
422
+ }
327
423
  const item = items.value[hoveredIndex.value];
328
424
  const xVal = resolveX(item);
329
425
  const xPx = axes.toX(xVal);
330
426
  const pts = [];
331
427
  let seriesIdx = 0;
332
- for (const [, seriesItems] of seriesMap.value) {
333
- const seriesItem = seriesItems.find((it) => resolveX(it) === xVal);
334
- if (seriesItem) {
428
+ for (const [, seriesList] of sortedSeriesMap.value) {
429
+ const found = seriesList.find((pt) => pt.xVal === xVal);
430
+ if (found) {
335
431
  pts.push({
336
432
  x: xPx,
337
- y: axes.toY(parseValue(seriesItem[props.yField])),
433
+ y: toStackedY(found.yVal),
338
434
  color: props.colors[seriesIdx % props.colors.length],
339
- item: seriesItem,
340
- index: items.value.indexOf(seriesItem)
435
+ item: found.item,
436
+ index: found.index
341
437
  });
342
438
  }
343
439
  seriesIdx++;
@@ -353,7 +449,7 @@ onMounted(async () => {
353
449
  }
354
450
  });
355
451
  const onMouseMove = (e) => {
356
- viewport.onMouseMove(e);
452
+ onViewportMouseMove(e);
357
453
  if (isDragging.value) {
358
454
  setHovered(-1);
359
455
  hideTooltip();
@@ -363,11 +459,24 @@ const onMouseMove = (e) => {
363
459
  if (!rect) return;
364
460
  const mouseX = e.clientX - rect.left;
365
461
  const mouseY = e.clientY - rect.top;
366
- const nearest = findNearestX(items.value, mouseX, props.xField, axes, (r, i) => resolveX(r, i));
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));
367
468
  if (nearest) {
368
469
  setHovered(nearest.index);
369
- showTooltip(mouseX + 10, mouseY - 20, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
370
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
+ );
371
480
  } else {
372
481
  setHovered(-1);
373
482
  hideTooltip();
@@ -383,7 +492,7 @@ defineExpose({
383
492
  zoomOut: viewport.zoomOut,
384
493
  resetZoom: viewport.resetZoom,
385
494
  exportPng: () => canvasRef.value && exportPng(canvasRef.value, "area-chart.png"),
386
- exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg")
495
+ exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "area-chart.svg", canvasRef.value)
387
496
  });
388
497
  </script>
389
498
 
@@ -391,6 +500,13 @@ defineExpose({
391
500
  .k-chart-wrapper {
392
501
  position: relative;
393
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;
394
510
  }
395
511
  .k-chart-wrapper .k-chart-controls {
396
512
  position: absolute;
@@ -400,9 +516,6 @@ defineExpose({
400
516
  display: flex;
401
517
  gap: 4px;
402
518
  }
403
- .k-chart-wrapper .k-chart-btn {
404
- display: none;
405
- }
406
519
  .k-chart-wrapper .k-chart-inner {
407
520
  width: 100%;
408
521
  height: 400px;