@katlux/block-charts 0.1.0-beta.8 → 0.1.1-alpha.100

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 (34) hide show
  1. package/LICENSE +200 -0
  2. package/dist/module.d.mts +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +2 -0
  5. package/dist/runtime/components/KAreaChart/KAreaChart.d.vue.ts +2 -0
  6. package/dist/runtime/components/KAreaChart/KAreaChart.vue +271 -69
  7. package/dist/runtime/components/KAreaChart/KAreaChart.vue.d.ts +2 -0
  8. package/dist/runtime/components/KBarChart/KBarChart.d.vue.ts +2 -0
  9. package/dist/runtime/components/KBarChart/KBarChart.vue +323 -125
  10. package/dist/runtime/components/KBarChart/KBarChart.vue.d.ts +2 -0
  11. package/dist/runtime/components/KHeatMap/KHeatMap.vue +56 -21
  12. package/dist/runtime/components/KLineChart/KLineChart.d.vue.ts +2 -0
  13. package/dist/runtime/components/KLineChart/KLineChart.vue +179 -48
  14. package/dist/runtime/components/KLineChart/KLineChart.vue.d.ts +2 -0
  15. package/dist/runtime/components/KPieChart/KPieChart.vue +32 -29
  16. package/dist/runtime/components/KScatterChart/KScatterChart.d.vue.ts +3 -1
  17. package/dist/runtime/components/KScatterChart/KScatterChart.vue +161 -49
  18. package/dist/runtime/components/KScatterChart/KScatterChart.vue.d.ts +3 -1
  19. package/dist/runtime/composables/chartUtils.d.ts +8 -0
  20. package/dist/runtime/composables/chartUtils.js +58 -0
  21. package/dist/runtime/composables/useChartAnimation.js +7 -1
  22. package/dist/runtime/composables/useChartAxes.d.ts +4 -2
  23. package/dist/runtime/composables/useChartAxes.js +32 -13
  24. package/dist/runtime/composables/useChartCanvas.d.ts +1 -0
  25. package/dist/runtime/composables/useChartCanvas.js +51 -18
  26. package/dist/runtime/composables/useChartData.d.ts +13 -7
  27. package/dist/runtime/composables/useChartData.js +99 -30
  28. package/dist/runtime/composables/useChartExport.d.ts +1 -1
  29. package/dist/runtime/composables/useChartExport.js +26 -3
  30. package/dist/runtime/composables/useChartHitTest.d.ts +2 -2
  31. package/dist/runtime/composables/useChartHitTest.js +7 -5
  32. package/dist/runtime/composables/useChartViewport.d.ts +3 -0
  33. package/dist/runtime/composables/useChartViewport.js +30 -7
  34. package/package.json +12 -7
@@ -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, and ISO date 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
- export declare function parseValue(val: any): number;
41
+ export declare function parseValue(val: any, fallbackIndex?: number): number;
40
42
  export declare function useChartAxes(opts: IChartAxesOptions): IChartAxesContext;
@@ -1,13 +1,15 @@
1
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;
2
+ import { cleanNumber, safeMax, safeMin } from "./chartUtils.js";
3
+ export function parseValue(val, fallbackIndex) {
4
+ if (val === null || val === void 0) return fallbackIndex ?? 0;
5
+ if (typeof val === "number") return Number.isFinite(val) ? val : fallbackIndex ?? 0;
6
6
  if (typeof val === "string") {
7
+ const cleaned = cleanNumber(val, NaN);
8
+ if (Number.isFinite(cleaned)) return cleaned;
7
9
  const d = Date.parse(val);
8
10
  if (!isNaN(d)) return d;
9
11
  }
10
- return 0;
12
+ return fallbackIndex ?? 0;
11
13
  }
