Skip to content

Instantly share code, notes, and snippets.

@jgorene
Last active September 1, 2018 15:55
Show Gist options
  • Save jgorene/967b90c0ca121841d0d4c025456ff5da to your computer and use it in GitHub Desktop.
Save jgorene/967b90c0ca121841d0d4c025456ff5da to your computer and use it in GitHub Desktop.
Timeline | example planning - with vis.js
<html>
<head>
<meta name="description" content="[Test planner-timeline vis.js]">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name="keywords" content="HTML,CSS,XML,JavaScript">
<meta name="author" content="J. Gorene">
<title>Timeline planning example</title>
</head>
<body>
<div id="imgLoad">
<div class="loader"></div><br>
<p id="textLoader">Please wait, loading data...</p>
</div>
<div id="main-div" class="panel-body" style="display:none">
<p>Small light tool to do quick planning or retro-planning in a few clicks, follow small projects or whatever you think you can do with it. In this example, the original dataset is not updated in real time... but could be. You can use it as is in your browser or why not connect it to Google Calendar (e.g) or another calendar api... Otherwise, to add an item, double-click the timeline according to the category or double-click on any bar (item) to edit it. You will discover easily features by using it. Enjoy!</p>
<div id="menu-bar" class="row">
<div class="col-xs-5">
<div class="input-group input-group-xs">
<div class="input-group-btn">
<button id="moveToDate" title="aller à la date" class="btn btn-default"><i>focus on week </i>
<i class="fa fa-bullseye fa-lg custom"></i></button>
</div>
<input id="inputDateToMove" type="text" class="form-control" />
<div class="input-group-btn">
<button id="globalView" class="btn btn-default" title="retour vue globale"><i class="glyphicon glyphicon-fullscreen custom"></i>
<i> global view (fit)</i></button>
</div>
</div>
</div>
<div class="col-xs-2">
<div class="input-group input-group-xs">
<div class="input-group-btn">
<button id="groupadd-btn" class="btn btn-default btn-xs custom">Category +</button>
</div>
<input id="groupadd-input" type="text" class="form-control btn-xs" placeholder="New category">
</div>
</div>
<div class="col-xs-5">
<div class="pull-right hidden-xs">
<button id="zoomIn" class="btn btn-default btn-xs custom"><i class="fa fa-search-plus fa-lg"></i></button>
<button id="zoomOut" class="btn btn-default btn-xs custom"><i class="fa fa-search-minus fa-lg"></i></button>
<button id="moveLeft" class="btn btn-default btn-xs custom"><span class="glyphicon glyphicon-arrow-left"></span></button>
<button id="moveRight" class="btn btn-default btn-xs custom"><span class="glyphicon glyphicon-arrow-right"></span></button>
<button id="exportCSV-btn" class="btn btn-default btn-xs custom" title="initial data"><i class="glyphicon glyphicon-export"></i> CSV</button>
</div>
</div>
</div>
<div style="height:1em"></div>
<div class="row">
<div id="titleTimeline-vis" class="vis-panel vis-top">
<div id="allGroups-div" class="clickable" data-value="allGroups-visible"><span id="puce-showHideAllGroups"></span>
<span id="spanSumCat" class="spanGroup"></span> categories <span id="spanSumEvents" class="spanGroup" title="nombre chantiers"></span> items <span id="spanGlobalDelta" title="delta heures"></span>
</div>
</div>
<div id="timeline-vis"></div>
</div>
</div>
</body>
</html>
var locale = navigator.language;
console.log(navigator.languages, locale, locale.length);
if (locale && locale !== undefined) locale = locale.match(/^\w{2}/)[0];
moment.locale(locale);
$(function() {
document.getElementById("imgLoad").style.display = "none";
document.getElementById("main-div").style.display = "block";
timelineVis();
});
function timelineVis() {
var dataArray = [
["Statut", "Pause", "Taĉhe", "Catégorie", "Ville", "Prévisionnel heures", "Réel",
"Prévisionnel jours", "Jours restant", "Progression", "Date début", "Date fin", "Prévisionnel salariés"
],
["en cours", "BREAK", "0402 - VILLAS", "#cat001", "Mulhouse, France", 1613, 1450, 539, 5, 0.95,
"2016-05-22", "2018-07-11", 2.06794871794872
],
["en cours", "", "0227 - LA CIE", "#cat002", "Strasbourg, France", 10200, 7040, 473, 81, 0.7,
"2016-12-04", "2018-10-25", 4.8433048433048445
],
["en cours", "", "0285 - CONSTRUCTION D'UN IMMEUBLE DE 56 LOGEMENTS", "#cat003", "Amiens, France", 2423,
1840, 262, 56, 0.8, "2017-09-03", "2018-09-20", 1.1094322344322343
],
["en cours", "", "0267 D - TRANCHE 4 / BAT 18", "#cat004", "Lyon, France", 1060, 624, 127, 43,
0.5, "2018-02-27", "2018-09-03", 1.5795020870602265
],
["en cours", "", "0254 - CREATION D'UNE PISCINE COUVERTE", "#cat005", "Paris, France", 709, 858, 107, 31,
0.6, "2018-03-11", "2018-08-16", 1.171712158808933
],
["en cours", "", "0200 - REAMENAGEMENT", "#cat006", "Mulhouse, France", 1500, 624, 139, 81, 0.35,
"2018-04-08", "2018-10-25", 1.5432098765432098
],
["en cours", "", "4500 - MUR COUPE FEU", "#cat007", "Amiens, France", 295, 351, 30, 1, 0.95,
"2018-05-27", "2018-07-05", 1.8918589743589762
],
["en cours", "", "0300 - COPRO VILLE", "#cat005", "Metz, France", 2365, 815, 153, 146, 0.15,
"2018-06-17", "2019-01-24", 1.7652353354408148
],
["prévu", "", "0400 - CAFETERIA", "#cat007", "Paris, France", 400, "", 19, "", "",
"2018-07-08", "2018-08-02", 2.699055330634278
],
["prévu", "", "0299 - MAGS", "#cat007", "Amiens, France", 800, "", 90, "", "",
"2018-09-02", "2019-01-10", 1.1396011396011396
],
["prévu", "", "5210 - TRUC BIDULE", "#cat007", "Amiens, France", 500, "", 29, "", "",
"2018-09-23", "2018-11-01", 2.2104332449160036
],
["prévu", "", "0265 - MJC", "#cat008", "Mulhouse, France", 663, "", 62, "", "",
"2018-11-25", "2019-02-21", 1.3690239867659222
],
["terminé", "", "0282 - ESPACE LOTS 10 A 15", "#cat001", "Paris, France", 5865, 5458, 523, "", 1,
"2016-06-05", "2016-12-03", ""
],
["terminé", "", "0126 - PÔLE CULTUREL", "#cat009", "Paris, France", 1654, 1536, 51, "", 1,
"2018-04-03", "2018-06-18", ""
]
];
var array = dataArray.slice(1, dataArray.length);
$("#puce-showHideAllGroups").removeClass("puce-triangle-right").addClass("puce-triangle-down");
var values = array.filter(function(el) {
return el[2] !== null
});
var container = document.getElementById("timeline-vis");
var types = ["box", "point", "range", "background"];
var categories = values.map(function(el) {
return el[3]
}).filter(function(elem, i, array) {
return array.indexOf(elem) === i
});
var groups = new vis.DataSet,
items = new vis.DataSet,
categories_maxSize = categories.length <= 1000 ? 1000 : 10000;
var groupId = categories_maxSize + 1, // 1000
globalDelta = 0;
for (var g = 0, lg = categories.length; g < lg; g++) {
var nested = [],
count = 0,
groupDelta = 0,
itemContent, tooltip, reel, className, visibleFrameTemplate, delta, classDelta, progress;
for (var k in values) {
var start = moment(values[k][10]).format("DD/MM/YYYY"),
end = moment(values[k][11]).format("DD/MM/YYYY");
(values[k][6]) ? reel = parseInt(values[k][6], 10): reel = 0;
progress = getStatut(moment(values[k][10]), moment(values[k][11]), values[k][9], values[k][1]).progress;
className = getClassName(getStatut(moment(values[k][10]), moment(values[k][11]), values[k][9], values[k][1]).statut, values[k][1]).className;
delta = getDelta(values[k][5], values[k][6]);
if (progress) {
progress = parseInt(progress * 100, 10) + "%";
visibleFrameTemplate = '<div id="' + groupId +
'" class="progress-wrapper"><div class="progress"><label class="progress-label">' + progress +
'<label></div></div>';
} else {
visibleFrameTemplate = 0;
}
(values[k][12] && values[k][12] !== undefined) ? values[k][12] = values[k][12]: values[k][12] = 0;
itemContent = parseInt(values[k][12], 10) + ' employees' + ' (' +
getClassName(getStatut(moment(values[k][10]), moment(values[k][11]), values[k][9], values[k][1]).statut, values[k][1]).statutContent + ')';
if (categories[g] == values[k][3]) {
delta >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
items.add({
id: groupId,
group: groupId,
content: itemContent,
statutContent: getClassName(getStatut(moment(values[k][10]), moment(values[k][11]), values[k][9], values[k][1]).statut, values[k][1]).statutContent,
value: getStatut(moment(values[k][10]), moment(values[k][11]), values[k][9], values[k][1]).progress,
progression: progress,
start: moment(values[k][10]).toDate(),
end: moment(values[k][11]).endOf("day").toDate(),
className: className,
title: getTooltip(getStatut(moment(values[k][10]), moment(values[k][11]), values[k][9], values[k][1]).statut,
delta, classDelta, values[k][2], start, end, values[k][5], reel),
classDelta: classDelta,
delta: delta,
visibleFrameTemplate: visibleFrameTemplate,
"statut": getStatut(moment(values[k][10]), moment(values[k][11]), values[k][9], values[k][1]).statut,
"task": values[k][2],
"break": values[k][1],
"category": values[k][3],
"location": values[k][4],
"estimated": values[k][5],
"worked": (values[k][6]) ? parseInt(values[k][6], 10) : 0,
"estimated days": values[k][7],
"remaining days": values[k][8],
"employees": values[k][12]
});
groups.add({
id: groupId,
content: values[k][2] + ' <span class="' + classDelta + '">&Delta; ' + parseInt(delta, 10) +
"</span>",
groupDates: getGroupDates(values[k][10], values[k][11]),
delta: delta,
order: groupId
});
nested.push(groupId);
count++;
groupDelta += parseInt(delta, 10);
groupId += 1;
}
} // (var k in values)
groupDelta > 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
groups.add({
id: g + 1,
category: categories[g],
title: getCategory(categories[g], count, groupDelta),
content: getContentCategorie(categories[g], count, classDelta, groupDelta),
nestedGroups: nested,
showNested: true,
order: g + 1
});
globalDelta += groupDelta
} // (var g = 0, lg = categories.length; g < lg; g++)
globalDelta > 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
var spanGlobalDelta = document.getElementById("spanGlobalDelta");
spanGlobalDelta.innerHTML = "&Delta; " + Math.round(globalDelta);
spanGlobalDelta.className += classDelta;
var timelineHeight = Math.round($(window).height() * .85) + "px";
var itemsOnUpdate = [];
var options = {
editable: true,
groupEditable: true,
itemsAlwaysDraggable: true,
verticalScroll: true,
orientation: {
axis: "both",
item: "top"
},
groupOrder: "id",
width: "100%",
height: timelineHeight,
stack: false,
onMoving: function(item, callback) {
var group = groups.get(item.group);
console.log(item, item.id === group.id);
if (item.content != null && item.id === group.id) {
callback(item); // send back adjusted item
} else {
callback(null); // cancel updating the item
}
},
onMove: function(item, callback) {
promiseUpdateItem(item).then(value => {
if (value) {
item["statut"] = getStatut(
moment(item.start),
moment(item.end),
item["Progression"],
item["break"]
).statut;
// console.log(item["statut"], item["break"]);
(item["statut"] === "planned" && item["break"] === "BREAK") ? item["break"] = "": item["break"] = item["break"];
item["title"] = changeTooltip(item);
item["content"] = changeContent(item);
item["className"] = getClassName(item["statut"], item["break"]).className;
item["progression"] = (item["statut"] !== "planned") ? parseFloat(item["value"] * 100).toFixed(0) + "%" : "0%";
item.visibleFrameTemplate = '<div id="' + item.id +
'" class="progress-wrapper"><div class="progress"><label class="progress-label">' + item["progression"] +
'<label></div></div>';
groups.update({
id: item.id,
content: item["task"] + ' <span class="' + item["classDelta"] + '">&Delta; ' + parseInt(item["delta"], 10) + "</span>",
groupDates: getGroupDates(item.start, item.end),
delta: item["delta"],
order: item.id
});
callback(item); // send back item as confirmation (can be changed)
timeline.redraw();
// https://github.com/almende/vis/issues/577
} else {
callback(null); // cancel editing item
}
}, function(err) {
console.error('Erreur !');
alert(err);
});
},
onUpdate: function(item, callback) {
prettyPromptItemUpdate('Modify item on update', 'SweatAlert!', item, groups, timeline, callback);
},
onRemove: function(item, callback) {
prettyConfirmRemove('Remove item', 'Do you really want to remove item: ', item, items, groups, timeline, callback)
},
onAdd: function(item, callback) {
var group = groups.get(item.group);
// console.log(getCategoriesIds());
if (item.content != null && getCategoriesIds().indexOf(group.id) !== -1) {
var index = getItemsIds()[getItemsIds().length - 1];
var nestedGroups = group.nestedGroups,
delta = 0,
classDelta;
nestedGroups.push(index + 1);
// Values for New item
item["value"] = 0;
item["break"] = "";
item["progression"] = "0%";
item["type"] = "range";
item["end"] = moment(item.start).add(3, 'months');
item["statutContent"] = getClassName(getStatut(moment(item.start), moment(item.end), 0, "").statut, "").statutContent;
item["content"] = "New item";
item["id"] = index + 1;
item["group"] = index + 1;
item["task"] = "New item";
item["category"] = "#cat00" + group.id;
item["location"] = "";
item["estimated"] = 0;
item["worked"] = 0;
item["delta"] = getDelta(item["estimated"], item["worked"]);
item["estimated days"] = "";
item["remaining days"] = "";
item["employees"] = 0;
item["className"] = getClassName(getStatut(moment(item.start), moment(item.end), 0, "").statut, "").className;
item["delta"] >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
item["title"] = getTooltip(getStatut(moment(item.start), moment(item.end), item.value, item["break"]).statut, item.delta, classDelta,
item["task"], moment(item.start).format("DD/MM/YYYY"), moment(item.end).format("DD/MM/YYYY"), item["estimated"], item["worked"]);
// add item group
groups.add({
id: index + 1,
groupDates: getGroupDates(item.start, item.end),
nestedInGroup: group.id,
showNested: true,
order: index + 1,
content: 'New item' + ' <span class="' + classDelta + '">&Delta; ' + delta + "</span>",
delta: 0
});
// update group category
groups.forEach(function(group) {
if (nestedGroups.indexOf(group.id) !== -1) delta += group.delta;
});
delta >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
// console.log(delta);
groups.update({
id: group.id,
category: group.category,
title: getCategory(group.category, nestedGroups.length, delta),
content: getContentCategorie(group.category, nestedGroups.length, classDelta, delta),
nestedGroups: nestedGroups,
showNested: true,
order: parseInt(group.id, 10)
});
callback(item); // send back adjusted item
document.getElementById("spanSumEvents").innerHTML = items.length;
timeline.redraw();
} else {
callback(null); // cancel updating the item
}
},
visibleFrameTemplate: function(item) {
if (item.visibleFrameTemplate) {
$($("#" + item.id).children()).css("width", item.progression);
if (item["statut"] === "done") {
$($("#" + item.id).children()).css({
color: "white",
background: "#B40404"
});
} else if (item["statut"] === "in progress") {
$($("#" + item.id).children()).css({
color: "#333",
background: "#63ed63"
});
} else if (item["statut"] === "planned") {
// console.log(item["statut"]);
$($("#" + item.id).children()).css({
color: "#333",
background: "#FE9A2E"
});
}
return item.visibleFrameTemplate;
}
},
tooltip: {
overflowMethod: "cap"
}
};
showHideAllGroups = function(button) {
var currentCategories = new vis.DataView(groups, { filter: function(item) { return (item.id < categories_maxSize); } });
if (button.dataset.value == "allGroups-hidden") {
groups.forEach(function(group) {
groups.update({
id: group.id,
visible: true,
showNested: true
})
});
button.dataset.value = "allGroups-visible";
$("#puce-showHideAllGroups").removeClass("puce-triangle-right").addClass("puce-triangle-down");
changeTitleAttr($("#puce-showHideAllGroups").hasClass("puce-triangle-down"));
} else {
groups.forEach(function(group) {
if (group["id"] <= currentCategories.length) groups.update({
id: group.id,
// visible: true,
showNested: false
});
else groups.update({
id: group.id,
visible: false
});
});
button.dataset.value = "allGroups-hidden";
$("#puce-showHideAllGroups").removeClass("puce-triangle-down").addClass("puce-triangle-right");
changeTitleAttr($("#puce-showHideAllGroups").hasClass("puce-triangle-down"));
}
};
document.getElementById('allGroups-div').addEventListener('click', function() {
showHideAllGroups(this);
}, false);
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
items.on('*', function(event, properties) {
console.log(event, properties);
});
// to set tooltip position if necessary
timeline.on('mouseOver', function(prop, timeZone) {
if (prop.what == "item") {
// console.log(prop.what, prop, $('.vis-tooltip'));
$('.vis-tooltip').offset({
left : prop.pageX + 50, // to adapt to its needs
// top : prop.pageY
});
}
});
function getCategoriesIds() {
var targetGroupsIds = [];
groups.forEach(function(group) {
if (group.id <= categories_maxSize) targetGroupsIds.push(group.id);
});
return targetGroupsIds;
}
function getItemsIds() {
var itemIds = [];
items.forEach(function(item) {
if (item.id > categories_maxSize) itemIds.push(item.id);
});
return itemIds;
}
document.getElementById('spanSumCat').innerHTML = categories.length;
document.getElementById("spanSumEvents").innerHTML = items.length;
var labelCurrentTime = document.createElement("label");
labelCurrentTime.setAttribute("id", "labelCurrentTime");
labelCurrentTime.innerHTML = moment(timeline.getCurrentTime()).format("dddd DD MMM YYYY");
timeline.currentTime.bar.appendChild(labelCurrentTime);
timeline.on("click", function(properties) {
var target = properties.event.target;
var event = properties.event;
var group = groups.get(properties.group);
var itemDates = group.groupDates;
// console.log(properties.what, items, groups)
if (properties.what === "group-label" && itemDates) {
timeline.fit();
if (itemDates && itemDates.length) {
var itemDateStart = itemDates[0],
itemDateEnd = itemDates[1];
if (itemDateStart && itemDateEnd) timeline.setWindow({
start: itemDateStart.valueOf() - 864E4,
end: itemDateEnd.valueOf() + 864E4
})
}
event.stopPropagation();
} else if (properties.what === "group-label" && !itemDates && target.tagName === "I") {
prettyPromptCat("Edit category", "SweatAlert!", target, group, groups, timeline, categories_maxSize)
event.stopPropagation();
} else if (properties.what === "item") {
var item = items.get(properties.item);
event.stopPropagation();
}
});
document.getElementById("moveToDate").addEventListener('click', function() {
var dateToMove = document.getElementById("inputDateToMove").value,
itemCustomTime, endDateToMove;
endDateToMove = moment(moment(dateToMove).valueOf() + 5184E5).format("YYYY-MM-DD");
if (items.get(dateToMove)) itemCustomTime = items.get(dateToMove).id;
if (dateToMove && !items.get(dateToMove)) {
timeline.addCustomTime(moment(dateToMove).toISOString(), {
id: dateToMove
});
items.add({
id: dateToMove,
content: moment(dateToMove).format("DD MMM"),
start: moment(dateToMove).toISOString(),
end: moment(moment(endDateToMove).endOf("day")).toISOString(),
type: "background"
});
timeline.setWindow(moment(dateToMove).toISOString(), moment(moment(endDateToMove).endOf("day")).toISOString(), {
animation: {
duration: 1000,
easingFunction: "linear"
}
});
showAllGroupsWeek(dateToMove, endDateToMove);
} else if (dateToMove && dateToMove === itemCustomTime) {
timeline.setWindow(moment(dateToMove).toISOString(), moment(moment(endDateToMove).endOf("day")).toISOString(), {
animation: {
duration: 1000,
easingFunction: "linear"
}
});
showAllGroupsWeek(dateToMove, endDateToMove);
}
}, false);
document.getElementById("globalView").addEventListener('click', function() {
var groupsVisible = [],
itemsVisible = [];
groups.forEach(function(group) {
if (group.id >= categories.length && group.visible === true) groupsVisible.push(group.id)
});
items.forEach(function(item) {
if (groupsVisible.indexOf(item.group) >= 0) itemsVisible.push(item.id)
});
timeline.focus(itemsVisible)
}, false);
function showAllGroups(dateToMove, endDateToMove) {
console.log(groups);
groups.forEach(function(group) {
if (group.id >= categories.length && group.groupDates && group.groupDates.length) {
var startDateGroup = group.groupDates[0],
endDateGroup = group.groupDates[1];
if (!(endDateGroup <=
moment(dateToMove) || startDateGroup >= moment(endDateToMove))) groups.update({
id: group.id,
visible: true,
showNested: true
});
else groups.update({
id: group.id,
visible: false
})
}
})
}
function showAllGroupsWeek(dateToMove, endDateToMove) {
groups.forEach(function(group) {
//console.log(group.groupDates);
if (group.id >= categories.length && group.groupDates && (group.groupDates).length) {
var startDateGroup = group.groupDates[0],
endDateGroup = group.groupDates[1];
//console.log(startDateGroup, endDateGroup, moment(dateToMove));
if (!(endDateGroup <= moment(dateToMove) || startDateGroup >= moment(endDateToMove))) {
groups.update({ id: group.nestedInGroup, visible: true, showNested: true });
groups.update({
id: group.id,
visible: true
});
} else {
groups.update({ id: group.id, visible: false });
}
}
});
}
function move(percentage) {
var range = timeline.getWindow();
var interval = range.end - range.start;
timeline.setWindow({
start: range.start.valueOf() - interval * percentage,
end: range.end.valueOf() - interval * percentage
})
}
document.getElementById("zoomIn").onclick = function() {
timeline.zoomIn(.5);
};
document.getElementById("zoomOut").onclick = function() {
timeline.zoomOut(.5);
};
document.getElementById("moveLeft").onclick = function() {
move(.5);
};
document.getElementById("moveRight").onclick = function() {
move(-.5);
}
document.getElementById('exportCSV-btn').addEventListener('click', function(e) {
// Get data updated
var datavis = (items.get()).map((obj) => {
var newObj = {};
newObj["Statut"] = obj["statut"];
newObj["Break"] = obj["break"];
newObj["Task"] = obj["task"];
newObj["Category"] = obj["category"];
newObj["Location"] = obj["location"];
newObj["Estimated hours"] = obj["estimated"];
newObj["Worked hours"] = obj["worked"];
newObj["Estimated days"] = ""; // obj["estimated days"];
newObj["Remaining days"] = ""; // obj["remaining days"];
newObj["Progress"] = obj["progression"];
newObj["Start"] = moment(new Date(obj["start"])).format("YYYY-MM-DD");
newObj["End"] = moment(new Date(obj["end"])).format("YYYY-MM-DD");
newObj["Employees"] = (isNaN(obj["employees"])) ? obj["employees"] : Math.round(parseFloat(obj["employees"]) * 100) / 100;
return newObj;
});
var delimiter = Papa.parse(Papa.unparse(datavis)).meta.delimiter;
var dataExport = d3.dsvFormat(delimiter).parseRows(Papa.unparse(datavis), (d) => d);
console.log(dataExport);
exportCSV(dataExport, "planning-export")
});
document.getElementById('groupadd-btn').addEventListener('click', function(e) {
var title = (document.getElementById('groupadd-input').value) ? (document.getElementById('groupadd-input').value) : "New category";
console.log(title);
var currentCategories = new vis.DataView(groups, { filter: function(item) { return (item.id < categories_maxSize); } });
console.log(currentCategories);
// update group item
groups.add({
id: d3.max(currentCategories.getIds()) + 1,
category: title,
title: getCategory(title, 0, 0),
content: getContentCategorie(title, 0, classDelta, 0),
nestedGroups: [],
showNested: true,
order: d3.max(currentCategories.getIds()) + 1
});
document.getElementById('spanSumCat').innerHTML = currentCategories.length;
timeline.redraw();
});
$(function() {
// redraw to consider the visibleFrameTemplate option after load to update the timeline but this needs to be reviewed.
timeline.redraw();
changeTitleAttr($("#puce-showHideAllGroups").hasClass("puce-triangle-down"));
});
} // END OF TIMELINE
/* SweetAlert */
function promiseUpdateItem(item) {
return new Promise((resolve, reject) => {
if (item) {
resolve("ok");
} else {
reject("error");
}
});
}
function prettyPromptCat(title, text, target, group, groups, timeline, categories_maxSize) {
console.log(group, groups);
swal(title, text, {
buttons: {
cancel: "Cancel",
catch: {
text: "Change title",
value: "title",
},
defeat: {
text: "Remove category",
value: "remove",
},
},
})
.then((value) => {
switch (value) {
case "remove":
swal("Remove category", 'Do you really want to remove this category and all items it contains?', {
buttons: ["Cancel", "Confirm"]
}
).then(value => {
var nestedGroups = group.nestedGroups;
var globalDelta = 0,
classDelta;
if (value) {
if (nestedGroups && nestedGroups !== undefined) {
console.log(nestedGroups);
nestedGroups.forEach(function(groupId) {
groups.remove(groupId);
});
groups.remove(group);
var currentCategories = new vis.DataView(groups, { filter: function(item) { return (item.id < categories_maxSize); } });
var currentItems = new vis.DataView(groups, { filter: function(item) { return (item.id > categories_maxSize); } });
groups.forEach(function(group) {
if (group.delta) globalDelta += group.delta;
});
console.log(globalDelta);
document.getElementById('spanSumCat').innerHTML = currentCategories.length;
document.getElementById("spanSumEvents").innerHTML = currentItems.length;
globalDelta > 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
var spanGlobalDelta = document.getElementById("spanGlobalDelta");
spanGlobalDelta.innerHTML = "&Delta; " + Math.round(globalDelta);
spanGlobalDelta.className = classDelta;
timeline.redraw();
}
}
});
break;
case "title":
swal({
title: "Change title",
text: '',
content: {
element: "input",
attributes: {
placeholder: "Enter new title...",
type: "text",
},
},
// icon: "success"
}).then(value => {
var nestedGroups = group.nestedGroups;
var delta = 0,
classDelta;
groups.forEach(function(group) {
if (nestedGroups.indexOf(group.id) !== -1) delta += group.delta;
});
delta >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
if (value) {
groups.update({
id: group.id,
category: value,
title: getCategory(value, group.nestedGroups.length, delta),
content: getContentCategorie(value, group.nestedGroups.length, classDelta, delta),
nestedGroups: group.nestedGroups,
showNested: true,
order: parseInt(group.id, 10)
});
}
});
break;
default:
;
}
});
}
function prettyConfirmRemove(title, text, item, items, groups, timeline, callback) {
swal({
title: title,
text: text + item.task + " ?",
}).then(value => {
console.log(value);
if (value && item.content != null) {
var group = groups.get(item.group),
groupParent
delta = 0;
if (group.nestedInGroup !== undefined) groupParent = groups.get(group.nestedInGroup);
if (groupParent !== undefined) {
var nestedGroups = groupParent.nestedGroups;
nestedGroups.splice(nestedGroups.indexOf(group.id), 1);
groups.remove(group.id);
groups.forEach(function(group) {
if (nestedGroups.indexOf(group.id) !== -1) delta += group.delta;
});
delta >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
groups.update({
id: groupParent.id,
category: groupParent.category,
title: getCategory(groupParent.category, nestedGroups.length, delta),
content: getContentCategorie(groupParent.category, nestedGroups.length, classDelta, delta),
nestedGroups: nestedGroups,
showNested: true,
order: parseInt(groupParent.id, 10)
});
} else {
groups.remove(group.id);
}
callback(item); // send back adjusted item
document.getElementById("spanSumEvents").innerHTML = items.length;
timeline.redraw();
} else {
callback(null); // cancel updating the item
}
});
}
function prettyPromptItemUpdate(title, text, item, groups, timeline, callback) {
var html = htmlForSweetAlert(item);
swal({
title: title,
text: text,
content: html,
// icon: "success"
}).then(value => {
if (value) {
var delta = 0,
globalDelta = 0,
classDelta, category;
item["task"] = document.getElementById('inputTitle').value;
item["estimated"] = document.getElementById('inputPrevisionnel').value;
item["worked"] = document.getElementById('inputReel').value;
item["delta"] = getDelta(item["estimated"], item["worked"]);
item["delta"] >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
item["break"] = document.getElementById('selectPause').value;
item["statut"] = getStatut(
moment(item.start),
moment(item.end),
document.getElementById('inputProgress').value,
item["break"]
).statut;
item["value"] = (item["statut"] !== "planned") ? document.getElementById('inputProgress').value : 0;
item["employees"] = document.getElementById('inputSalaries').value;
item["title"] = changeTooltip(item);
item["statutContent"] = getClassName(getStatut(moment(item.start), moment(item.end), 0, "").statut, "").statutContent;
item["content"] = changeContent(item);
item["className"] = getClassName(item["statut"], item["break"]).className;
item["progression"] = (item["statut"] !== "planned") ? parseFloat(document.getElementById('inputProgress').value * 100).toFixed(0) + "%" : "0%";
item["title"] = getTooltip(getStatut(moment(item.start), moment(item.end), item.value, item["break"]).statut, item.delta, classDelta,
item["task"], moment(item.start).format("DD/MM/YYYY"), moment(item.end).format("DD/MM/YYYY"), item["estimated"], item["worked"]);
item["visibleFrameTemplate"] = '<div id="' + item.id +
'" class="progress-wrapper"><div class="progress"><label class="progress-label">' + item["progression"] +
'<label></div></div>';
callback(item); // send back item as confirmation (can be changed)
groups.update({
id: item.id,
content: item["task"] + ' <span class="' + classDelta + '">&Delta; ' + parseInt(item["delta"], 10) + "</span>",
groupDates: getGroupDates(item.start, item.end),
delta: item["delta"],
order: item.id
});
// // update group category
var category = groups.get(groups.get(item.id).nestedInGroup);
var nestedGroups = category.nestedGroups;
// console.log(nestedGroups);
groups.forEach(function(group) {
if (nestedGroups.indexOf(group.id) !== -1) delta += group.delta;
});
delta >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
// console.log(nestedGroups, delta);
groups.update({
id: category.id,
category: category.category,
title: getCategory(category.category, nestedGroups.length, delta),
content: getContentCategorie(category.category, nestedGroups.length, classDelta, delta),
nestedGroups: nestedGroups,
showNested: true,
order: parseInt(category.id, 10)
});
groups.forEach(function(group) {
if (group.delta) globalDelta += group.delta;
});
// console.log(globalDelta);
globalDelta > 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
var spanGlobalDelta = document.getElementById("spanGlobalDelta");
spanGlobalDelta.innerHTML = "&Delta; " + Math.round(globalDelta);
spanGlobalDelta.className = classDelta;
timeline.redraw();
// https://github.com/almende/vis/issues/577
} else {
callback(null); // cancel editing item
}
});
}
function htmlForSweetAlert(item) {
var form = document.createElement('FORM');
form.setAttribute("role", "form");
form.setAttribute("class", "form-horizontal");
var divForm_row01 = document.createElement('DIV');
divForm_row01.setAttribute("class", "form-group");
var divTitle = document.createElement('DIV');
divTitle.setAttribute("class", "col-sm-8");
var labelTitle = document.createElement('LABEL');
labelTitle.innerHTML = "title";
labelTitle.setAttribute("for", "inputTitle");
labelTitle.setAttribute("class", "col-form-label")
var inputTitle = document.createElement('INPUT');
inputTitle.id = "inputTitle";
inputTitle.setAttribute("class", "form-control");
inputTitle.setAttribute("type", "text");
inputTitle.setAttribute("value", item["task"])
var divPause = document.createElement('DIV');
divPause.setAttribute("class", "col-sm-4");
var labelPause = document.createElement('LABEL');
labelPause.innerHTML = "break";
labelPause.setAttribute("for", "selectPause");
labelPause.setAttribute("class", "col-form-label")
var selectPause = document.createElement('SELECT');
selectPause.id = "selectPause";
selectPause.setAttribute("class", "form-control");
var array = ["", "BREAK"];
for (var i = 0; i < array.length; i++) {
var option = document.createElement("option");
option.value = array[i];
option.text = array[i];
selectPause.appendChild(option);
};
selectPause.value = item["break"];
/* row 2 */
var divForm_row02 = document.createElement('DIV');
divForm_row02.setAttribute("class", "form-group");
var divProgress = document.createElement('DIV');
divProgress.setAttribute("class", "col-sm-3");
var labelProgress = document.createElement('LABEL');
labelProgress.innerHTML = "progress";
labelProgress.setAttribute("for", "inputProgress");
labelProgress.setAttribute("class", "col-form-label")
var inputProgress = document.createElement('INPUT');
inputProgress.id = "inputProgress";
inputProgress.setAttribute("class", "form-control");
inputProgress.setAttribute("type", "number");
inputProgress.setAttribute("max", 1);
inputProgress.setAttribute("min", 0);
inputProgress.setAttribute("step", 0.05);
inputProgress.setAttribute("value", item["value"]);
var divSalaries = document.createElement('DIV');
divSalaries.setAttribute("class", "col-sm-3");
var labelSalaries = document.createElement('LABEL');
labelSalaries.innerHTML = "employees";
labelSalaries.setAttribute("for", "inputSalaries");
labelSalaries.setAttribute("class", "col-form-label")
var inputSalaries = document.createElement('INPUT');
inputSalaries.id = "inputSalaries";
inputSalaries.setAttribute("class", "form-control");
inputSalaries.setAttribute("type", "number");
inputSalaries.setAttribute("min", 0);
inputSalaries.setAttribute("step", 1);
inputSalaries.setAttribute("value", item["employees"]);
var divPrevisionnel = document.createElement('DIV');
divPrevisionnel.setAttribute("class", "col-sm-3");
var labelPrevisionnel = document.createElement('LABEL');
labelPrevisionnel.innerHTML = "estimated";
labelPrevisionnel.setAttribute("for", "inputPrevisionnel");
labelPrevisionnel.setAttribute("class", "col-form-label")
var inputPrevisionnel = document.createElement('INPUT');
inputPrevisionnel.id = "inputPrevisionnel";
inputPrevisionnel.setAttribute("class", "form-control");
inputPrevisionnel.setAttribute("type", "number");
inputPrevisionnel.setAttribute("min", 0);
inputPrevisionnel.setAttribute("step", 1);
inputPrevisionnel.setAttribute("value", item["estimated"]);
var divReel = document.createElement('DIV');
divReel.setAttribute("class", "col-sm-3");
var labelReel = document.createElement('LABEL');
labelReel.innerHTML = "worked";
labelReel.setAttribute("for", "inputReel");
labelReel.setAttribute("class", "col-form-label")
var inputReel = document.createElement('INPUT');
inputReel.id = "inputReel";
inputReel.setAttribute("class", "form-control");
inputReel.setAttribute("type", "number");
inputReel.setAttribute("min", 0);
inputReel.setAttribute("step", 1);
inputReel.setAttribute("value", item["worked"]);
/* BUILD FORM */
/* ROW 01*/
divTitle.appendChild(inputTitle);
inputTitle.parentNode.insertBefore(labelTitle, inputTitle);
divPause.appendChild(selectPause);
selectPause.parentNode.insertBefore(labelPause, selectPause);
divForm_row01.appendChild(divTitle);
divForm_row01.appendChild(divPause);
/* ROW 02 */
divProgress.appendChild(inputProgress);
inputProgress.parentNode.insertBefore(labelProgress, inputProgress);
divSalaries.appendChild(inputSalaries);
inputSalaries.parentNode.insertBefore(labelSalaries, inputSalaries);
divPrevisionnel.appendChild(inputPrevisionnel);
inputPrevisionnel.parentNode.insertBefore(labelPrevisionnel, inputPrevisionnel);
divReel.appendChild(inputReel);
inputReel.parentNode.insertBefore(labelReel, inputReel);
divForm_row02.appendChild(divProgress);
divForm_row02.appendChild(divSalaries);
divForm_row02.appendChild(divPrevisionnel);
divForm_row02.appendChild(divReel);
form.appendChild(divForm_row01);
form.appendChild(divForm_row02);
return form;
}
/* HELPERS dataset */
function getCategory(category, count, delta) {
return category + ", " + count + " items" + ", delta: " + Math.round(delta);
}
function getContentCategorie(category, count, classDelta, delta) {
return '<i class="glyphicon glyphicon-pencil" title="long click to edit"></i> ' + category +
' <span class="spanGroup" title="items">' + count + "</span>" + ' <span class="' +
classDelta + '" title="delta">&Delta; ' + Math.round(delta) + "</span>"
}
function getClassName(statut, pause) {
if (statut === "in progress") className = "default";
if (statut === "planned") className = "prevu";
if (statut === "done") className = "termine";
if (statut && pause && pause === "BREAK") {
statut = '<span class="glyphicon glyphicon-exclamation-sign" ></span> ' + pause;
className = "pause"
}
// console.log(statut);
return { 'statutContent': statut, 'className': className };
}
function getStatut(start, end, progress, pause) {
var currentDate = moment(),
statut;
// console.log(progress, pause);
if (start <= currentDate && end >= currentDate) {
statut = "in progress";
progress = progress;
className = "default";
} else if (start <= currentDate && end <= currentDate) {
statut = "done";
progress = (pause === "BREAK") ? progress : 1;
className = "termine";
} else if (start >= currentDate && end > currentDate) {
statut = "planned";
progress = 0;
className = "prevu";
}
// console.log(statut, progress);
return { 'statut': statut, 'progress': progress, "className": className };
}
function getDelta(previsionnel, reel) {
var delta = 0;
if (previsionnel && reel) {
delta = parseInt(previsionnel, 10) - parseInt(reel, 10);
} else {
delta = parseInt(previsionnel, 10);
reel = 0
}
return delta
}
function getTooltip(statut, delta, classDelta, title, start, end, previsionnel, reel) {
// console.log(statut, reel);
var tooltip;
if (statut === "in progress" || statut === "done") {
delta >= 0 ? classDelta = "spanDeltaGreen" : classDelta = "spanDeltaRed";
tooltip = title + "<br>du " + start + " au " + end + "<br>prévisionnel : " + previsionnel +
"hrs" + "<br>réel : " + reel + "hrs" + ' <span class="' + classDelta +
'">&Delta;' + delta + "</span>";
} else {
tooltip = title + "<br>du " + start +
" au " + end + "<br>prévisionnel : " + previsionnel + "hrs";
}
return tooltip
}
function getGroupDates(start, end) {
if (isValidDate(moment(start).toDate()) && isValidDate(moment(end).endOf("day").toDate())) {
groupDates = [moment(start).toDate(), moment(end).endOf("day").toDate()]
} else {
groupDates = [null, null];
}
return groupDates;
}
/* Change with modal */
function changeTooltip(item) {
var tooltip = item["task"] + "<br>du " + moment(item.start).format("DD/MM/YYYY") + " au " + moment(item.end).format("DD/MM/YYYY");
tooltip += "<br>prévisionnel : " + item["estimated"] + "hrs" + "<br>réel : " + item["worked"] + "hrs";
tooltip += ' <span class="' + item.classDelta + '">&Delta;' + item.delta + "</span>";
return tooltip;
}
function changeContent(item) {
var content = parseInt(item["employees"], 10) + ' employees' + ' (' +
getClassName(getStatut(moment(item.start), moment(item.end), item["value"], item["break"]).statut, item["break"]).statutContent + ')';
return content;
}
function changeVisibleFrameTemplate(item) {
console.log(item);
if (item["value"]) {
progress = parseInt(item["value"] * 100, 10) + "%";
visibleFrameTemplate = '<div id="' + item.id +
'" class="progress-wrapper"><div class="progress"><label class="progress-label">' + progress +
'<label></div></div>';
} else {
visibleFrameTemplate = 0;
}
return visibleFrameTemplate;
}
// To hide show all groups
function changeTitleAttr(flag) {
//console.log($("#puce-showHideAllGroups").hasClass("puce-triangle-down"));
if (flag) {
document.getElementById('allGroups-div').title = 'collapse all';
} else {
document.getElementById('allGroups-div').title = 'expand all';
}
}
/* Export data using with FileSaver.js and PapaParse libraries*/
function exportCSV(data, filename) {
//console.log(data);
dataToCSV = Papa.unparse(data);
var blob = new Blob([dataToCSV], { type: "text/csv" });
saveAs(blob, filename + ".csv");
}
// https://stackoverflow.com/questions/7445328/check-if-a-string-is-a-date-value
function isValidDate(d) {
var formats = [
moment.ISO_8601,
"YYYY-MM-DD"
];
return moment(d, formats, true).isValid();
}
/**
* CUstom date picker jQuery
*/
$(function() {
$("#inputDateToMove").datepicker();
$("#inputDateToMove").datepicker("setDate", moment().format("YYYY-MM-DD"));
});
$.datepicker.setDefaults({
dateFormat: "yy-mm-dd",
changeYear: true,
showWeek: true,
weekHeader: weekHeader(),
monthNames: monthNames(),
dayNamesMin: dayNamesMin(),
firstDay: 1
});
function dayNamesMin() {
if (locale === "fr") {
return ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"];
} else {
return ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
}
}
function monthNames() {
if (locale === "fr") {
return [
"Janvier",
"Février",
"Mars",
"Avril",
"Mai",
"Juin",
"Juillet",
"Août",
"Septembre",
"Octobre",
"Novembre",
"Decembre"
];
} else {
return [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
}
}
function weekHeader() {
if (locale === "fr") {
return ["Sem"];
} else {
return ["Wk"];
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/locale/fr.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/4.3.7/papaparse.min.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.20.1/vis.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.13/moment-timezone-with-data-2012-2022.min.js"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
body {
padding : 10px;
line-height : 1;
}
@media print {
.printButton {
display : none;
}
}
.loader {
border : #f3f3f3 solid 16px;
border-radius : 50%;
border-top : 16px solid #3498db;
width : 120px;
height : 120px;
margin : 10% auto 0 auto;
animation : spin 2s linear infinite;
}
@keyframes spin {
0% {
transform : rotate(0deg);
}
100% {
transform : rotate(360deg);
}
}
#imgLoad {
width : 100%;
text-align : center;
}
.input-group-xs {
width : 320px;
}
.input-group-xs > .form-control, .input-group-xs > .multiselect.dropdown-toggle.btn.btn-default, .input-group-xs > .input-group-addon, .input-group-xs > .input-group-btn > .btn {
height : 22px;
padding : 1px 5px;
font-size : 14px;
line-height : 1;
}
.custom {
opacity : 0.9;
}
.custom:hover {
opacity : 1;
color : blue;
}
.vis-time-axis .grid.vis-odd {
background : #f5f5f5;
}
.clickable {
cursor : pointer;
}
#inputDateToMove {
text-align : center;
min-width : 90px;
}
.puce-triangle-right:before {
content : "\25B6"; /* plus "\2b"; */
margin-left : -5px;
font-size : 16px;
font-weight : 900;
vertical-align : middle;
}
.puce-triangle-down:before {
content : "\25BC"; /* minus "\2212";*/
margin-left : -3px;
font-size : 16px;
font-weight : 900;
vertical-align : middle;
}
#titleTimeline-vis {
position : absolute;
width : 251px;
height : 52px;
padding : 10px 5px;
margin : 1px 1px;
border-right : 1px solid #bfbfbf;
z-index : 1;
}
.spanGroup {
border : rgba(0, 0, 0, 0.3) solid 1px;
border-radius : 50%;
text-align : center;
padding : 0 5px;
margin-right : 2px;
}
.spanDeltaGreen {
border : rgba(0, 0, 0, 0.3) solid 1px;
border-radius : 5%;
text-align : center;
padding : 0 2px;
font-size : 0.8em;
white-space : pre;
background : #adffb1;
background : linear-gradient(to right, #adffb1 0%, #ffffff 100%);
}
.spanDeltaRed {
border : rgba(0, 0, 0, 0.3) solid 1px;
border-radius : 5%;
text-align : center;
padding : 0 2px;
font-size : 0.8em;
white-space : pre;
background : #ffd9da;
background : linear-gradient(to right, #ffd9da 0%, #ffffff 100%);
}
.progress-wrapper {
background : white;
width : 100%;
height : 12px;
text-align : right;
position : relative;
overflow : hidden;
}
.progress {
height : 100%;
position : absolute;
left : 0;
top : 0;
background : #63ed63;
}
.progress-label {
font-size : 1em;
vertical-align : top;
}
.vis-item.pause {
color : rgba(0, 0, 0, 0.5);
background-color : rgba(213, 221, 246, 0.5);
border-color : rgba(151, 176, 248, 0.5);
}
.vis-item.pause:hover {
color : rgba(0, 0, 0, 1);
background-color : rgba(213, 221, 246, 1);
border-color : rgba(151, 176, 248, 1);
}
.vis-item.termine {
color : rgba(0, 0, 0, 0.5);
background-color : rgba(213, 221, 246, 0.5);
border-color : rgba(151, 176, 248, 0.5);
}
.vis-item.prevu {
background-color : #FE9A2E;
/*border-color : darkred;*/
}
.vis-panel.vis-top {
background : #e7ebf5;
background: linear-gradient(to bottom, hsl(222,70%,86%) 1%,hsl(0,0%,100%) 100%);
border : #bfbfbf 1px;
}
.vis-panel.vis-left {
background : #e7ebf5;
background: linear-gradient(to right, hsl(222,70%,86%) 1%,hsl(0,0%,100%) 100%);
width : 250px;
white-space : nowrap;
}
.vis-nested-group {
background : transparent;
white-space : normal;
}
.vis-item .vis-item-content {
white-space : nowrap;
box-sizing : border-box;
padding : 2px;
}
.vis-time-axis .grid.vis-odd {
background : #f5f5f5;
}
.vis-time-axis .vis-grid.vis-saturday, .vis-time-axis .vis-grid.vis-sunday {
background : rgba(50, 50, 50, 0.1);
}
.vis-time-axis .vis-text.vis-saturday, .vis-time-axis .vis-text.vis-sunday {
color : rgba(0, 0, 0, 0.8);
}
#labelCurrentTime {
margin-top: 20px;
margin-left: 5px;
color:red;
font-size:12px;
width:200px;
white-space: nowrap;
}
.vis-time-axis .vis-text {
position: absolute;
color: #4d4d4d;
padding: 6px;
overflow: hidden;
box-sizing: border-box;
white-space: nowrap;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.20.1/vis.min.css" rel="stylesheet" />
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet" />

Timeline | example planning - with vis.js

Simple working track to play around timeline vis.js ...

A Pen by Jean Gorene on CodePen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment