Skip to content

Instantly share code, notes, and snippets.

@chaudharydeepanshu
Last active September 12, 2024 22:21
Show Gist options
  • Save chaudharydeepanshu/2a2e815471eb9738ab08c86cf3fc616f to your computer and use it in GitHub Desktop.
Save chaudharydeepanshu/2a2e815471eb9738ab08c86cf3fc616f to your computer and use it in GitHub Desktop.
A gist with the core logic to create a calendar events layout.

Algorithm Steps

  1. Sort Events by Duration: Arrange events in descending order based on their duration (end - start).

  2. Assign Columns: Place each event into the leftmost available column where it does not overlap with existing events in that column.

  3. Determine Overlaps: For each event, identify overlapping events to the left and right. Store this information for width calculations.

  4. Calculate Event Widths:

    • For events in the leftmost column, assign widths based on overlapping events to the right.
    • For subsequent columns, adjust widths considering the available space and overlapping events from the left.

Input:

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}
];

Output:

List<Map<String, dynamic>> events = [
  {'id': 10059, 'start': 2, 'end': 10, 'columnNum': 1, 'leftOverlappingIds': [], 'rightOverlappingIds': [12576, 18937, 15169], 'earliestStart': 1.8, 'width': 33.333333333333336},
  {'id': 12576, 'start': 1.8, 'end': 4.5, 'columnNum': 2, 'leftOverlappingIds': [10059], 'rightOverlappingIds': [15169], 'earliestStart': 1.8, 'width': 33.33333333333333},
  {'id': 18937, 'start': 5, 'end': 6, 'columnNum': 2, 'leftOverlappingIds': [10059], 'rightOverlappingIds': [], 'earliestStart': 2, 'width': 66.66666666666666},
  {'id': 15169, 'start': 2.4, 'end': 3, 'columnNum': 3, 'leftOverlappingIds': [10059, 12576], 'rightOverlappingIds': [], 'earliestStart': 1.8, 'width': 33.33333333333334}
];
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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment