@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.
@@ -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 { createTimeSeriesTrace, getYAxisTitle, createDefaultPlotlyLayout } from './data-utils.js';
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
- // Add left Y-axis traces
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 trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y1', colorIndex);
74
- traces.push(trace);
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 trace = createTimeSeriesTrace(data, kpi, 'TIMESTAMP', 'y2', colorIndex);
81
- traces.push(trace);
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
- <ul class="nav nav-tabs" role="tablist">
263
- {#each layout.sections as section, index}
264
- <li class="nav-item" role="presentation">
265
- <button
266
- class="nav-link {section.id === activeTabId ? 'active' : ''}"
267
- id="{section.id}-tab"
268
- type="button"
269
- role="tab"
270
- aria-controls="{section.id}"
271
- aria-selected="{section.id === activeTabId}"
272
- onclick={() => activeTabId = section.id}
273
- >
274
- {section.title}
275
- </button>
276
- </li>
277
- {/each}
278
- </ul>
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
- <ul class="nav nav-pills">
299
- {#each layout.sections as section}
300
- <li class="nav-item">
301
- <a
302
- class="nav-link {section.id === activeSectionId ? 'active' : ''}"
303
- href="#{section.id}"
304
- onclick={(e) => scrollToSection(section.id, e)}
305
- >
306
- {section.title}
307
- </a>
308
- </li>
309
- {/each}
310
- </ul>
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 '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",