@@ -8,7 +8,9 @@ var _modIntradayCharts = [];
88/* QAM color scheme */
99var QAM_COLORS = {
1010 '4QAM' : '#ef4444' ,
11+ '8QAM' : '#fb7185' ,
1112 '16QAM' : '#f97316' ,
13+ '32QAM' : '#f59e0b' ,
1214 '64QAM' : '#eab308' ,
1315 '128QAM' : '#84cc16' ,
1416 '256QAM' : '#22c55e' ,
@@ -20,6 +22,22 @@ var QAM_COLORS = {
2022 'Unknown' : '#6b7280'
2123} ;
2224
25+ var MODULATION_LEVELS = [
26+ '4QAM' ,
27+ '8QAM' ,
28+ '16QAM' ,
29+ '32QAM' ,
30+ '64QAM' ,
31+ '128QAM' ,
32+ '256QAM' ,
33+ '512QAM' ,
34+ '1024QAM' ,
35+ '4096QAM' ,
36+ 'OFDM' ,
37+ 'OFDMA' ,
38+ 'Unknown'
39+ ] ;
40+
2341/* ── Direction tabs ── */
2442var dirTabs = document . querySelectorAll ( '#modulation-direction-tabs .trend-tab' ) ;
2543dirTabs . forEach ( function ( btn ) {
@@ -206,6 +224,9 @@ function renderProtocolGroups(data) {
206224 barDiv . style . height = '100%' ;
207225 barWrap . appendChild ( barDiv ) ;
208226 barCard . appendChild ( barWrap ) ;
227+ var legendDiv = _el ( 'div' , 'modulation-custom-legend' ) ;
228+ legendDiv . id = 'mod-dist-legend-' + idx ;
229+ barCard . appendChild ( legendDiv ) ;
209230 chartsGrid . appendChild ( barCard ) ;
210231
211232 // Trend line chart card
@@ -245,8 +266,10 @@ function _buildMiniKPI(label, value, cls) {
245266
246267function renderGroupDistChart ( pg , idx ) {
247268 var container = document . getElementById ( 'mod-dist-chart-' + idx ) ;
269+ var legendContainer = document . getElementById ( 'mod-dist-legend-' + idx ) ;
248270 if ( ! container ) return ;
249271 container . textContent = '' ;
272+ if ( legendContainer ) legendContainer . textContent = '' ;
250273
251274 var days = pg . days || [ ] ;
252275 var labels = days . map ( function ( d ) { return d . date . slice ( 5 ) ; } ) ; /* MM-DD, drop year */
@@ -269,8 +292,10 @@ function renderGroupDistChart(pg, idx) {
269292 /* Build cumulative sums for each modulation layer */
270293 var cumData = xData . map ( function ( ) { return 0 ; } ) ;
271294 var layerData = [ ] ;
295+ var rawSeriesByMod = { } ;
272296 modKeys . forEach ( function ( mod ) {
273297 var raw = days . map ( function ( d ) { return ( d . distribution || { } ) [ mod ] || 0 ; } ) ;
298+ rawSeriesByMod [ mod ] = raw ;
274299 var stacked = raw . map ( function ( v , j ) { cumData [ j ] += v ; return cumData [ j ] ; } ) ;
275300 layerData . push ( { mod : mod , data : stacked } ) ;
276301 } ) ;
@@ -291,6 +316,10 @@ function renderGroupDistChart(pg, idx) {
291316 } ) ;
292317 }
293318
319+ if ( legendContainer ) {
320+ renderDistributionLegend ( legendContainer , modKeys ) ;
321+ }
322+
294323 var w = container . offsetWidth || 400 ;
295324 var h = container . offsetHeight || 300 ;
296325 var chart = new uPlot ( {
@@ -321,8 +350,13 @@ function renderGroupDistChart(pg, idx) {
321350 ] ,
322351 series : uSeries ,
323352 cursor : { show : true , x : true , y : false , points : { show : false } } ,
324- legend : { show : true , live : false } ,
325- plugins : [ tooltipPlugin ( labels ) ]
353+ legend : { show : false , live : false } ,
354+ plugins : [ tooltipPlugin ( labels , function ( ctx ) {
355+ var mod = ctx . dataset . label ;
356+ var rawSeries = rawSeriesByMod [ mod ] || [ ] ;
357+ var raw = rawSeries [ ctx . dataIndex ] || 0 ;
358+ return mod + ': ' + raw . toFixed ( 1 ) + '%' ;
359+ } ) ]
326360 } , uData , container ) ;
327361 _modCharts . push ( chart ) ;
328362}
@@ -523,7 +557,7 @@ function renderChannelTimeline(canvasId, timeline) {
523557 var dataPoints = timeline . map ( function ( t ) { return modSortOrder ( t . modulation ) ; } ) ;
524558 var n = labels . length ;
525559 var textColor = _cssVar ( '--text-secondary' ) || '#9ca3af' ;
526- var qamLabels = [ '4QAM' , '16QAM' , '64QAM' , '128QAM' , '256QAM' , '512QAM' , '1024QAM' , '4096QAM' , 'OFDM' , 'OFDMA' , 'Unknown' ] ;
560+ var qamLabels = MODULATION_LEVELS ;
527561
528562 var xData = [ ] ;
529563 for ( var i = 0 ; i < n ; i ++ ) xData . push ( i ) ;
@@ -535,7 +569,7 @@ function renderChannelTimeline(canvasId, timeline) {
535569 height : h ,
536570 scales : {
537571 x : { time : false , range : function ( ) { return [ - 0.5 , n - 0.5 ] ; } } ,
538- y : { range : [ - 0.5 , 10 .5] }
572+ y : { range : [ - 0.5 , qamLabels . length - 0 .5] }
539573 } ,
540574 axes : [
541575 {
@@ -556,7 +590,9 @@ function renderChannelTimeline(canvasId, timeline) {
556590 scale : 'y' ,
557591 stroke : textColor ,
558592 grid : { stroke : 'rgba(255,255,255,0.04)' , width : 1 } ,
559- splits : function ( ) { return [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ] ; } ,
593+ splits : function ( ) {
594+ return qamLabels . map ( function ( _ , idx ) { return idx ; } ) ;
595+ } ,
560596 values : function ( u , vals ) { return vals . map ( function ( v ) { return qamLabels [ v ] || '' ; } ) ; } ,
561597 font : '10px system-ui' ,
562598 size : 60
@@ -609,8 +645,22 @@ function densityClass(v) {
609645 return 'critical' ;
610646}
611647function modSortOrder ( mod ) {
612- var order = { '4QAM' : 0 , '16QAM' : 1 , '64QAM' : 2 , '128QAM' : 3 , '256QAM' : 4 , '512QAM' : 5 , '1024QAM' : 6 , '4096QAM' : 7 , 'OFDM' : 8 , 'OFDMA' : 9 , 'Unknown' : 10 } ;
613- return order [ mod ] !== undefined ? order [ mod ] : 11 ;
648+ var order = { } ;
649+ MODULATION_LEVELS . forEach ( function ( label , idx ) {
650+ order [ label ] = idx ;
651+ } ) ;
652+ return order [ mod ] !== undefined ? order [ mod ] : ( MODULATION_LEVELS . length - 1 ) ;
653+ }
654+
655+ function renderDistributionLegend ( container , modKeys ) {
656+ modKeys . forEach ( function ( mod ) {
657+ var item = _el ( 'div' , 'modulation-custom-legend-item' ) ;
658+ var swatch = _el ( 'span' , 'modulation-custom-legend-swatch' ) ;
659+ swatch . style . background = QAM_COLORS [ mod ] || '#6b7280' ;
660+ item . appendChild ( swatch ) ;
661+ item . appendChild ( _el ( 'span' , 'modulation-custom-legend-label' , mod ) ) ;
662+ container . appendChild ( item ) ;
663+ } ) ;
614664}
615665
616666function destroyCharts ( ) {
0 commit comments