@smartnet360/svelte-components 0.0.28 → 0.0.29

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.
@@ -68,12 +68,8 @@
68
68
  return output;
69
69
  }
70
70
 
71
- function renderChart() {
72
- if (!chartDiv || !data?.length) return;
73
-
74
- const traces: any[] = [];
75
- let colorIndex = 0;
76
-
71
+ // Memoize MA hierarchy resolution - only recalculate when inputs change
72
+ let resolvedKPIs = $derived.by(() => {
77
73
  // Helper function to apply MA with full hierarchy:
78
74
  // Runtime Override > KPI > Chart > Section > Layout (higher priority wins)
79
75
  const applyMovingAverageHierarchy = (kpi: any) => {
@@ -143,18 +139,31 @@
143
139
  return kpi;
144
140
  };
145
141
 
142
+ return {
143
+ left: chart.yLeft.map(applyMovingAverageHierarchy),
144
+ right: chart.yRight.map(applyMovingAverageHierarchy)
145
+ };
146
+ });
147
+
148
+ // Extract timestamps once, not for every KPI
149
+ let timestamps = $derived(data.map(row => row['TIMESTAMP']));
150
+
151
+ function renderChart() {
152
+ if (!chartDiv || !data?.length) return;
153
+
154
+ const traces: any[] = [];
155
+ let colorIndex = 0;
156
+
146
157
  // Add left Y-axis traces (with moving average support)
147
- chart.yLeft.forEach(kpi => {
148
- const kpiWithMA = applyMovingAverageHierarchy(kpi);
149
- const kpiTraces = createTimeSeriesTraceWithMA(data, kpiWithMA, 'TIMESTAMP', 'y1', colorIndex);
158
+ resolvedKPIs.left.forEach(kpi => {
159
+ const kpiTraces = createTimeSeriesTraceWithMA(data, kpi, 'TIMESTAMP', 'y1', colorIndex, timestamps);
150
160
  traces.push(...kpiTraces);
151
161
  colorIndex++;
152
162
  });
153
163
 
154
164
  // Add right Y-axis traces (with moving average support)
155
- chart.yRight.forEach(kpi => {
156
- const kpiWithMA = applyMovingAverageHierarchy(kpi);
157
- const kpiTraces = createTimeSeriesTraceWithMA(data, kpiWithMA, 'TIMESTAMP', 'y2', colorIndex);
165
+ resolvedKPIs.right.forEach(kpi => {
166
+ const kpiTraces = createTimeSeriesTraceWithMA(data, kpi, 'TIMESTAMP', 'y2', colorIndex, timestamps);
158
167
  traces.push(...kpiTraces);
159
168
  colorIndex++;
160
169
  });
@@ -184,10 +193,10 @@
184
193
  }
185
194
 
186
195
  // Merge external layout with defaults using deep merge
187
- // Use structuredClone for deep copy to prevent mutation of defaultLayout
188
- let finalLayout = structuredClone(
189
- plotlyLayout ? deepMerge(defaultLayout, plotlyLayout) : defaultLayout
190
- );
196
+ // Only deep merge if plotlyLayout is provided (optimization)
197
+ let finalLayout = plotlyLayout
198
+ ? deepMerge(defaultLayout, plotlyLayout)
199
+ : defaultLayout;
191
200
 
192
201
  // Apply size-based adaptations using helper
193
202
  finalLayout = adaptPlotlyLayout(
@@ -237,43 +246,67 @@
237
246
 
238
247
  renderChart();
239
248
 
240
- // Set up ResizeObserver to handle container size changes
249
+ // Set up ResizeObserver with debouncing to prevent excessive re-renders
241
250
  if (chartDiv && window.ResizeObserver) {
251
+ let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
252
+
242
253
  const resizeObserver = new ResizeObserver((entries) => {
243
- for (const entry of entries) {
244
- const { width, height } = entry.contentRect;
245
-
246
- // Update container size state
247
- containerSize.width = width;
248
- containerSize.height = height;
249
-
250
- if (chartDiv && chartDiv.children.length > 0) {
251
- // Re-render chart with new adaptive layout
252
- renderChart();
253
- }
254
+ // Clear previous timeout
255
+ if (resizeTimeout) {
256
+ clearTimeout(resizeTimeout);
254
257
  }
258
+
259
+ // Debounce resize events - wait 150ms after last resize
260
+ resizeTimeout = setTimeout(() => {
261
+ for (const entry of entries) {
262
+ const { width, height } = entry.contentRect;
263
+
264
+ // Only re-render if size actually changed significantly (>5px)
265
+ const widthChanged = Math.abs(containerSize.width - width) > 5;
266
+ const heightChanged = Math.abs(containerSize.height - height) > 5;
267
+
268
+ if (widthChanged || heightChanged) {
269
+ // Update container size state
270
+ containerSize.width = width;
271
+ containerSize.height = height;
272
+
273
+ if (chartDiv && chartDiv.children.length > 0) {
274
+ // Use Plotly.Plots.resize for better performance than full re-render
275
+ Plotly.Plots.resize(chartDiv);
276
+ }
277
+ }
278
+ }
279
+ }, 150);
255
280
  });
256
281
 
257
282
  resizeObserver.observe(chartDiv);
258
283
 
259
284
  // Clean up observer on component destroy
260
285
  return () => {
286
+ if (resizeTimeout) {
287
+ clearTimeout(resizeTimeout);
288
+ }
261
289
  resizeObserver.disconnect();
262
290
  };
263
291
  }
264
292
  });
265
293
 
266
- // React to prop changes - re-render chart when runtime controls change
294
+ // React to prop changes - debounce re-renders for better performance
267
295
  $effect(() => {
268
296
  // Watch these props and re-render when they change
269
- runtimeMAOverride;
270
- runtimeShowOriginal;
271
- data;
272
- markers;
297
+ const currentData = data;
298
+ const currentMarkers = markers;
299
+ const currentMAOverride = runtimeMAOverride;
300
+ const currentShowOriginal = runtimeShowOriginal;
301
+ const currentShowMarkers = runtimeShowMarkers;
302
+ const currentShowLegend = runtimeShowLegend;
273
303
 
274
304
  // Only re-render if chartDiv is already initialized
275
305
  if (chartDiv && chartDiv.children.length > 0) {
276
- renderChart();
306
+ // Use requestAnimationFrame to batch updates
307
+ requestAnimationFrame(() => {
308
+ renderChart();
309
+ });
277
310
  }
278
311
  });
279
312
  </script>
@@ -1,11 +1,5 @@
1
1
  import type { KPI } from './charts.model.js';
2
2
  export declare function processKPIData(data: any[], kpi: KPI): number[];
3
- /**
4
- * Calculate moving average for a series of values
5
- * @param values - Array of numeric values
6
- * @param window - Number of periods for MA calculation
7
- * @returns Array of moving average values (NaN for insufficient data points)
8
- */
9
3
  export declare function calculateMovingAverage(values: number[], window: number): number[];
10
4
  export declare function createTimeSeriesTrace(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2', colorIndex?: number): any;
11
5
  /**
@@ -15,9 +9,10 @@ export declare function createTimeSeriesTrace(data: any[], kpi: KPI, timestampFi
15
9
  * @param timestampField - Field name for timestamp column
16
10
  * @param yaxis - Which Y-axis to use ('y1' or 'y2')
17
11
  * @param colorIndex - Index for color selection
12
+ * @param precomputedTimestamps - Optional pre-extracted timestamps array (performance optimization)
18
13
  * @returns Array of traces (original + MA if configured)
19
14
  */
20
- export declare function createTimeSeriesTraceWithMA(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2', colorIndex?: number): any[];
15
+ export declare function createTimeSeriesTraceWithMA(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2', colorIndex?: number, precomputedTimestamps?: any[]): any[];
21
16
  export declare function getYAxisTitle(kpis: KPI[]): string;
22
17
  export declare function formatValue(value: number, scale: 'percent' | 'absolute', unit: string): string;
23
18
  export declare function createDefaultPlotlyLayout(title?: string): any;
@@ -11,13 +11,28 @@ const modernColors = [
11
11
  '#EC4899', // Pink
12
12
  '#6B7280' // Gray
13
13
  ];
14
+ // Cache for processed KPI data to avoid reprocessing on every render
15
+ const dataCache = new WeakMap();
14
16
  export function processKPIData(data, kpi) {
15
- return data
17
+ // Check cache first
18
+ let kpiCache = dataCache.get(data);
19
+ if (kpiCache && kpiCache.has(kpi.rawName)) {
20
+ return kpiCache.get(kpi.rawName);
21
+ }
22
+ // Process data
23
+ const processed = data
16
24
  .map(row => {
17
25
  const val = row[kpi.rawName];
18
26
  return typeof val === 'number' ? val : parseFloat(val);
19
27
  })
20
28
  .filter(val => !isNaN(val));
29
+ // Store in cache
30
+ if (!kpiCache) {
31
+ kpiCache = new Map();
32
+ dataCache.set(data, kpiCache);
33
+ }
34
+ kpiCache.set(kpi.rawName, processed);
35
+ return processed;
21
36
  }
22
37
  /**
23
38
  * Calculate moving average for a series of values
@@ -25,7 +40,15 @@ export function processKPIData(data, kpi) {
25
40
  * @param window - Number of periods for MA calculation
26
41
  * @returns Array of moving average values (NaN for insufficient data points)
27
42
  */
43
+ // Cache for moving average calculations
44
+ const maCache = new Map();
28
45
  export function calculateMovingAverage(values, window) {
46
+ // Create cache key from values length + window + first/last few values
47
+ const cacheKey = `${values.length}-${window}-${values[0]}-${values[values.length - 1]}`;
48
+ // Check cache
49
+ if (maCache.has(cacheKey)) {
50
+ return maCache.get(cacheKey);
51
+ }
29
52
  const result = [];
30
53
  for (let i = 0; i < values.length; i++) {
31
54
  if (i < window - 1) {
@@ -39,6 +62,14 @@ export function calculateMovingAverage(values, window) {
39
62
  result.push(sum / window);
40
63
  }
41
64
  }
65
+ // Store in cache (limit cache size to prevent memory leaks)
66
+ if (maCache.size > 100) {
67
+ const firstKey = maCache.keys().next().value;
68
+ if (firstKey !== undefined) {
69
+ maCache.delete(firstKey);
70
+ }
71
+ }
72
+ maCache.set(cacheKey, result);
42
73
  return result;
43
74
  }
44
75
  export function createTimeSeriesTrace(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1', colorIndex = 0) {
@@ -72,12 +103,13 @@ export function createTimeSeriesTrace(data, kpi, timestampField = 'TIMESTAMP', y
72
103
  * @param timestampField - Field name for timestamp column
73
104
  * @param yaxis - Which Y-axis to use ('y1' or 'y2')
74
105
  * @param colorIndex - Index for color selection
106
+ * @param precomputedTimestamps - Optional pre-extracted timestamps array (performance optimization)
75
107
  * @returns Array of traces (original + MA if configured)
76
108
  */
77
- export function createTimeSeriesTraceWithMA(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1', colorIndex = 0) {
109
+ export function createTimeSeriesTraceWithMA(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1', colorIndex = 0, precomputedTimestamps) {
78
110
  const traces = [];
79
111
  const values = processKPIData(data, kpi);
80
- const timestamps = data.map(row => row[timestampField]);
112
+ const timestamps = precomputedTimestamps || data.map(row => row[timestampField]);
81
113
  const traceColor = kpi.color || modernColors[colorIndex % modernColors.length];
82
114
  // Add original trace (unless explicitly disabled)
83
115
  if (!kpi.movingAverage || kpi.movingAverage.showOriginal !== false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",