@katlux/block-charts 0.1.0-beta.0 → 0.1.0-beta.2

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.
Files changed (55) hide show
  1. package/dist/module.d.mts +6 -0
  2. package/dist/module.json +12 -0
  3. package/dist/module.mjs +23 -0
  4. package/dist/runtime/components/KAreaChart/KAreaChart.d.vue.ts +53 -0
  5. package/dist/runtime/components/KAreaChart/KAreaChart.vue +382 -0
  6. package/dist/runtime/components/KAreaChart/KAreaChart.vue.d.ts +53 -0
  7. package/dist/runtime/components/KBarChart/KBarChart.d.vue.ts +55 -0
  8. package/dist/runtime/components/KBarChart/KBarChart.vue +398 -0
  9. package/dist/runtime/components/KBarChart/KBarChart.vue.d.ts +55 -0
  10. package/dist/runtime/components/KHeatMap/KHeatMap.d.vue.ts +36 -0
  11. package/dist/runtime/components/KHeatMap/KHeatMap.vue +263 -0
  12. package/dist/runtime/components/KHeatMap/KHeatMap.vue.d.ts +36 -0
  13. package/dist/runtime/components/KLineChart/KLineChart.d.vue.ts +51 -0
  14. package/dist/runtime/components/KLineChart/KLineChart.vue +407 -0
  15. package/dist/runtime/components/KLineChart/KLineChart.vue.d.ts +51 -0
  16. package/dist/runtime/components/KPieChart/KPieChart.d.vue.ts +32 -0
  17. package/dist/runtime/components/KPieChart/KPieChart.vue +273 -0
  18. package/dist/runtime/components/KPieChart/KPieChart.vue.d.ts +32 -0
  19. package/dist/runtime/components/KScatterChart/KScatterChart.d.vue.ts +55 -0
  20. package/dist/runtime/components/KScatterChart/KScatterChart.vue +356 -0
  21. package/dist/runtime/components/KScatterChart/KScatterChart.vue.d.ts +55 -0
  22. package/dist/runtime/composables/useChartAnimation.d.ts +9 -0
  23. package/dist/runtime/composables/useChartAnimation.js +32 -0
  24. package/dist/runtime/composables/useChartAxes.d.ts +40 -0
  25. package/dist/runtime/composables/useChartAxes.js +58 -0
  26. package/dist/runtime/composables/useChartCanvas.d.ts +11 -0
  27. package/dist/runtime/composables/useChartCanvas.js +47 -0
  28. package/dist/runtime/composables/useChartData.d.ts +14 -0
  29. package/dist/runtime/composables/useChartData.js +45 -0
  30. package/dist/runtime/composables/useChartExport.d.ts +5 -0
  31. package/dist/runtime/composables/useChartExport.js +32 -0
  32. package/dist/runtime/composables/useChartHitTest.d.ts +11 -0
  33. package/dist/runtime/composables/useChartHitTest.js +39 -0
  34. package/dist/runtime/composables/useChartSvg.d.ts +17 -0
  35. package/dist/runtime/composables/useChartSvg.js +22 -0
  36. package/dist/runtime/composables/useChartViewport.d.ts +25 -0
  37. package/dist/runtime/composables/useChartViewport.js +92 -0
  38. package/dist/types.d.mts +3 -0
  39. package/package.json +7 -3
  40. package/build.config.ts +0 -4
  41. package/src/module.ts +0 -25
  42. package/src/runtime/components/KAreaChart/KAreaChart.vue +0 -410
  43. package/src/runtime/components/KBarChart/KBarChart.vue +0 -427
  44. package/src/runtime/components/KHeatMap/KHeatMap.vue +0 -301
  45. package/src/runtime/components/KLineChart/KLineChart.vue +0 -493
  46. package/src/runtime/components/KPieChart/KPieChart.vue +0 -307
  47. package/src/runtime/components/KScatterChart/KScatterChart.vue +0 -375
  48. package/src/runtime/composables/useChartAnimation.ts +0 -45
  49. package/src/runtime/composables/useChartAxes.ts +0 -105
  50. package/src/runtime/composables/useChartCanvas.ts +0 -67
  51. package/src/runtime/composables/useChartData.ts +0 -79
  52. package/src/runtime/composables/useChartExport.ts +0 -40
  53. package/src/runtime/composables/useChartHitTest.ts +0 -71
  54. package/src/runtime/composables/useChartSvg.ts +0 -45
  55. package/src/runtime/composables/useChartViewport.ts +0 -140
