Skip to content

Commit b3efbbf

Browse files
committed
Support passing right spacing for overlapping Timeline events
1 parent 7537375 commit b3efbbf

File tree

4 files changed

+89
-55
lines changed

4 files changed

+89
-55
lines changed

example/src/screens/timelineCalendar.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ const EVENTS: TimelineEventProps[] = [
2828
summary: '3412 Piedmont Rd NE, GA 3032',
2929
color: '#e6add8'
3030
},
31+
{
32+
start: `${getDate()} 01:45:00`,
33+
end: `${getDate()} 02:45:00`,
34+
title: 'Dr. Mariana Joseph',
35+
summary: '3412 Piedmont Rd NE, GA 3032',
36+
color: '#e6add8'
37+
},
38+
{
39+
start: `${getDate()} 02:40:00`,
40+
end: `${getDate()} 03:10:00`,
41+
title: 'Dr. Mariana Joseph',
42+
summary: '3412 Piedmont Rd NE, GA 3032',
43+
color: '#e6add8'
44+
},
3145
{
3246
start: `${getDate(1)} 00:30:00`,
3347
end: `${getDate(1)} 01:30:00`,
@@ -185,10 +199,11 @@ export default class TimelineCalendarScreen extends Component {
185199
private timelineProps = {
186200
format24h: true,
187201
onBackgroundLongPress: this.createNewEvent,
188-
onBackgroundLongPressOut: this.approveNewEvent
202+
onBackgroundLongPressOut: this.approveNewEvent,
189203
// scrollToFirst: true,
190204
// start: 0,
191-
// end: 24
205+
// end: 24,
206+
// eventRightSpacing: 4
192207
};
193208

194209
render() {
@@ -212,7 +227,7 @@ export default class TimelineCalendarScreen extends Component {
212227
events={eventsByDate}
213228
timelineProps={this.timelineProps}
214229
showNowIndicator
215-
scrollToNow
230+
// scrollToNow
216231
scrollToFirst
217232
initialTime={INITIAL_TIME}
218233
/>

src/timeline/Packer.ts

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,78 @@
11
// @flow
22
import XDate from 'xdate';
3+
import constants from '../commons/constants';
34
import {Event, PackedEvent} from './EventBlock';
45

6+
type PartialPackedEvent = Event & {index: number};
7+
interface PopulateOptions {
8+
screenWidth?: number;
9+
dayStart?: number;
10+
hourBlockHeight?: number;
11+
eventBlockRightMargin?: number;
12+
}
13+
514
export const HOUR_BLOCK_HEIGHT = 100;
615
const EVENT_BLOCK_RIGHT_MARGIN = 10;
716

8-
function buildEvent(column: any, left: number, width: number, dayStart: number) {
9-
const startTime = new XDate(column.start);
10-
const endTime = column.end ? new XDate(column.end) : new XDate(startTime).addHours(1);
17+
function buildEvent(event: Event & {index: number}, left: number, width: number, {dayStart = 0, hourBlockHeight = HOUR_BLOCK_HEIGHT}: PopulateOptions): PackedEvent {
18+
const startTime = new XDate(event.start);
19+
const endTime = event.end ? new XDate(event.end) : new XDate(startTime).addHours(1);
1120

1221
const dayStartTime = new XDate(startTime).clearTime();
1322

14-
column.top = (dayStartTime.diffHours(startTime) - dayStart) * HOUR_BLOCK_HEIGHT;
15-
column.height = startTime.diffHours(endTime) * HOUR_BLOCK_HEIGHT;
16-
column.width = width;
17-
column.left = left;
18-
return column;
23+
return {
24+
...event,
25+
top: (dayStartTime.diffHours(startTime) - dayStart) * hourBlockHeight,
26+
height: startTime.diffHours(endTime) * hourBlockHeight,
27+
width: width,
28+
left: left
29+
};
1930
}
2031

21-
function collision(a: Event, b: Event) {
32+
function hasCollision(a: Event, b: Event) {
2233
return a.end > b.start && a.start < b.end;
2334
}
2435

25-
function expand(ev: Event, column: any, columns: any) {
36+
function calcColumnSpan(event: Event, columnIndex: number, columns: Event[][]) {
2637
let colSpan = 1;
2738

28-
for (let i = column + 1; i < columns.length; i++) {
29-
const col = columns[i];
30-
for (let j = 0; j < col.length; j++) {
31-
const ev1 = col[j];
32-
if (collision(ev, ev1)) {
33-
return colSpan;
34-
}
39+
for (let i = columnIndex + 1; i < columns.length; i++) {
40+
const column = columns[i];
41+
42+
const foundCollision = column.find(ev => hasCollision(event, ev));
43+
if (foundCollision) {
44+
return colSpan;
3545
}
46+
3647
colSpan++;
3748
}
3849

3950
return colSpan;
4051
}
4152

42-
function pack(columns: any, width: number, calculatedEvents: Event[], dayStart: number) {
43-
const colLength = columns.length;
44-
45-
for (let i = 0; i < colLength; i++) {
46-
const col = columns[i];
47-
for (let j = 0; j < col.length; j++) {
48-
const colSpan = expand(col[j], i, columns);
49-
const L = (i / colLength) * width;
50-
const W = (width * colSpan) / colLength - EVENT_BLOCK_RIGHT_MARGIN;
51-
52-
calculatedEvents.push(buildEvent(col[j], L, W, dayStart));
53-
}
54-
}
53+
function packOverlappingEventGroup(
54+
columns: PartialPackedEvent[][],
55+
calculatedEvents: PackedEvent[],
56+
populateOptions: PopulateOptions
57+
) {
58+
const {screenWidth = constants.screenWidth, eventBlockRightMargin = EVENT_BLOCK_RIGHT_MARGIN} = populateOptions;
59+
columns.forEach((column, columnIndex) => {
60+
column.forEach(event => {
61+
const columnSpan = calcColumnSpan(event, columnIndex, columns);
62+
const eventLeft = (columnIndex / columns.length) * screenWidth;
63+
const eventWidth = screenWidth * (columnSpan / columns.length) - eventBlockRightMargin;
64+
65+
calculatedEvents.push(buildEvent(event, eventLeft, eventWidth, populateOptions));
66+
});
67+
});
5568
}
5669

57-
function populateEvents(events: Event[], screenWidth: number, dayStart: number) {
58-
let lastEnd: any;
59-
let columns: any;
60-
const calculatedEvents: Event[] = [];
70+
function populateEvents(_events: Event[], populateOptions: PopulateOptions) {
71+
let lastEnd: string | null = null;
72+
let columns: PartialPackedEvent[][] = [];
73+
const calculatedEvents: PackedEvent[] = [];
6174

62-
events = events
75+
const events: PartialPackedEvent[] = _events
6376
.map((ev: Event, index: number) => ({...ev, index: index}))
6477
.sort(function (a: Event, b: Event) {
6578
if (a.start < b.start) return -1;
@@ -69,26 +82,26 @@ function populateEvents(events: Event[], screenWidth: number, dayStart: number)
6982
return 0;
7083
});
7184

72-
columns = [];
73-
lastEnd = null;
74-
75-
events.forEach(function (ev: Event) {
85+
events.forEach(function (ev) {
86+
// Reset recent overlapping event group and start a new one
7687
if (lastEnd !== null && ev.start >= lastEnd) {
77-
pack(columns, screenWidth, calculatedEvents, dayStart);
88+
packOverlappingEventGroup(columns, calculatedEvents, populateOptions);
7889
columns = [];
7990
lastEnd = null;
8091
}
8192

93+
// Place current event in the right column where it doesn't overlap
8294
let placed = false;
8395
for (let i = 0; i < columns.length; i++) {
8496
const col = columns[i];
85-
if (!collision(col[col.length - 1], ev)) {
97+
if (!hasCollision(col[col.length - 1], ev)) {
8698
col.push(ev);
8799
placed = true;
88100
break;
89101
}
90102
}
91103

104+
// If curr event wasn't placed in any of the columns, create a new column for it
92105
if (!placed) {
93106
columns.push([ev]);
94107
}
@@ -99,9 +112,10 @@ function populateEvents(events: Event[], screenWidth: number, dayStart: number)
99112
});
100113

101114
if (columns.length > 0) {
102-
pack(columns, screenWidth, calculatedEvents, dayStart);
115+
packOverlappingEventGroup(columns, calculatedEvents, populateOptions);
103116
}
104-
return calculatedEvents as PackedEvent[];
117+
118+
return calculatedEvents;
105119
}
106120

107121
export default populateEvents;

src/timeline/Timeline.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ export interface TimelineProps {
8383
* Listen to onScroll event of the timeline component
8484
*/
8585
onChangeOffset?: (offset: number) => void;
86+
/**
87+
* Right spacing between overlapping events
88+
*/
89+
eventRightSpacing?: number;
8690
}
8791

8892
const Timeline = (props: TimelineProps) => {
@@ -103,6 +107,7 @@ const Timeline = (props: TimelineProps) => {
103107
showNowIndicator,
104108
scrollOffset,
105109
onChangeOffset,
110+
eventRightSpacing,
106111
eventTapped
107112
} = props;
108113

@@ -114,7 +119,7 @@ const Timeline = (props: TimelineProps) => {
114119

115120
const packedEvents = useMemo(() => {
116121
const width = constants.screenWidth - HOURS_SIDEBAR_WIDTH;
117-
return populateEvents(events, width, start);
122+
return populateEvents(events, {screenWidth: width, dayStart: start, eventBlockRightMargin: eventRightSpacing});
118123
}, [events, start]);
119124

120125
useEffect(() => {

src/timeline/__tests__/Packer.spec.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,56 +22,56 @@ describe('Timeline Packer utils', () => {
2222
}
2323
];
2424
it('should sort events by start and end times', () => {
25-
const packedEvents = uut(events, 300, 0);
25+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
2626
expect(packedEvents[0].id).toBe('event_1');
2727
expect(packedEvents[1].id).toBe('event_3');
2828
expect(packedEvents[2].id).toBe('event_2');
2929
});
3030

3131
describe('should set events block size', () => {
3232
it('should set event block height based on their duration', () => {
33-
const packedEvents = uut(events, 300, 0);
33+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
3434
expect(packedEvents[0].height).toBe(100); // event 1
3535
expect(packedEvents[1].height).toBeCloseTo(108.333); // event 3
3636
expect(packedEvents[2].height).toBe(50); // event 2
3737
});
3838

3939
it('should set event block width based on overlaps of 3 events', () => {
40-
const packedEvents = uut(events, 300, 0);
40+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
4141
expect(packedEvents[0].width).toBe(90); // event 1
4242
expect(packedEvents[1].width).toBe(90); // event 3
4343
expect(packedEvents[2].width).toBe(90); // event 2
4444
});
4545

4646
it('should set event block width based on overlaps of 2 events', () => {
47-
const packedEvents = uut([events[0], events[1]], 300, 0);
47+
const packedEvents = uut([events[0], events[1]], {screenWidth: 300, dayStart: 0});
4848
expect(packedEvents[0].width).toBe(140); // event 1
4949
expect(packedEvents[1].width).toBe(140); // event 2
5050
});
5151

5252
it('should set event block width when there is not overlaps', () => {
53-
const packedEvents = uut([events[0]], 300, 0);
53+
const packedEvents = uut([events[0]], {screenWidth: 300, dayStart: 0});
5454
expect(packedEvents[0].width).toBe(290); // event 1
5555
});
5656
});
5757

5858
describe('should set events block position', () => {
5959
it('should set top position base on the event start time', () => {
60-
const packedEvents = uut(events, 300, 0);
60+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
6161
expect(packedEvents[0].top).toBe(150); // event 1
6262
expect(packedEvents[1].top).toBeCloseTo(191.666); // event 3
6363
expect(packedEvents[2].top).toBe(225); // event 2
6464
});
6565

6666
it('should set left position base when the 3 events overlap', () => {
67-
const packedEvents = uut(events, 300, 0);
67+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
6868
expect(packedEvents[0].left).toBe(0); // event 1
6969
expect(packedEvents[1].left).toBe(100); // event 3
7070
expect(packedEvents[2].left).toBe(200); // event 2
7171
});
7272

7373
it('should set left position base when the 2 events overlap', () => {
74-
const packedEvents = uut([events[0], events[1]], 300, 0);
74+
const packedEvents = uut([events[0], events[1]], {screenWidth: 300, dayStart: 0});
7575
expect(packedEvents[0].left).toBe(0); // event 1
7676
expect(packedEvents[1].left).toBe(150); // event 3
7777
});

0 commit comments

Comments
 (0)