Skip to content

Commit 434fc66

Browse files
authored
Merge pull request #1765 from wix/feat/timeline_eventRightSpacing
Support passing right spacing for overlapping Timeline events
2 parents 2efbcdf + 225c115 commit 434fc66

File tree

4 files changed

+157
-60
lines changed

4 files changed

+157
-60
lines changed

example/src/screens/timelineCalendar.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,46 @@ const EVENTS: TimelineEventProps[] = [
2121
title: 'Merge Request to React Native Calendars',
2222
summary: 'Merge Timeline Calendar to React Native Calendars'
2323
},
24+
{
25+
start: `${getDate()} 01:15:00`,
26+
end: `${getDate()} 02:30:00`,
27+
title: 'Meeting A',
28+
summary: 'Summary for meeting A',
29+
color: '#e6add8'
30+
},
2431
{
2532
start: `${getDate()} 01:30:00`,
2633
end: `${getDate()} 02:30:00`,
27-
title: 'Dr. Mariana Joseph',
28-
summary: '3412 Piedmont Rd NE, GA 3032',
34+
title: 'Meeting B',
35+
summary: 'Summary for meeting B',
36+
color: '#e6add8'
37+
},
38+
{
39+
start: `${getDate()} 01:45:00`,
40+
end: `${getDate()} 02:45:00`,
41+
title: 'Meeting C',
42+
summary: 'Summary for meeting C',
43+
color: '#e6add8'
44+
},
45+
{
46+
start: `${getDate()} 02:40:00`,
47+
end: `${getDate()} 03:10:00`,
48+
title: 'Meeting D',
49+
summary: 'Summary for meeting D',
50+
color: '#e6add8'
51+
},
52+
{
53+
start: `${getDate()} 02:50:00`,
54+
end: `${getDate()} 03:20:00`,
55+
title: 'Meeting E',
56+
summary: 'Summary for meeting E',
57+
color: '#e6add8'
58+
},
59+
{
60+
start: `${getDate()} 04:30:00`,
61+
end: `${getDate()} 05:30:00`,
62+
title: 'Meeting F',
63+
summary: 'Summary for meeting F',
2964
color: '#e6add8'
3065
},
3166
{
@@ -185,10 +220,11 @@ export default class TimelineCalendarScreen extends Component {
185220
private timelineProps = {
186221
format24h: true,
187222
onBackgroundLongPress: this.createNewEvent,
188-
onBackgroundLongPressOut: this.approveNewEvent
223+
onBackgroundLongPressOut: this.approveNewEvent,
189224
// scrollToFirst: true,
190225
// start: 0,
191-
// end: 24
226+
// end: 24,
227+
overlapEventsSpacing: 8
192228
};
193229

194230
render() {
@@ -212,7 +248,7 @@ export default class TimelineCalendarScreen extends Component {
212248
events={eventsByDate}
213249
timelineProps={this.timelineProps}
214250
showNowIndicator
215-
scrollToNow
251+
// scrollToNow
216252
scrollToFirst
217253
initialTime={INITIAL_TIME}
218254
/>

src/timeline/Packer.ts

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,82 @@
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+
overlapEventsSpacing?: number;
12+
}
13+
514
export const HOUR_BLOCK_HEIGHT = 100;
6-
const EVENT_BLOCK_RIGHT_MARGIN = 10;
15+
const OVERLAP_EVENTS_SPACINGS = 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,
28+
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;
53+
function packOverlappingEventGroup(
54+
columns: PartialPackedEvent[][],
55+
calculatedEvents: PackedEvent[],
56+
populateOptions: PopulateOptions
57+
) {
58+
const {screenWidth = constants.screenWidth, overlapEventsSpacing = OVERLAP_EVENTS_SPACINGS} = 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+
let eventWidth = screenWidth * (columnSpan / columns.length);
64+
65+
if (columnIndex + columnSpan <= columns.length -1) {
66+
eventWidth -= overlapEventsSpacing;
67+
}
5168

52-
calculatedEvents.push(buildEvent(col[j], L, W, dayStart));
53-
}
54-
}
69+
calculatedEvents.push(buildEvent(event, eventLeft, eventWidth, populateOptions));
70+
});
71+
});
5572
}
5673

