@@ -153,6 +153,9 @@ export class FlameChart<E extends EventNode = EventNode> {
153153
154154 private hitTestManager : HitTestManager | null = null ;
155155
156+ // Flag to track if ResizeObserver has been set up (deferred until after first render)
157+ private resizeObserverActive = false ;
158+
156159 // Selection orchestrator (owns selection state and rendering)
157160 private selectionOrchestrator : SelectionOrchestrator < E > | null = null ;
158161 private viewportAnimator : ViewportAnimator | null = null ;
@@ -239,7 +242,7 @@ export class FlameChart<E extends EventNode = EventNode> {
239242 // Store truncation markers for rendering
240243 this . markers . push ( ...markers ) ;
241244
242- // Get container dimensions
245+ // Get container dimensions for validation
243246 const { width, height } = container . getBoundingClientRect ( ) ;
244247 if ( width === 0 || height === 0 ) {
245248 throw new TimelineError (
@@ -256,28 +259,22 @@ export class FlameChart<E extends EventNode = EventNode> {
256259 : undefined ,
257260 ) ;
258261
259- // Calculate minimap and metric strip heights BEFORE creating viewport
260- // Viewport needs the available height for main timeline (excluding minimap + metric strip + gaps)
261- // Metric strip starts collapsed, so use collapsed height for initial layout
262- const minimapHeight = calculateMinimapHeight ( height ) ;
263- const metricStripHeight = METRIC_STRIP_COLLAPSED_HEIGHT ;
264- const totalOverheadHeight = minimapHeight + MINIMAP_GAP + metricStripHeight + METRIC_STRIP_GAP ;
265- const mainTimelineHeight = height - totalOverheadHeight ;
262+ // Initialize PixiJS Application first - creates DOM and measures dimensions after RAF.
263+ // This ensures flex layout is computed before we measure mainDiv's actual height.
264+ const { mainTimelineHeight } = await this . setupPixiApplication ( width , height ) ;
266265
267- // Store offset for converting canvas-relative to container-relative coordinates
268- this . mainTimelineYOffset = totalOverheadHeight ;
266+ // Calculate mainTimelineYOffset from measured height
267+ // This is the vertical offset from container top to main timeline canvas
268+ this . mainTimelineYOffset = height - mainTimelineHeight ;
269269
270- // Create viewport manager with adjusted height for main timeline area
270+ // Create viewport manager with measured height for main timeline area
271271 this . viewport = new TimelineViewport (
272272 width ,
273273 mainTimelineHeight ,
274274 this . index . totalDuration ,
275275 this . index . maxDepth ,
276276 ) ;
277277
278- // Initialize PixiJS Application
279- await this . setupPixiApplication ( width , height ) ;
280-
281278 // Setup coordinate system (Y-axis inversion)
282279 this . setupCoordinateSystem ( ) ;
283280
@@ -410,12 +407,10 @@ export class FlameChart<E extends EventNode = EventNode> {
410407 // Setup keyboard handler
411408 this . setupKeyboardHandler ( ) ;
412409
413- // Pass the same dimensions that init() used to create the viewport.
414- // This ensures ResizeObserver's initial callback (which fires with current container
415- // dimensions) is correctly skipped, even if DOM manipulation during init caused
416- // a layout shift that changed the container size.
417- this . resizeHandler = new TimelineResizeHandler ( container , this , width , height ) ;
418- this . resizeHandler . setupResizeObserver ( ) ;
410+ // Container dimensions are unchanged by DOM setup (wrapper fills 100%).
411+ // The fix is measuring mainTimelineHeight from actual flexbox layout.
412+ // ResizeObserver setup is deferred until after first render to avoid double render on init.
413+ this . resizeHandler = new TimelineResizeHandler ( container , this ) ;
419414
420415 // Initialize search if enabled via options
421416 if ( options . enableSearch ) {
@@ -691,6 +686,13 @@ export class FlameChart<E extends EventNode = EventNode> {
691686 if ( this . state && this . state . needsRender ) {
692687 this . render ( ) ;
693688 this . state . needsRender = false ;
689+
690+ // Setup ResizeObserver after first render to avoid double render on init.
691+ // By this point, layout is finalized and ResizeObserver baseline matches rendered state.
692+ if ( this . resizeHandler && ! this . resizeObserverActive ) {
693+ this . resizeHandler . setupResizeObserver ( ) ;
694+ this . resizeObserverActive = true ;
695+ }
694696 }
695697 this . renderLoopId = null ;
696698 } ) ;
@@ -699,6 +701,7 @@ export class FlameChart<E extends EventNode = EventNode> {
699701
700702 /**
701703 * Handle window resize.
704+ * Calculates main timeline height by subtracting visible component heights.
702705 */
703706 public resize ( newWidth : number , newHeight : number ) : void {
704707 if ( ! this . app || ! this . viewport || ! this . container || ! this . index ) {
@@ -718,20 +721,31 @@ export class FlameChart<E extends EventNode = EventNode> {
718721
719722 const visibleWorldYBottom = - oldState . offsetY ;
720723
721- // Calculate new minimap, metric strip, and main timeline heights
722- // Query actual metric strip height (respects collapsed/expanded state)
724+ // Calculate component heights, only including visible components
723725 const minimapHeight = calculateMinimapHeight ( newHeight ) ;
724- const metricStripHeight =
725- this . metricStripOrchestrator ?. getHeight ( ) ?? METRIC_STRIP_COLLAPSED_HEIGHT ;
726- const totalOverheadHeight = minimapHeight + MINIMAP_GAP + metricStripHeight + METRIC_STRIP_GAP ;
726+
727+ // Only include metric strip in calculation if it's visible (has data)
728+ const metricStripVisible = this . metricStripOrchestrator ?. getIsVisible ( ) ?? false ;
729+ const metricStripHeight = metricStripVisible
730+ ? ( this . metricStripOrchestrator ?. getHeight ( ) ?? METRIC_STRIP_COLLAPSED_HEIGHT )
731+ : 0 ;
732+ const metricStripGap = metricStripVisible ? METRIC_STRIP_GAP : 0 ;
733+
734+ // Calculate main timeline height by subtracting all visible component heights
735+ const totalOverheadHeight = minimapHeight + MINIMAP_GAP + metricStripHeight + metricStripGap ;
727736 const mainTimelineHeight = newHeight - totalOverheadHeight ;
728737
738+ if ( mainTimelineHeight <= 0 ) {
739+ return ; // Invalid state, skip resize
740+ }
741+
729742 // Update offset for converting canvas-relative to container-relative coordinates
730743 this . mainTimelineYOffset = totalOverheadHeight ;
731744
732- // Update orchestrators with new offset
733- this . selectionOrchestrator ?. setMainTimelineYOffset ( this . mainTimelineYOffset ) ;
734- this . searchOrchestrator ?. setMainTimelineYOffset ( this . mainTimelineYOffset ) ;
745+ // Update minimap div height
746+ if ( this . minimapDiv ) {
747+ this . minimapDiv . style . height = `${ minimapHeight } px` ;
748+ }
735749
736750 // Resize minimap orchestrator
737751 if ( this . minimapOrchestrator ) {
@@ -743,17 +757,13 @@ export class FlameChart<E extends EventNode = EventNode> {
743757 this . metricStripOrchestrator . resize ( newWidth ) ;
744758 }
745759
760+ // Update orchestrators with new offset
761+ this . selectionOrchestrator ?. setMainTimelineYOffset ( this . mainTimelineYOffset ) ;
762+ this . searchOrchestrator ?. setMainTimelineYOffset ( this . mainTimelineYOffset ) ;
763+
746764 // Resize main timeline app
747765 this . app . renderer . resize ( newWidth , mainTimelineHeight ) ;
748766
749- // Update wrapper div heights
750- if ( this . wrapper ) {
751- const minimapDiv = this . wrapper . children [ 0 ] as HTMLElement ;
752- if ( minimapDiv ) {
753- minimapDiv . style . height = `${ minimapHeight } px` ;
754- }
755- }
756-
757767 const newZoom = newWidth / visibleTimeRange ;
758768 const newOffsetX = visibleTimeStart * newZoom ;
759769 const newOffsetY = - visibleWorldYBottom ;
@@ -806,6 +816,7 @@ export class FlameChart<E extends EventNode = EventNode> {
806816 /**
807817 * Update metric strip visibility based on whether there's data to display.
808818 * Hides the metric strip container and gap if no governor limit data exists.
819+ * Triggers resize to recalculate main timeline height after visibility change.
809820 */
810821 private updateMetricStripVisibility ( ) : void {
811822 const isVisible = this . metricStripOrchestrator ?. getIsVisible ( ) ?? false ;
@@ -817,13 +828,22 @@ export class FlameChart<E extends EventNode = EventNode> {
817828 if ( this . metricStripGapDiv ) {
818829 this . metricStripGapDiv . style . display = display ;
819830 }
831+
832+ // Trigger resize to recalculate main timeline height after visibility change
833+ if ( this . container && isVisible ) {
834+ const { width, height } = this . container . getBoundingClientRect ( ) ;
835+ this . resize ( width , height ) ;
836+ }
820837 }
821838
822839 // ============================================================================
823840 // PRIVATE SETUP METHODS
824841 // ============================================================================
825842
826- private async setupPixiApplication ( width : number , height : number ) : Promise < void > {
843+ private async setupPixiApplication (
844+ width : number ,
845+ height : number ,
846+ ) : Promise < { mainTimelineHeight : number } > {
827847 const ticker = PIXI . Ticker . shared ;
828848 ticker . autoStart = false ;
829849 ticker . stop ( ) ;
@@ -832,12 +852,10 @@ export class FlameChart<E extends EventNode = EventNode> {
832852 sysTicker . autoStart = false ;
833853 sysTicker . stop ( ) ;
834854
835- // Calculate minimap, metric strip, and main timeline heights
855+ // Calculate minimap, metric strip heights for fixed-height elements
836856 // Metric strip starts collapsed, so use collapsed height for initial layout
837857 const minimapHeight = calculateMinimapHeight ( height ) ;
838858 const metricStripHeight = METRIC_STRIP_COLLAPSED_HEIGHT ;
839- const totalOverheadHeight = minimapHeight + MINIMAP_GAP + metricStripHeight + METRIC_STRIP_GAP ;
840- const mainTimelineHeight = height - totalOverheadHeight ;
841859
842860 // Create wrapper container with flexbox layout
843861 this . wrapper = document . createElement ( 'div' ) ;
@@ -860,7 +878,7 @@ export class FlameChart<E extends EventNode = EventNode> {
860878 this . metricStripGapDiv = document . createElement ( 'div' ) ;
861879 this . metricStripGapDiv . style . cssText = `height:${ METRIC_STRIP_GAP } px;width:100%;flex-shrink:0;background:transparent` ;
862880
863- // Main timeline container (fills remaining space)
881+ // Main timeline container (fills remaining space via flex:1 )
864882 const mainDiv = document . createElement ( 'div' ) ;
865883 mainDiv . style . cssText = 'flex:1;width:100%;min-height:0' ;
866884
@@ -876,9 +894,21 @@ export class FlameChart<E extends EventNode = EventNode> {
876894 this . container . appendChild ( this . wrapper ) ;
877895 }
878896
897+ // Calculate main timeline height by subtracting fixed component heights.
898+ // At init time, all components are visible (metric strip hides later via setHeatStripTimeSeries).
899+ const mainTimelineHeight =
900+ height - minimapHeight - MINIMAP_GAP - metricStripHeight - METRIC_STRIP_GAP ;
901+
902+ if ( mainTimelineHeight <= 0 ) {
903+ throw new TimelineError (
904+ TimelineErrorCode . INVALID_CONTAINER ,
905+ 'Container height too small for timeline layout' ,
906+ ) ;
907+ }
908+
879909 // Minimap app is created by MinimapOrchestrator in setupMinimap()
880910
881- // Create main timeline app
911+ // Create main timeline app with measured height
882912 this . app = new PIXI . Application ( ) ;
883913 await this . app . init ( {
884914 width,
@@ -893,6 +923,8 @@ export class FlameChart<E extends EventNode = EventNode> {
893923 this . app . ticker . stop ( ) ;
894924 this . app . stage . eventMode = 'none' ;
895925 mainDiv . appendChild ( this . app . canvas ) ;
926+
927+ return { mainTimelineHeight } ;
896928 }
897929
898930 private setupCoordinateSystem ( ) : void {
0 commit comments