@smartnet360/svelte-components 0.0.42 → 0.0.44

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.
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { TreeNode } from '../../core/TreeView';
6
6
  import type { KPI, CellStylingConfig } from '../../core/Charts';
7
- import type { CellTrafficRecord } from './data-loader';
7
+ import type { CellTrafficRecord, TreeGroupingConfig } from './data-loader';
8
8
  /**
9
9
  * Extract band from cell name using regex pattern matching
10
10
  * @param cellName - Cell name like "LTE700_1", "NR3500_2", etc.
@@ -25,12 +25,16 @@ export declare function getBandFrequency(band: string | null): number;
25
25
  */
26
26
  export declare function sortCellsByBandFrequency(items: [string, CellTrafficRecord][]): [string, CellTrafficRecord][];
27
27
  /**
28
- * Build hierarchical tree structure: Site Sector (Azimuth) → Cell (Band)
28
+ * Build hierarchical tree structure with configurable grouping
29
+ * Supports both 2-level (level0 → cell) and 3-level (level0 → level1 → cell) trees
30
+ * @param data - Cell traffic records
31
+ * @param grouping - Tree grouping configuration (defaults to Site → Azimuth → Cell)
29
32
  */
30
- export declare function buildTreeNodes(data: CellTrafficRecord[]): TreeNode[];
33
+ export declare function buildTreeNodes(data: CellTrafficRecord[], grouping?: TreeGroupingConfig): TreeNode[];
31
34
  /**
32
35
  * Filter chart data based on selected tree paths
33
36
  * Only include cells that are checked in the tree
37
+ * Handles both 2-level (level0:cellName) and 3-level (level0:level1:cellName) paths
34
38
  */
35
39
  export declare function filterChartData(data: CellTrafficRecord[], checkedPaths: Set<string>): CellTrafficRecord[];
36
40
  /**
@@ -39,18 +43,15 @@ export declare function filterChartData(data: CellTrafficRecord[], checkedPaths:
39
43
  * Transforms from long format (many rows per cell) to wide format (one column per cell)
40
44
  *
41
45
  * @param data - Filtered cell traffic records
42
- * @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
46
+ * @param baseMetrics - Array of metric names to pivot (e.g., ['DL_GBYTES', 'UL_GBYTES'])
43
47
  */
44
48
  export declare function transformChartData(data: CellTrafficRecord[], baseMetrics: string[]): any[];
45
49
  /**
46
- * Apply cell styling based on band and sector
47
- * Modifies KPI objects to include color (from band) and lineStyle (from sector)
48
- * Updates KPI name to format: Band_Azimuth°
49
- *
50
- * @param metricName - Base metric name (e.g., 'dlGBytes')
51
- * @param cellRecord - Cell traffic record with band, sector, azimuth metadata
52
- * @param unit - Unit string for the metric
53
- * @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
54
- * @returns Styled KPI object
50
+ * Create a styled KPI with band colors and sector line styles
51
+ * @param metricName - Base metric name (e.g., 'DL_GBYTES')
52
+ * @param cellRecord - Cell traffic record with metadata
53
+ * @param unit - Unit string (e.g., 'GB', '%')
54
+ * @param stylingConfig - Optional styling configuration
55
+ * @returns KPI with cell-specific styling applied
55
56
  */
56
57
  export declare function createStyledKPI(metricName: string, cellRecord: CellTrafficRecord, unit: string, stylingConfig?: CellStylingConfig): KPI;
@@ -62,21 +62,34 @@ export function sortCellsByBandFrequency(items) {
62
62
  });
63
63
  }
64
64
  /**
65
- * Build hierarchical tree structure: Site Sector (Azimuth) → Cell (Band)
65
+ * Build hierarchical tree structure with configurable grouping
66
+ * Supports both 2-level (level0 → cell) and 3-level (level0 → level1 → cell) trees
67
+ * @param data - Cell traffic records
68
+ * @param grouping - Tree grouping configuration (defaults to Site → Azimuth → Cell)
66
69
  */
