Skip to content

Commit 50e905b

Browse files
committed
improved frontend issue bar positioning of the roadmap
1 parent 075b599 commit 50e905b

1 file changed

Lines changed: 33 additions & 50 deletions

File tree

src/main/frontend/src/app/pages/release-roadmap/milestone-row/milestone-row.component.ts

Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
22
import { CommonModule, DatePipe } from '@angular/common';
33
import { IssueBarComponent } from '../issue-bar/issue-bar.component';
44
import { Milestone } from '../../../services/milestone.service';
5-
import { Issue, IssuePriority } from '../../../services/issue.service';
5+
import { Issue } from '../../../services/issue.service';
66
import { GitHubStates } from '../../../app.service';
77

88
interface 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

Comments
 (0)