I'm building an event scheduler and I've gotten into a point that I can't figure out a way to spread events around without overlapping each other. (It might have events at the same time and without limits. Whenever is possible, use 100% of the available width
)
Here it is a picture of this scenario.
Some considerations:
The events is wrapped inside a div with position: relative
and all the events has position:absolute
.
Using javascript, I have to figure out what needs to be the value of top
left
width
and height
of each "div event" on the fly.
Events are an array of objects like the code below:
{ startAt: "12:00:30", endsAt: "13:00:00", description: "evt1", id: '00001' }
I'm using Vue.js to develop this project. But this is not an issue if you don't know Vue. I've build a small project using jsbin so you can just play around with a javascript function.
Live Code: https://jsbin.com/bipesoy/
Where I'm having problem?
I can't find a algorithm to calculate the top
left
width
and height
on the fly based on an array of events.
Some considerations about the jsbin code:
parsedEvents
parsedEvents
you can access the array of events using: this.events
parsedEvents
is loop through the array of events and add the style propertie to each one and then return a new array of events with the style object.Any ideas how to accomplish it or a better solution?
After some time playing with this challenge I think it's time to give up from the idea. It is possible to program many possible scenarios of arranging events, but when you dig deep you realize it's very difficult to even write what exactly you want to be done and even if you manage, some of your decisions just don't look good on the screen. This is only with expanding events in width. Positioning and even re-positioning them to fill gaps is solved and not too difficult.
Snippet is here (or JSBin if you prefer: http://jsbin.com/humiyi/99/edit?html,js,console,output).
Green events are detected as "expandable". Grey o nes cannot expand. To clarify problems with logic of expanding, some examples:
My conclusion is that writing rules and developing this is not worth the effort. UI usability will not get much. They will even loose in some scenarios, i.e. some events would be visually bigger just because they have space and not because they're more important.
I'd suggest layout similar or exactly the same as the one in example. Events just don't expand. I don't know how much daily events you expect, what's real life scenario, but only upgrade I'd potentially do is to split vertically calendar in separated regions - point in time which is not overlapping with any event, like line between evt7 and evt11 in example. Then run this same script per region independently. That will recalculate vertical slots per region so region with evt10 i evt11 will have only 2 vertical slots filling space 50% each. This maybe be worth it if your calendar has few crowded hours and only a few events later/before. This would fix an issue of too narrow events later in the day without spending much time. But if events are all over the day and overlapping alot I don't think it's worth it.
let events = [
{ startAt: "00:00", endsAt: "01:00", description: "evt1", id: '00001' },
{ startAt: "01:30", endsAt: "08:00", description: "evt2", id: '00002' },
{ startAt: "01:30", endsAt: "04:00", description: "evt3", id: '00003' },
{ startAt: "00:30", endsAt: "02:30", description: "evt3", id: '00013' },
{ startAt: "00:00", endsAt: "01:00", description: "evt3", id: '00014' },
{ startAt: "03:00", endsAt: "06:00", description: "evt4", id: '00004' },
{ startAt: "01:30", endsAt: "04:30", description: "evt5", id: '00005' },
{ startAt: "01:30", endsAt: "07:00", description: "evt6", id: '00006' },
{ startAt: "06:30", endsAt: "09:00", description: "evt7", id: '00007' },
{ startAt: "04:30", endsAt: "06:00", description: "evt8", id: '00008' },
{ startAt: "05:00", endsAt: "06:00", description: "evt9", id: '00009' },
{ startAt: "09:00", endsAt: "10:00", description: "evt10", id: '00010' },
{ startAt: "09:00", endsAt: "10:30", description: "evt11", id: '00011' },
{ startAt: "07:00", endsAt: "08:00", description: "evt12", id: '00012' }
]
console.time()
// will store counts of events in each 30-min chunk
// each element represents 30 min chunk starting from midnight
// ... so indexOf * 30 minutes = start time
// it will also store references to events for each chunk
// each element format will be: { count: <int>, eventIds: <array_of_ids> }
let counter = []
// helper to convert time to counter index
time2index = (time) => {
let splitTime = time.split(":")
return parseInt(splitTime[0]) * 2 + parseInt(splitTime[1])/30
}
// loop through events and fill up counter with data
events.map(event => {
for (let i = time2index(event.startAt); i < time2index(event.endsAt); i++) {
if (counter[i] && counter[i].count) {
counter[i].count++
counter[i].eventIds.push(event.id)
} else {
counter[i] = { count: 1, eventIds: [event.id] }
}
}
})
//find chunk with most items. This will become number of slots (vertical spaces) for our calendar grid
let calSlots = Math.max( ...counter.filter(c=>c).map(c=>c.count) ) // filtering out undefined elements
console.log("number of calendar slots: " + calSlots)
// loop through events and add some more props to each:
// - overlaps: all overlapped events (by ref)
// - maxOverlapsInChunk: number of overlapped events in the most crowded chunk
// (1/this is maximum number of slots event can occupy)
// - pos: position of event from left (in which slot it starts)
// - expandable: if maxOverlapsInChunk = calSlot, this event is not expandable for sure
events.map(event => {
let overlappedEvents = events.filter(comp => {
return !(comp.endsAt <= event.startAt || comp.startAt >= event.endsAt || comp.id === event.id)
})
event.overlaps = overlappedEvents //stores overlapped events by reference!
event.maxOverlapsInChunk = Math.max( ...counter.filter(c=>c).map(c=>c.eventIds.indexOf(event.id) > -1 ? c.count : 0))
event.expandable = event.maxOverlapsInChunk !== calSlots
event.pos = Math.max( ...counter.filter(c=>c).map( c => {
let p = c.eventIds.indexOf(event.id)
return p > -1 ? p+1 : 1
}))
})
// loop to move events leftmost possible and fill gaps if any
// some expandable events will stop being expandable if they fit gap perfectly - we will recheck those later
events.map(event => {
if (event.pos > 1) {
//find positions of overlapped events on the left side
let vertSlotsTakenLeft = event.overlaps.reduce((result, cur) => {
if (result.indexOf(cur.pos) < 0 && cur.pos < event.pos) result.push(cur.pos)
return result
}, [])
// check if empty space on the left
for (i = 1; i < event.pos; i++) {
if (vertSlotsTakenLeft.indexOf(i) < 0) {
event.pos = i
console.log("moving " + event.description + " left to pos " + i)
break
}
}
}
})
// fix moved events if they became non-expandable because of moving
events.filter(event=>event.expandable).map(event => {
let leftFixed = event.overlaps.filter(comp => {
return event.pos - 1 === comp.pos && comp.maxOverlapsInChunk === calSlots
})
let rightFixed = event.overlaps.filter(comp => {
return event.pos + 1 === comp.pos && comp.maxOverlapsInChunk === calSlots
})
event.expandable = (!leftFixed.length || !rightFixed.length)
})
//settings for calendar (positioning events)
let calendar = {width: 300, chunkHeight: 30}
// one more loop through events to calculate top, left, width and height
events.map(event => {
event.top = time2index(event.startAt) * calendar.chunkHeight
event.height = time2index(event.endsAt) * calendar.chunkHeight - event.top
//event.width = 1/event.maxOverlapsInChunk * calendar.width
event.width = calendar.width/calSlots // TODO: temporary width is 1 slot
event.left = (event.pos - 1) * calendar.width/calSlots
})
console.timeEnd()
// TEST drawing divs
events.map(event => {
$("body").append(`<div style="position: absolute;
top: ${event.top}px;
left: ${event.left}px;
width: ${event.width}px;
height: ${event.height}px;
background-color: ${event.expandable ? "green" : "grey"};
border: solid black 1px;
">${event.description}</div>`)
})
//console.log(events)
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
</body>
</html>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With