4

Good day people!

I have run into an issue with my simple single day calendar script.

I've been tasked to create a single day calendar, which shows each block hour from 9am to 6pm. If an event overlaps another, they should equal the same width and not overlap. I have managed to achieve this for two events, however if more than two overlap, things go abit south, I need help figuring out a method to fix this where any number of events overlap, their widths will equal the same.

Events are rendered on the calendar using a global function:

renderDay([{start: 30, end: 120},{start: 60, end: 120}])

which takes an array of objects as an argument, where the integers are the number of minutes pasted from 9am. eg. 30 is 9:30am, 120 is 11am

here is the collision function I took from stackoverflow

// collision function to return boolean
// attribute: http://stackoverflow.com/questions/14012766/detecting-whether-two-divs-overlap
function collision($div1, $div2) {
  let x1 = $div1.offset().left;
  let y1 = $div1.offset().top;
  let h1 = $div1.outerHeight(true);
  let w1 = $div1.outerWidth(true);
  let b1 = y1 + h1;
  let r1 = x1 + w1;
  let x2 = $div2.offset().left;
  let y2 = $div2.offset().top;
  let h2 = $div2.outerHeight(true);
  let w2 = $div2.outerWidth(true);
  let b2 = y2 + h2;
  let r2 = x2 + w2;

  if (b1 < y2 || y1 > b2 || r1 < x2 || x1 > r2) return false;
  return true;
}

I run a loop on all the event divs which I want to check for overlaps

// JQuery on each, check if collision
$('.event').each(function(index, value) {

      // statement to break out on final loop
      if(index === $('.event').length - 1) return;

      console.log('at index: ', index);

      // if collison === true, halve width of both event divs, re-position
      if(collision(  $('#e-'+index) , $('#e-'+(index + 1)) )) {

        $('#e-'+index).css('width', $('#e-'+index).width() / 2);
        $('#e-'+(index+ 1)).css('width', $('#e-'+(index+ 1)).width() / 2).css('left', $('#e-'+(index + 1)).width());

        if(collision)
      }
    })
  }
})

Screenshots to help visualize :)

When two overlap, they have equal widths

When three or more overlap, things go wrong

Any help would be greatly appreciated! DW

douglaswissett
  • 231
  • 1
  • 4
  • 14
  • 1
    When two event elements collide, you are halving its width and that of the next event element. So in your second screenshot where you have 3 event elements, the first 2 are halved while the third is half of a half which is a quarter. You need to count the number of elements which collide and then set all elements to the same width which is an equal percentage of the parent width. Can you provide a working example of your code? – Jaydo Apr 21 '16 at 09:02

2 Answers2

1

After looking at the code, it seems overly complex to check the rendered elements for collision when you can work this out from the start and end times.

The way I've done it, is to group events which collide in arrays like so:

let collisions = [
      // only 1 event in this array so no collisions
      [{
        start: 30,
        end: 120
      }],
      // 3 events in this array which have overlapping times
      [{
        start: 300,
        end: 330
      }, {
        start: 290,
        end: 330
      }, {
        start: 300,
        end: 330
      }]
    ];

Then we iterate through each group of collisions, and create the elements with the appropriate width and positioning.

for (var i = 0; i < collisions.length; i++) {
  var collision = collisions[i];

  for (var j = 0; j < collision.length; j++) {
    var event = collision[j];

    let height = event.end - event.start;
    let top = event.start + 50;

    // 360 = max width of event
    let width = 360 / collision.length;

    // a lot of this could be moved into a css class
    // I removed the "display: inline-block" code because these are absolutely positioned. Replaced it with "left: (j * width)px"
    let div = $(`<div id=${'e-'+ (i + j)}>`).css('position', 'absolute').css('top', top)
    .css('height', height).css('width', width).css('left', (j * width) + 'px')
    .css('backgroundColor', arrayOfColors.shift()).addClass('event')
    .text('New Event').css('fontWeight', 'bold');

    // append event div to parent container
    $('#events').append(div);
  }
}

//**********************************************************************
//
//  TITLE     - Thought Machine Coding Challenge, Single Day Calendar
//  AUTHOR    - DOUGLAS WISSETT WALKER
//  DATE      - 21/04/2016
//  VERSION   - 0.0.3
//  PREVIOUS  - 0.0.2
//
//**********************************************************************

let arr = [{
  start: 30,
  end: 120
}, {
  start: 70,
  end: 180
}, {
  start: 80,
  end: 190
}, {
  start: 300,
  end: 330
}, {
  start: 290,
  end: 330
}, {
  start: 220,
  end: 260
}, {
  start: 220,
  end: 260
}, {
  start: 220,
  end: 260
}, {
  start: 220,
  end: 260
}, {
  start: 400,
  end: 440
}, {
  start: 20,
  end: 200
}];