57-
function populateEvents(events: Event[], screenWidth: number, dayStart: number) {
58-
let lastEnd: any;
59-
let columns: any;
60-
const calculatedEvents: Event[] = [];
74+
function populateEvents(_events: Event[], populateOptions: PopulateOptions) {
75+
let lastEnd: string | null = null;
76+
let columns: PartialPackedEvent[][] = [];
77+
const calculatedEvents: PackedEvent[] = [];
6178

62-
events = events
79+
const events: PartialPackedEvent[] = _events
6380
.map((ev: Event, index: number) => ({...ev, index: index}))
6481
.sort(function (a: Event, b: Event) {
6582
if (a.start < b.start) return -1;
@@ -69,26 +86,26 @@ function populateEvents(events: Event[], screenWidth: number, dayStart: number)
6986
return 0;
7087
});
7188

72-
columns = [];
73-
lastEnd = null;
74-
75-
events.forEach(function (ev: Event) {
89+
events.forEach(function (ev) {
90+
// Reset recent overlapping event group and start a new one
7691
if (lastEnd !== null && ev.start >= lastEnd) {
77-
pack(columns, screenWidth, calculatedEvents, dayStart);
92+
packOverlappingEventGroup(columns, calculatedEvents, populateOptions);
7893
columns = [];
7994
lastEnd = null;
8095
}
8196

97+
// Place current event in the right column where it doesn't overlap
8298
let placed = false;
8399
for (let i = 0; i < columns.length; i++) {
84100
const col = columns[i];
85-
if (!collision(col[col.length - 1], ev)) {
101+
if (!hasCollision(col[col.length - 1], ev)) {
86102
col.push(ev);
87103
placed = true;
88104
break;
89105
}
90106
}
91107

108+
// If curr event wasn't placed in any of the columns, create a new column for it
92109
if (!placed) {
93110
columns.push([ev]);
94111
}
@@ -99,9 +116,10 @@ function populateEvents(events: Event[], screenWidth: number, dayStart: number)
99116
});
100117

101118
if (columns.length > 0) {
102-
pack(columns, screenWidth, calculatedEvents, dayStart);
119+
packOverlappingEventGroup(columns, calculatedEvents, populateOptions);
103120
}
104-
return calculatedEvents as PackedEvent[];
121+
122+
return calculatedEvents;
105123
}
106124

107125
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+
* Spacing between overlapping events
88+
*/
89+
overlapEventsSpacing?: number;
8690
}
8791

8892
const Timeline = (props: TimelineProps) => {
@@ -103,6 +107,7 @@ const Timeline = (props: TimelineProps) => {
103107
showNowIndicator,
104108
scrollOffset,
105109
onChangeOffset,
110+
overlapEventsSpacing,
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, overlapEventsSpacing});
118123
}, [events, start]);
119124

120125
useEffect(() => {

src/timeline/__tests__/Packer.spec.js

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import _ from 'lodash';
12
import uut from '../Packer';
23

34
describe('Timeline Packer utils', () => {
@@ -22,56 +23,93 @@ describe('Timeline Packer utils', () => {
2223
}
2324
];
2425
it('should sort events by start and end times', () => {
25-
const packedEvents = uut(events, 300, 0);
26+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
2627
expect(packedEvents[0].id).toBe('event_1');
2728
expect(packedEvents[1].id).toBe('event_3');
2829
expect(packedEvents[2].id).toBe('event_2');
2930
});
3031

3132
describe('should set events block size', () => {
3233
it('should set event block height based on their duration', () => {
33-
const packedEvents = uut(events, 300, 0);
34+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
3435
expect(packedEvents[0].height).toBe(100); // event 1
3536
expect(packedEvents[1].height).toBeCloseTo(108.333); // event 3
3637
expect(packedEvents[2].height).toBe(50); // event 2
3738
});
3839

3940
it('should set event block width based on overlaps of 3 events', () => {
40-
const packedEvents = uut(events, 300, 0);
41+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
4142
expect(packedEvents[0].width).toBe(90); // event 1
4243
expect(packedEvents[1].width).toBe(90); // event 3
43-
expect(packedEvents[2].width).toBe(90); // event 2
44+
expect(packedEvents[2].width).toBe(100); // event 2
4445
});
4546

4647
it('should set event block width based on overlaps of 2 events', () => {
47-
const packedEvents = uut([events[0], events[1]], 300, 0);
48+
const packedEvents = uut([events[0], events[1]], {screenWidth: 300, dayStart: 0});
4849
expect(packedEvents[0].width).toBe(140); // event 1
49-
expect(packedEvents[1].width).toBe(140); // event 2
50+
expect(packedEvents[1].width).toBe(150); // event 2
5051
});
5152

5253
it('should set event block width when there is not overlaps', () => {
53-
const packedEvents = uut([events[0]], 300, 0);
54-
expect(packedEvents[0].width).toBe(290); // event 1
54+
const packedEvents = uut([events[0]], {screenWidth: 300, dayStart: 0});
55+
expect(packedEvents[0].width).toBe(300); // event 1
56+
});
57+
58+
it('should handle a complex case of overlapping events', () => {
59+
const overlappingEvents = [
60+
{
61+
start: `2017-09-06 01:15:00`,
62+
end: `2017-09-06 02:30:00`
63+
},
64+
{
65+
start: `2017-09-06 01:30:00`,
66+
end: `2017-09-06 02:30:00`
67+
},
68+
{
69+
start: `2017-09-06 01:45:00`,
70+
end: `2017-09-06 02:45:00`
71+
},
72+
{
73+
start: `2017-09-06 02:40:00`,
74+
end: `2017-09-06 03:10:00`
75+
},
76+
{
77+
start: `2017-09-06 02:50:00`,
78+
end: `2017-09-06 03:20:00`
79+
},
80+
{
81+
start: `2017-09-06 04:30:00`,
82+
end: `2017-09-06 05:30:00`
83+
}
84+
];
85+
let packedEvents = uut(overlappingEvents, {screenWidth: 300, dayStart: 0, overlapEventsSpacing: 4});
86+
packedEvents = _.sortBy(packedEvents, 'index');
87+
expect(packedEvents[0].width).toBe(96);
88+
expect(packedEvents[1].width).toBe(96);
89+
expect(packedEvents[2].width).toBe(100);
90+
expect(packedEvents[3].width).toBe(96);
91+
expect(packedEvents[4].width).toBe(200);
92+
expect(packedEvents[5].width).toBe(300);
5593
});
5694
});
5795

5896
describe('should set events block position', () => {
5997
it('should set top position base on the event start time', () => {
60-
const packedEvents = uut(events, 300, 0);
98+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
6199
expect(packedEvents[0].top).toBe(150); // event 1
62100
expect(packedEvents[1].top).toBeCloseTo(191.666); // event 3
63101
expect(packedEvents[2].top).toBe(225); // event 2
64102
});
65103

66104
it('should set left position base when the 3 events overlap', () => {
67-
const packedEvents = uut(events, 300, 0);
105+
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
68106
expect(packedEvents[0].left).toBe(0); // event 1
69107
expect(packedEvents[1].left).toBe(100); // event 3
70108
expect(packedEvents[2].left).toBe(200); // event 2
71109
});
72110

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

0 commit comments

Comments
 (0)