@smartnet360/svelte-components 0.0.28 → 0.0.30

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.
@@ -19,6 +19,7 @@
19
19
  interface Props {
20
20
  chart: ChartModel;
21
21
  data: any[];
22
+ timestamps: any[]; // Pre-extracted timestamps from ChartComponent
22
23
  markers?: ChartMarker[]; // Global markers for all charts
23
24
  plotlyLayout?: any; // Optional custom Plotly layout for styling/theming
24
25
  enableAdaptation?: boolean; // Enable size-based adaptations (default: true)
@@ -31,7 +32,7 @@
31
32
  runtimeShowLegend?: boolean; // Runtime control for showing legend (default: true)
32
33
  }
33
34
 
34
- let { chart, data, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true }: Props = $props();
35
+ let { chart, data, timestamps, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true }: Props = $props();
35
36
 
36
37
  // Chart container div and state
37
38
  let chartDiv: HTMLElement;
@@ -68,12 +69,8 @@
68
69
  return output;
69
70
  }
70
71
 
71
- function renderChart() {
72
- if (!chartDiv || !data?.length) return;
73
-
74
- const traces: any[] = [];
75
- let colorIndex = 0;
76
-
72
+ // Memoize MA hierarchy resolution - only recalculate when inputs change
73
+ let resolvedKPIs = $derived.by(() => {
77
74
  // Helper function to apply MA with full hierarchy:
78
75
  // Runtime Override > KPI > Chart > Section > Layout (higher priority wins)
79
76
  const applyMovingAverageHierarchy = (kpi: any) => {
@@ -143,18 +140,28 @@
143
140
  return kpi;
144
141
  };
145
142
 
143
+ return {
144
+ left: chart.yLeft.map(applyMovingAverageHierarchy),
145
+ right: chart.yRight.map(applyMovingAverageHierarchy)
146
+ };
147
+ });
148
+
149
+ function renderChart() {
150
+ if (!chartDiv || !data?.length) return;
151
+
152
+ const traces: any[] = [];
153
+ let colorIndex = 0;
154
+
146
155
  // 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);
156
+ resolvedKPIs.left.forEach(kpi => {
157
+ const kpiTraces = createTimeSeriesTraceWithMA(data, kpi, 'TIMESTAMP', 'y1', colorIndex, timestamps);
150
158
  traces.push(...kpiTraces);
151
159
  colorIndex++;
152
160
  });
153
161
 
154
162
  // 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);
163
+ resolvedKPIs.right.forEach(kpi => {
164
+ const kpiTraces = createTimeSeriesTraceWithMA(data, kpi, 'TIMESTAMP', 'y2', colorIndex, timestamps);
158
165
  traces.push(...kpiTraces);
159
166
  colorIndex++;
160
167
  });
@@ -184,10 +191,10 @@
184
191
  }
185
192
 
186
193
  // 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
- );
194
+ // Only deep merge if plotlyLayout is provided (optimization)
195
+ let finalLayout = plotlyLayout
196
+ ? deepMerge(defaultLayout, plotlyLayout)
197
+ : defaultLayout;
191
198
 
192
199
  // Apply size-based adaptations using helper
193
200
  finalLayout = adaptPlotlyLayout(
@@ -237,43 +244,67 @@
237
244
 
238
245
  renderChart();
239
246
 
240
- // Set up ResizeObserver to handle container size changes
247
+ // Set up ResizeObserver with debouncing to prevent excessive re-renders
241
248
  if (chartDiv && window.ResizeObserver) {
249
+ let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
250
+
242
251
  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
- }
252
+ // Clear previous timeout
253
+ if (resizeTimeout) {
254
+ clearTimeout(resizeTimeout);
254
255
  }
256
+
257
+ // Debounce resize events - wait 150ms after last resize
258
+ resizeTimeout = setTimeout(() => {
259
+ for (const entry of entries) {
260
+ const { width, height } = entry.contentRect;
261
+
262
+ // Only re-render if size actually changed significantly (>5px)
263
+ const widthChanged = Math.abs(containerSize.width - width) > 5;
264
+ const heightChanged = Math.abs(containerSize.height - height) > 5;
265
+
266
+ if (widthChanged || heightChanged) {
267
+ // Update container size state
268
+ containerSize.width = width;
269
+ containerSize.height = height;
270
+
271
+ if (chartDiv && chartDiv.children.length > 0) {
272
+ // Use Plotly.Plots.resize for better performance than full re-render
273
+ Plotly.Plots.resize(chartDiv);
274
+ }
275
+ }
276
+ }
277
+ }, 150);
255
278
  });
256
279
 
257
280
  resizeObserver.observe(chartDiv);
258
281
 
259
282
  // Clean up observer on component destroy
260
283
  return () => {
284
+ if (resizeTimeout) {
285
+ clearTimeout(resizeTimeout);
286
+ }
261
287
  resizeObserver.disconnect();
262
288
  };
263
289
  }
264
290
  });
265
291
 
266
- // React to prop changes - re-render chart when runtime controls change
292
+ // React to prop changes - debounce re-renders for better performance
267
293
  $effect(() => {
268
294
  // Watch these props and re-render when they change
269
- runtimeMAOverride;
270
- runtimeShowOriginal;
271
- data;
272
- markers;
295
+ const currentData = data;
296
+ const currentMarkers = markers;
297
+ const currentMAOverride = runtimeMAOverride;
298
+ const currentShowOriginal = runtimeShowOriginal;
299
+ const currentShowMarkers = runtimeShowMarkers;
300
+ const currentShowLegend = runtimeShowLegend;
273
301
 
274
302
  // Only re-render if chartDiv is already initialized
275
303
  if (chartDiv && chartDiv.children.length > 0) {
276
- renderChart();
304
+ // Use requestAnimationFrame to batch updates
305
+ requestAnimationFrame(() => {
306
+ renderChart();
307
+ });
277
308
  }
278
309
  });
279
310
  </script>
@@ -2,6 +2,7 @@ import type { Chart as ChartModel, ChartMarker, MovingAverageConfig } from './ch
2
2
  interface Props {
3
3
  chart: ChartModel;
4
4
  data: any[];
5
+ timestamps: any[];
5
6
  markers?: ChartMarker[];
6
7
  plotlyLayout?: any;
7
8
  enableAdaptation?: boolean;
@@ -54,6 +54,9 @@
54
54
 
55
55
  let { layout, data, mode, markers, plotlyLayout, enableAdaptation = true, showGlobalControls = true }: Props = $props();
56
56
 
57
+ // Extract timestamps once at component level - all charts share the same time series
58
+ let timestamps = $derived(data.map(row => row['TIMESTAMP']));
59
+
57
60
  // Global runtime controls state - initialize from layout config
58
61
  let globalControls = $state<GlobalChartControls>({
59
62
  movingAverage: {
@@ -292,6 +295,7 @@
292
295
  <ChartCard
293
296
  {chart}
294
297
  {data}
298
+ {timestamps}
295
299
  {markers}
296
300
  {plotlyLayout}
297
301
  {enableAdaptation}
@@ -434,6 +438,7 @@
434
438
  <ChartCard
435
439
  chart={activeZoom.chart}
436
440
  {data}
441
+ {timestamps}
437
442
  {markers}
438
443
  {plotlyLayout}
439
444
  {enableAdaptation}
@@ -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.30",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",