Added canvas and rectangle

WIP: project gantt chart
This commit is contained in:
Ray 2023-12-27 18:38:37 +00:00
parent 2a8264b675
commit fc4f172e15
8 changed files with 367 additions and 78 deletions

View File

@ -0,0 +1,101 @@
Canvas = {};
Canvas.create = function (el) {
el.innerHTML = "<canvas></canvas>";
const canvas = el.getElementsByTagName("canvas")[0];
const width = (el.innerWidth || el.clientWidth);
const height = (el.innerHeight || el.clientHeight);
canvas.style.width = width + "px";
canvas.style.height = height + "px";
const ctx = canvas.getContext("2d");
ctx.canvas.width = width;
ctx.canvas.height = height;
return ctx;
};
class RyzCanvas {
constructor(el) {
const a = this;
a.Container = el;
a.CTX = Canvas.create(el);
a.Padding = {
Top: 0,
Right: 0,
Bottom: 0,
Left: 0
};
a.Clear();
}
get Width() {
return this.CTX.canvas.width;
}
get Height() {
return this.CTX.canvas.height;
}
get Rectangle() {
return {
X: this.Padding.Left,
Y: this.Padding.Top,
W: (this.Width - (this.Padding.Left + this.Padding.Right)),
H: (this.Height - (this.Padding.Top + this.Padding.Bottom))
}
}
Clear() {
const a = this;
a.CTX.clearRect(0, 0, a.CTX.canvas.width, a.CTX.canvas.height);
}
DrawRectangle(rectangle, penColour, penWidth) {
const a = this;
// Adjust for pen discrepancy
rectangle.X += 0.5;
rectangle.Y += 0.5;
rectangle.W -= penWidth;
rectangle.H -= penWidth;
a.CTX.beginPath();
a.CTX.strokeStyle = penColour;
a.CTX.lineWidth = penWidth;
a.CTX.rect(rectangle.X, rectangle.Y, rectangle.W, rectangle.H);
//a.ctx.fillStyle = 'yellow';
//a.ctx.fill();
a.CTX.stroke();
return rectangle;
}
MeasureText(font, value) {
const a = this;
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
};
}
}

View File

@ -22,6 +22,10 @@ Date.addYears = function (date, years) {
return result;
};
Date.diffDays = function (date1, date2) {
return (date2.getTime() - date1.getTime()) / (1000 * 3600 * 24);
};
Date.today = function () {
let result = new Date();

View File

@ -13,4 +13,8 @@ Math.average = function (values) {
});
return Math.round(result / values.length);
};
Math.half = function (value) {
return (value / 2);
};

View File

@ -0,0 +1,96 @@
class Rectangle {
constructor(x, y, w, h) {
this.X = x;
this.Y = y;
this.W = w;
this.H = h;
}
static containsPoint(rectangle, point) {
const x2 = (rectangle.X + rectangle.W);
const y2 = (rectangle.Y + rectangle.H);
return ((point.X >= rectangle.X) && (point.X <= x2) && (point.Y >= rectangle.Y) && (point.Y <= y2));
}
static combine(rect1, rect2) {
const x2 = Math.max((rect1.X + rect1.W), (rect2.X + rect2.W));
const y2 = Math.max((rect1.Y + rect1.H), (rect2.Y + rect2.H));
const rect = {
X: Math.min(rect1.X, rect2.X),
Y: Math.min(rect1.Y, rect2.Y),
W: 0,
H: 0
};
rect.W = x2 - rect.X;
rect.H = y2 - rect.Y;
return rect;
};
}
// Rectangle.containsPoint = function (rectangle, point) {
// const x2 = (rectangle.X + rectangle.W);
// const y2 = (rectangle.Y + rectangle.H);
// return ((point.X >= rectangle.X) && (point.X <= x2) && (point.Y >= rectangle.Y) && (point.Y <= y2));
// };
// Rectangle.combine = function(rect1, rect2) {
// const x2 = Math.max((rect1.X + rect1.W), (rect2.X + rect2.W));
// const y2 = Math.max((rect1.Y + rect1.H), (rect2.Y + rect2.H));
// const rect = {
// X: Math.min(rect1.X, rect2.X),
// Y: Math.min(rect1.Y, rect2.Y),
// W: 0,
// H: 0
// };
// rect.W = x2 - rect.X;
// rect.H = y2 - rect.Y;
// return rect;
// };
// class RyzRectangle {
// constructor(x, y, w, h) {
// const a = this;
// a.X = x;
// a.Y = y;
// a.W = w;
// a.H = h;
// }
// ContainsPoint(point) {
// const a = this;
// return Rectangle.containsPoint(a.ToModel, point);
// }
// ToModel() {
// const a = this;
// return {
// X: a.X,
// Y: a.Y,
// W: a.W,
// H: a.H
// };
// }
// ToString() {
// const a = this;
// return "x = " + a.X + ", y = " + a.Y + ", w = " + a.W + ", h = " + a.H;
// }
// }

