@@ -356,10 +356,10 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
356356
357357 // Track min start and max end from axisDomain
358358 if ( axisDomain && axisDomain . type === 'range' ) {
359- const start = Number ( axisDomain . start ) ;
360- const end = Number ( axisDomain . end ) ;
361- if ( start < minStart ) minStart = start ;
362- if ( end > maxEnd ) maxEnd = end ;
359+ const currentStart = Number ( axisDomain . start ) ;
360+ const currentEnd = Number ( axisDomain . end ) ;
361+ if ( currentStart < minStart ) minStart = currentStart ;
362+ if ( currentEnd > maxEnd ) maxEnd = currentEnd ;
363363 }
364364
365365 // Check if all series have same label/unit/dataType
@@ -375,9 +375,6 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
375375 }
376376 } ) ;
377377
378- // Calculate union range
379- const unionRange = maxEnd - minStart ;
380-
381378 // Default to 'time' if datatype differs across series
382379 const finalDataType = commonDataType || 'time' ;
383380
@@ -522,7 +519,6 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
522519 this . updateTimeline ( ) ;
523520 } , 0 ) ;
524521 }
525-
526522 }
527523
528524 componentWillUnmount ( ) : void {
@@ -820,38 +816,102 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
820816
821817 const { start, end } = this . getTimelineRange ( ) ;
822818
823- const scale = d3 . scaleLinear ( )
824- . domain ( [ start , end ] )
825- . range ( [ 0 , chartWidth ] ) ;
819+ const scale = d3 . scaleLinear ( ) . domain ( [ start , end ] ) . range ( [ 0 , chartWidth ] ) ;
820+
821+ const axis = d3 . axisBottom ( scale ) . tickFormat ( d => this . formatTimelineValue ( Number ( d ) ) ) ;
826822
827- const axis = d3 . axisBottom ( scale )
828- . tickFormat ( d => this . formatTimelineValue ( Number ( d ) ) ) ;
823+ const axisGroup = svg . append ( 'g' ) . attr ( 'transform' , `translate(0, 0)` ) . call ( axis ) ;
829824
830- const axisGroup = svg . append ( 'g' )
831- . attr ( 'transform' , `translate(0, 0)` )
832- . call ( axis ) ;
825+ // Handle label overlaps
826+ this . handleLabelOverlaps ( axisGroup ) ;
833827
834828 // Add minor ticks for range items
835829 this . addMinorTicks ( svg , scale , chartWidth ) ;
836830 }
837831
838- private addMinorTicks ( svg : d3 . Selection < SVGSVGElement , unknown , null , undefined > , scale : d3 . ScaleLinear < number , number > , chartWidth : number ) : void {
832+ private handleLabelOverlaps ( axisGroup : d3 . Selection < SVGGElement , unknown , null , undefined > ) : void {
833+ const labels = axisGroup . selectAll ( '.tick text' ) ;
834+ const labelNodes = labels . nodes ( ) as SVGTextElement [ ] ;
835+
836+ for ( let i = labelNodes . length - 1 ; i >= 0 ; i -- ) {
837+ const current = labelNodes [ i ] ;
838+ if ( ! current . textContent || current . textContent . trim ( ) === '' ) continue ;
839+
840+ const currentBBox = current . getBoundingClientRect ( ) ;
841+
842+ let overlaps = false ;
843+ let maxLabelWidth = currentBBox . width ;
844+
845+ // Check overlap with adjacent labels and calculate available space
846+ for ( let j = i - 1 ; j >= 0 ; j -- ) {
847+ const other = labelNodes [ j ] ;
848+ if ( ! other . textContent || other . textContent . trim ( ) === '' ) continue ;
849+
850+ const otherBBox = other . getBoundingClientRect ( ) ;
851+
852+ // Calculate available space between labels (with 5px margin)
853+ const availableSpace = Math . abs ( currentBBox . x - ( otherBBox . x + otherBBox . width ) ) - 5 ;
854+ maxLabelWidth = Math . min ( maxLabelWidth , availableSpace ) ;
855+
856+ // Check if bounding boxes overlap (with 5px margin)
857+ if (
858+ currentBBox . x < otherBBox . x + otherBBox . width + 5 &&
859+ currentBBox . x + currentBBox . width + 5 > otherBBox . x
860+ ) {
861+ overlaps = true ;
862+ }
863+ }
864+
865+ if ( overlaps && maxLabelWidth > 0 ) {
866+ const originalText = current . textContent || '' ;
867+ const truncated = this . truncateWithEllipsis ( originalText , maxLabelWidth ) ;
868+
869+ if ( truncated . length <= 3 ) {
870+ current . style . display = 'none' ;
871+ } else {
872+ current . textContent = truncated ;
873+ }
874+ }
875+ }
876+ }
877+
878+ private truncateWithEllipsis ( text : string , maxWidth : number = 80 ) : string {
879+ const canvas = document . createElement ( 'canvas' ) ;
880+ const ctx = canvas . getContext ( '2d' ) ! ;
881+ ctx . font = '12px sans-serif' ;
882+
883+ if ( ctx . measureText ( text ) . width <= maxWidth ) return text ;
884+
885+ const ellipsis = '…' ;
886+ let truncated = text ;
887+ while ( truncated . length > 0 && ctx . measureText ( truncated + ellipsis ) . width > maxWidth ) {
888+ truncated = truncated . slice ( 0 , - 1 ) ;
889+ }
890+
891+ return truncated + ellipsis ;
892+ }
893+
894+ private addMinorTicks (
895+ svg : d3 . Selection < SVGSVGElement , unknown , null , undefined > ,
896+ scale : d3 . ScaleLinear < number , number > ,
897+ chartWidth : number
898+ ) : void {
839899 const labels = this . state . xyData . labels ;
840900 if ( ! labels . length ) return ;
841901
842902 // For each range item, add minor ticks at start and end
843- labels . forEach ( ( label ) => {
903+ labels . forEach ( label => {
844904 if ( label . includes ( '[' ) && label . includes ( ',' ) ) {
845905 // Parse range format: "[start unit, end unit]"
846906 const match = label . match ( / \[ ( .* ?) , \s * ( .* ?) \] / ) ;
847907 if ( match ) {
848908 const startVal = parseFloat ( match [ 1 ] ) ;
849909 const endVal = parseFloat ( match [ 2 ] ) ;
850-
910+
851911 if ( ! isNaN ( startVal ) && ! isNaN ( endVal ) ) {
852912 const startX = scale ( startVal ) ;
853913 const endX = scale ( endVal ) ;
854-
914+
855915 // Add minor ticks
856916 if ( startX >= 0 && startX <= chartWidth ) {
857917 svg . append ( 'line' )
@@ -862,7 +922,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
862922 . attr ( 'stroke' , '#666' )
863923 . attr ( 'stroke-width' , 0.5 ) ;
864924 }
865-
925+
866926 if ( endX >= 0 && endX <= chartWidth ) {
867927 svg . append ( 'line' )
868928 . attr ( 'x1' , endX )
@@ -880,12 +940,12 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
880940
881941 private getTimelineRange ( ) : { start : number ; end : number } {
882942 const labels = this . state . xyData . labels ;
883-
943+
884944 // If we have range labels, extract the actual range from the data
885945 if ( labels . length > 0 && labels [ 0 ] . includes ( '[' ) && labels [ 0 ] . includes ( ',' ) ) {
886946 let minStart = Number . MAX_SAFE_INTEGER ;
887947 let maxEnd = Number . MIN_SAFE_INTEGER ;
888-
948+
889949 labels . forEach ( label => {
890950 const match = label . match ( / \[ ( .* ?) , \s * ( .* ?) \] / ) ;
891951 if ( match ) {
@@ -897,12 +957,12 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
897957 }
898958 }
899959 } ) ;
900-
960+
901961 if ( minStart !== Number . MAX_SAFE_INTEGER && maxEnd !== Number . MIN_SAFE_INTEGER ) {
902962 return { start : minStart , end : maxEnd } ;
903963 }
904964 }
905-
965+
906966 // Fallback to view/range based on timeline unit type
907967 switch ( this . state . timelineUnitType ) {
908968 case 'time' :
@@ -1016,7 +1076,11 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
10161076 onContextMenu = { e => e . preventDefault ( ) }
10171077 onMouseLeave = { e => this . onMouseLeave ( e ) }
10181078 onMouseDown = { e => this . onMouseDown ( e ) }
1019- style = { { height : parseInt ( String ( this . props . style . height ) ) - this . timelineHeight , position : 'relative' , cursor : this . state . cursor } }
1079+ style = { {
1080+ height : parseInt ( String ( this . props . style . height ) ) - this . timelineHeight ,
1081+ position : 'relative' ,
1082+ cursor : this . state . cursor
1083+ } }
10201084 ref = { this . divRef }
10211085 >
10221086 { this . chooseReactChart ( ) }
@@ -1031,17 +1095,19 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
10311095 < div style = { { position : 'relative' } } >
10321096 { this . renderTimeline ( ) }
10331097 { this . state . xAxisTitle && (
1034- < div style = { {
1035- position : 'absolute' ,
1036- top : '50%' ,
1037- left : '50%' ,
1038- transform : 'translate(-50%, -50%)' ,
1039- textAlign : 'center' ,
1040- fontSize : '12px' ,
1041- color : 'rgba(102, 102, 102, 0.5)' ,
1042- pointerEvents : 'none' ,
1043- zIndex : 10
1044- } } >
1098+ < div
1099+ style = { {
1100+ position : 'absolute' ,
1101+ top : '50%' ,
1102+ left : '50%' ,
1103+ transform : 'translate(-50%, -50%)' ,
1104+ textAlign : 'center' ,
1105+ fontSize : '12px' ,
1106+ color : 'rgba(102, 102, 102, 0.5)' ,
1107+ pointerEvents : 'none' ,
1108+ zIndex : 10
1109+ } }
1110+ >
10451111 { this . state . xAxisTitle }
10461112 </ div >
10471113 ) }
0 commit comments