literyzjs-timeline/bbtimeline.js

477 lines
12 KiB
JavaScript

/**
* BBTimeline
* @version v0.1.0.076 (2023/09/30 1432)
*/
class BBTimeline {
constructor(el) {
const a = this;
a.Container = document.getElementById(el);
a.Padding = {
Left: 20,
Top: 20,
Right: 20,
Bottom: 40
};
a.Size = {
Width: a.Container.innerWidth || a.Container.clientWidth,
Height: a.Container.innerHeight || a.Container.clientHeight
};
a.Axis = {
LineColour1: "#CFCFCF",
LineWidth: 1,
Font: "8pt Arial",
LabelColour: "#000000",
X: {
NoPartPerDay: 4,
HourLineSpace: 6,
HourLineHeight: 10,
HourLineColour: "#A6A6A6",
DayLineHeight: 20,
DayLineColour: "#282828"
}
};
a.Marker = {
BorderColour: "#3A5D9C",
BorderWidth: 2,
BackColour: "#D4DEEF",
Width: 10
};
a.HighlightLine = {
Colour: "#A6A6A6",
Width: 1,
};
a.Events = [];
a.StartDate = new Date();
a.ClientRectangle = a.getClientRectangle();
a.Enabled = false;
a.ctx = a.Container.getContext("2d");
a.ctx.canvas.width = a.Size.Width;
a.ctx.canvas.height = a.Size.Height;
a.initialiseComponents();
}
Load(startDate) {
const a = this;
a.StartDate = ((typeof(startDate) == "undefined") ? new Date() : startDate);
a.invalidate(true);
a.Enabled = true;
}
Clear(all) {
const a = this;
if (all) {
a.ctx.clearRect(0, 0, a.ctx.canvas.width, a.ctx.canvas.height);
a.Events = [];
a.Enabled = false;
} else {
const rect = a.getClientCoords();
a.ctx.clearRect((rect.X1 + a.Axis.LineWidth), rect.Y1, (rect.X2 - rect.X1 - a.Axis.LineWidth), (rect.Y2 - rect.Y1 - a.Axis.LineWidth));
}
}
FindDate(date) {
const a = this;
const points = a.getXAxis();
for (let i=0; i<points.length; i++) {
if (points[i].Date == date){
return points[i];
}
}
return null;
}
FindEventByCoords(x, y) {
const a = this;
for (let i=0; i<a.Events.length; i++) {
const e = a.Events[i].Rectangle;
if ((x >= e.X1) && (x <= e.X2) && (y >= e.Y1) && (y <= e.Y2)){
return a.Events[i];
}
}
return null;
}
AddEvent(date, title, description, link) {
const a = this;
a.Events.push({
Date: date,
Title: title,
Description: description,
Link: link
});
a.invalidate(false);
}
initialiseComponents() {
const a = this;
const coords = a.getClientCoords();
// Vertical highlight line
// a.ctx.canvas.addEventListener('mousemove', function (e) {
// if (!a.Enabled) {
// return;
// }
// a.invalidate(false);
// if ((e.offsetX > (coords.X1 + a.Axis.LineWidth)) && (e.offsetX < coords.X2) && (e.offsetY >= coords.Y1) && (e.offsetY < (coords.Y2 - a.Axis.LineWidth))){
// a.drawVerticalLine(e.offsetX);
// }
// });
a.ctx.canvas.addEventListener('mousedown', function (e) {
if (!a.Enabled) {
return;
}
var event = a.FindEventByCoords(e.offsetX, e.offsetY);
console.log(event);
// console.log(e);
});
}
invalidate(all) {
const a = this;
a.Clear(all);
if (all) {
a.drawAxis();
a.drawXAxis();
a.drawXAxisLabels();
} else {
const coords = a.getClientCoords();
coords.Y1 += a.Marker.Width;
for (let i=0; i<a.Events.length; i++) {
const event = a.FindDate(a.Events[i].Date);
a.drawVerticalLine(event.X, coords.Y1);
const rect = a.drawMarker(event.X, coords.Y1);
a.Events[i].Rectangle = rect;
}
console.log(a.Events);
}
}
drawAxis() {
const a = this;
const coords = a.getClientCoords();
if (coords == null) {
return;
}
a.ctx.beginPath();
a.ctx.moveTo(coords.X1, coords.Y1);
a.ctx.lineTo(coords.X1, coords.Y2);
a.ctx.lineTo(coords.X2, coords.Y2);
a.ctx.lineWidth = a.Axis.LineWidth;
a.ctx.strokeStyle = a.Axis.LineColour1;
a.ctx.stroke();
}
drawXAxis() {
const a = this;
const coords = a.getClientCoords();
if (coords == null) {
return;
}
let x = coords.X1;
let y = coords.Y2 + a.Axis.LineWidth;
let i = 0;
while (true) {
if (x >= coords.X2) {
break;
}
a.ctx.beginPath();
a.ctx.moveTo(x, y);
if ((i % a.Axis.X.NoPartPerDay) == 0) {
a.ctx.lineTo(x, (y + a.Axis.X.DayLineHeight));
a.ctx.strokeStyle = a.Axis.X.DayLineColour;
} else {
a.ctx.lineTo(x, (y + a.Axis.X.HourLineHeight));
a.ctx.strokeStyle = a.Axis.X.HourLineColour;
}
a.ctx.lineWidth = a.Axis.LineWidth;
a.ctx.stroke();
x += a.Axis.X.HourLineSpace;
i++;
}
}
drawXAxisLabels() {
const a = this;
const coords = a.getClientCoords();
if (coords == null) {
return;
}
const result = a.getXAxis();
const y = coords.Y2 + a.Axis.LineWidth;
result.forEach(function(e, i) {
const date = a.stringToDate(e.Date);
let writeLabel = false;
if ((i == 0)) {
// Don't label first entry if too close to the next month
if (date.getDate() < 25) {
writeLabel = true;
}
} else if (date.getDate() == 1) {
writeLabel = true;
}
// if (i == 0) {
// return;
// }
const labelSize = a.drawText(e.X, (y + a.Axis.X.DayLineHeight), a.dateToString(date, "dd"), "center");
const label2Spacing = 6;
// Write month on first of the month
if (writeLabel) {
a.drawText(e.X, (y + a.Axis.X.DayLineHeight + labelSize.Height + label2Spacing), a.dateToString(date, "MMMM yyyy"), "left");
}
});
}
drawMarker(x, y) {
const a = this;
const coords = a.getClientCoords();
if (coords == null) {
return;
}
const width = a.Marker.Width - (a.Marker.BorderWidth * 2);
a.ctx.beginPath();
a.ctx.arc(x, y, width, 0, 2 * Math.PI, false);
a.ctx.fillStyle = a.Marker.BackColour;
a.ctx.fill();
a.ctx.lineWidth = a.Marker.BorderWidth;
a.ctx.strokeStyle = a.Marker.BorderColour;
a.ctx.stroke();
const offset = a.half(a.Marker.Width);
const result = {
X1: x - (offset + a.Marker.BorderWidth),
Y1: y - (offset + a.Marker.BorderWidth),
X2: x + (offset + a.Marker.BorderWidth),
Y2: y + (offset + a.Marker.BorderWidth)
};
// a.ctx.beginPath();
// a.ctx.rect(result.X1, result.Y1, (result.X2 - result.X1), (result.Y2 - result.Y1));
// a.ctx.fillStyle = 'yellow';
// a.ctx.fill();
// a.ctx.lineWidth = 1;
// a.ctx.strokeStyle = 'black';
// a.ctx.stroke();
return result;
}
drawText(x, y, label, align) {
const a = this;
a.ctx.font = a.Axis.Font;
a.ctx.fillStyle = a.Axis.LabelColour;
const size = a.measureText(label);
switch (align) {
case "center":
x = (x - size.OffsetLeft);
break;
case "right":
x = (x - size.Width);
break;
case "left":
default:
// do nothing
break;
}
a.ctx.fillText(label, x, (y + size.Height));
return size;
}
drawVerticalLine(x, y) {
const a = this;
const coords = a.getClientCoords();
if (coords == null) {
return;
}
if (y <= 0) {
y = (coords.Y1 + a.HighlightLine.Width);
}
a.ctx.beginPath();
a.ctx.moveTo(x, y);
a.ctx.lineTo(x, (coords.Y2 - a.HighlightLine.Width));
a.ctx.lineWidth = a.HighlightLine.Width;
a.ctx.strokeStyle = a.HighlightLine.Colour;
a.ctx.stroke();
}
getClientCoords() {
const a = this;
if (a.ClientRectangle == null) {
return null;
}
return {
X1: a.ClientRectangle.X,
Y1: a.ClientRectangle.Y,
X2: (a.ClientRectangle.Width - a.ClientRectangle.X),
Y2: (a.ClientRectangle.Height - a.ClientRectangle.Y)
};
}
getClientRectangle() {
const a = this;
return {
X: a.Padding.Left,
Y: a.Padding.Top,
Width: (a.Size.Width - a.Padding.Right),
Height: (a.Size.Height - a.Padding.Bottom)
};
}
getXAxis() {
const a = this;
const coords = a.getClientCoords();
if (coords == null) {
return;
}
let result = [];
let x = coords.X1;
let date = a.stringToDate(a.dateToString(a.StartDate, "yyyy-MM-dd"));
// Rollback one day
date.setDate(date.getDate() - 1);
while (true) {
if (x >= coords.X2) {
break;
}
result.push({
Date: a.dateToString(date, "yyyy-MM-dd"),
X: x
});
x += (a.Axis.X.HourLineSpace * a.Axis.X.NoPartPerDay);
date.setDate(date.getDate() + 1);
}
return result;
}
half(value) {
return (value / 2);
}
measureText(value) {
const a = this;
const size = a.ctx.measureText(value);
return {
Width: size.width,
Height: size.fontBoundingBoxAscent,
OffsetLeft: a.half(size.width),
OffsetTop: a.half(size.fontBoundingBoxAscent)
};
}
dateToString(date, pattern) {
let result = pattern;
result = result.replace("fffffff", date.getMilliseconds().toString().padStart(7, '0'));
result = result.replace("ffffff", date.getMilliseconds().toString().padStart(6, '0'));
result = result.replace("fffff", date.getMilliseconds().toString().padStart(5, '0'));
result = result.replace("yyyy", date.getFullYear().toString().padStart(4, '0'));
result = result.replace("MMMM", "{1}");
result = result.replace("dddd", "{2}");
result = result.replace("ffff", date.getMilliseconds().toString().padStart(4, '0'));
result = result.replace("yyy", date.getFullYear().toString().padStart(3, '0'));
result = result.replace("MMM", "{3}");
result = result.replace("ddd", "{4}");
result = result.replace("fff", date.getMilliseconds().toString().padStart(3, '0'));
result = result.replace("zzz", "");
result = result.replace("yy", date.getFullYear().toString().slice(-2));
result = result.replace("MM", (date.getMonth() + 1).toString().padStart(2, '0'));
result = result.replace("dd", date.getDate().toString().padStart(2, '0'));
result = result.replace("HH", date.getHours().toString().padStart(2, '0'));
result = result.replace("hh", (date.getHours() > 12 ? (date.getHours() - 12) : date.getHours()).toString().padStart(2, '0'));
result = result.replace("mm", date.getMinutes().toString().padStart(2, '0'));
result = result.replace("ss", date.getSeconds().toString().padStart(2, '0'));
result = result.replace("ff", date.getMilliseconds().toString().padStart(2, '0'));
result = result.replace("tt", "{5}");
result = result.replace("zz", "");
result = result.replace("y", date.getFullYear().toString());
result = result.replace("M", (date.getMonth() + 1).toString());
result = result.replace("d", date.getDate().toString());
result = result.replace("H", date.getHours().toString());
result = result.replace("h", (date.getHours() > 12 ? (date.getHours() - 12) : date.getHours()).toString());
result = result.replace("m", date.getMinutes().toString());
result = result.replace("s", date.getSeconds().toString());
result = result.replace("z", "");
result = result.replace("t", "{6}");
result = result.replace("Z", "");
result = result.replace("{1}", date.toLocaleString('default', { month: 'long' }));
result = result.replace("{2}", date.toLocaleString('default', { weekday: 'long' }));
result = result.replace("{3}", date.toLocaleString('default', { month: 'short' }));
result = result.replace("{4}", date.toLocaleString('default', { weekday: 'short' }));
result = result.replace("{5}", (date.getHours() >= 12 ? "PM" : "AM"));
result = result.replace("{6}", (date.getHours() >= 12 ? "P" : "A"));
return result;
}
stringToDate(value) {
return new Date(Date.parse(value));
}
}