@@ -36,6 +36,8 @@ export class ReactGantt extends SVGZoomWidget {
3636 public _minStart : number ;
3737 public _maxEnd : number ;
3838
39+ protected _prevZoomState : { visibleStart : number ; visibleEnd : number } | null = null ;
40+
3941 protected _title_idx = 0 ;
4042 protected _startDate_idx = 1 ;
4143 protected _endDate_idx = 2 ;
@@ -190,12 +192,14 @@ export class ReactGantt extends SVGZoomWidget {
190192 this . _buckets = this . calcBuckets ( this . data ( ) , this . _startDate_idx , this . _endDate_idx ) ;
191193 }
192194 }
193- const interpedStart = this . _interpolateX ( this . _minStart ) ;
194195
195- this . zoomTo (
196- [ interpedStart , 0 ] ,
197- 1
198- ) ;
196+ if ( ! this . preserveZoom ( ) || ! this . _prevZoomState ) {
197+ const interpedStart = this . _interpolateX ( this . _minStart ) ;
198+ this . zoomTo (
199+ [ interpedStart , 0 ] ,
200+ 1
201+ ) ;
202+ }
199203
200204 const bucketHeight = this . bucketHeight ( ) ;
201205
@@ -331,6 +335,51 @@ export class ReactGantt extends SVGZoomWidget {
331335 } )
332336 ;
333337 element . on ( "dblclick.zoom" , null ) ;
338+
339+ // restore zoom state after all rendering is set up
340+ if ( this . preserveZoom ( ) && this . _prevZoomState && this . _interpolateX ) {
341+ const width = this . width ( ) ;
342+ if ( width > 0 ) {
343+ const visibleStart = this . _minStart ;
344+ const visibleEnd = this . _maxEnd ;
345+ const clampedStart = Math . max ( visibleStart , Math . min ( visibleEnd , this . _prevZoomState . visibleStart ) ) ;
346+ let clampedEnd = Math . max ( visibleStart , Math . min ( visibleEnd , this . _prevZoomState . visibleEnd ) ) ;
347+ if ( clampedEnd <= clampedStart ) {
348+ const visibleWidth = visibleEnd - visibleStart ;
349+ const epsilon = visibleWidth * 1e-6 || 1e-6 ;
350+ clampedEnd = Math . min ( visibleEnd , clampedStart + epsilon ) ;
351+ }
352+ const startPixel = this . _interpolateX ( clampedStart ) ;
353+ const endPixel = this . _interpolateX ( clampedEnd ) ;
354+ const span = endPixel - startPixel ;
355+ if ( isFinite ( span ) && Math . abs ( span ) > 1e-9 ) {
356+ const rawScale = width / span ;
357+ const minScale = 0.05 ; // must match zoomExtent minimum set at start of update()
358+ const maxScale = this . maxZoom ( ) ;
359+ const targetScale = Math . max ( minScale , Math . min ( maxScale , rawScale ) ) ;
360+
361+ if ( targetScale > 0 && isFinite ( targetScale ) ) {
362+ const centerPixel = ( startPixel + endPixel ) / 2 ;
363+ const halfViewport = width / ( 2 * targetScale ) ;
364+ const x0 = this . _interpolateX ( visibleStart ) ;
365+ const x1 = this . _interpolateX ( visibleEnd ) ;
366+
367+ let clampedCenter = centerPixel ;
368+ if ( clampedCenter - halfViewport < x0 ) {
369+ clampedCenter = x0 + halfViewport ;
370+ }
371+ if ( clampedCenter + halfViewport > x1 ) {
372+ clampedCenter = x1 - halfViewport ;
373+ }
374+
375+ const translateX = ( width / 2 ) - ( targetScale * clampedCenter ) ;
376+ if ( isFinite ( translateX ) ) {
377+ this . zoomTo ( [ translateX , 0 ] , targetScale ) ;
378+ }
379+ }
380+ }
381+ }
382+ }
334383 }
335384 exit ( domNode , element ) {
336385 this . _tooltip . target ( null ) ;
@@ -442,6 +491,27 @@ export class ReactGantt extends SVGZoomWidget {
442491 public _transform = { k : 1 , x : 0 , y : 0 } ;
443492 zoomed ( transform ) {
444493 this . _transform = transform ;
494+ // store current visible range for zoom preservation
495+ if ( this . _interpolateX && typeof this . _interpolateX . invert === "function" ) {
496+ const width = this . width ( ) ;
497+ if ( width > 0 && isFinite ( transform . k ) && transform . k !== 0 ) {
498+ const startPixel = ( 0 - transform . x ) / transform . k ;
499+ const endPixel = ( width - transform . x ) / transform . k ;
500+ let visibleStart = this . _interpolateX . invert ( startPixel ) ;
501+ let visibleEnd = this . _interpolateX . invert ( endPixel ) ;
502+ if ( isFinite ( visibleStart ) && isFinite ( visibleEnd ) ) {
503+ if ( visibleStart > visibleEnd ) {
504+ const tmp = visibleStart ;
505+ visibleStart = visibleEnd ;
506+ visibleEnd = tmp ;
507+ }
508+ this . _prevZoomState = {
509+ visibleStart,
510+ visibleEnd
511+ } ;
512+ }
513+ }
514+ }
445515 switch ( this . renderMode ( ) ) {
446516 case "scale-all" :
447517 this . _zoomScale = transform . k ;
@@ -667,6 +737,8 @@ export interface ReactGantt {
667737 fitWidthToContent ( _ : boolean ) : this;
668738 fitHeightToContent ( ) : boolean ;
669739 fitHeightToContent ( _ : boolean ) : this;
740+ preserveZoom ( ) : boolean ;
741+ preserveZoom ( _ : boolean ) : this;
670742 evenSeriesBackground ( ) : string ;
671743 evenSeriesBackground ( _ : string ) : this;
672744 oddSeriesBackground ( ) : string ;
@@ -675,6 +747,7 @@ export interface ReactGantt {
675747
676748ReactGantt . prototype . publish ( "fitWidthToContent" , false , "boolean" , "If true, resize will simply reapply the bounding box width" ) ;
677749ReactGantt . prototype . publish ( "fitHeightToContent" , false , "boolean" , "If true, resize will simply reapply the bounding box height" ) ;
750+ ReactGantt . prototype . publish ( "preserveZoom" , false , "boolean" , "If true, maintain zoom level when data is updated" ) ;
678751ReactGantt . prototype . publish ( "titleColumn" , null , "string" , "Column name to for the title" ) ;
679752ReactGantt . prototype . publish ( "startDateColumn" , null , "string" , "Column name to for the start date" ) ;
680753ReactGantt . prototype . publish ( "endDateColumn" , null , "string" , "Column name to for the end date" ) ;
0 commit comments