@katlux/block-charts 0.1.1-alpha.95 → 0.1.1-alpha.97

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.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0"
6
6
  },
7
- "version": "0.1.1-alpha.95",
7
+ "version": "0.1.1-alpha.97",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -140,13 +140,14 @@ const xFieldRef = computed(() => props.xField);
140
140
  const yFieldRef = computed(() => props.yField);
141
141
  const xAxisType = computed(() => {
142
142
  if (!items.value.length) return "linear";
143
- const firstVal = items.value[0]?.[props.xField];
144
- if (typeof firstVal === "number") return "linear";
145
- if (typeof firstVal === "string") {
146
- const cleaned = cleanNumber(firstVal, NaN);
143
+ const sample = items.value.find((r) => r?.[props.xField] !== void 0 && r?.[props.xField] !== null)?.[props.xField];
144
+ if (sample === void 0 || sample === null) return "linear";
145
+ if (typeof sample === "number") return "linear";
146
+ if (typeof sample === "string") {
147
+ const cleaned = cleanNumber(sample, NaN);
147
148
  if (Number.isFinite(cleaned)) return "linear";
148
- const parsed = Date.parse(firstVal);
149
- if (!isNaN(parsed) && !/^\d+$/.test(firstVal.trim()) && /[-/T:]|[A-Za-z]/.test(firstVal)) return "datetime";
149
+ const parsed = Date.parse(sample);
150
+ if (!isNaN(parsed) && !/^\d+$/.test(sample.trim()) && /[-/T:]|[A-Za-z]/.test(sample)) return "datetime";
150
151
  return "category";
151
152
  }
152
153
  return "linear";
@@ -60,7 +60,7 @@ import { useChartData } from "../../composables/useChartData";
60
60
  import { useChartViewport } from "../../composables/useChartViewport";
61
61
  import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
62
62
  import { useChartExport } from "../../composables/useChartExport";
63
- import { cleanNumber, safeMax } from "../../composables/chartUtils";
63
+ import { cleanNumber, safeMax, safeMin } from "../../composables/chartUtils";
64
64
  const clipId = useId();
65
65
  const props = defineProps({
66
66
  dataProvider: { type: null, required: false },
@@ -133,16 +133,38 @@ const plotWidth = computed(() => Math.max(0, width.value - PAD.left - PAD.right)
133
133
  const plotHeight = computed(() => Math.max(0, height.value - PAD.top - PAD.bottom));
134
134
  const categories = computed(() => [...new Set(items.value.map((r) => String(r[props.categoryField])))]);
135
135
  const groups = computed(() => [...new Set(items.value.map((r) => String(r[props.groupField] ?? "default")))]);
136
+ const categoryMap = computed(() => {
137
+ const map = /* @__PURE__ */ new Map();
138
+ for (const item of items.value) {
139
+ const cat = String(item[props.categoryField]);
140
+ let list = map.get(cat);
141
+ if (!list) {
142
+ list = [];
143
+ map.set(cat, list);
144
+ }
145
+ list.push(item);
146
+ }
147
+ return map;
148
+ });
136
149
  const minValue = computed(() => {
150
+ if (props.mode === "stacked") {
151
+ const negSums = categories.value.map((cat) => {
152
+ const catItems = categoryMap.value.get(cat) || [];
153
+ return catItems.reduce((acc, r) => acc + Math.min(0, cleanNumber(r[props.valueField])), 0);
154
+ });
155
+ const min2 = Math.min(0, safeMin(negSums, 0));
156
+ return isFinite(min2) ? min2 : 0;
157
+ }
137
158
  const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
138
- const min = Math.min(0, ...vals);
159
+ const min = Math.min(0, safeMin(vals, 0));
139
160
  return isFinite(min) ? min : 0;
140
161
  });
141
162
  const maxValue = computed(() => {
142
163
  if (props.mode === "stacked") {
143
- const sums = categories.value.map(
144
- (cat) => items.value.filter((r) => String(r[props.categoryField]) === cat).reduce((acc, r) => acc + Math.max(0, cleanNumber(r[props.valueField])), 0)
145
- );
164
+ const sums = categories.value.map((cat) => {
165
+ const catItems = categoryMap.value.get(cat) || [];
166
+ return catItems.reduce((acc, r) => acc + Math.max(0, cleanNumber(r[props.valueField])), 0);
167
+ });
146
168
  return safeMax(sums, 10);
147
169
  }
148
170
  const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
@@ -227,7 +249,7 @@ const draw = () => {
227
249
  c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value);
228
250
  c.clip();
229
251
  visibleCategories.value.forEach((cat, catIdx) => {
230
- const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
252
+ const catItems = categoryMap.value.get(cat) || [];
231
253
  const catY = plotTop + catIdx * catHeight;
232
254
  if (props.mode === "stacked") {
233
255
  let xOffsetPos = 0;
@@ -350,7 +372,7 @@ const draw = () => {
350
372
  c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value);
351
373
  c.clip();
352
374
  visibleCategories.value.forEach((cat, catIdx) => {
353
- const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
375
+ const catItems = categoryMap.value.get(cat) || [];
354
376
  const catX = plotLeft + catIdx * catWidth;
355
377
  if (props.mode === "stacked") {
356
378
  let yOffsetPos = 0;
@@ -105,9 +105,17 @@ const hoveredCell = computed(() => {
105
105
  if (hoveredIndex.value === -1) return null;
106
106
  return renderedCells.value[hoveredIndex.value];
107
107
  });
108
+ const parseHexColor = (colorStr, fallback) => {
109
+ let hex = (colorStr || fallback).replace("#", "").trim();
110
+ if (hex.length === 3) {
111
+ hex = hex.split("").map((c) => c + c).join("");
112
+ }
113
+ const num = parseInt(hex.padEnd(6, "0").slice(0, 6), 16);
114
+ return isNaN(num) ? parseInt(fallback.replace("#", ""), 16) : num;
115
+ };
108
116
  const lerp = (t) => {
109
- const low = parseInt((props.colorLow || "#1e1e2e").replace("#", ""), 16);
110
- const high = parseInt((props.colorHigh || "#6366f1").replace("#", ""), 16);
117
+ const low = parseHexColor(props.colorLow, "1e1e2e");
118
+ const high = parseHexColor(props.colorHigh, "6366f1");
111
119
  const lr = low >> 16 & 255;
112
120
  const lg = low >> 8 & 255;
113
121
  const lb = low & 255;
@@ -139,13 +139,14 @@ const {
139
139
  } = viewport;
140
140
  const xAxisType = computed(() => {
141
141
  if (!items.value.length) return "linear";
142
- const firstVal = items.value[0]?.[props.xField];
143
- if (typeof firstVal === "number") return "linear";
144
- if (typeof firstVal === "string") {
145
- const cleaned = cleanNumber(firstVal, NaN);
142
+ const sample = items.value.find((r) => r?.[props.xField] !== void 0 && r?.[props.xField] !== null)?.[props.xField];
143
+ if (sample === void 0 || sample === null) return "linear";
144
+ if (typeof sample === "number") return "linear";
145
+ if (typeof sample === "string") {
146
+ const cleaned = cleanNumber(sample, NaN);
146
147
  if (Number.isFinite(cleaned)) return "linear";
147
- const parsed = Date.parse(firstVal);
148
- if (!isNaN(parsed) && !/^\d+$/.test(firstVal.trim()) && /[-/T:]|[A-Za-z]/.test(firstVal)) return "datetime";
148
+ const parsed = Date.parse(sample);
149
+ if (!isNaN(parsed) && !/^\d+$/.test(sample.trim()) && /[-/T:]|[A-Za-z]/.test(sample)) return "datetime";
149
150
  return "category";
150
151
  }
151
152
  return "linear";
@@ -387,7 +388,7 @@ const drawLine = (c, data, color, progress2) => {
387
388
  c.restore();
388
389
  };
389
390
  watch(progress, draw);
390
- watch([scale, panOffset, hoveredIndex, resizeTick, width, height], draw);
391
+ watch([scale, panOffset, resizeTick, width, height], draw);
391
392
  watch(items, () => {
392
393
  if (props.animated) animate();
393
394
  else draw();
@@ -50,8 +50,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
50
50
  showLegend: boolean;
51
51
  xAxisTitle: string;
52
52
  yAxisTitle: string;
53
- sizeField: string;
54
53
  labelField: string;
54
+ sizeField: string;
55
55
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
56
56
  declare const _default: typeof __VLS_export;
57
57
  export default _default;
@@ -129,7 +129,8 @@ const xFieldRef = computed(() => props.xField);
129
129
  const yFieldRef = computed(() => props.yField);
130
130
  const xAxisType = computed(() => {
131
131
  if (!items.value.length) return "numeric";
132
- const sample = items.value[0][props.xField];
132
+ const sample = items.value.find((r) => r?.[props.xField] !== void 0 && r?.[props.xField] !== null)?.[props.xField];
133
+ if (sample === void 0 || sample === null) return "numeric";
133
134
  if (typeof sample === "number") return "numeric";
134
135
  if (typeof sample === "string") {
135
136
  const cleaned = cleanNumber(sample, NaN);
@@ -181,11 +182,25 @@ const axes = useChartAxes({
181
182
  const maxSize = computed(() => safeMax(items.value.map((r) => cleanNumber(r[props.sizeField] ?? 1, 1)), 1));
182
183
  const seriesMap = computed(() => {
183
184
  const map = /* @__PURE__ */ new Map();
184
- for (const item of items.value) {
185
+ const maxS = maxSize.value || 1;
186
+ items.value.forEach((item, idx) => {
185
187
  const key = String(item[props.seriesField] ?? "default");
186
- if (!map.has(key)) map.set(key, []);
187
- map.get(key).push(item);
188
- }
188
+ let list = map.get(key);
189
+ if (!list) {
190
+ list = [];
191
+ map.set(key, list);
192
+ }
193
+ const sizeRaw = cleanNumber(item[props.sizeField] ?? 1, 1);
194
+ const r = 4 + sizeRaw / maxS * 16;
195
+ list.push({
196
+ item,
197
+ index: idx,
198
+ xVal: resolveX(item, idx),
199
+ yVal: parseValue(item[props.yField]),
200
+ sizeRaw,
201
+ r
202
+ });
203
+ });
189
204
  return map;
190
205
  });
191
206
  const hoveredPoint = computed(() => {
@@ -291,14 +306,13 @@ const draw = () => {
291
306
  c.rect(axes.plotLeft, axes.plotTop, axes.plotWidth.value, axes.plotHeight.value);
292
307
  c.clip();
293
308
  let seriesIdx = 0;
294
- for (const [, seriesItems] of seriesMap.value) {
309
+ for (const [, points] of seriesMap.value) {
295
310
  const color = props.colors[seriesIdx % props.colors.length];
296
- seriesItems.forEach((item, idx) => {
297
- const x = axes.toX(resolveX(item, idx));
298
- const y = axes.toY(parseValue(item[props.yField]));
299
- const sizeRaw = cleanNumber(item[props.sizeField] ?? 1, 1);
300
- const r = 4 + sizeRaw / (maxSize.value || 1) * 16;
301
- const isHovered = items.value[hoveredIndex.value] === item;
311
+ points.forEach((pt) => {
312
+ const x = axes.toX(pt.xVal);
313
+ const y = axes.toY(pt.yVal);
314
+ const r = pt.r;
315
+ const isHovered = items.value[hoveredIndex.value] === pt.item;
302
316
  const alpha = p;
303
317
  c.save();
304
318
  c.globalAlpha = alpha * (isHovered ? 1 : 0.75);
@@ -50,8 +50,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
50
50
  showLegend: boolean;
51
51
  xAxisTitle: string;
52
52
  yAxisTitle: string;
53
- sizeField: string;
54
53
  labelField: string;
54
+ sizeField: string;
55
55
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
56
56
  declare const _default: typeof __VLS_export;
57
57
  export default _default;
@@ -69,6 +69,7 @@ export function useChartAxes(opts) {
69
69
  return plotTop + plotHeight.value - (value - yMin.value) / range * plotHeight.value;
70
70
  };
71
71
  const fromX = (px) => {
72
+ if (plotWidth.value <= 0) return xMin.value;
72
73
  const range = xMax.value - xMin.value;
73
74
  return xMin.value + (px - plotLeft) / plotWidth.value * range;
74
75
  };
@@ -26,6 +26,22 @@ export function useChartCanvas() {
26
26
  resizeTick.value++;
27
27
  }
28
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);
44
+ };
29
45
  const setupCanvas = (el) => {
30
46
  canvas.value = el;
31
47
  ctx.value = el.getContext("2d");
@@ -33,6 +49,7 @@ export function useChartCanvas() {
33
49
  const initialW = rect.width || el.clientWidth || 400;
34
50
  const initialH = rect.height || el.clientHeight || 400;
35
51
  applySize(el, initialW, initialH);
52
+ updateDprWatcher();
36
53
  if (typeof ResizeObserver !== "undefined") {
37
54
  resizeObserver?.disconnect();
38
55
  resizeObserver = new ResizeObserver((entries) => {
@@ -57,6 +74,7 @@ export function useChartCanvas() {
57
74
  };
58
75
  onUnmounted(() => {
59
76
  resizeObserver?.disconnect();
77
+ dprMediaQuery?.removeEventListener?.("change", handleDprChange);
60
78
  });
61
79
  return { canvas, ctx, width, height, dpr, resizeTick, clear, setupCanvas };
62
80
  }
@@ -1,7 +1,7 @@
1
- import { type Ref } from 'vue';
1
+ import { type Ref, type ShallowRef } from 'vue';
2
2
  import type { ADataProvider } from '@katlux/providers';
3
3
  export interface IChartDataContext<T = any> {
4
- items: Ref<T[]>;
4
+ items: ShallowRef<T[]>;
5
5
  loading: Ref<boolean>;
6
6
  error: Ref<string | null>;
7
7
  reload: () => Promise<void>;
@@ -1,4 +1,4 @@
1
- import { ref, watch, onMounted, onUnmounted, onScopeDispose, getCurrentScope, getCurrentInstance, isRef } from "vue";
1
+ import { shallowRef, ref, watch, onMounted, onUnmounted, onScopeDispose, getCurrentScope, getCurrentInstance, isRef } from "vue";
2
2
  export function extractProviderItems(provider) {
3
3
  if (!provider) return [];
4
4
  const instance = isRef(provider) ? provider.value : provider;
@@ -6,6 +6,15 @@ export function extractProviderItems(provider) {
6
6
  const hasActiveFilter = Boolean(
7
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
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
+ }
9
18
  const filtered = isRef(instance.filteredData) ? instance.filteredData.value : instance.filteredData;
10
19
  if (Array.isArray(filtered)) {
11
20
  if (hasActiveFilter) {
@@ -33,7 +42,7 @@ export function extractProviderItems(provider) {
33
42
  return [];
34
43
  }
35
44
  export function useChartData(dataProvider) {
36
- const items = ref([]);
45
+ const items = shallowRef([]);
37
46
  const loading = ref(false);
38
47
  const error = ref(null);
39
48
  const updateItems = () => {
@@ -37,6 +37,7 @@ export function useChartViewport(options) {
37
37
  panOffset.value = { x: 0, y: 0 };
38
38
  };
39
39
  const zoomTo = (newScale, anchorX) => {
40
+ if (!isZoomable()) return;
40
41
  const prev = scale.value;
41
42
  const clamped = Math.min(Math.max(newScale, MIN_SCALE), maxZoom.value);
42
43
  if (anchorX !== void 0 && prev !== clamped) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@katlux/block-charts",
3
- "version": "0.1.1-alpha.95",
3
+ "version": "0.1.1-alpha.97",
4
4
  "description": "Pre-built charting block components for Katlux toolkit",
5
5
  "author": "Katlux",
6
6
  "license": "Apache-2.0",
@@ -23,8 +23,8 @@
23
23
  "./components/*": "./dist/runtime/components/*"
24
24
  },
25
25
  "dependencies": {
26
- "@katlux/providers": "^0.1.1-alpha.100",
27
- "@katlux/toolkit": "^0.1.1-alpha.111",
26
+ "@katlux/providers": "^0.1.1-alpha.106",
27
+ "@katlux/toolkit": "^0.1.1-alpha.120",
28
28
  "@nuxt/kit": "^3.12.0",
29
29
  "pug": "^3.0.0",
30
30
  "sass": "^1.80.0"