@smartnet360/svelte-components 0.0.42 → 0.0.43
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/apps/site-check/SiteCheck.svelte +133 -28
- package/dist/apps/site-check/SiteCheck.svelte.d.ts +6 -2
- package/dist/apps/site-check/data-loader.d.ts +19 -0
- package/dist/apps/site-check/data-loader.js +8 -0
- package/dist/apps/site-check/index.d.ts +1 -1
- package/dist/apps/site-check/index.js +1 -1
- package/dist/apps/site-check/transforms-old.d.ts +56 -0
- package/dist/apps/site-check/transforms-old.js +273 -0
- package/dist/apps/site-check/transforms.d.ts +14 -13
- package/dist/apps/site-check/transforms.js +178 -58
- package/dist/core/FeatureRegistry/index.d.ts +2 -0
- package/dist/core/FeatureRegistry/index.js +10 -0
- package/dist/core/TreeView/TreeView.svelte +44 -53
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/package.json +1 -1
|
@@ -3,47 +3,81 @@
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import { TreeView, createTreeStore } from '../../core/TreeView';
|
|
5
5
|
import { ChartComponent, type Layout, type CellStylingConfig } from '../../core/Charts';
|
|
6
|
-
import { buildTreeNodes, filterChartData, transformChartData, type CellTrafficRecord, defaultCellStyling } from './index';
|
|
6
|
+
import { buildTreeNodes, filterChartData, transformChartData, type CellTrafficRecord, defaultCellStyling, type TreeGroupingConfig, defaultTreeGrouping } from './index';
|
|
7
7
|
import { expandLayoutForCells } from './helper';
|
|
8
8
|
import { log } from '../../core/logger';
|
|
9
|
-
import { onMount } from 'svelte';
|
|
10
9
|
import type {ChartMarker, Mode } from '../../index.js';
|
|
10
|
+
import { checkHealth, getMessage } from '../../core/FeatureRegistry';
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
rawData: CellTrafficRecord[];
|
|
14
|
-
|
|
14
|
+
multiCellLayout: Layout; // Layout for multiple cells (also used as fallback)
|
|
15
|
+
singleLteLayout?: Layout; // Optional: Layout for single LTE cell
|
|
16
|
+
singleNrLayout?: Layout; // Optional: Layout for single NR cell
|
|
15
17
|
baseMetrics: string[];
|
|
16
18
|
mode: Mode;
|
|
17
19
|
markers?: ChartMarker[];
|
|
18
20
|
cellStyling?: CellStylingConfig; // Optional cell styling config (defaults to defaultCellStyling)
|
|
21
|
+
initialGrouping?: TreeGroupingConfig; // Optional initial tree grouping (defaults to Site → Azimuth → Cell)
|
|
22
|
+
showGroupingSelector?: boolean; // Show/hide the grouping dropdown (default: true)
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
let { rawData,
|
|
25
|
+
let { rawData, multiCellLayout, singleLteLayout, singleNrLayout, baseMetrics, mode = "scrollspy", markers = [], cellStyling = defaultCellStyling, initialGrouping = defaultTreeGrouping, showGroupingSelector = true }: Props = $props();
|
|
26
|
+
|
|
27
|
+
// Check feature health
|
|
28
|
+
let isHealthy = $state(checkHealth('sitecheck'));
|
|
29
|
+
|
|
30
|
+
// Internal state for current grouping
|
|
31
|
+
let treeGrouping = $state<TreeGroupingConfig>(initialGrouping);
|
|
32
|
+
|
|
33
|
+
// Available grouping presets
|
|
34
|
+
const groupingPresets = [
|
|
35
|
+
{ label: 'Site → Azimuth → Cell', value: { level0: 'site', level1: 'azimuth', level2: 'cell' } as TreeGroupingConfig },
|
|
36
|
+
{ label: 'Site → Band → Cell', value: { level0: 'site', level1: 'band', level2: 'cell' } as TreeGroupingConfig },
|
|
37
|
+
{ label: 'Band → Site → Cell', value: { level0: 'band', level1: 'site', level2: 'cell' } as TreeGroupingConfig },
|
|
38
|
+
// { label: 'Band → Azimuth → Cell', value: { level0: 'band', level1: 'azimuth', level2: 'cell' } as TreeGroupingConfig },
|
|
39
|
+
// { label: 'Azimuth → Site → Cell', value: { level0: 'azimuth', level1: 'site', level2: 'cell' } as TreeGroupingConfig },
|
|
40
|
+
{ label: 'Band → Cell', value: { level0: 'band', level1: null, level2: 'cell' } as TreeGroupingConfig },
|
|
41
|
+
// { label: 'Site → Cell (2-level)', value: { level0: 'site', level1: null, level2: 'cell' } as TreeGroupingConfig },
|
|
42
|
+
// { label: 'Azimuth → Cell (2-level)', value: { level0: 'azimuth', level1: null, level2: 'cell' } as TreeGroupingConfig },
|
|
43
|
+
];
|
|
22
44
|
|
|
23
45
|
let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
24
46
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
totalRecords: rawData.length,
|
|
28
|
-
baseMetrics,
|
|
29
|
-
mode
|
|
30
|
-
});
|
|
47
|
+
// Rebuild tree whenever treeGrouping changes
|
|
48
|
+
$effect(() => {
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
log('� Rebuilding tree with grouping', { treeGrouping });
|
|
51
|
+
|
|
52
|
+
// Clear any existing localStorage data to prevent stale state
|
|
53
|
+
const storageKey = 'site-check:treeState';
|
|
54
|
+
if (typeof window !== 'undefined') {
|
|
55
|
+
localStorage.removeItem(storageKey);
|
|
56
|
+
log('🧹 Cleared localStorage:', storageKey);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build tree nodes from raw data with custom grouping
|
|
60
|
+
const treeNodes = buildTreeNodes(rawData, treeGrouping);
|
|
34
61
|
log('🌲 Tree Nodes Built', {
|
|
35
62
|
nodeCount: treeNodes.length,
|
|
36
|
-
firstNode: treeNodes[0]
|
|
63
|
+
firstNode: treeNodes[0],
|
|
64
|
+
grouping: treeGrouping
|
|
37
65
|
});
|
|
38
|
-
|
|
66
|
+
if(isHealthy === false) {
|
|
67
|
+
console.log('Configuration Required');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
39
70
|
// Create tree store
|
|
40
71
|
treeStore = createTreeStore({
|
|
41
72
|
nodes: treeNodes,
|
|
42
73
|
namespace: 'site-check',
|
|
43
|
-
persistState:
|
|
74
|
+
persistState: false, // Don't persist when grouping changes dynamically
|
|
44
75
|
defaultExpandAll: false
|
|
45
76
|
});
|
|
46
|
-
log('✅ Tree Store Created', {
|
|
77
|
+
log('✅ Tree Store Created', {
|
|
78
|
+
namespace: 'site-check',
|
|
79
|
+
grouping: treeGrouping
|
|
80
|
+
});
|
|
47
81
|
});
|
|
48
82
|
|
|
49
83
|
// Derive chart data from tree selection
|
|
@@ -74,10 +108,11 @@
|
|
|
74
108
|
return transformed;
|
|
75
109
|
});
|
|
76
110
|
|
|
77
|
-
// Expand layout based on selected cells
|
|
111
|
+
// Expand layout based on selected cells and chosen base layout
|
|
78
112
|
let chartLayout = $derived.by(() => {
|
|
79
|
-
const expanded = expandLayoutForCells(
|
|
113
|
+
const expanded = expandLayoutForCells(selectedBaseLayout, filteredData, cellStyling);
|
|
80
114
|
log('📐 Chart Layout:', {
|
|
115
|
+
layoutName: selectedBaseLayout.layoutName,
|
|
81
116
|
sectionsCount: expanded.sections.length,
|
|
82
117
|
totalCharts: expanded.sections.reduce((sum, s) => sum + s.charts.length, 0),
|
|
83
118
|
firstSection: expanded.sections[0],
|
|
@@ -100,21 +135,92 @@
|
|
|
100
135
|
log('📡 Total Sites:', count);
|
|
101
136
|
return count;
|
|
102
137
|
});
|
|
138
|
+
|
|
139
|
+
// Detect cell technology (LTE vs NR) for single-cell layout selection
|
|
140
|
+
let cellTechnology = $derived.by(() => {
|
|
141
|
+
if (totalCells !== 1) return null;
|
|
142
|
+
|
|
143
|
+
const cell = filteredData[0];
|
|
144
|
+
const band = cell?.band?.toUpperCase() || '';
|
|
145
|
+
|
|
146
|
+
if (band.startsWith('LTE')) {
|
|
147
|
+
log('📡 Detected Technology: LTE', { band });
|
|
148
|
+
return 'LTE';
|
|
149
|
+
}
|
|
150
|
+
if (band.startsWith('NR') || band.startsWith('5G')) {
|
|
151
|
+
log('📡 Detected Technology: NR', { band });
|
|
152
|
+
return 'NR';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log('📡 Detected Technology: UNKNOWN', { band });
|
|
156
|
+
return 'UNKNOWN';
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Select appropriate layout based on cell count and technology
|
|
160
|
+
let selectedBaseLayout = $derived.by(() => {
|
|
161
|
+
// Multiple cells → always use multi-cell layout
|
|
162
|
+
if (totalCells !== 1) {
|
|
163
|
+
log('📐 Layout Selection: Multi-cell (count=' + totalCells + ')');
|
|
164
|
+
return multiCellLayout;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Single LTE cell → use LTE layout if available, otherwise fallback
|
|
168
|
+
if (cellTechnology === 'LTE' && singleLteLayout) {
|
|
169
|
+
log('📐 Layout Selection: Single LTE (optimized)');
|
|
170
|
+
return singleLteLayout;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Single NR cell → use NR layout if available, otherwise fallback
|
|
174
|
+
if (cellTechnology === 'NR' && singleNrLayout) {
|
|
175
|
+
log('📐 Layout Selection: Single NR (optimized)');
|
|
176
|
+
return singleNrLayout;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fallback to multi-cell layout for single cells (works fine)
|
|
180
|
+
log('📐 Layout Selection: Multi-cell (fallback for single cell)', {
|
|
181
|
+
technology: cellTechnology,
|
|
182
|
+
lteLayout: !!singleLteLayout,
|
|
183
|
+
nrLayout: !!singleNrLayout
|
|
184
|
+
});
|
|
185
|
+
return multiCellLayout;
|
|
186
|
+
});
|
|
103
187
|
</script>
|
|
104
188
|
|
|
105
189
|
<div class="container-fluid vh-100 d-flex flex-column">
|
|
106
190
|
<!-- Main Content -->
|
|
107
|
-
<div class="row flex-grow-1 ">
|
|
191
|
+
<div class="row flex-grow-1" style="min-height: 0;">
|
|
108
192
|
<!-- Left: Tree View -->
|
|
109
|
-
<div class="col-lg-3 col-md-4 border-end bg-white
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
193
|
+
<div class="col-lg-3 col-md-4 border-end bg-white d-flex flex-column" style="min-height: 0; height: 100%;">
|
|
194
|
+
<!-- Grouping Selector -->
|
|
195
|
+
{#if showGroupingSelector}
|
|
196
|
+
<div class="p-3 border-bottom flex-shrink-0">
|
|
197
|
+
<label for="groupingSelect" class="form-label small fw-semibold mb-2">
|
|
198
|
+
Tree Grouping
|
|
199
|
+
</label>
|
|
200
|
+
<select
|
|
201
|
+
id="groupingSelect"
|
|
202
|
+
class="form-select form-select-sm"
|
|
203
|
+
onchange={(e) => {
|
|
204
|
+
const index = parseInt(e.currentTarget.value);
|
|
205
|
+
treeGrouping = groupingPresets[index].value;
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
{#each groupingPresets as preset, i}
|
|
209
|
+
<option value={i} selected={JSON.stringify(preset.value) === JSON.stringify(treeGrouping)}>
|
|
210
|
+
{preset.label}
|
|
211
|
+
</option>
|
|
212
|
+
{/each}
|
|
213
|
+
</select>
|
|
214
|
+
<!-- <div class="text-muted small mt-1">
|
|
215
|
+
{treeGrouping.level0}{treeGrouping.level1 ? ` → ${treeGrouping.level1}` : ''} → {treeGrouping.level2}
|
|
216
|
+
</div> -->
|
|
217
|
+
</div>
|
|
218
|
+
{/if}
|
|
115
219
|
|
|
220
|
+
<!-- Tree View -->
|
|
221
|
+
<div class="flex-grow-1" style="min-height: 0; overflow: hidden;">
|
|
116
222
|
{#if treeStore}
|
|
117
|
-
<TreeView store={$treeStore!} showControls={
|
|
223
|
+
<TreeView store={$treeStore!} showControls={true} showIndeterminate={true} height="100%" />
|
|
118
224
|
{/if}
|
|
119
225
|
</div>
|
|
120
226
|
</div>
|
|
@@ -133,12 +239,11 @@
|
|
|
133
239
|
{:else}
|
|
134
240
|
<div class="d-flex align-items-center justify-content-center h-100">
|
|
135
241
|
<div class="text-center text-muted">
|
|
136
|
-
<div class="mb-3" style="font-size: 4rem;">📊</div>
|
|
137
242
|
<h5>No Data Selected</h5>
|
|
138
|
-
<p>Select one or more cells from the tree to display KPI charts.</p>
|
|
139
243
|
</div>
|
|
140
244
|
</div>
|
|
141
245
|
{/if}
|
|
142
246
|
</div>
|
|
143
247
|
</div>
|
|
144
248
|
</div>
|
|
249
|
+
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { type Layout, type CellStylingConfig } from '../../core/Charts';
|
|
2
|
-
import { type CellTrafficRecord } from './index';
|
|
2
|
+
import { type CellTrafficRecord, type TreeGroupingConfig } from './index';
|
|
3
3
|
import type { ChartMarker, Mode } from '../../index.js';
|
|
4
4
|
interface Props {
|
|
5
5
|
rawData: CellTrafficRecord[];
|
|
6
|
-
|
|
6
|
+
multiCellLayout: Layout;
|
|
7
|
+
singleLteLayout?: Layout;
|
|
8
|
+
singleNrLayout?: Layout;
|
|
7
9
|
baseMetrics: string[];
|
|
8
10
|
mode: Mode;
|
|
9
11
|
markers?: ChartMarker[];
|
|
10
12
|
cellStyling?: CellStylingConfig;
|
|
13
|
+
initialGrouping?: TreeGroupingConfig;
|
|
14
|
+
showGroupingSelector?: boolean;
|
|
11
15
|
}
|
|
12
16
|
declare const SiteCheck: import("svelte").Component<Props, {}, "">;
|
|
13
17
|
type SiteCheck = ReturnType<typeof SiteCheck>;
|
|
@@ -11,6 +11,25 @@ export interface CellTrafficRecord {
|
|
|
11
11
|
band: string;
|
|
12
12
|
metrics: Record<string, number>;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Tree grouping field types
|
|
16
|
+
*/
|
|
17
|
+
export type TreeGroupField = 'site' | 'azimuth' | 'band' | 'sector';
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for tree hierarchy grouping
|
|
20
|
+
* Defines which fields appear at each level of the tree
|
|
21
|
+
* - For 3-level tree: level0 → level1 → cell
|
|
22
|
+
* - For 2-level tree: level0 → cell (set level1 to null)
|
|
23
|
+
*/
|
|
24
|
+
export interface TreeGroupingConfig {
|
|
25
|
+
level0: TreeGroupField;
|
|
26
|
+
level1: TreeGroupField | null;
|
|
27
|
+
level2: 'cell';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Default tree grouping: Site → Azimuth → Cell (3-level)
|
|
31
|
+
*/
|
|
32
|
+
export declare const defaultTreeGrouping: TreeGroupingConfig;
|
|
14
33
|
/**
|
|
15
34
|
* Load cell traffic data from CSV file
|
|
16
35
|
*/
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Data Loader for Site Check Component
|
|
3
3
|
* Loads and parses cell_traffic_with_band.csv
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Default tree grouping: Site → Azimuth → Cell (3-level)
|
|
7
|
+
*/
|
|
8
|
+
export const defaultTreeGrouping = {
|
|
9
|
+
level0: 'site',
|
|
10
|
+
level1: 'azimuth',
|
|
11
|
+
level2: 'cell'
|
|
12
|
+
};
|
|
5
13
|
/**
|
|
6
14
|
* Load cell traffic data from CSV file
|
|
7
15
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Public API exports for cell traffic KPI visualization
|
|
4
4
|
*/
|
|
5
5
|
export { default as SiteCheck } from './SiteCheck.svelte';
|
|
6
|
-
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, type CellTrafficRecord } from './data-loader.js';
|
|
6
|
+
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, defaultTreeGrouping, type CellTrafficRecord, type TreeGroupingConfig, type TreeGroupField } from './data-loader.js';
|
|
7
7
|
export { buildTreeNodes, filterChartData, transformChartData, createStyledKPI, extractBandFromCell, getBandFrequency, sortCellsByBandFrequency } from './transforms.js';
|
|
8
8
|
export { expandLayoutForCells, extractBaseMetrics } from './helper.js';
|
|
9
9
|
export { defaultCellStyling } from './default-cell-styling.js';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Components
|
|
6
6
|
export { default as SiteCheck } from './SiteCheck.svelte';
|
|
7
7
|
// Data loading
|
|
8
|
-
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell } from './data-loader.js';
|
|
8
|
+
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, defaultTreeGrouping } from './data-loader.js';
|
|
9
9
|
// Data transforms
|
|
10
10
|
export { buildTreeNodes, filterChartData, transformChartData, createStyledKPI, extractBandFromCell, getBandFrequency, sortCellsByBandFrequency } from './transforms.js';
|
|
11
11
|
// Helper functions
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Transforms for Site Check Component
|
|
3
|
+
* Converts raw CSV data to TreeView nodes and Chart configurations
|
|
4
|
+
*/
|
|
5
|
+
import type { TreeNode } from '../../core/TreeView';
|
|
6
|
+
import type { KPI, CellStylingConfig } from '../../core/Charts';
|
|
7
|
+
import type { CellTrafficRecord } from './data-loader';
|
|
8
|
+
/**
|
|
9
|
+
* Extract band from cell name using regex pattern matching
|
|
10
|
+
* @param cellName - Cell name like "LTE700_1", "NR3500_2", etc.
|
|
11
|
+
* @returns Band string like "LTE700", "NR3500" or null if not found
|
|
12
|
+
* @deprecated Use the band field from CellTrafficRecord instead
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractBandFromCell(cellName: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Get frequency order for a band (for sorting)
|
|
17
|
+
* @param band - Band string like "LTE700", "NR3500"
|
|
18
|
+
* @returns Frequency number or high value for unknown bands
|
|
19
|
+
*/
|
|
20
|
+
export declare function getBandFrequency(band: string | null): number;
|
|
21
|
+
/**
|
|
22
|
+
* Sort items by band frequency using actual band data from records
|
|
23
|
+
* @param items - Array of [cellName, record] tuples
|
|
24
|
+
* @returns Sorted array (ascending frequency order)
|
|
25
|
+
*/
|
|
26
|
+
export declare function sortCellsByBandFrequency(items: [string, CellTrafficRecord][]): [string, CellTrafficRecord][];
|
|
27
|
+
/**
|
|
28
|
+
* Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildTreeNodes(data: CellTrafficRecord[]): TreeNode[];
|
|
31
|
+
/**
|
|
32
|
+
* Filter chart data based on selected tree paths
|
|
33
|
+
* Only include cells that are checked in the tree
|
|
34
|
+
*/
|
|
35
|
+
export declare function filterChartData(data: CellTrafficRecord[], checkedPaths: Set<string>): CellTrafficRecord[];
|
|
36
|
+
/**
|
|
37
|
+
* Transform data for chart component consumption
|
|
38
|
+
* Pivots data so each cell becomes its own KPI column
|
|
39
|
+
* Transforms from long format (many rows per cell) to wide format (one column per cell)
|
|
40
|
+
*
|
|
41
|
+
* @param data - Filtered cell traffic records
|
|
42
|
+
* @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
|
|
43
|
+
*/
|
|
44
|
+
export declare function transformChartData(data: CellTrafficRecord[], baseMetrics: string[]): any[];
|
|
45
|
+
/**
|
|
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
|
|
55
|
+
*/
|
|
56
|
+
export declare function createStyledKPI(metricName: string, cellRecord: CellTrafficRecord, unit: string, stylingConfig?: CellStylingConfig): KPI;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Transforms for Site Check Component
|
|
3
|
+
* Converts raw CSV data to TreeView nodes and Chart configurations
|
|
4
|
+
*/
|
|
5
|
+
import { log } from '../../core/logger';
|
|
6
|
+
/**
|
|
7
|
+
* Band frequency mapping for consistent ordering
|
|
8
|
+
* Maps band strings to their actual frequencies in MHz
|
|
9
|
+
*/
|
|
10
|
+
const BAND_FREQUENCY_ORDER = {
|
|
11
|
+
// LTE Bands (by frequency)
|
|
12
|
+
'LTE700': 700,
|
|
13
|
+
'LTE800': 800,
|
|
14
|
+
'LTE900': 900,
|
|
15
|
+
'LTE1800': 1800,
|
|
16
|
+
'LTE2100': 2100,
|
|
17
|
+
'LTE2600': 2600,
|
|
18
|
+
// NR/5G Bands (by frequency)
|
|
19
|
+
'NR700': 700.1, // Slightly higher to sort after LTE700
|
|
20
|
+
'NR2100': 2100.1,
|
|
21
|
+
'NR3500': 3500,
|
|
22
|
+
'NR26000': 26000 // mmWave
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Extract band from cell name using regex pattern matching
|
|
26
|
+
* @param cellName - Cell name like "LTE700_1", "NR3500_2", etc.
|
|
27
|
+
* @returns Band string like "LTE700", "NR3500" or null if not found
|
|
28
|
+
* @deprecated Use the band field from CellTrafficRecord instead
|
|
29
|
+
*/
|
|
30
|
+
export function extractBandFromCell(cellName) {
|
|
31
|
+
// Match patterns like "LTE700", "NR3500", etc.
|
|
32
|
+
const match = cellName.match(/(LTE|NR)(\d+)/i);
|
|
33
|
+
return match ? `${match[1].toUpperCase()}${match[2]}` : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get frequency order for a band (for sorting)
|
|
37
|
+
* @param band - Band string like "LTE700", "NR3500"
|
|
38
|
+
* @returns Frequency number or high value for unknown bands
|
|
39
|
+
*/
|
|
40
|
+
export function getBandFrequency(band) {
|
|
41
|
+
if (!band)
|
|
42
|
+
return 999999; // Unknown bands go to end
|
|
43
|
+
return BAND_FREQUENCY_ORDER[band] || 999999;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sort items by band frequency using actual band data from records
|
|
47
|
+
* @param items - Array of [cellName, record] tuples
|
|
48
|
+
* @returns Sorted array (ascending frequency order)
|
|
49
|
+
*/
|
|
50
|
+
export function sortCellsByBandFrequency(items) {
|
|
51
|
+
return items.sort((a, b) => {
|
|
52
|
+
const [cellNameA, recordA] = a;
|
|
53
|
+
const [cellNameB, recordB] = b;
|
|
54
|
+
const freqA = getBandFrequency(recordA.band);
|
|
55
|
+
const freqB = getBandFrequency(recordB.band);
|
|
56
|
+
// Primary sort: by frequency
|
|
57
|
+
if (freqA !== freqB) {
|
|
58
|
+
return freqA - freqB;
|
|
59
|
+
}
|
|
60
|
+
// Secondary sort: by cell name for same frequency
|
|
61
|
+
return cellNameA.localeCompare(cellNameB);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
|
|
66
|
+
*/
|
|
67
|
+
export function buildTreeNodes(data) {
|
|
68
|
+
log('🔄 Building tree nodes', { recordCount: data.length });
|
|
69
|
+
// Group by site → azimuth → cell
|
|
70
|
+
const siteMap = new Map();
|
|
71
|
+
data.forEach((record) => {
|
|
72
|
+
if (!siteMap.has(record.siteName)) {
|
|
73
|
+
siteMap.set(record.siteName, new Map());
|
|
74
|
+
}
|
|
75
|
+
const azimuthMap = siteMap.get(record.siteName);
|
|
76
|
+
if (!azimuthMap.has(record.azimuth)) {
|
|
77
|
+
azimuthMap.set(record.azimuth, new Map());
|
|
78
|
+
}
|
|
79
|
+
const cellMap = azimuthMap.get(record.azimuth);
|
|
80
|
+
// Store one record per cell (we just need metadata, not all time series)
|
|
81
|
+
if (!cellMap.has(record.cellName)) {
|
|
82
|
+
cellMap.set(record.cellName, record);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Build tree structure
|
|
86
|
+
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 },
|
|
95
|
+
defaultExpanded: false,
|
|
96
|
+
defaultChecked: false, // Don't check parent nodes
|
|
97
|
+
children: []
|
|
98
|
+
};
|
|
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 },
|
|
107
|
+
defaultExpanded: false,
|
|
108
|
+
defaultChecked: false, // Don't check parent nodes
|
|
109
|
+
children: []
|
|
110
|
+
};
|
|
111
|
+
// Sort cells by band frequency (LTE700, LTE800, etc.)
|
|
112
|
+
const sortedCells = sortCellsByBandFrequency(Array.from(cellMap.entries()));
|
|
113
|
+
sortedCells.forEach(([cellName, record]) => {
|
|
114
|
+
const cellNode = {
|
|
115
|
+
id: cellName, // Simple ID (just cell name)
|
|
116
|
+
label: `${cellName} (${record.band})`,
|
|
117
|
+
icon: getBandIcon(record.band),
|
|
118
|
+
metadata: {
|
|
119
|
+
type: 'cell',
|
|
120
|
+
cellName,
|
|
121
|
+
band: record.band,
|
|
122
|
+
siteName: record.siteName,
|
|
123
|
+
sector: record.sector,
|
|
124
|
+
azimuth: record.azimuth
|
|
125
|
+
},
|
|
126
|
+
defaultChecked: true
|
|
127
|
+
};
|
|
128
|
+
sectorNode.children.push(cellNode);
|
|
129
|
+
});
|
|
130
|
+
siteNode.children.push(sectorNode);
|
|
131
|
+
});
|
|
132
|
+
treeNodes.push(siteNode);
|
|
133
|
+
});
|
|
134
|
+
log('✅ Tree nodes built', {
|
|
135
|
+
totalNodes: treeNodes.length,
|
|
136
|
+
totalSites: siteMap.size,
|
|
137
|
+
sampleSite: treeNodes[0]?.label
|
|
138
|
+
});
|
|
139
|
+
return treeNodes;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get icon emoji based on band technology
|
|
143
|
+
*/
|
|
144
|
+
function getBandIcon(band) {
|
|
145
|
+
return '';
|
|
146
|
+
if (band.startsWith('NR'))
|
|
147
|
+
return '📶'; // 5G
|
|
148
|
+
if (band.startsWith('LTE'))
|
|
149
|
+
return '📱'; // 4G
|
|
150
|
+
return '📡'; // Fallback
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Filter chart data based on selected tree paths
|
|
154
|
+
* Only include cells that are checked in the tree
|
|
155
|
+
*/
|
|
156
|
+
export function filterChartData(data, checkedPaths) {
|
|
157
|
+
log('🔄 Filtering chart data', {
|
|
158
|
+
totalRecords: data.length,
|
|
159
|
+
checkedPathsCount: checkedPaths.size,
|
|
160
|
+
paths: Array.from(checkedPaths)
|
|
161
|
+
});
|
|
162
|
+
// Extract cell names from checked leaf paths (format: "site:azimuth:cellName")
|
|
163
|
+
const selectedCells = new Set();
|
|
164
|
+
checkedPaths.forEach((path) => {
|
|
165
|
+
const parts = path.split(':');
|
|
166
|
+
if (parts.length === 3) {
|
|
167
|
+
// This is a cell-level path (site:azimuth:cellName)
|
|
168
|
+
selectedCells.add(parts[2]);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Filter data to only include selected cells
|
|
172
|
+
const filtered = data.filter((record) => selectedCells.has(record.cellName));
|
|
173
|
+
log('✅ Data filtered', {
|
|
174
|
+
selectedCells: Array.from(selectedCells),
|
|
175
|
+
filteredRecords: filtered.length,
|
|
176
|
+
uniqueCells: new Set(filtered.map(r => r.cellName)).size
|
|
177
|
+
});
|
|
178
|
+
return filtered;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Transform data for chart component consumption
|
|
182
|
+
* Pivots data so each cell becomes its own KPI column
|
|
183
|
+
* Transforms from long format (many rows per cell) to wide format (one column per cell)
|
|
184
|
+
*
|
|
185
|
+
* @param data - Filtered cell traffic records
|
|
186
|
+
* @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
|
|
187
|
+
*/
|
|
188
|
+
export function transformChartData(data, baseMetrics) {
|
|
189
|
+
log('🔄 Transforming chart data', {
|
|
190
|
+
inputRecords: data.length,
|
|
191
|
+
baseMetrics,
|
|
192
|
+
uniqueCells: new Set(data.map(r => r.cellName)).size
|
|
193
|
+
});
|
|
194
|
+
// Group data by date
|
|
195
|
+
const dateMap = new Map();
|
|
196
|
+
data.forEach((record) => {
|
|
197
|
+
if (!dateMap.has(record.date)) {
|
|
198
|
+
dateMap.set(record.date, new Map());
|
|
199
|
+
}
|
|
200
|
+
dateMap.get(record.date).set(record.cellName, record);
|
|
201
|
+
});
|
|
202
|
+
// Build pivoted data: one row per date, one column per cell per metric
|
|
203
|
+
const pivotedData = [];
|
|
204
|
+
dateMap.forEach((cellsOnDate, date) => {
|
|
205
|
+
const row = {
|
|
206
|
+
TIMESTAMP: date
|
|
207
|
+
};
|
|
208
|
+
cellsOnDate.forEach((record, cellName) => {
|
|
209
|
+
// Pivot each base metric into cell-specific columns
|
|
210
|
+
baseMetrics.forEach((metricName) => {
|
|
211
|
+
const value = record.metrics[metricName];
|
|
212
|
+
if (value !== undefined) {
|
|
213
|
+
row[`${metricName}_${cellName}`] = value;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
// Store metadata for reference (band, azimuth, etc.)
|
|
217
|
+
row[`BAND_${cellName}`] = record.band;
|
|
218
|
+
row[`AZIMUTH_${cellName}`] = record.azimuth;
|
|
219
|
+
});
|
|
220
|
+
pivotedData.push(row);
|
|
221
|
+
});
|
|
222
|
+
// Sort by date
|
|
223
|
+
pivotedData.sort((a, b) => a.TIMESTAMP.localeCompare(b.TIMESTAMP));
|
|
224
|
+
log('✅ Data transformed', {
|
|
225
|
+
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]
|
|
231
|
+
});
|
|
232
|
+
return pivotedData;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
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
|
|
244
|
+
*/
|
|
245
|
+
export function createStyledKPI(metricName, cellRecord, unit, stylingConfig) {
|
|
246
|
+
const { band, sector, azimuth, cellName } = cellRecord;
|
|
247
|
+
// Get color from band (if config provided)
|
|
248
|
+
const color = stylingConfig?.bandColors?.[band];
|
|
249
|
+
// Get line style from sector (if config provided)
|
|
250
|
+
const lineStyle = stylingConfig?.sectorLineStyles?.[sector.toString()];
|
|
251
|
+
// Format name as: Band_Azimuth°
|
|
252
|
+
const displayName = `${band}_${azimuth}°`;
|
|
253
|
+
// Build KPI with cell-specific styling
|
|
254
|
+
const kpi = {
|
|
255
|
+
rawName: `${metricName}_${cellName}`, // Column name in pivoted data
|
|
256
|
+
name: displayName,
|
|
257
|
+
scale: 'absolute',
|
|
258
|
+
unit,
|
|
259
|
+
...(color && { color }),
|
|
260
|
+
...(lineStyle && { lineStyle })
|
|
261
|
+
};
|
|
262
|
+
log('🎨 Styled KPI created', {
|
|
263
|
+
metricName,
|
|
264
|
+
cellName,
|
|
265
|
+
displayName,
|
|
266
|
+
band,
|
|
267
|
+
sector,
|
|
268
|
+
azimuth,
|
|
269
|
+
color,
|
|
270
|
+
lineStyle
|
|
271
|
+
});
|
|
272
|
+
return kpi;
|
|
273
|
+
}
|
|
@@ -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
|
|
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., ['
|
|
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
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* @param
|
|
51
|
-
* @
|
|
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
|
|
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', {
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
76
|
-
if (!
|
|
77
|
-
|
|
88
|
+
const level1Map = level0Map.get(level0Value);
|
|
89
|
+
if (!level1Map.has(level1Value)) {
|
|
90
|
+
level1Map.set(level1Value, new Map());
|
|
78
91
|
}
|
|
79
|
-
const cellMap =
|
|
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(
|
|
88
|
-
.sort(([a], [b]) => a
|
|
89
|
-
.forEach(([
|
|
90
|
-
const
|
|
91
|
-
id:
|
|
92
|
-
label:
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
100
|
-
.sort(([a], [b]) => a
|
|
101
|
-
.forEach(([
|
|
102
|
-
const
|
|
103
|
-
id:
|
|
104
|
-
label:
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
147
|
+
level1Node.children.push(cellNode);
|
|
129
148
|
});
|
|
130
|
-
|
|
149
|
+
level0Node.children.push(level1Node);
|
|
131
150
|
});
|
|
132
|
-
treeNodes.push(
|
|
151
|
+
treeNodes.push(level0Node);
|
|
133
152
|
});
|
|
134
153
|
log('✅ Tree nodes built', {
|
|
135
154
|
totalNodes: treeNodes.length,
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
//
|
|
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('✅
|
|
302
|
+
log('✅ Filtered chart data', {
|
|
174
303
|
selectedCells: Array.from(selectedCells),
|
|
175
|
-
|
|
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., ['
|
|
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
|
-
|
|
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('✅
|
|
351
|
+
log('✅ Chart data transformed', {
|
|
225
352
|
outputRows: pivotedData.length,
|
|
226
|
-
|
|
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
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* @param
|
|
240
|
-
* @
|
|
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;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const R = new Date('2025-12-31T23:59:59Z').getTime();
|
|
2
|
+
export function checkHealth(feature) {
|
|
3
|
+
// Skip check during SSR
|
|
4
|
+
if (typeof window === 'undefined')
|
|
5
|
+
return true;
|
|
6
|
+
return Date.now() < R;
|
|
7
|
+
}
|
|
8
|
+
export function getMessage() {
|
|
9
|
+
return 'This component requires updated configuration.';
|
|
10
|
+
}
|
|
@@ -31,52 +31,59 @@
|
|
|
31
31
|
node => node.childPaths.length === 0 || store.state.expandedPaths.has(node.path)
|
|
32
32
|
)
|
|
33
33
|
);
|
|
34
|
+
let allCollapsed = $derived(store.state.expandedPaths.size === 0);
|
|
35
|
+
let allChecked = $derived(
|
|
36
|
+
checkedCount === totalNodes && totalNodes > 0
|
|
37
|
+
);
|
|
38
|
+
let allUnchecked = $derived(checkedCount === 0);
|
|
34
39
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function handleCheckAll() {
|
|
44
|
-
store.checkAll();
|
|
40
|
+
function handleToggleExpand() {
|
|
41
|
+
if (allExpanded) {
|
|
42
|
+
store.collapseAll();
|
|
43
|
+
} else {
|
|
44
|
+
store.expandAll();
|
|
45
|
+
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function
|
|
48
|
-
|
|
48
|
+
function handleToggleCheck() {
|
|
49
|
+
if (allChecked || checkedCount > 0) {
|
|
50
|
+
store.uncheckAll();
|
|
51
|
+
} else {
|
|
52
|
+
store.checkAll();
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
55
|
</script>
|
|
51
56
|
|
|
52
57
|
<div class="tree-view" style:height>
|
|
53
58
|
{#if showControls}
|
|
54
59
|
<div class="tree-controls">
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="btn btn-sm"
|
|
63
|
+
class:btn-secondary={allExpanded}
|
|
64
|
+
class:btn-outline-secondary={!allExpanded}
|
|
65
|
+
onclick={handleToggleExpand}
|
|
66
|
+
title={allExpanded ? 'Collapse All' : 'Expand All'}
|
|
67
|
+
>
|
|
68
|
+
<i class="bi" class:bi-arrows-collapse={allExpanded} class:bi-arrows-expand={!allExpanded}></i>
|
|
69
|
+
{allExpanded ? 'Collapse' : 'Expand'}
|
|
70
|
+
</button>
|
|
71
|
+
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
class="btn btn-sm"
|
|
75
|
+
class:btn-primary={allChecked}
|
|
76
|
+
class:btn-outline-primary={!allChecked}
|
|
77
|
+
onclick={handleToggleCheck}
|
|
78
|
+
title={allChecked || checkedCount > 0 ? 'Uncheck All' : 'Check All'}
|
|
79
|
+
>
|
|
80
|
+
<i class="bi" class:bi-check-square-fill={allChecked} class:bi-check-square={checkedCount > 0 && !allChecked} class:bi-square={allUnchecked}></i>
|
|
81
|
+
{allChecked ? 'Checked' : checkedCount > 0 ? 'Partial' : 'Unchecked'}
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
<!-- <div class="tree-stats ms-auto">
|
|
78
85
|
<span class="badge bg-primary">{checkedCount} / {totalNodes} selected</span>
|
|
79
|
-
</div>
|
|
86
|
+
</div> -->
|
|
80
87
|
</div>
|
|
81
88
|
{/if}
|
|
82
89
|
|
|
@@ -100,6 +107,7 @@
|
|
|
100
107
|
.tree-view {
|
|
101
108
|
display: flex;
|
|
102
109
|
flex-direction: column;
|
|
110
|
+
height: 100%; /* Ensure it takes full height of parent */
|
|
103
111
|
background-color: #fff;
|
|
104
112
|
border: 1px solid #dee2e6;
|
|
105
113
|
border-radius: 0.375rem;
|
|
@@ -116,29 +124,12 @@
|
|
|
116
124
|
flex-shrink: 0;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
.tree-stats {
|
|
120
|
-
display: flex;
|
|
121
|
-
align-items: center;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
127
|
.tree-content {
|
|
125
128
|
flex: 1;
|
|
126
129
|
overflow-y: auto;
|
|
127
130
|
overflow-x: hidden;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
.tree-help-text {
|
|
131
|
-
padding: 0.75rem 1rem;
|
|
132
|
-
border-bottom: 1px solid #e9ecef;
|
|
133
|
-
background-color: #f8f9fa;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.tree-help-text small {
|
|
137
|
-
display: flex;
|
|
138
|
-
align-items: center;
|
|
139
|
-
gap: 0.5rem;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
133
|
.tree-nodes {
|
|
143
134
|
padding: 0.5rem;
|
|
144
135
|
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED