Лучший способ позиционирования divs в горизонтальной шкале времени с учетом свободного пространства/перекрытия

Я пытаюсь создать горизонтальную шкалу времени, но я не уверен, что лучше всего организовать правильные раскладки событий. На данный момент это выглядит так:

<div id="events">
    <div class="event" style="left:25; position:relative;" id="1">• Entry 1</div>
    <div class="event" style="left:25; position:relative;" id="2">• Entry 2</div>
    <div class="event" style="left:50; position:relative;" id="3">• Entry 3</div>
    <div class="event" style="left:375; position:relative;" id="4">• Entry 4</div>
</div>

enter image description here

Я пытаюсь сделать позицию Entry 4 на самом верху (так как на этом пути нет div), но я не уверен, что это лучшее решение. Он также должен позволять перекрывать любое количество событий.

enter image description here

Большинство css-опций/плагинов jQuery, похоже, не предлагают решения, поскольку я знаю, что они в основном предназначены для гибких сеток, но это только должно быть гибким по вертикали и иметь фиксированные позиции по горизонтали, чтобы правильно выстраиваться с датами.

Очевидным первым шагом является position: absolute и задание top: x но как можно было бы проверить предыдущие записи, чтобы убедиться, что он не перекрывает более старую и длинную запись. Временная шкала проведет довольно много событий различной длины, поэтому она также не может быть слишком интенсивной.

Любые предложения для самого лучшего/простого способа сделать это?

Ответ 1

Я полагаю, у вас есть массив событий с датами начала и окончания, тогда вы должны проверить, перекрываются ли события по датам начала и окончания. Чтобы имитировать это, вы можете проверить этот метод:

var events = [{
  start: "2018/10/24 15:00",
  end: "2018/10/24 18:00"
}, {
  start: "2018/10/25 12:00",
  end: "2018/10/26 12:00"
}, {
  start: "2018/10/25 07:00",
  end: "2018/10/25 10:00"
}, {
  start: "2018/10/24 12:00",
  end: "2018/10/24 20:00"
}, {
  start: "2018/10/25 08:00",
  end: "2018/10/25 13:00"
}];

var stack = [],  
  s = 0,
  lastStartDate, lastEndDate, newEvents;
events.sort(function(a,b){
   if(a.start > b.start) return 1;
   if(a.start < b.start) return -1;
   return 0;
});
while (events.length > 0) {
  stack[s] = [];
  newEvents = [];
  stack[s].push(events[0]);
  lastStartDate = events[0].start;
  lastEndDate = events[0].end;
  for (var i = 1; i < events.length; i++) {
    if (events[i].end < lastStartDate) {
      stack[s].push(events[i]);
      lastStartDate = events[i].start;
      delete events[i];
    } else if (events[i].start > lastEndDate) {
      stack[s].push(events[i]);
      lastEndDate = events[i].end;      
    }else{
      newEvents.push(events[i]);
    }
  }
  events = newEvents;
  s++;
}

console.log(stack);

Ответ 2

дисплей сетки имеет режим, автоматический поток: плотный, который может делать то, что вы хотите.

В следующем примере я разделил каждый видимый столбец на 4 подколонки, чтобы установить позиции в четверти часа.

Затем у меня есть классы, чтобы установить начало и конец событий. Конечно, вы можете вводить стили inline и получать одинаковый результат:

.grid {
  margin: 10px;
  width: 525px;
  border: solid 1px black;
  display: grid;
  grid-template-columns: repeat(24, 20px);
  grid-auto-flow: dense;
  grid-gap: 2px;
}

.head {
  grid-column: span 4;
  background-color: lightblue;
  border: solid 1px blue;
}

.elem {
  background-color: lightgreen;
}

.start1 {
  grid-column-start: 1;
}
.start2 {
  grid-column-start: 2;
}
.start3 {
  grid-column-start: 3;
}
.start4 {
  grid-column-start: 4;
}
.start5 {
  grid-column-start: 5;
}
.start6 {
  grid-column-start: 6;
}

.end2 {
  grid-column-end: 3;
}
.end3 {
  grid-column-end: 4;
}
.end4 {
  grid-column-end: 5;
}
.end5 {
  grid-column-end: 6;
}
.end6 {
  grid-column-end: 7;
}
<div class="grid">
    <div class="head">1</div>
    <div class="head">2</div>
    <div class="head">3</div>
    <div class="head">4</div>
    <div class="head">5</div>
    <div class="head">6</div>
    <div class="elem start1 end2">A</div>
    <div class="elem start2 end5">B</div>
    <div class="elem start2 end3">C</div>
    <div class="elem start3 end4">D</div>
    <div class="elem start3 end3">E</div>
    <div class="elem start5 end6">F</div>
    <div class="elem start5 end7">G</div>
    <div class="elem start6 end8">H</div>
