@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.
@@ -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
- return filterChartData(rawData, storeValue.state.checkedPaths);
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(transformChartData(filteredData, baseMetrics));
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(expandLayoutForCells(baseLayout, filteredData));
48
- console.log('chartLayout', chartLayout);
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(new Set(filteredData.map((r) => r.cellName)).size);
55
- let totalSites = $derived(new Set(filteredData.map((r) => r.siteName)).size);
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,6 @@
1
+ import type { CellStylingConfig } from '../../core/Charts';
2
+ /**
3
+ * Default cell styling configuration for SiteCheck component
4
+ * Provides band colors and sector line styles for network cells
5
+ */
6
+ export declare const defaultCellStyling: CellStylingConfig;
@@ -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
- * @returns Expanded array of KPIs with cell-specific rawNames and colors
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], 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
- });
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 utilities (for external configuration)
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
- return data.filter((record) => selectedCells.has(record.cellName));
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
- * Optimizes tooltip display and performance for different chart sizes
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
- // Priority 1: Disable hover in tiny charts (performance + UX)
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
- layout.hovermode = 'x'; // Single point instead of unified
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; // Smaller font
21
+ layout.hoverlabel.font.size = 9;
30
22
  }
31
23
  return layout;
32
24
  }
33
- // Priority 3: Adaptive hover mode based on series count
34
- if (totalSeries > 4 && isMedium) {
35
- // Too many series in medium chart - switch to closest
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
- // Otherwise keep default 'x unified' from base layout
43
- // Priority 4: Adaptive hover label font size
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 (isMedium) {
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 (disable in tiny, simplify in small, optimize for series count)
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
- localStorage.setItem(getStorageKey(namespace, 'checked'), JSON.stringify(Array.from(state.checkedPaths)));
148
- localStorage.setItem(getStorageKey(namespace, 'expanded'), JSON.stringify(Array.from(state.expandedPaths)));
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(),
@@ -1,3 +1,4 @@
1
1
  export * from './Desktop/index.js';
2
2
  export * from './Charts/index.js';
3
3
  export * from './TreeView/index.js';
4
+ export * from './logger/index.js';
@@ -6,3 +6,5 @@ export * from './Desktop/index.js';
6
6
  export * from './Charts/index.js';
7
7
  // TreeView generic hierarchical component
8
8
  export * from './TreeView/index.js';
9
+ // Logger utility for debugging and monitoring
10
+ export * from './logger/index.js';
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",