12
14
  export function useChartAxes(opts) {
13
15
  const pad = opts.padding ?? { top: 20, right: 20, bottom: 40, left: 55 };
@@ -15,21 +17,25 @@ export function useChartAxes(opts) {
15
17
  const plotTop = pad.top;
16
18
  const plotWidth = computed(() => Math.max(0, opts.width.value - pad.left - pad.right));
17
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
+ };
18
24
  const rawXMin = computed(() => {
19
- const vals = opts.items.value.map((r) => parseValue(r[opts.xField.value]));
20
- return vals.length ? Math.min(...vals) : 0;
25
+ const vals = opts.items.value.map((r, i) => getXVal(r, i));
26
+ return safeMin(vals, 0);
21
27
  });
22
28
  const rawXMax = computed(() => {
23
- const vals = opts.items.value.map((r) => parseValue(r[opts.xField.value]));
24
- return vals.length ? Math.max(...vals) : 1;
29
+ const vals = opts.items.value.map((r, i) => getXVal(r, i));
30
+ return safeMax(vals, 1);
25
31
  });
26
32
  const yMin = computed(() => {
27
- const vals = opts.items.value.map((r) => parseValue(r[opts.yField.value]));
28
- return vals.length ? Math.min(...vals) : 0;
33
+ const vals = opts.items.value.map((r, i) => parseValue(r[opts.yField.value], i));
34
+ return safeMin(vals, 0);
29
35
  });
30
36
  const yMax = computed(() => {
31
- const vals = opts.items.value.map((r) => parseValue(r[opts.yField.value]));
32
- return vals.length ? Math.max(...vals) : 1;
37
+ const vals = opts.items.value.map((r, i) => parseValue(r[opts.yField.value], i));
38
+ return safeMax(vals, 1);
33
39
  });
34
40
  const xRange = computed(() => rawXMax.value - rawXMin.value);
35
41
  const visibleXRange = computed(() => xRange.value / opts.scale.value);
@@ -46,11 +52,24 @@ export function useChartAxes(opts) {
46
52
  return plotLeft + (value - xMin.value) / range * plotWidth.value;
47
53
  };
48
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
+ }
49
67
  const range = yMax.value - yMin.value;
50
68
  if (range === 0) return plotTop + plotHeight.value;
51
69
  return plotTop + plotHeight.value - (value - yMin.value) / range * plotHeight.value;
52
70
  };
53
71
  const fromX = (px) => {
72
+ if (plotWidth.value <= 0) return xMin.value;
54
73
  const range = xMax.value - xMin.value;
55
74
  return xMin.value + (px - plotLeft) / plotWidth.value * range;
56
75
  };
@@ -5,6 +5,7 @@ export interface IChartCanvasContext {
5
5
  width: Ref<number>;
6
6
  height: Ref<number>;
7
7
  dpr: Ref<number>;
8
+ resizeTick: Ref<number>;
8
9
  clear: (color?: string) => void;
9
10
  setupCanvas: (el: HTMLCanvasElement) => void;
10
11
  }
