@@ -18,6 +18,10 @@ const kNegativeBorder = '#8b0000'; // darkred
1818const kPositiveFill = '#90ee90' ; // lightgreen
1919const kPositiveBorder = '#006400' ; // darkgreen
2020
21+ // Line chart series colors (Tableau 10)
22+ const kLineColors = [ '#4e79a7' , '#f28e2b' , '#e15759' , '#76b7b2' ,
23+ '#59a14f' , '#edc948' , '#b07aa1' , '#ff9da7' ] ;
24+
2125// Pure layout computation — extracted for testability.
2226export function computeHistogramLayout ( histogramData , canvasWidth , canvasHeight ) {
2327 const bins = histogramData ?. bins ;
@@ -107,6 +111,10 @@ export class ChartsWidget {
107111 this . _chartArea = null ;
108112 this . _hoveredBar = null ;
109113
114+ // Debug charts (line charts from GPL, etc.)
115+ this . _debugCharts = [ ] ;
116+ this . _activeDebugChart = - 1 ; // -1 = show histogram
117+
110118 this . _build ( ) ;
111119 }
112120
@@ -116,6 +124,12 @@ export class ChartsWidget {
116124 const el = document . createElement ( 'div' ) ;
117125 el . className = 'charts-widget' ;
118126
127+ // Debug chart tabs (hidden until debug charts arrive)
128+ this . _debugTabBar = document . createElement ( 'div' ) ;
129+ this . _debugTabBar . className = 'timing-tab-bar' ;
130+ this . _debugTabBar . style . display = 'none' ;
131+ el . appendChild ( this . _debugTabBar ) ;
132+
119133 // Toolbar
120134 const toolbar = document . createElement ( 'div' ) ;
121135 toolbar . className = 'charts-toolbar' ;
@@ -184,6 +198,10 @@ export class ChartsWidget {
184198 this . _tooltip . style . display = 'none' ;
185199 el . appendChild ( this . _tooltip ) ;
186200
201+ // Group histogram-specific elements so we can hide them
202+ // when a debug line chart tab is active.
203+ this . _histogramControls = [ toolbar , tabBar , filterRow ] ;
204+
187205 this . element = el ;
188206
189207 this . _ctx = this . _canvas . getContext ( '2d' ) ;
@@ -216,15 +234,13 @@ export class ChartsWidget {
216234 this . _canvas . addEventListener ( 'mouseleave' , ( ) => {
217235 this . _hoveredBar = null ;
218236 this . _tooltip . style . display = 'none' ;
219- this . _render ( ) ;
237+ this . _syncView ( ) ;
220238 } ) ;
221239 this . _canvas . addEventListener ( 'click' , ( e ) => this . _handleClick ( e ) ) ;
222240
223241 // Re-render on resize
224242 const ro = new ResizeObserver ( ( ) => {
225- this . _sizeCanvas ( ) ;
226- this . _computeLayout ( ) ;
227- this . _render ( ) ;
243+ this . _syncView ( ) ;
228244 } ) ;
229245 ro . observe ( this . _canvas ) ;
230246 }
@@ -288,9 +304,7 @@ export class ChartsWidget {
288304 }
289305 const data = await this . _app . websocketManager . request ( req ) ;
290306 this . _histogramData = data ;
291- this . _sizeCanvas ( ) ;
292- this . _computeLayout ( ) ;
293- this . _render ( ) ;
307+ this . _syncView ( ) ;
294308
295309 const total = data . total_endpoints || 0 ;
296310 const unconstrained = data . unconstrained_count || 0 ;
@@ -313,7 +327,7 @@ export class ChartsWidget {
313327 this . _chartArea = result . chartArea ;
314328 }
315329
316- render ( ) { this . _render ( ) ; }
330+ render ( ) { this . _syncView ( ) ; }
317331
318332 _render ( ) {
319333 const rect = this . _canvas . getBoundingClientRect ( ) ;
@@ -452,7 +466,7 @@ export class ChartsWidget {
452466 }
453467
454468 _hitTestBar ( e ) {
455- if ( ! this . _bars ) return null ;
469+ if ( this . _activeDebugChart >= 0 || ! this . _bars ) return null ;
456470 const rect = this . _canvas . getBoundingClientRect ( ) ;
457471 const mx = e . clientX - rect . left ;
458472 const my = e . clientY - rect . top ;
@@ -466,6 +480,11 @@ export class ChartsWidget {
466480 }
467481
468482 _handleHover ( e ) {
483+ if ( this . _activeDebugChart >= 0 ) {
484+ this . _hoveredBar = null ;
485+ this . _tooltip . style . display = 'none' ;
486+ return ;
487+ }
469488 const bar = this . _hitTestBar ( e ) ;
470489 if ( bar !== this . _hoveredBar ) {
471490 this . _hoveredBar = bar ;
@@ -492,6 +511,7 @@ export class ChartsWidget {
492511 }
493512
494513 async _handleClick ( e ) {
514+ if ( this . _activeDebugChart >= 0 ) return ;
495515 const bar = this . _hitTestBar ( e ) ;
496516 if ( ! bar || bar . count === 0 ) return ;
497517
@@ -515,4 +535,223 @@ export class ChartsWidget {
515535 console . error ( 'Charts bar click error:' , err ) ;
516536 }
517537 }
538+
539+ // ---- Debug line charts (GPL HPWL, density, etc.) ----
540+
541+ setDebugCharts ( charts ) {
542+ this . _debugCharts = charts ;
543+ this . _rebuildDebugTabs ( ) ;
544+ // Auto-select the first debug chart if none is active;
545+ // reset to histogram if charts are now empty.
546+ if ( charts . length === 0 ) {
547+ this . _activeDebugChart = - 1 ;
548+ } else if ( this . _activeDebugChart < 0 ) {
549+ this . _activeDebugChart = 0 ;
550+ } else if ( this . _activeDebugChart >= charts . length ) {
551+ this . _activeDebugChart = 0 ;
552+ }
553+ this . _syncView ( ) ;
554+ }
555+
556+ _rebuildDebugTabs ( ) {
557+ const bar = this . _debugTabBar ;
558+ bar . innerHTML = '' ;
559+ if ( this . _debugCharts . length === 0 ) {
560+ bar . style . display = 'none' ;
561+ return ;
562+ }
563+ bar . style . display = '' ;
564+
565+ // "Histogram" tab to switch back to the slack histogram.
566+ const histTab = document . createElement ( 'div' ) ;
567+ histTab . className = 'timing-tab' +
568+ ( this . _activeDebugChart < 0 ? ' active' : '' ) ;
569+ histTab . textContent = 'Histogram' ;
570+ histTab . addEventListener ( 'click' , ( ) => {
571+ this . _activeDebugChart = - 1 ;
572+ this . _rebuildDebugTabs ( ) ;
573+ this . _syncView ( ) ;
574+ } ) ;
575+ bar . appendChild ( histTab ) ;
576+
577+ // One tab per debug chart.
578+ this . _debugCharts . forEach ( ( chart , i ) => {
579+ const tab = document . createElement ( 'div' ) ;
580+ tab . className = 'timing-tab' +
581+ ( this . _activeDebugChart === i ? ' active' : '' ) ;
582+ tab . textContent = chart . name ;
583+ tab . addEventListener ( 'click' , ( ) => {
584+ this . _activeDebugChart = i ;
585+ this . _rebuildDebugTabs ( ) ;
586+ this . _syncView ( ) ;
587+ } ) ;
588+ bar . appendChild ( tab ) ;
589+ } ) ;
590+ }
591+
592+ _syncView ( ) {
593+ const showHist = this . _activeDebugChart < 0 ;
594+ if ( ! showHist ) {
595+ this . _hoveredBar = null ;
596+ this . _tooltip . style . display = 'none' ;
597+ }
598+ for ( const el of this . _histogramControls ) {
599+ el . style . display = showHist ? '' : 'none' ;
600+ }
601+ this . _sizeCanvas ( ) ;
602+ if ( showHist ) {
603+ this . _computeLayout ( ) ;
604+ this . _render ( ) ;
605+ } else {
606+ this . _renderLineChart ( this . _debugCharts [ this . _activeDebugChart ] ) ;
607+ }
608+ }
609+
610+ _renderLineChart ( chart ) {
611+ const rect = this . _canvas . getBoundingClientRect ( ) ;
612+ const w = rect . width ;
613+ const h = rect . height ;
614+ const ctx = this . _ctx ;
615+ const tc = getThemeColors ( ) ;
616+
617+ ctx . clearRect ( 0 , 0 , w , h ) ;
618+ ctx . fillStyle = tc . canvasBg ;
619+ ctx . fillRect ( 0 , 0 , w , h ) ;
620+
621+ const pts = chart . points ;
622+ if ( ! pts || pts . length === 0 ) {
623+ ctx . fillStyle = tc . canvasText ;
624+ ctx . font = '14px monospace' ;
625+ ctx . textAlign = 'center' ;
626+ ctx . textBaseline = 'middle' ;
627+ ctx . fillText ( 'No data points yet' , w / 2 , h / 2 ) ;
628+ return ;
629+ }
630+
631+ const numSeries = chart . y_labels ? chart . y_labels . length : 1 ;
632+ const cLeft = kLeftMargin ;
633+ const cRight = w - kRightMargin ;
634+ const cTop = kTopMargin ;
635+ const cBottom = h - kBottomMargin ;
636+ const cW = cRight - cLeft ;
637+ const cH = cBottom - cTop ;
638+ if ( cW <= 0 || cH <= 0 ) return ;
639+
640+ // Compute data ranges.
641+ let xMin = pts [ 0 ] . x , xMax = pts [ 0 ] . x ;
642+ let yMin = Infinity , yMax = - Infinity ;
643+ for ( const p of pts ) {
644+ if ( p . x < xMin ) xMin = p . x ;
645+ if ( p . x > xMax ) xMax = p . x ;
646+ for ( const v of p . ys ) {
647+ if ( v < yMin ) yMin = v ;
648+ if ( v > yMax ) yMax = v ;
649+ }
650+ }
651+ if ( xMin === xMax ) xMax = xMin + 1 ;
652+ if ( ! isFinite ( yMin ) || ! isFinite ( yMax ) ) { yMin = 0 ; yMax = 1 ; }
653+ // Add 5% padding to Y range.
654+ const yPad = ( yMax - yMin ) * 0.05 || 1 ;
655+ yMin -= yPad ;
656+ yMax += yPad ;
657+
658+ const toX = ( v ) => cLeft + ( ( v - xMin ) / ( xMax - xMin ) ) * cW ;
659+ const toY = ( v ) => cBottom - ( ( v - yMin ) / ( yMax - yMin ) ) * cH ;
660+
661+ // Axes.
662+ ctx . strokeStyle = tc . canvasAxis ;
663+ ctx . lineWidth = 1 ;
664+ ctx . beginPath ( ) ;
665+ ctx . moveTo ( cLeft , cTop ) ;
666+ ctx . lineTo ( cLeft , cBottom ) ;
667+ ctx . lineTo ( cRight , cBottom ) ;
668+ ctx . stroke ( ) ;
669+
670+ // Y axis ticks.
671+ ctx . fillStyle = tc . canvasLabel ;
672+ ctx . font = '10px monospace' ;
673+ ctx . textAlign = 'right' ;
674+ ctx . textBaseline = 'middle' ;
675+ const nYTicks = 5 ;
676+ for ( let i = 0 ; i <= nYTicks ; i ++ ) {
677+ const v = yMin + ( yMax - yMin ) * i / nYTicks ;
678+ const y = toY ( v ) ;
679+ ctx . fillText ( this . _formatNum ( v ) , cLeft - 4 , y ) ;
680+ if ( i > 0 && i < nYTicks ) {
681+ ctx . strokeStyle = tc . canvasGrid ;
682+ ctx . beginPath ( ) ;
683+ ctx . moveTo ( cLeft , y ) ;
684+ ctx . lineTo ( cRight , y ) ;
685+ ctx . stroke ( ) ;
686+ }
687+ }
688+
689+ // X axis ticks.
690+ ctx . textAlign = 'center' ;
691+ ctx . textBaseline = 'top' ;
692+ if ( pts . length === 1 ) {
693+ ctx . fillStyle = tc . canvasLabel ;
694+ ctx . fillText ( this . _formatNum ( pts [ 0 ] . x ) , toX ( pts [ 0 ] . x ) , cBottom + 4 ) ;
695+ } else {
696+ const nXTicks = Math . min ( pts . length - 1 , 6 ) ;
697+ for ( let i = 0 ; i <= nXTicks ; i ++ ) {
698+ const v = xMin + ( xMax - xMin ) * i / nXTicks ;
699+ const x = toX ( v ) ;
700+ ctx . fillStyle = tc . canvasLabel ;
701+ ctx . fillText ( this . _formatNum ( v ) , x , cBottom + 4 ) ;
702+ }
703+ }
704+
705+ // Axis labels.
706+ ctx . fillStyle = tc . canvasTitle ;
707+ ctx . font = '11px monospace' ;
708+ ctx . textAlign = 'center' ;
709+ ctx . fillText ( chart . x_label || '' , ( cLeft + cRight ) / 2 , cBottom + 22 ) ;
710+
711+ // Title.
712+ ctx . fillStyle = tc . fgPrimary ;
713+ ctx . font = '13px monospace' ;
714+ ctx . textBaseline = 'top' ;
715+ ctx . fillText ( chart . name , w / 2 , 4 ) ;
716+
717+ // Draw each Y series as a line.
718+ for ( let s = 0 ; s < numSeries ; s ++ ) {
719+ ctx . strokeStyle = kLineColors [ s % kLineColors . length ] ;
720+ ctx . lineWidth = 1.5 ;
721+ ctx . beginPath ( ) ;
722+ let first = true ;
723+ for ( const p of pts ) {
724+ const px = toX ( p . x ) ;
725+ const py = toY ( p . ys [ s ] ) ;
726+ if ( first ) { ctx . moveTo ( px , py ) ; first = false ; }
727+ else { ctx . lineTo ( px , py ) ; }
728+ }
729+ ctx . stroke ( ) ;
730+ }
731+
732+ // Legend (if multiple series).
733+ if ( numSeries > 1 && chart . y_labels ) {
734+ ctx . font = '10px monospace' ;
735+ ctx . textAlign = 'left' ;
736+ ctx . textBaseline = 'top' ;
737+ let lx = cLeft + 8 ;
738+ const ly = cTop + 4 ;
739+ for ( let s = 0 ; s < numSeries ; s ++ ) {
740+ const color = kLineColors [ s % kLineColors . length ] ;
741+ ctx . fillStyle = color ;
742+ ctx . fillRect ( lx , ly + s * 14 , 12 , 10 ) ;
743+ ctx . fillStyle = tc . canvasLabel ;
744+ ctx . fillText ( chart . y_labels [ s ] , lx + 16 , ly + s * 14 ) ;
745+ }
746+ }
747+ }
748+
749+ _formatNum ( v ) {
750+ const abs = Math . abs ( v ) ;
751+ if ( abs === 0 ) return '0' ;
752+ if ( abs >= 1e6 ) return ( v / 1e6 ) . toFixed ( 1 ) + 'M' ;
753+ if ( abs >= 1e3 ) return ( v / 1e3 ) . toFixed ( 1 ) + 'k' ;
754+ if ( abs >= 1 ) return v . toFixed ( 1 ) ;
755+ return v . toPrecision ( 3 ) ;
756+ }
518757}
0 commit comments