WIP: Gantt chart for project

This commit is contained in:
Ray 2023-12-31 19:10:46 +00:00
parent d30d60ae91
commit db458dc3a0
7 changed files with 296 additions and 54 deletions

View File

@ -7,8 +7,8 @@ Array.prototype.countMany=function(...a){let b=0;a.forEach(c=>{b+=this.count(c.p
Array.prototype.joinIfNotNullOrWhitespace=function(a){let b="";for(let c=0;c<this.length;c++)String.isNullOrWhitespace(this[c])||(String.isNullOrWhitespace(b)||(b+=a),b+=this[c]);return b};Array.prototype.first=function(a,b){for(let c=0;c<this.length;c++)if("undefined"!=typeof this[c][a]&&this[c][a]==b)return this[c];return null};Array.prototype.forEachTree=function(a,b){for(let c=0;c<this.length&&!1!==b(this[c])&&(0>=this[c][a].length||!1!==this[c][a].forEachTree(a,b));c++);};
Array.prototype.indexes=function(a,b){let c=[];for(let d=0;d<this.length;d++)"undefined"!=typeof this[d][a]&&this[d][a]==b&&c.push(d);return c};Array.prototype.orderBy=function(a){this.sort(function(b,c){return b[a]<c[a]?-1:b[a]>c[a]?1:0});return this};Array.prototype.orderByDesc=function(a){this.sort(function(b,c){return b[a]<c[a]?1:b[a]>c[a]?-1:0});return this};
Array.prototype.remove=function(a){let b=[];for(let c=0;c<this.length;c++)this[c]==a&&b.push(c);for(a=b.length-1;0<=a;a--)this.removeAt(b[a]);return this};Array.prototype.removeAt=function(a){if(0>a||a>=this.length)return this;this.splice(a,1);return this};Array.prototype.removeRange=function(a){for(let b=0;b<a.length;b++)this.remove(a[b]);return this};Array.prototype.select=function(a,b){let c=[];for(let d=0;d<this.length;d++)"undefined"!=typeof this[d][a]&&this[d][a]==b&&c.push(this[d]);return c};
Array.prototype.selectMany=function(...a){let b=this;a.forEach(c=>{b=b.select(c.propName,c.value)});return b};Array.prototype.toList=function(a){let b=[];this.forEach(c=>{b.push(c[a])});return b};Array.prototype.sortTree=function(a,b){this.orderBy(b);for(let c=0;c<this.length;c++)0>=this[c][a].length||this[c][a].orderBy(b)};Boolean.isFalse=function(a){return String.isNullOrUndefined(a)?!0:a.toString().containsCI("false","f","y","0","x")};Boolean.isTrue=function(a){return String.isNullOrUndefined(a)?!1:a.toString().containsCI("true","t","n","1","o")};Boolean.ifTrue=function(a,b,c){return Boolean.isTrue(a)?b:c};Date.addDays=function(a,b){a=new Date(a);a.addDays(b);return a};Date.addMonths=function(a,b){a=new Date(a);a.addMonths(b);return a};Date.addYears=function(a,b){a=new Date(a);a.addYears(b);return a};Date.diffDays=function(a,b){return(b.getTime()-a.getTime())/864E5};Date.today=function(){let a=new Date;a.setHours(0);a.setMinutes(0);a.setSeconds(0);a.setMilliseconds(0);return a};Date.prototype.addDays=function(a){this.setDate(this.getDate()+parseInt(a))};
Date.prototype.addMonths=function(a){this.setMonth(this.getMonth()+parseInt(a))};Date.prototype.addYears=function(a){this.setFullYear(this.getFullYear()+parseInt(a))};
Array.prototype.selectMany=function(...a){let b=this;a.forEach(c=>{b=b.select(c.propName,c.value)});return b};Array.prototype.toList=function(a){let b=[];this.forEach(c=>{b.push(c[a])});return b};Array.prototype.sortTree=function(a,b){this.orderBy(b);for(let c=0;c<this.length;c++)0>=this[c][a].length||this[c][a].orderBy(b)};Boolean.isFalse=function(a){return String.isNullOrUndefined(a)?!0:a.toString().containsCI("false","f","y","0","x")};Boolean.isTrue=function(a){return String.isNullOrUndefined(a)?!1:a.toString().containsCI("true","t","n","1","o")};Boolean.ifTrue=function(a,b,c){return Boolean.isTrue(a)?b:c};Date.addDays=function(a,b){a=new Date(a);a.addDays(b);return a};Date.addMonths=function(a,b){a=new Date(a);a.addMonths(b);return a};Date.addYears=function(a,b){a=new Date(a);a.addYears(b);return a};Date.max=function(a,b){return null==a?b:null==b?a:new Date(a)<=new Date(b)?new Date(b):new Date(a)};Date.min=function(a,b){return null==a?b:null==b?a:new Date(a)<=new Date(b)?new Date(a):new Date(b)};Date.diffDays=function(a,b){return Math.ceil(((new Date(b)).getTime()-(new Date(a)).getTime())/864E5)};
Date.today=function(){let a=new Date;a.setHours(0);a.setMinutes(0);a.setSeconds(0);a.setMilliseconds(0);return a};Date.prototype.addDays=function(a){this.setDate(this.getDate()+parseInt(a))};Date.prototype.addMonths=function(a){this.setMonth(this.getMonth()+parseInt(a))};Date.prototype.addYears=function(a){this.setFullYear(this.getFullYear()+parseInt(a))};
Date.prototype.toCString=function(a){a=a.replace("fffffff",this.getMilliseconds().toString().padStart(7,"0"));a=a.replace("ffffff",this.getMilliseconds().toString().padStart(6,"0"));a=a.replace("fffff",this.getMilliseconds().toString().padStart(5,"0"));a=a.replace("yyyy",this.getFullYear().toString().padStart(4,"0"));a=a.replace("MMMM","{1}");a=a.replace("dddd","{2}");a=a.replace("ffff",this.getMilliseconds().toString().padStart(4,"0"));a=a.replace("yyy",this.getFullYear().toString().padStart(3,"0"));
a=a.replace("MMM","{3}");a=a.replace("ddd","{4}");a=a.replace("fff",this.getMilliseconds().toString().padStart(3,"0"));a=a.replace("zzz","");a=a.replace("yy",this.getFullYear().toString().slice(-2));a=a.replace("MM",(this.getMonth()+1).toString().padStart(2,"0"));a=a.replace("dd",this.getDate().toString().padStart(2,"0"));a=a.replace("HH",this.getHours().toString().padStart(2,"0"));a=a.replace("hh",(12<this.getHours()?this.getHours()-12:this.getHours()).toString().padStart(2,"0"));a=a.replace("mm",
this.getMinutes().toString().padStart(2,"0"));a=a.replace("ss",this.getSeconds().toString().padStart(2,"0"));a=a.replace("ff",this.getMilliseconds().toString().padStart(2,"0"));a=a.replace("tt","{5}");a=a.replace("zz","");a=a.replace("y",this.getFullYear().toString());a=a.replace("M",(this.getMonth()+1).toString());a=a.replace("d",this.getDate().toString());a=a.replace("H",this.getHours().toString());a=a.replace("h",(12<this.getHours()?this.getHours()-12:this.getHours()).toString());a=a.replace("m",

235
canvas.js
View File

@ -1,6 +1,10 @@
class Canvas {
_container = null;
_flowContainer = null;
_canvasContainer = null;
_ctx = null;
_autoSize = true;
_padding = {
Top: 0,
Right: 0,
@ -25,8 +29,18 @@ class Canvas {
return;
}
a._container.innerHTML = "<canvas></canvas>";
a._container.innerHTML = "<div class='border'><canvas></canvas></div>";
if (a._container != null) {
a._container.style.width = "100%";
a._container.style.height = "100%";
a._flowContainer = a._container.getElementsByTagName("div")[0];
a._canvasContainer = a._flowContainer.getElementsByTagName("canvas")[0];
a._ctx = a._canvasContainer.getContext("2d");
}
a.AutoSize = a.AutoSize;
a.Width = a.Width;
a.Height = a.Height;
@ -34,6 +48,59 @@ class Canvas {
}
get AutoSize() {
return this._autoSize;
}
set AutoSize(value) {
const a = this;
a._autoSize = value;
if (a._flowContainer != null) {
if (value == true) {
a._flowContainer.style.overflow = "hidden";
} else {
a._flowContainer.style.overflowX = ((a.Width >= a.ClientWidth) ? "hidden" : "auto");
a._flowContainer.style.overflowY = ((a.Height >= a.ClientHeight) ? "hidden" : "auto");
}
}
}
get ClientHeight() {
const a = this;
if (a._canvasContainer == null) {
return 0;
}
return a.#getHeight(a._canvasContainer);
}
set ClientHeight(value) {
const a = this;
if (a._canvasContainer != null) a._canvasContainer.style.height = value + "px";
if (a._ctx != null) a._ctx.canvas.height = value;
}
get ClientWidth() {
const a = this;
if (a._canvasContainer == null) {
return 0;
}
return a.#getWidth(a._canvasContainer);
}
set ClientWidth(value) {
const a = this;
if (a._canvasContainer != null) a._canvasContainer.style.width = value + "px";
if (a._ctx != null) a._ctx.canvas.width = value;
}
get Height() {
const a = this;
@ -41,25 +108,18 @@ class Canvas {
return 0;
}
const h = (a._container.offsetHeight || a._container.innerHeight || a._container.clientHeight);
return h;
return a.#getHeight(a._container);
}
set Height(value) {
const a = this;
if (a._container == null) {
return;
if (a._container != null) a._container.style.height = value + "px";
if (a._flowContainer != null) a._flowContainer.style.height = value + "px";
if (a.AutoSize == true) {
a.ClientHeight = value;
}
a._container.style.height = value + "px";
const canvas = a._container.getElementsByTagName("canvas")[0];
canvas.style.height = value + "px";
a._ctx = canvas.getContext("2d");
a._ctx.canvas.height = value;
}
get Width() {
@ -69,25 +129,18 @@ class Canvas {
return 0;
}
const w = (a._container.offsetWidth || a._container.innerWidth || a._container.clientWidth);
return w;
return a.#getWidth(a._container);
}
set Width(value) {
const a = this;
if (a._container == null) {
return;
if (a._container != null) a._container.style.width = value + "px";
if (a._flowContainer != null) a._flowContainer.style.width = value + "px";
if (a.AutoSize == true) {
a.ClientWidth = value;
}
a._container.style.width = value + "px";
const canvas = a._container.getElementsByTagName("canvas")[0];
canvas.style.width = value + "px";
a._ctx = canvas.getContext("2d");
a._ctx.canvas.width = value;
}
get Size() {
@ -157,23 +210,143 @@ class Canvas {
return rectangle;
}
MeasureText(font, value) {
// DrawLine(x1, y1, x2, y2, width, colour) {
// const a = this;
// a.CTX.beginPath();
// a.CTX.moveTo(x1, y1);
// a.CTX.lineTo(x2, (y2 - width));
// a.CTX.lineWidth = width;
// a.CTX.strokeStyle = colour;
// a.CTX.stroke();
// }
DrawHorizontalLine(x, y, width, penColour, penWidth) {
const a = this;
if (a._ctx == null) {
return;
}
y -= 0.5;
// y -= penWidth;
a._ctx.beginPath();
a._ctx.strokeStyle = penColour;
a._ctx.lineWidth = penWidth;
a._ctx.moveTo(x, y);
a._ctx.lineTo((x + width), y);
a._ctx.stroke();
}
DrawText(x, y, text, font, foreColour, align) {
const a = this;
a._ctx.font = font;
a._ctx.fillStyle = foreColour;
a._ctx.textAlign = align;
a._ctx.textBaseline = "top";
a._ctx.fillText(text, x, y);
}
DrawVerticalLine(x, y, height, penColour, penWidth) {
const a = this;
if (a._ctx == null) {
return;
}
x -= 0.5;
y -= penWidth;
a._ctx.beginPath();
a._ctx.strokeStyle = penColour;
a._ctx.lineWidth = penWidth;
a._ctx.moveTo(x, y);
a._ctx.lineTo(x, (y + height));
a._ctx.stroke();
}
FillText(rectangle, text, font, foreColour, textAlign, verticalAlign) {
const a = this;
a._ctx.font = font;
a._ctx.fillStyle = foreColour;
a._ctx.textAlign = textAlign;
a._ctx.textBaseline = verticalAlign;
a._ctx.textBaseline = "top";
const size = a.MeasureText(font, text);
let x = rectangle.X + Math.half(rectangle.W);
// let y = rectangle.Y + Math.half(rectangle.H);
let y = rectangle.Y;
// switch (textAlign) {
// case "center":
// x += Math.half(rectangle.W - size.W);
// break;
// case "right":
// x += (rectangle.W - size.W);
// break;
// case "left":
// default:
// break;
// }
switch (verticalAlign) {
case "center":
case "middle":
y += Math.half((rectangle.H - size.H));
break;
case "bottom":
y += (rectangle.H - size.H);
break;
case "top":
default:
break;
}
a._ctx.fillText(text, x, y);
}
Invalidate() {
const a = this;
a.AutoSize = a.AutoSize;
a.Width = a.Width;
a.Height = a.Height;
}
MeasureText(font, value) {
const a = this;
if (a._ctx == null) {
return null;
}
a._ctx.font = font;
const size = a._ctx.measureText(value);
return {
X: a.half(size.width),
Y: a.half(size.fontBoundingBoxAscent),
W: size.width,
H: size.fontBoundingBoxAscent
H: Math.max(size.fontBoundingBoxDescent, size.fontBoundingBoxAscent)
};
}
#getWidth(el) {
const result = (el.offsetWidth || el.innerWidth || el.clientWidth);
return result;
}
#getHeight(el) {
const result = (el.offsetHeight || el.innerHeight || el.clientHeight);
return result;
}
}

View File

@ -22,8 +22,32 @@ Date.addYears = function (date, years) {
return result;
};
Date.max = function (date1, date2) {
if (date1 == null) {
return date2;
}
if (date2 == null) {
return date1;
}
return ((new Date(date1) <= new Date(date2)) ? new Date(date2) : new Date(date1));
};
Date.min = function (date1, date2) {
if (date1 == null) {
return date2;
}
if (date2 == null) {
return date1;
}
return ((new Date(date1) <= new Date(date2)) ? new Date(date1) : new Date(date2));
};
Date.diffDays = function (date1, date2) {
return (date2.getTime() - date1.getTime()) / (1000 * 3600 * 24);
return Math.ceil((new Date(date2).getTime() - new Date(date1).getTime()) / (1000 * 3600 * 24));
};
Date.today = function () {

View File

@ -3,8 +3,10 @@ class RyzGanttChart extends Canvas {
super(el);
const a = this;
a.Options = Object.assign(a.DefaultOptions, options);
// a.Debug = true;
a.Debug = false;
a.Project = null;
a.#initialiseComponents();
}
@ -18,9 +20,12 @@ class RyzGanttChart extends Canvas {
get DefaultOptions() {
return {
DayWidth: 100,
DayWidth: 32,
RowHeight: 28,
HeaderRowHeight: 40
HeaderRowHeight: 42,
DateFont: "8pt sans-serif",
DateForeColour: "#636363",
BorderColour: "#B8B8B8"
};
}
@ -50,22 +55,56 @@ class RyzGanttChart extends Canvas {
return;
}
a.Project = project;
const tasks = project.ExportTasks();
const width = (project.Duration * a.Options.DayWidth);
const width = ((project.Duration + 2) * a.Options.DayWidth);
const height = (tasks.length * a.Options.RowHeight) + a.Options.HeaderRowHeight;
console.log(tasks.length);
console.log(height);
a.AutoSize = false;
a.ClientWidth = width;
a.ClientHeight = height;
a.Width = width;
a.Height = height;
a.Invalidate();
// a.Invalidate();
a.#drawAxis();
}
// SetOptions(options) {
#drawAxis() {
const a = this;
// }
const width = a.ClientWidth;
const height = a.ClientHeight;
const borderWidth = 1;
const displayDays = a.Project.Duration + 2;
let startDate = new Date(a.Project.StartDate);
startDate.addDays(-1);
// Draw vertical lines
for (let i=1; i<displayDays; i++) {
a.DrawVerticalLine((a.Options.DayWidth * i), 22, 19, a.Options.BorderColour, borderWidth);
}
a.DrawHorizontalLine(0, 21, width, a.Options.BorderColour, borderWidth);
a.DrawHorizontalLine(0, 41, width, a.Options.BorderColour, borderWidth);
// Write dates
for (let i=0; i<displayDays; i++) {
const date = Date.addDays(startDate, i);
const rectangle = {
X: (a.Options.DayWidth * i),
Y: 21,
W: a.Options.DayWidth - borderWidth,
H: 19
};
if (a.Debug) a.DrawRectangle(rectangle, "red", 1);
a.FillText(rectangle, date.getDate(), a.Options.DateFont, a.Options.DateForeColour, "center", "middle");
}
}
}

View File

@ -41,10 +41,10 @@ body {
<div class="column" id="taskGrid1">
</div>
<div class="column" style="overflow: auto;">
<div class="column" style="padding-left: 3px">
<!-- <div style="width:100%; height: 100%; overflow: scroll;"> -->
<div id="ganttChart1" style="width:100%; height: 100%;"></div>
<div id="ganttChart1"></div>
<!-- </div> -->
</div>

View File

@ -2,6 +2,7 @@
border-spacing: 0px;
border-collapse: separate;
cursor: default;
padding: 0px 0px 30px 0px
}
.ryz-project .b {
@ -66,4 +67,13 @@
background-color: #D1F2C7;
color: #3E7138;
font-weight: bold;
}
.border {
border-color: #B8B8B8;
border-style: solid;
border-width: 1px;
}

View File

@ -46,16 +46,12 @@ class RyzProject {
let result = new Date(a.Project.StartDate);
a.Tasks.forEach(e => {
a.ExportTasks().forEach(e => {
if (e.FinishDate == null) {
return;
}
if (e.FinishDate <= result) {
return;
}
result = new Date(e.FinishDate);
result = Date.max(result, e.FinishDate);
});
return result;