Stacking and Sharecolumn WIP
This commit is contained in:
parent
c788a1695e
commit
6b8c5d4673
7 changed files with 763 additions and 51 deletions
|
|
@ -34,6 +34,9 @@ interface GridSettings {
|
|||
fitToWidth: boolean;
|
||||
scrollToHour: number | null;
|
||||
|
||||
// Event grouping settings
|
||||
gridStartThresholdMinutes: number; // ±N minutes for events to share grid columns
|
||||
|
||||
// Display options
|
||||
showCurrentTime: boolean;
|
||||
showWorkHours: boolean;
|
||||
|
|
@ -132,6 +135,7 @@ export class CalendarConfig {
|
|||
workStartHour: 8,
|
||||
workEndHour: 17,
|
||||
snapInterval: 15,
|
||||
gridStartThresholdMinutes: 30, // Events starting within ±15 min share grid columns
|
||||
showCurrentTime: true,
|
||||
showWorkHours: true,
|
||||
fitToWidth: false,
|
||||
|
|
|
|||
|
|
@ -1922,6 +1922,18 @@
|
|||
"duration": 120,
|
||||
"color": "#f44336"
|
||||
}
|
||||
},{
|
||||
"id": "1481",
|
||||
"title": "Kvartal Afslutning 2",
|
||||
"start": "2025-09-30T11:20:00Z",
|
||||
"end": "2025-09-30T13:00:00Z",
|
||||
"type": "milestone",
|
||||
"allDay": false,
|
||||
"syncStatus": "synced",
|
||||
"metadata": {
|
||||
"duration": 120,
|
||||
"color": "#f44336"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "149",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface GridGroupLayout {
|
|||
events: CalendarEvent[];
|
||||
stackLevel: number;
|
||||
position: { top: number };
|
||||
columns: CalendarEvent[][]; // Events grouped by column (events in same array share a column)
|
||||
}
|
||||
|
||||
export interface StackedEventLayout {
|
||||
|
|
@ -60,11 +61,13 @@ export class EventLayoutCoordinator {
|
|||
const gridStackLevel = this.calculateGridGroupStackLevel(group, columnEvents, allStackLinks);
|
||||
const earliestEvent = group.events[0];
|
||||
const position = PositionUtils.calculateEventPosition(earliestEvent.start, earliestEvent.end);
|
||||
const columns = this.allocateColumns(group.events);
|
||||
|
||||
gridGroupLayouts.push({
|
||||
events: group.events,
|
||||
stackLevel: gridStackLevel,
|
||||
position: { top: position.top + 1 }
|
||||
position: { top: position.top + 1 },
|
||||
columns
|
||||
});
|
||||
|
||||
group.events.forEach(e => renderedEventIds.add(e.id));
|
||||
|
|
@ -119,4 +122,45 @@ export class EventLayoutCoordinator {
|
|||
// Grid group should be one level above the highest overlapping event
|
||||
return maxOverlappingLevel + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate events to columns within a grid group
|
||||
*
|
||||
* Events that don't overlap can share the same column.
|
||||
* Uses a greedy algorithm to minimize the number of columns.
|
||||
*
|
||||
* @param events - Events in the grid group (should already be sorted by start time)
|
||||
* @returns Array of columns, where each column is an array of events
|
||||
*/
|
||||
private allocateColumns(events: CalendarEvent[]): CalendarEvent[][] {
|
||||
if (events.length === 0) return [];
|
||||
if (events.length === 1) return [[events[0]]];
|
||||
|
||||
const columns: CalendarEvent[][] = [];
|
||||
|
||||
// For each event, try to place it in an existing column where it doesn't overlap
|
||||
for (const event of events) {
|
||||
let placed = false;
|
||||
|
||||
// Try to find a column where this event doesn't overlap with any existing event
|
||||
for (const column of columns) {
|
||||
const hasOverlap = column.some(colEvent =>
|
||||
this.stackManager.doEventsOverlap(event, colEvent)
|
||||
);
|
||||
|
||||
if (!hasOverlap) {
|
||||
column.push(event);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no suitable column found, create a new one
|
||||
if (!placed) {
|
||||
columns.push([event]);
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@
|
|||
* This class handles the creation and maintenance of "stack chains" - doubly-linked
|
||||
* lists of overlapping events stored directly in DOM elements via data attributes.
|
||||
*
|
||||
* Implements 3-phase algorithm for flexbox + nested stacking:
|
||||
* Phase 1: Group events by start time proximity (±15 min threshold)
|
||||
* Phase 2: Decide container type (FLEXBOX vs STACKING)
|
||||
* Phase 3: Handle late arrivals (nested stacking)
|
||||
* Implements 3-phase algorithm for grid + nested stacking:
|
||||
* Phase 1: Group events by start time proximity (configurable threshold)
|
||||
* Phase 2: Decide container type (GRID vs STACKING)
|
||||
* Phase 3: Handle late arrivals (nested stacking - NOT IMPLEMENTED)
|
||||
*
|
||||
* @see STACKING_CONCEPT.md for detailed documentation
|
||||
* @see stacking-visualization.html for visual examples
|
||||
*/
|
||||
|
||||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
import { calendarConfig } from '../core/CalendarConfig';
|
||||
|
||||
export interface StackLink {
|
||||
prev?: string; // Event ID of previous event in stack
|
||||
|
|
@ -28,7 +29,6 @@ export interface EventGroup {
|
|||
}
|
||||
|
||||
export class EventStackManager {
|
||||
private static readonly FLEXBOX_START_THRESHOLD_MINUTES = 15;
|
||||
private static readonly STACK_OFFSET_PX = 15;
|
||||
|
||||
// ============================================
|
||||
|
|
@ -36,22 +36,49 @@ export class EventStackManager {
|
|||
// ============================================
|
||||
|
||||
/**
|
||||
* Group events by start time proximity (±15 min threshold)
|
||||
* Group events by time conflicts (both start-to-start and end-to-start within threshold)
|
||||
*
|
||||
* Events are grouped if:
|
||||
* 1. They start within ±threshold minutes of each other (start-to-start)
|
||||
* 2. One event starts within threshold minutes before another ends (end-to-start conflict)
|
||||
*/
|
||||
public groupEventsByStartTime(events: CalendarEvent[]): EventGroup[] {
|
||||
if (events.length === 0) return [];
|
||||
|
||||
// Get threshold from config
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const thresholdMinutes = gridSettings.gridStartThresholdMinutes;
|
||||
|
||||
// Sort events by start time
|
||||
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
|
||||
const groups: EventGroup[] = [];
|
||||
|
||||
for (const event of sorted) {
|
||||
// Find existing group within threshold
|
||||
// Find existing group that this event conflicts with
|
||||
const existingGroup = groups.find(group => {
|
||||
const groupStart = group.startTime;
|
||||
const diffMinutes = Math.abs(event.start.getTime() - groupStart.getTime()) / (1000 * 60);
|
||||
return diffMinutes <= EventStackManager.FLEXBOX_START_THRESHOLD_MINUTES;
|
||||
// Check if event conflicts with ANY event in the group
|
||||
return group.events.some(groupEvent => {
|
||||
// Start-to-start conflict: events start within threshold
|
||||
const startToStartMinutes = Math.abs(event.start.getTime() - groupEvent.start.getTime()) / (1000 * 60);
|
||||
if (startToStartMinutes <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// End-to-start conflict: event starts within threshold before groupEvent ends
|
||||
const endToStartMinutes = (groupEvent.end.getTime() - event.start.getTime()) / (1000 * 60);
|
||||
if (endToStartMinutes > 0 && endToStartMinutes <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check reverse: groupEvent starts within threshold before event ends
|
||||
const reverseEndToStart = (event.end.getTime() - groupEvent.start.getTime()) / (1000 * 60);
|
||||
if (reverseEndToStart > 0 && reverseEndToStart <= thresholdMinutes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
if (existingGroup) {
|
||||
|
|
@ -76,7 +103,7 @@ export class EventStackManager {
|
|||
/**
|
||||
* Decide container type for a group of events
|
||||
*
|
||||
* Rule: Events starting simultaneously (within ±15 min) should ALWAYS use GRID,
|
||||
* Rule: Events starting simultaneously (within threshold) should ALWAYS use GRID,
|
||||
* even if they overlap each other. This provides better visual indication that
|
||||
* events start at the same time.
|
||||
*/
|
||||
|
|
@ -85,7 +112,7 @@ export class EventStackManager {
|
|||
return 'NONE';
|
||||
}
|
||||
|
||||
// If events are grouped together (start within ±15 min), they should share columns (GRID)
|
||||
// If events are grouped together (start within threshold), they should share columns (GRID)
|
||||
// This is true EVEN if they overlap, because the visual priority is to show
|
||||
// that they start simultaneously.
|
||||
return 'GRID';
|
||||
|
|
|
|||
|
|
@ -206,13 +206,13 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
});
|
||||
}
|
||||
/**
|
||||
* Render events in a grid container (side-by-side)
|
||||
* Render events in a grid container (side-by-side with column sharing)
|
||||
*/
|
||||
private renderGridGroup(gridGroup: GridGroupLayout, eventsLayer: HTMLElement): void {
|
||||
const groupElement = document.createElement('swp-event-group');
|
||||
|
||||
// Add grid column class based on event count
|
||||
const colCount = gridGroup.events.length;
|
||||
// Add grid column class based on number of columns (not events)
|
||||
const colCount = gridGroup.columns.length;
|
||||
groupElement.classList.add(`cols-${colCount}`);
|
||||
|
||||
// Add stack level class for margin-left offset
|
||||
|
|
@ -231,18 +231,34 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
};
|
||||
this.stackManager.applyStackLinkToElement(groupElement, stackLink);
|
||||
|
||||
// Render each event within the grid
|
||||
// Render each column
|
||||
const earliestEvent = gridGroup.events[0];
|
||||
gridGroup.events.forEach(event => {
|
||||
const element = this.renderEventInGrid(event, earliestEvent.start);
|
||||
groupElement.appendChild(element);
|
||||
gridGroup.columns.forEach(columnEvents => {
|
||||
const columnContainer = this.renderGridColumn(columnEvents, earliestEvent.start);
|
||||
groupElement.appendChild(columnContainer);
|
||||
});
|
||||
|
||||
eventsLayer.appendChild(groupElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render event within a grid container (relative positioning)
|
||||
* Render a single column within a grid group
|
||||
* Column may contain multiple events that don't overlap
|
||||
*/
|
||||
private renderGridColumn(columnEvents: CalendarEvent[], containerStart: Date): HTMLElement {
|
||||
const columnContainer = document.createElement('div');
|
||||
columnContainer.style.position = 'relative';
|
||||
|
||||
columnEvents.forEach(event => {
|
||||
const element = this.renderEventInGrid(event, containerStart);
|
||||
columnContainer.appendChild(element);
|
||||
});
|
||||
|
||||
return columnContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render event within a grid container (absolute positioning within column)
|
||||
*/
|
||||
private renderEventInGrid(event: CalendarEvent, containerStart: Date): HTMLElement {
|
||||
const element = SwpEventElement.fromCalendarEvent(event);
|
||||
|
|
@ -250,10 +266,19 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
// Calculate event height
|
||||
const position = this.calculateEventPosition(event);
|
||||
|
||||
// Events in grid are positioned relatively - NO top offset needed
|
||||
// The grid container itself is positioned absolutely with the correct top
|
||||
element.style.position = 'relative';
|
||||
// Calculate relative top offset if event starts after container start
|
||||
// (e.g., if container starts at 07:00 and event starts at 08:15, offset = 75 min)
|
||||
const timeDiffMs = event.start.getTime() - containerStart.getTime();
|
||||
const timeDiffMinutes = timeDiffMs / (1000 * 60);
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const relativeTop = timeDiffMinutes > 0 ? (timeDiffMinutes / 60) * gridSettings.hourHeight : 0;
|
||||
|
||||
// Events in grid columns are positioned absolutely within their column container
|
||||
element.style.position = 'absolute';
|
||||
element.style.top = `${relativeTop}px`;
|
||||
element.style.height = `${position.height - 3}px`;
|
||||
element.style.left = '0';
|
||||
element.style.right = '0';
|
||||
|
||||
return element;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1415,6 +1415,397 @@ Result: One algorithm handles ALL scenarios!</pre>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scenario 8: Edge Case - Exactly 15 min apart with overlap -->
|
||||
<div class="section">
|
||||
<h2>Scenario 8: Edge Case - Events Starting Exactly 15 Minutes Apart (WITH Overlap)</h2>
|
||||
<p><strong>Edge Case:</strong> What happens when events start exactly at the ±15 min threshold AND overlap?</p>
|
||||
|
||||
<p><strong>Events:</strong></p>
|
||||
<ul style="margin: 10px 0 20px 20px;">
|
||||
<li>Event A: 11:00 - 12:00 (1 hour)</li>
|
||||
<li>Event B: 11:15 - 12:30 (1.25 hours)</li>
|
||||
</ul>
|
||||
|
||||
<div class="note">
|
||||
<strong>Analysis:</strong><br>
|
||||
• A starts at 11:00<br>
|
||||
• B starts at 11:15 (diff = 15 min ≤ 15 min) → <strong>Within threshold</strong> ✓<br>
|
||||
• A and B overlap (11:15 - 12:00) → <strong>They DO overlap</strong> ✓<br>
|
||||
• <strong>Visual priority:</strong> Show that they start simultaneously (±15 min)<br>
|
||||
• <strong>Result:</strong> Use GRID (column sharing) even though they overlap
|
||||
</div>
|
||||
|
||||
<div class="comparison">
|
||||
<!-- Wrong: Stacking -->
|
||||
<div class="calendar-column">
|
||||
<div class="column-title">❌ Wrong: Stacking (Hides Simultaneity)</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="time-marker" style="top: 0%">11:00</div>
|
||||
<div class="time-marker" style="top: 33%">11:30</div>
|
||||
<div class="time-marker" style="top: 66%">12:00</div>
|
||||
<div class="time-marker" style="top: 100%">12:30</div>
|
||||
</div>
|
||||
|
||||
<div class="events-container" style="height: 180px;">
|
||||
<!-- Event A: 11:00-12:00 (60 min = 66.7% of 90 min total) -->
|
||||
<div class="event event-a" style="top: 0%; height: 66.7%; left: 2px; right: 2px; z-index: 100;">
|
||||
Event A<br>
|
||||
<span style="font-size: 11px; opacity: 0.8;">11:00-12:00</span>
|
||||
</div>
|
||||
|
||||
<!-- Event B: 11:15-12:30 (75 min = 83.3% of 90 min total, starts at 16.7%) -->
|
||||
<div class="event event-b" style="top: 16.7%; height: 83.3%; left: 17px; right: 2px; z-index: 101; margin-left: 15px;">
|
||||
Event B<br>
|
||||
<span style="font-size: 11px; opacity: 0.8;">11:15-12:30</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="savings wrong">
|
||||
<strong>Problems:</strong><br>
|
||||
• B is offset to the right → looks like it happens AFTER A<br>
|
||||
• Doesn't convey that they start almost simultaneously (15 min apart)<br>
|
||||
• Wastes horizontal space
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: GRID -->
|
||||
<div class="calendar-column">
|
||||
<div class="column-title">✅ Correct: GRID Column Sharing</div>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="time-marker" style="top: 0%">11:00</div>
|
||||
<div class="time-marker" style="top: 33%">11:30</div>
|
||||
<div class="time-marker" style="top: 66%">12:00</div>
|
||||
<div class="time-marker" style="top: 100%">12:30</div>
|
||||
</div>
|
||||
|
||||
<div class="events-container" style="height: 180px;">
|
||||
<!-- Grid container for A and B -->
|
||||
<div style="position: absolute; top: 0%; left: 2px; right: 2px; display: grid; grid-template-columns: 1fr 1fr; gap: 2px; z-index: 100;">
|
||||
<!-- Event A: 11:00-12:00 -->
|
||||
<div class="event event-a" style="position: relative; height: 120px;">
|
||||
Event A<br>
|
||||
<span style="font-size: 11px; opacity: 0.8;">11:00-12:00</span>
|
||||
</div>
|
||||
|
||||
<!-- Event B: 11:15-12:30 (starts 15 min later = 10% offset) -->
|
||||
<div class="event event-b" style="position: relative; height: 150px; top: 10%;">
|
||||
Event B<br>
|
||||
<span style="font-size: 11px; opacity: 0.8;">11:15-12:30</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="savings">
|
||||
<strong>Benefits:</strong><br>
|
||||
• Side-by-side layout shows they're concurrent<br>
|
||||
• Each event gets 50% width<br>
|
||||
• Clear visual: these events start nearly simultaneously (±15 min)<br>
|
||||
• Despite overlapping, simultaneity is visual priority
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Key Rule:</strong> Events starting within ±15 minutes should ALWAYS use GRID (column sharing),
|
||||
even if they overlap. The visual priority is to show that events start <em>simultaneously</em>,
|
||||
not to avoid overlap. Overlap is handled by the grid container having appropriate height.
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<strong>Expected Behavior:</strong>
|
||||
<pre style="background: #f8f8f8; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 13px;">
|
||||
// Phase 1: Group by start time
|
||||
groupEventsByStartTime([A, B])
|
||||
→ Group 1: [A, B] // 15 min apart ≤ threshold
|
||||
|
||||
// Phase 2: Decide container type
|
||||
decideContainerType(Group 1)
|
||||
→ GRID // Always GRID for grouped events, even if overlapping
|
||||
|
||||
// Phase 3: Calculate stack level
|
||||
calculateGridGroupStackLevel(Group 1)
|
||||
→ stackLevel: 0 // No other events to stack above
|
||||
|
||||
// Result:
|
||||
<swp-event-group class="cols-2 stack-level-0" style="top: 0px; margin-left: 0px; z-index: 100;">
|
||||
<swp-event data-event-id="A" style="height: 120px;">Event A</swp-event>
|
||||
<swp-event data-event-id="B" style="height: 150px; top: 10%;">Event B</swp-event>
|
||||
</swp-event-group></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- SCENARIO 9: Grid with Staggered Start Times -->
|
||||
<!-- ============================================ -->
|
||||
<div class="section">
|
||||
<h2>Scenario 9: Grid with Staggered Start Times</h2>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item"><strong>Event A:</strong> 09:00 - 10:00 (1 hour)</div>
|
||||
<div class="legend-item"><strong>Event B:</strong> 09:30 - 10:30 (1 hour, starts 30 min after A)</div>
|
||||
<div class="legend-item"><strong>Event C:</strong> 10:15 - 12:00 (1h 45min, starts 45 min after B)</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Special Case: End-to-Start Conflicts Create Shared Columns</strong><br><br>
|
||||
• Event A: 09:00 - 10:00<br>
|
||||
• Event B: 09:30 - 10:30 (starts 30 min before A ends → conflicts with A)<br>
|
||||
• Event C: 10:15 - 12:00 (starts 15 min before B ends → conflicts with B)<br><br>
|
||||
<strong>Key Rule:</strong> Events share columns (GRID) when they conflict within threshold<br>
|
||||
• Conflict = Event starts within ±threshold minutes of another event's end time<br>
|
||||
• A and B: B starts 30 min before A ends → conflict (≤ 30 min threshold)<br>
|
||||
• B and C: C starts 15 min before B ends → conflict (≤ 30 min threshold)<br>
|
||||
• Therefore: A, B, and C all share columns in a 3-column GRID<br><br>
|
||||
<strong>With threshold = 15 min:</strong> Only A-B conflict (30 min > 15), C is separate → Stack<br>
|
||||
<strong>With threshold = 30 min:</strong> Both A-B and B-C conflict → All 3 share columns in GRID
|
||||
</div>
|
||||
|
||||
<div class="comparison">
|
||||
<!-- Threshold = 15 min -->
|
||||
<div class="calendar-column">
|
||||
<div class="column-title">With Threshold = 15 min</div>
|
||||
<div class="timeline">
|
||||
<div class="time-marker" style="top: 0%;">09:00</div>
|
||||
<div class="time-marker" style="top: 25%;">10:00</div>
|
||||
<div class="time-marker" style="top: 50%;">11:00</div>
|
||||
<div class="time-marker" style="top: 75%;">12:00</div>
|
||||
</div>
|
||||
|
||||
<div class="events-container">
|
||||
<!-- Event A: 09:00-10:00 (stackLevel 0) -->
|
||||
<div class="event event-a" style="top: 0px; left: 2px; right: 2px; height: 60px; margin-left: 0px; z-index: 100;">
|
||||
Event A<br>09:00-10:00
|
||||
</div>
|
||||
|
||||
<!-- Event B: 09:30-10:30 (stackLevel 1, overlaps A) -->
|
||||
<div class="event event-b" style="top: 30px; left: 2px; right: 2px; height: 60px; margin-left: 15px; z-index: 101;">
|
||||
Event B<br>09:30-10:30
|
||||
</div>
|
||||
|
||||
<!-- Event C: 10:15-12:00 (stackLevel 2, overlaps B) -->
|
||||
<div class="event event-c" style="top: 75px; left: 2px; right: 2px; height: 105px; margin-left: 30px; z-index: 102;">
|
||||
Event C<br>10:15-12:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Threshold = 30 min -->
|
||||
<div class="calendar-column">
|
||||
<div class="column-title">With Threshold = 30 min</div>
|
||||
<div class="timeline">
|
||||
<div class="time-marker" style="top: 0%;">09:00</div>
|
||||
<div class="time-marker" style="top: 25%;">10:00</div>
|
||||
<div class="time-marker" style="top: 50%;">11:00</div>
|
||||
<div class="time-marker" style="top: 75%;">12:00</div>
|
||||
</div>
|
||||
|
||||
<div class="events-container">
|
||||
<!-- Grid Group for A, B & C: stackLevel 0 (2 columns, A and C share column 1) -->
|
||||
<div style="position: absolute; top: 0px; left: 2px; right: 2px; margin-left: 0px; z-index: 100; display: grid; grid-template-columns: 1fr 1fr; gap: 2px;">
|
||||
<!-- Column 1: Event A and C (no overlap) -->
|
||||
<div style="position: relative;">
|
||||
<div class="event event-a" style="position: absolute; top: 0px; height: 60px; left: 0; right: 0;">
|
||||
Event A<br>09:00-10:00
|
||||
</div>
|
||||
<div class="event event-c" style="position: absolute; top: 75px; height: 105px; left: 0; right: 0;">
|
||||
Event C<br>10:15-12:00
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column 2: Event B -->
|
||||
<div style="position: relative;">
|
||||
<div class="event event-b" style="position: absolute; top: 30px; height: 60px; left: 0; right: 0;">
|
||||
Event B<br>09:30-10:30
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Stack Analysis</h3>
|
||||
<div class="legend">
|
||||
<p><strong>Threshold = 15 min (Stack):</strong></p>
|
||||
<ul>
|
||||
<li>Event A: stackLevel 0</li>
|
||||
<li>Event B: stackLevel 1 (starts 30 min before A ends, but 30 > 15 threshold) → Stack with margin-left: 15px</li>
|
||||
<li>Event C: stackLevel 2 (starts 15 min before B ends, but separate from A-B stack) → Stack with margin-left: 30px</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Threshold = 30 min (Shared GRID with 2 columns):</strong></p>
|
||||
<ul>
|
||||
<li>Grid Group (A, B & C): 2-column grid layout</li>
|
||||
<li><strong>Column 1:</strong> Event A (09:00-10:00) + Event C (10:15-12:00) - they don't overlap!</li>
|
||||
<li><strong>Column 2:</strong> Event B (09:30-10:30) - overlaps both A and C</li>
|
||||
<li>Event A: grid column 1, top: 0px</li>
|
||||
<li>Event B: grid column 2, top: 30px</li>
|
||||
<li>Event C: grid column 1, top: 75px (shares column with A, no overlap)</li>
|
||||
<li>All events: stackLevel 0, margin-left: 0px (no stacking, all in same grid container)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- SCENARIO 10: Complex Column Sharing with Multiple Events -->
|
||||
<!-- ============================================ -->
|
||||
<div class="section">
|
||||
<h2>Scenario 10: Complex Column Sharing</h2>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item"><strong>Event A:</strong> 12:00 - 15:00 (3 hours)</div>
|
||||
<div class="legend-item"><strong>Event B:</strong> 12:30 - 13:00 (30 min, starts 30 min after A)</div>
|
||||
<div class="legend-item"><strong>Event C:</strong> 13:30 - 14:30 (1 hour, starts 30 min after B ends)</div>
|
||||
<div class="legend-item"><strong>Event D:</strong> 14:00 - 15:00 (1 hour, starts 30 min before C ends)</div>
|
||||
<div class="legend-item"><strong>Event E:</strong> 14:00 - 15:00 (1 hour, starts same time as D)</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Analysis with threshold = 30 min:</strong><br>
|
||||
• A-B conflict: B starts 30 min after A (≤ 30) → grouped<br>
|
||||
• B-C conflict: C starts 30 min after B ends (≤ 30) → grouped with A-B<br>
|
||||
• C-D conflict: D starts 30 min before C ends (≤ 30) → grouped with A-B-C<br>
|
||||
• D-E conflict: D and E start at same time (0 min) → grouped with all<br>
|
||||
• Therefore: All 5 events in ONE grid group<br><br>
|
||||
<strong>Column allocation:</strong><br>
|
||||
• A overlaps: B, C, D, E → needs own column<br>
|
||||
• B overlaps: A → needs own column<br>
|
||||
• C overlaps: A, D, E → needs own column<br>
|
||||
• D overlaps: A, C, E → needs own column<br>
|
||||
• E overlaps: A, C, D → can share column with B (they don't overlap)<br>
|
||||
• Result: 4 columns needed
|
||||
</div>
|
||||
|
||||
<div class="comparison">
|
||||
<!-- Threshold = 15 min -->
|
||||
<div class="calendar-column">
|
||||
<div class="column-title">With Threshold = 15 min</div>
|
||||
<div class="timeline">
|
||||
<div class="time-marker" style="top: 0%;">12:00</div>
|
||||
<div class="time-marker" style="top: 33%;">13:00</div>
|
||||
<div class="time-marker" style="top: 67%;">14:00</div>
|
||||
<div class="time-marker" style="top: 100%;">15:00</div>
|
||||
</div>
|
||||
|
||||
<div class="events-container">
|
||||
<!-- Event A: stackLevel 0 -->
|
||||
<div class="event event-a" style="position: absolute; top: 0px; left: 2px; right: 2px; height: 180px; margin-left: 0px; z-index: 100;">
|
||||
Event A<br>12:00-15:00
|
||||
</div>
|
||||
|
||||
<!-- Event B: stackLevel 1 (overlaps A) -->
|
||||
<div class="event event-b" style="position: absolute; top: 30px; left: 2px; right: 2px; height: 30px; margin-left: 15px; z-index: 101;">
|
||||
Event B<br>12:30-13:00
|
||||
</div>
|
||||
|
||||
<!-- Event C: stackLevel 2 (overlaps A, not B) -->
|
||||
<div class="event event-c" style="position: absolute; top: 90px; left: 2px; right: 2px; height: 60px; margin-left: 30px; z-index: 102;">
|
||||
Event C<br>13:30-14:30
|
||||
</div>
|
||||
|
||||
<!-- Grid Group for D & E: stackLevel 3 (start simultaneously, overlap A and C) -->
|
||||
<div style="position: absolute; top: 120px; left: 2px; right: 2px; margin-left: 45px; z-index: 103; display: grid; grid-template-columns: 1fr 1fr; gap: 2px;">
|
||||
<div style="position: relative;">
|
||||
<div class="event event-d" style="position: absolute; top: 0px; height: 60px; left: 0; right: 0;">
|
||||
Event D<br>14:00-15:00
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<div class="event event-d" style="position: absolute; top: 0px; height: 60px; left: 0; right: 0;">
|
||||
Event E<br>14:00-15:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Threshold = 30 min -->
|
||||
<div class="calendar-column">
|
||||
<div class="column-title">With Threshold = 30 min</div>
|
||||
<div class="timeline">
|
||||
<div class="time-marker" style="top: 0%;">12:00</div>
|
||||
<div class="time-marker" style="top: 33%;">13:00</div>
|
||||
<div class="time-marker" style="top: 67%;">14:00</div>
|
||||
<div class="time-marker" style="top: 100%;">15:00</div>
|
||||
</div>
|
||||
|
||||
<div class="events-container">
|
||||
<!-- Grid Group: All 5 events (4 columns) -->
|
||||
<div style="position: absolute; top: 0px; left: 2px; right: 2px; margin-left: 0px; z-index: 100; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 2px;">
|
||||
|
||||
<!-- Column 1: Event A -->
|
||||
<div style="position: relative;">
|
||||
<div class="event event-a" style="position: absolute; top: 0px; height: 180px; left: 0; right: 0;">
|
||||
Event A<br>12:00-15:00
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column 2: Event B + E (don't overlap) -->
|
||||
<div style="position: relative;">
|
||||
<div class="event event-b" style="position: absolute; top: 30px; height: 30px; left: 0; right: 0;">
|
||||
Event B<br>12:30-13:00
|
||||
</div>
|
||||
<div class="event event-d" style="position: absolute; top: 120px; height: 60px; left: 0; right: 0;">
|
||||
Event E<br>14:00-15:00
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column 3: Event C -->
|
||||
<div style="position: relative;">
|
||||
<div class="event event-c" style="position: absolute; top: 90px; height: 60px; left: 0; right: 0;">
|
||||
Event C<br>13:30-14:30
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Column 4: Event D -->
|
||||
<div style="position: relative;">
|
||||
<div class="event event-d" style="position: absolute; top: 120px; height: 60px; left: 0; right: 0;">
|
||||
Event D<br>14:00-15:00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Expected Layout</h3>
|
||||
<div class="legend">
|
||||
<p><strong>Threshold = 15 min (Stack + Small Grid):</strong></p>
|
||||
<ul>
|
||||
<li>Event A: stackLevel 0</li>
|
||||
<li>Event B: stackLevel 1 (overlaps A, 30 min > 15 threshold) → margin-left: 15px</li>
|
||||
<li>Event C: stackLevel 2 (overlaps A, 30 min > 15 threshold) → margin-left: 30px</li>
|
||||
<li>Grid Group (D & E): stackLevel 3 (start simultaneously) → margin-left: 45px
|
||||
<ul>
|
||||
<li>2-column grid: D in column 1, E in column 2</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Threshold = 30 min (Large Grid):</strong></p>
|
||||
<ul>
|
||||
<li>Grid Group (A, B, C, D, E): All in ONE grid group
|
||||
<ul>
|
||||
<li><strong>Column 1:</strong> Event A (top: 0px, height: 180px)</li>
|
||||
<li><strong>Column 2:</strong> Event B (top: 30px) + Event E (top: 120px)</li>
|
||||
<li><strong>Column 3:</strong> Event C (top: 90px)</li>
|
||||
<li><strong>Column 4:</strong> Event D (top: 120px)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Key Points:</strong></p>
|
||||
<ul>
|
||||
<li>With threshold = 30: All events grouped due to chained end-to-start conflicts</li>
|
||||
<li>With threshold = 15: Only D & E grouped (start simultaneously), A/B/C stacked separately</li>
|
||||
<li>B and E can share column 2 (they don't overlap: B ends 13:00, E starts 14:00)</li>
|
||||
<li>D and E start at same time but need separate columns (they overlap perfectly)</li>
|
||||
<li>Result with 30 min: 4 columns instead of 5 (optimization saves 1 column)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style="text-align: center; color: #999; margin-top: 40px; padding: 20px;">
|
||||
<p>Event Stacking Visualization - Calendar Plantempus</p>
|
||||
<p style="font-size: 12px;">Static documentation for event stacking concepts</p>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@
|
|||
* EventStackManager - Flexbox + Nested Stacking Tests
|
||||
*
|
||||
* Tests for the 3-phase algorithm:
|
||||
* Phase 1: Group events by start time proximity (±15 min threshold)
|
||||
* Phase 1: Group events by start time proximity (±N min threshold - configurable)
|
||||
* Phase 2: Decide container type (GRID vs STACKING)
|
||||
* Phase 3: Handle late arrivals (nested stacking)
|
||||
*
|
||||
* Based on scenarios from stacking-visualization.html
|
||||
* Tests are dynamic and work with any gridStartThresholdMinutes value from config.
|
||||
*
|
||||
* @see STACKING_CONCEPT.md for concept documentation
|
||||
* @see stacking-visualization.html for visual examples
|
||||
|
|
@ -14,12 +15,17 @@
|
|||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { EventStackManager } from '../../src/managers/EventStackManager';
|
||||
import { EventLayoutCoordinator } from '../../src/managers/EventLayoutCoordinator';
|
||||
import { calendarConfig } from '../../src/core/CalendarConfig';
|
||||
|
||||
describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', () => {
|
||||
let manager: EventStackManager;
|
||||
let thresholdMinutes: number;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new EventStackManager();
|
||||
// Get threshold from config - tests should work with any value
|
||||
thresholdMinutes = calendarConfig.getGridSettings().gridStartThresholdMinutes;
|
||||
});
|
||||
|
||||
// ============================================
|
||||
|
|
@ -27,7 +33,7 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
// ============================================
|
||||
|
||||
describe('Phase 1: Start Time Grouping', () => {
|
||||
it('should group events starting within ±15 minutes together', () => {
|
||||
it('should group events starting within threshold minutes together', () => {
|
||||
const events = [
|
||||
{
|
||||
id: 'event-a',
|
||||
|
|
@ -53,28 +59,33 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
expect(groups[0].events.map(e => e.id)).toEqual(['event-a', 'event-b', 'event-c']);
|
||||
});
|
||||
|
||||
it('should NOT group events starting more than 15 minutes apart', () => {
|
||||
it('should NOT group events with no time conflicts', () => {
|
||||
// Event C starts (threshold + 1) minutes AFTER A ends (no conflict)
|
||||
const eventAEnd = new Date('2025-01-01T12:00:00');
|
||||
const eventCStart = new Date(eventAEnd.getTime());
|
||||
eventCStart.setMinutes(eventCStart.getMinutes() + thresholdMinutes + 1);
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: 'event-a',
|
||||
start: new Date('2025-01-01T11:00:00'),
|
||||
end: new Date('2025-01-01T12:30:00')
|
||||
end: eventAEnd // Ends at 12:00
|
||||
},
|
||||
{
|
||||
id: 'event-b',
|
||||
start: new Date('2025-01-01T11:00:00'),
|
||||
end: new Date('2025-01-01T12:00:00')
|
||||
end: new Date('2025-01-01T11:30:00')
|
||||
},
|
||||
{
|
||||
id: 'event-c',
|
||||
start: new Date('2025-01-01T11:30:00'), // 30 min after A (exceeds threshold)
|
||||
end: new Date('2025-01-01T11:45:00')
|
||||
start: eventCStart, // Starts at 12:00 + (threshold + 1) min
|
||||
end: new Date('2025-01-01T13:00:00')
|
||||
}
|
||||
];
|
||||
|
||||
const groups = manager.groupEventsByStartTime(events);
|
||||
|
||||
// Event C should be in separate group
|
||||
// Event C should be in separate group (no conflict with A or B)
|
||||
expect(groups).toHaveLength(2);
|
||||
|
||||
const firstGroup = groups.find(g => g.events.some(e => e.id === 'event-a'));
|
||||
|
|
@ -112,7 +123,11 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
expect(groups[0].events.map(e => e.id)).toEqual(['event-a', 'event-b', 'event-c']);
|
||||
});
|
||||
|
||||
it('should handle edge case: events exactly 15 minutes apart (should be grouped)', () => {
|
||||
it('should handle edge case: events exactly at threshold minutes apart (should be grouped)', () => {
|
||||
// Event B starts exactly threshold minutes after A
|
||||
const eventBStart = new Date('2025-01-01T11:00:00');
|
||||
eventBStart.setMinutes(eventBStart.getMinutes() + thresholdMinutes);
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: 'event-a',
|
||||
|
|
@ -121,8 +136,8 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
},
|
||||
{
|
||||
id: 'event-b',
|
||||
start: new Date('2025-01-01T11:15:00'), // Exactly 15 min
|
||||
end: new Date('2025-01-01T12:00:00')
|
||||
start: eventBStart, // Exactly threshold min
|
||||
end: new Date('2025-01-01T12:30:00')
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -132,17 +147,22 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
expect(groups[0].events).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle edge case: events exactly 16 minutes apart (should NOT be grouped)', () => {
|
||||
it('should handle edge case: events at (threshold + 1) minutes apart with no overlap (should NOT be grouped)', () => {
|
||||
// Event B starts (threshold + 1) minutes AFTER A ends (no conflict)
|
||||
const eventAEnd = new Date('2025-01-01T12:00:00');
|
||||
const eventBStart = new Date(eventAEnd.getTime());
|
||||
eventBStart.setMinutes(eventBStart.getMinutes() + thresholdMinutes + 1);
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: 'event-a',
|
||||
start: new Date('2025-01-01T11:00:00'),
|
||||
end: new Date('2025-01-01T12:00:00')
|
||||
end: eventAEnd
|
||||
},
|
||||
{
|
||||
id: 'event-b',
|
||||
start: new Date('2025-01-01T11:16:00'), // 16 min > 15 min threshold
|
||||
end: new Date('2025-01-01T12:00:00')
|
||||
start: eventBStart, // Starts after A ends + (threshold + 1) min
|
||||
end: new Date('2025-01-01T13:00:00')
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -796,7 +816,7 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
}
|
||||
];
|
||||
|
||||
// Test stack links
|
||||
// Test stack links - these should be consistent regardless of grouping
|
||||
const stackLinks = manager.createOptimizedStackLinks(events);
|
||||
|
||||
expect(stackLinks.get('151')?.stackLevel).toBe(0);
|
||||
|
|
@ -805,20 +825,25 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
expect(stackLinks.get('1513')?.stackLevel).toBe(3);
|
||||
expect(stackLinks.get('1514')?.stackLevel).toBe(4); // Must be above 1513 (they overlap)
|
||||
|
||||
// Test grouping
|
||||
// Test grouping - behavior depends on threshold
|
||||
const groups = manager.groupEventsByStartTime(events);
|
||||
|
||||
// Should have 4 groups: {151}, {1511}, {1512}, {1513, 1514}
|
||||
expect(groups).toHaveLength(4);
|
||||
// Events are spaced 30 min apart, so:
|
||||
// - If threshold >= 30: all 5 events group together
|
||||
// - If threshold < 30: events group separately
|
||||
|
||||
const group1513_1514 = groups.find(g => g.events.some(e => e.id === '1513'));
|
||||
expect(group1513_1514).toBeDefined();
|
||||
expect(group1513_1514?.events).toHaveLength(2);
|
||||
expect(group1513_1514?.events.map(e => e.id).sort()).toEqual(['1513', '1514']);
|
||||
// Find group containing 1513
|
||||
const group1513 = groups.find(g => g.events.some(e => e.id === '1513'));
|
||||
expect(group1513).toBeDefined();
|
||||
|
||||
// Test container type - should be GRID
|
||||
const containerType = manager.decideContainerType(group1513_1514!);
|
||||
// 1513 and 1514 start at same time, so should always be in same group
|
||||
expect(group1513?.events.some(e => e.id === '1514')).toBe(true);
|
||||
|
||||
// If group has multiple events, container type should be GRID
|
||||
if (group1513!.events.length > 1) {
|
||||
const containerType = manager.decideContainerType(group1513!);
|
||||
expect(containerType).toBe('GRID');
|
||||
}
|
||||
});
|
||||
|
||||
it('Debug: Events 144, 145, 146 overlap detection', () => {
|
||||
|
|
@ -954,6 +979,190 @@ describe('EventStackManager - Flexbox & Nested Stacking (3-Phase Algorithm)', ()
|
|||
// (they're side-by-side, not stacked)
|
||||
});
|
||||
|
||||
it('Scenario 8: Edge case - Events exactly 15 min apart WITH overlap', () => {
|
||||
// Event A: 11:00 - 12:00
|
||||
// Event B: 11:15 - 12:30
|
||||
// Difference: 15 min (exactly at threshold)
|
||||
// Overlap: YES (11:15 - 12:00)
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: 'event-a',
|
||||
start: new Date('2025-01-01T11:00:00'),
|
||||
end: new Date('2025-01-01T12:00:00')
|
||||
},
|
||||
{
|
||||
id: 'event-b',
|
||||
start: new Date('2025-01-01T11:15:00'),
|
||||
end: new Date('2025-01-01T12:30:00')
|
||||
}
|
||||
];
|
||||
|
||||
// Step 1: Verify they're grouped together (15 min ≤ threshold)
|
||||
const groups = manager.groupEventsByStartTime(events);
|
||||
expect(groups).toHaveLength(1); // Same group
|
||||
expect(groups[0].events).toHaveLength(2); // Both events in group
|
||||
|
||||
// Step 2: Verify they DO overlap
|
||||
const overlap = manager.doEventsOverlap(events[0], events[1]);
|
||||
expect(overlap).toBe(true);
|
||||
|
||||
// Step 3: CRITICAL: Despite overlapping, should use GRID (visual priority = simultaneity)
|
||||
const containerType = manager.decideContainerType(groups[0]);
|
||||
expect(containerType).toBe('GRID');
|
||||
|
||||
// Step 4: Verify stack levels for understanding (even though not used for GRID rendering)
|
||||
const stackLinks = manager.createOptimizedStackLinks(events);
|
||||
expect(stackLinks.get('event-a')?.stackLevel).toBe(0);
|
||||
expect(stackLinks.get('event-b')?.stackLevel).toBe(1); // Would be stacked if not in GRID
|
||||
});
|
||||
|
||||
it('Scenario 9: End-to-Start conflicts - Events share grid despite start times', () => {
|
||||
// Event A: 09:00 - 10:00
|
||||
// Event B: 09:30 - 10:30 (starts 30 min before A ends → conflicts with A)
|
||||
// Event C: 10:15 - 12:00 (starts 15 min before B ends → conflicts with B)
|
||||
//
|
||||
// Key Rule: Events share columns (GRID) when they conflict within threshold
|
||||
// Conflict = Event starts within ±threshold minutes of another event's end time
|
||||
//
|
||||
// With threshold = 30 min:
|
||||
// - A-B conflict: B starts 30 min before A ends (≤ 30 min) → grouped
|
||||
// - B-C conflict: C starts 15 min before B ends (≤ 30 min) → grouped
|
||||
// - Therefore: A, B, C all in same GRID group
|
||||
// - A and C don't overlap → share same grid column
|
||||
// - Result: 2-column grid (Column 1: A+C, Column 2: B)
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: 'event-a',
|
||||
start: new Date('2025-01-01T09:00:00'),
|
||||
end: new Date('2025-01-01T10:00:00')
|
||||
},
|
||||
{
|
||||
id: 'event-b',
|
||||
start: new Date('2025-01-01T09:30:00'),
|
||||
end: new Date('2025-01-01T10:30:00')
|
||||
},
|
||||
{
|
||||
id: 'event-c',
|
||||
start: new Date('2025-01-01T10:15:00'),
|
||||
end: new Date('2025-01-01T12:00:00')
|
||||
}
|
||||
];
|
||||
|
||||
// Step 1: Verify overlaps
|
||||
const overlapAB = manager.doEventsOverlap(events[0], events[1]);
|
||||
const overlapBC = manager.doEventsOverlap(events[1], events[2]);
|
||||
const overlapAC = manager.doEventsOverlap(events[0], events[2]);
|
||||
|
||||
expect(overlapAB).toBe(true); // A: 09:00-10:00, B: 09:30-10:30 → overlap 09:30-10:00
|
||||
expect(overlapBC).toBe(true); // B: 09:30-10:30, C: 10:15-12:00 → overlap 10:15-10:30
|
||||
expect(overlapAC).toBe(false); // A: 09:00-10:00, C: 10:15-12:00 → NO overlap
|
||||
|
||||
// Step 2: Grouping based on end-to-start conflicts
|
||||
const groups = manager.groupEventsByStartTime(events);
|
||||
|
||||
if (thresholdMinutes >= 30) {
|
||||
// All 3 events should be in ONE group (due to chained end-to-start conflicts)
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groups[0].events).toHaveLength(3);
|
||||
expect(groups[0].events.map(e => e.id).sort()).toEqual(['event-a', 'event-b', 'event-c']);
|
||||
|
||||
// Should use GRID container type
|
||||
const containerType = manager.decideContainerType(groups[0]);
|
||||
expect(containerType).toBe('GRID');
|
||||
} else if (thresholdMinutes >= 15) {
|
||||
// With 15 ≤ threshold < 30:
|
||||
// - A-B: 30 min conflict > 15 → NOT grouped
|
||||
// - B-C: 15 min conflict ≤ 15 → grouped
|
||||
// Result: 2 groups
|
||||
expect(groups.length).toBeGreaterThanOrEqual(2);
|
||||
}
|
||||
|
||||
// Step 3: Stack levels (for understanding, even though rendered as GRID)
|
||||
const stackLinks = manager.createOptimizedStackLinks(events);
|
||||
expect(stackLinks.get('event-a')?.stackLevel).toBe(0);
|
||||
expect(stackLinks.get('event-b')?.stackLevel).toBe(1); // overlaps A
|
||||
expect(stackLinks.get('event-c')?.stackLevel).toBe(2); // overlaps B (must be above B's level)
|
||||
|
||||
// Note: Even though C has stackLevel 2, it will share a grid column with A
|
||||
// because they don't overlap. Column allocation is different from stack levels.
|
||||
});
|
||||
|
||||
it('Scenario 9: Column allocation - A and C share column, B in separate column', () => {
|
||||
// This test verifies the column allocation logic for Scenario 9
|
||||
// Event A: 09:00-10:00, Event B: 09:30-10:30, Event C: 10:15-12:00
|
||||
//
|
||||
// Expected columns:
|
||||
// - Column 1: [A, C] (they don't overlap)
|
||||
// - Column 2: [B] (overlaps both A and C)
|
||||
|
||||
const events = [
|
||||
{
|
||||
id: 'event-a',
|
||||
start: new Date('2025-01-01T09:00:00'),
|
||||
end: new Date('2025-01-01T10:00:00'),
|
||||
title: 'Event A',
|
||||
type: 'work' as const,
|
||||
allDay: false,
|
||||
syncStatus: 'synced' as const
|
||||
},
|
||||
{
|
||||
id: 'event-b',
|
||||
start: new Date('2025-01-01T09:30:00'),
|
||||
end: new Date('2025-01-01T10:30:00'),
|
||||
title: 'Event B',
|
||||
type: 'work' as const,
|
||||
allDay: false,
|
||||
syncStatus: 'synced' as const
|
||||
},
|
||||
{
|
||||
id: 'event-c',
|
||||
start: new Date('2025-01-01T10:15:00'),
|
||||
end: new Date('2025-01-01T12:00:00'),
|
||||
title: 'Event C',
|
||||
type: 'work' as const,
|
||||
allDay: false,
|
||||
syncStatus: 'synced' as const
|
||||
}
|
||||
];
|
||||
|
||||
// Use EventLayoutCoordinator to test column allocation
|
||||
const coordinator = new EventLayoutCoordinator();
|
||||
|
||||
if (thresholdMinutes >= 30) {
|
||||
// Calculate layout
|
||||
const layout = coordinator.calculateColumnLayout(events);
|
||||
|
||||
// Should have 1 grid group (all events grouped together)
|
||||
expect(layout.gridGroups).toHaveLength(1);
|
||||
|
||||
const gridGroup = layout.gridGroups[0];
|
||||
|
||||
// Should have 2 columns
|
||||
expect(gridGroup.columns).toHaveLength(2);
|
||||
|
||||
// Column 1 should contain A and C (they don't overlap)
|
||||
const column1 = gridGroup.columns.find(col =>
|
||||
col.some(e => e.id === 'event-a') && col.some(e => e.id === 'event-c')
|
||||
);
|
||||
expect(column1).toBeDefined();
|
||||
expect(column1).toHaveLength(2);
|
||||
expect(column1?.map(e => e.id).sort()).toEqual(['event-a', 'event-c']);
|
||||
|
||||
// Column 2 should contain only B
|
||||
const column2 = gridGroup.columns.find(col =>
|
||||
col.some(e => e.id === 'event-b')
|
||||
);
|
||||
expect(column2).toBeDefined();
|
||||
expect(column2).toHaveLength(1);
|
||||
expect(column2?.[0].id).toBe('event-b');
|
||||
|
||||
// No stacked events (all in grid)
|
||||
expect(layout.stackedEvents).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it.skip('Scenario 6: Grid + D nested in B column (NOT IMPLEMENTED - requires Phase 3)', () => {
|
||||
// Event A: 10:00 - 13:00
|
||||
// Event B: 11:00 - 12:30 (flexbox column 1)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue