@smartnet360/svelte-components 0.0.103 β 0.0.104
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 +11 -77
- package/dist/apps/site-check/SiteCheckControls.svelte +0 -7
- package/dist/apps/site-check/helper.js +0 -33
- package/dist/apps/site-check/transforms.js +15 -65
- package/dist/core/CellTable/CellTable.svelte +456 -0
- package/dist/core/CellTable/CellTable.svelte.d.ts +27 -0
- package/dist/core/CellTable/CellTablePanel.svelte +211 -0
- package/dist/core/CellTable/CellTablePanel.svelte.d.ts +49 -0
- package/dist/core/CellTable/CellTableToolbar.svelte +218 -0
- package/dist/core/CellTable/CellTableToolbar.svelte.d.ts +32 -0
- package/dist/core/CellTable/column-config.d.ts +63 -0
- package/dist/core/CellTable/column-config.js +465 -0
- package/dist/core/CellTable/index.d.ts +10 -0
- package/dist/core/CellTable/index.js +11 -0
- package/dist/core/CellTable/types.d.ts +166 -0
- package/dist/core/CellTable/types.js +6 -0
- package/dist/core/Charts/ChartCard.svelte +0 -23
- package/dist/core/Charts/ChartComponent.svelte +0 -25
- package/dist/core/Charts/data-processor.js +1 -19
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/package.json +3 -2
- package/dist/apps/site-check/transforms-old.d.ts +0 -56
- package/dist/apps/site-check/transforms-old.js +0 -273
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import { createTimeSeriesTraceWithMA, getYAxisTitle, createDefaultPlotlyLayout } from './data-utils.js';
|
|
8
8
|
import { adaptPlotlyLayout, addMarkersToLayout, type ContainerSize } from './adapt.js';
|
|
9
9
|
import { getKPIValues, type ProcessedChartData } from './data-processor.js';
|
|
10
|
-
import { log } from '../logger';
|
|
11
10
|
import { checkHealth, getMessage } from '../FeatureRegistry';
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
@@ -244,22 +243,9 @@
|
|
|
244
243
|
// Use Plotly.react() for updates (preserves zoom/pan) or newPlot for initial render
|
|
245
244
|
if (chartInitialized) {
|
|
246
245
|
// Update existing chart - much faster, preserves user interactions
|
|
247
|
-
log('π Updating chart with Plotly.react', {
|
|
248
|
-
chartTitle: chart.title,
|
|
249
|
-
hoverMode: effectiveHoverMode,
|
|
250
|
-
layoutHoverMode: finalLayout.hovermode
|
|
251
|
-
});
|
|
252
246
|
Plotly.react(chartDiv, traces, finalLayout, config);
|
|
253
247
|
} else {
|
|
254
248
|
// Initial chart creation
|
|
255
|
-
log('π Creating new chart with Plotly.newPlot', {
|
|
256
|
-
chartTitle: chart.title,
|
|
257
|
-
traces: traces.length,
|
|
258
|
-
leftKPIs: chart.yLeft.length,
|
|
259
|
-
rightKPIs: chart.yRight.length,
|
|
260
|
-
hoverMode: effectiveHoverMode,
|
|
261
|
-
layoutHoverMode: finalLayout.hovermode
|
|
262
|
-
});
|
|
263
249
|
Plotly.newPlot(chartDiv, traces, finalLayout, config);
|
|
264
250
|
chartInitialized = true;
|
|
265
251
|
}
|
|
@@ -273,14 +259,6 @@
|
|
|
273
259
|
}
|
|
274
260
|
|
|
275
261
|
onMount(() => {
|
|
276
|
-
log('π ChartCard mounted', {
|
|
277
|
-
chartTitle: chart.title,
|
|
278
|
-
leftKPIs: chart.yLeft.length,
|
|
279
|
-
rightKPIs: chart.yRight.length,
|
|
280
|
-
renderDelay,
|
|
281
|
-
lazyRender
|
|
282
|
-
});
|
|
283
|
-
|
|
284
262
|
// Initial container size measurement
|
|
285
263
|
if (chartDiv) {
|
|
286
264
|
const rect = chartDiv.getBoundingClientRect();
|
|
@@ -312,7 +290,6 @@
|
|
|
312
290
|
(entries) => {
|
|
313
291
|
const [entry] = entries;
|
|
314
292
|
if (entry.isIntersecting && !chartInitialized) {
|
|
315
|
-
log('ποΈ Chart entering viewport', { chartTitle: chart.title });
|
|
316
293
|
isVisible = true;
|
|
317
294
|
doRender();
|
|
318
295
|
// Disconnect after first render - chart stays rendered
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import ChartCard from './ChartCard.svelte';
|
|
7
7
|
import GlobalControls from './GlobalControls.svelte';
|
|
8
8
|
import { getPreprocessedData, type ProcessedChartData } from './data-processor.js';
|
|
9
|
-
import { log } from '../logger';
|
|
10
9
|
import { checkHealth, getMessage } from '../FeatureRegistry';
|
|
11
10
|
|
|
12
11
|
interface Props {
|
|
@@ -58,18 +57,6 @@
|
|
|
58
57
|
|
|
59
58
|
let { layout, data, mode, markers, plotlyLayout, enableAdaptation = true, showGlobalControls = true, persistSettings = false }: Props = $props();
|
|
60
59
|
let isHealthy = $state(checkHealth('charts'));
|
|
61
|
-
// Log component initialization
|
|
62
|
-
$effect(() => {
|
|
63
|
-
log('π ChartComponent initialized', {
|
|
64
|
-
mode,
|
|
65
|
-
dataRows: data.length,
|
|
66
|
-
sections: layout.sections.length,
|
|
67
|
-
totalCharts: layout.sections.reduce((sum, s) => sum + s.charts.length, 0),
|
|
68
|
-
enableAdaptation,
|
|
69
|
-
showGlobalControls,
|
|
70
|
-
layoutHoverMode: layout.hoverMode
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
60
|
|
|
74
61
|
// Preprocess raw data once - automatically memoized by Svelte's $derived
|
|
75
62
|
// This extracts all KPI values and timestamps, cached until data or layout changes
|
|
@@ -98,13 +85,6 @@
|
|
|
98
85
|
|
|
99
86
|
// Handler for global controls updates
|
|
100
87
|
function handleControlsUpdate(updatedControls: GlobalChartControls) {
|
|
101
|
-
log('ποΈ Global controls updated', {
|
|
102
|
-
movingAverageEnabled: updatedControls.movingAverage?.enabled,
|
|
103
|
-
windowOverride: updatedControls.movingAverage?.windowOverride,
|
|
104
|
-
markersEnabled: updatedControls.markers?.enabled,
|
|
105
|
-
legendEnabled: updatedControls.legend?.enabled,
|
|
106
|
-
hoverMode: updatedControls.hoverMode?.mode
|
|
107
|
-
});
|
|
108
88
|
globalControls = updatedControls;
|
|
109
89
|
}
|
|
110
90
|
|
|
@@ -195,17 +175,12 @@
|
|
|
195
175
|
|
|
196
176
|
function zoomSelectedChart() {
|
|
197
177
|
if (contextMenu.chart && contextMenu.section) {
|
|
198
|
-
log('π Zooming chart', {
|
|
199
|
-
chartTitle: contextMenu.chart.title,
|
|
200
|
-
sectionId: contextMenu.section.id
|
|
201
|
-
});
|
|
202
178
|
zoomedChart = { chart: contextMenu.chart, section: contextMenu.section };
|
|
203
179
|
}
|
|
204
180
|
closeContextMenu();
|
|
205
181
|
}
|
|
206
182
|
|
|
207
183
|
function exitZoom() {
|
|
208
|
-
log('π Exiting zoom mode');
|
|
209
184
|
zoomedChart = null;
|
|
210
185
|
closeContextMenu();
|
|
211
186
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { log } from '../logger';
|
|
2
1
|
/**
|
|
3
2
|
* Extract all unique KPI rawNames from a layout configuration
|
|
4
3
|
* This determines which columns we need to extract from raw data
|
|
@@ -17,10 +16,6 @@ export function extractKPINames(layout) {
|
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
log('π KPI names extracted', {
|
|
21
|
-
totalKPIs: kpiNames.size,
|
|
22
|
-
kpiNames: Array.from(kpiNames)
|
|
23
|
-
});
|
|
24
19
|
return kpiNames;
|
|
25
20
|
}
|
|
26
21
|
/**
|
|
@@ -33,11 +28,6 @@ export function extractKPINames(layout) {
|
|
|
33
28
|
* @returns Preprocessed data ready for chart rendering
|
|
34
29
|
*/
|
|
35
30
|
export function preprocessChartData(data, layout, timestampField = 'TIMESTAMP') {
|
|
36
|
-
log('π Preprocessing chart data', {
|
|
37
|
-
rawDataRows: data.length,
|
|
38
|
-
sections: layout.sections.length,
|
|
39
|
-
timestampField
|
|
40
|
-
});
|
|
41
31
|
// Extract all unique KPI names we need to process
|
|
42
32
|
const kpiNames = extractKPINames(layout);
|
|
43
33
|
// Initialize the result map
|
|
@@ -53,17 +43,11 @@ export function preprocessChartData(data, layout, timestampField = 'TIMESTAMP')
|
|
|
53
43
|
.filter(val => !isNaN(val)); // Remove invalid values
|
|
54
44
|
kpiValues.set(kpiName, values);
|
|
55
45
|
if (values.length === 0) {
|
|
56
|
-
|
|
46
|
+
console.warn(`[Charts] No valid values found for KPI: ${kpiName}`);
|
|
57
47
|
}
|
|
58
48
|
}
|
|
59
49
|
// Extract timestamps once
|
|
60
50
|
const timestamps = data.map(row => row[timestampField]);
|
|
61
|
-
log('β
Data preprocessing complete', {
|
|
62
|
-
processedKPIs: kpiValues.size,
|
|
63
|
-
timestampCount: timestamps.length,
|
|
64
|
-
sampleKPI: kpiValues.keys().next().value,
|
|
65
|
-
sampleValues: kpiValues.values().next().value?.slice(0, 3)
|
|
66
|
-
});
|
|
67
51
|
return {
|
|
68
52
|
kpiValues,
|
|
69
53
|
timestamps,
|
|
@@ -91,11 +75,9 @@ export function getPreprocessedData(data, layout, timestampField = 'TIMESTAMP')
|
|
|
91
75
|
if (cached) {
|
|
92
76
|
// Verify cache is still valid (data reference matches)
|
|
93
77
|
if (cached._rawDataRef === data) {
|
|
94
|
-
log('πΎ Using cached preprocessed data');
|
|
95
78
|
return cached;
|
|
96
79
|
}
|
|
97
80
|
}
|
|
98
|
-
log('π Cache miss - preprocessing data');
|
|
99
81
|
// Cache miss or invalid - compute and cache
|
|
100
82
|
const processed = preprocessChartData(data, layout, timestampField);
|
|
101
83
|
preprocessCache.set(data, processed);
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -10,6 +10,8 @@ export * from './TreeView/index.js';
|
|
|
10
10
|
export * from './Settings/index.js';
|
|
11
11
|
// Logger utility for debugging and monitoring
|
|
12
12
|
export * from './logger/index.js';
|
|
13
|
+
// CellTable - Tabulator-based cell data table with Bootstrap theming
|
|
14
|
+
export * from './CellTable/index.js';
|
|
13
15
|
// Map component - Mapbox GL + Deck.GL integration
|
|
14
16
|
// TODO: Moved to top-level src/lib/map module
|
|
15
17
|
// export * from './Map/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartnet360/svelte-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.104",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"dexie": "^4.0.11",
|
|
40
40
|
"mapbox-gl": "^3.0.0",
|
|
41
41
|
"plotly.js-dist-min": "^3.1.0",
|
|
42
|
-
"svelte": "^5.0.0"
|
|
42
|
+
"svelte": "^5.0.0",
|
|
43
|
+
"tabulator-tables": "^6.3.1"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@eslint/compat": "^1.2.5",
|
|
@@ -1,56 +0,0 @@
|
|
|
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;
|
|
@@ -1,273 +0,0 @@
|
|
|
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
|
-
}
|