@smartnet360/svelte-components 0.0.36 → 0.0.38
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 +55 -9
- package/dist/apps/site-check/SiteCheck.svelte.d.ts +2 -1
- package/dist/apps/site-check/default-cell-styling.d.ts +6 -0
- package/dist/apps/site-check/default-cell-styling.js +24 -0
- package/dist/apps/site-check/default-cell-styling.json +20 -0
- package/dist/apps/site-check/helper.d.ts +3 -2
- package/dist/apps/site-check/helper.js +13 -13
- package/dist/apps/site-check/index.d.ts +2 -1
- package/dist/apps/site-check/index.js +4 -2
- package/dist/apps/site-check/transforms.d.ts +13 -0
- package/dist/apps/site-check/transforms.js +74 -1
- package/dist/core/Charts/ChartCard.svelte +27 -5
- package/dist/core/Charts/ChartCard.svelte.d.ts +3 -1
- package/dist/core/Charts/ChartComponent.svelte +26 -0
- package/dist/core/Charts/adapt.js +19 -27
- package/dist/core/Charts/charts.model.d.ts +9 -0
- package/dist/core/Charts/data-processor.js +21 -0
- package/dist/core/Charts/data-utils.d.ts +4 -4
- package/dist/core/Charts/data-utils.js +40 -8
- package/dist/core/Charts/index.d.ts +1 -1
- package/dist/core/TreeView/tree-utils.js +22 -2
- package/dist/core/TreeView/tree.store.js +38 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/dist/core/logger/index.d.ts +26 -0
- package/dist/core/logger/index.js +42 -0
- package/package.json +1 -1
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import { TreeView, createTreeStore } from '../../core/TreeView';
|
|
5
|
-
import { ChartComponent, type Layout } from '../../core/Charts';
|
|
6
|
-
import { buildTreeNodes, filterChartData, transformChartData, type CellTrafficRecord } from './index';
|
|
5
|
+
import { ChartComponent, type Layout, type CellStylingConfig } from '../../core/Charts';
|
|
6
|
+
import { buildTreeNodes, filterChartData, transformChartData, type CellTrafficRecord, defaultCellStyling } from './index';
|
|
7
7
|
import { expandLayoutForCells } from './helper';
|
|
8
|
+
import { log } from '../../core/logger';
|
|
8
9
|
import { onMount } from 'svelte';
|
|
9
10
|
import type { Mode } from '../../index.js';
|
|
10
11
|
|
|
@@ -13,15 +14,26 @@
|
|
|
13
14
|
baseLayout: Layout;
|
|
14
15
|
baseMetrics: string[];
|
|
15
16
|
mode: Mode;
|
|
17
|
+
cellStyling?: CellStylingConfig; // Optional cell styling config (defaults to defaultCellStyling)
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
let { rawData, baseLayout, baseMetrics, mode = "scrollspy" }: Props = $props();
|
|
20
|
+
let { rawData, baseLayout, baseMetrics, mode = "scrollspy", cellStyling = defaultCellStyling }: Props = $props();
|
|
19
21
|
|
|
20
22
|
let treeStore = $state<ReturnType<typeof createTreeStore> | null>(null);
|
|
21
23
|
|
|
22
24
|
onMount(() => {
|
|
25
|
+
log('🚀 SiteCheck Initializing', {
|
|
26
|
+
totalRecords: rawData.length,
|
|
27
|
+
baseMetrics,
|
|
28
|
+
mode
|
|
29
|
+
});
|
|
30
|
+
|
|
23
31
|
// Build tree nodes from raw data
|
|
24
32
|
const treeNodes = buildTreeNodes(rawData);
|
|
33
|
+
log('🌲 Tree Nodes Built', {
|
|
34
|
+
nodeCount: treeNodes.length,
|
|
35
|
+
firstNode: treeNodes[0]
|
|
36
|
+
});
|
|
25
37
|
|
|
26
38
|
// Create tree store
|
|
27
39
|
treeStore = createTreeStore({
|
|
@@ -30,6 +42,7 @@
|
|
|
30
42
|
persistState: true,
|
|
31
43
|
defaultExpandAll: false
|
|
32
44
|
});
|
|
45
|
+
log('✅ Tree Store Created', { namespace: 'site-check' });
|
|
33
46
|
});
|
|
34
47
|
|
|
35
48
|
// Derive chart data from tree selection
|
|
@@ -37,22 +50,55 @@
|
|
|
37
50
|
if (!treeStore) return [];
|
|
38
51
|
const storeValue = $treeStore;
|
|
39
52
|
if (!storeValue) return [];
|
|
40
|
-
|
|
53
|
+
const filtered = filterChartData(rawData, storeValue.state.checkedPaths);
|
|
54
|
+
log('🔍 Filtered Data:', {
|
|
55
|
+
totalRaw: rawData.length,
|
|
56
|
+
checkedPaths: Array.from(storeValue.state.checkedPaths),
|
|
57
|
+
filteredCount: filtered.length,
|
|
58
|
+
cells: Array.from(new Set(filtered.map(r => r.cellName)))
|
|
59
|
+
});
|
|
60
|
+
return filtered;
|
|
41
61
|
});
|
|
42
62
|
|
|
43
63
|
// Transform data using base metrics from layout
|
|
44
|
-
let chartData = $derived(
|
|
64
|
+
let chartData = $derived.by(() => {
|
|
65
|
+
const transformed = transformChartData(filteredData, baseMetrics);
|
|
66
|
+
log('📊 Chart Data:', {
|
|
67
|
+
filteredRows: filteredData.length,
|
|
68
|
+
transformedRows: transformed.length,
|
|
69
|
+
baseMetrics,
|
|
70
|
+
sampleRow: transformed[0],
|
|
71
|
+
columns: transformed[0] ? Object.keys(transformed[0]) : []
|
|
72
|
+
});
|
|
73
|
+
return transformed;
|
|
74
|
+
});
|
|
45
75
|
|
|
46
76
|
// Expand layout based on selected cells
|
|
47
|
-
let chartLayout = $derived(
|
|
48
|
-
|
|
77
|
+
let chartLayout = $derived.by(() => {
|
|
78
|
+
const expanded = expandLayoutForCells(baseLayout, filteredData, cellStyling);
|
|
79
|
+
log('📐 Chart Layout:', {
|
|
80
|
+
sectionsCount: expanded.sections.length,
|
|
81
|
+
totalCharts: expanded.sections.reduce((sum, s) => sum + s.charts.length, 0),
|
|
82
|
+
firstSection: expanded.sections[0],
|
|
83
|
+
cellStylingEnabled: !!cellStyling
|
|
84
|
+
});
|
|
85
|
+
return expanded;
|
|
86
|
+
});
|
|
49
87
|
|
|
50
88
|
let totalRecords = $derived(rawData.length);
|
|
51
89
|
let visibleRecords = $derived(filteredData.length);
|
|
52
90
|
|
|
53
91
|
// Compute simple stats
|
|
54
|
-
let totalCells = $derived
|
|
55
|
-
|
|
92
|
+
let totalCells = $derived.by(() => {
|
|
93
|
+
const count = new Set(filteredData.map((r) => r.cellName)).size;
|
|
94
|
+
log('📱 Total Cells:', count);
|
|
95
|
+
return count;
|
|
96
|
+
});
|
|
97
|
+
let totalSites = $derived.by(() => {
|
|
98
|
+
const count = new Set(filteredData.map((r) => r.siteName)).size;
|
|
99
|
+
log('📡 Total Sites:', count);
|
|
100
|
+
return count;
|
|
101
|
+
});
|
|
56
102
|
</script>
|
|
57
103
|
|
|
58
104
|
<div class="container-fluid vh-100 d-flex flex-column">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Layout } from '../../core/Charts';
|
|
1
|
+
import { type Layout, type CellStylingConfig } from '../../core/Charts';
|
|
2
2
|
import { type CellTrafficRecord } from './index';
|
|
3
3
|
import type { Mode } from '../../index.js';
|
|
4
4
|
interface Props {
|
|
@@ -6,6 +6,7 @@ interface Props {
|
|
|
6
6
|
baseLayout: Layout;
|
|
7
7
|
baseMetrics: string[];
|
|
8
8
|
mode: Mode;
|
|
9
|
+
cellStyling?: CellStylingConfig;
|
|
9
10
|
}
|
|
10
11
|
declare const SiteCheck: import("svelte").Component<Props, {}, "">;
|
|
11
12
|
type SiteCheck = ReturnType<typeof SiteCheck>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default cell styling configuration for SiteCheck component
|
|
3
|
+
* Provides band colors and sector line styles for network cells
|
|
4
|
+
*/
|
|
5
|
+
export const defaultCellStyling = {
|
|
6
|
+
bandColors: {
|
|
7
|
+
"LTE700": "#DC2626",
|
|
8
|
+
"LTE800": "#EA580C",
|
|
9
|
+
"LTE900": "#D97706",
|
|
10
|
+
"LTE1800": "#2563EB",
|
|
11
|
+
"LTE2100": "#7C3AED",
|
|
12
|
+
"LTE2600": "#DB2777",
|
|
13
|
+
"NR700": "#B91C1C",
|
|
14
|
+
"NR2100": "#059669",
|
|
15
|
+
"NR3500": "#0891B2",
|
|
16
|
+
"NR26000": "#BE185D"
|
|
17
|
+
},
|
|
18
|
+
sectorLineStyles: {
|
|
19
|
+
"1": "solid",
|
|
20
|
+
"2": "dash",
|
|
21
|
+
"3": "dot",
|
|
22
|
+
"4": "dashdot"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bandColors": {
|
|
3
|
+
"LTE700": "#DC2626",
|
|
4
|
+
"LTE800": "#EA580C",
|
|
5
|
+
"LTE900": "#D97706",
|
|
6
|
+
"LTE1800": "#2563EB",
|
|
7
|
+
"LTE2100": "#7C3AED",
|
|
8
|
+
"LTE2600": "#DB2777",
|
|
9
|
+
"NR700": "#B91C1C",
|
|
10
|
+
"NR2100": "#059669",
|
|
11
|
+
"NR3500": "#0891B2",
|
|
12
|
+
"NR26000": "#BE185D"
|
|
13
|
+
},
|
|
14
|
+
"sectorLineStyles": {
|
|
15
|
+
"1": "solid",
|
|
16
|
+
"2": "dash",
|
|
17
|
+
"3": "dot",
|
|
18
|
+
"4": "dashdot"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Layout } from '../../core/Charts';
|
|
1
|
+
import type { Layout, CellStylingConfig } from '../../core/Charts';
|
|
2
2
|
import type { CellTrafficRecord } from './';
|
|
3
3
|
/**
|
|
4
4
|
* Expand base layout configuration with dynamic KPIs based on selected cells
|
|
@@ -6,9 +6,10 @@ import type { CellTrafficRecord } from './';
|
|
|
6
6
|
*
|
|
7
7
|
* @param baseLayout - The base layout configuration from JSON
|
|
8
8
|
* @param data - Filtered cell traffic records for selected cells
|
|
9
|
+
* @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
|
|
9
10
|
* @returns Expanded layout with cell-specific KPIs
|
|
10
11
|
*/
|
|
11
|
-
export declare function expandLayoutForCells(baseLayout: Layout, data: CellTrafficRecord[]): Layout;
|
|
12
|
+
export declare function expandLayoutForCells(baseLayout: Layout, data: CellTrafficRecord[], stylingConfig?: CellStylingConfig): Layout;
|
|
12
13
|
/**
|
|
13
14
|
* Extract base metric names from a layout configuration
|
|
14
15
|
* Returns unique metric rawNames that need to be pivoted
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { createStyledKPI } from './transforms.js';
|
|
1
2
|
/**
|
|
2
3
|
* Expand base layout configuration with dynamic KPIs based on selected cells
|
|
3
4
|
* Takes a base layout (with one KPI per metric) and expands it to include one KPI per cell
|
|
4
5
|
*
|
|
5
6
|
* @param baseLayout - The base layout configuration from JSON
|
|
6
7
|
* @param data - Filtered cell traffic records for selected cells
|
|
8
|
+
* @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
|
|
7
9
|
* @returns Expanded layout with cell-specific KPIs
|
|
8
10
|
*/
|
|
9
|
-
export function expandLayoutForCells(baseLayout, data) {
|
|
11
|
+
export function expandLayoutForCells(baseLayout, data, stylingConfig) {
|
|
10
12
|
// Get unique cells and their metadata
|
|
11
13
|
const cellMap = new Map();
|
|
12
14
|
data.forEach((record) => {
|
|
@@ -22,8 +24,8 @@ export function expandLayoutForCells(baseLayout, data) {
|
|
|
22
24
|
...section,
|
|
23
25
|
charts: section.charts.map((chart) => ({
|
|
24
26
|
...chart,
|
|
25
|
-
yLeft: expandKPIs(chart.yLeft, cells),
|
|
26
|
-
yRight: expandKPIs(chart.yRight, cells)
|
|
27
|
+
yLeft: expandKPIs(chart.yLeft, cells, stylingConfig),
|
|
28
|
+
yRight: expandKPIs(chart.yRight, cells, stylingConfig)
|
|
27
29
|
}))
|
|
28
30
|
}))
|
|
29
31
|
};
|
|
@@ -31,22 +33,20 @@ export function expandLayoutForCells(baseLayout, data) {
|
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
35
|
* Expand a single KPI into multiple KPIs (one per cell)
|
|
36
|
+
* Now uses createStyledKPI to apply band colors and sector line styles
|
|
34
37
|
*
|
|
35
38
|
* @param baseKPIs - Array of base KPIs from layout
|
|
36
39
|
* @param cells - Array of [cellName, record] tuples
|
|
37
|
-
* @
|
|
40
|
+
* @param stylingConfig - Optional cell styling configuration
|
|
41
|
+
* @returns Expanded array of KPIs with cell-specific styling
|
|
38
42
|
*/
|
|
39
|
-
function expandKPIs(baseKPIs, cells) {
|
|
43
|
+
function expandKPIs(baseKPIs, cells, stylingConfig) {
|
|
40
44
|
const expandedKPIs = [];
|
|
41
45
|
baseKPIs.forEach((baseKPI) => {
|
|
42
|
-
cells.forEach(([cellName, record]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
scale: baseKPI.scale,
|
|
47
|
-
unit: baseKPI.unit,
|
|
48
|
-
color: getColorForIndex(index)
|
|
49
|
-
});
|
|
46
|
+
cells.forEach(([cellName, record]) => {
|
|
47
|
+
// Use createStyledKPI to apply band color and sector line style
|
|
48
|
+
const styledKPI = createStyledKPI(baseKPI.rawName, record, baseKPI.unit, stylingConfig);
|
|
49
|
+
expandedKPIs.push(styledKPI);
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
return expandedKPIs;
|
|
@@ -4,5 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { default as SiteCheck } from './SiteCheck.svelte';
|
|
6
6
|
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell, type CellTrafficRecord } from './data-loader.js';
|
|
7
|
-
export { buildTreeNodes, filterChartData, transformChartData } from './transforms.js';
|
|
7
|
+
export { buildTreeNodes, filterChartData, transformChartData, createStyledKPI } from './transforms.js';
|
|
8
8
|
export { expandLayoutForCells, extractBaseMetrics } from './helper.js';
|
|
9
|
+
export { defaultCellStyling } from './default-cell-styling.js';
|
|
@@ -7,6 +7,8 @@ export { default as SiteCheck } from './SiteCheck.svelte';
|
|
|
7
7
|
// Data loading
|
|
8
8
|
export { loadCellTrafficData, getUniqueSites, getUniqueCells, groupDataByCell } from './data-loader.js';
|
|
9
9
|
// Data transforms
|
|
10
|
-
export { buildTreeNodes, filterChartData, transformChartData } from './transforms.js';
|
|
11
|
-
// Helper
|
|
10
|
+
export { buildTreeNodes, filterChartData, transformChartData, createStyledKPI } from './transforms.js';
|
|
11
|
+
// Helper functions
|
|
12
12
|
export { expandLayoutForCells, extractBaseMetrics } from './helper.js';
|
|
13
|
+
// Default cell styling configuration
|
|
14
|
+
export { defaultCellStyling } from './default-cell-styling.js';
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Converts raw CSV data to TreeView nodes and Chart configurations
|
|
4
4
|
*/
|
|
5
5
|
import type { TreeNode } from '../../core/TreeView';
|
|
6
|
+
import type { KPI, CellStylingConfig } from '../../core/Charts';
|
|
6
7
|
import type { CellTrafficRecord } from './data-loader';
|
|
7
8
|
/**
|
|
8
9
|
* Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
|
|
@@ -22,3 +23,15 @@ export declare function filterChartData(data: CellTrafficRecord[], checkedPaths:
|
|
|
22
23
|
* @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
|
|
23
24
|
*/
|
|
24
25
|
export declare function transformChartData(data: CellTrafficRecord[], baseMetrics: string[]): any[];
|
|
26
|
+
/**
|
|
27
|
+
* Apply cell styling based on band and sector
|
|
28
|
+
* Modifies KPI objects to include color (from band) and lineStyle (from sector)
|
|
29
|
+
* Updates KPI name to format: Band_Azimuth°
|
|
30
|
+
*
|
|
31
|
+
* @param metricName - Base metric name (e.g., 'dlGBytes')
|
|
32
|
+
* @param cellRecord - Cell traffic record with band, sector, azimuth metadata
|
|
33
|
+
* @param unit - Unit string for the metric
|
|
34
|
+
* @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
|
|
35
|
+
* @returns Styled KPI object
|
|
36
|
+
*/
|
|
37
|
+
export declare function createStyledKPI(metricName: string, cellRecord: CellTrafficRecord, unit: string, stylingConfig?: CellStylingConfig): KPI;
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Data Transforms for Site Check Component
|
|
3
3
|
* Converts raw CSV data to TreeView nodes and Chart configurations
|
|
4
4
|
*/
|
|
5
|
+
import { log } from '../../core/logger';
|
|
5
6
|
/**
|
|
6
7
|
* Build hierarchical tree structure: Site → Sector (Azimuth) → Cell (Band)
|
|
7
8
|
*/
|
|
8
9
|
export function buildTreeNodes(data) {
|
|
10
|
+
log('🔄 Building tree nodes', { recordCount: data.length });
|
|
9
11
|
// Group by site → azimuth → cell
|
|
10
12
|
const siteMap = new Map();
|
|
11
13
|
data.forEach((record) => {
|
|
@@ -33,6 +35,7 @@ export function buildTreeNodes(data) {
|
|
|
33
35
|
// icon: '📡',
|
|
34
36
|
metadata: { type: 'site', siteName },
|
|
35
37
|
defaultExpanded: false,
|
|
38
|
+
defaultChecked: false, // Don't check parent nodes
|
|
36
39
|
children: []
|
|
37
40
|
};
|
|
38
41
|
Array.from(azimuthMap.entries())
|
|
@@ -44,6 +47,7 @@ export function buildTreeNodes(data) {
|
|
|
44
47
|
// icon: '📍',
|
|
45
48
|
metadata: { type: 'sector', azimuth, siteName },
|
|
46
49
|
defaultExpanded: false,
|
|
50
|
+
defaultChecked: false, // Don't check parent nodes
|
|
47
51
|
children: []
|
|
48
52
|
};
|
|
49
53
|
Array.from(cellMap.entries())
|
|
@@ -69,6 +73,11 @@ export function buildTreeNodes(data) {
|
|
|
69
73
|
});
|
|
70
74
|
treeNodes.push(siteNode);
|
|
71
75
|
});
|
|
76
|
+
log('✅ Tree nodes built', {
|
|
77
|
+
totalNodes: treeNodes.length,
|
|
78
|
+
totalSites: siteMap.size,
|
|
79
|
+
sampleSite: treeNodes[0]?.label
|
|
80
|
+
});
|
|
72
81
|
return treeNodes;
|
|
73
82
|
}
|
|
74
83
|
/**
|
|
@@ -87,6 +96,11 @@ function getBandIcon(band) {
|
|
|
87
96
|
* Only include cells that are checked in the tree
|
|
88
97
|
*/
|
|
89
98
|
export function filterChartData(data, checkedPaths) {
|
|
99
|
+
log('🔄 Filtering chart data', {
|
|
100
|
+
totalRecords: data.length,
|
|
101
|
+
checkedPathsCount: checkedPaths.size,
|
|
102
|
+
paths: Array.from(checkedPaths)
|
|
103
|
+
});
|
|
90
104
|
// Extract cell names from checked leaf paths (format: "site:azimuth:cellName")
|
|
91
105
|
const selectedCells = new Set();
|
|
92
106
|
checkedPaths.forEach((path) => {
|
|
@@ -97,7 +111,13 @@ export function filterChartData(data, checkedPaths) {
|
|
|
97
111
|
}
|
|
98
112
|
});
|
|
99
113
|
// Filter data to only include selected cells
|
|
100
|
-
|
|
114
|
+
const filtered = data.filter((record) => selectedCells.has(record.cellName));
|
|
115
|
+
log('✅ Data filtered', {
|
|
116
|
+
selectedCells: Array.from(selectedCells),
|
|
117
|
+
filteredRecords: filtered.length,
|
|
118
|
+
uniqueCells: new Set(filtered.map(r => r.cellName)).size
|
|
119
|
+
});
|
|
120
|
+
return filtered;
|
|
101
121
|
}
|
|
102
122
|
/**
|
|
103
123
|
* Transform data for chart component consumption
|
|
@@ -108,6 +128,11 @@ export function filterChartData(data, checkedPaths) {
|
|
|
108
128
|
* @param baseMetrics - Array of metric names to pivot (e.g., ['dlGBytes', 'ulGBytes'])
|
|
109
129
|
*/
|
|
110
130
|
export function transformChartData(data, baseMetrics) {
|
|
131
|
+
log('🔄 Transforming chart data', {
|
|
132
|
+
inputRecords: data.length,
|
|
133
|
+
baseMetrics,
|
|
134
|
+
uniqueCells: new Set(data.map(r => r.cellName)).size
|
|
135
|
+
});
|
|
111
136
|
// Group data by date
|
|
112
137
|
const dateMap = new Map();
|
|
113
138
|
data.forEach((record) => {
|
|
@@ -138,5 +163,53 @@ export function transformChartData(data, baseMetrics) {
|
|
|
138
163
|
});
|
|
139
164
|
// Sort by date
|
|
140
165
|
pivotedData.sort((a, b) => a.TIMESTAMP.localeCompare(b.TIMESTAMP));
|
|
166
|
+
log('✅ Data transformed', {
|
|
167
|
+
outputRows: pivotedData.length,
|
|
168
|
+
dateRange: pivotedData.length > 0 ?
|
|
169
|
+
`${pivotedData[0].TIMESTAMP} to ${pivotedData[pivotedData.length - 1].TIMESTAMP}` :
|
|
170
|
+
'none',
|
|
171
|
+
columnsPerRow: pivotedData[0] ? Object.keys(pivotedData[0]).length : 0,
|
|
172
|
+
sampleRow: pivotedData[0]
|
|
173
|
+
});
|
|
141
174
|
return pivotedData;
|
|
142
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Apply cell styling based on band and sector
|
|
178
|
+
* Modifies KPI objects to include color (from band) and lineStyle (from sector)
|
|
179
|
+
* Updates KPI name to format: Band_Azimuth°
|
|
180
|
+
*
|
|
181
|
+
* @param metricName - Base metric name (e.g., 'dlGBytes')
|
|
182
|
+
* @param cellRecord - Cell traffic record with band, sector, azimuth metadata
|
|
183
|
+
* @param unit - Unit string for the metric
|
|
184
|
+
* @param stylingConfig - Optional cell styling configuration (band colors, sector line styles)
|
|
185
|
+
* @returns Styled KPI object
|
|
186
|
+
*/
|
|
187
|
+
export function createStyledKPI(metricName, cellRecord, unit, stylingConfig) {
|
|
188
|
+
const { band, sector, azimuth, cellName } = cellRecord;
|
|
189
|
+
// Get color from band (if config provided)
|
|
190
|
+
const color = stylingConfig?.bandColors?.[band];
|
|
191
|
+
// Get line style from sector (if config provided)
|
|
192
|
+
const lineStyle = stylingConfig?.sectorLineStyles?.[sector.toString()];
|
|
193
|
+
// Format name as: Band_Azimuth°
|
|
194
|
+
const displayName = `${band}_${azimuth}°`;
|
|
195
|
+
// Build KPI with cell-specific styling
|
|
196
|
+
const kpi = {
|
|
197
|
+
rawName: `${metricName}_${cellName}`, // Column name in pivoted data
|
|
198
|
+
name: displayName,
|
|
199
|
+
scale: 'absolute',
|
|
200
|
+
unit,
|
|
201
|
+
...(color && { color }),
|
|
202
|
+
...(lineStyle && { lineStyle })
|
|
203
|
+
};
|
|
204
|
+
log('🎨 Styled KPI created', {
|
|
205
|
+
metricName,
|
|
206
|
+
cellName,
|
|
207
|
+
displayName,
|
|
208
|
+
band,
|
|
209
|
+
sector,
|
|
210
|
+
azimuth,
|
|
211
|
+
color,
|
|
212
|
+
lineStyle
|
|
213
|
+
});
|
|
214
|
+
return kpi;
|
|
215
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
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 } from './charts.model.js';
|
|
6
|
+
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig, HoverMode } 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';
|
|
10
|
+
import { log } from '../logger';
|
|
10
11
|
|
|
11
12
|
const dispatch = createEventDispatcher<{
|
|
12
13
|
chartcontextmenu: {
|
|
@@ -26,13 +27,15 @@
|
|
|
26
27
|
sectionId?: string;
|
|
27
28
|
sectionMovingAverage?: MovingAverageConfig; // Section-level MA config
|
|
28
29
|
layoutMovingAverage?: MovingAverageConfig; // Layout-level MA config
|
|
30
|
+
layoutHoverMode?: HoverMode; // Layout-level hover mode config
|
|
31
|
+
layoutColoredHover?: boolean; // Layout-level colored hover config (default: true)
|
|
29
32
|
runtimeMAOverride?: MovingAverageConfig | null; // Runtime override from global controls
|
|
30
33
|
runtimeShowOriginal?: boolean; // Runtime control for showing original lines
|
|
31
34
|
runtimeShowMarkers?: boolean; // Runtime control for showing markers (default: true)
|
|
32
35
|
runtimeShowLegend?: boolean; // Runtime control for showing legend (default: true)
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
let { chart, processedData, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true }: Props = $props();
|
|
38
|
+
let { chart, processedData, markers, plotlyLayout, enableAdaptation = true, sectionId, sectionMovingAverage, layoutMovingAverage, layoutHoverMode, layoutColoredHover = true, runtimeMAOverride, runtimeShowOriginal, runtimeShowMarkers = true, runtimeShowLegend = true }: Props = $props();
|
|
36
39
|
|
|
37
40
|
// Chart container div and state
|
|
38
41
|
let chartDiv: HTMLElement;
|
|
@@ -161,7 +164,7 @@
|
|
|
161
164
|
// Add left Y-axis traces (with moving average support)
|
|
162
165
|
resolvedKPIs.left.forEach(kpi => {
|
|
163
166
|
const values = getKPIValues(processedData, kpi);
|
|
164
|
-
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y1', colorIndex, chartType, stackGroup);
|
|
167
|
+
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y1', colorIndex, chartType, stackGroup, layoutColoredHover);
|
|
165
168
|
traces.push(...kpiTraces);
|
|
166
169
|
colorIndex++;
|
|
167
170
|
});
|
|
@@ -169,13 +172,13 @@
|
|
|
169
172
|
// Add right Y-axis traces (with moving average support)
|
|
170
173
|
resolvedKPIs.right.forEach(kpi => {
|
|
171
174
|
const values = getKPIValues(processedData, kpi);
|
|
172
|
-
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y2', colorIndex, chartType, stackGroup);
|
|
175
|
+
const kpiTraces = createTimeSeriesTraceWithMA(values, timestamps, kpi, 'y2', colorIndex, chartType, stackGroup, layoutColoredHover);
|
|
173
176
|
traces.push(...kpiTraces);
|
|
174
177
|
colorIndex++;
|
|
175
178
|
});
|
|
176
179
|
|
|
177
180
|
// Create default modern layout using the centralized function
|
|
178
|
-
const defaultLayout: any = createDefaultPlotlyLayout(chart.title);
|
|
181
|
+
const defaultLayout: any = createDefaultPlotlyLayout(chart.title, layoutHoverMode);
|
|
179
182
|
|
|
180
183
|
// Override specific properties for this chart
|
|
181
184
|
defaultLayout.yaxis.title = {
|
|
@@ -235,9 +238,16 @@
|
|
|
235
238
|
// Use Plotly.react() for updates (preserves zoom/pan) or newPlot for initial render
|
|
236
239
|
if (chartInitialized) {
|
|
237
240
|
// Update existing chart - much faster, preserves user interactions
|
|
241
|
+
log('🔄 Updating chart with Plotly.react', { chartTitle: chart.title });
|
|
238
242
|
Plotly.react(chartDiv, traces, finalLayout, config);
|
|
239
243
|
} else {
|
|
240
244
|
// Initial chart creation
|
|
245
|
+
log('📊 Creating new chart with Plotly.newPlot', {
|
|
246
|
+
chartTitle: chart.title,
|
|
247
|
+
traces: traces.length,
|
|
248
|
+
leftKPIs: chart.yLeft.length,
|
|
249
|
+
rightKPIs: chart.yRight.length
|
|
250
|
+
});
|
|
241
251
|
Plotly.newPlot(chartDiv, traces, finalLayout, config);
|
|
242
252
|
chartInitialized = true;
|
|
243
253
|
}
|
|
@@ -251,11 +261,23 @@
|
|
|
251
261
|
}
|
|
252
262
|
|
|
253
263
|
onMount(() => {
|
|
264
|
+
log('📈 ChartCard mounted', {
|
|
265
|
+
chartTitle: chart.title,
|
|
266
|
+
leftKPIs: chart.yLeft.length,
|
|
267
|
+
rightKPIs: chart.yRight.length
|
|
268
|
+
});
|
|
269
|
+
|
|
254
270
|
// Initial container size measurement
|
|
255
271
|
if (chartDiv) {
|
|
256
272
|
const rect = chartDiv.getBoundingClientRect();
|
|
257
273
|
containerSize.width = rect.width;
|
|
258
274
|
containerSize.height = rect.height;
|
|
275
|
+
|
|
276
|
+
log('📐 Initial container size', {
|
|
277
|
+
chartTitle: chart.title,
|
|
278
|
+
width: rect.width,
|
|
279
|
+
height: rect.height
|
|
280
|
+
});
|
|
259
281
|
}
|
|
260
282
|
|
|
261
283
|
renderChart();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig } from './charts.model.js';
|
|
1
|
+
import type { Chart as ChartModel, ChartMarker, MovingAverageConfig, HoverMode } from './charts.model.js';
|
|
2
2
|
import { type ProcessedChartData } from './data-processor.js';
|
|
3
3
|
interface Props {
|
|
4
4
|
chart: ChartModel;
|
|
@@ -9,6 +9,8 @@ interface Props {
|
|
|
9
9
|
sectionId?: string;
|
|
10
10
|
sectionMovingAverage?: MovingAverageConfig;
|
|
11
11
|
layoutMovingAverage?: MovingAverageConfig;
|
|
12
|
+
layoutHoverMode?: HoverMode;
|
|
13
|
+
layoutColoredHover?: boolean;
|
|
12
14
|
runtimeMAOverride?: MovingAverageConfig | null;
|
|
13
15
|
runtimeShowOriginal?: boolean;
|
|
14
16
|
runtimeShowMarkers?: boolean;
|
|
@@ -6,6 +6,7 @@
|
|
|
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';
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
layout: Layout;
|
|
@@ -55,6 +56,19 @@
|
|
|
55
56
|
|
|
56
57
|
let { layout, data, mode, markers, plotlyLayout, enableAdaptation = true, showGlobalControls = true }: Props = $props();
|
|
57
58
|
|
|
59
|
+
// Log component initialization
|
|
60
|
+
$effect(() => {
|
|
61
|
+
log('📊 ChartComponent initialized', {
|
|
62
|
+
mode,
|
|
63
|
+
dataRows: data.length,
|
|
64
|
+
sections: layout.sections.length,
|
|
65
|
+
totalCharts: layout.sections.reduce((sum, s) => sum + s.charts.length, 0),
|
|
66
|
+
enableAdaptation,
|
|
67
|
+
showGlobalControls,
|
|
68
|
+
layoutHoverMode: layout.hoverMode
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
58
72
|
// Preprocess raw data once - automatically memoized by Svelte's $derived
|
|
59
73
|
// This extracts all KPI values and timestamps, cached until data or layout changes
|
|
60
74
|
let processedData = $derived(getPreprocessedData(data, layout));
|
|
@@ -76,6 +90,12 @@
|
|
|
76
90
|
|
|
77
91
|
// Handler for global controls updates
|
|
78
92
|
function handleControlsUpdate(updatedControls: GlobalChartControls) {
|
|
93
|
+
log('🎛️ Global controls updated', {
|
|
94
|
+
movingAverageEnabled: updatedControls.movingAverage?.enabled,
|
|
95
|
+
windowOverride: updatedControls.movingAverage?.windowOverride,
|
|
96
|
+
markersEnabled: updatedControls.markers?.enabled,
|
|
97
|
+
legendEnabled: updatedControls.legend?.enabled
|
|
98
|
+
});
|
|
79
99
|
globalControls = updatedControls;
|
|
80
100
|
}
|
|
81
101
|
|
|
@@ -166,12 +186,17 @@
|
|
|
166
186
|
|
|
167
187
|
function zoomSelectedChart() {
|
|
168
188
|
if (contextMenu.chart && contextMenu.section) {
|
|
189
|
+
log('🔍 Zooming chart', {
|
|
190
|
+
chartTitle: contextMenu.chart.title,
|
|
191
|
+
sectionId: contextMenu.section.id
|
|
192
|
+
});
|
|
169
193
|
zoomedChart = { chart: contextMenu.chart, section: contextMenu.section };
|
|
170
194
|
}
|
|
171
195
|
closeContextMenu();
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
function exitZoom() {
|
|
199
|
+
log('🔍 Exiting zoom mode');
|
|
175
200
|
zoomedChart = null;
|
|
176
201
|
closeContextMenu();
|
|
177
202
|
}
|
|
@@ -303,6 +328,7 @@
|
|
|
303
328
|
sectionId={section.id}
|
|
304
329
|
sectionMovingAverage={section.movingAverage}
|
|
305
330
|
layoutMovingAverage={layout.movingAverage}
|
|
331
|
+
layoutHoverMode={layout.hoverMode}
|
|
306
332
|
runtimeMAOverride={effectiveMAOverride}
|
|
307
333
|
runtimeShowOriginal={globalControls.movingAverage?.showOriginal}
|
|
308
334
|
runtimeShowMarkers={globalControls.markers?.enabled}
|
|
@@ -4,46 +4,36 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Adapts hover behavior based on container size and series count
|
|
7
|
-
*
|
|
7
|
+
* Preserves user's configured hover mode unless adaptation is critical for performance/UX
|
|
8
8
|
*/
|
|
9
|
-
function adaptHoverBehavior(layout, containerSize, chartInfo) {
|
|
9
|
+
function adaptHoverBehavior(layout, containerSize, chartInfo, originalHoverMode) {
|
|
10
10
|
const { width, height } = containerSize;
|
|
11
11
|
const isTiny = width < 250 || height < 200;
|
|
12
12
|
const isSmall = width < 400 || height < 300;
|
|
13
13
|
const isMedium = width < 600 || height < 400;
|
|
14
14
|
const totalSeries = chartInfo.leftSeriesCount + chartInfo.rightSeriesCount;
|
|
15
|
-
//
|
|
16
|
-
if (isTiny) {
|
|
17
|
-
layout.hovermode = 'closest'; // Single point instead of unified
|
|
18
|
-
if (layout.hoverlabel) {
|
|
19
|
-
layout.hoverlabel.font = layout.hoverlabel.font || {};
|
|
20
|
-
layout.hoverlabel.font.size = 9; // Smaller font
|
|
21
|
-
}
|
|
22
|
-
return layout;
|
|
23
|
-
}
|
|
24
|
-
// Priority 2: Simplify hover in small charts
|
|
15
|
+
// Only override hover mode in critical cases for performance/UX
|
|
25
16
|
if (isSmall) {
|
|
26
|
-
|
|
17
|
+
// Force 'closest' in small charts for performance and readability
|
|
18
|
+
layout.hovermode = 'closest';
|
|
27
19
|
if (layout.hoverlabel) {
|
|
28
20
|
layout.hoverlabel.font = layout.hoverlabel.font || {};
|
|
29
|
-
layout.hoverlabel.font.size = 9;
|
|
21
|
+
layout.hoverlabel.font.size = 9;
|
|
30
22
|
}
|
|
31
23
|
return layout;
|
|
32
24
|
}
|
|
33
|
-
//
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
layout.hovermode = 'x';
|
|
37
|
-
}
|
|
38
|
-
else if (totalSeries > 8) {
|
|
39
|
-
// Very many series - even in large charts, use x
|
|
40
|
-
layout.hovermode = 'x';
|
|
25
|
+
// For all other sizes, preserve the user's configured hover mode
|
|
26
|
+
if (originalHoverMode !== undefined) {
|
|
27
|
+
layout.hovermode = originalHoverMode;
|
|
41
28
|
}
|
|
42
|
-
//
|
|
43
|
-
//
|
|
29
|
+
// If no original hover mode provided, keep whatever is in the layout
|
|
30
|
+
// Only adapt font sizes, not hover behavior for non-tiny charts
|
|
44
31
|
if (layout.hoverlabel) {
|
|
45
32
|
layout.hoverlabel.font = layout.hoverlabel.font || {};
|
|
46
|
-
if (
|
|
33
|
+
if (isSmall) {
|
|
34
|
+
layout.hoverlabel.font.size = 9;
|
|
35
|
+
}
|
|
36
|
+
else if (isMedium) {
|
|
47
37
|
layout.hoverlabel.font.size = 10;
|
|
48
38
|
}
|
|
49
39
|
else {
|
|
@@ -62,6 +52,8 @@ export function adaptPlotlyLayout(baseLayout, containerSize, chartInfo, config =
|
|
|
62
52
|
return baseLayout;
|
|
63
53
|
const { width, height } = containerSize;
|
|
64
54
|
const adaptedLayout = { ...baseLayout };
|
|
55
|
+
// Preserve the original hover mode before any adaptations
|
|
56
|
+
const originalHoverMode = baseLayout.hovermode;
|
|
65
57
|
// Size categories for adaptation rules
|
|
66
58
|
const isTiny = width < 250 || height < 200;
|
|
67
59
|
const isSmall = width < 400 || height < 300;
|
|
@@ -146,8 +138,8 @@ export function adaptPlotlyLayout(baseLayout, containerSize, chartInfo, config =
|
|
|
146
138
|
adaptedLayout.legend.font.size = 11;
|
|
147
139
|
}
|
|
148
140
|
}
|
|
149
|
-
// Apply adaptive hover behavior (
|
|
150
|
-
adaptHoverBehavior(adaptedLayout, containerSize, chartInfo);
|
|
141
|
+
// Apply adaptive hover behavior (preserve user config except in tiny charts)
|
|
142
|
+
adaptHoverBehavior(adaptedLayout, containerSize, chartInfo, originalHoverMode);
|
|
151
143
|
return adaptedLayout;
|
|
152
144
|
}
|
|
153
145
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type Scale = "percent" | "absolute";
|
|
2
|
+
export type LineStyle = 'solid' | 'dash' | 'dot' | 'dashdot' | 'longdash' | 'longdashdot';
|
|
2
3
|
export interface MovingAverageConfig {
|
|
3
4
|
enabled: boolean;
|
|
4
5
|
window: number;
|
|
@@ -11,6 +12,7 @@ export interface KPI {
|
|
|
11
12
|
scale: Scale;
|
|
12
13
|
unit: string;
|
|
13
14
|
color?: string;
|
|
15
|
+
lineStyle?: LineStyle;
|
|
14
16
|
movingAverage?: MovingAverageConfig;
|
|
15
17
|
}
|
|
16
18
|
export type ChartPosition = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
@@ -33,10 +35,13 @@ export interface Section {
|
|
|
33
35
|
movingAverage?: MovingAverageConfig;
|
|
34
36
|
}
|
|
35
37
|
export type Mode = "tabs" | "scrollspy";
|
|
38
|
+
export type HoverMode = 'x' | 'y' | 'closest' | 'x unified' | 'y unified' | false;
|
|
36
39
|
export interface Layout {
|
|
37
40
|
layoutName: string;
|
|
38
41
|
sections: Section[];
|
|
39
42
|
movingAverage?: MovingAverageConfig;
|
|
43
|
+
hoverMode?: HoverMode;
|
|
44
|
+
coloredHover?: boolean;
|
|
40
45
|
}
|
|
41
46
|
export interface ChartMarker {
|
|
42
47
|
date: string | Date;
|
|
@@ -59,3 +64,7 @@ export interface GlobalChartControls {
|
|
|
59
64
|
enabled: boolean;
|
|
60
65
|
};
|
|
61
66
|
}
|
|
67
|
+
export interface CellStylingConfig {
|
|
68
|
+
bandColors: Record<string, string>;
|
|
69
|
+
sectorLineStyles: Record<string, LineStyle>;
|
|
70
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '../logger';
|
|
1
2
|
/**
|
|
2
3
|
* Extract all unique KPI rawNames from a layout configuration
|
|
3
4
|
* This determines which columns we need to extract from raw data
|
|
@@ -16,6 +17,10 @@ export function extractKPINames(layout) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
}
|
|
20
|
+
log('📋 KPI names extracted', {
|
|
21
|
+
totalKPIs: kpiNames.size,
|
|
22
|
+
kpiNames: Array.from(kpiNames)
|
|
23
|
+
});
|
|
19
24
|
return kpiNames;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
@@ -28,6 +33,11 @@ export function extractKPINames(layout) {
|
|
|
28
33
|
* @returns Preprocessed data ready for chart rendering
|
|
29
34
|
*/
|
|
30
35
|
export function preprocessChartData(data, layout, timestampField = 'TIMESTAMP') {
|
|
36
|
+
log('🔄 Preprocessing chart data', {
|
|
37
|
+
rawDataRows: data.length,
|
|
38
|
+
sections: layout.sections.length,
|
|
39
|
+
timestampField
|
|
40
|
+
});
|
|
31
41
|
// Extract all unique KPI names we need to process
|
|
32
42
|
const kpiNames = extractKPINames(layout);
|
|
33
43
|
// Initialize the result map
|
|
@@ -42,9 +52,18 @@ export function preprocessChartData(data, layout, timestampField = 'TIMESTAMP')
|
|
|
42
52
|
})
|
|
43
53
|
.filter(val => !isNaN(val)); // Remove invalid values
|
|
44
54
|
kpiValues.set(kpiName, values);
|
|
55
|
+
if (values.length === 0) {
|
|
56
|
+
log('⚠️ No valid values found for KPI', { kpiName });
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
// Extract timestamps once
|
|
47
60
|
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
|
+
});
|
|
48
67
|
return {
|
|
49
68
|
kpiValues,
|
|
50
69
|
timestamps,
|
|
@@ -72,9 +91,11 @@ export function getPreprocessedData(data, layout, timestampField = 'TIMESTAMP')
|
|
|
72
91
|
if (cached) {
|
|
73
92
|
// Verify cache is still valid (data reference matches)
|
|
74
93
|
if (cached._rawDataRef === data) {
|
|
94
|
+
log('💾 Using cached preprocessed data');
|
|
75
95
|
return cached;
|
|
76
96
|
}
|
|
77
97
|
}
|
|
98
|
+
log('🔄 Cache miss - preprocessing data');
|
|
78
99
|
// Cache miss or invalid - compute and cache
|
|
79
100
|
const processed = preprocessChartData(data, layout, timestampField);
|
|
80
101
|
preprocessCache.set(data, processed);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { KPI } from './charts.model.js';
|
|
1
|
+
import type { KPI, HoverMode } 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, chartType?: string, stackGroup?: string): any;
|
|
4
|
+
export declare function createTimeSeriesTrace(values: number[], timestamps: any[], kpi: KPI, yaxis?: 'y1' | 'y2', colorIndex?: number, chartType?: string, stackGroup?: string, coloredHover?: boolean): 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
|
|
@@ -13,7 +13,7 @@ export declare function createTimeSeriesTrace(values: number[], timestamps: any[
|
|
|
13
13
|
* @param stackGroup - Optional stack group identifier
|
|
14
14
|
* @returns Array of traces (original + MA if configured)
|
|
15
15
|
*/
|
|
16
|
-
export declare function createTimeSeriesTraceWithMA(values: number[], timestamps: any[], kpi: KPI, yaxis?: 'y1' | 'y2', colorIndex?: number, chartType?: string, stackGroup?: string): any[];
|
|
16
|
+
export declare function createTimeSeriesTraceWithMA(values: number[], timestamps: any[], kpi: KPI, yaxis?: 'y1' | 'y2', colorIndex?: number, chartType?: string, stackGroup?: string, coloredHover?: boolean): any[];
|
|
17
17
|
export declare function getYAxisTitle(kpis: KPI[]): string;
|
|
18
18
|
export declare function formatValue(value: number, scale: 'percent' | 'absolute', unit: string): string;
|
|
19
|
-
export declare function createDefaultPlotlyLayout(title?: string): any;
|
|
19
|
+
export declare function createDefaultPlotlyLayout(title?: string, hoverMode?: HoverMode, coloredHover?: boolean): any;
|
|
@@ -72,7 +72,7 @@ export function calculateMovingAverage(values, window) {
|
|
|
72
72
|
maCache.set(cacheKey, result);
|
|
73
73
|
return result;
|
|
74
74
|
}
|
|
75
|
-
export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup) {
|
|
75
|
+
export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup, coloredHover = true) {
|
|
76
76
|
// Use KPI color if provided, otherwise cycle through modern colors
|
|
77
77
|
const traceColor = kpi.color || modernColors[colorIndex % modernColors.length];
|
|
78
78
|
// Base trace configuration
|
|
@@ -85,6 +85,18 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
85
85
|
`Value: %{y:,.2f} ${kpi.unit}<br>` +
|
|
86
86
|
'<extra></extra>'
|
|
87
87
|
};
|
|
88
|
+
// Add colored hover styling if enabled
|
|
89
|
+
if (coloredHover) {
|
|
90
|
+
baseTrace.hoverlabel = {
|
|
91
|
+
bgcolor: traceColor,
|
|
92
|
+
bordercolor: traceColor,
|
|
93
|
+
font: {
|
|
94
|
+
color: '#ffffff', // White text for better contrast
|
|
95
|
+
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
96
|
+
size: 11
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
88
100
|
// Configure based on chart type
|
|
89
101
|
switch (chartType) {
|
|
90
102
|
case 'stacked-area':
|
|
@@ -138,7 +150,7 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
138
150
|
width: 3,
|
|
139
151
|
shape: 'spline',
|
|
140
152
|
smoothing: 0.3,
|
|
141
|
-
dash: yaxis === 'y1' ? 'solid' : 'dot'
|
|
153
|
+
dash: kpi.lineStyle || (yaxis === 'y1' ? 'solid' : 'dot')
|
|
142
154
|
}
|
|
143
155
|
};
|
|
144
156
|
}
|
|
@@ -154,12 +166,12 @@ export function createTimeSeriesTrace(values, timestamps, kpi, yaxis = 'y1', col
|
|
|
154
166
|
* @param stackGroup - Optional stack group identifier
|
|
155
167
|
* @returns Array of traces (original + MA if configured)
|
|
156
168
|
*/
|
|
157
|
-
export function createTimeSeriesTraceWithMA(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup) {
|
|
169
|
+
export function createTimeSeriesTraceWithMA(values, timestamps, kpi, yaxis = 'y1', colorIndex = 0, chartType = 'line', stackGroup, coloredHover = true) {
|
|
158
170
|
const traces = [];
|
|
159
171
|
const traceColor = kpi.color || modernColors[colorIndex % modernColors.length];
|
|
160
172
|
// Add original trace (unless explicitly disabled)
|
|
161
173
|
if (!kpi.movingAverage || kpi.movingAverage.showOriginal !== false) {
|
|
162
|
-
const originalTrace = createTimeSeriesTrace(values, timestamps, kpi, yaxis, colorIndex, chartType, stackGroup);
|
|
174
|
+
const originalTrace = createTimeSeriesTrace(values, timestamps, kpi, yaxis, colorIndex, chartType, stackGroup, coloredHover);
|
|
163
175
|
// If MA is enabled, make the original line slightly transparent
|
|
164
176
|
if (kpi.movingAverage?.enabled) {
|
|
165
177
|
originalTrace.opacity = 0.4;
|
|
@@ -190,7 +202,18 @@ export function createTimeSeriesTraceWithMA(values, timestamps, kpi, yaxis = 'y1
|
|
|
190
202
|
},
|
|
191
203
|
hovertemplate: `<b>${maLabel}</b><br>` +
|
|
192
204
|
`Value: %{y:,.2f} ${kpi.unit}<br>` +
|
|
193
|
-
'<extra></extra>'
|
|
205
|
+
'<extra></extra>',
|
|
206
|
+
...(coloredHover && {
|
|
207
|
+
hoverlabel: {
|
|
208
|
+
bgcolor: traceColor,
|
|
209
|
+
bordercolor: traceColor,
|
|
210
|
+
font: {
|
|
211
|
+
color: '#ffffff',
|
|
212
|
+
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
213
|
+
size: 11
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
})
|
|
194
217
|
};
|
|
195
218
|
traces.push(maTrace);
|
|
196
219
|
}
|
|
@@ -209,7 +232,7 @@ export function formatValue(value, scale, unit) {
|
|
|
209
232
|
}
|
|
210
233
|
return `${value.toLocaleString()}${unit}`;
|
|
211
234
|
}
|
|
212
|
-
export function createDefaultPlotlyLayout(title) {
|
|
235
|
+
export function createDefaultPlotlyLayout(title, hoverMode, coloredHover = true) {
|
|
213
236
|
return {
|
|
214
237
|
title: title ? {
|
|
215
238
|
text: title,
|
|
@@ -254,8 +277,17 @@ export function createDefaultPlotlyLayout(title) {
|
|
|
254
277
|
paper_bgcolor: 'rgba(0,0,0,0)',
|
|
255
278
|
plot_bgcolor: 'rgba(0,0,0,0)',
|
|
256
279
|
font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif' },
|
|
257
|
-
hovermode: 'x',
|
|
258
|
-
hoverlabel: {
|
|
280
|
+
hovermode: hoverMode !== undefined ? hoverMode : 'x',
|
|
281
|
+
hoverlabel: coloredHover ? {
|
|
282
|
+
// When coloredHover is enabled, let each trace control its own hover colors
|
|
283
|
+
font: {
|
|
284
|
+
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
285
|
+
size: 11
|
|
286
|
+
// color will be set per trace when coloredHover is true
|
|
287
|
+
}
|
|
288
|
+
// bgcolor and bordercolor will be set per trace when coloredHover is true
|
|
289
|
+
} : {
|
|
290
|
+
// Default hover styling when coloredHover is disabled
|
|
259
291
|
font: {
|
|
260
292
|
family: 'Inter, Segoe UI, Tahoma, Geneva, Verdana, sans-serif',
|
|
261
293
|
size: 11,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { default as ChartComponent } from './ChartComponent.svelte';
|
|
2
2
|
export { default as ChartCard } from './ChartCard.svelte';
|
|
3
|
-
export type { Layout, Section, Chart, KPI, Mode, Scale, ChartMarker, ChartGrid, ChartPosition } from './charts.model.js';
|
|
3
|
+
export type { Layout, Section, Chart, KPI, Mode, Scale, ChartMarker, ChartGrid, ChartPosition, HoverMode, LineStyle, CellStylingConfig } from './charts.model.js';
|
|
4
4
|
export { createTimeSeriesTrace, getYAxisTitle, formatValue, processKPIData, createDefaultPlotlyLayout } from './data-utils.js';
|
|
5
5
|
export { adaptPlotlyLayout, getSizeCategory, createMarkerShapes, createMarkerAnnotations, addMarkersToLayout } from './adapt.js';
|
|
6
6
|
export type { ContainerSize, ChartInfo, AdaptationConfig } from './adapt.js';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Tree utility functions
|
|
3
3
|
* Helper functions for path manipulation, tree flattening, and state management
|
|
4
4
|
*/
|
|
5
|
+
import { log } from '../logger';
|
|
5
6
|
/**
|
|
6
7
|
* Get parent path from a node path
|
|
7
8
|
* @example getParentPath("site-a:sector-1:700", ":") => "site-a:sector-1"
|
|
@@ -144,10 +145,18 @@ export function saveStateToStorage(namespace, state) {
|
|
|
144
145
|
if (!namespace)
|
|
145
146
|
return;
|
|
146
147
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
const checkedArray = Array.from(state.checkedPaths);
|
|
149
|
+
const expandedArray = Array.from(state.expandedPaths);
|
|
150
|
+
localStorage.setItem(getStorageKey(namespace, 'checked'), JSON.stringify(checkedArray));
|
|
151
|
+
localStorage.setItem(getStorageKey(namespace, 'expanded'), JSON.stringify(expandedArray));
|
|
152
|
+
log('💾 State saved to localStorage', {
|
|
153
|
+
namespace,
|
|
154
|
+
checkedCount: checkedArray.length,
|
|
155
|
+
expandedCount: expandedArray.length
|
|
156
|
+
});
|
|
149
157
|
}
|
|
150
158
|
catch (error) {
|
|
159
|
+
log('❌ Failed to save tree state', { namespace, error });
|
|
151
160
|
console.warn('Failed to save tree state to localStorage:', error);
|
|
152
161
|
}
|
|
153
162
|
}
|
|
@@ -166,14 +175,23 @@ export function loadStateFromStorage(namespace, state) {
|
|
|
166
175
|
updates.checkedPaths = new Set(checkedArray);
|
|
167
176
|
// Recalculate indeterminate states
|
|
168
177
|
updates.indeterminatePaths = calculateIndeterminateStates(state.nodes, updates.checkedPaths);
|
|
178
|
+
log('📂 Loaded checked paths from localStorage', {
|
|
179
|
+
namespace,
|
|
180
|
+
checkedCount: checkedArray.length
|
|
181
|
+
});
|
|
169
182
|
}
|
|
170
183
|
if (expandedJson) {
|
|
171
184
|
const expandedArray = JSON.parse(expandedJson);
|
|
172
185
|
updates.expandedPaths = new Set(expandedArray);
|
|
186
|
+
log('📂 Loaded expanded paths from localStorage', {
|
|
187
|
+
namespace,
|
|
188
|
+
expandedCount: expandedArray.length
|
|
189
|
+
});
|
|
173
190
|
}
|
|
174
191
|
return updates;
|
|
175
192
|
}
|
|
176
193
|
catch (error) {
|
|
194
|
+
log('❌ Failed to load tree state', { namespace, error });
|
|
177
195
|
console.warn('Failed to load tree state from localStorage:', error);
|
|
178
196
|
return {};
|
|
179
197
|
}
|
|
@@ -187,8 +205,10 @@ export function clearStorageForNamespace(namespace) {
|
|
|
187
205
|
try {
|
|
188
206
|
localStorage.removeItem(getStorageKey(namespace, 'checked'));
|
|
189
207
|
localStorage.removeItem(getStorageKey(namespace, 'expanded'));
|
|
208
|
+
log('🗑️ Cleared tree state from localStorage', { namespace });
|
|
190
209
|
}
|
|
191
210
|
catch (error) {
|
|
211
|
+
log('❌ Failed to clear tree state', { namespace, error });
|
|
192
212
|
console.warn('Failed to clear tree state from localStorage:', error);
|
|
193
213
|
}
|
|
194
214
|
}
|
|
@@ -4,19 +4,39 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { writable } from 'svelte/store';
|
|
6
6
|
import { flattenTree, buildInitialState, calculateIndeterminateStates, getDescendantPaths, getParentPath, saveStateToStorage, loadStateFromStorage, clearStorageForNamespace } from './tree-utils';
|
|
7
|
+
import { log } from '../logger';
|
|
7
8
|
/**
|
|
8
9
|
* Create a tree store with state management and persistence
|
|
9
10
|
*/
|
|
10
11
|
export function createTreeStore(config) {
|
|
12
|
+
log('🌲 Creating TreeStore', {
|
|
13
|
+
namespace: config.namespace,
|
|
14
|
+
nodeCount: config.nodes.length,
|
|
15
|
+
persistState: config.persistState,
|
|
16
|
+
defaultExpandAll: config.defaultExpandAll
|
|
17
|
+
});
|
|
11
18
|
const separator = config.pathSeparator || ':';
|
|
12
19
|
// Flatten tree structure
|
|
13
20
|
const nodesMap = flattenTree(config.nodes, config);
|
|
21
|
+
log('📊 Tree flattened', {
|
|
22
|
+
totalNodes: nodesMap.size,
|
|
23
|
+
separator
|
|
24
|
+
});
|
|
14
25
|
// Build initial state
|
|
15
26
|
let state = buildInitialState(nodesMap, config);
|
|
27
|
+
log('🔧 Initial state built', {
|
|
28
|
+
checkedPaths: state.checkedPaths.size,
|
|
29
|
+
expandedPaths: state.expandedPaths.size
|
|
30
|
+
});
|
|
16
31
|
// Load persisted state if enabled
|
|
17
32
|
if (config.persistState && config.namespace) {
|
|
18
33
|
const persistedState = loadStateFromStorage(config.namespace, state);
|
|
19
34
|
state = { ...state, ...persistedState };
|
|
35
|
+
log('💾 Loaded persisted state', {
|
|
36
|
+
namespace: config.namespace,
|
|
37
|
+
checkedPaths: state.checkedPaths.size,
|
|
38
|
+
expandedPaths: state.expandedPaths.size
|
|
39
|
+
});
|
|
20
40
|
}
|
|
21
41
|
// Create writable store
|
|
22
42
|
const store = writable({
|
|
@@ -52,10 +72,13 @@ export function createTreeStore(config) {
|
|
|
52
72
|
* Toggle a node's checked state (with cascading)
|
|
53
73
|
*/
|
|
54
74
|
function toggle(path) {
|
|
75
|
+
log('🔄 Toggling node', { path });
|
|
55
76
|
updateState(state => {
|
|
56
77
|
const nodeState = state.nodes.get(path);
|
|
57
|
-
if (!nodeState)
|
|
78
|
+
if (!nodeState) {
|
|
79
|
+
log('⚠️ Node not found', { path });
|
|
58
80
|
return state;
|
|
81
|
+
}
|
|
59
82
|
const newChecked = !state.checkedPaths.has(path);
|
|
60
83
|
const newCheckedPaths = new Set(state.checkedPaths);
|
|
61
84
|
// Update this node
|
|
@@ -67,6 +90,11 @@ export function createTreeStore(config) {
|
|
|
67
90
|
}
|
|
68
91
|
// Cascade to all descendants
|
|
69
92
|
const descendants = getDescendantPaths(path, state.nodes, separator);
|
|
93
|
+
log('📦 Cascading to descendants', {
|
|
94
|
+
path,
|
|
95
|
+
descendantCount: descendants.length,
|
|
96
|
+
newChecked
|
|
97
|
+
});
|
|
70
98
|
descendants.forEach(descendantPath => {
|
|
71
99
|
if (newChecked) {
|
|
72
100
|
newCheckedPaths.add(descendantPath);
|
|
@@ -99,6 +127,12 @@ export function createTreeStore(config) {
|
|
|
99
127
|
}
|
|
100
128
|
// Recalculate indeterminate states
|
|
101
129
|
const newIndeterminatePaths = calculateIndeterminateStates(state.nodes, newCheckedPaths);
|
|
130
|
+
log('✅ Toggle complete', {
|
|
131
|
+
path,
|
|
132
|
+
newChecked,
|
|
133
|
+
totalChecked: newCheckedPaths.size,
|
|
134
|
+
indeterminate: newIndeterminatePaths.size
|
|
135
|
+
});
|
|
102
136
|
return {
|
|
103
137
|
...state,
|
|
104
138
|
checkedPaths: newCheckedPaths,
|
|
@@ -155,11 +189,13 @@ export function createTreeStore(config) {
|
|
|
155
189
|
* Check all nodes
|
|
156
190
|
*/
|
|
157
191
|
function checkAll() {
|
|
192
|
+
log('✅ Check all nodes');
|
|
158
193
|
updateState(state => {
|
|
159
194
|
const newCheckedPaths = new Set();
|
|
160
195
|
state.nodes.forEach((_, path) => {
|
|
161
196
|
newCheckedPaths.add(path);
|
|
162
197
|
});
|
|
198
|
+
log('✅ All nodes checked', { totalChecked: newCheckedPaths.size });
|
|
163
199
|
return {
|
|
164
200
|
...state,
|
|
165
201
|
checkedPaths: newCheckedPaths,
|
|
@@ -171,6 +207,7 @@ export function createTreeStore(config) {
|
|
|
171
207
|
* Uncheck all nodes
|
|
172
208
|
*/
|
|
173
209
|
function uncheckAll() {
|
|
210
|
+
log('❌ Uncheck all nodes');
|
|
174
211
|
updateState(state => ({
|
|
175
212
|
...state,
|
|
176
213
|
checkedPaths: new Set(),
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple debug logger utility
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { log } from './';
|
|
7
|
+
*
|
|
8
|
+
* log('User clicked button', { userId: 123 });
|
|
9
|
+
* log('Data loaded', data);
|
|
10
|
+
*
|
|
11
|
+
* // Disable logging globally
|
|
12
|
+
* log.disable();
|
|
13
|
+
*
|
|
14
|
+
* // Enable logging globally
|
|
15
|
+
* log.enable();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Simple log function - logs to console when enabled
|
|
20
|
+
*/
|
|
21
|
+
export declare function log(message: string, ...args: any[]): void;
|
|
22
|
+
export declare namespace log {
|
|
23
|
+
var disable: () => void;
|
|
24
|
+
var enable: () => void;
|
|
25
|
+
var isEnabled: () => boolean;
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple debug logger utility
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { log } from './';
|
|
7
|
+
*
|
|
8
|
+
* log('User clicked button', { userId: 123 });
|
|
9
|
+
* log('Data loaded', data);
|
|
10
|
+
*
|
|
11
|
+
* // Disable logging globally
|
|
12
|
+
* log.disable();
|
|
13
|
+
*
|
|
14
|
+
* // Enable logging globally
|
|
15
|
+
* log.enable();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
let enabled = true;
|
|
19
|
+
/**
|
|
20
|
+
* Simple log function - logs to console when enabled
|
|
21
|
+
*/
|
|
22
|
+
export function log(message, ...args) {
|
|
23
|
+
if (enabled) {
|
|
24
|
+
console.log(`[DEBUG]`, message, ...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Disable logging globally
|
|
29
|
+
*/
|
|
30
|
+
log.disable = () => {
|
|
31
|
+
enabled = false;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Enable logging globally
|
|
35
|
+
*/
|
|
36
|
+
log.enable = () => {
|
|
37
|
+
enabled = true;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Check if logging is enabled
|
|
41
|
+
*/
|
|
42
|
+
log.isEnabled = () => enabled;
|