@@ -2,7 +2,7 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
22import { CommonModule , DatePipe } from '@angular/common' ;
33import { IssueBarComponent } from '../issue-bar/issue-bar.component' ;
44import { Milestone } from '../../../services/milestone.service' ;
5- import { Issue , IssuePriority } from '../../../services/issue.service' ;
5+ import { Issue } from '../../../services/issue.service' ;
66import { GitHubStates } from '../../../app.service' ;
77
88interface PositionedIssue {
@@ -113,11 +113,6 @@ export class MilestoneRowComponent implements OnChanges {
113113 }
114114 }
115115
116- for ( const issues of quarterMap . values ( ) ) {
117- issues . open = this . getSortedIssues ( issues . open ) ;
118- issues . closed = this . getSortedIssues ( issues . closed ) ;
119- }
120-
121116 return quarterMap ;
122117 }
123118
@@ -177,8 +172,7 @@ export class MilestoneRowComponent implements OnChanges {
177172 return { positionedIssues : [ ] , trackCount : 0 } ;
178173 }
179174
180- const trackCount = this . estimateTrackCount ( issues , window ) ;
181- const issuesByTrack = this . distributeIssuesRoundRobin ( issues , trackCount ) ;
175+ const issuesByTrack = this . distributeIssuesWithBinPacking ( issues , window ) ;
182176 const positionedIssues : PositionedIssue [ ] = [ ] ;
183177
184178 for ( const [ trackIndex , trackIssues ] of issuesByTrack . entries ( ) ) {
@@ -188,6 +182,7 @@ export class MilestoneRowComponent implements OnChanges {
188182 ( sum , issue ) => sum + this . getIssueDurationMsWithMinWidth ( issue ) ,
189183 0 ,
190184 ) ;
185+
191186 const totalWhitespace = window . end - window . start - totalIssueDuration ;
192187 const gapSize = totalWhitespace > 0 ? totalWhitespace / ( trackIssues . length + 1 ) : 0 ;
193188 let cursor = window . start + gapSize ;
@@ -214,44 +209,42 @@ export class MilestoneRowComponent implements OnChanges {
214209 } ;
215210 }
216211
217- private estimateTrackCount ( issues : Issue [ ] , window : PlanningWindow ) : number {
218- const windowDurationMs = window . end - window . start ;
219- if ( windowDurationMs <= 0 ) return issues . length || 1 ;
220-
221- const totalDurationWithGaps = issues . reduce (
222- ( sum , issue ) => sum + this . getIssueDurationMsWithMinWidth ( issue ) + this . GAP_MS ,
223- - this . GAP_MS ,
224- ) ;
225-
226- return Math . max ( 1 , Math . ceil ( totalDurationWithGaps / windowDurationMs ) ) ;
227- }
228-
229- private distributeIssuesRoundRobin ( issues : Issue [ ] , trackCount : number ) : Map < number , Issue [ ] > {
212+ private distributeIssuesWithBinPacking ( issues : Issue [ ] , window : PlanningWindow ) : Map < number , Issue [ ] > {
230213 const issuesByTrack = new Map < number , Issue [ ] > ( ) ;
231- if ( trackCount === 0 ) return issuesByTrack ;
214+ const windowDurationMs = window . end - window . start ;
215+ const trackCapacities = new Map < number , number > ( ) ;
216+ let currentTrack = 0 ;
217+
218+ for ( const issue of issues ) {
219+ const issueDuration = this . getIssueDurationMsWithMinWidth ( issue ) ;
220+ const spaceNeeded = issueDuration + this . GAP_MS ;
221+
222+ let placed = false ;
223+ for ( let trackIndex = 0 ; trackIndex <= currentTrack ; trackIndex ++ ) {
224+ const trackUsed = trackCapacities . get ( trackIndex ) ?? 0 ;
225+ const trackRemaining = windowDurationMs - trackUsed ;
226+
227+ if ( trackRemaining >= spaceNeeded ) {
228+ if ( ! issuesByTrack . has ( trackIndex ) ) {
229+ issuesByTrack . set ( trackIndex , [ ] ) ;
230+ }
231+ issuesByTrack . get ( trackIndex ) ! . push ( issue ) ;
232+ trackCapacities . set ( trackIndex , trackUsed + spaceNeeded ) ;
233+ placed = true ;
234+ break ;
235+ }
236+ }
232237
233- for ( let index = 0 ; index < trackCount ; index ++ ) issuesByTrack . set ( index , [ ] ) ;
234- for ( const [ index , issue ] of issues . entries ( ) ) issuesByTrack . get ( index % trackCount ) ! . push ( issue ) ;
238+ if ( ! placed ) {
239+ currentTrack ++ ;
240+ issuesByTrack . set ( currentTrack , [ issue ] ) ;
241+ trackCapacities . set ( currentTrack , spaceNeeded ) ;
242+ }
243+ }
235244
236245 return issuesByTrack ;
237246 }
238247
239- private getSortedIssues ( issues : Issue [ ] ) : Issue [ ] {
240- const priorityOrder : Record < string , number > = { critical : 1 , high : 2 , medium : 3 , low : 4 , no : 5 } ;
241-
242- return [ ...issues ] . sort ( ( a , b ) => {
243- const priorityA = priorityOrder [ this . getPriorityKey ( a . issuePriority ) ] ?? 5 ;
244- const priorityB = priorityOrder [ this . getPriorityKey ( b . issuePriority ) ] ?? 5 ;
245- if ( priorityA !== priorityB ) return priorityA - priorityB ;
246-
247- const pointsA = a . points ?? this . DEFAULT_POINTS ;
248- const pointsB = b . points ?? this . DEFAULT_POINTS ;
249- if ( pointsA !== pointsB ) return pointsB - pointsA ;
250-
251- return b . number - a . number ;
252- } ) ;
253- }
254-
255248 private calculateBarPosition ( startDate : Date , durationDays : number ) : Record < string , string > {
256249 const startDays = ( startDate . getTime ( ) - this . timelineStartDate . getTime ( ) ) / ( 1000 * 3600 * 24 ) ;
257250 const leftPercentage = ( startDays / this . totalTimelineDays ) * 100 ;
@@ -290,14 +283,4 @@ export class MilestoneRowComponent implements OnChanges {
290283 const total = this . milestone . openIssueCount + this . milestone . closedIssueCount ;
291284 this . progressPercentage = total === 0 ? 0 : Math . round ( ( this . milestone . closedIssueCount / total ) * 100 ) ;
292285 }
293-
294- private getPriorityKey ( priority : IssuePriority | undefined | null ) : string {
295- if ( ! priority ?. name ) return 'no' ;
296- const lowerCaseName = priority . name . toLowerCase ( ) ;
297- const keys = [ 'critical' , 'high' , 'medium' , 'low' ] ;
298- for ( const key of keys ) {
299- if ( lowerCaseName . includes ( key ) ) return key ;
300- }
301- return 'no' ;
302- }
303286}
0 commit comments