@smartnet360/svelte-components 0.0.27 → 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.
@@ -27,9 +27,11 @@
27
27
  layoutMovingAverage?: MovingAverageConfig; // Layout-level MA config
28
28
  runtimeMAOverride?: MovingAverageConfig | null; // Runtime override from global controls
29
29
  runtimeShowOriginal?: boolean; // Runtime control for showing original lines
30
+ runtimeShowMarkers?: boolean; // Runtime control for showing markers (default: true)
31
+ runtimeShowLegend?: boolean; // Runtime control for showing legend (default: true)
30
32
  }
31
33
 
32
- let { chart, data, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, runtimeMAOverride, runtimeShowOriginal }: Props = $props();
34
+ let { chart, data, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true }: Props = $props();
33
35
 
34
36
  // Chart container div and state
35
37
  let chartDiv: HTMLElement;
@@ -66,12 +68,8 @@
66
68
  return output;
67
69
  }
68
70
 
69
- function renderChart() {
70
- if (!chartDiv || !data?.length) return;
71
-
72
- const traces: any[] = [];
73
- let colorIndex = 0;
74
-
71
+ // Memoize MA hierarchy resolution - only recalculate when inputs change
72
+ let resolvedKPIs = $derived.by(() => {
75
73
  // Helper function to apply MA with full hierarchy:
76
74
  // Runtime Override > KPI > Chart > Section > Layout (higher priority wins)
77
75
  const applyMovingAverageHierarchy = (kpi: any) => {
@@ -141,18 +139,31 @@
141
139
  return kpi;
142
140
  };
143
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
+
144
157
  // Add left Y-axis traces (with moving average support)
145
- chart.yLeft.forEach(kpi => {
146
- const kpiWithMA = applyMovingAverageHierarchy(kpi);
147
- const kpiTraces = createTimeSeriesTraceWithMA(data, kpiWithMA, 'TIMESTAMP', 'y1', colorIndex);
158
+ resolvedKPIs.left.forEach(kpi => {
159
+ const kpiTraces = createTimeSeriesTraceWithMA(data, kpi, 'TIMESTAMP', 'y1', colorIndex, timestamps);
148
160
  traces.push(...kpiTraces);
149
161
  colorIndex++;
150
162
  });
151
163
 
152
164
  // Add right Y-axis traces (with moving average support)
153
- chart.yRight.forEach(kpi => {
154
- const kpiWithMA = applyMovingAverageHierarchy(kpi);
155
- const kpiTraces = createTimeSeriesTraceWithMA(data, kpiWithMA, 'TIMESTAMP', 'y2', colorIndex);
165
+ resolvedKPIs.right.forEach(kpi => {
166
+ const kpiTraces = createTimeSeriesTraceWithMA(data, kpi, 'TIMESTAMP', 'y2', colorIndex, timestamps);
156
167
  traces.push(...kpiTraces);
157
168
  colorIndex++;
158
169
  });
@@ -182,10 +193,10 @@
182
193
  }
183
194
 
184
195
  // Merge external layout with defaults using deep merge
185
- // Use structuredClone for deep copy to prevent mutation of defaultLayout
186
- let finalLayout = structuredClone(
187
- plotlyLayout ? deepMerge(defaultLayout, plotlyLayout) : defaultLayout
188
- );
196
+ // Only deep merge if plotlyLayout is provided (optimization)
197
+ let finalLayout = plotlyLayout
198
+ ? deepMerge(defaultLayout, plotlyLayout)
199
+ : defaultLayout;
189
200
 
190
201
  // Apply size-based adaptations using helper
191
202
  finalLayout = adaptPlotlyLayout(
@@ -198,8 +209,16 @@
198
209
  { enableAdaptation }
199
210
  );
200
211
 
201
- // Add markers to the layout
202
- finalLayout = addMarkersToLayout(finalLayout, markers || [], containerSize, enableAdaptation);
212
+ // Add markers to the layout only if runtime control allows
213
+ if (runtimeShowMarkers) {
214
+ finalLayout = addMarkersToLayout(finalLayout, markers || [], containerSize, enableAdaptation);
215
+ }
216
+
217
+ // Apply legend visibility control
218
+ if (finalLayout.showlegend === undefined || finalLayout.showlegend) {
219
+ // Only override if legend would normally be shown
220
+ finalLayout.showlegend = runtimeShowLegend;
221
+ }
203
222
 
204
223
  const config = {
205
224
  responsive: true,
@@ -227,43 +246,67 @@
227
246
 
228
247
  renderChart();
229
248
 
230
- // Set up ResizeObserver to handle container size changes
249
+ // Set up ResizeObserver with debouncing to prevent excessive re-renders
231
250
  if (chartDiv && window.ResizeObserver) {
251
+ let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
252
+
232
253
  const resizeObserver = new ResizeObserver((entries) => {
233
- for (const entry of entries) {
234
- const { width, height } = entry.contentRect;
235
-
236
- // Update container size state
237
- containerSize.width = width;
238
- containerSize.height = height;
239
-
240
- if (chartDiv && chartDiv.children.length > 0) {
241
- // Re-render chart with new adaptive layout
242
- renderChart();
243
- }
254
+ // Clear previous timeout
255
+ if (resizeTimeout) {
256
+ clearTimeout(resizeTimeout);
244
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);
245
280
  });
246
281
 
247
282
  resizeObserver.observe(chartDiv);
248
283
 
249
284
  // Clean up observer on component destroy
250
285
  return () => {
286
+ if (resizeTimeout) {
287
+ clearTimeout(resizeTimeout);
288
+ }
251
289
  resizeObserver.disconnect();
252
290
  };
253
291
  }
254
292
  });
255
293
 
256
- // React to prop changes - re-render chart when runtime controls change
294
+ // React to prop changes - debounce re-renders for better performance
257
295
  $effect(() => {
258
296
  // Watch these props and re-render when they change
259
- runtimeMAOverride;
260
- runtimeShowOriginal;
261
- data;
262
- 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;
263
303
 
264
304
  // Only re-render if chartDiv is already initialized
265
305
  if (chartDiv && chartDiv.children.length > 0) {
266
- renderChart();
306
+ // Use requestAnimationFrame to batch updates
307
+ requestAnimationFrame(() => {
308
+ renderChart();
309
+ });
267
310
  }
268
311
  });
269
312
  </script>
@@ -10,6 +10,8 @@ interface Props {
10
10
  layoutMovingAverage?: MovingAverageConfig;
11
11
  runtimeMAOverride?: MovingAverageConfig | null;
12
12
  runtimeShowOriginal?: boolean;
13
+ runtimeShowMarkers?: boolean;
14
+ runtimeShowLegend?: boolean;
13
15
  }
14
16
  interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
15
17
  new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
@@ -60,6 +60,12 @@
60
60
  enabled: layout.movingAverage?.enabled ?? true,
61
61
  windowOverride: undefined,
62
62
  showOriginal: layout.movingAverage?.showOriginal ?? true
63
+ },
64
+ markers: {
65
+ enabled: true // Default to showing markers
66
+ },
67
+ legend: {
68
+ enabled: true // Default to showing legend
63
69
  }
64
70
  });
65
71
 
@@ -294,6 +300,8 @@
294
300
  layoutMovingAverage={layout.movingAverage}
295
301
  runtimeMAOverride={effectiveMAOverride}
296
302
  runtimeShowOriginal={globalControls.movingAverage?.showOriginal}
303
+ runtimeShowMarkers={globalControls.markers?.enabled}
304
+ runtimeShowLegend={globalControls.legend?.enabled}
297
305
  on:chartcontextmenu={(event) => handleChartContextMenu(event.detail, section)}
298
306
  />
299
307
  </div>
@@ -434,6 +442,8 @@
434
442
  layoutMovingAverage={layout.movingAverage}
435
443
  runtimeMAOverride={effectiveMAOverride}
436
444
  runtimeShowOriginal={globalControls.movingAverage?.showOriginal}
445
+ runtimeShowMarkers={globalControls.markers?.enabled}
446
+ runtimeShowLegend={globalControls.legend?.enabled}
437
447
  on:chartcontextmenu={(event) => handleChartContextMenu(event.detail, activeZoom.section)}
438
448
  />
439
449
  </div>
@@ -26,6 +26,26 @@
26
26
  }
27
27
  });