let renderDay;
$(document).ready(() => {

  renderDay = function(array) {
    $('.event').each(function(i, el) {
      $(el).remove();
    });

    // background colors for events
    let arrayOfColors = [
      'rgba(255, 153, 153, 0.75)',
      'rgba(255, 204, 153, 0.75)',
      'rgba(204, 255, 153, 0.75)',
      'rgba(153, 255, 255, 0.75)',
      'rgba(153, 153, 255, 0.75)',
      'rgba(255, 153, 255, 0.75)'
    ]

    let collisions = mapCollisions(array);

    let eventCount = 0; // used for unique id
    for (let i = 0; i < collisions.length; i++) {
      let collision = collisions[i];

      for (let j = 0; j < collision.length; j++) {
        let event = collision[j];

        let height = event.end - event.start;
        let top = event.start + 50;

        // 360 = max width of event
        let width = 360 / collision.length;

        // a lot of this could be moved into a css class
        // I removed the "display: inline-block" code because these are absolutely positioned
        // Replaced it with "left: (j * width)px"
        let div = $("<div id='e-" + eventCount + "'>").css('position', 'absolute').css('top', top)
          .css('height', height).css('width', width).css('left', (j * width) + 'px')
          .css('backgroundColor', arrayOfColors.shift()).addClass('event')
          .text('New Event').css('fontWeight', 'bold');

        eventCount++;
        // append event div to parent container
        $('#events').append(div);
      }
    }
  }

  renderDay(arr);
});

// Sorry this is pretty messy and I'm not familiar with ES6/Typescript or whatever you are using
function mapCollisions(array) {
  let collisions = [];

  for (let i = 0; i < array.length; i++) {
    let event = array[i];
    let collides = false;

    // for each group of colliding events, check if this event collides
    for (let j = 0; j < collisions.length; j++) {
      let collision = collisions[j];

      // for each event in a group of colliding events
      for (let k = 0; k < collision.length; k++) {
        let collidingEvent = collision[k]; // event which possibly collides

        // Not 100% sure if this will catch all collisions
        if (
          event.start >= collidingEvent.start && event.start < collidingEvent.end || event.end <= collidingEvent.end && event.end > collidingEvent.start || collidingEvent.start >= event.start && collidingEvent.start < event.end || collidingEvent.end <= event.end && collidingEvent.end > event.start) {
          collision.push(event);
          collides = true;
          break;
        }

      }

    }

    if (!collides) {
      collisions.push([event]);
    }
  }

  console.log(collisions);
  return collisions;
}
html,
body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
}
#container {
  height: 100%;
  width: 100%;
}
#header-title {
  text-align: center;
}
#calendar {
  width: 400px;
  height: 620px;
  margin-top: 70px;
}
#events {
  position: absolute;
  top: 80px;
  left: 100px;
  width: 800px;
  height: 620px;
}
.event {
  box-shadow: 0 0 20px black;
  border-radius: 5px;
}
.hr-block {
  border-top: 2px solid black;
  height: 58px;
  margin: 0;
  padding: 0;
  margin-left: 100px;
  min-width: 360px;
  opacity: .5;
}
.hr-header {
  position: relative;
  top: -33px;
  left: -68px;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="css/styles.css">
  <link rel="stylesheet" href="css/responsive.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>

  <script charset="UTF-8" src="js/moment.js"></script>
  <script charset="UTF-8" src="js/script2.js"></script>
  <title>Thought Machine Code Challenge</title>
</head>

<body>

  <div id="container">
    <div class="header">
      <h1 id="header-title"></h1>
    </div>
    <div id="calendar">
      <div class="hr-block">
        <h2 class="hr-header">09:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">10:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">11:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">12:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">13:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">14:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">15:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">16:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">17:00</h2>
      </div>
      <div class="hr-block">
        <h2 class="hr-header">18:00</h2>
      </div>
    </div>
  </div>

  <div id="events">

  </div>

  <script>
    document.getElementById("header-title").innerHTML = moment().calendar();
  </script>
</body>

</html>
Jaydo
  • 1,830
  • 2
  • 16
  • 21
  • 1
    Jaydo, Thanks for taking the time to answer my question :) Using the event object times to calculate whether an overlap has occurred seems a much more fool proof method to approach this. Thanks! – douglaswissett Apr 23 '16 at 08:41
  • @DWW Happy to help. I enjoy a challenge. Also, I was pretty tired when I wrote this and there were a couple of mistakes which I've fixed now. The ids generated for the divs weren't unique (don't know how I ever though `i + j` would work). The event collision check wouldn't have caught collisions if `collidingEvent` started before and ended after the `event` being checked. – Jaydo Apr 23 '16 at 08:56
0

working index, script and css files

//**********************************************************************
//
//  TITLE     - Thought Machine Coding Challenge, Single Day Calendar
//  AUTHOR    - DOUGLAS WISSETT WALKER
//  DATE      - 21/04/2016
//  VERSION   - 0.0.3
//  PREVIOUS  - 0.0.2
//
//**********************************************************************


let arr = [{start: 30, end: 120},{start: 300, end: 330},{start: 290, end: 330}];

let renderDay;
$(document).ready(() => {

  renderDay = function(array) {
    $('.event').each(function(i, el) {
      $(el).remove();
    });

    // background colors for events
    let arrayOfColors = [
      'rgba(255, 153, 153, 0.75)',
      'rgba(255, 204, 153, 0.75)',
      'rgba(204, 255, 153, 0.75)',
      'rgba(153, 255, 255, 0.75)',
      'rgba(153, 153, 255, 0.75)',
      'rgba(255, 153, 255, 0.75)'
    ]

    // iterate through each event time
    array.forEach((eventTimes, index) => {

      // define event height and top position on calendar
      let height = eventTimes.end - eventTimes.start;
      let top = eventTimes.start + 50;

      // max width of event
      let width = 360;

      // create event div
      let div = $(`<div id=${'e-'+index}>`).css('position', 'absolute').css('top', top)
                  .css('height', height).css('width', width).css('display', 'inline-block')
                  .css('backgroundColor', arrayOfColors.shift()).addClass('event')
                  .text('New Event').css('fontWeight', 'bold');
      // append event div to parent container
      $('#events').append(div);
    })

    // JQuery on each, check if collision
    $('.event').each(function(index, value) {

      // statement to break out on final loop
      if(index === $('.event').length - 1) return;

      console.log('at index: ', index);

      // if collison === true, halve width of both event divs, re-position
      if(collision(  $('#e-'+index) , $('#e-'+(index + 1)) )) {

        $('#e-'+index).css('width', $('#e-'+index).width() / 2);
        $('#e-'+(index+ 1)).css('width', $('#e-'+(index+ 1)).width() / 2).css('left', $('#e-'+(index + 1)).width());

      }
    })
  }
})


// collision function to return boolean
// attribute: http://stackoverflow.com/questions/14012766/detecting-whether-two-divs-overlap
function collision($div1, $div2) {
  let x1 = $div1.offset().left;
  let y1 = $div1.offset().top;
  let h1 = $div1.outerHeight(true);
  let w1 = $div1.outerWidth(true);
  let b1 = y1 + h1;
  let r1 = x1 + w1;
  let x2 = $div2.offset().left;
  let y2 = $div2.offset().top;
  let h2 = $div2.outerHeight(true);
  let w2 = $div2.outerWidth(true);
  let b2 = y2 + h2;
  let r2 = x2 + w2;

  if (b1 < y2 || y1 > b2 || r1 < x2 || x1 > r2) return false;
  return true;
}


// render events using renderDay(arr) in console
html, body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
}