@@ -0,0 +1,356 @@
1
+ <template lang="pug">
2
+ .k-chart-wrapper(
3
+ :class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
4
+ :style="{ cursor: isDragging ? 'grabbing' : canPan ? 'grab' : 'default' }"
5
+ @wheel.prevent="onWheel"
6
+ @mousedown="onMouseDown"
7
+ @mousemove="onMouseMove"
8
+ @mouseup="onMouseUp"
9
+ @mouseleave="onMouseLeave"
10
+ )
11
+ .k-chart-controls(v-if="zoomable")
12
+ KButton(size="small" @click="zoomIn" title="Zoom In") +
13
+ KButton(size="small" @click="zoomOut" title="Zoom Out") −
14
+ KButton(size="small" @click="resetZoom" title="Reset") ↺
15
+ .k-chart-inner(ref="containerRef")
16
+ KLoader(:loading="isLoading" overlay)
17
+ canvas(ref="canvasRef" v-show="!isLoading")
18
+ svg.k-chart-svg(v-show="!isLoading" ref="svgRef" :width="width" :height="height")
19
+ defs
20
+ clipPath(id="plot-clip")
21
+ rect(
22
+ :x="axes.plotLeft"
23
+ :y="axes.plotTop"
24
+ :width="axes.plotWidth.value"
25
+ :height="axes.plotHeight.value"
26
+ )
27
+ g.k-chart-hits
28
+ // No circles here! Distance-based hit testing now.
29
+
30
+ // Dynamic Interaction Layer
31
+ g.k-chart-interaction(v-if="hoveredIndex !== -1 && !isLoading" clip-path="url(#plot-clip)")
32
+ circle.k-chart-hit-proxy(
33
+ :cx="hoveredPoint.x"
34
+ :cy="hoveredPoint.y"
35
+ :r="20"
36
+ fill="transparent"
37
+ style="cursor: pointer"
38
+ @click="emit('click-point', hoveredPoint.item, hoveredPoint.index)"
39
+ )
40
+ circle(
41
+ :cx="hoveredPoint.x"
42
+ :cy="hoveredPoint.y"
43
+ :r="hoveredPoint.r + 3"
44
+ fill="transparent"
45
+ :stroke="hoveredPoint.color"
46
+ stroke-width="2"
47
+ pointer-events="none"
48
+ )
49
+ .k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="{ left: tooltipState.x + 'px', top: tooltipState.y + 'px' }")
50
+ slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
51
+ span {{ tooltipState.content }}
52
+ </template>
53
+
54
+ <script setup>
55
+ import { ref, watch, computed, onMounted, nextTick } from "vue";
56
+ import { useChartCanvas } from "../../composables/useChartCanvas";
57
+ import { useChartSvg } from "../../composables/useChartSvg";
58
+ import { useChartData } from "../../composables/useChartData";
59
+ import { useChartAxes } from "../../composables/useChartAxes";
60
+ import { useChartViewport } from "../../composables/useChartViewport";
61
+ import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
62
+ import { useChartExport } from "../../composables/useChartExport";
63
+ import { useChartHitTest } from "../../composables/useChartHitTest";
64
+ const props = defineProps({
65
+ dataProvider: { type: null, required: false },
66
+ xField: { type: String, required: false, default: "x" },
67
+ yField: { type: String, required: false, default: "y" },
68
+ seriesField: { type: String, required: false, default: "series" },
69
+ sizeField: { type: String, required: false, default: "size" },
70
+ labelField: { type: String, required: false, default: "label" },
71
+ animated: { type: Boolean, required: false, default: true },
72
+ zoomable: { type: Boolean, required: false, default: true },
73
+ maxZoom: { type: Number, required: false, default: 20 },
74
+ colors: { type: Array, required: false, default: () => ["#6366f1", "#22d3ee", "#f59e0b", "#10b981", "#f43f5e", "#a855f7", "#14b8a6", "#fb923c"] },
75
+ backgroundColor: { type: String, required: false, default: "#ffffff" },
76
+ showLegend: { type: Boolean, required: false, default: true },
77
+ xAxisTitle: { type: String, required: false, default: "" },
78
+ yAxisTitle: { type: String, required: false, default: "" }
79
+ });
80
+ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-change"]);
81
+ const containerRef = ref(null);
82
+ const canvasRef = ref(null);
83
+ const svgRef = ref(null);
84
+ const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
85
+ const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
86
+ const dataRef = computed(() => props.dataProvider);
87
+ const { items } = useChartData(dataRef);
88
+ const { progress, animate } = useChartAnimation();
89
+ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
90
+ const isLoading = computed(() => {
91
+ return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
92
+ });
93
+ const maxZoomRef = computed(() => props.maxZoom);
94
+ const plotWidthRef = computed(() => Math.max(0, width.value - 75));
95
+ const plotLeftRef = computed(() => 55);
96
+ const viewport = useChartViewport({ maxZoom: maxZoomRef, plotWidth: plotWidthRef, plotLeft: plotLeftRef });
97
+ const { findNearestEuclidean } = useChartHitTest();
98
+ const {
99
+ scale,
100
+ panOffset,
101
+ isDragging,
102
+ canPan,
103
+ zoomIn,
104
+ zoomOut,
105
+ resetZoom,
106
+ onWheel,
107
+ onMouseDown,
108
+ onMouseMove: onViewportMouseMove,
109
+ onMouseUp,
110
+ onMouseLeave: onViewportMouseLeave
111
+ } = viewport;
112
+ const xFieldRef = computed(() => props.xField);
113
+ 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 seriesMap = computed(() => {
117
+ const map = /* @__PURE__ */ new Map();
118
+ for (const item of items.value) {
119
+ const key = String(item[props.seriesField] ?? "default");
120
+ if (!map.has(key)) map.set(key, []);
121
+ map.get(key).push(item);
122
+ }
123
+ return map;
124
+ });
125
+ const hoveredPoint = computed(() => {
126
+ if (hoveredIndex.value === -1) return null;
127
+ const item = items.value[hoveredIndex.value];
128
+ const sizeRaw = Number(item[props.sizeField] ?? 1);
129
+ const r = 4 + sizeRaw / maxSize.value * 16;
130
+ const seriesKey = String(item[props.seriesField] ?? "default");
131
+ const seriesKeys = Array.from(seriesMap.value.keys());
132
+ const seriesIdx = seriesKeys.indexOf(seriesKey);
133
+ const color = props.colors[seriesIdx % props.colors.length];
134
+ return {
135
+ x: axes.toX(Number(item[props.xField])),
136
+ y: axes.toY(Number(item[props.yField])),
137
+ r,
138
+ color,
139
+ item,
140
+ index: hoveredIndex.value
141
+ };
142
+ });
143
+ const draw = () => {
144
+ if (!ctx.value) return;
145
+ clear(props.backgroundColor);
146
+ const c = ctx.value;
147
+ const p = easeOut(progress.value);
148
+ c.save();
149
+ c.strokeStyle = "rgba(100,100,120,0.1)";
150
+ c.lineWidth = 1;
151
+ for (let i = 0; i <= 6; i++) {
152
+ const y = axes.plotTop + axes.plotHeight.value / 6 * i;
153
+ c.beginPath();
154
+ c.moveTo(axes.plotLeft, y);
155
+ c.lineTo(axes.plotLeft + axes.plotWidth.value, y);
156
+ c.stroke();
157
+ }
158
+ for (let i = 0; i <= 6; i++) {
159
+ const x = axes.plotLeft + axes.plotWidth.value / 6 * i;
160
+ c.beginPath();
161
+ c.moveTo(x, axes.plotTop);
162
+ c.lineTo(x, axes.plotTop + axes.plotHeight.value);
163
+ c.stroke();
164
+ }
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();
174
+ c.fillStyle = "rgba(150,150,170,0.8)";
175
+ c.font = "11px system-ui, sans-serif";
176
+ c.textAlign = "right";
177
+ for (let i = 0; i <= 5; i++) {
178
+ const val = axes.yMin.value + (axes.yMax.value - axes.yMin.value) * (i / 5);
179
+ c.fillText(val.toFixed(1), axes.plotLeft - 8, axes.toY(val) + 4);
180
+ }
181
+ c.textAlign = "center";
182
+ for (let i = 0; i <= 5; i++) {
183
+ const val = axes.xMin.value + (axes.xMax.value - axes.xMin.value) * (i / 5);
184
+ c.fillText(val.toFixed(1), axes.toX(val), axes.plotTop + axes.plotHeight.value + 16);
185
+ }
186
+ c.fillStyle = "rgba(120,120,140,0.8)";
187
+ if (props.xAxisTitle) {
188
+ c.fillText(props.xAxisTitle, axes.plotLeft + axes.plotWidth.value / 2, axes.plotTop + axes.plotHeight.value + 34);
189
+ }
190
+ if (props.yAxisTitle) {
191
+ c.save();
192
+ c.translate(axes.plotLeft - 40, axes.plotTop + axes.plotHeight.value / 2);
193
+ c.rotate(-Math.PI / 2);
194
+ c.textAlign = "center";
195
+ c.fillText(props.yAxisTitle, 0, 0);
196
+ c.restore();
197
+ }
198
+ c.restore();
199
+ c.save();
200
+ c.beginPath();
201
+ c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
202
+ c.clip();
203
+ let seriesIdx = 0;
204
+ for (const [, seriesItems] of seriesMap.value) {
205
+ 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;
211
+ const isHovered = items.value[hoveredIndex.value] === item;
212
+ const alpha = p;
213
+ c.save();
214
+ c.globalAlpha = alpha * (isHovered ? 1 : 0.75);
215
+ c.beginPath();
216
+ c.arc(x, y, isHovered ? r + 3 : r, 0, Math.PI * 2);
217
+ c.fillStyle = color;
218
+ c.fill();
219
+ c.strokeStyle = "#fff";
220
+ c.lineWidth = 1.5;
221
+ c.stroke();
222
+ c.restore();
223
+ });
224
+ seriesIdx++;
225
+ }
226
+ c.restore();
227
+ if (props.showLegend) {
228
+ drawLegend(c);
229
+ }
230
+ };
231
+ const drawLegend = (c) => {
232
+ c.save();
233
+ c.font = "11px system-ui, sans-serif";
234
+ c.textAlign = "left";
235
+ c.textBaseline = "middle";
236
+ let xOffset = axes.plotLeft;
237
+ const yPos = axes.plotTop + axes.plotHeight.value + 28;
238
+ let seriesIdx = 0;
239
+ for (const [key] of seriesMap.value) {
240
+ const color = props.colors[seriesIdx % props.colors.length];
241
+ c.fillStyle = color;
242
+ c.beginPath();
243
+ c.arc(xOffset + 5, yPos, 4, 0, Math.PI * 2);
244
+ c.fill();
245
+ c.fillStyle = "rgba(150,150,170,0.9)";
246
+ const label = key === "default" ? "Series" : key;
247
+ c.fillText(label, xOffset + 15, yPos);
248
+ const metrics = c.measureText(label);
249
+ xOffset += metrics.width + 35;
250
+ seriesIdx++;
251
+ }
252
+ c.restore();
253
+ };
254
+ watch([items, scale, panOffset, hoveredIndex, progress], draw, { deep: true });
255
+ watch(items, () => {
256
+ if (props.animated) animate();
257
+ }, { deep: true });
258
+ watch(scale, (v) => emit("zoom-change", v));
259
+ watch(panOffset, (v) => emit("pan-change", v), { deep: true });
260
+ onMounted(async () => {
261
+ await nextTick();
262
+ if (containerRef.value && canvasRef.value) {
263
+ containerRef.value.style.position = "relative";
264
+ setupCanvas(canvasRef.value);
265
+ if (props.animated) animate();
266
+ else draw();
267
+ }
268
+ });
269
+ const onMouseMove = (e) => {
270
+ onViewportMouseMove(e);
271
+ if (isDragging.value) {
272
+ setHovered(-1);
273
+ hideTooltip();
274
+ return;
275
+ }
276
+ const rect = containerRef.value?.getBoundingClientRect();
277
+ if (!rect) return;
278
+ const mx = e.clientX - rect.left;
279
+ const my = e.clientY - rect.top;
280
+ const nearest = findNearestEuclidean(items.value, mx, my, axes, props.xField, props.yField, 20);
281
+ if (nearest) {
282
+ setHovered(nearest.index);
283
+ showTooltip(mx + 10, my - 10, `${nearest.item[props.xField]}: ${nearest.item[props.yField]}`, nearest.item, nearest.index);
284
+ emit("hover-point", nearest.item, nearest.index);
285
+ } else {
286
+ setHovered(-1);
287
+ hideTooltip();
288
+ }
289
+ };
290
+ const onMouseLeave = (e) => {
291
+ onViewportMouseLeave(e);
292
+ setHovered(-1);
293
+ hideTooltip();
294
+ };
295
+ defineExpose({
296
+ zoomIn: viewport.zoomIn,
297
+ zoomOut: viewport.zoomOut,
298
+ resetZoom: viewport.resetZoom,
299
+ exportPng: () => canvasRef.value && exportPng(canvasRef.value, "scatter-chart.png"),
300
+ exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "scatter-chart.svg")
301
+ });
302
+ </script>
303
+
304
+ <style scoped>
305
+ .k-chart-wrapper {
306
+ position: relative;
307
+ user-select: none;
308
+ }
309
+ .k-chart-wrapper .k-chart-controls {
310
+ position: absolute;
311
+ top: 8px;
312
+ right: 8px;
313
+ z-index: 10;
314
+ display: flex;
315
+ gap: 4px;
316
+ }
317
+ .k-chart-wrapper .k-chart-btn {
318
+ display: none;
319
+ }
320
+ .k-chart-wrapper .k-chart-inner {
321
+ width: 100%;
322
+ height: 400px;
323
+ position: relative;
324
+ }
325
+ .k-chart-wrapper .k-chart-inner canvas, .k-chart-wrapper .k-chart-inner .k-chart-svg {
326
+ position: absolute;
327
+ top: 0;
328
+ left: 0;
329
+ width: 100%;
330
+ height: 100%;
331
+ display: block;
332
+ }
333
+ .k-chart-wrapper .k-chart-inner .k-chart-svg {
334
+ overflow: visible;
335
+ }
336
+ .k-chart-wrapper .k-chart-svg {
337
+ pointer-events: none;
338
+ }
339
+ .k-chart-wrapper .k-chart-svg .k-chart-hit-proxy {
340
+ pointer-events: all;
341
+ cursor: pointer;
342
+ }
343
+ .k-chart-wrapper .k-chart-tooltip {
344
+ position: absolute;
345
+ background: var(--bg-color-elevated, #1e1e2e);
346
+ color: var(--text-color-primary, #cdd6f4);
347
+ border: 1px solid var(--border-color-medium, #45475a);
348
+ border-radius: 8px;
349
+ padding: 6px 10px;
350
+ font-size: 12px;
351
+ pointer-events: none;
352
+ z-index: 20;
353
+ white-space: nowrap;
354
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
355
+ }
356
+ </style>
@@ -0,0 +1,55 @@
1
+ type __VLS_Props = {
2
+ dataProvider?: any;
3
+ xField?: string;
4
+ yField?: string;
5
+ seriesField?: string;
6
+ sizeField?: string;
7
+ labelField?: string;
8
+ animated?: boolean;
9
+ zoomable?: boolean;
10
+ maxZoom?: number;
11
+ colors?: string[];
12
+ backgroundColor?: string;
13
+ showLegend?: boolean;
14
+ xAxisTitle?: string;
15
+ yAxisTitle?: string;
16
+ };
17
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
18
+ zoomIn: () => void;
19
+ zoomOut: () => void;
20
+ resetZoom: () => void;
21
+ exportPng: () => void | null;
22
+ exportSvg: () => void | null;
23
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
24
+ "click-point": (item: any, index: number) => any;
25
+ "hover-point": (item: any, index: number) => any;
26
+ "zoom-change": (scale: number) => any;
27
+ "pan-change": (offset: {
28
+ x: number;
29
+ y: number;
30
+ }) => any;
31
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
32
+ "onClick-point"?: ((item: any, index: number) => any) | undefined;
33
+ "onHover-point"?: ((item: any, index: number) => any) | undefined;
34
+ "onZoom-change"?: ((scale: number) => any) | undefined;
35
+ "onPan-change"?: ((offset: {
36
+ x: number;
37
+ y: number;
38
+ }) => any) | undefined;
39
+ }>, {
40
+ xField: string;
41
+ yField: string;
42
+ seriesField: string;
43
+ animated: boolean;
44
+ zoomable: boolean;
45
+ maxZoom: number;
46
+ colors: string[];
47
+ backgroundColor: string;
48
+ showLegend: boolean;
49
+ xAxisTitle: string;
50
+ yAxisTitle: string;
51
+ labelField: string;
52
+ sizeField: string;
53
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
54
+ declare const _default: typeof __VLS_export;
55
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { type Ref } from 'vue';
2
+ export interface IChartAnimationContext {
3
+ progress: Ref<number>;
4
+ animate: () => void;
5
+ stop: () => void;
6
+ }
7
+ export declare function useChartAnimation(duration?: number): IChartAnimationContext;
8
+ /** Easing function: ease-out cubic */
9
+ export declare function easeOut(t: number): number;
@@ -0,0 +1,32 @@
1
+ import { ref } from "vue";
2
+ export function useChartAnimation(duration = 600) {
3
+ const progress = ref(0);
4
+ let rafId = null;
5
+ let startTime = null;
6
+ const animate = () => {
7
+ stop();
8
+ progress.value = 0;
9
+ startTime = null;
10
+ const tick = (now) => {
11
+ if (startTime === null) startTime = now;
12
+ const elapsed = now - startTime;
13
+ progress.value = Math.min(elapsed / duration, 1);
14
+ if (progress.value < 1) {
15
+ rafId = requestAnimationFrame(tick);
16
+ } else {
17
+ rafId = null;
18
+ }
19
+ };
20
+ rafId = requestAnimationFrame(tick);
21
+ };
22
+ const stop = () => {
23
+ if (rafId !== null) {
24
+ cancelAnimationFrame(rafId);
25
+ rafId = null;
26
+ }
27
+ };
28
+ return { progress, animate, stop };
29
+ }
30
+ export function easeOut(t) {
31
+ return 1 - Math.pow(1 - t, 3);
32
+ }
@@ -0,0 +1,40 @@
1
+ import { type Ref, type ComputedRef } from 'vue';
2
+ export type TScaleMode = 'linear' | 'logarithmic' | 'time';
3
+ export interface IChartAxesOptions {
4
+ items: Ref<any[]>;
5
+ xField: Ref<string>;
6
+ yField: Ref<string>;
7
+ width: Ref<number>;
8
+ height: Ref<number>;
9
+ scale: Ref<number>;
10
+ panOffset: Ref<{
11
+ x: number;
12
+ y: number;
13
+ }>;
14
+ padding?: {
15
+ top: number;
16
+ right: number;
17
+ bottom: number;
18
+ left: number;
19
+ };
20
+ scaleMode?: TScaleMode;
21
+ }
22
+ export interface IChartAxesContext {
23
+ xMin: ComputedRef<number>;
24
+ xMax: ComputedRef<number>;
25
+ yMin: ComputedRef<number>;
26
+ yMax: ComputedRef<number>;
27
+ plotLeft: number;
28
+ plotTop: number;
29
+ plotWidth: ComputedRef<number>;
30
+ plotHeight: ComputedRef<number>;
31
+ toX: (value: number) => number;
32
+ toY: (value: number) => number;
33
+ fromX: (px: number) => number;
34
+ }
35
+ /**
36
+ * Robustly parses a value into a number.
37
+ * Handles: numbers, numeric strings, and ISO date strings.
38
+ */
39
+ export declare function parseValue(val: any): number;
40
+ export declare function useChartAxes(opts: IChartAxesOptions): IChartAxesContext;
@@ -0,0 +1,58 @@
1
+ import { computed } from "vue";
2
+ export function parseValue(val) {
3
+ if (val === null || val === void 0) return 0;
4
+ const n = Number(val);
5
+ if (!isNaN(n)) return n;
6
+ if (typeof val === "string") {
7
+ const d = Date.parse(val);
8
+ if (!isNaN(d)) return d;
9
+ }
10
+ return 0;
11
+ }
12
+ export function useChartAxes(opts) {
13
+ const pad = opts.padding ?? { top: 20, right: 20, bottom: 40, left: 55 };
14
+ const plotLeft = pad.left;
15
+ const plotTop = pad.top;
16
+ const plotWidth = computed(() => Math.max(0, opts.width.value - pad.left - pad.right));
17
+ const plotHeight = computed(() => Math.max(0, opts.height.value - pad.top - pad.bottom));
18
+ const rawXMin = computed(() => {
19
+ const vals = opts.items.value.map((r) => parseValue(r[opts.xField.value]));
20
+ return vals.length ? Math.min(...vals) : 0;
21
+ });
22
+ const rawXMax = computed(() => {
23
+ const vals = opts.items.value.map((r) => parseValue(r[opts.xField.value]));
24
+ return vals.length ? Math.max(...vals) : 1;
25
+ });
26
+ const yMin = computed(() => {
27
+ const vals = opts.items.value.map((r) => parseValue(r[opts.yField.value]));
28
+ return vals.length ? Math.min(...vals) : 0;
29
+ });
30
+ const yMax = computed(() => {
31
+ const vals = opts.items.value.map((r) => parseValue(r[opts.yField.value]));
32
+ return vals.length ? Math.max(...vals) : 1;
33
+ });
34
+ const xRange = computed(() => rawXMax.value - rawXMin.value);
35
+ const visibleXRange = computed(() => xRange.value / opts.scale.value);
36
+ const panOffsetDomain = computed(() => {
37
+ const pxRange = plotWidth.value;
38
+ if (pxRange === 0) return 0;
39
+ return opts.panOffset.value.x / pxRange * xRange.value;
40
+ });
41
+ const xMin = computed(() => rawXMin.value + panOffsetDomain.value);
42
+ const xMax = computed(() => xMin.value + visibleXRange.value);
43
+ const toX = (value) => {
44
+ const range = xMax.value - xMin.value;
45
+ if (range === 0) return plotLeft;
46
+ return plotLeft + (value - xMin.value) / range * plotWidth.value;
47
+ };
48
+ const toY = (value) => {
49
+ const range = yMax.value - yMin.value;
50
+ if (range === 0) return plotTop + plotHeight.value;
51
+ return plotTop + plotHeight.value - (value - yMin.value) / range * plotHeight.value;
52
+ };
53
+ const fromX = (px) => {
54
+ const range = xMax.value - xMin.value;
55
+ return xMin.value + (px - plotLeft) / plotWidth.value * range;
56
+ };
57
+ return { xMin, xMax, yMin, yMax, plotLeft, plotTop, plotWidth, plotHeight, toX, toY, fromX };
58
+ }
@@ -0,0 +1,11 @@
1
+ import { type Ref } from 'vue';
2
+ export interface IChartCanvasContext {
3
+ canvas: Ref<HTMLCanvasElement | null>;
4
+ ctx: Ref<CanvasRenderingContext2D | null>;
5
+ width: Ref<number>;
6
+ height: Ref<number>;
7
+ dpr: Ref<number>;
8
+ clear: (color?: string) => void;
9
+ setupCanvas: (el: HTMLCanvasElement) => void;
10
+ }
11
+ export declare function useChartCanvas(): IChartCanvasContext;
@@ -0,0 +1,47 @@
1
+ import { ref, onUnmounted } from "vue";
2
+ export function useChartCanvas() {
3
+ const canvas = ref(null);
4
+ const ctx = ref(null);
5
+ const width = ref(0);
6
+ const height = ref(0);
7
+ const dpr = ref(window?.devicePixelRatio ?? 1);
8
+ let resizeObserver = null;
9
+ const applySize = (el, w, h) => {
10
+ const ratio = window.devicePixelRatio ?? 1;
11
+ dpr.value = ratio;
12
+ el.width = w * ratio;
13
+ el.height = h * ratio;
14
+ ctx.value?.scale(ratio, ratio);
15
+ width.value = w;
16
+ height.value = h;
17
+ };
18
+ const setupCanvas = (el) => {
19
+ canvas.value = el;
20
+ ctx.value = el.getContext("2d");
21
+ const rect = el.getBoundingClientRect();
22
+ applySize(el, rect.width, rect.height);
23
+ resizeObserver = new ResizeObserver((entries) => {
24
+ for (const entry of entries) {
25
+ const { width: w, height: h } = entry.contentRect;
26
+ if (canvas.value && ctx.value) {
27
+ ctx.value = canvas.value.getContext("2d");
28
+ applySize(canvas.value, w, h);
29
+ }
30
+ }
31
+ });
32
+ resizeObserver.observe(el);
33
+ };
34
+ const clear = (color) => {
35
+ if (ctx.value && canvas.value) {
36
+ ctx.value.clearRect(0, 0, width.value, height.value);
37
+ if (color && color !== "transparent") {
38
+ ctx.value.fillStyle = color;
39
+ ctx.value.fillRect(0, 0, width.value, height.value);
40
+ }
41
+ }
42
+ };
43
+ onUnmounted(() => {
44
+ resizeObserver?.disconnect();
45
+ });
46
+ return { canvas, ctx, width, height, dpr, clear, setupCanvas };
47
+ }
@@ -0,0 +1,14 @@
1
+ import { type Ref } from 'vue';
2
+ export interface IChartDataContext<T = any> {
3
+ items: Ref<T[]>;
4
+ loading: Ref<boolean>;
5
+ error: Ref<string | null>;
6
+ reload: () => Promise<void>;
7
+ }
8
+ /**
9
+ * Robust data fetcher for charts. Supports:
10
+ * 1. Plain arrays
11
+ * 2. Katlux DataProviders (with .pageData ref)
12
+ * 3. Legacy fetchable providers (with .fetch() method)
13
+ */
14
+ export declare function useChartData<T = any>(providerOrData: Ref<any>): IChartDataContext<T>;
@@ -0,0 +1,45 @@
1
+ import { ref, watch, onMounted, isRef } from "vue";
2
+ export function useChartData(providerOrData) {
3
+ const items = ref([]);
4
+ const loading = ref(false);
5
+ const error = ref(null);
6
+ const load = async () => {
7
+ const source = providerOrData.value;
8
+ if (!source) {
9
+ items.value = [];
10
+ return;
11
+ }
12
+ loading.value = true;
13
+ error.value = null;
14
+ try {
15
+ if (Array.isArray(source)) {
16
+ items.value = source;
17
+ return;
18
+ }
19
+ if (source.pageData) {
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;
31
+ }
32
+ } catch (e) {
33
+ error.value = e?.message ?? "Failed to load chart data";
34
+ } finally {
35
+ loading.value = false;
36
+ }
37
+ };
38
+ const reload = load;
39
+ onMounted(load);
40
+ watch(providerOrData, load);
41
+ watch(() => providerOrData.value?.pageData?.value, (newRows) => {
42
+ if (newRows) items.value = newRows;
43
+ }, { deep: true });
44
+ return { items, loading, error, reload };
45
+ }
@@ -0,0 +1,5 @@
1
+ export interface IChartExportContext {
2
+ exportPng: (canvas: HTMLCanvasElement, filename?: string) => void;
3
+ exportSvg: (svgEl: SVGSVGElement, filename?: string) => void;
4
+ }
5
+ export declare function useChartExport(): IChartExportContext;