28
28
  }
29
+
30
+ function updateMarkers(updates: Partial<NonNullable<GlobalChartControls['markers']>>) {
31
+ onUpdate({
32
+ ...controls,
33
+ markers: {
34
+ ...controls.markers!,
35
+ ...updates
36
+ }
37
+ });
38
+ }
39
+
40
+ function updateLegend(updates: Partial<NonNullable<GlobalChartControls['legend']>>) {
41
+ onUpdate({
42
+ ...controls,
43
+ legend: {
44
+ ...controls.legend!,
45
+ ...updates
46
+ }
47
+ });
48
+ }
29
49
  </script>
30
50
 
31
51
  <div class="global-controls">
@@ -117,16 +137,39 @@
117
137
  {/if}
118
138
  </div>
119
139
  {/if}
120
-
121
- <!-- Future controls can be added here -->
122
- <!-- Example:
123
- <div class="control-group">
124
- <div class="form-check form-check-inline">
125
- <input type="checkbox" id="markersToggle" />
126
- <label for="markersToggle">Markers</label>
140
+
141
+ <!-- Markers Toggle -->
142
+ {#if controls.markers}
143
+ <div class="control-group">
144
+ <input
145
+ type="checkbox"
146
+ class="btn-check"
147
+ id="markersToggle"
148
+ checked={controls.markers.enabled}
149
+ onchange={() => updateMarkers({ enabled: !controls.markers!.enabled })}
150
+ />
151
+ <label class="btn btn-outline-secondary btn-sm" for="markersToggle">
152
+ Markers
153
+ </label>
127
154
  </div>
128
- </div>
129
- -->
155
+ {/if}
156
+
157
+ <!-- Legend Toggle -->
158
+ {#if controls.legend}
159
+ <div class="control-group">
160
+ <input
161
+ type="checkbox"
162
+ class="btn-check"
163
+ id="legendToggle"
164
+ checked={controls.legend.enabled}
165
+ onchange={() => updateLegend({ enabled: !controls.legend!.enabled })}
166
+ />
167
+ <label class="btn btn-outline-secondary btn-sm" for="legendToggle">
168
+ Legend
169
+ </label>
170
+ </div>
171
+ {/if}
172
+
130
173
  </div>
131
174
  </div>
132
175
 
@@ -147,12 +190,6 @@
147
190
  flex-wrap: wrap;
148
191
  }
149
192
 
150
- .controls-label {
151
- font-weight: 600;
152
- color: #495057;
153
- margin-right: 0.5rem;
154
- }
155
-
156
193
  .control-group {
157
194
  display: flex;
158
195
  align-items: center;
@@ -49,4 +49,10 @@ export interface GlobalChartControls {
49
49
  windowOverride?: number;
50
50
  showOriginal?: boolean;
51
51
  };
52
+ markers?: {
53
+ enabled: boolean;
54
+ };
55
+ legend?: {
56
+ enabled: boolean;
57
+ };
52
58
  }
@@ -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.27",
3
+ "version": "0.0.29",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",