I'm trying to create a horizontal timeline but I'm not sure the best way to go about arranging the events divs correctly. At the moment it looks like this:
<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>
I'm trying to make Entry 4
position itself at the top (as there are no divs in the way) but I'm not sure the best solution. It also needs to allow for any number of events overlapping.
Most css options / jQuery plugins don't seem to offer a solution as far as I'm aware they are mostly for flexible grids but this only needs to be flexible vertically and have fixed positions horizontally to line up correctly with the dates.
An obvious first step is to position: absolute
and set a top: x
but how would one go about checking previous entries to make sure it's not overlapping an older & longer entry. The timeline will hold quite a number of events with various lengths so it can't be too intensive either.
Any suggestions for the best/easiest way to do this?
I'm trying to create a horizontal timeline but I'm not sure the best way to go about arranging the events divs correctly. At the moment it looks like this:
<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>
I'm trying to make Entry 4
position itself at the top (as there are no divs in the way) but I'm not sure the best solution. It also needs to allow for any number of events overlapping.
Most css options / jQuery plugins don't seem to offer a solution as far as I'm aware they are mostly for flexible grids but this only needs to be flexible vertically and have fixed positions horizontally to line up correctly with the dates.
An obvious first step is to position: absolute
and set a top: x
but how would one go about checking previous entries to make sure it's not overlapping an older & longer entry. The timeline will hold quite a number of events with various lengths so it can't be too intensive either.
Any suggestions for the best/easiest way to do this?
Share Improve this question edited Apr 9, 2018 at 9:30 Brett DeWoody 62.8k31 gold badges144 silver badges192 bronze badges asked Mar 18, 2018 at 16:23 SteveSteve 2,5262 gold badges20 silver badges30 bronze badges 3- Look into CSS Grids, those support overlapping an provides quite an easy method of placing out elements – E. Sundin Commented Mar 18, 2018 at 16:44
- 1 Take a look at vis.js here visjs. They have examples similar to what you have asked. visjs/examples/timeline/items/itemOrdering.html – Karl Graham Commented Apr 3, 2018 at 11:07
- Are the event blocks all the same width? Or variable depending on the title of the event? – Brett DeWoody Commented Apr 9, 2018 at 15:48
6 Answers
Reset to default 4 +25I suppose you have an events array with start and end dates, then you should check whether events are overlapping by start and end dates. To simulate this, you can check this method:
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);
This method picks the first event as the key and checks for other events whether they overlap or not, if they don't overlap, they will be added to the first stack, then if there are any events left, a new stack will be created and then with the remaining events, the code will be run again and a new stack will be prepared again. Until there are any events left in the array, it should continue creating stacks.
Then you can use these stacks to build your events grid. One stack per line.
I used this algorithm in my Evendar plugin. You can check it's grid view.
grid display has a mode, auto-flow : dense, that can do what you want.
In the following example, I have divided every visible column in 4 subcolumns, to allow to set positions in quartes of an hour .
Then, I have classes to set the beginning and end of the events. Of course, you could inject the styles inline and get the same result:
.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>
UPDATE
In addition to inline style, You can also rely on CSS variables in order to easily manage the grid with less of code:
.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;
grid-column-start: var(--s, 1);
grid-column-end: var(--e, 1);
}
<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" style="--s:1;--e:3">A</div>
<div class="elem" style="--s:5;--e:6">B</div>
<div class="elem" style="--s:1;--e:2">C</div>
<div class="elem" style="--s:3;--e:5">D</div>
<div class="elem" style="--s:1;--e:8">E</div>
<div class="elem" style="--s:3;--e:4">F</div>
<div class="elem" style="--s:4;--e:6">G</div>
<div class="elem" style="--s:2;--e:3">H</div>
</div>
I think this cannot be done with only CSS, so we need to rely on some JS/jQuery.
My idea is to only set the left property within the element, then I loop through all the element to define the top value. I start by setting top to 0 and check if there is already an element in that position.
If not: I place it there and move to next element.
if yes: I increase the top value and check again. I continue until I find a place.
Here a simplified code where I set a random value to left and apply the logic described above.
$('.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./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>
You can get something of that effect with just display: grid
. Though you will need to be mindful of the order you create the divs.
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>
Next is an example of how I would go about it, I have populated events' starting/ending points with random values along a 12h time line. Since the values are random, they might be a little rough around the edges sometimes ;).
Also I assumed two events, A and B, can have the same top if, for instance, A ends at exactly the same time as B or vice versa.
I have mented the code as much as I thought necessary to explain the proceedings.
Hope it helps
$(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's 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 pensate for the 1px-wide border
}
/**
* Get the event's 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's 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./ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="time-line"></div>
<div id="events"></div>
Here's a relatively simple function which can be applied to the HTML markup you have. I have hardcoded the height (28px
) but the width of the event elements can be variable. The end result is something like this:
A few requirements for this solution to work:
The
left
property needs to be defined in the HTML markup of each event (as in your example), like:<div class="event" style="left:25px; position:relative;" id="1">• Entry 1</div>
- The events need to be in order, with each event element's
left
property equal to, or greater, than the previous element'sleft
property
arrangeEvents()
Accepts a selector for the event elements as an argument, then loops through them and applies a top
property as needed depending on the end of the longest previous event and start of the current event.
In short, if the start time of the current event is less than the end of a previous event, the current event is positioned below the previous events. If the start of the current event is greater than the end of the previous events, the current event is positioned at the 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>