#container {
  height: 100%;
  width: 100%;
}

#header-title {
  text-align: center;
}

#calendar {
  width: 400px;
  height: 620px;
  margin-top: 70px;
}

#events {
  position: absolute;
  top: 80px;
  left: 100px;
  width: 800px;
  height: 620px;
}

.event {
  box-shadow: 0 0 20px black;
  border-radius: 5px;
}

.hr-block {
  border-top: 2px solid black;
  height: 58px;
  margin: 0;
  padding: 0;
  margin-left: 100px;
  min-width: 360px;
  opacity: .5;
}

.hr-header {
  position: relative;
  top: -33px;
  left: -68px;
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="css/styles.css">
  <link rel="stylesheet" href="css/responsive.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>

  <script charset="UTF-8" src="js/moment.js"></script>
  <script charset="UTF-8" src="js/script2.js"></script>
  <title>Thought Machine Code Challenge</title>
</head>
<body>

  <div id="container">
    <div class="header">
      <h1 id="header-title"></h1>
    </div>
    <div id="calendar">
      <div class="hr-block"><h2 class="hr-header">09:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">10:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">11:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">12:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">13:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">14:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">15:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">16:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">17:00</h2></div>
      <div class="hr-block"><h2 class="hr-header">18:00</h2></div>
    </div>
  </div>
  
  <div id="events">

  </div>

  <script>
    document.getElementById("header-title").innerHTML = moment().calendar();
  </script>
</body>
</html>

events not rendering, you need to run:

renderDay([{start:30, end:120, start: 60, end: 120}]) in console

douglaswissett
  • 231
  • 1
  • 4
  • 14
  • 1
    Thanks for including this. You should move this into your question instead of having it as an answer as it's not really an answer. You can always hide the snippet if you think it will be too long. – Jaydo Apr 21 '16 at 10:25