4
ryzjsext.min.js vendored
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.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.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))};
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",

View File

@ -8,6 +8,8 @@
<meta name="keyword" content="" />
<script src="ryzjsext.min.js"></script>
<script src="javascript-extensions/ryz-js-ext-canvas.js"></script>
<script src="javascript-extensions/ryz-js-ext-rectangle.js"></script>
<script src="ryzproj.js"></script>
<link href="ryzproj.css" rel="stylesheet" />
@ -21,13 +23,21 @@ body {
font-size: 10pt;
}
.row {
display: flex;
}
.column {
flex: 50%;
}
</style>
<script>
var gantt1 = new RyzGantt({ Name: "New Project 1" });
var project1 = new RyzProject({ Name: "New Project 1" });
gantt1.AddTask({
project1.AddTask({
ID: 1,
Name: "Task A",
StartDelay: 0,
@ -37,7 +47,7 @@ gantt1.AddTask({
CollatedTaskID: null
});
gantt1.AddTask({
project1.AddTask({
ID: 2,
Name: "Task B",
StartDelay: 1,
@ -47,7 +57,7 @@ gantt1.AddTask({
CollatedTaskID: null
});
gantt1.AddTask({
project1.AddTask({
ID: 5,
Name: "Task B1",
StartDelay: 4,
@ -57,7 +67,7 @@ gantt1.AddTask({
CollatedTaskID: null
});
gantt1.AddTask({
project1.AddTask({
ID: 6,
Name: "Task B11",
StartDelay: 0,
@ -67,7 +77,7 @@ gantt1.AddTask({
CollatedTaskID: 5
});
gantt1.AddTask({
project1.AddTask({
ID: 7,
Name: "Task B12",
StartDelay: 0,
@ -78,7 +88,7 @@ gantt1.AddTask({
Progress: 50
});
gantt1.AddTask({
project1.AddTask({
ID: 8,
Name: "Task E",
StartDelay: 3,
@ -88,7 +98,7 @@ gantt1.AddTask({
CollatedTaskID: null
});
gantt1.AddTask({
project1.AddTask({
ID: 9,
Name: "Task E1",
StartDelay: 3,
@ -98,7 +108,7 @@ gantt1.AddTask({
CollatedTaskID: 8
});
gantt1.AddTask({
project1.AddTask({
ID: 3,
Name: "Task C",
StartDelay: 5,
@ -108,7 +118,7 @@ gantt1.AddTask({
CollatedTaskID: null
});
gantt1.AddTask({
project1.AddTask({
ID: 4,
Name: "Task D",
StartDelay: 1,
@ -118,16 +128,22 @@ gantt1.AddTask({
CollatedTaskID: null
});
gantt1.Invalidate();
project1.Invalidate();
// console.log(gantt1.ExportTasks());
Document.ready(async function() {
const taskGrid1 = document.getElementById("projectTaskGrid1");
const ganttChart1 = document.getElementById("projectGanttChart1");
taskGrid1.innerHTML = gantt1.RenderTaskGrid();
console.log(new Date(gantt1.StartDate).toLocaleDateString() + " - " + new Date(gantt1.FinishDate).toLocaleDateString());
console.log(new Date(project1.StartDate).toLocaleDateString() + " - " + new Date(project1.FinishDate).toLocaleDateString());
console.log(project1.Duration);
taskGrid1.innerHTML = project1.RenderTaskGrid();
project1.RenderGanttChart(ganttChart1);
});
@ -136,29 +152,38 @@ Document.ready(async function() {
</head>
<body>
<div>
<table class="ryz-project">
<thead>
<tr>
<th></th>
<th></th>
<th>Task Name</th>
<th>Duration</th>
<th>Start</th>
<th>Finish</th>
<th>Predecessor</th>
<th>Resource Names</th>
<th></th>
</tr>
</thead>
<tbody id="projectTaskGrid1">
<tr>
<td></td>
<td colspan="8" class="c">Loading...</td>
</tr>
</tbody>
</table>
</div>
<div class="row">
<div class="column">
<table class="ryz-project">
<thead>
<tr>
<th></th>
<th></th>
<th>Task Name</th>
<th>Duration</th>
<th>Start</th>
<th>Finish</th>
<th>Predecessor</th>
<th>Resource Names</th>
<th></th>
</tr>
</thead>
<tbody id="projectTaskGrid1">
<tr>
<td></td>
<td colspan="8" class="c">Loading...</td>
</tr>
</tbody>
</table>
</div>
<div class="column">
<div id="projectGanttChart1" style="width:100%; height: 100%;"></div>
</div>
</div>
</body>

View File

@ -46,7 +46,10 @@
border-style: solid;
border-width: 0px 0px 1px 0px;
margin: 0px;
overflow: hidden;
padding: 5px 5px 5px 5px;
text-overflow: ellipsis;
white-space: nowrap;
}
.ryz-project tbody tr td:first-child {
border-right-width: 1px;

View File

@ -1,4 +1,16 @@
class RyzGantt {
class RyzGanttChart extends RyzCanvas {
constructor(el) {
super(el);
this.Padding.Left = 3;
}
}
class RyzProject {
constructor(options) {
const a = this;
const _options = Object.assign(a.#newProject, options);
@ -61,6 +73,12 @@ class RyzGantt {
return result;
}
get Duration() {
const a = this;
return Date.diffDays(a.StartDate, a.FinishDate);
}
get #newProject() {
return {
@ -92,6 +110,7 @@ class RyzGantt {
Progress: 0,
Resources: [],
Level: 0,
PredecessorTaskNo: null,
Tasks: []
};
}
@ -270,7 +289,7 @@ class RyzGantt {
result[i].Progress = 0;
// update finish date, if possible
a.#updateCollatedTask(result, i);
a.#recalculateCollatedTask(result, i);
break;
case 3: // task with parent
@ -291,7 +310,7 @@ class RyzGantt {
result[i].Progress = 0;
// update finish date, if possible
a.#updateCollatedTask(result, i);
a.#recalculateCollatedTask(result, i);
}
}
@ -316,7 +335,7 @@ class RyzGantt {
result[i].Level = (node6.Level + 1);
// update finish date, if possible
a.#updateCollatedTask(result, i);
a.#recalculateCollatedTask(result, i);
}
}
@ -328,29 +347,10 @@ class RyzGantt {
}
// Calculate work-hours
for (var i=0; i<result.length; i++) {
if (result[i].StartDate == null) {
continue;
}
a.#recalculateWorkHours(result);
if (result[i].FinishDate == null) {
continue;
}
if (result[i].Duration <= 0) {
continue;
}
let workHours = 0;
let date = new Date(result[i].StartDate);
for (let i=0; i<result[i].Duration; i++) {
const d = date.getDay();
workHours += a.Project.WorkHours[d];
}
result[i].CalcWorkHours = workHours;
}
// Calculate predecessor
a.#recalculatePredecessorTaskNo(result);
return isSuccess;
}
@ -367,14 +367,6 @@ class RyzGantt {
const result = a.ExportTasks();
result.forEach(e => {
let predecessorNo = "";
if (e.PredecessorTaskID != null) {
const predecessor = a.FindTask(e.PredecessorTaskID);
if (predecessor != null) {
predecessorNo = predecessor.Order;
}
}
if (e.IsCollated == true) {
htmlContent += "<tr class='b'>";
} else {
@ -394,7 +386,7 @@ class RyzGantt {
htmlContent += "<td>" + e.Duration + " day" + (parseInt(e.Duration) == 1 ? "" : "s") + "</td>";
htmlContent += "<td class='c'>" + new Date(e.StartDate).toLocaleDateString() + "</td>";
htmlContent += "<td class='c'>" + new Date(e.FinishDate).toLocaleDateString() + "</td>";
htmlContent += "<td class='c'>" + predecessorNo + "</td>";
htmlContent += "<td class='c'>" + (e.PredecessorTaskNo ?? "") + "</td>";
htmlContent += "<td></td>";
htmlContent += "<td></td>";
htmlContent += "</tr>";
@ -403,7 +395,71 @@ class RyzGantt {
return htmlContent;
}
#updateCollatedTask(array, index) {
RenderGanttChart(el) {
const a = this;
const ganttCanvas = new RyzGanttChart(el);
ganttCanvas.DrawRectangle(ganttCanvas.Rectangle, "#B8B8B8", 1);
ganttCanvas.DrawRectangle({ X: 50, Y: 50, W: 100, H: 100 }, "#B8B8B8", 3);
// console.log(ganttCanvas.Width);
}
#log(message) {
console.log(message);
}
#recalculateWorkHours(array) {
const a = this;
for (var i=0; i<array.length; i++) {
if (array[i].StartDate == null) {
continue;
}
if (array[i].FinishDate == null) {
continue;
}
if (array[i].Duration <= 0) {
continue;
}
let workHours = 0;
let date = new Date(array[i].StartDate);
for (let i=0; i<array[i].Duration; i++) {
const d = date.getDay();
workHours += a.Project.WorkHours[d];
}
array[i].CalcWorkHours = workHours;
}
}
#recalculatePredecessorTaskNo(array) {
const a = this;
for (var i=0; i<array.length; i++) {
if (array[i].PredecessorTaskID == null) {
continue;
}
const predecessor = a.FindTask(array[i].PredecessorTaskID);
if (predecessor == null) {
continue;
}
array[i].PredecessorTaskNo = predecessor.Order;
}
}
#recalculateCollatedTask(array, index) {
let node2 = array.select("CollatedTaskID", array[index].ID);
if (node2.length <= 0) {
// No children
@ -426,8 +482,8 @@ class RyzGantt {
array[index].FinishDate = new Date(node2FinishDate);
}
#log(message) {
console.log(message);
}
}
}