@smartnet360/svelte-components 0.0.20 → 0.0.22
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 +98 -9
- package/dist/core/Charts/ChartCard.svelte.d.ts +5 -1
- package/dist/core/Charts/ChartComponent.svelte +170 -34
- package/dist/core/Charts/ChartComponent.svelte.d.ts +1 -0
- package/dist/core/Charts/GlobalControls.svelte +188 -0
- package/dist/core/Charts/GlobalControls.svelte.d.ts +8 -0
- package/dist/core/Charts/charts.model.d.ts +17 -0
- package/dist/core/Charts/data-utils.d.ts +17 -0
- package/dist/core/Charts/data-utils.js +73 -0
- package/package.json +1 -1
@@ -3,8 +3,8 @@
|
|
3
3
|
<script lang="ts">
|
4
4
|
import { onMount, createEventDispatcher } from 'svelte';
|
5
5
|
import Plotly from 'plotly.js-dist-min';
|
6
|
-
import type { Chart as ChartModel, ChartMarker } from './charts.model.js';
|
7
|
-
import {
|
6
|
+
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig } from './charts.model.js';
|
7
|
+
import { createTimeSeriesTraceWithMA, getYAxisTitle, createDefaultPlotlyLayout } from './data-utils.js';
|
8
8
|
import { adaptPlotlyLayout, addMarkersToLayout, type ContainerSize } from './adapt.js';
|
9
9
|
|
10
10
|
const dispatch = createEventDispatcher<{
|
@@ -23,9 +23,13 @@
|
|
23
23
|
plotlyLayout?: any; // Optional custom Plotly layout for styling/theming
|
24
24
|
enableAdaptation?: boolean; // Enable size-based adaptations (default: true)
|
25
25
|
sectionId?: string;
|
26
|
+
sectionMovingAverage?: MovingAverageConfig; // Section-level MA config
|
27
|
+
layoutMovingAverage?: MovingAverageConfig; // Layout-level MA config
|
28
|
+
runtimeMAOverride?: MovingAverageConfig | null; // Runtime override from global controls
|
29
|
+
runtimeShowOriginal?: boolean; // Runtime control for showing original lines
|
26
30
|
}
|
27
31
|
|
28
|
-
let { chart, data, markers, plotlyLayout, enableAdaptation = true, sectionId }: Props = $props();
|
32
|
+
let { chart, data, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, runtimeMAOverride, runtimeShowOriginal }: Props = $props();
|
29
33
|
|
30
34
|
// Chart container div and state
|
31
35
|
let chartDiv: HTMLElement;
|
@@ -68,17 +72,88 @@
|
|
68
72
|
const traces: any[] = [];
|
69
73
|
let colorIndex = 0;
|
70
74
|
|
71
|
-
//
|
75
|
+
// Helper function to apply MA with full hierarchy:
|
76
|
+
// Runtime Override > KPI > Chart > Section > Layout (higher priority wins)
|
77
|
+
const applyMovingAverageHierarchy = (kpi: any) => {
|
78
|
+
// Step 1: Get base MA config from hierarchy (KPI > Chart > Section > Layout)
|
79
|
+
let baseMA: MovingAverageConfig | undefined;
|
80
|
+
|
81
|
+
if (kpi.movingAverage) {
|
82
|
+
baseMA = kpi.movingAverage;
|
83
|
+
} else if (chart.movingAverage) {
|
84
|
+
baseMA = chart.movingAverage;
|
85
|
+
} else if (sectionMovingAverage) {
|
86
|
+
baseMA = sectionMovingAverage;
|
87
|
+
} else if (layoutMovingAverage) {
|
88
|
+
baseMA = layoutMovingAverage;
|
89
|
+
}
|
90
|
+
|
91
|
+
// Step 2: Apply runtime overrides (highest priority)
|
92
|
+
if (runtimeMAOverride !== undefined && runtimeMAOverride !== null) {
|
93
|
+
// Runtime override explicitly disables MA
|
94
|
+
if (runtimeMAOverride.enabled === false) {
|
95
|
+
return {
|
96
|
+
...kpi,
|
97
|
+
movingAverage: { enabled: false, window: 7, showOriginal: true }
|
98
|
+
};
|
99
|
+
}
|
100
|
+
|
101
|
+
// Runtime override provides window - merge with base or use override
|
102
|
+
if (baseMA) {
|
103
|
+
return {
|
104
|
+
...kpi,
|
105
|
+
movingAverage: {
|
106
|
+
enabled: runtimeMAOverride.enabled ?? baseMA.enabled,
|
107
|
+
window: runtimeMAOverride.window ?? baseMA.window,
|
108
|
+
showOriginal: runtimeShowOriginal ?? baseMA.showOriginal,
|
109
|
+
label: baseMA.label
|
110
|
+
}
|
111
|
+
};
|
112
|
+
} else {
|
113
|
+
// No base config, use runtime override entirely
|
114
|
+
return {
|
115
|
+
...kpi,
|
116
|
+
movingAverage: runtimeMAOverride
|
117
|
+
};
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
// Step 3: Apply showOriginal runtime control if set
|
122
|
+
if (baseMA && runtimeShowOriginal !== undefined) {
|
123
|
+
return {
|
124
|
+
...kpi,
|
125
|
+
movingAverage: {
|
126
|
+
...baseMA,
|
127
|
+
showOriginal: runtimeShowOriginal
|
128
|
+
}
|
129
|
+
};
|
130
|
+
}
|
131
|
+
|
132
|
+
// Step 4: Return KPI with base MA or no MA
|
133
|
+
if (baseMA) {
|
134
|
+
return {
|
135
|
+
...kpi,
|
136
|
+
movingAverage: baseMA
|
137
|
+
};
|
138
|
+
}
|
139
|
+
|
140
|
+
// No MA configuration at any level
|
141
|
+
return kpi;
|
142
|
+
};
|
143
|
+
|
144
|
+
// Add left Y-axis traces (with moving average support)
|
72
145
|
chart.yLeft.forEach(kpi => {
|
73
|
-
const
|
74
|
-
|
146
|
+
const kpiWithMA = applyMovingAverageHierarchy(kpi);
|
147
|
+
const kpiTraces = createTimeSeriesTraceWithMA(data, kpiWithMA, 'TIMESTAMP', 'y1', colorIndex);
|
148
|
+
traces.push(...kpiTraces);
|
75
149
|
colorIndex++;
|
76
150
|
});
|
77
151
|
|
78
|
-
// Add right Y-axis traces
|
152
|
+
// Add right Y-axis traces (with moving average support)
|
79
153
|
chart.yRight.forEach(kpi => {
|
80
|
-
const
|
81
|
-
|
154
|
+
const kpiWithMA = applyMovingAverageHierarchy(kpi);
|
155
|
+
const kpiTraces = createTimeSeriesTraceWithMA(data, kpiWithMA, 'TIMESTAMP', 'y2', colorIndex);
|
156
|
+
traces.push(...kpiTraces);
|
82
157
|
colorIndex++;
|
83
158
|
});
|
84
159
|
|
@@ -177,6 +252,20 @@
|
|
177
252
|
};
|
178
253
|
}
|
179
254
|
});
|
255
|
+
|
256
|
+
// React to prop changes - re-render chart when runtime controls change
|
257
|
+
$effect(() => {
|
258
|
+
// Watch these props and re-render when they change
|
259
|
+
runtimeMAOverride;
|
260
|
+
runtimeShowOriginal;
|
261
|
+
data;
|
262
|
+
markers;
|
263
|
+
|
264
|
+
// Only re-render if chartDiv is already initialized
|
265
|
+
if (chartDiv && chartDiv.children.length > 0) {
|
266
|
+
renderChart();
|
267
|
+
}
|
268
|
+
});
|
180
269
|
</script>
|
181
270
|
|
182
271
|
<div class="chart-card" role="group" oncontextmenu={handleContextMenu}>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import type { Chart as ChartModel, ChartMarker } from './charts.model.js';
|
1
|
+
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig } from './charts.model.js';
|
2
2
|
interface Props {
|
3
3
|
chart: ChartModel;
|
4
4
|
data: any[];
|
@@ -6,6 +6,10 @@ interface Props {
|
|
6
6
|
plotlyLayout?: any;
|
7
7
|
enableAdaptation?: boolean;
|
8
8
|
sectionId?: string;
|
9
|
+
sectionMovingAverage?: MovingAverageConfig;
|
10
|
+
layoutMovingAverage?: MovingAverageConfig;
|
11
|
+
runtimeMAOverride?: MovingAverageConfig | null;
|
12
|
+
runtimeShowOriginal?: boolean;
|
9
13
|
}
|
10
14
|
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> {
|
11
15
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
@@ -2,8 +2,9 @@
|
|
2
2
|
|
3
3
|
<script lang="ts">
|
4
4
|
import { onMount } from 'svelte';
|
5
|
-
import type { Layout, Mode, ChartMarker, Section, ChartGrid, Chart } from './charts.model.js';
|
5
|
+
import type { Layout, Mode, ChartMarker, Section, ChartGrid, Chart, GlobalChartControls, MovingAverageConfig } from './charts.model.js';
|
6
6
|
import ChartCard from './ChartCard.svelte';
|
7
|
+
import GlobalControls from './GlobalControls.svelte';
|
7
8
|
|
8
9
|
interface Props {
|
9
10
|
layout: Layout;
|
@@ -12,6 +13,7 @@
|
|
12
13
|
markers?: ChartMarker[]; // Global markers for all charts
|
13
14
|
plotlyLayout?: any; // Optional custom Plotly layout
|
14
15
|
enableAdaptation?: boolean; // Enable size-based adaptations
|
16
|
+
showGlobalControls?: boolean; // Show/hide global controls section (default: true)
|
15
17
|
}
|
16
18
|
|
17
19
|
const GRID_DIMENSIONS: Record<ChartGrid, { rows: number; columns: number }> = {
|
@@ -50,7 +52,49 @@
|
|
50
52
|
section: Section;
|
51
53
|
}
|
52
54
|
|
53
|
-
let { layout, data, mode, markers, plotlyLayout, enableAdaptation = true }: Props = $props();
|
55
|
+
let { layout, data, mode, markers, plotlyLayout, enableAdaptation = true, showGlobalControls = true }: Props = $props();
|
56
|
+
|
57
|
+
// Global runtime controls state
|
58
|
+
let globalControls = $state<GlobalChartControls>({
|
59
|
+
movingAverage: {
|
60
|
+
enabled: true,
|
61
|
+
windowOverride: undefined,
|
62
|
+
showOriginal: true
|
63
|
+
}
|
64
|
+
});
|
65
|
+
|
66
|
+
// Handler for global controls updates
|
67
|
+
function handleControlsUpdate(updatedControls: GlobalChartControls) {
|
68
|
+
globalControls = updatedControls;
|
69
|
+
}
|
70
|
+
|
71
|
+
// Toggle state for showing/hiding the controls panel
|
72
|
+
let showControlsPanel = $state(true);
|
73
|
+
|
74
|
+
// Derived reactive state for MA override - automatically updates when globalControls changes
|
75
|
+
let effectiveMAOverride = $derived.by(() => {
|
76
|
+
const maControls = globalControls.movingAverage;
|
77
|
+
|
78
|
+
if (!maControls) return null;
|
79
|
+
|
80
|
+
// If MA is disabled globally, return disabled config
|
81
|
+
if (!maControls.enabled) {
|
82
|
+
return { enabled: false, window: 7, showOriginal: true };
|
83
|
+
}
|
84
|
+
|
85
|
+
// If window override is set, return override config
|
86
|
+
if (maControls.windowOverride !== undefined) {
|
87
|
+
return {
|
88
|
+
enabled: true,
|
89
|
+
window: maControls.windowOverride,
|
90
|
+
showOriginal: maControls.showOriginal ?? true
|
91
|
+
};
|
92
|
+
}
|
93
|
+
|
94
|
+
// If only showOriginal is different, we need to pass that through
|
95
|
+
// Return null to use configured values (handled in ChartCard)
|
96
|
+
return null;
|
97
|
+
});
|
54
98
|
|
55
99
|
// Internal tab state management
|
56
100
|
let activeTabId = $state(layout.sections[0]?.id || '');
|
@@ -246,6 +290,10 @@
|
|
246
290
|
{plotlyLayout}
|
247
291
|
{enableAdaptation}
|
248
292
|
sectionId={section.id}
|
293
|
+
sectionMovingAverage={section.movingAverage}
|
294
|
+
layoutMovingAverage={layout.movingAverage}
|
295
|
+
runtimeMAOverride={effectiveMAOverride}
|
296
|
+
runtimeShowOriginal={globalControls.movingAverage?.showOriginal}
|
249
297
|
on:chartcontextmenu={(event) => handleChartContextMenu(event.detail, section)}
|
250
298
|
/>
|
251
299
|
</div>
|
@@ -254,28 +302,50 @@
|
|
254
302
|
{/snippet}
|
255
303
|
|
256
304
|
<div class="chart-component" bind:this={componentElement}>
|
305
|
+
<!-- Global Controls Section (appears above tabs/scrollspy) -->
|
306
|
+
{#if showGlobalControls && showControlsPanel}
|
307
|
+
<GlobalControls controls={globalControls} onUpdate={handleControlsUpdate} />
|
308
|
+
{/if}
|
309
|
+
|
257
310
|
<!-- Always render the main content (tabs or scrollspy) -->
|
258
311
|
{#if mode === 'tabs'}
|
259
312
|
<!-- Tab Mode with Navigation -->
|
260
313
|
<div class="tabs-container">
|
261
|
-
<!-- Tab Navigation -->
|
262
|
-
<
|
263
|
-
|
264
|
-
|
265
|
-
<
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
314
|
+
<!-- Tab Navigation with Controls Toggle -->
|
315
|
+
<div class="nav-tabs-wrapper">
|
316
|
+
<ul class="nav nav-tabs" role="tablist">
|
317
|
+
{#each layout.sections as section, index}
|
318
|
+
<li class="nav-item" role="presentation">
|
319
|
+
<button
|
320
|
+
class="nav-link {section.id === activeTabId ? 'active' : ''}"
|
321
|
+
id="{section.id}-tab"
|
322
|
+
type="button"
|
323
|
+
role="tab"
|
324
|
+
aria-controls="{section.id}"
|
325
|
+
aria-selected="{section.id === activeTabId}"
|
326
|
+
onclick={() => activeTabId = section.id}
|
327
|
+
>
|
328
|
+
{section.title}
|
329
|
+
</button>
|
330
|
+
</li>
|
331
|
+
{/each}
|
332
|
+
</ul>
|
333
|
+
|
334
|
+
<!-- Controls Toggle Button -->
|
335
|
+
{#if showGlobalControls}
|
336
|
+
<button
|
337
|
+
class="btn btn-sm btn-outline-secondary controls-toggle"
|
338
|
+
onclick={() => showControlsPanel = !showControlsPanel}
|
339
|
+
title={showControlsPanel ? "Hide Controls" : "Show Controls"}
|
340
|
+
type="button"
|
341
|
+
>
|
342
|
+
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
343
|
+
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
344
|
+
</svg>
|
345
|
+
<span class="ms-1">{showControlsPanel ? 'Hide' : 'Show'} Controls</span>
|
346
|
+
</button>
|
347
|
+
{/if}
|
348
|
+
</div>
|
279
349
|
|
280
350
|
<!-- Tab Content -->
|
281
351
|
<div class="tab-content">
|
@@ -293,21 +363,38 @@
|
|
293
363
|
{:else if mode === 'scrollspy'}
|
294
364
|
<!-- ScrollSpy Mode with Navigation -->
|
295
365
|
<div class="scrollspy-container">
|
296
|
-
<!-- ScrollSpy Navigation -->
|
366
|
+
<!-- ScrollSpy Navigation with Controls Toggle -->
|
297
367
|
<nav class="scrollspy-nav">
|
298
|
-
<
|
299
|
-
|
300
|
-
|
301
|
-
<
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
368
|
+
<div class="nav-wrapper">
|
369
|
+
<ul class="nav nav-pills">
|
370
|
+
{#each layout.sections as section}
|
371
|
+
<li class="nav-item">
|
372
|
+
<a
|
373
|
+
class="nav-link {section.id === activeSectionId ? 'active' : ''}"
|
374
|
+
href="#{section.id}"
|
375
|
+
onclick={(e) => scrollToSection(section.id, e)}
|
376
|
+
>
|
377
|
+
{section.title}
|
378
|
+
</a>
|
379
|
+
</li>
|
380
|
+
{/each}
|
381
|
+
</ul>
|
382
|
+
|
383
|
+
<!-- Controls Toggle Button -->
|
384
|
+
{#if showGlobalControls}
|
385
|
+
<button
|
386
|
+
class="btn btn-sm btn-outline-secondary controls-toggle"
|
387
|
+
onclick={() => showControlsPanel = !showControlsPanel}
|
388
|
+
title={showControlsPanel ? "Hide Controls" : "Show Controls"}
|
389
|
+
type="button"
|
390
|
+
>
|
391
|
+
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
392
|
+
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
393
|
+
</svg>
|
394
|
+
<span class="ms-1">{showControlsPanel ? 'Hide' : 'Show'} Controls</span>
|
395
|
+
</button>
|
396
|
+
{/if}
|
397
|
+
</div>
|
311
398
|
</nav>
|
312
399
|
|
313
400
|
<!-- ScrollSpy Content -->
|
@@ -343,6 +430,10 @@
|
|
343
430
|
{plotlyLayout}
|
344
431
|
{enableAdaptation}
|
345
432
|
sectionId={activeZoom.section.id}
|
433
|
+
sectionMovingAverage={activeZoom.section.movingAverage}
|
434
|
+
layoutMovingAverage={layout.movingAverage}
|
435
|
+
runtimeMAOverride={effectiveMAOverride}
|
436
|
+
runtimeShowOriginal={globalControls.movingAverage?.showOriginal}
|
346
437
|
on:chartcontextmenu={(event) => handleChartContextMenu(event.detail, activeZoom.section)}
|
347
438
|
/>
|
348
439
|
</div>
|
@@ -589,4 +680,49 @@
|
|
589
680
|
min-height: 80px; /* Even smaller minimum for grid */
|
590
681
|
max-height: 100%; /* Constrain to slot */
|
591
682
|
}
|
683
|
+
|
684
|
+
/* Tab navigation wrapper with toggle button */
|
685
|
+
.nav-tabs-wrapper {
|
686
|
+
display: flex;
|
687
|
+
align-items: center;
|
688
|
+
gap: 1rem;
|
689
|
+
border-bottom: 1px solid #dee2e6;
|
690
|
+
}
|
691
|
+
|
692
|
+
.nav-tabs-wrapper .nav-tabs {
|
693
|
+
flex-grow: 1;
|
694
|
+
border-bottom: none;
|
695
|
+
margin-bottom: 0;
|
696
|
+
}
|
697
|
+
|
698
|
+
.controls-toggle {
|
699
|
+
display: flex;
|
700
|
+
align-items: center;
|
701
|
+
gap: 0.25rem;
|
702
|
+
white-space: nowrap;
|
703
|
+
border-radius: 0.25rem;
|
704
|
+
transition: all 0.2s ease;
|
705
|
+
}
|
706
|
+
|
707
|
+
.controls-toggle:hover {
|
708
|
+
background-color: #f8f9fa;
|
709
|
+
border-color: #6c757d;
|
710
|
+
}
|
711
|
+
|
712
|
+
.controls-toggle svg {
|
713
|
+
flex-shrink: 0;
|
714
|
+
}
|
715
|
+
|
716
|
+
/* ScrollSpy navigation wrapper with toggle button */
|
717
|
+
.scrollspy-nav .nav-wrapper {
|
718
|
+
display: flex;
|
719
|
+
align-items: center;
|
720
|
+
gap: 1rem;
|
721
|
+
}
|
722
|
+
|
723
|
+
.scrollspy-nav .nav-wrapper .nav {
|
724
|
+
flex-grow: 1;
|
725
|
+
}
|
592
726
|
</style>
|
727
|
+
|
728
|
+
|
@@ -6,6 +6,7 @@ interface Props {
|
|
6
6
|
markers?: ChartMarker[];
|
7
7
|
plotlyLayout?: any;
|
8
8
|
enableAdaptation?: boolean;
|
9
|
+
showGlobalControls?: boolean;
|
9
10
|
}
|
10
11
|
declare const ChartComponent: import("svelte").Component<Props, {}, "">;
|
11
12
|
type ChartComponent = ReturnType<typeof ChartComponent>;
|
@@ -0,0 +1,188 @@
|
|
1
|
+
<svelte:options runes={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
import type { GlobalChartControls } from './charts.model.js';
|
5
|
+
|
6
|
+
interface Props {
|
7
|
+
controls: GlobalChartControls;
|
8
|
+
onUpdate: (controls: GlobalChartControls) => void;
|
9
|
+
}
|
10
|
+
|
11
|
+
let { controls, onUpdate }: Props = $props();
|
12
|
+
|
13
|
+
function updateControls(updates: Partial<GlobalChartControls>) {
|
14
|
+
onUpdate({
|
15
|
+
...controls,
|
16
|
+
...updates
|
17
|
+
});
|
18
|
+
}
|
19
|
+
|
20
|
+
function updateMovingAverage(updates: Partial<NonNullable<GlobalChartControls['movingAverage']>>) {
|
21
|
+
onUpdate({
|
22
|
+
...controls,
|
23
|
+
movingAverage: {
|
24
|
+
...controls.movingAverage!,
|
25
|
+
...updates
|
26
|
+
}
|
27
|
+
});
|
28
|
+
}
|
29
|
+
</script>
|
30
|
+
|
31
|
+
<div class="global-controls">
|
32
|
+
<div class="controls-section">
|
33
|
+
<!-- <span class="controls-label">Display Controls:</span> -->
|
34
|
+
|
35
|
+
<!-- Moving Average Controls -->
|
36
|
+
{#if controls.movingAverage}
|
37
|
+
<div class="control-group">
|
38
|
+
<!-- MA Enable Toggle Button -->
|
39
|
+
<input
|
40
|
+
type="checkbox"
|
41
|
+
class="btn-check"
|
42
|
+
id="maToggle"
|
43
|
+
checked={controls.movingAverage.enabled}
|
44
|
+
onchange={() => updateMovingAverage({ enabled: !controls.movingAverage!.enabled })}
|
45
|
+
/>
|
46
|
+
<label class="btn btn-outline-primary btn-sm" for="maToggle">
|
47
|
+
Moving Average
|
48
|
+
</label>
|
49
|
+
|
50
|
+
{#if controls.movingAverage.enabled}
|
51
|
+
<div class="control-subgroup">
|
52
|
+
<!-- MA Window Size -->
|
53
|
+
<div class="btn-group btn-group-sm" role="group" aria-label="MA Window">
|
54
|
+
<input
|
55
|
+
type="radio"
|
56
|
+
class="btn-check"
|
57
|
+
name="maWindow"
|
58
|
+
id="maWindowAuto"
|
59
|
+
checked={controls.movingAverage.windowOverride === undefined}
|
60
|
+
onchange={() => updateMovingAverage({ windowOverride: undefined })}
|
61
|
+
/>
|
62
|
+
<label class="btn btn-outline-primary" for="maWindowAuto">Auto</label>
|
63
|
+
|
64
|
+
<input
|
65
|
+
type="radio"
|
66
|
+
class="btn-check"
|
67
|
+
name="maWindow"
|
68
|
+
id="maWindow7"
|
69
|
+
checked={controls.movingAverage.windowOverride === 7}
|
70
|
+
onchange={() => updateMovingAverage({ windowOverride: 7 })}
|
71
|
+
/>
|
72
|
+
<label class="btn btn-outline-primary" for="maWindow7">7</label>
|
73
|
+
|
74
|
+
<input
|
75
|
+
type="radio"
|
76
|
+
class="btn-check"
|
77
|
+
name="maWindow"
|
78
|
+
id="maWindow14"
|
79
|
+
checked={controls.movingAverage.windowOverride === 14}
|
80
|
+
onchange={() => updateMovingAverage({ windowOverride: 14 })}
|
81
|
+
/>
|
82
|
+
<label class="btn btn-outline-primary" for="maWindow14">14</label>
|
83
|
+
|
84
|
+
<input
|
85
|
+
type="radio"
|
86
|
+
class="btn-check"
|
87
|
+
name="maWindow"
|
88
|
+
id="maWindow24"
|
89
|
+
checked={controls.movingAverage.windowOverride === 24}
|
90
|
+
onchange={() => updateMovingAverage({ windowOverride: 24 })}
|
91
|
+
/>
|
92
|
+
<label class="btn btn-outline-primary" for="maWindow24">24</label>
|
93
|
+
|
94
|
+
<input
|
95
|
+
type="radio"
|
96
|
+
class="btn-check"
|
97
|
+
name="maWindow"
|
98
|
+
id="maWindow30"
|
99
|
+
checked={controls.movingAverage.windowOverride === 30}
|
100
|
+
onchange={() => updateMovingAverage({ windowOverride: 30 })}
|
101
|
+
/>
|
102
|
+
<label class="btn btn-outline-primary" for="maWindow30">30</label>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<!-- Show Original Toggle Button -->
|
106
|
+
<input
|
107
|
+
type="checkbox"
|
108
|
+
class="btn-check"
|
109
|
+
id="showOriginal"
|
110
|
+
checked={controls.movingAverage.showOriginal}
|
111
|
+
onchange={() => updateMovingAverage({ showOriginal: !controls.movingAverage!.showOriginal })}
|
112
|
+
/>
|
113
|
+
<label class="btn btn-outline-primary btn-sm ms-2" for="showOriginal">
|
114
|
+
Show Original
|
115
|
+
</label>
|
116
|
+
</div>
|
117
|
+
{/if}
|
118
|
+
</div>
|
119
|
+
{/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>
|
127
|
+
</div>
|
128
|
+
</div>
|
129
|
+
-->
|
130
|
+
</div>
|
131
|
+
</div>
|
132
|
+
|
133
|
+
<style>
|
134
|
+
/* Global Controls Section */
|
135
|
+
.global-controls {
|
136
|
+
flex-shrink: 0;
|
137
|
+
background-color: #f8f9fa;
|
138
|
+
border-bottom: 1px solid #dee2e6;
|
139
|
+
padding: 0.5rem 1rem;
|
140
|
+
font-size: 0.875rem;
|
141
|
+
}
|
142
|
+
|
143
|
+
.controls-section {
|
144
|
+
display: flex;
|
145
|
+
align-items: center;
|
146
|
+
gap: 1.5rem;
|
147
|
+
flex-wrap: wrap;
|
148
|
+
}
|
149
|
+
|
150
|
+
.controls-label {
|
151
|
+
font-weight: 600;
|
152
|
+
color: #495057;
|
153
|
+
margin-right: 0.5rem;
|
154
|
+
}
|
155
|
+
|
156
|
+
.control-group {
|
157
|
+
display: flex;
|
158
|
+
align-items: center;
|
159
|
+
gap: 0.75rem;
|
160
|
+
}
|
161
|
+
|
162
|
+
.control-subgroup {
|
163
|
+
display: flex;
|
164
|
+
align-items: center;
|
165
|
+
gap: 0.5rem;
|
166
|
+
padding-left: 0.5rem;
|
167
|
+
border-left: 2px solid #dee2e6;
|
168
|
+
}
|
169
|
+
|
170
|
+
.global-controls :global(.btn-group-sm .btn) {
|
171
|
+
padding: 0.25rem 0.75rem;
|
172
|
+
font-size: 0.75rem;
|
173
|
+
font-weight: 500;
|
174
|
+
}
|
175
|
+
|
176
|
+
/* Responsive adjustments */
|
177
|
+
@media (max-width: 768px) {
|
178
|
+
.controls-section {
|
179
|
+
flex-direction: column;
|
180
|
+
align-items: flex-start;
|
181
|
+
gap: 0.75rem;
|
182
|
+
}
|
183
|
+
|
184
|
+
.control-subgroup {
|
185
|
+
flex-wrap: wrap;
|
186
|
+
}
|
187
|
+
}
|
188
|
+
</style>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import type { GlobalChartControls } from './charts.model.js';
|
2
|
+
interface Props {
|
3
|
+
controls: GlobalChartControls;
|
4
|
+
onUpdate: (controls: GlobalChartControls) => void;
|
5
|
+
}
|
6
|
+
declare const GlobalControls: import("svelte").Component<Props, {}, "">;
|
7
|
+
type GlobalControls = ReturnType<typeof GlobalControls>;
|
8
|
+
export default GlobalControls;
|
@@ -1,10 +1,17 @@
|
|
1
1
|
export type Scale = "percent" | "absolute";
|
2
|
+
export interface MovingAverageConfig {
|
3
|
+
enabled: boolean;
|
4
|
+
window: number;
|
5
|
+
showOriginal?: boolean;
|
6
|
+
label?: string;
|
7
|
+
}
|
2
8
|
export interface KPI {
|
3
9
|
rawName: string;
|
4
10
|
name: string;
|
5
11
|
scale: Scale;
|
6
12
|
unit: string;
|
7
13
|
color?: string;
|
14
|
+
movingAverage?: MovingAverageConfig;
|
8
15
|
}
|
9
16
|
export type ChartPosition = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
10
17
|
export type ChartGrid = "2x2" | "3x3" | "4x4" | "1x2" | "1x4" | "1x8";
|
@@ -13,17 +20,20 @@ export interface Chart {
|
|
13
20
|
title: string;
|
14
21
|
yLeft: KPI[];
|
15
22
|
yRight: KPI[];
|
23
|
+
movingAverage?: MovingAverageConfig;
|
16
24
|
}
|
17
25
|
export interface Section {
|
18
26
|
id: string;
|
19
27
|
title: string;
|
20
28
|
charts: Chart[];
|
21
29
|
grid?: ChartGrid;
|
30
|
+
movingAverage?: MovingAverageConfig;
|
22
31
|
}
|
23
32
|
export type Mode = "tabs" | "scrollspy";
|
24
33
|
export interface Layout {
|
25
34
|
layoutName: string;
|
26
35
|
sections: Section[];
|
36
|
+
movingAverage?: MovingAverageConfig;
|
27
37
|
}
|
28
38
|
export interface ChartMarker {
|
29
39
|
date: string | Date;
|
@@ -33,3 +43,10 @@ export interface ChartMarker {
|
|
33
43
|
showLabel?: boolean;
|
34
44
|
category?: 'release' | 'incident' | 'maintenance' | 'other';
|
35
45
|
}
|
46
|
+
export interface GlobalChartControls {
|
47
|
+
movingAverage?: {
|
48
|
+
enabled: boolean;
|
49
|
+
windowOverride?: number;
|
50
|
+
showOriginal?: boolean;
|
51
|
+
};
|
52
|
+
}
|
@@ -1,6 +1,23 @@
|
|
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
|
+
export declare function calculateMovingAverage(values: number[], window: number): number[];
|
3
10
|
export declare function createTimeSeriesTrace(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2', colorIndex?: number): any;
|
11
|
+
/**
|
12
|
+
* Create time series trace(s) with optional moving average
|
13
|
+
* @param data - Raw data array
|
14
|
+
* @param kpi - KPI configuration (may include movingAverage config)
|
15
|
+
* @param timestampField - Field name for timestamp column
|
16
|
+
* @param yaxis - Which Y-axis to use ('y1' or 'y2')
|
17
|
+
* @param colorIndex - Index for color selection
|
18
|
+
* @returns Array of traces (original + MA if configured)
|
19
|
+
*/
|
20
|
+
export declare function createTimeSeriesTraceWithMA(data: any[], kpi: KPI, timestampField?: string, yaxis?: 'y1' | 'y2', colorIndex?: number): any[];
|
4
21
|
export declare function getYAxisTitle(kpis: KPI[]): string;
|
5
22
|
export declare function formatValue(value: number, scale: 'percent' | 'absolute', unit: string): string;
|
6
23
|
export declare function createDefaultPlotlyLayout(title?: string): any;
|
@@ -19,6 +19,28 @@ export function processKPIData(data, kpi) {
|
|
19
19
|
})
|
20
20
|
.filter(val => !isNaN(val));
|
21
21
|
}
|
22
|
+
/**
|
23
|
+
* Calculate moving average for a series of values
|
24
|
+
* @param values - Array of numeric values
|
25
|
+
* @param window - Number of periods for MA calculation
|
26
|
+
* @returns Array of moving average values (NaN for insufficient data points)
|
27
|
+
*/
|
28
|
+
export function calculateMovingAverage(values, window) {
|
29
|
+
const result = [];
|
30
|
+
for (let i = 0; i < values.length; i++) {
|
31
|
+
if (i < window - 1) {
|
32
|
+
// Not enough data points yet - use NaN to skip these points
|
33
|
+
result.push(NaN);
|
34
|
+
}
|
35
|
+
else {
|
36
|
+
// Calculate average of the window
|
37
|
+
const windowValues = values.slice(i - window + 1, i + 1);
|
38
|
+
const sum = windowValues.reduce((a, b) => a + b, 0);
|
39
|
+
result.push(sum / window);
|
40
|
+
}
|
41
|
+
}
|
42
|
+
return result;
|
43
|
+
}
|
22
44
|
export function createTimeSeriesTrace(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1', colorIndex = 0) {
|
23
45
|
const values = processKPIData(data, kpi);
|
24
46
|
const timestamps = data.map(row => row[timestampField]);
|
@@ -43,6 +65,57 @@ export function createTimeSeriesTrace(data, kpi, timestampField = 'TIMESTAMP', y
|
|
43
65
|
'<extra></extra>'
|
44
66
|
};
|
45
67
|
}
|
68
|
+
/**
|
69
|
+
* Create time series trace(s) with optional moving average
|
70
|
+
* @param data - Raw data array
|
71
|
+
* @param kpi - KPI configuration (may include movingAverage config)
|
72
|
+
* @param timestampField - Field name for timestamp column
|
73
|
+
* @param yaxis - Which Y-axis to use ('y1' or 'y2')
|
74
|
+
* @param colorIndex - Index for color selection
|
75
|
+
* @returns Array of traces (original + MA if configured)
|
76
|
+
*/
|
77
|
+
export function createTimeSeriesTraceWithMA(data, kpi, timestampField = 'TIMESTAMP', yaxis = 'y1', colorIndex = 0) {
|
78
|
+
const traces = [];
|
79
|
+
const values = processKPIData(data, kpi);
|
80
|
+
const timestamps = data.map(row => row[timestampField]);
|
81
|
+
const traceColor = kpi.color || modernColors[colorIndex % modernColors.length];
|
82
|
+
// Add original trace (unless explicitly disabled)
|
83
|
+
if (!kpi.movingAverage || kpi.movingAverage.showOriginal !== false) {
|
84
|
+
const originalTrace = createTimeSeriesTrace(data, kpi, timestampField, yaxis, colorIndex);
|
85
|
+
// If MA is enabled, make the original line slightly transparent
|
86
|
+
if (kpi.movingAverage?.enabled) {
|
87
|
+
originalTrace.opacity = 0.4;
|
88
|
+
originalTrace.line.width = 2;
|
89
|
+
}
|
90
|
+
traces.push(originalTrace);
|
91
|
+
}
|
92
|
+
// Add moving average trace if configured
|
93
|
+
if (kpi.movingAverage?.enabled) {
|
94
|
+
const maValues = calculateMovingAverage(values, kpi.movingAverage.window);
|
95
|
+
const maLabel = kpi.movingAverage.label ||
|
96
|
+
`${kpi.name} (MA${kpi.movingAverage.window})`;
|
97
|
+
const maTrace = {
|
98
|
+
x: timestamps,
|
99
|
+
y: maValues,
|
100
|
+
type: 'scatter',
|
101
|
+
mode: 'lines',
|
102
|
+
name: maLabel,
|
103
|
+
yaxis: yaxis,
|
104
|
+
line: {
|
105
|
+
color: traceColor,
|
106
|
+
width: 3,
|
107
|
+
shape: 'spline',
|
108
|
+
smoothing: 0.3,
|
109
|
+
dash: yaxis === 'y1' ? 'solid' : 'dot'
|
110
|
+
},
|
111
|
+
hovertemplate: `<b>${maLabel}</b><br>` +
|
112
|
+
`Value: %{y:,.2f} ${kpi.unit}<br>` +
|
113
|
+
'<extra></extra>'
|
114
|
+
};
|
115
|
+
traces.push(maTrace);
|
116
|
+
}
|
117
|
+
return traces;
|
118
|
+
}
|
46
119
|
export function getYAxisTitle(kpis) {
|
47
120
|
if (kpis.length === 0)
|
48
121
|
return '';
|