@smartnet360/svelte-components 0.0.34 → 0.0.35
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 +96 -0
- package/dist/apps/site-check/SiteCheck.svelte.d.ts +12 -0
- package/dist/apps/site-check/data-loader.d.ts +29 -0
- package/dist/apps/site-check/data-loader.js +105 -0
- package/dist/apps/site-check/helper.d.ts +19 -0
- package/dist/apps/site-check/helper.js +103 -0
- package/dist/apps/site-check/index.d.ts +8 -0
- package/dist/apps/site-check/index.js +12 -0
- package/dist/apps/site-check/transforms.d.ts +24 -0
- package/dist/apps/site-check/transforms.js +142 -0
- package/dist/core/Charts/ChartCard.svelte +4 -2
- package/dist/core/Charts/charts.model.d.ts +3 -0
- package/dist/core/Charts/data-utils.d.ts +4 -2
- package/dist/core/Charts/data-utils.js +69 -16
- package/dist/core/TreeView/TreeView.svelte +2 -2
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +0 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/package.json +1 -1
- package/dist/cellular/CellularChartsView.svelte +0 -293
- package/dist/cellular/CellularChartsView.svelte.d.ts +0 -7
- package/dist/cellular/HierarchicalTree.svelte +0 -469
- package/dist/cellular/HierarchicalTree.svelte.d.ts +0 -9
- package/dist/cellular/SiteTree.svelte +0 -286
- package/dist/cellular/SiteTree.svelte.d.ts +0 -11
- package/dist/cellular/cellular-transforms.d.ts +0 -25
- package/dist/cellular/cellular-transforms.js +0 -129
- package/dist/cellular/cellular.model.d.ts +0 -63
- package/dist/cellular/cellular.model.js +0 -6
- package/dist/cellular/index.d.ts +0 -11
- package/dist/cellular/index.js +0 -11
- package/dist/cellular/mock-cellular-data.d.ts +0 -13
- package/dist/cellular/mock-cellular-data.js +0 -241
- package/dist/core/TreeChartView/TreeChartView.svelte +0 -208
- package/dist/core/TreeChartView/TreeChartView.svelte.d.ts +0 -42
- package/dist/core/TreeChartView/index.d.ts +0 -7
- package/dist/core/TreeChartView/index.js +0 -7
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { TreeView, createTreeStore } from '../../core/TreeView';
|
|
5
|
+
import { ChartComponent, type Layout } from '../../core/Charts';
|
|
6
|
+
import { buildTreeNodes, filterChartData, transformChartData, type CellTrafficRecord } from './index';
|
|
7
|
+
import { expandLayoutForCells } from './helper';
|
|
8
|
+
import { onMount } from 'svelte';
|
|
9
|
+
import type { Mode } from '../../index.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
rawData: CellTrafficRecord[];
|
|
13
|
+
baseLayout: Layout;
|
|
14
|
+
baseMetrics: string[];
|
|
15
|
+
mode: Mode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { rawData, baseLayout, baseMetrics, mode = "scrollspy" }: Props = $props();
|
|
19
|
+
|
|
20
|
+
let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
21
|
+
|
|
22
|
+
onMount(() => {
|
|
23
|
+
// Build tree nodes from raw data
|
|
24
|
+
const treeNodes = buildTreeNodes(rawData);
|
|
25
|
+
|
|
26
|
+
// Create tree store
|
|
27
|
+
treeStore = createTreeStore({
|
|
28
|
+
nodes: treeNodes,
|
|
29
|
+
namespace: 'site-check',
|
|
30
|
+
persistState: true,
|
|
31
|
+
defaultExpandAll: false
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Derive chart data from tree selection
|
|
36
|
+
let filteredData = $derived.by(() => {
|
|
37
|
+
if (!treeStore) return [];
|
|
38
|
+
const storeValue = $treeStore;
|
|
39
|
+
if (!storeValue) return [];
|
|
40
|
+
return filterChartData(rawData, storeValue.state.checkedPaths);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Transform data using base metrics from layout
|
|
44
|
+
let chartData = $derived(transformChartData(filteredData, baseMetrics));
|
|
45
|
+
|
|
46
|
+
// Expand layout based on selected cells
|
|
47
|
+
let chartLayout = $derived(expandLayoutForCells(baseLayout, filteredData));
|
|
48
|
+
console.log('chartLayout', chartLayout);
|
|
49
|
+
|
|
50
|
+
let totalRecords = $derived(rawData.length);
|
|
51
|
+
let visibleRecords = $derived(filteredData.length);
|
|
52
|
+
|
|
53
|
+
// Compute simple stats
|
|
54
|
+
let totalCells = $derived(new Set(filteredData.map((r) => r.cellName)).size);
|
|
55
|
+
let totalSites = $derived(new Set(filteredData.map((r) => r.siteName)).size);
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class="container-fluid vh-100 d-flex flex-column">
|
|
59
|
+
<!-- Main Content -->
|
|
60
|
+
<div class="row flex-grow-1 ">
|
|
61
|
+
<!-- Left: Tree View -->
|
|
62
|
+
<div class="col-lg-3 col-md-4 border-end bg-white overflow-auto">
|
|
63
|
+
<div class="p-3">
|
|
64
|
+
<!-- <h5 class="mb-3">
|
|
65
|
+
<span class="me-2">📡</span>
|
|
66
|
+
Site Selection
|
|
67
|
+
</h5> -->
|
|
68
|
+
|
|
69
|
+
{#if treeStore}
|
|
70
|
+
<TreeView store={$treeStore!} showControls={false} showIndeterminate={true} height="100%" />
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Right: Charts -->
|
|
76
|
+
<div class="col-lg-9 col-md-8 bg-light overflow-auto">
|
|
77
|
+
{#if chartData.length > 0}
|
|
78
|
+
<ChartComponent
|
|
79
|
+
layout={chartLayout}
|
|
80
|
+
data={chartData}
|
|
81
|
+
mode={mode}
|
|
82
|
+
showGlobalControls={true}
|
|
83
|
+
enableAdaptation={true}
|
|
84
|
+
/>
|
|
85
|
+
{:else}
|
|
86
|
+
<div class="d-flex align-items-center justify-content-center h-100">
|
|
87
|
+
<div class="text-center text-muted">
|
|
88
|
+
<div class="mb-3" style="font-size: 4rem;">📊</div>
|
|
89
|
+
<h5>No Data Selected</h5>
|
|
90
|
+
<p>Select one or more cells from the tree to display KPI charts.</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Layout } from '../../core/Charts';
|
|
2
|
+
import { type CellTrafficRecord } from './index';
|
|
3
|
+
import type { Mode } from '../../index.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
rawData: CellTrafficRecord[];
|
|
6
|
+
baseLayout: Layout;
|
|
7
|
+
baseMetrics: string[];
|
|
8
|
+
mode: Mode;
|
|
9
|
+
}
|
|
10
|
+
declare const SiteCheck: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type SiteCheck = ReturnType<typeof SiteCheck>;
|
|
12
|
+
export default SiteCheck;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Loader for Site Check Component
|
|
3
|
+
* Loads and parses cell_traffic_with_band.csv
|
|
4
|
+
*/
|
|
5
|
+
export interface CellTrafficRecord {
|
|
6
|
+
date: string;
|
|
7
|
+
cellName: string;
|
|
8
|
+
siteName: string;
|
|
9
|
+
sector: number;
|
|
10
|
+
azimuth: number;
|
|
11
|
+
band: string;
|
|
12
|
+
metrics: Record<string, number>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Load cell traffic data from CSV file
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadCellTrafficData(): Promise<CellTrafficRecord[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Get unique sites from data
|
|
20
|
+
*/
|
|
21
|
+
export declare function getUniqueSites(data: CellTrafficRecord[]): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Get unique cells from data
|
|
24
|
+
*/
|
|
25
|
+
export declare function getUniqueCells(data: CellTrafficRecord[]): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Group data by cell name for efficient lookup
|
|
28
|
+
*/
|
|
29
|
+
export declare function groupDataByCell(data: CellTrafficRecord[]): Map<string, CellTrafficRecord[]>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Loader for Site Check Component
|
|
3
|
+
* Loads and parses cell_traffic_with_band.csv
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Load cell traffic data from CSV file
|
|
7
|
+
*/
|
|
8
|
+
export async function loadCellTrafficData() {
|
|
9
|
+
const response = await fetch('/cell_traffic_with_band.csv');
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(`Failed to load CSV: ${response.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
const text = await response.text();
|
|
14
|
+
return parseCsvData(text);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse CSV text into structured records
|
|
18
|
+
*/
|
|
19
|
+
function parseCsvData(csv) {
|
|
20
|
+
const lines = csv.trim().split('\n');
|
|
21
|
+
// Parse header to identify metric columns
|
|
22
|
+
const header = lines[0].split(',').map(h => h.trim());
|
|
23
|
+
const dataLines = lines.slice(1);
|
|
24
|
+
return dataLines
|
|
25
|
+
.map((line) => {
|
|
26
|
+
const parts = line.split(',');
|
|
27
|
+
// Handle potential issues with CSV parsing
|
|
28
|
+
if (parts.length < 8) {
|
|
29
|
+
console.warn('Skipping malformed CSV line:', line);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Fixed columns (metadata) - based on known CSV structure
|
|
33
|
+
const date = parts[0].trim();
|
|
34
|
+
const cellName = parts[1].trim();
|
|
35
|
+
const siteName = parts[4].trim();
|
|
36
|
+
const sector = parseInt(parts[5]);
|
|
37
|
+
const azimuth = parseInt(parts[6]);
|
|
38
|
+
const band = parts[7].trim();
|
|
39
|
+
// Dynamic metrics - automatically map ALL numeric columns from CSV
|
|
40
|
+
const metrics = {};
|
|
41
|
+
// Start from column 2 (skip PERIOD_START_TIME, CELL_NAME)
|
|
42
|
+
// Stop before metadata columns (SITE_NAME, SECTOR, AZIMUTH, band)
|
|
43
|
+
for (let i = 2; i < Math.min(parts.length, 4); i++) {
|
|
44
|
+
const columnName = header[i]; // Use exact column name from CSV header
|
|
45
|
+
const value = parseFloat(parts[i]);
|
|
46
|
+
if (columnName && !isNaN(value)) {
|
|
47
|
+
metrics[columnName] = value; // e.g., metrics['DL_GBYTES'] = 123.45
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Add any additional metric columns after band column
|
|
51
|
+
for (let i = 8; i < parts.length; i++) {
|
|
52
|
+
const columnName = header[i];
|
|
53
|
+
const value = parseFloat(parts[i]);
|
|
54
|
+
if (columnName && !isNaN(value)) {
|
|
55
|
+
metrics[columnName] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
date,
|
|
60
|
+
cellName,
|
|
61
|
+
siteName,
|
|
62
|
+
sector,
|
|
63
|
+
azimuth,
|
|
64
|
+
band,
|
|
65
|
+
metrics
|
|
66
|
+
};
|
|
67
|
+
})
|
|
68
|
+
.filter((record) => record !== null);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get unique sites from data
|
|
72
|
+
*/
|
|
73
|
+
export function getUniqueSites(data) {
|
|
74
|
+
const sites = new Set();
|
|
75
|
+
data.forEach((record) => sites.add(record.siteName));
|
|
76
|
+
return Array.from(sites).sort();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get unique cells from data
|
|
80
|
+
*/
|
|
81
|
+
export function getUniqueCells(data) {
|
|
82
|
+
const cells = new Set();
|
|
83
|
+
data.forEach((record) => cells.add(record.cellName));
|
|
84
|
+
return Array.from(cells).sort();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Group data by cell name for efficient lookup
|
|
88
|
+
*/
|
|
89
|
+
export function groupDataByCell(data) {
|
|
90
|
+
const grouped = new Map();
|
|
91
|
+
data.forEach((record) => {
|
|
92
|
+
if (!grouped.has(record.cellName)) {
|
|
93
|
+
grouped.set(record.cellName, []);
|
|
94
|
+
}
|
|
95
|
+
grouped.get(record.cellName).push(record);
|
|
96
|
+
});
|
|
97
|
+
// Sort each cell's data by date
|
|
98
|
+
grouped.forEach((records) => {
|
|
99
|
+
records.sort((a, b) => {
|
|
100
|
+
// Simple date string comparison (works for DD-MMM-YY format)
|
|
101
|
+
return a.date.localeCompare(b.date);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return grouped;
|
|
105
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Layout } from '../../core/Charts';
|
|
2
|
+
import type { CellTrafficRecord } from './';
|
|
3
|
+
/**
|
|
4
|
+
* Expand base layout configuration with dynamic KPIs based on selected cells
|
|
5
|
+
* Takes a base layout (with one KPI per metric) and expands it to include one KPI per cell
|
|
6
|
+
*
|
|
7
|
+
* @param baseLayout - The base layout configuration from JSON
|
|
8
|
+
* @param data - Filtered cell traffic records for selected cells
|
|
9
|
+
* @returns Expanded layout with cell-specific KPIs
|
|
10
|
+
*/
|
|
11
|
+
export declare function expandLayoutForCells(baseLayout: Layout, data: CellTrafficRecord[]): Layout;
|
|
12
|
+
/**
|
|
13
|
+
* Extract base metric names from a layout configuration
|
|
14
|
+
* Returns unique metric rawNames that need to be pivoted
|
|
15
|
+
*
|
|
16
|
+
* @param layout - The layout configuration
|
|
17
|
+
* @returns Array of unique metric names (e.g., ['dlGBytes', 'ulGBytes'])
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractBaseMetrics(layout: Layout): string[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand base layout configuration with dynamic KPIs based on selected cells
|
|
3
|
+
* Takes a base layout (with one KPI per metric) and expands it to include one KPI per cell
|
|
4
|
+
*
|
|
5
|
+
* @param baseLayout - The base layout configuration from JSON
|
|
6
|
+
* @param data - Filtered cell traffic records for selected cells
|
|
7
|
+
* @returns Expanded layout with cell-specific KPIs
|
|
8
|
+
*/
|
|
9
|
+
export function expandLayoutForCells(baseLayout, data) {
|
|
10
|
+
// Get unique cells and their metadata
|
|
11
|
+
const cellMap = new Map();
|
|
12
|
+
data.forEach((record) => {
|
|
13
|
+
if (!cellMap.has(record.cellName)) {
|
|
14
|
+
cellMap.set(record.cellName, record);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const cells = Array.from(cellMap.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
18
|
+
// Deep clone the layout structure and expand KPIs
|
|
19
|
+
const expandedLayout = {
|
|
20
|
+
layoutName: baseLayout.layoutName,
|
|
21
|
+
sections: baseLayout.sections.map((section) => ({
|
|
22
|
+
...section,
|
|
23
|
+
charts: section.charts.map((chart) => ({
|
|
24
|
+
...chart,
|
|
25
|
+
yLeft: expandKPIs(chart.yLeft, cells),
|
|
26
|
+
yRight: expandKPIs(chart.yRight, cells)
|
|
27
|
+
}))
|
|
28
|
+
}))
|
|
29
|
+
};
|
|
30
|
+
return expandedLayout;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Expand a single KPI into multiple KPIs (one per cell)
|
|
34
|
+
*
|
|
35
|
+
* @param baseKPIs - Array of base KPIs from layout
|
|
36
|
+
* @param cells - Array of [cellName, record] tuples
|
|
37
|
+
* @returns Expanded array of KPIs with cell-specific rawNames and colors
|
|
38
|
+
*/
|
|
39
|
+
function expandKPIs(baseKPIs, cells) {
|
|
40
|
+
const expandedKPIs = [];
|
|
41
|
+
baseKPIs.forEach((baseKPI) => {
|
|
42
|
+
cells.forEach(([cellName, record], index) => {
|
|
43
|
+
expandedKPIs.push({
|
|
44
|
+
rawName: `${baseKPI.rawName}_${cellName}`,
|
|
45
|
+
name: `${cellName} (${record.band}) - ${baseKPI.name}`,
|
|
46
|
+
scale: baseKPI.scale,
|
|
47
|
+
unit: baseKPI.unit,
|
|
48
|
+
color: getColorForIndex(index)
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return expandedKPIs;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract base metric names from a layout configuration
|
|
56
|
+
* Returns unique metric rawNames that need to be pivoted
|
|
57
|
+
*
|
|
58
|
+
* @param layout - The layout configuration
|
|
59
|
+
* @returns Array of unique metric names (e.g., ['dlGBytes', 'ulGBytes'])
|
|
60
|
+
*/
|
|
61
|
+
export function extractBaseMetrics(layout) {
|
|
62
|
+
const metrics = new Set();
|
|
63
|
+
layout.sections.forEach((section) => {
|
|
64
|
+
section.charts.forEach((chart) => {
|
|
65
|
+
chart.yLeft.forEach((kpi) => metrics.add(kpi.rawName));
|
|
66
|
+
chart.yRight.forEach((kpi) => metrics.add(kpi.rawName));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return Array.from(metrics);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get a distinct color for each cell line
|
|
73
|
+
* Uses a predefined color palette with good contrast
|
|
74
|
+
*/
|
|
75
|
+
function getColorForIndex(index) {
|
|
76
|
+
const colors = [
|
|
77
|
+
'#0d6efd', // Blue
|
|
78
|
+
'#198754', // Green
|
|
79
|
+
'#dc3545', // Red
|
|
80
|
+
'#ffc107', // Yellow
|
|
81
|
+
'#0dcaf0', // Cyan
|
|
82
|
+
'#6f42c1', // Purple
|
|
83
|
+
'#fd7e14', // Orange
|
|
84
|
+
'#20c997', // Teal
|
|
85
|
+
'#d63384', // Pink
|
|
86
|
+
'#6610f2', // Indigo
|
|
87
|
+
'#17a2b8', // Info
|
|
88
|
+
'#28a745', // Success
|
|
89
|
+
'#e83e8c', // Magenta
|
|
90
|
+
'#6c757d', // Gray
|
|
91
|
+
'#007bff', // Primary
|
|
92
|
+
'#28a745', // Green variant
|
|
93
|
+
'#17a2b8', // Cyan variant
|
|
94
|
+
'#ffc107', // Amber
|
|
95
|
+
'#dc3545', // Danger
|
|
96
|
+
'#343a40', // Dark
|
|
97
|
+
'#6c757d', // Secondary
|
|
98
|
+
'#fd7e14', // Orange variant
|
|
99
|
+
'#20c997', // Teal variant
|
|
100
|
+
'#6f42c1' // Violet
|
|
101
|
+
];
|
|
102
|
+
return colors[index % colors.length];
|
|
103
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Check Component
|
|
3
|
+
* Public API exports for cell traffic KPI visualization
|
|
4
|
+
*/
|
|
5
|
+
export { default as SiteCheck } from './SiteCheck.svelte';
|
|
6
|
+
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, type CellTrafficRecord } from './data-loader.js';
|
|
7
|
+
export { buildTreeNodes, filterChartData, transformChartData } from './transforms.js';
|
|
8
|
+
export { expandLayoutForCells, extractBaseMetrics } from './helper.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Check Component
|
|
3
|
+
* Public API exports for cell traffic KPI visualization
|
|
4
|
+
*/
|
|
5
|
+
// Components
|
|
6
|
+
export { default as SiteCheck } from './SiteCheck.svelte';
|
|
7
|
+
// Data loading
|
|
8
|
+
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell } from './data-loader.js';
|
|
9
|
+
// Data transforms
|
|
10
|
+
export { buildTreeNodes, filterChartData, transformChartData } from './transforms.js';
|
|
11
|
+
// Helper utilities (for external configuration)
|
|
12
|
+
export { expandLayoutForCells, extractBaseMetrics } from './helper.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { CellTrafficRecord } from './data-loader';
|
|
7
|
+
/**
|
|
8
|
+
* Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildTreeNodes(data: CellTrafficRecord[]): TreeNode[];
|
|
11
|
+
/**
|
|
12
|
+
* Filter chart data based on selected tree paths
|
|
13
|
+
* Only include cells that are checked in the tree
|
|
14
|
+
*/
|
|
15
|
+
export declare function filterChartData(data: CellTrafficRecord[], checkedPaths: Set<string>): CellTrafficRecord[];
|
|
16
|
+
/**
|
|
17
|
+
* Transform data for chart component consumption
|
|
18
|
+
* Pivots data so each cell becomes its own KPI column
|
|
19
|
+
* Transforms from long format (many rows per cell) to wide format (one column per cell)
|
|
20
|
+
*
|
|
21
|
+
* @param data - Filtered cell traffic records
|
|
22
|
+
* @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
|
|
23
|
+
*/
|
|
24
|
+
export declare function transformChartData(data: CellTrafficRecord[], baseMetrics: string[]): any[];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Transforms for Site Check Component
|
|
3
|
+
* Converts raw CSV data to TreeView nodes and Chart configurations
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
|
|
7
|
+
*/
|
|
8
|
+
export function buildTreeNodes(data) {
|
|
9
|
+
// Group by site → azimuth → cell
|
|
10
|
+
const siteMap = new Map();
|
|
11
|
+
data.forEach((record) => {
|
|
12
|
+
if (!siteMap.has(record.siteName)) {
|
|
13
|
+
siteMap.set(record.siteName, new Map());
|
|
14
|
+
}
|
|
15
|
+
const azimuthMap = siteMap.get(record.siteName);
|
|
16
|
+
if (!azimuthMap.has(record.azimuth)) {
|
|
17
|
+
azimuthMap.set(record.azimuth, new Map());
|
|
18
|
+
}
|
|
19
|
+
const cellMap = azimuthMap.get(record.azimuth);
|
|
20
|
+
// Store one record per cell (we just need metadata, not all time series)
|
|
21
|
+
if (!cellMap.has(record.cellName)) {
|
|
22
|
+
cellMap.set(record.cellName, record);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
// Build tree structure
|
|
26
|
+
const treeNodes = [];
|
|
27
|
+
Array.from(siteMap.entries())
|
|
28
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
29
|
+
.forEach(([siteName, azimuthMap]) => {
|
|
30
|
+
const siteNode = {
|
|
31
|
+
id: siteName, // Simple ID
|
|
32
|
+
label: `Site ${siteName}`,
|
|
33
|
+
// icon: '📡',
|
|
34
|
+
metadata: { type: 'site', siteName },
|
|
35
|
+
defaultExpanded: false,
|
|
36
|
+
children: []
|
|
37
|
+
};
|
|
38
|
+
Array.from(azimuthMap.entries())
|
|
39
|
+
.sort(([a], [b]) => a - b)
|
|
40
|
+
.forEach(([azimuth, cellMap]) => {
|
|
41
|
+
const sectorNode = {
|
|
42
|
+
id: `${azimuth}`, // Simple ID (just azimuth)
|
|
43
|
+
label: `${azimuth}° Sector`,
|
|
44
|
+
// icon: '📍',
|
|
45
|
+
metadata: { type: 'sector', azimuth, siteName },
|
|
46
|
+
defaultExpanded: false,
|
|
47
|
+
children: []
|
|
48
|
+
};
|
|
49
|
+
Array.from(cellMap.entries())
|
|
50
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
51
|
+
.forEach(([cellName, record]) => {
|
|
52
|
+
const cellNode = {
|
|
53
|
+
id: cellName, // Simple ID (just cell name)
|
|
54
|
+
label: `${cellName} (${record.band})`,
|
|
55
|
+
icon: getBandIcon(record.band),
|
|
56
|
+
metadata: {
|
|
57
|
+
type: 'cell',
|
|
58
|
+
cellName,
|
|
59
|
+
band: record.band,
|
|
60
|
+
siteName: record.siteName,
|
|
61
|
+
sector: record.sector,
|
|
62
|
+
azimuth: record.azimuth
|
|
63
|
+
},
|
|
64
|
+
defaultChecked: true
|
|
65
|
+
};
|
|
66
|
+
sectorNode.children.push(cellNode);
|
|
67
|
+
});
|
|
68
|
+
siteNode.children.push(sectorNode);
|
|
69
|
+
});
|
|
70
|
+
treeNodes.push(siteNode);
|
|
71
|
+
});
|
|
72
|
+
return treeNodes;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get icon emoji based on band technology
|
|
76
|
+
*/
|
|
77
|
+
function getBandIcon(band) {
|
|
78
|
+
return '';
|
|
79
|
+
if (band.startsWith('NR'))
|
|
80
|
+
return '📶'; // 5G
|
|
81
|
+
if (band.startsWith('LTE'))
|
|
82
|
+
return '📱'; // 4G
|
|
83
|
+
return '📡'; // Fallback
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Filter chart data based on selected tree paths
|
|
87
|
+
* Only include cells that are checked in the tree
|
|
88
|
+
*/
|
|
89
|
+
export function filterChartData(data, checkedPaths) {
|
|
90
|
+
// Extract cell names from checked leaf paths (format: "site:azimuth:cellName")
|
|
91
|
+
const selectedCells = new Set();
|
|
92
|
+
checkedPaths.forEach((path) => {
|
|
93
|
+
const parts = path.split(':');
|
|
94
|
+
if (parts.length === 3) {
|
|
95
|
+
// This is a cell-level path (site:azimuth:cellName)
|
|
96
|
+
selectedCells.add(parts[2]);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// Filter data to only include selected cells
|
|
100
|
+
return data.filter((record) => selectedCells.has(record.cellName));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Transform data for chart component consumption
|
|
104
|
+
* Pivots data so each cell becomes its own KPI column
|
|
105
|
+
* Transforms from long format (many rows per cell) to wide format (one column per cell)
|
|
106
|
+
*
|
|
107
|
+
* @param data - Filtered cell traffic records
|
|
108
|
+
* @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
|
|
109
|
+
*/
|
|
110
|
+
export function transformChartData(data, baseMetrics) {
|
|
111
|
+
// Group data by date
|
|
112
|
+
const dateMap = new Map();
|
|
113
|
+
data.forEach((record) => {
|
|
114
|
+
if (!dateMap.has(record.date)) {
|
|
115
|
+
dateMap.set(record.date, new Map());
|
|
116
|
+
}
|
|
117
|
+
dateMap.get(record.date).set(record.cellName, record);
|
|
118
|
+
});
|
|
119
|
+
// Build pivoted data: one row per date, one column per cell per metric
|
|
120
|
+
const pivotedData = [];
|
|
121
|
+
dateMap.forEach((cellsOnDate, date) => {
|
|
122
|
+
const row = {
|
|
123
|
+
TIMESTAMP: date
|
|
124
|
+
};
|
|
125
|
+
cellsOnDate.forEach((record, cellName) => {
|
|
126
|
+
// Pivot each base metric into cell-specific columns
|
|
127
|
+
baseMetrics.forEach((metricName) => {
|
|
128
|
+
const value = record.metrics[metricName];
|
|
129
|
+
if (value !== undefined) {
|
|
130
|
+
row[`${metricName}_${cellName}`] = value;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// Store metadata for reference (band, azimuth, etc.)
|
|
134
|
+
row[`BAND_${cellName}`] = record.band;
|
|
135
|
+
row[`AZIMUTH_${cellName}`] = record.azimuth;
|
|
136
|
+
});
|
|
137
|
+
pivotedData.push(row);
|
|
138
|
+
});
|
|
139
|
+
// Sort by date
|
|
140
|
+
pivotedData.sort((a, b) => a.TIMESTAMP.localeCompare(b.TIMESTAMP));
|
|
141
|
+
return pivotedData;
|
|
142
|
+
}
|
|
@@ -155,11 +155,13 @@
|
|
|
155
155
|
|
|
156
156
|
const traces: any[] = [];
|
|
157
157
|
let colorIndex = 0;
|
|
158
|
+
const chartType = chart.type || 'line'; // Default to 'line' if not specified
|
|
159
|
+
const stackGroup = chart.stackGroup;
|
|
158
160
|
|
|
159
161
|
// Add left Y-axis traces (with moving average support)
|
|
160
162
|
resolvedKPIs.left.forEach(kpi => {
|
|
161
163
|
const values = getKPIValues(processedData, kpi);
|
|
162
|
-
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y1', colorIndex);
|
|
164
|
+
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y1', colorIndex, chartType, stackGroup);
|
|
163
165
|
traces.push(...kpiTraces);
|
|
164
166
|
colorIndex++;
|
|
165
167
|
});
|
|
@@ -167,7 +169,7 @@
|
|
|
167
169
|
// Add right Y-axis traces (with moving average support)
|
|
168
170
|
resolvedKPIs.right.forEach(kpi => {
|
|
169
171
|
const values = getKPIValues(processedData, kpi);
|
|
170
|
-
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y2', colorIndex);
|
|
172
|
+
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y2', colorIndex, chartType, stackGroup);
|
|
171
173
|
traces.push(...kpiTraces);
|
|
172
174
|
colorIndex++;
|
|
173
175
|
});
|
|
@@ -15,12 +15,15 @@ export interface KPI {
|
|
|
15
15
|
}
|
|
16
16
|
export type ChartPosition = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
17
17
|
export type ChartGrid = "2x2" | "3x3" | "4x4" | "1x2" | "1x4" | "1x8";
|
|
18
|
+
export type ChartType = "line" | "stacked-area" | "stacked-percentage" | "bar" | "scatter";
|
|
18
19
|
export interface Chart {
|
|
19
20
|
pos?: ChartPosition;
|
|
20
21
|
title: string;
|
|
22
|
+
type?: ChartType;
|
|
21
23
|
yLeft: KPI[];
|
|
22
24
|
yRight: KPI[];
|
|
23
25
|
movingAverage?: MovingAverageConfig;
|
|
26
|
+
stackGroup?: string;
|
|
24
27
|
}
|
|
25
28
|
export interface Section {
|
|
26
29
|
id: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { KPI } from './charts.model.js';
|
|
2
2
|
export declare function processKPIData(data: any[], kpi: KPI): number[];
|
|
3
3
|
export declare function calculateMovingAverage(values: number[], window: number): number[];
|
|
4
|
-
export declare function createTimeSeriesTrace(values: number[], timestamps: any[], kpi: KPI, yaxis?: 'y1' | 'y2', colorIndex?: number): any;
|
|
4
|
+
export declare function createTimeSeriesTrace(values: number[], timestamps: any[], kpi: KPI, yaxis?: 'y1' | 'y2', colorIndex?: number, chartType?: string, stackGroup?: string): any;
|
|
5
5
|
/**
|
|
6
6
|
* Create time series trace(s) with optional moving average
|
|
7
7
|
* @param values - Pre-processed numeric values array for the KPI
|
|
@@ -9,9 +9,11 @@ export declare function createTimeSeriesTrace(values: number[], timestamps: any[
|
|
|
9
9
|
* @param kpi - KPI configuration (may include movingAverage config)
|
|
10
10
|
* @param yaxis - Which Y-axis to use ('y1' or 'y2')
|
|
11
11
|
* @param colorIndex - Index for color selection
|
|
12
|
+
* @param chartType - Type of chart (line, stacked-area, etc.)
|
|
13
|
+
* @param stackGroup - Optional stack group identifier
|
|
12
14
|
* @returns Array of traces (original + MA if configured)
|
|
13
15
|
*/
|
|
14
|
-
export declare function createTimeSeriesTraceWithMA(values: number[], timestamps: any[], kpi: KPI, yaxis?: 'y1' | 'y2', colorIndex?: number): any[];
|
|
16
|
+
export declare function createTimeSeriesTraceWithMA(values: number[], timestamps: any[], kpi: KPI, yaxis?: 'y1' | 'y2', colorIndex?: number, chartType?: string, stackGroup?: string): any[];
|
|
15
17
|
export declare function getYAxisTitle(kpis: KPI[]): string;
|
|
16
18
|
export declare function formatValue(value: number, scale: 'percent' | 'absolute', unit: string): string;
|
|
17
19
|
export declare function createDefaultPlotlyLayout(title?: string): any;
|