67
- export function buildTreeNodes(data) {
68
- log('🔄 Building tree nodes', { recordCount: data.length });
69
- // Group by site → azimuth → cell
70
- const siteMap = new Map();
70
+ export function buildTreeNodes(data, grouping = { level0: 'site', level1: 'azimuth', level2: 'cell' }) {
71
+ log('🔄 Building tree nodes', {
72
+ recordCount: data.length,
73
+ grouping,
74
+ treeDepth: grouping.level1 === null ? 2 : 3
75
+ });
76
+ // Check if this is a 2-level tree (no level1)
77
+ if (grouping.level1 === null) {
78
+ return build2LevelTree(data, grouping);
79
+ }
80
+ // 3-level tree: Group data by level0 → level1 → cell
81
+ const level0Map = new Map();
71
82
  data.forEach((record) => {
72
- if (!siteMap.has(record.siteName)) {
73
- siteMap.set(record.siteName, new Map());
83
+ const level0Value = getFieldValue(record, grouping.level0);
84
+ const level1Value = getFieldValue(record, grouping.level1); // We know level1 is not null here
85
+ if (!level0Map.has(level0Value)) {
86
+ level0Map.set(level0Value, new Map());
74
87
  }
75
- const azimuthMap = siteMap.get(record.siteName);
76
- if (!azimuthMap.has(record.azimuth)) {
77
- azimuthMap.set(record.azimuth, new Map());
88
+ const level1Map = level0Map.get(level0Value);
89
+ if (!level1Map.has(level1Value)) {
90
+ level1Map.set(level1Value, new Map());
78
91
  }
79
- const cellMap = azimuthMap.get(record.azimuth);
92
+ const cellMap = level1Map.get(level1Value);
80
93
  // Store one record per cell (we just need metadata, not all time series)
81
94
  if (!cellMap.has(record.cellName)) {
82
95
  cellMap.set(record.cellName, record);
@@ -84,35 +97,41 @@ export function buildTreeNodes(data) {
84
97
  });
85
98
  // Build tree structure
86
99
  const treeNodes = [];
87
- Array.from(siteMap.entries())
88
- .sort(([a], [b]) => a.localeCompare(b))
89
- .forEach(([siteName, azimuthMap]) => {
90
- const siteNode = {
91
- id: siteName, // Simple ID
92
- label: `Site ${siteName}`,
93
- // icon: '📡',
94
- metadata: { type: 'site', siteName },
100
+ Array.from(level0Map.entries())
101
+ .sort(([a], [b]) => compareValues(a, b))
102
+ .forEach(([level0Value, level1Map]) => {
103
+ const level0Node = {
104
+ id: String(level0Value),
105
+ label: formatNodeLabel(grouping.level0, level0Value),
106
+ metadata: {
107
+ type: grouping.level0,
108
+ value: level0Value,
109
+ grouping: grouping.level0
110
+ },
95
111
  defaultExpanded: false,
96
112
  defaultChecked: false, // Don't check parent nodes
97
113
  children: []
98
114
  };
99
- Array.from(azimuthMap.entries())
100
- .sort(([a], [b]) => a - b)
101
- .forEach(([azimuth, cellMap]) => {
102
- const sectorNode = {
103
- id: `${azimuth}`, // Simple ID (just azimuth)
104
- label: `${azimuth}° Sector`,
105
- // icon: '📍',
106
- metadata: { type: 'sector', azimuth, siteName },
115
+ Array.from(level1Map.entries())
116
+ .sort(([a], [b]) => compareValues(a, b))
117
+ .forEach(([level1Value, cellMap]) => {
118
+ const level1Node = {
119
+ id: String(level1Value),
120
+ label: formatNodeLabel(grouping.level1, level1Value), // We know level1 is not null here
121
+ metadata: {
122
+ type: grouping.level1,
123
+ value: level1Value,
124
+ grouping: grouping.level1
125
+ },
107
126
  defaultExpanded: false,
108
127
  defaultChecked: false, // Don't check parent nodes
109
128
  children: []
110
129
  };
111
- // Sort cells by band frequency (LTE700, LTE800, etc.)
130
+ // Sort cells by band frequency
112
131
  const sortedCells = sortCellsByBandFrequency(Array.from(cellMap.entries()));
113
132
  sortedCells.forEach(([cellName, record]) => {
114
133
  const cellNode = {
115
- id: cellName, // Simple ID (just cell name)
134
+ id: cellName,
116
135
  label: `${cellName} (${record.band})`,
117
136
  icon: getBandIcon(record.band),
118
137
  metadata: {
@@ -125,19 +144,124 @@ export function buildTreeNodes(data) {
125
144
  },
126
145
  defaultChecked: true
127
146
  };
128
- sectorNode.children.push(cellNode);
147
+ level1Node.children.push(cellNode);
129
148
  });
130
- siteNode.children.push(sectorNode);
149
+ level0Node.children.push(level1Node);
131
150
  });
132
- treeNodes.push(siteNode);
151
+ treeNodes.push(level0Node);
133
152
  });
134
153
  log('✅ Tree nodes built', {
135
154
  totalNodes: treeNodes.length,
136
- totalSites: siteMap.size,
137
- sampleSite: treeNodes[0]?.label
155
+ grouping,
156
+ sampleNode: treeNodes[0]?.label
138
157
  });
139
158
  return treeNodes;
140
159
  }
160
+ /**
161
+ * Build 2-level tree: level0 → cell (no middle level)
162
+ */
163
+ function build2LevelTree(data, grouping) {
164
+ // Group data by level0 → cell
165
+ const level0Map = new Map();
166
+ data.forEach((record) => {
167
+ const level0Value = getFieldValue(record, grouping.level0);
168
+ if (!level0Map.has(level0Value)) {
169
+ level0Map.set(level0Value, new Map());
170
+ }
171
+ const cellMap = level0Map.get(level0Value);
172
+ // Store one record per cell
173
+ if (!cellMap.has(record.cellName)) {
174
+ cellMap.set(record.cellName, record);
175
+ }
176
+ });
177
+ // Build tree structure
178
+ const treeNodes = [];
179
+ Array.from(level0Map.entries())
180
+ .sort(([a], [b]) => compareValues(a, b))
181
+ .forEach(([level0Value, cellMap]) => {
182
+ const level0Node = {
183
+ id: String(level0Value),
184
+ label: formatNodeLabel(grouping.level0, level0Value),
185
+ metadata: {
186
+ type: grouping.level0,
187
+ value: level0Value,
188
+ grouping: grouping.level0
189
+ },
190
+ defaultExpanded: false,
191
+ defaultChecked: false, // Don't check parent nodes
192
+ children: []
193
+ };
194
+ // Sort cells by band frequency
195
+ const sortedCells = sortCellsByBandFrequency(Array.from(cellMap.entries()));
196
+ sortedCells.forEach(([cellName, record]) => {
197
+ const cellNode = {
198
+ id: cellName,
199
+ label: `${cellName} (${record.band})`,
200
+ icon: getBandIcon(record.band),
201
+ metadata: {
202
+ type: 'cell',
203
+ cellName,
204
+ band: record.band,
205
+ siteName: record.siteName,
206
+ sector: record.sector,
207
+ azimuth: record.azimuth
208
+ },
209
+ defaultChecked: true
210
+ };
211
+ level0Node.children.push(cellNode);
212
+ });
213
+ treeNodes.push(level0Node);
214
+ });
215
+ log('✅ 2-level tree nodes built', {
216
+ totalNodes: treeNodes.length,
217
+ grouping: `${grouping.level0} → cell`,
218
+ sampleNode: treeNodes[0]?.label
219
+ });
220
+ return treeNodes;
221
+ }
222
+ /**
223
+ * Get field value from record based on grouping field type
224
+ */
225
+ function getFieldValue(record, field) {
226
+ switch (field) {
227
+ case 'site':
228
+ return record.siteName;
229
+ case 'azimuth':
230
+ return record.azimuth;
231
+ case 'band':
232
+ return record.band;
233
+ case 'sector':
234
+ return record.sector;
235
+ default:
236
+ return record.siteName;
237
+ }
238
+ }
239
+ /**
240
+ * Format node label based on field type
241
+ */
242
+ function formatNodeLabel(field, value) {
243
+ switch (field) {
244
+ case 'site':
245
+ return `Site ${value}`;
246
+ case 'azimuth':
247
+ return `${value}° Sector`;
248
+ case 'band':
249
+ return `${value}`;
250
+ case 'sector':
251
+ return `Sector ${value}`;
252
+ default:
253
+ return String(value);
254
+ }
255
+ }
256
+ /**
257
+ * Compare values for sorting (handles both strings and numbers)
258
+ */
259
+ function compareValues(a, b) {
260
+ if (typeof a === 'number' && typeof b === 'number') {
261
+ return a - b;
262
+ }
263
+ return String(a).localeCompare(String(b));
264
+ }
141
265
  /**
142
266
  * Get icon emoji based on band technology
143
267
  */
@@ -152,6 +276,7 @@ function getBandIcon(band) {
152
276
  /**
153
277
  * Filter chart data based on selected tree paths
154
278
  * Only include cells that are checked in the tree
279
+ * Handles both 2-level (level0:cellName) and 3-level (level0:level1:cellName) paths
155
280
  */
156
281
  export function filterChartData(data, checkedPaths) {
157
282
  log('🔄 Filtering chart data', {
@@ -159,21 +284,24 @@ export function filterChartData(data, checkedPaths) {
159
284
  checkedPathsCount: checkedPaths.size,
160
285
  paths: Array.from(checkedPaths)
161
286
  });
162
- // Extract cell names from checked leaf paths (format: "site:azimuth:cellName")
287
+ // Extract cell names from checked leaf paths
163
288
  const selectedCells = new Set();
164
289
  checkedPaths.forEach((path) => {
165
290
  const parts = path.split(':');
166
291
  if (parts.length === 3) {
167
- // This is a cell-level path (site:azimuth:cellName)
292
+ // 3-level path: level0:level1:cellName
168
293
  selectedCells.add(parts[2]);
169
294
  }
295
+ else if (parts.length === 2) {
296
+ // 2-level path: level0:cellName
297
+ selectedCells.add(parts[1]);
298
+ }
170
299
  });
171
300
  // Filter data to only include selected cells
172
301
  const filtered = data.filter((record) => selectedCells.has(record.cellName));
173
- log('✅ Data filtered', {
302
+ log('✅ Filtered chart data', {
174
303
  selectedCells: Array.from(selectedCells),
175
- filteredRecords: filtered.length,
176
- uniqueCells: new Set(filtered.map(r => r.cellName)).size
304
+ filteredCount: filtered.length
177
305
  });
178
306
  return filtered;
179
307
  }
@@ -183,13 +311,12 @@ export function filterChartData(data, checkedPaths) {
183
311
  * Transforms from long format (many rows per cell) to wide format (one column per cell)
184
312
  *
185
313
  * @param data - Filtered cell traffic records
186
- * @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
314
+ * @param baseMetrics - Array of metric names to pivot (e.g., ['DL_GBYTES', 'UL_GBYTES'])
187
315
  */
188
316
  export function transformChartData(data, baseMetrics) {
189
317
  log('🔄 Transforming chart data', {
190
- inputRecords: data.length,
191
- baseMetrics,
192
- uniqueCells: new Set(data.map(r => r.cellName)).size
318
+ rowCount: data.length,
319
+ baseMetrics
193
320
  });
194
321
  // Group data by date
195
322
  const dateMap = new Map();
@@ -221,26 +348,19 @@ export function transformChartData(data, baseMetrics) {
221
348
  });
222
349
  // Sort by date
223
350
  pivotedData.sort((a, b) => a.TIMESTAMP.localeCompare(b.TIMESTAMP));
224
- log('✅ Data transformed', {
351
+ log('✅ Chart data transformed', {
225
352
  outputRows: pivotedData.length,
226
- dateRange: pivotedData.length > 0 ?
227
- `${pivotedData[0].TIMESTAMP} to ${pivotedData[pivotedData.length - 1].TIMESTAMP}` :
228
- 'none',
229
- columnsPerRow: pivotedData[0] ? Object.keys(pivotedData[0]).length : 0,
230
- sampleRow: pivotedData[0]
353
+ sampleColumns: pivotedData[0] ? Object.keys(pivotedData[0]) : []
231
354
  });
232
355
  return pivotedData;
233
356
  }
234
357
  /**
235
- * Apply cell styling based on band and sector
236
- * Modifies KPI objects to include color (from band) and lineStyle (from sector)
237
- * Updates KPI name to format: Band_Azimuth°
238
- *
239
- * @param metricName - Base metric name (e.g., 'dlGBytes')
240
- * @param cellRecord - Cell traffic record with band, sector, azimuth metadata
241
- * @param unit - Unit string for the metric
242
- * @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
243
- * @returns Styled KPI object
358
+ * Create a styled KPI with band colors and sector line styles
359
+ * @param metricName - Base metric name (e.g., 'DL_GBYTES')
360
+ * @param cellRecord - Cell traffic record with metadata
361
+ * @param unit - Unit string (e.g., 'GB', '%')
362
+ * @param stylingConfig - Optional styling configuration
363
+ * @returns KPI with cell-specific styling applied
244
364
  */
245
365
  export function createStyledKPI(metricName, cellRecord, unit, stylingConfig) {
246
366
  const { band, sector, azimuth, cellName } = cellRecord;
@@ -341,16 +341,21 @@
341
341
  {/snippet}
342
342
 
343
343
  <div class="chart-component" bind:this={componentElement}>
344
- <!-- Global Controls Section (appears above tabs/scrollspy) -->
345
- {#if showGlobalControls && showControlsPanel}
346
- <GlobalControls controls={globalControls} onUpdate={handleControlsUpdate} />
344
+ <!-- Floating Global Controls (renders as fixed position overlay) -->
345
+ {#if showGlobalControls}
346
+ <GlobalControls
347
+ controls={globalControls}
348
+ onUpdate={handleControlsUpdate}
349
+ isExpanded={showControlsPanel}
350
+ onToggle={() => showControlsPanel = !showControlsPanel}
351
+ />
347
352
  {/if}
348
353
 
349
354
  <!-- Always render the main content (tabs or scrollspy) -->
350
355
  {#if mode === 'tabs'}
351
356
  <!-- Tab Mode with Navigation -->
352
357
  <div class="tabs-container">
353
- <!-- Tab Navigation with Controls Toggle -->
358
+ <!-- Tab Navigation -->
354
359
  <div class="nav-tabs-wrapper">
355
360
  <ul class="nav nav-tabs" role="tablist">
356
361
  {#each layout.sections as section, index}
@@ -369,21 +374,6 @@
369
374
  </li>
370
375
  {/each}
371
376
  </ul>
372
-
373
- <!-- Controls Toggle Button -->
374
- {#if showGlobalControls}
375
- <button
376
- class="btn btn-sm btn-outline-secondary controls-toggle"
377
- onclick={() => showControlsPanel = !showControlsPanel}
378
- title={showControlsPanel ? "Hide Controls" : "Show Controls"}
379
- type="button"
380
- >
381
- <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
382
- <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"/>
383
- </svg>
384
- <span class="ms-1">{showControlsPanel ? 'Hide' : 'Show'} Controls</span>
385
- </button>
386
- {/if}
387
377
  </div>
388
378
 
389
379
  <!-- Tab Content -->
@@ -402,7 +392,7 @@
402
392
  {:else if mode === 'scrollspy'}
403
393
  <!-- ScrollSpy Mode with Navigation -->
404
394
  <div class="scrollspy-container">
405
- <!-- ScrollSpy Navigation with Controls Toggle -->
395
+ <!-- ScrollSpy Navigation -->
406
396
  <nav class="scrollspy-nav">
407
397
  <div class="nav-wrapper">
408
398
  <ul class="nav nav-pills">
@@ -418,21 +408,6 @@
418
408
  </li>
419
409
  {/each}
420
410
  </ul>
421
-
422
- <!-- Controls Toggle Button -->
423
- {#if showGlobalControls}
424
- <button
425
- class="btn btn-sm btn-outline-secondary controls-toggle"
426
- onclick={() => showControlsPanel = !showControlsPanel}
427
- title={showControlsPanel ? "Hide Controls" : "Show Controls"}
428
- type="button"
429
- >
430
- <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
431
- <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"/>
432
- </svg>
433
- <!-- <span class="ms-1">{showControlsPanel ? 'Hide' : 'Show'} Controls</span> -->
434
- </button>
435
- {/if}
436
411
  </div>
437
412
  </nav>
438
413
 
@@ -722,11 +697,10 @@
722
697
  max-height: 100%; /* Constrain to slot */
723
698
  }
724
699
 
725
- /* Tab navigation wrapper with toggle button */
700
+ /* Tab navigation wrapper */
726
701
  .nav-tabs-wrapper {
727
702
  display: flex;
728
703
  align-items: center;
729
- gap: 1rem;
730
704
  border-bottom: 1px solid #dee2e6;
731
705
  }
732
706
 
@@ -736,25 +710,7 @@
736
710
  margin-bottom: 0;
737
711
  }
738
712
 
739
- .controls-toggle {
740
- display: flex;
741
- align-items: center;
742
- gap: 0.25rem;
743
- white-space: nowrap;
744
- border-radius: 0.25rem;
745
- transition: all 0.2s ease;
746
- }
747
-
748
- .controls-toggle:hover {
749
- background-color: #f8f9fa;
750
- border-color: #6c757d;
751
- }
752
-
753
- .controls-toggle svg {
754
- flex-shrink: 0;
755
- }
756
-
757
- /* ScrollSpy navigation wrapper with toggle button */
713
+ /* ScrollSpy navigation wrapper */
758
714
  .scrollspy-nav .nav-wrapper {
759
715
  display: flex;
760
716
  align-items: center;