@@ -4,32 +4,64 @@ export function useChartCanvas() {
4
4
  const ctx = ref(null);
5
5
  const width = ref(0);
6
6
  const height = ref(0);
7
- const dpr = ref(window?.devicePixelRatio ?? 1);
7
+ const dpr = ref(typeof window !== "undefined" ? window.devicePixelRatio ?? 1 : 1);
8
+ const resizeTick = ref(0);
8
9
  let resizeObserver = null;
9
10
  const applySize = (el, w, h) => {
10
- const ratio = window.devicePixelRatio ?? 1;
11
+ const ratio = typeof window !== "undefined" ? window.devicePixelRatio ?? 1 : 1;
12
+ const newPixelW = Math.round(w * ratio);
13
+ const newPixelH = Math.round(h * ratio);
11
14
  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;
15
+ let sizeChanged = false;
16
+ if (el.width !== newPixelW || el.height !== newPixelH) {
17
+ el.width = newPixelW;
18
+ el.height = newPixelH;
19
+ ctx.value = el.getContext("2d");
20
+ ctx.value?.scale(ratio, ratio);
21
+ sizeChanged = true;
22
+ }
23
+ if (width.value !== w || height.value !== h || sizeChanged) {
24
+ width.value = w;
25
+ height.value = h;
26
+ resizeTick.value++;
27
+ }
28
+ };
29
+ let dprMediaQuery = null;
30
+ const handleDprChange = () => {
31
+ if (canvas.value) {
32
+ const rect = canvas.value.getBoundingClientRect();
33
+ const w = rect.width || canvas.value.clientWidth || width.value;
34
+ const h = rect.height || canvas.value.clientHeight || height.value;
35
+ applySize(canvas.value, w, h);
36
+ }
37
+ updateDprWatcher();
38
+ };
39
+ const updateDprWatcher = () => {
40
+ if (typeof window === "undefined" || !window.matchMedia) return;
41
+ dprMediaQuery?.removeEventListener?.("change", handleDprChange);
42
+ dprMediaQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio ?? 1}dppx)`);
43
+ dprMediaQuery.addEventListener?.("change", handleDprChange);
17
44
  };
18
45
  const setupCanvas = (el) => {
19
46
  canvas.value = el;
20
47
  ctx.value = el.getContext("2d");
21
48
  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);
49
+ const initialW = rect.width || el.clientWidth || 400;
50
+ const initialH = rect.height || el.clientHeight || 400;
51
+ applySize(el, initialW, initialH);
52
+ updateDprWatcher();
53
+ if (typeof ResizeObserver !== "undefined") {
54
+ resizeObserver?.disconnect();
55
+ resizeObserver = new ResizeObserver((entries) => {
56
+ for (const entry of entries) {
57
+ const { width: w, height: h } = entry.contentRect;
58
+ if (canvas.value && w > 0 && h > 0) {
59
+ applySize(canvas.value, w, h);
60
+ }
29
61
  }
30
- }
31
- });
32
- resizeObserver.observe(el);
62
+ });
63
+ resizeObserver.observe(el);
64
+ }
33
65
  };
34
66
  const clear = (color) => {
35
67
  if (ctx.value && canvas.value) {
@@ -42,6 +74,7 @@ export function useChartCanvas() {
42
74
  };
43
75
  onUnmounted(() => {
44
76
  resizeObserver?.disconnect();
77
+ dprMediaQuery?.removeEventListener?.("change", handleDprChange);
45
78
  });
46
- return { canvas, ctx, width, height, dpr, clear, setupCanvas };
79
+ return { canvas, ctx, width, height, dpr, resizeTick, clear, setupCanvas };
47
80
  }
@@ -1,14 +1,20 @@
1
- import { type Ref } from 'vue';
1
+ import { type Ref, type ShallowRef } from 'vue';
2
+ import type { ADataProvider } from '@katlux/providers';
2
3
  export interface IChartDataContext<T = any> {
3
- items: Ref<T[]>;
4
+ items: ShallowRef<T[]>;
4
5
  loading: Ref<boolean>;
5
6
  error: Ref<string | null>;
6
7
  reload: () => Promise<void>;
7
8
  }
8
9
  /**
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)
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).
13
13
  */
14
- export declare function useChartData<T = any>(providerOrData: Ref<any>): IChartDataContext<T>;
14
+ export declare function extractProviderItems<T = any>(provider?: ADataProvider | any): T[];
15
+ /**
16
+ * Provider-agnostic data watcher for Katlux chart components.
17
+ * Consumes data through Katlux DataProvider instances (Server-side or Client-side).
18
+ * Uses provider.subscribe() instead of Vue watch() so it works with __v_skip providers.
19
+ */
20
+ export declare function useChartData<T = any>(dataProvider: Ref<ADataProvider | null | undefined> | ADataProvider | null | undefined): IChartDataContext<T>;
@@ -1,45 +1,114 @@
1
- import { ref, watch, onMounted, isRef } from "vue";
2
- export function useChartData(providerOrData) {
3
- const items = ref([]);
1
+ import { shallowRef, ref, watch, onMounted, onUnmounted, onScopeDispose, getCurrentScope, getCurrentInstance, isRef } from "vue";
2
+ export function extractProviderItems(provider) {
3
+ if (!provider) return [];
4
+ const instance = isRef(provider) ? provider.value : provider;
5
+ if (!instance) return [];
6
+ const hasActiveFilter = Boolean(
7
+ instance.filter && (typeof instance.filter === "object" && Object.keys(instance.filter).length > 0 || typeof instance.hasFilter === "boolean" && instance.hasFilter || typeof instance.hasFilter === "function" && instance.hasFilter())
8
+ );
9
+ const rowMap = isRef(instance._rowMap) ? instance._rowMap.value : instance._rowMap;
10
+ if (Array.isArray(rowMap)) {
11
+ if (hasActiveFilter) {
12
+ return rowMap;
13
+ }
14
+ if (rowMap.length > 0) {
15
+ return rowMap;
16
+ }
17
+ }
18
+ const filtered = isRef(instance.filteredData) ? instance.filteredData.value : instance.filteredData;
19
+ if (Array.isArray(filtered)) {
20
+ if (hasActiveFilter) {
21
+ return filtered;
22
+ }
23
+ if (filtered.length > 0) {
24
+ return filtered;
25
+ }
26
+ }
27
+ const all = isRef(instance.allData) ? instance.allData.value : instance.allData;
28
+ if (Array.isArray(all) && all.length > 0) {
29
+ if (hasActiveFilter && Array.isArray(filtered)) {
30
+ return [];
31
+ }
32
+ return all;
33
+ }
34
+ let raw = isRef(instance.pageData) ? instance.pageData.value : instance.pageData;
35
+ if (!raw) return [];
36
+ if (Array.isArray(raw)) {
37
+ return raw;
38
+ }
39
+ if (Array.isArray(raw.rows)) {
40
+ return raw.rows;
41
+ }
42
+ return [];
43
+ }
44
+ export function useChartData(dataProvider) {
45
+ const items = shallowRef([]);
4
46
  const loading = ref(false);
5
47
  const error = ref(null);
6
- const load = async () => {
7
- const source = providerOrData.value;
8
- if (!source) {
48
+ const updateItems = () => {
49
+ const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
50
+ if (provider) {
51
+ items.value = extractProviderItems(provider);
52
+ const rawLoading = provider.loading !== void 0 ? isRef(provider.loading) ? provider.loading.value : provider.loading?.value ?? provider.loading : false;
53
+ const rawInitial = provider.initialLoad !== void 0 ? isRef(provider.initialLoad) ? provider.initialLoad.value : provider.initialLoad?.value ?? provider.initialLoad : false;
54
+ loading.value = Boolean(rawLoading || rawInitial);
55
+ } else {
9
56
  items.value = [];
10
- return;
57
+ loading.value = false;
58
+ }
59
+ };
60
+ let unsubscribe = null;
61
+ const setupSubscription = () => {
62
+ if (unsubscribe) {
63
+ unsubscribe();
64
+ unsubscribe = null;
65
+ }
66
+ const isServer = typeof window === "undefined" || typeof import.meta !== "undefined" && Boolean(import.meta.server);
67
+ const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
68
+ if (!isServer && provider?.subscribe) {
69
+ unsubscribe = provider.subscribe(() => updateItems());
11
70
  }
71
+ updateItems();
72
+ };
73
+ if (isRef(dataProvider)) {
74
+ watch(dataProvider, setupSubscription, { immediate: true });
75
+ } else {
76
+ setupSubscription();
77
+ }
78
+ if (getCurrentInstance()) {
79
+ onMounted(updateItems);
80
+ onUnmounted(() => {
81
+ if (unsubscribe) {
82
+ unsubscribe();
83
+ unsubscribe = null;
84
+ }
85
+ });
86
+ }
87
+ if (getCurrentScope()) {
88
+ onScopeDispose(() => {
89
+ if (unsubscribe) {
90
+ unsubscribe();
91
+ unsubscribe = null;
92
+ }
93
+ });
94
+ }
95
+ const reload = async () => {
96
+ const provider = isRef(dataProvider) ? dataProvider.value : dataProvider;
97
+ if (!provider) return;
12
98
  loading.value = true;
13
99
  error.value = null;
14
100
  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;
101
+ if (provider.refresh) {
102
+ await provider.refresh();
103
+ } else if (provider.loadPageData) {
104
+ await provider.loadPageData();
31
105
  }
106
+ updateItems();
32
107
  } catch (e) {
33
- error.value = e?.message ?? "Failed to load chart data";
108
+ error.value = e?.message ?? "Failed to reload chart provider";
34
109
  } finally {
35
110
  loading.value = false;
36
111
  }
37
112
  };
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
113
  return { items, loading, error, reload };
45
114
  }
@@ -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,14 +16,37 @@ 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 dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
32
+ const svgBox = svgEl.viewBox?.baseVal;
33
+ const w = svgBox && svgBox.width > 0 ? svgBox.width : canvas.style.width ? parseFloat(canvas.style.width) : svgEl.clientWidth || canvas.width / dpr || 400;
34
+ const h = svgBox && svgBox.height > 0 ? svgBox.height : canvas.style.height ? parseFloat(canvas.style.height) : svgEl.clientHeight || canvas.height / dpr || 400;
35
+ imageEl.setAttribute("width", String(w));
36
+ imageEl.setAttribute("height", String(h));
37
+ clone.insertBefore(imageEl, clone.firstChild);
38
+ }
21
39
  const serializer = new XMLSerializer();
22
- const svgStr = serializer.serializeToString(svgEl);
40
+ const svgStr = serializer.serializeToString(clone);
23
41
  const blob = new Blob([svgStr], { type: "image/svg+xml;charset=utf-8" });
24
42
  const url = URL.createObjectURL(blob);
25
43
  download(url, filename);
26
- URL.revokeObjectURL(url);
44
+ setTimeout(() => {
45
+ try {
46
+ URL.revokeObjectURL(url);
47
+ } catch {
48
+ }
49
+ }, 1e3);
27
50
  } catch (e) {
28
51
  console.error("[KChart] SVG export failed:", e);
29
52
  }
@@ -1,10 +1,10 @@
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;
7
- findNearestEuclidean: (items: any[], mouseX: number, mouseY: number, axes: IChartAxesContext, xField: string, yField: string, threshold?: number) => {
7
+ findNearestEuclidean: (items: any[], mouseX: number, mouseY: number, axes: IChartAxesContext, xField: string, yField: string, threshold?: number, xValueGetter?: (item: any, index: number) => number) => {
8
8
  item: any;
9
9
  index: number;
10
10
  } | 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;
@@ -15,12 +16,13 @@ export function useChartHitTest() {
15
16
  }
16
17
  return { item: items[nearestIdx], index: nearestIdx };
17
18
  };
18
- const findNearestEuclidean = (items, mouseX, mouseY, axes, xField, yField, threshold = 20) => {
19
+ const findNearestEuclidean = (items, mouseX, mouseY, axes, xField, yField, threshold = 20, xValueGetter) => {
19
20
  if (!items.length) return null;
20
21
  let nearestIdx = -1;
21
22
  let minDistanceSq = threshold * threshold;
22
23
  items.forEach((item, i) => {
23
- const px = axes.toX(parseValue(item[xField]));
24
+ const xVal = xValueGetter ? xValueGetter(item, i) : parseValue(item[xField], i);
25
+ const px = axes.toX(xVal);
24
26
  const py = axes.toY(parseValue(item[yField]));
25
27
  const dx = mouseX - px;
26
28
  const dy = mouseY - py;
@@ -9,6 +9,7 @@ export interface IViewportState {
9
9
  canPan: Ref<boolean>;
10
10
  }
11
11
  export interface IChartViewportContext extends IViewportState {
12
+ zoomTo: (newScale: number, anchorX?: number) => void;
12
13
  zoomIn: () => void;
13
14
  zoomOut: () => void;
14
15
  resetZoom: () => void;
@@ -22,4 +23,6 @@ export declare function useChartViewport(options: {
22
23
  maxZoom?: Ref<number>;
23
24
  plotWidth: Ref<number>;
24
25
  plotLeft?: Ref<number>;
26
+ zoomable?: Ref<boolean> | boolean;
27
+ wheelNavigation?: Ref<boolean> | boolean;
25
28
  }): IChartViewportContext;
@@ -1,10 +1,20 @@
1
- import { ref } from "vue";
1
+ import { ref, isRef } from "vue";
2
2
  const ZOOM_STEP = 0.25;
3
3
  const MIN_SCALE = 1;
4
4
  const PAN_CLAMP_THRESHOLD = 0.01;
5
5
  export function useChartViewport(options) {
6
6
  const maxZoom = options.maxZoom ?? ref(10);
7
7
  const scale = ref(1);
8
+ const isZoomable = () => {
9
+ if (options.zoomable === void 0) return true;
10
+ return isRef(options.zoomable) ? options.zoomable.value : options.zoomable;
11
+ };
12
+ const isWheelNavEnabled = () => {
13
+ if (options.wheelNavigation !== void 0) {
14
+ return isRef(options.wheelNavigation) ? options.wheelNavigation.value : options.wheelNavigation;
15
+ }
16
+ return isZoomable();
17
+ };
8
18
  const panOffset = ref({ x: 0, y: 0 });
9
19
  const isDragging = ref(false);
10
20
  const canPan = ref(false);
@@ -12,8 +22,12 @@ export function useChartViewport(options) {
12
22
  let panStart = { x: 0, y: 0 };
13
23
  const clampPan = () => {
14
24
  const maxPanX = plotWidth() * (1 - 1 / scale.value);
15
- panOffset.value.x = Math.max(0, Math.min(panOffset.value.x, maxPanX));
16
- panOffset.value.y = 0;
25
+ const clampedX = Math.max(0, Math.min(panOffset.value.x, maxPanX));
26
+ if (clampedX !== panOffset.value.x) {
27
+ panOffset.value = { x: clampedX, y: 0 };
28
+ } else if (panOffset.value.y !== 0) {
29
+ panOffset.value = { ...panOffset.value, y: 0 };
30
+ }
17
31
  };
18
32
  const plotWidth = () => options.plotWidth.value;
19
33
  const updateCanPan = () => {
@@ -23,10 +37,14 @@ export function useChartViewport(options) {
23
37
  panOffset.value = { x: 0, y: 0 };
24
38
  };
25
39
  const zoomTo = (newScale, anchorX) => {
40
+ if (!isZoomable()) return;
26
41
  const prev = scale.value;
27
42
  const clamped = Math.min(Math.max(newScale, MIN_SCALE), maxZoom.value);
28
43
  if (anchorX !== void 0 && prev !== clamped) {
29
- panOffset.value.x += anchorX * (1 / prev - 1 / clamped);
44
+ panOffset.value = {
45
+ ...panOffset.value,
46
+ x: panOffset.value.x + anchorX * (1 / prev - 1 / clamped)
47
+ };
30
48
  }
31
49
  scale.value = clamped;
32
50
  if (scale.value <= MIN_SCALE) resetPan();
@@ -41,7 +59,11 @@ export function useChartViewport(options) {
41
59
  updateCanPan();
42
60
  };
43
61
  const onWheel = (e) => {
62
+ if (!isWheelNavEnabled()) {
63
+ return;
64
+ }
44
65
  if (e.ctrlKey) {
66
+ if (!isZoomable()) return;
45
67
  e.preventDefault();
46
68
  const rect = e.currentTarget.getBoundingClientRect();
47
69
  const plotLeft = options.plotLeft?.value ?? 0;
@@ -49,11 +71,11 @@ export function useChartViewport(options) {
49
71
  const delta = e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP;
50
72
  zoomTo(scale.value + delta, anchorX);
51
73
  } else if (e.shiftKey) {
52
- e.preventDefault();
53
74
  if (!canPan.value) return;
75
+ e.preventDefault();
54
76
  const delta = e.deltaX !== 0 ? e.deltaX : e.deltaY;
55
77
  const dx = delta / scale.value;
56
- panOffset.value.x += dx;
78
+ panOffset.value = { ...panOffset.value, x: panOffset.value.x + dx };
57
79
  clampPan();
58
80
  }
59
81
  };
@@ -66,7 +88,7 @@ export function useChartViewport(options) {
66
88
  const onMouseMove = (e) => {
67
89
  if (!isDragging.value || !canPan.value) return;
68
90
  const dx = -(e.clientX - dragStart.x) / scale.value;
69
- panOffset.value.x = panStart.x + dx;
91
+ panOffset.value = { ...panOffset.value, x: panStart.x + dx };
70
92
  clampPan();
71
93
  };
72
94
  const onMouseUp = (_e) => {
@@ -80,6 +102,7 @@ export function useChartViewport(options) {
80
102
  panOffset,
81
103
  isDragging,
82
104
  canPan,
105
+ zoomTo,
83
106
  zoomIn,
84
107
  zoomOut,
85
108
  resetZoom,
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@katlux/block-charts",
3
- "version": "0.1.0-beta.8",
3
+ "version": "0.1.1-alpha.100",
4
4
  "description": "Pre-built charting block components for Katlux toolkit",
5
5
  "author": "Katlux",
6
- "license": "MIT",
6
+ "license": "Apache-2.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -15,17 +15,22 @@
15
15
  },
16
16
  "exports": {
17
17
  ".": {
18
- "types": "./dist/types.d.ts",
18
+ "types": "./dist/types.d.mts",
19
19
  "import": "./dist/module.mjs",
20
- "require": "./dist/module.cjs"
20
+ "require": "./dist/module.mjs"
21
21
  },
22
22
  "./package.json": "./package.json",
23
23
  "./components/*": "./dist/runtime/components/*"
24
24
  },
25
+ "sideEffects": [
26
+ "**/*.css",
27
+ "**/*.scss",
28
+ "**/*.sass"
29
+ ],
25
30
  "dependencies": {
26
- "@katlux/providers": "*",
27
- "@katlux/toolkit": "*",
28
- "@nuxt/kit": "^3.20.1",
31
+ "@katlux/providers": "^0.1.1-alpha.112",
32
+ "@katlux/toolkit": "^0.1.1-alpha.124",
33
+ "@nuxt/kit": "^3.12.0",
29
34
  "pug": "^3.0.0",
30
35
  "sass": "^1.80.0"
31
36
  },