</div>

Ответ 3

Я думаю, что это невозможно сделать только с CSS, поэтому нам нужно полагаться на некоторые JS/jQuery.

Моя идея состоит в том, чтобы установить только свойство left в элементе, а затем прочертить весь элемент, чтобы определить верхнее значение. Я начинаю с установки top на 0 и проверяет, есть ли в этой позиции элемент.

Если нет: я помещаю его туда и перехожу к следующему элементу.

если да: я увеличиваю верхнее значение и снова проверяю. Я продолжаю, пока не найду место.

Здесь приведен упрощенный код, где я устанавливаю случайное значение слева и применяю описанную выше логику.

$('.event').each(function(){
  var top=0;
  var left = Math.floor(Math.random() *(400));
  /* we need to test between left and left+ width of element*/
  var e1 = document.elementFromPoint(left, top);
  var e2 = document.elementFromPoint(left+80, top);
  /* we check only  placed element to avoid cycle*/
    while ((e1 && e1.classList.contains('placed')) || (e2 && e2.classList.contains('placed'))) {
        top += 20; /*Height of an element*/
        e1 = document.elementFromPoint(left, top)
        e2 = document.elementFromPoint(left+80, top)
    }
  $(this).css({top:top, // we set the top value
              left:left, // we set the left value
              zIndex:3// we increase z-index because elementFromPoint consider the topmost element
              });
                
  $(this).addClass('placed'); //we mark the element as placed
});
* {
  box-sizing: border-box;
}
body {
 margin:0;
}

#events {
  height: 300px;
  width:calc(80px * 6 + 2px);
  background:repeating-linear-gradient(to right,blue 0,blue 2px,transparent 2px,transparent 80px);
  position:relative;
}

.event {
  position: absolute;
  width: 80px;
  height: 20px;
  border: 1px solid #000;
  z-index:2;
  background:red;
  color:#fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="events">
  <div class="event" >• Entry 1</div>
  <div class="event" >• Entry 2</div>
  <div class="event" >• Entry 3</div>
  <div class="event" >• Entry 4</div>
  <div class="event" >• Entry 5</div>
  <div class="event" >• Entry 6</div>
  <div class="event" >• Entry 7</div>
  <div class="event" >• Entry 8</div>
</div>

Ответ 4

Вы можете получить что-то подобное с помощью только display: grid. Хотя вам нужно помнить о порядке, который вы создаете div.

var grid = document.getElementById("grid")

for (let i = 0; i <= 9; i++)
{
  let div = document.createElement("div");
  div.innerHTML = i
  grid.append(div);
}

let div

div = document.createElement("div");
div.innerHTML = "Entry 1";
div.style.gridColumn = 1
grid.append(div)

div = document.createElement("div");
div.innerHTML = "Entry 4";
div.style.gridColumn = 10
grid.append(div)

div = document.createElement("div");
div.innerHTML = "Entry 2";
div.style.gridColumn = 1
grid.append(div)

div = document.createElement("div");
div.innerHTML = "Entry 3";
div.style.gridColumn = 2
grid.append(div)
#grid {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
}
<div id="grid" >
  
</div>

Ответ 5

Далее приведен пример того, как я буду это делать, у меня есть начальные/конечные точки заполненных событий со случайными значениями в течение 12-часовой строки. Поскольку значения являются случайными, иногда они могут быть немного грубыми по краям;).

Также я предположил, что два события, A и B, могут иметь один и тот же верхний, если, например, A заканчивается точно в то же время, что и B, или наоборот.

Я прокомментировал этот код так сильно, как я думал, необходимо объяснить ход разбирательства.

Надеюсь, поможет

