|
void main() { |
|
double totalWidth = 100.0; |
|
List<Map<String, dynamic>> events = [ |
|
{'id': 12576, 'start': 1.8, 'end': 4.5}, |
|
{'id': 15169, 'start': 2.4, 'end': 3}, |
|
{'id': 10059, 'start': 2, 'end': 10}, |
|
{'id': 18937, 'start': 5, 'end': 6} |
|
]; |
|
|
|
// Call the function and display the output |
|
List<Map<String, dynamic>> result = |
|
processEvents(totalWidth: totalWidth, events: events); |
|
result.forEach((event) { |
|
print(event); |
|
}); |
|
} |
|
|
|
List<Map<String, dynamic>> processEvents({ |
|
double? totalWidth, |
|
double minimumWidthOfEachEvent = |
|
25, // minimumWidthOfEachEvent is only used if totalWidth is not provided. |
|
int? |
|
allowXColumnsOnlyToUseSpace, // If provided only x number of columns events from left to right can use the available space |
|
required List<Map<String, dynamic>> events, |
|
}) { |
|
// Step 1: Sort Events by Duration |
|
// Sort events based on the duration (end - start) in descending order. |
|
events.sort( |
|
(a, b) => (b['end']! - b['start']!).compareTo(a['end']! - a['start']!), |
|
); |
|
|
|
// Helper function to check if two events overlap |
|
bool isOverlapping(Map<String, dynamic> a, Map<String, dynamic> b) { |
|
return a['id'] != b['id'] && |
|
a['start']! < b['end']! && |
|
a['end']! > b['start']!; |
|
} |
|
|
|
// Step 2: Assign Columns |
|
// Create a list to store events organized by columns. |
|
List<List<Map<String, dynamic>>> columns = []; |
|
for (var event in events) { |
|
bool placed = false; |
|
// Try placing the event in the existing columns |
|
for (int i = 0; i < columns.length; i++) { |
|
bool canPlace = true; |
|
// Check for overlaps with events in the current column |
|
for (var e in columns[i]) { |
|
if (isOverlapping(event, e)) { |
|
canPlace = false; |
|
break; |
|
} |
|
} |
|
// If no overlap, assign the event to this column |
|
if (canPlace) { |
|
columns[i].add(event..['columnNum'] = i + 1); |
|
placed = true; |
|
break; |
|
} |
|
} |
|
// If the event couldn't be placed in existing columns, create a new column |
|
if (!placed) { |
|
columns.add([event..['columnNum'] = columns.length + 1]); |
|
} |
|
} |
|
|
|
// Step 2a: Sorting and Assigning Order to column events. |
|
for (var column in columns) { |
|
// Sort events in the column by end time |
|
column.sort((a, b) => a['end'].compareTo(b['end'])); |
|
|
|
// Assign position within the column |
|
for (int i = 0; i < column.length; i++) { |
|
if (i == 0) { |
|
column[i]['endOfPastEventInColumn'] = 0.0; |
|
column[i]['eventNumInColumn'] = 1; |
|
} else { |
|
column[i]['endOfPastEventInColumn'] = column[i - 1]['end']!; |
|
column[i]['eventNumInColumn'] = i + 1; |
|
} |
|
} |
|
} |
|
|
|
// Step 3: Determine Overlaps |
|
// Map to store overlaps for each event |
|
Map<dynamic, Map<String, List<Map<String, dynamic>>>> eventOverlaps = {}; |
|
for (var event in events) { |
|
List<Map<String, dynamic>> leftOverlaps = []; |
|
List<Map<String, dynamic>> rightOverlaps = []; |
|
|
|
List<dynamic> leftOverlappingIds = []; |
|
List<dynamic> rightOverlappingIds = []; |
|
|
|
// Initialize unaccountedRightOverlapsStart with the event's start value. |
|
// This is used to store the earliest start value for unaccounted right overlaps. |
|
// Unaccounted means no right overlap can be used by other events of this column. |
|
double unaccountedRightOverlapsStart = event['start']!; |
|
List<dynamic> unaccountedRightOverlapsEvents = []; |
|
double endOfPastEventInColumn = event['endOfPastEventInColumn']!; |
|
|
|
// Identify overlaps to the left and right of each event |
|
for (var otherEvent in events) { |
|
if (isOverlapping(event, otherEvent)) { |
|
if (otherEvent['columnNum']! < event['columnNum']!) { |
|
leftOverlaps.add(otherEvent); |
|
leftOverlappingIds.add(otherEvent['id'] as int); |
|
} else { |
|
rightOverlaps.add(otherEvent); |
|
rightOverlappingIds.add(otherEvent['id'] as int); |
|
|
|
// Update unaccountedRightOverlapsStart if the right overlapping event has a lower start value. |
|
if (otherEvent['start']! < unaccountedRightOverlapsStart && |
|
otherEvent['start']! > endOfPastEventInColumn) { |
|
unaccountedRightOverlapsEvents.add(otherEvent['id']!); |
|
unaccountedRightOverlapsStart = otherEvent['start']!; |
|
} else if (otherEvent['start']! >= event['start']! && |
|
otherEvent['start']! < event['end']! && |
|
otherEvent['end']! > event['start']!) { |
|
unaccountedRightOverlapsEvents.add(otherEvent['id']!); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Store the overlaps information |
|
eventOverlaps[event['id']!] = { |
|
'left': leftOverlaps, |
|
'right': rightOverlaps, |
|
}; |
|
|
|
// Add the overlapping event IDs to the event map |
|
event['leftOverlappingIds'] = leftOverlappingIds; |
|
event['rightOverlappingIds'] = rightOverlappingIds; |
|
|
|
// Store the unaccountedRightOverlapsStart value. |
|
event['unaccountedRightOverlapsStart'] = unaccountedRightOverlapsStart; |
|
event['unaccountedRightOverlapsEvents'] = unaccountedRightOverlapsEvents; |
|
} |
|
|
|
// Total width to be used |
|
double calculatedTotalWidth = |
|
totalWidth ?? (columns.length * minimumWidthOfEachEvent); |
|
|
|
// Step 4: Calculate Event Widths |
|
// Calculate widths based on column assignment and overlaps |
|
for (var i = 0; i < columns.length; i++) { |
|
List<Map<String, dynamic>> column = columns[i]; |
|
for (var event in column) { |
|
if (allowXColumnsOnlyToUseSpace != null && |
|
event['columnNum'] > allowXColumnsOnlyToUseSpace) { |
|
// If columnNum of event after allowXColumnsOnlyToUseSpace use 0 width. |
|
event['width'] = 0.0; |
|
event['spaceEventAwayFromLeft'] = calculatedTotalWidth; |
|
} else { |
|
List<Map<String, dynamic>> leftOverlaps = |
|
eventOverlaps[event['id']!]!['left']!; |
|
List<Map<String, dynamic>> rightOverlaps = |
|
eventOverlaps[event['id']!]!['right']!; |
|
|
|
// Left overlapping event closest to event column. |
|
// Here if in closest left overlap column has multiple event then |
|
// we will consider the max width event out of those events of the column. |
|
late Map<String, dynamic> maxColumnEvent; |
|
int maxColumnNumber = -1; |
|
double maxColumnWidth = -1.0; |
|
for (var leftEvent in leftOverlaps) { |
|
int column = leftEvent['columnNum']!; |
|
double width = |
|
leftEvent['width']! + leftEvent['spaceEventAwayFromLeft']!; |
|
if (column > maxColumnNumber) { |
|
// New greatest column number found |
|
maxColumnNumber = column; |
|
maxColumnWidth = width; |
|
maxColumnEvent = leftEvent; |
|
} else if (column == maxColumnNumber) { |
|
// Same column number as the current maximum. |
|
if (width > maxColumnWidth) { |
|
maxColumnWidth = width; |
|
maxColumnEvent = leftEvent; |
|
} |
|
} |
|
} |
|
|
|
// Calculate space taken by overlapping events to the left. |
|
double spaceTakenOnLeftOfEvent = maxColumnNumber == -1 |
|
? 0 |
|
: maxColumnEvent['width']! + |
|
maxColumnEvent['spaceEventAwayFromLeft']!; |
|
|
|
event['spaceEventAwayFromLeft'] = spaceTakenOnLeftOfEvent; |
|
|
|
// Calculate available space to the right of the event |
|
double availableSpaceToRightOfEvent = |
|
calculatedTotalWidth - spaceTakenOnLeftOfEvent; |
|
|
|
// Determine unique columns of overlapping events to the right. |
|
// And discard right overlaps having columnNum greater than allowXColumnsOnlyToUseSpace. |
|
Set<int> uniqueColumns = {}; |
|
for (var rightEvent in rightOverlaps) { |
|
if (allowXColumnsOnlyToUseSpace == null || |
|
rightEvent['columnNum'] <= allowXColumnsOnlyToUseSpace) { |
|
uniqueColumns.add(rightEvent['columnNum']!.toInt()); |
|
} |
|
} |
|
|
|
int rightOverlapsFromUniqueColumns = uniqueColumns.length; |
|
|
|
// Compute the width of the event |
|
double eventWidth = |
|
availableSpaceToRightOfEvent / (rightOverlapsFromUniqueColumns + 1); |
|
|
|
event['width'] = eventWidth; |
|
} |
|
} |
|
} |
|
|
|
// Step 5: Expansion of events to the right which got left out due to |
|
// next column overlapping event having dependency on the column other event for |
|
// width calculation. |
|
// Ex: In column 1 a event with width 45px and a event with width 30px. In column 2 |
|
// a event is a right overlap of both events of column 1. Which will make the column 2 |
|
// event width 45px. So, this leaves the column 1 event with 30px width space for expansion. |
|
// But use column 2 overlap event with the lowest event[spaceEventAwayFromLeft]. |
|
for (var i = 0; i < columns.length; i++) { |
|
List<Map<String, dynamic>> column = columns[i]; |
|
for (var event in column) { |
|
// Get all right overlaps of the event. |
|
List<Map<String, dynamic>> rightOverlaps = |
|
eventOverlaps[event['id']!]!['right']!; |
|
|
|
// Find the right overlap with the lowest spaceEventAwayFromLeft. |
|
double? lowestSpaceEventAwayFromLeft; |
|
for (var rightEvent in rightOverlaps) { |
|
if (lowestSpaceEventAwayFromLeft == null || |
|
rightEvent['spaceEventAwayFromLeft']! < |
|
lowestSpaceEventAwayFromLeft) { |
|
lowestSpaceEventAwayFromLeft = rightEvent['spaceEventAwayFromLeft']!; |
|
} |
|
} |
|
|
|
// Add the amount to the width which the event is away from closest right overlap. |
|
if (lowestSpaceEventAwayFromLeft != null) { |
|
double spaceToAddOnWidthOfEvent = lowestSpaceEventAwayFromLeft - |
|
(event['spaceEventAwayFromLeft']! + event['width']!); |
|
event['width'] = event['width']! + spaceToAddOnWidthOfEvent; |
|
} |
|
} |
|
} |
|
|
|
return events; |
|
} |