Skip to content

Commit

Permalink
Merge pull request #18 from bindable-ui/timeline-webworkers
Browse files Browse the repository at this point in the history
Web Workers for Timeline
  • Loading branch information
m0ngr31 authored Mar 5, 2020
2 parents e25ecb6 + 17dcc0d commit c78cea9
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 155 deletions.
12 changes: 12 additions & 0 deletions dev-app/routes/components/timeline/demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,16 @@ export class TimelineExample {
public displayView = 'week';
public loading = false;
public preventCreate = _isoTime => false;

// constructor() {
// const genRandom = (min, max) => Math.random() * (max - min + 1) + min;

// this.entries = _.map(_.times(1000, () => {
// return {
// duration: genRandom(60, 5000),
// title: 'something dumb',
// start: moment(this.today).add(genRandom(-50, 167), 'hours').toISOString(),
// };
// }));
// }
}
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
"jquery": "^3.4.1",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"moment-timezone": "^0.5.28"
"moment-timezone": "^0.5.28",
"workerize": "^0.1.8"
},
"husky": {
"hooks": {
Expand Down
190 changes: 38 additions & 152 deletions src/components/timeline/c-timeline/c-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {autoinject, bindable, bindingMode, containerless, TaskQueue} from 'aurel
import * as moment from 'moment-timezone';

import {ITimeBlock, ITimeDay, ITimeEntry, ITimeEntryBasic, ITimelineActions} from './c-timeline-interfaces';
import {filterEntriesDay, mapEntries} from './workers';

import {authState} from '../../../decorators/auth-state';
import {generateRandom} from '../../../helpers/generate-random';
Expand Down Expand Up @@ -157,17 +158,8 @@ export class CTimeline {
public buildTimeline = _.debounce(
() => {
this.buildBlocks();
this.transformEntries();
this.calculateCurrentTimeLine();

this.taskQueue.queueMicroTask(() => {
this.isRendering = false;

// Could potentially be 250ms behind with the throttle
_.delay(() => {
this.scrollToSpot();
}, 300);
});
this.transformEntries();
},
200,
{trailing: false, leading: true},
Expand All @@ -176,7 +168,7 @@ export class CTimeline {
/**
* Take entries that are input and transform them into entries the calendar can use
*/
public transformEntries = _.throttle(() => {
public transformEntries = _.throttle(async () => {
const zoomLevelData = ZOOM_LEVELS[this.zoomLevel];
const pxPerMinute = BLOCK_HEIGHT / zoomLevelData.minutes;
const [startTime, endTime] = this.getDayStartEndTimes();
Expand All @@ -190,34 +182,48 @@ export class CTimeline {
});

if (this.timeView === 'day') {
this.transformedEntries = this.mapEntries(sortedEntries, pxPerMinute, startTime, endTime);
return;
const dayEntries = await filterEntriesDay(sortedEntries, startTime.toISOString(), endTime.toISOString());
this.transformedEntries = await mapEntries(
dayEntries,
pxPerMinute,
startTime.toISOString(),
endTime.toISOString(),
this.timeView,
this.editEntryViewModel,
this.date,
);
}

if (this.timeView === 'week') {
_.forEach(this.displayDays, day => {
const clonedSort = _.cloneDeep(sortedEntries);

const dayEntries = _.filter(clonedSort, entry => {
const entryStart = this.date ? entry.start : moment(entry.start, 'hmm').toISOString();
const entryEnd = moment(entryStart)
.add(entry.duration, 'seconds')
.toISOString();

const startIn = moment(entryStart).isBetween(startTime, endTime, null, '[]');
const endIn = moment(entryEnd).isBetween(startTime, endTime, null, '[]');

return startIn || endIn;
});

day.entries = this.mapEntries(dayEntries, pxPerMinute, startTime, endTime);
for (const day of this.displayDays) {
const dayEntries = await filterEntriesDay(
sortedEntries,
startTime.toISOString(),
endTime.toISOString(),
);
day.entries = await mapEntries(
dayEntries,
pxPerMinute,
startTime.toISOString(),
endTime.toISOString(),
this.timeView,
this.editEntryViewModel,
this.date,
);

startTime.add(1, 'day');
endTime.add(1, 'day');
});

return;
}
}

this.taskQueue.queueMicroTask(() => {
this.isRendering = false;

// Could potentially be 350ms behind with the combined throttles
_.delay(() => {
this.scrollToSpot();
}, 400);
});
}, 100);

private calculateCurrentTimeLine = _.throttle(
Expand Down Expand Up @@ -727,124 +733,4 @@ export class CTimeline {

return [startTime, endTime];
}

/**
* Map a day of sorted entries to the ITimeEntry type
*
* @param sortedEntries any[]: Array of entries sorted by start time
* @param pxPerMinute number: The number of pixels it takes to display a minute
* @param startTime Moment: start time of the day
* @param endTime Moment: end time of the day
*/
private mapEntries(sortedEntries: any[], pxPerMinute: number, startTime, endTime): ITimeEntry[] {
let nestedEntryWidth = 80;

return _.map(
sortedEntries,
(entry: ITimeEntry, index: number): ITimeEntry => {
if (!this.date) {
entry.start = moment(entry.start, 'hmm').toISOString();
}

entry.startTime = moment(entry.start).format('HH:mm');

if (!entry.end) {
entry.end = moment(entry.start)
.add(entry.duration, 'seconds')
.toISOString();
}

entry.endTime = moment(entry.end).format('HH:mm');

let diff = moment(entry.start).diff(startTime, 'seconds');
const diffEnd = moment(endTime).diff(entry.end, 'seconds');

// If entry starts before the day make sure it only displays what it needs
if (diff < 0) {
entry.start = moment(startTime).toISOString();
entry.duration += diff;
diff = 0;
}

// If entry ends after the day make sure it only displays what it needs
if (diffEnd < 0) {
entry.duration += diffEnd;
}

entry.top = (diff / SECONDS_IN_MINUTE) * pxPerMinute + 1;
entry.height = (entry.duration / SECONDS_IN_MINUTE) * pxPerMinute;

if (this.editEntryViewModel) {
entry.contentViewModel = this.editEntryViewModel;
}

const width = this.timeView === 'week' ? 30 : 60;
const nextEntries = sortedEntries.slice(index + 1);

const sameTimeEntries = _.filter(nextEntries, filterEntry => {
if (!filterEntry.end) {
filterEntry.end = moment(filterEntry.start)
.add(filterEntry.duration, 'seconds')
.toISOString();
}

// Not the same time if it's later in the minute. Gives a buffer
return (
moment(entry.start).isSame(filterEntry.start, 'minute') &&
!moment(filterEntry.start).isAfter(entry.end, 'second')
);
});

const nestedEntries = _.filter(nextEntries, filterEntry => {
if (!filterEntry.end) {
filterEntry.end = moment(filterEntry.start)
.add(filterEntry.duration, 'seconds')
.toISOString();
}

// 10 second buffer
return moment(filterEntry.start).isBetween(
entry.start,
moment(entry.end).subtract(10, 'seconds'),
null,
'()',
);
});

const nestedEntry = _.first(nestedEntries); // We only care about the first one

if (!entry.widthCalc && sameTimeEntries.length) {
const totalSameTime = sameTimeEntries.length + 1;
const entryWidth = 100 / totalSameTime;

// We don't want to use these props if we are showing multiple things at the same time
entry.sizeDay = null;
entry.sizeWeek = null;

entry.widthCalc = `calc(${entryWidth}% - ${width / totalSameTime}px - 5px)`;

_.forEachRight(sameTimeEntries, (sameTimeEntry, sameTimeIndex) => {
const offsetIndex = sameTimeIndex + 1;

// We don't want to use these props if we are showing multiple things at the same time
sameTimeEntry.small = null;
sameTimeEntry.expandable = null;

sameTimeEntry.widthCalc = `calc(${entryWidth}% - ${width / totalSameTime}px - 5px)`;
sameTimeEntry.rightCalc = `calc(${offsetIndex * entryWidth}% - ${(width / totalSameTime) *
offsetIndex}px)`;
});
} else if (nestedEntry) {
if (nestedEntryWidth >= 40) {
nestedEntry.widthCalc = `calc(${nestedEntryWidth}% - ${width}px)`;
nestedEntryWidth -= 20;
} else {
nestedEntryWidth = 80; // Reset
}
}

return entry;
},
);
}
}
86 changes: 86 additions & 0 deletions src/components/timeline/c-timeline/workers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as moment from 'moment';

import {filterEntriesDay, mapEntries} from './workers';

// Plop it right in the middle of the day
const now = moment('12/12/2020', 'MM/DD/YYYY')
.startOf('day')
.add(12, 'hours');
const startDay = moment(now)
.startOf('day')
.toISOString();
const endDay = moment(now)
.endOf('day')
.toISOString();

const sortedEntries: any[] = [
{
duration: 240,
start: now.toISOString(),
},
{
duration: 120,
start: moment(now)
.add(3, 'minutes')
.toISOString(),
},
{
duration: 120,
start: moment(now)
.add(1, 'hour')
.toISOString(),
},
{
duration: 120,
start: moment(now)
.add(1, 'hour')
.toISOString(),
},
{
duration: 120,
start: moment(now)
.add(1, 'day')
.toISOString(),
},
];

describe('Web worker functions', () => {
describe('#filterEntriesDay', () => {
it('tests filtering entries for a day', async () => {
const data = await filterEntriesDay(sortedEntries, startDay, endDay);

expect(data.length).toBe(4);
});
});

describe('#mapEntries', () => {
it('tests formatting', async () => {
const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());

expect(data[0].startTime).toBe('12:00');
expect(data[0].endTime).toBe('12:04');
});

it('tests positioning', async () => {
const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());

expect(data[0].top).toBe(1441);
expect(data[0].height).toBe(8);
});

it('tests same time entries', async () => {
const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());

expect(data[0].widthCalc).toBeUndefined();
expect(data[2].widthCalc).toBeDefined();
expect(data[3].widthCalc).toBeDefined();
});

it('tests nested entries', async () => {
const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString());

expect(data[0].widthCalc).toBeUndefined();
expect(data[1].widthCalc).toBeDefined();
});
});
});
Loading

0 comments on commit c78cea9

Please sign in to comment.