WIP: gantt chart

This commit is contained in:
Ray 2024-01-02 17:58:28 +00:00
parent db458dc3a0
commit d6048a20be
4 changed files with 179 additions and 80 deletions

View File

@ -13,6 +13,6 @@ Date.prototype.toCString=function(a){a=a.replace("fffffff",this.getMilliseconds(
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",
this.getMinutes().toString());a=a.replace("s",this.getSeconds().toString());a=a.replace("z","");a=a.replace("t","{6}");a=a.replace("Z","");a=a.replace("{1}",this.toLocaleString("default",{month:"long"}));a=a.replace("{2}",this.toLocaleString("default",{weekday:"long"}));a=a.replace("{3}",this.toLocaleString("default",{month:"short"}));a=a.replace("{4}",this.toLocaleString("default",{weekday:"short"}));a=a.replace("{5}",12<=this.getHours()?"PM":"AM");return a=a.replace("{6}",12<=this.getHours()?"P":
"A")};Document.ready=async function(a){(async function(){"loading"!==document.readyState?a():document.addEventListener("DOMContentLoaded",function(){a()})})()};Math.randomN=function(a,b){a=Math.ceil(a);b=Math.floor(b);return Math.floor(Math.random()*(b-a)+a)};Math.average=function(a){let b=0;a.forEach(c=>{b+=parseFloat(c)});return Math.round(b/a.length)};Math.half=function(a){return a/2};class Rectangle{constructor(a,b,c,d){this.X=a;this.Y=b;this.W=c;this.H=d}static containsPoint(a,b){const c=a.X+a.W,d=a.Y+a.H;return b.X>=a.X&&b.X<=c&&b.Y>=a.Y&&b.Y<=d}static combine(a,b){const c=Math.max(a.Y+a.H,b.Y+b.H),d={X:Math.min(a.X,b.X),Y:Math.min(a.Y,b.Y),W:0,H:0};d.W=Math.max(a.X+a.W,b.X+b.W)-d.X;d.H=c-d.Y;return d}};String.isNullOrUndefined=function(a){return"undefined"==typeof a||null==a?!0:!1};String.isNullOrWhitespace=function(a){return String.isNullOrUndefined(a)?!0:"string"==typeof a?0>=a.trim().length:0>=a.toString().trim().length};String.joinIfNotNullOrWhitespace=function(a,...b){let c="";for(let d=0;d<b.length;d++)String.isNullOrWhitespace(b[d])||(String.isNullOrWhitespace(c)||(c+=a),c+=b[d]);return c};String.prototype.contains=function(...a){for(let b of a)if(this==b)return!0;return!1};
"A")};Document.ready=async function(a){(async function(){"loading"!==document.readyState?a():document.addEventListener("DOMContentLoaded",function(){a()})})()};Math.randomN=function(a,b){a=Math.ceil(a);b=Math.floor(b);return Math.floor(Math.random()*(b-a)+a)};Math.average=function(a){let b=0;a.forEach(c=>{b+=parseFloat(c)});return b/a.length};Math.avg=function(...a){let b=0;a.forEach(c=>{b+=parseFloat(c)});return b/a.length};Math.half=function(a){return a/2};class Rectangle{constructor(a,b,c,d){this.X=a;this.Y=b;this.W=c;this.H=d}static containsPoint(a,b){const c=a.X+a.W,d=a.Y+a.H;return b.X>=a.X&&b.X<=c&&b.Y>=a.Y&&b.Y<=d}static combine(a,b){const c=Math.max(a.Y+a.H,b.Y+b.H),d={X:Math.min(a.X,b.X),Y:Math.min(a.Y,b.Y),W:0,H:0};d.W=Math.max(a.X+a.W,b.X+b.W)-d.X;d.H=c-d.Y;return d}};String.isNullOrUndefined=function(a){return"undefined"==typeof a||null==a?!0:!1};String.isNullOrWhitespace=function(a){return String.isNullOrUndefined(a)?!0:"string"==typeof a?0>=a.trim().length:0>=a.toString().trim().length};String.joinIfNotNullOrWhitespace=function(a,...b){let c="";for(let d=0;d<b.length;d++)String.isNullOrWhitespace(b[d])||(String.isNullOrWhitespace(c)||(c+=a),c+=b[d]);return c};String.prototype.contains=function(...a){for(let b of a)if(this==b)return!0;return!1};
String.prototype.containsCI=function(...a){for(let b of a)if(this.toLowerCase()==b.toLowerCase())return!0;return!1};String.prototype.encodeHtmlLinks=function(){return value.replace(/(http[s]{0,1}:\/\/[^\s]+)/g,"<a href='$1'>$1</a>")};String.prototype.toTitleCase=function(){let a;a=this.replace(/([A-Z]{1})/g," $1");a=a.trim();return a=a.charAt(0).toUpperCase()+a.substr(1)};String.prototype.getFilename=function(){return this.substring(this.lastIndexOf("/")+1)};Window.goToTop=function(){Window.scrollTo(0,0)};
Window.fragment={get:function(){if(!window.location.hash)return null;const a=window.location.hash.indexOf("?");return 0>a?window.location.hash.substring(1):window.location.hash.substring(1,a)},getQuery:function(){if(!window.location.hash)return null;var a=window.location.hash;a=a.indexOf("?");if(0>a)return null;a=hasQueryString.substring(a+1);a=new URLSearchParams(a);const b={};for(const [c,d]of a.entries())b[c]=d;return b},clear:function(){location.hash="";history.replaceState("","",location.pathname)}};

