@smartnet360/svelte-components 0.0.33 → 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 +7 -6
- package/dist/core/Charts/ChartCard.svelte.d.ts +1 -2
- package/dist/core/Charts/ChartComponent.svelte +0 -2
- package/dist/core/Charts/adapt.d.ts +1 -2
- package/dist/core/Charts/adapt.js +7 -30
- package/dist/core/Charts/charts.model.d.ts +3 -2
- package/dist/core/Charts/data-utils.d.ts +4 -2
- package/dist/core/Charts/data-utils.js +71 -18
- package/dist/core/Charts/index.d.ts +1 -1
- 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
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import { onMount, createEventDispatcher } from 'svelte';
|
|
5
5
|
import Plotly from 'plotly.js-dist-min';
|
|
6
|
-
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig
|
|
6
|
+
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig } from './charts.model.js';
|
|
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';
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
markers?: ChartMarker[]; // Global markers for all charts
|
|
24
24
|
plotlyLayout?: any; // Optional custom Plotly layout for styling/theming
|
|
25
25
|
enableAdaptation?: boolean; // Enable size-based adaptations (default: true)
|
|
26
|
-
configuredHoverMode?: HoverMode; // Global hover mode from layout config
|
|
27
26
|
sectionId?: string;
|
|
28
27
|
sectionMovingAverage?: MovingAverageConfig; // Section-level MA config
|
|
29
28
|
layoutMovingAverage?: MovingAverageConfig; // Layout-level MA config
|
|
@@ -33,7 +32,7 @@
|
|
|
33
32
|
runtimeShowLegend?: boolean; // Runtime control for showing legend (default: true)
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
let { chart, processedData, markers, plotlyLayout, enableAdaptation = true,
|
|
35
|
+
let { chart, processedData, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true }: Props = $props();
|
|
37
36
|
|
|
38
37
|
// Chart container div and state
|
|
39
38
|
let chartDiv: HTMLElement;
|
|
@@ -156,11 +155,13 @@
|
|
|
156
155
|
|
|
157
156
|
const traces: any[] = [];
|
|
158
157
|
let colorIndex = 0;
|
|
158
|
+
const chartType = chart.type || 'line'; // Default to 'line' if not specified
|
|
159
|
+
const stackGroup = chart.stackGroup;
|
|
159
160
|
|
|
160
161
|
// Add left Y-axis traces (with moving average support)
|
|
161
162
|
resolvedKPIs.left.forEach(kpi => {
|
|
162
163
|
const values = getKPIValues(processedData, kpi);
|
|
163
|
-
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y1', colorIndex);
|
|
164
|
+
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y1', colorIndex, chartType, stackGroup);
|
|
164
165
|
traces.push(...kpiTraces);
|
|
165
166
|
colorIndex++;
|
|
166
167
|
});
|
|
@@ -168,7 +169,7 @@
|
|
|
168
169
|
// Add right Y-axis traces (with moving average support)
|
|
169
170
|
resolvedKPIs.right.forEach(kpi => {
|
|
170
171
|
const values = getKPIValues(processedData, kpi);
|
|
171
|
-
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y2', colorIndex);
|
|
172
|
+
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y2', colorIndex, chartType, stackGroup);
|
|
172
173
|
traces.push(...kpiTraces);
|
|
173
174
|
colorIndex++;
|
|
174
175
|
});
|
|
@@ -211,7 +212,7 @@
|
|
|
211
212
|
leftSeriesCount: chart.yLeft.length,
|
|
212
213
|
rightSeriesCount: chart.yRight.length
|
|
213
214
|
},
|
|
214
|
-
{ enableAdaptation
|
|
215
|
+
{ enableAdaptation }
|
|
215
216
|
);
|
|
216
217
|
|
|
217
218
|
// Add markers to the layout only if runtime control allows
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig
|
|
1
|
+
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig } from './charts.model.js';
|
|
2
2
|
import { type ProcessedChartData } from './data-processor.js';
|
|
3
3
|
interface Props {
|
|
4
4
|
chart: ChartModel;
|
|
@@ -6,7 +6,6 @@ interface Props {
|
|
|
6
6
|
markers?: ChartMarker[];
|
|
7
7
|
plotlyLayout?: any;
|
|
8
8
|
enableAdaptation?: boolean;
|
|
9
|
-
configuredHoverMode?: HoverMode;
|
|
10
9
|
sectionId?: string;
|
|
11
10
|
sectionMovingAverage?: MovingAverageConfig;
|
|
12
11
|
layoutMovingAverage?: MovingAverageConfig;
|
|
@@ -300,7 +300,6 @@
|
|
|
300
300
|
{markers}
|
|
301
301
|
{plotlyLayout}
|
|
302
302
|
{enableAdaptation}
|
|
303
|
-
configuredHoverMode={layout.hoverMode}
|
|
304
303
|
sectionId={section.id}
|
|
305
304
|
sectionMovingAverage={section.movingAverage}
|
|
306
305
|
layoutMovingAverage={layout.movingAverage}
|
|
@@ -443,7 +442,6 @@
|
|
|
443
442
|
{markers}
|
|
444
443
|
{plotlyLayout}
|
|
445
444
|
{enableAdaptation}
|
|
446
|
-
configuredHoverMode={layout.hoverMode}
|
|
447
445
|
sectionId={activeZoom.section.id}
|
|
448
446
|
sectionMovingAverage={activeZoom.section.movingAverage}
|
|
449
447
|
layoutMovingAverage={layout.movingAverage}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Chart-specific adaptation utilities for Plotly layouts
|
|
3
3
|
* Handles size-based adaptations while preserving external styling/theming
|
|
4
4
|
*/
|
|
5
|
-
import type { ChartMarker
|
|
5
|
+
import type { ChartMarker } from './charts.model.js';
|
|
6
6
|
export interface ContainerSize {
|
|
7
7
|
width: number;
|
|
8
8
|
height: number;
|
|
@@ -13,7 +13,6 @@ export interface ChartInfo {
|
|
|
13
13
|
}
|
|
14
14
|
export interface AdaptationConfig {
|
|
15
15
|
enableAdaptation?: boolean;
|
|
16
|
-
configuredHoverMode?: HoverMode;
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
19
18
|
* Adapts a Plotly layout based on container size
|