/** * 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.StartDate = new Date(); a.ClientRectangle = a.getClientRectangle(); a.ctx = a.Container.getContext("2d"); a.ctx.canvas.width = a.Size.Width; a.ctx.canvas.height = a.Size.Height; } Load(startDate) { const a = this; a.StartDate = ((typeof(startDate) == "undefined") ? new Date() : startDate); a.invalidate(); a.initialiseComponents(); } Clear(all) { const a = this; if (all) { a.ctx.clearRect(0, 0, a.ctx.canvas.width, a.ctx.canvas.height); } else { const rect = a.getChartCoords(); a.ctx.clearRect((rect.X1 + a.Axis.LineWidth), rect.Y1, (rect.X2 - rect.X1 - a.Axis.LineWidth), (rect.Y2 - rect.Y1 - a.Axis.LineWidth)); } } initialiseComponents() { const a = this; const coords = a.getChartCoords(); // Vertical highlight line a.ctx.canvas.addEventListener('mousemove', function (e) { a.Clear(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) { console.log('mousedown'); console.log(e); }); } invalidate() { const a = this; a.Clear(true); a.drawAxis(); a.drawXAxis(); a.drawXAxisLabels(); } drawAxis() { const a = this; const coords = a.getChartCoords(); 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.getChartCoords(); 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.getChartCoords(); 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.getChartCoords(); 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(); } 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) { const a = this; const coords = a.getChartCoords(); if (coords == null) { return; } a.ctx.beginPath(); a.ctx.moveTo(x, (coords.Y1 + a.HighlightLine.Width)); a.ctx.lineTo(x, (coords.Y2 - a.HighlightLine.Width)); a.ctx.lineWidth = a.HighlightLine.Width; a.ctx.strokeStyle = a.HighlightLine.Colour; a.ctx.stroke(); } getChartCoords() { 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.getChartCoords(); 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)); } }