ng2-gantt
Version:
Angular 2 Gantt
540 lines (459 loc) • 19.3 kB
text/typescript
import { Injectable } from '@angular/core';
import { GanttConfig } from './gantt-config.service';
import { IBarStyle, Task, IScale, Zooming } from '../interfaces';
import { GroupByPipe } from '../../shared/pipes/groupBy.pipe';
export class GanttService {
public rowHeight: number = 0;
public hourCellWidth: number = 60; // change to 60 so minutes can been seen more easily
public hoursCellWidth: number = this.hourCellWidth * 25;
public cellWidth: number = 0;
public windowInnerWidth: number = 0;
public activityHeight: number = 0;
public barHeight: number = 0;
public barLineHeight: number = 0;
public barTop: number = 0;
public barMoveable: boolean = false;
public gridWidth: number = 560;
private barStyles: IBarStyle[] = [
{ status: "information", backgroundColor: "rgb(18,195, 244)", border: "1px solid #2196F3", progressBackgroundColor: "#2196F3" },
{ status: "warning", backgroundColor: "#FFA726", border: "1px solid #EF6C00", progressBackgroundColor: "#EF6C00" },
{ status: "error", backgroundColor: "#EF5350", border: "1px solid #C62828", progressBackgroundColor: "#C62828" },
{ status: "completed", backgroundColor: "#66BB6A", border: "1px solid #2E7D32", progressBackgroundColor: "#2E7D32" }
];
public TASK_CACHE: any[];
public TIME_SCALE: any[];
constructor() {
let _ganttConfig = new GanttConfig();
this.rowHeight = _ganttConfig.rowHeight;
this.cellWidth = _ganttConfig.cellWidth;
this.activityHeight = _ganttConfig.activityHeight;
this.barHeight = _ganttConfig.barHeight;
this.barLineHeight = _ganttConfig.barLineHeight;
this.barTop = _ganttConfig.rowHeight;
this.barMoveable = _ganttConfig.barMoveable;
}
private calculateBarWidth(start: Date, end: Date, hours?: boolean): number {
if (typeof start === "string") {
start = new Date(start);
}
if (typeof end === "string") {
end = new Date(end);
}
let days = this.calculateDiffDays(start, end);
let width: number = days * this.cellWidth + days;
if (hours) {
width = days * this.hourCellWidth * 24 + days * 24;
}
return width;
}
private calculateBarLeft(start: Date, scale: any[], hours?: boolean): number {
var left: number = 0;
var hoursInDay: number = 24;
if (start != null) {
if (typeof start === "string") {
start = new Date();
}
for (var i = 0; i < scale.length; i++) {
if (start.getDate() === scale[i].getDate()) {
if (hours) {
left = i * hoursInDay * this.hourCellWidth + hoursInDay * i + this.calculateBarLeftDelta(start, hours);
} else {
left = i * this.cellWidth + i + this.calculateBarLeftDelta(start, hours);
}
break;
}
}
}
return left;
}
/** Calculates the height of the gantt grid, activity and vertical scroll */
public calculateGanttHeight(): string {
return `${this.TASK_CACHE.length * this.rowHeight + this.rowHeight * 3}px`;
}
private calculateBarLeftDelta(start: Date, hours?: boolean): number {
var offset: number = 0;
var hoursInDay: number = 24;
var minutesInHour: number = 60;
var secondsInHour: number = 3600;
var startHours: number = start.getHours() + start.getMinutes() / minutesInHour + start.getSeconds() / secondsInHour;
if (hours) {
offset = this.hoursCellWidth / hoursInDay * startHours - startHours;
} else {
offset = this.cellWidth / hoursInDay * startHours;
}
return offset;
}
public isParent(treePath: string): boolean {
try {
var depth = treePath.split('/').length;
if (depth === 1) {
return true;
}
}
catch (err) {
return false;
}
return false;
}
public isChild(treePath: string) {
if (this.isParent(treePath)) {
return '0px';
}
return '20px';
}
/** Calculate the bar styles */
public calculateBar(task: any, index: number, scale: any, hours?: boolean) {
var barStyle = this.getBarStyle(task.status);
//console.log(this.barTop * index + 2)
return {
'top': this.barTop * index + 2 + 'px',
'left': this.calculateBarLeft(task.start, scale, hours) + 'px',
'height': this.barHeight + 'px',
'line-height': this.barLineHeight + 'px',
'width': this.calculateBarWidth(task.start, task.end, hours) + 'px',
'background-color': barStyle["background-color"],
'border': barStyle["border"]
}
}
/** Get the bar style based on task status */
private getBarStyle(taskStatus: string = ""): any {
var style = {};
try {
taskStatus = taskStatus.toLowerCase();
} catch (err) {
taskStatus = "";
}
switch (taskStatus) {
case "information":
style["background-color"] = this.barStyles[0].backgroundColor;
style["border"] = this.barStyles[0].border;
break;
case "warning":
style["background-color"] = this.barStyles[1].backgroundColor;
style["border"] = this.barStyles[1].border;
break;
case "error":
style["background-color"] = this.barStyles[2].backgroundColor;
style["border"] = this.barStyles[2].border;
break;
case "completed":
style["background-color"] = this.barStyles[3].backgroundColor;
style["border"] = this.barStyles[3].border;
break;
default:
style["background-color"] = "rgb(18,195, 244)";
style["border"] = "1px solid #2196F3";
break;
}
return style;
}
/** Get the progresss bar background colour based on task status */
public getBarProgressStyle(taskStatus: string = ""): any {
var style = {};
try {
taskStatus = taskStatus.toLowerCase();
} catch (err) {
taskStatus = "";
}
switch (taskStatus) {
case "information":
style["background-color"] = this.barStyles[0].progressBackgroundColor;
break;
case "warning":
style["background-color"] = this.barStyles[1].progressBackgroundColor;
break;
case "error":
style["background-color"] = this.barStyles[2].progressBackgroundColor;
break;
case "completed":
style["background-color"] = this.barStyles[3].progressBackgroundColor;
break;
default:
style["background-color"] = this.barStyles[0].progressBackgroundColor;
break;
}
return style;
}
/** Calculates the bar progress width in pixels given task percent complete */
public calculateBarProgress(width: number, percent: number): string {
if (typeof percent === "number") {
if (percent > 100) {
percent = 100;
}
let progress: number = (width / 100) * percent - 2;
return `${progress}px`;
}
return `${0}px`;
}
/** Calculates the difference in two dates and returns number of days */
public calculateDiffDays(start: Date, end: Date): number {
try {
let oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds /ms
let diffDays = Math.abs((start.getTime() - end.getTime()) / (oneDay));
let days = diffDays; // don't use Math.round as it will draw an incorrect bar
return days;
} catch (err) {
return 0;
}
}
/** Calculates the difference in two dates and returns number of hours */
public calculateDuration(task: any): string {
try {
if (task.start != null && task.end != null) {
let oneHour = 60 * 60 * 1000;
let diffHours = (Math.abs((task.start.getTime() - task.end.getTime()) / oneHour));
let duration = diffHours;
if (duration > 24) {
return `${Math.round(duration / 24)} day(s)`; // duration in days
} else if (duration > 1) {
return `${Math.round(duration)} hr(s)`; // duration in hours
} else {
let minutes = duration * 60;
if (minutes < 1) {
return `${Math.round(minutes * 60)} second(s)`; // duration in seconds
}
return `${Math.round(minutes)} min(s)` // duration in minutes
}
}
return '';
} catch (err) {
return '';
}
}
calculateTotalDuration(tasks: any[]): string {
try {
tasks = tasks.filter(t => t.parentId === t.id); // only calculate total duration with parent tasks
let totalHours = 0;
let oneHour = 60 * 60 * 1000;
for (let i = 0; i < tasks.length; i++) {
let start = tasks[i].start;
let end = tasks[i].end;
if (start != null && end != null) {
let duration = Math.abs(tasks[i].end.getTime() - tasks[i].start.getTime()) / oneHour; // duration in hours
totalHours += duration;
}
}
if (totalHours === 0) {
return '';
}
if (totalHours > 24) {
return `${Math.round(totalHours / 24)} day(s)`; // duration in days
} else if (totalHours > 1) {
return `${Math.round(totalHours)} hr(s)`; // duration in hours
} else {
let minutes = totalHours * 60;
if (minutes < 1) {
return `${Math.round(minutes * 60)} second(s)`; // duration in seconds
}
return `${Math.round(minutes)} min(s)` // duration in minutes
}
} catch (err) {
return '';
}
}
/** Calculate the total percentage of a group of tasks */
calculateTotalPercentage(node: any): number {
var totalPercent: number = 0;
var children = node.children;
if (children.length > 0) {
children.forEach((child: any) => {
totalPercent += isNaN(child.percentComplete) ? 0 : child.percentComplete;
});
return Math.ceil(totalPercent / children.length);
} else {
return isNaN(node.percentComplete) ? 0 : node.percentComplete;
}
}
/** Calculate the total percent of the parent task */
calculateParentTotalPercentage(parent: any, tasks: any[]) {
var children = tasks.filter((task:any) => {
return task.parentId === parent.id && task.id != parent.id
}); // get only children tasks ignoring parent.
var totalPercent: number = 0;
if (children.length > 0) {
children.forEach((child:any) => {
totalPercent += isNaN(child.percentComplete) ? 0 : child.percentComplete;
});
return Math.ceil(totalPercent / children.length);
} else {
return isNaN(parent.percentComplete) ? 0 : parent.percentComplete;
}
}
/** Calculate the gantt scale range given the start and end date of tasks*/
public calculateScale(start: Date = new Date(), end: Date = this.addDays(start, 7)) {
let scale: any[] = [];
try {
while (start.getTime() <= end.getTime()) {
scale.push(start);
start = this.addDays(start, 1);
}
return scale;
} catch (err) {
return scale;
}
}
/** Determines whether given date is a weekend */
public isDayWeekend(date: Date): boolean {
let day = date.getDay();
if (day === 6 || day === 0) {
return true;
}
return false;
}
/** Add x number of days to a date object */
public addDays(date: Date, days: number): Date {
let result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
//** Remove x number of days from a date object */
public removeDays(date: Date, days: number): Date {
let result = new Date(date);
result.setDate(result.getDate() - days);
return result;
}
/** Calculates the grid scale for gantt based on tasks start and end dates */
public calculateGridScale(tasks: Task[]): IScale {
var start: Date;
var end: Date;
var dates = tasks.map((task:any) => {
return {
start: new Date(task.start),
end: new Date(task.end)
}
});
start = new Date(Math.min.apply(null, dates.map(function (t) {
return t.start;
})));
end = new Date(Math.max.apply(null, dates.map(function (t) {
return t.end;
})));
return {
start: start,
end: end
}
}
/** Create an hours array for use in time scale component */
public getHours(cols: number): string[] {
var hours: string[] = [];
while (hours.length <= cols * 24) {
for (var i = 0; i <= 23; i++) {
if (i < 10) {
hours.push('0' + i.toString());
} else {
hours.push(i.toString());
}
}
}
return hours;
}
public getComputedStyle(element: any, attribute: any) {
return parseInt(document.defaultView.getComputedStyle(element)[attribute], 10);
}
//TODO(dale): determine whether this is needed
public calculateContainerWidth(): number {
this.windowInnerWidth = window.innerWidth;
let containerWidth = (innerWidth - 18);
return containerWidth;
}
public calculateActivityContainerDimensions(): any {
var scrollWidth: number = 18;
this.windowInnerWidth = window.innerWidth;
let width = this.windowInnerWidth - this.gridWidth - scrollWidth;
return { height: this.activityHeight, width: width };
}
/** Set the vertical scroll top positions for gantt */
public scrollTop(verticalScrollElem: any, ganttGridElem: any, ganttActivityAreaElem: any) {
let verticalScrollTop = verticalScrollElem.scrollTop;
let scroll = this.setScrollTop;
// debounce
if (verticalScrollTop !== null && verticalScrollTop !== undefined) {
setTimeout(function() {
scroll(verticalScrollTop, ganttActivityAreaElem);
scroll(ganttActivityAreaElem.scrollTop, ganttGridElem);
}, 50);
}
}
/** Group data by id , only supports one level*/
public groupData(tasks: any): any {
var merged:any = [];
var groups:any = new GroupByPipe().transform(tasks, (task:any) => {
return [task.treePath.split('/')[0]]
});
return merged.concat.apply([], groups);
}
/** Create tree of data */
public transformData(input: any): any {
var output:any = [];
for (var i = 0; i < input.length; i++) {
var chain:any = input[i].id.split('/');
var currentNode:any = output;
for (var j = 0; j < chain.length; j++) {
var wantedNode:any = chain[j];
var lastNode:any = currentNode;
for (var k = 0; k < currentNode.length; k++) {
if (currentNode[k].name == wantedNode) {
currentNode = currentNode[k].children;
break;
}
}
// If we couldn't find an item in this list of children
// that has the right name, create one:
if (lastNode == currentNode) {
//TODO(dale): determine way to show percent complete on correct child
var newNode:any = currentNode[k] = {
name: wantedNode,
percentComplete: input[i].percentComplete,
start: input[i].start,
end: input[i].end,
children: []
};
currentNode = newNode.children;
}
}
}
return output;
}
/** Checks whether any new data needs to be added to task cache */
public doTaskCheck(tasks: any[], treeExpanded: boolean): boolean {
var cachedTaskIds = this.TASK_CACHE.map((task:any) => { return task.id });
var itemsToCache: any[] = [];
if (treeExpanded) {
// push children and parent tasks that are not cached
tasks.filter((task:any) => {
return cachedTaskIds.indexOf(task.id) === -1
}).forEach((task:any) => {
itemsToCache.push(task);
})
} else {
// only look at tasks that are not cached
tasks.filter((task:any) => {
return cachedTaskIds.indexOf(task.id) === -1 && task.treePath.split('/').length === 1
}).forEach((task:any) => {
itemsToCache.push(task);
});
}
itemsToCache.forEach((item:any) => {
this.TASK_CACHE.push(item);
});
if (itemsToCache.length > 0) {
return true;
}
return false;
}
/** Set a id prefix so CSS3 query selector can work with ids that contain numbers */
public setIdPrefix(id: string): string {
return `_${id}`;
}
// /** Remove the id prefix to allow querying of data */
// public removeIdPrefix(id: string): string {
// return id.substring(1, id.length - 1);
// }
/** Set the scroll top property of a native DOM element */
private setScrollTop(scrollTop: number, element: any): void {
if (element !== null && element !== undefined) {
element.scrollTop = scrollTop;
}
}
}