Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New algorithm for day view layout #677

Merged
merged 2 commits into from
Jan 11, 2018

Conversation

tobiasandersen
Copy link
Collaborator

This PR replaces the old day view layout algorithm with a new one. There are some problems with the old one that are quite hard to solve (e.g. #512). The render order is also quite messed up. I did my best to understand how Google render events in their (old) calendar. I think I got pretty close (they have since updated Google Calendar with a new layout, along with a new algorithm).

This new layout should fix #512 and #649. It's also more space efficient, and renders events in a more logical order (no need to keep track of different z-indexes for overlapping events, which was the only solution in the old version). The whole algorithm also makes a lot more sense than the old one, and is easier to follow/read (at least to me).

I've also introduced a 10px wide empty space column so that new events can always be created on a slot (same way google does it):

empty-space

I'm sure this is far from perfect and still has bugs, but it should be a pretty big improvement to the old one. The master branch is currently a bit broken though, so I haven't tested this with the latest code, so we should probably hold off on merging this until master is fixed and this has been tested. I'd welcome anyone to try it out before we merge though!

For anyone trying and finding problems, please post the relevant data set here!

@jquense
Copy link
Owner

jquense commented Jan 7, 2018

🎉🎉 awesome! This looks great. What's wrong with master at the moment? (did I forget to fix something??)

Copy link
Owner

@jquense jquense left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great 👍 i haven't had a chance to look over it in a ton of detail yet, but i'm really excited at how this is turning out :)

feel free to merge at when you're confident in it


<div className={cn('rbc-events-container', { rtl: this.props.rtl })}>
{this.renderEvents()}
</div>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to handle a tweak to DnD to account for the new div? maybe not

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll test the DnD stuff before merging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only tested some basic DnD in the examples, and it worked as far as I can tell.

height: 100%;
position: absolute;
top: 0;
width: 100%;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any particular reason for this set of styles to accomplish this? as opposed to like

position: absolute;
top: 0, bottom: 0: left: 0; right 10px;

i'm not saying the above is better/worse just curious

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly don't remember, I think I added this a few months back. I'll look over it...

return Math.min(diff, total)
}

