@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.
- package/dist/core/Charts/ChartCard.svelte +80 -37
- package/dist/core/Charts/ChartCard.svelte.d.ts +2 -0
- package/dist/core/Charts/ChartComponent.svelte +10 -0
- package/dist/core/Charts/GlobalControls.svelte +52 -15
- package/dist/core/Charts/charts.model.d.ts +6 -0
- package/dist/core/Charts/data-utils.d.ts +2 -7
- package/dist/core/Charts/data-utils.js +35 -3
- package/package.json +1 -1
|
@@ -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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
146
|
-
const
|
|
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
|
-
|
|
154
|
-
const
|
|
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
|
-
//
|
|
186
|
-
let finalLayout =
|
|
187
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
234
|
-
|
|
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-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
<!--
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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) {
|