115
canvas.js
View File

@ -185,26 +185,36 @@ class Canvas {
a._ctx.clearRect(0, 0, a._ctx.canvas.width, a._ctx.canvas.height);
}
DrawRectangle(rectangle, penColour, penWidth) {
DrawRectangle(rectangle, penColour, options) {
const a = this;
if (a._ctx == null) {
return;
}
const opt = Object.assign({
LineWidth: 1,
LineDash: [],
FillColour: null
}, options);
// Adjust for pen discrepancy
rectangle.X += 0.5;
rectangle.Y += 0.5;
rectangle.W -= penWidth;
rectangle.H -= penWidth;
rectangle.W -= opt.LineWidth;
rectangle.H -= opt.LineWidth;
a._ctx.beginPath();
a._ctx.strokeStyle = penColour;
a._ctx.lineWidth = penWidth;
a._ctx.lineWidth = opt.LineWidth;
a._ctx.setLineDash(opt.LineDash);
a._ctx.rect(rectangle.X, rectangle.Y, rectangle.W, rectangle.H);
//a.ctx.fillStyle = 'yellow';
//a.ctx.fill();
if (opt.FillColour != null) {
a._ctx.fillStyle = opt.FillColour;
a._ctx.fill();
}
a._ctx.stroke();
return rectangle;
@ -221,87 +231,102 @@ class Canvas {
// a.CTX.stroke();
// }
DrawHorizontalLine(x, y, width, penColour, penWidth) {
DrawHorizontalLine(x, y, width, penColour, options) {
const a = this;
if (a._ctx == null) {
return;
}
const opt = Object.assign({
LineWidth: 1,
LineDash: []
}, options);
y -= 0.5;
// y -= penWidth;
a._ctx.beginPath();
a._ctx.strokeStyle = penColour;
a._ctx.lineWidth = penWidth;
a._ctx.lineWidth = opt.LineWidth;
a._ctx.setLineDash(opt.LineDash);
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) {
DrawText(x, y, text, font, foreColour, options) {
const a = this;
if (a._ctx == null) {
return;
}
const opt = Object.assign({
Align: "left"
}, options);
a._ctx.font = font;
a._ctx.fillStyle = foreColour;
a._ctx.textAlign = opt.Align;
a._ctx.textBaseline = "top";
a._ctx.fillText(text, x, y);
}
DrawVerticalLine(x, y, height, penColour, options) {
const a = this;
if (a._ctx == null) {
return;
}
const opt = Object.assign({
LineWidth: 1,
LineDash: []
}, options);
x -= 0.5;
y -= penWidth;
y -= opt.LineWidth;
a._ctx.beginPath();
a._ctx.strokeStyle = penColour;
a._ctx.lineWidth = penWidth;
a._ctx.lineWidth = opt.LineWidth;
a._ctx.setLineDash(opt.LineDash);
a._ctx.moveTo(x, y);
a._ctx.lineTo(x, (y + height));
a._ctx.stroke();
}
FillText(rectangle, text, font, foreColour, textAlign, verticalAlign) {
FillText(rectangle, text, font, foreColour, options) {
const a = this;
if (a._ctx == null) {
return;
}
const opt = Object.assign({
Align: "center",
VAlign: "middle"
}, options);
a._ctx.font = font;
a._ctx.fillStyle = foreColour;
a._ctx.textAlign = textAlign;
a._ctx.textBaseline = verticalAlign;
a._ctx.textAlign = opt.Align;
// 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);
const size = a._ctx.measureText(text);
const x = rectangle.X + Math.half(rectangle.W);
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) {
switch (opt.VAlign) {
case "center":
case "middle":
y += Math.half((rectangle.H - size.H));
y += Math.half((rectangle.H - size.fontBoundingBoxDescent)) + size.fontBoundingBoxAscent;
break;
case "bottom":
y += (rectangle.H - size.H);
y += (rectangle.H - size.fontBoundingBoxDescent);
break;
case "top":
default:
@ -332,7 +357,7 @@ class Canvas {
return {
W: size.width,
H: Math.max(size.fontBoundingBoxDescent, size.fontBoundingBoxAscent)
H: Math.round(size.fontBoundingBoxDescent + size.fontBoundingBoxAscent)
};
}

View File

@ -12,7 +12,17 @@ Math.average = function (values) {
result += parseFloat(e);
});
return Math.round(result / values.length);
return (result / values.length);
};
Math.avg = function (...values) {
let result = 0;
values.forEach(e => {
result += parseFloat(e);
});
return (result / values.length);
};
Math.half = function (value) {

View File

@ -8,37 +8,47 @@ class RyzGanttChart extends Canvas {
a.Debug = false;
a.Project = null;
a.StartDate = null;
a.#initialiseComponents();
}
#initialiseComponents() {
const a = this;
}
get DefaultOptions() {
return {
DayWidth: 32,
RowHeight: 28,
HeaderRowHeight: 42,
DateFont: "8pt sans-serif",
DayWidth: 24,
HeaderRow: {
Height: [ 21, 21 ]
},
Row: {
Height: 28,
Task: {
Height: 13,
PaddingTop: 6,
BorderColour: "#555555",
FillColour: "#9CC2E6"
},
CollatedTask: {
Height: 3,
PaddingTop: 11,
BorderColour: "#555555",
FillColour: "#555555"
}
},
DateFont: "7pt sans-serif",
DateForeColour: "#636363",
BorderColour: "#B8B8B8"
BorderWidth: 1,
BorderColour: "#B8B8B8",
BorderDashPattern: [1, 3],
};
}
// Clear() {
// super.Clear();
// const a = this;
// a.Invalidate();
// }
// Invalidate() {
// const a = this;
// a._padding.Left = 3;
@ -56,10 +66,12 @@ class RyzGanttChart extends Canvas {
}
a.Project = project;
a.StartDate = new Date(project.StartDate);
const tasks = project.ExportTasks();
const width = ((project.Duration + 2) * a.Options.DayWidth);
const height = (tasks.length * a.Options.RowHeight) + a.Options.HeaderRowHeight;
const headerHeight = (a.Options.HeaderRow.Height[0] + a.Options.HeaderRow.Height[1]);
const height = (tasks.length * a.Options.Row.Height) + headerHeight;
a.AutoSize = false;
a.ClientWidth = width;
@ -67,44 +79,96 @@ class RyzGanttChart extends Canvas {
a.Invalidate();
a.#drawAxis();
a.#drawChartLabel(project);
a.#drawTasks(tasks);
}
#drawAxis() {
#drawChartLabel(project) {
const a = this;
const width = a.ClientWidth;
const height = a.ClientHeight;
const borderWidth = 1;
const displayDays = a.Project.Duration + 2;
const displayDays = project.Duration + 2;
const headerHeight = (a.Options.HeaderRow.Height[0] + a.Options.HeaderRow.Height[1]);
let startDate = new Date(a.Project.StartDate);
let startDate = new Date(a.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.DrawVerticalLine((a.Options.DayWidth * i), (a.Options.HeaderRow.Height[0] + a.Options.BorderWidth), (a.Options.HeaderRow.Height[1] - (a.Options.BorderWidth * 2)), a.Options.BorderColour, {});
}
a.DrawHorizontalLine(0, 21, width, a.Options.BorderColour, borderWidth);
a.DrawHorizontalLine(0, 41, width, a.Options.BorderColour, borderWidth);
a.DrawHorizontalLine(0, a.Options.HeaderRow.Height[0], width, a.Options.BorderColour);
a.DrawHorizontalLine(0, (headerHeight - a.Options.BorderWidth), width, a.Options.BorderColour);
// 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
const x = (a.Options.DayWidth * i);
// Draw month
if (date.getDate() == 1) {
const size = a.MeasureText(a.Options.DateFont, "#");
const monthPoint = {
X: (x + 2),
Y: Math.half(a.Options.HeaderRow.Height[0] - size.H)
};
a.DrawText(monthPoint.X, monthPoint.Y, date.toCString("MMMM"), a.Options.DateFont, a.Options.DateForeColour);
}
// Draw day
const dateRectangle = {
X: x,
Y: a.Options.HeaderRow.Height[1],
W: a.Options.DayWidth - a.Options.BorderWidth,
H: (a.Options.HeaderRow.Height[1] - (a.Options.BorderWidth * 2))
};
if (a.Debug) a.DrawRectangle(rectangle, "red", 1);
a.FillText(dateRectangle, date.getDate(), a.Options.DateFont, a.Options.DateForeColour);
a.FillText(rectangle, date.getDate(), a.Options.DateFont, a.Options.DateForeColour, "center", "middle");
if (a.Debug) a.DrawRectangle(dateRectangle, "red", {});
// Draw day-of-week guideline
const guidelineHeight = height - a.Options.BorderWidth;
if (project.Project.StartOfWeek == date.getDay()) {
// Draw start-of-the-week guideline
a.DrawVerticalLine(x, headerHeight, guidelineHeight, a.Options.BorderColour, {});
} else {
a.DrawVerticalLine(x, headerHeight, guidelineHeight, a.Options.BorderColour, { LineDash: a.Options.BorderDashPattern });
}
}
}
#drawTasks(tasks) {
const a = this;
const labelHeight = a.Options.HeaderRow.Height[0] + a.Options.HeaderRow.Height[1];
for (let i=0; i<tasks.length; i++) {
if (tasks[i].IsCollated == true) {
const rectangle = {
X: (a.Options.DayWidth * (Date.diffDays(a.StartDate, tasks[i].StartDate) + 1)),
Y: (labelHeight + (a.Options.Row.Height * i) + a.Options.Row.CollatedTask.PaddingTop),
W: (a.Options.DayWidth * tasks[i].Duration),
H: a.Options.Row.CollatedTask.Height
};
a.DrawRectangle(rectangle, a.Options.Row.CollatedTask.BorderColour, { FillColour: a.Options.Row.CollatedTask.FillColour });
} else {
const rectangle = {
X: (a.Options.DayWidth * (Date.diffDays(a.StartDate, tasks[i].StartDate) + 1)),
Y: (labelHeight + (a.Options.Row.Height * i) + a.Options.Row.Task.PaddingTop),
W: (a.Options.DayWidth * tasks[i].Duration),
H: a.Options.Row.Task.Height
};
a.DrawRectangle(rectangle, a.Options.Row.Task.BorderColour, { FillColour: a.Options.Row.Task.FillColour });
}
}
}
}