@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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
148
|
-
const
|
|
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
|
-
|
|
156
|
-
const
|
|
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
|
-
//
|
|
188
|
-
let finalLayout =
|
|
189
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
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-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
304
|
+
// Use requestAnimationFrame to batch updates
|
|
305
|
+
requestAnimationFrame(() => {
|
|
306
|
+
renderChart();
|
|
307
|
+
});
|
|
277
308
|
}
|
|
278
309
|
});
|
|
279
310
|
</script>
|
|
@@ -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
|
-
|
|
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) {
|