How to spread divs around without overlapping each other

I am creating an event planner, and I realized that I can’t determine how events spread around without overlapping each other. (It can have events at the same time and without restrictions. When possible, use 100% of the available width )

Here is a picture of this scenario. enter image description here

Some considerations:

  • Events are completed inside the div using position: relative , and all events have position:absolute .

  • Using javascript, I have to figure out what should be the value of top left width and height each "div event" on the fly.

  • Events are an array of objects, such as the code below:

    {startAt: "12:00:30", endsAt: "13:00:00", description: "evt1", id: '00001'}

  • I am using Vue.js to develop this project. But this is not a problem if you do not know Vue. I created a small project using jsbin so you can just play with the javascript function.

Live Code: https://jsbin.com/bipesoy/

Where is my problem?

I cannot find an algorithm to calculate top left width and height on the fly based on an array of events.

Some thoughts on jsbin code:

  • All code to find 4 properties above is inside the parsedEvents function
  • Inside parsedEvents you can access an array of events using: this.events
  • The task of parsedEvents is to loop through an array of events and add a style property to each of them, then return a new array of events with a stylish object.
  • Every 30 minutes have a height of 40 pixels;

Any ideas how to do this or a better solution?

+5
source share
2 answers

After some time playing with this challenge, I think it is time to abandon this idea. You can program many possible scenarios for organizing events, but when you dig deep, you realize that it’s very difficult to even write what exactly you want to do, and even if you can manage, some of your decisions just do not look good on the screen. This only happens when the events expand in width. Positioning and even re-positioning them to fill in the gaps is allowed and not too difficult.

The snippet is here (or JSBin if you prefer: http://jsbin.com/humiyi/99/edit?html,js,console,output ).

Green events are detected as "extensible." Grays cannot expand. To clarify problems with extension logic, some examples:

  • evt 1 and evt3? it may be so, or evt3 goes right and they both expand
  • evt7 and evt12? Many ways to expand this ... how to define a rule?
  • Imagine evt7 and evt11 ar combined into one big event. How to expand evt10, evt7 / 11 and evt 12? ... now try writing rules to consistently respond to above 3 (and many other possible scenarios are not in this example)

My conclusion is that writing rules and developing it is not worth the effort. UI usability will not be much. In some scenarios, they will even be lost, i.e. Some events will be visually larger only because they have a place, and not because they are more important.

I would suggest a layout that is similar or exactly the same as in the example. Events simply do not expand. I don’t know how many daily events you expect, a real-life scenario, but the only update I would potentially do is split the vertical calendar in individual regions - a point in time that does not overlap with any event, like the line between evt7 and evt11 in the example. Then run the same script for the region independently. This will recalculate the vertical intervals for each region, so the area with evt10 and evt11 will have only 2 vertical slots filling the space of 50% each. Perhaps it's worth it if the calendar has few crowded hours and only a few events later / earlier. This will solve the problem of too narrow events later that day, without spending a lot of time. But if all-day events overlap, 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> 
+3
source

thought about it more. I covered the width, height and top. But you may have trouble calculating left . So a little change ... the goal of creating counters for every 30 minute increment is still there, but instead of iterating over the increments and filter elements, do it the other way around. Scroll through the elements and fill this counter object from there. It will also be faster. While in this loop, do not just save the counter for the counter object, but also push itemID to another property of the same object (same ascending). Thus, each gain can be represented as {span: "17:00", counter: 2, items: [135, 148]}. This items array will later help you determine the left position of each item and avoid horizontal overlap. Since the maximum value of an element in the items array is equal to all the positions of the element = the position of the element in the calendar on the left. Multiply it by the width (-1) and you have left .

0
source

Source: https://habr.com/ru/post/1262642/


All Articles