I'm using the following options in Vis JS Timeline to produce a horizontal axis at the top of the timeline with time labels:
orientation: {
axis: 'both'
},
The horizontal axis looks like this:
My timeline has many rows, so the user needs to vertically scroll down the page to see everything. The problem is that the horizontal axis at the top does not stay in view when scrolling down the page.
Question: How can I freeze the horizontal axis at the top so that the time labels stay in view when scrolling down?
The following code snippet, or jsfiddle.net/nj1647tb, is my timeline:
const seed = '11';
Math.seedrandom(seed);
const nGroups = 40;
const maxSubGroups = 2;
const maxItemsPerSubGroup = 1;
const metaEventCount = 2;
const itemLengthScale = 200;
let now = moment().minutes(0).seconds(0).milliseconds(0);
var groupCount = 12;
var itemCount = 70;
var tcCrashProbability = 0.2;
function randInt(min, max) {
return Math.round(min + Math.random() * (max - min));
}
function getStartEnd(earliestStart) {
if (earliestStart === undefined) {
earliestStart = 0;
}
let startAdd = earliestStart + Math.random() * 200;
let length = Math.random() * itemLengthScale;
let endAdd = startAdd + length;
return {
startAdd: startAdd,
endAdd: endAdd
}
}
const stackTrace = `Traceback (most recent call last):
File "/usr/local/lib/python3.6/site-packages/requests/adapters.py", line 449, in send
timeout=timeout
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 756, in urlopen
method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
File "/usr/local/lib/python3.6/site-packages/urllib3/util/retry.py", line 532, in increment
raise six.reraise(type(error), error, _stacktrace)
File "/usr/local/lib/python3.6/site-packages/urllib3/packages/six.py", line 769, in reraise
raise value.with_traceback(tb)
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 706, in urlopen
chunked=chunked,
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 445, in _make_request
six.raise_from(e, None)
File "<string>", line 3, in raise_from
File "/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py", line 440, in _make_request
httplib_response = conn.getresponse()
File "/usr/lib64/python3.6/http/client.py", line 1346, in getresponse
response.begin()
File "/usr/lib64/python3.6/http/client.py", line 307, in begin
version, status, reason = self._read_status()
File "/usr/lib64/python3.6/http/client.py", line 268, in _read_status
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
File "/usr/lib64/python3.6/socket.py", line 586, in readinto
return self._sock.recv_into(b)
File "/usr/lib64/python3.6/ssl.py", line 971, in recv_into
`;
// create a data set with groups
var group_names = [];
var groups = new vis.DataSet();
for (let i = 0; i < nGroups; i++) {
group_names.push('GROUP_' + i);
groups.add({
id: group_names[i],
content: group_names[i]
});
}
// add meta group
groups.add({
id: -1,
content: ' '
});
// create a dataset with items
let items = new vis.DataSet();
for (let i = 0; i < nGroups; i++) {
let nSubGroups = randInt(1, maxSubGroups);
//console.log('group='+i+' nSubGroups='+nSubGroups);
let lastStartAdd = 0;
for (let sg = 0; sg < nSubGroups; sg++) {
let start_end = getStartEnd(lastStartAdd);
let start = now.clone().add(start_end['startAdd'], 'hours');
let end = now.clone().add(start_end['endAdd'], 'hours');
let itemID = 'G' + i + '_S' + sg + '_item';
let subgroupID = 'G' + i + '_S' + sg;
let subgroupOrder = sg;
let newItem = {
id: itemID,
group: group_names[i],
subgroup: subgroupID,
subgroupOrder: subgroupOrder,
content: 'ITEM_DU_' + 'G' + i + '_S' + sg,
start: start,
end: end,
title: 'ITEM_DU_' + 'G' + i + '_S' + sg
};
//console.log(group_names[i] + ', ' + 'S' + sg + ', ' +start_end['startAdd'] + ', ' + start_end['endAdd']);
items.add(newItem);
lastStartAdd = start_end['startAdd'];
// random crashes
if(Math.random() <= tcCrashProbability) {
let crashStart = now.clone().add(randInt(start_end['startAdd'], start_end['endAdd']), 'hours');
let newCrashItem = {
id: 'crash_' + itemID,
group: group_names[i],
subgroup: subgroupID,
subgroupOrder: subgroupOrder,
content: 'Crash',
start: crashStart,
type: 'box',
className: 'timeline-tc-crash',
title: '<pre>' + stackTrace + '</pre>'
};
items.add(newCrashItem);
}
}
}
// generate some meta events
for (let i = 0; i < metaEventCount; i++) {
let start = now.clone().add(Math.random() * 200, 'hours');
items.add({
id: 'M' + i,
group: -1,
content: 'Crash',
title: '<pre>' + stackTrace + '</pre>',
className: 'timeline-event-crash',
start: start,
type: 'box'
});
}
// create visualization
var container = document.getElementById('visualization');
var options = {
groupOrder: 'content',
stack: false,
stackSubgroups: true,
orientation: {
axis: 'both'
},
showCurrentTime: false
};
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
#visualization {
box-sizing: border-box;
width: 100%;
height: 300px;
}
.timeline-event-crash {
background-color: red !important;
border-color: darkred !important;
color: white !important;
font-family: monospace;
box-shadow: 0 0 10px gray;
}
.timeline-tc-crash {
color: red !important;
border-color: red !important;
background-color: #F4BBB5 !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.3.10/seedrandom.min.js"></script>
<link href="https://visjs.github.io/vis-timeline/styles/vis-timeline-graph2d.min.css" rel="stylesheet" />
<script src="https://visjs.github.io/vis-timeline/standalone/umd/vis-timeline-graph2d.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<html>
<head>
<title>Timeline</title>
</head>
<body>
<div id="visualization"></div>
</body>
</html>
I don't know if you ever tried this, but when i am investigating the issue, when my mouse is over the left column i was able to scroll vertically. So not a fix from code side, but if you could not find a proper solution maybe you can add some guidance for your users like "if you want to see groups down below you should scroll when the mouse is over them".
P.S Though i did not deeply investigated this yet but i think this is an expected behavior from vis side, because as far as i have seen from their examples they do not have any option to horizontal scroll when mouse is over timeline
Related
I've been working on a side project to recreate diablo / tarkov like inventory screens with pure html, css, and js. I'm attempting to use the draggable api's built into html for this but am hitting a blocker. Everything working fine for 1x1 wide/high cell's / items. The problem begins when trying to add an item that is longer in width and/or height. I can still drag around the item but I can't get it to do two things.
Cover all the cells correctly so there is no bleed through from the cells background.
Stop the item from being placed in a cell that would cover another item.
My ideal solution modifies either the linked codepen below or provides direction / a solution on a more appropriate approach whatever that may be. The only constraint is that it must use html, css, and javascript. The only exception is jQuery.
The Code
var draggedItem = null;
function Inventory(options) {
// Setup Rows and Items
this.init = function (options) {
let slotSizeW = options.size.w / options.slots.w;
let slotSizeH = options.size.h / options.slots.h;
this.html = { inventory: options.selector };
// Build grid
let grid = document.createElement("grid");
grid.style.width = options.size.w + "px";
grid.style.height = options.size.h + "px";
grid.style.gridTemplateColumns = "1fr ".repeat(options.slots.w);
grid.style.gridTemplateRows = "1fr".repeat(options.slots.h);
grid.cells = [];
for (var i = 0; i < options.slots.w * options.slots.h; i++) {
var cell = document.createElement("cell");
cell.style.width = slotSizeW + "px";
cell.style.height = slotSizeH + "px";
grid.appendChild(cell);
grid.cells.push(cell);
}
this.html.grid = grid;
this.html.inventory.appendChild(this.html.grid);
// Add all items
let items = [];
options.items.forEach(function (item, index, array) {
items.push(item);
let itemEl = document.createElement("item");
itemEl.setAttribute("draggable", true);
itemEl.style.background = item.color;
itemEl.style.width = 100 * item.w + "%";
itemEl.style.height = 100 * item.h + "%";
grid.cells[options.slots.w * item.x + item.y].appendChild(itemEl);
});
this.html.items = items;
let itemTags = document.querySelectorAll("item");
for (let i = 0; i < itemTags.length; i++) {
itemTags[i].addEventListener("dragstart", this.dragStart);
itemTags[i].addEventListener("dragend", this.dragEnd);
}
let cellTags = document.querySelectorAll("cell");
for (let i = 0; i < cellTags.length; i++) {
cellTags[i].addEventListener("dragover", this.dragOver);
cellTags[i].addEventListener("dragenter", this.dragEnter);
cellTags[i].addEventListener("dragleave", this.dragLeave);
cellTags[i].addEventListener("drop", this.dragDrop);
}
};
this.dragStart = function () {
this.classList.toggle("hold");
window.requestAnimationFrame(() => this.classList.toggle("invisible"));
draggedItem = this;
};
this.dragEnd = function () {
this.classList.toggle("hold");
this.classList.toggle("invisible");
};
this.dragOver = function (e) {
e.preventDefault();
};
this.dragEnter = function (e) {
e.preventDefault();
this.classList.toggle("hovered");
};
this.dragLeave = function (e) {
this.classList.remove("hovered");
};
this.dragDrop = function (e) {
this.classList.remove("hovered");
if (draggedItem != null) {
draggedItem.parentElement.removeChild(draggedItem);
this.appendChild(draggedItem);
}
};
this.init(options);
}
var inventory = new Inventory({
selector: document.querySelector("inventory"),
size: { w: 300, h: 300 },
slots: { w: 4, h: 4 },
items: [
{ x: 0, y: 0, w: 2, h: 2, content: "2x2", color: "#ffd54f" },
{ x: 2, y: 0, w: 1, h: 2, content: "1x2", color: "#66bb6a" },
{ x: 3, y: 3, w: 1, h: 1, content: "1x1", color: "#e53935" }
]
});
html,
body {
background: darksalmon;
}
grid {
position: relative;
display: grid;
}
cell {
position: relative;
border: 3px salmon solid;
background-color: white;
}
item {
display: block;
position: relative;
color: white;
cursor: pointer;
z-index: 2;
}
.hold {
border: solid #ccc 4px;
}
.hovered {
background: #f4f4f4;
border-style: dashed;
}
.invisible {
display: none;
}
<container>
<inventory>
</inventory>
</container>
To get each item's color to fill the underlying cells you have to take into account the extra width of each cell created by its border.
This snippet sets the width and height of an item using this formula:
itemEl.style.width = "calc((100% + 6px) * " + item.w + " - 4.9px)";
itemEl.style.height = "calc((100% + 6px) * " + item.h + " - 4.9px)";
This says add on an allowance for all 4 borders but remove the width of the outer borders. However, there can be a 'quirk' where the calculation the system has to do to map CSS pixels to screen pixels (several screen pixels may be used for one CSS pixel in modern displays) results in the odd screen pixel being left behind - in this case resulting in a thin white line around the item's color.
The item is made slightly wider (by just over 1 CSS pixel) and is placed half a pixel up and left which, at least in the tests I did with Edge/Chrome Windows10 removed this white 'border' at all zoom levels.
var draggedItem = null;
function Inventory(options) {
// Setup Rows and Items
this.init = function (options) {
let slotSizeW = options.size.w / options.slots.w;
let slotSizeH = options.size.h / options.slots.h;
this.html = { inventory: options.selector };
// Build grid
let grid = document.createElement("grid");
grid.style.width = options.size.w + "px";
grid.style.height = options.size.h + "px";
grid.style.gridTemplateColumns = "1fr ".repeat(options.slots.w);
grid.style.gridTemplateRows = "1fr".repeat(options.slots.h);
grid.cells = [];
for (var i = 0; i < options.slots.w * options.slots.h; i++) {
var cell = document.createElement("cell");
cell.style.width = slotSizeW + "px";
cell.style.height = slotSizeH + "px";
grid.appendChild(cell);
grid.cells.push(cell);
}
this.html.grid = grid;
this.html.inventory.appendChild(this.html.grid);
// Add all items
let items = [];
options.items.forEach(function (item, index, array) {
items.push(item);
let itemEl = document.createElement("item");
itemEl.setAttribute("draggable", true);
itemEl.style.background = item.color;
//itemEl.style.width = 100 * item.w + "%";
//itemEl.style.height = 100 * item.h + "%";
itemEl.style.width = "calc((100% + 6px) * " + item.w + " - 4.9px)";
itemEl.style.height = "calc((100% + 6px) * " + item.h + " - 4.9px)";
grid.cells[options.slots.w * item.x + item.y].appendChild(itemEl);
});
this.html.items = items;
let itemTags = document.querySelectorAll("item");
for (let i = 0; i < itemTags.length; i++) {
itemTags[i].addEventListener("dragstart", this.dragStart);
itemTags[i].addEventListener("dragend", this.dragEnd);
}
let cellTags = document.querySelectorAll("cell");
for (let i = 0; i < cellTags.length; i++) {
cellTags[i].addEventListener("dragover", this.dragOver);
cellTags[i].addEventListener("dragenter", this.dragEnter);
cellTags[i].addEventListener("dragleave", this.dragLeave);
cellTags[i].addEventListener("drop", this.dragDrop);
}
};
this.dragStart = function () {
this.classList.toggle("hold");
window.requestAnimationFrame(() => this.classList.toggle("invisible"));
draggedItem = this;
};
this.dragEnd = function () {
this.classList.toggle("hold");
this.classList.toggle("invisible");
};
this.dragOver = function (e) {
e.preventDefault();
};
this.dragEnter = function (e) {
e.preventDefault();
this.classList.toggle("hovered");
};
this.dragLeave = function (e) {
this.classList.remove("hovered");
};
this.dragDrop = function (e) {
this.classList.remove("hovered");
if (draggedItem != null) {
draggedItem.parentElement.removeChild(draggedItem);
this.appendChild(draggedItem);
}
};
this.init(options);
}
var inventory = new Inventory({
selector: document.querySelector("inventory"),
size: { w: 300, h: 300 },
slots: { w: 4, h: 4 },
items: [
{ x: 0, y: 0, w: 2, h: 2, content: "2x2", color: "#ffd54f" },
{ x: 2, y: 0, w: 1, h: 2, content: "1x2", color: "#66bb6a" },
{ x: 3, y: 3, w: 1, h: 1, content: "1x1", color: "#e53935" }
]
});
html,
body {
background: darksalmon;
}
grid {
position: relative;
display: grid;
}
cell {
position: relative;
border: 3px salmon solid;
background-color: white;
}
item {
display: block;
position: relative;
color: white;
cursor: pointer;
z-index: 2;
top: -0.5px;
left: -0.5px;
}
.hold {
border: solid #ccc 4px;
}
.hovered {
background: #f4f4f4;
border-style: dashed;
}
.invisible {
display: none;
}
<container>
<inventory>
</inventory>
</container>
For the second part of the question on a drop you will have to loop through the items to see if any overlaps with the dragged item and if it doesn't then you can remove the dragged item from its current cell and append it to its new one.
I added a legend / Key in the map which is alright in case of squares but in circles, they are not aligned with the text, is there any way to align the text and the circles properly.
Image
Here is the code
function getsizeLabel(d)
{
return d > 90 ? 18 :
d > 60 ? 14 :
d > 30 ? 9 :
5 ;
}
function AddLDLegend() {
legendLD = L.control({position: 'bottomright'});
legendLD.onAdd = function (map) {
var div = L.DomUtil.create('div', 'info legend');
labels = ['<strong>Lockdown Days</strong>'],
grades = [0, 30, 60, 90],
categories = ['1-30','31-60','61-90'];
for (var i = 0; i < grades.length; i++) {
div.innerHTML +=
labels.push(
'<i style="border-radius: 50%; border: 1px solid #8A2BE2; width:' + getsizeLabel(grades[i] + 1) +'px; height:' + getsizeLabel(grades[i] + 1) +'px;"></i> ' +
(grades[i] + grades[i+1] ? categories[i] : '91-100'));
}
div.innerHTML = labels.join('<br>');
return div;
};
legendLD.addTo(map);
}
The map is leaflet map I am just calling this function to add legend from another function but the design and everything related to the small legend div is in this function.
the preference for the text and circle alignment is in this image
enter image description here
I think this is what you was trying to achieve. Please let me know if you have any doubt.
const grades = [0, 30, 60, 90];
const categories = ['1-30', '31-60', '61-90'];
function getSize(g) {
return (
g > 90 ? 18 :
g > 60 ? 14 :
g > 30 ? 9 : 5
);
}
function getLabel(i) {
const size = getSize(grades[i] + 1);
const grade = (grades[i] + grades[i + 1] ? categories[i] : '91-100');
const bulletStyle = [
'display: inline-block;',
'vertical-align: middle;',
'border-radius: 50%;',
'border: 1px solid #8A2BE2;',
`height: ${size}px;`,
`width: ${size}px;`
];
const valueStyle = [
'display: inline-block;',
'vertical-align: middle;'
];
const bullet = `<i style="${bulletStyle.join(' ')}"></i>`;
const value = `<span style="${valueStyle.join(' ')}">${grade}</span>`
return bullet + ' ' + value;
}
function getDiv() {
const div = L.DomUtil.create('div', 'info legend');
const labels = ['<strong>Lockdown Days</strong>'];
for (var i = 0; i < grades.length; i++) {
div.innerHTML += labels.push(getLabel(i));
}
div.innerHTML = labels.join('<br>');
return div;
};
document.getElementsByTagName('BODY')[0].appendChild(getDiv());
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet#1.7.1/dist/leaflet.js"></script>
I have this code for build a linear gradient by array of colors (unknown length):
const colors = ['red', 'green', 'violet'];
const strips = [];
let start = 0;
let end = 0;
const stripPerc = Math.ceil(100 / colors.length);
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
start = end;
end = start + stripPerc;
if ( end > 100 ) {
end = 100;
}
let strip = color + ' ';
strip += start + '% ';
strip += end + '% ';
strips.push(strip.trim());
}
document.getElementById('foo').style.backgroundImage = 'linear-gradient(135deg, ' + strips.join(',') + ')';
div {
display: block;
width: 100%;
height: 50px;
}
<div id="foo"></div>
i want add a small (1%) white stripe between the colors:
const colors = ['red', 'green', 'violet'];
const strips = [];
let start = 0;
let end = 0;
const stripPerc = Math.ceil(100 / colors.length);
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
start = end;
end = start + stripPerc;
if ( end > 100 ) {
end = 100;
}
let strip = color + ' ';
strip += start + '% ';
strip += end + '% ';
strips.push(strip.trim());
strips.push(' white ' + end + '% '+ (end+1) +'%');
end++;
}
document.getElementById('foo').style.backgroundImage = 'linear-gradient(135deg, ' + strips.join(',') + ')';
div {
display: block;
width: 100%;
height: 20px;
}
<div id="foo"></div>
it works, but now the total length is over the 100%. How fix it?
I refactored your code a little bit, but logic is the same as expected - you should take in account white stripes width to calculate colored stripes width https://jsbin.com/supikoz/edit?html,js,output
const coloredWidth = 100 - stripWidth * (colors.length - 1);
const stripPerc = Math.ceil( coloredWidth / colors.length);
You can use flexbox for this. Accodording to your needs, you can style it or add white space. The trick is to use space-between. I would recommend doing the calculation with js and the placement with css.
const colors = ['red', 'green', 'violet'];
const stripPerc = Math.ceil(100 / colors.length) - 1;
colors.forEach(color => {
let strip_element = document.createElement('div');
strip_element.style.background = color;
strip_element.style.width = stripPerc + '%';
document.getElementById('flex').appendChild(strip_element);
})
#flex {
display: flex;
justify-content: space-between;
width: 100%;
height: 50px;
position:relative;
}
<div id='flex'>
</div>
I would like to create dynamically changing graph with timeline in javascript, That would look something like this.
Edit: I would like to decide by myself which node should be in which time slice.
I wonder, is there a library that I can use to do this, or I need to create it by myself ( by simply drawing on canvas )? I tried to find one, however it seems that there are many implementations of timelines and of graphs but the combination of those two is hard to find. The most suitable solution was using gojs. However I can't create a node with two parents in it because it is implemented as a tree data structure.
You may have to play around with the maths, but I hope this will be useful as a starting point:
DEMO: JSFiddle
HTML
<div id='board'>
<div id='titles'></div>
</div>
CSS
#board {
position: relative;
width: 500px;
height: 600px;
background-color:#f83213;
}
#titles {
color: #ffffff;
width: 100%;
height: 18px;
font-size: 12px;
}
#titles div {
display:inline-block;
margin: 10px;
}
.event{
border: 0px;
background-color: #3a2356;
color: #ffffff;
width: 18px;
height: 18px;
position: absolute;
padding: 4px;
font-size: 18px;
z-index: 2;
}
.line{
height: 1px;
width: 60px;
background-color: #3a2356;
position: absolute;
z-index: 1;
}
** JavaScript**
var margin = 20;
var events = {
"A": {
day: 0,
indexInDay: 0,
lineTos: ["D"]
},
"B": {
day: 0,
indexInDay: 1,
lineTos: ["D"]
},
"D": {
day: 1,
indexInDay: 0,
lineTos: ["E","F"]
},
"E": {
day: 2,
indexInDay: 0,
lineTos: null
},
"C": {
day: 0,
indexInDay: 2,
lineTos: ["F"]
},
"F": {
day: 2,
indexInDay: 2,
lineTos: null
},
};
drawAll(events);
function drawAll(events) {
drawTitles(events);
drawEvents(events);
drawLines(events);
}
function drawTitles(events) {
var titles = document.getElementById('titles');
var max = 0;
for (var name in events) {
if (events[name].day > max)
max = events[name].day;
}
for (var i = 0 ; i <= max ; i++)
titles.innerHTML += '<div>' + 'Day' + i + '</div>';
}
function drawEvents(events) {
var board = document.getElementById('board');
for (var name in events) {
var ev = events[name];
var eventDiv = document.createElement('DIV');
board.appendChild(eventDiv);
eventDiv.className = 'event';
setTopLeftEvent(ev, eventDiv);
eventDiv.innerText = name;
}
}
function drawLines(events) {
var board = document.getElementById('board');
for (var name in events) {
var from = events[name];
var tos = from.lineTos;
if (!tos) continue;
for (var j = 0 ; j < tos.length ; j++) {
var to = events[tos[j]];
var lineDiv = document.createElement('DIV');
board.appendChild(lineDiv);
lineDiv.className = 'line';
setTopLeftLine(from, lineDiv);
lineDiv.style.width = margin + 1 * margin * distance(to.indexInDay,from.indexInDay,to.day, from.day) + 'px';
var tan = (to.indexInDay - from.indexInDay) / (to.day - from.day);
lineDiv.style.top = lineDiv.offsetTop + (tan * margin) +'px';
var angle = Math.atan(tan) * 180/Math.PI;
// Code for Safari
lineDiv.style.WebkitTransform = "rotate(" + angle + "deg)";
// Code for IE9
lineDiv.style.msTransform = "rotate(" + angle + "deg)";
// Standard syntax
lineDiv.style.transform = "rotate(" + angle + "deg)";
}
}
}
function distance(x1, y1, x2, y2){
var res = Math.sqrt((y2-y1)*(y2-y1) + (x2-x1)*(x2-x1));
return res;
}
function setTopLeftEvent(event, eventDiv) {
eventDiv.style.left = (margin + event.day * (margin * 2)) + 'px';
eventDiv.style.top = (margin * 2 + event.indexInDay * (margin * 2)) + 'px';
}
function setTopLeftLine(event, lineDiv) {
lineDiv.style.left = (margin + event.day * (margin * 2)) + 'px';
lineDiv.style.top = (margin * 2.5 + event.indexInDay * (margin * 2)) + 'px';
}
As that GoJS sample mentions in the text, it is easy to replace the TreeLayout with a LayeredDigraphLayout and the TreeModel with a GraphLinksModel. Here's what I just did to modify the sample.
Replace go.TreeLayout with go.LayeredDigraphLayout, so that the custom layout no longer inherits from TreeLayout. Change the constructor not to bother setting TreeLayout specific properties. Change the diagram's layout to use LayeredDigraphLayout specific properties:
layout: $(LayeredTreeLayout, // custom layout is defined above
{
angle: HORIZONTAL ? 0 : 90,
columnSpacing: 5,
layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource
}),
Replace that sample's model with a GraphLinksModel holding the data that you want:
// define the node data
var nodearray = [
{ // this is the information needed for the headers of the bands
key: "_BANDS",
category: "Bands",
itemArray: [
{ text: "Day 0" },
{ text: "Day 1" },
{ text: "Day 2" },
{ text: "Day 3" },
{ text: "Day 4" },
{ text: "Day 5" }
]
}
];
var linkarray = [
{ from: "A", to: "D" },
{ from: "B", to: "D" },
{ from: "D", to: "E" },
{ from: "D", to: "F" },
{ from: "C", to: "F" }
];
myDiagram.model = $(go.GraphLinksModel,
{ // automatically create node data objects for each "from" or "to" reference
// (set this property before setting the linkDataArray)
archetypeNodeData: {},
nodeDataArray: nodearray,
linkDataArray: linkarray
});
Without having changed any of the templates or the styling, the result is:
Just to make sure it works, I also tried setting HORIZONTAL = false:
Sorry for the long question.
I have tried to create a meetings on a calendar for a day. I need help to take care of the overlapping intervals.
The code I have written in following :
HTML
<body>
<div id="timeline"></div>
<div id="calendar" class="calendar">
</div>
</body>
CSS
.calendar {
border: 1px solid black;
position: absolute;
width: 600px;
height: 1440px;
left: 60px;
}
.event {
position: absolute;
float: left;
width: 100%;
overflow: auto;
border: 0px solid red;
}
#timeline {
position: absolute;
float: left;
}
JS
function getRandomColor() {
var letters = '0123456789ABCDEF'.split('');
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function creatTimeline(tl) {
var i = 0;
while (i < tl.length) {
var divEl = document.createElement('div');
divEl.style.width = '50px';
divEl.style.height = '120px';
divEl.style.border = '0px solid yellow';
divEl.innerHTML = tl[i];
var timeLine = document.getElementById('timeline');
timeLine.appendChild(divEl);
i++;
}
}
function appendEventDivs(eventArr) {
var i = 0;
while (i < eventArr.length) {
var eventEl = document.createElement('div');
eventEl.className = 'event';
eventEl.style.height = eventArr[i].height;
eventEl.style.top = eventArr[i].top;
eventEl.style.background = eventArr[i].color;
eventEl.style.width = eventArr[i].width;
eventEl.style.left = eventArr[i].left;
eventEl.innerHTML = 'Meeting' + eventArr[i].id;
var cl = document.getElementById('calendar');
cl.appendChild(eventEl);
i++;
}
}
function collidesWith(a, b) {
return a.end > b.start && a.start < b.end;
}
function checkCollision(eventArr) {
for (var i = 0; i < eventArr.length; i++) {
eventArr[i].cols = [];
for (var j = 0; j < eventArr.length; j++) {
if (collidesWith(eventArr[i], eventArr[j])) {
eventArr[i].cols.push(i);
}
}
}
return eventArr;
}
function updateEvents(eventArr) {
eventArr = checkCollision(eventArr);
var arr = [];
arr = eventArr.map(function(el) {
//just to differentiate each event with different colours
el.color = getRandomColor();
el.height = (el.end - el.start) * 2 + 'px';
el.top = (el.start) * 2 + 'px';
el.width = (600 / el.cols.length) + 'px';
return el;
});
return arr;
}
var events = [{
id: 123,
start: 60,
end: 150
}, {
id: 124,
start: 540,
end: 570
}, {
id: 125,
start: 555,
end: 600
}, {
id: 126,
start: 585,
end: 660
}];
var timeline = ['9AM', '10AM', '11AM', '12Noon', '1PM', '2PM', '3PM', '4PM', '5PM', '6PM', '7PM', '8PM', '9PM'];
function getEvents (eventArr) {
eventArr.sort(function(a, b) {
return a.start - b.start;
});
eventArr = updateEvents(eventArr);
appendEventDivs(eventArr);
console.log(eventArr);
//PART 1 - function returning the eventArr with all the required attributes
return eventArr;
};
creatTimeline(timeline);
getEvents(events);
Working fiddle here
Can anybody guide me how to take care of the overlapping intervals so that they appear side-by-side and not on top of each other.
Thanks in advance.
You need to figure out in which column each of the events should be before you can determine their width or left-position. To do this, you need to also store which of the colliding events came before each event:
function checkCollision(eventArr) {
for (var i = 0; i < eventArr.length; i++) {
eventArr[i].cols = [];
eventArr[i].colsBefore=[];
for (var j = 0; j < eventArr.length; j++) {
if (collidesWith(eventArr[i], eventArr[j])) {
eventArr[i].cols.push(j);
if(i>j) eventArr[i].colsBefore.push(j); //also list which of the conflicts came before
}
}
}
return eventArr;
}
Now, we can figure out the column of each event. Once we've done that, we can figure out how wide they should be, and with that, the horizontal positioning should be easy. This should be done inside your updateEvents function. I've got more detailed explanation commented in the comments of the code below.
function updateEvents(eventArr) {
eventArr = checkCollision(eventArr);
var arr=eventArr.slice(0); //clone the array
for(var i=0; i<arr.length; i++){
var el=arr[i];
el.color = getRandomColor();
el.height = (el.end - el.start) * 2 + 'px';
el.top = (el.start) * 2 + 'px';
if(i>0 && el.colsBefore.length>0){ //check column if not the first event and the event has collisions with prior events
if(arr[i-1].column>0){ //if previous event wasn't in the first column, there may be space to the left of it
for(var j=0;j<arr[i-1].column;j++){ //look through all the columns to the left of the previous event
if(el.colsBefore.indexOf(i-(j+2))===-1){ //the current event doesn't collide with the event being checked...
el.column=arr[i-(j+2)].column; //...and can be put in the same column as it
}
}
if(typeof el.column==='undefined') el.column=arr[i-1].column+1; //if there wasn't any free space, but it ito the right of the previous event
}else{
var column=0;
for(var j=0;j<el.colsBefore.length;j++){ //go through each column to see where's space...
if(arr[el.colsBefore[el.colsBefore.length-1-j]].column==column) column++;
}
el.column=column;
}
}else el.column=0;
}
//We need the column for every event before we can determine the appropriate width and left-position, so this is in a different for-loop:
for(var i=0; i<arr.length; i++){
arr[i].totalColumns=0;
if(arr[i].cols.length>1){ //if event collides
var conflictGroup=[]; //store here each column in the current event group
var conflictingColumns=[]; //and here the column of each of the events in the group
addConflictsToGroup(arr[i]);
function addConflictsToGroup(a){
for(k=0;k<a.cols.length;k++){
if(conflictGroup.indexOf(a.cols[k])===-1){ //don't add same event twice to avoid infinite loop
conflictGroup.push(a.cols[k]);
conflictingColumns.push(arr[a.cols[k]].column);
addConflictsToGroup(arr[a.cols[k]]); //check also the events this event conflicts with
}
}
}
arr[i].totalColumns=Math.max.apply(null, conflictingColumns); //set the greatest value as number of columns
}
arr[i].width=(600/(arr[i].totalColumns+1))+'px';
arr[i].left=(600/(arr[i].totalColumns+1)*arr[i].column)+'px';
}
return arr;
}
Working Fiddle: https://jsfiddle.net/ilpo/ftbjan06/5/
I added a few other events to test different scenarios.
Oh, and by the way, absolutely positioned elements can't float.
You already know the top and height of every event, so you could map the calendar and check an event already exist within the area it will occupy, then offset the left value by the number of existing events.