export class Event {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like many of the getters here are essentially readonly at object construction? maybe it'd be worth doing some of the calculations then so they aren't run more than once? Does data change? Maybe it's over optimization, but it might simplify the compiled code if nothing else

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's definitely possible, I haven't really done any sort of optimizations yet...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should probably do this before merging though since it's an easy change 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a really good call, I even ended up removing the extra MultiDayEvent class.

@jquense
Copy link
Owner

jquense commented Jan 7, 2018

Also @taion if you have second I'd appreciate your expertise, if you could take a quick look on the layout algorithm code w/r/t complexity, efficiency, etc. The scale here is possibly 100s of events but probably mostly <50

@taion
Copy link

taion commented Jan 8, 2018

Well, for dev at least, we'd need to upgrade R-B to work with React 16

Copy link

@taion taion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sec, issue comment to follow

.sort((a, b) => {
if (a.start === b.start) {
if (a.end === b.end) {
return events.indexOf(a) - events.indexOf(b)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be expensive if it gets hit a lot. I see that Lodash is already a dep here, so maybe use sortBy there, as that thing is a stable sort.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!


const sorted = []
while (sortedByTime.length > 0) {
const [event] = sortedByTime.splice(0, 1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just

const event = sortedByTime.shift()

?

it's faster to remove things from end rather than start of array, but shouldn't matter if it's only 100s

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha yes should definitely use shift() instead (it didn't always splice from index 0 before).

const sorted = []
while (sortedByTime.length > 0) {
const [event] = sortedByTime.splice(0, 1)
event && sorted.push(event)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can event be falsy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't, I'll remove that check...

continue
}

// We've found the first event of the next event group.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic seems a little weird. Suppose you have the following hour-long events, respectively at:

  • 09:30
  • 10:00
  • 15:00

The sorting would give:

  • 09:30
  • 15:00
  • 10:00

But if you had:

  • 09:30
  • 10:00
  • 13:00
  • 15:00

The sorting would give:

  • 09:30
  • 13:00
  • 10:00
  • 15:00

This might be innocuous, but in a vacuum, it looks a little odd.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it seems a bit weird, even more so now when their new algorithm doesn't do it like this, but up until a few weeks ago google actually did it this way (and I tried to just copy their behavior).

const event = eventsInRenderOrder[i]

// Check if this event can go into a container event.
const container = containerEvents.find(c => c.endSlot >= event.endSlot)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like it might be inefficient

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'll see if I can simplify it.

@taion
Copy link

taion commented Jan 8, 2018

Assuming I didn't mess something up, this doesn't work quite right for overlapping events. For:

    [
      {
        title: 'test',
        start: new Date(y, m, d, 10, 0, 0, 0),
        end: new Date(y, m, d, 11, 0, 0, 0),
      },
      {
        title: 'test 2',
        start: new Date(y, m, d, 10, 30, 0, 0),
        end: new Date(y, m, d, 11, 30, 0, 0),
      },
    ],

I get:
image

GCal gives:
image

@taion
Copy link

taion commented Jan 8, 2018

The Google algorithm is a bit simpler, as far as I can tell:
image

It goes something like:

  1. Sort all events by start time and then end time
  2. For each event in order, render it in the first "column" where it would fit
  3. After laying everything out, go through each column and increment the X offsets such that event titles don't get occluded too much

Some more examples:
image
image
image
image
image

@taion
Copy link

taion commented Jan 8, 2018

As an edge case example, see
image

Note the "***" event is rendered in column 4. A non-greedy algorithm could probably render it "better" (i.e. to the left of its actual parent), but this is pretty straightforward.

@taion
Copy link

taion commented Jan 8, 2018

To demonstrate the greedy nature:
image

In this case it would definitely be better to render the "***" event to the left, but the greedy algorithm isn't cost-sensitive

@tobiasandersen
Copy link
Collaborator Author

Hey thanks a lot @taion, this is awesome!

I've barely looked at how google's new algorithm works, but I'll see if it seems straightforward enough for me to update.

This test case you provided:

    [
      {
        title: 'test',
        start: new Date(y, m, d, 10, 0, 0, 0),
        end: new Date(y, m, d, 11, 0, 0, 0),
      },
      {
        title: 'test 2',
        start: new Date(y, m, d, 10, 30, 0, 0),
        end: new Date(y, m, d, 11, 30, 0, 0),
      },
    ],

shouldn't be rendered like that though, so I'll have a look at it later today.

Again, really appreciate your help!

@zackify
Copy link

zackify commented Jan 9, 2018

Can't wait for this to be merged, amazing work :)

.rbc-event {
border: 1px solid @event-border;
display: flex;
max-height: 100%;
min-height: 20px;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Events smaller than this looks really weird. Is it okay to add this here?

@tobiasandersen
Copy link
Collaborator Author

I've addressed most of your and @taion's comments. But I kept the same algorithm (where I try to mimic google's old one) - I don't have the time or energy to do it all over again at this point. If anyone else wants to take a stab at the newer algorithm, please do!

As far as I can tell, this should be good enough to merge now. And as stated above, there're probably (definitely) problems with this one as well, but I'm pretty confident in this being better than what we have now.

@taion
Copy link

taion commented Jan 11, 2018

I don't understand. The classic Google calendar uses the same layout algorithm:
image

It handles event widths a little differently, and it doesn't "de-offset" things that don't overlap a title, but it follows (1) and (2) in the greedy algorithm I describe in #677 (comment) just the same.

@tobiasandersen
Copy link
Collaborator Author

Yes their algorithm definitely is better than what I've done here. For this example I think mine renders it ok though:

screen shot 2018-01-11 at 10 35 01

But I'm sure one can find other examples where the differences are more striking and problematic.

I know you know what you're talking about, so I'm not arguing here, but to me (2) isn't obvious. I'd love to fix/improve it, but I'm not going to have a lot of time the following weeks, and it seems like a pretty fundamental change is required. If anyone wants to pick this up from here, I really don't mind. And if @jquense wants to leave this here until I, or someone else, have the time to get back to it, that's also fine!

@jquense
Copy link
Owner

jquense commented Jan 11, 2018

I think this strictly an improvement over what we have so I say we merge it. For anyone that hits edge cases and may want to improve it this thread is a good resource. Thanks y'all

@tobiasandersen tobiasandersen merged commit 0b2c234 into jquense:master Jan 11, 2018
@tobiasandersen tobiasandersen deleted the new-dayview-algo branch January 11, 2018 13:14
@jadczyk
Copy link
Contributor

jadczyk commented Jan 14, 2018

Hi,
I ran into an issues with this version: the algorithm considers 2 events as overlapping if one ends when the other starts. I changed the comparison here for my needs:

src/utils/dayViewLayout/index.js
    const container = containerEvents.find(c => c.endSlot > event.startSlot)

Not sure how you wanted to treat this case.

@tobiasandersen
Copy link
Collaborator Author

You're definitely right! Care to make a PR? :)

@zackify
Copy link

zackify commented Jan 17, 2018

Any plans for a new release on npm soon? Also will this fix events that have the exact same start and end time being not viewable? or only fix ones that are slightly staggered. thanks again :)

@tobiasandersen
Copy link
Collaborator Author

tobiasandersen commented Jan 17, 2018

That should be fixed here! About the release, I personally feel like there're a few minor issues left to fix on master before we're ready for a release. E.g:

But once those are fixed, I think we should publish a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Event shown on top of another Event (hiding the one below)
5 participants