From 8b0701b2e54b7c0d9855e43bb66b00038cb16dce Mon Sep 17 00:00:00 2001 From: Ray Date: Sat, 30 Sep 2023 23:45:55 +0100 Subject: [PATCH] Initial commit --- bbtimeline.css | 0 bbtimeline.js | 378 +++++++++++++++++++++++++++++++++++++++++++++++++ demo-test.html | 66 +++++++++ 3 files changed, 444 insertions(+) create mode 100644 bbtimeline.css create mode 100644 bbtimeline.js create mode 100644 demo-test.html diff --git a/bbtimeline.css b/bbtimeline.css new file mode 100644 index 0000000..e69de29 diff --git a/bbtimeline.js b/bbtimeline.js new file mode 100644 index 0000000..6160e18 --- /dev/null +++ b/bbtimeline.js @@ -0,0 +1,378 @@ +/** + * 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)); + } + +} \ No newline at end of file diff --git a/demo-test.html b/demo-test.html new file mode 100644 index 0000000..4f964b1 --- /dev/null +++ b/demo-test.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + +

+ + +

+ + + + + + + + \ No newline at end of file