@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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
148
|
-
const
|
|
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
|
-
|
|
156
|
-
const
|
|
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
|
-
//
|
|
188
|
-
let finalLayout =
|
|
189
|
-
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
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-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|