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