$(document).ready(function() {
                var timeline = $('#time-line');

                for (var i = 0; i < 12; i++)
                    timeline.append('<div>' + (i + 1) + '</div>')

                /*to check event overlapping later*/
                var eventData = [];

                /*generate random amount of events*/
                var eventCount = Math.random() * 10 + 1;
                var eventsContainer = $('#events');
                var total = 720; //12h * 60min in the example
                for (var i = 0; i < eventCount; i++) {
                    var start = Math.floor(Math.random() * total);
                    var end = Math.floor(Math.random() * (total - start)) + start;
                    var duration = end - start;

                    var event = $('<div class="event" id="' + i + '" >E' + i + '(' + duration + ' min.' + ')</div>');

                    event.attr('title', 'Start: ' + Math.floor(start / 60) + ':' + (start % 60) + ' | '
                            + 'End: ' + Math.floor(end / 60) + ':' + (end % 60));

                    event.css('width', (duration * 100 / 720) + "%");
                    event.css('left', (start * 100 / 720) + "%");
                    var top = getTop(start, end);
                    event.css('top', top);

                    /*store this event data to use it to set next events' top property in getTop()*/
                    eventsContainer.append(event);
                    eventData.push([start, end, top, event.height() + 1]); //the +1 is to compensate for the 1px-wide border

                }

                /**
                 * Get the event top property by going through the previous events and
                 * getting the 'lowest' yet
                 *
                 * @param {Number} start
                 * @param {Number} end
                 * @returns {Number}
                 */
                function getTop(start, end) {
                    var top = 0;
                    /*for each previous event check for vertical collisions with current event*/
                    $.each(eventData, function(i, data /*[start, end, top]*/) {
                        if (data[2] + data[3] > top //if it height + top is not the largest yet, skip it
                                //if any of the next 6 conditions is met, we have a vertical collision
                                //feel free to optimize but tread carefully
                                && (data[0] >= start && data[0] < end
                                        || data[0] < start && data[1] >= end
                                        || data[0] < start && data[1] > start
                                        || data[0] < end && data[1] >= end
                                        || data[0] >= start && data[1] < end
                                        || data[0] <= start && data[1] > start)) {
                            top = data[2] + data[3];
                        }
                    });
                    return top;
                }
            });
#time-line {
    width: 100%;
    text-align: center
}

#time-line div {
    display: inline-block;
    width: calc(100% / 12); //12h for the example
    text-align: right;
    background: lightgray;
}
#time-line div:nth-child(odd) {
    background: gray
}
#events {
    position: relative
}

#events .event{
    position: absolute;
    background: lightblue;
    text-align: center;
    border: 1px solid black

}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="time-line"></div>
<div id="events"></div>

Ответ 6

Здесь относительно простая функция, которая может быть применена к разметке HTML, которую вы имеете. Я жестко закодировал высоту (28px), но ширина элементов события может быть переменной. Конечный результат выглядит примерно так:

enter image description here

Несколько требований к этому решению:

  • left свойство должно быть определено в разметке HTML каждого события (как в вашем примере), например:

    <div class="event" style="left:25px; position:relative;" id="1">• Entry 1</div>
    
  • События должны быть в порядке, при этом каждый элемент события left свойство, равное или большее, чем предыдущее свойство left

arrangeEvents()

Принимает селектор для элементов события в качестве аргумента, затем перебирает их и применяет top свойство по мере необходимости в зависимости от конца самого длинного предыдущего события и начала текущего события.

Короче говоря, если время начала текущего события меньше конца предыдущего события, текущее событие помещается ниже предыдущих событий. Если начало текущего события больше, чем конец предыдущих событий, текущее событие позиционируется вверху.

const arrangeEvents = (els) => {
  let offset = 1;
  let end = 0;

  Array.from(els).forEach((event, index) => {
    const posLeft = parseInt(event.style.left);
    
    offset = posLeft >= end ? 0 : ++offset;
    end = Math.max(end, posLeft + event.offsetWidth);
    
    event.style.top = '-${28 * (index - offset)}px';
  });
}

arrangeEvents(document.querySelectorAll('.event'));
#events {
  position: relative;
}

.event {
  display: inline-block;
  float: left;
  clear: both;
  border: 1px solid lightgray;
  padding: 4px;
  height: 28px;
  box-sizing: border-box;
}
<div id="events">
  <div class="event" style="left:25px; position:relative;" id="1">• Entry 1</div>
  <div class="event" style="left:25px; position:relative;" id="2">• Entry 2</div>
  <div class="event" style="left:50px; position:relative;" id="3">• #3</div>
  <div class="event" style="left:125px; position:relative;" id="4">• A Really Long Entry 4</div>
  <div class="event" style="left:175px; position:relative;" id="5">• Entry5</div>
  <div class="event" style="left:185px; position:relative;" id="6">• And Even Longer Entry 6</div>
  <div class="event" style="left:250px; position:relative;" id="7">• #7</div>
  <div class="event" style="left:330px; position:relative;" id="8">• Entry 8</div>
  <div class="event" style="left:330px; position:relative;" id="9">• Entry 9</div>
  <div class="event" style="left:330px; position:relative;" id="10">• Long Entry 10</div>
  <div class="event" style="left:330px; position:relative;" id="11">• Entry 11</div>
  <div class="event" style="left:410px; position:relative;" id="12">• Entry 12</div>
  <div class="event" style="left:490px; position:relative;" id="13">• Entry 13</div>
</div>